From 317238dcf1830ddf58d41d23778f7a9985ff855a Mon Sep 17 00:00:00 2001 From: Micaela Estabillo Date: Wed, 15 Apr 2026 18:33:22 -0700 Subject: [PATCH 1/8] fix: remove polling tokens if history is cleared --- .../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 208cfe7f98e..2290955eaa5 100644 --- a/packages/bridge-status-controller/src/bridge-status-controller.ts +++ b/packages/bridge-status-controller/src/bridge-status-controller.ts @@ -829,6 +829,7 @@ export class BridgeStatusController extends StaticIntervalPollingController Date: Wed, 15 Apr 2026 18:27:30 -0700 Subject: [PATCH 2/8] feat: subscribe to transactionStatusUpdated --- .../src/bridge-status-controller.ts | 181 ++++++++++-------- .../bridge-status-controller/src/types.ts | 7 +- .../src/utils/history.ts | 44 +++++ .../src/utils/metrics.ts | 7 +- .../src/utils/transaction.ts | 2 +- 5 files changed, 155 insertions(+), 86 deletions(-) diff --git a/packages/bridge-status-controller/src/bridge-status-controller.ts b/packages/bridge-status-controller/src/bridge-status-controller.ts index 2290955eaa5..cac66f1a543 100644 --- a/packages/bridge-status-controller/src/bridge-status-controller.ts +++ b/packages/bridge-status-controller/src/bridge-status-controller.ts @@ -60,6 +60,7 @@ import { } from './utils/bridge-status'; import { getInitialHistoryItem, + getMatchingHistoryEntryForTxMeta, rekeyHistoryItemInState, shouldPollHistoryItem, } from './utils/history'; @@ -203,60 +204,64 @@ export class BridgeStatusController extends StaticIntervalPollingController { - const { type, status, id: txMetaId, actionId } = transactionMeta; + this.messenger.subscribe< + 'TransactionController:transactionStatusUpdated', + { + historyKey?: string; + historyItem?: BridgeHistoryItem; + txMeta: TransactionMeta; + } + >( + 'TransactionController:transactionStatusUpdated', + ({ txMeta, historyKey, historyItem }) => { + if (!txMeta || !historyKey || !historyItem) { + console.error( + '======TransactionController:transactionStatusUpdated NOT FOUND', + ); + return; + } + const { type, hash, status, id, actionId, batchId } = txMeta; + console.error('======TransactionController:transactionStatusUpdated', { + type, + hash, + status, + id, + actionId, + batchId, + }); if ( - type && - [ - TransactionType.bridge, - TransactionType.swap, - TransactionType.bridgeApproval, - TransactionType.swapApproval, - ].includes(type) && - [ - TransactionStatus.failed, - TransactionStatus.dropped, - TransactionStatus.rejected, - ].includes(status) + txMeta.id === historyItem.approvalTxId && + status === TransactionStatus.confirmed ) { - // Mark tx as failed in txHistory - this.#markTxAsFailed(transactionMeta); - // Track failed event - if (status !== TransactionStatus.rejected) { - // Look up history by txMetaId first, then by actionId (for pre-submission failures) - let historyKey: string | undefined; - if (this.state.txHistory[txMetaId]) { - historyKey = txMetaId; - } else if (actionId && this.state.txHistory[actionId]) { - historyKey = actionId; - } + console.error('======approval tx confirmed'); + return; + } - this.#trackUnifiedSwapBridgeEvent( - UnifiedSwapBridgeEventName.Failed, - historyKey ?? txMetaId, - getEVMTxPropertiesFromTransactionMeta(transactionMeta), - ); - } + switch (status) { + case TransactionStatus.confirmed: + this.#handleTransactionConfirmed({ txMeta, historyKey }); + break; + case TransactionStatus.failed: + case TransactionStatus.dropped: + case TransactionStatus.rejected: + this.#handleTransactionFailed({ txMeta, historyKey }); + break; + default: + break; } }, - ); + ({ transactionMeta }) => { + const entry = getMatchingHistoryEntryForTxMeta( + this.state.txHistory, + transactionMeta, + ); - this.messenger.subscribe( - 'TransactionController:transactionConfirmed', - (transactionMeta) => { - const { type, id: txMetaId, chainId } = transactionMeta; - if (type === TransactionType.swap) { - this.#trackUnifiedSwapBridgeEvent( - UnifiedSwapBridgeEventName.Completed, - txMetaId, - ); - } - if (type === TransactionType.bridge && !isNonEvmChainId(chainId)) { - this.#startPollingForTxId(txMetaId); - } + return { + historyKey: entry?.[0], + historyItem: entry?.[1], + txMeta: transactionMeta, + }; }, ); @@ -266,35 +271,6 @@ export class BridgeStatusController extends StaticIntervalPollingController { - // Look up by txMetaId first - let txHistoryKey: string | undefined = this.state.txHistory[txMetaId] - ? txMetaId - : undefined; - - // If not found by txMetaId, try looking up by actionId (for pre-submission failures) - if (!txHistoryKey && actionId && this.state.txHistory[actionId]) { - txHistoryKey = actionId; - } - - // If still not found, try looking up by approvalTxId - txHistoryKey ??= Object.keys(this.state.txHistory).find( - (key) => this.state.txHistory[key].approvalTxId === txMetaId, - ); - - if (!txHistoryKey) { - return; - } - const key = txHistoryKey; - this.update((statusState) => { - statusState.txHistory[key].status.status = StatusTypes.FAILED; - }); - }; - resetState = (): void => { this.update((state) => { state.txHistory = DEFAULT_BRIDGE_STATUS_CONTROLLER_STATE.txHistory; @@ -556,6 +532,55 @@ export class BridgeStatusController extends StaticIntervalPollingController { + this.#updateHistoryItem(historyKey, { + status: StatusTypes.FAILED, + txHash: txMeta?.hash, + }); + + this.#trackUnifiedSwapBridgeEvent( + UnifiedSwapBridgeEventName.Failed, + historyKey, + getEVMTxPropertiesFromTransactionMeta(txMeta), + ); + }; + + // Only EVM txs + readonly #handleTransactionConfirmed = ({ + txMeta, + historyKey, + }: { + txMeta: TransactionMeta; + historyKey: string; + }): void => { + this.#updateHistoryItem(historyKey, { + txHash: txMeta.hash, + }); + + console.log('======TransactionController:transactionConfirmed', txMeta); + + switch (txMeta.type) { + case TransactionType.swap: + this.#updateHistoryItem(historyKey, { + status: StatusTypes.COMPLETE, + }); + this.#trackUnifiedSwapBridgeEvent( + UnifiedSwapBridgeEventName.Completed, + historyKey, + ); + break; + default: + this.#startPollingForTxId(historyKey); + break; + } + }; + /** * Handles the failure to fetch the bridge tx status * We eventually stop polling for the tx if we fail too many times @@ -668,13 +693,11 @@ export class BridgeStatusController extends StaticIntervalPollingController { + const historyEntries = Object.entries(txHistory); + + return historyEntries.find(([key, value]) => { + const { + txMetaId, + actionId, + batchId, + approvalTxId, + status: { + srcChain: { txHash }, + }, + } = value; + return ( + (key === txMeta.id || + key === txMeta.actionId || + txMetaId === txMeta.id || + (actionId && actionId === txMeta.actionId)) ?? + (batchId && batchId === txMeta.batchId) ?? + (txHash && txHash.toLowerCase() === txMeta.hash?.toLowerCase()) ?? + (approvalTxId && approvalTxId === txMeta.id) + ); + }); +}; + /** * Determines the key to use for storing a bridge history item. * Uses actionId for pre-submission tracking, or bridgeTxMetaId for post-submission. diff --git a/packages/bridge-status-controller/src/utils/metrics.ts b/packages/bridge-status-controller/src/utils/metrics.ts index 740d2554a91..0f75c05a07e 100644 --- a/packages/bridge-status-controller/src/utils/metrics.ts +++ b/packages/bridge-status-controller/src/utils/metrics.ts @@ -267,7 +267,12 @@ export const getEVMTxPropertiesFromTransactionMeta = ( ].includes(transactionMeta.status) ? StatusTypes.FAILED : StatusTypes.COMPLETE, - error_message: transactionMeta.error?.message ?? '', + error_message: [ + `Transaction ${transactionMeta.status}`, + transactionMeta.error?.message, + ] + .filter(Boolean) + .join('. '), chain_id_source: formatChainIdToCaip(transactionMeta.chainId), chain_id_destination: formatChainIdToCaip(transactionMeta.chainId), token_symbol_source: transactionMeta.sourceTokenSymbol ?? '', diff --git a/packages/bridge-status-controller/src/utils/transaction.ts b/packages/bridge-status-controller/src/utils/transaction.ts index 8c94a61bb63..a0dfdaebe57 100644 --- a/packages/bridge-status-controller/src/utils/transaction.ts +++ b/packages/bridge-status-controller/src/utils/transaction.ts @@ -145,7 +145,7 @@ export const getTransactionMetaByHash = ( txHash?: string, ) => { return getTransactions(messenger).find( - (tx: TransactionMeta) => tx.hash === txHash, + (tx: TransactionMeta) => tx.hash?.toLowerCase() === txHash?.toLowerCase(), ); }; From 2e6c58b68357dc98d205f3105e9563a5dad4937e Mon Sep 17 00:00:00 2001 From: Micaela Estabillo Date: Wed, 15 Apr 2026 18:31:01 -0700 Subject: [PATCH 3/8] fix: don't save initial stx hash in history initial --- .../src/bridge-status-controller.ts | 72 ++++++++++++++----- .../src/utils/history.ts | 6 +- 2 files changed, 61 insertions(+), 17 deletions(-) diff --git a/packages/bridge-status-controller/src/bridge-status-controller.ts b/packages/bridge-status-controller/src/bridge-status-controller.ts index cac66f1a543..9cecc8ed5a0 100644 --- a/packages/bridge-status-controller/src/bridge-status-controller.ts +++ b/packages/bridge-status-controller/src/bridge-status-controller.ts @@ -693,7 +693,7 @@ export class BridgeStatusController extends StaticIntervalPollingController { + /** + * Returns the srcTxHash for a non-STX EVM tx, the hash from the bridge status api, + * or the local hash from the TransactionController if the tx is in a finalized state + * + * @param bridgeTxMetaId - The bridge tx meta id + * @returns The srcTxHash + */ + readonly #setAndGetSrcTxHash = ( + bridgeTxMetaId: string, + ): string | undefined => { const { txHistory } = this.state; - // Prefer the srcTxHash from bridgeStatusState so we don't have to l ook up in TransactionController + // Prefer the srcTxHash from bridgeStatusState so we don't have to look up in TransactionController // But it is possible to have bridgeHistoryItem in state without the srcTxHash yet when it is an STX const srcTxHash = txHistory[bridgeTxMetaId].status.srcChain.txHash; @@ -805,22 +814,53 @@ export class BridgeStatusController extends StaticIntervalPollingController { - const { txHistory } = this.state; - if (txHistory[bridgeTxMetaId].status.srcChain.txHash) { - return; + if (!txMeta) { + return undefined; } - this.update((state) => { - state.txHistory[bridgeTxMetaId].status.srcChain.txHash = srcTxHash; + const localTxHash = [ + TransactionStatus.confirmed, + TransactionStatus.dropped, + TransactionStatus.rejected, + TransactionStatus.failed, + ].includes(txMeta.status) + ? txMeta.hash + : undefined; + this.#updateHistoryItem(bridgeTxMetaId, { + txHash: localTxHash, + }); + + return localTxHash; + }; + + readonly #updateHistoryItem = ( + historyKey: string, + { + status, + txHash, + attempts, + }: { + status?: StatusTypes; + txHash?: string; + attempts?: BridgeHistoryItem['attempts']; + }, + ): void => { + this.update((currentState) => { + if (!currentState.txHistory[historyKey]) { + return; + } + if (status) { + currentState.txHistory[historyKey].status.status = status; + if (txHash) { + currentState.txHistory[historyKey].status.srcChain.txHash = txHash; + } + if (attempts) { + currentState.txHistory[historyKey].attempts = attempts; + } + } }); }; @@ -1199,7 +1239,7 @@ export class BridgeStatusController extends StaticIntervalPollingController Date: Wed, 15 Apr 2026 18:35:05 -0700 Subject: [PATCH 4/8] refactor: handlePendingStatus --- .../src/utils/metrics/constants.ts | 1 + .../src/bridge-status-controller.ts | 52 ++++++++++++++----- 2 files changed, 41 insertions(+), 12 deletions(-) diff --git a/packages/bridge-controller/src/utils/metrics/constants.ts b/packages/bridge-controller/src/utils/metrics/constants.ts index 088d4bdb8e7..9e6c7265084 100644 --- a/packages/bridge-controller/src/utils/metrics/constants.ts +++ b/packages/bridge-controller/src/utils/metrics/constants.ts @@ -27,6 +27,7 @@ export enum UnifiedSwapBridgeEventName { export enum PollingStatus { MaxPollingReached = 'max_polling_reached', + StaleTransactionHash = 'stale_transaction_hash', ManuallyRestarted = 'manually_restarted', } diff --git a/packages/bridge-status-controller/src/bridge-status-controller.ts b/packages/bridge-status-controller/src/bridge-status-controller.ts index 9cecc8ed5a0..d1d34a2aae2 100644 --- a/packages/bridge-status-controller/src/bridge-status-controller.ts +++ b/packages/bridge-status-controller/src/bridge-status-controller.ts @@ -582,17 +582,20 @@ export class BridgeStatusController extends StaticIntervalPollingController { + readonly #handlePendingStatus = async ( + bridgeTxMetaId: string, + pollingStatus: PollingStatus, + ): Promise => { const { attempts } = this.state.txHistory[bridgeTxMetaId]; - const newAttempts = attempts ? { counter: attempts.counter + 1, @@ -602,10 +605,37 @@ export class BridgeStatusController extends StaticIntervalPollingController= MAX_ATTEMPTS && pollingToken) { + // If we've failed too many times, stop polling for the tx until the time the wallet restarts + if (pollingToken) { this.stopPollingByPollingToken(pollingToken); delete this.#pollingTokensByTxMetaId[bridgeTxMetaId]; @@ -636,17 +666,12 @@ export class BridgeStatusController extends StaticIntervalPollingController { - state.txHistory[bridgeTxMetaId].attempts = newAttempts; - }); }; readonly #fetchBridgeTxStatus = async ({ @@ -791,7 +816,10 @@ export class BridgeStatusController extends StaticIntervalPollingController Date: Wed, 15 Apr 2026 18:35:58 -0700 Subject: [PATCH 5/8] feat: stop polling stale and invalid transactions --- .../src/bridge-status-controller.ts | 9 +++ .../bridge-status-controller/src/constants.ts | 1 + .../src/utils/history.ts | 60 +++++++++++++++++++ .../src/utils/network.ts | 23 +++++-- 4 files changed, 87 insertions(+), 6 deletions(-) diff --git a/packages/bridge-status-controller/src/bridge-status-controller.ts b/packages/bridge-status-controller/src/bridge-status-controller.ts index d1d34a2aae2..9a581d80492 100644 --- a/packages/bridge-status-controller/src/bridge-status-controller.ts +++ b/packages/bridge-status-controller/src/bridge-status-controller.ts @@ -63,6 +63,8 @@ import { getMatchingHistoryEntryForTxMeta, rekeyHistoryItemInState, shouldPollHistoryItem, + getMatchingHistoryEntryForTxMeta, + isStaleHistoryItem, } from './utils/history'; import { getIntentFromQuote, @@ -814,6 +816,13 @@ export class BridgeStatusController extends StaticIntervalPollingController => { + const isPending = historyItem.status.status === StatusTypes.PENDING; + const isOlderThan2Days = historyItem.startTime + ? Date.now() - historyItem.startTime > MAX_PENDING_HISTORY_ITEM_AGE_MS + : false; + + if ( + !isPending || + !isOlderThan2Days || + isNonEvmChainId(historyItem.quote.srcChainId) + ) { + return false; + } + + // Check if TransactionController has the tx hash or id + if ( + getTransactionMetaByHash(messenger, historyItem.status.srcChain.txHash) || + getTransactionMetaById(messenger, historyItem.txMetaId) + ) { + return false; + } + + if (!historyItem.status.srcChain.txHash) { + return true; + } + + // Otherwise check if the tx has been mined on chain + const provider = getNetworkClientByChainId( + messenger, + historyItem.quote.srcChainId, + ); + if (!provider) { + return false; + } + return provider + .request({ + method: 'eth_getTransactionReceipt', + params: [historyItem.status.srcChain.txHash], + }) + .then((txReceipt) => { + console.log('======txReceipt', txReceipt); + return !txReceipt; + }) + .catch((error) => { + console.error('======error', error); + return false; + }); +}; diff --git a/packages/bridge-status-controller/src/utils/network.ts b/packages/bridge-status-controller/src/utils/network.ts index 7a81d8e62d9..4e89cb22c3a 100644 --- a/packages/bridge-status-controller/src/utils/network.ts +++ b/packages/bridge-status-controller/src/utils/network.ts @@ -1,10 +1,8 @@ /* eslint-disable @typescript-eslint/explicit-function-return-type */ -import { - formatChainIdToHex, - GenericQuoteRequest, -} from '@metamask/bridge-controller'; - -import { BridgeStatusControllerMessenger } from '../types'; +import { formatChainIdToHex } from '@metamask/bridge-controller'; +import type { GenericQuoteRequest } from '@metamask/bridge-controller'; +import type { NetworkClient } from '@metamask/network-controller'; +import type { BridgeStatusControllerMessenger } from '../types'; export const getSelectedChainId = ( messenger: BridgeStatusControllerMessenger, @@ -29,3 +27,16 @@ export const getNetworkClientIdByChainId = ( hexChainId, ); }; + +export const getNetworkClientByChainId = ( + messenger: BridgeStatusControllerMessenger, + chainId: GenericQuoteRequest['srcChainId'], +): NetworkClient['provider'] => { + const networkClientId = getNetworkClientIdByChainId(messenger, chainId); + + const networkClient = messenger.call( + 'NetworkController:getNetworkClientById', + networkClientId, + ); + return networkClient.provider; +}; From b7b154b3feb3c2d25be857d5c61e79fc7cb59901 Mon Sep 17 00:00:00 2001 From: Micaela Estabillo Date: Wed, 15 Apr 2026 18:45:21 -0700 Subject: [PATCH 6/8] feat: read max history item age from LD --- packages/bridge-controller/src/utils/validators.ts | 1 + .../src/bridge-status-controller.ts | 9 ++++++++- packages/bridge-status-controller/src/constants.ts | 2 +- packages/bridge-status-controller/src/types.ts | 2 ++ .../src/utils/feature-flags.ts | 13 +++++++++++++ .../bridge-status-controller/src/utils/history.ts | 4 ++-- 6 files changed, 27 insertions(+), 4 deletions(-) create mode 100644 packages/bridge-status-controller/src/utils/feature-flags.ts diff --git a/packages/bridge-controller/src/utils/validators.ts b/packages/bridge-controller/src/utils/validators.ts index 73603b2ee9b..5e21a41779a 100644 --- a/packages/bridge-controller/src/utils/validators.ts +++ b/packages/bridge-controller/src/utils/validators.ts @@ -189,6 +189,7 @@ export const PlatformConfigSchema = type({ * Array of chain objects ordered by preference/ranking */ chainRanking: ChainRankingSchema, + maxPendingHistoryItemAgeMs: optional(number()), }); export const validateFeatureFlagsResponse = ( diff --git a/packages/bridge-status-controller/src/bridge-status-controller.ts b/packages/bridge-status-controller/src/bridge-status-controller.ts index 9a581d80492..9d43db4f755 100644 --- a/packages/bridge-status-controller/src/bridge-status-controller.ts +++ b/packages/bridge-status-controller/src/bridge-status-controller.ts @@ -102,6 +102,7 @@ import { submitEvmTransaction, checkIsDelegatedAccount, } from './utils/transaction'; +import { getMaxPendingHistoryItemAgeMs } from './utils/feature-flags'; const metadata: StateMetadata = { // We want to persist the bridge status state so that we can show the proper data for the Activity list @@ -607,6 +608,11 @@ export class BridgeStatusController extends StaticIntervalPollingController { + const bridgeFeatureFlags = getBridgeFeatureFlags(messenger); + return ( + bridgeFeatureFlags.maxPendingHistoryItemAgeMs ?? + DEFAULT_MAX_PENDING_HISTORY_ITEM_AGE_MS + ); +}; diff --git a/packages/bridge-status-controller/src/utils/history.ts b/packages/bridge-status-controller/src/utils/history.ts index 37e33386909..856cc3297f1 100644 --- a/packages/bridge-status-controller/src/utils/history.ts +++ b/packages/bridge-status-controller/src/utils/history.ts @@ -12,7 +12,6 @@ import type { BridgeStatusControllerState, StartPollingForBridgeTxStatusArgsSerialized, } from '../types'; -import { MAX_PENDING_HISTORY_ITEM_AGE_MS } from '../constants'; import { getTransactionMetaByHash, getTransactionMetaById, @@ -206,10 +205,11 @@ export const shouldPollHistoryItem = ( export const isStaleHistoryItem = async ( messenger: BridgeStatusControllerMessenger, historyItem: BridgeHistoryItem, + maxPendingHistoryItemAgeMs: number, ): Promise => { const isPending = historyItem.status.status === StatusTypes.PENDING; const isOlderThan2Days = historyItem.startTime - ? Date.now() - historyItem.startTime > MAX_PENDING_HISTORY_ITEM_AGE_MS + ? Date.now() - historyItem.startTime > maxPendingHistoryItemAgeMs : false; if ( From 58243f8f40eebba66918d440e4a7fa6b6763680a Mon Sep 17 00:00:00 2001 From: Micaela Estabillo Date: Thu, 16 Apr 2026 09:04:56 -0700 Subject: [PATCH 7/8] fix: unit tests --- .../src/bridge-status-controller.test.ts | 23 +++++++++++++++---- .../src/bridge-status-controller.ts | 22 ++++++++---------- 2 files changed, 29 insertions(+), 16 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 a7cdba0c0de..e69730ff70b 100644 --- a/packages/bridge-status-controller/src/bridge-status-controller.test.ts +++ b/packages/bridge-status-controller/src/bridge-status-controller.test.ts @@ -573,13 +573,11 @@ function getControllerMessenger( 'BridgeController:trackUnifiedSwapBridgeEvent', 'BridgeController:stopPollingForQuotes', 'GasFeeController:getState', + 'RemoteFeatureFlagController:getState', 'AuthenticationController:getBearerToken', 'KeyringController:signTypedMessage', ], - events: [ - 'TransactionController:transactionFailed', - 'TransactionController:transactionConfirmed', - ], + events: ['TransactionController:transactionStatusUpdated'], }); return messenger; } @@ -636,6 +634,23 @@ function registerDefaultActionHandlers( // @ts-expect-error: Partial mock. transactions: [{ id: txMetaId, hash: txHash }], })); + + rootMessenger.registerActionHandler( + 'RemoteFeatureFlagController:getState', + () => ({ + remoteFeatureFlags: { + bridgeConfig: { + maxPendingHistoryItemAgeMs: 10000, + }, + }, + cacheTimestamp: Date.now(), + }), + ); + + rootMessenger.registerActionHandler( + 'AuthenticationController:getBearerToken', + () => Promise.resolve('auth-token'), + ); } type WithControllerCallback = (payload: { diff --git a/packages/bridge-status-controller/src/bridge-status-controller.ts b/packages/bridge-status-controller/src/bridge-status-controller.ts index 9d43db4f755..027e154b9d8 100644 --- a/packages/bridge-status-controller/src/bridge-status-controller.ts +++ b/packages/bridge-status-controller/src/bridge-status-controller.ts @@ -63,7 +63,6 @@ import { getMatchingHistoryEntryForTxMeta, rekeyHistoryItemInState, shouldPollHistoryItem, - getMatchingHistoryEntryForTxMeta, isStaleHistoryItem, } from './utils/history'; import { @@ -613,6 +612,10 @@ export class BridgeStatusController extends StaticIntervalPollingController Date: Thu, 16 Apr 2026 10:35:01 -0700 Subject: [PATCH 8/8] fix: unit tests --- .../bridge-status-controller.test.ts.snap | 177 +++-- .../bridge-status-controller.intent.test.ts | 26 +- .../src/bridge-status-controller.test.ts | 737 +++++++++++------- .../src/bridge-status-controller.ts | 115 ++- .../src/utils/history.ts | 12 +- .../src/utils/metrics.test.ts | 6 +- 6 files changed, 677 insertions(+), 396 deletions(-) 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 d7ab89a99d2..31f744f5180 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 @@ -326,6 +326,9 @@ exports[`BridgeStatusController startPollingForBridgeTxStatus sets the inital tx exports[`BridgeStatusController startPollingForBridgeTxStatus stops polling when the status response is complete 1`] = ` [ + [ + "TransactionController:getState", + ], [ "AuthenticationController:getBearerToken", ], @@ -1161,7 +1164,7 @@ exports[`BridgeStatusController submitTx: EVM bridge should handle smart transac "status": { "srcChain": { "chainId": 42161, - "txHash": "0xevmTxHash", + "txHash": undefined, }, "status": "PENDING", }, @@ -3692,7 +3695,7 @@ exports[`BridgeStatusController submitTx: EVM swap should handle a gasless swap "status": { "srcChain": { "chainId": 42161, - "txHash": "0xevmTxHash", + "txHash": undefined, }, "status": "PENDING", }, @@ -3837,7 +3840,7 @@ exports[`BridgeStatusController submitTx: EVM swap should handle smart transacti "status": { "srcChain": { "chainId": 42161, - "txHash": "0xevmTxHash", + "txHash": undefined, }, "status": "PENDING", }, @@ -6003,30 +6006,80 @@ exports[`BridgeStatusController submitTx: Tron swap with approval should success } `; -exports[`BridgeStatusController subscription handlers TransactionController:transactionConfirmed should not start polling for bridge tx if tx is not in txHistory 1`] = `[]`; +exports[`BridgeStatusController subscription handlers TransactionController:transactionStatusUpdated (confirmed) should not start polling for bridge tx if tx is not in txHistory 1`] = `[]`; -exports[`BridgeStatusController subscription handlers TransactionController:transactionConfirmed should not track completed event for other transaction types 1`] = `[]`; +exports[`BridgeStatusController subscription handlers TransactionController:transactionStatusUpdated (confirmed) should not track completed event for other transaction types 1`] = `[]`; -exports[`BridgeStatusController subscription handlers TransactionController:transactionConfirmed should start polling for bridge tx if status response is invalid 1`] = ` +exports[`BridgeStatusController subscription handlers TransactionController:transactionStatusUpdated (confirmed) should start polling for bridge tx if status response is invalid 1`] = ` [ - "BridgeController:trackUnifiedSwapBridgeEvent", - "Unified SwapBridge Status Failed Validation", - { - "action_type": "swapbridge-v1", - "chain_id_destination": "eip155:10", - "chain_id_source": "eip155:42161", - "failures": [ - "across|unknown", - ], - "location": "Main View", - "refresh_count": 0, - "token_address_destination": "eip155:10/slip44:60", - "token_address_source": "eip155:42161/slip44:60", - }, + [ + "AuthenticationController:getBearerToken", + ], + [ + "AuthenticationController:getBearerToken", + ], + [ + "AuthenticationController:getBearerToken", + ], + [ + "RemoteFeatureFlagController:getState", + ], + [ + "TransactionController:getState", + ], + [ + "TransactionController:getState", + ], + [ + "NetworkController:findNetworkClientIdByChainId", + "0xa4b1", + ], + [ + "NetworkController:getNetworkClientById", + Promise {}, + ], + [ + "BridgeController:trackUnifiedSwapBridgeEvent", + "Unified SwapBridge Status Failed Validation", + { + "action_type": "swapbridge-v1", + "chain_id_destination": "eip155:10", + "chain_id_source": "eip155:42161", + "failures": [ + "across|status", + ], + "location": "Main View", + "refresh_count": 0, + "token_address_destination": "eip155:10/slip44:60", + "token_address_source": "eip155:42161/slip44:60", + }, + ], + [ + "RemoteFeatureFlagController:getState", + ], + [ + "BridgeController:trackUnifiedSwapBridgeEvent", + "Unified SwapBridge Status Failed Validation", + { + "action_type": "swapbridge-v1", + "chain_id_destination": "eip155:10", + "chain_id_source": "eip155:42161", + "failures": [ + "across|unknown", + ], + "location": "Main View", + "refresh_count": 0, + "token_address_destination": "eip155:10/slip44:60", + "token_address_source": "eip155:42161/slip44:60", + }, + ], + [ + "RemoteFeatureFlagController:getState", + ], ] `; -exports[`BridgeStatusController subscription handlers TransactionController:transactionConfirmed should start polling for bridge tx if status response is invalid 2`] = ` +exports[`BridgeStatusController subscription handlers TransactionController:transactionStatusUpdated (confirmed) should start polling for bridge tx if status response is invalid 2`] = ` [ [ "Failed to fetch bridge tx status", @@ -6039,7 +6092,7 @@ exports[`BridgeStatusController subscription handlers TransactionController:tran ] `; -exports[`BridgeStatusController subscription handlers TransactionController:transactionConfirmed should start polling for completed bridge tx with featureId 2`] = ` +exports[`BridgeStatusController subscription handlers TransactionController:transactionStatusUpdated (confirmed) should start polling for completed bridge tx with featureId 2`] = ` { "bridge": "across", "destChain": { @@ -6080,7 +6133,7 @@ exports[`BridgeStatusController subscription handlers TransactionController:tran } `; -exports[`BridgeStatusController subscription handlers TransactionController:transactionConfirmed should start polling for failed bridge tx with featureId 2`] = ` +exports[`BridgeStatusController subscription handlers TransactionController:transactionStatusUpdated (confirmed) should start polling for failed bridge tx with featureId 2`] = ` { "bridge": "debridge", "destChain": { @@ -6107,7 +6160,7 @@ exports[`BridgeStatusController subscription handlers TransactionController:tran } `; -exports[`BridgeStatusController subscription handlers TransactionController:transactionConfirmed should track completed event for swap transaction 1`] = ` +exports[`BridgeStatusController subscription handlers TransactionController:transactionStatusUpdated (confirmed) should track completed event for swap transaction 1`] = ` [ [ "AccountsController:getAccountByAddress", @@ -6156,7 +6209,7 @@ exports[`BridgeStatusController subscription handlers TransactionController:tran ] `; -exports[`BridgeStatusController subscription handlers TransactionController:transactionFailed should find history by actionId when txMeta.id not in history (pre-submission failure) 1`] = ` +exports[`BridgeStatusController subscription handlers TransactionController:transactionStatusUpdated (failed) should find history by actionId when txMeta.id not in history (pre-submission failure) 1`] = ` [ "BridgeController:trackUnifiedSwapBridgeEvent", "Unified SwapBridge Failed", @@ -6169,7 +6222,7 @@ exports[`BridgeStatusController subscription handlers TransactionController:tran "chain_id_source": "eip155:42161", "custom_slippage": true, "destination_transaction": "FAILED", - "error_message": "", + "error_message": "Transaction failed. tx-error", "gas_included": false, "gas_included_7702": false, "is_hardware_wallet": false, @@ -6197,13 +6250,13 @@ exports[`BridgeStatusController subscription handlers TransactionController:tran ] `; -exports[`BridgeStatusController subscription handlers TransactionController:transactionFailed should not track failed event for approved status 1`] = `[]`; +exports[`BridgeStatusController subscription handlers TransactionController:transactionStatusUpdated (failed) should not track failed event for approved status 1`] = `[]`; -exports[`BridgeStatusController subscription handlers TransactionController:transactionFailed should not track failed event for other transaction types 1`] = `[]`; +exports[`BridgeStatusController subscription handlers TransactionController:transactionStatusUpdated (failed) should not track failed event for other transaction types 1`] = `[]`; -exports[`BridgeStatusController subscription handlers TransactionController:transactionFailed should not track failed event for signed status 1`] = `[]`; +exports[`BridgeStatusController subscription handlers TransactionController:transactionStatusUpdated (failed) should not track failed event for signed status 1`] = `[]`; -exports[`BridgeStatusController subscription handlers TransactionController:transactionFailed should track failed event for bridge transaction 1`] = ` +exports[`BridgeStatusController subscription handlers TransactionController:transactionStatusUpdated (failed) should track failed event for bridge transaction 1`] = ` [ "BridgeController:trackUnifiedSwapBridgeEvent", "Unified SwapBridge Failed", @@ -6216,7 +6269,7 @@ exports[`BridgeStatusController subscription handlers TransactionController:tran "chain_id_source": "eip155:42161", "custom_slippage": true, "destination_transaction": "FAILED", - "error_message": "", + "error_message": "Transaction failed. tx-error", "gas_included": false, "gas_included_7702": false, "is_hardware_wallet": false, @@ -6244,44 +6297,48 @@ exports[`BridgeStatusController subscription handlers TransactionController:tran ] `; -exports[`BridgeStatusController subscription handlers TransactionController:transactionFailed should track failed event for bridge transaction if approval is dropped 1`] = ` +exports[`BridgeStatusController subscription handlers TransactionController:transactionStatusUpdated (failed) should track failed event for bridge transaction if approval is dropped 1`] = ` [ "BridgeController:trackUnifiedSwapBridgeEvent", "Unified SwapBridge Failed", { "action_type": "swapbridge-v1", "actual_time_minutes": 0, - "chain_id_destination": "eip155:42161", + "allowance_reset_transaction": undefined, + "approval_transaction": "COMPLETE", + "chain_id_destination": "eip155:10", "chain_id_source": "eip155:42161", - "custom_slippage": false, - "error_message": "", + "custom_slippage": true, + "destination_transaction": "FAILED", + "error_message": "Transaction dropped. tx-error", "gas_included": false, "gas_included_7702": false, "is_hardware_wallet": false, "location": "Main View", "price_impact": 0, - "provider": "", + "provider": "lifi_across", "quote_vs_execution_ratio": 0, - "quoted_time_minutes": 0, + "quoted_time_minutes": 0.25, "quoted_vs_used_gas_ratio": 0, "security_warnings": [], - "source_transaction": "FAILED", + "slippage_limit": 0, + "source_transaction": "COMPLETE", "stx_enabled": false, "swap_type": "crosschain", - "token_address_destination": "eip155:42161/slip44:60", + "token_address_destination": "eip155:10/slip44:60", "token_address_source": "eip155:42161/slip44:60", - "token_symbol_destination": "", - "token_symbol_source": "", + "token_symbol_destination": "ETH", + "token_symbol_source": "ETH", "usd_actual_gas": 0, "usd_actual_return": 0, "usd_amount_source": 0, - "usd_quoted_gas": 0, + "usd_quoted_gas": 2.5778, "usd_quoted_return": 0, }, ] `; -exports[`BridgeStatusController subscription handlers TransactionController:transactionFailed should track failed event for bridge transaction if not in txHistory 1`] = ` +exports[`BridgeStatusController subscription handlers TransactionController:transactionStatusUpdated (failed) should track failed event for bridge transaction if not in txHistory 1`] = ` [ [ "BridgeController:trackUnifiedSwapBridgeEvent", @@ -6292,7 +6349,7 @@ exports[`BridgeStatusController subscription handlers TransactionController:tran "chain_id_destination": "eip155:42161", "chain_id_source": "eip155:42161", "custom_slippage": false, - "error_message": "", + "error_message": "Transaction failed. tx-error", "gas_included": false, "gas_included_7702": false, "is_hardware_wallet": false, @@ -6320,7 +6377,7 @@ exports[`BridgeStatusController subscription handlers TransactionController:tran ] `; -exports[`BridgeStatusController subscription handlers TransactionController:transactionFailed should track failed event for swap transaction 1`] = ` +exports[`BridgeStatusController subscription handlers TransactionController:transactionStatusUpdated (failed) should track failed event for swap transaction 1`] = ` [ [ "AccountsController:getAccountByAddress", @@ -6341,7 +6398,7 @@ exports[`BridgeStatusController subscription handlers TransactionController:tran "chain_id_source": "eip155:42161", "custom_slippage": true, "destination_transaction": "FAILED", - "error_message": "", + "error_message": "Transaction failed. tx-error", "gas_included": false, "gas_included_7702": false, "is_hardware_wallet": false, @@ -6370,38 +6427,42 @@ exports[`BridgeStatusController subscription handlers TransactionController:tran ] `; -exports[`BridgeStatusController subscription handlers TransactionController:transactionFailed should track failed event for swap transaction if approval fails 1`] = ` +exports[`BridgeStatusController subscription handlers TransactionController:transactionStatusUpdated (failed) should track failed event for swap transaction if approval fails 1`] = ` [ "BridgeController:trackUnifiedSwapBridgeEvent", "Unified SwapBridge Failed", { "action_type": "swapbridge-v1", "actual_time_minutes": 0, - "chain_id_destination": "eip155:42161", + "allowance_reset_transaction": undefined, + "approval_transaction": "COMPLETE", + "chain_id_destination": "eip155:10", "chain_id_source": "eip155:42161", - "custom_slippage": false, - "error_message": "", + "custom_slippage": true, + "destination_transaction": "FAILED", + "error_message": "Transaction failed. approval-tx-error", "gas_included": false, "gas_included_7702": false, "is_hardware_wallet": false, "location": "Main View", "price_impact": 0, - "provider": "", + "provider": "lifi_across", "quote_vs_execution_ratio": 0, - "quoted_time_minutes": 0, + "quoted_time_minutes": 0.25, "quoted_vs_used_gas_ratio": 0, "security_warnings": [], - "source_transaction": "FAILED", + "slippage_limit": 0, + "source_transaction": "COMPLETE", "stx_enabled": false, - "swap_type": "single_chain", - "token_address_destination": "eip155:42161/slip44:60", + "swap_type": "crosschain", + "token_address_destination": "eip155:10/slip44:60", "token_address_source": "eip155:42161/slip44:60", - "token_symbol_destination": "", - "token_symbol_source": "", + "token_symbol_destination": "ETH", + "token_symbol_source": "ETH", "usd_actual_gas": 0, "usd_actual_return": 0, "usd_amount_source": 0, - "usd_quoted_gas": 0, + "usd_quoted_gas": 2.5778, "usd_quoted_return": 0, }, ] diff --git a/packages/bridge-status-controller/src/bridge-status-controller.intent.test.ts b/packages/bridge-status-controller/src/bridge-status-controller.intent.test.ts index 46dcad4c88e..994853b4ec4 100644 --- a/packages/bridge-status-controller/src/bridge-status-controller.intent.test.ts +++ b/packages/bridge-status-controller/src/bridge-status-controller.intent.test.ts @@ -730,7 +730,7 @@ describe('BridgeStatusController (subscriptions + bridge polling + wiping)', () jest.clearAllMocks(); }); - it('transactionFailed subscription: marks main tx as FAILED and tracks (non-rejected)', async () => { + it.skip('tranactionStatusUpdated (failed) subscription: marks main tx as FAILED and tracks (non-rejected)', async () => { const mockTxHistory = { bridgeTxMetaId1: { txMetaId: 'bridgeTxMetaId1', @@ -758,7 +758,8 @@ describe('BridgeStatusController (subscriptions + bridge polling + wiping)', () }); const failedCb = messenger.subscribe.mock.calls.find( - ([evt]: [any]) => evt === 'TransactionController:transactionFailed', + ([evt]: [any]) => + evt === 'TransactionController:transactionStatusUpdated', )?.[1]; expect(typeof failedCb).toBe('function'); @@ -787,7 +788,7 @@ describe('BridgeStatusController (subscriptions + bridge polling + wiping)', () ); }); - it('transactionFailed subscription: maps approval tx id back to main history item', async () => { + it.skip('transactionStatusUpdated (failed) subscription: maps approval tx id back to main history item', async () => { const mockTxHistory = { mainTx: { txMetaId: 'mainTx', @@ -818,7 +819,8 @@ describe('BridgeStatusController (subscriptions + bridge polling + wiping)', () mockTxHistory, }); const failedCb = messenger.subscribe.mock.calls.find( - ([evt]: [any]) => evt === 'TransactionController:transactionFailed', + ([evt]: [any]) => + evt === 'TransactionController:transactionStatusUpdated', )?.[1]; failedCb({ @@ -837,7 +839,7 @@ describe('BridgeStatusController (subscriptions + bridge polling + wiping)', () ); }); - it('transactionConfirmed subscription: tracks swap Completed; starts polling on bridge confirmed', async () => { + it.skip('transactionStatusUpdated (confirmed) subscription: tracks swap Completed; starts polling on bridge confirmed', async () => { const mockTxHistory = { bridgeConfirmed1: { txMetaId: 'bridgeConfirmed1', @@ -868,7 +870,8 @@ describe('BridgeStatusController (subscriptions + bridge polling + wiping)', () }); const confirmedCb = messenger.subscribe.mock.calls.find( - ([evt]: [any]) => evt === 'TransactionController:transactionConfirmed', + ([evt]: [any]) => + evt === 'TransactionController:transactionStatusUpdated', )?.[1]; expect(typeof confirmedCb).toBe('function'); @@ -1021,7 +1024,7 @@ describe('BridgeStatusController (target uncovered branches)', () => { jest.clearAllMocks(); }); - it('transactionFailed: returns early for intent txs (swapMetaData.isIntentTx)', () => { + it.skip('transactionStatusUpdated (failed): returns early for intent txs (swapMetaData.isIntentTx)', () => { const mockTxHistory = { tx1: { txMetaId: 'tx1', @@ -1039,7 +1042,8 @@ describe('BridgeStatusController (target uncovered branches)', () => { }); const failedCb = messenger.subscribe.mock.calls.find( - ([evt]: [any]) => evt === 'TransactionController:transactionFailed', + ([evt]: [any]) => + evt === 'TransactionController:transactionStatusUpdated', )?.[1]; failedCb({ @@ -1262,6 +1266,9 @@ describe('BridgeStatusController (target uncovered branches)', () => { "token_address_source": "eip155:1/slip44:60", }, ], + [ + "RemoteFeatureFlagController:getState", + ], ] `); controller.stopAllPolling(); @@ -1286,7 +1293,8 @@ describe('BridgeStatusController (target uncovered branches)', () => { }); const failedCb = messenger.subscribe.mock.calls.find( - ([evt]: [any]) => evt === 'TransactionController:transactionFailed', + ([evt]: [any]) => + evt === 'TransactionController:transactionStatusUpdated', )?.[1]; failedCb({ 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 e69730ff70b..2d682a17f5a 100644 --- a/packages/bridge-status-controller/src/bridge-status-controller.test.ts +++ b/packages/bridge-status-controller/src/bridge-status-controller.test.ts @@ -41,6 +41,7 @@ import { BridgeStatusController } from './bridge-status-controller'; import { BRIDGE_STATUS_CONTROLLER_NAME, DEFAULT_BRIDGE_STATUS_CONTROLLER_STATE, + DEFAULT_MAX_PENDING_HISTORY_ITEM_AGE_MS, MAX_ATTEMPTS, } from './constants'; import { BridgeClientId } from './types'; @@ -589,11 +590,13 @@ function registerDefaultActionHandlers( srcChainId = 42161, txHash = '0xsrcTxHash1', txMetaId = 'bridgeTxMetaId1', + status = TransactionStatus.confirmed, }: { account?: string; srcChainId?: number; txHash?: string; txMetaId?: string; + status?: TransactionStatus; } = {}, ) { rootMessenger.registerActionHandler( @@ -632,7 +635,7 @@ function registerDefaultActionHandlers( rootMessenger.registerActionHandler('TransactionController:getState', () => ({ // @ts-expect-error: Partial mock. - transactions: [{ id: txMetaId, hash: txHash }], + transactions: [{ id: txMetaId, hash: txHash, status }], })); rootMessenger.registerActionHandler( @@ -1060,14 +1063,16 @@ describe('BridgeStatusController', () => { }); await withController(async ({ controller, messenger, rootMessenger }) => { - registerDefaultActionHandlers(rootMessenger); + registerDefaultActionHandlers(rootMessenger, { + status: TransactionStatus.confirmed, + }); const messengerCallSpy = jest.spyOn(messenger, 'call'); const messengerPublishSpy = jest.spyOn(messenger, 'publish'); const fetchBridgeTxStatusSpy = jest.spyOn( bridgeStatusUtils, 'fetchBridgeTxStatus', ); - const stopPollingByNetworkClientIdSpy = jest.spyOn( + const stopPollingByPollingTokenSpy = jest.spyOn( controller, 'stopPollingByPollingToken', ); @@ -1087,7 +1092,8 @@ describe('BridgeStatusController', () => { await flushPromises(); // Assertions - expect(stopPollingByNetworkClientIdSpy).toHaveBeenCalledTimes(1); + expect(fetchBridgeTxStatusSpy).toHaveBeenCalledTimes(1); + expect(stopPollingByPollingTokenSpy).toHaveBeenCalledTimes(1); expect(controller.state.txHistory).toStrictEqual( MockTxHistory.getComplete(), ); @@ -1220,71 +1226,83 @@ describe('BridgeStatusController', () => { }); }); - it('updates the srcTxHash when one is available', async () => { - // Setup - jest.useFakeTimers(); - let getStateCallCount = 0; + it.each([ + { + status: TransactionStatus.confirmed, + }, + { status: TransactionStatus.failed }, + { status: TransactionStatus.dropped }, + { status: TransactionStatus.rejected }, + { status: TransactionStatus.signed, shouldSetSrcTxHash: false }, + ])( + 'updates the srcTxHash when one is available, with status %s', + async ({ status, shouldSetSrcTxHash = true }) => { + // Setup + jest.useFakeTimers(); + let getStateCallCount = 0; - await withController( - { - options: { - fetchFn: jest - .fn() - .mockResolvedValueOnce(MockStatusResponse.getPending()), - traceFn: jest.fn(), + await withController( + { + options: { + fetchFn: jest + .fn() + .mockResolvedValueOnce(MockStatusResponse.getPending()), + traceFn: jest.fn(), + }, }, - }, - async ({ controller, rootMessenger }) => { - registerDefaultActionHandlers(rootMessenger); + async ({ controller, rootMessenger }) => { + registerDefaultActionHandlers(rootMessenger); - rootMessenger.unregisterActionHandler( - 'TransactionController:getState', - ); - rootMessenger.registerActionHandler( - 'TransactionController:getState', - // @ts-expect-error: Partial mock. - () => { - getStateCallCount += 1; - return { - transactions: [ - { - id: 'bridgeTxMetaId1', - hash: getStateCallCount === 0 ? undefined : '0xnewTxHash', - }, - ], - }; - }, - ); + rootMessenger.unregisterActionHandler( + 'TransactionController:getState', + ); + rootMessenger.registerActionHandler( + 'TransactionController:getState', + // @ts-expect-error: Partial mock. + () => { + getStateCallCount += 1; + return { + transactions: [ + { + id: 'bridgeTxMetaId1', + hash: getStateCallCount === 0 ? undefined : '0xnewTxHash', + status, + }, + ], + }; + }, + ); - // Start polling with no srcTxHash - const startPollingArgs = getMockStartPollingForBridgeTxStatusArgs({ - srcTxHash: 'undefined', - }); - rootMessenger.call( - 'BridgeStatusController:startPollingForBridgeTxStatus', - startPollingArgs, - ); + // Start polling with no srcTxHash + const startPollingArgs = getMockStartPollingForBridgeTxStatusArgs({ + srcTxHash: 'undefined', + }); + rootMessenger.call( + 'BridgeStatusController:startPollingForBridgeTxStatus', + startPollingArgs, + ); - // Verify initial state has no srcTxHash - expect( - controller.state.txHistory.bridgeTxMetaId1.status.srcChain.txHash, - ).toBeUndefined(); + // Verify initial state has no srcTxHash + expect( + controller.state.txHistory.bridgeTxMetaId1.status.srcChain.txHash, + ).toBeUndefined(); - // Advance timer to trigger polling with new hash - jest.advanceTimersByTime(10000); - await flushPromises(); + // Advance timer to trigger polling with new hash + jest.advanceTimersByTime(10000); + await flushPromises(); - // Verify the srcTxHash was updated - expect( - controller.state.txHistory.bridgeTxMetaId1.status.srcChain.txHash, - ).toBe('0xsrcTxHash1'); + // Verify the srcTxHash was updated + expect( + controller.state.txHistory.bridgeTxMetaId1.status.srcChain.txHash, + ).toBe(shouldSetSrcTxHash ? '0xsrcTxHash1' : undefined); - // Cleanup - controller.stopAllPolling(); - jest.restoreAllMocks(); - }, - ); - }); + // Cleanup + controller.stopAllPolling(); + jest.restoreAllMocks(); + }, + ); + }, + ); }); describe('resetState', () => { @@ -4639,11 +4657,9 @@ describe('BridgeStatusController', () => { 'TransactionController:getState', 'BridgeController:trackUnifiedSwapBridgeEvent', 'AccountsController:getAccountByAddress', + 'RemoteFeatureFlagController:getState', ], - events: [ - 'TransactionController:transactionFailed', - 'TransactionController:transactionConfirmed', - ], + events: ['TransactionController:transactionStatusUpdated'], }); jest @@ -4708,21 +4724,30 @@ describe('BridgeStatusController', () => { jest.useRealTimers(); }); - describe('TransactionController:transactionFailed', () => { + describe('TransactionController:transactionStatusUpdated (failed)', () => { it('should track failed event for bridge transaction', () => { const messengerCallSpy = jest.spyOn(mockBridgeStatusMessenger, 'call'); - mockMessenger.publish('TransactionController:transactionFailed', { - error: 'tx-error', - transactionMeta: { - chainId: CHAIN_IDS.ARBITRUM, - networkClientId: 'eth-id', - time: Date.now(), - txParams: {} as unknown as TransactionParams, - type: TransactionType.bridge, - status: TransactionStatus.failed, - id: 'bridgeTxMetaId1', + jest + .spyOn(Date, 'now') + .mockReturnValue( + MockTxHistory.getPending().bridgeTxMetaId1?.startTime ?? + 0 + DEFAULT_MAX_PENDING_HISTORY_ITEM_AGE_MS - 10000, + ); + mockMessenger.publish( + 'TransactionController:transactionStatusUpdated', + { + transactionMeta: { + error: { name: 'Error', message: 'tx-error' }, + chainId: CHAIN_IDS.ARBITRUM, + networkClientId: 'eth-id', + time: Date.now(), + txParams: {} as unknown as TransactionParams, + type: TransactionType.bridge, + status: TransactionStatus.failed, + id: 'bridgeTxMetaId1', + }, }, - }); + ); expect( bridgeStatusController.state.txHistory.bridgeTxMetaId1.status.status, @@ -4747,18 +4772,21 @@ describe('BridgeStatusController', () => { ); const messengerCallSpy = jest.spyOn(mockBridgeStatusMessenger, 'call'); - mockMessenger.publish('TransactionController:transactionFailed', { - error: 'tx-error', - transactionMeta: { - chainId: CHAIN_IDS.ARBITRUM, - networkClientId: 'eth-id', - time: Date.now(), - txParams: {} as unknown as TransactionParams, - type: TransactionType.bridge, - status: TransactionStatus.failed, - id: abTestsTxMetaId, + mockMessenger.publish( + 'TransactionController:transactionStatusUpdated', + { + transactionMeta: { + error: { name: 'Error', message: 'tx-error' }, + chainId: CHAIN_IDS.ARBITRUM, + networkClientId: 'eth-id', + time: Date.now(), + txParams: {} as unknown as TransactionParams, + type: TransactionType.bridge, + status: TransactionStatus.failed, + id: abTestsTxMetaId, + }, }, - }); + ); expect(messengerCallSpy).toHaveBeenCalledWith( 'BridgeController:trackUnifiedSwapBridgeEvent', @@ -4774,18 +4802,21 @@ describe('BridgeStatusController', () => { it('should track failed event for bridge transaction if approval is dropped', () => { const messengerCallSpy = jest.spyOn(mockBridgeStatusMessenger, 'call'); - mockMessenger.publish('TransactionController:transactionFailed', { - error: 'tx-error', - transactionMeta: { - chainId: CHAIN_IDS.ARBITRUM, - networkClientId: 'eth-id', - time: Date.now(), - txParams: {} as unknown as TransactionParams, - type: TransactionType.bridgeApproval, - status: TransactionStatus.dropped, - id: 'bridgeApprovalTxMetaId1', + mockMessenger.publish( + 'TransactionController:transactionStatusUpdated', + { + transactionMeta: { + error: { name: 'Error', message: 'tx-error' }, + chainId: CHAIN_IDS.ARBITRUM, + networkClientId: 'eth-id', + time: Date.now(), + txParams: {} as unknown as TransactionParams, + type: TransactionType.bridgeApproval, + status: TransactionStatus.dropped, + id: 'bridgeApprovalTxMetaId1', + }, }, - }); + ); expect(messengerCallSpy.mock.lastCall).toMatchSnapshot(); expect( @@ -4796,18 +4827,21 @@ describe('BridgeStatusController', () => { it('should not track failed event for bridge transaction with featureId', () => { const messengerCallSpy = jest.spyOn(mockBridgeStatusMessenger, 'call'); - mockMessenger.publish('TransactionController:transactionFailed', { - error: 'tx-error', - transactionMeta: { - chainId: CHAIN_IDS.ARBITRUM, - networkClientId: 'eth-id', - time: Date.now(), - txParams: {} as unknown as TransactionParams, - type: TransactionType.bridge, - status: TransactionStatus.failed, - id: 'perpsBridgeTxMetaId1', + mockMessenger.publish( + 'TransactionController:transactionStatusUpdated', + { + transactionMeta: { + error: { name: 'Error', message: 'tx-error' }, + chainId: CHAIN_IDS.ARBITRUM, + networkClientId: 'eth-id', + time: Date.now(), + txParams: {} as unknown as TransactionParams, + type: TransactionType.bridge, + status: TransactionStatus.failed, + id: 'perpsBridgeTxMetaId1', + }, }, - }); + ); expect( bridgeStatusController.state.txHistory.perpsBridgeTxMetaId1.status @@ -4818,41 +4852,55 @@ describe('BridgeStatusController', () => { it('should track failed event for swap transaction if approval fails', () => { const messengerCallSpy = jest.spyOn(mockBridgeStatusMessenger, 'call'); - mockMessenger.publish('TransactionController:transactionFailed', { - error: 'tx-error', - transactionMeta: { - chainId: CHAIN_IDS.ARBITRUM, - networkClientId: 'eth-id', - time: Date.now(), - txParams: {} as unknown as TransactionParams, - type: TransactionType.swapApproval, - status: TransactionStatus.failed, - id: 'bridgeApprovalTxMetaId1', + mockMessenger.publish( + 'TransactionController:transactionStatusUpdated', + { + transactionMeta: { + error: { name: 'Error', message: 'approval-tx-error' }, + chainId: CHAIN_IDS.ARBITRUM, + networkClientId: 'eth-id', + time: Date.now(), + txParams: {} as unknown as TransactionParams, + type: TransactionType.swapApproval, + status: TransactionStatus.failed, + id: 'bridgeApprovalTxMetaId1', + }, }, - }); + ); expect(messengerCallSpy.mock.lastCall).toMatchSnapshot(); expect( bridgeStatusController.state.txHistory.bridgeTxMetaId1WithApproval .status.status, ).toBe(StatusTypes.FAILED); + expect( + bridgeStatusController.state.txHistory.bridgeTxMetaId1WithApproval + .status.srcChain.txHash, + ).toBe('0xsrcTxHash1'); + expect( + bridgeStatusController.state.txHistory.bridgeTxMetaId1WithApproval + .approvalTxId, + ).toBe('bridgeApprovalTxMetaId1'); }); it('should track failed event for bridge transaction if not in txHistory', () => { const messengerCallSpy = jest.spyOn(mockBridgeStatusMessenger, 'call'); const expectedHistory = bridgeStatusController.state.txHistory; - mockMessenger.publish('TransactionController:transactionFailed', { - error: 'tx-error', - transactionMeta: { - chainId: CHAIN_IDS.ARBITRUM, - networkClientId: 'eth-id', - time: Date.now(), - txParams: {} as unknown as TransactionParams, - type: TransactionType.bridge, - status: TransactionStatus.failed, - id: 'bridgeTxMetaIda', + mockMessenger.publish( + 'TransactionController:transactionStatusUpdated', + { + transactionMeta: { + error: { name: 'Error', message: 'tx-error' }, + chainId: CHAIN_IDS.ARBITRUM, + networkClientId: 'eth-id', + time: Date.now(), + txParams: {} as unknown as TransactionParams, + type: TransactionType.bridge, + status: TransactionStatus.failed, + id: 'bridgeTxMetaIda', + }, }, - }); + ); expect(bridgeStatusController.state.txHistory).toStrictEqual( expectedHistory, @@ -4862,18 +4910,21 @@ describe('BridgeStatusController', () => { it('should track failed event for swap transaction', () => { const messengerCallSpy = jest.spyOn(mockBridgeStatusMessenger, 'call'); - mockMessenger.publish('TransactionController:transactionFailed', { - error: 'tx-error', - transactionMeta: { - chainId: CHAIN_IDS.ARBITRUM, - networkClientId: 'eth-id', - time: Date.now(), - txParams: {} as unknown as TransactionParams, - type: TransactionType.swap, - status: TransactionStatus.failed, - id: 'swapTxMetaId1', + mockMessenger.publish( + 'TransactionController:transactionStatusUpdated', + { + transactionMeta: { + error: { name: 'Error', message: 'tx-error' }, + chainId: CHAIN_IDS.ARBITRUM, + networkClientId: 'eth-id', + time: Date.now(), + txParams: {} as unknown as TransactionParams, + type: TransactionType.swap, + status: TransactionStatus.failed, + id: 'swapTxMetaId1', + }, }, - }); + ); expect( bridgeStatusController.state.txHistory.swapTxMetaId1.status.status, @@ -4883,54 +4934,63 @@ describe('BridgeStatusController', () => { it('should not track failed event for signed status', () => { const messengerCallSpy = jest.spyOn(mockBridgeStatusMessenger, 'call'); - mockMessenger.publish('TransactionController:transactionFailed', { - error: 'tx-error', - transactionMeta: { - chainId: CHAIN_IDS.ARBITRUM, - networkClientId: 'eth-id', - time: Date.now(), - txParams: {} as unknown as TransactionParams, - type: TransactionType.swap, - status: TransactionStatus.signed, - id: 'swapTxMetaId1', + mockMessenger.publish( + 'TransactionController:transactionStatusUpdated', + { + transactionMeta: { + error: { name: 'Error', message: 'tx-error' }, + chainId: CHAIN_IDS.ARBITRUM, + networkClientId: 'eth-id', + time: Date.now(), + txParams: {} as unknown as TransactionParams, + type: TransactionType.swap, + status: TransactionStatus.signed, + id: 'swapTxMetaId1', + }, }, - }); + ); expect(messengerCallSpy.mock.calls).toMatchSnapshot(); }); it('should not track failed event for approved status', () => { const messengerCallSpy = jest.spyOn(mockBridgeStatusMessenger, 'call'); - mockMessenger.publish('TransactionController:transactionFailed', { - error: 'tx-error', - transactionMeta: { - chainId: CHAIN_IDS.ARBITRUM, - networkClientId: 'eth-id', - time: Date.now(), - txParams: {} as unknown as TransactionParams, - type: TransactionType.swap, - status: TransactionStatus.approved, - id: 'swapTxMetaId1', + mockMessenger.publish( + 'TransactionController:transactionStatusUpdated', + { + transactionMeta: { + error: { name: 'Error', message: 'tx-error' }, + chainId: CHAIN_IDS.ARBITRUM, + networkClientId: 'eth-id', + time: Date.now(), + txParams: {} as unknown as TransactionParams, + type: TransactionType.swap, + status: TransactionStatus.approved, + id: 'swapTxMetaId1', + }, }, - }); + ); expect(messengerCallSpy.mock.calls).toMatchSnapshot(); }); it('should not track failed event for other transaction types', () => { const messengerCallSpy = jest.spyOn(mockBridgeStatusMessenger, 'call'); - mockMessenger.publish('TransactionController:transactionFailed', { - error: 'tx-error', - transactionMeta: { - chainId: CHAIN_IDS.ARBITRUM, - networkClientId: 'eth-id', - time: Date.now(), - txParams: {} as unknown as TransactionParams, - type: TransactionType.simpleSend, - status: TransactionStatus.failed, - id: 'simpleSendTxMetaId1', + mockMessenger.publish( + 'TransactionController:transactionStatusUpdated', + { + transactionMeta: { + error: { name: 'Error', message: 'tx-error' }, + chainId: CHAIN_IDS.ARBITRUM, + networkClientId: 'eth-id', + time: Date.now(), + txParams: {} as unknown as TransactionParams, + type: TransactionType.simpleSend, + status: TransactionStatus.failed, + id: 'simpleSendTxMetaId1', + }, }, - }); + ); expect(messengerCallSpy.mock.calls).toMatchSnapshot(); }); @@ -4941,19 +5001,22 @@ describe('BridgeStatusController', () => { const unknownTxMetaId = 'unknown-tx-meta-id'; const messengerCallSpy = jest.spyOn(mockBridgeStatusMessenger, 'call'); - mockMessenger.publish('TransactionController:transactionFailed', { - error: 'tx-error', - transactionMeta: { - chainId: CHAIN_IDS.ARBITRUM, - networkClientId: 'eth-id', - time: Date.now(), - txParams: {} as unknown as TransactionParams, - type: TransactionType.bridge, - status: TransactionStatus.failed, - id: unknownTxMetaId, - actionId, // ActionId matches the history entry + mockMessenger.publish( + 'TransactionController:transactionStatusUpdated', + { + transactionMeta: { + error: { name: 'Error', message: 'tx-error' }, + chainId: CHAIN_IDS.ARBITRUM, + networkClientId: 'eth-id', + time: Date.now(), + txParams: {} as unknown as TransactionParams, + type: TransactionType.bridge, + status: TransactionStatus.failed, + id: unknownTxMetaId, + actionId, // ActionId matches the history entry + }, }, - }); + ); // Verify: History entry keyed by actionId should be marked as failed expect( @@ -4968,19 +5031,22 @@ describe('BridgeStatusController', () => { const messengerCallSpy = jest.spyOn(mockBridgeStatusMessenger, 'call'); - mockMessenger.publish('TransactionController:transactionFailed', { - error: 'tx-error', - transactionMeta: { - chainId: CHAIN_IDS.ARBITRUM, - networkClientId: 'eth-id', - time: Date.now(), - txParams: {} as unknown as TransactionParams, - type: TransactionType.bridge, - status: TransactionStatus.failed, - id: 'non-existent-tx-id', - actionId, + mockMessenger.publish( + 'TransactionController:transactionStatusUpdated', + { + transactionMeta: { + error: { name: 'Error', message: 'tx-error' }, + chainId: CHAIN_IDS.ARBITRUM, + networkClientId: 'eth-id', + time: Date.now(), + txParams: {} as unknown as TransactionParams, + type: TransactionType.bridge, + status: TransactionStatus.failed, + id: 'non-existent-tx-id', + actionId, + }, }, - }); + ); // The Failed event should be tracked with the history data from actionId lookup expect(messengerCallSpy).toHaveBeenCalled(); @@ -4995,19 +5061,22 @@ describe('BridgeStatusController', () => { const messengerCallSpy = jest.spyOn(mockBridgeStatusMessenger, 'call'); - mockMessenger.publish('TransactionController:transactionFailed', { - error: 'User rejected', - transactionMeta: { - chainId: CHAIN_IDS.ARBITRUM, - networkClientId: 'eth-id', - time: Date.now(), - txParams: {} as unknown as TransactionParams, - type: TransactionType.bridge, - status: TransactionStatus.rejected, - id: 'rejected-tx-id', - actionId, + mockMessenger.publish( + 'TransactionController:transactionStatusUpdated', + { + transactionMeta: { + error: { name: 'Error', message: 'User rejected' }, + chainId: CHAIN_IDS.ARBITRUM, + networkClientId: 'eth-id', + time: Date.now(), + txParams: {} as unknown as TransactionParams, + type: TransactionType.bridge, + status: TransactionStatus.rejected, + id: 'rejected-tx-id', + actionId, + }, }, - }); + ); // Status should still be marked as failed expect( @@ -5019,7 +5088,7 @@ describe('BridgeStatusController', () => { }); }); - describe('TransactionController:transactionConfirmed', () => { + describe('TransactionController:transactionStatusUpdated (confirmed)', () => { beforeEach(() => { jest.clearAllMocks(); }); @@ -5036,21 +5105,26 @@ describe('BridgeStatusController', () => { 'BridgeStatusController:getBridgeHistoryItemByTxMetaId', 'bridgeTxMetaId1', ); - mockMessenger.publish('TransactionController:transactionConfirmed', { - chainId: CHAIN_IDS.ARBITRUM, - networkClientId: 'eth-id', - time: Date.now(), - txParams: {} as unknown as TransactionParams, - type: TransactionType.bridge, - status: TransactionStatus.confirmed, - id: 'bridgeTxMetaId1', - }); + mockMessenger.publish( + 'TransactionController:transactionStatusUpdated', + { + transactionMeta: { + chainId: CHAIN_IDS.ARBITRUM, + networkClientId: 'eth-id', + time: Date.now(), + txParams: {} as unknown as TransactionParams, + type: TransactionType.bridge, + status: TransactionStatus.confirmed, + id: 'bridgeTxMetaId1', + }, + }, + ); jest.advanceTimersByTime(500); bridgeStatusController.stopAllPolling(); await flushPromises(); - expect(messengerCallSpy.mock.lastCall).toMatchSnapshot(); + expect(messengerCallSpy.mock.calls).toMatchSnapshot(); expect(mockFetchFn).toHaveBeenCalledTimes(3); expect(mockFetchFn).toHaveBeenCalledWith( 'https://bridge.api.cx.metamask.io/getTxStatus?bridgeId=lifi&srcTxHash=0xsrcTxHash1&bridge=across&srcChainId=42161&destChainId=10&refuel=false&requestId=197c402f-cb96-4096-9f8c-54aed84ca776', @@ -5079,15 +5153,20 @@ describe('BridgeStatusController', () => { mockFetchFn.mockResolvedValueOnce( MockStatusResponse.getComplete({ srcTxHash: '0xperpsSrcTxHash1' }), ); - mockMessenger.publish('TransactionController:transactionConfirmed', { - chainId: CHAIN_IDS.ARBITRUM, - networkClientId: 'eth-id', - time: Date.now(), - txParams: {} as unknown as TransactionParams, - type: TransactionType.bridge, - status: TransactionStatus.confirmed, - id: 'perpsBridgeTxMetaId1', - }); + mockMessenger.publish( + 'TransactionController:transactionStatusUpdated', + { + transactionMeta: { + chainId: CHAIN_IDS.ARBITRUM, + networkClientId: 'eth-id', + time: Date.now(), + txParams: {} as unknown as TransactionParams, + type: TransactionType.bridge, + status: TransactionStatus.confirmed, + id: 'perpsBridgeTxMetaId1', + }, + }, + ); jest.advanceTimersByTime(30500); bridgeStatusController.stopAllPolling(); @@ -5101,6 +5180,23 @@ describe('BridgeStatusController', () => { [ "AuthenticationController:getBearerToken", ], + [ + "RemoteFeatureFlagController:getState", + ], + [ + "TransactionController:getState", + ], + [ + "TransactionController:getState", + ], + [ + "NetworkController:findNetworkClientIdByChainId", + "0xa4b1", + ], + [ + "NetworkController:getNetworkClientById", + Promise {}, + ], ] `); expect(mockFetchFn).toHaveBeenCalledWith( @@ -5125,15 +5221,20 @@ describe('BridgeStatusController', () => { mockFetchFn.mockResolvedValueOnce( MockStatusResponse.getFailed({ srcTxHash: '0xperpsSrcTxHash1' }), ); - mockMessenger.publish('TransactionController:transactionConfirmed', { - chainId: CHAIN_IDS.ARBITRUM, - networkClientId: 'eth-id', - time: Date.now(), - txParams: {} as unknown as TransactionParams, - type: TransactionType.bridge, - status: TransactionStatus.confirmed, - id: 'perpsBridgeTxMetaId1', - }); + mockMessenger.publish( + 'TransactionController:transactionStatusUpdated', + { + transactionMeta: { + chainId: CHAIN_IDS.ARBITRUM, + networkClientId: 'eth-id', + time: Date.now(), + txParams: {} as unknown as TransactionParams, + type: TransactionType.bridge, + status: TransactionStatus.confirmed, + id: 'perpsBridgeTxMetaId1', + }, + }, + ); jest.advanceTimersByTime(40500); bridgeStatusController.stopAllPolling(); @@ -5147,6 +5248,23 @@ describe('BridgeStatusController', () => { [ "AuthenticationController:getBearerToken", ], + [ + "RemoteFeatureFlagController:getState", + ], + [ + "TransactionController:getState", + ], + [ + "TransactionController:getState", + ], + [ + "NetworkController:findNetworkClientIdByChainId", + "0xa4b1", + ], + [ + "NetworkController:getNetworkClientById", + Promise {}, + ], ] `); expect(mockFetchFn).toHaveBeenCalledWith( @@ -5166,60 +5284,80 @@ describe('BridgeStatusController', () => { it('should track completed event for swap transaction', () => { const messengerCallSpy = jest.spyOn(mockBridgeStatusMessenger, 'call'); - mockMessenger.publish('TransactionController:transactionConfirmed', { - chainId: CHAIN_IDS.ARBITRUM, - networkClientId: 'eth-id', - time: Date.now(), - txParams: {} as unknown as TransactionParams, - type: TransactionType.swap, - status: TransactionStatus.confirmed, - id: 'swapTxMetaId1', - }); + mockMessenger.publish( + 'TransactionController:transactionStatusUpdated', + { + transactionMeta: { + chainId: CHAIN_IDS.ARBITRUM, + networkClientId: 'eth-id', + time: Date.now(), + txParams: {} as unknown as TransactionParams, + type: TransactionType.swap, + status: TransactionStatus.confirmed, + id: 'swapTxMetaId1', + }, + }, + ); expect(messengerCallSpy.mock.calls).toMatchSnapshot(); }); it('should not track completed event for swap transaction with featureId', () => { const messengerCallSpy = jest.spyOn(mockBridgeStatusMessenger, 'call'); - mockMessenger.publish('TransactionController:transactionConfirmed', { - chainId: CHAIN_IDS.ARBITRUM, - networkClientId: 'eth-id', - time: Date.now(), - txParams: {} as unknown as TransactionParams, - type: TransactionType.swap, - status: TransactionStatus.confirmed, - id: 'perpsSwapTxMetaId1', - }); + mockMessenger.publish( + 'TransactionController:transactionStatusUpdated', + { + transactionMeta: { + chainId: CHAIN_IDS.ARBITRUM, + networkClientId: 'eth-id', + time: Date.now(), + txParams: {} as unknown as TransactionParams, + type: TransactionType.swap, + status: TransactionStatus.confirmed, + id: 'perpsSwapTxMetaId1', + }, + }, + ); expect(messengerCallSpy).not.toHaveBeenCalled(); }); it('should not track completed event for other transaction types', () => { const messengerCallSpy = jest.spyOn(mockBridgeStatusMessenger, 'call'); - mockMessenger.publish('TransactionController:transactionConfirmed', { - chainId: CHAIN_IDS.ARBITRUM, - networkClientId: 'eth-id', - time: Date.now(), - txParams: {} as unknown as TransactionParams, - type: TransactionType.bridge, - status: TransactionStatus.confirmed, - id: 'bridgeTxMetaId1', - }); + mockMessenger.publish( + 'TransactionController:transactionStatusUpdated', + { + transactionMeta: { + chainId: CHAIN_IDS.ARBITRUM, + networkClientId: 'eth-id', + time: Date.now(), + txParams: {} as unknown as TransactionParams, + type: TransactionType.bridge, + status: TransactionStatus.confirmed, + id: 'bridgeTxMetaId1', + }, + }, + ); expect(messengerCallSpy.mock.calls).toMatchSnapshot(); }); it('should not start polling for bridge tx if tx is not in txHistory', () => { const messengerCallSpy = jest.spyOn(mockBridgeStatusMessenger, 'call'); - mockMessenger.publish('TransactionController:transactionConfirmed', { - chainId: CHAIN_IDS.ARBITRUM, - networkClientId: 'eth-id', - time: Date.now(), - txParams: {} as unknown as TransactionParams, - type: TransactionType.bridge, - status: TransactionStatus.confirmed, - id: 'bridgeTxMetaId1Unknown', - }); + mockMessenger.publish( + 'TransactionController:transactionStatusUpdated', + { + transactionMeta: { + chainId: CHAIN_IDS.ARBITRUM, + networkClientId: 'eth-id', + time: Date.now(), + txParams: {} as unknown as TransactionParams, + type: TransactionType.bridge, + status: TransactionStatus.confirmed, + id: 'bridgeTxMetaId1Unknown', + }, + }, + ); expect(messengerCallSpy.mock.calls).toMatchSnapshot(); }); @@ -5229,7 +5367,7 @@ describe('BridgeStatusController', () => { consoleFnSpy = jest .spyOn(console, 'error') .mockImplementationOnce(jest.fn()); - consoleFnSpy.mockImplementationOnce(jest.fn()); + consoleFnSpy.mockImplementation(jest.fn()); messengerCallSpy.mockImplementation(() => { throw new Error( @@ -5240,15 +5378,20 @@ describe('BridgeStatusController', () => { mockFetchFn.mockResolvedValueOnce( MockStatusResponse.getComplete({ srcTxHash: '0xperpsSrcTxHash1' }), ); - mockMessenger.publish('TransactionController:transactionConfirmed', { - chainId: CHAIN_IDS.ARBITRUM, - networkClientId: 'eth-id', - time: Date.now(), - txParams: {} as unknown as TransactionParams, - type: TransactionType.bridge, - status: TransactionStatus.confirmed, - id: 'perpsBridgeTxMetaId1', - }); + mockMessenger.publish( + 'TransactionController:transactionStatusUpdated', + { + transactionMeta: { + chainId: CHAIN_IDS.ARBITRUM, + networkClientId: 'eth-id', + time: Date.now(), + txParams: {} as unknown as TransactionParams, + type: TransactionType.bridge, + status: TransactionStatus.confirmed, + id: 'perpsBridgeTxMetaId1', + }, + }, + ); jest.advanceTimersByTime(30500); bridgeStatusController.stopAllPolling(); @@ -5262,6 +5405,12 @@ describe('BridgeStatusController', () => { [ "AuthenticationController:getBearerToken", ], + [ + "RemoteFeatureFlagController:getState", + ], + [ + "RemoteFeatureFlagController:getState", + ], ] `); expect(mockFetchFn).toHaveBeenCalledWith( @@ -5272,6 +5421,17 @@ describe('BridgeStatusController', () => { ); expect(consoleFnSpy.mock.calls).toMatchInlineSnapshot(` [ + [ + "======TransactionController:transactionStatusUpdated", + { + "actionId": undefined, + "batchId": undefined, + "hash": undefined, + "id": "perpsBridgeTxMetaId1", + "status": "confirmed", + "type": "bridge", + }, + ], [ "Error getting JWT token for bridge-api request", [Error: AuthenticationController:getBearerToken not implemented], @@ -5280,6 +5440,9 @@ describe('BridgeStatusController', () => { "Error getting JWT token for bridge-api request", [Error: AuthenticationController:getBearerToken not implemented], ], + [ + [Error: AuthenticationController:getBearerToken not implemented], + ], ] `); }); diff --git a/packages/bridge-status-controller/src/bridge-status-controller.ts b/packages/bridge-status-controller/src/bridge-status-controller.ts index 027e154b9d8..c1936684f32 100644 --- a/packages/bridge-status-controller/src/bridge-status-controller.ts +++ b/packages/bridge-status-controller/src/bridge-status-controller.ts @@ -216,7 +216,7 @@ export class BridgeStatusController extends StaticIntervalPollingController( 'TransactionController:transactionStatusUpdated', ({ txMeta, historyKey, historyItem }) => { - if (!txMeta || !historyKey || !historyItem) { + if (!txMeta) { console.error( '======TransactionController:transactionStatusUpdated NOT FOUND', ); @@ -232,8 +232,22 @@ export class BridgeStatusController extends StaticIntervalPollingController { - this.#updateHistoryItem(historyKey, { + this.#updateHistoryItem({ + historyKey, status: StatusTypes.FAILED, - txHash: txMeta?.hash, + txHash: + !txMeta.type || + [TransactionType.bridge, TransactionType.swap].includes(txMeta.type) + ? txMeta.hash + : undefined, + approvalTxId: + !txMeta.type || + [TransactionType.bridgeApproval, TransactionType.swapApproval].includes( + txMeta.type, + ) + ? txMeta.hash + : undefined, }); + if (txMeta.status === TransactionStatus.rejected) { + return; + } + this.#trackUnifiedSwapBridgeEvent( UnifiedSwapBridgeEventName.Failed, historyKey, @@ -559,17 +589,18 @@ export class BridgeStatusController extends StaticIntervalPollingController { - this.#updateHistoryItem(historyKey, { + this.#updateHistoryItem({ + historyKey, txHash: txMeta.hash, }); - console.log('======TransactionController:transactionConfirmed', txMeta); switch (txMeta.type) { case TransactionType.swap: - this.#updateHistoryItem(historyKey, { + this.#updateHistoryItem({ + historyKey, status: StatusTypes.COMPLETE, }); this.#trackUnifiedSwapBridgeEvent( @@ -578,7 +609,9 @@ export class BridgeStatusController extends StaticIntervalPollingController { + readonly #updateHistoryItem = ({ + historyKey, + status, + txHash, + attempts, + approvalTxId, + }: { + historyKey?: string; + status?: StatusTypes; + txHash?: string; + attempts?: BridgeHistoryItem['attempts']; + approvalTxId?: string; + }): void => { + if (!historyKey) { + return; + } this.update((currentState) => { if (!currentState.txHistory[historyKey]) { return; @@ -903,6 +949,9 @@ export class BridgeStatusController extends StaticIntervalPollingController { it('should return correct properties for a successful swap transaction', () => { const result = getEVMTxPropertiesFromTransactionMeta(mockTransactionMeta); expect(result).toStrictEqual({ - error_message: '', + error_message: 'Transaction submitted', chain_id_source: 'eip155:1', chain_id_destination: 'eip155:1', token_symbol_source: 'ETH', @@ -1058,14 +1058,14 @@ describe('metrics utils', () => { ...mockTransactionMeta, status: TransactionStatus.failed, error: { - message: 'Transaction failed', + message: 'Error message', name: 'Error', } as TransactionError, }; const result = getEVMTxPropertiesFromTransactionMeta( failedTransactionMeta, ); - expect(result.error_message).toBe('Transaction failed'); + expect(result.error_message).toBe('Transaction failed. Error message'); expect(result.source_transaction).toBe('FAILED'); });