From e008afb07196ce5b149d7c8053fc8083d74cd7d3 Mon Sep 17 00:00:00 2001 From: Goktug Poyraz Date: Wed, 29 Apr 2026 12:47:00 +0200 Subject: [PATCH 1/7] feat: read fiat asset from feature flags before hardcoded fallback --- .../transaction-pay-controller/CHANGELOG.md | 6 + .../src/constants.ts | 1 + .../src/strategy/fiat/constants.ts | 9 +- .../src/strategy/fiat/fiat-quotes.test.ts | 10 -- .../src/strategy/fiat/fiat-quotes.ts | 11 +- .../src/strategy/fiat/utils.test.ts | 127 ++++++++++++++++-- .../src/strategy/fiat/utils.ts | 40 ++++-- .../src/utils/feature-flags.test.ts | 77 +++++++++++ .../src/utils/feature-flags.ts | 25 ++++ 9 files changed, 267 insertions(+), 39 deletions(-) diff --git a/packages/transaction-pay-controller/CHANGELOG.md b/packages/transaction-pay-controller/CHANGELOG.md index cd14cdf303..e8423f5d4b 100644 --- a/packages/transaction-pay-controller/CHANGELOG.md +++ b/packages/transaction-pay-controller/CHANGELOG.md @@ -11,6 +11,12 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - Implement fiat strategy submit flow with order polling and relay execution ([#8347](https://github.com/MetaMask/core/pull/8347)) +### Changed + +- Add feature-flag-driven fiat asset resolution in `deriveFiatAssetForFiatPayment` ([#8631](https://github.com/MetaMask/core/pull/8631)) + - Read asset per transaction type from `confirmations_pay_fiat` remote feature flag before falling back to hardcoded map + - Fall back to ETH on mainnet when neither feature flag nor hardcoded map has an entry + ## [21.0.0] ### Added diff --git a/packages/transaction-pay-controller/src/constants.ts b/packages/transaction-pay-controller/src/constants.ts index 5a097ee85d..72c7a5f842 100644 --- a/packages/transaction-pay-controller/src/constants.ts +++ b/packages/transaction-pay-controller/src/constants.ts @@ -2,6 +2,7 @@ import type { Hex } from '@metamask/utils'; export const CONTROLLER_NAME = 'TransactionPayController'; export const CHAIN_ID_ARBITRUM = '0xa4b1' as Hex; +export const CHAIN_ID_MAINNET = '0x1' as Hex; export const CHAIN_ID_POLYGON = '0x89' as Hex; export const CHAIN_ID_HYPERCORE = '0x539' as Hex; diff --git a/packages/transaction-pay-controller/src/strategy/fiat/constants.ts b/packages/transaction-pay-controller/src/strategy/fiat/constants.ts index 5289ebcb82..ffaac5e28d 100644 --- a/packages/transaction-pay-controller/src/strategy/fiat/constants.ts +++ b/packages/transaction-pay-controller/src/strategy/fiat/constants.ts @@ -3,6 +3,7 @@ import type { Hex } from '@metamask/utils'; import { CHAIN_ID_ARBITRUM, + CHAIN_ID_MAINNET, CHAIN_ID_POLYGON, NATIVE_TOKEN_ADDRESS, } from '../../constants'; @@ -30,7 +31,13 @@ const ARBITRUM_ETH_FIAT_ASSET: TransactionPayFiatAsset = { decimals: 18, }; -// We might use feature flags to determine these later. +export const ETH_MAINNET_FIAT_ASSET: TransactionPayFiatAsset = { + address: NATIVE_TOKEN_ADDRESS, + caipAssetId: 'eip155:1/slip44:60', + chainId: CHAIN_ID_MAINNET, + decimals: 18, +}; + export const FIAT_ASSET_ID_BY_TX_TYPE: Partial< Record > = { diff --git a/packages/transaction-pay-controller/src/strategy/fiat/fiat-quotes.test.ts b/packages/transaction-pay-controller/src/strategy/fiat/fiat-quotes.test.ts index 0bc9d291fd..541065f920 100644 --- a/packages/transaction-pay-controller/src/strategy/fiat/fiat-quotes.test.ts +++ b/packages/transaction-pay-controller/src/strategy/fiat/fiat-quotes.test.ts @@ -349,16 +349,6 @@ describe('getFiatQuotes', () => { expect(getRelayQuotesMock).not.toHaveBeenCalled(); }); - it('returns empty array if fiat asset mapping is missing', async () => { - deriveFiatAssetForFiatPaymentMock.mockReturnValue(undefined); - const { request } = getRequest(); - - const result = await getFiatQuotes(request); - - expect(result).toStrictEqual([]); - expect(getRelayQuotesMock).not.toHaveBeenCalled(); - }); - it('returns empty array if source token fiat rate is missing', async () => { getTokenFiatRateMock.mockReturnValue(undefined); const { request } = getRequest(); diff --git a/packages/transaction-pay-controller/src/strategy/fiat/fiat-quotes.ts b/packages/transaction-pay-controller/src/strategy/fiat/fiat-quotes.ts index a4184af3d8..7c45fb7d04 100644 --- a/packages/transaction-pay-controller/src/strategy/fiat/fiat-quotes.ts +++ b/packages/transaction-pay-controller/src/strategy/fiat/fiat-quotes.ts @@ -46,14 +46,9 @@ export async function getFiatQuotes( const amountFiat = transactionData?.fiatPayment?.amountFiat; const walletAddress = transaction.txParams.from as Hex; const requiredTokens = getRequiredTokens(transactionData?.tokens); - const fiatAsset = deriveFiatAssetForFiatPayment(transaction); - - if ( - !amountFiat || - !fiatPaymentMethod || - !requiredTokens.length || - !fiatAsset - ) { + const fiatAsset = deriveFiatAssetForFiatPayment(transaction, messenger); + + if (!amountFiat || !fiatPaymentMethod || !requiredTokens.length) { return []; } diff --git a/packages/transaction-pay-controller/src/strategy/fiat/utils.test.ts b/packages/transaction-pay-controller/src/strategy/fiat/utils.test.ts index 1175bccd56..1597390faf 100644 --- a/packages/transaction-pay-controller/src/strategy/fiat/utils.test.ts +++ b/packages/transaction-pay-controller/src/strategy/fiat/utils.test.ts @@ -1,44 +1,155 @@ import type { TransactionMeta } from '@metamask/transaction-controller'; import { TransactionType } from '@metamask/transaction-controller'; -import { FIAT_ASSET_ID_BY_TX_TYPE } from './constants'; +import { ETH_MAINNET_FIAT_ASSET, FIAT_ASSET_ID_BY_TX_TYPE } from './constants'; import { deriveFiatAssetForFiatPayment } from './utils'; +import { getDefaultRemoteFeatureFlagControllerState } from '../../../../remote-feature-flag-controller/src/remote-feature-flag-controller'; +import { getMessengerMock } from '../../tests/messenger-mock'; +import type { TransactionPayFiatAsset } from './constants'; + +const FEATURE_FLAG_ASSET_MOCK: TransactionPayFiatAsset = { + address: '0x0000000000000000000000000000000000000abc', + caipAssetId: 'eip155:10/slip44:60', + chainId: '0xa', + decimals: 18, +}; describe('Fiat Utils', () => { + const { messenger, getRemoteFeatureFlagControllerStateMock } = + getMessengerMock(); + + beforeEach(() => { + jest.resetAllMocks(); + + getRemoteFeatureFlagControllerStateMock.mockReturnValue({ + ...getDefaultRemoteFeatureFlagControllerState(), + }); + }); + describe('deriveFiatAssetForFiatPayment', () => { - it('returns mapped fiat asset for direct transaction type', () => { + it('returns asset from feature flag when present', () => { + getRemoteFeatureFlagControllerStateMock.mockReturnValue({ + ...getDefaultRemoteFeatureFlagControllerState(), + remoteFeatureFlags: { + confirmations_pay_fiat: { + assetPerTransactionType: { + [TransactionType.predictDeposit]: FEATURE_FLAG_ASSET_MOCK, + }, + }, + }, + }); + + const transaction = { + type: TransactionType.predictDeposit, + } as TransactionMeta; + + const result = deriveFiatAssetForFiatPayment(transaction, messenger); + + expect(result).toStrictEqual(FEATURE_FLAG_ASSET_MOCK); + }); + + it('returns feature flag asset over hardcoded asset when both exist', () => { + getRemoteFeatureFlagControllerStateMock.mockReturnValue({ + ...getDefaultRemoteFeatureFlagControllerState(), + remoteFeatureFlags: { + confirmations_pay_fiat: { + assetPerTransactionType: { + [TransactionType.predictDeposit]: FEATURE_FLAG_ASSET_MOCK, + }, + }, + }, + }); + + const transaction = { + type: TransactionType.predictDeposit, + } as TransactionMeta; + + const result = deriveFiatAssetForFiatPayment(transaction, messenger); + + expect(result).toStrictEqual(FEATURE_FLAG_ASSET_MOCK); + expect(result).not.toStrictEqual( + FIAT_ASSET_ID_BY_TX_TYPE[TransactionType.predictDeposit], + ); + }); + + it('returns hardcoded asset when feature flag has no entry for the type', () => { const transaction = { type: TransactionType.predictDeposit, } as TransactionMeta; - const result = deriveFiatAssetForFiatPayment(transaction); + const result = deriveFiatAssetForFiatPayment(transaction, messenger); expect(result).toStrictEqual( FIAT_ASSET_ID_BY_TX_TYPE[TransactionType.predictDeposit], ); }); - it('returns mapped fiat asset for first nested transaction in batch', () => { + it('returns hardcoded asset for direct transaction type', () => { + const transaction = { + type: TransactionType.perpsDeposit, + } as TransactionMeta; + + const result = deriveFiatAssetForFiatPayment(transaction, messenger); + + expect(result).toStrictEqual( + FIAT_ASSET_ID_BY_TX_TYPE[TransactionType.perpsDeposit], + ); + }); + + it('returns hardcoded asset for first nested transaction in batch', () => { const transaction = { nestedTransactions: [{ type: TransactionType.perpsDeposit }], type: TransactionType.batch, } as TransactionMeta; - const result = deriveFiatAssetForFiatPayment(transaction); + const result = deriveFiatAssetForFiatPayment(transaction, messenger); expect(result).toStrictEqual( FIAT_ASSET_ID_BY_TX_TYPE[TransactionType.perpsDeposit], ); }); - it('returns undefined for unsupported type', () => { + it('returns feature flag asset for first nested transaction in batch', () => { + getRemoteFeatureFlagControllerStateMock.mockReturnValue({ + ...getDefaultRemoteFeatureFlagControllerState(), + remoteFeatureFlags: { + confirmations_pay_fiat: { + assetPerTransactionType: { + [TransactionType.perpsDeposit]: FEATURE_FLAG_ASSET_MOCK, + }, + }, + }, + }); + + const transaction = { + nestedTransactions: [{ type: TransactionType.perpsDeposit }], + type: TransactionType.batch, + } as TransactionMeta; + + const result = deriveFiatAssetForFiatPayment(transaction, messenger); + + expect(result).toStrictEqual(FEATURE_FLAG_ASSET_MOCK); + }); + + it('returns ETH mainnet fallback for unsupported type', () => { const transaction = { type: TransactionType.contractInteraction, } as TransactionMeta; - const result = deriveFiatAssetForFiatPayment(transaction); + const result = deriveFiatAssetForFiatPayment(transaction, messenger); + + expect(result).toStrictEqual(ETH_MAINNET_FIAT_ASSET); + }); + + it('returns ETH mainnet fallback for batch with no nested transactions', () => { + const transaction = { + nestedTransactions: [], + type: TransactionType.batch, + } as unknown as TransactionMeta; + + const result = deriveFiatAssetForFiatPayment(transaction, messenger); - expect(result).toBeUndefined(); + expect(result).toStrictEqual(ETH_MAINNET_FIAT_ASSET); }); }); }); diff --git a/packages/transaction-pay-controller/src/strategy/fiat/utils.ts b/packages/transaction-pay-controller/src/strategy/fiat/utils.ts index 6894127b13..534023e071 100644 --- a/packages/transaction-pay-controller/src/strategy/fiat/utils.ts +++ b/packages/transaction-pay-controller/src/strategy/fiat/utils.ts @@ -1,21 +1,37 @@ -import { - TransactionMeta, - TransactionType, -} from '@metamask/transaction-controller'; +import type { TransactionMeta } from '@metamask/transaction-controller'; +import { TransactionType } from '@metamask/transaction-controller'; -import { FIAT_ASSET_ID_BY_TX_TYPE, TransactionPayFiatAsset } from './constants'; +import type { TransactionPayControllerMessenger } from '../../types'; +import { getFiatAssetPerTransactionType } from '../../utils/feature-flags'; +import type { TransactionPayFiatAsset } from './constants'; +import { ETH_MAINNET_FIAT_ASSET, FIAT_ASSET_ID_BY_TX_TYPE } from './constants'; + +function resolveTransactionType( + transaction: TransactionMeta, +): TransactionType | undefined { + if (transaction.type === TransactionType.batch) { + return transaction.nestedTransactions?.[0]?.type; + } + return transaction.type; +} export function deriveFiatAssetForFiatPayment( transaction: TransactionMeta, -): TransactionPayFiatAsset | undefined { - const transactionType = transaction?.type; + messenger: TransactionPayControllerMessenger, +): TransactionPayFiatAsset { + const txType = resolveTransactionType(transaction); + + if (txType) { + const flagAsset = getFiatAssetPerTransactionType(messenger, txType); + if (flagAsset) { + return flagAsset; + } - if (transactionType === TransactionType.batch) { - const firstMatchingType = transaction.nestedTransactions?.[0]?.type; - if (firstMatchingType) { - return FIAT_ASSET_ID_BY_TX_TYPE[firstMatchingType]; + const hardcodedAsset = FIAT_ASSET_ID_BY_TX_TYPE[txType]; + if (hardcodedAsset) { + return hardcodedAsset; } } - return FIAT_ASSET_ID_BY_TX_TYPE[transactionType as TransactionType]; + return ETH_MAINNET_FIAT_ASSET; } 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 6e16c174cb..2ee2f84475 100644 --- a/packages/transaction-pay-controller/src/utils/feature-flags.test.ts +++ b/packages/transaction-pay-controller/src/utils/feature-flags.test.ts @@ -1,7 +1,9 @@ +import { TransactionType } from '@metamask/transaction-controller'; import type { Hex } from '@metamask/utils'; import { getDefaultRemoteFeatureFlagControllerState } from '../../../remote-feature-flag-controller/src/remote-feature-flag-controller'; import { TransactionPayStrategy } from '../constants'; +import type { TransactionPayFiatAsset } from '../strategy/fiat/constants'; import { getMessengerMock } from '../tests/messenger-mock'; import { DEFAULT_ACROSS_API_BASE, @@ -13,6 +15,7 @@ import { DEFAULT_SLIPPAGE, getAssetsUnifyStateFeature, getFallbackGas, + getFiatAssetPerTransactionType, DEFAULT_RELAY_EXECUTE_URL, getRelayOriginGasOverhead, getRelayPollingInterval, @@ -1168,4 +1171,78 @@ describe('Feature Flags Utils', () => { expect(getStrategy(messenger)).toBeUndefined(); }); }); + + describe('getFiatAssetPerTransactionType', () => { + const FIAT_ASSET_MOCK: TransactionPayFiatAsset = { + address: '0x0000000000000000000000000000000000001010', + caipAssetId: 'eip155:137/slip44:966', + chainId: '0x89', + decimals: 18, + }; + + it('returns undefined when confirmations_pay_fiat flag is absent', () => { + const result = getFiatAssetPerTransactionType( + messenger, + TransactionType.predictDeposit, + ); + + expect(result).toBeUndefined(); + }); + + it('returns undefined when flag exists but has no entry for the transaction type', () => { + getRemoteFeatureFlagControllerStateMock.mockReturnValue({ + ...getDefaultRemoteFeatureFlagControllerState(), + remoteFeatureFlags: { + confirmations_pay_fiat: { + assetPerTransactionType: { + [TransactionType.perpsDeposit]: FIAT_ASSET_MOCK, + }, + }, + }, + }); + + const result = getFiatAssetPerTransactionType( + messenger, + TransactionType.predictDeposit, + ); + + expect(result).toBeUndefined(); + }); + + it('returns the asset when entry matches the transaction type', () => { + getRemoteFeatureFlagControllerStateMock.mockReturnValue({ + ...getDefaultRemoteFeatureFlagControllerState(), + remoteFeatureFlags: { + confirmations_pay_fiat: { + assetPerTransactionType: { + [TransactionType.predictDeposit]: FIAT_ASSET_MOCK, + }, + }, + }, + }); + + const result = getFiatAssetPerTransactionType( + messenger, + TransactionType.predictDeposit, + ); + + expect(result).toStrictEqual(FIAT_ASSET_MOCK); + }); + + it('returns undefined when assetPerTransactionType is not defined', () => { + getRemoteFeatureFlagControllerStateMock.mockReturnValue({ + ...getDefaultRemoteFeatureFlagControllerState(), + remoteFeatureFlags: { + confirmations_pay_fiat: {}, + }, + }); + + const result = getFiatAssetPerTransactionType( + messenger, + TransactionType.predictDeposit, + ); + + expect(result).toBeUndefined(); + }); + }); }); diff --git a/packages/transaction-pay-controller/src/utils/feature-flags.ts b/packages/transaction-pay-controller/src/utils/feature-flags.ts index 911c5bc605..8ee521f8fc 100644 --- a/packages/transaction-pay-controller/src/utils/feature-flags.ts +++ b/packages/transaction-pay-controller/src/utils/feature-flags.ts @@ -1,9 +1,11 @@ +import type { TransactionType } from '@metamask/transaction-controller'; import type { Hex } from '@metamask/utils'; import { createModuleLogger } from '@metamask/utils'; import { uniq } from 'lodash'; import { isTransactionPayStrategy, TransactionPayStrategy } from '../constants'; import { projectLogger } from '../logger'; +import type { TransactionPayFiatAsset } from '../strategy/fiat/constants'; import { RELAY_EXECUTE_URL, RELAY_POLLING_INTERVAL, @@ -661,6 +663,29 @@ function getCaseInsensitive( return entry?.[1]; } +/** + * Get the fiat asset override for a specific transaction type from feature flags. + * + * @param messenger - Controller messenger. + * @param transactionType - Transaction type to look up. + * @returns The fiat asset if configured, undefined otherwise. + */ +export function getFiatAssetPerTransactionType( + messenger: TransactionPayControllerMessenger, + transactionType: TransactionType, +): TransactionPayFiatAsset | undefined { + const state = messenger.call('RemoteFeatureFlagController:getState'); + const fiatFlags = state.remoteFeatureFlags?.confirmations_pay_fiat as + | { + assetPerTransactionType?: Partial< + Record + >; + } + | undefined; + + return fiatFlags?.assetPerTransactionType?.[transactionType]; +} + /** * Checks if a chain supports EIP-7702. * From 469e1c7edeb2cf42001413d9ae1e7137aa9075b9 Mon Sep 17 00:00:00 2001 From: Goktug Poyraz Date: Mon, 4 May 2026 10:48:52 +0200 Subject: [PATCH 2/7] Update --- .../src/strategy/fiat/constants.ts | 8 ---- .../src/strategy/fiat/fiat-quotes.test.ts | 20 ++++++-- .../src/strategy/fiat/fiat-quotes.ts | 19 ++++++-- .../src/strategy/fiat/utils.test.ts | 22 +++++++-- .../src/strategy/fiat/utils.ts | 31 +++++------- .../src/utils/feature-flags.test.ts | 47 +++++++++++++++---- .../src/utils/feature-flags.ts | 39 +++++++++++---- .../src/utils/token.test.ts | 33 +++++++++++++ .../src/utils/token.ts | 37 ++++++++++++++- 9 files changed, 197 insertions(+), 59 deletions(-) diff --git a/packages/transaction-pay-controller/src/strategy/fiat/constants.ts b/packages/transaction-pay-controller/src/strategy/fiat/constants.ts index ffaac5e28d..83d85afa3d 100644 --- a/packages/transaction-pay-controller/src/strategy/fiat/constants.ts +++ b/packages/transaction-pay-controller/src/strategy/fiat/constants.ts @@ -12,30 +12,22 @@ export const DEFAULT_FIAT_CURRENCY = 'USD'; export type TransactionPayFiatAsset = { address: Hex; - caipAssetId: string; chainId: Hex; - decimals: number; }; const POLYGON_POL_FIAT_ASSET: TransactionPayFiatAsset = { address: '0x0000000000000000000000000000000000001010', - caipAssetId: 'eip155:137/slip44:966', chainId: CHAIN_ID_POLYGON, - decimals: 18, }; const ARBITRUM_ETH_FIAT_ASSET: TransactionPayFiatAsset = { address: NATIVE_TOKEN_ADDRESS, - caipAssetId: 'eip155:42161/slip44:60', chainId: CHAIN_ID_ARBITRUM, - decimals: 18, }; export const ETH_MAINNET_FIAT_ASSET: TransactionPayFiatAsset = { address: NATIVE_TOKEN_ADDRESS, - caipAssetId: 'eip155:1/slip44:60', chainId: CHAIN_ID_MAINNET, - decimals: 18, }; export const FIAT_ASSET_ID_BY_TX_TYPE: Partial< diff --git a/packages/transaction-pay-controller/src/strategy/fiat/fiat-quotes.test.ts b/packages/transaction-pay-controller/src/strategy/fiat/fiat-quotes.test.ts index 541065f920..0dcf662203 100644 --- a/packages/transaction-pay-controller/src/strategy/fiat/fiat-quotes.test.ts +++ b/packages/transaction-pay-controller/src/strategy/fiat/fiat-quotes.test.ts @@ -13,7 +13,11 @@ import type { TransactionPayQuote, TransactionPayRequiredToken, } from '../../types'; -import { computeRawFromFiatAmount, getTokenFiatRate } from '../../utils/token'; +import { + computeRawFromFiatAmount, + getTokenFiatRate, + getTokenInfo, +} from '../../utils/token'; import { getRelayQuotes } from '../relay/relay-quotes'; import type { RelayQuote } from '../relay/types'; import type { TransactionPayFiatAsset } from './constants'; @@ -52,9 +56,7 @@ const REQUIRED_TOKEN_MOCK: TransactionPayRequiredToken = { const FIAT_ASSET_MOCK: TransactionPayFiatAsset = { address: '0x0000000000000000000000000000000000001010', - caipAssetId: 'eip155:137/slip44:966', chainId: '0x89', - decimals: 18, }; const FIAT_QUOTE_MOCK: RampsQuote = { @@ -202,6 +204,7 @@ function getRequest({ describe('getFiatQuotes', () => { const getRelayQuotesMock = jest.mocked(getRelayQuotes); const getTokenFiatRateMock = jest.mocked(getTokenFiatRate); + const getTokenInfoMock = jest.mocked(getTokenInfo); const computeRawFromFiatAmountMock = jest.mocked(computeRawFromFiatAmount); const deriveFiatAssetForFiatPaymentMock = jest.mocked( deriveFiatAssetForFiatPayment, @@ -215,6 +218,7 @@ describe('getFiatQuotes', () => { fiatRate: '2', usdRate: '2', }); + getTokenInfoMock.mockReturnValue({ decimals: 18, symbol: 'POL' }); computeRawFromFiatAmountMock.mockReturnValue('5000000000000000000'); getRelayQuotesMock.mockResolvedValue([getRelayQuoteMock()]); }); @@ -359,6 +363,16 @@ describe('getFiatQuotes', () => { expect(getRelayQuotesMock).not.toHaveBeenCalled(); }); + it('returns empty array if token info is unavailable', async () => { + getTokenInfoMock.mockReturnValue(undefined); + const { request } = getRequest(); + + const result = await getFiatQuotes(request); + + expect(result).toStrictEqual([]); + expect(getRelayQuotesMock).not.toHaveBeenCalled(); + }); + it('returns empty array if computeRawFromFiatAmount returns undefined', async () => { computeRawFromFiatAmountMock.mockReturnValue(undefined); const { request } = getRequest(); diff --git a/packages/transaction-pay-controller/src/strategy/fiat/fiat-quotes.ts b/packages/transaction-pay-controller/src/strategy/fiat/fiat-quotes.ts index 7c45fb7d04..4049bf4667 100644 --- a/packages/transaction-pay-controller/src/strategy/fiat/fiat-quotes.ts +++ b/packages/transaction-pay-controller/src/strategy/fiat/fiat-quotes.ts @@ -11,7 +11,11 @@ import type { TransactionPayRequiredToken, TransactionPayQuote, } from '../../types'; -import { computeRawFromFiatAmount, getTokenFiatRate } from '../../utils/token'; +import { + computeRawFromFiatAmount, + getTokenFiatRate, + getTokenInfo, +} from '../../utils/token'; import { getRelayQuotes } from '../relay/relay-quotes'; import type { RelayQuote } from '../relay/types'; import { DEFAULT_FIAT_CURRENCY } from './constants'; @@ -198,7 +202,6 @@ function buildRelayRequestFromAmountFiat({ fiatAsset: { address: Hex; chainId: Hex; - decimals: number; }; messenger: PayStrategyGetQuotesRequest['messenger']; requiredToken: TransactionPayRequiredToken; @@ -214,9 +217,19 @@ function buildRelayRequestFromAmountFiat({ return undefined; } + const tokenInfo = getTokenInfo( + messenger, + fiatAsset.address, + fiatAsset.chainId, + ); + + if (!tokenInfo) { + return undefined; + } + const sourceAmountRaw = computeRawFromFiatAmount( amountFiat, - fiatAsset.decimals, + tokenInfo.decimals, sourceFiatRate.usdRate, ); diff --git a/packages/transaction-pay-controller/src/strategy/fiat/utils.test.ts b/packages/transaction-pay-controller/src/strategy/fiat/utils.test.ts index 1597390faf..41a458f30e 100644 --- a/packages/transaction-pay-controller/src/strategy/fiat/utils.test.ts +++ b/packages/transaction-pay-controller/src/strategy/fiat/utils.test.ts @@ -9,9 +9,7 @@ import type { TransactionPayFiatAsset } from './constants'; const FEATURE_FLAG_ASSET_MOCK: TransactionPayFiatAsset = { address: '0x0000000000000000000000000000000000000abc', - caipAssetId: 'eip155:10/slip44:60', chainId: '0xa', - decimals: 18, }; describe('Fiat Utils', () => { @@ -96,7 +94,7 @@ describe('Fiat Utils', () => { ); }); - it('returns hardcoded asset for first nested transaction in batch', () => { + it('returns hardcoded asset for supported nested transaction in batch', () => { const transaction = { nestedTransactions: [{ type: TransactionType.perpsDeposit }], type: TransactionType.batch, @@ -109,7 +107,23 @@ describe('Fiat Utils', () => { ); }); - it('returns feature flag asset for first nested transaction in batch', () => { + it('skips unsupported nested types and finds supported one in batch', () => { + const transaction = { + nestedTransactions: [ + { type: TransactionType.tokenMethodApprove }, + { type: TransactionType.perpsDeposit }, + ], + type: TransactionType.batch, + } as TransactionMeta; + + const result = deriveFiatAssetForFiatPayment(transaction, messenger); + + expect(result).toStrictEqual( + FIAT_ASSET_ID_BY_TX_TYPE[TransactionType.perpsDeposit], + ); + }); + + it('returns feature flag asset for supported nested transaction in batch', () => { getRemoteFeatureFlagControllerStateMock.mockReturnValue({ ...getDefaultRemoteFeatureFlagControllerState(), remoteFeatureFlags: { diff --git a/packages/transaction-pay-controller/src/strategy/fiat/utils.ts b/packages/transaction-pay-controller/src/strategy/fiat/utils.ts index 534023e071..8759473531 100644 --- a/packages/transaction-pay-controller/src/strategy/fiat/utils.ts +++ b/packages/transaction-pay-controller/src/strategy/fiat/utils.ts @@ -4,16 +4,7 @@ import { TransactionType } from '@metamask/transaction-controller'; import type { TransactionPayControllerMessenger } from '../../types'; import { getFiatAssetPerTransactionType } from '../../utils/feature-flags'; import type { TransactionPayFiatAsset } from './constants'; -import { ETH_MAINNET_FIAT_ASSET, FIAT_ASSET_ID_BY_TX_TYPE } from './constants'; - -function resolveTransactionType( - transaction: TransactionMeta, -): TransactionType | undefined { - if (transaction.type === TransactionType.batch) { - return transaction.nestedTransactions?.[0]?.type; - } - return transaction.type; -} +import { FIAT_ASSET_ID_BY_TX_TYPE } from './constants'; export function deriveFiatAssetForFiatPayment( transaction: TransactionMeta, @@ -21,17 +12,17 @@ export function deriveFiatAssetForFiatPayment( ): TransactionPayFiatAsset { const txType = resolveTransactionType(transaction); - if (txType) { - const flagAsset = getFiatAssetPerTransactionType(messenger, txType); - if (flagAsset) { - return flagAsset; - } + return getFiatAssetPerTransactionType(messenger, txType); +} - const hardcodedAsset = FIAT_ASSET_ID_BY_TX_TYPE[txType]; - if (hardcodedAsset) { - return hardcodedAsset; - } +function resolveTransactionType( + transaction: TransactionMeta, +): TransactionType | undefined { + if (transaction.type !== TransactionType.batch) { + return transaction.type; } - return ETH_MAINNET_FIAT_ASSET; + return transaction.nestedTransactions?.find( + (tx) => tx.type && FIAT_ASSET_ID_BY_TX_TYPE[tx.type] !== undefined, + )?.type; } 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 2ee2f84475..c3f51b2e3e 100644 --- a/packages/transaction-pay-controller/src/utils/feature-flags.test.ts +++ b/packages/transaction-pay-controller/src/utils/feature-flags.test.ts @@ -1175,21 +1175,22 @@ describe('Feature Flags Utils', () => { describe('getFiatAssetPerTransactionType', () => { const FIAT_ASSET_MOCK: TransactionPayFiatAsset = { address: '0x0000000000000000000000000000000000001010', - caipAssetId: 'eip155:137/slip44:966', chainId: '0x89', - decimals: 18, }; - it('returns undefined when confirmations_pay_fiat flag is absent', () => { + it('returns ETH mainnet fallback when confirmations_pay_fiat flag is absent', () => { const result = getFiatAssetPerTransactionType( messenger, - TransactionType.predictDeposit, + TransactionType.contractInteraction, ); - expect(result).toBeUndefined(); + expect(result).toStrictEqual({ + address: '0x0000000000000000000000000000000000000000', + chainId: '0x1', + }); }); - it('returns undefined when flag exists but has no entry for the transaction type', () => { + it('returns hardcoded asset when flag exists but has no entry for the transaction type', () => { getRemoteFeatureFlagControllerStateMock.mockReturnValue({ ...getDefaultRemoteFeatureFlagControllerState(), remoteFeatureFlags: { @@ -1206,10 +1207,13 @@ describe('Feature Flags Utils', () => { TransactionType.predictDeposit, ); - expect(result).toBeUndefined(); + expect(result).toStrictEqual({ + address: '0x0000000000000000000000000000000000001010', + chainId: '0x89', + }); }); - it('returns the asset when entry matches the transaction type', () => { + it('returns feature flag asset when entry matches the transaction type', () => { getRemoteFeatureFlagControllerStateMock.mockReturnValue({ ...getDefaultRemoteFeatureFlagControllerState(), remoteFeatureFlags: { @@ -1229,7 +1233,7 @@ describe('Feature Flags Utils', () => { expect(result).toStrictEqual(FIAT_ASSET_MOCK); }); - it('returns undefined when assetPerTransactionType is not defined', () => { + it('returns ETH mainnet fallback when assetPerTransactionType is not defined', () => { getRemoteFeatureFlagControllerStateMock.mockReturnValue({ ...getDefaultRemoteFeatureFlagControllerState(), remoteFeatureFlags: { @@ -1237,12 +1241,35 @@ describe('Feature Flags Utils', () => { }, }); + const result = getFiatAssetPerTransactionType( + messenger, + TransactionType.contractInteraction, + ); + + expect(result).toStrictEqual({ + address: '0x0000000000000000000000000000000000000000', + chainId: '0x1', + }); + }); + + it('prefers feature flag over hardcoded asset', () => { + getRemoteFeatureFlagControllerStateMock.mockReturnValue({ + ...getDefaultRemoteFeatureFlagControllerState(), + remoteFeatureFlags: { + confirmations_pay_fiat: { + assetPerTransactionType: { + [TransactionType.predictDeposit]: FIAT_ASSET_MOCK, + }, + }, + }, + }); + const result = getFiatAssetPerTransactionType( messenger, TransactionType.predictDeposit, ); - expect(result).toBeUndefined(); + expect(result).toStrictEqual(FIAT_ASSET_MOCK); }); }); }); diff --git a/packages/transaction-pay-controller/src/utils/feature-flags.ts b/packages/transaction-pay-controller/src/utils/feature-flags.ts index 8ee521f8fc..571722a860 100644 --- a/packages/transaction-pay-controller/src/utils/feature-flags.ts +++ b/packages/transaction-pay-controller/src/utils/feature-flags.ts @@ -6,6 +6,10 @@ import { uniq } from 'lodash'; import { isTransactionPayStrategy, TransactionPayStrategy } from '../constants'; import { projectLogger } from '../logger'; import type { TransactionPayFiatAsset } from '../strategy/fiat/constants'; +import { + ETH_MAINNET_FIAT_ASSET, + FIAT_ASSET_ID_BY_TX_TYPE, +} from '../strategy/fiat/constants'; import { RELAY_EXECUTE_URL, RELAY_POLLING_INTERVAL, @@ -77,6 +81,12 @@ type StrategyOverrides = { transactionTypes: Record; }; +type FiatFlags = { + assetPerTransactionType?: Partial< + Record + >; +}; + type StrategyRoutingConfig = { payStrategies: { across: { @@ -664,26 +674,35 @@ function getCaseInsensitive( } /** - * Get the fiat asset override for a specific transaction type from feature flags. + * Get the fiat asset for a specific transaction type. + * + * Resolution order: + * 1. Feature flag override (`confirmations_pay_fiat.assetPerTransactionType`) + * 2. Hardcoded constant (`FIAT_ASSET_ID_BY_TX_TYPE`) + * 3. ETH mainnet fallback * * @param messenger - Controller messenger. * @param transactionType - Transaction type to look up. - * @returns The fiat asset if configured, undefined otherwise. + * @returns The fiat asset for the given transaction type. */ export function getFiatAssetPerTransactionType( messenger: TransactionPayControllerMessenger, - transactionType: TransactionType, -): TransactionPayFiatAsset | undefined { + transactionType?: TransactionType, +): TransactionPayFiatAsset { + if (!transactionType) { + return ETH_MAINNET_FIAT_ASSET; + } + const state = messenger.call('RemoteFeatureFlagController:getState'); const fiatFlags = state.remoteFeatureFlags?.confirmations_pay_fiat as - | { - assetPerTransactionType?: Partial< - Record - >; - } + | FiatFlags | undefined; - return fiatFlags?.assetPerTransactionType?.[transactionType]; + return ( + fiatFlags?.assetPerTransactionType?.[transactionType] ?? + FIAT_ASSET_ID_BY_TX_TYPE[transactionType] ?? + ETH_MAINNET_FIAT_ASSET + ); } /** diff --git a/packages/transaction-pay-controller/src/utils/token.test.ts b/packages/transaction-pay-controller/src/utils/token.test.ts index 0e5cdc9343..e31fec3cc8 100644 --- a/packages/transaction-pay-controller/src/utils/token.test.ts +++ b/packages/transaction-pay-controller/src/utils/token.test.ts @@ -13,6 +13,7 @@ import { } from '../constants'; import { getMessengerMock } from '../tests/messenger-mock'; import { + buildCaipAssetType, computeRawFromFiatAmount, computeTokenAmounts, getTokenBalance, @@ -838,4 +839,36 @@ describe('Token Utils', () => { expect(isSameToken(token1, token2)).toBe(false); }); }); + + describe('buildCaipAssetType', () => { + it('returns slip44 asset type for native token on mainnet', () => { + expect( + buildCaipAssetType('0x1' as Hex, NATIVE_TOKEN_ADDRESS), + ).toBe('eip155:1/slip44:60'); + }); + + it('returns slip44 asset type for Polygon native token', () => { + const polygonNative = + '0x0000000000000000000000000000000000001010' as Hex; + + expect(buildCaipAssetType('0x89' as Hex, polygonNative, 966)).toBe( + 'eip155:137/slip44:966', + ); + }); + + it('returns erc20 asset type for ERC-20 token', () => { + const usdcAddress = + '0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48' as Hex; + + expect(buildCaipAssetType('0x1' as Hex, usdcAddress)).toBe( + `eip155:1/erc20:${usdcAddress}`, + ); + }); + + it('defaults slip44CoinType to 60 for native tokens', () => { + expect( + buildCaipAssetType('0xa4b1' as Hex, NATIVE_TOKEN_ADDRESS), + ).toBe('eip155:42161/slip44:60'); + }); + }); }); diff --git a/packages/transaction-pay-controller/src/utils/token.ts b/packages/transaction-pay-controller/src/utils/token.ts index a0a2c40b96..de33212cd4 100644 --- a/packages/transaction-pay-controller/src/utils/token.ts +++ b/packages/transaction-pay-controller/src/utils/token.ts @@ -3,7 +3,8 @@ import { Web3Provider } from '@ethersproject/providers'; import { 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 type { CaipAssetType, Hex } from '@metamask/utils'; +import { hexToBigInt, toCaipAssetType } from '@metamask/utils'; import { BigNumber } from 'bignumber.js'; import { @@ -340,6 +341,40 @@ export async function getLiveTokenBalance( return balance.toString(); } +/** + * Build a CAIP-19 asset type identifier for an EVM token. + * + * @param chainId - Hex chain ID (e.g. `0x1`). + * @param tokenAddress - Token contract address, or the native token address. + * @param slip44CoinType - SLIP-44 coin type for native tokens (defaults to 60 / ETH). + * @returns CAIP-19 asset type string. + */ +export function buildCaipAssetType( + chainId: Hex, + tokenAddress: Hex, + slip44CoinType = 60, +): CaipAssetType { + const chainReference = String(hexToBigInt(chainId)); + const isNative = + tokenAddress.toLowerCase() === getNativeToken(chainId).toLowerCase(); + + if (isNative) { + return toCaipAssetType( + 'eip155', + chainReference, + 'slip44', + String(slip44CoinType), + ); + } + + return toCaipAssetType( + 'eip155', + chainReference, + 'erc20', + tokenAddress, + ); +} + function getTicker( chainId: Hex, messenger: TransactionPayControllerMessenger, From 46b5349ae65e8857c2f47739e815b55400c5eb2b Mon Sep 17 00:00:00 2001 From: Goktug Poyraz Date: Tue, 5 May 2026 11:25:25 +0200 Subject: [PATCH 3/7] Update --- .../src/TransactionPayController.test.ts | 6 ++--- .../src/TransactionPayController.ts | 8 ++++-- .../src/strategy/fiat/fiat-quotes.test.ts | 7 ++++- .../src/strategy/fiat/fiat-quotes.ts | 3 ++- .../src/strategy/fiat/fiat-submit.test.ts | 20 +++++++++----- .../src/strategy/fiat/fiat-submit.ts | 27 +++++++++++++------ .../src/utils/token.test.ts | 11 +++++++- .../src/utils/token.ts | 17 +++++++++--- 8 files changed, 73 insertions(+), 26 deletions(-) diff --git a/packages/transaction-pay-controller/src/TransactionPayController.test.ts b/packages/transaction-pay-controller/src/TransactionPayController.test.ts index bd8e5a4cfe..9cdd0b9c39 100644 --- a/packages/transaction-pay-controller/src/TransactionPayController.test.ts +++ b/packages/transaction-pay-controller/src/TransactionPayController.test.ts @@ -646,9 +646,7 @@ describe('TransactionPayController', () => { const CAIP_ASSET_ID_MOCK = 'eip155:137/slip44:966'; const FIAT_ASSET_MOCK = { address: '0x0000000000000000000000000000000000001010' as Hex, - caipAssetId: CAIP_ASSET_ID_MOCK, chainId: '0x89' as Hex, - decimals: 18, }; let setSelectedTokenMock: jest.Mock; @@ -724,7 +722,9 @@ describe('TransactionPayController', () => { it('does not call setSelectedToken when fiat asset cannot be derived', () => { getTransactionMock.mockReturnValue(TRANSACTION_META_MOCK); - deriveFiatAssetForFiatPaymentMock.mockReturnValue(undefined); + deriveFiatAssetForFiatPaymentMock.mockReturnValue( + undefined as never, + ); const updateTransactionData = getUpdateTransactionData(); diff --git a/packages/transaction-pay-controller/src/TransactionPayController.ts b/packages/transaction-pay-controller/src/TransactionPayController.ts index e1b7670379..06df21d170 100644 --- a/packages/transaction-pay-controller/src/TransactionPayController.ts +++ b/packages/transaction-pay-controller/src/TransactionPayController.ts @@ -24,6 +24,7 @@ import type { UpdatePaymentTokenRequest, } from './types'; import { getStrategyOrder } from './utils/feature-flags'; +import { buildCaipAssetType } from './utils/token'; import { updateQuotes } from './utils/quotes'; import { updateSourceAmounts } from './utils/source-amounts'; import { getTransaction, pollTransactionChanges } from './utils/transaction'; @@ -284,12 +285,15 @@ export class TransactionPayController extends BaseController< transactionId, this.messenger, ) as TransactionMeta; - const fiatAsset = deriveFiatAssetForFiatPayment(transaction); + const fiatAsset = deriveFiatAssetForFiatPayment( + transaction, + this.messenger, + ); if (fiatAsset) { try { this.messenger.call( 'RampsController:setSelectedToken', - fiatAsset.caipAssetId, + buildCaipAssetType(fiatAsset.chainId, fiatAsset.address), ); } catch { // Intentionally no-op — tokens may not be loaded in RampsController yet. diff --git a/packages/transaction-pay-controller/src/strategy/fiat/fiat-quotes.test.ts b/packages/transaction-pay-controller/src/strategy/fiat/fiat-quotes.test.ts index 0dcf662203..5a5766b3af 100644 --- a/packages/transaction-pay-controller/src/strategy/fiat/fiat-quotes.test.ts +++ b/packages/transaction-pay-controller/src/strategy/fiat/fiat-quotes.test.ts @@ -14,6 +14,7 @@ import type { TransactionPayRequiredToken, } from '../../types'; import { + buildCaipAssetType, computeRawFromFiatAmount, getTokenFiatRate, getTokenInfo, @@ -201,7 +202,10 @@ function getRequest({ }; } +const FIAT_ASSET_CAIP_ID_MOCK = 'eip155:137/slip44:966'; + describe('getFiatQuotes', () => { + const buildCaipAssetTypeMock = jest.mocked(buildCaipAssetType); const getRelayQuotesMock = jest.mocked(getRelayQuotes); const getTokenFiatRateMock = jest.mocked(getTokenFiatRate); const getTokenInfoMock = jest.mocked(getTokenInfo); @@ -213,6 +217,7 @@ describe('getFiatQuotes', () => { beforeEach(() => { jest.resetAllMocks(); + buildCaipAssetTypeMock.mockReturnValue(FIAT_ASSET_CAIP_ID_MOCK); deriveFiatAssetForFiatPaymentMock.mockReturnValue(FIAT_ASSET_MOCK); getTokenFiatRateMock.mockReturnValue({ fiatRate: '2', @@ -246,7 +251,7 @@ describe('getFiatQuotes', () => { 'RampsController:getQuotes', expect.objectContaining({ amount: 20, - assetId: FIAT_ASSET_MOCK.caipAssetId, + assetId: FIAT_ASSET_CAIP_ID_MOCK, fiat: 'USD', paymentMethods: ['/payments/debit-credit-card'], providers: [SELECTED_PROVIDER_ID], diff --git a/packages/transaction-pay-controller/src/strategy/fiat/fiat-quotes.ts b/packages/transaction-pay-controller/src/strategy/fiat/fiat-quotes.ts index 4049bf4667..e401447f53 100644 --- a/packages/transaction-pay-controller/src/strategy/fiat/fiat-quotes.ts +++ b/packages/transaction-pay-controller/src/strategy/fiat/fiat-quotes.ts @@ -12,6 +12,7 @@ import type { TransactionPayQuote, } from '../../types'; import { + buildCaipAssetType, computeRawFromFiatAmount, getTokenFiatRate, getTokenInfo, @@ -170,7 +171,7 @@ async function getRampsQuote({ const quotes = await messenger.call('RampsController:getQuotes', { amount: adjustedAmount, - assetId: fiatAsset.caipAssetId, + assetId: buildCaipAssetType(fiatAsset.chainId, fiatAsset.address), fiat: DEFAULT_FIAT_CURRENCY, paymentMethods: [fiatPaymentMethod], providers: selectedProviderId ? [selectedProviderId] : undefined, diff --git a/packages/transaction-pay-controller/src/strategy/fiat/fiat-submit.test.ts b/packages/transaction-pay-controller/src/strategy/fiat/fiat-submit.test.ts index 8b94b88f2f..d76caa3ddb 100644 --- a/packages/transaction-pay-controller/src/strategy/fiat/fiat-submit.test.ts +++ b/packages/transaction-pay-controller/src/strategy/fiat/fiat-submit.test.ts @@ -20,9 +20,11 @@ import type { RelayQuote } from '../relay/types'; import type { TransactionPayFiatAsset } from './constants'; import { submitFiatQuotes } from './fiat-submit'; import type { FiatQuote } from './types'; +import { buildCaipAssetType, getTokenInfo } from '../../utils/token'; import { deriveFiatAssetForFiatPayment } from './utils'; jest.mock('./utils'); +jest.mock('../../utils/token'); jest.mock('../relay/relay-quotes'); jest.mock('../relay/relay-submit'); @@ -40,9 +42,7 @@ const TRANSACTION_MOCK = { const FIAT_ASSET_MOCK: TransactionPayFiatAsset = { address: '0x0000000000000000000000000000000000001010', - caipAssetId: 'eip155:137/slip44:966', chainId: '0x89', - decimals: 18, }; const RAMPS_QUOTE_MOCK: RampsQuote = { @@ -227,7 +227,11 @@ function getRequest({ }; } +const FIAT_ASSET_CAIP_ID_MOCK = 'eip155:137/slip44:966'; + describe('submitFiatQuotes', () => { + const buildCaipAssetTypeMock = jest.mocked(buildCaipAssetType); + const getTokenInfoMock = jest.mocked(getTokenInfo); const deriveFiatAssetForFiatPaymentMock = jest.mocked( deriveFiatAssetForFiatPayment, ); @@ -238,6 +242,8 @@ describe('submitFiatQuotes', () => { jest.resetAllMocks(); jest.useRealTimers(); + buildCaipAssetTypeMock.mockReturnValue(FIAT_ASSET_CAIP_ID_MOCK); + getTokenInfoMock.mockReturnValue({ decimals: 18, symbol: 'POL' }); deriveFiatAssetForFiatPaymentMock.mockReturnValue(FIAT_ASSET_MOCK); getRelayQuotesMock.mockResolvedValue([RELAY_QUOTE_RESULT_MOCK]); submitRelayQuotesMock.mockResolvedValue({ @@ -249,7 +255,7 @@ describe('submitFiatQuotes', () => { const order = getFiatOrderMock({ cryptoAmount: '1.2345', cryptoCurrency: { - assetId: FIAT_ASSET_MOCK.caipAssetId, + assetId: FIAT_ASSET_CAIP_ID_MOCK, chainId: 'eip155:137', symbol: 'POL', }, @@ -463,12 +469,12 @@ describe('submitFiatQuotes', () => { dateNowSpy.mockRestore(); }); - it('throws if fiat asset mapping is missing', async () => { - deriveFiatAssetForFiatPaymentMock.mockReturnValue(undefined); + it('throws if token info is unavailable for the fiat asset', async () => { + getTokenInfoMock.mockReturnValue(undefined); const { request } = getRequest(); await expect(submitFiatQuotes(request)).rejects.toThrow( - 'Missing fiat asset mapping for transaction type: predictDeposit', + `Unable to resolve token info for fiat asset ${FIAT_ASSET_MOCK.address} on chain ${FIAT_ASSET_MOCK.chainId}`, ); }); @@ -483,7 +489,7 @@ describe('submitFiatQuotes', () => { }); await expect(submitFiatQuotes(request)).rejects.toThrow( - `Fiat order asset mismatch for transaction ${TRANSACTION_ID_MOCK}: expected ${FIAT_ASSET_MOCK.caipAssetId}, got eip155:137/slip44:60`, + `Fiat order asset mismatch for transaction ${TRANSACTION_ID_MOCK}: expected ${FIAT_ASSET_CAIP_ID_MOCK.toLowerCase()}, got eip155:137/slip44:60`, ); }); diff --git a/packages/transaction-pay-controller/src/strategy/fiat/fiat-submit.ts b/packages/transaction-pay-controller/src/strategy/fiat/fiat-submit.ts index 94c2234428..f611e6cf19 100644 --- a/packages/transaction-pay-controller/src/strategy/fiat/fiat-submit.ts +++ b/packages/transaction-pay-controller/src/strategy/fiat/fiat-submit.ts @@ -14,6 +14,7 @@ import type { QuoteRequest, TransactionPayControllerMessenger, } from '../../types'; +import { buildCaipAssetType, getTokenInfo } from '../../utils/token'; import { getRelayQuotes } from '../relay/relay-quotes'; import { submitRelayQuotes } from '../relay/relay-submit'; import type { RelayQuote } from '../relay/types'; @@ -162,7 +163,10 @@ function validateOrderAsset({ transactionId: string; }): void { const orderAssetId = orderCrypto?.assetId?.toLowerCase(); - const expectedAssetId = expectedAsset.caipAssetId.toLowerCase(); + const expectedAssetId = buildCaipAssetType( + expectedAsset.chainId, + expectedAsset.address, + ).toLowerCase(); const expectedChainId = expectedAssetId.split('/')[0]; const orderChainId = orderCrypto?.chainId?.toLowerCase(); @@ -321,12 +325,7 @@ async function submitRelayAfterFiatCompletion({ throw new Error('Multiple fiat quotes are not supported for submission'); } - const fiatAsset = deriveFiatAssetForFiatPayment(transaction); - if (!fiatAsset) { - throw new Error( - `Missing fiat asset mapping for transaction type: ${String(transaction.type)}`, - ); - } + const fiatAsset = deriveFiatAssetForFiatPayment(transaction, messenger); validateOrderAsset({ expectedAsset: fiatAsset, @@ -334,9 +333,21 @@ async function submitRelayAfterFiatCompletion({ transactionId, }); + const tokenInfo = getTokenInfo( + messenger, + fiatAsset.address, + fiatAsset.chainId, + ); + + if (!tokenInfo) { + throw new Error( + `Unable to resolve token info for fiat asset ${fiatAsset.address} on chain ${fiatAsset.chainId}`, + ); + } + const sourceAmountRaw = getRawSourceAmountFromOrder({ cryptoAmount: order.cryptoAmount, - decimals: fiatAsset.decimals, + decimals: tokenInfo.decimals, }); const baseRequest = quotes[0].request; diff --git a/packages/transaction-pay-controller/src/utils/token.test.ts b/packages/transaction-pay-controller/src/utils/token.test.ts index e31fec3cc8..822fed783d 100644 --- a/packages/transaction-pay-controller/src/utils/token.test.ts +++ b/packages/transaction-pay-controller/src/utils/token.test.ts @@ -847,7 +847,16 @@ describe('Token Utils', () => { ).toBe('eip155:1/slip44:60'); }); - it('returns slip44 asset type for Polygon native token', () => { + it('returns slip44 asset type for Polygon native token with auto-mapped coin type', () => { + const polygonNative = + '0x0000000000000000000000000000000000001010' as Hex; + + expect(buildCaipAssetType('0x89' as Hex, polygonNative)).toBe( + 'eip155:137/slip44:966', + ); + }); + + it('returns slip44 asset type with explicit coin type override', () => { const polygonNative = '0x0000000000000000000000000000000000001010' as Hex; diff --git a/packages/transaction-pay-controller/src/utils/token.ts b/packages/transaction-pay-controller/src/utils/token.ts index de33212cd4..15c18f3287 100644 --- a/packages/transaction-pay-controller/src/utils/token.ts +++ b/packages/transaction-pay-controller/src/utils/token.ts @@ -341,29 +341,40 @@ export async function getLiveTokenBalance( return balance.toString(); } +const SLIP44_COIN_TYPE_BY_CHAIN: Record = { + [CHAIN_ID_POLYGON]: 966, // POL +}; + /** * Build a CAIP-19 asset type identifier for an EVM token. * + * For native tokens the SLIP-44 coin type is resolved automatically from + * a built-in chain→coin-type map, falling back to 60 (ETH). Callers can + * override via the optional third parameter. + * * @param chainId - Hex chain ID (e.g. `0x1`). * @param tokenAddress - Token contract address, or the native token address. - * @param slip44CoinType - SLIP-44 coin type for native tokens (defaults to 60 / ETH). + * @param slip44CoinType - Optional SLIP-44 coin type override for native tokens. * @returns CAIP-19 asset type string. */ export function buildCaipAssetType( chainId: Hex, tokenAddress: Hex, - slip44CoinType = 60, + slip44CoinType?: number, ): CaipAssetType { const chainReference = String(hexToBigInt(chainId)); const isNative = tokenAddress.toLowerCase() === getNativeToken(chainId).toLowerCase(); if (isNative) { + const coinType = + slip44CoinType ?? SLIP44_COIN_TYPE_BY_CHAIN[chainId] ?? 60; + return toCaipAssetType( 'eip155', chainReference, 'slip44', - String(slip44CoinType), + String(coinType), ); } From 966f75e99dfbeda01b0578b5715e11d9e9554576 Mon Sep 17 00:00:00 2001 From: Goktug Poyraz Date: Tue, 5 May 2026 11:26:26 +0200 Subject: [PATCH 4/7] Update --- packages/transaction-pay-controller/src/constants.ts | 4 ++++ packages/transaction-pay-controller/src/utils/token.ts | 5 +---- 2 files changed, 5 insertions(+), 4 deletions(-) diff --git a/packages/transaction-pay-controller/src/constants.ts b/packages/transaction-pay-controller/src/constants.ts index 72c7a5f842..c740bdbd6d 100644 --- a/packages/transaction-pay-controller/src/constants.ts +++ b/packages/transaction-pay-controller/src/constants.ts @@ -20,6 +20,10 @@ export const HYPERCORE_USDC_ADDRESS = '0x00000000000000000000000000000000'; export const HYPERCORE_USDC_DECIMALS = 8; export const USDC_DECIMALS = 6; +export const SLIP44_COIN_TYPE_BY_CHAIN: Record = { + [CHAIN_ID_POLYGON]: 966, // POL +}; + export const STABLECOINS: Record = { // Mainnet '0x1': [ diff --git a/packages/transaction-pay-controller/src/utils/token.ts b/packages/transaction-pay-controller/src/utils/token.ts index 15c18f3287..eb8971bf52 100644 --- a/packages/transaction-pay-controller/src/utils/token.ts +++ b/packages/transaction-pay-controller/src/utils/token.ts @@ -10,6 +10,7 @@ import { BigNumber } from 'bignumber.js'; import { CHAIN_ID_POLYGON, NATIVE_TOKEN_ADDRESS, + SLIP44_COIN_TYPE_BY_CHAIN, STABLECOINS, } from '../constants'; import type { FiatRates, TransactionPayControllerMessenger } from '../types'; @@ -341,10 +342,6 @@ export async function getLiveTokenBalance( return balance.toString(); } -const SLIP44_COIN_TYPE_BY_CHAIN: Record = { - [CHAIN_ID_POLYGON]: 966, // POL -}; - /** * Build a CAIP-19 asset type identifier for an EVM token. * From 75e5a37e7d0fb627e5b445b5506325fac94323d5 Mon Sep 17 00:00:00 2001 From: Goktug Poyraz Date: Tue, 5 May 2026 11:30:34 +0200 Subject: [PATCH 5/7] Fix lint issues --- .../src/TransactionPayController.test.ts | 4 +--- .../src/utils/token.test.ts | 21 ++++++++----------- .../src/utils/token.ts | 10 ++------- 3 files changed, 12 insertions(+), 23 deletions(-) diff --git a/packages/transaction-pay-controller/src/TransactionPayController.test.ts b/packages/transaction-pay-controller/src/TransactionPayController.test.ts index 9cdd0b9c39..9e9b8283e2 100644 --- a/packages/transaction-pay-controller/src/TransactionPayController.test.ts +++ b/packages/transaction-pay-controller/src/TransactionPayController.test.ts @@ -722,9 +722,7 @@ describe('TransactionPayController', () => { it('does not call setSelectedToken when fiat asset cannot be derived', () => { getTransactionMock.mockReturnValue(TRANSACTION_META_MOCK); - deriveFiatAssetForFiatPaymentMock.mockReturnValue( - undefined as never, - ); + deriveFiatAssetForFiatPaymentMock.mockReturnValue(undefined as never); const updateTransactionData = getUpdateTransactionData(); diff --git a/packages/transaction-pay-controller/src/utils/token.test.ts b/packages/transaction-pay-controller/src/utils/token.test.ts index 822fed783d..bb129d43b8 100644 --- a/packages/transaction-pay-controller/src/utils/token.test.ts +++ b/packages/transaction-pay-controller/src/utils/token.test.ts @@ -842,14 +842,13 @@ describe('Token Utils', () => { describe('buildCaipAssetType', () => { it('returns slip44 asset type for native token on mainnet', () => { - expect( - buildCaipAssetType('0x1' as Hex, NATIVE_TOKEN_ADDRESS), - ).toBe('eip155:1/slip44:60'); + expect(buildCaipAssetType('0x1' as Hex, NATIVE_TOKEN_ADDRESS)).toBe( + 'eip155:1/slip44:60', + ); }); it('returns slip44 asset type for Polygon native token with auto-mapped coin type', () => { - const polygonNative = - '0x0000000000000000000000000000000000001010' as Hex; + const polygonNative = '0x0000000000000000000000000000000000001010' as Hex; expect(buildCaipAssetType('0x89' as Hex, polygonNative)).toBe( 'eip155:137/slip44:966', @@ -857,8 +856,7 @@ describe('Token Utils', () => { }); it('returns slip44 asset type with explicit coin type override', () => { - const polygonNative = - '0x0000000000000000000000000000000000001010' as Hex; + const polygonNative = '0x0000000000000000000000000000000000001010' as Hex; expect(buildCaipAssetType('0x89' as Hex, polygonNative, 966)).toBe( 'eip155:137/slip44:966', @@ -866,8 +864,7 @@ describe('Token Utils', () => { }); it('returns erc20 asset type for ERC-20 token', () => { - const usdcAddress = - '0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48' as Hex; + const usdcAddress = '0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48' as Hex; expect(buildCaipAssetType('0x1' as Hex, usdcAddress)).toBe( `eip155:1/erc20:${usdcAddress}`, @@ -875,9 +872,9 @@ describe('Token Utils', () => { }); it('defaults slip44CoinType to 60 for native tokens', () => { - expect( - buildCaipAssetType('0xa4b1' as Hex, NATIVE_TOKEN_ADDRESS), - ).toBe('eip155:42161/slip44:60'); + expect(buildCaipAssetType('0xa4b1' as Hex, NATIVE_TOKEN_ADDRESS)).toBe( + 'eip155:42161/slip44:60', + ); }); }); }); diff --git a/packages/transaction-pay-controller/src/utils/token.ts b/packages/transaction-pay-controller/src/utils/token.ts index eb8971bf52..b7d4fde67c 100644 --- a/packages/transaction-pay-controller/src/utils/token.ts +++ b/packages/transaction-pay-controller/src/utils/token.ts @@ -364,8 +364,7 @@ export function buildCaipAssetType( tokenAddress.toLowerCase() === getNativeToken(chainId).toLowerCase(); if (isNative) { - const coinType = - slip44CoinType ?? SLIP44_COIN_TYPE_BY_CHAIN[chainId] ?? 60; + const coinType = slip44CoinType ?? SLIP44_COIN_TYPE_BY_CHAIN[chainId] ?? 60; return toCaipAssetType( 'eip155', @@ -375,12 +374,7 @@ export function buildCaipAssetType( ); } - return toCaipAssetType( - 'eip155', - chainReference, - 'erc20', - tokenAddress, - ); + return toCaipAssetType('eip155', chainReference, 'erc20', tokenAddress); } function getTicker( From 72f0ef0412eb6981b6f826d05297f14d69aa9ec5 Mon Sep 17 00:00:00 2001 From: Goktug Poyraz Date: Tue, 5 May 2026 11:37:09 +0200 Subject: [PATCH 6/7] Fix lint --- .../src/TransactionPayController.ts | 2 +- .../src/strategy/fiat/fiat-submit.test.ts | 2 +- .../src/strategy/fiat/utils.test.ts | 4 ++-- 3 files changed, 4 insertions(+), 4 deletions(-) diff --git a/packages/transaction-pay-controller/src/TransactionPayController.ts b/packages/transaction-pay-controller/src/TransactionPayController.ts index 06df21d170..b192ec91bd 100644 --- a/packages/transaction-pay-controller/src/TransactionPayController.ts +++ b/packages/transaction-pay-controller/src/TransactionPayController.ts @@ -24,9 +24,9 @@ import type { UpdatePaymentTokenRequest, } from './types'; import { getStrategyOrder } from './utils/feature-flags'; -import { buildCaipAssetType } from './utils/token'; import { updateQuotes } from './utils/quotes'; import { updateSourceAmounts } from './utils/source-amounts'; +import { buildCaipAssetType } from './utils/token'; import { getTransaction, pollTransactionChanges } from './utils/transaction'; const MESSENGER_EXPOSED_METHODS = [ diff --git a/packages/transaction-pay-controller/src/strategy/fiat/fiat-submit.test.ts b/packages/transaction-pay-controller/src/strategy/fiat/fiat-submit.test.ts index d76caa3ddb..3316f9c537 100644 --- a/packages/transaction-pay-controller/src/strategy/fiat/fiat-submit.test.ts +++ b/packages/transaction-pay-controller/src/strategy/fiat/fiat-submit.test.ts @@ -14,13 +14,13 @@ import type { QuoteRequest, TransactionPayQuote, } from '../../types'; +import { buildCaipAssetType, getTokenInfo } from '../../utils/token'; import { getRelayQuotes } from '../relay/relay-quotes'; import { submitRelayQuotes } from '../relay/relay-submit'; import type { RelayQuote } from '../relay/types'; import type { TransactionPayFiatAsset } from './constants'; import { submitFiatQuotes } from './fiat-submit'; import type { FiatQuote } from './types'; -import { buildCaipAssetType, getTokenInfo } from '../../utils/token'; import { deriveFiatAssetForFiatPayment } from './utils'; jest.mock('./utils'); diff --git a/packages/transaction-pay-controller/src/strategy/fiat/utils.test.ts b/packages/transaction-pay-controller/src/strategy/fiat/utils.test.ts index 41a458f30e..5f91a94114 100644 --- a/packages/transaction-pay-controller/src/strategy/fiat/utils.test.ts +++ b/packages/transaction-pay-controller/src/strategy/fiat/utils.test.ts @@ -1,11 +1,11 @@ import type { TransactionMeta } from '@metamask/transaction-controller'; import { TransactionType } from '@metamask/transaction-controller'; -import { ETH_MAINNET_FIAT_ASSET, FIAT_ASSET_ID_BY_TX_TYPE } from './constants'; -import { deriveFiatAssetForFiatPayment } from './utils'; import { getDefaultRemoteFeatureFlagControllerState } from '../../../../remote-feature-flag-controller/src/remote-feature-flag-controller'; import { getMessengerMock } from '../../tests/messenger-mock'; +import { ETH_MAINNET_FIAT_ASSET, FIAT_ASSET_ID_BY_TX_TYPE } from './constants'; import type { TransactionPayFiatAsset } from './constants'; +import { deriveFiatAssetForFiatPayment } from './utils'; const FEATURE_FLAG_ASSET_MOCK: TransactionPayFiatAsset = { address: '0x0000000000000000000000000000000000000abc', From 8c264962faca69bdb7fa96b2b01b7367ec4d42c2 Mon Sep 17 00:00:00 2001 From: Goktug Poyraz Date: Tue, 5 May 2026 11:38:53 +0200 Subject: [PATCH 7/7] Changelog update --- packages/transaction-pay-controller/CHANGELOG.md | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/packages/transaction-pay-controller/CHANGELOG.md b/packages/transaction-pay-controller/CHANGELOG.md index e8423f5d4b..9ca4c43d21 100644 --- a/packages/transaction-pay-controller/CHANGELOG.md +++ b/packages/transaction-pay-controller/CHANGELOG.md @@ -13,9 +13,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ### Changed -- Add feature-flag-driven fiat asset resolution in `deriveFiatAssetForFiatPayment` ([#8631](https://github.com/MetaMask/core/pull/8631)) - - Read asset per transaction type from `confirmations_pay_fiat` remote feature flag before falling back to hardcoded map - - Fall back to ETH on mainnet when neither feature flag nor hardcoded map has an entry +- Resolve fiat asset per transaction type from `confirmations_pay_fiat` remote feature flag, falling back to hardcoded map then ETH on mainnet ([#8631](https://github.com/MetaMask/core/pull/8631)) ## [21.0.0]