diff --git a/.gitignore b/.gitignore index 074f4076a7cc..6ee150dd8653 100644 --- a/.gitignore +++ b/.gitignore @@ -1,3 +1,6 @@ +.yalc +yalc.lock + npm-debug.log yarn-error.log node_modules diff --git a/app/_locales/en/messages.json b/app/_locales/en/messages.json index f125126e878c..07dc59d6c895 100644 --- a/app/_locales/en/messages.json +++ b/app/_locales/en/messages.json @@ -917,7 +917,7 @@ "message": "Bridge to" }, "bridgeToChain": { - "message": " to $1" + "message": "Bridge 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." 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 index ebd3a938822e..8bbeab6356d9 100644 --- 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 @@ -2,11 +2,13 @@ exports[`BridgeStatusController constructor rehydrates the tx history state 1`] = ` { - "0xsrcTxHash1": { + "bridgeTxMetaId1": { "account": "0xaccount1", "estimatedProcessingTimeInSeconds": 15, "initialDestAssetBalance": undefined, - "pricingData": undefined, + "pricingData": { + "amountSent": "1.234", + }, "quote": { "bridgeId": "lifi", "bridges": [ @@ -102,17 +104,20 @@ exports[`BridgeStatusController constructor rehydrates the tx history state 1`] "status": "PENDING", }, "targetContractAddress": "0x23981fC34e69eeDFE2BD9a0a9fCb0719Fe09DbFC", + "txMetaId": "bridgeTxMetaId1", }, } `; exports[`BridgeStatusController startPollingForBridgeTxStatus sets the inital tx history state 1`] = ` { - "0xsrcTxHash1": { + "bridgeTxMetaId1": { "account": "0xaccount1", "estimatedProcessingTimeInSeconds": 15, "initialDestAssetBalance": undefined, - "pricingData": undefined, + "pricingData": { + "amountSent": "1.234", + }, "quote": { "bridgeId": "lifi", "bridges": [ @@ -208,6 +213,7 @@ exports[`BridgeStatusController startPollingForBridgeTxStatus sets the inital tx "status": "PENDING", }, "targetContractAddress": "0x23981fC34e69eeDFE2BD9a0a9fCb0719Fe09DbFC", + "txMetaId": "bridgeTxMetaId1", }, } `; diff --git a/app/scripts/controllers/bridge-status/bridge-status-controller.test.ts b/app/scripts/controllers/bridge-status/bridge-status-controller.test.ts index 7d82d32284f4..0a8e331b2767 100644 --- a/app/scripts/controllers/bridge-status/bridge-status-controller.test.ts +++ b/app/scripts/controllers/bridge-status/bridge-status-controller.test.ts @@ -1,271 +1,19 @@ 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'; +import { + MockStatusResponse, + MockTxHistory, + getMockStartPollingForBridgeTxStatusArgs, +} from './mocks'; 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, @@ -513,6 +261,7 @@ describe('BridgeStatusController', () => { // Start polling for 0xaccount2 bridgeStatusController.startPollingForBridgeTxStatus( getMockStartPollingForBridgeTxStatusArgs({ + txMetaId: 'bridgeTxMetaId2', srcTxHash: '0xsrcTxHash2', account: '0xaccount2', }), @@ -523,10 +272,10 @@ describe('BridgeStatusController', () => { // Check that both accounts have a tx history entry expect( bridgeStatusController.state.bridgeStatusState.txHistory, - ).toHaveProperty('0xsrcTxHash1'); + ).toHaveProperty('bridgeTxMetaId1'); expect( bridgeStatusController.state.bridgeStatusState.txHistory, - ).toHaveProperty('0xsrcTxHash2'); + ).toHaveProperty('bridgeTxMetaId2'); // Wipe the status for 1 account only bridgeStatusController.wipeBridgeStatus({ @@ -586,6 +335,7 @@ describe('BridgeStatusController', () => { getMockStartPollingForBridgeTxStatusArgs({ account: '0xaccount1', srcTxHash: '0xsrcTxHash1', + txMetaId: 'bridgeTxMetaId1', srcChainId: 42161, destChainId: 1, }), @@ -598,6 +348,7 @@ describe('BridgeStatusController', () => { getMockStartPollingForBridgeTxStatusArgs({ account: '0xaccount1', srcTxHash: '0xsrcTxHash2', + txMetaId: 'bridgeTxMetaId2', srcChainId: 10, destChainId: 123, }), @@ -607,20 +358,20 @@ describe('BridgeStatusController', () => { // Check we have a tx history entry for each chainId expect( - bridgeStatusController.state.bridgeStatusState.txHistory['0xsrcTxHash1'] + bridgeStatusController.state.bridgeStatusState.txHistory.bridgeTxMetaId1 .quote.srcChainId, ).toEqual(42161); expect( - bridgeStatusController.state.bridgeStatusState.txHistory['0xsrcTxHash1'] + bridgeStatusController.state.bridgeStatusState.txHistory.bridgeTxMetaId1 .quote.destChainId, ).toEqual(1); expect( - bridgeStatusController.state.bridgeStatusState.txHistory['0xsrcTxHash2'] + bridgeStatusController.state.bridgeStatusState.txHistory.bridgeTxMetaId2 .quote.srcChainId, ).toEqual(10); expect( - bridgeStatusController.state.bridgeStatusState.txHistory['0xsrcTxHash2'] + bridgeStatusController.state.bridgeStatusState.txHistory.bridgeTxMetaId2 .quote.destChainId, ).toEqual(123); @@ -681,6 +432,7 @@ describe('BridgeStatusController', () => { getMockStartPollingForBridgeTxStatusArgs({ account: '0xaccount1', srcTxHash: '0xsrcTxHash1', + txMetaId: 'bridgeTxMetaId1', srcChainId: 42161, destChainId: 1, }), @@ -693,6 +445,7 @@ describe('BridgeStatusController', () => { getMockStartPollingForBridgeTxStatusArgs({ account: '0xaccount1', srcTxHash: '0xsrcTxHash2', + txMetaId: 'bridgeTxMetaId2', srcChainId: 10, destChainId: 123, }), @@ -702,20 +455,20 @@ describe('BridgeStatusController', () => { // Check we have a tx history entry for each chainId expect( - bridgeStatusController.state.bridgeStatusState.txHistory['0xsrcTxHash1'] + bridgeStatusController.state.bridgeStatusState.txHistory.bridgeTxMetaId1 .quote.srcChainId, ).toEqual(42161); expect( - bridgeStatusController.state.bridgeStatusState.txHistory['0xsrcTxHash1'] + bridgeStatusController.state.bridgeStatusState.txHistory.bridgeTxMetaId1 .quote.destChainId, ).toEqual(1); expect( - bridgeStatusController.state.bridgeStatusState.txHistory['0xsrcTxHash2'] + bridgeStatusController.state.bridgeStatusState.txHistory.bridgeTxMetaId2 .quote.srcChainId, ).toEqual(10); expect( - bridgeStatusController.state.bridgeStatusState.txHistory['0xsrcTxHash2'] + bridgeStatusController.state.bridgeStatusState.txHistory.bridgeTxMetaId2 .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 index 86e87d9d0622..1763e47feb77 100644 --- a/app/scripts/controllers/bridge-status/bridge-status-controller.ts +++ b/app/scripts/controllers/bridge-status/bridge-status-controller.ts @@ -1,13 +1,11 @@ import { StateMetadata } from '@metamask/base-controller'; -import type { NetworkClientId } from '@metamask/network-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, + StartPollingForBridgeTxStatusArgsSerialized, } from '../../../../shared/types/bridge-status'; import { decimalToPrefixedHex } from '../../../../shared/modules/conversion.utils'; import { @@ -16,7 +14,7 @@ import { REFRESH_INTERVAL_MS, } from './constants'; import { BridgeStatusControllerMessenger } from './types'; -import { fetchBridgeTxStatus } from './utils'; +import { fetchBridgeTxStatus, getStatusRequestWithSrcTxHash } from './utils'; const metadata: StateMetadata<{ bridgeStatusState: BridgeStatusControllerState; @@ -30,21 +28,18 @@ const metadata: StateMetadata<{ }; /** The input to start polling for the {@link BridgeStatusController} */ -type BridgeStatusPollingInput = { - networkClientId: NetworkClientId; - statusRequest: StatusRequest; -}; +type BridgeStatusPollingInput = FetchBridgeTxStatusArgs; -type SrcTxHash = string; +type SrcTxMetaId = string; export type FetchBridgeTxStatusArgs = { - statusRequest: StatusRequest; + bridgeTxMetaId: string; }; export default class BridgeStatusController extends StaticIntervalPollingController()< typeof BRIDGE_STATUS_CONTROLLER_NAME, { bridgeStatusState: BridgeStatusControllerState }, BridgeStatusControllerMessenger > { - #pollingTokensBySrcTxHash: Record = {}; + #pollingTokensByTxMetaId: Record = {}; constructor({ messenger, @@ -130,94 +125,82 @@ export default class BridgeStatusController extends StaticIntervalPollingControl const historyItems = Object.values(bridgeStatusState.txHistory); const incompleteHistoryItems = historyItems .filter( - (historyItem) => historyItem.status.status !== StatusTypes.COMPLETE, + (historyItem) => + historyItem.status.status === StatusTypes.PENDING || + historyItem.status.status === StatusTypes.UNKNOWN, ) .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]; + const srcTxMetaId = historyItem.txMetaId; + const pollingToken = this.#pollingTokensByTxMetaId[srcTxMetaId]; 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, - ); + const bridgeTxMetaId = historyItem.txMetaId; // We manually call startPolling() here rather than go through startPollingForBridgeTxStatus() // because we don't want to overwrite the existing historyItem in state - this.#pollingTokensBySrcTxHash[statusRequest.srcTxHash] = - this.startPolling({ networkClientId, statusRequest }); + this.#pollingTokensByTxMetaId[bridgeTxMetaId] = this.startPolling({ + bridgeTxMetaId, + }); }); }; startPollingForBridgeTxStatus = ( - startPollingForBridgeTxStatusArgs: StartPollingForBridgeTxStatusArgs, + startPollingForBridgeTxStatusArgs: StartPollingForBridgeTxStatusArgsSerialized, ) => { const { + bridgeTxMeta, 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 + const txHistoryItem = { + txMetaId: bridgeTxMeta.id, + quote: quoteResponse.quote, + startTime, + estimatedProcessingTimeInSeconds: + quoteResponse.estimatedProcessingTimeInSeconds, + slippagePercentage, + pricingData: { + amountSent: quoteResponse.sentAmount.amount, + }, + 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, + }, + }, + }; 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, - }, - }, - }, + // Use the txMeta.id as the key so we can reference the txMeta in TransactionController + [bridgeTxMeta.id]: txHistoryItem, }, }; }); - const networkClientId = this.messagingSystem.call( - 'NetworkController:findNetworkClientIdByChainId', - hexSourceChainId, - ); - this.#pollingTokensBySrcTxHash[statusRequest.srcTxHash] = this.startPolling( - { networkClientId, statusRequest }, - ); + this.#pollingTokensByTxMetaId[bridgeTxMeta.id] = this.startPolling({ + bridgeTxMetaId: bridgeTxMeta.id, + }); }; // This will be called after you call this.startPolling() @@ -231,14 +214,26 @@ export default class BridgeStatusController extends StaticIntervalPollingControl } #fetchBridgeTxStatus = async ({ - networkClientId: _networkClientId, - statusRequest, - }: BridgeStatusPollingInput) => { + bridgeTxMetaId, + }: 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. + // Also srcTxHash may not be available immediately for STX, so we don't want to fetch in those cases + const historyItem = bridgeStatusState.txHistory[bridgeTxMetaId]; + const srcTxHash = this.#getSrcTxHash(bridgeTxMetaId); + if (!srcTxHash) { + return; + } + + this.#updateSrcTxHash(bridgeTxMetaId, srcTxHash); + + const statusRequest = getStatusRequestWithSrcTxHash( + historyItem.quote, + srcTxHash, + ); const status = await fetchBridgeTxStatus(statusRequest); // No need to purge these on network change or account change, TransactionController does not purge either. @@ -247,13 +242,13 @@ export default class BridgeStatusController extends StaticIntervalPollingControl // 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.txHistory[bridgeTxMetaId]; _state.bridgeStatusState = { ...bridgeStatusState, txHistory: { ...bridgeStatusState.txHistory, - [statusRequest.srcTxHash]: { + [bridgeTxMetaId]: { ...bridgeHistoryItem, status, }, @@ -261,8 +256,7 @@ export default class BridgeStatusController extends StaticIntervalPollingControl }; }); - const pollingToken = - this.#pollingTokensBySrcTxHash[statusRequest.srcTxHash]; + const pollingToken = this.#pollingTokensByTxMetaId[bridgeTxMetaId]; if (status.status === StatusTypes.COMPLETE && pollingToken) { this.stopPollingByPollingToken(pollingToken); } @@ -271,14 +265,61 @@ export default class BridgeStatusController extends StaticIntervalPollingControl } }; + #getSrcTxHash = (bridgeTxMetaId: string): string | undefined => { + const { bridgeStatusState } = this.state; + // Prefer the srcTxHash from bridgeStatusState so we don't have to l ook up in TransactionController + // But it is possible to have bridgeHistoryItem in state without the srcTxHash yet when it is an STX + const srcTxHash = + bridgeStatusState.txHistory[bridgeTxMetaId].status.srcChain.txHash; + + if (srcTxHash) { + return srcTxHash; + } + + // Look up in TransactionController if txMeta has been updated with the srcTxHash + const txControllerState = this.messagingSystem.call( + 'TransactionController:getState', + ); + const txMeta = txControllerState.transactions.find( + (tx) => tx.id === bridgeTxMetaId, + ); + return txMeta?.hash; + }; + + #updateSrcTxHash = (bridgeTxMetaId: string, srcTxHash: string) => { + const { bridgeStatusState } = this.state; + if (bridgeStatusState.txHistory[bridgeTxMetaId].status.srcChain.txHash) { + return; + } + + this.update((_state) => { + _state.bridgeStatusState = { + ...bridgeStatusState, + txHistory: { + ...bridgeStatusState.txHistory, + [bridgeTxMetaId]: { + ...bridgeStatusState.txHistory[bridgeTxMetaId], + status: { + ...bridgeStatusState.txHistory[bridgeTxMetaId].status, + srcChain: { + ...bridgeStatusState.txHistory[bridgeTxMetaId].status.srcChain, + txHash: srcTxHash, + }, + }, + }, + }, + }; + }); + }; + // 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( + const sourceTxMetaIdsToDelete = Object.keys( this.state.bridgeStatusState.txHistory, - ).filter((sourceTxHash) => { + ).filter((txMetaId) => { const bridgeHistoryItem = - this.state.bridgeStatusState.txHistory[sourceTxHash]; + this.state.bridgeStatusState.txHistory[txMetaId]; const hexSourceChainId = decimalToPrefixedHex( bridgeHistoryItem.quote.srcChainId, @@ -294,20 +335,20 @@ export default class BridgeStatusController extends StaticIntervalPollingControl ); }); - sourceTxHashesToDelete.forEach((sourceTxHash) => { - const pollingToken = this.#pollingTokensBySrcTxHash[sourceTxHash]; + sourceTxMetaIdsToDelete.forEach((sourceTxMetaId) => { + const pollingToken = this.#pollingTokensByTxMetaId[sourceTxMetaId]; if (pollingToken) { this.stopPollingByPollingToken( - this.#pollingTokensBySrcTxHash[sourceTxHash], + this.#pollingTokensByTxMetaId[sourceTxMetaId], ); } }); this.update((_state) => { - _state.bridgeStatusState.txHistory = sourceTxHashesToDelete.reduce( - (acc, sourceTxHash) => { - delete acc[sourceTxHash]; + _state.bridgeStatusState.txHistory = sourceTxMetaIdsToDelete.reduce( + (acc, sourceTxMetaId) => { + delete acc[sourceTxMetaId]; return acc; }, _state.bridgeStatusState.txHistory, diff --git a/app/scripts/controllers/bridge-status/mocks.ts b/app/scripts/controllers/bridge-status/mocks.ts new file mode 100644 index 000000000000..1507af5830ec --- /dev/null +++ b/app/scripts/controllers/bridge-status/mocks.ts @@ -0,0 +1,327 @@ +import { TransactionMeta } from '@metamask/transaction-controller'; +import { + BridgeId, + StatusResponse, + StatusTypes, + ActionTypes, + StartPollingForBridgeTxStatusArgsSerialized, +} from '../../../../shared/types/bridge-status'; + +export 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', + }, + }, + }), +}; + +let mockFetchBridgeTxStatusCount = 0; +export const mockFetchBridgeTxStatus: () => Promise = () => { + return new Promise((resolve) => { + console.log('HELLO mockFetchBridgeTxStatus', mockFetchBridgeTxStatusCount); + setTimeout(() => { + if (mockFetchBridgeTxStatusCount === 0) { + resolve( + MockStatusResponse.getPending({ + srcTxHash: + '0xe3e223b9725765a7de557effdb2b507ace3534bcff2c1fe3a857e0791e56a518', + srcChainId: 1, + destChainId: 42161, + }), + ); + } else { + resolve( + MockStatusResponse.getComplete({ + srcTxHash: + '0xe3e223b9725765a7de557effdb2b507ace3534bcff2c1fe3a857e0791e56a518', + destTxHash: + '0x010e1bffe8288956012e6b6132d7eb3eaf9d0bbf066bd13aae13b973c678508f', + srcChainId: 1, + destChainId: 42161, + }), + ); + } + mockFetchBridgeTxStatusCount += 1; + }, 2000); + }); +}; + +export 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', + }, + ], +}); + +export const getMockStartPollingForBridgeTxStatusArgs = ({ + txMetaId = 'bridgeTxMetaId1', + srcTxHash = '0xsrcTxHash1', + account = '0xaccount1', + srcChainId = 42161, + destChainId = 10, +} = {}): StartPollingForBridgeTxStatusArgsSerialized => ({ + bridgeTxMeta: { + id: txMetaId, + } as TransactionMeta, + 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, + sentAmount: { amount: '1.234', fiat: null }, + }, + startTime: 1729964825189, + slippagePercentage: 0, + initialDestAssetBalance: undefined, + targetContractAddress: '0x23981fC34e69eeDFE2BD9a0a9fCb0719Fe09DbFC', +}); + +export const MockTxHistory = { + getInitNoSrcTxHash: ({ + txMetaId = 'bridgeTxMetaId1', + account = '0xaccount1', + srcChainId = 42161, + destChainId = 10, + } = {}) => ({ + [txMetaId]: { + txMetaId, + quote: getMockQuote({ srcChainId, destChainId }), + startTime: 1729964825189, + estimatedProcessingTimeInSeconds: 15, + slippagePercentage: 0, + account, + targetContractAddress: '0x23981fC34e69eeDFE2BD9a0a9fCb0719Fe09DbFC', + initialDestAssetBalance: undefined, + pricingData: { amountSent: '1.234' }, + }, + }), + getInit: ({ + txMetaId = 'bridgeTxMetaId1', + account = '0xaccount1', + srcChainId = 42161, + destChainId = 10, + } = {}) => ({ + [txMetaId]: { + txMetaId, + quote: getMockQuote({ srcChainId, destChainId }), + startTime: 1729964825189, + estimatedProcessingTimeInSeconds: 15, + slippagePercentage: 0, + account, + targetContractAddress: '0x23981fC34e69eeDFE2BD9a0a9fCb0719Fe09DbFC', + initialDestAssetBalance: undefined, + pricingData: { amountSent: '1.234' }, + }, + }), + getPending: ({ + txMetaId = 'bridgeTxMetaId1', + srcTxHash = '0xsrcTxHash1', + account = '0xaccount1', + srcChainId = 42161, + destChainId = 10, + } = {}) => ({ + [txMetaId]: { + txMetaId, + quote: getMockQuote({ srcChainId, destChainId }), + startTime: 1729964825189, + estimatedProcessingTimeInSeconds: 15, + slippagePercentage: 0, + account, + status: MockStatusResponse.getPending({ + srcTxHash, + srcChainId, + }), + targetContractAddress: '0x23981fC34e69eeDFE2BD9a0a9fCb0719Fe09DbFC', + initialDestAssetBalance: undefined, + pricingData: { amountSent: '1.234' }, + }, + }), + getComplete: ({ + txMetaId = 'bridgeTxMetaId1', + srcTxHash = '0xsrcTxHash1', + account = '0xaccount1', + srcChainId = 42161, + destChainId = 10, + } = {}) => ({ + [txMetaId]: { + txMetaId, + quote: getMockQuote({ srcChainId, destChainId }), + startTime: 1729964825189, + estimatedProcessingTimeInSeconds: 15, + slippagePercentage: 0, + account, + status: MockStatusResponse.getComplete({ srcTxHash }), + targetContractAddress: '0x23981fC34e69eeDFE2BD9a0a9fCb0719Fe09DbFC', + initialDestAssetBalance: undefined, + pricingData: { amountSent: '1.234' }, + }, + }), +}; diff --git a/app/scripts/controllers/bridge-status/types.ts b/app/scripts/controllers/bridge-status/types.ts index 040cd1e0c9bd..d06c3c40b3d8 100644 --- a/app/scripts/controllers/bridge-status/types.ts +++ b/app/scripts/controllers/bridge-status/types.ts @@ -9,6 +9,7 @@ import { NetworkControllerGetStateAction, } from '@metamask/network-controller'; import { AccountsControllerGetSelectedAccountAction } from '@metamask/accounts-controller'; +import { TransactionControllerGetStateAction } from '@metamask/transaction-controller'; import { BridgeStatusAction, BridgeStatusControllerState, @@ -37,11 +38,19 @@ type BridgeStatusControllerEvents = ControllerStateChangeEvent< BridgeStatusControllerState >; +/** + * The external actions available to the BridgeStatusController. + */ type AllowedActions = | NetworkControllerFindNetworkClientIdByChainIdAction | NetworkControllerGetStateAction | NetworkControllerGetNetworkClientByIdAction - | AccountsControllerGetSelectedAccountAction; + | AccountsControllerGetSelectedAccountAction + | TransactionControllerGetStateAction; + +/** + * The external events available to the BridgeStatusController. + */ type AllowedEvents = never; /** diff --git a/app/scripts/controllers/bridge-status/utils.ts b/app/scripts/controllers/bridge-status/utils.ts index 323e7e2faeab..33af3c09cb03 100644 --- a/app/scripts/controllers/bridge-status/utils.ts +++ b/app/scripts/controllers/bridge-status/utils.ts @@ -5,15 +5,20 @@ import { import fetchWithCache from '../../../../shared/lib/fetch-with-cache'; import { StatusResponse, - StatusRequest, + StatusRequestWithSrcTxHash, } from '../../../../shared/types/bridge-status'; +// TODO fix this +// eslint-disable-next-line import/no-restricted-paths +import { Quote } from '../../../../ui/pages/bridge/types'; 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) => { +export const fetchBridgeTxStatus = async ( + statusRequest: StatusRequestWithSrcTxHash, +) => { // Assemble params const { quote, ...statusRequestNoQuote } = statusRequest; const statusRequestNoQuoteFormatted = Object.fromEntries( @@ -47,3 +52,18 @@ export const fetchBridgeTxStatus = async (statusRequest: StatusRequest) => { // Return return rawTxStatus; }; + +export const getStatusRequestWithSrcTxHash = ( + quote: Quote, + srcTxHash: string, +): StatusRequestWithSrcTxHash => { + return { + bridgeId: quote.bridgeId, + srcTxHash, + bridge: quote.bridges[0], + srcChainId: quote.srcChainId, + destChainId: quote.destChainId, + quote, + refuel: Boolean(quote.refuel), + }; +}; diff --git a/app/scripts/lib/transaction/smart-transactions-mocks.ts b/app/scripts/lib/transaction/smart-transactions-mocks.ts new file mode 100644 index 000000000000..4d819aa1b7c5 --- /dev/null +++ b/app/scripts/lib/transaction/smart-transactions-mocks.ts @@ -0,0 +1,35 @@ +/** + * Mock for waitForTransactionHash. Simply replace the waitForTransactionHash + * with this mock so that we can debug locally without spending gas on mainnet. + * + * @returns Promise + */ +export const mockWaitForTransactionHash: () => Promise = () => { + return new Promise((resolve) => { + setTimeout(() => { + // Need a real tx hash to pass some downstream validation + resolve( + '0xe3e223b9725765a7de557effdb2b507ace3534bcff2c1fe3a857e0791e56a518', + ); + }, 20_000_000); + }); +}; + +/** + * Mock for signAndSubmitTransactions. Simply replace the signAndSubmitTransactions + * with this mock so that we can debug locally without spending gas on mainnet. + * + * @returns Promise<{ uuid: string; txHash?: string }> + */ +export const mockSignAndSubmitTransactions: () => Promise<{ + uuid: string; + txHash?: string; +}> = () => { + return new Promise((resolve) => { + setTimeout(() => { + resolve({ + uuid: 'uuid123456789', + }); + }, 100); + }); +}; diff --git a/app/scripts/lib/transaction/smart-transactions.ts b/app/scripts/lib/transaction/smart-transactions.ts index c80cd8373d2a..6f9acac1ce74 100644 --- a/app/scripts/lib/transaction/smart-transactions.ts +++ b/app/scripts/lib/transaction/smart-transactions.ts @@ -101,6 +101,8 @@ class SmartTransactionHook { #txParams: TransactionParams; + #shouldShowStatusPage: boolean; + constructor(request: SubmitSmartTransactionRequest) { const { transactionMeta, @@ -123,14 +125,18 @@ class SmartTransactionHook { this.#isDapp = transactionMeta.origin !== ORIGIN_METAMASK; this.#chainId = transactionMeta.chainId; this.#txParams = transactionMeta.txParams; + this.#shouldShowStatusPage = + transactionMeta.type !== TransactionType.bridge; } async submit() { const isUnsupportedTransactionTypeForSmartTransaction = this .#transactionMeta?.type - ? [TransactionType.swapAndSend, TransactionType.swapApproval].includes( - this.#transactionMeta.type, - ) + ? [ + TransactionType.swapAndSend, + TransactionType.swapApproval, + TransactionType.bridgeApproval, + ].includes(this.#transactionMeta.type) : false; // Will cause TransactionController to publish to the RPC provider as normal. @@ -141,10 +147,13 @@ class SmartTransactionHook { ) { return useRegularTransactionSubmit; } - const { id: approvalFlowId } = await this.#controllerMessenger.call( - 'ApprovalController:startFlow', - ); - this.#approvalFlowId = approvalFlowId; + + if (this.#shouldShowStatusPage) { + const { id: approvalFlowId } = await this.#controllerMessenger.call( + 'ApprovalController:startFlow', + ); + this.#approvalFlowId = approvalFlowId; + } let getFeesResponse; try { getFeesResponse = await this.#smartTransactionsController.getFees( @@ -169,12 +178,15 @@ class SmartTransactionHook { } const extensionReturnTxHashAsap = this.#featureFlags?.smartTransactions?.extensionReturnTxHashAsap; - this.#addApprovalRequest({ - uuid, - }); - this.#addListenerToUpdateStatusPage({ - uuid, - }); + + if (this.#shouldShowStatusPage) { + this.#addApprovalRequest({ + uuid, + }); + this.#addListenerToUpdateStatusPage({ + uuid, + }); + } let transactionHash: string | undefined | null; if (extensionReturnTxHashAsap && submitTransactionResponse?.txHash) { transactionHash = submitTransactionResponse.txHash; @@ -197,7 +209,7 @@ class SmartTransactionHook { } #onApproveOrReject() { - if (this.#approvalFlowEnded) { + if (!this.#shouldShowStatusPage || this.#approvalFlowEnded) { return; } this.#approvalFlowEnded = true; diff --git a/app/scripts/metamask-controller.js b/app/scripts/metamask-controller.js index 1eaf354b4caf..95dc69636a40 100644 --- a/app/scripts/metamask-controller.js +++ b/app/scripts/metamask-controller.js @@ -2192,6 +2192,7 @@ export default class MetamaskController extends EventEmitter { 'NetworkController:getNetworkClientById', 'NetworkController:findNetworkClientIdByChainId', 'NetworkController:getState', + 'TransactionController:getState', ], allowedEvents: [], }); diff --git a/shared/types/bridge-status.ts b/shared/types/bridge-status.ts index f4c5c82d26eb..845a3c7cec65 100644 --- a/shared/types/bridge-status.ts +++ b/shared/types/bridge-status.ts @@ -1,5 +1,12 @@ -// eslint-disable-next-line import/no-restricted-paths -import { ChainId, Quote, QuoteResponse } from '../../ui/pages/bridge/types'; +import { TransactionMeta } from '@metamask/transaction-controller'; +// TODO fix this +import { + ChainId, + Quote, + QuoteMetadata, + QuoteResponse, + // eslint-disable-next-line import/no-restricted-paths +} 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 @@ -13,7 +20,7 @@ export enum StatusTypes { export type StatusRequest = { bridgeId: string; // lifi, socket, squid - srcTxHash: string; // lifi, socket, squid + srcTxHash?: string; // lifi, socket, squid, might be undefined for STX bridge: string; // lifi, socket, squid srcChainId: ChainId; // lifi, socket, squid destChainId: ChainId; // lifi, socket, squid @@ -21,6 +28,10 @@ export type StatusRequest = { refuel?: boolean; // lifi }; +export type StatusRequestWithSrcTxHash = StatusRequest & { + srcTxHash: string; +}; + export type Asset = { chainId: ChainId; address: string; @@ -32,7 +43,7 @@ export type Asset = { export type SrcChainStatus = { chainId: ChainId; - txHash: string; + txHash?: string; // might be undefined if this is a smart transaction (STX) amount?: string; token?: Asset; }; @@ -105,6 +116,7 @@ export type RefuelStatusResponse = object & StatusResponse; export type RefuelData = object & Step; export type BridgeHistoryItem = { + txMetaId: string; // Need this to handle STX that might not have a txHash immediately quote: Quote; status: StatusResponse; startTime?: number; @@ -112,13 +124,15 @@ export type BridgeHistoryItem = { slippagePercentage: number; completionTime?: number; pricingData?: { - quotedGasInUsd: number; - quotedReturnInUsd: number; - amountSentInUsd: number; - quotedRefuelSrcAmountInUsd?: number; - quotedRefuelDestAmountInUsd?: number; + amountSent: string; // This is from QuoteMetadata.sentAmount.amount, accounts for the MM fees + + quotedGasInUsd?: string; + quotedReturnInUsd?: string; + amountSentInUsd?: string; + quotedRefuelSrcAmountInUsd?: string; + quotedRefuelDestAmountInUsd?: string; }; - initialDestAssetBalance?: number; + initialDestAssetBalance?: string; targetContractAddress?: string; account: string; }; @@ -129,16 +143,28 @@ export enum BridgeStatusAction { GET_STATE = 'getState', } +// The BigNumber values are serialized to strings +export type QuoteMetadataSerialized = { + sentAmount: { amount: string; fiat: string | null }; +}; + export type StartPollingForBridgeTxStatusArgs = { + bridgeTxMeta: TransactionMeta; statusRequest: StatusRequest; - quoteResponse: QuoteResponse; + quoteResponse: QuoteResponse & QuoteMetadata; startTime?: BridgeHistoryItem['startTime']; slippagePercentage: BridgeHistoryItem['slippagePercentage']; - pricingData?: BridgeHistoryItem['pricingData']; initialDestAssetBalance?: BridgeHistoryItem['initialDestAssetBalance']; targetContractAddress?: BridgeHistoryItem['targetContractAddress']; }; +export type StartPollingForBridgeTxStatusArgsSerialized = Omit< + StartPollingForBridgeTxStatusArgs, + 'quoteResponse' +> & { + quoteResponse: QuoteResponse & QuoteMetadataSerialized; +}; + export type SourceChainTxMetaId = string; export type BridgeStatusControllerState = { 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 060bc8158e92..5ffcb1405deb 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 @@ -69,7 +69,10 @@ 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 { + useBridgeTxHistoryData, + FINAL_NON_CONFIRMED_STATUSES, +} from '../../../hooks/bridge/useBridgeTxHistoryData'; import BridgeActivityItemTxSegments from '../../../pages/bridge/transaction-details/bridge-activity-item-tx-segments'; function TransactionListItemInner({ @@ -93,14 +96,10 @@ function TransactionListItemInner({ // Bridge transactions const isBridgeTx = transactionGroup.initialTransaction.type === TransactionType.bridge; - const { - bridgeTitleSuffix, - bridgeTxHistoryItem, - isBridgeComplete, - showBridgeTxDetails, - } = useBridgeTxHistoryData({ - transactionGroup, - }); + const { bridgeTxHistoryItem, isBridgeComplete, showBridgeTxDetails } = + useBridgeTxHistoryData({ + transactionGroup, + }); const { initialTransaction: { id }, @@ -309,7 +308,7 @@ function TransactionListItemInner({ : toggleShowDetails } className={className} - title={`${title}${bridgeTitleSuffix}`} + title={title} icon={ ///: BEGIN:ONLY_INCLUDE_IF(build-mmi) isCustodian ? ( @@ -355,7 +354,9 @@ function TransactionListItemInner({ ///: END:ONLY_INCLUDE_IF } subtitle={ - isBridgeTx && isBridgeComplete === false ? ( + !FINAL_NON_CONFIRMED_STATUSES.includes(status) && + isBridgeTx && + !isBridgeComplete ? ( { }; }); +jest.mock('../../../hooks/bridge/useBridgeTxHistoryData', () => { + return { + ...jest.requireActual('../../../hooks/bridge/useBridgeTxHistoryData'), + useBridgeTxHistoryData: jest.fn(() => ({ + bridgeTxHistoryItem: undefined, + isBridgeComplete: false, + showBridgeTxDetails: false, + })), + }; +}); + jest.mock('../../../hooks/useGasFeeEstimates', () => ({ useGasFeeEstimates: jest.fn(), })); @@ -110,6 +122,8 @@ const generateUseSelectorRouter = (opts) => (selector) => { return opts.shouldShowFiat ?? false; } else if (selector === getTokens) { return opts.tokens ?? []; + } else if (selector === selectBridgeHistoryForAccount) { + return opts.bridgeHistory ?? {}; } return undefined; }; diff --git a/ui/helpers/utils/token-util.js b/ui/helpers/utils/token-util.js index f64f74f62fa6..10beae68b39e 100644 --- a/ui/helpers/utils/token-util.js +++ b/ui/helpers/utils/token-util.js @@ -217,7 +217,8 @@ export function getTokenFiatAmount( if ( conversionRate <= 0 || !contractExchangeRate || - tokenAmount === undefined + tokenAmount === undefined || + tokenAmount === false ) { return undefined; } diff --git a/ui/hooks/bridge/useBridgeChainInfo.ts b/ui/hooks/bridge/useBridgeChainInfo.ts index f5229391ff44..b2027af8eb71 100644 --- a/ui/hooks/bridge/useBridgeChainInfo.ts +++ b/ui/hooks/bridge/useBridgeChainInfo.ts @@ -1,4 +1,8 @@ import { useSelector } from 'react-redux'; +import { + TransactionMeta, + TransactionType, +} from '@metamask/transaction-controller'; import { Hex } from '@metamask/utils'; import { NetworkConfiguration } from '@metamask/network-controller'; import { Numeric } from '../../../shared/modules/Numeric'; @@ -10,28 +14,53 @@ import { } from '../../../shared/constants/network'; import { CHAINID_DEFAULT_BLOCK_EXPLORER_URL_MAP } from '../../../shared/constants/common'; +const getSourceAndDestChainIds = ({ + bridgeHistoryItem, +}: UseBridgeChainInfoProps) => { + const hexSrcChainId = bridgeHistoryItem + ? (new Numeric( + bridgeHistoryItem.quote.srcChainId, + 10, + ).toPrefixedHexString() as Hex) + : undefined; + const hexDestChainId = bridgeHistoryItem + ? (new Numeric( + bridgeHistoryItem.quote.destChainId, + 10, + ).toPrefixedHexString() as Hex) + : undefined; + + return { + hexSrcChainId, + hexDestChainId, + }; +}; + export type UseBridgeChainInfoProps = { bridgeHistoryItem?: BridgeHistoryItem; + srcTxMeta?: TransactionMeta; }; export default function useBridgeChainInfo({ bridgeHistoryItem, + srcTxMeta, }: UseBridgeChainInfoProps) { const networkConfigurationsByChainId = useSelector( getNetworkConfigurationsByChainId, ); - const decSrcChainId = bridgeHistoryItem?.quote.srcChainId; - const hexSrcChainId = decSrcChainId - ? (new Numeric(decSrcChainId, 10).toPrefixedHexString() as Hex) - : undefined; + if (srcTxMeta?.type !== TransactionType.bridge) { + return { + srcNetwork: undefined, + destNetwork: undefined, + }; + } - const decDestChainId = bridgeHistoryItem?.quote.destChainId; - const hexDestChainId = decDestChainId - ? (new Numeric(decDestChainId, 10).toPrefixedHexString() as Hex) - : undefined; + const { hexSrcChainId, hexDestChainId } = getSourceAndDestChainIds({ + bridgeHistoryItem, + }); - if (!bridgeHistoryItem || !hexSrcChainId || !hexDestChainId) { + if (!hexSrcChainId || !hexDestChainId) { return { srcNetwork: undefined, destNetwork: undefined, diff --git a/ui/hooks/bridge/useBridgeTxHistoryData.ts b/ui/hooks/bridge/useBridgeTxHistoryData.ts index 97cc7f5675a3..6de4f7ef75b5 100644 --- a/ui/hooks/bridge/useBridgeTxHistoryData.ts +++ b/ui/hooks/bridge/useBridgeTxHistoryData.ts @@ -1,13 +1,20 @@ import { useSelector } from 'react-redux'; import { Hex } from '@metamask/utils'; -import { TransactionMeta } from '@metamask/transaction-controller'; +import { + TransactionMeta, + TransactionStatus, +} 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 = { +export const FINAL_NON_CONFIRMED_STATUSES = [ + TransactionStatus.failed, + TransactionStatus.dropped, + TransactionStatus.rejected, +]; + +export type UseBridgeTxHistoryDataProps = { transactionGroup: { hasCancelled: boolean; hasRetried: boolean; @@ -18,26 +25,14 @@ export type UseBridgeDataProps = { }; }; -export default function useBridgeTxHistoryData({ +export function useBridgeTxHistoryData({ transactionGroup, -}: UseBridgeDataProps) { - const t = useI18nContext(); +}: UseBridgeTxHistoryDataProps) { 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]) - : ''; + const txMeta = transactionGroup.initialTransaction; + const srcTxMetaId = txMeta.id; + const bridgeHistoryItem = bridgeHistory[srcTxMetaId]; // By complete, this means BOTH source and dest tx are confirmed const isBridgeComplete = bridgeHistoryItem @@ -47,14 +42,15 @@ export default function useBridgeTxHistoryData({ ) : null; - const showBridgeTxDetails = srcTxHash - ? () => { - history.push(`${CROSS_CHAIN_SWAP_TX_DETAILS_ROUTE}/${srcTxHash}`); - } - : null; + const showBridgeTxDetails = FINAL_NON_CONFIRMED_STATUSES.includes( + txMeta.status, + ) + ? undefined + : () => { + history.push(`${CROSS_CHAIN_SWAP_TX_DETAILS_ROUTE}/${srcTxMetaId}`); + }; return { - bridgeTitleSuffix, bridgeTxHistoryItem: bridgeHistoryItem, isBridgeComplete, showBridgeTxDetails, diff --git a/ui/hooks/useTransactionDisplayData.js b/ui/hooks/useTransactionDisplayData.js index 2adcdd03e760..66b9102d5c29 100644 --- a/ui/hooks/useTransactionDisplayData.js +++ b/ui/hooks/useTransactionDisplayData.js @@ -35,6 +35,7 @@ import { TransactionGroupCategory } from '../../shared/constants/transaction'; import { captureSingleException } from '../store/actions'; import { isEqualCaseInsensitive } from '../../shared/modules/string-utils'; import { getTokenValueParam } from '../../shared/lib/metamask-controller-utils'; +import { selectBridgeHistoryForAccount } from '../ducks/bridge-status/selectors'; import { useI18nContext } from './useI18nContext'; import { useTokenFiatAmount } from './useTokenFiatAmount'; import { useUserPreferencedCurrency } from './useUserPreferencedCurrency'; @@ -43,6 +44,7 @@ import { useTokenDisplayValue } from './useTokenDisplayValue'; import { useTokenData } from './useTokenData'; import { useSwappedTokenValue } from './useSwappedTokenValue'; import { useCurrentAsset } from './useCurrentAsset'; +import useBridgeChainInfo from './bridge/useBridgeChainInfo'; /** * There are seven types of transaction entries that are currently differentiated in the design: @@ -107,6 +109,16 @@ export function useTransactionDisplayData(transactionGroup) { const tokenList = useSelector(getTokenList); const t = useI18nContext(); + // Bridge data + const bridgeHistory = useSelector(selectBridgeHistoryForAccount); + const srcTxMetaId = transactionGroup.initialTransaction.id; + const bridgeHistoryItem = bridgeHistory[srcTxMetaId]; + const { destNetwork } = useBridgeChainInfo({ + bridgeHistoryItem, + srcTxMeta: transactionGroup.initialTransaction, + }); + const destChainName = destNetwork?.name; + const { initialTransaction, primaryTransaction } = transactionGroup; // initialTransaction contains the data we need to derive the primary purpose of this transaction group const { type } = initialTransaction; @@ -367,10 +379,12 @@ export function useTransactionDisplayData(transactionGroup) { title = t('bridgeApproval', [primaryTransaction.sourceTokenSymbol]); subtitle = origin; subtitleContainsOrigin = true; - primarySuffix = primaryTransaction.sourceTokenSymbol; // TODO this will be undefined right now + primarySuffix = primaryTransaction.sourceTokenSymbol; } else if (type === TransactionType.bridge) { - title = t('bridge'); + title = t('bridgeToChain', [destChainName || '']); category = TransactionGroupCategory.bridge; + // TODO add primaryDisplayValue,primarySuffix + // also secondaryDisplayValue, secondarySuffix } else { dispatch( captureSingleException( diff --git a/ui/pages/bridge/hooks/useHandleApprovalTx.ts b/ui/pages/bridge/hooks/useHandleApprovalTx.ts index c0fb7812cce0..23f4b19cf2b8 100644 --- a/ui/pages/bridge/hooks/useHandleApprovalTx.ts +++ b/ui/pages/bridge/hooks/useHandleApprovalTx.ts @@ -42,11 +42,8 @@ export default function useHandleApprovalTx() { await handleTx({ txType: TransactionType.bridgeApproval, txParams, - swapsOptions: { - hasApproveTx: true, - meta: { - type: TransactionType.bridgeApproval, - }, + fieldsToAddToTxMeta: { + sourceTokenSymbol: quoteResponse.quote.srcAsset.symbol, }, }); } @@ -74,12 +71,8 @@ export default function useHandleApprovalTx() { const txMeta = await handleTx({ txType: TransactionType.bridgeApproval, txParams: approval, - swapsOptions: { - hasApproveTx: true, - meta: { - type: TransactionType.bridgeApproval, - sourceTokenSymbol: quoteResponse.quote.srcAsset.symbol, - }, + fieldsToAddToTxMeta: { + sourceTokenSymbol: quoteResponse.quote.srcAsset.symbol, }, }); diff --git a/ui/pages/bridge/hooks/useHandleBridgeTx.ts b/ui/pages/bridge/hooks/useHandleBridgeTx.ts index f8135a1bbc08..6d35cc931b77 100644 --- a/ui/pages/bridge/hooks/useHandleBridgeTx.ts +++ b/ui/pages/bridge/hooks/useHandleBridgeTx.ts @@ -24,20 +24,26 @@ export default function useHandleBridgeTx() { const txMeta = await handleTx({ txType: TransactionType.bridge, txParams: quoteResponse.trade, - swapsOptions: { - hasApproveTx: Boolean(quoteResponse?.approval), - meta: { - // estimatedBaseFee: decEstimatedBaseFee, - // swapMetaData, - type: TransactionType.bridge, - sourceTokenSymbol: quoteResponse.quote.srcAsset.symbol, - destinationTokenSymbol: quoteResponse.quote.destAsset.symbol, - destinationTokenDecimals: quoteResponse.quote.destAsset.decimals, - destinationTokenAddress: quoteResponse.quote.destAsset.address, - approvalTxId, - // this is the decimal (non atomic) amount (not USD value) of source token to swap - swapTokenValue: sentAmountDec, - }, + fieldsToAddToTxMeta: { + // @ts-expect-error TODO get this added to TxMeta type + destinationChainId: new Numeric(quoteResponse.quote.destChainId, 10) + .toPrefixedHexString() + .toLowerCase() as `0x${string}`, + // estimatedBaseFee: decEstimatedBaseFee, + + sourceTokenAmount: quoteResponse.quote.srcTokenAmount, + sourceTokenSymbol: quoteResponse.quote.srcAsset.symbol, + sourceTokenDecimals: quoteResponse.quote.srcAsset.decimals, + sourceTokenAddress: quoteResponse.quote.srcAsset.address, + + destinationTokenAmount: quoteResponse.quote.destTokenAmount, + destinationTokenSymbol: quoteResponse.quote.destAsset.symbol, + destinationTokenDecimals: quoteResponse.quote.destAsset.decimals, + destinationTokenAddress: quoteResponse.quote.destAsset.address, + + approvalTxId, + // this is the decimal (non atomic) amount (not USD value) of source token to swap + swapTokenValue: sentAmountDec, }, }); diff --git a/ui/pages/bridge/hooks/useHandleTx.ts b/ui/pages/bridge/hooks/useHandleTx.ts index ffd378449bc7..70deb084c87c 100644 --- a/ui/pages/bridge/hooks/useHandleTx.ts +++ b/ui/pages/bridge/hooks/useHandleTx.ts @@ -5,7 +5,8 @@ import { import { useDispatch, useSelector } from 'react-redux'; import { forceUpdateMetamaskState, - addTransactionAndWaitForPublish, + addTransaction, + updateTransaction, } from '../../../store/actions'; import { getHexMaxGasLimit, @@ -26,7 +27,7 @@ export default function useHandleTx() { const handleTx = async ({ txType, txParams, - swapsOptions, + fieldsToAddToTxMeta, }: { txType: TransactionType.bridgeApproval | TransactionType.bridge; txParams: { @@ -37,10 +38,7 @@ export default function useHandleTx() { data: string; gasLimit: number | null; }; - swapsOptions: { - hasApproveTx: boolean; - meta: Partial; - }; + fieldsToAddToTxMeta: Omit, 'status'>; // We don't add status, so omit it to fix the type error }) => { const hexChainId = decimalToPrefixedHex(txParams.chainId); @@ -61,12 +59,18 @@ export default function useHandleTx() { maxPriorityFeePerGas, }; - const txMeta = await addTransactionAndWaitForPublish(finalTxParams, { + // Need access to the txMeta.id right away so we can track it in BridgeStatusController, + // so we call addTransaction instead of addTransactionAndWaitForPublish + // if it's an STX, addTransactionAndWaitForPublish blocks until there is a txHash + const txMeta = await addTransaction(finalTxParams, { requireApproval: false, type: txType, - swaps: swapsOptions, }); + // Note that updateTransaction doesn't actually error if you add fields that don't conform the to the txMeta type + // they will be there at runtime, but you just don't get any type safety checks on them + dispatch(updateTransaction({ ...txMeta, ...fieldsToAddToTxMeta }, true)); + await forceUpdateMetamaskState(dispatch); return txMeta; diff --git a/ui/pages/bridge/hooks/useSubmitBridgeTransaction.test.tsx b/ui/pages/bridge/hooks/useSubmitBridgeTransaction.test.tsx index 55c19821777b..a84dc7aae443 100644 --- a/ui/pages/bridge/hooks/useSubmitBridgeTransaction.test.tsx +++ b/ui/pages/bridge/hooks/useSubmitBridgeTransaction.test.tsx @@ -35,7 +35,7 @@ jest.mock('../../../store/actions', () => { const original = jest.requireActual('../../../store/actions'); return { ...original, - addTransactionAndWaitForPublish: jest.fn(), + addTransaction: jest.fn(), addToken: jest.fn().mockImplementation(original.addToken), addNetwork: jest.fn().mockImplementation(original.addNetwork), }; @@ -132,15 +132,15 @@ describe('ui/pages/bridge/hooks/useSubmitBridgeTransaction', () => { it('executes bridge transaction', async () => { // Setup - const mockAddTransactionAndWaitForPublish = jest.fn(() => { + const mockAddTransaction = jest.fn(() => { return { id: 'txMetaId-01', }; }); // For some reason, setBackgroundConnection does not work, gets hung up on the promise, so mock this way instead - (actions.addTransactionAndWaitForPublish as jest.Mock).mockImplementation( - mockAddTransactionAndWaitForPublish, + (actions.addTransaction as jest.Mock).mockImplementation( + mockAddTransaction, ); const store = makeMockStore(); const { result } = renderHook(() => useSubmitBridgeTransaction(), { @@ -153,7 +153,7 @@ describe('ui/pages/bridge/hooks/useSubmitBridgeTransaction', () => { ); // Assert - expect(mockAddTransactionAndWaitForPublish).toHaveBeenLastCalledWith( + expect(mockAddTransaction).toHaveBeenLastCalledWith( { chainId: '0x1', data: '0x3ce33bff0000000000000000000000000000000000000000000000000000000000000080000000000000000000000000a0b86991c6218b36c1d19d4a2e9eb0ce3606eb480000000000000000000000000000000000000000000000000000000000a7d8c000000000000000000000000000000000000000000000000000000000000000c0000000000000000000000000000000000000000000000000000000000000000d6c6966694164617074657256320000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000001e0000000000000000000000000e397c4883ec89ed4fc9d258f00c689708b2799c9000000000000000000000000e397c4883ec89ed4fc9d258f00c689708b2799c9000000000000000000000000000000000000000000000000000000000000a4b1000000000000000000000000a0b86991c6218b36c1d19d4a2e9eb0ce3606eb48000000000000000000000000af88d065e77c8cc2239327c5edb3a432268e58310000000000000000000000000000000000000000000000000000000000a660c6000000000000000000000000000000000000000000000000000000000000014000000000000000000000000000000000000000000000000000000000000177fa000000000000000000000000e6b738da243e8fa2a0ed5915645789add5de515200000000000000000000000000000000000000000000000000000000000000902340ab8fc3119af1d016a0eec5fe6ef47965741f6f7a4734bf784bf3ae3f2452a0b86991c6218b36c1d19d4a2e9eb0ce3606eb4800000000000000000000000000a660c60000a4b10008df3abdeb853d66fefedfffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffd00dfeeddeadbeef8932eb23bad9bddb5cf81426f78279a53c6c3b7100000000000000000000000000000000740cfc1bc02079862368cb4eea1332bd9f2dfa925fc757fd51e40919859b87ca031a2a12d67e4ca4ba67d52b59114b3e18c1e8c839ae015112af82e92251db701b', @@ -167,34 +167,21 @@ describe('ui/pages/bridge/hooks/useSubmitBridgeTransaction', () => { }, { requireApproval: false, - swaps: { - hasApproveTx: true, - meta: { - approvalTxId: 'txMetaId-01', - destinationTokenAddress: - '0xaf88d065e77c8cC2239327C5EDb3A432268e5831', - destinationTokenDecimals: 6, - destinationTokenSymbol: 'USDC', - sourceTokenSymbol: 'USDC', - swapTokenValue: '11', - type: 'bridge', - }, - }, type: 'bridge', }, ); }); it('executes approval transaction if it exists', async () => { // Setup - const mockAddTransactionAndWaitForPublish = jest.fn(() => { + const mockAddTransaction = jest.fn(() => { return { id: 'txMetaId-01', }; }); // For some reason, setBackgroundConnection does not work, gets hung up on the promise, so mock this way instead - (actions.addTransactionAndWaitForPublish as jest.Mock).mockImplementation( - mockAddTransactionAndWaitForPublish, + (actions.addTransaction as jest.Mock).mockImplementation( + mockAddTransaction, ); const store = makeMockStore(); const { result } = renderHook(() => useSubmitBridgeTransaction(), { @@ -207,7 +194,7 @@ describe('ui/pages/bridge/hooks/useSubmitBridgeTransaction', () => { ); // Assert - expect(mockAddTransactionAndWaitForPublish).toHaveBeenNthCalledWith( + expect(mockAddTransaction).toHaveBeenNthCalledWith( 1, { chainId: '0x1', @@ -222,14 +209,10 @@ describe('ui/pages/bridge/hooks/useSubmitBridgeTransaction', () => { }, { requireApproval: false, - swaps: { - hasApproveTx: true, - meta: { sourceTokenSymbol: 'USDC', type: 'bridgeApproval' }, - }, type: 'bridgeApproval', }, ); - expect(mockAddTransactionAndWaitForPublish).toHaveBeenNthCalledWith( + expect(mockAddTransaction).toHaveBeenNthCalledWith( 2, { chainId: '0x1', @@ -244,19 +227,6 @@ describe('ui/pages/bridge/hooks/useSubmitBridgeTransaction', () => { }, { requireApproval: false, - swaps: { - hasApproveTx: true, - meta: { - approvalTxId: 'txMetaId-01', - destinationTokenAddress: - '0xaf88d065e77c8cC2239327C5EDb3A432268e5831', - destinationTokenDecimals: 6, - destinationTokenSymbol: 'USDC', - sourceTokenSymbol: 'USDC', - swapTokenValue: '11', - type: 'bridge', - }, - }, type: 'bridge', }, ); @@ -300,14 +270,14 @@ describe('ui/pages/bridge/hooks/useSubmitBridgeTransaction', () => { wrapper: makeWrapper(store), }); - const mockAddTransactionAndWaitForPublish = jest.fn(() => { + const mockAddTransaction = jest.fn(() => { return { id: 'txMetaId-01', }; }); // For some reason, setBackgroundConnection does not work, gets hung up on the promise, so mock this way instead - (actions.addTransactionAndWaitForPublish as jest.Mock).mockImplementation( - mockAddTransactionAndWaitForPublish, + (actions.addTransaction as jest.Mock).mockImplementation( + mockAddTransaction, ); (actions.addToken as jest.Mock).mockImplementation( () => async () => ({}), @@ -366,14 +336,14 @@ describe('ui/pages/bridge/hooks/useSubmitBridgeTransaction', () => { wrapper: makeWrapper(store), }); - const mockAddTransactionAndWaitForPublish = jest.fn(() => { + const mockAddTransaction = jest.fn(() => { return { id: 'txMetaId-01', }; }); // For some reason, setBackgroundConnection does not work, gets hung up on the promise, so mock this way instead - (actions.addTransactionAndWaitForPublish as jest.Mock).mockImplementation( - mockAddTransactionAndWaitForPublish, + (actions.addTransaction as jest.Mock).mockImplementation( + mockAddTransaction, ); (actions.addToken as jest.Mock).mockImplementation( () => async () => ({}), @@ -397,14 +367,14 @@ describe('ui/pages/bridge/hooks/useSubmitBridgeTransaction', () => { // Setup const store = makeMockStore(); - const mockAddTransactionAndWaitForPublish = jest.fn(() => { + const mockAddTransaction = jest.fn(() => { return { id: 'txMetaId-01', }; }); // For some reason, setBackgroundConnection does not work, gets hung up on the promise, so mock this way instead - (actions.addTransactionAndWaitForPublish as jest.Mock).mockImplementation( - mockAddTransactionAndWaitForPublish, + (actions.addTransaction as jest.Mock).mockImplementation( + mockAddTransaction, ); const mockedGetNetworkConfigurationsByChainId = // @ts-expect-error this is a jest mock diff --git a/ui/pages/bridge/hooks/useSubmitBridgeTransaction.ts b/ui/pages/bridge/hooks/useSubmitBridgeTransaction.ts index fcdf8569bdd0..9985757d7fa3 100644 --- a/ui/pages/bridge/hooks/useSubmitBridgeTransaction.ts +++ b/ui/pages/bridge/hooks/useSubmitBridgeTransaction.ts @@ -2,7 +2,7 @@ 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 { QuoteMetadata, QuoteResponse } from '../types'; import { DEFAULT_ROUTE } from '../../../helpers/constants/routes'; import { setDefaultHomeActiveTabName } from '../../../store/actions'; import { startPollingForBridgeTxStatus } from '../../../ducks/bridge-status/actions'; @@ -17,41 +17,48 @@ export default function useSubmitBridgeTransaction() { const { handleApprovalTx } = useHandleApprovalTx(); const { handleBridgeTx } = useHandleBridgeTx(); - const submitBridgeTransaction = async (quoteResponse: QuoteResponse) => { + const submitBridgeTransaction = async ( + quoteResponse: QuoteResponse & QuoteMetadata, + ) => { // Execute transaction(s) let approvalTxMeta: TransactionMeta | undefined; if (quoteResponse?.approval) { + // This will never be an STX approvalTxMeta = await handleApprovalTx({ approval: quoteResponse.approval, quoteResponse, }); } + // Route user to activity tab on Home page + // Do it ahead of time because otherwise STX waits for a txHash on TransactionType.bridge and that can take a while + await dispatch(setDefaultHomeActiveTabName('activity')); + history.push(DEFAULT_ROUTE); + const bridgeTxMeta = await handleBridgeTx({ quoteResponse, 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, - }), - ); - } + const statusRequest = { + bridgeId: quoteResponse.quote.bridgeId, + srcTxHash: bridgeTxMeta.hash, // This might be undefined for STX + bridge: quoteResponse.quote.bridges[0], + srcChainId: quoteResponse.quote.srcChainId, + destChainId: quoteResponse.quote.destChainId, + quote: quoteResponse.quote, + refuel: Boolean(quoteResponse.quote.refuel), + }; + dispatch( + startPollingForBridgeTxStatus({ + bridgeTxMeta, + 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()) { @@ -60,10 +67,6 @@ export default function useSubmitBridgeTransaction() { if (quoteResponse.quote.destAsset.address !== zeroAddress()) { await addDestToken(quoteResponse); } - - // Route user to activity tab on Home page - await dispatch(setDefaultHomeActiveTabName('activity')); - history.push(DEFAULT_ROUTE); }; return { diff --git a/ui/pages/bridge/prepare/bridge-cta-button.tsx b/ui/pages/bridge/prepare/bridge-cta-button.tsx index 7355e6579dfa..b148b71c67f0 100644 --- a/ui/pages/bridge/prepare/bridge-cta-button.tsx +++ b/ui/pages/bridge/prepare/bridge-cta-button.tsx @@ -1,5 +1,5 @@ import React, { useMemo } from 'react'; -import { useDispatch, useSelector } from 'react-redux'; +import { useSelector } from 'react-redux'; import { Button } from '../../../components/component-library'; import { getFromAmount, @@ -13,7 +13,6 @@ import { useI18nContext } from '../../../hooks/useI18nContext'; import useSubmitBridgeTransaction from '../hooks/useSubmitBridgeTransaction'; export const BridgeCTAButton = () => { - const dispatch = useDispatch(); const t = useI18nContext(); const fromToken = useSelector(getFromToken); @@ -55,7 +54,7 @@ export const BridgeCTAButton = () => { data-testid="bridge-cta-button" onClick={() => { if (isTxSubmittable) { - dispatch(submitBridgeTransaction(activeQuote)); + submitBridgeTransaction(activeQuote); } }} disabled={!isTxSubmittable} diff --git a/ui/pages/bridge/transaction-details/bridge-activity-item-tx-segments.tsx b/ui/pages/bridge/transaction-details/bridge-activity-item-tx-segments.tsx index f19025e9b83b..a757eddffb79 100644 --- a/ui/pages/bridge/transaction-details/bridge-activity-item-tx-segments.tsx +++ b/ui/pages/bridge/transaction-details/bridge-activity-item-tx-segments.tsx @@ -14,7 +14,7 @@ import { FlexDirection, TextColor, } from '../../../helpers/constants/design-system'; -import { UseBridgeDataProps } from '../../../hooks/bridge/useBridgeTxHistoryData'; +import { UseBridgeTxHistoryDataProps } from '../../../hooks/bridge/useBridgeTxHistoryData'; import Segment from './segment'; const getTxIndex = (srcTxStatus: StatusTypes) => { @@ -35,10 +35,13 @@ const getSrcTxStatus = (initialTransaction: TransactionMeta) => { : StatusTypes.PENDING; }; -const getDestTxStatus = ( - bridgeTxHistoryItem: BridgeHistoryItem, - srcTxStatus: StatusTypes, -) => { +const getDestTxStatus = ({ + bridgeTxHistoryItem, + srcTxStatus, +}: { + bridgeTxHistoryItem?: BridgeHistoryItem; + srcTxStatus: StatusTypes; +}) => { if (srcTxStatus !== StatusTypes.COMPLETE) { return null; } @@ -60,12 +63,12 @@ export default function BridgeActivityItemTxSegments({ bridgeTxHistoryItem, transactionGroup, }: { - bridgeTxHistoryItem: BridgeHistoryItem; - transactionGroup: UseBridgeDataProps['transactionGroup']; + bridgeTxHistoryItem?: BridgeHistoryItem; + transactionGroup: UseBridgeTxHistoryDataProps['transactionGroup']; }) { const { initialTransaction } = transactionGroup; const srcTxStatus = getSrcTxStatus(initialTransaction); - const destTxStatus = getDestTxStatus(bridgeTxHistoryItem, srcTxStatus); + const destTxStatus = getDestTxStatus({ bridgeTxHistoryItem, srcTxStatus }); const txIndex = getTxIndex(srcTxStatus); return ( diff --git a/ui/pages/bridge/transaction-details/bridge-explorer-links.tsx b/ui/pages/bridge/transaction-details/bridge-explorer-links.tsx index 570680f416d2..6c7c06338567 100644 --- a/ui/pages/bridge/transaction-details/bridge-explorer-links.tsx +++ b/ui/pages/bridge/transaction-details/bridge-explorer-links.tsx @@ -73,9 +73,11 @@ export default function BridgeExplorerLinks({ const srcButtonText = t('bridgeExplorerLinkViewOn', [ getBlockExplorerName(srcChainId, srcBlockExplorerUrl), ]); - const destButtonText = t('bridgeExplorerLinkViewOn', [ - getBlockExplorerName(destChainId, destBlockExplorerUrl), - ]); + const destButtonText = destBlockExplorerUrl + ? t('bridgeExplorerLinkViewOn', [ + getBlockExplorerName(destChainId, destBlockExplorerUrl), + ]) + : undefined; return ( diff --git a/ui/pages/bridge/transaction-details/bridge-step-description.tsx b/ui/pages/bridge/transaction-details/bridge-step-description.tsx index ef69ea4ed116..03abb77a32e2 100644 --- a/ui/pages/bridge/transaction-details/bridge-step-description.tsx +++ b/ui/pages/bridge/transaction-details/bridge-step-description.tsx @@ -117,11 +117,19 @@ const getSwapActionText = ( ]); }; -export const getStepStatus = ( - bridgeHistoryItem: BridgeHistoryItem, - step: Step, - srcChainTxMeta?: TransactionMeta, -) => { +export const getStepStatus = ({ + bridgeHistoryItem, + step, + srcChainTxMeta, +}: { + bridgeHistoryItem?: BridgeHistoryItem; + step: Step; + srcChainTxMeta?: TransactionMeta; +}) => { + if (!bridgeHistoryItem) { + return StatusTypes.UNKNOWN; + } + if (step.action === ActionTypes.SWAP) { return getSwapActionStatus(bridgeHistoryItem, step, srcChainTxMeta); } else if (step.action === ActionTypes.BRIDGE) { diff --git a/ui/pages/bridge/transaction-details/bridge-step-list.tsx b/ui/pages/bridge/transaction-details/bridge-step-list.tsx index b715c29e1f0e..5c2dcc4453f4 100644 --- a/ui/pages/bridge/transaction-details/bridge-step-list.tsx +++ b/ui/pages/bridge/transaction-details/bridge-step-list.tsx @@ -6,6 +6,7 @@ import { Box } from '../../../components/component-library'; import { BridgeHistoryItem, StatusTypes, + Step, } from '../../../../shared/types/bridge-status'; import { formatDate } from '../../../helpers/utils/util'; import BridgeStepDescription, { @@ -29,7 +30,7 @@ const getTime = ( }; type BridgeStepsProps = { - bridgeHistoryItem: BridgeHistoryItem; + bridgeHistoryItem?: BridgeHistoryItem; srcChainTxMeta?: TransactionMeta; networkConfigurationsByChainId: Record; }; @@ -39,9 +40,9 @@ export default function BridgeStepList({ srcChainTxMeta, networkConfigurationsByChainId, }: BridgeStepsProps) { - const { steps } = bridgeHistoryItem.quote; + const steps = bridgeHistoryItem?.quote.steps || []; const stepStatuses = steps.map((step) => - getStepStatus(bridgeHistoryItem, step, srcChainTxMeta), + getStepStatus({ bridgeHistoryItem, step: step as Step, srcChainTxMeta }), ); return ( @@ -71,8 +72,8 @@ export default function BridgeStepList({ getTime( i, i === steps.length - 1, - bridgeHistoryItem.startTime, - bridgeHistoryItem.estimatedProcessingTimeInSeconds, + bridgeHistoryItem?.startTime || srcChainTxMeta?.time, + bridgeHistoryItem?.estimatedProcessingTimeInSeconds || 0, ), 'hh:mm a', ); diff --git a/ui/pages/bridge/transaction-details/step-progress-bar-item.tsx b/ui/pages/bridge/transaction-details/step-progress-bar-item.tsx index 504b2400b7be..95e0b52fcf78 100644 --- a/ui/pages/bridge/transaction-details/step-progress-bar-item.tsx +++ b/ui/pages/bridge/transaction-details/step-progress-bar-item.tsx @@ -49,7 +49,7 @@ export default function StepProgressBarItem({ return ( <> {/* Indicator dots */} - {stepStatus === null && ( + {(stepStatus === null || stepStatus === StatusTypes.UNKNOWN) && ( )} {stepStatus === StatusTypes.PENDING && ( diff --git a/ui/pages/bridge/transaction-details/transaction-details.tsx b/ui/pages/bridge/transaction-details/transaction-details.tsx index 780186a671e0..20ee8fb48754 100644 --- a/ui/pages/bridge/transaction-details/transaction-details.tsx +++ b/ui/pages/bridge/transaction-details/transaction-details.tsx @@ -23,7 +23,10 @@ import UserPreferencedCurrencyDisplay from '../../../components/app/user-prefere 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 { + BridgeHistoryItem, + StatusTypes, +} from '../../../../shared/types/bridge-status'; import { AlignItems, Display, @@ -33,7 +36,6 @@ import { } 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 { selectedAddressTxListSelector } from '../../../selectors'; @@ -60,6 +62,23 @@ const getBlockExplorerUrl = ( return `${rootUrl}/tx/${txHash}`; }; +/** + * @param options0 + * @param options0.bridgeHistoryItem + * @returns A string representing the bridge amount in decimal form + */ +const getBridgeAmount = ({ + bridgeHistoryItem, +}: { + bridgeHistoryItem?: BridgeHistoryItem; +}) => { + if (bridgeHistoryItem) { + return bridgeHistoryItem.pricingData?.amountSent; + } + + return undefined; +}; + const StatusToColorMap: Record = { [StatusTypes.PENDING]: TextColor.warningDefault, [StatusTypes.COMPLETE]: TextColor.successDefault, @@ -71,29 +90,38 @@ const CrossChainSwapTxDetails = () => { const t = useI18nContext(); const rootState = useSelector((state) => state); const history = useHistory(); - const { srcTxHash } = useParams<{ srcTxHash: string }>(); + const { srcTxMetaId } = useParams<{ srcTxMetaId: string }>(); const bridgeHistory = useSelector(selectBridgeHistoryForAccount); const selectedAddressTxList = useSelector( selectedAddressTxListSelector, ) as TransactionMeta[]; - const bridgeHistoryItem = srcTxHash ? bridgeHistory[srcTxHash] : undefined; + const networkConfigurationsByChainId = useSelector( getNetworkConfigurationsByChainId, ); + + const srcChainTxMeta = selectedAddressTxList.find( + (tx) => tx.id === srcTxMetaId, + ); + // Even if user is still on /tx-details/txMetaId, we want to be able to show the bridge history item + const bridgeHistoryItem = srcTxMetaId + ? bridgeHistory[srcTxMetaId] + : undefined; + const { srcNetwork, destNetwork } = useBridgeChainInfo({ bridgeHistoryItem, + srcTxMeta: srcChainTxMeta, }); + const srcTxHash = srcChainTxMeta?.hash; 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 status = bridgeHistoryItem + ? bridgeHistoryItem?.status.status + : StatusTypes.PENDING; const destChainIconUrl = destNetwork ? CHAIN_ID_TO_NETWORK_IMAGE_URL_MAP[ @@ -112,12 +140,7 @@ const CrossChainSwapTxDetails = () => { }) : undefined; - const bridgeAmount = bridgeHistoryItem - ? `${calcTokenAmount( - bridgeHistoryItem.quote.srcTokenAmount, - bridgeHistoryItem.quote.srcAsset.decimals, - ).toFixed()} ${bridgeHistoryItem.quote.srcAsset.symbol}` - : undefined; + const bridgeAmount = getBridgeAmount({ bridgeHistoryItem }); return (
@@ -141,13 +164,16 @@ const CrossChainSwapTxDetails = () => { flexDirection={FlexDirection.Column} gap={4} > - {status !== StatusTypes.COMPLETE && bridgeHistoryItem && ( - - )} + {status !== StatusTypes.COMPLETE && + (bridgeHistoryItem || srcChainTxMeta) && ( + + )} {/* Links to block explorers */} { srcBlockExplorerUrl={srcBlockExplorerUrl} destBlockExplorerUrl={destBlockExplorerUrl} /> - {/* General tx details */} diff --git a/ui/pages/routes/routes.component.js b/ui/pages/routes/routes.component.js index c70a808e4478..6212ed4258f3 100644 --- a/ui/pages/routes/routes.component.js +++ b/ui/pages/routes/routes.component.js @@ -320,7 +320,7 @@ export default class Routes extends Component { diff --git a/ui/store/actions.ts b/ui/store/actions.ts index 01b8b9e458d9..5ed6fe7ddf62 100644 --- a/ui/store/actions.ts +++ b/ui/store/actions.ts @@ -1066,6 +1066,44 @@ export async function addTransactionAndWaitForPublish( ); } +/** + * Wrapper around the promisifedBackground to create a new unapproved + * transaction in the background and return the newly created txMeta. + * This method does not show errors or route to a confirmation page + * + * @param txParams - the transaction parameters + * @param options - Additional options for the transaction. + * @param options.method + * @param options.requireApproval - Whether the transaction requires approval. + * @param options.swaps - Options specific to swaps transactions. + * @param options.swaps.hasApproveTx - Whether the swap required an approval transaction. + * @param options.swaps.meta - Additional transaction metadata required by swaps. + * @param options.type + * @returns + */ +export async function addTransaction( + txParams: TransactionParams, + options: { + method?: string; + requireApproval?: boolean; + swaps?: { hasApproveTx?: boolean; meta?: Record }; + type?: TransactionType; + }, +): Promise { + log.debug('background.addTransaction'); + + const actionId = generateActionId(); + + return await submitRequestToBackground('addTransaction', [ + txParams, + { + ...options, + origin: ORIGIN_METAMASK, + actionId, + }, + ]); +} + export function updateAndApproveTx( txMeta: TransactionMeta, dontShowLoadingIndicator: boolean,