diff --git a/packages/bridge-controller/CHANGELOG.md b/packages/bridge-controller/CHANGELOG.md index 2aaeb693fd..67678472ac 100644 --- a/packages/bridge-controller/CHANGELOG.md +++ b/packages/bridge-controller/CHANGELOG.md @@ -14,6 +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` 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] 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 7fcfe0b443..f7d7cf47f3 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/__snapshots__/bridge-controller.test.ts.snap b/packages/bridge-controller/src/__snapshots__/bridge-controller.test.ts.snap index 83398cef0f..21371892e0 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-controller/src/bridge-controller.ts b/packages/bridge-controller/src/bridge-controller.ts index 18780eedec..33d3100ba6 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 0799c778f9..ea203de236 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": "source_execution", "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": "source_execution", "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 ab7f4e2732..4ed3628249 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,10 @@ import { getEVMTxPropertiesFromTransactionMeta, getTxStatusesFromHistory, getPreConfirmationPropertiesFromQuote, + getHashPresenceProperties, + getSubmitFailureTelemetry, + getStatusFailureTelemetry, + promoteFailurePhase, } from './utils/metrics.js'; import { getSelectedChainId } from './utils/network.js'; import { @@ -1538,6 +1543,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 statusFailureTelemetry = getStatusFailureTelemetry( + historyItem.status.srcChain.txHash, + historyItem.status.destChain?.txHash, + ); + const failedHashPresence = { + source_hash_present: + historyHashPresence.source_hash_present || + Boolean(failedProperties?.source_hash_present), + destination_hash_present: + historyHashPresence.destination_hash_present || + Boolean(failedProperties?.destination_hash_present), + }; + const failedPhase = promoteFailurePhase( + failedProperties?.failure_phase ?? statusFailureTelemetry.failure_phase, + failedHashPresence, + ); + const failedErrorCode = + failedProperties?.failure_phase === undefined + ? statusFailureTelemetry.error_code + : (failedProperties.error_code ?? SwapBridgeErrorCode.Unknown); + const requiredEventProperties = { ...baseProperties, ...requestParamProperties, @@ -1839,6 +1877,7 @@ export class BridgeStatusController extends StaticIntervalPollingController { @@ -1176,6 +1177,10 @@ describe('metrics utils', () => { 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 +1198,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', () => { @@ -1313,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 bc01afb570..0cf555332c 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, @@ -379,90 +490,12 @@ export const getEVMTxPropertiesFromTransactionMeta = ( usd_actual_gas: 0, action_type: MetricsActionType.SWAPBRIDGE_V1, ...(transactionMeta.batchId ? { batch_id: transactionMeta.batchId } : {}), - }; -}; - -/** - * 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; -}; - -/** - * 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, + ...getHashPresenceProperties(transactionMeta.hash, undefined), + failure_phase: transactionMeta.hash + ? FailurePhase.SourceExecution + : FailurePhase.Broadcast, + error_code: transactionMeta.error + ? SwapBridgeErrorCode.Unknown + : SwapBridgeErrorCode.MissingErrorObject, }; };