diff --git a/.changeset/funny-pugs-applaud.md b/.changeset/funny-pugs-applaud.md new file mode 100644 index 00000000..18623ba9 --- /dev/null +++ b/.changeset/funny-pugs-applaud.md @@ -0,0 +1,7 @@ +--- +"@delvtech/evm-client-ethers": patch +"@delvtech/evm-client-viem": patch +"@delvtech/evm-client": patch +--- + +Add `waitForTransaction` method to `Network` type. diff --git a/packages/evm-client-ethers/src/network/createNetwork.ts b/packages/evm-client-ethers/src/network/createNetwork.ts index 2ff6e78a..0934f4ad 100644 --- a/packages/evm-client-ethers/src/network/createNetwork.ts +++ b/packages/evm-client-ethers/src/network/createNetwork.ts @@ -60,5 +60,30 @@ export function createNetwork(provider: Provider): Network { transactionIndex: index, }; }, + + async waitForTransaction(hash, options) { + const transaction = await provider.waitForTransaction( + hash, + undefined, + options?.timeout, + ); + + if (!transaction) { + return; + } + + return { + blockHash: transaction.blockHash as `0x${string}`, + blockNumber: BigInt(transaction.blockNumber), + from: transaction.from as `0x${string}`, + to: transaction.to as `0x${string}` | null, + cumulativeGasUsed: BigInt(transaction.cumulativeGasUsed), + gasUsed: BigInt(transaction.gasUsed), + logsBloom: transaction.logsBloom as `0x${string}`, + transactionHash: transaction.hash as `0x${string}`, + transactionIndex: transaction.index, + effectiveGasPrice: BigInt(transaction.gasPrice), + }; + }, }; } diff --git a/packages/evm-client-viem/src/network/createNetwork.ts b/packages/evm-client-viem/src/network/createNetwork.ts index d86cf5e2..78e84e18 100644 --- a/packages/evm-client-viem/src/network/createNetwork.ts +++ b/packages/evm-client-viem/src/network/createNetwork.ts @@ -47,5 +47,12 @@ export function createNetwork(publicClient: PublicClient): Network { transactionIndex: transactionIndex ?? undefined, }; }, + + async waitForTransaction(hash, options) { + return await publicClient.waitForTransactionReceipt({ + hash, + timeout: options?.timeout, + }); + }, }; } diff --git a/packages/evm-client/src/network/stubs/NetworkStub.test.ts b/packages/evm-client/src/network/stubs/NetworkStub.test.ts new file mode 100644 index 00000000..7f2bc0f5 --- /dev/null +++ b/packages/evm-client/src/network/stubs/NetworkStub.test.ts @@ -0,0 +1,47 @@ +import { + NetworkStub, + transactionToReceipt, +} from 'src/network/stubs/NetworkStub'; +import { describe, expect, it } from 'vitest'; + +describe('NetworkStub', () => { + it('waits for stubbed transactions', async () => { + const network = new NetworkStub(); + + const txHash = '0x123abc'; + const stubbedTx = { + gas: 100n, + gasPrice: 100n, + input: '0x456def', + nonce: 0, + type: '0x0', + value: 0n, + } as const; + + const waitPromise = network.waitForTransaction(txHash); + + await new Promise((resolve) => { + setTimeout(() => { + network.stubGetTransaction({ + args: [txHash], + value: stubbedTx, + }); + resolve(undefined); + }, 1000); + }); + + const tx = await waitPromise; + + expect(tx).toEqual(transactionToReceipt(stubbedTx)); + }); + + it('reaches timeout when waiting for transactions that are never stubbed', async () => { + const network = new NetworkStub(); + + const waitPromise = await network.waitForTransaction('0x123abc', { + timeout: 1000, + }); + + expect(waitPromise).toBe(undefined); + }); +}); diff --git a/packages/evm-client/src/network/stubs/NetworkStub.ts b/packages/evm-client/src/network/stubs/NetworkStub.ts index fbdd36a5..034062d8 100644 --- a/packages/evm-client/src/network/stubs/NetworkStub.ts +++ b/packages/evm-client/src/network/stubs/NetworkStub.ts @@ -4,8 +4,9 @@ import { Network, NetworkGetBlockArgs, NetworkGetTransactionArgs, + NetworkWaitForTransactionArgs, } from 'src/network/types/Network'; -import { Transaction } from 'src/network/types/Transaction'; +import { Transaction, TransactionReceipt } from 'src/network/types/Transaction'; /** * A mock implementation of a `Network` designed to facilitate unit @@ -78,4 +79,48 @@ export class NetworkStub implements Network { } return this.getTransactionStub(args); } + + async waitForTransaction( + ...[hash, { timeout = 60_000 } = {}]: NetworkWaitForTransactionArgs + ): Promise { + return new Promise(async (resolve) => { + let transaction: Transaction | undefined; + + transaction = await this.getTransactionStub?.([hash]).catch(); + + if (transaction) { + return resolve(transactionToReceipt(transaction)); + } + + // Poll for the transaction until it's found or the timeout is reached + let waitedTime = 0; + const interval = setInterval(async () => { + waitedTime += 1000; + transaction = await this.getTransactionStub?.([hash]).catch(); + if (transaction || waitedTime >= timeout) { + clearInterval(interval); + resolve(transactionToReceipt(transaction)); + } + }, 1000); + }); + } +} + +export function transactionToReceipt( + transaction: Transaction | undefined, +): TransactionReceipt | undefined { + return transaction + ? { + blockHash: transaction.blockHash!, + blockNumber: transaction.blockNumber!, + from: transaction.from!, + to: transaction.to!, + transactionIndex: transaction.transactionIndex!, + cumulativeGasUsed: 0n, + effectiveGasPrice: 0n, + transactionHash: transaction.hash!, + gasUsed: 0n, + logsBloom: '0x', + } + : undefined; } diff --git a/packages/evm-client/src/network/types/Network.ts b/packages/evm-client/src/network/types/Network.ts index d957f04d..1cec25fc 100644 --- a/packages/evm-client/src/network/types/Network.ts +++ b/packages/evm-client/src/network/types/Network.ts @@ -1,5 +1,5 @@ import { Block, BlockTag } from 'src/network/types/Block'; -import { Transaction } from 'src/network/types/Transaction'; +import { Transaction, TransactionReceipt } from 'src/network/types/Transaction'; // https://ethereum.github.io/execution-apis/api-documentation/ @@ -19,6 +19,13 @@ export interface Network { getTransaction( ...args: NetworkGetTransactionArgs ): Promise; + + /** + * Wait for a transaction to be mined. + */ + waitForTransaction( + ...args: NetworkWaitForTransactionArgs + ): Promise; } export type NetworkGetBlockOptions = @@ -41,3 +48,14 @@ export type NetworkGetBlockOptions = export type NetworkGetBlockArgs = [options?: NetworkGetBlockOptions]; export type NetworkGetTransactionArgs = [hash: `0x${string}`]; + +export type NetworkWaitForTransactionArgs = [ + hash: `0x${string}`, + options?: { + /** + * The number of milliseconds to wait for the transaction until rejecting + * the promise. + */ + timeout?: number; + }, +]; diff --git a/packages/evm-client/src/network/types/Transaction.ts b/packages/evm-client/src/network/types/Transaction.ts index 1865c41e..99548899 100644 --- a/packages/evm-client/src/network/types/Transaction.ts +++ b/packages/evm-client/src/network/types/Transaction.ts @@ -21,3 +21,34 @@ export interface Transaction extends TransactionInfo { } export type MinedTransaction = Transaction & Required; + +// https://github.com/ethereum/execution-apis/blob/e3d2745289bd2bb61dc8593069871be4be441952/src/schemas/receipt.yaml#L37 +export interface TransactionReceipt { + blockHash: `0x${string}`; + blockNumber: bigint; + from: `0x${string}`; + /** + * Address of the receiver or `null` in a contract creation transaction. + */ + to: `0x${string}` | null; + /** + * The sum of gas used by this transaction and all preceding transactions in + * the same block. + */ + cumulativeGasUsed: bigint; + /** + * The amount of gas used for this specific transaction alone. + */ + gasUsed: bigint; + // TODO: + // logs: Log[]; + logsBloom: `0x${string}`; + transactionHash: `0x${string}`; + transactionIndex: number; + /** + * The actual value per gas deducted from the sender's account. Before + * EIP-1559, this is equal to the transaction's gas price. After, it is equal + * to baseFeePerGas + min(maxFeePerGas - baseFeePerGas, maxPriorityFeePerGas). + */ + effectiveGasPrice: bigint; +}