diff --git a/.storybook/test-data.js b/.storybook/test-data.js index a36cbf944981..bce6d082e086 100644 --- a/.storybook/test-data.js +++ b/.storybook/test-data.js @@ -28,6 +28,9 @@ const state = { url: 'https://metamask.github.io/test-dapp/', }, metamask: { + bridgeStatusState: { + txHistory: {}, + }, announcements: { 22: { id: 22, diff --git a/app/_locales/en/messages.json b/app/_locales/en/messages.json index f51c9708fc20..5078593ddcd7 100644 --- a/app/_locales/en/messages.json +++ b/app/_locales/en/messages.json @@ -854,12 +854,18 @@ "bridgeCalculatingAmount": { "message": "Calculating..." }, + "bridgeDestination": { + "message": "Destination" + }, "bridgeDontSend": { "message": "Bridge, don't send" }, "bridgeEnterAmount": { "message": "Enter amount" }, + "bridgeExplorerLinkViewOn": { + "message": "View on $1" + }, "bridgeFrom": { "message": "Bridge from" }, @@ -872,6 +878,25 @@ "bridgeSelectTokenAndAmount": { "message": "Select token and amount" }, + "bridgeSource": { + "message": "Source" + }, + "bridgeStepActionBridgeComplete": { + "message": "$1 received on $2", + "description": "$1 is the amount of the destination asset, $2 is the name of the destination network" + }, + "bridgeStepActionBridgePending": { + "message": "Receiving $1 on $2", + "description": "$1 is the amount of the destination asset, $2 is the name of the destination network" + }, + "bridgeStepActionSwapComplete": { + "message": "Swapped $1 for $2", + "description": "$1 is the amount of the source asset, $2 is the amount of the destination asset" + }, + "bridgeStepActionSwapPending": { + "message": "Swapping $1 for $2", + "description": "$1 is the amount of the source asset, $2 is the amount of the destination asset" + }, "bridgeTimingMinutes": { "message": "$1 minutes", "description": "$1 is the ticker symbol of a an asset the user is being prompted to purchase" @@ -882,9 +907,55 @@ "bridgeTo": { "message": "Bridge to" }, + "bridgeToChain": { + "message": " to $1" + }, "bridgeTotalFeesTooltipText": { "message": "This includes gas fees (paid to crypto miners) and relayer fees (paid to power complex services like bridging).\nFees are based on network traffic and transaction complexity. MetaMask does not profit from either fee." }, + "bridgeTxDetailsBaseFee": { + "message": "Base fee (GWEI)" + }, + "bridgeTxDetailsBridgeAmount": { + "message": "Bridge amount" + }, + "bridgeTxDetailsBridgeType": { + "message": "Bridge type" + }, + "bridgeTxDetailsGasLimit": { + "message": "Gas limit (units)" + }, + "bridgeTxDetailsGasUsed": { + "message": "Gas used (units)" + }, + "bridgeTxDetailsMaxFeePerGas": { + "message": "Max fee per gas" + }, + "bridgeTxDetailsNonce": { + "message": "Nonce" + }, + "bridgeTxDetailsPriorityFee": { + "message": "Priority fee (GWEI)" + }, + "bridgeTxDetailsStatus": { + "message": "Status" + }, + "bridgeTxDetailsTimestamp": { + "message": "Time stamp" + }, + "bridgeTxDetailsTimestampValue": { + "message": "$1 at $2", + "description": "$1 is the date, $2 is the time" + }, + "bridgeTxDetailsTotal": { + "message": "Total" + }, + "bridgeTxDetailsTotalGasFee": { + "message": "Total gas fee" + }, + "bridgeTypeDirectionTo": { + "message": "To" + }, "browserNotSupported": { "message": "Your browser is not supported..." }, diff --git a/app/images/hollow-circle.svg b/app/images/hollow-circle.svg new file mode 100644 index 000000000000..3490e8439b5e --- /dev/null +++ b/app/images/hollow-circle.svg @@ -0,0 +1,3 @@ + + + \ No newline at end of file diff --git a/app/scripts/constants/sentry-state.ts b/app/scripts/constants/sentry-state.ts index d0fbe7bcb085..289bc0a0d29c 100644 --- a/app/scripts/constants/sentry-state.ts +++ b/app/scripts/constants/sentry-state.ts @@ -122,6 +122,11 @@ export const SENTRY_BACKGROUND_STATE = { quotesRefreshCount: true, }, }, + BridgeStatusController: { + bridgeStatusState: { + txHistory: false, + }, + }, CronjobController: { jobs: false, }, diff --git a/app/scripts/controllers/bridge-status/__snapshots__/bridge-status-controller.test.ts.snap b/app/scripts/controllers/bridge-status/__snapshots__/bridge-status-controller.test.ts.snap new file mode 100644 index 000000000000..ebd3a938822e --- /dev/null +++ b/app/scripts/controllers/bridge-status/__snapshots__/bridge-status-controller.test.ts.snap @@ -0,0 +1,213 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`BridgeStatusController constructor rehydrates the tx history state 1`] = ` +{ + "0xsrcTxHash1": { + "account": "0xaccount1", + "estimatedProcessingTimeInSeconds": 15, + "initialDestAssetBalance": undefined, + "pricingData": undefined, + "quote": { + "bridgeId": "lifi", + "bridges": [ + "across", + ], + "destAsset": { + "address": "0x0000000000000000000000000000000000000000", + "chainId": 10, + "coinKey": "ETH", + "decimals": 18, + "icon": "https://raw.githubusercontent.com/trustwallet/assets/master/blockchains/ethereum/assets/0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2/logo.png", + "logoURI": "https://raw.githubusercontent.com/trustwallet/assets/master/blockchains/ethereum/assets/0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2/logo.png", + "name": "ETH", + "priceUSD": "2478.63", + "symbol": "ETH", + }, + "destChainId": 10, + "destTokenAmount": "990654755978612", + "feeData": { + "metabridge": { + "amount": "8750000000000", + "asset": { + "address": "0x0000000000000000000000000000000000000000", + "chainId": 42161, + "coinKey": "ETH", + "decimals": 18, + "icon": "https://raw.githubusercontent.com/trustwallet/assets/master/blockchains/ethereum/assets/0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2/logo.png", + "logoURI": "https://raw.githubusercontent.com/trustwallet/assets/master/blockchains/ethereum/assets/0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2/logo.png", + "name": "ETH", + "priceUSD": "2478.7", + "symbol": "ETH", + }, + }, + }, + "requestId": "197c402f-cb96-4096-9f8c-54aed84ca776", + "srcAsset": { + "address": "0x0000000000000000000000000000000000000000", + "chainId": 42161, + "coinKey": "ETH", + "decimals": 18, + "icon": "https://raw.githubusercontent.com/trustwallet/assets/master/blockchains/ethereum/assets/0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2/logo.png", + "logoURI": "https://raw.githubusercontent.com/trustwallet/assets/master/blockchains/ethereum/assets/0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2/logo.png", + "name": "ETH", + "priceUSD": "2478.7", + "symbol": "ETH", + }, + "srcChainId": 42161, + "srcTokenAmount": "991250000000000", + "steps": [ + { + "action": "bridge", + "destAmount": "990654755978612", + "destAsset": { + "address": "0x0000000000000000000000000000000000000000", + "chainId": 10, + "coinKey": "ETH", + "decimals": 18, + "icon": "https://raw.githubusercontent.com/trustwallet/assets/master/blockchains/ethereum/assets/0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2/logo.png", + "logoURI": "https://raw.githubusercontent.com/trustwallet/assets/master/blockchains/ethereum/assets/0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2/logo.png", + "name": "ETH", + "priceUSD": "2478.63", + "symbol": "ETH", + }, + "destChainId": 10, + "protocol": { + "displayName": "Across", + "icon": "https://raw.githubusercontent.com/lifinance/types/main/src/assets/icons/bridges/acrossv2.png", + "name": "across", + }, + "srcAmount": "991250000000000", + "srcAsset": { + "address": "0x0000000000000000000000000000000000000000", + "chainId": 42161, + "coinKey": "ETH", + "decimals": 18, + "icon": "https://raw.githubusercontent.com/trustwallet/assets/master/blockchains/ethereum/assets/0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2/logo.png", + "logoURI": "https://raw.githubusercontent.com/trustwallet/assets/master/blockchains/ethereum/assets/0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2/logo.png", + "name": "ETH", + "priceUSD": "2478.7", + "symbol": "ETH", + }, + "srcChainId": 42161, + }, + ], + }, + "slippagePercentage": 0, + "startTime": 1729964825189, + "status": { + "srcChain": { + "chainId": 42161, + "txHash": "0xsrcTxHash1", + }, + "status": "PENDING", + }, + "targetContractAddress": "0x23981fC34e69eeDFE2BD9a0a9fCb0719Fe09DbFC", + }, +} +`; + +exports[`BridgeStatusController startPollingForBridgeTxStatus sets the inital tx history state 1`] = ` +{ + "0xsrcTxHash1": { + "account": "0xaccount1", + "estimatedProcessingTimeInSeconds": 15, + "initialDestAssetBalance": undefined, + "pricingData": undefined, + "quote": { + "bridgeId": "lifi", + "bridges": [ + "across", + ], + "destAsset": { + "address": "0x0000000000000000000000000000000000000000", + "chainId": 10, + "coinKey": "ETH", + "decimals": 18, + "icon": "https://raw.githubusercontent.com/trustwallet/assets/master/blockchains/ethereum/assets/0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2/logo.png", + "logoURI": "https://raw.githubusercontent.com/trustwallet/assets/master/blockchains/ethereum/assets/0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2/logo.png", + "name": "ETH", + "priceUSD": "2478.63", + "symbol": "ETH", + }, + "destChainId": 10, + "destTokenAmount": "990654755978612", + "feeData": { + "metabridge": { + "amount": "8750000000000", + "asset": { + "address": "0x0000000000000000000000000000000000000000", + "chainId": 42161, + "coinKey": "ETH", + "decimals": 18, + "icon": "https://raw.githubusercontent.com/trustwallet/assets/master/blockchains/ethereum/assets/0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2/logo.png", + "logoURI": "https://raw.githubusercontent.com/trustwallet/assets/master/blockchains/ethereum/assets/0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2/logo.png", + "name": "ETH", + "priceUSD": "2478.7", + "symbol": "ETH", + }, + }, + }, + "requestId": "197c402f-cb96-4096-9f8c-54aed84ca776", + "srcAsset": { + "address": "0x0000000000000000000000000000000000000000", + "chainId": 42161, + "coinKey": "ETH", + "decimals": 18, + "icon": "https://raw.githubusercontent.com/trustwallet/assets/master/blockchains/ethereum/assets/0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2/logo.png", + "logoURI": "https://raw.githubusercontent.com/trustwallet/assets/master/blockchains/ethereum/assets/0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2/logo.png", + "name": "ETH", + "priceUSD": "2478.7", + "symbol": "ETH", + }, + "srcChainId": 42161, + "srcTokenAmount": "991250000000000", + "steps": [ + { + "action": "bridge", + "destAmount": "990654755978612", + "destAsset": { + "address": "0x0000000000000000000000000000000000000000", + "chainId": 10, + "coinKey": "ETH", + "decimals": 18, + "icon": "https://raw.githubusercontent.com/trustwallet/assets/master/blockchains/ethereum/assets/0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2/logo.png", + "logoURI": "https://raw.githubusercontent.com/trustwallet/assets/master/blockchains/ethereum/assets/0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2/logo.png", + "name": "ETH", + "priceUSD": "2478.63", + "symbol": "ETH", + }, + "destChainId": 10, + "protocol": { + "displayName": "Across", + "icon": "https://raw.githubusercontent.com/lifinance/types/main/src/assets/icons/bridges/acrossv2.png", + "name": "across", + }, + "srcAmount": "991250000000000", + "srcAsset": { + "address": "0x0000000000000000000000000000000000000000", + "chainId": 42161, + "coinKey": "ETH", + "decimals": 18, + "icon": "https://raw.githubusercontent.com/trustwallet/assets/master/blockchains/ethereum/assets/0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2/logo.png", + "logoURI": "https://raw.githubusercontent.com/trustwallet/assets/master/blockchains/ethereum/assets/0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2/logo.png", + "name": "ETH", + "priceUSD": "2478.7", + "symbol": "ETH", + }, + "srcChainId": 42161, + }, + ], + }, + "slippagePercentage": 0, + "startTime": 1729964825189, + "status": { + "srcChain": { + "chainId": 42161, + "txHash": "0xsrcTxHash1", + }, + "status": "PENDING", + }, + "targetContractAddress": "0x23981fC34e69eeDFE2BD9a0a9fCb0719Fe09DbFC", + }, +} +`; diff --git a/app/scripts/controllers/bridge-status/bridge-status-controller.test.ts b/app/scripts/controllers/bridge-status/bridge-status-controller.test.ts new file mode 100644 index 000000000000..3890f27f7f65 --- /dev/null +++ b/app/scripts/controllers/bridge-status/bridge-status-controller.test.ts @@ -0,0 +1,739 @@ +import { flushPromises } from '../../../../test/lib/timer-helpers'; +import { Numeric } from '../../../../shared/modules/Numeric'; +import { + StatusTypes, + ActionTypes, + BridgeId, +} from '../../../../shared/types/bridge-status'; +import BridgeStatusController from './bridge-status-controller'; +import { BridgeStatusControllerMessenger } from './types'; +import { DEFAULT_BRIDGE_STATUS_CONTROLLER_STATE } from './constants'; +import * as bridgeStatusUtils from './utils'; + +const EMPTY_INIT_STATE = { + bridgeStatusState: DEFAULT_BRIDGE_STATUS_CONTROLLER_STATE, +}; + +const getMockQuote = ({ srcChainId = 42161, destChainId = 10 } = {}) => ({ + requestId: '197c402f-cb96-4096-9f8c-54aed84ca776', + srcChainId, + srcTokenAmount: '991250000000000', + srcAsset: { + address: '0x0000000000000000000000000000000000000000', + chainId: srcChainId, + symbol: 'ETH', + decimals: 18, + name: 'ETH', + coinKey: 'ETH', + logoURI: + 'https://raw.githubusercontent.com/trustwallet/assets/master/blockchains/ethereum/assets/0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2/logo.png', + priceUSD: '2478.7', + icon: 'https://raw.githubusercontent.com/trustwallet/assets/master/blockchains/ethereum/assets/0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2/logo.png', + }, + destChainId, + destTokenAmount: '990654755978612', + destAsset: { + address: '0x0000000000000000000000000000000000000000', + chainId: destChainId, + symbol: 'ETH', + decimals: 18, + name: 'ETH', + coinKey: 'ETH', + logoURI: + 'https://raw.githubusercontent.com/trustwallet/assets/master/blockchains/ethereum/assets/0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2/logo.png', + priceUSD: '2478.63', + icon: 'https://raw.githubusercontent.com/trustwallet/assets/master/blockchains/ethereum/assets/0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2/logo.png', + }, + feeData: { + metabridge: { + amount: '8750000000000', + asset: { + address: '0x0000000000000000000000000000000000000000', + chainId: srcChainId, + symbol: 'ETH', + decimals: 18, + name: 'ETH', + coinKey: 'ETH', + logoURI: + 'https://raw.githubusercontent.com/trustwallet/assets/master/blockchains/ethereum/assets/0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2/logo.png', + priceUSD: '2478.7', + icon: 'https://raw.githubusercontent.com/trustwallet/assets/master/blockchains/ethereum/assets/0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2/logo.png', + }, + }, + }, + bridgeId: 'lifi', + bridges: ['across'], + steps: [ + { + action: 'bridge' as ActionTypes, + srcChainId, + destChainId, + protocol: { + name: 'across', + displayName: 'Across', + icon: 'https://raw.githubusercontent.com/lifinance/types/main/src/assets/icons/bridges/acrossv2.png', + }, + srcAsset: { + address: '0x0000000000000000000000000000000000000000', + chainId: srcChainId, + symbol: 'ETH', + decimals: 18, + name: 'ETH', + coinKey: 'ETH', + logoURI: + 'https://raw.githubusercontent.com/trustwallet/assets/master/blockchains/ethereum/assets/0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2/logo.png', + priceUSD: '2478.7', + icon: 'https://raw.githubusercontent.com/trustwallet/assets/master/blockchains/ethereum/assets/0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2/logo.png', + }, + destAsset: { + address: '0x0000000000000000000000000000000000000000', + chainId: destChainId, + symbol: 'ETH', + decimals: 18, + name: 'ETH', + coinKey: 'ETH', + logoURI: + 'https://raw.githubusercontent.com/trustwallet/assets/master/blockchains/ethereum/assets/0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2/logo.png', + priceUSD: '2478.63', + icon: 'https://raw.githubusercontent.com/trustwallet/assets/master/blockchains/ethereum/assets/0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2/logo.png', + }, + srcAmount: '991250000000000', + destAmount: '990654755978612', + }, + ], +}); + +const getMockStartPollingForBridgeTxStatusArgs = ({ + srcTxHash = '0xsrcTxHash1', + account = '0xaccount1', + srcChainId = 42161, + destChainId = 10, +} = {}) => ({ + statusRequest: { + bridgeId: 'lifi', + srcTxHash, + bridge: 'across', + srcChainId, + destChainId, + quote: getMockQuote({ srcChainId, destChainId }), + refuel: false, + }, + quoteResponse: { + quote: getMockQuote({ srcChainId, destChainId }), + trade: { + chainId: srcChainId, + to: '0x23981fC34e69eeDFE2BD9a0a9fCb0719Fe09DbFC', + from: account, + value: '0x038d7ea4c68000', + data: '0x3ce33bff0000000000000000000000000000000000000000000000000000000000000080000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000038d7ea4c6800000000000000000000000000000000000000000000000000000000000000000c0000000000000000000000000000000000000000000000000000000000000000d6c6966694164617074657256320000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000001c0000000000000000000000000e397c4883ec89ed4fc9d258f00c689708b2799c9000000000000000000000000e397c4883ec89ed4fc9d258f00c689708b2799c9000000000000000000000000000000000000000000000000000000000000000a0000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000038589602234000000000000000000000000000000000000000000000000000000000000000140000000000000000000000000000000000000000000000000000007f544a44c0000000000000000000000000056ca675c3633cc16bd6849e2b431d4e8de5e23bf000000000000000000000000000000000000000000000000000000000000006c5a39b10a4f4f0747826140d2c5fe6ef47965741f6f7a4734bf784bf3ae3f24520000000a000222266cc2dca0671d2a17ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffd00dfeeddeadbeef8932eb23bad9bddb5cf81426f78279a53c6c3b7100000000000000000000000000000000000000009ce3c510b3f58edc8d53ae708056e30926f62d0b42d5c9b61c391bb4e8a2c1917f8ed995169ffad0d79af2590303e83c57e15a9e0b248679849556c2e03a1c811b', + gasLimit: 282915, + }, + approval: null, + estimatedProcessingTimeInSeconds: 15, + }, + startTime: 1729964825189, + slippagePercentage: 0, + pricingData: undefined, + initialDestAssetBalance: undefined, + targetContractAddress: '0x23981fC34e69eeDFE2BD9a0a9fCb0719Fe09DbFC', +}); + +const MockStatusResponse = { + getPending: ({ + srcTxHash = '0xsrcTxHash1', + srcChainId = 42161, + destChainId = 10, + } = {}) => ({ + status: 'PENDING' as StatusTypes, + srcChain: { + chainId: srcChainId, + txHash: srcTxHash, + amount: '991250000000000', + token: { + address: '0x0000000000000000000000000000000000000000', + chainId: srcChainId, + symbol: 'ETH', + decimals: 18, + name: 'ETH', + coinKey: 'ETH', + logoURI: + 'https://raw.githubusercontent.com/trustwallet/assets/master/blockchains/ethereum/assets/0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2/logo.png', + priceUSD: '2518.47', + icon: 'https://raw.githubusercontent.com/trustwallet/assets/master/blockchains/ethereum/assets/0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2/logo.png', + }, + }, + destChain: { + chainId: destChainId, + token: {}, + }, + }), + getComplete: ({ + srcTxHash = '0xsrcTxHash1', + destTxHash = '0xdestTxHash1', + srcChainId = 42161, + destChainId = 10, + } = {}) => ({ + status: 'COMPLETE' as StatusTypes, + isExpectedToken: true, + bridge: 'across' as BridgeId, + srcChain: { + chainId: srcChainId, + txHash: srcTxHash, + amount: '991250000000000', + token: { + address: '0x0000000000000000000000000000000000000000', + chainId: srcChainId, + symbol: 'ETH', + decimals: 18, + name: 'ETH', + coinKey: 'ETH', + logoURI: + 'https://raw.githubusercontent.com/trustwallet/assets/master/blockchains/ethereum/assets/0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2/logo.png', + priceUSD: '2478.7', + icon: 'https://raw.githubusercontent.com/trustwallet/assets/master/blockchains/ethereum/assets/0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2/logo.png', + }, + }, + destChain: { + chainId: destChainId, + txHash: destTxHash, + amount: '990654755978611', + token: { + address: '0x0000000000000000000000000000000000000000', + chainId: destChainId, + symbol: 'ETH', + decimals: 18, + name: 'ETH', + coinKey: 'ETH', + logoURI: + 'https://raw.githubusercontent.com/trustwallet/assets/master/blockchains/ethereum/assets/0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2/logo.png', + priceUSD: '2478.63', + icon: 'https://raw.githubusercontent.com/trustwallet/assets/master/blockchains/ethereum/assets/0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2/logo.png', + }, + }, + }), +}; + +const MockTxHistory = { + getInit: ({ + srcTxHash = '0xsrcTxHash1', + account = '0xaccount1', + srcChainId = 42161, + destChainId = 10, + } = {}) => ({ + [srcTxHash]: { + quote: getMockQuote({ srcChainId, destChainId }), + startTime: 1729964825189, + estimatedProcessingTimeInSeconds: 15, + slippagePercentage: 0, + account, + targetContractAddress: '0x23981fC34e69eeDFE2BD9a0a9fCb0719Fe09DbFC', + }, + }), + getPending: ({ + srcTxHash = '0xsrcTxHash1', + account = '0xaccount1', + srcChainId = 42161, + destChainId = 10, + } = {}) => ({ + [srcTxHash]: { + quote: getMockQuote({ srcChainId, destChainId }), + startTime: 1729964825189, + estimatedProcessingTimeInSeconds: 15, + slippagePercentage: 0, + account, + status: MockStatusResponse.getPending({ + srcTxHash, + srcChainId, + }), + targetContractAddress: '0x23981fC34e69eeDFE2BD9a0a9fCb0719Fe09DbFC', + }, + }), + getComplete: ({ + srcTxHash = '0xsrcTxHash1', + account = '0xaccount1', + srcChainId = 42161, + destChainId = 10, + } = {}) => ({ + [srcTxHash]: { + quote: getMockQuote({ srcChainId, destChainId }), + startTime: 1729964825189, + estimatedProcessingTimeInSeconds: 15, + slippagePercentage: 0, + account, + status: MockStatusResponse.getComplete({ srcTxHash }), + targetContractAddress: '0x23981fC34e69eeDFE2BD9a0a9fCb0719Fe09DbFC', + }, + }), +}; + +const getMessengerMock = ({ + account = '0xaccount1', + srcChainId = 42161, +} = {}) => + ({ + call: jest.fn((method: string) => { + if (method === 'AccountsController:getSelectedAccount') { + return { address: account }; + } else if (method === 'NetworkController:findNetworkClientIdByChainId') { + return 'networkClientId'; + } else if (method === 'NetworkController:getState') { + return { selectedNetworkClientId: 'networkClientId' }; + } else if (method === 'NetworkController:getNetworkClientById') { + return { + configuration: { + chainId: new Numeric(srcChainId, 10).toPrefixedHexString(), + }, + }; + } + return null; + }), + publish: jest.fn(), + registerActionHandler: jest.fn(), + registerInitialEventPayload: jest.fn(), + } as unknown as jest.Mocked); + +const executePollingWithPendingStatus = async () => { + // Setup + jest.useFakeTimers(); + const bridgeStatusController = new BridgeStatusController({ + messenger: getMessengerMock(), + }); + const startPollingByNetworkClientIdSpy = jest.spyOn( + bridgeStatusController, + 'startPollingByNetworkClientId', + ); + const fetchBridgeTxStatusSpy = jest.spyOn( + bridgeStatusUtils, + 'fetchBridgeTxStatus', + ); + + // Execution + await bridgeStatusController.startPollingForBridgeTxStatus( + getMockStartPollingForBridgeTxStatusArgs(), + ); + fetchBridgeTxStatusSpy.mockImplementationOnce(async () => { + return MockStatusResponse.getPending(); + }); + jest.advanceTimersByTime(10000); + await flushPromises(); + + return { + bridgeStatusController, + startPollingByNetworkClientIdSpy, + fetchBridgeTxStatusSpy, + }; +}; + +describe('BridgeStatusController', () => { + describe('constructor', () => { + it('should setup correctly', () => { + const bridgeStatusController = new BridgeStatusController({ + messenger: getMessengerMock(), + }); + expect(bridgeStatusController.state).toEqual(EMPTY_INIT_STATE); + }); + it('rehydrates the tx history state', async () => { + // Setup + const bridgeStatusController = new BridgeStatusController({ + messenger: getMessengerMock(), + state: { + bridgeStatusState: { + txHistory: MockTxHistory.getPending(), + }, + }, + }); + + // Execution + await bridgeStatusController.startPollingForBridgeTxStatus( + getMockStartPollingForBridgeTxStatusArgs(), + ); + + // Assertion + expect( + bridgeStatusController.state.bridgeStatusState.txHistory, + ).toMatchSnapshot(); + }); + it('restarts polling for history items that are not complete', async () => { + // Setup + jest.useFakeTimers(); + const fetchBridgeTxStatusSpy = jest.spyOn( + bridgeStatusUtils, + 'fetchBridgeTxStatus', + ); + + // Execution + // eslint-disable-next-line @typescript-eslint/no-unused-vars + const bridgeStatusController = new BridgeStatusController({ + messenger: getMessengerMock(), + state: { + bridgeStatusState: { + txHistory: MockTxHistory.getPending(), + }, + }, + }); + jest.advanceTimersByTime(10000); + await flushPromises(); + + // Assertions + expect(fetchBridgeTxStatusSpy).toHaveBeenCalledTimes(1); + }); + }); + describe('startPollingForBridgeTxStatus', () => { + it('sets the inital tx history state', async () => { + // Setup + const bridgeStatusController = new BridgeStatusController({ + messenger: getMessengerMock(), + }); + + // Execution + await bridgeStatusController.startPollingForBridgeTxStatus( + getMockStartPollingForBridgeTxStatusArgs(), + ); + + // Assertion + expect( + bridgeStatusController.state.bridgeStatusState.txHistory, + ).toMatchSnapshot(); + }); + it('starts polling and updates the tx history when the status response is received', async () => { + const { + bridgeStatusController, + startPollingByNetworkClientIdSpy, + fetchBridgeTxStatusSpy, + } = await executePollingWithPendingStatus(); + + // Assertions + expect(startPollingByNetworkClientIdSpy).toHaveBeenCalledTimes(1); + expect(fetchBridgeTxStatusSpy).toHaveBeenCalled(); + expect(bridgeStatusController.state.bridgeStatusState.txHistory).toEqual( + MockTxHistory.getPending(), + ); + }); + it('stops polling when the status response is complete', async () => { + // Setup + jest.useFakeTimers(); + const bridgeStatusController = new BridgeStatusController({ + messenger: getMessengerMock(), + }); + const fetchBridgeTxStatusSpy = jest.spyOn( + bridgeStatusUtils, + 'fetchBridgeTxStatus', + ); + const stopPollingByNetworkClientIdSpy = jest.spyOn( + bridgeStatusController, + 'stopPollingByPollingToken', + ); + + // Execution + await bridgeStatusController.startPollingForBridgeTxStatus( + getMockStartPollingForBridgeTxStatusArgs(), + ); + fetchBridgeTxStatusSpy.mockImplementationOnce(async () => { + return MockStatusResponse.getComplete(); + }); + jest.advanceTimersByTime(10000); + await flushPromises(); + + // Assertions + expect(stopPollingByNetworkClientIdSpy).toHaveBeenCalledTimes(1); + expect(bridgeStatusController.state.bridgeStatusState.txHistory).toEqual( + MockTxHistory.getComplete(), + ); + }); + }); + describe('resetState', () => { + it('resets the state', async () => { + const { bridgeStatusController } = + await executePollingWithPendingStatus(); + + expect(bridgeStatusController.state.bridgeStatusState.txHistory).toEqual( + MockTxHistory.getPending(), + ); + bridgeStatusController.resetState(); + expect(bridgeStatusController.state.bridgeStatusState.txHistory).toEqual( + EMPTY_INIT_STATE.bridgeStatusState.txHistory, + ); + }); + }); + describe('wipeBridgeStatus', () => { + it('wipes the bridge status for the given address', async () => { + // Setup + jest.useFakeTimers(); + + let getSelectedAccountCalledTimes = 0; + const messengerMock = { + call: jest.fn((method: string) => { + if (method === 'AccountsController:getSelectedAccount') { + let account; + if (getSelectedAccountCalledTimes === 0) { + account = '0xaccount1'; + } else { + account = '0xaccount2'; + } + getSelectedAccountCalledTimes += 1; + return { address: account }; + } else if ( + method === 'NetworkController:findNetworkClientIdByChainId' + ) { + return 'networkClientId'; + } else if (method === 'NetworkController:getState') { + return { selectedNetworkClientId: 'networkClientId' }; + } else if (method === 'NetworkController:getNetworkClientById') { + return { + configuration: { + chainId: new Numeric(42161, 10).toPrefixedHexString(), + }, + }; + } + return null; + }), + publish: jest.fn(), + registerActionHandler: jest.fn(), + registerInitialEventPayload: jest.fn(), + } as unknown as jest.Mocked; + const bridgeStatusController = new BridgeStatusController({ + messenger: messengerMock, + }); + const fetchBridgeTxStatusSpy = jest + .spyOn(bridgeStatusUtils, 'fetchBridgeTxStatus') + .mockImplementationOnce(async () => { + return MockStatusResponse.getComplete(); + }) + .mockImplementationOnce(async () => { + return MockStatusResponse.getComplete({ + srcTxHash: '0xsrcTxHash2', + destTxHash: '0xdestTxHash2', + }); + }); + + // Start polling for 0xaccount1 + bridgeStatusController.startPollingForBridgeTxStatus( + getMockStartPollingForBridgeTxStatusArgs(), + ); + jest.advanceTimersByTime(10_000); + expect(fetchBridgeTxStatusSpy).toHaveBeenCalledTimes(1); + + // Start polling for 0xaccount2 + bridgeStatusController.startPollingForBridgeTxStatus( + getMockStartPollingForBridgeTxStatusArgs({ + srcTxHash: '0xsrcTxHash2', + account: '0xaccount2', + }), + ); + jest.advanceTimersByTime(10_000); + expect(fetchBridgeTxStatusSpy).toHaveBeenCalledTimes(2); + + // Check that both accounts have a tx history entry + expect( + bridgeStatusController.state.bridgeStatusState.txHistory, + ).toHaveProperty('0xsrcTxHash1'); + expect( + bridgeStatusController.state.bridgeStatusState.txHistory, + ).toHaveProperty('0xsrcTxHash2'); + + // Wipe the status for 1 account only + bridgeStatusController.wipeBridgeStatus({ + address: '0xaccount1', + ignoreNetwork: false, + }); + + // Assertions + const txHistoryItems = Object.values( + bridgeStatusController.state.bridgeStatusState.txHistory, + ); + expect(txHistoryItems).toHaveLength(1); + expect(txHistoryItems[0].account).toEqual('0xaccount2'); + }); + it('wipes the bridge status for all networks if ignoreNetwork is true', () => { + // Setup + jest.useFakeTimers(); + const messengerMock = { + call: jest.fn((method: string) => { + if (method === 'AccountsController:getSelectedAccount') { + return { address: '0xaccount1' }; + } else if ( + method === 'NetworkController:findNetworkClientIdByChainId' + ) { + return 'networkClientId'; + } else if (method === 'NetworkController:getState') { + return { selectedNetworkClientId: 'networkClientId' }; + } else if (method === 'NetworkController:getNetworkClientById') { + return { + configuration: { + chainId: new Numeric(42161, 10).toPrefixedHexString(), + }, + }; + } + return null; + }), + publish: jest.fn(), + registerActionHandler: jest.fn(), + registerInitialEventPayload: jest.fn(), + } as unknown as jest.Mocked; + const bridgeStatusController = new BridgeStatusController({ + messenger: messengerMock, + }); + const fetchBridgeTxStatusSpy = jest + .spyOn(bridgeStatusUtils, 'fetchBridgeTxStatus') + .mockImplementationOnce(async () => { + return MockStatusResponse.getComplete(); + }) + .mockImplementationOnce(async () => { + return MockStatusResponse.getComplete({ + srcTxHash: '0xsrcTxHash2', + }); + }); + + // Start polling for chainId 42161 to chainId 1 + bridgeStatusController.startPollingForBridgeTxStatus( + getMockStartPollingForBridgeTxStatusArgs({ + account: '0xaccount1', + srcTxHash: '0xsrcTxHash1', + srcChainId: 42161, + destChainId: 1, + }), + ); + jest.advanceTimersByTime(10_000); + expect(fetchBridgeTxStatusSpy).toHaveBeenCalledTimes(1); + + // Start polling for chainId 10 to chainId 123 + bridgeStatusController.startPollingForBridgeTxStatus( + getMockStartPollingForBridgeTxStatusArgs({ + account: '0xaccount1', + srcTxHash: '0xsrcTxHash2', + srcChainId: 10, + destChainId: 123, + }), + ); + jest.advanceTimersByTime(10_000); + expect(fetchBridgeTxStatusSpy).toHaveBeenCalledTimes(2); + + // Check we have a tx history entry for each chainId + expect( + bridgeStatusController.state.bridgeStatusState.txHistory['0xsrcTxHash1'] + .quote.srcChainId, + ).toEqual(42161); + expect( + bridgeStatusController.state.bridgeStatusState.txHistory['0xsrcTxHash1'] + .quote.destChainId, + ).toEqual(1); + + expect( + bridgeStatusController.state.bridgeStatusState.txHistory['0xsrcTxHash2'] + .quote.srcChainId, + ).toEqual(10); + expect( + bridgeStatusController.state.bridgeStatusState.txHistory['0xsrcTxHash2'] + .quote.destChainId, + ).toEqual(123); + + bridgeStatusController.wipeBridgeStatus({ + address: '0xaccount1', + ignoreNetwork: true, + }); + + // Assertions + const txHistoryItems = Object.values( + bridgeStatusController.state.bridgeStatusState.txHistory, + ); + expect(txHistoryItems).toHaveLength(0); + }); + it('wipes the bridge status only for the current network if ignoreNetwork is false', () => { + // Setup + jest.useFakeTimers(); + const messengerMock = { + call: jest.fn((method: string) => { + if (method === 'AccountsController:getSelectedAccount') { + return { address: '0xaccount1' }; + } else if ( + method === 'NetworkController:findNetworkClientIdByChainId' + ) { + return 'networkClientId'; + } else if (method === 'NetworkController:getState') { + return { selectedNetworkClientId: 'networkClientId' }; + } else if (method === 'NetworkController:getNetworkClientById') { + return { + configuration: { + // This is what controls the selectedNetwork and what gets wiped in this test + chainId: new Numeric(42161, 10).toPrefixedHexString(), + }, + }; + } + return null; + }), + publish: jest.fn(), + registerActionHandler: jest.fn(), + registerInitialEventPayload: jest.fn(), + } as unknown as jest.Mocked; + const bridgeStatusController = new BridgeStatusController({ + messenger: messengerMock, + }); + const fetchBridgeTxStatusSpy = jest + .spyOn(bridgeStatusUtils, 'fetchBridgeTxStatus') + .mockImplementationOnce(async () => { + return MockStatusResponse.getComplete(); + }) + .mockImplementationOnce(async () => { + return MockStatusResponse.getComplete({ + srcTxHash: '0xsrcTxHash2', + }); + }); + + // Start polling for chainId 42161 to chainId 1 + bridgeStatusController.startPollingForBridgeTxStatus( + getMockStartPollingForBridgeTxStatusArgs({ + account: '0xaccount1', + srcTxHash: '0xsrcTxHash1', + srcChainId: 42161, + destChainId: 1, + }), + ); + jest.advanceTimersByTime(10_000); + expect(fetchBridgeTxStatusSpy).toHaveBeenCalledTimes(1); + + // Start polling for chainId 10 to chainId 123 + bridgeStatusController.startPollingForBridgeTxStatus( + getMockStartPollingForBridgeTxStatusArgs({ + account: '0xaccount1', + srcTxHash: '0xsrcTxHash2', + srcChainId: 10, + destChainId: 123, + }), + ); + jest.advanceTimersByTime(10_000); + expect(fetchBridgeTxStatusSpy).toHaveBeenCalledTimes(2); + + // Check we have a tx history entry for each chainId + expect( + bridgeStatusController.state.bridgeStatusState.txHistory['0xsrcTxHash1'] + .quote.srcChainId, + ).toEqual(42161); + expect( + bridgeStatusController.state.bridgeStatusState.txHistory['0xsrcTxHash1'] + .quote.destChainId, + ).toEqual(1); + + expect( + bridgeStatusController.state.bridgeStatusState.txHistory['0xsrcTxHash2'] + .quote.srcChainId, + ).toEqual(10); + expect( + bridgeStatusController.state.bridgeStatusState.txHistory['0xsrcTxHash2'] + .quote.destChainId, + ).toEqual(123); + + bridgeStatusController.wipeBridgeStatus({ + address: '0xaccount1', + ignoreNetwork: false, + }); + + // Assertions + const txHistoryItems = Object.values( + bridgeStatusController.state.bridgeStatusState.txHistory, + ); + expect(txHistoryItems).toHaveLength(1); + expect(txHistoryItems[0].quote.srcChainId).toEqual(10); + expect(txHistoryItems[0].quote.destChainId).toEqual(123); + }); + }); +}); diff --git a/app/scripts/controllers/bridge-status/bridge-status-controller.ts b/app/scripts/controllers/bridge-status/bridge-status-controller.ts new file mode 100644 index 000000000000..18010ae0de3d --- /dev/null +++ b/app/scripts/controllers/bridge-status/bridge-status-controller.ts @@ -0,0 +1,310 @@ +import { StateMetadata } from '@metamask/base-controller'; +import { StaticIntervalPollingController } from '@metamask/polling-controller'; +import { Hex } from '@metamask/utils'; +// eslint-disable-next-line import/no-restricted-paths +import { + StartPollingForBridgeTxStatusArgs, + StatusRequest, + StatusTypes, + BridgeStatusControllerState, +} from '../../../../shared/types/bridge-status'; +import { decimalToPrefixedHex } from '../../../../shared/modules/conversion.utils'; +import { + BRIDGE_STATUS_CONTROLLER_NAME, + DEFAULT_BRIDGE_STATUS_CONTROLLER_STATE, + REFRESH_INTERVAL_MS, +} from './constants'; +import { BridgeStatusControllerMessenger } from './types'; +import { fetchBridgeTxStatus } from './utils'; + +const metadata: StateMetadata<{ + bridgeStatusState: BridgeStatusControllerState; +}> = { + // We want to persist the bridge status state so that we can show the proper data for the Activity list + // basically match the behavior of TransactionController + bridgeStatusState: { + persist: true, + anonymous: false, + }, +}; + +type SrcTxHash = string; +export type FetchBridgeTxStatusArgs = { + statusRequest: StatusRequest; +}; +export default class BridgeStatusController extends StaticIntervalPollingController< + typeof BRIDGE_STATUS_CONTROLLER_NAME, + { bridgeStatusState: BridgeStatusControllerState }, + BridgeStatusControllerMessenger +> { + #pollingTokensBySrcTxHash: Record = {}; + + constructor({ + messenger, + state, + }: { + messenger: BridgeStatusControllerMessenger; + state?: Partial<{ + bridgeStatusState: BridgeStatusControllerState; + }>; + }) { + super({ + name: BRIDGE_STATUS_CONTROLLER_NAME, + metadata, + messenger, + // Restore the persisted state + state: { + ...state, + bridgeStatusState: { + ...DEFAULT_BRIDGE_STATUS_CONTROLLER_STATE, + ...state?.bridgeStatusState, + }, + }, + }); + + // Register action handlers + this.messagingSystem.registerActionHandler( + `${BRIDGE_STATUS_CONTROLLER_NAME}:startPollingForBridgeTxStatus`, + this.startPollingForBridgeTxStatus.bind(this), + ); + this.messagingSystem.registerActionHandler( + `${BRIDGE_STATUS_CONTROLLER_NAME}:wipeBridgeStatus`, + this.wipeBridgeStatus.bind(this), + ); + + // Set interval + this.setIntervalLength(REFRESH_INTERVAL_MS); + + // If you close the extension, but keep the browser open, the polling continues + // If you close the browser, the polling stops + // Check for historyItems that do not have a status of complete and restart polling + this.#restartPollingForIncompleteHistoryItems(); + } + + resetState = () => { + this.update((_state) => { + _state.bridgeStatusState = { + ...DEFAULT_BRIDGE_STATUS_CONTROLLER_STATE, + }; + }); + }; + + wipeBridgeStatus = ({ + address, + ignoreNetwork, + }: { + address: string; + ignoreNetwork: boolean; + }) => { + // Wipe all networks for this address + if (ignoreNetwork) { + this.update((_state) => { + _state.bridgeStatusState = { + ...DEFAULT_BRIDGE_STATUS_CONTROLLER_STATE, + }; + }); + } else { + const { selectedNetworkClientId } = this.messagingSystem.call( + 'NetworkController:getState', + ); + const selectedNetworkClient = this.messagingSystem.call( + 'NetworkController:getNetworkClientById', + selectedNetworkClientId, + ); + const selectedChainId = selectedNetworkClient.configuration.chainId; + + this.#wipeBridgeStatusByChainId(address, selectedChainId); + } + }; + + #restartPollingForIncompleteHistoryItems = () => { + // Check for historyItems that do not have a status of complete and restart polling + const { bridgeStatusState } = this.state; + const historyItems = Object.values(bridgeStatusState.txHistory); + const incompleteHistoryItems = historyItems + .filter( + (historyItem) => historyItem.status.status !== StatusTypes.COMPLETE, + ) + .filter((historyItem) => { + // Check if we are already polling this tx, if so, skip restarting polling for that + const srcTxHash = historyItem.status.srcChain.txHash; + const pollingToken = this.#pollingTokensBySrcTxHash[srcTxHash]; + return !pollingToken; + }); + + incompleteHistoryItems.forEach((historyItem) => { + const statusRequest = { + bridgeId: historyItem.quote.bridgeId, + srcTxHash: historyItem.status.srcChain.txHash, + bridge: historyItem.quote.bridges[0], + srcChainId: historyItem.quote.srcChainId, + destChainId: historyItem.quote.destChainId, + quote: historyItem.quote, + refuel: Boolean(historyItem.quote.refuel), + }; + + const hexSourceChainId = decimalToPrefixedHex(statusRequest.srcChainId); + const networkClientId = this.messagingSystem.call( + 'NetworkController:findNetworkClientIdByChainId', + hexSourceChainId, + ); + + // We manually call startPollingByNetworkClientId() here rather than go through startPollingForBridgeTxStatus() + // because we don't want to overwrite the existing historyItem in state + const options: FetchBridgeTxStatusArgs = { statusRequest }; + this.#pollingTokensBySrcTxHash[statusRequest.srcTxHash] = + this.startPollingByNetworkClientId(networkClientId, options); + }); + }; + + startPollingForBridgeTxStatus = ( + startPollingForBridgeTxStatusArgs: StartPollingForBridgeTxStatusArgs, + ) => { + const { + statusRequest, + quoteResponse, + startTime, + slippagePercentage, + pricingData, + initialDestAssetBalance, + targetContractAddress, + } = startPollingForBridgeTxStatusArgs; + const hexSourceChainId = decimalToPrefixedHex(statusRequest.srcChainId); + + const { bridgeStatusState } = this.state; + const { address: account } = this.#getSelectedAccount(); + + // Write all non-status fields to state so we can reference the quote in Activity list without the Bridge API + // We know it's in progress but not the exact status yet + this.update((_state) => { + _state.bridgeStatusState = { + ...bridgeStatusState, + txHistory: { + ...bridgeStatusState.txHistory, + [statusRequest.srcTxHash]: { + quote: quoteResponse.quote, + startTime, + estimatedProcessingTimeInSeconds: + quoteResponse.estimatedProcessingTimeInSeconds, + slippagePercentage, + pricingData, + initialDestAssetBalance, + targetContractAddress, + account, + status: { + // We always have a PENDING status when we start polling for a tx, don't need the Bridge API for that + // Also we know the bare minimum fields for status at this point in time + status: StatusTypes.PENDING, + srcChain: { + chainId: statusRequest.srcChainId, + txHash: statusRequest.srcTxHash, + }, + }, + }, + }, + }; + }); + + const networkClientId = this.messagingSystem.call( + 'NetworkController:findNetworkClientIdByChainId', + hexSourceChainId, + ); + this.#pollingTokensBySrcTxHash[statusRequest.srcTxHash] = + this.startPollingByNetworkClientId(networkClientId, { statusRequest }); + }; + + // This will be called after you call this.startPollingByNetworkClientId() + // The args passed in are the args you passed in to startPollingByNetworkClientId() + _executePoll = async ( + _networkClientId: string, + fetchBridgeTxStatusArgs: FetchBridgeTxStatusArgs, + ) => { + await this.#fetchBridgeTxStatus(fetchBridgeTxStatusArgs); + }; + + #getSelectedAccount() { + return this.messagingSystem.call('AccountsController:getSelectedAccount'); + } + + #fetchBridgeTxStatus = async ({ statusRequest }: FetchBridgeTxStatusArgs) => { + const { bridgeStatusState } = this.state; + + try { + // We try here because we receive 500 errors from Bridge API if we try to fetch immediately after submitting the source tx + // Oddly mostly happens on Optimism, never on Arbitrum. By the 2nd fetch, the Bridge API responds properly. + const status = await fetchBridgeTxStatus(statusRequest); + + // No need to purge these on network change or account change, TransactionController does not purge either. + // TODO In theory we can skip checking status if it's not the current account/network + // we need to keep track of the account that this is associated with as well so that we don't show it in Activity list for other accounts + // First stab at this will not stop polling when you are on a different account + this.update((_state) => { + const bridgeHistoryItem = + _state.bridgeStatusState.txHistory[statusRequest.srcTxHash]; + + _state.bridgeStatusState = { + ...bridgeStatusState, + txHistory: { + ...bridgeStatusState.txHistory, + [statusRequest.srcTxHash]: { + ...bridgeHistoryItem, + status, + }, + }, + }; + }); + + const pollingToken = + this.#pollingTokensBySrcTxHash[statusRequest.srcTxHash]; + if (status.status === StatusTypes.COMPLETE && pollingToken) { + this.stopPollingByPollingToken(pollingToken); + } + } catch (e) { + console.log('Failed to fetch bridge tx status', e); + } + }; + + // Wipes the bridge status for the given address and chainId + // Will match either source or destination chainId to the selectedChainId + #wipeBridgeStatusByChainId = (address: string, selectedChainId: Hex) => { + const sourceTxHashesToDelete = Object.keys( + this.state.bridgeStatusState.txHistory, + ).filter((sourceTxHash) => { + const bridgeHistoryItem = + this.state.bridgeStatusState.txHistory[sourceTxHash]; + + const hexSourceChainId = decimalToPrefixedHex( + bridgeHistoryItem.quote.srcChainId, + ); + const hexDestChainId = decimalToPrefixedHex( + bridgeHistoryItem.quote.destChainId, + ); + + return ( + bridgeHistoryItem.account === address && + (hexSourceChainId === selectedChainId || + hexDestChainId === selectedChainId) + ); + }); + + sourceTxHashesToDelete.forEach((sourceTxHash) => { + const pollingToken = this.#pollingTokensBySrcTxHash[sourceTxHash]; + + if (pollingToken) { + this.stopPollingByPollingToken( + this.#pollingTokensBySrcTxHash[sourceTxHash], + ); + } + }); + + this.update((_state) => { + _state.bridgeStatusState.txHistory = sourceTxHashesToDelete.reduce( + (acc, sourceTxHash) => { + delete acc[sourceTxHash]; + return acc; + }, + _state.bridgeStatusState.txHistory, + ); + }); + }; +} diff --git a/app/scripts/controllers/bridge-status/constants.ts b/app/scripts/controllers/bridge-status/constants.ts new file mode 100644 index 000000000000..83208bdc73d8 --- /dev/null +++ b/app/scripts/controllers/bridge-status/constants.ts @@ -0,0 +1,10 @@ +import { BridgeStatusControllerState } from '../../../../shared/types/bridge-status'; + +export const REFRESH_INTERVAL_MS = 10 * 1000; + +export const BRIDGE_STATUS_CONTROLLER_NAME = 'BridgeStatusController'; + +export const DEFAULT_BRIDGE_STATUS_CONTROLLER_STATE: BridgeStatusControllerState = + { + txHistory: {}, + }; diff --git a/app/scripts/controllers/bridge-status/types.ts b/app/scripts/controllers/bridge-status/types.ts new file mode 100644 index 000000000000..040cd1e0c9bd --- /dev/null +++ b/app/scripts/controllers/bridge-status/types.ts @@ -0,0 +1,56 @@ +import { + ControllerGetStateAction, + ControllerStateChangeEvent, + RestrictedControllerMessenger, +} from '@metamask/base-controller'; +import { + NetworkControllerFindNetworkClientIdByChainIdAction, + NetworkControllerGetNetworkClientByIdAction, + NetworkControllerGetStateAction, +} from '@metamask/network-controller'; +import { AccountsControllerGetSelectedAccountAction } from '@metamask/accounts-controller'; +import { + BridgeStatusAction, + BridgeStatusControllerState, +} from '../../../../shared/types/bridge-status'; +import { BRIDGE_STATUS_CONTROLLER_NAME } from './constants'; +import BridgeStatusController from './bridge-status-controller'; + +type BridgeStatusControllerAction< + FunctionName extends keyof BridgeStatusController, +> = { + type: `${typeof BRIDGE_STATUS_CONTROLLER_NAME}:${FunctionName}`; + handler: BridgeStatusController[FunctionName]; +}; + +// Maps to BridgeController function names +type BridgeStatusControllerActions = + | BridgeStatusControllerAction + | BridgeStatusControllerAction + | ControllerGetStateAction< + typeof BRIDGE_STATUS_CONTROLLER_NAME, + BridgeStatusControllerState + >; + +type BridgeStatusControllerEvents = ControllerStateChangeEvent< + typeof BRIDGE_STATUS_CONTROLLER_NAME, + BridgeStatusControllerState +>; + +type AllowedActions = + | NetworkControllerFindNetworkClientIdByChainIdAction + | NetworkControllerGetStateAction + | NetworkControllerGetNetworkClientByIdAction + | AccountsControllerGetSelectedAccountAction; +type AllowedEvents = never; + +/** + * The messenger for the BridgeStatusController. + */ +export type BridgeStatusControllerMessenger = RestrictedControllerMessenger< + typeof BRIDGE_STATUS_CONTROLLER_NAME, + BridgeStatusControllerActions | AllowedActions, + BridgeStatusControllerEvents | AllowedEvents, + AllowedActions['type'], + AllowedEvents['type'] +>; diff --git a/app/scripts/controllers/bridge-status/utils.ts b/app/scripts/controllers/bridge-status/utils.ts new file mode 100644 index 000000000000..323e7e2faeab --- /dev/null +++ b/app/scripts/controllers/bridge-status/utils.ts @@ -0,0 +1,49 @@ +import { + BRIDGE_API_BASE_URL, + BRIDGE_CLIENT_ID, +} from '../../../../shared/constants/bridge'; +import fetchWithCache from '../../../../shared/lib/fetch-with-cache'; +import { + StatusResponse, + StatusRequest, +} from '../../../../shared/types/bridge-status'; +import { validateResponse, validators } from './validators'; + +const CLIENT_ID_HEADER = { 'X-Client-Id': BRIDGE_CLIENT_ID }; + +export const BRIDGE_STATUS_BASE_URL = `${BRIDGE_API_BASE_URL}/getTxStatus`; + +export const fetchBridgeTxStatus = async (statusRequest: StatusRequest) => { + // Assemble params + const { quote, ...statusRequestNoQuote } = statusRequest; + const statusRequestNoQuoteFormatted = Object.fromEntries( + Object.entries(statusRequestNoQuote).map(([key, value]) => [ + key, + value.toString(), + ]), + ); + const params = new URLSearchParams(statusRequestNoQuoteFormatted); + + // Fetch + const url = `${BRIDGE_STATUS_BASE_URL}?${params.toString()}`; + + const rawTxStatus = await fetchWithCache({ + url, + fetchOptions: { method: 'GET', headers: CLIENT_ID_HEADER }, + cacheOptions: { cacheRefreshTime: 0 }, + functionName: 'fetchBridgeTxStatus', + }); + + // Validate + const isValid = validateResponse( + validators, + rawTxStatus, + BRIDGE_STATUS_BASE_URL, + ); + if (!isValid) { + throw new Error('Invalid response from bridge'); + } + + // Return + return rawTxStatus; +}; diff --git a/app/scripts/controllers/bridge-status/validators.test.ts b/app/scripts/controllers/bridge-status/validators.test.ts new file mode 100644 index 000000000000..18ca81d7a5b2 --- /dev/null +++ b/app/scripts/controllers/bridge-status/validators.test.ts @@ -0,0 +1,238 @@ +import { StatusResponse } from '../../../../shared/types/bridge-status'; +import { validateResponse, validators } from './validators'; + +const BridgeTxStatusResponses = { + STATUS_PENDING_VALID: { + status: 'PENDING', + bridge: 'across', + srcChain: { + chainId: 42161, + txHash: + '0x76a65e4cea35d8732f0e3250faed00ba764ad5a0e7c51cb1bafbc9d76ac0b325', + amount: '991250000000000', + token: { + address: '0x0000000000000000000000000000000000000000', + chainId: 42161, + symbol: 'ETH', + decimals: 18, + name: 'ETH', + coinKey: 'ETH', + logoURI: + 'https://raw.githubusercontent.com/trustwallet/assets/master/blockchains/ethereum/assets/0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2/logo.png', + priceUSD: '2550.12', + icon: 'https://raw.githubusercontent.com/trustwallet/assets/master/blockchains/ethereum/assets/0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2/logo.png', + }, + }, + destChain: { + chainId: '10', + token: {}, + }, + }, + STATUS_PENDING_VALID_MISSING_FIELDS: { + status: 'PENDING', + srcChain: { + chainId: 42161, + txHash: + '0x5cbda572c686a5a57fe62735325e408f9164f77a4787df29ce13edef765adaa9', + }, + }, + STATUS_PENDING_VALID_MISSING_FIELDS_2: { + status: 'PENDING', + bridge: 'hop', + srcChain: { + chainId: 42161, + txHash: + '0x5cbda572c686a5a57fe62735325e408f9164f77a4787df29ce13edef765adaa9', + amount: '991250000000000', + token: { + chainId: 42161, + address: '0xeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee', + symbol: 'ETH', + name: 'Ethereum', + decimals: 18, + icon: 'https://media.socket.tech/tokens/all/ETH', + logoURI: 'https://media.socket.tech/tokens/all/ETH', + chainAgnosticId: null, + }, + }, + }, + STATUS_PENDING_INVALID_MISSING_FIELDS: { + status: 'PENDING', + bridge: 'across', + srcChain: { + chainId: 42161, + txHash: + '0x76a65e4cea35d8732f0e3250faed00ba764ad5a0e7c51cb1bafbc9d76ac0b325', + amount: '991250000000000', + token: { + address: '0x0000000000000000000000000000000000000000', + chainId: 42161, + symbol: 'ETH', + decimals: 18, + name: 'ETH', + coinKey: 'ETH', + logoURI: + 'https://raw.githubusercontent.com/trustwallet/assets/master/blockchains/ethereum/assets/0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2/logo.png', + priceUSD: '2550.12', + icon: 'https://raw.githubusercontent.com/trustwallet/assets/master/blockchains/ethereum/assets/0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2/logo.png', + }, + }, + destChain: { + token: {}, + }, + }, + STATUS_COMPLETE_VALID: { + status: 'COMPLETE', + isExpectedToken: true, + bridge: 'across', + srcChain: { + chainId: 10, + txHash: + '0x9fdc426692aba1f81e145834602ed59ed331054e5b91a09a673cb12d4b4f6a33', + amount: '4956250000000000', + token: { + address: '0x0000000000000000000000000000000000000000', + chainId: 10, + symbol: 'ETH', + decimals: 18, + name: 'ETH', + coinKey: 'ETH', + logoURI: + 'https://raw.githubusercontent.com/trustwallet/assets/master/blockchains/ethereum/assets/0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2/logo.png', + priceUSD: '2649.21', + icon: 'https://raw.githubusercontent.com/trustwallet/assets/master/blockchains/ethereum/assets/0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2/logo.png', + }, + }, + destChain: { + chainId: '42161', + txHash: + '0x3a494e672717f9b1f2b64a48a19985842d82d0747400fccebebc7a4e99c8eaab', + amount: '4926701727965948', + token: { + address: '0x0000000000000000000000000000000000000000', + chainId: 42161, + symbol: 'ETH', + decimals: 18, + name: 'ETH', + coinKey: 'ETH', + logoURI: + 'https://raw.githubusercontent.com/trustwallet/assets/master/blockchains/ethereum/assets/0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2/logo.png', + priceUSD: '2648.72', + icon: 'https://raw.githubusercontent.com/trustwallet/assets/master/blockchains/ethereum/assets/0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2/logo.png', + }, + }, + }, + STATUS_COMPLETE_VALID_MISSING_FIELDS: { + status: 'COMPLETE', + bridge: 'across', + srcChain: { + chainId: 10, + txHash: + '0x9fdc426692aba1f81e145834602ed59ed331054e5b91a09a673cb12d4b4f6a33', + amount: '4956250000000000', + token: { + address: '0x0000000000000000000000000000000000000000', + chainId: 10, + symbol: 'ETH', + decimals: 18, + name: 'ETH', + coinKey: 'ETH', + logoURI: + 'https://raw.githubusercontent.com/trustwallet/assets/master/blockchains/ethereum/assets/0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2/logo.png', + priceUSD: '2649.21', + icon: 'https://raw.githubusercontent.com/trustwallet/assets/master/blockchains/ethereum/assets/0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2/logo.png', + }, + }, + destChain: { + chainId: '42161', + txHash: + '0x3a494e672717f9b1f2b64a48a19985842d82d0747400fccebebc7a4e99c8eaab', + amount: '4926701727965948', + token: { + address: '0x0000000000000000000000000000000000000000', + chainId: 42161, + symbol: 'ETH', + decimals: 18, + name: 'ETH', + coinKey: 'ETH', + logoURI: + 'https://raw.githubusercontent.com/trustwallet/assets/master/blockchains/ethereum/assets/0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2/logo.png', + priceUSD: '2648.72', + icon: 'https://raw.githubusercontent.com/trustwallet/assets/master/blockchains/ethereum/assets/0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2/logo.png', + }, + }, + }, + STATUS_COMPLETE_INVALID_MISSING_FIELDS: { + status: 'COMPLETE', + isExpectedToken: true, + bridge: 'across', + }, +}; + +describe('validators', () => { + describe('bridgeStatusValidator', () => { + // @ts-expect-error - it.each is a function + it.each([ + { + input: BridgeTxStatusResponses.STATUS_PENDING_VALID, + expected: true, + description: 'valid pending bridge status', + }, + { + input: BridgeTxStatusResponses.STATUS_PENDING_VALID_MISSING_FIELDS, + expected: true, + description: 'valid pending bridge status missing fields', + }, + { + input: BridgeTxStatusResponses.STATUS_PENDING_VALID_MISSING_FIELDS_2, + expected: true, + description: 'valid pending bridge status missing fields 2', + }, + { + input: BridgeTxStatusResponses.STATUS_PENDING_INVALID_MISSING_FIELDS, + expected: false, + description: 'pending bridge status with missing fields', + }, + { + input: BridgeTxStatusResponses.STATUS_COMPLETE_VALID, + expected: true, + description: 'valid complete bridge status', + }, + { + input: BridgeTxStatusResponses.STATUS_COMPLETE_INVALID_MISSING_FIELDS, + expected: false, + description: 'complete bridge status with missing fields', + }, + { + input: BridgeTxStatusResponses.STATUS_COMPLETE_VALID_MISSING_FIELDS, + expected: true, + description: 'complete bridge status with missing fields', + }, + { + input: undefined, + expected: false, + description: 'undefined', + }, + { + input: null, + expected: false, + description: 'null', + }, + { + input: {}, + expected: false, + description: 'empty object', + }, + ])( + 'should return $expected for $description', + ({ input, expected }: { input: unknown; expected: boolean }) => { + const res = validateResponse( + validators, + input, + 'dummyurl.com', + ); + expect(res).toBe(expected); + }, + ); + }); +}); diff --git a/app/scripts/controllers/bridge-status/validators.ts b/app/scripts/controllers/bridge-status/validators.ts new file mode 100644 index 000000000000..69e788025b01 --- /dev/null +++ b/app/scripts/controllers/bridge-status/validators.ts @@ -0,0 +1,179 @@ +import { validHex, validateData } from '../../../../shared/lib/swaps-utils'; +import { isValidHexAddress } from '../../../../shared/modules/hexstring-utils'; +import { + BridgeId, + DestChainStatus, + SrcChainStatus, + Asset, + StatusTypes, +} from '../../../../shared/types/bridge-status'; +import { BRIDGE_STATUS_BASE_URL } from './utils'; + +type Validator = { + property: keyof ExpectedResponse | string; + type: string; + validator: (value: DataToValidate) => boolean; +}; + +export const validateResponse = ( + validators: Validator[], + data: unknown, + urlUsed: string, +): data is ExpectedResponse => { + if (data === null || data === undefined) { + return false; + } + return validateData(validators, data, urlUsed); +}; + +const assetValidators = [ + { + property: 'chainId', + type: 'number', + validator: (v: unknown): v is number => typeof v === 'number', + }, + { + property: 'address', + type: 'string', + validator: (v: unknown): v is string => isValidHexAddress(v as string), + }, + { + property: 'symbol', + type: 'string', + validator: (v: unknown): v is string => typeof v === 'string', + }, + { + property: 'name', + type: 'string', + validator: (v: unknown): v is string => typeof v === 'string', + }, + { + property: 'decimals', + type: 'number', + validator: (v: unknown): v is number => typeof v === 'number', + }, + { + property: 'icon', + type: 'string|undefined', + validator: (v: unknown): v is string | undefined => + v === undefined || typeof v === 'string', + }, +]; + +const assetValidator = (v: unknown): v is Asset => + validateResponse(assetValidators, v, BRIDGE_STATUS_BASE_URL); + +const srcChainStatusValidators = [ + { + property: 'chainId', + // For some reason, API returns destChain.chainId as a string, it's a number everywhere else + type: 'number|string', + validator: (v: unknown): v is number | string => + typeof v === 'number' || typeof v === 'string', + }, + { + property: 'txHash', + type: 'string', + validator: validHex, + }, + { + property: 'amount', + type: 'string|undefined', + validator: (v: unknown): v is string | undefined => + v === undefined || typeof v === 'string', + }, + { + property: 'token', + type: 'object|undefined', + validator: (v: unknown): v is object | undefined => + v === undefined || assetValidator(v), + }, +]; + +const srcChainStatusValidator = (v: unknown): v is SrcChainStatus => + validateResponse( + srcChainStatusValidators, + v, + BRIDGE_STATUS_BASE_URL, + ); + +const destChainStatusValidators = [ + { + property: 'chainId', + // For some reason, API returns destChain.chainId as a string, it's a number everywhere else + type: 'number|string', + validator: (v: unknown): v is number | string => + typeof v === 'number' || typeof v === 'string', + }, + { + property: 'amount', + type: 'string|undefined', + validator: (v: unknown): v is string | undefined => + v === undefined || typeof v === 'string', + }, + { + property: 'txHash', + type: 'string|undefined', + validator: (v: unknown): v is string | undefined => + v === undefined || typeof v === 'string', + }, + { + property: 'token', + type: 'object|undefined', + validator: (v: unknown): v is Asset | undefined => + v === undefined || + (v && typeof v === 'object' && Object.keys(v).length === 0) || + assetValidator(v), + }, +]; + +const destChainStatusValidator = (v: unknown): v is DestChainStatus => + validateResponse( + destChainStatusValidators, + v, + BRIDGE_STATUS_BASE_URL, + ); + +export const validators = [ + { + property: 'status', + type: 'string', + validator: (v: unknown): v is StatusTypes => + Object.values(StatusTypes).includes(v as StatusTypes), + }, + { + property: 'srcChain', + type: 'object', + validator: srcChainStatusValidator, + }, + { + property: 'destChain', + type: 'object|undefined', + validator: (v: unknown): v is object | unknown => + v === undefined || destChainStatusValidator(v), + }, + { + property: 'bridge', + type: 'string|undefined', + validator: (v: unknown): v is BridgeId | undefined => + v === undefined || Object.values(BridgeId).includes(v as BridgeId), + }, + { + property: 'isExpectedToken', + type: 'boolean|undefined', + validator: (v: unknown): v is boolean | undefined => + v === undefined || typeof v === 'boolean', + }, + { + property: 'isUnrecognizedRouterAddress', + type: 'boolean|undefined', + validator: (v: unknown): v is boolean | undefined => + v === undefined || typeof v === 'boolean', + }, + // TODO: add refuel validator + // { + // property: 'refuel', + // type: 'object', + // validator: (v: unknown) => Object.values(RefuelStatusResponse).includes(v), + // }, +]; diff --git a/app/scripts/metamask-controller.js b/app/scripts/metamask-controller.js index ac633bbc8e5d..d208d072050b 100644 --- a/app/scripts/metamask-controller.js +++ b/app/scripts/metamask-controller.js @@ -246,6 +246,7 @@ import { getProviderConfig } from '../../ui/ducks/metamask/metamask'; import { endTrace, trace } from '../../shared/lib/trace'; // eslint-disable-next-line import/no-restricted-paths import { isSnapId } from '../../ui/helpers/utils/snaps'; +import { BridgeStatusAction } from '../../shared/types/bridge-status'; import { BalancesController as MultichainBalancesController } from './lib/accounts/BalancesController'; import { ///: BEGIN:ONLY_INCLUDE_IF(build-mmi) @@ -374,6 +375,8 @@ import { import createTracingMiddleware from './lib/createTracingMiddleware'; import { PatchStore } from './lib/PatchStore'; import { sanitizeUIState } from './lib/state-utils'; +import BridgeStatusController from './controllers/bridge-status/bridge-status-controller'; +import { BRIDGE_STATUS_CONTROLLER_NAME } from './controllers/bridge-status/constants'; export const METAMASK_CONTROLLER_EVENTS = { // Fired after state changes that impact the extension badge (unapproved msg count) @@ -2182,6 +2185,22 @@ export default class MetamaskController extends EventEmitter { messenger: bridgeControllerMessenger, }); + const bridgeStatusControllerMessenger = + this.controllerMessenger.getRestricted({ + name: BRIDGE_STATUS_CONTROLLER_NAME, + allowedActions: [ + 'AccountsController:getSelectedAccount', + 'NetworkController:getNetworkClientById', + 'NetworkController:findNetworkClientIdByChainId', + 'NetworkController:getState', + ], + allowedEvents: [], + }); + this.bridgeStatusController = new BridgeStatusController({ + messenger: bridgeStatusControllerMessenger, + state: initState.BridgeStatusController, + }); + const smartTransactionsControllerMessenger = this.controllerMessenger.getRestricted({ name: 'SmartTransactionsController', @@ -2421,6 +2440,7 @@ export default class MetamaskController extends EventEmitter { SignatureController: this.signatureController, SwapsController: this.swapsController, BridgeController: this.bridgeController, + BridgeStatusController: this.bridgeStatusController, EnsController: this.ensController, ApprovalController: this.approvalController, PPOMController: this.ppomController, @@ -4004,6 +4024,13 @@ export default class MetamaskController extends EventEmitter { `${BRIDGE_CONTROLLER_NAME}:${BridgeUserAction.UPDATE_QUOTE_PARAMS}`, ), + // Bridge Status + [BridgeStatusAction.START_POLLING_FOR_BRIDGE_TX_STATUS]: + this.controllerMessenger.call.bind( + this.controllerMessenger, + `${BRIDGE_STATUS_CONTROLLER_NAME}:${BridgeStatusAction.START_POLLING_FOR_BRIDGE_TX_STATUS}`, + ), + // Smart Transactions fetchSmartTransactionFees: smartTransactionsController.getFees.bind( smartTransactionsController, @@ -4994,6 +5021,10 @@ export default class MetamaskController extends EventEmitter { address: selectedAddress, ignoreNetwork: false, }); + this.bridgeStatusController.wipeBridgeStatus({ + address: selectedAddress, + ignoreNetwork: false, + }); this.networkController.resetConnection(); return selectedAddress; diff --git a/shared/constants/transaction.ts b/shared/constants/transaction.ts index 24b89140f941..38311de3be76 100644 --- a/shared/constants/transaction.ts +++ b/shared/constants/transaction.ts @@ -113,6 +113,11 @@ export enum TransactionGroupCategory { * Transaction group representing a token swap through MetaMask Swaps, where the final token is sent to another address. */ swapAndSend = 'swapAndSend', + /** + * Transaction group representing a token bridge through MetaMask Bridge, + * where the final token is sent to another chain. + */ + bridge = 'bridge', } /** diff --git a/shared/modules/conversion.utils.ts b/shared/modules/conversion.utils.ts index 75da336eb8e6..9e24a8427cce 100644 --- a/shared/modules/conversion.utils.ts +++ b/shared/modules/conversion.utils.ts @@ -184,6 +184,12 @@ export function decimalToHex(decimal: number | string | BigNumber | BN) { return new Numeric(decimal, 10).toBase(16).toString(); } +export function decimalToPrefixedHex( + decimal: number | string | BigNumber | BN, +): Hex { + return new Numeric(decimal, 10).toPrefixedHexString() as Hex; +} + export function hexToDecimal(hexValue: number | string | BigNumber | BN) { return new Numeric(hexValue, 16).toBase(10).toString(); } diff --git a/shared/types/bridge-status.ts b/shared/types/bridge-status.ts new file mode 100644 index 000000000000..601a2209aaf9 --- /dev/null +++ b/shared/types/bridge-status.ts @@ -0,0 +1,146 @@ +// eslint-disable-next-line import/no-restricted-paths +import { ChainId, Quote, QuoteResponse } from '../../ui/pages/bridge/types'; + +// All fields need to be types not interfaces, same with their children fields +// o/w you get a type error + +export enum StatusTypes { + UNKNOWN = 'UNKNOWN', + FAILED = 'FAILED', + PENDING = 'PENDING', + COMPLETE = 'COMPLETE', +} + +export type StatusRequest = { + bridgeId: string; // lifi, socket, squid + srcTxHash: string; // lifi, socket, squid + bridge: string; // lifi, socket, squid + srcChainId: ChainId; // lifi, socket, squid + destChainId: ChainId; // lifi, socket, squid + quote?: Quote; // squid + refuel?: boolean; // lifi +}; + +export type Asset = { + chainId: ChainId; + address: string; + symbol: string; + name: string; + decimals: number; + icon?: string; +}; + +export type SrcChainStatus = { + chainId: ChainId; + txHash: string; + amount?: string; + token?: Asset; +}; + +export type DestChainStatus = { + chainId: ChainId; + txHash?: string; + amount?: string; + token?: Record | Asset; +}; + +export enum BridgeId { + HOP = 'hop', + CELER = 'celer', + CELERCIRCLE = 'celercircle', + CONNEXT = 'connext', + POLYGON = 'polygon', + AVALANCHE = 'avalanche', + MULTICHAIN = 'multichain', + AXELAR = 'axelar', + ACROSS = 'across', + STARGATE = 'stargate', +} + +export enum FeeType { + METABRIDGE = 'metabridge', + REFUEL = 'refuel', +} + +export type FeeData = { + amount: string; + asset: Asset; +}; + +export type Protocol = { + displayName?: string; + icon?: string; + name?: string; // for legacy quotes +}; + +export enum ActionTypes { + BRIDGE = 'bridge', + SWAP = 'swap', + REFUEL = 'refuel', +} + +export type Step = { + action: ActionTypes; + srcChainId: ChainId; + destChainId?: ChainId; + srcAsset: Asset; + destAsset: Asset; + srcAmount: string; + destAmount: string; + protocol: Protocol; +}; + +export type StatusResponse = { + status: StatusTypes; + srcChain: SrcChainStatus; + destChain?: DestChainStatus; + bridge?: BridgeId; + isExpectedToken?: boolean; + isUnrecognizedRouterAddress?: boolean; + refuel?: RefuelStatusResponse; +}; + +export type RefuelStatusResponse = object & StatusResponse; + +export type RefuelData = object & Step; + +export type BridgeHistoryItem = { + quote: Quote; + status: StatusResponse; + startTime?: number; + estimatedProcessingTimeInSeconds: number; + slippagePercentage: number; + completionTime?: number; + pricingData?: { + quotedGasInUsd: number; + quotedReturnInUsd: number; + amountSentInUsd: number; + quotedRefuelSrcAmountInUsd?: number; + quotedRefuelDestAmountInUsd?: number; + }; + initialDestAssetBalance?: number; + targetContractAddress?: string; + account: string; +}; + +export enum BridgeStatusAction { + START_POLLING_FOR_BRIDGE_TX_STATUS = 'startPollingForBridgeTxStatus', + WIPE_BRIDGE_STATUS = 'wipeBridgeStatus', + GET_STATE = 'getState', +} + +export type StartPollingForBridgeTxStatusArgs = { + statusRequest: StatusRequest; + quoteResponse: QuoteResponse; + startTime?: BridgeHistoryItem['startTime']; + slippagePercentage: BridgeHistoryItem['slippagePercentage']; + pricingData?: BridgeHistoryItem['pricingData']; + initialDestAssetBalance?: BridgeHistoryItem['initialDestAssetBalance']; + targetContractAddress?: BridgeHistoryItem['targetContractAddress']; +}; + +export type SourceChainTxHash = string; + +export type BridgeStatusControllerState = { + txHistory: Record; +}; diff --git a/test/data/mock-state.json b/test/data/mock-state.json index d2b66cee3108..d826c25f1340 100644 --- a/test/data/mock-state.json +++ b/test/data/mock-state.json @@ -2031,6 +2031,9 @@ } } } + }, + "bridgeStatusState": { + "txHistory": {} } }, "ramps": { diff --git a/test/e2e/tests/metrics/state-snapshots/errors-after-init-opt-in-background-state.json b/test/e2e/tests/metrics/state-snapshots/errors-after-init-opt-in-background-state.json index 6252c91e73a2..7f09cc42a463 100644 --- a/test/e2e/tests/metrics/state-snapshots/errors-after-init-opt-in-background-state.json +++ b/test/e2e/tests/metrics/state-snapshots/errors-after-init-opt-in-background-state.json @@ -67,38 +67,39 @@ "srcNetworkAllowlist": { "0": "string", "1": "string", "2": "string" }, "destNetworkAllowlist": { "0": "string", "1": "string", "2": "string" } }, + "srcTokens": {}, + "srcTopAssets": {}, "destTokens": {}, "destTopAssets": {}, "quoteRequest": { - "slippage": 0.5, - "srcTokenAddress": "0x0000000000000000000000000000000000000000" + "srcTokenAddress": "0x0000000000000000000000000000000000000000", + "slippage": 0.5 }, "quotes": {}, - "quotesRefreshCount": 0, - "srcTokens": {}, - "srcTopAssets": {} + "quotesRefreshCount": 0 } }, + "BridgeStatusController": { "bridgeStatusState": { "txHistory": "object" } }, "CronjobController": { "jobs": "object" }, "CurrencyController": { + "currentCurrency": "usd", "currencyRates": { "ETH": { "conversionDate": "number", "conversionRate": 1700, "usdConversionRate": 1700 }, - "LineaETH": { + "SepoliaETH": { "conversionDate": "number", "conversionRate": 1700, "usdConversionRate": 1700 }, - "SepoliaETH": { + "LineaETH": { "conversionDate": "number", "conversionRate": 1700, "usdConversionRate": 1700 } - }, - "currentCurrency": "usd" + } }, "DecryptMessageController": { "unapprovedDecryptMsgs": "object", diff --git a/test/e2e/tests/metrics/state-snapshots/errors-after-init-opt-in-ui-state.json b/test/e2e/tests/metrics/state-snapshots/errors-after-init-opt-in-ui-state.json index 3bc7057435c8..8c3af692abdf 100644 --- a/test/e2e/tests/metrics/state-snapshots/errors-after-init-opt-in-ui-state.json +++ b/test/e2e/tests/metrics/state-snapshots/errors-after-init-opt-in-ui-state.json @@ -51,7 +51,6 @@ "completedOnboarding": true, "knownMethodData": "object", "use4ByteResolution": true, - "showIncomingTransactions": "object", "participateInMetaMetrics": true, "dataCollectionForMarketing": "boolean", "nextNonce": null, @@ -61,12 +60,12 @@ "conversionRate": 1700, "usdConversionRate": 1700 }, - "LineaETH": { + "SepoliaETH": { "conversionDate": "number", "conversionRate": 1700, "usdConversionRate": 1700 }, - "SepoliaETH": { + "LineaETH": { "conversionDate": "number", "conversionRate": 1700, "usdConversionRate": 1700 @@ -141,7 +140,6 @@ "forgottenPassword": false, "ipfsGateway": "string", "isIpfsGatewayEnabled": "boolean", - "isMultiAccountBalancesEnabled": "boolean", "useAddressBarEnsResolution": true, "ledgerTransportType": "webhid", "snapRegistryList": "object", @@ -151,6 +149,8 @@ "useTransactionSimulations": true, "enableMV3TimestampSave": true, "useExternalServices": "boolean", + "isMultiAccountBalancesEnabled": "boolean", + "showIncomingTransactions": "object", "metaMetricsId": "fake-metrics-id", "marketingCampaignCookieId": null, "eventsBeforeMetricsOptIn": "object", @@ -243,11 +243,11 @@ "accounts": "object", "accountsByChainId": "object", "marketData": "object", - "signatureRequests": "object", "unapprovedDecryptMsgs": "object", "unapprovedDecryptMsgCount": 0, "unapprovedEncryptionPublicKeyMsgs": "object", "unapprovedEncryptionPublicKeyMsgCount": 0, + "signatureRequests": "object", "unapprovedPersonalMsgs": "object", "unapprovedTypedMessages": "object", "unapprovedPersonalMsgCount": 0, @@ -287,17 +287,18 @@ "srcNetworkAllowlist": { "0": "string", "1": "string", "2": "string" }, "destNetworkAllowlist": { "0": "string", "1": "string", "2": "string" } }, + "srcTokens": {}, + "srcTopAssets": {}, "destTokens": {}, "destTopAssets": {}, "quoteRequest": { - "slippage": 0.5, - "srcTokenAddress": "0x0000000000000000000000000000000000000000" + "srcTokenAddress": "0x0000000000000000000000000000000000000000", + "slippage": 0.5 }, "quotes": {}, - "quotesRefreshCount": 0, - "srcTokens": {}, - "srcTopAssets": {} + "quotesRefreshCount": 0 }, + "bridgeStatusState": { "txHistory": "object" }, "ensEntries": "object", "ensResolutionsByAddress": "object", "pendingApprovals": "object", diff --git a/test/jest/mock-store.js b/test/jest/mock-store.js index 4720bf427372..e705d4a3e89d 100644 --- a/test/jest/mock-store.js +++ b/test/jest/mock-store.js @@ -4,6 +4,7 @@ import { KeyringType } from '../../shared/constants/keyring'; import { ETH_EOA_METHODS } from '../../shared/constants/eth-methods'; import { mockNetworkState } from '../stub/networks'; import { DEFAULT_BRIDGE_CONTROLLER_STATE } from '../../app/scripts/controllers/bridge/constants'; +import { DEFAULT_BRIDGE_STATUS_CONTROLLER_STATE } from '../../app/scripts/controllers/bridge-status/constants'; export const createGetSmartTransactionFeesApiResponse = () => { return { @@ -701,10 +702,19 @@ export const createSwapsMockStore = () => { }; export const createBridgeMockStore = ( - featureFlagOverrides = {}, - bridgeSliceOverrides = {}, - bridgeStateOverrides = {}, - metamaskStateOverrides = {}, + { + featureFlagOverrides = {}, + bridgeSliceOverrides = {}, + bridgeStateOverrides = {}, + bridgeStatusStateOverrides = {}, + metamaskStateOverrides = {}, + } = { + featureFlagOverrides: {}, + bridgeSliceOverrides: {}, + bridgeStateOverrides: {}, + bridgeStatusStateOverrides: {}, + metamaskStateOverrides: {}, + }, ) => { const swapsStore = createSwapsMockStore(); return { @@ -732,6 +742,10 @@ export const createBridgeMockStore = ( quoteRequest: DEFAULT_BRIDGE_CONTROLLER_STATE.quoteRequest, ...bridgeStateOverrides, }, + bridgeStatusState: { + ...DEFAULT_BRIDGE_STATUS_CONTROLLER_STATE, + ...bridgeStatusStateOverrides, + }, }, }; }; diff --git a/ui/components/app/selected-account/selected-account-component.test.js b/ui/components/app/selected-account/selected-account-component.test.js index 9545580bebbb..6440eddebb09 100644 --- a/ui/components/app/selected-account/selected-account-component.test.js +++ b/ui/components/app/selected-account/selected-account-component.test.js @@ -49,6 +49,7 @@ jest.mock('../../../selectors', () => { const mockGetSelectedAccount = jest.fn(() => mockSelectedAccount); return { + getSelectedAddress: jest.fn(() => '0xselectedaddress'), getAccountType: mockGetAccountType, getSelectedInternalAccount: mockGetSelectedAccount, getCurrentChainId: jest.fn(() => '0x5'), diff --git a/ui/components/app/transaction-breakdown/transaction-breakdown-utils.ts b/ui/components/app/transaction-breakdown/transaction-breakdown-utils.ts new file mode 100644 index 000000000000..dc40f23f4a92 --- /dev/null +++ b/ui/components/app/transaction-breakdown/transaction-breakdown-utils.ts @@ -0,0 +1,140 @@ +import { + TransactionMeta, + TransactionType, +} from '@metamask/transaction-controller'; +import { getShouldShowFiat } from '../../../selectors'; +import { getNativeCurrency } from '../../../ducks/metamask/metamask'; +import { getHexGasTotal } from '../../../helpers/utils/confirm-tx.util'; +import { isEIP1559Transaction } from '../../../../shared/modules/transaction.utils'; + +import { + subtractHexes, + sumHexes, +} from '../../../../shared/modules/conversion.utils'; +import { + calcTokenAmount, + getSwapsTokensReceivedFromTxMeta, +} from '../../../../shared/lib/transactions-controller-utils'; +import { CONFIRMED_STATUS } from '../transaction-activity-log/transaction-activity-log.constants'; +import { MetaMaskReduxState } from '../../../store/store'; + +export const getTransactionBreakdownData = ({ + state, + transaction, + isTokenApprove, +}: { + state: MetaMaskReduxState; + transaction: TransactionMeta; + isTokenApprove: boolean; +}) => { + const { + txParams: { gas, gasPrice, maxFeePerGas, value } = {}, + txReceipt: { gasUsed, effectiveGasPrice, l1Fee: l1HexGasTotal } = {}, + baseFeePerGas, + sourceTokenAmount: rawSourceTokenAmount, + sourceTokenDecimals, + sourceTokenSymbol, + destinationTokenAddress, + destinationTokenAmount: rawDestinationTokenAmountEstimate, + destinationTokenDecimals, + destinationTokenSymbol, + status, + type, + } = transaction; + + const sourceTokenAmount = + rawSourceTokenAmount && sourceTokenDecimals + ? calcTokenAmount(rawSourceTokenAmount, sourceTokenDecimals).toFixed() + : undefined; + let destinationTokenAmount; + + if ( + type === TransactionType.swapAndSend && + // ensure fallback values are available + rawDestinationTokenAmountEstimate && + destinationTokenDecimals && + destinationTokenSymbol + ) { + try { + // try to get the actual destination token amount from the on-chain events + destinationTokenAmount = getSwapsTokensReceivedFromTxMeta( + destinationTokenSymbol, + transaction, + destinationTokenAddress, + undefined, + destinationTokenDecimals, + undefined, + undefined, + // @ts-expect-error TODO: fix this, ported directly from original code + null, + ); + + // if no amount is found, throw + if (!destinationTokenAmount) { + throw new Error('Actual destination token amount not found'); + } + } catch (error) { + // if actual destination token amount is not found, use the estimated amount from the quote + destinationTokenAmount = + rawDestinationTokenAmountEstimate && destinationTokenDecimals + ? calcTokenAmount( + rawDestinationTokenAmountEstimate, + destinationTokenDecimals, + ).toFixed() + : undefined; + } + } + + const sourceAmountFormatted = + sourceTokenAmount && sourceTokenDecimals && sourceTokenSymbol + ? `${sourceTokenAmount} ${sourceTokenSymbol}` + : undefined; + const destinationAmountFormatted = + destinationTokenAmount && status === CONFIRMED_STATUS + ? `${destinationTokenAmount} ${destinationTokenSymbol}` + : undefined; + + const gasLimit = typeof gasUsed === 'string' ? gasUsed : gas; + + const priorityFee = + effectiveGasPrice && + baseFeePerGas && + subtractHexes(effectiveGasPrice, baseFeePerGas); + + // To calculate the total cost of the transaction, we use gasPrice if it is in the txParam, + // which will only be the case on non-EIP1559 networks. If it is not in the params, we can + // use the effectiveGasPrice from the receipt, which will ultimately represent to true cost + // of the transaction. Either of these are used the same way with gasLimit to calculate total + // cost. effectiveGasPrice will be available on the txReciept for all EIP1559 networks + const usedGasPrice = gasPrice || effectiveGasPrice; + const hexGasTotal = + (gasLimit && + usedGasPrice && + getHexGasTotal({ gasLimit, gasPrice: usedGasPrice })) || + '0x0'; + + const totalInHex = sumHexes( + hexGasTotal, + // @ts-expect-error TODO: fix this, ported directly from original code + value, + l1HexGasTotal ?? 0, + ); + + return { + nativeCurrency: getNativeCurrency(state), + showFiat: getShouldShowFiat(state), + totalInHex, + gas, + gasPrice, + maxFeePerGas, + gasUsed, + isTokenApprove, + hexGasTotal, + priorityFee, + baseFee: baseFeePerGas, + isEIP1559Transaction: isEIP1559Transaction(transaction), + l1HexGasTotal, + sourceAmountFormatted, + destinationAmountFormatted, + }; +}; diff --git a/ui/components/app/transaction-breakdown/transaction-breakdown.container.js b/ui/components/app/transaction-breakdown/transaction-breakdown.container.js index 5fdb37be3493..cef2e1b03712 100644 --- a/ui/components/app/transaction-breakdown/transaction-breakdown.container.js +++ b/ui/components/app/transaction-breakdown/transaction-breakdown.container.js @@ -1,127 +1,15 @@ import { connect } from 'react-redux'; -import { TransactionType } from '@metamask/transaction-controller'; -import { getShouldShowFiat } from '../../../selectors'; -import { getNativeCurrency } from '../../../ducks/metamask/metamask'; -import { getHexGasTotal } from '../../../helpers/utils/confirm-tx.util'; -import { isEIP1559Transaction } from '../../../../shared/modules/transaction.utils'; - -import { - subtractHexes, - sumHexes, -} from '../../../../shared/modules/conversion.utils'; -import { - calcTokenAmount, - getSwapsTokensReceivedFromTxMeta, -} from '../../../../shared/lib/transactions-controller-utils'; -import { CONFIRMED_STATUS } from '../transaction-activity-log/transaction-activity-log.constants'; import TransactionBreakdown from './transaction-breakdown.component'; +import { getTransactionBreakdownData } from './transaction-breakdown-utils'; const mapStateToProps = (state, ownProps) => { const { transaction, isTokenApprove } = ownProps; - const { - txParams: { gas, gasPrice, maxFeePerGas, value } = {}, - txReceipt: { gasUsed, effectiveGasPrice, l1Fee: l1HexGasTotal } = {}, - baseFeePerGas, - sourceTokenAmount: rawSourceTokenAmount, - sourceTokenDecimals, - sourceTokenSymbol, - destinationTokenAddress, - destinationTokenAmount: rawDestinationTokenAmountEstimate, - destinationTokenDecimals, - destinationTokenSymbol, - status, - type, - } = transaction; - - const sourceTokenAmount = - rawSourceTokenAmount && sourceTokenDecimals - ? calcTokenAmount(rawSourceTokenAmount, sourceTokenDecimals).toFixed() - : undefined; - let destinationTokenAmount; - - if ( - type === TransactionType.swapAndSend && - // ensure fallback values are available - rawDestinationTokenAmountEstimate && - destinationTokenDecimals && - destinationTokenSymbol - ) { - try { - // try to get the actual destination token amount from the on-chain events - destinationTokenAmount = getSwapsTokensReceivedFromTxMeta( - destinationTokenSymbol, - transaction, - destinationTokenAddress, - undefined, - destinationTokenDecimals, - undefined, - undefined, - null, - ); - - // if no amount is found, throw - if (!destinationTokenAmount) { - throw new Error('Actual destination token amount not found'); - } - } catch (error) { - // if actual destination token amount is not found, use the estimated amount from the quote - destinationTokenAmount = - rawDestinationTokenAmountEstimate && destinationTokenDecimals - ? calcTokenAmount( - rawDestinationTokenAmountEstimate, - destinationTokenDecimals, - ).toFixed() - : undefined; - } - } - - const sourceAmountFormatted = - sourceTokenAmount && sourceTokenDecimals && sourceTokenSymbol - ? `${sourceTokenAmount} ${sourceTokenSymbol}` - : undefined; - const destinationAmountFormatted = - destinationTokenAmount && status === CONFIRMED_STATUS - ? `${destinationTokenAmount} ${destinationTokenSymbol}` - : undefined; - - const gasLimit = typeof gasUsed === 'string' ? gasUsed : gas; - - const priorityFee = - effectiveGasPrice && - baseFeePerGas && - subtractHexes(effectiveGasPrice, baseFeePerGas); - - // To calculate the total cost of the transaction, we use gasPrice if it is in the txParam, - // which will only be the case on non-EIP1559 networks. If it is not in the params, we can - // use the effectiveGasPrice from the receipt, which will ultimately represent to true cost - // of the transaction. Either of these are used the same way with gasLimit to calculate total - // cost. effectiveGasPrice will be available on the txReciept for all EIP1559 networks - const usedGasPrice = gasPrice || effectiveGasPrice; - const hexGasTotal = - (gasLimit && - usedGasPrice && - getHexGasTotal({ gasLimit, gasPrice: usedGasPrice })) || - '0x0'; - - const totalInHex = sumHexes(hexGasTotal, value, l1HexGasTotal ?? 0); - - return { - nativeCurrency: getNativeCurrency(state), - showFiat: getShouldShowFiat(state), - totalInHex, - gas, - gasPrice, - maxFeePerGas, - gasUsed, + const data = getTransactionBreakdownData({ + state, + transaction, isTokenApprove, - hexGasTotal, - priorityFee, - baseFee: baseFeePerGas, - isEIP1559Transaction: isEIP1559Transaction(transaction), - l1HexGasTotal, - sourceAmountFormatted, - destinationAmountFormatted, - }; + }); + return data; }; export default connect(mapStateToProps)(TransactionBreakdown); diff --git a/ui/components/app/transaction-icon/transaction-icon.js b/ui/components/app/transaction-icon/transaction-icon.js index 2905f0090b1b..6051862aae26 100644 --- a/ui/components/app/transaction-icon/transaction-icon.js +++ b/ui/components/app/transaction-icon/transaction-icon.js @@ -21,6 +21,7 @@ const ICON_MAP = { [TransactionGroupCategory.signatureRequest]: IconName.SecurityTick, [TransactionGroupCategory.swap]: IconName.SwapHorizontal, [TransactionGroupCategory.swapAndSend]: IconName.Arrow2UpRight, + [TransactionGroupCategory.bridge]: IconName.Bridge, }; const COLOR_MAP = { @@ -97,5 +98,6 @@ TransactionIcon.propTypes = { TransactionGroupCategory.signatureRequest, TransactionGroupCategory.swap, TransactionGroupCategory.swapAndSend, + TransactionGroupCategory.bridge, ]).isRequired, }; diff --git a/ui/components/app/transaction-list-item/transaction-list-item.component.js b/ui/components/app/transaction-list-item/transaction-list-item.component.js index 9354a0d9e9ff..060bc8158e92 100644 --- a/ui/components/app/transaction-list-item/transaction-list-item.component.js +++ b/ui/components/app/transaction-list-item/transaction-list-item.component.js @@ -5,7 +5,10 @@ import classnames from 'classnames'; import { useHistory } from 'react-router-dom'; import { useDispatch, useSelector } from 'react-redux'; -import { TransactionStatus } from '@metamask/transaction-controller'; +import { + TransactionStatus, + TransactionType, +} from '@metamask/transaction-controller'; import { useTransactionDisplayData } from '../../../hooks/useTransactionDisplayData'; import { useI18nContext } from '../../../hooks/useI18nContext'; @@ -66,6 +69,8 @@ import { MetaMetricsContext } from '../../../contexts/metametrics'; import { ActivityListItem } from '../../multichain'; import { abortTransactionSigning } from '../../../store/actions'; import { getIsSmartTransaction } from '../../../../shared/modules/selectors'; +import useBridgeTxHistoryData from '../../../hooks/bridge/useBridgeTxHistoryData'; +import BridgeActivityItemTxSegments from '../../../pages/bridge/transaction-details/bridge-activity-item-tx-segments'; function TransactionListItemInner({ transactionGroup, @@ -85,6 +90,18 @@ function TransactionListItemInner({ const isSmartTransaction = useSelector(getIsSmartTransaction); const dispatch = useDispatch(); + // Bridge transactions + const isBridgeTx = + transactionGroup.initialTransaction.type === TransactionType.bridge; + const { + bridgeTitleSuffix, + bridgeTxHistoryItem, + isBridgeComplete, + showBridgeTxDetails, + } = useBridgeTxHistoryData({ + transactionGroup, + }); + const { initialTransaction: { id }, primaryTransaction: { error, status }, @@ -286,9 +303,13 @@ function TransactionListItemInner({ <> + isBridgeTx && isBridgeComplete === false ? ( + + ) : ( + + ) } rightContent={ !isSignatureReq && diff --git a/ui/components/app/transaction-list/transaction-list.component.js b/ui/components/app/transaction-list/transaction-list.component.js index 2fd7d0bc6d1a..fbd18bd337d9 100644 --- a/ui/components/app/transaction-list/transaction-list.component.js +++ b/ui/components/app/transaction-list/transaction-list.component.js @@ -150,6 +150,7 @@ export default function TransactionList({ const unfilteredCompletedTransactions = useSelector( nonceSortedCompletedTransactionsSelector, ); + const chainId = useSelector(getCurrentChainId); const selectedAccount = useSelector(getSelectedAccount); diff --git a/ui/components/multichain/activity-list-item/__snapshots__/activity-list-item.test.js.snap b/ui/components/multichain/activity-list-item/__snapshots__/activity-list-item.test.js.snap index 3bce90a09f76..8f0c24760246 100644 --- a/ui/components/multichain/activity-list-item/__snapshots__/activity-list-item.test.js.snap +++ b/ui/components/multichain/activity-list-item/__snapshots__/activity-list-item.test.js.snap @@ -10,10 +10,10 @@ exports[`ActivityListItem should match snapshot with no props 1`] = ` class="mm-box mm-box--display-flex mm-box--gap-4 mm-box--flex-direction-row mm-box--width-full" >

Content rendered to the right diff --git a/ui/components/multichain/activity-list-item/activity-list-item.js b/ui/components/multichain/activity-list-item/activity-list-item.js index 8fd66714574a..5160f9076ad6 100644 --- a/ui/components/multichain/activity-list-item/activity-list-item.js +++ b/ui/components/multichain/activity-list-item/activity-list-item.js @@ -9,6 +9,7 @@ import { FlexDirection, FlexWrap, FontWeight, + JustifyContent, TextAlign, TextColor, TextVariant, @@ -69,11 +70,11 @@ export const ActivityListItem = ({ @@ -113,7 +114,6 @@ export const ActivityListItem = ({ {rightContent && ( ( + bridgeAction: BridgeStatusAction, + args?: T, +) => { + return async (dispatch: MetaMaskReduxDispatch) => { + await submitRequestToBackground(bridgeAction, args); + await forceUpdateMetamaskState(dispatch); + }; +}; + +export const startPollingForBridgeTxStatus = ( + startPollingForBridgeTxStatusArgs: StartPollingForBridgeTxStatusArgs, +) => { + return async (dispatch: MetaMaskReduxDispatch) => { + return dispatch( + callBridgeStatusControllerMethod<[StartPollingForBridgeTxStatusArgs]>( + BridgeStatusAction.START_POLLING_FOR_BRIDGE_TX_STATUS, + [startPollingForBridgeTxStatusArgs], + ), + ); + }; +}; diff --git a/ui/ducks/bridge-status/selectors.ts b/ui/ducks/bridge-status/selectors.ts new file mode 100644 index 000000000000..88c33e10b2ad --- /dev/null +++ b/ui/ducks/bridge-status/selectors.ts @@ -0,0 +1,63 @@ +import { createSelector } from 'reselect'; +import { Hex } from '@metamask/utils'; +import { + BridgeStatusControllerState, + BridgeHistoryItem, +} from '../../../shared/types/bridge-status'; +import { getCurrentChainId, getSelectedAddress } from '../../selectors'; +import { Numeric } from '../../../shared/modules/Numeric'; + +export type BridgeStatusAppState = { + metamask: { + bridgeStatusState: BridgeStatusControllerState; + }; +}; + +export const selectBridgeStatusState = (state: BridgeStatusAppState) => + state.metamask.bridgeStatusState; + +/** + * Returns a mapping of srctxHash to txHistoryItem for the selected address + */ +export const selectBridgeHistoryForAccount = createSelector( + [getSelectedAddress, selectBridgeStatusState], + (selectedAddress, bridgeStatusState) => { + const { txHistory } = bridgeStatusState; + + return Object.keys(txHistory).reduce>( + (acc, txHash) => { + const txHistoryItem = txHistory[txHash]; + if (txHistoryItem.account === selectedAddress) { + acc[txHash] = txHistoryItem; + } + return acc; + }, + {}, + ); + }, +); + +/** + * Returns an array of sorted bridge history items for when the user's current chain is the destination chain for a bridge tx + */ +export const selectIncomingBridgeHistory = createSelector( + [selectBridgeHistoryForAccount, getCurrentChainId], + (bridgeHistory, currentChainId) => { + // Get all history items with dest chain that matches current chain + return Object.values(bridgeHistory) + .filter((bridgeHistoryItem) => { + const hexDestChainId = new Numeric( + bridgeHistoryItem.quote.destChainId, + 10, + ).toPrefixedHexString() as Hex; + + return hexDestChainId === currentChainId; + }) + .sort((a, b) => { + if (a.startTime && b.startTime) { + return b.startTime - a.startTime; + } + return 0; + }); + }, +); diff --git a/ui/ducks/bridge/bridge.test.ts b/ui/ducks/bridge/bridge.test.ts index dc9596fcafba..37db4e336a16 100644 --- a/ui/ducks/bridge/bridge.test.ts +++ b/ui/ducks/bridge/bridge.test.ts @@ -182,7 +182,9 @@ describe('Ducks - Bridge', () => { it('dispatches action to the bridge controller', () => { // eslint-disable-next-line @typescript-eslint/no-explicit-any const mockStore = configureMockStore(middleware)( - createBridgeMockStore({}, { fromTokenInputValue: '10' }), + createBridgeMockStore({ + bridgeSliceOverrides: { fromTokenInputValue: '10' }, + }), ); const state = mockStore.getState().bridge; const mockResetBridgeState = jest.fn(); diff --git a/ui/ducks/bridge/selectors.test.ts b/ui/ducks/bridge/selectors.test.ts index e39f73f2fa15..c009cad63e11 100644 --- a/ui/ducks/bridge/selectors.test.ts +++ b/ui/ducks/bridge/selectors.test.ts @@ -28,12 +28,13 @@ import { describe('Bridge selectors', () => { describe('getFromChain', () => { it('returns the fromChain from the state', () => { - const state = createBridgeMockStore( - { srcNetworkAllowlist: [CHAIN_IDS.ARBITRUM] }, - { toChainId: '0xe708' }, - {}, - { ...mockNetworkState(FEATURED_RPCS[1]) }, - ); + const state = createBridgeMockStore({ + featureFlagOverrides: { srcNetworkAllowlist: [CHAIN_IDS.ARBITRUM] }, + bridgeSliceOverrides: { toChainId: '0xe708' }, + metamaskStateOverrides: { + ...mockNetworkState(FEATURED_RPCS[1]), + }, + }); const result = getFromChain(state as never); expect(result).toStrictEqual({ @@ -56,10 +57,10 @@ describe('Bridge selectors', () => { describe('getToChain', () => { it('returns the toChain from the state', () => { - const state = createBridgeMockStore( - { destNetworkAllowlist: ['0xe708'] }, - { toChainId: '0xe708' }, - ); + const state = createBridgeMockStore({ + featureFlagOverrides: { destNetworkAllowlist: ['0xe708'] }, + bridgeSliceOverrides: { toChainId: '0xe708' }, + }); const result = getToChain(state as never); @@ -83,12 +84,11 @@ describe('Bridge selectors', () => { describe('getAllBridgeableNetworks', () => { it('returns list of ALLOWED_BRIDGE_CHAIN_IDS networks', () => { - const state = createBridgeMockStore( - {}, - {}, - {}, - mockNetworkState(...FEATURED_RPCS), - ); + const state = createBridgeMockStore({ + metamaskStateOverrides: { + ...mockNetworkState(...FEATURED_RPCS), + }, + }); const result = getAllBridgeableNetworks(state as never); expect(result).toHaveLength(8); @@ -149,8 +149,8 @@ describe('Bridge selectors', () => { describe('getFromChains', () => { it('excludes disabled chains from options', () => { - const state = createBridgeMockStore( - { + const state = createBridgeMockStore({ + featureFlagOverrides: { srcNetworkAllowlist: [ CHAIN_IDS.MAINNET, CHAIN_IDS.LINEA_MAINNET, @@ -158,8 +158,8 @@ describe('Bridge selectors', () => { CHAIN_IDS.POLYGON, ], }, - { toChainId: CHAIN_IDS.LINEA_MAINNET }, - ); + bridgeSliceOverrides: { toChainId: CHAIN_IDS.LINEA_MAINNET }, + }); const result = getFromChains(state as never); expect(result).toHaveLength(2); @@ -181,8 +181,8 @@ describe('Bridge selectors', () => { describe('getToChains', () => { it('excludes selected providerConfig and disabled chains from options', () => { - const state = createBridgeMockStore( - { + const state = createBridgeMockStore({ + featureFlagOverrides: { destNetworkAllowlist: [ CHAIN_IDS.ARBITRUM, CHAIN_IDS.LINEA_MAINNET, @@ -190,10 +190,10 @@ describe('Bridge selectors', () => { CHAIN_IDS.POLYGON, ], }, - {}, - {}, - mockNetworkState(...FEATURED_RPCS), - ); + metamaskStateOverrides: { + ...mockNetworkState(...FEATURED_RPCS), + }, + }); const result = getToChains(state as never); expect(result).toHaveLength(3); @@ -218,16 +218,17 @@ describe('Bridge selectors', () => { describe('getIsBridgeTx', () => { it('returns false if bridge is not enabled', () => { - const state = createBridgeMockStore( - { + const state = createBridgeMockStore({ + featureFlagOverrides: { extensionSupport: false, srcNetworkAllowlist: ['0x1'], destNetworkAllowlist: ['0x38'], }, - { toChainId: '0x38' }, - {}, - { ...mockNetworkState({ chainId: '0x1' }), useExternalServices: true }, - ); + bridgeSliceOverrides: { toChainId: '0x38' }, + metamaskStateOverrides: { + ...mockNetworkState({ chainId: '0x1' }), + }, + }); const result = getIsBridgeTx(state as never); @@ -235,16 +236,17 @@ describe('Bridge selectors', () => { }); it('returns false if toChainId is null', () => { - const state = createBridgeMockStore( - { + const state = createBridgeMockStore({ + featureFlagOverrides: { extensionSupport: true, srcNetworkAllowlist: ['0x1'], destNetworkAllowlist: ['0x1'], }, - { toChainId: null }, - {}, - { ...mockNetworkState({ chainId: '0x1' }), useExternalServices: true }, - ); + bridgeSliceOverrides: { toChainId: null }, + metamaskStateOverrides: { + ...mockNetworkState({ chainId: '0x1' }), + }, + }); const result = getIsBridgeTx(state as never); @@ -252,16 +254,17 @@ describe('Bridge selectors', () => { }); it('returns false if fromChain and toChainId have the same chainId', () => { - const state = createBridgeMockStore( - { + const state = createBridgeMockStore({ + featureFlagOverrides: { extensionSupport: true, srcNetworkAllowlist: ['0x1'], destNetworkAllowlist: ['0x1'], }, - { toChainId: '0x1' }, - {}, - { ...mockNetworkState({ chainId: '0x1' }), useExternalServices: true }, - ); + bridgeSliceOverrides: { toChainId: '0x1' }, + metamaskStateOverrides: { + ...mockNetworkState({ chainId: '0x1' }), + }, + }); const result = getIsBridgeTx(state as never); @@ -269,16 +272,17 @@ describe('Bridge selectors', () => { }); it('returns false if useExternalServices is not enabled', () => { - const state = createBridgeMockStore( - { + const state = createBridgeMockStore({ + featureFlagOverrides: { extensionSupport: true, srcNetworkAllowlist: ['0x1'], destNetworkAllowlist: ['0x38'], }, - { toChainId: '0x38' }, - {}, - { ...mockNetworkState({ chainId: '0x1' }), useExternalServices: false }, - ); + bridgeSliceOverrides: { toChainId: '0x38' }, + metamaskStateOverrides: { + ...mockNetworkState({ chainId: '0x1' }), + }, + }); const result = getIsBridgeTx(state as never); @@ -286,15 +290,14 @@ describe('Bridge selectors', () => { }); it('returns true if bridge is enabled and fromChain and toChainId have different chainIds', () => { - const state = createBridgeMockStore( - { + const state = createBridgeMockStore({ + featureFlagOverrides: { extensionSupport: true, srcNetworkAllowlist: ['0x1'], destNetworkAllowlist: ['0x38'], }, - { toChainId: '0x38' }, - {}, - { + bridgeSliceOverrides: { toChainId: '0x38' }, + metamaskStateOverrides: { ...mockNetworkState( ...Object.values(BUILT_IN_NETWORKS), ...FEATURED_RPCS.filter( @@ -303,7 +306,7 @@ describe('Bridge selectors', () => { ), useExternalServices: true, }, - ); + }); const result = getIsBridgeTx(state as never); @@ -313,21 +316,22 @@ describe('Bridge selectors', () => { describe('getFromToken', () => { it('returns fromToken', () => { - const state = createBridgeMockStore( - {}, - - { fromToken: { address: '0x123', symbol: 'TEST' } }, - ); + const state = createBridgeMockStore({ + bridgeSliceOverrides: { + fromToken: { address: '0x123', symbol: 'TEST' }, + }, + }); const result = getFromToken(state as never); expect(result).toStrictEqual({ address: '0x123', symbol: 'TEST' }); }); it('returns defaultToken if fromToken has no address', () => { - const state = createBridgeMockStore( - {}, - { fromToken: { symbol: 'NATIVE' } }, - ); + const state = createBridgeMockStore({ + bridgeSliceOverrides: { + fromToken: { symbol: 'NATIVE' }, + }, + }); const result = getFromToken(state as never); expect(result).toStrictEqual({ @@ -342,7 +346,9 @@ describe('Bridge selectors', () => { }); it('returns defaultToken if fromToken is undefined', () => { - const state = createBridgeMockStore({}, { fromToken: undefined }); + const state = createBridgeMockStore({ + bridgeSliceOverrides: { fromToken: undefined }, + }); const result = getFromToken(state as never); expect(result).toStrictEqual({ @@ -359,17 +365,20 @@ describe('Bridge selectors', () => { describe('getToToken', () => { it('returns toToken', () => { - const state = createBridgeMockStore( - {}, - { toToken: { address: '0x123', symbol: 'TEST' } }, - ); + const state = createBridgeMockStore({ + bridgeSliceOverrides: { + toToken: { address: '0x123', symbol: 'TEST' }, + }, + }); const result = getToToken(state as never); expect(result).toStrictEqual({ address: '0x123', symbol: 'TEST' }); }); it('returns undefined if toToken is undefined', () => { - const state = createBridgeMockStore({}, { toToken: null }); + const state = createBridgeMockStore({ + bridgeSliceOverrides: { toToken: null }, + }); const result = getToToken(state as never); expect(result).toStrictEqual(null); @@ -378,14 +387,18 @@ describe('Bridge selectors', () => { describe('getFromAmount', () => { it('returns fromTokenInputValue', () => { - const state = createBridgeMockStore({}, { fromTokenInputValue: '123' }); + const state = createBridgeMockStore({ + bridgeSliceOverrides: { fromTokenInputValue: '123' }, + }); const result = getFromAmount(state as never); expect(result).toStrictEqual('123'); }); it('returns empty string', () => { - const state = createBridgeMockStore({}, { fromTokenInputValue: '' }); + const state = createBridgeMockStore({ + bridgeSliceOverrides: { fromTokenInputValue: '' }, + }); const result = getFromAmount(state as never); expect(result).toStrictEqual(''); @@ -403,13 +416,12 @@ describe('Bridge selectors', () => { describe('getToTokens', () => { it('returns dest tokens from controller state when toChainId is defined', () => { - const state = createBridgeMockStore( - {}, - { toChainId: '0x1' }, - { + const state = createBridgeMockStore({ + bridgeSliceOverrides: { toChainId: '0x1' }, + bridgeStateOverrides: { destTokens: { '0x00': { address: '0x00', symbol: 'TEST' } }, }, - ); + }); const result = getToTokens(state as never); expect(result).toStrictEqual({ @@ -418,13 +430,11 @@ describe('Bridge selectors', () => { }); it('returns empty dest tokens from controller state when toChainId is undefined', () => { - const state = createBridgeMockStore( - {}, - {}, - { + const state = createBridgeMockStore({ + bridgeStateOverrides: { destTokens: { '0x00': { address: '0x00', symbol: 'TEST' } }, }, - ); + }); const result = getToTokens(state as never); expect(result).toStrictEqual({}); @@ -433,28 +443,25 @@ describe('Bridge selectors', () => { describe('getToTopAssets', () => { it('returns dest top assets from controller state when toChainId is defined', () => { - const state = createBridgeMockStore( - {}, - { toChainId: '0x1' }, - { + const state = createBridgeMockStore({ + bridgeSliceOverrides: { toChainId: '0x1' }, + bridgeStateOverrides: { destTokens: { '0x00': { address: '0x00', symbol: 'TEST' } }, destTopAssets: [{ address: '0x00', symbol: 'TEST' }], }, - ); + }); const result = getToTopAssets(state as never); expect(result).toStrictEqual([{ address: '0x00', symbol: 'TEST' }]); }); it('returns empty dest top assets from controller state when toChainId is undefined', () => { - const state = createBridgeMockStore( - {}, - {}, - { + const state = createBridgeMockStore({ + bridgeStateOverrides: { destTokens: { '0x00': { address: '0x00', symbol: 'TEST' } }, destTopAssets: [{ address: '0x00', symbol: 'TEST' }], }, - ); + }); const result = getToTopAssets(state as never); expect(result).toStrictEqual([]); @@ -463,13 +470,12 @@ describe('Bridge selectors', () => { describe('getFromTokens', () => { it('returns src tokens from controller state', () => { - const state = createBridgeMockStore( - {}, - { toChainId: '0x1' }, - { + const state = createBridgeMockStore({ + bridgeSliceOverrides: { toChainId: '0x1' }, + bridgeStateOverrides: { srcTokens: { '0x00': { address: '0x00', symbol: 'TEST' } }, }, - ); + }); const result = getFromTokens(state as never); expect(result).toStrictEqual({ @@ -480,14 +486,13 @@ describe('Bridge selectors', () => { describe('getFromTopAssets', () => { it('returns src top assets from controller state', () => { - const state = createBridgeMockStore( - {}, - { toChainId: '0x1' }, - { + const state = createBridgeMockStore({ + bridgeSliceOverrides: { toChainId: '0x1' }, + bridgeStateOverrides: { srcTokens: { '0x00': { address: '0x00', symbol: 'TEST' } }, srcTopAssets: [{ address: '0x00', symbol: 'TEST' }], }, - ); + }); const result = getFromTopAssets(state as never); expect(result).toStrictEqual([{ address: '0x00', symbol: 'TEST' }]); @@ -496,10 +501,10 @@ describe('Bridge selectors', () => { describe('getBridgeQuotes', () => { it('returns quote list and fetch data, insufficientBal=false,quotesRefreshCount=5', () => { - const state = createBridgeMockStore( - { extensionConfig: { maxRefreshCount: 5 } }, - { toChainId: '0x1' }, - { + const state = createBridgeMockStore({ + featureFlagOverrides: { extensionConfig: { maxRefreshCount: 5 } }, + bridgeSliceOverrides: { toChainId: '0x1' }, + bridgeStateOverrides: { quoteRequest: { insufficientBal: false }, quotes: mockErc20Erc20Quotes, quotesFetchStatus: 1, @@ -508,7 +513,7 @@ describe('Bridge selectors', () => { srcTokens: { '0x00': { address: '0x00', symbol: 'TEST' } }, srcTopAssets: [{ address: '0x00', symbol: 'TEST' }], }, - ); + }); const result = getBridgeQuotes(state as never); expect(result).toStrictEqual({ @@ -521,10 +526,10 @@ describe('Bridge selectors', () => { }); it('returns quote list and fetch data, insufficientBal=false,quotesRefreshCount=2', () => { - const state = createBridgeMockStore( - { extensionConfig: { maxRefreshCount: 5 } }, - { toChainId: '0x1' }, - { + const state = createBridgeMockStore({ + featureFlagOverrides: { extensionConfig: { maxRefreshCount: 5 } }, + bridgeSliceOverrides: { toChainId: '0x1' }, + bridgeStateOverrides: { quoteRequest: { insufficientBal: false }, quotes: mockErc20Erc20Quotes, quotesFetchStatus: 1, @@ -533,7 +538,7 @@ describe('Bridge selectors', () => { srcTokens: { '0x00': { address: '0x00', symbol: 'TEST' } }, srcTopAssets: [{ address: '0x00', symbol: 'TEST' }], }, - ); + }); const result = getBridgeQuotes(state as never); expect(result).toStrictEqual({ @@ -546,10 +551,10 @@ describe('Bridge selectors', () => { }); it('returns quote list and fetch data, insufficientBal=true', () => { - const state = createBridgeMockStore( - { extensionConfig: { maxRefreshCount: 5 } }, - { toChainId: '0x1' }, - { + const state = createBridgeMockStore({ + featureFlagOverrides: { extensionConfig: { maxRefreshCount: 5 } }, + bridgeSliceOverrides: { toChainId: '0x1' }, + bridgeStateOverrides: { quoteRequest: { insufficientBal: true }, quotes: mockErc20Erc20Quotes, quotesFetchStatus: 1, @@ -558,7 +563,7 @@ describe('Bridge selectors', () => { srcTokens: { '0x00': { address: '0x00', symbol: 'TEST' } }, srcTopAssets: [{ address: '0x00', symbol: 'TEST' }], }, - ); + }); const result = getBridgeQuotes(state as never); expect(result).toStrictEqual({ diff --git a/ui/helpers/constants/routes.ts b/ui/helpers/constants/routes.ts index bf38109ec9d7..330d3ad20815 100644 --- a/ui/helpers/constants/routes.ts +++ b/ui/helpers/constants/routes.ts @@ -232,6 +232,7 @@ PATH_NAME_MAP[ ] = 'Encryption Public Key Request Page'; export const CROSS_CHAIN_SWAP_ROUTE = '/cross-chain'; +export const CROSS_CHAIN_SWAP_TX_DETAILS_ROUTE = '/cross-chain/tx-details'; export const SWAPS_ROUTE = '/swaps'; diff --git a/ui/hooks/bridge/useBridgeChainInfo.ts b/ui/hooks/bridge/useBridgeChainInfo.ts new file mode 100644 index 000000000000..62b743ad3cf3 --- /dev/null +++ b/ui/hooks/bridge/useBridgeChainInfo.ts @@ -0,0 +1,83 @@ +import { useSelector } from 'react-redux'; +import { Hex } from '@metamask/utils'; +import { NetworkConfiguration } from '@metamask/network-controller'; +import { Numeric } from '../../../shared/modules/Numeric'; +import { getNetworkConfigurationsByChainId } from '../../selectors'; +import { BridgeHistoryItem } from '../../../shared/types/bridge-status'; +import { + CHAIN_ID_TO_CURRENCY_SYMBOL_MAP, + NETWORK_TO_NAME_MAP, +} from '../../../shared/constants/network'; +import { CHAINID_DEFAULT_BLOCK_EXPLORER_URL_MAP } from '../../../shared/constants/common'; + +export type UseBridgeChainInfoProps = { + bridgeHistoryItem?: BridgeHistoryItem; +}; + +export default function useBridgeChainInfo({ + bridgeHistoryItem, +}: UseBridgeChainInfoProps) { + const networkConfigurationsByChainId = useSelector( + getNetworkConfigurationsByChainId, + ); + + const decSrcChainId = bridgeHistoryItem?.quote.srcChainId; + const hexSrcChainId = decSrcChainId + ? (new Numeric(decSrcChainId, 10).toPrefixedHexString() as Hex) + : undefined; + + const decDestChainId = bridgeHistoryItem?.quote.destChainId; + const hexDestChainId = decDestChainId + ? (new Numeric(decDestChainId, 10).toPrefixedHexString() as Hex) + : undefined; + + if (!bridgeHistoryItem || !hexSrcChainId || !hexDestChainId) { + return { + srcNetwork: undefined, + destNetwork: undefined, + }; + } + + // Source chain info + const srcNetwork = networkConfigurationsByChainId[hexSrcChainId] + ? networkConfigurationsByChainId[hexSrcChainId] + : undefined; + const fallbackSrcNetwork: NetworkConfiguration = { + chainId: hexSrcChainId, + name: NETWORK_TO_NAME_MAP[ + hexSrcChainId as keyof typeof NETWORK_TO_NAME_MAP + ], + nativeCurrency: + CHAIN_ID_TO_CURRENCY_SYMBOL_MAP[ + hexSrcChainId as keyof typeof CHAIN_ID_TO_CURRENCY_SYMBOL_MAP + ], + defaultBlockExplorerUrlIndex: 0, + blockExplorerUrls: [CHAINID_DEFAULT_BLOCK_EXPLORER_URL_MAP[hexSrcChainId]], + defaultRpcEndpointIndex: 0, + rpcEndpoints: [], + }; + + // Dest chain info + const destNetwork = networkConfigurationsByChainId[hexDestChainId] + ? networkConfigurationsByChainId[hexDestChainId] + : undefined; + const fallbackDestNetwork: NetworkConfiguration = { + chainId: hexDestChainId, + name: NETWORK_TO_NAME_MAP[ + hexDestChainId as keyof typeof NETWORK_TO_NAME_MAP + ], + nativeCurrency: + CHAIN_ID_TO_CURRENCY_SYMBOL_MAP[ + hexDestChainId as keyof typeof CHAIN_ID_TO_CURRENCY_SYMBOL_MAP + ], + defaultBlockExplorerUrlIndex: 0, + blockExplorerUrls: [CHAINID_DEFAULT_BLOCK_EXPLORER_URL_MAP[hexDestChainId]], + defaultRpcEndpointIndex: 0, + rpcEndpoints: [], + }; + + return { + srcNetwork: srcNetwork || fallbackSrcNetwork, + destNetwork: destNetwork || fallbackDestNetwork, + }; +} diff --git a/ui/hooks/bridge/useBridgeTxHistoryData.ts b/ui/hooks/bridge/useBridgeTxHistoryData.ts new file mode 100644 index 000000000000..97cc7f5675a3 --- /dev/null +++ b/ui/hooks/bridge/useBridgeTxHistoryData.ts @@ -0,0 +1,62 @@ +import { useSelector } from 'react-redux'; +import { Hex } from '@metamask/utils'; +import { TransactionMeta } from '@metamask/transaction-controller'; +import { useHistory } from 'react-router-dom'; +import { selectBridgeHistoryForAccount } from '../../ducks/bridge-status/selectors'; +import { CROSS_CHAIN_SWAP_TX_DETAILS_ROUTE } from '../../helpers/constants/routes'; +import { useI18nContext } from '../useI18nContext'; +import useBridgeChainInfo from './useBridgeChainInfo'; + +export type UseBridgeDataProps = { + transactionGroup: { + hasCancelled: boolean; + hasRetried: boolean; + initialTransaction: TransactionMeta; + nonce: Hex; + primaryTransaction: TransactionMeta; + transactions: TransactionMeta[]; + }; +}; + +export default function useBridgeTxHistoryData({ + transactionGroup, +}: UseBridgeDataProps) { + const t = useI18nContext(); + const history = useHistory(); + const bridgeHistory = useSelector(selectBridgeHistoryForAccount); + + const srcTxHash = transactionGroup.initialTransaction.hash; + + // If this tx is a bridge tx, it will have a bridgeHistoryItem + const bridgeHistoryItem = srcTxHash ? bridgeHistory[srcTxHash] : undefined; + + const { destNetwork } = useBridgeChainInfo({ + bridgeHistoryItem, + }); + + const destChainName = destNetwork?.name; + const bridgeTitleSuffix = destChainName + ? t('bridgeToChain', [destChainName]) + : ''; + + // By complete, this means BOTH source and dest tx are confirmed + const isBridgeComplete = bridgeHistoryItem + ? Boolean( + bridgeHistoryItem?.status.srcChain.txHash && + bridgeHistoryItem.status.destChain?.txHash, + ) + : null; + + const showBridgeTxDetails = srcTxHash + ? () => { + history.push(`${CROSS_CHAIN_SWAP_TX_DETAILS_ROUTE}/${srcTxHash}`); + } + : null; + + return { + bridgeTitleSuffix, + bridgeTxHistoryItem: bridgeHistoryItem, + isBridgeComplete, + showBridgeTxDetails, + }; +} diff --git a/ui/hooks/bridge/useCountdownTimer.test.ts b/ui/hooks/bridge/useCountdownTimer.test.ts index f2cd1190b1ba..837c65dcde74 100644 --- a/ui/hooks/bridge/useCountdownTimer.test.ts +++ b/ui/hooks/bridge/useCountdownTimer.test.ts @@ -16,17 +16,15 @@ describe('useCountdownTimer', () => { it('returns time remaining', async () => { const quotesLastFetched = Date.now(); const { result } = renderUseCountdownTimer( - createBridgeMockStore( - {}, - {}, - { + createBridgeMockStore({ + bridgeStateOverrides: { quotesLastFetched, quotesRefreshCount: 0, bridgeFeatureFlags: { extensionConfig: { maxRefreshCount: 5, refreshRate: 40000 }, }, }, - ), + }), ); let i = 0; diff --git a/ui/hooks/useTransactionDisplayData.js b/ui/hooks/useTransactionDisplayData.js index b008f2fdaf7d..2adcdd03e760 100644 --- a/ui/hooks/useTransactionDisplayData.js +++ b/ui/hooks/useTransactionDisplayData.js @@ -79,6 +79,10 @@ const signatureTypes = [ * @property {boolean} subtitleContainsOrigin - true if the subtitle includes the origin of the tx * @property {string} title - the primary title of the tx that will be displayed in the activity list * @property {string} [secondaryCurrency] - the currency string to display in the secondary position + * @property {string} date - the formatted date of the transaction + * @property {string} displayedStatusKey - the key representing the displayed status of the transaction + * @property {boolean} isPending - indicates if the transaction is pending + * @property {boolean} isSubmitted - indicates if the transaction has been submitted */ /** @@ -366,6 +370,7 @@ export function useTransactionDisplayData(transactionGroup) { primarySuffix = primaryTransaction.sourceTokenSymbol; // TODO this will be undefined right now } else if (type === TransactionType.bridge) { title = t('bridge'); + category = TransactionGroupCategory.bridge; } else { dispatch( captureSingleException( diff --git a/ui/pages/bridge/hooks/useAddToken.ts b/ui/pages/bridge/hooks/useAddToken.ts index 597149b16e49..0a2ed28ef10a 100644 --- a/ui/pages/bridge/hooks/useAddToken.ts +++ b/ui/pages/bridge/hooks/useAddToken.ts @@ -1,6 +1,5 @@ import { useDispatch, useSelector } from 'react-redux'; import { NetworkConfiguration } from '@metamask/network-controller'; -import { Numeric } from '../../../../shared/modules/Numeric'; import { QuoteResponse } from '../types'; import { getNetworkConfigurationsByChainId, @@ -8,6 +7,7 @@ import { } from '../../../selectors'; import { FEATURED_RPCS } from '../../../../shared/constants/network'; import { addToken, addNetwork } from '../../../store/actions'; +import { decimalToPrefixedHex } from '../../../../shared/modules/conversion.utils'; export default function useAddToken() { const dispatch = useDispatch(); @@ -34,9 +34,9 @@ export default function useAddToken() { const addDestToken = async (quoteResponse: QuoteResponse) => { // Look up the destination chain - const hexDestChainId = new Numeric(quoteResponse.quote.destChainId, 10) - .toPrefixedHexString() - .toLowerCase() as `0x${string}`; + const hexDestChainId = decimalToPrefixedHex( + quoteResponse.quote.destChainId, + ); const foundDestNetworkConfig: NetworkConfiguration | undefined = networkConfigurations[hexDestChainId]; let addedDestNetworkConfig: NetworkConfiguration | undefined; diff --git a/ui/pages/bridge/hooks/useHandleApprovalTx.ts b/ui/pages/bridge/hooks/useHandleApprovalTx.ts index 67f2abf67e7e..c0fb7812cce0 100644 --- a/ui/pages/bridge/hooks/useHandleApprovalTx.ts +++ b/ui/pages/bridge/hooks/useHandleApprovalTx.ts @@ -3,9 +3,9 @@ import { Hex } from '@metamask/utils'; import { BigNumber } from 'bignumber.js'; import { TxData, QuoteResponse, FeeType } from '../types'; import { isEthUsdt, getEthUsdtResetData } from '../bridge.util'; -import { Numeric } from '../../../../shared/modules/Numeric'; import { ETH_USDT_ADDRESS } from '../../../../shared/constants/bridge'; import { getBridgeERC20Allowance } from '../../../ducks/bridge/actions'; +import { decimalToPrefixedHex } from '../../../../shared/modules/conversion.utils'; import useHandleTx from './useHandleTx'; export default function useHandleApprovalTx() { @@ -59,10 +59,7 @@ export default function useHandleApprovalTx() { approval: TxData; quoteResponse: QuoteResponse; }) => { - const hexChainId = new Numeric( - approval.chainId, - 10, - ).toPrefixedHexString() as `0x${string}`; + const hexChainId = decimalToPrefixedHex(approval.chainId); // On Ethereum, we need to reset the allowance to 0 for USDT first if we need to set a new allowance // https://www.google.com/url?q=https://docs.unizen.io/trade-api/before-you-get-started/token-allowance-management-for-non-updatable-allowance-tokens&sa=D&source=docs&ust=1727386175513609&usg=AOvVaw3Opm6BSJeu7qO0Ve5iLTOh @@ -86,7 +83,7 @@ export default function useHandleApprovalTx() { }, }); - return txMeta.id; + return txMeta; }; return { handleApprovalTx, diff --git a/ui/pages/bridge/hooks/useHandleBridgeTx.ts b/ui/pages/bridge/hooks/useHandleBridgeTx.ts index 22b2a74fa077..f8135a1bbc08 100644 --- a/ui/pages/bridge/hooks/useHandleBridgeTx.ts +++ b/ui/pages/bridge/hooks/useHandleBridgeTx.ts @@ -41,7 +41,7 @@ export default function useHandleBridgeTx() { }, }); - return txMeta.id; + return txMeta; }; return { handleBridgeTx }; diff --git a/ui/pages/bridge/hooks/useHandleTx.ts b/ui/pages/bridge/hooks/useHandleTx.ts index a4cbf631c338..ffd378449bc7 100644 --- a/ui/pages/bridge/hooks/useHandleTx.ts +++ b/ui/pages/bridge/hooks/useHandleTx.ts @@ -14,7 +14,7 @@ import { import { getGasFeeEstimates } from '../../../ducks/metamask/metamask'; import { checkNetworkAndAccountSupports1559 } from '../../../selectors'; import { ChainId } from '../types'; -import { Numeric } from '../../../../shared/modules/Numeric'; +import { decimalToPrefixedHex } from '../../../../shared/modules/conversion.utils'; export default function useHandleTx() { const dispatch = useDispatch(); @@ -42,10 +42,7 @@ export default function useHandleTx() { meta: Partial; }; }) => { - const hexChainId = new Numeric( - txParams.chainId, - 10, - ).toPrefixedHexString() as `0x${string}`; + const hexChainId = decimalToPrefixedHex(txParams.chainId); const { maxFeePerGas, maxPriorityFeePerGas } = await getTxGasEstimates({ networkAndAccountSupports1559, diff --git a/ui/pages/bridge/hooks/useSubmitBridgeTransaction.test.tsx b/ui/pages/bridge/hooks/useSubmitBridgeTransaction.test.tsx index 20f471b1065b..6398e66b4ad5 100644 --- a/ui/pages/bridge/hooks/useSubmitBridgeTransaction.test.tsx +++ b/ui/pages/bridge/hooks/useSubmitBridgeTransaction.test.tsx @@ -88,11 +88,8 @@ const middleware = [thunk]; const makeMockStore = () => { const store = configureMockStore(middleware)( - createBridgeMockStore( - {}, - {}, - {}, - { + createBridgeMockStore({ + metamaskStateOverrides: { gasFeeEstimates: { high: { maxWaitTimeEstimate: 30000, @@ -103,7 +100,7 @@ const makeMockStore = () => { }, useExternalServices: true, }, - ), + }), ); return store; }; diff --git a/ui/pages/bridge/hooks/useSubmitBridgeTransaction.ts b/ui/pages/bridge/hooks/useSubmitBridgeTransaction.ts index db3b1c86ca06..fcdf8569bdd0 100644 --- a/ui/pages/bridge/hooks/useSubmitBridgeTransaction.ts +++ b/ui/pages/bridge/hooks/useSubmitBridgeTransaction.ts @@ -1,9 +1,11 @@ import { useDispatch } from 'react-redux'; import { zeroAddress } from 'ethereumjs-util'; import { useHistory } from 'react-router-dom'; +import { TransactionMeta } from '@metamask/transaction-controller'; import { QuoteResponse } from '../types'; import { DEFAULT_ROUTE } from '../../../helpers/constants/routes'; import { setDefaultHomeActiveTabName } from '../../../store/actions'; +import { startPollingForBridgeTxStatus } from '../../../ducks/bridge-status/actions'; import useAddToken from './useAddToken'; import useHandleApprovalTx from './useHandleApprovalTx'; import useHandleBridgeTx from './useHandleBridgeTx'; @@ -17,19 +19,40 @@ export default function useSubmitBridgeTransaction() { const submitBridgeTransaction = async (quoteResponse: QuoteResponse) => { // Execute transaction(s) - let approvalTxId: string | undefined; + let approvalTxMeta: TransactionMeta | undefined; if (quoteResponse?.approval) { - approvalTxId = await handleApprovalTx({ + approvalTxMeta = await handleApprovalTx({ approval: quoteResponse.approval, quoteResponse, }); } - await handleBridgeTx({ + const bridgeTxMeta = await handleBridgeTx({ quoteResponse, - approvalTxId, + approvalTxId: approvalTxMeta?.id, }); + // Get bridge tx status + if (bridgeTxMeta.hash) { + const statusRequest = { + bridgeId: quoteResponse.quote.bridgeId, + srcTxHash: bridgeTxMeta.hash, + bridge: quoteResponse.quote.bridges[0], + srcChainId: quoteResponse.quote.srcChainId, + destChainId: quoteResponse.quote.destChainId, + quote: quoteResponse.quote, + refuel: Boolean(quoteResponse.quote.refuel), + }; + dispatch( + startPollingForBridgeTxStatus({ + statusRequest, + quoteResponse, + slippagePercentage: 0, // TODO pull this from redux/bridgecontroller once it's implemented. currently hardcoded in quoteRequest.slippage right now + startTime: bridgeTxMeta.time, + }), + ); + } + // Add tokens if not the native gas token if (quoteResponse.quote.srcAsset.address !== zeroAddress()) { addSourceToken(quoteResponse); diff --git a/ui/pages/bridge/index.scss b/ui/pages/bridge/index.scss index bc96dfbbe825..e9d6009676f9 100644 --- a/ui/pages/bridge/index.scss +++ b/ui/pages/bridge/index.scss @@ -2,6 +2,7 @@ @import 'prepare/index'; @import 'quotes/index'; +@import 'transaction-details/index'; .bridge { diff --git a/ui/pages/bridge/index.test.tsx b/ui/pages/bridge/index.test.tsx index 7d5f813513c5..a4c45dbffbdf 100644 --- a/ui/pages/bridge/index.test.tsx +++ b/ui/pages/bridge/index.test.tsx @@ -65,7 +65,9 @@ describe('Bridge', () => { }); it('renders the component with initial props', async () => { - const swapsMockStore = createBridgeMockStore({ extensionSupport: true }); + const swapsMockStore = createBridgeMockStore({ + featureFlagOverrides: { extensionSupport: true }, + }); const store = configureMockStore(middleware)(swapsMockStore); const { container, getByText } = renderWithProvider( diff --git a/ui/pages/bridge/prepare/bridge-cta-button.test.tsx b/ui/pages/bridge/prepare/bridge-cta-button.test.tsx index 91adb422e22b..e20a7e661796 100644 --- a/ui/pages/bridge/prepare/bridge-cta-button.test.tsx +++ b/ui/pages/bridge/prepare/bridge-cta-button.test.tsx @@ -11,13 +11,13 @@ import { BridgeCTAButton } from './bridge-cta-button'; describe('BridgeCTAButton', () => { it("should render the component's initial state", () => { - const mockStore = createBridgeMockStore( - { + const mockStore = createBridgeMockStore({ + featureFlagOverrides: { srcNetworkAllowlist: [CHAIN_IDS.MAINNET, CHAIN_IDS.OPTIMISM], destNetworkAllowlist: [CHAIN_IDS.OPTIMISM], }, - { fromTokenInputValue: 1 }, - ); + bridgeSliceOverrides: { fromTokenInputValue: 1 }, + }); const { container, getByText, getByRole } = renderWithProvider( , configureStore(mockStore), @@ -30,19 +30,18 @@ describe('BridgeCTAButton', () => { }); it('should render the component when amount is missing', () => { - const mockStore = createBridgeMockStore( - { + const mockStore = createBridgeMockStore({ + featureFlagOverrides: { srcNetworkAllowlist: [CHAIN_IDS.MAINNET, CHAIN_IDS.OPTIMISM], destNetworkAllowlist: [CHAIN_IDS.LINEA_MAINNET], }, - { + bridgeSliceOverrides: { fromTokenInputValue: null, fromToken: 'ETH', toToken: 'ETH', toChainId: CHAIN_IDS.LINEA_MAINNET, }, - {}, - ); + }); const { getByText, getByRole } = renderWithProvider( , configureStore(mockStore), @@ -53,19 +52,18 @@ describe('BridgeCTAButton', () => { }); it('should render the component when amount and dest token is missing', () => { - const mockStore = createBridgeMockStore( - { + const mockStore = createBridgeMockStore({ + featureFlagOverrides: { srcNetworkAllowlist: [CHAIN_IDS.MAINNET, CHAIN_IDS.OPTIMISM], destNetworkAllowlist: [CHAIN_IDS.LINEA_MAINNET], }, - { + bridgeSliceOverrides: { fromTokenInputValue: null, fromToken: 'ETH', toToken: null, toChainId: CHAIN_IDS.LINEA_MAINNET, }, - {}, - ); + }); const { getByText, getByRole } = renderWithProvider( , configureStore(mockStore), @@ -76,23 +74,23 @@ describe('BridgeCTAButton', () => { }); it('should render the component when tx is submittable', () => { - const mockStore = createBridgeMockStore( - { + const mockStore = createBridgeMockStore({ + featureFlagOverrides: { srcNetworkAllowlist: [CHAIN_IDS.MAINNET, CHAIN_IDS.OPTIMISM], destNetworkAllowlist: [CHAIN_IDS.LINEA_MAINNET], }, - { + bridgeSliceOverrides: { fromTokenInputValue: 1, fromToken: 'ETH', toToken: 'ETH', toChainId: CHAIN_IDS.LINEA_MAINNET, }, - { + bridgeStateOverrides: { quotes: mockBridgeQuotesNativeErc20, quotesLastFetched: Date.now(), quotesLoadingStatus: RequestStatus.FETCHED, }, - ); + }); const { getByText, getByRole } = renderWithProvider( , configureStore(mockStore), @@ -103,23 +101,23 @@ describe('BridgeCTAButton', () => { }); it('should disable the component when quotes are loading and there are no existing quotes', () => { - const mockStore = createBridgeMockStore( - { + const mockStore = createBridgeMockStore({ + featureFlagOverrides: { srcNetworkAllowlist: [CHAIN_IDS.MAINNET, CHAIN_IDS.OPTIMISM], destNetworkAllowlist: [CHAIN_IDS.LINEA_MAINNET], }, - { + bridgeSliceOverrides: { fromTokenInputValue: 1, fromToken: 'ETH', toToken: 'ETH', toChainId: CHAIN_IDS.LINEA_MAINNET, }, - { + bridgeStateOverrides: { quotes: [], quotesLastFetched: Date.now(), quotesLoadingStatus: RequestStatus.LOADING, }, - ); + }); const { getByText, getByRole } = renderWithProvider( , configureStore(mockStore), @@ -130,23 +128,23 @@ describe('BridgeCTAButton', () => { }); it('should enable the component when quotes are loading and there are existing quotes', () => { - const mockStore = createBridgeMockStore( - { + const mockStore = createBridgeMockStore({ + featureFlagOverrides: { srcNetworkAllowlist: [CHAIN_IDS.MAINNET, CHAIN_IDS.OPTIMISM], destNetworkAllowlist: [CHAIN_IDS.LINEA_MAINNET], }, - { + bridgeSliceOverrides: { fromTokenInputValue: 1, fromToken: 'ETH', toToken: 'ETH', toChainId: CHAIN_IDS.LINEA_MAINNET, }, - { + bridgeStateOverrides: { quotes: mockBridgeQuotesNativeErc20, quotesLastFetched: Date.now(), quotesLoadingStatus: RequestStatus.LOADING, }, - ); + }); const { getByText, getByRole } = renderWithProvider( , configureStore(mockStore), diff --git a/ui/pages/bridge/prepare/prepare-bridge-page.test.tsx b/ui/pages/bridge/prepare/prepare-bridge-page.test.tsx index aba35d5b89be..2534d8daa430 100644 --- a/ui/pages/bridge/prepare/prepare-bridge-page.test.tsx +++ b/ui/pages/bridge/prepare/prepare-bridge-page.test.tsx @@ -23,13 +23,12 @@ describe('PrepareBridgePage', () => { }); it('should render the component, with initial state', async () => { - const mockStore = createBridgeMockStore( - { + const mockStore = createBridgeMockStore({ + featureFlagOverrides: { srcNetworkAllowlist: [CHAIN_IDS.MAINNET, CHAIN_IDS.OPTIMISM], destNetworkAllowlist: [CHAIN_IDS.OPTIMISM], }, - {}, - ); + }); const { container, getByRole, getByTestId } = renderWithProvider( , configureStore(mockStore), @@ -54,8 +53,8 @@ describe('PrepareBridgePage', () => { }); it('should render the component, with inputs set', async () => { - const mockStore = createBridgeMockStore( - { + const mockStore = createBridgeMockStore({ + featureFlagOverrides: { srcNetworkAllowlist: [CHAIN_IDS.MAINNET, CHAIN_IDS.LINEA_MAINNET], destNetworkAllowlist: [CHAIN_IDS.LINEA_MAINNET], destTokens: { @@ -67,7 +66,7 @@ describe('PrepareBridgePage', () => { }, }, }, - { + bridgeSliceOverrides: { fromTokenInputValue: '1', fromToken: { address: '0x3103910', decimals: 6 }, toToken: { @@ -78,7 +77,7 @@ describe('PrepareBridgePage', () => { }, toChainId: CHAIN_IDS.LINEA_MAINNET, }, - { + bridgeStateOverrides: { quoteRequest: { srcTokenAddress: '0x3103910', destTokenAddress: '0x1f9840a85d5af5bf1d1762f925bdaddc4201f984', @@ -88,7 +87,7 @@ describe('PrepareBridgePage', () => { slippage: 0.5, }, }, - ); + }); const { container, getByRole, getByTestId } = renderWithProvider( , configureStore(mockStore), @@ -115,12 +114,12 @@ describe('PrepareBridgePage', () => { }); it('should throw an error if token decimals are not defined', async () => { - const mockStore = createBridgeMockStore( - { + const mockStore = createBridgeMockStore({ + featureFlagOverrides: { srcNetworkAllowlist: [CHAIN_IDS.MAINNET, CHAIN_IDS.LINEA_MAINNET], destNetworkAllowlist: [CHAIN_IDS.LINEA_MAINNET], }, - { + bridgeSliceOverrides: { fromTokenInputValue: 1, fromToken: { address: '0x3103910' }, toToken: { @@ -130,8 +129,7 @@ describe('PrepareBridgePage', () => { }, toChainId: CHAIN_IDS.LINEA_MAINNET, }, - {}, - ); + }); expect(() => renderWithProvider(, configureStore(mockStore)), diff --git a/ui/pages/bridge/quotes/bridge-quote-card.test.tsx b/ui/pages/bridge/quotes/bridge-quote-card.test.tsx index 274ade65a4d1..950f2641bd1c 100644 --- a/ui/pages/bridge/quotes/bridge-quote-card.test.tsx +++ b/ui/pages/bridge/quotes/bridge-quote-card.test.tsx @@ -16,13 +16,13 @@ describe('BridgeQuoteCard', () => { }); it('should render the recommended quote', async () => { - const mockStore = createBridgeMockStore( - { + const mockStore = createBridgeMockStore({ + featureFlagOverrides: { srcNetworkAllowlist: [CHAIN_IDS.MAINNET, CHAIN_IDS.OPTIMISM], destNetworkAllowlist: [CHAIN_IDS.OPTIMISM], }, - { fromTokenInputValue: 1 }, - { + bridgeSliceOverrides: { fromTokenInputValue: 1 }, + bridgeStateOverrides: { quoteRequest: { insufficientBal: false }, quotesRefreshCount: 1, quotes: mockBridgeQuotesErc20Erc20, @@ -32,7 +32,7 @@ describe('BridgeQuoteCard', () => { extensionConfig: { maxRefreshCount: 5, refreshRate: 30000 }, }, }, - ); + }); const { container } = renderWithProvider( , configureStore(mockStore), @@ -42,18 +42,18 @@ describe('BridgeQuoteCard', () => { }); it('should render the recommended quote while loading new quotes', async () => { - const mockStore = createBridgeMockStore( - { + const mockStore = createBridgeMockStore({ + featureFlagOverrides: { srcNetworkAllowlist: [CHAIN_IDS.MAINNET, CHAIN_IDS.OPTIMISM], destNetworkAllowlist: [CHAIN_IDS.OPTIMISM], }, - { fromTokenInputValue: 1 }, - { + bridgeSliceOverrides: { fromTokenInputValue: 1 }, + bridgeStateOverrides: { quotes: mockBridgeQuotesNativeErc20, getQuotesLastFetched: Date.now() - 5000, quotesLoadingStatus: RequestStatus.LOADING, }, - ); + }); const { container, queryByText } = renderWithProvider( , configureStore(mockStore), @@ -64,18 +64,18 @@ describe('BridgeQuoteCard', () => { }); it('should not render when there is no quote', async () => { - const mockStore = createBridgeMockStore( - { + const mockStore = createBridgeMockStore({ + featureFlagOverrides: { srcNetworkAllowlist: [CHAIN_IDS.MAINNET, CHAIN_IDS.OPTIMISM], destNetworkAllowlist: [CHAIN_IDS.OPTIMISM], }, - { fromTokenInputValue: 1 }, - { + bridgeSliceOverrides: { fromTokenInputValue: 1 }, + bridgeStateOverrides: { quotes: [], getQuotesLastFetched: Date.now() - 5000, quotesLoadingStatus: RequestStatus.FETCHED, }, - ); + }); const { container } = renderWithProvider( , configureStore(mockStore), @@ -85,18 +85,18 @@ describe('BridgeQuoteCard', () => { }); it('should not render when there is a quote fetch error', async () => { - const mockStore = createBridgeMockStore( - { + const mockStore = createBridgeMockStore({ + featureFlagOverrides: { srcNetworkAllowlist: [CHAIN_IDS.MAINNET, CHAIN_IDS.OPTIMISM], destNetworkAllowlist: [CHAIN_IDS.OPTIMISM], }, - { fromTokenInputValue: 1 }, - { + bridgeSliceOverrides: { fromTokenInputValue: 1 }, + bridgeStateOverrides: { quotes: [], getQuotesLastFetched: Date.now() - 5000, quotesLoadingStatus: RequestStatus.ERROR, }, - ); + }); const { container } = renderWithProvider( , configureStore(mockStore), diff --git a/ui/pages/bridge/quotes/bridge-quotes-modal.test.tsx b/ui/pages/bridge/quotes/bridge-quotes-modal.test.tsx index 0431f1b8eada..42c5968bb5b0 100644 --- a/ui/pages/bridge/quotes/bridge-quotes-modal.test.tsx +++ b/ui/pages/bridge/quotes/bridge-quotes-modal.test.tsx @@ -10,15 +10,13 @@ import { BridgeQuotesModal } from './bridge-quotes-modal'; describe('BridgeQuotesModal', () => { it('should render the modal', () => { - const mockStore = createBridgeMockStore( - {}, - {}, - { + const mockStore = createBridgeMockStore({ + bridgeStateOverrides: { quotes: mockBridgeQuotesErc20Erc20, getQuotesLastFetched: Date.now(), quotesLoadingStatus: RequestStatus.FETCHED, }, - ); + }); const { baseElement } = renderWithProvider( { + if (srcTxStatus === StatusTypes.PENDING) { + return 1; + } + + if (srcTxStatus === StatusTypes.COMPLETE) { + return 2; + } + + throw new Error('No more possible states for srcTxStatus'); +}; + +const getSrcTxStatus = (initialTransaction: TransactionMeta) => { + return initialTransaction.status === TransactionStatus.confirmed + ? StatusTypes.COMPLETE + : StatusTypes.PENDING; +}; + +const getDestTxStatus = ( + bridgeTxHistoryItem: BridgeHistoryItem, + srcTxStatus: StatusTypes, +) => { + if (srcTxStatus !== StatusTypes.COMPLETE) { + return null; + } + + return bridgeTxHistoryItem?.status.destChain?.txHash && + bridgeTxHistoryItem.status.status === StatusTypes.COMPLETE + ? StatusTypes.COMPLETE + : StatusTypes.PENDING; +}; + +/** + * Renders the 2 transaction segments for a bridge activity item + * + * @param options + * @param options.bridgeTxHistoryItem - The bridge history item for the transaction + * @param options.transactionGroup - The transaction group for the transaction + */ +export default function BridgeActivityItemTxSegments({ + bridgeTxHistoryItem, + transactionGroup, +}: { + bridgeTxHistoryItem: BridgeHistoryItem; + transactionGroup: UseBridgeDataProps['transactionGroup']; +}) { + const { initialTransaction } = transactionGroup; + const srcTxStatus = getSrcTxStatus(initialTransaction); + const destTxStatus = getDestTxStatus(bridgeTxHistoryItem, srcTxStatus); + const txIndex = getTxIndex(srcTxStatus); + + return ( + + Transaction {txIndex} of 2 + + + + + + ); +} diff --git a/ui/pages/bridge/transaction-details/bridge-explorer-links.tsx b/ui/pages/bridge/transaction-details/bridge-explorer-links.tsx new file mode 100644 index 000000000000..570680f416d2 --- /dev/null +++ b/ui/pages/bridge/transaction-details/bridge-explorer-links.tsx @@ -0,0 +1,116 @@ +import { NetworkConfiguration } from '@metamask/network-controller'; +import React, { useContext } from 'react'; +import { Hex } from '@metamask/utils'; +import { + Box, + IconName, + ButtonSecondary, +} from '../../../components/component-library'; +import { openBlockExplorer } from '../../../components/multichain/menu-items/view-explorer-menu-item'; +import { MetaMetricsContext } from '../../../contexts/metametrics'; +import { + Display, + FlexDirection, +} from '../../../helpers/constants/design-system'; +import { useI18nContext } from '../../../hooks/useI18nContext'; +import { CHAINID_DEFAULT_BLOCK_EXPLORER_HUMAN_READABLE_URL_MAP } from '../../../../shared/constants/common'; + +const getBlockExplorerName = ( + chainId: Hex | undefined, + blockExplorerUrl: string | undefined, +) => { + const humanReadableUrl = chainId + ? CHAINID_DEFAULT_BLOCK_EXPLORER_HUMAN_READABLE_URL_MAP[chainId] + : undefined; + if (humanReadableUrl) { + return humanReadableUrl; + } + + if (!blockExplorerUrl) { + return undefined; + } + return blockExplorerUrl.split('/')[2]; +}; + +export const getBlockExplorerUrl = ( + networkConfiguration: NetworkConfiguration | undefined, + txHash: string | undefined, +) => { + if (!networkConfiguration || !txHash) { + return undefined; + } + const index = networkConfiguration.defaultBlockExplorerUrlIndex; + if (index === undefined) { + return undefined; + } + + const rootUrl = networkConfiguration.blockExplorerUrls[index]?.replace( + /\/$/u, + '', + ); + return `${rootUrl}/tx/${txHash}`; +}; + +const METRICS_LOCATION = 'Activity Tab'; + +type ExplorerLinksProps = { + srcChainId?: Hex; + destChainId?: Hex; + srcBlockExplorerUrl?: string; + destBlockExplorerUrl?: string; +}; + +export default function BridgeExplorerLinks({ + srcChainId, + destChainId, + srcBlockExplorerUrl, + destBlockExplorerUrl, +}: ExplorerLinksProps) { + const trackEvent = useContext(MetaMetricsContext); + const t = useI18nContext(); + + // Not sure why but the text is not being changed to white on hover, unless it's put into a variable before the render + const srcButtonText = t('bridgeExplorerLinkViewOn', [ + getBlockExplorerName(srcChainId, srcBlockExplorerUrl), + ]); + const destButtonText = t('bridgeExplorerLinkViewOn', [ + getBlockExplorerName(destChainId, destBlockExplorerUrl), + ]); + + return ( + + {srcBlockExplorerUrl && ( + { + if (srcBlockExplorerUrl) { + openBlockExplorer( + srcBlockExplorerUrl, + METRICS_LOCATION, + trackEvent, + ); + } + }} + > + {srcButtonText} + + )} + {destBlockExplorerUrl && ( + { + if (destBlockExplorerUrl) { + openBlockExplorer( + destBlockExplorerUrl, + METRICS_LOCATION, + trackEvent, + ); + } + }} + > + {destButtonText} + + )} + + ); +} diff --git a/ui/pages/bridge/transaction-details/bridge-step-description.tsx b/ui/pages/bridge/transaction-details/bridge-step-description.tsx new file mode 100644 index 000000000000..ef69ea4ed116 --- /dev/null +++ b/ui/pages/bridge/transaction-details/bridge-step-description.tsx @@ -0,0 +1,185 @@ +import * as React from 'react'; +import { NetworkConfiguration } from '@metamask/network-controller'; +import { Hex } from '@metamask/utils'; +import { + TransactionMeta, + TransactionStatus, +} from '@metamask/transaction-controller'; +import { + BridgeHistoryItem, + Step, + ActionTypes, + StatusTypes, +} from '../../../../shared/types/bridge-status'; +import { Box, Text } from '../../../components/component-library'; +import { Numeric } from '../../../../shared/modules/Numeric'; +import { + AlignItems, + Display, + FontWeight, + TextColor, +} from '../../../helpers/constants/design-system'; +import { useI18nContext } from '../../../hooks/useI18nContext'; + +type I18nFunction = ( + key: string, + args: (string | number | undefined | null)[], +) => string; + +/** + * bridge actions will have step.srcChainId !== step.destChainId + * We cannot infer the status of the bridge action since 2 different chains are involved + * The best we can do is the bridgeHistoryItem.estimatedProcessingTimeInSeconds + * + * @param t - The i18n context return value to get translations + * @param stepStatus - The status of the step + * @param step - The step to be rendered + * @param networkConfigurationsByChainId - The network configurations by chain id + */ +const getBridgeActionText = ( + t: I18nFunction, + stepStatus: StatusTypes | null, + step: Step, + networkConfigurationsByChainId: Record<`0x${string}`, NetworkConfiguration>, +) => { + const hexDestChainId = step.destChainId + ? (new Numeric(step.destChainId, 10).toPrefixedHexString() as Hex) + : undefined; + const destNetworkConfiguration = hexDestChainId + ? networkConfigurationsByChainId[hexDestChainId] + : undefined; + + return stepStatus === StatusTypes.COMPLETE + ? t('bridgeStepActionBridgeComplete', [ + step.destAsset.symbol, + destNetworkConfiguration?.name, + ]) + : t('bridgeStepActionBridgePending', [ + step.destAsset.symbol, + destNetworkConfiguration?.name, + ]); +}; + +const getBridgeActionStatus = (bridgeHistoryItem: BridgeHistoryItem) => { + return bridgeHistoryItem.status ? bridgeHistoryItem.status.status : null; +}; + +/** + * swap actions can have step.srcChainId === step.destChainId, and can occur on + * EITHER the quote.srcChainId or the quote.destChainId + * Despite not having any actual timestamp,we can infer the status of the swap action + * based on the status of the source chain tx if srcChainId and destChainId are the same* + * + * @param bridgeHistoryItem + * @param step + * @param srcChainTxMeta + */ +const getSwapActionStatus = ( + bridgeHistoryItem: BridgeHistoryItem, + step: Step, + srcChainTxMeta?: TransactionMeta, +) => { + const isSrcAndDestChainSame = step.srcChainId === step.destChainId; + const isSwapOnSrcChain = + step.srcChainId === bridgeHistoryItem.quote.srcChainId; + + if (isSrcAndDestChainSame && isSwapOnSrcChain) { + // if the swap action is on the src chain (i.e. step.srcChainId === step.destChainId === bridgeHistoryItem.quote.srcChainId), + // we check the source chain tx status, since we know when it's confirmed + const isSrcChainTxConfirmed = + srcChainTxMeta?.status === TransactionStatus.confirmed; + return isSrcChainTxConfirmed ? StatusTypes.COMPLETE : StatusTypes.PENDING; + } + // if the swap action is on the dest chain, we check the bridgeHistoryItem.status, + // since we don't know when the dest tx is confirmed + if (srcChainTxMeta?.status === TransactionStatus.confirmed) { + return bridgeHistoryItem.status ? bridgeHistoryItem.status.status : null; + } + + // If the source chain tx is not confirmed, we know the swap hasn't started + // use null to represent this as we don't have an equivalent in StatusTypes + return null; +}; + +const getSwapActionText = ( + t: I18nFunction, + status: StatusTypes | null, + step: Step, +) => { + return status === StatusTypes.COMPLETE + ? t('bridgeStepActionSwapComplete', [ + step.srcAsset.symbol, + step.destAsset.symbol, + ]) + : t('bridgeStepActionSwapPending', [ + step.srcAsset.symbol, + step.destAsset.symbol, + ]); +}; + +export const getStepStatus = ( + bridgeHistoryItem: BridgeHistoryItem, + step: Step, + srcChainTxMeta?: TransactionMeta, +) => { + if (step.action === ActionTypes.SWAP) { + return getSwapActionStatus(bridgeHistoryItem, step, srcChainTxMeta); + } else if (step.action === ActionTypes.BRIDGE) { + return getBridgeActionStatus(bridgeHistoryItem); + } + + return StatusTypes.UNKNOWN; +}; + +type BridgeStepProps = { + step: Step; + networkConfigurationsByChainId: Record<`0x${string}`, NetworkConfiguration>; + time?: string; + stepStatus: StatusTypes | null; +}; + +// You can have the following cases: +// 1. Bridge: usually for cases like Optimism ETH to Arbitrum ETH +// 2. Swap > Bridge +// 3. Swap > Bridge > Swap: e.g. Optimism ETH to Avalanche USDC +export default function BridgeStepDescription({ + step, + networkConfigurationsByChainId, + time, + stepStatus, +}: BridgeStepProps) { + const t = useI18nContext() as I18nFunction; + return ( + + {time && {time}} + + {step.action === ActionTypes.BRIDGE && + getBridgeActionText( + t, + stepStatus, + step, + networkConfigurationsByChainId, + )} + {step.action === ActionTypes.SWAP && + getSwapActionText(t, stepStatus, step)} + + + ); +} diff --git a/ui/pages/bridge/transaction-details/bridge-step-list.stories.tsx b/ui/pages/bridge/transaction-details/bridge-step-list.stories.tsx new file mode 100644 index 000000000000..a02337fbce61 --- /dev/null +++ b/ui/pages/bridge/transaction-details/bridge-step-list.stories.tsx @@ -0,0 +1,1781 @@ +import React, { useEffect } from 'react'; +import { StoryFn, Meta } from '@storybook/react'; +import { + BlockSize, + Display, + FlexDirection, +} from '../../../helpers/constants/design-system'; +import { Box } from '../../../components/component-library'; +import BridgeStepList from './bridge-step-list'; + +export default { + title: 'pages/bridge/transaction-details/BridgeStepList', + component: BridgeStepList, +} as Meta; + +const networkConfigurationsByChainId = { + '0xa4b1': { + blockExplorerUrls: ['https://explorer.arbitrum.io'], + chainId: '0xa4b1', + defaultBlockExplorerUrlIndex: 0, + defaultRpcEndpointIndex: 0, + name: 'Arbitrum One', + nativeCurrency: 'ETH', + rpcEndpoints: [ + { + networkClientId: '1abbfb5d-ed75-4de7-8cff-694d863d8519', + type: 'custom', + url: 'https://arbitrum-mainnet.infura.io/v3/05412cce7f71411d93cc724545806fd3', + }, + ], + }, + '0xa86a': { + blockExplorerUrls: ['https://snowtrace.io/'], + chainId: '0xa86a', + defaultBlockExplorerUrlIndex: 0, + defaultRpcEndpointIndex: 0, + name: 'Avalanche Network C-Chain', + nativeCurrency: 'AVAX', + rpcEndpoints: [ + { + networkClientId: '2fb77c4b-4515-40e2-b717-5629a086e606', + type: 'custom', + url: 'https://avalanche-mainnet.infura.io/v3/05412cce7f71411d93cc724545806fd3', + }, + ], + }, +}; + +const Args = { + step1: { + bridgeHistoryItem: { + quote: { + requestId: '125a2aa2-c71f-4e96-9e3b-96fca826a608', + srcChainId: 43114, + srcTokenAmount: '99125000000000000', + srcAsset: { + address: '0x0000000000000000000000000000000000000000', + chainId: 43114, + symbol: 'AVAX', + decimals: 18, + name: 'AVAX', + coinKey: 'AVAX', + logoURI: + 'https://static.debank.com/image/avax_token/logo_url/avax/0b9c84359c84d6bdd5bfda9c2d4c4a82.png', + priceUSD: '26.56', + icon: 'https://static.debank.com/image/avax_token/logo_url/avax/0b9c84359c84d6bdd5bfda9c2d4c4a82.png', + }, + destChainId: 42161, + destTokenAmount: '976873412881134', + destAsset: { + address: '0x0000000000000000000000000000000000000000', + chainId: 42161, + symbol: 'ETH', + decimals: 18, + name: 'ETH', + coinKey: 'ETH', + logoURI: + 'https://raw.githubusercontent.com/trustwallet/assets/master/blockchains/ethereum/assets/0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2/logo.png', + priceUSD: '2689.39', + icon: 'https://raw.githubusercontent.com/trustwallet/assets/master/blockchains/ethereum/assets/0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2/logo.png', + }, + feeData: { + metabridge: { + amount: '875000000000000', + asset: { + address: '0x0000000000000000000000000000000000000000', + chainId: 43114, + symbol: 'AVAX', + decimals: 18, + name: 'AVAX', + coinKey: 'AVAX', + logoURI: + 'https://static.debank.com/image/avax_token/logo_url/avax/0b9c84359c84d6bdd5bfda9c2d4c4a82.png', + priceUSD: '26.56', + icon: 'https://static.debank.com/image/avax_token/logo_url/avax/0b9c84359c84d6bdd5bfda9c2d4c4a82.png', + }, + }, + }, + bridgeId: 'lifi', + bridges: ['stargate'], + steps: [ + { + action: 'swap', + srcChainId: 43114, + destChainId: 43114, + protocol: { + name: '1inch', + displayName: '1inch', + icon: 'https://raw.githubusercontent.com/lifinance/types/main/src/assets/icons/exchanges/oneinch.png', + }, + srcAsset: { + address: '0x0000000000000000000000000000000000000000', + chainId: 43114, + symbol: 'AVAX', + decimals: 18, + name: 'AVAX', + coinKey: 'AVAX', + logoURI: + 'https://static.debank.com/image/avax_token/logo_url/avax/0b9c84359c84d6bdd5bfda9c2d4c4a82.png', + priceUSD: '26.56', + icon: 'https://static.debank.com/image/avax_token/logo_url/avax/0b9c84359c84d6bdd5bfda9c2d4c4a82.png', + }, + destAsset: { + address: '0x9702230A8Ea53601f5cD2dc00fDBc13d4dF4A8c7', + chainId: 43114, + symbol: 'USDt', + decimals: 6, + name: 'TetherToken', + coinKey: 'USDt', + logoURI: + 'https://static.debank.com/image/eth_token/logo_url/0xdac17f958d2ee523a2206206994597c13d831ec7/464c0de678334b8fe87327e527bc476d.png', + priceUSD: '1.00125', + icon: 'https://static.debank.com/image/eth_token/logo_url/0xdac17f958d2ee523a2206206994597c13d831ec7/464c0de678334b8fe87327e527bc476d.png', + }, + srcAmount: '99125000000000000', + destAmount: '2630653', + }, + { + action: 'bridge', + srcChainId: 43114, + destChainId: 42161, + protocol: { + name: 'stargate', + displayName: 'StargateV2 (Fast mode)', + icon: 'https://raw.githubusercontent.com/lifinance/types/5685c638772f533edad80fcb210b4bb89e30a50f/src/assets/icons/bridges/stargate.png', + }, + srcAsset: { + address: '0x9702230A8Ea53601f5cD2dc00fDBc13d4dF4A8c7', + chainId: 43114, + symbol: 'USDt', + decimals: 6, + name: 'TetherToken', + coinKey: 'USDt', + logoURI: + 'https://static.debank.com/image/eth_token/logo_url/0xdac17f958d2ee523a2206206994597c13d831ec7/464c0de678334b8fe87327e527bc476d.png', + priceUSD: '1.00125', + icon: 'https://static.debank.com/image/eth_token/logo_url/0xdac17f958d2ee523a2206206994597c13d831ec7/464c0de678334b8fe87327e527bc476d.png', + }, + destAsset: { + address: '0xFd086bC7CD5C481DCC9C85ebE478A1C0b69FCbb9', + chainId: 42161, + symbol: 'USDT', + decimals: 6, + name: 'USDT', + coinKey: 'USDT', + logoURI: + 'https://raw.githubusercontent.com/trustwallet/assets/master/blockchains/ethereum/assets/0xdAC17F958D2ee523a2206206994597C13D831ec7/logo.png', + priceUSD: '1.00125', + icon: 'https://raw.githubusercontent.com/trustwallet/assets/master/blockchains/ethereum/assets/0xdAC17F958D2ee523a2206206994597C13D831ec7/logo.png', + }, + srcAmount: '2630653', + destAmount: '2626707', + }, + { + action: 'swap', + srcChainId: 42161, + destChainId: 42161, + protocol: { + name: '1inch', + displayName: '1inch', + icon: 'https://raw.githubusercontent.com/lifinance/types/main/src/assets/icons/exchanges/oneinch.png', + }, + srcAsset: { + address: '0xFd086bC7CD5C481DCC9C85ebE478A1C0b69FCbb9', + chainId: 42161, + symbol: 'USDT', + decimals: 6, + name: 'USDT', + coinKey: 'USDT', + logoURI: + 'https://raw.githubusercontent.com/trustwallet/assets/master/blockchains/ethereum/assets/0xdAC17F958D2ee523a2206206994597C13D831ec7/logo.png', + priceUSD: '1.00125', + icon: 'https://raw.githubusercontent.com/trustwallet/assets/master/blockchains/ethereum/assets/0xdAC17F958D2ee523a2206206994597C13D831ec7/logo.png', + }, + destAsset: { + address: '0x0000000000000000000000000000000000000000', + chainId: 42161, + symbol: 'ETH', + decimals: 18, + name: 'ETH', + coinKey: 'ETH', + logoURI: + 'https://raw.githubusercontent.com/trustwallet/assets/master/blockchains/ethereum/assets/0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2/logo.png', + priceUSD: '2689.39', + icon: 'https://raw.githubusercontent.com/trustwallet/assets/master/blockchains/ethereum/assets/0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2/logo.png', + }, + srcAmount: '2613573', + destAmount: '976873412881134', + }, + ], + }, + startTime: 1730925150751, + estimatedProcessingTimeInSeconds: 99, + slippagePercentage: 0, + targetContractAddress: '0x29106d08382d3c73bf477a94333c61db1142e1b6', + account: '0xc5fe6ef47965741f6f7a4734bf784bf3ae3f2452', + status: { + status: 'PENDING', + srcChain: { + chainId: 43114, + txHash: + '0x8586e162e456a23c1969573a4b79e77912705b474bc5aa0c2a63d56556623ab2', + }, + }, + }, + srcChainTxMeta: { + actionId: 1730925150740.3386, + chainId: '0xa86a', + id: '3ef01af0-9c7e-11ef-929c-517d218027d6', + origin: 'metamask', + status: 'submitted', + time: 1730925150751, + txParams: { + from: '0xc5fe6ef47965741f6f7a4734bf784bf3ae3f2452', + data: '0x3ce33bff00000000000000000000000000000000000000000000000000000000000000800000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000017bb83647a5333e00000000000000000000000000000000000000000000000000000000000000c0000000000000000000000000000000000000000000000000000000000000000d6c6966694164617074657256320000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000009600000000000000000000000001231deb6f5749ef6ce6943a275a1d3e7486f4eae0000000000000000000000001231deb6f5749ef6ce6943a275a1d3e7486f4eae000000000000000000000000000000000000000000000000000000000000a4b100000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000016029a98d5c5000000000000000000000000000000000000000000000000000000000000000014000000000000000000000000000000000000000000000000000031bced02db00000000000000000000000000068e21357555a15cd9df624f3bfb7aed0eae167e90000000000000000000000000000000000000000000000000000000000000804a6010a660000000000000000000000000000000000000000000000000000000000000060000000000000000000000000000000000000000000000000000000000000022000000000000000000000000000000000000000000000000000000000000003c07c8d02889018b288c8f3d7155bccc6f087fac6bd430d74cb798929f836fe1ae70000000000000000000000000000000000000000000000000000000000000140000000000000000000000000000000000000000000000000000000000000018000000000000000000000000000000000000000000000000000000000000000000000000000000000000000009702230a8ea53601f5cd2dc00fdbc13d4df4a8c7000000000000000000000000c5fe6ef47965741f6f7a4734bf784bf3ae3f2452000000000000000000000000000000000000000000000000000000000027f09c000000000000000000000000000000000000000000000000000000000000a4b100000000000000000000000000000000000000000000000000000000000000010000000000000000000000000000000000000000000000000000000000000001000000000000000000000000000000000000000000000000000000000000000a7374617267617465563200000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000f6d6574616d61736b2d627269646765000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000010000000000000000000000000000000000000000000000000000000000000020000000000000000000000000111111125421ca6dc452d289314280a0f8842a65000000000000000000000000111111125421ca6dc452d289314280a0f8842a6500000000000000000000000000000000000000000000000000000000000000000000000000000000000000009702230a8ea53601f5cd2dc00fdbc13d4df4a8c7000000000000000000000000000000000000000000000000016029a98d5c500000000000000000000000000000000000000000000000000000000000000000e000000000000000000000000000000000000000000000000000000000000000010000000000000000000000000000000000000000000000000000000000000048a76dfc3b000000000000000000000000000000000000000000000000000000000027f09b00000000000000003b6d0340e3ba3d5e3f98eeff5e9eddd5bd20e476202770da2a94d114000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000200000000000000000000000000000000000000000000000000000000000000a0000000000000000000000000000000000000000000000000001872bdea1b333e0000000000000000000000000000000000000000000000000000000000000000000000000000000000000000c5fe6ef47965741f6f7a4734bf784bf3ae3f2452000000000000000000000000000000000000000000000000000000000000759e0000000000000000000000001493e7b8d4dfade0a178dad9335470337a3a219a000000000000000000000000000000000000000000000000000000000027f09c000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000e00000000000000000000000000000000000000000000000000000000000000120000000000000000000000000000000000000000000000000000000000000038000000000000000000000000000000000000000000000000000000000000000180003010013030000000000000000000000000000003037c0000000000000000000000000000000000000000000000000000000000000000000000000000002407c8d02889018b288c8f3d7155bccc6f087fac6bd430d74cb798929f836fe1ae70000000000000000000000000000000000000000000000000000000000000060000000000000000000000000c5fe6ef47965741f6f7a4734bf784bf3ae3f245200000000000000000000000000000000000000000000000000000000000000010000000000000000000000000000000000000000000000000000000000000020000000000000000000000000111111125421ca6dc452d289314280a0f8842a65000000000000000000000000111111125421ca6dc452d289314280a0f8842a65000000000000000000000000fd086bc7cd5c481dcc9c85ebe478a1c0b69fcbb90000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000027ae3900000000000000000000000000000000000000000000000000000000000000e00000000000000000000000000000000000000000000000000000000000000001000000000000000000000000000000000000000000000000000000000000008883800a8e000000000000000000000000fd086bc7cd5c481dcc9c85ebe478a1c0b69fcbb9000000000000000000000000000000000000000000000000000000000027ae3900000000000000000000000000000000000000000000000000036f993610ce1138000000000000000000000042161084d0672e1d3f26a9b53e653be2084ff19c2a94d11400000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000051408a61b2fa890424911fb6896c7f34c97920f4db458717b3aadf236a5fc4ad77462476be8335db44a54d720c1f4b97035407d8b798000319982c4d9f64b7701c', + gas: '0xbcdcb', + gasLimit: '0xbcdcb', + nonce: '0x6', + to: '0x29106d08382d3c73bf477a94333c61db1142e1b6', + value: '0x017bb83647a5333e', + maxFeePerGas: '0xa5c681d00', + maxPriorityFeePerGas: '0x77359400', + type: '0x2', + }, + userEditedGasLimit: false, + verifiedOnBlockchain: false, + type: 'bridge', + networkClientId: '2fb77c4b-4515-40e2-b717-5629a086e606', + defaultGasEstimates: { + gas: '0xbcdcb', + maxFeePerGas: '0xa5c681d00', + maxPriorityFeePerGas: '0x77359400', + estimateType: 'medium', + }, + userFeeLevel: 'medium', + sendFlowHistory: [], + history: [ + { + actionId: 1730925150740.3386, + chainId: '0xa86a', + id: '3ef01af0-9c7e-11ef-929c-517d218027d6', + origin: 'metamask', + status: 'unapproved', + time: 1730925150751, + txParams: { + from: '0xc5fe6ef47965741f6f7a4734bf784bf3ae3f2452', + data: '0x3ce33bff00000000000000000000000000000000000000000000000000000000000000800000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000017bb83647a5333e00000000000000000000000000000000000000000000000000000000000000c0000000000000000000000000000000000000000000000000000000000000000d6c6966694164617074657256320000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000009600000000000000000000000001231deb6f5749ef6ce6943a275a1d3e7486f4eae0000000000000000000000001231deb6f5749ef6ce6943a275a1d3e7486f4eae000000000000000000000000000000000000000000000000000000000000a4b100000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000016029a98d5c5000000000000000000000000000000000000000000000000000000000000000014000000000000000000000000000000000000000000000000000031bced02db00000000000000000000000000068e21357555a15cd9df624f3bfb7aed0eae167e90000000000000000000000000000000000000000000000000000000000000804a6010a660000000000000000000000000000000000000000000000000000000000000060000000000000000000000000000000000000000000000000000000000000022000000000000000000000000000000000000000000000000000000000000003c07c8d02889018b288c8f3d7155bccc6f087fac6bd430d74cb798929f836fe1ae70000000000000000000000000000000000000000000000000000000000000140000000000000000000000000000000000000000000000000000000000000018000000000000000000000000000000000000000000000000000000000000000000000000000000000000000009702230a8ea53601f5cd2dc00fdbc13d4df4a8c7000000000000000000000000c5fe6ef47965741f6f7a4734bf784bf3ae3f2452000000000000000000000000000000000000000000000000000000000027f09c000000000000000000000000000000000000000000000000000000000000a4b100000000000000000000000000000000000000000000000000000000000000010000000000000000000000000000000000000000000000000000000000000001000000000000000000000000000000000000000000000000000000000000000a7374617267617465563200000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000f6d6574616d61736b2d627269646765000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000010000000000000000000000000000000000000000000000000000000000000020000000000000000000000000111111125421ca6dc452d289314280a0f8842a65000000000000000000000000111111125421ca6dc452d289314280a0f8842a6500000000000000000000000000000000000000000000000000000000000000000000000000000000000000009702230a8ea53601f5cd2dc00fdbc13d4df4a8c7000000000000000000000000000000000000000000000000016029a98d5c500000000000000000000000000000000000000000000000000000000000000000e000000000000000000000000000000000000000000000000000000000000000010000000000000000000000000000000000000000000000000000000000000048a76dfc3b000000000000000000000000000000000000000000000000000000000027f09b00000000000000003b6d0340e3ba3d5e3f98eeff5e9eddd5bd20e476202770da2a94d114000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000200000000000000000000000000000000000000000000000000000000000000a0000000000000000000000000000000000000000000000000001872bdea1b333e0000000000000000000000000000000000000000000000000000000000000000000000000000000000000000c5fe6ef47965741f6f7a4734bf784bf3ae3f2452000000000000000000000000000000000000000000000000000000000000759e0000000000000000000000001493e7b8d4dfade0a178dad9335470337a3a219a000000000000000000000000000000000000000000000000000000000027f09c000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000e00000000000000000000000000000000000000000000000000000000000000120000000000000000000000000000000000000000000000000000000000000038000000000000000000000000000000000000000000000000000000000000000180003010013030000000000000000000000000000003037c0000000000000000000000000000000000000000000000000000000000000000000000000000002407c8d02889018b288c8f3d7155bccc6f087fac6bd430d74cb798929f836fe1ae70000000000000000000000000000000000000000000000000000000000000060000000000000000000000000c5fe6ef47965741f6f7a4734bf784bf3ae3f245200000000000000000000000000000000000000000000000000000000000000010000000000000000000000000000000000000000000000000000000000000020000000000000000000000000111111125421ca6dc452d289314280a0f8842a65000000000000000000000000111111125421ca6dc452d289314280a0f8842a65000000000000000000000000fd086bc7cd5c481dcc9c85ebe478a1c0b69fcbb90000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000027ae3900000000000000000000000000000000000000000000000000000000000000e00000000000000000000000000000000000000000000000000000000000000001000000000000000000000000000000000000000000000000000000000000008883800a8e000000000000000000000000fd086bc7cd5c481dcc9c85ebe478a1c0b69fcbb9000000000000000000000000000000000000000000000000000000000027ae3900000000000000000000000000000000000000000000000000036f993610ce1138000000000000000000000042161084d0672e1d3f26a9b53e653be2084ff19c2a94d11400000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000051408a61b2fa890424911fb6896c7f34c97920f4db458717b3aadf236a5fc4ad77462476be8335db44a54d720c1f4b97035407d8b798000319982c4d9f64b7701c', + gas: '0xbcdcb', + gasLimit: '0xbcdcb', + to: '0x29106d08382d3c73bf477a94333c61db1142e1b6', + value: '0x017bb83647a5333e', + maxFeePerGas: '0xa5c681d00', + maxPriorityFeePerGas: '0x77359400', + }, + userEditedGasLimit: false, + verifiedOnBlockchain: false, + type: 'bridge', + networkClientId: '2fb77c4b-4515-40e2-b717-5629a086e606', + defaultGasEstimates: { + gas: '0xbcdcb', + maxFeePerGas: '0xa5c681d00', + maxPriorityFeePerGas: '0x77359400', + estimateType: 'medium', + }, + userFeeLevel: 'medium', + sendFlowHistory: [], + }, + [ + { + op: 'add', + path: '/txParams/nonce', + value: '0x6', + note: 'TransactionController#approveTransaction - Transaction approved', + timestamp: 1730925150919, + }, + { + op: 'add', + path: '/txParams/type', + value: '0x2', + }, + { + op: 'replace', + path: '/status', + value: 'approved', + }, + { + op: 'add', + path: '/gasFeeEstimates', + value: { + type: 'fee-market', + low: { + maxFeePerGas: '0x60db88400', + maxPriorityFeePerGas: '0x3b9aca00', + }, + medium: { + maxFeePerGas: '0x835105080', + maxPriorityFeePerGas: '0x59682f00', + }, + high: { + maxFeePerGas: '0xa5c681d00', + maxPriorityFeePerGas: '0x77359400', + }, + }, + }, + { + op: 'add', + path: '/gasFeeEstimatesLoaded', + value: true, + }, + ], + [ + { + op: 'replace', + path: '/status', + value: 'signed', + note: 'TransactionController#approveTransaction - RawTransaction added', + timestamp: 1730925150933, + }, + { + op: 'add', + path: '/r', + value: + '0xbe080a38b4ea9d4586fc525e2647deb483f47eafd54bf9a2381eb96b68cb6cc7', + }, + { + op: 'add', + path: '/s', + value: + '0x3c0a6564fc0c80a8b7a36eadaf069bbea3c97304588cb6e2d74c5f2af7ce01b1', + }, + { + op: 'add', + path: '/v', + value: '0x1', + }, + { + op: 'add', + path: '/rawTx', + value: + '0x02f90afd82a86a068477359400850a5c681d00830bcdcb9429106d08382d3c73bf477a94333c61db1142e1b688017bb83647a5333eb90a853ce33bff00000000000000000000000000000000000000000000000000000000000000800000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000017bb83647a5333e00000000000000000000000000000000000000000000000000000000000000c0000000000000000000000000000000000000000000000000000000000000000d6c6966694164617074657256320000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000009600000000000000000000000001231deb6f5749ef6ce6943a275a1d3e7486f4eae0000000000000000000000001231deb6f5749ef6ce6943a275a1d3e7486f4eae000000000000000000000000000000000000000000000000000000000000a4b100000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000016029a98d5c5000000000000000000000000000000000000000000000000000000000000000014000000000000000000000000000000000000000000000000000031bced02db00000000000000000000000000068e21357555a15cd9df624f3bfb7aed0eae167e90000000000000000000000000000000000000000000000000000000000000804a6010a660000000000000000000000000000000000000000000000000000000000000060000000000000000000000000000000000000000000000000000000000000022000000000000000000000000000000000000000000000000000000000000003c07c8d02889018b288c8f3d7155bccc6f087fac6bd430d74cb798929f836fe1ae70000000000000000000000000000000000000000000000000000000000000140000000000000000000000000000000000000000000000000000000000000018000000000000000000000000000000000000000000000000000000000000000000000000000000000000000009702230a8ea53601f5cd2dc00fdbc13d4df4a8c7000000000000000000000000c5fe6ef47965741f6f7a4734bf784bf3ae3f2452000000000000000000000000000000000000000000000000000000000027f09c000000000000000000000000000000000000000000000000000000000000a4b100000000000000000000000000000000000000000000000000000000000000010000000000000000000000000000000000000000000000000000000000000001000000000000000000000000000000000000000000000000000000000000000a7374617267617465563200000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000f6d6574616d61736b2d627269646765000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000010000000000000000000000000000000000000000000000000000000000000020000000000000000000000000111111125421ca6dc452d289314280a0f8842a65000000000000000000000000111111125421ca6dc452d289314280a0f8842a6500000000000000000000000000000000000000000000000000000000000000000000000000000000000000009702230a8ea53601f5cd2dc00fdbc13d4df4a8c7000000000000000000000000000000000000000000000000016029a98d5c500000000000000000000000000000000000000000000000000000000000000000e000000000000000000000000000000000000000000000000000000000000000010000000000000000000000000000000000000000000000000000000000000048a76dfc3b000000000000000000000000000000000000000000000000000000000027f09b00000000000000003b6d0340e3ba3d5e3f98eeff5e9eddd5bd20e476202770da2a94d114000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000200000000000000000000000000000000000000000000000000000000000000a0000000000000000000000000000000000000000000000000001872bdea1b333e0000000000000000000000000000000000000000000000000000000000000000000000000000000000000000c5fe6ef47965741f6f7a4734bf784bf3ae3f2452000000000000000000000000000000000000000000000000000000000000759e0000000000000000000000001493e7b8d4dfade0a178dad9335470337a3a219a000000000000000000000000000000000000000000000000000000000027f09c000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000e00000000000000000000000000000000000000000000000000000000000000120000000000000000000000000000000000000000000000000000000000000038000000000000000000000000000000000000000000000000000000000000000180003010013030000000000000000000000000000003037c0000000000000000000000000000000000000000000000000000000000000000000000000000002407c8d02889018b288c8f3d7155bccc6f087fac6bd430d74cb798929f836fe1ae70000000000000000000000000000000000000000000000000000000000000060000000000000000000000000c5fe6ef47965741f6f7a4734bf784bf3ae3f245200000000000000000000000000000000000000000000000000000000000000010000000000000000000000000000000000000000000000000000000000000020000000000000000000000000111111125421ca6dc452d289314280a0f8842a65000000000000000000000000111111125421ca6dc452d289314280a0f8842a65000000000000000000000000fd086bc7cd5c481dcc9c85ebe478a1c0b69fcbb90000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000027ae3900000000000000000000000000000000000000000000000000000000000000e00000000000000000000000000000000000000000000000000000000000000001000000000000000000000000000000000000000000000000000000000000008883800a8e000000000000000000000000fd086bc7cd5c481dcc9c85ebe478a1c0b69fcbb9000000000000000000000000000000000000000000000000000000000027ae3900000000000000000000000000000000000000000000000000036f993610ce1138000000000000000000000042161084d0672e1d3f26a9b53e653be2084ff19c2a94d11400000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000051408a61b2fa890424911fb6896c7f34c97920f4db458717b3aadf236a5fc4ad77462476be8335db44a54d720c1f4b97035407d8b798000319982c4d9f64b7701cc001a0be080a38b4ea9d4586fc525e2647deb483f47eafd54bf9a2381eb96b68cb6cc7a03c0a6564fc0c80a8b7a36eadaf069bbea3c97304588cb6e2d74c5f2af7ce01b1', + }, + ], + [ + { + op: 'replace', + path: '/status', + value: 'submitted', + note: 'TransactionController#approveTransaction - Transaction submitted', + timestamp: 1730925151001, + }, + { + op: 'add', + path: '/hash', + value: + '0x8586e162e456a23c1969573a4b79e77912705b474bc5aa0c2a63d56556623ab2', + }, + { + op: 'add', + path: '/submittedTime', + value: 1730925151001, + }, + ], + ], + gasFeeEstimates: { + type: 'fee-market', + low: { + maxFeePerGas: '0x60db88400', + maxPriorityFeePerGas: '0x3b9aca00', + }, + medium: { + maxFeePerGas: '0x835105080', + maxPriorityFeePerGas: '0x59682f00', + }, + high: { + maxFeePerGas: '0xa5c681d00', + maxPriorityFeePerGas: '0x77359400', + }, + }, + gasFeeEstimatesLoaded: true, + r: '0xbe080a38b4ea9d4586fc525e2647deb483f47eafd54bf9a2381eb96b68cb6cc7', + s: '0x3c0a6564fc0c80a8b7a36eadaf069bbea3c97304588cb6e2d74c5f2af7ce01b1', + v: '0x1', + rawTx: + '0x02f90afd82a86a068477359400850a5c681d00830bcdcb9429106d08382d3c73bf477a94333c61db1142e1b688017bb83647a5333eb90a853ce33bff00000000000000000000000000000000000000000000000000000000000000800000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000017bb83647a5333e00000000000000000000000000000000000000000000000000000000000000c0000000000000000000000000000000000000000000000000000000000000000d6c6966694164617074657256320000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000009600000000000000000000000001231deb6f5749ef6ce6943a275a1d3e7486f4eae0000000000000000000000001231deb6f5749ef6ce6943a275a1d3e7486f4eae000000000000000000000000000000000000000000000000000000000000a4b100000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000016029a98d5c5000000000000000000000000000000000000000000000000000000000000000014000000000000000000000000000000000000000000000000000031bced02db00000000000000000000000000068e21357555a15cd9df624f3bfb7aed0eae167e90000000000000000000000000000000000000000000000000000000000000804a6010a660000000000000000000000000000000000000000000000000000000000000060000000000000000000000000000000000000000000000000000000000000022000000000000000000000000000000000000000000000000000000000000003c07c8d02889018b288c8f3d7155bccc6f087fac6bd430d74cb798929f836fe1ae70000000000000000000000000000000000000000000000000000000000000140000000000000000000000000000000000000000000000000000000000000018000000000000000000000000000000000000000000000000000000000000000000000000000000000000000009702230a8ea53601f5cd2dc00fdbc13d4df4a8c7000000000000000000000000c5fe6ef47965741f6f7a4734bf784bf3ae3f2452000000000000000000000000000000000000000000000000000000000027f09c000000000000000000000000000000000000000000000000000000000000a4b100000000000000000000000000000000000000000000000000000000000000010000000000000000000000000000000000000000000000000000000000000001000000000000000000000000000000000000000000000000000000000000000a7374617267617465563200000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000f6d6574616d61736b2d627269646765000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000010000000000000000000000000000000000000000000000000000000000000020000000000000000000000000111111125421ca6dc452d289314280a0f8842a65000000000000000000000000111111125421ca6dc452d289314280a0f8842a6500000000000000000000000000000000000000000000000000000000000000000000000000000000000000009702230a8ea53601f5cd2dc00fdbc13d4df4a8c7000000000000000000000000000000000000000000000000016029a98d5c500000000000000000000000000000000000000000000000000000000000000000e000000000000000000000000000000000000000000000000000000000000000010000000000000000000000000000000000000000000000000000000000000048a76dfc3b000000000000000000000000000000000000000000000000000000000027f09b00000000000000003b6d0340e3ba3d5e3f98eeff5e9eddd5bd20e476202770da2a94d114000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000200000000000000000000000000000000000000000000000000000000000000a0000000000000000000000000000000000000000000000000001872bdea1b333e0000000000000000000000000000000000000000000000000000000000000000000000000000000000000000c5fe6ef47965741f6f7a4734bf784bf3ae3f2452000000000000000000000000000000000000000000000000000000000000759e0000000000000000000000001493e7b8d4dfade0a178dad9335470337a3a219a000000000000000000000000000000000000000000000000000000000027f09c000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000e00000000000000000000000000000000000000000000000000000000000000120000000000000000000000000000000000000000000000000000000000000038000000000000000000000000000000000000000000000000000000000000000180003010013030000000000000000000000000000003037c0000000000000000000000000000000000000000000000000000000000000000000000000000002407c8d02889018b288c8f3d7155bccc6f087fac6bd430d74cb798929f836fe1ae70000000000000000000000000000000000000000000000000000000000000060000000000000000000000000c5fe6ef47965741f6f7a4734bf784bf3ae3f245200000000000000000000000000000000000000000000000000000000000000010000000000000000000000000000000000000000000000000000000000000020000000000000000000000000111111125421ca6dc452d289314280a0f8842a65000000000000000000000000111111125421ca6dc452d289314280a0f8842a65000000000000000000000000fd086bc7cd5c481dcc9c85ebe478a1c0b69fcbb90000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000027ae3900000000000000000000000000000000000000000000000000000000000000e00000000000000000000000000000000000000000000000000000000000000001000000000000000000000000000000000000000000000000000000000000008883800a8e000000000000000000000000fd086bc7cd5c481dcc9c85ebe478a1c0b69fcbb9000000000000000000000000000000000000000000000000000000000027ae3900000000000000000000000000000000000000000000000000036f993610ce1138000000000000000000000042161084d0672e1d3f26a9b53e653be2084ff19c2a94d11400000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000051408a61b2fa890424911fb6896c7f34c97920f4db458717b3aadf236a5fc4ad77462476be8335db44a54d720c1f4b97035407d8b798000319982c4d9f64b7701cc001a0be080a38b4ea9d4586fc525e2647deb483f47eafd54bf9a2381eb96b68cb6cc7a03c0a6564fc0c80a8b7a36eadaf069bbea3c97304588cb6e2d74c5f2af7ce01b1', + hash: '0x8586e162e456a23c1969573a4b79e77912705b474bc5aa0c2a63d56556623ab2', + submittedTime: 1730925151001, + }, + }, + step2: { + bridgeHistoryItem: { + quote: { + requestId: '81dc4f52-6d53-4b85-a31a-f1c29fce3ea3', + srcChainId: 43114, + srcTokenAmount: '99125000000000000', + srcAsset: { + address: '0x0000000000000000000000000000000000000000', + chainId: 43114, + symbol: 'AVAX', + decimals: 18, + name: 'AVAX', + coinKey: 'AVAX', + logoURI: + 'https://static.debank.com/image/avax_token/logo_url/avax/0b9c84359c84d6bdd5bfda9c2d4c4a82.png', + priceUSD: '26.56', + icon: 'https://static.debank.com/image/avax_token/logo_url/avax/0b9c84359c84d6bdd5bfda9c2d4c4a82.png', + }, + destChainId: 42161, + destTokenAmount: '976471237080818', + destAsset: { + address: '0x0000000000000000000000000000000000000000', + chainId: 42161, + symbol: 'ETH', + decimals: 18, + name: 'ETH', + coinKey: 'ETH', + logoURI: + 'https://raw.githubusercontent.com/trustwallet/assets/master/blockchains/ethereum/assets/0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2/logo.png', + priceUSD: '2689.39', + icon: 'https://raw.githubusercontent.com/trustwallet/assets/master/blockchains/ethereum/assets/0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2/logo.png', + }, + feeData: { + metabridge: { + amount: '875000000000000', + asset: { + address: '0x0000000000000000000000000000000000000000', + chainId: 43114, + symbol: 'AVAX', + decimals: 18, + name: 'AVAX', + coinKey: 'AVAX', + logoURI: + 'https://static.debank.com/image/avax_token/logo_url/avax/0b9c84359c84d6bdd5bfda9c2d4c4a82.png', + priceUSD: '26.56', + icon: 'https://static.debank.com/image/avax_token/logo_url/avax/0b9c84359c84d6bdd5bfda9c2d4c4a82.png', + }, + }, + }, + bridgeId: 'lifi', + bridges: ['stargate'], + steps: [ + { + action: 'swap', + srcChainId: 43114, + destChainId: 43114, + protocol: { + name: '1inch', + displayName: '1inch', + icon: 'https://raw.githubusercontent.com/lifinance/types/main/src/assets/icons/exchanges/oneinch.png', + }, + srcAsset: { + address: '0x0000000000000000000000000000000000000000', + chainId: 43114, + symbol: 'AVAX', + decimals: 18, + name: 'AVAX', + coinKey: 'AVAX', + logoURI: + 'https://static.debank.com/image/avax_token/logo_url/avax/0b9c84359c84d6bdd5bfda9c2d4c4a82.png', + priceUSD: '26.56', + icon: 'https://static.debank.com/image/avax_token/logo_url/avax/0b9c84359c84d6bdd5bfda9c2d4c4a82.png', + }, + destAsset: { + address: '0x9702230A8Ea53601f5cD2dc00fDBc13d4dF4A8c7', + chainId: 43114, + symbol: 'USDt', + decimals: 6, + name: 'TetherToken', + coinKey: 'USDt', + logoURI: + 'https://static.debank.com/image/eth_token/logo_url/0xdac17f958d2ee523a2206206994597c13d831ec7/464c0de678334b8fe87327e527bc476d.png', + priceUSD: '1.00125', + icon: 'https://static.debank.com/image/eth_token/logo_url/0xdac17f958d2ee523a2206206994597c13d831ec7/464c0de678334b8fe87327e527bc476d.png', + }, + srcAmount: '99125000000000000', + destAmount: '2628919', + }, + { + action: 'bridge', + srcChainId: 43114, + destChainId: 42161, + protocol: { + name: 'stargate', + displayName: 'StargateV2 (Fast mode)', + icon: 'https://raw.githubusercontent.com/lifinance/types/5685c638772f533edad80fcb210b4bb89e30a50f/src/assets/icons/bridges/stargate.png', + }, + srcAsset: { + address: '0x9702230A8Ea53601f5cD2dc00fDBc13d4dF4A8c7', + chainId: 43114, + symbol: 'USDt', + decimals: 6, + name: 'TetherToken', + coinKey: 'USDt', + logoURI: + 'https://static.debank.com/image/eth_token/logo_url/0xdac17f958d2ee523a2206206994597c13d831ec7/464c0de678334b8fe87327e527bc476d.png', + priceUSD: '1.00125', + icon: 'https://static.debank.com/image/eth_token/logo_url/0xdac17f958d2ee523a2206206994597c13d831ec7/464c0de678334b8fe87327e527bc476d.png', + }, + destAsset: { + address: '0xFd086bC7CD5C481DCC9C85ebE478A1C0b69FCbb9', + chainId: 42161, + symbol: 'USDT', + decimals: 6, + name: 'USDT', + coinKey: 'USDT', + logoURI: + 'https://raw.githubusercontent.com/trustwallet/assets/master/blockchains/ethereum/assets/0xdAC17F958D2ee523a2206206994597C13D831ec7/logo.png', + priceUSD: '1.00125', + icon: 'https://raw.githubusercontent.com/trustwallet/assets/master/blockchains/ethereum/assets/0xdAC17F958D2ee523a2206206994597C13D831ec7/logo.png', + }, + srcAmount: '2628919', + destAmount: '2624975', + }, + { + action: 'swap', + srcChainId: 42161, + destChainId: 42161, + protocol: { + name: '1inch', + displayName: '1inch', + icon: 'https://raw.githubusercontent.com/lifinance/types/main/src/assets/icons/exchanges/oneinch.png', + }, + srcAsset: { + address: '0xFd086bC7CD5C481DCC9C85ebE478A1C0b69FCbb9', + chainId: 42161, + symbol: 'USDT', + decimals: 6, + name: 'USDT', + coinKey: 'USDT', + logoURI: + 'https://raw.githubusercontent.com/trustwallet/assets/master/blockchains/ethereum/assets/0xdAC17F958D2ee523a2206206994597C13D831ec7/logo.png', + priceUSD: '1.00125', + icon: 'https://raw.githubusercontent.com/trustwallet/assets/master/blockchains/ethereum/assets/0xdAC17F958D2ee523a2206206994597C13D831ec7/logo.png', + }, + destAsset: { + address: '0x0000000000000000000000000000000000000000', + chainId: 42161, + symbol: 'ETH', + decimals: 18, + name: 'ETH', + coinKey: 'ETH', + logoURI: + 'https://raw.githubusercontent.com/trustwallet/assets/master/blockchains/ethereum/assets/0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2/logo.png', + priceUSD: '2689.39', + icon: 'https://raw.githubusercontent.com/trustwallet/assets/master/blockchains/ethereum/assets/0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2/logo.png', + }, + srcAmount: '2611849', + destAmount: '976471237080818', + }, + ], + }, + startTime: 1730925261375, + estimatedProcessingTimeInSeconds: 99, + slippagePercentage: 0, + targetContractAddress: '0x29106d08382d3c73bf477a94333c61db1142e1b6', + account: '0xc5fe6ef47965741f6f7a4734bf784bf3ae3f2452', + status: { + status: 'PENDING', + srcChain: { + chainId: 43114, + txHash: + '0xe12fee9d2d73550236d233e8c541cee3237d7e9dc8085cd576d620fca1b659b7', + amount: '99125000000000000', + token: { + address: '0x0000000000000000000000000000000000000000', + chainId: 43114, + symbol: 'AVAX', + decimals: 18, + name: 'AVAX', + coinKey: 'AVAX', + logoURI: + 'https://static.debank.com/image/avax_token/logo_url/avax/0b9c84359c84d6bdd5bfda9c2d4c4a82.png', + priceUSD: '26.56', + icon: 'https://static.debank.com/image/avax_token/logo_url/avax/0b9c84359c84d6bdd5bfda9c2d4c4a82.png', + }, + }, + destChain: { + chainId: '42161', + token: {}, + }, + }, + }, + srcChainTxMeta: { + actionId: 1730925261366.3533, + chainId: '0xa86a', + id: '80dffcf0-9c7e-11ef-929c-517d218027d6', + origin: 'metamask', + status: 'confirmed', + time: 1730925261375, + txParams: { + from: '0xc5fe6ef47965741f6f7a4734bf784bf3ae3f2452', + data: '0x3ce33bff00000000000000000000000000000000000000000000000000000000000000800000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000017bb83647a5333e00000000000000000000000000000000000000000000000000000000000000c0000000000000000000000000000000000000000000000000000000000000000d6c6966694164617074657256320000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000009600000000000000000000000001231deb6f5749ef6ce6943a275a1d3e7486f4eae0000000000000000000000001231deb6f5749ef6ce6943a275a1d3e7486f4eae000000000000000000000000000000000000000000000000000000000000a4b100000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000016029a98d5c5000000000000000000000000000000000000000000000000000000000000000014000000000000000000000000000000000000000000000000000031bced02db00000000000000000000000000068e21357555a15cd9df624f3bfb7aed0eae167e90000000000000000000000000000000000000000000000000000000000000804a6010a660000000000000000000000000000000000000000000000000000000000000060000000000000000000000000000000000000000000000000000000000000022000000000000000000000000000000000000000000000000000000000000003c049edb1f86374f25f25bba1abda983a1ba299a1f4072203a477122ab74befd0000000000000000000000000000000000000000000000000000000000000000140000000000000000000000000000000000000000000000000000000000000018000000000000000000000000000000000000000000000000000000000000000000000000000000000000000009702230a8ea53601f5cd2dc00fdbc13d4df4a8c7000000000000000000000000c5fe6ef47965741f6f7a4734bf784bf3ae3f2452000000000000000000000000000000000000000000000000000000000027e9de000000000000000000000000000000000000000000000000000000000000a4b100000000000000000000000000000000000000000000000000000000000000010000000000000000000000000000000000000000000000000000000000000001000000000000000000000000000000000000000000000000000000000000000a7374617267617465563200000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000f6d6574616d61736b2d627269646765000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000010000000000000000000000000000000000000000000000000000000000000020000000000000000000000000111111125421ca6dc452d289314280a0f8842a65000000000000000000000000111111125421ca6dc452d289314280a0f8842a6500000000000000000000000000000000000000000000000000000000000000000000000000000000000000009702230a8ea53601f5cd2dc00fdbc13d4df4a8c7000000000000000000000000000000000000000000000000016029a98d5c500000000000000000000000000000000000000000000000000000000000000000e000000000000000000000000000000000000000000000000000000000000000010000000000000000000000000000000000000000000000000000000000000048a76dfc3b000000000000000000000000000000000000000000000000000000000027e9de00000000000000003b6d0340e3ba3d5e3f98eeff5e9eddd5bd20e476202770da2a94d114000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000200000000000000000000000000000000000000000000000000000000000000a0000000000000000000000000000000000000000000000000001872bdea1b333e0000000000000000000000000000000000000000000000000000000000000000000000000000000000000000c5fe6ef47965741f6f7a4734bf784bf3ae3f2452000000000000000000000000000000000000000000000000000000000000759e0000000000000000000000001493e7b8d4dfade0a178dad9335470337a3a219a000000000000000000000000000000000000000000000000000000000027e9de000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000e00000000000000000000000000000000000000000000000000000000000000120000000000000000000000000000000000000000000000000000000000000038000000000000000000000000000000000000000000000000000000000000000180003010013030000000000000000000000000000003037c00000000000000000000000000000000000000000000000000000000000000000000000000000024049edb1f86374f25f25bba1abda983a1ba299a1f4072203a477122ab74befd0000000000000000000000000000000000000000000000000000000000000000060000000000000000000000000c5fe6ef47965741f6f7a4734bf784bf3ae3f245200000000000000000000000000000000000000000000000000000000000000010000000000000000000000000000000000000000000000000000000000000020000000000000000000000000111111125421ca6dc452d289314280a0f8842a65000000000000000000000000111111125421ca6dc452d289314280a0f8842a65000000000000000000000000fd086bc7cd5c481dcc9c85ebe478a1c0b69fcbb90000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000027a78600000000000000000000000000000000000000000000000000000000000000e00000000000000000000000000000000000000000000000000000000000000001000000000000000000000000000000000000000000000000000000000000008883800a8e000000000000000000000000fd086bc7cd5c481dcc9c85ebe478a1c0b69fcbb9000000000000000000000000000000000000000000000000000000000027a78600000000000000000000000000000000000000000000000000036f3c9b851bf338000000000000000000000042161084d0672e1d3f26a9b53e653be2084ff19c2a94d114000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000505b476320219f1b44f1f2f21b17912641058e450ba4b94381cc082ec1c42c9d717e9ffa7b6bb9231fcddae65bf8e3bbcf0f9b5d7720347c2337670b2681b00b1b', + gas: '0xbcdae', + gasLimit: '0xbcdae', + nonce: '0x7', + to: '0x29106d08382d3c73bf477a94333c61db1142e1b6', + value: '0x017bb83647a5333e', + maxFeePerGas: '0xa5c681d00', + maxPriorityFeePerGas: '0x77359400', + type: '0x2', + }, + userEditedGasLimit: false, + verifiedOnBlockchain: true, + type: 'bridge', + networkClientId: '2fb77c4b-4515-40e2-b717-5629a086e606', + defaultGasEstimates: { + gas: '0xbcdae', + maxFeePerGas: '0xa5c681d00', + maxPriorityFeePerGas: '0x77359400', + estimateType: 'medium', + }, + userFeeLevel: 'medium', + sendFlowHistory: [], + history: [ + { + actionId: 1730925261366.3533, + chainId: '0xa86a', + id: '80dffcf0-9c7e-11ef-929c-517d218027d6', + origin: 'metamask', + status: 'unapproved', + time: 1730925261375, + txParams: { + from: '0xc5fe6ef47965741f6f7a4734bf784bf3ae3f2452', + data: '0x3ce33bff00000000000000000000000000000000000000000000000000000000000000800000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000017bb83647a5333e00000000000000000000000000000000000000000000000000000000000000c0000000000000000000000000000000000000000000000000000000000000000d6c6966694164617074657256320000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000009600000000000000000000000001231deb6f5749ef6ce6943a275a1d3e7486f4eae0000000000000000000000001231deb6f5749ef6ce6943a275a1d3e7486f4eae000000000000000000000000000000000000000000000000000000000000a4b100000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000016029a98d5c5000000000000000000000000000000000000000000000000000000000000000014000000000000000000000000000000000000000000000000000031bced02db00000000000000000000000000068e21357555a15cd9df624f3bfb7aed0eae167e90000000000000000000000000000000000000000000000000000000000000804a6010a660000000000000000000000000000000000000000000000000000000000000060000000000000000000000000000000000000000000000000000000000000022000000000000000000000000000000000000000000000000000000000000003c049edb1f86374f25f25bba1abda983a1ba299a1f4072203a477122ab74befd0000000000000000000000000000000000000000000000000000000000000000140000000000000000000000000000000000000000000000000000000000000018000000000000000000000000000000000000000000000000000000000000000000000000000000000000000009702230a8ea53601f5cd2dc00fdbc13d4df4a8c7000000000000000000000000c5fe6ef47965741f6f7a4734bf784bf3ae3f2452000000000000000000000000000000000000000000000000000000000027e9de000000000000000000000000000000000000000000000000000000000000a4b100000000000000000000000000000000000000000000000000000000000000010000000000000000000000000000000000000000000000000000000000000001000000000000000000000000000000000000000000000000000000000000000a7374617267617465563200000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000f6d6574616d61736b2d627269646765000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000010000000000000000000000000000000000000000000000000000000000000020000000000000000000000000111111125421ca6dc452d289314280a0f8842a65000000000000000000000000111111125421ca6dc452d289314280a0f8842a6500000000000000000000000000000000000000000000000000000000000000000000000000000000000000009702230a8ea53601f5cd2dc00fdbc13d4df4a8c7000000000000000000000000000000000000000000000000016029a98d5c500000000000000000000000000000000000000000000000000000000000000000e000000000000000000000000000000000000000000000000000000000000000010000000000000000000000000000000000000000000000000000000000000048a76dfc3b000000000000000000000000000000000000000000000000000000000027e9de00000000000000003b6d0340e3ba3d5e3f98eeff5e9eddd5bd20e476202770da2a94d114000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000200000000000000000000000000000000000000000000000000000000000000a0000000000000000000000000000000000000000000000000001872bdea1b333e0000000000000000000000000000000000000000000000000000000000000000000000000000000000000000c5fe6ef47965741f6f7a4734bf784bf3ae3f2452000000000000000000000000000000000000000000000000000000000000759e0000000000000000000000001493e7b8d4dfade0a178dad9335470337a3a219a000000000000000000000000000000000000000000000000000000000027e9de000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000e00000000000000000000000000000000000000000000000000000000000000120000000000000000000000000000000000000000000000000000000000000038000000000000000000000000000000000000000000000000000000000000000180003010013030000000000000000000000000000003037c00000000000000000000000000000000000000000000000000000000000000000000000000000024049edb1f86374f25f25bba1abda983a1ba299a1f4072203a477122ab74befd0000000000000000000000000000000000000000000000000000000000000000060000000000000000000000000c5fe6ef47965741f6f7a4734bf784bf3ae3f245200000000000000000000000000000000000000000000000000000000000000010000000000000000000000000000000000000000000000000000000000000020000000000000000000000000111111125421ca6dc452d289314280a0f8842a65000000000000000000000000111111125421ca6dc452d289314280a0f8842a65000000000000000000000000fd086bc7cd5c481dcc9c85ebe478a1c0b69fcbb90000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000027a78600000000000000000000000000000000000000000000000000000000000000e00000000000000000000000000000000000000000000000000000000000000001000000000000000000000000000000000000000000000000000000000000008883800a8e000000000000000000000000fd086bc7cd5c481dcc9c85ebe478a1c0b69fcbb9000000000000000000000000000000000000000000000000000000000027a78600000000000000000000000000000000000000000000000000036f3c9b851bf338000000000000000000000042161084d0672e1d3f26a9b53e653be2084ff19c2a94d114000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000505b476320219f1b44f1f2f21b17912641058e450ba4b94381cc082ec1c42c9d717e9ffa7b6bb9231fcddae65bf8e3bbcf0f9b5d7720347c2337670b2681b00b1b', + gas: '0xbcdae', + gasLimit: '0xbcdae', + to: '0x29106d08382d3c73bf477a94333c61db1142e1b6', + value: '0x017bb83647a5333e', + maxFeePerGas: '0xa5c681d00', + maxPriorityFeePerGas: '0x77359400', + }, + userEditedGasLimit: false, + verifiedOnBlockchain: false, + type: 'bridge', + networkClientId: '2fb77c4b-4515-40e2-b717-5629a086e606', + defaultGasEstimates: { + gas: '0xbcdae', + maxFeePerGas: '0xa5c681d00', + maxPriorityFeePerGas: '0x77359400', + estimateType: 'medium', + }, + userFeeLevel: 'medium', + sendFlowHistory: [], + }, + [ + { + op: 'add', + path: '/txParams/nonce', + value: '0x7', + note: 'TransactionController#approveTransaction - Transaction approved', + timestamp: 1730925261449, + }, + { + op: 'add', + path: '/txParams/type', + value: '0x2', + }, + { + op: 'replace', + path: '/status', + value: 'approved', + }, + ], + [ + { + op: 'replace', + path: '/status', + value: 'signed', + note: 'TransactionController#approveTransaction - RawTransaction added', + timestamp: 1730925261459, + }, + { + op: 'add', + path: '/r', + value: + '0xfd3ca699de97aabaefb3b5c51dea239efaf3c01aa65be1a2fa5d4e1931eba1e8', + }, + { + op: 'add', + path: '/s', + value: + '0x5c63a89126570ccb0a1ac39bde25c56c20c76fcf4c94fd61588989eb05f723ad', + }, + { + op: 'add', + path: '/v', + value: '0x0', + }, + { + op: 'add', + path: '/rawTx', + value: + '0x02f90afd82a86a078477359400850a5c681d00830bcdae9429106d08382d3c73bf477a94333c61db1142e1b688017bb83647a5333eb90a853ce33bff00000000000000000000000000000000000000000000000000000000000000800000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000017bb83647a5333e00000000000000000000000000000000000000000000000000000000000000c0000000000000000000000000000000000000000000000000000000000000000d6c6966694164617074657256320000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000009600000000000000000000000001231deb6f5749ef6ce6943a275a1d3e7486f4eae0000000000000000000000001231deb6f5749ef6ce6943a275a1d3e7486f4eae000000000000000000000000000000000000000000000000000000000000a4b100000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000016029a98d5c5000000000000000000000000000000000000000000000000000000000000000014000000000000000000000000000000000000000000000000000031bced02db00000000000000000000000000068e21357555a15cd9df624f3bfb7aed0eae167e90000000000000000000000000000000000000000000000000000000000000804a6010a660000000000000000000000000000000000000000000000000000000000000060000000000000000000000000000000000000000000000000000000000000022000000000000000000000000000000000000000000000000000000000000003c049edb1f86374f25f25bba1abda983a1ba299a1f4072203a477122ab74befd0000000000000000000000000000000000000000000000000000000000000000140000000000000000000000000000000000000000000000000000000000000018000000000000000000000000000000000000000000000000000000000000000000000000000000000000000009702230a8ea53601f5cd2dc00fdbc13d4df4a8c7000000000000000000000000c5fe6ef47965741f6f7a4734bf784bf3ae3f2452000000000000000000000000000000000000000000000000000000000027e9de000000000000000000000000000000000000000000000000000000000000a4b100000000000000000000000000000000000000000000000000000000000000010000000000000000000000000000000000000000000000000000000000000001000000000000000000000000000000000000000000000000000000000000000a7374617267617465563200000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000f6d6574616d61736b2d627269646765000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000010000000000000000000000000000000000000000000000000000000000000020000000000000000000000000111111125421ca6dc452d289314280a0f8842a65000000000000000000000000111111125421ca6dc452d289314280a0f8842a6500000000000000000000000000000000000000000000000000000000000000000000000000000000000000009702230a8ea53601f5cd2dc00fdbc13d4df4a8c7000000000000000000000000000000000000000000000000016029a98d5c500000000000000000000000000000000000000000000000000000000000000000e000000000000000000000000000000000000000000000000000000000000000010000000000000000000000000000000000000000000000000000000000000048a76dfc3b000000000000000000000000000000000000000000000000000000000027e9de00000000000000003b6d0340e3ba3d5e3f98eeff5e9eddd5bd20e476202770da2a94d114000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000200000000000000000000000000000000000000000000000000000000000000a0000000000000000000000000000000000000000000000000001872bdea1b333e0000000000000000000000000000000000000000000000000000000000000000000000000000000000000000c5fe6ef47965741f6f7a4734bf784bf3ae3f2452000000000000000000000000000000000000000000000000000000000000759e0000000000000000000000001493e7b8d4dfade0a178dad9335470337a3a219a000000000000000000000000000000000000000000000000000000000027e9de000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000e00000000000000000000000000000000000000000000000000000000000000120000000000000000000000000000000000000000000000000000000000000038000000000000000000000000000000000000000000000000000000000000000180003010013030000000000000000000000000000003037c00000000000000000000000000000000000000000000000000000000000000000000000000000024049edb1f86374f25f25bba1abda983a1ba299a1f4072203a477122ab74befd0000000000000000000000000000000000000000000000000000000000000000060000000000000000000000000c5fe6ef47965741f6f7a4734bf784bf3ae3f245200000000000000000000000000000000000000000000000000000000000000010000000000000000000000000000000000000000000000000000000000000020000000000000000000000000111111125421ca6dc452d289314280a0f8842a65000000000000000000000000111111125421ca6dc452d289314280a0f8842a65000000000000000000000000fd086bc7cd5c481dcc9c85ebe478a1c0b69fcbb90000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000027a78600000000000000000000000000000000000000000000000000000000000000e00000000000000000000000000000000000000000000000000000000000000001000000000000000000000000000000000000000000000000000000000000008883800a8e000000000000000000000000fd086bc7cd5c481dcc9c85ebe478a1c0b69fcbb9000000000000000000000000000000000000000000000000000000000027a78600000000000000000000000000000000000000000000000000036f3c9b851bf338000000000000000000000042161084d0672e1d3f26a9b53e653be2084ff19c2a94d114000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000505b476320219f1b44f1f2f21b17912641058e450ba4b94381cc082ec1c42c9d717e9ffa7b6bb9231fcddae65bf8e3bbcf0f9b5d7720347c2337670b2681b00b1bc080a0fd3ca699de97aabaefb3b5c51dea239efaf3c01aa65be1a2fa5d4e1931eba1e8a05c63a89126570ccb0a1ac39bde25c56c20c76fcf4c94fd61588989eb05f723ad', + }, + ], + [ + { + op: 'replace', + path: '/status', + value: 'submitted', + note: 'TransactionController#approveTransaction - Transaction submitted', + timestamp: 1730925261526, + }, + { + op: 'add', + path: '/gasFeeEstimates', + value: { + type: 'fee-market', + low: { + maxFeePerGas: '0x60db88400', + maxPriorityFeePerGas: '0x3b9aca00', + }, + medium: { + maxFeePerGas: '0x835105080', + maxPriorityFeePerGas: '0x59682f00', + }, + high: { + maxFeePerGas: '0xa5c681d00', + maxPriorityFeePerGas: '0x77359400', + }, + }, + }, + { + op: 'add', + path: '/gasFeeEstimatesLoaded', + value: true, + }, + { + op: 'add', + path: '/hash', + value: + '0xe12fee9d2d73550236d233e8c541cee3237d7e9dc8085cd576d620fca1b659b7', + }, + { + op: 'add', + path: '/submittedTime', + value: 1730925261525, + }, + ], + [ + { + op: 'replace', + path: '/verifiedOnBlockchain', + value: true, + note: 'PendingTransactionTracker:#onTransactionConfirmed - Transaction confirmed', + timestamp: 1730925278122, + }, + { + op: 'replace', + path: '/status', + value: 'confirmed', + }, + { + op: 'add', + path: '/baseFeePerGas', + value: '0x5d21dba00', + }, + { + op: 'add', + path: '/blockTimestamp', + value: '0x672bd2cd', + }, + { + op: 'add', + path: '/txReceipt', + value: { + blockHash: + '0x158b659f69b0d465b88ad768147057f9c8dc01ae0631e88b04d9406a19fcc9f6', + blockNumber: '0x324d0fb', + contractAddress: null, + cumulativeGasUsed: '0x160ccc', + effectiveGasPrice: '0x649534e00', + from: '0xc5fe6ef47965741f6f7a4734bf784bf3ae3f2452', + gasUsed: '0xa1211', + logs: [ + { + address: '0x68e21357555a15cd9df624f3bfb7aed0eae167e9', + blockHash: + '0x158b659f69b0d465b88ad768147057f9c8dc01ae0631e88b04d9406a19fcc9f6', + blockNumber: '0x324d0fb', + data: '0x00000000000000000000000000000000000000000000000000031bced02db000', + logIndex: '0x27', + removed: false, + topics: [ + '0x3d0ce9bfc3ed7d6862dbb28b2dea94561fe714a1b4d019aa8af39730d1ad7c3d', + '0x000000000000000000000000f3e6bb4227c0040e0e274388269ee3cec1de8447', + ], + transactionHash: + '0xe12fee9d2d73550236d233e8c541cee3237d7e9dc8085cd576d620fca1b659b7', + transactionIndex: '0x5', + }, + { + address: '0xf3e6bb4227c0040e0e274388269ee3cec1de8447', + blockHash: + '0x158b659f69b0d465b88ad768147057f9c8dc01ae0631e88b04d9406a19fcc9f6', + blockNumber: '0x324d0fb', + data: '0x000000000000000000000000000000000000000000000000000000000000000000000000000000000000000068e21357555a15cd9df624f3bfb7aed0eae167e900000000000000000000000000000000000000000000000000031bced02db000', + logIndex: '0x28', + removed: false, + topics: [ + '0x6ded982279c8387ad8a63e73385031a3807c1862e633f06e09d11bcb6e282f60', + ], + transactionHash: + '0xe12fee9d2d73550236d233e8c541cee3237d7e9dc8085cd576d620fca1b659b7', + transactionIndex: '0x5', + }, + { + address: '0xb31f66aa3c1e785363f0875a1b74e27b85fd66c7', + blockHash: + '0x158b659f69b0d465b88ad768147057f9c8dc01ae0631e88b04d9406a19fcc9f6', + blockNumber: '0x324d0fb', + data: '0x000000000000000000000000000000000000000000000000016029a98d5c5000', + logIndex: '0x29', + removed: false, + topics: [ + '0xe1fffcc4923d04b559f4d29a8bfc6cda04eb5b0d3c460751c2402c5c5cc9109c', + '0x000000000000000000000000111111125421ca6dc452d289314280a0f8842a65', + ], + transactionHash: + '0xe12fee9d2d73550236d233e8c541cee3237d7e9dc8085cd576d620fca1b659b7', + transactionIndex: '0x5', + }, + { + address: '0xb31f66aa3c1e785363f0875a1b74e27b85fd66c7', + blockHash: + '0x158b659f69b0d465b88ad768147057f9c8dc01ae0631e88b04d9406a19fcc9f6', + blockNumber: '0x324d0fb', + data: '0x000000000000000000000000000000000000000000000000016029a98d5c5000', + logIndex: '0x2a', + removed: false, + topics: [ + '0xddf252ad1be2c89b69c2b068fc378daa952ba7f163c4a11628f55a4df523b3ef', + '0x000000000000000000000000111111125421ca6dc452d289314280a0f8842a65', + '0x000000000000000000000000e3ba3d5e3f98eeff5e9eddd5bd20e476202770da', + ], + transactionHash: + '0xe12fee9d2d73550236d233e8c541cee3237d7e9dc8085cd576d620fca1b659b7', + transactionIndex: '0x5', + }, + { + address: '0x9702230a8ea53601f5cd2dc00fdbc13d4df4a8c7', + blockHash: + '0x158b659f69b0d465b88ad768147057f9c8dc01ae0631e88b04d9406a19fcc9f6', + blockNumber: '0x324d0fb', + data: '0x0000000000000000000000000000000000000000000000000000000000281d37', + logIndex: '0x2b', + removed: false, + topics: [ + '0xddf252ad1be2c89b69c2b068fc378daa952ba7f163c4a11628f55a4df523b3ef', + '0x000000000000000000000000e3ba3d5e3f98eeff5e9eddd5bd20e476202770da', + '0x0000000000000000000000001231deb6f5749ef6ce6943a275a1d3e7486f4eae', + ], + transactionHash: + '0xe12fee9d2d73550236d233e8c541cee3237d7e9dc8085cd576d620fca1b659b7', + transactionIndex: '0x5', + }, + { + address: '0xe3ba3d5e3f98eeff5e9eddd5bd20e476202770da', + blockHash: + '0x158b659f69b0d465b88ad768147057f9c8dc01ae0631e88b04d9406a19fcc9f6', + blockNumber: '0x324d0fb', + data: '0x0000000000000000000000000000000000000000000000000000000ee03b968f00000000000000000000000000000000000000000000008235a12766ecdf66da', + logIndex: '0x2c', + removed: false, + topics: [ + '0x1c411e9a96e071241c2f21f7726b17ae89e3cab4c78be50e062b03a9fffbbad1', + ], + transactionHash: + '0xe12fee9d2d73550236d233e8c541cee3237d7e9dc8085cd576d620fca1b659b7', + transactionIndex: '0x5', + }, + { + address: '0xe3ba3d5e3f98eeff5e9eddd5bd20e476202770da', + blockHash: + '0x158b659f69b0d465b88ad768147057f9c8dc01ae0631e88b04d9406a19fcc9f6', + blockNumber: '0x324d0fb', + data: '0x0000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000016029a98d5c50000000000000000000000000000000000000000000000000000000000000281d370000000000000000000000000000000000000000000000000000000000000000', + logIndex: '0x2d', + removed: false, + topics: [ + '0xd78ad95fa46c994b6551d0da85fc275fe613ce37657fb8d5e3d130840159d822', + '0x000000000000000000000000111111125421ca6dc452d289314280a0f8842a65', + '0x0000000000000000000000001231deb6f5749ef6ce6943a275a1d3e7486f4eae', + ], + transactionHash: + '0xe12fee9d2d73550236d233e8c541cee3237d7e9dc8085cd576d620fca1b659b7', + transactionIndex: '0x5', + }, + { + address: '0x1231deb6f5749ef6ce6943a275a1d3e7486f4eae', + blockHash: + '0x158b659f69b0d465b88ad768147057f9c8dc01ae0631e88b04d9406a19fcc9f6', + blockNumber: '0x324d0fb', + data: '0x49edb1f86374f25f25bba1abda983a1ba299a1f4072203a477122ab74befd000000000000000000000000000111111125421ca6dc452d289314280a0f8842a6500000000000000000000000000000000000000000000000000000000000000000000000000000000000000009702230a8ea53601f5cd2dc00fdbc13d4df4a8c7000000000000000000000000000000000000000000000000016029a98d5c50000000000000000000000000000000000000000000000000000000000000281d3700000000000000000000000000000000000000000000000000000000672bd2cd', + logIndex: '0x2e', + removed: false, + topics: [ + '0x7bfdfdb5e3a3776976e53cb0607060f54c5312701c8cba1155cc4d5394440b38', + ], + transactionHash: + '0xe12fee9d2d73550236d233e8c541cee3237d7e9dc8085cd576d620fca1b659b7', + transactionIndex: '0x5', + }, + { + address: '0x9702230a8ea53601f5cd2dc00fdbc13d4df4a8c7', + blockHash: + '0x158b659f69b0d465b88ad768147057f9c8dc01ae0631e88b04d9406a19fcc9f6', + blockNumber: '0x324d0fb', + data: '0x0000000000000000000000000000000000000000000000000000000000281d37', + logIndex: '0x2f', + removed: false, + topics: [ + '0xddf252ad1be2c89b69c2b068fc378daa952ba7f163c4a11628f55a4df523b3ef', + '0x0000000000000000000000001231deb6f5749ef6ce6943a275a1d3e7486f4eae', + '0x00000000000000000000000012dc9256acc9895b076f6638d628382881e62cee', + ], + transactionHash: + '0xe12fee9d2d73550236d233e8c541cee3237d7e9dc8085cd576d620fca1b659b7', + transactionIndex: '0x5', + }, + { + address: '0x9702230a8ea53601f5cd2dc00fdbc13d4df4a8c7', + blockHash: + '0x158b659f69b0d465b88ad768147057f9c8dc01ae0631e88b04d9406a19fcc9f6', + blockNumber: '0x324d0fb', + data: '0xffffffffffffffffffffffffffffffffffffffffffffffffffffff55e55aa213', + logIndex: '0x30', + removed: false, + topics: [ + '0x8c5be1e5ebec7d5bd14f71427d1e84f3dd0314c0f7b2291e5b200ac8c7c3b925', + '0x0000000000000000000000001231deb6f5749ef6ce6943a275a1d3e7486f4eae', + '0x00000000000000000000000012dc9256acc9895b076f6638d628382881e62cee', + ], + transactionHash: + '0xe12fee9d2d73550236d233e8c541cee3237d7e9dc8085cd576d620fca1b659b7', + transactionIndex: '0x5', + }, + { + address: '0x197d1333dea5fe0d6600e9b396c7f1b1cfcc558a', + blockHash: + '0x158b659f69b0d465b88ad768147057f9c8dc01ae0631e88b04d9406a19fcc9f6', + blockNumber: '0x324d0fb', + data: '0x00000000000000000000000090e595783e43eb89ff07f63d27b8430e6b44bd9c000000000000000000000000000000000000000000000000001445775143bee0', + logIndex: '0x31', + removed: false, + topics: [ + '0x61ed099e74a97a1d7f8bb0952a88ca8b7b8ebd00c126ea04671f92a81213318a', + ], + transactionHash: + '0xe12fee9d2d73550236d233e8c541cee3237d7e9dc8085cd576d620fca1b659b7', + transactionIndex: '0x5', + }, + { + address: '0x197d1333dea5fe0d6600e9b396c7f1b1cfcc558a', + blockHash: + '0x158b659f69b0d465b88ad768147057f9c8dc01ae0631e88b04d9406a19fcc9f6', + blockNumber: '0x324d0fb', + data: '0x000000000000000000000000000000000000000000000000000000000000006000000000000000000000000000000000000000000000000000000000000000c000000000000000000000000000000000000000000000000000000000000000e00000000000000000000000000000000000000000000000000000000000000002000000000000000000000000252b234545e154543ad2784c7111eb90406be836000000000000000000000000a59ba433ac34d2927232918ef5b2eaafcf130ba500000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000002000000000000000000000000000000000000000000000000000216a34c6bba2f000000000000000000000000000000000000000000000000000216a34c6bba2f', + logIndex: '0x32', + removed: false, + topics: [ + '0x07ea52d82345d6e838192107d8fd7123d9c2ec8e916cd0aad13fd2b60db24644', + ], + transactionHash: + '0xe12fee9d2d73550236d233e8c541cee3237d7e9dc8085cd576d620fca1b659b7', + transactionIndex: '0x5', + }, + { + address: '0x1a44076050125825900e736c501f859c50fe728c', + blockHash: + '0x158b659f69b0d465b88ad768147057f9c8dc01ae0631e88b04d9406a19fcc9f6', + blockNumber: '0x324d0fb', + data: '0x00000000000000000000000000000000000000000000000000000000000000600000000000000000000000000000000000000000000000000000000000000380000000000000000000000000197d1333dea5fe0d6600e9b396c7f1b1cfcc558a00000000000000000000000000000000000000000000000000000000000002fc010000000000001eb30000759a00000000000000000000000017e450be3ba9557f2378e20d64ad417e59ef9a340000759e00000000000000000000000019cfce47ed54a88614648dc3f19a5980097007dd0a668d7b26be67905413d7478cff17283cd09f3076ec95fd14469766b9ec837f0100020000000000000000000000001493e7b8d4dfade0a178dad9335470337a3a219a0000000000280dcf0000000000000000000000001231deb6f5749ef6ce6943a275a1d3e7486f4eae49edb1f86374f25f25bba1abda983a1ba299a1f4072203a477122ab74befd0000000000000000000000000000000000000000000000000000000000000000060000000000000000000000000c5fe6ef47965741f6f7a4734bf784bf3ae3f245200000000000000000000000000000000000000000000000000000000000000010000000000000000000000000000000000000000000000000000000000000020000000000000000000000000111111125421ca6dc452d289314280a0f8842a65000000000000000000000000111111125421ca6dc452d289314280a0f8842a65000000000000000000000000fd086bc7cd5c481dcc9c85ebe478a1c0b69fcbb90000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000027a78600000000000000000000000000000000000000000000000000000000000000e00000000000000000000000000000000000000000000000000000000000000001000000000000000000000000000000000000000000000000000000000000008883800a8e000000000000000000000000fd086bc7cd5c481dcc9c85ebe478a1c0b69fcbb9000000000000000000000000000000000000000000000000000000000027a78600000000000000000000000000000000000000000000000000036f3c9b851bf338000000000000000000000042161084d0672e1d3f26a9b53e653be2084ff19c2a94d11400000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000002c000301001101000000000000000000000000000249f0010013030000000000000000000000000000003037c00000000000000000000000000000000000000000', + logIndex: '0x33', + removed: false, + topics: [ + '0x1ab700d4ced0c005b164c0f789fd09fcbb0156d4c2041b8a3bfbcd961cd1567f', + ], + transactionHash: + '0xe12fee9d2d73550236d233e8c541cee3237d7e9dc8085cd576d620fca1b659b7', + transactionIndex: '0x5', + }, + { + address: '0x12dc9256acc9895b076f6638d628382881e62cee', + blockHash: + '0x158b659f69b0d465b88ad768147057f9c8dc01ae0631e88b04d9406a19fcc9f6', + blockNumber: '0x324d0fb', + data: '0x000000000000000000000000000000000000000000000000000000000000759e0000000000000000000000000000000000000000000000000000000000281d370000000000000000000000000000000000000000000000000000000000280dcf', + logIndex: '0x34', + removed: false, + topics: [ + '0x85496b760a4b7f8d66384b9df21b381f5d1b1e79f229a47aaf4c232edc2fe59a', + '0x0a668d7b26be67905413d7478cff17283cd09f3076ec95fd14469766b9ec837f', + '0x0000000000000000000000001231deb6f5749ef6ce6943a275a1d3e7486f4eae', + ], + transactionHash: + '0xe12fee9d2d73550236d233e8c541cee3237d7e9dc8085cd576d620fca1b659b7', + transactionIndex: '0x5', + }, + { + address: '0x1231deb6f5749ef6ce6943a275a1d3e7486f4eae', + blockHash: + '0x158b659f69b0d465b88ad768147057f9c8dc01ae0631e88b04d9406a19fcc9f6', + blockNumber: '0x324d0fb', + data: '0x000000000000000000000000000000000000000000000000000000000000002049edb1f86374f25f25bba1abda983a1ba299a1f4072203a477122ab74befd0000000000000000000000000000000000000000000000000000000000000000140000000000000000000000000000000000000000000000000000000000000018000000000000000000000000000000000000000000000000000000000000000000000000000000000000000009702230a8ea53601f5cd2dc00fdbc13d4df4a8c7000000000000000000000000c5fe6ef47965741f6f7a4734bf784bf3ae3f24520000000000000000000000000000000000000000000000000000000000281d37000000000000000000000000000000000000000000000000000000000000a4b100000000000000000000000000000000000000000000000000000000000000010000000000000000000000000000000000000000000000000000000000000001000000000000000000000000000000000000000000000000000000000000000a7374617267617465563200000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000f6d6574616d61736b2d6272696467650000000000000000000000000000000000', + logIndex: '0x35', + removed: false, + topics: [ + '0xcba69f43792f9f399347222505213b55af8e0b0b54b893085c2e27ecbe1644f1', + ], + transactionHash: + '0xe12fee9d2d73550236d233e8c541cee3237d7e9dc8085cd576d620fca1b659b7', + transactionIndex: '0x5', + }, + { + address: '0xf3e6bb4227c0040e0e274388269ee3cec1de8447', + blockHash: + '0x158b659f69b0d465b88ad768147057f9c8dc01ae0631e88b04d9406a19fcc9f6', + blockNumber: '0x324d0fb', + data: '0x000000000000000000000000c5fe6ef47965741f6f7a4734bf784bf3ae3f24520000000000000000000000001231deb6f5749ef6ce6943a275a1d3e7486f4eae000000000000000000000000000000000000000000000000000000000000a4b100000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000016029a98d5c5000', + logIndex: '0x36', + removed: false, + topics: [ + '0x831bac9533a2034226daa21109dbd4f887674f0fe4877e1a8b35b3ffe1bdce76', + ], + transactionHash: + '0xe12fee9d2d73550236d233e8c541cee3237d7e9dc8085cd576d620fca1b659b7', + transactionIndex: '0x5', + }, + ], + logsBloom: + '0x00200002000000000000000080600000000000000000000020000000001000000000000004000000004004000002008000000000100000000000000000200080000082000000002000000008000000200004000000000010000004008000000004000000804008008000000000000001002000022010000000000014000000000000020808000000000000080000000000000011000000080004005000000008060081028000000080100040080000000000000000400000004540002000000008081022002008000000000080000000800000000000001000000000000000000010040000000000000000800000002008000400018004420800000000000000', + status: '0x1', + to: '0x29106d08382d3c73bf477a94333c61db1142e1b6', + transactionHash: + '0xe12fee9d2d73550236d233e8c541cee3237d7e9dc8085cd576d620fca1b659b7', + transactionIndex: '0x5', + type: '0x2', + }, + }, + ], + ], + r: '0xfd3ca699de97aabaefb3b5c51dea239efaf3c01aa65be1a2fa5d4e1931eba1e8', + s: '0x5c63a89126570ccb0a1ac39bde25c56c20c76fcf4c94fd61588989eb05f723ad', + v: '0x0', + rawTx: + '0x02f90afd82a86a078477359400850a5c681d00830bcdae9429106d08382d3c73bf477a94333c61db1142e1b688017bb83647a5333eb90a853ce33bff00000000000000000000000000000000000000000000000000000000000000800000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000017bb83647a5333e00000000000000000000000000000000000000000000000000000000000000c0000000000000000000000000000000000000000000000000000000000000000d6c6966694164617074657256320000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000009600000000000000000000000001231deb6f5749ef6ce6943a275a1d3e7486f4eae0000000000000000000000001231deb6f5749ef6ce6943a275a1d3e7486f4eae000000000000000000000000000000000000000000000000000000000000a4b100000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000016029a98d5c5000000000000000000000000000000000000000000000000000000000000000014000000000000000000000000000000000000000000000000000031bced02db00000000000000000000000000068e21357555a15cd9df624f3bfb7aed0eae167e90000000000000000000000000000000000000000000000000000000000000804a6010a660000000000000000000000000000000000000000000000000000000000000060000000000000000000000000000000000000000000000000000000000000022000000000000000000000000000000000000000000000000000000000000003c049edb1f86374f25f25bba1abda983a1ba299a1f4072203a477122ab74befd0000000000000000000000000000000000000000000000000000000000000000140000000000000000000000000000000000000000000000000000000000000018000000000000000000000000000000000000000000000000000000000000000000000000000000000000000009702230a8ea53601f5cd2dc00fdbc13d4df4a8c7000000000000000000000000c5fe6ef47965741f6f7a4734bf784bf3ae3f2452000000000000000000000000000000000000000000000000000000000027e9de000000000000000000000000000000000000000000000000000000000000a4b100000000000000000000000000000000000000000000000000000000000000010000000000000000000000000000000000000000000000000000000000000001000000000000000000000000000000000000000000000000000000000000000a7374617267617465563200000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000f6d6574616d61736b2d627269646765000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000010000000000000000000000000000000000000000000000000000000000000020000000000000000000000000111111125421ca6dc452d289314280a0f8842a65000000000000000000000000111111125421ca6dc452d289314280a0f8842a6500000000000000000000000000000000000000000000000000000000000000000000000000000000000000009702230a8ea53601f5cd2dc00fdbc13d4df4a8c7000000000000000000000000000000000000000000000000016029a98d5c500000000000000000000000000000000000000000000000000000000000000000e000000000000000000000000000000000000000000000000000000000000000010000000000000000000000000000000000000000000000000000000000000048a76dfc3b000000000000000000000000000000000000000000000000000000000027e9de00000000000000003b6d0340e3ba3d5e3f98eeff5e9eddd5bd20e476202770da2a94d114000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000200000000000000000000000000000000000000000000000000000000000000a0000000000000000000000000000000000000000000000000001872bdea1b333e0000000000000000000000000000000000000000000000000000000000000000000000000000000000000000c5fe6ef47965741f6f7a4734bf784bf3ae3f2452000000000000000000000000000000000000000000000000000000000000759e0000000000000000000000001493e7b8d4dfade0a178dad9335470337a3a219a000000000000000000000000000000000000000000000000000000000027e9de000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000e00000000000000000000000000000000000000000000000000000000000000120000000000000000000000000000000000000000000000000000000000000038000000000000000000000000000000000000000000000000000000000000000180003010013030000000000000000000000000000003037c00000000000000000000000000000000000000000000000000000000000000000000000000000024049edb1f86374f25f25bba1abda983a1ba299a1f4072203a477122ab74befd0000000000000000000000000000000000000000000000000000000000000000060000000000000000000000000c5fe6ef47965741f6f7a4734bf784bf3ae3f245200000000000000000000000000000000000000000000000000000000000000010000000000000000000000000000000000000000000000000000000000000020000000000000000000000000111111125421ca6dc452d289314280a0f8842a65000000000000000000000000111111125421ca6dc452d289314280a0f8842a65000000000000000000000000fd086bc7cd5c481dcc9c85ebe478a1c0b69fcbb90000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000027a78600000000000000000000000000000000000000000000000000000000000000e00000000000000000000000000000000000000000000000000000000000000001000000000000000000000000000000000000000000000000000000000000008883800a8e000000000000000000000000fd086bc7cd5c481dcc9c85ebe478a1c0b69fcbb9000000000000000000000000000000000000000000000000000000000027a78600000000000000000000000000000000000000000000000000036f3c9b851bf338000000000000000000000042161084d0672e1d3f26a9b53e653be2084ff19c2a94d114000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000505b476320219f1b44f1f2f21b17912641058e450ba4b94381cc082ec1c42c9d717e9ffa7b6bb9231fcddae65bf8e3bbcf0f9b5d7720347c2337670b2681b00b1bc080a0fd3ca699de97aabaefb3b5c51dea239efaf3c01aa65be1a2fa5d4e1931eba1e8a05c63a89126570ccb0a1ac39bde25c56c20c76fcf4c94fd61588989eb05f723ad', + gasFeeEstimates: { + type: 'fee-market', + low: { + maxFeePerGas: '0x60db88400', + maxPriorityFeePerGas: '0x3b9aca00', + }, + medium: { + maxFeePerGas: '0x835105080', + maxPriorityFeePerGas: '0x59682f00', + }, + high: { + maxFeePerGas: '0xa5c681d00', + maxPriorityFeePerGas: '0x77359400', + }, + }, + gasFeeEstimatesLoaded: true, + hash: '0xe12fee9d2d73550236d233e8c541cee3237d7e9dc8085cd576d620fca1b659b7', + submittedTime: 1730925261525, + baseFeePerGas: '0x5d21dba00', + blockTimestamp: '0x672bd2cd', + txReceipt: { + blockHash: + '0x158b659f69b0d465b88ad768147057f9c8dc01ae0631e88b04d9406a19fcc9f6', + blockNumber: '0x324d0fb', + contractAddress: null, + cumulativeGasUsed: '0x160ccc', + effectiveGasPrice: '0x649534e00', + from: '0xc5fe6ef47965741f6f7a4734bf784bf3ae3f2452', + gasUsed: '0xa1211', + logs: [ + { + address: '0x68e21357555a15cd9df624f3bfb7aed0eae167e9', + blockHash: + '0x158b659f69b0d465b88ad768147057f9c8dc01ae0631e88b04d9406a19fcc9f6', + blockNumber: '0x324d0fb', + data: '0x00000000000000000000000000000000000000000000000000031bced02db000', + logIndex: '0x27', + removed: false, + topics: [ + '0x3d0ce9bfc3ed7d6862dbb28b2dea94561fe714a1b4d019aa8af39730d1ad7c3d', + '0x000000000000000000000000f3e6bb4227c0040e0e274388269ee3cec1de8447', + ], + transactionHash: + '0xe12fee9d2d73550236d233e8c541cee3237d7e9dc8085cd576d620fca1b659b7', + transactionIndex: '0x5', + }, + { + address: '0xf3e6bb4227c0040e0e274388269ee3cec1de8447', + blockHash: + '0x158b659f69b0d465b88ad768147057f9c8dc01ae0631e88b04d9406a19fcc9f6', + blockNumber: '0x324d0fb', + data: '0x000000000000000000000000000000000000000000000000000000000000000000000000000000000000000068e21357555a15cd9df624f3bfb7aed0eae167e900000000000000000000000000000000000000000000000000031bced02db000', + logIndex: '0x28', + removed: false, + topics: [ + '0x6ded982279c8387ad8a63e73385031a3807c1862e633f06e09d11bcb6e282f60', + ], + transactionHash: + '0xe12fee9d2d73550236d233e8c541cee3237d7e9dc8085cd576d620fca1b659b7', + transactionIndex: '0x5', + }, + { + address: '0xb31f66aa3c1e785363f0875a1b74e27b85fd66c7', + blockHash: + '0x158b659f69b0d465b88ad768147057f9c8dc01ae0631e88b04d9406a19fcc9f6', + blockNumber: '0x324d0fb', + data: '0x000000000000000000000000000000000000000000000000016029a98d5c5000', + logIndex: '0x29', + removed: false, + topics: [ + '0xe1fffcc4923d04b559f4d29a8bfc6cda04eb5b0d3c460751c2402c5c5cc9109c', + '0x000000000000000000000000111111125421ca6dc452d289314280a0f8842a65', + ], + transactionHash: + '0xe12fee9d2d73550236d233e8c541cee3237d7e9dc8085cd576d620fca1b659b7', + transactionIndex: '0x5', + }, + { + address: '0xb31f66aa3c1e785363f0875a1b74e27b85fd66c7', + blockHash: + '0x158b659f69b0d465b88ad768147057f9c8dc01ae0631e88b04d9406a19fcc9f6', + blockNumber: '0x324d0fb', + data: '0x000000000000000000000000000000000000000000000000016029a98d5c5000', + logIndex: '0x2a', + removed: false, + topics: [ + '0xddf252ad1be2c89b69c2b068fc378daa952ba7f163c4a11628f55a4df523b3ef', + '0x000000000000000000000000111111125421ca6dc452d289314280a0f8842a65', + '0x000000000000000000000000e3ba3d5e3f98eeff5e9eddd5bd20e476202770da', + ], + transactionHash: + '0xe12fee9d2d73550236d233e8c541cee3237d7e9dc8085cd576d620fca1b659b7', + transactionIndex: '0x5', + }, + { + address: '0x9702230a8ea53601f5cd2dc00fdbc13d4df4a8c7', + blockHash: + '0x158b659f69b0d465b88ad768147057f9c8dc01ae0631e88b04d9406a19fcc9f6', + blockNumber: '0x324d0fb', + data: '0x0000000000000000000000000000000000000000000000000000000000281d37', + logIndex: '0x2b', + removed: false, + topics: [ + '0xddf252ad1be2c89b69c2b068fc378daa952ba7f163c4a11628f55a4df523b3ef', + '0x000000000000000000000000e3ba3d5e3f98eeff5e9eddd5bd20e476202770da', + '0x0000000000000000000000001231deb6f5749ef6ce6943a275a1d3e7486f4eae', + ], + transactionHash: + '0xe12fee9d2d73550236d233e8c541cee3237d7e9dc8085cd576d620fca1b659b7', + transactionIndex: '0x5', + }, + { + address: '0xe3ba3d5e3f98eeff5e9eddd5bd20e476202770da', + blockHash: + '0x158b659f69b0d465b88ad768147057f9c8dc01ae0631e88b04d9406a19fcc9f6', + blockNumber: '0x324d0fb', + data: '0x0000000000000000000000000000000000000000000000000000000ee03b968f00000000000000000000000000000000000000000000008235a12766ecdf66da', + logIndex: '0x2c', + removed: false, + topics: [ + '0x1c411e9a96e071241c2f21f7726b17ae89e3cab4c78be50e062b03a9fffbbad1', + ], + transactionHash: + '0xe12fee9d2d73550236d233e8c541cee3237d7e9dc8085cd576d620fca1b659b7', + transactionIndex: '0x5', + }, + { + address: '0xe3ba3d5e3f98eeff5e9eddd5bd20e476202770da', + blockHash: + '0x158b659f69b0d465b88ad768147057f9c8dc01ae0631e88b04d9406a19fcc9f6', + blockNumber: '0x324d0fb', + data: '0x0000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000016029a98d5c50000000000000000000000000000000000000000000000000000000000000281d370000000000000000000000000000000000000000000000000000000000000000', + logIndex: '0x2d', + removed: false, + topics: [ + '0xd78ad95fa46c994b6551d0da85fc275fe613ce37657fb8d5e3d130840159d822', + '0x000000000000000000000000111111125421ca6dc452d289314280a0f8842a65', + '0x0000000000000000000000001231deb6f5749ef6ce6943a275a1d3e7486f4eae', + ], + transactionHash: + '0xe12fee9d2d73550236d233e8c541cee3237d7e9dc8085cd576d620fca1b659b7', + transactionIndex: '0x5', + }, + { + address: '0x1231deb6f5749ef6ce6943a275a1d3e7486f4eae', + blockHash: + '0x158b659f69b0d465b88ad768147057f9c8dc01ae0631e88b04d9406a19fcc9f6', + blockNumber: '0x324d0fb', + data: '0x49edb1f86374f25f25bba1abda983a1ba299a1f4072203a477122ab74befd000000000000000000000000000111111125421ca6dc452d289314280a0f8842a6500000000000000000000000000000000000000000000000000000000000000000000000000000000000000009702230a8ea53601f5cd2dc00fdbc13d4df4a8c7000000000000000000000000000000000000000000000000016029a98d5c50000000000000000000000000000000000000000000000000000000000000281d3700000000000000000000000000000000000000000000000000000000672bd2cd', + logIndex: '0x2e', + removed: false, + topics: [ + '0x7bfdfdb5e3a3776976e53cb0607060f54c5312701c8cba1155cc4d5394440b38', + ], + transactionHash: + '0xe12fee9d2d73550236d233e8c541cee3237d7e9dc8085cd576d620fca1b659b7', + transactionIndex: '0x5', + }, + { + address: '0x9702230a8ea53601f5cd2dc00fdbc13d4df4a8c7', + blockHash: + '0x158b659f69b0d465b88ad768147057f9c8dc01ae0631e88b04d9406a19fcc9f6', + blockNumber: '0x324d0fb', + data: '0x0000000000000000000000000000000000000000000000000000000000281d37', + logIndex: '0x2f', + removed: false, + topics: [ + '0xddf252ad1be2c89b69c2b068fc378daa952ba7f163c4a11628f55a4df523b3ef', + '0x0000000000000000000000001231deb6f5749ef6ce6943a275a1d3e7486f4eae', + '0x00000000000000000000000012dc9256acc9895b076f6638d628382881e62cee', + ], + transactionHash: + '0xe12fee9d2d73550236d233e8c541cee3237d7e9dc8085cd576d620fca1b659b7', + transactionIndex: '0x5', + }, + { + address: '0x9702230a8ea53601f5cd2dc00fdbc13d4df4a8c7', + blockHash: + '0x158b659f69b0d465b88ad768147057f9c8dc01ae0631e88b04d9406a19fcc9f6', + blockNumber: '0x324d0fb', + data: '0xffffffffffffffffffffffffffffffffffffffffffffffffffffff55e55aa213', + logIndex: '0x30', + removed: false, + topics: [ + '0x8c5be1e5ebec7d5bd14f71427d1e84f3dd0314c0f7b2291e5b200ac8c7c3b925', + '0x0000000000000000000000001231deb6f5749ef6ce6943a275a1d3e7486f4eae', + '0x00000000000000000000000012dc9256acc9895b076f6638d628382881e62cee', + ], + transactionHash: + '0xe12fee9d2d73550236d233e8c541cee3237d7e9dc8085cd576d620fca1b659b7', + transactionIndex: '0x5', + }, + { + address: '0x197d1333dea5fe0d6600e9b396c7f1b1cfcc558a', + blockHash: + '0x158b659f69b0d465b88ad768147057f9c8dc01ae0631e88b04d9406a19fcc9f6', + blockNumber: '0x324d0fb', + data: '0x00000000000000000000000090e595783e43eb89ff07f63d27b8430e6b44bd9c000000000000000000000000000000000000000000000000001445775143bee0', + logIndex: '0x31', + removed: false, + topics: [ + '0x61ed099e74a97a1d7f8bb0952a88ca8b7b8ebd00c126ea04671f92a81213318a', + ], + transactionHash: + '0xe12fee9d2d73550236d233e8c541cee3237d7e9dc8085cd576d620fca1b659b7', + transactionIndex: '0x5', + }, + { + address: '0x197d1333dea5fe0d6600e9b396c7f1b1cfcc558a', + blockHash: + '0x158b659f69b0d465b88ad768147057f9c8dc01ae0631e88b04d9406a19fcc9f6', + blockNumber: '0x324d0fb', + data: '0x000000000000000000000000000000000000000000000000000000000000006000000000000000000000000000000000000000000000000000000000000000c000000000000000000000000000000000000000000000000000000000000000e00000000000000000000000000000000000000000000000000000000000000002000000000000000000000000252b234545e154543ad2784c7111eb90406be836000000000000000000000000a59ba433ac34d2927232918ef5b2eaafcf130ba500000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000002000000000000000000000000000000000000000000000000000216a34c6bba2f000000000000000000000000000000000000000000000000000216a34c6bba2f', + logIndex: '0x32', + removed: false, + topics: [ + '0x07ea52d82345d6e838192107d8fd7123d9c2ec8e916cd0aad13fd2b60db24644', + ], + transactionHash: + '0xe12fee9d2d73550236d233e8c541cee3237d7e9dc8085cd576d620fca1b659b7', + transactionIndex: '0x5', + }, + { + address: '0x1a44076050125825900e736c501f859c50fe728c', + blockHash: + '0x158b659f69b0d465b88ad768147057f9c8dc01ae0631e88b04d9406a19fcc9f6', + blockNumber: '0x324d0fb', + data: '0x00000000000000000000000000000000000000000000000000000000000000600000000000000000000000000000000000000000000000000000000000000380000000000000000000000000197d1333dea5fe0d6600e9b396c7f1b1cfcc558a00000000000000000000000000000000000000000000000000000000000002fc010000000000001eb30000759a00000000000000000000000017e450be3ba9557f2378e20d64ad417e59ef9a340000759e00000000000000000000000019cfce47ed54a88614648dc3f19a5980097007dd0a668d7b26be67905413d7478cff17283cd09f3076ec95fd14469766b9ec837f0100020000000000000000000000001493e7b8d4dfade0a178dad9335470337a3a219a0000000000280dcf0000000000000000000000001231deb6f5749ef6ce6943a275a1d3e7486f4eae49edb1f86374f25f25bba1abda983a1ba299a1f4072203a477122ab74befd0000000000000000000000000000000000000000000000000000000000000000060000000000000000000000000c5fe6ef47965741f6f7a4734bf784bf3ae3f245200000000000000000000000000000000000000000000000000000000000000010000000000000000000000000000000000000000000000000000000000000020000000000000000000000000111111125421ca6dc452d289314280a0f8842a65000000000000000000000000111111125421ca6dc452d289314280a0f8842a65000000000000000000000000fd086bc7cd5c481dcc9c85ebe478a1c0b69fcbb90000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000027a78600000000000000000000000000000000000000000000000000000000000000e00000000000000000000000000000000000000000000000000000000000000001000000000000000000000000000000000000000000000000000000000000008883800a8e000000000000000000000000fd086bc7cd5c481dcc9c85ebe478a1c0b69fcbb9000000000000000000000000000000000000000000000000000000000027a78600000000000000000000000000000000000000000000000000036f3c9b851bf338000000000000000000000042161084d0672e1d3f26a9b53e653be2084ff19c2a94d11400000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000002c000301001101000000000000000000000000000249f0010013030000000000000000000000000000003037c00000000000000000000000000000000000000000', + logIndex: '0x33', + removed: false, + topics: [ + '0x1ab700d4ced0c005b164c0f789fd09fcbb0156d4c2041b8a3bfbcd961cd1567f', + ], + transactionHash: + '0xe12fee9d2d73550236d233e8c541cee3237d7e9dc8085cd576d620fca1b659b7', + transactionIndex: '0x5', + }, + { + address: '0x12dc9256acc9895b076f6638d628382881e62cee', + blockHash: + '0x158b659f69b0d465b88ad768147057f9c8dc01ae0631e88b04d9406a19fcc9f6', + blockNumber: '0x324d0fb', + data: '0x000000000000000000000000000000000000000000000000000000000000759e0000000000000000000000000000000000000000000000000000000000281d370000000000000000000000000000000000000000000000000000000000280dcf', + logIndex: '0x34', + removed: false, + topics: [ + '0x85496b760a4b7f8d66384b9df21b381f5d1b1e79f229a47aaf4c232edc2fe59a', + '0x0a668d7b26be67905413d7478cff17283cd09f3076ec95fd14469766b9ec837f', + '0x0000000000000000000000001231deb6f5749ef6ce6943a275a1d3e7486f4eae', + ], + transactionHash: + '0xe12fee9d2d73550236d233e8c541cee3237d7e9dc8085cd576d620fca1b659b7', + transactionIndex: '0x5', + }, + { + address: '0x1231deb6f5749ef6ce6943a275a1d3e7486f4eae', + blockHash: + '0x158b659f69b0d465b88ad768147057f9c8dc01ae0631e88b04d9406a19fcc9f6', + blockNumber: '0x324d0fb', + data: '0x000000000000000000000000000000000000000000000000000000000000002049edb1f86374f25f25bba1abda983a1ba299a1f4072203a477122ab74befd0000000000000000000000000000000000000000000000000000000000000000140000000000000000000000000000000000000000000000000000000000000018000000000000000000000000000000000000000000000000000000000000000000000000000000000000000009702230a8ea53601f5cd2dc00fdbc13d4df4a8c7000000000000000000000000c5fe6ef47965741f6f7a4734bf784bf3ae3f24520000000000000000000000000000000000000000000000000000000000281d37000000000000000000000000000000000000000000000000000000000000a4b100000000000000000000000000000000000000000000000000000000000000010000000000000000000000000000000000000000000000000000000000000001000000000000000000000000000000000000000000000000000000000000000a7374617267617465563200000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000f6d6574616d61736b2d6272696467650000000000000000000000000000000000', + logIndex: '0x35', + removed: false, + topics: [ + '0xcba69f43792f9f399347222505213b55af8e0b0b54b893085c2e27ecbe1644f1', + ], + transactionHash: + '0xe12fee9d2d73550236d233e8c541cee3237d7e9dc8085cd576d620fca1b659b7', + transactionIndex: '0x5', + }, + { + address: '0xf3e6bb4227c0040e0e274388269ee3cec1de8447', + blockHash: + '0x158b659f69b0d465b88ad768147057f9c8dc01ae0631e88b04d9406a19fcc9f6', + blockNumber: '0x324d0fb', + data: '0x000000000000000000000000c5fe6ef47965741f6f7a4734bf784bf3ae3f24520000000000000000000000001231deb6f5749ef6ce6943a275a1d3e7486f4eae000000000000000000000000000000000000000000000000000000000000a4b100000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000016029a98d5c5000', + logIndex: '0x36', + removed: false, + topics: [ + '0x831bac9533a2034226daa21109dbd4f887674f0fe4877e1a8b35b3ffe1bdce76', + ], + transactionHash: + '0xe12fee9d2d73550236d233e8c541cee3237d7e9dc8085cd576d620fca1b659b7', + transactionIndex: '0x5', + }, + ], + logsBloom: + '0x00200002000000000000000080600000000000000000000020000000001000000000000004000000004004000002008000000000100000000000000000200080000082000000002000000008000000200004000000000010000004008000000004000000804008008000000000000001002000022010000000000014000000000000020808000000000000080000000000000011000000080004005000000008060081028000000080100040080000000000000000400000004540002000000008081022002008000000000080000000800000000000001000000000000000000010040000000000000000800000002008000400018004420800000000000000', + status: '0x1', + to: '0x29106d08382d3c73bf477a94333c61db1142e1b6', + transactionHash: + '0xe12fee9d2d73550236d233e8c541cee3237d7e9dc8085cd576d620fca1b659b7', + transactionIndex: '0x5', + type: '0x2', + }, + }, + }, + step1Long: { + bridgeHistoryItem: { + quote: { + requestId: '8d46a335-a67d-4583-993e-eab0e22e16f4', + srcChainId: 42161, + srcTokenAmount: '991250000000000', + srcAsset: { + address: '0x0000000000000000000000000000000000000000', + chainId: 42161, + symbol: 'ETH', + decimals: 18, + name: 'ETH', + coinKey: 'ETH', + logoURI: + 'https://raw.githubusercontent.com/trustwallet/assets/master/blockchains/ethereum/assets/0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2/logo.png', + priceUSD: '2672.2', + icon: 'https://raw.githubusercontent.com/trustwallet/assets/master/blockchains/ethereum/assets/0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2/logo.png', + }, + destChainId: 43114, + destTokenAmount: '99073691907804692', + destAsset: { + address: '0x0000000000000000000000000000000000000000', + chainId: 43114, + symbol: 'AVAX', + decimals: 18, + name: 'AVAX', + coinKey: 'AVAX', + logoURI: + 'https://static.debank.com/image/avax_token/logo_url/avax/0b9c84359c84d6bdd5bfda9c2d4c4a82.png', + priceUSD: '26.51', + icon: 'https://static.debank.com/image/avax_token/logo_url/avax/0b9c84359c84d6bdd5bfda9c2d4c4a82.png', + }, + feeData: { + metabridge: { + amount: '8750000000000', + asset: { + address: '0x0000000000000000000000000000000000000000', + chainId: 42161, + symbol: 'ETH', + decimals: 18, + name: 'ETH', + coinKey: 'ETH', + logoURI: + 'https://raw.githubusercontent.com/trustwallet/assets/master/blockchains/ethereum/assets/0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2/logo.png', + priceUSD: '2672.2', + icon: 'https://raw.githubusercontent.com/trustwallet/assets/master/blockchains/ethereum/assets/0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2/logo.png', + }, + }, + }, + bridgeId: 'lifi', + bridges: ['stargate'], + steps: [ + { + action: 'swap', + srcChainId: 42161, + destChainId: 42161, + protocol: { + name: 'openocean', + displayName: 'OpenOcean', + icon: 'https://raw.githubusercontent.com/lifinance/types/main/src/assets/icons/exchanges/openocean.png', + }, + srcAsset: { + address: '0x0000000000000000000000000000000000000000', + chainId: 42161, + symbol: 'ETH', + decimals: 18, + name: 'ETH', + coinKey: 'ETH', + logoURI: + 'https://raw.githubusercontent.com/trustwallet/assets/master/blockchains/ethereum/assets/0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2/logo.png', + priceUSD: '2672.2', + icon: 'https://raw.githubusercontent.com/trustwallet/assets/master/blockchains/ethereum/assets/0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2/logo.png', + }, + destAsset: { + address: '0xaf88d065e77c8cC2239327C5EDb3A432268e5831', + chainId: 42161, + symbol: 'USDC', + decimals: 6, + name: 'USD Coin', + coinKey: 'USDC', + logoURI: + 'https://raw.githubusercontent.com/trustwallet/assets/master/blockchains/ethereum/assets/0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48/logo.png', + priceUSD: '0.9990009990009991', + icon: 'https://raw.githubusercontent.com/trustwallet/assets/master/blockchains/ethereum/assets/0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48/logo.png', + }, + srcAmount: '991250000000000', + destAmount: '2656446', + }, + { + action: 'bridge', + srcChainId: 42161, + destChainId: 43114, + protocol: { + name: 'stargate', + displayName: 'StargateV2 (Fast mode)', + icon: 'https://raw.githubusercontent.com/lifinance/types/5685c638772f533edad80fcb210b4bb89e30a50f/src/assets/icons/bridges/stargate.png', + }, + srcAsset: { + address: '0xaf88d065e77c8cC2239327C5EDb3A432268e5831', + chainId: 42161, + symbol: 'USDC', + decimals: 6, + name: 'USD Coin', + coinKey: 'USDC', + logoURI: + 'https://raw.githubusercontent.com/trustwallet/assets/master/blockchains/ethereum/assets/0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48/logo.png', + priceUSD: '0.9990009990009991', + icon: 'https://raw.githubusercontent.com/trustwallet/assets/master/blockchains/ethereum/assets/0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48/logo.png', + }, + destAsset: { + address: '0xB97EF9Ef8734C71904D8002F8b6Bc66Dd9c48a6E', + chainId: 43114, + symbol: 'USDC', + decimals: 6, + name: 'USD Coin', + coinKey: 'USDC', + logoURI: + 'https://raw.githubusercontent.com/trustwallet/assets/master/blockchains/ethereum/assets/0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48/logo.png', + priceUSD: '0.9990009990009991', + icon: 'https://raw.githubusercontent.com/trustwallet/assets/master/blockchains/ethereum/assets/0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48/logo.png', + }, + srcAmount: '2656446', + destAmount: '2655659', + }, + { + action: 'swap', + srcChainId: 43114, + destChainId: 43114, + protocol: { + name: '1inch', + displayName: '1inch', + icon: 'https://raw.githubusercontent.com/lifinance/types/main/src/assets/icons/exchanges/oneinch.png', + }, + srcAsset: { + address: '0xB97EF9Ef8734C71904D8002F8b6Bc66Dd9c48a6E', + chainId: 43114, + symbol: 'USDC', + decimals: 6, + name: 'USD Coin', + coinKey: 'USDC', + logoURI: + 'https://raw.githubusercontent.com/trustwallet/assets/master/blockchains/ethereum/assets/0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48/logo.png', + priceUSD: '0.9990009990009991', + icon: 'https://raw.githubusercontent.com/trustwallet/assets/master/blockchains/ethereum/assets/0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48/logo.png', + }, + destAsset: { + address: '0x0000000000000000000000000000000000000000', + chainId: 43114, + symbol: 'AVAX', + decimals: 18, + name: 'AVAX', + coinKey: 'AVAX', + logoURI: + 'https://static.debank.com/image/avax_token/logo_url/avax/0b9c84359c84d6bdd5bfda9c2d4c4a82.png', + priceUSD: '26.51', + icon: 'https://static.debank.com/image/avax_token/logo_url/avax/0b9c84359c84d6bdd5bfda9c2d4c4a82.png', + }, + srcAmount: '2642380', + destAmount: '99073691907804692', + }, + ], + }, + startTime: 1730926316058, + estimatedProcessingTimeInSeconds: 105, + slippagePercentage: 0, + targetContractAddress: '0x23981fc34e69eedfe2bd9a0a9fcb0719fe09dbfc', + account: '0xc5fe6ef47965741f6f7a4734bf784bf3ae3f2452', + status: { + status: 'PENDING', + srcChain: { + chainId: 42161, + txHash: + '0x010e1bffe8288956012e6b6132d7eb3eaf9d0bbf066bd13aae13b973c678508f', + amount: '991250000000000', + token: { + address: '0x0000000000000000000000000000000000000000', + chainId: 42161, + symbol: 'ETH', + decimals: 18, + name: 'ETH', + coinKey: 'ETH', + logoURI: + 'https://raw.githubusercontent.com/trustwallet/assets/master/blockchains/ethereum/assets/0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2/logo.png', + priceUSD: '2672.2', + icon: 'https://raw.githubusercontent.com/trustwallet/assets/master/blockchains/ethereum/assets/0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2/logo.png', + }, + }, + destChain: { + chainId: '43114', + token: {}, + }, + }, + }, + srcChainTxMeta: { + actionId: 1730926316049.0693, + chainId: '0xa4b1', + id: 'f583d7a0-9c80-11ef-929c-517d218027d6', + origin: 'metamask', + status: 'submitted', + time: 1730926316058, + txParams: { + from: '0xc5fe6ef47965741f6f7a4734bf784bf3ae3f2452', + data: '0x3ce33bff000000000000000000000000000000000000000000000000000000000000008000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000004b46d729a445900000000000000000000000000000000000000000000000000000000000000c0000000000000000000000000000000000000000000000000000000000000000d6c6966694164617074657256320000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000011800000000000000000000000001231deb6f5749ef6ce6943a275a1d3e7486f4eae0000000000000000000000001231deb6f5749ef6ce6943a275a1d3e7486f4eae000000000000000000000000000000000000000000000000000000000000a86a0000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000038589602234000000000000000000000000000000000000000000000000000000000000000140000000000000000000000000000000000000000000000000000007f544a44c0000000000000000000000000056ca675c3633cc16bd6849e2b431d4e8de5e23bf0000000000000000000000000000000000000000000000000000000000001024a6010a66000000000000000000000000000000000000000000000000000000000000006000000000000000000000000000000000000000000000000000000000000002200000000000000000000000000000000000000000000000000000000000000be0eec104bed47120f58804c59125ae2572682610d39e80abc3838c99052db28275000000000000000000000000000000000000000000000000000000000000014000000000000000000000000000000000000000000000000000000000000001800000000000000000000000000000000000000000000000000000000000000000000000000000000000000000af88d065e77c8cc2239327c5edb3a432268e5831000000000000000000000000c5fe6ef47965741f6f7a4734bf784bf3ae3f245200000000000000000000000000000000000000000000000000000000002854dc000000000000000000000000000000000000000000000000000000000000a86a00000000000000000000000000000000000000000000000000000000000000010000000000000000000000000000000000000000000000000000000000000001000000000000000000000000000000000000000000000000000000000000000a7374617267617465563200000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000f6d6574616d61736b2d6272696467650000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000100000000000000000000000000000000000000000000000000000000000000200000000000000000000000006352a56caadc4f1e25cd6c75970fa768a3304e640000000000000000000000006352a56caadc4f1e25cd6c75970fa768a3304e640000000000000000000000000000000000000000000000000000000000000000000000000000000000000000af88d065e77c8cc2239327c5edb3a432268e5831000000000000000000000000000000000000000000000000000385896022340000000000000000000000000000000000000000000000000000000000000000e00000000000000000000000000000000000000000000000000000000000000001000000000000000000000000000000000000000000000000000000000000086490411a32000000000000000000000000f851d3d46237ec552a4c6e383a973115e781b1a5000000000000000000000000000000000000000000000000000000000000006000000000000000000000000000000000000000000000000000000000000001c0000000000000000000000000eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee000000000000000000000000af88d065e77c8cc2239327c5edb3a432268e5831000000000000000000000000f851d3d46237ec552a4c6e383a973115e781b1a50000000000000000000000001231deb6f5749ef6ce6943a275a1d3e7486f4eae0000000000000000000000000000000000000000000000000003858960223400000000000000000000000000000000000000000000000000000000000028524100000000000000000000000000000000000000000000000000000000002886200000000000000000000000000000000000000000000000000000000000000000000000000000000000000000933a06c631ed8b5e4f3848c91a1cfc45e5c7eab3000000000000000000000000000000000000000000000000000000000000014000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000004000000000000000000000000000000000000000000000000000000000000008000000000000000000000000000000000000000000000000000000000000001400000000000000000000000000000000000000000000000000000000000000300000000000000000000000000000000000000000000000000000000000000042000000000000000000000000082af49447d8a07e3bd95bd0d56f35241523fbab10000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000385896022340000000000000000000000000000000000000000000000000000000000000000800000000000000000000000000000000000000000000000000000000000000004d0e30db00000000000000000000000000000000000000000000000000000000000000000000000000000000071cb8a26f2c4eb68288bcc2ebaba4f72b065f07600000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000008000000000000000000000000000000000000000000000000000000000000001043eece7db000000000000000000000000f851d3d46237ec552a4c6e383a973115e781b1a5000000000000000000000000000000000000000000000000000385896022340000000000000000000000000000000000000000000000000000000000000000010000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000007fffffff00000000000000000000000000000000000000000000000000000000000000c000000000000000000000000000000000000000000000000000000000000000200000000000000000000000000a7e848aca42d879ef06507fca0e7b33a0a63c1e00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000008000000000000000000000000000000000000000000000000000000000000000648a6a1e85000000000000000000000000af88d065e77c8cc2239327c5edb3a432268e5831000000000000000000000000353c1f0bc78fbbc245b3c93ef77b1dcc5b77d2a0000000000000000000000000000000000000000000000000000000000028862000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000008000000000000000000000000000000000000000000000000000000000000001a49f865422000000000000000000000000af88d065e77c8cc2239327c5edb3a432268e583100000000000000000000000000000001000000000000000000000000000000010000000000000000000000000000000000000000000000000000000000000080000000000000000000000000000000000000000000000000000000000000004400000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000800000000000000000000000000000000000000000000000000000000000000064d1660f99000000000000000000000000af88d065e77c8cc2239327c5edb3a432268e58310000000000000000000000001231deb6f5749ef6ce6943a275a1d3e7486f4eae0000000000000000000000000000000000000000000000000000000000000001000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000100000000000000000000000000000000000000000000000000000000000000a0000000000000000000000000000000000000000000000000000126eecdd3c4590000000000000000000000000000000000000000000000000000000000000000000000000000000000000000c5fe6ef47965741f6f7a4734bf784bf3ae3f2452000000000000000000000000000000000000000000000000000000000000759a0000000000000000000000001493e7b8d4dfade0a178dad9335470337a3a219a0000000000000000000000000000000000000000000000000000000000285241000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000e000000000000000000000000000000000000000000000000000000000000001200000000000000000000000000000000000000000000000000000000000000380000000000000000000000000000000000000000000000000000000000000001800030100130300000000000000000000000000000008f11000000000000000000000000000000000000000000000000000000000000000000000000000000240eec104bed47120f58804c59125ae2572682610d39e80abc3838c99052db282750000000000000000000000000000000000000000000000000000000000000060000000000000000000000000c5fe6ef47965741f6f7a4734bf784bf3ae3f245200000000000000000000000000000000000000000000000000000000000000010000000000000000000000000000000000000000000000000000000000000020000000000000000000000000111111125421ca6dc452d289314280a0f8842a65000000000000000000000000111111125421ca6dc452d289314280a0f8842a65000000000000000000000000b97ef9ef8734c71904d8002f8b6bc66dd9c48a6e0000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000028190200000000000000000000000000000000000000000000000000000000000000e00000000000000000000000000000000000000000000000000000000000000001000000000000000000000000000000000000000000000000000000000000008883800a8e000000000000000000000000b97ef9ef8734c71904d8002f8b6bc66dd9c48a6e0000000000000000000000000000000000000000000000000000000000281902000000000000000000000000000000000000000000000000015c4b33745816ab18000000000000003b6d03400e0100ab771e9288e0aa97e11557e6654c3a96652a94d114000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000993b046fd255d06d2dd8b115d91923fdcc52bd60070f3bedce2fc45e7e8c6b7e4f1add057efd388c8a24d3eb0e6736cf87170452068cb12b7e09772463e1a5b71c', + gas: '0x1ceed0', + gasLimit: '0x1ceed0', + nonce: '0xc3', + to: '0x23981fc34e69eedfe2bd9a0a9fcb0719fe09dbfc', + value: '0x04b46d729a4459', + maxFeePerGas: '0x104d37c', + maxPriorityFeePerGas: '0x0', + type: '0x2', + }, + userEditedGasLimit: false, + verifiedOnBlockchain: false, + type: 'bridge', + networkClientId: '1abbfb5d-ed75-4de7-8cff-694d863d8519', + defaultGasEstimates: { + gas: '0x1ceed0', + maxFeePerGas: '0x104d37c', + maxPriorityFeePerGas: '0x0', + estimateType: 'medium', + }, + userFeeLevel: 'medium', + sendFlowHistory: [], + history: [ + { + actionId: 1730926316049.0693, + chainId: '0xa4b1', + id: 'f583d7a0-9c80-11ef-929c-517d218027d6', + origin: 'metamask', + status: 'unapproved', + time: 1730926316058, + txParams: { + from: '0xc5fe6ef47965741f6f7a4734bf784bf3ae3f2452', + data: '0x3ce33bff000000000000000000000000000000000000000000000000000000000000008000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000004b46d729a445900000000000000000000000000000000000000000000000000000000000000c0000000000000000000000000000000000000000000000000000000000000000d6c6966694164617074657256320000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000011800000000000000000000000001231deb6f5749ef6ce6943a275a1d3e7486f4eae0000000000000000000000001231deb6f5749ef6ce6943a275a1d3e7486f4eae000000000000000000000000000000000000000000000000000000000000a86a0000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000038589602234000000000000000000000000000000000000000000000000000000000000000140000000000000000000000000000000000000000000000000000007f544a44c0000000000000000000000000056ca675c3633cc16bd6849e2b431d4e8de5e23bf0000000000000000000000000000000000000000000000000000000000001024a6010a66000000000000000000000000000000000000000000000000000000000000006000000000000000000000000000000000000000000000000000000000000002200000000000000000000000000000000000000000000000000000000000000be0eec104bed47120f58804c59125ae2572682610d39e80abc3838c99052db28275000000000000000000000000000000000000000000000000000000000000014000000000000000000000000000000000000000000000000000000000000001800000000000000000000000000000000000000000000000000000000000000000000000000000000000000000af88d065e77c8cc2239327c5edb3a432268e5831000000000000000000000000c5fe6ef47965741f6f7a4734bf784bf3ae3f245200000000000000000000000000000000000000000000000000000000002854dc000000000000000000000000000000000000000000000000000000000000a86a00000000000000000000000000000000000000000000000000000000000000010000000000000000000000000000000000000000000000000000000000000001000000000000000000000000000000000000000000000000000000000000000a7374617267617465563200000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000f6d6574616d61736b2d6272696467650000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000100000000000000000000000000000000000000000000000000000000000000200000000000000000000000006352a56caadc4f1e25cd6c75970fa768a3304e640000000000000000000000006352a56caadc4f1e25cd6c75970fa768a3304e640000000000000000000000000000000000000000000000000000000000000000000000000000000000000000af88d065e77c8cc2239327c5edb3a432268e5831000000000000000000000000000000000000000000000000000385896022340000000000000000000000000000000000000000000000000000000000000000e00000000000000000000000000000000000000000000000000000000000000001000000000000000000000000000000000000000000000000000000000000086490411a32000000000000000000000000f851d3d46237ec552a4c6e383a973115e781b1a5000000000000000000000000000000000000000000000000000000000000006000000000000000000000000000000000000000000000000000000000000001c0000000000000000000000000eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee000000000000000000000000af88d065e77c8cc2239327c5edb3a432268e5831000000000000000000000000f851d3d46237ec552a4c6e383a973115e781b1a50000000000000000000000001231deb6f5749ef6ce6943a275a1d3e7486f4eae0000000000000000000000000000000000000000000000000003858960223400000000000000000000000000000000000000000000000000000000000028524100000000000000000000000000000000000000000000000000000000002886200000000000000000000000000000000000000000000000000000000000000000000000000000000000000000933a06c631ed8b5e4f3848c91a1cfc45e5c7eab3000000000000000000000000000000000000000000000000000000000000014000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000004000000000000000000000000000000000000000000000000000000000000008000000000000000000000000000000000000000000000000000000000000001400000000000000000000000000000000000000000000000000000000000000300000000000000000000000000000000000000000000000000000000000000042000000000000000000000000082af49447d8a07e3bd95bd0d56f35241523fbab10000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000385896022340000000000000000000000000000000000000000000000000000000000000000800000000000000000000000000000000000000000000000000000000000000004d0e30db00000000000000000000000000000000000000000000000000000000000000000000000000000000071cb8a26f2c4eb68288bcc2ebaba4f72b065f07600000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000008000000000000000000000000000000000000000000000000000000000000001043eece7db000000000000000000000000f851d3d46237ec552a4c6e383a973115e781b1a5000000000000000000000000000000000000000000000000000385896022340000000000000000000000000000000000000000000000000000000000000000010000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000007fffffff00000000000000000000000000000000000000000000000000000000000000c000000000000000000000000000000000000000000000000000000000000000200000000000000000000000000a7e848aca42d879ef06507fca0e7b33a0a63c1e00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000008000000000000000000000000000000000000000000000000000000000000000648a6a1e85000000000000000000000000af88d065e77c8cc2239327c5edb3a432268e5831000000000000000000000000353c1f0bc78fbbc245b3c93ef77b1dcc5b77d2a0000000000000000000000000000000000000000000000000000000000028862000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000008000000000000000000000000000000000000000000000000000000000000001a49f865422000000000000000000000000af88d065e77c8cc2239327c5edb3a432268e583100000000000000000000000000000001000000000000000000000000000000010000000000000000000000000000000000000000000000000000000000000080000000000000000000000000000000000000000000000000000000000000004400000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000800000000000000000000000000000000000000000000000000000000000000064d1660f99000000000000000000000000af88d065e77c8cc2239327c5edb3a432268e58310000000000000000000000001231deb6f5749ef6ce6943a275a1d3e7486f4eae0000000000000000000000000000000000000000000000000000000000000001000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000100000000000000000000000000000000000000000000000000000000000000a0000000000000000000000000000000000000000000000000000126eecdd3c4590000000000000000000000000000000000000000000000000000000000000000000000000000000000000000c5fe6ef47965741f6f7a4734bf784bf3ae3f2452000000000000000000000000000000000000000000000000000000000000759a0000000000000000000000001493e7b8d4dfade0a178dad9335470337a3a219a0000000000000000000000000000000000000000000000000000000000285241000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000e000000000000000000000000000000000000000000000000000000000000001200000000000000000000000000000000000000000000000000000000000000380000000000000000000000000000000000000000000000000000000000000001800030100130300000000000000000000000000000008f11000000000000000000000000000000000000000000000000000000000000000000000000000000240eec104bed47120f58804c59125ae2572682610d39e80abc3838c99052db282750000000000000000000000000000000000000000000000000000000000000060000000000000000000000000c5fe6ef47965741f6f7a4734bf784bf3ae3f245200000000000000000000000000000000000000000000000000000000000000010000000000000000000000000000000000000000000000000000000000000020000000000000000000000000111111125421ca6dc452d289314280a0f8842a65000000000000000000000000111111125421ca6dc452d289314280a0f8842a65000000000000000000000000b97ef9ef8734c71904d8002f8b6bc66dd9c48a6e0000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000028190200000000000000000000000000000000000000000000000000000000000000e00000000000000000000000000000000000000000000000000000000000000001000000000000000000000000000000000000000000000000000000000000008883800a8e000000000000000000000000b97ef9ef8734c71904d8002f8b6bc66dd9c48a6e0000000000000000000000000000000000000000000000000000000000281902000000000000000000000000000000000000000000000000015c4b33745816ab18000000000000003b6d03400e0100ab771e9288e0aa97e11557e6654c3a96652a94d114000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000993b046fd255d06d2dd8b115d91923fdcc52bd60070f3bedce2fc45e7e8c6b7e4f1add057efd388c8a24d3eb0e6736cf87170452068cb12b7e09772463e1a5b71c', + gas: '0x1ceed0', + gasLimit: '0x1ceed0', + to: '0x23981fc34e69eedfe2bd9a0a9fcb0719fe09dbfc', + value: '0x04b46d729a4459', + maxFeePerGas: '0x104d37c', + maxPriorityFeePerGas: '0x0', + }, + userEditedGasLimit: false, + verifiedOnBlockchain: false, + type: 'bridge', + networkClientId: '1abbfb5d-ed75-4de7-8cff-694d863d8519', + defaultGasEstimates: { + gas: '0x1ceed0', + maxFeePerGas: '0x104d37c', + maxPriorityFeePerGas: '0x0', + estimateType: 'medium', + }, + userFeeLevel: 'medium', + sendFlowHistory: [], + }, + [ + { + op: 'add', + path: '/txParams/nonce', + value: '0xc3', + note: 'TransactionController#approveTransaction - Transaction approved', + timestamp: 1730926316070, + }, + { + op: 'add', + path: '/txParams/type', + value: '0x2', + }, + { + op: 'replace', + path: '/status', + value: 'approved', + }, + ], + [ + { + op: 'replace', + path: '/status', + value: 'signed', + note: 'TransactionController#approveTransaction - RawTransaction added', + timestamp: 1730926316079, + }, + { + op: 'add', + path: '/r', + value: + '0xdffb2def3272d3cfb47b41bd4e7163f2101aedf4a6315df0e5ac4acd60cd5363', + }, + { + op: 'add', + path: '/s', + value: + '0x4269e8777e5f5dba8d8fb8b9ead2b08c90b22642f9525e98539eeeda467a68c5', + }, + { + op: 'add', + path: '/v', + value: '0x1', + }, + { + op: 'add', + path: '/rawTx', + value: + '0x02f9131882a4b181c380840104d37c831ceed09423981fc34e69eedfe2bd9a0a9fcb0719fe09dbfc8704b46d729a4459b912a53ce33bff000000000000000000000000000000000000000000000000000000000000008000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000004b46d729a445900000000000000000000000000000000000000000000000000000000000000c0000000000000000000000000000000000000000000000000000000000000000d6c6966694164617074657256320000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000011800000000000000000000000001231deb6f5749ef6ce6943a275a1d3e7486f4eae0000000000000000000000001231deb6f5749ef6ce6943a275a1d3e7486f4eae000000000000000000000000000000000000000000000000000000000000a86a0000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000038589602234000000000000000000000000000000000000000000000000000000000000000140000000000000000000000000000000000000000000000000000007f544a44c0000000000000000000000000056ca675c3633cc16bd6849e2b431d4e8de5e23bf0000000000000000000000000000000000000000000000000000000000001024a6010a66000000000000000000000000000000000000000000000000000000000000006000000000000000000000000000000000000000000000000000000000000002200000000000000000000000000000000000000000000000000000000000000be0eec104bed47120f58804c59125ae2572682610d39e80abc3838c99052db28275000000000000000000000000000000000000000000000000000000000000014000000000000000000000000000000000000000000000000000000000000001800000000000000000000000000000000000000000000000000000000000000000000000000000000000000000af88d065e77c8cc2239327c5edb3a432268e5831000000000000000000000000c5fe6ef47965741f6f7a4734bf784bf3ae3f245200000000000000000000000000000000000000000000000000000000002854dc000000000000000000000000000000000000000000000000000000000000a86a00000000000000000000000000000000000000000000000000000000000000010000000000000000000000000000000000000000000000000000000000000001000000000000000000000000000000000000000000000000000000000000000a7374617267617465563200000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000f6d6574616d61736b2d6272696467650000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000100000000000000000000000000000000000000000000000000000000000000200000000000000000000000006352a56caadc4f1e25cd6c75970fa768a3304e640000000000000000000000006352a56caadc4f1e25cd6c75970fa768a3304e640000000000000000000000000000000000000000000000000000000000000000000000000000000000000000af88d065e77c8cc2239327c5edb3a432268e5831000000000000000000000000000000000000000000000000000385896022340000000000000000000000000000000000000000000000000000000000000000e00000000000000000000000000000000000000000000000000000000000000001000000000000000000000000000000000000000000000000000000000000086490411a32000000000000000000000000f851d3d46237ec552a4c6e383a973115e781b1a5000000000000000000000000000000000000000000000000000000000000006000000000000000000000000000000000000000000000000000000000000001c0000000000000000000000000eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee000000000000000000000000af88d065e77c8cc2239327c5edb3a432268e5831000000000000000000000000f851d3d46237ec552a4c6e383a973115e781b1a50000000000000000000000001231deb6f5749ef6ce6943a275a1d3e7486f4eae0000000000000000000000000000000000000000000000000003858960223400000000000000000000000000000000000000000000000000000000000028524100000000000000000000000000000000000000000000000000000000002886200000000000000000000000000000000000000000000000000000000000000000000000000000000000000000933a06c631ed8b5e4f3848c91a1cfc45e5c7eab3000000000000000000000000000000000000000000000000000000000000014000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000004000000000000000000000000000000000000000000000000000000000000008000000000000000000000000000000000000000000000000000000000000001400000000000000000000000000000000000000000000000000000000000000300000000000000000000000000000000000000000000000000000000000000042000000000000000000000000082af49447d8a07e3bd95bd0d56f35241523fbab10000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000385896022340000000000000000000000000000000000000000000000000000000000000000800000000000000000000000000000000000000000000000000000000000000004d0e30db00000000000000000000000000000000000000000000000000000000000000000000000000000000071cb8a26f2c4eb68288bcc2ebaba4f72b065f07600000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000008000000000000000000000000000000000000000000000000000000000000001043eece7db000000000000000000000000f851d3d46237ec552a4c6e383a973115e781b1a5000000000000000000000000000000000000000000000000000385896022340000000000000000000000000000000000000000000000000000000000000000010000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000007fffffff00000000000000000000000000000000000000000000000000000000000000c000000000000000000000000000000000000000000000000000000000000000200000000000000000000000000a7e848aca42d879ef06507fca0e7b33a0a63c1e00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000008000000000000000000000000000000000000000000000000000000000000000648a6a1e85000000000000000000000000af88d065e77c8cc2239327c5edb3a432268e5831000000000000000000000000353c1f0bc78fbbc245b3c93ef77b1dcc5b77d2a0000000000000000000000000000000000000000000000000000000000028862000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000008000000000000000000000000000000000000000000000000000000000000001a49f865422000000000000000000000000af88d065e77c8cc2239327c5edb3a432268e583100000000000000000000000000000001000000000000000000000000000000010000000000000000000000000000000000000000000000000000000000000080000000000000000000000000000000000000000000000000000000000000004400000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000800000000000000000000000000000000000000000000000000000000000000064d1660f99000000000000000000000000af88d065e77c8cc2239327c5edb3a432268e58310000000000000000000000001231deb6f5749ef6ce6943a275a1d3e7486f4eae0000000000000000000000000000000000000000000000000000000000000001000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000100000000000000000000000000000000000000000000000000000000000000a0000000000000000000000000000000000000000000000000000126eecdd3c4590000000000000000000000000000000000000000000000000000000000000000000000000000000000000000c5fe6ef47965741f6f7a4734bf784bf3ae3f2452000000000000000000000000000000000000000000000000000000000000759a0000000000000000000000001493e7b8d4dfade0a178dad9335470337a3a219a0000000000000000000000000000000000000000000000000000000000285241000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000e000000000000000000000000000000000000000000000000000000000000001200000000000000000000000000000000000000000000000000000000000000380000000000000000000000000000000000000000000000000000000000000001800030100130300000000000000000000000000000008f11000000000000000000000000000000000000000000000000000000000000000000000000000000240eec104bed47120f58804c59125ae2572682610d39e80abc3838c99052db282750000000000000000000000000000000000000000000000000000000000000060000000000000000000000000c5fe6ef47965741f6f7a4734bf784bf3ae3f245200000000000000000000000000000000000000000000000000000000000000010000000000000000000000000000000000000000000000000000000000000020000000000000000000000000111111125421ca6dc452d289314280a0f8842a65000000000000000000000000111111125421ca6dc452d289314280a0f8842a65000000000000000000000000b97ef9ef8734c71904d8002f8b6bc66dd9c48a6e0000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000028190200000000000000000000000000000000000000000000000000000000000000e00000000000000000000000000000000000000000000000000000000000000001000000000000000000000000000000000000000000000000000000000000008883800a8e000000000000000000000000b97ef9ef8734c71904d8002f8b6bc66dd9c48a6e0000000000000000000000000000000000000000000000000000000000281902000000000000000000000000000000000000000000000000015c4b33745816ab18000000000000003b6d03400e0100ab771e9288e0aa97e11557e6654c3a96652a94d114000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000993b046fd255d06d2dd8b115d91923fdcc52bd60070f3bedce2fc45e7e8c6b7e4f1add057efd388c8a24d3eb0e6736cf87170452068cb12b7e09772463e1a5b71cc001a0dffb2def3272d3cfb47b41bd4e7163f2101aedf4a6315df0e5ac4acd60cd5363a04269e8777e5f5dba8d8fb8b9ead2b08c90b22642f9525e98539eeeda467a68c5', + }, + ], + [ + { + op: 'replace', + path: '/status', + value: 'submitted', + note: 'TransactionController#approveTransaction - Transaction submitted', + timestamp: 1730926316445, + }, + { + op: 'add', + path: '/gasFeeEstimates', + value: { + type: 'fee-market', + low: { + maxFeePerGas: '0x989680', + maxPriorityFeePerGas: '0x0', + }, + medium: { + maxFeePerGas: '0xcdfe60', + maxPriorityFeePerGas: '0x0', + }, + high: { + maxFeePerGas: '0x1036640', + maxPriorityFeePerGas: '0x0', + }, + }, + }, + { + op: 'add', + path: '/gasFeeEstimatesLoaded', + value: true, + }, + { + op: 'add', + path: '/hash', + value: + '0x010e1bffe8288956012e6b6132d7eb3eaf9d0bbf066bd13aae13b973c678508f', + }, + { + op: 'add', + path: '/submittedTime', + value: 1730926316444, + }, + ], + ], + r: '0xdffb2def3272d3cfb47b41bd4e7163f2101aedf4a6315df0e5ac4acd60cd5363', + s: '0x4269e8777e5f5dba8d8fb8b9ead2b08c90b22642f9525e98539eeeda467a68c5', + v: '0x1', + rawTx: + '0x02f9131882a4b181c380840104d37c831ceed09423981fc34e69eedfe2bd9a0a9fcb0719fe09dbfc8704b46d729a4459b912a53ce33bff000000000000000000000000000000000000000000000000000000000000008000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000004b46d729a445900000000000000000000000000000000000000000000000000000000000000c0000000000000000000000000000000000000000000000000000000000000000d6c6966694164617074657256320000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000011800000000000000000000000001231deb6f5749ef6ce6943a275a1d3e7486f4eae0000000000000000000000001231deb6f5749ef6ce6943a275a1d3e7486f4eae000000000000000000000000000000000000000000000000000000000000a86a0000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000038589602234000000000000000000000000000000000000000000000000000000000000000140000000000000000000000000000000000000000000000000000007f544a44c0000000000000000000000000056ca675c3633cc16bd6849e2b431d4e8de5e23bf0000000000000000000000000000000000000000000000000000000000001024a6010a66000000000000000000000000000000000000000000000000000000000000006000000000000000000000000000000000000000000000000000000000000002200000000000000000000000000000000000000000000000000000000000000be0eec104bed47120f58804c59125ae2572682610d39e80abc3838c99052db28275000000000000000000000000000000000000000000000000000000000000014000000000000000000000000000000000000000000000000000000000000001800000000000000000000000000000000000000000000000000000000000000000000000000000000000000000af88d065e77c8cc2239327c5edb3a432268e5831000000000000000000000000c5fe6ef47965741f6f7a4734bf784bf3ae3f245200000000000000000000000000000000000000000000000000000000002854dc000000000000000000000000000000000000000000000000000000000000a86a00000000000000000000000000000000000000000000000000000000000000010000000000000000000000000000000000000000000000000000000000000001000000000000000000000000000000000000000000000000000000000000000a7374617267617465563200000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000f6d6574616d61736b2d6272696467650000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000100000000000000000000000000000000000000000000000000000000000000200000000000000000000000006352a56caadc4f1e25cd6c75970fa768a3304e640000000000000000000000006352a56caadc4f1e25cd6c75970fa768a3304e640000000000000000000000000000000000000000000000000000000000000000000000000000000000000000af88d065e77c8cc2239327c5edb3a432268e5831000000000000000000000000000000000000000000000000000385896022340000000000000000000000000000000000000000000000000000000000000000e00000000000000000000000000000000000000000000000000000000000000001000000000000000000000000000000000000000000000000000000000000086490411a32000000000000000000000000f851d3d46237ec552a4c6e383a973115e781b1a5000000000000000000000000000000000000000000000000000000000000006000000000000000000000000000000000000000000000000000000000000001c0000000000000000000000000eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee000000000000000000000000af88d065e77c8cc2239327c5edb3a432268e5831000000000000000000000000f851d3d46237ec552a4c6e383a973115e781b1a50000000000000000000000001231deb6f5749ef6ce6943a275a1d3e7486f4eae0000000000000000000000000000000000000000000000000003858960223400000000000000000000000000000000000000000000000000000000000028524100000000000000000000000000000000000000000000000000000000002886200000000000000000000000000000000000000000000000000000000000000000000000000000000000000000933a06c631ed8b5e4f3848c91a1cfc45e5c7eab3000000000000000000000000000000000000000000000000000000000000014000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000004000000000000000000000000000000000000000000000000000000000000008000000000000000000000000000000000000000000000000000000000000001400000000000000000000000000000000000000000000000000000000000000300000000000000000000000000000000000000000000000000000000000000042000000000000000000000000082af49447d8a07e3bd95bd0d56f35241523fbab10000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000385896022340000000000000000000000000000000000000000000000000000000000000000800000000000000000000000000000000000000000000000000000000000000004d0e30db00000000000000000000000000000000000000000000000000000000000000000000000000000000071cb8a26f2c4eb68288bcc2ebaba4f72b065f07600000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000008000000000000000000000000000000000000000000000000000000000000001043eece7db000000000000000000000000f851d3d46237ec552a4c6e383a973115e781b1a5000000000000000000000000000000000000000000000000000385896022340000000000000000000000000000000000000000000000000000000000000000010000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000007fffffff00000000000000000000000000000000000000000000000000000000000000c000000000000000000000000000000000000000000000000000000000000000200000000000000000000000000a7e848aca42d879ef06507fca0e7b33a0a63c1e00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000008000000000000000000000000000000000000000000000000000000000000000648a6a1e85000000000000000000000000af88d065e77c8cc2239327c5edb3a432268e5831000000000000000000000000353c1f0bc78fbbc245b3c93ef77b1dcc5b77d2a0000000000000000000000000000000000000000000000000000000000028862000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000008000000000000000000000000000000000000000000000000000000000000001a49f865422000000000000000000000000af88d065e77c8cc2239327c5edb3a432268e583100000000000000000000000000000001000000000000000000000000000000010000000000000000000000000000000000000000000000000000000000000080000000000000000000000000000000000000000000000000000000000000004400000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000800000000000000000000000000000000000000000000000000000000000000064d1660f99000000000000000000000000af88d065e77c8cc2239327c5edb3a432268e58310000000000000000000000001231deb6f5749ef6ce6943a275a1d3e7486f4eae0000000000000000000000000000000000000000000000000000000000000001000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000100000000000000000000000000000000000000000000000000000000000000a0000000000000000000000000000000000000000000000000000126eecdd3c4590000000000000000000000000000000000000000000000000000000000000000000000000000000000000000c5fe6ef47965741f6f7a4734bf784bf3ae3f2452000000000000000000000000000000000000000000000000000000000000759a0000000000000000000000001493e7b8d4dfade0a178dad9335470337a3a219a0000000000000000000000000000000000000000000000000000000000285241000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000e000000000000000000000000000000000000000000000000000000000000001200000000000000000000000000000000000000000000000000000000000000380000000000000000000000000000000000000000000000000000000000000001800030100130300000000000000000000000000000008f11000000000000000000000000000000000000000000000000000000000000000000000000000000240eec104bed47120f58804c59125ae2572682610d39e80abc3838c99052db282750000000000000000000000000000000000000000000000000000000000000060000000000000000000000000c5fe6ef47965741f6f7a4734bf784bf3ae3f245200000000000000000000000000000000000000000000000000000000000000010000000000000000000000000000000000000000000000000000000000000020000000000000000000000000111111125421ca6dc452d289314280a0f8842a65000000000000000000000000111111125421ca6dc452d289314280a0f8842a65000000000000000000000000b97ef9ef8734c71904d8002f8b6bc66dd9c48a6e0000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000028190200000000000000000000000000000000000000000000000000000000000000e00000000000000000000000000000000000000000000000000000000000000001000000000000000000000000000000000000000000000000000000000000008883800a8e000000000000000000000000b97ef9ef8734c71904d8002f8b6bc66dd9c48a6e0000000000000000000000000000000000000000000000000000000000281902000000000000000000000000000000000000000000000000015c4b33745816ab18000000000000003b6d03400e0100ab771e9288e0aa97e11557e6654c3a96652a94d114000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000993b046fd255d06d2dd8b115d91923fdcc52bd60070f3bedce2fc45e7e8c6b7e4f1add057efd388c8a24d3eb0e6736cf87170452068cb12b7e09772463e1a5b71cc001a0dffb2def3272d3cfb47b41bd4e7163f2101aedf4a6315df0e5ac4acd60cd5363a04269e8777e5f5dba8d8fb8b9ead2b08c90b22642f9525e98539eeeda467a68c5', + gasFeeEstimates: { + type: 'fee-market', + low: { + maxFeePerGas: '0x989680', + maxPriorityFeePerGas: '0x0', + }, + medium: { + maxFeePerGas: '0xcdfe60', + maxPriorityFeePerGas: '0x0', + }, + high: { + maxFeePerGas: '0x1036640', + maxPriorityFeePerGas: '0x0', + }, + }, + gasFeeEstimatesLoaded: true, + hash: '0x010e1bffe8288956012e6b6132d7eb3eaf9d0bbf066bd13aae13b973c678508f', + submittedTime: 1730926316444, + }, + }, +}; + +export const DefaultStory: StoryFn = () => { + return ( + + + + + + ); +}; +DefaultStory.storyName = 'Default'; + +export const LongNetworkNameStory: StoryFn = () => { + return ( + + + + ); +}; +LongNetworkNameStory.storyName = 'Long Network Name'; diff --git a/ui/pages/bridge/transaction-details/bridge-step-list.tsx b/ui/pages/bridge/transaction-details/bridge-step-list.tsx new file mode 100644 index 000000000000..b715c29e1f0e --- /dev/null +++ b/ui/pages/bridge/transaction-details/bridge-step-list.tsx @@ -0,0 +1,98 @@ +import React from 'react'; +import { NetworkConfiguration } from '@metamask/network-controller'; +import { TransactionMeta } from '@metamask/transaction-controller'; +import { Hex } from '@metamask/utils'; +import { Box } from '../../../components/component-library'; +import { + BridgeHistoryItem, + StatusTypes, +} from '../../../../shared/types/bridge-status'; +import { formatDate } from '../../../helpers/utils/util'; +import BridgeStepDescription, { + getStepStatus, +} from './bridge-step-description'; +import StepProgressBarItem from './step-progress-bar-item'; + +const getTime = ( + index: number, + isLastIndex: boolean, + startTime?: number, + estimatedProcessingTimeInSeconds?: number, +) => { + if (index === 0) { + return startTime; + } + + return isLastIndex && startTime && estimatedProcessingTimeInSeconds + ? startTime + estimatedProcessingTimeInSeconds * 1000 + : undefined; +}; + +type BridgeStepsProps = { + bridgeHistoryItem: BridgeHistoryItem; + srcChainTxMeta?: TransactionMeta; + networkConfigurationsByChainId: Record; +}; + +export default function BridgeStepList({ + bridgeHistoryItem, + srcChainTxMeta, + networkConfigurationsByChainId, +}: BridgeStepsProps) { + const { steps } = bridgeHistoryItem.quote; + const stepStatuses = steps.map((step) => + getStepStatus(bridgeHistoryItem, step, srcChainTxMeta), + ); + + return ( + + {steps.map((step, i) => { + const prevStepStatus = i > 0 ? stepStatuses[i - 1] : null; + const stepStatus = stepStatuses[i]; + const nextStepStatus = + i < stepStatuses.length - 1 ? stepStatuses[i + 1] : null; + + const isEdgeComplete = + stepStatus === StatusTypes.COMPLETE && + (nextStepStatus === StatusTypes.PENDING || + nextStepStatus === StatusTypes.COMPLETE); + + // Making a distinction betweeen displayedStepStatus and stepStatus + // stepStatus is determined independently of other steps + // So despite both being technically PENDING, + // We only want a single spinner animation at a time, so we need to take into account other steps + const displayedStepStatus = + prevStepStatus === StatusTypes.PENDING && + stepStatus === StatusTypes.PENDING + ? null + : stepStatus; + + const time = formatDate( + getTime( + i, + i === steps.length - 1, + bridgeHistoryItem.startTime, + bridgeHistoryItem.estimatedProcessingTimeInSeconds, + ), + 'hh:mm a', + ); + + return ( + + + + ); + })} + + ); +} diff --git a/ui/pages/bridge/transaction-details/hollow-circle.tsx b/ui/pages/bridge/transaction-details/hollow-circle.tsx new file mode 100644 index 000000000000..4f84cf5ec0e7 --- /dev/null +++ b/ui/pages/bridge/transaction-details/hollow-circle.tsx @@ -0,0 +1,39 @@ +import * as React from 'react'; +import classnames from 'classnames'; +import { + Box, + IconProps, + IconSize, + PolymorphicRef, +} from '../../../components/component-library'; +import { + BackgroundColor, + Display, +} from '../../../helpers/constants/design-system'; + +const HollowCircle = React.forwardRef( + ( + { size = IconSize.Md, color, className = '', style }: IconProps, + ref?: PolymorphicRef, + ) => { + return ( + + ); + }, +); + +export default HollowCircle; diff --git a/ui/pages/bridge/transaction-details/index.scss b/ui/pages/bridge/transaction-details/index.scss new file mode 100644 index 000000000000..7832fffdd6a5 --- /dev/null +++ b/ui/pages/bridge/transaction-details/index.scss @@ -0,0 +1,50 @@ +.bridge-transaction-details { + &__icon-loading { + animation: loading-dot 1.2s linear infinite; + } + + &__segment { + height: 4px; + width: 0; + transition: width 1.5s cubic-bezier(0.68, -0.55, 0.27, 1.55); + + &--pending { + width: 50%; + } + + &--complete { + width: 100%; + } + } + + &__step-grid { + display: grid; + grid-template-columns: 1rem 6fr; + column-gap: 1rem; + justify-items: center; + align-items: center; + + &--desc { + justify-self: start; + } + } + + @keyframes loading-dot { + 0% { + transform: scale(1); + opacity: 1; + } + + 45% { + transform: scale(0.7); + opacity: 0.7; + } + + 80% { + transform: scale(1); + opacity: 1; + } + } +} + + diff --git a/ui/pages/bridge/transaction-details/pulsing-circle.tsx b/ui/pages/bridge/transaction-details/pulsing-circle.tsx new file mode 100644 index 000000000000..9933e3f8f613 --- /dev/null +++ b/ui/pages/bridge/transaction-details/pulsing-circle.tsx @@ -0,0 +1,51 @@ +import React from 'react'; +import { Box, IconSize } from '../../../components/component-library'; +import { + AlignItems, + BackgroundColor, + BorderRadius, + Display, + IconColor, + JustifyContent, +} from '../../../helpers/constants/design-system'; +import HollowCircle from './hollow-circle'; + +/** + * Renders the steps in the Bridge Transaction Details page + * + * @param options + * @param options.iconSize - The size of the icon + * @param options.color - The color of the icon + */ +export default function PulsingCircle({ + iconSize, + color, +}: { + iconSize: IconSize; + color: IconColor; +}) { + return ( + + + + + ); +} diff --git a/ui/pages/bridge/transaction-details/segment.stories.tsx b/ui/pages/bridge/transaction-details/segment.stories.tsx new file mode 100644 index 000000000000..d00b3dc8fedd --- /dev/null +++ b/ui/pages/bridge/transaction-details/segment.stories.tsx @@ -0,0 +1,46 @@ +import React, { useEffect } from 'react'; +import { StoryFn, Meta } from '@storybook/react'; +import { + BlockSize, + Display, + FlexDirection, +} from '../../../helpers/constants/design-system'; +import Segment from './segment'; +import { Box } from '../../../components/component-library'; +import { StatusTypes } from '../../../../shared/types/bridge-status'; + +export default { + title: 'pages/bridge/transaction-details/Segment', + component: Segment, +} as Meta; + +const types = [StatusTypes.PENDING, StatusTypes.COMPLETE, null]; + +export const DefaultStory: StoryFn = () => { + const [typeIndex, setType] = React.useState( + StatusTypes.PENDING, + ); + + useEffect(() => { + let currentIndex = 0; + + const interval = setInterval(() => { + currentIndex = (currentIndex + 1) % types.length; + setType(types[currentIndex]); + }, 2000); + + return () => clearInterval(interval); + }, []); + + return ( + + + + ); +}; +DefaultStory.storyName = 'Default'; diff --git a/ui/pages/bridge/transaction-details/segment.tsx b/ui/pages/bridge/transaction-details/segment.tsx new file mode 100644 index 000000000000..4cbe1354bbe3 --- /dev/null +++ b/ui/pages/bridge/transaction-details/segment.tsx @@ -0,0 +1,32 @@ +import React from 'react'; +import classnames from 'classnames'; +import { Box } from '../../../components/component-library'; +import { + BackgroundColor, + BlockSize, + BorderRadius, +} from '../../../helpers/constants/design-system'; +import { StatusTypes } from '../../../../shared/types/bridge-status'; + +export default function Segment({ type }: { type: StatusTypes | null }) { + return ( + + + + ); +} diff --git a/ui/pages/bridge/transaction-details/step-progress-bar-item.tsx b/ui/pages/bridge/transaction-details/step-progress-bar-item.tsx new file mode 100644 index 000000000000..504b2400b7be --- /dev/null +++ b/ui/pages/bridge/transaction-details/step-progress-bar-item.tsx @@ -0,0 +1,82 @@ +import React from 'react'; +import { StatusTypes } from '../../../../shared/types/bridge-status'; +import { + Icon, + IconName, + IconSize, +} from '../../../components/component-library'; +import { IconColor } from '../../../helpers/constants/design-system'; +import HollowCircle from './hollow-circle'; +import PulsingCircle from './pulsing-circle'; + +const ICON_SIZE = IconSize.Xs; + +const VerticalLine = ({ color }: { color: IconColor }) => ( +

+); + +type StepsProgressBarItemProps = { + stepStatus: StatusTypes | null; + isLastItem: boolean; + isEdgeComplete: boolean; + children: React.ReactNode; +}; + +/** + * Renders the steps in the Bridge Transaction Details page + * + * @param options + * @param options.stepStatus - The status of the step + * @param options.isLastItem - Whether the step is the last item + * @param options.isEdgeComplete - Whether the edge is complete + * @param options.children - The description of the step to be rendered + */ +export default function StepProgressBarItem({ + stepStatus, + isLastItem, + isEdgeComplete, + children, +}: StepsProgressBarItemProps) { + return ( + <> + {/* Indicator dots */} + {stepStatus === null && ( + + )} + {stepStatus === StatusTypes.PENDING && ( + + )} + {stepStatus === StatusTypes.COMPLETE && ( + + )} + + {/* Description */} + {children} + + {/* Line */} + {!isLastItem && ( + + )} + + {/* Blank div to take up space to make sure everything is aligned */} + {!isLastItem &&
} + + ); +} diff --git a/ui/pages/bridge/transaction-details/transaction-detail-row.tsx b/ui/pages/bridge/transaction-details/transaction-detail-row.tsx new file mode 100644 index 000000000000..99b3756ee7e0 --- /dev/null +++ b/ui/pages/bridge/transaction-details/transaction-detail-row.tsx @@ -0,0 +1,40 @@ +import React from 'react'; +import { Box, Text } from '../../../components/component-library'; +import { + JustifyContent, + Display, + FlexDirection, + AlignItems, + TextAlign, + TextVariant, +} from '../../../helpers/constants/design-system'; + +type TransactionDetailRowProps = { + title: string; + value: React.ReactNode; +}; + +export default function TransactionDetailRow({ + title, + value, +}: TransactionDetailRowProps) { + return ( + + + {title} + + + {value} + + + ); +} diff --git a/ui/pages/bridge/transaction-details/transaction-details.tsx b/ui/pages/bridge/transaction-details/transaction-details.tsx new file mode 100644 index 000000000000..de8829143eca --- /dev/null +++ b/ui/pages/bridge/transaction-details/transaction-details.tsx @@ -0,0 +1,330 @@ +import React from 'react'; +import { useSelector } from 'react-redux'; +import { useHistory, useParams } from 'react-router-dom'; +import { NetworkConfiguration } from '@metamask/network-controller'; +import { TransactionMeta } from '@metamask/transaction-controller'; +import { + AvatarNetwork, + AvatarNetworkSize, + Box, + ButtonIcon, + ButtonIconSize, + IconName, + Text, +} from '../../../components/component-library'; +import { Content, Header } from '../../../components/multichain/pages/page'; +import { selectBridgeHistoryForAccount } from '../../../ducks/bridge-status/selectors'; +import useBridgeChainInfo from '../../../hooks/bridge/useBridgeChainInfo'; +import { + getNetworkConfigurationsByChainId, + selectedAddressTxListSelector, +} from '../../../selectors'; +import { getTransactionBreakdownData } from '../../../components/app/transaction-breakdown/transaction-breakdown-utils'; +import { MetaMaskReduxState } from '../../../store/store'; +import { hexToDecimal } from '../../../../shared/modules/conversion.utils'; +import UserPreferencedCurrencyDisplay from '../../../components/app/user-preferenced-currency-display/user-preferenced-currency-display.component'; +import { EtherDenomination } from '../../../../shared/constants/common'; +import { PRIMARY } from '../../../helpers/constants/common'; +import CurrencyDisplay from '../../../components/ui/currency-display/currency-display.component'; +import { StatusTypes } from '../../../../shared/types/bridge-status'; +import { + AlignItems, + Display, + FlexDirection, + TextColor, + TextTransform, +} from '../../../helpers/constants/design-system'; +import { formatDate } from '../../../helpers/utils/util'; +import { ConfirmInfoRowDivider as Divider } from '../../../components/app/confirm/info/row'; +import { calcTokenAmount } from '../../../../shared/lib/transactions-controller-utils'; +import { useI18nContext } from '../../../hooks/useI18nContext'; +import { CHAIN_ID_TO_NETWORK_IMAGE_URL_MAP } from '../../../../shared/constants/network'; +import TransactionDetailRow from './transaction-detail-row'; +import BridgeExplorerLinks from './bridge-explorer-links'; +import BridgeStepList from './bridge-step-list'; + +const getBlockExplorerUrl = ( + networkConfiguration: NetworkConfiguration | undefined, + txHash: string | undefined, +) => { + if (!networkConfiguration || !txHash) { + return undefined; + } + const index = networkConfiguration.defaultBlockExplorerUrlIndex; + if (index === undefined) { + return undefined; + } + + const rootUrl = networkConfiguration.blockExplorerUrls[index]?.replace( + /\/$/u, + '', + ); + return `${rootUrl}/tx/${txHash}`; +}; + +const StatusToColorMap: Record = { + [StatusTypes.PENDING]: TextColor.warningDefault, + [StatusTypes.COMPLETE]: TextColor.successDefault, + [StatusTypes.FAILED]: TextColor.errorDefault, + [StatusTypes.UNKNOWN]: TextColor.errorDefault, +}; + +const CrossChainSwapTxDetails = () => { + const t = useI18nContext(); + const rootState = useSelector((state) => state); + const history = useHistory(); + const { srcTxHash } = useParams<{ srcTxHash: string }>(); + const bridgeHistory = useSelector(selectBridgeHistoryForAccount); + const selectedAddressTxList = useSelector( + selectedAddressTxListSelector, + ) as TransactionMeta[]; + const bridgeHistoryItem = srcTxHash ? bridgeHistory[srcTxHash] : undefined; + const networkConfigurationsByChainId = useSelector( + getNetworkConfigurationsByChainId, + ); + const { srcNetwork, destNetwork } = useBridgeChainInfo({ + bridgeHistoryItem, + }); + + const srcBlockExplorerUrl = getBlockExplorerUrl(srcNetwork, srcTxHash); + + const destTxHash = bridgeHistoryItem?.status.destChain?.txHash; + const destBlockExplorerUrl = getBlockExplorerUrl(destNetwork, destTxHash); + + const srcChainTxMeta = selectedAddressTxList.find( + (tx) => tx.hash === srcTxHash, + ); + + const status = bridgeHistoryItem?.status.status; + + const destChainIconUrl = destNetwork + ? CHAIN_ID_TO_NETWORK_IMAGE_URL_MAP[ + destNetwork.chainId as keyof typeof CHAIN_ID_TO_NETWORK_IMAGE_URL_MAP + ] + : undefined; + const bridgeTypeDirection = t('bridgeTypeDirectionTo'); + const srcNetworkName = srcNetwork?.name; + const destNetworkName = destNetwork?.name; + + const data = srcChainTxMeta + ? getTransactionBreakdownData({ + state: rootState as MetaMaskReduxState, + transaction: srcChainTxMeta, + isTokenApprove: false, + }) + : undefined; + + const bridgeAmount = bridgeHistoryItem + ? `${calcTokenAmount( + bridgeHistoryItem.quote.srcTokenAmount, + bridgeHistoryItem.quote.srcAsset.decimals, + ).toFixed()} ${bridgeHistoryItem.quote.srcAsset.symbol}` + : undefined; + + return ( +
+
+
history.goBack()} + /> + } + > + {t('bridge')} details +
+ + + {status !== StatusTypes.COMPLETE && bridgeHistoryItem && ( + + )} + + {/* Links to block explorers */} + + + + + {/* General tx details */} + + + {status?.toLowerCase()} + + } + /> + + {status !== StatusTypes.COMPLETE && ( + + {bridgeTypeDirection}{' '} + {destNetwork && ( + + )} + {destNetworkName} + + } + /> + )} + {status === StatusTypes.COMPLETE && ( + <> + + + + )} + + + + + + + + + + + {data?.isEIP1559Transaction && + typeof data?.baseFee !== 'undefined' && ( + + } + /> + )} + {data?.isEIP1559Transaction && + typeof data?.priorityFee !== 'undefined' && ( + + } + /> + )} + + + } + /> + + } + /> + + + + + + } + /> + + + + +
+
+ ); +}; + +export default CrossChainSwapTxDetails; diff --git a/ui/pages/create-account/connect-hardware/index.test.tsx b/ui/pages/create-account/connect-hardware/index.test.tsx index 3f7782c1416d..d5f367e3d74f 100644 --- a/ui/pages/create-account/connect-hardware/index.test.tsx +++ b/ui/pages/create-account/connect-hardware/index.test.tsx @@ -23,6 +23,7 @@ jest.mock('../../../store/actions', () => ({ jest.mock('../../../selectors', () => ({ getCurrentChainId: () => '0x1', + getSelectedAddress: () => '0xselectedAddress', getRpcPrefsForCurrentProvider: () => { return {}; }, diff --git a/ui/pages/routes/routes.component.js b/ui/pages/routes/routes.component.js index edf1ff8bbe22..0416cecee4f5 100644 --- a/ui/pages/routes/routes.component.js +++ b/ui/pages/routes/routes.component.js @@ -83,6 +83,7 @@ import { NOTIFICATIONS_ROUTE, NOTIFICATIONS_SETTINGS_ROUTE, CROSS_CHAIN_SWAP_ROUTE, + CROSS_CHAIN_SWAP_TX_DETAILS_ROUTE, } from '../../helpers/constants/routes'; import { @@ -114,6 +115,7 @@ import NetworkConfirmationPopover from '../../components/multichain/network-list import NftFullImage from '../../components/app/assets/nfts/nft-details/nft-full-image'; import CrossChainSwap from '../bridge'; import { ToastMaster } from '../../components/app/toast-master/toast-master'; +import CrossChainSwapTxDetails from '../bridge/transaction-details/transaction-details'; import { isCorrectDeveloperTransactionType, isCorrectSignatureApprovalType, @@ -300,6 +302,11 @@ export default class Routes extends Component { /> +