diff --git a/packages/bridge-controller/CHANGELOG.md b/packages/bridge-controller/CHANGELOG.md index cc91e42ccc4..fa650d8ad9a 100644 --- a/packages/bridge-controller/CHANGELOG.md +++ b/packages/bridge-controller/CHANGELOG.md @@ -10,6 +10,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ### Changed - **BREAKING:** Bump peer dependency `@metamask/snaps-controllers` from `^12.0.0` to `^14.0.0` ([#6035](https://github.com/MetaMask/core/pull/6035)) +- Remove `addUserOperationFromTransaction` tx submission code and constructor arg since it is unsupported ([#6057](https://github.com/MetaMask/core/pull/6057)) +- Remove @metamask/user-operation-controller dependency ([#6057](https://github.com/MetaMask/core/pull/6057)) ## [34.0.0] diff --git a/packages/bridge-status-controller/CHANGELOG.md b/packages/bridge-status-controller/CHANGELOG.md index 46c0f80111d..696873f3960 100644 --- a/packages/bridge-status-controller/CHANGELOG.md +++ b/packages/bridge-status-controller/CHANGELOG.md @@ -7,8 +7,15 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ## [Unreleased] +### Added + +- Add `batchId` to BridgeHistoryItem to enable querying history by batchId when txId is not defined ([#6051](https://github.com/MetaMask/core/pull/6051)) +- Add optional `type` to BridgeHistoryItem to indicate the TransactionType (bridge or swap). This mitigates an edge case in which batched STX lose their TransactionType after confirmation ([#6051](https://github.com/MetaMask/core/pull/6051)) + ### Changed +- **BREAKING** Add tx batching functionality, which requires an `addTransactionBatchFn` handler to be passed to the BridgeStatusController's constructor ([#6051](https://github.com/MetaMask/core/pull/6051)) +- **BREAKING** Make BridgeHistoryItem.txMetaId optional. Batched transactions don't immediately have a transaction ID so a transaction may be keyed using batchId until it is confirmed ([#6051](https://github.com/MetaMask/core/pull/6051)) - **BREAKING:** Bump peer dependency `@metamask/snaps-controllers` from `^12.0.0` to `^14.0.0` ([#6035](https://github.com/MetaMask/core/pull/6035)) ### Fixed diff --git a/packages/bridge-status-controller/package.json b/packages/bridge-status-controller/package.json index 325add35b76..003f8656013 100644 --- a/packages/bridge-status-controller/package.json +++ b/packages/bridge-status-controller/package.json @@ -52,7 +52,6 @@ "@metamask/keyring-api": "^18.0.0", "@metamask/polling-controller": "^14.0.0", "@metamask/superstruct": "^3.1.0", - "@metamask/user-operation-controller": "^37.0.0", "@metamask/utils": "^11.2.0", "bignumber.js": "^9.1.2", "uuid": "^8.3.2" diff --git a/packages/bridge-status-controller/src/__snapshots__/bridge-status-controller.test.ts.snap b/packages/bridge-status-controller/src/__snapshots__/bridge-status-controller.test.ts.snap index 13c197ba61a..ba3558a8c3d 100644 --- a/packages/bridge-status-controller/src/__snapshots__/bridge-status-controller.test.ts.snap +++ b/packages/bridge-status-controller/src/__snapshots__/bridge-status-controller.test.ts.snap @@ -5,6 +5,7 @@ Object { "bridgeTxMetaId1": Object { "account": "0xaccount1", "approvalTxId": undefined, + "batchId": undefined, "estimatedProcessingTimeInSeconds": 15, "hasApprovalTx": false, "initialDestAssetBalance": undefined, @@ -185,6 +186,7 @@ Object { "bridgeTxMetaId1": Object { "account": "0xaccount1", "approvalTxId": undefined, + "batchId": undefined, "estimatedProcessingTimeInSeconds": 15, "hasApprovalTx": false, "initialDestAssetBalance": undefined, @@ -347,704 +349,21 @@ Array [ ] `; -exports[`BridgeStatusController submitTx: EVM bridge should delay after submitting linea approval 1`] = ` -Object { - "chainId": "0xa4b1", - "hash": "0xevmTxHash", - "id": "test-tx-id", - "status": "unapproved", - "time": 1234567890, - "txParams": Object { - "chainId": "0xa4b1", - "data": "0xdata", - "from": "0xaccount1", - "gasLimit": "0x5208", - "to": "0xbridgeContract", - "value": "0x0", - }, - "type": "bridge", -} -`; - -exports[`BridgeStatusController submitTx: EVM bridge should delay after submitting linea approval 2`] = ` -Object { - "account": "0xaccount1", - "approvalTxId": "test-approval-tx-id", - "estimatedProcessingTimeInSeconds": 15, - "hasApprovalTx": true, - "initialDestAssetBalance": undefined, - "isStxEnabled": false, - "pricingData": Object { - "amountSent": "1.234", - "amountSentInUsd": undefined, - "quotedGasInUsd": undefined, - "quotedReturnInUsd": undefined, - }, - "quote": Object { - "bridgeId": "lifi", - "bridges": Array [ - "across", - ], - "destAsset": Object { - "address": "0x0000000000000000000000000000000000000000", - "assetId": "eip155:10/slip44:60", - "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": Object { - "metabridge": Object { - "amount": "8750000000000", - "asset": Object { - "address": "0x0000000000000000000000000000000000000000", - "assetId": "eip155:42161/slip44:60", - "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": Object { - "address": "0x0000000000000000000000000000000000000000", - "assetId": "eip155:42161/slip44:60", - "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": 59144, - "srcTokenAmount": "991250000000000", - "steps": Array [ - Object { - "action": "bridge", - "destAmount": "990654755978612", - "destAsset": Object { - "address": "0x0000000000000000000000000000000000000000", - "assetId": "eip155:10/slip44:60", - "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": Object { - "displayName": "Across", - "icon": "https://raw.githubusercontent.com/lifinance/types/main/src/assets/icons/bridges/acrossv2.png", - "name": "across", - }, - "srcAmount": "991250000000000", - "srcAsset": Object { - "address": "0x0000000000000000000000000000000000000000", - "assetId": "eip155:42161/slip44:60", - "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": 1234567890, - "status": Object { - "srcChain": Object { - "chainId": 59144, - "txHash": "0xevmTxHash", - }, - "status": "PENDING", - }, - "targetContractAddress": undefined, - "txMetaId": "test-tx-id", -} -`; - -exports[`BridgeStatusController submitTx: EVM bridge should delay after submitting linea approval 3`] = ` -Array [ - Array [ - "BridgeController:stopPollingForQuotes", - ], - Array [ - "BridgeController:trackUnifiedSwapBridgeEvent", - "Unified SwapBridge Submitted", - Object { - "gas_included": false, - "price_impact": 0, - "provider": "lifi_across", - "quoted_time_minutes": 0.25, - "stx_enabled": false, - "token_symbol_destination": "ETH", - "token_symbol_source": "ETH", - "usd_amount_source": 0, - "usd_quoted_gas": 0, - "usd_quoted_return": 0, - }, - ], - Array [ - "AccountsController:getAccountByAddress", - "0xaccount1", - ], - Array [ - "NetworkController:findNetworkClientIdByChainId", - "0xa4b1", - ], - Array [ - "GasFeeController:getState", - ], - Array [ - "TransactionController:getState", - ], - Array [ - "AccountsController:getAccountByAddress", - "0xaccount1", - ], - Array [ - "NetworkController:findNetworkClientIdByChainId", - "0xa4b1", - ], - Array [ - "GasFeeController:getState", - ], - Array [ - "TransactionController:getState", - ], - Array [ - "AccountsController:getSelectedMultichainAccount", - ], -] -`; - -exports[`BridgeStatusController submitTx: EVM bridge should delay after submitting linea approval 4`] = ` -Array [ - Array [ - Object { - "data": Object { - "srcChainId": "eip155:59144", - "stxEnabled": false, - }, - "name": "Bridge Transaction Approval Completed", - }, - [Function], - ], - Array [ - Object { - "data": Object { - "srcChainId": "eip155:59144", - "stxEnabled": false, - }, - "name": "Bridge Transaction Completed", - }, - [Function], - ], -] -`; - -exports[`BridgeStatusController submitTx: EVM bridge should handle smart accounts (4337) 1`] = ` -Object { - "chainId": "0xa4b1", - "hash": "0xevmTxHash", - "id": "test-tx-id", - "status": "unapproved", - "time": 1234567890, - "txParams": Object { - "chainId": "0xa4b1", - "data": "0xdata", - "from": "0xaccount1", - "gasLimit": "0x5208", - "to": "0xbridgeContract", - "value": "0x0", - }, - "type": "bridge", -} -`; - -exports[`BridgeStatusController submitTx: EVM bridge should handle smart accounts (4337) 2`] = ` -Object { - "account": "0xaccount1", - "approvalTxId": undefined, - "estimatedProcessingTimeInSeconds": 15, - "hasApprovalTx": false, - "initialDestAssetBalance": undefined, - "isStxEnabled": false, - "pricingData": Object { - "amountSent": "1.234", - "amountSentInUsd": undefined, - "quotedGasInUsd": undefined, - "quotedReturnInUsd": undefined, - }, - "quote": Object { - "bridgeId": "lifi", - "bridges": Array [ - "across", - ], - "destAsset": Object { - "address": "0x0000000000000000000000000000000000000000", - "assetId": "eip155:10/slip44:60", - "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": Object { - "metabridge": Object { - "amount": "8750000000000", - "asset": Object { - "address": "0x0000000000000000000000000000000000000000", - "assetId": "eip155:42161/slip44:60", - "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": Object { - "address": "0x0000000000000000000000000000000000000000", - "assetId": "eip155:42161/slip44:60", - "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": Array [ - Object { - "action": "bridge", - "destAmount": "990654755978612", - "destAsset": Object { - "address": "0x0000000000000000000000000000000000000000", - "assetId": "eip155:10/slip44:60", - "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": Object { - "displayName": "Across", - "icon": "https://raw.githubusercontent.com/lifinance/types/main/src/assets/icons/bridges/acrossv2.png", - "name": "across", - }, - "srcAmount": "991250000000000", - "srcAsset": Object { - "address": "0x0000000000000000000000000000000000000000", - "assetId": "eip155:42161/slip44:60", - "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": 1234567890, - "status": Object { - "srcChain": Object { - "chainId": 42161, - "txHash": "0xevmTxHash", - }, - "status": "PENDING", - }, - "targetContractAddress": undefined, - "txMetaId": "test-tx-id", -} -`; - -exports[`BridgeStatusController submitTx: EVM bridge should handle smart accounts (4337) 3`] = ` -Array [ - Array [ - Object { - "chainId": "0xa4b1", - "networkClientId": "arbitrum", - "transactionParams": Object { - "chainId": "0xa4b1", - "data": "0xdata", - "from": "0xaccount1", - "gas": "21000", - "gasLimit": "21000", - "to": "0xbridgeContract", - "value": "0x0", - }, - }, - ], -] -`; - -exports[`BridgeStatusController submitTx: EVM bridge should handle smart accounts (4337) 4`] = ` -Array [ - Array [ - "BridgeController:stopPollingForQuotes", - ], - Array [ - "BridgeController:trackUnifiedSwapBridgeEvent", - "Unified SwapBridge Submitted", - Object { - "gas_included": false, - "price_impact": 0, - "provider": "lifi_across", - "quoted_time_minutes": 0.25, - "stx_enabled": false, - "token_symbol_destination": "ETH", - "token_symbol_source": "ETH", - "usd_amount_source": 0, - "usd_quoted_gas": 0, - "usd_quoted_return": 0, - }, - ], - Array [ - "AccountsController:getAccountByAddress", - "0xaccount1", - ], - Array [ - "NetworkController:findNetworkClientIdByChainId", - "0xa4b1", - ], - Array [ - "GasFeeController:getState", - ], - Array [ - "TransactionController:getState", - ], - Array [ - "AccountsController:getSelectedMultichainAccount", - ], -] -`; - -exports[`BridgeStatusController submitTx: EVM bridge should handle smart accounts (4337) 5`] = ` -Array [ - Array [ - Object { - "chainId": "0xa4b1", - "data": "0xdata", - "from": "0xaccount1", - "gas": "0x5208", - "gasLimit": "21000", - "maxFeePerGas": undefined, - "maxPriorityFeePerGas": undefined, - "to": "0xbridgeContract", - "value": "0x0", - }, - Object { - "actionId": "1234567890.456", - "networkClientId": "arbitrum", - "origin": "metamask", - "requireApproval": false, - "type": "bridge", - }, - ], -] -`; - -exports[`BridgeStatusController submitTx: EVM bridge should handle smart transactions 1`] = ` -Object { - "approvalTxId": undefined, - "chainId": "0xa4b1", - "destinationChainId": "0xa", - "destinationTokenAddress": "0x0000000000000000000000000000000000000000", - "destinationTokenAmount": "990654755978612", - "destinationTokenDecimals": 18, - "destinationTokenSymbol": "ETH", - "hash": "0xevmTxHash", - "id": "test-tx-id", - "sourceTokenAddress": "0x0000000000000000000000000000000000000000", - "sourceTokenAmount": "991250000000000", - "sourceTokenDecimals": 18, - "sourceTokenSymbol": "ETH", - "status": "unapproved", - "swapTokenValue": "1.234", - "time": 1234567890, - "txParams": Object { - "chainId": "0xa4b1", - "data": "0xdata", - "from": "0xaccount1", - "gasLimit": "0x5208", - "to": "0xbridgeContract", - "value": "0x0", - }, - "type": "bridge", -} -`; - -exports[`BridgeStatusController submitTx: EVM bridge should handle smart transactions 2`] = ` -Object { - "account": "", - "approvalTxId": undefined, - "estimatedProcessingTimeInSeconds": 15, - "hasApprovalTx": false, - "initialDestAssetBalance": undefined, - "isStxEnabled": true, - "pricingData": Object { - "amountSent": "1.234", - "amountSentInUsd": undefined, - "quotedGasInUsd": undefined, - "quotedReturnInUsd": undefined, - }, - "quote": Object { - "bridgeId": "lifi", - "bridges": Array [ - "across", - ], - "destAsset": Object { - "address": "0x0000000000000000000000000000000000000000", - "assetId": "eip155:10/slip44:60", - "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": Object { - "metabridge": Object { - "amount": "8750000000000", - "asset": Object { - "address": "0x0000000000000000000000000000000000000000", - "assetId": "eip155:42161/slip44:60", - "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": Object { - "address": "0x0000000000000000000000000000000000000000", - "assetId": "eip155:42161/slip44:60", - "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": Array [ - Object { - "action": "bridge", - "destAmount": "990654755978612", - "destAsset": Object { - "address": "0x0000000000000000000000000000000000000000", - "assetId": "eip155:10/slip44:60", - "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": Object { - "displayName": "Across", - "icon": "https://raw.githubusercontent.com/lifinance/types/main/src/assets/icons/bridges/acrossv2.png", - "name": "across", - }, - "srcAmount": "991250000000000", - "srcAsset": Object { - "address": "0x0000000000000000000000000000000000000000", - "assetId": "eip155:42161/slip44:60", - "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": 1234567890, - "status": Object { - "srcChain": Object { - "chainId": 42161, - "txHash": "0xevmTxHash", - }, - "status": "PENDING", - }, - "targetContractAddress": undefined, - "txMetaId": "test-tx-id", -} -`; - -exports[`BridgeStatusController submitTx: EVM bridge should handle smart transactions 3`] = ` -Array [ - Array [ - Object { - "chainId": "0xa4b1", - "networkClientId": "arbitrum", - "transactionParams": Object { - "chainId": "0xa4b1", - "data": "0xdata", - "from": "0xaccount1", - "gas": "21000", - "gasLimit": "21000", - "to": "0xbridgeContract", - "value": "0x0", - }, - }, - ], -] -`; - -exports[`BridgeStatusController submitTx: EVM bridge should handle smart transactions 4`] = ` -Array [ - Array [ - Object { - "chainId": "0xa4b1", - "data": "0xdata", - "from": "0xaccount1", - "gas": "0x5208", - "gasLimit": "21000", - "maxFeePerGas": undefined, - "maxPriorityFeePerGas": undefined, - "to": "0xbridgeContract", - "value": "0x0", - }, - Object { - "actionId": "1234567890.456", - "networkClientId": "arbitrum", - "origin": "metamask", - "requireApproval": false, - "type": "bridge", - }, - ], -] -`; - -exports[`BridgeStatusController submitTx: EVM bridge should handle smart transactions 5`] = ` -Array [ - Array [ - "BridgeController:stopPollingForQuotes", - ], - Array [ - "BridgeController:trackUnifiedSwapBridgeEvent", - "Unified SwapBridge Submitted", - Object { - "gas_included": false, - "price_impact": 0, - "provider": "lifi_across", - "quoted_time_minutes": 0.25, - "stx_enabled": true, - "token_symbol_destination": "ETH", - "token_symbol_source": "ETH", - "usd_amount_source": 0, - "usd_quoted_gas": 0, - "usd_quoted_return": 0, - }, - ], - Array [ - "AccountsController:getAccountByAddress", - "0xaccount1", - ], - Array [ - "NetworkController:findNetworkClientIdByChainId", - "0xa4b1", - ], - Array [ - "GasFeeController:getState", - ], - Array [ - "AccountsController:getSelectedMultichainAccount", - ], -] -`; - -exports[`BridgeStatusController submitTx: EVM bridge should reset USDT allowance 1`] = ` +exports[`BridgeStatusController submitTx: EVM bridge should handle smart transactions 1`] = ` Object { - "chainId": "0xa4b1", - "hash": "0xevmTxHash", - "id": "test-tx-id", - "status": "unapproved", - "time": 1234567890, - "txParams": Object { - "chainId": "0xa4b1", - "data": "0xdata", - "from": "0xaccount1", - "gasLimit": "0x5208", - "to": "0xbridgeContract", - "value": "0x0", - }, - "type": "bridge", + "batchId": "0xevmTxHash", } `; -exports[`BridgeStatusController submitTx: EVM bridge should reset USDT allowance 2`] = ` +exports[`BridgeStatusController submitTx: EVM bridge should handle smart transactions 2`] = ` Object { "account": "0xaccount1", - "approvalTxId": "test-approval-tx-id", + "approvalTxId": undefined, + "batchId": "0xevmTxHash", "estimatedProcessingTimeInSeconds": 15, - "hasApprovalTx": true, + "hasApprovalTx": false, "initialDestAssetBalance": undefined, - "isStxEnabled": false, + "isStxEnabled": true, "pricingData": Object { "amountSent": "1.234", "amountSentInUsd": undefined, @@ -1146,57 +465,25 @@ Object { "status": Object { "srcChain": Object { "chainId": 42161, - "txHash": "0xevmTxHash", + "txHash": undefined, }, "status": "PENDING", }, "targetContractAddress": undefined, - "txMetaId": "test-tx-id", + "txMetaId": undefined, } `; -exports[`BridgeStatusController submitTx: EVM bridge should reset USDT allowance 3`] = ` +exports[`BridgeStatusController submitTx: EVM bridge should handle smart transactions 3`] = ` Array [ - Array [ - Object { - "chainId": "0xa4b1", - "networkClientId": "arbitrum-client-id", - "transactionParams": Object { - "chainId": "0xa4b1", - "data": "0x095ea7b30000000000000000000000000439e60f02a8900a951603950d8d4527f400c3f10000000000000000000000000000000000000000000000000000000000000000", - "from": "0xaccount1", - "gas": "21000", - "gasLimit": "21000", - "to": "0xtokenContract", - "value": "0x0", - }, - }, - ], - Array [ - Object { - "chainId": "0xa4b1", - "networkClientId": "arbitrum-client-id", - "transactionParams": Object { - "chainId": "0xa4b1", - "data": "0xapprovalData", - "from": "0xaccount1", - "gas": "21000", - "gasLimit": "21000", - "to": "0xtokenContract", - "value": "0x0", - }, - }, - ], Array [ Object { "chainId": "0xa4b1", "networkClientId": "arbitrum", "transactionParams": Object { - "chainId": "0xa4b1", "data": "0xdata", "from": "0xaccount1", "gas": "21000", - "gasLimit": "21000", "to": "0xbridgeContract", "value": "0x0", }, @@ -1205,72 +492,35 @@ Array [ ] `; -exports[`BridgeStatusController submitTx: EVM bridge should reset USDT allowance 4`] = ` +exports[`BridgeStatusController submitTx: EVM bridge should handle smart transactions 4`] = ` Array [ Array [ Object { - "chainId": "0xa4b1", - "data": "0x095ea7b30000000000000000000000000439e60f02a8900a951603950d8d4527f400c3f10000000000000000000000000000000000000000000000000000000000000000", - "from": "0xaccount1", - "gas": "0x5208", - "gasLimit": "21000", - "maxFeePerGas": undefined, - "maxPriorityFeePerGas": undefined, - "to": "0xtokenContract", - "value": "0x0", - }, - Object { - "actionId": "1234567890.456", - "networkClientId": "arbitrum-client-id", - "origin": "metamask", - "requireApproval": false, - "type": "bridgeApproval", - }, - ], - Array [ - Object { - "chainId": "0xa4b1", - "data": "0xapprovalData", - "from": "0xaccount1", - "gas": "0x5208", - "gasLimit": "21000", - "maxFeePerGas": undefined, - "maxPriorityFeePerGas": undefined, - "to": "0xtokenContract", - "value": "0x0", - }, - Object { - "actionId": "1234567890.456", - "networkClientId": "arbitrum-client-id", - "origin": "metamask", - "requireApproval": false, - "type": "bridgeApproval", - }, - ], - Array [ - Object { - "chainId": "0xa4b1", - "data": "0xdata", + "disable7702": true, "from": "0xaccount1", - "gas": "0x5208", - "gasLimit": "21000", - "maxFeePerGas": undefined, - "maxPriorityFeePerGas": undefined, - "to": "0xbridgeContract", - "value": "0x0", - }, - Object { - "actionId": "1234567890.456", "networkClientId": "arbitrum", "origin": "metamask", "requireApproval": false, - "type": "bridge", + "transactions": Array [ + Object { + "params": Object { + "data": "0xdata", + "from": "0xaccount1", + "gas": "0x5208", + "maxFeePerGas": "0x0", + "maxPriorityFeePerGas": "0x0", + "to": "0xbridgeContract", + "value": "0x0", + }, + "type": "bridge", + }, + ], }, ], ] `; -exports[`BridgeStatusController submitTx: EVM bridge should reset USDT allowance 5`] = ` +exports[`BridgeStatusController submitTx: EVM bridge should handle smart transactions 5`] = ` Array [ Array [ "BridgeController:stopPollingForQuotes", @@ -1283,7 +533,7 @@ Array [ "price_impact": 0, "provider": "lifi_across", "quoted_time_minutes": 0.25, - "stx_enabled": false, + "stx_enabled": true, "token_symbol_destination": "ETH", "token_symbol_source": "ETH", "usd_amount_source": 0, @@ -1291,39 +541,6 @@ Array [ "usd_quoted_return": 0, }, ], - Array [ - "BridgeController:getBridgeERC20Allowance", - "0x0000000000000000000000000000000000000000", - "0xa4b1", - ], - Array [ - "AccountsController:getAccountByAddress", - "0xaccount1", - ], - Array [ - "NetworkController:findNetworkClientIdByChainId", - "0xa4b1", - ], - Array [ - "GasFeeController:getState", - ], - Array [ - "TransactionController:getState", - ], - Array [ - "AccountsController:getAccountByAddress", - "0xaccount1", - ], - Array [ - "NetworkController:findNetworkClientIdByChainId", - "0xa4b1", - ], - Array [ - "GasFeeController:getState", - ], - Array [ - "TransactionController:getState", - ], Array [ "AccountsController:getAccountByAddress", "0xaccount1", @@ -1335,38 +552,23 @@ Array [ Array [ "GasFeeController:getState", ], - Array [ - "TransactionController:getState", - ], Array [ "AccountsController:getSelectedMultichainAccount", ], ] `; -exports[`BridgeStatusController submitTx: EVM bridge should successfully submit an EVM bridge transaction with approval 1`] = ` +exports[`BridgeStatusController submitTx: EVM bridge should reset USDT allowance 1`] = ` Object { - "chainId": "0xa4b1", - "hash": "0xevmTxHash", - "id": "test-tx-id", - "status": "unapproved", - "time": 1234567890, - "txParams": Object { - "chainId": "0xa4b1", - "data": "0xdata", - "from": "0xaccount1", - "gasLimit": "0x5208", - "to": "0xbridgeContract", - "value": "0x0", - }, - "type": "bridge", + "batchId": "0xapprovalTxHash", } `; -exports[`BridgeStatusController submitTx: EVM bridge should successfully submit an EVM bridge transaction with approval 2`] = ` +exports[`BridgeStatusController submitTx: EVM bridge should reset USDT allowance 2`] = ` Object { - "account": "0xaccount1", - "approvalTxId": "test-approval-tx-id", + "account": "", + "approvalTxId": undefined, + "batchId": "0xapprovalTxHash", "estimatedProcessingTimeInSeconds": 15, "hasApprovalTx": true, "initialDestAssetBalance": undefined, @@ -1472,61 +674,99 @@ Object { "status": Object { "srcChain": Object { "chainId": 42161, - "txHash": "0xevmTxHash", + "txHash": undefined, }, "status": "PENDING", }, "targetContractAddress": undefined, - "txMetaId": "test-tx-id", + "txMetaId": undefined, } `; -exports[`BridgeStatusController submitTx: EVM bridge should successfully submit an EVM bridge transaction with approval 3`] = ` +exports[`BridgeStatusController submitTx: EVM bridge should reset USDT allowance 3`] = ` Array [ Array [ Object { "chainId": "0xa4b1", - "data": "0xapprovalData", - "from": "0xaccount1", - "gas": "0x5208", - "gasLimit": "21000", - "maxFeePerGas": undefined, - "maxPriorityFeePerGas": undefined, - "to": "0xtokenContract", - "value": "0x0", - }, - Object { - "actionId": "1234567890.456", - "networkClientId": "arbitrum-client-id", - "origin": "metamask", - "requireApproval": false, - "type": "bridgeApproval", + "networkClientId": Object { + "gasFeeEstimates": Object { + "estimatedBaseFee": "0x1234", + }, + }, + "transactionParams": Object { + "data": "0xapprovalData", + "from": "0xaccount1", + "gas": "21000", + "to": "0xtokenContract", + "value": "0x0", + }, }, ], Array [ Object { "chainId": "0xa4b1", - "data": "0xdata", - "from": "0xaccount1", - "gas": "0x5208", - "gasLimit": "21000", - "maxFeePerGas": undefined, - "maxPriorityFeePerGas": undefined, - "to": "0xbridgeContract", - "value": "0x0", + "networkClientId": Object { + "gasFeeEstimates": Object { + "estimatedBaseFee": "0x1234", + }, + }, + "transactionParams": Object { + "data": "0xdata", + "from": "0xaccount1", + "gas": "21000", + "to": "0xbridgeContract", + "value": "0x0", + }, }, + ], +] +`; + +exports[`BridgeStatusController submitTx: EVM bridge should reset USDT allowance 4`] = ` +Array [ + Array [ Object { - "actionId": "1234567890.456", - "networkClientId": "arbitrum", + "disable7702": true, + "from": "0xaccount1", + "networkClientId": Object { + "gasFeeEstimates": Object { + "estimatedBaseFee": "0x1234", + }, + }, "origin": "metamask", "requireApproval": false, - "type": "bridge", + "transactions": Array [ + Object { + "params": Object { + "data": "0xapprovalData", + "from": "0xaccount1", + "gas": "0x5208", + "maxFeePerGas": "0x0", + "maxPriorityFeePerGas": "0x0", + "to": "0xtokenContract", + "value": "0x0", + }, + "type": "bridgeApproval", + }, + Object { + "params": Object { + "data": "0xdata", + "from": "0xaccount1", + "gas": "0x5208", + "maxFeePerGas": "0x0", + "maxPriorityFeePerGas": "0x0", + "to": "0xbridgeContract", + "value": "0x0", + }, + "type": "bridge", + }, + ], }, ], ] `; -exports[`BridgeStatusController submitTx: EVM bridge should successfully submit an EVM bridge transaction with approval 4`] = ` +exports[`BridgeStatusController submitTx: EVM bridge should reset USDT allowance 5`] = ` Array [ Array [ "BridgeController:stopPollingForQuotes", @@ -1548,19 +788,10 @@ Array [ }, ], Array [ - "AccountsController:getAccountByAddress", - "0xaccount1", - ], - Array [ - "NetworkController:findNetworkClientIdByChainId", + "BridgeController:getBridgeERC20Allowance", + "0x0000000000000000000000000000000000000000", "0xa4b1", ], - Array [ - "GasFeeController:getState", - ], - Array [ - "TransactionController:getState", - ], Array [ "AccountsController:getAccountByAddress", "0xaccount1", @@ -1573,7 +804,7 @@ Array [ "GasFeeController:getState", ], Array [ - "TransactionController:getState", + "GasFeeController:getState", ], Array [ "AccountsController:getSelectedMultichainAccount", @@ -1581,31 +812,19 @@ Array [ ] `; -exports[`BridgeStatusController submitTx: EVM bridge should successfully submit an EVM bridge transaction with no approval 1`] = ` +exports[`BridgeStatusController submitTx: EVM bridge should successfully submit an EVM bridge transaction with approval 1`] = ` Object { - "chainId": "0xa4b1", - "hash": "0xevmTxHash", - "id": "test-tx-id", - "status": "unapproved", - "time": 1234567890, - "txParams": Object { - "chainId": "0xa4b1", - "data": "0xdata", - "from": "0xaccount1", - "gasLimit": "0x5208", - "to": "0xbridgeContract", - "value": "0x0", - }, - "type": "bridge", + "batchId": "0xapprovalTxHash", } `; -exports[`BridgeStatusController submitTx: EVM bridge should successfully submit an EVM bridge transaction with no approval 2`] = ` +exports[`BridgeStatusController submitTx: EVM bridge should successfully submit an EVM bridge transaction with approval 2`] = ` Object { "account": "0xaccount1", "approvalTxId": undefined, + "batchId": "0xapprovalTxHash", "estimatedProcessingTimeInSeconds": 15, - "hasApprovalTx": false, + "hasApprovalTx": true, "initialDestAssetBalance": undefined, "isStxEnabled": false, "pricingData": Object { @@ -1620,16 +839,16 @@ Object { "across", ], "destAsset": Object { - "address": "0x0000000000000000000000000000000000000032", + "address": "0x0000000000000000000000000000000000000000", "assetId": "eip155:10/slip44:60", "chainId": 10, - "coinKey": "WETH", + "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": "WETH", + "name": "ETH", "priceUSD": "2478.63", - "symbol": "WETH", + "symbol": "ETH", }, "destChainId": 10, "destTokenAmount": "990654755978612", @@ -1700,136 +919,65 @@ Object { "priceUSD": "2478.7", "symbol": "ETH", }, - "srcChainId": 42161, - }, - ], - }, - "slippagePercentage": 0, - "startTime": 1234567890, - "status": Object { - "srcChain": Object { - "chainId": 42161, - "txHash": "0xevmTxHash", - }, - "status": "PENDING", - }, - "targetContractAddress": undefined, - "txMetaId": "test-tx-id", -} -`; - -exports[`BridgeStatusController submitTx: EVM bridge should successfully submit an EVM bridge transaction with no approval 3`] = ` -Array [ - Array [ - Object { - "chainId": "0xa4b1", - "networkClientId": "arbitrum", - "transactionParams": Object { - "chainId": "0xa4b1", - "data": "0xdata", - "from": "0xaccount1", - "gas": "21000", - "gasLimit": "21000", - "to": "0xbridgeContract", - "value": "0x0", + "srcChainId": 42161, }, + ], + }, + "slippagePercentage": 0, + "startTime": 1234567890, + "status": Object { + "srcChain": Object { + "chainId": 42161, + "txHash": undefined, }, - ], -] -`; - -exports[`BridgeStatusController submitTx: EVM bridge should successfully submit an EVM bridge transaction with no approval 4`] = ` -Array [ - Array [ - Object { - "chainId": "0xa4b1", - "data": "0xdata", - "from": "0xaccount1", - "gas": "0x5208", - "gasLimit": "21000", - "maxFeePerGas": undefined, - "maxPriorityFeePerGas": undefined, - "to": "0xbridgeContract", - "value": "0x0", - }, - Object { - "actionId": "1234567890.456", - "networkClientId": "arbitrum", - "origin": "metamask", - "requireApproval": false, - "type": "bridge", - }, - ], -] -`; - -exports[`BridgeStatusController submitTx: EVM bridge should successfully submit an EVM bridge transaction with no approval 5`] = ` -Array [ - Array [ - "BridgeController:stopPollingForQuotes", - ], - Array [ - "BridgeController:trackUnifiedSwapBridgeEvent", - "Unified SwapBridge Submitted", - Object { - "gas_included": false, - "price_impact": 0, - "provider": "lifi_across", - "quoted_time_minutes": 0.25, - "stx_enabled": false, - "token_symbol_destination": "WETH", - "token_symbol_source": "ETH", - "usd_amount_source": 0, - "usd_quoted_gas": 0, - "usd_quoted_return": 0, - }, - ], - Array [ - "AccountsController:getAccountByAddress", - "0xaccount1", - ], - Array [ - "NetworkController:findNetworkClientIdByChainId", - "0xa4b1", - ], - Array [ - "GasFeeController:getState", - ], - Array [ - "TransactionController:getState", - ], - Array [ - "AccountsController:getSelectedMultichainAccount", - ], -] + "status": "PENDING", + }, + "targetContractAddress": undefined, + "txMetaId": undefined, +} `; -exports[`BridgeStatusController submitTx: EVM bridge should throw an error if approval tx fails 1`] = ` +exports[`BridgeStatusController submitTx: EVM bridge should successfully submit an EVM bridge transaction with approval 3`] = ` Array [ Array [ Object { - "chainId": "0xa4b1", - "data": "0xapprovalData", + "disable7702": true, "from": "0xaccount1", - "gas": "0x5208", - "gasLimit": "21000", - "maxFeePerGas": undefined, - "maxPriorityFeePerGas": undefined, - "to": "0xtokenContract", - "value": "0x0", - }, - Object { - "actionId": "1234567890.456", "networkClientId": "arbitrum-client-id", "origin": "metamask", "requireApproval": false, - "type": "bridgeApproval", + "transactions": Array [ + Object { + "params": Object { + "data": "0xapprovalData", + "from": "0xaccount1", + "gas": "0x5208", + "maxFeePerGas": "0x0", + "maxPriorityFeePerGas": "0x0", + "to": "0xtokenContract", + "value": "0x0", + }, + "type": "bridgeApproval", + }, + Object { + "params": Object { + "data": "0xdata", + "from": "0xaccount1", + "gas": "0x5208", + "maxFeePerGas": "0x0", + "maxPriorityFeePerGas": "0x0", + "to": "0xbridgeContract", + "value": "0x0", + }, + "type": "bridge", + }, + ], }, ], ] `; -exports[`BridgeStatusController submitTx: EVM bridge should throw an error if approval tx fails 2`] = ` +exports[`BridgeStatusController submitTx: EVM bridge should successfully submit an EVM bridge transaction with approval 4`] = ` Array [ Array [ "BridgeController:stopPollingForQuotes", @@ -1861,96 +1009,27 @@ Array [ Array [ "GasFeeController:getState", ], -] -`; - -exports[`BridgeStatusController submitTx: EVM bridge should throw an error if approval tx meta does not exist 1`] = ` -Array [ - Array [ - Object { - "chainId": "0xa4b1", - "data": "0xapprovalData", - "from": "0xaccount1", - "gas": "0x5208", - "gasLimit": "21000", - "maxFeePerGas": undefined, - "maxPriorityFeePerGas": undefined, - "to": "0xtokenContract", - "value": "0x0", - }, - Object { - "actionId": "1234567890.456", - "networkClientId": "arbitrum-client-id", - "origin": "metamask", - "requireApproval": false, - "type": "bridgeApproval", - }, - ], -] -`; - -exports[`BridgeStatusController submitTx: EVM bridge should throw an error if approval tx meta does not exist 2`] = ` -Array [ - Array [ - "BridgeController:stopPollingForQuotes", - ], - Array [ - "BridgeController:trackUnifiedSwapBridgeEvent", - "Unified SwapBridge Submitted", - Object { - "gas_included": false, - "price_impact": 0, - "provider": "lifi_across", - "quoted_time_minutes": 0.25, - "stx_enabled": false, - "token_symbol_destination": "ETH", - "token_symbol_source": "ETH", - "usd_amount_source": 0, - "usd_quoted_gas": 0, - "usd_quoted_return": 0, - }, - ], - Array [ - "AccountsController:getAccountByAddress", - "0xaccount1", - ], - Array [ - "NetworkController:findNetworkClientIdByChainId", - "0xa4b1", - ], Array [ "GasFeeController:getState", ], Array [ - "TransactionController:getState", + "AccountsController:getSelectedMultichainAccount", ], ] `; -exports[`BridgeStatusController submitTx: EVM swap should handle smart accounts (4337) 1`] = ` +exports[`BridgeStatusController submitTx: EVM bridge should successfully submit an EVM bridge transaction with no approval 1`] = ` Object { - "chainId": "0xa4b1", - "hash": "0xevmTxHash", - "id": "test-tx-id", - "status": "unapproved", - "time": 1234567890, - "txParams": Object { - "chainId": "0xa4b1", - "data": "0xdata", - "from": "0xaccount1", - "gasLimit": "0x5208", - "to": "0xbridgeContract", - "value": "0x0", - }, - "type": "swap", + "batchId": "0xevmTxHash", } `; -exports[`BridgeStatusController submitTx: EVM swap should handle smart accounts (4337) 2`] = ` +exports[`BridgeStatusController submitTx: EVM bridge should successfully submit an EVM bridge transaction with no approval 2`] = ` Object { - "account": "", + "account": "0xaccount1", "approvalTxId": undefined, - "estimatedProcessingTimeInSeconds": 0, + "batchId": "0xevmTxHash", + "estimatedProcessingTimeInSeconds": 15, "hasApprovalTx": false, "initialDestAssetBalance": undefined, "isStxEnabled": false, @@ -1966,18 +1045,18 @@ Object { "across", ], "destAsset": Object { - "address": "0x0000000000000000000000000000000000000000", + "address": "0x0000000000000000000000000000000000000032", "assetId": "eip155:10/slip44:60", "chainId": 10, - "coinKey": "ETH", + "coinKey": "WETH", "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", + "name": "WETH", "priceUSD": "2478.63", - "symbol": "ETH", + "symbol": "WETH", }, - "destChainId": 42161, + "destChainId": 10, "destTokenAmount": "990654755978612", "feeData": Object { "metabridge": Object { @@ -2055,27 +1134,25 @@ Object { "status": Object { "srcChain": Object { "chainId": 42161, - "txHash": "0xevmTxHash", + "txHash": undefined, }, "status": "PENDING", }, "targetContractAddress": undefined, - "txMetaId": "test-tx-id", + "txMetaId": undefined, } `; -exports[`BridgeStatusController submitTx: EVM swap should handle smart accounts (4337) 3`] = ` +exports[`BridgeStatusController submitTx: EVM bridge should successfully submit an EVM bridge transaction with no approval 3`] = ` Array [ Array [ Object { "chainId": "0xa4b1", "networkClientId": "arbitrum", "transactionParams": Object { - "chainId": "0xa4b1", "data": "0xdata", "from": "0xaccount1", "gas": "21000", - "gasLimit": "21000", "to": "0xbridgeContract", "value": "0x0", }, @@ -2084,7 +1161,35 @@ Array [ ] `; -exports[`BridgeStatusController submitTx: EVM swap should handle smart accounts (4337) 4`] = ` +exports[`BridgeStatusController submitTx: EVM bridge should successfully submit an EVM bridge transaction with no approval 4`] = ` +Array [ + Array [ + Object { + "disable7702": true, + "from": "0xaccount1", + "networkClientId": "arbitrum", + "origin": "metamask", + "requireApproval": false, + "transactions": Array [ + Object { + "params": Object { + "data": "0xdata", + "from": "0xaccount1", + "gas": "0x5208", + "maxFeePerGas": "0x0", + "maxPriorityFeePerGas": "0x0", + "to": "0xbridgeContract", + "value": "0x0", + }, + "type": "bridge", + }, + ], + }, + ], +] +`; + +exports[`BridgeStatusController submitTx: EVM bridge should successfully submit an EVM bridge transaction with no approval 5`] = ` Array [ Array [ "BridgeController:stopPollingForQuotes", @@ -2096,9 +1201,9 @@ Array [ "gas_included": false, "price_impact": 0, "provider": "lifi_across", - "quoted_time_minutes": 0, + "quoted_time_minutes": 0.25, "stx_enabled": false, - "token_symbol_destination": "ETH", + "token_symbol_destination": "WETH", "token_symbol_source": "ETH", "usd_amount_source": 0, "usd_quoted_gas": 0, @@ -2116,67 +1221,15 @@ Array [ Array [ "GasFeeController:getState", ], - Array [ - "TransactionController:getState", - ], Array [ "AccountsController:getSelectedMultichainAccount", ], ] `; -exports[`BridgeStatusController submitTx: EVM swap should handle smart accounts (4337) 5`] = ` -Array [ - Array [ - Object { - "chainId": "0xa4b1", - "data": "0xdata", - "from": "0xaccount1", - "gas": "0x5208", - "gasLimit": "21000", - "maxFeePerGas": undefined, - "maxPriorityFeePerGas": undefined, - "to": "0xbridgeContract", - "value": "0x0", - }, - Object { - "actionId": "1234567890.456", - "networkClientId": "arbitrum", - "origin": "metamask", - "requireApproval": false, - "type": "swap", - }, - ], -] -`; - exports[`BridgeStatusController submitTx: EVM swap should handle smart transactions 1`] = ` Object { - "approvalTxId": undefined, - "chainId": "0xa4b1", - "destinationChainId": "0xa4b1", - "destinationTokenAddress": "0x0000000000000000000000000000000000000000", - "destinationTokenAmount": "990654755978612", - "destinationTokenDecimals": 18, - "destinationTokenSymbol": "ETH", - "hash": "0xevmTxHash", - "id": "test-tx-id", - "sourceTokenAddress": "0x0000000000000000000000000000000000000000", - "sourceTokenAmount": "991250000000000", - "sourceTokenDecimals": 18, - "sourceTokenSymbol": "ETH", - "status": "unapproved", - "swapTokenValue": "1.234", - "time": 1234567890, - "txParams": Object { - "chainId": "0xa4b1", - "data": "0xdata", - "from": "0xaccount1", - "gasLimit": "0x5208", - "to": "0xbridgeContract", - "value": "0x0", - }, - "type": "swap", + "batchId": "0xevmTxHash", } `; @@ -2184,6 +1237,7 @@ exports[`BridgeStatusController submitTx: EVM swap should handle smart transacti Object { "account": "", "approvalTxId": undefined, + "batchId": "0xevmTxHash", "estimatedProcessingTimeInSeconds": 0, "hasApprovalTx": false, "initialDestAssetBalance": undefined, @@ -2289,12 +1343,12 @@ Object { "status": Object { "srcChain": Object { "chainId": 42161, - "txHash": "0xevmTxHash", + "txHash": undefined, }, "status": "PENDING", }, "targetContractAddress": undefined, - "txMetaId": "test-tx-id", + "txMetaId": undefined, } `; @@ -2305,11 +1359,9 @@ Array [ "chainId": "0xa4b1", "networkClientId": "arbitrum", "transactionParams": Object { - "chainId": "0xa4b1", "data": "0xdata", "from": "0xaccount1", "gas": "21000", - "gasLimit": "21000", "to": "0xbridgeContract", "value": "0x0", }, @@ -2322,22 +1374,25 @@ exports[`BridgeStatusController submitTx: EVM swap should handle smart transacti Array [ Array [ Object { - "chainId": "0xa4b1", - "data": "0xdata", + "disable7702": true, "from": "0xaccount1", - "gas": "0x5208", - "gasLimit": "21000", - "maxFeePerGas": undefined, - "maxPriorityFeePerGas": undefined, - "to": "0xbridgeContract", - "value": "0x0", - }, - Object { - "actionId": "1234567890.456", "networkClientId": "arbitrum", "origin": "metamask", "requireApproval": false, - "type": "swap", + "transactions": Array [ + Object { + "params": Object { + "data": "0xdata", + "from": "0xaccount1", + "gas": "0x5208", + "maxFeePerGas": "0x0", + "maxPriorityFeePerGas": "0x0", + "to": "0xbridgeContract", + "value": "0x0", + }, + "type": "swap", + }, + ], }, ], ] @@ -2383,27 +1438,15 @@ Array [ exports[`BridgeStatusController submitTx: EVM swap should successfully submit an EVM swap transaction with approval 1`] = ` Object { - "chainId": "0xa4b1", - "hash": "0xevmTxHash", - "id": "test-tx-id", - "status": "unapproved", - "time": 1234567890, - "txParams": Object { - "chainId": "0xa4b1", - "data": "0xdata", - "from": "0xaccount1", - "gasLimit": "0x5208", - "to": "0xbridgeContract", - "value": "0x0", - }, - "type": "swap", + "batchId": "0xapprovalTxHash", } `; exports[`BridgeStatusController submitTx: EVM swap should successfully submit an EVM swap transaction with approval 2`] = ` Object { - "account": "0xaccount1", - "approvalTxId": "test-approval-tx-id", + "account": "", + "approvalTxId": undefined, + "batchId": "0xapprovalTxHash", "estimatedProcessingTimeInSeconds": 0, "hasApprovalTx": true, "initialDestAssetBalance": undefined, @@ -2509,12 +1552,12 @@ Object { "status": Object { "srcChain": Object { "chainId": 42161, - "txHash": "0xevmTxHash", + "txHash": undefined, }, "status": "PENDING", }, "targetContractAddress": undefined, - "txMetaId": "test-tx-id", + "txMetaId": undefined, } `; @@ -2522,42 +1565,37 @@ exports[`BridgeStatusController submitTx: EVM swap should successfully submit an Array [ Array [ Object { - "chainId": "0xa4b1", - "data": "0xapprovalData", + "disable7702": true, "from": "0xaccount1", - "gas": "0x5208", - "gasLimit": "21000", - "maxFeePerGas": undefined, - "maxPriorityFeePerGas": undefined, - "to": "0xtokenContract", - "value": "0x0", - }, - Object { - "actionId": "1234567890.456", "networkClientId": "arbitrum-client-id", "origin": "metamask", "requireApproval": false, - "type": "swapApproval", - }, - ], - Array [ - Object { - "chainId": "0xa4b1", - "data": "0xdata", - "from": "0xaccount1", - "gas": "0x5208", - "gasLimit": "21000", - "maxFeePerGas": undefined, - "maxPriorityFeePerGas": undefined, - "to": "0xbridgeContract", - "value": "0x0", - }, - Object { - "actionId": "1234567890.456", - "networkClientId": "arbitrum", - "origin": "metamask", - "requireApproval": false, - "type": "swap", + "transactions": Array [ + Object { + "params": Object { + "data": "0xapprovalData", + "from": "0xaccount1", + "gas": "0x5208", + "maxFeePerGas": "0x0", + "maxPriorityFeePerGas": "0x0", + "to": "0xtokenContract", + "value": "0x0", + }, + "type": "swapApproval", + }, + Object { + "params": Object { + "data": "0xdata", + "from": "0xaccount1", + "gas": "0x5208", + "maxFeePerGas": "0x0", + "maxPriorityFeePerGas": "0x0", + "to": "0xbridgeContract", + "value": "0x0", + }, + "type": "swap", + }, + ], }, ], ] @@ -2595,23 +1633,9 @@ Array [ Array [ "GasFeeController:getState", ], - Array [ - "TransactionController:getState", - ], - Array [ - "AccountsController:getAccountByAddress", - "0xaccount1", - ], - Array [ - "NetworkController:findNetworkClientIdByChainId", - "0xa4b1", - ], Array [ "GasFeeController:getState", ], - Array [ - "TransactionController:getState", - ], Array [ "AccountsController:getSelectedMultichainAccount", ], @@ -2620,20 +1644,7 @@ Array [ exports[`BridgeStatusController submitTx: EVM swap should successfully submit an EVM swap transaction with no approval 1`] = ` Object { - "chainId": "0xa4b1", - "hash": "0xevmTxHash", - "id": "test-tx-id", - "status": "unapproved", - "time": 1234567890, - "txParams": Object { - "chainId": "0xa4b1", - "data": "0xdata", - "from": "0xaccount1", - "gasLimit": "0x5208", - "to": "0xbridgeContract", - "value": "0x0", - }, - "type": "swap", + "batchId": "0xevmTxHash", } `; @@ -2641,6 +1652,7 @@ exports[`BridgeStatusController submitTx: EVM swap should successfully submit an Object { "account": "0xaccount1", "approvalTxId": undefined, + "batchId": "0xevmTxHash", "estimatedProcessingTimeInSeconds": 0, "hasApprovalTx": false, "initialDestAssetBalance": undefined, @@ -2746,12 +1758,12 @@ Object { "status": Object { "srcChain": Object { "chainId": 42161, - "txHash": "0xevmTxHash", + "txHash": undefined, }, "status": "PENDING", }, "targetContractAddress": undefined, - "txMetaId": "test-tx-id", + "txMetaId": undefined, } `; @@ -2762,11 +1774,9 @@ Array [ "chainId": "0xa4b1", "networkClientId": "arbitrum", "transactionParams": Object { - "chainId": "0xa4b1", "data": "0xdata", "from": "0xaccount1", "gas": "21000", - "gasLimit": "21000", "to": "0xbridgeContract", "value": "0x0", }, @@ -2779,22 +1789,25 @@ exports[`BridgeStatusController submitTx: EVM swap should successfully submit an Array [ Array [ Object { - "chainId": "0xa4b1", - "data": "0xdata", + "disable7702": true, "from": "0xaccount1", - "gas": "0x5208", - "gasLimit": "21000", - "maxFeePerGas": undefined, - "maxPriorityFeePerGas": undefined, - "to": "0xbridgeContract", - "value": "0x0", - }, - Object { - "actionId": "1234567890.456", "networkClientId": "arbitrum", "origin": "metamask", "requireApproval": false, - "type": "swap", + "transactions": Array [ + Object { + "params": Object { + "data": "0xdata", + "from": "0xaccount1", + "gas": "0x5208", + "maxFeePerGas": "0x0", + "maxPriorityFeePerGas": "0x0", + "to": "0xbridgeContract", + "value": "0x0", + }, + "type": "swap", + }, + ], }, ], ] @@ -2832,9 +1845,6 @@ Array [ Array [ "GasFeeController:getState", ], - Array [ - "TransactionController:getState", - ], Array [ "AccountsController:getSelectedMultichainAccount", ], @@ -3001,6 +2011,7 @@ exports[`BridgeStatusController submitTx: Solana bridge should successfully subm Object { "account": "0x123...", "approvalTxId": undefined, + "batchId": undefined, "estimatedProcessingTimeInSeconds": 300, "hasApprovalTx": false, "initialDestAssetBalance": undefined, @@ -3332,6 +2343,7 @@ exports[`BridgeStatusController submitTx: Solana swap should successfully submit Object { "account": "0x123...", "approvalTxId": undefined, + "batchId": undefined, "estimatedProcessingTimeInSeconds": 300, "hasApprovalTx": false, "initialDestAssetBalance": undefined, diff --git a/packages/bridge-status-controller/src/bridge-status-controller.test.ts b/packages/bridge-status-controller/src/bridge-status-controller.test.ts index 246a5cc771d..38886d6550a 100644 --- a/packages/bridge-status-controller/src/bridge-status-controller.test.ts +++ b/packages/bridge-status-controller/src/bridge-status-controller.test.ts @@ -1,4 +1,3 @@ -/* eslint-disable jest/no-conditional-in-test */ /* eslint-disable jest/no-restricted-matchers */ import type { AccountsControllerActions } from '@metamask/accounts-controller'; import { Messenger } from '@metamask/base-controller'; @@ -16,7 +15,6 @@ import { } from '@metamask/bridge-controller'; import { ChainId } from '@metamask/bridge-controller'; import { ActionTypes, FeeType } from '@metamask/bridge-controller'; -import { EthAccountType } from '@metamask/keyring-api'; import { TransactionType, TransactionStatus, @@ -360,12 +358,14 @@ const MockTxHistory = { }), getPending: ({ txMetaId = 'bridgeTxMetaId1', + batchId = undefined, srcTxHash = '0xsrcTxHash1', account = '0xaccount1', srcChainId = 42161, destChainId = 10, } = {}): Record => ({ [txMetaId]: { + batchId, txMetaId, quote: getMockQuote({ srcChainId, destChainId }), startTime: 1729964825189, @@ -458,6 +458,7 @@ const MockTxHistory = { }), getComplete: ({ txMetaId = 'bridgeTxMetaId1', + batchId = undefined, srcTxHash = '0xsrcTxHash1', account = '0xaccount1', srcChainId = 42161, @@ -465,6 +466,7 @@ const MockTxHistory = { } = {}): Record => ({ [txMetaId]: { txMetaId, + batchId, quote: getMockQuote({ srcChainId, destChainId }), startTime: 1729964825189, completionTime: 1736277625746, @@ -553,10 +555,12 @@ const executePollingWithPendingStatus = async () => { messenger: getMessengerMock(), clientId: BridgeClientId.EXTENSION, fetchFn: jest.fn(), - addTransactionFn: jest.fn(), estimateGasFeeFn: jest.fn(), - addUserOperationFromTransactionFn: jest.fn(), + addTransactionBatchFn: jest.fn(), config: {}, + state: { + txHistory: {}, + }, }); const startPollingSpy = jest.spyOn(bridgeStatusController, 'startPolling'); @@ -590,9 +594,8 @@ const mockSelectedAccount = { }, }; -const addTransactionFn = jest.fn(); const estimateGasFeeFn = jest.fn(); -const addUserOperationFromTransactionFn = jest.fn(); +const addTransactionBatchFn = jest.fn(); const getController = (call: jest.Mock, traceFn?: jest.Mock) => { const controller = new BridgeStatusController({ @@ -605,9 +608,8 @@ const getController = (call: jest.Mock, traceFn?: jest.Mock) => { } as never, clientId: BridgeClientId.EXTENSION, fetchFn: mockFetchFn, - addTransactionFn, estimateGasFeeFn, - addUserOperationFromTransactionFn, + addTransactionBatchFn, traceFn, }); @@ -631,9 +633,8 @@ describe('BridgeStatusController', () => { messenger: getMessengerMock(), clientId: BridgeClientId.EXTENSION, fetchFn: jest.fn(), - addTransactionFn: jest.fn(), estimateGasFeeFn: jest.fn(), - addUserOperationFromTransactionFn: jest.fn(), + addTransactionBatchFn: jest.fn(), }); expect(bridgeStatusController.state).toStrictEqual(EMPTY_INIT_STATE); expect(mockMessengerSubscribe.mock.calls).toMatchSnapshot(); @@ -644,9 +645,9 @@ describe('BridgeStatusController', () => { messenger: getMessengerMock(), clientId: BridgeClientId.EXTENSION, fetchFn: jest.fn(), - addTransactionFn: jest.fn(), + estimateGasFeeFn: jest.fn(), - addUserOperationFromTransactionFn: jest.fn(), + addTransactionBatchFn: jest.fn(), state: { txHistory: MockTxHistory.getPending(), }, @@ -681,8 +682,8 @@ describe('BridgeStatusController', () => { }, clientId: BridgeClientId.EXTENSION, fetchFn: jest.fn(), - addTransactionFn: jest.fn(), estimateGasFeeFn: jest.fn(), + addTransactionBatchFn: jest.fn(), }); jest.advanceTimersByTime(10000); await flushPromises(); @@ -703,9 +704,12 @@ describe('BridgeStatusController', () => { messenger: getMessengerMock(), clientId: BridgeClientId.EXTENSION, fetchFn: jest.fn(), - addTransactionFn: jest.fn(), + estimateGasFeeFn: jest.fn(), - addUserOperationFromTransactionFn: jest.fn(), + addTransactionBatchFn: jest.fn(), + state: { + txHistory: MockTxHistory.getPending(), + }, }); // Execution @@ -741,9 +745,9 @@ describe('BridgeStatusController', () => { messenger: messengerMock, clientId: BridgeClientId.EXTENSION, fetchFn: jest.fn(), - addTransactionFn: jest.fn(), + estimateGasFeeFn: jest.fn(), - addUserOperationFromTransactionFn: jest.fn(), + addTransactionBatchFn: jest.fn(), }); const fetchBridgeTxStatusSpy = jest.spyOn( bridgeStatusUtils, @@ -822,9 +826,9 @@ describe('BridgeStatusController', () => { messenger: messengerMock, clientId: BridgeClientId.EXTENSION, fetchFn: jest.fn(), - addTransactionFn: jest.fn(), + estimateGasFeeFn: jest.fn(), - addUserOperationFromTransactionFn: jest.fn(), + addTransactionBatchFn: jest.fn(), }); // Start polling with args that have no srcTxHash @@ -861,9 +865,8 @@ describe('BridgeStatusController', () => { messenger: messengerMock, clientId: BridgeClientId.EXTENSION, fetchFn: jest.fn(), - addTransactionFn: jest.fn(), estimateGasFeeFn: jest.fn(), - addUserOperationFromTransactionFn: jest.fn(), + addTransactionBatchFn: jest.fn(), }); const fetchBridgeTxStatusSpy = jest @@ -902,9 +905,9 @@ describe('BridgeStatusController', () => { messenger: messengerMock, clientId: BridgeClientId.EXTENSION, fetchFn: jest.fn(), - addTransactionFn: jest.fn(), + estimateGasFeeFn: jest.fn(), - addUserOperationFromTransactionFn: jest.fn(), + addTransactionBatchFn: jest.fn(), }); // Execution @@ -965,9 +968,8 @@ describe('BridgeStatusController', () => { messenger: messengerMock, clientId: BridgeClientId.EXTENSION, fetchFn: jest.fn(), - addTransactionFn: jest.fn(), estimateGasFeeFn: jest.fn(), - addUserOperationFromTransactionFn: jest.fn(), + addTransactionBatchFn: jest.fn(), }); // Start polling with no srcTxHash @@ -1053,9 +1055,9 @@ describe('BridgeStatusController', () => { messenger: messengerMock, clientId: BridgeClientId.EXTENSION, fetchFn: jest.fn(), - addTransactionFn: jest.fn(), + estimateGasFeeFn: jest.fn(), - addUserOperationFromTransactionFn: jest.fn(), + addTransactionBatchFn: jest.fn(), }); const fetchBridgeTxStatusSpy = jest .spyOn(bridgeStatusUtils, 'fetchBridgeTxStatus') @@ -1140,9 +1142,9 @@ describe('BridgeStatusController', () => { messenger: messengerMock, clientId: BridgeClientId.EXTENSION, fetchFn: jest.fn(), - addTransactionFn: jest.fn(), + estimateGasFeeFn: jest.fn(), - addUserOperationFromTransactionFn: jest.fn(), + addTransactionBatchFn: jest.fn(), }); const fetchBridgeTxStatusSpy = jest .spyOn(bridgeStatusUtils, 'fetchBridgeTxStatus') @@ -1241,9 +1243,9 @@ describe('BridgeStatusController', () => { messenger: messengerMock, clientId: BridgeClientId.EXTENSION, fetchFn: jest.fn(), - addTransactionFn: jest.fn(), + estimateGasFeeFn: jest.fn(), - addUserOperationFromTransactionFn: jest.fn(), + addTransactionBatchFn: jest.fn(), }); const fetchBridgeTxStatusSpy = jest .spyOn(bridgeStatusUtils, 'fetchBridgeTxStatus') @@ -1476,7 +1478,7 @@ describe('BridgeStatusController', () => { expect( startPollingForBridgeTxStatusSpy.mock.lastCall[0], ).toMatchSnapshot(); - expect(controller.state.txHistory[result.id]).toMatchSnapshot(); + expect(controller.state.txHistory[result.id as never]).toMatchSnapshot(); }); it('should throw error when snap ID is missing', async () => { @@ -1714,7 +1716,7 @@ describe('BridgeStatusController', () => { expect(mockMessengerCall.mock.calls).toMatchSnapshot(); expect(result).toMatchSnapshot(); - expect(controller.state.txHistory[result.id]).toMatchSnapshot(); + expect(controller.state.txHistory[result.id as never]).toMatchSnapshot(); expect(startPollingForBridgeTxStatusSpy).toHaveBeenCalledTimes(0); }); @@ -1880,12 +1882,33 @@ describe('BridgeStatusController', () => { gasFeeEstimates: { estimatedBaseFee: '0x1234' }, }); estimateGasFeeFn.mockResolvedValueOnce(mockEstimateGasFeeResult); - addTransactionFn.mockResolvedValueOnce({ - transactionMeta: mockApprovalTxMeta, - result: Promise.resolve('0xapprovalTxHash'), + mockMessengerCall.mockReturnValueOnce({ + gasFeeEstimates: { estimatedBaseFee: '0x1234' }, + }); + estimateGasFeeFn.mockResolvedValueOnce(mockEstimateGasFeeResult); + addTransactionBatchFn.mockResolvedValueOnce({ + batchId: '0xapprovalTxHash', }); + mockMessengerCall.mockReturnValueOnce(mockSelectedAccount); + }; + + const setup2ApprovalMocks = () => { + mockMessengerCall.mockReturnValueOnce(mockSelectedAccount); + mockMessengerCall.mockReturnValueOnce('arbitrum-client-id'); mockMessengerCall.mockReturnValueOnce({ - transactions: [mockApprovalTxMeta], + gasFeeEstimates: { estimatedBaseFee: '0x1234' }, + }); + estimateGasFeeFn.mockResolvedValueOnce(mockEstimateGasFeeResult); + mockMessengerCall.mockReturnValueOnce({ + gasFeeEstimates: { estimatedBaseFee: '0x1234' }, + }); + estimateGasFeeFn.mockResolvedValueOnce(mockEstimateGasFeeResult); + mockMessengerCall.mockReturnValueOnce({ + gasFeeEstimates: { estimatedBaseFee: '0x1234' }, + }); + estimateGasFeeFn.mockResolvedValueOnce(mockEstimateGasFeeResult); + addTransactionBatchFn.mockResolvedValueOnce({ + batchId: '0xapprovalTxHash', }); }; @@ -1896,12 +1919,8 @@ describe('BridgeStatusController', () => { gasFeeEstimates: { estimatedBaseFee: '0x1234' }, }); estimateGasFeeFn.mockResolvedValueOnce(mockEstimateGasFeeResult); - addTransactionFn.mockResolvedValueOnce({ - transactionMeta: mockEvmTxMeta, - result: Promise.resolve('0xevmTxHash'), - }); - mockMessengerCall.mockReturnValueOnce({ - transactions: [mockEvmTxMeta], + addTransactionBatchFn.mockResolvedValueOnce({ + batchId: '0xevmTxHash', }); mockMessengerCall.mockReturnValueOnce(mockSelectedAccount); @@ -1909,7 +1928,6 @@ describe('BridgeStatusController', () => { it('should successfully submit an EVM bridge transaction with approval', async () => { setupApprovalMocks(); - setupBridgeMocks(); const { controller, startPollingForBridgeTxStatusSpy } = getController(mockMessengerCall); @@ -1918,10 +1936,11 @@ describe('BridgeStatusController', () => { expect(result).toMatchSnapshot(); expect(startPollingForBridgeTxStatusSpy).toHaveBeenCalledTimes(0); - expect(controller.state.txHistory[result.id]).toMatchSnapshot(); - expect(addTransactionFn.mock.calls).toMatchSnapshot(); + expect( + controller.state.txHistory[result.batchId as never], + ).toMatchSnapshot(); + expect(addTransactionBatchFn.mock.calls).toMatchSnapshot(); expect(mockMessengerCall.mock.calls).toMatchSnapshot(); - expect(addUserOperationFromTransactionFn).not.toHaveBeenCalled(); }); it('should successfully submit an EVM bridge transaction with no approval', async () => { @@ -1953,11 +1972,12 @@ describe('BridgeStatusController', () => { expect(result).toMatchSnapshot(); expect(startPollingForBridgeTxStatusSpy).toHaveBeenCalledTimes(0); - expect(controller.state.txHistory[result.id]).toMatchSnapshot(); + expect( + controller.state.txHistory[result.batchId as never], + ).toMatchSnapshot(); expect(estimateGasFeeFn.mock.calls).toMatchSnapshot(); - expect(addTransactionFn.mock.calls).toMatchSnapshot(); + expect(addTransactionBatchFn.mock.calls).toMatchSnapshot(); expect(mockMessengerCall.mock.calls).toMatchSnapshot(); - expect(addUserOperationFromTransactionFn).not.toHaveBeenCalled(); }); it('should handle smart transactions', async () => { @@ -1971,49 +1991,12 @@ describe('BridgeStatusController', () => { expect(result).toMatchSnapshot(); expect(startPollingForBridgeTxStatusSpy).toHaveBeenCalledTimes(0); - expect(controller.state.txHistory[result.id]).toMatchSnapshot(); - expect(estimateGasFeeFn.mock.calls).toMatchSnapshot(); - expect(addTransactionFn.mock.calls).toMatchSnapshot(); - expect(mockMessengerCall.mock.calls).toMatchSnapshot(); - expect(addUserOperationFromTransactionFn).not.toHaveBeenCalled(); - }); - - it('should handle smart accounts (4337)', async () => { - mockMessengerCall.mockReturnValueOnce({ - ...mockSelectedAccount, - type: EthAccountType.Erc4337, - }); - mockMessengerCall.mockReturnValueOnce('arbitrum'); - estimateGasFeeFn.mockResolvedValueOnce(mockEstimateGasFeeResult); - mockMessengerCall.mockReturnValueOnce({ - gasFeeEstimates: { estimatedBaseFee: '0x1234' }, - }); - addUserOperationFromTransactionFn.mockResolvedValueOnce({ - id: 'user-op-id', - transactionHash: Promise.resolve('0xevmTxHash'), - hash: Promise.resolve('0xevmTxHash'), - }); - mockMessengerCall.mockReturnValueOnce({ - transactions: [mockEvmTxMeta], - }); - mockMessengerCall.mockReturnValueOnce({ - ...mockSelectedAccount, - type: EthAccountType.Erc4337, - }); - - const { controller, startPollingForBridgeTxStatusSpy } = - getController(mockMessengerCall); - const { approval, ...quoteWithoutApproval } = mockEvmQuoteResponse; - const result = await controller.submitTx(quoteWithoutApproval, false); - controller.stopAllPolling(); - - expect(result).toMatchSnapshot(); - expect(startPollingForBridgeTxStatusSpy).toHaveBeenCalledTimes(0); - expect(controller.state.txHistory[result.id]).toMatchSnapshot(); + expect( + controller.state.txHistory[result.batchId as never], + ).toMatchSnapshot(); expect(estimateGasFeeFn.mock.calls).toMatchSnapshot(); - expect(addTransactionFn).not.toHaveBeenCalled(); + expect(addTransactionBatchFn.mock.calls).toMatchSnapshot(); expect(mockMessengerCall.mock.calls).toMatchSnapshot(); - expect(addUserOperationFromTransactionFn.mock.calls).toMatchSnapshot(); }); it('should throw an error if account is not found', async () => { @@ -2026,27 +2009,18 @@ describe('BridgeStatusController', () => { await expect( controller.submitTx(quoteWithoutApproval, false), ).rejects.toThrow( - 'Failed to submit cross-chain swap transaction: unknown account in trade data', + 'Failed to submit cross-chain swap batch transaction: unknown account in trade data', ); controller.stopAllPolling(); expect(startPollingForBridgeTxStatusSpy).toHaveBeenCalledTimes(0); - expect(addTransactionFn).not.toHaveBeenCalled(); - expect(addUserOperationFromTransactionFn).not.toHaveBeenCalled(); + expect(addTransactionBatchFn).not.toHaveBeenCalled(); }); it('should reset USDT allowance', async () => { mockIsEthUsdt.mockReturnValueOnce(true); - // USDT approval reset - mockMessengerCall.mockReturnValueOnce('1'); - setupApprovalMocks(); - - // Approval tx - setupApprovalMocks(); - - // Bridge transaction - setupBridgeMocks(); + setup2ApprovalMocks(); const { controller, startPollingForBridgeTxStatusSpy } = getController(mockMessengerCall); @@ -2055,100 +2029,12 @@ describe('BridgeStatusController', () => { expect(result).toMatchSnapshot(); expect(startPollingForBridgeTxStatusSpy).toHaveBeenCalledTimes(0); - expect(controller.state.txHistory[result.id]).toMatchSnapshot(); + expect( + controller.state.txHistory[result.batchId as never], + ).toMatchSnapshot(); expect(estimateGasFeeFn.mock.calls).toMatchSnapshot(); - expect(addTransactionFn.mock.calls).toMatchSnapshot(); - expect(mockMessengerCall.mock.calls).toMatchSnapshot(); - }); - - it('should throw an error if approval tx fails', async () => { - mockMessengerCall.mockReturnValueOnce(mockSelectedAccount); - mockMessengerCall.mockReturnValueOnce('arbitrum-client-id'); - mockMessengerCall.mockReturnValueOnce({ - gasFeeEstimates: { estimatedBaseFee: '0x1234' }, - }); - estimateGasFeeFn.mockResolvedValueOnce(mockEstimateGasFeeResult); - addTransactionFn.mockRejectedValueOnce(new Error('Approval tx failed')); - - const { controller, startPollingForBridgeTxStatusSpy } = - getController(mockMessengerCall); - - await expect( - controller.submitTx(mockEvmQuoteResponse, false), - ).rejects.toThrow('Approval tx failed'); - - expect(startPollingForBridgeTxStatusSpy).toHaveBeenCalledTimes(0); - expect(addTransactionFn.mock.calls).toMatchSnapshot(); + expect(addTransactionBatchFn.mock.calls).toMatchSnapshot(); expect(mockMessengerCall.mock.calls).toMatchSnapshot(); - expect(addUserOperationFromTransactionFn).not.toHaveBeenCalled(); - }); - - it('should throw an error if approval tx meta does not exist', async () => { - mockMessengerCall.mockReturnValueOnce(mockSelectedAccount); - mockMessengerCall.mockReturnValueOnce('arbitrum-client-id'); - mockMessengerCall.mockReturnValueOnce({ - gasFeeEstimates: { estimatedBaseFee: '0x1234' }, - }); - estimateGasFeeFn.mockResolvedValueOnce(mockEstimateGasFeeResult); - addTransactionFn.mockResolvedValueOnce({ - transactionMeta: undefined, - result: new Promise((resolve) => resolve('0xevmTxHash')), - }); - mockMessengerCall.mockReturnValueOnce({ - transactions: [], - }); - - setupBridgeMocks(); - const { controller, startPollingForBridgeTxStatusSpy } = - getController(mockMessengerCall); - - await expect( - controller.submitTx(mockEvmQuoteResponse, false), - ).rejects.toThrow( - 'Failed to submit cross-chain swap tx: txMeta for txHash was not found', - ); - - expect(startPollingForBridgeTxStatusSpy).toHaveBeenCalledTimes(0); - expect(addTransactionFn.mock.calls).toMatchSnapshot(); - expect(mockMessengerCall.mock.calls).toMatchSnapshot(); - expect(addUserOperationFromTransactionFn).not.toHaveBeenCalled(); - }); - - it('should delay after submitting linea approval', async () => { - const handleLineaDelaySpy = jest - .spyOn(transactionUtils, 'handleLineaDelay') - .mockResolvedValueOnce(); - const mockTraceFn = jest - .fn() - .mockImplementation((_p, callback) => callback()); - - setupApprovalMocks(); - setupBridgeMocks(); - - const { controller, startPollingForBridgeTxStatusSpy } = getController( - mockMessengerCall, - mockTraceFn, - ); - - const lineaQuoteResponse = { - ...mockEvmQuoteResponse, - quote: { ...mockEvmQuoteResponse.quote, srcChainId: 59144 }, - trade: { - ...(mockEvmQuoteResponse.trade as TxData), - gasLimit: undefined, - } as never, - }; - - const result = await controller.submitTx(lineaQuoteResponse, false); - controller.stopAllPolling(); - - expect(mockTraceFn).toHaveBeenCalledTimes(2); - expect(handleLineaDelaySpy).toHaveBeenCalledTimes(1); - expect(result).toMatchSnapshot(); - expect(startPollingForBridgeTxStatusSpy).toHaveBeenCalledTimes(0); - expect(controller.state.txHistory[result.id]).toMatchSnapshot(); - expect(mockMessengerCall.mock.calls).toMatchSnapshot(); - expect(mockTraceFn.mock.calls).toMatchSnapshot(); }); }); @@ -2250,12 +2136,12 @@ describe('BridgeStatusController', () => { gasFeeEstimates: { estimatedBaseFee: '0x1234' }, }); estimateGasFeeFn.mockResolvedValueOnce(mockEstimateGasFeeResult); - addTransactionFn.mockResolvedValueOnce({ - transactionMeta: mockApprovalTxMeta, - result: Promise.resolve('0xapprovalTxHash'), - }); mockMessengerCall.mockReturnValueOnce({ - transactions: [mockApprovalTxMeta], + gasFeeEstimates: { estimatedBaseFee: '0x1234' }, + }); + estimateGasFeeFn.mockResolvedValueOnce(mockEstimateGasFeeResult); + addTransactionBatchFn.mockResolvedValueOnce({ + batchId: '0xapprovalTxHash', }); }; @@ -2266,12 +2152,8 @@ describe('BridgeStatusController', () => { gasFeeEstimates: { estimatedBaseFee: '0x1234' }, }); estimateGasFeeFn.mockResolvedValueOnce(mockEstimateGasFeeResult); - addTransactionFn.mockResolvedValueOnce({ - transactionMeta: mockEvmTxMeta, - result: Promise.resolve('0xevmTxHash'), - }); - mockMessengerCall.mockReturnValueOnce({ - transactions: [mockEvmTxMeta], + addTransactionBatchFn.mockResolvedValueOnce({ + batchId: '0xevmTxHash', }); mockMessengerCall.mockReturnValueOnce(mockSelectedAccount); @@ -2279,7 +2161,6 @@ describe('BridgeStatusController', () => { it('should successfully submit an EVM swap transaction with approval', async () => { setupApprovalMocks(); - setupBridgeMocks(); const { controller, startPollingForBridgeTxStatusSpy } = getController(mockMessengerCall); @@ -2288,10 +2169,11 @@ describe('BridgeStatusController', () => { expect(result).toMatchSnapshot(); expect(startPollingForBridgeTxStatusSpy).toHaveBeenCalledTimes(0); - expect(controller.state.txHistory[result.id]).toMatchSnapshot(); - expect(addTransactionFn.mock.calls).toMatchSnapshot(); + expect( + controller.state.txHistory[result.batchId as never], + ).toMatchSnapshot(); + expect(addTransactionBatchFn.mock.calls).toMatchSnapshot(); expect(mockMessengerCall.mock.calls).toMatchSnapshot(); - expect(addUserOperationFromTransactionFn).not.toHaveBeenCalled(); }); it('should successfully submit an EVM swap transaction with no approval', async () => { @@ -2323,11 +2205,12 @@ describe('BridgeStatusController', () => { expect(result).toMatchSnapshot(); expect(startPollingForBridgeTxStatusSpy).toHaveBeenCalledTimes(0); - expect(controller.state.txHistory[result.id]).toMatchSnapshot(); + expect( + controller.state.txHistory[result.batchId as never], + ).toMatchSnapshot(); expect(estimateGasFeeFn.mock.calls).toMatchSnapshot(); - expect(addTransactionFn.mock.calls).toMatchSnapshot(); + expect(addTransactionBatchFn.mock.calls).toMatchSnapshot(); expect(mockMessengerCall.mock.calls).toMatchSnapshot(); - expect(addUserOperationFromTransactionFn).not.toHaveBeenCalled(); }); it('should handle smart transactions', async () => { @@ -2337,9 +2220,8 @@ describe('BridgeStatusController', () => { gasFeeEstimates: { estimatedBaseFee: '0x1234' }, }); estimateGasFeeFn.mockResolvedValueOnce(mockEstimateGasFeeResult); - addTransactionFn.mockResolvedValueOnce({ - transactionMeta: mockEvmTxMeta, - result: Promise.resolve('0xevmTxHash'), + addTransactionBatchFn.mockResolvedValueOnce({ + batchId: '0xevmTxHash', }); // mockMessengerCall.mockReturnValueOnce(mockSelectedAccount); @@ -2351,46 +2233,12 @@ describe('BridgeStatusController', () => { expect(result).toMatchSnapshot(); expect(startPollingForBridgeTxStatusSpy).toHaveBeenCalledTimes(0); - expect(controller.state.txHistory[result.id]).toMatchSnapshot(); - expect(estimateGasFeeFn.mock.calls).toMatchSnapshot(); - expect(addTransactionFn.mock.calls).toMatchSnapshot(); - expect(mockMessengerCall.mock.calls).toMatchSnapshot(); - expect(addUserOperationFromTransactionFn).not.toHaveBeenCalled(); - }); - - it('should handle smart accounts (4337)', async () => { - mockMessengerCall.mockReturnValueOnce({ - ...mockSelectedAccount, - type: EthAccountType.Erc4337, - }); - mockMessengerCall.mockReturnValueOnce('arbitrum'); - estimateGasFeeFn.mockResolvedValueOnce(mockEstimateGasFeeResult); - mockMessengerCall.mockReturnValueOnce({ - gasFeeEstimates: { estimatedBaseFee: '0x1234' }, - }); - addUserOperationFromTransactionFn.mockResolvedValueOnce({ - id: 'user-op-id', - transactionHash: Promise.resolve('0xevmTxHash'), - hash: Promise.resolve('0xevmTxHash'), - }); - mockMessengerCall.mockReturnValueOnce({ - transactions: [mockEvmTxMeta], - }); - estimateGasFeeFn.mockResolvedValueOnce(mockEstimateGasFeeResult); - - const { controller, startPollingForBridgeTxStatusSpy } = - getController(mockMessengerCall); - const { approval, ...quoteWithoutApproval } = mockEvmQuoteResponse; - const result = await controller.submitTx(quoteWithoutApproval, false); - controller.stopAllPolling(); - - expect(result).toMatchSnapshot(); - expect(startPollingForBridgeTxStatusSpy).toHaveBeenCalledTimes(0); - expect(controller.state.txHistory[result.id]).toMatchSnapshot(); + expect( + controller.state.txHistory[result.batchId as never], + ).toMatchSnapshot(); expect(estimateGasFeeFn.mock.calls).toMatchSnapshot(); - expect(addTransactionFn).not.toHaveBeenCalled(); + expect(addTransactionBatchFn.mock.calls).toMatchSnapshot(); expect(mockMessengerCall.mock.calls).toMatchSnapshot(); - expect(addUserOperationFromTransactionFn.mock.calls).toMatchSnapshot(); }); }); @@ -2455,9 +2303,9 @@ describe('BridgeStatusController', () => { messenger: mockBridgeStatusMessenger, clientId: BridgeClientId.EXTENSION, fetchFn: jest.fn(), - addTransactionFn: jest.fn(), + estimateGasFeeFn: jest.fn(), - addUserOperationFromTransactionFn: jest.fn(), + addTransactionBatchFn: jest.fn(), state: { txHistory: { ...MockTxHistory.getPending(), diff --git a/packages/bridge-status-controller/src/bridge-status-controller.ts b/packages/bridge-status-controller/src/bridge-status-controller.ts index e2c7f73bfe3..eef492ed4de 100644 --- a/packages/bridge-status-controller/src/bridge-status-controller.ts +++ b/packages/bridge-status-controller/src/bridge-status-controller.ts @@ -6,9 +6,6 @@ import type { QuoteResponse, } from '@metamask/bridge-controller'; import { - formatChainIdToHex, - getEthUsdtResetData, - isEthUsdt, isSolanaChainId, StatusTypes, UnifiedSwapBridgeEventName, @@ -19,21 +16,15 @@ import { isHardwareWallet, } from '@metamask/bridge-controller'; import type { TraceCallback } from '@metamask/controller-utils'; -import { toHex } from '@metamask/controller-utils'; -import { EthAccountType, SolScope } from '@metamask/keyring-api'; +import { SolScope } from '@metamask/keyring-api'; import { StaticIntervalPollingController } from '@metamask/polling-controller'; -import type { - TransactionController, - TransactionParams, -} from '@metamask/transaction-controller'; +import type { TransactionController } from '@metamask/transaction-controller'; import { TransactionStatus, TransactionType, type TransactionMeta, } from '@metamask/transaction-controller'; -import type { UserOperationController } from '@metamask/user-operation-controller'; import { numberToHex, type Hex } from '@metamask/utils'; -import { BigNumber } from 'bignumber.js'; import { BRIDGE_PROD_API_BASE_URL, @@ -55,7 +46,6 @@ import { fetchBridgeTxStatus, getStatusRequestWithSrcTxHash, } from './utils/bridge-status'; -import { getTxGasEstimates } from './utils/gas'; import { getFinalizedTxProperties, getPriceImpactFromQuote, @@ -67,14 +57,13 @@ import { getTxStatusesFromHistory, } from './utils/metrics'; import { + getAddTransactionBatchParams, getClientRequest, getKeyringRequest, getStatusRequestParams, - getTxMetaFields, - handleLineaDelay, + getUSDTAllowanceResetTx, handleSolanaTxResponse, } from './utils/transaction'; -import { generateActionId } from './utils/transaction'; const metadata: StateMetadata = { // We want to persist the bridge status state so that we can show the proper data for the Activity list @@ -107,12 +96,10 @@ export class BridgeStatusController extends StaticIntervalPollingController; clientId: BridgeClientId; fetchFn: FetchFunction; - addTransactionFn: typeof TransactionController.prototype.addTransaction; estimateGasFeeFn: typeof TransactionController.prototype.estimateGasFee; - addUserOperationFromTransactionFn?: typeof UserOperationController.prototype.addUserOperationFromTransaction; + addTransactionBatchFn: typeof TransactionController.prototype.addTransactionBatch; config?: { customBridgeApiBaseUrl?: string; }; @@ -151,8 +136,7 @@ export class BridgeStatusController extends StaticIntervalPollingController { + console.error('=====transactionFailed', transactionMeta); const { type, status, id } = transactionMeta; if ( type && @@ -192,6 +177,7 @@ export class BridgeStatusController extends StaticIntervalPollingController { if (bridgeStatusState.txHistory[id]) { @@ -218,13 +204,21 @@ export class BridgeStatusController extends StaticIntervalPollingController { const { type, id, chainId } = transactionMeta; + this.#updateTxHistoryWithTxMeta(transactionMeta); + + console.error('=====transactionConfirmed', transactionMeta); + if (type === TransactionType.swap) { + // TODO update status to COMPLETED and completion time this.#trackUnifiedSwapBridgeEvent( UnifiedSwapBridgeEventName.Completed, id, getEVMTxPropertiesFromTransactionMeta(transactionMeta), ); } + // Start polling for tx status when bridge src tx is confirmed + // Polling starts for solana bridges right away so skip here + // Also, this event is only published for EVM transactions if (type === TransactionType.bridge && !isSolanaChainId(chainId)) { this.#startPollingForTxId(id); } @@ -237,6 +231,67 @@ export class BridgeStatusController extends StaticIntervalPollingController { + const { id, batchId, type, status } = transactionMeta; + + // TODO if txMeta is failed, stop polling and update item + const txHistoryItemById = id ? this.state.txHistory[id] : undefined; + const txHistoryItemByBatchId = batchId + ? this.state.txHistory[batchId] + : undefined; + if ( + status === TransactionStatus.failed && + (txHistoryItemById || txHistoryItemByBatchId) + ) { + const pollingToken = this.#pollingTokensByTxMetaId[id]; + if (pollingToken) { + this.stopPollingByPollingToken(pollingToken); + } + if (txHistoryItemById) { + this.update((state) => { + state.txHistory[id].status = { + ...txHistoryItemById.status, + status: StatusTypes.FAILED, + }; + }); + } + if (txHistoryItemByBatchId && batchId) { + this.update((state) => { + state.txHistory[batchId].status = { + ...txHistoryItemByBatchId.status, + status: StatusTypes.FAILED, + }; + }); + } + } + // if tx batchId is in history, update the history item with the new transactionMeta + if (batchId && txHistoryItemByBatchId) { + this.update((state) => { + state.txHistory[id] = state.txHistory[batchId]; + state.txHistory[id].txMetaId = id; + delete state.txHistory[batchId]; + }); + } + if ( + txHistoryItemById && + batchId === txHistoryItemById.batchId && + type && + [ + TransactionType.swapApproval, + TransactionType.bridgeApproval, + TransactionType.tokenMethodApprove, + ].includes(type) + ) { + // TODO if tx is approval, append to either batchId or id as approvalTxId + this.update((state) => { + state.txHistory[id].approvalTxId = id; + }); + } + }; + resetState = () => { this.update((state) => { state.txHistory = DEFAULT_BRIDGE_STATUS_CONTROLLER_STATE.txHistory; @@ -282,6 +337,9 @@ export class BridgeStatusController extends StaticIntervalPollingController { // Check if we are already polling this tx, if so, skip restarting polling for that const srcTxMetaId = historyItem.txMetaId; + if (!srcTxMetaId) { + return false; + } const pollingToken = this.#pollingTokensByTxMetaId[srcTxMetaId]; return !pollingToken; }) @@ -297,8 +355,6 @@ export class BridgeStatusController extends StaticIntervalPollingController { 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.#startPollingForTxId(bridgeTxMetaId); }); }; @@ -317,11 +373,22 @@ export class BridgeStatusController extends StaticIntervalPollingController { - // Use the txMeta.id as the key so we can reference the txMeta in TransactionController - state.txHistory[bridgeTxMeta.id] = txHistoryItem; + // Use the txMeta.id or batchId as the key so we can reference the txMeta in TransactionController + state.txHistory[txHistoryKey] = txHistoryItem; }); }; - readonly #startPollingForTxId = (txId: string) => { + readonly #startPollingForTxId = (txId?: string) => { + if (!txId) { + return; + } // If we are already polling for this tx, stop polling for it before restarting const existingPollingToken = this.#pollingTokensByTxMetaId[txId]; if (existingPollingToken) { @@ -379,7 +449,7 @@ export class BridgeStatusController extends StaticIntervalPollingController>['result'] - | Awaited< - ReturnType - >['hash'], - ): Promise => { - const transactionHash = await hashPromise; - const finalTransactionMeta: TransactionMeta | undefined = - this.messagingSystem - .call('TransactionController:getState') - .transactions.find( - (tx: TransactionMeta) => tx.hash === transactionHash, - ); - if (!finalTransactionMeta) { - throw new Error( - 'Failed to submit cross-chain swap tx: txMeta for txHash was not found', - ); - } - return finalTransactionMeta; - }; - - readonly #handleApprovalTx = async ( - isBridgeTx: boolean, - quoteResponse: QuoteResponse & QuoteMetadata, - requireApproval = false, - ): Promise => { - const { approval } = quoteResponse; - - if (approval) { - const approveTx = async () => { - await this.#handleUSDTAllowanceReset(quoteResponse); - - const approvalTxMeta = await this.#handleEvmTransaction({ - transactionType: isBridgeTx - ? TransactionType.bridgeApproval - : TransactionType.swapApproval, - trade: approval, - quoteResponse, - requireApproval, - }); - - await handleLineaDelay(quoteResponse); - return approvalTxMeta; - }; - - return await this.#trace( - { - name: isBridgeTx - ? TraceName.BridgeTransactionApprovalCompleted - : TraceName.SwapTransactionApprovalCompleted, - data: { - srcChainId: formatChainIdToCaip(quoteResponse.quote.srcChainId), - stxEnabled: false, - }, - }, - approveTx, - ); - } - - return undefined; - }; - - readonly #handleEvmSmartTransaction = async ({ - isBridgeTx, - trade, - quoteResponse, - approvalTxId, - requireApproval = false, - }: { - isBridgeTx: boolean; - trade: TxData; - quoteResponse: Omit & QuoteMetadata; - approvalTxId?: string; - requireApproval?: boolean; - }) => { - return await this.#handleEvmTransaction({ - transactionType: isBridgeTx - ? TransactionType.bridge - : TransactionType.swap, - trade, - quoteResponse, - approvalTxId, - shouldWaitForHash: false, // Set to false to indicate we don't want to wait for hash - requireApproval, - }); - }; - /** - * Submits an EVM transaction to the TransactionController + * Submits EVM transactions to the TransactionController * - * @param params - The parameters for the transaction - * @param params.transactionType - The type of transaction to submit - * @param params.trade - The trade data to confirm - * @param params.quoteResponse - The quote response - * @param params.approvalTxId - The tx id of the approval tx - * @param params.shouldWaitForHash - Whether to wait for the hash of the transaction - * @param params.requireApproval - Whether to require approval for the transaction - * @returns The transaction meta + * @param args - The parameters for the transaction + * @param args.isBridgeTx - Whether the transaction is a bridge transaction + * @param args.trade - The trade data to confirm + * @param args.approval - The approval data to confirm + * @param args.resetApproval - The ethereum:USDT reset approval data to confirm + * @param args.quoteResponse - The quote response + * @param args.requireApproval - Whether to require approval for the transaction + * @returns A partial TransactionMeta that contains the batchId */ - readonly #handleEvmTransaction = async ({ - transactionType, - trade, - quoteResponse, - approvalTxId, - shouldWaitForHash = true, - requireApproval = false, - }: { - transactionType: TransactionType; - trade: TxData; - quoteResponse: Omit & QuoteMetadata; - approvalTxId?: string; - shouldWaitForHash?: boolean; - requireApproval?: boolean; - }): Promise => { - const actionId = generateActionId().toString(); - - const selectedAccount = this.messagingSystem.call( - 'AccountsController:getAccountByAddress', - trade.from, - ); - if (!selectedAccount) { - throw new Error( - 'Failed to submit cross-chain swap transaction: unknown account in trade data', - ); - } - const hexChainId = formatChainIdToHex(trade.chainId); - const networkClientId = this.messagingSystem.call( - 'NetworkController:findNetworkClientIdByChainId', - hexChainId, - ); - - const requestOptions = { - actionId, - networkClientId, - requireApproval, - type: transactionType, - origin: 'metamask', - }; - const transactionParams: Parameters< - TransactionController['addTransaction'] - >[0] = { - ...trade, - chainId: hexChainId, - gasLimit: trade.gasLimit?.toString(), - gas: trade.gasLimit?.toString(), - }; - const transactionParamsWithMaxGas: TransactionParams = { - ...transactionParams, - ...(await this.#calculateGasFees( - transactionParams, - networkClientId, - hexChainId, - )), - }; - - let result: - | Awaited>['result'] - | Awaited< - ReturnType - >['hash'] - | undefined; - let transactionMeta: TransactionMeta | undefined; - - const isSmartContractAccount = - selectedAccount.type === EthAccountType.Erc4337; - if (isSmartContractAccount && this.#addUserOperationFromTransactionFn) { - const smartAccountTxResult = - await this.#addUserOperationFromTransactionFn( - transactionParamsWithMaxGas, - requestOptions, - ); - result = smartAccountTxResult.transactionHash; - transactionMeta = { - ...requestOptions, - chainId: hexChainId, - txParams: transactionParamsWithMaxGas, - time: Date.now(), - id: smartAccountTxResult.id, - status: TransactionStatus.confirmed, - }; - } else { - const addTransactionResult = await this.#addTransactionFn( - transactionParamsWithMaxGas, - requestOptions, - ); - result = addTransactionResult.result; - transactionMeta = addTransactionResult.transactionMeta; - } - - if (shouldWaitForHash) { - return await this.#waitForHashAndReturnFinalTxMeta(result); - } - - return { - ...getTxMetaFields(quoteResponse, approvalTxId), - ...transactionMeta, - }; - }; - - readonly #handleUSDTAllowanceReset = async ( - quoteResponse: QuoteResponse & QuoteMetadata, - ) => { - const hexChainId = formatChainIdToHex(quoteResponse.quote.srcChainId); - if ( - quoteResponse.approval && - isEthUsdt(hexChainId, quoteResponse.quote.srcAsset.address) - ) { - const allowance = new BigNumber( - await this.messagingSystem.call( - 'BridgeController:getBridgeERC20Allowance', - quoteResponse.quote.srcAsset.address, - hexChainId, - ), - ); - const shouldResetApproval = - allowance.lt(quoteResponse.sentAmount.amount) && allowance.gt(0); - if (shouldResetApproval) { - await this.#handleEvmTransaction({ - transactionType: TransactionType.bridgeApproval, - trade: { ...quoteResponse.approval, data: getEthUsdtResetData() }, - quoteResponse, - }); - } - } - }; - - readonly #calculateGasFees = async ( - transactionParams: TransactionParams, - networkClientId: string, - chainId: Hex, + readonly #handleEvmTransactionBatch = async ( + args: Omit< + Parameters[0], + 'messagingSystem' | 'estimateGasFeeFn' + >, ) => { - const { gasFeeEstimates } = this.messagingSystem.call( - 'GasFeeController:getState', - ); - const { estimates: txGasFeeEstimates } = await this.#estimateGasFeeFn({ - transactionParams, - chainId, - networkClientId, - }); - const { maxFeePerGas, maxPriorityFeePerGas } = getTxGasEstimates({ - networkGasFeeEstimates: gasFeeEstimates, - txGasFeeEstimates, + const transactionParams = await getAddTransactionBatchParams({ + messagingSystem: this.messagingSystem, + estimateGasFeeFn: this.#estimateGasFeeFn, + ...args, }); - const maxGasLimit = toHex(transactionParams.gas ?? 0); - - return { - maxFeePerGas, - maxPriorityFeePerGas, - gas: maxGasLimit, - }; + return await this.#addTransactionBatchFn(transactionParams); }; /** @@ -866,7 +709,7 @@ export class BridgeStatusController extends StaticIntervalPollingController & QuoteMetadata, isStxEnabledOnClient: boolean, - ): Promise> => { + ): Promise & Partial> => { this.messagingSystem.call('BridgeController:stopPollingForQuotes'); // Before the tx is confirmed, its data is not available in txHistory @@ -886,13 +729,13 @@ export class BridgeStatusController extends StaticIntervalPollingController; - let approvalTime: number | undefined, approvalTxId: string | undefined; + let txMeta: Partial & Partial; const isBridgeTx = isCrossChain( quoteResponse.quote.srcChainId, quoteResponse.quote.destChainId, ); + const startTime = Date.now(); // Submit SOLANA tx if ( @@ -935,14 +778,6 @@ export class BridgeStatusController extends StaticIntervalPollingController - isStxEnabledOnClient - ? await this.#handleEvmSmartTransaction({ - isBridgeTx, - trade: quoteResponse.trade as TxData, - quoteResponse, - approvalTxId, - requireApproval, - }) - : await this.#handleEvmTransaction({ - transactionType: isBridgeTx - ? TransactionType.bridge - : TransactionType.swap, - trade: quoteResponse.trade as TxData, - quoteResponse, - approvalTxId, - requireApproval, - }), + async () => { + const { approval, trade } = quoteResponse as QuoteResponse; + const resetApproval = approval + ? await getUSDTAllowanceResetTx(this.messagingSystem, quoteResponse) + : undefined; + return await this.#handleEvmTransactionBatch({ + isBridgeTx, + trade, + approval, + resetApproval, + quoteResponse, + requireApproval, + }); + }, ); } try { // Add swap or bridge tx to history this.#addTxToHistory({ - bridgeTxMeta: txMeta, // Only the id field is used by the BridgeStatusController + bridgeTxMeta: txMeta, // Only the id and batchId fields are used by the BridgeStatusController statusRequest: { ...getStatusRequestParams(quoteResponse), srcTxHash: txMeta.hash, @@ -986,8 +817,7 @@ export class BridgeStatusController extends StaticIntervalPollingController; export type RefuelStatusResponse = object & StatusResponse; export type BridgeHistoryItem = { - txMetaId: string; // Need this to handle STX that might not have a txHash immediately + txMetaId?: string; // Need this to handle STX that might not have a txHash immediately + batchId?: string; // Need this to handle STX that might not have a txHash or txMetaId immediately quote: Quote; status: StatusResponse; startTime?: number; // timestamp in ms @@ -176,7 +177,7 @@ export type QuoteMetadataSerialized = { }; export type StartPollingForBridgeTxStatusArgs = { - bridgeTxMeta: TransactionMeta; + bridgeTxMeta: Partial; statusRequest: StatusRequest; quoteResponse: QuoteResponse & QuoteMetadata; startTime?: BridgeHistoryItem['startTime']; diff --git a/packages/bridge-status-controller/src/utils/gas.ts b/packages/bridge-status-controller/src/utils/gas.ts index f3e91def1e0..35a978f80e7 100644 --- a/packages/bridge-status-controller/src/utils/gas.ts +++ b/packages/bridge-status-controller/src/utils/gas.ts @@ -1,3 +1,5 @@ +import type { TxData } from '@metamask/bridge-controller'; +import { toHex } from '@metamask/controller-utils'; import type { GasFeeEstimates, GasFeeState, @@ -6,7 +8,9 @@ import type { FeeMarketGasFeeEstimates, TransactionController, } from '@metamask/transaction-controller'; +import type { Hex } from '@metamask/utils'; import { BigNumber } from 'bignumber.js'; +import type { BridgeStatusControllerMessenger } from 'src/types'; const getTransaction1559GasFeeEstimates = ( txGasFeeEstimates: FeeMarketGasFeeEstimates, @@ -50,3 +54,44 @@ export const getTxGasEstimates = ({ estimatedBaseFee, ); }; + +export const calculateGasFees = async ( + disable7702: boolean, + messagingSystem: BridgeStatusControllerMessenger, + estimateGasFeeFn: typeof TransactionController.prototype.estimateGasFee, + { chainId: _, gasLimit, ...trade }: TxData, + networkClientId: string, + chainId: Hex, + txFee?: { maxFeePerGas: string; maxPriorityFeePerGas: string }, +) => { + if (!disable7702) { + return {}; + } + if (txFee) { + return { ...txFee, gas: gasLimit?.toString() }; + } + const transactionParams = { + ...trade, + gas: gasLimit?.toString(), + data: trade.data as `0x${string}`, + to: trade.to as `0x${string}`, + value: trade.value as `0x${string}`, + }; + const { gasFeeEstimates } = messagingSystem.call('GasFeeController:getState'); + const { estimates: txGasFeeEstimates } = await estimateGasFeeFn({ + transactionParams, + chainId, + networkClientId, + }); + const { maxFeePerGas, maxPriorityFeePerGas } = getTxGasEstimates({ + networkGasFeeEstimates: gasFeeEstimates, + txGasFeeEstimates, + }); + const maxGasLimit = toHex(transactionParams.gas ?? 0); + + return { + maxFeePerGas, + maxPriorityFeePerGas, + gas: maxGasLimit, + }; +}; diff --git a/packages/bridge-status-controller/src/utils/transaction.ts b/packages/bridge-status-controller/src/utils/transaction.ts index 4a2b1a1f907..f1877198c8c 100644 --- a/packages/bridge-status-controller/src/utils/transaction.ts +++ b/packages/bridge-status-controller/src/utils/transaction.ts @@ -1,26 +1,62 @@ import type { AccountsControllerState } from '@metamask/accounts-controller'; -import type { TxData } from '@metamask/bridge-controller'; import { ChainId, + type TxData, formatChainIdToHex, + getEthUsdtResetData, isCrossChain, + isEthUsdt, type QuoteMetadata, type QuoteResponse, } from '@metamask/bridge-controller'; +import { toHex } from '@metamask/controller-utils'; import { SolScope } from '@metamask/keyring-api'; +import type { TransactionController } from '@metamask/transaction-controller'; import { TransactionStatus, TransactionType, + type BatchTransactionParams, type TransactionMeta, } from '@metamask/transaction-controller'; import { createProjectLogger } from '@metamask/utils'; +import { BigNumber } from 'bignumber.js'; import { v4 as uuid } from 'uuid'; +import { calculateGasFees } from './gas'; +import type { TransactionBatchSingleRequest } from '../../../transaction-controller/src/types'; import { LINEA_DELAY_MS } from '../constants'; -import type { SolanaTransactionMeta } from '../types'; +import type { + BridgeStatusControllerMessenger, + SolanaTransactionMeta, +} from '../types'; export const generateActionId = () => (Date.now() + Math.random()).toString(); +export const getUSDTAllowanceResetTx = async ( + messagingSystem: BridgeStatusControllerMessenger, + quoteResponse: QuoteResponse & QuoteMetadata, +) => { + const hexChainId = formatChainIdToHex(quoteResponse.quote.srcChainId); + if ( + quoteResponse.approval && + isEthUsdt(hexChainId, quoteResponse.quote.srcAsset.address) + ) { + const allowance = new BigNumber( + await messagingSystem.call( + 'BridgeController:getBridgeERC20Allowance', + quoteResponse.quote.srcAsset.address, + hexChainId, + ), + ); + const shouldResetApproval = + allowance.lt(quoteResponse.sentAmount.amount) && allowance.gt(0); + if (shouldResetApproval) { + return { ...quoteResponse.approval, data: getEthUsdtResetData() }; + } + } + return undefined; +}; + export const getStatusRequestParams = ( quoteResponse: QuoteResponse, ) => { @@ -193,3 +229,137 @@ export const getClientRequest = ( }, }; }; + +export const toTransactionBatchParams = async ( + disable7702: boolean, + { chainId, gasLimit, ...trade }: TxData, + { + maxFeePerGas, + maxPriorityFeePerGas, + gas, + }: { maxFeePerGas?: string; maxPriorityFeePerGas?: string; gas?: string }, +): Promise => { + const params = { + ...trade, + data: trade.data as `0x${string}`, + to: trade.to as `0x${string}`, + value: trade.value as `0x${string}`, + }; + if (!disable7702) { + return params; + } + + return { + ...params, + gas: toHex(gas ?? 0), + maxFeePerGas: toHex(maxFeePerGas ?? 0), + maxPriorityFeePerGas: toHex(maxPriorityFeePerGas ?? 0), + }; +}; + +export const getAddTransactionBatchParams = async ({ + messagingSystem, + isBridgeTx, + approval, + resetApproval, + trade, + quoteResponse: { + quote: { + feeData: { txFee }, + gasIncluded, + }, + }, + requireApproval = false, + estimateGasFeeFn, +}: { + messagingSystem: BridgeStatusControllerMessenger; + isBridgeTx: boolean; + approval?: TxData; + resetApproval?: TxData; + trade: TxData; + quoteResponse: Omit & QuoteMetadata; + requireApproval?: boolean; + estimateGasFeeFn: typeof TransactionController.prototype.estimateGasFee; +}) => { + const selectedAccount = messagingSystem.call( + 'AccountsController:getAccountByAddress', + trade.from, + ); + if (!selectedAccount) { + throw new Error( + 'Failed to submit cross-chain swap batch transaction: unknown account in trade data', + ); + } + const hexChainId = formatChainIdToHex(trade.chainId); + const networkClientId = messagingSystem.call( + 'NetworkController:findNetworkClientIdByChainId', + hexChainId, + ); + + // 7702 enables gasless txs for smart accounts, so we disable it for now + const disable7702 = true; + const transactions: TransactionBatchSingleRequest[] = []; + if (resetApproval) { + const gasFees = await calculateGasFees( + disable7702, + messagingSystem, + estimateGasFeeFn, + resetApproval, + networkClientId, + hexChainId, + gasIncluded ? txFee : undefined, + ); + transactions.push({ + type: isBridgeTx + ? TransactionType.bridgeApproval + : TransactionType.swapApproval, + params: await toTransactionBatchParams( + disable7702, + resetApproval, + gasFees, + ), + }); + } + if (approval) { + const gasFees = await calculateGasFees( + disable7702, + messagingSystem, + estimateGasFeeFn, + approval, + networkClientId, + hexChainId, + gasIncluded ? txFee : undefined, + ); + transactions.push({ + type: isBridgeTx + ? TransactionType.bridgeApproval + : TransactionType.swapApproval, + params: await toTransactionBatchParams(disable7702, approval, gasFees), + }); + } + const gasFees = await calculateGasFees( + disable7702, + messagingSystem, + estimateGasFeeFn, + trade, + networkClientId, + hexChainId, + gasIncluded ? txFee : undefined, + ); + transactions.push({ + type: isBridgeTx ? TransactionType.bridge : TransactionType.swap, + params: await toTransactionBatchParams(disable7702, trade, gasFees), + }); + const transactionParams: Parameters< + TransactionController['addTransactionBatch'] + >[0] = { + disable7702, + networkClientId, + requireApproval, + origin: 'metamask', + from: trade.from as `0x${string}`, + transactions, + }; + + return transactionParams; +}; diff --git a/packages/bridge-status-controller/tsconfig.build.json b/packages/bridge-status-controller/tsconfig.build.json index 1ec93edefdf..806aaa6b4df 100644 --- a/packages/bridge-status-controller/tsconfig.build.json +++ b/packages/bridge-status-controller/tsconfig.build.json @@ -13,8 +13,7 @@ { "path": "../network-controller/tsconfig.build.json" }, { "path": "../gas-fee-controller/tsconfig.build.json" }, { "path": "../polling-controller/tsconfig.build.json" }, - { "path": "../transaction-controller/tsconfig.build.json" }, - { "path": "../user-operation-controller/tsconfig.build.json" } + { "path": "../transaction-controller/tsconfig.build.json" } ], "include": ["../../types", "./src"] } diff --git a/packages/bridge-status-controller/tsconfig.json b/packages/bridge-status-controller/tsconfig.json index 7935a0447f7..e41150bdaf3 100644 --- a/packages/bridge-status-controller/tsconfig.json +++ b/packages/bridge-status-controller/tsconfig.json @@ -12,8 +12,7 @@ { "path": "../network-controller" }, { "path": "../polling-controller" }, { "path": "../transaction-controller" }, - { "path": "../gas-fee-controller" }, - { "path": "../user-operation-controller" } + { "path": "../gas-fee-controller" } ], "include": ["../../types", "./src"] } diff --git a/packages/transaction-controller/CHANGELOG.md b/packages/transaction-controller/CHANGELOG.md index 1e189e0e80d..faec546da35 100644 --- a/packages/transaction-controller/CHANGELOG.md +++ b/packages/transaction-controller/CHANGELOG.md @@ -7,6 +7,10 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ## [Unreleased] +### Fixed + +- Preserve type of nested transactions in batch if specified in `TransactionBatchSingleRequest` ([]()) + ## [58.1.0] ### Added diff --git a/packages/transaction-controller/src/utils/batch.test.ts b/packages/transaction-controller/src/utils/batch.test.ts index 0f484dd0f15..54f97c3e93d 100644 --- a/packages/transaction-controller/src/utils/batch.test.ts +++ b/packages/transaction-controller/src/utils/batch.test.ts @@ -834,6 +834,110 @@ describe('Batch Utils', () => { }); }); + it('calls publish batch hook with requested transaction type', async () => { + const publishBatchHook: jest.MockedFn = jest.fn(); + + addTransactionMock + .mockResolvedValueOnce({ + transactionMeta: { + ...TRANSACTION_META_MOCK, + id: TRANSACTION_ID_MOCK, + }, + result: Promise.resolve(''), + }) + .mockResolvedValueOnce({ + transactionMeta: { + ...TRANSACTION_META_MOCK, + id: TRANSACTION_ID_2_MOCK, + }, + result: Promise.resolve(''), + }); + + publishBatchHook.mockResolvedValue({ + results: [ + { + transactionHash: TRANSACTION_HASH_MOCK, + }, + { + transactionHash: TRANSACTION_HASH_2_MOCK, + }, + ], + }); + + addTransactionBatch({ + ...request, + publishBatchHook, + request: { + ...request.request, + transactions: [ + { + ...request.request.transactions[0], + type: TransactionType.swap, + }, + { + ...request.request.transactions[1], + type: TransactionType.bridge, + }, + ], + disable7702: true, + }, + }).catch(() => { + // Intentionally empty + }); + + await flushPromises(); + + const publishHooks = addTransactionMock.mock.calls.map( + ([, options]) => options.publishHook, + ); + + publishHooks[0]?.( + TRANSACTION_META_MOCK, + TRANSACTION_SIGNATURE_MOCK, + ).catch(() => { + // Intentionally empty + }); + + publishHooks[1]?.( + TRANSACTION_META_MOCK, + TRANSACTION_SIGNATURE_2_MOCK, + ).catch(() => { + // Intentionally empty + }); + + await flushPromises(); + + expect(publishBatchHook).toHaveBeenCalledTimes(1); + expect(publishBatchHook).toHaveBeenCalledWith({ + from: FROM_MOCK, + networkClientId: NETWORK_CLIENT_ID_MOCK, + transactions: [ + { + id: TRANSACTION_ID_MOCK, + params: { + ...TRANSACTION_BATCH_PARAMS_MOCK, + gas: undefined, + maxFeePerGas: undefined, + maxPriorityFeePerGas: undefined, + }, + signedTx: TRANSACTION_SIGNATURE_MOCK, + type: TransactionType.swap, + }, + { + id: TRANSACTION_ID_2_MOCK, + params: { + ...TRANSACTION_BATCH_PARAMS_MOCK, + gas: undefined, + maxFeePerGas: undefined, + maxPriorityFeePerGas: undefined, + }, + signedTx: TRANSACTION_SIGNATURE_2_MOCK, + type: TransactionType.bridge, + }, + ], + }); + }); + it('resolves individual publish hooks with transaction hashes from publish batch hook', async () => { const publishBatchHook: jest.MockedFn = jest.fn(); diff --git a/packages/transaction-controller/src/utils/batch.ts b/packages/transaction-controller/src/utils/batch.ts index 53d489662f5..84665ab34c5 100644 --- a/packages/transaction-controller/src/utils/batch.ts +++ b/packages/transaction-controller/src/utils/batch.ts @@ -246,7 +246,7 @@ async function getNestedTransactionMeta( ethQuery: EthQuery, ): Promise { const { from } = request; - const { params } = singleRequest; + const { params, type: requestType } = singleRequest; const { type } = await determineTransactionType( { from, ...params }, @@ -255,7 +255,7 @@ async function getNestedTransactionMeta( return { ...params, - type, + type: requestType ?? type, }; } @@ -544,7 +544,7 @@ async function processTransactionWithHook( request: AddTransactionBatchRequest, txBatchMeta?: TransactionBatchMeta, ) { - const { existingTransaction, params } = nestedTransaction; + const { existingTransaction, params, type } = nestedTransaction; const { addTransaction, @@ -595,6 +595,7 @@ async function processTransactionWithHook( const { transactionMeta } = await addTransaction( transactionMetaForGasEstimates.txParams, { + type, batchId, disableGasBuffer: true, networkClientId, @@ -620,11 +621,16 @@ async function processTransactionWithHook( value, }; - log('Processed new transaction with hook', { id, params: newParams }); + log('Processed new transaction with hook', { + id, + params: newParams, + type: nestedTransaction.type, + }); return { id, params: newParams, + type: nestedTransaction.type, }; } diff --git a/yarn.lock b/yarn.lock index 6ea59387efa..3bcdfaa7ad3 100644 --- a/yarn.lock +++ b/yarn.lock @@ -2820,7 +2820,6 @@ __metadata: "@metamask/snaps-controllers": "npm:^14.0.1" "@metamask/superstruct": "npm:^3.1.0" "@metamask/transaction-controller": "npm:^58.1.0" - "@metamask/user-operation-controller": "npm:^37.0.0" "@metamask/utils": "npm:^11.2.0" "@types/jest": "npm:^27.4.1" bignumber.js: "npm:^9.1.2" @@ -4621,7 +4620,7 @@ __metadata: languageName: unknown linkType: soft -"@metamask/user-operation-controller@npm:^37.0.0, @metamask/user-operation-controller@workspace:packages/user-operation-controller": +"@metamask/user-operation-controller@workspace:packages/user-operation-controller": version: 0.0.0-use.local resolution: "@metamask/user-operation-controller@workspace:packages/user-operation-controller" dependencies: