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

Commit 6d99cd0

Browse files
authored
waitForTransactionReceipt fix (#6464)
* added pollTillDefinedAndReturnIntervalId * waitForTransactionReceipt updated interval calls * tests pollTillDefinedAndReturnIntervalId * reusing func for lib size optmz * lint * test fix * waitWithTimeout * unit test * formatting * lint fix * waitForTransactionReceipt unit tests
1 parent 0e78235 commit 6d99cd0

File tree

5 files changed

+229
-31
lines changed

5 files changed

+229
-31
lines changed

packages/web3-eth/src/utils/wait_for_transaction_receipt.ts

Lines changed: 7 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -20,7 +20,7 @@ import { TransactionPollingTimeoutError } from 'web3-errors';
2020
import { EthExecutionAPI, Bytes, TransactionReceipt, DataFormat } from 'web3-types';
2121

2222
// eslint-disable-next-line import/no-cycle
23-
import { pollTillDefined, rejectIfTimeout } from 'web3-utils';
23+
import { pollTillDefinedAndReturnIntervalId, rejectIfTimeout } from 'web3-utils';
2424
// eslint-disable-next-line import/no-cycle
2525
import { rejectIfBlockTimeout } from './reject_if_block_timeout.js';
2626
// eslint-disable-next-line import/no-cycle
@@ -31,10 +31,11 @@ export async function waitForTransactionReceipt<ReturnFormat extends DataFormat>
3131
transactionHash: Bytes,
3232
returnFormat: ReturnFormat,
3333
): Promise<TransactionReceipt> {
34+
3435
const pollingInterval =
3536
web3Context.transactionReceiptPollingInterval ?? web3Context.transactionPollingInterval;
3637

37-
const awaitableTransactionReceipt: Promise<TransactionReceipt> = pollTillDefined(async () => {
38+
const [awaitableTransactionReceipt, IntervalId] = pollTillDefinedAndReturnIntervalId(async () => {
3839
try {
3940
return getTransactionReceipt(web3Context, transactionHash, returnFormat);
4041
} catch (error) {
@@ -64,7 +65,10 @@ export async function waitForTransactionReceipt<ReturnFormat extends DataFormat>
6465
rejectOnBlockTimeout, // this will throw an error on Transaction Block Timeout
6566
]);
6667
} finally {
67-
clearTimeout(timeoutId);
68+
if(timeoutId)
69+
clearTimeout(timeoutId);
70+
if(IntervalId)
71+
clearInterval(IntervalId);
6872
blockTimeoutResourceCleaner.clean();
6973
}
7074
}

packages/web3-eth/test/integration/defaults.transactionBlockTimeout.test.ts

Lines changed: 9 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -45,8 +45,6 @@ describe('defaults', () => {
4545
beforeEach(() => {
4646
clientUrl = getSystemTestProvider();
4747
web3 = new Web3(clientUrl);
48-
// Make the test run faster by casing the polling to start after 2 blocks
49-
web3.eth.transactionBlockTimeout = 2;
5048

5149
// Increase other timeouts so only `transactionBlockTimeout` would be reached
5250
web3.eth.transactionSendTimeout = MAX_32_SIGNED_INTEGER;
@@ -64,6 +62,7 @@ describe('defaults', () => {
6462
account1 = await createLocalAccount(web3);
6563
account2 = await createLocalAccount(web3);
6664
// Setting a high `nonce` when sending a transaction, to cause the RPC call to stuck at the Node
65+
6766
const sentTx: Web3PromiEvent<
6867
TransactionReceipt,
6968
SendTransactionEvents<typeof DEFAULT_RETURN_FORMAT>
@@ -81,18 +80,13 @@ describe('defaults', () => {
8180
// So, send 2 transactions, one after another, because in this test `transactionBlockTimeout = 2`.
8281
// eslint-disable-next-line no-void
8382
await sendFewSampleTxs(2);
83+
84+
web3.eth.transactionBlockTimeout = 2;
85+
86+
await expect(sentTx).rejects.toThrow(/was not mined within [0-9]+ blocks/);
87+
88+
await expect(sentTx).rejects.toThrow(TransactionBlockTimeoutError);
8489

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

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

125+
web3.eth.transactionBlockTimeout = 2;
126+
131127
await expect(sentTx).rejects.toThrow(/was not mined within [0-9]+ blocks/);
132128

133129
await expect(sentTx).rejects.toThrow(TransactionBlockTimeoutError);
Lines changed: 126 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,126 @@
1+
/*
2+
This file is part of web3.js.
3+
4+
web3.js is free software: you can redistribute it and/or modify
5+
it under the terms of the GNU Lesser General Public License as published by
6+
the Free Software Foundation, either version 3 of the License, or
7+
(at your option) any later version.
8+
9+
web3.js is distributed in the hope that it will be useful,
10+
but WITHOUT ANY WARRANTY; without even the implied warranty of
11+
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12+
GNU Lesser General Public License for more details.
13+
14+
You should have received a copy of the GNU Lesser General Public License
15+
along with web3.js. If not, see <http://www.gnu.org/licenses/>.
16+
*/
17+
import { Web3Context } from 'web3-core';
18+
import { DEFAULT_RETURN_FORMAT, Web3EthExecutionAPI } from 'web3-types';
19+
import { TransactionBlockTimeoutError } from 'web3-errors';
20+
import { waitForTransactionReceipt } from '../../../src/utils/wait_for_transaction_receipt';
21+
22+
describe('waitForTransactionReceipt unit test', () => {
23+
let web3Context: Web3Context<Web3EthExecutionAPI>;
24+
25+
it(`waitForTransactionReceipt should throw error after block timeout`, async () => {
26+
let blockNum = 1;
27+
28+
web3Context = new Web3Context(
29+
{
30+
request: async (payload: any) => {
31+
let response: { jsonrpc: string; id: any; result: string } | undefined;
32+
33+
switch (payload.method) {
34+
case 'eth_blockNumber':
35+
blockNum += 50;
36+
response = {
37+
jsonrpc: '2.0',
38+
id: payload.id,
39+
result: `0x${blockNum.toString(16)}`,
40+
};
41+
break;
42+
43+
case 'eth_getTransactionReceipt':
44+
response = undefined;
45+
break;
46+
47+
default:
48+
throw new Error(`Unknown payload ${payload}`);
49+
}
50+
51+
return new Promise(resolve => {
52+
resolve(response as any);
53+
});
54+
},
55+
supportsSubscriptions: () => false,
56+
},
57+
);
58+
59+
await expect(async () =>
60+
waitForTransactionReceipt(
61+
web3Context,
62+
'0x0430b701e657e634a9d5480eae0387a473913ef29af8e60c38a3cee24494ed54',
63+
DEFAULT_RETURN_FORMAT
64+
)
65+
).rejects.toThrow(TransactionBlockTimeoutError);
66+
67+
});
68+
69+
it(`waitForTransactionReceipt should resolve immediatly if receipt is avalible`, async () => {
70+
let blockNum = 1;
71+
const txHash = '0x85d995eba9763907fdf35cd2034144dd9d53ce32cbec21349d4b12823c6860c5';
72+
const blockHash = '0xa957d47df264a31badc3ae823e10ac1d444b098d9b73d204c40426e57f47e8c3';
73+
74+
web3Context = new Web3Context(
75+
{
76+
request: async (payload: any) => {
77+
const response = {
78+
jsonrpc: '2.0',
79+
id: payload.id,
80+
result: {},
81+
};
82+
83+
switch (payload.method) {
84+
case 'eth_blockNumber':
85+
blockNum += 10;
86+
response.result = `0x${blockNum.toString(16)}`;
87+
break;
88+
89+
case 'eth_getTransactionReceipt':
90+
response.result = {
91+
blockHash,
92+
blockNumber: `0x1`,
93+
cumulativeGasUsed: '0xa12515',
94+
from: payload.from,
95+
gasUsed: payload.gasLimit,
96+
status: '0x1',
97+
to: payload.to,
98+
transactionHash: txHash,
99+
transactionIndex: '0x66',
100+
101+
};
102+
break;
103+
104+
default:
105+
throw new Error(`Unknown payload ${payload}`);
106+
}
107+
108+
return new Promise(resolve => {
109+
resolve(response as any);
110+
});
111+
},
112+
supportsSubscriptions: () => false,
113+
},
114+
);
115+
116+
const res = await waitForTransactionReceipt(
117+
web3Context,
118+
'0x0430b701e657e634a9d5480eae0387a473913ef29af8e60c38a3cee24494ed54',
119+
DEFAULT_RETURN_FORMAT
120+
);
121+
122+
expect(res).toBeDefined();
123+
expect(res.transactionHash).toStrictEqual(txHash);
124+
expect(res.blockHash).toStrictEqual(blockHash);
125+
});
126+
})

packages/web3-utils/src/promise_helpers.ts

Lines changed: 24 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -73,21 +73,22 @@ export async function waitWithTimeout<T>(
7373
}
7474
return result;
7575
}
76+
77+
7678
/**
7779
* Repeatedly calls an async function with a given interval until the result of the function is defined (not undefined or null),
78-
* or until a timeout is reached.
80+
* or until a timeout is reached. It returns promise and intervalId.
7981
* @param func - The function to call.
8082
* @param interval - The interval in milliseconds.
8183
*/
82-
export async function pollTillDefined<T>(
84+
export function pollTillDefinedAndReturnIntervalId<T>(
8385
func: AsyncFunction<T>,
8486
interval: number,
85-
): Promise<Exclude<T, undefined>> {
86-
const awaitableRes = waitWithTimeout(func, interval);
87+
): [Promise<Exclude<T, undefined>>, Timer] {
8788

8889
let intervalId: Timer | undefined;
8990
const polledRes = new Promise<Exclude<T, undefined>>((resolve, reject) => {
90-
intervalId = setInterval(() => {
91+
intervalId = setInterval(function intervalCallbackFunc(){
9192
(async () => {
9293
try {
9394
const res = await waitWithTimeout(func, interval);
@@ -101,19 +102,26 @@ export async function pollTillDefined<T>(
101102
reject(error);
102103
}
103104
})() as unknown;
104-
}, interval);
105+
return intervalCallbackFunc;}() // this will immediate invoke first call
106+
, interval);
105107
});
106108

107-
// If the first call to awaitableRes succeeded, return the result
108-
const res = await awaitableRes;
109-
if (!isNullish(res)) {
110-
if (intervalId) {
111-
clearInterval(intervalId);
112-
}
113-
return res as unknown as Exclude<T, undefined>;
114-
}
109+
return [polledRes as unknown as Promise<Exclude<T, undefined>>, intervalId!];
110+
}
115111

116-
return polledRes;
112+
/**
113+
* Repeatedly calls an async function with a given interval until the result of the function is defined (not undefined or null),
114+
* or until a timeout is reached.
115+
* pollTillDefinedAndReturnIntervalId() function should be used instead of pollTillDefined if you need IntervalId in result.
116+
* This function will be deprecated in next major release so use pollTillDefinedAndReturnIntervalId().
117+
* @param func - The function to call.
118+
* @param interval - The interval in milliseconds.
119+
*/
120+
export async function pollTillDefined<T>(
121+
func: AsyncFunction<T>,
122+
interval: number,
123+
): Promise<Exclude<T, undefined>> {
124+
return pollTillDefinedAndReturnIntervalId(func, interval)[0];
117125
}
118126
/**
119127
* Enforce a timeout on a promise, so that it can be rejected if it takes too long to complete
@@ -160,3 +168,4 @@ export function rejectIfConditionAtInterval<T>(
160168
});
161169
return [intervalId!, rejectIfCondition];
162170
}
171+

packages/web3-utils/test/unit/promise_helpers.test.ts

Lines changed: 63 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@ import {
2121
isPromise,
2222
pollTillDefined,
2323
rejectIfConditionAtInterval,
24+
pollTillDefinedAndReturnIntervalId,
2425
} from '../../src/promise_helpers';
2526

2627
describe('promise helpers', () => {
@@ -121,6 +122,68 @@ describe('promise helpers', () => {
121122
});
122123
});
123124

125+
describe('pollTillDefinedAndReturnIntervalId', () => {
126+
it('returns when immediately resolved', async () => {
127+
const asyncHelper = async () =>
128+
new Promise(resolve => {
129+
resolve('resolved');
130+
});
131+
const [promise] = pollTillDefinedAndReturnIntervalId(asyncHelper, 100);
132+
await expect(promise).resolves.toBe('resolved');
133+
});
134+
it('returns if later resolved', async () => {
135+
let counter = 0;
136+
const asyncHelper = async () => {
137+
if (counter === 0) {
138+
counter += 1;
139+
return undefined;
140+
}
141+
return new Promise(resolve => {
142+
resolve('resolved');
143+
});
144+
};
145+
const [promise] = pollTillDefinedAndReturnIntervalId(asyncHelper, 100);
146+
await expect(promise).resolves.toBe('resolved');
147+
});
148+
149+
it('should return interval id if not resolved in specific time', async () => {
150+
151+
let counter = 0;
152+
const asyncHelper = async () => {
153+
if (counter <= 3000000) {
154+
counter += 1;
155+
return undefined;
156+
}
157+
return "result";
158+
};
159+
160+
const testError = new Error('Test P2 Error');
161+
162+
const [neverResolvePromise, intervalId] = pollTillDefinedAndReturnIntervalId(asyncHelper, 100);
163+
const promiCheck = Promise.race([neverResolvePromise, rejectIfTimeout(500,testError)[1]]);
164+
165+
await expect(promiCheck).rejects.toThrow(testError);
166+
expect(intervalId).toBeDefined();
167+
clearInterval(intervalId);
168+
});
169+
170+
it('throws if later throws', async () => {
171+
const dummyError = new Error('error');
172+
let counter = 0;
173+
const asyncHelper = async () => {
174+
if (counter === 0) {
175+
counter += 1;
176+
return undefined;
177+
}
178+
return new Promise((_, reject) => {
179+
reject(dummyError);
180+
});
181+
};
182+
const [promise] = pollTillDefinedAndReturnIntervalId(asyncHelper, 100);
183+
await expect(promise).rejects.toThrow(dummyError);
184+
});
185+
});
186+
124187
describe('rejectIfConditionAtInterval', () => {
125188
it('reject if later throws', async () => {
126189
const dummyError = new Error('error');

0 commit comments

Comments
 (0)