Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 6 additions & 0 deletions packages/transaction-pay-controller/CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,12 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0

## [Unreleased]

### 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

## [20.0.1]

### Changed
Expand Down
1 change: 1 addition & 0 deletions packages/transaction-pay-controller/src/constants.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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;

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,32 +3,31 @@ import type { Hex } from '@metamask/utils';

import {
CHAIN_ID_ARBITRUM,
CHAIN_ID_MAINNET,
CHAIN_ID_POLYGON,
NATIVE_TOKEN_ADDRESS,
} from '../../constants';

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,
};

// We might use feature flags to determine these later.
export const ETH_MAINNET_FIAT_ASSET: TransactionPayFiatAsset = {
address: NATIVE_TOKEN_ADDRESS,
chainId: CHAIN_ID_MAINNET,
};

export const FIAT_ASSET_ID_BY_TX_TYPE: Partial<
Record<TransactionType, TransactionPayFiatAsset>
> = {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,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';
Expand Down Expand Up @@ -51,9 +55,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 = {
Expand Down Expand Up @@ -177,6 +179,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,
Expand All @@ -191,6 +194,7 @@ describe('getFiatQuotes', () => {
fiatRate: '2',
usdRate: '2',
});
getTokenInfoMock.mockReturnValue({ decimals: 18, symbol: 'POL' });
computeRawFromFiatAmountMock.mockReturnValue('5000000000000000000');
getRelayQuotesMock.mockResolvedValue([getRelayQuoteMock()]);
pickBestFiatQuoteMock.mockReturnValue(FIAT_QUOTE_MOCK);
Expand Down Expand Up @@ -316,8 +320,8 @@ describe('getFiatQuotes', () => {
expect(getRelayQuotesMock).not.toHaveBeenCalled();
});

it('returns empty array if fiat asset mapping is missing', async () => {
deriveFiatAssetForFiatPaymentMock.mockReturnValue(undefined);
it('returns empty array if source token fiat rate is missing', async () => {
getTokenFiatRateMock.mockReturnValue(undefined);
const { request } = getRequest();

const result = await getFiatQuotes(request);
Expand All @@ -326,8 +330,8 @@ describe('getFiatQuotes', () => {
expect(getRelayQuotesMock).not.toHaveBeenCalled();
});

it('returns empty array if source token fiat rate is missing', async () => {
getTokenFiatRateMock.mockReturnValue(undefined);
it('returns empty array if token info is unavailable', async () => {
getTokenInfoMock.mockReturnValue(undefined);
const { request } = getRequest();

const result = await getFiatQuotes(request);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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 type { FiatQuote } from './types';
Expand Down Expand Up @@ -44,14 +48,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 [];
}

Expand Down Expand Up @@ -164,7 +163,6 @@ function buildRelayRequestFromAmountFiat({
fiatAsset: {
address: Hex;
chainId: Hex;
decimals: number;
};
messenger: PayStrategyGetQuotesRequest['messenger'];
requiredToken: TransactionPayRequiredToken;
Expand All @@ -180,9 +178,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,
);

Expand Down
141 changes: 133 additions & 8 deletions packages/transaction-pay-controller/src/strategy/fiat/utils.test.ts
Original file line number Diff line number Diff line change
@@ -1,45 +1,170 @@
import type { TransactionMeta } from '@metamask/transaction-controller';
import { TransactionType } from '@metamask/transaction-controller';

import { FIAT_ASSET_ID_BY_TX_TYPE } from './constants';
import { getDefaultRemoteFeatureFlagControllerState } from '../../../../remote-feature-flag-controller/src/remote-feature-flag-controller';
import { getMessengerMock } from '../../tests/messenger-mock';
import type { TransactionPayFiatAsset } from './constants';
import { ETH_MAINNET_FIAT_ASSET, FIAT_ASSET_ID_BY_TX_TYPE } from './constants';
import type { FiatQuotesResponse } from './types';
import { deriveFiatAssetForFiatPayment, pickBestFiatQuote } from './utils';

const FEATURE_FLAG_ASSET_MOCK: TransactionPayFiatAsset = {
address: '0x0000000000000000000000000000000000000abc',
chainId: '0xa',
};

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 supported 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('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 undefined for unsupported type', () => {
it('returns feature flag asset for supported 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).toBeUndefined();
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).toStrictEqual(ETH_MAINNET_FIAT_ASSET);
});
});

Expand Down
Loading