From f31596af246e4a2e538b34d15785f78035659bb7 Mon Sep 17 00:00:00 2001 From: George Marshall Date: Sun, 9 Apr 2023 22:57:56 -0700 Subject: [PATCH 01/36] Fix hold to reveal button (#18496) --- .../app/hold-to-reveal-button/hold-to-reveal-button.js | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/ui/components/app/hold-to-reveal-button/hold-to-reveal-button.js b/ui/components/app/hold-to-reveal-button/hold-to-reveal-button.js index 16e86396a..c08df6016 100644 --- a/ui/components/app/hold-to-reveal-button/hold-to-reveal-button.js +++ b/ui/components/app/hold-to-reveal-button/hold-to-reveal-button.js @@ -170,8 +170,9 @@ export default function HoldToRevealButton({ buttonText, onLongPressed }) { onMouseDown={onMouseDown} onMouseUp={onMouseUp} className="hold-to-reveal-button__button-hold" + textProps={{ display: DISPLAY.FLEX, alignItems: AlignItems.center }} > - + {renderPreCompleteContent()} {renderPostCompleteContent()} From 826ac55c32c60f0a89afdb53e1ec2bbb43637aea Mon Sep 17 00:00:00 2001 From: dswilson4 <33137497+dswilson4@users.noreply.github.com> Date: Mon, 10 Apr 2023 01:07:07 -0700 Subject: [PATCH 02/36] initial changes to support controls over knobs in storybook (#18502) * initial changes to support controls over knobs in storybook * fix linting issue --- .../snap-ui-renderer.stories.js | 27 ++++++++++++------- 1 file changed, 18 insertions(+), 9 deletions(-) diff --git a/ui/components/app/flask/snap-ui-renderer/snap-ui-renderer.stories.js b/ui/components/app/flask/snap-ui-renderer/snap-ui-renderer.stories.js index 06c80a19a..d2fdcd7d9 100644 --- a/ui/components/app/flask/snap-ui-renderer/snap-ui-renderer.stories.js +++ b/ui/components/app/flask/snap-ui-renderer/snap-ui-renderer.stories.js @@ -1,6 +1,5 @@ import React from 'react'; import { Provider } from 'react-redux'; -import { object } from '@storybook/addon-knobs'; import { panel, text, heading, divider, copyable } from '@metamask/snaps-ui'; import configureStore from '../../../../store/store'; import testData from '../../../../../.storybook/test-data'; @@ -10,8 +9,13 @@ const store = configureStore(testData); export default { title: 'Components/App/SnapUIRenderer', - + component: SnapUIRenderer, decorators: [(story) => {story()}], + argTypes: { + data: { + control: 'object', + }, + }, }; const DATA = panel([ @@ -22,13 +26,18 @@ const DATA = panel([ copyable('Text you can copy'), ]); -export const DefaultStory = () => ( - +export const DefaultStory = (args) => ( + ); -export const ErrorStory = () => ( - +DefaultStory.args = { + data: DATA, +}; + +export const ErrorStory = (args) => ( + ); + +ErrorStory.args = { + data: 'foo', +}; From 6638d799327283f635085c2bfc933d779de602f5 Mon Sep 17 00:00:00 2001 From: legobeat <109787230+legobeat@users.noreply.github.com> Date: Mon, 10 Apr 2023 20:29:07 +0900 Subject: [PATCH 03/36] test: increase jest testTimeout 2500->5500 (#18480) --- jest.config.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/jest.config.js b/jest.config.js index ba0c5219e..21d41a96d 100644 --- a/jest.config.js +++ b/jest.config.js @@ -51,7 +51,7 @@ module.exports = { '/ui/**/*.test.(js|ts|tsx)', '/development/fitness-functions/**/*.test.(js|ts|tsx)', ], - testTimeout: 2500, + testTimeout: 5500, // We have to specify the environment we are running in, which is jsdom. The // default is 'node'. This can be modified *per file* using a comment at the // head of the file. So it may be worthwhile to switch to 'node' in any From 463fe40fdef7cfa89894dbf3e5c85c7585be8725 Mon Sep 17 00:00:00 2001 From: Amer Kadic <97883527+amerkadicE@users.noreply.github.com> Date: Tue, 11 Apr 2023 02:56:02 +0200 Subject: [PATCH 04/36] Fix Unable to determine contract standard error (#18300) Co-authored-by: legobeat <109787230+legobeat@users.noreply.github.com> --- ui/hooks/useTransactionDisplayData.js | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/ui/hooks/useTransactionDisplayData.js b/ui/hooks/useTransactionDisplayData.js index c9a3a939b..8d5f7bc3d 100644 --- a/ui/hooks/useTransactionDisplayData.js +++ b/ui/hooks/useTransactionDisplayData.js @@ -152,7 +152,7 @@ export function useTransactionDisplayData(transactionGroup) { async function getAndSetAssetDetails() { if (isTokenCategory && !token) { const assetDetails = await getAssetDetails( - recipientAddress, + to, senderAddress, initialTransaction?.txParams?.data, knownNfts, @@ -168,6 +168,7 @@ export function useTransactionDisplayData(transactionGroup) { senderAddress, initialTransaction?.txParams?.data, knownNfts, + to, ]); if (currentAssetDetails) { token = { From 54aeb1b791a9818ae825a435553257b6b0787b87 Mon Sep 17 00:00:00 2001 From: Filip Sekulic Date: Tue, 11 Apr 2023 05:19:42 +0200 Subject: [PATCH 05/36] Replace contract with third party within the token allowance flow (#18101) --- app/_locales/en/messages.json | 18 +++++++++--------- .../e2e/tests/custom-token-add-approve.spec.js | 6 +++--- test/e2e/tests/signature-request.spec.js | 4 ++-- .../signature-request.component.test.js.snap | 4 ++-- ...firm-approve-content.component.test.js.snap | 8 ++++---- .../confirm-approve-content.component.test.js | 8 ++++---- .../__snapshots__/index.test.js.snap | 2 +- ui/pages/token-allowance/index.scss | 4 ++-- .../token-allowance/token-allowance.test.js | 8 ++++---- 9 files changed, 31 insertions(+), 31 deletions(-) diff --git a/app/_locales/en/messages.json b/app/_locales/en/messages.json index 4766b0221..708197d16 100644 --- a/app/_locales/en/messages.json +++ b/app/_locales/en/messages.json @@ -815,7 +815,7 @@ "message": "Contract deployment" }, "contractDescription": { - "message": "To protect yourself against scammers, take a moment to verify contract details." + "message": "To protect yourself against scammers, take a moment to verify third-party details." }, "contractInteraction": { "message": "Contract interaction" @@ -830,10 +830,10 @@ "message": "Contract requesting signature" }, "contractRequestingSpendingCap": { - "message": "Contract requesting spending cap" + "message": "Third party requesting spending cap" }, "contractTitle": { - "message": "Contract details" + "message": "Third-party details" }, "contractToken": { "message": "Token contract" @@ -1808,14 +1808,14 @@ "message": "Your initial transaction was confirmed by the network. Click OK to go back." }, "inputLogicEmptyState": { - "message": "Only enter a number that you're comfortable with the contract spending now or in the future. You can always increase the spending cap later." + "message": "Only enter a number that you're comfortable with the third party spending now or in the future. You can always increase the spending cap later." }, "inputLogicEqualOrSmallerNumber": { - "message": "This allows the contract to spend $1 from your current balance.", + "message": "This allows the third party to spend $1 from your current balance.", "description": "$1 is the current token balance in the account and the name of the current token" }, "inputLogicHigherNumber": { - "message": "This allows the contract to spend all your token balance until it reaches the cap or you revoke the spending cap. If this is not intended, consider setting a lower spending cap." + "message": "This allows the third party to spend all your token balance until it reaches the cap or you revoke the spending cap. If this is not intended, consider setting a lower spending cap." }, "insightsFromSnap": { "message": "Insights from $1", @@ -3328,7 +3328,7 @@ "description": "$1 is a token symbol" }, "revokeSpendingCapTooltipText": { - "message": "This contract will be unable to spend any more of your current or future tokens." + "message": "This third party will be unable to spend any more of your current or future tokens." }, "rpcUrl": { "message": "New RPC URL" @@ -4660,7 +4660,7 @@ "message": "Username" }, "verifyContractDetails": { - "message": "Verify contract details" + "message": "Verify third-party details" }, "verifyThisTokenDecimalOn": { "message": "Token decimal can be found on $1", @@ -4746,7 +4746,7 @@ "message": "Warning" }, "warningTooltipText": { - "message": "$1 The contract could spend your entire token balance without further notice or consent. Protect yourself by customizing a lower spending cap.", + "message": "$1 The third party could spend your entire token balance without further notice or consent. Protect yourself by customizing a lower spending cap.", "description": "$1 is a warning icon with text 'Be careful' in 'warning' colour" }, "weak": { diff --git a/test/e2e/tests/custom-token-add-approve.spec.js b/test/e2e/tests/custom-token-add-approve.spec.js index b520f1bcf..5e1addcc5 100644 --- a/test/e2e/tests/custom-token-add-approve.spec.js +++ b/test/e2e/tests/custom-token-add-approve.spec.js @@ -119,16 +119,16 @@ describe('Create token, approve token and approve token without gas', function ( ); await driver.clickElement({ - text: 'Verify contract details', + text: 'Verify third-party details', css: '.token-allowance-container__verify-link', }); const modalTitle = await driver.waitForSelector({ - text: 'Contract details', + text: 'Third-party details', tag: 'h5', }); - assert.equal(await modalTitle.getText(), 'Contract details'); + assert.equal(await modalTitle.getText(), 'Third-party details'); await driver.clickElement({ text: 'Got it', diff --git a/test/e2e/tests/signature-request.spec.js b/test/e2e/tests/signature-request.spec.js index 9b508d37b..62ac5cd78 100644 --- a/test/e2e/tests/signature-request.spec.js +++ b/test/e2e/tests/signature-request.spec.js @@ -60,7 +60,7 @@ describe('Sign Typed Data V4 Signature Request', function () { assert.equal(await origin.getText(), 'http://127.0.0.1:8080'); verifyContractDetailsButton.click(); - await driver.findElement({ text: 'Contract details', tag: 'h5' }); + await driver.findElement({ text: 'Third-party details', tag: 'h5' }); await driver.findElement('[data-testid="recipient"]'); await driver.clickElement({ text: 'Got it', tag: 'button' }); @@ -142,7 +142,7 @@ describe('Sign Typed Data V3 Signature Request', function () { assert.equal(await origin.getText(), 'http://127.0.0.1:8080'); verifyContractDetailsButton.click(); - await driver.findElement({ text: 'Contract details', tag: 'h5' }); + await driver.findElement({ text: 'Third-party details', tag: 'h5' }); await driver.findElement('[data-testid="recipient"]'); await driver.clickElement({ text: 'Got it', tag: 'button' }); diff --git a/ui/components/app/signature-request/__snapshots__/signature-request.component.test.js.snap b/ui/components/app/signature-request/__snapshots__/signature-request.component.test.js.snap index d1e6c7708..1892c6ec4 100644 --- a/ui/components/app/signature-request/__snapshots__/signature-request.component.test.js.snap +++ b/ui/components/app/signature-request/__snapshots__/signature-request.component.test.js.snap @@ -224,7 +224,7 @@ exports[`Signature Request Component render should match snapshot when we are us
- Verify contract details + Verify third-party details
@@ -999,7 +999,7 @@ exports[`Signature Request Component render should match snapshot when we want t
- Verify contract details + Verify third-party details
diff --git a/ui/pages/confirm-approve/confirm-approve-content/__snapshots__/confirm-approve-content.component.test.js.snap b/ui/pages/confirm-approve/confirm-approve-content/__snapshots__/confirm-approve-content.component.test.js.snap index e7a265e1a..5d1310891 100644 --- a/ui/pages/confirm-approve/confirm-approve-content/__snapshots__/confirm-approve-content.component.test.js.snap +++ b/ui/pages/confirm-approve/confirm-approve-content/__snapshots__/confirm-approve-content.component.test.js.snap @@ -57,7 +57,7 @@ exports[`ConfirmApproveContent Component should render Confirm approve page corr role="button" tabindex="0" > - Verify contract details + Verify third-party details
- Verify contract details + Verify third-party details
- Verify contract details + Verify third-party details
- Verify contract details + Verify third-party details
{ 'This allows a third party to access and transfer the following NFTs without further notice until you revoke its access.', ), ).toBeInTheDocument(); - expect(queryByText('Verify contract details')).toBeInTheDocument(); + expect(queryByText('Verify third-party details')).toBeInTheDocument(); expect( queryByText( 'We were not able to estimate gas. There might be an error in the contract and this transaction may fail.', @@ -119,7 +119,7 @@ describe('ConfirmApproveContent Component', () => { 'This allows a third party to access and transfer the following NFTs without further notice until you revoke its access.', ), ).toBeInTheDocument(); - expect(queryByText('Verify contract details')).toBeInTheDocument(); + expect(queryByText('Verify third-party details')).toBeInTheDocument(); expect( queryByText( 'We were not able to estimate gas. There might be an error in the contract and this transaction may fail.', @@ -181,7 +181,7 @@ describe('ConfirmApproveContent Component', () => { 'This allows a third party to access and transfer the following NFTs without further notice until you revoke its access.', ), ).toBeInTheDocument(); - expect(queryByText('Verify contract details')).toBeInTheDocument(); + expect(queryByText('Verify third-party details')).toBeInTheDocument(); expect( queryByText( 'We were not able to estimate gas. There might be an error in the contract and this transaction may fail.', @@ -239,7 +239,7 @@ describe('ConfirmApproveContent Component', () => { 'This allows a third party to access and transfer the following NFTs without further notice until you revoke its access.', ), ).toBeInTheDocument(); - expect(queryByText('Verify contract details')).toBeInTheDocument(); + expect(queryByText('Verify third-party details')).toBeInTheDocument(); expect( queryByText( 'We were not able to estimate gas. There might be an error in the contract and this transaction may fail.', diff --git a/ui/pages/confirm-signature-request/__snapshots__/index.test.js.snap b/ui/pages/confirm-signature-request/__snapshots__/index.test.js.snap index fbf3c10c1..ab888b4cf 100644 --- a/ui/pages/confirm-signature-request/__snapshots__/index.test.js.snap +++ b/ui/pages/confirm-signature-request/__snapshots__/index.test.js.snap @@ -223,7 +223,7 @@ exports[`Signature Request Component render should match snapshot 1`] = `
- Verify contract details + Verify third-party details
diff --git a/ui/pages/token-allowance/index.scss b/ui/pages/token-allowance/index.scss index 057be3d10..836625da4 100644 --- a/ui/pages/token-allowance/index.scss +++ b/ui/pages/token-allowance/index.scss @@ -16,8 +16,8 @@ a.token-allowance-container__verify-link { width: fit-content; - margin-inline-start: 96px; - margin-inline-end: 96px; + margin-inline-start: auto; + margin-inline-end: auto; padding: 0; } diff --git a/ui/pages/token-allowance/token-allowance.test.js b/ui/pages/token-allowance/token-allowance.test.js index a438caabb..a8f22dc39 100644 --- a/ui/pages/token-allowance/token-allowance.test.js +++ b/ui/pages/token-allowance/token-allowance.test.js @@ -237,16 +237,16 @@ describe('TokenAllowancePage', () => { expect(getByText('Set a spending cap for your')).toBeInTheDocument(); }); - it('should click Verify contract details and show popup Contract details, then close popup', () => { + it('should click Verify third-party details and show popup Third-party details, then close popup', () => { const { getByText } = renderWithProvider( , store, ); - const verifyContractDetails = getByText('Verify contract details'); - fireEvent.click(verifyContractDetails); + const verifyThirdPartyDetails = getByText('Verify third-party details'); + fireEvent.click(verifyThirdPartyDetails); - expect(getByText('Contract details')).toBeInTheDocument(); + expect(getByText('Third-party details')).toBeInTheDocument(); const gotIt = getByText('Got it'); fireEvent.click(gotIt); From 0b1354e44686ebeffc01dc5d870e051bc5e2b981 Mon Sep 17 00:00:00 2001 From: vthomas13 <10986371+vthomas13@users.noreply.github.com> Date: Tue, 11 Apr 2023 07:46:11 -0400 Subject: [PATCH 06/36] adding notification 18 title (#18526) Co-authored-by: legobeat <109787230+legobeat@users.noreply.github.com> --- app/_locales/en/messages.json | 3 +++ 1 file changed, 3 insertions(+) diff --git a/app/_locales/en/messages.json b/app/_locales/en/messages.json index 708197d16..382dd314a 100644 --- a/app/_locales/en/messages.json +++ b/app/_locales/en/messages.json @@ -2486,6 +2486,9 @@ "message": "OpenSea is the first provider for this feature. More providers coming soon!", "description": "Description of a notification in the 'See What's New' popup. Describes Opensea Security Provider feature." }, + "notifications18Title": { + "message": "Stay safe with security alerts" + }, "notifications19ActionText": { "message": "Enable NFT autodetection" }, From bb0dff94434444a6c1043b5d2c835f29c9865d09 Mon Sep 17 00:00:00 2001 From: Matthew Walsh Date: Tue, 11 Apr 2023 14:18:43 +0100 Subject: [PATCH 07/36] Trigger transaction popup using ApprovalController (#18400) --- app/scripts/background.js | 2 - app/scripts/controllers/transactions/index.js | 60 +++++++- .../controllers/transactions/index.test.js | 135 ++++++++++++++++-- app/scripts/metamask-controller.js | 9 +- shared/constants/app.ts | 1 + test/data/mock-state.json | 13 +- ui/selectors/selectors.js | 7 - 7 files changed, 200 insertions(+), 27 deletions(-) diff --git a/app/scripts/background.js b/app/scripts/background.js index d92fddb81..1f73492cc 100644 --- a/app/scripts/background.js +++ b/app/scripts/background.js @@ -727,7 +727,6 @@ export function setupController( } function getUnapprovedTransactionCount() { - const unapprovedTxCount = controller.txController.getUnapprovedTxCount(); const { unapprovedDecryptMsgCount } = controller.decryptMessageManager; const { unapprovedEncryptionPublicKeyMsgCount } = controller.encryptionPublicKeyManager; @@ -736,7 +735,6 @@ export function setupController( const waitingForUnlockCount = controller.appStateController.waitingForUnlock.length; return ( - unapprovedTxCount + unapprovedDecryptMsgCount + unapprovedEncryptionPublicKeyMsgCount + pendingApprovalCount + diff --git a/app/scripts/controllers/transactions/index.js b/app/scripts/controllers/transactions/index.js index c3026e043..ce5e85d0d 100644 --- a/app/scripts/controllers/transactions/index.js +++ b/app/scripts/controllers/transactions/index.js @@ -52,7 +52,10 @@ import { determineTransactionType, isEIP1559Transaction, } from '../../../../shared/modules/transaction.utils'; -import { ORIGIN_METAMASK } from '../../../../shared/constants/app'; +import { + ORIGIN_METAMASK, + MESSAGE_TYPE, +} from '../../../../shared/constants/app'; import { calcGasTotal, getSwapsTokensReceivedFromTxMeta, @@ -156,6 +159,7 @@ export default class TransactionController extends EventEmitter { this.getAccountType = opts.getAccountType; this.getTokenStandardAndDetails = opts.getTokenStandardAndDetails; this.securityProviderRequest = opts.securityProviderRequest; + this.messagingSystem = opts.messenger; this.memStore = new ObservableStore({}); @@ -798,6 +802,7 @@ export default class TransactionController extends EventEmitter { this.txStateManager.getTransactionWithActionId(actionId); if (existingTxMeta) { this.emit('newUnapprovedTx', existingTxMeta); + this._requestApproval(existingTxMeta); existingTxMeta = await this.addTransactionGasDefaults(existingTxMeta); return existingTxMeta; } @@ -870,6 +875,7 @@ export default class TransactionController extends EventEmitter { this.addTransaction(txMeta); this.emit('newUnapprovedTx', txMeta); + this._requestApproval(txMeta); txMeta = await this.addTransactionGasDefaults(txMeta); @@ -1355,6 +1361,7 @@ export default class TransactionController extends EventEmitter { try { // approve this.txStateManager.setTxStatusApproved(txId); + this._acceptApproval(txMeta); // get next nonce const fromAddress = txMeta.txParams.from; // wait for a nonce @@ -1734,6 +1741,7 @@ export default class TransactionController extends EventEmitter { async cancelTransaction(txId, actionId) { const txMeta = this.txStateManager.getTransaction(txId); this.txStateManager.setTxStatusRejected(txId); + this._rejectApproval(txMeta); this._trackTransactionMetricsEvent( txMeta, TransactionMetaMetricsEvent.rejected, @@ -2596,4 +2604,54 @@ export default class TransactionController extends EventEmitter { }, ); } + + _requestApproval(txMeta) { + const id = this._getApprovalId(txMeta); + const { origin } = txMeta; + const type = MESSAGE_TYPE.TRANSACTION; + const requestData = { txId: txMeta.id }; + + this.messagingSystem + .call( + 'ApprovalController:addRequest', + { + id, + origin, + type, + requestData, + }, + true, + ) + .catch(() => { + // Intentionally ignored as promise not currently used + }); + } + + _acceptApproval(txMeta) { + const id = this._getApprovalId(txMeta); + + try { + this.messagingSystem.call('ApprovalController:acceptRequest', id); + } catch (error) { + log.error('Failed to accept transaction approval request', error); + } + } + + _rejectApproval(txMeta) { + const id = this._getApprovalId(txMeta); + + try { + this.messagingSystem.call( + 'ApprovalController:rejectRequest', + id, + new Error('Rejected'), + ); + } catch (error) { + log.error('Failed to reject transaction approval request', error); + } + } + + _getApprovalId(txMeta) { + return String(txMeta.id); + } } diff --git a/app/scripts/controllers/transactions/index.test.js b/app/scripts/controllers/transactions/index.test.js index c16f1fa2c..601b1fb88 100644 --- a/app/scripts/controllers/transactions/index.test.js +++ b/app/scripts/controllers/transactions/index.test.js @@ -29,7 +29,10 @@ import { GasRecommendations, } from '../../../../shared/constants/gas'; import { METAMASK_CONTROLLER_EVENTS } from '../../metamask-controller'; -import { ORIGIN_METAMASK } from '../../../../shared/constants/app'; +import { + MESSAGE_TYPE, + ORIGIN_METAMASK, +} from '../../../../shared/constants/app'; import { NetworkStatus } from '../../../../shared/constants/network'; import { TRANSACTION_ENVELOPE_TYPE_NAMES } from '../../../../shared/lib/transactions-controller-utils'; import TransactionController from '.'; @@ -52,7 +55,8 @@ describe('Transaction Controller', function () { fromAccount, fragmentExists, networkStatusStore, - getCurrentChainId; + getCurrentChainId, + messengerMock; beforeEach(function () { fragmentExists = false; @@ -76,6 +80,7 @@ describe('Transaction Controller', function () { blockTrackerStub.getLatestBlock = noop; getCurrentChainId = sinon.stub().callsFake(() => currentChainId); + messengerMock = { call: sinon.stub().returns(Promise.resolve()) }; txController = new TransactionController({ provider, @@ -108,6 +113,7 @@ describe('Transaction Controller', function () { getAccountType: () => 'MetaMask', getDeviceModel: () => 'N/A', securityProviderRequest: () => undefined, + messenger: messengerMock, }); txController.nonceTracker.getNonceLock = () => Promise.resolve({ nextNonce: 0, releaseLock: noop }); @@ -489,6 +495,67 @@ describe('Transaction Controller', function () { { message: 'MetaMask is having trouble connecting to the network' }, ); }); + + it('should create an approval request', async function () { + const txMeta = await txController.addUnapprovedTransaction( + undefined, + { + from: selectedAddress, + to: recipientAddress, + }, + ORIGIN_METAMASK, + ); + + assert.equal(messengerMock.call.callCount, 1); + assert.deepEqual(messengerMock.call.getCall(0).args, [ + 'ApprovalController:addRequest', + { + id: String(txMeta.id), + origin: ORIGIN_METAMASK, + requestData: { txId: txMeta.id }, + type: MESSAGE_TYPE.TRANSACTION, + }, + true, // Show popup + ]); + }); + + it('should still create an approval request when called twice with same actionId', async function () { + await txController.addUnapprovedTransaction( + undefined, + { + from: selectedAddress, + to: recipientAddress, + }, + ORIGIN_METAMASK, + undefined, + undefined, + '12345', + ); + + const secondTxMeta = await txController.addUnapprovedTransaction( + undefined, + { + from: selectedAddress, + to: recipientAddress, + }, + undefined, + undefined, + undefined, + '12345', + ); + + assert.equal(messengerMock.call.callCount, 2); + assert.deepEqual(messengerMock.call.getCall(1).args, [ + 'ApprovalController:addRequest', + { + id: String(secondTxMeta.id), + origin: ORIGIN_METAMASK, + requestData: { txId: secondTxMeta.id }, + type: MESSAGE_TYPE.TRANSACTION, + }, + true, // Show popup + ]); + }); }); describe('#createCancelTransaction', function () { @@ -997,9 +1064,11 @@ describe('Transaction Controller', function () { }); describe('#approveTransaction', function () { - it('does not overwrite set values', async function () { - const originalValue = '0x01'; - const txMeta = { + let originalValue, txMeta, signStub, pubStub; + + beforeEach(function () { + originalValue = '0x01'; + txMeta = { id: '1', status: TransactionStatus.unapproved, metamaskNetworkId: currentNetworkId, @@ -1019,17 +1088,22 @@ describe('Transaction Controller', function () { providerResultStub.eth_gasPrice = wrongValue; providerResultStub.eth_estimateGas = '0x5209'; - const signStub = sinon + signStub = sinon .stub(txController, 'signTransaction') .callsFake(() => Promise.resolve()); - const pubStub = sinon - .stub(txController, 'publishTransaction') - .callsFake(() => { - txController.setTxHash('1', originalValue); - txController.txStateManager.setTxStatusSubmitted('1'); - }); + pubStub = sinon.stub(txController, 'publishTransaction').callsFake(() => { + txController.setTxHash('1', originalValue); + txController.txStateManager.setTxStatusSubmitted('1'); + }); + }); + afterEach(function () { + signStub.restore(); + pubStub.restore(); + }); + + it('does not overwrite set values', async function () { await txController.approveTransaction(txMeta.id); const result = txController.txStateManager.getTransaction(txMeta.id); const params = result.txParams; @@ -1042,8 +1116,21 @@ describe('Transaction Controller', function () { TransactionStatus.submitted, 'should have reached the submitted status.', ); - signStub.restore(); - pubStub.restore(); + }); + + it('should accept the approval request', async function () { + await txController.approveTransaction(txMeta.id); + + assert.equal(messengerMock.call.callCount, 1); + assert.deepEqual(messengerMock.call.getCall(0).args, [ + 'ApprovalController:acceptRequest', + txMeta.id, + ]); + }); + + it('should not throw if accepting approval request throws', async function () { + messengerMock.call.throws(); + await txController.approveTransaction(txMeta.id); }); }); @@ -1108,7 +1195,7 @@ describe('Transaction Controller', function () { }); describe('#cancelTransaction', function () { - it('should emit a status change to rejected', function (done) { + beforeEach(function () { txController.txStateManager._addTransactionsToState([ { id: 0, @@ -1181,7 +1268,9 @@ describe('Transaction Controller', function () { history: [{}], }, ]); + }); + it('should emit a status change to rejected', function (done) { txController.once('tx:status-update', (txId, status) => { try { assert.equal( @@ -1198,6 +1287,22 @@ describe('Transaction Controller', function () { txController.cancelTransaction(0); }); + + it('should reject the approval request', function () { + txController.cancelTransaction(0); + + assert.equal(messengerMock.call.callCount, 1); + assert.deepEqual(messengerMock.call.getCall(0).args, [ + 'ApprovalController:rejectRequest', + '0', + new Error('Rejected'), + ]); + }); + + it('should not throw if rejecting approval request throws', async function () { + messengerMock.call.throws(); + txController.cancelTransaction(0); + }); }); describe('#createSpeedUpTransaction', function () { diff --git a/app/scripts/metamask-controller.js b/app/scripts/metamask-controller.js index 28a7311c0..c1d8216a0 100644 --- a/app/scripts/metamask-controller.js +++ b/app/scripts/metamask-controller.js @@ -1007,8 +1007,15 @@ export default class MetamaskController extends EventEmitter { getDeviceModel: this.getDeviceModel.bind(this), getTokenStandardAndDetails: this.getTokenStandardAndDetails.bind(this), securityProviderRequest: this.securityProviderRequest.bind(this), + messenger: this.controllerMessenger.getRestricted({ + name: 'TransactionController', + allowedActions: [ + `${this.approvalController.name}:addRequest`, + `${this.approvalController.name}:acceptRequest`, + `${this.approvalController.name}:rejectRequest`, + ], + }), }); - this.txController.on('newUnapprovedTx', () => opts.showUserConfirmation()); this.txController.on(`tx:status-update`, async (txId, status) => { if ( diff --git a/shared/constants/app.ts b/shared/constants/app.ts index 54a39496a..c8be0573e 100644 --- a/shared/constants/app.ts +++ b/shared/constants/app.ts @@ -53,6 +53,7 @@ export const MESSAGE_TYPE = { PERSONAL_SIGN: 'personal_sign', SEND_METADATA: 'metamask_sendDomainMetadata', SWITCH_ETHEREUM_CHAIN: 'wallet_switchEthereumChain', + TRANSACTION: 'transaction', WALLET_REQUEST_PERMISSIONS: 'wallet_requestPermissions', WATCH_ASSET: 'wallet_watchAsset', WATCH_ASSET_LEGACY: 'metamask_watchAsset', diff --git a/test/data/mock-state.json b/test/data/mock-state.json index 93d4515f4..41d995b9e 100644 --- a/test/data/mock-state.json +++ b/test/data/mock-state.json @@ -1535,7 +1535,18 @@ "origin": "tmashuang.github.io" } ], - "desktopEnabled": false + "desktopEnabled": false, + "pendingApprovals": { + "testApprovalId": { + "id": "testApprovalId", + "time": 1528133319641, + "origin": "metamask", + "type": "transaction", + "requestData": { "txId": "testTransactionId" }, + "requestState": { "test": "value" } + } + }, + "pendingApprovalCount": 1 }, "send": { "amountMode": "INPUT", diff --git a/ui/selectors/selectors.js b/ui/selectors/selectors.js index bbe5e53e8..b4e04c7f6 100644 --- a/ui/selectors/selectors.js +++ b/ui/selectors/selectors.js @@ -484,21 +484,14 @@ export function getCurrentCurrency(state) { export function getTotalUnapprovedCount(state) { const { - unapprovedMsgCount = 0, - unapprovedPersonalMsgCount = 0, unapprovedDecryptMsgCount = 0, unapprovedEncryptionPublicKeyMsgCount = 0, - unapprovedTypedMessagesCount = 0, pendingApprovalCount = 0, } = state.metamask; return ( - unapprovedMsgCount + - unapprovedPersonalMsgCount + unapprovedDecryptMsgCount + unapprovedEncryptionPublicKeyMsgCount + - unapprovedTypedMessagesCount + - getUnapprovedTxCount(state) + pendingApprovalCount + getSuggestedAssetCount(state) ); From 16bfa1f728db7d4a5005aafd0104742e56c9ec70 Mon Sep 17 00:00:00 2001 From: Mark Stacey Date: Tue, 11 Apr 2023 13:11:26 -0230 Subject: [PATCH 08/36] Update the gas fee controller from v1 to v3 (#18466) The gas fee controller has been updated from v1 to v3. The breaking changes for v2 and v3 related to the `@metamask/network-controller` type that was referenced. They don't affect the extension's usage of this package, so they are non-breaking for the extension. Co-authored-by: legobeat <109787230+legobeat@users.noreply.github.com> --- lavamoat/browserify/beta/policy.json | 7 +--- lavamoat/browserify/desktop/policy.json | 7 +--- lavamoat/browserify/flask/policy.json | 7 +--- lavamoat/browserify/main/policy.json | 7 +--- package.json | 2 +- yarn.lock | 50 +++++++++++++------------ 6 files changed, 31 insertions(+), 49 deletions(-) diff --git a/lavamoat/browserify/beta/policy.json b/lavamoat/browserify/beta/policy.json index 7d359e522..ed13fb1b9 100644 --- a/lavamoat/browserify/beta/policy.json +++ b/lavamoat/browserify/beta/policy.json @@ -1193,7 +1193,7 @@ "setInterval": true }, "packages": { - "@metamask/gas-fee-controller>@metamask/base-controller": true, + "@metamask/base-controller": true, "@metamask/gas-fee-controller>@metamask/controller-utils": true, "eth-query": true, "ethereumjs-util": true, @@ -1201,11 +1201,6 @@ "uuid": true } }, - "@metamask/gas-fee-controller>@metamask/base-controller": { - "packages": { - "immer": true - } - }, "@metamask/gas-fee-controller>@metamask/controller-utils": { "globals": { "console.error": true, diff --git a/lavamoat/browserify/desktop/policy.json b/lavamoat/browserify/desktop/policy.json index 8fc8f7bcd..0d9164963 100644 --- a/lavamoat/browserify/desktop/policy.json +++ b/lavamoat/browserify/desktop/policy.json @@ -1265,7 +1265,7 @@ "setInterval": true }, "packages": { - "@metamask/gas-fee-controller>@metamask/base-controller": true, + "@metamask/base-controller": true, "@metamask/gas-fee-controller>@metamask/controller-utils": true, "eth-query": true, "ethereumjs-util": true, @@ -1273,11 +1273,6 @@ "uuid": true } }, - "@metamask/gas-fee-controller>@metamask/base-controller": { - "packages": { - "immer": true - } - }, "@metamask/gas-fee-controller>@metamask/controller-utils": { "globals": { "console.error": true, diff --git a/lavamoat/browserify/flask/policy.json b/lavamoat/browserify/flask/policy.json index 8fc8f7bcd..0d9164963 100644 --- a/lavamoat/browserify/flask/policy.json +++ b/lavamoat/browserify/flask/policy.json @@ -1265,7 +1265,7 @@ "setInterval": true }, "packages": { - "@metamask/gas-fee-controller>@metamask/base-controller": true, + "@metamask/base-controller": true, "@metamask/gas-fee-controller>@metamask/controller-utils": true, "eth-query": true, "ethereumjs-util": true, @@ -1273,11 +1273,6 @@ "uuid": true } }, - "@metamask/gas-fee-controller>@metamask/base-controller": { - "packages": { - "immer": true - } - }, "@metamask/gas-fee-controller>@metamask/controller-utils": { "globals": { "console.error": true, diff --git a/lavamoat/browserify/main/policy.json b/lavamoat/browserify/main/policy.json index 7d359e522..ed13fb1b9 100644 --- a/lavamoat/browserify/main/policy.json +++ b/lavamoat/browserify/main/policy.json @@ -1193,7 +1193,7 @@ "setInterval": true }, "packages": { - "@metamask/gas-fee-controller>@metamask/base-controller": true, + "@metamask/base-controller": true, "@metamask/gas-fee-controller>@metamask/controller-utils": true, "eth-query": true, "ethereumjs-util": true, @@ -1201,11 +1201,6 @@ "uuid": true } }, - "@metamask/gas-fee-controller>@metamask/base-controller": { - "packages": { - "immer": true - } - }, "@metamask/gas-fee-controller>@metamask/controller-utils": { "globals": { "console.error": true, diff --git a/package.json b/package.json index 2b1815983..47cb164e9 100644 --- a/package.json +++ b/package.json @@ -242,7 +242,7 @@ "@metamask/eth-ledger-bridge-keyring": "^0.13.0", "@metamask/eth-token-tracker": "^4.0.0", "@metamask/etherscan-link": "^2.2.0", - "@metamask/gas-fee-controller": "^1.0.0", + "@metamask/gas-fee-controller": "^3.0.0", "@metamask/jazzicon": "^2.0.0", "@metamask/key-tree": "^7.0.0", "@metamask/logo": "^3.1.1", diff --git a/yarn.lock b/yarn.lock index 4ceafeafb..28686cc9d 100644 --- a/yarn.lock +++ b/yarn.lock @@ -3984,13 +3984,13 @@ __metadata: languageName: node linkType: hard -"@metamask/gas-fee-controller@npm:^1.0.0": - version: 1.0.0 - resolution: "@metamask/gas-fee-controller@npm:1.0.0" +"@metamask/gas-fee-controller@npm:^3.0.0": + version: 3.0.0 + resolution: "@metamask/gas-fee-controller@npm:3.0.0" dependencies: - "@metamask/base-controller": ~1.0.0 - "@metamask/controller-utils": ~1.0.0 - "@metamask/network-controller": ~1.0.0 + "@metamask/base-controller": ^1.1.2 + "@metamask/controller-utils": ^2.0.0 + "@metamask/network-controller": ^3.0.0 "@types/uuid": ^8.3.0 babel-runtime: ^6.26.0 eth-query: ^2.1.2 @@ -3998,7 +3998,9 @@ __metadata: ethjs-unit: ^0.1.6 immer: ^9.0.6 uuid: ^8.3.2 - checksum: fef5255532a6cd5325ddfbbfec11140e6629c011a8cc6b126672ef7a6e93a327d059935cdc6fc7089562f3277fb70541b5ea54cd31c0e5b350ceebbe73d5d59f + peerDependencies: + "@metamask/network-controller": ^3.0.0 + checksum: 8cdd43a265094dd5e41f0094c278cde351d290446711e6b39de26f842faa993c050e5506cafe8d1c2fb0c4ee3f0f97c5af5fa6528de10e76d071b56fb9673da8 languageName: node linkType: hard @@ -4074,6 +4076,22 @@ __metadata: languageName: node linkType: hard +"@metamask/network-controller@npm:^3.0.0": + version: 3.0.0 + resolution: "@metamask/network-controller@npm:3.0.0" + dependencies: + "@metamask/base-controller": ^1.1.2 + "@metamask/controller-utils": ^2.0.0 + async-mutex: ^0.2.6 + babel-runtime: ^6.26.0 + eth-json-rpc-infura: ^5.1.0 + eth-query: ^2.1.2 + immer: ^9.0.6 + web3-provider-engine: ^16.0.3 + checksum: 3ae56a252c11dbd6dc843f9db8b30768d2475afd499c99bdccdc850517031b447bab9ca4f6647da7e64c7a0efd61d029f59a89e4ec702e34a99733dd8e7f93ff + languageName: node + linkType: hard + "@metamask/network-controller@npm:^4.0.0": version: 4.0.0 resolution: "@metamask/network-controller@npm:4.0.0" @@ -4090,22 +4108,6 @@ __metadata: languageName: node linkType: hard -"@metamask/network-controller@npm:~1.0.0": - version: 1.0.0 - resolution: "@metamask/network-controller@npm:1.0.0" - dependencies: - "@metamask/base-controller": ~1.0.0 - "@metamask/controller-utils": ~1.0.0 - async-mutex: ^0.2.6 - babel-runtime: ^6.26.0 - eth-json-rpc-infura: ^5.1.0 - eth-query: ^2.1.2 - immer: ^9.0.6 - web3-provider-engine: ^16.0.3 - checksum: a138943fecc27630e6fe392b9d237405e61b55e17b9dcfc7c434ccc59582fc775aec54e765c2e98f2b1579f760c7d163156450184172128079ce3c4d8e4bc725 - languageName: node - linkType: hard - "@metamask/notification-controller@npm:^1.0.0": version: 1.0.0 resolution: "@metamask/notification-controller@npm:1.0.0" @@ -24299,7 +24301,7 @@ __metadata: "@metamask/eth-token-tracker": ^4.0.0 "@metamask/etherscan-link": ^2.2.0 "@metamask/forwarder": ^1.1.0 - "@metamask/gas-fee-controller": ^1.0.0 + "@metamask/gas-fee-controller": ^3.0.0 "@metamask/jazzicon": ^2.0.0 "@metamask/key-tree": ^7.0.0 "@metamask/logo": ^3.1.1 From 3577d3545f79ed0ffa8a7ad8a14cd177c7898293 Mon Sep 17 00:00:00 2001 From: Elliot Winkler Date: Tue, 11 Apr 2023 10:07:24 -0600 Subject: [PATCH 09/36] Convert NetworkController to TS (#18358) Converting this controller to TypeScript furthers the goal of getting this whole codebase converted, of course, but it also helps in comparing the differences between this version of the NetworkController and the version in the `core` repo more easily, which will ultimately help us in coalescing the two implementations. Co-authored-by: Mark Stacey Co-authored-by: legobeat <109787230+legobeat@users.noreply.github.com> --- app/scripts/controllers/detect-tokens.test.js | 4 +- app/scripts/controllers/network/index.js | 1 - app/scripts/controllers/network/index.ts | 1 + .../controllers/network/network-controller.js | 679 ---------- .../network/network-controller.test.js | 10 +- .../controllers/network/network-controller.ts | 1171 +++++++++++++++++ app/scripts/controllers/preferences.test.js | 2 +- app/scripts/metamask-controller.js | 27 +- package.json | 2 +- shared/constants/network.ts | 2 +- types/eth-query.d.ts | 50 + yarn.lock | 10 +- 12 files changed, 1251 insertions(+), 708 deletions(-) delete mode 100644 app/scripts/controllers/network/index.js create mode 100644 app/scripts/controllers/network/index.ts delete mode 100644 app/scripts/controllers/network/network-controller.js create mode 100644 app/scripts/controllers/network/network-controller.ts create mode 100644 types/eth-query.d.ts diff --git a/app/scripts/controllers/detect-tokens.test.js b/app/scripts/controllers/detect-tokens.test.js index c90ebc590..10db93359 100644 --- a/app/scripts/controllers/detect-tokens.test.js +++ b/app/scripts/controllers/detect-tokens.test.js @@ -13,7 +13,7 @@ import { convertHexToDecimal } from '@metamask/controller-utils'; import { NETWORK_TYPES } from '../../../shared/constants/network'; import { toChecksumHexAddress } from '../../../shared/modules/hexstring-utils'; import DetectTokensController from './detect-tokens'; -import NetworkController, { NetworkControllerEventTypes } from './network'; +import { NetworkController, NetworkControllerEventType } from './network'; import PreferencesController from './preferences'; describe('DetectTokensController', function () { @@ -248,7 +248,7 @@ describe('DetectTokensController', function () { ), onNetworkStateChange: (cb) => networkControllerMessenger.subscribe( - NetworkControllerEventTypes.NetworkDidChange, + NetworkControllerEventType.NetworkDidChange, () => { const networkState = network.store.getState(); const modifiedNetworkState = { diff --git a/app/scripts/controllers/network/index.js b/app/scripts/controllers/network/index.js deleted file mode 100644 index b91e16698..000000000 --- a/app/scripts/controllers/network/index.js +++ /dev/null @@ -1 +0,0 @@ -export { default, NetworkControllerEventTypes } from './network-controller'; diff --git a/app/scripts/controllers/network/index.ts b/app/scripts/controllers/network/index.ts new file mode 100644 index 000000000..de3e59ea1 --- /dev/null +++ b/app/scripts/controllers/network/index.ts @@ -0,0 +1 @@ +export * from './network-controller'; diff --git a/app/scripts/controllers/network/network-controller.js b/app/scripts/controllers/network/network-controller.js deleted file mode 100644 index 4449bc683..000000000 --- a/app/scripts/controllers/network/network-controller.js +++ /dev/null @@ -1,679 +0,0 @@ -import { strict as assert } from 'assert'; -import EventEmitter from 'events'; -import { ComposedStore, ObservableStore } from '@metamask/obs-store'; -import log from 'loglevel'; -import { - createSwappableProxy, - createEventEmitterProxy, -} from '@metamask/swappable-obj-proxy'; -import EthQuery from 'eth-query'; -// ControllerMessenger is referred to in the JSDocs -// eslint-disable-next-line no-unused-vars -import { ControllerMessenger } from '@metamask/base-controller'; -import { v4 as random } from 'uuid'; -import { hasProperty, isPlainObject } from '@metamask/utils'; -import { errorCodes } from 'eth-rpc-errors'; -import { - INFURA_PROVIDER_TYPES, - BUILT_IN_NETWORKS, - INFURA_BLOCKED_KEY, - TEST_NETWORK_TICKER_MAP, - CHAIN_IDS, - NETWORK_TYPES, - NetworkStatus, -} from '../../../../shared/constants/network'; -import { - isPrefixedFormattedHexString, - isSafeChainId, -} from '../../../../shared/modules/network.utils'; -import { MetaMetricsEventCategory } from '../../../../shared/constants/metametrics'; -import { createNetworkClient } from './create-network-client'; - -/** - * @typedef {object} NetworkConfiguration - * @property {string} rpcUrl - RPC target URL. - * @property {string} chainId - Network ID as per EIP-155 - * @property {string} ticker - Currency ticker. - * @property {object} [rpcPrefs] - Personalized preferences. - * @property {string} [nickname] - Personalized network name. - */ - -function buildDefaultProviderConfigState() { - if (process.env.IN_TEST) { - return { - type: NETWORK_TYPES.RPC, - rpcUrl: 'http://localhost:8545', - chainId: '0x539', - nickname: 'Localhost 8545', - ticker: 'ETH', - }; - } else if ( - process.env.METAMASK_DEBUG || - process.env.METAMASK_ENV === 'test' - ) { - return { - type: NETWORK_TYPES.GOERLI, - chainId: CHAIN_IDS.GOERLI, - ticker: TEST_NETWORK_TICKER_MAP.GOERLI, - }; - } - - return { - type: NETWORK_TYPES.MAINNET, - chainId: CHAIN_IDS.MAINNET, - ticker: 'ETH', - }; -} - -function buildDefaultNetworkIdState() { - return null; -} - -function buildDefaultNetworkStatusState() { - return NetworkStatus.Unknown; -} - -function buildDefaultNetworkDetailsState() { - return { - EIPS: { - 1559: undefined, - }, - }; -} - -function buildDefaultNetworkConfigurationsState() { - return {}; -} - -/** - * The name of the controller. - */ -const name = 'NetworkController'; - -/** - * The set of event types that this controller can publish via its messenger. - */ -export const NetworkControllerEventTypes = { - /** - * Fired after the current network is changed. - */ - NetworkDidChange: `${name}:networkDidChange`, - /** - * Fired when there is a request to change the current network, but no state - * changes have occurred yet. - */ - NetworkWillChange: `${name}:networkWillChange`, - /** - * Fired after the network is changed to an Infura network, but when Infura - * returns an error denying support for the user's location. - */ - InfuraIsBlocked: `${name}:infuraIsBlocked`, - /** - * Fired after the network is changed to an Infura network and Infura does not - * return an error denying support for the user's location, or after the - * network is changed to a custom network. - */ - InfuraIsUnblocked: `${name}:infuraIsUnblocked`, -}; - -export default class NetworkController extends EventEmitter { - /** - * Construct a NetworkController. - * - * @param {object} options - Options for this controller. - * @param {ControllerMessenger} options.messenger - The controller messenger. - * @param {object} [options.state] - Initial controller state. - * @param {string} [options.infuraProjectId] - The Infura project ID. - * @param {string} [options.trackMetaMetricsEvent] - A method to forward events to the MetaMetricsController - */ - constructor({ - messenger, - state = {}, - infuraProjectId, - trackMetaMetricsEvent, - } = {}) { - super(); - - this.messenger = messenger; - - // create stores - this.providerStore = new ObservableStore( - state.provider || buildDefaultProviderConfigState(), - ); - this.previousProviderStore = new ObservableStore( - this.providerStore.getState(), - ); - this.networkIdStore = new ObservableStore(buildDefaultNetworkIdState()); - this.networkStatusStore = new ObservableStore( - buildDefaultNetworkStatusState(), - ); - // We need to keep track of a few details about the current network. - // Ideally we'd merge this.networkStatusStore with this new store, but doing - // so will require a decent sized refactor of how we're accessing network - // state. Currently this is only used for detecting EIP-1559 support but can - // be extended to track other network details. - this.networkDetails = new ObservableStore( - state.networkDetails || buildDefaultNetworkDetailsState(), - ); - - this.networkConfigurationsStore = new ObservableStore( - state.networkConfigurations || buildDefaultNetworkConfigurationsState(), - ); - - this.store = new ComposedStore({ - provider: this.providerStore, - previousProviderStore: this.previousProviderStore, - networkId: this.networkIdStore, - networkStatus: this.networkStatusStore, - networkDetails: this.networkDetails, - networkConfigurations: this.networkConfigurationsStore, - }); - - // provider and block tracker - this._provider = null; - this._blockTracker = null; - - // provider and block tracker proxies - because the network changes - this._providerProxy = null; - this._blockTrackerProxy = null; - - if (!infuraProjectId || typeof infuraProjectId !== 'string') { - throw new Error('Invalid Infura project ID'); - } - this._infuraProjectId = infuraProjectId; - - this._trackMetaMetricsEvent = trackMetaMetricsEvent; - } - - /** - * Destroy the network controller, stopping any ongoing polling. - * - * In-progress requests will not be aborted. - */ - async destroy() { - await this._blockTracker?.destroy(); - } - - async initializeProvider() { - const { type, rpcUrl, chainId } = this.providerStore.getState(); - this._configureProvider({ type, rpcUrl, chainId }); - await this.lookupNetwork(); - } - - // return the proxies so the references will always be good - getProviderAndBlockTracker() { - const provider = this._providerProxy; - const blockTracker = this._blockTrackerProxy; - return { provider, blockTracker }; - } - - /** - * Determines whether the network supports EIP-1559 by checking whether the - * latest block has a `baseFeePerGas` property, then updates state - * appropriately. - * - * @returns {Promise} A promise that resolves to true if the network - * supports EIP-1559 and false otherwise. - */ - async getEIP1559Compatibility() { - const { EIPS } = this.networkDetails.getState(); - // NOTE: This isn't necessary anymore because the block cache middleware - // already prevents duplicate requests from taking place - if (EIPS[1559] !== undefined) { - return EIPS[1559]; - } - const supportsEIP1559 = await this._determineEIP1559Compatibility(); - this.networkDetails.updateState({ - EIPS: { - ...this.networkDetails.getState().EIPS, - 1559: supportsEIP1559, - }, - }); - return supportsEIP1559; - } - - /** - * Captures information about the currently selected network — namely, - * the network ID and whether the network supports EIP-1559 — and then uses - * the results of these requests to determine the status of the network. - */ - async lookupNetwork() { - const { chainId, type } = this.providerStore.getState(); - let networkChanged = false; - let networkId; - let supportsEIP1559; - let networkStatus; - - if (!this._provider) { - log.warn( - 'NetworkController - lookupNetwork aborted due to missing provider', - ); - return; - } - - if (!chainId) { - log.warn( - 'NetworkController - lookupNetwork aborted due to missing chainId', - ); - this._resetNetworkId(); - this._resetNetworkStatus(); - this._resetNetworkDetails(); - return; - } - - const isInfura = INFURA_PROVIDER_TYPES.includes(type); - - const listener = () => { - networkChanged = true; - this.messenger.unsubscribe( - NetworkControllerEventTypes.NetworkDidChange, - listener, - ); - }; - this.messenger.subscribe( - NetworkControllerEventTypes.NetworkDidChange, - listener, - ); - - try { - const results = await Promise.all([ - this._getNetworkId(), - this._determineEIP1559Compatibility(), - ]); - networkId = results[0]; - supportsEIP1559 = results[1]; - networkStatus = NetworkStatus.Available; - } catch (error) { - if (hasProperty(error, 'code')) { - let responseBody; - try { - responseBody = JSON.parse(error.message); - } catch { - // error.message must not be JSON - } - - if ( - isPlainObject(responseBody) && - responseBody.error === INFURA_BLOCKED_KEY - ) { - networkStatus = NetworkStatus.Blocked; - } else if (error.code === errorCodes.rpc.internal) { - networkStatus = NetworkStatus.Unknown; - } else { - networkStatus = NetworkStatus.Unavailable; - } - } else { - log.warn( - 'NetworkController - could not determine network status', - error, - ); - networkStatus = NetworkStatus.Unknown; - } - } - - if (networkChanged) { - // If the network has changed, then `lookupNetwork` either has been or is - // in the process of being called, so we don't need to go further. - return; - } - this.messenger.unsubscribe( - NetworkControllerEventTypes.NetworkDidChange, - listener, - ); - - this.networkStatusStore.putState(networkStatus); - - if (networkStatus === NetworkStatus.Available) { - this.networkIdStore.putState(networkId); - this.networkDetails.updateState({ - EIPS: { - ...this.networkDetails.getState().EIPS, - 1559: supportsEIP1559, - }, - }); - } else { - this._resetNetworkId(); - this._resetNetworkDetails(); - } - - if (isInfura) { - if (networkStatus === NetworkStatus.Available) { - this.messenger.publish(NetworkControllerEventTypes.InfuraIsUnblocked); - } else if (networkStatus === NetworkStatus.Blocked) { - this.messenger.publish(NetworkControllerEventTypes.InfuraIsBlocked); - } - } else { - // Always publish infuraIsUnblocked regardless of network status to - // prevent consumers from being stuck in a blocked state if they were - // previously connected to an Infura network that was blocked - this.messenger.publish(NetworkControllerEventTypes.InfuraIsUnblocked); - } - } - - /** - * A method for setting the currently selected network provider by networkConfigurationId. - * - * @param {string} networkConfigurationId - the universal unique identifier that corresponds to the network configuration to set as active. - * @returns {string} The rpcUrl of the network that was just set as active - */ - setActiveNetwork(networkConfigurationId) { - const targetNetwork = - this.networkConfigurationsStore.getState()[networkConfigurationId]; - - if (!targetNetwork) { - throw new Error( - `networkConfigurationId ${networkConfigurationId} does not match a configured networkConfiguration`, - ); - } - - this._setProviderConfig({ - type: NETWORK_TYPES.RPC, - ...targetNetwork, - }); - - return targetNetwork.rpcUrl; - } - - setProviderType(type) { - assert.notStrictEqual( - type, - NETWORK_TYPES.RPC, - `NetworkController - cannot call "setProviderType" with type "${NETWORK_TYPES.RPC}". Use "setActiveNetwork"`, - ); - assert.ok( - INFURA_PROVIDER_TYPES.includes(type), - `Unknown Infura provider type "${type}".`, - ); - const { chainId, ticker, blockExplorerUrl } = BUILT_IN_NETWORKS[type]; - this._setProviderConfig({ - type, - rpcUrl: '', - chainId, - ticker: ticker ?? 'ETH', - nickname: '', - rpcPrefs: { blockExplorerUrl }, - }); - } - - resetConnection() { - this._setProviderConfig(this.providerStore.getState()); - } - - rollbackToPreviousProvider() { - const config = this.previousProviderStore.getState(); - this.providerStore.putState(config); - this._switchNetwork(config); - } - - // - // Private - // - - /** - * Method to return the latest block for the current network - * - * @returns {object} Block header - */ - _getLatestBlock() { - const { provider } = this.getProviderAndBlockTracker(); - const ethQuery = new EthQuery(provider); - - return new Promise((resolve, reject) => { - ethQuery.sendAsync( - { method: 'eth_getBlockByNumber', params: ['latest', false] }, - (error, result) => { - if (error) { - reject(error); - } else { - resolve(result); - } - }, - ); - }); - } - - /** - * Get the network ID for the current selected network - * - * @returns {string} The network ID for the current network. - */ - async _getNetworkId() { - const { provider } = this.getProviderAndBlockTracker(); - const ethQuery = new EthQuery(provider); - - return await new Promise((resolve, reject) => { - ethQuery.sendAsync({ method: 'net_version' }, (error, result) => { - if (error) { - reject(error); - } else { - resolve(result); - } - }); - }); - } - - /** - * Clears the stored network ID. - */ - _resetNetworkId() { - this.networkIdStore.putState(buildDefaultNetworkIdState()); - } - - /** - * Resets network status to the default ("unknown"). - */ - _resetNetworkStatus() { - this.networkStatusStore.putState(buildDefaultNetworkStatusState()); - } - - /** - * Clears details previously stored for the network. - */ - _resetNetworkDetails() { - this.networkDetails.putState(buildDefaultNetworkDetailsState()); - } - - /** - * Sets the provider config and switches the network. - * - * @param config - */ - _setProviderConfig(config) { - this.previousProviderStore.putState(this.providerStore.getState()); - this.providerStore.putState(config); - this._switchNetwork(config); - } - - /** - * Retrieves the latest block from the currently selected network; if the - * block has a `baseFeePerGas` property, then we know that the network - * supports EIP-1559; otherwise it doesn't. - * - * @returns {Promise} A promise that resolves to true if the network - * supports EIP-1559 and false otherwise. - */ - async _determineEIP1559Compatibility() { - const latestBlock = await this._getLatestBlock(); - return latestBlock && latestBlock.baseFeePerGas !== undefined; - } - - _switchNetwork(opts) { - this.messenger.publish(NetworkControllerEventTypes.NetworkWillChange); - this._resetNetworkId(); - this._resetNetworkStatus(); - this._resetNetworkDetails(); - this._configureProvider(opts); - this.messenger.publish(NetworkControllerEventTypes.NetworkDidChange); - this.lookupNetwork(); - } - - _configureProvider({ type, rpcUrl, chainId }) { - // infura type-based endpoints - const isInfura = INFURA_PROVIDER_TYPES.includes(type); - if (isInfura) { - this._configureInfuraProvider({ - type, - infuraProjectId: this._infuraProjectId, - }); - // url-based rpc endpoints - } else if (type === NETWORK_TYPES.RPC) { - this._configureStandardProvider(rpcUrl, chainId); - } else { - throw new Error( - `NetworkController - _configureProvider - unknown type "${type}"`, - ); - } - } - - _configureInfuraProvider({ type, infuraProjectId }) { - log.info('NetworkController - configureInfuraProvider', type); - const { provider, blockTracker } = createNetworkClient({ - network: type, - infuraProjectId, - type: 'infura', - }); - this._setProviderAndBlockTracker({ provider, blockTracker }); - } - - _configureStandardProvider(rpcUrl, chainId) { - log.info('NetworkController - configureStandardProvider', rpcUrl); - const { provider, blockTracker } = createNetworkClient({ - chainId, - rpcUrl, - type: 'custom', - }); - this._setProviderAndBlockTracker({ provider, blockTracker }); - } - - _setProviderAndBlockTracker({ provider, blockTracker }) { - // update or initialize proxies - if (this._providerProxy) { - this._providerProxy.setTarget(provider); - } else { - this._providerProxy = createSwappableProxy(provider); - } - if (this._blockTrackerProxy) { - this._blockTrackerProxy.setTarget(blockTracker); - } else { - this._blockTrackerProxy = createEventEmitterProxy(blockTracker, { - eventFilter: 'skipInternal', - }); - } - // set new provider and blockTracker - this._provider = provider; - this._blockTracker = blockTracker; - } - - /** - * Network Configuration management functions - */ - - /** - * Adds a network configuration if the rpcUrl is not already present on an - * existing network configuration. Otherwise updates the entry with the matching rpcUrl. - * - * @param {NetworkConfiguration} networkConfiguration - The network configuration to add or, if rpcUrl matches an existing entry, to modify. - * @param {object} options - * @param {boolean} options.setActive - An option to set the newly added networkConfiguration as the active provider. - * @param {string} options.referrer - The site from which the call originated, or 'metamask' for internal calls - used for event metrics. - * @param {string} options.source - Where the upsertNetwork event originated (i.e. from a dapp or from the network form)- used for event metrics. - * @returns {string} id for the added or updated network configuration - */ - upsertNetworkConfiguration( - { rpcUrl, chainId, ticker, nickname, rpcPrefs }, - { setActive = false, referrer, source }, - ) { - assert.ok( - isPrefixedFormattedHexString(chainId), - `Invalid chain ID "${chainId}": invalid hex string.`, - ); - assert.ok( - isSafeChainId(parseInt(chainId, 16)), - `Invalid chain ID "${chainId}": numerical value greater than max safe value.`, - ); - - if (!rpcUrl) { - throw new Error( - 'An rpcUrl is required to add or update network configuration', - ); - } - - if (!referrer || !source) { - throw new Error( - 'referrer and source are required arguments for adding or updating a network configuration', - ); - } - - try { - // eslint-disable-next-line no-new - new URL(rpcUrl); - } catch (e) { - if (e.message.includes('Invalid URL')) { - throw new Error('rpcUrl must be a valid URL'); - } - } - - if (!ticker) { - throw new Error( - 'A ticker is required to add or update networkConfiguration', - ); - } - - const networkConfigurations = this.networkConfigurationsStore.getState(); - const newNetworkConfiguration = { - rpcUrl, - chainId, - ticker, - nickname, - rpcPrefs, - }; - - const oldNetworkConfigurationId = Object.values(networkConfigurations).find( - (networkConfiguration) => - networkConfiguration.rpcUrl?.toLowerCase() === rpcUrl?.toLowerCase(), - )?.id; - - const newNetworkConfigurationId = oldNetworkConfigurationId || random(); - this.networkConfigurationsStore.putState({ - ...networkConfigurations, - [newNetworkConfigurationId]: { - ...newNetworkConfiguration, - id: newNetworkConfigurationId, - }, - }); - - if (!oldNetworkConfigurationId) { - this._trackMetaMetricsEvent({ - event: 'Custom Network Added', - category: MetaMetricsEventCategory.Network, - referrer: { - url: referrer, - }, - properties: { - chain_id: chainId, - symbol: ticker, - source, - }, - }); - } - - if (setActive) { - this.setActiveNetwork(newNetworkConfigurationId); - } - - return newNetworkConfigurationId; - } - - /** - * Removes network configuration from state. - * - * @param {string} networkConfigurationId - the unique id for the network configuration to remove. - */ - removeNetworkConfiguration(networkConfigurationId) { - const networkConfigurations = { - ...this.networkConfigurationsStore.getState(), - }; - delete networkConfigurations[networkConfigurationId]; - this.networkConfigurationsStore.putState(networkConfigurations); - } -} diff --git a/app/scripts/controllers/network/network-controller.test.js b/app/scripts/controllers/network/network-controller.test.js index a58c563a8..fb86440c4 100644 --- a/app/scripts/controllers/network/network-controller.test.js +++ b/app/scripts/controllers/network/network-controller.test.js @@ -6,7 +6,7 @@ import sinon from 'sinon'; import { ControllerMessenger } from '@metamask/base-controller'; import { BUILT_IN_NETWORKS } from '../../../../shared/constants/network'; import { MetaMetricsNetworkEventSource } from '../../../../shared/constants/metametrics'; -import NetworkController from './network-controller'; +import { NetworkController } from './network-controller'; jest.mock('uuid', () => { const actual = jest.requireActual('uuid'); @@ -1100,7 +1100,7 @@ describe('NetworkController', () => { }); describe('when the request for the latest block responds with null', () => { - it('stores null as whether the network supports EIP-1559', async () => { + it('persists false to state as whether the network supports EIP-1559', async () => { await withController( { state: { @@ -1118,13 +1118,13 @@ describe('NetworkController', () => { await controller.getEIP1559Compatibility(); expect(controller.store.getState().networkDetails.EIPS[1559]).toBe( - null, + false, ); }, ); }); - it('returns null', async () => { + it('returns false', async () => { await withController(async ({ controller, network }) => { network.mockEssentialRpcCalls({ latestBlock: null, @@ -1133,7 +1133,7 @@ describe('NetworkController', () => { const supportsEIP1559 = await controller.getEIP1559Compatibility(); - expect(supportsEIP1559).toBe(null); + expect(supportsEIP1559).toBe(false); }); }); }); diff --git a/app/scripts/controllers/network/network-controller.ts b/app/scripts/controllers/network/network-controller.ts new file mode 100644 index 000000000..9249e0fa2 --- /dev/null +++ b/app/scripts/controllers/network/network-controller.ts @@ -0,0 +1,1171 @@ +import { strict as assert } from 'assert'; +import EventEmitter from 'events'; +import { ComposedStore, ObservableStore } from '@metamask/obs-store'; +import log from 'loglevel'; +import { + createSwappableProxy, + createEventEmitterProxy, + SwappableProxy, +} from '@metamask/swappable-obj-proxy'; +import EthQuery from 'eth-query'; +import { RestrictedControllerMessenger } from '@metamask/base-controller'; +import { v4 as uuid } from 'uuid'; +import { Hex, isPlainObject } from '@metamask/utils'; +import { errorCodes } from 'eth-rpc-errors'; +import { SafeEventEmitterProvider } from '@metamask/eth-json-rpc-provider'; +import { PollingBlockTracker } from 'eth-block-tracker'; +import { + INFURA_PROVIDER_TYPES, + INFURA_BLOCKED_KEY, + TEST_NETWORK_TICKER_MAP, + CHAIN_IDS, + NETWORK_TYPES, + BUILT_IN_INFURA_NETWORKS, + BuiltInInfuraNetwork, + NetworkStatus, +} from '../../../../shared/constants/network'; +import { + isPrefixedFormattedHexString, + isSafeChainId, +} from '../../../../shared/modules/network.utils'; +import { + MetaMetricsEventCategory, + MetaMetricsEventPayload, +} from '../../../../shared/constants/metametrics'; +import { isErrorWithMessage } from '../../../../shared/modules/error'; +import { + createNetworkClient, + NetworkClientType, +} from './create-network-client'; + +/** + * The name of NetworkController. + */ +const name = 'NetworkController'; + +/** + * A block header object that `eth_getBlockByNumber` returns. Note that this + * type does not specify all of the properties present within the block header; + * within NetworkController, we are only interested in `baseFeePerGas`. + */ +type Block = { + baseFeePerGas?: unknown; +}; + +/** + * Encodes a few pieces of information: + * + * - Whether or not a provider is configured for an Infura network or a + * non-Infura network. + * - If an Infura network, then which network. + * - If a non-Infura network, then whether the network exists locally or + * remotely. + * + * Primarily used to build the network client and check the availability of a + * network. + */ +type ProviderType = BuiltInInfuraNetwork | typeof NETWORK_TYPES.RPC; + +/** + * The network ID of a network. + */ +type NetworkId = `${number}`; + +/** + * The ID of a network configuration. + */ +type NetworkConfigurationId = string; + +/** + * The chain ID of a network. + */ +type ChainId = Hex; + +/** + * The set of event types that NetworkController can publish via its messenger. + */ +export enum NetworkControllerEventType { + /** + * @see {@link NetworkControllerNetworkWillChangeEvent} + */ + NetworkWillChange = 'NetworkController:networkWillChange', + /** + * @see {@link NetworkControllerNetworkDidChangeEvent} + */ + NetworkDidChange = 'NetworkController:networkDidChange', + /** + * @see {@link NetworkControllerInfuraIsBlockedEvent} + */ + InfuraIsBlocked = 'NetworkController:infuraIsBlocked', + /** + * @see {@link NetworkControllerInfuraIsUnblockedEvent} + */ + InfuraIsUnblocked = 'NetworkController:infuraIsUnblocked', +} + +/** + * `networkWillChange` is published when the current network is about to be + * switched, but the new provider has not been created and no state changes have + * occurred yet. + */ +type NetworkControllerNetworkWillChangeEvent = { + type: NetworkControllerEventType.NetworkWillChange; + payload: []; +}; + +/** + * `networkDidChange` is published after a provider has been created for a newly + * switched network (but before the network has been confirmed to be available). + */ +type NetworkControllerNetworkDidChangeEvent = { + type: NetworkControllerEventType.NetworkDidChange; + payload: []; +}; + +/** + * `infuraIsBlocked` is published after the network is switched to an Infura + * network, but when Infura returns an error blocking the user based on their + * location. + */ +type NetworkControllerInfuraIsBlockedEvent = { + type: NetworkControllerEventType.InfuraIsBlocked; + payload: []; +}; + +/** + * `infuraIsBlocked` is published either after the network is switched to an + * Infura network and Infura does not return an error blocking the user based on + * their location, or the network is switched to a non-Infura network. + */ +type NetworkControllerInfuraIsUnblockedEvent = { + type: NetworkControllerEventType.InfuraIsUnblocked; + payload: []; +}; + +/** + * The set of events that the NetworkController messenger can publish. + */ +type NetworkControllerEvent = + | NetworkControllerNetworkDidChangeEvent + | NetworkControllerNetworkWillChangeEvent + | NetworkControllerInfuraIsBlockedEvent + | NetworkControllerInfuraIsUnblockedEvent; + +/** + * The messenger that the NetworkController uses to publish events. + */ +type NetworkControllerMessenger = RestrictedControllerMessenger< + typeof name, + never, + NetworkControllerEvent, + never, + NetworkControllerEventType +>; + +/** + * Information used to set up the middleware stack for a particular kind of + * network. Currently has overlap with `NetworkConfiguration`, although the + * two will be merged down the road. + */ +type ProviderConfiguration = { + /** + * Either a type of Infura network, "localhost" for a locally operated + * network, or "rpc" for everything else. + */ + type: ProviderType; + /** + * The chain ID as per EIP-155. + */ + chainId: ChainId; + /** + * The URL of the RPC endpoint. Only used when `type` is "localhost" or "rpc". + */ + rpcUrl?: string; + /** + * The shortname of the currency used by the network. + */ + ticker?: string; + /** + * The user-customizable name of the network. + */ + nickname?: string; + /** + * User-customizable details for the network. + */ + rpcPrefs?: { + blockExplorerUrl?: string; + }; +}; + +/** + * The contents of the `networkId` store. + */ +type NetworkIdState = NetworkId | null; + +/** + * Information about the network not held by any other part of state. Currently + * only used to capture whether a network supports EIP-1559. + */ +type NetworkDetails = { + /** + * EIPs supported by the network. + */ + EIPS: { + [eipNumber: number]: boolean | undefined; + }; +}; + +/** + * A "network configuration" represents connection data directly provided by + * users via the wallet UI for a custom network (we already have this + * information for networks that come pre-shipped with the wallet). Ultimately + * used to set up the middleware stack so that the wallet can make requests to + * the network. Currently has overlap with `ProviderConfiguration`, although the + * two will be merged down the road. + */ +type NetworkConfiguration = { + /** + * The unique ID of the network configuration. Useful for switching to and + * removing specific networks. + */ + id: NetworkConfigurationId; + /** + * The URL of the RPC endpoint. Only used when `type` is "localhost" or "rpc". + */ + rpcUrl: string; + /** + * The chain ID as per EIP-155. + */ + chainId: ChainId; + /** + * The shortname of the currency used for this network. + */ + ticker: string; + /** + * The user-customizable name of the network. + */ + nickname?: string; + /** + * User-customizable details for the network. + */ + rpcPrefs?: { + blockExplorerUrl: string; + }; +}; + +/** + * A set of network configurations, keyed by ID. + */ +type NetworkConfigurations = Record< + NetworkConfigurationId, + NetworkConfiguration +>; + +/** + * The state that NetworkController holds after combining its individual stores. + */ +type CompositeState = { + provider: ProviderConfiguration; + previousProviderStore: ProviderConfiguration; + networkId: NetworkIdState; + networkStatus: NetworkStatus; + networkDetails: NetworkDetails; + networkConfigurations: NetworkConfigurations; +}; + +/** + * The options that NetworkController takes. + */ +type NetworkControllerOptions = { + messenger: NetworkControllerMessenger; + state?: { + provider?: ProviderConfiguration; + networkDetails?: NetworkDetails; + networkConfigurations?: NetworkConfigurations; + }; + infuraProjectId: string; + trackMetaMetricsEvent: (payload: MetaMetricsEventPayload) => void; +}; + +/** + * Type guard for determining whether the given value is an error object with a + * `code` property, such as an instance of Error. + * + * TODO: Move this to @metamask/utils + * + * @param error - The object to check. + * @returns True if `error` has a `code`, false otherwise. + */ +function isErrorWithCode(error: unknown): error is { code: string | number } { + return typeof error === 'object' && error !== null && 'code' in error; +} + +/** + * Asserts that the given value is a network ID, i.e., that it is a decimal + * number represented as a string. + * + * @param value - The value to check. + */ +function assertNetworkId(value: any): asserts value is NetworkId { + assert( + /^\d+$/u.test(value) && !Number.isNaN(Number(value)), + 'value is not a number', + ); +} + +/** + * Builds the default provider config used to initialize the network controller. + */ +function buildDefaultProviderConfigState(): ProviderConfiguration { + if (process.env.IN_TEST) { + return { + type: NETWORK_TYPES.RPC, + rpcUrl: 'http://localhost:8545', + chainId: '0x539', + nickname: 'Localhost 8545', + ticker: 'ETH', + }; + } else if ( + process.env.METAMASK_DEBUG || + process.env.METAMASK_ENV === 'test' + ) { + return { + type: NETWORK_TYPES.GOERLI, + chainId: CHAIN_IDS.GOERLI, + ticker: TEST_NETWORK_TICKER_MAP[NETWORK_TYPES.GOERLI], + }; + } + + return { + type: NETWORK_TYPES.MAINNET, + chainId: CHAIN_IDS.MAINNET, + ticker: 'ETH', + }; +} + +/** + * Builds the default network ID state used to initialize the network + * controller. + */ +function buildDefaultNetworkIdState(): NetworkIdState { + return null; +} + +/** + * Builds the default network status state used to initialize the network + * controller. + */ +function buildDefaultNetworkStatusState(): NetworkStatus { + return NetworkStatus.Unknown; +} + +/** + * Builds the default network details state used to initialize the + * network controller. + */ +function buildDefaultNetworkDetailsState(): NetworkDetails { + return { + EIPS: { + 1559: undefined, + }, + }; +} + +/** + * Builds the default network configurations state used to initialize the + * network controller. + */ +function buildDefaultNetworkConfigurationsState(): NetworkConfigurations { + return {}; +} + +/** + * Returns whether the given argument is a type that our Infura middleware + * recognizes. We can't calculate this inline because the usual type of `type`, + * which we get from the provider config, is not a subset of the type of + * `INFURA_PROVIDER_TYPES`, but rather a superset, and therefore we cannot make + * a proper comparison without TypeScript complaining. However, if we downcast + * both variables, then we are able to achieve this. As a bonus, this function + * also types the given argument as a `BuiltInInfuraNetwork` assuming that the + * check succeeds. + * + * @param type - A type to compare. + * @returns True or false, depending on whether the given type is one that our + * Infura middleware recognizes. + */ +function isInfuraProviderType(type: string): type is BuiltInInfuraNetwork { + const infuraProviderTypes: readonly string[] = INFURA_PROVIDER_TYPES; + return infuraProviderTypes.includes(type); +} + +/** + * The network controller creates and manages the "provider" object which allows + * our code and external dapps to make requests to a network. The requests are + * filtered through a set of middleware (provided by + * [`eth-json-rpc-middleware`][1]) which not only performs the HTTP request to + * the appropriate RPC endpoint but also uses caching to limit duplicate + * requests to Infura and smoothens interactions with the blockchain in general. + * + * [1]: https://github.com/MetaMask/eth-json-rpc-middleware + */ +export class NetworkController extends EventEmitter { + /** + * The messenger that NetworkController uses to publish events. + */ + messenger: NetworkControllerMessenger; + + /** + * Observable store containing the provider configuration. + */ + providerStore: ObservableStore; + + /** + * Observable store containing the provider configuration for the previously + * configured network. + */ + previousProviderStore: ObservableStore; + + /** + * Observable store containing the network ID for the current network or null + * if there is no current network. + */ + networkIdStore: ObservableStore; + + /** + * Observable store for the network status. + */ + networkStatusStore: ObservableStore; + + /** + * Observable store for details about the network. + */ + networkDetails: ObservableStore; + + /** + * Observable store for network configurations. + */ + networkConfigurationsStore: ObservableStore; + + /** + * Observable store containing a combination of data from all of the + * individual stores. + */ + store: ComposedStore; + + _provider: SafeEventEmitterProvider | null; + + _blockTracker: PollingBlockTracker | null; + + _providerProxy: SwappableProxy | null; + + _blockTrackerProxy: SwappableProxy | null; + + _infuraProjectId: NetworkControllerOptions['infuraProjectId']; + + _trackMetaMetricsEvent: NetworkControllerOptions['trackMetaMetricsEvent']; + + /** + * Constructs a network controller. + * + * @param options - Options for this constructor. + * @param options.messenger - The NetworkController messenger. + * @param options.state - Initial controller state. + * @param options.infuraProjectId - The Infura project ID. + * @param options.trackMetaMetricsEvent - A method to forward events to the + * {@link MetaMetricsController}. + */ + constructor({ + messenger, + state = {}, + infuraProjectId, + trackMetaMetricsEvent, + }: NetworkControllerOptions) { + super(); + + this.messenger = messenger; + + // create stores + this.providerStore = new ObservableStore( + state.provider || buildDefaultProviderConfigState(), + ); + this.previousProviderStore = new ObservableStore( + this.providerStore.getState(), + ); + this.networkIdStore = new ObservableStore(buildDefaultNetworkIdState()); + this.networkStatusStore = new ObservableStore( + buildDefaultNetworkStatusState(), + ); + // We need to keep track of a few details about the current network. + // Ideally we'd merge this.networkStatusStore with this new store, but doing + // so will require a decent sized refactor of how we're accessing network + // state. Currently this is only used for detecting EIP-1559 support but can + // be extended to track other network details. + this.networkDetails = new ObservableStore( + state.networkDetails || buildDefaultNetworkDetailsState(), + ); + + this.networkConfigurationsStore = new ObservableStore( + state.networkConfigurations || buildDefaultNetworkConfigurationsState(), + ); + + this.store = new ComposedStore({ + provider: this.providerStore, + previousProviderStore: this.previousProviderStore, + networkId: this.networkIdStore, + networkStatus: this.networkStatusStore, + networkDetails: this.networkDetails, + networkConfigurations: this.networkConfigurationsStore, + }); + + // provider and block tracker + this._provider = null; + this._blockTracker = null; + + // provider and block tracker proxies - because the network changes + this._providerProxy = null; + this._blockTrackerProxy = null; + + if (!infuraProjectId || typeof infuraProjectId !== 'string') { + throw new Error('Invalid Infura project ID'); + } + this._infuraProjectId = infuraProjectId; + this._trackMetaMetricsEvent = trackMetaMetricsEvent; + } + + /** + * Deactivates the controller, stopping any ongoing polling. + * + * In-progress requests will not be aborted. + */ + async destroy(): Promise { + await this._blockTracker?.destroy(); + } + + /** + * Creates the provider and block tracker for the configured network, + * using the provider to gather details about the network. + */ + async initializeProvider(): Promise { + const { type, rpcUrl, chainId } = this.providerStore.getState(); + this._configureProvider({ type, rpcUrl, chainId }); + await this.lookupNetwork(); + } + + /** + * Returns the proxies wrapping the currently set provider and block tracker. + */ + getProviderAndBlockTracker(): { + provider: SwappableProxy | null; + blockTracker: SwappableProxy | null; + } { + const provider = this._providerProxy; + const blockTracker = this._blockTrackerProxy; + return { provider, blockTracker }; + } + + /** + * Determines whether the network supports EIP-1559 by checking whether the + * latest block has a `baseFeePerGas` property, then updates state + * appropriately. + * + * @returns A promise that resolves to true if the network supports EIP-1559 + * and false otherwise. + */ + async getEIP1559Compatibility(): Promise { + const { EIPS } = this.networkDetails.getState(); + // NOTE: This isn't necessary anymore because the block cache middleware + // already prevents duplicate requests from taking place + if (EIPS[1559] !== undefined) { + return EIPS[1559]; + } + + const { provider } = this.getProviderAndBlockTracker(); + if (!provider) { + // Really we should throw an error if a provider hasn't been initialized + // yet, but that might have undesirable repercussions, so return false for + // now + return false; + } + + const supportsEIP1559 = await this._determineEIP1559Compatibility(provider); + this.networkDetails.updateState({ + EIPS: { + ...this.networkDetails.getState().EIPS, + 1559: supportsEIP1559, + }, + }); + return supportsEIP1559; + } + + /** + * Performs side effects after switching to a network. If the network is + * available, updates the network state with the network ID of the network and + * stores whether the network supports EIP-1559; otherwise clears said + * information about the network that may have been previously stored. + * + * @fires infuraIsBlocked if the network is Infura-supported and is blocking + * requests. + * @fires infuraIsUnblocked if the network is Infura-supported and is not + * blocking requests, or if the network is not Infura-supported. + */ + async lookupNetwork(): Promise { + const { chainId, type } = this.providerStore.getState(); + const { provider } = this.getProviderAndBlockTracker(); + let networkChanged = false; + let networkId: NetworkIdState = null; + let supportsEIP1559 = false; + let networkStatus: NetworkStatus; + + if (provider === null) { + log.warn( + 'NetworkController - lookupNetwork aborted due to missing provider', + ); + return; + } + + if (!chainId) { + log.warn( + 'NetworkController - lookupNetwork aborted due to missing chainId', + ); + this._resetNetworkId(); + this._resetNetworkStatus(); + this._resetNetworkDetails(); + return; + } + + const isInfura = isInfuraProviderType(type); + + const listener = () => { + networkChanged = true; + this.messenger.unsubscribe( + NetworkControllerEventType.NetworkDidChange, + listener, + ); + }; + this.messenger.subscribe( + NetworkControllerEventType.NetworkDidChange, + listener, + ); + + try { + const results = await Promise.all([ + this._getNetworkId(provider), + this._determineEIP1559Compatibility(provider), + ]); + const possibleNetworkId = results[0]; + assertNetworkId(possibleNetworkId); + networkId = possibleNetworkId; + supportsEIP1559 = results[1]; + networkStatus = NetworkStatus.Available; + } catch (error) { + if (isErrorWithCode(error) && isErrorWithMessage(error)) { + let responseBody; + try { + responseBody = JSON.parse(error.message); + } catch { + // error.message must not be JSON + } + + if ( + isPlainObject(responseBody) && + responseBody.error === INFURA_BLOCKED_KEY + ) { + networkStatus = NetworkStatus.Blocked; + } else if (error.code === errorCodes.rpc.internal) { + networkStatus = NetworkStatus.Unknown; + } else { + networkStatus = NetworkStatus.Unavailable; + } + } else { + log.warn( + 'NetworkController - could not determine network status', + error, + ); + networkStatus = NetworkStatus.Unknown; + } + } + + if (networkChanged) { + // If the network has changed, then `lookupNetwork` either has been or is + // in the process of being called, so we don't need to go further. + return; + } + this.messenger.unsubscribe( + NetworkControllerEventType.NetworkDidChange, + listener, + ); + + this.networkStatusStore.putState(networkStatus); + + if (networkStatus === NetworkStatus.Available) { + this.networkIdStore.putState(networkId); + this.networkDetails.updateState({ + EIPS: { + ...this.networkDetails.getState().EIPS, + 1559: supportsEIP1559, + }, + }); + } else { + this._resetNetworkId(); + this._resetNetworkDetails(); + } + + if (isInfura) { + if (networkStatus === NetworkStatus.Available) { + this.messenger.publish(NetworkControllerEventType.InfuraIsUnblocked); + } else if (networkStatus === NetworkStatus.Blocked) { + this.messenger.publish(NetworkControllerEventType.InfuraIsBlocked); + } + } else { + // Always publish infuraIsUnblocked regardless of network status to + // prevent consumers from being stuck in a blocked state if they were + // previously connected to an Infura network that was blocked + this.messenger.publish(NetworkControllerEventType.InfuraIsUnblocked); + } + } + + /** + * Switches to the network specified by a network configuration. + * + * @param networkConfigurationId - The unique identifier that refers to a + * previously added network configuration. + * @returns The URL of the RPC endpoint representing the newly switched + * network. + */ + setActiveNetwork(networkConfigurationId: NetworkConfigurationId): string { + const targetNetwork = + this.networkConfigurationsStore.getState()[networkConfigurationId]; + + if (!targetNetwork) { + throw new Error( + `networkConfigurationId ${networkConfigurationId} does not match a configured networkConfiguration`, + ); + } + + this._setProviderConfig({ + type: NETWORK_TYPES.RPC, + ...targetNetwork, + }); + + return targetNetwork.rpcUrl; + } + + /** + * Switches to an Infura-supported network. + * + * @param type - The shortname of the network. + * @throws if the `type` is "rpc" or if it is not a known Infura-supported + * network. + */ + setProviderType(type: string): void { + assert.notStrictEqual( + type, + NETWORK_TYPES.RPC, + `NetworkController - cannot call "setProviderType" with type "${NETWORK_TYPES.RPC}". Use "setActiveNetwork"`, + ); + assert.ok( + isInfuraProviderType(type), + `Unknown Infura provider type "${type}".`, + ); + const network = BUILT_IN_INFURA_NETWORKS[type]; + this._setProviderConfig({ + type, + rpcUrl: '', + chainId: network.chainId, + ticker: 'ticker' in network ? network.ticker : 'ETH', + nickname: '', + rpcPrefs: { blockExplorerUrl: network.blockExplorerUrl }, + }); + } + + /** + * Re-initializes the provider and block tracker for the current network. + */ + resetConnection(): void { + this._setProviderConfig(this.providerStore.getState()); + } + + /** + * Switches to the previous network, assuming that the current network is + * different than the initial network (if it is, then this is equivalent to + * calling `resetConnection`). + */ + rollbackToPreviousProvider(): void { + const config = this.previousProviderStore.getState(); + this.providerStore.putState(config); + this._switchNetwork(config); + } + + /** + * Fetches the latest block for the network. + * + * @param provider - A provider, which is guaranteed to be available. + * @returns A promise that either resolves to the block header or null if + * there is no latest block, or rejects with an error. + */ + _getLatestBlock(provider: SafeEventEmitterProvider): Promise { + return new Promise((resolve, reject) => { + const ethQuery = new EthQuery(provider); + ethQuery.sendAsync<['latest', false], Block | null>( + { method: 'eth_getBlockByNumber', params: ['latest', false] }, + (...args) => { + if (args[0] === null) { + resolve(args[1]); + } else { + reject(args[0]); + } + }, + ); + }); + } + + /** + * Fetches the network ID for the network. + * + * @param provider - A provider, which is guaranteed to be available. + * @returns A promise that either resolves to the network ID, or rejects with + * an error. + */ + async _getNetworkId(provider: SafeEventEmitterProvider): Promise { + const ethQuery = new EthQuery(provider); + return await new Promise((resolve, reject) => { + ethQuery.sendAsync( + { method: 'net_version' }, + (...args) => { + if (args[0] === null) { + resolve(args[1]); + } else { + reject(args[0]); + } + }, + ); + }); + } + + /** + * Clears the stored network ID. + */ + _resetNetworkId(): void { + this.networkIdStore.putState(buildDefaultNetworkIdState()); + } + + /** + * Resets network status to the default ("unknown"). + */ + _resetNetworkStatus(): void { + this.networkStatusStore.putState(buildDefaultNetworkStatusState()); + } + + /** + * Clears details previously stored for the network. + */ + _resetNetworkDetails(): void { + this.networkDetails.putState(buildDefaultNetworkDetailsState()); + } + + /** + * Stores the given provider configuration representing a network in state, + * then uses it to create a new provider for that network. + * + * @param providerConfig - The provider configuration. + */ + _setProviderConfig(providerConfig: ProviderConfiguration): void { + this.previousProviderStore.putState(this.providerStore.getState()); + this.providerStore.putState(providerConfig); + this._switchNetwork(providerConfig); + } + + /** + * Retrieves the latest block from the currently selected network; if the + * block has a `baseFeePerGas` property, then we know that the network + * supports EIP-1559; otherwise it doesn't. + * + * @param provider - A provider, which is guaranteed to be available. + * @returns A promise that resolves to true if the network supports EIP-1559 + * and false otherwise. + */ + async _determineEIP1559Compatibility( + provider: SafeEventEmitterProvider, + ): Promise { + const latestBlock = await this._getLatestBlock(provider); + return latestBlock?.baseFeePerGas !== undefined; + } + + /** + * Executes a series of steps to change the current network: + * + * 1. Notifies subscribers that the network is about to change. + * 2. Clears state associated with the current network. + * 3. Creates a new network client along with a provider for the desired + * network. + * 4. Notifies subscribes that the network has changed. + * + * @param providerConfig - The provider configuration object that specifies + * the new network. + */ + _switchNetwork(providerConfig: ProviderConfiguration): void { + this.messenger.publish(NetworkControllerEventType.NetworkWillChange); + this._resetNetworkId(); + this._resetNetworkStatus(); + this._resetNetworkDetails(); + this._configureProvider(providerConfig); + this.messenger.publish(NetworkControllerEventType.NetworkDidChange); + this.lookupNetwork(); + } + + /** + * Creates a network client (a stack of middleware along with a provider and + * block tracker) to talk to a network. + * + * @param args - The arguments. + * @param args.type - The shortname of an Infura-supported network (see + * {@link NETWORK_TYPES}). + * @param args.rpcUrl - The URL of the RPC endpoint that represents the + * network. Only used for non-Infura networks. + * @param args.chainId - The chain ID of the network (as per EIP-155). Only + * used for non-Infura-supported networks (as we already know the chain ID of + * any Infura-supported network). + * @throws if the `type` if not a known Infura-supported network. + */ + _configureProvider({ type, rpcUrl, chainId }: ProviderConfiguration): void { + const isInfura = isInfuraProviderType(type); + if (isInfura) { + // infura type-based endpoints + this._configureInfuraProvider({ + type, + infuraProjectId: this._infuraProjectId, + }); + } else if (type === NETWORK_TYPES.RPC && rpcUrl) { + // url-based rpc endpoints + this._configureStandardProvider(rpcUrl, chainId); + } else { + throw new Error( + `NetworkController - _configureProvider - unknown type "${type}"`, + ); + } + } + + /** + * Creates a network client (a stack of middleware along with a provider and + * block tracker) to talk to an Infura-supported network. + * + * @param args - The arguments. + * @param args.type - The shortname of the Infura network (see + * {@link NETWORK_TYPES}). + * @param args.infuraProjectId - An Infura API key. ("Project ID" is a + * now-obsolete term we've retained for backward compatibility.) + */ + _configureInfuraProvider({ + type, + infuraProjectId, + }: { + type: BuiltInInfuraNetwork; + infuraProjectId: NetworkControllerOptions['infuraProjectId']; + }): void { + log.info('NetworkController - configureInfuraProvider', type); + const { provider, blockTracker } = createNetworkClient({ + network: type, + infuraProjectId, + type: NetworkClientType.Infura, + }); + this._setProviderAndBlockTracker({ provider, blockTracker }); + } + + /** + * Creates a network client (a stack of middleware along with a provider and + * block tracker) to talk to a non-Infura-supported network. + * + * @param rpcUrl - The URL of the RPC endpoint that represents the network. + * @param chainId - The chain ID of the network (as per EIP-155). + */ + _configureStandardProvider(rpcUrl: string, chainId: ChainId): void { + log.info('NetworkController - configureStandardProvider', rpcUrl); + const { provider, blockTracker } = createNetworkClient({ + chainId, + rpcUrl, + type: NetworkClientType.Custom, + }); + this._setProviderAndBlockTracker({ provider, blockTracker }); + } + + /** + * Given a provider and a block tracker, updates any proxies pointing to + * these objects that have been previously set, or initializes any proxies + * that have not been previously set. + * + * @param args - The arguments. + * @param args.provider - The provider. + * @param args.blockTracker - The block tracker. + */ + _setProviderAndBlockTracker({ + provider, + blockTracker, + }: { + provider: SafeEventEmitterProvider; + blockTracker: PollingBlockTracker; + }): void { + // update or initialize proxies + if (this._providerProxy) { + this._providerProxy.setTarget(provider); + } else { + this._providerProxy = createSwappableProxy(provider); + } + if (this._blockTrackerProxy) { + this._blockTrackerProxy.setTarget(blockTracker); + } else { + this._blockTrackerProxy = createEventEmitterProxy(blockTracker, { + eventFilter: 'skipInternal', + }); + } + // set new provider and blockTracker + this._provider = provider; + this._blockTracker = blockTracker; + } + + /** + * Network Configuration management functions + */ + + /** + * Updates an existing network configuration matching the same RPC URL as the + * given network configuration; otherwise adds the network configuration. + * Following the upsert, the `trackMetaMetricsEvent` callback specified + * via the NetworkController constructor will be called to (presumably) create + * a MetaMetrics event. + * + * @param networkConfiguration - The network configuration to upsert. + * @param networkConfiguration.chainId - The chain ID of the network as per + * EIP-155. + * @param networkConfiguration.ticker - The shortname of the currency used by + * the network. + * @param networkConfiguration.nickname - The user-customizable name of the + * network. + * @param networkConfiguration.rpcPrefs - User-customizable details for the + * network. + * @param networkConfiguration.rpcUrl - The URL of the RPC endpoint. + * @param additionalArgs - Additional arguments. + * @param additionalArgs.setActive - Switches to the network specified by + * the given network configuration following the upsert. + * @param additionalArgs.referrer - The site from which the call originated, + * or 'metamask' for internal calls; used for event metrics. + * @param additionalArgs.source - Where the metric event originated (i.e. from + * a dapp or from the network form); used for event metrics. + * @throws if the `chainID` does not match EIP-155 or is too large. + * @throws if `rpcUrl` is not a valid URL. + * @returns The ID for the added or updated network configuration. + */ + upsertNetworkConfiguration( + { + rpcUrl, + chainId, + ticker, + nickname, + rpcPrefs, + }: Omit, + { + setActive = false, + referrer, + source, + }: { + setActive?: boolean; + referrer: string; + source: string; + }, + ): NetworkConfigurationId { + assert.ok( + isPrefixedFormattedHexString(chainId), + `Invalid chain ID "${chainId}": invalid hex string.`, + ); + assert.ok( + isSafeChainId(parseInt(chainId, 16)), + `Invalid chain ID "${chainId}": numerical value greater than max safe value.`, + ); + + if (!rpcUrl) { + throw new Error( + 'An rpcUrl is required to add or update network configuration', + ); + } + + if (!referrer || !source) { + throw new Error( + 'referrer and source are required arguments for adding or updating a network configuration', + ); + } + + try { + // eslint-disable-next-line no-new + new URL(rpcUrl); + } catch (e) { + if (isErrorWithMessage(e) && e.message.includes('Invalid URL')) { + throw new Error('rpcUrl must be a valid URL'); + } + } + + if (!ticker) { + throw new Error( + 'A ticker is required to add or update networkConfiguration', + ); + } + + const networkConfigurations = this.networkConfigurationsStore.getState(); + const newNetworkConfiguration = { + rpcUrl, + chainId, + ticker, + nickname, + rpcPrefs, + }; + + const oldNetworkConfigurationId = Object.values(networkConfigurations).find( + (networkConfiguration) => + networkConfiguration.rpcUrl?.toLowerCase() === rpcUrl?.toLowerCase(), + )?.id; + + const newNetworkConfigurationId = oldNetworkConfigurationId || uuid(); + this.networkConfigurationsStore.putState({ + ...networkConfigurations, + [newNetworkConfigurationId]: { + ...newNetworkConfiguration, + id: newNetworkConfigurationId, + }, + }); + + if (!oldNetworkConfigurationId) { + this._trackMetaMetricsEvent({ + event: 'Custom Network Added', + category: MetaMetricsEventCategory.Network, + referrer: { + url: referrer, + }, + properties: { + chain_id: chainId, + symbol: ticker, + source, + }, + }); + } + + if (setActive) { + this.setActiveNetwork(newNetworkConfigurationId); + } + + return newNetworkConfigurationId; + } + + /** + * Removes a network configuration from state. + * + * @param networkConfigurationId - The unique id for the network configuration + * to remove. + */ + removeNetworkConfiguration( + networkConfigurationId: NetworkConfigurationId, + ): void { + const networkConfigurations = { + ...this.networkConfigurationsStore.getState(), + }; + delete networkConfigurations[networkConfigurationId]; + this.networkConfigurationsStore.putState(networkConfigurations); + } +} diff --git a/app/scripts/controllers/preferences.test.js b/app/scripts/controllers/preferences.test.js index 51f2ba5a0..6fcb1bac5 100644 --- a/app/scripts/controllers/preferences.test.js +++ b/app/scripts/controllers/preferences.test.js @@ -4,7 +4,7 @@ import { ControllerMessenger } from '@metamask/base-controller'; import { TokenListController } from '@metamask/assets-controllers'; import { CHAIN_IDS } from '../../../shared/constants/network'; import PreferencesController from './preferences'; -import NetworkController from './network'; +import { NetworkController } from './network'; describe('preferences controller', function () { let preferencesController; diff --git a/app/scripts/metamask-controller.js b/app/scripts/metamask-controller.js index c1d8216a0..7315f301f 100644 --- a/app/scripts/metamask-controller.js +++ b/app/scripts/metamask-controller.js @@ -143,8 +143,9 @@ import createTabIdMiddleware from './lib/createTabIdMiddleware'; import createOnboardingMiddleware from './lib/createOnboardingMiddleware'; import { setupMultiplex } from './lib/stream-utils'; import EnsController from './controllers/ens'; -import NetworkController, { - NetworkControllerEventTypes, +import { + NetworkController, + NetworkControllerEventType, } from './controllers/network'; import PreferencesController from './controllers/preferences'; import AppStateController from './controllers/app-state'; @@ -263,7 +264,7 @@ export default class MetamaskController extends EventEmitter { const networkControllerMessenger = this.controllerMessenger.getRestricted({ name: 'NetworkController', - allowedEvents: Object.values(NetworkControllerEventTypes), + allowedEvents: Object.values(NetworkControllerEventType), }); this.networkController = new NetworkController({ messenger: networkControllerMessenger, @@ -310,11 +311,11 @@ export default class MetamaskController extends EventEmitter { initLangCode: opts.initLangCode, onInfuraIsBlocked: networkControllerMessenger.subscribe.bind( networkControllerMessenger, - NetworkControllerEventTypes.InfuraIsBlocked, + NetworkControllerEventType.InfuraIsBlocked, ), onInfuraIsUnblocked: networkControllerMessenger.subscribe.bind( networkControllerMessenger, - NetworkControllerEventTypes.InfuraIsUnblocked, + NetworkControllerEventType.InfuraIsUnblocked, ), tokenListController: this.tokenListController, provider: this.provider, @@ -452,7 +453,7 @@ export default class MetamaskController extends EventEmitter { preferencesStore: this.preferencesController.store, onNetworkDidChange: networkControllerMessenger.subscribe.bind( networkControllerMessenger, - NetworkControllerEventTypes.NetworkDidChange, + NetworkControllerEventType.NetworkDidChange, ), getNetworkIdentifier: () => { const { type, rpcUrl } = @@ -491,7 +492,7 @@ export default class MetamaskController extends EventEmitter { // onNetworkDidChange onNetworkStateChange: networkControllerMessenger.subscribe.bind( networkControllerMessenger, - NetworkControllerEventTypes.NetworkDidChange, + NetworkControllerEventType.NetworkDidChange, ), getCurrentNetworkEIP1559Compatibility: this.networkController.getEIP1559Compatibility.bind( @@ -609,7 +610,7 @@ export default class MetamaskController extends EventEmitter { this.networkController.store.getState().provider.chainId, onNetworkDidChange: networkControllerMessenger.subscribe.bind( networkControllerMessenger, - NetworkControllerEventTypes.NetworkDidChange, + NetworkControllerEventType.NetworkDidChange, ), }); @@ -621,7 +622,7 @@ export default class MetamaskController extends EventEmitter { blockTracker: this.blockTracker, onNetworkDidChange: networkControllerMessenger.subscribe.bind( networkControllerMessenger, - NetworkControllerEventTypes.NetworkDidChange, + NetworkControllerEventType.NetworkDidChange, ), getCurrentChainId: () => this.networkController.store.getState().provider.chainId, @@ -1106,7 +1107,7 @@ export default class MetamaskController extends EventEmitter { }); networkControllerMessenger.subscribe( - NetworkControllerEventTypes.NetworkDidChange, + NetworkControllerEventType.NetworkDidChange, async () => { const { ticker } = this.networkController.store.getState().provider; try { @@ -1152,7 +1153,7 @@ export default class MetamaskController extends EventEmitter { networkController: this.networkController, onNetworkDidChange: networkControllerMessenger.subscribe.bind( networkControllerMessenger, - NetworkControllerEventTypes.NetworkDidChange, + NetworkControllerEventType.NetworkDidChange, ), provider: this.provider, getProviderConfig: () => this.networkController.store.getState().provider, @@ -1195,7 +1196,7 @@ export default class MetamaskController extends EventEmitter { // ensure accountTracker updates balances after network change networkControllerMessenger.subscribe( - NetworkControllerEventTypes.NetworkDidChange, + NetworkControllerEventType.NetworkDidChange, () => { this.accountTracker._updateAccounts(); }, @@ -1203,7 +1204,7 @@ export default class MetamaskController extends EventEmitter { // clear unapproved transactions and messages when the network will change networkControllerMessenger.subscribe( - NetworkControllerEventTypes.NetworkWillChange, + NetworkControllerEventType.NetworkWillChange, () => { this.txController.txStateManager.clearUnapprovedTxs(); this.encryptionPublicKeyManager.clearUnapproved(); diff --git a/package.json b/package.json index 47cb164e9..80df52fff 100644 --- a/package.json +++ b/package.json @@ -249,7 +249,7 @@ "@metamask/message-manager": "^2.1.0", "@metamask/metamask-eth-abis": "^3.0.0", "@metamask/notification-controller": "^1.0.0", - "@metamask/obs-store": "^8.0.0", + "@metamask/obs-store": "^8.1.0", "@metamask/permission-controller": "^3.1.0", "@metamask/phishing-controller": "^2.0.0", "@metamask/post-message-stream": "^6.0.0", diff --git a/shared/constants/network.ts b/shared/constants/network.ts index 1f20538c3..ab5115bee 100644 --- a/shared/constants/network.ts +++ b/shared/constants/network.ts @@ -238,7 +238,7 @@ export const INFURA_PROVIDER_TYPES = [ NETWORK_TYPES.MAINNET, NETWORK_TYPES.GOERLI, NETWORK_TYPES.SEPOLIA, -]; +] as const; export const TEST_CHAINS = [ CHAIN_IDS.GOERLI, diff --git a/types/eth-query.d.ts b/types/eth-query.d.ts new file mode 100644 index 000000000..726300f68 --- /dev/null +++ b/types/eth-query.d.ts @@ -0,0 +1,50 @@ +declare module 'eth-query' { + // What it says on the tin. We omit `null` because confusingly, this is used + // for a successful response to indicate a lack of an error. + type EverythingButNull = + | string + | number + | boolean + | object + | symbol + | undefined; + + type ProviderSendAsyncResponse = { + error?: { message: string }; + result?: Result; + }; + + type ProviderSendAsyncCallback = ( + error: unknown, + response: ProviderSendAsyncResponse, + ) => void; + + type Provider = { + sendAsync( + payload: SendAsyncPayload, + callback: ProviderSendAsyncCallback, + ): void; + }; + + type SendAsyncPayload = { + id: number; + jsonrpc: '2.0'; + method: string; + params: Params; + }; + + type SendAsyncCallback = ( + ...args: + | [error: EverythingButNull, result: undefined] + | [error: null, result: Result] + ) => void; + + export default class EthQuery { + constructor(provider: Provider); + + sendAsync( + opts: Partial>, + callback: SendAsyncCallback, + ): void; + } +} diff --git a/yarn.lock b/yarn.lock index 28686cc9d..0921937e9 100644 --- a/yarn.lock +++ b/yarn.lock @@ -4152,13 +4152,13 @@ __metadata: languageName: node linkType: hard -"@metamask/obs-store@npm:^8.0.0": - version: 8.0.0 - resolution: "@metamask/obs-store@npm:8.0.0" +"@metamask/obs-store@npm:^8.1.0": + version: 8.1.0 + resolution: "@metamask/obs-store@npm:8.1.0" dependencies: "@metamask/safe-event-emitter": ^2.0.0 through2: ^2.0.3 - checksum: 232362e65a3563f0bd3299cec48f5adb37e68d4f066b7de90f2b044480d3b16c2d918c12d672c825e1d9b55344ae818fb8494d91129e4613555097653b9bb887 + checksum: 92356067fa3517526d656f2f0bdfbc4d39f65e27fb30d84240cfc9c1aa9cd5d743498952df18ed8efbb8887b6cc1bc1fab37bde3fb0fc059539e0dfcc67ff86f languageName: node linkType: hard @@ -24308,7 +24308,7 @@ __metadata: "@metamask/message-manager": ^2.1.0 "@metamask/metamask-eth-abis": ^3.0.0 "@metamask/notification-controller": ^1.0.0 - "@metamask/obs-store": ^8.0.0 + "@metamask/obs-store": ^8.1.0 "@metamask/permission-controller": ^3.1.0 "@metamask/phishing-controller": ^2.0.0 "@metamask/phishing-warning": ^2.1.0 From b3c4790f437d6007098b049ba04dd49325c07b6e Mon Sep 17 00:00:00 2001 From: Dan J Miller Date: Tue, 11 Apr 2023 16:46:31 -0230 Subject: [PATCH 10/36] Ensure that all networkConfiguration object in networkController state have an id (#18513) * Ensure that all networkConfiguration object in networkController state have an id * Lint fix * Update app/scripts/migrations/084.ts Co-authored-by: Mark Stacey * Add unit tests for error cases * Simplify code * Remove unnecessary any typing * Fix network controller type checking * Lint fix * Improve typing --------- Co-authored-by: legobeat <109787230+legobeat@users.noreply.github.com> Co-authored-by: Mark Stacey --- app/scripts/migrations/084.test.js | 254 +++++++++++++++++++++++++++++ app/scripts/migrations/084.ts | 58 +++++++ app/scripts/migrations/index.js | 2 + 3 files changed, 314 insertions(+) create mode 100644 app/scripts/migrations/084.test.js create mode 100644 app/scripts/migrations/084.ts diff --git a/app/scripts/migrations/084.test.js b/app/scripts/migrations/084.test.js new file mode 100644 index 000000000..e93b561e5 --- /dev/null +++ b/app/scripts/migrations/084.test.js @@ -0,0 +1,254 @@ +import { v4 } from 'uuid'; +import { migrate, version } from './084'; + +jest.mock('uuid', () => { + const actual = jest.requireActual('uuid'); + + return { + ...actual, + v4: jest.fn(), + }; +}); + +describe('migration #84', () => { + beforeEach(() => { + v4.mockImplementationOnce(() => 'network-configuration-id-1') + .mockImplementationOnce(() => 'network-configuration-id-2') + .mockImplementationOnce(() => 'network-configuration-id-3') + .mockImplementationOnce(() => 'network-configuration-id-4'); + }); + + afterEach(() => { + jest.resetAllMocks(); + }); + it('should update the version metadata', async () => { + const oldStorage = { + meta: { + version: 83, + }, + data: {}, + }; + + const newStorage = await migrate(oldStorage); + expect(newStorage.meta).toStrictEqual({ + version, + }); + }); + + it('should use the key of the networkConfigurations object to set the id of each network configuration', async () => { + const oldStorage = { + meta: { + version, + }, + data: { + NetworkController: { + networkConfigurations: { + 'network-configuration-id-1': { + chainId: '0x539', + nickname: 'Localhost 8545', + rpcPrefs: {}, + rpcUrl: 'http://localhost:8545', + ticker: 'ETH', + }, + 'network-configuration-id-2': { + chainId: '0xa4b1', + nickname: 'Arbitrum One', + rpcPrefs: { + blockExplorerUrl: 'https://explorer.arbitrum.io', + }, + rpcUrl: + 'https://arbitrum-mainnet.infura.io/v3/373266a93aab4acda48f89d4fe77c748', + ticker: 'ETH', + }, + 'network-configuration-id-3': { + chainId: '0x4e454152', + nickname: 'Aurora Mainnet', + rpcPrefs: { + blockExplorerUrl: 'https://aurorascan.dev/', + }, + rpcUrl: + 'https://aurora-mainnet.infura.io/v3/373266a93aab4acda48f89d4fe77c748', + ticker: 'Aurora ETH', + }, + 'network-configuration-id-4': { + chainId: '0x38', + nickname: + 'BNB Smart Chain (previously Binance Smart Chain Mainnet)', + rpcPrefs: { + blockExplorerUrl: 'https://bscscan.com/', + }, + rpcUrl: 'https://bsc-dataseed.binance.org/', + ticker: 'BNB', + }, + }, + }, + }, + }; + + const newStorage = await migrate(oldStorage); + + const expectedNewStorage = { + meta: { + version, + }, + data: { + NetworkController: { + networkConfigurations: { + 'network-configuration-id-1': { + chainId: '0x539', + nickname: 'Localhost 8545', + rpcPrefs: {}, + rpcUrl: 'http://localhost:8545', + ticker: 'ETH', + id: 'network-configuration-id-1', + }, + 'network-configuration-id-2': { + chainId: '0xa4b1', + nickname: 'Arbitrum One', + rpcPrefs: { + blockExplorerUrl: 'https://explorer.arbitrum.io', + }, + rpcUrl: + 'https://arbitrum-mainnet.infura.io/v3/373266a93aab4acda48f89d4fe77c748', + ticker: 'ETH', + id: 'network-configuration-id-2', + }, + 'network-configuration-id-3': { + chainId: '0x4e454152', + nickname: 'Aurora Mainnet', + rpcPrefs: { + blockExplorerUrl: 'https://aurorascan.dev/', + }, + rpcUrl: + 'https://aurora-mainnet.infura.io/v3/373266a93aab4acda48f89d4fe77c748', + ticker: 'Aurora ETH', + id: 'network-configuration-id-3', + }, + 'network-configuration-id-4': { + chainId: '0x38', + nickname: + 'BNB Smart Chain (previously Binance Smart Chain Mainnet)', + rpcPrefs: { + blockExplorerUrl: 'https://bscscan.com/', + }, + rpcUrl: 'https://bsc-dataseed.binance.org/', + ticker: 'BNB', + id: 'network-configuration-id-4', + }, + }, + }, + }, + }; + expect(newStorage).toStrictEqual(expectedNewStorage); + }); + + it('should not modify state if state.NetworkController is undefined', async () => { + const oldStorage = { + meta: { + version, + }, + data: { + testProperty: 'testValue', + }, + }; + + const newStorage = await migrate(oldStorage); + + const expectedNewStorage = { + meta: { + version, + }, + data: { + testProperty: 'testValue', + }, + }; + expect(newStorage).toStrictEqual(expectedNewStorage); + }); + + it('should not modify state if state.NetworkController is not an object', async () => { + const oldStorage = { + meta: { + version, + }, + data: { + NetworkController: false, + testProperty: 'testValue', + }, + }; + + const newStorage = await migrate(oldStorage); + + const expectedNewStorage = { + meta: { + version, + }, + data: { + NetworkController: false, + testProperty: 'testValue', + }, + }; + expect(newStorage).toStrictEqual(expectedNewStorage); + }); + + it('should not modify state if state.NetworkController.networkConfigurations is undefined', async () => { + const oldStorage = { + meta: { + version, + }, + data: { + NetworkController: { + testNetworkControllerProperty: 'testNetworkControllerValue', + networkConfigurations: undefined, + }, + testProperty: 'testValue', + }, + }; + + const newStorage = await migrate(oldStorage); + + const expectedNewStorage = { + meta: { + version, + }, + data: { + NetworkController: { + testNetworkControllerProperty: 'testNetworkControllerValue', + networkConfigurations: undefined, + }, + testProperty: 'testValue', + }, + }; + expect(newStorage).toStrictEqual(expectedNewStorage); + }); + + it('should not modify state if state.NetworkController.networkConfigurations is an empty object', async () => { + const oldStorage = { + meta: { + version, + }, + data: { + NetworkController: { + testNetworkControllerProperty: 'testNetworkControllerValue', + networkConfigurations: {}, + }, + testProperty: 'testValue', + }, + }; + + const newStorage = await migrate(oldStorage); + + const expectedNewStorage = { + meta: { + version, + }, + data: { + NetworkController: { + testNetworkControllerProperty: 'testNetworkControllerValue', + networkConfigurations: {}, + }, + testProperty: 'testValue', + }, + }; + expect(newStorage).toStrictEqual(expectedNewStorage); + }); +}); diff --git a/app/scripts/migrations/084.ts b/app/scripts/migrations/084.ts new file mode 100644 index 000000000..4ae81cdc8 --- /dev/null +++ b/app/scripts/migrations/084.ts @@ -0,0 +1,58 @@ +import { cloneDeep } from 'lodash'; +import { isObject } from '@metamask/utils'; + +export const version = 84; + +/** + * Ensure that each networkConfigurations object in state.NetworkController.networkConfigurations has an + * `id` property which matches the key pointing that object + * + * @param originalVersionedData - Versioned MetaMask extension state, exactly what we persist to dist. + * @param originalVersionedData.meta - State metadata. + * @param originalVersionedData.meta.version - The current state version. + * @param originalVersionedData.data - The persisted MetaMask state, keyed by controller. + * @returns Updated versioned MetaMask extension state. + */ +export async function migrate(originalVersionedData: { + meta: { version: number }; + data: Record; +}) { + const versionedData = cloneDeep(originalVersionedData); + versionedData.meta.version = version; + versionedData.data = transformState(versionedData.data); + return versionedData; +} + +function transformState(state: Record) { + if (!isObject(state.NetworkController)) { + return state; + } + const { NetworkController } = state; + + if (!isObject(NetworkController.networkConfigurations)) { + return state; + } + + const { networkConfigurations } = NetworkController; + + const newNetworkConfigurations: Record> = {}; + + for (const networkConfigurationId of Object.keys(networkConfigurations)) { + const networkConfiguration = networkConfigurations[networkConfigurationId]; + if (!isObject(networkConfiguration)) { + return state; + } + newNetworkConfigurations[networkConfigurationId] = { + ...networkConfiguration, + id: networkConfigurationId, + }; + } + + return { + ...state, + NetworkController: { + ...NetworkController, + networkConfigurations: newNetworkConfigurations, + }, + }; +} diff --git a/app/scripts/migrations/index.js b/app/scripts/migrations/index.js index c3f8e515f..54a09c2b4 100644 --- a/app/scripts/migrations/index.js +++ b/app/scripts/migrations/index.js @@ -87,6 +87,7 @@ import m080 from './080'; import * as m081 from './081'; import * as m082 from './082'; import * as m083 from './083'; +import * as m084 from './084'; const migrations = [ m002, @@ -171,6 +172,7 @@ const migrations = [ m081, m082, m083, + m084, ]; export default migrations; From 7f6bdf0178ea1e056cf8cdfc3fae61dd5adcf672 Mon Sep 17 00:00:00 2001 From: Mark Stacey Date: Wed, 12 Apr 2023 09:21:33 -0230 Subject: [PATCH 11/36] Fix e2e test for NFT interactions (#18540) The test `should transfer a single ERC721 NFT from one account to another` has been failing intermittently. It seems to be failing due to a race condition; the first render shows "Send Token" but later renders show "Send TDC". The test only passes if it runs fast enough to read the first render of the list item component. The test has been updated to look for the text "Send TDC", which is what the component shows from the second render onward. --- test/e2e/nft/erc721-interaction.spec.js | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/test/e2e/nft/erc721-interaction.spec.js b/test/e2e/nft/erc721-interaction.spec.js index 6e9a9e959..2d7d3abe9 100644 --- a/test/e2e/nft/erc721-interaction.spec.js +++ b/test/e2e/nft/erc721-interaction.spec.js @@ -62,9 +62,7 @@ describe('ERC721 NFTs testdapp interaction', function () { ); // Verify transaction - const completedTx = await driver.findElement('.list-item__title'); - const completedTxText = await completedTx.getText(); - assert.equal(completedTxText, 'Send Token'); + await driver.findElement({ text: 'Send TDC' }); }, ); }); From 6ebef431a800adac1895d74dbcfbd2c571a84719 Mon Sep 17 00:00:00 2001 From: Vinicius Stevam <45455812+vinistevam@users.noreply.github.com> Date: Wed, 12 Apr 2023 13:25:28 +0100 Subject: [PATCH 12/36] Remove METAMASK_NOTIFIER (#18437) --- app/scripts/background.js | 1 - 1 file changed, 1 deletion(-) diff --git a/app/scripts/background.js b/app/scripts/background.js index 1f73492cc..3aff0170b 100644 --- a/app/scripts/background.js +++ b/app/scripts/background.js @@ -80,7 +80,6 @@ log.setDefaultLevel(process.env.METAMASK_DEBUG ? 'debug' : 'info'); const platform = new ExtensionPlatform(); const notificationManager = new NotificationManager(); -global.METAMASK_NOTIFIER = notificationManager; let popupIsOpen = false; let notificationIsOpen = false; From 4c62bc445e11d024802730cfa9c703abf9e0c404 Mon Sep 17 00:00:00 2001 From: Mark Stacey Date: Wed, 12 Apr 2023 11:02:14 -0230 Subject: [PATCH 13/36] Update controllers to include core v42 release (#18464) All controllers from the core monorepo have been updated to be equal to or greater than the versions included in the v42 core release. This release included a breaking change to all controllers because the package `isomorphic-fetch` was removed. That package was used to ensure that a `fetch` polyfill was present, so that the controllers could be used in a non-browser context. This breaking change does not affect the extension because we already install a `fetch` polyfill in our unit test environment, and in a real build the real `fetch` API is present. The gas fee controller had an additional breaking change: the EIP 1559 API endpoint is now a required argument. This does not affect the extension because this argument was already being set. --- lavamoat/browserify/beta/policy.json | 139 ++++----------- lavamoat/browserify/desktop/policy.json | 184 +++++--------------- lavamoat/browserify/flask/policy.json | 184 +++++--------------- lavamoat/browserify/main/policy.json | 139 ++++----------- package.json | 20 +-- yarn.lock | 217 ++++++++---------------- 6 files changed, 230 insertions(+), 653 deletions(-) diff --git a/lavamoat/browserify/beta/policy.json b/lavamoat/browserify/beta/policy.json index ed13fb1b9..e594e00f6 100644 --- a/lavamoat/browserify/beta/policy.json +++ b/lavamoat/browserify/beta/policy.json @@ -660,28 +660,8 @@ }, "@metamask/address-book-controller": { "packages": { - "@metamask/address-book-controller>@metamask/base-controller": true, - "@metamask/address-book-controller>@metamask/controller-utils": true - } - }, - "@metamask/address-book-controller>@metamask/base-controller": { - "packages": { - "immer": true - } - }, - "@metamask/address-book-controller>@metamask/controller-utils": { - "globals": { - "console.error": true, - "fetch": true, - "setTimeout": true - }, - "packages": { - "@metamask/phishing-controller>isomorphic-fetch": true, - "browserify>buffer": true, - "eslint>fast-deep-equal": true, - "eth-ens-namehash": true, - "ethereumjs-util": true, - "ethjs>ethjs-unit": true + "@metamask/base-controller": true, + "@metamask/controller-utils": true } }, "@metamask/announcement-controller": { @@ -716,12 +696,12 @@ "@ethersproject/contracts": true, "@ethersproject/providers": true, "@metamask/assets-controllers>@metamask/abi-utils": true, - "@metamask/assets-controllers>@metamask/controller-utils": true, "@metamask/assets-controllers>@metamask/utils": true, "@metamask/assets-controllers>abort-controller": true, "@metamask/assets-controllers>multiformats": true, "@metamask/base-controller": true, "@metamask/contract-metadata": true, + "@metamask/controller-utils": true, "@metamask/metamask-eth-abis": true, "browserify>events": true, "eth-json-rpc-filters>async-mutex": true, @@ -750,21 +730,6 @@ "semver": true } }, - "@metamask/assets-controllers>@metamask/controller-utils": { - "globals": { - "console.error": true, - "fetch": true, - "setTimeout": true - }, - "packages": { - "@metamask/phishing-controller>isomorphic-fetch": true, - "browserify>buffer": true, - "eslint>fast-deep-equal": true, - "eth-ens-namehash": true, - "ethereumjs-util": true, - "ethjs>ethjs-unit": true - } - }, "@metamask/assets-controllers>@metamask/utils": { "globals": { "TextDecoder": true, @@ -1194,28 +1159,13 @@ }, "packages": { "@metamask/base-controller": true, - "@metamask/gas-fee-controller>@metamask/controller-utils": true, + "@metamask/controller-utils": true, "eth-query": true, "ethereumjs-util": true, "ethjs>ethjs-unit": true, "uuid": true } }, - "@metamask/gas-fee-controller>@metamask/controller-utils": { - "globals": { - "console.error": true, - "fetch": true, - "setTimeout": true - }, - "packages": { - "@metamask/phishing-controller>isomorphic-fetch": true, - "browserify>buffer": true, - "eslint>fast-deep-equal": true, - "eth-ens-namehash": true, - "ethereumjs-util": true, - "ethjs>ethjs-unit": true - } - }, "@metamask/jazzicon": { "globals": { "document.createElement": true, @@ -1314,8 +1264,8 @@ }, "@metamask/message-manager": { "packages": { + "@metamask/base-controller": true, "@metamask/controller-utils": true, - "@metamask/message-manager>@metamask/base-controller": true, "@metamask/message-manager>jsonschema": true, "browserify>buffer": true, "browserify>events": true, @@ -1324,11 +1274,6 @@ "uuid": true } }, - "@metamask/message-manager>@metamask/base-controller": { - "packages": { - "immer": true - } - }, "@metamask/message-manager>jsonschema": { "packages": { "browserify>url": true @@ -1359,8 +1304,8 @@ "console.error": true }, "packages": { + "@metamask/base-controller": true, "@metamask/controller-utils": true, - "@metamask/permission-controller>@metamask/base-controller": true, "@metamask/permission-controller>nanoid": true, "deep-freeze-strict": true, "eth-rpc-errors": true, @@ -1368,11 +1313,6 @@ "json-rpc-engine": true } }, - "@metamask/permission-controller>@metamask/base-controller": { - "packages": { - "immer": true - } - }, "@metamask/permission-controller>nanoid": { "globals": { "crypto.getRandomValues": true @@ -1384,46 +1324,11 @@ }, "packages": { "@metamask/base-controller": true, - "@metamask/phishing-controller>@metamask/controller-utils": true, - "@metamask/phishing-controller>isomorphic-fetch": true, + "@metamask/controller-utils": true, "@metamask/phishing-warning>eth-phishing-detect": true, "punycode": true } }, - "@metamask/phishing-controller>@metamask/controller-utils": { - "globals": { - "console.error": true, - "fetch": true, - "setTimeout": true - }, - "packages": { - "@metamask/phishing-controller>isomorphic-fetch": true, - "browserify>buffer": true, - "eslint>fast-deep-equal": true, - "eth-ens-namehash": true, - "ethereumjs-util": true, - "ethjs>ethjs-unit": true - } - }, - "@metamask/phishing-controller>isomorphic-fetch": { - "globals": { - "fetch.bind": true - }, - "packages": { - "@metamask/phishing-controller>isomorphic-fetch>whatwg-fetch": true - } - }, - "@metamask/phishing-controller>isomorphic-fetch>whatwg-fetch": { - "globals": { - "Blob": true, - "FileReader": true, - "FormData": true, - "URLSearchParams.prototype.isPrototypeOf": true, - "XMLHttpRequest": true, - "define": true, - "setTimeout": true - } - }, "@metamask/phishing-warning>eth-phishing-detect": { "packages": { "eslint>optionator>fast-levenshtein": true @@ -1486,14 +1391,19 @@ "@ethersproject/abi>@ethersproject/bytes": true, "@ethersproject/bignumber": true, "@ethersproject/providers": true, - "@metamask/base-controller": true, - "@metamask/phishing-controller>isomorphic-fetch": true, + "@metamask/smart-transactions-controller>@metamask/base-controller": true, "@metamask/smart-transactions-controller>@metamask/controller-utils": true, "@metamask/smart-transactions-controller>bignumber.js": true, + "@metamask/smart-transactions-controller>isomorphic-fetch": true, "fast-json-patch": true, "lodash": true } }, + "@metamask/smart-transactions-controller>@metamask/base-controller": { + "packages": { + "immer": true + } + }, "@metamask/smart-transactions-controller>@metamask/controller-utils": { "globals": { "console.error": true, @@ -1501,7 +1411,7 @@ "setTimeout": true }, "packages": { - "@metamask/phishing-controller>isomorphic-fetch": true, + "@metamask/smart-transactions-controller>isomorphic-fetch": true, "browserify>buffer": true, "eslint>fast-deep-equal": true, "eth-ens-namehash": true, @@ -1520,6 +1430,25 @@ "define": true } }, + "@metamask/smart-transactions-controller>isomorphic-fetch": { + "globals": { + "fetch.bind": true + }, + "packages": { + "@metamask/smart-transactions-controller>isomorphic-fetch>whatwg-fetch": true + } + }, + "@metamask/smart-transactions-controller>isomorphic-fetch>whatwg-fetch": { + "globals": { + "Blob": true, + "FileReader": true, + "FormData": true, + "URLSearchParams.prototype.isPrototypeOf": true, + "XMLHttpRequest": true, + "define": true, + "setTimeout": true + } + }, "@metamask/snaps-controllers>nanoid": { "globals": { "crypto.getRandomValues": true diff --git a/lavamoat/browserify/desktop/policy.json b/lavamoat/browserify/desktop/policy.json index 0d9164963..16b051712 100644 --- a/lavamoat/browserify/desktop/policy.json +++ b/lavamoat/browserify/desktop/policy.json @@ -660,28 +660,8 @@ }, "@metamask/address-book-controller": { "packages": { - "@metamask/address-book-controller>@metamask/base-controller": true, - "@metamask/address-book-controller>@metamask/controller-utils": true - } - }, - "@metamask/address-book-controller>@metamask/base-controller": { - "packages": { - "immer": true - } - }, - "@metamask/address-book-controller>@metamask/controller-utils": { - "globals": { - "console.error": true, - "fetch": true, - "setTimeout": true - }, - "packages": { - "@metamask/phishing-controller>isomorphic-fetch": true, - "browserify>buffer": true, - "eslint>fast-deep-equal": true, - "eth-ens-namehash": true, - "ethereumjs-util": true, - "ethjs>ethjs-unit": true + "@metamask/base-controller": true, + "@metamask/controller-utils": true } }, "@metamask/announcement-controller": { @@ -716,12 +696,12 @@ "@ethersproject/contracts": true, "@ethersproject/providers": true, "@metamask/assets-controllers>@metamask/abi-utils": true, - "@metamask/assets-controllers>@metamask/controller-utils": true, "@metamask/assets-controllers>@metamask/utils": true, "@metamask/assets-controllers>abort-controller": true, "@metamask/assets-controllers>multiformats": true, "@metamask/base-controller": true, "@metamask/contract-metadata": true, + "@metamask/controller-utils": true, "@metamask/metamask-eth-abis": true, "browserify>events": true, "eth-json-rpc-filters>async-mutex": true, @@ -750,21 +730,6 @@ "semver": true } }, - "@metamask/assets-controllers>@metamask/controller-utils": { - "globals": { - "console.error": true, - "fetch": true, - "setTimeout": true - }, - "packages": { - "@metamask/phishing-controller>isomorphic-fetch": true, - "browserify>buffer": true, - "eslint>fast-deep-equal": true, - "eth-ens-namehash": true, - "ethereumjs-util": true, - "ethjs>ethjs-unit": true - } - }, "@metamask/assets-controllers>@metamask/utils": { "globals": { "TextDecoder": true, @@ -1266,28 +1231,13 @@ }, "packages": { "@metamask/base-controller": true, - "@metamask/gas-fee-controller>@metamask/controller-utils": true, + "@metamask/controller-utils": true, "eth-query": true, "ethereumjs-util": true, "ethjs>ethjs-unit": true, "uuid": true } }, - "@metamask/gas-fee-controller>@metamask/controller-utils": { - "globals": { - "console.error": true, - "fetch": true, - "setTimeout": true - }, - "packages": { - "@metamask/phishing-controller>isomorphic-fetch": true, - "browserify>buffer": true, - "eslint>fast-deep-equal": true, - "eth-ens-namehash": true, - "ethereumjs-util": true, - "ethjs>ethjs-unit": true - } - }, "@metamask/jazzicon": { "globals": { "document.createElement": true, @@ -1386,8 +1336,8 @@ }, "@metamask/message-manager": { "packages": { + "@metamask/base-controller": true, "@metamask/controller-utils": true, - "@metamask/message-manager>@metamask/base-controller": true, "@metamask/message-manager>jsonschema": true, "browserify>buffer": true, "browserify>events": true, @@ -1396,11 +1346,6 @@ "uuid": true } }, - "@metamask/message-manager>@metamask/base-controller": { - "packages": { - "immer": true - } - }, "@metamask/message-manager>jsonschema": { "packages": { "browserify>url": true @@ -1408,31 +1353,11 @@ }, "@metamask/notification-controller": { "packages": { - "@metamask/notification-controller>@metamask/base-controller": true, - "@metamask/notification-controller>@metamask/controller-utils": true, + "@metamask/base-controller": true, + "@metamask/controller-utils": true, "@metamask/notification-controller>nanoid": true } }, - "@metamask/notification-controller>@metamask/base-controller": { - "packages": { - "immer": true - } - }, - "@metamask/notification-controller>@metamask/controller-utils": { - "globals": { - "console.error": true, - "fetch": true, - "setTimeout": true - }, - "packages": { - "@metamask/phishing-controller>isomorphic-fetch": true, - "browserify>buffer": true, - "eslint>fast-deep-equal": true, - "eth-ens-namehash": true, - "ethereumjs-util": true, - "ethjs>ethjs-unit": true - } - }, "@metamask/notification-controller>nanoid": { "globals": { "crypto.getRandomValues": true @@ -1458,8 +1383,8 @@ "console.error": true }, "packages": { + "@metamask/base-controller": true, "@metamask/controller-utils": true, - "@metamask/permission-controller>@metamask/base-controller": true, "@metamask/permission-controller>nanoid": true, "deep-freeze-strict": true, "eth-rpc-errors": true, @@ -1467,11 +1392,6 @@ "json-rpc-engine": true } }, - "@metamask/permission-controller>@metamask/base-controller": { - "packages": { - "immer": true - } - }, "@metamask/permission-controller>nanoid": { "globals": { "crypto.getRandomValues": true @@ -1483,46 +1403,11 @@ }, "packages": { "@metamask/base-controller": true, - "@metamask/phishing-controller>@metamask/controller-utils": true, - "@metamask/phishing-controller>isomorphic-fetch": true, + "@metamask/controller-utils": true, "@metamask/phishing-warning>eth-phishing-detect": true, "punycode": true } }, - "@metamask/phishing-controller>@metamask/controller-utils": { - "globals": { - "console.error": true, - "fetch": true, - "setTimeout": true - }, - "packages": { - "@metamask/phishing-controller>isomorphic-fetch": true, - "browserify>buffer": true, - "eslint>fast-deep-equal": true, - "eth-ens-namehash": true, - "ethereumjs-util": true, - "ethjs>ethjs-unit": true - } - }, - "@metamask/phishing-controller>isomorphic-fetch": { - "globals": { - "fetch.bind": true - }, - "packages": { - "@metamask/phishing-controller>isomorphic-fetch>whatwg-fetch": true - } - }, - "@metamask/phishing-controller>isomorphic-fetch>whatwg-fetch": { - "globals": { - "Blob": true, - "FileReader": true, - "FormData": true, - "URLSearchParams.prototype.isPrototypeOf": true, - "XMLHttpRequest": true, - "define": true, - "setTimeout": true - } - }, "@metamask/phishing-warning>eth-phishing-detect": { "packages": { "eslint>optionator>fast-levenshtein": true @@ -1602,15 +1487,10 @@ "setTimeout": true }, "packages": { - "@metamask/rate-limit-controller>@metamask/base-controller": true, + "@metamask/base-controller": true, "eth-rpc-errors": true } }, - "@metamask/rate-limit-controller>@metamask/base-controller": { - "packages": { - "immer": true - } - }, "@metamask/rpc-methods": { "packages": { "@metamask/key-tree": true, @@ -1674,14 +1554,19 @@ "@ethersproject/abi>@ethersproject/bytes": true, "@ethersproject/bignumber": true, "@ethersproject/providers": true, - "@metamask/base-controller": true, - "@metamask/phishing-controller>isomorphic-fetch": true, + "@metamask/smart-transactions-controller>@metamask/base-controller": true, "@metamask/smart-transactions-controller>@metamask/controller-utils": true, "@metamask/smart-transactions-controller>bignumber.js": true, + "@metamask/smart-transactions-controller>isomorphic-fetch": true, "fast-json-patch": true, "lodash": true } }, + "@metamask/smart-transactions-controller>@metamask/base-controller": { + "packages": { + "immer": true + } + }, "@metamask/smart-transactions-controller>@metamask/controller-utils": { "globals": { "console.error": true, @@ -1689,7 +1574,7 @@ "setTimeout": true }, "packages": { - "@metamask/phishing-controller>isomorphic-fetch": true, + "@metamask/smart-transactions-controller>isomorphic-fetch": true, "browserify>buffer": true, "eslint>fast-deep-equal": true, "eth-ens-namehash": true, @@ -1708,6 +1593,25 @@ "define": true } }, + "@metamask/smart-transactions-controller>isomorphic-fetch": { + "globals": { + "fetch.bind": true + }, + "packages": { + "@metamask/smart-transactions-controller>isomorphic-fetch>whatwg-fetch": true + } + }, + "@metamask/smart-transactions-controller>isomorphic-fetch>whatwg-fetch": { + "globals": { + "Blob": true, + "FileReader": true, + "FormData": true, + "URLSearchParams.prototype.isPrototypeOf": true, + "XMLHttpRequest": true, + "define": true, + "setTimeout": true + } + }, "@metamask/snaps-controllers": { "globals": { "URL": true, @@ -1719,12 +1623,11 @@ "setTimeout": true }, "packages": { + "@metamask/base-controller": true, "@metamask/permission-controller": true, "@metamask/post-message-stream": true, "@metamask/providers>@metamask/object-multiplex": true, "@metamask/rpc-methods": true, - "@metamask/snaps-controllers>@metamask/base-controller": true, - "@metamask/snaps-controllers>@metamask/subject-metadata-controller": true, "@metamask/snaps-controllers>@xstate/fsm": true, "@metamask/snaps-controllers>concat-stream": true, "@metamask/snaps-controllers>gunzip-maybe": true, @@ -1733,6 +1636,7 @@ "@metamask/snaps-controllers>tar-stream": true, "@metamask/snaps-utils": true, "@metamask/snaps-utils>@metamask/snaps-registry": true, + "@metamask/subject-metadata-controller": true, "@metamask/utils": true, "eth-rpc-errors": true, "json-rpc-engine": true, @@ -1740,16 +1644,6 @@ "pump": true } }, - "@metamask/snaps-controllers>@metamask/base-controller": { - "packages": { - "immer": true - } - }, - "@metamask/snaps-controllers>@metamask/subject-metadata-controller": { - "packages": { - "@metamask/snaps-controllers>@metamask/base-controller": true - } - }, "@metamask/snaps-controllers>concat-stream": { "packages": { "@metamask/snaps-controllers>concat-stream>readable-stream": true, diff --git a/lavamoat/browserify/flask/policy.json b/lavamoat/browserify/flask/policy.json index 0d9164963..16b051712 100644 --- a/lavamoat/browserify/flask/policy.json +++ b/lavamoat/browserify/flask/policy.json @@ -660,28 +660,8 @@ }, "@metamask/address-book-controller": { "packages": { - "@metamask/address-book-controller>@metamask/base-controller": true, - "@metamask/address-book-controller>@metamask/controller-utils": true - } - }, - "@metamask/address-book-controller>@metamask/base-controller": { - "packages": { - "immer": true - } - }, - "@metamask/address-book-controller>@metamask/controller-utils": { - "globals": { - "console.error": true, - "fetch": true, - "setTimeout": true - }, - "packages": { - "@metamask/phishing-controller>isomorphic-fetch": true, - "browserify>buffer": true, - "eslint>fast-deep-equal": true, - "eth-ens-namehash": true, - "ethereumjs-util": true, - "ethjs>ethjs-unit": true + "@metamask/base-controller": true, + "@metamask/controller-utils": true } }, "@metamask/announcement-controller": { @@ -716,12 +696,12 @@ "@ethersproject/contracts": true, "@ethersproject/providers": true, "@metamask/assets-controllers>@metamask/abi-utils": true, - "@metamask/assets-controllers>@metamask/controller-utils": true, "@metamask/assets-controllers>@metamask/utils": true, "@metamask/assets-controllers>abort-controller": true, "@metamask/assets-controllers>multiformats": true, "@metamask/base-controller": true, "@metamask/contract-metadata": true, + "@metamask/controller-utils": true, "@metamask/metamask-eth-abis": true, "browserify>events": true, "eth-json-rpc-filters>async-mutex": true, @@ -750,21 +730,6 @@ "semver": true } }, - "@metamask/assets-controllers>@metamask/controller-utils": { - "globals": { - "console.error": true, - "fetch": true, - "setTimeout": true - }, - "packages": { - "@metamask/phishing-controller>isomorphic-fetch": true, - "browserify>buffer": true, - "eslint>fast-deep-equal": true, - "eth-ens-namehash": true, - "ethereumjs-util": true, - "ethjs>ethjs-unit": true - } - }, "@metamask/assets-controllers>@metamask/utils": { "globals": { "TextDecoder": true, @@ -1266,28 +1231,13 @@ }, "packages": { "@metamask/base-controller": true, - "@metamask/gas-fee-controller>@metamask/controller-utils": true, + "@metamask/controller-utils": true, "eth-query": true, "ethereumjs-util": true, "ethjs>ethjs-unit": true, "uuid": true } }, - "@metamask/gas-fee-controller>@metamask/controller-utils": { - "globals": { - "console.error": true, - "fetch": true, - "setTimeout": true - }, - "packages": { - "@metamask/phishing-controller>isomorphic-fetch": true, - "browserify>buffer": true, - "eslint>fast-deep-equal": true, - "eth-ens-namehash": true, - "ethereumjs-util": true, - "ethjs>ethjs-unit": true - } - }, "@metamask/jazzicon": { "globals": { "document.createElement": true, @@ -1386,8 +1336,8 @@ }, "@metamask/message-manager": { "packages": { + "@metamask/base-controller": true, "@metamask/controller-utils": true, - "@metamask/message-manager>@metamask/base-controller": true, "@metamask/message-manager>jsonschema": true, "browserify>buffer": true, "browserify>events": true, @@ -1396,11 +1346,6 @@ "uuid": true } }, - "@metamask/message-manager>@metamask/base-controller": { - "packages": { - "immer": true - } - }, "@metamask/message-manager>jsonschema": { "packages": { "browserify>url": true @@ -1408,31 +1353,11 @@ }, "@metamask/notification-controller": { "packages": { - "@metamask/notification-controller>@metamask/base-controller": true, - "@metamask/notification-controller>@metamask/controller-utils": true, + "@metamask/base-controller": true, + "@metamask/controller-utils": true, "@metamask/notification-controller>nanoid": true } }, - "@metamask/notification-controller>@metamask/base-controller": { - "packages": { - "immer": true - } - }, - "@metamask/notification-controller>@metamask/controller-utils": { - "globals": { - "console.error": true, - "fetch": true, - "setTimeout": true - }, - "packages": { - "@metamask/phishing-controller>isomorphic-fetch": true, - "browserify>buffer": true, - "eslint>fast-deep-equal": true, - "eth-ens-namehash": true, - "ethereumjs-util": true, - "ethjs>ethjs-unit": true - } - }, "@metamask/notification-controller>nanoid": { "globals": { "crypto.getRandomValues": true @@ -1458,8 +1383,8 @@ "console.error": true }, "packages": { + "@metamask/base-controller": true, "@metamask/controller-utils": true, - "@metamask/permission-controller>@metamask/base-controller": true, "@metamask/permission-controller>nanoid": true, "deep-freeze-strict": true, "eth-rpc-errors": true, @@ -1467,11 +1392,6 @@ "json-rpc-engine": true } }, - "@metamask/permission-controller>@metamask/base-controller": { - "packages": { - "immer": true - } - }, "@metamask/permission-controller>nanoid": { "globals": { "crypto.getRandomValues": true @@ -1483,46 +1403,11 @@ }, "packages": { "@metamask/base-controller": true, - "@metamask/phishing-controller>@metamask/controller-utils": true, - "@metamask/phishing-controller>isomorphic-fetch": true, + "@metamask/controller-utils": true, "@metamask/phishing-warning>eth-phishing-detect": true, "punycode": true } }, - "@metamask/phishing-controller>@metamask/controller-utils": { - "globals": { - "console.error": true, - "fetch": true, - "setTimeout": true - }, - "packages": { - "@metamask/phishing-controller>isomorphic-fetch": true, - "browserify>buffer": true, - "eslint>fast-deep-equal": true, - "eth-ens-namehash": true, - "ethereumjs-util": true, - "ethjs>ethjs-unit": true - } - }, - "@metamask/phishing-controller>isomorphic-fetch": { - "globals": { - "fetch.bind": true - }, - "packages": { - "@metamask/phishing-controller>isomorphic-fetch>whatwg-fetch": true - } - }, - "@metamask/phishing-controller>isomorphic-fetch>whatwg-fetch": { - "globals": { - "Blob": true, - "FileReader": true, - "FormData": true, - "URLSearchParams.prototype.isPrototypeOf": true, - "XMLHttpRequest": true, - "define": true, - "setTimeout": true - } - }, "@metamask/phishing-warning>eth-phishing-detect": { "packages": { "eslint>optionator>fast-levenshtein": true @@ -1602,15 +1487,10 @@ "setTimeout": true }, "packages": { - "@metamask/rate-limit-controller>@metamask/base-controller": true, + "@metamask/base-controller": true, "eth-rpc-errors": true } }, - "@metamask/rate-limit-controller>@metamask/base-controller": { - "packages": { - "immer": true - } - }, "@metamask/rpc-methods": { "packages": { "@metamask/key-tree": true, @@ -1674,14 +1554,19 @@ "@ethersproject/abi>@ethersproject/bytes": true, "@ethersproject/bignumber": true, "@ethersproject/providers": true, - "@metamask/base-controller": true, - "@metamask/phishing-controller>isomorphic-fetch": true, + "@metamask/smart-transactions-controller>@metamask/base-controller": true, "@metamask/smart-transactions-controller>@metamask/controller-utils": true, "@metamask/smart-transactions-controller>bignumber.js": true, + "@metamask/smart-transactions-controller>isomorphic-fetch": true, "fast-json-patch": true, "lodash": true } }, + "@metamask/smart-transactions-controller>@metamask/base-controller": { + "packages": { + "immer": true + } + }, "@metamask/smart-transactions-controller>@metamask/controller-utils": { "globals": { "console.error": true, @@ -1689,7 +1574,7 @@ "setTimeout": true }, "packages": { - "@metamask/phishing-controller>isomorphic-fetch": true, + "@metamask/smart-transactions-controller>isomorphic-fetch": true, "browserify>buffer": true, "eslint>fast-deep-equal": true, "eth-ens-namehash": true, @@ -1708,6 +1593,25 @@ "define": true } }, + "@metamask/smart-transactions-controller>isomorphic-fetch": { + "globals": { + "fetch.bind": true + }, + "packages": { + "@metamask/smart-transactions-controller>isomorphic-fetch>whatwg-fetch": true + } + }, + "@metamask/smart-transactions-controller>isomorphic-fetch>whatwg-fetch": { + "globals": { + "Blob": true, + "FileReader": true, + "FormData": true, + "URLSearchParams.prototype.isPrototypeOf": true, + "XMLHttpRequest": true, + "define": true, + "setTimeout": true + } + }, "@metamask/snaps-controllers": { "globals": { "URL": true, @@ -1719,12 +1623,11 @@ "setTimeout": true }, "packages": { + "@metamask/base-controller": true, "@metamask/permission-controller": true, "@metamask/post-message-stream": true, "@metamask/providers>@metamask/object-multiplex": true, "@metamask/rpc-methods": true, - "@metamask/snaps-controllers>@metamask/base-controller": true, - "@metamask/snaps-controllers>@metamask/subject-metadata-controller": true, "@metamask/snaps-controllers>@xstate/fsm": true, "@metamask/snaps-controllers>concat-stream": true, "@metamask/snaps-controllers>gunzip-maybe": true, @@ -1733,6 +1636,7 @@ "@metamask/snaps-controllers>tar-stream": true, "@metamask/snaps-utils": true, "@metamask/snaps-utils>@metamask/snaps-registry": true, + "@metamask/subject-metadata-controller": true, "@metamask/utils": true, "eth-rpc-errors": true, "json-rpc-engine": true, @@ -1740,16 +1644,6 @@ "pump": true } }, - "@metamask/snaps-controllers>@metamask/base-controller": { - "packages": { - "immer": true - } - }, - "@metamask/snaps-controllers>@metamask/subject-metadata-controller": { - "packages": { - "@metamask/snaps-controllers>@metamask/base-controller": true - } - }, "@metamask/snaps-controllers>concat-stream": { "packages": { "@metamask/snaps-controllers>concat-stream>readable-stream": true, diff --git a/lavamoat/browserify/main/policy.json b/lavamoat/browserify/main/policy.json index ed13fb1b9..e594e00f6 100644 --- a/lavamoat/browserify/main/policy.json +++ b/lavamoat/browserify/main/policy.json @@ -660,28 +660,8 @@ }, "@metamask/address-book-controller": { "packages": { - "@metamask/address-book-controller>@metamask/base-controller": true, - "@metamask/address-book-controller>@metamask/controller-utils": true - } - }, - "@metamask/address-book-controller>@metamask/base-controller": { - "packages": { - "immer": true - } - }, - "@metamask/address-book-controller>@metamask/controller-utils": { - "globals": { - "console.error": true, - "fetch": true, - "setTimeout": true - }, - "packages": { - "@metamask/phishing-controller>isomorphic-fetch": true, - "browserify>buffer": true, - "eslint>fast-deep-equal": true, - "eth-ens-namehash": true, - "ethereumjs-util": true, - "ethjs>ethjs-unit": true + "@metamask/base-controller": true, + "@metamask/controller-utils": true } }, "@metamask/announcement-controller": { @@ -716,12 +696,12 @@ "@ethersproject/contracts": true, "@ethersproject/providers": true, "@metamask/assets-controllers>@metamask/abi-utils": true, - "@metamask/assets-controllers>@metamask/controller-utils": true, "@metamask/assets-controllers>@metamask/utils": true, "@metamask/assets-controllers>abort-controller": true, "@metamask/assets-controllers>multiformats": true, "@metamask/base-controller": true, "@metamask/contract-metadata": true, + "@metamask/controller-utils": true, "@metamask/metamask-eth-abis": true, "browserify>events": true, "eth-json-rpc-filters>async-mutex": true, @@ -750,21 +730,6 @@ "semver": true } }, - "@metamask/assets-controllers>@metamask/controller-utils": { - "globals": { - "console.error": true, - "fetch": true, - "setTimeout": true - }, - "packages": { - "@metamask/phishing-controller>isomorphic-fetch": true, - "browserify>buffer": true, - "eslint>fast-deep-equal": true, - "eth-ens-namehash": true, - "ethereumjs-util": true, - "ethjs>ethjs-unit": true - } - }, "@metamask/assets-controllers>@metamask/utils": { "globals": { "TextDecoder": true, @@ -1194,28 +1159,13 @@ }, "packages": { "@metamask/base-controller": true, - "@metamask/gas-fee-controller>@metamask/controller-utils": true, + "@metamask/controller-utils": true, "eth-query": true, "ethereumjs-util": true, "ethjs>ethjs-unit": true, "uuid": true } }, - "@metamask/gas-fee-controller>@metamask/controller-utils": { - "globals": { - "console.error": true, - "fetch": true, - "setTimeout": true - }, - "packages": { - "@metamask/phishing-controller>isomorphic-fetch": true, - "browserify>buffer": true, - "eslint>fast-deep-equal": true, - "eth-ens-namehash": true, - "ethereumjs-util": true, - "ethjs>ethjs-unit": true - } - }, "@metamask/jazzicon": { "globals": { "document.createElement": true, @@ -1314,8 +1264,8 @@ }, "@metamask/message-manager": { "packages": { + "@metamask/base-controller": true, "@metamask/controller-utils": true, - "@metamask/message-manager>@metamask/base-controller": true, "@metamask/message-manager>jsonschema": true, "browserify>buffer": true, "browserify>events": true, @@ -1324,11 +1274,6 @@ "uuid": true } }, - "@metamask/message-manager>@metamask/base-controller": { - "packages": { - "immer": true - } - }, "@metamask/message-manager>jsonschema": { "packages": { "browserify>url": true @@ -1359,8 +1304,8 @@ "console.error": true }, "packages": { + "@metamask/base-controller": true, "@metamask/controller-utils": true, - "@metamask/permission-controller>@metamask/base-controller": true, "@metamask/permission-controller>nanoid": true, "deep-freeze-strict": true, "eth-rpc-errors": true, @@ -1368,11 +1313,6 @@ "json-rpc-engine": true } }, - "@metamask/permission-controller>@metamask/base-controller": { - "packages": { - "immer": true - } - }, "@metamask/permission-controller>nanoid": { "globals": { "crypto.getRandomValues": true @@ -1384,46 +1324,11 @@ }, "packages": { "@metamask/base-controller": true, - "@metamask/phishing-controller>@metamask/controller-utils": true, - "@metamask/phishing-controller>isomorphic-fetch": true, + "@metamask/controller-utils": true, "@metamask/phishing-warning>eth-phishing-detect": true, "punycode": true } }, - "@metamask/phishing-controller>@metamask/controller-utils": { - "globals": { - "console.error": true, - "fetch": true, - "setTimeout": true - }, - "packages": { - "@metamask/phishing-controller>isomorphic-fetch": true, - "browserify>buffer": true, - "eslint>fast-deep-equal": true, - "eth-ens-namehash": true, - "ethereumjs-util": true, - "ethjs>ethjs-unit": true - } - }, - "@metamask/phishing-controller>isomorphic-fetch": { - "globals": { - "fetch.bind": true - }, - "packages": { - "@metamask/phishing-controller>isomorphic-fetch>whatwg-fetch": true - } - }, - "@metamask/phishing-controller>isomorphic-fetch>whatwg-fetch": { - "globals": { - "Blob": true, - "FileReader": true, - "FormData": true, - "URLSearchParams.prototype.isPrototypeOf": true, - "XMLHttpRequest": true, - "define": true, - "setTimeout": true - } - }, "@metamask/phishing-warning>eth-phishing-detect": { "packages": { "eslint>optionator>fast-levenshtein": true @@ -1486,14 +1391,19 @@ "@ethersproject/abi>@ethersproject/bytes": true, "@ethersproject/bignumber": true, "@ethersproject/providers": true, - "@metamask/base-controller": true, - "@metamask/phishing-controller>isomorphic-fetch": true, + "@metamask/smart-transactions-controller>@metamask/base-controller": true, "@metamask/smart-transactions-controller>@metamask/controller-utils": true, "@metamask/smart-transactions-controller>bignumber.js": true, + "@metamask/smart-transactions-controller>isomorphic-fetch": true, "fast-json-patch": true, "lodash": true } }, + "@metamask/smart-transactions-controller>@metamask/base-controller": { + "packages": { + "immer": true + } + }, "@metamask/smart-transactions-controller>@metamask/controller-utils": { "globals": { "console.error": true, @@ -1501,7 +1411,7 @@ "setTimeout": true }, "packages": { - "@metamask/phishing-controller>isomorphic-fetch": true, + "@metamask/smart-transactions-controller>isomorphic-fetch": true, "browserify>buffer": true, "eslint>fast-deep-equal": true, "eth-ens-namehash": true, @@ -1520,6 +1430,25 @@ "define": true } }, + "@metamask/smart-transactions-controller>isomorphic-fetch": { + "globals": { + "fetch.bind": true + }, + "packages": { + "@metamask/smart-transactions-controller>isomorphic-fetch>whatwg-fetch": true + } + }, + "@metamask/smart-transactions-controller>isomorphic-fetch>whatwg-fetch": { + "globals": { + "Blob": true, + "FileReader": true, + "FormData": true, + "URLSearchParams.prototype.isPrototypeOf": true, + "XMLHttpRequest": true, + "define": true, + "setTimeout": true + } + }, "@metamask/snaps-controllers>nanoid": { "globals": { "crypto.getRandomValues": true diff --git a/package.json b/package.json index 80df52fff..c5a718e75 100644 --- a/package.json +++ b/package.json @@ -226,11 +226,11 @@ "@keystonehq/metamask-airgapped-keyring": "^0.6.1", "@lavamoat/snow": "^1.5.0", "@material-ui/core": "^4.11.0", - "@metamask/address-book-controller": "^1.0.0", - "@metamask/announcement-controller": "^2.0.1", - "@metamask/approval-controller": "^1.0.0", - "@metamask/assets-controllers": "^4.0.1", - "@metamask/base-controller": "^1.0.0", + "@metamask/address-book-controller": "^2.0.0", + "@metamask/announcement-controller": "^3.0.0", + "@metamask/approval-controller": "^2.0.0", + "@metamask/assets-controllers": "^5.0.0", + "@metamask/base-controller": "^2.0.0", "@metamask/contract-metadata": "^2.3.1", "@metamask/controller-utils": "^3.1.0", "@metamask/design-tokens": "^1.9.0", @@ -242,19 +242,19 @@ "@metamask/eth-ledger-bridge-keyring": "^0.13.0", "@metamask/eth-token-tracker": "^4.0.0", "@metamask/etherscan-link": "^2.2.0", - "@metamask/gas-fee-controller": "^3.0.0", + "@metamask/gas-fee-controller": "^4.0.0", "@metamask/jazzicon": "^2.0.0", "@metamask/key-tree": "^7.0.0", "@metamask/logo": "^3.1.1", "@metamask/message-manager": "^2.1.0", "@metamask/metamask-eth-abis": "^3.0.0", - "@metamask/notification-controller": "^1.0.0", + "@metamask/notification-controller": "^2.0.0", "@metamask/obs-store": "^8.1.0", "@metamask/permission-controller": "^3.1.0", - "@metamask/phishing-controller": "^2.0.0", + "@metamask/phishing-controller": "^3.0.0", "@metamask/post-message-stream": "^6.0.0", "@metamask/providers": "^10.2.1", - "@metamask/rate-limit-controller": "^1.0.0", + "@metamask/rate-limit-controller": "^2.0.0", "@metamask/rpc-methods": "^0.32.2", "@metamask/safe-event-emitter": "^2.0.0", "@metamask/scure-bip39": "^2.0.3", @@ -263,7 +263,7 @@ "@metamask/snaps-controllers": "^0.32.2", "@metamask/snaps-ui": "^0.32.2", "@metamask/snaps-utils": "^0.32.2", - "@metamask/subject-metadata-controller": "^1.0.0", + "@metamask/subject-metadata-controller": "^2.0.0", "@metamask/swappable-obj-proxy": "^2.1.0", "@metamask/utils": "^5.0.0", "@ngraveio/bc-ur": "^1.1.6", diff --git a/yarn.lock b/yarn.lock index 0921937e9..f31c8b428 100644 --- a/yarn.lock +++ b/yarn.lock @@ -3576,35 +3576,22 @@ __metadata: languageName: node linkType: hard -"@metamask/address-book-controller@npm:^1.0.0": - version: 1.0.0 - resolution: "@metamask/address-book-controller@npm:1.0.0" +"@metamask/address-book-controller@npm:^2.0.0": + version: 2.0.0 + resolution: "@metamask/address-book-controller@npm:2.0.0" dependencies: - "@metamask/base-controller": ~1.0.0 - "@metamask/controller-utils": ~1.0.0 - checksum: bb0bf08bf5fd7f78ab62c6212d9ec6767860c17d7096e21cbbe46c0c86b03916433cf2d08f95a45b08fe05a9ad4a2f0292056e874e68cabd37d1df02917bfa83 + "@metamask/base-controller": ^2.0.0 + "@metamask/controller-utils": ^3.0.0 + checksum: 7f0c091a2d95a398900b6126ca2331772a65531aa8750e2270f72a806c2ea2e0a26bd2836ab81f13efb550f240309b254395e8fb16b087b6897beb162e175c33 languageName: node linkType: hard -"@metamask/announcement-controller@npm:^2.0.1": - version: 2.0.1 - resolution: "@metamask/announcement-controller@npm:2.0.1" +"@metamask/announcement-controller@npm:^3.0.0": + version: 3.0.0 + resolution: "@metamask/announcement-controller@npm:3.0.0" dependencies: - "@metamask/base-controller": ^1.1.2 - checksum: 170db513315dc81f131fc38a560a6d8959c4af9111a26c4f263847ab41cf2520a6dc4c77cfe40d8566ca4e87c3fe85a67def9e5499e9ccd02678792f4a8b9e56 - languageName: node - linkType: hard - -"@metamask/approval-controller@npm:^1.0.0, @metamask/approval-controller@npm:^1.0.1": - version: 1.1.0 - resolution: "@metamask/approval-controller@npm:1.1.0" - dependencies: - "@metamask/base-controller": ^1.1.2 - "@metamask/controller-utils": ^2.0.0 - eth-rpc-errors: ^4.0.0 - immer: ^9.0.6 - nanoid: ^3.1.31 - checksum: 96a354ccd4765eb997f35ccbc86114c40e6da839e83b89bf64eedc986d74f28898c204eae5037978497f823fabc2a675c9c19c1ccae6535f70eb7ab0a72cbfc4 + "@metamask/base-controller": ^2.0.0 + checksum: 14fe28e7db72ca3457618016d33dd66a566c991d4692a62e83ddf89b74f82277e5ed47dbd128aff9079cc3a5045ef79b808c106acc18717a51fb8ef9d05e0d5b languageName: node linkType: hard @@ -3621,20 +3608,20 @@ __metadata: languageName: node linkType: hard -"@metamask/assets-controllers@npm:^4.0.1": - version: 4.0.1 - resolution: "@metamask/assets-controllers@npm:4.0.1" +"@metamask/assets-controllers@npm:^5.0.0": + version: 5.0.1 + resolution: "@metamask/assets-controllers@npm:5.0.1" dependencies: "@ethersproject/bignumber": ^5.7.0 "@ethersproject/contracts": ^5.7.0 "@ethersproject/providers": ^5.7.0 "@metamask/abi-utils": ^1.1.0 - "@metamask/base-controller": ^1.1.2 - "@metamask/contract-metadata": ^2.1.0 - "@metamask/controller-utils": ^2.0.0 + "@metamask/base-controller": ^2.0.0 + "@metamask/contract-metadata": ^2.3.1 + "@metamask/controller-utils": ^3.1.0 "@metamask/metamask-eth-abis": 3.0.0 - "@metamask/network-controller": ^4.0.0 - "@metamask/preferences-controller": ^1.0.2 + "@metamask/network-controller": ^6.0.0 + "@metamask/preferences-controller": ^3.0.0 "@metamask/utils": ^3.3.1 "@types/uuid": ^8.3.0 abort-controller: ^3.0.0 @@ -3648,8 +3635,8 @@ __metadata: single-call-balance-checker-abi: ^1.0.0 uuid: ^8.3.2 peerDependencies: - "@metamask/network-controller": ^4.0.0 - checksum: 18d3212640a50d2ff56134bceb275718880b80fccddc8b52b325f9119a6fb06919935320b9604550bbda1fc73f13122b3d185330cb423a616daf8f465ace29e8 + "@metamask/network-controller": ^6.0.0 + checksum: aa2ab83752c121fe410f191660c4b57be9cc74cbe462e8c35b86077160c8d6640ccf19d0fa423c8123803e00ba6d7c9112d68b9e058c0fbac1df6210ad3be2b7 languageName: node linkType: hard @@ -3667,7 +3654,7 @@ __metadata: languageName: node linkType: hard -"@metamask/base-controller@npm:^1.0.0, @metamask/base-controller@npm:^1.1.1, @metamask/base-controller@npm:^1.1.2": +"@metamask/base-controller@npm:^1.0.0, @metamask/base-controller@npm:^1.1.1": version: 1.1.2 resolution: "@metamask/base-controller@npm:1.1.2" dependencies: @@ -3687,16 +3674,6 @@ __metadata: languageName: node linkType: hard -"@metamask/base-controller@npm:~1.0.0": - version: 1.0.0 - resolution: "@metamask/base-controller@npm:1.0.0" - dependencies: - "@metamask/controller-utils": ~1.0.0 - immer: ^9.0.6 - checksum: 40affd024add235548c886bd8ad3348682231af8d4a2f7e6fbc5dea5ffa077ed48535f21ac17017c640c50b5d17b5211c30cbb23b7025f360801e72ccbd61a5d - languageName: node - linkType: hard - "@metamask/browser-passworder@npm:^4.0.2": version: 4.0.2 resolution: "@metamask/browser-passworder@npm:4.0.2" @@ -3704,14 +3681,14 @@ __metadata: languageName: node linkType: hard -"@metamask/contract-metadata@npm:^2.1.0, @metamask/contract-metadata@npm:^2.3.1": +"@metamask/contract-metadata@npm:^2.3.1": version: 2.3.1 resolution: "@metamask/contract-metadata@npm:2.3.1" checksum: 95dcc27f661a3e380c0cca8c6d90fb1777e31ab3fe16909fd5c67844125510e3f8e9eca05c9069fde34c77df3b66e56111c7a908a07623e88052ef147eff4315 languageName: node linkType: hard -"@metamask/controller-utils@npm:^1.0.0, @metamask/controller-utils@npm:~1.0.0": +"@metamask/controller-utils@npm:^1.0.0": version: 1.0.0 resolution: "@metamask/controller-utils@npm:1.0.0" dependencies: @@ -3984,13 +3961,13 @@ __metadata: languageName: node linkType: hard -"@metamask/gas-fee-controller@npm:^3.0.0": - version: 3.0.0 - resolution: "@metamask/gas-fee-controller@npm:3.0.0" +"@metamask/gas-fee-controller@npm:^4.0.0": + version: 4.0.1 + resolution: "@metamask/gas-fee-controller@npm:4.0.1" dependencies: - "@metamask/base-controller": ^1.1.2 - "@metamask/controller-utils": ^2.0.0 - "@metamask/network-controller": ^3.0.0 + "@metamask/base-controller": ^2.0.0 + "@metamask/controller-utils": ^3.1.0 + "@metamask/network-controller": ^6.0.0 "@types/uuid": ^8.3.0 babel-runtime: ^6.26.0 eth-query: ^2.1.2 @@ -3999,8 +3976,8 @@ __metadata: immer: ^9.0.6 uuid: ^8.3.2 peerDependencies: - "@metamask/network-controller": ^3.0.0 - checksum: 8cdd43a265094dd5e41f0094c278cde351d290446711e6b39de26f842faa993c050e5506cafe8d1c2fb0c4ee3f0f97c5af5fa6528de10e76d071b56fb9673da8 + "@metamask/network-controller": ^6.0.0 + checksum: a7f1557321679a289bc6dcae107c6af57d6862db3120c35bd27fff744ec12b80ee6e5294eb9d658a1f8415920267326167098739f2c7c1abac299d8a5cc69dcc languageName: node linkType: hard @@ -4076,47 +4053,34 @@ __metadata: languageName: node linkType: hard -"@metamask/network-controller@npm:^3.0.0": - version: 3.0.0 - resolution: "@metamask/network-controller@npm:3.0.0" +"@metamask/network-controller@npm:^6.0.0": + version: 6.0.0 + resolution: "@metamask/network-controller@npm:6.0.0" dependencies: - "@metamask/base-controller": ^1.1.2 - "@metamask/controller-utils": ^2.0.0 + "@metamask/base-controller": ^2.0.0 + "@metamask/controller-utils": ^3.1.0 + "@metamask/swappable-obj-proxy": ^2.1.0 + "@metamask/utils": ^3.3.1 async-mutex: ^0.2.6 babel-runtime: ^6.26.0 eth-json-rpc-infura: ^5.1.0 eth-query: ^2.1.2 immer: ^9.0.6 + uuid: ^8.3.2 web3-provider-engine: ^16.0.3 - checksum: 3ae56a252c11dbd6dc843f9db8b30768d2475afd499c99bdccdc850517031b447bab9ca4f6647da7e64c7a0efd61d029f59a89e4ec702e34a99733dd8e7f93ff + checksum: 5bbc0c1c876b7842d2a643962174880695df05b75164ddbd68e2ee62b352ad89525c49635ae256b11a035573bc0ea985a563f1bd86d84775d35afe8597c18dc5 languageName: node linkType: hard -"@metamask/network-controller@npm:^4.0.0": - version: 4.0.0 - resolution: "@metamask/network-controller@npm:4.0.0" +"@metamask/notification-controller@npm:^2.0.0": + version: 2.0.0 + resolution: "@metamask/notification-controller@npm:2.0.0" dependencies: - "@metamask/base-controller": ^1.1.2 - "@metamask/controller-utils": ^2.0.0 - async-mutex: ^0.2.6 - babel-runtime: ^6.26.0 - eth-json-rpc-infura: ^5.1.0 - eth-query: ^2.1.2 - immer: ^9.0.6 - web3-provider-engine: ^16.0.3 - checksum: 19dfa74cefc435f5205020c68b948956c52689cdfaa153dc37d116e866f61903396b4b19055975d6fc9ab4185b34e87a641eba8aebb864e9161ed5e561b35263 - languageName: node - linkType: hard - -"@metamask/notification-controller@npm:^1.0.0": - version: 1.0.0 - resolution: "@metamask/notification-controller@npm:1.0.0" - dependencies: - "@metamask/base-controller": ~1.0.0 - "@metamask/controller-utils": ~1.0.0 + "@metamask/base-controller": ^2.0.0 + "@metamask/controller-utils": ^3.0.0 immer: ^9.0.6 nanoid: ^3.1.31 - checksum: 317d94b329123cba0fea526b7c5508ef943a655e57beaeeb21269a471bda1f30e9bbdd5583bb1d6e3e0715db48ddf07748a94d1f83b8c6171decfd2e6b992e4b + checksum: 22d299bc2816f1822d63b71820bb8c5b55b28696a91c6a2f38fcc743e79140de4f5d0a30daffcd0a141ca7efe07024c80340fb19a457375698fb5bf9fc181859 languageName: node linkType: hard @@ -4162,26 +4126,6 @@ __metadata: languageName: node linkType: hard -"@metamask/permission-controller@npm:^1.0.1": - version: 1.0.2 - resolution: "@metamask/permission-controller@npm:1.0.2" - dependencies: - "@metamask/approval-controller": ^1.0.1 - "@metamask/base-controller": ^1.1.1 - "@metamask/controller-utils": ^1.0.0 - "@metamask/types": ^1.1.0 - "@types/deep-freeze-strict": ^1.1.0 - deep-freeze-strict: ^1.1.1 - eth-rpc-errors: ^4.0.0 - immer: ^9.0.6 - json-rpc-engine: ^6.1.0 - nanoid: ^3.1.31 - peerDependencies: - "@metamask/approval-controller": ^1.0.1 - checksum: 06c062d232817f6816fb6038b7139f32324ceb3f6c7398effabd5fe6eed8e643b414202e20239302ce01aaa68503a0a49f0692512a433040e89907306260a3db - languageName: node - linkType: hard - "@metamask/permission-controller@npm:^3.0.0, @metamask/permission-controller@npm:^3.1.0": version: 3.1.0 resolution: "@metamask/permission-controller@npm:3.1.0" @@ -4202,17 +4146,16 @@ __metadata: languageName: node linkType: hard -"@metamask/phishing-controller@npm:^2.0.0": - version: 2.0.0 - resolution: "@metamask/phishing-controller@npm:2.0.0" +"@metamask/phishing-controller@npm:^3.0.0": + version: 3.0.0 + resolution: "@metamask/phishing-controller@npm:3.0.0" dependencies: - "@metamask/base-controller": ^1.1.2 - "@metamask/controller-utils": ^2.0.0 + "@metamask/base-controller": ^2.0.0 + "@metamask/controller-utils": ^3.0.0 "@types/punycode": ^2.1.0 eth-phishing-detect: ^1.2.0 - isomorphic-fetch: ^3.0.0 punycode: ^2.1.1 - checksum: 8ad20a7cdac8fc5f450bb157d19b0b780d82490571f4d33e1afde871ace078cf4f885e2e5be4abc6e1e551986d5f44a3200b076d5545764f5492dfaf005419e4 + checksum: b0b9a86cba1928f0fd22a2aed196d75dc19a5e56547efe1b533d7ae06eaaf9432a6ee5004a8fd477f52310b50c2f3635a1e70ac83e3670f4cc6a1f488a674d73 languageName: node linkType: hard @@ -4243,13 +4186,13 @@ __metadata: languageName: node linkType: hard -"@metamask/preferences-controller@npm:^1.0.2": - version: 1.0.2 - resolution: "@metamask/preferences-controller@npm:1.0.2" +"@metamask/preferences-controller@npm:^3.0.0": + version: 3.0.0 + resolution: "@metamask/preferences-controller@npm:3.0.0" dependencies: - "@metamask/base-controller": ^1.1.2 - "@metamask/controller-utils": ^2.0.0 - checksum: 466fbe6919da3e9003a7325443fc9c3f609072b6bbfe2216ed66bca04b43cc2bb033a016f4d0f906dc1cf19a8ad0c1abe7e767c02c38684293f5e12423a89f1d + "@metamask/base-controller": ^2.0.0 + "@metamask/controller-utils": ^3.1.0 + checksum: e86d689270704b7ac72d1d72762d957e2ebca649fa3c354490cbc939eeaed22ce81d4ccfa1afba1167c3bbf17c9444286c7d79904820980007e1f8e8927dc588 languageName: node linkType: hard @@ -4273,14 +4216,14 @@ __metadata: languageName: node linkType: hard -"@metamask/rate-limit-controller@npm:^1.0.0": - version: 1.0.0 - resolution: "@metamask/rate-limit-controller@npm:1.0.0" +"@metamask/rate-limit-controller@npm:^2.0.0": + version: 2.0.0 + resolution: "@metamask/rate-limit-controller@npm:2.0.0" dependencies: - "@metamask/base-controller": ~1.0.0 + "@metamask/base-controller": ^2.0.0 eth-rpc-errors: ^4.0.0 immer: ^9.0.6 - checksum: ad8242bf8cf5e42217437736ed546df6aeb3355d192575dfd916052b11e7f95a0d078ef25154e2582626a6f339353dc180827255df85148ba82ff2be795cd293 + checksum: da0b3cb6607201b07ee54f81a4ce8ce3ce047acde33c5f483aa0c8c907bcbe32e70944d95cf4f0df5d4b664388be832320d6973cf61cd00e22f81d16ff0ddfd2 languageName: node linkType: hard @@ -4445,18 +4388,6 @@ __metadata: languageName: node linkType: hard -"@metamask/subject-metadata-controller@npm:^1.0.0": - version: 1.0.1 - resolution: "@metamask/subject-metadata-controller@npm:1.0.1" - dependencies: - "@metamask/base-controller": ^1.1.1 - "@metamask/permission-controller": ^1.0.1 - "@metamask/types": ^1.1.0 - immer: ^9.0.6 - checksum: 8686dd9a7a274bc7fe2315f63687ec91161a7d18da829a2c6a08728c0ab61ceabbeae256e96247a059acc5c74d5dfbe48667893791d9f608e4989d5c4cc62c89 - languageName: node - linkType: hard - "@metamask/subject-metadata-controller@npm:^2.0.0": version: 2.0.0 resolution: "@metamask/subject-metadata-controller@npm:2.0.0" @@ -24278,12 +24209,12 @@ __metadata: "@lavamoat/lavapack": ^5.0.0 "@lavamoat/snow": ^1.5.0 "@material-ui/core": ^4.11.0 - "@metamask/address-book-controller": ^1.0.0 - "@metamask/announcement-controller": ^2.0.1 - "@metamask/approval-controller": ^1.0.0 - "@metamask/assets-controllers": ^4.0.1 + "@metamask/address-book-controller": ^2.0.0 + "@metamask/announcement-controller": ^3.0.0 + "@metamask/approval-controller": ^2.0.0 + "@metamask/assets-controllers": ^5.0.0 "@metamask/auto-changelog": ^2.1.0 - "@metamask/base-controller": ^1.0.0 + "@metamask/base-controller": ^2.0.0 "@metamask/contract-metadata": ^2.3.1 "@metamask/controller-utils": ^3.1.0 "@metamask/design-tokens": ^1.9.0 @@ -24301,20 +24232,20 @@ __metadata: "@metamask/eth-token-tracker": ^4.0.0 "@metamask/etherscan-link": ^2.2.0 "@metamask/forwarder": ^1.1.0 - "@metamask/gas-fee-controller": ^3.0.0 + "@metamask/gas-fee-controller": ^4.0.0 "@metamask/jazzicon": ^2.0.0 "@metamask/key-tree": ^7.0.0 "@metamask/logo": ^3.1.1 "@metamask/message-manager": ^2.1.0 "@metamask/metamask-eth-abis": ^3.0.0 - "@metamask/notification-controller": ^1.0.0 + "@metamask/notification-controller": ^2.0.0 "@metamask/obs-store": ^8.1.0 "@metamask/permission-controller": ^3.1.0 - "@metamask/phishing-controller": ^2.0.0 + "@metamask/phishing-controller": ^3.0.0 "@metamask/phishing-warning": ^2.1.0 "@metamask/post-message-stream": ^6.0.0 "@metamask/providers": ^10.2.1 - "@metamask/rate-limit-controller": ^1.0.0 + "@metamask/rate-limit-controller": ^2.0.0 "@metamask/rpc-methods": ^0.32.2 "@metamask/safe-event-emitter": ^2.0.0 "@metamask/scure-bip39": ^2.0.3 @@ -24323,7 +24254,7 @@ __metadata: "@metamask/snaps-controllers": ^0.32.2 "@metamask/snaps-ui": ^0.32.2 "@metamask/snaps-utils": ^0.32.2 - "@metamask/subject-metadata-controller": ^1.0.0 + "@metamask/subject-metadata-controller": ^2.0.0 "@metamask/swappable-obj-proxy": ^2.1.0 "@metamask/test-dapp": ^5.6.0 "@metamask/utils": ^5.0.0 From 065c4997536d7a369ae6315634cd796de08af247 Mon Sep 17 00:00:00 2001 From: Garrett Bear Date: Wed, 12 Apr 2023 08:55:24 -0700 Subject: [PATCH 14/36] update ButtonIcon to TS (#18448) * update ButtonIcon to TS lint updates fix lint issues add ref fix as prop test updates * box and icon updates for support * Update ui/components/component-library/text-field/README.mdx Co-authored-by: George Marshall * fix disabled * update types for as * update readme * fix storybook * george changes to button icon * revert headerbase * box prop back to HTMLElementTagNameMap --------- Co-authored-by: George Marshall Co-authored-by: legobeat <109787230+legobeat@users.noreply.github.com> --- ui/components/app/beta-header/index.js | 2 +- ...nnected-accounts-list-options.component.js | 2 +- .../app/dropdowns/network-dropdown.js | 2 +- .../flask/snap-authorship/snap-authorship.js | 3 +- ui/components/app/menu-bar/menu-bar.js | 2 +- .../contract-details-modal.js | 2 +- .../customize-nonce.component.js | 2 +- .../edit-approval-permission.component.js | 2 +- .../hold-to-reveal-modal.js | 8 +- .../new-account-modal.component.js | 2 +- ui/components/app/nft-details/nft-details.js | 2 +- ui/components/app/nft-options/nft-options.js | 2 +- .../app/wallet-overview/eth-overview.js | 5 +- .../banner-base/banner-base.js | 7 +- .../component-library/button-icon/README.mdx | 15 +- ...test.js.snap => button-icon.test.tsx.snap} | 0 ...con.stories.js => button-icon.stories.tsx} | 115 +++----------- .../button-icon/button-icon.test.tsx | 147 ++++++++++++++++++ .../button-icon/button-icon.tsx | 72 +++++++++ .../button-icon/button-icon.types.ts | 50 ++++++ .../__snapshots__/button-icon.test.js.snap | 16 ++ .../{ => deprecated}/button-icon.constants.js | 2 +- .../{ => deprecated}/button-icon.js | 8 +- .../{ => deprecated}/button-icon.test.js | 26 ++-- .../button-icon/{ => deprecated}/index.js | 0 .../component-library/button-icon/index.ts | 3 + .../component-library/header-base/README.mdx | 7 +- .../header-base/header-base.stories.tsx | 27 ++-- .../component-library/icon/icon.stories.tsx | 4 +- ui/components/component-library/index.js | 2 +- .../text-field-search/text-field-search.js | 6 +- .../component-library/text-field/README.mdx | 4 +- .../account-list-item/account-list-item.js | 2 +- .../network-list-item/network-list-item.js | 3 +- ui/components/ui/box/box.d.ts | 1 - ui/components/ui/callout/callout.js | 2 +- .../contract-token-values.js | 2 +- .../ui/editable-label/editable-label.js | 2 +- .../nickname-popover.component.js | 2 +- ui/components/ui/popover/popover.component.js | 3 +- ui/pages/add-nft/add-nft.js | 2 +- ui/pages/asset/components/asset-options.js | 2 +- .../confirm-approve-content.component.js | 3 +- ui/pages/home/home.component.js | 3 +- .../compliance-feature-page.js | 3 +- ui/pages/notifications/notifications.js | 2 +- .../add-recipient/domain-input.component.js | 2 +- .../view-contact/view-contact.component.js | 2 +- ui/pages/settings/settings.component.js | 2 +- ui/pages/token-details/token-details-page.js | 2 +- 50 files changed, 405 insertions(+), 182 deletions(-) rename ui/components/component-library/button-icon/__snapshots__/{button-icon.test.js.snap => button-icon.test.tsx.snap} (100%) rename ui/components/component-library/button-icon/{button-icon.stories.js => button-icon.stories.tsx} (50%) create mode 100644 ui/components/component-library/button-icon/button-icon.test.tsx create mode 100644 ui/components/component-library/button-icon/button-icon.tsx create mode 100644 ui/components/component-library/button-icon/button-icon.types.ts create mode 100644 ui/components/component-library/button-icon/deprecated/__snapshots__/button-icon.test.js.snap rename ui/components/component-library/button-icon/{ => deprecated}/button-icon.constants.js (50%) rename ui/components/component-library/button-icon/{ => deprecated}/button-icon.js (91%) rename ui/components/component-library/button-icon/{ => deprecated}/button-icon.test.js (85%) rename ui/components/component-library/button-icon/{ => deprecated}/index.js (100%) create mode 100644 ui/components/component-library/button-icon/index.ts diff --git a/ui/components/app/beta-header/index.js b/ui/components/app/beta-header/index.js index 12c922be6..f030d53f6 100644 --- a/ui/components/app/beta-header/index.js +++ b/ui/components/app/beta-header/index.js @@ -14,7 +14,7 @@ import { import { BETA_BUGS_URL } from '../../../helpers/constants/beta'; import { hideBetaHeader } from '../../../store/actions'; -import { ButtonIcon } from '../../component-library'; +import { ButtonIcon } from '../../component-library/button-icon/deprecated'; import { ICON_NAMES, ICON_SIZES, diff --git a/ui/components/app/connected-accounts-list/connected-accounts-list-options/connected-accounts-list-options.component.js b/ui/components/app/connected-accounts-list/connected-accounts-list-options/connected-accounts-list-options.component.js index 522de0097..90d1146c9 100644 --- a/ui/components/app/connected-accounts-list/connected-accounts-list-options/connected-accounts-list-options.component.js +++ b/ui/components/app/connected-accounts-list/connected-accounts-list-options/connected-accounts-list-options.component.js @@ -2,7 +2,7 @@ import PropTypes from 'prop-types'; import React, { useRef } from 'react'; import { Menu } from '../../../ui/menu'; import { ICON_NAMES } from '../../../component-library/icon/deprecated'; -import { ButtonIcon } from '../../../component-library'; +import { ButtonIcon } from '../../../component-library/button-icon/deprecated'; import { useI18nContext } from '../../../../hooks/useI18nContext'; const ConnectedAccountsListOptions = ({ diff --git a/ui/components/app/dropdowns/network-dropdown.js b/ui/components/app/dropdowns/network-dropdown.js index 33f95e3b4..131b700f6 100644 --- a/ui/components/app/dropdowns/network-dropdown.js +++ b/ui/components/app/dropdowns/network-dropdown.js @@ -32,7 +32,7 @@ import { ADD_POPULAR_CUSTOM_NETWORK, ADVANCED_ROUTE, } from '../../../helpers/constants/routes'; -import { ButtonIcon } from '../../component-library'; +import { ButtonIcon } from '../../component-library/button-icon/deprecated'; import { Icon, ICON_NAMES, diff --git a/ui/components/app/flask/snap-authorship/snap-authorship.js b/ui/components/app/flask/snap-authorship/snap-authorship.js index 760d42096..4c44d20eb 100644 --- a/ui/components/app/flask/snap-authorship/snap-authorship.js +++ b/ui/components/app/flask/snap-authorship/snap-authorship.js @@ -20,7 +20,8 @@ import { getSnapName, removeSnapIdPrefix, } from '../../../../helpers/utils/util'; -import { Text, ButtonIcon } from '../../../component-library'; +import { ButtonIcon } from '../../../component-library/button-icon/deprecated'; +import { Text } from '../../../component-library'; import { ICON_NAMES, ICON_SIZES, diff --git a/ui/components/app/menu-bar/menu-bar.js b/ui/components/app/menu-bar/menu-bar.js index 32ce255b3..fac87c26f 100644 --- a/ui/components/app/menu-bar/menu-bar.js +++ b/ui/components/app/menu-bar/menu-bar.js @@ -14,7 +14,7 @@ import { CONNECTED_ACCOUNTS_ROUTE } from '../../../helpers/constants/routes'; import { useI18nContext } from '../../../hooks/useI18nContext'; import { getOriginOfCurrentTab } from '../../../selectors'; import { MetaMetricsContext } from '../../../contexts/metametrics'; -import { ButtonIcon } from '../../component-library'; +import { ButtonIcon } from '../../component-library/button-icon/deprecated'; import { ICON_NAMES } from '../../component-library/icon/deprecated'; import { GlobalMenu } from '../../multichain/global-menu'; import AccountOptionsMenu from './account-options-menu'; diff --git a/ui/components/app/modals/contract-details-modal/contract-details-modal.js b/ui/components/app/modals/contract-details-modal/contract-details-modal.js index 6c6a5e7ec..563feb982 100644 --- a/ui/components/app/modals/contract-details-modal/contract-details-modal.js +++ b/ui/components/app/modals/contract-details-modal/contract-details-modal.js @@ -25,7 +25,7 @@ import { useCopyToClipboard } from '../../../../hooks/useCopyToClipboard'; import { getAddressBookEntry } from '../../../../selectors'; import { TokenStandard } from '../../../../../shared/constants/transaction'; import NftCollectionImage from '../../../ui/nft-collection-image/nft-collection-image'; -import { ButtonIcon } from '../../../component-library'; +import { ButtonIcon } from '../../../component-library/button-icon/deprecated'; import { ICON_NAMES } from '../../../component-library/icon/deprecated'; export default function ContractDetailsModal({ diff --git a/ui/components/app/modals/customize-nonce/customize-nonce.component.js b/ui/components/app/modals/customize-nonce/customize-nonce.component.js index 0c4f8b4cb..e1e0a944b 100644 --- a/ui/components/app/modals/customize-nonce/customize-nonce.component.js +++ b/ui/components/app/modals/customize-nonce/customize-nonce.component.js @@ -15,7 +15,7 @@ import Box from '../../../ui/box'; import withModalProps from '../../../../helpers/higher-order-components/with-modal-props'; import { useI18nContext } from '../../../../hooks/useI18nContext'; import ZENDESK_URLS from '../../../../helpers/constants/zendesk-url'; -import { ButtonIcon } from '../../../component-library'; +import { ButtonIcon } from '../../../component-library/button-icon/deprecated'; import { ICON_NAMES, ICON_SIZES, diff --git a/ui/components/app/modals/edit-approval-permission/edit-approval-permission.component.js b/ui/components/app/modals/edit-approval-permission/edit-approval-permission.component.js index 43602ba42..3c3b85cf0 100644 --- a/ui/components/app/modals/edit-approval-permission/edit-approval-permission.component.js +++ b/ui/components/app/modals/edit-approval-permission/edit-approval-permission.component.js @@ -10,7 +10,7 @@ import { calcTokenAmount, toPrecisionWithoutTrailingZeros, } from '../../../../../shared/lib/transactions-controller-utils'; -import { ButtonIcon } from '../../../component-library'; +import { ButtonIcon } from '../../../component-library/button-icon/deprecated'; import { ICON_SIZES, ICON_NAMES, diff --git a/ui/components/app/modals/hold-to-reveal-modal/hold-to-reveal-modal.js b/ui/components/app/modals/hold-to-reveal-modal/hold-to-reveal-modal.js index 3368017e3..de5a6a962 100644 --- a/ui/components/app/modals/hold-to-reveal-modal/hold-to-reveal-modal.js +++ b/ui/components/app/modals/hold-to-reveal-modal/hold-to-reveal-modal.js @@ -2,12 +2,8 @@ import PropTypes from 'prop-types'; import React from 'react'; import withModalProps from '../../../../helpers/higher-order-components/with-modal-props'; import Box from '../../../ui/box'; -import { - Text, - Button, - BUTTON_TYPES, - ButtonIcon, -} from '../../../component-library'; +import { Text, Button, BUTTON_TYPES } from '../../../component-library'; +import { ButtonIcon } from '../../../component-library/button-icon/deprecated'; import { ICON_NAMES } from '../../../component-library/icon/deprecated'; import { AlignItems, diff --git a/ui/components/app/modals/new-account-modal/new-account-modal.component.js b/ui/components/app/modals/new-account-modal/new-account-modal.component.js index 5e05df4ea..9ff7f9cdd 100644 --- a/ui/components/app/modals/new-account-modal/new-account-modal.component.js +++ b/ui/components/app/modals/new-account-modal/new-account-modal.component.js @@ -1,7 +1,7 @@ import React, { Component } from 'react'; import PropTypes from 'prop-types'; import Button from '../../../ui/button/button.component'; -import { ButtonIcon } from '../../../component-library'; +import { ButtonIcon } from '../../../component-library/button-icon/deprecated'; import { ICON_NAMES } from '../../../component-library/icon/deprecated'; export default class NewAccountModal extends Component { diff --git a/ui/components/app/nft-details/nft-details.js b/ui/components/app/nft-details/nft-details.js index 3885126dd..1888f32f1 100644 --- a/ui/components/app/nft-details/nft-details.js +++ b/ui/components/app/nft-details/nft-details.js @@ -53,7 +53,7 @@ import { TokenStandard, } from '../../../../shared/constants/transaction'; import NftDefaultImage from '../nft-default-image'; -import { ButtonIcon } from '../../component-library'; +import { ButtonIcon } from '../../component-library/button-icon/deprecated'; import { ICON_NAMES } from '../../component-library/icon/deprecated'; import Tooltip from '../../ui/tooltip'; import { decWEIToDecETH } from '../../../../shared/modules/conversion.utils'; diff --git a/ui/components/app/nft-options/nft-options.js b/ui/components/app/nft-options/nft-options.js index 2241da905..e67525f7c 100644 --- a/ui/components/app/nft-options/nft-options.js +++ b/ui/components/app/nft-options/nft-options.js @@ -3,7 +3,7 @@ import PropTypes from 'prop-types'; import { I18nContext } from '../../../contexts/i18n'; import { Menu, MenuItem } from '../../ui/menu'; -import { ButtonIcon } from '../../component-library'; +import { ButtonIcon } from '../../component-library/button-icon/deprecated'; import { ICON_NAMES } from '../../component-library/icon/deprecated'; import { Color } from '../../../helpers/constants/design-system'; diff --git a/ui/components/app/wallet-overview/eth-overview.js b/ui/components/app/wallet-overview/eth-overview.js index 8027e1e89..0352039c4 100644 --- a/ui/components/app/wallet-overview/eth-overview.js +++ b/ui/components/app/wallet-overview/eth-overview.js @@ -37,7 +37,10 @@ import { import Spinner from '../../ui/spinner'; import { startNewDraftTransaction } from '../../../ducks/send'; import { AssetType } from '../../../../shared/constants/transaction'; -import { ButtonIcon, BUTTON_ICON_SIZES } from '../../component-library'; +import { + ButtonIcon, + BUTTON_ICON_SIZES, +} from '../../component-library/button-icon/deprecated'; import { Icon, ICON_NAMES } from '../../component-library/icon/deprecated'; import { IconColor } from '../../../helpers/constants/design-system'; import useRamps from '../../../hooks/experiences/useRamps'; diff --git a/ui/components/component-library/banner-base/banner-base.js b/ui/components/component-library/banner-base/banner-base.js index 5f9dcc28e..fa4d39464 100644 --- a/ui/components/component-library/banner-base/banner-base.js +++ b/ui/components/component-library/banner-base/banner-base.js @@ -1,8 +1,9 @@ import React from 'react'; import PropTypes from 'prop-types'; import classnames from 'classnames'; -import { ButtonIcon, ButtonLink, Text } from '..'; -import { IconName } from '../icon'; +import { ButtonIcon } from '../button-icon/deprecated'; +import { ButtonLink, Text } from '..'; +import { ICON_NAMES } from '../icon/deprecated'; import Box from '../../ui/box'; @@ -72,7 +73,7 @@ export const BannerBase = ({ ```jsx -import { Size } from '../../../helpers/constants/design-system'; +import { ButtonIconSize } from '../../../helpers/constants/design-system'; import { ButtonIcon } from '../ui/component-library'; - - + + ``` ### Aria Label diff --git a/ui/components/component-library/button-icon/__snapshots__/button-icon.test.js.snap b/ui/components/component-library/button-icon/__snapshots__/button-icon.test.tsx.snap similarity index 100% rename from ui/components/component-library/button-icon/__snapshots__/button-icon.test.js.snap rename to ui/components/component-library/button-icon/__snapshots__/button-icon.test.tsx.snap diff --git a/ui/components/component-library/button-icon/button-icon.stories.js b/ui/components/component-library/button-icon/button-icon.stories.tsx similarity index 50% rename from ui/components/component-library/button-icon/button-icon.stories.js rename to ui/components/component-library/button-icon/button-icon.stories.tsx index a86ad8312..d9afbca05 100644 --- a/ui/components/component-library/button-icon/button-icon.stories.js +++ b/ui/components/component-library/button-icon/button-icon.stories.tsx @@ -1,35 +1,11 @@ import React from 'react'; -import { - AlignItems, - Color, - DISPLAY, - FLEX_DIRECTION, - Size, -} from '../../../helpers/constants/design-system'; -import Box from '../../ui/box/box'; +import { ComponentStory, ComponentMeta } from '@storybook/react'; +import { Color } from '../../../helpers/constants/design-system'; import { IconName } from '..'; -import { BUTTON_ICON_SIZES } from './button-icon.constants'; +import { ButtonIconSize } from './button-icon.types'; import { ButtonIcon } from './button-icon'; import README from './README.mdx'; -const marginSizeControlOptions = [ - undefined, - 0, - 1, - 2, - 3, - 4, - 5, - 6, - 7, - 8, - 9, - 10, - 11, - 12, - 'auto', -]; - export default { title: 'Components/ComponentLibrary/ButtonIcon', @@ -40,58 +16,18 @@ export default { }, }, argTypes: { - ariaLabel: { - control: 'text', - }, as: { control: 'select', options: ['button', 'a'], }, - className: { - control: 'text', - }, - color: { - control: 'select', - options: Object.values(Color), - }, - disabled: { - control: 'boolean', - }, - href: { - control: 'text', - }, - iconName: { - control: 'select', - options: Object.values(IconName), - }, - size: { - control: 'select', - options: Object.values(BUTTON_ICON_SIZES), - }, - marginTop: { - options: marginSizeControlOptions, - control: 'select', - table: { category: 'box props' }, - }, - marginRight: { - options: marginSizeControlOptions, - control: 'select', - table: { category: 'box props' }, - }, - marginBottom: { - options: marginSizeControlOptions, - control: 'select', - table: { category: 'box props' }, - }, - marginLeft: { - options: marginSizeControlOptions, - control: 'select', - table: { category: 'box props' }, - }, }, -}; +} as ComponentMeta; -export const DefaultStory = (args) => ; +const Template: ComponentStory = (args) => ( + +); + +export const DefaultStory = Template.bind({}); DefaultStory.args = { iconName: IconName.Close, @@ -100,7 +36,9 @@ DefaultStory.args = { DefaultStory.storyName = 'Default'; -export const IconNameStory = (args) => ; +export const IconNameStory: ComponentStory = (args) => ( + +); IconNameStory.args = { iconName: IconName.Close, @@ -109,32 +47,27 @@ IconNameStory.args = { IconNameStory.storyName = 'IconName'; -export const SizeStory = (args) => ( - +export const SizeStory: ComponentStory = (args) => ( + <> - + ); SizeStory.storyName = 'Size'; -export const AriaLabel = (args) => ( +export const AriaLabel: ComponentStory = (args) => ( <> ( ); -export const As = (args) => ( - +export const As: ComponentStory = (args) => ( + <> ( iconName={IconName.Export} ariaLabel="demo" /> - + ); -export const Href = (args) => ( +export const Href: ComponentStory = (args) => ( ); @@ -178,7 +111,7 @@ Href.args = { color: Color.primaryDefault, }; -export const ColorStory = (args) => ( +export const ColorStory: ComponentStory = (args) => ( ); ColorStory.storyName = 'Color'; @@ -187,7 +120,7 @@ ColorStory.args = { color: Color.primaryDefault, }; -export const Disabled = (args) => ( +export const Disabled: ComponentStory = (args) => ( ); diff --git a/ui/components/component-library/button-icon/button-icon.test.tsx b/ui/components/component-library/button-icon/button-icon.test.tsx new file mode 100644 index 000000000..1b4ae6a93 --- /dev/null +++ b/ui/components/component-library/button-icon/button-icon.test.tsx @@ -0,0 +1,147 @@ +/* eslint-disable jest/require-top-level-describe */ +import { render } from '@testing-library/react'; +import React from 'react'; +import { IconColor } from '../../../helpers/constants/design-system'; +import { IconName } from '..'; +import { ButtonIconSize } from './button-icon.types'; +import { ButtonIcon } from './button-icon'; + +describe('ButtonIcon', () => { + it('should render button element correctly', () => { + const { getByTestId, container } = render( + , + ); + expect(container.querySelector('button')).toBeDefined(); + expect(getByTestId('button-icon')).toHaveClass('mm-button-icon'); + expect(container).toMatchSnapshot(); + }); + + it('should render anchor element correctly', () => { + const { getByTestId, container } = render( + , + ); + expect(getByTestId('button-icon')).toHaveClass('mm-button-icon'); + const anchor = container.getElementsByTagName('a').length; + expect(anchor).toBe(1); + }); + + it('should render anchor element correctly using href', () => { + const { getByTestId, getByRole } = render( + , + ); + expect(getByTestId('button-icon')).toHaveClass('mm-button-icon'); + expect(getByRole('link')).toBeDefined(); + }); + + it('should render with different size classes', () => { + const { getByTestId } = render( + <> + + + , + ); + expect(getByTestId(ButtonIconSize.Sm)).toHaveClass( + `mm-button-icon--size-${ButtonIconSize.Sm}`, + ); + expect(getByTestId(ButtonIconSize.Lg)).toHaveClass( + `mm-button-icon--size-${ButtonIconSize.Lg}`, + ); + }); + + it('should render with different colors', () => { + const { getByTestId } = render( + <> + + + , + ); + expect(getByTestId(IconColor.iconDefault)).toHaveClass( + `box--color-${IconColor.iconDefault}`, + ); + expect(getByTestId(IconColor.errorDefault)).toHaveClass( + `box--color-${IconColor.errorDefault}`, + ); + }); + + it('should render with added classname', () => { + const { getByTestId } = render( + , + ); + expect(getByTestId('classname')).toHaveClass('mm-button-icon--test'); + }); + + it('should render with different button states', () => { + const { getByTestId } = render( + <> + + , + ); + + expect(getByTestId('disabled')).toHaveClass(`mm-button-icon--disabled`); + expect(getByTestId('disabled')).toBeDisabled(); + }); + it('should render with icon', () => { + const { getByTestId } = render( + , + ); + + expect(getByTestId('button-icon')).toBeDefined(); + }); + + it('should render with aria-label', () => { + const { getByLabelText } = render( + , + ); + + expect(getByLabelText('add')).toBeDefined(); + }); +}); diff --git a/ui/components/component-library/button-icon/button-icon.tsx b/ui/components/component-library/button-icon/button-icon.tsx new file mode 100644 index 000000000..3831cb838 --- /dev/null +++ b/ui/components/component-library/button-icon/button-icon.tsx @@ -0,0 +1,72 @@ +import React from 'react'; +import classnames from 'classnames'; + +import { + AlignItems, + BackgroundColor, + BorderRadius, + DISPLAY, + IconColor, + JustifyContent, +} from '../../../helpers/constants/design-system'; + +import Box from '../../ui/box'; +import { Icon, IconSize } from '../icon'; + +import { ButtonIconSize, ButtonIconProps } from './button-icon.types'; + +const buttonIconSizeToIconSize: Record = { + [ButtonIconSize.Sm]: IconSize.Sm, + [ButtonIconSize.Lg]: IconSize.Lg, +}; + +export const ButtonIcon = React.forwardRef( + ( + { + ariaLabel, + as = 'button', + className = '', + color = IconColor.iconDefault, + href, + size = ButtonIconSize.Lg, + iconName, + disabled, + iconProps, + ...props + }: ButtonIconProps, + ref: React.Ref, + ) => { + const Tag = href ? 'a' : as; + const isDisabled = disabled && Tag === 'button'; + return ( + + + + ); + }, +); diff --git a/ui/components/component-library/button-icon/button-icon.types.ts b/ui/components/component-library/button-icon/button-icon.types.ts new file mode 100644 index 000000000..d50d4cd85 --- /dev/null +++ b/ui/components/component-library/button-icon/button-icon.types.ts @@ -0,0 +1,50 @@ +import type { BoxProps } from '../../ui/box/box.d'; +import { IconName } from '../icon'; +import type { IconProps } from '../icon'; +import { IconColor } from '../../../helpers/constants/design-system'; + +export enum ButtonIconSize { + Sm = 'sm', + Lg = 'lg', +} + +export interface ButtonIconProps extends BoxProps { + /** + * String that adds an accessible name for ButtonIcon + */ + ariaLabel: string; + /** + * The polymorphic `as` prop allows you to change the root HTML element of the Button component between `button` and `a` tag + */ + as?: 'button' | 'a'; + /** + * An additional className to apply to the ButtonIcon. + */ + className?: string; + /** + * The color of the ButtonIcon component should use the IconColor object from + * ./ui/helpers/constants/design-system.js + */ + color?: IconColor; + /** + * Boolean to disable button + */ + disabled?: boolean; + /** + * When an `href` prop is passed, ButtonIcon will automatically change the root element to be an `a` (anchor) tag + */ + href?: string; + /** + * The name of the icon to display. Should be one of IconName + */ + iconName: IconName; + /** + * iconProps accepts all the props from Icon + */ + iconProps?: IconProps; + /** + * The size of the ButtonIcon. + * Possible values could be 'ButtonIconSize.Sm' 24px, 'ButtonIconSize.Lg' 32px, + */ + size?: ButtonIconSize; +} diff --git a/ui/components/component-library/button-icon/deprecated/__snapshots__/button-icon.test.js.snap b/ui/components/component-library/button-icon/deprecated/__snapshots__/button-icon.test.js.snap new file mode 100644 index 000000000..8a527a095 --- /dev/null +++ b/ui/components/component-library/button-icon/deprecated/__snapshots__/button-icon.test.js.snap @@ -0,0 +1,16 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`ButtonIcon should render button element correctly 1`] = ` +
+ +
+`; diff --git a/ui/components/component-library/button-icon/button-icon.constants.js b/ui/components/component-library/button-icon/deprecated/button-icon.constants.js similarity index 50% rename from ui/components/component-library/button-icon/button-icon.constants.js rename to ui/components/component-library/button-icon/deprecated/button-icon.constants.js index 138a42034..de770633f 100644 --- a/ui/components/component-library/button-icon/button-icon.constants.js +++ b/ui/components/component-library/button-icon/deprecated/button-icon.constants.js @@ -1,4 +1,4 @@ -import { Size } from '../../../helpers/constants/design-system'; +import { Size } from '../../../../helpers/constants/design-system'; export const BUTTON_ICON_SIZES = { SM: Size.SM, diff --git a/ui/components/component-library/button-icon/button-icon.js b/ui/components/component-library/button-icon/deprecated/button-icon.js similarity index 91% rename from ui/components/component-library/button-icon/button-icon.js rename to ui/components/component-library/button-icon/deprecated/button-icon.js index 2428aedf6..bf3ecc9e8 100644 --- a/ui/components/component-library/button-icon/button-icon.js +++ b/ui/components/component-library/button-icon/deprecated/button-icon.js @@ -10,10 +10,10 @@ import { IconColor, JustifyContent, Size, -} from '../../../helpers/constants/design-system'; +} from '../../../../helpers/constants/design-system'; -import Box from '../../ui/box'; -import { Icon, IconName } from '../icon'; +import Box from '../../../ui/box'; +import { Icon, ICON_NAMES } from '../../icon/deprecated'; import { BUTTON_ICON_SIZES } from './button-icon.constants'; @@ -86,7 +86,7 @@ ButtonIcon.propTypes = { /** * The name of the icon to display. Should be one of IconName */ - iconName: PropTypes.oneOf(Object.values(IconName)).isRequired, + iconName: PropTypes.oneOf(Object.values(ICON_NAMES)).isRequired, /** * iconProps accepts all the props from Icon */ diff --git a/ui/components/component-library/button-icon/button-icon.test.js b/ui/components/component-library/button-icon/deprecated/button-icon.test.js similarity index 85% rename from ui/components/component-library/button-icon/button-icon.test.js rename to ui/components/component-library/button-icon/deprecated/button-icon.test.js index 236c258a8..d5ee5b88b 100644 --- a/ui/components/component-library/button-icon/button-icon.test.js +++ b/ui/components/component-library/button-icon/deprecated/button-icon.test.js @@ -1,8 +1,8 @@ /* eslint-disable jest/require-top-level-describe */ import { render } from '@testing-library/react'; import React from 'react'; -import { IconColor } from '../../../helpers/constants/design-system'; -import { IconName } from '..'; +import { IconColor } from '../../../../helpers/constants/design-system'; +import { ICON_NAMES } from '../../icon/deprecated'; import { BUTTON_ICON_SIZES } from './button-icon.constants'; import { ButtonIcon } from './button-icon'; @@ -11,7 +11,7 @@ describe('ButtonIcon', () => { const { getByTestId, container } = render( , ); @@ -25,7 +25,7 @@ describe('ButtonIcon', () => { , ); @@ -39,7 +39,7 @@ describe('ButtonIcon', () => { , ); @@ -51,13 +51,13 @@ describe('ButtonIcon', () => { const { getByTestId } = render( <> { const { getByTestId } = render( <> { , ); @@ -115,7 +115,7 @@ describe('ButtonIcon', () => { , @@ -128,7 +128,7 @@ describe('ButtonIcon', () => { const { getByTestId } = render( , @@ -139,7 +139,7 @@ describe('ButtonIcon', () => { it('should render with aria-label', () => { const { getByLabelText } = render( - , + , ); expect(getByLabelText('add')).toBeDefined(); diff --git a/ui/components/component-library/button-icon/index.js b/ui/components/component-library/button-icon/deprecated/index.js similarity index 100% rename from ui/components/component-library/button-icon/index.js rename to ui/components/component-library/button-icon/deprecated/index.js diff --git a/ui/components/component-library/button-icon/index.ts b/ui/components/component-library/button-icon/index.ts new file mode 100644 index 000000000..466166377 --- /dev/null +++ b/ui/components/component-library/button-icon/index.ts @@ -0,0 +1,3 @@ +export { ButtonIcon } from './button-icon'; +export { ButtonIconSize } from './button-icon.types'; +export type { ButtonIconProps } from './button-icon.types'; diff --git a/ui/components/component-library/header-base/README.mdx b/ui/components/component-library/header-base/README.mdx index 9b7ea11c1..547e374b5 100644 --- a/ui/components/component-library/header-base/README.mdx +++ b/ui/components/component-library/header-base/README.mdx @@ -56,6 +56,7 @@ import { HeaderBase, Text, ButtonIcon, + ButtonIconSize, IconName, } from '../../component-library'; import { @@ -66,7 +67,7 @@ import { @@ -91,7 +92,7 @@ Use the `endAccessoryWrapperProps` prop to customize the wrapper element around ```jsx import { ButtonIcon, - BUTTON_ICON_SIZES, + ButtonIconSize, HeaderBase, IconName, Text, @@ -104,7 +105,7 @@ import { diff --git a/ui/components/component-library/header-base/header-base.stories.tsx b/ui/components/component-library/header-base/header-base.stories.tsx index 8ca56d49b..c02ade945 100644 --- a/ui/components/component-library/header-base/header-base.stories.tsx +++ b/ui/components/component-library/header-base/header-base.stories.tsx @@ -4,11 +4,12 @@ import Box from '../../ui/box'; import { IconName, Button, - ButtonIcon, - BUTTON_ICON_SIZES, BUTTON_SIZES, + ButtonIcon, + ButtonIconSize, Text, } from '..'; + import { AlignItems, BackgroundColor, @@ -42,14 +43,14 @@ DefaultStory.args = { ), startAccessory: ( ), endAccessory: ( @@ -74,7 +75,7 @@ export const StartAccessory = (args) => { marginBottom={4} startAccessory={ @@ -94,7 +95,7 @@ export const EndAccessory = (args) => { marginBottom={4} endAccessory={ @@ -129,7 +130,7 @@ export const UseCaseDemos = (args) => ( endAccessory={ @@ -152,7 +153,7 @@ export const UseCaseDemos = (args) => ( startAccessory={ @@ -175,7 +176,7 @@ export const UseCaseDemos = (args) => ( startAccessory={ @@ -183,7 +184,7 @@ export const UseCaseDemos = (args) => ( endAccessory={ @@ -215,7 +216,7 @@ export const UseCaseDemos = (args) => ( endAccessory={ @@ -242,7 +243,7 @@ export const UseCaseDemos = (args) => ( startAccessory={ @@ -281,7 +282,7 @@ export const UseCaseDemos = (args) => ( endAccessory={ diff --git a/ui/components/component-library/icon/icon.stories.tsx b/ui/components/component-library/icon/icon.stories.tsx index b3523d2df..0fdc61942 100644 --- a/ui/components/component-library/icon/icon.stories.tsx +++ b/ui/components/component-library/icon/icon.stories.tsx @@ -26,7 +26,7 @@ import { TextField, TextFieldSearch, TEXT_FIELD_SIZES, - BUTTON_ICON_SIZES, + ButtonIconSize, BUTTON_LINK_SIZES, } from '..'; @@ -131,7 +131,7 @@ export const DefaultStory: ComponentStory = (args) => { endAccessory={ diff --git a/ui/components/component-library/text-field/README.mdx b/ui/components/component-library/text-field/README.mdx index 81631aec5..62a4c0c5d 100644 --- a/ui/components/component-library/text-field/README.mdx +++ b/ui/components/component-library/text-field/README.mdx @@ -90,8 +90,8 @@ Use the `startAccessory` and `endAccessory` props to add components such as icon ```jsx import { Color, IconColor, SIZES, DISPLAY } from '../../../helpers/constants/design-system'; -import { Icon, IconName } from '../../component-library/deprecated' -import { ButtonIcon, TextField } from '../../component-library'; +import { ButtonIcon } from '../../component-library/button-icon/deprecated'; +import { TextField, Icon, IconName } from '../../component-library'; { /** * The content of the Box component. diff --git a/ui/components/ui/callout/callout.js b/ui/components/ui/callout/callout.js index 95585a728..8538d69cd 100644 --- a/ui/components/ui/callout/callout.js +++ b/ui/components/ui/callout/callout.js @@ -5,7 +5,7 @@ import InfoIconInverted from '../icon/info-icon-inverted.component'; import { SEVERITIES, Color } from '../../../helpers/constants/design-system'; import { MILLISECOND } from '../../../../shared/constants/time'; import Typography from '../typography'; -import { ButtonIcon } from '../../component-library'; +import { ButtonIcon } from '../../component-library/button-icon/deprecated'; import { ICON_NAMES, ICON_SIZES, diff --git a/ui/components/ui/contract-token-values/contract-token-values.js b/ui/components/ui/contract-token-values/contract-token-values.js index 8667a379c..b34b34b6a 100644 --- a/ui/components/ui/contract-token-values/contract-token-values.js +++ b/ui/components/ui/contract-token-values/contract-token-values.js @@ -16,7 +16,7 @@ import { Color, } from '../../../helpers/constants/design-system'; import { useCopyToClipboard } from '../../../hooks/useCopyToClipboard'; -import { ButtonIcon } from '../../component-library'; +import { ButtonIcon } from '../../component-library/button-icon/deprecated'; import { ICON_NAMES } from '../../component-library/icon/deprecated'; export default function ContractTokenValues({ diff --git a/ui/components/ui/editable-label/editable-label.js b/ui/components/ui/editable-label/editable-label.js index bab04c839..98385776d 100644 --- a/ui/components/ui/editable-label/editable-label.js +++ b/ui/components/ui/editable-label/editable-label.js @@ -3,7 +3,7 @@ import PropTypes from 'prop-types'; import React, { Component } from 'react'; import { Color } from '../../../helpers/constants/design-system'; import { getAccountNameErrorMessage } from '../../../helpers/utils/accounts'; -import { ButtonIcon } from '../../component-library'; +import { ButtonIcon } from '../../component-library/button-icon/deprecated'; import { ICON_NAMES } from '../../component-library/icon/deprecated'; export default class EditableLabel extends Component { diff --git a/ui/components/ui/nickname-popover/nickname-popover.component.js b/ui/components/ui/nickname-popover/nickname-popover.component.js index 2c48d086c..60785b18a 100644 --- a/ui/components/ui/nickname-popover/nickname-popover.component.js +++ b/ui/components/ui/nickname-popover/nickname-popover.component.js @@ -15,7 +15,7 @@ import { ICON_NAMES, ICON_SIZES, } from '../../component-library/icon/deprecated'; -import { ButtonIcon } from '../../component-library'; +import { ButtonIcon } from '../../component-library/button-icon/deprecated'; const NicknamePopover = ({ address, diff --git a/ui/components/ui/popover/popover.component.js b/ui/components/ui/popover/popover.component.js index 6d0a1ba8f..6e62e0bfd 100644 --- a/ui/components/ui/popover/popover.component.js +++ b/ui/components/ui/popover/popover.component.js @@ -23,7 +23,8 @@ import { ICON_NAMES, ICON_SIZES, } from '../../component-library/icon/deprecated'; -import { ButtonIcon, Text } from '../../component-library'; +import { ButtonIcon } from '../../component-library/button-icon/deprecated'; +import { Text } from '../../component-library'; const defaultHeaderProps = { padding: [6, 4, 4], diff --git a/ui/pages/add-nft/add-nft.js b/ui/pages/add-nft/add-nft.js index a4569da92..1463896b0 100644 --- a/ui/pages/add-nft/add-nft.js +++ b/ui/pages/add-nft/add-nft.js @@ -40,7 +40,7 @@ import { ICON_NAMES, ICON_SIZES, } from '../../components/component-library/icon/deprecated'; -import { ButtonIcon } from '../../components/component-library'; +import { ButtonIcon } from '../../components/component-library/button-icon/deprecated'; export default function AddNft() { const t = useI18nContext(); diff --git a/ui/pages/asset/components/asset-options.js b/ui/pages/asset/components/asset-options.js index 5498e279c..b5a46a683 100644 --- a/ui/pages/asset/components/asset-options.js +++ b/ui/pages/asset/components/asset-options.js @@ -8,7 +8,7 @@ import { Menu, MenuItem } from '../../../components/ui/menu'; import { getBlockExplorerLinkText } from '../../../selectors'; import { NETWORKS_ROUTE } from '../../../helpers/constants/routes'; import { ICON_NAMES } from '../../../components/component-library/icon/deprecated'; -import { ButtonIcon } from '../../../components/component-library'; +import { ButtonIcon } from '../../../components/component-library/button-icon/deprecated'; import { Color } from '../../../helpers/constants/design-system'; const AssetOptions = ({ diff --git a/ui/pages/confirm-approve/confirm-approve-content/confirm-approve-content.component.js b/ui/pages/confirm-approve/confirm-approve-content/confirm-approve-content.component.js index 1a91543f8..4d1f8dfbf 100644 --- a/ui/pages/confirm-approve/confirm-approve-content/confirm-approve-content.component.js +++ b/ui/pages/confirm-approve/confirm-approve-content/confirm-approve-content.component.js @@ -30,7 +30,8 @@ import { ICON_NAMES, Icon, } from '../../../components/component-library/icon/deprecated'; -import { ButtonIcon, Text } from '../../../components/component-library'; +import { ButtonIcon } from '../../../components/component-library/button-icon/deprecated'; +import { Text } from '../../../components/component-library'; export default class ConfirmApproveContent extends Component { static contextTypes = { diff --git a/ui/pages/home/home.component.js b/ui/pages/home/home.component.js index 86d2f63b7..c525bd161 100644 --- a/ui/pages/home/home.component.js +++ b/ui/pages/home/home.component.js @@ -35,7 +35,8 @@ import { ICON_NAMES, ICON_SIZES, } from '../../components/component-library/icon/deprecated'; -import { ButtonIcon, Text } from '../../components/component-library'; +import { ButtonIcon } from '../../components/component-library/button-icon/deprecated'; +import { Text } from '../../components/component-library'; import { ASSET_ROUTE, diff --git a/ui/pages/institutional/compliance-feature-page/compliance-feature-page.js b/ui/pages/institutional/compliance-feature-page/compliance-feature-page.js index ac2491593..acc8e63f4 100644 --- a/ui/pages/institutional/compliance-feature-page/compliance-feature-page.js +++ b/ui/pages/institutional/compliance-feature-page/compliance-feature-page.js @@ -13,7 +13,8 @@ import { Color, FLEX_DIRECTION, } from '../../../helpers/constants/design-system'; -import { ButtonIcon, Text } from '../../../components/component-library'; +import { ButtonIcon } from '../../../components/component-library/button-icon/deprecated'; +import { Text } from '../../../components/component-library'; import { ICON_NAMES, ICON_SIZES, diff --git a/ui/pages/notifications/notifications.js b/ui/pages/notifications/notifications.js index c52332c89..5e37225e1 100644 --- a/ui/pages/notifications/notifications.js +++ b/ui/pages/notifications/notifications.js @@ -20,7 +20,7 @@ import { ICON_SIZES, ICON_NAMES, } from '../../components/component-library/icon/deprecated'; -import { ButtonIcon } from '../../components/component-library'; +import { ButtonIcon } from '../../components/component-library/button-icon/deprecated'; import { Color } from '../../helpers/constants/design-system'; export function NotificationItem({ notification, snaps, onItemClick }) { diff --git a/ui/pages/send/send-content/add-recipient/domain-input.component.js b/ui/pages/send/send-content/add-recipient/domain-input.component.js index a074937a6..26f0898ae 100644 --- a/ui/pages/send/send-content/add-recipient/domain-input.component.js +++ b/ui/pages/send/send-content/add-recipient/domain-input.component.js @@ -13,7 +13,7 @@ import { ICON_NAMES, ICON_SIZES, } from '../../../../components/component-library/icon/deprecated'; -import { ButtonIcon } from '../../../../components/component-library'; +import { ButtonIcon } from '../../../../components/component-library/button-icon/deprecated'; import { IconColor } from '../../../../helpers/constants/design-system'; export default class DomainInput extends Component { diff --git a/ui/pages/settings/contact-list-tab/view-contact/view-contact.component.js b/ui/pages/settings/contact-list-tab/view-contact/view-contact.component.js index b4c6ed2f1..8a28efe00 100644 --- a/ui/pages/settings/contact-list-tab/view-contact/view-contact.component.js +++ b/ui/pages/settings/contact-list-tab/view-contact/view-contact.component.js @@ -9,7 +9,7 @@ import { ICON_SIZES, } from '../../../../components/component-library/icon/deprecated'; -import { ButtonIcon } from '../../../../components/component-library'; +import { ButtonIcon } from '../../../../components/component-library/button-icon/deprecated'; import Tooltip from '../../../../components/ui/tooltip'; import { useI18nContext } from '../../../../hooks/useI18nContext'; diff --git a/ui/pages/settings/settings.component.js b/ui/pages/settings/settings.component.js index d87ab0c4e..b9e007e86 100644 --- a/ui/pages/settings/settings.component.js +++ b/ui/pages/settings/settings.component.js @@ -27,7 +27,7 @@ import { import { getSettingsRoutes } from '../../helpers/utils/settings-search'; import AddNetwork from '../../components/app/add-network/add-network'; -import { ButtonIcon } from '../../components/component-library'; +import { ButtonIcon } from '../../components/component-library/button-icon/deprecated'; import { Icon, ICON_NAMES, diff --git a/ui/pages/token-details/token-details-page.js b/ui/pages/token-details/token-details-page.js index 8959547fe..91a088b3d 100644 --- a/ui/pages/token-details/token-details-page.js +++ b/ui/pages/token-details/token-details-page.js @@ -29,7 +29,7 @@ import { ICON_SIZES, ICON_NAMES, } from '../../components/component-library/icon/deprecated'; -import { ButtonIcon } from '../../components/component-library'; +import { ButtonIcon } from '../../components/component-library/button-icon/deprecated'; export default function TokenDetailsPage() { const dispatch = useDispatch(); From 6439551075e585c9ec27667e68df0cca9d4ef468 Mon Sep 17 00:00:00 2001 From: Elliot Winkler Date: Wed, 12 Apr 2023 13:53:34 -0600 Subject: [PATCH 15/36] Convert NetworkController unit tests to TypeScript (#18476) This helps us more easily compare the unit tests for NetworkController in this repo and the NetworkController in the `core` repo. Co-authored-by: Mark Stacey Co-authored-by: legobeat <109787230+legobeat@users.noreply.github.com> --- .eslintrc.js | 1 + ...ler.test.js => network-controller.test.ts} | 2020 +++++++++-------- .../controllers/network/network-controller.ts | 33 +- jest.config.js | 2 + package.json | 1 + yarn.lock | 17 + 6 files changed, 1145 insertions(+), 929 deletions(-) rename app/scripts/controllers/network/{network-controller.test.js => network-controller.test.ts} (83%) diff --git a/.eslintrc.js b/.eslintrc.js index 6851d9566..bcbfb44b7 100644 --- a/.eslintrc.js +++ b/.eslintrc.js @@ -266,6 +266,7 @@ module.exports = { '**/__snapshots__/*.snap', 'app/scripts/controllers/app-state.test.js', 'app/scripts/controllers/network/**/*.test.js', + 'app/scripts/controllers/network/**/*.test.ts', 'app/scripts/controllers/network/provider-api-tests/*.js', 'app/scripts/controllers/permissions/**/*.test.js', 'app/scripts/lib/**/*.test.js', diff --git a/app/scripts/controllers/network/network-controller.test.js b/app/scripts/controllers/network/network-controller.test.ts similarity index 83% rename from app/scripts/controllers/network/network-controller.test.js rename to app/scripts/controllers/network/network-controller.test.ts index fb86440c4..5e848830a 100644 --- a/app/scripts/controllers/network/network-controller.test.js +++ b/app/scripts/controllers/network/network-controller.test.ts @@ -1,56 +1,181 @@ import { inspect, isDeepStrictEqual, promisify } from 'util'; -import { isMatch } from 'lodash'; +import assert from 'assert'; +import { get, isMatch, omit } from 'lodash'; import { v4 } from 'uuid'; -import nock from 'nock'; -import sinon from 'sinon'; +import nock, { Scope as NockScope } from 'nock'; +import sinon, { SinonFakeTimers } from 'sinon'; +import { isPlainObject } from '@metamask/utils'; import { ControllerMessenger } from '@metamask/base-controller'; -import { BUILT_IN_NETWORKS } from '../../../../shared/constants/network'; +import { + BuiltInInfuraNetwork, + BUILT_IN_NETWORKS, + NETWORK_TYPES, +} from '../../../../shared/constants/network'; import { MetaMetricsNetworkEventSource } from '../../../../shared/constants/metametrics'; -import { NetworkController } from './network-controller'; +import { + NetworkController, + NetworkControllerEvent, + NetworkControllerEventType, + NetworkControllerOptions, + NetworkControllerState, + ProviderConfiguration, + ProviderType, +} from './network-controller'; jest.mock('uuid', () => { - const actual = jest.requireActual('uuid'); - return { - ...actual, + __esModule: true, + ...jest.requireActual('uuid'), v4: jest.fn(), }; }); -// Store this up front so it doesn't get lost when it is stubbed -const originalSetTimeout = global.setTimeout; +const uuidV4Mock = jest.mocked(v4); /** - * @typedef {import('nock').Scope} NockScope - * - * A object returned by the `nock` function which holds all of the request mocks - * for a network. + * A block header object that `eth_getBlockByNumber` can be mocked to return. + * Note that this type does not specify all of the properties present within the + * block header; within these tests, we are only interested in `number` and + * `baseFeePerGas`. */ +type Block = { + number: string; + baseFeePerGas?: string; +}; /** - * @typedef {{request: MockJsonResponseBody, response: { httpStatus?: number } & MockJsonResponseBody, error?: unknown, delay?: number; times?: number, beforeCompleting: () => void | Promise}} RpcMock - * - * Arguments to `mockRpcCall` which allow for specifying a canned response for a - * particular RPC request. - */ - -/** - * @typedef {{id?: number; jsonrpc?: string, method: string, params?: unknown[]}} MockJsonRpcRequestBody - * * A partial form of a prototypical JSON-RPC request body. */ +type MockJsonRpcRequestBody = { + id?: number; + jsonrpc?: string; + method: string; + params?: unknown[]; +}; /** - * @typedef {{id?: number; jsonrpc?: string; result?: string; error?: string}} MockJsonResponseBody - * - * A partial form of a prototypical JSON-RPC response body. + * A composite form of a prototypical JSON-RPC response body. */ +type MockJsonRpcResponseBody = { + id?: number | string; + jsonrpc?: '2.0'; + result?: unknown; + error?: string | null; +}; + +/** + * Arguments to `mockRpcCall` which specify the behavior of a mocked RPC + * request. + */ +type RpcCallMockSpec = { + request: MockJsonRpcRequestBody; + response: MockJsonRpcResponseBody & { httpStatus?: number }; + error?: unknown; + delay?: number; + times?: number; + beforeCompleting?: () => void | Promise; +}; + +/** + * A partial form of `RpcCallMockSpec`, which is preferred in + * `mockEssentialRpcCalls` for brevity. + */ +type PartialRpcCallMockSpec = { + request?: Partial; + response?: Partial; + error?: unknown; + delay?: number; + times?: number; + beforeCompleting?: () => void | Promise; +}; + +/** + * An RPC method that `mockEssentialRpcCalls` recognizes. + */ +enum KnownMockableRpcMethod { + EthBlockNumber = 'eth_blockNumber', + EthGetBlockByNumber = 'eth_getBlockByNumber', + NetVersion = 'net_version', +} + +/** + * The callback that `withController` takes. + */ +type WithControllerCallback = (args: { + controller: NetworkController; + network: NetworkCommunications; +}) => Promise | ReturnValue; + +/** + * A variant of the options that the NetworkController constructor takes, where + * the provider state has been preconfigured with an Infura network. This is + * extracted so that we can give `withController` a better signature. + */ +type NetworkControllerOptionsWithInfuraProviderConfig = + Partial & { + state: Partial & { + provider: ProviderConfiguration & { + type: Exclude; + }; + }; + }; + +/** + * A variant of the options that `withController` takes, where the provider + * state has been preconfigured with an Infura network. This is + * extracted so that we know which code path to take in `withController` + * depending on the given options. + */ +type WithControllerArgsWithConfiguredInfuraProvider = [ + options: NetworkControllerOptionsWithInfuraProviderConfig, + callback: WithControllerCallback, +]; + +/** + * The arguments that `withController` takes. + */ +type WithControllerArgs = + | WithControllerArgsWithConfiguredInfuraProvider + | [ + options: Partial, + callback: WithControllerCallback< + CustomNetworkCommunications, + ReturnValue + >, + ] + | [ + callback: WithControllerCallback< + CustomNetworkCommunications, + ReturnValue + >, + ]; + +/** + * The options that the InfuraNetworkCommunications constructor takes. + */ +type InfuraNetworkCommunicationsOptions = { + infuraNetwork: BuiltInInfuraNetwork; + infuraProjectId?: string; +}; + +/** + * The options that the CustomNetworkCommunications constructor takes. + */ +type CustomNetworkCommunicationsOptions = { + customRpcUrl: string; +}; + +/** + * As we use fake timers in these tests, we need a reference to the global + * `setTimeout` function so that we can still use it in test helpers. + */ +const originalSetTimeout = global.setTimeout; /** * A dummy block that matches the pre-EIP-1559 format (i.e. it doesn't have the * `baseFeePerGas` property). */ -const PRE_1559_BLOCK = { +const PRE_1559_BLOCK: Block = { number: '0x42', }; @@ -58,7 +183,7 @@ const PRE_1559_BLOCK = { * A dummy block that matches the pre-EIP-1559 format (i.e. it has the * `baseFeePerGas` property). */ -const POST_1559_BLOCK = { +const POST_1559_BLOCK: Block = { ...PRE_1559_BLOCK, baseFeePerGas: '0x63c498a46', }; @@ -67,7 +192,7 @@ const POST_1559_BLOCK = { * An alias for `POST_1559_BLOCK`, for tests that don't care about which kind of * block they're looking for. */ -const BLOCK = POST_1559_BLOCK; +const BLOCK: Block = POST_1559_BLOCK; /** * A dummy value for the `projectId` option that `createInfuraClient` needs. @@ -76,30 +201,25 @@ const BLOCK = POST_1559_BLOCK; */ const DEFAULT_INFURA_PROJECT_ID = 'fake-infura-project-id'; -/** - * The set of properties allowed in a valid JSON-RPC response object. - */ -const JSONRPC_RESPONSE_BODY_PROPERTIES = ['id', 'jsonrpc', 'result', 'error']; - /** * The set of networks that, when specified, create an Infura provider as * opposed to a "standard" provider (one suited for a custom RPC endpoint). */ const INFURA_NETWORKS = [ { - networkType: 'mainnet', + networkType: NETWORK_TYPES.MAINNET, chainId: '0x1', networkId: '1', ticker: 'ETH', }, { - networkType: 'goerli', + networkType: NETWORK_TYPES.GOERLI, chainId: '0x5', networkId: '5', ticker: 'GoerliETH', }, { - networkType: 'sepolia', + networkType: NETWORK_TYPES.SEPOLIA, chainId: '0xaa36a7', networkId: '11155111', ticker: 'SepoliaETH', @@ -144,102 +264,83 @@ const UNSUCCESSFUL_JSON_RPC_RESPONSE = { }; /** - * Handles mocking provider requests for a particular network. + * Handles mocking requests made by NetworkController for a particular network. */ -class NetworkCommunications { - #networkClientOptions; +abstract class NetworkCommunications { + /** + * Holds the options used to construct the instance. Employed by `with`. + */ + protected options: Options; /** - * Builds an object for mocking provider requests. + * The path used for all requests. Customized per network type. + */ + #requestPath: string; + + /** + * The Nock scope object that holds the mocked requests. + */ + nockScope: NockScope; + + /** + * Constructs a NetworkCommunications. Don't use this directly; instead + * instantiate either {@link InfuraNetworkCommunications} or {@link + * CustomNetworkCommunications}. * - * @param {object} args - The arguments. - * @param {"infura" | "custom"} args.networkClientType - Specifies the - * expected middleware stack that will represent the provider: "infura" for an - * Infura network; "custom" for a custom RPC endpoint. - * @param {object} args.networkClientOptions - Details about the network - * client used to determine the base URL or URL path to mock. - * @param {string} [args.networkClientOptions.infuraNetwork] - The name of the - * Infura network being tested, assuming that `networkClientType` is "infura". - * @param {string} [args.networkClientOptions.infuraProjectId] - The project - * ID of the Infura network being tested, assuming that `networkClientType` is - * "infura". - * @param {string} [args.networkClientOptions.customRpcUrl] - The URL of the - * custom RPC endpoint, assuming that `networkClientType` is "custom". - * @returns {NockScope} The nock scope. + * @param args - The arguments. + * @param args.options - Options to customize request mocks. + * @param args.requestBaseUrl - The base URL to use for all requests. + * @param args.requestPath - The path to use for all requests. */ constructor({ - networkClientType, - networkClientOptions: { - infuraNetwork, - infuraProjectId = DEFAULT_INFURA_PROJECT_ID, - customRpcUrl, - } = {}, + options, + requestBaseUrl, + requestPath, + }: { + options: Options; + requestBaseUrl: string; + requestPath: string; }) { - const networkClientOptions = { - infuraNetwork, - infuraProjectId, - customRpcUrl, - }; - this.networkClientType = networkClientType; - if (networkClientType !== 'infura' && networkClientType !== 'custom') { - throw new Error("networkClientType must be 'infura' or 'custom'"); - } - this.#networkClientOptions = networkClientOptions; - this.infuraProjectId = infuraProjectId; - const rpcUrl = - networkClientType === 'infura' - ? `https://${infuraNetwork}.infura.io` - : customRpcUrl; - this.nockScope = nock(rpcUrl); - } - - /** - * Constructs a new NetworkCommunications object using a different set of - * options, using the options from this instance as a base. - * - * @param args - The same arguments that NetworkCommunications takes. - */ - with(args) { - return new NetworkCommunications({ - networkClientType: this.networkClientType, - networkClientOptions: this.#networkClientOptions, - ...args, - }); + this.options = options; + this.nockScope = nock(requestBaseUrl); + this.#requestPath = requestPath; } /** * Mocks the RPC calls that NetworkController makes internally. * - * @param {object} args - The arguments. - * @param {{number: string, baseFeePerGas?: string} | null} [args.latestBlock] - The - * block object that will be used to mock `eth_blockNumber` and - * `eth_getBlockByNumber`. If null, then both `eth_blockNumber` and - * `eth_getBlockByNumber` will respond with null. - * @param {RpcMock | Partial[] | null} [args.eth_blockNumber] - - * Options for mocking the `eth_blockNumber` RPC method (see `mockRpcCall` for - * valid properties). By default, the number from the `latestBlock` will be - * used as the result. Use `null` to prevent this method from being mocked. - * @param {RpcMock | Partial[] | null} [args.eth_getBlockByNumber] - - * Options for mocking the `eth_getBlockByNumber` RPC method (see - * `mockRpcCall` for valid properties). By default, the `latestBlock` will be - * used as the result. Use `null` to prevent this method from being mocked. - * @param {RpcMock | Partial[] | null} [args.net_version] - Options - * for mocking the `net_version` RPC method (see `mockRpcCall` for valid - * properties). By default, "1" will be used as the result. Use `null` to + * @param args - The arguments. + * @param args.latestBlock - The block object that will be used to mock + * `eth_blockNumber` and `eth_getBlockByNumber`. If null, then both + * `eth_blockNumber` and `eth_getBlockByNumber` will respond with null. + * @param args.eth_blockNumber - Options for mocking the `eth_blockNumber` RPC + * method (see `mockRpcCall` for valid properties). By default, the number + * from the `latestBlock` will be used as the result. Use `null` to prevent + * this method from being mocked. + * @param args.eth_getBlockByNumber - Options for mocking the + * `eth_getBlockByNumber` RPC method (see `mockRpcCall` for valid properties). + * By default, the `latestBlock` will be used as the result. Use `null` to * prevent this method from being mocked. + * @param args.net_version - Options for mocking the `net_version` RPC method + * (see `mockRpcCall` for valid properties). By default, "1" will be used as + * the result. Use `null` to prevent this method from being mocked. */ mockEssentialRpcCalls({ latestBlock = BLOCK, eth_blockNumber: ethBlockNumberMocks = [], eth_getBlockByNumber: ethGetBlockByNumberMocks = [], net_version: netVersionMocks = [], + }: { + latestBlock?: Block | null; + eth_blockNumber?: PartialRpcCallMockSpec | PartialRpcCallMockSpec[]; + eth_getBlockByNumber?: PartialRpcCallMockSpec | PartialRpcCallMockSpec[]; + net_version?: PartialRpcCallMockSpec | PartialRpcCallMockSpec[]; } = {}) { const latestBlockNumber = latestBlock === null ? null : latestBlock.number; - if (latestBlock && latestBlock.number === undefined) { - throw new Error('The latest block must have a `number`.'); - } - - const defaultMocksByRpcMethod = { + const defaultMocksByRpcMethod: Record< + KnownMockableRpcMethod, + RpcCallMockSpec + > = { eth_getBlockByNumber: { request: { method: 'eth_getBlockByNumber', @@ -274,7 +375,7 @@ class NetworkCommunications { // block tracker won't be cached inside of the block tracker, so the // block tracker makes another request when it is asked for the latest // block. - times: latestBlock === null ? 2 : 1, + times: latestBlockNumber === null ? 2 : 1, }, }; const providedMocksByRpcMethod = { @@ -283,22 +384,28 @@ class NetworkCommunications { eth_blockNumber: ethBlockNumberMocks, }; - const allMocks = []; - - Object.keys(defaultMocksByRpcMethod).forEach((rpcMethod) => { + const allMocks: RpcCallMockSpec[] = []; + for (const rpcMethod of knownOwnKeysOf(defaultMocksByRpcMethod)) { const defaultMock = defaultMocksByRpcMethod[rpcMethod]; const providedMockOrMocks = providedMocksByRpcMethod[rpcMethod]; const providedMocks = Array.isArray(providedMockOrMocks) ? providedMockOrMocks : [providedMockOrMocks]; if (providedMocks.length > 0) { - providedMocks.forEach((providedMock) => { - allMocks.push({ ...defaultMock, ...providedMock }); - }); + for (const providedMock of providedMocks) { + // Using the spread operator seems to confuse TypeScript because + // it doesn't know that `request` and `response` will be non-optional + // in the end, even though it is non-optional only in RpcCallMockSpec + // and not PartialRpcCallMockSpec. However, `Object.assign` assigns + // the correct type. + /* eslint-disable-next-line prefer-object-spread */ + const completeMock = Object.assign({}, defaultMock, providedMock); + allMocks.push(completeMock); + } } else { allMocks.push(defaultMock); } - }); + } allMocks.forEach((mock) => { this.mockRpcCall(mock); @@ -306,52 +413,53 @@ class NetworkCommunications { } /** - * Mocks a JSON-RPC request sent to the provider with the given response. + * Uses Nock to mock a JSON-RPC request with the given response. * - * @param {RpcMock} args - The arguments. - * @param {MockJsonRpcRequestBody} args.request - The request data. Must + * @param args - The arguments. + * @param args.request - The request data. Must * include a `method`. Note that EthQuery's `sendAsync` method implicitly uses * an empty array for `params` if it is not provided in the original request, * so make sure to include this. - * @param {MockJsonResponseBody & { httpStatus?: number }} [args.response] - Information - * concerning the response that the request should have. Takes one of two - * forms. The simplest form is an object that represents the response body; - * the second form allows you to specify the HTTP status, as well as a - * potentially async function to generate the response body. - * @param {unknown} [args.error] - An error to throw while - * making the request. Takes precedence over `response`. - * @param {number} [args.delay] - The amount of time that should - * pass before the request resolves with the response. - * @param {number} [args.times] - The number of times that the - * request is expected to be made. - * @param {() => void | Promise} [args.beforeCompleting] - Sometimes it is useful to do - * something after the request is kicked off but before it ends (or, in terms - * of a `fetch` promise, when the promise is initiated but before it is - * resolved). You can pass an (async) function for this option to do this. - * @returns {NockScope | null} The nock scope object that represents all of - * the mocks for the network, or null if `times` is 0. + * @param args.response - Information concerning the response that the request + * should have. Takes one of two forms. The simplest form is an object that + * represents the response body; the second form allows you to specify the + * HTTP status, as well as a potentially async function to generate the + * response body. + * @param args.error - An error to throw while making the request. Takes + * precedence over `response`. + * @param args.delay - The amount of time that should pass before the request + * resolves with the response. + * @param args.times - The number of times that the request is expected to be + * made. + * @param args.beforeCompleting - Sometimes it is useful to do something after + * the request is kicked off but before it ends (or, in terms of a `fetch` + * promise, when the promise is initiated but before it is resolved). You can + * pass an (async) function for this option to do this. + * @returns The nock scope object that represents all of the mocks for the + * network, or null if `times` is 0. */ - mockRpcCall({ request, response, error, delay, times, beforeCompleting }) { + mockRpcCall({ + request, + response, + error, + delay, + times, + beforeCompleting, + }: RpcCallMockSpec): nock.Scope | null { if (times === 0) { return null; } - const url = - this.networkClientType === 'infura' ? `/v3/${this.infuraProjectId}` : '/'; - const httpStatus = response?.httpStatus ?? 200; - this.#validateMockResponseBody(response); - const partialResponseBody = { jsonrpc: '2.0' }; - JSONRPC_RESPONSE_BODY_PROPERTIES.forEach((prop) => { - if (response[prop] !== undefined) { - partialResponseBody[prop] = response[prop]; - } - }); + const partialResponseBody = omit(response, 'httpStatus'); - let nockInterceptor = this.nockScope.post(url, (actualBody) => { - const expectedPartialBody = { jsonrpc: '2.0', ...request }; - return isMatch(actualBody, expectedPartialBody); - }); + let nockInterceptor = this.nockScope.post( + this.#requestPath, + (actualBody) => { + const expectedPartialBody = { jsonrpc: '2.0', ...request }; + return isMatch(actualBody, expectedPartialBody); + }, + ); if (delay !== undefined) { nockInterceptor = nockInterceptor.delay(delay); @@ -361,7 +469,10 @@ class NetworkCommunications { nockInterceptor = nockInterceptor.times(times); } - if (error !== undefined) { + if ( + error !== undefined && + (typeof error === 'string' || isPlainObject(error)) + ) { return nockInterceptor.replyWithError(error); } if (response !== undefined) { @@ -371,8 +482,11 @@ class NetworkCommunications { } const completeResponseBody = { + id: + isPlainObject(requestBody) && 'id' in requestBody + ? requestBody.id + : undefined, jsonrpc: '2.0', - ...(requestBody.id === undefined ? {} : { id: requestBody.id }), ...partialResponseBody, }; @@ -384,23 +498,95 @@ class NetworkCommunications { ); } - #validateMockResponseBody(mockResponseBody) { - const invalidProperties = Object.keys(mockResponseBody).filter( - (key) => - key !== 'httpStatus' && !JSONRPC_RESPONSE_BODY_PROPERTIES.includes(key), - ); - if (invalidProperties.length > 0) { - throw new Error( - `Mock response object ${inspect( - mockResponseBody, - )} has invalid properties: ${inspect(invalidProperties)}.`, - ); - } + /** + * The number of times to mock `eth_blockNumber` by default. Customized + * for Infura. + */ + protected getDefaultNumTimesToMockEthBlockNumber(): number { + return 0; + } +} + +/** + * Handles mocking requests made by NetworkController for an Infura network. + */ +class InfuraNetworkCommunications extends NetworkCommunications { + /** + * Constructs an InfuraNetworkCommunications. + * + * @param args - The arguments. + * @param args.infuraProjectId - TODO. + * @param args.infuraNetwork - TODO. + */ + constructor({ + infuraProjectId = DEFAULT_INFURA_PROJECT_ID, + infuraNetwork, + }: InfuraNetworkCommunicationsOptions) { + super({ + options: { infuraProjectId, infuraNetwork }, + requestBaseUrl: `https://${infuraNetwork}.infura.io`, + requestPath: `/v3/${infuraProjectId}`, + }); + } + + /** + * Constructs a new InfuraNetworkCommunications object using a different set + * of options, using the options from this instance as a base. + * + * @param overrides - Options with which you want to extend the new + * InfuraNetworkCommunications. + */ + with( + overrides: Partial = {}, + ): InfuraNetworkCommunications { + return new InfuraNetworkCommunications({ + ...this.options, + ...overrides, + }); + } + + protected getDefaultNumTimesToMockEthBlockNumber(): number { + return 1; + } +} + +/** + * Handles mocking requests made by NetworkController for a non-Infura network. + */ +class CustomNetworkCommunications extends NetworkCommunications { + /** + * Constructs a CustomNetworkCommunications. + * + * @param args - The arguments. + * @param args.customRpcUrl - The URL that points to the RPC endpoint. + */ + constructor({ customRpcUrl }: CustomNetworkCommunicationsOptions) { + super({ + options: { customRpcUrl }, + requestBaseUrl: customRpcUrl, + requestPath: '/', + }); + } + + /** + * Constructs a new CustomNetworkCommunications object using a different set + * of options, using the options from this instance as a base. + * + * @param overrides - Options with which you want to extend the new + * CustomNetworkCommunications. + */ + with( + overrides: Partial = {}, + ): CustomNetworkCommunications { + return new CustomNetworkCommunications({ + ...this.options, + ...overrides, + }); } } describe('NetworkController', () => { - let clock; + let clock: SinonFakeTimers; beforeEach(() => { // Disable all requests, even those to localhost @@ -421,14 +607,15 @@ describe('NetworkController', () => { }); describe('constructor', () => { - const invalidInfuraIds = [undefined, null, {}, 1]; - invalidInfuraIds.forEach((invalidId) => { + const invalidInfuraProjectIds = [undefined, null, {}, 1]; + invalidInfuraProjectIds.forEach((invalidProjectId) => { it(`throws if an invalid Infura ID of "${inspect( - invalidId, + invalidProjectId, )}" is provided`, () => { - expect(() => new NetworkController({ infuraId: invalidId })).toThrow( - 'Invalid Infura project ID', - ); + expect( + // @ts-expect-error We are intentionally passing bad input. + () => new NetworkController({ infuraProjectId: invalidProjectId }), + ).toThrow('Invalid Infura project ID'); }); }); @@ -439,7 +626,7 @@ describe('NetworkController', () => { provider: { type: 'rpc', rpcUrl: 'http://example-custom-rpc.metamask.io', - chainId: '0x9999', + chainId: '0x9999' as const, nickname: 'Test initial state', }, networkDetails: { @@ -528,6 +715,7 @@ describe('NetworkController', () => { }); await controller.initializeProvider(); const { blockTracker } = controller.getProviderAndBlockTracker(); + assert(blockTracker, 'Block tracker is somehow unset'); // The block tracker starts running after a listener is attached blockTracker.addListener('latest', () => { // do nothing @@ -547,6 +735,7 @@ describe('NetworkController', () => { await withController( { state: { + /* @ts-expect-error We're intentionally passing bad input. */ provider: invalidProviderConfig, }, }, @@ -580,10 +769,13 @@ describe('NetworkController', () => { await controller.initializeProvider(); const { provider } = controller.getProviderAndBlockTracker(); + assert(provider, 'Provider is somehow unset'); const promisifiedSendAsync = promisify(provider.sendAsync).bind( provider, ); const { result: chainIdResult } = await promisifiedSendAsync({ + id: '1', + jsonrpc: '2.0', method: 'eth_chainId', }); expect(chainIdResult).toBe(chainId); @@ -592,11 +784,12 @@ describe('NetworkController', () => { }); it('emits infuraIsBlocked or infuraIsUnblocked, depending on whether Infura is blocking requests', async () => { - const messenger = buildMessenger(); + const { unrestrictedMessenger, restrictedMessenger } = + buildMessengerGroup(); await withController( { - messenger, + messenger: restrictedMessenger, state: { provider: { type: networkType, @@ -610,8 +803,8 @@ describe('NetworkController', () => { network.mockEssentialRpcCalls(); const infuraIsUnblocked = await waitForPublishedEvents({ - messenger, - eventType: 'NetworkController:infuraIsUnblocked', + messenger: unrestrictedMessenger, + eventType: NetworkControllerEventType.InfuraIsUnblocked, operation: async () => { await controller.initializeProvider(); }, @@ -692,7 +885,6 @@ describe('NetworkController', () => { rpcUrl: 'https://mock-rpc-url', chainId: '0xtest', ticker: 'TEST', - id: 'testNetworkConfigurationId', }, networkConfigurations: { testNetworkConfigurationId: { @@ -719,17 +911,20 @@ describe('NetworkController', () => { await controller.initializeProvider(); const { provider } = controller.getProviderAndBlockTracker(); + assert(provider, 'Provider is somehow unset'); const promisifiedSendAsync = promisify(provider.sendAsync).bind( provider, ); const { result: testResult } = await promisifiedSendAsync({ - id: 99999, + id: '1', jsonrpc: '2.0', method: 'test', params: [], }); expect(testResult).toBe('test response'); const { result: chainIdResult } = await promisifiedSendAsync({ + id: '2', + jsonrpc: '2.0', method: 'eth_chainId', }); expect(chainIdResult).toBe('0xtest'); @@ -738,18 +933,18 @@ describe('NetworkController', () => { }); it('emits infuraIsUnblocked', async () => { - const messenger = buildMessenger(); + const { unrestrictedMessenger, restrictedMessenger } = + buildMessengerGroup(); await withController( { - messenger, + messenger: restrictedMessenger, state: { provider: { type: 'rpc', rpcUrl: 'https://mock-rpc-url', chainId: '0xtest', ticker: 'TEST', - id: 'testNetworkConfigurationId', }, networkConfigurations: { testNetworkConfigurationId: { @@ -765,8 +960,8 @@ describe('NetworkController', () => { network.mockEssentialRpcCalls(); const infuraIsUnblocked = await waitForPublishedEvents({ - messenger, - eventType: 'NetworkController:infuraIsUnblocked', + messenger: unrestrictedMessenger, + eventType: NetworkControllerEventType.InfuraIsUnblocked, operation: async () => { await controller.initializeProvider(); }, @@ -778,18 +973,18 @@ describe('NetworkController', () => { }); it('does not emit infuraIsBlocked', async () => { - const messenger = buildMessenger(); + const { unrestrictedMessenger, restrictedMessenger } = + buildMessengerGroup(); await withController( { - messenger, + messenger: restrictedMessenger, state: { provider: { type: 'rpc', rpcUrl: 'https://mock-rpc-url', chainId: '0xtest', ticker: 'TEST', - id: 'testNetworkConfigurationId', }, networkConfigurations: { testNetworkConfigurationId: { @@ -805,8 +1000,8 @@ describe('NetworkController', () => { network.mockEssentialRpcCalls(); const promiseForNoInfuraIsBlockedEvents = waitForPublishedEvents({ - messenger, - eventType: 'NetworkController:infuraIsBlocked', + messenger: unrestrictedMessenger, + eventType: NetworkControllerEventType.InfuraIsBlocked, count: 0, operation: async () => { await controller.initializeProvider(); @@ -827,7 +1022,6 @@ describe('NetworkController', () => { rpcUrl: 'https://mock-rpc-url', chainId: '0xtest', ticker: 'TEST', - id: 'testNetworkConfigurationId', }, networkConfigurations: { testNetworkConfigurationId: { @@ -863,7 +1057,6 @@ describe('NetworkController', () => { rpcUrl: 'https://mock-rpc-url', chainId: '0xtest', ticker: 'TEST', - id: 'testNetworkConfigurationId', }, networkConfigurations: { testNetworkConfigurationId: { @@ -937,20 +1130,20 @@ describe('NetworkController', () => { }, async ({ controller, network: network1 }) => { network1.mockEssentialRpcCalls(); - const network2 = new NetworkCommunications({ - networkClientType: 'infura', - networkClientOptions: { - infuraNetwork: networkType, - }, + const network2 = new InfuraNetworkCommunications({ + infuraNetwork: networkType, }); network2.mockEssentialRpcCalls(); await controller.initializeProvider(); const { provider } = controller.getProviderAndBlockTracker(); + assert(provider, 'Provider is somehow unset'); const promisifiedSendAsync1 = promisify(provider.sendAsync).bind( provider, ); const { result: oldChainIdResult } = await promisifiedSendAsync1({ + id: '1', + jsonrpc: '2.0', method: 'eth_chainId', }); expect(oldChainIdResult).toBe('0x1337'); @@ -960,6 +1153,8 @@ describe('NetworkController', () => { provider, ); const { result: newChainIdResult } = await promisifiedSendAsync2({ + id: '2', + jsonrpc: '2.0', method: 'eth_chainId', }); expect(newChainIdResult).toBe(chainId); @@ -976,11 +1171,15 @@ describe('NetworkController', () => { state: { provider: { type: 'goerli', + // NOTE: This doesn't need to match the logical chain ID + // of the network selected, it just needs to exist + chainId: '0x9999999', }, networkConfigurations: { testNetworkConfigurationId: { rpcUrl: 'https://mock-rpc-url', chainId: '0x1337', + ticker: 'ABC', id: 'testNetworkConfigurationId', }, }, @@ -988,20 +1187,20 @@ describe('NetworkController', () => { }, async ({ controller, network: network1 }) => { network1.mockEssentialRpcCalls(); - const network2 = new NetworkCommunications({ - networkClientType: 'custom', - networkClientOptions: { - customRpcUrl: 'https://mock-rpc-url', - }, + const network2 = new CustomNetworkCommunications({ + customRpcUrl: 'https://mock-rpc-url', }); network2.mockEssentialRpcCalls(); await controller.initializeProvider(); const { provider } = controller.getProviderAndBlockTracker(); + assert(provider, 'Provider is somehow unset'); const promisifiedSendAsync1 = promisify(provider.sendAsync).bind( provider, ); const { result: oldChainIdResult } = await promisifiedSendAsync1({ + id: '1', + jsonrpc: '2.0', method: 'eth_chainId', }); expect(oldChainIdResult).toBe('0x5'); @@ -1011,6 +1210,8 @@ describe('NetworkController', () => { provider, ); const { result: newChainIdResult } = await promisifiedSendAsync2({ + id: '2', + jsonrpc: '2.0', method: 'eth_chainId', }); expect(newChainIdResult).toBe('0x1337'); @@ -1164,9 +1365,9 @@ describe('NetworkController', () => { describe('if the provider has not been initialized', () => { it('does not update state in any way', async () => { const providerConfig = { - type: 'rpc', + type: NETWORK_TYPES.RPC, rpcUrl: 'http://example-custom-rpc.metamask.io', - chainId: '0x9999', + chainId: '0x9999' as const, nickname: 'Test initial state', }; const initialState = { @@ -1196,46 +1397,55 @@ describe('NetworkController', () => { }); it('does not emit infuraIsUnblocked', async () => { - const messenger = buildMessenger(); + const { unrestrictedMessenger, restrictedMessenger } = + buildMessengerGroup(); - await withController({ messenger }, async ({ controller, network }) => { - network.mockEssentialRpcCalls(); + await withController( + { messenger: restrictedMessenger }, + async ({ controller, network }) => { + network.mockEssentialRpcCalls(); - const promiseForNoInfuraIsUnblockedEvents = waitForPublishedEvents({ - messenger, - eventType: 'NetworkController:infuraIsUnblocked', - count: 0, - operation: async () => { - await controller.lookupNetwork(); - }, - }); + const promiseForNoInfuraIsUnblockedEvents = waitForPublishedEvents({ + messenger: unrestrictedMessenger, + eventType: NetworkControllerEventType.InfuraIsUnblocked, + count: 0, + operation: async () => { + await controller.lookupNetwork(); + }, + }); - expect(await promiseForNoInfuraIsUnblockedEvents).toBeTruthy(); - }); + expect(await promiseForNoInfuraIsUnblockedEvents).toBeTruthy(); + }, + ); }); it('does not emit infuraIsBlocked', async () => { - const messenger = buildMessenger(); + const { unrestrictedMessenger, restrictedMessenger } = + buildMessengerGroup(); - await withController({ messenger }, async ({ controller, network }) => { - network.mockEssentialRpcCalls(); + await withController( + { messenger: restrictedMessenger }, + async ({ controller, network }) => { + network.mockEssentialRpcCalls(); - const promiseForNoInfuraIsBlockedEvents = waitForPublishedEvents({ - messenger, - eventType: 'NetworkController:infuraIsBlocked', - count: 0, - operation: async () => { - await controller.lookupNetwork(); - }, - }); + const promiseForNoInfuraIsBlockedEvents = waitForPublishedEvents({ + messenger: unrestrictedMessenger, + eventType: NetworkControllerEventType.InfuraIsBlocked, + count: 0, + operation: async () => { + await controller.lookupNetwork(); + }, + }); - expect(await promiseForNoInfuraIsBlockedEvents).toBeTruthy(); - }); + expect(await promiseForNoInfuraIsBlockedEvents).toBeTruthy(); + }, + ); }); }); describe('if the provider has initialized, but the current network has no chainId', () => { it('does not update state in any way', async () => { + /* @ts-expect-error We are intentionally not including a chainId in the provider config. */ await withController( { state: { @@ -1265,11 +1475,13 @@ describe('NetworkController', () => { }); it('does not emit infuraIsUnblocked', async () => { - const messenger = buildMessenger(); + const { unrestrictedMessenger, restrictedMessenger } = + buildMessengerGroup(); + /* @ts-expect-error We are intentionally not including a chainId in the provider config. */ await withController( { - messenger, + messenger: restrictedMessenger, state: { provider: { type: 'rpc', @@ -1282,8 +1494,8 @@ describe('NetworkController', () => { await controller.initializeProvider(); const promiseForNoInfuraIsUnblockedEvents = waitForPublishedEvents({ - messenger, - eventType: 'NetworkController:infuraIsUnblocked', + messenger: unrestrictedMessenger, + eventType: NetworkControllerEventType.InfuraIsUnblocked, count: 0, operation: async () => { await controller.lookupNetwork(); @@ -1296,11 +1508,13 @@ describe('NetworkController', () => { }); it('does not emit infuraIsBlocked', async () => { - const messenger = buildMessenger(); + const { unrestrictedMessenger, restrictedMessenger } = + buildMessengerGroup(); + /* @ts-expect-error We are intentionally not including a chainId in the provider config. */ await withController( { - messenger, + messenger: restrictedMessenger, state: { provider: { type: 'rpc', @@ -1313,8 +1527,8 @@ describe('NetworkController', () => { await controller.initializeProvider(); const promiseForNoInfuraIsBlockedEvents = waitForPublishedEvents({ - messenger, - eventType: 'NetworkController:infuraIsBlocked', + messenger: unrestrictedMessenger, + eventType: NetworkControllerEventType.InfuraIsBlocked, count: 0, operation: async () => { await controller.lookupNetwork(); @@ -1499,11 +1713,12 @@ describe('NetworkController', () => { }); it('emits infuraIsUnblocked', async () => { - const messenger = buildMessenger(); + const { unrestrictedMessenger, restrictedMessenger } = + buildMessengerGroup(); await withController( { - messenger, + messenger: restrictedMessenger, state: { provider: { type: networkType, @@ -1515,11 +1730,9 @@ describe('NetworkController', () => { }, async ({ controller, network }) => { network.mockEssentialRpcCalls({ - eth_getBlockByNumber: { - // This results in a successful call to eth_getBlockByNumber - // implicitly - latestBlock: POST_1559_BLOCK, - }, + // This results in a successful call to eth_getBlockByNumber + // implicitly + latestBlock: POST_1559_BLOCK, }); await withoutCallingLookupNetwork({ controller, @@ -1529,8 +1742,8 @@ describe('NetworkController', () => { }); const infuraIsUnblocked = await waitForPublishedEvents({ - messenger, - eventType: 'NetworkController:infuraIsUnblocked', + messenger: unrestrictedMessenger, + eventType: NetworkControllerEventType.InfuraIsUnblocked, operation: async () => { await controller.lookupNetwork(); }, @@ -1703,11 +1916,12 @@ describe('NetworkController', () => { }); it('emits infuraIsBlocked', async () => { - const messenger = buildMessenger(); + const { unrestrictedMessenger, restrictedMessenger } = + buildMessengerGroup(); await withController( { - messenger, + messenger: restrictedMessenger, state: { provider: { type: networkType, @@ -1731,8 +1945,8 @@ describe('NetworkController', () => { }); const infuraIsBlocked = await waitForPublishedEvents({ - messenger, - eventType: 'NetworkController:infuraIsBlocked', + messenger: unrestrictedMessenger, + eventType: NetworkControllerEventType.InfuraIsBlocked, operation: async () => { await controller.lookupNetwork(); }, @@ -1744,11 +1958,12 @@ describe('NetworkController', () => { }); it('does not emit infuraIsUnblocked', async () => { - const messenger = buildMessenger(); + const { unrestrictedMessenger, restrictedMessenger } = + buildMessengerGroup(); await withController( { - messenger, + messenger: restrictedMessenger, state: { provider: { type: networkType, @@ -1773,8 +1988,8 @@ describe('NetworkController', () => { const promiseForNoInfuraIsUnblockedEvents = waitForPublishedEvents({ - messenger, - eventType: 'NetworkController:infuraIsUnblocked', + messenger: unrestrictedMessenger, + eventType: NetworkControllerEventType.InfuraIsUnblocked, count: 0, operation: async () => { await controller.lookupNetwork(); @@ -2077,11 +2292,12 @@ describe('NetworkController', () => { }); it('does not emit infuraIsBlocked', async () => { - const messenger = buildMessenger(); + const { unrestrictedMessenger, restrictedMessenger } = + buildMessengerGroup(); await withController( { - messenger, + messenger: restrictedMessenger, state: { provider: { type: networkType, @@ -2106,8 +2322,8 @@ describe('NetworkController', () => { const promiseForNoInfuraIsBlockedEvents = waitForPublishedEvents({ - messenger, - eventType: 'NetworkController:infuraIsBlocked', + messenger: unrestrictedMessenger, + eventType: NetworkControllerEventType.InfuraIsBlocked, count: 0, operation: async () => { await controller.lookupNetwork(); @@ -2120,11 +2336,12 @@ describe('NetworkController', () => { }); it('does not emit infuraIsUnblocked', async () => { - const messenger = buildMessenger(); + const { unrestrictedMessenger, restrictedMessenger } = + buildMessengerGroup(); await withController( { - messenger, + messenger: restrictedMessenger, state: { provider: { type: networkType, @@ -2149,8 +2366,8 @@ describe('NetworkController', () => { const promiseForNoInfuraIsUnblockedEvents = waitForPublishedEvents({ - messenger, - eventType: 'NetworkController:infuraIsUnblocked', + messenger: unrestrictedMessenger, + eventType: NetworkControllerEventType.InfuraIsUnblocked, count: 0, operation: async () => { await controller.lookupNetwork(); @@ -2177,9 +2394,9 @@ describe('NetworkController', () => { networkConfigurations: { testNetworkConfigurationId: { id: 'testNetworkConfigurationId', - type: 'rpc', rpcUrl: 'https://mock-rpc-url', chainId: '0x1337', + ticker: 'ABC', }, }, }, @@ -2240,11 +2457,8 @@ describe('NetworkController', () => { }, ], }); - const network2 = new NetworkCommunications({ - networkClientType: 'custom', - networkClientOptions: { - customRpcUrl: 'https://mock-rpc-url', - }, + const network2 = new CustomNetworkCommunications({ + customRpcUrl: 'https://mock-rpc-url', }); network2.mockEssentialRpcCalls({ net_version: { @@ -2294,9 +2508,9 @@ describe('NetworkController', () => { networkConfigurations: { testNetworkConfigurationId: { id: 'testNetworkConfigurationId', - type: 'rpc', rpcUrl: 'https://mock-rpc-url', chainId: '0x1337', + ticker: 'ABC', }, }, }, @@ -2315,18 +2529,15 @@ describe('NetworkController', () => { }, }); }, - net_version: { - response: { - result: '111', - }, + }, + net_version: { + response: { + result: '111', }, }, }); - const network2 = new NetworkCommunications({ - networkClientType: 'custom', - networkClientOptions: { - customRpcUrl: 'https://mock-rpc-url', - }, + const network2 = new CustomNetworkCommunications({ + customRpcUrl: 'https://mock-rpc-url', }); network2.mockEssentialRpcCalls({ net_version: { @@ -2368,9 +2579,9 @@ describe('NetworkController', () => { networkConfigurations: { testNetworkConfigurationId: { id: 'testNetworkConfigurationId', - type: 'rpc', rpcUrl: 'https://mock-rpc-url', chainId: '0x1337', + ticker: 'ABC', }, }, }, @@ -2392,11 +2603,8 @@ describe('NetworkController', () => { }, }, }); - const network2 = new NetworkCommunications({ - networkClientType: 'custom', - networkClientOptions: { - customRpcUrl: 'https://mock-rpc-url', - }, + const network2 = new CustomNetworkCommunications({ + customRpcUrl: 'https://mock-rpc-url', }); network2.mockEssentialRpcCalls({ latestBlock: PRE_1559_BLOCK, @@ -2438,11 +2646,12 @@ describe('NetworkController', () => { ); } - const messenger = buildMessenger(); + const { unrestrictedMessenger, restrictedMessenger } = + buildMessengerGroup(); await withController( { - messenger, + messenger: restrictedMessenger, state: { provider: { type: networkType, @@ -2457,8 +2666,8 @@ describe('NetworkController', () => { eth_getBlockByNumber: { beforeCompleting: async () => { await waitForPublishedEvents({ - messenger, - eventType: 'NetworkController:networkDidChange', + messenger: unrestrictedMessenger, + eventType: NetworkControllerEventType.NetworkDidChange, operation: async () => { await waitForStateChanges({ controller, @@ -2475,10 +2684,7 @@ describe('NetworkController', () => { }, }); const network2 = network1.with({ - networkClientType: 'infura', - networkClientOptions: { - infuraNetwork: anotherNetwork.networkType, - }, + infuraNetwork: anotherNetwork.networkType, }); network2.mockEssentialRpcCalls({ eth_getBlockByNumber: { @@ -2493,13 +2699,13 @@ describe('NetworkController', () => { }); const promiseForNoInfuraIsUnblockedEvents = waitForPublishedEvents({ - messenger, - eventType: 'NetworkController:infuraIsUnblocked', + messenger: unrestrictedMessenger, + eventType: NetworkControllerEventType.InfuraIsUnblocked, count: 0, }); const promiseForInfuraIsBlocked = waitForPublishedEvents({ - messenger, - eventType: 'NetworkController:infuraIsBlocked', + messenger: unrestrictedMessenger, + eventType: NetworkControllerEventType.InfuraIsBlocked, }); await controller.lookupNetwork(); @@ -2686,11 +2892,12 @@ describe('NetworkController', () => { }); it('emits infuraIsUnblocked', async () => { - const messenger = buildMessenger(); + const { unrestrictedMessenger, restrictedMessenger } = + buildMessengerGroup(); await withController( { - messenger, + messenger: restrictedMessenger, state: { provider: { type: 'rpc', @@ -2716,8 +2923,8 @@ describe('NetworkController', () => { }); const infuraIsUnblocked = await waitForPublishedEvents({ - messenger, - eventType: 'NetworkController:infuraIsUnblocked', + messenger: unrestrictedMessenger, + eventType: NetworkControllerEventType.InfuraIsUnblocked, operation: async () => { await controller.lookupNetwork(); }, @@ -2946,11 +3153,12 @@ describe('NetworkController', () => { }); it('does not emit infuraIsBlocked', async () => { - const messenger = buildMessenger(); + const { unrestrictedMessenger, restrictedMessenger } = + buildMessengerGroup(); await withController( { - messenger, + messenger: restrictedMessenger, state: { provider: { type: 'rpc', @@ -2976,8 +3184,8 @@ describe('NetworkController', () => { }); const promiseForNoInfuraIsBlockedEvents = waitForPublishedEvents({ - messenger, - eventType: 'NetworkController:infuraIsBlocked', + messenger: unrestrictedMessenger, + eventType: NetworkControllerEventType.InfuraIsBlocked, count: 0, operation: async () => { await controller.lookupNetwork(); @@ -2990,11 +3198,12 @@ describe('NetworkController', () => { }); it('emits infuraIsUnblocked', async () => { - const messenger = buildMessenger(); + const { unrestrictedMessenger, restrictedMessenger } = + buildMessengerGroup(); await withController( { - messenger, + messenger: restrictedMessenger, state: { provider: { type: 'rpc', @@ -3020,8 +3229,8 @@ describe('NetworkController', () => { }); const infuraIsUnblocked = await waitForPublishedEvents({ - messenger, - eventType: 'NetworkController:infuraIsUnblocked', + messenger: unrestrictedMessenger, + eventType: NetworkControllerEventType.InfuraIsUnblocked, operation: async () => { await controller.lookupNetwork(); }, @@ -3213,11 +3422,12 @@ describe('NetworkController', () => { }); it('does not emit infuraIsBlocked', async () => { - const messenger = buildMessenger(); + const { unrestrictedMessenger, restrictedMessenger } = + buildMessengerGroup(); await withController( { - messenger, + messenger: restrictedMessenger, state: { provider: { type: 'rpc', @@ -3240,8 +3450,8 @@ describe('NetworkController', () => { }); const promiseForNoInfuraIsBlockedEvents = waitForPublishedEvents({ - messenger, - eventType: 'NetworkController:infuraIsBlocked', + messenger: unrestrictedMessenger, + eventType: NetworkControllerEventType.InfuraIsBlocked, count: 0, operation: async () => { await controller.lookupNetwork(); @@ -3254,11 +3464,12 @@ describe('NetworkController', () => { }); it('emits infuraIsUnblocked', async () => { - const messenger = buildMessenger(); + const { unrestrictedMessenger, restrictedMessenger } = + buildMessengerGroup(); await withController( { - messenger, + messenger: restrictedMessenger, state: { provider: { type: 'rpc', @@ -3281,8 +3492,8 @@ describe('NetworkController', () => { }); const infuraIsUnblocked = await waitForPublishedEvents({ - messenger, - eventType: 'NetworkController:infuraIsUnblocked', + messenger: unrestrictedMessenger, + eventType: NetworkControllerEventType.InfuraIsUnblocked, operation: async () => { await controller.lookupNetwork(); }, @@ -3365,11 +3576,8 @@ describe('NetworkController', () => { }, ], }); - const network2 = new NetworkCommunications({ - networkClientType: 'infura', - networkClientOptions: { - infuraNetwork: 'goerli', - }, + const network2 = new InfuraNetworkCommunications({ + infuraNetwork: 'goerli', }); network2.mockEssentialRpcCalls({ eth_getBlockByNumber: { @@ -3433,11 +3641,8 @@ describe('NetworkController', () => { }, }, }); - const network2 = new NetworkCommunications({ - networkClientType: 'infura', - networkClientOptions: { - infuraNetwork: 'goerli', - }, + const network2 = new InfuraNetworkCommunications({ + infuraNetwork: 'goerli', }); network2.mockEssentialRpcCalls(); @@ -3493,11 +3698,8 @@ describe('NetworkController', () => { }, }, }); - const network2 = new NetworkCommunications({ - networkClientType: 'infura', - networkClientOptions: { - infuraNetwork: 'goerli', - }, + const network2 = new InfuraNetworkCommunications({ + infuraNetwork: 'goerli', }); network2.mockEssentialRpcCalls({ latestBlock: PRE_1559_BLOCK, @@ -3530,11 +3732,12 @@ describe('NetworkController', () => { }); it('emits infuraIsBlocked, not infuraIsUnblocked, if the second network is blocked, even if the first one is not', async () => { - const messenger = buildMessenger(); + const { unrestrictedMessenger, restrictedMessenger } = + buildMessengerGroup(); await withController( { - messenger, + messenger: restrictedMessenger, state: { provider: { type: 'rpc', @@ -3558,11 +3761,8 @@ describe('NetworkController', () => { }, }, }); - const network2 = new NetworkCommunications({ - networkClientType: 'infura', - networkClientOptions: { - infuraNetwork: 'goerli', - }, + const network2 = new InfuraNetworkCommunications({ + infuraNetwork: 'goerli', }); network2.mockEssentialRpcCalls({ eth_getBlockByNumber: { @@ -3577,13 +3777,13 @@ describe('NetworkController', () => { }); const promiseForNoInfuraIsUnblockedEvents = waitForPublishedEvents({ - messenger, - eventType: 'NetworkController:infuraIsUnblocked', + messenger: unrestrictedMessenger, + eventType: NetworkControllerEventType.InfuraIsUnblocked, count: 0, }); const promiseForInfuraIsBlocked = waitForPublishedEvents({ - messenger, - eventType: 'NetworkController:infuraIsBlocked', + messenger: unrestrictedMessenger, + eventType: NetworkControllerEventType.InfuraIsBlocked, }); await controller.lookupNetwork(); @@ -3665,11 +3865,8 @@ describe('NetworkController', () => { }, ], }); - const network2 = new NetworkCommunications({ - networkClientType: 'infura', - networkClientOptions: { - infuraNetwork: 'goerli', - }, + const network2 = new InfuraNetworkCommunications({ + infuraNetwork: 'goerli', }); network2.mockEssentialRpcCalls({ eth_getBlockByNumber: { @@ -3735,11 +3932,8 @@ describe('NetworkController', () => { }, }, }); - const network2 = new NetworkCommunications({ - networkClientType: 'infura', - networkClientOptions: { - infuraNetwork: 'goerli', - }, + const network2 = new InfuraNetworkCommunications({ + infuraNetwork: 'goerli', }); network2.mockEssentialRpcCalls(); @@ -3794,11 +3988,8 @@ describe('NetworkController', () => { }, }, }); - const network2 = new NetworkCommunications({ - networkClientType: 'infura', - networkClientOptions: { - infuraNetwork: 'goerli', - }, + const network2 = new InfuraNetworkCommunications({ + infuraNetwork: 'goerli', }); network2.mockEssentialRpcCalls({ latestBlock: PRE_1559_BLOCK, @@ -3831,11 +4022,12 @@ describe('NetworkController', () => { }); it('emits infuraIsBlocked, not infuraIsUnblocked, if the second network is blocked, even if the first one is not', async () => { - const messenger = buildMessenger(); + const { unrestrictedMessenger, restrictedMessenger } = + buildMessengerGroup(); await withController( { - messenger, + messenger: restrictedMessenger, state: { provider: { type: 'rpc', @@ -3859,11 +4051,8 @@ describe('NetworkController', () => { }, }, }); - const network2 = new NetworkCommunications({ - networkClientType: 'infura', - networkClientOptions: { - infuraNetwork: 'goerli', - }, + const network2 = new InfuraNetworkCommunications({ + infuraNetwork: 'goerli', }); network2.mockEssentialRpcCalls({ eth_getBlockByNumber: { @@ -3878,13 +4067,13 @@ describe('NetworkController', () => { }); const promiseForNoInfuraIsUnblockedEvents = waitForPublishedEvents({ - messenger, - eventType: 'NetworkController:infuraIsUnblocked', + messenger: unrestrictedMessenger, + eventType: NetworkControllerEventType.InfuraIsUnblocked, count: 0, }); const promiseForInfuraIsBlocked = waitForPublishedEvents({ - messenger, - eventType: 'NetworkController:infuraIsBlocked', + messenger: unrestrictedMessenger, + eventType: NetworkControllerEventType.InfuraIsBlocked, }); await controller.lookupNetwork(); @@ -3936,23 +4125,20 @@ describe('NetworkController', () => { rpcUrl: 'https://mock-rpc-url-1', chainId: '0x111', ticker: 'TEST', - id: 'testNetworkConfigurationId1', }, networkConfigurations: { testNetworkConfigurationId2: { + id: 'testNetworkConfigurationId2', rpcUrl: 'https://mock-rpc-url-2', chainId: '0x222', - id: 'testNetworkConfigurationId2', + ticker: 'ABC', }, }, }, }, async ({ controller }) => { - const network = new NetworkCommunications({ - networkClientType: 'custom', - networkClientOptions: { - customRpcUrl: 'https://mock-rpc-url-2', - }, + const network = new CustomNetworkCommunications({ + customRpcUrl: 'https://mock-rpc-url-2', }); network.mockEssentialRpcCalls(); @@ -3965,7 +4151,6 @@ describe('NetworkController', () => { rpcUrl: 'https://mock-rpc-url-1', chainId: '0x111', ticker: 'TEST', - id: 'testNetworkConfigurationId1', }); }, ); @@ -3980,32 +4165,26 @@ describe('NetworkController', () => { rpcUrl: 'http://example-custom-rpc.metamask.io', chainId: '0x9999', ticker: 'RPC', - id: 'testNetworkConfigurationId2', }, networkConfigurations: { testNetworkConfigurationId1: { rpcUrl: 'https://mock-rpc-url', chainId: '0xtest', ticker: 'TEST', - type: 'rpc', id: 'testNetworkConfigurationId1', }, testNetworkConfigurationId2: { rpcUrl: 'http://example-custom-rpc.metamask.io', chainId: '0x9999', ticker: 'RPC', - type: 'rpc', id: 'testNetworkConfigurationId2', }, }, }, }, async ({ controller }) => { - const network = new NetworkCommunications({ - networkClientType: 'custom', - networkClientOptions: { - customRpcUrl: 'https://mock-rpc-url', - }, + const network = new CustomNetworkCommunications({ + customRpcUrl: 'https://mock-rpc-url', }); network.mockEssentialRpcCalls(); @@ -4023,18 +4202,18 @@ describe('NetworkController', () => { }); it('emits networkWillChange before making any changes to the network status', async () => { - const messenger = buildMessenger(); + const { unrestrictedMessenger, restrictedMessenger } = + buildMessengerGroup(); await withController( { - messenger, + messenger: restrictedMessenger, state: { provider: { type: 'rpc', rpcUrl: 'https://mock-rpc-url-1', chainId: '0x111', ticker: 'TEST2', - id: 'testNetworkConfigurationId1', }, networkConfigurations: { testNetworkConfigurationId1: { @@ -4059,9 +4238,7 @@ describe('NetworkController', () => { }, }); const network2 = network1.with({ - networkClientOptions: { - customRpcUrl: 'https://mock-rpc-url-2', - }, + customRpcUrl: 'https://mock-rpc-url-2', }); network2.mockEssentialRpcCalls({ net_version: UNSUCCESSFUL_JSON_RPC_RESPONSE, @@ -4077,8 +4254,8 @@ describe('NetworkController', () => { expect(initialNetworkStatus).toBe('available'); const networkWillChange = await waitForPublishedEvents({ - messenger, - eventType: 'NetworkController:networkWillChange', + messenger: unrestrictedMessenger, + eventType: NetworkControllerEventType.NetworkWillChange, operation: () => { controller.setActiveNetwork('testNetworkConfigurationId2'); }, @@ -4102,7 +4279,6 @@ describe('NetworkController', () => { rpcUrl: 'http://mock-rpc-url-2', chainId: '0xtest2', ticker: 'TEST2', - id: 'testNetworkConfigurationId1', }, networkConfigurations: { testNetworkConfigurationId1: { @@ -4127,10 +4303,7 @@ describe('NetworkController', () => { }, }); const network2 = network1.with({ - networkClientType: 'custom', - networkClientOptions: { - customRpcUrl: 'https://mock-rpc-url-1', - }, + customRpcUrl: 'https://mock-rpc-url-1', }); network2.mockEssentialRpcCalls({ net_version: { @@ -4165,7 +4338,6 @@ describe('NetworkController', () => { rpcUrl: 'https://mock-rpc-url-1', chainId: '0x111', ticker: 'TEST1', - id: 'testNetworkConfigurationId1', }, networkConfigurations: { testNetworkConfigurationId1: { @@ -4188,10 +4360,7 @@ describe('NetworkController', () => { latestBlock: POST_1559_BLOCK, }); const network2 = network1.with({ - networkClientType: 'custom', - networkClientOptions: { - customRpcUrl: 'https://mock-rpc-url-2', - }, + customRpcUrl: 'https://mock-rpc-url-2', }); network2.mockEssentialRpcCalls({ latestBlock: PRE_1559_BLOCK, @@ -4238,11 +4407,8 @@ describe('NetworkController', () => { }, }, async ({ controller }) => { - const network = new NetworkCommunications({ - networkClientType: 'custom', - networkClientOptions: { - customRpcUrl: 'https://mock-rpc-url', - }, + const network = new CustomNetworkCommunications({ + customRpcUrl: 'https://mock-rpc-url', }); network.mockEssentialRpcCalls(); network.mockRpcCall({ @@ -4258,17 +4424,20 @@ describe('NetworkController', () => { controller.setActiveNetwork('testNetworkConfigurationId'); const { provider } = controller.getProviderAndBlockTracker(); + assert(provider, 'Provider is somehow unset'); const promisifiedSendAsync = promisify(provider.sendAsync).bind( provider, ); const { result: testResult } = await promisifiedSendAsync({ - id: 99999, + id: '1', jsonrpc: '2.0', method: 'test', params: [], }); expect(testResult).toBe('test response'); const { result: chainIdResult } = await promisifiedSendAsync({ + id: '2', + jsonrpc: '2.0', method: 'eth_chainId', }); expect(chainIdResult).toBe('0xtest'); @@ -4293,10 +4462,7 @@ describe('NetworkController', () => { async ({ controller, network: network1 }) => { network1.mockEssentialRpcCalls(); const network2 = network1.with({ - networkClientType: 'custom', - networkClientOptions: { - customRpcUrl: 'https://mock-rpc-url', - }, + customRpcUrl: 'https://mock-rpc-url', }); network2.mockEssentialRpcCalls(); await controller.initializeProvider(); @@ -4313,11 +4479,12 @@ describe('NetworkController', () => { }); it('emits networkDidChange', async () => { - const messenger = buildMessenger(); + const { unrestrictedMessenger, restrictedMessenger } = + buildMessengerGroup(); await withController( { - messenger, + messenger: restrictedMessenger, state: { networkConfigurations: { testNetworkConfigurationId: { @@ -4330,17 +4497,14 @@ describe('NetworkController', () => { }, }, async ({ controller }) => { - const network = new NetworkCommunications({ - networkClientType: 'custom', - networkClientOptions: { - customRpcUrl: 'https://mock-rpc-url', - }, + const network = new CustomNetworkCommunications({ + customRpcUrl: 'https://mock-rpc-url', }); network.mockEssentialRpcCalls(); const networkDidChange = await waitForPublishedEvents({ - messenger, - eventType: 'NetworkController:networkDidChange', + messenger: unrestrictedMessenger, + eventType: NetworkControllerEventType.NetworkDidChange, operation: () => { controller.setActiveNetwork('testNetworkConfigurationId'); }, @@ -4352,11 +4516,12 @@ describe('NetworkController', () => { }); it('emits infuraIsUnblocked', async () => { - const messenger = buildMessenger(); + const { unrestrictedMessenger, restrictedMessenger } = + buildMessengerGroup(); await withController( { - messenger, + messenger: restrictedMessenger, state: { networkConfigurations: { testNetworkConfigurationId: { @@ -4369,17 +4534,14 @@ describe('NetworkController', () => { }, }, async ({ controller }) => { - const network = new NetworkCommunications({ - networkClientType: 'custom', - networkClientOptions: { - customRpcUrl: 'https://mock-rpc-url', - }, + const network = new CustomNetworkCommunications({ + customRpcUrl: 'https://mock-rpc-url', }); network.mockEssentialRpcCalls(); const infuraIsUnblocked = await waitForPublishedEvents({ - messenger, - eventType: 'NetworkController:infuraIsUnblocked', + messenger: unrestrictedMessenger, + eventType: NetworkControllerEventType.InfuraIsUnblocked, operation: () => { controller.setActiveNetwork('testNetworkConfigurationId'); }, @@ -4405,11 +4567,8 @@ describe('NetworkController', () => { }, }, async ({ controller }) => { - const network = new NetworkCommunications({ - networkClientType: 'custom', - networkClientOptions: { - customRpcUrl: 'https://mock-rpc-url', - }, + const network = new CustomNetworkCommunications({ + customRpcUrl: 'https://mock-rpc-url', }); network.mockEssentialRpcCalls({ net_version: { @@ -4448,11 +4607,8 @@ describe('NetworkController', () => { }, }, async ({ controller }) => { - const network = new NetworkCommunications({ - networkClientType: 'custom', - networkClientOptions: { - customRpcUrl: 'https://mock-rpc-url', - }, + const network = new CustomNetworkCommunications({ + customRpcUrl: 'https://mock-rpc-url', }); network.mockEssentialRpcCalls({ latestBlock: POST_1559_BLOCK, @@ -4487,6 +4643,7 @@ describe('NetworkController', () => { { state: { provider: { + type: 'rpc', rpcUrl: 'http://mock-rpc-url-2', chainId: '0xtest2', nickname: 'test-chain-2', @@ -4494,7 +4651,6 @@ describe('NetworkController', () => { rpcPrefs: { blockExplorerUrl: 'test-block-explorer-2.com', }, - id: 'testNetworkConfigurationId2', }, networkConfigurations: { testNetworkConfigurationId1: { @@ -4521,11 +4677,8 @@ describe('NetworkController', () => { }, }, async ({ controller }) => { - const network = new NetworkCommunications({ - networkClientType: 'infura', - networkClientOptions: { - infuraNetwork: networkType, - }, + const network = new InfuraNetworkCommunications({ + infuraNetwork: networkType, }); network.mockEssentialRpcCalls(); @@ -4534,6 +4687,7 @@ describe('NetworkController', () => { expect( controller.store.getState().previousProviderStore, ).toStrictEqual({ + type: 'rpc', rpcUrl: 'http://mock-rpc-url-2', chainId: '0xtest2', nickname: 'test-chain-2', @@ -4541,7 +4695,6 @@ describe('NetworkController', () => { rpcPrefs: { blockExplorerUrl: 'test-block-explorer-2.com', }, - id: 'testNetworkConfigurationId2', }); }, ); @@ -4552,6 +4705,7 @@ describe('NetworkController', () => { { state: { provider: { + type: 'rpc', rpcUrl: 'http://mock-rpc-url-2', chainId: '0xtest2', nickname: 'test-chain-2', @@ -4559,7 +4713,6 @@ describe('NetworkController', () => { rpcPrefs: { blockExplorerUrl: 'test-block-explorer-2.com', }, - id: 'testNetworkConfigurationId2', }, networkConfigurations: { testNetworkConfigurationId1: { @@ -4586,11 +4739,8 @@ describe('NetworkController', () => { }, }, async ({ controller }) => { - const network = new NetworkCommunications({ - networkClientType: 'infura', - networkClientOptions: { - infuraNetwork: networkType, - }, + const network = new InfuraNetworkCommunications({ + infuraNetwork: networkType, }); network.mockEssentialRpcCalls(); @@ -4612,27 +4762,28 @@ describe('NetworkController', () => { }); it('emits networkWillChange', async () => { - const messenger = buildMessenger(); + const { unrestrictedMessenger, restrictedMessenger } = + buildMessengerGroup(); - await withController({ messenger }, async ({ controller }) => { - const network = new NetworkCommunications({ - networkClientType: 'infura', - networkClientOptions: { + await withController( + { messenger: restrictedMessenger }, + async ({ controller }) => { + const network = new InfuraNetworkCommunications({ infuraNetwork: networkType, - }, - }); - network.mockEssentialRpcCalls(); + }); + network.mockEssentialRpcCalls(); - const networkWillChange = await waitForPublishedEvents({ - messenger, - eventType: 'NetworkController:networkWillChange', - operation: () => { - controller.setProviderType(networkType); - }, - }); + const networkWillChange = await waitForPublishedEvents({ + messenger: unrestrictedMessenger, + eventType: NetworkControllerEventType.NetworkWillChange, + operation: () => { + controller.setProviderType(networkType); + }, + }); - expect(networkWillChange).toBeTruthy(); - }); + expect(networkWillChange).toBeTruthy(); + }, + ); }); it('resets the network status to "unknown" before emitting networkDidChange', async () => { @@ -4640,11 +4791,10 @@ describe('NetworkController', () => { { state: { provider: { + type: 'rpc', rpcUrl: 'https://mock-rpc-url', chainId: '0xtest', ticker: 'TEST', - id: 'testNetworkConfigurationId', - type: 'rpc', }, networkConfigurations: { testNetworkConfigurationId: { @@ -4662,11 +4812,8 @@ describe('NetworkController', () => { response: SUCCESSFUL_NET_VERSION_RESPONSE, }, }); - const network2 = network1.with({ - networkClientType: 'infura', - networkClientOptions: { - infuraNetwork: networkType, - }, + const network2 = new InfuraNetworkCommunications({ + infuraNetwork: networkType, }); network2.mockEssentialRpcCalls(); @@ -4699,7 +4846,6 @@ describe('NetworkController', () => { rpcUrl: 'https://mock-rpc-url', chainId: '0xtest', ticker: 'TEST', - id: 'testNetworkConfigurationId', }, networkConfigurations: { testNetworkConfigurationId: { @@ -4715,11 +4861,8 @@ describe('NetworkController', () => { network1.mockEssentialRpcCalls({ latestBlock: POST_1559_BLOCK, }); - const network2 = network1.with({ - networkClientType: 'infura', - networkClientOptions: { - infuraNetwork: networkType, - }, + const network2 = new InfuraNetworkCommunications({ + infuraNetwork: networkType, }); network2.mockEssentialRpcCalls({ latestBlock: PRE_1559_BLOCK, @@ -4753,21 +4896,21 @@ describe('NetworkController', () => { it(`initializes a provider pointed to the "${networkType}" Infura network (chainId: ${chainId})`, async () => { await withController(async ({ controller }) => { - const network = new NetworkCommunications({ - networkClientType: 'infura', - networkClientOptions: { - infuraNetwork: networkType, - }, + const network = new InfuraNetworkCommunications({ + infuraNetwork: networkType, }); network.mockEssentialRpcCalls(); controller.setProviderType(networkType); const { provider } = controller.getProviderAndBlockTracker(); + assert(provider, 'Provider is somehow unset'); const promisifiedSendAsync = promisify(provider.sendAsync).bind( provider, ); const { result: chainIdResult } = await promisifiedSendAsync({ + id: '1', + jsonrpc: '2.0', method: 'eth_chainId', }); expect(chainIdResult).toBe(chainId); @@ -4777,11 +4920,8 @@ describe('NetworkController', () => { it('replaces the provider object underlying the provider proxy without creating a new instance of the proxy itself', async () => { await withController(async ({ controller, network: network1 }) => { network1.mockEssentialRpcCalls(); - const network2 = network1.with({ - networkClientType: 'infura', - networkClientOptions: { - infuraNetwork: networkType, - }, + const network2 = new InfuraNetworkCommunications({ + infuraNetwork: networkType, }); network2.mockEssentialRpcCalls(); await controller.initializeProvider(); @@ -4797,68 +4937,68 @@ describe('NetworkController', () => { }); it('emits networkDidChange', async () => { - const messenger = buildMessenger(); + const { unrestrictedMessenger, restrictedMessenger } = + buildMessengerGroup(); - await withController({ messenger }, async ({ controller }) => { - const network = new NetworkCommunications({ - networkClientType: 'infura', - networkClientOptions: { + await withController( + { messenger: restrictedMessenger }, + async ({ controller }) => { + const network = new InfuraNetworkCommunications({ infuraNetwork: networkType, - }, - }); - network.mockEssentialRpcCalls(); + }); + network.mockEssentialRpcCalls(); - const networkDidChange = await waitForPublishedEvents({ - messenger, - eventType: 'NetworkController:networkDidChange', - operation: () => { - controller.setProviderType(networkType); - }, - }); + const networkDidChange = await waitForPublishedEvents({ + messenger: unrestrictedMessenger, + eventType: NetworkControllerEventType.NetworkDidChange, + operation: () => { + controller.setProviderType(networkType); + }, + }); - expect(networkDidChange).toBeTruthy(); - }); + expect(networkDidChange).toBeTruthy(); + }, + ); }); it('emits infuraIsBlocked or infuraIsUnblocked, depending on whether Infura is blocking requests', async () => { - const messenger = buildMessenger(); + const { unrestrictedMessenger, restrictedMessenger } = + buildMessengerGroup(); - await withController({ messenger }, async ({ controller }) => { - const network = new NetworkCommunications({ - networkClientType: 'infura', - networkClientOptions: { + await withController( + { messenger: restrictedMessenger }, + async ({ controller }) => { + const network = new InfuraNetworkCommunications({ infuraNetwork: networkType, - }, - }); - network.mockEssentialRpcCalls({ - eth_getBlockByNumber: { - response: BLOCKED_INFURA_RESPONSE, - }, - }); - const promiseForNoInfuraIsUnblockedEvents = waitForPublishedEvents({ - messenger, - eventType: 'NetworkController:infuraIsUnblocked', - count: 0, - }); - const promiseForInfuraIsBlocked = waitForPublishedEvents({ - messenger, - eventType: 'NetworkController:infuraIsBlocked', - }); + }); + network.mockEssentialRpcCalls({ + eth_getBlockByNumber: { + response: BLOCKED_INFURA_RESPONSE, + }, + }); + const promiseForNoInfuraIsUnblockedEvents = + waitForPublishedEvents({ + messenger: unrestrictedMessenger, + eventType: NetworkControllerEventType.InfuraIsUnblocked, + count: 0, + }); + const promiseForInfuraIsBlocked = waitForPublishedEvents({ + messenger: unrestrictedMessenger, + eventType: NetworkControllerEventType.InfuraIsBlocked, + }); - controller.setProviderType(networkType); + controller.setProviderType(networkType); - expect(await promiseForNoInfuraIsUnblockedEvents).toBeTruthy(); - expect(await promiseForInfuraIsBlocked).toBeTruthy(); - }); + expect(await promiseForNoInfuraIsUnblockedEvents).toBeTruthy(); + expect(await promiseForInfuraIsBlocked).toBeTruthy(); + }, + ); }); it('determines the status of the network, storing it in state', async () => { await withController(async ({ controller }) => { - const network = new NetworkCommunications({ - networkClientType: 'infura', - networkClientOptions: { - infuraNetwork: networkType, - }, + const network = new InfuraNetworkCommunications({ + infuraNetwork: networkType, }); network.mockEssentialRpcCalls({ // This results in a successful call to eth_getBlockByNumber @@ -4889,11 +5029,8 @@ describe('NetworkController', () => { }, }, async ({ controller }) => { - const network = new NetworkCommunications({ - networkClientType: 'infura', - networkClientOptions: { - infuraNetwork: networkType, - }, + const network = new InfuraNetworkCommunications({ + infuraNetwork: networkType, }); network.mockEssentialRpcCalls({ latestBlock: POST_1559_BLOCK, @@ -4948,11 +5085,12 @@ describe('NetworkController', () => { for (const { networkType, chainId } of INFURA_NETWORKS) { describe(`when the type in the provider configuration is "${networkType}"`, () => { it('emits networkWillChange', async () => { - const messenger = buildMessenger(); + const { unrestrictedMessenger, restrictedMessenger } = + buildMessengerGroup(); await withController( { - messenger, + messenger: restrictedMessenger, state: { provider: { type: networkType, @@ -4966,8 +5104,8 @@ describe('NetworkController', () => { network.mockEssentialRpcCalls(); const networkWillChange = await waitForPublishedEvents({ - messenger, - eventType: 'NetworkController:networkWillChange', + messenger: unrestrictedMessenger, + eventType: NetworkControllerEventType.NetworkWillChange, operation: () => { controller.resetConnection(); }, @@ -5081,10 +5219,13 @@ describe('NetworkController', () => { controller.resetConnection(); const { provider } = controller.getProviderAndBlockTracker(); + assert(provider, 'Provider is somehow unset'); const promisifiedSendAsync = promisify(provider.sendAsync).bind( provider, ); const { result: chainIdResult } = await promisifiedSendAsync({ + id: '1', + jsonrpc: '2.0', method: 'eth_chainId', }); expect(chainIdResult).toBe(chainId); @@ -5124,11 +5265,12 @@ describe('NetworkController', () => { }); it('emits networkDidChange', async () => { - const messenger = buildMessenger(); + const { unrestrictedMessenger, restrictedMessenger } = + buildMessengerGroup(); await withController( { - messenger, + messenger: restrictedMessenger, state: { provider: { type: networkType, @@ -5142,8 +5284,8 @@ describe('NetworkController', () => { network.mockEssentialRpcCalls(); const networkDidChange = await waitForPublishedEvents({ - messenger, - eventType: 'NetworkController:networkDidChange', + messenger: unrestrictedMessenger, + eventType: NetworkControllerEventType.NetworkDidChange, operation: () => { controller.resetConnection(); }, @@ -5155,11 +5297,12 @@ describe('NetworkController', () => { }); it('emits infuraIsBlocked or infuraIsUnblocked, depending on whether Infura is blocking requests', async () => { - const messenger = buildMessenger(); + const { unrestrictedMessenger, restrictedMessenger } = + buildMessengerGroup(); await withController( { - messenger, + messenger: restrictedMessenger, state: { provider: { type: networkType, @@ -5177,13 +5320,13 @@ describe('NetworkController', () => { }); const promiseForNoInfuraIsUnblockedEvents = waitForPublishedEvents({ - messenger, - eventType: 'NetworkController:infuraIsUnblocked', + messenger: unrestrictedMessenger, + eventType: NetworkControllerEventType.InfuraIsUnblocked, count: 0, }); const promiseForInfuraIsBlocked = waitForPublishedEvents({ - messenger, - eventType: 'NetworkController:infuraIsBlocked', + messenger: unrestrictedMessenger, + eventType: NetworkControllerEventType.InfuraIsBlocked, }); controller.resetConnection(); @@ -5268,18 +5411,18 @@ describe('NetworkController', () => { describe(`when the type in the provider configuration is "rpc"`, () => { it('emits networkWillChange', async () => { - const messenger = buildMessenger(); + const { unrestrictedMessenger, restrictedMessenger } = + buildMessengerGroup(); await withController( { - messenger, + messenger: restrictedMessenger, state: { provider: { type: 'rpc', rpcUrl: 'https://mock-rpc-url', chainId: '0xtest', ticker: 'TEST', - id: 'testNetworkConfigurationId', }, networkConfigurations: { testNetworkConfigurationId: { @@ -5295,8 +5438,8 @@ describe('NetworkController', () => { network.mockEssentialRpcCalls(); const networkWillChange = await waitForPublishedEvents({ - messenger, - eventType: 'NetworkController:networkWillChange', + messenger: unrestrictedMessenger, + eventType: NetworkControllerEventType.NetworkWillChange, operation: () => { controller.resetConnection(); }, @@ -5316,7 +5459,6 @@ describe('NetworkController', () => { rpcUrl: 'https://mock-rpc-url', chainId: '0xtest', ticker: 'TEST', - id: 'testNetworkConfigurationId', }, networkConfigurations: { testNetworkConfigurationId: { @@ -5366,7 +5508,6 @@ describe('NetworkController', () => { rpcUrl: 'https://mock-rpc-url', chainId: '0xtest', ticker: 'TEST', - id: 'testNetworkConfigurationId', }, networkConfigurations: { testNetworkConfigurationId: { @@ -5421,7 +5562,6 @@ describe('NetworkController', () => { rpcUrl: 'https://mock-rpc-url', chainId: '0x1337', ticker: 'TEST', - id: 'testNetworkConfigurationId', }, networkConfigurations: { testNetworkConfigurationId: { @@ -5439,10 +5579,13 @@ describe('NetworkController', () => { controller.resetConnection(); const { provider } = controller.getProviderAndBlockTracker(); + assert(provider, 'Provider is somehow unset'); const promisifiedSendAsync = promisify(provider.sendAsync).bind( provider, ); const { result: chainIdResult } = await promisifiedSendAsync({ + id: '1', + jsonrpc: '2.0', method: 'eth_chainId', }); expect(chainIdResult).toBe('0x1337'); @@ -5459,7 +5602,6 @@ describe('NetworkController', () => { rpcUrl: 'https://mock-rpc-url', chainId: '0xtest', ticker: 'TEST', - id: 'testNetworkConfigurationId', }, networkConfigurations: { testNetworkConfigurationId: { @@ -5496,18 +5638,18 @@ describe('NetworkController', () => { }); it('emits networkDidChange', async () => { - const messenger = buildMessenger(); + const { unrestrictedMessenger, restrictedMessenger } = + buildMessengerGroup(); await withController( { - messenger, + messenger: restrictedMessenger, state: { provider: { type: 'rpc', rpcUrl: 'https://mock-rpc-url', chainId: '0xtest', ticker: 'TEST', - id: 'testNetworkConfigurationId', }, networkConfigurations: { testNetworkConfigurationId: { @@ -5523,8 +5665,8 @@ describe('NetworkController', () => { network.mockEssentialRpcCalls(); const networkDidChange = await waitForPublishedEvents({ - messenger, - eventType: 'NetworkController:networkDidChange', + messenger: unrestrictedMessenger, + eventType: NetworkControllerEventType.NetworkDidChange, operation: () => { controller.resetConnection(); }, @@ -5536,18 +5678,18 @@ describe('NetworkController', () => { }); it('emits infuraIsUnblocked', async () => { - const messenger = buildMessenger(); + const { unrestrictedMessenger, restrictedMessenger } = + buildMessengerGroup(); await withController( { - messenger, + messenger: restrictedMessenger, state: { provider: { type: 'rpc', rpcUrl: 'https://mock-rpc-url', chainId: '0xtest', ticker: 'TEST', - id: 'testNetworkConfigurationId', }, networkConfigurations: { testNetworkConfigurationId: { @@ -5563,8 +5705,8 @@ describe('NetworkController', () => { network.mockEssentialRpcCalls(); const infuraIsUnblocked = await waitForPublishedEvents({ - messenger, - eventType: 'NetworkController:infuraIsUnblocked', + messenger: unrestrictedMessenger, + eventType: NetworkControllerEventType.InfuraIsUnblocked, operation: () => { controller.resetConnection(); }, @@ -5584,7 +5726,6 @@ describe('NetworkController', () => { rpcUrl: 'https://mock-rpc-url', chainId: '0xtest', ticker: 'TEST', - id: 'testNetworkConfigurationId', }, networkConfigurations: { testNetworkConfigurationId: { @@ -5626,7 +5767,6 @@ describe('NetworkController', () => { rpcUrl: 'https://mock-rpc-url', chainId: '0xtest', ticker: 'TEST', - id: 'testNetworkConfigurationId', }, networkConfigurations: { testNetworkConfigurationId: { @@ -5684,7 +5824,6 @@ describe('NetworkController', () => { rpcPrefs: { blockExplorerUrl: 'https://test-block-explorer-1.com', }, - id: 'testNetworkConfigurationId1', }, networkConfigurations: { testNetworkConfigurationId1: { @@ -5711,11 +5850,8 @@ describe('NetworkController', () => { }, }, async ({ controller, network: previousNetwork }) => { - const currentNetwork = new NetworkCommunications({ - networkClientType: 'custom', - networkClientOptions: { - customRpcUrl: 'https://mock-rpc-url-2', - }, + const currentNetwork = new CustomNetworkCommunications({ + customRpcUrl: 'https://mock-rpc-url-2', }); currentNetwork.mockEssentialRpcCalls(); previousNetwork.mockEssentialRpcCalls(); @@ -5753,18 +5889,18 @@ describe('NetworkController', () => { rpcPrefs: { blockExplorerUrl: 'https://test-block-explorer-1.com', }, - id: 'testNetworkConfigurationId1', }); }, ); }); it('emits networkWillChange', async () => { - const messenger = buildMessenger(); + const { unrestrictedMessenger, restrictedMessenger } = + buildMessengerGroup(); await withController( { - messenger, + messenger: restrictedMessenger, state: { provider: { type: networkType, @@ -5788,11 +5924,8 @@ describe('NetworkController', () => { }, }, async ({ controller, network: previousNetwork }) => { - const currentNetwork = new NetworkCommunications({ - networkClientType: 'custom', - networkClientOptions: { - customRpcUrl: 'https://mock-rpc-url', - }, + const currentNetwork = new CustomNetworkCommunications({ + customRpcUrl: 'https://mock-rpc-url', }); currentNetwork.mockEssentialRpcCalls(); previousNetwork.mockEssentialRpcCalls(); @@ -5807,8 +5940,8 @@ describe('NetworkController', () => { controller, operation: async () => { const networkWillChange = await waitForPublishedEvents({ - messenger, - eventType: 'NetworkController:networkWillChange', + messenger: unrestrictedMessenger, + eventType: NetworkControllerEventType.NetworkWillChange, operation: () => { controller.rollbackToPreviousProvider(); }, @@ -5847,11 +5980,8 @@ describe('NetworkController', () => { }, }, async ({ controller, network: previousNetwork }) => { - const currentNetwork = new NetworkCommunications({ - networkClientType: 'custom', - networkClientOptions: { - customRpcUrl: 'https://mock-rpc-url', - }, + const currentNetwork = new CustomNetworkCommunications({ + customRpcUrl: 'https://mock-rpc-url', }); currentNetwork.mockEssentialRpcCalls(); previousNetwork.mockEssentialRpcCalls(); @@ -5914,11 +6044,8 @@ describe('NetworkController', () => { }, }, async ({ controller, network: previousNetwork }) => { - const currentNetwork = new NetworkCommunications({ - networkClientType: 'custom', - networkClientOptions: { - customRpcUrl: 'https://mock-rpc-url', - }, + const currentNetwork = new CustomNetworkCommunications({ + customRpcUrl: 'https://mock-rpc-url', }); currentNetwork.mockEssentialRpcCalls({ latestBlock: POST_1559_BLOCK, @@ -5989,11 +6116,8 @@ describe('NetworkController', () => { }, }, async ({ controller, network: previousNetwork }) => { - const currentNetwork = new NetworkCommunications({ - networkClientType: 'custom', - networkClientOptions: { - customRpcUrl: 'https://mock-rpc-url', - }, + const currentNetwork = new CustomNetworkCommunications({ + customRpcUrl: 'https://mock-rpc-url', }); currentNetwork.mockEssentialRpcCalls(); previousNetwork.mockEssentialRpcCalls(); @@ -6012,10 +6136,13 @@ describe('NetworkController', () => { }); const { provider } = controller.getProviderAndBlockTracker(); + assert(provider, 'Provider is somehow unset'); const promisifiedSendAsync = promisify(provider.sendAsync).bind( provider, ); const { result: chainIdResult } = await promisifiedSendAsync({ + id: '1', + jsonrpc: '2.0', method: 'eth_chainId', }); expect(chainIdResult).toBe(chainId); @@ -6049,11 +6176,8 @@ describe('NetworkController', () => { }, }, async ({ controller, network: previousNetwork }) => { - const currentNetwork = new NetworkCommunications({ - networkClientType: 'custom', - networkClientOptions: { - customRpcUrl: 'https://mock-rpc-url', - }, + const currentNetwork = new CustomNetworkCommunications({ + customRpcUrl: 'https://mock-rpc-url', }); currentNetwork.mockEssentialRpcCalls(); previousNetwork.mockEssentialRpcCalls(); @@ -6081,11 +6205,12 @@ describe('NetworkController', () => { }); it('emits networkDidChange', async () => { - const messenger = buildMessenger(); + const { unrestrictedMessenger, restrictedMessenger } = + buildMessengerGroup(); await withController( { - messenger, + messenger: restrictedMessenger, state: { provider: { type: networkType, @@ -6109,11 +6234,8 @@ describe('NetworkController', () => { }, }, async ({ controller, network: previousNetwork }) => { - const currentNetwork = new NetworkCommunications({ - networkClientType: 'custom', - networkClientOptions: { - customRpcUrl: 'https://mock-rpc-url', - }, + const currentNetwork = new CustomNetworkCommunications({ + customRpcUrl: 'https://mock-rpc-url', }); currentNetwork.mockEssentialRpcCalls(); previousNetwork.mockEssentialRpcCalls(); @@ -6129,8 +6251,8 @@ describe('NetworkController', () => { controller, operation: async () => { const networkDidChange = await waitForPublishedEvents({ - messenger, - eventType: 'NetworkController:networkDidChange', + messenger: unrestrictedMessenger, + eventType: NetworkControllerEventType.NetworkDidChange, operation: () => { controller.rollbackToPreviousProvider(); }, @@ -6143,11 +6265,12 @@ describe('NetworkController', () => { }); it('emits infuraIsBlocked or infuraIsUnblocked, depending on whether Infura is blocking requests for the previous network', async () => { - const messenger = buildMessenger(); + const { unrestrictedMessenger, restrictedMessenger } = + buildMessengerGroup(); await withController( { - messenger, + messenger: restrictedMessenger, state: { provider: { type: networkType, @@ -6166,11 +6289,8 @@ describe('NetworkController', () => { }, }, async ({ controller, network: previousNetwork }) => { - const currentNetwork = new NetworkCommunications({ - networkClientType: 'custom', - networkClientOptions: { - customRpcUrl: 'https://mock-rpc-url', - }, + const currentNetwork = new CustomNetworkCommunications({ + customRpcUrl: 'https://mock-rpc-url', }); currentNetwork.mockEssentialRpcCalls(); previousNetwork.mockEssentialRpcCalls({ @@ -6186,13 +6306,13 @@ describe('NetworkController', () => { }); const promiseForNoInfuraIsUnblockedEvents = waitForPublishedEvents({ - messenger, - eventType: 'NetworkController:infuraIsUnblocked', + messenger: unrestrictedMessenger, + eventType: NetworkControllerEventType.InfuraIsUnblocked, count: 0, }); const promiseForInfuraIsBlocked = waitForPublishedEvents({ - messenger, - eventType: 'NetworkController:infuraIsBlocked', + messenger: unrestrictedMessenger, + eventType: NetworkControllerEventType.InfuraIsBlocked, }); await waitForLookupNetworkToComplete({ @@ -6213,12 +6333,12 @@ describe('NetworkController', () => { type: networkType, // NOTE: This doesn't need to match the logical chain ID of // the network selected, it just needs to exist - chainId: '0x9999999', + chainId: '0x9999999' as const, }; const currentNetworkConfiguration = { id: 'currentNetworkConfiguration', rpcUrl: 'https://mock-rpc-url', - chainId: '0x1337', + chainId: '0x1337' as const, ticker: 'TEST', }; await withController( @@ -6231,11 +6351,8 @@ describe('NetworkController', () => { }, }, async ({ controller, network: previousNetwork }) => { - const currentNetwork = new NetworkCommunications({ - networkClientType: 'custom', - networkClientOptions: { - customRpcUrl: 'https://mock-rpc-url', - }, + const currentNetwork = new CustomNetworkCommunications({ + customRpcUrl: 'https://mock-rpc-url', }); currentNetwork.mockEssentialRpcCalls({ net_version: { @@ -6296,11 +6413,8 @@ describe('NetworkController', () => { }, }, async ({ controller, network: previousNetwork }) => { - const currentNetwork = new NetworkCommunications({ - networkClientType: 'custom', - networkClientOptions: { - customRpcUrl: 'https://mock-rpc-url', - }, + const currentNetwork = new CustomNetworkCommunications({ + customRpcUrl: 'https://mock-rpc-url', }); currentNetwork.mockEssentialRpcCalls({ latestBlock: PRE_1559_BLOCK, @@ -6353,7 +6467,6 @@ describe('NetworkController', () => { rpcPrefs: { blockExplorerUrl: 'test-block-explorer-2.com', }, - id: 'testNetworkConfigurationId2', }, networkDetails: { EIPS: { @@ -6385,11 +6498,8 @@ describe('NetworkController', () => { }, }, async ({ controller, network: previousNetwork }) => { - const currentNetwork = new NetworkCommunications({ - networkClientType: 'infura', - networkClientOptions: { - infuraNetwork: 'goerli', - }, + const currentNetwork = new InfuraNetworkCommunications({ + infuraNetwork: 'goerli', }); currentNetwork.mockEssentialRpcCalls(); previousNetwork.mockEssentialRpcCalls(); @@ -6426,25 +6536,24 @@ describe('NetworkController', () => { rpcPrefs: { blockExplorerUrl: 'test-block-explorer-2.com', }, - id: 'testNetworkConfigurationId2', }); }, ); }); it('emits networkWillChange', async () => { - const messenger = buildMessenger(); + const { unrestrictedMessenger, restrictedMessenger } = + buildMessengerGroup(); await withController( { - messenger, + messenger: restrictedMessenger, state: { provider: { type: 'rpc', rpcUrl: 'https://mock-rpc-url-2', chainId: '0x1337', ticker: 'TEST2', - id: 'testNetworkConfigurationId2', }, networkConfigurations: { testNetworkConfigurationId1: { @@ -6463,11 +6572,8 @@ describe('NetworkController', () => { }, }, async ({ controller, network: previousNetwork }) => { - const currentNetwork = new NetworkCommunications({ - networkClientType: 'infura', - networkClientOptions: { - infuraNetwork: 'goerli', - }, + const currentNetwork = new InfuraNetworkCommunications({ + infuraNetwork: 'goerli', }); currentNetwork.mockEssentialRpcCalls(); previousNetwork.mockEssentialRpcCalls(); @@ -6482,8 +6588,8 @@ describe('NetworkController', () => { controller, operation: async () => { const networkWillChange = await waitForPublishedEvents({ - messenger, - eventType: 'NetworkController:networkWillChange', + messenger: unrestrictedMessenger, + eventType: NetworkControllerEventType.NetworkWillChange, operation: () => { controller.rollbackToPreviousProvider(); }, @@ -6505,7 +6611,6 @@ describe('NetworkController', () => { rpcUrl: 'https://mock-rpc-url', chainId: '0xtest', ticker: 'TEST', - id: 'testNetworkConfigurationId', }, networkConfigurations: { testNetworkConfigurationId: { @@ -6518,11 +6623,8 @@ describe('NetworkController', () => { }, }, async ({ controller, network: previousNetwork }) => { - const currentNetwork = new NetworkCommunications({ - networkClientType: 'infura', - networkClientOptions: { - infuraNetwork: 'goerli', - }, + const currentNetwork = new InfuraNetworkCommunications({ + infuraNetwork: 'goerli', }); currentNetwork.mockEssentialRpcCalls(); previousNetwork.mockEssentialRpcCalls(); @@ -6566,7 +6668,6 @@ describe('NetworkController', () => { rpcUrl: 'https://mock-rpc-url', chainId: '0xtest', ticker: 'TEST', - id: 'testNetworkConfigurationId', }, networkConfigurations: { testNetworkConfigurationId: { @@ -6579,11 +6680,8 @@ describe('NetworkController', () => { }, }, async ({ controller, network: previousNetwork }) => { - const currentNetwork = new NetworkCommunications({ - networkClientType: 'infura', - networkClientOptions: { - infuraNetwork: 'goerli', - }, + const currentNetwork = new InfuraNetworkCommunications({ + infuraNetwork: 'goerli', }); currentNetwork.mockEssentialRpcCalls({ latestBlock: POST_1559_BLOCK, @@ -6637,7 +6735,6 @@ describe('NetworkController', () => { rpcUrl: 'https://mock-rpc-url', chainId: '0x1337', ticker: 'TEST', - id: 'testNetworkConfigurationId', }, networkConfigurations: { testNetworkConfigurationId: { @@ -6650,11 +6747,8 @@ describe('NetworkController', () => { }, }, async ({ controller, network: previousNetwork }) => { - const currentNetwork = new NetworkCommunications({ - networkClientType: 'infura', - networkClientOptions: { - infuraNetwork: 'goerli', - }, + const currentNetwork = new InfuraNetworkCommunications({ + infuraNetwork: 'goerli', }); currentNetwork.mockEssentialRpcCalls(); previousNetwork.mockEssentialRpcCalls(); @@ -6673,10 +6767,13 @@ describe('NetworkController', () => { }); const { provider } = controller.getProviderAndBlockTracker(); + assert(provider, 'Provider is somehow unset'); const promisifiedSendAsync = promisify(provider.sendAsync).bind( provider, ); const { result: chainIdResult } = await promisifiedSendAsync({ + id: '1', + jsonrpc: '2.0', method: 'eth_chainId', }); expect(chainIdResult).toBe('0x1337'); @@ -6693,7 +6790,6 @@ describe('NetworkController', () => { rpcUrl: 'https://mock-rpc-url', chainId: '0xtest', ticker: 'TEST', - id: 'testNetworkConfigurationId', }, networkConfigurations: { testNetworkConfigurationId: { @@ -6706,11 +6802,8 @@ describe('NetworkController', () => { }, }, async ({ controller, network: previousNetwork }) => { - const currentNetwork = new NetworkCommunications({ - networkClientType: 'infura', - networkClientOptions: { - infuraNetwork: 'goerli', - }, + const currentNetwork = new InfuraNetworkCommunications({ + infuraNetwork: 'goerli', }); currentNetwork.mockEssentialRpcCalls(); previousNetwork.mockEssentialRpcCalls(); @@ -6738,18 +6831,18 @@ describe('NetworkController', () => { }); it('emits networkDidChange', async () => { - const messenger = buildMessenger(); + const { unrestrictedMessenger, restrictedMessenger } = + buildMessengerGroup(); await withController( { - messenger, + messenger: restrictedMessenger, state: { provider: { type: 'rpc', rpcUrl: 'https://mock-rpc-url', chainId: '0xtest', ticker: 'TEST', - id: 'testNetworkConfigurationId', }, networkConfigurations: { testNetworkConfigurationId: { @@ -6762,11 +6855,8 @@ describe('NetworkController', () => { }, }, async ({ controller, network: previousNetwork }) => { - const currentNetwork = new NetworkCommunications({ - networkClientType: 'infura', - networkClientOptions: { - infuraNetwork: 'goerli', - }, + const currentNetwork = new InfuraNetworkCommunications({ + infuraNetwork: 'goerli', }); currentNetwork.mockEssentialRpcCalls(); previousNetwork.mockEssentialRpcCalls(); @@ -6782,8 +6872,8 @@ describe('NetworkController', () => { controller, operation: async () => { const networkDidChange = await waitForPublishedEvents({ - messenger, - eventType: 'NetworkController:networkDidChange', + messenger: unrestrictedMessenger, + eventType: NetworkControllerEventType.NetworkDidChange, operation: () => { controller.rollbackToPreviousProvider(); }, @@ -6796,18 +6886,18 @@ describe('NetworkController', () => { }); it('emits infuraIsUnblocked', async () => { - const messenger = buildMessenger(); + const { unrestrictedMessenger, restrictedMessenger } = + buildMessengerGroup(); await withController( { - messenger, + messenger: restrictedMessenger, state: { provider: { type: 'rpc', rpcUrl: 'https://mock-rpc-url', chainId: '0xtest', ticker: 'TEST', - id: 'testNetworkConfigurationId', }, networkConfigurations: { testNetworkConfigurationId: { @@ -6820,11 +6910,8 @@ describe('NetworkController', () => { }, }, async ({ controller, network: previousNetwork }) => { - const currentNetwork = new NetworkCommunications({ - networkClientType: 'infura', - networkClientOptions: { - infuraNetwork: 'goerli', - }, + const currentNetwork = new InfuraNetworkCommunications({ + infuraNetwork: 'goerli', }); currentNetwork.mockEssentialRpcCalls(); previousNetwork.mockEssentialRpcCalls(); @@ -6840,8 +6927,8 @@ describe('NetworkController', () => { controller, operation: async () => { const infuraIsUnblocked = await waitForPublishedEvents({ - messenger, - eventType: 'NetworkController:infuraIsUnblocked', + messenger: unrestrictedMessenger, + eventType: NetworkControllerEventType.InfuraIsUnblocked, operation: () => { controller.rollbackToPreviousProvider(); }, @@ -6863,7 +6950,6 @@ describe('NetworkController', () => { rpcUrl: 'https://mock-rpc-url', chainId: '0xtest', ticker: 'TEST', - id: 'testNetworkConfigurationId', }, networkConfigurations: { testNetworkConfigurationId: { @@ -6876,11 +6962,8 @@ describe('NetworkController', () => { }, }, async ({ controller, network: previousNetwork }) => { - const currentNetwork = new NetworkCommunications({ - networkClientType: 'infura', - networkClientOptions: { - infuraNetwork: 'goerli', - }, + const currentNetwork = new InfuraNetworkCommunications({ + infuraNetwork: 'goerli', }); currentNetwork.mockEssentialRpcCalls({ // This results in a successful call to eth_getBlockByNumber @@ -6921,7 +7004,6 @@ describe('NetworkController', () => { rpcUrl: 'https://mock-rpc-url', chainId: '0xtest', ticker: 'TEST', - id: 'testNetworkConfigurationId', }, networkConfigurations: { testNetworkConfigurationId: { @@ -6934,11 +7016,8 @@ describe('NetworkController', () => { }, }, async ({ controller, network: previousNetwork }) => { - const currentNetwork = new NetworkCommunications({ - networkClientType: 'infura', - networkClientOptions: { - infuraNetwork: 'goerli', - }, + const currentNetwork = new InfuraNetworkCommunications({ + infuraNetwork: 'goerli', }); currentNetwork.mockEssentialRpcCalls({ latestBlock: PRE_1559_BLOCK, @@ -6983,6 +7062,7 @@ describe('NetworkController', () => { expect(() => controller.upsertNetworkConfiguration( { + /* @ts-expect-error We are intentionally passing bad input. */ chainId: invalidChainId, nickname: 'RPC', rpcPrefs: { blockExplorerUrl: 'test-block-explorer.com' }, @@ -7030,6 +7110,7 @@ describe('NetworkController', () => { await withController(async ({ controller }) => { expect(() => controller.upsertNetworkConfiguration( + /* @ts-expect-error We are intentionally passing bad input. */ { chainId: '0x9999', nickname: 'RPC', @@ -7073,6 +7154,7 @@ describe('NetworkController', () => { await withController(async ({ controller }) => { expect(() => controller.upsertNetworkConfiguration( + /* @ts-expect-error We are intentionally passing bad input. */ { chainId: '0x5', nickname: 'RPC', @@ -7095,6 +7177,7 @@ describe('NetworkController', () => { it('throws if an options object is not passed as a second argument', async () => { await withController(async ({ controller }) => { expect(() => + /* @ts-expect-error We are intentionally passing bad input. */ controller.upsertNetworkConfiguration({ chainId: '0x5', nickname: 'RPC', @@ -7110,7 +7193,7 @@ describe('NetworkController', () => { }); it('should add the given network if all required properties are present but nither rpcPrefs nor nickname properties are passed', async () => { - v4.mockImplementationOnce(() => 'networkConfigurationId'); + uuidV4Mock.mockImplementationOnce(() => 'networkConfigurationId'); await withController( { state: { @@ -7119,7 +7202,7 @@ describe('NetworkController', () => { }, async ({ controller }) => { const rpcUrlNetwork = { - chainId: '0x1', + chainId: '0x1' as const, rpcUrl: 'https://test-rpc-url', ticker: 'test_ticker', }; @@ -7146,7 +7229,7 @@ describe('NetworkController', () => { }); it('adds new networkConfiguration to networkController store, but only adds valid properties (rpcUrl, chainId, ticker, nickname, rpcPrefs) and fills any missing properties from this list as undefined', async function () { - v4.mockImplementationOnce(() => 'networkConfigurationId'); + uuidV4Mock.mockImplementationOnce(() => 'networkConfigurationId'); await withController( { state: { @@ -7155,7 +7238,7 @@ describe('NetworkController', () => { }, async ({ controller }) => { const rpcUrlNetwork = { - chainId: '0x1', + chainId: '0x1' as const, rpcUrl: 'https://test-rpc-url', ticker: 'test_ticker', invalidKey: 'new-chain', @@ -7186,7 +7269,7 @@ describe('NetworkController', () => { }); it('should add the given network configuration if its rpcURL does not match an existing configuration without changing or overwriting other configurations', async () => { - v4.mockImplementationOnce(() => 'networkConfigurationId2'); + uuidV4Mock.mockImplementationOnce(() => 'networkConfigurationId2'); await withController( { state: { @@ -7204,7 +7287,7 @@ describe('NetworkController', () => { }, async ({ controller }) => { const rpcUrlNetwork = { - chainId: '0x1', + chainId: '0x1' as const, nickname: 'RPC', rpcPrefs: undefined, rpcUrl: 'https://test-rpc-url-2', @@ -7258,7 +7341,7 @@ describe('NetworkController', () => { ticker: 'new_rpc_ticker', nickname: 'new_rpc_chainName', rpcPrefs: { blockExplorerUrl: 'alternativetestchainscan.io' }, - chainId: '0x1', + chainId: '0x1' as const, }; controller.upsertNetworkConfiguration(updatedConfiguration, { referrer: 'https://test-dapp.com', @@ -7344,13 +7427,12 @@ describe('NetworkController', () => { }); it('should add the given network and not set it to active if the setActive option is not passed (or a falsy value is passed)', async () => { - v4.mockImplementationOnce(() => 'networkConfigurationId'); + uuidV4Mock.mockImplementationOnce(() => 'networkConfigurationId'); const originalProvider = { - type: 'rpc', + type: NETWORK_TYPES.RPC, rpcUrl: 'https://mock-rpc-url', - chainId: '0xtest', + chainId: '0xtest' as const, ticker: 'TEST', - id: 'testNetworkConfigurationId', }; await withController( { @@ -7368,7 +7450,7 @@ describe('NetworkController', () => { }, async ({ controller }) => { const rpcUrlNetwork = { - chainId: '0x1', + chainId: '0x1' as const, rpcUrl: 'https://test-rpc-url', ticker: 'test_ticker', }; @@ -7386,7 +7468,7 @@ describe('NetworkController', () => { }); it('should add the given network and set it to active if the setActive option is passed as true', async () => { - v4.mockImplementationOnce(() => 'networkConfigurationId'); + uuidV4Mock.mockImplementationOnce(() => 'networkConfigurationId'); await withController( { state: { @@ -7395,7 +7477,6 @@ describe('NetworkController', () => { rpcUrl: 'https://mock-rpc-url', chainId: '0xtest', ticker: 'TEST', - id: 'testNetworkConfigurationId', }, networkConfigurations: { testNetworkConfigurationId: { @@ -7408,15 +7489,12 @@ describe('NetworkController', () => { }, }, async ({ controller }) => { - const network = new NetworkCommunications({ - networkClientType: 'custom', - networkClientOptions: { - customRpcUrl: 'https://test-rpc-url', - }, + const network = new CustomNetworkCommunications({ + customRpcUrl: 'https://test-rpc-url', }); network.mockEssentialRpcCalls(); const rpcUrlNetwork = { - chainId: '0x1', + chainId: '0x1' as const, rpcUrl: 'https://test-rpc-url', ticker: 'test_ticker', }; @@ -7439,7 +7517,7 @@ describe('NetworkController', () => { }); it('adds new networkConfiguration to networkController store and calls to the metametrics event tracking with the correct values', async () => { - v4.mockImplementationOnce(() => 'networkConfigurationId'); + uuidV4Mock.mockImplementationOnce(() => 'networkConfigurationId'); const trackEventSpy = jest.fn(); await withController( { @@ -7449,7 +7527,6 @@ describe('NetworkController', () => { rpcUrl: 'https://mock-rpc-url', chainId: '0xtest', ticker: 'TEST', - id: 'testNetworkConfigurationId', }, networkConfigurations: { testNetworkConfigurationId: { @@ -7465,7 +7542,7 @@ describe('NetworkController', () => { async ({ controller }) => { const newNetworkConfiguration = { rpcUrl: 'https://new-chain-rpc-url', - chainId: '0x9999', + chainId: '0x9999' as const, ticker: 'NEW', nickname: 'new-chain', rpcPrefs: { blockExplorerUrl: 'https://block-explorer' }, @@ -7507,7 +7584,7 @@ describe('NetworkController', () => { }); it('throws if referrer and source arguments are not passed', async () => { - v4.mockImplementationOnce(() => 'networkConfigurationId'); + uuidV4Mock.mockImplementationOnce(() => 'networkConfigurationId'); const trackEventSpy = jest.fn(); await withController( { @@ -7517,7 +7594,6 @@ describe('NetworkController', () => { rpcUrl: 'https://mock-rpc-url', chainId: '0xtest', ticker: 'TEST', - id: 'testNetworkConfigurationId', }, networkConfigurations: { testNetworkConfigurationId: { @@ -7533,15 +7609,16 @@ describe('NetworkController', () => { async ({ controller }) => { const newNetworkConfiguration = { rpcUrl: 'https://new-chain-rpc-url', - chainId: '0x9999', + chainId: '0x9999' as const, ticker: 'NEW', nickname: 'new-chain', rpcPrefs: { blockExplorerUrl: 'https://block-explorer' }, }; - expect(() => - controller.upsertNetworkConfiguration(newNetworkConfiguration, {}), - ).toThrow( + expect(() => { + /* @ts-expect-error We are intentionally passing bad input. */ + controller.upsertNetworkConfiguration(newNetworkConfiguration, {}); + }).toThrow( 'referrer and source are required arguments for adding or updating a network configuration', ); }, @@ -7557,11 +7634,12 @@ describe('NetworkController', () => { state: { networkConfigurations: { [networkConfigurationId]: { + id: 'aaaaaa', rpcUrl: 'https://test-rpc-url', ticker: 'old_rpc_ticker', nickname: 'old_rpc_chainName', rpcPrefs: { blockExplorerUrl: 'testchainscan.io' }, - chainId: '1', + chainId: '0x1', }, }, }, @@ -7571,11 +7649,12 @@ describe('NetworkController', () => { Object.values(controller.store.getState().networkConfigurations), ).toStrictEqual([ { + id: 'aaaaaa', rpcUrl: 'https://test-rpc-url', ticker: 'old_rpc_ticker', nickname: 'old_rpc_chainName', rpcPrefs: { blockExplorerUrl: 'testchainscan.io' }, - chainId: '1', + chainId: '0x1', }, ]); controller.removeNetworkConfiguration(networkConfigurationId); @@ -7589,22 +7668,31 @@ describe('NetworkController', () => { }); /** - * Builds the controller messenger that NetworkController is designed to work - * with. + * Builds the set of controller messengers that recognizes the events that + * NetworkController emits: one designed to be used directly by + * NetworkController, and one designed to be used in tests. * * @returns The controller messenger. */ -function buildMessenger() { - return new ControllerMessenger().getRestricted({ +function buildMessengerGroup() { + const unrestrictedMessenger = new ControllerMessenger< + never, + NetworkControllerEvent + >(); + const restrictedMessenger = unrestrictedMessenger.getRestricted< + 'NetworkController', + never, + NetworkControllerEventType + >({ name: 'NetworkController', - allowedActions: [], allowedEvents: [ - 'NetworkController:networkDidChange', - 'NetworkController:networkWillChange', - 'NetworkController:infuraIsBlocked', - 'NetworkController:infuraIsUnblocked', + NetworkControllerEventType.NetworkDidChange, + NetworkControllerEventType.NetworkWillChange, + NetworkControllerEventType.InfuraIsBlocked, + NetworkControllerEventType.InfuraIsUnblocked, ], }); + return { unrestrictedMessenger, restrictedMessenger }; } /** @@ -7612,16 +7700,46 @@ function buildMessenger() { * Infura project ID. The object that this function returns is mixed into the * options first when a NetworkController is instantiated in tests. * - * @returns {object} The controller options. + * @returns The controller options. */ function buildDefaultNetworkControllerOptions() { + const { restrictedMessenger } = buildMessengerGroup(); return { - messenger: buildMessenger(), + messenger: restrictedMessenger, infuraProjectId: DEFAULT_INFURA_PROJECT_ID, trackMetaMetricsEvent: jest.fn(), }; } +/** + * `withController` takes a callback as its last argument. It also takes an + * options bag, which may be specified before the callback. The callback itself + * is called with one of two variants of NetworkCommunications. If the options + * bag was specified and it is being used to configure an Infura provider based + * on the provider type, then the callback is called with an + * InfuraNetworkCommunications; otherwise it is called with a + * CustomNetworkCommunications. + * + * How do we test for the exact code path in `withController`? Because the type + * of the options bag is not a discriminated union, we can't "reach" into the + * bag and test for the provider type. Instead, we need to use a type guard. + * This is that type guard. + * + * @param args - The arguments to `withController`. + * @returns True if the arguments feature an options bag and this bag contains + * provider configuration for an Infura network. + */ +function hasOptionsWithInfuraProviderConfig( + args: WithControllerArgs, +): args is WithControllerArgsWithConfiguredInfuraProvider { + return ( + args.length === 2 && + args[0].state !== undefined && + args[0].state.provider !== undefined && + args[0].state.provider.type !== 'rpc' + ); +} + /** * Builds a controller based on the given options, and calls the given function * with that controller. @@ -7632,31 +7750,58 @@ function buildDefaultNetworkControllerOptions() { * requests. * @returns Whatever the callback returns. */ -async function withController(...args) { - const [givenNetworkControllerOptions, fn] = - args.length === 2 ? args : [{}, args[0]]; - const constructorOptions = { - ...buildDefaultNetworkControllerOptions(), - ...givenNetworkControllerOptions, - }; - const controller = new NetworkController(constructorOptions); +async function withController( + options: NetworkControllerOptionsWithInfuraProviderConfig, + callback: WithControllerCallback, +): Promise; +async function withController( + options: Partial, + callback: WithControllerCallback, +): Promise; +async function withController( + callback: WithControllerCallback, +): Promise; +async function withController( + ...args: WithControllerArgs +) { + if (args.length === 2 && hasOptionsWithInfuraProviderConfig(args)) { + const [givenNetworkControllerOptions, callback] = args; + const constructorOptions = { + ...buildDefaultNetworkControllerOptions(), + ...givenNetworkControllerOptions, + }; + const controller = new NetworkController(constructorOptions); - const providerConfig = controller.store.getState().provider; - const networkClientType = providerConfig.type === 'rpc' ? 'custom' : 'infura'; - const { infuraProjectId } = constructorOptions; - const infuraNetwork = - networkClientType === 'infura' ? providerConfig.type : undefined; - const customRpcUrl = - networkClientType === 'custom' ? providerConfig.rpcUrl : undefined; - const network = new NetworkCommunications({ - networkClientType, - networkClientOptions: { infuraProjectId, infuraNetwork, customRpcUrl }, - }); + const providerType = givenNetworkControllerOptions.state.provider.type; + const network = new InfuraNetworkCommunications({ + infuraProjectId: constructorOptions.infuraProjectId, + infuraNetwork: providerType, + }); - try { - return await fn({ controller, network }); - } finally { - await controller.destroy(); + try { + return await callback({ controller, network }); + } finally { + await controller.destroy(); + } + } else { + const [givenNetworkControllerOptions, callback] = + args.length === 2 ? args : [{}, args[0]]; + const constructorOptions = { + ...buildDefaultNetworkControllerOptions(), + ...givenNetworkControllerOptions, + }; + const controller = new NetworkController(constructorOptions); + const providerConfig = controller.store.getState().provider; + assert(providerConfig.rpcUrl, 'rpcUrl must be set'); + const network = new CustomNetworkCommunications({ + customRpcUrl: providerConfig.rpcUrl, + }); + + try { + return await callback({ controller, network }); + } finally { + await controller.destroy(); + } } } @@ -7668,12 +7813,18 @@ async function withController(...args) { * stubbing `lookupNetwork` before the function and releasing the stub * afterward. * - * @param {object} args - The arguments. - * @param {NetworkController} args.controller - The network controller. - * @param {() => void | Promise} args.operation - The function that - * presumably involves `lookupNetwork`. + * @param args - The arguments. + * @param args.controller - The network controller. + * @param args.operation - The function that presumably involves + * `lookupNetwork`. */ -async function withoutCallingLookupNetwork({ controller, operation }) { +async function withoutCallingLookupNetwork({ + controller, + operation, +}: { + controller: NetworkController; + operation: () => void | Promise; +}) { const spy = jest .spyOn(controller, 'lookupNetwork') .mockResolvedValue(undefined); @@ -7689,18 +7840,21 @@ async function withoutCallingLookupNetwork({ controller, operation }) { * stubbing `getEIP1559Compatibility` before the function and releasing the stub * afterward. * - * @param {object} args - The arguments. - * @param {NetworkController} args.controller - The network controller. - * @param {() => void | Promise} args.operation - The function that - * presumably involves `getEIP1559Compatibility`. + * @param args - The arguments. + * @param args.controller - The network controller. + * @param args.operation - The function that presumably involves + * `getEIP1559Compatibility`. */ async function withoutCallingGetEIP1559Compatibility({ controller, operation, +}: { + controller: NetworkController; + operation: () => void | Promise; }) { const spy = jest .spyOn(controller, 'getEIP1559Compatibility') - .mockResolvedValue(undefined); + .mockResolvedValue(false); await operation(); spy.mockRestore(); } @@ -7711,18 +7865,18 @@ async function withoutCallingGetEIP1559Compatibility({ * occur after the function is called; or may be called standalone if you want * to assert that no state changes occurred. * - * @param {object} [args] - The arguments. - * @param {NetworkController} args.controller - The network controller. - * @param {string[]} [args.propertyPath] - The path of the property you - * expect the state changes to concern. - * @param {number | null} [args.count] - The number of events you expect to - * occur. If null, this function will wait until no events have occurred in - * `wait` number of milliseconds. Default: 1. - * @param {number} [args.duration] - The amount of time in milliseconds to - * wait for the expected number of filtered state changes to occur before - * resolving the promise that this function returns (default: 150). - * @param {() => void | Promise} [args.operation] - A function to run - * that will presumably produce the state changes in question. + * @param args - The arguments. + * @param args.controller - The network controller. + * @param args.propertyPath - The path of the property you expect the state + * changes to concern. + * @param args.count - The number of events you expect to occur. If null, this + * function will wait until no events have occurred in `wait` number of + * milliseconds. Default: 1. + * @param args.duration - The amount of time in milliseconds to wait for the + * expected number of filtered state changes to occur before resolving the + * promise that this function returns (default: 150). + * @param args.operation - A function to run that will presumably produce the + * state changes in question. * @returns A promise that resolves to an array of state objects (that is, the * contents of the store) when the specified number of filtered state changes * have occurred, or all of them if no number has been specified. @@ -7735,33 +7889,26 @@ async function waitForStateChanges({ operation = () => { // do nothing }, +}: { + controller: NetworkController; + propertyPath: string[]; + count?: number | null; + duration?: number; + operation?: () => void | Promise; }) { const initialState = { ...controller.store.getState() }; let isTimerRunning = false; - const getPropertyFrom = (state) => { - return propertyPath === undefined - ? state - : propertyPath.reduce((finalValue, part) => finalValue[part], state); - }; - - const isStateChangeInteresting = (newState, prevState) => { - return !isDeepStrictEqual( - getPropertyFrom(newState, propertyPath), - getPropertyFrom(prevState, propertyPath), - ); - }; - const promiseForStateChanges = new Promise((resolve, reject) => { // We need to declare this variable first, then assign it later, so that // ESLint won't complain that resetTimer is referring to this variable // before it's declared. And we need to use let so that we can assign it // below. /* eslint-disable-next-line prefer-const */ - let eventListener; - let timer; - const allStates = []; - const interestingStates = []; + let eventListener: (...args: any[]) => void; + let timer: NodeJS.Timeout | undefined; + const allStates: NetworkControllerState[] = []; + const interestingStates: NetworkControllerState[] = []; const stopTimer = () => { if (timer) { @@ -7824,6 +7971,7 @@ async function waitForStateChanges({ const isInteresting = isStateChangeInteresting( newState, allStates.length > 0 ? allStates[allStates.length - 1] : initialState, + propertyPath, ); allStates.push({ ...newState }); @@ -7850,24 +7998,22 @@ async function waitForStateChanges({ /** * Waits for controller events to be emitted before proceeding. * - * @param {object} options - An options bag. - * @param {ControllerMessenger} options.messenger - The messenger suited for - * NetworkController. - * @param {string} options.eventType - The type of NetworkController event you - * want to wait for. - * @param {number} options.count - The number of events you expect to occur - * (default: 1). - * @param {(payload: any) => boolean} options.filter - A function used to - * discard events that are not of interest. - * @param {number} options.wait - The amount of time in milliseconds to wait for - * the expected number of filtered events to occur before resolving the promise - * that this function returns (default: 150). - * @param {() => void | Promise} options.operation - A function to run - * that will presumably produce the events in question. - * @param {() => void | Promise} [options.beforeResolving] - In some - * tests, state updates happen so fast, we need to make an assertion immediately - * after the event in question occurs. However, if we wait until the promise - * this function returns resolves to do so, some other state update to the same + * @param args - The arguments to this function. + * @param args.messenger - The messenger suited for NetworkController. + * @param args.eventType - The type of NetworkController event you want to wait + * for. + * @param args.count - The number of events you expect to occur (default: 1). + * @param args.filter - A function used to discard events that are not of + * interest. + * @param args.wait - The amount of time in milliseconds to wait for the + * expected number of filtered events to occur before resolving the promise that + * this function returns (default: 150). + * @param args.operation - A function to run that will presumably produce the + * events in question. + * @param args.beforeResolving - In some tests, state updates happen so fast, we + * need to make an assertion immediately after the event in question occurs. + * However, if we wait until the promise this function returns resolves to do + * so, some other state update to the same * property may have happened. This option allows you to make an assertion * _before_ the promise resolves. This has the added benefit of allowing you to * maintain the "arrange, act, assert" ordering in your test, meaning that you @@ -7876,7 +8022,7 @@ async function waitForStateChanges({ * @returns A promise that resolves to the list of payloads for the set of * events, optionally filtered, when a specific number of them have occurred. */ -async function waitForPublishedEvents({ +async function waitForPublishedEvents({ messenger, eventType, count: expectedNumberOfEvents = 1, @@ -7888,72 +8034,80 @@ async function waitForPublishedEvents({ beforeResolving = async () => { // do nothing }, -}) { - const promiseForEventPayloads = new Promise((resolve, reject) => { - // We need to declare this variable first, then assign it later, so that - // ESLint won't complain that resetTimer is referring to this variable - // before it's declared. And we need to use let so that we can assign it - // below. - /* eslint-disable-next-line prefer-const */ - let eventListener; - let timer; - const allEventPayloads = []; - const interestingEventPayloads = []; - let alreadyEnded = false; +}: { + messenger: ControllerMessenger; + eventType: E['type']; + count?: number; + filter?: (payload: E['payload']) => boolean; + wait?: number; + operation?: () => void | Promise; + beforeResolving?: () => void | Promise; +}): Promise { + const promiseForEventPayloads = new Promise( + (resolve, reject) => { + let timer: NodeJS.Timeout | undefined; + const allEventPayloads: E['payload'][] = []; + const interestingEventPayloads: E['payload'][] = []; + let alreadyEnded = false; - const end = () => { - if (!alreadyEnded) { - alreadyEnded = true; - messenger.unsubscribe(eventType.toString(), eventListener); - Promise.resolve(beforeResolving()).then(() => { + // We're using `any` here because there seems to be some mismatch between + // the signature of `subscribe` and the way that we're using it. Try + // changing `any` to either `((...args: E['payload']) => void)` or + // `ExtractEventHandler` to see the issue. + const eventListener: any = (...payload: E['payload']) => { + allEventPayloads.push(payload); + + if (isEventPayloadInteresting(payload)) { + interestingEventPayloads.push(payload); if (interestingEventPayloads.length === expectedNumberOfEvents) { - resolve(interestingEventPayloads); + stopTimer(); + end(); } else { - // Using a string instead of an Error leads to better backtraces. - /* eslint-disable-next-line prefer-promise-reject-errors */ - reject( - `Expected to receive ${expectedNumberOfEvents} ${eventType} event(s), but received ${ - interestingEventPayloads.length - } after ${timeBeforeAssumingNoMoreEvents}ms.\n\nAll payloads:\n\n${inspect( - allEventPayloads, - { depth: null }, - )}`, - ); + resetTimer(); } - }); - } - }; + } + }; - const stopTimer = () => { - if (timer) { - clearTimeout(timer); - } - }; - - const resetTimer = () => { - stopTimer(); - timer = originalSetTimeout(() => { - end(); - }, timeBeforeAssumingNoMoreEvents); - }; - - eventListener = (...payload) => { - allEventPayloads.push(payload); - - if (isEventPayloadInteresting(payload)) { - interestingEventPayloads.push(payload); - if (interestingEventPayloads.length === expectedNumberOfEvents) { - stopTimer(); - end(); - } else { - resetTimer(); + function end() { + if (!alreadyEnded) { + alreadyEnded = true; + messenger.unsubscribe(eventType, eventListener); + Promise.resolve(beforeResolving()).then(() => { + if (interestingEventPayloads.length === expectedNumberOfEvents) { + resolve(interestingEventPayloads); + } else { + // Using a string instead of an Error leads to better backtraces. + /* eslint-disable-next-line prefer-promise-reject-errors */ + reject( + `Expected to receive ${expectedNumberOfEvents} ${eventType} event(s), but received ${ + interestingEventPayloads.length + } after ${timeBeforeAssumingNoMoreEvents}ms.\n\nAll payloads:\n\n${inspect( + allEventPayloads, + { depth: null }, + )}`, + ); + } + }); } } - }; - messenger.subscribe(eventType.toString(), eventListener); - resetTimer(); - }); + function stopTimer() { + if (timer) { + clearTimeout(timer); + } + } + + function resetTimer() { + stopTimer(); + timer = originalSetTimeout(() => { + end(); + }, timeBeforeAssumingNoMoreEvents); + } + + messenger.subscribe(eventType, eventListener); + resetTimer(); + }, + ); if (operation) { await operation(); @@ -7978,18 +8132,21 @@ async function waitForPublishedEvents({ * times this will happen, so this function does incur some time when it's used. * To speed up tests, you can pass `numberOfNetworkDetailsChanges`. * - * - * @param {object} args - The arguments. - * @param {NetworkController} args.controller - The network controller. - * @param {count} [args.numberOfNetworkDetailsChanges] - The number of times - * that `networkDetails` is expected to be updated. - * @param {() => void | Promise} [args.operation] - The function that - * presumably involves `lookupNetwork`. + * @param args - The arguments. + * @param args.controller - The network controller. + * @param args.numberOfNetworkDetailsChanges - The number of times that + * `networkDetails` is expected to be updated. + * @param args.operation - The function that presumably involves + * `lookupNetwork`. */ async function waitForLookupNetworkToComplete({ controller, numberOfNetworkDetailsChanges = null, operation, +}: { + controller: NetworkController; + numberOfNetworkDetailsChanges?: number | null; + operation: () => void | Promise; }) { await waitForStateChanges({ controller, @@ -7998,3 +8155,40 @@ async function waitForLookupNetworkToComplete({ count: numberOfNetworkDetailsChanges, }); } + +/** + * Returns whether two places in different state objects have different values. + * + * @param currentState - The current state object. + * @param prevState - The previous state object. + * @param propertyPath - A property path within both objects. + * @returns True or false, depending on the result. + */ +function isStateChangeInteresting( + currentState: Record, + prevState: Record, + propertyPath: PropertyKey[], +): boolean { + return !isDeepStrictEqual( + get(currentState, propertyPath), + get(prevState, propertyPath), + ); +} +/** + * `Object.getOwnPropertyNames()` is intentionally generic: it returns the own + * property names of an object, but it cannot make guarantees about the contents + * of that object, so the type of the names is merely `string[]`. While this is + * technically accurate, it is also unnecessary if we have an object that we've + * created and whose contents we know exactly. + * + * TODO: Move this to @metamask/utils + * + * @param object - The object. + * @returns The own property names of an object, typed according to the type of + * the object itself. + */ +function knownOwnKeysOf( + object: Partial>, +) { + return Object.getOwnPropertyNames(object) as K[]; +} diff --git a/app/scripts/controllers/network/network-controller.ts b/app/scripts/controllers/network/network-controller.ts index 9249e0fa2..74bd39f42 100644 --- a/app/scripts/controllers/network/network-controller.ts +++ b/app/scripts/controllers/network/network-controller.ts @@ -64,7 +64,7 @@ type Block = { * Primarily used to build the network client and check the availability of a * network. */ -type ProviderType = BuiltInInfuraNetwork | typeof NETWORK_TYPES.RPC; +export type ProviderType = BuiltInInfuraNetwork | typeof NETWORK_TYPES.RPC; /** * The network ID of a network. @@ -85,14 +85,14 @@ type ChainId = Hex; * The set of event types that NetworkController can publish via its messenger. */ export enum NetworkControllerEventType { - /** - * @see {@link NetworkControllerNetworkWillChangeEvent} - */ - NetworkWillChange = 'NetworkController:networkWillChange', /** * @see {@link NetworkControllerNetworkDidChangeEvent} */ NetworkDidChange = 'NetworkController:networkDidChange', + /** + * @see {@link NetworkControllerNetworkWillChangeEvent} + */ + NetworkWillChange = 'NetworkController:networkWillChange', /** * @see {@link NetworkControllerInfuraIsBlockedEvent} */ @@ -108,7 +108,7 @@ export enum NetworkControllerEventType { * switched, but the new provider has not been created and no state changes have * occurred yet. */ -type NetworkControllerNetworkWillChangeEvent = { +export type NetworkControllerNetworkWillChangeEvent = { type: NetworkControllerEventType.NetworkWillChange; payload: []; }; @@ -117,7 +117,7 @@ type NetworkControllerNetworkWillChangeEvent = { * `networkDidChange` is published after a provider has been created for a newly * switched network (but before the network has been confirmed to be available). */ -type NetworkControllerNetworkDidChangeEvent = { +export type NetworkControllerNetworkDidChangeEvent = { type: NetworkControllerEventType.NetworkDidChange; payload: []; }; @@ -127,7 +127,7 @@ type NetworkControllerNetworkDidChangeEvent = { * network, but when Infura returns an error blocking the user based on their * location. */ -type NetworkControllerInfuraIsBlockedEvent = { +export type NetworkControllerInfuraIsBlockedEvent = { type: NetworkControllerEventType.InfuraIsBlocked; payload: []; }; @@ -137,7 +137,7 @@ type NetworkControllerInfuraIsBlockedEvent = { * Infura network and Infura does not return an error blocking the user based on * their location, or the network is switched to a non-Infura network. */ -type NetworkControllerInfuraIsUnblockedEvent = { +export type NetworkControllerInfuraIsUnblockedEvent = { type: NetworkControllerEventType.InfuraIsUnblocked; payload: []; }; @@ -145,7 +145,7 @@ type NetworkControllerInfuraIsUnblockedEvent = { /** * The set of events that the NetworkController messenger can publish. */ -type NetworkControllerEvent = +export type NetworkControllerEvent = | NetworkControllerNetworkDidChangeEvent | NetworkControllerNetworkWillChangeEvent | NetworkControllerInfuraIsBlockedEvent @@ -154,7 +154,7 @@ type NetworkControllerEvent = /** * The messenger that the NetworkController uses to publish events. */ -type NetworkControllerMessenger = RestrictedControllerMessenger< +export type NetworkControllerMessenger = RestrictedControllerMessenger< typeof name, never, NetworkControllerEvent, @@ -167,7 +167,7 @@ type NetworkControllerMessenger = RestrictedControllerMessenger< * network. Currently has overlap with `NetworkConfiguration`, although the * two will be merged down the road. */ -type ProviderConfiguration = { +export type ProviderConfiguration = { /** * Either a type of Infura network, "localhost" for a locally operated * network, or "rpc" for everything else. @@ -213,6 +213,7 @@ type NetworkDetails = { EIPS: { [eipNumber: number]: boolean | undefined; }; + [otherProperty: string]: unknown; }; /** @@ -264,7 +265,7 @@ type NetworkConfigurations = Record< /** * The state that NetworkController holds after combining its individual stores. */ -type CompositeState = { +export type NetworkControllerState = { provider: ProviderConfiguration; previousProviderStore: ProviderConfiguration; networkId: NetworkIdState; @@ -276,7 +277,7 @@ type CompositeState = { /** * The options that NetworkController takes. */ -type NetworkControllerOptions = { +export type NetworkControllerOptions = { messenger: NetworkControllerMessenger; state?: { provider?: ProviderConfiguration; @@ -450,7 +451,7 @@ export class NetworkController extends EventEmitter { * Observable store containing a combination of data from all of the * individual stores. */ - store: ComposedStore; + store: ComposedStore; _provider: SafeEventEmitterProvider | null; @@ -508,7 +509,7 @@ export class NetworkController extends EventEmitter { state.networkConfigurations || buildDefaultNetworkConfigurationsState(), ); - this.store = new ComposedStore({ + this.store = new ComposedStore({ provider: this.providerStore, previousProviderStore: this.previousProviderStore, networkId: this.networkIdStore, diff --git a/jest.config.js b/jest.config.js index 21d41a96d..86588a42a 100644 --- a/jest.config.js +++ b/jest.config.js @@ -2,6 +2,7 @@ module.exports = { collectCoverageFrom: [ '/app/scripts/constants/error-utils.js', '/app/scripts/controllers/network/**/*.js', + '/app/scripts/controllers/network/**/*.ts', '/app/scripts/controllers/permissions/**/*.js', '/app/scripts/controllers/sign.ts', '/app/scripts/flask/**/*.js', @@ -39,6 +40,7 @@ module.exports = { '/app/scripts/constants/error-utils.test.js', '/app/scripts/controllers/app-state.test.js', '/app/scripts/controllers/network/**/*.test.js', + '/app/scripts/controllers/network/**/*.test.ts', '/app/scripts/controllers/permissions/**/*.test.js', '/app/scripts/controllers/sign.test.ts', '/app/scripts/flask/**/*.test.js', diff --git a/package.json b/package.json index c5a718e75..3b6b5f057 100644 --- a/package.json +++ b/package.json @@ -419,6 +419,7 @@ "@types/react-dom": "^17.0.11", "@types/react-redux": "^7.1.25", "@types/remote-redux-devtools": "^0.5.5", + "@types/sinon": "^10.0.13", "@types/w3c-web-hid": "^1.0.3", "@types/watchify": "^3.11.1", "@types/yargs": "^17.0.8", diff --git a/yarn.lock b/yarn.lock index f31c8b428..475744a26 100644 --- a/yarn.lock +++ b/yarn.lock @@ -7726,6 +7726,22 @@ __metadata: languageName: node linkType: hard +"@types/sinon@npm:^10.0.13": + version: 10.0.13 + resolution: "@types/sinon@npm:10.0.13" + dependencies: + "@types/sinonjs__fake-timers": "*" + checksum: 46a14c888db50f0098ec53d451877e0111d878ec4a653b9e9ed7f8e54de386d6beb0e528ddc3e95cd3361a8ab9ad54e4cca33cd88d45b9227b83e9fc8fb6688a + languageName: node + linkType: hard + +"@types/sinonjs__fake-timers@npm:*": + version: 8.1.2 + resolution: "@types/sinonjs__fake-timers@npm:8.1.2" + checksum: bbc73a5ab6c0ec974929392f3d6e1e8db4ebad97ec506d785301e1c3d8a4f98a35b1aa95b97035daef02886fd8efd7788a2fa3ced2ec7105988bfd8dce61eedd + languageName: node + linkType: hard + "@types/source-list-map@npm:*": version: 0.1.2 resolution: "@types/source-list-map@npm:0.1.2" @@ -24308,6 +24324,7 @@ __metadata: "@types/react-dom": ^17.0.11 "@types/react-redux": ^7.1.25 "@types/remote-redux-devtools": ^0.5.5 + "@types/sinon": ^10.0.13 "@types/w3c-web-hid": ^1.0.3 "@types/watchify": ^3.11.1 "@types/yargs": ^17.0.8 From 6ed72d69348c803d1aaa670b7b9265a698e0b771 Mon Sep 17 00:00:00 2001 From: Bernardo Garces Chapero Date: Thu, 13 Apr 2023 09:24:59 +0100 Subject: [PATCH 16/36] Refactor eth_getEncryptionPublicKey handling (#18319) * add EncryptionPublicKeyController * update message-managers package --- app/scripts/background.js | 20 +- .../controllers/encryption-public-key.test.ts | 400 +++++++++++++++++ .../controllers/encryption-public-key.ts | 421 ++++++++++++++++++ app/scripts/controllers/sign.ts | 1 - .../lib/encryption-public-key-manager.js | 318 ------------- app/scripts/metamask-controller.js | 143 ++---- package.json | 2 +- types/eth-keyring-controller.d.ts | 6 + ui/selectors/selectors.js | 8 +- yarn.lock | 10 +- 10 files changed, 871 insertions(+), 458 deletions(-) create mode 100644 app/scripts/controllers/encryption-public-key.test.ts create mode 100644 app/scripts/controllers/encryption-public-key.ts delete mode 100644 app/scripts/lib/encryption-public-key-manager.js diff --git a/app/scripts/background.js b/app/scripts/background.js index 3aff0170b..622e888f9 100644 --- a/app/scripts/background.js +++ b/app/scripts/background.js @@ -687,7 +687,7 @@ export function setupController( METAMASK_CONTROLLER_EVENTS.UPDATE_BADGE, updateBadge, ); - controller.encryptionPublicKeyManager.on( + controller.encryptionPublicKeyController.hub.on( METAMASK_CONTROLLER_EVENTS.UPDATE_BADGE, updateBadge, ); @@ -727,17 +727,12 @@ export function setupController( function getUnapprovedTransactionCount() { const { unapprovedDecryptMsgCount } = controller.decryptMessageManager; - const { unapprovedEncryptionPublicKeyMsgCount } = - controller.encryptionPublicKeyManager; const pendingApprovalCount = controller.approvalController.getTotalApprovalCount(); const waitingForUnlockCount = controller.appStateController.waitingForUnlock.length; return ( - unapprovedDecryptMsgCount + - unapprovedEncryptionPublicKeyMsgCount + - pendingApprovalCount + - waitingForUnlockCount + unapprovedDecryptMsgCount + pendingApprovalCount + waitingForUnlockCount ); } @@ -767,14 +762,9 @@ export function setupController( REJECT_NOTIFICATION_CLOSE, ), ); - controller.encryptionPublicKeyManager.messages - .filter((msg) => msg.status === 'unapproved') - .forEach((tx) => - controller.encryptionPublicKeyManager.rejectMsg( - tx.id, - REJECT_NOTIFICATION_CLOSE, - ), - ); + controller.encryptionPublicKeyController.rejectUnapproved( + REJECT_NOTIFICATION_CLOSE, + ); // Finally, resolve snap dialog approvals on Flask and reject all the others managed by the ApprovalController. Object.values(controller.approvalController.state.pendingApprovals).forEach( diff --git a/app/scripts/controllers/encryption-public-key.test.ts b/app/scripts/controllers/encryption-public-key.test.ts new file mode 100644 index 000000000..cc5b61cc1 --- /dev/null +++ b/app/scripts/controllers/encryption-public-key.test.ts @@ -0,0 +1,400 @@ +import { EncryptionPublicKeyManager } from '@metamask/message-manager'; +import { + AbstractMessage, + OriginalRequest, +} from '@metamask/message-manager/dist/AbstractMessageManager'; +import { KeyringType } from '../../../shared/constants/keyring'; +import { MetaMetricsEventCategory } from '../../../shared/constants/metametrics'; +import EncryptionPublicKeyController, { + EncryptionPublicKeyControllerMessenger, + EncryptionPublicKeyControllerOptions, +} from './encryption-public-key'; + +jest.mock('@metamask/message-manager', () => ({ + EncryptionPublicKeyManager: jest.fn(), +})); + +const messageIdMock = '123'; +const messageIdMock2 = '456'; +const stateMock = { test: 123 }; +const addressMock = '0xc38bf1ad06ef69f0c04e29dbeb4152b4175f0a8d'; +const publicKeyMock = '32762347862378feb87123781623a='; +const keyringMock = { type: KeyringType.hdKeyTree }; + +const messageParamsMock = { + from: addressMock, + origin: 'http://test.com', + data: addressMock, + metamaskId: messageIdMock, +}; + +const messageMock = { + id: messageIdMock, + time: 123, + status: 'unapproved', + type: 'testType', + rawSig: undefined, +} as any as AbstractMessage; + +const coreMessageMock = { + ...messageMock, + messageParams: messageParamsMock, +}; + +const stateMessageMock = { + ...messageMock, + msgParams: addressMock, + origin: messageParamsMock.origin, +}; + +const requestMock = { + origin: 'http://test2.com', +} as OriginalRequest; + +const createMessengerMock = () => + ({ + registerActionHandler: jest.fn(), + publish: jest.fn(), + call: jest.fn(), + } as any as jest.Mocked); + +const createEncryptionPublicKeyManagerMock = () => + ({ + getUnapprovedMessages: jest.fn(), + getUnapprovedMessagesCount: jest.fn(), + addUnapprovedMessageAsync: jest.fn(), + approveMessage: jest.fn(), + setMessageStatusAndResult: jest.fn(), + rejectMessage: jest.fn(), + subscribe: jest.fn(), + update: jest.fn(), + hub: { + on: jest.fn(), + }, + } as any as jest.Mocked); + +const createKeyringControllerMock = () => ({ + getKeyringForAccount: jest.fn(), + getEncryptionPublicKey: jest.fn(), +}); + +describe('EncryptionPublicKeyController', () => { + let encryptionPublicKeyController: EncryptionPublicKeyController; + + const encryptionPublicKeyManagerConstructorMock = + EncryptionPublicKeyManager as jest.MockedClass< + typeof EncryptionPublicKeyManager + >; + const encryptionPublicKeyManagerMock = + createEncryptionPublicKeyManagerMock(); + const messengerMock = createMessengerMock(); + const keyringControllerMock = createKeyringControllerMock(); + const getStateMock = jest.fn(); + const metricsEventMock = jest.fn(); + + beforeEach(() => { + jest.resetAllMocks(); + + encryptionPublicKeyManagerConstructorMock.mockReturnValue( + encryptionPublicKeyManagerMock, + ); + + encryptionPublicKeyController = new EncryptionPublicKeyController({ + messenger: messengerMock as any, + keyringController: keyringControllerMock as any, + getState: getStateMock as any, + metricsEvent: metricsEventMock as any, + } as EncryptionPublicKeyControllerOptions); + }); + + describe('unapprovedMsgCount', () => { + it('returns value from message manager getter', () => { + encryptionPublicKeyManagerMock.getUnapprovedMessagesCount.mockReturnValueOnce( + 10, + ); + expect(encryptionPublicKeyController.unapprovedMsgCount).toBe(10); + }); + }); + + describe('resetState', () => { + it('sets state to initial state', () => { + // eslint-disable-next-line @typescript-eslint/ban-ts-comment + // @ts-ignore + encryptionPublicKeyController.update(() => ({ + unapprovedEncryptionPublicKeyMsgs: { + [messageIdMock]: messageMock, + } as any, + unapprovedEncryptionPublicKeyMsgCount: 1, + })); + + encryptionPublicKeyController.resetState(); + + expect(encryptionPublicKeyController.state).toEqual({ + unapprovedEncryptionPublicKeyMsgs: {}, + unapprovedEncryptionPublicKeyMsgCount: 0, + }); + }); + }); + + describe('rejectUnapproved', () => { + beforeEach(() => { + const messages = { + [messageIdMock]: messageMock, + [messageIdMock2]: messageMock, + }; + encryptionPublicKeyManagerMock.getUnapprovedMessages.mockReturnValueOnce( + messages as any, + ); + // eslint-disable-next-line @typescript-eslint/ban-ts-comment + // @ts-ignore + encryptionPublicKeyController.update(() => ({ + unapprovedEncryptionPublicKeyMsgs: messages as any, + })); + }); + + it('rejects all messages in the message manager', () => { + encryptionPublicKeyController.rejectUnapproved('Test Reason'); + expect( + encryptionPublicKeyManagerMock.rejectMessage, + ).toHaveBeenCalledTimes(2); + expect(encryptionPublicKeyManagerMock.rejectMessage).toHaveBeenCalledWith( + messageIdMock, + ); + expect(encryptionPublicKeyManagerMock.rejectMessage).toHaveBeenCalledWith( + messageIdMock2, + ); + }); + + it('fires metrics event with reject reason', () => { + encryptionPublicKeyController.rejectUnapproved('Test Reason'); + expect(metricsEventMock).toHaveBeenCalledTimes(2); + expect(metricsEventMock).toHaveBeenLastCalledWith({ + event: 'Test Reason', + category: MetaMetricsEventCategory.Messages, + properties: { + action: 'Encryption public key Request', + }, + }); + }); + }); + + describe('clearUnapproved', () => { + it('resets state in all message managers', () => { + encryptionPublicKeyController.clearUnapproved(); + + const defaultState = { + unapprovedMessages: {}, + unapprovedMessagesCount: 0, + }; + + expect(encryptionPublicKeyManagerMock.update).toHaveBeenCalledTimes(1); + expect(encryptionPublicKeyManagerMock.update).toHaveBeenCalledWith( + defaultState, + ); + }); + }); + + describe('newRequestEncryptionPublicKey', () => { + it.each([ + ['Ledger', KeyringType.ledger], + ['Trezor', KeyringType.trezor], + ['Lattice', KeyringType.lattice], + ['QR hardware', KeyringType.qr], + ])( + 'throws if keyring is not supported', + async (keyringName, keyringType) => { + keyringControllerMock.getKeyringForAccount.mockResolvedValueOnce({ + type: keyringType, + }); + + await expect( + encryptionPublicKeyController.newRequestEncryptionPublicKey( + addressMock, + requestMock, + ), + ).rejects.toThrowError( + `${keyringName} does not support eth_getEncryptionPublicKey.`, + ); + }, + ); + + it('adds message to message manager', async () => { + keyringControllerMock.getKeyringForAccount.mockResolvedValueOnce( + keyringMock, + ); + + await encryptionPublicKeyController.newRequestEncryptionPublicKey( + addressMock, + requestMock, + ); + + expect( + encryptionPublicKeyManagerMock.addUnapprovedMessageAsync, + ).toHaveBeenCalledTimes(1); + expect( + encryptionPublicKeyManagerMock.addUnapprovedMessageAsync, + ).toHaveBeenCalledWith({ from: addressMock }, requestMock); + }); + }); + + describe('encryptionPublicKey', () => { + beforeEach(() => { + encryptionPublicKeyManagerMock.approveMessage.mockResolvedValueOnce({ + from: messageParamsMock.data, + }); + + keyringControllerMock.getEncryptionPublicKey.mockResolvedValueOnce( + publicKeyMock, + ); + }); + + it('approves message and signs', async () => { + await encryptionPublicKeyController.encryptionPublicKey( + messageParamsMock, + ); + + expect( + keyringControllerMock.getEncryptionPublicKey, + ).toHaveBeenCalledTimes(1); + expect(keyringControllerMock.getEncryptionPublicKey).toHaveBeenCalledWith( + messageParamsMock.data, + ); + + expect( + encryptionPublicKeyManagerMock.setMessageStatusAndResult, + ).toHaveBeenCalledTimes(1); + expect( + encryptionPublicKeyManagerMock.setMessageStatusAndResult, + ).toHaveBeenCalledWith( + messageParamsMock.metamaskId, + publicKeyMock, + 'received', + ); + }); + + it('returns current state', async () => { + getStateMock.mockReturnValueOnce(stateMock); + expect( + await encryptionPublicKeyController.encryptionPublicKey( + messageParamsMock, + ), + ).toEqual(stateMock); + }); + + it('accepts approval', async () => { + await encryptionPublicKeyController.encryptionPublicKey( + messageParamsMock, + ); + + expect(messengerMock.call).toHaveBeenCalledTimes(1); + expect(messengerMock.call).toHaveBeenCalledWith( + 'ApprovalController:acceptRequest', + messageParamsMock.metamaskId, + ); + }); + + it('rejects message on error', async () => { + keyringControllerMock.getEncryptionPublicKey.mockReset(); + keyringControllerMock.getEncryptionPublicKey.mockRejectedValue( + new Error('Test Error'), + ); + + await expect( + encryptionPublicKeyController.encryptionPublicKey(messageParamsMock), + ).rejects.toThrow('Test Error'); + + expect( + encryptionPublicKeyManagerMock.rejectMessage, + ).toHaveBeenCalledTimes(1); + expect(encryptionPublicKeyManagerMock.rejectMessage).toHaveBeenCalledWith( + messageParamsMock.metamaskId, + ); + }); + + it('rejects approval on error', async () => { + keyringControllerMock.getEncryptionPublicKey.mockReset(); + keyringControllerMock.getEncryptionPublicKey.mockRejectedValue( + new Error('Test Error'), + ); + + await expect( + encryptionPublicKeyController.encryptionPublicKey(messageParamsMock), + ).rejects.toThrow('Test Error'); + + expect(messengerMock.call).toHaveBeenCalledTimes(1); + expect(messengerMock.call).toHaveBeenCalledWith( + 'ApprovalController:rejectRequest', + messageParamsMock.metamaskId, + 'Cancel', + ); + }); + }); + + describe('cancelEncryptionPublicKey', () => { + it('rejects message using message manager', async () => { + encryptionPublicKeyController.cancelEncryptionPublicKey(messageIdMock); + + expect( + encryptionPublicKeyManagerMock.rejectMessage, + ).toHaveBeenCalledTimes(1); + expect(encryptionPublicKeyManagerMock.rejectMessage).toHaveBeenCalledWith( + messageParamsMock.metamaskId, + ); + }); + + it('rejects approval using approval controller', async () => { + encryptionPublicKeyController.cancelEncryptionPublicKey(messageIdMock); + + expect(messengerMock.call).toHaveBeenCalledTimes(1); + expect(messengerMock.call).toHaveBeenCalledWith( + 'ApprovalController:rejectRequest', + messageParamsMock.metamaskId, + 'Cancel', + ); + }); + }); + + describe('message manager events', () => { + it('bubbles update badge event from EncryptionPublicKeyManager', () => { + const mockListener = jest.fn(); + + encryptionPublicKeyController.hub.on('updateBadge', mockListener); + (encryptionPublicKeyManagerMock.hub.on as any).mock.calls[0][1](); + + expect(mockListener).toHaveBeenCalledTimes(1); + }); + + it('requires approval on unapproved message event from EncryptionPublicKeyManager', () => { + messengerMock.call.mockResolvedValueOnce({}); + + (encryptionPublicKeyManagerMock.hub.on as any).mock.calls[1][1]( + messageParamsMock, + ); + + expect(messengerMock.call).toHaveBeenCalledTimes(1); + expect(messengerMock.call).toHaveBeenCalledWith( + 'ApprovalController:addRequest', + { + id: messageIdMock, + origin: messageParamsMock.origin, + type: 'eth_getEncryptionPublicKey', + }, + true, + ); + }); + + it('updates state on EncryptionPublicKeyManager state change', async () => { + await encryptionPublicKeyManagerMock.subscribe.mock.calls[0][0]({ + unapprovedMessages: { [messageIdMock]: coreMessageMock as any }, + unapprovedMessagesCount: 3, + }); + + expect(encryptionPublicKeyController.state).toEqual({ + unapprovedEncryptionPublicKeyMsgs: { + [messageIdMock]: stateMessageMock as any, + }, + unapprovedEncryptionPublicKeyMsgCount: 3, + }); + }); + }); +}); diff --git a/app/scripts/controllers/encryption-public-key.ts b/app/scripts/controllers/encryption-public-key.ts new file mode 100644 index 000000000..f4cb5e25e --- /dev/null +++ b/app/scripts/controllers/encryption-public-key.ts @@ -0,0 +1,421 @@ +import EventEmitter from 'events'; +import log from 'loglevel'; +import { + EncryptionPublicKeyManager, + EncryptionPublicKeyParamsMetamask, +} from '@metamask/message-manager'; +import { KeyringController } from '@metamask/eth-keyring-controller'; +import { + AbstractMessageManager, + AbstractMessage, + MessageManagerState, + AbstractMessageParams, + AbstractMessageParamsMetamask, + OriginalRequest, +} from '@metamask/message-manager/dist/AbstractMessageManager'; +import { + BaseControllerV2, + RestrictedControllerMessenger, +} from '@metamask/base-controller'; +import { Patch } from 'immer'; +import { + AcceptRequest, + AddApprovalRequest, + RejectRequest, +} from '@metamask/approval-controller'; +import { MetaMetricsEventCategory } from '../../../shared/constants/metametrics'; +import { KeyringType } from '../../../shared/constants/keyring'; +import { ORIGIN_METAMASK } from '../../../shared/constants/app'; + +const controllerName = 'EncryptionPublicKeyController'; +const methodNameGetEncryptionPublicKey = 'eth_getEncryptionPublicKey'; + +const stateMetadata = { + unapprovedEncryptionPublicKeyMsgs: { persist: false, anonymous: false }, + unapprovedEncryptionPublicKeyMsgCount: { persist: false, anonymous: false }, +}; + +const getDefaultState = () => ({ + unapprovedEncryptionPublicKeyMsgs: {}, + unapprovedEncryptionPublicKeyMsgCount: 0, +}); + +export type CoreMessage = AbstractMessage & { + messageParams: AbstractMessageParams; +}; + +export type StateMessage = Required< + Omit +> & { + msgParams: string; +}; + +export type EncryptionPublicKeyControllerState = { + unapprovedEncryptionPublicKeyMsgs: Record; + unapprovedEncryptionPublicKeyMsgCount: number; +}; + +export type GetEncryptionPublicKeyState = { + type: `${typeof controllerName}:getState`; + handler: () => EncryptionPublicKeyControllerState; +}; + +export type EncryptionPublicKeyStateChange = { + type: `${typeof controllerName}:stateChange`; + payload: [EncryptionPublicKeyControllerState, Patch[]]; +}; + +export type EncryptionPublicKeyControllerActions = GetEncryptionPublicKeyState; + +export type EncryptionPublicKeyControllerEvents = + EncryptionPublicKeyStateChange; + +type AllowedActions = AddApprovalRequest | AcceptRequest | RejectRequest; + +export type EncryptionPublicKeyControllerMessenger = + RestrictedControllerMessenger< + typeof controllerName, + EncryptionPublicKeyControllerActions | AllowedActions, + EncryptionPublicKeyControllerEvents, + AllowedActions['type'], + never + >; + +export type EncryptionPublicKeyControllerOptions = { + messenger: EncryptionPublicKeyControllerMessenger; + keyringController: KeyringController; + getState: () => any; + metricsEvent: (payload: any, options?: any) => void; +}; + +/** + * Controller for requesting encryption public key requests requiring user approval. + */ +export default class EncryptionPublicKeyController extends BaseControllerV2< + typeof controllerName, + EncryptionPublicKeyControllerState, + EncryptionPublicKeyControllerMessenger +> { + hub: EventEmitter; + + private _keyringController: KeyringController; + + private _getState: () => any; + + private _encryptionPublicKeyManager: EncryptionPublicKeyManager; + + private _metricsEvent: (payload: any, options?: any) => void; + + /** + * Construct a EncryptionPublicKey controller. + * + * @param options - The controller options. + * @param options.messenger - The restricted controller messenger for the EncryptionPublicKey controller. + * @param options.keyringController - An instance of a keyring controller used to extract the encryption public key. + * @param options.getState - Callback to retrieve all user state. + * @param options.metricsEvent - A function for emitting a metric event. + */ + constructor({ + messenger, + keyringController, + getState, + metricsEvent, + }: EncryptionPublicKeyControllerOptions) { + super({ + name: controllerName, + metadata: stateMetadata, + messenger, + state: getDefaultState(), + }); + + this._keyringController = keyringController; + this._getState = getState; + this._metricsEvent = metricsEvent; + + this.hub = new EventEmitter(); + this._encryptionPublicKeyManager = new EncryptionPublicKeyManager( + undefined, + undefined, + undefined, + ['received'], + ); + + this._encryptionPublicKeyManager.hub.on('updateBadge', () => { + this.hub.emit('updateBadge'); + }); + + this._encryptionPublicKeyManager.hub.on( + 'unapprovedMessage', + (msgParams: AbstractMessageParamsMetamask) => { + this._requestApproval(msgParams, methodNameGetEncryptionPublicKey); + }, + ); + + this._subscribeToMessageState( + this._encryptionPublicKeyManager, + (state, newMessages, messageCount) => { + state.unapprovedEncryptionPublicKeyMsgs = newMessages; + state.unapprovedEncryptionPublicKeyMsgCount = messageCount; + }, + ); + } + + /** + * A getter for the number of 'unapproved' Messages in this.messages + * + * @returns The number of 'unapproved' Messages in this.messages + */ + get unapprovedMsgCount(): number { + return this._encryptionPublicKeyManager.getUnapprovedMessagesCount(); + } + + /** + * Reset the controller state to the initial state. + */ + resetState() { + this.update(() => getDefaultState()); + } + + /** + * Called when a Dapp uses the eth_getEncryptionPublicKey method, to request user approval. + * + * @param address - The address from the encryption public key will be extracted. + * @param [req] - The original request, containing the origin. + */ + async newRequestEncryptionPublicKey( + address: string, + req: OriginalRequest, + ): Promise { + const keyring = await this._keyringController.getKeyringForAccount(address); + + switch (keyring.type) { + case KeyringType.ledger: { + return new Promise((_, reject) => { + reject( + new Error('Ledger does not support eth_getEncryptionPublicKey.'), + ); + }); + } + + case KeyringType.trezor: { + return new Promise((_, reject) => { + reject( + new Error('Trezor does not support eth_getEncryptionPublicKey.'), + ); + }); + } + + case KeyringType.lattice: { + return new Promise((_, reject) => { + reject( + new Error('Lattice does not support eth_getEncryptionPublicKey.'), + ); + }); + } + + case KeyringType.qr: { + return Promise.reject( + new Error('QR hardware does not support eth_getEncryptionPublicKey.'), + ); + } + + default: { + return this._encryptionPublicKeyManager.addUnapprovedMessageAsync( + { from: address }, + req, + ); + } + } + } + + /** + * Signifies a user's approval to receiving encryption public key in queue. + * + * @param msgParams - The params of the message to receive & return to the Dapp. + * @returns A full state update. + */ + async encryptionPublicKey(msgParams: EncryptionPublicKeyParamsMetamask) { + log.info('MetaMaskController - encryptionPublicKey'); + const messageId = msgParams.metamaskId as string; + // sets the status op the message to 'approved' + // and removes the metamaskId for decryption + try { + const cleanMessageParams = + await this._encryptionPublicKeyManager.approveMessage(msgParams); + + // EncryptionPublicKey message + const publicKey = await this._keyringController.getEncryptionPublicKey( + cleanMessageParams.from, + ); + + // tells the listener that the message has been processed + // and can be returned to the dapp + this._encryptionPublicKeyManager.setMessageStatusAndResult( + messageId, + publicKey, + 'received', + ); + + this._acceptApproval(messageId); + + return this._getState(); + } catch (error) { + log.info( + 'MetaMaskController - eth_getEncryptionPublicKey failed.', + error, + ); + this._cancelAbstractMessage(this._encryptionPublicKeyManager, messageId); + throw error; + } + } + + /** + * Used to cancel a message submitted via eth_getEncryptionPublicKey. + * + * @param msgId - The id of the message to cancel. + */ + cancelEncryptionPublicKey(msgId: string) { + this._cancelAbstractMessage(this._encryptionPublicKeyManager, msgId); + } + + /** + * Reject all unapproved messages of any type. + * + * @param reason - A message to indicate why. + */ + rejectUnapproved(reason?: string) { + Object.keys( + this._encryptionPublicKeyManager.getUnapprovedMessages(), + ).forEach((messageId) => { + this._cancelAbstractMessage( + this._encryptionPublicKeyManager, + messageId, + reason, + ); + }); + } + + /** + * Clears all unapproved messages from memory. + */ + clearUnapproved() { + this._encryptionPublicKeyManager.update({ + unapprovedMessages: {}, + unapprovedMessagesCount: 0, + }); + } + + private _cancelAbstractMessage( + messageManager: AbstractMessageManager< + AbstractMessage, + AbstractMessageParams, + AbstractMessageParamsMetamask + >, + messageId: string, + reason?: string, + ) { + if (reason) { + this._metricsEvent({ + event: reason, + category: MetaMetricsEventCategory.Messages, + properties: { + action: 'Encryption public key Request', + }, + }); + } + + messageManager.rejectMessage(messageId); + this._rejectApproval(messageId); + + return this._getState(); + } + + private _subscribeToMessageState( + messageManager: AbstractMessageManager< + AbstractMessage, + AbstractMessageParams, + AbstractMessageParamsMetamask + >, + updateState: ( + state: EncryptionPublicKeyControllerState, + newMessages: Record, + messageCount: number, + ) => void, + ) { + messageManager.subscribe( + async (state: MessageManagerState) => { + const newMessages = await this._migrateMessages( + state.unapprovedMessages as any, + ); + this.update((draftState) => { + updateState(draftState, newMessages, state.unapprovedMessagesCount); + }); + }, + ); + } + + private async _migrateMessages( + coreMessages: Record, + ): Promise> { + const stateMessages: Record = {}; + + for (const messageId of Object.keys(coreMessages)) { + const coreMessage = coreMessages[messageId]; + const stateMessage = await this._migrateMessage(coreMessage); + + stateMessages[messageId] = stateMessage; + } + + return stateMessages; + } + + private async _migrateMessage( + coreMessage: CoreMessage, + ): Promise { + const { messageParams, ...coreMessageData } = coreMessage; + + // Core message managers use messageParams but frontend uses msgParams with lots of references + const stateMessage = { + ...coreMessageData, + rawSig: coreMessage.rawSig as string, + msgParams: messageParams.from, + origin: messageParams.origin, + }; + + return stateMessage; + } + + private _requestApproval( + msgParams: AbstractMessageParamsMetamask, + type: string, + ) { + const id = msgParams.metamaskId as string; + const origin = msgParams.origin || ORIGIN_METAMASK; + + this.messagingSystem + .call( + 'ApprovalController:addRequest', + { + id, + origin, + type, + }, + true, + ) + .catch(() => { + // Intentionally ignored as promise not currently used + }); + } + + private _acceptApproval(messageId: string) { + this.messagingSystem.call('ApprovalController:acceptRequest', messageId); + } + + private _rejectApproval(messageId: string) { + this.messagingSystem.call( + 'ApprovalController:rejectRequest', + messageId, + 'Cancel', + ); + } +} diff --git a/app/scripts/controllers/sign.ts b/app/scripts/controllers/sign.ts index e04d70c09..1712ed1ee 100644 --- a/app/scripts/controllers/sign.ts +++ b/app/scripts/controllers/sign.ts @@ -104,7 +104,6 @@ export type SignControllerOptions = { messenger: SignControllerMessenger; keyringController: KeyringController; preferencesController: PreferencesController; - sendUpdate: () => void; getState: () => any; metricsEvent: (payload: any, options?: any) => void; securityProviderRequest: ( diff --git a/app/scripts/lib/encryption-public-key-manager.js b/app/scripts/lib/encryption-public-key-manager.js deleted file mode 100644 index 9791e0378..000000000 --- a/app/scripts/lib/encryption-public-key-manager.js +++ /dev/null @@ -1,318 +0,0 @@ -import EventEmitter from 'events'; -import { ObservableStore } from '@metamask/obs-store'; -import { ethErrors } from 'eth-rpc-errors'; -import log from 'loglevel'; -import { MESSAGE_TYPE } from '../../../shared/constants/app'; -import { MetaMetricsEventCategory } from '../../../shared/constants/metametrics'; -import { METAMASK_CONTROLLER_EVENTS } from '../metamask-controller'; -import createId from '../../../shared/modules/random-id'; - -/** - * Represents, and contains data about, an 'eth_getEncryptionPublicKey' type request. These are created when - * an eth_getEncryptionPublicKey call is requested. - * - * @typedef {object} EncryptionPublicKey - * @property {number} id An id to track and identify the message object - * @property {object} msgParams The parameters to pass to the encryptionPublicKey method once the request is - * approved. - * @property {object} msgParams.metamaskId Added to msgParams for tracking and identification within MetaMask. - * @property {string} msgParams.data A hex string conversion of the raw buffer data of the request - * @property {number} time The epoch time at which the this message was created - * @property {string} status Indicates whether the request is 'unapproved', 'approved', 'received' or 'rejected' - * @property {string} type The json-prc method for which a request has been made. A 'Message' will - * always have a 'eth_getEncryptionPublicKey' type. - */ - -export default class EncryptionPublicKeyManager extends EventEmitter { - /** - * Controller in charge of managing - storing, adding, removing, updating - EncryptionPublicKey. - * - * @param {object} opts - Controller options - * @param {Function} opts.metricEvent - A function for emitting a metric event. - */ - constructor(opts) { - super(); - this.memStore = new ObservableStore({ - unapprovedEncryptionPublicKeyMsgs: {}, - unapprovedEncryptionPublicKeyMsgCount: 0, - }); - - this.resetState = () => { - this.memStore.updateState({ - unapprovedEncryptionPublicKeyMsgs: {}, - unapprovedEncryptionPublicKeyMsgCount: 0, - }); - }; - - this.messages = []; - this.metricsEvent = opts.metricsEvent; - } - - /** - * A getter for the number of 'unapproved' EncryptionPublicKeys in this.messages - * - * @returns {number} The number of 'unapproved' EncryptionPublicKeys in this.messages - */ - get unapprovedEncryptionPublicKeyMsgCount() { - return Object.keys(this.getUnapprovedMsgs()).length; - } - - /** - * A getter for the 'unapproved' EncryptionPublicKeys in this.messages - * - * @returns {object} An index of EncryptionPublicKey ids to EncryptionPublicKeys, for all 'unapproved' EncryptionPublicKeys in - * this.messages - */ - getUnapprovedMsgs() { - return this.messages - .filter((msg) => msg.status === 'unapproved') - .reduce((result, msg) => { - result[msg.id] = msg; - return result; - }, {}); - } - - /** - * Creates a new EncryptionPublicKey with an 'unapproved' status using the passed msgParams. this.addMsg is called to add - * the new EncryptionPublicKey to this.messages, and to save the unapproved EncryptionPublicKeys from that list to - * this.memStore. - * - * @param {object} address - The param for the eth_getEncryptionPublicKey call to be made after the message is approved. - * @param {object} [req] - The original request object possibly containing the origin - * @returns {Promise} The raw public key contents - */ - addUnapprovedMessageAsync(address, req) { - return new Promise((resolve, reject) => { - if (!address) { - reject(new Error('MetaMask Message: address field is required.')); - return; - } - const msgId = this.addUnapprovedMessage(address, req); - this.once(`${msgId}:finished`, (data) => { - switch (data.status) { - case 'received': - resolve(data.rawData); - return; - case 'rejected': - reject( - ethErrors.provider.userRejectedRequest( - 'MetaMask EncryptionPublicKey: User denied message EncryptionPublicKey.', - ), - ); - return; - default: - reject( - new Error( - `MetaMask EncryptionPublicKey: Unknown problem: ${JSON.stringify( - address, - )}`, - ), - ); - } - }); - }); - } - - /** - * Creates a new EncryptionPublicKey with an 'unapproved' status using the passed msgParams. this.addMsg is called to add - * the new EncryptionPublicKey to this.messages, and to save the unapproved EncryptionPublicKeys from that list to - * this.memStore. - * - * @param {object} address - The param for the eth_getEncryptionPublicKey call to be made after the message is approved. - * @param {object} [req] - The original request object possibly containing the origin - * @returns {number} The id of the newly created EncryptionPublicKey. - */ - addUnapprovedMessage(address, req) { - log.debug(`EncryptionPublicKeyManager addUnapprovedMessage: address`); - // create txData obj with parameters and meta data - const time = new Date().getTime(); - const msgId = createId(); - const msgData = { - id: msgId, - msgParams: address, - time, - status: 'unapproved', - type: MESSAGE_TYPE.ETH_GET_ENCRYPTION_PUBLIC_KEY, - }; - - if (req) { - msgData.origin = req.origin; - } - - this.addMsg(msgData); - - // signal update - this.emit('update'); - return msgId; - } - - /** - * Adds a passed EncryptionPublicKey to this.messages, and calls this._saveMsgList() to save the unapproved EncryptionPublicKeys from that - * list to this.memStore. - * - * @param {Message} msg - The EncryptionPublicKey to add to this.messages - */ - addMsg(msg) { - this.messages.push(msg); - this._saveMsgList(); - } - - /** - * Returns a specified EncryptionPublicKey. - * - * @param {number} msgId - The id of the EncryptionPublicKey to get - * @returns {EncryptionPublicKey|undefined} The EncryptionPublicKey with the id that matches the passed msgId, or undefined - * if no EncryptionPublicKey has that id. - */ - getMsg(msgId) { - return this.messages.find((msg) => msg.id === msgId); - } - - /** - * Approves a EncryptionPublicKey. Sets the message status via a call to this.setMsgStatusApproved, and returns a promise - * with any the message params modified for proper providing. - * - * @param {object} msgParams - The msgParams to be used when eth_getEncryptionPublicKey is called, plus data added by MetaMask. - * @param {object} msgParams.metamaskId - Added to msgParams for tracking and identification within MetaMask. - * @returns {Promise} Promises the msgParams object with metamaskId removed. - */ - approveMessage(msgParams) { - this.setMsgStatusApproved(msgParams.metamaskId); - return this.prepMsgForEncryptionPublicKey(msgParams); - } - - /** - * Sets a EncryptionPublicKey status to 'approved' via a call to this._setMsgStatus. - * - * @param {number} msgId - The id of the EncryptionPublicKey to approve. - */ - setMsgStatusApproved(msgId) { - this._setMsgStatus(msgId, 'approved'); - } - - /** - * Sets a EncryptionPublicKey status to 'received' via a call to this._setMsgStatus and updates that EncryptionPublicKey in - * this.messages by adding the raw data of request to the EncryptionPublicKey - * - * @param {number} msgId - The id of the EncryptionPublicKey. - * @param {buffer} rawData - The raw data of the message request - */ - setMsgStatusReceived(msgId, rawData) { - const msg = this.getMsg(msgId); - msg.rawData = rawData; - this._updateMsg(msg); - this._setMsgStatus(msgId, 'received'); - } - - /** - * Removes the metamaskId property from passed msgParams and returns a promise which resolves the updated msgParams - * - * @param {object} msgParams - The msgParams to modify - * @returns {Promise} Promises the msgParams with the metamaskId property removed - */ - async prepMsgForEncryptionPublicKey(msgParams) { - delete msgParams.metamaskId; - return msgParams; - } - - /** - * Sets a EncryptionPublicKey status to 'rejected' via a call to this._setMsgStatus. - * - * @param {number} msgId - The id of the EncryptionPublicKey to reject. - * @param reason - */ - rejectMsg(msgId, reason = undefined) { - if (reason) { - this.metricsEvent({ - event: reason, - category: MetaMetricsEventCategory.Messages, - properties: { - action: 'Encryption public key Request', - }, - }); - } - this._setMsgStatus(msgId, 'rejected'); - } - - /** - * Sets a TypedMessage status to 'errored' via a call to this._setMsgStatus. - * - * @param {number} msgId - The id of the TypedMessage to error - * @param error - */ - errorMessage(msgId, error) { - const msg = this.getMsg(msgId); - msg.error = error; - this._updateMsg(msg); - this._setMsgStatus(msgId, 'errored'); - } - - /** - * Clears all unapproved messages from memory. - */ - clearUnapproved() { - this.messages = this.messages.filter((msg) => msg.status !== 'unapproved'); - this._saveMsgList(); - } - - /** - * Updates the status of a EncryptionPublicKey in this.messages via a call to this._updateMsg - * - * @private - * @param {number} msgId - The id of the EncryptionPublicKey to update. - * @param {string} status - The new status of the EncryptionPublicKey. - * @throws A 'EncryptionPublicKeyManager - EncryptionPublicKey not found for id: "${msgId}".' if there is no EncryptionPublicKey - * in this.messages with an id equal to the passed msgId - * @fires An event with a name equal to `${msgId}:${status}`. The EncryptionPublicKey is also fired. - * @fires If status is 'rejected' or 'received', an event with a name equal to `${msgId}:finished` is fired along - * with the EncryptionPublicKey - */ - _setMsgStatus(msgId, status) { - const msg = this.getMsg(msgId); - if (!msg) { - throw new Error( - `EncryptionPublicKeyManager - Message not found for id: "${msgId}".`, - ); - } - msg.status = status; - this._updateMsg(msg); - this.emit(`${msgId}:${status}`, msg); - if (status === 'rejected' || status === 'received') { - this.emit(`${msgId}:finished`, msg); - } - } - - /** - * Sets a EncryptionPublicKey in this.messages to the passed EncryptionPublicKey if the ids are equal. Then saves the - * unapprovedEncryptionPublicKeyMsgs index to storage via this._saveMsgList - * - * @private - * @param {EncryptionPublicKey} msg - A EncryptionPublicKey that will replace an existing EncryptionPublicKey (with the same - * id) in this.messages - */ - _updateMsg(msg) { - const index = this.messages.findIndex((message) => message.id === msg.id); - if (index !== -1) { - this.messages[index] = msg; - } - this._saveMsgList(); - } - - /** - * Saves the unapproved EncryptionPublicKeys, and their count, to this.memStore - * - * @private - * @fires 'updateBadge' - */ - _saveMsgList() { - const unapprovedEncryptionPublicKeyMsgs = this.getUnapprovedMsgs(); - const unapprovedEncryptionPublicKeyMsgCount = Object.keys( - unapprovedEncryptionPublicKeyMsgs, - ).length; - this.memStore.updateState({ - unapprovedEncryptionPublicKeyMsgs, - unapprovedEncryptionPublicKeyMsgCount, - }); - this.emit(METAMASK_CONTROLLER_EVENTS.UPDATE_BADGE); - } -} diff --git a/app/scripts/metamask-controller.js b/app/scripts/metamask-controller.js index 7315f301f..133f5a877 100644 --- a/app/scripts/metamask-controller.js +++ b/app/scripts/metamask-controller.js @@ -155,7 +155,6 @@ import OnboardingController from './controllers/onboarding'; import BackupController from './controllers/backup'; import IncomingTransactionsController from './controllers/incoming-transactions'; import DecryptMessageManager from './lib/decrypt-message-manager'; -import EncryptionPublicKeyManager from './lib/encryption-public-key-manager'; import TransactionController from './controllers/transactions'; import DetectTokensController from './controllers/detect-tokens'; import SwapsController from './controllers/swaps'; @@ -167,6 +166,7 @@ import createMetaRPCHandler from './lib/createMetaRPCHandler'; import { previousValueComparator } from './lib/util'; import createMetamaskMiddleware from './lib/createMetamaskMiddleware'; import SignController from './controllers/sign'; +import EncryptionPublicKeyController from './controllers/encryption-public-key'; import { CaveatMutatorFactories, @@ -1125,7 +1125,18 @@ export default class MetamaskController extends EventEmitter { this.metaMetricsController, ), }); - this.encryptionPublicKeyManager = new EncryptionPublicKeyManager({ + + this.encryptionPublicKeyController = new EncryptionPublicKeyController({ + messenger: this.controllerMessenger.getRestricted({ + name: 'EncryptionPublicKeyController', + allowedActions: [ + `${this.approvalController.name}:addRequest`, + `${this.approvalController.name}:acceptRequest`, + `${this.approvalController.name}:rejectRequest`, + ], + }), + keyringController: this.keyringController, + getState: this.getState.bind(this), metricsEvent: this.metaMetricsController.trackEvent.bind( this.metaMetricsController, ), @@ -1207,7 +1218,7 @@ export default class MetamaskController extends EventEmitter { NetworkControllerEventType.NetworkWillChange, () => { this.txController.txStateManager.clearUnapprovedTxs(); - this.encryptionPublicKeyManager.clearUnapproved(); + this.encryptionPublicKeyController.clearUnapproved(); this.decryptMessageManager.clearUnapproved(); this.signController.clearUnapproved(); }, @@ -1274,7 +1285,10 @@ export default class MetamaskController extends EventEmitter { this.signController, ), processDecryptMessage: this.newRequestDecryptMessage.bind(this), - processEncryptionPublicKey: this.newRequestEncryptionPublicKey.bind(this), + processEncryptionPublicKey: + this.encryptionPublicKeyController.newRequestEncryptionPublicKey.bind( + this.encryptionPublicKeyController, + ), getPendingNonce: this.getPendingNonce.bind(this), getPendingTransactionByHash: (hash) => this.txController.getTransactions({ @@ -1297,7 +1311,7 @@ export default class MetamaskController extends EventEmitter { TxController: this.txController.memStore, TokenRatesController: this.tokenRatesController, DecryptMessageManager: this.decryptMessageManager.memStore, - EncryptionPublicKeyManager: this.encryptionPublicKeyManager.memStore, + EncryptionPublicKeyController: this.encryptionPublicKeyController, SignController: this.signController, SwapsController: this.swapsController.store, EnsController: this.ensController.store, @@ -1379,7 +1393,9 @@ export default class MetamaskController extends EventEmitter { this.accountTracker.resetState, this.txController.resetState, this.decryptMessageManager.resetState, - this.encryptionPublicKeyManager.resetState, + this.encryptionPublicKeyController.resetState.bind( + this.encryptionPublicKeyController, + ), this.signController.resetState.bind(this.signController), this.swapsController.resetState, this.ensController.resetState, @@ -2090,9 +2106,15 @@ export default class MetamaskController extends EventEmitter { decryptMessageInline: this.decryptMessageInline.bind(this), cancelDecryptMessage: this.cancelDecryptMessage.bind(this), - // EncryptionPublicKeyManager - encryptionPublicKey: this.encryptionPublicKey.bind(this), - cancelEncryptionPublicKey: this.cancelEncryptionPublicKey.bind(this), + // EncryptionPublicKeyController + encryptionPublicKey: + this.encryptionPublicKeyController.encryptionPublicKey.bind( + this.encryptionPublicKeyController, + ), + cancelEncryptionPublicKey: + this.encryptionPublicKeyController.cancelEncryptionPublicKey.bind( + this.encryptionPublicKeyController, + ), // onboarding controller setSeedPhraseBackedUp: @@ -3317,109 +3339,6 @@ export default class MetamaskController extends EventEmitter { return this.getState(); } - // eth_getEncryptionPublicKey methods - - /** - * Called when a dapp uses the eth_getEncryptionPublicKey method. - * - * @param {object} msgParams - The params of the message to sign & return to the Dapp. - * @param {object} req - (optional) the original request, containing the origin - * Passed back to the requesting Dapp. - */ - async newRequestEncryptionPublicKey(msgParams, req) { - const address = msgParams; - const keyring = await this.keyringController.getKeyringForAccount(address); - - switch (keyring.type) { - case KeyringType.ledger: { - return new Promise((_, reject) => { - reject( - new Error('Ledger does not support eth_getEncryptionPublicKey.'), - ); - }); - } - - case KeyringType.trezor: { - return new Promise((_, reject) => { - reject( - new Error('Trezor does not support eth_getEncryptionPublicKey.'), - ); - }); - } - - case KeyringType.lattice: { - return new Promise((_, reject) => { - reject( - new Error('Lattice does not support eth_getEncryptionPublicKey.'), - ); - }); - } - - case KeyringType.qr: { - return Promise.reject( - new Error('QR hardware does not support eth_getEncryptionPublicKey.'), - ); - } - - default: { - const promise = - this.encryptionPublicKeyManager.addUnapprovedMessageAsync( - msgParams, - req, - ); - this.sendUpdate(); - this.opts.showUserConfirmation(); - return promise; - } - } - } - - /** - * Signifies a user's approval to receiving encryption public key in queue. - * Triggers receiving, and the callback function from newUnsignedEncryptionPublicKey. - * - * @param {object} msgParams - The params of the message to receive & return to the Dapp. - * @returns {Promise} A full state update. - */ - async encryptionPublicKey(msgParams) { - log.info('MetaMaskController - encryptionPublicKey'); - const msgId = msgParams.metamaskId; - // sets the status op the message to 'approved' - // and removes the metamaskId for decryption - try { - const params = await this.encryptionPublicKeyManager.approveMessage( - msgParams, - ); - - // EncryptionPublicKey message - const publicKey = await this.keyringController.getEncryptionPublicKey( - params.data, - ); - - // tells the listener that the message has been processed - // and can be returned to the dapp - this.encryptionPublicKeyManager.setMsgStatusReceived(msgId, publicKey); - } catch (error) { - log.info( - 'MetaMaskController - eth_getEncryptionPublicKey failed.', - error, - ); - this.encryptionPublicKeyManager.errorMessage(msgId, error); - } - return this.getState(); - } - - /** - * Used to cancel a eth_getEncryptionPublicKey type message. - * - * @param {string} msgId - The ID of the message to cancel. - */ - cancelEncryptionPublicKey(msgId) { - const messageManager = this.encryptionPublicKeyManager; - messageManager.rejectMsg(msgId); - return this.getState(); - } - /** * @returns {boolean} true if the keyring type supports EIP-1559 */ diff --git a/package.json b/package.json index 3b6b5f057..ac58eca27 100644 --- a/package.json +++ b/package.json @@ -246,7 +246,7 @@ "@metamask/jazzicon": "^2.0.0", "@metamask/key-tree": "^7.0.0", "@metamask/logo": "^3.1.1", - "@metamask/message-manager": "^2.1.0", + "@metamask/message-manager": "^3.0.0", "@metamask/metamask-eth-abis": "^3.0.0", "@metamask/notification-controller": "^2.0.0", "@metamask/obs-store": "^8.1.0", diff --git a/types/eth-keyring-controller.d.ts b/types/eth-keyring-controller.d.ts index 86d8ffc6b..81145fa60 100644 --- a/types/eth-keyring-controller.d.ts +++ b/types/eth-keyring-controller.d.ts @@ -5,5 +5,11 @@ declare module '@metamask/eth-keyring-controller' { signPersonalMessage: (...any) => any; signTypedMessage: (...any) => any; + + getKeyringForAccount: (address: string) => Promise<{ + type: string; + }>; + + getEncryptionPublicKey: (address: string) => Promise; } } diff --git a/ui/selectors/selectors.js b/ui/selectors/selectors.js index b4e04c7f6..1b48a1e84 100644 --- a/ui/selectors/selectors.js +++ b/ui/selectors/selectors.js @@ -483,15 +483,11 @@ export function getCurrentCurrency(state) { } export function getTotalUnapprovedCount(state) { - const { - unapprovedDecryptMsgCount = 0, - unapprovedEncryptionPublicKeyMsgCount = 0, - pendingApprovalCount = 0, - } = state.metamask; + const { unapprovedDecryptMsgCount = 0, pendingApprovalCount = 0 } = + state.metamask; return ( unapprovedDecryptMsgCount + - unapprovedEncryptionPublicKeyMsgCount + pendingApprovalCount + getSuggestedAssetCount(state) ); diff --git a/yarn.lock b/yarn.lock index 475744a26..b2d8e4cee 100644 --- a/yarn.lock +++ b/yarn.lock @@ -4015,9 +4015,9 @@ __metadata: languageName: node linkType: hard -"@metamask/message-manager@npm:^2.1.0": - version: 2.1.0 - resolution: "@metamask/message-manager@npm:2.1.0" +"@metamask/message-manager@npm:^3.0.0": + version: 3.0.0 + resolution: "@metamask/message-manager@npm:3.0.0" dependencies: "@metamask/base-controller": ^2.0.0 "@metamask/controller-utils": ^3.1.0 @@ -4026,7 +4026,7 @@ __metadata: ethereumjs-util: ^7.0.10 jsonschema: ^1.2.4 uuid: ^8.3.2 - checksum: f3a233a84aec73051f8f1183dab32c4d9a976edaa3c6461b118a8e6f20cf43f8757827ad6c877aed635ef850944ce054af03b34592f5b72f5d0667fa8b179dc9 + checksum: 14e0a4a398d95ce720e515bd1f35aee7b7b9f5f59367210a9125fe66fb561b630ae51b61f32048767f0bb30dd4a2e442e47c8d850de78f820feda7f72e4dc05e languageName: node linkType: hard @@ -24252,7 +24252,7 @@ __metadata: "@metamask/jazzicon": ^2.0.0 "@metamask/key-tree": ^7.0.0 "@metamask/logo": ^3.1.1 - "@metamask/message-manager": ^2.1.0 + "@metamask/message-manager": ^3.0.0 "@metamask/metamask-eth-abis": ^3.0.0 "@metamask/notification-controller": ^2.0.0 "@metamask/obs-store": ^8.1.0 From 300bfd6e693b711efe2c509a2b879c2fd0aef34e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ant=C3=B3nio=20Regadas?= Date: Thu, 13 Apr 2023 11:14:44 +0100 Subject: [PATCH 17/36] [MMI] 2642 compliance modal component (#18410) * adds component, styles and storybook file * wip * prettier and adds test * prettier * lint * review fix * lint * updates to IconSize, IconName --- .../compliance-modal.test.js.snap | 93 +++++++++++++++++++ .../compliance-modal/compliance-modal.js | 88 ++++++++++++++++++ .../compliance-modal/compliance-modal.scss | 6 ++ .../compliance-modal.stories.js | 22 +++++ .../compliance-modal/compliance-modal.test.js | 55 +++++++++++ .../institutional/compliance-modal/index.js | 1 + 6 files changed, 265 insertions(+) create mode 100644 ui/components/institutional/compliance-modal/__snapshots__/compliance-modal.test.js.snap create mode 100644 ui/components/institutional/compliance-modal/compliance-modal.js create mode 100644 ui/components/institutional/compliance-modal/compliance-modal.scss create mode 100644 ui/components/institutional/compliance-modal/compliance-modal.stories.js create mode 100644 ui/components/institutional/compliance-modal/compliance-modal.test.js create mode 100644 ui/components/institutional/compliance-modal/index.js diff --git a/ui/components/institutional/compliance-modal/__snapshots__/compliance-modal.test.js.snap b/ui/components/institutional/compliance-modal/__snapshots__/compliance-modal.test.js.snap new file mode 100644 index 000000000..60d85a469 --- /dev/null +++ b/ui/components/institutional/compliance-modal/__snapshots__/compliance-modal.test.js.snap @@ -0,0 +1,93 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`ComplianceModal should render the correct content 1`] = ` +
+ +
+`; diff --git a/ui/components/institutional/compliance-modal/compliance-modal.js b/ui/components/institutional/compliance-modal/compliance-modal.js new file mode 100644 index 000000000..5bf2f2497 --- /dev/null +++ b/ui/components/institutional/compliance-modal/compliance-modal.js @@ -0,0 +1,88 @@ +import React from 'react'; +import { useDispatch } from 'react-redux'; +import { useI18nContext } from '../../../hooks/useI18nContext'; +import { hideModal } from '../../../store/actions'; +import Modal from '../../app/modal'; +import Box from '../../ui/box'; +import { Text, ButtonIcon, IconSize, IconName } from '../../component-library'; +import { + AlignItems, + JustifyContent, + TextColor, + DISPLAY, +} from '../../../helpers/constants/design-system'; + +const ComplianceModal = () => { + const dispatch = useDispatch(); + const t = useI18nContext(); + + const handleSubmit = () => { + global.platform.openTab({ + url: 'https://start.compliance.codefi.network/', + }); + }; + + const handleClose = () => dispatch(hideModal()); + + return ( + + + + + Codefi Compliance + + {t('codefiCompliance')} + + + + + + {t('complianceBlurb0')} + + + {t('complianceBlurb1')} + + + {t('complianceBlurpStep0')} + +
    +
  1. {t('complianceBlurbStep1')}
  2. +
  3. {t('complianceBlurbStep2')}
  4. +
  5. {t('complianceBlurbStep3')}
  6. +
  7. {t('complianceBlurbStep4')}
  8. +
  9. {t('complianceBlurbStep5')}
  10. +
+
+
+ ); +}; + +export default ComplianceModal; diff --git a/ui/components/institutional/compliance-modal/compliance-modal.scss b/ui/components/institutional/compliance-modal/compliance-modal.scss new file mode 100644 index 000000000..76570844e --- /dev/null +++ b/ui/components/institutional/compliance-modal/compliance-modal.scss @@ -0,0 +1,6 @@ +.compliance-modal { + ol { + list-style: decimal; + list-style-position: inside; + } +} diff --git a/ui/components/institutional/compliance-modal/compliance-modal.stories.js b/ui/components/institutional/compliance-modal/compliance-modal.stories.js new file mode 100644 index 000000000..5416daeba --- /dev/null +++ b/ui/components/institutional/compliance-modal/compliance-modal.stories.js @@ -0,0 +1,22 @@ +import React from 'react'; +import { Provider } from 'react-redux'; +import configureStore from '../../../store/store'; +import testData from '../../../../.storybook/test-data'; +import ComplianceModal from '.'; + +const store = configureStore(testData); + +export default { + title: 'Components/Institutional/ComplianceModal', + decorators: [(story) => {story()}], + component: ComplianceModal, + argTypes: { + onClick: { + action: 'onClick', + }, + }, +}; + +export const DefaultStory = (args) => ; + +DefaultStory.storyName = 'ComplianceModal'; diff --git a/ui/components/institutional/compliance-modal/compliance-modal.test.js b/ui/components/institutional/compliance-modal/compliance-modal.test.js new file mode 100644 index 000000000..259340edd --- /dev/null +++ b/ui/components/institutional/compliance-modal/compliance-modal.test.js @@ -0,0 +1,55 @@ +import React from 'react'; +import { render, fireEvent } from '@testing-library/react'; +import { useDispatch } from 'react-redux'; +import sinon from 'sinon'; +import { hideModal } from '../../../store/actions'; +import ComplianceModal from '.'; + +jest.mock('react-redux', () => ({ + useDispatch: jest.fn(), +})); + +jest.mock('../../../store/actions', () => ({ + hideModal: jest.fn(), +})); + +describe('ComplianceModal', () => { + let dispatchMock; + + beforeEach(() => { + dispatchMock = jest.fn(); + useDispatch.mockReturnValue(dispatchMock); + }); + + afterEach(() => { + jest.resetAllMocks(); + }); + + it('should render the correct content', () => { + const { container, getByTestId } = render(); + + expect(getByTestId('compliance-info')).toBeInTheDocument(); + expect(getByTestId('compliance-bullets')).toBeInTheDocument(); + expect(container).toMatchSnapshot(); + }); + + it('should close the modal when close button is clicked', () => { + const { getByTestId } = render(); + + fireEvent.click(getByTestId('compliance-modal-close')); + + expect(hideModal).toHaveBeenCalled(); + expect(dispatchMock).toHaveBeenCalledWith(hideModal()); + }); + + it('should open the Compliance page when submit button is clicked', () => { + global.platform = { openTab: sinon.spy() }; + const { container } = render(); + + const btn = container.getElementsByClassName('btn-primary')[0]; + + fireEvent.click(btn); + + expect(global.platform.openTab.called).toBeTruthy(); + }); +}); diff --git a/ui/components/institutional/compliance-modal/index.js b/ui/components/institutional/compliance-modal/index.js new file mode 100644 index 000000000..04582c660 --- /dev/null +++ b/ui/components/institutional/compliance-modal/index.js @@ -0,0 +1 @@ +export { default } from './compliance-modal'; From 135d4eaaaa587e819b59beee755077e34939e278 Mon Sep 17 00:00:00 2001 From: legobeat <109787230+legobeat@users.noreply.github.com> Date: Thu, 13 Apr 2023 23:39:41 +0900 Subject: [PATCH 18/36] deps/security: vm2@3.9.15->3.9.16 (#18555) CVE-2023-29199 / GHSA-xj72-wvfv-8985 --- yarn.lock | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/yarn.lock b/yarn.lock index b2d8e4cee..93b2f813e 100644 --- a/yarn.lock +++ b/yarn.lock @@ -34566,14 +34566,14 @@ __metadata: linkType: hard "vm2@npm:^3.9.3": - version: 3.9.15 - resolution: "vm2@npm:3.9.15" + version: 3.9.16 + resolution: "vm2@npm:3.9.16" dependencies: acorn: ^8.7.0 acorn-walk: ^8.2.0 bin: vm2: bin/vm2 - checksum: 1df70d5a88173651c0062901aba67e5edfeeb3f699fe6c305f5efb6a5a7391e5724cbf98a6516600b65016c6824dc07cc79947ea4222f8537ae1d9ce0b730ad7 + checksum: 646b45dca721acb3c8e4ae0742129f13612972387911c2475f3c06ac2b4232000cab0bdaaa65d97d6ea8dc70880e039542618b1b3d04adea79cd94803cbc4ab3 languageName: node linkType: hard From 643a89f24d452de008b999e9663728dbbd8a3e8f Mon Sep 17 00:00:00 2001 From: Niranjana Binoy <43930900+NiranjanaBinoy@users.noreply.github.com> Date: Thu, 13 Apr 2023 12:51:20 -0400 Subject: [PATCH 19/36] Disabling network and account changes after the send flow is initiated (#18086) --- coverage-targets.js | 8 +- test/data/mock-send-state.json | 25 +++- .../app/app-header/app-header.component.js | 1 + ui/pages/routes/routes.component.js | 6 +- ui/pages/routes/routes.component.test.js | 122 ++++++++++++++++++ 5 files changed, 156 insertions(+), 6 deletions(-) create mode 100644 ui/pages/routes/routes.component.test.js diff --git a/coverage-targets.js b/coverage-targets.js index 6097331d1..1ed939c2f 100644 --- a/coverage-targets.js +++ b/coverage-targets.js @@ -6,10 +6,10 @@ // subset of files to check against these targets. module.exports = { global: { - lines: 66, - branches: 54.4, - statements: 65, - functions: 58.5, + lines: 67.8, + branches: 55.84, + statements: 67.13, + functions: 59.66, }, transforms: { branches: 100, diff --git a/test/data/mock-send-state.json b/test/data/mock-send-state.json index 6e460ef8e..a8d379dcd 100644 --- a/test/data/mock-send-state.json +++ b/test/data/mock-send-state.json @@ -2,6 +2,13 @@ "DNS": { "resolution": "" }, + "activeTab": { + "id": 113, + "title": "E2E Test Dapp", + "origin": "https://metamask.github.io", + "protocol": "https:", + "url": "https://metamask.github.io/test-dapp/" + }, "appState": { "networkDropdownOpen": false, "gasIsLoading": false, @@ -16,7 +23,8 @@ "name": null } }, - "warning": null + "warning": null, + "alertOpen": false }, "confirmTransaction": { "txData": { @@ -42,6 +50,10 @@ "history": { "mostRecentOverviewPage": "/mostRecentOverviewPage" }, + "invalidCustomNetwork": { + "state": "CLOSED", + "networkName": "" + }, "metamask": { "ipfsGateway": "", "dismissSeedBackUpReminder": false, @@ -88,6 +100,14 @@ "ensResolutionsByAddress": {}, "isAccountMenuOpen": false, "isUnlocked": true, + "completedOnboarding": true, + "usedNetworks": { + "0x1": true, + "0x5": true, + "0x539": true + }, + "showTestnetMessageInDropdown": true, + "networkConfigurations": {}, "alertEnabledness": { "unconnectedAccount": true }, @@ -1358,5 +1378,8 @@ "balance": "0x4563918244f40000" }, "stage": "DRAFT" + }, + "unconnectedAccount": { + "state": "CLOSED" } } diff --git a/ui/components/app/app-header/app-header.component.js b/ui/components/app/app-header/app-header.component.js index 248dda89f..6ef03c773 100644 --- a/ui/components/app/app-header/app-header.component.js +++ b/ui/components/app/app-header/app-header.component.js @@ -89,6 +89,7 @@ export default class AppHeader extends PureComponent { className={classnames('account-menu__icon', { 'account-menu__icon--disabled': disabled, })} + disabled={Boolean(disabled)} onClick={() => { if (!disabled) { !isAccountMenuOpen && diff --git a/ui/pages/routes/routes.component.js b/ui/pages/routes/routes.component.js index 8cb160764..c5804d146 100644 --- a/ui/pages/routes/routes.component.js +++ b/ui/pages/routes/routes.component.js @@ -322,7 +322,11 @@ export default class Routes extends Component { } onEditTransactionPage() { - return this.props.sendStage === SEND_STAGES.EDIT; + return ( + this.props.sendStage === SEND_STAGES.EDIT || + this.props.sendStage === SEND_STAGES.DRAFT || + this.props.sendStage === SEND_STAGES.ADD_RECIPIENT + ); } onSwapsPage() { diff --git a/ui/pages/routes/routes.component.test.js b/ui/pages/routes/routes.component.test.js new file mode 100644 index 000000000..fba5da41f --- /dev/null +++ b/ui/pages/routes/routes.component.test.js @@ -0,0 +1,122 @@ +import React from 'react'; +import configureMockStore from 'redux-mock-store'; +import { fireEvent } from '@testing-library/react'; + +import { SEND_STAGES } from '../../ducks/send'; +import { renderWithProvider } from '../../../test/jest'; +import mockSendState from '../../../test/data/mock-send-state.json'; +import Routes from '.'; + +const mockShowNetworkDropdown = jest.fn(); +const mockHideNetworkDropdown = jest.fn(); + +jest.mock('webextension-polyfill', () => ({ + runtime: { + onMessage: { + addListener: jest.fn(), + removeListener: jest.fn(), + }, + getManifest: () => ({ manifest_version: 2 }), + }, +})); + +jest.mock('../../store/actions', () => ({ + getGasFeeTimeEstimate: jest.fn().mockImplementation(() => Promise.resolve()), + getGasFeeEstimatesAndStartPolling: jest + .fn() + .mockImplementation(() => Promise.resolve()), + addPollingTokenToAppState: jest.fn(), + showNetworkDropdown: () => mockShowNetworkDropdown, + hideNetworkDropdown: () => mockHideNetworkDropdown, +})); + +jest.mock('react-router-dom', () => ({ + ...jest.requireActual('react-router-dom'), + useHistory: () => ({ + push: jest.fn(), + }), +})); + +jest.mock('../../ducks/send', () => ({ + ...jest.requireActual('../../ducks/send'), + resetSendState: () => ({ type: 'XXX' }), + getGasPrice: jest.fn(), +})); + +jest.mock('../../ducks/domains', () => ({ + ...jest.requireActual('../../ducks/domains'), + initializeDomainSlice: () => ({ type: 'XXX' }), +})); + +describe('Routes Component', () => { + afterEach(() => { + mockShowNetworkDropdown.mockClear(); + mockHideNetworkDropdown.mockClear(); + }); + describe('render during send flow', () => { + it('should render with network and account change disabled while adding recipient for send flow', () => { + const store = configureMockStore()({ + ...mockSendState, + send: { + ...mockSendState.send, + stage: SEND_STAGES.ADD_RECIPIENT, + }, + }); + const { getByTestId } = renderWithProvider(, store, ['/send']); + + expect(getByTestId('account-menu-icon')).toBeDisabled(); + + const networkDisplay = getByTestId('network-display'); + fireEvent.click(networkDisplay); + expect(mockShowNetworkDropdown).not.toHaveBeenCalled(); + }); + it('should render with network and account change disabled while user is in send page', () => { + const store = configureMockStore()({ + ...mockSendState, + }); + const { getByTestId } = renderWithProvider(, store, ['/send']); + + expect(getByTestId('account-menu-icon')).toBeDisabled(); + + const networkDisplay = getByTestId('network-display'); + fireEvent.click(networkDisplay); + expect(mockShowNetworkDropdown).not.toHaveBeenCalled(); + }); + it('should render with network and account change disabled while editing a send transaction', () => { + const store = configureMockStore()({ + ...mockSendState, + send: { + ...mockSendState.send, + stage: SEND_STAGES.EDIT, + }, + }); + const { getByTestId } = renderWithProvider(, store, ['/send']); + + expect(getByTestId('account-menu-icon')).toBeDisabled(); + + const networkDisplay = getByTestId('network-display'); + fireEvent.click(networkDisplay); + expect(mockShowNetworkDropdown).not.toHaveBeenCalled(); + }); + it('should render when send transaction is not active', () => { + const store = configureMockStore()({ + ...mockSendState, + metamask: { + ...mockSendState.metamask, + swapsState: { + ...mockSendState.metamask.swapsState, + swapsFeatureIsLive: true, + }, + pendingApprovals: {}, + announcements: {}, + }, + send: { + ...mockSendState.send, + stage: SEND_STAGES.INACTIVE, + }, + }); + const { getByTestId } = renderWithProvider(, store); + expect(getByTestId('account-menu-icon')).not.toBeDisabled(); + }); + }); +}); From eb51460cae0ad1843f80329d423b6ed35f420d59 Mon Sep 17 00:00:00 2001 From: David Walsh Date: Thu, 13 Apr 2023 11:54:03 -0500 Subject: [PATCH 20/36] UX: Multichain: App header (#18363) * UX: Multichain: App header * Export app header, provide required information, put feature flag in place * Provide available data * Implement account picker -- centered and opens account popover * Remove backgrounds, use isUnlocked * Fix placement of the global menu * Show logo when unlocked * Add selector for getting current network, provide props to AvatarNetwork and PickerNetwork * Wire up the network menu to the header * fixed ui for all the screens * updated story for header * fixed import and header settings * updated lint error * fixed tests * updated header * removed test * updated snapshot test * updated network menu * updated changes * removed comment from menu bar * updated css * updated test for network list menu * updated stylesheet * updated ButtonIcon import --------- Co-authored-by: NidhiKJha --- app/scripts/background.js | 1 + ui/components/app/menu-bar/menu-bar.js | 21 +- .../account-list-menu/account-list-menu.js | 1 - .../__snapshots__/app-header.test.js.snap | 224 ++++++++++++++++++ .../multichain/app-header/app-header.js | 215 +++++++++++++++++ .../multichain/app-header/app-header.scss | 74 ++++++ .../app-header/app-header.stories.js | 71 ++++++ .../multichain/app-header/app-header.test.js | 107 +++++++++ ui/components/multichain/app-header/index.js | 1 + ui/components/multichain/index.js | 1 + .../multichain/multichain-components.scss | 1 + .../network-list-menu/network-list-menu.js | 12 +- .../network-list-menu.stories.js | 4 +- .../network-list-menu.test.js | 5 +- ui/ducks/metamask/metamask.js | 7 + ui/ducks/metamask/metamask.test.js | 11 + ui/pages/home/home.component.js | 2 +- ui/pages/routes/routes.component.js | 40 +++- ui/pages/routes/routes.container.js | 3 + ui/selectors/selectors.js | 7 + ui/store/actionConstants.ts | 1 + ui/store/actions.ts | 6 + 22 files changed, 778 insertions(+), 37 deletions(-) create mode 100644 ui/components/multichain/app-header/__snapshots__/app-header.test.js.snap create mode 100644 ui/components/multichain/app-header/app-header.js create mode 100644 ui/components/multichain/app-header/app-header.scss create mode 100644 ui/components/multichain/app-header/app-header.stories.js create mode 100644 ui/components/multichain/app-header/app-header.test.js create mode 100644 ui/components/multichain/app-header/index.js diff --git a/app/scripts/background.js b/app/scripts/background.js index 622e888f9..048d494f8 100644 --- a/app/scripts/background.js +++ b/app/scripts/background.js @@ -208,6 +208,7 @@ browser.runtime.onConnectExternal.addListener(async (...args) => { * @property {boolean} isInitialized - Whether the first vault has been created. * @property {boolean} isUnlocked - Whether the vault is currently decrypted and accounts are available for selection. * @property {boolean} isAccountMenuOpen - Represents whether the main account selection UI is currently displayed. + * @property {boolean} isNetworkMenuOpen - Represents whether the main network selection UI is currently displayed. * @property {object} identities - An object matching lower-case hex addresses to Identity objects with "address" and "name" (nickname) keys. * @property {object} unapprovedTxs - An object mapping transaction hashes to unapproved transactions. * @property {object} networkConfigurations - A list of network configurations, containing RPC provider details (eg chainId, rpcUrl, rpcPreferences). diff --git a/ui/components/app/menu-bar/menu-bar.js b/ui/components/app/menu-bar/menu-bar.js index fac87c26f..08da8208f 100644 --- a/ui/components/app/menu-bar/menu-bar.js +++ b/ui/components/app/menu-bar/menu-bar.js @@ -16,7 +16,6 @@ import { getOriginOfCurrentTab } from '../../../selectors'; import { MetaMetricsContext } from '../../../contexts/metametrics'; import { ButtonIcon } from '../../component-library/button-icon/deprecated'; import { ICON_NAMES } from '../../component-library/icon/deprecated'; -import { GlobalMenu } from '../../multichain/global-menu'; import AccountOptionsMenu from './account-options-menu'; export default function MenuBar() { @@ -34,7 +33,7 @@ export default function MenuBar() { return (
- {showStatus ? ( // TODO: Move the connection status menu icon to the correct position in header once we implement the new header + {showStatus ? ( history.push(CONNECTED_ACCOUNTS_ROUTE)} /> @@ -58,18 +57,12 @@ export default function MenuBar() { }} /> - {accountOptionsMenuOpen && - (process.env.MULTICHAIN ? ( - setAccountOptionsMenuOpen(false)} - /> - ) : ( - setAccountOptionsMenuOpen(false)} - /> - ))} + {accountOptionsMenuOpen && ( + setAccountOptionsMenuOpen(false)} + /> + )}
); } diff --git a/ui/components/multichain/account-list-menu/account-list-menu.js b/ui/components/multichain/account-list-menu/account-list-menu.js index fcb7d1046..7ffbcf5fa 100644 --- a/ui/components/multichain/account-list-menu/account-list-menu.js +++ b/ui/components/multichain/account-list-menu/account-list-menu.js @@ -103,7 +103,6 @@ export const AccountListMenu = ({ onClose }) => { }, }); dispatch(setSelectedAccount(account.address)); - onClose(); }} identity={account} key={account.address} diff --git a/ui/components/multichain/app-header/__snapshots__/app-header.test.js.snap b/ui/components/multichain/app-header/__snapshots__/app-header.test.js.snap new file mode 100644 index 000000000..d4d1cc8da --- /dev/null +++ b/ui/components/multichain/app-header/__snapshots__/app-header.test.js.snap @@ -0,0 +1,224 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`App Header should match snapshot 1`] = ` +
+
+
+ +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+
+
+
+`; diff --git a/ui/components/multichain/app-header/app-header.js b/ui/components/multichain/app-header/app-header.js new file mode 100644 index 000000000..d7f278530 --- /dev/null +++ b/ui/components/multichain/app-header/app-header.js @@ -0,0 +1,215 @@ +import React, { useContext, useState, useRef } from 'react'; +import PropTypes from 'prop-types'; +import browser from 'webextension-polyfill'; +import { useDispatch, useSelector } from 'react-redux'; +import { useHistory } from 'react-router-dom'; +import { MetaMetricsContext } from '../../../contexts/metametrics'; +import { + MetaMetricsEventCategory, + MetaMetricsEventName, +} from '../../../../shared/constants/metametrics'; +import { + CONNECTED_ACCOUNTS_ROUTE, + DEFAULT_ROUTE, +} from '../../../helpers/constants/routes'; + +import { + AlignItems, + BackgroundColor, + BLOCK_SIZES, + DISPLAY, + JustifyContent, + Size, +} from '../../../helpers/constants/design-system'; +import { AvatarNetwork, Button, PickerNetwork } from '../../component-library'; +import { ButtonIcon } from '../../component-library/button-icon/deprecated'; +import { ICON_NAMES } from '../../component-library/icon/deprecated'; +import { + getCurrentNetwork, + getOriginOfCurrentTab, + getSelectedIdentity, +} from '../../../selectors'; +import { GlobalMenu, AccountPicker } from '..'; + +import Box from '../../ui/box/box'; +import { toggleAccountMenu, toggleNetworkMenu } from '../../../store/actions'; +import MetafoxLogo from '../../ui/metafox-logo'; +import { getEnvironmentType } from '../../../../app/scripts/lib/util'; +import { ENVIRONMENT_TYPE_POPUP } from '../../../../shared/constants/app'; +import ConnectedStatusIndicator from '../../app/connected-status-indicator'; + +export const AppHeader = ({ onClick }) => { + const trackEvent = useContext(MetaMetricsContext); + const [accountOptionsMenuOpen, setAccountOptionsMenuOpen] = useState(false); + const menuRef = useRef(false); + const origin = useSelector(getOriginOfCurrentTab); + const history = useHistory(); + const isUnlocked = useSelector((state) => state.metamask.isUnlocked); + + // Used for account picker + const identity = useSelector(getSelectedIdentity); + const dispatch = useDispatch(); + + // Used for network icon / dropdown + const currentNetwork = useSelector(getCurrentNetwork); + + // used to get the environment and connection status + const popupStatus = getEnvironmentType() === ENVIRONMENT_TYPE_POPUP; + const showStatus = + getEnvironmentType() === ENVIRONMENT_TYPE_POPUP && + origin && + origin !== browser.runtime.id; + + return ( + <> + {isUnlocked && !popupStatus ? ( + + { + if (onClick) { + await onClick(); + } + history.push(DEFAULT_ROUTE); + }} + /> + + ) : null} + + <> + {isUnlocked ? ( + + {popupStatus ? ( + + ) : ( + dispatch(toggleNetworkMenu())} + /> + )} + + dispatch(toggleAccountMenu())} + /> + + {showStatus ? ( + history.push(CONNECTED_ACCOUNTS_ROUTE)} + /> + ) : null} + + { + trackEvent({ + event: MetaMetricsEventName.NavAccountMenuOpened, + category: MetaMetricsEventCategory.Navigation, + properties: { + location: 'Home', + }, + }); + setAccountOptionsMenuOpen(true); + }} + /> + + + {accountOptionsMenuOpen ? ( + setAccountOptionsMenuOpen(false)} + /> + ) : null} + + ) : ( + + dispatch(toggleNetworkMenu())} + /> + { + if (onClick) { + await onClick(); + } + history.push(DEFAULT_ROUTE); + }} + /> + + )} + + + + ); +}; + +AppHeader.propTypes = { + /** + * The onClick handler to be passed to the MetaMask Logo in the App Header + */ + onClick: PropTypes.func, +}; diff --git a/ui/components/multichain/app-header/app-header.scss b/ui/components/multichain/app-header/app-header.scss new file mode 100644 index 000000000..7ab95825a --- /dev/null +++ b/ui/components/multichain/app-header/app-header.scss @@ -0,0 +1,74 @@ +.multichain-app-header { + $height-screen-sm-max: 100%; + $width-screen-sm-min: 85vw; + $width-screen-md-min: 80vw; + $width-screen-lg-min: 62vw; + + flex-flow: column nowrap; + z-index: 55; + min-height: 64px; + + &__contents { + display: grid; + grid-template-columns: 1fr 2fr 1fr; + height: 64px; + + @include screen-sm-max { + height: $height-screen-sm-max; + } + + @include screen-sm-min { + width: $width-screen-sm-min; + } + + @include screen-md-min { + width: $width-screen-md-min; + } + + @include screen-lg-min { + width: $width-screen-lg-min; + } + + &--avatar-network { + background-color: transparent; + width: min-content; + padding: 8px; + + &:hover, + &:active { + box-shadow: none; + background: transparent; + } + } + } + + &__lock-contents { + flex-flow: row nowrap; + height: 64px; + + @include screen-sm-max { + height: $height-screen-sm-max; + } + + @include screen-sm-min { + width: $width-screen-sm-min; + } + + @include screen-md-min { + width: $width-screen-md-min; + } + + @include screen-lg-min { + width: $width-screen-lg-min; + } + } +} + +.multichain-app-header-shadow { + box-shadow: var(--shadow-size-md) var(--color-shadow-default); +} + +.multichain-app-header-logo { + height: 75px; + flex: 0 0 auto; +} diff --git a/ui/components/multichain/app-header/app-header.stories.js b/ui/components/multichain/app-header/app-header.stories.js new file mode 100644 index 000000000..c8ae6a810 --- /dev/null +++ b/ui/components/multichain/app-header/app-header.stories.js @@ -0,0 +1,71 @@ +import React from 'react'; +import { Provider } from 'react-redux'; +import configureStore from '../../../store/store'; +import testData from '../../../../.storybook/test-data'; +import { AppHeader } from '.'; + +const store = configureStore(testData); + +export default { + title: 'Components/Multichain/AppHeader', + decorators: [(story) => {story()}], + component: AppHeader, + argTypes: { + onClick: { + action: 'onClick', + }, + }, +}; +const customNetworkUnlockedData = { + ...testData, + metamask: { + ...testData.metamask, + preferences: { + showTestNetworks: true, + }, + isUnlocked: true, + networkConfigurations: { + ...testData.metamask.networkConfigurations, + }, + }, +}; +const customNetworkUnlockedStore = configureStore(customNetworkUnlockedData); + +const customNetworkLockedData = { + ...testData, + metamask: { + ...testData.metamask, + preferences: { + showTestNetworks: true, + }, + isUnlocked: false, + networkConfigurations: { + ...testData.metamask.networkConfigurations, + }, + }, +}; +const customNetworkLockedStore = configureStore(customNetworkLockedData); + +const Template = (args) => { + return ; +}; + +export const FullScreenAndUnlockedStory = Template.bind({}); + +FullScreenAndUnlockedStory.decorators = [ + (Story) => ( + + + + ), +]; + +export const FullScreenAndLockedStory = Template.bind({}); + +FullScreenAndLockedStory.decorators = [ + (Story) => ( + + + + ), +]; diff --git a/ui/components/multichain/app-header/app-header.test.js b/ui/components/multichain/app-header/app-header.test.js new file mode 100644 index 000000000..c69caf12f --- /dev/null +++ b/ui/components/multichain/app-header/app-header.test.js @@ -0,0 +1,107 @@ +import React from 'react'; +import configureStore from 'redux-mock-store'; +import { CHAIN_IDS } from '../../../../shared/constants/network'; +import { renderWithProvider } from '../../../../test/lib/render-helpers'; +import { AppHeader } from '.'; + +describe('App Header', () => { + it('should match snapshot', () => { + const mockState = { + activeTab: { + title: 'Eth Sign Tests', + origin: 'https://remix.ethereum.org', + protocol: 'https:', + url: 'https://remix.ethereum.org/', + }, + metamask: { + provider: { + chainId: CHAIN_IDS.GOERLI, + }, + accounts: { + '0x7250739de134d33ec7ab1ee592711e15098c9d2d': { + address: '0x7250739de134d33ec7ab1ee592711e15098c9d2d', + }, + '0x8e5d75d60224ea0c33d0041e75de68b1c3cb6dd5': { + address: '0x8e5d75d60224ea0c33d0041e75de68b1c3cb6dd5', + }, + }, + preferences: { + showTestNetworks: true, + }, + cachedBalances: {}, + subjects: { + 'https://remix.ethereum.org': { + permissions: { + eth_accounts: { + caveats: [ + { + type: 'restrictReturnedAccounts', + value: [ + '0x8e5d75d60224ea0c33d0041e75de68b1c3cb6dd5', + '0x7250739de134d33ec7ab1ee592711e15098c9d2d', + ], + }, + ], + date: 1586359844177, + id: '3aa65a8b-3bcb-4944-941b-1baa5fe0ed8b', + invoker: 'https://remix.ethereum.org', + parentCapability: 'eth_accounts', + }, + }, + }, + 'peepeth.com': { + permissions: { + eth_accounts: { + caveats: [ + { + type: 'restrictReturnedAccounts', + value: ['0x8e5d75d60224ea0c33d0041e75de68b1c3cb6dd5'], + }, + ], + date: 1585676177970, + id: '840d72a0-925f-449f-830a-1aa1dd5ce151', + invoker: 'peepeth.com', + parentCapability: 'eth_accounts', + }, + }, + }, + }, + identities: { + '0x7250739de134d33ec7ab1ee592711e15098c9d2d': { + address: '0x7250739de134d33ec7ab1ee592711e15098c9d2d', + name: 'Really Long Name That Should Be Truncated', + }, + '0x8e5d75d60224ea0c33d0041e75de68b1c3cb6dd5': { + address: '0x8e5d75d60224ea0c33d0041e75de68b1c3cb6dd5', + lastSelected: 1586359844192, + name: 'Account 1', + }, + }, + keyrings: [ + { + accounts: [ + '0x8e5d75d60224ea0c33d0041e75de68b1c3cb6dd5', + '0x7250739de134d33ec7ab1ee592711e15098c9d2d', + ], + }, + ], + permissionHistory: { + 'https://remix.ethereum.org': { + eth_accounts: { + accounts: { + '0x7250739de134d33ec7ab1ee592711e15098c9d2d': 1586359844192, + '0x8e5d75d60224ea0c33d0041e75de68b1c3cb6dd5': 1586359844192, + }, + lastApproved: 1586359844192, + }, + }, + }, + }, + }; + + const mockStore = configureStore(); + const store = mockStore(mockState); + const { container } = renderWithProvider(, store); + expect(container).toMatchSnapshot(); + }); +}); diff --git a/ui/components/multichain/app-header/index.js b/ui/components/multichain/app-header/index.js new file mode 100644 index 000000000..1126d287c --- /dev/null +++ b/ui/components/multichain/app-header/index.js @@ -0,0 +1 @@ +export { AppHeader } from './app-header'; diff --git a/ui/components/multichain/index.js b/ui/components/multichain/index.js index a4703b158..c9cf2efe1 100644 --- a/ui/components/multichain/index.js +++ b/ui/components/multichain/index.js @@ -2,6 +2,7 @@ export { AccountListItem } from './account-list-item'; export { AccountListItemMenu } from './account-list-item-menu'; export { AccountListMenu } from './account-list-menu'; export { AccountPicker } from './account-picker'; +export { AppHeader } from './app-header'; export { DetectedTokensBanner } from './detected-token-banner'; export { GlobalMenu } from './global-menu'; export { MultichainImportTokenLink } from './multichain-import-token-link'; diff --git a/ui/components/multichain/multichain-components.scss b/ui/components/multichain/multichain-components.scss index 63b19d690..75b42ef2a 100644 --- a/ui/components/multichain/multichain-components.scss +++ b/ui/components/multichain/multichain-components.scss @@ -8,6 +8,7 @@ @import 'account-list-item/index'; @import 'account-list-menu/index'; @import 'account-picker/index'; +@import 'app-header/app-header'; @import 'multichain-connected-site-menu/index'; @import 'account-list-menu/'; @import 'multichain-token-list-item/multichain-token-list-item'; diff --git a/ui/components/multichain/network-list-menu/network-list-menu.js b/ui/components/multichain/network-list-menu/network-list-menu.js index 0cf1818ca..64a84f9da 100644 --- a/ui/components/multichain/network-list-menu/network-list-menu.js +++ b/ui/components/multichain/network-list-menu/network-list-menu.js @@ -10,6 +10,7 @@ import { showModal, setShowTestNetworks, setProviderType, + toggleNetworkMenu, } from '../../../store/actions'; import { CHAIN_IDS, TEST_CHAINS } from '../../../../shared/constants/network'; import { @@ -30,7 +31,7 @@ import { ENVIRONMENT_TYPE_FULLSCREEN } from '../../../../shared/constants/app'; const UNREMOVABLE_CHAIN_IDS = [CHAIN_IDS.MAINNET, ...TEST_CHAINS]; -export const NetworkListMenu = ({ closeMenu }) => { +export const NetworkListMenu = ({ onClose }) => { const t = useI18nContext(); const networks = useSelector(getAllNetworks); const showTestNetworks = useSelector(getShowTestNetworks); @@ -42,7 +43,7 @@ export const NetworkListMenu = ({ closeMenu }) => { const isFullScreen = environmentType === ENVIRONMENT_TYPE_FULLSCREEN; return ( - + <> {networks.map((network) => { @@ -58,16 +59,17 @@ export const NetworkListMenu = ({ closeMenu }) => { key={network.id || network.chainId} selected={isCurrentNetwork} onClick={() => { + dispatch(toggleNetworkMenu()); if (network.providerType) { dispatch(setProviderType(network.providerType)); } else { dispatch(setActiveNetwork(network.id)); } - closeMenu(); }} onDeleteClick={ canDeleteNetwork ? () => { + dispatch(toggleNetworkMenu()); dispatch( showModal({ name: 'CONFIRM_DELETE_NETWORK', @@ -75,7 +77,6 @@ export const NetworkListMenu = ({ closeMenu }) => { onConfirm: () => undefined, }), ); - closeMenu(); } : null } @@ -104,6 +105,7 @@ export const NetworkListMenu = ({ closeMenu }) => { : global.platform.openExtensionInBrowser( ADD_POPULAR_CUSTOM_NETWORK, ); + dispatch(toggleNetworkMenu()); }} > {t('addNetwork')} @@ -118,5 +120,5 @@ NetworkListMenu.propTypes = { /** * Executes when the menu should be closed */ - closeMenu: PropTypes.func.isRequired, + onClose: PropTypes.func.isRequired, }; diff --git a/ui/components/multichain/network-list-menu/network-list-menu.stories.js b/ui/components/multichain/network-list-menu/network-list-menu.stories.js index 0629cd8e6..f2c5fabfb 100644 --- a/ui/components/multichain/network-list-menu/network-list-menu.stories.js +++ b/ui/components/multichain/network-list-menu/network-list-menu.stories.js @@ -42,8 +42,8 @@ export default { title: 'Components/Multichain/NetworkListMenu', component: NetworkListMenu, argTypes: { - closeMenu: { - action: 'closeMenu', + onClose: { + action: 'onClose', }, }, }; diff --git a/ui/components/multichain/network-list-menu/network-list-menu.test.js b/ui/components/multichain/network-list-menu/network-list-menu.test.js index e87876f39..cd6dae498 100644 --- a/ui/components/multichain/network-list-menu/network-list-menu.test.js +++ b/ui/components/multichain/network-list-menu/network-list-menu.test.js @@ -11,9 +11,11 @@ import { NetworkListMenu } from '.'; const mockSetShowTestNetworks = jest.fn(); const mockSetProviderType = jest.fn(); +const mockToggleNetworkMenu = jest.fn(); jest.mock('../../../store/actions.ts', () => ({ setShowTestNetworks: () => mockSetShowTestNetworks, setProviderType: () => mockSetProviderType, + toggleNetworkMenu: () => mockToggleNetworkMenu, })); const render = (showTestNetworks = false) => { @@ -25,7 +27,7 @@ const render = (showTestNetworks = false) => { }, }, }); - return renderWithProvider(, store); + return renderWithProvider(, store); }; describe('NetworkListMenu', () => { @@ -56,6 +58,7 @@ describe('NetworkListMenu', () => { it('switches networks when an item is clicked', () => { const { getByText } = render(); fireEvent.click(getByText(MAINNET_DISPLAY_NAME)); + expect(mockToggleNetworkMenu).toHaveBeenCalled(); expect(mockSetProviderType).toHaveBeenCalled(); }); }); diff --git a/ui/ducks/metamask/metamask.js b/ui/ducks/metamask/metamask.js index cd3adaca8..7ffa78625 100644 --- a/ui/ducks/metamask/metamask.js +++ b/ui/ducks/metamask/metamask.js @@ -26,6 +26,7 @@ const initialState = { isInitialized: false, isUnlocked: false, isAccountMenuOpen: false, + isNetworkMenuOpen: false, identities: {}, unapprovedTxs: {}, networkConfigurations: {}, @@ -102,6 +103,12 @@ export default function reduceMetamask(state = initialState, action) { isAccountMenuOpen: !metamaskState.isAccountMenuOpen, }; + case actionConstants.TOGGLE_NETWORK_MENU: + return { + ...metamaskState, + isNetworkMenuOpen: !metamaskState.isNetworkMenuOpen, + }; + case actionConstants.UPDATE_TRANSACTION_PARAMS: { const { id: txId, value } = action; let { currentNetworkTxList } = metamaskState; diff --git a/ui/ducks/metamask/metamask.test.js b/ui/ducks/metamask/metamask.test.js index 2ca94f71b..92241d589 100644 --- a/ui/ducks/metamask/metamask.test.js +++ b/ui/ducks/metamask/metamask.test.js @@ -157,6 +157,17 @@ describe('MetaMask Reducers', () => { expect(state.isAccountMenuOpen).toStrictEqual(true); }); + it('toggles network menu', () => { + const state = reduceMetamask( + {}, + { + type: actionConstants.TOGGLE_NETWORK_MENU, + }, + ); + + expect(state.isNetworkMenuOpen).toStrictEqual(true); + }); + it('updates value of tx by id', () => { const oldState = { currentNetworkTxList: [ diff --git a/ui/pages/home/home.component.js b/ui/pages/home/home.component.js index c525bd161..8f874e7db 100644 --- a/ui/pages/home/home.component.js +++ b/ui/pages/home/home.component.js @@ -641,7 +641,7 @@ export default class Home extends PureComponent { ? this.renderPopover() : null}
- + {process.env.MULTICHAIN ? null : }
diff --git a/ui/pages/routes/routes.component.js b/ui/pages/routes/routes.component.js index c5804d146..fae501142 100644 --- a/ui/pages/routes/routes.component.js +++ b/ui/pages/routes/routes.component.js @@ -31,6 +31,11 @@ import AccountMenu from '../../components/app/account-menu'; import { Modal } from '../../components/app/modals'; import Alert from '../../components/ui/alert'; import AppHeader from '../../components/app/app-header'; +import { + AppHeader as MultichainAppHeader, + AccountListMenu, + NetworkListMenu, +} from '../../components/multichain'; import UnlockPage from '../unlock-page'; import Alerts from '../../components/app/alerts'; import Asset from '../asset'; @@ -90,7 +95,6 @@ import { SEND_STAGES } from '../../ducks/send'; import DeprecatedTestNetworks from '../../components/ui/deprecated-test-networks/deprecated-test-networks'; import NewNetworkInfo from '../../components/ui/new-network-info/new-network-info'; import { ThemeType } from '../../../shared/constants/preferences'; -import { AccountListMenu } from '../../components/multichain'; export default class Routes extends Component { static propTypes = { @@ -128,6 +132,8 @@ export default class Routes extends Component { completedOnboarding: PropTypes.bool, isAccountMenuOpen: PropTypes.bool, toggleAccountMenu: PropTypes.func, + isNetworkMenuOpen: PropTypes.bool, + toggleNetworkMenu: PropTypes.func, }; static contextTypes = { @@ -436,6 +442,8 @@ export default class Routes extends Component { completedOnboarding, isAccountMenuOpen, toggleAccountMenu, + isNetworkMenuOpen, + toggleNetworkMenu, } = this.props; const loadMessage = loadingMessage || isNetworkLoading @@ -478,24 +486,30 @@ export default class Routes extends Component { - {!this.hideAppHeader() && ( - - )} + {!this.hideAppHeader() && + (process.env.MULTICHAIN ? ( + + ) : ( + + ))} {this.showOnboardingHeader() && } {completedOnboarding ? : null} {process.env.MULTICHAIN ? null : } {process.env.MULTICHAIN && isAccountMenuOpen ? ( toggleAccountMenu()} /> ) : null} + {process.env.MULTICHAIN && isNetworkMenuOpen ? ( + toggleNetworkMenu()} /> + ) : null}
{isLoading ? : null} {!isLoading && isNetworkLoading ? : null} diff --git a/ui/pages/routes/routes.container.js b/ui/pages/routes/routes.container.js index c132bcfa8..3f5c857ba 100644 --- a/ui/pages/routes/routes.container.js +++ b/ui/pages/routes/routes.container.js @@ -19,6 +19,7 @@ import { setLastActiveTime, setMouseUserState, toggleAccountMenu, + toggleNetworkMenu, } from '../../store/actions'; import { pageChanged } from '../../ducks/history/history'; import { prepareToLeaveSwaps } from '../../ducks/swaps/swaps'; @@ -57,6 +58,7 @@ function mapStateToProps(state) { isCurrentProviderCustom: isCurrentProviderCustom(state), completedOnboarding, isAccountMenuOpen: state.metamask.isAccountMenuOpen, + isNetworkMenuOpen: state.metamask.isNetworkMenuOpen, }; } @@ -70,6 +72,7 @@ function mapDispatchToProps(dispatch) { pageChanged: (path) => dispatch(pageChanged(path)), prepareToLeaveSwaps: () => dispatch(prepareToLeaveSwaps()), toggleAccountMenu: () => dispatch(toggleAccountMenu()), + toggleNetworkMenu: () => dispatch(toggleNetworkMenu()), }; } diff --git a/ui/selectors/selectors.js b/ui/selectors/selectors.js index 1b48a1e84..44d2b914d 100644 --- a/ui/selectors/selectors.js +++ b/ui/selectors/selectors.js @@ -1111,6 +1111,13 @@ export function getNetworkConfigurations(state) { return state.metamask.networkConfigurations; } +export function getCurrentNetwork(state) { + const allNetworks = getAllNetworks(state); + const currentChainId = getCurrentChainId(state); + + return allNetworks.find((network) => network.chainId === currentChainId); +} + export function getAllNetworks(state) { const networkConfigurations = getNetworkConfigurations(state) || {}; const showTestnetNetworks = getShowTestNetworks(state); diff --git a/ui/store/actionConstants.ts b/ui/store/actionConstants.ts index b58276ba9..34e1e9dfc 100644 --- a/ui/store/actionConstants.ts +++ b/ui/store/actionConstants.ts @@ -48,6 +48,7 @@ export const SHOW_LOADING = 'SHOW_LOADING_INDICATION'; export const HIDE_LOADING = 'HIDE_LOADING_INDICATION'; export const TOGGLE_ACCOUNT_MENU = 'TOGGLE_ACCOUNT_MENU'; +export const TOGGLE_NETWORK_MENU = 'TOGGLE_NETWORK_MENU'; // preferences export const UPDATE_CUSTOM_NONCE = 'UPDATE_CUSTOM_NONCE'; diff --git a/ui/store/actions.ts b/ui/store/actions.ts index 97be01ad5..fe47604c9 100644 --- a/ui/store/actions.ts +++ b/ui/store/actions.ts @@ -3122,6 +3122,12 @@ export function toggleAccountMenu() { }; } +export function toggleNetworkMenu() { + return { + type: actionConstants.TOGGLE_NETWORK_MENU, + }; +} + export function setParticipateInMetaMetrics( participationPreference: boolean, ): ThunkAction< From a2bec15c68d11cbf6c6ebc65c901c10328ae663c Mon Sep 17 00:00:00 2001 From: Jyoti Puri Date: Thu, 13 Apr 2023 22:54:33 +0530 Subject: [PATCH 21/36] Improvement on approval pages (#18545) --- .../custom-spending-cap/custom-spending-cap.js | 14 ++++++++++++-- ui/components/ui/form-field/form-field.js | 7 +++++++ .../numeric-input/numeric-input.component.js | 3 +++ .../confirm-approve-content.component.js | 11 +++++++++++ .../confirm-approve-content.component.test.js | 18 ++++++++++++++++++ ui/pages/token-allowance/token-allowance.js | 11 +++++++++++ .../token-allowance/token-allowance.test.js | 18 ++++++++++++++++++ 7 files changed, 80 insertions(+), 2 deletions(-) diff --git a/ui/components/app/custom-spending-cap/custom-spending-cap.js b/ui/components/app/custom-spending-cap/custom-spending-cap.js index 12f1ddce4..7f4a46c26 100644 --- a/ui/components/app/custom-spending-cap/custom-spending-cap.js +++ b/ui/components/app/custom-spending-cap/custom-spending-cap.js @@ -1,4 +1,4 @@ -import React, { useState, useContext, useEffect } from 'react'; +import React, { useState, useContext, useEffect, useRef } from 'react'; import { useDispatch, useSelector } from 'react-redux'; import PropTypes from 'prop-types'; import classnames from 'classnames'; @@ -41,6 +41,7 @@ export default function CustomSpendingCap({ }) { const t = useContext(I18nContext); const dispatch = useDispatch(); + const inputRef = useRef(null); const value = useSelector(getCustomTokenAmount); @@ -139,6 +140,15 @@ export default function CustomSpendingCap({ passTheErrorText(error); }, [error, passTheErrorText]); + useEffect(() => { + if (inputRef.current) { + inputRef.current.focus({ + preventScroll: true, + }); + } + // eslint-disable-next-line react-hooks/exhaustive-deps + }, [inputRef.current]); + const chooseTooltipContentText = decConversionGreaterThan( value, currentTokenBalance, @@ -182,8 +192,8 @@ export default function CustomSpendingCap({ } > ) : ( )} @@ -285,4 +288,8 @@ FormField.propTypes = { * If used ensure the id prop is set on the input and a label element is present using htmlFor with the same id to ensure accessibility. */ wrappingLabelProps: PropTypes.object, + /** + * ref for input component + */ + inputRef: PropTypes.object, }; diff --git a/ui/components/ui/numeric-input/numeric-input.component.js b/ui/components/ui/numeric-input/numeric-input.component.js index e7ac094c9..8d16351a8 100644 --- a/ui/components/ui/numeric-input/numeric-input.component.js +++ b/ui/components/ui/numeric-input/numeric-input.component.js @@ -20,6 +20,7 @@ export default function NumericInput({ placeholder, id, name, + inputRef, }) { return (
{detailText && ( + {(txData?.securityProviderResponse?.flagAsDangerous !== undefined && + txData?.securityProviderResponse?.flagAsDangerous !== + SECURITY_PROVIDER_MESSAGE_SEVERITIES.NOT_MALICIOUS) || + (txData?.securityProviderResponse && + Object.keys(txData.securityProviderResponse).length === 0) ? ( + + ) : null} {warning && (
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 1b89d136d..c82da63b8 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 @@ -325,4 +325,22 @@ describe('ConfirmApproveContent Component', () => { expect(container).toMatchSnapshot(); }); + + it('should render security provider response if transaction is malicious', () => { + const securityProviderResponse = { + flagAsDangerous: 1, + reason: + 'This has been flagged as potentially suspicious. If you sign, you could lose access to all of your NFTs and any funds or other assets in your wallet.', + reason_header: 'Warning', + }; + const { getByText } = renderComponent({ + ...props, + txData: { + ...props.txData, + securityProviderResponse, + }, + }); + + expect(getByText(securityProviderResponse.reason)).toBeInTheDocument(); + }); }); diff --git a/ui/pages/token-allowance/token-allowance.js b/ui/pages/token-allowance/token-allowance.js index ef1583ef0..7ee7fd887 100644 --- a/ui/pages/token-allowance/token-allowance.js +++ b/ui/pages/token-allowance/token-allowance.js @@ -67,6 +67,8 @@ import { ICON_NAMES, } from '../../components/component-library/icon/deprecated'; import LedgerInstructionField from '../../components/app/ledger-instruction-field/ledger-instruction-field'; +import { SECURITY_PROVIDER_MESSAGE_SEVERITIES } from '../../components/app/security-provider-banner-message/security-provider-banner-message.constants'; +import SecurityProviderBannerMessage from '../../components/app/security-provider-banner-message/security-provider-banner-message'; const ALLOWED_HOSTS = ['portfolio.metamask.io']; @@ -272,6 +274,15 @@ export default function TokenAllowance({ + {(txData?.securityProviderResponse?.flagAsDangerous !== undefined && + txData?.securityProviderResponse?.flagAsDangerous !== + SECURITY_PROVIDER_MESSAGE_SEVERITIES.NOT_MALICIOUS) || + (txData?.securityProviderResponse && + Object.keys(txData.securityProviderResponse).length === 0) ? ( + + ) : null} { expect(queryByText('Prior to clicking confirm:')).toBeNull(); }); + + it('should render security provider response if transaction is malicious', () => { + const securityProviderResponse = { + flagAsDangerous: 1, + reason: + 'This has been flagged as potentially suspicious. If you sign, you could lose access to all of your NFTs and any funds or other assets in your wallet.', + reason_header: 'Warning', + }; + const { getByText } = renderWithProvider( + , + store, + ); + + expect(getByText(securityProviderResponse.reason)).toBeInTheDocument(); + }); }); From 8becb57d9e6a569536ba0be4c4d8596b51e35da1 Mon Sep 17 00:00:00 2001 From: Nidhi Kumari Date: Thu, 13 Apr 2023 23:07:33 +0530 Subject: [PATCH 22/36] updated copy Address (#18557) Co-authored-by: Danica Shen --- ui/components/app/wallet-overview/wallet-overview.js | 12 +++++++++++- 1 file changed, 11 insertions(+), 1 deletion(-) diff --git a/ui/components/app/wallet-overview/wallet-overview.js b/ui/components/app/wallet-overview/wallet-overview.js index 9265e31b0..5be0ef73b 100644 --- a/ui/components/app/wallet-overview/wallet-overview.js +++ b/ui/components/app/wallet-overview/wallet-overview.js @@ -1,12 +1,22 @@ import React from 'react'; import PropTypes from 'prop-types'; import classnames from 'classnames'; +import { useSelector } from 'react-redux'; +import { toChecksumHexAddress } from '../../../../shared/modules/hexstring-utils'; +import { getSelectedIdentity } from '../../../selectors'; +import { AddressCopyButton } from '../../multichain'; const WalletOverview = ({ balance, buttons, className, icon, loading }) => { + const selectedIdentity = useSelector(getSelectedIdentity); + const checksummedAddress = toChecksumHexAddress(selectedIdentity?.address); return (
- {loading ? null : icon} + {process.env.MULTICHAIN ? ( + + ) : ( + <>{loading ? null : icon} + )} {balance}
{buttons}
From e09028b4e575a6ea95f2099f0998681bd2392b94 Mon Sep 17 00:00:00 2001 From: Garrett Bear Date: Thu, 13 Apr 2023 15:24:47 -0700 Subject: [PATCH 23/36] Update multichain Icon imports (#18536) * Update multichain Icon imports * replace ButtonIcon with updated version * Buttons with Icon TODO added * update header to use TS version Icon --- .../account-list-item-menu.js | 9 ++++----- .../account-list-item/account-list-item.js | 12 +++++------- .../account-list-menu/account-list-menu.js | 1 + .../multichain/account-picker/account-picker.js | 5 +++-- .../address-copy-button/address-copy-button.js | 1 + ui/components/multichain/app-header/app-header.js | 13 +++++++++---- .../multichain/global-menu/global-menu.js | 14 +++++++------- .../multichain-connected-site-menu.js | 5 ++--- .../multichain-import-token-link.js | 1 + .../network-list-item/network-list-item.js | 11 +++++++---- ui/components/ui/menu/menu-item.js | 5 ++--- 11 files changed, 42 insertions(+), 35 deletions(-) diff --git a/ui/components/multichain/account-list-item-menu/account-list-item-menu.js b/ui/components/multichain/account-list-item-menu/account-list-item-menu.js index ac65bcd5b..df3cece9a 100644 --- a/ui/components/multichain/account-list-item-menu/account-list-item-menu.js +++ b/ui/components/multichain/account-list-item-menu/account-list-item-menu.js @@ -12,8 +12,7 @@ import { } from '../../../selectors'; import { NETWORKS_ROUTE } from '../../../helpers/constants/routes'; import { Menu, MenuItem } from '../../ui/menu'; -import { Text } from '../../component-library'; -import { ICON_NAMES } from '../../component-library/icon/deprecated'; +import { Text, IconName } from '../../component-library'; import { MetaMetricsEventCategory, MetaMetricsEventLinkType, @@ -74,7 +73,7 @@ export const AccountListItemMenu = ({ : openBlockExplorer } subtitle={blockExplorerUrlSubTitle || null} - iconName={ICON_NAMES.EXPORT} + iconName={IconName.Export} data-testid="account-list-menu-open-explorer" > {t('viewOnExplorer')} @@ -92,7 +91,7 @@ export const AccountListItemMenu = ({ onClose(); closeMenu?.(); }} - iconName={ICON_NAMES.SCAN_BARCODE} + iconName={IconName.ScanBarcode} > {t('accountDetails')} @@ -108,7 +107,7 @@ export const AccountListItemMenu = ({ ); onClose(); }} - iconName={ICON_NAMES.TRASH} + iconName={IconName.Trash} > {t('removeAccount')} diff --git a/ui/components/multichain/account-list-item/account-list-item.js b/ui/components/multichain/account-list-item/account-list-item.js index deeddff19..05a33ea0a 100644 --- a/ui/components/multichain/account-list-item/account-list-item.js +++ b/ui/components/multichain/account-list-item/account-list-item.js @@ -15,12 +15,10 @@ import { AvatarFavicon, Tag, ButtonLink, + ButtonIcon, + IconName, + IconSize, } from '../../component-library'; -import { ButtonIcon } from '../../component-library/button-icon/deprecated'; -import { - ICON_NAMES, - ICON_SIZES, -} from '../../component-library/icon/deprecated'; import { Color, TEXT_ALIGN, @@ -197,8 +195,8 @@ export const AccountListItem = ({
{ e.stopPropagation(); setAccountOptionsMenuOpen(true); diff --git a/ui/components/multichain/account-list-menu/account-list-menu.js b/ui/components/multichain/account-list-menu/account-list-menu.js index 7ffbcf5fa..aea2e81c6 100644 --- a/ui/components/multichain/account-list-menu/account-list-menu.js +++ b/ui/components/multichain/account-list-menu/account-list-menu.js @@ -5,6 +5,7 @@ import Fuse from 'fuse.js'; import { useDispatch, useSelector } from 'react-redux'; import Box from '../../ui/box/box'; import { ButtonLink, TextFieldSearch, Text } from '../../component-library'; +// TODO: Replace ICON_NAMES with IconName when ButtonBase/Buttons have been updated import { ICON_NAMES } from '../../component-library/icon/deprecated'; import { AccountListItem } from '..'; import { diff --git a/ui/components/multichain/account-picker/account-picker.js b/ui/components/multichain/account-picker/account-picker.js index 72b522893..5701249a1 100644 --- a/ui/components/multichain/account-picker/account-picker.js +++ b/ui/components/multichain/account-picker/account-picker.js @@ -5,9 +5,10 @@ import { Button, AvatarAccount, AvatarAccountVariant, + Icon, + IconName, Text, } from '../../component-library'; -import { ICON_NAMES, Icon } from '../../component-library/icon/deprecated'; import { AlignItems, BackgroundColor, @@ -47,7 +48,7 @@ export const AccountPicker = ({ address, name, onClick }) => { {name} diff --git a/ui/components/multichain/address-copy-button/address-copy-button.js b/ui/components/multichain/address-copy-button/address-copy-button.js index ec15b3497..c71f8ba3d 100644 --- a/ui/components/multichain/address-copy-button/address-copy-button.js +++ b/ui/components/multichain/address-copy-button/address-copy-button.js @@ -2,6 +2,7 @@ import React from 'react'; import PropTypes from 'prop-types'; import classnames from 'classnames'; import { ButtonBase } from '../../component-library'; +// TODO: Replace ICON_NAMES with IconName when ButtonBase/Buttons have been updated import { ICON_NAMES } from '../../component-library/icon/deprecated'; import { BackgroundColor, diff --git a/ui/components/multichain/app-header/app-header.js b/ui/components/multichain/app-header/app-header.js index d7f278530..61662c503 100644 --- a/ui/components/multichain/app-header/app-header.js +++ b/ui/components/multichain/app-header/app-header.js @@ -21,9 +21,14 @@ import { JustifyContent, Size, } from '../../../helpers/constants/design-system'; -import { AvatarNetwork, Button, PickerNetwork } from '../../component-library'; -import { ButtonIcon } from '../../component-library/button-icon/deprecated'; -import { ICON_NAMES } from '../../component-library/icon/deprecated'; +import { + AvatarNetwork, + Button, + ButtonIcon, + IconName, + PickerNetwork, +} from '../../component-library'; + import { getCurrentNetwork, getOriginOfCurrentTab, @@ -149,7 +154,7 @@ export const AppHeader = ({ onClick }) => { width={BLOCK_SIZES.FULL} > { diff --git a/ui/components/multichain/global-menu/global-menu.js b/ui/components/multichain/global-menu/global-menu.js index 7500c3046..2d89cf688 100644 --- a/ui/components/multichain/global-menu/global-menu.js +++ b/ui/components/multichain/global-menu/global-menu.js @@ -9,7 +9,7 @@ import { } from '../../../helpers/constants/routes'; import { lockMetamask } from '../../../store/actions'; import { useI18nContext } from '../../../hooks/useI18nContext'; -import { ICON_NAMES } from '../../component-library/icon/deprecated'; +import { IconName } from '../../component-library'; import { Menu, MenuItem } from '../../ui/menu'; import { getEnvironmentType } from '../../../../app/scripts/lib/util'; import { ENVIRONMENT_TYPE_FULLSCREEN } from '../../../../shared/constants/app'; @@ -31,7 +31,7 @@ export const GlobalMenu = ({ closeMenu, anchorElement }) => { return ( { history.push(CONNECTED_ROUTE); trackEvent({ @@ -47,7 +47,7 @@ export const GlobalMenu = ({ closeMenu, anchorElement }) => { {t('connectedSites')} { const portfolioUrl = process.env.PORTFOLIO_URL; global.platform.openTab({ @@ -75,7 +75,7 @@ export const GlobalMenu = ({ closeMenu, anchorElement }) => { {getEnvironmentType() === ENVIRONMENT_TYPE_FULLSCREEN ? null : ( { global.platform.openExtensionInBrowser(); trackEvent({ @@ -93,7 +93,7 @@ export const GlobalMenu = ({ closeMenu, anchorElement }) => { )} { global.platform.openTab({ url: SUPPORT_LINK }); trackEvent( @@ -117,7 +117,7 @@ export const GlobalMenu = ({ closeMenu, anchorElement }) => { {t('support')} { history.push(SETTINGS_ROUTE); trackEvent({ @@ -133,7 +133,7 @@ export const GlobalMenu = ({ closeMenu, anchorElement }) => { {t('settings')} { dispatch(lockMetamask()); history.push(DEFAULT_ROUTE); diff --git a/ui/components/multichain/multichain-connected-site-menu/multichain-connected-site-menu.js b/ui/components/multichain/multichain-connected-site-menu/multichain-connected-site-menu.js index e0303dd0b..abc22247e 100644 --- a/ui/components/multichain/multichain-connected-site-menu/multichain-connected-site-menu.js +++ b/ui/components/multichain/multichain-connected-site-menu/multichain-connected-site-menu.js @@ -13,8 +13,7 @@ import { IconColor, Size, } from '../../../helpers/constants/design-system'; -import { BadgeWrapper } from '../../component-library'; -import { Icon, ICON_NAMES } from '../../component-library/icon/deprecated'; +import { BadgeWrapper, Icon, IconName } from '../../component-library'; import Box from '../../ui/box'; import { getSelectedIdentity } from '../../../selectors'; import Tooltip from '../../ui/tooltip'; @@ -69,7 +68,7 @@ export const MultichainConnectedSiteMenu = ({ } > diff --git a/ui/components/multichain/multichain-import-token-link/multichain-import-token-link.js b/ui/components/multichain/multichain-import-token-link/multichain-import-token-link.js index d2ced700b..34ad99dba 100644 --- a/ui/components/multichain/multichain-import-token-link/multichain-import-token-link.js +++ b/ui/components/multichain/multichain-import-token-link/multichain-import-token-link.js @@ -5,6 +5,7 @@ import PropTypes from 'prop-types'; import classnames from 'classnames'; import Box from '../../ui/box/box'; import { ButtonLink } from '../../component-library'; +// TODO: Replace ICON_NAMES with IconName when ButtonBase/Buttons have been updated import { ICON_NAMES } from '../../component-library/icon/deprecated'; import { AlignItems, diff --git a/ui/components/multichain/network-list-item/network-list-item.js b/ui/components/multichain/network-list-item/network-list-item.js index 3dedbfdd3..d21832732 100644 --- a/ui/components/multichain/network-list-item/network-list-item.js +++ b/ui/components/multichain/network-list-item/network-list-item.js @@ -12,9 +12,12 @@ import { TextColor, BLOCK_SIZES, } from '../../../helpers/constants/design-system'; -import { AvatarNetwork, ButtonLink } from '../../component-library'; -import { ButtonIcon } from '../../component-library/button-icon/deprecated'; -import { ICON_NAMES } from '../../component-library/icon/deprecated'; +import { + AvatarNetwork, + ButtonIcon, + ButtonLink, + IconName, +} from '../../component-library'; import { useI18nContext } from '../../../hooks/useI18nContext'; import Tooltip from '../../ui/tooltip/tooltip'; @@ -68,7 +71,7 @@ export const NetworkListItem = ({ { diff --git a/ui/components/ui/menu/menu-item.js b/ui/components/ui/menu/menu-item.js index 3c8040f9c..6e005e1ea 100644 --- a/ui/components/ui/menu/menu-item.js +++ b/ui/components/ui/menu/menu-item.js @@ -2,8 +2,7 @@ import React from 'react'; import PropTypes from 'prop-types'; import classnames from 'classnames'; -import { Text } from '../../component-library'; -import { Icon, ICON_SIZES } from '../../component-library/icon/deprecated'; +import { Text, Icon, IconSize } from '../../component-library'; import { TextVariant } from '../../../helpers/constants/design-system'; const MenuItem = ({ @@ -20,7 +19,7 @@ const MenuItem = ({ onClick={onClick} > {iconName ? ( - + ) : null}
{children}
From 5d2c4c143a2908313805cf6a1b28a4f4dfcc6b15 Mon Sep 17 00:00:00 2001 From: legobeat <109787230+legobeat@users.noreply.github.com> Date: Fri, 14 Apr 2023 10:49:22 +0900 Subject: [PATCH 24/36] devdeps: mocha@7.2.0->9.2.2 (#18195) * devdeps: mocha@7.2.0->9.2.2 Maintenance upgrade - Closes subdependency flat advisory - CVE-2020-36632 / GHSA-2j2x-2gpw-d8fm - upgrade eslint-plugin-mocha to match - previously used `eslint-plugin-mocha` depended on mocha@^8.2.0 * devdeps: patch-bump ansi-regex closes GHSA-93q8-gq69-wqmw * update lavamoat policies --- lavamoat/browserify/beta/policy.json | 10 +- lavamoat/browserify/desktop/policy.json | 12 +- lavamoat/browserify/flask/policy.json | 12 +- lavamoat/browserify/main/policy.json | 10 +- lavamoat/build-system/policy.json | 234 +++++----- package.json | 4 +- yarn.lock | 583 +++++++++--------------- 7 files changed, 349 insertions(+), 516 deletions(-) diff --git a/lavamoat/browserify/beta/policy.json b/lavamoat/browserify/beta/policy.json index e594e00f6..6f5312b9d 100644 --- a/lavamoat/browserify/beta/policy.json +++ b/lavamoat/browserify/beta/policy.json @@ -1054,7 +1054,7 @@ "@metamask/eth-token-tracker>deep-equal>is-date-object": true, "@ngraveio/bc-ur>assert>object-is": true, "@storybook/api>telejson>is-regex": true, - "mocha>object.assign>object-keys": true, + "globalthis>define-properties>object-keys": true, "string.prototype.matchall>regexp.prototype.flags": true } }, @@ -2484,7 +2484,7 @@ }, "browserify>has": { "packages": { - "mocha>object.assign>function-bind": true + "browserify>has>function-bind": true } }, "browserify>os-browserify": { @@ -3539,7 +3539,7 @@ "globalthis>define-properties": { "packages": { "globalthis>define-properties>has-property-descriptors": true, - "mocha>object.assign>object-keys": true + "globalthis>define-properties>object-keys": true } }, "globalthis>define-properties>has-property-descriptors": { @@ -4159,7 +4159,7 @@ }, "string.prototype.matchall>call-bind": { "packages": { - "mocha>object.assign>function-bind": true, + "browserify>has>function-bind": true, "string.prototype.matchall>get-intrinsic": true } }, @@ -4171,7 +4171,7 @@ }, "packages": { "browserify>has": true, - "mocha>object.assign>function-bind": true, + "browserify>has>function-bind": true, "string.prototype.matchall>has-symbols": true } }, diff --git a/lavamoat/browserify/desktop/policy.json b/lavamoat/browserify/desktop/policy.json index 16b051712..ac604bd4d 100644 --- a/lavamoat/browserify/desktop/policy.json +++ b/lavamoat/browserify/desktop/policy.json @@ -1126,7 +1126,7 @@ "@metamask/eth-token-tracker>deep-equal>is-date-object": true, "@ngraveio/bc-ur>assert>object-is": true, "@storybook/api>telejson>is-regex": true, - "mocha>object.assign>object-keys": true, + "globalthis>define-properties>object-keys": true, "string.prototype.matchall>regexp.prototype.flags": true } }, @@ -2877,7 +2877,7 @@ }, "browserify>has": { "packages": { - "mocha>object.assign>function-bind": true + "browserify>has>function-bind": true } }, "browserify>os-browserify": { @@ -3932,7 +3932,7 @@ "globalthis>define-properties": { "packages": { "globalthis>define-properties>has-property-descriptors": true, - "mocha>object.assign>object-keys": true + "globalthis>define-properties>object-keys": true } }, "globalthis>define-properties>has-property-descriptors": { @@ -4346,9 +4346,9 @@ "react-markdown>unified": { "packages": { "jsdom>request>extend": true, + "mocha>yargs-unparser>is-plain-obj": true, "react-markdown>unified>bail": true, "react-markdown>unified>is-buffer": true, - "react-markdown>unified>is-plain-obj": true, "react-markdown>unified>trough": true, "react-markdown>vfile": true } @@ -4684,7 +4684,7 @@ }, "string.prototype.matchall>call-bind": { "packages": { - "mocha>object.assign>function-bind": true, + "browserify>has>function-bind": true, "string.prototype.matchall>get-intrinsic": true } }, @@ -4696,7 +4696,7 @@ }, "packages": { "browserify>has": true, - "mocha>object.assign>function-bind": true, + "browserify>has>function-bind": true, "string.prototype.matchall>has-symbols": true } }, diff --git a/lavamoat/browserify/flask/policy.json b/lavamoat/browserify/flask/policy.json index 16b051712..ac604bd4d 100644 --- a/lavamoat/browserify/flask/policy.json +++ b/lavamoat/browserify/flask/policy.json @@ -1126,7 +1126,7 @@ "@metamask/eth-token-tracker>deep-equal>is-date-object": true, "@ngraveio/bc-ur>assert>object-is": true, "@storybook/api>telejson>is-regex": true, - "mocha>object.assign>object-keys": true, + "globalthis>define-properties>object-keys": true, "string.prototype.matchall>regexp.prototype.flags": true } }, @@ -2877,7 +2877,7 @@ }, "browserify>has": { "packages": { - "mocha>object.assign>function-bind": true + "browserify>has>function-bind": true } }, "browserify>os-browserify": { @@ -3932,7 +3932,7 @@ "globalthis>define-properties": { "packages": { "globalthis>define-properties>has-property-descriptors": true, - "mocha>object.assign>object-keys": true + "globalthis>define-properties>object-keys": true } }, "globalthis>define-properties>has-property-descriptors": { @@ -4346,9 +4346,9 @@ "react-markdown>unified": { "packages": { "jsdom>request>extend": true, + "mocha>yargs-unparser>is-plain-obj": true, "react-markdown>unified>bail": true, "react-markdown>unified>is-buffer": true, - "react-markdown>unified>is-plain-obj": true, "react-markdown>unified>trough": true, "react-markdown>vfile": true } @@ -4684,7 +4684,7 @@ }, "string.prototype.matchall>call-bind": { "packages": { - "mocha>object.assign>function-bind": true, + "browserify>has>function-bind": true, "string.prototype.matchall>get-intrinsic": true } }, @@ -4696,7 +4696,7 @@ }, "packages": { "browserify>has": true, - "mocha>object.assign>function-bind": true, + "browserify>has>function-bind": true, "string.prototype.matchall>has-symbols": true } }, diff --git a/lavamoat/browserify/main/policy.json b/lavamoat/browserify/main/policy.json index e594e00f6..6f5312b9d 100644 --- a/lavamoat/browserify/main/policy.json +++ b/lavamoat/browserify/main/policy.json @@ -1054,7 +1054,7 @@ "@metamask/eth-token-tracker>deep-equal>is-date-object": true, "@ngraveio/bc-ur>assert>object-is": true, "@storybook/api>telejson>is-regex": true, - "mocha>object.assign>object-keys": true, + "globalthis>define-properties>object-keys": true, "string.prototype.matchall>regexp.prototype.flags": true } }, @@ -2484,7 +2484,7 @@ }, "browserify>has": { "packages": { - "mocha>object.assign>function-bind": true + "browserify>has>function-bind": true } }, "browserify>os-browserify": { @@ -3539,7 +3539,7 @@ "globalthis>define-properties": { "packages": { "globalthis>define-properties>has-property-descriptors": true, - "mocha>object.assign>object-keys": true + "globalthis>define-properties>object-keys": true } }, "globalthis>define-properties>has-property-descriptors": { @@ -4159,7 +4159,7 @@ }, "string.prototype.matchall>call-bind": { "packages": { - "mocha>object.assign>function-bind": true, + "browserify>has>function-bind": true, "string.prototype.matchall>get-intrinsic": true } }, @@ -4171,7 +4171,7 @@ }, "packages": { "browserify>has": true, - "mocha>object.assign>function-bind": true, + "browserify>has>function-bind": true, "string.prototype.matchall>has-symbols": true } }, diff --git a/lavamoat/build-system/policy.json b/lavamoat/build-system/policy.json index 871dcc172..b3d9ce0d5 100644 --- a/lavamoat/build-system/policy.json +++ b/lavamoat/build-system/policy.json @@ -1697,7 +1697,7 @@ }, "browserify>has": { "packages": { - "mocha>object.assign>function-bind": true + "browserify>has>function-bind": true } }, "browserify>insert-module-globals": { @@ -1885,12 +1885,7 @@ "process.platform": true }, "packages": { - "chalk>supports-color>has-flag": true - } - }, - "chalk>supports-color>has-flag": { - "globals": { - "process.argv": true + "sinon>supports-color>has-flag": true } }, "chokidar": { @@ -2002,7 +1997,7 @@ "packages": { "cross-spawn>path-key": true, "cross-spawn>shebang-command": true, - "cross-spawn>which": true + "mocha>which": true } }, "cross-spawn>path-key": { @@ -2016,21 +2011,6 @@ "cross-spawn>shebang-command>shebang-regex": true } }, - "cross-spawn>which": { - "builtin": { - "path.join": true - }, - "globals": { - "process.cwd": true, - "process.env.OSTYPE": true, - "process.env.PATH": true, - "process.env.PATHEXT": true, - "process.platform": true - }, - "packages": { - "mocha>which>isexe": true - } - }, "debounce-stream>duplexer": { "builtin": { "stream": true @@ -2350,7 +2330,7 @@ "process": true }, "packages": { - "gulp-livereload>debug>ms": true, + "mocha>ms": true, "mocha>supports-color": true } }, @@ -2471,7 +2451,7 @@ "process": true }, "packages": { - "gulp-livereload>debug>ms": true, + "mocha>ms": true, "mocha>supports-color": true } }, @@ -2871,8 +2851,8 @@ "eslint>@eslint/eslintrc>globals": true, "eslint>ajv": true, "eslint>minimatch": true, - "eslint>strip-json-comments": true, "globby>ignore": true, + "mocha>strip-json-comments": true, "nock>debug": true } }, @@ -3094,9 +3074,9 @@ "process.cwd": true }, "packages": { - "chokidar>glob-parent": true, "fast-glob>@nodelib/fs.stat": true, "fast-glob>@nodelib/fs.walk": true, + "fast-glob>glob-parent": true, "globby>merge2": true, "stylelint>micromatch": true } @@ -3151,6 +3131,15 @@ "fast-glob>@nodelib/fs.walk>fastq>reusify": true } }, + "fast-glob>glob-parent": { + "builtin": { + "os.platform": true, + "path.posix.dirname": true + }, + "packages": { + "eslint>is-glob": true + } + }, "fs-extra": { "builtin": { "assert": true, @@ -3200,7 +3189,7 @@ "globalthis>define-properties": { "packages": { "globalthis>define-properties>has-property-descriptors": true, - "mocha>object.assign>object-keys": true + "globalthis>define-properties>object-keys": true } }, "globalthis>define-properties>has-property-descriptors": { @@ -3370,8 +3359,8 @@ }, "packages": { "gulp-dart-sass>chalk>ansi-styles": true, - "gulp-dart-sass>chalk>supports-color": true, - "mocha>escape-string-regexp": true + "gulp-dart-sass>chalk>escape-string-regexp": true, + "gulp-dart-sass>chalk>supports-color": true } }, "gulp-dart-sass>chalk>ansi-styles": { @@ -3391,7 +3380,12 @@ "process.versions.node.split": true }, "packages": { - "mocha>supports-color>has-flag": true + "gulp-dart-sass>chalk>supports-color>has-flag": true + } + }, + "gulp-dart-sass>chalk>supports-color>has-flag": { + "globals": { + "process.argv": true } }, "gulp-dart-sass>strip-ansi": { @@ -3430,9 +3424,9 @@ "process.platform": true }, "packages": { + "gulp-dart-sass>chalk>escape-string-regexp": true, "gulp-livereload>chalk>ansi-styles": true, - "gulp-livereload>chalk>supports-color": true, - "mocha>escape-string-regexp": true + "gulp-livereload>chalk>supports-color": true } }, "gulp-livereload>chalk>ansi-styles": { @@ -3452,7 +3446,12 @@ "process.versions.node.split": true }, "packages": { - "mocha>supports-color>has-flag": true + "gulp-livereload>chalk>supports-color>has-flag": true + } + }, + "gulp-livereload>chalk>supports-color>has-flag": { + "globals": { + "process.argv": true } }, "gulp-livereload>debug": { @@ -3469,7 +3468,7 @@ }, "packages": { "gulp-livereload>chalk>supports-color": true, - "gulp-livereload>debug>ms": true + "mocha>ms": true } }, "gulp-livereload>event-stream": { @@ -3595,7 +3594,7 @@ "process": true }, "packages": { - "gulp-livereload>debug>ms": true, + "mocha>ms": true, "mocha>supports-color": true } }, @@ -3699,9 +3698,9 @@ "process.platform": true }, "packages": { + "gulp-dart-sass>chalk>escape-string-regexp": true, "gulp-rtlcss>rtlcss>chalk>ansi-styles": true, - "gulp-rtlcss>rtlcss>chalk>supports-color": true, - "mocha>escape-string-regexp": true + "gulp-rtlcss>rtlcss>chalk>supports-color": true } }, "gulp-rtlcss>rtlcss>chalk>ansi-styles": { @@ -3721,7 +3720,12 @@ "process.versions.node.split": true }, "packages": { - "mocha>supports-color>has-flag": true + "gulp-rtlcss>rtlcss>chalk>supports-color>has-flag": true + } + }, + "gulp-rtlcss>rtlcss>chalk>supports-color>has-flag": { + "globals": { + "process.argv": true } }, "gulp-rtlcss>rtlcss>postcss": { @@ -3947,7 +3951,7 @@ "process": true }, "packages": { - "gulp-livereload>debug>ms": true, + "mocha>ms": true, "mocha>supports-color": true } }, @@ -5204,11 +5208,11 @@ "chokidar>normalize-path": true, "eslint>is-glob": true, "gulp-watch>chokidar>async-each": true, + "gulp-watch>glob-parent": true, "gulp-watch>path-is-absolute": true, "gulp>glob-watcher>anymatch": true, "gulp>glob-watcher>chokidar>braces": true, "gulp>glob-watcher>chokidar>fsevents": true, - "gulp>glob-watcher>chokidar>glob-parent": true, "gulp>glob-watcher>chokidar>is-binary-path": true, "gulp>glob-watcher>chokidar>readdirp": true, "gulp>glob-watcher>chokidar>upath": true, @@ -5274,21 +5278,6 @@ "gulp-watch>chokidar>fsevents>node-pre-gyp": true } }, - "gulp>glob-watcher>chokidar>glob-parent": { - "builtin": { - "os.platform": true, - "path": true - }, - "packages": { - "gulp-watch>glob-parent>path-dirname": true, - "gulp>glob-watcher>chokidar>glob-parent>is-glob": true - } - }, - "gulp>glob-watcher>chokidar>glob-parent>is-glob": { - "packages": { - "gulp>glob-watcher>chokidar>glob-parent>is-glob>is-extglob": true - } - }, "gulp>glob-watcher>chokidar>is-binary-path": { "builtin": { "path.extname": true @@ -5663,8 +5652,8 @@ "process.nextTick": true }, "packages": { + "gulp-watch>glob-parent": true, "gulp>glob-watcher>is-negated-glob": true, - "gulp>vinyl-fs>glob-stream>glob-parent": true, "gulp>vinyl-fs>glob-stream>ordered-read-streams": true, "gulp>vinyl-fs>glob-stream>pumpify": true, "gulp>vinyl-fs>glob-stream>to-absolute-glob": true, @@ -5675,21 +5664,6 @@ "vinyl>remove-trailing-separator": true } }, - "gulp>vinyl-fs>glob-stream>glob-parent": { - "builtin": { - "os.platform": true, - "path": true - }, - "packages": { - "gulp-watch>glob-parent>path-dirname": true, - "gulp>vinyl-fs>glob-stream>glob-parent>is-glob": true - } - }, - "gulp>vinyl-fs>glob-stream>glob-parent>is-glob": { - "packages": { - "gulp>vinyl-fs>glob-stream>glob-parent>is-glob>is-extglob": true - } - }, "gulp>vinyl-fs>glob-stream>ordered-read-streams": { "builtin": { "util.inherits": true @@ -5791,7 +5765,7 @@ "gulp>vinyl-fs>object.assign": { "packages": { "globalthis>define-properties": true, - "mocha>object.assign>object-keys": true, + "globalthis>define-properties>object-keys": true, "string.prototype.matchall>call-bind": true, "string.prototype.matchall>has-symbols": true } @@ -6092,9 +6066,9 @@ "process.platform": true }, "packages": { + "gulp-dart-sass>chalk>escape-string-regexp": true, "lavamoat>@babel/highlight>chalk>ansi-styles": true, - "lavamoat>@babel/highlight>chalk>supports-color": true, - "mocha>escape-string-regexp": true + "lavamoat>@babel/highlight>chalk>supports-color": true } }, "lavamoat>@babel/highlight>chalk>ansi-styles": { @@ -6114,7 +6088,12 @@ "process.versions.node.split": true }, "packages": { - "mocha>supports-color>has-flag": true + "lavamoat>@babel/highlight>chalk>supports-color>has-flag": true + } + }, + "lavamoat>@babel/highlight>chalk>supports-color>has-flag": { + "globals": { + "process.argv": true } }, "lavamoat>@lavamoat/aa": { @@ -6272,6 +6251,31 @@ "process.platform": true } }, + "mocha>log-symbols": { + "packages": { + "madge>ora>is-unicode-supported": true, + "mocha>log-symbols>chalk": true + } + }, + "mocha>log-symbols>chalk": { + "packages": { + "chalk>ansi-styles": true, + "mocha>log-symbols>chalk>supports-color": true + } + }, + "mocha>log-symbols>chalk>supports-color": { + "builtin": { + "os.release": true, + "tty.isatty": true + }, + "globals": { + "process.env": true, + "process.platform": true + }, + "packages": { + "sinon>supports-color>has-flag": true + } + }, "mocha>minimatch>brace-expansion": { "packages": { "mocha>minimatch>brace-expansion>concat-map": true, @@ -6280,22 +6284,15 @@ }, "mocha>supports-color": { "builtin": { - "os.release": true + "os.release": true, + "tty.isatty": true }, "globals": { "process.env": true, - "process.platform": true, - "process.stderr": true, - "process.stdout": true, - "process.versions.node.split": true + "process.platform": true }, "packages": { - "mocha>supports-color>has-flag": true - } - }, - "mocha>supports-color>has-flag": { - "globals": { - "process.argv": true + "sinon>supports-color>has-flag": true } }, "mocha>which": { @@ -6565,9 +6562,9 @@ "react-markdown>unified": { "packages": { "jsdom>request>extend": true, + "mocha>yargs-unparser>is-plain-obj": true, "react-markdown>unified>bail": true, "react-markdown>unified>is-buffer": true, - "react-markdown>unified>is-plain-obj": true, "react-markdown>unified>trough": true, "react-markdown>vfile": true } @@ -6767,6 +6764,11 @@ "process.platform": true } }, + "sinon>supports-color>has-flag": { + "globals": { + "process.argv": true + } + }, "source-map": { "builtin": { "fs.readFile": true, @@ -6801,7 +6803,7 @@ }, "string.prototype.matchall>call-bind": { "packages": { - "mocha>object.assign>function-bind": true, + "browserify>has>function-bind": true, "string.prototype.matchall>get-intrinsic": true } }, @@ -6840,7 +6842,7 @@ }, "packages": { "browserify>has": true, - "mocha>object.assign>function-bind": true, + "browserify>has>function-bind": true, "string.prototype.matchall>has-symbols": true } }, @@ -6896,6 +6898,7 @@ "globby>ignore": true, "globby>slash": true, "lodash": true, + "mocha>log-symbols": true, "nock>debug": true, "nyc>resolve-from": true, "stylelint>@stylelint/postcss-css-in-js": true, @@ -6912,7 +6915,6 @@ "stylelint>import-lazy": true, "stylelint>known-css-properties": true, "stylelint>leven": true, - "stylelint>log-symbols": true, "stylelint>mathml-tag-names": true, "stylelint>micromatch": true, "stylelint>normalize-selector": true, @@ -7094,12 +7096,7 @@ "process.platform": true }, "packages": { - "stylelint>chalk>supports-color>has-flag": true - } - }, - "stylelint>chalk>supports-color>has-flag": { - "globals": { - "process.argv": true + "sinon>supports-color>has-flag": true } }, "stylelint>cosmiconfig": { @@ -7212,8 +7209,8 @@ "process.platform": true }, "packages": { - "mocha>which": true, - "stylelint>global-modules>global-prefix>ini": true + "stylelint>global-modules>global-prefix>ini": true, + "stylelint>global-modules>global-prefix>which": true } }, "stylelint>global-modules>global-prefix>ini": { @@ -7221,39 +7218,24 @@ "process": true } }, - "stylelint>globjoin": { + "stylelint>global-modules>global-prefix>which": { "builtin": { "path.join": true - } - }, - "stylelint>log-symbols": { - "packages": { - "madge>ora>is-unicode-supported": true, - "stylelint>log-symbols>chalk": true - } - }, - "stylelint>log-symbols>chalk": { - "packages": { - "chalk>ansi-styles": true, - "stylelint>log-symbols>chalk>supports-color": true - } - }, - "stylelint>log-symbols>chalk>supports-color": { - "builtin": { - "os.release": true, - "tty.isatty": true }, "globals": { - "process.env": true, + "process.cwd": true, + "process.env.OSTYPE": true, + "process.env.PATH": true, + "process.env.PATHEXT": true, "process.platform": true }, "packages": { - "stylelint>log-symbols>chalk>supports-color>has-flag": true + "mocha>which>isexe": true } }, - "stylelint>log-symbols>chalk>supports-color>has-flag": { - "globals": { - "process.argv": true + "stylelint>globjoin": { + "builtin": { + "path.join": true } }, "stylelint>micromatch": { @@ -7557,9 +7539,9 @@ }, "stylelint>table>slice-ansi": { "packages": { - "mocha>yargs>string-width>is-fullwidth-code-point": true, "stylelint>table>slice-ansi>ansi-styles": true, - "stylelint>table>slice-ansi>astral-regex": true + "stylelint>table>slice-ansi>astral-regex": true, + "stylelint>table>slice-ansi>is-fullwidth-code-point": true } }, "stylelint>table>slice-ansi>ansi-styles": { @@ -7569,7 +7551,7 @@ }, "stylelint>table>string-width": { "packages": { - "mocha>yargs>string-width>is-fullwidth-code-point": true, + "stylelint>table>slice-ansi>is-fullwidth-code-point": true, "stylelint>table>string-width>emoji-regex": true, "stylelint>table>string-width>strip-ansi": true } diff --git a/package.json b/package.json index ac58eca27..c3eb9e568 100644 --- a/package.json +++ b/package.json @@ -452,7 +452,7 @@ "eslint-plugin-import": "^2.22.1", "eslint-plugin-jest": "^26.6.0", "eslint-plugin-jsdoc": "^39.3.3", - "eslint-plugin-mocha": "^8.1.0", + "eslint-plugin-mocha": "^10.1.0", "eslint-plugin-node": "^11.1.0", "eslint-plugin-prettier": "^4.2.1", "eslint-plugin-react": "^7.23.1", @@ -496,7 +496,7 @@ "lockfile-lint": "^4.9.6", "loose-envify": "^1.4.0", "madge": "^5.0.1", - "mocha": "^7.2.0", + "mocha": "^9.2.2", "mockttp": "^2.6.0", "nock": "^13.2.9", "node-fetch": "^2.6.1", diff --git a/yarn.lock b/yarn.lock index 93b2f813e..81c7c68e9 100644 --- a/yarn.lock +++ b/yarn.lock @@ -8174,6 +8174,13 @@ __metadata: languageName: node linkType: hard +"@ungap/promise-all-settled@npm:1.1.2": + version: 1.1.2 + resolution: "@ungap/promise-all-settled@npm:1.1.2" + checksum: 08d37fdfa23a6fe8139f1305313562ebad973f3fac01bcce2773b2bda5bcb0146dfdcf3cb6a722cf0a5f2ca0bc56a827eac8f1e7b3beddc548f654addf1fc34c + languageName: node + linkType: hard + "@vue/compiler-core@npm:3.1.4": version: 3.1.4 resolution: "@vue/compiler-core@npm:3.1.4" @@ -9133,10 +9140,10 @@ __metadata: languageName: node linkType: hard -"ansi-colors@npm:3.2.3": - version: 3.2.3 - resolution: "ansi-colors@npm:3.2.3" - checksum: 018a92fbf8b143feb9e00559655072598902ff2cdfa07dbe24b933c70ae04845e3dda2c091ab128920fc50b3db06c3f09947f49fcb287d53beb6c5869b8bb32b +"ansi-colors@npm:4.1.1": + version: 4.1.1 + resolution: "ansi-colors@npm:4.1.1" + checksum: 138d04a51076cb085da0a7e2d000c5c0bb09f6e772ed5c65c53cb118d37f6c5f1637506d7155fb5f330f0abcf6f12fa2e489ac3f8cdab9da393bf1bb4f9a32b0 languageName: node linkType: hard @@ -9191,16 +9198,16 @@ __metadata: linkType: hard "ansi-regex@npm:^3.0.0": - version: 3.0.0 - resolution: "ansi-regex@npm:3.0.0" - checksum: 2ad11c416f81c39f5c65eafc88cf1d71aa91d76a2f766e75e457c2a3c43e8a003aadbf2966b61c497aa6a6940a36412486c975b3270cdfc3f413b69826189ec3 + version: 3.0.1 + resolution: "ansi-regex@npm:3.0.1" + checksum: 09daf180c5f59af9850c7ac1bd7fda85ba596cc8cbeb210826e90755f06c818af86d9fa1e6e8322fab2c3b9e9b03f56c537b42241139f824dd75066a1e7257cc languageName: node linkType: hard "ansi-regex@npm:^4.1.0": - version: 4.1.0 - resolution: "ansi-regex@npm:4.1.0" - checksum: 97aa4659538d53e5e441f5ef2949a3cffcb838e57aeaad42c4194e9d7ddb37246a6526c4ca85d3940a9d1e19b11cc2e114530b54c9d700c8baf163c31779baf8 + version: 4.1.1 + resolution: "ansi-regex@npm:4.1.1" + checksum: b1a6ee44cb6ecdabaa770b2ed500542714d4395d71c7e5c25baa631f680fb2ad322eb9ba697548d498a6fd366949fc8b5bfcf48d49a32803611f648005b01888 languageName: node linkType: hard @@ -9289,7 +9296,7 @@ __metadata: languageName: node linkType: hard -"anymatch@npm:^3.0.0, anymatch@npm:^3.0.3, anymatch@npm:^3.1.0, anymatch@npm:~3.1.1, anymatch@npm:~3.1.2": +"anymatch@npm:^3.0.0, anymatch@npm:^3.0.3, anymatch@npm:^3.1.0, anymatch@npm:~3.1.2": version: 3.1.2 resolution: "anymatch@npm:3.1.2" dependencies: @@ -11835,26 +11842,7 @@ __metadata: languageName: node linkType: hard -"chokidar@npm:3.3.0": - version: 3.3.0 - resolution: "chokidar@npm:3.3.0" - dependencies: - anymatch: ~3.1.1 - braces: ~3.0.2 - fsevents: ~2.1.1 - glob-parent: ~5.1.0 - is-binary-path: ~2.1.0 - is-glob: ~4.0.1 - normalize-path: ~3.0.0 - readdirp: ~3.2.0 - dependenciesMeta: - fsevents: - optional: true - checksum: e9863256ebb29dbc5e58a7e2637439814beb63b772686cb9e94478312c24dcaf3d0570220c5e75ea29029f43b664f9956d87b716120d38cf755f32124f047e8e - languageName: node - linkType: hard - -"chokidar@npm:>=3.0.0 <4.0.0, chokidar@npm:^3.3.1, chokidar@npm:^3.4.0, chokidar@npm:^3.4.1, chokidar@npm:^3.4.2, chokidar@npm:^3.5.3": +"chokidar@npm:3.5.3, chokidar@npm:>=3.0.0 <4.0.0, chokidar@npm:^3.3.1, chokidar@npm:^3.4.0, chokidar@npm:^3.4.1, chokidar@npm:^3.4.2, chokidar@npm:^3.5.3": version: 3.5.3 resolution: "chokidar@npm:3.5.3" dependencies: @@ -12104,17 +12092,6 @@ __metadata: languageName: node linkType: hard -"cliui@npm:^5.0.0": - version: 5.0.0 - resolution: "cliui@npm:5.0.0" - dependencies: - string-width: ^3.1.0 - strip-ansi: ^5.2.0 - wrap-ansi: ^5.1.0 - checksum: 0bb8779efe299b8f3002a73619eaa8add4081eb8d1c17bc4fedc6240557fb4eacdc08fe87c39b002eacb6cfc117ce736b362dbfd8bf28d90da800e010ee97df4 - languageName: node - linkType: hard - "cliui@npm:^6.0.0": version: 6.0.0 resolution: "cliui@npm:6.0.0" @@ -13482,15 +13459,6 @@ __metadata: languageName: node linkType: hard -"debug@npm:3.2.6": - version: 3.2.6 - resolution: "debug@npm:3.2.6" - dependencies: - ms: ^2.1.1 - checksum: 07bc8b3a13ef3cfa6c06baf7871dfb174c291e5f85dbf566f086620c16b9c1a0e93bb8f1935ebbd07a683249e7e30286f2966e2ef461e8fd17b1b60732062d6b - languageName: node - linkType: hard - "debug@npm:3.X, debug@npm:^3.0.0, debug@npm:^3.1.0, debug@npm:^3.2.6, debug@npm:^3.2.7": version: 3.2.7 resolution: "debug@npm:3.2.7" @@ -13521,6 +13489,18 @@ __metadata: languageName: node linkType: hard +"debug@npm:4.3.3": + version: 4.3.3 + resolution: "debug@npm:4.3.3" + dependencies: + ms: 2.1.2 + peerDependenciesMeta: + supports-color: + optional: true + checksum: 14472d56fe4a94dbcfaa6dbed2dd3849f1d72ba78104a1a328047bb564643ca49df0224c3a17fa63533fd11dd3d4c8636cd861191232a2c6735af00cc2d4de16 + languageName: node + linkType: hard + "debug@npm:~3.1.0": version: 3.1.0 resolution: "debug@npm:3.1.0" @@ -13554,6 +13534,13 @@ __metadata: languageName: node linkType: hard +"decamelize@npm:^4.0.0": + version: 4.0.0 + resolution: "decamelize@npm:4.0.0" + checksum: b7d09b82652c39eead4d6678bb578e3bebd848add894b76d0f6b395bc45b2d692fb88d977e7cfb93c4ed6c119b05a1347cef261174916c2e75c0a8ca57da1809 + languageName: node + linkType: hard + "decimal.js@npm:^10.2.0, decimal.js@npm:^10.3.1": version: 10.4.0 resolution: "decimal.js@npm:10.4.0" @@ -14148,10 +14135,10 @@ __metadata: languageName: node linkType: hard -"diff@npm:3.5.0": - version: 3.5.0 - resolution: "diff@npm:3.5.0" - checksum: 00842950a6551e26ce495bdbce11047e31667deea546527902661f25cc2e73358967ebc78cf86b1a9736ec3e14286433225f9970678155753a6291c3bca5227b +"diff@npm:5.0.0, diff@npm:^5.0.0": + version: 5.0.0 + resolution: "diff@npm:5.0.0" + checksum: f19fe29284b633afdb2725c2a8bb7d25761ea54d321d8e67987ac851c5294be4afeab532bd84531e02583a3fe7f4014aa314a3eda84f5590e7a9e6b371ef3b46 languageName: node linkType: hard @@ -14162,13 +14149,6 @@ __metadata: languageName: node linkType: hard -"diff@npm:^5.0.0": - version: 5.0.0 - resolution: "diff@npm:5.0.0" - checksum: f19fe29284b633afdb2725c2a8bb7d25761ea54d321d8e67987ac851c5294be4afeab532bd84531e02583a3fe7f4014aa314a3eda84f5590e7a9e6b371ef3b46 - languageName: node - linkType: hard - "diffable-html@npm:^4.1.0": version: 4.1.0 resolution: "diffable-html@npm:4.1.0" @@ -14951,7 +14931,14 @@ __metadata: languageName: node linkType: hard -"escape-string-regexp@npm:1.0.5, escape-string-regexp@npm:^1.0.2, escape-string-regexp@npm:^1.0.5": +"escape-string-regexp@npm:4.0.0, escape-string-regexp@npm:^4.0.0": + version: 4.0.0 + resolution: "escape-string-regexp@npm:4.0.0" + checksum: 98b48897d93060f2322108bf29db0feba7dd774be96cd069458d1453347b25ce8682ecc39859d4bca2203cc0ab19c237bcc71755eff49a0f8d90beadeeba5cc5 + languageName: node + linkType: hard + +"escape-string-regexp@npm:^1.0.2, escape-string-regexp@npm:^1.0.5": version: 1.0.5 resolution: "escape-string-regexp@npm:1.0.5" checksum: 6092fda75c63b110c706b6a9bfde8a612ad595b628f0bd2147eea1d3406723020810e591effc7db1da91d80a71a737a313567c5abb3813e8d9c71f4aa595b410 @@ -14965,13 +14952,6 @@ __metadata: languageName: node linkType: hard -"escape-string-regexp@npm:^4.0.0": - version: 4.0.0 - resolution: "escape-string-regexp@npm:4.0.0" - checksum: 98b48897d93060f2322108bf29db0feba7dd774be96cd069458d1453347b25ce8682ecc39859d4bca2203cc0ab19c237bcc71755eff49a0f8d90beadeeba5cc5 - languageName: node - linkType: hard - "escodegen@npm:^1.11.1, escodegen@npm:^1.8.1, escodegen@npm:^1.9.0": version: 1.14.3 resolution: "escodegen@npm:1.14.3" @@ -15142,15 +15122,15 @@ __metadata: languageName: node linkType: hard -"eslint-plugin-mocha@npm:^8.1.0": - version: 8.1.0 - resolution: "eslint-plugin-mocha@npm:8.1.0" +"eslint-plugin-mocha@npm:^10.1.0": + version: 10.1.0 + resolution: "eslint-plugin-mocha@npm:10.1.0" dependencies: - eslint-utils: ^2.1.0 - ramda: ^0.27.1 + eslint-utils: ^3.0.0 + rambda: ^7.1.0 peerDependencies: eslint: ">=7.0.0" - checksum: c3efc9482fbda003f15847ee581f57e68e2c223664c743ab0613488894c00dc6bb2e9cca52718b964aa6e241e6d74292fa324a6ad9b697f7046643d616bf860f + checksum: 67c063ba190fe8ab3186baaf800a375e9f16a17f69deaac2ea0d1825f6e4260f9a56bd510ceb2ffbe6644d7090beda0efbd2ab7824e4852ce2abee53a1086179 languageName: node linkType: hard @@ -15278,7 +15258,7 @@ __metadata: languageName: node linkType: hard -"eslint-utils@npm:^2.0.0, eslint-utils@npm:^2.1.0": +"eslint-utils@npm:^2.0.0": version: 2.1.0 resolution: "eslint-utils@npm:2.1.0" dependencies: @@ -17076,12 +17056,13 @@ __metadata: languageName: node linkType: hard -"find-up@npm:3.0.0, find-up@npm:^3.0.0": - version: 3.0.0 - resolution: "find-up@npm:3.0.0" +"find-up@npm:5.0.0, find-up@npm:^5.0.0": + version: 5.0.0 + resolution: "find-up@npm:5.0.0" dependencies: - locate-path: ^3.0.0 - checksum: 38eba3fe7a66e4bc7f0f5a1366dc25508b7cfc349f852640e3678d26ad9a6d7e2c43eff0a472287de4a9753ef58f066a0ea892a256fa3636ad51b3fe1e17fae9 + locate-path: ^6.0.0 + path-exists: ^4.0.0 + checksum: 07955e357348f34660bde7920783204ff5a26ac2cafcaa28bace494027158a97b9f56faaf2d89a6106211a8174db650dd9f503f9c0d526b1202d5554a00b9095 languageName: node linkType: hard @@ -17104,6 +17085,15 @@ __metadata: languageName: node linkType: hard +"find-up@npm:^3.0.0": + version: 3.0.0 + resolution: "find-up@npm:3.0.0" + dependencies: + locate-path: ^3.0.0 + checksum: 38eba3fe7a66e4bc7f0f5a1366dc25508b7cfc349f852640e3678d26ad9a6d7e2c43eff0a472287de4a9753ef58f066a0ea892a256fa3636ad51b3fe1e17fae9 + languageName: node + linkType: hard + "find-up@npm:^4.0.0, find-up@npm:^4.1.0": version: 4.1.0 resolution: "find-up@npm:4.1.0" @@ -17114,16 +17104,6 @@ __metadata: languageName: node linkType: hard -"find-up@npm:^5.0.0": - version: 5.0.0 - resolution: "find-up@npm:5.0.0" - dependencies: - locate-path: ^6.0.0 - path-exists: ^4.0.0 - checksum: 07955e357348f34660bde7920783204ff5a26ac2cafcaa28bace494027158a97b9f56faaf2d89a6106211a8174db650dd9f503f9c0d526b1202d5554a00b9095 - languageName: node - linkType: hard - "findup-sync@npm:^2.0.0": version: 2.0.0 resolution: "findup-sync@npm:2.0.0" @@ -17205,14 +17185,12 @@ __metadata: languageName: node linkType: hard -"flat@npm:^4.1.0": - version: 4.1.0 - resolution: "flat@npm:4.1.0" - dependencies: - is-buffer: ~2.0.3 +"flat@npm:^5.0.2": + version: 5.0.2 + resolution: "flat@npm:5.0.2" bin: flat: cli.js - checksum: 41a91335be78c5c16813672a6371871034763db85ed84b31926b132ebeb145d63cd05460e33e4197358ed6a862e2c25c01721c8b2b20d292ff1e166795655f09 + checksum: 12a1536ac746db74881316a181499a78ef953632ddd28050b7a3a43c62ef5462e3357c8c29d76072bb635f147f7a9a1f0c02efef6b4be28f8db62ceb3d5c7f5d languageName: node linkType: hard @@ -17601,16 +17579,6 @@ __metadata: languageName: node linkType: hard -"fsevents@npm:~2.1.1": - version: 2.1.3 - resolution: "fsevents@npm:2.1.3" - dependencies: - node-gyp: latest - checksum: b5ec0516b44d75b60af5c01ff80a80cd995d175e4640d2a92fbabd02991dd664d76b241b65feef0775c23d531c3c74742c0fbacd6205af812a9c3cef59f04292 - conditions: os=darwin - languageName: node - linkType: hard - "fsevents@patch:fsevents@^1.2.7#~builtin": version: 1.2.9 resolution: "fsevents@patch:fsevents@npm%3A1.2.9#~builtin::version=1.2.9&hash=18f3a7" @@ -17630,15 +17598,6 @@ __metadata: languageName: node linkType: hard -"fsevents@patch:fsevents@~2.1.1#~builtin": - version: 2.1.3 - resolution: "fsevents@patch:fsevents@npm%3A2.1.3#~builtin::version=2.1.3&hash=18f3a7" - dependencies: - node-gyp: latest - conditions: os=darwin - languageName: node - linkType: hard - "ftp@npm:^0.3.10": version: 0.3.10 resolution: "ftp@npm:0.3.10" @@ -18035,7 +17994,7 @@ __metadata: languageName: node linkType: hard -"glob-parent@npm:^5.1.1, glob-parent@npm:^5.1.2, glob-parent@npm:~5.1.0, glob-parent@npm:~5.1.2": +"glob-parent@npm:^5.1.1, glob-parent@npm:^5.1.2, glob-parent@npm:~5.1.2": version: 5.1.2 resolution: "glob-parent@npm:5.1.2" dependencies: @@ -18110,9 +18069,9 @@ __metadata: languageName: node linkType: hard -"glob@npm:7.1.3": - version: 7.1.3 - resolution: "glob@npm:7.1.3" +"glob@npm:7.2.0, glob@npm:^7.0.0, glob@npm:^7.0.3, glob@npm:^7.1.0, glob@npm:^7.1.1, glob@npm:^7.1.3, glob@npm:^7.1.4, glob@npm:^7.1.6, glob@npm:^7.1.7": + version: 7.2.0 + resolution: "glob@npm:7.2.0" dependencies: fs.realpath: ^1.0.0 inflight: ^1.0.4 @@ -18120,7 +18079,7 @@ __metadata: minimatch: ^3.0.4 once: ^1.3.0 path-is-absolute: ^1.0.0 - checksum: d72a834a393948d6c4a5cacc6a29fe5fe190e1cd134e55dfba09aee0be6fe15be343e96d8ec43558ab67ff8af28e4420c7f63a4d4db1c779e515015e9c318616 + checksum: 78a8ea942331f08ed2e055cb5b9e40fe6f46f579d7fd3d694f3412fe5db23223d29b7fee1575440202e9a7ff9a72ab106a39fee39934c7bedafe5e5f8ae20134 languageName: node linkType: hard @@ -18138,20 +18097,6 @@ __metadata: languageName: node linkType: hard -"glob@npm:^7.0.0, glob@npm:^7.0.3, glob@npm:^7.1.0, glob@npm:^7.1.1, glob@npm:^7.1.3, glob@npm:^7.1.4, glob@npm:^7.1.6, glob@npm:^7.1.7": - version: 7.2.0 - resolution: "glob@npm:7.2.0" - dependencies: - fs.realpath: ^1.0.0 - inflight: ^1.0.4 - inherits: 2 - minimatch: ^3.0.4 - once: ^1.3.0 - path-is-absolute: ^1.0.0 - checksum: 78a8ea942331f08ed2e055cb5b9e40fe6f46f579d7fd3d694f3412fe5db23223d29b7fee1575440202e9a7ff9a72ab106a39fee39934c7bedafe5e5f8ae20134 - languageName: node - linkType: hard - "glob@npm:^8.0.1": version: 8.0.3 resolution: "glob@npm:8.0.3" @@ -19998,7 +19943,7 @@ __metadata: languageName: node linkType: hard -"is-buffer@npm:^2.0.0, is-buffer@npm:^2.0.5, is-buffer@npm:~2.0.3": +"is-buffer@npm:^2.0.0, is-buffer@npm:^2.0.5": version: 2.0.5 resolution: "is-buffer@npm:2.0.5" checksum: 764c9ad8b523a9f5a32af29bdf772b08eb48c04d2ad0a7240916ac2688c983bf5f8504bf25b35e66240edeb9d9085461f9b5dae1f3d2861c6b06a65fe983de42 @@ -20483,7 +20428,7 @@ __metadata: languageName: node linkType: hard -"is-plain-obj@npm:^2.0.0": +"is-plain-obj@npm:^2.0.0, is-plain-obj@npm:^2.1.0": version: 2.1.0 resolution: "is-plain-obj@npm:2.1.0" checksum: cec9100678b0a9fe0248a81743041ed990c2d4c99f893d935545cfbc42876cbe86d207f3b895700c690ad2fa520e568c44afc1605044b535a7820c1d40e38daa @@ -22102,18 +22047,6 @@ __metadata: languageName: node linkType: hard -"js-yaml@npm:3.13.1": - version: 3.13.1 - resolution: "js-yaml@npm:3.13.1" - dependencies: - argparse: ^1.0.7 - esprima: ^4.0.0 - bin: - js-yaml: bin/js-yaml.js - checksum: 7511b764abb66d8aa963379f7d2a404f078457d106552d05a7b556d204f7932384e8477513c124749fa2de52eb328961834562bd09924902c6432e40daa408bc - languageName: node - linkType: hard - "js-yaml@npm:3.14.0": version: 3.14.0 resolution: "js-yaml@npm:3.14.0" @@ -22126,6 +22059,17 @@ __metadata: languageName: node linkType: hard +"js-yaml@npm:4.1.0, js-yaml@npm:^4.1.0": + version: 4.1.0 + resolution: "js-yaml@npm:4.1.0" + dependencies: + argparse: ^2.0.1 + bin: + js-yaml: bin/js-yaml.js + checksum: c7830dfd456c3ef2c6e355cc5a92e6700ceafa1d14bba54497b34a99f0376cecbb3e9ac14d3e5849b426d5a5140709a66237a8c991c675431271c4ce5504151a + languageName: node + linkType: hard + "js-yaml@npm:^3.10.0, js-yaml@npm:^3.13.1, js-yaml@npm:^3.14.0": version: 3.14.1 resolution: "js-yaml@npm:3.14.1" @@ -22138,17 +22082,6 @@ __metadata: languageName: node linkType: hard -"js-yaml@npm:^4.1.0": - version: 4.1.0 - resolution: "js-yaml@npm:4.1.0" - dependencies: - argparse: ^2.0.1 - bin: - js-yaml: bin/js-yaml.js - checksum: c7830dfd456c3ef2c6e355cc5a92e6700ceafa1d14bba54497b34a99f0376cecbb3e9ac14d3e5849b426d5a5140709a66237a8c991c675431271c4ce5504151a - languageName: node - linkType: hard - "jsan@npm:^3.1.13": version: 3.1.13 resolution: "jsan@npm:3.1.13" @@ -23447,12 +23380,13 @@ __metadata: languageName: node linkType: hard -"log-symbols@npm:3.0.0": - version: 3.0.0 - resolution: "log-symbols@npm:3.0.0" +"log-symbols@npm:4.1.0, log-symbols@npm:^4.0.0, log-symbols@npm:^4.1.0": + version: 4.1.0 + resolution: "log-symbols@npm:4.1.0" dependencies: - chalk: ^2.4.2 - checksum: f2322e1452d819050b11aad247660e1494f8b2219d40a964af91d5f9af1a90636f1b3d93f2952090e42af07cc5550aecabf6c1d8ec1181207e95cb66ba112361 + chalk: ^4.1.0 + is-unicode-supported: ^0.1.0 + checksum: fce1497b3135a0198803f9f07464165e9eb83ed02ceb2273930a6f8a508951178d8cf4f0378e9d28300a2ed2bc49050995d2bd5f53ab716bb15ac84d58c6ef74 languageName: node linkType: hard @@ -23465,16 +23399,6 @@ __metadata: languageName: node linkType: hard -"log-symbols@npm:^4.0.0, log-symbols@npm:^4.1.0": - version: 4.1.0 - resolution: "log-symbols@npm:4.1.0" - dependencies: - chalk: ^4.1.0 - is-unicode-supported: ^0.1.0 - checksum: fce1497b3135a0198803f9f07464165e9eb83ed02ceb2273930a6f8a508951178d8cf4f0378e9d28300a2ed2bc49050995d2bd5f53ab716bb15ac84d58c6ef74 - languageName: node - linkType: hard - "loglevel@npm:^1.8.0, loglevel@npm:^1.8.1": version: 1.8.1 resolution: "loglevel@npm:1.8.1" @@ -24371,7 +24295,7 @@ __metadata: eslint-plugin-import: ^2.22.1 eslint-plugin-jest: ^26.6.0 eslint-plugin-jsdoc: ^39.3.3 - eslint-plugin-mocha: ^8.1.0 + eslint-plugin-mocha: ^10.1.0 eslint-plugin-node: ^11.1.0 eslint-plugin-prettier: ^4.2.1 eslint-plugin-react: ^7.23.1 @@ -24446,7 +24370,7 @@ __metadata: loose-envify: ^1.4.0 luxon: ^3.2.1 madge: ^5.0.1 - mocha: ^7.2.0 + mocha: ^9.2.2 mockttp: ^2.6.0 nanoid: ^2.1.6 nock: ^13.2.9 @@ -24751,6 +24675,15 @@ __metadata: languageName: node linkType: hard +"minimatch@npm:4.2.1": + version: 4.2.1 + resolution: "minimatch@npm:4.2.1" + dependencies: + brace-expansion: ^1.1.7 + checksum: 2b1514e3d0f29a549912f0db7ae7b82c5cab4a8f2dd0369f1c6451a325b3f12b2cf473c95873b6157bb8df183d6cf6db82ff03614b6adaaf1d7e055beccdfd01 + languageName: node + linkType: hard + "minimatch@npm:^3.0.2, minimatch@npm:^3.0.4, minimatch@npm:^3.1.2": version: 3.1.2 resolution: "minimatch@npm:3.1.2" @@ -24921,17 +24854,6 @@ __metadata: languageName: node linkType: hard -"mkdirp@npm:0.5.5": - version: 0.5.5 - resolution: "mkdirp@npm:0.5.5" - dependencies: - minimist: ^1.2.5 - bin: - mkdirp: bin/cmd.js - checksum: 3bce20ea525f9477befe458ab85284b0b66c8dc3812f94155af07c827175948cdd8114852ac6c6d82009b13c1048c37f6d98743eb019651ee25c39acc8aabe7d - languageName: node - linkType: hard - "mkdirp@npm:^0.5.1, mkdirp@npm:^0.5.3, mkdirp@npm:^0.5.4, mkdirp@npm:^0.5.5, mkdirp@npm:^0.5.6": version: 0.5.6 resolution: "mkdirp@npm:0.5.6" @@ -24952,38 +24874,38 @@ __metadata: languageName: node linkType: hard -"mocha@npm:^7.2.0": - version: 7.2.0 - resolution: "mocha@npm:7.2.0" +"mocha@npm:^9.2.2": + version: 9.2.2 + resolution: "mocha@npm:9.2.2" dependencies: - ansi-colors: 3.2.3 + "@ungap/promise-all-settled": 1.1.2 + ansi-colors: 4.1.1 browser-stdout: 1.3.1 - chokidar: 3.3.0 - debug: 3.2.6 - diff: 3.5.0 - escape-string-regexp: 1.0.5 - find-up: 3.0.0 - glob: 7.1.3 + chokidar: 3.5.3 + debug: 4.3.3 + diff: 5.0.0 + escape-string-regexp: 4.0.0 + find-up: 5.0.0 + glob: 7.2.0 growl: 1.10.5 he: 1.2.0 - js-yaml: 3.13.1 - log-symbols: 3.0.0 - minimatch: 3.0.4 - mkdirp: 0.5.5 - ms: 2.1.1 - node-environment-flags: 1.0.6 - object.assign: 4.1.0 - strip-json-comments: 2.0.1 - supports-color: 6.0.0 - which: 1.3.1 - wide-align: 1.1.3 - yargs: 13.3.2 - yargs-parser: 13.1.2 - yargs-unparser: 1.6.0 + js-yaml: 4.1.0 + log-symbols: 4.1.0 + minimatch: 4.2.1 + ms: 2.1.3 + nanoid: 3.3.1 + serialize-javascript: 6.0.0 + strip-json-comments: 3.1.1 + supports-color: 8.1.1 + which: 2.0.2 + workerpool: 6.2.0 + yargs: 16.2.0 + yargs-parser: 20.2.4 + yargs-unparser: 2.0.0 bin: _mocha: bin/_mocha mocha: bin/mocha - checksum: d098484fe1b165bb964fdbf6b88b256c71fead47575ca7c5bcf8ed07db0dcff41905f6d2f0a05111a0441efaef9d09241a8cc1ddf7961056b28984ec63ba2874 + checksum: 4d5ca4ce33fc66627e63acdf09a634e2358c9a00f61de7788b1091b6aad430da04f97f9ecb82d56dc034b623cb833b65576136fd010d77679c03fcea5bc1e12d languageName: node linkType: hard @@ -25129,7 +25051,7 @@ __metadata: languageName: node linkType: hard -"ms@npm:^2.0.0, ms@npm:^2.1.1": +"ms@npm:2.1.3, ms@npm:^2.0.0, ms@npm:^2.1.1": version: 2.1.3 resolution: "ms@npm:2.1.3" checksum: aa92de608021b242401676e35cfa5aa42dd70cbdc082b916da7fb925c542173e36bce97ea3e804923fe92c0ad991434e4a38327e15a1b5b5f945d66df615ae6d @@ -25302,6 +25224,15 @@ __metadata: languageName: node linkType: hard +"nanoid@npm:3.3.1": + version: 3.3.1 + resolution: "nanoid@npm:3.3.1" + bin: + nanoid: bin/nanoid.cjs + checksum: 4ef0969e1bbe866fc223eb32276cbccb0961900bfe79104fa5abe34361979dead8d0e061410a5c03bc3d47455685adf32c09d6f27790f4a6898fb51f7df7ec86 + languageName: node + linkType: hard + "nanoid@npm:^2.0.0, nanoid@npm:^2.1.6": version: 2.1.11 resolution: "nanoid@npm:2.1.11" @@ -25502,16 +25433,6 @@ __metadata: languageName: node linkType: hard -"node-environment-flags@npm:1.0.6": - version: 1.0.6 - resolution: "node-environment-flags@npm:1.0.6" - dependencies: - object.getownpropertydescriptors: ^2.0.3 - semver: ^5.7.0 - checksum: 268139ed0f7fabdca346dcb26931300ec7a1dc54a58085a849e5c78a82b94967f55df40177a69d4e819da278d98686d5c4fd49ab0d7bcff16fda25b6fffc4ca3 - languageName: node - linkType: hard - "node-fetch@npm:2.6.7, node-fetch@npm:^2, node-fetch@npm:^2.6.0, node-fetch@npm:^2.6.1, node-fetch@npm:^2.6.7, node-fetch@npm:~2.6.1": version: 2.6.7 resolution: "node-fetch@npm:2.6.7" @@ -26045,7 +25966,7 @@ __metadata: languageName: node linkType: hard -"object-keys@npm:^1.0.11, object-keys@npm:^1.1.1": +"object-keys@npm:^1.1.1": version: 1.1.1 resolution: "object-keys@npm:1.1.1" checksum: b363c5e7644b1e1b04aa507e88dcb8e3a2f52b6ffd0ea801e4c7a62d5aa559affe21c55a07fd4b1fd55fc03a33c610d73426664b20032405d7b92a1414c34d6a @@ -26068,18 +25989,6 @@ __metadata: languageName: node linkType: hard -"object.assign@npm:4.1.0": - version: 4.1.0 - resolution: "object.assign@npm:4.1.0" - dependencies: - define-properties: ^1.1.2 - function-bind: ^1.1.1 - has-symbols: ^1.0.0 - object-keys: ^1.0.11 - checksum: 648a9a463580bf48332d9a49a76fede2660ab1ee7104d9459b8a240562246da790b4151c3c073f28fda31c1fdc555d25a1d871e72be403e997e4468c91f4801f - languageName: node - linkType: hard - "object.assign@npm:^4.0.4, object.assign@npm:^4.1.0, object.assign@npm:^4.1.2, object.assign@npm:^4.1.4": version: 4.1.4 resolution: "object.assign@npm:4.1.4" @@ -28441,6 +28350,13 @@ __metadata: languageName: node linkType: hard +"rambda@npm:^7.1.0": + version: 7.5.0 + resolution: "rambda@npm:7.5.0" + checksum: ad608a9a4160d0b6b0921047cea1329276bf239ff58d439135288712dcdbbf0df47c76591843ad249d89e7c5a9109ce86fe099aa54aef0dc0aa92a9b4dd1b8eb + languageName: node + linkType: hard + "ramda@npm:^0.21.0": version: 0.21.0 resolution: "ramda@npm:0.21.0" @@ -28448,13 +28364,6 @@ __metadata: languageName: node linkType: hard -"ramda@npm:^0.27.1": - version: 0.27.1 - resolution: "ramda@npm:0.27.1" - checksum: 31a0c0ef739b2525d7615f84cbb5d3cb89ee0c795469b711f729ea1d8df0dccc3cd75d3717a1e9742d42315ce86435680b7c87743eb7618111c60c144a5b8059 - languageName: node - linkType: hard - "randomatic@npm:^3.0.0": version: 3.0.0 resolution: "randomatic@npm:3.0.0" @@ -29208,15 +29117,6 @@ __metadata: languageName: node linkType: hard -"readdirp@npm:~3.2.0": - version: 3.2.0 - resolution: "readdirp@npm:3.2.0" - dependencies: - picomatch: ^2.0.4 - checksum: 0456a4465a13eb5eaf40f0e0836b1bc6b9ebe479b48ba6f63a738b127a1990fb7b38f3ec4b4b6052f9230f976bc0558f12812347dc6b42ce4d548cfe82a9b6f3 - languageName: node - linkType: hard - "real-require@npm:^0.1.0": version: 0.1.0 resolution: "real-require@npm:0.1.0" @@ -30746,7 +30646,7 @@ __metadata: languageName: node linkType: hard -"semver@npm:2 || 3 || 4 || 5, semver@npm:^5.0.3, semver@npm:^5.1.0, semver@npm:^5.3.0, semver@npm:^5.4.1, semver@npm:^5.5.0, semver@npm:^5.6.0, semver@npm:^5.7.0": +"semver@npm:2 || 3 || 4 || 5, semver@npm:^5.0.3, semver@npm:^5.1.0, semver@npm:^5.3.0, semver@npm:^5.4.1, semver@npm:^5.5.0, semver@npm:^5.6.0": version: 5.7.1 resolution: "semver@npm:5.7.1" bin: @@ -30835,6 +30735,15 @@ __metadata: languageName: node linkType: hard +"serialize-javascript@npm:6.0.0, serialize-javascript@npm:^6.0.0": + version: 6.0.0 + resolution: "serialize-javascript@npm:6.0.0" + dependencies: + randombytes: ^2.1.0 + checksum: 56f90b562a1bdc92e55afb3e657c6397c01a902c588c0fe3d4c490efdcc97dcd2a3074ba12df9e94630f33a5ce5b76a74784a7041294628a6f4306e0ec84bf93 + languageName: node + linkType: hard + "serialize-javascript@npm:^4.0.0": version: 4.0.0 resolution: "serialize-javascript@npm:4.0.0" @@ -30853,15 +30762,6 @@ __metadata: languageName: node linkType: hard -"serialize-javascript@npm:^6.0.0": - version: 6.0.0 - resolution: "serialize-javascript@npm:6.0.0" - dependencies: - randombytes: ^2.1.0 - checksum: 56f90b562a1bdc92e55afb3e657c6397c01a902c588c0fe3d4c490efdcc97dcd2a3074ba12df9e94630f33a5ce5b76a74784a7041294628a6f4306e0ec84bf93 - languageName: node - linkType: hard - "serve-favicon@npm:^2.5.0": version: 2.5.0 resolution: "serve-favicon@npm:2.5.0" @@ -31904,7 +31804,7 @@ __metadata: languageName: node linkType: hard -"string-width@npm:^1.0.2 || 2, string-width@npm:^2.0.0, string-width@npm:^2.1.1": +"string-width@npm:^2.0.0, string-width@npm:^2.1.1": version: 2.1.1 resolution: "string-width@npm:2.1.1" dependencies: @@ -31914,7 +31814,7 @@ __metadata: languageName: node linkType: hard -"string-width@npm:^3.0.0, string-width@npm:^3.1.0": +"string-width@npm:^3.0.0": version: 3.1.0 resolution: "string-width@npm:3.1.0" dependencies: @@ -32072,7 +31972,7 @@ __metadata: languageName: node linkType: hard -"strip-ansi@npm:^5.0.0, strip-ansi@npm:^5.1.0, strip-ansi@npm:^5.2.0": +"strip-ansi@npm:^5.1.0": version: 5.2.0 resolution: "strip-ansi@npm:5.2.0" dependencies: @@ -32201,20 +32101,20 @@ __metadata: languageName: node linkType: hard -"strip-json-comments@npm:2.0.1, strip-json-comments@npm:^2.0.0, strip-json-comments@npm:~2.0.1": - version: 2.0.1 - resolution: "strip-json-comments@npm:2.0.1" - checksum: 1074ccb63270d32ca28edfb0a281c96b94dc679077828135141f27d52a5a398ef5e78bcf22809d23cadc2b81dfbe345eb5fd8699b385c8b1128907dec4a7d1e1 - languageName: node - linkType: hard - -"strip-json-comments@npm:^3.1.0, strip-json-comments@npm:^3.1.1": +"strip-json-comments@npm:3.1.1, strip-json-comments@npm:^3.1.0, strip-json-comments@npm:^3.1.1": version: 3.1.1 resolution: "strip-json-comments@npm:3.1.1" checksum: 492f73e27268f9b1c122733f28ecb0e7e8d8a531a6662efbd08e22cccb3f9475e90a1b82cab06a392f6afae6d2de636f977e231296400d0ec5304ba70f166443 languageName: node linkType: hard +"strip-json-comments@npm:^2.0.0, strip-json-comments@npm:~2.0.1": + version: 2.0.1 + resolution: "strip-json-comments@npm:2.0.1" + checksum: 1074ccb63270d32ca28edfb0a281c96b94dc679077828135141f27d52a5a398ef5e78bcf22809d23cadc2b81dfbe345eb5fd8699b385c8b1128907dec4a7d1e1 + languageName: node + linkType: hard + "strip-outer@npm:^1.0.1": version: 1.0.1 resolution: "strip-outer@npm:1.0.1" @@ -32473,12 +32373,12 @@ __metadata: languageName: node linkType: hard -"supports-color@npm:6.0.0": - version: 6.0.0 - resolution: "supports-color@npm:6.0.0" +"supports-color@npm:8.1.1, supports-color@npm:^8.0.0, supports-color@npm:^8.1.0": + version: 8.1.1 + resolution: "supports-color@npm:8.1.1" dependencies: - has-flag: ^3.0.0 - checksum: 005b4a7e5d78a9a703454f5b7da34336b82825747724d1f3eefea6c3956afcb33b79b31854a93cef0fc1f2449919ae952f79abbfd09a5b5b43ecd26407d3a3a1 + has-flag: ^4.0.0 + checksum: c052193a7e43c6cdc741eb7f378df605636e01ad434badf7324f17fb60c69a880d8d8fcdcb562cf94c2350e57b937d7425ab5b8326c67c2adc48f7c87c1db406 languageName: node linkType: hard @@ -32509,15 +32409,6 @@ __metadata: languageName: node linkType: hard -"supports-color@npm:^8.0.0, supports-color@npm:^8.1.0": - version: 8.1.1 - resolution: "supports-color@npm:8.1.1" - dependencies: - has-flag: ^4.0.0 - checksum: c052193a7e43c6cdc741eb7f378df605636e01ad434badf7324f17fb60c69a880d8d8fcdcb562cf94c2350e57b937d7425ab5b8326c67c2adc48f7c87c1db406 - languageName: node - linkType: hard - "supports-hyperlinks@npm:^2.0.0": version: 2.2.0 resolution: "supports-hyperlinks@npm:2.2.0" @@ -35169,18 +35060,7 @@ __metadata: languageName: node linkType: hard -"which@npm:1.3.1, which@npm:^1.2.12, which@npm:^1.2.14, which@npm:^1.2.9, which@npm:^1.3.1": - version: 1.3.1 - resolution: "which@npm:1.3.1" - dependencies: - isexe: ^2.0.0 - bin: - which: ./bin/which - checksum: f2e185c6242244b8426c9df1510e86629192d93c1a986a7d2a591f2c24869e7ffd03d6dac07ca863b2e4c06f59a4cc9916c585b72ee9fa1aa609d0124df15e04 - languageName: node - linkType: hard - -"which@npm:^2.0.1, which@npm:^2.0.2": +"which@npm:2.0.2, which@npm:^2.0.1, which@npm:^2.0.2": version: 2.0.2 resolution: "which@npm:2.0.2" dependencies: @@ -35191,12 +35071,14 @@ __metadata: languageName: node linkType: hard -"wide-align@npm:1.1.3": - version: 1.1.3 - resolution: "wide-align@npm:1.1.3" +"which@npm:^1.2.12, which@npm:^1.2.14, which@npm:^1.2.9, which@npm:^1.3.1": + version: 1.3.1 + resolution: "which@npm:1.3.1" dependencies: - string-width: ^1.0.2 || 2 - checksum: d09c8012652a9e6cab3e82338d1874a4d7db2ad1bd19ab43eb744acf0b9b5632ec406bdbbbb970a8f4771a7d5ef49824d038ba70aa884e7723f5b090ab87134d + isexe: ^2.0.0 + bin: + which: ./bin/which + checksum: f2e185c6242244b8426c9df1510e86629192d93c1a986a7d2a591f2c24869e7ffd03d6dac07ca863b2e4c06f59a4cc9916c585b72ee9fa1aa609d0124df15e04 languageName: node linkType: hard @@ -35277,6 +35159,13 @@ __metadata: languageName: node linkType: hard +"workerpool@npm:6.2.0": + version: 6.2.0 + resolution: "workerpool@npm:6.2.0" + checksum: 3493b4f0ef979a23d2c1583d7ef85f62fc9463cc02f82829d3e7e663b517f8ae9707da0249b382e46ac58986deb0ca2232ee1081713741211bda9254b429c9bb + languageName: node + linkType: hard + "wrap-ansi@npm:^2.0.0": version: 2.1.0 resolution: "wrap-ansi@npm:2.1.0" @@ -35287,17 +35176,6 @@ __metadata: languageName: node linkType: hard -"wrap-ansi@npm:^5.1.0": - version: 5.1.0 - resolution: "wrap-ansi@npm:5.1.0" - dependencies: - ansi-styles: ^3.2.0 - string-width: ^3.0.0 - strip-ansi: ^5.0.0 - checksum: 9b48c862220e541eb0daa22661b38b947973fc57054e91be5b0f2dcc77741a6875ccab4ebe970a394b4682c8dfc17e888266a105fb8b0a9b23c19245e781ceae - languageName: node - linkType: hard - "wrap-ansi@npm:^6.2.0": version: 6.2.0 resolution: "wrap-ansi@npm:6.2.0" @@ -35577,13 +35455,10 @@ __metadata: languageName: node linkType: hard -"yargs-parser@npm:13.1.2, yargs-parser@npm:^13.1.2": - version: 13.1.2 - resolution: "yargs-parser@npm:13.1.2" - dependencies: - camelcase: ^5.0.0 - decamelize: ^1.2.0 - checksum: c8bb6f44d39a4acd94462e96d4e85469df865de6f4326e0ab1ac23ae4a835e5dd2ddfe588317ebf80c3a7e37e741bd5cb0dc8d92bcc5812baefb7df7c885e86b +"yargs-parser@npm:20.2.4, yargs-parser@npm:^20.2.2": + version: 20.2.4 + resolution: "yargs-parser@npm:20.2.4" + checksum: d251998a374b2743a20271c2fd752b9fbef24eb881d53a3b99a7caa5e8227fcafd9abf1f345ac5de46435821be25ec12189a11030c12ee6481fef6863ed8b924 languageName: node linkType: hard @@ -35617,13 +35492,6 @@ __metadata: languageName: node linkType: hard -"yargs-parser@npm:^20.2.2": - version: 20.2.4 - resolution: "yargs-parser@npm:20.2.4" - checksum: d251998a374b2743a20271c2fd752b9fbef24eb881d53a3b99a7caa5e8227fcafd9abf1f345ac5de46435821be25ec12189a11030c12ee6481fef6863ed8b924 - languageName: node - linkType: hard - "yargs-parser@npm:^21.0.0": version: 21.0.1 resolution: "yargs-parser@npm:21.0.1" @@ -35631,32 +35499,30 @@ __metadata: languageName: node linkType: hard -"yargs-unparser@npm:1.6.0": - version: 1.6.0 - resolution: "yargs-unparser@npm:1.6.0" +"yargs-unparser@npm:2.0.0": + version: 2.0.0 + resolution: "yargs-unparser@npm:2.0.0" dependencies: - flat: ^4.1.0 - lodash: ^4.17.15 - yargs: ^13.3.0 - checksum: ca662bb94af53d816d47f2162f0a1d135783f09de9fd47645a5cb18dd25532b0b710432b680d2c065ff45de122ba4a96433c41595fa7bfcc08eb12e889db95c1 + camelcase: ^6.0.0 + decamelize: ^4.0.0 + flat: ^5.0.2 + is-plain-obj: ^2.1.0 + checksum: 68f9a542c6927c3768c2f16c28f71b19008710abd6b8f8efbac6dcce26bbb68ab6503bed1d5994bdbc2df9a5c87c161110c1dfe04c6a3fe5c6ad1b0e15d9a8a3 languageName: node linkType: hard -"yargs@npm:13.3.2, yargs@npm:^13.3.0": - version: 13.3.2 - resolution: "yargs@npm:13.3.2" +"yargs@npm:16.2.0, yargs@npm:^16.0.0, yargs@npm:^16.1.0, yargs@npm:^16.2.0": + version: 16.2.0 + resolution: "yargs@npm:16.2.0" dependencies: - cliui: ^5.0.0 - find-up: ^3.0.0 - get-caller-file: ^2.0.1 + cliui: ^7.0.2 + escalade: ^3.1.1 + get-caller-file: ^2.0.5 require-directory: ^2.1.1 - require-main-filename: ^2.0.0 - set-blocking: ^2.0.0 - string-width: ^3.0.0 - which-module: ^2.0.0 - y18n: ^4.0.0 - yargs-parser: ^13.1.2 - checksum: 75c13e837eb2bb25717957ba58d277e864efc0cca7f945c98bdf6477e6ec2f9be6afa9ed8a876b251a21423500c148d7b91e88dee7adea6029bdec97af1ef3e8 + string-width: ^4.2.0 + y18n: ^5.0.5 + yargs-parser: ^20.2.2 + checksum: b14afbb51e3251a204d81937c86a7e9d4bdbf9a2bcee38226c900d00f522969ab675703bee2a6f99f8e20103f608382936034e64d921b74df82b63c07c5e8f59 languageName: node linkType: hard @@ -35694,21 +35560,6 @@ __metadata: languageName: node linkType: hard -"yargs@npm:^16.0.0, yargs@npm:^16.1.0, yargs@npm:^16.2.0": - version: 16.2.0 - resolution: "yargs@npm:16.2.0" - dependencies: - cliui: ^7.0.2 - escalade: ^3.1.1 - get-caller-file: ^2.0.5 - require-directory: ^2.1.1 - string-width: ^4.2.0 - y18n: ^5.0.5 - yargs-parser: ^20.2.2 - checksum: b14afbb51e3251a204d81937c86a7e9d4bdbf9a2bcee38226c900d00f522969ab675703bee2a6f99f8e20103f608382936034e64d921b74df82b63c07c5e8f59 - languageName: node - linkType: hard - "yargs@npm:^17.0.1, yargs@npm:^17.3.1": version: 17.5.1 resolution: "yargs@npm:17.5.1" From a3af0b53e32e7f04e90f9ddbb6cc1b8a723184e8 Mon Sep 17 00:00:00 2001 From: Vinicius Stevam <45455812+vinistevam@users.noreply.github.com> Date: Fri, 14 Apr 2023 05:50:17 +0100 Subject: [PATCH 25/36] Trigger unlock popup in appStateController using ApprovalController (#18386) --- app/scripts/controllers/app-state.js | 51 +++- app/scripts/controllers/app-state.test.js | 330 +++++++++++++++++++++- app/scripts/metamask-controller.js | 8 +- 3 files changed, 377 insertions(+), 12 deletions(-) diff --git a/app/scripts/controllers/app-state.js b/app/scripts/controllers/app-state.js index f7669e055..3360cd4f9 100644 --- a/app/scripts/controllers/app-state.js +++ b/app/scripts/controllers/app-state.js @@ -1,5 +1,7 @@ import EventEmitter from 'events'; import { ObservableStore } from '@metamask/obs-store'; +import { v4 as uuid } from 'uuid'; +import log from 'loglevel'; import { METAMASK_CONTROLLER_EVENTS } from '../metamask-controller'; import { MINUTE } from '../../../shared/constants/time'; import { AUTO_LOCK_TIMEOUT_ALARM } from '../../../shared/constants/alarms'; @@ -8,8 +10,11 @@ import { isBeta } from '../../../ui/helpers/utils/build-types'; import { ENVIRONMENT_TYPE_BACKGROUND, POLLING_TOKEN_ENVIRONMENT_TYPES, + ORIGIN_METAMASK, } from '../../../shared/constants/app'; +const APPROVAL_REQUEST_TYPE = 'unlock'; + export default class AppStateController extends EventEmitter { /** * @param {object} opts @@ -20,9 +25,9 @@ export default class AppStateController extends EventEmitter { isUnlocked, initState, onInactiveTimeout, - showUnlockRequest, preferencesStore, qrHardwareStore, + messenger, } = opts; super(); @@ -59,8 +64,6 @@ export default class AppStateController extends EventEmitter { this.waitingForUnlock = []; addUnlockListener(this.handleUnlock.bind(this)); - this._showUnlockRequest = showUnlockRequest; - preferencesStore.subscribe(({ preferences }) => { const currentState = this.store.getState(); if (currentState.timeoutMinutes !== preferences.autoLockTimeLimit) { @@ -74,6 +77,9 @@ export default class AppStateController extends EventEmitter { const { preferences } = preferencesStore.getState(); this._setInactiveTimeout(preferences.autoLockTimeLimit); + + this.messagingSystem = messenger; + this._approvalRequestId = null; } /** @@ -108,7 +114,7 @@ export default class AppStateController extends EventEmitter { this.waitingForUnlock.push({ resolve }); this.emit(METAMASK_CONTROLLER_EVENTS.UPDATE_BADGE); if (shouldShowUnlockRequest) { - this._showUnlockRequest(); + this._requestApproval(); } } @@ -122,6 +128,8 @@ export default class AppStateController extends EventEmitter { } this.emit(METAMASK_CONTROLLER_EVENTS.UPDATE_BADGE); } + + this._acceptApproval(); } /** @@ -369,4 +377,39 @@ export default class AppStateController extends EventEmitter { serviceWorkerLastActiveTime, }); } + + _requestApproval() { + this._approvalRequestId = uuid(); + + this.messagingSystem + .call( + 'ApprovalController:addRequest', + { + id: this._approvalRequestId, + origin: ORIGIN_METAMASK, + type: APPROVAL_REQUEST_TYPE, + }, + true, + ) + .catch(() => { + // Intentionally ignored as promise not currently used + }); + } + + _acceptApproval() { + if (!this._approvalRequestId) { + log.error('Attempted to accept missing unlock approval request'); + return; + } + try { + this.messagingSystem.call( + 'ApprovalController:acceptRequest', + this._approvalRequestId, + ); + } catch (error) { + log.error('Failed to accept transaction approval request', error); + } + + this._approvalRequestId = null; + } } diff --git a/app/scripts/controllers/app-state.test.js b/app/scripts/controllers/app-state.test.js index 7aa27e44b..02d3cda3c 100644 --- a/app/scripts/controllers/app-state.test.js +++ b/app/scripts/controllers/app-state.test.js @@ -1,12 +1,158 @@ +import { ObservableStore } from '@metamask/obs-store'; +import log from 'loglevel'; +import { ORIGIN_METAMASK } from '../../../shared/constants/app'; import AppStateController from './app-state'; +jest.mock('loglevel'); + +let appStateController, mockStore; + describe('AppStateController', () => { + mockStore = new ObservableStore(); + const createAppStateController = (initState = {}) => { + return new AppStateController({ + addUnlockListener: jest.fn(), + isUnlocked: jest.fn(() => true), + initState, + onInactiveTimeout: jest.fn(), + showUnlockRequest: jest.fn(), + preferencesStore: { + subscribe: jest.fn(), + getState: jest.fn(() => ({ + preferences: { + autoLockTimeLimit: 0, + }, + })), + }, + qrHardwareStore: { + subscribe: jest.fn(), + }, + messenger: { + call: jest.fn(() => ({ + catch: jest.fn(), + })), + }, + }); + }; + + beforeEach(() => { + appStateController = createAppStateController({ store: mockStore }); + }); + describe('setOutdatedBrowserWarningLastShown', () => { - it('should set the last shown time', () => { - const appStateController = new AppStateController({ + it('sets the last shown time', () => { + appStateController = createAppStateController(); + const date = new Date(); + + appStateController.setOutdatedBrowserWarningLastShown(date); + + expect( + appStateController.store.getState().outdatedBrowserWarningLastShown, + ).toStrictEqual(date); + }); + + it('sets outdated browser warning last shown timestamp', () => { + const lastShownTimestamp = Date.now(); + appStateController = createAppStateController(); + const updateStateSpy = jest.spyOn( + appStateController.store, + 'updateState', + ); + + appStateController.setOutdatedBrowserWarningLastShown(lastShownTimestamp); + + expect(updateStateSpy).toHaveBeenCalledTimes(1); + expect(updateStateSpy).toHaveBeenCalledWith({ + outdatedBrowserWarningLastShown: lastShownTimestamp, + }); + + updateStateSpy.mockRestore(); + }); + }); + + describe('getUnlockPromise', () => { + it('waits for unlock if the extension is locked', async () => { + appStateController = createAppStateController(); + const isUnlockedMock = jest + .spyOn(appStateController, 'isUnlocked') + .mockReturnValue(false); + const waitForUnlockSpy = jest.spyOn(appStateController, 'waitForUnlock'); + + appStateController.getUnlockPromise(true); + expect(isUnlockedMock).toHaveBeenCalled(); + expect(waitForUnlockSpy).toHaveBeenCalledWith(expect.any(Function), true); + }); + + it('resolves immediately if the extension is already unlocked', async () => { + appStateController = createAppStateController(); + const isUnlockedMock = jest + .spyOn(appStateController, 'isUnlocked') + .mockReturnValue(true); + + await expect( + appStateController.getUnlockPromise(false), + ).resolves.toBeUndefined(); + + expect(isUnlockedMock).toHaveBeenCalled(); + }); + }); + + describe('waitForUnlock', () => { + it('resolves immediately if already unlocked', async () => { + const emitSpy = jest.spyOn(appStateController, 'emit'); + const resolveFn = jest.fn(); + appStateController.waitForUnlock(resolveFn, false); + expect(emitSpy).toHaveBeenCalledWith('updateBadge'); + expect(appStateController.messagingSystem.call).toHaveBeenCalledTimes(0); + }); + + it('creates approval request when waitForUnlock is called with shouldShowUnlockRequest as true', async () => { + jest.spyOn(appStateController, 'isUnlocked').mockReturnValue(false); + + const resolveFn = jest.fn(); + appStateController.waitForUnlock(resolveFn, true); + + expect(appStateController.messagingSystem.call).toHaveBeenCalledTimes(1); + expect(appStateController.messagingSystem.call).toHaveBeenCalledWith( + 'ApprovalController:addRequest', + expect.objectContaining({ + id: expect.any(String), + origin: ORIGIN_METAMASK, + type: 'unlock', + }), + true, + ); + }); + }); + + describe('handleUnlock', () => { + beforeEach(() => { + jest.spyOn(appStateController, 'isUnlocked').mockReturnValue(false); + }); + afterEach(() => { + jest.clearAllMocks(); + }); + it('accepts approval request revolving all the related promises', async () => { + const emitSpy = jest.spyOn(appStateController, 'emit'); + const resolveFn = jest.fn(); + appStateController.waitForUnlock(resolveFn, true); + + appStateController.handleUnlock(); + + expect(emitSpy).toHaveBeenCalled(); + expect(emitSpy).toHaveBeenCalledWith('updateBadge'); + expect(appStateController.messagingSystem.call).toHaveBeenCalled(); + expect(appStateController.messagingSystem.call).toHaveBeenCalledWith( + 'ApprovalController:acceptRequest', + expect.any(String), + ); + }); + + it('logs if rejecting approval request throws', async () => { + appStateController._approvalRequestId = 'mock-approval-request-id'; + appStateController = new AppStateController({ addUnlockListener: jest.fn(), isUnlocked: jest.fn(() => true), - initState: {}, onInactiveTimeout: jest.fn(), showUnlockRequest: jest.fn(), preferencesStore: { @@ -20,14 +166,184 @@ describe('AppStateController', () => { qrHardwareStore: { subscribe: jest.fn(), }, + messenger: { + call: jest.fn(() => { + throw new Error('mock error'); + }), + }, }); - const date = new Date(); - appStateController.setOutdatedBrowserWarningLastShown(date); + appStateController.handleUnlock(); + + expect(log.error).toHaveBeenCalledTimes(1); + expect(log.error).toHaveBeenCalledWith( + 'Attempted to accept missing unlock approval request', + ); + }); + + it('returns without call messenger if no approval request in pending', async () => { + const emitSpy = jest.spyOn(appStateController, 'emit'); + + appStateController.handleUnlock(); + + expect(emitSpy).toHaveBeenCalledTimes(0); + expect(appStateController.messagingSystem.call).toHaveBeenCalledTimes(0); + expect(log.error).toHaveBeenCalledTimes(1); + expect(log.error).toHaveBeenCalledWith( + 'Attempted to accept missing unlock approval request', + ); + }); + }); + + describe('setDefaultHomeActiveTabName', () => { + it('sets the default home tab name', () => { + appStateController.setDefaultHomeActiveTabName('testTabName'); + expect(appStateController.store.getState().defaultHomeActiveTabName).toBe( + 'testTabName', + ); + }); + }); + + describe('setConnectedStatusPopoverHasBeenShown', () => { + it('sets connected status popover as shown', () => { + appStateController.setConnectedStatusPopoverHasBeenShown(); + expect( + appStateController.store.getState().connectedStatusPopoverHasBeenShown, + ).toBe(true); + }); + }); + + describe('setRecoveryPhraseReminderHasBeenShown', () => { + it('sets recovery phrase reminder as shown', () => { + appStateController.setRecoveryPhraseReminderHasBeenShown(); + expect( + appStateController.store.getState().recoveryPhraseReminderHasBeenShown, + ).toBe(true); + }); + }); + + describe('setRecoveryPhraseReminderLastShown', () => { + it('sets the last shown time of recovery phrase reminder', () => { + const timestamp = Date.now(); + appStateController.setRecoveryPhraseReminderLastShown(timestamp); expect( - appStateController.store.getState().outdatedBrowserWarningLastShown, - ).toStrictEqual(date); + appStateController.store.getState().recoveryPhraseReminderLastShown, + ).toBe(timestamp); + }); + }); + + describe('setLastActiveTime', () => { + it('sets the last active time to the current time', () => { + const spy = jest.spyOn(appStateController, '_resetTimer'); + appStateController.setLastActiveTime(); + + expect(spy).toHaveBeenCalled(); + }); + }); + + describe('setBrowserEnvironment', () => { + it('sets the current browser and OS environment', () => { + appStateController.setBrowserEnvironment('Windows', 'Chrome'); + expect( + appStateController.store.getState().browserEnvironment, + ).toStrictEqual({ + os: 'Windows', + browser: 'Chrome', + }); + }); + }); + + describe('addPollingToken', () => { + it('adds a pollingToken for a given environmentType', () => { + const pollingTokenType = 'popupGasPollTokens'; + appStateController.addPollingToken('token1', pollingTokenType); + expect(appStateController.store.getState()[pollingTokenType]).toContain( + 'token1', + ); + }); + }); + + describe('removePollingToken', () => { + it('removes a pollingToken for a given environmentType', () => { + const pollingTokenType = 'popupGasPollTokens'; + appStateController.addPollingToken('token1', pollingTokenType); + appStateController.removePollingToken('token1', pollingTokenType); + expect( + appStateController.store.getState()[pollingTokenType], + ).not.toContain('token1'); + }); + }); + + describe('clearPollingTokens', () => { + it('clears all pollingTokens', () => { + appStateController.addPollingToken('token1', 'popupGasPollTokens'); + appStateController.addPollingToken('token2', 'notificationGasPollTokens'); + appStateController.addPollingToken('token3', 'fullScreenGasPollTokens'); + appStateController.clearPollingTokens(); + + expect( + appStateController.store.getState().popupGasPollTokens, + ).toStrictEqual([]); + expect( + appStateController.store.getState().notificationGasPollTokens, + ).toStrictEqual([]); + expect( + appStateController.store.getState().fullScreenGasPollTokens, + ).toStrictEqual([]); + }); + }); + + describe('setShowTestnetMessageInDropdown', () => { + it('sets whether the testnet dismissal link should be shown in the network dropdown', () => { + appStateController.setShowTestnetMessageInDropdown(true); + expect( + appStateController.store.getState().showTestnetMessageInDropdown, + ).toBe(true); + + appStateController.setShowTestnetMessageInDropdown(false); + expect( + appStateController.store.getState().showTestnetMessageInDropdown, + ).toBe(false); + }); + }); + + describe('setShowBetaHeader', () => { + it('sets whether the beta notification heading on the home page', () => { + appStateController.setShowBetaHeader(true); + expect(appStateController.store.getState().showBetaHeader).toBe(true); + + appStateController.setShowBetaHeader(false); + expect(appStateController.store.getState().showBetaHeader).toBe(false); + }); + }); + + describe('setCurrentPopupId', () => { + it('sets the currentPopupId in the appState', () => { + const popupId = 'popup1'; + + appStateController.setCurrentPopupId(popupId); + expect(appStateController.store.getState().currentPopupId).toBe(popupId); + }); + }); + + describe('getCurrentPopupId', () => { + it('retrieves the currentPopupId saved in the appState', () => { + const popupId = 'popup1'; + + appStateController.setCurrentPopupId(popupId); + expect(appStateController.getCurrentPopupId()).toBe(popupId); + }); + }); + + describe('setFirstTimeUsedNetwork', () => { + it('updates the array of the first time used networks', () => { + const chainId = '0x1'; + + appStateController.setFirstTimeUsedNetwork(chainId); + expect(appStateController.store.getState().usedNetworks[chainId]).toBe( + true, + ); }); }); }); diff --git a/app/scripts/metamask-controller.js b/app/scripts/metamask-controller.js index 133f5a877..53bba6767 100644 --- a/app/scripts/metamask-controller.js +++ b/app/scripts/metamask-controller.js @@ -520,9 +520,15 @@ export default class MetamaskController extends EventEmitter { isUnlocked: this.isUnlocked.bind(this), initState: initState.AppStateController, onInactiveTimeout: () => this.setLocked(), - showUnlockRequest: opts.showUserConfirmation, preferencesStore: this.preferencesController.store, qrHardwareStore: this.qrHardwareKeyring.getMemStore(), + messenger: this.controllerMessenger.getRestricted({ + name: 'AppStateController', + allowedActions: [ + `${this.approvalController.name}:addRequest`, + `${this.approvalController.name}:acceptRequest`, + ], + }), }); const currencyRateMessenger = this.controllerMessenger.getRestricted({ From 17147b381711a59d059f88534b22b640bb41c456 Mon Sep 17 00:00:00 2001 From: legobeat <109787230+legobeat@users.noreply.github.com> Date: Fri, 14 Apr 2023 16:59:51 +0900 Subject: [PATCH 26/36] test: increase timeout for failing tests (#18189) --- test/e2e/tests/add-custom-network.spec.js | 4 +++- test/e2e/tests/custom-token-add-approve.spec.js | 11 +++++++---- test/e2e/tests/send-eth.spec.js | 9 ++++++--- 3 files changed, 16 insertions(+), 8 deletions(-) diff --git a/test/e2e/tests/add-custom-network.spec.js b/test/e2e/tests/add-custom-network.spec.js index 3564f9f2e..45bdc8e36 100644 --- a/test/e2e/tests/add-custom-network.spec.js +++ b/test/e2e/tests/add-custom-network.spec.js @@ -392,7 +392,9 @@ describe('Custom network', function () { text: 'Delete', }); - await driver.findElement('.modal-container__footer'); + await driver.waitForSelector('.modal-container__footer', { + timeout: 15000, + }); // should be deleted from the modal shown again to complete deletion custom network await driver.clickElement({ tag: 'button', diff --git a/test/e2e/tests/custom-token-add-approve.spec.js b/test/e2e/tests/custom-token-add-approve.spec.js index 5e1addcc5..c3e73d71e 100644 --- a/test/e2e/tests/custom-token-add-approve.spec.js +++ b/test/e2e/tests/custom-token-add-approve.spec.js @@ -292,10 +292,13 @@ describe('Create token, approve token and approve token without gas', function ( await gasLimitInput.fill('60001'); await driver.clickElement({ text: 'Save', tag: 'button' }); - await driver.waitForSelector({ - css: '.box--flex-direction-row > h6', - text: '0.0006 ETH', - }); + await driver.waitForSelector( + { + css: '.box--flex-direction-row > h6', + text: '0.0006 ETH', + }, + { timeout: 15000 }, + ); // editing spending cap await driver.clickElement({ diff --git a/test/e2e/tests/send-eth.spec.js b/test/e2e/tests/send-eth.spec.js index b5542c1cb..d287184c3 100644 --- a/test/e2e/tests/send-eth.spec.js +++ b/test/e2e/tests/send-eth.spec.js @@ -354,9 +354,12 @@ describe('Send ETH from dapp using advanced gas controls', function () { '.transaction-list-item__primary-currency', ); await txValue.click(); - const baseFeeValue = await driver.waitForSelector({ - text: '0.000000025', - }); + const baseFeeValue = await driver.waitForSelector( + { + text: '0.000000025', + }, + { timeout: 15000 }, + ); assert.equal(await baseFeeValue.getText(), '0.000000025'); }, ); From 3eefe874a8a224d8dfbce3d39bd51e1ba36dff2c Mon Sep 17 00:00:00 2001 From: David Drazic Date: Fri, 14 Apr 2023 12:04:23 +0200 Subject: [PATCH 27/36] [FLASK] Revert changes made to stable permission display (UI design) (#18470) * Revert changes made to stable permission display (UI design) * Add test for new component * Update paddings for install flow * Fix missing icons on snap installation flow * Update storybook path * Add targetSubjectMetadata param --------- Co-authored-by: legobeat <109787230+legobeat@users.noreply.github.com> Co-authored-by: Frederik Bolding --- .../app/flask/snap-permissions-list/index.js | 1 + .../snap-permissions-list.js | 37 +++++++++++++++ .../snap-permissions-list.stories.js | 37 +++++++++++++++ .../snap-permissions-list.test.js | 37 +++++++++++++++ .../app/permission-cell/permission-cell.js | 11 +++-- ...ission-page-container-content.component.js | 7 ++- .../permissions-connect-permission-list.js | 43 +++++++++++------- ...issions-connect-permission-list.stories.js | 13 ------ ui/helpers/utils/permission.js | 45 ++++++++++++++++--- .../flask/snap-install/snap-install.js | 18 +++++--- .../settings/flask/view-snap/view-snap.js | 4 +- 11 files changed, 205 insertions(+), 48 deletions(-) create mode 100644 ui/components/app/flask/snap-permissions-list/index.js create mode 100644 ui/components/app/flask/snap-permissions-list/snap-permissions-list.js create mode 100644 ui/components/app/flask/snap-permissions-list/snap-permissions-list.stories.js create mode 100644 ui/components/app/flask/snap-permissions-list/snap-permissions-list.test.js diff --git a/ui/components/app/flask/snap-permissions-list/index.js b/ui/components/app/flask/snap-permissions-list/index.js new file mode 100644 index 000000000..457784a5f --- /dev/null +++ b/ui/components/app/flask/snap-permissions-list/index.js @@ -0,0 +1 @@ +export { default } from './snap-permissions-list'; diff --git a/ui/components/app/flask/snap-permissions-list/snap-permissions-list.js b/ui/components/app/flask/snap-permissions-list/snap-permissions-list.js new file mode 100644 index 000000000..6dda11d04 --- /dev/null +++ b/ui/components/app/flask/snap-permissions-list/snap-permissions-list.js @@ -0,0 +1,37 @@ +import React from 'react'; +import PropTypes from 'prop-types'; +import { getWeightedPermissions } from '../../../../helpers/utils/permission'; +import { useI18nContext } from '../../../../hooks/useI18nContext'; +import PermissionCell from '../../permission-cell'; +import Box from '../../../ui/box'; + +export default function SnapPermissionsList({ + permissions, + targetSubjectMetadata, +}) { + const t = useI18nContext(); + + return ( + + {getWeightedPermissions(t, permissions, targetSubjectMetadata).map( + (permission, index) => { + return ( + + ); + }, + )} + + ); +} + +SnapPermissionsList.propTypes = { + permissions: PropTypes.object.isRequired, + targetSubjectMetadata: PropTypes.object.isRequired, +}; diff --git a/ui/components/app/flask/snap-permissions-list/snap-permissions-list.stories.js b/ui/components/app/flask/snap-permissions-list/snap-permissions-list.stories.js new file mode 100644 index 000000000..e3f24287e --- /dev/null +++ b/ui/components/app/flask/snap-permissions-list/snap-permissions-list.stories.js @@ -0,0 +1,37 @@ +import React from 'react'; + +import SnapPermissionsList from '.'; + +export default { + title: 'Components/App/flask/SnapPermissionsList', + + component: SnapPermissionsList, + argTypes: { + permissions: { + control: 'object', + }, + }, +}; + +export const DefaultStory = (args) => ; + +DefaultStory.storyName = 'Default'; + +DefaultStory.args = { + permissions: { + eth_accounts: {}, + snap_dialog: {}, + snap_getBip32PublicKey: { + caveats: [ + { + value: [ + { + path: ['m', `44'`, `0'`], + curve: 'secp256k1', + }, + ], + }, + ], + }, + }, +}; diff --git a/ui/components/app/flask/snap-permissions-list/snap-permissions-list.test.js b/ui/components/app/flask/snap-permissions-list/snap-permissions-list.test.js new file mode 100644 index 000000000..4ba174224 --- /dev/null +++ b/ui/components/app/flask/snap-permissions-list/snap-permissions-list.test.js @@ -0,0 +1,37 @@ +import React from 'react'; +import { screen } from '@testing-library/react'; +import { renderWithProvider } from '../../../../../test/jest'; +import SnapPermissionsList from './snap-permissions-list'; + +describe('Snap Permission List', () => { + const mockPermissionData = { + snap_dialog: { + caveats: null, + date: 1680709920602, + id: '4dduR1BpsmS0ZJfeVtiAh', + invoker: 'local:http://localhost:8080', + parentCapability: 'snap_dialog', + }, + }; + const mockTargetSubjectMetadata = { + extensionId: null, + iconUrl: null, + name: 'TypeScript Example Snap', + origin: 'local:http://localhost:8080', + subjectType: 'snap', + version: '0.2.2', + }; + + it('renders permissions list for snaps', () => { + renderWithProvider( + , + ); + expect( + screen.getByText('Display dialog windows in MetaMask.'), + ).toBeInTheDocument(); + expect(screen.getByText('Approved on 2023-04-05')).toBeInTheDocument(); + }); +}); diff --git a/ui/components/app/permission-cell/permission-cell.js b/ui/components/app/permission-cell/permission-cell.js index ea87a381e..fbbafea43 100644 --- a/ui/components/app/permission-cell/permission-cell.js +++ b/ui/components/app/permission-cell/permission-cell.js @@ -53,6 +53,11 @@ const PermissionCell = ({ iconBackgroundColor = Color.backgroundAlternative; } + let permissionIcon = avatarIcon; + if (typeof avatarIcon !== 'string' && avatarIcon?.props?.iconName) { + permissionIcon = avatarIcon.props.iconName; + } + return ( - {typeof avatarIcon === 'string' ? ( + {typeof permissionIcon === 'string' ? ( ) : ( - avatarIcon + permissionIcon )} diff --git a/ui/components/app/permission-page-container/permission-page-container-content/permission-page-container-content.component.js b/ui/components/app/permission-page-container/permission-page-container-content/permission-page-container-content.component.js index ffbbca22d..accab2e15 100644 --- a/ui/components/app/permission-page-container/permission-page-container-content/permission-page-container-content.component.js +++ b/ui/components/app/permission-page-container/permission-page-container-content/permission-page-container-content.component.js @@ -28,11 +28,14 @@ export default class PermissionPageContainerContent extends PureComponent { }; renderRequestedPermissions() { - const { selectedPermissions } = this.props; + const { selectedPermissions, subjectMetadata } = this.props; return (
- +
); } diff --git a/ui/components/app/permissions-connect-permission-list/permissions-connect-permission-list.js b/ui/components/app/permissions-connect-permission-list/permissions-connect-permission-list.js index 5c5201f60..6d0ccfcb7 100644 --- a/ui/components/app/permissions-connect-permission-list/permissions-connect-permission-list.js +++ b/ui/components/app/permissions-connect-permission-list/permissions-connect-permission-list.js @@ -1,9 +1,29 @@ import React from 'react'; import PropTypes from 'prop-types'; -import { getWeightedPermissions } from '../../../helpers/utils/permission'; +import { + getRightIcon, + getWeightedPermissions, +} from '../../../helpers/utils/permission'; import { useI18nContext } from '../../../hooks/useI18nContext'; -import PermissionCell from '../permission-cell'; -import Box from '../../ui/box'; + +/** + * Get one or more permission descriptions for a permission name. + * + * @param permission - The permission to render. + * @param index - The index of the permission. + * @returns {JSX.Element} A permission description node. + */ +function getDescriptionNode(permission, index) { + const { label, leftIcon, permissionName } = permission; + + return ( +
+ {typeof leftIcon === 'string' ? : leftIcon} + {label} + {getRightIcon(permission)} +
+ ); +} export default function PermissionsConnectPermissionList({ permissions, @@ -12,22 +32,11 @@ export default function PermissionsConnectPermissionList({ const t = useI18nContext(); return ( - +
{getWeightedPermissions(t, permissions, targetSubjectMetadata).map( - (permission, index) => { - return ( - - ); - }, + getDescriptionNode, )} - +
); } diff --git a/ui/components/app/permissions-connect-permission-list/permissions-connect-permission-list.stories.js b/ui/components/app/permissions-connect-permission-list/permissions-connect-permission-list.stories.js index dd5cfb9ba..9ee800b03 100644 --- a/ui/components/app/permissions-connect-permission-list/permissions-connect-permission-list.stories.js +++ b/ui/components/app/permissions-connect-permission-list/permissions-connect-permission-list.stories.js @@ -20,18 +20,5 @@ DefaultStory.storyName = 'Default'; DefaultStory.args = { permissions: { eth_accounts: {}, - snap_dialog: {}, - snap_getBip32PublicKey: { - caveats: [ - { - value: [ - { - path: ['m', `44'`, `0'`], - curve: 'secp256k1', - }, - ], - }, - ], - }, }, }; diff --git a/ui/helpers/utils/permission.js b/ui/helpers/utils/permission.js index b931f0656..09b7e1600 100644 --- a/ui/helpers/utils/permission.js +++ b/ui/helpers/utils/permission.js @@ -15,13 +15,23 @@ import { } from '../../../shared/constants/permissions'; import Tooltip from '../../components/ui/tooltip'; import { + AvatarIcon, ///: BEGIN:ONLY_INCLUDE_IN(flask) Text, + Icon, ///: END:ONLY_INCLUDE_IN } from '../../components/component-library'; -import { ICON_NAMES } from '../../components/component-library/icon/deprecated'; +import { + ICON_NAMES, + ICON_SIZES, +} from '../../components/component-library/icon/deprecated'; ///: BEGIN:ONLY_INCLUDE_IN(flask) -import { Color, FONT_WEIGHT, TextVariant } from '../constants/design-system'; +import { + Color, + FONT_WEIGHT, + IconColor, + TextVariant, +} from '../constants/design-system'; import { coinTypeToProtocolName, getSnapDerivationPathName, @@ -31,10 +41,33 @@ import { const UNKNOWN_PERMISSION = Symbol('unknown'); +///: BEGIN:ONLY_INCLUDE_IN(flask) +const RIGHT_INFO_ICON = ( + +); +///: END:ONLY_INCLUDE_IN + +function getLeftIcon(iconName) { + return ( + + ); +} + export const PERMISSION_DESCRIPTIONS = deepFreeze({ [RestrictedMethods.eth_accounts]: ({ t }) => ({ label: t('permission_ethereumAccounts'), - leftIcon: ICON_NAMES.EYE, + leftIcon: getLeftIcon(ICON_NAMES.EYE), + rightIcon: null, weight: 2, }), ///: BEGIN:ONLY_INCLUDE_IN(flask) @@ -251,7 +284,8 @@ export const PERMISSION_DESCRIPTIONS = deepFreeze({ [RestrictedMethods.wallet_snap]: ({ t, permissionValue }) => { const snaps = permissionValue.caveats[0].value; const baseDescription = { - leftIcon: ICON_NAMES.FLASH, + leftIcon: getLeftIcon(ICON_NAMES.FLASH), + rightIcon: RIGHT_INFO_ICON, }; return Object.keys(snaps).map((snapId) => { @@ -373,7 +407,8 @@ export const PERMISSION_DESCRIPTIONS = deepFreeze({ ///: END:ONLY_INCLUDE_IN [UNKNOWN_PERMISSION]: ({ t, permissionName }) => ({ label: t('permission_unknown', [permissionName ?? 'undefined']), - leftIcon: ICON_NAMES.QUESTION, + leftIcon: getLeftIcon(ICON_NAMES.QUESTION), + rightIcon: null, weight: 4, }), }); diff --git a/ui/pages/permissions-connect/flask/snap-install/snap-install.js b/ui/pages/permissions-connect/flask/snap-install/snap-install.js index 34a0cbcd3..d4b712623 100644 --- a/ui/pages/permissions-connect/flask/snap-install/snap-install.js +++ b/ui/pages/permissions-connect/flask/snap-install/snap-install.js @@ -1,7 +1,6 @@ import PropTypes from 'prop-types'; import React, { useCallback, useState } from 'react'; import { PageContainerFooter } from '../../../../components/ui/page-container'; -import PermissionsConnectPermissionList from '../../../../components/app/permissions-connect-permission-list'; import { useI18nContext } from '../../../../hooks/useI18nContext'; import SnapInstallWarning from '../../../../components/app/flask/snap-install-warning'; import Box from '../../../../components/ui/box/box'; @@ -21,6 +20,7 @@ import SnapAuthorship from '../../../../components/app/flask/snap-authorship'; import { Text } from '../../../../components/component-library'; import { useOriginMetadata } from '../../../../hooks/useOriginMetadata'; import { getSnapName } from '../../../../helpers/utils/util'; +import SnapPermissionsList from '../../../../components/app/flask/snap-permissions-list'; export default function SnapInstall({ request, @@ -87,12 +87,16 @@ export default function SnapInstall({ className="headers" alignItems={AlignItems.center} flexDirection={FLEX_DIRECTION.COLUMN} - paddingLeft={4} - paddingRight={4} > - + + + {!hasError && ( - + {t('snapInstall')} )} @@ -114,6 +118,8 @@ export default function SnapInstall({ {t('snapInstallRequestsPermission', [ @@ -121,7 +127,7 @@ export default function SnapInstall({ {snapName}, ])} - diff --git a/ui/pages/settings/flask/view-snap/view-snap.js b/ui/pages/settings/flask/view-snap/view-snap.js index 3261652d9..a9ab3a096 100644 --- a/ui/pages/settings/flask/view-snap/view-snap.js +++ b/ui/pages/settings/flask/view-snap/view-snap.js @@ -19,7 +19,6 @@ import SnapAuthorship from '../../../../components/app/flask/snap-authorship'; import Box from '../../../../components/ui/box'; import SnapRemoveWarning from '../../../../components/app/flask/snap-remove-warning'; import ToggleButton from '../../../../components/ui/toggle-button'; -import PermissionsConnectPermissionList from '../../../../components/app/permissions-connect-permission-list/permissions-connect-permission-list'; import ConnectedSitesList from '../../../../components/app/connected-sites-list'; import Tooltip from '../../../../components/ui/tooltip'; import { SNAPS_LIST_ROUTE } from '../../../../helpers/constants/routes'; @@ -38,6 +37,7 @@ import { getTargetSubjectMetadata, } from '../../../../selectors'; import { formatDate } from '../../../../helpers/utils/util'; +import SnapPermissionsList from '../../../../components/app/flask/snap-permissions-list'; function ViewSnap() { const t = useI18nContext(); @@ -182,7 +182,7 @@ function ViewSnap() { {t('snapAccess', [snap.manifest.proposedName])} - From 8fdbd07c91309568e1d148c47ed265f60a92d74f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ant=C3=B3nio=20Regadas?= Date: Fri, 14 Apr 2023 11:35:58 +0100 Subject: [PATCH 28/36] [MMI] Interactive replacement token modal (#18523) * updates styles for confirm-add-institutional-feature * initial component * adds tests and storybook file * clean up styles and adds MM design system * prettier * locale update * lint * snapshot update --------- Co-authored-by: legobeat <109787230+legobeat@users.noreply.github.com> --- app/_locales/en/messages.json | 15 ++ .../index.js | 1 + .../interactive-replacement-token-modal.js | 154 ++++++++++++++++++ ...ractive-replacement-token-modal.stories.js | 72 ++++++++ ...nteractive-replacement-token-modal.test.js | 91 +++++++++++ ...irm-add-institutional-feature.test.js.snap | 16 +- .../confirm-add-institutional-feature.js | 25 +-- 7 files changed, 357 insertions(+), 17 deletions(-) create mode 100644 ui/components/institutional/interactive-replacement-token-modal/index.js create mode 100644 ui/components/institutional/interactive-replacement-token-modal/interactive-replacement-token-modal.js create mode 100644 ui/components/institutional/interactive-replacement-token-modal/interactive-replacement-token-modal.stories.js create mode 100644 ui/components/institutional/interactive-replacement-token-modal/interactive-replacement-token-modal.test.js diff --git a/app/_locales/en/messages.json b/app/_locales/en/messages.json index 382dd314a..5d7a113e0 100644 --- a/app/_locales/en/messages.json +++ b/app/_locales/en/messages.json @@ -929,6 +929,21 @@ "custodianAccount": { "message": "Custodian account" }, + "custodyRefreshTokenModalDescription": { + "message": "Please go to $1 and click the 'Connect to MMI' button within their user interface to connect your accounts to MMI again." + }, + "custodyRefreshTokenModalDescription1": { + "message": "Your custodian issues a token that authenticates the MetaMask Institutional extension, allowing you to connect your accounts." + }, + "custodyRefreshTokenModalDescription2": { + "message": "This token expires after a certain period for security reasons. This requires you to reconnect to MMI." + }, + "custodyRefreshTokenModalSubtitle": { + "message": "Why am I seeing this?" + }, + "custodyRefreshTokenModalTitle": { + "message": "Your custodian session has expired" + }, "custom": { "message": "Advanced" }, diff --git a/ui/components/institutional/interactive-replacement-token-modal/index.js b/ui/components/institutional/interactive-replacement-token-modal/index.js new file mode 100644 index 000000000..1f1b5f7f6 --- /dev/null +++ b/ui/components/institutional/interactive-replacement-token-modal/index.js @@ -0,0 +1 @@ +export { default } from './interactive-replacement-token-modal'; diff --git a/ui/components/institutional/interactive-replacement-token-modal/interactive-replacement-token-modal.js b/ui/components/institutional/interactive-replacement-token-modal/interactive-replacement-token-modal.js new file mode 100644 index 000000000..0f40723d4 --- /dev/null +++ b/ui/components/institutional/interactive-replacement-token-modal/interactive-replacement-token-modal.js @@ -0,0 +1,154 @@ +import React, { useContext } from 'react'; +import { useDispatch, useSelector } from 'react-redux'; +import Modal from '../../app/modal'; +import { useI18nContext } from '../../../hooks/useI18nContext'; +import { MetaMetricsContext } from '../../../contexts/metametrics'; +import { hideModal } from '../../../store/actions'; +import { getSelectedAddress } from '../../../selectors/selectors'; +import { toChecksumHexAddress } from '../../../../shared/modules/hexstring-utils'; +import { Text } from '../../component-library'; +import Box from '../../ui/box'; +import { + BLOCK_SIZES, + BackgroundColor, + DISPLAY, + FLEX_WRAP, + FLEX_DIRECTION, + BorderRadius, + FONT_WEIGHT, + TEXT_ALIGN, + AlignItems, +} from '../../../helpers/constants/design-system'; + +const InteractiveReplacementTokenModal = () => { + const t = useI18nContext(); + const trackEvent = useContext(MetaMetricsContext); + const dispatch = useDispatch(); + + const { url } = useSelector( + (state) => state.metamask.interactiveReplacementToken || {}, + ); + + const { custodians } = useSelector( + (state) => state.metamask.mmiConfiguration, + ); + const address = useSelector(getSelectedAddress); + const custodyAccountDetails = useSelector( + (state) => + state.metamask.custodyAccountDetails[toChecksumHexAddress(address)], + ); + + const custodianName = custodyAccountDetails?.custodianName; + const custodian = + custodians.find((item) => item.name === custodianName) || {}; + + const renderCustodyInfo = () => { + let img; + + if (custodian.iconUrl) { + img = ( + + + {custodian.displayName} + + + ); + } else { + img = ( + + {custodian.displayName} + + ); + } + + return ( + <> + {img} + + {t('custodyRefreshTokenModalTitle')} + + + {t('custodyRefreshTokenModalDescription', [custodian.displayName])} + + + {t('custodyRefreshTokenModalSubtitle')} + + + {t('custodyRefreshTokenModalDescription1')} + + + {t('custodyRefreshTokenModalDescription2')} + + + ); + }; + + const handleSubmit = () => { + global.platform.openTab({ + url, + }); + + trackEvent({ + category: 'MMI', + event: 'User clicked refresh token link', + }); + }; + + const handleClose = () => { + dispatch(hideModal()); + }; + + return ( + + + {renderCustodyInfo(custodian)} + + + ); +}; + +export default InteractiveReplacementTokenModal; diff --git a/ui/components/institutional/interactive-replacement-token-modal/interactive-replacement-token-modal.stories.js b/ui/components/institutional/interactive-replacement-token-modal/interactive-replacement-token-modal.stories.js new file mode 100644 index 000000000..86c03504c --- /dev/null +++ b/ui/components/institutional/interactive-replacement-token-modal/interactive-replacement-token-modal.stories.js @@ -0,0 +1,72 @@ +import React from 'react'; +import { Provider } from 'react-redux'; +import configureStore from '../../../store/store'; +import testData from '../../../../.storybook/test-data'; +import InteractiveReplacementTokenModal from '.'; + +const customData = { + ...testData, + metamask: { + ...testData.metamask, + mmiConfiguration: { + portfolio: { + enabled: true, + url: 'https://dev.metamask-institutional.io/', + }, + features: { + websocketApi: true, + }, + custodians: [ + { + refreshTokenUrl: + 'https://saturn-custody.dev.metamask-institutional.io/oauth/token', + name: 'saturn-dev', + displayName: 'Saturn Custody', + enabled: true, + mmiApiUrl: 'https://api.dev.metamask-institutional.io/v1', + websocketApiUrl: + 'wss://websocket.dev.metamask-institutional.io/v1/ws', + apiBaseUrl: + 'https://saturn-custody.dev.metamask-institutional.io/eth', + iconUrl: + 'https://saturn-custody-ui.dev.metamask-institutional.io/saturn.svg', + isNoteToTraderSupported: true, + }, + ], + }, + custodyAccountDetails: { + '0xAddress': { + address: '0xAddress', + details: 'details', + custodyType: 'testCustody - Saturn', + custodianName: 'saturn-dev', + }, + }, + provider: { + type: 'test', + }, + selectedAddress: '0xAddress', + isUnlocked: true, + interactiveReplacementToken: { + oldRefreshToken: 'abc', + url: 'https://saturn-custody-ui.dev.metamask-institutional.io', + }, + preferences: { + useNativeCurrencyAsPrimaryCurrency: true, + }, + }, +}; + +const store = configureStore(customData); + +export default { + title: 'Components/Institutional/InteractiveReplacementToken-Modal', + decorators: [(story) => {story()}], + component: InteractiveReplacementTokenModal, +}; + +export const DefaultStory = (args) => ( + +); + +DefaultStory.storyName = 'InteractiveReplacementTokenModal'; diff --git a/ui/components/institutional/interactive-replacement-token-modal/interactive-replacement-token-modal.test.js b/ui/components/institutional/interactive-replacement-token-modal/interactive-replacement-token-modal.test.js new file mode 100644 index 000000000..4a78a2237 --- /dev/null +++ b/ui/components/institutional/interactive-replacement-token-modal/interactive-replacement-token-modal.test.js @@ -0,0 +1,91 @@ +import React from 'react'; +import sinon from 'sinon'; +import configureMockStore from 'redux-mock-store'; +import { fireEvent, waitFor } from '@testing-library/react'; +import { renderWithProvider } from '../../../../test/lib/render-helpers'; +import testData from '../../../../.storybook/test-data'; +import InteractiveReplacementTokenModal from '.'; + +describe('Interactive Replacement Token Modal', function () { + const mockStore = { + ...testData, + metamask: { + ...testData.metamask, + mmiConfiguration: { + portfolio: { + enabled: true, + url: 'https://dev.metamask-institutional.io/', + }, + features: { + websocketApi: true, + }, + custodians: [ + { + refreshTokenUrl: + 'https://saturn-custody.dev.metamask-institutional.io/oauth/token', + name: 'saturn-dev', + displayName: 'Saturn Custody', + enabled: true, + mmiApiUrl: 'https://api.dev.metamask-institutional.io/v1', + websocketApiUrl: + 'wss://websocket.dev.metamask-institutional.io/v1/ws', + apiBaseUrl: + 'https://saturn-custody.dev.metamask-institutional.io/eth', + iconUrl: + 'https://saturn-custody-ui.dev.metamask-institutional.io/saturn.svg', + isNoteToTraderSupported: true, + }, + ], + }, + custodyAccountDetails: { + '0xAddress': { + address: '0xAddress', + details: 'details', + custodyType: 'testCustody - Saturn', + custodianName: 'saturn-dev', + }, + }, + provider: { + type: 'test', + }, + selectedAddress: '0xAddress', + isUnlocked: true, + interactiveReplacementToken: { + oldRefreshToken: 'abc', + url: 'https://saturn-custody-ui.dev.metamask-institutional.io', + }, + preferences: { + useNativeCurrencyAsPrimaryCurrency: true, + }, + }, + }; + + const store = configureMockStore()(mockStore); + + it('should render the interactive-replacement-token-modal', () => { + const { getByText, getByTestId } = renderWithProvider( + , + store, + ); + + expect(getByTestId('interactive-replacement-token-modal')).toBeVisible(); + expect(getByText('Your custodian session has expired')).toBeInTheDocument(); + }); + + it('opens new tab on Open Codefi Compliance click', async () => { + global.platform = { openTab: sinon.spy() }; + + const { container } = renderWithProvider( + , + store, + ); + + const button = container.getElementsByClassName('btn-primary')[0]; + + fireEvent.click(button); + + await waitFor(() => { + expect(global.platform.openTab.calledOnce).toStrictEqual(true); + }); + }); +}); diff --git a/ui/pages/institutional/confirm-add-institutional-feature/__snapshots__/confirm-add-institutional-feature.test.js.snap b/ui/pages/institutional/confirm-add-institutional-feature/__snapshots__/confirm-add-institutional-feature.test.js.snap index 2d68b5531..10ee36b9b 100644 --- a/ui/pages/institutional/confirm-add-institutional-feature/__snapshots__/confirm-add-institutional-feature.test.js.snap +++ b/ui/pages/institutional/confirm-add-institutional-feature/__snapshots__/confirm-add-institutional-feature.test.js.snap @@ -42,26 +42,28 @@ exports[`Confirm Add Institutional Feature opens confirm institutional sucessful

- +
+
`; diff --git a/ui/pages/institutional/confirm-add-institutional-feature/confirm-add-institutional-feature.js b/ui/pages/institutional/confirm-add-institutional-feature/confirm-add-institutional-feature.js index 25a52fc16..0e89910e2 100644 --- a/ui/pages/institutional/confirm-add-institutional-feature/confirm-add-institutional-feature.js +++ b/ui/pages/institutional/confirm-add-institutional-feature/confirm-add-institutional-feature.js @@ -7,12 +7,17 @@ import PulseLoader from '../../../components/ui/pulse-loader'; import { INSTITUTIONAL_FEATURES_DONE_ROUTE } from '../../../helpers/constants/routes'; import { MetaMetricsContext } from '../../../contexts/metametrics'; import { getMostRecentOverviewPage } from '../../../ducks/history/history'; -import { Text } from '../../../components/component-library'; +import { + Text, + BUTTON_SIZES, + BUTTON_TYPES, +} from '../../../components/component-library'; import { TextColor, TextVariant, OVERFLOW_WRAP, TEXT_ALIGN, + DISPLAY, } from '../../../helpers/constants/design-system'; import Box from '../../../components/ui/box'; import { mmiActionsFactory } from '../../../store/institutional/institution-background'; @@ -169,16 +174,15 @@ export default function ConfirmAddInstitutionalFeature({ history }) { )} - + {isLoading ? ( -
- -
+ ) : ( -
+ -
+
)}
From 26db0aee46ad5dcac7753fe5cd685ca14c21c20d Mon Sep 17 00:00:00 2001 From: Elliot Winkler Date: Fri, 14 Apr 2023 10:21:29 -0600 Subject: [PATCH 29/36] Convert NetworkController net client tests to TS (#18490) In order to be able to better compare differences between the version of NetworkController in this repo and the version in the `core` repo before we replace this version with the `core` version, this commit converts the NetworkController network client tests to TypeScript. The added types here are copied from the `core` repo. We plan on making more improvements on the `core` side at some point to polish the tests and types and reduce some of the duplication, but for now we're just trying to keep things as similar as possible. --- .eslintrc.js | 2 +- ....test.js => create-network-client.test.ts} | 0 ...-response.js => block-hash-in-response.ts} | 18 +- .../{block-param.js => block-param.ts} | 19 +- .../{helpers.js => helpers.ts} | 400 ++++++++++-------- .../{no-block-param.js => no-block-param.ts} | 13 +- ...leware.js => not-handled-by-middleware.ts} | 18 +- .../{shared-tests.js => shared-tests.ts} | 14 +- package.json | 1 + yarn.lock | 8 + 10 files changed, 297 insertions(+), 196 deletions(-) rename app/scripts/controllers/network/{create-network-client.test.js => create-network-client.test.ts} (100%) rename app/scripts/controllers/network/provider-api-tests/{block-hash-in-response.js => block-hash-in-response.ts} (96%) rename app/scripts/controllers/network/provider-api-tests/{block-param.js => block-param.ts} (99%) rename app/scripts/controllers/network/provider-api-tests/{helpers.js => helpers.ts} (56%) rename app/scripts/controllers/network/provider-api-tests/{no-block-param.js => no-block-param.ts} (99%) rename app/scripts/controllers/network/provider-api-tests/{not-handled-by-middleware.js => not-handled-by-middleware.ts} (83%) rename app/scripts/controllers/network/provider-api-tests/{shared-tests.js => shared-tests.ts} (98%) diff --git a/.eslintrc.js b/.eslintrc.js index bcbfb44b7..89578fa19 100644 --- a/.eslintrc.js +++ b/.eslintrc.js @@ -267,7 +267,7 @@ module.exports = { 'app/scripts/controllers/app-state.test.js', 'app/scripts/controllers/network/**/*.test.js', 'app/scripts/controllers/network/**/*.test.ts', - 'app/scripts/controllers/network/provider-api-tests/*.js', + 'app/scripts/controllers/network/provider-api-tests/*.ts', 'app/scripts/controllers/permissions/**/*.test.js', 'app/scripts/lib/**/*.test.js', 'app/scripts/migrations/*.test.js', diff --git a/app/scripts/controllers/network/create-network-client.test.js b/app/scripts/controllers/network/create-network-client.test.ts similarity index 100% rename from app/scripts/controllers/network/create-network-client.test.js rename to app/scripts/controllers/network/create-network-client.test.ts diff --git a/app/scripts/controllers/network/provider-api-tests/block-hash-in-response.js b/app/scripts/controllers/network/provider-api-tests/block-hash-in-response.ts similarity index 96% rename from app/scripts/controllers/network/provider-api-tests/block-hash-in-response.js rename to app/scripts/controllers/network/provider-api-tests/block-hash-in-response.ts index c1778f628..4ee0f633d 100644 --- a/app/scripts/controllers/network/provider-api-tests/block-hash-in-response.js +++ b/app/scripts/controllers/network/provider-api-tests/block-hash-in-response.ts @@ -1,6 +1,15 @@ /* eslint-disable jest/require-top-level-describe, jest/no-export */ -import { withMockedCommunications, withNetworkClient } from './helpers'; +import { + ProviderType, + withMockedCommunications, + withNetworkClient, +} from './helpers'; + +type TestsForRpcMethodThatCheckForBlockHashInResponseOptions = { + providerType: ProviderType; + numberOfParameters: number; +}; /** * Defines tests which exercise the behavior exhibited by an RPC method that @@ -15,8 +24,11 @@ import { withMockedCommunications, withNetworkClient } from './helpers'; * either `infura` or `custom` (default: "infura"). */ export function testsForRpcMethodsThatCheckForBlockHashInResponse( - method, - { numberOfParameters, providerType }, + method: string, + { + numberOfParameters, + providerType, + }: TestsForRpcMethodThatCheckForBlockHashInResponseOptions, ) { if (providerType !== 'infura' && providerType !== 'custom') { throw new Error( diff --git a/app/scripts/controllers/network/provider-api-tests/block-param.js b/app/scripts/controllers/network/provider-api-tests/block-param.ts similarity index 99% rename from app/scripts/controllers/network/provider-api-tests/block-param.js rename to app/scripts/controllers/network/provider-api-tests/block-param.ts index 49bd6e772..92fb65f04 100644 --- a/app/scripts/controllers/network/provider-api-tests/block-param.js +++ b/app/scripts/controllers/network/provider-api-tests/block-param.ts @@ -3,6 +3,7 @@ import { buildMockParams, buildRequestWithReplacedBlockParam, + ProviderType, waitForPromiseToBeFulfilledAfterRunningAllTimers, withMockedCommunications, withNetworkClient, @@ -13,6 +14,12 @@ import { buildJsonRpcEngineEmptyResponseErrorMessage, } from './shared-tests'; +type TestsForRpcMethodSupportingBlockParam = { + providerType: ProviderType; + blockParamIndex: number; + numberOfParameters: number; +}; + /** * Defines tests which exercise the behavior exhibited by an RPC method that * takes a block parameter. The value of this parameter can be either a block @@ -28,8 +35,12 @@ import { */ /* eslint-disable-next-line jest/no-export */ export function testsForRpcMethodSupportingBlockParam( - method, - { blockParamIndex, numberOfParameters, providerType }, + method: string, + { + blockParamIndex, + numberOfParameters, + providerType, + }: TestsForRpcMethodSupportingBlockParam, ) { describe.each([ ['given no block tag', undefined], @@ -1718,9 +1729,9 @@ export function testsForRpcMethodSupportingBlockParam( [ ['less than the current block number', '0x200'], ['equal to the curent block number', '0x100'], - ], + ] as any, '%s', - (_nestedDesc, currentBlockNumber) => { + (_nestedDesc: string, currentBlockNumber: string) => { it('makes an additional request to the RPC endpoint', async () => { await withMockedCommunications({ providerType }, async (comms) => { const request = { diff --git a/app/scripts/controllers/network/provider-api-tests/helpers.js b/app/scripts/controllers/network/provider-api-tests/helpers.ts similarity index 56% rename from app/scripts/controllers/network/provider-api-tests/helpers.js rename to app/scripts/controllers/network/provider-api-tests/helpers.ts index afa91a7bb..edcc1ca02 100644 --- a/app/scripts/controllers/network/provider-api-tests/helpers.js +++ b/app/scripts/controllers/network/provider-api-tests/helpers.ts @@ -1,14 +1,13 @@ -import nock from 'nock'; +import nock, { Scope as NockScope } from 'nock'; import sinon from 'sinon'; +import type { JSONRPCResponse } from '@json-rpc-specification/meta-schema'; import EthQuery from 'eth-query'; -import { createNetworkClient } from '../create-network-client'; - -/** - * @typedef {import('nock').Scope} NockScope - * - * A object returned by the `nock` function for mocking requests to a particular - * base URL. - */ +import { Hex } from '@metamask/utils'; +import { BuiltInInfuraNetwork } from '../../../../../shared/constants/network'; +import { + createNetworkClient, + NetworkClientType, +} from '../create-network-client'; /** * A dummy value for the `infuraProjectId` option that `createInfuraClient` @@ -41,9 +40,9 @@ const originalSetTimeout = setTimeout; * keeps failing, you can set `process.env.DEBUG_PROVIDER_TESTS` to `1`. This * will turn on some extra logging. * - * @param {any[]} args - The arguments that `console.log` takes. + * @param args - The arguments that `console.log` takes. */ -function debug(...args) { +function debug(...args: any) { if (process.env.DEBUG_PROVIDER_TESTS === '1') { console.log(...args); } @@ -52,96 +51,89 @@ function debug(...args) { /** * Builds a Nock scope object for mocking provider requests. * - * @param {string} rpcUrl - The URL of the RPC endpoint. - * @returns {NockScope} The nock scope. + * @param rpcUrl - The URL of the RPC endpoint. + * @returns The nock scope. */ -function buildScopeForMockingRequests(rpcUrl) { +function buildScopeForMockingRequests(rpcUrl: string): NockScope { return nock(rpcUrl).filteringRequestBody((body) => { debug('Nock Received Request: ', body); return body; }); } -/** - * @typedef {{ nockScope: NockScope, blockNumber: string }} MockBlockTrackerRequestOptions - * - * The options to `mockNextBlockTrackerRequest` and `mockAllBlockTrackerRequests`. - */ +type Request = { method: string; params?: any[] }; +type Response = { + id?: number | string; + jsonrpc?: '2.0'; + error?: any; + result?: any; + httpStatus?: number; +}; +type ResponseBody = { body: JSONRPCResponse }; +type BodyOrResponse = ResponseBody | Response; +type CurriedMockRpcCallOptions = { + request: Request; + // The response data. + response?: BodyOrResponse; + /** + * An error to throw while making the request. + * Takes precedence over `response`. + */ + error?: Error | string; + /** + * The amount of time that should pass before the + * request resolves with the response. + */ + delay?: number; + /** + * The number of times that the request is + * expected to be made. + */ + times?: number; +}; -/** - * Mocks the next request for the latest block that the block tracker will make. - * - * @param {MockBlockTrackerRequestOptions} args - The arguments. - * @param {NockScope} args.nockScope - A nock scope (a set of mocked requests - * scoped to a certain base URL). - * @param {string} args.blockNumber - The block number that the block tracker - * should report, as a 0x-prefixed hex string. - */ -async function mockNextBlockTrackerRequest({ - nockScope, - blockNumber = DEFAULT_LATEST_BLOCK_NUMBER, -}) { - await mockRpcCall({ - nockScope, - request: { method: 'eth_blockNumber', params: [] }, - response: { result: blockNumber }, - }); -} +type MockRpcCallOptions = { + // A nock scope (a set of mocked requests scoped to a certain base URL). + nockScope: nock.Scope; +} & CurriedMockRpcCallOptions; -/** - * Mocks all requests for the latest block that the block tracker will make. - * - * @param {MockBlockTrackerRequestOptions} args - The arguments. - * @param {NockScope} args.nockScope - A nock scope (a set of mocked requests - * scoped to a certain base URL). - * @param {string} args.blockNumber - The block number that the block tracker - * should report, as a 0x-prefixed hex string. - */ -async function mockAllBlockTrackerRequests({ - nockScope, - blockNumber = DEFAULT_LATEST_BLOCK_NUMBER, -}) { - await mockRpcCall({ - nockScope, - request: { method: 'eth_blockNumber', params: [] }, - response: { result: blockNumber }, - }).persist(); -} - -/** - * @typedef {{ nockScope: NockScope, request: object, response: object, delay?: number }} MockRpcCallOptions - * - * The options to `mockRpcCall`. - */ +type MockRpcCallResult = nock.Interceptor | nock.Scope; /** * Mocks a JSON-RPC request sent to the provider with the given response. * Provider type is inferred from the base url set on the nockScope. * - * @param {MockRpcCallOptions} args - The arguments. - * @param {NockScope} args.nockScope - A nock scope (a set of mocked requests - * scoped to a certain base URL). - * @param {object} args.request - The request data. - * @param {{body: string} | {httpStatus?: number; id?: number; method?: string; params?: string[]}} [args.response] - Information - * concerning the response that the request should have. If a `body` property is - * present, this is taken as the complete response body. If an `httpStatus` - * property is present, then it is taken as the HTTP status code to respond - * with. Properties other than these two are used to build a complete response - * body (including `id` and `jsonrpc` properties). - * @param {Error | string} [args.error] - An error to throw while making the - * request. Takes precedence over `response`. - * @param {number} [args.delay] - The amount of time that should pass before the - * request resolves with the response. - * @param {number} [args.times] - The number of times that the request is - * expected to be made. - * @returns {NockScope} The nock scope. + * @param args - The arguments. + * @param args.nockScope - A nock scope (a set of mocked requests scoped to a + * certain base URL). + * @param args.request - The request data. + * @param args.response - Information concerning the response that the request + * should have. If a `body` property is present, this is taken as the complete + * response body. If an `httpStatus` property is present, then it is taken as + * the HTTP status code to respond with. Properties other than these two are + * used to build a complete response body (including `id` and `jsonrpc` + * properties). + * @param args.error - An error to throw while making the request. Takes + * precedence over `response`. + * @param args.delay - The amount of time that should pass before the request + * resolves with the response. + * @param args.times - The number of times that the request is expected to be + * made. + * @returns The nock scope. */ -function mockRpcCall({ nockScope, request, response, error, delay, times }) { +function mockRpcCall({ + nockScope, + request, + response, + error, + delay, + times, +}: MockRpcCallOptions): MockRpcCallResult { // eth-query always passes `params`, so even if we don't supply this property, // for consistency with makeRpcCall, assume that the `body` contains it const { method, params = [], ...rest } = request; let httpStatus = 200; - let completeResponse = { id: 2, jsonrpc: '2.0' }; + let completeResponse: JSONRPCResponse = { id: 2, jsonrpc: '2.0' }; if (response !== undefined) { if ('body' in response) { completeResponse = response.body; @@ -156,6 +148,7 @@ function mockRpcCall({ nockScope, request, response, error, delay, times }) { } } } + /* @ts-expect-error The types for Nock do not include `basePath` in the interface for Nock.Scope. */ const url = nockScope.basePath.includes('infura.io') ? `/v3/${MOCK_INFURA_PROJECT_ID}` : '/'; @@ -189,7 +182,7 @@ function mockRpcCall({ nockScope, request, response, error, delay, times }) { if (error !== undefined) { return nockRequest.replyWithError(error); } else if (completeResponse !== undefined) { - return nockRequest.reply(httpStatus, (_, requestBody) => { + return nockRequest.reply(httpStatus, (_, requestBody: any) => { if (response !== undefined && !('body' in response)) { if (response.id === undefined) { completeResponse.id = requestBody.id; @@ -204,16 +197,72 @@ function mockRpcCall({ nockScope, request, response, error, delay, times }) { return nockRequest; } +type MockBlockTrackerRequestOptions = { + /** + * A nock scope (a set of mocked requests scoped to a certain base url). + */ + nockScope: NockScope; + /** + * The block number that the block tracker should report, as a 0x-prefixed hex + * string. + */ + blockNumber: string; +}; + +/** + * Mocks the next request for the latest block that the block tracker will make. + * + * @param args - The arguments. + * @param args.nockScope - A nock scope (a set of mocked requests scoped to a + * certain base URL). + * @param args.blockNumber - The block number that the block tracker should + * report, as a 0x-prefixed hex string. + */ +function mockNextBlockTrackerRequest({ + nockScope, + blockNumber = DEFAULT_LATEST_BLOCK_NUMBER, +}: MockBlockTrackerRequestOptions) { + mockRpcCall({ + nockScope, + request: { method: 'eth_blockNumber', params: [] }, + response: { result: blockNumber }, + }); +} + +/** + * Mocks all requests for the latest block that the block tracker will make. + * + * @param args - The arguments. + * @param args.nockScope - A nock scope (a set of mocked requests scoped to a + * certain base URL). + * @param args.blockNumber - The block number that the block tracker should + * report, as a 0x-prefixed hex string. + */ +async function mockAllBlockTrackerRequests({ + nockScope, + blockNumber = DEFAULT_LATEST_BLOCK_NUMBER, +}: MockBlockTrackerRequestOptions) { + const result = await mockRpcCall({ + nockScope, + request: { method: 'eth_blockNumber', params: [] }, + response: { result: blockNumber }, + }); + + if ('persist' in result) { + result.persist(); + } +} + /** * Makes a JSON-RPC call through the given eth-query object. * - * @param {any} ethQuery - The eth-query object. - * @param {object} request - The request data. - * @returns {Promise} A promise that either resolves with the result from - * the JSON-RPC response if it is successful or rejects with the error from the - * JSON-RPC response otherwise. + * @param ethQuery - The eth-query object. + * @param request - The request data. + * @returns A promise that either resolves with the result from the JSON-RPC + * response if it is successful or rejects with the error from the JSON-RPC + * response otherwise. */ -function makeRpcCall(ethQuery, request) { +function makeRpcCall(ethQuery: EthQuery, request: Request) { return new Promise((resolve, reject) => { debug('[makeRpcCall] making request', request); ethQuery.sendAsync(request, (error, result) => { @@ -227,41 +276,43 @@ function makeRpcCall(ethQuery, request) { }); } -/** - * @typedef {{providerType: 'infura' | 'custom', infuraNetwork?: string}} WithMockedCommunicationsOptions - * - * The options bag that `Communications` takes. - */ +export type ProviderType = 'infura' | 'custom'; -/** - * @typedef {{mockNextBlockTrackerRequest: (options: Omit) => void, mockAllBlockTrackerRequests: (options: Omit) => void, mockRpcCall: (options: Omit) => NockScope, rpcUrl: string, infuraNetwork: string}} Communications - * - * Provides methods to mock different kinds of requests to the provider. - */ +export type MockOptions = { + infuraNetwork?: BuiltInInfuraNetwork; + providerType: ProviderType; + customRpcUrl?: string; + customChainId?: Hex; +}; -/** - * @typedef {(comms: Communications) => Promise} WithMockedCommunicationsCallback - * - * The callback that `mockingCommunications` takes. - */ +export type MockCommunications = { + mockNextBlockTrackerRequest: (options?: any) => void; + mockAllBlockTrackerRequests: (options?: any) => void; + mockRpcCall: (options: CurriedMockRpcCallOptions) => MockRpcCallResult; + rpcUrl: string; + infuraNetwork: BuiltInInfuraNetwork; +}; /** * Sets up request mocks for requests to the provider. * - * @param {WithMockedCommunicationsOptions} options - An options bag. - * @param {"infura" | "custom"} options.providerType - The type of network - * client being tested. - * @param {string} [options.infuraNetwork] - The name of the Infura network being - * tested, assuming that `providerType` is "infura" (default: "mainnet"). - * @param {string} [options.customRpcUrl] - The URL of the custom RPC endpoint, - * assuming that `providerType` is "custom". - * @param {WithMockedCommunicationsCallback} fn - A function which will be - * called with an object that allows interaction with the network client. - * @returns {Promise} The return value of the given function. + * @param options - An options bag. + * @param options.providerType - The type of network client being tested. + * @param options.infuraNetwork - The name of the Infura network being tested, + * assuming that `providerType` is "infura" (default: "mainnet"). + * @param options.customRpcUrl - The URL of the custom RPC endpoint, assuming + * that `providerType` is "custom". + * @param fn - A function which will be called with an object that allows + * interaction with the network client. + * @returns The return value of the given function. */ export async function withMockedCommunications( - { providerType, infuraNetwork = 'mainnet', customRpcUrl = MOCK_RPC_URL }, - fn, + { + providerType, + infuraNetwork = 'mainnet', + customRpcUrl = MOCK_RPC_URL, + }: MockOptions, + fn: (comms: MockCommunications) => Promise, ) { if (providerType !== 'infura' && providerType !== 'custom') { throw new Error( @@ -274,11 +325,11 @@ export async function withMockedCommunications( ? `https://${infuraNetwork}.infura.io` : customRpcUrl; const nockScope = buildScopeForMockingRequests(rpcUrl); - const curriedMockNextBlockTrackerRequest = (localOptions) => + const curriedMockNextBlockTrackerRequest = (localOptions: any) => mockNextBlockTrackerRequest({ nockScope, ...localOptions }); - const curriedMockAllBlockTrackerRequests = (localOptions) => + const curriedMockAllBlockTrackerRequests = (localOptions: any) => mockAllBlockTrackerRequests({ nockScope, ...localOptions }); - const curriedMockRpcCall = (localOptions) => + const curriedMockRpcCall = (localOptions: any) => mockRpcCall({ nockScope, ...localOptions }); const comms = { @@ -297,12 +348,12 @@ export async function withMockedCommunications( } } -/** - * @typedef {{blockTracker: import('eth-block-tracker').PollingBlockTracker, clock: sinon.SinonFakeTimers, makeRpcCall: (request: Partial) => Promise, makeRpcCallsInSeries: (requests: Partial[]) => Promise}} MockNetworkClient - * - * Provides methods to interact with the suite of middleware that - * `createInfuraClient` or `createJsonRpcClient` exposes. - */ +type MockNetworkClient = { + blockTracker: any; + clock: sinon.SinonFakeTimers; + makeRpcCall: (request: Request) => Promise; + makeRpcCallsInSeries: (requests: Request[]) => Promise; +}; /** * Some middleware contain logic which retries the request if some condition @@ -321,14 +372,14 @@ export async function withMockedCommunications( * `setTimeout` handler. */ export async function waitForPromiseToBeFulfilledAfterRunningAllTimers( - promise, - clock, + promise: any, + clock: any, ) { let hasPromiseBeenFulfilled = false; let numTimesClockHasBeenAdvanced = 0; promise - .catch((error) => { + .catch((error: any) => { // This is used to silence Node.js warnings about the rejection // being handled asynchronously. The error is handled later when // `promise` is awaited, but we log it here anyway in case it gets @@ -350,36 +401,22 @@ export async function waitForPromiseToBeFulfilledAfterRunningAllTimers( return promise; } -/** - * @typedef {{providerType: "infura" | "custom", infuraNetwork?: string, customRpcUrl?: string, customChainId?: string}} WithClientOptions - * - * The options bag that `withNetworkClient` takes. - */ - -/** - * @typedef {(client: MockNetworkClient) => Promise} WithClientCallback - * - * The callback that `withNetworkClient` takes. - */ - /** * Builds a provider from the middleware (for the provider type) along with a * block tracker, runs the given function with those two things, and then * ensures the block tracker is stopped at the end. * - * @param {WithClientOptions} options - An options bag. - * @param {"infura" | "custom"} options.providerType - The type of network - * client being tested. - * @param {string} [options.infuraNetwork] - The name of the Infura network being - * tested, assuming that `providerType` is "infura" (default: "mainnet"). - * @param {string} [options.customRpcUrl] - The URL of the custom RPC endpoint, - * assuming that `providerType` is "custom". - * @param {string} [options.customChainId] - The chain id belonging to the - * custom RPC endpoint, assuming that `providerType` is "custom" (default: - * "0x1"). - * @param {WithClientCallback} fn - A function which will be called with an - * object that allows interaction with the network client. - * @returns {Promise} The return value of the given function. + * @param options - An options bag. + * @param options.providerType - The type of network client being tested. + * @param options.infuraNetwork - The name of the Infura network being tested, + * assuming that `providerType` is "infura" (default: "mainnet"). + * @param options.customRpcUrl - The URL of the custom RPC endpoint, assuming + * that `providerType` is "custom". + * @param options.customChainId - The chain id belonging to the custom RPC + * endpoint, assuming that `providerType` is "custom" (default: "0x1"). + * @param fn - A function which will be called with an object that allows + * interaction with the network client. + * @returns The return value of the given function. */ export async function withNetworkClient( { @@ -387,8 +424,8 @@ export async function withNetworkClient( infuraNetwork = 'mainnet', customRpcUrl = MOCK_RPC_URL, customChainId = '0x1', - }, - fn, + }: MockOptions, + fn: (client: MockNetworkClient) => Promise, ) { if (providerType !== 'infura' && providerType !== 'custom') { throw new Error( @@ -414,20 +451,21 @@ export async function withNetworkClient( ? createNetworkClient({ network: infuraNetwork, infuraProjectId: MOCK_INFURA_PROJECT_ID, - type: 'infura', + type: NetworkClientType.Infura, }) : createNetworkClient({ chainId: customChainId, rpcUrl: customRpcUrl, - type: 'custom', + type: NetworkClientType.Custom, }); process.env.IN_TEST = inTest; const { provider, blockTracker } = clientUnderTest; const ethQuery = new EthQuery(provider); - const curriedMakeRpcCall = (request) => makeRpcCall(ethQuery, request); - const makeRpcCallsInSeries = async (requests) => { + const curriedMakeRpcCall = (request: Request) => + makeRpcCall(ethQuery, request); + const makeRpcCallsInSeries = async (requests: Request[]) => { const responses = []; for (const request of requests) { responses.push(await curriedMakeRpcCall(request)); @@ -451,6 +489,13 @@ export async function withNetworkClient( } } +type BuildMockParamsOptions = { + // The block parameter value to set. + blockParam: any; + // The index of the block parameter. + blockParamIndex: number; +}; + /** * Build mock parameters for a JSON-RPC call. * @@ -460,16 +505,15 @@ export async function withNetworkClient( * The block parameter can be set to a custom value. If no value is given, it * is set as undefined. * - * @param {object} args - Arguments. - * @param {number} args.blockParamIndex - The index of the block parameter. - * @param {any} [args.blockParam] - The block parameter value to set. - * @returns {any[]} The mock params. + * @param args - Arguments. + * @param args.blockParamIndex - The index of the block parameter. + * @param args.blockParam - The block parameter value to set. + * @returns The mock params. */ -export function buildMockParams({ blockParam, blockParamIndex }) { - if (blockParamIndex === undefined) { - throw new Error(`Missing 'blockParamIndex'`); - } - +export function buildMockParams({ + blockParam, + blockParamIndex, +}: BuildMockParamsOptions) { const params = new Array(blockParamIndex).fill('some value'); params[blockParamIndex] = blockParam; @@ -480,18 +524,18 @@ export function buildMockParams({ blockParam, blockParamIndex }) { * Returns a partial JSON-RPC request object, with the "block" param replaced * with the given value. * - * @param {object} request - The request object. - * @param {string} request.method - The request method. - * @param {params} [request.params] - The request params. - * @param {number} blockParamIndex - The index within the `params` array of the - * block param. - * @param {any} blockParam - The desired block param value. - * @returns {object} The updated request object. + * @param request - The request object. + * @param request.method - The request method. + * @param request.params - The request params. + * @param blockParamIndex - The index within the `params` array of the block + * param. + * @param blockParam - The desired block param value. + * @returns The updated request object. */ export function buildRequestWithReplacedBlockParam( - { method, params = [] }, - blockParamIndex, - blockParam, + { method, params = [] }: Request, + blockParamIndex: number, + blockParam: any, ) { const updatedParams = params.slice(); updatedParams[blockParamIndex] = blockParam; diff --git a/app/scripts/controllers/network/provider-api-tests/no-block-param.js b/app/scripts/controllers/network/provider-api-tests/no-block-param.ts similarity index 99% rename from app/scripts/controllers/network/provider-api-tests/no-block-param.js rename to app/scripts/controllers/network/provider-api-tests/no-block-param.ts index 08ae7edd0..662c7fac9 100644 --- a/app/scripts/controllers/network/provider-api-tests/no-block-param.js +++ b/app/scripts/controllers/network/provider-api-tests/no-block-param.ts @@ -1,6 +1,7 @@ /* eslint-disable jest/require-top-level-describe, jest/no-export */ import { + ProviderType, waitForPromiseToBeFulfilledAfterRunningAllTimers, withMockedCommunications, withNetworkClient, @@ -11,6 +12,11 @@ import { buildJsonRpcEngineEmptyResponseErrorMessage, } from './shared-tests'; +type TestsForRpcMethodAssumingNoBlockParamOptions = { + providerType: ProviderType; + numberOfParameters: number; +}; + /** * Defines tests which exercise the behavior exhibited by an RPC method which is * assumed to not take a block parameter. Even if it does, the value of this @@ -23,8 +29,11 @@ import { * either `infura` or `custom` (default: "infura"). */ export function testsForRpcMethodAssumingNoBlockParam( - method, - { numberOfParameters, providerType }, + method: string, + { + numberOfParameters, + providerType, + }: TestsForRpcMethodAssumingNoBlockParamOptions, ) { if (providerType !== 'infura' && providerType !== 'custom') { throw new Error( diff --git a/app/scripts/controllers/network/provider-api-tests/not-handled-by-middleware.js b/app/scripts/controllers/network/provider-api-tests/not-handled-by-middleware.ts similarity index 83% rename from app/scripts/controllers/network/provider-api-tests/not-handled-by-middleware.js rename to app/scripts/controllers/network/provider-api-tests/not-handled-by-middleware.ts index 693d9f779..ee92bb07f 100644 --- a/app/scripts/controllers/network/provider-api-tests/not-handled-by-middleware.js +++ b/app/scripts/controllers/network/provider-api-tests/not-handled-by-middleware.ts @@ -1,7 +1,16 @@ /* eslint-disable jest/require-top-level-describe, jest/no-export */ import { fill } from 'lodash'; -import { withMockedCommunications, withNetworkClient } from './helpers'; +import { + ProviderType, + withMockedCommunications, + withNetworkClient, +} from './helpers'; + +type TestsForRpcMethodNotHandledByMiddlewareOptions = { + providerType: ProviderType; + numberOfParameters: number; +}; /** * Defines tests which exercise the behavior exhibited by an RPC method that @@ -15,8 +24,11 @@ import { withMockedCommunications, withNetworkClient } from './helpers'; * RPC method takes. */ export function testsForRpcMethodNotHandledByMiddleware( - method, - { providerType, numberOfParameters }, + method: string, + { + providerType, + numberOfParameters, + }: TestsForRpcMethodNotHandledByMiddlewareOptions, ) { if (providerType !== 'infura' && providerType !== 'custom') { throw new Error( diff --git a/app/scripts/controllers/network/provider-api-tests/shared-tests.js b/app/scripts/controllers/network/provider-api-tests/shared-tests.ts similarity index 98% rename from app/scripts/controllers/network/provider-api-tests/shared-tests.js rename to app/scripts/controllers/network/provider-api-tests/shared-tests.ts index 04412d3f0..6337bb56a 100644 --- a/app/scripts/controllers/network/provider-api-tests/shared-tests.js +++ b/app/scripts/controllers/network/provider-api-tests/shared-tests.ts @@ -2,7 +2,11 @@ import { testsForRpcMethodsThatCheckForBlockHashInResponse } from './block-hash-in-response'; import { testsForRpcMethodSupportingBlockParam } from './block-param'; -import { withMockedCommunications, withNetworkClient } from './helpers'; +import { + ProviderType, + withMockedCommunications, + withNetworkClient, +} from './helpers'; import { testsForRpcMethodAssumingNoBlockParam } from './no-block-param'; import { testsForRpcMethodNotHandledByMiddleware } from './not-handled-by-middleware'; @@ -13,7 +17,7 @@ import { testsForRpcMethodNotHandledByMiddleware } from './not-handled-by-middle * @param reason - The exact reason for failure. * @returns The error message. */ -export function buildInfuraClientRetriesExhaustedErrorMessage(reason) { +export function buildInfuraClientRetriesExhaustedErrorMessage(reason: string) { return new RegExp( `^InfuraProvider - cannot complete request. All retries exhausted\\..+${reason}`, 'us', @@ -27,7 +31,7 @@ export function buildInfuraClientRetriesExhaustedErrorMessage(reason) { * @param method - The RPC method. * @returns The error message. */ -export function buildJsonRpcEngineEmptyResponseErrorMessage(method) { +export function buildJsonRpcEngineEmptyResponseErrorMessage(method: string) { return new RegExp( `^JsonRpcEngine: Response has no error or result for request:.+"method": "${method}"`, 'us', @@ -42,7 +46,7 @@ export function buildJsonRpcEngineEmptyResponseErrorMessage(method) { * @param reason - The reason. * @returns The error message. */ -export function buildFetchFailedErrorMessage(url, reason) { +export function buildFetchFailedErrorMessage(url: string, reason: string) { return new RegExp( `^request to ${url}(/[^/ ]*)+ failed, reason: ${reason}`, 'us', @@ -57,7 +61,7 @@ export function buildFetchFailedErrorMessage(url, reason) { * exposed by `createInfuraClient` is tested; if `custom`, then the middleware * exposed by `createJsonRpcClient` will be tested. */ -export function testsForProviderType(providerType) { +export function testsForProviderType(providerType: ProviderType) { // Ethereum JSON-RPC spec: // Infura documentation: diff --git a/package.json b/package.json index c3eb9e568..710d9fa11 100644 --- a/package.json +++ b/package.json @@ -368,6 +368,7 @@ "@babel/preset-typescript": "^7.16.7", "@babel/register": "^7.5.5", "@ethersproject/bignumber": "^5.7.0", + "@json-rpc-specification/meta-schema": "^1.0.6", "@lavamoat/allow-scripts": "^2.0.3", "@lavamoat/lavapack": "^5.0.0", "@metamask/auto-changelog": "^2.1.0", diff --git a/yarn.lock b/yarn.lock index 81c7c68e9..d6733e961 100644 --- a/yarn.lock +++ b/yarn.lock @@ -3286,6 +3286,13 @@ __metadata: languageName: node linkType: hard +"@json-rpc-specification/meta-schema@npm:^1.0.6": + version: 1.0.6 + resolution: "@json-rpc-specification/meta-schema@npm:1.0.6" + checksum: 2eb9c6c6c73bb38350c7180d1ad3c5b8462406926cae753741895b457d7b1b9f0b74148daf3462bb167cef39efdd1d9090308edf4d4938956863acb643c146eb + languageName: node + linkType: hard + "@keystonehq/base-eth-keyring@npm:^0.7.1": version: 0.7.1 resolution: "@keystonehq/base-eth-keyring@npm:0.7.1" @@ -24143,6 +24150,7 @@ __metadata: "@ethersproject/providers": ^5.7.2 "@formatjs/intl-relativetimeformat": ^5.2.6 "@fortawesome/fontawesome-free": ^5.13.0 + "@json-rpc-specification/meta-schema": ^1.0.6 "@keystonehq/bc-ur-registry-eth": ^0.12.1 "@keystonehq/metamask-airgapped-keyring": ^0.6.1 "@lavamoat/allow-scripts": ^2.0.3 From 40e4a3653ff0fdfb8e6a864f9aa7be284eff2f2d Mon Sep 17 00:00:00 2001 From: vthomas13 <10986371+vthomas13@users.noreply.github.com> Date: Fri, 14 Apr 2023 12:51:13 -0400 Subject: [PATCH 30/36] Updating Terms of Use, Adding popover and onboarding flow check (#18221) * WIP commit * Moving copy out of messages.json, styling changes * handling scroll button click and disable logic * moving scrollButton up to popover component, adding logic for accepting terms of use in popover and onboarding flows * adding terms of use to e2e wallet creation/import * adjusting failing unit test * fixing QR code e2e * updating welcome test * setting app state in fixtures * Update app/scripts/controllers/app-state.js removing console log Co-authored-by: Nidhi Kumari * Update ui/components/app/terms-of-use-popup/terms-of-use-popup.stories.js adding args to ToU popup storybook Co-authored-by: Nidhi Kumari * Update ui/components/app/terms-of-use-popup/terms-of-use-popup.js Co-authored-by: Nidhi Kumari * updating DS components in terms of use * popover styling changes * adding metametrics tracking * editing scrollbutton behavior * adding unit test * code fencing --------- Co-authored-by: Nidhi Kumari --- app/_locales/en/messages.json | 16 + app/scripts/controllers/app-state.js | 11 + app/scripts/metamask-controller.js | 2 + shared/constants/metametrics.ts | 2 + shared/constants/terms.js | 1 + test/e2e/fixture-builder.js | 1 + test/e2e/helpers.js | 12 + test/e2e/metamask-ui.spec.js | 1 + test/e2e/tests/incremental-security.spec.js | 2 + test/e2e/tests/metamask-responsive-ui.spec.js | 2 + test/e2e/tests/onboarding.spec.js | 5 + ui/components/app/app-components.scss | 1 + ui/components/app/terms-of-use-popup/index.js | 1 + .../app/terms-of-use-popup/index.scss | 30 + .../terms-of-use-popup/terms-of-use-popup.js | 1181 +++++++++++++++++ .../terms-of-use-popup.stories.js | 16 + .../terms-of-use-popup.test.js | 48 + ui/components/app/whats-new-popup/index.scss | 22 - ui/components/ui/popover/index.scss | 20 + ui/components/ui/popover/popover.component.js | 25 +- ui/ducks/app/app.ts | 2 + ui/pages/home/home.component.js | 29 +- ui/pages/home/home.container.js | 6 + ui/pages/onboarding-flow/welcome/index.scss | 5 + ui/pages/onboarding-flow/welcome/welcome.js | 50 +- .../onboarding-flow/welcome/welcome.test.js | 17 +- ui/selectors/selectors.js | 13 + ui/store/actions.ts | 6 + 28 files changed, 1488 insertions(+), 39 deletions(-) create mode 100644 shared/constants/terms.js create mode 100644 ui/components/app/terms-of-use-popup/index.js create mode 100644 ui/components/app/terms-of-use-popup/index.scss create mode 100644 ui/components/app/terms-of-use-popup/terms-of-use-popup.js create mode 100644 ui/components/app/terms-of-use-popup/terms-of-use-popup.stories.js create mode 100644 ui/components/app/terms-of-use-popup/terms-of-use-popup.test.js diff --git a/app/_locales/en/messages.json b/app/_locales/en/messages.json index 5d7a113e0..e3a0e127d 100644 --- a/app/_locales/en/messages.json +++ b/app/_locales/en/messages.json @@ -109,6 +109,9 @@ "about": { "message": "About" }, + "accept": { + "message": "Accept" + }, "acceptTermsOfUse": { "message": "I have read and agree to the $1", "description": "$1 is the `terms` message" @@ -305,6 +308,10 @@ "advancedPriorityFeeToolTip": { "message": "Priority fee (aka “miner tip”) goes directly to miners and incentivizes them to prioritize your transaction." }, + "agreeTermsOfUse": { + "message": "I agree to Metamask's $1", + "description": "$1 is the `terms` link" + }, "airgapVault": { "message": "AirGap Vault" }, @@ -4310,6 +4317,15 @@ "termsOfUse": { "message": "terms of use" }, + "termsOfUseAgreeText": { + "message": " I agree to the Terms of Use, which apply to my use of MetaMask and all of its features" + }, + "termsOfUseFooterText": { + "message": "Please scroll to read all sections" + }, + "termsOfUseTitle": { + "message": "Our Terms of Use have updated" + }, "testNetworks": { "message": "Test networks" }, diff --git a/app/scripts/controllers/app-state.js b/app/scripts/controllers/app-state.js index 3360cd4f9..cf61daff6 100644 --- a/app/scripts/controllers/app-state.js +++ b/app/scripts/controllers/app-state.js @@ -172,6 +172,17 @@ export default class AppStateController extends EventEmitter { }); } + /** + * Record the timestamp of the last time the user has acceoted the terms of use + * + * @param {number} lastAgreed - timestamp when user last accepted the terms of use + */ + setTermsOfUseLastAgreed(lastAgreed) { + this.store.updateState({ + termsOfUseLastAgreed: lastAgreed, + }); + } + /** * Record the timestamp of the last time the user has seen the outdated browser warning * diff --git a/app/scripts/metamask-controller.js b/app/scripts/metamask-controller.js index 53bba6767..1dcea32f1 100644 --- a/app/scripts/metamask-controller.js +++ b/app/scripts/metamask-controller.js @@ -2032,6 +2032,8 @@ export default class MetamaskController extends EventEmitter { appStateController.setRecoveryPhraseReminderLastShown.bind( appStateController, ), + setTermsOfUseLastAgreed: + appStateController.setTermsOfUseLastAgreed.bind(appStateController), setOutdatedBrowserWarningLastShown: appStateController.setOutdatedBrowserWarningLastShown.bind( appStateController, diff --git a/shared/constants/metametrics.ts b/shared/constants/metametrics.ts index 03626ff61..44647668b 100644 --- a/shared/constants/metametrics.ts +++ b/shared/constants/metametrics.ts @@ -510,6 +510,8 @@ export enum MetaMetricsEventName { SignatureFailed = 'Signature Failed', SignatureRejected = 'Signature Rejected', SignatureRequested = 'Signature Requested', + TermsOfUseShown = 'Terms of Use Shown', + TermsOfUseAccepted = 'Terms of Use Accepted', TokenImportButtonClicked = 'Import Token Button Clicked', TokenScreenOpened = 'Token Screen Opened', SupportLinkClicked = 'Support Link Clicked', diff --git a/shared/constants/terms.js b/shared/constants/terms.js new file mode 100644 index 000000000..5ced85fb5 --- /dev/null +++ b/shared/constants/terms.js @@ -0,0 +1 @@ +export const TERMS_OF_USE_LAST_UPDATED = '2023-03-25'; diff --git a/test/e2e/fixture-builder.js b/test/e2e/fixture-builder.js index 3ded4081b..aeecb1388 100644 --- a/test/e2e/fixture-builder.js +++ b/test/e2e/fixture-builder.js @@ -140,6 +140,7 @@ function defaultFixture() { browserEnvironment: {}, nftsDropdownState: {}, connectedStatusPopoverHasBeenShown: true, + termsOfUseLastAgreed: 86400000000000, defaultHomeActiveTabName: null, fullScreenGasPollTokens: [], notificationGasPollTokens: [], diff --git a/test/e2e/helpers.js b/test/e2e/helpers.js index 2569dd4a3..132003451 100644 --- a/test/e2e/helpers.js +++ b/test/e2e/helpers.js @@ -222,6 +222,9 @@ const getWindowHandles = async (driver, handlesCount) => { }; const importSRPOnboardingFlow = async (driver, seedPhrase, password) => { + // agree to terms of use + await driver.clickElement('[data-testid="onboarding-terms-checkbox"]'); + // welcome await driver.clickElement('[data-testid="onboarding-import-wallet"]'); @@ -262,6 +265,9 @@ const completeImportSRPOnboardingFlowWordByWord = async ( seedPhrase, password, ) => { + // agree to terms of use + await driver.clickElement('[data-testid="onboarding-terms-checkbox"]'); + // welcome await driver.clickElement('[data-testid="onboarding-import-wallet"]'); @@ -293,6 +299,9 @@ const completeImportSRPOnboardingFlowWordByWord = async ( }; const completeCreateNewWalletOnboardingFlow = async (driver, password) => { + // agree to terms of use + await driver.clickElement('[data-testid="onboarding-terms-checkbox"]'); + // welcome await driver.clickElement('[data-testid="onboarding-create-wallet"]'); @@ -342,6 +351,9 @@ const completeCreateNewWalletOnboardingFlow = async (driver, password) => { }; const importWrongSRPOnboardingFlow = async (driver, seedPhrase) => { + // agree to terms of use + await driver.clickElement('[data-testid="onboarding-terms-checkbox"]'); + // welcome await driver.clickElement('[data-testid="onboarding-import-wallet"]'); diff --git a/test/e2e/metamask-ui.spec.js b/test/e2e/metamask-ui.spec.js index 6cc81b400..e3a53ffdd 100644 --- a/test/e2e/metamask-ui.spec.js +++ b/test/e2e/metamask-ui.spec.js @@ -84,6 +84,7 @@ describe('MetaMask', function () { describe('Going through the first time flow', function () { it('clicks the "Create New Wallet" button on the welcome screen', async function () { + await driver.clickElement('[data-testid="onboarding-terms-checkbox"]'); await driver.clickElement('[data-testid="onboarding-create-wallet"]'); }); diff --git a/test/e2e/tests/incremental-security.spec.js b/test/e2e/tests/incremental-security.spec.js index 2daf70a91..2213046e3 100644 --- a/test/e2e/tests/incremental-security.spec.js +++ b/test/e2e/tests/incremental-security.spec.js @@ -29,6 +29,8 @@ describe('Incremental Security', function () { }, async ({ driver }) => { await driver.navigate(); + // agree to terms of use + await driver.clickElement('[data-testid="onboarding-terms-checkbox"]'); // welcome await driver.clickElement('[data-testid="onboarding-create-wallet"]'); diff --git a/test/e2e/tests/metamask-responsive-ui.spec.js b/test/e2e/tests/metamask-responsive-ui.spec.js index 4c83081c2..e21668a10 100644 --- a/test/e2e/tests/metamask-responsive-ui.spec.js +++ b/test/e2e/tests/metamask-responsive-ui.spec.js @@ -15,6 +15,8 @@ describe('MetaMask Responsive UI', function () { }, async ({ driver }) => { await driver.navigate(); + // agree to terms of use + await driver.clickElement('[data-testid="onboarding-terms-checkbox"]'); // welcome await driver.clickElement('[data-testid="onboarding-create-wallet"]'); diff --git a/test/e2e/tests/onboarding.spec.js b/test/e2e/tests/onboarding.spec.js index 7778989d3..260857ce9 100644 --- a/test/e2e/tests/onboarding.spec.js +++ b/test/e2e/tests/onboarding.spec.js @@ -108,6 +108,9 @@ describe('MetaMask onboarding', function () { async ({ driver }) => { await driver.navigate(); + // accept terms of use + await driver.clickElement('[data-testid="onboarding-terms-checkbox"]'); + // welcome await driver.clickElement('[data-testid="onboarding-import-wallet"]'); @@ -145,6 +148,7 @@ describe('MetaMask onboarding', function () { async ({ driver }) => { await driver.navigate(); + await driver.clickElement('[data-testid="onboarding-terms-checkbox"]'); await driver.clickElement('[data-testid="onboarding-create-wallet"]'); // metrics @@ -208,6 +212,7 @@ describe('MetaMask onboarding', function () { async ({ driver }) => { await driver.navigate(); + await driver.clickElement('[data-testid="onboarding-terms-checkbox"]'); await driver.clickElement('[data-testid="onboarding-create-wallet"]'); // metrics diff --git a/ui/components/app/app-components.scss b/ui/components/app/app-components.scss index adb1ff1f6..0fb69b866 100644 --- a/ui/components/app/app-components.scss +++ b/ui/components/app/app-components.scss @@ -83,6 +83,7 @@ @import 'transaction-status-label/index'; @import 'wallet-overview/index'; @import 'whats-new-popup/index'; +@import 'terms-of-use-popup/index'; @import 'loading-network-screen/index'; @import 'transaction-decoding/index'; @import 'advanced-gas-fee-popover/index'; diff --git a/ui/components/app/terms-of-use-popup/index.js b/ui/components/app/terms-of-use-popup/index.js new file mode 100644 index 000000000..e9333ccd1 --- /dev/null +++ b/ui/components/app/terms-of-use-popup/index.js @@ -0,0 +1 @@ +export { default } from './terms-of-use-popup'; diff --git a/ui/components/app/terms-of-use-popup/index.scss b/ui/components/app/terms-of-use-popup/index.scss new file mode 100644 index 000000000..7ba93df4a --- /dev/null +++ b/ui/components/app/terms-of-use-popup/index.scss @@ -0,0 +1,30 @@ +.popover-wrap.terms-of-use__popover { + .terms-of-use { + &__terms-list { + list-style: decimal none outside; + } + + &__footer-text { + align-self: center; + } + } + + .popover-header { + &__title { + margin-bottom: 0; + } + } + + .popover-footer { + border-top: none; + } + + @include screen-sm-min { + max-height: 750px; + width: 500px; + } + + @include screen-sm-max { + max-height: 568px; + } +} diff --git a/ui/components/app/terms-of-use-popup/terms-of-use-popup.js b/ui/components/app/terms-of-use-popup/terms-of-use-popup.js new file mode 100644 index 000000000..9fe664303 --- /dev/null +++ b/ui/components/app/terms-of-use-popup/terms-of-use-popup.js @@ -0,0 +1,1181 @@ +import React, { useContext, useEffect, useRef, useState } from 'react'; +import PropTypes from 'prop-types'; +import { I18nContext } from '../../../contexts/i18n'; +import Popover from '../../ui/popover'; +import { + AlignItems, + FLEX_DIRECTION, + TextVariant, + Color, + TextColor, +} from '../../../helpers/constants/design-system'; +import { + Text, + Button, + BUTTON_TYPES, + ButtonLink, + Label, +} from '../../component-library'; +import Box from '../../ui/box'; +import CheckBox from '../../ui/check-box/check-box.component'; +import { + MetaMetricsEventCategory, + MetaMetricsEventName, +} from '../../../../shared/constants/metametrics'; +import { MetaMetricsContext } from '../../../contexts/metametrics'; + +export default function TermsOfUsePopup({ onAccept }) { + const t = useContext(I18nContext); + const trackEvent = useContext(MetaMetricsContext); + const [isTermsOfUseChecked, setIsTermsOfUseChecked] = useState(false); + + const popoverRef = useRef(); + const bottomRef = React.createRef(); + + const handleScrollDownClick = (e) => { + e.stopPropagation(); + bottomRef.current.scrollIntoView({ + behavior: 'smooth', + }); + }; + + useEffect(() => { + trackEvent({ + category: MetaMetricsEventCategory.Onboarding, + event: MetaMetricsEventName.TermsOfUseShown, + properties: { + location: 'Terms Of Use Popover', + }, + }); + // eslint-disable-next-line react-hooks/exhaustive-deps + }, []); + + return ( + + + + {t('termsOfUseFooterText')} + + + } + > + + + + IMPORTANT NOTICE: THIS AGREEMENT IS SUBJECT TO BINDING ARBITRATION + AND A WAIVER OF CLASS ACTION RIGHTS AS DETAILED IN SECTION 11. + PLEASE READ THE AGREEMENT CAREFULLY. + + + ConsenSys Software Inc. (“ConsenSys,” “we,” “us,” or “our”) is the + leading blockchain software development company. With a focus on + utilizing decentralized technologies, such as Ethereum, our software + is powering a revolution in commerce and finance and helping to + optimize business processes. ConsenSys hosts a top level domain + website, www.consensys.net, that serves information regarding + ConsenSys and our Offerings, as defined below, as well as + sub-domains for our products or services (the top level domain with + the sub-domains collectively referred to as the “Site”), which + include text, images, audio, code and other materials or third party + information.  + + + These Terms of Use (the “Terms,” “Terms of Use” or “Agreement”) + contain the terms and conditions that govern your access to and use + of the Site and Offerings provided by us and is an agreement between + us and you or the entity you represent (“you” or “your”). Please + read these Terms of Use carefully before using the Site or + Offerings. By using the Site, clicking a button or checkbox to + accept or agree to these Terms where that option is made available, + clicking a button to use or access any of the Offerings, completing + an Order, or,  if earlier, using or otherwise accessing the + Offerings (the date on which any of the events listed above occur + being the “Effective Date”), you (1) accept and agree to these Terms + and any additional terms, rules and conditions of participation + issued by ConsenSys from time to time and (2) consent to the + collection, use, disclosure and other handling of information as + described in our{' '} + + Privacy Policy. + {' '} + If you do not agree to the Terms or perform any and all obligations + you accept under the Terms, then you may not access or use the + Offerings.  + + + You represent to us that you are lawfully able to enter into + contracts. If you are entering into this Agreement for an entity, + such as the company you work for, you represent to us that you have + legal authority to bind that entity. Please see Section 13 for + definitions of certain capitalized terms used in this Agreement. + + + In addition, you represent to us that you and your financial + institutions, or any party that owns or controls you or your + financial institutions, are (1) not subject to sanctions or + otherwise designated on any list of prohibited or restricted + parties, including but not limited to the lists maintained by the + United Nations Security Council, the U.S. Government (i.e., the + Specially Designated Nationals List and Foreign Sanctions Evaders + List of the U.S. Department of Treasury and the Entity List of the + U.S. Department of Commerce), the European Union or its Member + States, or other applicable government authority and (2) not located + in any country subject to a comprehensive sanctions program + implemented by the United States. + + + 1. The Offerings. + + + 1.1 Generally. You may access and use the Offerings in accordance + with this Agreement. You agree to comply with the terms of this + Agreement and all laws, rules and regulations applicable to your use + of the Offerings. + + + 1.2 Offerings and Access. ConsenSys offers a number of products and + services, each an “Offering”, under the ConsenSys brand or brands + owned by us. These include Codefi, Quorum, Infura, MetaMask and + others. Offerings are generally accessed through the Site or through + a third party provider of which we approved, such as the Google Play + or Apple App Store, unless otherwise agreed in writing. Some + Offerings may require you to create an account, enter a valid form + of payment, and select a paid plan (a “Paid Plan”), or initiate an + Order.  + + + 1.3 Third-Party Content. In certain Offerings, Third-Party Content + may be used by you at your election. Third-Party Content is governed + by this Agreement and, if applicable, separate terms and conditions + accompanying such Third-Party Content, which terms and conditions + may include separate fees and charges. + + + 1.4 Third-Party Offerings. When you use our Offerings, you may also + be using the products or services of one or more third parties. Your + use of these third party offerings may be subject to the separate + policies, terms of use, and fees of these third parties. + + + 2. Changes. + + + 2.1 To the Offerings. We may change or discontinue any or all of the + Offerings or change or remove functionality of any or all of the + Offerings from time to time. We will use commercially reasonable + efforts to communicate to you any material change or discontinuation + of an Offering through the Site or public communication + channels.  If you are on a Paid Plan, we will use commercially + reasonable efforts to communicate to you  any material changes + to or discontinuation of the Offering at least 30 days in advance of + such change, and we will use commercially reasonable efforts to + continue supporting the previous version of the Offering for up to + three months after the change or discontinuation, except if doing so + (a) would pose an information security or intellectual property + issue, (b) is economically or technically burdensome, or (c) would + create undue risk of us violating the law. + + + 2.2 To this Agreement. We reserve the right, at our sole discretion, + to modify or replace any part of this Agreement or any Policies at + any time. It is your responsibility to check this Agreement + periodically for changes, but we will also use commercially + reasonable efforts to communicate any material changes to this + Agreement through the Site or other public channels. Your continued + use of or access to the Offerings following the posting of any + changes to this Agreement constitutes acceptance of those changes. + + + 3. Your Responsibilities. + + + 3.1 Your Accounts.  For those Offerings that require an + account, and except to the extent caused by our breach of this + Agreement, (a) you are responsible for all activities that occur + under your account, regardless of whether the activities are + authorized by you or undertaken by you, your employees or a third + party (including your contractors, agents or other End Users), and + (b) we and our affiliates are not responsible for unauthorized + access to your account, including any access that occurred as a + result of fraud, phishing, or other criminal activity perpetrated by + third parties.   + + + 3.2 Your Use. You are responsible for all activities that occur + through your use of those Offerings that do not require an account, + except to the extent caused by our breach of this Agreement, + regardless of whether the activities are authorized by you or + undertaken by you, your employees or a third party (including your + contractors, agents or other End Users).  We and our affiliates + are not responsible for unauthorized access that may occur during + your use of the Offerings, including any access that occurred as a + result of fraud, phishing, or other criminal activity perpetrated by + third parties.  You will ensure that your use of the Offerings + does not violate any applicable law.   + + + 3.3 Your Security and Backup. You are solely responsible for + properly configuring and using the Offerings and otherwise taking + appropriate action to secure, protect and backup your accounts + and/or Your Content in a manner that will provide appropriate + security and protection, which might include use of + encryption.  This includes your obligation under this Agreement + to record and securely maintain any passwords or backup security + phrases (i.e. “seed” phrases) that relate to your use of the + Offerings. You acknowledge that you will not share with us nor any + other third party any password or backup/seed phrase that relates to + your use of the Offerings, and that we will not be held responsible + if you do share any such phrase or password. + + + 3.4 Log-In Credentials and API Authentication. To the extent we + provide you with log-in credentials and API authentication generated + by the Offerings, such log-in credentials and API authentication are + for your use only and you will not sell, transfer or sublicense them + to any other entity or person, except that you may disclose your + password or private key to your agents and subcontractors performing + work on your behalf. + + + 3.5 Applicability to MetaMask Offerings. For the avoidance of doubt, + the terms of this Section 3 are applicable to all Offerings, + including MetaMask and any accounts you create through MetaMask with + Third Party Offerings, such as decentralized applications, or + blockchain-based accounts themselves. + + + 4. Fees and Payment. + + + 4.1 Publicly Available Offerings. Some Offerings may be offered to + the public and licensed on a royalty free basis, including Offerings + that require a Paid Plan for software licensing fees above a certain + threshold of use.  + + + 4.2 Offering Fees.  If your use of an Offering does not require + an Order or Paid Plan but software licensing fees are charged + contemporaneously with your use of the Offering, those fees will be + charged as described on the Site or in the user interface of the + Offering.  Such fees may be calculated by combining a fee + charged by us and a fee charged by a Third Party Offering that + provides certain functionality related to the Offering.  For + those Offerings which entail an Order or Paid Plan, we calculate and + bill fees and charges according to your Order or Paid Plan. For such + Offerings, on the first day of each billing period, you will pay us + the applicable fees (the “Base Fees”) and any applicable taxes based + on the Offerings in the Paid Plan. In addition, we may, for + particular Orders, issue an invoice to you for all charges above the + applicable threshold for your Paid Plan which constitute overage + fees for the previous billing period. If you make any other changes + to the Offerings during a billing period (e.g. upgrading or + downgrading your Paid Plan), we will apply any additional charges or + credits to the next billing period. We may bill you more frequently + for fees accrued at our discretion upon notice to you.  You + will pay all fees in U.S. dollars unless the particular Offering + specifies a different form of payment or otherwise agreed to in + writing. All amounts payable by you under this Agreement will be + paid to us without setoff or counterclaim, and without any deduction + or withholding. Fees and charges for any new Offering or new feature + of an Offering will be effective when we use commercially reasonable + efforts to communicate updated fees and charges through our Site or + other public channels or, if you are on a Paid Plan, upon + commercially reasonable efforts to notify you, unless we expressly + state otherwise in a notice. We may increase or add new fees and + charges for any existing Offerings you are using by using + commercially reasonable efforts to notify users of the Offerings + through our Site or other public channels or, if you are on a Paid + Plan, by giving you at least 30 days’ prior notice.  Unless + otherwise specified in an Order, if you are on a Paid Plan, all + amounts due under this Agreement are payable within thirty (30) days + following receipt of your invoice.  We may elect to charge you + interest at the rate of 1.5% per month (or the highest rate + permitted by law, if less) on all late payments. + + + 4.3 Taxes. Each party will be responsible, as required under + applicable law, for identifying and paying all taxes and other + governmental fees and charges (and any penalties, interest, and + other additions thereto) that are imposed on that party upon or with + respect to the transactions and payments under this Agreement. All + fees payable by you are exclusive taxes unless otherwise noted. We + reserve the right to withhold taxes where required. + + + 5. Temporary Suspension; Limiting API Requests. + + + 5.1 Generally. We may suspend your right to access or use any + portion or all of the Offerings immediately if we determine: + + + (a) your use of the Offerings (i) poses a security risk to the + Offerings or any third party, (ii) could adversely impact our + systems, the Offerings or the systems of any other user, (iii) could + subject us, our affiliates, or any third party to liability, or (iv) + could be unlawful; + + + (b) you are, or any End User is, in breach of this Agreement; + + + (c) you are in breach of your payment obligations under Section 4 + and such breach continues for 30 days or longer; or + + + (d) for entities, you have ceased to operate in the ordinary course, + made an assignment for the benefit of creditors or similar + disposition of your assets, or become the subject of any bankruptcy, + reorganization, liquidation, dissolution or similar proceeding. + + + 5.2 Effect of Suspension. If we suspend your right to access or use + any portion or all of the Offerings: + + + (a) you remain responsible for all fees and charges you incur during + the period of suspension; and + + + (b) you will not be entitled to any fee credits for any period of + suspension. + + + 5.3 Limiting API Requests. If applicable to a particular Offering, + we retain sole discretion to limit your usage of the Offerings + (including without limitation by limiting the number of API requests + you may submit (“API Requests”)) at any time if your usage of the + Offerings exceeds the usage threshold specified in your Paid + Plan.    + + + 6. Term; Termination. + + + 6.1 Term. For Offerings subject to a Paid Plan, the term of this + Agreement will commence on the Effective Date and will remain in + effect until terminated under this Section 6. Any notice of + termination of this Agreement by either party to the other must + include a Termination Date that complies with the notice periods in + Section 6.2.  For Offerings that are not subject to a Paid + Plan, the term of this Agreement will commence on the Effective Date + and will remain in effect until you stop accessing or using the + Offerings.  + + + 6.2 Termination. + + + (a) Termination for Convenience. If you are not on a Paid Plan, you + may terminate this Agreement for any reason by ceasing use of the + Offering. If you are on a Paid Plan, each party may terminate this + Agreement for any reason by giving the other party at least 30 days’ + written notice, subject to the provisions in Section 6.2(b). + + + (b) Termination for Cause. + + + (i) By Either Party. Either party may terminate this Agreement for + cause if the other party is in material breach of this Agreement and + the material breach remains uncured for a period of 30 days from + receipt of notice by the other party.  + + + (ii) By Us. We may also terminate this Agreement immediately (A) for + cause if we have the right to suspend under Section 5, (B) if our + relationship with a third-party partner who provides software or + other technology we use to provide the Offerings expires, terminates + or requires us to change the way we provide the software or other + technology as part of the Offerings, or (C) in order to avoid undue + risk of violating the law. + + + 6.3 Effect of Termination. Upon the Termination Date: + + + (i) all your rights under this Agreement immediately terminate; and + + + (ii) each party remains responsible for all fees and charges it has + incurred through the Termination Date and are responsible for any + fees and charges it incurs during the post-termination period; + + + (iii) the terms and conditions of this Agreement shall survive the + expiration or termination of this Agreement to the full extent + necessary for their enforcement and for the protection of the party + in whose favor they operate.  For instance, despite this + Agreement between you and us terminating, any dispute raised after + you stop accessing or using the Offerings will be subject to the + applicable provisions of this Agreement if that dispute relates to + your prior access or use. + + + For any use of the Offerings after the Termination Date, the terms + of this Agreement will again apply and, if your use is under a Paid + Plan, you will pay the applicable fees at the rates under Section 4. + + + 7. Proprietary Rights. + + + 7.1 Your Content. Depending on the Offering, you may share Content + with us. Except as provided in this Section 7, we obtain no rights + under this Agreement from you (or your licensors) to Your Content. + You consent to our use of Your Content to provide the Offerings to + you. + + + 7.2 Offerings License. We or our licensors own all right, title, and + interest in and to the Offerings, and all related technology and + intellectual property rights. Subject to the terms of this + Agreement, we grant you a limited, revocable, non-exclusive, + non-sublicensable, non-transferable license to do the following: (a) + access and use the Offerings solely in accordance with this + Agreement; and (b) copy and use Our Content solely in connection + with your permitted use of the Offerings. Except as provided in this + Section 7.2, you obtain no rights under this Agreement from us, our + affiliates or our licensors to the Offerings, including any related + intellectual property rights. Some of Our Content and Third-Party + Content may be provided to you under a separate license, such as the + Apache License, Version 2.0, or other open source license. In the + event of a conflict between this Agreement and any separate license, + the separate license will prevail with respect to Our Content or + Third-Party Content that is the subject of such separate license. + + + 7.3 License Restrictions. Neither you nor any End User will use the + Offerings in any manner or for any purpose other than as expressly + permitted by this Agreement. Except for as authorized, neither you + nor any End User will, or will attempt to (a) modify, distribute, + alter, tamper with, repair, or otherwise create derivative works of + any Content included in the Offerings (except to the extent Content + included in the Offerings is provided to you under a separate + license that expressly permits the creation of derivative works), + (b) reverse engineer, disassemble, or decompile the Offerings or + apply any other process or procedure to derive the source code of + any software included in the Offerings (except to the extent + applicable law doesn’t allow this restriction), (c) access or use + the Offerings in a way intended to avoid incurring fees or exceeding + usage limits or quotas, (d) use scraping techniques to mine or + otherwise scrape data except as permitted by a Plan, or (e) resell + or sublicense the Offerings unless otherwise agreed in writing. You + will not use Our Marks unless you obtain our prior written consent. + You will not misrepresent or embellish the relationship between us + and you (including by expressing or implying that we support, + sponsor, endorse, or contribute to you or your business endeavors). + You will not imply any relationship or affiliation between us and + you except as expressly permitted by this Agreement. + + + 7.4 Suggestions. If you provide any Suggestions to us or our + affiliates, we and our affiliates will be entitled to use the + Suggestions without restriction. You hereby irrevocably assign to us + all right, title, and interest in and to the Suggestions and agree + to provide us any assistance we require to document, perfect, and + maintain our rights in the Suggestions. + + + 7.5 U.S. Government Users. If you are a U.S. Government End User, we + are licensing the Offerings to you as a “Commercial Item” as that + term is defined in the U.S. Code of Federal Regulations (see 48 + C.F.R. § 2.101), and the rights we grant you to the Offerings are + the same as the rights we grant to all others under these Terms of + Use. + + + 8. Indemnification. + + + 8.1 General.  + + + (a) You will defend, indemnify, and hold harmless us, our affiliates + and licensors, and each of their respective employees, officers, + directors, and representatives from and against any Losses arising + out of or relating to any claim concerning: (a) breach of this + Agreement or violation of applicable law by you; and (b) a dispute + between you and any of your customers or users. You will reimburse + us for reasonable attorneys’ fees and expenses, associated with + claims described in (a) and (b) above. + + + (b) We will defend, indemnify, and hold harmless you and your + employees, officers, directors, and representatives from and against + any Losses arising out of or relating to any claim concerning our + material and intentional breach of this Agreement.  We will + reimburse you for reasonable attorneys’ fees and expenses associated + with the claims described in this paragraph. + + + 8.2 Intellectual Property. + + + (a) Subject to the limitations in this Section 8, you will defend + ConsenSys, its affiliates, and their respective employees, officers, + and directors against any third-party claim alleging that any of + Your Content infringes or misappropriates that third party’s + intellectual property rights, and will pay the amount of any adverse + final judgment or settlement. + + + (b) Subject to the limitations in this Section 8 and the limitations + in Section 10, we will defend you and your employees, officers, and + directors against any third-party claim alleging that the Offerings + infringe or misappropriate that third party’s intellectual property + rights, and will pay the amount of any adverse final judgment or + settlement.  However, we will not be required to spend more + than $200,000 pursuant to this Section 8, including without + limitation attorneys’ fees, court costs, settlements, judgments, and + reimbursement costs. + + + (c) Neither party will have obligations or liability under this + Section 8.2 arising from infringement by you combining the Offerings + with any other product, service, software, data, content or method. + In addition, we will have no obligations or liability arising from + your use of the Offerings after we have notified you to discontinue + such use. The remedies provided in this Section 8.2 are the sole and + exclusive remedies for any third-party claims of infringement or + misappropriation of intellectual property rights by the Offerings or + by Your Content. + + + 8.3 Process. In no event will a party agree to any settlement of any + claim that involves any commitment, other than the payment of money, + without the written consent of the other party. + + + 9. Disclaimers; Risk. + + + 9.1 DISCLAIMER. THE OFFERINGS ARE PROVIDED “AS IS.” EXCEPT TO THE + EXTENT PROHIBITED BY LAW, OR TO THE EXTENT ANY STATUTORY RIGHTS + APPLY THAT CANNOT BE EXCLUDED, LIMITED OR WAIVED, WE AND OUR + AFFILIATES AND LICENSORS (A) MAKE NO REPRESENTATIONS OR WARRANTIES + OF ANY KIND, WHETHER EXPRESS, IMPLIED, STATUTORY OR OTHERWISE + REGARDING THE OFFERINGS OR THE THIRD-PARTY CONTENT, AND (B) DISCLAIM + ALL WARRANTIES, INCLUDING ANY IMPLIED OR EXPRESS WARRANTIES (I) OF + MERCHANTABILITY, SATISFACTORY QUALITY, FITNESS FOR A PARTICULAR + PURPOSE, NON-INFRINGEMENT, OR QUIET ENJOYMENT, (II) ARISING OUT OF + ANY COURSE OF DEALING OR USAGE OF TRADE, (III) THAT THE OFFERINGS OR + THIRD-PARTY CONTENT WILL BE UNINTERRUPTED, ERROR FREE OR FREE OF + HARMFUL COMPONENTS, AND (IV) THAT ANY CONTENT WILL BE SECURE OR NOT + OTHERWISE LOST OR ALTERED. + + + 9.2 RISKS. OUR OFFERINGS RELY ON EMERGING TECHNOLOGIES, SUCH AS + ETHEREUM. SOME OFFERINGS ARE SUBJECT TO INCREASED RISK THROUGH YOUR + POTENTIAL MISUSE OF THINGS SUCH AS PUBLIC/PRIVATE KEY CRYPTOGRAPHY, + OR FAILING TO PROPERLY UPDATE OR RUN SOFTWARE TO ACCOMMODATE + PROTOCOL UPGRADES, LIKE THE TRANSITION TO PROOF OF STAKE CONSENSUS. + BY USING THE OFFERINGS YOU EXPLICITLY ACKNOWLEDGE AND ACCEPT THESE + HEIGHTENED RISKS.  YOU REPRESENT THAT YOU ARE FINANCIALLY AND + TECHNICALLY SOPHISTICATED ENOUGH TO UNDERSTAND THE INHERENT RISKS + ASSOCIATED WITH USING CRYPTOGRAPHIC AND BLOCKCHAIN-BASED SYSTEMS AND + UPGRADING YOUR SOFTWARE AND PROCESSES TO ACCOMMODATE PROTOCOL + UPGRADES, AND THAT YOU HAVE A WORKING KNOWLEDGE OF THE USAGE AND + INTRICACIES OF DIGITAL ASSETS SUCH AS ETHER (ETH) AND OTHER DIGITAL + TOKENS, SUCH AS THOSE FOLLOWING THE ERC-20 TOKEN STANDARD.  IN + PARTICULAR, YOU UNDERSTAND THAT WE DO NOT OPERATE THE ETHEREUM + PROTOCOL OR ANY OTHER BLOCKCHAIN PROTOCOL, COMMUNICATE OR EXECUTE + PROTOCOL UPGRADES, OR APPROVE OR PROCESS BLOCKCHAIN TRANSACTIONS ON + BEHALF OF YOU.  YOU FURTHER UNDERSTAND THAT BLOCKCHAIN + PROTOCOLS PRESENT THEIR OWN RISKS OF USE, THAT SUPPORTING OR + PARTICIPATING IN THE PROTOCOL MAY RESULT IN LOSSES IF YOUR + PARTICIPATION VIOLATES CERTAIN PROTOCOL RULES, THAT  + BLOCKCHAIN-BASED TRANSACTIONS ARE IRREVERSIBLE, THAT YOUR PRIVATE + KEY AND BACKUP SEED PHRASE MUST BE KEPT SECRET AT ALL TIMES, THAT + CONSENSYS WILL NOT STORE A BACKUP OF, NOR WILL BE ABLE TO DISCOVER + OR RECOVER, YOUR PRIVATE KEY OR BACKUP SEED PHRASE, AND THAT YOU ARE + SOLELY RESPONSIBLE FOR ANY APPROVALS OR PERMISSIONS YOU PROVIDE BY + CRYPTOGRAPHICALLY SIGNING BLOCKCHAIN MESSAGES OR TRANSACTIONS. + + + YOU FURTHER UNDERSTAND AND ACCEPT THAT DIGITAL TOKENS PRESENT MARKET + VOLATILITY RISK, TECHNICAL SOFTWARE RISKS, REGULATORY RISKS, AND + CYBERSECURITY RISKS.  YOU UNDERSTAND THAT THE COST AND SPEED OF + A BLOCKCHAIN-BASED SYSTEM IS VARIABLE, THAT COST MAY INCREASE + DRAMATICALLY AT ANY TIME, AND THAT COST AND SPEED IS NOT WITHIN THE + CAPABILITY OF CONSENSYS TO CONTROL.  YOU UNDERSTAND THAT + PROTOCOL UPGRADES MAY INADVERTENTLY CONTAIN BUGS OR SECURITY + VULNERABILITIES THAT MAY RESULT IN LOSS OF FUNCTIONALITY AND + ULTIMATELY FUNDS. + + + YOU UNDERSTAND AND ACCEPT THAT CONSENSYS DOES NOT CONTROL ANY + BLOCKCHAIN PROTOCOL, NOR DOES CONSENSYS CONTROL ANY SMART CONTRACT + THAT IS NOT OTHERWISE OFFERED BY CONSENSYS AS PART OF THE + OFFERINGS.  YOU UNDERSTAND AND ACCEPT THAT CONSENSYS DOES NOT + CONTROL AND IS NOT RESPONSIBLE FOR THE TRANSITION OF ANY BLOCKCHAIN + PROTOCOL FROM PROOF OF WORK TO PROOF OF STAKE CONSENSUS.  YOU + AGREE THAT YOU ALONE, AND NOT CONSENSYS, IS RESPONSIBLE FOR ANY + TRANSACTIONS THAT YOU ENGAGE IN WITH REGARD TO SUPPORTING ANY + BLOCKCHAIN PROTOCOL WHETHER THROUGH TRANSACTION VALIDATION OR + OTHERWISE, OR ANY TRANSACTIONS THAT YOU ENGAGE IN WITHANY + THIRD-PARTY-DEVELOPED SMART CONTRACT OR TOKEN, INCLUDING TOKENS THAT + WERE CREATED BY A THIRD PARTY FOR THE PURPOSE OF FRAUDULENTLY + MISREPRESENTING AFFILIATION WITH ANY BLOCKCHAIN PROJECT.  YOU + AGREE THAT CONSENSYS IS NOT RESPONSIBLE FOR THE REGULATORY STATUS OR + TREATMENT OF ANY DIGITAL ASSETS THAT YOU MAY ACCESS OR TRANSACT WITH + USING CONSENSYS OFFERINGS.  YOU EXPRESSLY ASSUME FULL + RESPONSIBILITY FOR ALL OF THE RISKS OF ACCESSING AND USING THE + OFFERINGS TO INTERACT WITH BLOCKCHAIN PROTOCOLS.  + + + 10. Limitations of Liability. + + + 10.1 Limitation of Liability. WITH THE EXCEPTION OF CLAIMS RELATING + TO A BREACH OF OUR PROPRIETARY RIGHTS AS GOVERNED BY SECTION 7 AND + INTELLECTUAL PROPERTY CLAIMS AS GOVERNED BY SECTION 8, IN NO EVENT + SHALL THE AGGREGATE LIABILITY OF EACH PARTY TOGETHER WITH ALL OF ITS + AFFILIATES ARISING OUT OF OR RELATED TO THIS AGREEMENT EXCEED THE + TOTAL AMOUNT PAID BY YOU HEREUNDER FOR THE OFFERINGS GIVING RISE TO + THE LIABILITY IN THE TWELVE MONTHS PRECEDING THE FIRST INCIDENT OUT + OF WHICH THE LIABILITY AROSE, OR, IF NO FEES HAVE BEEN PAID, + $25,000. THE FOREGOING LIMITATION WILL APPLY WHETHER AN ACTION IS IN + CONTRACT OR TORT AND REGARDLESS OF THE THEORY OF LIABILITY, BUT WILL + NOT LIMIT YOUR PAYMENT OBLIGATIONS UNDER SECTION 4.  + + + 10.2 Exclusion of Consequential and Related Damages. IN NO EVENT + WILL EITHER PARTY OR ITS AFFILIATES HAVE ANY LIABILITY ARISING OUT + OF OR RELATED TO THIS AGREEMENT FOR ANY LOST PROFITS, REVENUES, + GOODWILL, OR INDIRECT, SPECIAL, INCIDENTAL, CONSEQUENTIAL, COVER, + BUSINESS INTERRUPTION OR PUNITIVE DAMAGES, WHETHER AN ACTION IS IN + CONTRACT OR TORT AND REGARDLESS OF THE THEORY OF LIABILITY, EVEN IF + A PARTY OR ITS AFFILIATES HAVE BEEN ADVISED OF THE POSSIBILITY OF + SUCH DAMAGES OR IF A PARTY’S OR ITS AFFILIATES’ REMEDY OTHERWISE + FAILS OF ITS ESSENTIAL PURPOSE. THE FOREGOING DISCLAIMER WILL NOT + APPLY TO THE EXTENT PROHIBITED BY LAW. + + + 11. Binding Arbitration and Class Action Waiver. + + + PLEASE READ THIS SECTION CAREFULLY – IT MAY SIGNIFICANTLY AFFECT + YOUR LEGAL RIGHTS, INCLUDING YOUR RIGHT TO FILE A LAWSUIT IN COURT. + + + 11.1 Binding Arbitration. Any dispute, claim or controversy + (“Claim”) relating in any way to this Agreement, the Site, or your + use of the Offerings will be resolved by binding arbitration as + provided in this Section 11, rather than in court, except that you + may assert claims in small claims court if your claims qualify. + + + 11.1.1 If you are located in the United States: This agreement and + any dispute or claim (including non-contractual disputes or claims) + arising out of or in connection with it or its subject matter or + formation shall be governed by and construed in accordance with the + laws of the State of New York. The Federal Arbitration Act and + federal arbitration law apply to this Agreement. There is no judge + or jury in arbitration, and court review of an arbitration award is + limited. However, an arbitrator can award on an individual basis the + same damages and relief as a court (including injunctive and + declaratory relief or statutory damages), and must follow the terms + of this Agreement as a court would. The arbitration will be + conducted in accordance with the expedited procedures set forth in + the JAMS Comprehensive Arbitration Rules and Procedures (the + “Rules”) as those Rules exist on the effective date of this + Agreement, including Rules 16.1 and 16.2 of those Rules. The + arbitrator’s decision shall be final, binding, and non-appealable. + Judgment upon the award may be entered and enforced in any court + having jurisdiction. Neither party shall sue the other party other + than as provided herein or for enforcement of this clause or of the + arbitrator’s award; any such suit may be brought only in a Federal + District Court or a New York state court located in New York County, + New York. The arbitrator, and not any federal, state, or local + court, shall have exclusive authority to resolve any dispute + relating to the interpretation, applicability, unconscionability, + arbitrability, enforceability, or formation of this Agreement + including any claim that all or any part of the Agreement is void or + voidable.  If for any reason a claim proceeds in court rather + than in arbitration we and you waive any right to a jury trial. + Notwithstanding the foregoing we and you both agree that you or we + may bring suit in court to enjoin infringement or other misuse of + intellectual property rights.  + + + 11.1.2 If you are located in the United Kingdom: This agreement and + any dispute or claim (including non-contractual disputes or claims) + arising out of or in connection with it or its subject matter or + formation shall be governed by and construed in accordance with the + law of England and Wales. Any dispute, claim or controversy relating + in any way to this Agreement, the Offerings, your use of the + Offerings, or to any products or services licensed or distributed by + us will be resolved by binding arbitration as provided in this + clause. Prior to commencing any formal arbitration proceedings, + parties shall first seek settlement of any claim by mediation in + accordance with the LCIA Mediation Rules, which Rules are deemed to + be incorporated by reference into this clause. If the dispute is not + settled by mediation within 14 days of the commencement of the + mediation, or such further period as the parties shall agree in + writing, the dispute shall be referred to and finally resolved by + arbitration under the LCIA Rules, which are deemed to be + incorporated by reference into this clause. The language to be used + in the mediation and in the arbitration shall be English. The seat + or legal place of arbitration shall be London. + + + 11.1.3 If you are located in any territory that is not specifically + enumerated in Sections 11.1.1 or 11.1.2, you may elect for either of + Section 11.1.1 or 11.1.2 to apply to you, otherwise this Agreement + and any Claim (including non-contractual disputes or claims) arising + out of or in connection with it or its subject matter or formation + shall be governed by and construed in accordance with the law of + Ireland. Any Claim relating in any way to this Agreement, the + Offerings, your use of the Offerings, or to any products or services + licensed or distributed by us will be resolved by binding + arbitration as provided in this clause. Prior to commencing any + formal arbitration proceedings, parties shall first seek settlement + of any claim by mediation in accordance with the LCIA Mediation + Rules, which Rules are deemed to be incorporated by reference into + this clause. If the dispute is not settled by mediation within 14 + days of the commencement of the mediation, or such further period as + the parties shall agree in writing, the Claim shall be referred to + and finally resolved by arbitration under the LCIA Rules, which are + deemed to be incorporated by reference into this clause. The + language to be used in the mediation and in the arbitration shall be + English. The seat or legal place of arbitration shall be Dublin, + Ireland. + + + 11.2 Class Action Waiver. YOU AND WE AGREE THAT EACH MAY BRING + CLAIMS AGAINST THE OTHER ONLY ON AN INDIVIDUAL BASIS, AND NOT AS A + PLAINTIFF OR CLASS MEMBER IN ANY PURPORTED CLASS OR REPRESENTATIVE + PROCEEDING. YOU AND WE EXPRESSLY WAIVE ANY RIGHT TO FILE A CLASS + ACTION OR SEEK RELIEF ON A CLASS BASIS. Unless both you and we + agree, no arbitrator or judge may consolidate more than one person’s + claims or otherwise preside over any form of a representative or + class proceeding. The arbitrator may award injunctive relief only in + favor of the individual party seeking relief and only to the extent + necessary to provide relief warranted by that party’s individual + claim. If a court decides that applicable law precludes enforcement + of any of this paragraph’s limitations as to a particular claim for + relief, then that claim (and only that claim) must be severed from + the arbitration and may be brought in court. If any court or + arbitrator determines that the class action waiver set forth in this + paragraph is void or unenforceable for any reason or that an + arbitration can proceed on a class basis, then the arbitration + provision set forth above shall be deemed null and void in its + entirety and the parties shall be deemed to have not agreed to + arbitrate disputes. + + + 11.3 30-Day Right to Opt Out. You have the right to opt-out and not + be bound by the arbitration and class action waiver provisions set + forth above by sending written notice of your decision to opt-out to + the email address notices@consensys.net with subject line LEGAL OPT + OUT. The notice must be sent within 30 days of your first use of the + Offerings, otherwise you shall be bound to arbitrate disputes and + will be deemed to have agreed to waive any right to pursue a class + action in accordance with the terms of those paragraphs. If you + opt-out of these provisions, we will also not be bound by them. + + + 12. Miscellaneous. + + + 12.1 Assignment. You will not assign or otherwise transfer this + Agreement or any of your rights and obligations under this + Agreement, without our prior written consent. Any assignment or + transfer in violation of this Section 12.1 will be void. We may + assign this Agreement without your consent (a) in connection with a + merger, acquisition or sale of all or substantially all of our + assets, or (b) to any Affiliate or as part of a corporate + reorganization; and effective upon such assignment, the assignee is + deemed substituted for us as a party to this Agreement and we are + fully released from all of our obligations and duties to perform + under this Agreement. Subject to the foregoing, this Agreement will + be binding upon, and inure to the benefit of the parties and their + respective permitted successors and assigns. + + + 12.2 DAOs. As a blockchain native company, we may interact with and + provide certain Offerings to DAOs. Due to the unique nature of DAOs, + to the extent the DAO votes in favor of and/or accepts such + Offerings from ConsenSys, the DAO has acknowledged and agreed to + these Terms in their entirety. + + + 12.2 Entire Agreement and Modifications. This Agreement incorporates + the Policies by reference and is the entire agreement between you + and us regarding the subject matter of this Agreement. If the terms + of this document are inconsistent with the terms contained in any + Policy, the terms contained in this document will control. Any + modification to the terms of this Agreement may only be made in + writing. + + + 12.3 Force Majeure. Neither party nor their respective affiliates + will be liable for any delay or failure to perform any obligation + under this Agreement where the delay or failure results from any + cause beyond such party’s reasonable control, including but not + limited to acts of God, utilities or other telecommunications + failures, cyber attacks, earthquake, storms or other elements of + nature, pandemics, blockages, embargoes, riots, acts or orders of + government, acts of terrorism, or war. + + + 12.4 Export and Sanctions Compliance. In connection with this + Agreement, you will comply with all applicable import, re-import, + sanctions, anti-boycott, export, and re-export control laws and + regulations, including all such laws and regulations that may apply. + For clarity, you are solely responsible for compliance related to + the manner in which you choose to use the Offerings. You may not use + any Offering if you are the subject of U.S. sanctions or of + sanctions consistent with U.S. law imposed by the governments of the + country where you are using the Offering.  + + + 12.5 Independent Contractors; Non-Exclusive Rights. We and you are + independent contractors, and this Agreement will not be construed to + create a partnership, joint venture, agency, or employment + relationship. Neither party, nor any of their respective affiliates, + is an agent of the other for any purpose or has the authority to + bind the other. Both parties reserve the right (a) to develop or + have developed for it products, services, concepts, systems, or + techniques that are similar to or compete with the products, + services, concepts, systems, or techniques developed or contemplated + by the other party, and (b) to assist third party developers or + systems integrators who may offer products or services which compete + with the other party’s products or services. + + + 12.6 Eligibility. If you are under the age of majority in your + jurisdiction of residence, you may use the Site or Offerings only + with the consent of or under the supervision of your parent or legal + guardian. + + + NOTICE TO PARENTS AND GUARDIANS: By granting your minor permission + to access the Site or Offerings, you agree to these Terms of Use on + behalf of your minor. You are responsible for exercising supervision + over your minor’s online activities. If you do not agree to these + Terms of Use, do not let your minor use the Site or Offerings. + + + 12.7 Language. All communications and notices made or given pursuant + to this Agreement must be in the English language. If we provide a + translation of the English language version of this Agreement, the + English language version of the Agreement will control if there is + any conflict. + + + 12.8 Notice. + + + (a) To You. We may provide any notice to you under this Agreement + using commercially reasonable means, including: (i) posting a notice + on the Site; (ii) sending a message to the email address then + associated with your account; or (iii) using public communication + channels . Notices we provide by posting on the Site or using public + communication channels will be effective upon posting, and notices + we provide by email will be effective when we send the email. It is + your responsibility to keep your email address current to the extent + you have an account. You will be deemed to have received any email + sent to the email address then associated with your account when we + send the email, whether or not you actually receive the email. + + + (b) To Us. To give us notice under this Agreement, you must contact + us by email at notices@consensys.net.  + + + 12.9 No Third-Party Beneficiaries. Except as otherwise set forth + herein, this Agreement does not create any third-party beneficiary + rights in any individual or entity that is not a party to this + Agreement. + + + 12.10 No Waivers. The failure by us to enforce any provision of this + Agreement will not constitute a present or future waiver of such + provision nor limit our right to enforce such provision at a later + time. All waivers by us must be in writing to be effective. + + + 12.11 Severability. If any portion of this Agreement is held to be + invalid or unenforceable, the remaining portions of this Agreement + will remain in full force and effect. Any invalid or unenforceable + portions will be interpreted to effect and intent of the original + portion. If such construction is not possible, the invalid or + unenforceable portion will be severed from this Agreement but the + rest of the Agreement will remain in full force and effect. + + + 12.12 Notice and Procedure for Making Claims of Copyright + Infringement. If you are a copyright owner or agent of the owner, + and you believe that your copyright or the copyright of a person on + whose behalf you are authorized to act has been infringed, please + provide us a written notice at the address below with the following + information: + + + + an electronic or physical signature of the person authorized to + act on behalf of the owner of the copyright or other intellectual + property interest; + + + a description of the copyrighted work or other intellectual + property that you claim has been infringed; + + + a description of where the material that you claim is infringing + is located with respect to the Offerings; + + + your address, telephone number, and email address; + + + a statement by you that you have a good faith belief that the + disputed use is not authorized by the copyright owner, its agent, + or the law; + + + a statement by you, made under penalty of perjury, that the above + information in your notice is accurate and that you are the + copyright or intellectual property owner or authorized to act on + the copyright or intellectual property owner’s behalf. + + + + You can reach us at: + + + Email: notices@consensys.net + + + Subject Line: Copyright Notification Mail + + + Attention: Copyright ℅ + + + ConsenSys Software Inc.  + + + 49 Bogart Street Suite 22 Brooklyn, NY 11206 + + + 13. Definitions. + + + “Acceptable Use Policy” means the policy set forth below, as it may + be updated by us from time to time. You agree not to, and not to + allow third parties to, use the Offerings: + + + + to violate, or encourage the violation of, the legal rights of + others (for example, this may include allowing End Users to + infringe or misappropriate the intellectual property rights of + others in violation of the Digital Millennium Copyright Act); + + + to engage in, promote or encourage any illegal or infringing + content; + + + for any unlawful, invasive, infringing, defamatory or fraudulent + purpose (for example, this may include phishing, creating a + pyramid scheme or mirroring a website); + + + to intentionally distribute viruses, worms, Trojan horses, + corrupted files, hoaxes, or other items of a destructive or + deceptive nature; + + + to interfere with the use of the Offerings, or the equipment used + to provide the Offerings, by customers, authorized resellers, or + other authorized users; + + + to disable, interfere with or circumvent any aspect of the + Offerings (for example, any thresholds or limits); + + + to generate, distribute, publish or facilitate unsolicited mass + email, promotions, advertising or other solicitation; or + + + to use the Offerings, or any interfaces provided with the + Offerings, to access any other product or service in a manner that + violates the terms of service of such other product or service. + + + + “API” means an application program interface. + + + “API Requests” has the meaning set forth in Section 5.3. + + + “Applicable Threshold” has the meaning set forth in Section 4.2. + + + “Base Fees” has the meaning set forth in Section 4.2. + + + “Content” means any data, text, audio, video or images, software + (including machine images), and any documentation. + + + “DAO” means Decentralized Autonomous Organization. + + + “End User” means any individual or entity that directly or + indirectly through another user: (a) accesses or uses Your Content; + or (b) otherwise accesses or uses the Offerings under your + account.  + + + “Fees” has the meaning set forth in Section 4.2. + + + “Losses” means any claims, damages, losses, liabilities, costs, and + expenses (including reasonable attorneys’ fees).’ + + + “Our Content” means any software (including machine images), data, + text, audio, video, images, or documentation that we offer in + connection with the Offerings.  + + + “Our Marks” means any trademarks, service marks, service or trade + names, logos, and other designations of ConsenSys Software Inc. and + their affiliates or licensors that we may make available to you in + connection with this Agreement. + + + “Order” means an order for Offerings executed through an order form + directly with ConsenSys, or through a cloud vendor, such as Amazon + Web Services, Microsoft Azure, or Google Cloud. + + + “Offerings” means each of the products and services, including but + not limited to Codefi, Infura, MetaMask, Quorum and any other + features, tools, materials, or services offered from time to time, + by us or our affiliates.  + + + “Policies” means the Acceptable Use Policy, Privacy Policy, any + supplemental policies or addendums applicable to any Service as + provided to you, and any other policy or terms referenced in or + incorporated into this Agreement, each as may be updated by us from + time to time. + + + “Privacy Policy” means the privacy policy located at{' '} + + https://consensys.net/privacy-policy + {' '} + (and any successor or related locations designated by us), as it may + be updated by us from time to time. + + + “Service Offerings” means the Services (including associated APIs), + Our Content, Our Marks, and any other product or service provided by + us under this Agreement. Service Offerings do not include + Third-Party Content or Third-Party Services. + + + “Suggestions” means all suggested improvements to the Service + Offerings that you provide to us.. + + + “Term” means the term of this Agreement described in Section 6.1. + + + “Termination Date” means the effective date of termination provided + in accordance with Section 6, in a notice from one party to the + other. + + + “Third-Party Content” means Content made available to you by any + third party on the Site or in conjunction with the Offerings. + + + “Your Content” means content that you or any End User transfers to + us, storage or hosting by the Offerings in connection with account + and any computational results that you or any End User derive from + the foregoing through their use of the Offerings, excluding however + any information submitted to a blockchain protocol for + processing.  + + + { + setIsTermsOfUseChecked(!isTermsOfUseChecked); + }} + /> + + + + + + ); +} + +TermsOfUsePopup.propTypes = { + onAccept: PropTypes.func.isRequired, +}; diff --git a/ui/components/app/terms-of-use-popup/terms-of-use-popup.stories.js b/ui/components/app/terms-of-use-popup/terms-of-use-popup.stories.js new file mode 100644 index 000000000..18272ab61 --- /dev/null +++ b/ui/components/app/terms-of-use-popup/terms-of-use-popup.stories.js @@ -0,0 +1,16 @@ +import React from 'react'; +import TermsOfUsePopup from '.'; + +export default { + title: 'Components/App/TermsOfUsePopup', + component: TermsOfUsePopup, + argTypes: { + onAccept: { + action: 'onAccept', + }, + }, +}; + +export const DefaultStory = (args) => ; + +DefaultStory.storyName = 'Default'; diff --git a/ui/components/app/terms-of-use-popup/terms-of-use-popup.test.js b/ui/components/app/terms-of-use-popup/terms-of-use-popup.test.js new file mode 100644 index 000000000..02125e981 --- /dev/null +++ b/ui/components/app/terms-of-use-popup/terms-of-use-popup.test.js @@ -0,0 +1,48 @@ +import React from 'react'; +import { fireEvent, screen } from '@testing-library/react'; +import { renderWithProvider } from '../../../../test/jest'; +import configureStore from '../../../store/store'; +import mockState from '../../../../test/data/mock-state.json'; +import TermsOfUsePopup from './terms-of-use-popup'; + +const render = () => { + const store = configureStore({ + metamask: { + ...mockState.metamask, + }, + }); + const onAccept = jest.fn(); + return renderWithProvider(, store); +}; + +describe('TermsOfUsePopup', () => { + beforeEach(() => { + const mockIntersectionObserver = jest.fn(); + mockIntersectionObserver.mockReturnValue({ + observe: () => null, + unobserve: () => null, + disconnect: () => null, + }); + window.IntersectionObserver = mockIntersectionObserver; + }); + + it('renders TermsOfUse component and shows Terms of Use text', () => { + render(); + expect( + screen.getByText('Our Terms of Use have updated'), + ).toBeInTheDocument(); + }); + + it('scrolls down when handleScrollDownClick is called', () => { + render(); + const mockScrollIntoView = jest.fn(); + window.HTMLElement.prototype.scrollIntoView = mockScrollIntoView; + const button = document.querySelector( + "[data-testid='popover-scroll-button']", + ); + fireEvent.click(button); + expect(mockScrollIntoView).toHaveBeenCalledWith({ + behavior: 'smooth', + }); + }); +}); diff --git a/ui/components/app/whats-new-popup/index.scss b/ui/components/app/whats-new-popup/index.scss index 04c59af1f..788eb651f 100644 --- a/ui/components/app/whats-new-popup/index.scss +++ b/ui/components/app/whats-new-popup/index.scss @@ -66,28 +66,6 @@ height: 1px; width: 100%; } - - &__scroll-button { - position: absolute; - bottom: 12px; - right: 12px; - height: 32px; - width: 32px; - border-radius: 14px; - border: 1px solid var(--color-border-default); - background: var(--color-background-alternative); - color: var(--color-icon-default); - z-index: 201; - cursor: pointer; - opacity: 0.8; - display: flex; - justify-content: center; - align-items: center; - - &:hover { - opacity: 1; - } - } } .popover-wrap.whats-new-popup__popover { diff --git a/ui/components/ui/popover/index.scss b/ui/components/ui/popover/index.scss index 0a22355eb..760c73212 100644 --- a/ui/components/ui/popover/index.scss +++ b/ui/components/ui/popover/index.scss @@ -62,6 +62,26 @@ } } + &-scroll-button { + position: absolute; + bottom: 12px; + right: 12px; + height: 32px; + width: 32px; + border-radius: 50%; + border: 1px solid var(--color-primary-default); + z-index: 201; + cursor: pointer; + opacity: 1; + display: flex; + justify-content: center; + align-items: center; + + &:hover { + opacity: 1; + } + } + &-arrow { width: 22px; height: 22px; diff --git a/ui/components/ui/popover/popover.component.js b/ui/components/ui/popover/popover.component.js index 6e62e0bfd..2653d64c9 100644 --- a/ui/components/ui/popover/popover.component.js +++ b/ui/components/ui/popover/popover.component.js @@ -127,6 +127,16 @@ const Popover = ({ className={classnames('popover-wrap', className)} ref={popoverRef} > + {showArrow ?
: null} + {showHeader &&
} + {children ? ( + + {children} + + ) : null} {showScrollDown ? ( ) : null} - {showArrow ?
: null} - {showHeader &&
} - {children ? ( - - {children} - - ) : null} {footer ? ( ; // openMetamaskTabsIDs[tab.id]): true/false currentWindowTab: Record; // tabs.tab https://developer.mozilla.org/en-US/docs/Mozilla/Add-ons/WebExtensions/API/tabs/Tab showWhatsNewPopup: boolean; + showTermsOfUsePopup: boolean; singleExceptions: { testKey: string | null; }; @@ -109,6 +110,7 @@ const initialState: AppState = { openMetaMaskTabs: {}, currentWindowTab: {}, showWhatsNewPopup: true, + showTermsOfUsePopup: true, singleExceptions: { testKey: null, }, diff --git a/ui/pages/home/home.component.js b/ui/pages/home/home.component.js index 8f874e7db..616256f21 100644 --- a/ui/pages/home/home.component.js +++ b/ui/pages/home/home.component.js @@ -2,12 +2,14 @@ import React, { PureComponent } from 'react'; import PropTypes from 'prop-types'; import { Redirect, Route } from 'react-router-dom'; ///: BEGIN:ONLY_INCLUDE_IN(main) +// eslint-disable-next-line import/no-duplicates +import { MetaMetricsContextProp } from '../../../shared/constants/metametrics'; +///: END:ONLY_INCLUDE_IN import { - MetaMetricsContextProp, MetaMetricsEventCategory, MetaMetricsEventName, + // eslint-disable-next-line import/no-duplicates } from '../../../shared/constants/metametrics'; -///: END:ONLY_INCLUDE_IN import AssetList from '../../components/app/asset-list'; import NftsTab from '../../components/app/nfts-tab'; import HomeNotification from '../../components/app/home-notification'; @@ -22,6 +24,7 @@ import ConnectedAccounts from '../connected-accounts'; import { Tabs, Tab } from '../../components/ui/tabs'; import { EthOverview } from '../../components/app/wallet-overview'; import WhatsNewPopup from '../../components/app/whats-new-popup'; +import TermsOfUsePopup from '../../components/app/terms-of-use-popup'; import RecoveryPhraseReminder from '../../components/app/recovery-phrase-reminder'; import ActionableMessage from '../../components/ui/actionable-message/actionable-message'; import { @@ -111,6 +114,7 @@ export default class Home extends PureComponent { infuraBlocked: PropTypes.bool.isRequired, showWhatsNewPopup: PropTypes.bool.isRequired, hideWhatsNewPopup: PropTypes.func.isRequired, + showTermsOfUsePopup: PropTypes.bool.isRequired, announcementsToShow: PropTypes.bool.isRequired, ///: BEGIN:ONLY_INCLUDE_IN(flask) errorsToShow: PropTypes.object.isRequired, @@ -120,6 +124,7 @@ export default class Home extends PureComponent { showRecoveryPhraseReminder: PropTypes.bool.isRequired, setRecoveryPhraseReminderHasBeenShown: PropTypes.func.isRequired, setRecoveryPhraseReminderLastShown: PropTypes.func.isRequired, + setTermsOfUseLastAgreed: PropTypes.func.isRequired, showOutdatedBrowserWarning: PropTypes.bool.isRequired, setOutdatedBrowserWarningLastShown: PropTypes.func.isRequired, seedPhraseBackedUp: (props) => { @@ -243,6 +248,18 @@ export default class Home extends PureComponent { setRecoveryPhraseReminderLastShown(new Date().getTime()); }; + onAcceptTermsOfUse = () => { + const { setTermsOfUseLastAgreed } = this.props; + setTermsOfUseLastAgreed(new Date().getTime()); + this.context.trackEvent({ + category: MetaMetricsEventCategory.Onboarding, + event: MetaMetricsEventName.TermsOfUseAccepted, + properties: { + location: 'Terms Of Use Popover', + }, + }); + }; + onOutdatedBrowserWarningClose = () => { const { setOutdatedBrowserWarningLastShown } = this.props; setOutdatedBrowserWarningLastShown(new Date().getTime()); @@ -600,6 +617,7 @@ export default class Home extends PureComponent { announcementsToShow, showWhatsNewPopup, hideWhatsNewPopup, + showTermsOfUsePopup, seedPhraseBackedUp, showRecoveryPhraseReminder, firstTimeFlowType, @@ -621,6 +639,10 @@ export default class Home extends PureComponent { showWhatsNewPopup && !process.env.IN_TEST && !newNetworkAddedConfigurationId; + + const showTermsOfUse = + completedOnboarding && !onboardedInThisUISession && showTermsOfUsePopup; + return (
@@ -637,6 +659,9 @@ export default class Home extends PureComponent { onConfirm={this.onRecoveryPhraseReminderClose} /> ) : null} + {showTermsOfUse ? ( + + ) : null} {isPopup && !connectedStatusPopoverHasBeenShown ? this.renderPopover() : null} diff --git a/ui/pages/home/home.container.js b/ui/pages/home/home.container.js index be0ef90c3..0601c7e1e 100644 --- a/ui/pages/home/home.container.js +++ b/ui/pages/home/home.container.js @@ -17,6 +17,7 @@ import { getShowWhatsNewPopup, getSortedAnnouncementsToShow, getShowRecoveryPhraseReminder, + getShowTermsOfUse, getShowOutdatedBrowserWarning, getNewNetworkAdded, hasUnsignedQRHardwareTransaction, @@ -35,6 +36,7 @@ import { setAlertEnabledness, setRecoveryPhraseReminderHasBeenShown, setRecoveryPhraseReminderLastShown, + setTermsOfUseLastAgreed, setOutdatedBrowserWarningLastShown, setNewNetworkAdded, setNewNftAddedMessage, @@ -136,6 +138,7 @@ const mapStateToProps = (state) => { ///: END:ONLY_INCLUDE_IN showWhatsNewPopup: getShowWhatsNewPopup(state), showRecoveryPhraseReminder: getShowRecoveryPhraseReminder(state), + showTermsOfUsePopup: getShowTermsOfUse(state), showOutdatedBrowserWarning: getIsBrowserDeprecated() && getShowOutdatedBrowserWarning(state), seedPhraseBackedUp, @@ -166,6 +169,9 @@ const mapDispatchToProps = (dispatch) => ({ dispatch(setRecoveryPhraseReminderHasBeenShown()), setRecoveryPhraseReminderLastShown: (lastShown) => dispatch(setRecoveryPhraseReminderLastShown(lastShown)), + setTermsOfUseLastAgreed: (lastAgreed) => { + dispatch(setTermsOfUseLastAgreed(lastAgreed)); + }, setOutdatedBrowserWarningLastShown: (lastShown) => { dispatch(setOutdatedBrowserWarningLastShown(lastShown)); }, diff --git a/ui/pages/onboarding-flow/welcome/index.scss b/ui/pages/onboarding-flow/welcome/index.scss index a2cab4582..595adf152 100644 --- a/ui/pages/onboarding-flow/welcome/index.scss +++ b/ui/pages/onboarding-flow/welcome/index.scss @@ -51,4 +51,9 @@ margin-bottom: 24px; } } + + &__terms-checkbox { + margin: 0; + align-self: flex-start; + } } diff --git a/ui/pages/onboarding-flow/welcome/welcome.js b/ui/pages/onboarding-flow/welcome/welcome.js index 042282dc2..00f83d79a 100644 --- a/ui/pages/onboarding-flow/welcome/welcome.js +++ b/ui/pages/onboarding-flow/welcome/welcome.js @@ -6,10 +6,13 @@ import { Carousel } from 'react-responsive-carousel'; import Mascot from '../../../components/ui/mascot'; import Button from '../../../components/ui/button'; import { Text } from '../../../components/component-library'; +import CheckBox from '../../../components/ui/check-box'; +import Box from '../../../components/ui/box'; import { FONT_WEIGHT, TEXT_ALIGN, TextVariant, + AlignItems, } from '../../../helpers/constants/design-system'; import { useI18nContext } from '../../../hooks/useI18nContext'; import { MetaMetricsContext } from '../../../contexts/metametrics'; @@ -17,7 +20,10 @@ import { MetaMetricsEventCategory, MetaMetricsEventName, } from '../../../../shared/constants/metametrics'; -import { setFirstTimeFlowType } from '../../../store/actions'; +import { + setFirstTimeFlowType, + setTermsOfUseLastAgreed, +} from '../../../store/actions'; import { ONBOARDING_METAMETRICS, ONBOARDING_SECURE_YOUR_WALLET_ROUTE, @@ -33,6 +39,7 @@ export default function OnboardingWelcome() { const [eventEmitter] = useState(new EventEmitter()); const currentKeyring = useSelector(getCurrentKeyring); const firstTimeFlowType = useSelector(getFirstTimeFlowType); + const [termsChecked, setTermsChecked] = useState(false); // Don't allow users to come back to this screen after they // have already imported or created a wallet @@ -56,8 +63,23 @@ export default function OnboardingWelcome() { account_type: 'metamask', }, }); + dispatch(setTermsOfUseLastAgreed(new Date().getTime())); history.push(ONBOARDING_METAMETRICS); }; + const toggleTermsCheck = () => { + setTermsChecked((currentTermsChecked) => !currentTermsChecked); + }; + const termsOfUse = t('agreeTermsOfUse', [ + + {t('terms')} + , + ]); const onImportClick = () => { dispatch(setFirstTimeFlowType('import')); @@ -68,6 +90,7 @@ export default function OnboardingWelcome() { account_type: 'imported', }, }); + dispatch(setTermsOfUseLastAgreed(new Date().getTime())); history.push(ONBOARDING_METAMETRICS); }; @@ -147,11 +170,35 @@ export default function OnboardingWelcome() {
    +
  • + + + + +
  • +
  • @@ -161,6 +208,7 @@ export default function OnboardingWelcome() { data-testid="onboarding-import-wallet" type="secondary" onClick={onImportClick} + disabled={!termsChecked} > {t('onboardingImportWallet')} diff --git a/ui/pages/onboarding-flow/welcome/welcome.test.js b/ui/pages/onboarding-flow/welcome/welcome.test.js index 8cd699012..c7965efd5 100644 --- a/ui/pages/onboarding-flow/welcome/welcome.test.js +++ b/ui/pages/onboarding-flow/welcome/welcome.test.js @@ -4,7 +4,10 @@ import configureMockStore from 'redux-mock-store'; import thunk from 'redux-thunk'; import initializedMockState from '../../../../test/data/mock-state.json'; import { renderWithProvider } from '../../../../test/lib/render-helpers'; -import { setFirstTimeFlowType } from '../../../store/actions'; +import { + setFirstTimeFlowType, + setTermsOfUseLastAgreed, +} from '../../../store/actions'; import { ONBOARDING_METAMETRICS, ONBOARDING_SECURE_YOUR_WALLET_ROUTE, @@ -21,6 +24,11 @@ jest.mock('../../../store/actions.ts', () => ({ return type; }), ), + setTermsOfUseLastAgreed: jest.fn().mockReturnValue( + jest.fn((type) => { + return type; + }), + ), })); jest.mock('react-router-dom', () => ({ @@ -81,19 +89,24 @@ describe('Onboarding Welcome Component', () => { it('should set first time flow to create and route to metametrics', () => { renderWithProvider(, mockStore); - + const termsCheckbox = screen.getByTestId('onboarding-terms-checkbox'); + fireEvent.click(termsCheckbox); const createWallet = screen.getByTestId('onboarding-create-wallet'); fireEvent.click(createWallet); + expect(setTermsOfUseLastAgreed).toHaveBeenCalled(); expect(setFirstTimeFlowType).toHaveBeenCalledWith('create'); }); it('should set first time flow to import and route to metametrics', () => { renderWithProvider(, mockStore); + const termsCheckbox = screen.getByTestId('onboarding-terms-checkbox'); + fireEvent.click(termsCheckbox); const createWallet = screen.getByTestId('onboarding-import-wallet'); fireEvent.click(createWallet); + expect(setTermsOfUseLastAgreed).toHaveBeenCalled(); expect(setFirstTimeFlowType).toHaveBeenCalledWith('import'); expect(mockHistoryPush).toHaveBeenCalledWith(ONBOARDING_METAMETRICS); }); diff --git a/ui/selectors/selectors.js b/ui/selectors/selectors.js index 44d2b914d..7c3f6bf6d 100644 --- a/ui/selectors/selectors.js +++ b/ui/selectors/selectors.js @@ -57,6 +57,7 @@ import { import { TEMPLATED_CONFIRMATION_MESSAGE_TYPES } from '../pages/confirmation/templates'; import { STATIC_MAINNET_TOKEN_LIST } from '../../shared/constants/tokens'; import { DAY } from '../../shared/constants/time'; +import { TERMS_OF_USE_LAST_UPDATED } from '../../shared/constants/terms'; import { getNativeCurrency, getConversionRate, @@ -995,6 +996,18 @@ export function getShowRecoveryPhraseReminder(state) { return currentTime - recoveryPhraseReminderLastShown >= frequency; } +export function getShowTermsOfUse(state) { + const { termsOfUseLastAgreed } = state.metamask; + + if (!termsOfUseLastAgreed) { + return true; + } + return ( + new Date(termsOfUseLastAgreed).getTime() < + new Date(TERMS_OF_USE_LAST_UPDATED).getTime() + ); +} + export function getShowOutdatedBrowserWarning(state) { const { outdatedBrowserWarningLastShown } = state.metamask; if (!outdatedBrowserWarningLastShown) { diff --git a/ui/store/actions.ts b/ui/store/actions.ts index fe47604c9..dd897d303 100644 --- a/ui/store/actions.ts +++ b/ui/store/actions.ts @@ -3980,6 +3980,12 @@ export function setRecoveryPhraseReminderLastShown( }; } +export function setTermsOfUseLastAgreed(lastAgreed: number) { + return async () => { + await submitRequestToBackground('setTermsOfUseLastAgreed', [lastAgreed]); + }; +} + export function setOutdatedBrowserWarningLastShown(lastShown: number) { return async () => { await submitRequestToBackground('setOutdatedBrowserWarningLastShown', [ From d85d2318a354e7390cfbb9dabbb034df13087eae Mon Sep 17 00:00:00 2001 From: Mark Stacey Date: Fri, 14 Apr 2023 14:53:57 -0230 Subject: [PATCH 31/36] Make `_switchNetwork` async (#18597) The network controller internal method `_switchNetwork` has been made async, and the `lookupNetwork` call is now awaited. Because this method is only used internally, and because the `await`-ed async operation was the last operation in this function, this change has no functional impact whatsoever. Relates to https://github.com/MetaMask/metamask-extension/issues/18587 --- app/scripts/controllers/network/network-controller.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/app/scripts/controllers/network/network-controller.ts b/app/scripts/controllers/network/network-controller.ts index 74bd39f42..19857d16f 100644 --- a/app/scripts/controllers/network/network-controller.ts +++ b/app/scripts/controllers/network/network-controller.ts @@ -904,14 +904,14 @@ export class NetworkController extends EventEmitter { * @param providerConfig - The provider configuration object that specifies * the new network. */ - _switchNetwork(providerConfig: ProviderConfiguration): void { + async _switchNetwork(providerConfig: ProviderConfiguration) { this.messenger.publish(NetworkControllerEventType.NetworkWillChange); this._resetNetworkId(); this._resetNetworkStatus(); this._resetNetworkDetails(); this._configureProvider(providerConfig); this.messenger.publish(NetworkControllerEventType.NetworkDidChange); - this.lookupNetwork(); + await this.lookupNetwork(); } /** From fa32c5deb942884717317732f0c1b0aeead139fa Mon Sep 17 00:00:00 2001 From: Matthew Walsh Date: Fri, 14 Apr 2023 18:33:53 +0100 Subject: [PATCH 32/36] Disable rate limiting for signature approval requests (#18594) --- app/scripts/controllers/sign.test.ts | 16 ++++++++++++++++ app/scripts/controllers/sign.ts | 27 ++++++++++++++++++--------- app/scripts/metamask-controller.js | 7 ++++++- package.json | 2 +- yarn.lock | 12 ++++++------ 5 files changed, 47 insertions(+), 17 deletions(-) diff --git a/app/scripts/controllers/sign.test.ts b/app/scripts/controllers/sign.test.ts index 36dddf65b..dac749466 100644 --- a/app/scripts/controllers/sign.test.ts +++ b/app/scripts/controllers/sign.test.ts @@ -409,6 +409,14 @@ describe('SignController', () => { ); }); + it('does not throw if accepting approval throws', async () => { + messengerMock.call.mockImplementation(() => { + throw new Error('Test Error'); + }); + + await signController[signMethodName](messageParamsMock); + }); + it('rejects message on error', async () => { keyringControllerMock[signMethodName].mockReset(); keyringControllerMock[signMethodName].mockRejectedValue( @@ -468,6 +476,14 @@ describe('SignController', () => { 'Cancel', ); }); + + it('does not throw if rejecting approval throws', async () => { + messengerMock.call.mockImplementation(() => { + throw new Error('Test Error'); + }); + + await signController[cancelMethodName](messageParamsMock); + }); }); describe('message manager events', () => { diff --git a/app/scripts/controllers/sign.ts b/app/scripts/controllers/sign.ts index 1712ed1ee..d27de2f06 100644 --- a/app/scripts/controllers/sign.ts +++ b/app/scripts/controllers/sign.ts @@ -33,12 +33,13 @@ import { RejectRequest, } from '@metamask/approval-controller'; import { MetaMetricsEventCategory } from '../../../shared/constants/metametrics'; +import { MESSAGE_TYPE } from '../../../shared/constants/app'; import PreferencesController from './preferences'; const controllerName = 'SignController'; -const methodNameSign = 'eth_sign'; -const methodNamePersonalSign = 'personal_sign'; -const methodNameTypedSign = 'eth_signTypedData'; +const methodNameSign = MESSAGE_TYPE.ETH_SIGN; +const methodNamePersonalSign = MESSAGE_TYPE.PERSONAL_SIGN; +const methodNameTypedSign = MESSAGE_TYPE.ETH_SIGN_TYPED_DATA; const stateMetadata = { unapprovedMsgs: { persist: false, anonymous: false }, @@ -636,14 +637,22 @@ export default class SignController extends BaseControllerV2< } private _acceptApproval(messageId: string) { - this.messagingSystem.call('ApprovalController:acceptRequest', messageId); + try { + this.messagingSystem.call('ApprovalController:acceptRequest', messageId); + } catch (error) { + log.info('Failed to accept signature approval request', error); + } } private _rejectApproval(messageId: string) { - this.messagingSystem.call( - 'ApprovalController:rejectRequest', - messageId, - 'Cancel', - ); + try { + this.messagingSystem.call( + 'ApprovalController:rejectRequest', + messageId, + 'Cancel', + ); + } catch (error) { + log.info('Failed to reject signature approval request', error); + } } } diff --git a/app/scripts/metamask-controller.js b/app/scripts/metamask-controller.js index 1dcea32f1..48a130dd5 100644 --- a/app/scripts/metamask-controller.js +++ b/app/scripts/metamask-controller.js @@ -97,8 +97,8 @@ import { import { MILLISECOND, SECOND } from '../../shared/constants/time'; import { ORIGIN_METAMASK, - ///: BEGIN:ONLY_INCLUDE_IN(flask) MESSAGE_TYPE, + ///: BEGIN:ONLY_INCLUDE_IN(flask) SNAP_DIALOG_TYPES, ///: END:ONLY_INCLUDE_IN POLLING_TOKEN_ENVIRONMENT_TYPES, @@ -260,6 +260,11 @@ export default class MetamaskController extends EventEmitter { name: 'ApprovalController', }), showApprovalRequest: opts.showUserConfirmation, + typesExcludedFromRateLimiting: [ + MESSAGE_TYPE.ETH_SIGN, + MESSAGE_TYPE.PERSONAL_SIGN, + MESSAGE_TYPE.ETH_SIGN_TYPED_DATA, + ], }); const networkControllerMessenger = this.controllerMessenger.getRestricted({ diff --git a/package.json b/package.json index 710d9fa11..f0c815146 100644 --- a/package.json +++ b/package.json @@ -228,7 +228,7 @@ "@material-ui/core": "^4.11.0", "@metamask/address-book-controller": "^2.0.0", "@metamask/announcement-controller": "^3.0.0", - "@metamask/approval-controller": "^2.0.0", + "@metamask/approval-controller": "^2.1.0", "@metamask/assets-controllers": "^5.0.0", "@metamask/base-controller": "^2.0.0", "@metamask/contract-metadata": "^2.3.1", diff --git a/yarn.lock b/yarn.lock index d6733e961..080233a0b 100644 --- a/yarn.lock +++ b/yarn.lock @@ -3602,16 +3602,16 @@ __metadata: languageName: node linkType: hard -"@metamask/approval-controller@npm:^2.0.0": - version: 2.0.0 - resolution: "@metamask/approval-controller@npm:2.0.0" +"@metamask/approval-controller@npm:^2.0.0, @metamask/approval-controller@npm:^2.1.0": + version: 2.1.0 + resolution: "@metamask/approval-controller@npm:2.1.0" dependencies: "@metamask/base-controller": ^2.0.0 - "@metamask/controller-utils": ^3.0.0 + "@metamask/controller-utils": ^3.1.0 eth-rpc-errors: ^4.0.0 immer: ^9.0.6 nanoid: ^3.1.31 - checksum: 1db5f9c21b04fa4688c17cdfb7da0a14b3fee084fbd8c0cfdcc41572e54140ce093c24b811b85e8ee9d3ccd8987db04d9150d7c6d5ab21daf72b4364a05f3428 + checksum: 207380e3ed0007aec3b9efcde62ac3ece9fa46cc7b9e6157c0d54271e0936b5a9a05e59adcfb1e47e3f3df397d2d2dc757f3b97745528695182e7c66a5207aca languageName: node linkType: hard @@ -24159,7 +24159,7 @@ __metadata: "@material-ui/core": ^4.11.0 "@metamask/address-book-controller": ^2.0.0 "@metamask/announcement-controller": ^3.0.0 - "@metamask/approval-controller": ^2.0.0 + "@metamask/approval-controller": ^2.1.0 "@metamask/assets-controllers": ^5.0.0 "@metamask/auto-changelog": ^2.1.0 "@metamask/base-controller": ^2.0.0 From b9ff4e879890c8bc84ba8b50d9aeb11142c02a0b Mon Sep 17 00:00:00 2001 From: Mark Stacey Date: Fri, 14 Apr 2023 16:24:57 -0230 Subject: [PATCH 33/36] Remove the network controller `previousProviderStore` (#18593) * Remove the network controller `previousProviderStore` The `previousProvider` network controller state has been replaced with a private internal property. This was only used internally, it did not need to be part of state. This relates to https://github.com/MetaMask/metamask-extension/issues/18303 * Remove redundant tests --- .storybook/test-data.js | 7 -- .../network/network-controller.test.ts | 115 ------------------ .../controllers/network/network-controller.ts | 12 +- app/scripts/migrations/085.test.js | 91 ++++++++++++++ app/scripts/migrations/085.ts | 33 +++++ app/scripts/migrations/index.js | 2 + 6 files changed, 130 insertions(+), 130 deletions(-) create mode 100644 app/scripts/migrations/085.test.js create mode 100644 app/scripts/migrations/085.ts diff --git a/.storybook/test-data.js b/.storybook/test-data.js index 3f133fcba..5fe59bb84 100644 --- a/.storybook/test-data.js +++ b/.storybook/test-data.js @@ -606,13 +606,6 @@ const state = { rpcUrl: '', chainId: '0x5', }, - previousProviderStore: { - type: 'goerli', - ticker: 'ETH', - nickname: '', - rpcUrl: '', - chainId: '0x5', - }, network: '5', accounts: { '0x64a845a5b02460acf8a3d84503b0d68d028b4bb4': { diff --git a/app/scripts/controllers/network/network-controller.test.ts b/app/scripts/controllers/network/network-controller.test.ts index 5e848830a..6ea6527f5 100644 --- a/app/scripts/controllers/network/network-controller.test.ts +++ b/app/scripts/controllers/network/network-controller.test.ts @@ -647,12 +647,6 @@ describe('NetworkController', () => { }, "networkId": null, "networkStatus": "unknown", - "previousProviderStore": { - "chainId": "0x9999", - "nickname": "Test initial state", - "rpcUrl": "http://example-custom-rpc.metamask.io", - "type": "rpc", - }, "provider": { "chainId": "0x9999", "nickname": "Test initial state", @@ -677,13 +671,6 @@ describe('NetworkController', () => { }, "networkId": null, "networkStatus": "unknown", - "previousProviderStore": { - "chainId": "0x539", - "nickname": "Localhost 8545", - "rpcUrl": "http://localhost:8545", - "ticker": "ETH", - "type": "rpc", - }, "provider": { "chainId": "0x539", "nickname": "Localhost 8545", @@ -4116,46 +4103,6 @@ describe('NetworkController', () => { ); }); - it('stores the current provider configuration before overwriting it', async () => { - await withController( - { - state: { - provider: { - type: 'rpc', - rpcUrl: 'https://mock-rpc-url-1', - chainId: '0x111', - ticker: 'TEST', - }, - networkConfigurations: { - testNetworkConfigurationId2: { - id: 'testNetworkConfigurationId2', - rpcUrl: 'https://mock-rpc-url-2', - chainId: '0x222', - ticker: 'ABC', - }, - }, - }, - }, - async ({ controller }) => { - const network = new CustomNetworkCommunications({ - customRpcUrl: 'https://mock-rpc-url-2', - }); - network.mockEssentialRpcCalls(); - - controller.setActiveNetwork('testNetworkConfigurationId2'); - - expect( - controller.store.getState().previousProviderStore, - ).toStrictEqual({ - type: 'rpc', - rpcUrl: 'https://mock-rpc-url-1', - chainId: '0x111', - ticker: 'TEST', - }); - }, - ); - }); - it('overwrites the provider configuration given a networkConfigurationId that matches a configured networkConfiguration', async () => { await withController( { @@ -4638,68 +4585,6 @@ describe('NetworkController', () => { describe('setProviderType', () => { for (const { networkType, chainId, ticker } of INFURA_NETWORKS) { describe(`given a type of "${networkType}"`, () => { - it('stores the current provider configuration before overwriting it', async () => { - await withController( - { - state: { - provider: { - type: 'rpc', - rpcUrl: 'http://mock-rpc-url-2', - chainId: '0xtest2', - nickname: 'test-chain-2', - ticker: 'TEST2', - rpcPrefs: { - blockExplorerUrl: 'test-block-explorer-2.com', - }, - }, - networkConfigurations: { - testNetworkConfigurationId1: { - rpcUrl: 'https://mock-rpc-url-1', - chainId: '0xtest', - nickname: 'test-chain', - ticker: 'TEST', - rpcPrefs: { - blockExplorerUrl: 'test-block-explorer.com', - }, - id: 'testNetworkConfigurationId1', - }, - testNetworkConfigurationId2: { - rpcUrl: 'http://mock-rpc-url-2', - chainId: '0xtest2', - nickname: 'test-chain-2', - ticker: 'TEST2', - rpcPrefs: { - blockExplorerUrl: 'test-block-explorer-2.com', - }, - id: 'testNetworkConfigurationId2', - }, - }, - }, - }, - async ({ controller }) => { - const network = new InfuraNetworkCommunications({ - infuraNetwork: networkType, - }); - network.mockEssentialRpcCalls(); - - controller.setProviderType(networkType); - - expect( - controller.store.getState().previousProviderStore, - ).toStrictEqual({ - type: 'rpc', - rpcUrl: 'http://mock-rpc-url-2', - chainId: '0xtest2', - nickname: 'test-chain-2', - ticker: 'TEST2', - rpcPrefs: { - blockExplorerUrl: 'test-block-explorer-2.com', - }, - }); - }, - ); - }); - it(`overwrites the provider configuration using type: "${networkType}", chainId: "${chainId}", and ticker "${ticker}", clearing rpcUrl and nickname, and removing rpcPrefs`, async () => { await withController( { diff --git a/app/scripts/controllers/network/network-controller.ts b/app/scripts/controllers/network/network-controller.ts index 19857d16f..a75cd6100 100644 --- a/app/scripts/controllers/network/network-controller.ts +++ b/app/scripts/controllers/network/network-controller.ts @@ -267,7 +267,6 @@ type NetworkConfigurations = Record< */ export type NetworkControllerState = { provider: ProviderConfiguration; - previousProviderStore: ProviderConfiguration; networkId: NetworkIdState; networkStatus: NetworkStatus; networkDetails: NetworkDetails; @@ -424,7 +423,7 @@ export class NetworkController extends EventEmitter { * Observable store containing the provider configuration for the previously * configured network. */ - previousProviderStore: ObservableStore; + #previousProviderConfig: ProviderConfiguration; /** * Observable store containing the network ID for the current network or null @@ -489,9 +488,7 @@ export class NetworkController extends EventEmitter { this.providerStore = new ObservableStore( state.provider || buildDefaultProviderConfigState(), ); - this.previousProviderStore = new ObservableStore( - this.providerStore.getState(), - ); + this.#previousProviderConfig = this.providerStore.getState(); this.networkIdStore = new ObservableStore(buildDefaultNetworkIdState()); this.networkStatusStore = new ObservableStore( buildDefaultNetworkStatusState(), @@ -511,7 +508,6 @@ export class NetworkController extends EventEmitter { this.store = new ComposedStore({ provider: this.providerStore, - previousProviderStore: this.previousProviderStore, networkId: this.networkIdStore, networkStatus: this.networkStatusStore, networkDetails: this.networkDetails, @@ -792,7 +788,7 @@ export class NetworkController extends EventEmitter { * calling `resetConnection`). */ rollbackToPreviousProvider(): void { - const config = this.previousProviderStore.getState(); + const config = this.#previousProviderConfig; this.providerStore.putState(config); this._switchNetwork(config); } @@ -871,7 +867,7 @@ export class NetworkController extends EventEmitter { * @param providerConfig - The provider configuration. */ _setProviderConfig(providerConfig: ProviderConfiguration): void { - this.previousProviderStore.putState(this.providerStore.getState()); + this.#previousProviderConfig = this.providerStore.getState(); this.providerStore.putState(providerConfig); this._switchNetwork(providerConfig); } diff --git a/app/scripts/migrations/085.test.js b/app/scripts/migrations/085.test.js new file mode 100644 index 000000000..6b7b4967d --- /dev/null +++ b/app/scripts/migrations/085.test.js @@ -0,0 +1,91 @@ +import { migrate, version } from './085'; + +jest.mock('uuid', () => { + const actual = jest.requireActual('uuid'); + + return { + ...actual, + v4: jest.fn(), + }; +}); + +describe('migration #85', () => { + it('should update the version metadata', async () => { + const oldStorage = { + meta: { + version: 84, + }, + data: {}, + }; + + const newStorage = await migrate(oldStorage); + expect(newStorage.meta).toStrictEqual({ + version, + }); + }); + + it('should return state unaltered if there is no network controller state', async () => { + const oldData = { + other: 'data', + }; + const oldStorage = { + meta: { + version: 84, + }, + data: oldData, + }; + + const newStorage = await migrate(oldStorage); + expect(newStorage.data).toStrictEqual(oldData); + }); + + it('should return state unaltered if there is no network controller previous provider state', async () => { + const oldData = { + other: 'data', + NetworkController: { + provider: { + some: 'provider', + }, + }, + }; + const oldStorage = { + meta: { + version: 84, + }, + data: oldData, + }; + + const newStorage = await migrate(oldStorage); + expect(newStorage.data).toStrictEqual(oldData); + }); + + it('should remove the previous provider state', async () => { + const oldData = { + other: 'data', + NetworkController: { + previousProviderStore: { + example: 'config', + }, + provider: { + some: 'provider', + }, + }, + }; + const oldStorage = { + meta: { + version: 84, + }, + data: oldData, + }; + + const newStorage = await migrate(oldStorage); + expect(newStorage.data).toStrictEqual({ + other: 'data', + NetworkController: { + provider: { + some: 'provider', + }, + }, + }); + }); +}); diff --git a/app/scripts/migrations/085.ts b/app/scripts/migrations/085.ts new file mode 100644 index 000000000..03499d2b2 --- /dev/null +++ b/app/scripts/migrations/085.ts @@ -0,0 +1,33 @@ +import { cloneDeep } from 'lodash'; +import { isObject } from '@metamask/utils'; + +export const version = 85; + +/** + * Remove the now-obsolete network controller `previousProviderStore` state. + * + * @param originalVersionedData - Versioned MetaMask extension state, exactly what we persist to dist. + * @param originalVersionedData.meta - State metadata. + * @param originalVersionedData.meta.version - The current state version. + * @param originalVersionedData.data - The persisted MetaMask state, keyed by controller. + * @returns Updated versioned MetaMask extension state. + */ +export async function migrate(originalVersionedData: { + meta: { version: number }; + data: Record; +}) { + const versionedData = cloneDeep(originalVersionedData); + versionedData.meta.version = version; + versionedData.data = transformState(versionedData.data); + return versionedData; +} + +function transformState(state: Record) { + if (!isObject(state.NetworkController)) { + return state; + } + + delete state.NetworkController.previousProviderStore; + + return state; +} diff --git a/app/scripts/migrations/index.js b/app/scripts/migrations/index.js index 54a09c2b4..5cbe4ee04 100644 --- a/app/scripts/migrations/index.js +++ b/app/scripts/migrations/index.js @@ -88,6 +88,7 @@ import * as m081 from './081'; import * as m082 from './082'; import * as m083 from './083'; import * as m084 from './084'; +import * as m085 from './085'; const migrations = [ m002, @@ -173,6 +174,7 @@ const migrations = [ m082, m083, m084, + m085, ]; export default migrations; From e6d1b052f024f174aa5be06363334d24136f3073 Mon Sep 17 00:00:00 2001 From: Mark Stacey Date: Fri, 14 Apr 2023 19:10:32 -0230 Subject: [PATCH 34/36] Make `_setProviderConfig` async (#18600) The network controller internal method `_setProviderConfig` has been made async, the async `_switchNetwork` operation is now `await`-ed. Since the `_switchNetwork` call was the last operation, this has zero functional impact. Relates to https://github.com/MetaMask/metamask-extension/issues/18587 --- app/scripts/controllers/network/network-controller.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/app/scripts/controllers/network/network-controller.ts b/app/scripts/controllers/network/network-controller.ts index a75cd6100..ee0ea686c 100644 --- a/app/scripts/controllers/network/network-controller.ts +++ b/app/scripts/controllers/network/network-controller.ts @@ -866,10 +866,10 @@ export class NetworkController extends EventEmitter { * * @param providerConfig - The provider configuration. */ - _setProviderConfig(providerConfig: ProviderConfiguration): void { + async _setProviderConfig(providerConfig: ProviderConfiguration) { this.#previousProviderConfig = this.providerStore.getState(); this.providerStore.putState(providerConfig); - this._switchNetwork(providerConfig); + await this._switchNetwork(providerConfig); } /** From 1406885115cf7d15d4fb7aee3939ca776ba95db3 Mon Sep 17 00:00:00 2001 From: Ariella Vu <20778143+digiwand@users.noreply.github.com> Date: Fri, 14 Apr 2023 21:10:27 -0300 Subject: [PATCH 35/36] siwe: add ledger instructions (#18589) --- .../signature-request-siwe.js | 11 ++++++++ .../signature-request-siwe.test.js | 26 +++++++++++++++++++ 2 files changed, 37 insertions(+) diff --git a/ui/components/app/signature-request-siwe/signature-request-siwe.js b/ui/components/app/signature-request-siwe/signature-request-siwe.js index 11e515eee..de7441cb7 100644 --- a/ui/components/app/signature-request-siwe/signature-request-siwe.js +++ b/ui/components/app/signature-request-siwe/signature-request-siwe.js @@ -7,6 +7,7 @@ import Popover from '../../ui/popover'; import Checkbox from '../../ui/check-box'; import { I18nContext } from '../../../contexts/i18n'; import { PageContainerFooter } from '../../ui/page-container'; +import { isAddressLedger } from '../../../ducks/metamask/metamask'; import { accountsWithSendEtherInfoSelector, getSubjectMetadata, @@ -20,6 +21,7 @@ import { import SecurityProviderBannerMessage from '../security-provider-banner-message/security-provider-banner-message'; import { SECURITY_PROVIDER_MESSAGE_SEVERITIES } from '../security-provider-banner-message/security-provider-banner-message.constants'; +import LedgerInstructionField from '../ledger-instruction-field'; import Header from './signature-request-siwe-header'; import Message from './signature-request-siwe-message'; @@ -39,6 +41,8 @@ export default function SignatureRequestSIWE({ }, } = txData; + const isLedgerWallet = useSelector((state) => isAddressLedger(state, from)); + const fromAccount = getAccountByAddress(allAccounts, from); const targetSubjectMetadata = subjectMetadata[origin]; @@ -115,6 +119,13 @@ export default function SignatureRequestSIWE({ ])} )} + + {isLedgerWallet && ( +
    + +
    + )} + {!isSIWEDomainValid && ( { + return { + __esModule: true, + default: () => { + return
    ; + }, + }; +}); + const render = (txData = mockProps.txData) => { const store = configureStore(mockStoreInitialState); @@ -110,4 +119,21 @@ describe('SignatureRequestSIWE (Sign in with Ethereum)', () => { expect(bannerAlert).toBeTruthy(); expect(bannerAlert).toHaveTextContent('Deceptive site request.'); }); + + it('should not show Ledger instructions if the address is not a Ledger address', () => { + const { container } = render(); + expect( + container.querySelector('.mock-ledger-instruction-field'), + ).not.toBeTruthy(); + }); + + it('should show Ledger instructions if the address is a Ledger address', () => { + const mockTxData = cloneDeep(mockProps.txData); + mockTxData.msgParams.from = '0xc42edfcc21ed14dda456aa0756c153f7985d8813'; + const { container } = render(mockTxData); + + expect( + container.querySelector('.mock-ledger-instruction-field'), + ).toBeTruthy(); + }); }); From 73efd2edebaee5f86a62b5ec8ffc448535b79736 Mon Sep 17 00:00:00 2001 From: Mark Stacey Date: Sat, 15 Apr 2023 10:31:59 -0230 Subject: [PATCH 36/36] Make `rollbackToPreviousProvider` async (#18599) The network controller method `rollbackToPreviousProvider` is now async. It will resolve when the network switch has completed. Relates to https://github.com/MetaMask/metamask-extension/issues/18587 --- .../network/network-controller.test.ts | 70 +++++++++++-------- .../controllers/network/network-controller.ts | 4 +- 2 files changed, 41 insertions(+), 33 deletions(-) diff --git a/app/scripts/controllers/network/network-controller.test.ts b/app/scripts/controllers/network/network-controller.test.ts index 6ea6527f5..e1cf8edcb 100644 --- a/app/scripts/controllers/network/network-controller.test.ts +++ b/app/scripts/controllers/network/network-controller.test.ts @@ -5761,8 +5761,8 @@ describe('NetworkController', () => { await waitForLookupNetworkToComplete({ controller, - operation: () => { - controller.rollbackToPreviousProvider(); + operation: async () => { + await controller.rollbackToPreviousProvider(); }, }); expect(controller.store.getState().provider).toStrictEqual({ @@ -5827,8 +5827,8 @@ describe('NetworkController', () => { const networkWillChange = await waitForPublishedEvents({ messenger: unrestrictedMessenger, eventType: NetworkControllerEventType.NetworkWillChange, - operation: () => { - controller.rollbackToPreviousProvider(); + operation: async () => { + await controller.rollbackToPreviousProvider(); }, }); @@ -5891,6 +5891,8 @@ describe('NetworkController', () => { // happens before networkDidChange count: 1, operation: () => { + // Intentionally not awaited because we want to check state + // while this operation is in-progress controller.rollbackToPreviousProvider(); }, }); @@ -5959,6 +5961,8 @@ describe('NetworkController', () => { // happens before networkDidChange count: 1, operation: () => { + // Intentionally not awaited because we want to check state + // while this operation is in-progress controller.rollbackToPreviousProvider(); }, }); @@ -6015,8 +6019,8 @@ describe('NetworkController', () => { await waitForLookupNetworkToComplete({ controller, - operation: () => { - controller.rollbackToPreviousProvider(); + operation: async () => { + await controller.rollbackToPreviousProvider(); }, }); @@ -6077,8 +6081,8 @@ describe('NetworkController', () => { controller.getProviderAndBlockTracker(); await waitForLookupNetworkToComplete({ controller, - operation: () => { - controller.rollbackToPreviousProvider(); + operation: async () => { + await controller.rollbackToPreviousProvider(); }, }); const { provider: providerAfter } = @@ -6138,8 +6142,8 @@ describe('NetworkController', () => { const networkDidChange = await waitForPublishedEvents({ messenger: unrestrictedMessenger, eventType: NetworkControllerEventType.NetworkDidChange, - operation: () => { - controller.rollbackToPreviousProvider(); + operation: async () => { + await controller.rollbackToPreviousProvider(); }, }); expect(networkDidChange).toBeTruthy(); @@ -6203,7 +6207,7 @@ describe('NetworkController', () => { await waitForLookupNetworkToComplete({ controller, operation: async () => { - controller.rollbackToPreviousProvider(); + await controller.rollbackToPreviousProvider(); }, }); @@ -6266,8 +6270,8 @@ describe('NetworkController', () => { await waitForLookupNetworkToComplete({ controller, - operation: () => { - controller.rollbackToPreviousProvider(); + operation: async () => { + await controller.rollbackToPreviousProvider(); }, }); expect(controller.store.getState().networkStatus).toBe( @@ -6323,8 +6327,8 @@ describe('NetworkController', () => { await waitForLookupNetworkToComplete({ controller, numberOfNetworkDetailsChanges: 2, - operation: () => { - controller.rollbackToPreviousProvider(); + operation: async () => { + await controller.rollbackToPreviousProvider(); }, }); expect(controller.store.getState().networkDetails).toStrictEqual({ @@ -6408,8 +6412,8 @@ describe('NetworkController', () => { await waitForLookupNetworkToComplete({ controller, - operation: () => { - controller.rollbackToPreviousProvider(); + operation: async () => { + await controller.rollbackToPreviousProvider(); }, }); expect(controller.store.getState().provider).toStrictEqual({ @@ -6475,8 +6479,8 @@ describe('NetworkController', () => { const networkWillChange = await waitForPublishedEvents({ messenger: unrestrictedMessenger, eventType: NetworkControllerEventType.NetworkWillChange, - operation: () => { - controller.rollbackToPreviousProvider(); + operation: async () => { + await controller.rollbackToPreviousProvider(); }, }); @@ -6532,6 +6536,8 @@ describe('NetworkController', () => { // happens before networkDidChange count: 1, operation: () => { + // Intentionally not awaited because we want to check state + // while this operation is in-progress controller.rollbackToPreviousProvider(); }, }); @@ -6595,6 +6601,8 @@ describe('NetworkController', () => { // happens before networkDidChange count: 1, operation: () => { + // Intentionally not awaited because we want to check state + // while this operation is in-progress controller.rollbackToPreviousProvider(); }, }); @@ -6646,8 +6654,8 @@ describe('NetworkController', () => { await waitForLookupNetworkToComplete({ controller, - operation: () => { - controller.rollbackToPreviousProvider(); + operation: async () => { + await controller.rollbackToPreviousProvider(); }, }); @@ -6703,8 +6711,8 @@ describe('NetworkController', () => { controller.getProviderAndBlockTracker(); await waitForLookupNetworkToComplete({ controller, - operation: () => { - controller.rollbackToPreviousProvider(); + operation: async () => { + await controller.rollbackToPreviousProvider(); }, }); const { provider: providerAfter } = @@ -6759,8 +6767,8 @@ describe('NetworkController', () => { const networkDidChange = await waitForPublishedEvents({ messenger: unrestrictedMessenger, eventType: NetworkControllerEventType.NetworkDidChange, - operation: () => { - controller.rollbackToPreviousProvider(); + operation: async () => { + await controller.rollbackToPreviousProvider(); }, }); expect(networkDidChange).toBeTruthy(); @@ -6814,8 +6822,8 @@ describe('NetworkController', () => { const infuraIsUnblocked = await waitForPublishedEvents({ messenger: unrestrictedMessenger, eventType: NetworkControllerEventType.InfuraIsUnblocked, - operation: () => { - controller.rollbackToPreviousProvider(); + operation: async () => { + await controller.rollbackToPreviousProvider(); }, }); @@ -6871,8 +6879,8 @@ describe('NetworkController', () => { await waitForLookupNetworkToComplete({ controller, - operation: () => { - controller.rollbackToPreviousProvider(); + operation: async () => { + await controller.rollbackToPreviousProvider(); }, }); expect(controller.store.getState().networkStatus).toBe('unknown'); @@ -6925,8 +6933,8 @@ describe('NetworkController', () => { await waitForLookupNetworkToComplete({ controller, - operation: () => { - controller.rollbackToPreviousProvider(); + operation: async () => { + await controller.rollbackToPreviousProvider(); }, }); expect(controller.store.getState().networkDetails).toStrictEqual({ diff --git a/app/scripts/controllers/network/network-controller.ts b/app/scripts/controllers/network/network-controller.ts index ee0ea686c..eb98f8494 100644 --- a/app/scripts/controllers/network/network-controller.ts +++ b/app/scripts/controllers/network/network-controller.ts @@ -787,10 +787,10 @@ export class NetworkController extends EventEmitter { * different than the initial network (if it is, then this is equivalent to * calling `resetConnection`). */ - rollbackToPreviousProvider(): void { + async rollbackToPreviousProvider() { const config = this.#previousProviderConfig; this.providerStore.putState(config); - this._switchNetwork(config); + await this._switchNetwork(config); } /**