api.ts 8.4 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263
  1. import { ApiPromise, WsProvider, SubmittableResult } from '@polkadot/api'
  2. import type { Index } from '@polkadot/types/interfaces/runtime'
  3. import { ISubmittableResult, IEvent } from '@polkadot/types/types'
  4. import { types } from '@joystream/types/'
  5. import { TypeRegistry } from '@polkadot/types'
  6. import { KeyringPair } from '@polkadot/keyring/types'
  7. import { SubmittableExtrinsic, AugmentedEvent } from '@polkadot/api/types'
  8. import { DispatchError, DispatchResult } from '@polkadot/types/interfaces/system'
  9. import logger from '../../services/logger'
  10. import ExitCodes from '../../command-base/ExitCodes'
  11. import { CLIError } from '@oclif/errors'
  12. import stringify from 'fast-safe-stringify'
  13. import sleep from 'sleep-promise'
  14. import AwaitLock from 'await-lock'
  15. /**
  16. * Dedicated error for the failed extrinsics.
  17. */
  18. export class ExtrinsicFailedError extends CLIError {}
  19. /**
  20. * Initializes the runtime API and Joystream runtime types.
  21. *
  22. * @param apiUrl - API URL string
  23. * @returns runtime API promise
  24. */
  25. export async function createApi(apiUrl: string): Promise<ApiPromise> {
  26. const provider = new WsProvider(apiUrl)
  27. provider.on('error', (err) => logger.error(`Api provider error: ${err.target?._url}`, { err }))
  28. const api = new ApiPromise({ provider, types })
  29. await api.isReadyOrError
  30. await untilChainIsSynced(api)
  31. api.on('error', (err) => logger.error(`Api promise error: ${err.target?._url}`, { err }))
  32. return api
  33. }
  34. /**
  35. * Awaits the chain to be fully synchronized.
  36. */
  37. async function untilChainIsSynced(api: ApiPromise) {
  38. logger.info('Waiting for chain to be synced before proceeding.')
  39. while (true) {
  40. const isSyncing = await chainIsSyncing(api)
  41. if (isSyncing) {
  42. logger.info('Still waiting for chain to be synced.')
  43. await sleep(1 * 30 * 1000)
  44. } else {
  45. return
  46. }
  47. }
  48. }
  49. /**
  50. * Checks the chain sync status.
  51. *
  52. * @param api api promise
  53. * @returns
  54. */
  55. async function chainIsSyncing(api: ApiPromise) {
  56. const { isSyncing } = await api.rpc.system.health()
  57. return isSyncing.isTrue
  58. }
  59. const lock = new AwaitLock()
  60. /**
  61. * Sends an extrinsic to the runtime and follows the result.
  62. *
  63. * @param api - API promise
  64. * @param account - KeyPair instance
  65. * @param tx - runtime transaction object to send
  66. * @returns extrinsic result promise.
  67. */
  68. async function sendExtrinsic(
  69. api: ApiPromise,
  70. account: KeyringPair,
  71. tx: SubmittableExtrinsic<'promise'>
  72. ): Promise<ISubmittableResult> {
  73. const nonce = await lockAndGetNonce(api, account)
  74. return new Promise((resolve, reject) => {
  75. let unsubscribe: () => void
  76. tx.signAndSend(account, { nonce }, (result) => {
  77. // Implementation loosely based on /pioneer/packages/react-signer/src/Modal.tsx
  78. if (!result || !result.status) {
  79. return
  80. }
  81. if (result.status.isInBlock) {
  82. unsubscribe()
  83. result.events
  84. .filter(({ event }) => event.section === 'system')
  85. .forEach(({ event }) => {
  86. if (event.method === 'ExtrinsicFailed') {
  87. const dispatchError = event.data[0] as DispatchError
  88. let errorMsg = dispatchError.toString()
  89. if (dispatchError.isModule) {
  90. try {
  91. errorMsg = formatDispatchError(api, dispatchError)
  92. } catch (e) {
  93. // This probably means we don't have this error in the metadata
  94. // In this case - continue (we'll just display dispatchError.toString())
  95. }
  96. }
  97. reject(
  98. new ExtrinsicFailedError(`Extrinsic execution error: ${errorMsg}`, {
  99. exit: ExitCodes.ApiError,
  100. })
  101. )
  102. } else if (event.method === 'ExtrinsicSuccess') {
  103. const sudid = result.findRecord('sudo', 'Sudid')
  104. if (sudid) {
  105. const dispatchResult = sudid.event.data[0] as DispatchResult
  106. if (dispatchResult.isOk) {
  107. resolve(result)
  108. } else {
  109. const errorMsg = formatDispatchError(api, dispatchResult.asErr)
  110. reject(
  111. new ExtrinsicFailedError(`Sudo extrinsic execution error! ${errorMsg}`, {
  112. exit: ExitCodes.ApiError,
  113. })
  114. )
  115. }
  116. } else {
  117. resolve(result)
  118. }
  119. }
  120. })
  121. } else if (result.isError) {
  122. reject(
  123. new ExtrinsicFailedError('Extrinsic execution error!', {
  124. exit: ExitCodes.ApiError,
  125. })
  126. )
  127. }
  128. })
  129. .then((unsubFunc) => {
  130. unsubscribe = unsubFunc
  131. })
  132. .catch((e) =>
  133. reject(
  134. new ExtrinsicFailedError(`Cannot send the extrinsic: ${e.message ? e.message : stringify(e)}`, {
  135. exit: ExitCodes.ApiError,
  136. })
  137. )
  138. )
  139. .finally(() => lock.release())
  140. })
  141. }
  142. /**
  143. * Set the API lock and gets the last account nonce. It removes the lock on
  144. * exception and rethrows the error.
  145. *
  146. * @param api runtime API promise
  147. * @param account account to get the last nonce from.
  148. * @returns
  149. */
  150. async function lockAndGetNonce(api: ApiPromise, account: KeyringPair): Promise<Index> {
  151. await lock.acquireAsync()
  152. try {
  153. return await api.rpc.system.accountNextIndex(account.address)
  154. } catch (err) {
  155. lock.release()
  156. throw err
  157. }
  158. }
  159. /**
  160. * Helper function for formatting dispatch error.
  161. *
  162. * @param api - API promise
  163. * @param error - DispatchError instance
  164. * @returns error string.
  165. */
  166. function formatDispatchError(api: ApiPromise, error: DispatchError): string {
  167. // Need to assert that registry is of TypeRegistry type, since Registry intefrace
  168. // seems outdated and doesn't include DispatchErrorModule as possible argument for "findMetaError"
  169. const typeRegistry = api.registry as TypeRegistry
  170. const { name, docs } = typeRegistry.findMetaError(error.asModule)
  171. const errorMsg = `${name} (${docs.join(', ')})`
  172. return errorMsg
  173. }
  174. /**
  175. * Helper function for sending an extrinsic to the runtime. It constructs an
  176. * actual transaction object.
  177. *
  178. * @param api - API promise
  179. * @param account - KeyPair instance
  180. * @param tx - prepared extrinsic with arguments
  181. * @param sudoCall - defines whether the transaction call should be wrapped in
  182. * the sudo call (false by default).
  183. * @param eventParser - defines event parsing function (null by default) for
  184. * getting any information from the successful extrinsic events.
  185. * @returns void or event parsing result promise.
  186. */
  187. export async function sendAndFollowNamedTx<T>(
  188. api: ApiPromise,
  189. account: KeyringPair,
  190. tx: SubmittableExtrinsic<'promise'>,
  191. sudoCall = false,
  192. eventParser: ((result: ISubmittableResult) => T) | null = null
  193. ): Promise<T | void> {
  194. logger.debug(`Sending ${tx.method.section}.${tx.method.method} extrinsic...`)
  195. if (sudoCall) {
  196. tx = api.tx.sudo.sudo(tx)
  197. }
  198. const result = await sendExtrinsic(api, account, tx)
  199. let eventResult: T | void
  200. if (eventParser) {
  201. eventResult = eventParser(result)
  202. }
  203. logger.debug(`Extrinsic successful!`)
  204. return eventResult
  205. }
  206. /**
  207. * Helper function for sending an extrinsic to the runtime. It constructs an
  208. * actual transaction object and sends a transactions wrapped in sudo call.
  209. *
  210. * @param api - API promise
  211. * @param account - KeyPair instance
  212. * @param tx - prepared extrinsic with arguments
  213. * @param eventParser - defines event parsing function (null by default) for
  214. * getting any information from the successful extrinsic events.
  215. * @returns void promise.
  216. */
  217. export async function sendAndFollowSudoNamedTx<T>(
  218. api: ApiPromise,
  219. account: KeyringPair,
  220. tx: SubmittableExtrinsic<'promise'>,
  221. eventParser: ((result: ISubmittableResult) => T) | null = null
  222. ): Promise<T | void> {
  223. return sendAndFollowNamedTx(api, account, tx, true, eventParser)
  224. }
  225. /**
  226. * Helper function for parsing the successful extrinsic result for event.
  227. *
  228. * @param result - extrinsic result
  229. * @param section - pallet name
  230. * @param eventName - event name
  231. * @returns void promise.
  232. */
  233. export function getEvent<
  234. S extends keyof ApiPromise['events'] & string,
  235. M extends keyof ApiPromise['events'][S] & string,
  236. EventType = ApiPromise['events'][S][M] extends AugmentedEvent<'promise', infer T> ? IEvent<T> : never
  237. >(result: SubmittableResult, section: S, eventName: M): EventType {
  238. const event = result.findRecord(section, eventName)?.event as EventType | undefined
  239. if (!event) {
  240. throw new ExtrinsicFailedError(`Cannot find expected ${section}.${eventName} event in result: ${result.toHuman()}`)
  241. }
  242. return event as EventType
  243. }