UNPKG

evm-blockchain-tools

Version:

This is a collection of resuseable tools to support development for EVM-powered blockchains

183 lines (157 loc) 5.28 kB
import pino from "pino"; import { TaskQueue, timeoutHelper, workflows } from "mvc-common-toolkit"; import { BigNumber, Signer } from "ethers"; import { TransactionHistoryStorage, IWeb3Gateway, SignerPicker, } from "../common/interfaces"; import { ContractModel } from "../models"; import { AdditionalGas, MinGas, TransactionStatus } from "../common/constants"; import { RoundRobinSignerPicker } from "../services"; import { getOptimizedGasPriceV2 } from "../utils"; export class BlockchainTransactionRegistry { protected _providerMap: Map<string, IWeb3Gateway>; private logger = pino(); protected signerPicker: SignerPicker; constructor( protected transactionStorage: TransactionHistoryStorage, signerPicker: SignerPicker, protected taskQueue: TaskQueue ) { this.signerPicker = signerPicker || new RoundRobinSignerPicker(transactionStorage); } /** * This function based on some assumptions: * - The user of this class already implemented concurrency control * - The providers are only used programmatically, with no ad-hoc transactions * @param contract * @param method * @param params * @param options */ public async sendContractTransaction( contract: ContractModel, method: string, params: any[], options?: { signerPicker?: SignerPicker; timeoutInMs?: number; minGas?: string; } ) { const signerPicker = options?.signerPicker || this.signerPicker; const pickedSigner = await signerPicker.pick(contract.address, []); const signerAddress = await pickedSigner.getAddress(); const queueName = `signer_queue_${signerAddress.toLowerCase()}`; const taskName = `exec_${method}`; // This makes sure parallelism for all signers not having to queue up return this.taskQueue.push(queueName, taskName, async () => { let generatedTxHash: string; try { const { isOverride, nextNonce } = await this.reconcileSignerLastTx( pickedSigner, signerAddress ); const gasPrice = await getOptimizedGasPriceV2( pickedSigner.provider, AdditionalGas.toString(), options?.minGas || MinGas.toString(), { useOverridingGas: isOverride, } ); const { signedTransaction, preCalculatedHash: txHash } = await contract.generateTransaction( { data: params, functionName: method, nonce: nextNonce, }, { gasPrice, } ); await this.transactionStorage.create({ nonce: nextNonce, txHash, signedTransaction, signerAddress, status: TransactionStatus.SCHEDULED, metadata: { isOverride, }, }); generatedTxHash = txHash; if (!contract.provider.sendTransaction) { throw new Error("provider has no sendTransaction method"); } await timeoutHelper.runWithTimeout(async () => { const submittedTx = await pickedSigner.provider.sendTransaction( signedTransaction ); await submittedTx.wait(1); await this.transactionStorage.updateByTxHash(txHash, { status: "executed", }); }, options?.timeoutInMs || 30000); } catch (error) { this.logger.error(error.message, error.stack); if (generatedTxHash) { await this.transactionStorage.updateByTxHash(generatedTxHash, { status: TransactionStatus.FAILED, metadata: error, }); } } }); } /** * Check if last tx was actually failed, if so then update its status to EXECUTED. * Edge case: The overriding tx failed, but the original timeout tx actually succeeded. * In this scenario, * @param signer The picked signer * @param signerAddress The signer address */ protected async reconcileSignerLastTx( signer: Signer, signerAddress: string ): Promise<{ nextNonce: string; isOverride: boolean }> { // Notice: This function should sort transactions nonce DESC and status ASC (so EXECUTED comes before FAILED and SCHEDULED) const lastTx = await this.transactionStorage.findSignerLastTransaction( signerAddress ); if (!lastTx) { const count = await signer.getTransactionCount(); return { nextNonce: String(count), isOverride: false, }; } const { txHash, status, nonce } = lastTx; if (status === TransactionStatus.SCHEDULED) { throw new Error( `txHash ${txHash} for signer ${signerAddress} is still pending` ); } if (status === TransactionStatus.FAILED) { const foundTx = await signer.provider.getTransaction(txHash); /** * Last tx was not found, meaning it hasn't been accepted. This doesn't mean the transaction * will not be picked, but we can TRY to override it. */ if (!foundTx) { return { nextNonce: nonce, isOverride: true, }; } } const nextNonce = BigNumber.from(nonce).add(1).toString(); return { nextNonce, isOverride: false, }; } }