Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
28 commits
Select commit Hold shift + click to select a range
0355e59
Update swap transaction depending on user selection
jpuri Nov 5, 2025
89d8619
Adding unit test coverage
jpuri Nov 5, 2025
1a991a3
Merge branch 'main' into trade_swap
jpuri Nov 5, 2025
9528a13
Adding unit test coverage
jpuri Nov 5, 2025
75538f8
Merge branch 'trade_swap' of https://github.com/MetaMask/metamask-ext…
jpuri Nov 5, 2025
3bb477c
update
jpuri Nov 5, 2025
cc37af1
Merge branch 'main' into trade_swap
jpuri Nov 5, 2025
a338213
update
jpuri Nov 5, 2025
033a8d0
Merge branch 'trade_swap' of https://github.com/MetaMask/metamask-ext…
jpuri Nov 5, 2025
c8fedc9
update
jpuri Nov 5, 2025
b7ae186
update
jpuri Nov 6, 2025
933ebfb
update
jpuri Nov 6, 2025
ec93043
For quoted swap use data from quote to populate estimated changes
jpuri Nov 6, 2025
dddc1c5
adding test coverage
jpuri Nov 6, 2025
ac73f16
update
jpuri Nov 6, 2025
056ea61
update
jpuri Nov 6, 2025
cacbbec
update
jpuri Nov 6, 2025
a049e99
merge
jpuri Nov 7, 2025
6a06b53
update
jpuri Nov 7, 2025
12c0d94
Merge branch 'main' into simulation_fix
jpuri Nov 7, 2025
f50ae83
update
jpuri Nov 7, 2025
25b8695
Merge branch 'simulation_fix' of https://github.com/MetaMask/metamask…
jpuri Nov 7, 2025
aded29b
Merge branch 'main' into simulation_fix
jpuri Nov 7, 2025
5ef409a
Merge branch 'main' into simulation_fix
jpuri Nov 7, 2025
a4ae3c5
update
jpuri Nov 7, 2025
548d49a
Merge branch 'simulation_fix' of https://github.com/MetaMask/metamask…
jpuri Nov 7, 2025
d147390
update
jpuri Nov 7, 2025
eaae3e6
update
jpuri Nov 7, 2025
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
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@ import {
BatchTransaction,
TransactionMeta,
} from '@metamask/transaction-controller';
import { TxData } from '@metamask/bridge-controller';
import { QuoteResponse, TxData } from '@metamask/bridge-controller';
import { toHex } from '@metamask/controller-utils';
import { useDispatch, useSelector } from 'react-redux';

Expand All @@ -28,6 +28,7 @@ import { updateTransaction } from '../../../../../store/actions';
import { useConfirmContext } from '../../../context/confirm';
import { useDappSwapComparisonInfo } from '../../../hooks/transactions/dapp-swap-comparison/useDappSwapComparisonInfo';
import { useSwapCheck } from '../../../hooks/transactions/dapp-swap-comparison/useSwapCheck';
import { QuoteSwapSimulationDetails } from '../quote-swap-simulation-details/quote-swap-simulation-details';

const DAPP_SWAP_COMPARISON_ORIGIN = 'https://app.uniswap.org';
const TEST_DAPP_ORIGIN = 'https://metamask.github.io';
Expand Down Expand Up @@ -79,12 +80,16 @@ const SwapButton = ({
const DappSwapComparisonInner = () => {
const t = useI18nContext();
const {
fiatRates,
destinationTokenSymbol,
gasDifference,
selectedQuote,
selectedQuoteValueDifference,
gasDifference,
sourceTokenAmount,
tokenAmountDifference,
destinationTokenSymbol,
tokenDetails,
} = useDappSwapComparisonInfo();

const { isQuotedSwap } = useSwapCheck();
const dispatch = useDispatch();
const { currentConfirmation } = useConfirmContext<TransactionMeta>();
Expand Down Expand Up @@ -246,6 +251,14 @@ const DappSwapComparisonInner = () => {
</Text>
</Box>
)}
{selectedSwapType === SwapType.Metamask && (
<QuoteSwapSimulationDetails
fiatRates={fiatRates}
quote={selectedQuote as QuoteResponse}
tokenDetails={tokenDetails}
sourceTokenAmount={sourceTokenAmount}
/>
)}
</Box>
);
};
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@ import {
import { updateAtomicBatchData } from '../../../../../../../store/controller-actions/transaction-controller';
import { Confirmation } from '../../../../../types/confirm';
import { updateApprovalAmount } from '../../../../../../../../shared/lib/transactions/approvals';
import * as SwapCheckHook from '../../../../../hooks/transactions/dapp-swap-comparison/useSwapCheck';
import { BatchSimulationDetails } from './batch-simulation-details';

jest.mock('../../../../../../../../shared/lib/transactions/approvals');
Expand Down Expand Up @@ -291,6 +292,14 @@ describe('BatchSimulationDetails', () => {
expect(container.firstChild).toBeNull();
});

it('return null for MetaMask Swap transaction', () => {
jest.spyOn(SwapCheckHook, 'useSwapCheck').mockReturnValue({
isQuotedSwap: true,
});
const { container } = render();
expect(container.firstChild).toBeNull();
});

it('return null for upgrade transaction if there are no nested transactions', () => {
const { container } = render(upgradeAccountConfirmationOnly);
expect(container.firstChild).toBeNull();
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -17,13 +17,15 @@ import { EditSpendingCapModal } from '../../approve/edit-spending-cap-modal/edit
import { TokenStandard } from '../../../../../../../../shared/constants/transaction';
import { useI18nContext } from '../../../../../../../hooks/useI18nContext';
import { updateAtomicBatchData } from '../../../../../../../store/controller-actions/transaction-controller';
import { useSwapCheck } from '../../../../../hooks/transactions/dapp-swap-comparison/useSwapCheck';
import { useIsUpgradeTransaction } from '../../hooks/useIsUpgradeTransaction';

// TODO: Fix in https://github.com/MetaMask/metamask-extension/issues/31860
// eslint-disable-next-line @typescript-eslint/naming-convention
export function BatchSimulationDetails() {
const t = useI18nContext();
const { isUpgradeOnly } = useIsUpgradeTransaction();
const { isQuotedSwap } = useSwapCheck();

const { currentConfirmation: transactionMeta } =
useConfirmContext<TransactionMeta>();
Expand Down Expand Up @@ -58,13 +60,6 @@ export function BatchSimulationDetails() {
[id, nestedTransactionIndexToEdit],
);

if (
transactionMeta?.type === TransactionType.revokeDelegation ||
isUpgradeOnly
) {
return null;
}

const approveRows: StaticRow[] = useMemo(() => {
const finalBalanceChanges = approveBalanceChanges?.map((change) => ({
...change,
Expand All @@ -82,6 +77,14 @@ export function BatchSimulationDetails() {
];
}, [approveBalanceChanges, handleEdit]);

if (
transactionMeta?.type === TransactionType.revokeDelegation ||
isUpgradeOnly ||
isQuotedSwap
) {
return null;
}

const nestedTransactionToEdit =
nestedTransactionIndexToEdit === undefined
? undefined
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,93 @@
import React from 'react';
import configureMockStore from 'redux-mock-store';
import { QuoteResponse } from '@metamask/bridge-controller';

import { getMockConfirmStateForTransaction } from '../../../../../../test/data/confirmations/helper';
import { mockSwapConfirmation } from '../../../../../../test/data/confirmations/contract-interaction';
import { renderWithConfirmContextProvider } from '../../../../../../test/lib/confirmations/render-helpers';
import { Confirmation } from '../../../types/confirm';
import { QuoteSwapSimulationDetails } from './quote-swap-simulation-details';

jest.mock(
'../../../../../components/app/alert-system/contexts/alertMetricsContext',
() => ({
useAlertMetrics: jest.fn(() => ({
trackAlertActionClicked: jest.fn(),
trackInlineAlertClicked: jest.fn(),
trackAlertRender: jest.fn(),
})),
}),
);

const quote = {
quote: {
srcAsset: {
address: '0x833589fCD6eDb6E08f4c7C32D4f71b54bdA02913',
chainId: 8453,
assetId: 'eip155:8453/erc20:0x833589fcd6edb6e08f4c7c32d4f71b54bda02913',
symbol: 'USDC',
decimals: 6,
iconUrl:
'https://static.cx.metamask.io/api/v2/tokenIcons/assets/eip155/8453/erc20/0x833589fcd6edb6e08f4c7c32d4f71b54bda02913.png',
metadata: { storage: { balance: 9, approval: 10 } },
},
srcTokenAmount: '99125',
destAsset: {
address: '0xfde4C96c8593536E31F229EA8f37b2ADa2699bb2',
chainId: 8453,
assetId: 'eip155:8453/erc20:0xfde4c96c8593536e31f229ea8f37b2ada2699bb2',
symbol: 'USDT',
decimals: 6,
iconUrl:
'https://static.cx.metamask.io/api/v2/tokenIcons/assets/eip155/8453/erc20/0xfde4c96c8593536e31f229ea8f37b2ada2699bb2.png',
metadata: { storage: { balance: 0, approval: 1 } },
},
destTokenAmount: '99132',
minDestTokenAmount: '97149',
},
};

function render(args: Record<string, string> = {}) {
const state = getMockConfirmStateForTransaction({
...mockSwapConfirmation,
...args,
} as Confirmation);

const mockStore = configureMockStore()(state);

return renderWithConfirmContextProvider(
<QuoteSwapSimulationDetails
fiatRates={{
'0x0000000000000000000000000000000000000000': 3377.19,
'0x833589fCD6eDb6E08f4c7C32D4f71b54bdA02913': 0.999877,
'0xfde4C96c8593536E31F229EA8f37b2ADa2699bb2': 0.999578,
}}
quote={quote as unknown as QuoteResponse}
sourceTokenAmount={'0x186a0'}
tokenDetails={{
'0xfde4c96c8593536e31f229ea8f37b2ada2699bb2': {
decimals: '6',
symbol: 'USDT',
standard: 'ERC20',
},
'0x833589fcd6edb6e08f4c7c32d4f71b54bda02913': {
decimals: '6',
symbol: 'USDC',
standard: 'ERC20',
},
}}
/>,
mockStore,
);
}

describe('<QuoteSwapSimulationDetails />', () => {
it('renders component without errors', () => {
const { getByText } = render();
expect(getByText('Estimated changes')).toBeInTheDocument();
expect(getByText('You send')).toBeInTheDocument();
expect(getByText('You receive')).toBeInTheDocument();
expect(getByText('- 0.1')).toBeInTheDocument();
expect(getByText('+ 0.0991')).toBeInTheDocument();
});
});
Original file line number Diff line number Diff line change
@@ -0,0 +1,108 @@
import React, { useMemo } from 'react';
import { BigNumber } from 'bignumber.js';
import { Box, BoxFlexDirection } from '@metamask/design-system-react';
import { Hex } from '@metamask/utils';
import { QuoteResponse } from '@metamask/bridge-controller';
import { TransactionMeta } from '@metamask/transaction-controller';
import { toHex } from '@metamask/controller-utils';

import { TokenStandAndDetails } from '../../../../../store/actions';
import { useI18nContext } from '../../../../../hooks/useI18nContext';
import { useConfirmContext } from '../../../context/confirm';
import { SimulationDetailsLayout } from '../../simulation-details/simulation-details';
import { BalanceChangeRow } from '../../simulation-details/balance-change-row';
import { TokenAssetIdentifier } from '../../simulation-details/types';

export const QuoteSwapSimulationDetails = ({
fiatRates,
quote,
sourceTokenAmount,
tokenDetails,
}: {
fiatRates?: Record<Hex, number | undefined>;
quote?: QuoteResponse;
sourceTokenAmount?: string;
tokenDetails?: Record<Hex, TokenStandAndDetails>;
}) => {
const t = useI18nContext();
const { currentConfirmation } = useConfirmContext<TransactionMeta>();
const { id: transactionId } = currentConfirmation;

const { srcAssetBalanceChange, destAssetBalanceChange } = useMemo(() => {
if (!quote || !tokenDetails || !fiatRates) {
return {};
}
const { srcAsset, destAsset, destTokenAmount } = quote.quote;
return {
srcAssetBalanceChange: {
asset: {
...tokenDetails[srcAsset.address as Hex],
chainId: toHex(srcAsset.chainId),
address: srcAsset.address as Hex,
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Bug: Case-Sensitive Token Detail Lookup Mismatch Failure

The tokenDetails lookup uses srcAsset.address directly without lowercasing, but fetchAllTokenDetails returns keys normalized to lowercase. This causes the spread operation to fail silently when the address has mixed case, resulting in missing token details. The same code correctly lowercases addresses when accessing fiatRates on line 49.

Fix in Cursor Fix in Web

} as unknown as TokenAssetIdentifier,
amount: new BigNumber(sourceTokenAmount ?? '0x0', 16)
.negated()
.dividedBy(new BigNumber(10).pow(srcAsset.decimals)),
fiatAmount: new BigNumber(sourceTokenAmount ?? '0x0', 16)
.negated()
.dividedBy(new BigNumber(10).pow(srcAsset.decimals))
.times(fiatRates[srcAsset.address.toLowerCase() as Hex] ?? 0)
.toNumber(),
usdAmount: 0,
},
destAssetBalanceChange: {
asset: {
...tokenDetails[destAsset.address as Hex],
chainId: toHex(destAsset.chainId),
address: destAsset.address as Hex,
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Bug: Case-Sensitive Token Detail Lookup Mismatch

The tokenDetails lookup uses destAsset.address directly without lowercasing, but fetchAllTokenDetails returns keys normalized to lowercase. This causes the spread operation to fail silently when the address has mixed case, resulting in missing token details. The same code correctly lowercases addresses when accessing fiatRates on line 64.

Fix in Cursor Fix in Web

} as unknown as TokenAssetIdentifier,
amount: new BigNumber(destTokenAmount).dividedBy(
new BigNumber(10).pow(destAsset.decimals),
),
fiatAmount: new BigNumber(destTokenAmount)
.dividedBy(new BigNumber(10).pow(destAsset.decimals))
.times(fiatRates[destAsset.address.toLowerCase() as Hex] ?? 0)
.toNumber(),
usdAmount: 0,
},
};
}, [fiatRates, quote, sourceTokenAmount, tokenDetails]);

if (
!quote ||
!tokenDetails ||
!fiatRates ||
!srcAssetBalanceChange ||
!destAssetBalanceChange
) {
return null;
}

return (
<SimulationDetailsLayout
isTransactionsRedesign
transactionId={transactionId}
>
<Box
flexDirection={BoxFlexDirection.Column}
gap={3}
data-testid="quote-swap-simulation-details"
>
<BalanceChangeRow
balanceChange={srcAssetBalanceChange}
confirmationId={currentConfirmation?.id}
isFirstRow
label={t('simulationDetailsOutgoingHeading')}
showFiat
/>
<BalanceChangeRow
balanceChange={destAssetBalanceChange}
hasIncomingTokens
confirmationId={currentConfirmation?.id}
label={t('simulationDetailsIncomingHeading')}
showFiat
/>
</Box>
</SimulationDetailsLayout>
);
};
Original file line number Diff line number Diff line change
Expand Up @@ -266,7 +266,7 @@ const HeaderLayout: React.FC<{
* @param props.children
* @param props.transactionId
*/
const SimulationDetailsLayout: React.FC<{
export const SimulationDetailsLayout: React.FC<{
inHeader?: React.ReactNode;
isTransactionsRedesign: boolean;
transactionId: string;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -316,16 +316,35 @@ describe('useDappSwapComparisonInfo', () => {
});

const {
fiatRates,
destinationTokenSymbol,
gasDifference,
selectedQuote,
selectedQuoteValueDifference,
gasDifference,
sourceTokenAmount,
tokenAmountDifference,
destinationTokenSymbol,
tokenDetails,
} = await runHook();
expect(selectedQuote).toEqual(quotes[3]);
expect(selectedQuoteValueDifference).toBe(0.012494042894187605);
expect(gasDifference).toBe(0.005686377458187605);
expect(tokenAmountDifference).toBe(0.006807665436);
expect(destinationTokenSymbol).toBe('USDC');
expect(sourceTokenAmount).toBe('0xde0b6b3a7640000');
expect(tokenDetails).toEqual({
'0x833589fcd6edb6e08f4c7c32d4f71b54bda02913': {
symbol: 'USDC',
decimals: '6',
},
'0xfd086bc7cd5c481dcc9c85ebe478a1c0b69fcbb9': {
decimals: '6',
symbol: 'USDT',
},
});
expect(fiatRates).toEqual({
'0x0000000000000000000000000000000000000000': 4052.27,
'0x833589fcd6edb6e08f4c7c32d4f71b54bda02913': 0.999804,
'0xfdcc3dd6671eab0709a4c0f3f53de9a333d80798': 1,
});
});
});
Original file line number Diff line number Diff line change
Expand Up @@ -86,6 +86,7 @@ export function useDappSwapComparisonInfo() {
}, [chainId, data, nestedTransactions, updateRequestDetectionLatency]);

const {
fiatRates,
getGasUSDValue,
getTokenUSDValue,
getDestinationTokenUSDValue,
Expand Down Expand Up @@ -369,10 +370,13 @@ export function useDappSwapComparisonInfo() {
]);

return {
fiatRates,
destinationTokenSymbol,
gasDifference,
selectedQuote,
selectedQuoteValueDifference,
gasDifference,
sourceTokenAmount: quotesInput?.srcTokenAmount,
tokenAmountDifference,
destinationTokenSymbol,
tokenDetails,
};
}
Loading
Loading