Skip to content

Commit 77e338b

Browse files
Adds confirmation screen for 'increaseAllowance'
1 parent ffd58dd commit 77e338b

File tree

14 files changed

+361
-6
lines changed

14 files changed

+361
-6
lines changed

app/_locales/en/messages.json

Lines changed: 4 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

app/scripts/lib/transaction/metrics.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -848,6 +848,7 @@ async function buildEventFragmentProperties({
848848
TransactionType.tokenMethodSetApprovalForAll,
849849
TransactionType.tokenMethodTransfer,
850850
TransactionType.tokenMethodTransferFrom,
851+
TransactionType.tokenMethodIncreaseAllowance,
851852
TransactionType.smart,
852853
TransactionType.swap,
853854
TransactionType.swapApproval,

shared/modules/transaction.utils.ts

Lines changed: 16 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,11 @@
11
import { isHexString } from 'ethereumjs-util';
22
import { Interface } from '@ethersproject/abi';
3-
import { abiERC721, abiERC20, abiERC1155 } from '@metamask/metamask-eth-abis';
3+
import {
4+
abiERC721,
5+
abiERC20,
6+
abiERC1155,
7+
USDC_ABI,
8+
} from '@metamask/metamask-eth-abis';
49
import type EthQuery from '@metamask/eth-query';
510
import log from 'loglevel';
611
import {
@@ -18,6 +23,7 @@ const INFERRABLE_TRANSACTION_TYPES: TransactionType[] = [
1823
TransactionType.tokenMethodSetApprovalForAll,
1924
TransactionType.tokenMethodTransfer,
2025
TransactionType.tokenMethodTransferFrom,
26+
TransactionType.tokenMethodIncreaseAllowance,
2127
TransactionType.contractInteraction,
2228
TransactionType.simpleSend,
2329
];
@@ -32,6 +38,7 @@ type InferTransactionTypeResult = {
3238
const erc20Interface = new Interface(abiERC20);
3339
const erc721Interface = new Interface(abiERC721);
3440
const erc1155Interface = new Interface(abiERC1155);
41+
const USDCInterface = new Interface(USDC_ABI);
3542

3643
/**
3744
* Determines if the maxFeePerGas and maxPriorityFeePerGas fields are supplied
@@ -116,6 +123,12 @@ export function parseStandardTokenTransactionData(data: string) {
116123
// ignore and return undefined
117124
}
118125

126+
try {
127+
return USDCInterface.parseTransaction({ data });
128+
} catch {
129+
// ignore and return undefined
130+
}
131+
119132
return undefined;
120133
}
121134

@@ -169,6 +182,7 @@ export async function determineTransactionType(
169182
TransactionType.tokenMethodSetApprovalForAll,
170183
TransactionType.tokenMethodTransfer,
171184
TransactionType.tokenMethodTransferFrom,
185+
TransactionType.tokenMethodIncreaseAllowance,
172186
TransactionType.tokenMethodSafeTransferFrom,
173187
].find((methodName) => isEqualCaseInsensitive(methodName, name));
174188
return {
@@ -227,6 +241,7 @@ export async function determineTransactionAssetType(
227241
TransactionType.tokenMethodSetApprovalForAll,
228242
TransactionType.tokenMethodTransfer,
229243
TransactionType.tokenMethodTransferFrom,
244+
TransactionType.tokenMethodIncreaseAllowance,
230245
].find((methodName) => methodName === inferrableType);
231246

232247
if (

test/e2e/helpers.js

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -681,6 +681,9 @@ const PRIVATE_KEY =
681681
const PRIVATE_KEY_TWO =
682682
'0xa444f52ea41e3a39586d7069cb8e8233e9f6b9dea9cbb700cce69ae860661cc8';
683683

684+
const ACCOUNT_1 = '0x5cfe73b6021e818b776b421b1c4db2474086a7e1';
685+
const ACCOUNT_2 = '0x09781764c08de8ca82e156bbf156a3ca217c7950';
686+
684687
const defaultGanacheOptions = {
685688
accounts: [{ secretKey: PRIVATE_KEY, balance: convertETHToHexGwei(25) }],
686689
};
@@ -1081,6 +1084,8 @@ module.exports = {
10811084
TEST_SEED_PHRASE_TWO,
10821085
PRIVATE_KEY,
10831086
PRIVATE_KEY_TWO,
1087+
ACCOUNT_1,
1088+
ACCOUNT_2,
10841089
getWindowHandles,
10851090
convertToHexValue,
10861091
tinyDelayMs,
Lines changed: 277 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,277 @@
1+
const FixtureBuilder = require('../../fixture-builder');
2+
const {
3+
defaultGanacheOptions,
4+
openDapp,
5+
sendTransaction,
6+
unlockWallet,
7+
withFixtures,
8+
ACCOUNT_1,
9+
ACCOUNT_2,
10+
WINDOW_TITLES,
11+
} = require('../../helpers');
12+
const { SMART_CONTRACTS } = require('../../seeder/smart-contracts');
13+
14+
describe('Increase Token Allowance', function () {
15+
const smartContract = SMART_CONTRACTS.HST;
16+
17+
it('increases token spending cap to allow other accounts to transfer tokens @no-mmi', async function () {
18+
await withFixtures(
19+
{
20+
dapp: true,
21+
fixtures: new FixtureBuilder()
22+
.withPermissionControllerConnectedToTestDapp()
23+
.build(),
24+
ganacheOptions: defaultGanacheOptions,
25+
smartContract,
26+
title: this.test.fullTitle(),
27+
},
28+
async ({ driver, contractRegistry }) => {
29+
const ACCOUNT_1_NAME = 'Account 1';
30+
const ACCOUNT_2_NAME = '2nd Account';
31+
32+
const initialSpendingCap = '1';
33+
const additionalSpendingCap = '1';
34+
35+
await unlockWallet(driver);
36+
37+
const contractAddress = await contractRegistry.getContractAddress(
38+
smartContract,
39+
);
40+
await openDapp(driver, contractAddress);
41+
42+
await deployTokenContract(driver);
43+
await approveTokenSpendingCapTo(driver, ACCOUNT_2, initialSpendingCap);
44+
45+
await sendTransaction(driver, ACCOUNT_2, '1');
46+
await addAccount(driver, ACCOUNT_2_NAME);
47+
48+
await triggerTransferFromTokens(driver, ACCOUNT_1, ACCOUNT_2);
49+
// 'Transfer From Tokens' on the test dApp attempts to transfer 1.5 TST.
50+
// Since this is higher than the 'initialSpendingCap', it should fail.
51+
await pollForTokenAddressesError(
52+
driver,
53+
'reverted ERC20: insufficient allowance',
54+
);
55+
56+
await switchToAccountWithName(driver, ACCOUNT_1_NAME);
57+
58+
await increaseTokenAllowance(driver, additionalSpendingCap);
59+
60+
await switchToAccountWithName(driver, ACCOUNT_2_NAME);
61+
await triggerTransferFromTokens(driver, ACCOUNT_1, ACCOUNT_2);
62+
await confirmTransferFromTokensSuccess(driver);
63+
},
64+
);
65+
});
66+
67+
async function deployTokenContract(driver) {
68+
await driver.findClickableElement('#deployButton');
69+
}
70+
71+
async function approveTokenSpendingCapTo(
72+
driver,
73+
accountToApproveFor,
74+
initialSpendingCap,
75+
) {
76+
await driver.switchToWindowWithTitle(WINDOW_TITLES.TestDApp);
77+
78+
let approveToFillEl = await driver.findElement('[id="approveTo"]');
79+
await approveToFillEl.clear();
80+
await approveToFillEl.fill(accountToApproveFor);
81+
82+
await driver.clickElement({ text: 'Approve Tokens', tag: 'button' });
83+
84+
await driver.switchToWindowWithTitle(
85+
WINDOW_TITLES.ExtensionInFullScreenView,
86+
);
87+
await driver.clickElement({ tag: 'button', text: 'Activity' });
88+
89+
const pendingTransactions = await driver.findElements(
90+
'.transaction-list__pending-transactions .activity-list-item',
91+
);
92+
pendingTransactions[0].click();
93+
94+
let setSpendingCap = await driver.findElement(
95+
'[data-testid="custom-spending-cap-input"]',
96+
);
97+
await setSpendingCap.fill(initialSpendingCap);
98+
99+
await driver.clickElement({
100+
tag: 'button',
101+
text: 'Next',
102+
});
103+
driver.waitForSelector({
104+
css: '.box--display-flex > h6',
105+
text: `10 TST`,
106+
});
107+
await driver.waitForSelector({
108+
text: `${initialSpendingCap} TST`,
109+
css: '.mm-box > h6',
110+
});
111+
await driver.clickElement({
112+
tag: 'button',
113+
text: 'Approve',
114+
});
115+
116+
await driver.waitForSelector({
117+
css: '.transaction-list__completed-transactions .activity-list-item [data-testid="activity-list-item-action"]',
118+
text: 'Approve TST spending cap',
119+
});
120+
}
121+
122+
async function addAccount(driver, newAccountName) {
123+
await driver.clickElement('[data-testid="account-menu-icon"]');
124+
await driver.clickElement(
125+
'[data-testid="multichain-account-menu-popover-action-button"]',
126+
);
127+
await driver.clickElement(
128+
'[data-testid="multichain-account-menu-popover-add-account"]',
129+
);
130+
131+
await driver.fill('[placeholder="Account 2"]', newAccountName);
132+
await driver.clickElement({ text: 'Create', tag: 'button' });
133+
await driver.findElement({
134+
css: '[data-testid="account-menu-icon"]',
135+
text: newAccountName,
136+
});
137+
}
138+
139+
async function triggerTransferFromTokens(
140+
driver,
141+
senderAccount,
142+
recipientAccount,
143+
) {
144+
await driver.switchToWindowWithTitle(WINDOW_TITLES.TestDApp);
145+
let transferFromSenderInputEl = await driver.findElement(
146+
'[id="transferFromSenderInput"]',
147+
);
148+
await transferFromSenderInputEl.clear();
149+
await transferFromSenderInputEl.fill(senderAccount);
150+
151+
let transferFromRecipientInputEl = await driver.findElement(
152+
'[id="transferFromRecipientInput"]',
153+
);
154+
await transferFromRecipientInputEl.clear();
155+
await transferFromRecipientInputEl.fill(recipientAccount);
156+
157+
await driver.clickElement({
158+
text: 'Transfer From Tokens',
159+
tag: 'button',
160+
});
161+
}
162+
163+
async function pollForTokenAddressesError(
164+
driver,
165+
errorMessagePart,
166+
timeout = driver.timeout,
167+
) {
168+
const pollInterval = 500;
169+
let elapsedTime = 0;
170+
171+
await new Promise((resolve, reject) => {
172+
const pollInsufficientAllowanceError = setInterval(async () => {
173+
try {
174+
const tokenAddressesElement = await driver.findElement(
175+
'#tokenAddresses',
176+
);
177+
const tokenAddressesMsgText = await tokenAddressesElement.getText();
178+
const isErrorThrown =
179+
tokenAddressesMsgText.includes(errorMessagePart);
180+
181+
if (isErrorThrown) {
182+
// Condition satisfied, stopping poll.
183+
clearInterval(pollInsufficientAllowanceError);
184+
resolve();
185+
} else {
186+
elapsedTime += pollInterval;
187+
if (elapsedTime >= timeout) {
188+
// Timeout reached, stopping poll.
189+
clearInterval(pollInsufficientAllowanceError);
190+
reject(
191+
new Error(
192+
`Did not throw '${errorMessagePart}' error as expected. Timeout reached, stopping poll.`,
193+
),
194+
);
195+
}
196+
}
197+
} catch (error) {
198+
clearInterval(pollInsufficientAllowanceError);
199+
reject(error);
200+
}
201+
}, pollInterval);
202+
});
203+
}
204+
205+
async function switchToAccountWithName(driver, accountName) {
206+
await driver.switchToWindowWithTitle(
207+
WINDOW_TITLES.ExtensionInFullScreenView,
208+
);
209+
await driver.clickElement('[data-testid="account-menu-icon"]');
210+
211+
await driver.findElement({
212+
css: `.multichain-account-list-item .multichain-account-list-item__account-name__button`,
213+
text: accountName,
214+
});
215+
216+
await driver.clickElement({
217+
css: `.multichain-account-list-item .multichain-account-list-item__account-name__button`,
218+
text: accountName,
219+
});
220+
}
221+
222+
async function increaseTokenAllowance(driver, finalSpendingCap) {
223+
await driver.switchToWindowWithTitle(WINDOW_TITLES.TestDApp);
224+
await driver.clickElement({
225+
text: 'Increase Token Allowance',
226+
tag: 'button',
227+
});
228+
229+
await driver.switchToWindowWithTitle(WINDOW_TITLES.Dialog);
230+
let setSpendingCap = await driver.findElement(
231+
'[data-testid="custom-spending-cap-input"]',
232+
);
233+
await setSpendingCap.fill(finalSpendingCap);
234+
235+
await driver.clickElement({
236+
tag: 'button',
237+
text: 'Next',
238+
});
239+
driver.waitForSelector({
240+
css: '.box--display-flex > h6',
241+
text: `10 TST`,
242+
});
243+
await driver.waitForSelector({
244+
text: `${finalSpendingCap} TST`,
245+
css: '.mm-box > h6',
246+
});
247+
await driver.clickElement({
248+
tag: 'button',
249+
text: 'Approve',
250+
});
251+
252+
await driver.switchToWindowWithTitle(
253+
WINDOW_TITLES.ExtensionInFullScreenView,
254+
);
255+
await driver.clickElement({ tag: 'button', text: 'Activity' });
256+
await driver.waitForSelector({
257+
css: '.transaction-list__completed-transactions .activity-list-item [data-testid="activity-list-item-action"]',
258+
text: 'Increase TST spending cap',
259+
});
260+
}
261+
262+
async function confirmTransferFromTokensSuccess(driver) {
263+
await driver.switchToWindowWithTitle(WINDOW_TITLES.Dialog);
264+
await driver.waitForSelector({ text: '1.5 TST', tag: 'h1' });
265+
await driver.clickElement({ text: 'Confirm', tag: 'button' });
266+
267+
await driver.switchToWindowWithTitle(
268+
WINDOW_TITLES.ExtensionInFullScreenView,
269+
);
270+
await driver.clickElement({ tag: 'button', text: 'Activity' });
271+
272+
await driver.waitForSelector({
273+
css: '.transaction-list__completed-transactions .activity-list-item [data-testid="activity-list-item-action"]',
274+
text: 'Send TST',
275+
});
276+
}
277+
});

ui/helpers/constants/routes.ts

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -98,6 +98,7 @@ const CONFIRM_SET_APPROVAL_FOR_ALL_PATH = '/set-approval-for-all';
9898
const CONFIRM_TRANSFER_FROM_PATH = '/transfer-from';
9999
const CONFIRM_SAFE_TRANSFER_FROM_PATH = '/safe-transfer-from';
100100
const CONFIRM_TOKEN_METHOD_PATH = '/token-method';
101+
const CONFIRM_INCREASE_ALLOWANCE_PATH = '/increase-allowance';
101102
const SIGNATURE_REQUEST_PATH = '/signature-request';
102103
const DECRYPT_MESSAGE_REQUEST_PATH = '/decrypt-message-request';
103104
const ENCRYPTION_PUBLIC_KEY_REQUEST_PATH = '/encryption-public-key-request';
@@ -177,6 +178,8 @@ const PATH_NAME_MAP = {
177178
'Confirm Approve Transaction Page',
178179
[`${CONFIRM_TRANSACTION_ROUTE}/:id${CONFIRM_SET_APPROVAL_FOR_ALL_PATH}`]:
179180
'Confirm Set Approval For All Transaction Page',
181+
[`${CONFIRM_TRANSACTION_ROUTE}/:id${CONFIRM_INCREASE_ALLOWANCE_PATH}`]:
182+
'Confirm Increase Allowance Transaction Page',
180183
[`${CONFIRM_TRANSACTION_ROUTE}/:id${CONFIRM_TRANSFER_FROM_PATH}`]:
181184
'Confirm Transfer From Transaction Page',
182185
[`${CONFIRM_TRANSACTION_ROUTE}/:id${CONFIRM_SAFE_TRANSFER_FROM_PATH}`]:
@@ -225,6 +228,7 @@ export {
225228
CONFIRM_TRANSFER_FROM_PATH,
226229
CONFIRM_SAFE_TRANSFER_FROM_PATH,
227230
CONFIRM_TOKEN_METHOD_PATH,
231+
CONFIRM_INCREASE_ALLOWANCE_PATH,
228232
SIGNATURE_REQUEST_PATH,
229233
DECRYPT_MESSAGE_REQUEST_PATH,
230234
ENCRYPTION_PUBLIC_KEY_REQUEST_PATH,

0 commit comments

Comments
 (0)