From 3a17f05968647915923e80d380341fef0e56dd7e Mon Sep 17 00:00:00 2001 From: GustavoRSSilva Date: Mon, 21 Apr 2025 15:22:40 +0100 Subject: [PATCH 1/9] chore: poll timeout --- .../src/bridge-status-controller.ts | 15 +- .../TimedIntervalPollingController.test.ts | 357 ++++++++++++++++++ .../src/TimedIntervalPollingController.ts | 110 ++++++ packages/polling-controller/src/index.ts | 5 + 4 files changed, 480 insertions(+), 7 deletions(-) create mode 100644 packages/polling-controller/src/TimedIntervalPollingController.test.ts create mode 100644 packages/polling-controller/src/TimedIntervalPollingController.ts diff --git a/packages/bridge-status-controller/src/bridge-status-controller.ts b/packages/bridge-status-controller/src/bridge-status-controller.ts index 3398b8b4a68..d6b0c6e6dfc 100644 --- a/packages/bridge-status-controller/src/bridge-status-controller.ts +++ b/packages/bridge-status-controller/src/bridge-status-controller.ts @@ -1,7 +1,7 @@ import type { StateMetadata } from '@metamask/base-controller'; import type { BridgeClientId } from '@metamask/bridge-controller'; import { BRIDGE_PROD_API_BASE_URL } from '@metamask/bridge-controller'; -import { StaticIntervalPollingController } from '@metamask/polling-controller'; +import { TimedIntervalPollingController } from '@metamask/polling-controller'; import { numberToHex, type Hex } from '@metamask/utils'; import { @@ -9,11 +9,12 @@ import { DEFAULT_BRIDGE_STATUS_CONTROLLER_STATE, REFRESH_INTERVAL_MS, } from './constants'; -import { StatusTypes, type BridgeStatusControllerMessenger } from './types'; -import type { - BridgeStatusControllerState, - StartPollingForBridgeTxStatusArgsSerialized, - FetchFunction, +import { + type BridgeStatusControllerState, + type StartPollingForBridgeTxStatusArgsSerialized, + type FetchFunction, + type BridgeStatusControllerMessenger, + StatusTypes, } from './types'; import { fetchBridgeTxStatus, @@ -36,7 +37,7 @@ type SrcTxMetaId = string; export type FetchBridgeTxStatusArgs = { bridgeTxMetaId: string; }; -export class BridgeStatusController extends StaticIntervalPollingController()< +export class BridgeStatusController extends TimedIntervalPollingController()< typeof BRIDGE_STATUS_CONTROLLER_NAME, BridgeStatusControllerState, BridgeStatusControllerMessenger diff --git a/packages/polling-controller/src/TimedIntervalPollingController.test.ts b/packages/polling-controller/src/TimedIntervalPollingController.test.ts new file mode 100644 index 00000000000..baab0d34ea4 --- /dev/null +++ b/packages/polling-controller/src/TimedIntervalPollingController.test.ts @@ -0,0 +1,357 @@ +import { Messenger } from '@metamask/base-controller'; +import { createDeferredPromise } from '@metamask/utils'; +import { getKey } from './AbstractPollingController'; + +import { useFakeTimers } from 'sinon'; + +import { TimedIntervalPollingController } from './TimedIntervalPollingController'; +import { advanceTime } from '../../../tests/helpers'; + +const TICK_TIME = 5; + +type PollingInput = { + networkClientId: string; + address?: string; +}; + +class ChildBlockTrackerPollingController extends TimedIntervalPollingController()< + // TODO: Replace `any` with type + // eslint-disable-next-line @typescript-eslint/no-explicit-any + any, + // TODO: Replace `any` with type + // eslint-disable-next-line @typescript-eslint/no-explicit-any + any, + // TODO: Replace `any` with type + // eslint-disable-next-line @typescript-eslint/no-explicit-any + any +> { + executePollPromises: { + reject: (err: unknown) => void; + resolve: () => void; + }[] = []; + + _executePoll = jest.fn().mockImplementation(() => { + const { promise, reject, resolve } = createDeferredPromise({ + suppressUnhandledRejection: true, + }); + this.executePollPromises.push({ reject, resolve }); + return promise; + }); +} + +describe('StaticIntervalPollingController', () => { + let clock: sinon.SinonFakeTimers; + // TODO: Replace `any` with type + // eslint-disable-next-line @typescript-eslint/no-explicit-any + let mockMessenger: any; + let controller: ChildBlockTrackerPollingController; + beforeEach(() => { + // TODO: Replace `any` with type + // eslint-disable-next-line @typescript-eslint/no-explicit-any + mockMessenger = new Messenger(); + controller = new ChildBlockTrackerPollingController({ + messenger: mockMessenger, + metadata: {}, + name: 'PollingController', + state: { foo: 'bar' }, + }); + controller.setIntervalLength(TICK_TIME); + clock = useFakeTimers(); + }); + + afterEach(() => { + clock.restore(); + }); + + describe('startPolling', () => { + it('should start polling if not already polling', async () => { + controller.startPolling({ networkClientId: 'mainnet' }); + await advanceTime({ clock, duration: 0 }); + expect(controller._executePoll).toHaveBeenCalledTimes(1); + controller.executePollPromises[0].resolve(); + await advanceTime({ clock, duration: TICK_TIME }); + expect(controller._executePoll).toHaveBeenCalledTimes(2); + controller.stopAllPolling(); + }); + + it('should call _executePoll immediately once and continue calling _executePoll on interval when called again with the same networkClientId', async () => { + controller.startPolling({ networkClientId: 'mainnet' }); + await advanceTime({ clock, duration: 0 }); + + controller.startPolling({ networkClientId: 'mainnet' }); + await advanceTime({ clock, duration: 0 }); + + expect(controller._executePoll).toHaveBeenCalledTimes(1); + controller.executePollPromises[0].resolve(); + await advanceTime({ clock, duration: TICK_TIME }); + controller.executePollPromises[1].resolve(); + await advanceTime({ clock, duration: TICK_TIME }); + controller.executePollPromises[2].resolve(); + + expect(controller._executePoll).toHaveBeenCalledTimes(3); + controller.stopAllPolling(); + }); + + describe('multiple networkClientIds', () => { + it('should poll for each networkClientId', async () => { + controller.startPolling({ + networkClientId: 'mainnet', + }); + await advanceTime({ clock, duration: 0 }); + + controller.startPolling({ + networkClientId: 'rinkeby', + }); + await advanceTime({ clock, duration: 0 }); + + expect(controller._executePoll.mock.calls).toMatchObject([ + [{ networkClientId: 'mainnet' }], + [{ networkClientId: 'rinkeby' }], + ]); + + controller.executePollPromises[0].resolve(); + controller.executePollPromises[1].resolve(); + await advanceTime({ clock, duration: TICK_TIME }); + + expect(controller._executePoll.mock.calls).toMatchObject([ + [{ networkClientId: 'mainnet' }], + [{ networkClientId: 'rinkeby' }], + [{ networkClientId: 'mainnet' }], + [{ networkClientId: 'rinkeby' }], + ]); + + controller.executePollPromises[2].resolve(); + controller.executePollPromises[3].resolve(); + await advanceTime({ clock, duration: TICK_TIME }); + + expect(controller._executePoll.mock.calls).toMatchObject([ + [{ networkClientId: 'mainnet' }], + [{ networkClientId: 'rinkeby' }], + [{ networkClientId: 'mainnet' }], + [{ networkClientId: 'rinkeby' }], + [{ networkClientId: 'mainnet' }], + [{ networkClientId: 'rinkeby' }], + ]); + controller.stopAllPolling(); + }); + + it('should poll multiple networkClientIds when setting interval length', async () => { + controller.setIntervalLength(TICK_TIME * 2); + controller.startPolling({ + networkClientId: 'mainnet', + }); + await advanceTime({ clock, duration: 0 }); + + expect(controller._executePoll.mock.calls).toMatchObject([ + [{ networkClientId: 'mainnet' }], + ]); + controller.executePollPromises[0].resolve(); + await advanceTime({ clock, duration: TICK_TIME }); + + controller.startPolling({ + networkClientId: 'sepolia', + }); + await advanceTime({ clock, duration: 0 }); + + expect(controller._executePoll.mock.calls).toMatchObject([ + [{ networkClientId: 'mainnet' }], + [{ networkClientId: 'sepolia' }], + ]); + + controller.executePollPromises[1].resolve(); + await advanceTime({ clock, duration: TICK_TIME }); + + expect(controller._executePoll.mock.calls).toMatchObject([ + [{ networkClientId: 'mainnet' }], + [{ networkClientId: 'sepolia' }], + [{ networkClientId: 'mainnet' }], + ]); + + controller.executePollPromises[2].resolve(); + await advanceTime({ clock, duration: TICK_TIME }); + + expect(controller._executePoll.mock.calls).toMatchObject([ + [{ networkClientId: 'mainnet' }], + [{ networkClientId: 'sepolia' }], + [{ networkClientId: 'mainnet' }], + [{ networkClientId: 'sepolia' }], + ]); + + controller.executePollPromises[3].resolve(); + await advanceTime({ clock, duration: TICK_TIME }); + + expect(controller._executePoll.mock.calls).toMatchObject([ + [{ networkClientId: 'mainnet' }], + [{ networkClientId: 'sepolia' }], + [{ networkClientId: 'mainnet' }], + [{ networkClientId: 'sepolia' }], + [{ networkClientId: 'mainnet' }], + ]); + + controller.executePollPromises[4].resolve(); + await advanceTime({ clock, duration: TICK_TIME }); + + expect(controller._executePoll.mock.calls).toMatchObject([ + [{ networkClientId: 'mainnet' }], + [{ networkClientId: 'sepolia' }], + [{ networkClientId: 'mainnet' }], + [{ networkClientId: 'sepolia' }], + [{ networkClientId: 'mainnet' }], + [{ networkClientId: 'sepolia' }], + ]); + }); + }); + }); + + describe('stopPollingByPollingToken', () => { + it('should stop polling when called with a valid polling that was the only active pollingToken for a given networkClient', async () => { + const pollingToken = controller.startPolling({ + networkClientId: 'mainnet', + }); + await advanceTime({ clock, duration: 0 }); + expect(controller._executePoll).toHaveBeenCalledTimes(1); + controller.executePollPromises[0].resolve(); + await advanceTime({ clock, duration: TICK_TIME }); + controller.stopPollingByPollingToken(pollingToken); + await advanceTime({ clock, duration: TICK_TIME }); + expect(controller._executePoll).toHaveBeenCalledTimes(2); + controller.stopAllPolling(); + }); + + it('should not stop polling if called with one of multiple active polling tokens for a given networkClient', async () => { + const pollingToken1 = controller.startPolling({ + networkClientId: 'mainnet', + }); + await advanceTime({ clock, duration: 0 }); + + controller.startPolling({ networkClientId: 'mainnet' }); + expect(controller._executePoll).toHaveBeenCalledTimes(1); + controller.executePollPromises[0].resolve(); + await advanceTime({ clock, duration: TICK_TIME }); + controller.stopPollingByPollingToken(pollingToken1); + controller.executePollPromises[1].resolve(); + await advanceTime({ clock, duration: TICK_TIME }); + expect(controller._executePoll).toHaveBeenCalledTimes(3); + controller.stopAllPolling(); + }); + + it('should error if no pollingToken is passed', () => { + controller.startPolling({ networkClientId: 'mainnet' }); + expect(() => { + controller.stopPollingByPollingToken(''); + }).toThrow('pollingToken required'); + controller.stopAllPolling(); + }); + + it('should start and stop polling sessions for different networkClientIds with the same options', async () => { + const pollToken1 = controller.startPolling({ + networkClientId: 'mainnet', + address: '0x1', + }); + await advanceTime({ clock, duration: 0 }); + controller.startPolling({ + networkClientId: 'mainnet', + address: '0x2', + }); + await advanceTime({ clock, duration: 0 }); + + controller.startPolling({ + networkClientId: 'sepolia', + address: '0x2', + }); + await advanceTime({ clock, duration: 0 }); + + expect(controller._executePoll.mock.calls).toMatchObject([ + [{ networkClientId: 'mainnet', address: '0x1' }], + [{ networkClientId: 'mainnet', address: '0x2' }], + [{ networkClientId: 'sepolia', address: '0x2' }], + ]); + + controller.executePollPromises[0].resolve(); + controller.executePollPromises[1].resolve(); + controller.executePollPromises[2].resolve(); + await advanceTime({ clock, duration: TICK_TIME }); + + expect(controller._executePoll.mock.calls).toMatchObject([ + [{ networkClientId: 'mainnet', address: '0x1' }], + [{ networkClientId: 'mainnet', address: '0x2' }], + [{ networkClientId: 'sepolia', address: '0x2' }], + [{ networkClientId: 'mainnet', address: '0x1' }], + [{ networkClientId: 'mainnet', address: '0x2' }], + [{ networkClientId: 'sepolia', address: '0x2' }], + ]); + controller.stopPollingByPollingToken(pollToken1); + controller.executePollPromises[3].resolve(); + controller.executePollPromises[4].resolve(); + controller.executePollPromises[5].resolve(); + await advanceTime({ clock, duration: TICK_TIME }); + + expect(controller._executePoll.mock.calls).toMatchObject([ + [{ networkClientId: 'mainnet', address: '0x1' }], + [{ networkClientId: 'mainnet', address: '0x2' }], + [{ networkClientId: 'sepolia', address: '0x2' }], + [{ networkClientId: 'mainnet', address: '0x1' }], + [{ networkClientId: 'mainnet', address: '0x2' }], + [{ networkClientId: 'sepolia', address: '0x2' }], + [{ networkClientId: 'mainnet', address: '0x2' }], + [{ networkClientId: 'sepolia', address: '0x2' }], + ]); + }); + + it('should stop polling session after current iteration if stop is requested while current iteration is still executing', async () => { + const pollingToken = controller.startPolling({ + networkClientId: 'mainnet', + }); + await advanceTime({ clock, duration: 0 }); + expect(controller._executePoll).toHaveBeenCalledTimes(1); + controller.stopPollingByPollingToken(pollingToken); + controller.executePollPromises[0].resolve(); + await advanceTime({ clock, duration: TICK_TIME }); + expect(controller._executePoll).toHaveBeenCalledTimes(1); + await advanceTime({ clock, duration: TICK_TIME }); + expect(controller._executePoll).toHaveBeenCalledTimes(1); + controller.stopAllPolling(); + }); + + it('should stop polling session if it goes beyond the time', async () => { + const input = { + networkClientId: 'mainnet', + }; + const key = getKey(input); + + controller.startPolling(input); + controller.setKeyDuration(key, TICK_TIME * 2); + + await advanceTime({ clock, duration: 0 }); + expect(controller._executePoll).toHaveBeenCalledTimes(1); + controller.executePollPromises[0].resolve(); + await advanceTime({ clock, duration: TICK_TIME }); + expect(controller._executePoll).toHaveBeenCalledTimes(2); + controller.executePollPromises[1].resolve(); + await advanceTime({ clock, duration: TICK_TIME }); + expect(controller._executePoll).toHaveBeenCalledTimes(2); + await advanceTime({ clock, duration: TICK_TIME }); + expect(controller._executePoll).toHaveBeenCalledTimes(2); + }); + }); + + describe('onPollingCompleteByNetworkClientId', () => { + it('should publish "pollingComplete" callback function set by "onPollingCompleteByNetworkClientId" when polling stops', async () => { + // TODO: Replace `any` with type + // eslint-disable-next-line @typescript-eslint/no-explicit-any + const pollingComplete: any = jest.fn(); + controller.onPollingComplete( + { networkClientId: 'mainnet' }, + pollingComplete, + ); + const pollingToken = controller.startPolling({ + networkClientId: 'mainnet', + }); + controller.stopPollingByPollingToken(pollingToken); + expect(pollingComplete).toHaveBeenCalledTimes(1); + expect(pollingComplete).toHaveBeenCalledWith({ + networkClientId: 'mainnet', + }); + }); + }); +}); diff --git a/packages/polling-controller/src/TimedIntervalPollingController.ts b/packages/polling-controller/src/TimedIntervalPollingController.ts new file mode 100644 index 00000000000..6e93ba3643d --- /dev/null +++ b/packages/polling-controller/src/TimedIntervalPollingController.ts @@ -0,0 +1,110 @@ +import { BaseController } from '@metamask/base-controller'; +import type { Json } from '@metamask/utils'; + +import { + AbstractPollingControllerBaseMixin, + getKey, +} from './AbstractPollingController'; +import type { + Constructor, + IPollingController, + PollingTokenSetId, +} from './types'; + +/** + * TimedIntervalPollingControllerMixin + * A polling controller that polls on a static interval for a duration of time. + * You will set the interval length (poll every interval of time) and a total duration of time (poll for a duration of time). + * + * @param Base - The base class to mix onto. + * @returns The composed class. + */ +function TimedIntervalPollingControllerMixin< + TBase extends Constructor, + PollingInput extends Json, +>(Base: TBase) { + abstract class TimedIntervalPollingController + extends AbstractPollingControllerBaseMixin(Base) + implements IPollingController + { + readonly #intervalIds: Record = {}; + + #durationIds: Record = {}; + + #intervalLength: number | undefined = 1000; + + setIntervalLength(intervalLength: number) { + this.#intervalLength = intervalLength; + } + + getIntervalLength() { + return this.#intervalLength; + } + + setKeyDuration(key: string, duration: number) { + this.#durationIds[key] = duration; + } + + getKeyDuration(key: string) { + return this.#durationIds[key]; + } + + getDurationToPoll() { + return this.#intervalLength; + } + + _startPolling(input: PollingInput) { + if (!this.#intervalLength) { + throw new Error('intervalLength must be defined and greater than 0'); + } + + const key = getKey(input); + const existingInterval = this.#intervalIds[key]; + this._stopPollingByPollingTokenSetId(key); + + // eslint-disable-next-line no-multi-assign + const intervalId = (this.#intervalIds[key] = setTimeout( + // TODO: Either fix this lint violation or explain why it's necessary to ignore. + // eslint-disable-next-line @typescript-eslint/no-misused-promises + async () => { + if (this.#durationIds[key] && Date.now() >= this.#durationIds[key]) { + this._stopPollingByPollingTokenSetId(key); + delete this.#durationIds[key]; + return; + } + + try { + await this._executePoll(input); + } catch (error) { + console.error(error); + } + if (intervalId === this.#intervalIds[key]) { + this._startPolling(input); + } + }, + existingInterval ? this.#intervalLength : 0, + )); + } + + _stopPollingByPollingTokenSetId(key: PollingTokenSetId) { + const intervalId = this.#intervalIds[key]; + if (intervalId) { + clearTimeout(intervalId); + delete this.#intervalIds[key]; + } + } + } + + return TimedIntervalPollingController; +} + +class Empty {} + +export const TimedIntervalPollingControllerOnly = < + PollingInput extends Json, +>() => TimedIntervalPollingControllerMixin(Empty); + +export const TimedIntervalPollingController = () => + TimedIntervalPollingControllerMixin( + BaseController, + ); diff --git a/packages/polling-controller/src/index.ts b/packages/polling-controller/src/index.ts index ba1758c443b..a1c0e1639c5 100644 --- a/packages/polling-controller/src/index.ts +++ b/packages/polling-controller/src/index.ts @@ -8,4 +8,9 @@ export { StaticIntervalPollingController, } from './StaticIntervalPollingController'; +export { + TimedIntervalPollingControllerOnly, + TimedIntervalPollingController, +} from './TimedIntervalPollingController'; + export type { IPollingController } from './types'; From c99ba1fed82d9475f651d8a8c760a367f73756f0 Mon Sep 17 00:00:00 2001 From: GustavoRSSilva Date: Mon, 21 Apr 2025 15:35:57 +0100 Subject: [PATCH 2/9] chore: bridge polling duration --- .../src/bridge-status-controller.ts | 24 +- .../bridge-status-controller/src/constants.ts | 4 + .../StaticIntervalPollingController.test.ts | 22 ++ .../src/StaticIntervalPollingController.ts | 16 + .../TimedIntervalPollingController.test.ts | 357 ------------------ .../src/TimedIntervalPollingController.ts | 110 ------ packages/polling-controller/src/index.ts | 5 - 7 files changed, 59 insertions(+), 479 deletions(-) delete mode 100644 packages/polling-controller/src/TimedIntervalPollingController.test.ts delete mode 100644 packages/polling-controller/src/TimedIntervalPollingController.ts diff --git a/packages/bridge-status-controller/src/bridge-status-controller.ts b/packages/bridge-status-controller/src/bridge-status-controller.ts index d6b0c6e6dfc..7069fff21c6 100644 --- a/packages/bridge-status-controller/src/bridge-status-controller.ts +++ b/packages/bridge-status-controller/src/bridge-status-controller.ts @@ -1,13 +1,14 @@ import type { StateMetadata } from '@metamask/base-controller'; import type { BridgeClientId } from '@metamask/bridge-controller'; import { BRIDGE_PROD_API_BASE_URL } from '@metamask/bridge-controller'; -import { TimedIntervalPollingController } from '@metamask/polling-controller'; +import { StaticIntervalPollingController } from '@metamask/polling-controller'; import { numberToHex, type Hex } from '@metamask/utils'; import { BRIDGE_STATUS_CONTROLLER_NAME, DEFAULT_BRIDGE_STATUS_CONTROLLER_STATE, REFRESH_INTERVAL_MS, + POLLING_DURATION, } from './constants'; import { type BridgeStatusControllerState, @@ -20,6 +21,8 @@ import { fetchBridgeTxStatus, getStatusRequestWithSrcTxHash, } from './utils/bridge-status'; +import { getKey } from '../../polling-controller/src/AbstractPollingController'; +import { get } from 'lodash'; const metadata: StateMetadata = { // We want to persist the bridge status state so that we can show the proper data for the Activity list @@ -37,7 +40,7 @@ type SrcTxMetaId = string; export type FetchBridgeTxStatusArgs = { bridgeTxMetaId: string; }; -export class BridgeStatusController extends TimedIntervalPollingController()< +export class BridgeStatusController extends StaticIntervalPollingController()< typeof BRIDGE_STATUS_CONTROLLER_NAME, BridgeStatusControllerState, BridgeStatusControllerMessenger @@ -157,11 +160,13 @@ export class BridgeStatusController extends TimedIntervalPollingController { const bridgeTxMetaId = historyItem.txMetaId; + const input = { + bridgeTxMetaId, + }; // We manually call startPolling() here rather than go through startPollingForBridgeTxStatus() // because we don't want to overwrite the existing historyItem in state - this.#pollingTokensByTxMetaId[bridgeTxMetaId] = this.startPolling({ - bridgeTxMetaId, - }); + this.#pollingTokensByTxMetaId[bridgeTxMetaId] = this.startPolling(input); + this.setKeyDuration(getKey(input), POLLING_DURATION); }); }; @@ -212,9 +217,14 @@ export class BridgeStatusController extends TimedIntervalPollingController { expect(controller._executePoll).toHaveBeenCalledTimes(1); controller.stopAllPolling(); }); + + it('should stop polling session if the key is assigned duration and goes beyond it', async () => { + const input = { + networkClientId: 'mainnet', + }; + const key = getKey(input); + + controller.startPolling(input); + controller.setKeyDuration(key, TICK_TIME * 2); + + await advanceTime({ clock, duration: 0 }); + expect(controller._executePoll).toHaveBeenCalledTimes(1); + controller.executePollPromises[0].resolve(); + await advanceTime({ clock, duration: TICK_TIME }); + expect(controller._executePoll).toHaveBeenCalledTimes(2); + controller.executePollPromises[1].resolve(); + await advanceTime({ clock, duration: TICK_TIME }); + expect(controller._executePoll).toHaveBeenCalledTimes(2); + await advanceTime({ clock, duration: TICK_TIME }); + expect(controller._executePoll).toHaveBeenCalledTimes(2); + }); }); describe('onPollingCompleteByNetworkClientId', () => { diff --git a/packages/polling-controller/src/StaticIntervalPollingController.ts b/packages/polling-controller/src/StaticIntervalPollingController.ts index 5076dfcffdf..e9f16fc8815 100644 --- a/packages/polling-controller/src/StaticIntervalPollingController.ts +++ b/packages/polling-controller/src/StaticIntervalPollingController.ts @@ -30,8 +30,18 @@ function StaticIntervalPollingControllerMixin< { readonly #intervalIds: Record = {}; + #durationIds: Record = {}; + #intervalLength: number | undefined = 1000; + setKeyDuration(key: string, duration: number) { + this.#durationIds[key] = duration; + } + + getKeyDuration(key: string) { + return this.#durationIds[key]; + } + setIntervalLength(intervalLength: number) { this.#intervalLength = intervalLength; } @@ -54,6 +64,12 @@ function StaticIntervalPollingControllerMixin< // TODO: Either fix this lint violation or explain why it's necessary to ignore. // eslint-disable-next-line @typescript-eslint/no-misused-promises async () => { + if (this.#durationIds[key] && Date.now() >= this.#durationIds[key]) { + this._stopPollingByPollingTokenSetId(key); + delete this.#durationIds[key]; + return; + } + try { await this._executePoll(input); } catch (error) { diff --git a/packages/polling-controller/src/TimedIntervalPollingController.test.ts b/packages/polling-controller/src/TimedIntervalPollingController.test.ts deleted file mode 100644 index baab0d34ea4..00000000000 --- a/packages/polling-controller/src/TimedIntervalPollingController.test.ts +++ /dev/null @@ -1,357 +0,0 @@ -import { Messenger } from '@metamask/base-controller'; -import { createDeferredPromise } from '@metamask/utils'; -import { getKey } from './AbstractPollingController'; - -import { useFakeTimers } from 'sinon'; - -import { TimedIntervalPollingController } from './TimedIntervalPollingController'; -import { advanceTime } from '../../../tests/helpers'; - -const TICK_TIME = 5; - -type PollingInput = { - networkClientId: string; - address?: string; -}; - -class ChildBlockTrackerPollingController extends TimedIntervalPollingController()< - // TODO: Replace `any` with type - // eslint-disable-next-line @typescript-eslint/no-explicit-any - any, - // TODO: Replace `any` with type - // eslint-disable-next-line @typescript-eslint/no-explicit-any - any, - // TODO: Replace `any` with type - // eslint-disable-next-line @typescript-eslint/no-explicit-any - any -> { - executePollPromises: { - reject: (err: unknown) => void; - resolve: () => void; - }[] = []; - - _executePoll = jest.fn().mockImplementation(() => { - const { promise, reject, resolve } = createDeferredPromise({ - suppressUnhandledRejection: true, - }); - this.executePollPromises.push({ reject, resolve }); - return promise; - }); -} - -describe('StaticIntervalPollingController', () => { - let clock: sinon.SinonFakeTimers; - // TODO: Replace `any` with type - // eslint-disable-next-line @typescript-eslint/no-explicit-any - let mockMessenger: any; - let controller: ChildBlockTrackerPollingController; - beforeEach(() => { - // TODO: Replace `any` with type - // eslint-disable-next-line @typescript-eslint/no-explicit-any - mockMessenger = new Messenger(); - controller = new ChildBlockTrackerPollingController({ - messenger: mockMessenger, - metadata: {}, - name: 'PollingController', - state: { foo: 'bar' }, - }); - controller.setIntervalLength(TICK_TIME); - clock = useFakeTimers(); - }); - - afterEach(() => { - clock.restore(); - }); - - describe('startPolling', () => { - it('should start polling if not already polling', async () => { - controller.startPolling({ networkClientId: 'mainnet' }); - await advanceTime({ clock, duration: 0 }); - expect(controller._executePoll).toHaveBeenCalledTimes(1); - controller.executePollPromises[0].resolve(); - await advanceTime({ clock, duration: TICK_TIME }); - expect(controller._executePoll).toHaveBeenCalledTimes(2); - controller.stopAllPolling(); - }); - - it('should call _executePoll immediately once and continue calling _executePoll on interval when called again with the same networkClientId', async () => { - controller.startPolling({ networkClientId: 'mainnet' }); - await advanceTime({ clock, duration: 0 }); - - controller.startPolling({ networkClientId: 'mainnet' }); - await advanceTime({ clock, duration: 0 }); - - expect(controller._executePoll).toHaveBeenCalledTimes(1); - controller.executePollPromises[0].resolve(); - await advanceTime({ clock, duration: TICK_TIME }); - controller.executePollPromises[1].resolve(); - await advanceTime({ clock, duration: TICK_TIME }); - controller.executePollPromises[2].resolve(); - - expect(controller._executePoll).toHaveBeenCalledTimes(3); - controller.stopAllPolling(); - }); - - describe('multiple networkClientIds', () => { - it('should poll for each networkClientId', async () => { - controller.startPolling({ - networkClientId: 'mainnet', - }); - await advanceTime({ clock, duration: 0 }); - - controller.startPolling({ - networkClientId: 'rinkeby', - }); - await advanceTime({ clock, duration: 0 }); - - expect(controller._executePoll.mock.calls).toMatchObject([ - [{ networkClientId: 'mainnet' }], - [{ networkClientId: 'rinkeby' }], - ]); - - controller.executePollPromises[0].resolve(); - controller.executePollPromises[1].resolve(); - await advanceTime({ clock, duration: TICK_TIME }); - - expect(controller._executePoll.mock.calls).toMatchObject([ - [{ networkClientId: 'mainnet' }], - [{ networkClientId: 'rinkeby' }], - [{ networkClientId: 'mainnet' }], - [{ networkClientId: 'rinkeby' }], - ]); - - controller.executePollPromises[2].resolve(); - controller.executePollPromises[3].resolve(); - await advanceTime({ clock, duration: TICK_TIME }); - - expect(controller._executePoll.mock.calls).toMatchObject([ - [{ networkClientId: 'mainnet' }], - [{ networkClientId: 'rinkeby' }], - [{ networkClientId: 'mainnet' }], - [{ networkClientId: 'rinkeby' }], - [{ networkClientId: 'mainnet' }], - [{ networkClientId: 'rinkeby' }], - ]); - controller.stopAllPolling(); - }); - - it('should poll multiple networkClientIds when setting interval length', async () => { - controller.setIntervalLength(TICK_TIME * 2); - controller.startPolling({ - networkClientId: 'mainnet', - }); - await advanceTime({ clock, duration: 0 }); - - expect(controller._executePoll.mock.calls).toMatchObject([ - [{ networkClientId: 'mainnet' }], - ]); - controller.executePollPromises[0].resolve(); - await advanceTime({ clock, duration: TICK_TIME }); - - controller.startPolling({ - networkClientId: 'sepolia', - }); - await advanceTime({ clock, duration: 0 }); - - expect(controller._executePoll.mock.calls).toMatchObject([ - [{ networkClientId: 'mainnet' }], - [{ networkClientId: 'sepolia' }], - ]); - - controller.executePollPromises[1].resolve(); - await advanceTime({ clock, duration: TICK_TIME }); - - expect(controller._executePoll.mock.calls).toMatchObject([ - [{ networkClientId: 'mainnet' }], - [{ networkClientId: 'sepolia' }], - [{ networkClientId: 'mainnet' }], - ]); - - controller.executePollPromises[2].resolve(); - await advanceTime({ clock, duration: TICK_TIME }); - - expect(controller._executePoll.mock.calls).toMatchObject([ - [{ networkClientId: 'mainnet' }], - [{ networkClientId: 'sepolia' }], - [{ networkClientId: 'mainnet' }], - [{ networkClientId: 'sepolia' }], - ]); - - controller.executePollPromises[3].resolve(); - await advanceTime({ clock, duration: TICK_TIME }); - - expect(controller._executePoll.mock.calls).toMatchObject([ - [{ networkClientId: 'mainnet' }], - [{ networkClientId: 'sepolia' }], - [{ networkClientId: 'mainnet' }], - [{ networkClientId: 'sepolia' }], - [{ networkClientId: 'mainnet' }], - ]); - - controller.executePollPromises[4].resolve(); - await advanceTime({ clock, duration: TICK_TIME }); - - expect(controller._executePoll.mock.calls).toMatchObject([ - [{ networkClientId: 'mainnet' }], - [{ networkClientId: 'sepolia' }], - [{ networkClientId: 'mainnet' }], - [{ networkClientId: 'sepolia' }], - [{ networkClientId: 'mainnet' }], - [{ networkClientId: 'sepolia' }], - ]); - }); - }); - }); - - describe('stopPollingByPollingToken', () => { - it('should stop polling when called with a valid polling that was the only active pollingToken for a given networkClient', async () => { - const pollingToken = controller.startPolling({ - networkClientId: 'mainnet', - }); - await advanceTime({ clock, duration: 0 }); - expect(controller._executePoll).toHaveBeenCalledTimes(1); - controller.executePollPromises[0].resolve(); - await advanceTime({ clock, duration: TICK_TIME }); - controller.stopPollingByPollingToken(pollingToken); - await advanceTime({ clock, duration: TICK_TIME }); - expect(controller._executePoll).toHaveBeenCalledTimes(2); - controller.stopAllPolling(); - }); - - it('should not stop polling if called with one of multiple active polling tokens for a given networkClient', async () => { - const pollingToken1 = controller.startPolling({ - networkClientId: 'mainnet', - }); - await advanceTime({ clock, duration: 0 }); - - controller.startPolling({ networkClientId: 'mainnet' }); - expect(controller._executePoll).toHaveBeenCalledTimes(1); - controller.executePollPromises[0].resolve(); - await advanceTime({ clock, duration: TICK_TIME }); - controller.stopPollingByPollingToken(pollingToken1); - controller.executePollPromises[1].resolve(); - await advanceTime({ clock, duration: TICK_TIME }); - expect(controller._executePoll).toHaveBeenCalledTimes(3); - controller.stopAllPolling(); - }); - - it('should error if no pollingToken is passed', () => { - controller.startPolling({ networkClientId: 'mainnet' }); - expect(() => { - controller.stopPollingByPollingToken(''); - }).toThrow('pollingToken required'); - controller.stopAllPolling(); - }); - - it('should start and stop polling sessions for different networkClientIds with the same options', async () => { - const pollToken1 = controller.startPolling({ - networkClientId: 'mainnet', - address: '0x1', - }); - await advanceTime({ clock, duration: 0 }); - controller.startPolling({ - networkClientId: 'mainnet', - address: '0x2', - }); - await advanceTime({ clock, duration: 0 }); - - controller.startPolling({ - networkClientId: 'sepolia', - address: '0x2', - }); - await advanceTime({ clock, duration: 0 }); - - expect(controller._executePoll.mock.calls).toMatchObject([ - [{ networkClientId: 'mainnet', address: '0x1' }], - [{ networkClientId: 'mainnet', address: '0x2' }], - [{ networkClientId: 'sepolia', address: '0x2' }], - ]); - - controller.executePollPromises[0].resolve(); - controller.executePollPromises[1].resolve(); - controller.executePollPromises[2].resolve(); - await advanceTime({ clock, duration: TICK_TIME }); - - expect(controller._executePoll.mock.calls).toMatchObject([ - [{ networkClientId: 'mainnet', address: '0x1' }], - [{ networkClientId: 'mainnet', address: '0x2' }], - [{ networkClientId: 'sepolia', address: '0x2' }], - [{ networkClientId: 'mainnet', address: '0x1' }], - [{ networkClientId: 'mainnet', address: '0x2' }], - [{ networkClientId: 'sepolia', address: '0x2' }], - ]); - controller.stopPollingByPollingToken(pollToken1); - controller.executePollPromises[3].resolve(); - controller.executePollPromises[4].resolve(); - controller.executePollPromises[5].resolve(); - await advanceTime({ clock, duration: TICK_TIME }); - - expect(controller._executePoll.mock.calls).toMatchObject([ - [{ networkClientId: 'mainnet', address: '0x1' }], - [{ networkClientId: 'mainnet', address: '0x2' }], - [{ networkClientId: 'sepolia', address: '0x2' }], - [{ networkClientId: 'mainnet', address: '0x1' }], - [{ networkClientId: 'mainnet', address: '0x2' }], - [{ networkClientId: 'sepolia', address: '0x2' }], - [{ networkClientId: 'mainnet', address: '0x2' }], - [{ networkClientId: 'sepolia', address: '0x2' }], - ]); - }); - - it('should stop polling session after current iteration if stop is requested while current iteration is still executing', async () => { - const pollingToken = controller.startPolling({ - networkClientId: 'mainnet', - }); - await advanceTime({ clock, duration: 0 }); - expect(controller._executePoll).toHaveBeenCalledTimes(1); - controller.stopPollingByPollingToken(pollingToken); - controller.executePollPromises[0].resolve(); - await advanceTime({ clock, duration: TICK_TIME }); - expect(controller._executePoll).toHaveBeenCalledTimes(1); - await advanceTime({ clock, duration: TICK_TIME }); - expect(controller._executePoll).toHaveBeenCalledTimes(1); - controller.stopAllPolling(); - }); - - it('should stop polling session if it goes beyond the time', async () => { - const input = { - networkClientId: 'mainnet', - }; - const key = getKey(input); - - controller.startPolling(input); - controller.setKeyDuration(key, TICK_TIME * 2); - - await advanceTime({ clock, duration: 0 }); - expect(controller._executePoll).toHaveBeenCalledTimes(1); - controller.executePollPromises[0].resolve(); - await advanceTime({ clock, duration: TICK_TIME }); - expect(controller._executePoll).toHaveBeenCalledTimes(2); - controller.executePollPromises[1].resolve(); - await advanceTime({ clock, duration: TICK_TIME }); - expect(controller._executePoll).toHaveBeenCalledTimes(2); - await advanceTime({ clock, duration: TICK_TIME }); - expect(controller._executePoll).toHaveBeenCalledTimes(2); - }); - }); - - describe('onPollingCompleteByNetworkClientId', () => { - it('should publish "pollingComplete" callback function set by "onPollingCompleteByNetworkClientId" when polling stops', async () => { - // TODO: Replace `any` with type - // eslint-disable-next-line @typescript-eslint/no-explicit-any - const pollingComplete: any = jest.fn(); - controller.onPollingComplete( - { networkClientId: 'mainnet' }, - pollingComplete, - ); - const pollingToken = controller.startPolling({ - networkClientId: 'mainnet', - }); - controller.stopPollingByPollingToken(pollingToken); - expect(pollingComplete).toHaveBeenCalledTimes(1); - expect(pollingComplete).toHaveBeenCalledWith({ - networkClientId: 'mainnet', - }); - }); - }); -}); diff --git a/packages/polling-controller/src/TimedIntervalPollingController.ts b/packages/polling-controller/src/TimedIntervalPollingController.ts deleted file mode 100644 index 6e93ba3643d..00000000000 --- a/packages/polling-controller/src/TimedIntervalPollingController.ts +++ /dev/null @@ -1,110 +0,0 @@ -import { BaseController } from '@metamask/base-controller'; -import type { Json } from '@metamask/utils'; - -import { - AbstractPollingControllerBaseMixin, - getKey, -} from './AbstractPollingController'; -import type { - Constructor, - IPollingController, - PollingTokenSetId, -} from './types'; - -/** - * TimedIntervalPollingControllerMixin - * A polling controller that polls on a static interval for a duration of time. - * You will set the interval length (poll every interval of time) and a total duration of time (poll for a duration of time). - * - * @param Base - The base class to mix onto. - * @returns The composed class. - */ -function TimedIntervalPollingControllerMixin< - TBase extends Constructor, - PollingInput extends Json, ->(Base: TBase) { - abstract class TimedIntervalPollingController - extends AbstractPollingControllerBaseMixin(Base) - implements IPollingController - { - readonly #intervalIds: Record = {}; - - #durationIds: Record = {}; - - #intervalLength: number | undefined = 1000; - - setIntervalLength(intervalLength: number) { - this.#intervalLength = intervalLength; - } - - getIntervalLength() { - return this.#intervalLength; - } - - setKeyDuration(key: string, duration: number) { - this.#durationIds[key] = duration; - } - - getKeyDuration(key: string) { - return this.#durationIds[key]; - } - - getDurationToPoll() { - return this.#intervalLength; - } - - _startPolling(input: PollingInput) { - if (!this.#intervalLength) { - throw new Error('intervalLength must be defined and greater than 0'); - } - - const key = getKey(input); - const existingInterval = this.#intervalIds[key]; - this._stopPollingByPollingTokenSetId(key); - - // eslint-disable-next-line no-multi-assign - const intervalId = (this.#intervalIds[key] = setTimeout( - // TODO: Either fix this lint violation or explain why it's necessary to ignore. - // eslint-disable-next-line @typescript-eslint/no-misused-promises - async () => { - if (this.#durationIds[key] && Date.now() >= this.#durationIds[key]) { - this._stopPollingByPollingTokenSetId(key); - delete this.#durationIds[key]; - return; - } - - try { - await this._executePoll(input); - } catch (error) { - console.error(error); - } - if (intervalId === this.#intervalIds[key]) { - this._startPolling(input); - } - }, - existingInterval ? this.#intervalLength : 0, - )); - } - - _stopPollingByPollingTokenSetId(key: PollingTokenSetId) { - const intervalId = this.#intervalIds[key]; - if (intervalId) { - clearTimeout(intervalId); - delete this.#intervalIds[key]; - } - } - } - - return TimedIntervalPollingController; -} - -class Empty {} - -export const TimedIntervalPollingControllerOnly = < - PollingInput extends Json, ->() => TimedIntervalPollingControllerMixin(Empty); - -export const TimedIntervalPollingController = () => - TimedIntervalPollingControllerMixin( - BaseController, - ); diff --git a/packages/polling-controller/src/index.ts b/packages/polling-controller/src/index.ts index a1c0e1639c5..ba1758c443b 100644 --- a/packages/polling-controller/src/index.ts +++ b/packages/polling-controller/src/index.ts @@ -8,9 +8,4 @@ export { StaticIntervalPollingController, } from './StaticIntervalPollingController'; -export { - TimedIntervalPollingControllerOnly, - TimedIntervalPollingController, -} from './TimedIntervalPollingController'; - export type { IPollingController } from './types'; From 1a4860e282f93e57fe109569a25526ababfb19c8 Mon Sep 17 00:00:00 2001 From: GustavoRSSilva Date: Mon, 21 Apr 2025 15:39:38 +0100 Subject: [PATCH 3/9] chore: bridge polling duration --- .../src/StaticIntervalPollingController.test.ts | 2 +- .../polling-controller/src/StaticIntervalPollingController.ts | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/polling-controller/src/StaticIntervalPollingController.test.ts b/packages/polling-controller/src/StaticIntervalPollingController.test.ts index edf01cfa95a..ad638bda18a 100644 --- a/packages/polling-controller/src/StaticIntervalPollingController.test.ts +++ b/packages/polling-controller/src/StaticIntervalPollingController.test.ts @@ -319,7 +319,7 @@ describe('StaticIntervalPollingController', () => { const key = getKey(input); controller.startPolling(input); - controller.setKeyDuration(key, TICK_TIME * 2); + controller.setKeyDuration(key, TICK_TIME * 2 - 1); await advanceTime({ clock, duration: 0 }); expect(controller._executePoll).toHaveBeenCalledTimes(1); diff --git a/packages/polling-controller/src/StaticIntervalPollingController.ts b/packages/polling-controller/src/StaticIntervalPollingController.ts index e9f16fc8815..4e41b15df73 100644 --- a/packages/polling-controller/src/StaticIntervalPollingController.ts +++ b/packages/polling-controller/src/StaticIntervalPollingController.ts @@ -64,7 +64,7 @@ function StaticIntervalPollingControllerMixin< // TODO: Either fix this lint violation or explain why it's necessary to ignore. // eslint-disable-next-line @typescript-eslint/no-misused-promises async () => { - if (this.#durationIds[key] && Date.now() >= this.#durationIds[key]) { + if (this.#durationIds[key] && Date.now() > this.#durationIds[key]) { this._stopPollingByPollingTokenSetId(key); delete this.#durationIds[key]; return; From 14fd97a0429c4a2cce5e331b4c9a833fa4c6e018 Mon Sep 17 00:00:00 2001 From: GustavoRSSilva Date: Mon, 21 Apr 2025 16:20:18 +0100 Subject: [PATCH 4/9] chore: bridge polling duration --- .../bridge-status-controller/src/bridge-status-controller.ts | 1 - 1 file changed, 1 deletion(-) diff --git a/packages/bridge-status-controller/src/bridge-status-controller.ts b/packages/bridge-status-controller/src/bridge-status-controller.ts index 7069fff21c6..e0932a5faff 100644 --- a/packages/bridge-status-controller/src/bridge-status-controller.ts +++ b/packages/bridge-status-controller/src/bridge-status-controller.ts @@ -22,7 +22,6 @@ import { getStatusRequestWithSrcTxHash, } from './utils/bridge-status'; import { getKey } from '../../polling-controller/src/AbstractPollingController'; -import { get } from 'lodash'; const metadata: StateMetadata = { // We want to persist the bridge status state so that we can show the proper data for the Activity list From 28d3d064a6e9749637b18f5a95cad089f07fe336 Mon Sep 17 00:00:00 2001 From: GustavoRSSilva Date: Mon, 21 Apr 2025 18:07:16 +0100 Subject: [PATCH 5/9] chore: added changelog --- packages/bridge-status-controller/CHANGELOG.md | 1 + packages/polling-controller/CHANGELOG.md | 1 + 2 files changed, 2 insertions(+) diff --git a/packages/bridge-status-controller/CHANGELOG.md b/packages/bridge-status-controller/CHANGELOG.md index 7c747029016..2482db29130 100644 --- a/packages/bridge-status-controller/CHANGELOG.md +++ b/packages/bridge-status-controller/CHANGELOG.md @@ -10,6 +10,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ### Changed - Bump `@metamask/bridge-controller` dependency to `^14.0.0` ([#5657](https://github.com/MetaMask/core/pull/5657)) +- Bridge status controller now sets the polling duration to 5 minutes ([#5683](https://github.com/MetaMask/core/pull/5683)) ## [12.0.1] diff --git a/packages/polling-controller/CHANGELOG.md b/packages/polling-controller/CHANGELOG.md index bb73c25f8f8..7c50e82c459 100644 --- a/packages/polling-controller/CHANGELOG.md +++ b/packages/polling-controller/CHANGELOG.md @@ -10,6 +10,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ### Changed - Bump `@metamask/controller-utils` to `^11.7.0` ([#5583](https://github.com/MetaMask/core/pull/5583)) +- StaticIntervalPollingController now allows to set duration to key ([#5683](https://github.com/MetaMask/core/pull/5683)) ## [13.0.0] From 16c78120efe252671ba15570a4d70d4142af3ee7 Mon Sep 17 00:00:00 2001 From: Gustavo Silva Date: Mon, 21 Apr 2025 22:29:43 +0100 Subject: [PATCH 6/9] Update packages/bridge-status-controller/src/constants.ts Co-authored-by: infiniteflower <139582705+infiniteflower@users.noreply.github.com> --- packages/bridge-status-controller/src/constants.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/bridge-status-controller/src/constants.ts b/packages/bridge-status-controller/src/constants.ts index d1e3bcd9e4b..2ae5d618ff0 100644 --- a/packages/bridge-status-controller/src/constants.ts +++ b/packages/bridge-status-controller/src/constants.ts @@ -2,7 +2,7 @@ import type { BridgeStatusControllerState } from './types'; export const REFRESH_INTERVAL_MS = 10 * 1000; -// The time interval for polling the bridge status API +// The time duration for polling the bridge status API // defaults to 5 minutes export const POLLING_DURATION = 5 * 60 * 1000; From e1f46f6749a724ed80afbdb25182a5a381c778d8 Mon Sep 17 00:00:00 2001 From: GustavoRSSilva Date: Mon, 28 Apr 2025 10:53:05 +0100 Subject: [PATCH 7/9] chore: pool limit --- .../src/bridge-status-controller.test.ts | 6 +++--- .../src/bridge-status-controller.ts | 20 +++++++++---------- packages/polling-controller/src/index.ts | 2 ++ 3 files changed, 14 insertions(+), 14 deletions(-) diff --git a/packages/bridge-status-controller/src/bridge-status-controller.test.ts b/packages/bridge-status-controller/src/bridge-status-controller.test.ts index c1a2bae0de5..0563cafed16 100644 --- a/packages/bridge-status-controller/src/bridge-status-controller.test.ts +++ b/packages/bridge-status-controller/src/bridge-status-controller.test.ts @@ -1,8 +1,7 @@ /* eslint-disable jest/no-conditional-in-test */ /* eslint-disable jest/no-restricted-matchers */ import type { QuoteResponse, QuoteMetadata } from '@metamask/bridge-controller'; -import { ChainId } from '@metamask/bridge-controller'; -import { ActionTypes, FeeType } from '@metamask/bridge-controller'; +import { ChainId, ActionTypes, FeeType } from '@metamask/bridge-controller'; import { EthAccountType } from '@metamask/keyring-api'; import type { TransactionMeta } from '@metamask/transaction-controller'; import { TransactionType } from '@metamask/transaction-controller'; @@ -572,6 +571,7 @@ describe('BridgeStatusController', () => { // Assertion expect(bridgeStatusController.state.txHistory).toMatchSnapshot(); }); + it('restarts polling for history items that are not complete', async () => { // Setup jest.useFakeTimers(); @@ -608,7 +608,7 @@ describe('BridgeStatusController', () => { jest.clearAllMocks(); }); - it('sets the inital tx history state', async () => { + it('sets the initial tx history state', async () => { // Setup const bridgeStatusController = new BridgeStatusController({ messenger: getMessengerMock(), diff --git a/packages/bridge-status-controller/src/bridge-status-controller.ts b/packages/bridge-status-controller/src/bridge-status-controller.ts index a9caf238228..e17da5c794d 100644 --- a/packages/bridge-status-controller/src/bridge-status-controller.ts +++ b/packages/bridge-status-controller/src/bridge-status-controller.ts @@ -6,23 +6,22 @@ import { isNativeAddress, isSolanaChainId, type QuoteResponse, -} from '@metamask/bridge-controller'; -import type { - BridgeAsset, - QuoteMetadata, - TxData, + type BridgeAsset, + type QuoteMetadata, + type TxData, } from '@metamask/bridge-controller'; import { toHex } from '@metamask/controller-utils'; import { EthAccountType } from '@metamask/keyring-api'; -import { StaticIntervalPollingController } from '@metamask/polling-controller'; -import type { - TransactionController, - TransactionParams, -} from '@metamask/transaction-controller'; +import { + StaticIntervalPollingController, + getKey, +} from '@metamask/polling-controller'; import { TransactionStatus, TransactionType, type TransactionMeta, + type TransactionController, + type TransactionParams, } from '@metamask/transaction-controller'; import type { UserOperationController } from '@metamask/user-operation-controller'; import { numberToHex, type Hex } from '@metamask/utils'; @@ -57,7 +56,6 @@ import { handleSolanaTxResponse, generateActionId, } from './utils/transaction'; -import { getKey } from '../../polling-controller/src/AbstractPollingController'; const metadata: StateMetadata = { // We want to persist the bridge status state so that we can show the proper data for the Activity list diff --git a/packages/polling-controller/src/index.ts b/packages/polling-controller/src/index.ts index ba1758c443b..e10bbcc8793 100644 --- a/packages/polling-controller/src/index.ts +++ b/packages/polling-controller/src/index.ts @@ -8,4 +8,6 @@ export { StaticIntervalPollingController, } from './StaticIntervalPollingController'; +export { getKey } from './AbstractPollingController'; + export type { IPollingController } from './types'; From 23d5f205d04e3ea7eaf206415d27edbd5906f9e5 Mon Sep 17 00:00:00 2001 From: GustavoRSSilva Date: Mon, 28 Apr 2025 10:53:43 +0100 Subject: [PATCH 8/9] chore: pool limit --- .../bridge-status-controller.test.ts.snap | 119 ++++++++++++++++++ 1 file changed, 119 insertions(+) diff --git a/packages/bridge-status-controller/src/__snapshots__/bridge-status-controller.test.ts.snap b/packages/bridge-status-controller/src/__snapshots__/bridge-status-controller.test.ts.snap index d4d3b5745d2..d444b95841d 100644 --- a/packages/bridge-status-controller/src/__snapshots__/bridge-status-controller.test.ts.snap +++ b/packages/bridge-status-controller/src/__snapshots__/bridge-status-controller.test.ts.snap @@ -238,6 +238,125 @@ Object { } `; +exports[`BridgeStatusController startPollingForBridgeTxStatus sets the initial tx history state 1`] = ` +Object { + "bridgeTxMetaId1": Object { + "account": "0xaccount1", + "approvalTxId": undefined, + "estimatedProcessingTimeInSeconds": 15, + "hasApprovalTx": false, + "initialDestAssetBalance": undefined, + "pricingData": Object { + "amountSent": "1.234", + "amountSentInUsd": undefined, + "quotedGasInUsd": undefined, + "quotedReturnInUsd": undefined, + }, + "quote": Object { + "bridgeId": "lifi", + "bridges": Array [ + "across", + ], + "destAsset": Object { + "address": "0x0000000000000000000000000000000000000000", + "assetId": "eip155:10/slip44:60", + "chainId": 10, + "coinKey": "ETH", + "decimals": 18, + "icon": "https://raw.githubusercontent.com/trustwallet/assets/master/blockchains/ethereum/assets/0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2/logo.png", + "logoURI": "https://raw.githubusercontent.com/trustwallet/assets/master/blockchains/ethereum/assets/0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2/logo.png", + "name": "ETH", + "priceUSD": "2478.63", + "symbol": "ETH", + }, + "destChainId": 10, + "destTokenAmount": "990654755978612", + "feeData": Object { + "metabridge": Object { + "amount": "8750000000000", + "asset": Object { + "address": "0x0000000000000000000000000000000000000000", + "assetId": "eip155:42161/slip44:60", + "chainId": 42161, + "coinKey": "ETH", + "decimals": 18, + "icon": "https://raw.githubusercontent.com/trustwallet/assets/master/blockchains/ethereum/assets/0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2/logo.png", + "logoURI": "https://raw.githubusercontent.com/trustwallet/assets/master/blockchains/ethereum/assets/0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2/logo.png", + "name": "ETH", + "priceUSD": "2478.7", + "symbol": "ETH", + }, + }, + }, + "requestId": "197c402f-cb96-4096-9f8c-54aed84ca776", + "srcAsset": Object { + "address": "0x0000000000000000000000000000000000000000", + "assetId": "eip155:42161/slip44:60", + "chainId": 42161, + "coinKey": "ETH", + "decimals": 18, + "icon": "https://raw.githubusercontent.com/trustwallet/assets/master/blockchains/ethereum/assets/0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2/logo.png", + "logoURI": "https://raw.githubusercontent.com/trustwallet/assets/master/blockchains/ethereum/assets/0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2/logo.png", + "name": "ETH", + "priceUSD": "2478.7", + "symbol": "ETH", + }, + "srcChainId": 42161, + "srcTokenAmount": "991250000000000", + "steps": Array [ + Object { + "action": "bridge", + "destAmount": "990654755978612", + "destAsset": Object { + "address": "0x0000000000000000000000000000000000000000", + "assetId": "eip155:10/slip44:60", + "chainId": 10, + "coinKey": "ETH", + "decimals": 18, + "icon": "https://raw.githubusercontent.com/trustwallet/assets/master/blockchains/ethereum/assets/0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2/logo.png", + "logoURI": "https://raw.githubusercontent.com/trustwallet/assets/master/blockchains/ethereum/assets/0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2/logo.png", + "name": "ETH", + "priceUSD": "2478.63", + "symbol": "ETH", + }, + "destChainId": 10, + "protocol": Object { + "displayName": "Across", + "icon": "https://raw.githubusercontent.com/lifinance/types/main/src/assets/icons/bridges/acrossv2.png", + "name": "across", + }, + "srcAmount": "991250000000000", + "srcAsset": Object { + "address": "0x0000000000000000000000000000000000000000", + "assetId": "eip155:42161/slip44:60", + "chainId": 42161, + "coinKey": "ETH", + "decimals": 18, + "icon": "https://raw.githubusercontent.com/trustwallet/assets/master/blockchains/ethereum/assets/0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2/logo.png", + "logoURI": "https://raw.githubusercontent.com/trustwallet/assets/master/blockchains/ethereum/assets/0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2/logo.png", + "name": "ETH", + "priceUSD": "2478.7", + "symbol": "ETH", + }, + "srcChainId": 42161, + }, + ], + }, + "slippagePercentage": 0, + "startTime": 1729964825189, + "status": Object { + "srcChain": Object { + "chainId": 42161, + "txHash": "0xsrcTxHash1", + }, + "status": "PENDING", + }, + "targetContractAddress": "0x23981fC34e69eeDFE2BD9a0a9fCb0719Fe09DbFC", + "txMetaId": "bridgeTxMetaId1", + }, +} +`; + exports[`BridgeStatusController submitTx: EVM should delay after submitting linea approval 1`] = ` Object { "chainId": "0xa4b1", From ce5f465dbaf692e0baa2075c0d44e1da88d307e5 Mon Sep 17 00:00:00 2001 From: GustavoRSSilva Date: Mon, 28 Apr 2025 10:57:39 +0100 Subject: [PATCH 9/9] chore: merge main --- .../bridge-status-controller/src/bridge-status-controller.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/packages/bridge-status-controller/src/bridge-status-controller.ts b/packages/bridge-status-controller/src/bridge-status-controller.ts index dd646642ff1..5a12024113e 100644 --- a/packages/bridge-status-controller/src/bridge-status-controller.ts +++ b/packages/bridge-status-controller/src/bridge-status-controller.ts @@ -45,6 +45,7 @@ import { type FetchFunction, type BridgeClientId, type SolanaTransactionMeta, + type BridgeHistoryItem, } from './types'; import { fetchBridgeTxStatus,