Skip to content

Commit

Permalink
feat: Add redesigned ERC20 Approve confirmation and SpendingCap secti…
Browse files Browse the repository at this point in the history
…on (#26606)

<!--
Please submit this PR as a draft initially.
Do not mark it as "Ready for review" until the template has been
completely filled out, and PR status checks have passed at least once.
-->

## **Description**

Introduces support for ERC20 token transactions with the `approve` type.
This includes a new spending cap section and bespoke logic in the
"static simulation" section. Above a certain threshold, spending caps
are represented as "Unlimited".

<!--
Write a short description of the changes included in this pull request,
also include relevant motivation and context. Have in mind the following
questions:
1. What is the reason for the change?
2. What is the improvement/solution?
-->

[![Open in GitHub
Codespaces](https://github.com/codespaces/badge.svg)](https://codespaces.new/MetaMask/metamask-extension/pull/26606?quickstart=1)

## **Related issues**

Fixes:
[#2924](MetaMask/MetaMask-planning#2924)

## **Manual testing steps**

1. Go to this page...
2.
3.

## **Screenshots/Recordings**

<!-- If applicable, add screenshots and/or recordings to visualize the
before and after of your change. -->

### **Before**

<!-- [screenshots/recordings] -->

### **After**

<img width="472" alt="Screenshot 2024-08-22 at 12 30 02"
src="https://github.com/user-attachments/assets/2b23d5bd-f4d4-4301-b13d-89746954545c">
<img width="472" alt="Screenshot 2024-08-22 at 12 30 06"
src="https://github.com/user-attachments/assets/a4fd67bb-31a6-4d43-84c8-bca6fa4a5b45">


## **Pre-merge author checklist**

- [ ] I've followed [MetaMask Contributor
Docs](https://github.com/MetaMask/contributor-docs) and [MetaMask
Extension Coding
Standards](https://github.com/MetaMask/metamask-extension/blob/develop/.github/guidelines/CODING_GUIDELINES.md).
- [ ] I've completed the PR template to the best of my ability
- [ ] I’ve included tests if applicable
- [ ] I’ve documented my code using [JSDoc](https://jsdoc.app/) format
if applicable
- [ ] I’ve applied the right labels on the PR (see [labeling
guidelines](https://github.com/MetaMask/metamask-extension/blob/develop/.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.
  • Loading branch information
pedronfigueiredo authored Sep 2, 2024
1 parent 0d971b9 commit 4844619
Show file tree
Hide file tree
Showing 13 changed files with 1,012 additions and 28 deletions.
9 changes: 9 additions & 0 deletions app/_locales/en/messages.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Original file line number Diff line number Diff line change
@@ -0,0 +1,227 @@
/* eslint-disable @typescript-eslint/no-require-imports, @typescript-eslint/no-var-requires */
import { MockttpServer } from 'mockttp';
import { tinyDelayMs, veryLargeDelayMs, WINDOW_TITLES } from '../../../helpers';
import { Driver } from '../../../webdriver/driver';
import { scrollAndConfirmAndAssertConfirm } from '../helpers';
import {
openDAppWithContract,
TestSuiteArguments,
toggleAdvancedDetails,
} from './shared';

const {
defaultGanacheOptions,
defaultGanacheOptionsForType2Transactions,
withFixtures,
} = require('../../../helpers');
const FixtureBuilder = require('../../../fixture-builder');
const { SMART_CONTRACTS } = require('../../../seeder/smart-contracts');

describe('Confirmation Redesign ERC20 Approve Component', function () {
const smartContract = SMART_CONTRACTS.HST;

describe('Submit an Approve transaction @no-mmi', function () {
it('Sends a type 0 transaction (Legacy)', async function () {
await withFixtures(
{
dapp: true,
fixtures: new FixtureBuilder()
.withPermissionControllerConnectedToTestDapp()
.withPreferencesController({
preferences: {
redesignedConfirmationsEnabled: true,
isRedesignedConfirmationsDeveloperEnabled: true,
},
})
.build(),
ganacheOptions: defaultGanacheOptions,
smartContract,
testSpecificMock: mocks,
title: this.test?.fullTitle(),
},
async ({ driver, contractRegistry }: TestSuiteArguments) => {
await openDAppWithContract(driver, contractRegistry, smartContract);

await importTST(driver);

await createERC20ApproveTransaction(driver);

await assertApproveDetails(driver);

await confirmApproveTransaction(driver);
},
);
});

it('Sends a type 2 transaction (EIP1559)', async function () {
await withFixtures(
{
dapp: true,
fixtures: new FixtureBuilder()
.withPermissionControllerConnectedToTestDapp()
.withPreferencesController({
preferences: {
redesignedConfirmationsEnabled: true,
isRedesignedConfirmationsDeveloperEnabled: true,
},
})
.build(),
ganacheOptions: defaultGanacheOptionsForType2Transactions,
smartContract,
testSpecificMock: mocks,
title: this.test?.fullTitle(),
},
async ({ driver, contractRegistry }: TestSuiteArguments) => {
await openDAppWithContract(driver, contractRegistry, smartContract);

await importTST(driver);

await createERC20ApproveTransaction(driver);

await assertApproveDetails(driver);

await confirmApproveTransaction(driver);
},
);
});
});
});

async function mocked4Bytes(mockServer: MockttpServer) {
return await mockServer
.forGet('https://www.4byte.directory/api/v1/signatures/')
.withQuery({ hex_signature: '0x095ea7b3' })
.thenCallback(() => ({
statusCode: 200,
json: {
count: 1,
next: null,
previous: null,
results: [
{
id: 149,
created_at: '2016-07-09T03:58:29.617584Z',
text_signature: 'approve(address,uint256)',
hex_signature: '0x095ea7b3',
bytes_signature: '\t^§³',
},
],
},
}));
}

async function mocks(server: MockttpServer) {
return [await mocked4Bytes(server)];
}

async function importTST(driver: Driver) {
await driver.switchToWindowWithTitle(WINDOW_TITLES.ExtensionInFullScreenView);
await driver.clickElement('[data-testid="import-token-button"]');

await driver.waitForSelector({
css: '.import-tokens-modal__button-tab',
text: 'Custom token',
});
await driver.clickElement({
css: '.import-tokens-modal__button-tab',
text: 'Custom token',
});

await driver.fill(
'[data-testid="import-tokens-modal-custom-address"]',
'0x581c3C1A2A4EBDE2A0Df29B5cf4c116E42945947',
);

await driver.delay(tinyDelayMs);

await driver.clickElement({
css: '[data-testid="import-tokens-button-next"]',
text: 'Next',
});

await driver.clickElement({
css: '[data-testid="import-tokens-modal-import-button"]',
text: 'Import',
});
}

async function createERC20ApproveTransaction(driver: Driver) {
await driver.switchToWindowWithTitle(WINDOW_TITLES.TestDApp);
await driver.clickElement('#approveTokens');
}

async function assertApproveDetails(driver: Driver) {
await driver.delay(veryLargeDelayMs);
await driver.waitUntilXWindowHandles(3);
await driver.switchToWindowWithTitle(WINDOW_TITLES.Dialog);

await driver.waitForSelector({
css: 'h2',
text: 'Spending cap request',
});

await driver.waitForSelector({
css: 'p',
text: 'This site wants permission to withdraw your tokens',
});

await driver.waitForSelector({
css: 'p',
text: 'Estimated changes',
});

await driver.waitForSelector({
css: 'p',
text: 'Spending cap',
});

await driver.waitForSelector({
css: 'p',
text: '7',
});

await toggleAdvancedDetails(driver);

await driver.waitForSelector({
css: 'p',
text: 'Spender',
});

await driver.waitForSelector({
css: 'p',
text: 'Request from',
});

await driver.waitForSelector({
css: 'p',
text: 'Interacting with',
});

await driver.waitForSelector({
css: 'p',
text: 'Method',
});

await driver.waitForSelector({
css: 'p',
text: 'Account balance',
});

await driver.waitForSelector({
css: 'p',
text: 'Spending cap',
});
}

async function confirmApproveTransaction(driver: Driver) {
await scrollAndConfirmAndAssertConfirm(driver);

await driver.delay(veryLargeDelayMs);
await driver.waitUntilXWindowHandles(2);
await driver.switchToWindowWithTitle(WINDOW_TITLES.ExtensionInFullScreenView);

await driver.clickElement({ text: 'Activity', tag: 'button' });
await driver.waitForSelector(
'.transaction-list__completed-transactions .activity-list-item:nth-of-type(1)',
);
}
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
/* eslint-disable @typescript-eslint/no-require-imports, @typescript-eslint/no-var-requires */
import { MockttpServer } from 'mockttp';
import { veryLargeDelayMs, WINDOW_TITLES } from '../../../helpers';
import { Driver } from '../../../webdriver/driver';
import { scrollAndConfirmAndAssertConfirm } from '../helpers';
Expand Down Expand Up @@ -35,6 +36,7 @@ describe('Confirmation Redesign ERC721 Approve Component', function () {
.build(),
ganacheOptions: defaultGanacheOptions,
smartContract,
testSpecificMock: mocks,
title: this.test?.fullTitle(),
},
async ({ driver, contractRegistry }: TestSuiteArguments) => {
Expand Down Expand Up @@ -66,6 +68,7 @@ describe('Confirmation Redesign ERC721 Approve Component', function () {
.build(),
ganacheOptions: defaultGanacheOptionsForType2Transactions,
smartContract,
testSpecificMock: mocks,
title: this.test?.fullTitle(),
},
async ({ driver, contractRegistry }: TestSuiteArguments) => {
Expand All @@ -83,6 +86,33 @@ describe('Confirmation Redesign ERC721 Approve Component', function () {
});
});

async function mocked4Bytes(mockServer: MockttpServer) {
return await mockServer
.forGet('https://www.4byte.directory/api/v1/signatures/')
.withQuery({ hex_signature: '0x095ea7b3' })
.thenCallback(() => ({
statusCode: 200,
json: {
count: 1,
next: null,
previous: null,
results: [
{
id: 149,
created_at: '2016-07-09T03:58:29.617584Z',
text_signature: 'approve(address,uint256)',
hex_signature: '0x095ea7b3',
bytes_signature: '\t^§³',
},
],
},
}));
}

async function mocks(server: MockttpServer) {
return [await mocked4Bytes(server)];
}

async function createMintTransaction(driver: Driver) {
await driver.switchToWindowWithTitle(WINDOW_TITLES.TestDApp);
await driver.clickElement('#mintButton');
Expand Down Expand Up @@ -121,8 +151,28 @@ async function assertApproveDetails(driver: Driver) {
text: 'This site wants permission to withdraw your NFTs',
});

await driver.waitForSelector({
css: 'p',
text: 'Estimated changes',
});

await driver.waitForSelector({
css: 'p',
text: 'Withdraw',
});

await driver.waitForSelector({
css: 'p',
text: '#1',
});

await toggleAdvancedDetails(driver);

await driver.waitForSelector({
css: 'p',
text: 'Spender',
});

await driver.waitForSelector({
css: 'p',
text: 'Request from',
Expand Down
Loading

0 comments on commit 4844619

Please sign in to comment.