From 065c4997536d7a369ae6315634cd796de08af247 Mon Sep 17 00:00:00 2001 From: Garrett Bear Date: Wed, 12 Apr 2023 08:55:24 -0700 Subject: [PATCH 1/4] 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 12c922be646d..f030d53f6502 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 522de00977dd..90d1146c9d5f 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 33f95e3b4225..131b700f63be 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 760d420961fd..4c44d20ebdea 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 32ce255b3f8b..fac87c26fa25 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 6c6a5e7ec643..563feb982f05 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 0c4f8b4cbf02..e1e0a944b274 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 43602ba42af8..3c3b85cf029d 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 3368017e333a..de5a6a962a63 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 5e05df4ea18d..9ff7f9cdd09f 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 3885126ddacb..1888f32f1acb 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 2241da905ee8..e67525f7c4fd 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 8027e1e89ff4..0352039c4be0 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 5f9dcc28ef43..fa4d39464288 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 a86ad8312c23..d9afbca056c8 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 000000000000..1b4ae6a9390b --- /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 000000000000..3831cb838f45 --- /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 000000000000..d50d4cd85c1a --- /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 000000000000..8a527a095ec9 --- /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 138a420342f5..de770633f913 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 2428aedf6852..bf3ecc9e8c86 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 236c258a8163..d5ee5b88bed7 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 000000000000..466166377b43 --- /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 9b7ea11c10b9..547e374b5f2b 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 8ca56d49b9e9..c02ade945e78 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 b3523d2df125..0fdc619427e0 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 81631aec5a06..62a4c0c5d21a 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 95585a7282e2..8538d69cd2c6 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 8667a379c0a0..b34b34b6aad1 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 bab04c839b92..98385776d255 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 2c48d086ccf1..60785b18adc3 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 6d0a1ba8f5f9..6e62e0bfdcd0 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 a4569da92f6f..1463896b00b5 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 5498e279cf49..b5a46a683ca5 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 1a91543f8983..4d1f8dfbf9ca 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 86d2f63b7686..c525bd161e27 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 ac2491593f68..acc8e63f481b 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 c52332c89f3b..5e37225e1504 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 a074937a677a..26f0898ae9a8 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 b4c6ed2f1bca..8a28efe00b92 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 d87ab0c4e694..b9e007e86841 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 8959547feab5..91a088b3d83c 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 2/4] 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} | 2012 +++++++++-------- .../controllers/network/network-controller.ts | 33 +- jest.config.js | 2 + package.json | 1 + yarn.lock | 17 + 6 files changed, 1141 insertions(+), 925 deletions(-) rename app/scripts/controllers/network/{network-controller.test.js => network-controller.test.ts} (83%) diff --git a/.eslintrc.js b/.eslintrc.js index 6851d9566a5b..bcbfb44b7f8c 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 fb86440c47b9..5e848830a752 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. + * 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, method: string, params?: unknown[]}} MockJsonRpcRequestBody - * - * A partial form of a prototypical JSON-RPC request body. + * A composite form of a prototypical JSON-RPC response body. */ +type MockJsonRpcResponseBody = { + id?: number | string; + jsonrpc?: '2.0'; + result?: unknown; + error?: string | null; +}; /** - * @typedef {{id?: number; jsonrpc?: string; result?: string; error?: string}} MockJsonResponseBody - * - * A partial form of a prototypical JSON-RPC response body. + * 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. - * - * @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. + * The path used for all requests. Customized per network type. */ - constructor({ - networkClientType, - networkClientOptions: { - infuraNetwork, - infuraProjectId = DEFAULT_INFURA_PROJECT_ID, - customRpcUrl, - } = {}, - }) { - 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); - } + #requestPath: string; /** - * Constructs a new NetworkCommunications object using a different set of - * options, using the options from this instance as a base. + * 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 args - The same arguments that NetworkCommunications takes. + * @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. */ - with(args) { - return new NetworkCommunications({ - networkClientType: this.networkClientType, - networkClientOptions: this.#networkClientOptions, - ...args, - }); + constructor({ + options, + requestBaseUrl, + requestPath, + }: { + options: Options; + requestBaseUrl: string; + requestPath: string; + }) { + 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); - - 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 }, - }); +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); - try { - return await fn({ controller, network }); - } finally { - await controller.destroy(); + const providerType = givenNetworkControllerOptions.state.provider.type; + const network = new InfuraNetworkCommunications({ + infuraProjectId: constructorOptions.infuraProjectId, + infuraNetwork: providerType, + }); + + 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; - - const end = () => { - if (!alreadyEnded) { - alreadyEnded = true; - messenger.unsubscribe(eventType.toString(), eventListener); - Promise.resolve(beforeResolving()).then(() => { +}: { + 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; + + // 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); + 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 }, + )}`, + ); + } + }); + } } - }; - const resetTimer = () => { - stopTimer(); - timer = originalSetTimeout(() => { - end(); - }, timeBeforeAssumingNoMoreEvents); - }; - - eventListener = (...payload) => { - allEventPayloads.push(payload); + function stopTimer() { + if (timer) { + clearTimeout(timer); + } + } - if (isEventPayloadInteresting(payload)) { - interestingEventPayloads.push(payload); - if (interestingEventPayloads.length === expectedNumberOfEvents) { - stopTimer(); + function resetTimer() { + stopTimer(); + timer = originalSetTimeout(() => { end(); - } else { - resetTimer(); - } + }, timeBeforeAssumingNoMoreEvents); } - }; - messenger.subscribe(eventType.toString(), eventListener); - resetTimer(); - }); + 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 9249e0fa2b22..74bd39f42b62 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 21d41a96d670..86588a42ab1f 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 c5a718e753d8..3b6b5f057fe9 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 f31c8b4284c5..475744a264df 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 3/4] 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 3aff0170bf2f..622e888f9630 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 000000000000..cc5b61cc12e8 --- /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 000000000000..f4cb5e25ec3a --- /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 e04d70c099a7..1712ed1ee1dd 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 9791e0378e95..000000000000 --- 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 7315f301f574..133f5a877cd8 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 3b6b5f057fe9..ac58eca27183 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 86d8ffc6b07b..81145fa6053a 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 b4e04c7f611f..1b48a1e849f8 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 475744a264df..b2d8e4cee53e 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 4/4] [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 000000000000..60d85a469c90 --- /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 000000000000..5bf2f249703e --- /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 000000000000..76570844e86f --- /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 000000000000..5416daebaca0 --- /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 000000000000..259340edd0f9 --- /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 000000000000..04582c660f25 --- /dev/null +++ b/ui/components/institutional/compliance-modal/index.js @@ -0,0 +1 @@ +export { default } from './compliance-modal';