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

### Fixed

- Use the typed required amount for Money Account `isMaxAmount` source calculations instead of the pay token's on-chain balance, so Max deposits funded from the money account (e.g. Send to Perps) keep the full withdrawable total (mUSD + vmUSD) and can use `EXACT_INPUT` ([#9707](https://github.com/MetaMask/core/pull/9707))
- Fix Relay quote validation ([#9723](https://github.com/MetaMask/core/pull/9723))
- Keep the quote when validation fails with reason `insufficient-source-balance`, while still surfacing `quoteError`; all other validation-failure reasons continue to remove the quote.
- Exclude a zero `gas` value from the simulated transaction.
Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,10 @@
import { TransactionType } from '@metamask/transaction-controller';

import { ARBITRUM_USDC_ADDRESS, CHAIN_ID_ARBITRUM } from '../constants.js';
import {
ARBITRUM_USDC_ADDRESS,
CHAIN_ID_ARBITRUM,
PaymentOverride,
} from '../constants.js';
import { TransactionPayStrategy } from '../index.js';
import type { TransactionPaymentToken } from '../index.js';
import { getMessengerMock } from '../tests/messenger-mock.js';
Expand Down Expand Up @@ -264,6 +268,42 @@ describe('Source Amounts Utils', () => {
]);
});

it('uses fiat-derived source amount for MoneyAccount max instead of payment token balance', () => {
// Money account withdrawable (mUSD + vmUSD) is reflected in the typed
// required token amount. The pay token's on-chain balance is only the
// un-vaulted mUSD portion and must not collapse Max.
const transactionData: TransactionData = {
isLoading: false,
isMaxAmount: true,
paymentOverride: PaymentOverride.MoneyAccount,
paymentToken: {
...PAYMENT_TOKEN_MOCK,
// Bare on-chain mUSD — much smaller than the typed max.
balanceHuman: '0.62',
balanceRaw: '620000',
balanceUsd: '0.62',
},
tokens: [
{
...TRANSACTION_TOKEN_MOCK,
// Full withdrawable max typed by the client ($6.00 USD).
amountUsd: '6.0',
},
],
};

updateSourceAmounts(TRANSACTION_ID_MOCK, transactionData, messenger);

// usdRate mock is 3.0 → source human = 6 / 3 = 2, raw = 2 * 10^6.
expect(transactionData.sourceAmounts).toStrictEqual([
{
sourceAmountHuman: '2',
sourceAmountRaw: '2000000',
targetTokenAddress: TRANSACTION_TOKEN_MOCK.address,
},
]);
});

it('does nothing if no payment token', () => {
const transactionData: TransactionData = {
isLoading: false,
Expand Down
14 changes: 12 additions & 2 deletions packages/transaction-pay-controller/src/utils/source-amounts.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ import { BigNumber } from 'bignumber.js';
import {
ARBITRUM_USDC_ADDRESS,
CHAIN_ID_ARBITRUM,
PaymentOverride,
PERPS_DEPOSIT_TYPES,
} from '../constants.js';
import type {
Expand Down Expand Up @@ -42,7 +43,8 @@ export function updateSourceAmounts(
return;
}

const { isMaxAmount, isPostQuote, paymentToken, tokens } = transactionData;
const { isMaxAmount, isPostQuote, paymentOverride, paymentToken, tokens } =
transactionData;

if (!tokens.length || !paymentToken) {
return;
Expand Down Expand Up @@ -75,6 +77,7 @@ export function updateSourceAmounts(
transactionId,
isMaxAmount ?? false,
isQuoteRequired,
paymentOverride,
),
)
.filter(Boolean) as TransactionPaySourceAmount[];
Expand Down Expand Up @@ -148,6 +151,7 @@ function calculatePostQuoteSourceAmounts(
* @param transactionId - ID of the transaction.
* @param isMaxAmount - Whether the transaction is a maximum amount transaction.
* @param isQuoteRequired - When true, a quote is always fetched even when source and target tokens are identical.
* @param paymentOverride - Optional payment source override for the transaction.
* @returns The source amount or undefined if calculation failed.
*/
function calculateSourceAmount(
Expand All @@ -157,6 +161,7 @@ function calculateSourceAmount(
transactionId: string,
isMaxAmount: boolean,
isQuoteRequired?: boolean,
paymentOverride?: PaymentOverride,
): TransactionPaySourceAmount | undefined {
const paymentTokenFiatRate = getTokenFiatRate(
messenger,
Expand Down Expand Up @@ -208,7 +213,12 @@ function calculateSourceAmount(
return undefined;
}

if (isMaxAmount) {
// Money account Max must not use the pay token's on-chain balance. That
// balance is only un-vaulted mUSD, while the typed required amount already
// reflects the full withdrawable total (mUSD + vmUSD). Using the typed
// fiat-derived source keeps isMaxAmount=true (EXACT_INPUT) correct for
// deposits funded from the money account (e.g. Send to Perps).
if (isMaxAmount && paymentOverride !== PaymentOverride.MoneyAccount) {
Comment thread
jpuri marked this conversation as resolved.
return {
sourceAmountHuman: paymentToken.balanceHuman,
sourceAmountRaw: paymentToken.balanceRaw,
Expand Down