|
@@ -6,11 +6,12 @@ import { TypeRegistry } from '@polkadot/types'
|
|
|
import { KeyringPair } from '@polkadot/keyring/types'
|
|
|
import { SubmittableExtrinsic, AugmentedEvent } from '@polkadot/api/types'
|
|
|
import { DispatchError, DispatchResult } from '@polkadot/types/interfaces/system'
|
|
|
-import { getTransactionNonce, resetTransactionNonceCache } from '../caching/transactionNonceKeeper'
|
|
|
import logger from '../../services/logger'
|
|
|
import ExitCodes from '../../command-base/ExitCodes'
|
|
|
import { CLIError } from '@oclif/errors'
|
|
|
import stringify from 'fast-safe-stringify'
|
|
|
+import sleep from 'sleep-promise'
|
|
|
+import AwaitLock from 'await-lock'
|
|
|
|
|
|
/**
|
|
|
* Dedicated error for the failed extrinsics.
|
|
@@ -29,27 +30,57 @@ export async function createApi(apiUrl: string): Promise<ApiPromise> {
|
|
|
|
|
|
const api = new ApiPromise({ provider, types })
|
|
|
await api.isReadyOrError
|
|
|
+ await untilChainIsSynced(api)
|
|
|
|
|
|
api.on('error', (err) => logger.error(`Api promise error: ${err.target?._url}`, { err }))
|
|
|
|
|
|
return api
|
|
|
}
|
|
|
|
|
|
+/**
|
|
|
+ * Awaits the chain to be fully synchronized.
|
|
|
+ */
|
|
|
+async function untilChainIsSynced(api: ApiPromise) {
|
|
|
+ logger.info('Waiting for chain to be synced before proceeding.')
|
|
|
+ while (true) {
|
|
|
+ const isSyncing = await chainIsSyncing(api)
|
|
|
+ if (isSyncing) {
|
|
|
+ logger.info('Still waiting for chain to be synced.')
|
|
|
+ await sleep(1 * 30 * 1000)
|
|
|
+ } else {
|
|
|
+ return
|
|
|
+ }
|
|
|
+ }
|
|
|
+}
|
|
|
+
|
|
|
+/**
|
|
|
+ * Checks the chain sync status.
|
|
|
+ *
|
|
|
+ * @param api api promise
|
|
|
+ * @returns
|
|
|
+ */
|
|
|
+async function chainIsSyncing(api: ApiPromise) {
|
|
|
+ const { isSyncing } = await api.rpc.system.health()
|
|
|
+ return isSyncing.isTrue
|
|
|
+}
|
|
|
+
|
|
|
+const lock = new AwaitLock()
|
|
|
+
|
|
|
/**
|
|
|
* Sends an extrinsic to the runtime and follows the result.
|
|
|
*
|
|
|
* @param api - API promise
|
|
|
* @param account - KeyPair instance
|
|
|
* @param tx - runtime transaction object to send
|
|
|
- * @param nonce - transaction nonce for a given account.
|
|
|
* @returns extrinsic result promise.
|
|
|
*/
|
|
|
-function sendExtrinsic(
|
|
|
+async function sendExtrinsic(
|
|
|
api: ApiPromise,
|
|
|
account: KeyringPair,
|
|
|
- tx: SubmittableExtrinsic<'promise'>,
|
|
|
- nonce: Index
|
|
|
+ tx: SubmittableExtrinsic<'promise'>
|
|
|
): Promise<ISubmittableResult> {
|
|
|
+ const nonce = await lockAndGetNonce(api, account)
|
|
|
+
|
|
|
return new Promise((resolve, reject) => {
|
|
|
let unsubscribe: () => void
|
|
|
tx.signAndSend(account, { nonce }, (result) => {
|
|
@@ -107,7 +138,9 @@ function sendExtrinsic(
|
|
|
)
|
|
|
}
|
|
|
})
|
|
|
- .then((unsubFunc) => (unsubscribe = unsubFunc))
|
|
|
+ .then((unsubFunc) => {
|
|
|
+ unsubscribe = unsubFunc
|
|
|
+ })
|
|
|
.catch((e) =>
|
|
|
reject(
|
|
|
new ExtrinsicFailedError(`Cannot send the extrinsic: ${e.message ? e.message : stringify(e)}`, {
|
|
@@ -115,9 +148,28 @@ function sendExtrinsic(
|
|
|
})
|
|
|
)
|
|
|
)
|
|
|
+ .finally(() => lock.release())
|
|
|
})
|
|
|
}
|
|
|
|
|
|
+/**
|
|
|
+ * Set the API lock and gets the last account nonce. It removes the lock on
|
|
|
+ * exception and rethrows the error.
|
|
|
+ *
|
|
|
+ * @param api runtime API promise
|
|
|
+ * @param account account to get the last nonce from.
|
|
|
+ * @returns
|
|
|
+ */
|
|
|
+async function lockAndGetNonce(api: ApiPromise, account: KeyringPair): Promise<Index> {
|
|
|
+ await lock.acquireAsync()
|
|
|
+ try {
|
|
|
+ return await api.rpc.system.accountNextIndex(account.address)
|
|
|
+ } catch (err) {
|
|
|
+ lock.release()
|
|
|
+ throw err
|
|
|
+ }
|
|
|
+}
|
|
|
+
|
|
|
/**
|
|
|
* Helper function for formatting dispatch error.
|
|
|
*
|
|
@@ -155,25 +207,18 @@ export async function sendAndFollowNamedTx<T>(
|
|
|
sudoCall = false,
|
|
|
eventParser: ((result: ISubmittableResult) => T) | null = null
|
|
|
): Promise<T | void> {
|
|
|
- try {
|
|
|
- logger.debug(`Sending ${tx.method.section}.${tx.method.method} extrinsic...`)
|
|
|
- if (sudoCall) {
|
|
|
- tx = api.tx.sudo.sudo(tx)
|
|
|
- }
|
|
|
- const nonce = await getTransactionNonce(api, account)
|
|
|
-
|
|
|
- const result = await sendExtrinsic(api, account, tx, nonce)
|
|
|
- let eventResult: T | void
|
|
|
- if (eventParser) {
|
|
|
- eventResult = eventParser(result)
|
|
|
- }
|
|
|
- logger.debug(`Extrinsic successful!`)
|
|
|
-
|
|
|
- return eventResult
|
|
|
- } catch (err) {
|
|
|
- await resetTransactionNonceCache()
|
|
|
- throw err
|
|
|
+ logger.debug(`Sending ${tx.method.section}.${tx.method.method} extrinsic...`)
|
|
|
+ if (sudoCall) {
|
|
|
+ tx = api.tx.sudo.sudo(tx)
|
|
|
}
|
|
|
+ const result = await sendExtrinsic(api, account, tx)
|
|
|
+ let eventResult: T | void
|
|
|
+ if (eventParser) {
|
|
|
+ eventResult = eventParser(result)
|
|
|
+ }
|
|
|
+ logger.debug(`Extrinsic successful!`)
|
|
|
+
|
|
|
+ return eventResult
|
|
|
}
|
|
|
|
|
|
/**
|