1
0
mirror of https://github.com/kremalicious/metamask-extension.git synced 2024-11-22 01:47:00 +01:00

Extend wallet_watchAsset to support ERC721 and ERC1155 tokens (#19454)

* Extend wallet_watchAsset to support ERC721 and ERC1155 tokens
This commit is contained in:
Alex Donesky 2023-06-15 15:18:12 -05:00 committed by GitHub
parent 8b3e3c8a58
commit c16b35c029
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
34 changed files with 1508 additions and 295 deletions

View File

@ -1,77 +1,121 @@
import { ApprovalType } from '@metamask/controller-utils';
const suggestedAssets = [
{
export const pendingTokenApprovals = {
1: {
id: 1,
type: ApprovalType.WatchAsset,
requestData: {
asset: {
address: '0x6b175474e89094c44da98b954eedeac495271d0f',
symbol: 'ETH',
decimals: 18,
image: './images/eth_logo.png',
unlisted: false,
},
{
},
},
2: {
id: 2,
type: ApprovalType.WatchAsset,
requestData: {
asset: {
address: '0xB8c77482e45F1F44dE1745F52C74426C631bDD52',
symbol: '0X',
decimals: 18,
image: '0x.svg',
unlisted: false,
},
{
},
},
3: {
id: 3,
type: ApprovalType.WatchAsset,
requestData: {
asset: {
address: '0x1f9840a85d5af5bf1d1762f925bdaddc4201f984',
symbol: 'AST',
decimals: 18,
image: 'ast.png',
unlisted: false,
},
{
},
},
4: {
id: 4,
type: ApprovalType.WatchAsset,
requestData: {
asset: {
address: '0x9f8f72aa9304c8b593d555f12ef6589cc3a579a2',
symbol: 'BAT',
decimals: 18,
image: 'BAT_icon.svg',
unlisted: false,
},
{
},
},
5: {
id: 5,
type: ApprovalType.WatchAsset,
requestData: {
asset: {
address: '0xe83cccfabd4ed148903bf36d4283ee7c8b3494d1',
symbol: 'CVL',
decimals: 18,
image: 'CVL_token.svg',
unlisted: false,
},
{
},
},
6: {
id: 6,
type: ApprovalType.WatchAsset,
requestData: {
asset: {
address: '0x0bc529c00C6401aEF6D220BE8C6Ea1667F6Ad93e',
symbol: 'GLA',
decimals: 18,
image: 'gladius.svg',
unlisted: false,
},
{
},
},
7: {
id: 7,
type: ApprovalType.WatchAsset,
requestData: {
asset: {
address: '0x467Bccd9d29f223BcE8043b84E8C8B282827790F',
symbol: 'GNO',
decimals: 18,
image: 'gnosis.svg',
unlisted: false,
},
{
},
},
8: {
id: 8,
type: ApprovalType.WatchAsset,
requestData: {
asset: {
address: '0xff20817765cb7f73d4bde2e66e067e58d11095c2',
symbol: 'OMG',
decimals: 18,
image: 'omg.jpg',
unlisted: false,
},
{
},
},
9: {
id: 9,
type: ApprovalType.WatchAsset,
requestData: {
asset: {
address: '0x8e870d67f660d95d5be530380d0ec0bd388289e1',
symbol: 'WED',
decimals: 18,
image: 'wed.png',
unlisted: false,
},
];
export const pendingAssetApprovals = suggestedAssets.map((asset, index) => {
return {
type: ApprovalType.WatchAsset,
requestData: {
id: index,
asset,
},
},
};
});

View File

@ -265,6 +265,15 @@
"addNewToken": {
"message": "Add new token"
},
"addNft": {
"message": "Add NFT"
},
"addNfts": {
"message": "Add NFTs"
},
"addSuggestedNFTs": {
"message": "Add suggested NFTs"
},
"addSuggestedTokens": {
"message": "Add suggested tokens"
},
@ -5108,6 +5117,10 @@
"wantToAddThisNetwork": {
"message": "Want to add this network?"
},
"wantsToAddThisAsset": {
"message": "$1 wants to add this asset to your wallet",
"description": "$1 is the name of the website that wants to add an asset to your wallet"
},
"warning": {
"message": "Warning"
},

View File

@ -35,8 +35,11 @@ async function watchAssetHandler(
{ handleWatchAssetRequest },
) {
try {
const { options: asset, type } = req.params;
await handleWatchAssetRequest(asset, type);
const {
params: { options: asset, type },
origin,
} = req;
await handleWatchAssetRequest(asset, type, origin);
res.result = true;
return end();
} catch (error) {

View File

@ -80,7 +80,12 @@ import { SignatureController } from '@metamask/signature-controller';
import { DesktopController } from '@metamask/desktop/dist/controllers/desktop';
///: END:ONLY_INCLUDE_IN
import { ApprovalType } from '@metamask/controller-utils';
import {
ApprovalType,
ERC1155,
ERC20,
ERC721,
} from '@metamask/controller-utils';
///: BEGIN:ONLY_INCLUDE_IN(build-mmi)
import { toChecksumHexAddress } from '../../shared/modules/hexstring-utils';
@ -401,7 +406,7 @@ export default class MetamaskController extends EventEmitter {
const nftControllerMessenger = this.controllerMessenger.getRestricted({
name: 'NftController',
allowedActions: ['ApprovalController:addRequest'],
allowedActions: [`${this.approvalController.name}:addRequest`],
});
this.nftController = new NftController(
{
@ -3449,6 +3454,18 @@ export default class MetamaskController extends EventEmitter {
});
}
handleWatchAssetRequest = (asset, type, origin) => {
switch (type) {
case ERC20:
return this.tokensController.watchAsset(asset, type);
case ERC721:
case ERC1155:
return this.nftController.watchNft(asset, type, origin);
default:
throw new Error(`Asset type ${type} not supported`);
}
};
//=============================================================================
// PASSWORD MANAGEMENT
//=============================================================================
@ -3828,9 +3845,7 @@ export default class MetamaskController extends EventEmitter {
getUnlockPromise: this.appStateController.getUnlockPromise.bind(
this.appStateController,
),
handleWatchAssetRequest: this.tokensController.watchAsset.bind(
this.tokensController,
),
handleWatchAssetRequest: this.handleWatchAssetRequest.bind(this),
requestUserApproval:
this.approvalController.addAndShowApprovalRequest.bind(
this.approvalController,

View File

@ -386,7 +386,7 @@
"@metamask/eslint-config-typescript": "^9.0.1",
"@metamask/forwarder": "^1.1.0",
"@metamask/phishing-warning": "^2.1.0",
"@metamask/test-dapp": "^6.0.0",
"@metamask/test-dapp": "^7.0.0",
"@sentry/cli": "^1.58.0",
"@storybook/addon-a11y": "^7.0.11",
"@storybook/addon-actions": "^7.0.11",

View File

@ -561,7 +561,7 @@ class FixtureBuilder {
[toHex(1337)]: [
{
address: `__FIXTURE_SUBSTITUTION__CONTRACT${SMART_CONTRACTS.NFTS}`,
name: 'TestDappCollectibles',
name: 'TestDappNFTs',
symbol: 'TDC',
},
],
@ -572,12 +572,12 @@ class FixtureBuilder {
[toHex(1337)]: [
{
address: `__FIXTURE_SUBSTITUTION__CONTRACT${SMART_CONTRACTS.NFTS}`,
description: 'Test Dapp Collectibles for testing.',
description: 'Test Dapp NFTs for testing.',
favorite: false,
image:
'data:image/svg+xml;base64,PHN2ZyBoZWlnaHQ9IjM1MCIgd2lkdGg9IjM1MCIgdmlld0JveD0iMCAwIDEwMCAxMDAiIHhtbG5zPSJodHRwOi8vd3d3LnczLm9yZy8yMDAwL3N2ZyI+PGRlZnM+PHBhdGggaWQ9Ik15UGF0aCIgZmlsbD0ibm9uZSIgc3Ryb2tlPSJyZWQiIGQ9Ik0xMCw5MCBROTAsOTAgOTAsNDUgUTkwLDEwIDUwLDEwIFExMCwxMCAxMCw0MCBRMTAsNzAgNDUsNzAgUTcwLDcwIDc1LDUwIiAvPjwvZGVmcz48dGV4dD48dGV4dFBhdGggaHJlZj0iI015UGF0aCI+UXVpY2sgYnJvd24gZm94IGp1bXBzIG92ZXIgdGhlIGxhenkgZG9nLjwvdGV4dFBhdGg+PC90ZXh0Pjwvc3ZnPg==',
isCurrentlyOwned: true,
name: 'Test Dapp Collectibles #1',
name: 'Test Dapp NFTs #1',
standard: 'ERC721',
tokenId: '1',
},

View File

@ -15,6 +15,214 @@ describe('ERC721 NFTs testdapp interaction', function () {
],
};
it('should prompt users to add their NFTs to their wallet (one by one)', async function () {
await withFixtures(
{
dapp: true,
fixtures: new FixtureBuilder()
.withPermissionControllerConnectedToTestDapp()
.build(),
ganacheOptions,
smartContract,
title: this.test.title,
failOnConsoleError: false,
},
async ({ driver, _, contractRegistry }) => {
const contract = contractRegistry.getContractAddress(smartContract);
await driver.navigate();
await driver.fill('#password', 'correct horse battery staple');
await driver.press('#password', driver.Key.ENTER);
// Open Dapp and wait for deployed contract
await openDapp(driver, contract);
await driver.findClickableElement('#deployButton');
// mint NFT
await driver.fill('#mintAmountInput', '5');
await driver.clickElement({ text: 'Mint', tag: 'button' });
// Notification
await driver.waitUntilXWindowHandles(3);
let windowHandles = await driver.getAllWindowHandles();
const [extension] = windowHandles;
await driver.switchToWindowWithTitle(
'MetaMask Notification',
windowHandles,
);
await driver.waitForSelector({
css: '.confirm-page-container-summary__action__name',
text: 'Deposit',
});
await driver.clickElement({ text: 'Confirm', tag: 'button' });
await driver.waitUntilXWindowHandles(2);
await driver.switchToWindow(extension);
await driver.clickElement('[data-testid="home__activity-tab"]');
const transactionItem = await driver.waitForSelector({
css: '.list-item__title',
text: 'Deposit',
});
assert.equal(await transactionItem.isDisplayed(), true);
// verify the mint transaction has finished
await driver.switchToWindowWithTitle('E2E Test Dapp', windowHandles);
const nftsMintStatus = await driver.findElement({
css: '#nftsStatus',
text: 'Mint completed',
});
assert.equal(await nftsMintStatus.isDisplayed(), true);
// watch 3 of the nfts
await driver.clickElement({ text: 'Watch NFT 1', tag: 'button' });
await driver.clickElement({ text: 'Watch NFT 2', tag: 'button' });
await driver.clickElement({ text: 'Watch NFT 3', tag: 'button' });
await driver.waitUntilXWindowHandles(3);
windowHandles = await driver.getAllWindowHandles();
await driver.switchToWindowWithTitle(
'MetaMask Notification',
windowHandles,
);
// confirm watchNFT
await driver.waitForSelector({
css: '.mm-text--heading-lg',
text: 'Add suggested NFTs',
});
await driver.clickElement({ text: 'Add NFTs', tag: 'button' });
await driver.switchToWindow(extension);
await driver.clickElement({ text: 'NFTs', tag: 'button' });
await driver.findElement({ text: 'TestDappNFTs (3)' });
const nftsListItemsFirstCheck = await driver.findElements(
'.nft-item__item',
);
assert.equal(nftsListItemsFirstCheck.length, 3);
await driver.switchToWindowWithTitle('E2E Test Dapp', windowHandles);
await driver.clickElement({ text: 'Watch NFT 4', tag: 'button' });
await driver.clickElement({ text: 'Watch NFT 5', tag: 'button' });
await driver.clickElement({ text: 'Watch NFT 6', tag: 'button' });
await driver.waitUntilXWindowHandles(3);
windowHandles = await driver.getAllWindowHandles();
await driver.switchToWindowWithTitle(
'MetaMask Notification',
windowHandles,
);
// confirm watchNFT
await driver.waitForSelector({
css: '.mm-text--heading-lg',
text: 'Add suggested NFTs',
});
await driver.clickElement({ text: 'Add NFTs', tag: 'button' });
await driver.switchToWindow(extension);
await driver.clickElement({ text: 'NFTs', tag: 'button' });
await driver.findElement({ text: 'TestDappNFTs (6)' });
const nftsListItemsSecondCheck = await driver.findElements(
'.nft-item__item',
);
assert.equal(nftsListItemsSecondCheck.length, 6);
},
);
});
it('should prompt users to add their NFTs to their wallet (all at once)', async function () {
await withFixtures(
{
dapp: true,
fixtures: new FixtureBuilder()
.withPermissionControllerConnectedToTestDapp()
.build(),
ganacheOptions,
smartContract,
title: this.test.title,
failOnConsoleError: false,
},
async ({ driver, _, contractRegistry }) => {
const contract = contractRegistry.getContractAddress(smartContract);
await driver.navigate();
await driver.fill('#password', 'correct horse battery staple');
await driver.press('#password', driver.Key.ENTER);
// Open Dapp and wait for deployed contract
await openDapp(driver, contract);
await driver.findClickableElement('#deployButton');
// mint NFT
await driver.fill('#mintAmountInput', '5');
await driver.clickElement({ text: 'Mint', tag: 'button' });
// Notification
await driver.waitUntilXWindowHandles(3);
let windowHandles = await driver.getAllWindowHandles();
const [extension] = windowHandles;
await driver.switchToWindowWithTitle(
'MetaMask Notification',
windowHandles,
);
await driver.waitForSelector({
css: '.confirm-page-container-summary__action__name',
text: 'Deposit',
});
await driver.clickElement({ text: 'Confirm', tag: 'button' });
await driver.waitUntilXWindowHandles(2);
await driver.switchToWindow(extension);
await driver.clickElement('[data-testid="home__activity-tab"]');
const transactionItem = await driver.waitForSelector({
css: '.list-item__title',
text: 'Deposit',
});
assert.equal(await transactionItem.isDisplayed(), true);
// verify the mint transaction has finished
await driver.switchToWindowWithTitle('E2E Test Dapp', windowHandles);
const nftsMintStatus = await driver.findElement({
css: '#nftsStatus',
text: 'Mint completed',
});
assert.equal(await nftsMintStatus.isDisplayed(), true);
// watch all nfts
await driver.clickElement({ text: 'Watch all NFTs', tag: 'button' });
await driver.waitUntilXWindowHandles(3);
windowHandles = await driver.getAllWindowHandles();
await driver.switchToWindowWithTitle(
'MetaMask Notification',
windowHandles,
);
// confirm watchNFT
await driver.waitForSelector({
css: '.mm-text--heading-lg',
text: 'Add suggested NFTs',
});
await driver.findElements('.confirm-add-suggested-nft__nft-list-item');
const suggestedNftListItems = await driver.findElements(
'.confirm-add-suggested-nft__nft-list-item',
);
// there are 6 nfts to add because one is minted as part of the fixture
assert.equal(suggestedNftListItems.length, 6);
// remove one nft from the list
const removeButtons = await driver.findElements(
'.confirm-add-suggested-nft__nft-remove',
);
await removeButtons[0].click();
await driver.clickElement({ text: 'Add NFTs', tag: 'button' });
await driver.switchToWindow(extension);
await driver.clickElement({ text: 'NFTs', tag: 'button' });
await driver.findElement({ text: 'TestDappNFTs (5)' });
const nftsListItemsSecondCheck = await driver.findElements(
'.nft-item__item',
);
assert.equal(nftsListItemsSecondCheck.length, 5);
},
);
});
it('should transfer a single ERC721 NFT from one account to another', async function () {
await withFixtures(
{
@ -51,7 +259,7 @@ describe('ERC721 NFTs testdapp interaction', function () {
// Confirm transfer
await driver.waitForSelector({
css: '.mm-text--heading-md',
text: 'TestDappCollectibles',
text: 'TestDappNFTs',
});
await driver.clickElement({ text: 'Confirm', tag: 'button' });
await driver.waitUntilXWindowHandles(2);
@ -62,7 +270,7 @@ describe('ERC721 NFTs testdapp interaction', function () {
);
// Verify transaction
await driver.findElement({ text: 'Send TDC' });
await driver.findElement({ text: 'Send TDN' });
},
);
});
@ -116,7 +324,7 @@ describe('ERC721 NFTs testdapp interaction', function () {
);
assert.equal(
await title.getText(),
'Allow access to and transfer of your TestDappCollectibles (#1)?',
'Allow access to and transfer of your TestDappNFTs (#1)?',
);
assert.equal(await func.getText(), 'Function: Approve');
@ -132,7 +340,7 @@ describe('ERC721 NFTs testdapp interaction', function () {
// Verify transaction
const completedTx = await driver.waitForSelector({
css: '.list-item__title',
text: 'Approve TDC spending cap',
text: 'Approve TDN spending cap',
});
assert.equal(await completedTx.isDisplayed(), true);
},
@ -184,7 +392,7 @@ describe('ERC721 NFTs testdapp interaction', function () {
);
assert.equal(
await title.getText(),
'Allow access to and transfer of all your TestDappCollectibles?',
'Allow access to and transfer of all your TestDappNFTs?',
);
assert.equal(await func.getText(), 'Function: SetApprovalForAll');
assert.equal(await params.getText(), 'Parameters: true');
@ -203,7 +411,7 @@ describe('ERC721 NFTs testdapp interaction', function () {
// Verify transaction
const completedTx = await driver.waitForSelector({
css: '.list-item__title',
text: 'Approve TDC with no spend limit',
text: 'Approve TDN with no spend limit',
});
assert.equal(await completedTx.isDisplayed(), true);
},
@ -258,7 +466,7 @@ describe('ERC721 NFTs testdapp interaction', function () {
);
assert.equal(
await title.getText(),
'Revoke permission to access and transfer all of your TestDappCollectibles?',
'Revoke permission to access and transfer all of your TestDappNFTs?',
);
assert.equal(await func.getText(), 'Function: SetApprovalForAll');
assert.equal(await params.getText(), 'Parameters: false');
@ -277,7 +485,7 @@ describe('ERC721 NFTs testdapp interaction', function () {
// Verify transaction
const completedTx = await driver.waitForSelector({
css: '.list-item__title',
text: 'Approve TDC with no spend limit',
text: 'Approve TDN with no spend limit',
});
assert.equal(await completedTx.isDisplayed(), true);
},

View File

@ -51,7 +51,7 @@ describe('Import NFT', function () {
// Check the imported NFT and its image are displayed in the NFT tab
const importedNft = await driver.waitForSelector({
css: 'h5',
text: 'TestDappCollectibles',
text: 'TestDappNFTs',
});
const importedNftImage = await driver.findElement(
'.nft-item__item-image',

View File

@ -71,7 +71,7 @@ describe('Send NFT', function () {
const sendNftItem = await driver.findElement({
css: 'h2',
text: 'Send Test Dapp Collectibles',
text: 'Send Test Dapp NFTs',
});
assert.equal(await sendNftItem.isDisplayed(), true);

View File

@ -38,19 +38,19 @@ describe('View NFT details', function () {
const detailsPageTitle = await driver.findElement('.asset-breadcrumb');
assert.equal(
await detailsPageTitle.getText(),
'Account 1 / TestDappCollectibles',
'Account 1 / TestDappNFTs',
);
// Check the displayed NFT details
const nftName = await driver.findElement('.nft-details__info h4');
assert.equal(await nftName.getText(), 'Test Dapp Collectibles #1');
assert.equal(await nftName.getText(), 'Test Dapp NFTs #1');
const nftDescription = await driver.findElement(
'.nft-details__info h6:nth-of-type(2)',
);
assert.equal(
await nftDescription.getText(),
'Test Dapp Collectibles for testing.',
'Test Dapp NFTs for testing.',
);
const nftImage = await driver.findElement('.nft-item__item-image');

View File

@ -45,7 +45,7 @@ class GanacheSeeder {
await contract.deployTransaction.wait();
if (contractName === SMART_CONTRACTS.NFTS) {
const transaction = await contract.mintCollectibles(1, {
const transaction = await contract.mintNFTs(1, {
from: fromAddress,
});
await transaction.wait();

View File

@ -3,8 +3,8 @@ const {
hstAbi,
piggybankBytecode,
piggybankAbi,
collectiblesAbi,
collectiblesBytecode,
nftsAbi,
nftsBytecode,
erc1155Abi,
erc1155Bytecode,
failingContractAbi,
@ -23,8 +23,8 @@ const hstFactory = {
};
const nftsFactory = {
bytecode: collectiblesBytecode,
abi: collectiblesAbi,
bytecode: nftsBytecode,
abi: nftsAbi,
};
const erc1155Factory = {

View File

@ -15,13 +15,18 @@ import { useI18nContext } from '../../../hooks/useI18nContext';
import { Text } from '../../component-library';
import Box from '../../ui/box/box';
export default function NftDefaultImage({ name, tokenId, clickable = false }) {
export default function NftDefaultImage({
name,
tokenId,
className,
clickable = false,
}) {
const t = useI18nContext();
return (
<Box
tabIndex={0}
data-testid="nft-default-image"
className={classnames('nft-default', {
className={classnames(className, 'nft-default', {
'nft-default--clickable': clickable,
})}
display={Display.Flex}
@ -57,4 +62,8 @@ NftDefaultImage.propTypes = {
* Controls the css class for the cursor hover
*/
clickable: PropTypes.bool,
/**
* An additional className to apply to the NFT default image
*/
className: PropTypes.string,
};

View File

@ -26,6 +26,7 @@ const IMPORT_TOKEN_ROUTE = '/import-token';
const CONFIRM_IMPORT_TOKEN_ROUTE = '/confirm-import-token';
const CONFIRM_ADD_SUGGESTED_TOKEN_ROUTE = '/confirm-add-suggested-token';
const NEW_ACCOUNT_ROUTE = '/new-account';
const CONFIRM_ADD_SUGGESTED_NFT_ROUTE = '/confirm-add-suggested-nft';
const CONNECT_HARDWARE_ROUTE = '/new-account/connect';
///: BEGIN:ONLY_INCLUDE_IN(build-mmi)
const CUSTODY_ACCOUNT_ROUTE = '/new-account/custody';
@ -131,6 +132,7 @@ const PATH_NAME_MAP = {
[CONFIRM_IMPORT_TOKEN_ROUTE]: 'Confirm Import Token Page',
[CONFIRM_ADD_SUGGESTED_TOKEN_ROUTE]: 'Confirm Add Suggested Token Page',
[NEW_ACCOUNT_ROUTE]: 'New Account Page',
[CONFIRM_ADD_SUGGESTED_NFT_ROUTE]: 'Confirm Add Suggested NFT Page',
[CONNECT_HARDWARE_ROUTE]: 'Connect Hardware Wallet Page',
///: BEGIN:ONLY_INCLUDE_IN(build-mmi)
[INSTITUTIONAL_FEATURES_DONE_ROUTE]: 'Institutional Features Done Page',
@ -197,6 +199,7 @@ export {
CONFIRM_IMPORT_TOKEN_ROUTE,
CONFIRM_ADD_SUGGESTED_TOKEN_ROUTE,
NEW_ACCOUNT_ROUTE,
CONFIRM_ADD_SUGGESTED_NFT_ROUTE,
CONNECT_HARDWARE_ROUTE,
SEND_ROUTE,
TOKEN_DETAILS,

View File

@ -492,11 +492,11 @@ export const sanitizeMessage = (msg, primaryType, types) => {
};
export function getAssetImageURL(image, ipfsGateway) {
if (!image || !ipfsGateway || typeof image !== 'string') {
if (!image || typeof image !== 'string') {
return '';
}
if (image.startsWith('ipfs://')) {
if (ipfsGateway && image.startsWith('ipfs://')) {
return getFormattedIpfsUrl(ipfsGateway, image, true);
}
return image;

View File

@ -0,0 +1,343 @@
import React, { useCallback, useContext, useEffect } from 'react';
import { useDispatch, useSelector } from 'react-redux';
import { useHistory } from 'react-router-dom';
import { ethErrors, serializeError } from 'eth-rpc-errors';
import { getTokenTrackerLink } from '@metamask/etherscan-link';
import classnames from 'classnames';
import { PageContainerFooter } from '../../components/ui/page-container';
import { I18nContext } from '../../contexts/i18n';
import { MetaMetricsContext } from '../../contexts/metametrics';
import { getMostRecentOverviewPage } from '../../ducks/history/history';
import {
resolvePendingApproval,
rejectPendingApproval,
} from '../../store/actions';
import {
MetaMetricsEventCategory,
MetaMetricsEventName,
MetaMetricsTokenEventSource,
} from '../../../shared/constants/metametrics';
import { AssetType } from '../../../shared/constants/transaction';
import {
BUTTON_SIZES,
ButtonIcon,
ButtonIconSize,
ButtonLink,
IconName,
Text,
Box,
} from '../../components/component-library';
import {
getCurrentChainId,
getRpcPrefsForCurrentProvider,
getSuggestedNfts,
getIpfsGateway,
} from '../../selectors';
import NftDefaultImage from '../../components/app/nft-default-image/nft-default-image';
import { getAssetImageURL, shortenAddress } from '../../helpers/utils/util';
import {
AlignItems,
BorderRadius,
Display,
FlexDirection,
FlexWrap,
IconColor,
JustifyContent,
TextAlign,
TextVariant,
BlockSize,
} from '../../helpers/constants/design-system';
const ConfirmAddSuggestedNFT = () => {
const t = useContext(I18nContext);
const dispatch = useDispatch();
const history = useHistory();
const mostRecentOverviewPage = useSelector(getMostRecentOverviewPage);
const suggestedNfts = useSelector(getSuggestedNfts);
const rpcPrefs = useSelector(getRpcPrefsForCurrentProvider);
const chainId = useSelector(getCurrentChainId);
const ipfsGateway = useSelector(getIpfsGateway);
const trackEvent = useContext(MetaMetricsContext);
const handleAddNftsClick = useCallback(async () => {
await Promise.all(
suggestedNfts.map(async ({ requestData: { asset }, id }) => {
await dispatch(resolvePendingApproval(id, null));
trackEvent({
event: MetaMetricsEventName.NftAdded,
category: MetaMetricsEventCategory.Wallet,
sensitiveProperties: {
token_symbol: asset.symbol,
token_id: asset.tokenId,
token_contract_address: asset.address,
source_connection_method: MetaMetricsTokenEventSource.Dapp,
token_standard: asset.standard,
asset_type: AssetType.NFT,
},
});
}),
);
history.push(mostRecentOverviewPage);
}, [dispatch, history, trackEvent, mostRecentOverviewPage, suggestedNfts]);
const handleCancelNftClick = useCallback(async () => {
await Promise.all(
suggestedNfts.map(async ({ id }) => {
return dispatch(
rejectPendingApproval(
id,
serializeError(ethErrors.provider.userRejectedRequest()),
),
);
}),
);
history.push(mostRecentOverviewPage);
}, [dispatch, history, mostRecentOverviewPage, suggestedNfts]);
useEffect(() => {
const goBackIfNoSuggestedNftsOnFirstRender = () => {
if (!suggestedNfts.length) {
history.push(mostRecentOverviewPage);
}
};
goBackIfNoSuggestedNftsOnFirstRender();
}, [history, mostRecentOverviewPage, suggestedNfts]);
let origin;
if (suggestedNfts.length) {
try {
origin = new URL(suggestedNfts[0].origin)?.host;
} catch {
origin = 'dapp';
}
}
return (
<Box
height={BlockSize.Full}
width={BlockSize.Full}
display={Display.Flex}
flexDirection={FlexDirection.Column}
>
<Box paddingBottom={2} className="confirm-add-suggested-nft__header">
<Text
variant={TextVariant.headingLg}
textAlign={TextAlign.Center}
margin={2}
>
{t('addSuggestedNFTs')}
</Text>
<Text variant={TextVariant.bodyMd} textAlign={TextAlign.Center}>
{t('wantsToAddThisAsset', [
origin === 'dapp' ? (
<Text key={origin} variant={TextVariant.bodyMd} fontWeight="bold">
{origin}
</Text>
) : (
<ButtonLink
key={origin}
size={BUTTON_SIZES.INHERIT}
href={origin}
target="_blank"
>
{origin}
</ButtonLink>
),
])}
</Text>
</Box>
<Box className="confirm-add-suggested-nft__content">
<Box
className="confirm-add-suggested-nft__card"
padding={2}
borderRadius={BorderRadius.MD}
>
<Box
className={classnames({
'confirm-add-suggested-nft__nft-list': suggestedNfts.length > 1,
})}
>
{suggestedNfts.map(
({
id,
requestData: {
asset: { address, tokenId, symbol, image, name },
},
}) => {
const nftImageURL = getAssetImageURL(image, ipfsGateway);
const blockExplorerLink = getTokenTrackerLink(
address,
chainId,
null,
null,
{
blockExplorerUrl: rpcPrefs?.blockExplorerUrl ?? null,
},
);
if (suggestedNfts.length === 1) {
return (
<Box
className="confirm-add-suggested-nft__nft-single"
borderRadius={BorderRadius.MD}
margin={0}
padding={0}
>
{nftImageURL ? (
<img
className="confirm-add-suggested-nft__nft-single-image"
src={nftImageURL}
alt={name || tokenId}
/>
) : (
<NftDefaultImage
className="confirm-add-suggested-nft__nft-single-image-default"
tokenId={tokenId}
name={name || symbol || shortenAddress(address)}
/>
)}
<Box
padding={1}
display={Display.Flex}
flexDirection={FlexDirection.Row}
justifyContent={JustifyContent.spaceBetween}
alignItems={AlignItems.Center}
>
<Box
display={Display.Flex}
flexDirection={FlexDirection.Column}
justifyContent={JustifyContent.spaceEvenly}
flexWrap={FlexWrap.NoWrap}
width={BlockSize.Full}
className="confirm-add-suggested-nft__nft-single-sub-details"
>
{rpcPrefs.blockExplorerUrl ? (
<ButtonLink
className="confirm-add-suggested-nft__nft-name"
href={blockExplorerLink}
title={address}
target="_blank"
size={BUTTON_SIZES.INHERIT}
>
{name || symbol || shortenAddress(address)}
</ButtonLink>
) : (
<Text
variant={TextVariant.bodyMd}
className="confirm-add-suggested-nft__nft-name"
title={address}
>
{name || symbol || shortenAddress(address)}
</Text>
)}
<Text
variant={TextVariant.bodyMd}
className="confirm-add-suggested-nft__nft-tokenId"
>
#{tokenId}
</Text>
</Box>
</Box>
</Box>
);
}
return (
<Box
display={Display.Flex}
flexDirection={FlexDirection.Row}
flexWrap={FlexWrap.NoWrap}
alignItems={AlignItems.Center}
justifyContent={JustifyContent.spaceBetween}
marginBottom={4}
className="confirm-add-suggested-nft__nft-list-item"
key={`${address}-${tokenId}`}
>
<Box
display={Display.Flex}
flexDirection={FlexDirection.Row}
flexWrap={FlexWrap.NoWrap}
alignItems={AlignItems.Center}
justifyContent={JustifyContent.spaceBetween}
>
{nftImageURL ? (
<img
className="confirm-add-suggested-nft__nft-image"
src={nftImageURL}
alt={name || tokenId}
/>
) : (
<NftDefaultImage className="confirm-add-suggested-nft__nft-image-default" />
)}
<Box
display={Display.Flex}
flexDirection={FlexDirection.Column}
justifyContent={JustifyContent.spaceEvenly}
flexWrap={FlexWrap.NoWrap}
width={BlockSize.Full}
className="confirm-add-suggested-nft__nft-sub-details"
>
{rpcPrefs.blockExplorerUrl ? (
<ButtonLink
className="confirm-add-suggested-nft__nft-name"
href={blockExplorerLink}
title={address}
target="_blank"
size={BUTTON_SIZES.INHERIT}
>
{name || symbol || shortenAddress(address)}
</ButtonLink>
) : (
<Text
variant={TextVariant.bodySm}
className="confirm-add-suggested-nft__nft-name"
title={address}
>
{name || symbol || shortenAddress(address)}
</Text>
)}
<Text
variant={TextVariant.bodySm}
className="confirm-add-suggested-nft__nft-tokenId"
>
#{tokenId}
</Text>
</Box>
</Box>
<ButtonIcon
className="confirm-add-suggested-nft__nft-remove"
data-testid={`confirm-add-suggested-nft__nft-remove-${id}`}
iconName={IconName.Close}
size={ButtonIconSize.Sm}
color={IconColor.iconMuted}
onClick={(e) => {
e.preventDefault();
e.stopPropagation();
dispatch(
rejectPendingApproval(
id,
serializeError(
ethErrors.provider.userRejectedRequest(),
),
),
);
}}
/>
</Box>
);
},
)}
</Box>
</Box>
</Box>
<PageContainerFooter
cancelText={t('cancel')}
submitText={suggestedNfts.length === 1 ? t('addNft') : t('addNfts')}
onCancel={handleCancelNftClick}
onSubmit={handleAddNftsClick}
/>
</Box>
);
};
export default ConfirmAddSuggestedNFT;

View File

@ -0,0 +1,71 @@
/* eslint-disable react/prop-types */
import React from 'react';
import { Provider } from 'react-redux';
import { ApprovalType } from '@metamask/controller-utils';
import configureStore from '../../store/store';
import mockState from '../../../.storybook/test-data';
import ConfirmAddSuggestedNFT from '.';
const pendingNftApprovals = {
1: {
id: '1',
origin: 'https://www.opensea.io',
time: 1,
type: ApprovalType.WatchAsset,
requestData: {
asset: {
address: '0xb7F7F6C52F2e2fdb1963Eab30438024864c313F6',
name: 'Wrapped CryptoPunks',
tokenId: '1848',
standard: 'ERC721',
image: 'https://images.wrappedpunks.com/images/punks/1848.png',
},
},
},
2: {
id: '2',
origin: 'https://www.nft-collector.io',
time: 1,
type: ApprovalType.WatchAsset,
requestData: {
asset: {
address: '0xC8c77482e45F1F44dE1745F52C74426C631bDD51',
name: 'Legends of the Dance Floor',
tokenId: '1',
standard: 'ERC721',
image: 'https://www.miladymaker.net/milady/736.png',
},
},
},
};
const store = configureStore({
metamask: {
...mockState.metamask,
pendingApprovals: {
1: Object.values(pendingNftApprovals)[0],
},
},
});
export default {
title: 'Pages/ConfirmAddSuggestedNFT',
decorators: [(story) => <Provider store={store}>{story()}</Provider>],
};
export const DefaultStory = () => <ConfirmAddSuggestedNFT />;
DefaultStory.storyName = 'Default';
export const WithMultipleSuggestedNFTs = () => <ConfirmAddSuggestedNFT />;
const WithDuplicateAddressStore = configureStore({
metamask: {
...mockState.metamask,
pendingApprovals: pendingNftApprovals,
},
});
WithMultipleSuggestedNFTs.decorators = [
(story) => <Provider store={WithDuplicateAddressStore}>{story()}</Provider>,
];

View File

@ -0,0 +1,191 @@
import React from 'react';
import { act } from 'react-dom/test-utils';
import { fireEvent, screen } from '@testing-library/react';
import { ApprovalType } from '@metamask/controller-utils';
import {
resolvePendingApproval,
rejectPendingApproval,
} from '../../store/actions';
import configureStore from '../../store/store';
import { renderWithProvider } from '../../../test/jest/rendering';
import ConfirmAddSuggestedNFT from '.';
const PENDING_NFT_APPROVALS = {
1: {
id: '1',
origin: 'https://www.opensea.io',
time: 1,
type: ApprovalType.WatchAsset,
requestData: {
asset: {
address: '0x8b175474e89094c44da98b954eedeac495271d0a',
name: 'CryptoKitty',
tokenId: '15',
standard: 'ERC721',
image: 'https://www.cryptokitties.com/images/kitty-eth.svg',
},
},
},
2: {
id: '2',
origin: 'https://www.nft-collector.io',
time: 1,
type: ApprovalType.WatchAsset,
requestData: {
asset: {
address: '0xC8c77482e45F1F44dE1745F52C74426C631bDD51',
name: 'Legends of the Dance Floor',
tokenId: '1',
standard: 'ERC721',
image:
'https://www.nft-collector.io/images/legends-of-the-dance-floor.png',
},
},
},
};
const PENDING_TOKEN_APPROVALS = {
3: {
id: '3',
origin: 'https://www.uniswap.io',
time: 2,
type: ApprovalType.WatchAsset,
requestData: {
asset: {
address: '0x1f9840a85d5af5bf1d1762f925bdaddc4201f984',
symbol: 'UNI',
decimals: '18',
},
},
},
};
jest.mock('../../store/actions', () => ({
resolvePendingApproval: jest.fn().mockReturnValue({ type: 'test' }),
rejectPendingApproval: jest.fn().mockReturnValue({ type: 'test' }),
}));
const renderComponent = (pendingNfts = {}) => {
const store = configureStore({
metamask: {
pendingApprovals: pendingNfts,
providerConfig: { chainId: '0x1' },
},
history: {
mostRecentOverviewPage: '/',
},
});
return renderWithProvider(<ConfirmAddSuggestedNFT />, store);
};
describe('ConfirmAddSuggestedNFT Component', () => {
beforeEach(() => {
jest.clearAllMocks();
});
it('should render one suggested NFT', () => {
renderComponent({
1: {
id: '1',
origin: 'https://www.opensea.io',
time: 1,
type: ApprovalType.WatchAsset,
requestData: {
asset: {
address: '0x8b175474e89094c44da98b954eedeac495271d0a',
name: 'CryptoKitty',
tokenId: '15',
standard: 'ERC721',
},
},
},
});
expect(screen.getByText('Add suggested NFTs')).toBeInTheDocument();
expect(screen.getByText('www.opensea.io')).toBeInTheDocument();
expect(
screen.getByText('wants to add this asset to your wallet'),
).toBeInTheDocument();
expect(screen.getByText('CryptoKitty')).toBeInTheDocument();
expect(screen.getByText('#15')).toBeInTheDocument();
expect(screen.getByRole('button', { name: 'Cancel' })).toBeInTheDocument();
expect(screen.getByRole('button', { name: 'Add NFT' })).toBeInTheDocument();
});
it('should render a list of suggested NFTs', () => {
renderComponent({ ...PENDING_NFT_APPROVALS, ...PENDING_TOKEN_APPROVALS });
for (const {
requestData: { asset },
} of Object.values(PENDING_NFT_APPROVALS)) {
expect(screen.getByText(asset.name)).toBeInTheDocument();
expect(screen.getByText(`#${asset.tokenId}`)).toBeInTheDocument();
}
expect(screen.getAllByRole('img')).toHaveLength(
Object.values(PENDING_NFT_APPROVALS).length,
);
});
it('should dispatch resolvePendingApproval when clicking the "Add NFTs" button', async () => {
renderComponent(PENDING_NFT_APPROVALS);
const addNftButton = screen.getByRole('button', { name: 'Add NFTs' });
await act(async () => {
fireEvent.click(addNftButton);
});
expect(resolvePendingApproval).toHaveBeenCalledTimes(
Object.values(PENDING_NFT_APPROVALS).length,
);
Object.values(PENDING_NFT_APPROVALS).forEach(({ id }) => {
expect(resolvePendingApproval).toHaveBeenCalledWith(id, null);
});
});
it('should dispatch rejectPendingApproval when clicking the "Cancel" button', async () => {
renderComponent(PENDING_NFT_APPROVALS);
const cancelBtn = screen.getByRole('button', { name: 'Cancel' });
await act(async () => {
fireEvent.click(cancelBtn);
});
expect(rejectPendingApproval).toHaveBeenCalledTimes(
Object.values(PENDING_NFT_APPROVALS).length,
);
Object.values(PENDING_NFT_APPROVALS).forEach(({ id }) => {
expect(rejectPendingApproval).toHaveBeenCalledWith(
id,
expect.objectContaining({
code: 4001,
message: 'User rejected the request.',
stack: expect.any(String),
}),
);
});
});
it('should allow users to remove individual NFTs from the list of NFTs to add', async () => {
renderComponent(PENDING_NFT_APPROVALS);
const idToRemove = Object.values(PENDING_NFT_APPROVALS)[0].id;
const removeBtn = screen.getByTestId(
`confirm-add-suggested-nft__nft-remove-${idToRemove}`,
);
await act(async () => {
fireEvent.click(removeBtn);
});
expect(rejectPendingApproval).toHaveBeenCalledTimes(1);
expect(rejectPendingApproval).toHaveBeenCalledWith(
idToRemove,
expect.objectContaining({
code: 4001,
message: 'User rejected the request.',
stack: expect.any(String),
}),
);
});
});

View File

@ -0,0 +1 @@
export { default } from './confirm-add-suggested-nft';

View File

@ -0,0 +1,81 @@
.confirm-add-suggested-nft {
&__card {
margin: 16px;
box-shadow: 0 4px 16px rgba(0, 0, 0, 0.15);
}
&__header {
border-bottom: 1px solid var(--color-border-muted);
}
&__content {
overflow-y: auto;
flex: 1;
}
&__nft-list {
padding: 8px;
display: flex;
flex-flow: column nowrap;
width: 100%;
}
&__nft-list-item {
&:last-child {
margin-bottom: 0;
}
}
&__nft-image {
margin-right: 12px;
width: 48px;
border-radius: 8px;
flex: 0 0 auto;
}
&__nft-image-default {
margin-right: 12px;
flex: 0 0 auto;
width: 48px;
height: 48px;
border-radius: 8px;
padding: 0 !important;
}
&__nft-sub-details {
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
&__nft-name {
@include H6;
align-self: flex-start;
}
&__nft-tokenid {
@include H7;
color: #606060;
}
&__nft-remove-tooltip {
background-color: black;
color: white;
}
&__nft-single-image {
border-radius: inherit;
width: 100%;
}
&__nft-single-image-default {
border-radius: inherit;
}
&__nft-single-sub-details {
overflow: hidden;
text-overflow: ellipsis;
}
}

View File

@ -2,7 +2,6 @@ import React, { useCallback, useContext, useEffect, useMemo } from 'react';
import { useDispatch, useSelector } from 'react-redux';
import { useHistory } from 'react-router-dom';
import { ethErrors, serializeError } from 'eth-rpc-errors';
import { ApprovalType } from '@metamask/controller-utils';
import ActionableMessage from '../../components/ui/actionable-message/actionable-message';
import Button from '../../components/ui/button';
import Identicon from '../../components/ui/identicon';
@ -14,7 +13,6 @@ import { getMostRecentOverviewPage } from '../../ducks/history/history';
import { getTokens } from '../../ducks/metamask/metamask';
import ZENDESK_URLS from '../../helpers/constants/zendesk-url';
import { isEqualCaseInsensitive } from '../../../shared/modules/string-utils';
import { getApprovalRequestsByType } from '../../selectors';
import {
resolvePendingApproval,
rejectPendingApproval,
@ -28,22 +26,23 @@ import {
AssetType,
TokenStandard,
} from '../../../shared/constants/transaction';
import { getSuggestedTokens } from '../../selectors';
function getTokenName(name, symbol) {
return name === undefined ? symbol : `${name} (${symbol})`;
}
/**
* @param {Array} suggestedAssets - an array of assets suggested to add to the user's wallet
* @param {Array} suggestedTokens - an array of assets suggested to add to the user's wallet
* via the RPC method `wallet_watchAsset`
* @param {Array} tokens - the list of tokens currently tracked in state
* @returns {boolean} Returns true when the list of suggestedAssets contains an entry with
* @returns {boolean} Returns true when the list of suggestedTokens contains an entry with
* an address that matches an existing token.
*/
function hasDuplicateAddress(suggestedAssets, tokens) {
const duplicate = suggestedAssets.find(({ asset }) => {
function hasDuplicateAddress(suggestedTokens, tokens) {
const duplicate = suggestedTokens.find(({ requestData: { asset } }) => {
const dupe = tokens.find(({ address }) => {
return isEqualCaseInsensitive(address, asset.address);
return isEqualCaseInsensitive(address, asset?.address);
});
return Boolean(dupe);
});
@ -51,19 +50,19 @@ function hasDuplicateAddress(suggestedAssets, tokens) {
}
/**
* @param {Array} suggestedAssets - a list of assets suggested to add to the user's wallet
* @param {Array} suggestedTokens - a list of assets suggested to add to the user's wallet
* via RPC method `wallet_watchAsset`
* @param {Array} tokens - the list of tokens currently tracked in state
* @returns {boolean} Returns true when the list of suggestedAssets contains an entry with both
* @returns {boolean} Returns true when the list of suggestedTokens contains an entry with both
* 1. a symbol that matches an existing token
* 2. an address that does not match an existing token
*/
function hasDuplicateSymbolAndDiffAddress(suggestedAssets, tokens) {
const duplicate = suggestedAssets.find(({ asset }) => {
function hasDuplicateSymbolAndDiffAddress(suggestedTokens, tokens) {
const duplicate = suggestedTokens.find(({ requestData: { asset } }) => {
const dupe = tokens.find((token) => {
return (
isEqualCaseInsensitive(token.symbol, asset.symbol) &&
!isEqualCaseInsensitive(token.address, asset.address)
isEqualCaseInsensitive(token.symbol, asset?.symbol) &&
!isEqualCaseInsensitive(token.address, asset?.address)
);
});
return Boolean(dupe);
@ -77,18 +76,13 @@ const ConfirmAddSuggestedToken = () => {
const history = useHistory();
const mostRecentOverviewPage = useSelector(getMostRecentOverviewPage);
const suggestedAssets = useSelector((metamaskState) =>
getApprovalRequestsByType(metamaskState, ApprovalType.WatchAsset).map(
({ requestData }) => requestData,
),
);
const suggestedTokens = useSelector(getSuggestedTokens);
const tokens = useSelector(getTokens);
const trackEvent = useContext(MetaMetricsContext);
const knownTokenActionableMessage = useMemo(() => {
return (
hasDuplicateAddress(suggestedAssets, tokens) && (
hasDuplicateAddress(suggestedTokens, tokens) && (
<ActionableMessage
message={t('knownTokenWarning', [
<Button
@ -109,11 +103,11 @@ const ConfirmAddSuggestedToken = () => {
/>
)
);
}, [suggestedAssets, tokens, t]);
}, [suggestedTokens, tokens, t]);
const reusedTokenNameActionableMessage = useMemo(() => {
return (
hasDuplicateSymbolAndDiffAddress(suggestedAssets, tokens) && (
hasDuplicateSymbolAndDiffAddress(suggestedTokens, tokens) && (
<ActionableMessage
message={t('reusedTokenNameWarning')}
type="warning"
@ -123,11 +117,11 @@ const ConfirmAddSuggestedToken = () => {
/>
)
);
}, [suggestedAssets, tokens, t]);
}, [suggestedTokens, tokens, t]);
const handleAddTokensClick = useCallback(async () => {
await Promise.all(
suggestedAssets.map(async ({ asset, id }) => {
suggestedTokens.map(async ({ requestData: { asset }, id }) => {
await dispatch(resolvePendingApproval(id, null));
trackEvent({
@ -146,11 +140,11 @@ const ConfirmAddSuggestedToken = () => {
}),
);
history.push(mostRecentOverviewPage);
}, [dispatch, history, trackEvent, mostRecentOverviewPage, suggestedAssets]);
}, [dispatch, history, trackEvent, mostRecentOverviewPage, suggestedTokens]);
const handleCancelClick = useCallback(async () => {
const handleCancelTokenClick = useCallback(async () => {
await Promise.all(
suggestedAssets.map(({ id }) =>
suggestedTokens.map(({ id }) =>
dispatch(
rejectPendingApproval(
id,
@ -160,16 +154,16 @@ const ConfirmAddSuggestedToken = () => {
),
);
history.push(mostRecentOverviewPage);
}, [dispatch, history, mostRecentOverviewPage, suggestedAssets]);
}, [dispatch, history, mostRecentOverviewPage, suggestedTokens]);
const goBackIfNoSuggestedAssetsOnFirstRender = () => {
if (!suggestedAssets.length) {
const goBackIfNoSuggestedTokensOnFirstRender = () => {
if (!suggestedTokens.length) {
history.push(mostRecentOverviewPage);
}
};
useEffect(() => {
goBackIfNoSuggestedAssetsOnFirstRender();
goBackIfNoSuggestedTokensOnFirstRender();
// eslint-disable-next-line react-hooks/exhaustive-deps
}, []);
@ -194,7 +188,7 @@ const ConfirmAddSuggestedToken = () => {
</div>
</div>
<div className="confirm-add-suggested-token__token-list">
{suggestedAssets.map(({ asset }) => {
{suggestedTokens.map(({ requestData: { asset } }) => {
return (
<div
className="confirm-add-suggested-token__token-list-item"
@ -223,9 +217,9 @@ const ConfirmAddSuggestedToken = () => {
<PageContainerFooter
cancelText={t('cancel')}
submitText={t('addToken')}
onCancel={handleCancelClick}
onCancel={handleCancelTokenClick}
onSubmit={handleAddTokensClick}
disabled={suggestedAssets.length === 0}
disabled={suggestedTokens.length === 0}
/>
</div>
);

View File

@ -1,7 +1,7 @@
/* eslint-disable react/prop-types */
import React from 'react';
import { Provider } from 'react-redux';
import { pendingAssetApprovals as mockPendingAssetApprovals } from '../../../.storybook/initial-states/approval-screens/add-suggested-token';
import { pendingTokenApprovals as mockPendingTokenApprovals } from '../../../.storybook/initial-states/approval-screens/add-suggested-token';
import configureStore from '../../store/store';
@ -12,7 +12,7 @@ import ConfirmAddSuggestedToken from '.';
const store = configureStore({
metamask: {
...mockState.metamask,
pendingApprovals: [...mockPendingAssetApprovals],
pendingApprovals: mockPendingTokenApprovals,
tokens: [],
},
});
@ -29,10 +29,11 @@ export const WithDuplicateAddress = () => <ConfirmAddSuggestedToken />;
const WithDuplicateAddressStore = configureStore({
metamask: {
...mockState.metamask,
pendingApprovals: [...mockPendingAssetApprovals],
pendingApprovals: mockPendingTokenApprovals,
tokens: [
{
...mockPendingAssetApprovals[0].requestData.asset,
...Object.values(mockPendingTokenApprovals)[0].asset,
},
],
},
@ -47,10 +48,11 @@ export const WithDuplicateSymbolAndDifferentAddress = () => (
const WithDuplicateSymbolAndDifferentAddressStore = configureStore({
metamask: {
...mockState.metamask,
pendingApprovals: [...mockPendingAssetApprovals],
pendingApprovals: mockPendingTokenApprovals,
tokens: [
{
...mockPendingAssetApprovals[0].requestData.asset,
...Object.values(mockPendingTokenApprovals)[0].asset,
address: '0xNonSuggestedAddress',
},
],

View File

@ -1,7 +1,7 @@
import React from 'react';
import { act } from 'react-dom/test-utils';
import { ApprovalType } from '@metamask/controller-utils';
import { fireEvent, screen } from '@testing-library/react';
import { ApprovalType } from '@metamask/controller-utils';
import {
resolvePendingApproval,
rejectPendingApproval,
@ -10,9 +10,13 @@ import configureStore from '../../store/store';
import { renderWithProvider } from '../../../test/jest/rendering';
import ConfirmAddSuggestedToken from '.';
const MOCK_SUGGESTED_ASSETS = [
{
id: 1,
const PENDING_APPROVALS = {
1: {
id: '1',
origin: 'https://test-dapp.com',
time: Date.now(),
type: ApprovalType.WatchAsset,
requestData: {
asset: {
address: '0x8b175474e89094c44da98b954eedeac495271d0a',
symbol: 'NEW',
@ -21,8 +25,14 @@ const MOCK_SUGGESTED_ASSETS = [
unlisted: false,
},
},
{
id: 2,
requestState: null,
},
2: {
id: '2',
origin: 'https://test-dapp.com',
time: Date.now(),
type: ApprovalType.WatchAsset,
requestData: {
asset: {
address: '0xC8c77482e45F1F44dE1745F52C74426C631bDD51',
symbol: '0XYX',
@ -31,16 +41,9 @@ const MOCK_SUGGESTED_ASSETS = [
unlisted: false,
},
},
];
const MOCK_PENDING_ASSET_APPROVALS = MOCK_SUGGESTED_ASSETS.map(
(requestData) => {
return {
type: ApprovalType.WatchAsset,
requestData,
};
requestState: null,
},
);
};
const MOCK_TOKEN = {
address: '0x108cf70c7d384c552f42c07c41c0e1e46d77ea0d',
@ -56,7 +59,7 @@ jest.mock('../../store/actions', () => ({
const renderComponent = (tokens = []) => {
const store = configureStore({
metamask: {
pendingApprovals: [...MOCK_PENDING_ASSET_APPROVALS],
pendingApprovals: PENDING_APPROVALS,
tokens,
providerConfig: { chainId: '0x1' },
},
@ -86,11 +89,13 @@ describe('ConfirmAddSuggestedToken Component', () => {
it('should render the list of suggested tokens', () => {
renderComponent();
for (const { asset } of MOCK_SUGGESTED_ASSETS) {
for (const {
requestData: { asset },
} of Object.values(PENDING_APPROVALS)) {
expect(screen.getByText(asset.symbol)).toBeInTheDocument();
}
expect(screen.getAllByRole('img')).toHaveLength(
MOCK_SUGGESTED_ASSETS.length,
Object.values(PENDING_APPROVALS).length,
);
});
@ -103,10 +108,10 @@ describe('ConfirmAddSuggestedToken Component', () => {
});
expect(resolvePendingApproval).toHaveBeenCalledTimes(
MOCK_SUGGESTED_ASSETS.length,
Object.values(PENDING_APPROVALS).length,
);
MOCK_SUGGESTED_ASSETS.forEach(({ id }) => {
Object.values(PENDING_APPROVALS).forEach(({ id }) => {
expect(resolvePendingApproval).toHaveBeenCalledWith(id, null);
});
});
@ -120,10 +125,10 @@ describe('ConfirmAddSuggestedToken Component', () => {
});
expect(rejectPendingApproval).toHaveBeenCalledTimes(
MOCK_SUGGESTED_ASSETS.length,
Object.values(PENDING_APPROVALS).length,
);
MOCK_SUGGESTED_ASSETS.forEach(({ id }) => {
Object.values(PENDING_APPROVALS).forEach(({ id }) => {
expect(rejectPendingApproval).toHaveBeenCalledWith(
id,
expect.objectContaining({
@ -140,7 +145,8 @@ describe('ConfirmAddSuggestedToken Component', () => {
const mockTokens = [
{
...MOCK_TOKEN,
address: MOCK_SUGGESTED_ASSETS[0].asset.address,
address:
Object.values(PENDING_APPROVALS)[0].requestData.asset.address,
},
];
renderComponent(mockTokens);
@ -163,7 +169,7 @@ describe('ConfirmAddSuggestedToken Component', () => {
const mockTokens = [
{
...MOCK_TOKEN,
symbol: MOCK_SUGGESTED_ASSETS[0].asset.symbol,
symbol: Object.values(PENDING_APPROVALS)[0].requestData.asset.symbol,
},
];
renderComponent(mockTokens);

View File

@ -15,7 +15,7 @@ const renderComponent = (props) => {
const props = {
siteImage: 'https://metamask.github.io/test-dapp/metamask-fox.svg',
origin: 'https://metamask.github.io/test-dapp/',
tokenSymbol: 'TestDappCollectibles (#1)',
tokenSymbol: 'TestDappNFTs (#1)',
assetStandard: TokenStandard.ERC721,
tokenImage: 'https://metamask.github.io/test-dapp/metamask-fox.svg',
showCustomizeGasModal: jest.fn(),
@ -49,7 +49,7 @@ describe('ConfirmApproveContent Component', () => {
queryByText('https://metamask.github.io/test-dapp/'),
).toBeInTheDocument();
expect(getByTestId('confirm-approve-title').textContent).toStrictEqual(
' Allow access to and transfer of your TestDappCollectibles (#1)? ',
' Allow access to and transfer of your TestDappNFTs (#1)? ',
);
expect(
queryByText(
@ -112,7 +112,7 @@ describe('ConfirmApproveContent Component', () => {
queryByText('https://metamask.github.io/test-dapp/'),
).toBeInTheDocument();
expect(getByTestId('confirm-approve-title').textContent).toStrictEqual(
' Allow access to and transfer of your TestDappCollectibles (#1)? ',
' Allow access to and transfer of your TestDappNFTs (#1)? ',
);
expect(
queryByText(
@ -174,7 +174,7 @@ describe('ConfirmApproveContent Component', () => {
queryByText('https://metamask.github.io/test-dapp/'),
).toBeInTheDocument();
expect(getByTestId('confirm-approve-title').textContent).toStrictEqual(
' Allow access to and transfer of your TestDappCollectibles (#1)? ',
' Allow access to and transfer of your TestDappNFTs (#1)? ',
);
expect(
queryByText(
@ -232,7 +232,7 @@ describe('ConfirmApproveContent Component', () => {
queryByText('https://metamask.github.io/test-dapp/'),
).toBeInTheDocument();
expect(getByTestId('confirm-approve-title').textContent).toStrictEqual(
' Allow access to and transfer of your TestDappCollectibles (#1)? ',
' Allow access to and transfer of your TestDappNFTs (#1)? ',
);
expect(
queryByText(

View File

@ -45,6 +45,7 @@ import {
RESTORE_VAULT_ROUTE,
CONFIRM_TRANSACTION_ROUTE,
CONFIRM_ADD_SUGGESTED_TOKEN_ROUTE,
CONFIRM_ADD_SUGGESTED_NFT_ROUTE,
CONNECT_ROUTE,
CONNECTED_ROUTE,
CONNECTED_ACCOUNTS_ROUTE,
@ -105,8 +106,9 @@ export default class Home extends PureComponent {
static propTypes = {
history: PropTypes.object,
forgottenPassword: PropTypes.bool,
hasWatchAssetPendingApprovals: PropTypes.bool,
hasTransactionPendingApprovals: PropTypes.bool.isRequired,
hasWatchTokenPendingApprovals: PropTypes.bool,
hasWatchNftPendingApprovals: PropTypes.bool,
shouldShowSeedPhraseReminder: PropTypes.bool.isRequired,
isPopup: PropTypes.bool,
isNotification: PropTypes.bool.isRequired,
@ -194,7 +196,8 @@ export default class Home extends PureComponent {
haveSwapsQuotes,
isNotification,
showAwaitingSwapScreen,
hasWatchAssetPendingApprovals,
hasWatchTokenPendingApprovals,
hasWatchNftPendingApprovals,
swapsFetchParams,
hasTransactionPendingApprovals,
} = this.props;
@ -205,7 +208,8 @@ export default class Home extends PureComponent {
} else if (
firstPermissionsRequestId ||
hasTransactionPendingApprovals ||
hasWatchAssetPendingApprovals ||
hasWatchTokenPendingApprovals ||
hasWatchNftPendingApprovals ||
(!isNotification &&
(showAwaitingSwapScreen || haveSwapsQuotes || swapsFetchParams))
) {
@ -267,8 +271,9 @@ export default class Home extends PureComponent {
firstPermissionsRequestId,
history,
isNotification,
hasWatchAssetPendingApprovals,
hasTransactionPendingApprovals,
hasWatchTokenPendingApprovals,
hasWatchNftPendingApprovals,
haveSwapsQuotes,
showAwaitingSwapScreen,
swapsFetchParams,
@ -289,8 +294,10 @@ export default class Home extends PureComponent {
history.push(`${CONNECT_ROUTE}/${firstPermissionsRequestId}`);
} else if (hasTransactionPendingApprovals) {
history.push(CONFIRM_TRANSACTION_ROUTE);
} else if (hasWatchAssetPendingApprovals) {
} else if (hasWatchTokenPendingApprovals) {
history.push(CONFIRM_ADD_SUGGESTED_TOKEN_ROUTE);
} else if (hasWatchNftPendingApprovals) {
history.push(CONFIRM_ADD_SUGGESTED_NFT_ROUTE);
} else if (pendingConfirmations.length > 0) {
history.push(CONFIRMATION_V_NEXT_ROUTE);
}

View File

@ -1,7 +1,6 @@
import { compose } from 'redux';
import { connect } from 'react-redux';
import { withRouter } from 'react-router-dom';
import { ApprovalType } from '@metamask/controller-utils';
///: BEGIN:ONLY_INCLUDE_IN(build-mmi)
import {
getMmiPortfolioEnabled,
@ -35,7 +34,8 @@ import {
getNewTokensImported,
getShouldShowSeedPhraseReminder,
getRemoveNftMessage,
hasPendingApprovals,
getSuggestedTokens,
getSuggestedNfts,
} from '../../selectors';
import {
@ -121,14 +121,14 @@ const mapStateToProps = (state) => {
hasUnsignedQRHardwareTransaction(state) ||
hasUnsignedQRHardwareMessage(state);
const hasWatchAssetPendingApprovals = hasPendingApprovals(
state,
ApprovalType.WatchAsset,
);
const hasWatchTokenPendingApprovals = getSuggestedTokens(state).length > 0;
const hasWatchNftPendingApprovals = getSuggestedNfts(state).length > 0;
return {
forgottenPassword,
hasWatchAssetPendingApprovals,
hasWatchTokenPendingApprovals,
hasWatchNftPendingApprovals,
swapsEnabled,
hasTransactionPendingApprovals: hasTransactionPendingApprovals(state),
shouldShowSeedPhraseReminder: getShouldShowSeedPhraseReminder(state),

View File

@ -4,6 +4,7 @@
@import 'asset/asset';
@import 'confirm-import-token/index';
@import 'confirm-add-suggested-token/index';
@import 'confirm-add-suggested-nft/index';
@import 'confirm-approve/index';
@import 'confirm-decrypt-message/confirm-decrypt-message';
@import 'confirm-encryption-public-key/confirm-encryption-public-key';

View File

@ -23,6 +23,7 @@ import AddNftPage from '../add-nft';
import ConfirmImportTokenPage from '../confirm-import-token';
import ConfirmAddSuggestedTokenPage from '../confirm-add-suggested-token';
import CreateAccountPage from '../create-account/create-account.component';
import ConfirmAddSuggestedNftPage from '../confirm-add-suggested-nft';
import Loading from '../../components/ui/loading-screen';
import LoadingNetwork from '../../components/app/loading-network-screen';
import { Modal } from '../../components/app/modals';
@ -59,6 +60,7 @@ import {
IMPORT_TOKEN_ROUTE,
ASSET_ROUTE,
CONFIRM_ADD_SUGGESTED_TOKEN_ROUTE,
CONFIRM_ADD_SUGGESTED_NFT_ROUTE,
CONFIRM_TRANSACTION_ROUTE,
CONNECT_ROUTE,
DEFAULT_ROUTE,
@ -284,6 +286,11 @@ export default class Routes extends Component {
component={ConfirmAddSuggestedTokenPage}
exact
/>
<Authenticated
path={CONFIRM_ADD_SUGGESTED_NFT_ROUTE}
component={ConfirmAddSuggestedNftPage}
exact
/>
<Authenticated
path={CONFIRMATION_V_NEXT_ROUTE}
component={ConfirmationPage}

View File

@ -21,7 +21,7 @@ const props = {
tokenURI:
'data:application/json;base64,eyJuYW1lIjogIlRlc3QgRGFwcCBDb2xsZWN0aWJsZXMgIzIiLCAiZGVzY3JpcHRpb24iOiAiVGVzdCBEYXBwIENvbGxlY3RpYmxlcyBmb3IgdGVzdGluZy4iLCAiaW1hZ2UiOiAiZGF0YTppbWFnZS9zdmcreG1sO2Jhc2U2NCxQSE4yWnlCb1pXbG5hSFE5SWpNMU1DSWdkMmxrZEdnOUlqTTFNQ0lnZG1sbGQwSnZlRDBpTUNBd0lERXdNQ0F4TURBaUlIaHRiRzV6UFNKb2RIUndPaTh2ZDNkM0xuY3pMbTl5Wnk4eU1EQXdMM04yWnlJK1BHUmxabk0rUEhCaGRHZ2dhV1E5SWsxNVVHRjBhQ0lnWm1sc2JEMGlibTl1WlNJZ2MzUnliMnRsUFNKeVpXUWlJR1E5SWsweE1DdzVNQ0JST1RBc09UQWdPVEFzTkRVZ1VUa3dMREV3SURVd0xERXdJRkV4TUN3eE1DQXhNQ3cwTUNCUk1UQXNOekFnTkRVc056QWdVVGN3TERjd0lEYzFMRFV3SWlBdlBqd3ZaR1ZtY3o0OGRHVjRkRDQ4ZEdWNGRGQmhkR2dnYUhKbFpqMGlJMDE1VUdGMGFDSStVWFZwWTJzZ1luSnZkMjRnWm05NElHcDFiWEJ6SUc5MlpYSWdkR2hsSUd4aGVua2daRzluTGp3dmRHVjRkRkJoZEdnK1BDOTBaWGgwUGp3dmMzWm5QZz09IiwgImF0dHJpYnV0ZXMiOiBbeyJ0cmFpdF90eXBlIjogIlRva2VuIElkIiwgInZhbHVlIjogIjIifV19',
symbol: 'TDC',
name: 'TestDappCollectibles',
name: 'TestDappNFTs',
image:
'data:image/svg+xml;base64,PHN2ZyBoZWlnaHQ9IjM1MCIgd2lkdGg9IjM1MCIgdmlld0JveD0iMCAwIDEwMCAxMDAiIHhtbG5zPSJodHRwOi8vd3d3LnczLm9yZy8yMDAwL3N2ZyI+PGRlZnM+PHBhdGggaWQ9Ik15UGF0aCIgZmlsbD0ibm9uZSIgc3Ryb2tlPSJyZWQiIGQ9Ik0xMCw5MCBROTAsOTAgOTAsNDUgUTkwLDEwIDUwLDEwIFExMCwxMCAxMCw0MCBRMTAsNzAgNDUsNzAgUTcwLDcwIDc1LDUwIiAvPjwvZGVmcz48dGV4dD48dGV4dFBhdGggaHJlZj0iI015UGF0aCI+UXVpY2sgYnJvd24gZm94IGp1bXBzIG92ZXIgdGhlIGxhenkgZG9nLjwvdGV4dFBhdGg+PC90ZXh0Pjwvc3ZnPg==',
},

View File

@ -11,25 +11,38 @@ type ApprovalsMetaMaskState = {
};
};
export function hasPendingApprovals(
state: ApprovalsMetaMaskState,
approvalType: ApprovalType,
predicate?: (
approval: ApprovalControllerState['pendingApprovals'][string],
) => boolean,
) {
const pendingApprovalRequests = Object.values(
state.metamask.pendingApprovals,
).filter(({ type }) => type === approvalType);
if (predicate) {
return pendingApprovalRequests.some(predicate);
}
return pendingApprovalRequests.length > 0;
}
export const getApprovalRequestsByType = (
state: ApprovalsMetaMaskState,
approvalType: ApprovalType,
predicate?: (
approval: ApprovalControllerState['pendingApprovals'][string],
) => boolean,
) => {
const pendingApprovalRequests = Object.values(
state.metamask.pendingApprovals,
).filter(({ type }) => type === approvalType);
if (predicate) {
return pendingApprovalRequests.filter(predicate);
}
return pendingApprovalRequests;
};
export function hasPendingApprovals(
state: ApprovalsMetaMaskState,
approvalType: ApprovalType,
) {
const pendingApprovalRequests = getApprovalRequestsByType(
state,
approvalType,
);
return pendingApprovalRequests.length > 0;
}

View File

@ -346,7 +346,7 @@ export function getPermissionsRequests(state) {
return getApprovalRequestsByType(
state,
ApprovalType.WalletRequestPermissions,
).map(({ requestData }) => requestData);
)?.map(({ requestData }) => requestData);
}
export function getFirstPermissionRequest(state) {

View File

@ -1,6 +1,7 @@
///: BEGIN:ONLY_INCLUDE_IN(snaps)
import { SubjectType } from '@metamask/subject-metadata-controller';
///: END:ONLY_INCLUDE_IN
import { ApprovalType, ERC1155, ERC721 } from '@metamask/controller-utils';
import {
createSelector,
createSelectorCreator,
@ -544,7 +545,7 @@ export function getUnapprovedTxCount(state) {
}
export function getUnapprovedConfirmations(state) {
const { pendingApprovals } = state.metamask;
const { pendingApprovals = {} } = state.metamask;
return Object.values(pendingApprovals);
}
@ -555,6 +556,28 @@ export function getUnapprovedTemplatedConfirmations(state) {
);
}
export function getSuggestedTokens(state) {
return (
getUnapprovedConfirmations(state)?.filter(({ type, requestData }) => {
return (
type === ApprovalType.WatchAsset &&
requestData?.asset?.tokenId === undefined
);
}) || []
);
}
export function getSuggestedNfts(state) {
return (
getUnapprovedConfirmations(state)?.filter(({ requestData, type }) => {
return (
type === ApprovalType.WatchAsset &&
[ERC721, ERC1155].includes(requestData?.asset?.standard)
);
}) || []
);
}
export function getIsMainnet(state) {
const chainId = getCurrentChainId(state);
return chainId === CHAIN_IDS.MAINNET;

View File

@ -1,3 +1,4 @@
import { ApprovalType } from '@metamask/controller-utils';
import mockState from '../../test/data/mock-state.json';
import { KeyringType } from '../../shared/constants/keyring';
import {
@ -20,6 +21,184 @@ describe('Selectors', () => {
});
});
describe('#getSuggestedTokens', () => {
it('returns an empty array if pendingApprovals is undefined', () => {
expect(selectors.getSuggestedTokens({ metamask: {} })).toStrictEqual([]);
});
it('returns suggestedTokens from filtered pending approvals', () => {
const pendingApprovals = {
1: {
id: '1',
origin: 'dapp',
time: 1,
type: ApprovalType.WatchAsset,
requestData: {
asset: {
address: '0x8b175474e89094c44da98b954eedeac495271d0a',
symbol: 'NEW',
decimals: 18,
image: 'metamark.svg',
},
},
requestState: null,
},
2: {
id: '2',
origin: 'dapp',
time: 1,
type: ApprovalType.WatchAsset,
requestData: {
asset: {
address: '0xC8c77482e45F1F44dE1745F52C74426C631bDD51',
symbol: '0XYX',
decimals: 18,
image: '0x.svg',
},
},
},
3: {
id: '3',
origin: 'origin',
time: 1,
type: ApprovalType.Transaction,
requestData: {
// something that is not an asset
},
},
4: {
id: '4',
origin: 'dapp',
time: 1,
type: ApprovalType.WatchAsset,
requestData: {
asset: {
address: '0x1234abcd',
symbol: '0XYX',
tokenId: '123',
},
},
},
};
expect(
selectors.getSuggestedTokens({ metamask: { pendingApprovals } }),
).toStrictEqual([
{
id: '1',
origin: 'dapp',
time: 1,
type: ApprovalType.WatchAsset,
requestData: {
asset: {
address: '0x8b175474e89094c44da98b954eedeac495271d0a',
symbol: 'NEW',
decimals: 18,
image: 'metamark.svg',
},
},
requestState: null,
},
{
id: '2',
origin: 'dapp',
time: 1,
type: ApprovalType.WatchAsset,
requestData: {
asset: {
address: '0xC8c77482e45F1F44dE1745F52C74426C631bDD51',
symbol: '0XYX',
decimals: 18,
image: '0x.svg',
},
},
},
]);
});
});
describe('#getSuggestedNfts', () => {
it('returns an empty array if pendingApprovals is undefined', () => {
expect(selectors.getSuggestedNfts({ metamask: {} })).toStrictEqual([]);
});
it('returns suggestedNfts from filtered pending approvals', () => {
const pendingApprovals = {
1: {
id: '1',
origin: 'dapp',
time: 1,
type: ApprovalType.WatchAsset,
requestData: {
asset: {
address: '0x8b175474e89094c44da98b954eedeac495271d0a',
symbol: 'NEW',
decimals: 18,
image: 'metamark.svg',
},
},
requestState: null,
},
2: {
id: '2',
origin: 'dapp',
time: 1,
type: ApprovalType.WatchAsset,
requestData: {
asset: {
address: '0xC8c77482e45F1F44dE1745F52C74426C631bDD51',
symbol: '0XYX',
decimals: 18,
image: '0x.svg',
},
},
},
3: {
id: '3',
origin: 'origin',
time: 1,
type: ApprovalType.Transaction,
requestData: {
// something that is not an asset
},
},
4: {
id: '4',
origin: 'dapp',
time: 1,
type: ApprovalType.WatchAsset,
requestData: {
asset: {
address: '0x1234abcd',
symbol: '0XYX',
tokenId: '123',
standard: 'ERC721',
},
},
},
};
expect(
selectors.getSuggestedNfts({ metamask: { pendingApprovals } }),
).toStrictEqual([
{
id: '4',
origin: 'dapp',
time: 1,
type: ApprovalType.WatchAsset,
requestData: {
asset: {
address: '0x1234abcd',
symbol: '0XYX',
tokenId: '123',
standard: 'ERC721',
},
},
},
]);
});
});
describe('#getNewNetworkAdded', () => {
it('returns undefined if newNetworkAddedName is undefined', () => {
expect(selectors.getNewNetworkAdded({ appState: {} })).toBeUndefined();

161
yarn.lock
View File

@ -4931,10 +4931,10 @@ __metadata:
languageName: node
linkType: hard
"@metamask/test-dapp@npm:^6.0.0":
version: 6.0.0
resolution: "@metamask/test-dapp@npm:6.0.0"
checksum: eee793c8816d1205667002bd0ef60d248f3a35027937d66a51fd6a87e6eb7be70efd63052412d4dbabdd4329a5e729ab8293655dbdd8d3329bd732b1b1be45d1
"@metamask/test-dapp@npm:^7.0.0":
version: 7.0.0
resolution: "@metamask/test-dapp@npm:7.0.0"
checksum: 4a03ed86a97c94eef8131c0951fb7bf7eb0c988c407ae8e5ed6a897da30d1c48adf3e1e51e45220df6e5d51c22736a06c732ac95ba5741223573cd660706876b
languageName: node
linkType: hard
@ -24367,7 +24367,7 @@ __metadata:
"@metamask/snaps-utils-flask": "npm:@metamask/snaps-utils@0.34.0-flask.1"
"@metamask/subject-metadata-controller": ^2.0.0
"@metamask/swappable-obj-proxy": ^2.1.0
"@metamask/test-dapp": ^6.0.0
"@metamask/test-dapp": ^7.0.0
"@metamask/utils": ^5.0.0
"@ngraveio/bc-ur": ^1.1.6
"@popperjs/core": ^2.4.0
@ -24669,20 +24669,20 @@ __metadata:
linkType: hard
"micromark-extension-gfm-autolink-literal@npm:^1.0.0":
version: 1.0.4
resolution: "micromark-extension-gfm-autolink-literal@npm:1.0.4"
version: 1.0.5
resolution: "micromark-extension-gfm-autolink-literal@npm:1.0.5"
dependencies:
micromark-util-character: ^1.0.0
micromark-util-sanitize-uri: ^1.0.0
micromark-util-symbol: ^1.0.0
micromark-util-types: ^1.0.0
checksum: ea66602cc8375bffb414a662f54d7868ed8ba38a7fe9fca6b2c5f6d9ac632f6ed29e88a58dbd45a580c5c629e50c13e9b864382b796d549a69c5f69ba1df51f9
checksum: ec2f6bc4a3eb238c1b8be9744454ffbc2957e3d8a248697af5a26bb21479862300c0e40e0a92baf17c299ddf70d4bc4470d4eee112cd92322f87d81e45c2e83d
languageName: node
linkType: hard
"micromark-extension-gfm-footnote@npm:^1.0.0":
version: 1.1.0
resolution: "micromark-extension-gfm-footnote@npm:1.1.0"
version: 1.1.2
resolution: "micromark-extension-gfm-footnote@npm:1.1.2"
dependencies:
micromark-core-commonmark: ^1.0.0
micromark-factory-space: ^1.0.0
@ -24692,13 +24692,13 @@ __metadata:
micromark-util-symbol: ^1.0.0
micromark-util-types: ^1.0.0
uvu: ^0.5.0
checksum: 7a5408625ef2cca5cc18e6591c2522a8a409f466a6fbc0ed938950aafe5fc9bf1eada65e1a4dd4e36ec3e7b24920de1f4b3e2c365d8f5cd2d6ccb1f8c2377c49
checksum: c151a629ee1cd92363c018a50f926a002c944ac481ca72b3720b9529e9c20f1cbef98b0fefdcd2d594af37d0d9743673409cac488af0d2b194210fd16375dcb7
languageName: node
linkType: hard
"micromark-extension-gfm-strikethrough@npm:^1.0.0":
version: 1.0.5
resolution: "micromark-extension-gfm-strikethrough@npm:1.0.5"
version: 1.0.7
resolution: "micromark-extension-gfm-strikethrough@npm:1.0.7"
dependencies:
micromark-util-chunked: ^1.0.0
micromark-util-classify-character: ^1.0.0
@ -24706,20 +24706,20 @@ __metadata:
micromark-util-symbol: ^1.0.0
micromark-util-types: ^1.0.0
uvu: ^0.5.0
checksum: 548c0f257753d735c741533411957f04253da53db31e1f398dc5dc1de9f398c45586baad5223dce8f3b55f9433c255e6eb695fc3104256b8c332dd8737136882
checksum: 169e310a4408feade0df80180f60d48c5cc5b7070e5e75e0bbd914e9100273508162c4bb20b72d53081dc37f1ff5834b3afa137862576f763878552c03389811
languageName: node
linkType: hard
"micromark-extension-gfm-table@npm:^1.0.0":
version: 1.0.6
resolution: "micromark-extension-gfm-table@npm:1.0.6"
version: 1.0.7
resolution: "micromark-extension-gfm-table@npm:1.0.7"
dependencies:
micromark-factory-space: ^1.0.0
micromark-util-character: ^1.0.0
micromark-util-symbol: ^1.0.0
micromark-util-types: ^1.0.0
uvu: ^0.5.0
checksum: 92a5c15314bc87c9630a0cb1bd0b0ba4493e13e1bc5d02d55fdd843b56bf6b229ced2c73e331dd98d90d721e0929f5cf16737d3dd1864d61e6d0b7748695349a
checksum: 4853731285224e409d7e2c94c6ec849165093bff819e701221701aa7b7b34c17702c44f2f831e96b49dc27bb07e445b02b025561b68e62f5c3254415197e7af6
languageName: node
linkType: hard
@ -24733,15 +24733,15 @@ __metadata:
linkType: hard
"micromark-extension-gfm-task-list-item@npm:^1.0.0":
version: 1.0.4
resolution: "micromark-extension-gfm-task-list-item@npm:1.0.4"
version: 1.0.5
resolution: "micromark-extension-gfm-task-list-item@npm:1.0.5"
dependencies:
micromark-factory-space: ^1.0.0
micromark-util-character: ^1.0.0
micromark-util-symbol: ^1.0.0
micromark-util-types: ^1.0.0
uvu: ^0.5.0
checksum: 2575bb47b320f2479d3cc2492ba7cf79d6baa9cd0200c0ed120fd0e318e64e8ebab4a93a056a3781cb5107193f3b36ebd2d86a5928308bef45fc121291f97eb5
checksum: 929f05343d272cffb8008899289f4cffe986ef98fc622ebbd1aa4ff11470e6b32ed3e1f18cd294adb69cabb961a400650078f6c12b322cc515b82b5068b31960
languageName: node
linkType: hard
@ -24762,196 +24762,195 @@ __metadata:
linkType: hard
"micromark-factory-destination@npm:^1.0.0":
version: 1.0.0
resolution: "micromark-factory-destination@npm:1.0.0"
version: 1.1.0
resolution: "micromark-factory-destination@npm:1.1.0"
dependencies:
micromark-util-character: ^1.0.0
micromark-util-symbol: ^1.0.0
micromark-util-types: ^1.0.0
checksum: 8e733ae9c1c2342f14ff290bf09946e20f6f540117d80342377a765cac48df2ea5e748f33c8b07501ad7a43414b1a6597c8510ede2052b6bf1251fab89748e20
checksum: 9e2b5fb5fedbf622b687e20d51eb3d56ae90c0e7ecc19b37bd5285ec392c1e56f6e21aa7cfcb3c01eda88df88fe528f3acb91a5f57d7f4cba310bc3cd7f824fa
languageName: node
linkType: hard
"micromark-factory-label@npm:^1.0.0":
version: 1.0.2
resolution: "micromark-factory-label@npm:1.0.2"
version: 1.1.0
resolution: "micromark-factory-label@npm:1.1.0"
dependencies:
micromark-util-character: ^1.0.0
micromark-util-symbol: ^1.0.0
micromark-util-types: ^1.0.0
uvu: ^0.5.0
checksum: 957e9366bdc8dbc1437c0706ff96972fa985ab4b1274abcae12f6094f527cbf5c69e7f2304c23c7f4b96e311ff7911d226563b8b43dcfcd4091e8c985fb97ce6
checksum: fcda48f1287d9b148c562c627418a2ab759cdeae9c8e017910a0cba94bb759a96611e1fc6df33182e97d28fbf191475237298983bb89ef07d5b02464b1ad28d5
languageName: node
linkType: hard
"micromark-factory-space@npm:^1.0.0":
version: 1.0.0
resolution: "micromark-factory-space@npm:1.0.0"
version: 1.1.0
resolution: "micromark-factory-space@npm:1.1.0"
dependencies:
micromark-util-character: ^1.0.0
micromark-util-types: ^1.0.0
checksum: 70d3aafde4e68ef4e509a3b644e9a29e4aada00801279e346577b008cbca06d78051bcd62aa7ea7425856ed73f09abd2b36607803055f726f52607ee7cb706b0
checksum: b58435076b998a7e244259a4694eb83c78915581206b6e7fc07b34c6abd36a1726ade63df8972fbf6c8fa38eecb9074f4e17be8d53f942e3b3d23d1a0ecaa941
languageName: node
linkType: hard
"micromark-factory-title@npm:^1.0.0":
version: 1.0.2
resolution: "micromark-factory-title@npm:1.0.2"
version: 1.1.0
resolution: "micromark-factory-title@npm:1.1.0"
dependencies:
micromark-factory-space: ^1.0.0
micromark-util-character: ^1.0.0
micromark-util-symbol: ^1.0.0
micromark-util-types: ^1.0.0
uvu: ^0.5.0
checksum: 9a9cf66babde0bad1e25d6c1087082bfde6dfc319a36cab67c89651cc1a53d0e21cdec83262b5a4c33bff49f0e3c8dc2a7bd464e991d40dbea166a8f9b37e5b2
checksum: 4432d3dbc828c81f483c5901b0c6591a85d65a9e33f7d96ba7c3ae821617a0b3237ff5faf53a9152d00aaf9afb3a9f185b205590f40ed754f1d9232e0e9157b1
languageName: node
linkType: hard
"micromark-factory-whitespace@npm:^1.0.0":
version: 1.0.0
resolution: "micromark-factory-whitespace@npm:1.0.0"
version: 1.1.0
resolution: "micromark-factory-whitespace@npm:1.1.0"
dependencies:
micromark-factory-space: ^1.0.0
micromark-util-character: ^1.0.0
micromark-util-symbol: ^1.0.0
micromark-util-types: ^1.0.0
checksum: 0888386e6ea2dd665a5182c570d9b3d0a172d3f11694ca5a2a84e552149c9f1429f5b975ec26e1f0fa4388c55a656c9f359ce5e0603aff6175ba3e255076f20b
checksum: ef0fa682c7d593d85a514ee329809dee27d10bc2a2b65217d8ef81173e33b8e83c549049764b1ad851adfe0a204dec5450d9d20a4ca8598f6c94533a73f73fcd
languageName: node
linkType: hard
"micromark-util-character@npm:^1.0.0":
version: 1.1.0
resolution: "micromark-util-character@npm:1.1.0"
version: 1.2.0
resolution: "micromark-util-character@npm:1.2.0"
dependencies:
micromark-util-symbol: ^1.0.0
micromark-util-types: ^1.0.0
checksum: 504a4e3321f69bddf3fec9f0c1058239fc23336bda5be31d532b150491eda47965a251b37f8a7a9db0c65933b3aaa49cf88044fb1028be3af7c5ee6212bf8d5f
checksum: 089e79162a19b4a28731736246579ab7e9482ac93cd681c2bfca9983dcff659212ef158a66a5957e9d4b1dba957d1b87b565d85418a5b009f0294f1f07f2aaac
languageName: node
linkType: hard
"micromark-util-chunked@npm:^1.0.0":
version: 1.0.0
resolution: "micromark-util-chunked@npm:1.0.0"
version: 1.1.0
resolution: "micromark-util-chunked@npm:1.1.0"
dependencies:
micromark-util-symbol: ^1.0.0
checksum: c1efd56e8c4217bcf1c6f1a9fb9912b4a2a5503b00d031da902be922fb3fee60409ac53f11739991291357b2784fb0647ddfc74c94753a068646c0cb0fd71421
checksum: c435bde9110cb595e3c61b7f54c2dc28ee03e6a57fa0fc1e67e498ad8bac61ee5a7457a2b6a73022ddc585676ede4b912d28dcf57eb3bd6951e54015e14dc20b
languageName: node
linkType: hard
"micromark-util-classify-character@npm:^1.0.0":
version: 1.0.0
resolution: "micromark-util-classify-character@npm:1.0.0"
version: 1.1.0
resolution: "micromark-util-classify-character@npm:1.1.0"
dependencies:
micromark-util-character: ^1.0.0
micromark-util-symbol: ^1.0.0
micromark-util-types: ^1.0.0
checksum: 180446e6a1dec653f625ded028f244784e1db8d10ad05c5d70f08af9de393b4a03dc6cf6fa5ed8ccc9c24bbece7837abf3bf66681c0b4adf159364b7d5236dfd
checksum: 8499cb0bb1f7fb946f5896285fcca65cd742f66cd3e79ba7744792bd413ec46834f932a286de650349914d02e822946df3b55d03e6a8e1d245d1ddbd5102e5b0
languageName: node
linkType: hard
"micromark-util-combine-extensions@npm:^1.0.0":
version: 1.0.0
resolution: "micromark-util-combine-extensions@npm:1.0.0"
version: 1.1.0
resolution: "micromark-util-combine-extensions@npm:1.1.0"
dependencies:
micromark-util-chunked: ^1.0.0
micromark-util-types: ^1.0.0
checksum: 5304a820ef75340e1be69d6ad167055b6ba9a3bafe8171e5945a935752f462415a9dd61eb3490220c055a8a11167209a45bfa73f278338b7d3d61fa1464d3f35
checksum: ee78464f5d4b61ccb437850cd2d7da4d690b260bca4ca7a79c4bb70291b84f83988159e373b167181b6716cb197e309bc6e6c96a68cc3ba9d50c13652774aba9
languageName: node
linkType: hard
"micromark-util-decode-numeric-character-reference@npm:^1.0.0":
version: 1.0.0
resolution: "micromark-util-decode-numeric-character-reference@npm:1.0.0"
version: 1.1.0
resolution: "micromark-util-decode-numeric-character-reference@npm:1.1.0"
dependencies:
micromark-util-symbol: ^1.0.0
checksum: f3ae2bb582a80f1e9d3face026f585c0c472335c064bd850bde152376f0394cb2831746749b6be6e0160f7d73626f67d10716026c04c87f402c0dd45a1a28633
checksum: 4733fe75146e37611243f055fc6847137b66f0cde74d080e33bd26d0408c1d6f44cabc984063eee5968b133cb46855e729d555b9ff8d744652262b7b51feec73
languageName: node
linkType: hard
"micromark-util-decode-string@npm:^1.0.0":
version: 1.0.2
resolution: "micromark-util-decode-string@npm:1.0.2"
version: 1.1.0
resolution: "micromark-util-decode-string@npm:1.1.0"
dependencies:
decode-named-character-reference: ^1.0.0
micromark-util-character: ^1.0.0
micromark-util-decode-numeric-character-reference: ^1.0.0
micromark-util-symbol: ^1.0.0
checksum: 2dbb41c9691cc71505d39706405139fb7d6699429d577a524c7c248ac0cfd09d3dd212ad8e91c143a00b2896f26f81136edc67c5bda32d20446f0834d261b17a
checksum: f1625155db452f15aa472918499689ba086b9c49d1322a08b22bfbcabe918c61b230a3002c8bc3ea9b1f52ca7a9bb1c3dd43ccb548c7f5f8b16c24a1ae77a813
languageName: node
linkType: hard
"micromark-util-encode@npm:^1.0.0":
version: 1.0.1
resolution: "micromark-util-encode@npm:1.0.1"
checksum: 9290583abfdc79ea3e7eb92c012c47a0e14327888f8aaa6f57ff79b3058d8e7743716b9d91abca3646f15ab3d78fdad9779fdb4ccf13349cd53309dfc845253a
version: 1.1.0
resolution: "micromark-util-encode@npm:1.1.0"
checksum: 4ef29d02b12336918cea6782fa87c8c578c67463925221d4e42183a706bde07f4b8b5f9a5e1c7ce8c73bb5a98b261acd3238fecd152e6dd1cdfa2d1ae11b60a0
languageName: node
linkType: hard
"micromark-util-html-tag-name@npm:^1.0.0":
version: 1.1.0
resolution: "micromark-util-html-tag-name@npm:1.1.0"
checksum: a9b783cec89ec813648d59799464c1950fe281ae797b2a965f98ad0167d7fa1a247718eff023b4c015f47211a172f9446b8e6b98aad50e3cd44a3337317dad2c
version: 1.2.0
resolution: "micromark-util-html-tag-name@npm:1.2.0"
checksum: ccf0fa99b5c58676dc5192c74665a3bfd1b536fafaf94723bd7f31f96979d589992df6fcf2862eba290ef18e6a8efb30ec8e1e910d9f3fc74f208871e9f84750
languageName: node
linkType: hard
"micromark-util-normalize-identifier@npm:^1.0.0":
version: 1.0.0
resolution: "micromark-util-normalize-identifier@npm:1.0.0"
version: 1.1.0
resolution: "micromark-util-normalize-identifier@npm:1.1.0"
dependencies:
micromark-util-symbol: ^1.0.0
checksum: d7c09d5e8318fb72f194af72664bd84a48a2928e3550b2b21c8fbc0ec22524f2a72e0f6663d2b95dc189a6957d3d7759b60716e888909710767cd557be821f8b
checksum: 8655bea41ffa4333e03fc22462cb42d631bbef9c3c07b625fd852b7eb442a110f9d2e5902a42e65188d85498279569502bf92f3434a1180fc06f7c37edfbaee2
languageName: node
linkType: hard
"micromark-util-resolve-all@npm:^1.0.0":
version: 1.0.0
resolution: "micromark-util-resolve-all@npm:1.0.0"
version: 1.1.0
resolution: "micromark-util-resolve-all@npm:1.1.0"
dependencies:
micromark-util-types: ^1.0.0
checksum: 409667f2bd126ef8acce009270d2aecaaa5584c5807672bc657b09e50aa91bd2e552cf41e5be1e6469244a83349cbb71daf6059b746b1c44e3f35446fef63e50
checksum: 1ce6c0237cd3ca061e76fae6602cf95014e764a91be1b9f10d36cb0f21ca88f9a07de8d49ab8101efd0b140a4fbfda6a1efb72027ab3f4d5b54c9543271dc52c
languageName: node
linkType: hard
"micromark-util-sanitize-uri@npm:^1.0.0":
version: 1.1.0
resolution: "micromark-util-sanitize-uri@npm:1.1.0"
version: 1.2.0
resolution: "micromark-util-sanitize-uri@npm:1.2.0"
dependencies:
micromark-util-character: ^1.0.0
micromark-util-encode: ^1.0.0
micromark-util-symbol: ^1.0.0
checksum: fe6093faa0adeb8fad606184d927ce37f207dcc2ec7256438e7f273c8829686245dd6161b597913ef25a3c4fb61863d3612a40cb04cf15f83ba1b4087099996b
checksum: 6663f365c4fe3961d622a580f4a61e34867450697f6806f027f21cf63c92989494895fcebe2345d52e249fe58a35be56e223a9776d084c9287818b40c779acc1
languageName: node
linkType: hard
"micromark-util-subtokenize@npm:^1.0.0":
version: 1.0.2
resolution: "micromark-util-subtokenize@npm:1.0.2"
version: 1.1.0
resolution: "micromark-util-subtokenize@npm:1.1.0"
dependencies:
micromark-util-chunked: ^1.0.0
micromark-util-symbol: ^1.0.0
micromark-util-types: ^1.0.0
uvu: ^0.5.0
checksum: c32ee58a7e1384ab1161a9ee02fbb04ad7b6e96d0b8c93dba9803c329a53d07f22ab394c7a96b2e30d6b8fbe3585b85817dba07277b1317111fc234e166bd2d1
checksum: 4a9d780c4d62910e196ea4fd886dc4079d8e424e5d625c0820016da0ed399a281daff39c50f9288045cc4bcd90ab47647e5396aba500f0853105d70dc8b1fc45
languageName: node
linkType: hard
"micromark-util-symbol@npm:^1.0.0":
version: 1.0.1
resolution: "micromark-util-symbol@npm:1.0.1"
checksum: c6a3023b3a7432c15864b5e33a1bcb5042ac7aa097f2f452e587bef45433d42d39e0a5cce12fbea91e0671098ba0c3f62a2b30ce1cde66ecbb5e8336acf4391d
version: 1.1.0
resolution: "micromark-util-symbol@npm:1.1.0"
checksum: 02414a753b79f67ff3276b517eeac87913aea6c028f3e668a19ea0fc09d98aea9f93d6222a76ca783d20299af9e4b8e7c797fe516b766185dcc6e93290f11f88
languageName: node
linkType: hard
"micromark-util-types@npm:^1.0.0, micromark-util-types@npm:^1.0.1":
version: 1.0.2
resolution: "micromark-util-types@npm:1.0.2"
checksum: 08dc901b7c06ee3dfeb54befca05cbdab9525c1cf1c1080967c3878c9e72cb9856c7e8ff6112816e18ead36ce6f99d55aaa91560768f2f6417b415dcba1244df
version: 1.1.0
resolution: "micromark-util-types@npm:1.1.0"
checksum: b0ef2b4b9589f15aec2666690477a6a185536927ceb7aa55a0f46475852e012d75a1ab945187e5c7841969a842892164b15d58ff8316b8e0d6cc920cabd5ede7
languageName: node
linkType: hard
"micromark@npm:^3.0.0":
version: 3.1.0
resolution: "micromark@npm:3.1.0"
version: 3.2.0
resolution: "micromark@npm:3.2.0"
dependencies:
"@types/debug": ^4.0.0
debug: ^4.0.0
@ -24970,7 +24969,7 @@ __metadata:
micromark-util-symbol: ^1.0.0
micromark-util-types: ^1.0.1
uvu: ^0.5.0
checksum: 5fe5bc3bf92e2ddd37b5f0034080fc3a4d4b3c1130dd5e435bb96ec75e9453091272852e71a4d74906a8fcf992d6f79d794607657c534bda49941e9950a92e28
checksum: 56c15851ad3eb8301aede65603473443e50c92a54849cac1dadd57e4ec33ab03a0a77f3df03de47133e6e8f695dae83b759b514586193269e98c0bf319ecd5e4
languageName: node
linkType: hard