From 66b2f5a7cb067c138cefb9f730c3fbc033fc5427 Mon Sep 17 00:00:00 2001 From: jpuri Date: Wed, 29 Jul 2026 13:09:25 +0530 Subject: [PATCH 1/2] fix: correct Send to Perps Max amount when funded from Money account Deposits funded from the Money account (e.g. Send to Perps) were setting isMaxAmount=true on Max, causing TPC's calculatePostQuoteSourceAmounts to substitute the pay token's raw balance (bare mUSD on Monad) as the source amount. This collapsed Max to the un-vaulted mUSD portion instead of the full withdrawable balance (mUSD + vmUSD), so a $4.70 balance showed $0.62. Exclude the money-account payment override from shouldSetMax so these deposits keep isMaxAmount=false and route the typed amount through as token.amountRaw, matching money-account withdraw behavior. The withdrawableFiatRaw balance source is unchanged. --- .../useTransactionCustomAmount.test.ts | 28 +++++++++++++++++++ .../useTransactionCustomAmount.ts | 18 +++++++++--- 2 files changed, 42 insertions(+), 4 deletions(-) diff --git a/app/components/Views/confirmations/hooks/transactions/useTransactionCustomAmount.test.ts b/app/components/Views/confirmations/hooks/transactions/useTransactionCustomAmount.test.ts index 9d91fb890e7e..bf5b89814dd9 100644 --- a/app/components/Views/confirmations/hooks/transactions/useTransactionCustomAmount.test.ts +++ b/app/components/Views/confirmations/hooks/transactions/useTransactionCustomAmount.test.ts @@ -1434,6 +1434,34 @@ describe('useTransactionCustomAmount', () => { expect(result.current.amountFiat).toBe('750.50'); }); + it('does not set isMaxAmount for Max when payment override is MoneyAccount (e.g. Send to Perps)', async () => { + // Setting isMaxAmount=true would make TPC substitute the pay token's raw + // balance (bare mUSD on Monad) for the source amount, collapsing Max to + // the un-vaulted mUSD portion instead of the full withdrawable balance. + useMoneyAccountBalanceMock.mockReturnValue({ + withdrawableFiatRaw: '4.70', + } as ReturnType); + + const { result } = runHook({ + transactionMeta: { + type: TransactionType.perpsDeposit, + id: transactionIdMock, + chainId: '0x1' as Hex, + txParams: { from: '0xabc' }, + } as unknown as Partial, + stateOverrides: moneyAccountStateOverrides, + }); + + await act(async () => { + result.current.updatePendingAmountPercentage(100); + }); + + // Full withdrawable balance is shown, not the reduced bare-mUSD figure. + expect(result.current.amountFiat).toBe('4.70'); + // isMaxAmount must never be set to true for the money account override. + expect(setTransactionConfigMock).not.toHaveBeenCalled(); + }); + it('returns 0 when payment override is MoneyAccount but withdrawableFiatRaw is undefined', async () => { useMoneyAccountBalanceMock.mockReturnValue({ withdrawableFiatRaw: undefined, diff --git a/app/components/Views/confirmations/hooks/transactions/useTransactionCustomAmount.ts b/app/components/Views/confirmations/hooks/transactions/useTransactionCustomAmount.ts index 161244c57337..7a0ad50b85a8 100644 --- a/app/components/Views/confirmations/hooks/transactions/useTransactionCustomAmount.ts +++ b/app/components/Views/confirmations/hooks/transactions/useTransactionCustomAmount.ts @@ -100,6 +100,11 @@ export function useTransactionCustomAmount({ const isMoneyAccountWithdraw = hasTransactionType(transactionMeta, [ TransactionType.moneyAccountWithdraw, ]); + const paymentOverride = useSelector((state: RootState) => + selectPaymentOverrideByTransactionId(state, transactionId), + ); + const isMoneyPaymentOverride = + paymentOverride === PaymentOverride.MoneyAccount; const tokenAddress = getTokenAddress(transactionMeta); const payTokenFiatRate = useTokenFiatRate(tokenAddress, chainId, currency); const musdFiatRate = @@ -292,17 +297,21 @@ export function useTransactionCustomAmount({ }, }); - // Do NOT set isMaxAmount=true for perps or money-account withdraw. TPC's + // Do NOT set isMaxAmount=true for perps or money-account flows. TPC's // calculatePostQuoteSourceAmounts substitutes `token.balanceRaw` when // isMaxAmount is true: wrong for HyperLiquid (wallet USDC vs typed HL // balance) and wrong for money account (on-chain mUSD only vs mUSD + - // vmUSD fiat total). Keeping isMaxAmount false routes the typed - // amount through as token.amountRaw. + // vmUSD fiat total). This includes deposits funded from the money + // account (e.g. Send to Perps): the pay token is bare mUSD on Monad, so + // substituting its raw balance collapses Max to the un-vaulted mUSD + // portion instead of the full withdrawable (mUSD + vmUSD). Keeping + // isMaxAmount false routes the typed amount through as token.amountRaw. const shouldSetMax = percentage === 100 && !isPerpsWithdraw && !isMoneyAccountWithdraw && - !isMoneyAccountDeposit; + !isMoneyAccountDeposit && + !isMoneyPaymentOverride; if (shouldSetMax) { setIsMax(true); @@ -338,6 +347,7 @@ export function useTransactionCustomAmount({ isPerpsWithdraw, isMoneyAccountWithdraw, isMoneyAccountDeposit, + isMoneyPaymentOverride, payToken?.balanceRaw, payToken?.decimals, setIsMax, From 8edf8865c5604d1e39c8a14d65dd1f4bfed33050 Mon Sep 17 00:00:00 2001 From: jpuri Date: Thu, 30 Jul 2026 16:58:01 +0530 Subject: [PATCH 2/2] fix: enable Max isMaxAmount for Money Account Send to Perps With MetaMask/core#9707, TPC uses the typed required amount for Money Account max instead of bare on-chain mUSD. Re-enable isMaxAmount on Max for deposits funded from the money account so quotes use EXACT_INPUT and fees are taken from the full withdrawable balance. Co-authored-by: Cursor --- .../useTransactionCustomAmount.test.ts | 15 ++++++---- .../useTransactionCustomAmount.ts | 28 ++++++++----------- 2 files changed, 20 insertions(+), 23 deletions(-) diff --git a/app/components/Views/confirmations/hooks/transactions/useTransactionCustomAmount.test.ts b/app/components/Views/confirmations/hooks/transactions/useTransactionCustomAmount.test.ts index bf5b89814dd9..268d9fa528c8 100644 --- a/app/components/Views/confirmations/hooks/transactions/useTransactionCustomAmount.test.ts +++ b/app/components/Views/confirmations/hooks/transactions/useTransactionCustomAmount.test.ts @@ -1434,10 +1434,10 @@ describe('useTransactionCustomAmount', () => { expect(result.current.amountFiat).toBe('750.50'); }); - it('does not set isMaxAmount for Max when payment override is MoneyAccount (e.g. Send to Perps)', async () => { - // Setting isMaxAmount=true would make TPC substitute the pay token's raw - // balance (bare mUSD on Monad) for the source amount, collapsing Max to - // the un-vaulted mUSD portion instead of the full withdrawable balance. + it('sets isMaxAmount for Max when payment override is MoneyAccount (e.g. Send to Perps)', async () => { + // With TPC MoneyAccount max support (MetaMask/core#9707), isMaxAmount=true + // uses the typed required amount (full withdrawable) rather than the pay + // token's bare on-chain mUSD balance, enabling EXACT_INPUT Max quotes. useMoneyAccountBalanceMock.mockReturnValue({ withdrawableFiatRaw: '4.70', } as ReturnType); @@ -1458,8 +1458,11 @@ describe('useTransactionCustomAmount', () => { // Full withdrawable balance is shown, not the reduced bare-mUSD figure. expect(result.current.amountFiat).toBe('4.70'); - // isMaxAmount must never be set to true for the money account override. - expect(setTransactionConfigMock).not.toHaveBeenCalled(); + expect(setTransactionConfigMock).toHaveBeenCalledTimes(1); + + const config = { isMaxAmount: false }; + setTransactionConfigMock.mock.calls[0][1](config); + expect(config.isMaxAmount).toBe(true); }); it('returns 0 when payment override is MoneyAccount but withdrawableFiatRaw is undefined', async () => { diff --git a/app/components/Views/confirmations/hooks/transactions/useTransactionCustomAmount.ts b/app/components/Views/confirmations/hooks/transactions/useTransactionCustomAmount.ts index d20e98d36891..02b84836eee3 100644 --- a/app/components/Views/confirmations/hooks/transactions/useTransactionCustomAmount.ts +++ b/app/components/Views/confirmations/hooks/transactions/useTransactionCustomAmount.ts @@ -98,11 +98,6 @@ export function useTransactionCustomAmount({ const isMoneyAccountWithdraw = hasTransactionType(transactionMeta, [ TransactionType.moneyAccountWithdraw, ]); - const paymentOverride = useSelector((state: RootState) => - selectPaymentOverrideByTransactionId(state, transactionId), - ); - const isMoneyPaymentOverride = - paymentOverride === PaymentOverride.MoneyAccount; const tokenAddress = getTokenAddress(transactionMeta); const payTokenFiatRate = useTokenFiatRate(tokenAddress, chainId, currency); const musdFiatRate = @@ -297,21 +292,21 @@ export function useTransactionCustomAmount({ }, }); - // Do NOT set isMaxAmount=true for perps or money-account flows. TPC's - // calculatePostQuoteSourceAmounts substitutes `token.balanceRaw` when - // isMaxAmount is true: wrong for HyperLiquid (wallet USDC vs typed HL - // balance) and wrong for money account (on-chain mUSD only vs mUSD + - // vmUSD fiat total). This includes deposits funded from the money - // account (e.g. Send to Perps): the pay token is bare mUSD on Monad, so - // substituting its raw balance collapses Max to the un-vaulted mUSD - // portion instead of the full withdrawable (mUSD + vmUSD). Keeping - // isMaxAmount false routes the typed amount through as token.amountRaw. + // Do NOT set isMaxAmount=true for perps/money-account withdraw or money + // account deposit flows. For those, TPC would substitute an on-chain + // balanceRaw that does not match the typed balance (HyperLiquid wallet + // USDC vs HL balance; money-account on-chain mUSD vs mUSD + vmUSD). + // + // Deposits funded from the money account (e.g. Send to Perps) DO set + // isMaxAmount=true so quotes use EXACT_INPUT. That is safe once TPC uses + // the typed required amount for MoneyAccount max instead of the pay + // token's bare mUSD balance (MetaMask/core#9707). Mobile still types the + // full withdrawableFiatRaw amount into the required token. const shouldSetMax = percentage === 100 && !isPerpsWithdraw && !isMoneyAccountWithdraw && - !isMoneyAccountDeposit && - !isMoneyPaymentOverride; + !isMoneyAccountDeposit; if (shouldSetMax) { setIsMax(true); @@ -348,7 +343,6 @@ export function useTransactionCustomAmount({ isPerpsWithdraw, isMoneyAccountWithdraw, isMoneyAccountDeposit, - isMoneyPaymentOverride, payToken?.balanceRaw, payToken?.decimals, setIsMax,