Skip to content

Commit

Permalink
feat: NFT permit simulations (#27825)
Browse files Browse the repository at this point in the history
## **Description**

Add simulation section to NFT permit

## **Related issues**

Fixes: #27394

## **Manual testing steps**

1. Submit NFT permit signature request
2. Check simulation section on the confirmation page

## **Screenshots/Recordings**
<img width="355" alt="Screenshot 2024-10-14 at 5 40 21 PM"
src="https://github.com/user-attachments/assets/53bde7f8-56aa-4f84-b748-dfa08d0bcef2">

## **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/develop/.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
- [ ] 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/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
jpuri authored Oct 17, 2024
1 parent 327a260 commit fac4422
Show file tree
Hide file tree
Showing 6 changed files with 230 additions and 17 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -127,3 +127,120 @@ exports[`PermitSimulation renders component correctly 1`] = `
</div>
</div>
`;

exports[`PermitSimulation renders correctly for NFT permit 1`] = `
<div>
<div
class="mm-box mm-box--margin-bottom-4 mm-box--padding-2 mm-box--background-color-background-default mm-box--rounded-md"
data-testid="confirmation__simulation_section"
>
<div
class="mm-box confirm-info-row mm-box--margin-top-2 mm-box--margin-bottom-2 mm-box--padding-right-2 mm-box--padding-left-2 mm-box--display-flex mm-box--flex-direction-row mm-box--flex-wrap-wrap mm-box--justify-content-space-between mm-box--align-items-center mm-box--color-text-default mm-box--rounded-lg"
style="overflow-wrap: anywhere; min-height: 24px; position: relative;"
>
<div
class="mm-box mm-box--display-flex mm-box--flex-direction-row mm-box--justify-content-center mm-box--align-items-flex-start"
>
<div
class="mm-box mm-box--display-flex mm-box--align-items-center"
>
<p
class="mm-box mm-text mm-text--body-md-medium mm-box--color-inherit"
>
Estimated changes
</p>
<div>
<div
aria-describedby="tippy-tooltip-3"
class=""
data-original-title="Estimated changes are what might happen if you go through with this transaction. This is just a prediction, not a guarantee."
data-tooltipped=""
style="display: flex;"
tabindex="0"
>
<span
class="mm-box mm-icon mm-icon--size-sm mm-box--margin-left-1 mm-box--display-inline-block mm-box--color-icon-muted"
style="mask-image: url('./images/icons/question.svg');"
/>
</div>
</div>
</div>
</div>
<div
class="mm-box mm-box--display-flex mm-box--gap-2 mm-box--flex-wrap-wrap mm-box--align-items-center mm-box--min-width-0"
>
<p
class="mm-box mm-text mm-text--body-md mm-box--color-inherit"
style="white-space: pre-wrap;"
>
You're giving someone else permission to withdraw NFTs from your account.
</p>
</div>
</div>
<div
class="mm-box confirm-info-row mm-box--margin-top-2 mm-box--margin-bottom-2 mm-box--padding-right-2 mm-box--padding-left-2 mm-box--display-flex mm-box--flex-direction-row mm-box--flex-wrap-wrap mm-box--justify-content-space-between mm-box--align-items-center mm-box--color-text-default mm-box--rounded-lg"
style="overflow-wrap: anywhere; min-height: 24px; position: relative;"
>
<div
class="mm-box mm-box--display-flex mm-box--flex-direction-row mm-box--justify-content-center mm-box--align-items-flex-start"
>
<div
class="mm-box mm-box--display-flex mm-box--align-items-center"
>
<p
class="mm-box mm-text mm-text--body-md-medium mm-box--color-inherit"
>
Withdraw
</p>
</div>
</div>
<div
class="mm-box"
style="margin-left: auto; max-width: 100%;"
>
<div
class="mm-box"
>
<div
class="mm-box mm-box--display-flex mm-box--justify-content-flex-end"
>
<div
class="mm-box mm-box--margin-inline-end-1 mm-box--display-inline mm-box--min-width-0"
>
<div>
<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;"
>
#3606393
</p>
</div>
</div>
<div
class="mm-box mm-box--display-flex"
>
<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"
>
0xC3644...1FE88
</p>
</div>
</div>
</div>
<div
class="mm-box"
/>
</div>
</div>
</div>
</div>
</div>
`;
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,10 @@ import { act } from 'react-dom/test-utils';

import { getMockTypedSignConfirmStateForRequest } from '../../../../../../../../test/data/confirmations/helper';
import { renderWithConfirmContextProvider } from '../../../../../../../../test/lib/confirmations/render-helpers';
import { permitSignatureMsg } from '../../../../../../../../test/data/confirmations/typed_sign';
import {
permitNFTSignatureMsg,
permitSignatureMsg,
} from '../../../../../../../../test/data/confirmations/typed_sign';
import PermitSimulation from './permit-simulation';

jest.mock('../../../../../../../store/actions', () => {
Expand All @@ -28,4 +31,20 @@ describe('PermitSimulation', () => {
expect(container).toMatchSnapshot();
});
});

it('renders correctly for NFT permit', async () => {
const state = getMockTypedSignConfirmStateForRequest(permitNFTSignatureMsg);
const mockStore = configureMockStore([])(state);

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

expect(await findByText('Withdraw')).toBeInTheDocument();
expect(await findByText('#3606393')).toBeInTheDocument();
expect(container).toMatchSnapshot();
});
});
});
Original file line number Diff line number Diff line change
Expand Up @@ -45,8 +45,10 @@ const PermitSimulation: React.FC<object> = () => {
const {
domain: { verifyingContract },
message,
message: { tokenId },
primaryType,
} = parseTypedDataMessage(msgData as string);
const isNFT = tokenId !== undefined;

const tokenDetails = extractTokenDetailsByPrimaryType(message, primaryType);

Expand All @@ -68,7 +70,9 @@ const PermitSimulation: React.FC<object> = () => {
);

const SpendingCapRow = (
<ConfirmInfoRow label={t('spendingCap')}>
<ConfirmInfoRow
label={t(isNFT ? 'simulationApproveHeading' : 'spendingCap')}
>
<Box style={{ marginLeft: 'auto', maxWidth: '100%' }}>
{Array.isArray(tokenDetails) ? (
<Box
Expand All @@ -89,6 +93,7 @@ const PermitSimulation: React.FC<object> = () => {
<PermitSimulationValueDisplay
tokenContract={verifyingContract}
value={message.value}
tokenId={message.tokenId}
/>
)}
</Box>
Expand All @@ -99,7 +104,9 @@ const PermitSimulation: React.FC<object> = () => {
<StaticSimulation
title={t('simulationDetailsTitle')}
titleTooltip={t('simulationDetailsTitleTooltip')}
description={t('permitSimulationDetailInfo')}
description={t(
isNFT ? 'simulationDetailsApproveDesc' : 'permitSimulationDetailInfo',
)}
simulationElements={SpendingCapRow}
/>
);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -56,3 +56,49 @@ exports[`PermitSimulationValueDisplay renders component correctly 1`] = `
</div>
</div>
`;

exports[`PermitSimulationValueDisplay renders component correctly for NFT token 1`] = `
<div>
<div
class="mm-box"
>
<div
class="mm-box mm-box--display-flex mm-box--justify-content-flex-end"
>
<div
class="mm-box mm-box--margin-inline-end-1 mm-box--display-inline mm-box--min-width-0"
>
<div>
<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;"
>
#4321
</p>
</div>
</div>
<div
class="mm-box mm-box--display-flex"
>
<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"
>
0xA0b86...6eB48
</p>
</div>
</div>
</div>
<div
class="mm-box"
/>
</div>
</div>
`;
Original file line number Diff line number Diff line change
Expand Up @@ -29,4 +29,21 @@ describe('PermitSimulationValueDisplay', () => {
expect(container).toMatchSnapshot();
});
});

it('renders component correctly for NFT token', async () => {
const mockStore = configureMockStore([])(mockState);

await act(async () => {
const { container, findByText } = renderWithProvider(
<PermitSimulationValueDisplay
tokenContract="0xa0b86991c6218b36c1d19d4a2e9eb0ce3606eb48"
tokenId="4321"
/>,
mockStore,
);

expect(await findByText('#4321')).toBeInTheDocument();
expect(container).toMatchSnapshot();
});
});
});
Original file line number Diff line number Diff line change
Expand Up @@ -41,29 +41,34 @@ type PermitSimulationValueDisplayParams = {
tokenContract: Hex | string;

/** The token amount */
value: number | string;
value?: number | string;

/** The tokenId for NFT */
tokenId?: string;
};

const PermitSimulationValueDisplay: React.FC<
PermitSimulationValueDisplayParams
> = ({ primaryType, tokenContract, value }) => {
> = ({ primaryType, tokenContract, value, tokenId }) => {
const exchangeRate = useTokenExchangeRate(tokenContract);

const { value: tokenDecimals } = useAsyncResult(
async () => await fetchErc20Decimals(tokenContract),
[tokenContract],
);
const { value: tokenDecimals } = useAsyncResult(async () => {
if (tokenId) {
return undefined;
}
return await fetchErc20Decimals(tokenContract);
}, [tokenContract]);

const fiatValue = useMemo(() => {
if (exchangeRate && value) {
if (exchangeRate && value && !tokenId) {
const tokenAmount = calcTokenAmount(value, tokenDecimals);
return exchangeRate.times(tokenAmount).toNumber();
}
return undefined;
}, [exchangeRate, tokenDecimals, value]);

const { tokenValue, tokenValueMaxPrecision } = useMemo(() => {
if (!value) {
if (!value || tokenId) {
return { tokenValue: null, tokenValueMaxPrecision: null };
}

Expand Down Expand Up @@ -107,12 +112,14 @@ const PermitSimulationValueDisplay: React.FC<
style={{ paddingTop: '1px', paddingBottom: '1px' }}
textAlign={TextAlign.Center}
>
{shortenString(tokenValue || '', {
truncatedCharLimit: 15,
truncatedStartChars: 15,
truncatedEndChars: 0,
skipCharacterInEnd: true,
})}
{tokenValue !== null &&
shortenString(tokenValue || '', {
truncatedCharLimit: 15,
truncatedStartChars: 15,
truncatedEndChars: 0,
skipCharacterInEnd: true,
})}
{tokenId && `#${tokenId}`}
</Text>
</Tooltip>
</Box>
Expand Down

0 comments on commit fac4422

Please sign in to comment.