From 6e407d3e1e43e72142b1fb55bd9554ce6b0daf0d Mon Sep 17 00:00:00 2001 From: Bernardo Garces Chapero Date: Tue, 10 Mar 2026 11:59:45 +0000 Subject: [PATCH 01/14] feature flag validation --- .../transaction-pay-controller/package.json | 1 + .../src/tests/messenger-mock.ts | 11 ++ .../transaction-pay-controller/src/types.ts | 7 + .../src/utils/feature-flags.test.ts | 123 +++++++++++++++++- .../src/utils/feature-flags.ts | 51 +++++++- .../src/utils/token.ts | 5 + .../tsconfig.build.json | 3 + .../transaction-pay-controller/tsconfig.json | 3 + yarn.lock | 3 +- 9 files changed, 203 insertions(+), 4 deletions(-) diff --git a/packages/transaction-pay-controller/package.json b/packages/transaction-pay-controller/package.json index fc83d598788..164fb3da92d 100644 --- a/packages/transaction-pay-controller/package.json +++ b/packages/transaction-pay-controller/package.json @@ -51,6 +51,7 @@ "@ethersproject/abi": "^5.7.0", "@ethersproject/contracts": "^5.7.0", "@ethersproject/providers": "^5.7.0", + "@metamask/app-metadata-controller": "^2.0.0", "@metamask/assets-controllers": "^100.2.0", "@metamask/base-controller": "^9.0.0", "@metamask/bridge-controller": "^69.0.0", diff --git a/packages/transaction-pay-controller/src/tests/messenger-mock.ts b/packages/transaction-pay-controller/src/tests/messenger-mock.ts index b6d69d4f22d..f70321e207e 100644 --- a/packages/transaction-pay-controller/src/tests/messenger-mock.ts +++ b/packages/transaction-pay-controller/src/tests/messenger-mock.ts @@ -1,3 +1,4 @@ +import type { AppMetadataControllerGetStateAction } from '@metamask/app-metadata-controller'; import type { TokensControllerGetStateAction } from '@metamask/assets-controllers'; import type { TokenBalancesControllerGetStateAction } from '@metamask/assets-controllers'; import type { TokenRatesControllerGetStateAction } from '@metamask/assets-controllers'; @@ -45,6 +46,10 @@ type RootMessenger = Messenger; export function getMessengerMock({ skipRegister, }: { skipRegister?: boolean } = {}) { + const getAppMetadataControllerStateMock: jest.MockedFn< + AppMetadataControllerGetStateAction['handler'] + > = jest.fn(); + const getControllerStateMock: jest.MockedFn< TransactionPayControllerGetStateAction['handler'] > = jest.fn(); @@ -241,12 +246,18 @@ export function getMessengerMock({ 'TransactionController:estimateGasBatch', estimateGasBatchMock, ); + + messenger.registerActionHandler( + 'AppMetadataController:getState', + getAppMetadataControllerStateMock, + ); } const publish = messenger.publish.bind(messenger); return { addTransactionMock, + getAppMetadataControllerStateMock, addTransactionBatchMock, estimateGasMock, estimateGasBatchMock, diff --git a/packages/transaction-pay-controller/src/types.ts b/packages/transaction-pay-controller/src/types.ts index ff99540dac5..dd4d9d1c3be 100644 --- a/packages/transaction-pay-controller/src/types.ts +++ b/packages/transaction-pay-controller/src/types.ts @@ -1,3 +1,5 @@ +import type { AppMetadataControllerGetStateAction } from '@metamask/app-metadata-controller'; +import type { AssetsControllerGetStateAction } from '@metamask/assets-controller'; import type { CurrencyRateControllerActions, TokenBalancesControllerGetStateAction, @@ -38,6 +40,8 @@ import type { CONTROLLER_NAME, TransactionPayStrategy } from './constants'; export type AllowedActions = | AccountTrackerControllerGetStateAction + | AppMetadataControllerGetStateAction + | AssetsControllerGetStateAction | BridgeControllerActions | BridgeStatusControllerActions | CurrencyRateControllerActions @@ -159,6 +163,9 @@ export type TransactionPayControllerOptions = { /** Callback to select ordered PayStrategies for a transaction. */ getStrategies?: (transaction: TransactionMeta) => TransactionPayStrategy[]; + /** Callback to determine whether to use AssetsController for state. */ + getUseAssetsController?: () => boolean; + /** Controller messenger. */ messenger: TransactionPayControllerMessenger; diff --git a/packages/transaction-pay-controller/src/utils/feature-flags.test.ts b/packages/transaction-pay-controller/src/utils/feature-flags.test.ts index 8711713267c..7ed1feca597 100644 --- a/packages/transaction-pay-controller/src/utils/feature-flags.test.ts +++ b/packages/transaction-pay-controller/src/utils/feature-flags.test.ts @@ -9,6 +9,7 @@ import { DEFAULT_RELAY_QUOTE_URL, DEFAULT_SLIPPAGE, DEFAULT_STRATEGY_ORDER, + getAssetsUnifyStateFeature, getFallbackGas, DEFAULT_RELAY_EXECUTE_URL, getRelayOriginGasOverhead, @@ -38,8 +39,11 @@ const TOKEN_ADDRESS_DIFFERENT_MOCK = '0xdef789abc012' as Hex; const TOKEN_SPECIFIC_SLIPPAGE_MOCK = 0.02; describe('Feature Flags Utils', () => { - const { messenger, getRemoteFeatureFlagControllerStateMock } = - getMessengerMock(); + const { + messenger, + getAppMetadataControllerStateMock, + getRemoteFeatureFlagControllerStateMock, + } = getMessengerMock(); beforeEach(() => { jest.resetAllMocks(); @@ -562,6 +566,121 @@ describe('Feature Flags Utils', () => { }); }); + describe('getAssetsUnifyStateFeature', () => { + it('returns false when assetsUnifyState is not set', () => { + const result = getAssetsUnifyStateFeature(messenger); + + expect(result).toBe(false); + }); + + it('returns false when assetsUnifyState.enabled is false', () => { + getRemoteFeatureFlagControllerStateMock.mockReturnValue({ + ...getDefaultRemoteFeatureFlagControllerState(), + remoteFeatureFlags: { + confirmations_pay: { + assetsUnifyState: { + enabled: false, + featureVersion: '1', + minimumVersion: null, + }, + }, + }, + }); + + const result = getAssetsUnifyStateFeature(messenger); + + expect(result).toBe(false); + }); + + it('returns false when featureVersion does not match expected version', () => { + getRemoteFeatureFlagControllerStateMock.mockReturnValue({ + ...getDefaultRemoteFeatureFlagControllerState(), + remoteFeatureFlags: { + confirmations_pay: { + assetsUnifyState: { + enabled: true, + featureVersion: '2', + minimumVersion: null, + }, + }, + }, + }); + + const result = getAssetsUnifyStateFeature(messenger); + + expect(result).toBe(false); + }); + + it('returns true when minimumVersion is null', () => { + getRemoteFeatureFlagControllerStateMock.mockReturnValue({ + ...getDefaultRemoteFeatureFlagControllerState(), + remoteFeatureFlags: { + confirmations_pay: { + assetsUnifyState: { + enabled: true, + featureVersion: '1', + minimumVersion: null, + }, + }, + }, + }); + + const result = getAssetsUnifyStateFeature(messenger); + + expect(result).toBe(true); + }); + + it('returns false when app version does not satisfy minimumVersion', () => { + getRemoteFeatureFlagControllerStateMock.mockReturnValue({ + ...getDefaultRemoteFeatureFlagControllerState(), + remoteFeatureFlags: { + confirmations_pay: { + assetsUnifyState: { + enabled: true, + featureVersion: '1', + minimumVersion: '2.0.0', + }, + }, + }, + }); + getAppMetadataControllerStateMock.mockReturnValue({ + currentAppVersion: '1.0.0', + previousAppVersion: '', + previousMigrationVersion: 0, + currentMigrationVersion: 0, + }); + + const result = getAssetsUnifyStateFeature(messenger); + + expect(result).toBe(false); + }); + + it('returns true when app version satisfies minimumVersion', () => { + getRemoteFeatureFlagControllerStateMock.mockReturnValue({ + ...getDefaultRemoteFeatureFlagControllerState(), + remoteFeatureFlags: { + confirmations_pay: { + assetsUnifyState: { + enabled: true, + featureVersion: '1', + minimumVersion: '1.0.0', + }, + }, + }, + }); + getAppMetadataControllerStateMock.mockReturnValue({ + currentAppVersion: '2.0.0', + previousAppVersion: '', + previousMigrationVersion: 0, + currentMigrationVersion: 0, + }); + + const result = getAssetsUnifyStateFeature(messenger); + + expect(result).toBe(true); + }); + }); + describe('getStrategyOrder', () => { it('returns default strategy order when none is set', () => { const strategyOrder = getStrategyOrder(messenger); diff --git a/packages/transaction-pay-controller/src/utils/feature-flags.ts b/packages/transaction-pay-controller/src/utils/feature-flags.ts index 1307150cad0..1e4cfb5d817 100644 --- a/packages/transaction-pay-controller/src/utils/feature-flags.ts +++ b/packages/transaction-pay-controller/src/utils/feature-flags.ts @@ -1,5 +1,10 @@ import type { Hex } from '@metamask/utils'; -import { createModuleLogger } from '@metamask/utils'; +import { + createModuleLogger, + isValidSemVerRange, + isValidSemVerVersion, + satisfiesVersionRange, +} from '@metamask/utils'; import { uniq } from 'lodash'; import type { TransactionPayControllerMessenger } from '..'; @@ -28,6 +33,11 @@ export const DEFAULT_STRATEGY_ORDER: StrategyOrder = [ ]; type FeatureFlagsRaw = { + assetsUnifyState?: { + enabled: boolean; + featureVersion: string | null; + minimumVersion: string | null; + }; gasBuffer?: { default?: number; perChainConfig?: Record< @@ -297,6 +307,45 @@ export function getSlippage( return slippage; } +export function getAssetsUnifyStateFeature( + messenger: TransactionPayControllerMessenger, +): boolean { + const featureFlags = getFeatureFlagsRaw(messenger); + const { assetsUnifyState } = featureFlags; + + if (!assetsUnifyState?.enabled) { + return false; + } + + const AssetsUnifyStateFeatureVersion = '1'; + + return ( + assetsUnifyState.featureVersion === AssetsUnifyStateFeatureVersion && + hasMinimumRequiredVersion(messenger, assetsUnifyState.minimumVersion) + ); +} + +function hasMinimumRequiredVersion( + messenger: TransactionPayControllerMessenger, + minRequiredVersion: string | null, +): boolean { + if (!minRequiredVersion) { + return true; + } + + const appVersion = messenger.call( + 'AppMetadataController:getState', + )?.currentAppVersion; + + const semverRange = `>=${minRequiredVersion}`; + + return ( + isValidSemVerVersion(appVersion) && + isValidSemVerRange(semverRange) && + satisfiesVersionRange(appVersion, semverRange) + ); +} + /** * Get a value from a record using a case-insensitive key lookup. * diff --git a/packages/transaction-pay-controller/src/utils/token.ts b/packages/transaction-pay-controller/src/utils/token.ts index cc3d33f448c..45f236e8c39 100644 --- a/packages/transaction-pay-controller/src/utils/token.ts +++ b/packages/transaction-pay-controller/src/utils/token.ts @@ -49,6 +49,8 @@ export function getTokenBalance( chainId: Hex, tokenAddress: Hex, ): string { + // TODO AssetsController use new state + const tokenBalanceControllerState = messenger.call( 'TokenBalancesController:getState', ); @@ -98,6 +100,7 @@ export function getAllTokenBalances( chainId: Hex; tokenAddress: Hex; }[] { + // TODO AssetsController use new state const tokenBalanceControllerState = messenger.call( 'TokenBalancesController:getState', ); @@ -145,6 +148,7 @@ export function getTokenInfo( tokenAddress: Hex, chainId: Hex, ): { decimals: number; symbol: string } | undefined { + // TODO AssetsController use new state const controllerState = messenger.call('TokensController:getState'); const normalizedTokenAddress = tokenAddress.toLowerCase() as Hex; @@ -188,6 +192,7 @@ export function getTokenFiatRate( tokenAddress: Hex, chainId: Hex, ): FiatRates | undefined { + // TODO AssetsController use new state const ticker = getTicker(chainId, messenger); if (!ticker) { diff --git a/packages/transaction-pay-controller/tsconfig.build.json b/packages/transaction-pay-controller/tsconfig.build.json index aa8dd9cb92e..fdba8d46462 100644 --- a/packages/transaction-pay-controller/tsconfig.build.json +++ b/packages/transaction-pay-controller/tsconfig.build.json @@ -6,6 +6,9 @@ "rootDir": "./src" }, "references": [ + { + "path": "../app-metadata-controller/tsconfig.build.json" + }, { "path": "../assets-controllers/tsconfig.build.json" }, diff --git a/packages/transaction-pay-controller/tsconfig.json b/packages/transaction-pay-controller/tsconfig.json index 0452cae3d20..c4f699133f2 100644 --- a/packages/transaction-pay-controller/tsconfig.json +++ b/packages/transaction-pay-controller/tsconfig.json @@ -4,6 +4,9 @@ "baseUrl": "./" }, "references": [ + { + "path": "../app-metadata-controller" + }, { "path": "../assets-controllers" }, diff --git a/yarn.lock b/yarn.lock index 828e068e9bd..f8eb0e8f783 100644 --- a/yarn.lock +++ b/yarn.lock @@ -2720,7 +2720,7 @@ __metadata: languageName: node linkType: hard -"@metamask/app-metadata-controller@workspace:packages/app-metadata-controller": +"@metamask/app-metadata-controller@npm:^2.0.0, @metamask/app-metadata-controller@workspace:packages/app-metadata-controller": version: 0.0.0-use.local resolution: "@metamask/app-metadata-controller@workspace:packages/app-metadata-controller" dependencies: @@ -5412,6 +5412,7 @@ __metadata: "@ethersproject/abi": "npm:^5.7.0" "@ethersproject/contracts": "npm:^5.7.0" "@ethersproject/providers": "npm:^5.7.0" + "@metamask/app-metadata-controller": "npm:^2.0.0" "@metamask/assets-controllers": "npm:^100.2.0" "@metamask/auto-changelog": "npm:^3.4.4" "@metamask/base-controller": "npm:^9.0.0" From 5b84cccc7717b9569232161a19e43bd143973ec5 Mon Sep 17 00:00:00 2001 From: Bernardo Garces Chapero Date: Tue, 10 Mar 2026 13:54:16 +0000 Subject: [PATCH 02/14] addd wrappers --- packages/assets-controller/src/index.ts | 3 - .../src/utils/formatExchangeRatesForBridge.ts | 66 ++----- packages/assets-controller/src/utils/index.ts | 7 +- .../transaction-pay-controller/src/types.ts | 4 +- .../src/utils/token.test.ts | 50 ----- .../src/utils/token.ts | 187 +++++++++++------- 6 files changed, 139 insertions(+), 178 deletions(-) diff --git a/packages/assets-controller/src/index.ts b/packages/assets-controller/src/index.ts index f6a6fbb4d06..66aa56f7a8a 100644 --- a/packages/assets-controller/src/index.ts +++ b/packages/assets-controller/src/index.ts @@ -167,10 +167,7 @@ export { } from './utils'; export type { AccountForLegacyFormat, - BridgeConversionRateEntry, - BridgeCurrencyRateEntry, BridgeExchangeRatesFormat, - BridgeMarketDataEntry, LegacyToken, TransactionPayLegacyFormat, } from './utils'; diff --git a/packages/assets-controller/src/utils/formatExchangeRatesForBridge.ts b/packages/assets-controller/src/utils/formatExchangeRatesForBridge.ts index 47d597b567d..3b15002d66f 100644 --- a/packages/assets-controller/src/utils/formatExchangeRatesForBridge.ts +++ b/packages/assets-controller/src/utils/formatExchangeRatesForBridge.ts @@ -1,51 +1,25 @@ import { toChecksumAddress } from '@ethereumjs/util'; -import { MAP_CAIP_CURRENCIES } from '@metamask/assets-controllers'; -import { KnownCaipNamespace, numberToHex } from '@metamask/utils'; +import { + CurrencyRateState, + MAP_CAIP_CURRENCIES, + MarketDataDetails, + MultichainAssetsRatesControllerState, + TokenRatesControllerState, +} from '@metamask/assets-controllers'; +import { Hex, KnownCaipNamespace, numberToHex } from '@metamask/utils'; import { parseCaipAssetType, parseCaipChainId } from '@metamask/utils'; import type { AssetPrice, FungibleAssetPrice, Caip19AssetId } from '../types'; -/** - * Bridge-compatible conversion rate entry (MultichainAssetsRatesController shape). - */ -export type BridgeConversionRateEntry = { - rate: string; - currency?: string; - conversionTime?: number; - expirationTime?: number; - marketData?: Record; -}; - -/** - * Bridge-compatible currency rate entry (CurrencyRateController shape). - */ -export type BridgeCurrencyRateEntry = { - conversionDate: number; - conversionRate: number; - usdConversionRate: number; -}; - -/** - * Bridge-compatible market data entry (TokenRatesController marketData shape). - */ -export type BridgeMarketDataEntry = { - price: number; - currency: string; - assetId?: string; - chainId?: string; - tokenAddress?: string; - [key: string]: unknown; -}; - /** * Exchange rates in the format expected by the bridge controller: * conversionRates (MultichainAssetsRatesController) + currencyRates (CurrencyRateController) * + marketData (TokenRatesController) + currentCurrency. */ export type BridgeExchangeRatesFormat = { - conversionRates: Record; - currencyRates: Record; - marketData: Record>; + conversionRates: MultichainAssetsRatesControllerState['conversionRates']; + currencyRates: CurrencyRateState['currencyRates']; + marketData: TokenRatesControllerState['marketData']; currentCurrency: string; }; @@ -74,9 +48,10 @@ export function formatExchangeRatesForBridge(params: { nativeAssetIdentifiers = {}, networkConfigurationsByChainId = {}, } = params; - const conversionRates: Record = {}; - const currencyRates: Record = {}; - const marketData: Record> = {}; + const conversionRates: MultichainAssetsRatesControllerState['conversionRates'] = + {}; + const currencyRates: CurrencyRateState['currencyRates'] = {}; + const marketData: TokenRatesControllerState['marketData'] = {}; const currencyCaip = MAP_CAIP_CURRENCIES[selectedCurrency.toLowerCase()]; if (!currencyCaip) { @@ -133,7 +108,7 @@ export function formatExchangeRatesForBridge(params: { continue; } - let tokenAddress: string | undefined; + let tokenAddress: Hex | undefined; if (parsed.assetNamespace === 'erc20') { tokenAddress = toChecksumAddress(String(parsed.assetReference)); } else if (parsed.assetNamespace === 'slip44') { @@ -153,7 +128,7 @@ export function formatExchangeRatesForBridge(params: { assetId, chainId: chainIdHex, tokenAddress, - }; + } as MarketDataDetails; } if (parsed.assetNamespace === 'slip44' && nativeAssetId) { @@ -164,13 +139,14 @@ export function formatExchangeRatesForBridge(params: { }; } } else { - conversionRates[assetId] = { + conversionRates[assetId as Caip19AssetId] = { rate: String(price), currency: currencyCaip, conversionTime: lastUpdatedInSeconds, expirationTime, - marketData: priceData, - }; + marketData: + priceData as unknown as MultichainAssetsRatesControllerState['conversionRates'][Caip19AssetId]['marketData'], + } as MultichainAssetsRatesControllerState['conversionRates'][Caip19AssetId]; } } catch { // Skip malformed asset IDs diff --git a/packages/assets-controller/src/utils/index.ts b/packages/assets-controller/src/utils/index.ts index 708ec658402..9d6551daa7b 100644 --- a/packages/assets-controller/src/utils/index.ts +++ b/packages/assets-controller/src/utils/index.ts @@ -1,12 +1,7 @@ export { normalizeAssetId } from './normalizeAssetId'; export { formatExchangeRatesForBridge } from './formatExchangeRatesForBridge'; export { formatStateForTransactionPay } from './formatStateForTransactionPay'; -export type { - BridgeConversionRateEntry, - BridgeCurrencyRateEntry, - BridgeExchangeRatesFormat, - BridgeMarketDataEntry, -} from './formatExchangeRatesForBridge'; +export type { BridgeExchangeRatesFormat } from './formatExchangeRatesForBridge'; export type { AccountForLegacyFormat, LegacyToken, diff --git a/packages/transaction-pay-controller/src/types.ts b/packages/transaction-pay-controller/src/types.ts index dd4d9d1c3be..4c7faa8be6d 100644 --- a/packages/transaction-pay-controller/src/types.ts +++ b/packages/transaction-pay-controller/src/types.ts @@ -1,5 +1,5 @@ import type { AppMetadataControllerGetStateAction } from '@metamask/app-metadata-controller'; -import type { AssetsControllerGetStateAction } from '@metamask/assets-controller'; +import type { AssetsControllerGetStateForTransactionPayAction } from '@metamask/assets-controller'; import type { CurrencyRateControllerActions, TokenBalancesControllerGetStateAction, @@ -41,7 +41,7 @@ import type { CONTROLLER_NAME, TransactionPayStrategy } from './constants'; export type AllowedActions = | AccountTrackerControllerGetStateAction | AppMetadataControllerGetStateAction - | AssetsControllerGetStateAction + | AssetsControllerGetStateForTransactionPayAction | BridgeControllerActions | BridgeStatusControllerActions | CurrencyRateControllerActions diff --git a/packages/transaction-pay-controller/src/utils/token.test.ts b/packages/transaction-pay-controller/src/utils/token.test.ts index 46a0a03017b..753e14f1e5b 100644 --- a/packages/transaction-pay-controller/src/utils/token.test.ts +++ b/packages/transaction-pay-controller/src/utils/token.test.ts @@ -10,7 +10,6 @@ import { getTokenBalance, getTokenInfo, getTokenFiatRate, - getAllTokenBalances, getNativeToken, isSameToken, getLiveTokenBalance, @@ -630,55 +629,6 @@ describe('Token Utils', () => { }); }); - describe('getAllTokenBalances', () => { - it('returns all token balances including native token', () => { - getTokenBalanceControllerStateMock.mockReturnValue({ - tokenBalances: { - [FROM_MOCK]: { - '0x1': { - [TOKEN_ADDRESS_MOCK]: '0x10', - [TOKEN_ADDRESS_2_MOCK]: '0x20', - }, - '0x2': { - [TOKEN_ADDRESS_MOCK]: '0x30', - }, - }, - }, - }); - - getAccountTrackerControllerStateMock.mockReturnValue({ - accountsByChainId: { - '0x1': { - [FROM_MOCK]: { - balance: '0x40', - }, - }, - '0x2': { - [FROM_MOCK]: { - balance: '0x50', - }, - }, - '0x3': { - [FROM_MOCK]: { - balance: '0x60', - }, - }, - }, - }); - - const result = getAllTokenBalances(messenger, FROM_MOCK); - - expect(result).toStrictEqual([ - { chainId: '0x1', tokenAddress: TOKEN_ADDRESS_MOCK, balance: '16' }, - { chainId: '0x1', tokenAddress: TOKEN_ADDRESS_2_MOCK, balance: '32' }, - { chainId: '0x1', tokenAddress: NATIVE_TOKEN_ADDRESS, balance: '64' }, - { chainId: '0x2', tokenAddress: TOKEN_ADDRESS_MOCK, balance: '48' }, - { chainId: '0x2', tokenAddress: NATIVE_TOKEN_ADDRESS, balance: '80' }, - { chainId: '0x3', tokenAddress: NATIVE_TOKEN_ADDRESS, balance: '96' }, - ]); - }); - }); - describe('isSameToken', () => { it('returns true for same address and chain', () => { const token1 = { address: TOKEN_ADDRESS_MOCK, chainId: CHAIN_ID_MOCK }; diff --git a/packages/transaction-pay-controller/src/utils/token.ts b/packages/transaction-pay-controller/src/utils/token.ts index 45f236e8c39..8a4f1215ef8 100644 --- a/packages/transaction-pay-controller/src/utils/token.ts +++ b/packages/transaction-pay-controller/src/utils/token.ts @@ -1,11 +1,18 @@ import { Contract } from '@ethersproject/contracts'; import { Web3Provider } from '@ethersproject/providers'; +import { + AccountTrackerControllerState, + CurrencyRateState, + TokenBalancesControllerState, + TokenRatesControllerState, + TokensControllerState, +} from '@metamask/assets-controllers'; import { toChecksumHexAddress } from '@metamask/controller-utils'; import { abiERC20 } from '@metamask/metamask-eth-abis'; import type { Hex } from '@metamask/utils'; import { BigNumber } from 'bignumber.js'; -import { uniq } from 'lodash'; +import { getAssetsUnifyStateFeature } from './feature-flags'; import { CHAIN_ID_POLYGON, NATIVE_TOKEN_ADDRESS, @@ -49,18 +56,58 @@ export function getTokenBalance( chainId: Hex, tokenAddress: Hex, ): string { - // TODO AssetsController use new state + const assetsUnifyStateFeatureEnabled = getAssetsUnifyStateFeature(messenger); + + let getTokenBalances; + let getAccountsByChainId; + if (assetsUnifyStateFeatureEnabled) { + const assetsControllerState = messenger.call( + 'AssetsController:getStateForTransactionPay', + ); + + getTokenBalances = (): + | TokenBalancesControllerState['tokenBalances'] + | undefined => assetsControllerState?.tokenBalances; + getAccountsByChainId = (): + | AccountTrackerControllerState['accountsByChainId'] + | undefined => assetsControllerState?.accountsByChainId; + } else { + getTokenBalances = (): + | TokenBalancesControllerState['tokenBalances'] + | undefined => + messenger.call('TokenBalancesController:getState')?.tokenBalances; + getAccountsByChainId = (): + | AccountTrackerControllerState['accountsByChainId'] + | undefined => + messenger.call('AccountTrackerController:getState')?.accountsByChainId; + } - const tokenBalanceControllerState = messenger.call( - 'TokenBalancesController:getState', + return _getTokenBalance( + account, + chainId, + tokenAddress, + getTokenBalances, + getAccountsByChainId, ); +} +function _getTokenBalance( + account: Hex, + chainId: Hex, + tokenAddress: Hex, + getTokenBalances: () => + | TokenBalancesControllerState['tokenBalances'] + | undefined, + getAccountsByChainId: () => + | AccountTrackerControllerState['accountsByChainId'] + | undefined, +): string { const normalizedAccount = account.toLowerCase() as Hex; const normalizedTokenAddress = toChecksumHexAddress(tokenAddress) as Hex; const isNative = normalizedTokenAddress === getNativeToken(chainId); const balanceHex = - tokenBalanceControllerState.tokenBalances?.[normalizedAccount]?.[chainId]?.[ + getTokenBalances()?.[normalizedAccount]?.[chainId]?.[ normalizedTokenAddress ]; @@ -72,12 +119,7 @@ export function getTokenBalance( return new BigNumber(balanceHex, 16).toString(10); } - const accountTrackerControllerState = messenger.call( - 'AccountTrackerController:getState', - ); - - const chainAccounts = - accountTrackerControllerState.accountsByChainId?.[chainId]; + const chainAccounts = getAccountsByChainId()?.[chainId]; const checksumAccount = toChecksumHexAddress(normalizedAccount) as Hex; const nativeBalanceHex = chainAccounts?.[checksumAccount]?.balance as Hex; @@ -86,76 +128,48 @@ export function getTokenBalance( } /** - * Get the token balance for a specific account and token. + * Get the token decimals for a specific token. * * @param messenger - Controller messenger. - * @param account - Address of the account. - * @returns The token balance as a BigNumber. + * @param tokenAddress - Address of the token contract. + * @param chainId - Id of the chain. + * @returns The token decimals or undefined if the token is not found. */ -export function getAllTokenBalances( +export function getTokenInfo( messenger: TransactionPayControllerMessenger, - account: Hex, -): { - balance: string; - chainId: Hex; - tokenAddress: Hex; -}[] { - // TODO AssetsController use new state - const tokenBalanceControllerState = messenger.call( - 'TokenBalancesController:getState', - ); - - const accountTrackerControllerState = messenger.call( - 'AccountTrackerController:getState', - ); - - const nativeChainIds = Object.keys( - accountTrackerControllerState.accountsByChainId, - ) as Hex[]; - - const normalizedAccount = account.toLowerCase() as Hex; - - const balancesByTokenByChain = - tokenBalanceControllerState.tokenBalances?.[normalizedAccount]; + tokenAddress: Hex, + chainId: Hex, +): { decimals: number; symbol: string } | undefined { + const assetsUnifyStateFeatureEnabled = getAssetsUnifyStateFeature(messenger); - const tokenChainIds = Object.keys(balancesByTokenByChain) as Hex[]; - const chainIds = uniq([...tokenChainIds, ...nativeChainIds]); + let getAllTokens; + if (assetsUnifyStateFeatureEnabled) { + const assetsControllerState = messenger.call( + 'AssetsController:getStateForTransactionPay', + ); - return chainIds.flatMap((chainId) => { - const tokenAddresses = [ - ...(Object.keys(balancesByTokenByChain[chainId] ?? {}) as Hex[]), - getNativeToken(chainId), - ]; + getAllTokens = (): TokensControllerState['allTokens'] | undefined => + assetsControllerState?.allTokens; + } else { + getAllTokens = (): TokensControllerState['allTokens'] | undefined => + messenger.call('TokensController:getState')?.allTokens; + } - return tokenAddresses.map((tokenAddress) => ({ - chainId, - tokenAddress, - balance: getTokenBalance(messenger, account, chainId, tokenAddress), - })); - }); + return _getTokenInfo(messenger, tokenAddress, chainId, getAllTokens); } -/** - * Get the token decimals for a specific token. - * - * @param messenger - Controller messenger. - * @param tokenAddress - Address of the token contract. - * @param chainId - Id of the chain. - * @returns The token decimals or undefined if the token is not found. - */ -export function getTokenInfo( +function _getTokenInfo( messenger: TransactionPayControllerMessenger, tokenAddress: Hex, chainId: Hex, + getAllTokens: () => TokensControllerState['allTokens'] | undefined, ): { decimals: number; symbol: string } | undefined { - // TODO AssetsController use new state - const controllerState = messenger.call('TokensController:getState'); const normalizedTokenAddress = tokenAddress.toLowerCase() as Hex; const isNative = normalizedTokenAddress === getNativeToken(chainId).toLowerCase(); - const token = Object.values(controllerState.allTokens?.[chainId] ?? {}) + const token = Object.values(getAllTokens()?.[chainId] ?? {}) .flat() .find( (singleToken) => @@ -192,24 +206,53 @@ export function getTokenFiatRate( tokenAddress: Hex, chainId: Hex, ): FiatRates | undefined { - // TODO AssetsController use new state + const assetsUnifyStateFeatureEnabled = getAssetsUnifyStateFeature(messenger); + + let getMarketData; + let getCurrencyRates; + if (assetsUnifyStateFeatureEnabled) { + const assetsControllerState = messenger.call( + 'AssetsController:getStateForTransactionPay', + ); + + getMarketData = (): TokenRatesControllerState['marketData'] | undefined => + assetsControllerState?.marketData; + getCurrencyRates = (): CurrencyRateState['currencyRates'] | undefined => + assetsControllerState?.currencyRates; + } else { + getMarketData = (): TokenRatesControllerState['marketData'] | undefined => + messenger.call('TokenRatesController:getState')?.marketData; + getCurrencyRates = (): CurrencyRateState['currencyRates'] | undefined => + messenger.call('CurrencyRateController:getState')?.currencyRates; + } + + return _getTokenFiatRate( + messenger, + tokenAddress, + chainId, + getMarketData, + getCurrencyRates, + ); +} + +function _getTokenFiatRate( + messenger: TransactionPayControllerMessenger, + tokenAddress: Hex, + chainId: Hex, + getMarketData: () => TokenRatesControllerState['marketData'] | undefined, + getCurrencyRates: () => CurrencyRateState['currencyRates'] | undefined, +): FiatRates | undefined { const ticker = getTicker(chainId, messenger); if (!ticker) { return undefined; } - const rateControllerState = messenger.call('TokenRatesController:getState'); - - const currencyRateControllerState = messenger.call( - 'CurrencyRateController:getState', - ); - const normalizedTokenAddress = toChecksumHexAddress(tokenAddress) as Hex; const isNative = normalizedTokenAddress === getNativeToken(chainId); const tokenToNativeRate = - rateControllerState.marketData?.[chainId]?.[normalizedTokenAddress]?.price; + getMarketData()?.[chainId]?.[normalizedTokenAddress]?.price; if (tokenToNativeRate === undefined && !isNative) { return undefined; @@ -218,7 +261,7 @@ export function getTokenFiatRate( const { conversionRate: nativeToFiatRate, usdConversionRate: nativeToUsdRate, - } = currencyRateControllerState.currencyRates?.[ticker] ?? { + } = getCurrencyRates()?.[ticker] ?? { conversionRate: null, usdConversionRate: null, }; From 1e09ec5f3cff58abd40c93f6adf89847926eedf9 Mon Sep 17 00:00:00 2001 From: Bernardo Garces Chapero Date: Tue, 10 Mar 2026 14:41:15 +0000 Subject: [PATCH 03/14] fix feature flag check --- .../src/utils/feature-flags.test.ts | 50 ++++++++----------- .../src/utils/feature-flags.ts | 28 ++++++++--- 2 files changed, 41 insertions(+), 37 deletions(-) diff --git a/packages/transaction-pay-controller/src/utils/feature-flags.test.ts b/packages/transaction-pay-controller/src/utils/feature-flags.test.ts index 7ed1feca597..d349573aad8 100644 --- a/packages/transaction-pay-controller/src/utils/feature-flags.test.ts +++ b/packages/transaction-pay-controller/src/utils/feature-flags.test.ts @@ -577,12 +577,10 @@ describe('Feature Flags Utils', () => { getRemoteFeatureFlagControllerStateMock.mockReturnValue({ ...getDefaultRemoteFeatureFlagControllerState(), remoteFeatureFlags: { - confirmations_pay: { - assetsUnifyState: { - enabled: false, - featureVersion: '1', - minimumVersion: null, - }, + assetsUnifyState: { + enabled: false, + featureVersion: '1', + minimumVersion: null, }, }, }); @@ -596,12 +594,10 @@ describe('Feature Flags Utils', () => { getRemoteFeatureFlagControllerStateMock.mockReturnValue({ ...getDefaultRemoteFeatureFlagControllerState(), remoteFeatureFlags: { - confirmations_pay: { - assetsUnifyState: { - enabled: true, - featureVersion: '2', - minimumVersion: null, - }, + assetsUnifyState: { + enabled: true, + featureVersion: '2', + minimumVersion: null, }, }, }); @@ -615,12 +611,10 @@ describe('Feature Flags Utils', () => { getRemoteFeatureFlagControllerStateMock.mockReturnValue({ ...getDefaultRemoteFeatureFlagControllerState(), remoteFeatureFlags: { - confirmations_pay: { - assetsUnifyState: { - enabled: true, - featureVersion: '1', - minimumVersion: null, - }, + assetsUnifyState: { + enabled: true, + featureVersion: '1', + minimumVersion: null, }, }, }); @@ -634,12 +628,10 @@ describe('Feature Flags Utils', () => { getRemoteFeatureFlagControllerStateMock.mockReturnValue({ ...getDefaultRemoteFeatureFlagControllerState(), remoteFeatureFlags: { - confirmations_pay: { - assetsUnifyState: { - enabled: true, - featureVersion: '1', - minimumVersion: '2.0.0', - }, + assetsUnifyState: { + enabled: true, + featureVersion: '1', + minimumVersion: '2.0.0', }, }, }); @@ -659,12 +651,10 @@ describe('Feature Flags Utils', () => { getRemoteFeatureFlagControllerStateMock.mockReturnValue({ ...getDefaultRemoteFeatureFlagControllerState(), remoteFeatureFlags: { - confirmations_pay: { - assetsUnifyState: { - enabled: true, - featureVersion: '1', - minimumVersion: '1.0.0', - }, + assetsUnifyState: { + enabled: true, + featureVersion: '1', + minimumVersion: '1.0.0', }, }, }); diff --git a/packages/transaction-pay-controller/src/utils/feature-flags.ts b/packages/transaction-pay-controller/src/utils/feature-flags.ts index 1e4cfb5d817..a5d4a90ba4b 100644 --- a/packages/transaction-pay-controller/src/utils/feature-flags.ts +++ b/packages/transaction-pay-controller/src/utils/feature-flags.ts @@ -33,11 +33,6 @@ export const DEFAULT_STRATEGY_ORDER: StrategyOrder = [ ]; type FeatureFlagsRaw = { - assetsUnifyState?: { - enabled: boolean; - featureVersion: string | null; - minimumVersion: string | null; - }; gasBuffer?: { default?: number; perChainConfig?: Record< @@ -307,11 +302,23 @@ export function getSlippage( return slippage; } +/** + * Get the AssetsUnifyState feature flag state. + * + * @param messenger - Controller messenger. + * @returns True if the assets unify state feature is enabled, false otherwise. + */ export function getAssetsUnifyStateFeature( messenger: TransactionPayControllerMessenger, ): boolean { - const featureFlags = getFeatureFlagsRaw(messenger); - const { assetsUnifyState } = featureFlags; + const state = messenger.call('RemoteFeatureFlagController:getState'); + const assetsUnifyState = state.remoteFeatureFlags.assetsUnifyState as + | { + enabled: boolean; + featureVersion: string | null; + minimumVersion: string | null; + } + | undefined; if (!assetsUnifyState?.enabled) { return false; @@ -325,6 +332,13 @@ export function getAssetsUnifyStateFeature( ); } +/** + * Check if the app version satisfies the minimum required version. + * + * @param messenger - Controller messenger. + * @param minRequiredVersion - The minimum required version. + * @returns True if the app version satisfies the minimum required version, false otherwise. + */ function hasMinimumRequiredVersion( messenger: TransactionPayControllerMessenger, minRequiredVersion: string | null, From e8ab44558299f19209beef99f08b9eaefcf6e399 Mon Sep 17 00:00:00 2001 From: Bernardo Garces Chapero Date: Tue, 10 Mar 2026 15:06:28 +0000 Subject: [PATCH 04/14] refactor to simplify callbacks --- .../src/utils/token.ts | 48 ++++++++----------- 1 file changed, 21 insertions(+), 27 deletions(-) diff --git a/packages/transaction-pay-controller/src/utils/token.ts b/packages/transaction-pay-controller/src/utils/token.ts index 8a4f1215ef8..9e7a11d2fe4 100644 --- a/packages/transaction-pay-controller/src/utils/token.ts +++ b/packages/transaction-pay-controller/src/utils/token.ts @@ -142,34 +142,30 @@ export function getTokenInfo( ): { decimals: number; symbol: string } | undefined { const assetsUnifyStateFeatureEnabled = getAssetsUnifyStateFeature(messenger); - let getAllTokens; + let allTokens; if (assetsUnifyStateFeatureEnabled) { - const assetsControllerState = messenger.call( + allTokens = messenger.call( 'AssetsController:getStateForTransactionPay', - ); - - getAllTokens = (): TokensControllerState['allTokens'] | undefined => - assetsControllerState?.allTokens; + )?.allTokens; } else { - getAllTokens = (): TokensControllerState['allTokens'] | undefined => - messenger.call('TokensController:getState')?.allTokens; + allTokens = messenger.call('TokensController:getState')?.allTokens; } - return _getTokenInfo(messenger, tokenAddress, chainId, getAllTokens); + return _getTokenInfo(messenger, tokenAddress, chainId, allTokens); } function _getTokenInfo( messenger: TransactionPayControllerMessenger, tokenAddress: Hex, chainId: Hex, - getAllTokens: () => TokensControllerState['allTokens'] | undefined, + allTokens: TokensControllerState['allTokens'] | undefined, ): { decimals: number; symbol: string } | undefined { const normalizedTokenAddress = tokenAddress.toLowerCase() as Hex; const isNative = normalizedTokenAddress === getNativeToken(chainId).toLowerCase(); - const token = Object.values(getAllTokens()?.[chainId] ?? {}) + const token = Object.values(allTokens?.[chainId] ?? {}) .flat() .find( (singleToken) => @@ -208,30 +204,28 @@ export function getTokenFiatRate( ): FiatRates | undefined { const assetsUnifyStateFeatureEnabled = getAssetsUnifyStateFeature(messenger); - let getMarketData; - let getCurrencyRates; + let marketData; + let currencyRates; if (assetsUnifyStateFeatureEnabled) { const assetsControllerState = messenger.call( 'AssetsController:getStateForTransactionPay', ); - getMarketData = (): TokenRatesControllerState['marketData'] | undefined => - assetsControllerState?.marketData; - getCurrencyRates = (): CurrencyRateState['currencyRates'] | undefined => - assetsControllerState?.currencyRates; + marketData = assetsControllerState?.marketData; + currencyRates = assetsControllerState?.currencyRates; } else { - getMarketData = (): TokenRatesControllerState['marketData'] | undefined => - messenger.call('TokenRatesController:getState')?.marketData; - getCurrencyRates = (): CurrencyRateState['currencyRates'] | undefined => - messenger.call('CurrencyRateController:getState')?.currencyRates; + marketData = messenger.call('TokenRatesController:getState')?.marketData; + currencyRates = messenger.call( + 'CurrencyRateController:getState', + )?.currencyRates; } return _getTokenFiatRate( messenger, tokenAddress, chainId, - getMarketData, - getCurrencyRates, + marketData, + currencyRates, ); } @@ -239,8 +233,8 @@ function _getTokenFiatRate( messenger: TransactionPayControllerMessenger, tokenAddress: Hex, chainId: Hex, - getMarketData: () => TokenRatesControllerState['marketData'] | undefined, - getCurrencyRates: () => CurrencyRateState['currencyRates'] | undefined, + marketData: TokenRatesControllerState['marketData'] | undefined, + currencyRates: CurrencyRateState['currencyRates'] | undefined, ): FiatRates | undefined { const ticker = getTicker(chainId, messenger); @@ -252,7 +246,7 @@ function _getTokenFiatRate( const isNative = normalizedTokenAddress === getNativeToken(chainId); const tokenToNativeRate = - getMarketData()?.[chainId]?.[normalizedTokenAddress]?.price; + marketData?.[chainId]?.[normalizedTokenAddress]?.price; if (tokenToNativeRate === undefined && !isNative) { return undefined; @@ -261,7 +255,7 @@ function _getTokenFiatRate( const { conversionRate: nativeToFiatRate, usdConversionRate: nativeToUsdRate, - } = getCurrencyRates()?.[ticker] ?? { + } = currencyRates?.[ticker] ?? { conversionRate: null, usdConversionRate: null, }; From 73586b34a52c1675e343a971dac96eee0a369026 Mon Sep 17 00:00:00 2001 From: Bernardo Garces Chapero Date: Tue, 10 Mar 2026 15:23:58 +0000 Subject: [PATCH 05/14] coverage --- .../src/tests/messenger-mock.ts | 8 ++ .../src/utils/token.test.ts | 120 ++++++++++++++++++ 2 files changed, 128 insertions(+) diff --git a/packages/transaction-pay-controller/src/tests/messenger-mock.ts b/packages/transaction-pay-controller/src/tests/messenger-mock.ts index f70321e207e..c161a324884 100644 --- a/packages/transaction-pay-controller/src/tests/messenger-mock.ts +++ b/packages/transaction-pay-controller/src/tests/messenger-mock.ts @@ -132,6 +132,8 @@ export function getMessengerMock({ TransactionControllerEstimateGasBatchAction['handler'] > = jest.fn(); + const getAssetsControllerStateMock = jest.fn(); + const messenger: RootMessenger = new Messenger({ namespace: MOCK_ANY_NAMESPACE, }); @@ -251,6 +253,11 @@ export function getMessengerMock({ 'AppMetadataController:getState', getAppMetadataControllerStateMock, ); + + messenger.registerActionHandler( + 'AssetsController:getStateForTransactionPay', + getAssetsControllerStateMock, + ); } const publish = messenger.publish.bind(messenger); @@ -258,6 +265,7 @@ export function getMessengerMock({ return { addTransactionMock, getAppMetadataControllerStateMock, + getAssetsControllerStateMock, addTransactionBatchMock, estimateGasMock, estimateGasBatchMock, diff --git a/packages/transaction-pay-controller/src/utils/token.test.ts b/packages/transaction-pay-controller/src/utils/token.test.ts index 753e14f1e5b..e24338cca8d 100644 --- a/packages/transaction-pay-controller/src/utils/token.test.ts +++ b/packages/transaction-pay-controller/src/utils/token.test.ts @@ -16,6 +16,7 @@ import { normalizeTokenAddress, TokenAddressTarget, } from './token'; +import { getDefaultRemoteFeatureFlagControllerState } from '../../../remote-feature-flag-controller/src/remote-feature-flag-controller'; import { CHAIN_ID_POLYGON, NATIVE_TOKEN_ADDRESS, @@ -49,6 +50,8 @@ const PROVIDER_MOCK = { request: jest.fn() }; describe('Token Utils', () => { const { messenger, + getAssetsControllerStateMock, + getRemoteFeatureFlagControllerStateMock, getTokensControllerStateMock, getNetworkClientByIdMock, getTokenBalanceControllerStateMock, @@ -67,6 +70,10 @@ describe('Token Utils', () => { mockBalanceOf = jest.fn(); mockGetBalance = jest.fn(); + getRemoteFeatureFlagControllerStateMock.mockReturnValue({ + ...getDefaultRemoteFeatureFlagControllerState(), + }); + findNetworkClientIdByChainIdMock.mockReturnValue(NETWORK_CLIENT_ID_MOCK); getNetworkClientByIdMock.mockReturnValue({ @@ -83,7 +90,44 @@ describe('Token Utils', () => { })); }); + function enableAssetsUnifyState(): void { + getRemoteFeatureFlagControllerStateMock.mockReturnValue({ + ...getDefaultRemoteFeatureFlagControllerState(), + remoteFeatureFlags: { + assetsUnifyState: { + enabled: true, + featureVersion: '1', + minimumVersion: null, + }, + }, + }); + } + describe('getTokenInfo', () => { + it('returns decimals and symbol from AssetsController when assets unify state feature is enabled', () => { + enableAssetsUnifyState(); + getAssetsControllerStateMock.mockReturnValue({ + allTokens: { + [CHAIN_ID_MOCK]: { + test123: [ + { + address: TOKEN_ADDRESS_MOCK.toLowerCase() as Hex, + decimals: DECIMALS_MOCK, + symbol: SYMBOL_MOCK, + }, + ], + }, + }, + }); + + const result = getTokenInfo(messenger, TOKEN_ADDRESS_MOCK, CHAIN_ID_MOCK); + + expect(result).toStrictEqual({ + decimals: DECIMALS_MOCK, + symbol: SYMBOL_MOCK, + }); + }); + it('returns decimals and symbol from controller state', () => { getTokensControllerStateMock.mockReturnValue({ allTokens: { @@ -191,6 +235,51 @@ describe('Token Utils', () => { }); describe('getTokenBalance', () => { + it('returns token balance from AssetsController when assets unify state feature is enabled', () => { + enableAssetsUnifyState(); + getAssetsControllerStateMock.mockReturnValue({ + tokenBalances: { + [FROM_MOCK]: { + [CHAIN_ID_MOCK]: { + [TOKEN_ADDRESS_MOCK]: BALANCE_MOCK, + }, + }, + }, + }); + + const result = getTokenBalance( + messenger, + FROM_MOCK, + CHAIN_ID_MOCK, + TOKEN_ADDRESS_MOCK.toLowerCase() as Hex, + ); + + expect(result).toBe('291'); + }); + + it('returns native balance from AssetsController when assets unify state feature is enabled', () => { + enableAssetsUnifyState(); + getAssetsControllerStateMock.mockReturnValue({ + tokenBalances: {}, + accountsByChainId: { + [CHAIN_ID_MOCK]: { + [FROM_MOCK]: { + balance: '0x123', + }, + }, + }, + }); + + const result = getTokenBalance( + messenger, + FROM_MOCK, + CHAIN_ID_MOCK, + NATIVE_TOKEN_ADDRESS, + ); + + expect(result).toBe('291'); + }); + it('returns balance from controller state', () => { getTokenBalanceControllerStateMock.mockReturnValue({ tokenBalances: { @@ -277,6 +366,37 @@ describe('Token Utils', () => { }); describe('getTokenFiatRate', () => { + it('returns fiat rates from AssetsController when assets unify state feature is enabled', () => { + enableAssetsUnifyState(); + + getAssetsControllerStateMock.mockReturnValue({ + marketData: { + [CHAIN_ID_MOCK]: { + [TOKEN_ADDRESS_MOCK]: { + price: 2.0, + }, + }, + }, + currencyRates: { + [TICKER_MOCK]: { + conversionRate: 3.0, + usdConversionRate: 4.0, + }, + }, + }); + + const result = getTokenFiatRate( + messenger, + TOKEN_ADDRESS_MOCK, + CHAIN_ID_MOCK, + ); + + expect(result).toStrictEqual({ + fiatRate: '6', + usdRate: '8', + }); + }); + it('returns fiat rates', () => { findNetworkClientIdByChainIdMock.mockReturnValue(NETWORK_CLIENT_ID_MOCK); From 3769141f0269155c65a1ccb97527d0799663b2e1 Mon Sep 17 00:00:00 2001 From: Bernardo Garces Chapero Date: Tue, 10 Mar 2026 15:42:55 +0000 Subject: [PATCH 06/14] changelog --- packages/assets-controller/CHANGELOG.md | 5 +++++ packages/transaction-pay-controller/CHANGELOG.md | 5 +++++ packages/transaction-pay-controller/src/types.ts | 3 --- 3 files changed, 10 insertions(+), 3 deletions(-) diff --git a/packages/assets-controller/CHANGELOG.md b/packages/assets-controller/CHANGELOG.md index 8afe9e5030c..ad9c2ce59a8 100644 --- a/packages/assets-controller/CHANGELOG.md +++ b/packages/assets-controller/CHANGELOG.md @@ -7,6 +7,11 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ## [Unreleased] +### Changed + +- `BridgeExchangeRatesFormat` type now uses canonical `@metamask/assets-controllers` state types instead of locally-defined bridge rate entry types ([#8163](https://github.com/MetaMask/core/pull/8163)) + + ## [2.3.0] ### Added diff --git a/packages/transaction-pay-controller/CHANGELOG.md b/packages/transaction-pay-controller/CHANGELOG.md index a6f68b75bc3..a86cedc55ac 100644 --- a/packages/transaction-pay-controller/CHANGELOG.md +++ b/packages/transaction-pay-controller/CHANGELOG.md @@ -9,8 +9,13 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ### Added +- **BREAKING:** Add `AppMetadataControllerGetStateAction` and `AssetsControllerGetStateForTransactionPayAction` to the `AllowedActions` messenger type ([#8163](https://github.com/MetaMask/core/pull/8163)) - Support gasless Relay deposits via `execute` endpoint ([#8133](https://github.com/MetaMask/core/pull/8133)) +### Changed + +- `getTokenBalance`, `getTokenInfo`, and `getTokenFiatRate` now source token metadata, balances, and pricing from `AssetsController:getStateForTransactionPay` when the `assetsUnifyState` remote feature flag is enabled, falling back to individual controller state calls otherwise ([#8163](https://github.com/MetaMask/core/pull/8163)) + ## [16.4.1] ### Changed diff --git a/packages/transaction-pay-controller/src/types.ts b/packages/transaction-pay-controller/src/types.ts index 4c7faa8be6d..481f7b3271d 100644 --- a/packages/transaction-pay-controller/src/types.ts +++ b/packages/transaction-pay-controller/src/types.ts @@ -163,9 +163,6 @@ export type TransactionPayControllerOptions = { /** Callback to select ordered PayStrategies for a transaction. */ getStrategies?: (transaction: TransactionMeta) => TransactionPayStrategy[]; - /** Callback to determine whether to use AssetsController for state. */ - getUseAssetsController?: () => boolean; - /** Controller messenger. */ messenger: TransactionPayControllerMessenger; From 199802cc2fe78ca83c9dac8dc9f7f71712fdb95b Mon Sep 17 00:00:00 2001 From: Bernardo Garces Chapero Date: Tue, 10 Mar 2026 16:05:24 +0000 Subject: [PATCH 07/14] missing dependency --- packages/transaction-pay-controller/package.json | 1 + yarn.lock | 1 + 2 files changed, 2 insertions(+) diff --git a/packages/transaction-pay-controller/package.json b/packages/transaction-pay-controller/package.json index 78477132316..fd037bcc716 100644 --- a/packages/transaction-pay-controller/package.json +++ b/packages/transaction-pay-controller/package.json @@ -52,6 +52,7 @@ "@ethersproject/contracts": "^5.7.0", "@ethersproject/providers": "^5.7.0", "@metamask/app-metadata-controller": "^2.0.0", + "@metamask/assets-controller": "^2.3.0", "@metamask/assets-controllers": "^100.2.1", "@metamask/base-controller": "^9.0.0", "@metamask/bridge-controller": "^69.0.1", diff --git a/yarn.lock b/yarn.lock index a4b56847900..20887061caa 100644 --- a/yarn.lock +++ b/yarn.lock @@ -5414,6 +5414,7 @@ __metadata: "@ethersproject/contracts": "npm:^5.7.0" "@ethersproject/providers": "npm:^5.7.0" "@metamask/app-metadata-controller": "npm:^2.0.0" + "@metamask/assets-controller": "npm:^2.3.0" "@metamask/assets-controllers": "npm:^100.2.1" "@metamask/auto-changelog": "npm:^3.4.4" "@metamask/base-controller": "npm:^9.0.0" From deb5f051acc9d10069ed521e93575e5c0916659f Mon Sep 17 00:00:00 2001 From: Bernardo Garces Chapero Date: Tue, 10 Mar 2026 16:15:39 +0000 Subject: [PATCH 08/14] tsconfig --- packages/transaction-pay-controller/tsconfig.build.json | 3 +++ packages/transaction-pay-controller/tsconfig.json | 3 +++ 2 files changed, 6 insertions(+) diff --git a/packages/transaction-pay-controller/tsconfig.build.json b/packages/transaction-pay-controller/tsconfig.build.json index fdba8d46462..8d530779845 100644 --- a/packages/transaction-pay-controller/tsconfig.build.json +++ b/packages/transaction-pay-controller/tsconfig.build.json @@ -9,6 +9,9 @@ { "path": "../app-metadata-controller/tsconfig.build.json" }, + { + "path": "../assets-controller/tsconfig.build.json" + }, { "path": "../assets-controllers/tsconfig.build.json" }, diff --git a/packages/transaction-pay-controller/tsconfig.json b/packages/transaction-pay-controller/tsconfig.json index c4f699133f2..e9e8544595e 100644 --- a/packages/transaction-pay-controller/tsconfig.json +++ b/packages/transaction-pay-controller/tsconfig.json @@ -7,6 +7,9 @@ { "path": "../app-metadata-controller" }, + { + "path": "../assets-controller" + }, { "path": "../assets-controllers" }, From 61e089016ac06de4e8bac8ec17ff81a0359345de Mon Sep 17 00:00:00 2001 From: Bernardo Garces Chapero Date: Wed, 11 Mar 2026 10:38:19 +0000 Subject: [PATCH 09/14] correct conversion --- .../src/utils/formatExchangeRatesForBridge.ts | 42 ++++++++++++++++++- 1 file changed, 40 insertions(+), 2 deletions(-) diff --git a/packages/assets-controller/src/utils/formatExchangeRatesForBridge.ts b/packages/assets-controller/src/utils/formatExchangeRatesForBridge.ts index 3b15002d66f..0df36cefb97 100644 --- a/packages/assets-controller/src/utils/formatExchangeRatesForBridge.ts +++ b/packages/assets-controller/src/utils/formatExchangeRatesForBridge.ts @@ -139,13 +139,51 @@ export function formatExchangeRatesForBridge(params: { }; } } else { + const pricePercentChange: Record = {}; + if (priceData.pricePercentChange1h !== undefined) { + pricePercentChange.PT1H = priceData.pricePercentChange1h; + } + if (priceData.pricePercentChange1d !== undefined) { + pricePercentChange.P1D = priceData.pricePercentChange1d; + } + if (priceData.pricePercentChange7d !== undefined) { + pricePercentChange.P7D = priceData.pricePercentChange7d; + } + if (priceData.pricePercentChange14d !== undefined) { + pricePercentChange.P14D = priceData.pricePercentChange14d; + } + if (priceData.pricePercentChange30d !== undefined) { + pricePercentChange.P30D = priceData.pricePercentChange30d; + } + if (priceData.pricePercentChange200d !== undefined) { + pricePercentChange.P200D = priceData.pricePercentChange200d; + } + if (priceData.pricePercentChange1y !== undefined) { + pricePercentChange.P365D = priceData.pricePercentChange1y; + } + conversionRates[assetId as Caip19AssetId] = { rate: String(price), currency: currencyCaip, conversionTime: lastUpdatedInSeconds, expirationTime, - marketData: - priceData as unknown as MultichainAssetsRatesControllerState['conversionRates'][Caip19AssetId]['marketData'], + marketData: { + fungible: true, + allTimeHigh: `${priceData.allTimeHigh}`, + allTimeLow: `${priceData.allTimeLow}`, + circulatingSupply: `${priceData.circulatingSupply}`, + marketCap: `${priceData.marketCap}`, + totalVolume: `${priceData.totalVolume}`, + pricePercentChange: { + PT1H: priceData.pricePercentChange1h, + P1D: priceData.pricePercentChange1d, + P7D: priceData.pricePercentChange7d, + P14D: priceData.pricePercentChange14d, + P30D: priceData.pricePercentChange30d, + P200D: priceData.pricePercentChange200d, + P1Y: priceData.pricePercentChange1y, + }, + }, } as MultichainAssetsRatesControllerState['conversionRates'][Caip19AssetId]; } } catch { From 20e274c070c31cd3f0e88d0a53106513a31afb3b Mon Sep 17 00:00:00 2001 From: Bernardo Garces Chapero Date: Wed, 11 Mar 2026 10:55:12 +0000 Subject: [PATCH 10/14] fix types --- .../src/utils/formatExchangeRatesForBridge.ts | 37 ++++--------------- .../src/TokenRatesController.ts | 3 ++ 2 files changed, 10 insertions(+), 30 deletions(-) diff --git a/packages/assets-controller/src/utils/formatExchangeRatesForBridge.ts b/packages/assets-controller/src/utils/formatExchangeRatesForBridge.ts index 0df36cefb97..d3e5271ecf9 100644 --- a/packages/assets-controller/src/utils/formatExchangeRatesForBridge.ts +++ b/packages/assets-controller/src/utils/formatExchangeRatesForBridge.ts @@ -139,29 +139,6 @@ export function formatExchangeRatesForBridge(params: { }; } } else { - const pricePercentChange: Record = {}; - if (priceData.pricePercentChange1h !== undefined) { - pricePercentChange.PT1H = priceData.pricePercentChange1h; - } - if (priceData.pricePercentChange1d !== undefined) { - pricePercentChange.P1D = priceData.pricePercentChange1d; - } - if (priceData.pricePercentChange7d !== undefined) { - pricePercentChange.P7D = priceData.pricePercentChange7d; - } - if (priceData.pricePercentChange14d !== undefined) { - pricePercentChange.P14D = priceData.pricePercentChange14d; - } - if (priceData.pricePercentChange30d !== undefined) { - pricePercentChange.P30D = priceData.pricePercentChange30d; - } - if (priceData.pricePercentChange200d !== undefined) { - pricePercentChange.P200D = priceData.pricePercentChange200d; - } - if (priceData.pricePercentChange1y !== undefined) { - pricePercentChange.P365D = priceData.pricePercentChange1y; - } - conversionRates[assetId as Caip19AssetId] = { rate: String(price), currency: currencyCaip, @@ -175,13 +152,13 @@ export function formatExchangeRatesForBridge(params: { marketCap: `${priceData.marketCap}`, totalVolume: `${priceData.totalVolume}`, pricePercentChange: { - PT1H: priceData.pricePercentChange1h, - P1D: priceData.pricePercentChange1d, - P7D: priceData.pricePercentChange7d, - P14D: priceData.pricePercentChange14d, - P30D: priceData.pricePercentChange30d, - P200D: priceData.pricePercentChange200d, - P1Y: priceData.pricePercentChange1y, + PT1H: priceData.pricePercentChange1h as number, + P1D: priceData.pricePercentChange1d as number, + P7D: priceData.pricePercentChange7d as number, + P14D: priceData.pricePercentChange14d as number, + P30D: priceData.pricePercentChange30d as number, + P200D: priceData.pricePercentChange200d as number, + P1Y: priceData.pricePercentChange1y as number, }, }, } as MultichainAssetsRatesControllerState['conversionRates'][Caip19AssetId]; diff --git a/packages/assets-controllers/src/TokenRatesController.ts b/packages/assets-controllers/src/TokenRatesController.ts index 6b8df1e3f1c..571bd020441 100644 --- a/packages/assets-controllers/src/TokenRatesController.ts +++ b/packages/assets-controllers/src/TokenRatesController.ts @@ -77,6 +77,9 @@ export type MarketDataDetails = { pricePercentChange30d: number; pricePercentChange200d: number; totalVolume: number; + assetId: string; + chainId: string; + id: string; }; /** From 0f01832075971441a17c3b3388dbc81c31cd65d5 Mon Sep 17 00:00:00 2001 From: Bernardo Garces Chapero Date: Wed, 11 Mar 2026 11:01:50 +0000 Subject: [PATCH 11/14] test fix --- .../src/utils/formatExchangeRatesForBridge.test.ts | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/packages/assets-controller/src/utils/formatExchangeRatesForBridge.test.ts b/packages/assets-controller/src/utils/formatExchangeRatesForBridge.test.ts index e85fcc908f1..63b46fd16f8 100644 --- a/packages/assets-controller/src/utils/formatExchangeRatesForBridge.test.ts +++ b/packages/assets-controller/src/utils/formatExchangeRatesForBridge.test.ts @@ -239,10 +239,8 @@ describe('formatExchangeRatesForBridge', () => { const entry = result.conversionRates[bitcoinAssetId]; expect(entry.marketData).toStrictEqual( expect.objectContaining({ - price: 50000, - id: 'bitcoin', - marketCap: 1_000_000_000_000, - lastUpdated: 1_600_000_000, + fungible: true, + marketCap: '1000000000000', }), ); }); From 6638faa55a35c5087c74882f50962b7410f1c599 Mon Sep 17 00:00:00 2001 From: Bernardo Garces Chapero Date: Wed, 11 Mar 2026 11:12:36 +0000 Subject: [PATCH 12/14] remove assertion --- .../assets-controller/src/utils/formatExchangeRatesForBridge.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/assets-controller/src/utils/formatExchangeRatesForBridge.ts b/packages/assets-controller/src/utils/formatExchangeRatesForBridge.ts index d3e5271ecf9..44e80144966 100644 --- a/packages/assets-controller/src/utils/formatExchangeRatesForBridge.ts +++ b/packages/assets-controller/src/utils/formatExchangeRatesForBridge.ts @@ -161,7 +161,7 @@ export function formatExchangeRatesForBridge(params: { P1Y: priceData.pricePercentChange1y as number, }, }, - } as MultichainAssetsRatesControllerState['conversionRates'][Caip19AssetId]; + }; } } catch { // Skip malformed asset IDs From 9b43494dbb3223f7e78015ab31301fe4f701bdcb Mon Sep 17 00:00:00 2001 From: Bernardo Garces Chapero Date: Wed, 11 Mar 2026 13:48:04 +0000 Subject: [PATCH 13/14] revert type change --- packages/assets-controllers/src/TokenRatesController.ts | 3 --- 1 file changed, 3 deletions(-) diff --git a/packages/assets-controllers/src/TokenRatesController.ts b/packages/assets-controllers/src/TokenRatesController.ts index 571bd020441..6b8df1e3f1c 100644 --- a/packages/assets-controllers/src/TokenRatesController.ts +++ b/packages/assets-controllers/src/TokenRatesController.ts @@ -77,9 +77,6 @@ export type MarketDataDetails = { pricePercentChange30d: number; pricePercentChange200d: number; totalVolume: number; - assetId: string; - chainId: string; - id: string; }; /** From ab401466168fafef2a210b6bf95744d5bf0a4ec4 Mon Sep 17 00:00:00 2001 From: Bernardo Garces Chapero Date: Wed, 11 Mar 2026 14:59:50 +0000 Subject: [PATCH 14/14] remove functions --- .../src/utils/token.ts | 49 +------------------ 1 file changed, 1 insertion(+), 48 deletions(-) diff --git a/packages/transaction-pay-controller/src/utils/token.ts b/packages/transaction-pay-controller/src/utils/token.ts index 9e7a11d2fe4..2dc9e9acad0 100644 --- a/packages/transaction-pay-controller/src/utils/token.ts +++ b/packages/transaction-pay-controller/src/utils/token.ts @@ -2,9 +2,7 @@ import { Contract } from '@ethersproject/contracts'; import { Web3Provider } from '@ethersproject/providers'; import { AccountTrackerControllerState, - CurrencyRateState, TokenBalancesControllerState, - TokenRatesControllerState, TokensControllerState, } from '@metamask/assets-controllers'; import { toChecksumHexAddress } from '@metamask/controller-utils'; @@ -82,26 +80,6 @@ export function getTokenBalance( messenger.call('AccountTrackerController:getState')?.accountsByChainId; } - return _getTokenBalance( - account, - chainId, - tokenAddress, - getTokenBalances, - getAccountsByChainId, - ); -} - -function _getTokenBalance( - account: Hex, - chainId: Hex, - tokenAddress: Hex, - getTokenBalances: () => - | TokenBalancesControllerState['tokenBalances'] - | undefined, - getAccountsByChainId: () => - | AccountTrackerControllerState['accountsByChainId'] - | undefined, -): string { const normalizedAccount = account.toLowerCase() as Hex; const normalizedTokenAddress = toChecksumHexAddress(tokenAddress) as Hex; const isNative = normalizedTokenAddress === getNativeToken(chainId); @@ -142,7 +120,7 @@ export function getTokenInfo( ): { decimals: number; symbol: string } | undefined { const assetsUnifyStateFeatureEnabled = getAssetsUnifyStateFeature(messenger); - let allTokens; + let allTokens: TokensControllerState['allTokens']; if (assetsUnifyStateFeatureEnabled) { allTokens = messenger.call( 'AssetsController:getStateForTransactionPay', @@ -151,15 +129,6 @@ export function getTokenInfo( allTokens = messenger.call('TokensController:getState')?.allTokens; } - return _getTokenInfo(messenger, tokenAddress, chainId, allTokens); -} - -function _getTokenInfo( - messenger: TransactionPayControllerMessenger, - tokenAddress: Hex, - chainId: Hex, - allTokens: TokensControllerState['allTokens'] | undefined, -): { decimals: number; symbol: string } | undefined { const normalizedTokenAddress = tokenAddress.toLowerCase() as Hex; const isNative = @@ -220,22 +189,6 @@ export function getTokenFiatRate( )?.currencyRates; } - return _getTokenFiatRate( - messenger, - tokenAddress, - chainId, - marketData, - currencyRates, - ); -} - -function _getTokenFiatRate( - messenger: TransactionPayControllerMessenger, - tokenAddress: Hex, - chainId: Hex, - marketData: TokenRatesControllerState['marketData'] | undefined, - currencyRates: CurrencyRateState['currencyRates'] | undefined, -): FiatRates | undefined { const ticker = getTicker(chainId, messenger); if (!ticker) {