Skip to content

Commit 77e2574

Browse files
fix: cp-12.17.3 support permit2 approvals in batch simulation (#32733)
## **Description** Include Permit2 approvals in batch simulation changes. In addition: - Fix incorrect `Unlimited` label. - Show loader in `Edit Spending Cap Modal` to prevent temporary token ID display. [![Open in GitHub Codespaces](https://github.com/codespaces/badge.svg)](https://codespaces.new/MetaMask/metamask-extension/pull/32733?quickstart=1) ## **Related issues** Fixes: [#4847](MetaMask/MetaMask-planning#4847) [#4846](MetaMask/MetaMask-planning#4846) ## **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 Extension Coding Standards](https://github.com/MetaMask/metamask-extension/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-extension/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.
1 parent 9dec650 commit 77e2574

File tree

13 files changed

+456
-112
lines changed

13 files changed

+456
-112
lines changed
+59
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,59 @@
1+
import {
2+
buildApproveTransactionData,
3+
buildIncreaseAllowanceTransactionData,
4+
buildPermit2ApproveTransactionData,
5+
} from '../../../test/data/confirmations/token-approve';
6+
import { updateApprovalAmount } from './approvals';
7+
8+
const SPENDER_MOCK = '0x0c54FcCd2e384b4BB6f2E405Bf5Cbc15a017AaFb';
9+
const TOKEN_ADDRESS_MOCK = '0x1234567890abcdef1234567890abcdef12345678';
10+
const AMOUNT_MOCK = 123;
11+
const EXPIRATION_MOCK = 456;
12+
13+
describe('Approvals Utils', () => {
14+
describe('updateApprovalAmount', () => {
15+
it('updates legacy approval amount', () => {
16+
expect(
17+
updateApprovalAmount(
18+
buildApproveTransactionData(SPENDER_MOCK, AMOUNT_MOCK),
19+
1.23,
20+
5,
21+
),
22+
).toStrictEqual(buildApproveTransactionData(SPENDER_MOCK, 123000));
23+
});
24+
25+
it('updates increaseAllowance amount', () => {
26+
expect(
27+
updateApprovalAmount(
28+
buildIncreaseAllowanceTransactionData(SPENDER_MOCK, AMOUNT_MOCK),
29+
1.23,
30+
5,
31+
),
32+
).toStrictEqual(
33+
buildIncreaseAllowanceTransactionData(SPENDER_MOCK, 123000),
34+
);
35+
});
36+
37+
it('updates Permit2 approval amount', () => {
38+
expect(
39+
updateApprovalAmount(
40+
buildPermit2ApproveTransactionData(
41+
SPENDER_MOCK,
42+
TOKEN_ADDRESS_MOCK,
43+
AMOUNT_MOCK,
44+
EXPIRATION_MOCK,
45+
),
46+
1.23,
47+
5,
48+
),
49+
).toStrictEqual(
50+
buildPermit2ApproveTransactionData(
51+
SPENDER_MOCK,
52+
TOKEN_ADDRESS_MOCK,
53+
123000,
54+
EXPIRATION_MOCK,
55+
),
56+
);
57+
});
58+
});
59+
});

shared/lib/transactions/approvals.ts

+45
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,45 @@
1+
import { Hex, add0x } from '@metamask/utils';
2+
import { BigNumber } from 'bignumber.js';
3+
import { Interface } from '@ethersproject/abi';
4+
import { parseApprovalTransactionData } from '../../modules/transaction.utils';
5+
6+
const SIGNATURE_LEGACY = 'function approve(address,uint256)';
7+
const SIGNATURE_PERMIT2 = 'function approve(address,address,uint160,uint48)';
8+
const SIGNATURE_INCREASE_ALLOWANCE =
9+
'function increaseAllowance(address,uint256)';
10+
11+
export function updateApprovalAmount(
12+
originalData: Hex,
13+
newAmount: string | number | BigNumber,
14+
decimals: number,
15+
): Hex {
16+
const { name, tokenAddress } =
17+
parseApprovalTransactionData(originalData) ?? {};
18+
19+
if (!name) {
20+
throw new Error('Invalid approval transaction data');
21+
}
22+
23+
const multiplier = new BigNumber(10).pow(decimals);
24+
const value = add0x(new BigNumber(newAmount).times(multiplier).toString(16));
25+
26+
let signature = tokenAddress ? SIGNATURE_PERMIT2 : SIGNATURE_LEGACY;
27+
28+
if (name === 'increaseAllowance') {
29+
signature = SIGNATURE_INCREASE_ALLOWANCE;
30+
}
31+
32+
const iface = new Interface([signature]);
33+
const decoded = iface.decodeFunctionData(name, originalData);
34+
35+
if (signature === SIGNATURE_PERMIT2) {
36+
return iface.encodeFunctionData(name, [
37+
tokenAddress,
38+
decoded[1],
39+
value,
40+
decoded[3],
41+
]) as Hex;
42+
}
43+
44+
return iface.encodeFunctionData(name, [decoded[0], value]) as Hex;
45+
}

shared/modules/transaction.utils.test.js

+52
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ import { createTestProviderTools } from '../../test/stub/provider';
55
import {
66
buildApproveTransactionData,
77
buildIncreaseAllowanceTransactionData,
8+
buildPermit2ApproveTransactionData,
89
} from '../../test/data/confirmations/token-approve';
910
import { buildSetApproveForAllTransactionData } from '../../test/data/confirmations/set-approval-for-all';
1011
import {
@@ -19,6 +20,8 @@ import {
1920

2021
const DATA_MOCK = '0x12345678';
2122
const ADDRESS_MOCK = '0x1234567890123456789012345678901234567890';
23+
const ADDRESS_2_MOCK = '0x1234567890123456789012345678901234567891';
24+
const EXPIRATION_MOCK = 1234567890;
2225
const AMOUNT_MOCK = 123;
2326

2427
describe('Transaction.utils', function () {
@@ -39,7 +42,29 @@ describe('Transaction.utils', function () {
3942
it('should not throw errors when called without arguments', () => {
4043
expect(() => parseStandardTokenTransactionData()).not.toThrow();
4144
});
45+
46+
it('decodes Permit2 function', () => {
47+
const result = parseStandardTokenTransactionData(
48+
buildPermit2ApproveTransactionData(
49+
ADDRESS_MOCK,
50+
ADDRESS_2_MOCK,
51+
AMOUNT_MOCK,
52+
EXPIRATION_MOCK,
53+
),
54+
);
55+
56+
expect(result.name).toBe('approve');
57+
expect(result.args).toStrictEqual(
58+
expect.objectContaining({
59+
token: ADDRESS_MOCK,
60+
spender: ADDRESS_2_MOCK,
61+
expiration: EXPIRATION_MOCK,
62+
}),
63+
);
64+
expect(result.args.amount.toString()).toBe(AMOUNT_MOCK.toString());
65+
});
4266
});
67+
4368
describe('isEIP1559Transaction', function () {
4469
it('should return true if both maxFeePerGas and maxPriorityFeePerGas are hex strings', () => {
4570
expect(
@@ -458,6 +483,8 @@ describe('Transaction.utils', function () {
458483
amountOrTokenId: new BigNumber(AMOUNT_MOCK),
459484
isApproveAll: false,
460485
isRevokeAll: false,
486+
name: 'approve',
487+
tokenAddress: undefined,
461488
});
462489
});
463490

@@ -470,6 +497,8 @@ describe('Transaction.utils', function () {
470497
amountOrTokenId: new BigNumber(AMOUNT_MOCK),
471498
isApproveAll: false,
472499
isRevokeAll: false,
500+
name: 'increaseAllowance',
501+
tokenAddress: undefined,
473502
});
474503
});
475504

@@ -482,6 +511,8 @@ describe('Transaction.utils', function () {
482511
amountOrTokenId: undefined,
483512
isApproveAll: true,
484513
isRevokeAll: false,
514+
name: 'setApprovalForAll',
515+
tokenAddress: undefined,
485516
});
486517
});
487518

@@ -494,6 +525,27 @@ describe('Transaction.utils', function () {
494525
amountOrTokenId: undefined,
495526
isApproveAll: false,
496527
isRevokeAll: true,
528+
name: 'setApprovalForAll',
529+
tokenAddress: undefined,
530+
});
531+
});
532+
533+
it('returns parsed data if Permit2 approve', () => {
534+
expect(
535+
parseApprovalTransactionData(
536+
buildPermit2ApproveTransactionData(
537+
ADDRESS_MOCK,
538+
ADDRESS_2_MOCK,
539+
AMOUNT_MOCK,
540+
EXPIRATION_MOCK,
541+
),
542+
),
543+
).toStrictEqual({
544+
amountOrTokenId: new BigNumber(AMOUNT_MOCK),
545+
isApproveAll: false,
546+
isRevokeAll: false,
547+
name: 'approve',
548+
tokenAddress: ADDRESS_MOCK,
497549
});
498550
});
499551
});

shared/modules/transaction.utils.ts

+39-24
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,19 @@ const INFERRABLE_TRANSACTION_TYPES: TransactionType[] = [
3030
TransactionType.simpleSend,
3131
];
3232

33+
const ABI_PERMIT_2_APPROVE = {
34+
inputs: [
35+
{ internalType: 'address', name: 'token', type: 'address' },
36+
{ internalType: 'address', name: 'spender', type: 'address' },
37+
{ internalType: 'uint160', name: 'amount', type: 'uint160' },
38+
{ internalType: 'uint48', name: 'expiration', type: 'uint48' },
39+
],
40+
name: 'approve',
41+
outputs: [],
42+
stateMutability: 'nonpayable',
43+
type: 'function',
44+
};
45+
3346
type InferTransactionTypeResult = {
3447
// The type of transaction
3548
type: TransactionType;
@@ -41,6 +54,7 @@ const erc20Interface = new Interface(abiERC20);
4154
const erc721Interface = new Interface(abiERC721);
4255
const erc1155Interface = new Interface(abiERC1155);
4356
const USDCInterface = new Interface(abiFiatTokenV2);
57+
const permit2Interface = new Interface([ABI_PERMIT_2_APPROVE]);
4458

4559
/**
4660
* Determines if the maxFeePerGas and maxPriorityFeePerGas fields are supplied
@@ -109,28 +123,20 @@ export function txParamsAreDappSuggested(
109123
* @returns TransactionDescription | undefined
110124
*/
111125
export function parseStandardTokenTransactionData(data: string) {
112-
try {
113-
return erc20Interface.parseTransaction({ data });
114-
} catch {
115-
// ignore and next try to parse with erc721 ABI
116-
}
117-
118-
try {
119-
return erc721Interface.parseTransaction({ data });
120-
} catch {
121-
// ignore and next try to parse with erc1155 ABI
122-
}
123-
124-
try {
125-
return erc1155Interface.parseTransaction({ data });
126-
} catch {
127-
// ignore and return undefined
128-
}
129-
130-
try {
131-
return USDCInterface.parseTransaction({ data });
132-
} catch {
133-
// ignore and return undefined
126+
const interfaces = [
127+
erc20Interface,
128+
erc721Interface,
129+
erc1155Interface,
130+
USDCInterface,
131+
permit2Interface,
132+
];
133+
134+
for (const iface of interfaces) {
135+
try {
136+
return iface.parseTransaction({ data });
137+
} catch {
138+
// Intentionally empty
139+
}
134140
}
135141

136142
return undefined;
@@ -339,31 +345,40 @@ export function parseApprovalTransactionData(data: Hex):
339345
amountOrTokenId?: BigNumber;
340346
isApproveAll?: boolean;
341347
isRevokeAll?: boolean;
348+
name: string;
349+
tokenAddress?: Hex;
342350
}
343351
| undefined {
344352
const transactionDescription = parseStandardTokenTransactionData(data);
345353
const { args, name } = transactionDescription ?? {};
346354

347355
if (
348-
!['approve', 'increaseAllowance', 'setApprovalForAll'].includes(name ?? '')
356+
!['approve', 'increaseAllowance', 'setApprovalForAll'].includes(
357+
name ?? '',
358+
) ||
359+
!name
349360
) {
350361
return undefined;
351362
}
352363

353364
const rawAmountOrTokenId =
354365
args?._value ?? // ERC-20 - approve
355-
args?.increment; // Fiat Token V2 - increaseAllowance
366+
args?.increment ?? // Fiat Token V2 - increaseAllowance
367+
args?.amount; // Permit2 - approve
356368

357369
const amountOrTokenId = rawAmountOrTokenId
358370
? new BigNumber(rawAmountOrTokenId?.toString())
359371
: undefined;
360372

361373
const isApproveAll = name === 'setApprovalForAll' && args?._approved === true;
362374
const isRevokeAll = name === 'setApprovalForAll' && args?._approved === false;
375+
const tokenAddress = name === 'approve' ? args?.token : undefined;
363376

364377
return {
365378
amountOrTokenId,
366379
isApproveAll,
367380
isRevokeAll,
381+
name,
382+
tokenAddress,
368383
};
369384
}

test/data/confirmations/token-approve.ts

+11
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,17 @@ export function buildApproveTransactionData(
1616
]).encodeFunctionData('approve', [address, amountOrTokenId]) as Hex;
1717
}
1818

19+
export function buildPermit2ApproveTransactionData(
20+
token: string,
21+
spender: string,
22+
amount: number,
23+
expiration: number,
24+
): Hex {
25+
return new Interface([
26+
'function approve(address token, address spender, uint160 amount, uint48 nonce)',
27+
]).encodeFunctionData('approve', [token, spender, amount, expiration]) as Hex;
28+
}
29+
1930
export function buildIncreaseAllowanceTransactionData(
2031
address: string,
2132
amount: number,

ui/pages/confirmations/components/confirm/info/approve/edit-spending-cap-modal/__snapshots__/edit-spending-cap-modal.test.tsx.snap

+1-1
Original file line numberDiff line numberDiff line change
@@ -63,7 +63,7 @@ exports[`EditSpendingCapModal renders component 1`] = `
6363
<p
6464
class="mm-box mm-text mm-text--body-sm mm-box--padding-top-1 mm-box--color-text-alternative"
6565
>
66-
Account balance: 0.000000000001 TST
66+
Account balance: 100 TST
6767
</p>
6868
</div>
6969
<div

0 commit comments

Comments
 (0)