From 96812891914269731fa0efce585bcf4878d66ad2 Mon Sep 17 00:00:00 2001 From: michael1011 Date: Thu, 13 Feb 2025 09:40:01 +0100 Subject: [PATCH] feat: sequential EVM signer --- lib/db/models/PendingEthereumTransaction.ts | 5 ++ .../PendingEthereumTransactionRepository.ts | 10 +++ lib/wallet/ethereum/EthereumManager.ts | 5 +- lib/wallet/ethereum/SequentialSigner.ts | 61 +++++++++++++++++++ ...ndingEthereumTransactionRepository.spec.ts | 31 +++++++++- 5 files changed, 110 insertions(+), 2 deletions(-) create mode 100644 lib/wallet/ethereum/SequentialSigner.ts diff --git a/lib/db/models/PendingEthereumTransaction.ts b/lib/db/models/PendingEthereumTransaction.ts index 69c4e123c..30fa64550 100644 --- a/lib/db/models/PendingEthereumTransaction.ts +++ b/lib/db/models/PendingEthereumTransaction.ts @@ -10,6 +10,7 @@ class PendingEthereumTransaction implements PendingEthereumTransactionType { public hash!: string; + public etherAmount!: bigint; public nonce!: number; public static load = (sequelize: Sequelize): void => { @@ -20,6 +21,10 @@ class PendingEthereumTransaction primaryKey: true, allowNull: false, }, + etherAmount: { + type: new DataTypes.DECIMAL(), + allowNull: false, + }, nonce: { type: new DataTypes.INTEGER(), unique: true, diff --git a/lib/db/repositories/PendingEthereumTransactionRepository.ts b/lib/db/repositories/PendingEthereumTransactionRepository.ts index df663321a..825a65a2b 100644 --- a/lib/db/repositories/PendingEthereumTransactionRepository.ts +++ b/lib/db/repositories/PendingEthereumTransactionRepository.ts @@ -17,12 +17,22 @@ class PendingEthereumTransactionRepository { return nonce + 1; }; + public static getTotalSent = async (): Promise => { + return BigInt( + (await PendingEthereumTransaction.sum( + 'etherAmount', + )) ?? 0, + ); + }; + public static addTransaction = ( hash: string, + etherAmount: bigint, nonce: number, ): Promise => { return PendingEthereumTransaction.create({ hash, + etherAmount, nonce, }); }; diff --git a/lib/wallet/ethereum/EthereumManager.ts b/lib/wallet/ethereum/EthereumManager.ts index a2f6816eb..3aa8ceb02 100644 --- a/lib/wallet/ethereum/EthereumManager.ts +++ b/lib/wallet/ethereum/EthereumManager.ts @@ -20,6 +20,7 @@ import ConsolidatedEventHandler from './ConsolidatedEventHandler'; import EthereumTransactionTracker from './EthereumTransactionTracker'; import { Ethereum, NetworkDetails, Rsk } from './EvmNetworks'; import InjectedProvider from './InjectedProvider'; +import SequentialSigner from './SequentialSigner'; import Contracts from './contracts/Contracts'; type Network = { @@ -81,7 +82,9 @@ class EthereumManager { name: this.config.networkName || network.name, }; - this.signer = EthersWallet.fromPhrase(mnemonic).connect(this.provider); + this.signer = new SequentialSigner( + EthersWallet.fromPhrase(mnemonic), + ).connect(this.provider); this.address = await this.signer.getAddress(); this.logger.verbose( diff --git a/lib/wallet/ethereum/SequentialSigner.ts b/lib/wallet/ethereum/SequentialSigner.ts new file mode 100644 index 000000000..06ec4bf45 --- /dev/null +++ b/lib/wallet/ethereum/SequentialSigner.ts @@ -0,0 +1,61 @@ +import AsyncLock from 'async-lock'; +import { + AbstractSigner, + Provider, + Signer, + TransactionRequest, + TypedDataDomain, + TypedDataField, +} from 'ethers'; +import PendingEthereumTransactionRepository from '../../db/repositories/PendingEthereumTransactionRepository'; + +class SequentialSigner extends AbstractSigner { + private static readonly txLock = 'txLock'; + + private readonly lock = new AsyncLock(); + + constructor(private signer: AbstractSigner) { + super(); + } + + public getAddress = (): Promise => this.signer.getAddress(); + + public connect = (provider: null | Provider): Signer => { + this.signer = this.signer.connect(provider); + return this; + }; + + public signTransaction = async (tx: TransactionRequest): Promise => { + return await this.lock.acquire(SequentialSigner.txLock, async () => { + if (tx.value !== undefined && tx.value !== null) { + const [ourBalance, pendingTxsValue] = await Promise.all([ + this.signer.provider!.getBalance(await this.getAddress()), + PendingEthereumTransactionRepository.getTotalSent(), + ]); + + if (ourBalance - pendingTxsValue < BigInt(tx.value)) { + throw new Error('insufficient balance'); + } + } + + if (tx.nonce === undefined || tx.nonce === null) { + tx.nonce = await this.signer.provider!.getTransactionCount( + await this.getAddress(), + ); + } + + return await this.signer.signTransaction(tx); + }); + }; + + public signMessage = (message: string | Uint8Array): Promise => + this.signer.signMessage(message); + + public signTypedData = ( + domain: TypedDataDomain, + types: Record>, + value: Record, + ): Promise => this.signer.signTypedData(domain, types, value); +} + +export default SequentialSigner; diff --git a/test/integration/db/repositories/PendingEthereumTransactionRepository.spec.ts b/test/integration/db/repositories/PendingEthereumTransactionRepository.spec.ts index a55c6f5a2..7e8833899 100644 --- a/test/integration/db/repositories/PendingEthereumTransactionRepository.spec.ts +++ b/test/integration/db/repositories/PendingEthereumTransactionRepository.spec.ts @@ -27,11 +27,40 @@ describe('PendingEthereumTransactionRepository', () => { }); test('should get highest nonce when there are pending transactions', async () => { - await PendingEthereumTransactionRepository.addTransaction('txHash', 20); + await PendingEthereumTransactionRepository.addTransaction( + 'txHash', + 1n, + 20, + ); await expect( PendingEthereumTransactionRepository.getHighestNonce(), ).resolves.toEqual(21); }); }); + + describe('getTotalSent', () => { + test('should get total sent when there are no pending transactions', async () => { + await expect( + PendingEthereumTransactionRepository.getTotalSent(), + ).resolves.toEqual(BigInt(0)); + }); + + test('should get total sent when there are pending transactions', async () => { + await PendingEthereumTransactionRepository.addTransaction( + 'txHash', + 21n, + 20, + ); + await PendingEthereumTransactionRepository.addTransaction( + 'txHash2', + 21n, + 21, + ); + + await expect( + PendingEthereumTransactionRepository.getTotalSent(), + ).resolves.toEqual(BigInt(42)); + }); + }); });