Skip to content

Commit e1acbc5

Browse files
fix: max predict withdraw (#22350)
## **Description** Ensure the Predict withdraw confirmation uses the entire balance if the `Max` button is used. ## **Changelog** CHANGELOG entry: null ## **Related issues** Fixes: [#6156](MetaMask/MetaMask-planning#6156) #22190 ## **Manual testing steps** ## **Screenshots/Recordings** ### **Before** ### **After** ## **Pre-merge author checklist** - [x] I’ve followed [MetaMask Contributor Docs](https://github.com/MetaMask/contributor-docs) and [MetaMask Mobile Coding Standards](https://github.com/MetaMask/metamask-mobile/blob/main/.github/guidelines/CODING_GUIDELINES.md). - [x] I've completed the PR template to the best of my ability - [x] I’ve included tests if applicable - [x] I’ve documented my code using [JSDoc](https://jsdoc.app/) format if applicable - [x] I’ve applied the right labels on the PR (see [labeling guidelines](https://github.com/MetaMask/metamask-mobile/blob/main/.github/guidelines/LABELING_GUIDELINES.md)). Not required for external contributors. ## **Pre-merge reviewer checklist** - [ ] I've manually tested the PR (e.g. pull and build branch, run the app, test code being changed). - [ ] I confirm that this PR addresses all acceptance criteria described in the ticket it closes and includes the necessary testing evidence such as recordings and or screenshots. <!-- CURSOR_SUMMARY --> --- > [!NOTE] > Ensure Predict withdraw Max uses the full balance converted to USD and add a fixed 1:1 USD rate for Polygon USDCE. > > - **Confirmations / Amounts**: > - Convert Predict balance to USD via `tokenFiatRate` for withdraw calculations and `Max` behavior; default rate falls back to `1`. > - Refine max-percentage logic when pay token is missing or matches the first required token. > - **Token Fiat Rates**: > - Return fixed `1` rate for `POLYGON_USDCE` on `POLYGON` when currency is USD (alongside `ARBITRUM_USDC`). > - Minor refactor for USD checks and token matches. > - **Alerts**: > - Use human Predict balance in `useInsufficientPredictBalanceAlert` comparison. > - **Tests**: > - Update and add tests for USDCE fixed rate and Predict withdraw USD conversion/max behavior. > > <sup>Written by [Cursor Bugbot](https://cursor.com/dashboard?tab=bugbot) for commit f179f68. This will update automatically on new commits. Configure [here](https://cursor.com/dashboard?tab=bugbot).</sup> <!-- /CURSOR_SUMMARY -->
1 parent 88a6815 commit e1acbc5

File tree

5 files changed

+63
-22
lines changed

5 files changed

+63
-22
lines changed

app/components/Views/confirmations/hooks/alerts/useInsufficientPredictBalanceAlert.ts

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -22,7 +22,7 @@ export function useInsufficientPredictBalanceAlert({
2222
const { amountPrecise } = useTokenAmount();
2323
const amountHuman = pendingAmount ?? amountPrecise ?? '0';
2424

25-
const { balance: predictBalanceUsd } = usePredictBalance({
25+
const { balance: predictBalanceHuman } = usePredictBalance({
2626
loadOnMount: true,
2727
});
2828

@@ -33,8 +33,8 @@ export function useInsufficientPredictBalanceAlert({
3333
const isInsufficient = useMemo(
3434
() =>
3535
isPredictWithdraw &&
36-
new BigNumber(predictBalanceUsd ?? '0').isLessThan(amountHuman),
37-
[amountHuman, isPredictWithdraw, predictBalanceUsd],
36+
new BigNumber(predictBalanceHuman ?? '0').isLessThan(amountHuman),
37+
[amountHuman, isPredictWithdraw, predictBalanceHuman],
3838
);
3939

4040
return useMemo(() => {

app/components/Views/confirmations/hooks/tokens/useTokenFiatRates.test.ts

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ import { backgroundState } from '../../../../../util/test/initial-root-state';
33
import { renderHookWithProvider } from '../../../../../util/test/renderWithProvider';
44
import { TokenFiatRateRequest, useTokenFiatRates } from './useTokenFiatRates';
55
import { ARBITRUM_USDC } from '../../constants/perps';
6+
import { POLYGON_USDCE } from '../../constants/predict';
67

78
jest.mock('../../../../../util/address', () => ({
89
toChecksumAddress: jest.fn((address) => address),
@@ -120,6 +121,20 @@ describe('useTokenFiatRates', () => {
120121
expect(result).toEqual([1]);
121122
});
122123

124+
it('returns fixed exchange rate if Polygon USDCE and selected currency is USD', () => {
125+
const result = runHook({
126+
requests: [
127+
{
128+
address: POLYGON_USDCE.address,
129+
chainId: CHAIN_IDS.POLYGON,
130+
currency: 'usd',
131+
},
132+
],
133+
});
134+
135+
expect(result).toEqual([1]);
136+
});
137+
123138
it('returns USD conversion rates if currency is USD', () => {
124139
const result = runHook({
125140
requests: [

app/components/Views/confirmations/hooks/tokens/useTokenFiatRates.ts

Lines changed: 10 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@ import { useDeepMemo } from '../useDeepMemo';
1111
import { toChecksumAddress } from '../../../../../util/address';
1212
import { CHAIN_IDS } from '@metamask/transaction-controller';
1313
import { ARBITRUM_USDC } from '../../constants/perps';
14+
import { POLYGON_USDCE } from '../../constants/predict';
1415

1516
export interface TokenFiatRateRequest {
1617
address: Hex;
@@ -29,12 +30,17 @@ export function useTokenFiatRates(requests: TokenFiatRateRequest[]) {
2930
() =>
3031
safeRequests.map(({ address, chainId, currency: currencyOverride }) => {
3132
const currency = currencyOverride ?? selectedCurrency;
33+
const isUsd = currency.toLowerCase() === 'usd';
3234

33-
if (
34-
currency.toLowerCase() === 'usd' &&
35+
const isArbitrumUSDC =
3536
address.toLowerCase() === ARBITRUM_USDC.address.toLowerCase() &&
36-
chainId === CHAIN_IDS.ARBITRUM
37-
) {
37+
chainId === CHAIN_IDS.ARBITRUM;
38+
39+
const isPolygonUSDCE =
40+
address.toLowerCase() === POLYGON_USDCE.address.toLowerCase() &&
41+
chainId === CHAIN_IDS.POLYGON;
42+
43+
if (isUsd && (isArbitrumUSDC || isPolygonUSDCE)) {
3844
return 1;
3945
}
4046

app/components/Views/confirmations/hooks/transactions/useTransactionCustomAmount.test.ts

Lines changed: 18 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -342,8 +342,6 @@ describe('useTransactionCustomAmount', () => {
342342
{},
343343
] as TransactionPayRequiredToken[]);
344344

345-
useParamsMock.mockReturnValue({ amount: '43.21' });
346-
347345
const { result } = runHook();
348346

349347
await act(async () => {
@@ -368,7 +366,7 @@ describe('useTransactionCustomAmount', () => {
368366
expect(result.current.amountFiat).toBe('1234.56');
369367
});
370368

371-
it('to percentage of predict balance', async () => {
369+
it('to percentage of predict balance converted to USD', async () => {
372370
usePredictBalanceMock.mockReturnValue({ balance: 4321.23 } as never);
373371

374372
const { result } = runHook({
@@ -381,7 +379,23 @@ describe('useTransactionCustomAmount', () => {
381379
result.current.updatePendingAmountPercentage(43);
382380
});
383381

384-
expect(result.current.amountFiat).toBe('1858.12');
382+
expect(result.current.amountFiat).toBe('3716.25');
383+
});
384+
385+
it('to total predict balance with no buffers if 100', async () => {
386+
usePredictBalanceMock.mockReturnValue({ balance: 4321.23 } as never);
387+
388+
const { result } = runHook({
389+
transactionMeta: {
390+
type: TransactionType.predictWithdraw,
391+
},
392+
});
393+
394+
await act(async () => {
395+
result.current.updatePendingAmountPercentage(100);
396+
});
397+
398+
expect(result.current.amountFiat).toBe('8642.46');
385399
});
386400
});
387401
});

app/components/Views/confirmations/hooks/transactions/useTransactionCustomAmount.ts

Lines changed: 17 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -44,17 +44,15 @@ export function useTransactionCustomAmount({
4444
const { chainId } = transactionMeta;
4545

4646
const tokenAddress = getTokenAddress(transactionMeta);
47-
const tokenFiatRate = useTokenFiatRate(tokenAddress, chainId, currency);
48-
const balanceUsd = useTokenBalance();
47+
const tokenFiatRate = useTokenFiatRate(tokenAddress, chainId, currency) ?? 1;
48+
const balanceUsd = useTokenBalance(tokenFiatRate);
4949

5050
const { updateTokenAmount: updateTokenAmountCallback } =
5151
useUpdateTokenAmount();
5252

5353
const amountHuman = useMemo(
5454
() =>
55-
new BigNumber(amountFiat || '0')
56-
.dividedBy(tokenFiatRate ?? 1)
57-
.toString(10),
55+
new BigNumber(amountFiat || '0').dividedBy(tokenFiatRate).toString(10),
5856
[amountFiat, tokenFiatRate],
5957
);
6058

@@ -129,11 +127,12 @@ function useMaxPercentage() {
129127

130128
return useMemo(() => {
131129
// Assumes we're not targetting native tokens.
132-
if (
130+
const payTokenIsRequiredToken =
133131
payToken?.chainId === chainId &&
134132
payToken?.address.toLowerCase() ===
135-
requiredTokens[0]?.address?.toLowerCase()
136-
) {
133+
requiredTokens[0]?.address?.toLowerCase();
134+
135+
if (!payToken || payTokenIsRequiredToken) {
137136
return 100;
138137
}
139138

@@ -162,15 +161,22 @@ function useMaxPercentage() {
162161
}, [chainId, featureFlags, payToken, requiredTokens]);
163162
}
164163

165-
function useTokenBalance() {
164+
function useTokenBalance(tokenFiatRate: number) {
166165
const transactionMeta = useTransactionMetadataRequest() as TransactionMeta;
167166
const { convertFiat } = useTransactionPayFiat();
168167

169168
const { payToken } = useTransactionPayToken();
170169
const payTokenBalance = convertFiat(payToken?.balanceUsd ?? 0);
171-
const { balance: predictBalance } = usePredictBalance({ loadOnMount: true });
170+
171+
const { balance: predictBalanceHuman } = usePredictBalance({
172+
loadOnMount: true,
173+
});
174+
175+
const predictBalanceUsd = new BigNumber(predictBalanceHuman ?? '0')
176+
.multipliedBy(tokenFiatRate)
177+
.toNumber();
172178

173179
return hasTransactionType(transactionMeta, [TransactionType.predictWithdraw])
174-
? predictBalance
180+
? predictBalanceUsd
175181
: payTokenBalance;
176182
}

0 commit comments

Comments
 (0)