sender.ts 5.1 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137
  1. import { ApiPromise, Keyring } from '@polkadot/api'
  2. import { SubmittableExtrinsic } from '@polkadot/api/types'
  3. import { ISubmittableResult, AnyJson } from '@polkadot/types/types/'
  4. import { AccountId, EventRecord } from '@polkadot/types/interfaces'
  5. import { DispatchError, DispatchResult } from '@polkadot/types/interfaces/system'
  6. import { TypeRegistry } from '@polkadot/types'
  7. import { KeyringPair } from '@polkadot/keyring/types'
  8. import { Debugger, extendDebug } from './Debugger'
  9. import AsyncLock from 'async-lock'
  10. import { assert } from 'chai'
  11. export enum LogLevel {
  12. None,
  13. Debug,
  14. Verbose,
  15. }
  16. export class Sender {
  17. private readonly api: ApiPromise
  18. private static readonly asyncLock: AsyncLock = new AsyncLock()
  19. private readonly keyring: Keyring
  20. private readonly debug: Debugger.Debugger
  21. private logs: LogLevel = LogLevel.None
  22. private static instance = 0
  23. constructor(api: ApiPromise, keyring: Keyring, label: string) {
  24. this.api = api
  25. this.keyring = keyring
  26. this.debug = extendDebug(`sender:${Sender.instance++}:${label}`)
  27. }
  28. // Synchronize all sending of transactions into mempool, so we can always safely read
  29. // the next account nonce taking mempool into account. This is safe as long as all sending of transactions
  30. // from same account occurs in the same process. Returns a promise of the Extrinsic Dispatch Result ISubmittableResult.
  31. // The promise resolves on tx finalization (For both Dispatch success and failure)
  32. // The promise is rejected if transaction is rejected by node.
  33. public setLogLevel(level: LogLevel): void {
  34. this.logs = level
  35. }
  36. public async signAndSend(
  37. tx: SubmittableExtrinsic<'promise'>,
  38. account: AccountId | string
  39. ): Promise<ISubmittableResult> {
  40. const addr = this.keyring.encodeAddress(account)
  41. const senderKeyPair: KeyringPair = this.keyring.getPair(addr)
  42. let finalized: { (result: ISubmittableResult): void }
  43. const whenFinalized: Promise<ISubmittableResult> = new Promise(async (resolve, reject) => {
  44. finalized = resolve
  45. })
  46. // saved human representation of the signed tx, will be set before it is submitted.
  47. // On error it is logged to help in debugging.
  48. let sentTx: AnyJson
  49. const handleEvents = (result: ISubmittableResult) => {
  50. if (result.status.isFuture) {
  51. // Its virtually impossible for us to continue with tests
  52. // when this occurs and we don't expect the tests to handle this correctly
  53. // so just abort!
  54. console.error('Future Tx, aborting!')
  55. process.exit(-1)
  56. }
  57. if (!result.status.isInBlock) {
  58. return
  59. }
  60. const success = result.findRecord('system', 'ExtrinsicSuccess')
  61. const failed = result.findRecord('system', 'ExtrinsicFailed')
  62. // Log failed transactions
  63. if (this.logs === LogLevel.Debug || this.logs === LogLevel.Verbose) {
  64. if (failed) {
  65. const record = failed as EventRecord
  66. assert(record)
  67. const {
  68. event: { data },
  69. } = record
  70. const err = data[0] as DispatchError
  71. if (err.isModule) {
  72. const { name } = (this.api.registry as TypeRegistry).findMetaError(err.asModule)
  73. this.debug('Dispatch Error:', name, sentTx)
  74. } else {
  75. this.debug('Dispatch Error:', sentTx)
  76. }
  77. } else {
  78. assert(success)
  79. const sudid = result.findRecord('sudo', 'Sudid')
  80. if (sudid) {
  81. const dispatchResult = sudid.event.data[0] as DispatchResult
  82. assert(dispatchResult)
  83. if (dispatchResult.isError) {
  84. const err = dispatchResult.asError
  85. if (err.isModule) {
  86. const { name } = (this.api.registry as TypeRegistry).findMetaError(err.asModule)
  87. this.debug('Sudo Dispatch Failed', name, sentTx)
  88. } else {
  89. this.debug('Sudo Dispatch Failed', sentTx)
  90. }
  91. }
  92. }
  93. }
  94. }
  95. // Always resolve irrespective of success or failure. Error handling should
  96. // be dealt with by caller.
  97. if (success || failed) finalized(result)
  98. }
  99. // We used to do this: Sender.asyncLock.acquire(`${senderKeyPair.address}` ...
  100. // Instead use a single lock for all calls, to force all transactions to be submitted in same order
  101. // of call to signAndSend. Otherwise it raises chance of race conditions.
  102. // It happens in rare cases and has lead some tests to fail occasionally in the past
  103. await Sender.asyncLock.acquire('tx-queue', async () => {
  104. const nonce = await this.api.rpc.system.accountNextIndex(senderKeyPair.address)
  105. const signedTx = tx.sign(senderKeyPair, { nonce })
  106. sentTx = signedTx.toHuman()
  107. const { method, section } = signedTx.method
  108. try {
  109. await signedTx.send(handleEvents)
  110. if (this.logs === LogLevel.Verbose) {
  111. this.debug('Submitted tx:', `${section}.${method}`)
  112. }
  113. } catch (err) {
  114. if (this.logs === LogLevel.Debug) {
  115. this.debug('Submitting tx failed:', sentTx, err)
  116. }
  117. throw err
  118. }
  119. })
  120. return whenFinalized
  121. }
  122. }