From 791b619931be00b2c4e5cd0a3d1632e57ec3fafc Mon Sep 17 00:00:00 2001 From: Frederic HENG Date: Tue, 25 Aug 2026 10:07:58 +0200 Subject: [PATCH 1/6] feat(bridge): emit SwapBridge failure_phase, error_code, and hash presence Wire Quotes Error, submit Failed, EVM Failed, poll Failed, Submitted, and Completed to the classifiers from #9947. Mixpanel can now split quote vs broadcast vs source vs dest vs poll without parsing error_message. --- packages/bridge-controller/CHANGELOG.md | 1 + .../bridge-controller.sse.test.ts.snap | 4 ++ .../src/bridge-controller.ts | 42 +++++++++++++------ .../bridge-status-controller/CHANGELOG.md | 1 + ...ridge-status-controller.batch-sell.test.ts | 14 +++++++ .../src/bridge-status-controller.test.ts | 33 +++++++++++++++ .../src/bridge-status-controller.ts | 34 +++++++++++++++ .../src/utils/metrics.test.ts | 22 ++++++++++ .../src/utils/metrics.ts | 7 ++++ 9 files changed, 145 insertions(+), 13 deletions(-) diff --git a/packages/bridge-controller/CHANGELOG.md b/packages/bridge-controller/CHANGELOG.md index 2aaeb693fdc..662f53093bc 100644 --- a/packages/bridge-controller/CHANGELOG.md +++ b/packages/bridge-controller/CHANGELOG.md @@ -14,6 +14,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - Optional `failure_phase` and `error_code` on Quotes Error and Failed event context types - Optional `source_hash_present` and `destination_hash_present` on Failed, Submitted, and Completed event context types - Submit and status classifiers live in `@metamask/bridge-status-controller` +- Emit `failure_phase`, `error_code`, `source_hash_present`, and `destination_hash_present` on Quotes Error and Failed events ## [80.1.0] diff --git a/packages/bridge-controller/src/__snapshots__/bridge-controller.sse.test.ts.snap b/packages/bridge-controller/src/__snapshots__/bridge-controller.sse.test.ts.snap index 7fcfe0b4439..f7d7cf47f39 100644 --- a/packages/bridge-controller/src/__snapshots__/bridge-controller.sse.test.ts.snap +++ b/packages/bridge-controller/src/__snapshots__/bridge-controller.sse.test.ts.snap @@ -159,7 +159,9 @@ exports[`BridgeController SSE should reset quotes list if quote refresh fails 2` "chain_id_destination": "solana:5eykt4UsFv8P8NJdTREpY1vzqKqZKvdp", "chain_id_source": "eip155:1", "custom_slippage": true, + "error_code": "non_error_rejection", "error_message": "Network error", + "failure_phase": "quote", "feature_id": "unified_swap_bridge", "has_sufficient_funds": true, "is_hardware_wallet": false, @@ -293,7 +295,9 @@ exports[`BridgeController SSE should rethrow error from server 3`] = ` "chain_id_destination": "solana:5eykt4UsFv8P8NJdTREpY1vzqKqZKvdp", "chain_id_source": "eip155:1", "custom_slippage": true, + "error_code": "quote_fetch_failed", "error_message": "Bridge-api error: timeout from server", + "failure_phase": "quote", "feature_id": "unified_swap_bridge", "has_sufficient_funds": true, "is_hardware_wallet": false, diff --git a/packages/bridge-controller/src/bridge-controller.ts b/packages/bridge-controller/src/bridge-controller.ts index 18780eedec8..33d3100ba67 100644 --- a/packages/bridge-controller/src/bridge-controller.ts +++ b/packages/bridge-controller/src/bridge-controller.ts @@ -61,14 +61,17 @@ import { import { AbortReason, BatchSellMetricsEventName, + FailurePhase, MetaMetricsSwapsEventSource, MetricsActionType, + SwapBridgeErrorCode, UnifiedSwapBridgeEventName, } from './utils/metrics/constants.js'; import type { BridgeControllerMetricsEventName, BridgeControllerMetricsLocation, } from './utils/metrics/constants.js'; +import { getQuoteFetchErrorCode } from './utils/metrics/failure-telemetry.js'; import { formatProviderLabel, getAccountHardwareType, @@ -977,10 +980,11 @@ export class BridgeController extends StaticIntervalPollingController { chain_id_destination: 'eip155:10', chain_id_source: 'eip155:10', custom_slippage: false, + destination_hash_present: false, feature_id: FeatureId.BATCH_SELL, gas_included: gasIncluded, gas_included_7702: gasIncluded7702, @@ -327,6 +328,7 @@ describe('BridgeStatusController', () => { provider: 'socket_across', quoted_time_minutes: 1, slippage_limit: 0, + source_hash_present: false, stx_enabled: stxEnabled, swap_type: 'single_chain', token_address_destination: @@ -551,6 +553,7 @@ describe('BridgeStatusController', () => { chain_id_destination: 'eip155:10', chain_id_source: 'eip155:10', custom_slippage: true, + destination_hash_present: false, destination_transaction: 'PENDING', gas_included: gasIncluded, gas_included_7702: gasIncluded7702, @@ -563,6 +566,7 @@ describe('BridgeStatusController', () => { quoted_vs_used_gas_ratio: 0, security_warnings: [], slippage_limit: 0, + source_hash_present: true, source_transaction: 'COMPLETE', stx_enabled: stxEnabled, swap_type: 'single_chain', @@ -629,8 +633,11 @@ describe('BridgeStatusController', () => { chain_id_destination: 'eip155:10', chain_id_source: 'eip155:10', custom_slippage: true, + destination_hash_present: false, destination_transaction: 'FAILED', + error_code: 'missing_error_object', error_message: 'Transaction failed', + failure_phase: 'source_execution', feature_id: FeatureId.BATCH_SELL, gas_included: gasIncluded, gas_included_7702: gasIncluded7702, @@ -645,6 +652,7 @@ describe('BridgeStatusController', () => { quoted_vs_used_gas_ratio: 0, security_warnings: [], slippage_limit: 0, + source_hash_present: true, source_transaction: 'COMPLETE', stx_enabled: stxEnabled, swap_type: 'single_chain', @@ -765,6 +773,7 @@ describe('BridgeStatusController', () => { chain_id_destination: 'eip155:10', chain_id_source: 'eip155:10', custom_slippage: false, + destination_hash_present: false, feature_id: FeatureId.BATCH_SELL, gas_included: gasIncluded, gas_included_7702: gasIncluded7702, @@ -774,6 +783,7 @@ describe('BridgeStatusController', () => { provider: 'socket_across', quoted_time_minutes: 1, slippage_limit: 0, + source_hash_present: false, stx_enabled: stxEnabled, swap_type: 'single_chain', token_address_destination: @@ -807,8 +817,11 @@ describe('BridgeStatusController', () => { chain_id_destination: 'eip155:10', chain_id_source: 'eip155:10', custom_slippage: false, + destination_hash_present: false, + error_code: 'unknown', error_message: 'Failed to add BatchSell trade to history: txMeta not found', + failure_phase: 'broadcast', feature_id: FeatureId.BATCH_SELL, gas_included: false, gas_included_7702: true, @@ -818,6 +831,7 @@ describe('BridgeStatusController', () => { provider: 'socket_across', quoted_time_minutes: 1, slippage_limit: 0, + source_hash_present: false, stx_enabled: false, swap_type: 'single_chain', token_address_destination: 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 0799c778f90..2b2f888899c 100644 --- a/packages/bridge-status-controller/src/bridge-status-controller.test.ts +++ b/packages/bridge-status-controller/src/bridge-status-controller.test.ts @@ -1778,6 +1778,19 @@ describe('BridgeStatusController', () => { // Assertions expect(fetchBridgeTxStatusSpy).toHaveBeenCalledTimes(1); expect(messengerCallSpy.mock.calls).toMatchSnapshot(); + const failedCall = messengerCallSpy.mock.calls.find( + ([action, eventName]) => + action === 'BridgeController:trackUnifiedSwapBridgeEvent' && + eventName === UnifiedSwapBridgeEventName.Failed, + ); + expect(failedCall?.[2]).toStrictEqual( + expect.objectContaining({ + failure_phase: 'source_execution', + error_code: 'status_failed_without_reason', + source_hash_present: true, + destination_hash_present: false, + }), + ); expect(messengerPublishSpy).not.toHaveBeenCalledWith( 'BridgeStatusController:destinationTransactionCompleted', ); @@ -5004,7 +5017,10 @@ describe('BridgeStatusController', () => { "chain_id_destination": "eip155:42161", "chain_id_source": "eip155:42161", "custom_slippage": false, + "destination_hash_present": false, + "error_code": "unknown", "error_message": "Failed to submit cross-chain swap batch transaction: unknown account in trade data", + "failure_phase": "broadcast", "feature_id": "unified_swap_bridge", "gas_included": false, "gas_included_7702": false, @@ -5014,6 +5030,7 @@ describe('BridgeStatusController', () => { "provider": "lifi_across", "quoted_time_minutes": 0, "slippage_limit": 0, + "source_hash_present": false, "stx_enabled": true, "swap_type": "single_chain", "token_address_destination": "eip155:10/slip44:60", @@ -5093,7 +5110,10 @@ describe('BridgeStatusController', () => { "chain_id_destination": "eip155:42161", "chain_id_source": "eip155:42161", "custom_slippage": false, + "destination_hash_present": false, + "error_code": "unknown", "error_message": "Failed to update cross-chain swap transaction batch: tradeMeta not found", + "failure_phase": "broadcast", "feature_id": "unified_swap_bridge", "gas_included": false, "gas_included_7702": false, @@ -5103,6 +5123,7 @@ describe('BridgeStatusController', () => { "provider": "lifi_across", "quoted_time_minutes": 0, "slippage_limit": 0, + "source_hash_present": false, "stx_enabled": true, "swap_type": "single_chain", "token_address_destination": "eip155:10/slip44:60", @@ -5686,7 +5707,10 @@ describe('BridgeStatusController', () => { "chain_id_destination": "eip155:42161", "chain_id_source": "eip155:42161", "custom_slippage": false, + "destination_hash_present": false, + "error_code": "unknown", "error_message": "Transaction failed. tx-error", + "failure_phase": "broadcast", "feature_id": "unified_swap_bridge", "gas_included": false, "gas_included_7702": false, @@ -5699,6 +5723,7 @@ describe('BridgeStatusController', () => { "quoted_vs_used_gas_ratio": 0, "security_warnings": [], "slippage_limit": 0, + "source_hash_present": false, "source_transaction": "FAILED", "stx_enabled": false, "swap_type": "crosschain", @@ -5874,8 +5899,11 @@ describe('BridgeStatusController', () => { "chain_id_destination": "eip155:42161", "chain_id_source": "eip155:42161", "custom_slippage": true, + "destination_hash_present": false, "destination_transaction": "FAILED", + "error_code": "unknown", "error_message": "Transaction failed. tx-error", + "failure_phase": "broadcast", "feature_id": "quick_buy_follow_trading", "gas_included": false, "gas_included_7702": false, @@ -5888,6 +5916,7 @@ describe('BridgeStatusController', () => { "quoted_vs_used_gas_ratio": 0, "security_warnings": [], "slippage_limit": 0, + "source_hash_present": true, "source_transaction": "COMPLETE", "stx_enabled": false, "swap_type": "single_chain", @@ -5952,8 +5981,11 @@ describe('BridgeStatusController', () => { "chain_id_destination": "eip155:42161", "chain_id_source": "eip155:42161", "custom_slippage": true, + "destination_hash_present": false, "destination_transaction": "FAILED", + "error_code": "unknown", "error_message": "Transaction failed. tx-error", + "failure_phase": "broadcast", "feature_id": "quick_buy_explore", "gas_included": false, "gas_included_7702": false, @@ -5966,6 +5998,7 @@ describe('BridgeStatusController', () => { "quoted_vs_used_gas_ratio": 0, "security_warnings": [], "slippage_limit": 0, + "source_hash_present": true, "source_transaction": "COMPLETE", "stx_enabled": false, "swap_type": "single_chain", diff --git a/packages/bridge-status-controller/src/bridge-status-controller.ts b/packages/bridge-status-controller/src/bridge-status-controller.ts index ab7f4e27328..1e22f2c3c48 100644 --- a/packages/bridge-status-controller/src/bridge-status-controller.ts +++ b/packages/bridge-status-controller/src/bridge-status-controller.ts @@ -11,6 +11,7 @@ import { toQuoteMetadataV1, toQuoteResponseV1, isQuoteResponseV2, + SwapBridgeErrorCode, } from '@metamask/bridge-controller'; import type { QuoteMetadataMigrationPhase } from '@metamask/bridge-controller'; import { @@ -96,6 +97,9 @@ import { getEVMTxPropertiesFromTransactionMeta, getTxStatusesFromHistory, getPreConfirmationPropertiesFromQuote, + getHashPresenceProperties, + getSubmitFailureTelemetry, + getStatusFailureTelemetry, } from './utils/metrics.js'; import { getSelectedChainId } from './utils/network.js'; import { @@ -1538,6 +1542,8 @@ export class BridgeStatusController extends StaticIntervalPollingController tx.id === approvalTxId, ); + const failedProperties = eventProperties as + | RequiredEventContextFromClient[typeof UnifiedSwapBridgeEventName.Failed] + | undefined; + + const historyHashPresence = getHashPresenceProperties( + historyItem.status.srcChain.txHash, + historyItem.status.destChain?.txHash, + ); + const requiredEventProperties = { ...baseProperties, ...requestParamProperties, @@ -1839,6 +1855,7 @@ export class BridgeStatusController extends StaticIntervalPollingController { usd_actual_return: 0, usd_actual_gas: 0, action_type: MetricsActionType.SWAPBRIDGE_V1, + source_hash_present: false, + destination_hash_present: false, + failure_phase: FailurePhase.Broadcast, + error_code: SwapBridgeErrorCode.MissingErrorObject, }); }); @@ -1193,6 +1197,24 @@ describe('metrics utils', () => { ); expect(result.error_message).toBe('Transaction failed. Error message'); expect(result.source_transaction).toBe('FAILED'); + expect(result.failure_phase).toBe(FailurePhase.Broadcast); + expect(result.error_code).toBe(SwapBridgeErrorCode.Unknown); + }); + + it('sets source_execution when the failed tx has a hash', () => { + const failedWithHash: TransactionMeta = { + ...mockTransactionMeta, + status: TransactionStatus.failed, + hash: '0xabc', + error: { + message: 'reverted', + name: 'Error', + } as TransactionError, + }; + const result = getEVMTxPropertiesFromTransactionMeta(failedWithHash); + expect(result.source_hash_present).toBe(true); + expect(result.failure_phase).toBe(FailurePhase.SourceExecution); + expect(result.error_code).toBe(SwapBridgeErrorCode.Unknown); }); it('should handle missing token symbols', () => { diff --git a/packages/bridge-status-controller/src/utils/metrics.ts b/packages/bridge-status-controller/src/utils/metrics.ts index bc01afb5700..c1d73aaa855 100644 --- a/packages/bridge-status-controller/src/utils/metrics.ts +++ b/packages/bridge-status-controller/src/utils/metrics.ts @@ -379,6 +379,13 @@ export const getEVMTxPropertiesFromTransactionMeta = ( usd_actual_gas: 0, action_type: MetricsActionType.SWAPBRIDGE_V1, ...(transactionMeta.batchId ? { batch_id: transactionMeta.batchId } : {}), + ...getHashPresenceProperties(transactionMeta.hash, undefined), + failure_phase: transactionMeta.hash + ? FailurePhase.SourceExecution + : FailurePhase.Broadcast, + error_code: transactionMeta.error + ? SwapBridgeErrorCode.Unknown + : SwapBridgeErrorCode.MissingErrorObject, }; }; From cf1cccca817e5f48740b296b269f081cc82e0b70 Mon Sep 17 00:00:00 2001 From: Frederic HENG Date: Tue, 25 Aug 2026 11:42:53 +0200 Subject: [PATCH 2/6] chore(bridge): add PR link to failure telemetry emit changelog --- packages/bridge-controller/CHANGELOG.md | 2 +- packages/bridge-status-controller/CHANGELOG.md | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/bridge-controller/CHANGELOG.md b/packages/bridge-controller/CHANGELOG.md index 662f53093bc..ef92c6dc26f 100644 --- a/packages/bridge-controller/CHANGELOG.md +++ b/packages/bridge-controller/CHANGELOG.md @@ -14,7 +14,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - Optional `failure_phase` and `error_code` on Quotes Error and Failed event context types - Optional `source_hash_present` and `destination_hash_present` on Failed, Submitted, and Completed event context types - Submit and status classifiers live in `@metamask/bridge-status-controller` -- Emit `failure_phase`, `error_code`, `source_hash_present`, and `destination_hash_present` on Quotes Error and Failed events +- Emit `failure_phase`, `error_code`, `source_hash_present`, and `destination_hash_present` on Quotes Error and Failed events ([#9949](https://github.com/MetaMask/core/pull/9949)) ## [80.1.0] diff --git a/packages/bridge-status-controller/CHANGELOG.md b/packages/bridge-status-controller/CHANGELOG.md index 9473421b02b..dc3d0980e6b 100644 --- a/packages/bridge-status-controller/CHANGELOG.md +++ b/packages/bridge-status-controller/CHANGELOG.md @@ -12,7 +12,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - Add SwapBridge submit and status failure telemetry classifiers for a later emit ([#9947](https://github.com/MetaMask/core/pull/9947)) - New exports: `getHashPresenceProperties`, `getStatusFailurePhase`, `getSubmitErrorCode`, `getSubmitFailureTelemetry`, and `getStatusFailureTelemetry` - Classifies submit and status failures from the code path (not from `error_message`) -- Emit `failure_phase`, `error_code`, `source_hash_present`, and `destination_hash_present` on SwapBridge Submitted, Completed, and Failed events +- Emit `failure_phase`, `error_code`, `source_hash_present`, and `destination_hash_present` on SwapBridge Submitted, Completed, and Failed events ([#9949](https://github.com/MetaMask/core/pull/9949)) ## [75.4.0] From e458d0fafa04ae24f69952ff48e7aeb551a3256a Mon Sep 17 00:00:00 2001 From: Frederic HENG Date: Wed, 26 Aug 2026 12:14:15 +0200 Subject: [PATCH 3/6] fix(bridge-status-controller): align Failed failure_phase with combined hash presence Promote broadcast, poll, or unknown to source_execution when history already has a source hash, so Mixpanel does not report a pre-broadcast failure after a source hash exists. --- .../src/utils/metrics/failure-telemetry.ts | 2 + .../bridge-status-controller.test.ts.snap | 5 ++ .../src/bridge-status-controller.test.ts | 4 +- .../src/bridge-status-controller.ts | 41 ++++++++++------ .../bridge-status-controller/src/index.ts | 1 + .../src/utils/metrics.test.ts | 48 +++++++++++++++++++ .../src/utils/metrics.ts | 26 ++++++++++ 7 files changed, 110 insertions(+), 17 deletions(-) diff --git a/packages/bridge-controller/src/utils/metrics/failure-telemetry.ts b/packages/bridge-controller/src/utils/metrics/failure-telemetry.ts index 9d3c213275d..325b8c873f9 100644 --- a/packages/bridge-controller/src/utils/metrics/failure-telemetry.ts +++ b/packages/bridge-controller/src/utils/metrics/failure-telemetry.ts @@ -4,6 +4,8 @@ import { SwapBridgeErrorCode } from './constants.js'; * Classify a thrown value for Quotes Error. Quote fetch always stays in the * `quote` phase; this only chooses `error_code`. * + * Submit and status failure classifiers live in `@metamask/bridge-status-controller`. + * * @param error - The thrown value from quote fetch. * @returns The Mixpanel `error_code`. */ 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 406a3771e5a..64a8a21390f 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 @@ -6435,6 +6435,7 @@ exports[`BridgeStatusController subscription handlers TransactionController:tran "custom_slippage": false, "destination_transaction": "FAILED", "error_message": "Transaction failed. tx-error", + "failure_phase": "source_execution", "feature_id": "unified_swap_bridge", "gas_included": false, "gas_included_7702": false, @@ -6486,6 +6487,7 @@ exports[`BridgeStatusController subscription handlers TransactionController:tran "custom_slippage": false, "destination_transaction": "FAILED", "error_message": "Transaction failed. tx-error", + "failure_phase": "source_execution", "feature_id": "unified_swap_bridge", "gas_included": false, "gas_included_7702": false, @@ -6538,6 +6540,7 @@ exports[`BridgeStatusController subscription handlers TransactionController:tran "custom_slippage": false, "destination_transaction": "FAILED", "error_message": "Transaction dropped. tx-error", + "failure_phase": "source_execution", "feature_id": "unified_swap_bridge", "gas_included": false, "gas_included_7702": false, @@ -6634,6 +6637,7 @@ exports[`BridgeStatusController subscription handlers TransactionController:tran "custom_slippage": false, "destination_transaction": "FAILED", "error_message": "Transaction failed. tx-error", + "failure_phase": "source_execution", "feature_id": "unified_swap_bridge", "gas_included": false, "gas_included_7702": false, @@ -6679,6 +6683,7 @@ exports[`BridgeStatusController subscription handlers TransactionController:tran "custom_slippage": false, "destination_transaction": "FAILED", "error_message": "Transaction failed. approval-tx-error", + "failure_phase": "source_execution", "feature_id": "unified_swap_bridge", "gas_included": false, "gas_included_7702": false, 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 2b2f888899c..ea203de2361 100644 --- a/packages/bridge-status-controller/src/bridge-status-controller.test.ts +++ b/packages/bridge-status-controller/src/bridge-status-controller.test.ts @@ -5903,7 +5903,7 @@ describe('BridgeStatusController', () => { "destination_transaction": "FAILED", "error_code": "unknown", "error_message": "Transaction failed. tx-error", - "failure_phase": "broadcast", + "failure_phase": "source_execution", "feature_id": "quick_buy_follow_trading", "gas_included": false, "gas_included_7702": false, @@ -5985,7 +5985,7 @@ describe('BridgeStatusController', () => { "destination_transaction": "FAILED", "error_code": "unknown", "error_message": "Transaction failed. tx-error", - "failure_phase": "broadcast", + "failure_phase": "source_execution", "feature_id": "quick_buy_explore", "gas_included": false, "gas_included_7702": false, diff --git a/packages/bridge-status-controller/src/bridge-status-controller.ts b/packages/bridge-status-controller/src/bridge-status-controller.ts index 1e22f2c3c48..4ed36282496 100644 --- a/packages/bridge-status-controller/src/bridge-status-controller.ts +++ b/packages/bridge-status-controller/src/bridge-status-controller.ts @@ -100,6 +100,7 @@ import { getHashPresenceProperties, getSubmitFailureTelemetry, getStatusFailureTelemetry, + promoteFailurePhase, } from './utils/metrics.js'; import { getSelectedChainId } from './utils/network.js'; import { @@ -1846,6 +1847,27 @@ export class BridgeStatusController extends StaticIntervalPollingController { @@ -1335,6 +1336,53 @@ describe('metrics utils', () => { }); }); + describe('promoteFailurePhase', () => { + it('keeps broadcast when no hashes are present', () => { + expect( + promoteFailurePhase(FailurePhase.Broadcast, { + source_hash_present: false, + destination_hash_present: false, + }), + ).toBe(FailurePhase.Broadcast); + }); + + it('promotes broadcast or poll to source_execution when a source hash is present', () => { + expect( + promoteFailurePhase(FailurePhase.Broadcast, { + source_hash_present: true, + destination_hash_present: false, + }), + ).toBe(FailurePhase.SourceExecution); + expect( + promoteFailurePhase(FailurePhase.Poll, { + source_hash_present: true, + destination_hash_present: false, + }), + ).toBe(FailurePhase.SourceExecution); + expect( + promoteFailurePhase(FailurePhase.Unknown, { + source_hash_present: true, + destination_hash_present: false, + }), + ).toBe(FailurePhase.SourceExecution); + }); + + it('promotes to destination_execution when a dest hash is present', () => { + expect( + promoteFailurePhase(FailurePhase.SourceExecution, { + source_hash_present: true, + destination_hash_present: true, + }), + ).toBe(FailurePhase.DestinationExecution); + expect( + promoteFailurePhase(FailurePhase.Broadcast, { + source_hash_present: false, + destination_hash_present: true, + }), + ).toBe(FailurePhase.DestinationExecution); + }); + }); + describe('getSubmitFailureTelemetry', () => { it('uses broadcast for submit failures with no hash', () => { expect(getSubmitFailureTelemetry(new Error('snap failed'))).toStrictEqual( diff --git a/packages/bridge-status-controller/src/utils/metrics.ts b/packages/bridge-status-controller/src/utils/metrics.ts index c1d73aaa855..629ae1f8ddc 100644 --- a/packages/bridge-status-controller/src/utils/metrics.ts +++ b/packages/bridge-status-controller/src/utils/metrics.ts @@ -438,6 +438,32 @@ export const getStatusFailurePhase = ( return FailurePhase.Poll; }; +/** + * Align `failure_phase` with combined hash presence without turning a + * no-hash `broadcast` failure into `poll`. + * + * @param phase - Phase from the emitting path. + * @param hashPresence - Combined history + caller hash flags. + * @returns The Mixpanel `failure_phase`. + */ +export const promoteFailurePhase = ( + phase: FailurePhase, + hashPresence: HashPresenceProperties, +): FailurePhase => { + if (hashPresence.destination_hash_present) { + return FailurePhase.DestinationExecution; + } + if ( + hashPresence.source_hash_present && + (phase === FailurePhase.Broadcast || + phase === FailurePhase.Poll || + phase === FailurePhase.Unknown) + ) { + return FailurePhase.SourceExecution; + } + return phase; +}; + /** * Telemetry for Failed events emitted from the submit catch (no tx hash yet). * From cfcc62de129736d1bfe07f503df470afc3a24e4e Mon Sep 17 00:00:00 2001 From: Frederic HENG Date: Wed, 26 Aug 2026 14:31:26 +0200 Subject: [PATCH 4/6] chore(bridge-controller): clarify Quotes Error emit has no hash flags Quote fetch is pre-tx, so the emit changelog should not list source_hash_present or destination_hash_present on Quotes Error. --- packages/bridge-controller/CHANGELOG.md | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/packages/bridge-controller/CHANGELOG.md b/packages/bridge-controller/CHANGELOG.md index ef92c6dc26f..67678472ac6 100644 --- a/packages/bridge-controller/CHANGELOG.md +++ b/packages/bridge-controller/CHANGELOG.md @@ -14,7 +14,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - Optional `failure_phase` and `error_code` on Quotes Error and Failed event context types - Optional `source_hash_present` and `destination_hash_present` on Failed, Submitted, and Completed event context types - Submit and status classifiers live in `@metamask/bridge-status-controller` -- Emit `failure_phase`, `error_code`, `source_hash_present`, and `destination_hash_present` on Quotes Error and Failed events ([#9949](https://github.com/MetaMask/core/pull/9949)) +- Emit `failure_phase` and `error_code` on Quotes Error events ([#9949](https://github.com/MetaMask/core/pull/9949)) +- Emit `failure_phase`, `error_code`, `source_hash_present`, and `destination_hash_present` on Failed events ([#9949](https://github.com/MetaMask/core/pull/9949)) ## [80.1.0] From aefdaa314df373412386a3a25e0c92f857bcc6f0 Mon Sep 17 00:00:00 2001 From: Frederic HENG Date: Thu, 27 Aug 2026 18:30:59 +0200 Subject: [PATCH 5/6] fix(bridge-status-controller): define hash helpers before EVM metrics use Move failure classifiers above getEVMTxPropertiesFromTransactionMeta so ESLint no-use-before-define passes. --- .../src/utils/metrics.ts | 222 +++++++++--------- 1 file changed, 111 insertions(+), 111 deletions(-) diff --git a/packages/bridge-status-controller/src/utils/metrics.ts b/packages/bridge-status-controller/src/utils/metrics.ts index 629ae1f8ddc..0cf555332c3 100644 --- a/packages/bridge-status-controller/src/utils/metrics.ts +++ b/packages/bridge-status-controller/src/utils/metrics.ts @@ -57,6 +57,117 @@ export type FailureTelemetryProperties = HashPresenceProperties & { error_code: SwapBridgeErrorCode; }; +/** + * Classify a thrown value from submit (sign/broadcast) catch paths. + * + * @param error - The thrown value from submit. + * @returns The Mixpanel `error_code`. + */ +export const getSubmitErrorCode = (error: unknown): SwapBridgeErrorCode => { + if (error === undefined || error === null) { + return SwapBridgeErrorCode.MissingErrorObject; + } + if (error instanceof Error) { + return SwapBridgeErrorCode.Unknown; + } + return SwapBridgeErrorCode.NonErrorRejection; +}; + +/** + * @param sourceHash - Source tx hash if known at emit time. + * @param destinationHash - Destination tx hash if known at emit time. + * @returns Boolean hash-presence properties. + */ +export const getHashPresenceProperties = ( + sourceHash?: string | null, + destinationHash?: string | null, +): HashPresenceProperties => { + return { + source_hash_present: Boolean(sourceHash), + destination_hash_present: Boolean(destinationHash), + }; +}; + +/** + * Prefer destination_execution over source_execution over poll. + * + * @param hashPresence - Hash presence at emit time. + * @returns The Mixpanel `failure_phase` for a status/polling Failed event. + */ +export const getStatusFailurePhase = ( + hashPresence: HashPresenceProperties, +): FailurePhase => { + if (hashPresence.destination_hash_present) { + return FailurePhase.DestinationExecution; + } + if (hashPresence.source_hash_present) { + return FailurePhase.SourceExecution; + } + return FailurePhase.Poll; +}; + +/** + * Align `failure_phase` with combined hash presence without turning a + * no-hash `broadcast` failure into `poll`. + * + * @param phase - Phase from the emitting path. + * @param hashPresence - Combined history + caller hash flags. + * @returns The Mixpanel `failure_phase`. + */ +export const promoteFailurePhase = ( + phase: FailurePhase, + hashPresence: HashPresenceProperties, +): FailurePhase => { + if (hashPresence.destination_hash_present) { + return FailurePhase.DestinationExecution; + } + if ( + hashPresence.source_hash_present && + (phase === FailurePhase.Broadcast || + phase === FailurePhase.Poll || + phase === FailurePhase.Unknown) + ) { + return FailurePhase.SourceExecution; + } + return phase; +}; + +/** + * Telemetry for Failed events emitted from the submit catch (no tx hash yet). + * + * @param error - The thrown value from submit. + * @returns Phase, error code, and hash-presence flags. + */ +export const getSubmitFailureTelemetry = ( + error: unknown, +): FailureTelemetryProperties => { + return { + failure_phase: FailurePhase.Broadcast, + error_code: getSubmitErrorCode(error), + source_hash_present: false, + destination_hash_present: false, + }; +}; + +/** + * Telemetry for Failed events derived from a status poll. + * + * @param sourceHash - Source tx hash if known. + * @param destinationHash - Destination tx hash if known. + * @returns Phase, error code, and hash-presence flags. + */ +export const getStatusFailureTelemetry = ( + sourceHash?: string | null, + destinationHash?: string | null, +): FailureTelemetryProperties => { + const hashPresence = getHashPresenceProperties(sourceHash, destinationHash); + return { + ...hashPresence, + failure_phase: getStatusFailurePhase(hashPresence), + error_code: SwapBridgeErrorCode.StatusFailedWithoutReason, + }; +}; + export const getTxStatusesFromHistory = ({ status, hasApprovalTx, @@ -388,114 +499,3 @@ export const getEVMTxPropertiesFromTransactionMeta = ( : SwapBridgeErrorCode.MissingErrorObject, }; }; - -/** - * Classify a thrown value from submit (sign/broadcast) catch paths. - * - * @param error - The thrown value from submit. - * @returns The Mixpanel `error_code`. - */ -export const getSubmitErrorCode = (error: unknown): SwapBridgeErrorCode => { - if (error === undefined || error === null) { - return SwapBridgeErrorCode.MissingErrorObject; - } - if (error instanceof Error) { - return SwapBridgeErrorCode.Unknown; - } - return SwapBridgeErrorCode.NonErrorRejection; -}; - -/** - * @param sourceHash - Source tx hash if known at emit time. - * @param destinationHash - Destination tx hash if known at emit time. - * @returns Boolean hash-presence properties. - */ -export const getHashPresenceProperties = ( - sourceHash?: string | null, - destinationHash?: string | null, -): HashPresenceProperties => { - return { - source_hash_present: Boolean(sourceHash), - destination_hash_present: Boolean(destinationHash), - }; -}; - -/** - * Prefer destination_execution over source_execution over poll. - * - * @param hashPresence - Hash presence at emit time. - * @returns The Mixpanel `failure_phase` for a status/polling Failed event. - */ -export const getStatusFailurePhase = ( - hashPresence: HashPresenceProperties, -): FailurePhase => { - if (hashPresence.destination_hash_present) { - return FailurePhase.DestinationExecution; - } - if (hashPresence.source_hash_present) { - return FailurePhase.SourceExecution; - } - return FailurePhase.Poll; -}; - -/** - * Align `failure_phase` with combined hash presence without turning a - * no-hash `broadcast` failure into `poll`. - * - * @param phase - Phase from the emitting path. - * @param hashPresence - Combined history + caller hash flags. - * @returns The Mixpanel `failure_phase`. - */ -export const promoteFailurePhase = ( - phase: FailurePhase, - hashPresence: HashPresenceProperties, -): FailurePhase => { - if (hashPresence.destination_hash_present) { - return FailurePhase.DestinationExecution; - } - if ( - hashPresence.source_hash_present && - (phase === FailurePhase.Broadcast || - phase === FailurePhase.Poll || - phase === FailurePhase.Unknown) - ) { - return FailurePhase.SourceExecution; - } - return phase; -}; - -/** - * Telemetry for Failed events emitted from the submit catch (no tx hash yet). - * - * @param error - The thrown value from submit. - * @returns Phase, error code, and hash-presence flags. - */ -export const getSubmitFailureTelemetry = ( - error: unknown, -): FailureTelemetryProperties => { - return { - failure_phase: FailurePhase.Broadcast, - error_code: getSubmitErrorCode(error), - source_hash_present: false, - destination_hash_present: false, - }; -}; - -/** - * Telemetry for Failed events derived from a status poll. - * - * @param sourceHash - Source tx hash if known. - * @param destinationHash - Destination tx hash if known. - * @returns Phase, error code, and hash-presence flags. - */ -export const getStatusFailureTelemetry = ( - sourceHash?: string | null, - destinationHash?: string | null, -): FailureTelemetryProperties => { - const hashPresence = getHashPresenceProperties(sourceHash, destinationHash); - return { - ...hashPresence, - failure_phase: getStatusFailurePhase(hashPresence), - error_code: SwapBridgeErrorCode.StatusFailedWithoutReason, - }; -}; From b13d916251e9bba2c00965299d110c34e7a7ab80 Mon Sep 17 00:00:00 2001 From: Frederic HENG Date: Fri, 28 Aug 2026 12:14:50 +0200 Subject: [PATCH 6/6] chore(bridge): refresh failure telemetry snapshots after rebase onto #9986 --- .../bridge-controller.test.ts.snap | 10 ++ .../bridge-status-controller.test.ts.snap | 103 ++++++++++++++++++ 2 files changed, 113 insertions(+) diff --git a/packages/bridge-controller/src/__snapshots__/bridge-controller.test.ts.snap b/packages/bridge-controller/src/__snapshots__/bridge-controller.test.ts.snap index 83398cef0fa..21371892e0f 100644 --- a/packages/bridge-controller/src/__snapshots__/bridge-controller.test.ts.snap +++ b/packages/bridge-controller/src/__snapshots__/bridge-controller.test.ts.snap @@ -121,8 +121,11 @@ exports[`BridgeController trackUnifiedSwapBridgeEvent bridge-status-controller c "chain_id_destination": "solana:5eykt4UsFv8P8NJdTREpY1vzqKqZKvdp", "chain_id_source": "eip155:1", "custom_slippage": true, + "destination_hash_present": false, "destination_transaction": "PENDING", + "error_code": "unknown", "error_message": "error_message", + "failure_phase": "unknown", "feature_id": "unified_swap_bridge", "gas_included": false, "gas_included_7702": false, @@ -137,6 +140,7 @@ exports[`BridgeController trackUnifiedSwapBridgeEvent bridge-status-controller c "quotes_list": [], "security_warnings": [], "slippage_limit": 0, + "source_hash_present": false, "source_transaction": "PENDING", "stx_enabled": false, "swap_type": "crosschain", @@ -163,7 +167,10 @@ exports[`BridgeController trackUnifiedSwapBridgeEvent bridge-status-controller c "chain_id_destination": "eip155:1", "chain_id_source": "solana:5eykt4UsFv8P8NJdTREpY1vzqKqZKvdp", "custom_slippage": true, + "destination_hash_present": false, + "error_code": "unknown", "error_message": "Failed to submit tx", + "failure_phase": "unknown", "feature_id": "unified_swap_bridge", "gas_included": false, "gas_included_7702": false, @@ -180,6 +187,7 @@ exports[`BridgeController trackUnifiedSwapBridgeEvent bridge-status-controller c "lifi_mayanMCTP", ], "slippage_limit": 0.5, + "source_hash_present": false, "stx_enabled": false, "swap_type": "crosschain", "token_address_destination": "eip155:1/erc20:0x1234", @@ -1078,7 +1086,9 @@ exports[`BridgeController updateBridgeQuoteRequestParams should trigger quote po "chain_id_destination": "solana:5eykt4UsFv8P8NJdTREpY1vzqKqZKvdp", "chain_id_source": "eip155:1", "custom_slippage": true, + "error_code": "quote_fetch_failed", "error_message": "Network error", + "failure_phase": "quote", "feature_id": "unified_swap_bridge", "has_sufficient_funds": true, "is_hardware_wallet": false, 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 64a8a21390f..2352bef56b2 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 @@ -228,7 +228,10 @@ exports[`BridgeStatusController startPollingForBridgeTxStatus emits bridgeTransa "chain_id_destination": "eip155:10", "chain_id_source": "eip155:42161", "custom_slippage": false, + "destination_hash_present": false, "destination_transaction": "FAILED", + "error_code": "status_failed_without_reason", + "failure_phase": "source_execution", "feature_id": "unified_swap_bridge", "gas_included": false, "gas_included_7702": false, @@ -241,6 +244,7 @@ exports[`BridgeStatusController startPollingForBridgeTxStatus emits bridgeTransa "quoted_vs_used_gas_ratio": 0, "security_warnings": [], "slippage_limit": 0, + "source_hash_present": true, "source_transaction": "COMPLETE", "stx_enabled": false, "swap_type": "crosschain", @@ -414,6 +418,7 @@ exports[`BridgeStatusController startPollingForBridgeTxStatus stops polling when "chain_id_destination": "eip155:10", "chain_id_source": "eip155:42161", "custom_slippage": false, + "destination_hash_present": true, "destination_transaction": "COMPLETE", "feature_id": "unified_swap_bridge", "gas_included": false, @@ -427,6 +432,7 @@ exports[`BridgeStatusController startPollingForBridgeTxStatus stops polling when "quoted_vs_used_gas_ratio": 0, "security_warnings": [], "slippage_limit": 0, + "source_hash_present": true, "source_transaction": "COMPLETE", "stx_enabled": true, "swap_type": "crosschain", @@ -622,6 +628,7 @@ exports[`BridgeStatusController submitTx: EVM bridge should delay after submitti "chain_id_destination": "eip155:10", "chain_id_source": "eip155:8453", "custom_slippage": false, + "destination_hash_present": false, "feature_id": "unified_swap_bridge", "gas_included": false, "gas_included_7702": false, @@ -631,6 +638,7 @@ exports[`BridgeStatusController submitTx: EVM bridge should delay after submitti "provider": "lifi_across", "quoted_time_minutes": 0.25, "slippage_limit": 0, + "source_hash_present": false, "stx_enabled": false, "swap_type": "crosschain", "token_address_destination": "eip155:10/slip44:60", @@ -941,6 +949,7 @@ exports[`BridgeStatusController submitTx: EVM bridge should delay after submitti "chain_id_destination": "eip155:10", "chain_id_source": "eip155:59144", "custom_slippage": false, + "destination_hash_present": false, "feature_id": "unified_swap_bridge", "gas_included": false, "gas_included_7702": false, @@ -950,6 +959,7 @@ exports[`BridgeStatusController submitTx: EVM bridge should delay after submitti "provider": "lifi_across", "quoted_time_minutes": 0.25, "slippage_limit": 0, + "source_hash_present": false, "stx_enabled": false, "swap_type": "crosschain", "token_address_destination": "eip155:10/slip44:60", @@ -1316,6 +1326,7 @@ exports[`BridgeStatusController submitTx: EVM bridge should handle smart transac "chain_id_destination": "eip155:10", "chain_id_source": "eip155:42161", "custom_slippage": false, + "destination_hash_present": false, "feature_id": "unified_swap_bridge", "gas_included": false, "gas_included_7702": false, @@ -1325,6 +1336,7 @@ exports[`BridgeStatusController submitTx: EVM bridge should handle smart transac "provider": "lifi_across", "quoted_time_minutes": 0.25, "slippage_limit": 0, + "source_hash_present": false, "stx_enabled": true, "swap_type": "crosschain", "token_address_destination": "eip155:10/slip44:60", @@ -1568,6 +1580,7 @@ exports[`BridgeStatusController submitTx: EVM bridge should not call handleMobil "chain_id_destination": "eip155:10", "chain_id_source": "eip155:42161", "custom_slippage": false, + "destination_hash_present": false, "feature_id": "unified_swap_bridge", "gas_included": false, "gas_included_7702": false, @@ -1577,6 +1590,7 @@ exports[`BridgeStatusController submitTx: EVM bridge should not call handleMobil "provider": "lifi_across", "quoted_time_minutes": 0.25, "slippage_limit": 0, + "source_hash_present": false, "stx_enabled": false, "swap_type": "crosschain", "token_address_destination": "eip155:10/slip44:60", @@ -1887,6 +1901,7 @@ exports[`BridgeStatusController submitTx: EVM bridge should not call handleMobil "chain_id_destination": "eip155:10", "chain_id_source": "eip155:42161", "custom_slippage": false, + "destination_hash_present": false, "feature_id": "unified_swap_bridge", "gas_included": false, "gas_included_7702": false, @@ -1896,6 +1911,7 @@ exports[`BridgeStatusController submitTx: EVM bridge should not call handleMobil "provider": "lifi_across", "quoted_time_minutes": 0.25, "slippage_limit": 0, + "source_hash_present": false, "stx_enabled": false, "swap_type": "crosschain", "token_address_destination": "eip155:10/slip44:60", @@ -2206,6 +2222,7 @@ exports[`BridgeStatusController submitTx: EVM bridge should reset USDT allowance "chain_id_destination": "eip155:10", "chain_id_source": "eip155:42161", "custom_slippage": false, + "destination_hash_present": false, "feature_id": "unified_swap_bridge", "gas_included": false, "gas_included_7702": false, @@ -2215,6 +2232,7 @@ exports[`BridgeStatusController submitTx: EVM bridge should reset USDT allowance "provider": "lifi_across", "quoted_time_minutes": 0.25, "slippage_limit": 0, + "source_hash_present": false, "stx_enabled": false, "swap_type": "crosschain", "token_address_destination": "eip155:10/slip44:60", @@ -2543,6 +2561,7 @@ exports[`BridgeStatusController submitTx: EVM bridge should successfully submit "chain_id_destination": "eip155:10", "chain_id_source": "eip155:42161", "custom_slippage": false, + "destination_hash_present": false, "feature_id": "unified_swap_bridge", "gas_included": false, "gas_included_7702": false, @@ -2552,6 +2571,7 @@ exports[`BridgeStatusController submitTx: EVM bridge should successfully submit "provider": "lifi_across", "quoted_time_minutes": 0.25, "slippage_limit": 0, + "source_hash_present": false, "stx_enabled": false, "swap_type": "crosschain", "token_address_destination": "eip155:10/slip44:60", @@ -2835,6 +2855,7 @@ exports[`BridgeStatusController submitTx: EVM bridge should successfully submit "chain_id_destination": "eip155:10", "chain_id_source": "eip155:42161", "custom_slippage": false, + "destination_hash_present": false, "feature_id": "unified_swap_bridge", "gas_included": false, "gas_included_7702": false, @@ -2844,6 +2865,7 @@ exports[`BridgeStatusController submitTx: EVM bridge should successfully submit "provider": "lifi_across", "quoted_time_minutes": 0.25, "slippage_limit": 0, + "source_hash_present": false, "stx_enabled": false, "swap_type": "crosschain", "token_address_destination": "eip155:10/slip44:60", @@ -2933,6 +2955,7 @@ exports[`BridgeStatusController submitTx: EVM bridge should throw an error if ap "chain_id_destination": "eip155:10", "chain_id_source": "eip155:42161", "custom_slippage": false, + "destination_hash_present": false, "feature_id": "unified_swap_bridge", "gas_included": false, "gas_included_7702": false, @@ -2942,6 +2965,7 @@ exports[`BridgeStatusController submitTx: EVM bridge should throw an error if ap "provider": "lifi_across", "quoted_time_minutes": 0.25, "slippage_limit": 0, + "source_hash_present": false, "stx_enabled": false, "swap_type": "crosschain", "token_address_destination": "eip155:10/slip44:60", @@ -3014,7 +3038,10 @@ exports[`BridgeStatusController submitTx: EVM bridge should throw an error if ap "chain_id_destination": "eip155:10", "chain_id_source": "eip155:42161", "custom_slippage": false, + "destination_hash_present": false, + "error_code": "unknown", "error_message": "Approval tx failed", + "failure_phase": "broadcast", "feature_id": "unified_swap_bridge", "gas_included": false, "gas_included_7702": false, @@ -3024,6 +3051,7 @@ exports[`BridgeStatusController submitTx: EVM bridge should throw an error if ap "provider": "lifi_across", "quoted_time_minutes": 0.25, "slippage_limit": 0, + "source_hash_present": false, "stx_enabled": false, "swap_type": "crosschain", "token_address_destination": "eip155:10/slip44:60", @@ -3059,6 +3087,7 @@ exports[`BridgeStatusController submitTx: EVM bridge should throw an error if ap "chain_id_destination": "eip155:10", "chain_id_source": "eip155:42161", "custom_slippage": false, + "destination_hash_present": false, "feature_id": "unified_swap_bridge", "gas_included": false, "gas_included_7702": false, @@ -3068,6 +3097,7 @@ exports[`BridgeStatusController submitTx: EVM bridge should throw an error if ap "provider": "lifi_across", "quoted_time_minutes": 0.25, "slippage_limit": 0, + "source_hash_present": false, "stx_enabled": false, "swap_type": "crosschain", "token_address_destination": "eip155:10/slip44:60", @@ -3143,7 +3173,10 @@ exports[`BridgeStatusController submitTx: EVM bridge should throw an error if ap "chain_id_destination": "eip155:10", "chain_id_source": "eip155:42161", "custom_slippage": false, + "destination_hash_present": false, + "error_code": "unknown", "error_message": "Failed to submit cross-chain swap tx: txMeta for txHash was not found", + "failure_phase": "broadcast", "feature_id": "unified_swap_bridge", "gas_included": false, "gas_included_7702": false, @@ -3153,6 +3186,7 @@ exports[`BridgeStatusController submitTx: EVM bridge should throw an error if ap "provider": "lifi_across", "quoted_time_minutes": 0.25, "slippage_limit": 0, + "source_hash_present": false, "stx_enabled": false, "swap_type": "crosschain", "token_address_destination": "eip155:10/slip44:60", @@ -3337,6 +3371,7 @@ exports[`BridgeStatusController submitTx: EVM bridge waits for approval tx confi "chain_id_destination": "eip155:10", "chain_id_source": "eip155:42161", "custom_slippage": false, + "destination_hash_present": false, "feature_id": "unified_swap_bridge", "gas_included": false, "gas_included_7702": false, @@ -3346,6 +3381,7 @@ exports[`BridgeStatusController submitTx: EVM bridge waits for approval tx confi "provider": "lifi_across", "quoted_time_minutes": 0.25, "slippage_limit": 0, + "source_hash_present": false, "stx_enabled": false, "swap_type": "crosschain", "token_address_destination": "eip155:10/slip44:60", @@ -3526,6 +3562,7 @@ exports[`BridgeStatusController submitTx: EVM swap should gracefully handle isAt "chain_id_destination": "eip155:42161", "chain_id_source": "eip155:42161", "custom_slippage": false, + "destination_hash_present": false, "feature_id": "unified_swap_bridge", "gas_included": false, "gas_included_7702": false, @@ -3535,6 +3572,7 @@ exports[`BridgeStatusController submitTx: EVM swap should gracefully handle isAt "provider": "lifi_across", "quoted_time_minutes": 0, "slippage_limit": 0, + "source_hash_present": false, "stx_enabled": false, "swap_type": "single_chain", "token_address_destination": "eip155:10/slip44:60", @@ -4040,6 +4078,7 @@ exports[`BridgeStatusController submitTx: EVM swap should handle smart transacti "chain_id_destination": "eip155:42161", "chain_id_source": "eip155:42161", "custom_slippage": false, + "destination_hash_present": false, "feature_id": "unified_swap_bridge", "gas_included": false, "gas_included_7702": false, @@ -4049,6 +4088,7 @@ exports[`BridgeStatusController submitTx: EVM swap should handle smart transacti "provider": "lifi_across", "quoted_time_minutes": 0, "slippage_limit": 0, + "source_hash_present": false, "stx_enabled": true, "swap_type": "single_chain", "token_address_destination": "eip155:10/slip44:60", @@ -4449,6 +4489,7 @@ exports[`BridgeStatusController submitTx: EVM swap should successfully submit an "chain_id_destination": "eip155:42161", "chain_id_source": "eip155:42161", "custom_slippage": false, + "destination_hash_present": false, "feature_id": "unified_swap_bridge", "gas_included": false, "gas_included_7702": false, @@ -4458,6 +4499,7 @@ exports[`BridgeStatusController submitTx: EVM swap should successfully submit an "provider": "lifi_across", "quoted_time_minutes": 0, "slippage_limit": 0, + "source_hash_present": false, "stx_enabled": false, "swap_type": "single_chain", "token_address_destination": "eip155:10/slip44:60", @@ -4936,6 +4978,7 @@ exports[`BridgeStatusController submitTx: Solana bridge should handle snap contr "chain_id_destination": "eip155:1", "chain_id_source": "solana:5eykt4UsFv8P8NJdTREpY1vzqKqZKvdp", "custom_slippage": false, + "destination_hash_present": false, "feature_id": "unified_swap_bridge", "gas_included": false, "gas_included_7702": false, @@ -4945,6 +4988,7 @@ exports[`BridgeStatusController submitTx: Solana bridge should handle snap contr "provider": "test-bridge_test-bridge", "quoted_time_minutes": 5, "slippage_limit": 0, + "source_hash_present": false, "stx_enabled": false, "swap_type": "crosschain", "token_address_destination": "eip155:1/slip44:60", @@ -4984,7 +5028,10 @@ exports[`BridgeStatusController submitTx: Solana bridge should handle snap contr "chain_id_destination": "eip155:1", "chain_id_source": "solana:5eykt4UsFv8P8NJdTREpY1vzqKqZKvdp", "custom_slippage": false, + "destination_hash_present": false, + "error_code": "unknown", "error_message": "Snap error", + "failure_phase": "broadcast", "feature_id": "unified_swap_bridge", "gas_included": false, "gas_included_7702": false, @@ -4994,6 +5041,7 @@ exports[`BridgeStatusController submitTx: Solana bridge should handle snap contr "provider": "test-bridge_test-bridge", "quoted_time_minutes": 5, "slippage_limit": 0, + "source_hash_present": false, "stx_enabled": false, "swap_type": "crosschain", "token_address_destination": "eip155:1/slip44:60", @@ -5029,6 +5077,7 @@ exports[`BridgeStatusController submitTx: Solana bridge should successfully subm "chain_id_destination": "eip155:1", "chain_id_source": "solana:5eykt4UsFv8P8NJdTREpY1vzqKqZKvdp", "custom_slippage": false, + "destination_hash_present": false, "feature_id": "unified_swap_bridge", "gas_included": false, "gas_included_7702": false, @@ -5038,6 +5087,7 @@ exports[`BridgeStatusController submitTx: Solana bridge should successfully subm "provider": "test-bridge_test-bridge", "quoted_time_minutes": 5, "slippage_limit": 0, + "source_hash_present": false, "stx_enabled": false, "swap_type": "crosschain", "token_address_destination": "eip155:1/slip44:60", @@ -5226,6 +5276,7 @@ exports[`BridgeStatusController submitTx: Solana bridge should throw error when "chain_id_destination": "eip155:1", "chain_id_source": "solana:5eykt4UsFv8P8NJdTREpY1vzqKqZKvdp", "custom_slippage": false, + "destination_hash_present": false, "feature_id": "unified_swap_bridge", "gas_included": false, "gas_included_7702": false, @@ -5235,6 +5286,7 @@ exports[`BridgeStatusController submitTx: Solana bridge should throw error when "provider": "test-bridge_test-bridge", "quoted_time_minutes": 5, "slippage_limit": 0, + "source_hash_present": false, "stx_enabled": false, "swap_type": "crosschain", "token_address_destination": "eip155:1/slip44:60", @@ -5256,7 +5308,10 @@ exports[`BridgeStatusController submitTx: Solana bridge should throw error when "chain_id_destination": "eip155:1", "chain_id_source": "solana:5eykt4UsFv8P8NJdTREpY1vzqKqZKvdp", "custom_slippage": false, + "destination_hash_present": false, + "error_code": "unknown", "error_message": "Failed to submit cross-chain swap transaction: undefined snap id", + "failure_phase": "broadcast", "feature_id": "unified_swap_bridge", "gas_included": false, "gas_included_7702": false, @@ -5266,6 +5321,7 @@ exports[`BridgeStatusController submitTx: Solana bridge should throw error when "provider": "test-bridge_test-bridge", "quoted_time_minutes": 5, "slippage_limit": 0, + "source_hash_present": false, "stx_enabled": false, "swap_type": "crosschain", "token_address_destination": "eip155:1/slip44:60", @@ -5301,6 +5357,7 @@ exports[`BridgeStatusController submitTx: Solana swap should handle snap control "chain_id_destination": "solana:5eykt4UsFv8P8NJdTREpY1vzqKqZKvdp", "chain_id_source": "solana:5eykt4UsFv8P8NJdTREpY1vzqKqZKvdp", "custom_slippage": false, + "destination_hash_present": false, "feature_id": "unified_swap_bridge", "gas_included": false, "gas_included_7702": false, @@ -5310,6 +5367,7 @@ exports[`BridgeStatusController submitTx: Solana swap should handle snap control "provider": "test-bridge_undefined", "quoted_time_minutes": 5, "slippage_limit": 0, + "source_hash_present": false, "stx_enabled": false, "swap_type": "single_chain", "token_address_destination": "eip155:1399811149/slip44:501", @@ -5349,7 +5407,10 @@ exports[`BridgeStatusController submitTx: Solana swap should handle snap control "chain_id_destination": "solana:5eykt4UsFv8P8NJdTREpY1vzqKqZKvdp", "chain_id_source": "solana:5eykt4UsFv8P8NJdTREpY1vzqKqZKvdp", "custom_slippage": false, + "destination_hash_present": false, + "error_code": "unknown", "error_message": "Snap error", + "failure_phase": "broadcast", "feature_id": "unified_swap_bridge", "gas_included": false, "gas_included_7702": false, @@ -5359,6 +5420,7 @@ exports[`BridgeStatusController submitTx: Solana swap should handle snap control "provider": "test-bridge_undefined", "quoted_time_minutes": 5, "slippage_limit": 0, + "source_hash_present": false, "stx_enabled": false, "swap_type": "single_chain", "token_address_destination": "eip155:1399811149/slip44:501", @@ -5394,6 +5456,7 @@ exports[`BridgeStatusController submitTx: Solana swap should successfully submit "chain_id_destination": "solana:5eykt4UsFv8P8NJdTREpY1vzqKqZKvdp", "chain_id_source": "solana:5eykt4UsFv8P8NJdTREpY1vzqKqZKvdp", "custom_slippage": false, + "destination_hash_present": false, "feature_id": "unified_swap_bridge", "gas_included": false, "gas_included_7702": false, @@ -5403,6 +5466,7 @@ exports[`BridgeStatusController submitTx: Solana swap should successfully submit "provider": "test-bridge_undefined", "quoted_time_minutes": 5, "slippage_limit": 0, + "source_hash_present": false, "stx_enabled": false, "swap_type": "single_chain", "token_address_destination": "eip155:1399811149/slip44:501", @@ -5452,6 +5516,7 @@ exports[`BridgeStatusController submitTx: Solana swap should successfully submit "chain_id_destination": "solana:5eykt4UsFv8P8NJdTREpY1vzqKqZKvdp", "chain_id_source": "solana:5eykt4UsFv8P8NJdTREpY1vzqKqZKvdp", "custom_slippage": false, + "destination_hash_present": false, "destination_transaction": "PENDING", "feature_id": "unified_swap_bridge", "gas_included": false, @@ -5465,6 +5530,7 @@ exports[`BridgeStatusController submitTx: Solana swap should successfully submit "quoted_vs_used_gas_ratio": 0, "security_warnings": [], "slippage_limit": 0, + "source_hash_present": true, "source_transaction": "COMPLETE", "stx_enabled": false, "swap_type": "single_chain", @@ -5644,6 +5710,7 @@ exports[`BridgeStatusController submitTx: Solana swap should throw error when sn "chain_id_destination": "solana:5eykt4UsFv8P8NJdTREpY1vzqKqZKvdp", "chain_id_source": "solana:5eykt4UsFv8P8NJdTREpY1vzqKqZKvdp", "custom_slippage": false, + "destination_hash_present": false, "feature_id": "unified_swap_bridge", "gas_included": false, "gas_included_7702": false, @@ -5653,6 +5720,7 @@ exports[`BridgeStatusController submitTx: Solana swap should throw error when sn "provider": "test-bridge_undefined", "quoted_time_minutes": 5, "slippage_limit": 0, + "source_hash_present": false, "stx_enabled": false, "swap_type": "single_chain", "token_address_destination": "eip155:1399811149/slip44:501", @@ -5674,7 +5742,10 @@ exports[`BridgeStatusController submitTx: Solana swap should throw error when sn "chain_id_destination": "solana:5eykt4UsFv8P8NJdTREpY1vzqKqZKvdp", "chain_id_source": "solana:5eykt4UsFv8P8NJdTREpY1vzqKqZKvdp", "custom_slippage": false, + "destination_hash_present": false, + "error_code": "unknown", "error_message": "Failed to submit cross-chain swap transaction: undefined snap id", + "failure_phase": "broadcast", "feature_id": "unified_swap_bridge", "gas_included": false, "gas_included_7702": false, @@ -5684,6 +5755,7 @@ exports[`BridgeStatusController submitTx: Solana swap should throw error when sn "provider": "test-bridge_undefined", "quoted_time_minutes": 5, "slippage_limit": 0, + "source_hash_present": false, "stx_enabled": false, "swap_type": "single_chain", "token_address_destination": "eip155:1399811149/slip44:501", @@ -5719,6 +5791,7 @@ exports[`BridgeStatusController submitTx: Tron swap with approval should handle "chain_id_destination": "tron:728126428", "chain_id_source": "tron:728126428", "custom_slippage": false, + "destination_hash_present": false, "feature_id": "unified_swap_bridge", "gas_included": false, "gas_included_7702": false, @@ -5728,6 +5801,7 @@ exports[`BridgeStatusController submitTx: Tron swap with approval should handle "provider": "test-bridge_undefined", "quoted_time_minutes": 0.5, "slippage_limit": 0, + "source_hash_present": false, "stx_enabled": false, "swap_type": "single_chain", "token_address_destination": "tron:728126428/slip44:195", @@ -5771,7 +5845,10 @@ exports[`BridgeStatusController submitTx: Tron swap with approval should handle "chain_id_destination": "tron:728126428", "chain_id_source": "tron:728126428", "custom_slippage": false, + "destination_hash_present": false, + "error_code": "unknown", "error_message": "Approval transaction failed", + "failure_phase": "broadcast", "feature_id": "unified_swap_bridge", "gas_included": false, "gas_included_7702": false, @@ -5781,6 +5858,7 @@ exports[`BridgeStatusController submitTx: Tron swap with approval should handle "provider": "test-bridge_undefined", "quoted_time_minutes": 0.5, "slippage_limit": 0, + "source_hash_present": false, "stx_enabled": false, "swap_type": "single_chain", "token_address_destination": "tron:728126428/slip44:195", @@ -5816,6 +5894,7 @@ exports[`BridgeStatusController submitTx: Tron swap with approval should success "chain_id_destination": "eip155:1", "chain_id_source": "tron:728126428", "custom_slippage": false, + "destination_hash_present": false, "feature_id": "unified_swap_bridge", "gas_included": false, "gas_included_7702": false, @@ -5825,6 +5904,7 @@ exports[`BridgeStatusController submitTx: Tron swap with approval should success "provider": "test-bridge_undefined", "quoted_time_minutes": 0.5, "slippage_limit": 0, + "source_hash_present": false, "stx_enabled": false, "swap_type": "crosschain", "token_address_destination": "tron:728126428/slip44:195", @@ -6037,6 +6117,7 @@ exports[`BridgeStatusController submitTx: Tron swap with approval should success "chain_id_destination": "tron:728126428", "chain_id_source": "tron:728126428", "custom_slippage": false, + "destination_hash_present": false, "feature_id": "unified_swap_bridge", "gas_included": false, "gas_included_7702": false, @@ -6046,6 +6127,7 @@ exports[`BridgeStatusController submitTx: Tron swap with approval should success "provider": "test-bridge_undefined", "quoted_time_minutes": 0.5, "slippage_limit": 0, + "source_hash_present": false, "stx_enabled": false, "swap_type": "single_chain", "token_address_destination": "tron:728126428/slip44:195", @@ -6388,6 +6470,7 @@ exports[`BridgeStatusController subscription handlers TransactionController:tran "chain_id_destination": "eip155:42161", "chain_id_source": "eip155:42161", "custom_slippage": false, + "destination_hash_present": false, "destination_transaction": "PENDING", "feature_id": "unified_swap_bridge", "gas_included": false, @@ -6401,6 +6484,7 @@ exports[`BridgeStatusController subscription handlers TransactionController:tran "quoted_vs_used_gas_ratio": 0, "security_warnings": [], "slippage_limit": 0, + "source_hash_present": true, "source_transaction": "COMPLETE", "stx_enabled": false, "swap_type": "single_chain", @@ -6433,7 +6517,9 @@ exports[`BridgeStatusController subscription handlers TransactionController:tran "chain_id_destination": "eip155:10", "chain_id_source": "eip155:42161", "custom_slippage": false, + "destination_hash_present": false, "destination_transaction": "FAILED", + "error_code": "unknown", "error_message": "Transaction failed. tx-error", "failure_phase": "source_execution", "feature_id": "unified_swap_bridge", @@ -6448,6 +6534,7 @@ exports[`BridgeStatusController subscription handlers TransactionController:tran "quoted_vs_used_gas_ratio": 0, "security_warnings": [], "slippage_limit": 0, + "source_hash_present": true, "source_transaction": "COMPLETE", "stx_enabled": false, "swap_type": "crosschain", @@ -6485,7 +6572,9 @@ exports[`BridgeStatusController subscription handlers TransactionController:tran "chain_id_destination": "eip155:10", "chain_id_source": "eip155:42161", "custom_slippage": false, + "destination_hash_present": false, "destination_transaction": "FAILED", + "error_code": "unknown", "error_message": "Transaction failed. tx-error", "failure_phase": "source_execution", "feature_id": "unified_swap_bridge", @@ -6500,6 +6589,7 @@ exports[`BridgeStatusController subscription handlers TransactionController:tran "quoted_vs_used_gas_ratio": 0, "security_warnings": [], "slippage_limit": 0, + "source_hash_present": true, "source_transaction": "COMPLETE", "stx_enabled": false, "swap_type": "crosschain", @@ -6538,7 +6628,9 @@ exports[`BridgeStatusController subscription handlers TransactionController:tran "chain_id_destination": "eip155:10", "chain_id_source": "eip155:42161", "custom_slippage": false, + "destination_hash_present": false, "destination_transaction": "FAILED", + "error_code": "unknown", "error_message": "Transaction dropped. tx-error", "failure_phase": "source_execution", "feature_id": "unified_swap_bridge", @@ -6553,6 +6645,7 @@ exports[`BridgeStatusController subscription handlers TransactionController:tran "quoted_vs_used_gas_ratio": 0, "security_warnings": [], "slippage_limit": 0, + "source_hash_present": true, "source_transaction": "COMPLETE", "stx_enabled": false, "swap_type": "crosschain", @@ -6583,7 +6676,10 @@ exports[`BridgeStatusController subscription handlers TransactionController:tran "chain_id_destination": "eip155:42161", "chain_id_source": "eip155:42161", "custom_slippage": false, + "destination_hash_present": false, + "error_code": "unknown", "error_message": "Transaction failed. tx-error", + "failure_phase": "broadcast", "feature_id": "unified_swap_bridge", "gas_included": false, "gas_included_7702": false, @@ -6596,6 +6692,7 @@ exports[`BridgeStatusController subscription handlers TransactionController:tran "quoted_vs_used_gas_ratio": 0, "security_warnings": [], "slippage_limit": 0, + "source_hash_present": false, "source_transaction": "FAILED", "stx_enabled": false, "swap_type": "crosschain", @@ -6635,7 +6732,9 @@ exports[`BridgeStatusController subscription handlers TransactionController:tran "chain_id_destination": "eip155:42161", "chain_id_source": "eip155:42161", "custom_slippage": false, + "destination_hash_present": false, "destination_transaction": "FAILED", + "error_code": "unknown", "error_message": "Transaction failed. tx-error", "failure_phase": "source_execution", "feature_id": "unified_swap_bridge", @@ -6650,6 +6749,7 @@ exports[`BridgeStatusController subscription handlers TransactionController:tran "quoted_vs_used_gas_ratio": 0, "security_warnings": [], "slippage_limit": 0, + "source_hash_present": true, "source_transaction": "COMPLETE", "stx_enabled": false, "swap_type": "single_chain", @@ -6681,7 +6781,9 @@ exports[`BridgeStatusController subscription handlers TransactionController:tran "chain_id_destination": "eip155:10", "chain_id_source": "eip155:42161", "custom_slippage": false, + "destination_hash_present": false, "destination_transaction": "FAILED", + "error_code": "unknown", "error_message": "Transaction failed. approval-tx-error", "failure_phase": "source_execution", "feature_id": "unified_swap_bridge", @@ -6696,6 +6798,7 @@ exports[`BridgeStatusController subscription handlers TransactionController:tran "quoted_vs_used_gas_ratio": 0, "security_warnings": [], "slippage_limit": 0, + "source_hash_present": true, "source_transaction": "COMPLETE", "stx_enabled": false, "swap_type": "crosschain",