api.ts 6.0 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166
  1. import { types } from '@joystream/types/'
  2. import { ApiPromise, WsProvider, SubmittableResult } from '@polkadot/api'
  3. import { SubmittableExtrinsic, AugmentedEvent } from '@polkadot/api/types'
  4. import { KeyringPair } from '@polkadot/keyring/types'
  5. import { Balance, Call } from '@polkadot/types/interfaces'
  6. import { formatBalance } from '@polkadot/util'
  7. import { IEvent } from '@polkadot/types/types'
  8. import { DispatchError } from '@polkadot/types/interfaces/system'
  9. import { LoggingService } from '../../logging'
  10. import { Logger } from 'winston'
  11. export class ExtrinsicFailedError extends Error {}
  12. export class RuntimeApi {
  13. private _api: ApiPromise
  14. private logger: Logger
  15. public isDevelopment = false
  16. private constructor(logging: LoggingService, originalApi: ApiPromise, isDevelopment: boolean) {
  17. this.isDevelopment = isDevelopment
  18. this.logger = logging.createLogger('SubstrateApi')
  19. this._api = originalApi
  20. }
  21. static async create(
  22. logging: LoggingService,
  23. apiUri: string,
  24. metadataCache?: Record<string, string>
  25. ): Promise<RuntimeApi> {
  26. const { api, chainType } = await RuntimeApi.initApi(apiUri, metadataCache)
  27. return new RuntimeApi(logging, api, chainType.isDevelopment || chainType.isLocal)
  28. }
  29. private static async initApi(apiUri: string, metadataCache?: Record<string, string>) {
  30. const wsProvider: WsProvider = new WsProvider(apiUri)
  31. const api = await ApiPromise.create({ provider: wsProvider, types, metadata: metadataCache })
  32. // Initializing some api params based on pioneer/packages/react-api/Api.tsx
  33. const [properties, chainType] = await Promise.all([api.rpc.system.properties(), api.rpc.system.chainType()])
  34. const tokenSymbol = properties.tokenSymbol.unwrap()[0].toString()
  35. const tokenDecimals = properties.tokenDecimals.unwrap()[0].toNumber()
  36. // formatBlanace config
  37. formatBalance.setDefaults({
  38. decimals: tokenDecimals,
  39. unit: tokenSymbol,
  40. })
  41. return { api, properties, chainType }
  42. }
  43. public get query(): ApiPromise['query'] {
  44. return this._api.query
  45. }
  46. public get tx(): ApiPromise['tx'] {
  47. return this._api.tx
  48. }
  49. public get consts(): ApiPromise['consts'] {
  50. return this._api.consts
  51. }
  52. public get derive(): ApiPromise['derive'] {
  53. return this._api.derive
  54. }
  55. public get createType(): ApiPromise['createType'] {
  56. return this._api.createType.bind(this._api)
  57. }
  58. public sudo(tx: SubmittableExtrinsic<'promise'>): SubmittableExtrinsic<'promise'> {
  59. return this._api.tx.sudo.sudo(tx)
  60. }
  61. public async estimateFee(account: KeyringPair, tx: SubmittableExtrinsic<'promise'>): Promise<Balance> {
  62. const paymentInfo = await tx.paymentInfo(account)
  63. return paymentInfo.partialFee
  64. }
  65. public findEvent<
  66. S extends keyof ApiPromise['events'] & string,
  67. M extends keyof ApiPromise['events'][S] & string,
  68. EventType = ApiPromise['events'][S][M] extends AugmentedEvent<'promise', infer T> ? IEvent<T> : never
  69. >(result: SubmittableResult, section: S, method: M): EventType | undefined {
  70. return result.findRecord(section, method)?.event as EventType | undefined
  71. }
  72. public getEvent<
  73. S extends keyof ApiPromise['events'] & string,
  74. M extends keyof ApiPromise['events'][S] & string,
  75. EventType = ApiPromise['events'][S][M] extends AugmentedEvent<'promise', infer T> ? IEvent<T> : never
  76. >(result: SubmittableResult, section: S, method: M): EventType {
  77. const event = this.findEvent(result, section, method)
  78. if (!event) {
  79. throw new Error(`Cannot find expected ${section}.${method} event in result: ${result.toHuman()}`)
  80. }
  81. return (event as unknown) as EventType
  82. }
  83. private formatDispatchError(err: DispatchError): string {
  84. try {
  85. const { name, docs } = this._api.registry.findMetaError(err.asModule)
  86. return `${name} (${docs.join(', ')})`
  87. } catch (e) {
  88. return err.toString()
  89. }
  90. }
  91. sendExtrinsic(keyPair: KeyringPair, tx: SubmittableExtrinsic<'promise'>): Promise<SubmittableResult> {
  92. let txName = `${tx.method.section}.${tx.method.method}`
  93. if (txName === 'sudo.sudo') {
  94. const innerCall = tx.args[0] as Call
  95. txName = `sudo.sudo(${innerCall.section}.${innerCall.method})`
  96. }
  97. this.logger.info(`Sending ${txName} extrinsic from ${keyPair.address}`)
  98. return new Promise((resolve, reject) => {
  99. let unsubscribe: () => void
  100. tx.signAndSend(keyPair, {}, (result) => {
  101. // Implementation loosely based on /pioneer/packages/react-signer/src/Modal.tsx
  102. if (!result || !result.status) {
  103. return
  104. }
  105. if (result.status.isInBlock) {
  106. unsubscribe()
  107. result.events
  108. .filter(({ event }) => event.section === 'system')
  109. .forEach(({ event }) => {
  110. if (event.method === 'ExtrinsicFailed') {
  111. const dispatchError = event.data[0] as DispatchError
  112. reject(
  113. new ExtrinsicFailedError(`Extrinsic execution error: ${this.formatDispatchError(dispatchError)}`)
  114. )
  115. } else if (event.method === 'ExtrinsicSuccess') {
  116. const sudidEvent = this.findEvent(result, 'sudo', 'Sudid')
  117. if (sudidEvent) {
  118. const [dispatchResult] = sudidEvent.data
  119. if (dispatchResult.isOk) {
  120. resolve(result)
  121. } else {
  122. reject(
  123. new ExtrinsicFailedError(
  124. `Sudo extrinsic execution error! ${this.formatDispatchError(dispatchResult.asErr)}`
  125. )
  126. )
  127. }
  128. } else {
  129. resolve(result)
  130. }
  131. }
  132. })
  133. } else if (result.isError) {
  134. reject(new ExtrinsicFailedError('Extrinsic execution error!'))
  135. }
  136. })
  137. .then((unsubFunc) => (unsubscribe = unsubFunc))
  138. .catch((e) =>
  139. reject(new ExtrinsicFailedError(`Cannot send the extrinsic: ${e.message ? e.message : JSON.stringify(e)}`))
  140. )
  141. })
  142. }
  143. }