api.ts 5.4 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145
  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 } 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, any>
  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, any>) {
  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. sendExtrinsic(keyPair: KeyringPair, tx: SubmittableExtrinsic<'promise'>): Promise<SubmittableResult> {
  84. this.logger.info(`Sending ${tx.method.section}.${tx.method.method} extrinsic from ${keyPair.address}`)
  85. return new Promise((resolve, reject) => {
  86. let unsubscribe: () => void
  87. tx.signAndSend(keyPair, {}, (result) => {
  88. // Implementation loosely based on /pioneer/packages/react-signer/src/Modal.tsx
  89. if (!result || !result.status) {
  90. return
  91. }
  92. if (result.status.isInBlock) {
  93. unsubscribe()
  94. result.events
  95. .filter(({ event }) => event.section === 'system')
  96. .forEach(({ event }) => {
  97. if (event.method === 'ExtrinsicFailed') {
  98. const dispatchError = event.data[0] as DispatchError
  99. let errorMsg = dispatchError.toString()
  100. if (dispatchError.isModule) {
  101. try {
  102. const { name, documentation } = this._api.registry.findMetaError(dispatchError.asModule)
  103. errorMsg = `${name} (${documentation})`
  104. } catch (e) {
  105. // This probably means we don't have this error in the metadata
  106. // In this case - continue (we'll just display dispatchError.toString())
  107. }
  108. }
  109. reject(new ExtrinsicFailedError(`Extrinsic execution error: ${errorMsg}`))
  110. } else if (event.method === 'ExtrinsicSuccess') {
  111. resolve(result)
  112. }
  113. })
  114. } else if (result.isError) {
  115. reject(new ExtrinsicFailedError('Extrinsic execution error!'))
  116. }
  117. })
  118. .then((unsubFunc) => (unsubscribe = unsubFunc))
  119. .catch((e) =>
  120. reject(new ExtrinsicFailedError(`Cannot send the extrinsic: ${e.message ? e.message : JSON.stringify(e)}`))
  121. )
  122. })
  123. }
  124. }