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
4 changes: 4 additions & 0 deletions packages/transaction-pay-controller/CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,10 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0

## [Unreleased]

### Added

- Implement fiat strategy submit flow with order polling and relay execution ([#8347](https://github.com/MetaMask/core/pull/8347))

## [19.0.3]

### Changed
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,18 +7,21 @@ import { TransactionPayController } from '.';
import { updateFiatPayment } from './actions/update-fiat-payment';
import { updatePaymentToken } from './actions/update-payment-token';
import { TransactionPayStrategy } from './constants';
import { deriveFiatAssetForFiatPayment } from './strategy/fiat/utils';
import { getMessengerMock } from './tests/messenger-mock';
import type {
TransactionPayControllerMessenger,
TransactionPaySourceAmount,
UpdateTransactionDataCallback,
} from './types';
import { getStrategyOrder } from './utils/feature-flags';
import { updateQuotes } from './utils/quotes';
import { updateSourceAmounts } from './utils/source-amounts';
import { pollTransactionChanges } from './utils/transaction';
import { getTransaction, pollTransactionChanges } from './utils/transaction';

jest.mock('./actions/update-fiat-payment');
jest.mock('./actions/update-payment-token');
jest.mock('./strategy/fiat/utils');
jest.mock('./utils/source-amounts');
jest.mock('./utils/quotes');
jest.mock('./utils/transaction');
Expand All @@ -32,6 +35,10 @@ const CHAIN_ID_MOCK = '0x1' as Hex;
describe('TransactionPayController', () => {
const updateFiatPaymentMock = jest.mocked(updateFiatPayment);
const updatePaymentTokenMock = jest.mocked(updatePaymentToken);
const deriveFiatAssetForFiatPaymentMock = jest.mocked(
deriveFiatAssetForFiatPayment,
);
const getTransactionMock = jest.mocked(getTransaction);
const updateSourceAmountsMock = jest.mocked(updateSourceAmounts);
const updateQuotesMock = jest.mocked(updateQuotes);
const pollTransactionChangesMock = jest.mocked(pollTransactionChanges);
Expand Down Expand Up @@ -503,4 +510,129 @@ describe('TransactionPayController', () => {
).toBeUndefined();
});
});

describe('fiat token selection', () => {
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;

beforeEach(() => {
setSelectedTokenMock = jest.fn();
messenger.registerActionHandler(
'RampsController:setSelectedToken' as never,
setSelectedTokenMock as never,
);
});

function getUpdateTransactionData(): UpdateTransactionDataCallback {
const controller = createController();
controller.updatePaymentToken({
transactionId: TRANSACTION_ID_MOCK,
tokenAddress: TOKEN_ADDRESS_MOCK,
chainId: CHAIN_ID_MOCK,
});
return updatePaymentTokenMock.mock.calls[0][1].updateTransactionData;
}

it('does not call setSelectedToken when only fiat amount changes', () => {
getTransactionMock.mockReturnValue(TRANSACTION_META_MOCK);
deriveFiatAssetForFiatPaymentMock.mockReturnValue(FIAT_ASSET_MOCK);

const updateTransactionData = getUpdateTransactionData();

updateTransactionData(TRANSACTION_ID_MOCK, (data) => {
data.fiatPayment = { amountFiat: '100' };
});

expect(setSelectedTokenMock).not.toHaveBeenCalled();
});

it('calls RampsController:setSelectedToken when payment method changes', () => {
getTransactionMock.mockReturnValue(TRANSACTION_META_MOCK);
deriveFiatAssetForFiatPaymentMock.mockReturnValue(FIAT_ASSET_MOCK);

const updateTransactionData = getUpdateTransactionData();

updateTransactionData(TRANSACTION_ID_MOCK, (data) => {
data.fiatPayment = { selectedPaymentMethodId: 'card-123' };
});

expect(setSelectedTokenMock).toHaveBeenCalledWith(CAIP_ASSET_ID_MOCK);
});

it('triggers quote update when fiat payment changes', () => {
getTransactionMock.mockReturnValue(TRANSACTION_META_MOCK);
deriveFiatAssetForFiatPaymentMock.mockReturnValue(FIAT_ASSET_MOCK);

const updateTransactionData = getUpdateTransactionData();

updateTransactionData(TRANSACTION_ID_MOCK, (data) => {
data.fiatPayment = { amountFiat: '100' };
});

expect(updateQuotesMock).toHaveBeenCalledTimes(1);
});

it('does not call setSelectedToken when transaction is not found', () => {
getTransactionMock.mockReturnValue(undefined);

const updateTransactionData = getUpdateTransactionData();

updateTransactionData(TRANSACTION_ID_MOCK, (data) => {
data.fiatPayment = { selectedPaymentMethodId: 'card-123' };
});

expect(setSelectedTokenMock).not.toHaveBeenCalled();
});

it('does not call setSelectedToken when fiat asset cannot be derived', () => {
getTransactionMock.mockReturnValue(TRANSACTION_META_MOCK);
deriveFiatAssetForFiatPaymentMock.mockReturnValue(undefined);

const updateTransactionData = getUpdateTransactionData();

updateTransactionData(TRANSACTION_ID_MOCK, (data) => {
data.fiatPayment = { selectedPaymentMethodId: 'card-123' };
});

expect(setSelectedTokenMock).not.toHaveBeenCalled();
});

it('does not call setSelectedToken when fiat payment does not change', () => {
getTransactionMock.mockReturnValue(TRANSACTION_META_MOCK);
deriveFiatAssetForFiatPaymentMock.mockReturnValue(FIAT_ASSET_MOCK);

const updateTransactionData = getUpdateTransactionData();

updateTransactionData(TRANSACTION_ID_MOCK, (data) => {
data.sourceAmounts = [
{ sourceAmountHuman: '1.23' } as TransactionPaySourceAmount,
];
});

expect(setSelectedTokenMock).not.toHaveBeenCalled();
});

it('does not throw when setSelectedToken throws', () => {
getTransactionMock.mockReturnValue(TRANSACTION_META_MOCK);
deriveFiatAssetForFiatPaymentMock.mockReturnValue(FIAT_ASSET_MOCK);
setSelectedTokenMock.mockImplementation(() => {
throw new Error('Tokens not loaded');
});

const updateTransactionData = getUpdateTransactionData();

expect(() => {
updateTransactionData(TRANSACTION_ID_MOCK, (data) => {
data.fiatPayment = { selectedPaymentMethodId: 'card-123' };
});
}).not.toThrow();
});
});
});
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ import {
TransactionPayStrategy,
} from './constants';
import { QuoteRefresher } from './helpers/QuoteRefresher';
import { deriveFiatAssetForFiatPayment } from './strategy/fiat/utils';
import type {
GetDelegationTransactionCallback,
TransactionConfigCallback,
Expand All @@ -25,7 +26,7 @@ import type {
import { getStrategyOrder } from './utils/feature-flags';
import { updateQuotes } from './utils/quotes';
import { updateSourceAmounts } from './utils/source-amounts';
import { pollTransactionChanges } from './utils/transaction';
import { getTransaction, pollTransactionChanges } from './utils/transaction';

const MESSENGER_EXPOSED_METHODS = [
'getDelegationTransaction',
Expand Down Expand Up @@ -207,6 +208,7 @@ export class TransactionPayController extends BaseController<
fn: (transactionData: Draft<TransactionData>) => void,
): void {
let shouldUpdateQuotes = false;
let shouldUpdateFiatToken = false;

this.update((state) => {
const { transactionData } = state;
Expand All @@ -215,6 +217,9 @@ export class TransactionPayController extends BaseController<
const originalTokens = current?.tokens;
const originalIsMaxAmount = current?.isMaxAmount;
const originalIsPostQuote = current?.isPostQuote;
const originalFiatPaymentAmount = current?.fiatPayment?.amountFiat;
const originalFiatPaymentMethodId =
current?.fiatPayment?.selectedPaymentMethodId;

if (!current) {
transactionData[transactionId] = {
Expand All @@ -236,6 +241,11 @@ export class TransactionPayController extends BaseController<
const isTokensUpdated = current.tokens !== originalTokens;
const isIsMaxUpdated = current.isMaxAmount !== originalIsMaxAmount;
const isPostQuoteUpdated = current.isPostQuote !== originalIsPostQuote;
const isFiatAmountUpdated =
current.fiatPayment?.amountFiat !== originalFiatPaymentAmount;
const isFiatPaymentMethodUpdated =
current.fiatPayment?.selectedPaymentMethodId !==
originalFiatPaymentMethodId;

if (
isPaymentTokenUpdated ||
Expand All @@ -247,8 +257,34 @@ export class TransactionPayController extends BaseController<

shouldUpdateQuotes = true;
}

if (isFiatAmountUpdated || isFiatPaymentMethodUpdated) {
shouldUpdateQuotes = true;
}

if (isFiatPaymentMethodUpdated) {
shouldUpdateFiatToken = true;
}
});

if (shouldUpdateFiatToken) {
const transaction = getTransaction(
transactionId,
this.messenger,
) as TransactionMeta;
const fiatAsset = deriveFiatAssetForFiatPayment(transaction);
if (fiatAsset) {
try {
this.messenger.call(
'RampsController:setSelectedToken',
fiatAsset.caipAssetId,
);
} catch {
// Intentionally no-op — tokens may not be loaded in RampsController yet.
}
}
}

if (shouldUpdateQuotes) {
updateQuotes({
getStrategies: this.#getStrategiesWithFallback.bind(this),
Expand Down
Loading
Loading