diff --git a/sandbox/src/chain/jsonRpcWriteClient.ts b/sandbox/src/chain/jsonRpcWriteClient.ts index b1a44b6..c297257 100644 --- a/sandbox/src/chain/jsonRpcWriteClient.ts +++ b/sandbox/src/chain/jsonRpcWriteClient.ts @@ -25,6 +25,7 @@ export interface CreateJsonRpcWriteClientOptions { chainId: number; privateKey: string; pollIntervalMs?: number; + receiptTimeoutMs?: number; fetchImpl?: typeof fetch; } @@ -109,8 +110,11 @@ async function waitForReceipt( rpcUrl: string, transactionHash: `0x${string}`, pollIntervalMs: number, + receiptTimeoutMs: number, fetchImpl: typeof fetch ): Promise { + const startedAt = Date.now(); + for (;;) { const receipt = await jsonRpcRequest( rpcUrl, @@ -120,6 +124,11 @@ async function waitForReceipt( ); if (!receipt) { + const elapsedMs = Date.now() - startedAt; + if (elapsedMs >= receiptTimeoutMs) { + throw new Error(`transaction ${transactionHash} was not mined within ${receiptTimeoutMs}ms`); + } + await sleep(pollIntervalMs); continue; } @@ -146,6 +155,7 @@ export function createJsonRpcWriteClient( const wallet = new Wallet(options.privateKey); const fetchImpl = options.fetchImpl ?? fetch; const pollIntervalMs = options.pollIntervalMs ?? 1000; + const receiptTimeoutMs = options.receiptTimeoutMs ?? 120_000; const signerAddress = wallet.address.toLowerCase(); return { @@ -195,7 +205,7 @@ export function createJsonRpcWriteClient( fetchImpl ); - return waitForReceipt(options.rpcUrl, transactionHash, pollIntervalMs, fetchImpl); + return waitForReceipt(options.rpcUrl, transactionHash, pollIntervalMs, receiptTimeoutMs, fetchImpl); } }; } diff --git a/sandbox/tests/chain/jsonRpcWriteClient.test.ts b/sandbox/tests/chain/jsonRpcWriteClient.test.ts index 83c2659..72912cf 100644 --- a/sandbox/tests/chain/jsonRpcWriteClient.test.ts +++ b/sandbox/tests/chain/jsonRpcWriteClient.test.ts @@ -162,6 +162,39 @@ test("createJsonRpcWriteClient sends a signed raw transaction and waits for a su assert.equal(parsedTx.from?.toLowerCase(), SIGNER_ADDRESS); }); + +test("createJsonRpcWriteClient times out when the receipt never arrives", async () => { + const txHash = `0x${"d".repeat(64)}`; + const fetchImpl = buildMockFetch( + [ + { jsonrpc: "2.0", id: 1, result: TEST_NONCE_HEX }, + { jsonrpc: "2.0", id: 1, result: TEST_GAS_LIMIT_HEX }, + { jsonrpc: "2.0", id: 1, result: TEST_GAS_PRICE_HEX }, + { jsonrpc: "2.0", id: 1, result: txHash }, + { jsonrpc: "2.0", id: 1, result: null } + ], + [] + ); + + const client = createJsonRpcWriteClient({ + rpcUrl: "http://localhost:8545", + chainId: TEST_CHAIN_ID, + privateKey: PRIVATE_KEY, + pollIntervalMs: 0, + receiptTimeoutMs: 0, + fetchImpl + }); + + await assert.rejects( + () => + client.submitTransaction({ + to: CONTRACT_ADDRESS, + data: RECORD_AUDIT_RESULT_CALL_DATA + }), + /was not mined within 0ms/i + ); +}); + test("createJsonRpcWriteClient throws a clear error when the mined receipt has status 0x0", async () => { const txHash = `0x${"e".repeat(64)}`; const fetchImpl = buildMockFetch(