diff --git a/packages/transaction-controller/CHANGELOG.md b/packages/transaction-controller/CHANGELOG.md index a9e76b3b064..34e0fd85f32 100644 --- a/packages/transaction-controller/CHANGELOG.md +++ b/packages/transaction-controller/CHANGELOG.md @@ -9,6 +9,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ### Changed +- Update transaction timeout to check time since submission or when transaction was last seen on network ([#7464](https://github.com/MetaMask/core/pull/7464)) + - Uses `blockTime` from `acceleratedPolling` feature flag. - Deprecate `history` and `sendFlowHistory` properties from `TransactionMeta` and `TransactionController` options ([#7326](https://github.com/MetaMask/core/pull/7326)) - Bump `@metamask/remote-feature-flag-controller` from `^3.0.0` to `^3.1.0` ([#7519](https://github.com/MetaMask/core/pull/7519)) - Bump `@metamask/network-controller` from `^27.0.0` to `^27.1.0` ([#7534](https://github.com/MetaMask/core/pull/7534)) diff --git a/packages/transaction-controller/src/helpers/PendingTransactionTracker.test.ts b/packages/transaction-controller/src/helpers/PendingTransactionTracker.test.ts index 05987812f3f..883a8583f67 100644 --- a/packages/transaction-controller/src/helpers/PendingTransactionTracker.test.ts +++ b/packages/transaction-controller/src/helpers/PendingTransactionTracker.test.ts @@ -1321,23 +1321,40 @@ describe('PendingTransactionTracker', () => { }); describe('timeout', () => { + beforeEach(() => { + jest.useFakeTimers(); + jest.setSystemTime(0); + + mockFeatureFlags(messenger, { + confirmations_transactions: { + timeoutAttempts: { + default: 3, + }, + acceleratedPolling: { + perChainConfig: { + [CHAIN_ID_MOCK]: { + blockTime: 12000, + }, + }, + }, + }, + }); + }); + + afterEach(() => { + jest.useRealTimers(); + }); + describe('does not timeout', () => { it('if isTimeoutEnabled returns false', async () => { const listener = jest.fn(); const isTimeoutEnabled = jest.fn().mockReturnValue(false); - - mockFeatureFlags(messenger, { - confirmations_transactions: { - timeoutAttempts: { - default: 1, - }, - }, - }); + const submittedTime = Date.now(); pendingTransactionTracker = new PendingTransactionTracker({ ...options, getTransactions: (): TransactionMeta[] => - freeze([{ ...TRANSACTION_SUBMITTED_MOCK }], true), + freeze([{ ...TRANSACTION_SUBMITTED_MOCK, submittedTime }], true), isTimeoutEnabled, }); @@ -1350,23 +1367,26 @@ describe('PendingTransactionTracker', () => { getTransactionCountMock.mockResolvedValueOnce('0x3'); getTransactionByHashMock.mockResolvedValueOnce(null); + jest.advanceTimersByTime(20000); // Advance past blockTime + await onPoll(); expect(isTimeoutEnabled).toHaveBeenCalledWith( - TRANSACTION_SUBMITTED_MOCK, + expect.objectContaining(TRANSACTION_SUBMITTED_MOCK), ); expect(listener).toHaveBeenCalledTimes(0); }); it('if timeout threshold is undefined', async () => { const listener = jest.fn(); + const submittedTime = Date.now(); mockFeatureFlags(messenger, {}); pendingTransactionTracker = new PendingTransactionTracker({ ...options, getTransactions: (): TransactionMeta[] => - freeze([{ ...TRANSACTION_SUBMITTED_MOCK }], true), + freeze([{ ...TRANSACTION_SUBMITTED_MOCK, submittedTime }], true), }); pendingTransactionTracker.hub.addListener( @@ -1378,6 +1398,8 @@ describe('PendingTransactionTracker', () => { getTransactionCountMock.mockResolvedValueOnce('0x3'); getTransactionByHashMock.mockResolvedValueOnce(null); + jest.advanceTimersByTime(20000); // Advance past blockTime + await onPoll(); expect(listener).toHaveBeenCalledTimes(0); @@ -1385,6 +1407,7 @@ describe('PendingTransactionTracker', () => { it('if timeout threshold is zero', async () => { const listener = jest.fn(); + const submittedTime = Date.now(); mockFeatureFlags(messenger, { confirmations_transactions: { @@ -1397,7 +1420,7 @@ describe('PendingTransactionTracker', () => { pendingTransactionTracker = new PendingTransactionTracker({ ...options, getTransactions: (): TransactionMeta[] => - freeze([{ ...TRANSACTION_SUBMITTED_MOCK }], true), + freeze([{ ...TRANSACTION_SUBMITTED_MOCK, submittedTime }], true), }); pendingTransactionTracker.hub.addListener( @@ -1409,6 +1432,8 @@ describe('PendingTransactionTracker', () => { getTransactionCountMock.mockResolvedValueOnce('0x3'); getTransactionByHashMock.mockResolvedValueOnce(null); + jest.advanceTimersByTime(20000); // Advance past blockTime + await onPoll(); expect(listener).toHaveBeenCalledTimes(0); @@ -1416,19 +1441,12 @@ describe('PendingTransactionTracker', () => { it('if transaction nonce is greater than next nonce', async () => { const listener = jest.fn(); - - mockFeatureFlags(messenger, { - confirmations_transactions: { - timeoutAttempts: { - default: 1, - }, - }, - }); + const submittedTime = Date.now(); pendingTransactionTracker = new PendingTransactionTracker({ ...options, getTransactions: (): TransactionMeta[] => - freeze([{ ...TRANSACTION_SUBMITTED_MOCK }], true), + freeze([{ ...TRANSACTION_SUBMITTED_MOCK, submittedTime }], true), }); pendingTransactionTracker.hub.addListener( @@ -1440,6 +1458,8 @@ describe('PendingTransactionTracker', () => { getTransactionCountMock.mockResolvedValueOnce('0x1'); getTransactionByHashMock.mockResolvedValueOnce(null); + jest.advanceTimersByTime(20000); // Advance past blockTime + await onPoll(); expect(listener).toHaveBeenCalledTimes(0); @@ -1447,19 +1467,13 @@ describe('PendingTransactionTracker', () => { it('if transaction has no hash', async () => { const listener = jest.fn(); + const submittedTime = Date.now(); const transactionWithoutHash = { ...TRANSACTION_SUBMITTED_MOCK, hash: undefined, + submittedTime, }; - mockFeatureFlags(messenger, { - confirmations_transactions: { - timeoutAttempts: { - default: 1, - }, - }, - }); - pendingTransactionTracker = new PendingTransactionTracker({ ...options, getTransactions: (): TransactionMeta[] => @@ -1478,6 +1492,8 @@ describe('PendingTransactionTracker', () => { getTransactionReceiptMock.mockResolvedValueOnce(undefined); getTransactionCountMock.mockResolvedValueOnce('0x3'); + jest.advanceTimersByTime(20000); // Advance past blockTime + await onPoll(); expect(listener).toHaveBeenCalledTimes(0); @@ -1485,26 +1501,48 @@ describe('PendingTransactionTracker', () => { it('if transaction has no nonce', async () => { const listener = jest.fn(); + const submittedTime = Date.now(); const transactionWithoutNonce = { ...TRANSACTION_SUBMITTED_MOCK, txParams: { ...TRANSACTION_SUBMITTED_MOCK.txParams, nonce: undefined, }, + submittedTime, }; - mockFeatureFlags(messenger, { - confirmations_transactions: { - timeoutAttempts: { - default: 3, - }, - }, + pendingTransactionTracker = new PendingTransactionTracker({ + ...options, + getTransactions: (): TransactionMeta[] => + freeze([transactionWithoutNonce], true), }); + pendingTransactionTracker.hub.addListener( + 'transaction-failed', + listener, + ); + + getTransactionReceiptMock.mockResolvedValueOnce(undefined); + getTransactionCountMock.mockResolvedValueOnce('0x3'); + + jest.advanceTimersByTime(50000); // Advance past blockTime * threshold + + await onPoll(); + + expect(listener).toHaveBeenCalledTimes(0); + }); + + it('if transaction has no submittedTime', async () => { + const listener = jest.fn(); + const transactionWithoutSubmittedTime = { + ...TRANSACTION_SUBMITTED_MOCK, + submittedTime: undefined, + }; + pendingTransactionTracker = new PendingTransactionTracker({ ...options, getTransactions: (): TransactionMeta[] => - freeze([transactionWithoutNonce], true), + freeze([transactionWithoutSubmittedTime], true), }); pendingTransactionTracker.hub.addListener( @@ -1514,6 +1552,9 @@ describe('PendingTransactionTracker', () => { getTransactionReceiptMock.mockResolvedValueOnce(undefined); getTransactionCountMock.mockResolvedValueOnce('0x3'); + getTransactionByHashMock.mockResolvedValueOnce(null); + + jest.advanceTimersByTime(50000); // Advance past blockTime * threshold await onPoll(); @@ -1524,19 +1565,12 @@ describe('PendingTransactionTracker', () => { describe('resets timeout counter', () => { it('when transaction is found on network', async () => { const listener = jest.fn(); - - mockFeatureFlags(messenger, { - confirmations_transactions: { - timeoutAttempts: { - default: 3, - }, - }, - }); + const submittedTime = Date.now(); pendingTransactionTracker = new PendingTransactionTracker({ ...options, getTransactions: (): TransactionMeta[] => - freeze([{ ...TRANSACTION_SUBMITTED_MOCK }], true), + freeze([{ ...TRANSACTION_SUBMITTED_MOCK, submittedTime }], true), }); pendingTransactionTracker.hub.addListener( @@ -1544,32 +1578,41 @@ describe('PendingTransactionTracker', () => { listener, ); - // First check - transaction not found + // First check - transaction not found, advance time slightly getTransactionReceiptMock.mockResolvedValueOnce(undefined); getTransactionCountMock.mockResolvedValueOnce('0x3'); getTransactionByHashMock.mockResolvedValueOnce(null); + jest.advanceTimersByTime(10000); // Advance 10 seconds + await onPoll(); expect(listener).toHaveBeenCalledTimes(0); - // Second check - transaction found on network + // Second check - transaction found on network, this resets the timestamp getTransactionReceiptMock.mockResolvedValueOnce(undefined); getTransactionCountMock.mockResolvedValueOnce('0x3'); getTransactionByHashMock.mockResolvedValueOnce({ hash: '0x1' }); + jest.advanceTimersByTime(10000); // Advance another 10 seconds + await onPoll(); expect(listener).toHaveBeenCalledTimes(0); - // Third check - transaction not found again (counter should be reset) + // Third check - transaction not found again (timestamp should have been reset) + // Even though we advance by 30 seconds (10 + 10 + 10), since timestamp was reset + // the duration since last seen should only be 10 seconds, which is less than + // the timeout duration (blockTime=12000ms * threshold=3 = 36000ms) getTransactionReceiptMock.mockResolvedValueOnce(undefined); getTransactionCountMock.mockResolvedValueOnce('0x3'); getTransactionByHashMock.mockResolvedValueOnce(null); + jest.advanceTimersByTime(10000); // Advance another 10 seconds + await onPoll(); - // Should not fail because counter was reset + // Should not fail because timestamp was reset expect(listener).toHaveBeenCalledTimes(0); }); }); @@ -1577,19 +1620,12 @@ describe('PendingTransactionTracker', () => { describe('fails transaction', () => { it('when timeout threshold is reached', async () => { const listener = jest.fn(); - - mockFeatureFlags(messenger, { - confirmations_transactions: { - timeoutAttempts: { - default: 3, - }, - }, - }); + const submittedTime = Date.now(); pendingTransactionTracker = new PendingTransactionTracker({ ...options, getTransactions: (): TransactionMeta[] => - freeze([{ ...TRANSACTION_SUBMITTED_MOCK }], true), + freeze([{ ...TRANSACTION_SUBMITTED_MOCK, submittedTime }], true), }); pendingTransactionTracker.hub.addListener( @@ -1597,77 +1633,38 @@ describe('PendingTransactionTracker', () => { listener, ); - // Attempt 1 + // First poll - transaction not found, time hasn't elapsed getTransactionReceiptMock.mockResolvedValueOnce(undefined); getTransactionCountMock.mockResolvedValueOnce('0x3'); getTransactionByHashMock.mockResolvedValueOnce(null); + jest.advanceTimersByTime(10000); // Advance 10 seconds + await onPoll(); expect(listener).toHaveBeenCalledTimes(0); - // Attempt 2 + // Second poll - still under timeout (threshold=3, blockTime=12000ms, timeout=36000ms) getTransactionReceiptMock.mockResolvedValueOnce(undefined); getTransactionCountMock.mockResolvedValueOnce('0x3'); getTransactionByHashMock.mockResolvedValueOnce(null); + jest.advanceTimersByTime(20000); // Advance 20 more seconds (total: 30 seconds) + await onPoll(); expect(listener).toHaveBeenCalledTimes(0); - // Attempt 3 - should fail + // Third poll - should fail as we exceed timeout threshold getTransactionReceiptMock.mockResolvedValueOnce(undefined); getTransactionCountMock.mockResolvedValueOnce('0x3'); getTransactionByHashMock.mockResolvedValueOnce(null); - await onPoll(); - - expect(listener).toHaveBeenCalledTimes(1); - expect(listener).toHaveBeenCalledWith( - TRANSACTION_SUBMITTED_MOCK, - new Error('Transaction not found on network after timeout'), - ); - }); - - it('with custom threshold value', async () => { - const listener = jest.fn(); - - mockFeatureFlags(messenger, { - confirmations_transactions: { - timeoutAttempts: { - default: 5, - }, - }, - }); - - pendingTransactionTracker = new PendingTransactionTracker({ - ...options, - getTransactions: (): TransactionMeta[] => - freeze([{ ...TRANSACTION_SUBMITTED_MOCK }], true), - }); - - pendingTransactionTracker.hub.addListener( - 'transaction-failed', - listener, - ); - - for (let i = 0; i < 4; i++) { - getTransactionReceiptMock.mockResolvedValueOnce(undefined); - getTransactionCountMock.mockResolvedValueOnce('0x3'); - getTransactionByHashMock.mockResolvedValueOnce(null); - - await onPoll(); - expect(listener).toHaveBeenCalledTimes(0); - } - - // Attempt 5 - should fail - getTransactionReceiptMock.mockResolvedValueOnce(undefined); - getTransactionCountMock.mockResolvedValueOnce('0x3'); - getTransactionByHashMock.mockResolvedValueOnce(null); + jest.advanceTimersByTime(10000); // Advance 10 more seconds (total: 40 seconds > 36 seconds) await onPoll(); expect(listener).toHaveBeenCalledTimes(1); expect(listener).toHaveBeenCalledWith( - TRANSACTION_SUBMITTED_MOCK, + expect.objectContaining(TRANSACTION_SUBMITTED_MOCK), new Error('Transaction not found on network after timeout'), ); }); @@ -1676,19 +1673,12 @@ describe('PendingTransactionTracker', () => { describe('error handling', () => { it('does not fail transaction if getTransactionByHash throws error', async () => { const listener = jest.fn(); - - mockFeatureFlags(messenger, { - confirmations_transactions: { - timeoutAttempts: { - default: 3, - }, - }, - }); + const submittedTime = Date.now(); pendingTransactionTracker = new PendingTransactionTracker({ ...options, getTransactions: (): TransactionMeta[] => - freeze([{ ...TRANSACTION_SUBMITTED_MOCK }], true), + freeze([{ ...TRANSACTION_SUBMITTED_MOCK, submittedTime }], true), }); pendingTransactionTracker.hub.addListener( @@ -1702,6 +1692,8 @@ describe('PendingTransactionTracker', () => { new Error('Network error'), ); + jest.advanceTimersByTime(50000); // Advance past timeout threshold + await onPoll(); expect(listener).toHaveBeenCalledTimes(0); diff --git a/packages/transaction-controller/src/helpers/PendingTransactionTracker.ts b/packages/transaction-controller/src/helpers/PendingTransactionTracker.ts index eb7b280b4e8..fb1673eaeea 100644 --- a/packages/transaction-controller/src/helpers/PendingTransactionTracker.ts +++ b/packages/transaction-controller/src/helpers/PendingTransactionTracker.ts @@ -15,7 +15,10 @@ import { createModuleLogger, projectLogger } from '../logger'; import type { TransactionControllerMessenger } from '../TransactionController'; import type { TransactionMeta, TransactionReceipt } from '../types'; import { TransactionStatus, TransactionType } from '../types'; -import { getTimeoutAttempts } from '../utils/feature-flags'; +import { + getAcceleratedPollingParams, + getTimeoutAttempts, +} from '../utils/feature-flags'; /** * We wait this many blocks before emitting a 'transaction-dropped' event @@ -88,6 +91,8 @@ export class PendingTransactionTracker { readonly #isResubmitEnabled: () => boolean; + readonly #lastSeenTimestampByHash: Map; + // TODO: Replace `any` with type // eslint-disable-next-line @typescript-eslint/no-explicit-any readonly #listener: any; @@ -103,8 +108,6 @@ export class PendingTransactionTracker { #running: boolean; - readonly #timeoutCountByHash: Map; - readonly #transactionPoller: TransactionPoller; #transactionToForcePoll: TransactionMeta | undefined; @@ -150,11 +153,11 @@ export class PendingTransactionTracker { this.#getNetworkClientId = getNetworkClientId; this.#getTransactions = getTransactions; this.#isResubmitEnabled = isResubmitEnabled ?? ((): boolean => true); + this.#lastSeenTimestampByHash = new Map(); this.#listener = this.#onLatestBlock.bind(this); this.#messenger = messenger; this.#publishTransaction = publishTransaction; this.#running = false; - this.#timeoutCountByHash = new Map(); this.#transactionToForcePoll = undefined; this.#transactionPoller = new TransactionPoller({ @@ -391,10 +394,16 @@ export class PendingTransactionTracker { return blocksSinceFirstRetry >= requiredBlocksSinceFirstRetry; } - #cleanTransactionToForcePoll(transactionId: string): void { - if (this.#transactionToForcePoll?.id === transactionId) { + #cleanTransaction(txMeta: TransactionMeta): void { + const { hash, id } = txMeta; + + if (this.#transactionToForcePoll?.id === id) { this.#transactionToForcePoll = undefined; } + + if (hash) { + this.#lastSeenTimestampByHash.delete(hash); + } } async #checkTransaction(txMeta: TransactionMeta): Promise { @@ -489,8 +498,11 @@ export class PendingTransactionTracker { this.#log('Transaction confirmed', id); - if (this.#transactionToForcePoll) { - this.#cleanTransactionToForcePoll(txMeta.id); + const isForcePollTransaction = this.#transactionToForcePoll?.id === id; + + this.#cleanTransaction(txMeta); + + if (isForcePollTransaction) { this.hub.emit('transaction-confirmed', txMeta); return; } @@ -529,6 +541,7 @@ export class PendingTransactionTracker { chainId, hash, id: transactionId, + submittedTime, txParams: { nonce }, } = txMeta; @@ -564,36 +577,54 @@ export class PendingTransactionTracker { // Check if transaction exists on the network const transaction = await this.#getTransactionByHash(hash); - // If transaction exists, reset the counter + // If transaction exists, record the timestamp if (transaction !== null) { + const currentTimestamp = Date.now(); + this.#log( - 'Transaction found on network, resetting timeout counter', + 'Transaction found on network, recording timestamp', transactionId, ); - this.#timeoutCountByHash.delete(hash); + this.#lastSeenTimestampByHash.set(hash, currentTimestamp); return false; } - // Transaction doesn't exist, increment counter - let attempts = this.#timeoutCountByHash.get(hash); + const lastSeenTimestamp = + this.#lastSeenTimestampByHash.get(hash) ?? submittedTime; + + if (lastSeenTimestamp === undefined) { + this.#log( + 'Transaction not yet seen on network and has no submitted time, skipping timeout check', + transactionId, + ); + + return false; + } + + const { blockTime } = getAcceleratedPollingParams( + chainId, + this.#messenger, + ); - attempts ??= 0; - attempts += 1; - this.#timeoutCountByHash.set(hash, attempts); + const currentTimestamp = Date.now(); + const durationSinceLastSeen = currentTimestamp - lastSeenTimestamp; + const timeoutDuration = blockTime * threshold; - this.#log('Incrementing timeout counter', { + this.#log('Checking timeout duration', { transactionId, - attempts, + durationSinceLastSeen, + timeoutDuration, threshold, + blockTime, }); - if (attempts < threshold) { + if (durationSinceLastSeen < timeoutDuration) { return false; } - this.#log('Hit timeout threshold', transactionId); - this.#timeoutCountByHash.delete(hash); + this.#log('Hit timeout duration threshold', transactionId); + this.#lastSeenTimestampByHash.delete(hash); this.#failTransaction( txMeta, @@ -687,13 +718,13 @@ export class PendingTransactionTracker { #failTransaction(txMeta: TransactionMeta, error: Error): void { this.#log('Transaction failed', txMeta.id, error); - this.#cleanTransactionToForcePoll(txMeta.id); + this.#cleanTransaction(txMeta); this.hub.emit('transaction-failed', txMeta, error); } #dropTransaction(txMeta: TransactionMeta): void { this.#log('Transaction dropped', txMeta.id); - this.#cleanTransactionToForcePoll(txMeta.id); + this.#cleanTransaction(txMeta); this.hub.emit('transaction-dropped', txMeta); } diff --git a/packages/transaction-controller/src/utils/feature-flags.test.ts b/packages/transaction-controller/src/utils/feature-flags.test.ts index ee03a1d0b38..b89b7a66eb8 100644 --- a/packages/transaction-controller/src/utils/feature-flags.test.ts +++ b/packages/transaction-controller/src/utils/feature-flags.test.ts @@ -359,6 +359,7 @@ describe('Feature Flags Utils', () => { ); expect(params).toStrictEqual({ + blockTime: 12000, countMax: 10, intervalMs: 3000, }); @@ -370,6 +371,7 @@ describe('Feature Flags Utils', () => { acceleratedPolling: { perChainConfig: { [CHAIN_ID_MOCK]: { + blockTime: 15000, countMax: 5, intervalMs: 2000, }, @@ -384,6 +386,7 @@ describe('Feature Flags Utils', () => { ); expect(params).toStrictEqual({ + blockTime: 15000, countMax: 5, intervalMs: 2000, }); @@ -405,6 +408,7 @@ describe('Feature Flags Utils', () => { ); expect(params).toStrictEqual({ + blockTime: 12000, countMax: 15, intervalMs: 4000, }); @@ -418,6 +422,7 @@ describe('Feature Flags Utils', () => { defaultIntervalMs: 4000, perChainConfig: { [CHAIN_ID_MOCK]: { + blockTime: 10000, countMax: 5, intervalMs: 2000, }, @@ -432,6 +437,7 @@ describe('Feature Flags Utils', () => { ); expect(params).toStrictEqual({ + blockTime: 10000, countMax: 5, intervalMs: 2000, }); @@ -445,6 +451,7 @@ describe('Feature Flags Utils', () => { defaultIntervalMs: 4000, perChainConfig: { [CHAIN_ID_2_MOCK]: { + blockTime: 8000, countMax: 5, intervalMs: 2000, }, @@ -459,6 +466,7 @@ describe('Feature Flags Utils', () => { ); expect(params).toStrictEqual({ + blockTime: 12000, countMax: 15, intervalMs: 4000, }); @@ -472,7 +480,7 @@ describe('Feature Flags Utils', () => { defaultIntervalMs: 4000, perChainConfig: { [CHAIN_ID_MOCK]: { - // Only specify countMax, intervalMs should use default + // Only specify countMax, intervalMs and blockTime should use default countMax: 5, }, }, @@ -486,6 +494,7 @@ describe('Feature Flags Utils', () => { ); expect(params).toStrictEqual({ + blockTime: 12000, countMax: 5, intervalMs: 4000, }); diff --git a/packages/transaction-controller/src/utils/feature-flags.ts b/packages/transaction-controller/src/utils/feature-flags.ts index fe9753fedcd..b504f9709d2 100644 --- a/packages/transaction-controller/src/utils/feature-flags.ts +++ b/packages/transaction-controller/src/utils/feature-flags.ts @@ -9,6 +9,7 @@ import type { TransactionControllerMessenger } from '../TransactionController'; const DEFAULT_BATCH_SIZE_LIMIT = 10; const DEFAULT_ACCELERATED_POLLING_COUNT_MAX = 10; const DEFAULT_ACCELERATED_POLLING_INTERVAL_MS = 3 * 1000; +const DEFAULT_BLOCK_TIME = 12 * 1000; const DEFAULT_GAS_ESTIMATE_FALLBACK_BLOCK_PERCENT = 35; const DEFAULT_GAS_ESTIMATE_BUFFER = 1; const DEFAULT_INCOMING_TRANSACTIONS_POLLING_INTERVAL_MS = 1000 * 60 * 4; // 4 Minutes @@ -116,6 +117,11 @@ export type TransactionControllerFeatureFlags = { /** Accelerated polling parameters on a per-chain basis. */ perChainConfig?: { [chainId: Hex]: { + /** + * Block time in milliseconds. + */ + blockTime?: number; + /** * Maximum number of polling requests that can be made in a row, before * the normal polling resumes. @@ -264,7 +270,7 @@ export function getBatchSizeLimit( export function getAcceleratedPollingParams( chainId: Hex, messenger: TransactionControllerMessenger, -): { countMax: number; intervalMs: number } { +): { blockTime: number; countMax: number; intervalMs: number } { const featureFlags = getFeatureFlags(messenger); const acceleratedPollingParams = @@ -280,7 +286,11 @@ export function getAcceleratedPollingParams( acceleratedPollingParams?.defaultIntervalMs ?? DEFAULT_ACCELERATED_POLLING_INTERVAL_MS; - return { countMax, intervalMs }; + const blockTime = + acceleratedPollingParams?.perChainConfig?.[chainId]?.blockTime ?? + DEFAULT_BLOCK_TIME; + + return { blockTime, countMax, intervalMs }; } /**