From c16b35c029c2dcd2de1af86940c1eb4bc3dd0fdf Mon Sep 17 00:00:00 2001 From: Alex Donesky Date: Thu, 15 Jun 2023 15:18:12 -0500 Subject: [PATCH] Extend `wallet_watchAsset` to support `ERC721` and `ERC1155` tokens (#19454) * Extend wallet_watchAsset to support ERC721 and ERC1155 tokens --- .../approval-screens/add-suggested-token.js | 188 ++++++---- app/_locales/en/messages.json | 13 + .../handlers/watch-asset.js | 7 +- app/scripts/metamask-controller.js | 25 +- package.json | 2 +- test/e2e/fixture-builder.js | 6 +- test/e2e/nft/erc721-interaction.spec.js | 224 +++++++++++- test/e2e/nft/import-nft.spec.js | 2 +- test/e2e/nft/send-nft.spec.js | 2 +- test/e2e/nft/view-nft-details.spec.js | 6 +- test/e2e/seeder/ganache-seeder.js | 2 +- test/e2e/seeder/smart-contracts.js | 8 +- .../nft-default-image/nft-default-image.js | 13 +- ui/helpers/constants/routes.ts | 3 + ui/helpers/utils/util.js | 4 +- .../confirm-add-suggested-nft.js | 343 ++++++++++++++++++ .../confirm-add-suggested-nft.stories.js | 71 ++++ .../confirm-add-suggested-nft.test.js | 191 ++++++++++ ui/pages/confirm-add-suggested-nft/index.js | 1 + ui/pages/confirm-add-suggested-nft/index.scss | 81 +++++ .../confirm-add-suggested-token.js | 62 ++-- .../confirm-add-suggested-token.stories.js | 14 +- .../confirm-add-suggested-token.test.js | 80 ++-- .../confirm-approve-content.component.test.js | 10 +- ui/pages/home/home.component.js | 17 +- ui/pages/home/home.container.js | 14 +- ui/pages/pages.scss | 1 + ui/pages/routes/routes.component.js | 7 + .../send-asset-row/send-asset-row.test.js | 2 +- ui/selectors/approvals.ts | 37 +- ui/selectors/permissions.js | 2 +- ui/selectors/selectors.js | 25 +- ui/selectors/selectors.test.js | 179 +++++++++ yarn.lock | 161 ++++---- 34 files changed, 1508 insertions(+), 295 deletions(-) create mode 100644 ui/pages/confirm-add-suggested-nft/confirm-add-suggested-nft.js create mode 100644 ui/pages/confirm-add-suggested-nft/confirm-add-suggested-nft.stories.js create mode 100644 ui/pages/confirm-add-suggested-nft/confirm-add-suggested-nft.test.js create mode 100644 ui/pages/confirm-add-suggested-nft/index.js create mode 100644 ui/pages/confirm-add-suggested-nft/index.scss diff --git a/.storybook/initial-states/approval-screens/add-suggested-token.js b/.storybook/initial-states/approval-screens/add-suggested-token.js index 301e6bfa1..4197140e6 100644 --- a/.storybook/initial-states/approval-screens/add-suggested-token.js +++ b/.storybook/initial-states/approval-screens/add-suggested-token.js @@ -1,77 +1,121 @@ import { ApprovalType } from '@metamask/controller-utils'; -const suggestedAssets = [ - { - address: '0x6b175474e89094c44da98b954eedeac495271d0f', - symbol: 'ETH', - decimals: 18, - image: './images/eth_logo.png', - unlisted: false, - }, - { - address: '0xB8c77482e45F1F44dE1745F52C74426C631bDD52', - symbol: '0X', - decimals: 18, - image: '0x.svg', - unlisted: false, - }, - { - address: '0x1f9840a85d5af5bf1d1762f925bdaddc4201f984', - symbol: 'AST', - decimals: 18, - image: 'ast.png', - unlisted: false, - }, - { - address: '0x9f8f72aa9304c8b593d555f12ef6589cc3a579a2', - symbol: 'BAT', - decimals: 18, - image: 'BAT_icon.svg', - unlisted: false, - }, - { - address: '0xe83cccfabd4ed148903bf36d4283ee7c8b3494d1', - symbol: 'CVL', - decimals: 18, - image: 'CVL_token.svg', - unlisted: false, - }, - { - address: '0x0bc529c00C6401aEF6D220BE8C6Ea1667F6Ad93e', - symbol: 'GLA', - decimals: 18, - image: 'gladius.svg', - unlisted: false, - }, - { - address: '0x467Bccd9d29f223BcE8043b84E8C8B282827790F', - symbol: 'GNO', - decimals: 18, - image: 'gnosis.svg', - unlisted: false, - }, - { - address: '0xff20817765cb7f73d4bde2e66e067e58d11095c2', - symbol: 'OMG', - decimals: 18, - image: 'omg.jpg', - unlisted: false, - }, - { - address: '0x8e870d67f660d95d5be530380d0ec0bd388289e1', - symbol: 'WED', - decimals: 18, - image: 'wed.png', - unlisted: false, - }, -]; - -export const pendingAssetApprovals = suggestedAssets.map((asset, index) => { - return { +export const pendingTokenApprovals = { + 1: { + id: 1, type: ApprovalType.WatchAsset, requestData: { - id: index, - asset, + 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, + }, + }, + }, +}; \ No newline at end of file diff --git a/app/_locales/en/messages.json b/app/_locales/en/messages.json index 9bb43c4cc..cc1803255 100644 --- a/app/_locales/en/messages.json +++ b/app/_locales/en/messages.json @@ -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" }, diff --git a/app/scripts/lib/rpc-method-middleware/handlers/watch-asset.js b/app/scripts/lib/rpc-method-middleware/handlers/watch-asset.js index 58a3c22dd..8954a15f5 100644 --- a/app/scripts/lib/rpc-method-middleware/handlers/watch-asset.js +++ b/app/scripts/lib/rpc-method-middleware/handlers/watch-asset.js @@ -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) { diff --git a/app/scripts/metamask-controller.js b/app/scripts/metamask-controller.js index 0af0d6c7d..1ae038534 100644 --- a/app/scripts/metamask-controller.js +++ b/app/scripts/metamask-controller.js @@ -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, diff --git a/package.json b/package.json index cbd0eb645..1db9ab5d3 100644 --- a/package.json +++ b/package.json @@ -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", diff --git a/test/e2e/fixture-builder.js b/test/e2e/fixture-builder.js index 80e25d6cf..428814562 100644 --- a/test/e2e/fixture-builder.js +++ b/test/e2e/fixture-builder.js @@ -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', }, diff --git a/test/e2e/nft/erc721-interaction.spec.js b/test/e2e/nft/erc721-interaction.spec.js index f8c703354..e886be2e9 100644 --- a/test/e2e/nft/erc721-interaction.spec.js +++ b/test/e2e/nft/erc721-interaction.spec.js @@ -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); }, diff --git a/test/e2e/nft/import-nft.spec.js b/test/e2e/nft/import-nft.spec.js index bfb038b56..60e375b5c 100644 --- a/test/e2e/nft/import-nft.spec.js +++ b/test/e2e/nft/import-nft.spec.js @@ -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', diff --git a/test/e2e/nft/send-nft.spec.js b/test/e2e/nft/send-nft.spec.js index 47145ca51..e08d12eb1 100644 --- a/test/e2e/nft/send-nft.spec.js +++ b/test/e2e/nft/send-nft.spec.js @@ -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); diff --git a/test/e2e/nft/view-nft-details.spec.js b/test/e2e/nft/view-nft-details.spec.js index 890cc72e2..2c3169dbe 100644 --- a/test/e2e/nft/view-nft-details.spec.js +++ b/test/e2e/nft/view-nft-details.spec.js @@ -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'); diff --git a/test/e2e/seeder/ganache-seeder.js b/test/e2e/seeder/ganache-seeder.js index 5a68134ef..73b554236 100644 --- a/test/e2e/seeder/ganache-seeder.js +++ b/test/e2e/seeder/ganache-seeder.js @@ -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(); diff --git a/test/e2e/seeder/smart-contracts.js b/test/e2e/seeder/smart-contracts.js index 48d472b4e..365173a66 100644 --- a/test/e2e/seeder/smart-contracts.js +++ b/test/e2e/seeder/smart-contracts.js @@ -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 = { diff --git a/ui/components/app/nft-default-image/nft-default-image.js b/ui/components/app/nft-default-image/nft-default-image.js index ca24c384a..54c5d688d 100644 --- a/ui/components/app/nft-default-image/nft-default-image.js +++ b/ui/components/app/nft-default-image/nft-default-image.js @@ -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 ( { }; 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; diff --git a/ui/pages/confirm-add-suggested-nft/confirm-add-suggested-nft.js b/ui/pages/confirm-add-suggested-nft/confirm-add-suggested-nft.js new file mode 100644 index 000000000..309a14b39 --- /dev/null +++ b/ui/pages/confirm-add-suggested-nft/confirm-add-suggested-nft.js @@ -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 ( + + + + {t('addSuggestedNFTs')} + + + {t('wantsToAddThisAsset', [ + origin === 'dapp' ? ( + + {origin} + + ) : ( + + {origin} + + ), + ])} + + + + + 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 ( + + {nftImageURL ? ( + {name + ) : ( + + )} + + + {rpcPrefs.blockExplorerUrl ? ( + + {name || symbol || shortenAddress(address)} + + ) : ( + + {name || symbol || shortenAddress(address)} + + )} + + #{tokenId} + + + + + ); + } + return ( + + + {nftImageURL ? ( + {name + ) : ( + + )} + + {rpcPrefs.blockExplorerUrl ? ( + + {name || symbol || shortenAddress(address)} + + ) : ( + + {name || symbol || shortenAddress(address)} + + )} + + #{tokenId} + + + + { + e.preventDefault(); + e.stopPropagation(); + dispatch( + rejectPendingApproval( + id, + serializeError( + ethErrors.provider.userRejectedRequest(), + ), + ), + ); + }} + /> + + ); + }, + )} + + + + + + ); +}; + +export default ConfirmAddSuggestedNFT; diff --git a/ui/pages/confirm-add-suggested-nft/confirm-add-suggested-nft.stories.js b/ui/pages/confirm-add-suggested-nft/confirm-add-suggested-nft.stories.js new file mode 100644 index 000000000..434e7b4a6 --- /dev/null +++ b/ui/pages/confirm-add-suggested-nft/confirm-add-suggested-nft.stories.js @@ -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) => {story()}], +}; + +export const DefaultStory = () => ; +DefaultStory.storyName = 'Default'; + +export const WithMultipleSuggestedNFTs = () => ; +const WithDuplicateAddressStore = configureStore({ + metamask: { + ...mockState.metamask, + pendingApprovals: pendingNftApprovals, + }, +}); +WithMultipleSuggestedNFTs.decorators = [ + (story) => {story()}, +]; diff --git a/ui/pages/confirm-add-suggested-nft/confirm-add-suggested-nft.test.js b/ui/pages/confirm-add-suggested-nft/confirm-add-suggested-nft.test.js new file mode 100644 index 000000000..fac4064d9 --- /dev/null +++ b/ui/pages/confirm-add-suggested-nft/confirm-add-suggested-nft.test.js @@ -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(, 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), + }), + ); + }); +}); diff --git a/ui/pages/confirm-add-suggested-nft/index.js b/ui/pages/confirm-add-suggested-nft/index.js new file mode 100644 index 000000000..71519330f --- /dev/null +++ b/ui/pages/confirm-add-suggested-nft/index.js @@ -0,0 +1 @@ +export { default } from './confirm-add-suggested-nft'; diff --git a/ui/pages/confirm-add-suggested-nft/index.scss b/ui/pages/confirm-add-suggested-nft/index.scss new file mode 100644 index 000000000..e2b0ea7b7 --- /dev/null +++ b/ui/pages/confirm-add-suggested-nft/index.scss @@ -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; + } +} diff --git a/ui/pages/confirm-add-suggested-token/confirm-add-suggested-token.js b/ui/pages/confirm-add-suggested-token/confirm-add-suggested-token.js index 1a1c783d0..9867e144b 100644 --- a/ui/pages/confirm-add-suggested-token/confirm-add-suggested-token.js +++ b/ui/pages/confirm-add-suggested-token/confirm-add-suggested-token.js @@ -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) && ( { /> ) ); - }, [suggestedAssets, tokens, t]); + }, [suggestedTokens, tokens, t]); const reusedTokenNameActionableMessage = useMemo(() => { return ( - hasDuplicateSymbolAndDiffAddress(suggestedAssets, tokens) && ( + hasDuplicateSymbolAndDiffAddress(suggestedTokens, tokens) && ( { /> ) ); - }, [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 = () => {
- {suggestedAssets.map(({ asset }) => { + {suggestedTokens.map(({ requestData: { asset } }) => { return (
{
); diff --git a/ui/pages/confirm-add-suggested-token/confirm-add-suggested-token.stories.js b/ui/pages/confirm-add-suggested-token/confirm-add-suggested-token.stories.js index f3f8c4a8d..40a05a24a 100644 --- a/ui/pages/confirm-add-suggested-token/confirm-add-suggested-token.stories.js +++ b/ui/pages/confirm-add-suggested-token/confirm-add-suggested-token.stories.js @@ -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 = () => ; 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', }, ], diff --git a/ui/pages/confirm-add-suggested-token/confirm-add-suggested-token.test.js b/ui/pages/confirm-add-suggested-token/confirm-add-suggested-token.test.js index db96097d9..720f0202b 100644 --- a/ui/pages/confirm-add-suggested-token/confirm-add-suggested-token.test.js +++ b/ui/pages/confirm-add-suggested-token/confirm-add-suggested-token.test.js @@ -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,37 +10,40 @@ import configureStore from '../../store/store'; import { renderWithProvider } from '../../../test/jest/rendering'; import ConfirmAddSuggestedToken from '.'; -const MOCK_SUGGESTED_ASSETS = [ - { - id: 1, - asset: { - address: '0x8b175474e89094c44da98b954eedeac495271d0a', - symbol: 'NEW', - decimals: 18, - image: 'metamark.svg', - unlisted: false, +const PENDING_APPROVALS = { + 1: { + id: '1', + origin: 'https://test-dapp.com', + time: Date.now(), + type: ApprovalType.WatchAsset, + requestData: { + asset: { + address: '0x8b175474e89094c44da98b954eedeac495271d0a', + symbol: 'NEW', + decimals: 18, + image: 'metamark.svg', + unlisted: false, + }, }, + requestState: null, }, - { - id: 2, - asset: { - address: '0xC8c77482e45F1F44dE1745F52C74426C631bDD51', - symbol: '0XYX', - decimals: 18, - image: '0x.svg', - unlisted: false, + 2: { + id: '2', + origin: 'https://test-dapp.com', + time: Date.now(), + type: ApprovalType.WatchAsset, + requestData: { + asset: { + address: '0xC8c77482e45F1F44dE1745F52C74426C631bDD51', + symbol: '0XYX', + decimals: 18, + image: '0x.svg', + unlisted: false, + }, }, + requestState: null, }, -]; - -const MOCK_PENDING_ASSET_APPROVALS = MOCK_SUGGESTED_ASSETS.map( - (requestData) => { - return { - type: ApprovalType.WatchAsset, - requestData, - }; - }, -); +}; 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); diff --git a/ui/pages/confirm-approve/confirm-approve-content/confirm-approve-content.component.test.js b/ui/pages/confirm-approve/confirm-approve-content/confirm-approve-content.component.test.js index 332d562a3..bb5cfcd36 100644 --- a/ui/pages/confirm-approve/confirm-approve-content/confirm-approve-content.component.test.js +++ b/ui/pages/confirm-approve/confirm-approve-content/confirm-approve-content.component.test.js @@ -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( diff --git a/ui/pages/home/home.component.js b/ui/pages/home/home.component.js index 78517b883..9263dff65 100644 --- a/ui/pages/home/home.component.js +++ b/ui/pages/home/home.component.js @@ -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); } diff --git a/ui/pages/home/home.container.js b/ui/pages/home/home.container.js index ce1931476..9ccfbbf90 100644 --- a/ui/pages/home/home.container.js +++ b/ui/pages/home/home.container.js @@ -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), diff --git a/ui/pages/pages.scss b/ui/pages/pages.scss index 8815f2565..ef23b24fc 100644 --- a/ui/pages/pages.scss +++ b/ui/pages/pages.scss @@ -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'; diff --git a/ui/pages/routes/routes.component.js b/ui/pages/routes/routes.component.js index 0532647db..f9adb501b 100644 --- a/ui/pages/routes/routes.component.js +++ b/ui/pages/routes/routes.component.js @@ -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 /> + 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; -} diff --git a/ui/selectors/permissions.js b/ui/selectors/permissions.js index fd7ac5767..c14b50bbf 100644 --- a/ui/selectors/permissions.js +++ b/ui/selectors/permissions.js @@ -346,7 +346,7 @@ export function getPermissionsRequests(state) { return getApprovalRequestsByType( state, ApprovalType.WalletRequestPermissions, - ).map(({ requestData }) => requestData); + )?.map(({ requestData }) => requestData); } export function getFirstPermissionRequest(state) { diff --git a/ui/selectors/selectors.js b/ui/selectors/selectors.js index 3deab6b68..ce0054015 100644 --- a/ui/selectors/selectors.js +++ b/ui/selectors/selectors.js @@ -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; diff --git a/ui/selectors/selectors.test.js b/ui/selectors/selectors.test.js index f31e73f1e..f4a0dd783 100644 --- a/ui/selectors/selectors.test.js +++ b/ui/selectors/selectors.test.js @@ -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(); diff --git a/yarn.lock b/yarn.lock index 8357b6d2c..56345e62f 100644 --- a/yarn.lock +++ b/yarn.lock @@ -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