Skip to content

Commit

Permalink
fix: [cherrypick][V12.3.0] PermitSingle, PermitBatch, PermitTransferF…
Browse files Browse the repository at this point in the history
…rom, PermitBatchTransferFrom simulation "Spending cap" (#27300)

<!--
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**

Cherry-picks #26684
into V12.3.0

resolved conflicts:
- permit-simulation (does not have static simulation yet)
- tests + snaps (12.3.0 does not have useConfirmContext yet)
<!--
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/27300?quickstart=1)

## **Related issues**

Fixes: #26591
(PermitSingle)
Fixes: #26592
(PermitBatch)

## **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**

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

## **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
digiwand authored Sep 20, 2024
1 parent 5e6d703 commit 3fde91a
Show file tree
Hide file tree
Showing 11 changed files with 2,284 additions and 420 deletions.
38 changes: 38 additions & 0 deletions test/data/confirmations/typed_sign.ts
Original file line number Diff line number Diff line change
Expand Up @@ -173,3 +173,41 @@ export const permitSignatureMsg = {
origin: 'https://metamask.github.io',
},
} as SignatureRequestType;

export const permitBatchSignatureMsg = {
id: '0b1787a0-1c44-11ef-b70d-e7064bd7b659',
securityAlertResponse: {
reason: 'loading',
result_type: 'validation_in_progress',
securityAlertId: 'ab21395f-2190-472f-8cfa-3d224e7529d8',
},
status: 'unapproved',
time: 1716826404122,
type: 'eth_signTypedData',
msgParams: {
data: '{"types":{"PermitBatch":[{"name":"details","type":"PermitDetails[]"},{"name":"spender","type":"address"},{"name":"sigDeadline","type":"uint256"}],"PermitDetails":[{"name":"token","type":"address"},{"name":"amount","type":"uint160"},{"name":"expiration","type":"uint48"},{"name":"nonce","type":"uint48"}],"EIP712Domain":[{"name":"name","type":"string"},{"name":"chainId","type":"uint256"},{"name":"verifyingContract","type":"address"}]},"domain":{"name":"Permit2","chainId":"1","verifyingContract":"0x000000000022d473030f116ddee9f6b43ac78ba3"},"primaryType":"PermitBatch","message":{"details":[{"token":"0xa0b86991c6218b36c1d19d4a2e9eb0ce3606eb48","amount":"1461501637330902918203684832716283019655932542975","expiration":"1722887542","nonce":"5"},{"token":"0xb0b86991c6218b36c1d19d4a2e9eb0ce3606eb48","amount":"2461501637330902918203684832716283019655932542975","expiration":"1722887642","nonce":"6"}],"spender":"0x3fc91a3afd70395cd496c647d5a6cc9d4b2b7fad","sigDeadline":"1720297342"}}',
from: '0x935e73edb9ff52e23bac7f7e043a1ecd06d05477',
version: 'V4',
signatureMethod: 'eth_signTypedData_v4',
origin: 'https://metamask.github.io',
},
} as SignatureRequestType;

export const permitSingleSignatureMsg = {
id: '0b1787a0-1c44-11ef-b70d-e7064bd7b659',
securityAlertResponse: {
reason: 'loading',
result_type: 'validation_in_progress',
securityAlertId: 'ab21395f-2190-472f-8cfa-3d224e7529d8',
},
status: 'unapproved',
time: 1716826404122,
type: 'eth_signTypedData',
msgParams: {
data: '{"types":{"PermitSingle":[{"name":"details","type":"PermitDetails"},{"name":"spender","type":"address"},{"name":"sigDeadline","type":"uint256"}],"PermitDetails":[{"name":"token","type":"address"},{"name":"amount","type":"uint160"},{"name":"expiration","type":"uint48"},{"name":"nonce","type":"uint48"}],"EIP712Domain":[{"name":"name","type":"string"},{"name":"chainId","type":"uint256"},{"name":"verifyingContract","type":"address"}]},"domain":{"name":"Permit2","chainId":"1","verifyingContract":"0x000000000022d473030f116ddee9f6b43ac78ba3"},"primaryType":"PermitSingle","message":{"details":{"token":"0xa0b86991c6218b36c1d19d4a2e9eb0ce3606eb48","amount":"1461501637330902918203684832716283019655932542975","expiration":"1722887542","nonce":"5"},"spender":"0x3fc91a3afd70395cd496c647d5a6cc9d4b2b7fad","sigDeadline":"1720297342"}}',
from: '0x935e73edb9ff52e23bac7f7e043a1ecd06d05477',
version: 'V4',
signatureMethod: 'eth_signTypedData_v4',
origin: 'https://metamask.github.io',
},
} as SignatureRequestType;
Original file line number Diff line number Diff line change
Expand Up @@ -63,51 +63,55 @@ exports[`PermitSimulation renders component correctly 1`] = `
style="margin-left: auto; max-width: 100%;"
>
<div
class="mm-box mm-box--display-flex"
class="mm-box"
>
<div
class="mm-box mm-box--margin-inline-end-1 mm-box--display-inline mm-box--min-width-0"
class="mm-box mm-box--display-flex mm-box--justify-content-flex-end"
>
<div
style="min-width: 0;"
class="mm-box mm-box--margin-inline-end-1 mm-box--display-inline mm-box--min-width-0"
>
<div
aria-describedby="tippy-tooltip-2"
class=""
data-original-title="30"
data-tooltipped=""
style="display: inline;"
tabindex="0"
style="min-width: 0;"
>
<p
class="mm-box mm-text mm-text--body-md mm-text--text-align-center mm-box--padding-inline-2 mm-box--color-text-default mm-box--background-color-background-alternative mm-box--rounded-xl"
data-testid="simulation-token-value"
style="padding-top: 1px; padding-bottom: 1px;"
<div
aria-describedby="tippy-tooltip-2"
class=""
data-original-title="30"
data-tooltipped=""
style="display: inline;"
tabindex="0"
>
30
</p>
<p
class="mm-box mm-text mm-text--body-md mm-text--text-align-center mm-box--padding-inline-2 mm-box--color-text-default mm-box--background-color-background-alternative mm-box--rounded-xl"
data-testid="simulation-token-value"
style="padding-top: 1px; padding-bottom: 1px;"
>
30
</p>
</div>
</div>
</div>
</div>
<div>
<div
class="name name__missing"
>
<span
class="mm-box name__icon mm-icon mm-icon--size-md mm-box--display-inline-block mm-box--color-inherit"
style="mask-image: url('./images/icons/question.svg');"
/>
<p
class="mm-box mm-text name__value mm-text--body-md mm-box--color-text-default"
<div>
<div
class="name name__missing"
>
0xCcCCc...ccccC
</p>
<span
class="mm-box name__icon mm-icon mm-icon--size-md mm-box--display-inline-block mm-box--color-inherit"
style="mask-image: url('./images/icons/question.svg');"
/>
<p
class="mm-box mm-text name__value mm-text--body-md mm-box--color-text-default"
>
0xCcCCc...ccccC
</p>
</div>
</div>
</div>
<div
class="mm-box"
/>
</div>
<div
class="mm-box"
/>
</div>
</div>
</div>
Expand Down
Original file line number Diff line number Diff line change
@@ -1,24 +1,41 @@
import React from 'react';
import configureMockStore from 'redux-mock-store';

import { act } from 'react-dom/test-utils';
import mockState from '../../../../../../../../test/data/mock-state.json';
// import { getMockTypedSignConfirmStateForRequest } from '../../../../../../../../test/data/confirmations/helper';
import { renderWithProvider } from '../../../../../../../../test/lib/render-helpers';
// TODO: readd in 12.4.0
// import { renderWithConfirmContextProvider } from '../../../../../../../../test/lib/confirmations/render-helpers';
import { permitSignatureMsg } from '../../../../../../../../test/data/confirmations/typed_sign';
import PermitSimulation from './permit-simulation';

jest.mock('../../../../../../../store/actions', () => {
return {
getTokenStandardAndDetails: jest.fn().mockResolvedValue({ decimals: 2 }),
};
});

describe('PermitSimulation', () => {
it('renders component correctly', () => {
it('renders component correctly', async () => {
const state = {
...mockState,
confirm: {
currentConfirmation: permitSignatureMsg,
},
};
// TODO: readd in 12.4.0
// const state = getMockTypedSignConfirmStateForRequest(permitSignatureMsg);
const mockStore = configureMockStore([])(state);
const { container } = renderWithProvider(
<PermitSimulation tokenDecimals={2} />,
mockStore,
);
expect(container).toMatchSnapshot();

await act(async () => {
const { container, findByText } = renderWithProvider(
<PermitSimulation />,
mockStore,
);

expect(await findByText('30')).toBeInTheDocument();
expect(container).toMatchSnapshot();
});
});
});
Original file line number Diff line number Diff line change
@@ -1,69 +1,61 @@
import React, { useMemo } from 'react';
import React from 'react';
import { useSelector } from 'react-redux';
import { NameType } from '@metamask/name-controller';

import { calcTokenAmount } from '../../../../../../../../shared/lib/transactions-controller-utils';
import { PrimaryType } from '../../../../../../../../shared/constants/signatures';
import { parseTypedDataMessage } from '../../../../../../../../shared/modules/transaction.utils';
import { Numeric } from '../../../../../../../../shared/modules/Numeric';
import Name from '../../../../../../../components/app/name/name';
import {
ConfirmInfoRow,
ConfirmInfoRowText,
} from '../../../../../../../components/app/confirm/info/row';
import { shortenString } from '../../../../../../../helpers/utils/util';
import { useI18nContext } from '../../../../../../../hooks/useI18nContext';
import { currentConfirmationSelector } from '../../../../../../../selectors';
import { Box, Text } from '../../../../../../../components/component-library';
import Tooltip from '../../../../../../../components/ui/tooltip';
import { Box } from '../../../../../../../components/component-library';
import {
BackgroundColor,
BlockSize,
BorderRadius,
Display,
TextAlign,
FlexDirection,
} from '../../../../../../../helpers/constants/design-system';
import { SignatureRequestType } from '../../../../../types/confirm';
import useTokenExchangeRate from '../../../../../../../components/app/currency-input/hooks/useTokenExchangeRate';
import { IndividualFiatDisplay } from '../../../../simulation-details/fiat-display';
import {
formatAmount,
formatAmountMaxPrecision,
} from '../../../../simulation-details/formatAmount';
import { ConfirmInfoSection } from '../../../../../../../components/app/confirm/info/row/section';
import PermitSimulationValueDisplay from './value-display/value-display';

function extractTokenDetailsByPrimaryType(
message: Record<string, unknown>,
primaryType: PrimaryType,
): object[] | unknown {
let tokenDetails;

switch (primaryType) {
case PrimaryType.PermitBatch:
case PrimaryType.PermitSingle:
tokenDetails = message?.details;
break;
case PrimaryType.PermitBatchTransferFrom:
case PrimaryType.PermitTransferFrom:
tokenDetails = message?.permitted;
break;
default:
break;
}

const isNonArrayObject = tokenDetails && !Array.isArray(tokenDetails);

const PermitSimulation: React.FC<{
tokenDecimals: number;
}> = ({ tokenDecimals }) => {
return isNonArrayObject ? [tokenDetails] : tokenDetails;
}

const PermitSimulation: React.FC<object> = () => {
const t = useI18nContext();
const currentConfirmation = useSelector(
currentConfirmationSelector,
) as SignatureRequestType;

const msgData = currentConfirmation.msgParams?.data;
const {
domain: { verifyingContract },
message: { value },
} = parseTypedDataMessage(currentConfirmation.msgParams?.data as string);

const exchangeRate = useTokenExchangeRate(verifyingContract);

const fiatValue = useMemo(() => {
if (exchangeRate && value) {
return exchangeRate.times(new Numeric(value, 10)).toNumber();
}
return undefined;
}, [exchangeRate, value]);

const { tokenValue, tokenValueMaxPrecision } = useMemo(() => {
if (!value) {
return { tokenValue: null, tokenValueMaxPrecision: null };
}
const tokenAmount = calcTokenAmount(value, tokenDecimals);
message,
primaryType,
} = parseTypedDataMessage(msgData as string);

return {
tokenValue: formatAmount('en-US', tokenAmount),
tokenValueMaxPrecision: formatAmountMaxPrecision('en-US', tokenAmount),
};
}, [tokenDecimals, value]);
const tokenDetails = extractTokenDetailsByPrimaryType(message, primaryType);

return (
<ConfirmInfoSection data-testid="confirmation__simulation_section">
Expand All @@ -75,42 +67,32 @@ const PermitSimulation: React.FC<{
</ConfirmInfoRow>
<ConfirmInfoRow label={t('spendingCap')}>
<Box style={{ marginLeft: 'auto', maxWidth: '100%' }}>
<Box display={Display.Flex}>
{Array.isArray(tokenDetails) ? (
<Box
display={Display.Inline}
marginInlineEnd={1}
minWidth={BlockSize.Zero}
display={Display.Flex}
flexDirection={FlexDirection.Column}
gap={2}
>
<Tooltip
position="bottom"
title={tokenValueMaxPrecision}
wrapperStyle={{ minWidth: 0 }}
interactive
>
<Text
data-testid="simulation-token-value"
backgroundColor={BackgroundColor.backgroundAlternative}
borderRadius={BorderRadius.XL}
paddingInline={2}
style={{ paddingTop: '1px', paddingBottom: '1px' }}
textAlign={TextAlign.Center}
>
{shortenString(tokenValue || '', {
truncatedCharLimit: 15,
truncatedStartChars: 15,
truncatedEndChars: 0,
skipCharacterInEnd: true,
})}
</Text>
</Tooltip>
{tokenDetails.map(
(
{ token, amount }: { token: string; amount: string },
i: number,
) => (
<PermitSimulationValueDisplay
key={`${token}-${i}`}
primaryType={primaryType}
tokenContract={token}
value={amount}
/>
),
)}
</Box>
<Name value={verifyingContract} type={NameType.ETHEREUM_ADDRESS} />
</Box>
<Box>
{fiatValue && (
<IndividualFiatDisplay fiatAmount={fiatValue} shorten />
)}
</Box>
) : (
<PermitSimulationValueDisplay
tokenContract={verifyingContract}
value={message.value}
/>
)}
</Box>
</ConfirmInfoRow>
</ConfirmInfoSection>
Expand Down
Loading

0 comments on commit 3fde91a

Please sign in to comment.