Skip to content

Commit

Permalink
watchTransactionBySubscription fallback fix (#7118)
Browse files Browse the repository at this point in the history
* watchTransactionByPolling fix

* watchTransactionBySubscription fallback test
  • Loading branch information
jdevcs authored Jun 25, 2024
1 parent bfe2769 commit 750411c
Show file tree
Hide file tree
Showing 2 changed files with 90 additions and 81 deletions.
4 changes: 3 additions & 1 deletion packages/web3-eth/src/utils/watch_transaction_by_polling.ts
Original file line number Diff line number Diff line change
Expand Up @@ -56,8 +56,10 @@ export const watchTransactionByPolling = <
let confirmations = 1;
const intervalId = setInterval(() => {
(async () => {
if (confirmations >= web3Context.transactionConfirmationBlocks)
if (confirmations >= web3Context.transactionConfirmationBlocks){
clearInterval(intervalId);
return;
}

const nextBlock = await ethRpcMethods.getBlockByNumber(
web3Context.requestManager,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -14,117 +14,124 @@ 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, Web3RequestManager } from 'web3-core';
import { format } from 'web3-utils';
import { DEFAULT_RETURN_FORMAT, JsonRpcResponseWithResult, Web3EthExecutionAPI } from 'web3-types';
import { ethRpcMethods } from 'web3-rpc-methods';
import { WebSocketProvider } from 'web3-providers-ws';

import { Web3Context } from 'web3-core';
import { DEFAULT_RETURN_FORMAT, Web3EthExecutionAPI } from 'web3-types';
import WebSocketProvider from 'web3-providers-ws';

import * as rpcMethodWrappers from '../../../src/rpc_method_wrappers';
import * as WatchTransactionBySubscription from '../../../src/utils/watch_transaction_by_subscription';
import {
expectedTransactionReceipt,
expectedTransactionHash,
testData,
} from '../rpc_method_wrappers/fixtures/send_signed_transaction';
import { transactionReceiptSchema } from '../../../src/schemas';
import { registeredSubscriptions } from '../../../src';
import { blockMockResult } from '../../fixtures/transactions_data';


jest.mock('web3-rpc-methods');
jest.mock('web3-providers-ws');
jest.mock('../../../src/utils/watch_transaction_by_polling');

const testMessage =
'Title: %s\ninputSignedTransaction: %s\nexpectedTransactionHash: %s\nexpectedTransactionReceipt: %s\n';

async function waitUntilCalled(mock: jest.Mock, timeout = 1000): Promise<jest.Mock> {
return new Promise((resolve, reject) => {
let timeoutId: NodeJS.Timeout | undefined;
const intervalId = setInterval(() => {
if (mock.mock.calls.length > 0) {
clearInterval(intervalId);
if (timeoutId) {
clearTimeout(timeoutId);
}
resolve(mock);
}
}, 100);
timeoutId = setTimeout(() => {
clearInterval(intervalId);
if (timeoutId) {
clearTimeout(timeoutId);
}
reject(new Error('timeout'));
}, timeout);
});
}

describe('watchTransactionBySubscription', () => {
const CONFIRMATION_BLOCKS = 5;
describe('should revert to polling in cases where getting by subscription did not workout', () => {
let web3Context: Web3Context<Web3EthExecutionAPI>;

beforeEach(() => {
jest.spyOn(Web3RequestManager.prototype, 'send').mockImplementation(async () => {
return {} as Promise<unknown>;
});
jest.spyOn(WebSocketProvider.prototype, 'request').mockImplementation(async () => {
return {} as Promise<JsonRpcResponseWithResult<unknown>>;
});

(ethRpcMethods.sendRawTransaction as jest.Mock).mockResolvedValue(
expectedTransactionHash,
);
(ethRpcMethods.getTransactionReceipt as jest.Mock).mockResolvedValue(
expectedTransactionHash,
);
web3Context = new Web3Context({
// dummy provider that does supports subscription
provider: new WebSocketProvider('ws://localhost:8546'),
registeredSubscriptions,
});
provider: new WebSocketProvider('wss://localhost:8546'),}
);

(web3Context.provider as any).supportsSubscriptions = () => true;
web3Context.transactionConfirmationBlocks = CONFIRMATION_BLOCKS;
web3Context.enableExperimentalFeatures.useSubscriptionWhenCheckingBlockTimeout =
true;

});
afterEach(() => {
// to clear the interval inside the subscription function:
web3Context.transactionConfirmationBlocks = 0;
});
let counter = 0;
it.each(testData)(
`should call getBlockNumber if blockHeaderTimeout reached\n ${testMessage}`,
async (_, inputTransaction) => {
if (counter > 0) {
return;
}
counter += 1;
const formattedTransactionReceipt = format(
transactionReceiptSchema,
expectedTransactionReceipt,
DEFAULT_RETURN_FORMAT,
);

web3Context.enableExperimentalFeatures.useSubscriptionWhenCheckingBlockTimeout =
true;
// this will case the function to revert to polling:
web3Context.blockHeaderTimeout = 0;
it.each(testData)(
`should call getBlockByNumber if blockHeaderTimeout reached\n ${testMessage}`,
async (_, inputTransaction,) => {

web3Context.transactionSendTimeout = 2;
let blockNum = 100;
let ethGetBlockByNumberCount = 0;
web3Context.requestManager.send = jest.fn(async (request) => {

if (request.method === 'eth_getBlockByNumber') {
ethGetBlockByNumberCount += 1;
return Promise.resolve(
{ ...blockMockResult.result,
number: (request as any).params[0]
});
}
if (request.method === 'eth_call') {

return Promise.resolve("0x");
}
if (request.method === 'eth_blockNumber') {
blockNum += 1;
return Promise.resolve(blockNum.toString(16));
}
if (request.method === 'eth_sendRawTransaction') {
return Promise.resolve(expectedTransactionHash);
}
if (request.method === 'eth_getTransactionReceipt') {
return Promise.resolve(expectedTransactionReceipt);
}

// eslint-disable-next-line @typescript-eslint/no-unsafe-return
return Promise.reject(new Error("Unknown Request")) as any;
});

const promiEvent = rpcMethodWrappers.sendSignedTransaction(
web3Context,
inputTransaction,
DEFAULT_RETURN_FORMAT,
);
// await promiEvent;
WatchTransactionBySubscription.watchTransactionBySubscription({
web3Context,
transactionReceipt: formattedTransactionReceipt,
transactionPromiEvent: promiEvent,
returnFormat: DEFAULT_RETURN_FORMAT,

let confirmationsCount = 0;
const confirmationPromise = new Promise<void>((resolve, reject) => {

const handleConfirmation = (confirmation: { confirmations: bigint }) => {
confirmationsCount += 1;

if (confirmation.confirmations >= CONFIRMATION_BLOCKS) {
resolve();
}
};

const handleError = (_error: any) => {
reject();
};

promiEvent
.on('confirmation', handleConfirmation)
.on('error', handleError)
.then((res) => {
// eslint-disable-next-line jest/no-conditional-expect
expect(res).toBeDefined();
})
.catch(reject);
});

// Wait for the confirmationPromise to resolve or timeout after 5 seconds
let timeoutId;
const timeout = new Promise((_res, reject) => {
timeoutId = setTimeout(() => reject(new Error('Timeout waiting for confirmations')), 500000);
});
await waitUntilCalled(ethRpcMethods.getBlockNumber as jest.Mock, 5000);

await promiEvent;
},
60000,
await Promise.race([confirmationPromise, timeout]);

clearTimeout(timeoutId);

expect(confirmationsCount).toBe(CONFIRMATION_BLOCKS);
expect(ethGetBlockByNumberCount).toBe(CONFIRMATION_BLOCKS - 1); // means polling called getblock 4 times as first confirmation is receipt it self

}
);


});
});

1 comment on commit 750411c

@github-actions
Copy link

Choose a reason for hiding this comment

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

Benchmark

Benchmark suite Current: 750411c Previous: bfe2769 Ratio
processingTx 9220 ops/sec (±4.37%) 8959 ops/sec (±4.44%) 0.97
processingContractDeploy 39401 ops/sec (±6.50%) 39629 ops/sec (±8.47%) 1.01
processingContractMethodSend 15383 ops/sec (±7.70%) 16647 ops/sec (±9.83%) 1.08
processingContractMethodCall 27237 ops/sec (±8.48%) 28056 ops/sec (±6.60%) 1.03
abiEncode 42968 ops/sec (±6.77%) 43700 ops/sec (±8.36%) 1.02
abiDecode 29647 ops/sec (±7.15%) 30625 ops/sec (±8.11%) 1.03
sign 1558 ops/sec (±0.85%) 1567 ops/sec (±0.76%) 1.01
verify 369 ops/sec (±0.64%) 376 ops/sec (±0.39%) 1.02

This comment was automatically generated by workflow using github-action-benchmark.

Please sign in to comment.