Skip to content

Commit a4f45f0

Browse files
authored
fix: update required assets correctly for money account deposits (#29806)
<!-- Please submit this PR as a draft initially. Do not mark it as "Ready for review" until this PR meets the canonical Definition of Ready For Review in `docs/readme/ready-for-review.md`. In short: the template must be materially complete (not just section titles present), all status checks must be currently passing, and the only expected follow-up commits must be reviewer-driven. --> ## **Description** Fix money account deposits by setting correst required asset in transaction. ## **Changelog** <!-- If this PR is not End-User-Facing and should not show up in the CHANGELOG, you can choose to either: 1. Write `CHANGELOG entry: null` 2. Label with `no-changelog` If this PR is End-User-Facing, please write a short User-Facing description in the past tense like: `CHANGELOG entry: Added a new tab for users to see their NFTs` `CHANGELOG entry: Fixed a bug that was causing some NFTs to flicker` (This helps the Release Engineer do their job more quickly and accurately) --> CHANGELOG entry: ## **Related issues** Ref: https://consensyssoftware.atlassian.net/browse/CONF-1324 ## **Manual testing steps** ```gherkin Feature: my feature name Scenario: user [verb for user action] Given [describe expected initial app state] When user [verb for user action] Then [describe expected outcome] ``` ## **Screenshots/Recordings** NA ## **Pre-merge author checklist** <!-- Every checklist item must be consciously assessed before marking this PR as "Ready for review". A checked box means you deliberately considered that responsibility, not that you literally performed every action listed. Unchecked boxes are ambiguous: they are not an implicit "N/A" and they are not a silent "skip". See `docs/readme/ready-for-review.md` for the full checklist semantics. --> - [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. #### Performance checks (if applicable) - [ ] I've tested on Android - Ideally on a mid-range device; emulator is acceptable - [ ] I've tested with a power user scenario - Use these [power-user SRPs](https://consensyssoftware.atlassian.net/wiki/spaces/TL1/pages/edit-v2/401401446401?draftShareId=9d77e1e1-4bdc-4be1-9ebb-ccd916988d93) to import wallets with many accounts and tokens - [ ] I've instrumented key operations with Sentry traces for production performance metrics - See [`trace()`](/app/util/trace.ts) for usage and [`addToken`](/app/components/Views/AddAsset/components/AddCustomToken/AddCustomToken.tsx#L274) for an example For performance guidelines and tooling, see the [Performance Guide](https://consensyssoftware.atlassian.net/wiki/spaces/TL1/pages/400085549067/Performance+Guide+for+Engineers). ## **Pre-merge reviewer checklist** <!-- Reviewer checklist items follow the same semantics as the author checklist: an unchecked box is ambiguous, a checked box means the reviewer consciously assessed that responsibility. See `docs/readme/ready-for-review.md`. --> - [ ] 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] > **Medium Risk** > Touches transaction amount update flow for `moneyAccountDeposit`, including writing `requiredAssets` via `updateTransaction`, which could affect what token/amount is used for deposits if decimals or asset ordering are wrong. > > **Overview** > Fixes Money Account deposit pay-amount updates by **syncing `transactionMeta.requiredAssets[0].amount`** to the user-entered amount (converted to atomic units using required-token decimals, rounded up, and hex-encoded) before applying nested transaction updates. > > Also updates `getTokenAddress` to **prefer `requiredAssets[0].address`** when no ERC-20 transfer call is detected, and adds/expands unit tests covering the new required-asset sync and address selection behavior. > > <sup>Reviewed by [Cursor Bugbot](https://cursor.com/bugbot) for commit c14d618. Bugbot is set up for automated code reviews on this repo. Configure [here](https://www.cursor.com/dashboard/bugbot).</sup> <!-- /CURSOR_SUMMARY -->
1 parent 9d7c607 commit a4f45f0

4 files changed

Lines changed: 250 additions & 3 deletions

File tree

app/components/Views/confirmations/hooks/pay/useUpdateTransactionPayAmount.test.ts

Lines changed: 161 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,10 @@ import { useUpdateTransactionPayAmount } from './useUpdateTransactionPayAmount';
44
import { simpleSendTransactionControllerMock } from '../../__mocks__/controllers/transaction-controller-mock';
55
import { transactionApprovalControllerMock } from '../../__mocks__/controllers/approval-controller-mock';
66
import { otherControllersMock } from '../../__mocks__/controllers/other-controllers-mock';
7-
import { updateAtomicBatchData } from '../../../../../util/transaction-controller';
7+
import {
8+
updateAtomicBatchData,
9+
updateTransaction,
10+
} from '../../../../../util/transaction-controller';
811
import {
912
updateMoneyAccountDepositTokenAmount,
1013
updateMoneyAccountWithdrawTokenAmount,
@@ -15,11 +18,15 @@ import {
1518
} from '@metamask/transaction-controller';
1619
import { useUpdateTokenAmount } from '../transactions/useUpdateTokenAmount';
1720
import Logger from '../../../../../util/Logger';
21+
import { useTransactionPayRequiredTokens } from './useTransactionPayData';
22+
import { TransactionPayRequiredToken } from '@metamask/transaction-pay-controller';
23+
import { Hex } from '@metamask/utils';
1824

1925
jest.mock('../../../../../util/transaction-controller');
2026
jest.mock('../../../../UI/Money/utils/moneyAccountTransactions');
2127
jest.mock('../transactions/useUpdateTokenAmount');
2228
jest.mock('../../../../../util/Logger');
29+
jest.mock('./useTransactionPayData');
2330

2431
const moneyAccountDepositMeta: Partial<TransactionMeta> = {
2532
type: TransactionType.moneyAccountDeposit,
@@ -57,13 +64,17 @@ function runHook({
5764

5865
describe('useUpdateTransactionPayAmount', () => {
5966
const updateAtomicBatchDataMock = jest.mocked(updateAtomicBatchData);
67+
const updateTransactionMock = jest.mocked(updateTransaction);
6068
const updateMoneyAccountDepositTokenAmountMock = jest.mocked(
6169
updateMoneyAccountDepositTokenAmount,
6270
);
6371
const updateMoneyAccountWithdrawTokenAmountMock = jest.mocked(
6472
updateMoneyAccountWithdrawTokenAmount,
6573
);
6674
const useUpdateTokenAmountMock = jest.mocked(useUpdateTokenAmount);
75+
const useTransactionPayRequiredTokensMock = jest.mocked(
76+
useTransactionPayRequiredTokens,
77+
);
6778
const updateTokenAmountMock = jest.fn();
6879
const loggerErrorMock = jest.mocked(Logger.error);
6980

@@ -73,6 +84,7 @@ describe('useUpdateTransactionPayAmount', () => {
7384
useUpdateTokenAmountMock.mockReturnValue({
7485
updateTokenAmount: updateTokenAmountMock,
7586
});
87+
useTransactionPayRequiredTokensMock.mockReturnValue([]);
7688
});
7789

7890
async function flushPromises() {
@@ -259,4 +271,152 @@ describe('useUpdateTransactionPayAmount', () => {
259271
expect.stringContaining('Failed to prepare Money Account withdraw'),
260272
);
261273
});
274+
275+
describe('syncMoneyAccountDepositRequiredAssets', () => {
276+
const TOKEN_ADDRESS_MOCK = '0xToken' as Hex;
277+
const existingRequiredAsset = {
278+
address: TOKEN_ADDRESS_MOCK,
279+
amount: '0x0' as Hex,
280+
standard: 'erc20',
281+
};
282+
const moneyAccountDepositMetaWithRequiredAssets = {
283+
...moneyAccountDepositMeta,
284+
requiredAssets: [existingRequiredAsset],
285+
};
286+
287+
beforeEach(() => {
288+
updateMoneyAccountDepositTokenAmountMock.mockResolvedValue([]);
289+
useTransactionPayRequiredTokensMock.mockReturnValue([
290+
{ decimals: 6 } as TransactionPayRequiredToken,
291+
]);
292+
});
293+
294+
it('calls updateTransaction with hex-encoded amount when requiredAssets exist', async () => {
295+
const { result } = runHook({
296+
transactionMeta: moneyAccountDepositMetaWithRequiredAssets,
297+
});
298+
299+
result.current.updateTransactionPayAmount('1');
300+
301+
await flushPromises();
302+
303+
expect(updateTransactionMock).toHaveBeenCalledTimes(1);
304+
expect(updateTransactionMock).toHaveBeenCalledWith(
305+
expect.objectContaining({
306+
requiredAssets: [{ ...existingRequiredAsset, amount: '0xf4240' }],
307+
}),
308+
'Money Account deposit: sync requiredAssets amount',
309+
);
310+
});
311+
312+
it('rounds fractional atomic amounts up before encoding', async () => {
313+
const { result } = runHook({
314+
transactionMeta: moneyAccountDepositMetaWithRequiredAssets,
315+
});
316+
317+
result.current.updateTransactionPayAmount('1.0000005');
318+
319+
await flushPromises();
320+
321+
expect(updateTransactionMock).toHaveBeenCalledWith(
322+
expect.objectContaining({
323+
requiredAssets: [{ ...existingRequiredAsset, amount: '0xf4241' }],
324+
}),
325+
expect.any(String),
326+
);
327+
});
328+
329+
it('does not call updateTransaction when transactionMeta has no requiredAssets', async () => {
330+
const { result } = runHook({ transactionMeta: moneyAccountDepositMeta });
331+
332+
result.current.updateTransactionPayAmount('1');
333+
334+
await flushPromises();
335+
336+
expect(updateTransactionMock).not.toHaveBeenCalled();
337+
});
338+
339+
it('does not call updateTransaction when no required tokens are available', async () => {
340+
useTransactionPayRequiredTokensMock.mockReturnValue([]);
341+
342+
const { result } = runHook({
343+
transactionMeta: moneyAccountDepositMetaWithRequiredAssets,
344+
});
345+
346+
result.current.updateTransactionPayAmount('1');
347+
348+
await flushPromises();
349+
350+
expect(updateTransactionMock).not.toHaveBeenCalled();
351+
});
352+
353+
it('does not call updateTransaction when computed amount matches existing amount', async () => {
354+
const { result } = runHook({
355+
transactionMeta: {
356+
...moneyAccountDepositMeta,
357+
requiredAssets: [{ ...existingRequiredAsset, amount: '0xf4240' }],
358+
},
359+
});
360+
361+
result.current.updateTransactionPayAmount('1');
362+
363+
await flushPromises();
364+
365+
expect(updateTransactionMock).not.toHaveBeenCalled();
366+
});
367+
368+
it('does not run sync logic for non-deposit transaction types', async () => {
369+
updateMoneyAccountWithdrawTokenAmountMock.mockResolvedValue([]);
370+
371+
const { result } = runHook({
372+
transactionMeta: {
373+
...moneyAccountWithdrawMeta,
374+
requiredAssets: [existingRequiredAsset],
375+
},
376+
});
377+
378+
result.current.updateTransactionPayAmount('1');
379+
380+
await flushPromises();
381+
382+
expect(updateTransactionMock).not.toHaveBeenCalled();
383+
});
384+
385+
it('logs an error when updateTransaction throws', async () => {
386+
const error = new Error('updateTransaction failed');
387+
updateTransactionMock.mockImplementation(() => {
388+
throw error;
389+
});
390+
391+
const { result } = runHook({
392+
transactionMeta: moneyAccountDepositMetaWithRequiredAssets,
393+
});
394+
395+
result.current.updateTransactionPayAmount('1');
396+
397+
await flushPromises();
398+
399+
expect(loggerErrorMock).toHaveBeenCalledWith(
400+
error,
401+
'Failed to sync Money Account deposit requiredAssets amount',
402+
);
403+
});
404+
405+
it('still applies money account deposit updates after syncing requiredAssets', async () => {
406+
updateMoneyAccountDepositTokenAmountMock.mockResolvedValue([
407+
{ nestedTransactionIndex: 0, transactionData: '0xaaaa' },
408+
]);
409+
410+
const { result } = runHook({
411+
transactionMeta: moneyAccountDepositMetaWithRequiredAssets,
412+
});
413+
414+
result.current.updateTransactionPayAmount('1');
415+
416+
await flushPromises();
417+
418+
expect(updateTransactionMock).toHaveBeenCalledTimes(1);
419+
expect(updateAtomicBatchDataMock).toHaveBeenCalledTimes(1);
420+
});
421+
});
262422
});

app/components/Views/confirmations/hooks/pay/useUpdateTransactionPayAmount.ts

Lines changed: 52 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,18 +1,25 @@
11
import { useCallback } from 'react';
2+
import { BigNumber } from 'bignumber.js';
3+
import { toHex } from '@metamask/controller-utils';
24
import {
35
TransactionMeta,
46
TransactionType,
57
} from '@metamask/transaction-controller';
8+
import { Hex } from '@metamask/utils';
69
import { useTransactionMetadataRequest } from '../transactions/useTransactionMetadataRequest';
710
import { useUpdateTokenAmount } from '../transactions/useUpdateTokenAmount';
8-
import { updateAtomicBatchData } from '../../../../../util/transaction-controller';
11+
import {
12+
updateAtomicBatchData,
13+
updateTransaction,
14+
} from '../../../../../util/transaction-controller';
915
import {
1016
updateMoneyAccountDepositTokenAmount,
1117
updateMoneyAccountWithdrawTokenAmount,
1218
} from '../../../../UI/Money/utils/moneyAccountTransactions';
1319
import { UpdateTransactionPayAmountCall } from '../../types/transactions';
1420
import { hasTransactionType } from '../../utils/transaction';
1521
import Logger from '../../../../../util/Logger';
22+
import { useTransactionPayRequiredTokens } from './useTransactionPayData';
1623

1724
type MoneyAccountAmountUpdater = (
1825
transactionMeta: TransactionMeta,
@@ -22,6 +29,7 @@ type MoneyAccountAmountUpdater = (
2229
export function useUpdateTransactionPayAmount() {
2330
const transactionMeta = useTransactionMetadataRequest();
2431
const { updateTokenAmount } = useUpdateTokenAmount();
32+
const requiredTokens = useTransactionPayRequiredTokens();
2533

2634
const applyMoneyAccountAmountUpdates = useCallback(
2735
async (
@@ -72,6 +80,11 @@ export function useUpdateTransactionPayAmount() {
7280
TransactionType.moneyAccountDeposit,
7381
])
7482
) {
83+
syncMoneyAccountDepositRequiredAssets(
84+
transactionMeta,
85+
amountHuman,
86+
requiredTokens?.[0]?.decimals,
87+
);
7588
await applyMoneyAccountAmountUpdates(
7689
amountHuman,
7790
updateMoneyAccountDepositTokenAmount,
@@ -95,8 +108,45 @@ export function useUpdateTransactionPayAmount() {
95108

96109
updateTokenAmount(amountHuman);
97110
},
98-
[transactionMeta, applyMoneyAccountAmountUpdates, updateTokenAmount],
111+
[
112+
transactionMeta,
113+
applyMoneyAccountAmountUpdates,
114+
updateTokenAmount,
115+
requiredTokens,
116+
],
99117
);
100118

101119
return { updateTransactionPayAmount };
102120
}
121+
122+
function syncMoneyAccountDepositRequiredAssets(
123+
transactionMeta: TransactionMeta,
124+
amountHuman: string,
125+
decimals: number | undefined,
126+
): void {
127+
const existing = transactionMeta.requiredAssets;
128+
if (!existing?.length || decimals === undefined) return;
129+
130+
try {
131+
const amount = toHex(
132+
new BigNumber(amountHuman)
133+
.shiftedBy(decimals)
134+
.decimalPlaces(0, BigNumber.ROUND_UP)
135+
.toFixed(0),
136+
) as Hex;
137+
if (existing[0].amount === amount) return;
138+
139+
updateTransaction(
140+
{
141+
...transactionMeta,
142+
requiredAssets: [{ ...existing[0], amount }, ...existing.slice(1)],
143+
},
144+
'Money Account deposit: sync requiredAssets amount',
145+
);
146+
} catch (error) {
147+
Logger.error(
148+
error as Error,
149+
'Failed to sync Money Account deposit requiredAssets amount',
150+
);
151+
}
152+
}

app/components/Views/confirmations/utils/transaction-pay.test.ts

Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -185,6 +185,38 @@ describe('Transaction Pay Utils', () => {
185185

186186
expect(getTokenAddress(transactionMeta)).toBe(TO_MOCK);
187187
});
188+
189+
it('returns first requiredAsset address if no nested transfer and requiredAssets present', () => {
190+
const requiredAssetAddress =
191+
'0xrequiredAssetAddress00000000000000000000' as Hex;
192+
const transactionMeta = {
193+
txParams: {
194+
data: '0x1234',
195+
to: TO_MOCK,
196+
},
197+
requiredAssets: [
198+
{
199+
address: requiredAssetAddress,
200+
amount: '0x1' as Hex,
201+
standard: 'erc20',
202+
},
203+
],
204+
} as TransactionMeta;
205+
206+
expect(getTokenAddress(transactionMeta)).toBe(requiredAssetAddress);
207+
});
208+
209+
it('falls back to txParams.to when requiredAssets is empty', () => {
210+
const transactionMeta = {
211+
txParams: {
212+
data: '0x1234',
213+
to: TO_MOCK,
214+
},
215+
requiredAssets: [],
216+
} as unknown as TransactionMeta;
217+
218+
expect(getTokenAddress(transactionMeta)).toBe(TO_MOCK);
219+
});
188220
});
189221

190222
describe('getAvailableTokens', () => {

app/components/Views/confirmations/utils/transaction-pay.ts

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -85,6 +85,11 @@ export function getTokenAddress(
8585
return nestedCall.to;
8686
}
8787

88+
const requiredAssetAddress = transactionMeta?.requiredAssets?.[0]?.address;
89+
if (requiredAssetAddress) {
90+
return requiredAssetAddress;
91+
}
92+
8893
return transactionMeta?.txParams?.to as Hex;
8994
}
9095

0 commit comments

Comments
 (0)