From e1b186cee9a125123d8dc51438de3ca978481c31 Mon Sep 17 00:00:00 2001 From: micaelae Date: Tue, 27 Oct 2026 14:11:37 -0700 Subject: [PATCH 01/11] refactor: getNetworkClientByChainId, getNetworkClientIdByChainId --- .../src/bridge-controller.ts | 31 +++++-------- packages/bridge-controller/src/index.ts | 2 + .../src/utils/controllers/network.ts | 45 +++++++++++++++++++ .../src/utils/feature-flags.ts | 8 ++-- .../src/strategy/evm-strategy.ts | 7 ++- .../src/strategy/intent-strategy.ts | 2 +- .../src/utils/bridge-status.ts | 4 +- .../src/utils/network.ts | 23 +++------- .../src/utils/transaction.ts | 2 +- tsconfig.json | 6 +++ yarn.lock | 4 +- 11 files changed, 88 insertions(+), 46 deletions(-) create mode 100644 packages/bridge-controller/src/utils/controllers/network.ts diff --git a/packages/bridge-controller/src/bridge-controller.ts b/packages/bridge-controller/src/bridge-controller.ts index c6415c52528..d6aa0a6a7d7 100644 --- a/packages/bridge-controller/src/bridge-controller.ts +++ b/packages/bridge-controller/src/bridge-controller.ts @@ -102,6 +102,7 @@ import { } from './validators/quote-request.js'; import type { QuoteResponseV1 } from './validators/quote-response-v1.js'; import type { QuoteResponse } from './validators/quote-response.js'; +import { getNetworkClientByChainId } from './utils/controllers/network.js'; const metadata: StateMetadata = { quoteRequest: { @@ -622,8 +623,10 @@ export class BridgeController extends StaticIntervalPollingController { try { const srcChainIdInHex = formatChainIdToHex(quoteRequest.srcChainId); - const provider = - this.#getNetworkClientByChainId(srcChainIdInHex)?.provider; + const provider = getNetworkClientByChainId( + this.messenger, + srcChainIdInHex, + )?.provider; const normalizedSrcTokenAddress = formatAddressToCaipReference( quoteRequest.srcTokenAddress, ); @@ -654,7 +657,8 @@ export class BridgeController extends StaticIntervalPollingController => { try { const token = await this.messenger.call( @@ -1538,8 +1527,10 @@ export class BridgeController extends StaticIntervalPollingController => { - const networkClient = this.#getNetworkClientByChainId(CHAIN_IDS.MAINNET); - const provider = networkClient?.provider; + const provider = getNetworkClientByChainId( + this.messenger, + CHAIN_IDS.MAINNET, + )?.provider; if (!provider) { throw new Error('No provider found'); } diff --git a/packages/bridge-controller/src/index.ts b/packages/bridge-controller/src/index.ts index cddeda11541..b814490dcff 100644 --- a/packages/bridge-controller/src/index.ts +++ b/packages/bridge-controller/src/index.ts @@ -291,3 +291,5 @@ export { fetchTokens, type SwapsToken, } from './utils/swaps.js'; + +export { getNetworkClientIdByChainId } from './utils/controllers/network.js'; diff --git a/packages/bridge-controller/src/utils/controllers/network.ts b/packages/bridge-controller/src/utils/controllers/network.ts new file mode 100644 index 00000000000..0967f5c2a1e --- /dev/null +++ b/packages/bridge-controller/src/utils/controllers/network.ts @@ -0,0 +1,45 @@ +/* eslint-disable @typescript-eslint/explicit-function-return-type */ +import type { NetworkControllerFindNetworkClientIdByChainIdAction } from '@metamask/network-controller'; + +import type { + BridgeControllerMessenger, + GenericQuoteRequest, +} from '../../types.js'; +import { formatChainIdToHex } from '../caip-formatters.js'; + +export const getNetworkClientIdByChainId = < + T extends { + call( + action: NetworkControllerFindNetworkClientIdByChainIdAction['type'], + chainId: Parameters< + NetworkControllerFindNetworkClientIdByChainIdAction['handler'] + >[0], + ): ReturnType< + NetworkControllerFindNetworkClientIdByChainIdAction['handler'] + >; + }, +>( + messenger: T, + chainId: GenericQuoteRequest['srcChainId'], +) => { + const hexChainId = formatChainIdToHex(chainId); + return messenger.call( + 'NetworkController:findNetworkClientIdByChainId', + hexChainId, + ); +}; + +export const getNetworkClientByChainId = ( + messenger: BridgeControllerMessenger, + chainId: GenericQuoteRequest['srcChainId'], +) => { + const networkClientId = getNetworkClientIdByChainId(messenger, chainId); + if (!networkClientId) { + throw new Error(`No network client found for chainId: ${chainId}`); + } + + return messenger.call( + 'NetworkController:getNetworkClientById', + networkClientId, + ); +}; diff --git a/packages/bridge-controller/src/utils/feature-flags.ts b/packages/bridge-controller/src/utils/feature-flags.ts index d4f304e7df0..59af3d10c6e 100644 --- a/packages/bridge-controller/src/utils/feature-flags.ts +++ b/packages/bridge-controller/src/utils/feature-flags.ts @@ -55,13 +55,15 @@ export const processFeatureFlags = ( * @param messenger - Any messenger with access to RemoteFeatureFlagController:getState * @returns The bridge feature flags */ -export function getBridgeFeatureFlags< +export const getBridgeFeatureFlags = < T extends { call( action: 'RemoteFeatureFlagController:getState', ): RemoteFeatureFlagControllerState; }, ->(messenger: T): FeatureFlagsPlatformConfig { +>( + messenger: T, +): FeatureFlagsPlatformConfig => { // This will return the bridgeConfig for the current platform even without specifying the platform const remoteFeatureFlagControllerState = messenger.call( 'RemoteFeatureFlagController:getState', @@ -80,7 +82,7 @@ export function getBridgeFeatureFlags< remoteFeatureFlagControllerState?.remoteFeatureFlags?.bridgeConfig; return processFeatureFlags(rawMobileFlags || rawBridgeConfig); -} +}; /** * Checks if the client version is greater than or equal to the minimum required version diff --git a/packages/bridge-status-controller/src/strategy/evm-strategy.ts b/packages/bridge-status-controller/src/strategy/evm-strategy.ts index 991f58bb457..cbb8ed43e76 100644 --- a/packages/bridge-status-controller/src/strategy/evm-strategy.ts +++ b/packages/bridge-status-controller/src/strategy/evm-strategy.ts @@ -1,6 +1,10 @@ /* eslint-disable consistent-return */ /* eslint-disable @typescript-eslint/explicit-function-return-type */ -import { formatChainIdToHex, isEvmTxData } from '@metamask/bridge-controller'; +import { + formatChainIdToHex, + isEvmTxData, + getNetworkClientIdByChainId, +} from '@metamask/bridge-controller'; import type { TxData } from '@metamask/bridge-controller'; import { TransactionMeta, @@ -9,7 +13,6 @@ import { import { BridgeStatusControllerMessenger } from '../types.js'; import { getAccountByAddress } from '../utils/accounts.js'; -import { getNetworkClientIdByChainId } from '../utils/network.js'; import { getApprovalTraceParams } from '../utils/trace.js'; import { addTransaction, diff --git a/packages/bridge-status-controller/src/strategy/intent-strategy.ts b/packages/bridge-status-controller/src/strategy/intent-strategy.ts index 11828b620db..d84070c68f4 100644 --- a/packages/bridge-status-controller/src/strategy/intent-strategy.ts +++ b/packages/bridge-status-controller/src/strategy/intent-strategy.ts @@ -3,6 +3,7 @@ import { formatChainIdToHex, isEvmTxData, TxData, + getNetworkClientIdByChainId, } from '@metamask/bridge-controller'; import { TransactionType } from '@metamask/transaction-controller'; @@ -13,7 +14,6 @@ import { postSubmitOrder, } from '../utils/intent-api.js'; import { signTypedMessage } from '../utils/keyring.js'; -import { getNetworkClientIdByChainId } from '../utils/network.js'; import { addSyntheticTransaction, waitForTxConfirmation, diff --git a/packages/bridge-status-controller/src/utils/bridge-status.ts b/packages/bridge-status-controller/src/utils/bridge-status.ts index 4be7bab50aa..f59a34ed808 100644 --- a/packages/bridge-status-controller/src/utils/bridge-status.ts +++ b/packages/bridge-status-controller/src/utils/bridge-status.ts @@ -19,7 +19,7 @@ import type { BridgeStatusControllerMessenger, } from '../types.js'; import { isHistoryItemTooOld } from './history.js'; -import { getNetworkClientByChainId } from './network.js'; +import { getNetworkProviderByChainId } from './network.js'; import { validateBridgeStatusResponse } from './validators.js'; export const getBridgeStatusUrl = (bridgeApiBaseUrl: string): string => @@ -169,7 +169,7 @@ export const shouldWaitForFinalBridgeStatus = async ( let provider: Provider; try { - provider = getNetworkClientByChainId( + provider = getNetworkProviderByChainId( messenger, historyItem.quote.srcChainId, ); diff --git a/packages/bridge-status-controller/src/utils/network.ts b/packages/bridge-status-controller/src/utils/network.ts index e0ef393d277..8feecf07bbb 100644 --- a/packages/bridge-status-controller/src/utils/network.ts +++ b/packages/bridge-status-controller/src/utils/network.ts @@ -1,5 +1,5 @@ /* eslint-disable @typescript-eslint/explicit-function-return-type */ -import { formatChainIdToHex } from '@metamask/bridge-controller'; +import { getNetworkClientIdByChainId } from '@metamask/bridge-controller'; import type { GenericQuoteRequest } from '@metamask/bridge-controller'; import type { NetworkClient } from '@metamask/network-controller'; @@ -18,26 +18,17 @@ export const getSelectedChainId = ( return networkClient.configuration.chainId; }; -export const getNetworkClientIdByChainId = ( - messenger: BridgeStatusControllerMessenger, - chainId: GenericQuoteRequest['srcChainId'], -) => { - const hexChainId = formatChainIdToHex(chainId); - return messenger.call( - 'NetworkController:findNetworkClientIdByChainId', - hexChainId, - ); -}; - -export const getNetworkClientByChainId = ( +export const getNetworkProviderByChainId = ( messenger: BridgeStatusControllerMessenger, chainId: GenericQuoteRequest['srcChainId'], ): NetworkClient['provider'] => { const networkClientId = getNetworkClientIdByChainId(messenger, chainId); + if (!networkClientId) { + throw new Error(`No network client found for chainId: ${chainId}`); + } - const networkClient = messenger.call( + return messenger.call( 'NetworkController:getNetworkClientById', networkClientId, - ); - return networkClient.provider; + ).provider; }; diff --git a/packages/bridge-status-controller/src/utils/transaction.ts b/packages/bridge-status-controller/src/utils/transaction.ts index 15b6baf9fe9..cccd0b3db5e 100644 --- a/packages/bridge-status-controller/src/utils/transaction.ts +++ b/packages/bridge-status-controller/src/utils/transaction.ts @@ -6,6 +6,7 @@ import { isEvmTxData, FeeType, BatchSellTransactionType, + getNetworkClientIdByChainId, } from '@metamask/bridge-controller'; import type { BatchSellTradesResponse, @@ -36,7 +37,6 @@ import { APPROVAL_DELAY_MS } from '../constants.js'; import type { BridgeStatusControllerMessenger } from '../types.js'; import type { QuoteAndTxMetadata } from '../types.js'; import { getAccountByAddress } from './accounts.js'; -import { getNetworkClientIdByChainId } from './network.js'; export const isApprovalTx = (type: TransactionType) => type === TransactionType.bridgeApproval || diff --git a/tsconfig.json b/tsconfig.json index 97715c9ef59..19590f2f261 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -349,6 +349,12 @@ }, { "path": "./packages/watch-only-account-service" + }, + { + "path": "./packages/money-account-controller" + }, + { + "path": "./packages/money-account-utils" } ], "files": [], diff --git a/yarn.lock b/yarn.lock index eaec8faf8f0..5bea0f9040a 100644 --- a/yarn.lock +++ b/yarn.lock @@ -5812,6 +5812,8 @@ __metadata: "@metamask/gas-fee-controller": "npm:^27.0.0" "@metamask/keyring-controller": "npm:^28.2.0" "@metamask/messenger": "npm:^3.0.0" + "@metamask/money-account-controller": "npm:^3.0.0" + "@metamask/money-account-utils": "npm:^2.1.0" "@metamask/network-controller": "npm:^37.1.1" "@metamask/polling-controller": "npm:^17.0.0" "@metamask/profile-sync-controller": "npm:^36.0.0" @@ -7399,7 +7401,7 @@ __metadata: languageName: unknown linkType: soft -"@metamask/money-account-controller@workspace:packages/money-account-controller": +"@metamask/money-account-controller@npm:^3.0.0, @metamask/money-account-controller@workspace:packages/money-account-controller": version: 0.0.0-use.local resolution: "@metamask/money-account-controller@workspace:packages/money-account-controller" dependencies: From 0da68dbb2228b531ab97b9158d7fd807c6abe3b9 Mon Sep 17 00:00:00 2001 From: micaelae Date: Tue, 27 Oct 2026 14:39:27 -0700 Subject: [PATCH 02/11] refactor: getAccountByAddress --- .../bridge-controller/src/bridge-controller.ts | 6 ++---- packages/bridge-controller/src/index.ts | 1 + .../src/utils/controllers/accounts.ts | 16 ++++++++++++++++ .../src/bridge-status-controller.ts | 2 +- .../src/strategy/evm-strategy.ts | 2 +- .../src/utils/accounts.ts | 9 --------- .../src/utils/transaction.ts | 2 +- 7 files changed, 22 insertions(+), 16 deletions(-) create mode 100644 packages/bridge-controller/src/utils/controllers/accounts.ts delete mode 100644 packages/bridge-status-controller/src/utils/accounts.ts diff --git a/packages/bridge-controller/src/bridge-controller.ts b/packages/bridge-controller/src/bridge-controller.ts index d6aa0a6a7d7..05049481e99 100644 --- a/packages/bridge-controller/src/bridge-controller.ts +++ b/packages/bridge-controller/src/bridge-controller.ts @@ -40,6 +40,7 @@ import type { } from './types.js'; import { getAssetIdsForToken, toExchangeRates } from './utils/assets.js'; import { hasSufficientBalance } from './utils/balance.js'; +import { getAccountByAddress } from './utils/controllers/accounts.js'; import { getDefaultBridgeControllerState, isCrossChain, @@ -1184,10 +1185,7 @@ export class BridgeController extends StaticIntervalPollingController; + }, +>( + messenger: T, + address: string, +) => { + return messenger.call('AccountsController:getAccountByAddress', address); +}; diff --git a/packages/bridge-status-controller/src/bridge-status-controller.ts b/packages/bridge-status-controller/src/bridge-status-controller.ts index 45f3050710b..a1f269199ec 100644 --- a/packages/bridge-status-controller/src/bridge-status-controller.ts +++ b/packages/bridge-status-controller/src/bridge-status-controller.ts @@ -29,6 +29,7 @@ import { MetaMetricsSwapsEventSource, PollingStatus, formatChainIdToHex, + getAccountByAddress, } from '@metamask/bridge-controller'; import { StaticIntervalPollingController } from '@metamask/polling-controller'; import { @@ -72,7 +73,6 @@ import type { } from './types.js'; import type { BridgeStatusControllerMessenger } from './types.js'; import { BridgeClientId } from './types.js'; -import { getAccountByAddress } from './utils/accounts.js'; import { getJwt } from './utils/authentication.js'; import { fetchBridgeTxStatus, diff --git a/packages/bridge-status-controller/src/strategy/evm-strategy.ts b/packages/bridge-status-controller/src/strategy/evm-strategy.ts index cbb8ed43e76..c46c2147d5f 100644 --- a/packages/bridge-status-controller/src/strategy/evm-strategy.ts +++ b/packages/bridge-status-controller/src/strategy/evm-strategy.ts @@ -4,6 +4,7 @@ import { formatChainIdToHex, isEvmTxData, getNetworkClientIdByChainId, + getAccountByAddress, } from '@metamask/bridge-controller'; import type { TxData } from '@metamask/bridge-controller'; import { @@ -12,7 +13,6 @@ import { } from '@metamask/transaction-controller'; import { BridgeStatusControllerMessenger } from '../types.js'; -import { getAccountByAddress } from '../utils/accounts.js'; import { getApprovalTraceParams } from '../utils/trace.js'; import { addTransaction, diff --git a/packages/bridge-status-controller/src/utils/accounts.ts b/packages/bridge-status-controller/src/utils/accounts.ts deleted file mode 100644 index 01b56075ad6..00000000000 --- a/packages/bridge-status-controller/src/utils/accounts.ts +++ /dev/null @@ -1,9 +0,0 @@ -/* eslint-disable @typescript-eslint/explicit-function-return-type */ -import { BridgeStatusControllerMessenger } from '../types.js'; - -export const getAccountByAddress = ( - messenger: BridgeStatusControllerMessenger, - address: string, -) => { - return messenger.call('AccountsController:getAccountByAddress', address); -}; diff --git a/packages/bridge-status-controller/src/utils/transaction.ts b/packages/bridge-status-controller/src/utils/transaction.ts index cccd0b3db5e..e3fb70ffb06 100644 --- a/packages/bridge-status-controller/src/utils/transaction.ts +++ b/packages/bridge-status-controller/src/utils/transaction.ts @@ -7,6 +7,7 @@ import { FeeType, BatchSellTransactionType, getNetworkClientIdByChainId, + getAccountByAddress, } from '@metamask/bridge-controller'; import type { BatchSellTradesResponse, @@ -36,7 +37,6 @@ import type { Hex } from '@metamask/utils'; import { APPROVAL_DELAY_MS } from '../constants.js'; import type { BridgeStatusControllerMessenger } from '../types.js'; import type { QuoteAndTxMetadata } from '../types.js'; -import { getAccountByAddress } from './accounts.js'; export const isApprovalTx = (type: TransactionType) => type === TransactionType.bridgeApproval || From 7c16b028453886180d5ea1dd3bdd4d35e85d1d41 Mon Sep 17 00:00:00 2001 From: micaelae Date: Tue, 27 Oct 2026 14:40:33 -0700 Subject: [PATCH 03/11] refactor: getFeatureFlags --- .../utils/controllers/remote-feature-flags.ts | 16 ++++++++++++++++ .../bridge-controller/src/utils/feature-flags.ts | 11 ++++------- 2 files changed, 20 insertions(+), 7 deletions(-) create mode 100644 packages/bridge-controller/src/utils/controllers/remote-feature-flags.ts diff --git a/packages/bridge-controller/src/utils/controllers/remote-feature-flags.ts b/packages/bridge-controller/src/utils/controllers/remote-feature-flags.ts new file mode 100644 index 00000000000..04b45253a3b --- /dev/null +++ b/packages/bridge-controller/src/utils/controllers/remote-feature-flags.ts @@ -0,0 +1,16 @@ +import type { RemoteFeatureFlagControllerGetStateAction } from '@metamask/remote-feature-flag-controller'; + +export const getFeatureFlags = < + T extends { + call( + action: RemoteFeatureFlagControllerGetStateAction['type'], + ): ReturnType; + }, +>( + messenger: T, +) => { + const remoteFeatureFlags = messenger.call( + 'RemoteFeatureFlagController:getState', + )?.remoteFeatureFlags; + return remoteFeatureFlags; +}; diff --git a/packages/bridge-controller/src/utils/feature-flags.ts b/packages/bridge-controller/src/utils/feature-flags.ts index 59af3d10c6e..c487187cdfb 100644 --- a/packages/bridge-controller/src/utils/feature-flags.ts +++ b/packages/bridge-controller/src/utils/feature-flags.ts @@ -10,6 +10,7 @@ import type { } from '../types.js'; import { validateFeatureFlagsResponse } from '../validators/feature-flags.js'; import { formatChainIdToCaip } from './caip-formatters.js'; +import { getFeatureFlags } from './controllers/remote-feature-flags.js'; export const formatFeatureFlags = ( bridgeFeatureFlags: FeatureFlagsPlatformConfig, @@ -65,21 +66,17 @@ export const getBridgeFeatureFlags = < messenger: T, ): FeatureFlagsPlatformConfig => { // This will return the bridgeConfig for the current platform even without specifying the platform - const remoteFeatureFlagControllerState = messenger.call( - 'RemoteFeatureFlagController:getState', - ); + const remoteFeatureFlagControllerState = getFeatureFlags(messenger); // bridgeConfigV2 is the feature flag for the mobile app // bridgeConfig for Mobile has been deprecated since release of bridge and Solana in 7.46.0 was pushed back // and there's no way to turn on bridgeConfig for 7.47.0 without affecting 7.46.0 as well. // You will still get bridgeConfig returned from remoteFeatureFlagControllerState but you should use bridgeConfigV2 instead // Mobile's bridgeConfig will be permanently serving the disabled variation, so falling back to it in Mobile will be ok - const rawMobileFlags = - remoteFeatureFlagControllerState?.remoteFeatureFlags?.bridgeConfigV2; + const rawMobileFlags = remoteFeatureFlagControllerState?.bridgeConfigV2; // Extension LaunchDarkly will not have the bridgeConfigV2 field, so we'll continue to use bridgeConfig - const rawBridgeConfig = - remoteFeatureFlagControllerState?.remoteFeatureFlags?.bridgeConfig; + const rawBridgeConfig = remoteFeatureFlagControllerState?.bridgeConfig; return processFeatureFlags(rawMobileFlags || rawBridgeConfig); }; From 2b28435a2dd135f2d01edd13bf234f59b33a70d5 Mon Sep 17 00:00:00 2001 From: micaelae Date: Tue, 27 Oct 2026 15:06:23 -0700 Subject: [PATCH 04/11] feat: append isMoneyAccount to quoteRequest --- packages/bridge-controller/src/types.ts | 4 ++++ packages/bridge-controller/src/utils/fetch.test.ts | 8 ++++---- packages/bridge-controller/src/utils/fetch.ts | 7 ++++++- 3 files changed, 14 insertions(+), 5 deletions(-) diff --git a/packages/bridge-controller/src/types.ts b/packages/bridge-controller/src/types.ts index 2c341d63e34..828c9b90f77 100644 --- a/packages/bridge-controller/src/types.ts +++ b/packages/bridge-controller/src/types.ts @@ -200,6 +200,10 @@ export type QuoteRequest< * The fee that will be charged by MetaMask */ fee?: number; + /** + * Whether the destWalletAddress is a money account, only needed for same-chain swaps + */ + isMoneyAccount?: boolean; }; export enum StatusTypes { diff --git a/packages/bridge-controller/src/utils/fetch.test.ts b/packages/bridge-controller/src/utils/fetch.test.ts index a6842acbfda..b35bef0d9f7 100644 --- a/packages/bridge-controller/src/utils/fetch.test.ts +++ b/packages/bridge-controller/src/utils/fetch.test.ts @@ -196,7 +196,7 @@ describe('fetch', () => { ); expect(mockFetchFn).toHaveBeenCalledWith( - 'https://bridge.api.cx.metamask.io/getQuote?walletAddress=0x1f9840a85d5aF5bf1D1762F925BDADdC4201F984&destWalletAddress=0x1f9840a85d5aF5bf1D1762F925BDADdC4201F984&srcChainId=1&destChainId=10&srcTokenAddress=0x0000000000000000000000000000000000000000&destTokenAddress=0x0000000000000000000000000000000000000000&srcTokenAmount=20000&insufficientBal=false&resetApproval=false&gasIncluded=false&gasIncluded7702=false&slippage=0.5', + 'https://bridge.api.cx.metamask.io/getQuote?walletAddress=0x1f9840a85d5aF5bf1D1762F925BDADdC4201F984&destWalletAddress=0x1f9840a85d5aF5bf1D1762F925BDADdC4201F984&srcChainId=1&destChainId=10&srcTokenAddress=0x0000000000000000000000000000000000000000&destTokenAddress=0x0000000000000000000000000000000000000000&srcTokenAmount=20000&insufficientBal=false&resetApproval=false&gasIncluded=false&gasIncluded7702=false&isMoneyAccount=false&slippage=0.5', { headers: { 'X-Client-Id': 'extension', @@ -260,7 +260,7 @@ describe('fetch', () => { ); expect(mockFetchFn).toHaveBeenCalledWith( - 'https://bridge.api.cx.metamask.io/getQuote?walletAddress=0x1f9840a85d5aF5bf1D1762F925BDADdC4201F984&destWalletAddress=0x1f9840a85d5aF5bf1D1762F925BDADdC4201F984&srcChainId=1&destChainId=10&srcTokenAddress=0x0000000000000000000000000000000000000000&destTokenAddress=0x0000000000000000000000000000000000000000&srcTokenAmount=20000&insufficientBal=false&resetApproval=false&gasIncluded=false&gasIncluded7702=false&slippage=0.5', + 'https://bridge.api.cx.metamask.io/getQuote?walletAddress=0x1f9840a85d5aF5bf1D1762F925BDADdC4201F984&destWalletAddress=0x1f9840a85d5aF5bf1D1762F925BDADdC4201F984&srcChainId=1&destChainId=10&srcTokenAddress=0x0000000000000000000000000000000000000000&destTokenAddress=0x0000000000000000000000000000000000000000&srcTokenAmount=20000&insufficientBal=false&resetApproval=false&gasIncluded=false&gasIncluded7702=false&isMoneyAccount=false&slippage=0.5', { headers: { 'X-Client-Id': 'extension', @@ -352,7 +352,7 @@ describe('fetch', () => { ); expect(mockFetchFn).toHaveBeenCalledWith( - 'https://bridge.api.cx.metamask.io/getQuote?walletAddress=0x1f9840a85d5aF5bf1D1762F925BDADdC4201F984&destWalletAddress=0x1f9840a85d5aF5bf1D1762F925BDADdC4201F984&srcChainId=1&destChainId=10&srcTokenAddress=0x0000000000000000000000000000000000000000&destTokenAddress=0x0000000000000000000000000000000000000000&srcTokenAmount=20000&insufficientBal=false&resetApproval=false&gasIncluded=false&gasIncluded7702=false&slippage=0.5', + 'https://bridge.api.cx.metamask.io/getQuote?walletAddress=0x1f9840a85d5aF5bf1D1762F925BDADdC4201F984&destWalletAddress=0x1f9840a85d5aF5bf1D1762F925BDADdC4201F984&srcChainId=1&destChainId=10&srcTokenAddress=0x0000000000000000000000000000000000000000&destTokenAddress=0x0000000000000000000000000000000000000000&srcTokenAmount=20000&insufficientBal=false&resetApproval=false&gasIncluded=false&gasIncluded7702=false&isMoneyAccount=false&slippage=0.5', { headers: { 'X-Client-Id': 'extension', @@ -427,7 +427,7 @@ describe('fetch', () => { ); expect(mockFetchFn).toHaveBeenCalledWith( - 'https://bridge.api.cx.metamask.io/getQuote?walletAddress=0x1f9840a85d5aF5bf1D1762F925BDADdC4201F984&destWalletAddress=0x1f9840a85d5aF5bf1D1762F925BDADdC4201F984&srcChainId=1&destChainId=10&srcTokenAddress=0x0000000000000000000000000000000000000000&destTokenAddress=0x0000000000000000000000000000000000000000&srcTokenAmount=20000&insufficientBal=false&resetApproval=false&gasIncluded=false&gasIncluded7702=false&slippage=0.5&fee=0&aggIds=socket%2Clifi&bridgeIds=bridge1%2Cbridge2', + 'https://bridge.api.cx.metamask.io/getQuote?walletAddress=0x1f9840a85d5aF5bf1D1762F925BDADdC4201F984&destWalletAddress=0x1f9840a85d5aF5bf1D1762F925BDADdC4201F984&srcChainId=1&destChainId=10&srcTokenAddress=0x0000000000000000000000000000000000000000&destTokenAddress=0x0000000000000000000000000000000000000000&srcTokenAmount=20000&insufficientBal=false&resetApproval=false&gasIncluded=false&gasIncluded7702=false&isMoneyAccount=false&slippage=0.5&fee=0&aggIds=socket%2Clifi&bridgeIds=bridge1%2Cbridge2', { headers: { 'X-Client-Id': 'extension', diff --git a/packages/bridge-controller/src/utils/fetch.ts b/packages/bridge-controller/src/utils/fetch.ts index bde74d3d691..175a076466b 100644 --- a/packages/bridge-controller/src/utils/fetch.ts +++ b/packages/bridge-controller/src/utils/fetch.ts @@ -96,6 +96,7 @@ export async function fetchBridgeTokens( */ const formatQuoteRequest = (request: GenericQuoteRequest): QuoteRequest => { const destWalletAddress = request.destWalletAddress ?? request.walletAddress; + const isMoneyAccount = Boolean(request.isMoneyAccount); // Transform the generic quote request into QuoteRequest const normalizedRequest: QuoteRequest = { walletAddress: formatAddressToCaipReference(request.walletAddress), @@ -109,6 +110,7 @@ const formatQuoteRequest = (request: GenericQuoteRequest): QuoteRequest => { resetApproval: Boolean(request.resetApproval), gasIncluded: Boolean(request.gasIncluded), gasIncluded7702: Boolean(request.gasIncluded7702), + isMoneyAccount, }; if (request.slippage !== undefined) { normalizedRequest.slippage = request.slippage; @@ -116,9 +118,12 @@ const formatQuoteRequest = (request: GenericQuoteRequest): QuoteRequest => { if (request.fee !== undefined) { normalizedRequest.fee = request.fee; } - if (request.aggIds && request.aggIds.length > 0) { + if (request.aggIds && request.aggIds.length > 0 && !isMoneyAccount) { normalizedRequest.aggIds = request.aggIds; } + if (isMoneyAccount) { + normalizedRequest.aggIds = ['relay']; + } if (request.bridgeIds && request.bridgeIds.length > 0) { normalizedRequest.bridgeIds = request.bridgeIds; } From af732a4637baa97c839e2f3944b73ec24201c036 Mon Sep 17 00:00:00 2001 From: micaelae Date: Tue, 27 Oct 2026 15:21:44 -0700 Subject: [PATCH 05/11] feat: append depositConfig to QuoteResponse if dest wallet is a money account append --- packages/bridge-controller/package.json | 2 + .../src/bridge-controller.ts | 1 + packages/bridge-controller/src/types.ts | 9 +-- .../src/utils/controllers/money-account.ts | 70 +++++++++++++++++++ packages/bridge-controller/src/utils/fetch.ts | 9 +++ .../src/validators/quote-response.ts | 16 +++++ .../bridge-controller/tsconfig.build.json | 6 ++ packages/bridge-controller/tsconfig.json | 6 ++ 8 files changed, 115 insertions(+), 4 deletions(-) create mode 100644 packages/bridge-controller/src/utils/controllers/money-account.ts diff --git a/packages/bridge-controller/package.json b/packages/bridge-controller/package.json index d09071343e6..65cd985201b 100644 --- a/packages/bridge-controller/package.json +++ b/packages/bridge-controller/package.json @@ -65,6 +65,8 @@ "@metamask/messenger": "^3.0.0", "@metamask/metamask-eth-abis": "^3.1.1", "@metamask/multichain-network-controller": "^4.0.0", + "@metamask/money-account-controller": "^3.0.0", + "@metamask/money-account-utils": "^2.1.0", "@metamask/network-controller": "^37.1.1", "@metamask/polling-controller": "^17.0.0", "@metamask/profile-sync-controller": "^36.0.0", diff --git a/packages/bridge-controller/src/bridge-controller.ts b/packages/bridge-controller/src/bridge-controller.ts index 05049481e99..c879c2f0694 100644 --- a/packages/bridge-controller/src/bridge-controller.ts +++ b/packages/bridge-controller/src/bridge-controller.ts @@ -1072,6 +1072,7 @@ export class BridgeController extends StaticIntervalPollingController>(); await fetchBridgeQuoteStream( + this.messenger, this.#fetchFn, quoteRequests, signal, diff --git a/packages/bridge-controller/src/types.ts b/packages/bridge-controller/src/types.ts index 828c9b90f77..e04421445f1 100644 --- a/packages/bridge-controller/src/types.ts +++ b/packages/bridge-controller/src/types.ts @@ -15,6 +15,7 @@ import type { import type { AuthenticationControllerGetBearerTokenAction } from '@metamask/profile-sync-controller/auth'; import type { RemoteFeatureFlagControllerGetStateAction } from '@metamask/remote-feature-flag-controller'; import type { SnapControllerHandleRequestAction } from '@metamask/snaps-controllers'; +import type { MoneyAccountControllerGetMoneyAccountAction } from '@metamask/money-account-controller'; import type { Infer } from '@metamask/superstruct'; import type { CaipAccountId, @@ -394,20 +395,20 @@ export type BridgeControllerStateChangeEvent = ControllerStateChangeEvent< >; export type BridgeControllerActions = - | BridgeControllerGetStateAction - | BridgeControllerMethodActions; + BridgeControllerGetStateAction | BridgeControllerMethodActions; export type BridgeControllerEvents = BridgeControllerStateChangeEvent; export type AllowedActions = | AccountsControllerGetAccountByAddressAction + | AssetsControllerGetExchangeRatesForBridgeAction | AuthenticationControllerGetBearerTokenAction | CurrencyRateControllerGetStateAction - | SnapControllerHandleRequestAction + | MoneyAccountControllerGetMoneyAccountAction | NetworkControllerFindNetworkClientIdByChainIdAction | NetworkControllerGetNetworkClientByIdAction | RemoteFeatureFlagControllerGetStateAction - | AssetsControllerGetExchangeRatesForBridgeAction; + | SnapControllerHandleRequestAction; export type AllowedEvents = never; /** diff --git a/packages/bridge-controller/src/utils/controllers/money-account.ts b/packages/bridge-controller/src/utils/controllers/money-account.ts new file mode 100644 index 00000000000..fd6119151ee --- /dev/null +++ b/packages/bridge-controller/src/utils/controllers/money-account.ts @@ -0,0 +1,70 @@ +import type { BridgeControllerMessenger, QuoteRequest } from '../../types.js'; +import { getMoneyAccountVaultConfig } from '@metamask/money-account-utils'; +import { getFeatureFlags } from './remote-feature-flags.js'; +import { getAccountByAddress } from './accounts.js'; +import { CHAIN_IDS } from '../../constants/chains.js'; + +/** + * Resolves the primary money account and vault config to use for the money account deposit request + * + * UI: Money is dest token + * + * emvAccount swap X to MUSD at moneyAccount's evmAddress (swap + send) + * moneyAccount's evmAddress deposit MUSD to moneyAccount + * + * @param params - The parameters for the function + * @param params.messenger - The bridge-controller messenger instance + * @param params.quoteRequest - The normalized quote request + * @returns The money account deposit config, undefined if the dest wallet address is not a money account + */ +export const getMoneyAccountDepositSetup = async ({ + messenger, + quoteRequest, +}: { + messenger: BridgeControllerMessenger; + quoteRequest: QuoteRequest; +}) => { + // Get primary money account for dest wallet address + const depositor = getAccountByAddress( + messenger, + quoteRequest.destWalletAddress ?? quoteRequest.walletAddress, + )?.id; + if (!depositor) { + return undefined; + } + const primaryMoneyAccount = messenger.call( + 'MoneyAccountController:getMoneyAccount', + { id: depositor }, + ); + if (!primaryMoneyAccount) { + return undefined; + } + if (!primaryMoneyAccount.address) { + return undefined; + } + + // Get vault config + const ff = getFeatureFlags(messenger); + const vaultConfig = getMoneyAccountVaultConfig(ff); + if (!vaultConfig) { + return undefined; + } + + const { + chainId, + boringVault, + tellerAddress, + accountantAddress, + lensAddress, + } = vaultConfig; + + return { + chainIdHex: chainId, + boringVault, + tellerAddress, + accountantAddress, + lensAddress, + moneyAccountAddress: primaryMoneyAccount.address as `0x${string}`, + isGasFeeSponsored: CHAIN_IDS.MONAD === chainId, + }; +}; diff --git a/packages/bridge-controller/src/utils/fetch.ts b/packages/bridge-controller/src/utils/fetch.ts index 175a076466b..a586eb0b1d5 100644 --- a/packages/bridge-controller/src/utils/fetch.ts +++ b/packages/bridge-controller/src/utils/fetch.ts @@ -13,6 +13,7 @@ import type { QuoteStreamCompleteData, BatchSellTradesRequest, BatchSellTradesResponse, + BridgeControllerMessenger, } from '../types.js'; import { validateBatchSellTradesResponse } from '../validators/batch-sell.js'; import type { BridgeAsset } from '../validators/bridge-asset.js'; @@ -34,6 +35,7 @@ import { import { fetchServerEvents } from './fetch-server-events.js'; import type { QuoteMetadata } from './quote-metadata/types.js'; import { formatStructErrors } from './struct-error.js'; +import { getMoneyAccountDepositSetup } from './controllers/money-account.js'; export const getClientHeaders = ({ clientId, @@ -349,6 +351,7 @@ const getQuoteResponseId = ({ * @returns A list of bridge tx quote promises */ export async function fetchBridgeQuoteStream( + messenger: BridgeControllerMessenger, fetchFn: FetchFunction, quoteRequests: GenericQuoteRequest[], signal: AbortSignal | undefined, @@ -409,6 +412,12 @@ export async function fetchBridgeQuoteStream( ...(isBatchSellRequest && { quoteRequestIndex: matchedQuoteRequestIdx, }), + ...(matchingQuoteRequest.isMoneyAccount && { + depositConfig: await getMoneyAccountDepositSetup({ + messenger, + quoteRequest: matchingQuoteRequest, + }), + }), }); } catch (error) { if (error instanceof StructError) { diff --git a/packages/bridge-controller/src/validators/quote-response.ts b/packages/bridge-controller/src/validators/quote-response.ts index 741a4438034..d4185502c44 100644 --- a/packages/bridge-controller/src/validators/quote-response.ts +++ b/packages/bridge-controller/src/validators/quote-response.ts @@ -12,6 +12,7 @@ import { enums, literal, AnyStruct, + boolean, } from '@metamask/superstruct'; import { CaipChainId, @@ -27,6 +28,7 @@ import { QuoteSchemaV2 } from './quote.js'; import { BitcoinTradeData, BitcoinTradeDataSchema, + HexString, StellarTradeData, StellarTradeDataSchema, TronTradeData, @@ -60,6 +62,20 @@ const CommonQuoteResponseSchema = type({ * @deprecated Use network feeData */ l1GasFeesInHexWei: optional(StrictHexStruct), + /** + * Appended to the quote response if a money account deposit is needed after the swap + */ + depositConfig: optional( + type({ + chainIdHex: HexString, + boringVault: HexString, + tellerAddress: HexString, + accountantAddress: HexString, + lensAddress: HexString, + moneyAccountAddress: HexString, + isGasFeeSponsored: boolean(), + }), + ), }); const EvmQuoteResponseSchema = intersection([ diff --git a/packages/bridge-controller/tsconfig.build.json b/packages/bridge-controller/tsconfig.build.json index df1bff7dd74..8b426424aa5 100644 --- a/packages/bridge-controller/tsconfig.build.json +++ b/packages/bridge-controller/tsconfig.build.json @@ -49,6 +49,12 @@ }, { "path": "../utils/tsconfig.build.json" + }, + { + "path": "../money-account-controller/tsconfig.build.json" + }, + { + "path": "../money-account-utils/tsconfig.build.json" } ], "include": ["../../types", "./src"] diff --git a/packages/bridge-controller/tsconfig.json b/packages/bridge-controller/tsconfig.json index 4045b866183..729100798ac 100644 --- a/packages/bridge-controller/tsconfig.json +++ b/packages/bridge-controller/tsconfig.json @@ -48,6 +48,12 @@ }, { "path": "../utils" + }, + { + "path": "../money-account-controller" + }, + { + "path": "../money-account-utils" } ], "include": ["../../types", "./src", "./tests"] From 0e167b3f09e54d421688f385c0b9932de966a6d7 Mon Sep 17 00:00:00 2001 From: micaelae Date: Tue, 27 Oct 2026 15:32:33 -0700 Subject: [PATCH 06/11] feat: deposit to money account after swap --- .../src/utils/controllers/money-account.ts | 2 +- .../bridge-status-controller/package.json | 2 + .../src/bridge-status-controller.ts | 46 +++++++-- .../bridge-status-controller/src/types.ts | 14 ++- .../src/utils/bridge-status.ts | 12 +-- .../src/utils/history.ts | 5 + .../src/utils/money-account.ts | 94 +++++++++++++++++++ .../src/utils/swap-received-amount.ts | 33 +++---- .../tsconfig.build.json | 6 ++ .../bridge-status-controller/tsconfig.json | 6 ++ 10 files changed, 184 insertions(+), 36 deletions(-) create mode 100644 packages/bridge-status-controller/src/utils/money-account.ts diff --git a/packages/bridge-controller/src/utils/controllers/money-account.ts b/packages/bridge-controller/src/utils/controllers/money-account.ts index fd6119151ee..3de3d3463ed 100644 --- a/packages/bridge-controller/src/utils/controllers/money-account.ts +++ b/packages/bridge-controller/src/utils/controllers/money-account.ts @@ -64,7 +64,7 @@ export const getMoneyAccountDepositSetup = async ({ tellerAddress, accountantAddress, lensAddress, - moneyAccountAddress: primaryMoneyAccount.address as `0x${string}`, + moneyAccountAddress: primaryMoneyAccount.address, isGasFeeSponsored: CHAIN_IDS.MONAD === chainId, }; }; diff --git a/packages/bridge-status-controller/package.json b/packages/bridge-status-controller/package.json index 4089c5c96d6..c6b5956a904 100644 --- a/packages/bridge-status-controller/package.json +++ b/packages/bridge-status-controller/package.json @@ -57,6 +57,8 @@ "@metamask/gas-fee-controller": "^27.0.0", "@metamask/keyring-controller": "^28.2.0", "@metamask/messenger": "^3.0.0", + "@metamask/money-account-controller": "^3.0.0", + "@metamask/money-account-utils": "^2.1.0", "@metamask/network-controller": "^37.1.1", "@metamask/polling-controller": "^17.0.0", "@metamask/profile-sync-controller": "^36.0.0", diff --git a/packages/bridge-status-controller/src/bridge-status-controller.ts b/packages/bridge-status-controller/src/bridge-status-controller.ts index a1f269199ec..2da62b64135 100644 --- a/packages/bridge-status-controller/src/bridge-status-controller.ts +++ b/packages/bridge-status-controller/src/bridge-status-controller.ts @@ -125,6 +125,7 @@ import { updateTransactionsInBatch, hasNestedSwapTransactions, } from './utils/transaction.js'; +import { handleMoneyAccountDeposit } from './utils/money-account.js'; const metadata: StateMetadata = { // We want to persist the bridge status state so that we can show the proper data for the Activity list @@ -538,6 +539,14 @@ export class BridgeStatusController extends StaticIntervalPollingController undefined); + if (historyItem && historyItem.depositConfig && txMeta.hash) { + handleMoneyAccountDeposit({ + messenger: this.messenger, + historyItem, + txHash: txMeta.hash, + addTransactionBatchFn: this.#addTransactionBatchFn, + }); + } } this.#trackUnifiedSwapBridgeEvent( UnifiedSwapBridgeEventName.Completed, @@ -1220,6 +1229,18 @@ export class BridgeStatusController extends StaticIntervalPollingController & QuoteMetadata)[]; + QuoteResponse[] | (QuoteResponseV1 & QuoteMetadata)[]; isStxEnabled: boolean; + isMoneyAccountDeposit?: boolean; quotesReceivedContext?: RequiredEventContextFromClient[UnifiedSwapBridgeEventName.QuotesReceived]; location?: MetaMetricsSwapsEventSource; abTests?: Record; @@ -1548,7 +1573,11 @@ export class BridgeStatusController extends StaticIntervalPollingController; export type BridgeStatusControllerActions = - | BridgeStatusControllerGetStateAction - | BridgeStatusControllerMethodActions; + BridgeStatusControllerGetStateAction | BridgeStatusControllerMethodActions; // Events export type BridgeStatusControllerStateChangeEvent = ControllerStateChangeEvent< @@ -391,7 +398,8 @@ type AllowedActions = | BridgeControllerGetStateAction | AccountsControllerGetAccountByAddressAction | AuthenticationControllerGetBearerTokenAction - | KeyringControllerSignTypedMessageAction; + | KeyringControllerSignTypedMessageAction + | MoneyAccountControllerGetMoneyAccountAction; /** * The external events available to the BridgeStatusController. diff --git a/packages/bridge-status-controller/src/utils/bridge-status.ts b/packages/bridge-status-controller/src/utils/bridge-status.ts index f59a34ed808..cc994fca141 100644 --- a/packages/bridge-status-controller/src/utils/bridge-status.ts +++ b/packages/bridge-status-controller/src/utils/bridge-status.ts @@ -152,14 +152,12 @@ export const shouldWaitForFinalBridgeStatus = async ( historyItem: BridgeHistoryItem, ): Promise => { // Keep waiting for status if the history is not pending or is not old enough yet - if ( - !( - isHistoryItemTooOld(messenger, historyItem) && - [StatusTypes.PENDING, StatusTypes.UNKNOWN].includes( - historyItem.status.status, - ) + if (!( + isHistoryItemTooOld(messenger, historyItem) && + [StatusTypes.PENDING, StatusTypes.UNKNOWN].includes( + historyItem.status.status, ) - ) { + )) { return true; } diff --git a/packages/bridge-status-controller/src/utils/history.ts b/packages/bridge-status-controller/src/utils/history.ts index 0b2a4a586c0..2f571807871 100644 --- a/packages/bridge-status-controller/src/utils/history.ts +++ b/packages/bridge-status-controller/src/utils/history.ts @@ -267,6 +267,7 @@ export const getInitialHistoryItem = ( accountAddress: selectedAddress, originalTransactionId, actionId, + depositConfig, tokenSecurityTypeDestination, inputPrimaryDenomination, batchSellData, @@ -328,6 +329,10 @@ export const getInitialHistoryItem = ( }), }; + if (depositConfig) { + txHistoryItem.depositConfig = depositConfig; + } + if (batchSellData) { txHistoryItem.batchSellData = batchSellData; } diff --git a/packages/bridge-status-controller/src/utils/money-account.ts b/packages/bridge-status-controller/src/utils/money-account.ts new file mode 100644 index 00000000000..63e52f5895e --- /dev/null +++ b/packages/bridge-status-controller/src/utils/money-account.ts @@ -0,0 +1,94 @@ +import { + formatChainIdToHex, + getNetworkClientIdByChainId, +} from '@metamask/bridge-controller'; +import { Web3Provider } from '@ethersproject/providers'; +import type { TransactionController } from '@metamask/transaction-controller'; +import type { + BridgeHistoryItem, + BridgeStatusControllerMessenger, +} from '../types.js'; +import { getReceivedERC20Amount } from './swap-received-amount.js'; +import { getNetworkProviderByChainId } from './network.js'; +import { getMoneyAccountDepositAssetAddress } from '@metamask/money-account-utils'; +import { buildMoneyAccountDepositBatch } from '@metamask/money-account-utils'; +import { ORIGIN_METAMASK } from '@metamask/controller-utils'; + +export const handleMoneyAccountDeposit = async ({ + messenger, + historyItem: { depositConfig, quote, account }, + txHash, + addTransactionBatchFn, +}: { + messenger: BridgeStatusControllerMessenger; + historyItem: BridgeHistoryItem; + txHash: string; + addTransactionBatchFn: typeof TransactionController.prototype.addTransactionBatch; +}) => { + if (!depositConfig) { + return; + } + + // Get actual received amount from receipt + const provider = getNetworkProviderByChainId(messenger, quote.destChainId); + if (!provider) { + throw new Error(`No provider available for chain ${quote.destChainId}`); + } + const ethersProvider = new Web3Provider(provider); + const txReceipt = await ethersProvider.getTransactionReceipt(txHash); + const receivedAmount = getReceivedERC20Amount( + quote.destWalletAddress ?? quote.walletAddress ?? account, + quote.destAsset.address, + txReceipt?.logs, + ); + + if (!receivedAmount) { + throw new Error(`Missing received amount`); + } + + const { + boringVault, + tellerAddress, + accountantAddress, + lensAddress, + moneyAccountAddress, + chainIdHex, + isGasFeeSponsored, + } = depositConfig; + + // Build txs + const txs = buildMoneyAccountDepositBatch({ + amount: BigInt(receivedAmount.toNumber()), + chainId: formatChainIdToHex(quote.destChainId), + provider: ethersProvider, + boringVault, + tellerAddress, + accountantAddress, + lensAddress, + }); + + const networkClientId = getNetworkClientIdByChainId(messenger, chainIdHex); + + // Add tx batch + return await addTransactionBatchFn({ + // batchId, + disableHook: true, + disableSequential: true, + disableUpgrade: true, + from: moneyAccountAddress, + isGasFeeSponsored, + isInternal: true, + networkClientId, + origin: ORIGIN_METAMASK, + requireApproval: false, + requiredAssets: [ + { + address: getMoneyAccountDepositAssetAddress(chainIdHex), + amount: '0x0', + standard: 'erc20', + }, + ], + skipInitialGasEstimate: true, + transactions: Object.values(txs), + }); +}; diff --git a/packages/bridge-status-controller/src/utils/swap-received-amount.ts b/packages/bridge-status-controller/src/utils/swap-received-amount.ts index 9827fb41c74..d5284af4d64 100644 --- a/packages/bridge-status-controller/src/utils/swap-received-amount.ts +++ b/packages/bridge-status-controller/src/utils/swap-received-amount.ts @@ -28,30 +28,28 @@ const getReceivedNativeAmount = ( : null; }; -const getReceivedERC20Amount = ( - historyItem: BridgeHistoryItem, - txMeta: TransactionMeta, +export const getReceivedERC20Amount = ( + walletAddress: string, + destAssetAddress: string, + logs?: { address?: string; topics?: string[]; data?: string }[], ) => { - const { txReceipt } = txMeta; - if (!txReceipt?.logs || txReceipt.status === '0x0') { + if (!logs) { return null; } - const { account: accountAddress, quote } = historyItem; const TOKEN_TRANSFER_LOG_TOPIC_HASH = '0xddf252ad1be2c89b69c2b068fc378daa952ba7f163c4a11628f55a4df523b3ef'; - const tokenTransferLog = txReceipt.logs.find((txReceiptLog) => { + const tokenTransferLog = logs.find((txReceiptLog) => { const isTokenTransfer = txReceiptLog.topics?.[0]?.startsWith( TOKEN_TRANSFER_LOG_TOPIC_HASH, ); const isTransferFromGivenToken = - txReceiptLog.address?.toLowerCase() === - quote.destAsset.address?.toLowerCase(); + txReceiptLog.address?.toLowerCase() === destAssetAddress?.toLowerCase(); const isTransferFromGivenAddress = txReceiptLog.topics?.[2] && - (txReceiptLog.topics[2] === accountAddress || - txReceiptLog.topics[2].match(accountAddress?.slice(2))); + (txReceiptLog.topics[2] === walletAddress || + txReceiptLog.topics[2].match(walletAddress?.slice(2))); return ( isTokenTransfer && isTransferFromGivenToken && isTransferFromGivenAddress @@ -59,9 +57,7 @@ const getReceivedERC20Amount = ( }); if (tokenTransferLog?.data) { - return new BigNumber(tokenTransferLog.data, 16).div( - new BigNumber(10).pow(quote.destAsset.decimals), - ); + return new BigNumber(tokenTransferLog.data, 16); } return null; @@ -83,7 +79,7 @@ export const getActualSwapReceivedAmount = ( const { pricingData } = historyItem; const quotedReturnAmount = historyItem.quote.destTokenAmount; - if (!txMeta?.txReceipt) { + if (!txMeta?.txReceipt || txMeta?.txReceipt.status === '0x0') { return null; } @@ -91,7 +87,12 @@ export const getActualSwapReceivedAmount = ( historyItem.quote.destAsset.address, ) ? getReceivedNativeAmount(historyItem, actualGas, txMeta) - : getReceivedERC20Amount(historyItem, txMeta); + : getReceivedERC20Amount( + historyItem.account, + historyItem.quote.destAsset.address, + // @ts-expect-error - txReceipt.logs is not typed correctly + txMeta?.txReceipt?.logs, + )?.div(new BigNumber(10).pow(historyItem.quote.destAsset.decimals)); const returnUsdExchangeRate = pricingData?.quotedReturnInUsd && quotedReturnAmount diff --git a/packages/bridge-status-controller/tsconfig.build.json b/packages/bridge-status-controller/tsconfig.build.json index 53f5aa7df7e..00f0553720e 100644 --- a/packages/bridge-status-controller/tsconfig.build.json +++ b/packages/bridge-status-controller/tsconfig.build.json @@ -40,6 +40,12 @@ }, { "path": "../utils/tsconfig.build.json" + }, + { + "path": "../money-account-utils/tsconfig.build.json" + }, + { + "path": "../money-account-controller/tsconfig.build.json" } ], "include": ["../../types", "./src"] diff --git a/packages/bridge-status-controller/tsconfig.json b/packages/bridge-status-controller/tsconfig.json index e3577be88f0..5591c00e0d9 100644 --- a/packages/bridge-status-controller/tsconfig.json +++ b/packages/bridge-status-controller/tsconfig.json @@ -39,6 +39,12 @@ }, { "path": "../utils" + }, + { + "path": "../money-account-utils" + }, + { + "path": "../money-account-controller" } ], "include": ["../../types", "./src", "./test"] From 9647c80ec6d5ab8d11584e7653964758870102c0 Mon Sep 17 00:00:00 2001 From: micaelae Date: Wed, 28 Oct 2026 15:38:43 -0700 Subject: [PATCH 07/11] append --- .../bridge-controller/src/utils/controllers/money-account.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/bridge-controller/src/utils/controllers/money-account.ts b/packages/bridge-controller/src/utils/controllers/money-account.ts index 3de3d3463ed..fd6119151ee 100644 --- a/packages/bridge-controller/src/utils/controllers/money-account.ts +++ b/packages/bridge-controller/src/utils/controllers/money-account.ts @@ -64,7 +64,7 @@ export const getMoneyAccountDepositSetup = async ({ tellerAddress, accountantAddress, lensAddress, - moneyAccountAddress: primaryMoneyAccount.address, + moneyAccountAddress: primaryMoneyAccount.address as `0x${string}`, isGasFeeSponsored: CHAIN_IDS.MONAD === chainId, }; }; From f5d806c717cb69e86206874645738a4b9817a6fa Mon Sep 17 00:00:00 2001 From: micaelae Date: Wed, 28 Oct 2026 15:46:12 -0700 Subject: [PATCH 08/11] fix: add depositConfig to history (submission) --- .../src/bridge-status-controller.test.ts | 4 ++-- .../src/bridge-status-controller.ts | 10 ++-------- packages/bridge-status-controller/src/index.ts | 1 - packages/bridge-status-controller/src/types.ts | 16 ++-------------- .../src/utils/history.test.ts | 4 ++-- .../src/utils/history.ts | 9 ++++----- 6 files changed, 12 insertions(+), 32 deletions(-) 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 97ad4444b82..8bbfa1e1c4e 100644 --- a/packages/bridge-status-controller/src/bridge-status-controller.test.ts +++ b/packages/bridge-status-controller/src/bridge-status-controller.test.ts @@ -63,7 +63,7 @@ import { QuoteStatusManager } from './quote-status-manager/quotes-status-manager import { BridgeClientId } from './types.js'; import type { BridgeId, - StartPollingForBridgeTxStatusArgsSerialized, + StartPollingForBridgeTxStatusArgs, BridgeHistoryItem, BridgeStatusControllerState, BridgeStatusControllerMessenger, @@ -317,7 +317,7 @@ const getMockStartPollingForBridgeTxStatusArgs = ({ srcChainId = 42161, destChainId = 10, isStxEnabled = false, -} = {}): StartPollingForBridgeTxStatusArgsSerialized => ({ +} = {}): StartPollingForBridgeTxStatusArgs => ({ bridgeTxMeta: { id: txMetaId, hash: srcTxHash === 'undefined' ? undefined : srcTxHash, diff --git a/packages/bridge-status-controller/src/bridge-status-controller.ts b/packages/bridge-status-controller/src/bridge-status-controller.ts index 2da62b64135..f215e0f5035 100644 --- a/packages/bridge-status-controller/src/bridge-status-controller.ts +++ b/packages/bridge-status-controller/src/bridge-status-controller.ts @@ -67,7 +67,7 @@ import { SubmitStep } from './strategy/types.js'; import type { SubmitStrategyParams } from './strategy/types.js'; import type { BridgeStatusControllerState, - StartPollingForBridgeTxStatusArgsSerialized, + StartPollingForBridgeTxStatusArgs, FetchFunction, BridgeHistoryItem, } from './types.js'; @@ -941,7 +941,7 @@ export class BridgeStatusController extends StaticIntervalPollingController { const { bridgeTxMeta } = txHistoryMeta; @@ -1401,8 +1401,6 @@ export class BridgeStatusController extends StaticIntervalPollingController & QuoteMetadata)[]; isStxEnabled: boolean; - isMoneyAccountDeposit?: boolean; quotesReceivedContext?: RequiredEventContextFromClient[UnifiedSwapBridgeEventName.QuotesReceived]; location?: MetaMetricsSwapsEventSource; abTests?: Record; @@ -1687,7 +1682,6 @@ export class BridgeStatusController extends StaticIntervalPollingController & { - quoteResponse: QuoteResponseV1 & QuoteMetadata; }; export type SourceChainTxMetaId = string; diff --git a/packages/bridge-status-controller/src/utils/history.test.ts b/packages/bridge-status-controller/src/utils/history.test.ts index a7d6563abdb..31e35d514b5 100644 --- a/packages/bridge-status-controller/src/utils/history.test.ts +++ b/packages/bridge-status-controller/src/utils/history.test.ts @@ -13,7 +13,7 @@ import { describe, expect, it } from 'vitest'; import type { BridgeStatusControllerState, BridgeHistoryItem, - StartPollingForBridgeTxStatusArgsSerialized, + StartPollingForBridgeTxStatusArgs, StatusResponse, } from '../types.js'; import { @@ -140,7 +140,7 @@ describe('History Utils', () => { slippagePercentage: 0, accountAddress: '0xaccount', isStxEnabled: false, - } as unknown as StartPollingForBridgeTxStatusArgsSerialized; + } as unknown as StartPollingForBridgeTxStatusArgs; it('omits tokenSecurityTypeDestination when not provided', () => { const txHistoryItem = getInitialHistoryItem(baseArgs); diff --git a/packages/bridge-status-controller/src/utils/history.ts b/packages/bridge-status-controller/src/utils/history.ts index 2f571807871..38a1120b664 100644 --- a/packages/bridge-status-controller/src/utils/history.ts +++ b/packages/bridge-status-controller/src/utils/history.ts @@ -11,7 +11,7 @@ import type { BridgeHistoryItem, BridgeStatusControllerMessenger, BridgeStatusControllerState, - StartPollingForBridgeTxStatusArgsSerialized, + StartPollingForBridgeTxStatusArgs, } from '../types.js'; import { getMaxPendingHistoryItemAgeMs } from './feature-flags.js'; import { isApprovalTx, isAtomic7702BatchTx } from './transaction.js'; @@ -249,7 +249,7 @@ export function getHistoryKey( } export const getInitialHistoryItem = ( - args: StartPollingForBridgeTxStatusArgsSerialized, + args: StartPollingForBridgeTxStatusArgs, ): BridgeHistoryItem => { const { bridgeTxMeta, @@ -267,7 +267,6 @@ export const getInitialHistoryItem = ( accountAddress: selectedAddress, originalTransactionId, actionId, - depositConfig, tokenSecurityTypeDestination, inputPrimaryDenomination, batchSellData, @@ -329,8 +328,8 @@ export const getInitialHistoryItem = ( }), }; - if (depositConfig) { - txHistoryItem.depositConfig = depositConfig; + if (quoteResponse.depositConfig) { + txHistoryItem.depositConfig = quoteResponse.depositConfig; } if (batchSellData) { From 3175af7d677280740a5cbb20919014a1cb5a59c4 Mon Sep 17 00:00:00 2001 From: micaelae Date: Wed, 28 Oct 2026 16:29:56 -0700 Subject: [PATCH 09/11] fix: dedupe deposit confirmations by depositBatchId (submission) --- .../src/bridge-status-controller.ts | 15 ++++++- .../bridge-status-controller/src/types.ts | 5 +++ .../src/utils/history.ts | 7 +++- .../src/utils/money-account.ts | 6 +-- .../src/utils/transaction.ts | 42 +++++++++++++++++-- 5 files changed, 67 insertions(+), 8 deletions(-) diff --git a/packages/bridge-status-controller/src/bridge-status-controller.ts b/packages/bridge-status-controller/src/bridge-status-controller.ts index f215e0f5035..79c2a0ddfe3 100644 --- a/packages/bridge-status-controller/src/bridge-status-controller.ts +++ b/packages/bridge-status-controller/src/bridge-status-controller.ts @@ -124,6 +124,7 @@ import { isCrossChainTx, updateTransactionsInBatch, hasNestedSwapTransactions, + checkIsBatchIdSubmitted, } from './utils/transaction.js'; import { handleMoneyAccountDeposit } from './utils/money-account.js'; @@ -272,7 +273,7 @@ export class BridgeStatusController extends StaticIntervalPollingController( @@ -331,6 +332,18 @@ export class BridgeStatusController extends StaticIntervalPollingController + messenger.call('TransactionController:getState').batchTransactionCounts[ + batchId + ]; + +const getTransactionMetaByBatchId = ({ + messenger, + batchId, +}: { + messenger: BridgeStatusControllerMessenger; + batchId: string; +}): TransactionMeta[] => + getTransactions(messenger).filter( + (tx: TransactionMeta) => tx.batchId === batchId, + ); + +export const checkIsBatchIdSubmitted = ( + messenger: BridgeStatusControllerMessenger, + batchId: `0x${string}`, +) => { + // If > 0, batch submission is in progress + const batchTransactionCounts = getBatchTransactionCounts(messenger, batchId); + if (batchTransactionCounts) { + return true; + } + // If length > 0, batch has been approved + const transactions = getTransactionMetaByBatchId({ + messenger, + batchId, + }); + if (transactions.length > 0) { + return true; + } +}; + const waitForHashAndReturnFinalTxMeta = async ( messenger: BridgeStatusControllerMessenger, hashPromise?: Awaited< @@ -540,9 +578,7 @@ export const findAllTransactionsInBatch = ({ tradeData: QuoteAndTxMetadata[]; }): QuoteAndTxMetadata[] => { // Filter for transactions with batchId - const txs = getTransactions(messenger).filter( - (tx: TransactionMeta) => tx.batchId === batchId, - ); + const txs = getTransactionMetaByBatchId({ messenger, batchId }); return tradeData.map((tradeWithMetadata) => { const { tx, type } = tradeWithMetadata; From 3e3abff292ebea85657f9d0251dd1d557ca41ede Mon Sep 17 00:00:00 2001 From: micaelae Date: Wed, 28 Oct 2026 16:40:58 -0700 Subject: [PATCH 10/11] fix: catch and capture deposit exceptions --- .../src/bridge-status-controller.ts | 8 +- .../src/utils/money-account.ts | 123 ++++++++++-------- 2 files changed, 70 insertions(+), 61 deletions(-) diff --git a/packages/bridge-status-controller/src/bridge-status-controller.ts b/packages/bridge-status-controller/src/bridge-status-controller.ts index 79c2a0ddfe3..268ed10ec98 100644 --- a/packages/bridge-status-controller/src/bridge-status-controller.ts +++ b/packages/bridge-status-controller/src/bridge-status-controller.ts @@ -494,7 +494,7 @@ export class BridgeStatusController extends StaticIntervalPollingController { + }): Promise => { // Return early if the confirmed txMeta is for an approval since we // still need to wait for the trade to be confirmed if (isApprovalTxMeta) { @@ -552,7 +552,7 @@ export class BridgeStatusController extends StaticIntervalPollingController undefined); - if (historyItem && historyItem.depositConfig && txMeta.hash) { + if (historyItem && txMeta.hash) { handleMoneyAccountDeposit({ messenger: this.messenger, historyItem, @@ -1243,7 +1243,7 @@ export class BridgeStatusController extends StaticIntervalPollingController Date: Wed, 28 Oct 2026 17:09:57 -0700 Subject: [PATCH 11/11] fix: await deposit batch and case insensitive transfer match --- .../src/utils/money-account.ts | 2 +- .../src/utils/swap-received-amount.ts | 12 +++++++----- 2 files changed, 8 insertions(+), 6 deletions(-) diff --git a/packages/bridge-status-controller/src/utils/money-account.ts b/packages/bridge-status-controller/src/utils/money-account.ts index f039f5bc736..a33a4c7e240 100644 --- a/packages/bridge-status-controller/src/utils/money-account.ts +++ b/packages/bridge-status-controller/src/utils/money-account.ts @@ -58,7 +58,7 @@ export const handleMoneyAccountDeposit = async ({ } = depositConfig; // Build txs - const txs = buildMoneyAccountDepositBatch({ + const txs = await buildMoneyAccountDepositBatch({ amount: BigInt(receivedAmount.toNumber()), chainId: formatChainIdToHex(quote.destChainId), provider: ethersProvider, diff --git a/packages/bridge-status-controller/src/utils/swap-received-amount.ts b/packages/bridge-status-controller/src/utils/swap-received-amount.ts index d5284af4d64..272888b08ab 100644 --- a/packages/bridge-status-controller/src/utils/swap-received-amount.ts +++ b/packages/bridge-status-controller/src/utils/swap-received-amount.ts @@ -41,15 +41,17 @@ export const getReceivedERC20Amount = ( '0xddf252ad1be2c89b69c2b068fc378daa952ba7f163c4a11628f55a4df523b3ef'; const tokenTransferLog = logs.find((txReceiptLog) => { - const isTokenTransfer = txReceiptLog.topics?.[0]?.startsWith( - TOKEN_TRANSFER_LOG_TOPIC_HASH, - ); + const isTokenTransfer = txReceiptLog.topics?.[0] + ?.toLowerCase() + .startsWith(TOKEN_TRANSFER_LOG_TOPIC_HASH); const isTransferFromGivenToken = txReceiptLog.address?.toLowerCase() === destAssetAddress?.toLowerCase(); const isTransferFromGivenAddress = txReceiptLog.topics?.[2] && - (txReceiptLog.topics[2] === walletAddress || - txReceiptLog.topics[2].match(walletAddress?.slice(2))); + (txReceiptLog.topics[2].toLowerCase() === walletAddress?.toLowerCase() || + txReceiptLog.topics[2] + .toLowerCase() + .match(walletAddress?.slice(2).toLowerCase())); return ( isTokenTransfer && isTransferFromGivenToken && isTransferFromGivenAddress