From b1ba64226fccb521b738ec6cc11e921bb52e0750 Mon Sep 17 00:00:00 2001 From: IF <139582705+infiniteflower@users.noreply.github.com> Date: Thu, 21 Nov 2024 16:47:47 -0500 Subject: [PATCH 1/4] chore: commit all background code --- app/scripts/constants/sentry-state.ts | 5 + .../bridge-status-controller.test.ts.snap | 213 +++++ .../bridge-status-controller.test.ts | 739 ++++++++++++++++++ .../bridge-status/bridge-status-controller.ts | 316 ++++++++ .../controllers/bridge-status/constants.ts | 10 + .../controllers/bridge-status/types.ts | 56 ++ .../controllers/bridge-status/utils.ts | 49 ++ .../bridge-status/validators.test.ts | 238 ++++++ .../controllers/bridge-status/validators.ts | 179 +++++ app/scripts/metamask-controller.js | 31 + shared/constants/transaction.ts | 5 + shared/types/bridge-status.ts | 146 ++++ test/data/mock-state.json | 3 + ...rs-after-init-opt-in-background-state.json | 19 +- .../errors-after-init-opt-in-ui-state.json | 21 +- 15 files changed, 2011 insertions(+), 19 deletions(-) create mode 100644 app/scripts/controllers/bridge-status/__snapshots__/bridge-status-controller.test.ts.snap create mode 100644 app/scripts/controllers/bridge-status/bridge-status-controller.test.ts create mode 100644 app/scripts/controllers/bridge-status/bridge-status-controller.ts create mode 100644 app/scripts/controllers/bridge-status/constants.ts create mode 100644 app/scripts/controllers/bridge-status/types.ts create mode 100644 app/scripts/controllers/bridge-status/utils.ts create mode 100644 app/scripts/controllers/bridge-status/validators.test.ts create mode 100644 app/scripts/controllers/bridge-status/validators.ts create mode 100644 shared/types/bridge-status.ts 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..502077543c5d --- /dev/null +++ b/app/scripts/controllers/bridge-status/bridge-status-controller.ts @@ -0,0 +1,316 @@ +import { StateMetadata } from '@metamask/base-controller'; +import { StaticIntervalPollingController } from '@metamask/polling-controller'; +import { Hex } from '@metamask/utils'; +import { Numeric } from '../../../../shared/modules/Numeric'; +// eslint-disable-next-line import/no-restricted-paths +import { + StartPollingForBridgeTxStatusArgs, + StatusRequest, + StatusTypes, + BridgeStatusControllerState, +} from '../../../../shared/types/bridge-status'; +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 = new Numeric(statusRequest.srcChainId, 10) + .toPrefixedHexString() + .toLowerCase() as `0x${string}`; + 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 = new Numeric(statusRequest.srcChainId, 10) + .toPrefixedHexString() + .toLowerCase() as `0x${string}`; + + 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 = new Numeric( + bridgeHistoryItem.quote.srcChainId, + 10, + ).toPrefixedHexString() as `0x${string}`; + const hexDestChainId = new Numeric( + bridgeHistoryItem.quote.destChainId, + 10, + ).toPrefixedHexString() as `0x${string}`; + + 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/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", From 72452fe8330bda0e10c46b55c29744947dae47ed Mon Sep 17 00:00:00 2001 From: IF <139582705+infiniteflower@users.noreply.github.com> Date: Fri, 22 Nov 2024 13:42:05 -0500 Subject: [PATCH 2/4] chore: make a decToPrefixedHex util and use it in bridgeStatus code --- .../bridge-status/bridge-status-controller.ts | 20 +++++++------------ shared/modules/conversion.utils.ts | 6 ++++++ ui/pages/bridge/hooks/useAddToken.ts | 8 ++++---- ui/pages/bridge/hooks/useHandleApprovalTx.ts | 7 ++----- ui/pages/bridge/hooks/useHandleTx.ts | 7 ++----- 5 files changed, 21 insertions(+), 27 deletions(-) diff --git a/app/scripts/controllers/bridge-status/bridge-status-controller.ts b/app/scripts/controllers/bridge-status/bridge-status-controller.ts index 502077543c5d..18010ae0de3d 100644 --- a/app/scripts/controllers/bridge-status/bridge-status-controller.ts +++ b/app/scripts/controllers/bridge-status/bridge-status-controller.ts @@ -1,7 +1,6 @@ import { StateMetadata } from '@metamask/base-controller'; import { StaticIntervalPollingController } from '@metamask/polling-controller'; import { Hex } from '@metamask/utils'; -import { Numeric } from '../../../../shared/modules/Numeric'; // eslint-disable-next-line import/no-restricted-paths import { StartPollingForBridgeTxStatusArgs, @@ -9,6 +8,7 @@ import { StatusTypes, BridgeStatusControllerState, } from '../../../../shared/types/bridge-status'; +import { decimalToPrefixedHex } from '../../../../shared/modules/conversion.utils'; import { BRIDGE_STATUS_CONTROLLER_NAME, DEFAULT_BRIDGE_STATUS_CONTROLLER_STATE, @@ -143,9 +143,7 @@ export default class BridgeStatusController extends StaticIntervalPollingControl refuel: Boolean(historyItem.quote.refuel), }; - const hexSourceChainId = new Numeric(statusRequest.srcChainId, 10) - .toPrefixedHexString() - .toLowerCase() as `0x${string}`; + const hexSourceChainId = decimalToPrefixedHex(statusRequest.srcChainId); const networkClientId = this.messagingSystem.call( 'NetworkController:findNetworkClientIdByChainId', hexSourceChainId, @@ -171,9 +169,7 @@ export default class BridgeStatusController extends StaticIntervalPollingControl initialDestAssetBalance, targetContractAddress, } = startPollingForBridgeTxStatusArgs; - const hexSourceChainId = new Numeric(statusRequest.srcChainId, 10) - .toPrefixedHexString() - .toLowerCase() as `0x${string}`; + const hexSourceChainId = decimalToPrefixedHex(statusRequest.srcChainId); const { bridgeStatusState } = this.state; const { address: account } = this.#getSelectedAccount(); @@ -277,14 +273,12 @@ export default class BridgeStatusController extends StaticIntervalPollingControl const bridgeHistoryItem = this.state.bridgeStatusState.txHistory[sourceTxHash]; - const hexSourceChainId = new Numeric( + const hexSourceChainId = decimalToPrefixedHex( bridgeHistoryItem.quote.srcChainId, - 10, - ).toPrefixedHexString() as `0x${string}`; - const hexDestChainId = new Numeric( + ); + const hexDestChainId = decimalToPrefixedHex( bridgeHistoryItem.quote.destChainId, - 10, - ).toPrefixedHexString() as `0x${string}`; + ); return ( bridgeHistoryItem.account === address && 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/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..f681f04c3cb4 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 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, From 0cd08cb04267ceaa3e69fc0f2458f11e4d207398 Mon Sep 17 00:00:00 2001 From: IF <139582705+infiniteflower@users.noreply.github.com> Date: Fri, 22 Nov 2024 13:43:41 -0500 Subject: [PATCH 3/4] Revert "chore: make a decToPrefixedHex util and use it in bridgeStatus code" This reverts commit 72452fe8330bda0e10c46b55c29744947dae47ed. --- .../bridge-status/bridge-status-controller.ts | 20 ++++++++++++------- shared/modules/conversion.utils.ts | 6 ------ ui/pages/bridge/hooks/useAddToken.ts | 8 ++++---- ui/pages/bridge/hooks/useHandleApprovalTx.ts | 7 +++++-- ui/pages/bridge/hooks/useHandleTx.ts | 7 +++++-- 5 files changed, 27 insertions(+), 21 deletions(-) diff --git a/app/scripts/controllers/bridge-status/bridge-status-controller.ts b/app/scripts/controllers/bridge-status/bridge-status-controller.ts index 18010ae0de3d..502077543c5d 100644 --- a/app/scripts/controllers/bridge-status/bridge-status-controller.ts +++ b/app/scripts/controllers/bridge-status/bridge-status-controller.ts @@ -1,6 +1,7 @@ import { StateMetadata } from '@metamask/base-controller'; import { StaticIntervalPollingController } from '@metamask/polling-controller'; import { Hex } from '@metamask/utils'; +import { Numeric } from '../../../../shared/modules/Numeric'; // eslint-disable-next-line import/no-restricted-paths import { StartPollingForBridgeTxStatusArgs, @@ -8,7 +9,6 @@ import { StatusTypes, BridgeStatusControllerState, } from '../../../../shared/types/bridge-status'; -import { decimalToPrefixedHex } from '../../../../shared/modules/conversion.utils'; import { BRIDGE_STATUS_CONTROLLER_NAME, DEFAULT_BRIDGE_STATUS_CONTROLLER_STATE, @@ -143,7 +143,9 @@ export default class BridgeStatusController extends StaticIntervalPollingControl refuel: Boolean(historyItem.quote.refuel), }; - const hexSourceChainId = decimalToPrefixedHex(statusRequest.srcChainId); + const hexSourceChainId = new Numeric(statusRequest.srcChainId, 10) + .toPrefixedHexString() + .toLowerCase() as `0x${string}`; const networkClientId = this.messagingSystem.call( 'NetworkController:findNetworkClientIdByChainId', hexSourceChainId, @@ -169,7 +171,9 @@ export default class BridgeStatusController extends StaticIntervalPollingControl initialDestAssetBalance, targetContractAddress, } = startPollingForBridgeTxStatusArgs; - const hexSourceChainId = decimalToPrefixedHex(statusRequest.srcChainId); + const hexSourceChainId = new Numeric(statusRequest.srcChainId, 10) + .toPrefixedHexString() + .toLowerCase() as `0x${string}`; const { bridgeStatusState } = this.state; const { address: account } = this.#getSelectedAccount(); @@ -273,12 +277,14 @@ export default class BridgeStatusController extends StaticIntervalPollingControl const bridgeHistoryItem = this.state.bridgeStatusState.txHistory[sourceTxHash]; - const hexSourceChainId = decimalToPrefixedHex( + const hexSourceChainId = new Numeric( bridgeHistoryItem.quote.srcChainId, - ); - const hexDestChainId = decimalToPrefixedHex( + 10, + ).toPrefixedHexString() as `0x${string}`; + const hexDestChainId = new Numeric( bridgeHistoryItem.quote.destChainId, - ); + 10, + ).toPrefixedHexString() as `0x${string}`; return ( bridgeHistoryItem.account === address && diff --git a/shared/modules/conversion.utils.ts b/shared/modules/conversion.utils.ts index 9e24a8427cce..75da336eb8e6 100644 --- a/shared/modules/conversion.utils.ts +++ b/shared/modules/conversion.utils.ts @@ -184,12 +184,6 @@ 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/ui/pages/bridge/hooks/useAddToken.ts b/ui/pages/bridge/hooks/useAddToken.ts index 0a2ed28ef10a..597149b16e49 100644 --- a/ui/pages/bridge/hooks/useAddToken.ts +++ b/ui/pages/bridge/hooks/useAddToken.ts @@ -1,5 +1,6 @@ import { useDispatch, useSelector } from 'react-redux'; import { NetworkConfiguration } from '@metamask/network-controller'; +import { Numeric } from '../../../../shared/modules/Numeric'; import { QuoteResponse } from '../types'; import { getNetworkConfigurationsByChainId, @@ -7,7 +8,6 @@ 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 = decimalToPrefixedHex( - quoteResponse.quote.destChainId, - ); + const hexDestChainId = new Numeric(quoteResponse.quote.destChainId, 10) + .toPrefixedHexString() + .toLowerCase() as `0x${string}`; 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 f681f04c3cb4..67f2abf67e7e 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,7 +59,10 @@ export default function useHandleApprovalTx() { approval: TxData; quoteResponse: QuoteResponse; }) => { - const hexChainId = decimalToPrefixedHex(approval.chainId); + const hexChainId = new Numeric( + approval.chainId, + 10, + ).toPrefixedHexString() as `0x${string}`; // 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 diff --git a/ui/pages/bridge/hooks/useHandleTx.ts b/ui/pages/bridge/hooks/useHandleTx.ts index ffd378449bc7..a4cbf631c338 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 { decimalToPrefixedHex } from '../../../../shared/modules/conversion.utils'; +import { Numeric } from '../../../../shared/modules/Numeric'; export default function useHandleTx() { const dispatch = useDispatch(); @@ -42,7 +42,10 @@ export default function useHandleTx() { meta: Partial; }; }) => { - const hexChainId = decimalToPrefixedHex(txParams.chainId); + const hexChainId = new Numeric( + txParams.chainId, + 10, + ).toPrefixedHexString() as `0x${string}`; const { maxFeePerGas, maxPriorityFeePerGas } = await getTxGasEstimates({ networkAndAccountSupports1559, From 8fc0d07ea4b4d1b5102f8a9fd58f186328b7bf22 Mon Sep 17 00:00:00 2001 From: IF <139582705+infiniteflower@users.noreply.github.com> Date: Fri, 22 Nov 2024 13:44:46 -0500 Subject: [PATCH 4/4] chore: add dec to prefixed hex and consume --- .../bridge-status/bridge-status-controller.ts | 20 +++++++------------ shared/modules/conversion.utils.ts | 6 ++++++ 2 files changed, 13 insertions(+), 13 deletions(-) diff --git a/app/scripts/controllers/bridge-status/bridge-status-controller.ts b/app/scripts/controllers/bridge-status/bridge-status-controller.ts index 502077543c5d..18010ae0de3d 100644 --- a/app/scripts/controllers/bridge-status/bridge-status-controller.ts +++ b/app/scripts/controllers/bridge-status/bridge-status-controller.ts @@ -1,7 +1,6 @@ import { StateMetadata } from '@metamask/base-controller'; import { StaticIntervalPollingController } from '@metamask/polling-controller'; import { Hex } from '@metamask/utils'; -import { Numeric } from '../../../../shared/modules/Numeric'; // eslint-disable-next-line import/no-restricted-paths import { StartPollingForBridgeTxStatusArgs, @@ -9,6 +8,7 @@ import { StatusTypes, BridgeStatusControllerState, } from '../../../../shared/types/bridge-status'; +import { decimalToPrefixedHex } from '../../../../shared/modules/conversion.utils'; import { BRIDGE_STATUS_CONTROLLER_NAME, DEFAULT_BRIDGE_STATUS_CONTROLLER_STATE, @@ -143,9 +143,7 @@ export default class BridgeStatusController extends StaticIntervalPollingControl refuel: Boolean(historyItem.quote.refuel), }; - const hexSourceChainId = new Numeric(statusRequest.srcChainId, 10) - .toPrefixedHexString() - .toLowerCase() as `0x${string}`; + const hexSourceChainId = decimalToPrefixedHex(statusRequest.srcChainId); const networkClientId = this.messagingSystem.call( 'NetworkController:findNetworkClientIdByChainId', hexSourceChainId, @@ -171,9 +169,7 @@ export default class BridgeStatusController extends StaticIntervalPollingControl initialDestAssetBalance, targetContractAddress, } = startPollingForBridgeTxStatusArgs; - const hexSourceChainId = new Numeric(statusRequest.srcChainId, 10) - .toPrefixedHexString() - .toLowerCase() as `0x${string}`; + const hexSourceChainId = decimalToPrefixedHex(statusRequest.srcChainId); const { bridgeStatusState } = this.state; const { address: account } = this.#getSelectedAccount(); @@ -277,14 +273,12 @@ export default class BridgeStatusController extends StaticIntervalPollingControl const bridgeHistoryItem = this.state.bridgeStatusState.txHistory[sourceTxHash]; - const hexSourceChainId = new Numeric( + const hexSourceChainId = decimalToPrefixedHex( bridgeHistoryItem.quote.srcChainId, - 10, - ).toPrefixedHexString() as `0x${string}`; - const hexDestChainId = new Numeric( + ); + const hexDestChainId = decimalToPrefixedHex( bridgeHistoryItem.quote.destChainId, - 10, - ).toPrefixedHexString() as `0x${string}`; + ); return ( bridgeHistoryItem.account === address && 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(); }