Skip to content
This repository has been archived by the owner on Mar 5, 2025. It is now read-only.

waitForTransactionReceipt fix #6464

Merged
merged 12 commits into from
Oct 5, 2023
10 changes: 7 additions & 3 deletions packages/web3-eth/src/utils/wait_for_transaction_receipt.ts
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@ import { TransactionPollingTimeoutError } from 'web3-errors';
import { EthExecutionAPI, Bytes, TransactionReceipt, DataFormat } from 'web3-types';

// eslint-disable-next-line import/no-cycle
import { pollTillDefined, rejectIfTimeout } from 'web3-utils';
import { pollTillDefinedAndReturnIntervalId, rejectIfTimeout } from 'web3-utils';
// eslint-disable-next-line import/no-cycle
import { rejectIfBlockTimeout } from './reject_if_block_timeout.js';
// eslint-disable-next-line import/no-cycle
Expand All @@ -31,10 +31,11 @@ export async function waitForTransactionReceipt<ReturnFormat extends DataFormat>
transactionHash: Bytes,
returnFormat: ReturnFormat,
): Promise<TransactionReceipt> {

const pollingInterval =
web3Context.transactionReceiptPollingInterval ?? web3Context.transactionPollingInterval;

const awaitableTransactionReceipt: Promise<TransactionReceipt> = pollTillDefined(async () => {
const [awaitableTransactionReceipt, IntervalId] = pollTillDefinedAndReturnIntervalId(async () => {
try {
return getTransactionReceipt(web3Context, transactionHash, returnFormat);
} catch (error) {
Expand Down Expand Up @@ -64,7 +65,10 @@ export async function waitForTransactionReceipt<ReturnFormat extends DataFormat>
rejectOnBlockTimeout, // this will throw an error on Transaction Block Timeout
]);
} finally {
clearTimeout(timeoutId);
if(timeoutId)
clearTimeout(timeoutId);
if(IntervalId)
clearInterval(IntervalId);
blockTimeoutResourceCleaner.clean();
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -45,8 +45,6 @@ describe('defaults', () => {
beforeEach(() => {
clientUrl = getSystemTestProvider();
web3 = new Web3(clientUrl);
// Make the test run faster by casing the polling to start after 2 blocks
web3.eth.transactionBlockTimeout = 2;

// Increase other timeouts so only `transactionBlockTimeout` would be reached
web3.eth.transactionSendTimeout = MAX_32_SIGNED_INTEGER;
Expand All @@ -64,6 +62,7 @@ describe('defaults', () => {
account1 = await createLocalAccount(web3);
account2 = await createLocalAccount(web3);
// Setting a high `nonce` when sending a transaction, to cause the RPC call to stuck at the Node

const sentTx: Web3PromiEvent<
TransactionReceipt,
SendTransactionEvents<typeof DEFAULT_RETURN_FORMAT>
Expand All @@ -81,18 +80,13 @@ describe('defaults', () => {
// So, send 2 transactions, one after another, because in this test `transactionBlockTimeout = 2`.
// eslint-disable-next-line no-void
await sendFewSampleTxs(2);

web3.eth.transactionBlockTimeout = 2;

await expect(sentTx).rejects.toThrow(/was not mined within [0-9]+ blocks/);

await expect(sentTx).rejects.toThrow(TransactionBlockTimeoutError);

try {
await sentTx;
throw new Error(
'The test should fail if there is no exception when sending a transaction that could not be mined within transactionBlockTimeout',
);
} catch (error) {
// eslint-disable-next-line jest/no-conditional-expect
expect(error).toBeInstanceOf(TransactionBlockTimeoutError);
// eslint-disable-next-line jest/no-conditional-expect
expect((error as Error).message).toMatch(/was not mined within [0-9]+ blocks/);
}
await closeOpenConnection(web3.eth);
});

Expand Down Expand Up @@ -128,6 +122,8 @@ describe('defaults', () => {
// eslint-disable-next-line no-void, @typescript-eslint/no-unsafe-call
void sendFewSampleTxs(2);

web3.eth.transactionBlockTimeout = 2;

await expect(sentTx).rejects.toThrow(/was not mined within [0-9]+ blocks/);

await expect(sentTx).rejects.toThrow(TransactionBlockTimeoutError);
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,126 @@
/*
This file is part of web3.js.

web3.js is free software: you can redistribute it and/or modify
it under the terms of the GNU Lesser General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.

web3.js is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU Lesser General Public License for more details.

You should have received a copy of the GNU Lesser General Public License
along with web3.js. If not, see <http://www.gnu.org/licenses/>.
*/
import { Web3Context } from 'web3-core';
import { DEFAULT_RETURN_FORMAT, Web3EthExecutionAPI } from 'web3-types';
import { TransactionBlockTimeoutError } from 'web3-errors';
import { waitForTransactionReceipt } from '../../../src/utils/wait_for_transaction_receipt';

describe('waitForTransactionReceipt unit test', () => {
let web3Context: Web3Context<Web3EthExecutionAPI>;

it(`waitForTransactionReceipt should throw error after block timeout`, async () => {
let blockNum = 1;

web3Context = new Web3Context(
{
request: async (payload: any) => {
let response: { jsonrpc: string; id: any; result: string } | undefined;

switch (payload.method) {
case 'eth_blockNumber':
blockNum += 50;
response = {
jsonrpc: '2.0',
id: payload.id,
result: `0x${blockNum.toString(16)}`,
};
break;

case 'eth_getTransactionReceipt':
response = undefined;
break;

default:
throw new Error(`Unknown payload ${payload}`);
}

return new Promise(resolve => {
resolve(response as any);
});
},
supportsSubscriptions: () => false,
},
);

await expect(async () =>
waitForTransactionReceipt(
web3Context,
'0x0430b701e657e634a9d5480eae0387a473913ef29af8e60c38a3cee24494ed54',
DEFAULT_RETURN_FORMAT
)
).rejects.toThrow(TransactionBlockTimeoutError);

});

it(`waitForTransactionReceipt should resolve immediatly if receipt is avalible`, async () => {
let blockNum = 1;
const txHash = '0x85d995eba9763907fdf35cd2034144dd9d53ce32cbec21349d4b12823c6860c5';
const blockHash = '0xa957d47df264a31badc3ae823e10ac1d444b098d9b73d204c40426e57f47e8c3';

web3Context = new Web3Context(
{
request: async (payload: any) => {
const response = {
jsonrpc: '2.0',
id: payload.id,
result: {},
};

switch (payload.method) {
case 'eth_blockNumber':
blockNum += 10;
response.result = `0x${blockNum.toString(16)}`;
break;

case 'eth_getTransactionReceipt':
response.result = {
blockHash,
blockNumber: `0x1`,
cumulativeGasUsed: '0xa12515',
from: payload.from,
gasUsed: payload.gasLimit,
status: '0x1',
to: payload.to,
transactionHash: txHash,
transactionIndex: '0x66',

};
break;

default:
throw new Error(`Unknown payload ${payload}`);
}

return new Promise(resolve => {
resolve(response as any);
});
},
supportsSubscriptions: () => false,
},
);

const res = await waitForTransactionReceipt(
web3Context,
'0x0430b701e657e634a9d5480eae0387a473913ef29af8e60c38a3cee24494ed54',
DEFAULT_RETURN_FORMAT
);

expect(res).toBeDefined();
expect(res.transactionHash).toStrictEqual(txHash);
expect(res.blockHash).toStrictEqual(blockHash);
});
})
39 changes: 24 additions & 15 deletions packages/web3-utils/src/promise_helpers.ts
Original file line number Diff line number Diff line change
Expand Up @@ -73,21 +73,22 @@ export async function waitWithTimeout<T>(
}
return result;
}


/**
* Repeatedly calls an async function with a given interval until the result of the function is defined (not undefined or null),
* or until a timeout is reached.
* or until a timeout is reached. It returns promise and intervalId.
* @param func - The function to call.
* @param interval - The interval in milliseconds.
*/
export async function pollTillDefined<T>(
export function pollTillDefinedAndReturnIntervalId<T>(
func: AsyncFunction<T>,
interval: number,
): Promise<Exclude<T, undefined>> {
const awaitableRes = waitWithTimeout(func, interval);
): [Promise<Exclude<T, undefined>>, Timer] {

let intervalId: Timer | undefined;
const polledRes = new Promise<Exclude<T, undefined>>((resolve, reject) => {
intervalId = setInterval(() => {
intervalId = setInterval(function intervalCallbackFunc(){
(async () => {
try {
const res = await waitWithTimeout(func, interval);
Expand All @@ -101,19 +102,26 @@ export async function pollTillDefined<T>(
reject(error);
}
})() as unknown;
}, interval);
return intervalCallbackFunc;}() // this will immediate invoke first call
, interval);
});

// If the first call to awaitableRes succeeded, return the result
const res = await awaitableRes;
if (!isNullish(res)) {
if (intervalId) {
clearInterval(intervalId);
}
return res as unknown as Exclude<T, undefined>;
}
return [polledRes as unknown as Promise<Exclude<T, undefined>>, intervalId!];
}

return polledRes;
/**
* Repeatedly calls an async function with a given interval until the result of the function is defined (not undefined or null),
* or until a timeout is reached.
* pollTillDefinedAndReturnIntervalId() function should be used instead of pollTillDefined if you need IntervalId in result.
* This function will be deprecated in next major release so use pollTillDefinedAndReturnIntervalId().
* @param func - The function to call.
* @param interval - The interval in milliseconds.
*/
export async function pollTillDefined<T>(
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

keeping pollTillDefined so we dnt break API as its exported function from utils.

func: AsyncFunction<T>,
interval: number,
): Promise<Exclude<T, undefined>> {
return pollTillDefinedAndReturnIntervalId(func, interval)[0];
}
/**
* Enforce a timeout on a promise, so that it can be rejected if it takes too long to complete
Expand Down Expand Up @@ -160,3 +168,4 @@ export function rejectIfConditionAtInterval<T>(
});
return [intervalId!, rejectIfCondition];
}

63 changes: 63 additions & 0 deletions packages/web3-utils/test/unit/promise_helpers.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@ import {
isPromise,
pollTillDefined,
rejectIfConditionAtInterval,
pollTillDefinedAndReturnIntervalId,
} from '../../src/promise_helpers';

describe('promise helpers', () => {
Expand Down Expand Up @@ -121,6 +122,68 @@ describe('promise helpers', () => {
});
});

describe('pollTillDefinedAndReturnIntervalId', () => {
it('returns when immediately resolved', async () => {
const asyncHelper = async () =>
new Promise(resolve => {
resolve('resolved');
});
const [promise] = pollTillDefinedAndReturnIntervalId(asyncHelper, 100);
await expect(promise).resolves.toBe('resolved');
});
it('returns if later resolved', async () => {
let counter = 0;
const asyncHelper = async () => {
if (counter === 0) {
counter += 1;
return undefined;
}
return new Promise(resolve => {
resolve('resolved');
});
};
const [promise] = pollTillDefinedAndReturnIntervalId(asyncHelper, 100);
await expect(promise).resolves.toBe('resolved');
});

it('should return interval id if not resolved in specific time', async () => {

let counter = 0;
const asyncHelper = async () => {
if (counter <= 3000000) {
counter += 1;
return undefined;
}
return "result";
};

const testError = new Error('Test P2 Error');

const [neverResolvePromise, intervalId] = pollTillDefinedAndReturnIntervalId(asyncHelper, 100);
const promiCheck = Promise.race([neverResolvePromise, rejectIfTimeout(500,testError)[1]]);

await expect(promiCheck).rejects.toThrow(testError);
expect(intervalId).toBeDefined();
clearInterval(intervalId);
});

it('throws if later throws', async () => {
const dummyError = new Error('error');
let counter = 0;
const asyncHelper = async () => {
if (counter === 0) {
counter += 1;
return undefined;
}
return new Promise((_, reject) => {
reject(dummyError);
});
};
const [promise] = pollTillDefinedAndReturnIntervalId(asyncHelper, 100);
await expect(promise).rejects.toThrow(dummyError);
});
});

describe('rejectIfConditionAtInterval', () => {
it('reject if later throws', async () => {
const dummyError = new Error('error');
Expand Down