diff --git a/.circleci/config.yml b/.circleci/config.yml index b2c5ab712973..2bf244b9bf8a 100644 --- a/.circleci/config.yml +++ b/.circleci/config.yml @@ -1250,7 +1250,7 @@ jobs: command: mv ./builds-test-flask-mv2 ./builds - run: name: test:e2e:firefox:flask - command: ENABLE_MV3=false .circleci/scripts/test-run-e2e.sh yarn test:e2e:firefox:flask + command: .circleci/scripts/test-run-e2e.sh yarn test:e2e:firefox:flask no_output_timeout: 5m - store_artifacts: path: test-artifacts @@ -1393,7 +1393,7 @@ jobs: command: mv ./builds-test-mv2 ./builds - run: name: test:e2e:firefox - command: ENABLE_MV3=false .circleci/scripts/test-run-e2e.sh yarn test:e2e:firefox + command: .circleci/scripts/test-run-e2e.sh yarn test:e2e:firefox no_output_timeout: 5m - store_artifacts: path: test-artifacts diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml index 5d1b4d73bdab..f3cc68bebcec 100644 --- a/.github/workflows/main.yml +++ b/.github/workflows/main.yml @@ -32,21 +32,12 @@ jobs: name: Run tests uses: ./.github/workflows/run-tests.yml - sonarcloud: - name: SonarCloud - uses: ./.github/workflows/sonarcloud.yml - secrets: - SONAR_TOKEN: ${{ secrets.SONAR_TOKEN }} - needs: - - run-tests - all-jobs-completed: name: All jobs completed runs-on: ubuntu-latest needs: - check-workflows - run-tests - - sonarcloud outputs: PASSED: ${{ steps.set-output.outputs.PASSED }} steps: diff --git a/.github/workflows/sonarcloud.yml b/.github/workflows/sonarcloud.yml index 460d5c140462..9ca9f02e2ae5 100644 --- a/.github/workflows/sonarcloud.yml +++ b/.github/workflows/sonarcloud.yml @@ -1,19 +1,33 @@ +# This GitHub action will checkout and scan third party code. +# Please ensure that any changes to this action do not perform +# actions that may result in code from that branch being executed +# such as installing dependencies or running build scripts. + name: SonarCloud on: - workflow_call: - secrets: - SONAR_TOKEN: - required: true + workflow_run: + workflows: + - Run tests + types: + - completed + +permissions: + actions: read jobs: sonarcloud: + # Only scan code from non-forked repositories that have passed the tests + # This will skip scanning the code for forks, but will run for the main repository on PRs from forks + if: ${{ github.event.workflow_run.conclusion == 'success' && github.event.workflow_run.repository.fork == false }} name: SonarCloud runs-on: ubuntu-latest steps: - name: Checkout repository uses: actions/checkout@v4 with: + repository: ${{ github.event.workflow_run.head_repository.full_name }} # Use the repository that triggered the workflow + ref: ${{ github.event.workflow_run.head_branch }} # Use the branch that triggered the workflow fetch-depth: 0 # Shallow clones should be disabled for better relevancy of analysis - name: Download artifacts @@ -21,6 +35,20 @@ jobs: with: name: lcov.info path: coverage + github-token: ${{ github.token }} # This is required when downloading artifacts from a different repository or from a different workflow run. + run-id: ${{ github.event.workflow_run.id }} # Use the workflow id that triggered the workflow + + - name: Download sonar-project.properties + env: + GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} + REPOSITORY: MetaMask/metamask-extension + run: | + sonar_project_properties=$(gh api -H "Accept: application/vnd.github.raw" "repos/$REPOSITORY/contents/sonar-project.properties") + if [ -z "$sonar_project_properties" ]; then + echo "::error::sonar-project.properties not found in $REPOSITORY. Please make sure this file exists on the default branch." + exit 1 + fi + echo "$sonar_project_properties" > sonar-project.properties - name: SonarCloud Scan # This is SonarSource/sonarcloud-github-action@v2.0.0 diff --git a/.yarn/patches/@metamask-snaps-utils-npm-8.1.1-7d5dd6a26a.patch b/.yarn/patches/@metamask-snaps-utils-npm-8.1.1-7d5dd6a26a.patch deleted file mode 100644 index 3361025d4860..000000000000 --- a/.yarn/patches/@metamask-snaps-utils-npm-8.1.1-7d5dd6a26a.patch +++ /dev/null @@ -1,120 +0,0 @@ -diff --git a/dist/ui.cjs b/dist/ui.cjs -index 300fe9e97bba85945e3c2d200e736987453f8268..d6fa322e2b3629f41d653b91db52c3db85064276 100644 ---- a/dist/ui.cjs -+++ b/dist/ui.cjs -@@ -200,13 +200,23 @@ function getMarkdownLinks(text) { - * @param link - The link to validate. - * @param isOnPhishingList - The function that checks the link against the - * phishing list. -+ * @throws If the link is invalid. - */ - function validateLink(link, isOnPhishingList) { - try { - const url = new URL(link); - (0, utils_1.assert)(ALLOWED_PROTOCOLS.includes(url.protocol), `Protocol must be one of: ${ALLOWED_PROTOCOLS.join(', ')}.`); -- const hostname = url.protocol === 'mailto:' ? url.pathname.split('@')[1] : url.hostname; -- (0, utils_1.assert)(!isOnPhishingList(hostname), 'The specified URL is not allowed.'); -+ if (url.protocol === 'mailto:') { -+ const emails = url.pathname.split(','); -+ for (const email of emails) { -+ const hostname = email.split('@')[1]; -+ (0, utils_1.assert)(!hostname.includes(':')); -+ const href = `https://${hostname}`; -+ (0, utils_1.assert)(!isOnPhishingList(href), 'The specified URL is not allowed.'); -+ } -+ return; -+ } -+ (0, utils_1.assert)(!isOnPhishingList(url.href), 'The specified URL is not allowed.'); - } - catch (error) { - throw new Error(`Invalid URL: ${error?.code === 'ERR_ASSERTION' ? error.message : 'Unable to parse URL.'}`); -diff --git a/dist/ui.cjs.map b/dist/ui.cjs.map -index 71b5ecb9eb8bc8bdf919daccf24b25737ee69819..6d6e56cd7fea85e4d477c0399506a03d465ca740 100644 ---- a/dist/ui.cjs.map -+++ b/dist/ui.cjs.map -@@ -1 +1 @@ --{"version":3,"file":"ui.cjs","sourceRoot":"","sources":["../src/ui.tsx"],"names":[],"mappings":"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","sourcesContent":["import type { Component } from '@metamask/snaps-sdk';\nimport { NodeType } from '@metamask/snaps-sdk';\nimport type {\n BoldChildren,\n GenericSnapElement,\n ItalicChildren,\n JSXElement,\n LinkElement,\n Nestable,\n RowChildren,\n SnapNode,\n StandardFormattingElement,\n TextChildren,\n} from '@metamask/snaps-sdk/jsx';\nimport {\n Italic,\n Link,\n Bold,\n Row,\n Text,\n Field,\n Image,\n Input,\n Heading,\n Form,\n Divider,\n Spinner,\n Copyable,\n Box,\n Button,\n Address,\n} from '@metamask/snaps-sdk/jsx';\nimport {\n assert,\n assertExhaustive,\n hasProperty,\n isPlainObject,\n} from '@metamask/utils';\nimport { lexer, walkTokens } from 'marked';\nimport type { Token, Tokens } from 'marked';\n\nconst MAX_TEXT_LENGTH = 50_000; // 50 kb\nconst ALLOWED_PROTOCOLS = ['https:', 'mailto:'];\n\n/**\n * Get the button variant from a legacy button component variant.\n *\n * @param variant - The legacy button component variant.\n * @returns The button variant.\n */\nfunction getButtonVariant(variant?: 'primary' | 'secondary' | undefined) {\n switch (variant) {\n case 'primary':\n return 'primary';\n case 'secondary':\n return 'destructive';\n default:\n return undefined;\n }\n}\n\n/**\n * Get the children of a JSX element. If there is only one child, the child is\n * returned directly. Otherwise, the children are returned as an array.\n *\n * @param elements - The JSX elements.\n * @returns The child or children.\n */\nfunction getChildren(elements: Type[]) {\n if (elements.length === 1) {\n return elements[0];\n }\n\n return elements;\n}\n\n/**\n * Get the text of a link token.\n *\n * @param token - The link token.\n * @returns The text of the link token.\n */\nfunction getLinkText(token: Tokens.Link | Tokens.Generic) {\n if (token.tokens && token.tokens.length > 0) {\n return getChildren(token.tokens.flatMap(getTextChildFromToken));\n }\n\n return token.href;\n}\n\n/**\n * Get the text child from a list of markdown tokens.\n *\n * @param tokens - The markdown tokens.\n * @returns The text child.\n */\nfunction getTextChildFromTokens(tokens: Token[]) {\n return getChildren(tokens.flatMap(getTextChildFromToken));\n}\n\n/**\n * Get the text child from a markdown token.\n *\n * @param token - The markdown token.\n * @returns The text child.\n */\nfunction getTextChildFromToken(token: Token): TextChildren {\n switch (token.type) {\n case 'link': {\n return ;\n }\n\n case 'text':\n return token.text;\n\n case 'strong':\n return (\n \n {\n getTextChildFromTokens(\n // Due to the way `marked` is typed, `token.tokens` can be\n // `undefined`, but it's a required field of `Tokens.Bold`, so we\n // can safely cast it to `Token[]`.\n token.tokens as Token[],\n ) as BoldChildren\n }\n \n );\n\n case 'em':\n return (\n \n {\n getTextChildFromTokens(\n // Due to the way `marked` is typed, `token.tokens` can be\n // `undefined`, but it's a required field of `Tokens.Bold`, so we\n // can safely cast it to `Token[]`.\n token.tokens as Token[],\n ) as ItalicChildren\n }\n \n );\n\n default:\n return null;\n }\n}\n\n/**\n * Get all text children from a markdown string.\n *\n * @param value - The markdown string.\n * @returns The text children.\n */\nexport function getTextChildren(\n value: string,\n): (string | StandardFormattingElement | LinkElement)[] {\n const rootTokens = lexer(value, { gfm: false });\n const children: (string | StandardFormattingElement | LinkElement | null)[] =\n [];\n\n walkTokens(rootTokens, (token) => {\n if (token.type === 'paragraph') {\n if (children.length > 0) {\n children.push('\\n\\n');\n }\n\n const { tokens } = token as Tokens.Paragraph;\n // We do not need to consider nesting deeper than 1 level here and we can therefore cast.\n children.push(\n ...(tokens.flatMap(getTextChildFromToken) as (\n | string\n | StandardFormattingElement\n | LinkElement\n | null\n )[]),\n );\n }\n });\n\n // eslint-disable-next-line @typescript-eslint/no-unnecessary-type-assertion\n return children.filter((child) => child !== null) as (\n | string\n | StandardFormattingElement\n | LinkElement\n )[];\n}\n\n/**\n * Validate the text size of a component. The text size is the total length of\n * all text in the component.\n *\n * @param component - The component to validate.\n * @throws An error if the text size exceeds the maximum allowed size.\n */\nfunction validateComponentTextSize(component: Component) {\n const textSize = getTotalTextLength(component);\n assert(\n textSize <= MAX_TEXT_LENGTH,\n `The text in a Snap UI may not be larger than ${\n MAX_TEXT_LENGTH / 1000\n } kB.`,\n );\n}\n\n/**\n * Get a JSX element from a legacy UI component. This supports all legacy UI\n * components, and maps them to their JSX equivalents where possible.\n *\n * This function validates the text size of the component, but does not validate\n * the total size. The total size of the component should be validated before\n * calling this function.\n *\n * @param legacyComponent - The legacy UI component.\n * @returns The JSX element.\n */\nexport function getJsxElementFromComponent(\n legacyComponent: Component,\n): JSXElement {\n validateComponentTextSize(legacyComponent);\n\n /**\n * Get the JSX element for a component. This function is recursive and will\n * call itself for child components.\n *\n * @param component - The component to convert to a JSX element.\n * @returns The JSX element.\n */\n function getElement(component: Component) {\n switch (component.type) {\n case NodeType.Address:\n return
;\n\n case NodeType.Button:\n return (\n \n {component.value}\n \n );\n\n case NodeType.Copyable:\n return (\n \n );\n\n case NodeType.Divider:\n return ;\n\n case NodeType.Form:\n return (\n
\n {getChildren(component.children.map(getElement))}\n
\n );\n\n case NodeType.Heading:\n return ;\n\n case NodeType.Image:\n // `Image` supports `alt`, but the legacy `Image` component does not.\n return ;\n\n case NodeType.Input:\n return (\n \n \n \n );\n\n case NodeType.Panel:\n // `Panel` is renamed to `Box` in JSX.\n return (\n \n );\n\n case NodeType.Row:\n return (\n \n {getElement(component.value) as RowChildren}\n \n );\n\n case NodeType.Spinner:\n return ;\n\n case NodeType.Text:\n return {getChildren(getTextChildren(component.value))};\n\n /* istanbul ignore next 2 */\n default:\n return assertExhaustive(component);\n }\n }\n\n return getElement(legacyComponent);\n}\n\n/**\n * Extract all links from a Markdown text string using the `marked` lexer.\n *\n * @param text - The markdown text string.\n * @returns A list of URLs linked to in the string.\n */\nfunction getMarkdownLinks(text: string) {\n const tokens = lexer(text, { gfm: false });\n const links: Tokens.Link[] = [];\n\n // Walk the lexed tokens and collect all link tokens\n walkTokens(tokens, (token) => {\n if (token.type === 'link') {\n links.push(token as Tokens.Link);\n }\n });\n\n return links;\n}\n\n/**\n * Validate a link against the phishing list.\n *\n * @param link - The link to validate.\n * @param isOnPhishingList - The function that checks the link against the\n * phishing list.\n */\nexport function validateLink(\n link: string,\n isOnPhishingList: (url: string) => boolean,\n) {\n try {\n const url = new URL(link);\n assert(\n ALLOWED_PROTOCOLS.includes(url.protocol),\n `Protocol must be one of: ${ALLOWED_PROTOCOLS.join(', ')}.`,\n );\n\n const hostname =\n url.protocol === 'mailto:' ? url.pathname.split('@')[1] : url.hostname;\n\n assert(!isOnPhishingList(hostname), 'The specified URL is not allowed.');\n } catch (error) {\n throw new Error(\n `Invalid URL: ${\n error?.code === 'ERR_ASSERTION' ? error.message : 'Unable to parse URL.'\n }`,\n );\n }\n}\n\n/**\n * Search for Markdown links in a string and checks them against the phishing\n * list.\n *\n * @param text - The text to verify.\n * @param isOnPhishingList - The function that checks the link against the\n * phishing list.\n * @throws If the text contains a link that is not allowed.\n */\nexport function validateTextLinks(\n text: string,\n isOnPhishingList: (url: string) => boolean,\n) {\n const links = getMarkdownLinks(text);\n\n for (const link of links) {\n validateLink(link.href, isOnPhishingList);\n }\n}\n\n/**\n * Walk a JSX tree and validate each {@link LinkElement} node against the\n * phishing list.\n *\n * @param node - The JSX node to walk.\n * @param isOnPhishingList - The function that checks the link against the\n * phishing list.\n */\nexport function validateJsxLinks(\n node: JSXElement,\n isOnPhishingList: (url: string) => boolean,\n) {\n walkJsx(node, (childNode) => {\n if (childNode.type !== 'Link') {\n return;\n }\n\n validateLink(childNode.props.href, isOnPhishingList);\n });\n}\n\n/**\n * Calculate the total length of all text in the component.\n *\n * @param component - A custom UI component.\n * @returns The total length of all text components in the component.\n */\nexport function getTotalTextLength(component: Component): number {\n const { type } = component;\n\n switch (type) {\n case NodeType.Panel:\n return component.children.reduce(\n // This is a bug in TypeScript: https://github.com/microsoft/TypeScript/issues/48313\n // eslint-disable-next-line @typescript-eslint/restrict-plus-operands\n (sum, node) => sum + getTotalTextLength(node),\n 0,\n );\n\n case NodeType.Row:\n return getTotalTextLength(component.value);\n\n case NodeType.Text:\n return component.value.length;\n\n default:\n return 0;\n }\n}\n\n/**\n * Check if a JSX element has children.\n *\n * @param element - A JSX element.\n * @returns `true` if the element has children, `false` otherwise.\n */\nexport function hasChildren(\n element: Element,\n): element is Element & {\n props: { children: Nestable };\n} {\n return hasProperty(element.props, 'children');\n}\n\n/**\n * Filter a JSX child to remove `null`, `undefined`, plain booleans, and empty\n * strings.\n *\n * @param child - The JSX child to filter.\n * @returns `true` if the child is not `null`, `undefined`, a plain boolean, or\n * an empty string, `false` otherwise.\n */\nfunction filterJsxChild(child: JSXElement | string | boolean | null): boolean {\n return Boolean(child) && child !== true;\n}\n\n/**\n * Get the children of a JSX element as an array. If the element has only one\n * child, the child is returned as an array.\n *\n * @param element - A JSX element.\n * @returns The children of the element.\n */\nexport function getJsxChildren(element: JSXElement): (JSXElement | string)[] {\n if (hasChildren(element)) {\n if (Array.isArray(element.props.children)) {\n // @ts-expect-error - Each member of the union type has signatures, but\n // none of those signatures are compatible with each other.\n return element.props.children.filter(filterJsxChild).flat(Infinity);\n }\n\n if (element.props.children) {\n return [element.props.children];\n }\n }\n\n return [];\n}\n\n/**\n * Walk a JSX tree and call a callback on each node.\n *\n * @param node - The JSX node to walk.\n * @param callback - The callback to call on each node.\n * @param depth - The current depth in the JSX tree for a walk.\n * @returns The result of the callback, if any.\n */\nexport function walkJsx(\n node: JSXElement | JSXElement[],\n callback: (node: JSXElement, depth: number) => Value | undefined,\n depth = 0,\n): Value | undefined {\n if (Array.isArray(node)) {\n for (const child of node) {\n const childResult = walkJsx(child as JSXElement, callback, depth);\n if (childResult !== undefined) {\n return childResult;\n }\n }\n\n return undefined;\n }\n\n const result = callback(node, depth);\n if (result !== undefined) {\n return result;\n }\n\n if (\n hasProperty(node, 'props') &&\n isPlainObject(node.props) &&\n hasProperty(node.props, 'children')\n ) {\n const children = getJsxChildren(node);\n for (const child of children) {\n if (isPlainObject(child)) {\n const childResult = walkJsx(child, callback, depth + 1);\n if (childResult !== undefined) {\n return childResult;\n }\n }\n }\n }\n\n return undefined;\n}\n\n/**\n * Serialise a JSX prop to a string.\n *\n * @param prop - The JSX prop.\n * @returns The serialised JSX prop.\n */\nfunction serialiseProp(prop: unknown): string {\n if (typeof prop === 'string') {\n return `\"${prop}\"`;\n }\n\n return `{${JSON.stringify(prop)}}`;\n}\n\n/**\n * Serialise JSX props to a string.\n *\n * @param props - The JSX props.\n * @returns The serialised JSX props.\n */\nfunction serialiseProps(props: Record): string {\n return Object.entries(props)\n .filter(([key]) => key !== 'children')\n .sort(([a], [b]) => a.localeCompare(b))\n .map(([key, value]) => ` ${key}=${serialiseProp(value)}`)\n .join('');\n}\n\n/**\n * Serialise a JSX node to a string.\n *\n * @param node - The JSX node.\n * @param indentation - The indentation level. Defaults to `0`. This should not\n * be set by the caller, as it is used for recursion.\n * @returns The serialised JSX node.\n */\nexport function serialiseJsx(node: SnapNode, indentation = 0): string {\n if (Array.isArray(node)) {\n return node.map((child) => serialiseJsx(child, indentation)).join('');\n }\n\n const indent = ' '.repeat(indentation);\n if (typeof node === 'string') {\n return `${indent}${node}\\n`;\n }\n\n if (!node) {\n return '';\n }\n\n const { type, props } = node as GenericSnapElement;\n const trailingNewline = indentation > 0 ? '\\n' : '';\n\n if (hasProperty(props, 'children')) {\n const children = serialiseJsx(props.children as SnapNode, indentation + 1);\n return `${indent}<${type}${serialiseProps(\n props,\n )}>\\n${children}${indent}${trailingNewline}`;\n }\n\n return `${indent}<${type}${serialiseProps(props)} />${trailingNewline}`;\n}\n"]} -\ No newline at end of file -+{"version":3,"file":"ui.cjs","sourceRoot":"","sources":["../src/ui.tsx"],"names":[],"mappings":"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","sourcesContent":["import type { Component } from '@metamask/snaps-sdk';\nimport { NodeType } from '@metamask/snaps-sdk';\nimport type {\n BoldChildren,\n GenericSnapElement,\n ItalicChildren,\n JSXElement,\n LinkElement,\n Nestable,\n RowChildren,\n SnapNode,\n StandardFormattingElement,\n TextChildren,\n} from '@metamask/snaps-sdk/jsx';\nimport {\n Italic,\n Link,\n Bold,\n Row,\n Text,\n Field,\n Image,\n Input,\n Heading,\n Form,\n Divider,\n Spinner,\n Copyable,\n Box,\n Button,\n Address,\n} from '@metamask/snaps-sdk/jsx';\nimport {\n assert,\n assertExhaustive,\n hasProperty,\n isPlainObject,\n} from '@metamask/utils';\nimport { lexer, walkTokens } from 'marked';\nimport type { Token, Tokens } from 'marked';\n\nconst MAX_TEXT_LENGTH = 50_000; // 50 kb\nconst ALLOWED_PROTOCOLS = ['https:', 'mailto:'];\n\n/**\n * Get the button variant from a legacy button component variant.\n *\n * @param variant - The legacy button component variant.\n * @returns The button variant.\n */\nfunction getButtonVariant(variant?: 'primary' | 'secondary' | undefined) {\n switch (variant) {\n case 'primary':\n return 'primary';\n case 'secondary':\n return 'destructive';\n default:\n return undefined;\n }\n}\n\n/**\n * Get the children of a JSX element. If there is only one child, the child is\n * returned directly. Otherwise, the children are returned as an array.\n *\n * @param elements - The JSX elements.\n * @returns The child or children.\n */\nfunction getChildren(elements: Type[]) {\n if (elements.length === 1) {\n return elements[0];\n }\n\n return elements;\n}\n\n/**\n * Get the text of a link token.\n *\n * @param token - The link token.\n * @returns The text of the link token.\n */\nfunction getLinkText(token: Tokens.Link | Tokens.Generic) {\n if (token.tokens && token.tokens.length > 0) {\n return getChildren(token.tokens.flatMap(getTextChildFromToken));\n }\n\n return token.href;\n}\n\n/**\n * Get the text child from a list of markdown tokens.\n *\n * @param tokens - The markdown tokens.\n * @returns The text child.\n */\nfunction getTextChildFromTokens(tokens: Token[]) {\n return getChildren(tokens.flatMap(getTextChildFromToken));\n}\n\n/**\n * Get the text child from a markdown token.\n *\n * @param token - The markdown token.\n * @returns The text child.\n */\nfunction getTextChildFromToken(token: Token): TextChildren {\n switch (token.type) {\n case 'link': {\n return ;\n }\n\n case 'text':\n return token.text;\n\n case 'strong':\n return (\n \n {\n getTextChildFromTokens(\n // Due to the way `marked` is typed, `token.tokens` can be\n // `undefined`, but it's a required field of `Tokens.Bold`, so we\n // can safely cast it to `Token[]`.\n token.tokens as Token[],\n ) as BoldChildren\n }\n \n );\n\n case 'em':\n return (\n \n {\n getTextChildFromTokens(\n // Due to the way `marked` is typed, `token.tokens` can be\n // `undefined`, but it's a required field of `Tokens.Bold`, so we\n // can safely cast it to `Token[]`.\n token.tokens as Token[],\n ) as ItalicChildren\n }\n \n );\n\n default:\n return null;\n }\n}\n\n/**\n * Get all text children from a markdown string.\n *\n * @param value - The markdown string.\n * @returns The text children.\n */\nexport function getTextChildren(\n value: string,\n): (string | StandardFormattingElement | LinkElement)[] {\n const rootTokens = lexer(value, { gfm: false });\n const children: (string | StandardFormattingElement | LinkElement | null)[] =\n [];\n\n walkTokens(rootTokens, (token) => {\n if (token.type === 'paragraph') {\n if (children.length > 0) {\n children.push('\\n\\n');\n }\n\n const { tokens } = token as Tokens.Paragraph;\n // We do not need to consider nesting deeper than 1 level here and we can therefore cast.\n children.push(\n ...(tokens.flatMap(getTextChildFromToken) as (\n | string\n | StandardFormattingElement\n | LinkElement\n | null\n )[]),\n );\n }\n });\n\n // eslint-disable-next-line @typescript-eslint/no-unnecessary-type-assertion\n return children.filter((child) => child !== null) as (\n | string\n | StandardFormattingElement\n | LinkElement\n )[];\n}\n\n/**\n * Validate the text size of a component. The text size is the total length of\n * all text in the component.\n *\n * @param component - The component to validate.\n * @throws An error if the text size exceeds the maximum allowed size.\n */\nfunction validateComponentTextSize(component: Component) {\n const textSize = getTotalTextLength(component);\n assert(\n textSize <= MAX_TEXT_LENGTH,\n `The text in a Snap UI may not be larger than ${\n MAX_TEXT_LENGTH / 1000\n } kB.`,\n );\n}\n\n/**\n * Get a JSX element from a legacy UI component. This supports all legacy UI\n * components, and maps them to their JSX equivalents where possible.\n *\n * This function validates the text size of the component, but does not validate\n * the total size. The total size of the component should be validated before\n * calling this function.\n *\n * @param legacyComponent - The legacy UI component.\n * @returns The JSX element.\n */\nexport function getJsxElementFromComponent(\n legacyComponent: Component,\n): JSXElement {\n validateComponentTextSize(legacyComponent);\n\n /**\n * Get the JSX element for a component. This function is recursive and will\n * call itself for child components.\n *\n * @param component - The component to convert to a JSX element.\n * @returns The JSX element.\n */\n function getElement(component: Component) {\n switch (component.type) {\n case NodeType.Address:\n return
;\n\n case NodeType.Button:\n return (\n \n {component.value}\n \n );\n\n case NodeType.Copyable:\n return (\n \n );\n\n case NodeType.Divider:\n return ;\n\n case NodeType.Form:\n return (\n
\n {getChildren(component.children.map(getElement))}\n
\n );\n\n case NodeType.Heading:\n return ;\n\n case NodeType.Image:\n // `Image` supports `alt`, but the legacy `Image` component does not.\n return ;\n\n case NodeType.Input:\n return (\n \n \n \n );\n\n case NodeType.Panel:\n // `Panel` is renamed to `Box` in JSX.\n return (\n \n );\n\n case NodeType.Row:\n return (\n \n {getElement(component.value) as RowChildren}\n \n );\n\n case NodeType.Spinner:\n return ;\n\n case NodeType.Text:\n return {getChildren(getTextChildren(component.value))};\n\n /* istanbul ignore next 2 */\n default:\n return assertExhaustive(component);\n }\n }\n\n return getElement(legacyComponent);\n}\n\n/**\n * Extract all links from a Markdown text string using the `marked` lexer.\n *\n * @param text - The markdown text string.\n * @returns A list of URLs linked to in the string.\n */\nfunction getMarkdownLinks(text: string) {\n const tokens = lexer(text, { gfm: false });\n const links: Tokens.Link[] = [];\n\n // Walk the lexed tokens and collect all link tokens\n walkTokens(tokens, (token) => {\n if (token.type === 'link') {\n links.push(token as Tokens.Link);\n }\n });\n\n return links;\n}\n\n/**\n * Validate a link against the phishing list.\n *\n * @param link - The link to validate.\n * @param isOnPhishingList - The function that checks the link against the\n * phishing list.\n * @throws If the link is invalid.\n */\nexport function validateLink(\n link: string,\n isOnPhishingList: (url: string) => boolean,\n) {\n try {\n const url = new URL(link);\n assert(\n ALLOWED_PROTOCOLS.includes(url.protocol),\n `Protocol must be one of: ${ALLOWED_PROTOCOLS.join(', ')}.`,\n );\n\n if (url.protocol === 'mailto:') {\n const emails = url.pathname.split(',');\n for (const email of emails) {\n const hostname = email.split('@')[1];\n assert(!hostname.includes(':'));\n const href = `https://${hostname}`;\n assert(!isOnPhishingList(href), 'The specified URL is not allowed.');\n }\n\n return;\n }\n\n assert(!isOnPhishingList(url.href), 'The specified URL is not allowed.');\n } catch (error) {\n throw new Error(\n `Invalid URL: ${\n error?.code === 'ERR_ASSERTION' ? error.message : 'Unable to parse URL.'\n }`,\n );\n }\n}\n\n/**\n * Search for Markdown links in a string and checks them against the phishing\n * list.\n *\n * @param text - The text to verify.\n * @param isOnPhishingList - The function that checks the link against the\n * phishing list.\n * @throws If the text contains a link that is not allowed.\n */\nexport function validateTextLinks(\n text: string,\n isOnPhishingList: (url: string) => boolean,\n) {\n const links = getMarkdownLinks(text);\n\n for (const link of links) {\n validateLink(link.href, isOnPhishingList);\n }\n}\n\n/**\n * Walk a JSX tree and validate each {@link LinkElement} node against the\n * phishing list.\n *\n * @param node - The JSX node to walk.\n * @param isOnPhishingList - The function that checks the link against the\n * phishing list.\n */\nexport function validateJsxLinks(\n node: JSXElement,\n isOnPhishingList: (url: string) => boolean,\n) {\n walkJsx(node, (childNode) => {\n if (childNode.type !== 'Link') {\n return;\n }\n\n validateLink(childNode.props.href, isOnPhishingList);\n });\n}\n\n/**\n * Calculate the total length of all text in the component.\n *\n * @param component - A custom UI component.\n * @returns The total length of all text components in the component.\n */\nexport function getTotalTextLength(component: Component): number {\n const { type } = component;\n\n switch (type) {\n case NodeType.Panel:\n return component.children.reduce(\n // This is a bug in TypeScript: https://github.com/microsoft/TypeScript/issues/48313\n // eslint-disable-next-line @typescript-eslint/restrict-plus-operands\n (sum, node) => sum + getTotalTextLength(node),\n 0,\n );\n\n case NodeType.Row:\n return getTotalTextLength(component.value);\n\n case NodeType.Text:\n return component.value.length;\n\n default:\n return 0;\n }\n}\n\n/**\n * Check if a JSX element has children.\n *\n * @param element - A JSX element.\n * @returns `true` if the element has children, `false` otherwise.\n */\nexport function hasChildren(\n element: Element,\n): element is Element & {\n props: { children: Nestable };\n} {\n return hasProperty(element.props, 'children');\n}\n\n/**\n * Filter a JSX child to remove `null`, `undefined`, plain booleans, and empty\n * strings.\n *\n * @param child - The JSX child to filter.\n * @returns `true` if the child is not `null`, `undefined`, a plain boolean, or\n * an empty string, `false` otherwise.\n */\nfunction filterJsxChild(child: JSXElement | string | boolean | null): boolean {\n return Boolean(child) && child !== true;\n}\n\n/**\n * Get the children of a JSX element as an array. If the element has only one\n * child, the child is returned as an array.\n *\n * @param element - A JSX element.\n * @returns The children of the element.\n */\nexport function getJsxChildren(element: JSXElement): (JSXElement | string)[] {\n if (hasChildren(element)) {\n if (Array.isArray(element.props.children)) {\n // @ts-expect-error - Each member of the union type has signatures, but\n // none of those signatures are compatible with each other.\n return element.props.children.filter(filterJsxChild).flat(Infinity);\n }\n\n if (element.props.children) {\n return [element.props.children];\n }\n }\n\n return [];\n}\n\n/**\n * Walk a JSX tree and call a callback on each node.\n *\n * @param node - The JSX node to walk.\n * @param callback - The callback to call on each node.\n * @param depth - The current depth in the JSX tree for a walk.\n * @returns The result of the callback, if any.\n */\nexport function walkJsx(\n node: JSXElement | JSXElement[],\n callback: (node: JSXElement, depth: number) => Value | undefined,\n depth = 0,\n): Value | undefined {\n if (Array.isArray(node)) {\n for (const child of node) {\n const childResult = walkJsx(child as JSXElement, callback, depth);\n if (childResult !== undefined) {\n return childResult;\n }\n }\n\n return undefined;\n }\n\n const result = callback(node, depth);\n if (result !== undefined) {\n return result;\n }\n\n if (\n hasProperty(node, 'props') &&\n isPlainObject(node.props) &&\n hasProperty(node.props, 'children')\n ) {\n const children = getJsxChildren(node);\n for (const child of children) {\n if (isPlainObject(child)) {\n const childResult = walkJsx(child, callback, depth + 1);\n if (childResult !== undefined) {\n return childResult;\n }\n }\n }\n }\n\n return undefined;\n}\n\n/**\n * Serialise a JSX prop to a string.\n *\n * @param prop - The JSX prop.\n * @returns The serialised JSX prop.\n */\nfunction serialiseProp(prop: unknown): string {\n if (typeof prop === 'string') {\n return `\"${prop}\"`;\n }\n\n return `{${JSON.stringify(prop)}}`;\n}\n\n/**\n * Serialise JSX props to a string.\n *\n * @param props - The JSX props.\n * @returns The serialised JSX props.\n */\nfunction serialiseProps(props: Record): string {\n return Object.entries(props)\n .filter(([key]) => key !== 'children')\n .sort(([a], [b]) => a.localeCompare(b))\n .map(([key, value]) => ` ${key}=${serialiseProp(value)}`)\n .join('');\n}\n\n/**\n * Serialise a JSX node to a string.\n *\n * @param node - The JSX node.\n * @param indentation - The indentation level. Defaults to `0`. This should not\n * be set by the caller, as it is used for recursion.\n * @returns The serialised JSX node.\n */\nexport function serialiseJsx(node: SnapNode, indentation = 0): string {\n if (Array.isArray(node)) {\n return node.map((child) => serialiseJsx(child, indentation)).join('');\n }\n\n const indent = ' '.repeat(indentation);\n if (typeof node === 'string') {\n return `${indent}${node}\\n`;\n }\n\n if (!node) {\n return '';\n }\n\n const { type, props } = node as GenericSnapElement;\n const trailingNewline = indentation > 0 ? '\\n' : '';\n\n if (hasProperty(props, 'children')) {\n const children = serialiseJsx(props.children as SnapNode, indentation + 1);\n return `${indent}<${type}${serialiseProps(\n props,\n )}>\\n${children}${indent}${trailingNewline}`;\n }\n\n return `${indent}<${type}${serialiseProps(props)} />${trailingNewline}`;\n}\n"]} -\ No newline at end of file -diff --git a/dist/ui.d.cts b/dist/ui.d.cts -index c9bd215bf861b83df1d9b63acd586d71a37d896f..b7e6a58104694f96ac1f1608492fe71182a1c15f 100644 ---- a/dist/ui.d.cts -+++ b/dist/ui.d.cts -@@ -25,6 +25,7 @@ export declare function getJsxElementFromComponent(legacyComponent: Component): - * @param link - The link to validate. - * @param isOnPhishingList - The function that checks the link against the - * phishing list. -+ * @throws If the link is invalid. - */ - export declare function validateLink(link: string, isOnPhishingList: (url: string) => boolean): void; - /** -diff --git a/dist/ui.d.cts.map b/dist/ui.d.cts.map -index 7c6a6f95c8aa97d0e048e32d4f76c46a0cd7bd15..66fa95b636d7dc2e8d467e129dccc410b9b27b8a 100644 ---- a/dist/ui.d.cts.map -+++ b/dist/ui.d.cts.map -@@ -1 +1 @@ --{"version":3,"file":"ui.d.cts","sourceRoot":"","sources":["../src/ui.tsx"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,SAAS,EAAE,4BAA4B;AAErD,OAAO,KAAK,EAIV,UAAU,EACV,WAAW,EACX,QAAQ,EAER,QAAQ,EACR,yBAAyB,EAE1B,gCAAgC;AAuIjC;;;;;GAKG;AACH,wBAAgB,eAAe,CAC7B,KAAK,EAAE,MAAM,GACZ,CAAC,MAAM,GAAG,yBAAyB,GAAG,WAAW,CAAC,EAAE,CA8BtD;AAmBD;;;;;;;;;;GAUG;AACH,wBAAgB,0BAA0B,CACxC,eAAe,EAAE,SAAS,GACzB,UAAU,CAsFZ;AAsBD;;;;;;GAMG;AACH,wBAAgB,YAAY,CAC1B,IAAI,EAAE,MAAM,EACZ,gBAAgB,EAAE,CAAC,GAAG,EAAE,MAAM,KAAK,OAAO,QAoB3C;AAED;;;;;;;;GAQG;AACH,wBAAgB,iBAAiB,CAC/B,IAAI,EAAE,MAAM,EACZ,gBAAgB,EAAE,CAAC,GAAG,EAAE,MAAM,KAAK,OAAO,QAO3C;AAED;;;;;;;GAOG;AACH,wBAAgB,gBAAgB,CAC9B,IAAI,EAAE,UAAU,EAChB,gBAAgB,EAAE,CAAC,GAAG,EAAE,MAAM,KAAK,OAAO,QAS3C;AAED;;;;;GAKG;AACH,wBAAgB,kBAAkB,CAAC,SAAS,EAAE,SAAS,GAAG,MAAM,CAqB/D;AAED;;;;;GAKG;AACH,wBAAgB,WAAW,CAAC,OAAO,SAAS,UAAU,EACpD,OAAO,EAAE,OAAO,GACf,OAAO,IAAI,OAAO,GAAG;IACtB,KAAK,EAAE;QAAE,QAAQ,EAAE,QAAQ,CAAC,UAAU,GAAG,MAAM,CAAC,CAAA;KAAE,CAAC;CACpD,CAEA;AAcD;;;;;;GAMG;AACH,wBAAgB,cAAc,CAAC,OAAO,EAAE,UAAU,GAAG,CAAC,UAAU,GAAG,MAAM,CAAC,EAAE,CAc3E;AAED;;;;;;;GAOG;AACH,wBAAgB,OAAO,CAAC,KAAK,EAC3B,IAAI,EAAE,UAAU,GAAG,UAAU,EAAE,EAC/B,QAAQ,EAAE,CAAC,IAAI,EAAE,UAAU,EAAE,KAAK,EAAE,MAAM,KAAK,KAAK,GAAG,SAAS,EAChE,KAAK,SAAI,GACR,KAAK,GAAG,SAAS,CAkCnB;AA8BD;;;;;;;GAOG;AACH,wBAAgB,YAAY,CAAC,IAAI,EAAE,QAAQ,EAAE,WAAW,SAAI,GAAG,MAAM,CAyBpE"} -\ No newline at end of file -+{"version":3,"file":"ui.d.cts","sourceRoot":"","sources":["../src/ui.tsx"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,SAAS,EAAE,4BAA4B;AAErD,OAAO,KAAK,EAIV,UAAU,EACV,WAAW,EACX,QAAQ,EAER,QAAQ,EACR,yBAAyB,EAE1B,gCAAgC;AAuIjC;;;;;GAKG;AACH,wBAAgB,eAAe,CAC7B,KAAK,EAAE,MAAM,GACZ,CAAC,MAAM,GAAG,yBAAyB,GAAG,WAAW,CAAC,EAAE,CA8BtD;AAmBD;;;;;;;;;;GAUG;AACH,wBAAgB,0BAA0B,CACxC,eAAe,EAAE,SAAS,GACzB,UAAU,CAsFZ;AAsBD;;;;;;;GAOG;AACH,wBAAgB,YAAY,CAC1B,IAAI,EAAE,MAAM,EACZ,gBAAgB,EAAE,CAAC,GAAG,EAAE,MAAM,KAAK,OAAO,QA6B3C;AAED;;;;;;;;GAQG;AACH,wBAAgB,iBAAiB,CAC/B,IAAI,EAAE,MAAM,EACZ,gBAAgB,EAAE,CAAC,GAAG,EAAE,MAAM,KAAK,OAAO,QAO3C;AAED;;;;;;;GAOG;AACH,wBAAgB,gBAAgB,CAC9B,IAAI,EAAE,UAAU,EAChB,gBAAgB,EAAE,CAAC,GAAG,EAAE,MAAM,KAAK,OAAO,QAS3C;AAED;;;;;GAKG;AACH,wBAAgB,kBAAkB,CAAC,SAAS,EAAE,SAAS,GAAG,MAAM,CAqB/D;AAED;;;;;GAKG;AACH,wBAAgB,WAAW,CAAC,OAAO,SAAS,UAAU,EACpD,OAAO,EAAE,OAAO,GACf,OAAO,IAAI,OAAO,GAAG;IACtB,KAAK,EAAE;QAAE,QAAQ,EAAE,QAAQ,CAAC,UAAU,GAAG,MAAM,CAAC,CAAA;KAAE,CAAC;CACpD,CAEA;AAcD;;;;;;GAMG;AACH,wBAAgB,cAAc,CAAC,OAAO,EAAE,UAAU,GAAG,CAAC,UAAU,GAAG,MAAM,CAAC,EAAE,CAc3E;AAED;;;;;;;GAOG;AACH,wBAAgB,OAAO,CAAC,KAAK,EAC3B,IAAI,EAAE,UAAU,GAAG,UAAU,EAAE,EAC/B,QAAQ,EAAE,CAAC,IAAI,EAAE,UAAU,EAAE,KAAK,EAAE,MAAM,KAAK,KAAK,GAAG,SAAS,EAChE,KAAK,SAAI,GACR,KAAK,GAAG,SAAS,CAkCnB;AA8BD;;;;;;;GAOG;AACH,wBAAgB,YAAY,CAAC,IAAI,EAAE,QAAQ,EAAE,WAAW,SAAI,GAAG,MAAM,CAyBpE"} -\ No newline at end of file -diff --git a/dist/ui.d.mts b/dist/ui.d.mts -index 9047d932564925a86e7b82a09b17c72aee1273fe..a34aa56c5cdd8fcb7022cebbb036665a180c3d05 100644 ---- a/dist/ui.d.mts -+++ b/dist/ui.d.mts -@@ -25,6 +25,7 @@ export declare function getJsxElementFromComponent(legacyComponent: Component): - * @param link - The link to validate. - * @param isOnPhishingList - The function that checks the link against the - * phishing list. -+ * @throws If the link is invalid. - */ - export declare function validateLink(link: string, isOnPhishingList: (url: string) => boolean): void; - /** -diff --git a/dist/ui.d.mts.map b/dist/ui.d.mts.map -index e2a961017b4f1cf120155b371776653e1a1d9d0b..d551ff82192402da07af285050ca4d5cf0c258ed 100644 ---- a/dist/ui.d.mts.map -+++ b/dist/ui.d.mts.map -@@ -1 +1 @@ --{"version":3,"file":"ui.d.mts","sourceRoot":"","sources":["../src/ui.tsx"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,SAAS,EAAE,4BAA4B;AAErD,OAAO,KAAK,EAIV,UAAU,EACV,WAAW,EACX,QAAQ,EAER,QAAQ,EACR,yBAAyB,EAE1B,gCAAgC;AAuIjC;;;;;GAKG;AACH,wBAAgB,eAAe,CAC7B,KAAK,EAAE,MAAM,GACZ,CAAC,MAAM,GAAG,yBAAyB,GAAG,WAAW,CAAC,EAAE,CA8BtD;AAmBD;;;;;;;;;;GAUG;AACH,wBAAgB,0BAA0B,CACxC,eAAe,EAAE,SAAS,GACzB,UAAU,CAsFZ;AAsBD;;;;;;GAMG;AACH,wBAAgB,YAAY,CAC1B,IAAI,EAAE,MAAM,EACZ,gBAAgB,EAAE,CAAC,GAAG,EAAE,MAAM,KAAK,OAAO,QAoB3C;AAED;;;;;;;;GAQG;AACH,wBAAgB,iBAAiB,CAC/B,IAAI,EAAE,MAAM,EACZ,gBAAgB,EAAE,CAAC,GAAG,EAAE,MAAM,KAAK,OAAO,QAO3C;AAED;;;;;;;GAOG;AACH,wBAAgB,gBAAgB,CAC9B,IAAI,EAAE,UAAU,EAChB,gBAAgB,EAAE,CAAC,GAAG,EAAE,MAAM,KAAK,OAAO,QAS3C;AAED;;;;;GAKG;AACH,wBAAgB,kBAAkB,CAAC,SAAS,EAAE,SAAS,GAAG,MAAM,CAqB/D;AAED;;;;;GAKG;AACH,wBAAgB,WAAW,CAAC,OAAO,SAAS,UAAU,EACpD,OAAO,EAAE,OAAO,GACf,OAAO,IAAI,OAAO,GAAG;IACtB,KAAK,EAAE;QAAE,QAAQ,EAAE,QAAQ,CAAC,UAAU,GAAG,MAAM,CAAC,CAAA;KAAE,CAAC;CACpD,CAEA;AAcD;;;;;;GAMG;AACH,wBAAgB,cAAc,CAAC,OAAO,EAAE,UAAU,GAAG,CAAC,UAAU,GAAG,MAAM,CAAC,EAAE,CAc3E;AAED;;;;;;;GAOG;AACH,wBAAgB,OAAO,CAAC,KAAK,EAC3B,IAAI,EAAE,UAAU,GAAG,UAAU,EAAE,EAC/B,QAAQ,EAAE,CAAC,IAAI,EAAE,UAAU,EAAE,KAAK,EAAE,MAAM,KAAK,KAAK,GAAG,SAAS,EAChE,KAAK,SAAI,GACR,KAAK,GAAG,SAAS,CAkCnB;AA8BD;;;;;;;GAOG;AACH,wBAAgB,YAAY,CAAC,IAAI,EAAE,QAAQ,EAAE,WAAW,SAAI,GAAG,MAAM,CAyBpE"} -\ No newline at end of file -+{"version":3,"file":"ui.d.mts","sourceRoot":"","sources":["../src/ui.tsx"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,SAAS,EAAE,4BAA4B;AAErD,OAAO,KAAK,EAIV,UAAU,EACV,WAAW,EACX,QAAQ,EAER,QAAQ,EACR,yBAAyB,EAE1B,gCAAgC;AAuIjC;;;;;GAKG;AACH,wBAAgB,eAAe,CAC7B,KAAK,EAAE,MAAM,GACZ,CAAC,MAAM,GAAG,yBAAyB,GAAG,WAAW,CAAC,EAAE,CA8BtD;AAmBD;;;;;;;;;;GAUG;AACH,wBAAgB,0BAA0B,CACxC,eAAe,EAAE,SAAS,GACzB,UAAU,CAsFZ;AAsBD;;;;;;;GAOG;AACH,wBAAgB,YAAY,CAC1B,IAAI,EAAE,MAAM,EACZ,gBAAgB,EAAE,CAAC,GAAG,EAAE,MAAM,KAAK,OAAO,QA6B3C;AAED;;;;;;;;GAQG;AACH,wBAAgB,iBAAiB,CAC/B,IAAI,EAAE,MAAM,EACZ,gBAAgB,EAAE,CAAC,GAAG,EAAE,MAAM,KAAK,OAAO,QAO3C;AAED;;;;;;;GAOG;AACH,wBAAgB,gBAAgB,CAC9B,IAAI,EAAE,UAAU,EAChB,gBAAgB,EAAE,CAAC,GAAG,EAAE,MAAM,KAAK,OAAO,QAS3C;AAED;;;;;GAKG;AACH,wBAAgB,kBAAkB,CAAC,SAAS,EAAE,SAAS,GAAG,MAAM,CAqB/D;AAED;;;;;GAKG;AACH,wBAAgB,WAAW,CAAC,OAAO,SAAS,UAAU,EACpD,OAAO,EAAE,OAAO,GACf,OAAO,IAAI,OAAO,GAAG;IACtB,KAAK,EAAE;QAAE,QAAQ,EAAE,QAAQ,CAAC,UAAU,GAAG,MAAM,CAAC,CAAA;KAAE,CAAC;CACpD,CAEA;AAcD;;;;;;GAMG;AACH,wBAAgB,cAAc,CAAC,OAAO,EAAE,UAAU,GAAG,CAAC,UAAU,GAAG,MAAM,CAAC,EAAE,CAc3E;AAED;;;;;;;GAOG;AACH,wBAAgB,OAAO,CAAC,KAAK,EAC3B,IAAI,EAAE,UAAU,GAAG,UAAU,EAAE,EAC/B,QAAQ,EAAE,CAAC,IAAI,EAAE,UAAU,EAAE,KAAK,EAAE,MAAM,KAAK,KAAK,GAAG,SAAS,EAChE,KAAK,SAAI,GACR,KAAK,GAAG,SAAS,CAkCnB;AA8BD;;;;;;;GAOG;AACH,wBAAgB,YAAY,CAAC,IAAI,EAAE,QAAQ,EAAE,WAAW,SAAI,GAAG,MAAM,CAyBpE"} -\ No newline at end of file -diff --git a/dist/ui.mjs b/dist/ui.mjs -index 11b2b5625df002c0962216a06f258869ba65e06b..7499feea1cd9df0d90d2756741bc8e035200506f 100644 ---- a/dist/ui.mjs -+++ b/dist/ui.mjs -@@ -195,13 +195,23 @@ function getMarkdownLinks(text) { - * @param link - The link to validate. - * @param isOnPhishingList - The function that checks the link against the - * phishing list. -+ * @throws If the link is invalid. - */ - export function validateLink(link, isOnPhishingList) { - try { - const url = new URL(link); - assert(ALLOWED_PROTOCOLS.includes(url.protocol), `Protocol must be one of: ${ALLOWED_PROTOCOLS.join(', ')}.`); -- const hostname = url.protocol === 'mailto:' ? url.pathname.split('@')[1] : url.hostname; -- assert(!isOnPhishingList(hostname), 'The specified URL is not allowed.'); -+ if (url.protocol === 'mailto:') { -+ const emails = url.pathname.split(','); -+ for (const email of emails) { -+ const hostname = email.split('@')[1]; -+ assert(!hostname.includes(':')); -+ const href = `https://${hostname}`; -+ assert(!isOnPhishingList(href), 'The specified URL is not allowed.'); -+ } -+ return; -+ } -+ assert(!isOnPhishingList(url.href), 'The specified URL is not allowed.'); - } - catch (error) { - throw new Error(`Invalid URL: ${error?.code === 'ERR_ASSERTION' ? error.message : 'Unable to parse URL.'}`); -diff --git a/dist/ui.mjs.map b/dist/ui.mjs.map -index 1600ced3d6bfc87a5b75328b776dc93e54402201..0d1ffdd50173f534e9dc2ce041ca83e7926750b0 100644 ---- a/dist/ui.mjs.map -+++ b/dist/ui.mjs.map -@@ -1 +1 @@ --{"version":3,"file":"ui.mjs","sourceRoot":"","sources":["../src/ui.tsx"],"names":[],"mappings":"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","sourcesContent":["import type { Component } from '@metamask/snaps-sdk';\nimport { NodeType } from '@metamask/snaps-sdk';\nimport type {\n BoldChildren,\n GenericSnapElement,\n ItalicChildren,\n JSXElement,\n LinkElement,\n Nestable,\n RowChildren,\n SnapNode,\n StandardFormattingElement,\n TextChildren,\n} from '@metamask/snaps-sdk/jsx';\nimport {\n Italic,\n Link,\n Bold,\n Row,\n Text,\n Field,\n Image,\n Input,\n Heading,\n Form,\n Divider,\n Spinner,\n Copyable,\n Box,\n Button,\n Address,\n} from '@metamask/snaps-sdk/jsx';\nimport {\n assert,\n assertExhaustive,\n hasProperty,\n isPlainObject,\n} from '@metamask/utils';\nimport { lexer, walkTokens } from 'marked';\nimport type { Token, Tokens } from 'marked';\n\nconst MAX_TEXT_LENGTH = 50_000; // 50 kb\nconst ALLOWED_PROTOCOLS = ['https:', 'mailto:'];\n\n/**\n * Get the button variant from a legacy button component variant.\n *\n * @param variant - The legacy button component variant.\n * @returns The button variant.\n */\nfunction getButtonVariant(variant?: 'primary' | 'secondary' | undefined) {\n switch (variant) {\n case 'primary':\n return 'primary';\n case 'secondary':\n return 'destructive';\n default:\n return undefined;\n }\n}\n\n/**\n * Get the children of a JSX element. If there is only one child, the child is\n * returned directly. Otherwise, the children are returned as an array.\n *\n * @param elements - The JSX elements.\n * @returns The child or children.\n */\nfunction getChildren(elements: Type[]) {\n if (elements.length === 1) {\n return elements[0];\n }\n\n return elements;\n}\n\n/**\n * Get the text of a link token.\n *\n * @param token - The link token.\n * @returns The text of the link token.\n */\nfunction getLinkText(token: Tokens.Link | Tokens.Generic) {\n if (token.tokens && token.tokens.length > 0) {\n return getChildren(token.tokens.flatMap(getTextChildFromToken));\n }\n\n return token.href;\n}\n\n/**\n * Get the text child from a list of markdown tokens.\n *\n * @param tokens - The markdown tokens.\n * @returns The text child.\n */\nfunction getTextChildFromTokens(tokens: Token[]) {\n return getChildren(tokens.flatMap(getTextChildFromToken));\n}\n\n/**\n * Get the text child from a markdown token.\n *\n * @param token - The markdown token.\n * @returns The text child.\n */\nfunction getTextChildFromToken(token: Token): TextChildren {\n switch (token.type) {\n case 'link': {\n return ;\n }\n\n case 'text':\n return token.text;\n\n case 'strong':\n return (\n \n {\n getTextChildFromTokens(\n // Due to the way `marked` is typed, `token.tokens` can be\n // `undefined`, but it's a required field of `Tokens.Bold`, so we\n // can safely cast it to `Token[]`.\n token.tokens as Token[],\n ) as BoldChildren\n }\n \n );\n\n case 'em':\n return (\n \n {\n getTextChildFromTokens(\n // Due to the way `marked` is typed, `token.tokens` can be\n // `undefined`, but it's a required field of `Tokens.Bold`, so we\n // can safely cast it to `Token[]`.\n token.tokens as Token[],\n ) as ItalicChildren\n }\n \n );\n\n default:\n return null;\n }\n}\n\n/**\n * Get all text children from a markdown string.\n *\n * @param value - The markdown string.\n * @returns The text children.\n */\nexport function getTextChildren(\n value: string,\n): (string | StandardFormattingElement | LinkElement)[] {\n const rootTokens = lexer(value, { gfm: false });\n const children: (string | StandardFormattingElement | LinkElement | null)[] =\n [];\n\n walkTokens(rootTokens, (token) => {\n if (token.type === 'paragraph') {\n if (children.length > 0) {\n children.push('\\n\\n');\n }\n\n const { tokens } = token as Tokens.Paragraph;\n // We do not need to consider nesting deeper than 1 level here and we can therefore cast.\n children.push(\n ...(tokens.flatMap(getTextChildFromToken) as (\n | string\n | StandardFormattingElement\n | LinkElement\n | null\n )[]),\n );\n }\n });\n\n // eslint-disable-next-line @typescript-eslint/no-unnecessary-type-assertion\n return children.filter((child) => child !== null) as (\n | string\n | StandardFormattingElement\n | LinkElement\n )[];\n}\n\n/**\n * Validate the text size of a component. The text size is the total length of\n * all text in the component.\n *\n * @param component - The component to validate.\n * @throws An error if the text size exceeds the maximum allowed size.\n */\nfunction validateComponentTextSize(component: Component) {\n const textSize = getTotalTextLength(component);\n assert(\n textSize <= MAX_TEXT_LENGTH,\n `The text in a Snap UI may not be larger than ${\n MAX_TEXT_LENGTH / 1000\n } kB.`,\n );\n}\n\n/**\n * Get a JSX element from a legacy UI component. This supports all legacy UI\n * components, and maps them to their JSX equivalents where possible.\n *\n * This function validates the text size of the component, but does not validate\n * the total size. The total size of the component should be validated before\n * calling this function.\n *\n * @param legacyComponent - The legacy UI component.\n * @returns The JSX element.\n */\nexport function getJsxElementFromComponent(\n legacyComponent: Component,\n): JSXElement {\n validateComponentTextSize(legacyComponent);\n\n /**\n * Get the JSX element for a component. This function is recursive and will\n * call itself for child components.\n *\n * @param component - The component to convert to a JSX element.\n * @returns The JSX element.\n */\n function getElement(component: Component) {\n switch (component.type) {\n case NodeType.Address:\n return
;\n\n case NodeType.Button:\n return (\n \n {component.value}\n \n );\n\n case NodeType.Copyable:\n return (\n \n );\n\n case NodeType.Divider:\n return ;\n\n case NodeType.Form:\n return (\n
\n {getChildren(component.children.map(getElement))}\n
\n );\n\n case NodeType.Heading:\n return ;\n\n case NodeType.Image:\n // `Image` supports `alt`, but the legacy `Image` component does not.\n return ;\n\n case NodeType.Input:\n return (\n \n \n \n );\n\n case NodeType.Panel:\n // `Panel` is renamed to `Box` in JSX.\n return (\n \n );\n\n case NodeType.Row:\n return (\n \n {getElement(component.value) as RowChildren}\n \n );\n\n case NodeType.Spinner:\n return ;\n\n case NodeType.Text:\n return {getChildren(getTextChildren(component.value))};\n\n /* istanbul ignore next 2 */\n default:\n return assertExhaustive(component);\n }\n }\n\n return getElement(legacyComponent);\n}\n\n/**\n * Extract all links from a Markdown text string using the `marked` lexer.\n *\n * @param text - The markdown text string.\n * @returns A list of URLs linked to in the string.\n */\nfunction getMarkdownLinks(text: string) {\n const tokens = lexer(text, { gfm: false });\n const links: Tokens.Link[] = [];\n\n // Walk the lexed tokens and collect all link tokens\n walkTokens(tokens, (token) => {\n if (token.type === 'link') {\n links.push(token as Tokens.Link);\n }\n });\n\n return links;\n}\n\n/**\n * Validate a link against the phishing list.\n *\n * @param link - The link to validate.\n * @param isOnPhishingList - The function that checks the link against the\n * phishing list.\n */\nexport function validateLink(\n link: string,\n isOnPhishingList: (url: string) => boolean,\n) {\n try {\n const url = new URL(link);\n assert(\n ALLOWED_PROTOCOLS.includes(url.protocol),\n `Protocol must be one of: ${ALLOWED_PROTOCOLS.join(', ')}.`,\n );\n\n const hostname =\n url.protocol === 'mailto:' ? url.pathname.split('@')[1] : url.hostname;\n\n assert(!isOnPhishingList(hostname), 'The specified URL is not allowed.');\n } catch (error) {\n throw new Error(\n `Invalid URL: ${\n error?.code === 'ERR_ASSERTION' ? error.message : 'Unable to parse URL.'\n }`,\n );\n }\n}\n\n/**\n * Search for Markdown links in a string and checks them against the phishing\n * list.\n *\n * @param text - The text to verify.\n * @param isOnPhishingList - The function that checks the link against the\n * phishing list.\n * @throws If the text contains a link that is not allowed.\n */\nexport function validateTextLinks(\n text: string,\n isOnPhishingList: (url: string) => boolean,\n) {\n const links = getMarkdownLinks(text);\n\n for (const link of links) {\n validateLink(link.href, isOnPhishingList);\n }\n}\n\n/**\n * Walk a JSX tree and validate each {@link LinkElement} node against the\n * phishing list.\n *\n * @param node - The JSX node to walk.\n * @param isOnPhishingList - The function that checks the link against the\n * phishing list.\n */\nexport function validateJsxLinks(\n node: JSXElement,\n isOnPhishingList: (url: string) => boolean,\n) {\n walkJsx(node, (childNode) => {\n if (childNode.type !== 'Link') {\n return;\n }\n\n validateLink(childNode.props.href, isOnPhishingList);\n });\n}\n\n/**\n * Calculate the total length of all text in the component.\n *\n * @param component - A custom UI component.\n * @returns The total length of all text components in the component.\n */\nexport function getTotalTextLength(component: Component): number {\n const { type } = component;\n\n switch (type) {\n case NodeType.Panel:\n return component.children.reduce(\n // This is a bug in TypeScript: https://github.com/microsoft/TypeScript/issues/48313\n // eslint-disable-next-line @typescript-eslint/restrict-plus-operands\n (sum, node) => sum + getTotalTextLength(node),\n 0,\n );\n\n case NodeType.Row:\n return getTotalTextLength(component.value);\n\n case NodeType.Text:\n return component.value.length;\n\n default:\n return 0;\n }\n}\n\n/**\n * Check if a JSX element has children.\n *\n * @param element - A JSX element.\n * @returns `true` if the element has children, `false` otherwise.\n */\nexport function hasChildren(\n element: Element,\n): element is Element & {\n props: { children: Nestable };\n} {\n return hasProperty(element.props, 'children');\n}\n\n/**\n * Filter a JSX child to remove `null`, `undefined`, plain booleans, and empty\n * strings.\n *\n * @param child - The JSX child to filter.\n * @returns `true` if the child is not `null`, `undefined`, a plain boolean, or\n * an empty string, `false` otherwise.\n */\nfunction filterJsxChild(child: JSXElement | string | boolean | null): boolean {\n return Boolean(child) && child !== true;\n}\n\n/**\n * Get the children of a JSX element as an array. If the element has only one\n * child, the child is returned as an array.\n *\n * @param element - A JSX element.\n * @returns The children of the element.\n */\nexport function getJsxChildren(element: JSXElement): (JSXElement | string)[] {\n if (hasChildren(element)) {\n if (Array.isArray(element.props.children)) {\n // @ts-expect-error - Each member of the union type has signatures, but\n // none of those signatures are compatible with each other.\n return element.props.children.filter(filterJsxChild).flat(Infinity);\n }\n\n if (element.props.children) {\n return [element.props.children];\n }\n }\n\n return [];\n}\n\n/**\n * Walk a JSX tree and call a callback on each node.\n *\n * @param node - The JSX node to walk.\n * @param callback - The callback to call on each node.\n * @param depth - The current depth in the JSX tree for a walk.\n * @returns The result of the callback, if any.\n */\nexport function walkJsx(\n node: JSXElement | JSXElement[],\n callback: (node: JSXElement, depth: number) => Value | undefined,\n depth = 0,\n): Value | undefined {\n if (Array.isArray(node)) {\n for (const child of node) {\n const childResult = walkJsx(child as JSXElement, callback, depth);\n if (childResult !== undefined) {\n return childResult;\n }\n }\n\n return undefined;\n }\n\n const result = callback(node, depth);\n if (result !== undefined) {\n return result;\n }\n\n if (\n hasProperty(node, 'props') &&\n isPlainObject(node.props) &&\n hasProperty(node.props, 'children')\n ) {\n const children = getJsxChildren(node);\n for (const child of children) {\n if (isPlainObject(child)) {\n const childResult = walkJsx(child, callback, depth + 1);\n if (childResult !== undefined) {\n return childResult;\n }\n }\n }\n }\n\n return undefined;\n}\n\n/**\n * Serialise a JSX prop to a string.\n *\n * @param prop - The JSX prop.\n * @returns The serialised JSX prop.\n */\nfunction serialiseProp(prop: unknown): string {\n if (typeof prop === 'string') {\n return `\"${prop}\"`;\n }\n\n return `{${JSON.stringify(prop)}}`;\n}\n\n/**\n * Serialise JSX props to a string.\n *\n * @param props - The JSX props.\n * @returns The serialised JSX props.\n */\nfunction serialiseProps(props: Record): string {\n return Object.entries(props)\n .filter(([key]) => key !== 'children')\n .sort(([a], [b]) => a.localeCompare(b))\n .map(([key, value]) => ` ${key}=${serialiseProp(value)}`)\n .join('');\n}\n\n/**\n * Serialise a JSX node to a string.\n *\n * @param node - The JSX node.\n * @param indentation - The indentation level. Defaults to `0`. This should not\n * be set by the caller, as it is used for recursion.\n * @returns The serialised JSX node.\n */\nexport function serialiseJsx(node: SnapNode, indentation = 0): string {\n if (Array.isArray(node)) {\n return node.map((child) => serialiseJsx(child, indentation)).join('');\n }\n\n const indent = ' '.repeat(indentation);\n if (typeof node === 'string') {\n return `${indent}${node}\\n`;\n }\n\n if (!node) {\n return '';\n }\n\n const { type, props } = node as GenericSnapElement;\n const trailingNewline = indentation > 0 ? '\\n' : '';\n\n if (hasProperty(props, 'children')) {\n const children = serialiseJsx(props.children as SnapNode, indentation + 1);\n return `${indent}<${type}${serialiseProps(\n props,\n )}>\\n${children}${indent}${trailingNewline}`;\n }\n\n return `${indent}<${type}${serialiseProps(props)} />${trailingNewline}`;\n}\n"]} -\ No newline at end of file -+{"version":3,"file":"ui.mjs","sourceRoot":"","sources":["../src/ui.tsx"],"names":[],"mappings":"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","sourcesContent":["import type { Component } from '@metamask/snaps-sdk';\nimport { NodeType } from '@metamask/snaps-sdk';\nimport type {\n BoldChildren,\n GenericSnapElement,\n ItalicChildren,\n JSXElement,\n LinkElement,\n Nestable,\n RowChildren,\n SnapNode,\n StandardFormattingElement,\n TextChildren,\n} from '@metamask/snaps-sdk/jsx';\nimport {\n Italic,\n Link,\n Bold,\n Row,\n Text,\n Field,\n Image,\n Input,\n Heading,\n Form,\n Divider,\n Spinner,\n Copyable,\n Box,\n Button,\n Address,\n} from '@metamask/snaps-sdk/jsx';\nimport {\n assert,\n assertExhaustive,\n hasProperty,\n isPlainObject,\n} from '@metamask/utils';\nimport { lexer, walkTokens } from 'marked';\nimport type { Token, Tokens } from 'marked';\n\nconst MAX_TEXT_LENGTH = 50_000; // 50 kb\nconst ALLOWED_PROTOCOLS = ['https:', 'mailto:'];\n\n/**\n * Get the button variant from a legacy button component variant.\n *\n * @param variant - The legacy button component variant.\n * @returns The button variant.\n */\nfunction getButtonVariant(variant?: 'primary' | 'secondary' | undefined) {\n switch (variant) {\n case 'primary':\n return 'primary';\n case 'secondary':\n return 'destructive';\n default:\n return undefined;\n }\n}\n\n/**\n * Get the children of a JSX element. If there is only one child, the child is\n * returned directly. Otherwise, the children are returned as an array.\n *\n * @param elements - The JSX elements.\n * @returns The child or children.\n */\nfunction getChildren(elements: Type[]) {\n if (elements.length === 1) {\n return elements[0];\n }\n\n return elements;\n}\n\n/**\n * Get the text of a link token.\n *\n * @param token - The link token.\n * @returns The text of the link token.\n */\nfunction getLinkText(token: Tokens.Link | Tokens.Generic) {\n if (token.tokens && token.tokens.length > 0) {\n return getChildren(token.tokens.flatMap(getTextChildFromToken));\n }\n\n return token.href;\n}\n\n/**\n * Get the text child from a list of markdown tokens.\n *\n * @param tokens - The markdown tokens.\n * @returns The text child.\n */\nfunction getTextChildFromTokens(tokens: Token[]) {\n return getChildren(tokens.flatMap(getTextChildFromToken));\n}\n\n/**\n * Get the text child from a markdown token.\n *\n * @param token - The markdown token.\n * @returns The text child.\n */\nfunction getTextChildFromToken(token: Token): TextChildren {\n switch (token.type) {\n case 'link': {\n return ;\n }\n\n case 'text':\n return token.text;\n\n case 'strong':\n return (\n \n {\n getTextChildFromTokens(\n // Due to the way `marked` is typed, `token.tokens` can be\n // `undefined`, but it's a required field of `Tokens.Bold`, so we\n // can safely cast it to `Token[]`.\n token.tokens as Token[],\n ) as BoldChildren\n }\n \n );\n\n case 'em':\n return (\n \n {\n getTextChildFromTokens(\n // Due to the way `marked` is typed, `token.tokens` can be\n // `undefined`, but it's a required field of `Tokens.Bold`, so we\n // can safely cast it to `Token[]`.\n token.tokens as Token[],\n ) as ItalicChildren\n }\n \n );\n\n default:\n return null;\n }\n}\n\n/**\n * Get all text children from a markdown string.\n *\n * @param value - The markdown string.\n * @returns The text children.\n */\nexport function getTextChildren(\n value: string,\n): (string | StandardFormattingElement | LinkElement)[] {\n const rootTokens = lexer(value, { gfm: false });\n const children: (string | StandardFormattingElement | LinkElement | null)[] =\n [];\n\n walkTokens(rootTokens, (token) => {\n if (token.type === 'paragraph') {\n if (children.length > 0) {\n children.push('\\n\\n');\n }\n\n const { tokens } = token as Tokens.Paragraph;\n // We do not need to consider nesting deeper than 1 level here and we can therefore cast.\n children.push(\n ...(tokens.flatMap(getTextChildFromToken) as (\n | string\n | StandardFormattingElement\n | LinkElement\n | null\n )[]),\n );\n }\n });\n\n // eslint-disable-next-line @typescript-eslint/no-unnecessary-type-assertion\n return children.filter((child) => child !== null) as (\n | string\n | StandardFormattingElement\n | LinkElement\n )[];\n}\n\n/**\n * Validate the text size of a component. The text size is the total length of\n * all text in the component.\n *\n * @param component - The component to validate.\n * @throws An error if the text size exceeds the maximum allowed size.\n */\nfunction validateComponentTextSize(component: Component) {\n const textSize = getTotalTextLength(component);\n assert(\n textSize <= MAX_TEXT_LENGTH,\n `The text in a Snap UI may not be larger than ${\n MAX_TEXT_LENGTH / 1000\n } kB.`,\n );\n}\n\n/**\n * Get a JSX element from a legacy UI component. This supports all legacy UI\n * components, and maps them to their JSX equivalents where possible.\n *\n * This function validates the text size of the component, but does not validate\n * the total size. The total size of the component should be validated before\n * calling this function.\n *\n * @param legacyComponent - The legacy UI component.\n * @returns The JSX element.\n */\nexport function getJsxElementFromComponent(\n legacyComponent: Component,\n): JSXElement {\n validateComponentTextSize(legacyComponent);\n\n /**\n * Get the JSX element for a component. This function is recursive and will\n * call itself for child components.\n *\n * @param component - The component to convert to a JSX element.\n * @returns The JSX element.\n */\n function getElement(component: Component) {\n switch (component.type) {\n case NodeType.Address:\n return
;\n\n case NodeType.Button:\n return (\n \n {component.value}\n \n );\n\n case NodeType.Copyable:\n return (\n \n );\n\n case NodeType.Divider:\n return ;\n\n case NodeType.Form:\n return (\n
\n {getChildren(component.children.map(getElement))}\n
\n );\n\n case NodeType.Heading:\n return ;\n\n case NodeType.Image:\n // `Image` supports `alt`, but the legacy `Image` component does not.\n return ;\n\n case NodeType.Input:\n return (\n \n \n \n );\n\n case NodeType.Panel:\n // `Panel` is renamed to `Box` in JSX.\n return (\n \n );\n\n case NodeType.Row:\n return (\n \n {getElement(component.value) as RowChildren}\n \n );\n\n case NodeType.Spinner:\n return ;\n\n case NodeType.Text:\n return {getChildren(getTextChildren(component.value))};\n\n /* istanbul ignore next 2 */\n default:\n return assertExhaustive(component);\n }\n }\n\n return getElement(legacyComponent);\n}\n\n/**\n * Extract all links from a Markdown text string using the `marked` lexer.\n *\n * @param text - The markdown text string.\n * @returns A list of URLs linked to in the string.\n */\nfunction getMarkdownLinks(text: string) {\n const tokens = lexer(text, { gfm: false });\n const links: Tokens.Link[] = [];\n\n // Walk the lexed tokens and collect all link tokens\n walkTokens(tokens, (token) => {\n if (token.type === 'link') {\n links.push(token as Tokens.Link);\n }\n });\n\n return links;\n}\n\n/**\n * Validate a link against the phishing list.\n *\n * @param link - The link to validate.\n * @param isOnPhishingList - The function that checks the link against the\n * phishing list.\n * @throws If the link is invalid.\n */\nexport function validateLink(\n link: string,\n isOnPhishingList: (url: string) => boolean,\n) {\n try {\n const url = new URL(link);\n assert(\n ALLOWED_PROTOCOLS.includes(url.protocol),\n `Protocol must be one of: ${ALLOWED_PROTOCOLS.join(', ')}.`,\n );\n\n if (url.protocol === 'mailto:') {\n const emails = url.pathname.split(',');\n for (const email of emails) {\n const hostname = email.split('@')[1];\n assert(!hostname.includes(':'));\n const href = `https://${hostname}`;\n assert(!isOnPhishingList(href), 'The specified URL is not allowed.');\n }\n\n return;\n }\n\n assert(!isOnPhishingList(url.href), 'The specified URL is not allowed.');\n } catch (error) {\n throw new Error(\n `Invalid URL: ${\n error?.code === 'ERR_ASSERTION' ? error.message : 'Unable to parse URL.'\n }`,\n );\n }\n}\n\n/**\n * Search for Markdown links in a string and checks them against the phishing\n * list.\n *\n * @param text - The text to verify.\n * @param isOnPhishingList - The function that checks the link against the\n * phishing list.\n * @throws If the text contains a link that is not allowed.\n */\nexport function validateTextLinks(\n text: string,\n isOnPhishingList: (url: string) => boolean,\n) {\n const links = getMarkdownLinks(text);\n\n for (const link of links) {\n validateLink(link.href, isOnPhishingList);\n }\n}\n\n/**\n * Walk a JSX tree and validate each {@link LinkElement} node against the\n * phishing list.\n *\n * @param node - The JSX node to walk.\n * @param isOnPhishingList - The function that checks the link against the\n * phishing list.\n */\nexport function validateJsxLinks(\n node: JSXElement,\n isOnPhishingList: (url: string) => boolean,\n) {\n walkJsx(node, (childNode) => {\n if (childNode.type !== 'Link') {\n return;\n }\n\n validateLink(childNode.props.href, isOnPhishingList);\n });\n}\n\n/**\n * Calculate the total length of all text in the component.\n *\n * @param component - A custom UI component.\n * @returns The total length of all text components in the component.\n */\nexport function getTotalTextLength(component: Component): number {\n const { type } = component;\n\n switch (type) {\n case NodeType.Panel:\n return component.children.reduce(\n // This is a bug in TypeScript: https://github.com/microsoft/TypeScript/issues/48313\n // eslint-disable-next-line @typescript-eslint/restrict-plus-operands\n (sum, node) => sum + getTotalTextLength(node),\n 0,\n );\n\n case NodeType.Row:\n return getTotalTextLength(component.value);\n\n case NodeType.Text:\n return component.value.length;\n\n default:\n return 0;\n }\n}\n\n/**\n * Check if a JSX element has children.\n *\n * @param element - A JSX element.\n * @returns `true` if the element has children, `false` otherwise.\n */\nexport function hasChildren(\n element: Element,\n): element is Element & {\n props: { children: Nestable };\n} {\n return hasProperty(element.props, 'children');\n}\n\n/**\n * Filter a JSX child to remove `null`, `undefined`, plain booleans, and empty\n * strings.\n *\n * @param child - The JSX child to filter.\n * @returns `true` if the child is not `null`, `undefined`, a plain boolean, or\n * an empty string, `false` otherwise.\n */\nfunction filterJsxChild(child: JSXElement | string | boolean | null): boolean {\n return Boolean(child) && child !== true;\n}\n\n/**\n * Get the children of a JSX element as an array. If the element has only one\n * child, the child is returned as an array.\n *\n * @param element - A JSX element.\n * @returns The children of the element.\n */\nexport function getJsxChildren(element: JSXElement): (JSXElement | string)[] {\n if (hasChildren(element)) {\n if (Array.isArray(element.props.children)) {\n // @ts-expect-error - Each member of the union type has signatures, but\n // none of those signatures are compatible with each other.\n return element.props.children.filter(filterJsxChild).flat(Infinity);\n }\n\n if (element.props.children) {\n return [element.props.children];\n }\n }\n\n return [];\n}\n\n/**\n * Walk a JSX tree and call a callback on each node.\n *\n * @param node - The JSX node to walk.\n * @param callback - The callback to call on each node.\n * @param depth - The current depth in the JSX tree for a walk.\n * @returns The result of the callback, if any.\n */\nexport function walkJsx(\n node: JSXElement | JSXElement[],\n callback: (node: JSXElement, depth: number) => Value | undefined,\n depth = 0,\n): Value | undefined {\n if (Array.isArray(node)) {\n for (const child of node) {\n const childResult = walkJsx(child as JSXElement, callback, depth);\n if (childResult !== undefined) {\n return childResult;\n }\n }\n\n return undefined;\n }\n\n const result = callback(node, depth);\n if (result !== undefined) {\n return result;\n }\n\n if (\n hasProperty(node, 'props') &&\n isPlainObject(node.props) &&\n hasProperty(node.props, 'children')\n ) {\n const children = getJsxChildren(node);\n for (const child of children) {\n if (isPlainObject(child)) {\n const childResult = walkJsx(child, callback, depth + 1);\n if (childResult !== undefined) {\n return childResult;\n }\n }\n }\n }\n\n return undefined;\n}\n\n/**\n * Serialise a JSX prop to a string.\n *\n * @param prop - The JSX prop.\n * @returns The serialised JSX prop.\n */\nfunction serialiseProp(prop: unknown): string {\n if (typeof prop === 'string') {\n return `\"${prop}\"`;\n }\n\n return `{${JSON.stringify(prop)}}`;\n}\n\n/**\n * Serialise JSX props to a string.\n *\n * @param props - The JSX props.\n * @returns The serialised JSX props.\n */\nfunction serialiseProps(props: Record): string {\n return Object.entries(props)\n .filter(([key]) => key !== 'children')\n .sort(([a], [b]) => a.localeCompare(b))\n .map(([key, value]) => ` ${key}=${serialiseProp(value)}`)\n .join('');\n}\n\n/**\n * Serialise a JSX node to a string.\n *\n * @param node - The JSX node.\n * @param indentation - The indentation level. Defaults to `0`. This should not\n * be set by the caller, as it is used for recursion.\n * @returns The serialised JSX node.\n */\nexport function serialiseJsx(node: SnapNode, indentation = 0): string {\n if (Array.isArray(node)) {\n return node.map((child) => serialiseJsx(child, indentation)).join('');\n }\n\n const indent = ' '.repeat(indentation);\n if (typeof node === 'string') {\n return `${indent}${node}\\n`;\n }\n\n if (!node) {\n return '';\n }\n\n const { type, props } = node as GenericSnapElement;\n const trailingNewline = indentation > 0 ? '\\n' : '';\n\n if (hasProperty(props, 'children')) {\n const children = serialiseJsx(props.children as SnapNode, indentation + 1);\n return `${indent}<${type}${serialiseProps(\n props,\n )}>\\n${children}${indent}${trailingNewline}`;\n }\n\n return `${indent}<${type}${serialiseProps(props)} />${trailingNewline}`;\n}\n"]} -\ No newline at end of file diff --git a/CHANGELOG.md b/CHANGELOG.md index c5fbcff7a94f..f7b07834e387 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6,6 +6,11 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ## [Unreleased] +## [12.4.1] +### Fixed +- Fix crash on swaps review page ([#27708](https://github.com/MetaMask/metamask-extension/pull/27708)) +- Fix bug that could prevent the phishing detection feature from having the most up to date info on which web pages to block ([#27743](https://github.com/MetaMask/metamask-extension/pull/27743)) + ## [12.4.0] ### Added - Added a receive button to the home screen, allowing users to easily get their address or QR-code for receiving cryptocurrency ([#26148](https://github.com/MetaMask/metamask-extension/pull/26148)) @@ -5139,7 +5144,8 @@ Update styles and spacing on the critical error page ([#20350](https://github.c - Added the ability to restore accounts from seed words. -[Unreleased]: https://github.com/MetaMask/metamask-extension/compare/v12.4.0...HEAD +[Unreleased]: https://github.com/MetaMask/metamask-extension/compare/v12.4.1...HEAD +[12.4.1]: https://github.com/MetaMask/metamask-extension/compare/v12.4.0...v12.4.1 [12.4.0]: https://github.com/MetaMask/metamask-extension/compare/v12.3.1...v12.4.0 [12.3.1]: https://github.com/MetaMask/metamask-extension/compare/v12.3.0...v12.3.1 [12.3.0]: https://github.com/MetaMask/metamask-extension/compare/v12.2.4...v12.3.0 diff --git a/README.md b/README.md index 4f15e138be56..f3e738a40abc 100644 --- a/README.md +++ b/README.md @@ -84,12 +84,15 @@ If you are using VS Code and are unable to make commits from the source control To start a development build (e.g. with logging and file watching) run `yarn start`. #### Development build with wallet state + You can start a development build with a preloaded wallet state, by adding `TEST_SRP=''` and `PASSWORD=''` to the `.metamaskrc` file. Then you have the following options: + 1. Start the wallet with the default fixture flags, by running `yarn start:with-state`. 2. Check the list of available fixture flags, by running `yarn start:with-state --help`. 3. Start the wallet with custom fixture flags, by running `yarn start:with-state --FIXTURE_NAME=VALUE` for example `yarn start:with-state --withAccounts=100`. You can pass as many flags as you want. The rest of the fixtures will take the default values. #### Development build with Webpack + You can also start a development build using the `yarn webpack` command, or `yarn webpack --watch`. This uses an alternative build system that is much faster, but not yet production ready. See the [Webpack README](./development/webpack/README.md) for more information. #### React and Redux DevTools @@ -191,7 +194,7 @@ Different build types have different e2e tests sets. In order to run them look i ```console "test:e2e:chrome:mmi": "SELENIUM_BROWSER=chrome node test/e2e/run-all.js --mmi", "test:e2e:chrome:snaps": "SELENIUM_BROWSER=chrome node test/e2e/run-all.js --snaps", - "test:e2e:firefox": "ENABLE_MV3=false SELENIUM_BROWSER=firefox node test/e2e/run-all.js", + "test:e2e:firefox": "SELENIUM_BROWSER=firefox node test/e2e/run-all.js", ``` #### Note: Running MMI e2e tests diff --git a/app/_locales/en/messages.json b/app/_locales/en/messages.json index 49d48b9c71ac..bb10d6f579a0 100644 --- a/app/_locales/en/messages.json +++ b/app/_locales/en/messages.json @@ -854,9 +854,15 @@ "bridgeDontSend": { "message": "Bridge, don't send" }, + "bridgeFrom": { + "message": "Bridge from" + }, "bridgeSelectNetwork": { "message": "Select network" }, + "bridgeTo": { + "message": "Bridge to" + }, "browserNotSupported": { "message": "Your browser is not supported..." }, @@ -1640,8 +1646,7 @@ "message": "Snaps" }, "disconnectMessage": { - "message": "This will disconnect you from $1", - "description": "$1 is the name of the dapp" + "message": "This will disconnect you from this site" }, "disconnectPrompt": { "message": "Disconnect $1" diff --git a/app/scripts/background.js b/app/scripts/background.js index b6fe63b9aff1..9f203b35661d 100644 --- a/app/scripts/background.js +++ b/app/scripts/background.js @@ -18,7 +18,7 @@ import { isObject } from '@metamask/utils'; import { ApprovalType } from '@metamask/controller-utils'; import PortStream from 'extension-port-stream'; -import { ethErrors } from 'eth-rpc-errors'; +import { providerErrors } from '@metamask/rpc-errors'; import { DIALOG_APPROVAL_TYPES } from '@metamask/snaps-rpc-methods'; import { NotificationServicesController } from '@metamask/notification-services-controller'; @@ -1159,7 +1159,7 @@ export function setupController( default: controller.approvalController.reject( id, - ethErrors.provider.userRejectedRequest(), + providerErrors.userRejectedRequest(), ); break; } diff --git a/app/scripts/controllers/mmi-controller.test.ts b/app/scripts/controllers/mmi-controller.test.ts index dbef190a5573..0c4aa2d5d874 100644 --- a/app/scripts/controllers/mmi-controller.test.ts +++ b/app/scripts/controllers/mmi-controller.test.ts @@ -24,6 +24,7 @@ import { mmiKeyringBuilderFactory } from '../mmi-keyring-builder-factory'; import MetaMetricsController from './metametrics'; import { ETH_EOA_METHODS } from '../../../shared/constants/eth-methods'; import { mockNetworkState } from '../../../test/stub/networks'; +import { API_REQUEST_LOG_EVENT } from '@metamask-institutional/sdk'; jest.mock('@metamask-institutional/portfolio-dashboard', () => ({ handleMmiPortfolio: jest.fn(), @@ -353,6 +354,33 @@ describe('MMIController', function () { mmiController.mmiConfigurationController.storeConfiguration, ).toHaveBeenCalled(); }); + + it('should set up API_REQUEST_LOG_EVENT listener on keyring', async () => { + const mockKeyring = { + on: jest.fn(), + getAccounts: jest.fn().mockResolvedValue([]), + getSupportedChains: jest.fn().mockResolvedValue({}), + }; + + mmiController.addKeyringIfNotExists = jest.fn().mockResolvedValue(mockKeyring); + mmiController.custodyController.getAllCustodyTypes = jest.fn().mockReturnValue(['mock-custody-type']); + mmiController.logAndStoreApiRequest = jest.fn(); + + await mmiController.onSubmitPassword(); + + expect(mockKeyring.on).toHaveBeenCalledWith( + API_REQUEST_LOG_EVENT, + expect.any(Function) + ); + + const mockLogData = { someKey: 'someValue' }; + const apiRequestLogEventHandler = mockKeyring.on.mock.calls.find( + call => call[0] === API_REQUEST_LOG_EVENT + )[1]; + apiRequestLogEventHandler(mockLogData); + + expect(mmiController.logAndStoreApiRequest).toHaveBeenCalledWith(mockLogData); + }); }); describe('connectCustodyAddresses', () => { @@ -408,6 +436,54 @@ describe('MMIController', function () { ).toHaveBeenCalled(); expect(result).toEqual(['0x1']); }); + + it('should set up API_REQUEST_LOG_EVENT listener on keyring', async () => { + const custodianType = 'mock-custodian-type'; + const custodianName = 'mock-custodian-name'; + const accounts = { + '0x1': { + name: 'Account 1', + custodianDetails: {}, + labels: [], + token: 'token', + chainId: 1, + }, + }; + CUSTODIAN_TYPES['MOCK-CUSTODIAN-TYPE'] = { + keyringClass: { type: 'mock-keyring-class' }, + }; + + const mockKeyring = { + on: jest.fn(), + setSelectedAddresses: jest.fn(), + addAccounts: jest.fn(), + getStatusMap: jest.fn(), + }; + + mmiController.addKeyringIfNotExists = jest.fn().mockResolvedValue(mockKeyring); + mmiController.keyringController.getAccounts = jest.fn().mockResolvedValue(['0x2']); + mmiController.keyringController.addNewAccountForKeyring = jest.fn().mockResolvedValue('0x3'); + mmiController.custodyController.setAccountDetails = jest.fn(); + mmiController.accountTrackerController.syncWithAddresses = jest.fn(); + mmiController.storeCustodianSupportedChains = jest.fn(); + mmiController.custodyController.storeCustodyStatusMap = jest.fn(); + mmiController.logAndStoreApiRequest = jest.fn(); + + await mmiController.connectCustodyAddresses(custodianType, custodianName, accounts); + + expect(mockKeyring.on).toHaveBeenCalledWith( + API_REQUEST_LOG_EVENT, + expect.any(Function) + ); + + const mockLogData = { someKey: 'someValue' }; + const apiRequestLogEventHandler = mockKeyring.on.mock.calls.find( + call => call[0] === API_REQUEST_LOG_EVENT + )[1]; + apiRequestLogEventHandler(mockLogData); + + expect(mmiController.logAndStoreApiRequest).toHaveBeenCalledWith(mockLogData); + }); }); describe('getCustodianAccounts', () => { @@ -783,4 +859,27 @@ describe('MMIController', function () { ).toHaveBeenCalledWith('/new-account/connect'); }); }); + + describe('logAndStoreApiRequest', () => { + it('should call custodyController.sanitizeAndLogApiCall with the provided log data', async () => { + const mockLogData = { someKey: 'someValue' }; + const mockSanitizedLogs = { sanitizedKey: 'sanitizedValue' }; + + mmiController.custodyController.sanitizeAndLogApiCall = jest.fn().mockResolvedValue(mockSanitizedLogs); + + const result = await mmiController.logAndStoreApiRequest(mockLogData); + + expect(mmiController.custodyController.sanitizeAndLogApiCall).toHaveBeenCalledWith(mockLogData); + expect(result).toEqual(mockSanitizedLogs); + }); + + it('should handle errors and throw them', async () => { + const mockLogData = { someKey: 'someValue' }; + const mockError = new Error('Sanitize error'); + + mmiController.custodyController.sanitizeAndLogApiCall = jest.fn().mockRejectedValue(mockError); + + await expect(mmiController.logAndStoreApiRequest(mockLogData)).rejects.toThrow('Sanitize error'); + }); + }); }); diff --git a/app/scripts/controllers/mmi-controller.ts b/app/scripts/controllers/mmi-controller.ts index 2373484d4a6e..8c5f1ee4b49b 100644 --- a/app/scripts/controllers/mmi-controller.ts +++ b/app/scripts/controllers/mmi-controller.ts @@ -13,12 +13,14 @@ import { import { REFRESH_TOKEN_CHANGE_EVENT, INTERACTIVE_REPLACEMENT_TOKEN_CHANGE_EVENT, + API_REQUEST_LOG_EVENT, } from '@metamask-institutional/sdk'; import { handleMmiPortfolio } from '@metamask-institutional/portfolio-dashboard'; -import { TransactionMeta } from '@metamask/transaction-controller'; -import { KeyringTypes } from '@metamask/keyring-controller'; import { CustodyController } from '@metamask-institutional/custody-controller'; +import { IApiCallLogEntry } from '@metamask-institutional/types'; import { TransactionUpdateController } from '@metamask-institutional/transaction-update'; +import { TransactionMeta } from '@metamask/transaction-controller'; +import { KeyringTypes } from '@metamask/keyring-controller'; import { SignatureController } from '@metamask/signature-controller'; import { OriginalRequest, @@ -304,6 +306,10 @@ export default class MMIController extends EventEmitter { }, ); + keyring.on(API_REQUEST_LOG_EVENT, (logData: IApiCallLogEntry) => { + this.logAndStoreApiRequest(logData); + }); + // store the supported chains for this custodian type const accounts = await keyring.getAccounts(); addresses = addresses.concat(...accounts); @@ -419,6 +425,10 @@ export default class MMIController extends EventEmitter { }, ); + keyring.on(API_REQUEST_LOG_EVENT, (logData: IApiCallLogEntry) => { + this.logAndStoreApiRequest(logData); + }); + if (!keyring) { throw new Error('Unable to get keyring'); } @@ -884,4 +894,14 @@ export default class MMIController extends EventEmitter { this.platform.openExtensionInBrowser(CONNECT_HARDWARE_ROUTE); return true; } + + async logAndStoreApiRequest(logData: IApiCallLogEntry) { + try { + const logs = await this.custodyController.sanitizeAndLogApiCall(logData); + return logs; + } catch (error) { + log.error('Error fetching extension request logs:', error); + throw error; + } + } } diff --git a/app/scripts/controllers/permissions/specifications.js b/app/scripts/controllers/permissions/specifications.js index 8a40082d4d80..fffc9ae44f49 100644 --- a/app/scripts/controllers/permissions/specifications.js +++ b/app/scripts/controllers/permissions/specifications.js @@ -413,6 +413,7 @@ export const unrestrictedMethods = Object.freeze([ 'snap_updateInterface', 'snap_getInterfaceState', 'snap_resolveInterface', + 'snap_getCurrencyRate', ///: BEGIN:ONLY_INCLUDE_IF(build-mmi) 'metamaskinstitutional_authenticate', 'metamaskinstitutional_reauthenticate', diff --git a/app/scripts/lib/createMetaRPCHandler.js b/app/scripts/lib/createMetaRPCHandler.js index 77f86d23fe02..9d72620b4013 100644 --- a/app/scripts/lib/createMetaRPCHandler.js +++ b/app/scripts/lib/createMetaRPCHandler.js @@ -1,4 +1,4 @@ -import { ethErrors, serializeError } from 'eth-rpc-errors'; +import { rpcErrors, serializeError } from '@metamask/rpc-errors'; import { isStreamWritable } from './stream-utils'; const createMetaRPCHandler = (api, outStream) => { @@ -9,7 +9,7 @@ const createMetaRPCHandler = (api, outStream) => { if (!api[data.method]) { outStream.write({ jsonrpc: '2.0', - error: ethErrors.rpc.methodNotFound({ + error: rpcErrors.methodNotFound({ message: `${data.method} not found`, }), id: data.id, diff --git a/app/scripts/lib/createMetaRPCHandler.test.js b/app/scripts/lib/createMetaRPCHandler.test.js index 842af632e830..873366d53443 100644 --- a/app/scripts/lib/createMetaRPCHandler.test.js +++ b/app/scripts/lib/createMetaRPCHandler.test.js @@ -71,6 +71,7 @@ describe('createMetaRPCHandler', () => { }); streamTest.on('data', (data) => { expect(data.error.message).toStrictEqual('foo-error'); + expect(data.error.data.cause.message).toStrictEqual('foo-error'); streamTest.end(); }); }); diff --git a/app/scripts/lib/createRPCMethodTrackingMiddleware.js b/app/scripts/lib/createRPCMethodTrackingMiddleware.js index b50905b8cb65..a5f12687f89e 100644 --- a/app/scripts/lib/createRPCMethodTrackingMiddleware.js +++ b/app/scripts/lib/createRPCMethodTrackingMiddleware.js @@ -1,5 +1,5 @@ import { ApprovalType, detectSIWE } from '@metamask/controller-utils'; -import { errorCodes } from 'eth-rpc-errors'; +import { errorCodes } from '@metamask/rpc-errors'; import { isValidAddress } from 'ethereumjs-util'; import { MESSAGE_TYPE, ORIGIN_METAMASK } from '../../../shared/constants/app'; import { diff --git a/app/scripts/lib/createRPCMethodTrackingMiddleware.test.js b/app/scripts/lib/createRPCMethodTrackingMiddleware.test.js index b96c708be2d3..01daaf2974a4 100644 --- a/app/scripts/lib/createRPCMethodTrackingMiddleware.test.js +++ b/app/scripts/lib/createRPCMethodTrackingMiddleware.test.js @@ -1,4 +1,4 @@ -import { errorCodes } from 'eth-rpc-errors'; +import { errorCodes } from '@metamask/rpc-errors'; import { detectSIWE } from '@metamask/controller-utils'; import MetaMetricsController from '../controllers/metametrics'; diff --git a/app/scripts/lib/metaRPCClientFactory.js b/app/scripts/lib/metaRPCClientFactory.js index 3aae9962dbdb..2451189836e9 100644 --- a/app/scripts/lib/metaRPCClientFactory.js +++ b/app/scripts/lib/metaRPCClientFactory.js @@ -1,4 +1,4 @@ -import { EthereumRpcError } from 'eth-rpc-errors'; +import { JsonRpcError } from '@metamask/rpc-errors'; import SafeEventEmitter from '@metamask/safe-event-emitter'; import createRandomId from '../../../shared/modules/random-id'; import { TEN_SECONDS_IN_MILLISECONDS } from '../../../shared/lib/transactions-controller-utils'; @@ -77,7 +77,7 @@ class MetaRPCClient { } if (error) { - const e = new EthereumRpcError(error.code, error.message, error.data); + const e = new JsonRpcError(error.code, error.message, error.data); // preserve the stack from serializeError e.stack = error.stack; if (cb) { diff --git a/app/scripts/lib/rpc-method-middleware/createMethodMiddleware.js b/app/scripts/lib/rpc-method-middleware/createMethodMiddleware.js index e4b436163fc6..cee4e7763255 100644 --- a/app/scripts/lib/rpc-method-middleware/createMethodMiddleware.js +++ b/app/scripts/lib/rpc-method-middleware/createMethodMiddleware.js @@ -1,7 +1,7 @@ import { permissionRpcMethods } from '@metamask/permission-controller'; +import { rpcErrors } from '@metamask/rpc-errors'; import { selectHooks } from '@metamask/snaps-rpc-methods'; import { hasProperty } from '@metamask/utils'; -import { ethErrors } from 'eth-rpc-errors'; import { handlers as localHandlers, legacyHandlers } from './handlers'; const allHandlers = [...localHandlers, ...permissionRpcMethods.handlers]; @@ -67,7 +67,7 @@ function makeMethodMiddlewareMaker(handlers) { return end( error instanceof Error ? error - : ethErrors.rpc.internal({ data: error }), + : rpcErrors.internal({ data: error }), ); } } diff --git a/app/scripts/lib/rpc-method-middleware/createUnsupportedMethodMiddleware.ts b/app/scripts/lib/rpc-method-middleware/createUnsupportedMethodMiddleware.ts index 193cc54b5a38..12abc82d4b21 100644 --- a/app/scripts/lib/rpc-method-middleware/createUnsupportedMethodMiddleware.ts +++ b/app/scripts/lib/rpc-method-middleware/createUnsupportedMethodMiddleware.ts @@ -1,4 +1,4 @@ -import { ethErrors } from 'eth-rpc-errors'; +import { rpcErrors } from '@metamask/rpc-errors'; import type { JsonRpcMiddleware } from 'json-rpc-engine'; import { UNSUPPORTED_RPC_METHODS } from '../../../../shared/constants/network'; @@ -12,7 +12,7 @@ export function createUnsupportedMethodMiddleware(): JsonRpcMiddleware< > { return async function unsupportedMethodMiddleware(req, _res, next, end) { if ((UNSUPPORTED_RPC_METHODS as Set).has(req.method)) { - return end(ethErrors.rpc.methodNotSupported()); + return end(rpcErrors.methodNotSupported()); } return next(); }; diff --git a/app/scripts/lib/rpc-method-middleware/handlers/add-ethereum-chain.js b/app/scripts/lib/rpc-method-middleware/handlers/add-ethereum-chain.js index 2f4727fdab36..afcc2e167043 100644 --- a/app/scripts/lib/rpc-method-middleware/handlers/add-ethereum-chain.js +++ b/app/scripts/lib/rpc-method-middleware/handlers/add-ethereum-chain.js @@ -1,7 +1,7 @@ -import { ApprovalType } from '@metamask/controller-utils'; import * as URI from 'uri-js'; +import { ApprovalType } from '@metamask/controller-utils'; import { RpcEndpointType } from '@metamask/network-controller'; -import { ethErrors } from 'eth-rpc-errors'; +import { rpcErrors } from '@metamask/rpc-errors'; import { cloneDeep } from 'lodash'; import { MESSAGE_TYPE } from '../../../../../shared/constants/app'; import { @@ -73,7 +73,7 @@ async function addEthereumChainHandler( existingNetwork.nativeCurrency !== ticker ) { return end( - ethErrors.rpc.invalidParams({ + rpcErrors.invalidParams({ message: `nativeCurrency.symbol does not match currency symbol for a network the user already has added with the same chainId. Received:\n${ticker}`, }), ); diff --git a/app/scripts/lib/rpc-method-middleware/handlers/add-ethereum-chain.test.js b/app/scripts/lib/rpc-method-middleware/handlers/add-ethereum-chain.test.js index 945953cff562..ee0c9d3f732b 100644 --- a/app/scripts/lib/rpc-method-middleware/handlers/add-ethereum-chain.test.js +++ b/app/scripts/lib/rpc-method-middleware/handlers/add-ethereum-chain.test.js @@ -1,4 +1,4 @@ -import { ethErrors } from 'eth-rpc-errors'; +import { rpcErrors } from '@metamask/rpc-errors'; import { CHAIN_IDS } from '../../../../../shared/constants/network'; import addEthereumChain from './add-ethereum-chain'; @@ -350,7 +350,7 @@ describe('addEthereumChainHandler', () => { ); expect(mockEnd).toHaveBeenCalledWith( - ethErrors.rpc.invalidParams({ + rpcErrors.invalidParams({ message: `Expected 0x-prefixed, unpadded, non-zero hexadecimal string 'chainId'. Received:\ninvalid_chain_id`, }), ); @@ -573,7 +573,7 @@ describe('addEthereumChainHandler', () => { ); expect(mockEnd).toHaveBeenCalledWith( - ethErrors.rpc.invalidParams({ + rpcErrors.invalidParams({ message: `Received unexpected keys on object parameter. Unsupported keys:\n${unexpectedParam}`, }), ); @@ -657,7 +657,7 @@ describe('addEthereumChainHandler', () => { ); expect(mockEnd).toHaveBeenCalledWith( - ethErrors.rpc.invalidParams({ + rpcErrors.invalidParams({ message: `nativeCurrency.symbol does not match currency symbol for a network the user already has added with the same chainId. Received:\nWRONG`, }), ); diff --git a/app/scripts/lib/rpc-method-middleware/handlers/ethereum-chain-utils.js b/app/scripts/lib/rpc-method-middleware/handlers/ethereum-chain-utils.js index 080fef549564..10973e052715 100644 --- a/app/scripts/lib/rpc-method-middleware/handlers/ethereum-chain-utils.js +++ b/app/scripts/lib/rpc-method-middleware/handlers/ethereum-chain-utils.js @@ -1,4 +1,4 @@ -import { errorCodes, ethErrors } from 'eth-rpc-errors'; +import { errorCodes, rpcErrors } from '@metamask/rpc-errors'; import { isPrefixedFormattedHexString, isSafeChainId, @@ -11,13 +11,13 @@ import { getValidUrl } from '../../util'; export function validateChainId(chainId) { const _chainId = typeof chainId === 'string' && chainId.toLowerCase(); if (!isPrefixedFormattedHexString(_chainId)) { - throw ethErrors.rpc.invalidParams({ + throw rpcErrors.invalidParams({ message: `Expected 0x-prefixed, unpadded, non-zero hexadecimal string 'chainId'. Received:\n${chainId}`, }); } if (!isSafeChainId(parseInt(_chainId, 16))) { - throw ethErrors.rpc.invalidParams({ + throw rpcErrors.invalidParams({ message: `Invalid chain ID "${_chainId}": numerical value greater than max safe value. Received:\n${chainId}`, }); } @@ -27,7 +27,7 @@ export function validateChainId(chainId) { export function validateSwitchEthereumChainParams(req, end) { if (!req.params?.[0] || typeof req.params[0] !== 'object') { - throw ethErrors.rpc.invalidParams({ + throw rpcErrors.invalidParams({ message: `Expected single, object parameter. Received:\n${JSON.stringify( req.params, )}`, @@ -36,7 +36,7 @@ export function validateSwitchEthereumChainParams(req, end) { const { chainId, ...otherParams } = req.params[0]; if (Object.keys(otherParams).length > 0) { - throw ethErrors.rpc.invalidParams({ + throw rpcErrors.invalidParams({ message: `Received unexpected keys on object parameter. Unsupported keys:\n${Object.keys( otherParams, )}`, @@ -48,7 +48,7 @@ export function validateSwitchEthereumChainParams(req, end) { export function validateAddEthereumChainParams(params, end) { if (!params || typeof params !== 'object') { - throw ethErrors.rpc.invalidParams({ + throw rpcErrors.invalidParams({ message: `Expected single, object parameter. Received:\n${JSON.stringify( params, )}`, @@ -70,14 +70,14 @@ export function validateAddEthereumChainParams(params, end) { ); if (otherKeys.length > 0) { - throw ethErrors.rpc.invalidParams({ + throw rpcErrors.invalidParams({ message: `Received unexpected keys on object parameter. Unsupported keys:\n${otherKeys}`, }); } const _chainId = validateChainId(chainId, end); if (!rpcUrls || !Array.isArray(rpcUrls) || rpcUrls.length === 0) { - throw ethErrors.rpc.invalidParams({ + throw rpcErrors.invalidParams({ message: `Expected an array with at least one valid string HTTPS url 'rpcUrls', Received:\n${rpcUrls}`, }); } @@ -100,13 +100,13 @@ export function validateAddEthereumChainParams(params, end) { : null; if (!firstValidRPCUrl) { - throw ethErrors.rpc.invalidParams({ + throw rpcErrors.invalidParams({ message: `Expected an array with at least one valid string HTTPS url 'rpcUrls', Received:\n${rpcUrls}`, }); } if (typeof chainName !== 'string' || !chainName) { - throw ethErrors.rpc.invalidParams({ + throw rpcErrors.invalidParams({ message: `Expected non-empty string 'chainName'. Received:\n${chainName}`, }); } @@ -116,18 +116,18 @@ export function validateAddEthereumChainParams(params, end) { if (nativeCurrency !== null) { if (typeof nativeCurrency !== 'object' || Array.isArray(nativeCurrency)) { - throw ethErrors.rpc.invalidParams({ + throw rpcErrors.invalidParams({ message: `Expected null or object 'nativeCurrency'. Received:\n${nativeCurrency}`, }); } if (nativeCurrency.decimals !== 18) { - throw ethErrors.rpc.invalidParams({ + throw rpcErrors.invalidParams({ message: `Expected the number 18 for 'nativeCurrency.decimals' when 'nativeCurrency' is provided. Received: ${nativeCurrency.decimals}`, }); } if (!nativeCurrency.symbol || typeof nativeCurrency.symbol !== 'string') { - throw ethErrors.rpc.invalidParams({ + throw rpcErrors.invalidParams({ message: `Expected a string 'nativeCurrency.symbol'. Received: ${nativeCurrency.symbol}`, }); } @@ -138,7 +138,7 @@ export function validateAddEthereumChainParams(params, end) { ticker !== UNKNOWN_TICKER_SYMBOL && (typeof ticker !== 'string' || ticker.length < 1 || ticker.length > 6) ) { - throw ethErrors.rpc.invalidParams({ + throw rpcErrors.invalidParams({ message: `Expected 1-6 character string 'nativeCurrency.symbol'. Received:\n${ticker}`, }); } diff --git a/app/scripts/lib/rpc-method-middleware/handlers/institutional/mmi-set-account-and-network.js b/app/scripts/lib/rpc-method-middleware/handlers/institutional/mmi-set-account-and-network.js index 9dca692601a3..6c3dc41da9d2 100644 --- a/app/scripts/lib/rpc-method-middleware/handlers/institutional/mmi-set-account-and-network.js +++ b/app/scripts/lib/rpc-method-middleware/handlers/institutional/mmi-set-account-and-network.js @@ -1,5 +1,5 @@ -import { ethErrors } from 'eth-rpc-errors'; import { isAllowedRPCOrigin } from '@metamask-institutional/rpc-allowlist'; +import { rpcErrors } from '@metamask/rpc-errors'; import { MESSAGE_TYPE } from '../../../../../../shared/constants/app'; const mmiSetAccountAndNetwork = { @@ -46,7 +46,7 @@ async function mmiSetAccountAndNetworkHandler( if (!req.params?.[0] || typeof req.params[0] !== 'object') { return end( - ethErrors.rpc.invalidParams({ + rpcErrors.invalidParams({ message: `Expected single, object parameter. Received:\n${JSON.stringify( req.params, )}`, diff --git a/app/scripts/lib/rpc-method-middleware/handlers/request-accounts.js b/app/scripts/lib/rpc-method-middleware/handlers/request-accounts.js index f90fb5bd0d42..04977fe465d9 100644 --- a/app/scripts/lib/rpc-method-middleware/handlers/request-accounts.js +++ b/app/scripts/lib/rpc-method-middleware/handlers/request-accounts.js @@ -1,4 +1,4 @@ -import { ethErrors } from 'eth-rpc-errors'; +import { rpcErrors } from '@metamask/rpc-errors'; import { MESSAGE_TYPE } from '../../../../../shared/constants/app'; import { MetaMetricsEventName, @@ -71,7 +71,7 @@ async function requestEthereumAccountsHandler( }, ) { if (locks.has(origin)) { - res.error = ethErrors.rpc.resourceUnavailable( + res.error = rpcErrors.resourceUnavailable( `Already processing ${MESSAGE_TYPE.ETH_REQUEST_ACCOUNTS}. Please wait.`, ); return end(); @@ -132,7 +132,7 @@ async function requestEthereumAccountsHandler( } else { // This should never happen, because it should be caught in the // above catch clause - res.error = ethErrors.rpc.internal( + res.error = rpcErrors.internal( 'Accounts unexpectedly unavailable. Please report this bug.', ); } diff --git a/app/scripts/lib/rpc-method-middleware/handlers/send-metadata.js b/app/scripts/lib/rpc-method-middleware/handlers/send-metadata.js index a32fa497f248..35ec117a1f63 100644 --- a/app/scripts/lib/rpc-method-middleware/handlers/send-metadata.js +++ b/app/scripts/lib/rpc-method-middleware/handlers/send-metadata.js @@ -1,4 +1,4 @@ -import { ethErrors } from 'eth-rpc-errors'; +import { rpcErrors } from '@metamask/rpc-errors'; import { MESSAGE_TYPE } from '../../../../../shared/constants/app'; /** @@ -50,7 +50,7 @@ function sendMetadataHandler( origin, }); } else { - return end(ethErrors.rpc.invalidParams({ data: params })); + return end(rpcErrors.invalidParams({ data: params })); } res.result = true; diff --git a/app/scripts/lib/rpc-method-middleware/handlers/switch-ethereum-chain.js b/app/scripts/lib/rpc-method-middleware/handlers/switch-ethereum-chain.js index f43973e4ba57..5f907bef4d4b 100644 --- a/app/scripts/lib/rpc-method-middleware/handlers/switch-ethereum-chain.js +++ b/app/scripts/lib/rpc-method-middleware/handlers/switch-ethereum-chain.js @@ -1,4 +1,4 @@ -import { ethErrors } from 'eth-rpc-errors'; +import { providerErrors } from '@metamask/rpc-errors'; import { MESSAGE_TYPE } from '../../../../../shared/constants/app'; import { validateSwitchEthereumChainParams, @@ -57,7 +57,7 @@ async function switchEthereumChainHandler( if (!networkClientIdToSwitchTo) { return end( - ethErrors.provider.custom({ + providerErrors.custom({ code: 4902, message: `Unrecognized chain ID "${chainId}". Try adding the chain using ${MESSAGE_TYPE.ADD_ETHEREUM_CHAIN} first.`, }), diff --git a/app/scripts/lib/rpc-method-middleware/handlers/watch-asset.js b/app/scripts/lib/rpc-method-middleware/handlers/watch-asset.js index 6bce7d7dca18..fdfacb373c77 100644 --- a/app/scripts/lib/rpc-method-middleware/handlers/watch-asset.js +++ b/app/scripts/lib/rpc-method-middleware/handlers/watch-asset.js @@ -1,5 +1,5 @@ import { ERC1155, ERC721 } from '@metamask/controller-utils'; -import { ethErrors } from 'eth-rpc-errors'; +import { rpcErrors } from '@metamask/rpc-errors'; import { MESSAGE_TYPE } from '../../../../../shared/constants/app'; const watchAsset = { @@ -51,7 +51,7 @@ async function watchAssetHandler( typeof tokenId !== 'string' ) { return end( - ethErrors.rpc.invalidParams({ + rpcErrors.invalidParams({ message: `Expected parameter 'tokenId' to be type 'string'. Received type '${typeof tokenId}'`, }), ); diff --git a/app/scripts/lib/rpc-method-middleware/handlers/watch-asset.test.js b/app/scripts/lib/rpc-method-middleware/handlers/watch-asset.test.js index eebe8a470ead..73efdd2a2798 100644 --- a/app/scripts/lib/rpc-method-middleware/handlers/watch-asset.test.js +++ b/app/scripts/lib/rpc-method-middleware/handlers/watch-asset.test.js @@ -1,5 +1,5 @@ import { ERC20, ERC721 } from '@metamask/controller-utils'; -import { ethErrors } from 'eth-rpc-errors'; +import { rpcErrors } from '@metamask/rpc-errors'; import watchAssetHandler from './watch-asset'; describe('watchAssetHandler', () => { @@ -95,7 +95,7 @@ describe('watchAssetHandler', () => { }); expect(mockEnd).toHaveBeenCalledWith( - ethErrors.rpc.invalidParams({ + rpcErrors.invalidParams({ message: `Expected parameter 'tokenId' to be type 'string'. Received type 'number'`, }), ); diff --git a/app/scripts/metamask-controller.js b/app/scripts/metamask-controller.js index 5b3693960113..176c7aea10e5 100644 --- a/app/scripts/metamask-controller.js +++ b/app/scripts/metamask-controller.js @@ -27,9 +27,9 @@ import createFilterMiddleware from '@metamask/eth-json-rpc-filters'; import createSubscriptionManager from '@metamask/eth-json-rpc-filters/subscriptionManager'; import { errorCodes as rpcErrorCodes, - EthereumRpcError, - ethErrors, -} from 'eth-rpc-errors'; + JsonRpcError, + providerErrors, +} from '@metamask/rpc-errors'; import { Mutex } from 'await-semaphore'; import log from 'loglevel'; @@ -42,6 +42,7 @@ import { LedgerIframeBridge, } from '@metamask/eth-ledger-bridge-keyring'; import LatticeKeyring from 'eth-lattice-keyring'; +import { rawChainData } from 'eth-chainlist'; import { MetaMaskKeyring as QRHardwareKeyring } from '@keystonehq/metamask-airgapped-keyring'; import EthQuery from '@metamask/eth-query'; import EthJSQuery from '@metamask/ethjs-query'; @@ -169,6 +170,7 @@ import { } from '../../shared/constants/swaps'; import { CHAIN_IDS, + CHAIN_SPEC_URL, NETWORK_TYPES, NetworkStatus, MAINNET_DISPLAY_NAME, @@ -200,6 +202,10 @@ import { } from '../../shared/constants/metametrics'; import { LOG_EVENT } from '../../shared/constants/logs'; +import { + getStorageItem, + setStorageItem, +} from '../../shared/lib/storage-helpers'; import { getTokenIdParam, fetchTokenBalance, @@ -413,6 +419,8 @@ export default class MetamaskController extends EventEmitter { this.getRequestAccountTabIds = opts.getRequestAccountTabIds; this.getOpenMetamaskTabsIds = opts.getOpenMetamaskTabsIds; + this.initializeChainlist(); + this.controllerMessenger = new ControllerMessenger(); this.loggingController = new LoggingController({ @@ -471,7 +479,7 @@ export default class MetamaskController extends EventEmitter { this.encryptionPublicKeyController.clearUnapproved(); this.decryptMessageController.clearUnapproved(); this.signatureController.clearUnapproved(); - this.approvalController.clear(ethErrors.provider.userRejectedRequest()); + this.approvalController.clear(providerErrors.userRejectedRequest()); }; this.queuedRequestController = new QueuedRequestController({ @@ -1479,6 +1487,7 @@ export default class MetamaskController extends EventEmitter { `${this.phishingController.name}:testOrigin`, `${this.approvalController.name}:hasRequest`, `${this.approvalController.name}:acceptRequest`, + `${this.snapController.name}:get`, ], }); @@ -3767,6 +3776,9 @@ export default class MetamaskController extends EventEmitter { appStateController.setCustodianDeepLink.bind(appStateController), setNoteToTraderMessage: appStateController.setNoteToTraderMessage.bind(appStateController), + logAndStoreApiRequest: this.mmiController.logAndStoreApiRequest.bind( + this.mmiController, + ), ///: END:ONLY_INCLUDE_IF // snaps @@ -5975,6 +5987,19 @@ export default class MetamaskController extends EventEmitter { this.controllerMessenger, 'SnapController:getAll', ), + getCurrencyRate: (currency) => { + const rate = this.multichainRatesController.state.rates[currency]; + const { fiatCurrency } = this.multichainRatesController.state; + + if (!rate) { + return undefined; + } + + return { + ...rate, + currency: fiatCurrency, + }; + }, ///: BEGIN:ONLY_INCLUDE_IF(keyring-snaps) hasPermission: this.permissionController.hasPermission.bind( this.permissionController, @@ -6290,6 +6315,23 @@ export default class MetamaskController extends EventEmitter { }); } + /** + * The chain list is fetched live at runtime, falling back to a cache. + * This preseeds the cache at startup with a static list provided at build. + */ + async initializeChainlist() { + const cacheKey = `cachedFetch:${CHAIN_SPEC_URL}`; + const { cachedResponse } = (await getStorageItem(cacheKey)) || {}; + if (cachedResponse) { + return; + } + await setStorageItem(cacheKey, { + cachedResponse: rawChainData(), + // Cached value is immediately invalidated + cachedTime: 0, + }); + } + /** * Returns the nonce that will be associated with a transaction once approved * @@ -6712,7 +6754,7 @@ export default class MetamaskController extends EventEmitter { try { this.approvalController.reject( id, - new EthereumRpcError(error.code, error.message, error.data), + new JsonRpcError(error.code, error.message, error.data), ); } catch (exp) { if (!(exp instanceof ApprovalRequestNotFoundError)) { diff --git a/builds.yml b/builds.yml index a69bf611a322..bcd035b56bc1 100644 --- a/builds.yml +++ b/builds.yml @@ -26,7 +26,7 @@ buildTypes: - SEGMENT_WRITE_KEY_REF: SEGMENT_PROD_WRITE_KEY - ALLOW_LOCAL_SNAPS: false - REQUIRE_SNAPS_ALLOWLIST: true - - IFRAME_EXECUTION_ENVIRONMENT_URL: https://execution.metamask.io/iframe/6.7.0/index.html + - IFRAME_EXECUTION_ENVIRONMENT_URL: https://execution.metamask.io/iframe/6.9.1/index.html - ACCOUNT_SNAPS_DIRECTORY_URL: https://snaps.metamask.io/account-management # Main build uses the default browser manifest manifestOverrides: false @@ -46,7 +46,7 @@ buildTypes: - SEGMENT_WRITE_KEY_REF: SEGMENT_BETA_WRITE_KEY - ALLOW_LOCAL_SNAPS: false - REQUIRE_SNAPS_ALLOWLIST: true - - IFRAME_EXECUTION_ENVIRONMENT_URL: https://execution.metamask.io/iframe/6.7.0/index.html + - IFRAME_EXECUTION_ENVIRONMENT_URL: https://execution.metamask.io/iframe/6.9.1/index.html - ACCOUNT_SNAPS_DIRECTORY_URL: https://snaps.metamask.io/account-management # Modifies how the version is displayed. # eg. instead of 10.25.0 -> 10.25.0-beta.2 @@ -67,7 +67,7 @@ buildTypes: - SEGMENT_FLASK_WRITE_KEY - ALLOW_LOCAL_SNAPS: true - REQUIRE_SNAPS_ALLOWLIST: false - - IFRAME_EXECUTION_ENVIRONMENT_URL: https://execution.metamask.io/iframe/6.7.0/index.html + - IFRAME_EXECUTION_ENVIRONMENT_URL: https://execution.metamask.io/iframe/6.9.1/index.html - SUPPORT_LINK: https://support.metamask.io/ - SUPPORT_REQUEST_LINK: https://support.metamask.io/ - INFURA_ENV_KEY_REF: INFURA_FLASK_PROJECT_ID @@ -90,7 +90,7 @@ buildTypes: - SEGMENT_WRITE_KEY_REF: SEGMENT_MMI_WRITE_KEY - ALLOW_LOCAL_SNAPS: false - REQUIRE_SNAPS_ALLOWLIST: true - - IFRAME_EXECUTION_ENVIRONMENT_URL: https://execution.metamask.io/iframe/6.7.0/index.html + - IFRAME_EXECUTION_ENVIRONMENT_URL: https://execution.metamask.io/iframe/6.9.1/index.html - MMI_CONFIGURATION_SERVICE_URL: https://configuration.metamask-institutional.io/v2/configuration/default - SUPPORT_LINK: https://support.metamask-institutional.io - SUPPORT_REQUEST_LINK: https://support.metamask-institutional.io diff --git a/docs/confirmations.md b/docs/confirmations.md index 7af838d2053e..86577fc8f691 100644 --- a/docs/confirmations.md +++ b/docs/confirmations.md @@ -168,7 +168,7 @@ function getValues(pendingApproval, t, actions, _history) { onCancel: () => actions.rejectPendingApproval( pendingApproval.id, - ethErrors.provider.userRejectedRequest().serialize(), + providerErrors.userRejectedRequest().serialize(), ), networkDisplay: true, }; @@ -401,4 +401,4 @@ When an approval flow is created, this is reflected in the state and the UI will ### Custom Success Approval -[](assets/confirmation.png) \ No newline at end of file +[](assets/confirmation.png) diff --git a/lavamoat/browserify/beta/policy.json b/lavamoat/browserify/beta/policy.json index d7522783f9fc..880b542673ea 100644 --- a/lavamoat/browserify/beta/policy.json +++ b/lavamoat/browserify/beta/policy.json @@ -662,8 +662,8 @@ }, "packages": { "@metamask/approval-controller>@metamask/base-controller": true, - "@metamask/approval-controller>nanoid": true, - "@metamask/rpc-errors": true + "@metamask/approval-controller>@metamask/rpc-errors": true, + "@metamask/approval-controller>nanoid": true } }, "@metamask/approval-controller>@metamask/base-controller": { @@ -674,6 +674,12 @@ "immer": true } }, + "@metamask/approval-controller>@metamask/rpc-errors": { + "packages": { + "@metamask/rpc-errors>fast-safe-stringify": true, + "@metamask/utils": true + } + }, "@metamask/approval-controller>nanoid": { "globals": { "crypto.getRandomValues": true @@ -699,13 +705,13 @@ "@ethersproject/providers": true, "@metamask/abi-utils": true, "@metamask/assets-controllers>@metamask/polling-controller": true, + "@metamask/assets-controllers>@metamask/rpc-errors": true, "@metamask/base-controller": true, "@metamask/contract-metadata": true, "@metamask/controller-utils": true, "@metamask/eth-query": true, "@metamask/metamask-eth-abis": true, "@metamask/name-controller>async-mutex": true, - "@metamask/rpc-errors": true, "@metamask/utils": true, "bn.js": true, "cockatiel": true, @@ -727,6 +733,12 @@ "uuid": true } }, + "@metamask/assets-controllers>@metamask/rpc-errors": { + "packages": { + "@metamask/rpc-errors>fast-safe-stringify": true, + "@metamask/utils": true + } + }, "@metamask/base-controller": { "globals": { "setTimeout": true @@ -851,11 +863,32 @@ }, "@metamask/eth-json-rpc-filters>@metamask/json-rpc-engine": { "packages": { + "@metamask/eth-json-rpc-filters>@metamask/json-rpc-engine>@metamask/rpc-errors": true, "@metamask/eth-json-rpc-filters>@metamask/json-rpc-engine>@metamask/utils": true, - "@metamask/rpc-errors": true, "@metamask/safe-event-emitter": true } }, + "@metamask/eth-json-rpc-filters>@metamask/json-rpc-engine>@metamask/rpc-errors": { + "packages": { + "@metamask/eth-json-rpc-filters>@metamask/json-rpc-engine>@metamask/rpc-errors>@metamask/utils": true, + "@metamask/rpc-errors>fast-safe-stringify": true + } + }, + "@metamask/eth-json-rpc-filters>@metamask/json-rpc-engine>@metamask/rpc-errors>@metamask/utils": { + "globals": { + "TextDecoder": true, + "TextEncoder": true + }, + "packages": { + "@metamask/utils>@metamask/superstruct": true, + "@metamask/utils>@scure/base": true, + "@metamask/utils>pony-cause": true, + "@noble/hashes": true, + "browserify>buffer": true, + "nock>debug": true, + "semver": true + } + }, "@metamask/eth-json-rpc-filters>@metamask/json-rpc-engine>@metamask/utils": { "globals": { "TextDecoder": true, @@ -886,14 +919,20 @@ "setTimeout": true }, "packages": { + "@metamask/eth-json-rpc-middleware>@metamask/rpc-errors": true, "@metamask/eth-json-rpc-middleware>klona": true, "@metamask/eth-json-rpc-middleware>safe-stable-stringify": true, "@metamask/eth-sig-util": true, - "@metamask/rpc-errors": true, "@metamask/snaps-controllers>@metamask/json-rpc-engine": true, "@metamask/utils": true } }, + "@metamask/eth-json-rpc-middleware>@metamask/rpc-errors": { + "packages": { + "@metamask/rpc-errors>fast-safe-stringify": true, + "@metamask/utils": true + } + }, "@metamask/eth-ledger-bridge-keyring": { "globals": { "addEventListener": true, @@ -1505,9 +1544,9 @@ "@metamask/network-controller>@metamask/eth-json-rpc-infura": true, "@metamask/network-controller>@metamask/eth-json-rpc-middleware": true, "@metamask/network-controller>@metamask/eth-json-rpc-provider": true, + "@metamask/network-controller>@metamask/rpc-errors": true, "@metamask/network-controller>@metamask/swappable-obj-proxy": true, "@metamask/network-controller>reselect": true, - "@metamask/rpc-errors": true, "@metamask/snaps-controllers>@metamask/json-rpc-engine": true, "@metamask/utils": true, "browserify>assert": true, @@ -1551,8 +1590,8 @@ "packages": { "@metamask/network-controller>@metamask/eth-json-rpc-infura>@metamask/eth-json-rpc-provider": true, "@metamask/network-controller>@metamask/eth-json-rpc-infura>@metamask/json-rpc-engine": true, + "@metamask/network-controller>@metamask/eth-json-rpc-infura>@metamask/rpc-errors": true, "@metamask/network-controller>@metamask/eth-json-rpc-infura>@metamask/utils": true, - "@metamask/rpc-errors": true, "node-fetch": true } }, @@ -1564,11 +1603,32 @@ }, "@metamask/network-controller>@metamask/eth-json-rpc-infura>@metamask/json-rpc-engine": { "packages": { + "@metamask/network-controller>@metamask/eth-json-rpc-infura>@metamask/rpc-errors": true, "@metamask/network-controller>@metamask/eth-json-rpc-infura>@metamask/utils": true, - "@metamask/rpc-errors": true, "@metamask/safe-event-emitter": true } }, + "@metamask/network-controller>@metamask/eth-json-rpc-infura>@metamask/rpc-errors": { + "packages": { + "@metamask/network-controller>@metamask/eth-json-rpc-infura>@metamask/rpc-errors>@metamask/utils": true, + "@metamask/rpc-errors>fast-safe-stringify": true + } + }, + "@metamask/network-controller>@metamask/eth-json-rpc-infura>@metamask/rpc-errors>@metamask/utils": { + "globals": { + "TextDecoder": true, + "TextEncoder": true + }, + "packages": { + "@metamask/utils>@metamask/superstruct": true, + "@metamask/utils>@scure/base": true, + "@metamask/utils>pony-cause": true, + "@noble/hashes": true, + "browserify>buffer": true, + "nock>debug": true, + "semver": true + } + }, "@metamask/network-controller>@metamask/eth-json-rpc-infura>@metamask/utils": { "globals": { "TextDecoder": true, @@ -1595,7 +1655,7 @@ "@metamask/eth-json-rpc-middleware>safe-stable-stringify": true, "@metamask/eth-sig-util": true, "@metamask/network-controller>@metamask/eth-json-rpc-middleware>@metamask/utils": true, - "@metamask/rpc-errors": true, + "@metamask/network-controller>@metamask/rpc-errors": true, "@metamask/snaps-controllers>@metamask/json-rpc-engine": true, "bn.js": true, "pify": true @@ -1618,12 +1678,24 @@ }, "@metamask/network-controller>@metamask/eth-json-rpc-provider": { "packages": { - "@metamask/rpc-errors": true, + "@metamask/network-controller>@metamask/eth-json-rpc-provider>@metamask/rpc-errors": true, "@metamask/safe-event-emitter": true, "@metamask/snaps-controllers>@metamask/json-rpc-engine": true, "uuid": true } }, + "@metamask/network-controller>@metamask/eth-json-rpc-provider>@metamask/rpc-errors": { + "packages": { + "@metamask/rpc-errors>fast-safe-stringify": true, + "@metamask/utils": true + } + }, + "@metamask/network-controller>@metamask/rpc-errors": { + "packages": { + "@metamask/rpc-errors>fast-safe-stringify": true, + "@metamask/utils": true + } + }, "@metamask/network-controller>reselect": { "globals": { "WeakRef": true, @@ -1832,9 +1904,9 @@ "packages": { "@metamask/controller-utils": true, "@metamask/permission-controller>@metamask/base-controller": true, + "@metamask/permission-controller>@metamask/rpc-errors": true, "@metamask/permission-controller>@metamask/utils": true, "@metamask/permission-controller>nanoid": true, - "@metamask/rpc-errors": true, "@metamask/snaps-controllers>@metamask/json-rpc-engine": true, "deep-freeze-strict": true, "immer": true @@ -1848,6 +1920,27 @@ "immer": true } }, + "@metamask/permission-controller>@metamask/rpc-errors": { + "packages": { + "@metamask/permission-controller>@metamask/rpc-errors>@metamask/utils": true, + "@metamask/rpc-errors>fast-safe-stringify": true + } + }, + "@metamask/permission-controller>@metamask/rpc-errors>@metamask/utils": { + "globals": { + "TextDecoder": true, + "TextEncoder": true + }, + "packages": { + "@metamask/utils>@metamask/superstruct": true, + "@metamask/utils>@scure/base": true, + "@metamask/utils>pony-cause": true, + "@noble/hashes": true, + "browserify>buffer": true, + "nock>debug": true, + "semver": true + } + }, "@metamask/permission-controller>@metamask/utils": { "globals": { "TextDecoder": true, @@ -1938,9 +2031,9 @@ "@metamask/eth-query>json-rpc-random-id": true, "@metamask/ppom-validator>@metamask/base-controller": true, "@metamask/ppom-validator>@metamask/controller-utils": true, + "@metamask/ppom-validator>@metamask/rpc-errors": true, "@metamask/ppom-validator>crypto-js": true, "@metamask/ppom-validator>elliptic": true, - "@metamask/rpc-errors": true, "await-semaphore": true, "browserify>buffer": true } @@ -1971,6 +2064,27 @@ "eth-ens-namehash": true } }, + "@metamask/ppom-validator>@metamask/rpc-errors": { + "packages": { + "@metamask/ppom-validator>@metamask/rpc-errors>@metamask/utils": true, + "@metamask/rpc-errors>fast-safe-stringify": true + } + }, + "@metamask/ppom-validator>@metamask/rpc-errors>@metamask/utils": { + "globals": { + "TextDecoder": true, + "TextEncoder": true + }, + "packages": { + "@metamask/utils>@metamask/superstruct": true, + "@metamask/utils>@scure/base": true, + "@metamask/utils>pony-cause": true, + "@noble/hashes": true, + "browserify>buffer": true, + "nock>debug": true, + "semver": true + } + }, "@metamask/ppom-validator>@metamask/utils": { "globals": { "TextDecoder": true, @@ -2096,8 +2210,8 @@ "@metamask/queued-request-controller": { "packages": { "@metamask/queued-request-controller>@metamask/base-controller": true, + "@metamask/queued-request-controller>@metamask/rpc-errors": true, "@metamask/queued-request-controller>@metamask/utils": true, - "@metamask/rpc-errors": true, "@metamask/selected-network-controller": true, "@metamask/snaps-controllers>@metamask/json-rpc-engine": true } @@ -2110,6 +2224,27 @@ "immer": true } }, + "@metamask/queued-request-controller>@metamask/rpc-errors": { + "packages": { + "@metamask/queued-request-controller>@metamask/rpc-errors>@metamask/utils": true, + "@metamask/rpc-errors>fast-safe-stringify": true + } + }, + "@metamask/queued-request-controller>@metamask/rpc-errors>@metamask/utils": { + "globals": { + "TextDecoder": true, + "TextEncoder": true + }, + "packages": { + "@metamask/utils>@metamask/superstruct": true, + "@metamask/utils>@scure/base": true, + "@metamask/utils>pony-cause": true, + "@noble/hashes": true, + "browserify>buffer": true, + "nock>debug": true, + "semver": true + } + }, "@metamask/queued-request-controller>@metamask/utils": { "globals": { "TextDecoder": true, @@ -2131,8 +2266,8 @@ }, "packages": { "@metamask/rate-limit-controller>@metamask/base-controller": true, - "@metamask/rate-limit-controller>@metamask/utils": true, - "@metamask/rpc-errors": true + "@metamask/rate-limit-controller>@metamask/rpc-errors": true, + "@metamask/rate-limit-controller>@metamask/utils": true } }, "@metamask/rate-limit-controller>@metamask/base-controller": { @@ -2143,6 +2278,27 @@ "immer": true } }, + "@metamask/rate-limit-controller>@metamask/rpc-errors": { + "packages": { + "@metamask/rate-limit-controller>@metamask/rpc-errors>@metamask/utils": true, + "@metamask/rpc-errors>fast-safe-stringify": true + } + }, + "@metamask/rate-limit-controller>@metamask/rpc-errors>@metamask/utils": { + "globals": { + "TextDecoder": true, + "TextEncoder": true + }, + "packages": { + "@metamask/utils>@metamask/superstruct": true, + "@metamask/utils>@scure/base": true, + "@metamask/utils>pony-cause": true, + "@noble/hashes": true, + "browserify>buffer": true, + "nock>debug": true, + "semver": true + } + }, "@metamask/rate-limit-controller>@metamask/utils": { "globals": { "TextDecoder": true, @@ -2160,8 +2316,8 @@ }, "@metamask/rpc-errors": { "packages": { - "@metamask/utils": true, - "eth-rpc-errors>fast-safe-stringify": true + "@metamask/rpc-errors>fast-safe-stringify": true, + "@metamask/utils": true } }, "@metamask/rpc-methods-flask>nanoid": { @@ -2310,11 +2466,11 @@ "@metamask/metamask-eth-abis": true, "@metamask/name-controller>async-mutex": true, "@metamask/network-controller": true, - "@metamask/rpc-errors": true, "@metamask/smart-transactions-controller>@metamask/base-controller": true, "@metamask/smart-transactions-controller>@metamask/transaction-controller>@ethereumjs/tx": true, "@metamask/smart-transactions-controller>@metamask/transaction-controller>@ethereumjs/util": true, "@metamask/smart-transactions-controller>@metamask/transaction-controller>@metamask/nonce-tracker": true, + "@metamask/smart-transactions-controller>@metamask/transaction-controller>@metamask/rpc-errors": true, "@metamask/smart-transactions-controller>@metamask/transaction-controller>@metamask/utils": true, "@metamask/smart-transactions-controller>@metamask/transaction-controller>eth-method-registry": true, "bn.js": true, @@ -2364,6 +2520,12 @@ "@swc/helpers>tslib": true } }, + "@metamask/smart-transactions-controller>@metamask/transaction-controller>@metamask/rpc-errors": { + "packages": { + "@metamask/rpc-errors>fast-safe-stringify": true, + "@metamask/utils": true + } + }, "@metamask/smart-transactions-controller>@metamask/transaction-controller>@metamask/utils": { "globals": { "TextDecoder": true, @@ -2449,11 +2611,11 @@ "packages": { "@metamask/object-multiplex": true, "@metamask/post-message-stream": true, - "@metamask/rpc-errors": true, "@metamask/snaps-controllers>@metamask/base-controller": true, "@metamask/snaps-controllers>@metamask/json-rpc-engine": true, "@metamask/snaps-controllers>@metamask/json-rpc-middleware-stream": true, "@metamask/snaps-controllers>@metamask/permission-controller": true, + "@metamask/snaps-controllers>@metamask/rpc-errors": true, "@metamask/snaps-controllers>@xstate/fsm": true, "@metamask/snaps-controllers>concat-stream": true, "@metamask/snaps-controllers>get-npm-tarball-url": true, @@ -2486,8 +2648,14 @@ }, "@metamask/snaps-controllers>@metamask/json-rpc-engine": { "packages": { - "@metamask/rpc-errors": true, "@metamask/safe-event-emitter": true, + "@metamask/snaps-controllers>@metamask/json-rpc-engine>@metamask/rpc-errors": true, + "@metamask/utils": true + } + }, + "@metamask/snaps-controllers>@metamask/json-rpc-engine>@metamask/rpc-errors": { + "packages": { + "@metamask/rpc-errors>fast-safe-stringify": true, "@metamask/utils": true } }, @@ -2508,15 +2676,21 @@ }, "packages": { "@metamask/controller-utils": true, - "@metamask/rpc-errors": true, "@metamask/snaps-controllers>@metamask/base-controller": true, "@metamask/snaps-controllers>@metamask/json-rpc-engine": true, + "@metamask/snaps-controllers>@metamask/rpc-errors": true, "@metamask/snaps-controllers>nanoid": true, "@metamask/utils": true, "deep-freeze-strict": true, "immer": true } }, + "@metamask/snaps-controllers>@metamask/rpc-errors": { + "packages": { + "@metamask/rpc-errors>fast-safe-stringify": true, + "@metamask/utils": true + } + }, "@metamask/snaps-controllers>concat-stream": { "packages": { "browserify>buffer": true, @@ -2574,8 +2748,8 @@ }, "@metamask/snaps-rpc-methods": { "packages": { - "@metamask/rpc-errors": true, "@metamask/snaps-rpc-methods>@metamask/permission-controller": true, + "@metamask/snaps-rpc-methods>@metamask/rpc-errors": true, "@metamask/snaps-sdk": true, "@metamask/snaps-sdk>@metamask/key-tree": true, "@metamask/snaps-utils": true, @@ -2590,10 +2764,10 @@ }, "packages": { "@metamask/controller-utils": true, - "@metamask/rpc-errors": true, "@metamask/snaps-controllers>@metamask/json-rpc-engine": true, "@metamask/snaps-rpc-methods>@metamask/permission-controller>@metamask/base-controller": true, "@metamask/snaps-rpc-methods>@metamask/permission-controller>nanoid": true, + "@metamask/snaps-rpc-methods>@metamask/rpc-errors": true, "@metamask/utils": true, "deep-freeze-strict": true, "immer": true @@ -2612,12 +2786,18 @@ "crypto.getRandomValues": true } }, + "@metamask/snaps-rpc-methods>@metamask/rpc-errors": { + "packages": { + "@metamask/rpc-errors>fast-safe-stringify": true, + "@metamask/utils": true + } + }, "@metamask/snaps-sdk": { "globals": { "fetch": true }, "packages": { - "@metamask/rpc-errors": true, + "@metamask/snaps-sdk>@metamask/rpc-errors": true, "@metamask/utils": true, "@metamask/utils>@metamask/superstruct": true } @@ -2631,6 +2811,12 @@ "@noble/hashes": true } }, + "@metamask/snaps-sdk>@metamask/rpc-errors": { + "packages": { + "@metamask/rpc-errors>fast-safe-stringify": true, + "@metamask/utils": true + } + }, "@metamask/snaps-utils": { "globals": { "File": true, @@ -2647,10 +2833,10 @@ "fetch": true }, "packages": { - "@metamask/rpc-errors": true, "@metamask/snaps-sdk": true, "@metamask/snaps-sdk>@metamask/key-tree": true, "@metamask/snaps-utils>@metamask/permission-controller": true, + "@metamask/snaps-utils>@metamask/rpc-errors": true, "@metamask/snaps-utils>@metamask/slip44": true, "@metamask/snaps-utils>cron-parser": true, "@metamask/snaps-utils>fast-json-stable-stringify": true, @@ -2680,10 +2866,10 @@ }, "packages": { "@metamask/controller-utils": true, - "@metamask/rpc-errors": true, "@metamask/snaps-controllers>@metamask/json-rpc-engine": true, "@metamask/snaps-utils>@metamask/base-controller": true, "@metamask/snaps-utils>@metamask/permission-controller>nanoid": true, + "@metamask/snaps-utils>@metamask/rpc-errors": true, "@metamask/utils": true, "deep-freeze-strict": true, "immer": true @@ -2694,6 +2880,12 @@ "crypto.getRandomValues": true } }, + "@metamask/snaps-utils>@metamask/rpc-errors": { + "packages": { + "@metamask/rpc-errors>fast-safe-stringify": true, + "@metamask/utils": true + } + }, "@metamask/snaps-utils>@metamask/snaps-registry": { "packages": { "@metamask/message-signing-snap>@noble/curves": true, @@ -2766,8 +2958,8 @@ "@metamask/metamask-eth-abis": true, "@metamask/name-controller>async-mutex": true, "@metamask/network-controller": true, - "@metamask/rpc-errors": true, "@metamask/transaction-controller>@metamask/nonce-tracker": true, + "@metamask/transaction-controller>@metamask/rpc-errors": true, "@metamask/utils": true, "bn.js": true, "browserify>buffer": true, @@ -2794,6 +2986,12 @@ "@swc/helpers>tslib": true } }, + "@metamask/transaction-controller>@metamask/rpc-errors": { + "packages": { + "@metamask/rpc-errors>fast-safe-stringify": true, + "@metamask/utils": true + } + }, "@metamask/user-operation-controller": { "globals": { "fetch": true @@ -2803,9 +3001,9 @@ "@metamask/eth-query": true, "@metamask/gas-fee-controller": true, "@metamask/gas-fee-controller>@metamask/polling-controller": true, - "@metamask/rpc-errors": true, "@metamask/transaction-controller": true, "@metamask/user-operation-controller>@metamask/base-controller": true, + "@metamask/user-operation-controller>@metamask/rpc-errors": true, "@metamask/user-operation-controller>@metamask/utils": true, "bn.js": true, "lodash": true, @@ -2822,6 +3020,27 @@ "immer": true } }, + "@metamask/user-operation-controller>@metamask/rpc-errors": { + "packages": { + "@metamask/rpc-errors>fast-safe-stringify": true, + "@metamask/user-operation-controller>@metamask/rpc-errors>@metamask/utils": true + } + }, + "@metamask/user-operation-controller>@metamask/rpc-errors>@metamask/utils": { + "globals": { + "TextDecoder": true, + "TextEncoder": true + }, + "packages": { + "@metamask/utils>@metamask/superstruct": true, + "@metamask/utils>@scure/base": true, + "@metamask/utils>pony-cause": true, + "@noble/hashes": true, + "browserify>buffer": true, + "nock>debug": true, + "semver": true + } + }, "@metamask/user-operation-controller>@metamask/utils": { "globals": { "TextDecoder": true, @@ -4067,11 +4286,6 @@ "@metamask/ethjs-query": true } }, - "eth-rpc-errors": { - "packages": { - "eth-rpc-errors>fast-safe-stringify": true - } - }, "ethereumjs-util": { "packages": { "bn.js": true, @@ -4525,8 +4739,8 @@ }, "json-rpc-engine": { "packages": { - "eth-rpc-errors": true, - "json-rpc-engine>@metamask/safe-event-emitter": true + "json-rpc-engine>@metamask/safe-event-emitter": true, + "json-rpc-engine>eth-rpc-errors": true } }, "json-rpc-engine>@metamask/safe-event-emitter": { @@ -4537,6 +4751,11 @@ "webpack>events": true } }, + "json-rpc-engine>eth-rpc-errors": { + "packages": { + "@metamask/rpc-errors>fast-safe-stringify": true + } + }, "json-rpc-middleware-stream": { "globals": { "console.warn": true, diff --git a/lavamoat/browserify/flask/policy.json b/lavamoat/browserify/flask/policy.json index d7522783f9fc..880b542673ea 100644 --- a/lavamoat/browserify/flask/policy.json +++ b/lavamoat/browserify/flask/policy.json @@ -662,8 +662,8 @@ }, "packages": { "@metamask/approval-controller>@metamask/base-controller": true, - "@metamask/approval-controller>nanoid": true, - "@metamask/rpc-errors": true + "@metamask/approval-controller>@metamask/rpc-errors": true, + "@metamask/approval-controller>nanoid": true } }, "@metamask/approval-controller>@metamask/base-controller": { @@ -674,6 +674,12 @@ "immer": true } }, + "@metamask/approval-controller>@metamask/rpc-errors": { + "packages": { + "@metamask/rpc-errors>fast-safe-stringify": true, + "@metamask/utils": true + } + }, "@metamask/approval-controller>nanoid": { "globals": { "crypto.getRandomValues": true @@ -699,13 +705,13 @@ "@ethersproject/providers": true, "@metamask/abi-utils": true, "@metamask/assets-controllers>@metamask/polling-controller": true, + "@metamask/assets-controllers>@metamask/rpc-errors": true, "@metamask/base-controller": true, "@metamask/contract-metadata": true, "@metamask/controller-utils": true, "@metamask/eth-query": true, "@metamask/metamask-eth-abis": true, "@metamask/name-controller>async-mutex": true, - "@metamask/rpc-errors": true, "@metamask/utils": true, "bn.js": true, "cockatiel": true, @@ -727,6 +733,12 @@ "uuid": true } }, + "@metamask/assets-controllers>@metamask/rpc-errors": { + "packages": { + "@metamask/rpc-errors>fast-safe-stringify": true, + "@metamask/utils": true + } + }, "@metamask/base-controller": { "globals": { "setTimeout": true @@ -851,11 +863,32 @@ }, "@metamask/eth-json-rpc-filters>@metamask/json-rpc-engine": { "packages": { + "@metamask/eth-json-rpc-filters>@metamask/json-rpc-engine>@metamask/rpc-errors": true, "@metamask/eth-json-rpc-filters>@metamask/json-rpc-engine>@metamask/utils": true, - "@metamask/rpc-errors": true, "@metamask/safe-event-emitter": true } }, + "@metamask/eth-json-rpc-filters>@metamask/json-rpc-engine>@metamask/rpc-errors": { + "packages": { + "@metamask/eth-json-rpc-filters>@metamask/json-rpc-engine>@metamask/rpc-errors>@metamask/utils": true, + "@metamask/rpc-errors>fast-safe-stringify": true + } + }, + "@metamask/eth-json-rpc-filters>@metamask/json-rpc-engine>@metamask/rpc-errors>@metamask/utils": { + "globals": { + "TextDecoder": true, + "TextEncoder": true + }, + "packages": { + "@metamask/utils>@metamask/superstruct": true, + "@metamask/utils>@scure/base": true, + "@metamask/utils>pony-cause": true, + "@noble/hashes": true, + "browserify>buffer": true, + "nock>debug": true, + "semver": true + } + }, "@metamask/eth-json-rpc-filters>@metamask/json-rpc-engine>@metamask/utils": { "globals": { "TextDecoder": true, @@ -886,14 +919,20 @@ "setTimeout": true }, "packages": { + "@metamask/eth-json-rpc-middleware>@metamask/rpc-errors": true, "@metamask/eth-json-rpc-middleware>klona": true, "@metamask/eth-json-rpc-middleware>safe-stable-stringify": true, "@metamask/eth-sig-util": true, - "@metamask/rpc-errors": true, "@metamask/snaps-controllers>@metamask/json-rpc-engine": true, "@metamask/utils": true } }, + "@metamask/eth-json-rpc-middleware>@metamask/rpc-errors": { + "packages": { + "@metamask/rpc-errors>fast-safe-stringify": true, + "@metamask/utils": true + } + }, "@metamask/eth-ledger-bridge-keyring": { "globals": { "addEventListener": true, @@ -1505,9 +1544,9 @@ "@metamask/network-controller>@metamask/eth-json-rpc-infura": true, "@metamask/network-controller>@metamask/eth-json-rpc-middleware": true, "@metamask/network-controller>@metamask/eth-json-rpc-provider": true, + "@metamask/network-controller>@metamask/rpc-errors": true, "@metamask/network-controller>@metamask/swappable-obj-proxy": true, "@metamask/network-controller>reselect": true, - "@metamask/rpc-errors": true, "@metamask/snaps-controllers>@metamask/json-rpc-engine": true, "@metamask/utils": true, "browserify>assert": true, @@ -1551,8 +1590,8 @@ "packages": { "@metamask/network-controller>@metamask/eth-json-rpc-infura>@metamask/eth-json-rpc-provider": true, "@metamask/network-controller>@metamask/eth-json-rpc-infura>@metamask/json-rpc-engine": true, + "@metamask/network-controller>@metamask/eth-json-rpc-infura>@metamask/rpc-errors": true, "@metamask/network-controller>@metamask/eth-json-rpc-infura>@metamask/utils": true, - "@metamask/rpc-errors": true, "node-fetch": true } }, @@ -1564,11 +1603,32 @@ }, "@metamask/network-controller>@metamask/eth-json-rpc-infura>@metamask/json-rpc-engine": { "packages": { + "@metamask/network-controller>@metamask/eth-json-rpc-infura>@metamask/rpc-errors": true, "@metamask/network-controller>@metamask/eth-json-rpc-infura>@metamask/utils": true, - "@metamask/rpc-errors": true, "@metamask/safe-event-emitter": true } }, + "@metamask/network-controller>@metamask/eth-json-rpc-infura>@metamask/rpc-errors": { + "packages": { + "@metamask/network-controller>@metamask/eth-json-rpc-infura>@metamask/rpc-errors>@metamask/utils": true, + "@metamask/rpc-errors>fast-safe-stringify": true + } + }, + "@metamask/network-controller>@metamask/eth-json-rpc-infura>@metamask/rpc-errors>@metamask/utils": { + "globals": { + "TextDecoder": true, + "TextEncoder": true + }, + "packages": { + "@metamask/utils>@metamask/superstruct": true, + "@metamask/utils>@scure/base": true, + "@metamask/utils>pony-cause": true, + "@noble/hashes": true, + "browserify>buffer": true, + "nock>debug": true, + "semver": true + } + }, "@metamask/network-controller>@metamask/eth-json-rpc-infura>@metamask/utils": { "globals": { "TextDecoder": true, @@ -1595,7 +1655,7 @@ "@metamask/eth-json-rpc-middleware>safe-stable-stringify": true, "@metamask/eth-sig-util": true, "@metamask/network-controller>@metamask/eth-json-rpc-middleware>@metamask/utils": true, - "@metamask/rpc-errors": true, + "@metamask/network-controller>@metamask/rpc-errors": true, "@metamask/snaps-controllers>@metamask/json-rpc-engine": true, "bn.js": true, "pify": true @@ -1618,12 +1678,24 @@ }, "@metamask/network-controller>@metamask/eth-json-rpc-provider": { "packages": { - "@metamask/rpc-errors": true, + "@metamask/network-controller>@metamask/eth-json-rpc-provider>@metamask/rpc-errors": true, "@metamask/safe-event-emitter": true, "@metamask/snaps-controllers>@metamask/json-rpc-engine": true, "uuid": true } }, + "@metamask/network-controller>@metamask/eth-json-rpc-provider>@metamask/rpc-errors": { + "packages": { + "@metamask/rpc-errors>fast-safe-stringify": true, + "@metamask/utils": true + } + }, + "@metamask/network-controller>@metamask/rpc-errors": { + "packages": { + "@metamask/rpc-errors>fast-safe-stringify": true, + "@metamask/utils": true + } + }, "@metamask/network-controller>reselect": { "globals": { "WeakRef": true, @@ -1832,9 +1904,9 @@ "packages": { "@metamask/controller-utils": true, "@metamask/permission-controller>@metamask/base-controller": true, + "@metamask/permission-controller>@metamask/rpc-errors": true, "@metamask/permission-controller>@metamask/utils": true, "@metamask/permission-controller>nanoid": true, - "@metamask/rpc-errors": true, "@metamask/snaps-controllers>@metamask/json-rpc-engine": true, "deep-freeze-strict": true, "immer": true @@ -1848,6 +1920,27 @@ "immer": true } }, + "@metamask/permission-controller>@metamask/rpc-errors": { + "packages": { + "@metamask/permission-controller>@metamask/rpc-errors>@metamask/utils": true, + "@metamask/rpc-errors>fast-safe-stringify": true + } + }, + "@metamask/permission-controller>@metamask/rpc-errors>@metamask/utils": { + "globals": { + "TextDecoder": true, + "TextEncoder": true + }, + "packages": { + "@metamask/utils>@metamask/superstruct": true, + "@metamask/utils>@scure/base": true, + "@metamask/utils>pony-cause": true, + "@noble/hashes": true, + "browserify>buffer": true, + "nock>debug": true, + "semver": true + } + }, "@metamask/permission-controller>@metamask/utils": { "globals": { "TextDecoder": true, @@ -1938,9 +2031,9 @@ "@metamask/eth-query>json-rpc-random-id": true, "@metamask/ppom-validator>@metamask/base-controller": true, "@metamask/ppom-validator>@metamask/controller-utils": true, + "@metamask/ppom-validator>@metamask/rpc-errors": true, "@metamask/ppom-validator>crypto-js": true, "@metamask/ppom-validator>elliptic": true, - "@metamask/rpc-errors": true, "await-semaphore": true, "browserify>buffer": true } @@ -1971,6 +2064,27 @@ "eth-ens-namehash": true } }, + "@metamask/ppom-validator>@metamask/rpc-errors": { + "packages": { + "@metamask/ppom-validator>@metamask/rpc-errors>@metamask/utils": true, + "@metamask/rpc-errors>fast-safe-stringify": true + } + }, + "@metamask/ppom-validator>@metamask/rpc-errors>@metamask/utils": { + "globals": { + "TextDecoder": true, + "TextEncoder": true + }, + "packages": { + "@metamask/utils>@metamask/superstruct": true, + "@metamask/utils>@scure/base": true, + "@metamask/utils>pony-cause": true, + "@noble/hashes": true, + "browserify>buffer": true, + "nock>debug": true, + "semver": true + } + }, "@metamask/ppom-validator>@metamask/utils": { "globals": { "TextDecoder": true, @@ -2096,8 +2210,8 @@ "@metamask/queued-request-controller": { "packages": { "@metamask/queued-request-controller>@metamask/base-controller": true, + "@metamask/queued-request-controller>@metamask/rpc-errors": true, "@metamask/queued-request-controller>@metamask/utils": true, - "@metamask/rpc-errors": true, "@metamask/selected-network-controller": true, "@metamask/snaps-controllers>@metamask/json-rpc-engine": true } @@ -2110,6 +2224,27 @@ "immer": true } }, + "@metamask/queued-request-controller>@metamask/rpc-errors": { + "packages": { + "@metamask/queued-request-controller>@metamask/rpc-errors>@metamask/utils": true, + "@metamask/rpc-errors>fast-safe-stringify": true + } + }, + "@metamask/queued-request-controller>@metamask/rpc-errors>@metamask/utils": { + "globals": { + "TextDecoder": true, + "TextEncoder": true + }, + "packages": { + "@metamask/utils>@metamask/superstruct": true, + "@metamask/utils>@scure/base": true, + "@metamask/utils>pony-cause": true, + "@noble/hashes": true, + "browserify>buffer": true, + "nock>debug": true, + "semver": true + } + }, "@metamask/queued-request-controller>@metamask/utils": { "globals": { "TextDecoder": true, @@ -2131,8 +2266,8 @@ }, "packages": { "@metamask/rate-limit-controller>@metamask/base-controller": true, - "@metamask/rate-limit-controller>@metamask/utils": true, - "@metamask/rpc-errors": true + "@metamask/rate-limit-controller>@metamask/rpc-errors": true, + "@metamask/rate-limit-controller>@metamask/utils": true } }, "@metamask/rate-limit-controller>@metamask/base-controller": { @@ -2143,6 +2278,27 @@ "immer": true } }, + "@metamask/rate-limit-controller>@metamask/rpc-errors": { + "packages": { + "@metamask/rate-limit-controller>@metamask/rpc-errors>@metamask/utils": true, + "@metamask/rpc-errors>fast-safe-stringify": true + } + }, + "@metamask/rate-limit-controller>@metamask/rpc-errors>@metamask/utils": { + "globals": { + "TextDecoder": true, + "TextEncoder": true + }, + "packages": { + "@metamask/utils>@metamask/superstruct": true, + "@metamask/utils>@scure/base": true, + "@metamask/utils>pony-cause": true, + "@noble/hashes": true, + "browserify>buffer": true, + "nock>debug": true, + "semver": true + } + }, "@metamask/rate-limit-controller>@metamask/utils": { "globals": { "TextDecoder": true, @@ -2160,8 +2316,8 @@ }, "@metamask/rpc-errors": { "packages": { - "@metamask/utils": true, - "eth-rpc-errors>fast-safe-stringify": true + "@metamask/rpc-errors>fast-safe-stringify": true, + "@metamask/utils": true } }, "@metamask/rpc-methods-flask>nanoid": { @@ -2310,11 +2466,11 @@ "@metamask/metamask-eth-abis": true, "@metamask/name-controller>async-mutex": true, "@metamask/network-controller": true, - "@metamask/rpc-errors": true, "@metamask/smart-transactions-controller>@metamask/base-controller": true, "@metamask/smart-transactions-controller>@metamask/transaction-controller>@ethereumjs/tx": true, "@metamask/smart-transactions-controller>@metamask/transaction-controller>@ethereumjs/util": true, "@metamask/smart-transactions-controller>@metamask/transaction-controller>@metamask/nonce-tracker": true, + "@metamask/smart-transactions-controller>@metamask/transaction-controller>@metamask/rpc-errors": true, "@metamask/smart-transactions-controller>@metamask/transaction-controller>@metamask/utils": true, "@metamask/smart-transactions-controller>@metamask/transaction-controller>eth-method-registry": true, "bn.js": true, @@ -2364,6 +2520,12 @@ "@swc/helpers>tslib": true } }, + "@metamask/smart-transactions-controller>@metamask/transaction-controller>@metamask/rpc-errors": { + "packages": { + "@metamask/rpc-errors>fast-safe-stringify": true, + "@metamask/utils": true + } + }, "@metamask/smart-transactions-controller>@metamask/transaction-controller>@metamask/utils": { "globals": { "TextDecoder": true, @@ -2449,11 +2611,11 @@ "packages": { "@metamask/object-multiplex": true, "@metamask/post-message-stream": true, - "@metamask/rpc-errors": true, "@metamask/snaps-controllers>@metamask/base-controller": true, "@metamask/snaps-controllers>@metamask/json-rpc-engine": true, "@metamask/snaps-controllers>@metamask/json-rpc-middleware-stream": true, "@metamask/snaps-controllers>@metamask/permission-controller": true, + "@metamask/snaps-controllers>@metamask/rpc-errors": true, "@metamask/snaps-controllers>@xstate/fsm": true, "@metamask/snaps-controllers>concat-stream": true, "@metamask/snaps-controllers>get-npm-tarball-url": true, @@ -2486,8 +2648,14 @@ }, "@metamask/snaps-controllers>@metamask/json-rpc-engine": { "packages": { - "@metamask/rpc-errors": true, "@metamask/safe-event-emitter": true, + "@metamask/snaps-controllers>@metamask/json-rpc-engine>@metamask/rpc-errors": true, + "@metamask/utils": true + } + }, + "@metamask/snaps-controllers>@metamask/json-rpc-engine>@metamask/rpc-errors": { + "packages": { + "@metamask/rpc-errors>fast-safe-stringify": true, "@metamask/utils": true } }, @@ -2508,15 +2676,21 @@ }, "packages": { "@metamask/controller-utils": true, - "@metamask/rpc-errors": true, "@metamask/snaps-controllers>@metamask/base-controller": true, "@metamask/snaps-controllers>@metamask/json-rpc-engine": true, + "@metamask/snaps-controllers>@metamask/rpc-errors": true, "@metamask/snaps-controllers>nanoid": true, "@metamask/utils": true, "deep-freeze-strict": true, "immer": true } }, + "@metamask/snaps-controllers>@metamask/rpc-errors": { + "packages": { + "@metamask/rpc-errors>fast-safe-stringify": true, + "@metamask/utils": true + } + }, "@metamask/snaps-controllers>concat-stream": { "packages": { "browserify>buffer": true, @@ -2574,8 +2748,8 @@ }, "@metamask/snaps-rpc-methods": { "packages": { - "@metamask/rpc-errors": true, "@metamask/snaps-rpc-methods>@metamask/permission-controller": true, + "@metamask/snaps-rpc-methods>@metamask/rpc-errors": true, "@metamask/snaps-sdk": true, "@metamask/snaps-sdk>@metamask/key-tree": true, "@metamask/snaps-utils": true, @@ -2590,10 +2764,10 @@ }, "packages": { "@metamask/controller-utils": true, - "@metamask/rpc-errors": true, "@metamask/snaps-controllers>@metamask/json-rpc-engine": true, "@metamask/snaps-rpc-methods>@metamask/permission-controller>@metamask/base-controller": true, "@metamask/snaps-rpc-methods>@metamask/permission-controller>nanoid": true, + "@metamask/snaps-rpc-methods>@metamask/rpc-errors": true, "@metamask/utils": true, "deep-freeze-strict": true, "immer": true @@ -2612,12 +2786,18 @@ "crypto.getRandomValues": true } }, + "@metamask/snaps-rpc-methods>@metamask/rpc-errors": { + "packages": { + "@metamask/rpc-errors>fast-safe-stringify": true, + "@metamask/utils": true + } + }, "@metamask/snaps-sdk": { "globals": { "fetch": true }, "packages": { - "@metamask/rpc-errors": true, + "@metamask/snaps-sdk>@metamask/rpc-errors": true, "@metamask/utils": true, "@metamask/utils>@metamask/superstruct": true } @@ -2631,6 +2811,12 @@ "@noble/hashes": true } }, + "@metamask/snaps-sdk>@metamask/rpc-errors": { + "packages": { + "@metamask/rpc-errors>fast-safe-stringify": true, + "@metamask/utils": true + } + }, "@metamask/snaps-utils": { "globals": { "File": true, @@ -2647,10 +2833,10 @@ "fetch": true }, "packages": { - "@metamask/rpc-errors": true, "@metamask/snaps-sdk": true, "@metamask/snaps-sdk>@metamask/key-tree": true, "@metamask/snaps-utils>@metamask/permission-controller": true, + "@metamask/snaps-utils>@metamask/rpc-errors": true, "@metamask/snaps-utils>@metamask/slip44": true, "@metamask/snaps-utils>cron-parser": true, "@metamask/snaps-utils>fast-json-stable-stringify": true, @@ -2680,10 +2866,10 @@ }, "packages": { "@metamask/controller-utils": true, - "@metamask/rpc-errors": true, "@metamask/snaps-controllers>@metamask/json-rpc-engine": true, "@metamask/snaps-utils>@metamask/base-controller": true, "@metamask/snaps-utils>@metamask/permission-controller>nanoid": true, + "@metamask/snaps-utils>@metamask/rpc-errors": true, "@metamask/utils": true, "deep-freeze-strict": true, "immer": true @@ -2694,6 +2880,12 @@ "crypto.getRandomValues": true } }, + "@metamask/snaps-utils>@metamask/rpc-errors": { + "packages": { + "@metamask/rpc-errors>fast-safe-stringify": true, + "@metamask/utils": true + } + }, "@metamask/snaps-utils>@metamask/snaps-registry": { "packages": { "@metamask/message-signing-snap>@noble/curves": true, @@ -2766,8 +2958,8 @@ "@metamask/metamask-eth-abis": true, "@metamask/name-controller>async-mutex": true, "@metamask/network-controller": true, - "@metamask/rpc-errors": true, "@metamask/transaction-controller>@metamask/nonce-tracker": true, + "@metamask/transaction-controller>@metamask/rpc-errors": true, "@metamask/utils": true, "bn.js": true, "browserify>buffer": true, @@ -2794,6 +2986,12 @@ "@swc/helpers>tslib": true } }, + "@metamask/transaction-controller>@metamask/rpc-errors": { + "packages": { + "@metamask/rpc-errors>fast-safe-stringify": true, + "@metamask/utils": true + } + }, "@metamask/user-operation-controller": { "globals": { "fetch": true @@ -2803,9 +3001,9 @@ "@metamask/eth-query": true, "@metamask/gas-fee-controller": true, "@metamask/gas-fee-controller>@metamask/polling-controller": true, - "@metamask/rpc-errors": true, "@metamask/transaction-controller": true, "@metamask/user-operation-controller>@metamask/base-controller": true, + "@metamask/user-operation-controller>@metamask/rpc-errors": true, "@metamask/user-operation-controller>@metamask/utils": true, "bn.js": true, "lodash": true, @@ -2822,6 +3020,27 @@ "immer": true } }, + "@metamask/user-operation-controller>@metamask/rpc-errors": { + "packages": { + "@metamask/rpc-errors>fast-safe-stringify": true, + "@metamask/user-operation-controller>@metamask/rpc-errors>@metamask/utils": true + } + }, + "@metamask/user-operation-controller>@metamask/rpc-errors>@metamask/utils": { + "globals": { + "TextDecoder": true, + "TextEncoder": true + }, + "packages": { + "@metamask/utils>@metamask/superstruct": true, + "@metamask/utils>@scure/base": true, + "@metamask/utils>pony-cause": true, + "@noble/hashes": true, + "browserify>buffer": true, + "nock>debug": true, + "semver": true + } + }, "@metamask/user-operation-controller>@metamask/utils": { "globals": { "TextDecoder": true, @@ -4067,11 +4286,6 @@ "@metamask/ethjs-query": true } }, - "eth-rpc-errors": { - "packages": { - "eth-rpc-errors>fast-safe-stringify": true - } - }, "ethereumjs-util": { "packages": { "bn.js": true, @@ -4525,8 +4739,8 @@ }, "json-rpc-engine": { "packages": { - "eth-rpc-errors": true, - "json-rpc-engine>@metamask/safe-event-emitter": true + "json-rpc-engine>@metamask/safe-event-emitter": true, + "json-rpc-engine>eth-rpc-errors": true } }, "json-rpc-engine>@metamask/safe-event-emitter": { @@ -4537,6 +4751,11 @@ "webpack>events": true } }, + "json-rpc-engine>eth-rpc-errors": { + "packages": { + "@metamask/rpc-errors>fast-safe-stringify": true + } + }, "json-rpc-middleware-stream": { "globals": { "console.warn": true, diff --git a/lavamoat/browserify/main/policy.json b/lavamoat/browserify/main/policy.json index d7522783f9fc..880b542673ea 100644 --- a/lavamoat/browserify/main/policy.json +++ b/lavamoat/browserify/main/policy.json @@ -662,8 +662,8 @@ }, "packages": { "@metamask/approval-controller>@metamask/base-controller": true, - "@metamask/approval-controller>nanoid": true, - "@metamask/rpc-errors": true + "@metamask/approval-controller>@metamask/rpc-errors": true, + "@metamask/approval-controller>nanoid": true } }, "@metamask/approval-controller>@metamask/base-controller": { @@ -674,6 +674,12 @@ "immer": true } }, + "@metamask/approval-controller>@metamask/rpc-errors": { + "packages": { + "@metamask/rpc-errors>fast-safe-stringify": true, + "@metamask/utils": true + } + }, "@metamask/approval-controller>nanoid": { "globals": { "crypto.getRandomValues": true @@ -699,13 +705,13 @@ "@ethersproject/providers": true, "@metamask/abi-utils": true, "@metamask/assets-controllers>@metamask/polling-controller": true, + "@metamask/assets-controllers>@metamask/rpc-errors": true, "@metamask/base-controller": true, "@metamask/contract-metadata": true, "@metamask/controller-utils": true, "@metamask/eth-query": true, "@metamask/metamask-eth-abis": true, "@metamask/name-controller>async-mutex": true, - "@metamask/rpc-errors": true, "@metamask/utils": true, "bn.js": true, "cockatiel": true, @@ -727,6 +733,12 @@ "uuid": true } }, + "@metamask/assets-controllers>@metamask/rpc-errors": { + "packages": { + "@metamask/rpc-errors>fast-safe-stringify": true, + "@metamask/utils": true + } + }, "@metamask/base-controller": { "globals": { "setTimeout": true @@ -851,11 +863,32 @@ }, "@metamask/eth-json-rpc-filters>@metamask/json-rpc-engine": { "packages": { + "@metamask/eth-json-rpc-filters>@metamask/json-rpc-engine>@metamask/rpc-errors": true, "@metamask/eth-json-rpc-filters>@metamask/json-rpc-engine>@metamask/utils": true, - "@metamask/rpc-errors": true, "@metamask/safe-event-emitter": true } }, + "@metamask/eth-json-rpc-filters>@metamask/json-rpc-engine>@metamask/rpc-errors": { + "packages": { + "@metamask/eth-json-rpc-filters>@metamask/json-rpc-engine>@metamask/rpc-errors>@metamask/utils": true, + "@metamask/rpc-errors>fast-safe-stringify": true + } + }, + "@metamask/eth-json-rpc-filters>@metamask/json-rpc-engine>@metamask/rpc-errors>@metamask/utils": { + "globals": { + "TextDecoder": true, + "TextEncoder": true + }, + "packages": { + "@metamask/utils>@metamask/superstruct": true, + "@metamask/utils>@scure/base": true, + "@metamask/utils>pony-cause": true, + "@noble/hashes": true, + "browserify>buffer": true, + "nock>debug": true, + "semver": true + } + }, "@metamask/eth-json-rpc-filters>@metamask/json-rpc-engine>@metamask/utils": { "globals": { "TextDecoder": true, @@ -886,14 +919,20 @@ "setTimeout": true }, "packages": { + "@metamask/eth-json-rpc-middleware>@metamask/rpc-errors": true, "@metamask/eth-json-rpc-middleware>klona": true, "@metamask/eth-json-rpc-middleware>safe-stable-stringify": true, "@metamask/eth-sig-util": true, - "@metamask/rpc-errors": true, "@metamask/snaps-controllers>@metamask/json-rpc-engine": true, "@metamask/utils": true } }, + "@metamask/eth-json-rpc-middleware>@metamask/rpc-errors": { + "packages": { + "@metamask/rpc-errors>fast-safe-stringify": true, + "@metamask/utils": true + } + }, "@metamask/eth-ledger-bridge-keyring": { "globals": { "addEventListener": true, @@ -1505,9 +1544,9 @@ "@metamask/network-controller>@metamask/eth-json-rpc-infura": true, "@metamask/network-controller>@metamask/eth-json-rpc-middleware": true, "@metamask/network-controller>@metamask/eth-json-rpc-provider": true, + "@metamask/network-controller>@metamask/rpc-errors": true, "@metamask/network-controller>@metamask/swappable-obj-proxy": true, "@metamask/network-controller>reselect": true, - "@metamask/rpc-errors": true, "@metamask/snaps-controllers>@metamask/json-rpc-engine": true, "@metamask/utils": true, "browserify>assert": true, @@ -1551,8 +1590,8 @@ "packages": { "@metamask/network-controller>@metamask/eth-json-rpc-infura>@metamask/eth-json-rpc-provider": true, "@metamask/network-controller>@metamask/eth-json-rpc-infura>@metamask/json-rpc-engine": true, + "@metamask/network-controller>@metamask/eth-json-rpc-infura>@metamask/rpc-errors": true, "@metamask/network-controller>@metamask/eth-json-rpc-infura>@metamask/utils": true, - "@metamask/rpc-errors": true, "node-fetch": true } }, @@ -1564,11 +1603,32 @@ }, "@metamask/network-controller>@metamask/eth-json-rpc-infura>@metamask/json-rpc-engine": { "packages": { + "@metamask/network-controller>@metamask/eth-json-rpc-infura>@metamask/rpc-errors": true, "@metamask/network-controller>@metamask/eth-json-rpc-infura>@metamask/utils": true, - "@metamask/rpc-errors": true, "@metamask/safe-event-emitter": true } }, + "@metamask/network-controller>@metamask/eth-json-rpc-infura>@metamask/rpc-errors": { + "packages": { + "@metamask/network-controller>@metamask/eth-json-rpc-infura>@metamask/rpc-errors>@metamask/utils": true, + "@metamask/rpc-errors>fast-safe-stringify": true + } + }, + "@metamask/network-controller>@metamask/eth-json-rpc-infura>@metamask/rpc-errors>@metamask/utils": { + "globals": { + "TextDecoder": true, + "TextEncoder": true + }, + "packages": { + "@metamask/utils>@metamask/superstruct": true, + "@metamask/utils>@scure/base": true, + "@metamask/utils>pony-cause": true, + "@noble/hashes": true, + "browserify>buffer": true, + "nock>debug": true, + "semver": true + } + }, "@metamask/network-controller>@metamask/eth-json-rpc-infura>@metamask/utils": { "globals": { "TextDecoder": true, @@ -1595,7 +1655,7 @@ "@metamask/eth-json-rpc-middleware>safe-stable-stringify": true, "@metamask/eth-sig-util": true, "@metamask/network-controller>@metamask/eth-json-rpc-middleware>@metamask/utils": true, - "@metamask/rpc-errors": true, + "@metamask/network-controller>@metamask/rpc-errors": true, "@metamask/snaps-controllers>@metamask/json-rpc-engine": true, "bn.js": true, "pify": true @@ -1618,12 +1678,24 @@ }, "@metamask/network-controller>@metamask/eth-json-rpc-provider": { "packages": { - "@metamask/rpc-errors": true, + "@metamask/network-controller>@metamask/eth-json-rpc-provider>@metamask/rpc-errors": true, "@metamask/safe-event-emitter": true, "@metamask/snaps-controllers>@metamask/json-rpc-engine": true, "uuid": true } }, + "@metamask/network-controller>@metamask/eth-json-rpc-provider>@metamask/rpc-errors": { + "packages": { + "@metamask/rpc-errors>fast-safe-stringify": true, + "@metamask/utils": true + } + }, + "@metamask/network-controller>@metamask/rpc-errors": { + "packages": { + "@metamask/rpc-errors>fast-safe-stringify": true, + "@metamask/utils": true + } + }, "@metamask/network-controller>reselect": { "globals": { "WeakRef": true, @@ -1832,9 +1904,9 @@ "packages": { "@metamask/controller-utils": true, "@metamask/permission-controller>@metamask/base-controller": true, + "@metamask/permission-controller>@metamask/rpc-errors": true, "@metamask/permission-controller>@metamask/utils": true, "@metamask/permission-controller>nanoid": true, - "@metamask/rpc-errors": true, "@metamask/snaps-controllers>@metamask/json-rpc-engine": true, "deep-freeze-strict": true, "immer": true @@ -1848,6 +1920,27 @@ "immer": true } }, + "@metamask/permission-controller>@metamask/rpc-errors": { + "packages": { + "@metamask/permission-controller>@metamask/rpc-errors>@metamask/utils": true, + "@metamask/rpc-errors>fast-safe-stringify": true + } + }, + "@metamask/permission-controller>@metamask/rpc-errors>@metamask/utils": { + "globals": { + "TextDecoder": true, + "TextEncoder": true + }, + "packages": { + "@metamask/utils>@metamask/superstruct": true, + "@metamask/utils>@scure/base": true, + "@metamask/utils>pony-cause": true, + "@noble/hashes": true, + "browserify>buffer": true, + "nock>debug": true, + "semver": true + } + }, "@metamask/permission-controller>@metamask/utils": { "globals": { "TextDecoder": true, @@ -1938,9 +2031,9 @@ "@metamask/eth-query>json-rpc-random-id": true, "@metamask/ppom-validator>@metamask/base-controller": true, "@metamask/ppom-validator>@metamask/controller-utils": true, + "@metamask/ppom-validator>@metamask/rpc-errors": true, "@metamask/ppom-validator>crypto-js": true, "@metamask/ppom-validator>elliptic": true, - "@metamask/rpc-errors": true, "await-semaphore": true, "browserify>buffer": true } @@ -1971,6 +2064,27 @@ "eth-ens-namehash": true } }, + "@metamask/ppom-validator>@metamask/rpc-errors": { + "packages": { + "@metamask/ppom-validator>@metamask/rpc-errors>@metamask/utils": true, + "@metamask/rpc-errors>fast-safe-stringify": true + } + }, + "@metamask/ppom-validator>@metamask/rpc-errors>@metamask/utils": { + "globals": { + "TextDecoder": true, + "TextEncoder": true + }, + "packages": { + "@metamask/utils>@metamask/superstruct": true, + "@metamask/utils>@scure/base": true, + "@metamask/utils>pony-cause": true, + "@noble/hashes": true, + "browserify>buffer": true, + "nock>debug": true, + "semver": true + } + }, "@metamask/ppom-validator>@metamask/utils": { "globals": { "TextDecoder": true, @@ -2096,8 +2210,8 @@ "@metamask/queued-request-controller": { "packages": { "@metamask/queued-request-controller>@metamask/base-controller": true, + "@metamask/queued-request-controller>@metamask/rpc-errors": true, "@metamask/queued-request-controller>@metamask/utils": true, - "@metamask/rpc-errors": true, "@metamask/selected-network-controller": true, "@metamask/snaps-controllers>@metamask/json-rpc-engine": true } @@ -2110,6 +2224,27 @@ "immer": true } }, + "@metamask/queued-request-controller>@metamask/rpc-errors": { + "packages": { + "@metamask/queued-request-controller>@metamask/rpc-errors>@metamask/utils": true, + "@metamask/rpc-errors>fast-safe-stringify": true + } + }, + "@metamask/queued-request-controller>@metamask/rpc-errors>@metamask/utils": { + "globals": { + "TextDecoder": true, + "TextEncoder": true + }, + "packages": { + "@metamask/utils>@metamask/superstruct": true, + "@metamask/utils>@scure/base": true, + "@metamask/utils>pony-cause": true, + "@noble/hashes": true, + "browserify>buffer": true, + "nock>debug": true, + "semver": true + } + }, "@metamask/queued-request-controller>@metamask/utils": { "globals": { "TextDecoder": true, @@ -2131,8 +2266,8 @@ }, "packages": { "@metamask/rate-limit-controller>@metamask/base-controller": true, - "@metamask/rate-limit-controller>@metamask/utils": true, - "@metamask/rpc-errors": true + "@metamask/rate-limit-controller>@metamask/rpc-errors": true, + "@metamask/rate-limit-controller>@metamask/utils": true } }, "@metamask/rate-limit-controller>@metamask/base-controller": { @@ -2143,6 +2278,27 @@ "immer": true } }, + "@metamask/rate-limit-controller>@metamask/rpc-errors": { + "packages": { + "@metamask/rate-limit-controller>@metamask/rpc-errors>@metamask/utils": true, + "@metamask/rpc-errors>fast-safe-stringify": true + } + }, + "@metamask/rate-limit-controller>@metamask/rpc-errors>@metamask/utils": { + "globals": { + "TextDecoder": true, + "TextEncoder": true + }, + "packages": { + "@metamask/utils>@metamask/superstruct": true, + "@metamask/utils>@scure/base": true, + "@metamask/utils>pony-cause": true, + "@noble/hashes": true, + "browserify>buffer": true, + "nock>debug": true, + "semver": true + } + }, "@metamask/rate-limit-controller>@metamask/utils": { "globals": { "TextDecoder": true, @@ -2160,8 +2316,8 @@ }, "@metamask/rpc-errors": { "packages": { - "@metamask/utils": true, - "eth-rpc-errors>fast-safe-stringify": true + "@metamask/rpc-errors>fast-safe-stringify": true, + "@metamask/utils": true } }, "@metamask/rpc-methods-flask>nanoid": { @@ -2310,11 +2466,11 @@ "@metamask/metamask-eth-abis": true, "@metamask/name-controller>async-mutex": true, "@metamask/network-controller": true, - "@metamask/rpc-errors": true, "@metamask/smart-transactions-controller>@metamask/base-controller": true, "@metamask/smart-transactions-controller>@metamask/transaction-controller>@ethereumjs/tx": true, "@metamask/smart-transactions-controller>@metamask/transaction-controller>@ethereumjs/util": true, "@metamask/smart-transactions-controller>@metamask/transaction-controller>@metamask/nonce-tracker": true, + "@metamask/smart-transactions-controller>@metamask/transaction-controller>@metamask/rpc-errors": true, "@metamask/smart-transactions-controller>@metamask/transaction-controller>@metamask/utils": true, "@metamask/smart-transactions-controller>@metamask/transaction-controller>eth-method-registry": true, "bn.js": true, @@ -2364,6 +2520,12 @@ "@swc/helpers>tslib": true } }, + "@metamask/smart-transactions-controller>@metamask/transaction-controller>@metamask/rpc-errors": { + "packages": { + "@metamask/rpc-errors>fast-safe-stringify": true, + "@metamask/utils": true + } + }, "@metamask/smart-transactions-controller>@metamask/transaction-controller>@metamask/utils": { "globals": { "TextDecoder": true, @@ -2449,11 +2611,11 @@ "packages": { "@metamask/object-multiplex": true, "@metamask/post-message-stream": true, - "@metamask/rpc-errors": true, "@metamask/snaps-controllers>@metamask/base-controller": true, "@metamask/snaps-controllers>@metamask/json-rpc-engine": true, "@metamask/snaps-controllers>@metamask/json-rpc-middleware-stream": true, "@metamask/snaps-controllers>@metamask/permission-controller": true, + "@metamask/snaps-controllers>@metamask/rpc-errors": true, "@metamask/snaps-controllers>@xstate/fsm": true, "@metamask/snaps-controllers>concat-stream": true, "@metamask/snaps-controllers>get-npm-tarball-url": true, @@ -2486,8 +2648,14 @@ }, "@metamask/snaps-controllers>@metamask/json-rpc-engine": { "packages": { - "@metamask/rpc-errors": true, "@metamask/safe-event-emitter": true, + "@metamask/snaps-controllers>@metamask/json-rpc-engine>@metamask/rpc-errors": true, + "@metamask/utils": true + } + }, + "@metamask/snaps-controllers>@metamask/json-rpc-engine>@metamask/rpc-errors": { + "packages": { + "@metamask/rpc-errors>fast-safe-stringify": true, "@metamask/utils": true } }, @@ -2508,15 +2676,21 @@ }, "packages": { "@metamask/controller-utils": true, - "@metamask/rpc-errors": true, "@metamask/snaps-controllers>@metamask/base-controller": true, "@metamask/snaps-controllers>@metamask/json-rpc-engine": true, + "@metamask/snaps-controllers>@metamask/rpc-errors": true, "@metamask/snaps-controllers>nanoid": true, "@metamask/utils": true, "deep-freeze-strict": true, "immer": true } }, + "@metamask/snaps-controllers>@metamask/rpc-errors": { + "packages": { + "@metamask/rpc-errors>fast-safe-stringify": true, + "@metamask/utils": true + } + }, "@metamask/snaps-controllers>concat-stream": { "packages": { "browserify>buffer": true, @@ -2574,8 +2748,8 @@ }, "@metamask/snaps-rpc-methods": { "packages": { - "@metamask/rpc-errors": true, "@metamask/snaps-rpc-methods>@metamask/permission-controller": true, + "@metamask/snaps-rpc-methods>@metamask/rpc-errors": true, "@metamask/snaps-sdk": true, "@metamask/snaps-sdk>@metamask/key-tree": true, "@metamask/snaps-utils": true, @@ -2590,10 +2764,10 @@ }, "packages": { "@metamask/controller-utils": true, - "@metamask/rpc-errors": true, "@metamask/snaps-controllers>@metamask/json-rpc-engine": true, "@metamask/snaps-rpc-methods>@metamask/permission-controller>@metamask/base-controller": true, "@metamask/snaps-rpc-methods>@metamask/permission-controller>nanoid": true, + "@metamask/snaps-rpc-methods>@metamask/rpc-errors": true, "@metamask/utils": true, "deep-freeze-strict": true, "immer": true @@ -2612,12 +2786,18 @@ "crypto.getRandomValues": true } }, + "@metamask/snaps-rpc-methods>@metamask/rpc-errors": { + "packages": { + "@metamask/rpc-errors>fast-safe-stringify": true, + "@metamask/utils": true + } + }, "@metamask/snaps-sdk": { "globals": { "fetch": true }, "packages": { - "@metamask/rpc-errors": true, + "@metamask/snaps-sdk>@metamask/rpc-errors": true, "@metamask/utils": true, "@metamask/utils>@metamask/superstruct": true } @@ -2631,6 +2811,12 @@ "@noble/hashes": true } }, + "@metamask/snaps-sdk>@metamask/rpc-errors": { + "packages": { + "@metamask/rpc-errors>fast-safe-stringify": true, + "@metamask/utils": true + } + }, "@metamask/snaps-utils": { "globals": { "File": true, @@ -2647,10 +2833,10 @@ "fetch": true }, "packages": { - "@metamask/rpc-errors": true, "@metamask/snaps-sdk": true, "@metamask/snaps-sdk>@metamask/key-tree": true, "@metamask/snaps-utils>@metamask/permission-controller": true, + "@metamask/snaps-utils>@metamask/rpc-errors": true, "@metamask/snaps-utils>@metamask/slip44": true, "@metamask/snaps-utils>cron-parser": true, "@metamask/snaps-utils>fast-json-stable-stringify": true, @@ -2680,10 +2866,10 @@ }, "packages": { "@metamask/controller-utils": true, - "@metamask/rpc-errors": true, "@metamask/snaps-controllers>@metamask/json-rpc-engine": true, "@metamask/snaps-utils>@metamask/base-controller": true, "@metamask/snaps-utils>@metamask/permission-controller>nanoid": true, + "@metamask/snaps-utils>@metamask/rpc-errors": true, "@metamask/utils": true, "deep-freeze-strict": true, "immer": true @@ -2694,6 +2880,12 @@ "crypto.getRandomValues": true } }, + "@metamask/snaps-utils>@metamask/rpc-errors": { + "packages": { + "@metamask/rpc-errors>fast-safe-stringify": true, + "@metamask/utils": true + } + }, "@metamask/snaps-utils>@metamask/snaps-registry": { "packages": { "@metamask/message-signing-snap>@noble/curves": true, @@ -2766,8 +2958,8 @@ "@metamask/metamask-eth-abis": true, "@metamask/name-controller>async-mutex": true, "@metamask/network-controller": true, - "@metamask/rpc-errors": true, "@metamask/transaction-controller>@metamask/nonce-tracker": true, + "@metamask/transaction-controller>@metamask/rpc-errors": true, "@metamask/utils": true, "bn.js": true, "browserify>buffer": true, @@ -2794,6 +2986,12 @@ "@swc/helpers>tslib": true } }, + "@metamask/transaction-controller>@metamask/rpc-errors": { + "packages": { + "@metamask/rpc-errors>fast-safe-stringify": true, + "@metamask/utils": true + } + }, "@metamask/user-operation-controller": { "globals": { "fetch": true @@ -2803,9 +3001,9 @@ "@metamask/eth-query": true, "@metamask/gas-fee-controller": true, "@metamask/gas-fee-controller>@metamask/polling-controller": true, - "@metamask/rpc-errors": true, "@metamask/transaction-controller": true, "@metamask/user-operation-controller>@metamask/base-controller": true, + "@metamask/user-operation-controller>@metamask/rpc-errors": true, "@metamask/user-operation-controller>@metamask/utils": true, "bn.js": true, "lodash": true, @@ -2822,6 +3020,27 @@ "immer": true } }, + "@metamask/user-operation-controller>@metamask/rpc-errors": { + "packages": { + "@metamask/rpc-errors>fast-safe-stringify": true, + "@metamask/user-operation-controller>@metamask/rpc-errors>@metamask/utils": true + } + }, + "@metamask/user-operation-controller>@metamask/rpc-errors>@metamask/utils": { + "globals": { + "TextDecoder": true, + "TextEncoder": true + }, + "packages": { + "@metamask/utils>@metamask/superstruct": true, + "@metamask/utils>@scure/base": true, + "@metamask/utils>pony-cause": true, + "@noble/hashes": true, + "browserify>buffer": true, + "nock>debug": true, + "semver": true + } + }, "@metamask/user-operation-controller>@metamask/utils": { "globals": { "TextDecoder": true, @@ -4067,11 +4286,6 @@ "@metamask/ethjs-query": true } }, - "eth-rpc-errors": { - "packages": { - "eth-rpc-errors>fast-safe-stringify": true - } - }, "ethereumjs-util": { "packages": { "bn.js": true, @@ -4525,8 +4739,8 @@ }, "json-rpc-engine": { "packages": { - "eth-rpc-errors": true, - "json-rpc-engine>@metamask/safe-event-emitter": true + "json-rpc-engine>@metamask/safe-event-emitter": true, + "json-rpc-engine>eth-rpc-errors": true } }, "json-rpc-engine>@metamask/safe-event-emitter": { @@ -4537,6 +4751,11 @@ "webpack>events": true } }, + "json-rpc-engine>eth-rpc-errors": { + "packages": { + "@metamask/rpc-errors>fast-safe-stringify": true + } + }, "json-rpc-middleware-stream": { "globals": { "console.warn": true, diff --git a/lavamoat/browserify/mmi/policy.json b/lavamoat/browserify/mmi/policy.json index 3df824f29c78..25756f84ccc4 100644 --- a/lavamoat/browserify/mmi/policy.json +++ b/lavamoat/browserify/mmi/policy.json @@ -754,8 +754,8 @@ }, "packages": { "@metamask/approval-controller>@metamask/base-controller": true, - "@metamask/approval-controller>nanoid": true, - "@metamask/rpc-errors": true + "@metamask/approval-controller>@metamask/rpc-errors": true, + "@metamask/approval-controller>nanoid": true } }, "@metamask/approval-controller>@metamask/base-controller": { @@ -766,6 +766,12 @@ "immer": true } }, + "@metamask/approval-controller>@metamask/rpc-errors": { + "packages": { + "@metamask/rpc-errors>fast-safe-stringify": true, + "@metamask/utils": true + } + }, "@metamask/approval-controller>nanoid": { "globals": { "crypto.getRandomValues": true @@ -791,13 +797,13 @@ "@ethersproject/providers": true, "@metamask/abi-utils": true, "@metamask/assets-controllers>@metamask/polling-controller": true, + "@metamask/assets-controllers>@metamask/rpc-errors": true, "@metamask/base-controller": true, "@metamask/contract-metadata": true, "@metamask/controller-utils": true, "@metamask/eth-query": true, "@metamask/metamask-eth-abis": true, "@metamask/name-controller>async-mutex": true, - "@metamask/rpc-errors": true, "@metamask/utils": true, "bn.js": true, "cockatiel": true, @@ -819,6 +825,12 @@ "uuid": true } }, + "@metamask/assets-controllers>@metamask/rpc-errors": { + "packages": { + "@metamask/rpc-errors>fast-safe-stringify": true, + "@metamask/utils": true + } + }, "@metamask/base-controller": { "globals": { "setTimeout": true @@ -943,11 +955,32 @@ }, "@metamask/eth-json-rpc-filters>@metamask/json-rpc-engine": { "packages": { + "@metamask/eth-json-rpc-filters>@metamask/json-rpc-engine>@metamask/rpc-errors": true, "@metamask/eth-json-rpc-filters>@metamask/json-rpc-engine>@metamask/utils": true, - "@metamask/rpc-errors": true, "@metamask/safe-event-emitter": true } }, + "@metamask/eth-json-rpc-filters>@metamask/json-rpc-engine>@metamask/rpc-errors": { + "packages": { + "@metamask/eth-json-rpc-filters>@metamask/json-rpc-engine>@metamask/rpc-errors>@metamask/utils": true, + "@metamask/rpc-errors>fast-safe-stringify": true + } + }, + "@metamask/eth-json-rpc-filters>@metamask/json-rpc-engine>@metamask/rpc-errors>@metamask/utils": { + "globals": { + "TextDecoder": true, + "TextEncoder": true + }, + "packages": { + "@metamask/utils>@metamask/superstruct": true, + "@metamask/utils>@scure/base": true, + "@metamask/utils>pony-cause": true, + "@noble/hashes": true, + "browserify>buffer": true, + "nock>debug": true, + "semver": true + } + }, "@metamask/eth-json-rpc-filters>@metamask/json-rpc-engine>@metamask/utils": { "globals": { "TextDecoder": true, @@ -978,14 +1011,20 @@ "setTimeout": true }, "packages": { + "@metamask/eth-json-rpc-middleware>@metamask/rpc-errors": true, "@metamask/eth-json-rpc-middleware>klona": true, "@metamask/eth-json-rpc-middleware>safe-stable-stringify": true, "@metamask/eth-sig-util": true, - "@metamask/rpc-errors": true, "@metamask/snaps-controllers>@metamask/json-rpc-engine": true, "@metamask/utils": true } }, + "@metamask/eth-json-rpc-middleware>@metamask/rpc-errors": { + "packages": { + "@metamask/rpc-errors>fast-safe-stringify": true, + "@metamask/utils": true + } + }, "@metamask/eth-ledger-bridge-keyring": { "globals": { "addEventListener": true, @@ -1597,9 +1636,9 @@ "@metamask/network-controller>@metamask/eth-json-rpc-infura": true, "@metamask/network-controller>@metamask/eth-json-rpc-middleware": true, "@metamask/network-controller>@metamask/eth-json-rpc-provider": true, + "@metamask/network-controller>@metamask/rpc-errors": true, "@metamask/network-controller>@metamask/swappable-obj-proxy": true, "@metamask/network-controller>reselect": true, - "@metamask/rpc-errors": true, "@metamask/snaps-controllers>@metamask/json-rpc-engine": true, "@metamask/utils": true, "browserify>assert": true, @@ -1643,8 +1682,8 @@ "packages": { "@metamask/network-controller>@metamask/eth-json-rpc-infura>@metamask/eth-json-rpc-provider": true, "@metamask/network-controller>@metamask/eth-json-rpc-infura>@metamask/json-rpc-engine": true, + "@metamask/network-controller>@metamask/eth-json-rpc-infura>@metamask/rpc-errors": true, "@metamask/network-controller>@metamask/eth-json-rpc-infura>@metamask/utils": true, - "@metamask/rpc-errors": true, "node-fetch": true } }, @@ -1656,11 +1695,32 @@ }, "@metamask/network-controller>@metamask/eth-json-rpc-infura>@metamask/json-rpc-engine": { "packages": { + "@metamask/network-controller>@metamask/eth-json-rpc-infura>@metamask/rpc-errors": true, "@metamask/network-controller>@metamask/eth-json-rpc-infura>@metamask/utils": true, - "@metamask/rpc-errors": true, "@metamask/safe-event-emitter": true } }, + "@metamask/network-controller>@metamask/eth-json-rpc-infura>@metamask/rpc-errors": { + "packages": { + "@metamask/network-controller>@metamask/eth-json-rpc-infura>@metamask/rpc-errors>@metamask/utils": true, + "@metamask/rpc-errors>fast-safe-stringify": true + } + }, + "@metamask/network-controller>@metamask/eth-json-rpc-infura>@metamask/rpc-errors>@metamask/utils": { + "globals": { + "TextDecoder": true, + "TextEncoder": true + }, + "packages": { + "@metamask/utils>@metamask/superstruct": true, + "@metamask/utils>@scure/base": true, + "@metamask/utils>pony-cause": true, + "@noble/hashes": true, + "browserify>buffer": true, + "nock>debug": true, + "semver": true + } + }, "@metamask/network-controller>@metamask/eth-json-rpc-infura>@metamask/utils": { "globals": { "TextDecoder": true, @@ -1687,7 +1747,7 @@ "@metamask/eth-json-rpc-middleware>safe-stable-stringify": true, "@metamask/eth-sig-util": true, "@metamask/network-controller>@metamask/eth-json-rpc-middleware>@metamask/utils": true, - "@metamask/rpc-errors": true, + "@metamask/network-controller>@metamask/rpc-errors": true, "@metamask/snaps-controllers>@metamask/json-rpc-engine": true, "bn.js": true, "pify": true @@ -1710,12 +1770,24 @@ }, "@metamask/network-controller>@metamask/eth-json-rpc-provider": { "packages": { - "@metamask/rpc-errors": true, + "@metamask/network-controller>@metamask/eth-json-rpc-provider>@metamask/rpc-errors": true, "@metamask/safe-event-emitter": true, "@metamask/snaps-controllers>@metamask/json-rpc-engine": true, "uuid": true } }, + "@metamask/network-controller>@metamask/eth-json-rpc-provider>@metamask/rpc-errors": { + "packages": { + "@metamask/rpc-errors>fast-safe-stringify": true, + "@metamask/utils": true + } + }, + "@metamask/network-controller>@metamask/rpc-errors": { + "packages": { + "@metamask/rpc-errors>fast-safe-stringify": true, + "@metamask/utils": true + } + }, "@metamask/network-controller>reselect": { "globals": { "WeakRef": true, @@ -1924,9 +1996,9 @@ "packages": { "@metamask/controller-utils": true, "@metamask/permission-controller>@metamask/base-controller": true, + "@metamask/permission-controller>@metamask/rpc-errors": true, "@metamask/permission-controller>@metamask/utils": true, "@metamask/permission-controller>nanoid": true, - "@metamask/rpc-errors": true, "@metamask/snaps-controllers>@metamask/json-rpc-engine": true, "deep-freeze-strict": true, "immer": true @@ -1940,6 +2012,27 @@ "immer": true } }, + "@metamask/permission-controller>@metamask/rpc-errors": { + "packages": { + "@metamask/permission-controller>@metamask/rpc-errors>@metamask/utils": true, + "@metamask/rpc-errors>fast-safe-stringify": true + } + }, + "@metamask/permission-controller>@metamask/rpc-errors>@metamask/utils": { + "globals": { + "TextDecoder": true, + "TextEncoder": true + }, + "packages": { + "@metamask/utils>@metamask/superstruct": true, + "@metamask/utils>@scure/base": true, + "@metamask/utils>pony-cause": true, + "@noble/hashes": true, + "browserify>buffer": true, + "nock>debug": true, + "semver": true + } + }, "@metamask/permission-controller>@metamask/utils": { "globals": { "TextDecoder": true, @@ -2030,9 +2123,9 @@ "@metamask/eth-query>json-rpc-random-id": true, "@metamask/ppom-validator>@metamask/base-controller": true, "@metamask/ppom-validator>@metamask/controller-utils": true, + "@metamask/ppom-validator>@metamask/rpc-errors": true, "@metamask/ppom-validator>crypto-js": true, "@metamask/ppom-validator>elliptic": true, - "@metamask/rpc-errors": true, "await-semaphore": true, "browserify>buffer": true } @@ -2063,6 +2156,27 @@ "eth-ens-namehash": true } }, + "@metamask/ppom-validator>@metamask/rpc-errors": { + "packages": { + "@metamask/ppom-validator>@metamask/rpc-errors>@metamask/utils": true, + "@metamask/rpc-errors>fast-safe-stringify": true + } + }, + "@metamask/ppom-validator>@metamask/rpc-errors>@metamask/utils": { + "globals": { + "TextDecoder": true, + "TextEncoder": true + }, + "packages": { + "@metamask/utils>@metamask/superstruct": true, + "@metamask/utils>@scure/base": true, + "@metamask/utils>pony-cause": true, + "@noble/hashes": true, + "browserify>buffer": true, + "nock>debug": true, + "semver": true + } + }, "@metamask/ppom-validator>@metamask/utils": { "globals": { "TextDecoder": true, @@ -2188,8 +2302,8 @@ "@metamask/queued-request-controller": { "packages": { "@metamask/queued-request-controller>@metamask/base-controller": true, + "@metamask/queued-request-controller>@metamask/rpc-errors": true, "@metamask/queued-request-controller>@metamask/utils": true, - "@metamask/rpc-errors": true, "@metamask/selected-network-controller": true, "@metamask/snaps-controllers>@metamask/json-rpc-engine": true } @@ -2202,6 +2316,27 @@ "immer": true } }, + "@metamask/queued-request-controller>@metamask/rpc-errors": { + "packages": { + "@metamask/queued-request-controller>@metamask/rpc-errors>@metamask/utils": true, + "@metamask/rpc-errors>fast-safe-stringify": true + } + }, + "@metamask/queued-request-controller>@metamask/rpc-errors>@metamask/utils": { + "globals": { + "TextDecoder": true, + "TextEncoder": true + }, + "packages": { + "@metamask/utils>@metamask/superstruct": true, + "@metamask/utils>@scure/base": true, + "@metamask/utils>pony-cause": true, + "@noble/hashes": true, + "browserify>buffer": true, + "nock>debug": true, + "semver": true + } + }, "@metamask/queued-request-controller>@metamask/utils": { "globals": { "TextDecoder": true, @@ -2223,8 +2358,8 @@ }, "packages": { "@metamask/rate-limit-controller>@metamask/base-controller": true, - "@metamask/rate-limit-controller>@metamask/utils": true, - "@metamask/rpc-errors": true + "@metamask/rate-limit-controller>@metamask/rpc-errors": true, + "@metamask/rate-limit-controller>@metamask/utils": true } }, "@metamask/rate-limit-controller>@metamask/base-controller": { @@ -2235,6 +2370,27 @@ "immer": true } }, + "@metamask/rate-limit-controller>@metamask/rpc-errors": { + "packages": { + "@metamask/rate-limit-controller>@metamask/rpc-errors>@metamask/utils": true, + "@metamask/rpc-errors>fast-safe-stringify": true + } + }, + "@metamask/rate-limit-controller>@metamask/rpc-errors>@metamask/utils": { + "globals": { + "TextDecoder": true, + "TextEncoder": true + }, + "packages": { + "@metamask/utils>@metamask/superstruct": true, + "@metamask/utils>@scure/base": true, + "@metamask/utils>pony-cause": true, + "@noble/hashes": true, + "browserify>buffer": true, + "nock>debug": true, + "semver": true + } + }, "@metamask/rate-limit-controller>@metamask/utils": { "globals": { "TextDecoder": true, @@ -2252,8 +2408,8 @@ }, "@metamask/rpc-errors": { "packages": { - "@metamask/utils": true, - "eth-rpc-errors>fast-safe-stringify": true + "@metamask/rpc-errors>fast-safe-stringify": true, + "@metamask/utils": true } }, "@metamask/rpc-methods-flask>nanoid": { @@ -2402,11 +2558,11 @@ "@metamask/metamask-eth-abis": true, "@metamask/name-controller>async-mutex": true, "@metamask/network-controller": true, - "@metamask/rpc-errors": true, "@metamask/smart-transactions-controller>@metamask/base-controller": true, "@metamask/smart-transactions-controller>@metamask/transaction-controller>@ethereumjs/tx": true, "@metamask/smart-transactions-controller>@metamask/transaction-controller>@ethereumjs/util": true, "@metamask/smart-transactions-controller>@metamask/transaction-controller>@metamask/nonce-tracker": true, + "@metamask/smart-transactions-controller>@metamask/transaction-controller>@metamask/rpc-errors": true, "@metamask/smart-transactions-controller>@metamask/transaction-controller>@metamask/utils": true, "@metamask/smart-transactions-controller>@metamask/transaction-controller>eth-method-registry": true, "bn.js": true, @@ -2456,6 +2612,12 @@ "@swc/helpers>tslib": true } }, + "@metamask/smart-transactions-controller>@metamask/transaction-controller>@metamask/rpc-errors": { + "packages": { + "@metamask/rpc-errors>fast-safe-stringify": true, + "@metamask/utils": true + } + }, "@metamask/smart-transactions-controller>@metamask/transaction-controller>@metamask/utils": { "globals": { "TextDecoder": true, @@ -2541,11 +2703,11 @@ "packages": { "@metamask/object-multiplex": true, "@metamask/post-message-stream": true, - "@metamask/rpc-errors": true, "@metamask/snaps-controllers>@metamask/base-controller": true, "@metamask/snaps-controllers>@metamask/json-rpc-engine": true, "@metamask/snaps-controllers>@metamask/json-rpc-middleware-stream": true, "@metamask/snaps-controllers>@metamask/permission-controller": true, + "@metamask/snaps-controllers>@metamask/rpc-errors": true, "@metamask/snaps-controllers>@xstate/fsm": true, "@metamask/snaps-controllers>concat-stream": true, "@metamask/snaps-controllers>get-npm-tarball-url": true, @@ -2578,8 +2740,14 @@ }, "@metamask/snaps-controllers>@metamask/json-rpc-engine": { "packages": { - "@metamask/rpc-errors": true, "@metamask/safe-event-emitter": true, + "@metamask/snaps-controllers>@metamask/json-rpc-engine>@metamask/rpc-errors": true, + "@metamask/utils": true + } + }, + "@metamask/snaps-controllers>@metamask/json-rpc-engine>@metamask/rpc-errors": { + "packages": { + "@metamask/rpc-errors>fast-safe-stringify": true, "@metamask/utils": true } }, @@ -2600,15 +2768,21 @@ }, "packages": { "@metamask/controller-utils": true, - "@metamask/rpc-errors": true, "@metamask/snaps-controllers>@metamask/base-controller": true, "@metamask/snaps-controllers>@metamask/json-rpc-engine": true, + "@metamask/snaps-controllers>@metamask/rpc-errors": true, "@metamask/snaps-controllers>nanoid": true, "@metamask/utils": true, "deep-freeze-strict": true, "immer": true } }, + "@metamask/snaps-controllers>@metamask/rpc-errors": { + "packages": { + "@metamask/rpc-errors>fast-safe-stringify": true, + "@metamask/utils": true + } + }, "@metamask/snaps-controllers>concat-stream": { "packages": { "browserify>buffer": true, @@ -2666,8 +2840,8 @@ }, "@metamask/snaps-rpc-methods": { "packages": { - "@metamask/rpc-errors": true, "@metamask/snaps-rpc-methods>@metamask/permission-controller": true, + "@metamask/snaps-rpc-methods>@metamask/rpc-errors": true, "@metamask/snaps-sdk": true, "@metamask/snaps-sdk>@metamask/key-tree": true, "@metamask/snaps-utils": true, @@ -2682,10 +2856,10 @@ }, "packages": { "@metamask/controller-utils": true, - "@metamask/rpc-errors": true, "@metamask/snaps-controllers>@metamask/json-rpc-engine": true, "@metamask/snaps-rpc-methods>@metamask/permission-controller>@metamask/base-controller": true, "@metamask/snaps-rpc-methods>@metamask/permission-controller>nanoid": true, + "@metamask/snaps-rpc-methods>@metamask/rpc-errors": true, "@metamask/utils": true, "deep-freeze-strict": true, "immer": true @@ -2704,12 +2878,18 @@ "crypto.getRandomValues": true } }, + "@metamask/snaps-rpc-methods>@metamask/rpc-errors": { + "packages": { + "@metamask/rpc-errors>fast-safe-stringify": true, + "@metamask/utils": true + } + }, "@metamask/snaps-sdk": { "globals": { "fetch": true }, "packages": { - "@metamask/rpc-errors": true, + "@metamask/snaps-sdk>@metamask/rpc-errors": true, "@metamask/utils": true, "@metamask/utils>@metamask/superstruct": true } @@ -2723,6 +2903,12 @@ "@noble/hashes": true } }, + "@metamask/snaps-sdk>@metamask/rpc-errors": { + "packages": { + "@metamask/rpc-errors>fast-safe-stringify": true, + "@metamask/utils": true + } + }, "@metamask/snaps-utils": { "globals": { "File": true, @@ -2739,10 +2925,10 @@ "fetch": true }, "packages": { - "@metamask/rpc-errors": true, "@metamask/snaps-sdk": true, "@metamask/snaps-sdk>@metamask/key-tree": true, "@metamask/snaps-utils>@metamask/permission-controller": true, + "@metamask/snaps-utils>@metamask/rpc-errors": true, "@metamask/snaps-utils>@metamask/slip44": true, "@metamask/snaps-utils>cron-parser": true, "@metamask/snaps-utils>fast-json-stable-stringify": true, @@ -2772,10 +2958,10 @@ }, "packages": { "@metamask/controller-utils": true, - "@metamask/rpc-errors": true, "@metamask/snaps-controllers>@metamask/json-rpc-engine": true, "@metamask/snaps-utils>@metamask/base-controller": true, "@metamask/snaps-utils>@metamask/permission-controller>nanoid": true, + "@metamask/snaps-utils>@metamask/rpc-errors": true, "@metamask/utils": true, "deep-freeze-strict": true, "immer": true @@ -2786,6 +2972,12 @@ "crypto.getRandomValues": true } }, + "@metamask/snaps-utils>@metamask/rpc-errors": { + "packages": { + "@metamask/rpc-errors>fast-safe-stringify": true, + "@metamask/utils": true + } + }, "@metamask/snaps-utils>@metamask/snaps-registry": { "packages": { "@metamask/message-signing-snap>@noble/curves": true, @@ -2858,8 +3050,8 @@ "@metamask/metamask-eth-abis": true, "@metamask/name-controller>async-mutex": true, "@metamask/network-controller": true, - "@metamask/rpc-errors": true, "@metamask/transaction-controller>@metamask/nonce-tracker": true, + "@metamask/transaction-controller>@metamask/rpc-errors": true, "@metamask/utils": true, "bn.js": true, "browserify>buffer": true, @@ -2886,6 +3078,12 @@ "@swc/helpers>tslib": true } }, + "@metamask/transaction-controller>@metamask/rpc-errors": { + "packages": { + "@metamask/rpc-errors>fast-safe-stringify": true, + "@metamask/utils": true + } + }, "@metamask/user-operation-controller": { "globals": { "fetch": true @@ -2895,9 +3093,9 @@ "@metamask/eth-query": true, "@metamask/gas-fee-controller": true, "@metamask/gas-fee-controller>@metamask/polling-controller": true, - "@metamask/rpc-errors": true, "@metamask/transaction-controller": true, "@metamask/user-operation-controller>@metamask/base-controller": true, + "@metamask/user-operation-controller>@metamask/rpc-errors": true, "@metamask/user-operation-controller>@metamask/utils": true, "bn.js": true, "lodash": true, @@ -2914,6 +3112,27 @@ "immer": true } }, + "@metamask/user-operation-controller>@metamask/rpc-errors": { + "packages": { + "@metamask/rpc-errors>fast-safe-stringify": true, + "@metamask/user-operation-controller>@metamask/rpc-errors>@metamask/utils": true + } + }, + "@metamask/user-operation-controller>@metamask/rpc-errors>@metamask/utils": { + "globals": { + "TextDecoder": true, + "TextEncoder": true + }, + "packages": { + "@metamask/utils>@metamask/superstruct": true, + "@metamask/utils>@scure/base": true, + "@metamask/utils>pony-cause": true, + "@noble/hashes": true, + "browserify>buffer": true, + "nock>debug": true, + "semver": true + } + }, "@metamask/user-operation-controller>@metamask/utils": { "globals": { "TextDecoder": true, @@ -4159,11 +4378,6 @@ "@metamask/ethjs-query": true } }, - "eth-rpc-errors": { - "packages": { - "eth-rpc-errors>fast-safe-stringify": true - } - }, "ethereumjs-util": { "packages": { "bn.js": true, @@ -4617,8 +4831,8 @@ }, "json-rpc-engine": { "packages": { - "eth-rpc-errors": true, - "json-rpc-engine>@metamask/safe-event-emitter": true + "json-rpc-engine>@metamask/safe-event-emitter": true, + "json-rpc-engine>eth-rpc-errors": true } }, "json-rpc-engine>@metamask/safe-event-emitter": { @@ -4629,6 +4843,11 @@ "webpack>events": true } }, + "json-rpc-engine>eth-rpc-errors": { + "packages": { + "@metamask/rpc-errors>fast-safe-stringify": true + } + }, "json-rpc-middleware-stream": { "globals": { "console.warn": true, diff --git a/lavamoat/build-system/policy.json b/lavamoat/build-system/policy.json index 6e3b319da1e8..e7ce64ceec23 100644 --- a/lavamoat/build-system/policy.json +++ b/lavamoat/build-system/policy.json @@ -1995,7 +1995,7 @@ "Buffer.isBuffer": true }, "packages": { - "eth-rpc-errors>fast-safe-stringify": true + "@metamask/rpc-errors>fast-safe-stringify": true } }, "browserify>string_decoder": { diff --git a/package.json b/package.json index 76dfb15c1ba7..fca1780d3300 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "metamask-crx", - "version": "12.4.0", + "version": "12.4.1", "private": true, "repository": { "type": "git", @@ -53,7 +53,7 @@ "test:e2e:chrome": "SELENIUM_BROWSER=chrome node test/e2e/run-all.js", "test:e2e:chrome:mmi": "SELENIUM_BROWSER=chrome node test/e2e/run-all.js --mmi", "test:e2e:chrome:flask": "SELENIUM_BROWSER=chrome node test/e2e/run-all.js --build-type flask", - "test:e2e:chrome:webpack": "ENABLE_MV3=false SELENIUM_BROWSER=chrome node test/e2e/run-all.js", + "test:e2e:chrome:webpack": "SELENIUM_BROWSER=chrome node test/e2e/run-all.js", "test:api-specs": "SELENIUM_BROWSER=chrome ts-node test/e2e/run-openrpc-api-test-coverage.ts", "test:e2e:mmi:ci": "yarn playwright test --project=mmi --project=mmi.visual", "test:e2e:mmi:all": "yarn playwright test --project=mmi && yarn test:e2e:mmi:visual", @@ -229,7 +229,7 @@ "semver@7.3.8": "^7.5.4", "@trezor/schema-utils@npm:1.0.2": "patch:@trezor/schema-utils@npm%3A1.0.2#~/.yarn/patches/@trezor-schema-utils-npm-1.0.2-7dd48689b2.patch", "lavamoat-core@npm:^15.1.1": "patch:lavamoat-core@npm%3A15.1.1#~/.yarn/patches/lavamoat-core-npm-15.1.1-51fbe39988.patch", - "@metamask/snaps-sdk": "^6.5.1", + "@metamask/snaps-sdk": "^6.9.0", "@swc/types@0.1.5": "^0.1.6", "@babel/runtime@npm:^7.7.6": "patch:@babel/runtime@npm%3A7.24.0#~/.yarn/patches/@babel-runtime-npm-7.24.0-7eb1dd11a2.patch", "@babel/runtime@npm:^7.9.2": "patch:@babel/runtime@npm%3A7.24.0#~/.yarn/patches/@babel-runtime-npm-7.24.0-7eb1dd11a2.patch", @@ -264,8 +264,7 @@ "@metamask/network-controller@npm:^17.0.0": "patch:@metamask/network-controller@npm%3A21.0.0#~/.yarn/patches/@metamask-network-controller-npm-21.0.0-559aa8e395.patch", "@metamask/network-controller@npm:^19.0.0": "patch:@metamask/network-controller@npm%3A21.0.0#~/.yarn/patches/@metamask-network-controller-npm-21.0.0-559aa8e395.patch", "@metamask/network-controller@npm:^20.0.0": "patch:@metamask/network-controller@npm%3A21.0.0#~/.yarn/patches/@metamask-network-controller-npm-21.0.0-559aa8e395.patch", - "path-to-regexp": "1.9.0", - "@metamask/snaps-utils@npm:^8.1.1": "patch:@metamask/snaps-utils@npm%3A8.1.1#~/.yarn/patches/@metamask-snaps-utils-npm-8.1.1-7d5dd6a26a.patch" + "path-to-regexp": "1.9.0" }, "dependencies": { "@babel/runtime": "patch:@babel/runtime@npm%3A7.24.0#~/.yarn/patches/@babel-runtime-npm-7.24.0-7eb1dd11a2.patch", @@ -286,15 +285,15 @@ "@lavamoat/lavadome-react": "0.0.17", "@lavamoat/snow": "^2.0.2", "@material-ui/core": "^4.11.0", - "@metamask-institutional/custody-controller": "^0.2.31", - "@metamask-institutional/custody-keyring": "^2.0.3", - "@metamask-institutional/extension": "^0.3.27", - "@metamask-institutional/institutional-features": "^1.3.5", + "@metamask-institutional/custody-controller": "^0.3.0", + "@metamask-institutional/custody-keyring": "^2.1.0", + "@metamask-institutional/extension": "^0.3.28", + "@metamask-institutional/institutional-features": "^1.3.6", "@metamask-institutional/portfolio-dashboard": "^1.4.1", "@metamask-institutional/rpc-allowlist": "^1.0.3", - "@metamask-institutional/sdk": "^0.1.30", - "@metamask-institutional/transaction-update": "^0.2.5", - "@metamask-institutional/types": "^1.1.0", + "@metamask-institutional/sdk": "^0.2.0", + "@metamask-institutional/transaction-update": "^0.2.6", + "@metamask-institutional/types": "^1.2.0", "@metamask/abi-utils": "^2.0.2", "@metamask/account-watcher": "^4.1.1", "@metamask/accounts-controller": "^18.2.2", @@ -303,7 +302,7 @@ "@metamask/approval-controller": "^7.0.0", "@metamask/assets-controllers": "patch:@metamask/assets-controllers@npm%3A38.3.0#~/.yarn/patches/@metamask-assets-controllers-npm-38.3.0-57b3d695bb.patch", "@metamask/base-controller": "^7.0.0", - "@metamask/bitcoin-wallet-snap": "^0.6.1", + "@metamask/bitcoin-wallet-snap": "^0.7.0", "@metamask/browser-passworder": "^4.3.0", "@metamask/contract-metadata": "^2.5.0", "@metamask/controller-utils": "^11.2.0", @@ -315,7 +314,7 @@ "@metamask/eth-ledger-bridge-keyring": "^3.0.1", "@metamask/eth-query": "^4.0.0", "@metamask/eth-sig-util": "^7.0.1", - "@metamask/eth-snap-keyring": "^4.3.6", + "@metamask/eth-snap-keyring": "^4.4.0", "@metamask/eth-token-tracker": "^8.0.0", "@metamask/eth-trezor-keyring": "^3.1.3", "@metamask/etherscan-link": "^3.0.0", @@ -342,22 +341,22 @@ "@metamask/phishing-controller": "^12.0.1", "@metamask/post-message-stream": "^8.0.0", "@metamask/ppom-validator": "0.34.0", - "@metamask/preinstalled-example-snap": "^0.1.0", + "@metamask/preinstalled-example-snap": "^0.2.0", "@metamask/profile-sync-controller": "^0.9.7", "@metamask/providers": "^14.0.2", "@metamask/queued-request-controller": "^2.0.0", "@metamask/rate-limit-controller": "^6.0.0", - "@metamask/rpc-errors": "^6.2.1", + "@metamask/rpc-errors": "^7.0.0", "@metamask/safe-event-emitter": "^3.1.1", "@metamask/scure-bip39": "^2.0.3", "@metamask/selected-network-controller": "^18.0.1", "@metamask/signature-controller": "^19.1.0", "@metamask/smart-transactions-controller": "^13.0.0", - "@metamask/snaps-controllers": "^9.7.0", - "@metamask/snaps-execution-environments": "^6.7.2", - "@metamask/snaps-rpc-methods": "^11.1.1", - "@metamask/snaps-sdk": "^6.5.1", - "@metamask/snaps-utils": "patch:@metamask/snaps-utils@npm%3A8.1.1#~/.yarn/patches/@metamask-snaps-utils-npm-8.1.1-7d5dd6a26a.patch", + "@metamask/snaps-controllers": "^9.11.1", + "@metamask/snaps-execution-environments": "^6.9.1", + "@metamask/snaps-rpc-methods": "^11.5.0", + "@metamask/snaps-sdk": "^6.9.0", + "@metamask/snaps-utils": "^8.4.1", "@metamask/transaction-controller": "^37.2.0", "@metamask/user-operation-controller": "^13.0.0", "@metamask/utils": "^9.3.0", @@ -388,10 +387,10 @@ "currency-formatter": "^1.4.2", "debounce-stream": "^2.0.0", "deep-freeze-strict": "1.1.1", + "eth-chainlist": "~0.0.498", "eth-ens-namehash": "^2.0.8", "eth-lattice-keyring": "^0.12.4", "eth-method-registry": "^4.0.0", - "eth-rpc-errors": "^4.0.2", "ethereumjs-util": "^7.0.10", "extension-port-stream": "^3.0.0", "fast-json-patch": "^3.1.1", @@ -479,7 +478,7 @@ "@metamask/phishing-warning": "^4.0.0", "@metamask/preferences-controller": "^13.0.2", "@metamask/test-bundler": "^1.0.0", - "@metamask/test-dapp": "^8.4.0", + "@metamask/test-dapp": "8.7.0", "@octokit/core": "^3.6.0", "@open-rpc/meta-schema": "^1.14.6", "@open-rpc/mock-server": "^1.7.5", diff --git a/privacy-snapshot.json b/privacy-snapshot.json index 37a05025382d..41b04a9b5210 100644 --- a/privacy-snapshot.json +++ b/privacy-snapshot.json @@ -1,4 +1,5 @@ [ + "*.btc*.quiknode.pro", "accounts.api.cx.metamask.io", "acl.execution.metamask.io", "api.blockchair.com", diff --git a/shared/constants/metametrics.ts b/shared/constants/metametrics.ts index 544d24ce1271..8107a1040127 100644 --- a/shared/constants/metametrics.ts +++ b/shared/constants/metametrics.ts @@ -742,7 +742,8 @@ export enum MetaMetricsEventName { sendFlowExited = 'Send Flow Exited', sendRecipientSelected = 'Send Recipient Selected', sendSwapQuoteError = 'Send Swap Quote Error', - sendSwapQuoteFetched = 'Send Swap Quote Fetched', + sendSwapQuoteRequested = 'Send Swap Quote Requested', + sendSwapQuoteReceived = 'Send Swap Quote Received', sendTokenModalOpened = 'Send Token Modal Opened', } diff --git a/shared/constants/network.ts b/shared/constants/network.ts index a98417794d81..9ed2e26150a9 100644 --- a/shared/constants/network.ts +++ b/shared/constants/network.ts @@ -96,6 +96,7 @@ export const NETWORK_NAMES = { HOMESTEAD: 'homestead', }; +export const CHAIN_SPEC_URL = 'https://chainid.network/chains.json'; /** * An object containing all of the chain ids for networks both built in and * those that we have added custom code to support our feature set. diff --git a/shared/lib/fetch-with-cache.ts b/shared/lib/fetch-with-cache.ts index 969fba9f869f..66610ec925b8 100644 --- a/shared/lib/fetch-with-cache.ts +++ b/shared/lib/fetch-with-cache.ts @@ -7,6 +7,7 @@ const fetchWithCache = async ({ fetchOptions = {}, cacheOptions: { cacheRefreshTime = MINUTE * 6, timeout = SECOND * 30 } = {}, functionName = '', + allowStale = false, }: { url: string; // TODO: Replace `any` with type @@ -16,6 +17,7 @@ const fetchWithCache = async ({ // eslint-disable-next-line @typescript-eslint/no-explicit-any cacheOptions?: Record; functionName: string; + allowStale?: boolean; }) => { if ( fetchOptions.body || @@ -49,6 +51,11 @@ const fetchWithCache = async ({ ...fetchOptions, }); if (!response.ok) { + const message = `Fetch with cache failed within function ${functionName} with status'${response.status}': '${response.statusText}'`; + if (allowStale) { + console.debug(`${message}. Returning cached result`); + return cachedResponse; + } throw new Error( `Fetch with cache failed within function ${functionName} with status'${response.status}': '${response.statusText}'`, ); diff --git a/shared/lib/four-byte.test.ts b/shared/lib/four-byte.test.ts index 2867aa2e51b7..77271c4aeba3 100644 --- a/shared/lib/four-byte.test.ts +++ b/shared/lib/four-byte.test.ts @@ -10,12 +10,14 @@ import { getMethodDataAsync, getMethodFrom4Byte } from './four-byte'; const FOUR_BYTE_MOCK = TRANSACTION_DATA_FOUR_BYTE.slice(0, 10); describe('Four Byte', () => { - const fetchMock = jest.fn(); - describe('getMethodFrom4Byte', () => { - it('returns signature with earliest creation date', async () => { + const fetchMock = jest.fn(); + + beforeEach(() => { jest.spyOn(global, 'fetch').mockImplementation(fetchMock); + }); + it('returns signature with earliest creation date', async () => { fetchMock.mockResolvedValue({ ok: true, json: async () => FOUR_BYTE_RESPONSE, @@ -44,6 +46,25 @@ describe('Four Byte', () => { expect(await getMethodFrom4Byte(prefix)).toBeUndefined(); }, ); + + // @ts-expect-error This is missing from the Mocha type definitions + it.each([ + ['undefined', { results: undefined }], + ['object', { results: {} }], + ['empty', { results: [] }], + ])( + 'returns `undefined` if fourByteResponse.results is %s', + async (_: string, mockResponse: { results: unknown }) => { + fetchMock.mockResolvedValue({ + ok: true, + json: async () => mockResponse, + }); + + const result = await getMethodFrom4Byte('0x913aa952'); + + expect(result).toBeUndefined(); + }, + ); }); describe('getMethodDataAsync', () => { diff --git a/shared/lib/four-byte.ts b/shared/lib/four-byte.ts index e28f4d4c0c5c..c6b9da22e617 100644 --- a/shared/lib/four-byte.ts +++ b/shared/lib/four-byte.ts @@ -34,6 +34,10 @@ export async function getMethodFrom4Byte( functionName: 'getMethodFrom4Byte', })) as FourByteResponse; + if (!fourByteResponse.results?.length) { + return undefined; + } + fourByteResponse.results.sort((a, b) => { return new Date(a.created_at).getTime() < new Date(b.created_at).getTime() ? -1 diff --git a/shared/modules/error.test.ts b/shared/modules/error.test.ts index 247ef302d09e..7fab2ee4e2e4 100644 --- a/shared/modules/error.test.ts +++ b/shared/modules/error.test.ts @@ -24,7 +24,7 @@ describe('error module', () => { expect(log.error).toHaveBeenCalledWith('test'); }); - it('calls loglevel.error with the parameter passed in when parameter is not an instance of Error', () => { + it('calls loglevel.error with string representation of parameter passed in when parameter is not an instance of Error', () => { logErrorWithMessage({ test: 'test' }); expect(log.error).toHaveBeenCalledWith({ test: 'test' }); }); diff --git a/shared/modules/error.ts b/shared/modules/error.ts index fa212365570f..04b754625257 100644 --- a/shared/modules/error.ts +++ b/shared/modules/error.ts @@ -1,24 +1,33 @@ import log from 'loglevel'; +import { + getErrorMessage as _getErrorMessage, + hasProperty, + isObject, + isErrorWithMessage, +} from '@metamask/utils'; + +export { isErrorWithMessage } from '@metamask/utils'; /** - * Type guard for determining whether the given value is an error object with a - * `message` property, such as an instance of Error. - * - * TODO: Remove once this becomes available at @metamask/utils + * Attempts to obtain the message from a possible error object, defaulting to an + * empty string if it is impossible to do so. * - * @param error - The object to check. - * @returns True or false, depending on the result. + * @param error - The possible error to get the message from. + * @returns The message if `error` is an object with a `message` property; + * the string version of `error` if it is not `undefined` or `null`; otherwise + * an empty string. */ -export function isErrorWithMessage( - error: unknown, -): error is { message: string } { - return typeof error === 'object' && error !== null && 'message' in error; +// TODO: Remove completely once changes implemented in @metamask/utils +export function getErrorMessage(error: unknown): string { + return isErrorWithMessage(error) && + hasProperty(error, 'cause') && + isObject(error.cause) && + hasProperty(error.cause, 'message') && + typeof error.cause.message === 'string' + ? error.cause.message + : _getErrorMessage(error); } export function logErrorWithMessage(error: unknown) { - if (isErrorWithMessage(error)) { - log.error(error.message); - } else { - log.error(error); - } + log.error(isErrorWithMessage(error) ? getErrorMessage(error) : error); } diff --git a/shared/modules/mv3.utils.js b/shared/modules/mv3.utils.js index 417484c46de6..24a4bc8880f4 100644 --- a/shared/modules/mv3.utils.js +++ b/shared/modules/mv3.utils.js @@ -6,14 +6,13 @@ const runtimeManifest = /** * A boolean indicating whether the manifest of the current extension is set to manifest version 3. * - * We have found that when this is run early in a service worker process, the runtime manifest is - * unavailable. That's why we have a fallback using the ENABLE_MV3 constant. The fallback is also - * used in unit tests. + * If this function is running in the Extension, it will use the runtime manifest. + * If this function is running in Node doing a build job, it will read process.env.ENABLE_MV3. + * If this function is running in Node doing an E2E test, it will `fs.readFileSync` the manifest.json file. */ const isManifestV3 = runtimeManifest ? runtimeManifest.manifest_version === 3 - : // Our build system sets this as a boolean, but in a Node.js context (e.g. unit tests) it will - // always be a string + : // Our build system sets this as a boolean, but in a Node.js context (e.g. unit tests) it can be a string process.env.ENABLE_MV3 === true || process.env.ENABLE_MV3 === 'true' || process.env.ENABLE_MV3 === undefined; diff --git a/sonar-project.properties b/sonar-project.properties index ad18a60d6fc7..4362539a94ff 100644 --- a/sonar-project.properties +++ b/sonar-project.properties @@ -1,3 +1,7 @@ +# Note: Updating this file on feature branches or forks will not reflect changes in the SonarCloud scan results. +# The SonarCloud scan workflow always uses the latest version from the default branch. +# This means any changes made to this file in a feature branch will not be considered until they are merged. + sonar.projectKey=metamask-extension sonar.organization=consensys diff --git a/test/data/confirmations/typed_sign.ts b/test/data/confirmations/typed_sign.ts index f02705a2540b..7be24a1389c6 100644 --- a/test/data/confirmations/typed_sign.ts +++ b/test/data/confirmations/typed_sign.ts @@ -183,6 +183,21 @@ export const permitSignatureMsg = { }, } as SignatureRequestType; +export const permitNFTSignatureMsg = { + id: 'c5067710-87cf-11ef-916c-71f266571322', + status: 'unapproved', + time: 1728651190529, + type: 'eth_signTypedData', + msgParams: { + data: '{"domain":{"name":"Uniswap V3 Positions NFT-V1","version":"1","chainId":1,"verifyingContract":"0xC36442b4a4522E871399CD717aBDD847Ab11FE88"},"types":{"Permit":[{"name":"spender","type":"address"},{"name":"tokenId","type":"uint256"},{"name":"nonce","type":"uint256"},{"name":"deadline","type":"uint256"}]},"primaryType":"Permit","message":{"spender":"0x00000000Ede6d8D217c60f93191C060747324bca","tokenId":"3606393","nonce":"0","deadline":"1734995006"}}', + from: '0x935e73edb9ff52e23bac7f7e043a1ecd06d05477', + version: 'V4', + signatureMethod: 'eth_signTypedData_v4', + requestId: 2874791875, + origin: 'https://metamask.github.io', + }, +} as SignatureRequestType; + export const permitSignatureMsgWithNoDeadline = { id: '0b1787a0-1c44-11ef-b70d-e7064bd7b659', securityAlertResponse: { diff --git a/test/e2e/accounts/common.ts b/test/e2e/accounts/common.ts index eda4ef5fbf6f..62f3fc082b53 100644 --- a/test/e2e/accounts/common.ts +++ b/test/e2e/accounts/common.ts @@ -10,7 +10,6 @@ import { unlockWallet, validateContractDetails, multipleGanacheOptions, - regularDelayMs, } from '../helpers'; import { Driver } from '../webdriver/driver'; import { DAPP_URL, TEST_SNAPS_SIMPLE_KEYRING_WEBSITE_URL } from '../constants'; @@ -67,23 +66,16 @@ export async function installSnapSimpleKeyring( await driver.clickElementSafe('[data-testid="snap-install-scroll"]', 200); - await driver.waitForSelector({ text: 'Confirm' }); - await driver.clickElement({ text: 'Confirm', tag: 'button', }); - await driver.waitForSelector({ text: 'OK' }); - - await driver.clickElement({ + await driver.clickElementAndWaitForWindowToClose({ text: 'OK', tag: 'button', }); - // Wait until popup is closed before proceeding - await driver.waitUntilXWindowHandles(2); - await driver.switchToWindowWithTitle(WINDOW_TITLES.SnapSimpleKeyringDapp); await driver.waitForSelector({ @@ -159,7 +151,7 @@ export async function makeNewAccountAndSwitch(driver: Driver) { text: 'Add account', }); // Click the ok button on the success modal - await driver.clickElement({ + await driver.clickElementAndWaitForWindowToClose({ css: '[data-testid="confirmation-submit-button"]', text: 'Ok', }); @@ -196,17 +188,40 @@ async function switchToAccount2(driver: Driver) { export async function connectAccountToTestDapp(driver: Driver) { await switchToOrOpenDapp(driver); - await driver.clickElement('#connectButton'); - await driver.delay(regularDelayMs); await driver.switchToWindowWithTitle(WINDOW_TITLES.Dialog); - await driver.clickElementAndWaitForWindowToClose({ + + // Extra steps needed to preserve the current network. + // Those can be removed once the issue is fixed (#27891) + const edit = await driver.findClickableElements({ + text: 'Edit', + tag: 'button', + }); + await edit[1].click(); + + await driver.clickElement({ + tag: 'p', + text: 'Localhost 8545', + }); + + await driver.clickElement({ + text: 'Update', + tag: 'button', + }); + + // Connect to the test dapp + await driver.clickElement({ text: 'Connect', tag: 'button', }); await driver.switchToWindowWithUrl(DAPP_URL); + // Ensure network is preserved after connecting + await driver.waitForSelector({ + css: '[id="chainId"]', + text: '0x539', + }); } export async function disconnectFromTestDapp(driver: Driver) { @@ -296,12 +311,8 @@ export async function signData( }, async () => { await switchToOrOpenDapp(driver); - await driver.clickElement(locatorID); - // take extra time to load the popup - await driver.delay(500); - await driver.switchToWindowWithTitle(WINDOW_TITLES.Dialog); }, ); diff --git a/test/e2e/accounts/snap-account-signatures.spec.ts b/test/e2e/accounts/snap-account-signatures.spec.ts deleted file mode 100644 index 536d8168b1a3..000000000000 --- a/test/e2e/accounts/snap-account-signatures.spec.ts +++ /dev/null @@ -1,51 +0,0 @@ -import { Suite } from 'mocha'; -import { - tempToggleSettingRedesignedConfirmations, - withFixtures, -} from '../helpers'; -import { Driver } from '../webdriver/driver'; -import { - accountSnapFixtures, - installSnapSimpleKeyring, - makeNewAccountAndSwitch, - signData, -} from './common'; - -describe('Snap Account Signatures', function (this: Suite) { - this.timeout(120000); // This test is very long, so we need an unusually high timeout - - // Run sync, async approve, and async reject flows - // (in Jest we could do this with test.each, but that does not exist here) - ['sync', 'approve', 'reject'].forEach((flowType) => { - // generate title of the test from flowType - const title = `can sign with ${flowType} flow`; - - it(title, async () => { - await withFixtures( - accountSnapFixtures(title), - async ({ driver }: { driver: Driver }) => { - const isAsyncFlow = flowType !== 'sync'; - - await installSnapSimpleKeyring(driver, isAsyncFlow); - - const newPublicKey = await makeNewAccountAndSwitch(driver); - - await tempToggleSettingRedesignedConfirmations(driver); - - // Run all 5 signature types - const locatorIDs = [ - '#personalSign', - '#signTypedData', - '#signTypedDataV3', - '#signTypedDataV4', - '#signPermit', - ]; - - for (const locatorID of locatorIDs) { - await signData(driver, locatorID, newPublicKey, flowType); - } - }, - ); - }); - }); -}); diff --git a/test/e2e/constants.ts b/test/e2e/constants.ts index 7e92a28cf463..c3957cb6fbbf 100644 --- a/test/e2e/constants.ts +++ b/test/e2e/constants.ts @@ -46,3 +46,6 @@ export const DAPP_ONE_URL = 'http://127.0.0.1:8081'; /* Default BTC address created using test SRP */ export const DEFAULT_BTC_ACCOUNT = 'bc1qg6whd6pc0cguh6gpp3ewujm53hv32ta9hdp252'; + +/* Default (mocked) BTC balance used by the Bitcoin RPC provider */ +export const DEFAULT_BTC_BALANCE = 1; // BTC diff --git a/test/e2e/flask/btc/btc-account-overview.spec.ts b/test/e2e/flask/btc/btc-account-overview.spec.ts index 5f0277c191de..24eedb60b6a2 100644 --- a/test/e2e/flask/btc/btc-account-overview.spec.ts +++ b/test/e2e/flask/btc/btc-account-overview.spec.ts @@ -1,4 +1,6 @@ +import { strict as assert } from 'assert'; import { Suite } from 'mocha'; +import { DEFAULT_BTC_BALANCE } from '../../constants'; import { withBtcAccountSnap } from './common-btc'; describe('BTC Account - Overview', function (this: Suite) { @@ -39,4 +41,23 @@ describe('BTC Account - Overview', function (this: Suite) { }, ); }); + + it('has balance', async function () { + await withBtcAccountSnap( + { title: this.test?.fullTitle() }, + async (driver) => { + // Wait for the balance to load up + await driver.delay(2000); + + const balanceElement = await driver.findElement( + '.coin-overview__balance', + ); + const balanceText = await balanceElement.getText(); + + const [balance, unit] = balanceText.split('\n'); + assert(Number(balance) === DEFAULT_BTC_BALANCE); + assert(unit === 'BTC'); + }, + ); + }); }); diff --git a/test/e2e/flask/btc/common-btc.ts b/test/e2e/flask/btc/common-btc.ts index a33ab1241a1c..15bf7d49eb0b 100644 --- a/test/e2e/flask/btc/common-btc.ts +++ b/test/e2e/flask/btc/common-btc.ts @@ -1,34 +1,36 @@ import { Mockttp } from 'mockttp'; import FixtureBuilder from '../../fixture-builder'; import { withFixtures, unlockWallet } from '../../helpers'; -import { DEFAULT_BTC_ACCOUNT } from '../../constants'; +import { DEFAULT_BTC_ACCOUNT, DEFAULT_BTC_BALANCE } from '../../constants'; import { MultichainNetworks } from '../../../../shared/constants/multichain/networks'; import { Driver } from '../../webdriver/driver'; import { createBtcAccount } from '../../accounts/common'; -const GENERATE_MOCK_BTC_BALANCE_CALL = ( - address: string = DEFAULT_BTC_ACCOUNT, -): { data: { [address: string]: number } } => { - return { - data: { - [address]: 9999, - }, - }; -}; - export async function mockBtcBalanceQuote( mockServer: Mockttp, address: string = DEFAULT_BTC_ACCOUNT, ) { return await mockServer - .forGet(/https:\/\/api\.blockchair\.com\/bitcoin\/addresses\/balances/u) - .withQuery({ - addresses: address, + .forPost(/^https:\/\/.*\.btc.*\.quiknode\.pro(\/|$)/u) + .withJsonBodyIncluding({ + method: 'bb_getaddress', }) - .thenCallback(() => ({ - statusCode: 200, - json: GENERATE_MOCK_BTC_BALANCE_CALL(address), - })); + .thenCallback(() => { + return { + statusCode: 200, + json: { + result: { + address, + balance: (DEFAULT_BTC_BALANCE * 1e8).toString(), // Converts from BTC to sats + totalReceived: '0', + totalSent: '0', + unconfirmedBalance: '0', + unconfirmedTxs: 0, + txs: 0, + }, + }, + }; + }); } export async function mockRampsDynamicFeatureFlag( diff --git a/test/e2e/helpers.js b/test/e2e/helpers.js index 65a405f5325d..c857838f0810 100644 --- a/test/e2e/helpers.js +++ b/test/e2e/helpers.js @@ -535,7 +535,10 @@ const onboardingRevealAndConfirmSRP = async (driver) => { await driver.clickElement('[data-testid="confirm-recovery-phrase"]'); - await driver.clickElement({ text: 'Confirm', tag: 'button' }); + await driver.clickElementAndWaitToDisappear({ + tag: 'button', + text: 'Confirm', + }); }; /** @@ -566,7 +569,7 @@ const onboardingCompleteWalletCreationWithOptOut = async (driver) => { await driver.findElement({ text: 'Congratulations!', tag: 'h2' }); // opt-out from third party API on general section - await driver.clickElement({ + await driver.clickElementAndWaitToDisappear({ text: 'Manage default privacy settings', tag: 'button', }); @@ -575,7 +578,10 @@ const onboardingCompleteWalletCreationWithOptOut = async (driver) => { '[data-testid="basic-functionality-toggle"] .toggle-button', ); await driver.clickElement('[id="basic-configuration-checkbox"]'); - await driver.clickElement({ text: 'Turn off', tag: 'button' }); + await driver.clickElementAndWaitToDisappear({ + tag: 'button', + text: 'Turn off', + }); // opt-out from third party API on assets section await driver.clickElement('[data-testid="category-back-button"]'); @@ -588,10 +594,19 @@ const onboardingCompleteWalletCreationWithOptOut = async (driver) => { ).map((toggle) => toggle.click()), ); await driver.clickElement('[data-testid="category-back-button"]'); + + // Wait until the onboarding carousel has stopped moving + // otherwise the click has no effect. + await driver.waitForElementToStopMoving( + '[data-testid="privacy-settings-back-button"]', + ); await driver.clickElement('[data-testid="privacy-settings-back-button"]'); // complete onboarding - await driver.clickElement({ text: 'Done', tag: 'button' }); + await driver.clickElementAndWaitToDisappear({ + tag: 'button', + text: 'Done', + }); await onboardingPinExtension(driver); }; @@ -1208,10 +1223,7 @@ async function tempToggleSettingRedesignedConfirmations(driver) { await driver.switchToWindowWithTitle(WINDOW_TITLES.ExtensionInFullScreenView); // Open settings menu button - const accountOptionsMenuSelector = - '[data-testid="account-options-menu-button"]'; - await driver.waitForSelector(accountOptionsMenuSelector); - await driver.clickElement(accountOptionsMenuSelector); + await driver.clickElement('[data-testid="account-options-menu-button"]'); // fix race condition with mmi build if (process.env.MMI) { diff --git a/test/e2e/manifest-flag-mocha-hooks.ts b/test/e2e/manifest-flag-mocha-hooks.ts index f319984eb067..f5c8d5e8099c 100644 --- a/test/e2e/manifest-flag-mocha-hooks.ts +++ b/test/e2e/manifest-flag-mocha-hooks.ts @@ -14,7 +14,9 @@ */ import fs from 'fs'; import { hasProperty } from '@metamask/utils'; -import { folder } from './set-manifest-flags'; +import { folder, getManifestVersion } from './set-manifest-flags'; + +process.env.ENABLE_MV3 = getManifestVersion() === 3 ? 'true' : 'false'; // Global beforeEach hook to backup the manifest.json file if (typeof beforeEach === 'function' && process.env.SELENIUM_BROWSER) { diff --git a/test/e2e/mock-e2e.js b/test/e2e/mock-e2e.js index 209777f32bd7..12d0fb293e15 100644 --- a/test/e2e/mock-e2e.js +++ b/test/e2e/mock-e2e.js @@ -66,6 +66,19 @@ const emptyHtmlPage = () => ` const browserAPIRequestDomains = /^.*\.(googleapis\.com|google\.com|mozilla\.net|mozilla\.com|mozilla\.org|gvt1\.com)$/iu; +/** + * Some third-party providers might use random URLs that we don't want to track + * in the privacy report "in clear". We identify those private hosts with a + * `pattern` regexp and replace the original host by a more generic one (`host`). + * For example, "my-secret-host.provider.com" could be denoted as "*.provider.com" in + * the privacy report. This would prevent disclosing the "my-secret-host" subdomain + * in this case. + */ +const privateHostMatchers = [ + // { pattern: RegExp, host: string } + { pattern: /^.*\.btc.*\.quiknode.pro$/iu, host: '*.btc*.quiknode.pro' }, +]; + /** * @typedef {import('mockttp').Mockttp} Mockttp * @typedef {import('mockttp').MockedEndpoint} MockedEndpoint @@ -712,6 +725,25 @@ async function setupMocking( const portfolioRequestsMatcher = (request) => request.headers.referer === 'https://portfolio.metamask.io/'; + /** + * Tests a request against private domains and returns a set of generic hostnames that + * match. + * + * @param request + * @returns A set of matched results. + */ + const matchPrivateHosts = (request) => { + const privateHosts = new Set(); + + for (const { pattern, host: privateHost } of privateHostMatchers) { + if (request.headers.host.match(pattern)) { + privateHosts.add(privateHost); + } + } + + return privateHosts; + }; + /** * Listen for requests and add the hostname to the privacy report if it did * not previously exist. This is used to track which hosts are requested @@ -721,6 +753,16 @@ async function setupMocking( * operation. See the browserAPIRequestDomains regex above. */ server.on('request-initiated', (request) => { + const privateHosts = matchPrivateHosts(request); + if (privateHosts.size) { + for (const privateHost of privateHosts) { + privacyReport.add(privateHost); + } + // At this point, we know the request at least one private doamin, so we just stops here to avoid + // using the request any further. + return; + } + if ( request.headers.host.match(browserAPIRequestDomains) === null && !portfolioRequestsMatcher(request) diff --git a/test/e2e/page-objects/flows/sign.flow.ts b/test/e2e/page-objects/flows/sign.flow.ts new file mode 100644 index 000000000000..c7d03bb4f96e --- /dev/null +++ b/test/e2e/page-objects/flows/sign.flow.ts @@ -0,0 +1,169 @@ +import { Driver } from '../../webdriver/driver'; +import { WINDOW_TITLES } from '../../helpers'; +import SnapSimpleKeyringPage from '../pages/snap-simple-keyring-page'; +import TestDapp from '../pages/test-dapp'; + +/** + * This function initiates the steps for a personal sign with snap account on test dapp. + * + * @param driver - The webdriver instance. + * @param publicAddress - The public address of the snap account. + * @param isSyncFlow - Indicates whether synchronous approval option is on for the snap. Defaults to true. + * @param approveTransaction - Indicates whether the transaction should be approved. Defaults to true. + */ +export const personalSignWithSnapAccount = async ( + driver: Driver, + publicAddress: string, + isSyncFlow: boolean = true, + approveTransaction: boolean = true, +): Promise => { + const testDapp = new TestDapp(driver); + await testDapp.check_pageIsLoaded(); + await testDapp.personalSign(); + if (!isSyncFlow) { + await driver.switchToWindowWithTitle(WINDOW_TITLES.Dialog); + await new SnapSimpleKeyringPage(driver).approveRejectSnapAccountTransaction( + approveTransaction, + true, + ); + } + if ((!isSyncFlow && approveTransaction) || isSyncFlow) { + await testDapp.check_successPersonalSign(publicAddress); + } else { + await testDapp.check_failedPersonalSign( + 'Error: Request rejected by user or snap.', + ); + } +}; + +/** + * This function initiates the steps for a signTypedData with snap account on test dapp. + * + * @param driver - The webdriver instance. + * @param publicAddress - The public address of the snap account. + * @param isSyncFlow - Indicates whether synchronous approval option is on for the snap. Defaults to true. + * @param approveTransaction - Indicates whether the transaction should be approved. Defaults to true. + */ +export const signTypedDataWithSnapAccount = async ( + driver: Driver, + publicAddress: string, + isSyncFlow: boolean = true, + approveTransaction: boolean = true, +): Promise => { + const testDapp = new TestDapp(driver); + await testDapp.check_pageIsLoaded(); + await testDapp.signTypedData(); + if (!isSyncFlow) { + await driver.switchToWindowWithTitle(WINDOW_TITLES.Dialog); + await new SnapSimpleKeyringPage(driver).approveRejectSnapAccountTransaction( + approveTransaction, + true, + ); + } + if ((!isSyncFlow && approveTransaction) || isSyncFlow) { + await testDapp.check_successSignTypedData(publicAddress); + } else { + await testDapp.check_failedSignTypedData( + 'Error: Request rejected by user or snap.', + ); + } +}; + +/** + * This function initiates the steps for a signTypedDataV3 with snap account on test dapp. + * + * @param driver - The webdriver instance. + * @param publicAddress - The public address of the snap account. + * @param isSyncFlow - Indicates whether synchronous approval option is on for the snap. Defaults to true. + * @param approveTransaction - Indicates whether the transaction should be approved. Defaults to true. + */ +export const signTypedDataV3WithSnapAccount = async ( + driver: Driver, + publicAddress: string, + isSyncFlow: boolean = true, + approveTransaction: boolean = true, +): Promise => { + const testDapp = new TestDapp(driver); + await testDapp.check_pageIsLoaded(); + await testDapp.signTypedDataV3(); + if (!isSyncFlow) { + await driver.switchToWindowWithTitle(WINDOW_TITLES.Dialog); + await new SnapSimpleKeyringPage(driver).approveRejectSnapAccountTransaction( + approveTransaction, + true, + ); + } + if ((!isSyncFlow && approveTransaction) || isSyncFlow) { + await testDapp.check_successSignTypedDataV3(publicAddress); + } else { + await testDapp.check_failedSignTypedDataV3( + 'Error: Request rejected by user or snap.', + ); + } +}; + +/** + * This function initiates the steps for a signTypedDataV4 with snap account on test dapp. + * + * @param driver - The webdriver instance. + * @param publicAddress - The public address of the snap account. + * @param isSyncFlow - Indicates whether synchronous approval option is on for the snap. Defaults to true. + * @param approveTransaction - Indicates whether the transaction should be approved. Defaults to true. + */ +export const signTypedDataV4WithSnapAccount = async ( + driver: Driver, + publicAddress: string, + isSyncFlow: boolean = true, + approveTransaction: boolean = true, +): Promise => { + const testDapp = new TestDapp(driver); + await testDapp.check_pageIsLoaded(); + await testDapp.signTypedDataV4(); + if (!isSyncFlow) { + await driver.switchToWindowWithTitle(WINDOW_TITLES.Dialog); + await new SnapSimpleKeyringPage(driver).approveRejectSnapAccountTransaction( + approveTransaction, + true, + ); + } + if ((!isSyncFlow && approveTransaction) || isSyncFlow) { + await testDapp.check_successSignTypedDataV4(publicAddress); + } else { + await testDapp.check_failedSignTypedDataV4( + 'Error: Request rejected by user or snap.', + ); + } +}; + +/** + * This function initiates the steps for a signPermit with snap account on test dapp. + * + * @param driver - The webdriver instance. + * @param publicAddress - The public address of the snap account. + * @param isSyncFlow - Indicates whether synchronous approval option is on for the snap. Defaults to true. + * @param approveTransaction - Indicates whether the transaction should be approved. Defaults to true. + */ +export const signPermitWithSnapAccount = async ( + driver: Driver, + publicAddress: string, + isSyncFlow: boolean = true, + approveTransaction: boolean = true, +): Promise => { + const testDapp = new TestDapp(driver); + await testDapp.check_pageIsLoaded(); + await testDapp.signPermit(); + if (!isSyncFlow) { + await driver.switchToWindowWithTitle(WINDOW_TITLES.Dialog); + await new SnapSimpleKeyringPage(driver).approveRejectSnapAccountTransaction( + approveTransaction, + true, + ); + } + if ((!isSyncFlow && approveTransaction) || isSyncFlow) { + await testDapp.check_successSignPermit(publicAddress); + } else { + await testDapp.check_failedSignPermit( + 'Error: Request rejected by user or snap.', + ); + } +}; diff --git a/test/e2e/page-objects/pages/experimental-settings.ts b/test/e2e/page-objects/pages/experimental-settings.ts index 8c7129b17555..7cd780229acd 100644 --- a/test/e2e/page-objects/pages/experimental-settings.ts +++ b/test/e2e/page-objects/pages/experimental-settings.ts @@ -9,9 +9,12 @@ class ExperimentalSettings { private readonly experimentalPageTitle: object = { text: 'Experimental', - css: '.h4', + tag: 'h4', }; + private readonly redesignedSignatureToggle: string = + '[data-testid="toggle-redesigned-confirmations-container"]'; + constructor(driver: Driver) { this.driver = driver; } @@ -33,6 +36,11 @@ class ExperimentalSettings { console.log('Toggle Add Account Snap on experimental setting page'); await this.driver.clickElement(this.addAccountSnapToggle); } + + async toggleRedesignedSignature(): Promise { + console.log('Toggle Redesigned Signature on experimental setting page'); + await this.driver.clickElement(this.redesignedSignatureToggle); + } } export default ExperimentalSettings; diff --git a/test/e2e/page-objects/pages/snap-simple-keyring-page.ts b/test/e2e/page-objects/pages/snap-simple-keyring-page.ts index 7f7a97d7d861..c75adb06da3a 100644 --- a/test/e2e/page-objects/pages/snap-simple-keyring-page.ts +++ b/test/e2e/page-objects/pages/snap-simple-keyring-page.ts @@ -9,11 +9,6 @@ class SnapSimpleKeyringPage { tag: 'h3', }; - private readonly accountSupportedMethods = { - text: 'Account Supported Methods', - tag: 'p', - }; - private readonly addtoMetamaskMessage = { text: 'Add to MetaMask', tag: 'h3', @@ -104,6 +99,11 @@ class SnapSimpleKeyringPage { tag: 'div', }; + private readonly newAccountMessage = { + text: '"address":', + tag: 'div', + }; + private readonly pageTitle = { text: 'Snap Simple Keyring', tag: 'p', @@ -161,16 +161,25 @@ class SnapSimpleKeyringPage { * Approves or rejects a transaction from a snap account on Snap Simple Keyring page. * * @param approveTransaction - Indicates if the transaction should be approved. Defaults to true. + * @param isSignatureRequest - Indicates if the request is a signature request. Defaults to false. */ async approveRejectSnapAccountTransaction( approveTransaction: boolean = true, + isSignatureRequest: boolean = false, ): Promise { console.log( 'Approve/Reject snap account transaction on Snap Simple Keyring page', ); - await this.driver.clickElementAndWaitToDisappear( - this.confirmationSubmitButton, - ); + if (isSignatureRequest) { + await this.driver.clickElementAndWaitForWindowToClose( + this.confirmationSubmitButton, + ); + } else { + // For send eth requests, the origin screen is not closed automatically, so we cannot call clickElementAndWaitForWindowToClose here. + await this.driver.clickElementAndWaitToDisappear( + this.confirmationSubmitButton, + ); + } await this.driver.switchToWindowWithTitle( WINDOW_TITLES.SnapSimpleKeyringDapp, ); @@ -242,7 +251,7 @@ class SnapSimpleKeyringPage { await this.driver.switchToWindowWithTitle( WINDOW_TITLES.SnapSimpleKeyringDapp, ); - await this.check_accountSupportedMethodsDisplayed(); + await this.driver.waitForSelector(this.newAccountMessage); } async confirmCreateSnapOnConfirmationScreen(): Promise { @@ -255,15 +264,21 @@ class SnapSimpleKeyringPage { * * @param accountName - Optional: name for the snap account. Defaults to "SSK Account". * @param isFirstAccount - Indicates if this is the first snap account being created. Defaults to true. + * @returns the public key of the new created account */ async createNewAccount( accountName: string = 'SSK Account', isFirstAccount: boolean = true, - ): Promise { + ): Promise { console.log('Create new account on Snap Simple Keyring page'); await this.openCreateSnapAccountConfirmationScreen(isFirstAccount); await this.confirmCreateSnapOnConfirmationScreen(); await this.confirmAddAccountDialog(accountName); + const newAccountJSONMessage = await ( + await this.driver.waitForSelector(this.newAccountMessage) + ).getText(); + const newPublicKey = JSON.parse(newAccountJSONMessage).address; + return newPublicKey; } /** @@ -331,13 +346,6 @@ class SnapSimpleKeyringPage { await this.driver.clickElement(this.useSyncApprovalToggle); } - async check_accountSupportedMethodsDisplayed(): Promise { - console.log( - 'Check new created account supported methods are displayed on simple keyring snap page', - ); - await this.driver.waitForSelector(this.accountSupportedMethods); - } - async check_errorRequestMessageDisplayed(): Promise { console.log( 'Check error request message is displayed on snap simple keyring page', diff --git a/test/e2e/page-objects/pages/test-dapp.ts b/test/e2e/page-objects/pages/test-dapp.ts index 89ee6bc9cbd3..ffb1f9033bdb 100644 --- a/test/e2e/page-objects/pages/test-dapp.ts +++ b/test/e2e/page-objects/pages/test-dapp.ts @@ -1,5 +1,5 @@ import { Driver } from '../../webdriver/driver'; -import { RawLocator } from '../common'; +import { WINDOW_TITLES } from '../../helpers'; const DAPP_HOST_ADDRESS = '127.0.0.1:8080'; const DAPP_URL = `http://${DAPP_HOST_ADDRESS}`; @@ -7,40 +7,120 @@ const DAPP_URL = `http://${DAPP_HOST_ADDRESS}`; class TestDapp { private driver: Driver; - private erc721SetApprovalForAllButton: RawLocator; + private readonly confirmDialogScrollButton = + '[data-testid="signature-request-scroll-button"]'; - private erc1155SetApprovalForAllButton: RawLocator; + private readonly confirmSignatureButton = + '[data-testid="page-container-footer-next"]'; - private erc721RevokeSetApprovalForAllButton: RawLocator; + private readonly erc1155RevokeSetApprovalForAllButton = + '#revokeERC1155Button'; - private erc1155RevokeSetApprovalForAllButton: RawLocator; + private readonly erc1155SetApprovalForAllButton = + '#setApprovalForAllERC1155Button'; + + private readonly erc721RevokeSetApprovalForAllButton = '#revokeButton'; + + private readonly erc721SetApprovalForAllButton = '#setApprovalForAllButton'; + + private readonly mmlogo = '#mm-logo'; + + private readonly personalSignButton = '#personalSign'; + + private readonly personalSignResult = '#personalSignVerifyECRecoverResult'; + + private readonly personalSignSignatureRequestMessage = { + text: 'personal_sign', + tag: 'div', + }; + + private readonly personalSignVerifyButton = '#personalSignVerify'; + + private readonly signPermitButton = '#signPermit'; + + private readonly signPermitResult = '#signPermitResult'; + + private readonly signPermitSignatureRequestMessage = { + text: 'Permit', + tag: 'p', + }; + + private readonly signPermitVerifyButton = '#signPermitVerify'; + + private readonly signPermitVerifyResult = '#signPermitVerifyResult'; + + private readonly signTypedDataButton = '#signTypedData'; + + private readonly signTypedDataResult = '#signTypedDataResult'; + + private readonly signTypedDataSignatureRequestMessage = { + text: 'Hi, Alice!', + tag: 'div', + }; + + private readonly signTypedDataV3Button = '#signTypedDataV3'; + + private readonly signTypedDataV3Result = '#signTypedDataV3Result'; + + private readonly signTypedDataV3V4SignatureRequestMessage = { + text: 'Hello, Bob!', + tag: 'div', + }; + + private readonly signTypedDataV3VerifyButton = '#signTypedDataV3Verify'; + + private readonly signTypedDataV3VerifyResult = '#signTypedDataV3VerifyResult'; + + private readonly signTypedDataV4Button = '#signTypedDataV4'; + + private readonly signTypedDataV4Result = '#signTypedDataV4Result'; + + private readonly signTypedDataV4VerifyButton = '#signTypedDataV4Verify'; + + private readonly signTypedDataV4VerifyResult = '#signTypedDataV4VerifyResult'; + + private readonly signTypedDataVerifyButton = '#signTypedDataVerify'; + + private readonly signTypedDataVerifyResult = '#signTypedDataVerifyResult'; constructor(driver: Driver) { this.driver = driver; + } - this.erc721SetApprovalForAllButton = '#setApprovalForAllButton'; - this.erc1155SetApprovalForAllButton = '#setApprovalForAllERC1155Button'; - this.erc721RevokeSetApprovalForAllButton = '#revokeButton'; - this.erc1155RevokeSetApprovalForAllButton = '#revokeERC1155Button'; + async check_pageIsLoaded(): Promise { + try { + await this.driver.waitForSelector(this.mmlogo); + } catch (e) { + console.log('Timeout while waiting for Test Dapp page to be loaded', e); + throw e; + } + console.log('Test Dapp page is loaded'); } - async open({ - contractAddress, + /** + * Open the test dapp page. + * + * @param options - The options for opening the test dapp page. + * @param options.contractAddress - The contract address to open the dapp with. Defaults to null. + * @param options.url - The URL of the dapp. Defaults to DAPP_URL. + * @returns A promise that resolves when the new page is opened. + */ + async openTestDappPage({ + contractAddress = null, url = DAPP_URL, }: { - contractAddress?: string; + contractAddress?: string | null; url?: string; - }) { + } = {}): Promise { const dappUrl = contractAddress ? `${url}/?contract=${contractAddress}` : url; - - return await this.driver.openNewPage(dappUrl); + await this.driver.openNewPage(dappUrl); } // eslint-disable-next-line @typescript-eslint/no-explicit-any async request(method: string, params: any[]) { - await this.open({ + await this.openTestDappPage({ url: `${DAPP_URL}/request?method=${method}¶ms=${JSON.stringify( params, )}`, @@ -55,13 +135,230 @@ class TestDapp { await this.driver.clickElement(this.erc1155SetApprovalForAllButton); } - public async clickERC721RevokeSetApprovalForAllButton() { + async clickERC721RevokeSetApprovalForAllButton() { await this.driver.clickElement(this.erc721RevokeSetApprovalForAllButton); } - public async clickERC1155RevokeSetApprovalForAllButton() { + async clickERC1155RevokeSetApprovalForAllButton() { await this.driver.clickElement(this.erc1155RevokeSetApprovalForAllButton); } -} + /** + * Verify the failed personal sign signature. + * + * @param expectedFailedMessage - The expected failed message. + */ + async check_failedPersonalSign(expectedFailedMessage: string) { + console.log('Verify failed personal sign signature'); + await this.driver.switchToWindowWithTitle(WINDOW_TITLES.TestDApp); + await this.driver.waitForSelector({ + css: this.personalSignButton, + text: expectedFailedMessage, + }); + } + + /** + * Verify the failed signPermit signature. + * + * @param expectedFailedMessage - The expected failed message. + */ + async check_failedSignPermit(expectedFailedMessage: string) { + console.log('Verify failed signPermit signature'); + await this.driver.switchToWindowWithTitle(WINDOW_TITLES.TestDApp); + await this.driver.waitForSelector({ + css: this.signPermitResult, + text: expectedFailedMessage, + }); + } + + /** + * Verify the failed signTypedData signature. + * + * @param expectedFailedMessage - The expected failed message. + */ + async check_failedSignTypedData(expectedFailedMessage: string) { + console.log('Verify failed signTypedData signature'); + await this.driver.switchToWindowWithTitle(WINDOW_TITLES.TestDApp); + await this.driver.waitForSelector({ + css: this.signTypedDataResult, + text: expectedFailedMessage, + }); + } + + /** + * Verify the failed signTypedDataV3 signature. + * + * @param expectedFailedMessage - The expected failed message. + */ + async check_failedSignTypedDataV3(expectedFailedMessage: string) { + console.log('Verify failed signTypedDataV3 signature'); + await this.driver.switchToWindowWithTitle(WINDOW_TITLES.TestDApp); + await this.driver.waitForSelector({ + css: this.signTypedDataV3Result, + text: expectedFailedMessage, + }); + } + + /** + * Verify the failed signTypedDataV4 signature. + * + * @param expectedFailedMessage - The expected failed message. + */ + async check_failedSignTypedDataV4(expectedFailedMessage: string) { + console.log('Verify failed signTypedDataV4 signature'); + await this.driver.switchToWindowWithTitle(WINDOW_TITLES.TestDApp); + await this.driver.waitForSelector({ + css: this.signTypedDataV4Result, + text: expectedFailedMessage, + }); + } + + /** + * Verify the successful personal sign signature. + * + * @param publicKey - The public key to verify the signature with. + */ + async check_successPersonalSign(publicKey: string) { + console.log('Verify successful personal sign signature'); + await this.driver.switchToWindowWithTitle(WINDOW_TITLES.TestDApp); + await this.driver.clickElement(this.personalSignVerifyButton); + await this.driver.waitForSelector({ + css: this.personalSignResult, + text: publicKey.toLowerCase(), + }); + } + + /** + * Verify the successful signPermit signature. + * + * @param publicKey - The public key to verify the signature with. + */ + async check_successSignPermit(publicKey: string) { + console.log('Verify successful signPermit signature'); + await this.driver.switchToWindowWithTitle(WINDOW_TITLES.TestDApp); + await this.driver.clickElement(this.signPermitVerifyButton); + await this.driver.waitForSelector({ + css: this.signPermitVerifyResult, + text: publicKey.toLowerCase(), + }); + } + + /** + * Verify the successful signTypedData signature. + * + * @param publicKey - The public key to verify the signature with. + */ + async check_successSignTypedData(publicKey: string) { + console.log('Verify successful signTypedData signature'); + await this.driver.switchToWindowWithTitle(WINDOW_TITLES.TestDApp); + await this.driver.clickElement(this.signTypedDataVerifyButton); + await this.driver.waitForSelector({ + css: this.signTypedDataVerifyResult, + text: publicKey.toLowerCase(), + }); + } + + /** + * Verify the successful signTypedDataV3 signature. + * + * @param publicKey - The public key to verify the signature with. + */ + async check_successSignTypedDataV3(publicKey: string) { + console.log('Verify successful signTypedDataV3 signature'); + await this.driver.switchToWindowWithTitle(WINDOW_TITLES.TestDApp); + await this.driver.clickElement(this.signTypedDataV3VerifyButton); + await this.driver.waitForSelector({ + css: this.signTypedDataV3VerifyResult, + text: publicKey.toLowerCase(), + }); + } + + /** + * Verify the successful signTypedDataV4 signature. + * + * @param publicKey - The public key to verify the signature with. + */ + async check_successSignTypedDataV4(publicKey: string) { + console.log('Verify successful signTypedDataV4 signature'); + await this.driver.switchToWindowWithTitle(WINDOW_TITLES.TestDApp); + await this.driver.clickElement(this.signTypedDataV4VerifyButton); + await this.driver.waitForSelector({ + css: this.signTypedDataV4VerifyResult, + text: publicKey.toLowerCase(), + }); + } + + /** + * Sign a message with the personal sign method. + */ + async personalSign() { + console.log('Sign message with personal sign'); + await this.driver.clickElement(this.personalSignButton); + await this.driver.switchToWindowWithTitle(WINDOW_TITLES.Dialog); + await this.driver.waitForSelector(this.personalSignSignatureRequestMessage); + await this.driver.clickElementAndWaitForWindowToClose( + this.confirmSignatureButton, + ); + } + + /** + * Sign message with the signPermit method. + */ + async signPermit() { + console.log('Sign message with signPermit'); + await this.driver.clickElement(this.signPermitButton); + await this.driver.switchToWindowWithTitle(WINDOW_TITLES.Dialog); + await this.driver.waitForSelector(this.signPermitSignatureRequestMessage); + await this.driver.clickElementAndWaitForWindowToClose( + this.confirmSignatureButton, + ); + } + + /** + * Sign a message with the signTypedData method. + */ + async signTypedData() { + console.log('Sign message with signTypedData'); + await this.driver.clickElement(this.signTypedDataButton); + await this.driver.switchToWindowWithTitle(WINDOW_TITLES.Dialog); + await this.driver.waitForSelector( + this.signTypedDataSignatureRequestMessage, + ); + await this.driver.clickElementAndWaitForWindowToClose( + this.confirmSignatureButton, + ); + } + + /** + * Sign a message with the signTypedDataV3 method. + */ + async signTypedDataV3() { + console.log('Sign message with signTypedDataV3'); + await this.driver.clickElement(this.signTypedDataV3Button); + await this.driver.switchToWindowWithTitle(WINDOW_TITLES.Dialog); + await this.driver.waitForSelector( + this.signTypedDataV3V4SignatureRequestMessage, + ); + await this.driver.clickElementSafe(this.confirmDialogScrollButton, 200); + await this.driver.clickElementAndWaitForWindowToClose( + this.confirmSignatureButton, + ); + } + + /** + * Sign a message with the signTypedDataV4 method. + */ + async signTypedDataV4() { + console.log('Sign message with signTypedDataV4'); + await this.driver.clickElement(this.signTypedDataV4Button); + await this.driver.switchToWindowWithTitle(WINDOW_TITLES.Dialog); + await this.driver.waitForSelector( + this.signTypedDataV3V4SignatureRequestMessage, + ); + await this.driver.clickElementSafe(this.confirmDialogScrollButton, 200); + await this.driver.clickElementAndWaitForWindowToClose( + this.confirmSignatureButton, + ); + } +} export default TestDapp; diff --git a/test/e2e/playwright/mmi/pageObjects/mmi-dummyApp-page.ts b/test/e2e/playwright/mmi/pageObjects/mmi-dummyApp-page.ts index a9a175e82971..cf455dc0a7e0 100644 --- a/test/e2e/playwright/mmi/pageObjects/mmi-dummyApp-page.ts +++ b/test/e2e/playwright/mmi/pageObjects/mmi-dummyApp-page.ts @@ -34,10 +34,11 @@ export class DummyAppPage { this.connectBtn.click(), ]); await popup1.waitForLoadState(); - // Check which account is selected and select if required - await popup1.locator('.check-box__indeterminate'); - await popup1.locator('button:has-text("Next")').click(); - await popup1.locator('button:has-text("Confirm")').click(); + await popup1.getByTestId('edit').nth(1).click(); + await popup1.getByText('Select all').click(); + await popup1.getByTestId('Sepolia').click(); + await popup1.getByTestId('connect-more-chains-button').click(); + await popup1.getByTestId('confirm-btn').click(); await popup1.close(); } @@ -60,11 +61,7 @@ export class DummyAppPage { if (isSign) { await popup.click('button:has-text("Confirm")'); } else { - await popup.getByTestId('page-container-footer-next').click(); - - if (buttonId === 'approveTokens') { - await popup.getByTestId('page-container-footer-next').click(); - } + await popup.getByTestId('confirm-footer-button').click(); await popup .getByTestId('custody-confirm-link__btn') diff --git a/test/e2e/set-manifest-flags.ts b/test/e2e/set-manifest-flags.ts index 290e8b863a9e..75339250506f 100644 --- a/test/e2e/set-manifest-flags.ts +++ b/test/e2e/set-manifest-flags.ts @@ -5,6 +5,9 @@ import { ManifestFlags } from '../../app/scripts/lib/manifestFlags'; export const folder = `dist/${process.env.SELENIUM_BROWSER}`; +type ManifestType = { _flags?: ManifestFlags; manifest_version: string }; +let manifest: ManifestType; + function parseIntOrUndefined(value: string | undefined): number | undefined { return value ? parseInt(value, 10) : undefined; } @@ -113,11 +116,23 @@ export function setManifestFlags(flags: ManifestFlags = {}) { } } - const manifest = JSON.parse( - fs.readFileSync(`${folder}/manifest.json`).toString(), - ); + readManifest(); manifest._flags = flags; fs.writeFileSync(`${folder}/manifest.json`, JSON.stringify(manifest)); } + +export function getManifestVersion(): number { + readManifest(); + + return parseInt(manifest.manifest_version, 10); +} + +function readManifest() { + if (!manifest) { + manifest = JSON.parse( + fs.readFileSync(`${folder}/manifest.json`).toString(), + ); + } +} diff --git a/test/e2e/snaps/enums.js b/test/e2e/snaps/enums.js index 2b1a6bc6532d..7fdbf5e1fd24 100644 --- a/test/e2e/snaps/enums.js +++ b/test/e2e/snaps/enums.js @@ -1,3 +1,3 @@ module.exports = { - TEST_SNAPS_WEBSITE_URL: 'https://metamask.github.io/snaps/test-snaps/2.13.1/', + TEST_SNAPS_WEBSITE_URL: 'https://metamask.github.io/snaps/test-snaps/2.15.2', }; diff --git a/test/e2e/tests/account/snap-account-settings.spec.ts b/test/e2e/tests/account/snap-account-settings.spec.ts index cbd5f8814b7b..1a0c761fb4df 100644 --- a/test/e2e/tests/account/snap-account-settings.spec.ts +++ b/test/e2e/tests/account/snap-account-settings.spec.ts @@ -33,7 +33,7 @@ describe('Add snap account experimental settings @no-mmi', function (this: Suite await settingsPage.goToExperimentalSettings(); const experimentalSettings = new ExperimentalSettings(driver); - await settingsPage.check_pageIsLoaded(); + await experimentalSettings.check_pageIsLoaded(); await experimentalSettings.toggleAddAccountSnap(); // Make sure the "Add account Snap" button is visible. diff --git a/test/e2e/tests/account/snap-account-signatures.spec.ts b/test/e2e/tests/account/snap-account-signatures.spec.ts new file mode 100644 index 000000000000..f5010fb61269 --- /dev/null +++ b/test/e2e/tests/account/snap-account-signatures.spec.ts @@ -0,0 +1,100 @@ +import { Suite } from 'mocha'; +import { Driver } from '../../webdriver/driver'; +import { WINDOW_TITLES, withFixtures } from '../../helpers'; +import ExperimentalSettings from '../../page-objects/pages/experimental-settings'; +import FixtureBuilder from '../../fixture-builder'; +import HeaderNavbar from '../../page-objects/pages/header-navbar'; +import { installSnapSimpleKeyring } from '../../page-objects/flows/snap-simple-keyring.flow'; +import { loginWithBalanceValidation } from '../../page-objects/flows/login.flow'; +import { + personalSignWithSnapAccount, + signPermitWithSnapAccount, + signTypedDataV3WithSnapAccount, + signTypedDataV4WithSnapAccount, + signTypedDataWithSnapAccount, +} from '../../page-objects/flows/sign.flow'; +import SettingsPage from '../../page-objects/pages/settings-page'; +import SnapSimpleKeyringPage from '../../page-objects/pages/snap-simple-keyring-page'; +import TestDapp from '../../page-objects/pages/test-dapp'; + +describe('Snap Account Signatures @no-mmi', function (this: Suite) { + // Run sync, async approve, and async reject flows + // (in Jest we could do this with test.each, but that does not exist here) + + ['sync', 'approve', 'reject'].forEach((flowType) => { + // generate title of the test from flowType + const title = `can sign with ${flowType} flow`; + + it(title, async () => { + await withFixtures( + { + dapp: true, + fixtures: new FixtureBuilder() + .withPermissionControllerConnectedToTestDapp({ + restrictReturnedAccounts: false, + }) + .build(), + title, + }, + async ({ driver }: { driver: Driver }) => { + const isSyncFlow = flowType === 'sync'; + const approveTransaction = flowType === 'approve'; + await loginWithBalanceValidation(driver); + await installSnapSimpleKeyring(driver, isSyncFlow); + const snapSimpleKeyringPage = new SnapSimpleKeyringPage(driver); + const newPublicKey = await snapSimpleKeyringPage.createNewAccount(); + + // Check snap account is displayed after adding the snap account. + await driver.switchToWindowWithTitle( + WINDOW_TITLES.ExtensionInFullScreenView, + ); + const headerNavbar = new HeaderNavbar(driver); + await headerNavbar.check_accountLabel('SSK Account'); + + // Navigate to experimental settings and disable redesigned signature. + await headerNavbar.openSettingsPage(); + const settingsPage = new SettingsPage(driver); + await settingsPage.check_pageIsLoaded(); + await settingsPage.goToExperimentalSettings(); + + const experimentalSettings = new ExperimentalSettings(driver); + await experimentalSettings.check_pageIsLoaded(); + await experimentalSettings.toggleRedesignedSignature(); + + // Run all 5 signature types + await new TestDapp(driver).openTestDappPage(); + await personalSignWithSnapAccount( + driver, + newPublicKey, + isSyncFlow, + approveTransaction, + ); + await signTypedDataWithSnapAccount( + driver, + newPublicKey, + isSyncFlow, + approveTransaction, + ); + await signTypedDataV3WithSnapAccount( + driver, + newPublicKey, + isSyncFlow, + approveTransaction, + ); + await signTypedDataV4WithSnapAccount( + driver, + newPublicKey, + isSyncFlow, + approveTransaction, + ); + await signPermitWithSnapAccount( + driver, + newPublicKey, + isSyncFlow, + approveTransaction, + ); + }, + ); + }); + }); +}); diff --git a/test/e2e/tests/bridge/bridge-test-utils.ts b/test/e2e/tests/bridge/bridge-test-utils.ts index 40bb8c6bd97f..1f4a3e5cda79 100644 --- a/test/e2e/tests/bridge/bridge-test-utils.ts +++ b/test/e2e/tests/bridge/bridge-test-utils.ts @@ -84,10 +84,6 @@ export class BridgePage { verifySwapPage = async (expectedHandleCount: number) => { await this.driver.delay(4000); - await this.driver.waitForSelector({ - css: '.bridge__title', - text: 'Bridge', - }); assert.equal( (await this.driver.getAllWindowHandles()).length, IS_FIREFOX || !isManifestV3 diff --git a/test/e2e/tests/confirmations/signatures/permit.spec.ts b/test/e2e/tests/confirmations/signatures/permit.spec.ts index c9c4ca9399f4..5c52d1f029ee 100644 --- a/test/e2e/tests/confirmations/signatures/permit.spec.ts +++ b/test/e2e/tests/confirmations/signatures/permit.spec.ts @@ -152,22 +152,22 @@ async function assertVerifiedResults(driver: Driver, publicAddress: string) { await driver.waitForSelector({ css: '#signPermitResult', - text: '0x0a396f89ee073214f7e055e700048abd7b4aba6ecca0352937d6a2ebb7176f2f43c63097ad7597632e34d6a801695702ba603d5872a33ee7d7562fcdb9e816ee1c', + text: '0xf6555e4cc39bdec3397c357af876f87de00667c942f22dec555c28d290ed7d730103fe85c9d7c66d808a0a972f69ae00741a11df449475280772e7d9a232ea491b', }); await driver.waitForSelector({ css: '#signPermitResultR', - text: 'r: 0x0a396f89ee073214f7e055e700048abd7b4aba6ecca0352937d6a2ebb7176f2f', + text: 'r: 0xf6555e4cc39bdec3397c357af876f87de00667c942f22dec555c28d290ed7d73', }); await driver.waitForSelector({ css: '#signPermitResultS', - text: 's: 0x43c63097ad7597632e34d6a801695702ba603d5872a33ee7d7562fcdb9e816ee', + text: 's: 0x0103fe85c9d7c66d808a0a972f69ae00741a11df449475280772e7d9a232ea49', }); await driver.waitForSelector({ css: '#signPermitResultV', - text: 'v: 28', + text: 'v: 27', }); await driver.waitForSelector({ css: '#signPermitVerifyResult', diff --git a/test/e2e/tests/confirmations/transactions/erc1155-revoke-set-approval-for-all-redesign.ts b/test/e2e/tests/confirmations/transactions/erc1155-revoke-set-approval-for-all-redesign.ts index 7f26e02a572c..3e75adb34db8 100644 --- a/test/e2e/tests/confirmations/transactions/erc1155-revoke-set-approval-for-all-redesign.ts +++ b/test/e2e/tests/confirmations/transactions/erc1155-revoke-set-approval-for-all-redesign.ts @@ -57,7 +57,7 @@ async function createTransactionAndAssertDetails( const testDapp = new TestDapp(driver); - await testDapp.open({ contractAddress, url: DAPP_URL }); + await testDapp.openTestDappPage({ contractAddress, url: DAPP_URL }); await testDapp.clickERC1155RevokeSetApprovalForAllButton(); diff --git a/test/e2e/tests/confirmations/transactions/erc1155-set-approval-for-all-redesign.spec.ts b/test/e2e/tests/confirmations/transactions/erc1155-set-approval-for-all-redesign.spec.ts index 438b3e979d0a..0e1134737c87 100644 --- a/test/e2e/tests/confirmations/transactions/erc1155-set-approval-for-all-redesign.spec.ts +++ b/test/e2e/tests/confirmations/transactions/erc1155-set-approval-for-all-redesign.spec.ts @@ -85,7 +85,7 @@ async function createTransactionAssertDetailsAndConfirm( const testDapp = new TestDapp(driver); - await testDapp.open({ contractAddress, url: DAPP_URL }); + await testDapp.openTestDappPage({ contractAddress, url: DAPP_URL }); await testDapp.clickERC1155SetApprovalForAllButton(); await driver.switchToWindowWithTitle(WINDOW_TITLES.Dialog); diff --git a/test/e2e/tests/confirmations/transactions/erc721-revoke-set-approval-for-all-redesign.ts b/test/e2e/tests/confirmations/transactions/erc721-revoke-set-approval-for-all-redesign.ts index 5a8dcd3768f7..138695904e55 100644 --- a/test/e2e/tests/confirmations/transactions/erc721-revoke-set-approval-for-all-redesign.ts +++ b/test/e2e/tests/confirmations/transactions/erc721-revoke-set-approval-for-all-redesign.ts @@ -80,7 +80,7 @@ async function createTransactionAndAssertDetails( const testDapp = new TestDapp(driver); - await testDapp.open({ contractAddress, url: DAPP_URL }); + await testDapp.openTestDappPage({ contractAddress, url: DAPP_URL }); await testDapp.clickERC721RevokeSetApprovalForAllButton(); diff --git a/test/e2e/tests/confirmations/transactions/erc721-set-approval-for-all-redesign.spec.ts b/test/e2e/tests/confirmations/transactions/erc721-set-approval-for-all-redesign.spec.ts index 7ca9518cabc2..589670212be1 100644 --- a/test/e2e/tests/confirmations/transactions/erc721-set-approval-for-all-redesign.spec.ts +++ b/test/e2e/tests/confirmations/transactions/erc721-set-approval-for-all-redesign.spec.ts @@ -85,7 +85,7 @@ async function createTransactionAssertDetailsAndConfirm( const testDapp = new TestDapp(driver); - await testDapp.open({ contractAddress, url: DAPP_URL }); + await testDapp.openTestDappPage({ contractAddress, url: DAPP_URL }); await testDapp.clickERC721SetApprovalForAllButton(); await driver.switchToWindowWithTitle(WINDOW_TITLES.Dialog); diff --git a/test/e2e/tests/dapp-interactions/permissions.spec.js b/test/e2e/tests/dapp-interactions/permissions.spec.js index adf3b809a656..4b6c210f0a98 100644 --- a/test/e2e/tests/dapp-interactions/permissions.spec.js +++ b/test/e2e/tests/dapp-interactions/permissions.spec.js @@ -28,19 +28,15 @@ describe('Permissions', function () { tag: 'button', }); - await driver.waitUntilXWindowHandles(3); - const windowHandles = await driver.getAllWindowHandles(); - const extension = windowHandles[0]; - await driver.switchToWindowWithTitle( - WINDOW_TITLES.Dialog, - windowHandles, - ); - await driver.clickElement({ + await driver.switchToWindowWithTitle(WINDOW_TITLES.Dialog); + await driver.clickElementAndWaitForWindowToClose({ text: 'Connect', tag: 'button', }); - await driver.switchToWindow(extension); + await driver.switchToWindowWithTitle( + WINDOW_TITLES.ExtensionInFullScreenView, + ); // shows connected sites await driver.clickElement( @@ -64,21 +60,17 @@ describe('Permissions', function () { assert.equal(domains.length, 1); // can get accounts within the dapp - await driver.switchToWindowWithTitle('E2E Test Dapp', windowHandles); + await driver.switchToWindowWithTitle(WINDOW_TITLES.TestDApp); await driver.clickElement({ text: 'eth_accounts', tag: 'button', }); - const getAccountsResult = await driver.waitForSelector({ + await driver.waitForSelector({ css: '#getAccountsResult', text: publicAddress, }); - assert.equal( - (await getAccountsResult.getText()).toLowerCase(), - publicAddress.toLowerCase(), - ); }, ); }); diff --git a/test/e2e/tests/dapp-interactions/provider-api.spec.js b/test/e2e/tests/dapp-interactions/provider-api.spec.js index 80cca60afb95..1c20b9fb2f6e 100644 --- a/test/e2e/tests/dapp-interactions/provider-api.spec.js +++ b/test/e2e/tests/dapp-interactions/provider-api.spec.js @@ -1,5 +1,5 @@ const { strict: assert } = require('assert'); -const { errorCodes } = require('eth-rpc-errors'); +const { errorCodes } = require('@metamask/rpc-errors'); const { defaultGanacheOptions, withFixtures, diff --git a/test/e2e/tests/dapp-interactions/revoke-permissions.spec.js b/test/e2e/tests/dapp-interactions/revoke-permissions.spec.js index 14d2af60bdbc..696095e3fd79 100644 --- a/test/e2e/tests/dapp-interactions/revoke-permissions.spec.js +++ b/test/e2e/tests/dapp-interactions/revoke-permissions.spec.js @@ -1,4 +1,3 @@ -const { strict: assert } = require('assert'); const { withFixtures, openDapp, @@ -25,12 +24,10 @@ describe('Wallet Revoke Permissions', function () { // Get initial accounts permissions await driver.clickElement('#getPermissions'); - const permissionsResult = await driver.findElement( - '#permissionsResult', - ); - - // Eth_accounts permission - assert.equal(await permissionsResult.getText(), 'eth_accounts'); + await driver.waitForSelector({ + css: '#permissionsResult', + text: 'eth_accounts', + }); // Revoke eth_accounts permissions await driver.clickElement('#revokeAccountsPermission'); @@ -39,10 +36,10 @@ describe('Wallet Revoke Permissions', function () { await driver.clickElement('#getPermissions'); // Eth_accounts permissions removed - assert.equal( - await permissionsResult.getText(), - 'No permissions found.', - ); + await driver.waitForSelector({ + css: '#permissionsResult', + text: 'No permissions found.', + }); }, ); }); diff --git a/test/e2e/tests/onboarding/onboarding.spec.js b/test/e2e/tests/onboarding/onboarding.spec.js index 8d6b00de07ed..b5e273b7e978 100644 --- a/test/e2e/tests/onboarding/onboarding.spec.js +++ b/test/e2e/tests/onboarding/onboarding.spec.js @@ -328,28 +328,24 @@ describe('MetaMask onboarding @no-mmi', function () { tag: 'button', }); - await driver.clickElement({ text: 'Save', tag: 'button' }); - - await driver.delay(largeDelayMs); - await driver.waitForSelector('[data-testid="category-back-button"]'); - const generalBackButton = await driver.waitForSelector( - '[data-testid="category-back-button"]', - ); - await generalBackButton.click(); + await driver.clickElementAndWaitToDisappear({ + tag: 'button', + text: 'Save', + }); - await driver.delay(largeDelayMs); + await driver.clickElement('[data-testid="category-back-button"]'); - await driver.waitForSelector( + // Wait until the onboarding carousel has stopped moving + // otherwise the click has no effect. + await driver.waitForElementToStopMoving( '[data-testid="privacy-settings-back-button"]', ); - const defaultSettingsBackButton = await driver.findElement( + + await driver.clickElement( '[data-testid="privacy-settings-back-button"]', ); - await defaultSettingsBackButton.click(); - - await driver.delay(largeDelayMs); - await driver.clickElement({ + await driver.clickElementAndWaitToDisappear({ text: 'Done', tag: 'button', }); @@ -359,9 +355,14 @@ describe('MetaMask onboarding @no-mmi', function () { tag: 'button', }); - await driver.delay(largeDelayMs); + // Wait until the onboarding carousel has stopped moving + // otherwise the click has no effect. + await driver.waitForElementToStopMoving({ + text: 'Done', + tag: 'button', + }); - await driver.clickElement({ + await driver.clickElementAndWaitToDisappear({ text: 'Done', tag: 'button', }); @@ -412,6 +413,12 @@ describe('MetaMask onboarding @no-mmi', function () { await driver.clickElement('[id="basic-configuration-checkbox"]'); await driver.clickElement({ text: 'Turn off', tag: 'button' }); await driver.clickElement('[data-testid="category-back-button"]'); + + // Wait until the onboarding carousel has stopped moving + // otherwise the click has no effect. + await driver.waitForElementToStopMoving( + '[data-testid="privacy-settings-back-button"]', + ); await driver.clickElement( '[data-testid="privacy-settings-back-button"]', ); diff --git a/test/e2e/tests/privacy/basic-functionality.spec.js b/test/e2e/tests/privacy/basic-functionality.spec.js index b4fc0e138104..6ae14ca660be 100644 --- a/test/e2e/tests/privacy/basic-functionality.spec.js +++ b/test/e2e/tests/privacy/basic-functionality.spec.js @@ -81,15 +81,35 @@ describe('MetaMask onboarding @no-mmi', function () { '[data-testid="currency-rate-check-toggle"] .toggle-button', ); await driver.clickElement('[data-testid="category-back-button"]'); - await driver.delay(regularDelayMs); + + // Wait until the onboarding carousel has stopped moving + // otherwise the click has no effect. + await driver.waitForElementToStopMoving( + '[data-testid="privacy-settings-back-button"]', + ); await driver.clickElement( '[data-testid="privacy-settings-back-button"]', ); - await driver.delay(regularDelayMs); - await driver.clickElement({ text: 'Done', tag: 'button' }); - await driver.clickElement('[data-testid="pin-extension-next"]'); - await driver.clickElement({ text: 'Done', tag: 'button' }); + await driver.clickElementAndWaitToDisappear({ + text: 'Done', + tag: 'button', + }); + await driver.clickElement({ + text: 'Next', + tag: 'button', + }); + + // Wait until the onboarding carousel has stopped moving + // otherwise the click has no effect. + await driver.waitForElementToStopMoving({ + text: 'Done', + tag: 'button', + }); + await driver.clickElementAndWaitToDisappear({ + text: 'Done', + tag: 'button', + }); await driver.clickElement('[data-testid="network-display"]'); diff --git a/test/e2e/tests/tokens/add-hide-token.spec.js b/test/e2e/tests/tokens/add-hide-token.spec.js index 535948ba1c9b..5eb60d3db17b 100644 --- a/test/e2e/tests/tokens/add-hide-token.spec.js +++ b/test/e2e/tests/tokens/add-hide-token.spec.js @@ -8,6 +8,7 @@ const { clickNestedButton, } = require('../../helpers'); const FixtureBuilder = require('../../fixture-builder'); +const { SMART_CONTRACTS } = require('../../seeder/smart-contracts'); const { CHAIN_IDS } = require('../../../../shared/constants/network'); describe('Add hide token', function () { @@ -126,16 +127,16 @@ describe('Add existing token using search', function () { tag: 'p', }); await driver.clickElement({ text: 'Next', tag: 'button' }); - await driver.clickElement( + await driver.clickElementAndWaitToDisappear( '[data-testid="import-tokens-modal-import-button"]', ); await driver.clickElement( '[data-testid="account-overview__asset-tab"]', ); - const [, tkn] = await driver.findElements( - '[data-testid="multichain-token-list-button"]', - ); - await tkn.click(); + await driver.clickElement({ + tag: 'span', + text: 'Basic Attention Token', + }); await driver.waitForSelector({ css: '[data-testid="multichain-token-list-item-value"]', @@ -147,6 +148,8 @@ describe('Add existing token using search', function () { }); describe('Add token using wallet_watchAsset', function () { + const smartContract = SMART_CONTRACTS.HST; + it('opens a notification that adds a token when wallet_watchAsset is executed, then approves', async function () { await withFixtures( { @@ -155,9 +158,13 @@ describe('Add token using wallet_watchAsset', function () { .withPermissionControllerConnectedToTestDapp() .build(), ganacheOptions: defaultGanacheOptions, + smartContract, title: this.test.fullTitle(), }, - async ({ driver }) => { + async ({ driver, contractRegistry }) => { + const contractAddress = await contractRegistry.getContractAddress( + smartContract, + ); await unlockWallet(driver); await driver.openNewPage('http://127.0.0.1:8080/'); @@ -168,7 +175,7 @@ describe('Add token using wallet_watchAsset', function () { params: { type: 'ERC20', options: { - address: '0x86002be4cdd922de1ccb831582bf99284b99ac12', + address: '${contractAddress}', symbol: 'TST', decimals: 4 }, @@ -176,19 +183,16 @@ describe('Add token using wallet_watchAsset', function () { }) `); - const windowHandles = await driver.waitUntilXWindowHandles(3); - - await driver.switchToWindowWithTitle( - WINDOW_TITLES.Dialog, - windowHandles, - ); + await driver.switchToWindowWithTitle(WINDOW_TITLES.Dialog); - await driver.clickElement({ + await driver.clickElementAndWaitForWindowToClose({ tag: 'button', text: 'Add token', }); - await driver.switchToWindowWithTitle('MetaMask', windowHandles); + await driver.switchToWindowWithTitle( + WINDOW_TITLES.ExtensionInFullScreenView, + ); await driver.waitForSelector({ css: '[data-testid="multichain-token-list-item-value"]', @@ -206,9 +210,13 @@ describe('Add token using wallet_watchAsset', function () { .withPermissionControllerConnectedToTestDapp() .build(), ganacheOptions: defaultGanacheOptions, + smartContract, title: this.test.fullTitle(), }, - async ({ driver }) => { + async ({ driver, contractRegistry }) => { + const contractAddress = await contractRegistry.getContractAddress( + smartContract, + ); await unlockWallet(driver); await driver.openNewPage('http://127.0.0.1:8080/'); @@ -219,7 +227,7 @@ describe('Add token using wallet_watchAsset', function () { params: { type: 'ERC20', options: { - address: '0x86002be4cdd922de1ccb831582bf99284b99ac12', + address: '${contractAddress}', symbol: 'TST', decimals: 4 }, @@ -227,19 +235,16 @@ describe('Add token using wallet_watchAsset', function () { }) `); - const windowHandles = await driver.waitUntilXWindowHandles(3); + await driver.switchToWindowWithTitle(WINDOW_TITLES.Dialog); - await driver.switchToWindowWithTitle( - WINDOW_TITLES.Dialog, - windowHandles, - ); - - await driver.clickElement({ + await driver.clickElementAndWaitForWindowToClose({ tag: 'button', text: 'Cancel', }); - await driver.switchToWindowWithTitle('MetaMask', windowHandles); + await driver.switchToWindowWithTitle( + WINDOW_TITLES.ExtensionInFullScreenView, + ); const assetListItems = await driver.findElements( '.multichain-token-list-item', diff --git a/test/e2e/tests/tokens/nft/erc1155-interaction.spec.js b/test/e2e/tests/tokens/nft/erc1155-interaction.spec.js index 31425140c7f4..1fed3946dea9 100644 --- a/test/e2e/tests/tokens/nft/erc1155-interaction.spec.js +++ b/test/e2e/tests/tokens/nft/erc1155-interaction.spec.js @@ -38,33 +38,27 @@ describe('ERC1155 NFTs testdapp interaction', function () { await driver.clickElement('#batchMintButton'); // Notification - const windowHandles = await driver.waitUntilXWindowHandles(3); - const [extension] = windowHandles; - await driver.switchToWindowWithTitle( - WINDOW_TITLES.Dialog, - windowHandles, - ); + await driver.switchToWindowWithTitle(WINDOW_TITLES.Dialog); // Confirm Mint await driver.waitForSelector({ css: '.confirm-page-container-summary__action__name', text: 'Deposit', }); - await driver.clickElement({ text: 'Confirm', tag: 'button' }); - await driver.waitUntilXWindowHandles(2); - await driver.switchToWindow(extension); + await driver.clickElementAndWaitForWindowToClose({ + text: 'Confirm', + tag: 'button', + }); + await driver.switchToWindowWithTitle( + WINDOW_TITLES.ExtensionInFullScreenView, + ); await driver.clickElement( '[data-testid="account-overview__activity-tab"]', ); - const transactionItem = await driver.waitForSelector({ + await driver.waitForSelector({ css: '[data-testid="activity-list-item-action"]', text: 'Deposit', }); - assert.equal( - await transactionItem.isDisplayed(), - true, - `transaction item should be displayed in activity tab`, - ); }, ); }); @@ -90,33 +84,27 @@ describe('ERC1155 NFTs testdapp interaction', function () { await driver.fill('#batchTransferTokenAmounts', '1, 1, 1000000000000'); await driver.clickElement('#batchTransferFromButton'); - const windowHandles = await driver.waitUntilXWindowHandles(3); - const [extension] = windowHandles; - await driver.switchToWindowWithTitle( - WINDOW_TITLES.Dialog, - windowHandles, - ); + await driver.switchToWindowWithTitle(WINDOW_TITLES.Dialog); // Confirm Transfer await driver.waitForSelector({ css: '.confirm-page-container-summary__action__name', text: 'Deposit', }); - await driver.clickElement({ text: 'Confirm', tag: 'button' }); - await driver.waitUntilXWindowHandles(2); - await driver.switchToWindow(extension); + await driver.clickElementAndWaitForWindowToClose({ + text: 'Confirm', + tag: 'button', + }); + await driver.switchToWindowWithTitle( + WINDOW_TITLES.ExtensionInFullScreenView, + ); await driver.clickElement( '[data-testid="account-overview__activity-tab"]', ); - const transactionItem = await driver.waitForSelector({ + await driver.waitForSelector({ css: '[data-testid="activity-list-item-action"]', text: 'Deposit', }); - assert.equal( - await transactionItem.isDisplayed(), - true, - `transaction item should be displayed in activity tab`, - ); }, ); }); @@ -147,26 +135,20 @@ describe('ERC1155 NFTs testdapp interaction', function () { await driver.clickElement('#setApprovalForAllERC1155Button'); // Wait for notification popup and check the displayed message - let windowHandles = await driver.waitUntilXWindowHandles(3); - await driver.switchToWindowWithTitle( - WINDOW_TITLES.Dialog, - windowHandles, - ); - const displayedMessageTitle = await driver.findElement( - '[data-testid="confirm-approve-title"]', - ); - assert.equal( - await displayedMessageTitle.getText(), - expectedMessageTitle, - ); - const displayedUrl = await driver.findElement( - '.confirm-approve-content h6', - ); - assert.equal(await displayedUrl.getText(), DAPP_URL); - const displayedDescription = await driver.findElement( - '.confirm-approve-content__description', - ); - assert.equal(await displayedDescription.getText(), expectedDescription); + await driver.switchToWindowWithTitle(WINDOW_TITLES.Dialog); + await driver.waitForSelector({ + css: '[data-testid="confirm-approve-title"]', + text: expectedMessageTitle, + }); + await driver.waitForSelector({ + css: '.confirm-approve-content h6', + text: DAPP_URL, + }); + + await driver.waitForSelector({ + css: '.confirm-approve-content__description', + text: expectedDescription, + }); // Check displayed transaction details await driver.clickElement({ @@ -185,27 +167,29 @@ describe('ERC1155 NFTs testdapp interaction', function () { '.set-approval-for-all-warning__content__header', ); assert.equal(await displayedWarning.getText(), expectedWarningMessage); - await driver.clickElement({ text: 'Approve', tag: 'button' }); - windowHandles = await driver.waitUntilXWindowHandles(2); + await driver.clickElementAndWaitForWindowToClose({ + text: 'Approve', + tag: 'button', + }); // Switch to extension and check set approval for all transaction is displayed in activity tab - await driver.switchToWindowWithTitle('MetaMask', windowHandles); + await driver.switchToWindowWithTitle( + WINDOW_TITLES.ExtensionInFullScreenView, + ); await driver.clickElement( '[data-testid="account-overview__activity-tab"]', ); - const setApprovalItem = await driver.findElement({ + await driver.waitForSelector({ css: '.transaction-list__completed-transactions', text: 'Approve Token with no spend limit', }); - assert.equal(await setApprovalItem.isDisplayed(), true); // Switch back to the dapp and verify that set approval for all action completed message is displayed - await driver.switchToWindowWithTitle('E2E Test Dapp', windowHandles); - const setApprovalStatus = await driver.findElement({ + await driver.switchToWindowWithTitle(WINDOW_TITLES.TestDApp); + await driver.waitForSelector({ css: '#erc1155Status', text: 'Set Approval For All completed', }); - assert.equal(await setApprovalStatus.isDisplayed(), true); }, ); }); @@ -235,27 +219,22 @@ describe('ERC1155 NFTs testdapp interaction', function () { await driver.clickElement('#revokeERC1155Button'); // Wait for notification popup and check the displayed message - let windowHandles = await driver.waitUntilXWindowHandles(3); - await driver.switchToWindowWithTitle( - WINDOW_TITLES.Dialog, - windowHandles, - ); + await driver.switchToWindowWithTitle(WINDOW_TITLES.Dialog); - const displayedMessageTitle = await driver.findElement( - '.confirm-approve-content__title', - ); - assert.equal( - await displayedMessageTitle.getText(), - expectedMessageTitle, - ); - const displayedUrl = await driver.findElement( - '.confirm-approve-content h6', - ); - assert.equal(await displayedUrl.getText(), DAPP_URL); - const displayedDescription = await driver.findElement( - '.confirm-approve-content__description', - ); - assert.equal(await displayedDescription.getText(), expectedDescription); + await driver.waitForSelector({ + css: '.confirm-approve-content__title', + text: expectedMessageTitle, + }); + + await driver.waitForSelector({ + css: '.confirm-approve-content h6', + text: DAPP_URL, + }); + + await driver.waitForSelector({ + css: '.confirm-approve-content__description', + text: expectedDescription, + }); // Check displayed transaction details await driver.clickElement({ @@ -269,22 +248,25 @@ describe('ERC1155 NFTs testdapp interaction', function () { assert.equal(await params.getText(), 'Parameters: false'); // Click on extension popup to confirm revoke approval for all - await driver.clickElement('[data-testid="page-container-footer-next"]'); - windowHandles = await driver.waitUntilXWindowHandles(2); + await driver.clickElementAndWaitForWindowToClose( + '[data-testid="page-container-footer-next"]', + ); // Switch to extension and check revoke approval transaction is displayed in activity tab - await driver.switchToWindowWithTitle('MetaMask', windowHandles); + await driver.switchToWindowWithTitle( + WINDOW_TITLES.ExtensionInFullScreenView, + ); + await driver.clickElement( '[data-testid="account-overview__activity-tab"]', ); - const revokeApprovalItem = await driver.findElement({ + await driver.waitForSelector({ css: '.transaction-list__completed-transactions', text: 'Approve Token with no spend limit', }); - assert.equal(await revokeApprovalItem.isDisplayed(), true); // Switch back to the dapp and verify that revoke approval for all message is displayed - await driver.switchToWindowWithTitle('E2E Test Dapp', windowHandles); + await driver.switchToWindowWithTitle(WINDOW_TITLES.TestDApp); const revokeApprovalStatus = await driver.findElement({ css: '#erc1155Status', text: 'Revoke completed', diff --git a/test/e2e/tests/tokens/nft/erc721-interaction.spec.js b/test/e2e/tests/tokens/nft/erc721-interaction.spec.js index c2d3b7a8ce82..9ebc247ea795 100644 --- a/test/e2e/tests/tokens/nft/erc721-interaction.spec.js +++ b/test/e2e/tests/tokens/nft/erc721-interaction.spec.js @@ -225,25 +225,30 @@ describe('ERC721 NFTs testdapp interaction', function () { await driver.clickElement({ text: 'Mint', tag: 'button' }); // Notification - await driver.waitUntilXWindowHandles(3); await driver.switchToWindowWithTitle(WINDOW_TITLES.Dialog); await driver.waitForSelector({ css: '.confirm-page-container-summary__action__name', text: 'Deposit', }); - await driver.clickElement({ text: 'Confirm', tag: 'button' }); - await driver.waitUntilXWindowHandles(2); + await driver.clickElementAndWaitForWindowToClose({ + text: 'Confirm', + tag: 'button', + }); await driver.switchToWindowWithTitle( WINDOW_TITLES.ExtensionInFullScreenView, ); await driver.clickElement( '[data-testid="account-overview__activity-tab"]', ); - const transactionItem = await driver.waitForSelector({ + + // We need to wait until the transaction is confirmed before looking for the tx + // otherwise the element becomes stale, as it updates from 'pending' to 'confirmed' + await driver.waitForSelector('.transaction-status-label--confirmed'); + + await driver.waitForSelector({ css: '[data-testid="activity-list-item-action"]', text: 'Deposit', }); - assert.equal(await transactionItem.isDisplayed(), true); // verify the mint transaction has finished await driver.switchToWindowWithTitle(WINDOW_TITLES.TestDApp); const nftsMintStatus = await driver.findElement({ @@ -255,7 +260,6 @@ describe('ERC721 NFTs testdapp interaction', function () { // watch all nfts await driver.clickElement({ text: 'Watch all NFTs', tag: 'button' }); - await driver.waitUntilXWindowHandles(3); await driver.switchToWindowWithTitle(WINDOW_TITLES.Dialog); // confirm watchNFT @@ -277,8 +281,10 @@ describe('ERC721 NFTs testdapp interaction', function () { ); await removeButtons[0].click(); - await driver.clickElement({ text: 'Add NFTs', tag: 'button' }); - await driver.waitUntilXWindowHandles(2); + await driver.clickElementAndWaitForWindowToClose({ + text: 'Add NFTs', + tag: 'button', + }); await driver.switchToWindowWithTitle( WINDOW_TITLES.ExtensionInFullScreenView, diff --git a/test/e2e/webdriver/driver.js b/test/e2e/webdriver/driver.js index b0648f122fb9..813d00d5e0e8 100644 --- a/test/e2e/webdriver/driver.js +++ b/test/e2e/webdriver/driver.js @@ -597,6 +597,46 @@ class Driver { } } + /** + * Checks if an element is moving by comparing its position at two different times. + * + * @param {string | object} rawLocator - Element locator. + * @returns {Promise} Promise that resolves to a boolean indicating if the element is moving. + */ + async isElementMoving(rawLocator) { + const element = await this.findElement(rawLocator); + const initialPosition = await element.getRect(); + + await new Promise((resolve) => setTimeout(resolve, 500)); // Wait for a short period + + const newPosition = await element.getRect(); + + return ( + initialPosition.x !== newPosition.x || initialPosition.y !== newPosition.y + ); + } + + /** + * Waits until an element stops moving within a specified timeout period. + * + * @param {string | object} rawLocator - Element locator. + * @param {number} timeout - The maximum time to wait for the element to stop moving. + * @returns {Promise} Promise that resolves when the element stops moving. + * @throws {Error} Throws an error if the element does not stop moving within the timeout period. + */ + async waitForElementToStopMoving(rawLocator, timeout = 5000) { + const startTime = Date.now(); + + while (Date.now() - startTime < timeout) { + if (!(await this.isElementMoving(rawLocator))) { + return; + } + await new Promise((resolve) => setTimeout(resolve, 500)); // Check every 500ms + } + + throw new Error('Element did not stop moving within the timeout period'); + } + /** @param {string} title - The title of the window or tab the screenshot is being taken in */ async takeScreenshot(title) { const filepathBase = `${artifactDir(title)}/test-screenshot`; diff --git a/ui/components/app/metamask-template-renderer/safe-component-list.js b/ui/components/app/metamask-template-renderer/safe-component-list.js index 14abe8eceea7..a41b92c9f463 100644 --- a/ui/components/app/metamask-template-renderer/safe-component-list.js +++ b/ui/components/app/metamask-template-renderer/safe-component-list.js @@ -43,6 +43,7 @@ import { SnapUICheckbox } from '../snaps/snap-ui-checkbox'; import { SnapUITooltip } from '../snaps/snap-ui-tooltip'; import { SnapUICard } from '../snaps/snap-ui-card'; import { SnapUIAddress } from '../snaps/snap-ui-address'; +import { SnapUIAvatar } from '../snaps/snap-ui-avatar'; import { SnapUISelector } from '../snaps/snap-ui-selector'; import { SnapUIFooterButton } from '../snaps/snap-ui-footer-button'; ///: BEGIN:ONLY_INCLUDE_IF(keyring-snaps) @@ -106,6 +107,7 @@ export const safeComponentList = { SnapUICard, SnapUISelector, SnapUIAddress, + SnapUIAvatar, SnapUIFooterButton, FormTextField, ///: BEGIN:ONLY_INCLUDE_IF(keyring-snaps) diff --git a/ui/components/app/qr-hardware-popover/qr-hardware-popover.js b/ui/components/app/qr-hardware-popover/qr-hardware-popover.js index 40d577380f49..ad0dff033ae9 100644 --- a/ui/components/app/qr-hardware-popover/qr-hardware-popover.js +++ b/ui/components/app/qr-hardware-popover/qr-hardware-popover.js @@ -1,6 +1,6 @@ import React, { useCallback, useMemo, useState } from 'react'; import { useDispatch, useSelector } from 'react-redux'; -import { ethErrors, serializeError } from 'eth-rpc-errors'; +import { providerErrors, serializeError } from '@metamask/rpc-errors'; import { getCurrentQRHardwareState } from '../../../selectors'; import Popover from '../../ui/popover'; import { useI18nContext } from '../../../hooks/useI18nContext'; @@ -44,7 +44,7 @@ const QRHardwarePopover = () => { dispatch( rejectPendingApproval( _txData.id, - serializeError(ethErrors.provider.userRejectedRequest()), + serializeError(providerErrors.userRejectedRequest()), ), ); dispatch(cancelTx(_txData)); diff --git a/ui/components/app/snaps/snap-ui-address/snap-ui-address.tsx b/ui/components/app/snaps/snap-ui-address/snap-ui-address.tsx index 669f7dd30799..539548622135 100644 --- a/ui/components/app/snaps/snap-ui-address/snap-ui-address.tsx +++ b/ui/components/app/snaps/snap-ui-address/snap-ui-address.tsx @@ -1,5 +1,4 @@ import React, { useMemo } from 'react'; -import { useSelector } from 'react-redux'; import { CaipAccountId, isHexString, @@ -11,32 +10,35 @@ import { Display, TextColor, } from '../../../../helpers/constants/design-system'; -import BlockieIdenticon from '../../../ui/identicon/blockieIdenticon'; -import Jazzicon from '../../../ui/jazzicon'; -import { getUseBlockie } from '../../../../selectors'; import { shortenAddress } from '../../../../helpers/utils/util'; import { toChecksumHexAddress } from '../../../../../shared/modules/hexstring-utils'; +import { SnapUIAvatar } from '../snap-ui-avatar'; export type SnapUIAddressProps = { // The address must be a CAIP-10 string. address: string; - diameter?: number; + // This is not currently exposed to Snaps. + avatarSize?: 'xs' | 'sm' | 'md' | 'lg'; }; export const SnapUIAddress: React.FunctionComponent = ({ address, - diameter = 32, + avatarSize = 'md', }) => { - const parsed = useMemo(() => { + const caipIdentifier = useMemo(() => { if (isHexString(address)) { // For legacy address inputs we assume them to be Ethereum addresses. // NOTE: This means the chain ID is not gonna be reliable. - return parseCaipAccountId(`eip155:1:${address}`); + return `eip155:1:${address}`; } - return parseCaipAccountId(address as CaipAccountId); + return address; }, [address]); - const useBlockie = useSelector(getUseBlockie); + + const parsed = useMemo( + () => parseCaipAccountId(caipIdentifier as CaipAccountId), + [caipIdentifier], + ); // For EVM addresses, we make sure they are checksummed. const transformedAddress = @@ -47,20 +49,7 @@ export const SnapUIAddress: React.FunctionComponent = ({ return ( - {useBlockie ? ( - - ) : ( - - )} + {shortenedAddress} ); diff --git a/ui/components/app/snaps/snap-ui-avatar/index.ts b/ui/components/app/snaps/snap-ui-avatar/index.ts new file mode 100644 index 000000000000..44fc129d6b39 --- /dev/null +++ b/ui/components/app/snaps/snap-ui-avatar/index.ts @@ -0,0 +1 @@ +export * from './snap-ui-avatar'; diff --git a/ui/components/app/snaps/snap-ui-avatar/snap-ui-avatar.tsx b/ui/components/app/snaps/snap-ui-avatar/snap-ui-avatar.tsx new file mode 100644 index 000000000000..7e6de5f3370b --- /dev/null +++ b/ui/components/app/snaps/snap-ui-avatar/snap-ui-avatar.tsx @@ -0,0 +1,44 @@ +import React, { useMemo } from 'react'; +import { useSelector } from 'react-redux'; +import { CaipAccountId, parseCaipAccountId } from '@metamask/utils'; +import BlockieIdenticon from '../../../ui/identicon/blockieIdenticon'; +import Jazzicon from '../../../ui/jazzicon'; +import { getUseBlockie } from '../../../../selectors'; + +export const DIAMETERS: Record = { + xs: 16, + sm: 24, + md: 32, + lg: 40, +}; + +export type SnapUIAvatarProps = { + // The address must be a CAIP-10 string. + address: string; + size?: 'xs' | 'sm' | 'md' | 'lg'; +}; + +export const SnapUIAvatar: React.FunctionComponent = ({ + address, + size = 'md', +}) => { + const parsed = useMemo(() => { + return parseCaipAccountId(address as CaipAccountId); + }, [address]); + const useBlockie = useSelector(getUseBlockie); + + return useBlockie ? ( + + ) : ( + + ); +}; diff --git a/ui/components/app/snaps/snap-ui-link/snap-ui-link.js b/ui/components/app/snaps/snap-ui-link/snap-ui-link.js index 46439c523a68..a1289543fd45 100644 --- a/ui/components/app/snaps/snap-ui-link/snap-ui-link.js +++ b/ui/components/app/snaps/snap-ui-link/snap-ui-link.js @@ -9,18 +9,39 @@ import { IconSize, } from '../../../component-library'; import SnapLinkWarning from '../snap-link-warning'; +import useSnapNavigation from '../../../../hooks/snaps/useSnapNavigation'; export const SnapUILink = ({ href, children }) => { const [isOpen, setIsOpen] = useState(false); + const isMetaMaskUrl = href.startsWith('metamask:'); + const { navigate } = useSnapNavigation(); + const handleLinkClick = () => { - setIsOpen(true); + if (isMetaMaskUrl) { + navigate(href); + } else { + setIsOpen(true); + } }; const handleModalClose = () => { setIsOpen(false); }; + if (isMetaMaskUrl) { + return ( + + {children} + + ); + } + return ( <> diff --git a/ui/components/app/snaps/snap-ui-renderer/components/address.ts b/ui/components/app/snaps/snap-ui-renderer/components/address.ts index 108ff37f33a5..1e39966df760 100644 --- a/ui/components/app/snaps/snap-ui-renderer/components/address.ts +++ b/ui/components/app/snaps/snap-ui-renderer/components/address.ts @@ -5,6 +5,6 @@ export const address: UIComponentFactory = ({ element }) => ({ element: 'SnapUIAddress', props: { address: element.props.address, - diameter: 16, + avatarSize: 'xs', }, }); diff --git a/ui/components/app/snaps/snap-ui-renderer/components/avatar.ts b/ui/components/app/snaps/snap-ui-renderer/components/avatar.ts new file mode 100644 index 000000000000..9572516383b6 --- /dev/null +++ b/ui/components/app/snaps/snap-ui-renderer/components/avatar.ts @@ -0,0 +1,9 @@ +import { AvatarElement } from '@metamask/snaps-sdk/jsx'; +import { UIComponentFactory } from './types'; + +export const avatar: UIComponentFactory = ({ element }) => ({ + element: 'SnapUIAvatar', + props: { + address: element.props.address, + }, +}); diff --git a/ui/components/app/snaps/snap-ui-renderer/components/field.ts b/ui/components/app/snaps/snap-ui-renderer/components/field.ts index 0bafec17b2bf..169619b9a561 100644 --- a/ui/components/app/snaps/snap-ui-renderer/components/field.ts +++ b/ui/components/app/snaps/snap-ui-renderer/components/field.ts @@ -14,6 +14,7 @@ import { radioGroup as radioGroupFn } from './radioGroup'; import { checkbox as checkboxFn } from './checkbox'; import { selector as selectorFn } from './selector'; import { UIComponentFactory, UIComponentParams } from './types'; +import { constructInputProps } from './input'; export const field: UIComponentFactory = ({ element, @@ -79,9 +80,7 @@ export const field: UIComponentFactory = ({ id: input.props.name, placeholder: input.props.placeholder, label: element.props.label, - textFieldProps: { - type: input.props.type, - }, + ...constructInputProps(input.props), name: input.props.name, form, error: element.props.error !== undefined, diff --git a/ui/components/app/snaps/snap-ui-renderer/components/heading.ts b/ui/components/app/snaps/snap-ui-renderer/components/heading.ts index 709868fd4a6e..f0d6dee396d1 100644 --- a/ui/components/app/snaps/snap-ui-renderer/components/heading.ts +++ b/ui/components/app/snaps/snap-ui-renderer/components/heading.ts @@ -5,11 +5,24 @@ import { } from '../../../../../helpers/constants/design-system'; import { UIComponentFactory } from './types'; +export const generateSize = (size: HeadingElement['props']['size']) => { + switch (size) { + case 'sm': + return TextVariant.headingSm; + case 'md': + return TextVariant.headingMd; + case 'lg': + return TextVariant.headingLg; + default: + return TextVariant.headingSm; + } +}; + export const heading: UIComponentFactory = ({ element }) => ({ element: 'Text', children: element.props.children, props: { - variant: TextVariant.headingSm, + variant: generateSize(element.props.size), overflowWrap: OverflowWrap.Anywhere, }, }); diff --git a/ui/components/app/snaps/snap-ui-renderer/components/index.ts b/ui/components/app/snaps/snap-ui-renderer/components/index.ts index 5d3b8fa16789..17a9b6aa37c1 100644 --- a/ui/components/app/snaps/snap-ui-renderer/components/index.ts +++ b/ui/components/app/snaps/snap-ui-renderer/components/index.ts @@ -26,6 +26,7 @@ import { container } from './container'; import { selector } from './selector'; import { icon } from './icon'; import { section } from './section'; +import { avatar } from './avatar'; export const COMPONENT_MAPPING = { Box: box, @@ -38,6 +39,7 @@ export const COMPONENT_MAPPING = { Copyable: copyable, Row: row, Address: address, + Avatar: avatar, Button: button, FileInput: fileInput, Form: form, diff --git a/ui/components/app/snaps/snap-ui-renderer/components/input.ts b/ui/components/app/snaps/snap-ui-renderer/components/input.ts index 9cc565d5d7f5..beda6c5ba4cc 100644 --- a/ui/components/app/snaps/snap-ui-renderer/components/input.ts +++ b/ui/components/app/snaps/snap-ui-renderer/components/input.ts @@ -1,16 +1,50 @@ -import { InputElement } from '@metamask/snaps-sdk/jsx'; +import { InputElement, NumberInputProps } from '@metamask/snaps-sdk/jsx'; +import { hasProperty } from '@metamask/utils'; import { UIComponentFactory } from './types'; -export const input: UIComponentFactory = ({ element, form }) => ({ - element: 'SnapUIInput', - props: { - id: element.props.name, - placeholder: element.props.placeholder, - textFieldProps: { - type: element.props.type, +export const constructInputProps = (props: InputElement['props']) => { + if (!hasProperty(props, 'type')) { + return { + textFieldProps: { + type: 'text', + }, + }; + } + + switch (props.type) { + case 'number': { + const { step, min, max, type } = props as NumberInputProps; + + return { + textFieldProps: { + type, + inputProps: { + step: step?.toString(), + min: min?.toString(), + max: max?.toString(), + }, + }, + }; + } + default: + return { + textFieldProps: { + type: props.type, + }, + }; + } +}; + +export const input: UIComponentFactory = ({ element, form }) => { + return { + element: 'SnapUIInput', + props: { + id: element.props.name, + placeholder: element.props.placeholder, + ...constructInputProps(element.props), + name: element.props.name, + form, }, - name: element.props.name, - form, - }, -}); + }; +}; diff --git a/ui/components/institutional/interactive-replacement-token-modal/interactive-replacement-token-modal.tsx b/ui/components/institutional/interactive-replacement-token-modal/interactive-replacement-token-modal.tsx index 4526595fa455..06fe1336ca7e 100644 --- a/ui/components/institutional/interactive-replacement-token-modal/interactive-replacement-token-modal.tsx +++ b/ui/components/institutional/interactive-replacement-token-modal/interactive-replacement-token-modal.tsx @@ -85,30 +85,26 @@ const InteractiveReplacementTokenModal: React.FC = () => { {t('custodyRefreshTokenModalTitle')} - { - // @ts-expect-error: todo: Merge MetaMask Institutional PR 778 to fix this - custodian.iconUrl ? ( - - - {custodian.displayName} - - - ) : ( + {custodian.iconUrl ? ( + - {custodian.displayName} + {custodian.displayName} - ) - } + + ) : ( + + {custodian.displayName} + + )} , store, diff --git a/ui/components/multichain/edit-accounts-modal/edit-accounts-modal.tsx b/ui/components/multichain/edit-accounts-modal/edit-accounts-modal.tsx index ba842efc6a11..ddc2749e2ee7 100644 --- a/ui/components/multichain/edit-accounts-modal/edit-accounts-modal.tsx +++ b/ui/components/multichain/edit-accounts-modal/edit-accounts-modal.tsx @@ -27,17 +27,17 @@ import { IconColor, FlexDirection, AlignItems, + BlockSize, } from '../../../helpers/constants/design-system'; -import { getURLHost } from '../../../helpers/utils/util'; import { MergedInternalAccount } from '../../../selectors/selectors.types'; import { MetaMetricsEventCategory, MetaMetricsEventName, } from '../../../../shared/constants/metametrics'; import { MetaMetricsContext } from '../../../contexts/metametrics'; +import { isEqualCaseInsensitive } from '../../../../shared/modules/string-utils'; type EditAccountsModalProps = { - activeTabOrigin: string; accounts: MergedInternalAccount[]; defaultSelectedAccountAddresses: string[]; onClose: () => void; @@ -45,7 +45,6 @@ type EditAccountsModalProps = { }; export const EditAccountsModal: React.FC = ({ - activeTabOrigin, accounts, defaultSelectedAccountAddresses, onClose, @@ -55,7 +54,6 @@ export const EditAccountsModal: React.FC = ({ const trackEvent = useContext(MetaMetricsContext); const [showAddNewAccounts, setShowAddNewAccounts] = useState(false); - const [selectedAccountAddresses, setSelectedAccountAddresses] = useState( defaultSelectedAccountAddresses, ); @@ -83,151 +81,151 @@ export const EditAccountsModal: React.FC = ({ } }; - const allAreSelected = () => { - return accounts.length === selectedAccountAddresses.length; - }; - + const allAreSelected = () => + accounts.length === selectedAccountAddresses.length; const checked = allAreSelected(); const isIndeterminate = !checked && selectedAccountAddresses.length > 0; - const hostName = getURLHost(activeTabOrigin); - const defaultSet = new Set(defaultSelectedAccountAddresses); const selectedSet = new Set(selectedAccountAddresses); return ( - <> - - - - {t('editAccounts')} - - {showAddNewAccounts ? ( - - setShowAddNewAccounts(false)} + + + + {t('editAccounts')} + + {showAddNewAccounts ? ( + + setShowAddNewAccounts(false)} + /> + + ) : ( + <> + + + allAreSelected() ? deselectAll() : selectAll() + } + isIndeterminate={isIndeterminate} /> + setShowAddNewAccounts(true)}> + {t('newAccount')} + - ) : ( - <> - - - allAreSelected() ? deselectAll() : selectAll() - } - isIndeterminate={isIndeterminate} - /> - setShowAddNewAccounts(true)}> - {t('newAccount')} - - - {accounts.map((account) => ( - handleAccountClick(account.address)} - account={account} - key={account.address} - isPinned={Boolean(account.pinned)} - startAccessory={ - - } - selected={false} - /> - ))} - - - {selectedAccountAddresses.length === 0 ? ( - - - - - {t('disconnectMessage', [hostName])} - - - { - onSubmit([]); - onClose(); - }} - size={ButtonPrimarySize.Lg} - block - danger - > - {t('disconnect')} - - - ) : ( - { - // Get accounts that are in `selectedAccountAddresses` but not in `defaultSelectedAccountAddresses` - const addedAccounts = selectedAccountAddresses.filter( - (address) => !defaultSet.has(address), - ); - // Get accounts that are in `defaultSelectedAccountAddresses` but not in `selectedAccountAddresses` - const removedAccounts = - defaultSelectedAccountAddresses.filter( - (address) => !selectedSet.has(address), - ); - - onSubmit(selectedAccountAddresses); - trackEvent({ - category: MetaMetricsEventCategory.Permissions, - event: - MetaMetricsEventName.UpdatePermissionedAccounts, - properties: { - addedAccounts: addedAccounts.length, - removedAccounts: removedAccounts.length, - location: 'Edit Accounts Modal', - }, - }); - - onClose(); - }} - size={ButtonPrimarySize.Lg} - block - > - {t('update')} - - )} - - - )} - - - - + {accounts.map((account) => ( + handleAccountClick(account.address)} + account={account} + key={account.address} + isPinned={Boolean(account.pinned)} + startAccessory={ + + isEqualCaseInsensitive( + selectedAccountAddress, + account.address, + ), + )} + /> + } + selected={false} + /> + ))} + + )} + + + + {selectedAccountAddresses.length === 0 ? ( + + + + + {t('disconnectMessage')} + + + { + onSubmit([]); + onClose(); + }} + size={ButtonPrimarySize.Lg} + block + danger + > + {t('disconnect')} + + + ) : ( + { + const addedAccounts = selectedAccountAddresses.filter( + (address) => !defaultSet.has(address), + ); + const removedAccounts = defaultSelectedAccountAddresses.filter( + (address) => !selectedSet.has(address), + ); + + onSubmit(selectedAccountAddresses); + trackEvent({ + category: MetaMetricsEventCategory.Permissions, + event: MetaMetricsEventName.UpdatePermissionedAccounts, + properties: { + addedAccounts: addedAccounts.length, + removedAccounts: removedAccounts.length, + location: 'Edit Accounts Modal', + }, + }); + + onClose(); + }} + size={ButtonPrimarySize.Lg} + block + > + {t('update')} + + )} + + + ); }; diff --git a/ui/components/multichain/edit-accounts-modal/index.scss b/ui/components/multichain/edit-accounts-modal/index.scss new file mode 100644 index 000000000000..887b8afb8183 --- /dev/null +++ b/ui/components/multichain/edit-accounts-modal/index.scss @@ -0,0 +1,5 @@ +.edit-accounts-modal { + &__body { + overflow: auto; + } +} diff --git a/ui/components/multichain/edit-networks-modal/edit-networks-modal.js b/ui/components/multichain/edit-networks-modal/edit-networks-modal.js index e4a7c391b4df..0b86716af50a 100644 --- a/ui/components/multichain/edit-networks-modal/edit-networks-modal.js +++ b/ui/components/multichain/edit-networks-modal/edit-networks-modal.js @@ -2,9 +2,11 @@ import React, { useContext, useEffect, useState } from 'react'; import PropTypes from 'prop-types'; import { AlignItems, + BlockSize, Display, FlexDirection, IconColor, + JustifyContent, TextColor, TextVariant, } from '../../../helpers/constants/design-system'; @@ -26,7 +28,6 @@ import { IconSize, } from '../../component-library'; import { NetworkListItem } from '..'; -import { getURLHost } from '../../../helpers/utils/util'; import { CHAIN_ID_TO_NETWORK_IMAGE_URL_MAP } from '../../../../shared/constants/network'; import { MetaMetricsEventCategory, @@ -35,7 +36,6 @@ import { import { MetaMetricsContext } from '../../../contexts/metametrics'; export const EditNetworksModal = ({ - activeTabOrigin, nonTestNetworks, testNetworks, defaultSelectedChainIds, @@ -80,8 +80,6 @@ export const EditNetworksModal = ({ const checked = allAreSelected(); const isIndeterminate = !checked && selectedChainIds.length > 0; - const hostName = getURLHost(activeTabOrigin); - const defaultChainIdsSet = new Set(defaultSelectedChainIds); const selectedChainIdsSet = new Set(selectedChainIds); @@ -102,7 +100,11 @@ export const EditNetworksModal = ({ > {t('editNetworksTitle')} - + ))} - - {selectedChainIds.length === 0 ? ( + + + {selectedChainIds.length === 0 ? ( + - - - - {t('disconnectMessage', [hostName])} - - - { - onSubmit([]); - onClose(); - }} - size={ButtonPrimarySize.Lg} - block - danger + + - {t('disconnect')} - + {t('disconnectMessage')} + - ) : ( { onSubmit(selectedChainIds); // Get networks that are in `selectedChainIds` but not in `defaultSelectedChainIds` @@ -211,23 +203,31 @@ export const EditNetworksModal = ({ }} size={ButtonPrimarySize.Lg} block + danger > - {t('update')} + {t('disconnect')} - )} - - + + ) : ( + { + onSubmit(selectedChainIds); + onClose(); + }} + size={ButtonPrimarySize.Lg} + block + > + {t('update')} + + )} + ); }; EditNetworksModal.propTypes = { - /** - * Origin for the active tab. - */ - activeTabOrigin: PropTypes.string, - /** * Array of network objects representing available non-test networks to choose from. */ diff --git a/ui/components/multichain/edit-networks-modal/index.scss b/ui/components/multichain/edit-networks-modal/index.scss new file mode 100644 index 000000000000..113351b8cd2e --- /dev/null +++ b/ui/components/multichain/edit-networks-modal/index.scss @@ -0,0 +1,5 @@ +.edit-networks-modal { + &__body { + overflow: auto; + } +} diff --git a/ui/components/multichain/import-account/import-account.js b/ui/components/multichain/import-account/import-account.js index cf5d13494e07..a37958074003 100644 --- a/ui/components/multichain/import-account/import-account.js +++ b/ui/components/multichain/import-account/import-account.js @@ -1,6 +1,7 @@ import React, { useContext, useState } from 'react'; import PropTypes from 'prop-types'; import { useDispatch } from 'react-redux'; +import { getErrorMessage } from '../../../../shared/modules/error'; import { MetaMetricsEventAccountImportType, MetaMetricsEventAccountType, @@ -50,8 +51,9 @@ export const ImportAccount = ({ onActionComplete }) => { return false; } } catch (error) { - trackImportEvent(strategy, error.message); - translateWarning(error.message); + const message = getErrorMessage(error); + trackImportEvent(strategy, message); + translateWarning(message); return false; } diff --git a/ui/components/multichain/import-nfts-modal/import-nfts-modal.js b/ui/components/multichain/import-nfts-modal/import-nfts-modal.js index 3d40209ba62b..8b85e5cf1a4b 100644 --- a/ui/components/multichain/import-nfts-modal/import-nfts-modal.js +++ b/ui/components/multichain/import-nfts-modal/import-nfts-modal.js @@ -3,6 +3,7 @@ import PropTypes from 'prop-types'; import React, { useContext, useState } from 'react'; import { useDispatch, useSelector } from 'react-redux'; import { useHistory } from 'react-router-dom'; +import { getErrorMessage } from '../../../../shared/modules/error'; import { MetaMetricsEventName, MetaMetricsTokenEventSource, @@ -95,7 +96,7 @@ export const ImportNftsModal = ({ onClose }) => { dispatch(updateNftDropDownState(newNftDropdownState)); } catch (error) { - const { message } = error; + const message = getErrorMessage(error); dispatch(setNewNftAddedMessage(message)); setNftAddFailed(true); return; diff --git a/ui/components/multichain/multichain-components.scss b/ui/components/multichain/multichain-components.scss index 2d2d6b3fdef0..bf3191c7e994 100644 --- a/ui/components/multichain/multichain-components.scss +++ b/ui/components/multichain/multichain-components.scss @@ -19,6 +19,8 @@ @import 'connected-site-menu'; @import 'create-named-snap-account'; @import 'dropdown-editor'; +@import "edit-accounts-modal"; +@import "edit-networks-modal"; @import 'token-list-item'; @import 'network-list-item'; @import 'network-list-item-menu'; diff --git a/ui/components/multichain/pages/review-permissions-page/review-permissions-page.tsx b/ui/components/multichain/pages/review-permissions-page/review-permissions-page.tsx index b12fea776c65..66e7cadd7546 100644 --- a/ui/components/multichain/pages/review-permissions-page/review-permissions-page.tsx +++ b/ui/components/multichain/pages/review-permissions-page/review-permissions-page.tsx @@ -195,7 +195,6 @@ export const ReviewPermissions = () => { onSelectChainIds={handleSelectChainIds} selectedAccountAddresses={connectedAccountAddresses} selectedChainIds={connectedChainIds} - activeTabOrigin={activeTabOrigin} /> ) : ( diff --git a/ui/components/multichain/pages/review-permissions-page/site-cell/site-cell.tsx b/ui/components/multichain/pages/review-permissions-page/site-cell/site-cell.tsx index bb3a14a8f5e8..ae7a93283ead 100644 --- a/ui/components/multichain/pages/review-permissions-page/site-cell/site-cell.tsx +++ b/ui/components/multichain/pages/review-permissions-page/site-cell/site-cell.tsx @@ -19,6 +19,7 @@ import { MetaMetricsEventCategory, MetaMetricsEventName, } from '../../../../../../shared/constants/metametrics'; +import { isEqualCaseInsensitive } from '../../../../../../shared/modules/string-utils'; import { SiteCellTooltip } from './site-cell-tooltip'; import { SiteCellConnectionListItem } from './site-cell-connection-list-item'; @@ -36,7 +37,6 @@ type SiteCellProps = { onSelectChainIds: (chainIds: Hex[]) => void; selectedAccountAddresses: string[]; selectedChainIds: string[]; - activeTabOrigin: string; isConnectFlow?: boolean; }; @@ -48,7 +48,6 @@ export const SiteCell: React.FC = ({ onSelectChainIds, selectedAccountAddresses, selectedChainIds, - activeTabOrigin, isConnectFlow, }) => { const t = useI18nContext(); @@ -59,7 +58,9 @@ export const SiteCell: React.FC = ({ const [showEditNetworksModal, setShowEditNetworksModal] = useState(false); const selectedAccounts = accounts.filter(({ address }) => - selectedAccountAddresses.includes(address), + selectedAccountAddresses.some((selectedAccountAddress) => + isEqualCaseInsensitive(selectedAccountAddress, address), + ), ); const selectedNetworks = allNetworks.filter(({ chainId }) => selectedChainIds.includes(chainId), @@ -145,7 +146,6 @@ export const SiteCell: React.FC = ({ {showEditAccountsModal && ( setShowEditAccountsModal(false)} @@ -155,7 +155,6 @@ export const SiteCell: React.FC = ({ {showEditNetworksModal && ( clearTimeout(timeout); }, [timeLeft]); + // use to track when a quote is requested and received + useEffect(() => { + if (isSwapQuoteLoading) { + trackEvent( + { + event: MetaMetricsEventName.sendSwapQuoteRequested, + category: MetaMetricsEventCategory.Send, + sensitiveProperties: { + ...sendAnalytics, + }, + }, + { excludeMetaMetricsId: false }, + ); + } else if (bestQuote) { + trackEvent( + { + event: MetaMetricsEventName.sendSwapQuoteReceived, + category: MetaMetricsEventCategory.Send, + sensitiveProperties: { + ...sendAnalytics, + }, + }, + { excludeMetaMetricsId: false }, + ); + } + }, [isSwapQuoteLoading]); + const infoText = useMemo(() => { if (isSwapQuoteLoading) { return t('swapFetchingQuotes'); diff --git a/ui/components/multichain/pages/send/send.js b/ui/components/multichain/pages/send/send.js index 3fe8ef3652eb..b77a5e7f5810 100644 --- a/ui/components/multichain/pages/send/send.js +++ b/ui/components/multichain/pages/send/send.js @@ -260,6 +260,9 @@ export const SendPage = () => { { event: MetaMetricsEventName.sendSwapQuoteError, category: MetaMetricsEventCategory.Send, + properties: { + error: swapQuotesError, + }, sensitiveProperties: { ...sendAnalytics, }, diff --git a/ui/ducks/bridge/actions.ts b/ui/ducks/bridge/actions.ts index 5bfbda1e23cf..5e50b004b774 100644 --- a/ui/ducks/bridge/actions.ts +++ b/ui/ducks/bridge/actions.ts @@ -18,9 +18,17 @@ const { setFromToken, setToToken, setFromTokenInputValue, + resetInputFields, + switchToAndFromTokens, } = bridgeSlice.actions; -export { setFromToken, setToToken, setFromTokenInputValue }; +export { + setFromToken, + setToToken, + setFromTokenInputValue, + switchToAndFromTokens, + resetInputFields, +}; const callBridgeControllerMethod = ( bridgeAction: BridgeUserAction | BridgeBackgroundAction, diff --git a/ui/ducks/bridge/bridge.test.ts b/ui/ducks/bridge/bridge.test.ts index b8d2e09eb0ea..f4a566c233b5 100644 --- a/ui/ducks/bridge/bridge.test.ts +++ b/ui/ducks/bridge/bridge.test.ts @@ -17,6 +17,8 @@ import { setToChain, setToToken, setFromChain, + resetInputFields, + switchToAndFromTokens, } from './actions'; const middleware = [thunk]; @@ -43,9 +45,9 @@ describe('Ducks - Bridge', () => { // Check redux state const actions = store.getActions(); - expect(actions[0].type).toBe('bridge/setToChainId'); + expect(actions[0].type).toStrictEqual('bridge/setToChainId'); const newState = bridgeReducer(state, actions[0]); - expect(newState.toChainId).toBe(actionPayload); + expect(newState.toChainId).toStrictEqual(actionPayload); // Check background state expect(mockSelectDestNetwork).toHaveBeenCalledTimes(1); expect(mockSelectDestNetwork).toHaveBeenCalledWith( @@ -61,9 +63,9 @@ describe('Ducks - Bridge', () => { const actionPayload = { symbol: 'SYMBOL', address: '0x13341432' }; store.dispatch(setFromToken(actionPayload)); const actions = store.getActions(); - expect(actions[0].type).toBe('bridge/setFromToken'); + expect(actions[0].type).toStrictEqual('bridge/setFromToken'); const newState = bridgeReducer(state, actions[0]); - expect(newState.fromToken).toBe(actionPayload); + expect(newState.fromToken).toStrictEqual(actionPayload); }); }); @@ -73,9 +75,9 @@ describe('Ducks - Bridge', () => { const actionPayload = { symbol: 'SYMBOL', address: '0x13341431' }; store.dispatch(setToToken(actionPayload)); const actions = store.getActions(); - expect(actions[0].type).toBe('bridge/setToToken'); + expect(actions[0].type).toStrictEqual('bridge/setToToken'); const newState = bridgeReducer(state, actions[0]); - expect(newState.toToken).toBe(actionPayload); + expect(newState.toToken).toStrictEqual(actionPayload); }); }); @@ -85,9 +87,9 @@ describe('Ducks - Bridge', () => { const actionPayload = '10'; store.dispatch(setFromTokenInputValue(actionPayload)); const actions = store.getActions(); - expect(actions[0].type).toBe('bridge/setFromTokenInputValue'); + expect(actions[0].type).toStrictEqual('bridge/setFromTokenInputValue'); const newState = bridgeReducer(state, actions[0]); - expect(newState.fromTokenInputValue).toBe(actionPayload); + expect(newState.fromTokenInputValue).toStrictEqual(actionPayload); }); }); @@ -118,4 +120,48 @@ describe('Ducks - Bridge', () => { ); }); }); + + describe('resetInputFields', () => { + it('resets to initalState', async () => { + const state = store.getState().bridge; + store.dispatch(resetInputFields()); + const actions = store.getActions(); + expect(actions[0].type).toStrictEqual('bridge/resetInputFields'); + const newState = bridgeReducer(state, actions[0]); + expect(newState).toStrictEqual({ + toChainId: null, + fromToken: null, + toToken: null, + fromTokenInputValue: null, + }); + }); + }); + + describe('switchToAndFromTokens', () => { + it('switches to and from input values', async () => { + // eslint-disable-next-line @typescript-eslint/no-explicit-any + const bridgeStore = configureMockStore(middleware)( + createBridgeMockStore( + {}, + { + toChainId: CHAIN_IDS.MAINNET, + fromToken: { symbol: 'WETH', address: '0x13341432' }, + toToken: { symbol: 'USDC', address: '0x13341431' }, + fromTokenInputValue: '10', + }, + ), + ); + const state = bridgeStore.getState().bridge; + bridgeStore.dispatch(switchToAndFromTokens(CHAIN_IDS.POLYGON)); + const actions = bridgeStore.getActions(); + expect(actions[0].type).toStrictEqual('bridge/switchToAndFromTokens'); + const newState = bridgeReducer(state, actions[0]); + expect(newState).toStrictEqual({ + toChainId: CHAIN_IDS.POLYGON, + fromToken: { symbol: 'USDC', address: '0x13341431' }, + toToken: { symbol: 'WETH', address: '0x13341432' }, + fromTokenInputValue: null, + }); + }); + }); }); diff --git a/ui/ducks/bridge/bridge.ts b/ui/ducks/bridge/bridge.ts index f2469d1025f3..9ec744d9e953 100644 --- a/ui/ducks/bridge/bridge.ts +++ b/ui/ducks/bridge/bridge.ts @@ -36,6 +36,15 @@ const bridgeSlice = createSlice({ setFromTokenInputValue: (state, action) => { state.fromTokenInputValue = action.payload; }, + resetInputFields: () => ({ + ...initialState, + }), + switchToAndFromTokens: (state, { payload }) => ({ + toChainId: payload, + fromToken: state.toToken, + toToken: state.fromToken, + fromTokenInputValue: null, + }), }, }); diff --git a/ui/ducks/send/helpers.js b/ui/ducks/send/helpers.js index 43582c6e3504..b48e05034f59 100644 --- a/ui/ducks/send/helpers.js +++ b/ui/ducks/send/helpers.js @@ -2,6 +2,7 @@ import { addHexPrefix, toChecksumAddress } from 'ethereumjs-util'; import abi from 'human-standard-token-abi'; import BigNumber from 'bignumber.js'; import { TransactionEnvelopeType } from '@metamask/transaction-controller'; +import { getErrorMessage } from '../../../shared/modules/error'; import { GAS_LIMITS, MIN_GAS_LIMIT_HEX } from '../../../shared/constants/gas'; import { calcTokenAmount } from '../../../shared/lib/transactions-controller-utils'; import { CHAIN_ID_TO_GAS_LIMIT_BUFFER_MAP } from '../../../shared/constants/network'; @@ -157,13 +158,14 @@ export async function estimateGasLimitForSend({ ); return addHexPrefix(estimateWithBuffer); } catch (error) { + const errorMessage = getErrorMessage(error); const simulationFailed = - error.message.includes('Transaction execution error.') || - error.message.includes( + errorMessage.includes('Transaction execution error.') || + errorMessage.includes( 'gas required exceeds allowance or always failing transaction', ) || (CHAIN_ID_TO_GAS_LIMIT_BUFFER_MAP[chainId] && - error.message.includes('gas required exceeds allowance')); + errorMessage.includes('gas required exceeds allowance')); if (simulationFailed) { const estimateWithBuffer = addGasBuffer( paramsForGasEstimate?.gas ?? gasLimit, diff --git a/ui/ducks/send/send.js b/ui/ducks/send/send.js index cdbe7d2daa86..700fd466004d 100644 --- a/ui/ducks/send/send.js +++ b/ui/ducks/send/send.js @@ -7,11 +7,12 @@ import BigNumber from 'bignumber.js'; import { addHexPrefix, zeroAddress } from 'ethereumjs-util'; import { cloneDeep, debounce } from 'lodash'; import { v4 as uuidv4 } from 'uuid'; +import { providerErrors } from '@metamask/rpc-errors'; import { TransactionEnvelopeType, TransactionType, } from '@metamask/transaction-controller'; -import { ethErrors } from 'eth-rpc-errors'; +import { getErrorMessage } from '../../../shared/modules/error'; import { decimalToHex, hexToDecimal, @@ -2702,12 +2703,13 @@ export function updateSendAsset( details.tokenId, ); } catch (err) { - if (err.message.includes('Unable to verify ownership.')) { + const message = getErrorMessage(err); + if (message.includes('Unable to verify ownership.')) { // this would indicate that either our attempts to verify ownership failed because of network issues, // or, somehow a token has been added to NFTs state with an incorrect chainId. } else { // Any other error is unexpected and should be surfaced. - dispatch(displayWarning(err.message)); + dispatch(displayWarning(err)); } } @@ -2965,7 +2967,7 @@ export function signTransaction(history) { await dispatch( rejectPendingApproval( unapprovedSendTx.id, - ethErrors.provider.userRejectedRequest().serialize(), + providerErrors.userRejectedRequest().serialize(), ), ); } diff --git a/ui/ducks/swaps/swaps.js b/ui/ducks/swaps/swaps.js index ece4292fdf14..91ed081eb719 100644 --- a/ui/ducks/swaps/swaps.js +++ b/ui/ducks/swaps/swaps.js @@ -751,7 +751,7 @@ export const fetchQuotesAndSetQuoteState = ( const currentSmartTransactionsEnabled = getCurrentSmartTransactionsEnabled(state); trackEvent({ - event: 'Quotes Requested', + event: MetaMetricsEventName.QuotesRequested, category: MetaMetricsEventCategory.Swaps, sensitiveProperties: { token_from: fromTokenSymbol, @@ -839,7 +839,7 @@ export const fetchQuotesAndSetQuoteState = ( const tokenToAmountToString = tokenToAmountBN.toString(10); trackEvent({ - event: 'Quotes Received', + event: MetaMetricsEventName.QuotesReceived, category: MetaMetricsEventCategory.Swaps, sensitiveProperties: { token_from: fromTokenSymbol, diff --git a/ui/hooks/bridge/useBridging.test.ts b/ui/hooks/bridge/useBridging.test.ts index df8bbb940f4e..6e3f3b534e35 100644 --- a/ui/hooks/bridge/useBridging.test.ts +++ b/ui/hooks/bridge/useBridging.test.ts @@ -174,7 +174,7 @@ describe('useBridging', () => { result.current.openBridgeExperience(location, token, urlSuffix); - expect(mockDispatch.mock.calls).toHaveLength(3); + expect(mockDispatch.mock.calls).toHaveLength(2); expect(mockHistoryPush.mock.calls).toHaveLength(1); expect(mockHistoryPush).toHaveBeenCalledWith(expectedUrl); expect(openTabSpy).not.toHaveBeenCalled(); diff --git a/ui/hooks/bridge/useBridging.ts b/ui/hooks/bridge/useBridging.ts index ce4b8c48b89c..a68aeb361bdd 100644 --- a/ui/hooks/bridge/useBridging.ts +++ b/ui/hooks/bridge/useBridging.ts @@ -1,10 +1,7 @@ import { useCallback, useContext, useEffect } from 'react'; import { useDispatch, useSelector } from 'react-redux'; import { useHistory } from 'react-router-dom'; -import { - setBridgeFeatureFlags, - setFromChain, -} from '../../ducks/bridge/actions'; +import { setBridgeFeatureFlags } from '../../ducks/bridge/actions'; import { ///: BEGIN:ONLY_INCLUDE_IF(build-main,build-beta,build-flask) getCurrentKeyring, @@ -55,13 +52,6 @@ const useBridging = () => { dispatch(setBridgeFeatureFlags()); }, [dispatch, setBridgeFeatureFlags]); - useEffect(() => { - isBridgeChain && - isBridgeSupported && - providerConfig && - dispatch(setFromChain(providerConfig.chainId)); - }, []); - const openBridgeExperience = useCallback( ( location: string, diff --git a/ui/hooks/bridge/useLatestBalance.test.ts b/ui/hooks/bridge/useLatestBalance.test.ts new file mode 100644 index 000000000000..d1186c3eeb91 --- /dev/null +++ b/ui/hooks/bridge/useLatestBalance.test.ts @@ -0,0 +1,89 @@ +import { BigNumber } from 'ethers'; +import { renderHookWithProvider } from '../../../test/lib/render-helpers'; +import { CHAIN_IDS } from '../../../shared/constants/network'; +import { createBridgeMockStore } from '../../../test/jest/mock-store'; +import { zeroAddress } from '../../__mocks__/ethereumjs-util'; +import { createTestProviderTools } from '../../../test/stub/provider'; +import * as tokenutil from '../../../shared/lib/token-util'; +import useLatestBalance from './useLatestBalance'; + +const mockGetBalance = jest.fn(); +jest.mock('@ethersproject/providers', () => { + return { + Web3Provider: jest.fn().mockImplementation(() => { + return { + getBalance: mockGetBalance, + }; + }), + }; +}); + +const mockFetchTokenBalance = jest.spyOn(tokenutil, 'fetchTokenBalance'); +jest.mock('../../../shared/lib/token-util', () => ({ + ...jest.requireActual('../../../shared/lib/token-util'), + fetchTokenBalance: jest.fn(), +})); + +const renderUseLatestBalance = ( + token: { address: string; decimals?: number | string }, + chainId: string, + mockStoreState: object, +) => + renderHookWithProvider( + // eslint-disable-next-line @typescript-eslint/no-explicit-any + () => useLatestBalance(token as any, chainId as any), + mockStoreState, + ); + +describe('useLatestBalance', () => { + beforeEach(() => { + jest.clearAllMocks(); + const { provider } = createTestProviderTools({ + networkId: 'Ethereum', + chainId: CHAIN_IDS.MAINNET, + }); + + // eslint-disable-next-line @typescript-eslint/no-explicit-any + global.ethereumProvider = provider as any; + }); + + it('returns formattedBalance for native asset in current chain', async () => { + mockGetBalance.mockResolvedValue(BigNumber.from('1000000000000000000')); + + const { result, waitForNextUpdate } = renderUseLatestBalance( + { address: zeroAddress(), decimals: 18 }, + CHAIN_IDS.MAINNET, + createBridgeMockStore(), + ); + + await waitForNextUpdate(); + expect(result.current.formattedBalance).toStrictEqual('1'); + + expect(mockGetBalance).toHaveBeenCalledTimes(1); + expect(mockGetBalance).toHaveBeenCalledWith( + '0x0dcd5d886577d5081b0c52e242ef29e70be3e7bc', + ); + expect(mockFetchTokenBalance).toHaveBeenCalledTimes(0); + }); + + it('returns formattedBalance for ERC20 asset in current chain', async () => { + mockFetchTokenBalance.mockResolvedValueOnce(BigNumber.from('15390000')); + + const { result, waitForNextUpdate } = renderUseLatestBalance( + { address: '0xa0b86991c6218b36c1d19d4a2e9eb0ce3606eb48', decimals: '6' }, + CHAIN_IDS.MAINNET, + createBridgeMockStore(), + ); + + await waitForNextUpdate(); + expect(result.current.formattedBalance).toStrictEqual('15.39'); + + expect(mockFetchTokenBalance).toHaveBeenCalledTimes(1); + expect(mockFetchTokenBalance).toHaveBeenCalledWith( + '0xa0b86991c6218b36c1d19d4a2e9eb0ce3606eb48', + '0x0dcd5d886577d5081b0c52e242ef29e70be3e7bc', + global.ethereumProvider, + ); + expect(mockGetBalance).toHaveBeenCalledTimes(0); + }); +}); diff --git a/ui/hooks/bridge/useLatestBalance.ts b/ui/hooks/bridge/useLatestBalance.ts new file mode 100644 index 000000000000..dfe868a04090 --- /dev/null +++ b/ui/hooks/bridge/useLatestBalance.ts @@ -0,0 +1,66 @@ +import { useSelector } from 'react-redux'; +import { zeroAddress } from 'ethereumjs-util'; +import { Web3Provider } from '@ethersproject/providers'; +import { Hex } from '@metamask/utils'; +import { BigNumber } from 'ethers'; +import { Numeric } from '../../../shared/modules/Numeric'; +import { DEFAULT_PRECISION } from '../useCurrencyDisplay'; +import { fetchTokenBalance } from '../../../shared/lib/token-util'; +import { + getCurrentChainId, + getSelectedInternalAccount, + SwapsEthToken, +} from '../../selectors'; +import { SwapsTokenObject } from '../../../shared/constants/swaps'; +import { useAsyncResult } from '../useAsyncResult'; + +/** + * Custom hook to fetch and format the latest balance of a given token or native asset. + * + * @param token - The token object for which the balance is to be fetched. Can be null. + * @param chainId - The chain ID to be used for fetching the balance. Optional. + * @returns An object containing the formatted balance as a string. + */ +const useLatestBalance = ( + token: SwapsTokenObject | SwapsEthToken | null, + chainId?: Hex, +) => { + const { address: selectedAddress } = useSelector(getSelectedInternalAccount); + const currentChainId = useSelector(getCurrentChainId); + + const { value: latestBalance } = useAsyncResult(async () => { + if (token && chainId && currentChainId === chainId) { + if (!token.address || token.address === zeroAddress()) { + const ethersProvider = new Web3Provider(global.ethereumProvider); + return await ethersProvider.getBalance(selectedAddress); + } + return await fetchTokenBalance( + token.address, + selectedAddress, + global.ethereumProvider, + ); + } + + return undefined; + }, [token, selectedAddress, global.ethereumProvider]); + + if (token && !token.decimals) { + throw new Error( + `Failed to calculate latest balance - ${token.symbol} token is missing "decimals" value`, + ); + } + + const tokenDecimals = token?.decimals ? Number(token.decimals) : 1; + + return { + formattedBalance: + token && latestBalance + ? Numeric.from(latestBalance.toString(), 10) + .shiftedBy(tokenDecimals) + .round(DEFAULT_PRECISION) + .toString() + : undefined, + }; +}; + +export default useLatestBalance; diff --git a/ui/hooks/snaps/useSnapNavigation.ts b/ui/hooks/snaps/useSnapNavigation.ts new file mode 100644 index 000000000000..047b6fb13d36 --- /dev/null +++ b/ui/hooks/snaps/useSnapNavigation.ts @@ -0,0 +1,22 @@ +import { useHistory } from 'react-router-dom'; +import { parseMetaMaskUrl } from '@metamask/snaps-utils'; +import { getSnapRoute } from '../../helpers/utils/util'; + +const useSnapNavigation = () => { + const history = useHistory(); + const navigate = (url: string) => { + let path; + const linkData = parseMetaMaskUrl(url); + if (linkData.snapId) { + path = getSnapRoute(linkData.snapId); + } else { + path = linkData.path; + } + history.push(path); + }; + return { + navigate, + }; +}; + +export default useSnapNavigation; diff --git a/ui/hooks/useIsOriginalNativeTokenSymbol.js b/ui/hooks/useIsOriginalNativeTokenSymbol.js index 9a546dba8305..65811c4d656c 100644 --- a/ui/hooks/useIsOriginalNativeTokenSymbol.js +++ b/ui/hooks/useIsOriginalNativeTokenSymbol.js @@ -4,6 +4,7 @@ import fetchWithCache from '../../shared/lib/fetch-with-cache'; import { CHAIN_ID_TO_CURRENCY_SYMBOL_MAP, CHAIN_ID_TO_CURRENCY_SYMBOL_MAP_NETWORK_COLLISION, + CHAIN_SPEC_URL, } from '../../shared/constants/network'; import { DAY } from '../../shared/constants/time'; import { useSafeChainsListValidationSelector } from '../selectors'; @@ -78,7 +79,8 @@ export function useIsOriginalNativeTokenSymbol( } const safeChainsList = await fetchWithCache({ - url: 'https://chainid.network/chains.json', + url: CHAIN_SPEC_URL, + allowStale: true, cacheOptions: { cacheRefreshTime: DAY }, functionName: 'getSafeChainsList', }); diff --git a/ui/hooks/useTokensWithFiltering.test.ts b/ui/hooks/useTokensWithFiltering.test.ts new file mode 100644 index 000000000000..f5ea05e02b8d --- /dev/null +++ b/ui/hooks/useTokensWithFiltering.test.ts @@ -0,0 +1,153 @@ +import { renderHookWithProvider } from '../../test/lib/render-helpers'; +import { createBridgeMockStore } from '../../test/jest/mock-store'; +import { STATIC_MAINNET_TOKEN_LIST } from '../../shared/constants/tokens'; +import { + SWAPS_CHAINID_DEFAULT_TOKEN_MAP, + SwapsTokenObject, + TokenBucketPriority, +} from '../../shared/constants/swaps'; +import { useTokensWithFiltering } from './useTokensWithFiltering'; + +const mockUseTokenTracker = jest + .fn() + .mockReturnValue({ tokensWithBalances: [] }); +jest.mock('./useTokenTracker', () => ({ + useTokenTracker: () => mockUseTokenTracker(), +})); + +const TEST_CHAIN_ID = '0x1'; +const NATIVE_TOKEN = SWAPS_CHAINID_DEFAULT_TOKEN_MAP[TEST_CHAIN_ID]; + +const MOCK_TOP_ASSETS = [ + { address: '0x6b3595068778dd592e39a122f4f5a5cf09c90fe2' }, // UNI + { address: NATIVE_TOKEN.address }, + { address: '0x1f9840a85d5af5bf1d1762f925bdaddc4201f984' }, // USDC + { address: '0xdac17f958d2ee523a2206206994597c13d831ec7' }, // USDT +]; + +const MOCK_TOKEN_LIST_BY_ADDRESS: Record = { + [NATIVE_TOKEN.address]: NATIVE_TOKEN, + ...STATIC_MAINNET_TOKEN_LIST, +}; + +describe('useTokensWithFiltering should return token list generator', () => { + afterEach(() => { + jest.clearAllMocks(); + }); + + it('when chainId === activeChainId and sorted by topAssets', () => { + const mockStore = createBridgeMockStore(); + const { result } = renderHookWithProvider( + () => + useTokensWithFiltering( + MOCK_TOKEN_LIST_BY_ADDRESS, + MOCK_TOP_ASSETS, + TokenBucketPriority.top, + TEST_CHAIN_ID, + ), + mockStore, + ); + + expect(result.current).toHaveLength(1); + expect(typeof result.current).toStrictEqual('function'); + const tokenGenerator = result.current(() => true); + expect(tokenGenerator.next().value).toStrictEqual({ + address: '0x0000000000000000000000000000000000000000', + balance: undefined, + decimals: 18, + iconUrl: './images/eth_logo.svg', + identiconAddress: null, + image: './images/eth_logo.svg', + name: 'Ether', + primaryLabel: 'ETH', + rawFiat: '', + rightPrimaryLabel: undefined, + rightSecondaryLabel: '', + secondaryLabel: 'Ether', + symbol: 'ETH', + type: 'NATIVE', + }); + expect(tokenGenerator.next().value).toStrictEqual({ + address: '0x6b3595068778dd592e39a122f4f5a5cf09c90fe2', + aggregators: [], + balance: undefined, + decimals: 18, + erc20: true, + erc721: false, + iconUrl: + 'https://static.cx.metamask.io/api/v1/tokenIcons/1/0x6b3595068778dd592e39a122f4f5a5cf09c90fe2.png', + identiconAddress: null, + image: 'images/contract/sushi.svg', + name: 'SushiSwap', + primaryLabel: 'SUSHI', + rawFiat: '', + rightPrimaryLabel: undefined, + rightSecondaryLabel: '', + secondaryLabel: 'SushiSwap', + symbol: 'SUSHI', + type: 'TOKEN', + }); + }); + + it('when chainId === activeChainId and sorted by balance', () => { + const mockStore = createBridgeMockStore(); + mockUseTokenTracker.mockReturnValue({ + tokensWithBalances: [ + { + address: '0xdac17f958d2ee523a2206206994597c13d831ec7', + balance: '0xa', + }, + ], + }); + const { result } = renderHookWithProvider( + () => + useTokensWithFiltering( + MOCK_TOKEN_LIST_BY_ADDRESS, + MOCK_TOP_ASSETS, + TokenBucketPriority.owned, + TEST_CHAIN_ID, + ), + mockStore, + ); + + expect(result.current).toHaveLength(1); + expect(typeof result.current).toStrictEqual('function'); + const tokenGenerator = result.current(() => true); + expect(tokenGenerator.next().value).toStrictEqual({ + address: '0x0000000000000000000000000000000000000000', + balance: '0x0', + decimals: 18, + iconUrl: './images/eth_logo.svg', + identiconAddress: null, + image: './images/eth_logo.svg', + name: 'Ether', + primaryLabel: 'ETH', + rawFiat: '0', + rightPrimaryLabel: '0 ETH', + rightSecondaryLabel: '$0.00 USD', + secondaryLabel: 'Ether', + string: '0', + symbol: 'ETH', + type: 'NATIVE', + }); + expect(tokenGenerator.next().value).toStrictEqual({ + address: '0xdac17f958d2ee523a2206206994597c13d831ec7', + aggregators: [], + balance: '0xa', + decimals: 6, + erc20: true, + iconUrl: + 'https://static.cx.metamask.io/api/v1/tokenIcons/1/0xdac17f958d2ee523a2206206994597c13d831ec7.png', + identiconAddress: null, + image: 'images/contract/usdt.svg', + name: 'Tether USD', + primaryLabel: 'USDT', + rawFiat: '', + rightPrimaryLabel: undefined, + rightSecondaryLabel: '', + secondaryLabel: 'Tether USD', + symbol: 'USDT', + type: 'TOKEN', + }); + }); +}); diff --git a/ui/hooks/useTokensWithFiltering.ts b/ui/hooks/useTokensWithFiltering.ts new file mode 100644 index 000000000000..a7ff3f2513ac --- /dev/null +++ b/ui/hooks/useTokensWithFiltering.ts @@ -0,0 +1,178 @@ +import { useCallback, useMemo } from 'react'; +import { useSelector } from 'react-redux'; +import { isEqual } from 'lodash'; +import { ChainId, hexToBN } from '@metamask/controller-utils'; +import { Hex } from '@metamask/utils'; +import { + getAllTokens, + getCurrentCurrency, + getSelectedInternalAccountWithBalance, + getShouldHideZeroBalanceTokens, + getTokenExchangeRates, +} from '../selectors'; +import { getConversionRate } from '../ducks/metamask/metamask'; +import { + SWAPS_CHAINID_DEFAULT_TOKEN_MAP, + SwapsTokenObject, + TokenBucketPriority, +} from '../../shared/constants/swaps'; +import { getValueFromWeiHex } from '../../shared/modules/conversion.utils'; +import { EtherDenomination } from '../../shared/constants/common'; +import { + AssetWithDisplayData, + ERC20Asset, + NativeAsset, + TokenWithBalance, +} from '../components/multichain/asset-picker-amount/asset-picker-modal/types'; +import { AssetType } from '../../shared/constants/transaction'; +import { isSwapsDefaultTokenSymbol } from '../../shared/modules/swaps.utils'; +import { useTokenTracker } from './useTokenTracker'; +import { getRenderableTokenData } from './useTokensToSearch'; + +/* + * Returns a token list generator that filters and sorts tokens based on + * query match, balance/popularity, all other tokens + */ +export const useTokensWithFiltering = ( + tokenList: Record, + topTokens: { address: string }[], + sortOrder: TokenBucketPriority = TokenBucketPriority.owned, + chainId?: ChainId | Hex, +) => { + // Only includes non-native tokens + const allDetectedTokens = useSelector(getAllTokens); + const { address: selectedAddress, balance: balanceOnActiveChain } = + useSelector(getSelectedInternalAccountWithBalance); + + const allDetectedTokensForChainAndAddress = chainId + ? allDetectedTokens?.[chainId]?.[selectedAddress] ?? [] + : []; + + const shouldHideZeroBalanceTokens = useSelector( + getShouldHideZeroBalanceTokens, + ); + const { + tokensWithBalances: erc20TokensWithBalances, + }: { tokensWithBalances: TokenWithBalance[] } = useTokenTracker({ + tokens: allDetectedTokensForChainAndAddress, + address: selectedAddress, + hideZeroBalanceTokens: Boolean(shouldHideZeroBalanceTokens), + }); + + const tokenConversionRates = useSelector(getTokenExchangeRates, isEqual); + const conversionRate = useSelector(getConversionRate); + const currentCurrency = useSelector(getCurrentCurrency); + + const sortedErc20TokensWithBalances = useMemo( + () => + erc20TokensWithBalances.toSorted( + (a, b) => Number(b.string) - Number(a.string), + ), + [erc20TokensWithBalances], + ); + + const filteredTokenListGenerator = useCallback( + (shouldAddToken: (symbol: string, address?: string) => boolean) => { + const buildTokenData = ( + token: SwapsTokenObject, + ): + | AssetWithDisplayData + | AssetWithDisplayData + | undefined => { + if (chainId && shouldAddToken(token.symbol, token.address)) { + return getRenderableTokenData( + { + ...token, + type: isSwapsDefaultTokenSymbol(token.symbol, chainId) + ? AssetType.native + : AssetType.token, + image: token.iconUrl, + }, + tokenConversionRates, + conversionRate, + currentCurrency, + chainId, + tokenList, + ); + } + return undefined; + }; + + return (function* (): Generator< + AssetWithDisplayData | AssetWithDisplayData + > { + const balance = hexToBN(balanceOnActiveChain); + const srcBalanceFields = + sortOrder === TokenBucketPriority.owned + ? { + balance: balanceOnActiveChain, + string: getValueFromWeiHex({ + value: balance, + numberOfDecimals: 4, + toDenomination: EtherDenomination.ETH, + }), + } + : {}; + const nativeToken = buildTokenData({ + ...SWAPS_CHAINID_DEFAULT_TOKEN_MAP[ + chainId as keyof typeof SWAPS_CHAINID_DEFAULT_TOKEN_MAP + ], + ...srcBalanceFields, + }); + if (nativeToken) { + yield nativeToken; + } + + if (sortOrder === TokenBucketPriority.owned) { + for (const tokenWithBalance of sortedErc20TokensWithBalances) { + const cachedTokenData = + tokenWithBalance.address && + tokenList && + (tokenList[tokenWithBalance.address] ?? + tokenList[tokenWithBalance.address.toLowerCase()]); + if (cachedTokenData) { + const combinedTokenData = buildTokenData({ + ...tokenWithBalance, + ...(cachedTokenData ?? {}), + }); + if (combinedTokenData) { + yield combinedTokenData; + } + } + } + } + + for (const topToken of topTokens) { + const tokenListItem = + tokenList?.[topToken.address] ?? + tokenList?.[topToken.address.toLowerCase()]; + if (tokenListItem) { + const tokenWithTokenListData = buildTokenData(tokenListItem); + if (tokenWithTokenListData) { + yield tokenWithTokenListData; + } + } + } + + for (const token of Object.values(tokenList)) { + const tokenWithTokenListData = buildTokenData(token); + if (tokenWithTokenListData) { + yield tokenWithTokenListData; + } + } + })(); + }, + [ + balanceOnActiveChain, + sortedErc20TokensWithBalances, + topTokens, + tokenConversionRates, + conversionRate, + currentCurrency, + chainId, + tokenList, + ], + ); + + return filteredTokenListGenerator; +}; diff --git a/ui/pages/bridge/__snapshots__/index.test.tsx.snap b/ui/pages/bridge/__snapshots__/index.test.tsx.snap index 14514e59987d..cebca14e93bb 100644 --- a/ui/pages/bridge/__snapshots__/index.test.tsx.snap +++ b/ui/pages/bridge/__snapshots__/index.test.tsx.snap @@ -9,27 +9,61 @@ exports[`Bridge renders the component with initial props 1`] = ` class="bridge__container" >
- +
- Bridge +

+ Bridge +

+
+
+
+
diff --git a/ui/pages/bridge/index.scss b/ui/pages/bridge/index.scss index 99b8712cc63e..98a3a3ee5c34 100644 --- a/ui/pages/bridge/index.scss +++ b/ui/pages/bridge/index.scss @@ -1 +1,29 @@ @use "design-system"; + +@import 'prepare/index'; + +.bridge { + max-height: 100vh; + width: 360px; + position: relative; + + &__container { + width: 100%; + + .multichain-page-footer { + position: absolute; + width: 100%; + height: 80px; + bottom: 0; + padding: 16px; + display: flex; + + button { + flex: 1; + height: 100%; + font-size: 14px; + font-weight: 500; + } + } + } +} diff --git a/ui/pages/bridge/index.tsx b/ui/pages/bridge/index.tsx index e4b5c0b930d4..e81b20670011 100644 --- a/ui/pages/bridge/index.tsx +++ b/ui/pages/bridge/index.tsx @@ -1,9 +1,7 @@ -import React, { useContext } from 'react'; +import React, { useContext, useEffect } from 'react'; import { useDispatch, useSelector } from 'react-redux'; import { Switch, useHistory } from 'react-router-dom'; -import classnames from 'classnames'; import { I18nContext } from '../../contexts/i18n'; - import { clearSwapsState } from '../../ducks/swaps/swaps'; import { DEFAULT_ROUTE, @@ -11,25 +9,24 @@ import { PREPARE_SWAP_ROUTE, CROSS_CHAIN_SWAP_ROUTE, } from '../../helpers/constants/routes'; - import { resetBackgroundSwapsState } from '../../store/actions'; - import FeatureToggledRoute from '../../helpers/higher-order-components/feature-toggled-route'; import { - Box, - Icon, + ButtonIcon, + ButtonIconSize, IconName, - IconSize, } from '../../components/component-library'; -import { - JustifyContent, - IconColor, - Display, - BlockSize, -} from '../../helpers/constants/design-system'; -import { getIsBridgeEnabled } from '../../selectors'; +import { getIsBridgeChain, getIsBridgeEnabled } from '../../selectors'; import useBridging from '../../hooks/bridge/useBridging'; -import { PrepareBridgePage } from './prepare/prepare-bridge-page'; +import { + Content, + Footer, + Header, +} from '../../components/multichain/pages/page'; +import { getProviderConfig } from '../../ducks/metamask/metamask'; +import { resetInputFields, setFromChain } from '../../ducks/bridge/actions'; +import PrepareBridgePage from './prepare/prepare-bridge-page'; +import { BridgeCTAButton } from './prepare/bridge-cta-button'; const CrossChainSwap = () => { const t = useContext(I18nContext); @@ -40,6 +37,19 @@ const CrossChainSwap = () => { const dispatch = useDispatch(); const isBridgeEnabled = useSelector(getIsBridgeEnabled); + const providerConfig = useSelector(getProviderConfig); + const isBridgeChain = useSelector(getIsBridgeChain); + + useEffect(() => { + isBridgeChain && + isBridgeEnabled && + providerConfig && + dispatch(setFromChain(providerConfig.chainId)); + + return () => { + dispatch(resetInputFields()); + }; + }, [isBridgeChain, isBridgeEnabled, providerConfig]); const redirectToDefaultRoute = async () => { history.push({ @@ -53,37 +63,27 @@ const CrossChainSwap = () => { return (
-
- ) => { - if (e.key === 'Enter') { - redirectToDefaultRoute(); - } - }} - > - - - -
{t('bridge')}
-
-
+ } > + {t('bridge')} + + { }} /> -
+ +
+ +
); diff --git a/ui/pages/bridge/prepare/__snapshots__/bridge-cta-button.test.tsx.snap b/ui/pages/bridge/prepare/__snapshots__/bridge-cta-button.test.tsx.snap new file mode 100644 index 000000000000..f225adec3b6d --- /dev/null +++ b/ui/pages/bridge/prepare/__snapshots__/bridge-cta-button.test.tsx.snap @@ -0,0 +1,14 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`BridgeCTAButton should render the component's initial state 1`] = ` +
+ +
+`; diff --git a/ui/pages/bridge/prepare/__snapshots__/prepare-bridge-page.test.tsx.snap b/ui/pages/bridge/prepare/__snapshots__/prepare-bridge-page.test.tsx.snap new file mode 100644 index 000000000000..b406cafe0941 --- /dev/null +++ b/ui/pages/bridge/prepare/__snapshots__/prepare-bridge-page.test.tsx.snap @@ -0,0 +1,456 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`PrepareBridgePage should render the component, with initial state 1`] = ` +
+
+
+
+
+ +
+
+ +
+
+
+
+

+ +

+
+ + $0.00 + +
+
+
+
+ +
+
+
+ +
+
+
+ +
+
+
+
+
+

+ +

+
+ + $0.00 + +
+
+
+
+
+
+`; + +exports[`PrepareBridgePage should render the component, with inputs set 1`] = ` +
+
+
+
+
+ +
+
+
+ +
+
+
+
+
+

+ +

+
+ + $0.00 + +
+
+
+
+ +
+
+
+ +
+
+
+ +
+
+
+
+
+

+ +

+
+ + $0.00 + +
+
+
+
+
+
+`; diff --git a/ui/pages/bridge/prepare/bridge-cta-button.test.tsx b/ui/pages/bridge/prepare/bridge-cta-button.test.tsx new file mode 100644 index 000000000000..5e42823c885b --- /dev/null +++ b/ui/pages/bridge/prepare/bridge-cta-button.test.tsx @@ -0,0 +1,50 @@ +import React from 'react'; +import { renderWithProvider } from '../../../../test/jest'; +import configureStore from '../../../store/store'; +import { createBridgeMockStore } from '../../../../test/jest/mock-store'; +import { CHAIN_IDS } from '../../../../shared/constants/network'; +import { BridgeCTAButton } from './bridge-cta-button'; + +describe('BridgeCTAButton', () => { + it("should render the component's initial state", () => { + const mockStore = createBridgeMockStore( + { + srcNetworkAllowlist: [CHAIN_IDS.MAINNET, CHAIN_IDS.OPTIMISM], + destNetworkAllowlist: [CHAIN_IDS.OPTIMISM], + }, + { fromTokenInputValue: 1 }, + ); + const { container, getByText, getByRole } = renderWithProvider( + , + configureStore(mockStore), + ); + + expect(container).toMatchSnapshot(); + + expect(getByText('Select token')).toBeInTheDocument(); + expect(getByRole('button')).toBeDisabled(); + }); + + it('should render the component when tx is submittable', () => { + const mockStore = createBridgeMockStore( + { + srcNetworkAllowlist: [CHAIN_IDS.MAINNET, CHAIN_IDS.OPTIMISM], + destNetworkAllowlist: [CHAIN_IDS.LINEA_MAINNET], + }, + { + fromTokenInputValue: 1, + fromToken: 'ETH', + toToken: 'ETH', + toChainId: CHAIN_IDS.LINEA_MAINNET, + }, + {}, + ); + const { getByText, getByRole } = renderWithProvider( + , + configureStore(mockStore), + ); + + expect(getByText('Bridge')).toBeInTheDocument(); + expect(getByRole('button')).not.toBeDisabled(); + }); +}); diff --git a/ui/pages/bridge/prepare/bridge-cta-button.tsx b/ui/pages/bridge/prepare/bridge-cta-button.tsx new file mode 100644 index 000000000000..fedcf4d4606a --- /dev/null +++ b/ui/pages/bridge/prepare/bridge-cta-button.tsx @@ -0,0 +1,49 @@ +import React, { useMemo } from 'react'; +import { useSelector } from 'react-redux'; +import { Button } from '../../../components/component-library'; +import { + getFromAmount, + getFromChain, + getFromToken, + getToAmount, + getToChain, + getToToken, +} from '../../../ducks/bridge/selectors'; +import { useI18nContext } from '../../../hooks/useI18nContext'; + +export const BridgeCTAButton = () => { + const t = useI18nContext(); + const fromToken = useSelector(getFromToken); + const toToken = useSelector(getToToken); + + const fromChain = useSelector(getFromChain); + const toChain = useSelector(getToChain); + + const fromAmount = useSelector(getFromAmount); + const toAmount = useSelector(getToAmount); + + const isTxSubmittable = + fromToken && toToken && fromChain && toChain && fromAmount && toAmount; + + const label = useMemo(() => { + if (isTxSubmittable) { + return t('bridge'); + } + + return t('swapSelectToken'); + }, [isTxSubmittable]); + + return ( + + ); +}; diff --git a/ui/pages/bridge/prepare/bridge-input-group.tsx b/ui/pages/bridge/prepare/bridge-input-group.tsx new file mode 100644 index 000000000000..811310590c71 --- /dev/null +++ b/ui/pages/bridge/prepare/bridge-input-group.tsx @@ -0,0 +1,157 @@ +import React from 'react'; +import { Hex } from '@metamask/utils'; +import { SwapsTokenObject } from '../../../../shared/constants/swaps'; +import { + Box, + Text, + TextField, + TextFieldType, +} from '../../../components/component-library'; +import { AssetPicker } from '../../../components/multichain/asset-picker-amount/asset-picker'; +import { TabName } from '../../../components/multichain/asset-picker-amount/asset-picker-modal/asset-picker-modal-tabs'; +import CurrencyDisplay from '../../../components/ui/currency-display'; +import { useI18nContext } from '../../../hooks/useI18nContext'; +import { useTokenFiatAmount } from '../../../hooks/useTokenFiatAmount'; +import { useEthFiatAmount } from '../../../hooks/useEthFiatAmount'; +import { isSwapsDefaultTokenSymbol } from '../../../../shared/modules/swaps.utils'; +import Tooltip from '../../../components/ui/tooltip'; +import { SwapsEthToken } from '../../../selectors'; +import { + ERC20Asset, + NativeAsset, +} from '../../../components/multichain/asset-picker-amount/asset-picker-modal/types'; +import { zeroAddress } from '../../../__mocks__/ethereumjs-util'; +import { AssetType } from '../../../../shared/constants/transaction'; +import { + CHAIN_ID_TO_CURRENCY_SYMBOL_MAP, + CHAIN_ID_TOKEN_IMAGE_MAP, +} from '../../../../shared/constants/network'; +import useLatestBalance from '../../../hooks/bridge/useLatestBalance'; + +const generateAssetFromToken = ( + chainId: Hex, + tokenDetails: SwapsTokenObject | SwapsEthToken, +): ERC20Asset | NativeAsset => { + if ('iconUrl' in tokenDetails && tokenDetails.address !== zeroAddress()) { + return { + type: AssetType.token, + image: tokenDetails.iconUrl, + symbol: tokenDetails.symbol, + address: tokenDetails.address, + }; + } + + return { + type: AssetType.native, + image: + CHAIN_ID_TOKEN_IMAGE_MAP[ + chainId as keyof typeof CHAIN_ID_TOKEN_IMAGE_MAP + ], + symbol: + CHAIN_ID_TO_CURRENCY_SYMBOL_MAP[ + chainId as keyof typeof CHAIN_ID_TO_CURRENCY_SYMBOL_MAP + ], + }; +}; + +export const BridgeInputGroup = ({ + className, + header, + token, + onAssetChange, + onAmountChange, + networkProps, + customTokenListGenerator, + amountFieldProps = {}, +}: { + className: string; + onAmountChange?: (value: string) => void; + token: SwapsTokenObject | SwapsEthToken | null; + amountFieldProps?: Pick< + React.ComponentProps, + 'testId' | 'autoFocus' | 'value' | 'readOnly' | 'disabled' + >; +} & Pick< + React.ComponentProps, + 'networkProps' | 'header' | 'customTokenListGenerator' | 'onAssetChange' +>) => { + const t = useI18nContext(); + + const tokenFiatValue = useTokenFiatAmount( + token?.address || undefined, + amountFieldProps?.value?.toString() || '0x0', + token?.symbol, + { + showFiat: true, + }, + true, + ); + const ethFiatValue = useEthFiatAmount( + amountFieldProps?.value?.toString() || '0x0', + { showFiat: true }, + true, + ); + + const { formattedBalance } = useLatestBalance( + token, + networkProps?.network?.chainId, + ); + + return ( + + + + + { + onAmountChange?.(e.target.value); + }} + {...amountFieldProps} + /> + + + + + {formattedBalance ? `${t('balance')}: ${formattedBalance}` : ' '} + + + + + ); +}; diff --git a/ui/pages/bridge/prepare/index.scss b/ui/pages/bridge/prepare/index.scss new file mode 100644 index 000000000000..1fa416df727f --- /dev/null +++ b/ui/pages/bridge/prepare/index.scss @@ -0,0 +1,170 @@ +@use "design-system"; + +.tokens-main-view-modal { + .multichain-asset-picker-list-item .mm-badge-wrapper__badge-container { + display: none; + } +} + +.prepare-bridge-page { + display: flex; + flex-flow: column; + flex: 1; + width: 100%; + + &__content { + display: flex; + flex-direction: column; + padding: 16px 0 16px 0; + border-radius: 8px; + border: 1px solid var(--color-border-muted); + } + + &__from, + &__to { + display: flex; + flex-direction: column; + gap: 4px; + justify-content: center; + padding: 8px 16px 8px 16px; + } + + &__input-row { + display: flex; + flex-direction: row; + justify-content: space-between; + width: 298px; + gap: 16px; + + input[type="number"]::-webkit-inner-spin-button { + -webkit-appearance: none; + -moz-appearance: none; + display: none; + } + + input[type="number"]:hover::-webkit-inner-spin-button { + -webkit-appearance: none; + -moz-appearance: none; + display: none; + } + + .mm-text-field { + background-color: inherit; + + &--focused { + outline: none; + } + } + } + + &__amounts-row { + display: flex; + flex-direction: row; + justify-content: space-between; + width: 298px; + height: 22px; + + p, + span { + color: var(--color-text-alternative); + font-size: 12px; + } + } + + .asset-picker { + border: 1px solid var(--color-border-muted); + height: 40px; + min-width: fit-content; + max-width: fit-content; + background-color: inherit; + + p { + font-size: 14px; + font-weight: 500; + } + + .mm-avatar-token { + height: 24px; + width: 24px; + border: 1px solid var(--color-border-muted); + } + + .mm-badge-wrapper__badge-container .mm-avatar-base { + height: 10px; + width: 10px; + border: none; + } + } + + .amount-input { + border: none; + + input { + text-align: right; + padding-right: 0; + font-size: 24px; + font-weight: 700; + + &:focus, + &:focus-visible { + outline: none; + } + } + + .mm-text-field--focused { + outline: none; + } + } + + &__switch-tokens { + display: flex; + justify-content: center; + align-items: center; + + &::before, + &::after { + content: ''; + border-top: 1px solid var(--color-border-muted); + flex-grow: 1; + } + + button { + border-radius: 50%; + padding: 10px; + border: 1px solid var(--color-border-muted); + transition: all 0.3s ease-in-out; + cursor: pointer; + width: 40px; + height: 40px; + + &:hover:enabled { + background: var(--color-background-default-hover); + + .mm-icon { + color: var(--color-icon-default); + } + } + + &:active { + background: var(--color-background-default-pressed); + + .mm-icon { + color: var(--color-icon-default); + } + } + + .rotate { + transform: rotate(360deg); + } + } + + .mm-icon { + color: var(--color-icon-alternative); + transition: all 0.3s ease-in-out; + } + + &:disabled { + cursor: not-allowed; + } + } +} diff --git a/ui/pages/bridge/prepare/prepare-bridge-page.test.tsx b/ui/pages/bridge/prepare/prepare-bridge-page.test.tsx new file mode 100644 index 000000000000..82441aad218d --- /dev/null +++ b/ui/pages/bridge/prepare/prepare-bridge-page.test.tsx @@ -0,0 +1,118 @@ +import React from 'react'; +import { act } from '@testing-library/react'; +import { fireEvent, renderWithProvider } from '../../../../test/jest'; +import configureStore from '../../../store/store'; +import { createBridgeMockStore } from '../../../../test/jest/mock-store'; +import { CHAIN_IDS } from '../../../../shared/constants/network'; +import { createTestProviderTools } from '../../../../test/stub/provider'; +import PrepareBridgePage from './prepare-bridge-page'; + +describe('PrepareBridgePage', () => { + beforeAll(() => { + const { provider } = createTestProviderTools({ + networkId: 'Ethereum', + chainId: CHAIN_IDS.MAINNET, + }); + + // eslint-disable-next-line @typescript-eslint/no-explicit-any + global.ethereumProvider = provider as any; + }); + + it('should render the component, with initial state', async () => { + const mockStore = createBridgeMockStore( + { + srcNetworkAllowlist: [CHAIN_IDS.MAINNET, CHAIN_IDS.OPTIMISM], + destNetworkAllowlist: [CHAIN_IDS.OPTIMISM], + }, + {}, + ); + const { container, getByRole, getByTestId } = renderWithProvider( + , + configureStore(mockStore), + ); + + expect(container).toMatchSnapshot(); + + expect(getByRole('button', { name: /ETH/u })).toBeInTheDocument(); + expect(getByRole('button', { name: /Select token/u })).toBeInTheDocument(); + + expect(getByTestId('from-amount')).toBeInTheDocument(); + expect(getByTestId('from-amount').closest('input')).not.toBeDisabled(); + await act(() => { + fireEvent.change(getByTestId('from-amount'), { target: { value: '2' } }); + }); + expect(getByTestId('from-amount').closest('input')).toHaveValue(2); + + expect(getByTestId('to-amount')).toBeInTheDocument(); + expect(getByTestId('to-amount').closest('input')).toBeDisabled(); + + expect(getByTestId('switch-tokens').closest('button')).toBeDisabled(); + }); + + it('should render the component, with inputs set', async () => { + const mockStore = createBridgeMockStore( + { + srcNetworkAllowlist: [CHAIN_IDS.MAINNET, CHAIN_IDS.LINEA_MAINNET], + destNetworkAllowlist: [CHAIN_IDS.LINEA_MAINNET], + }, + { + fromTokenInputValue: 1, + fromToken: { address: '0x3103910', decimals: 6 }, + toToken: { + iconUrl: 'http://url', + symbol: 'UNI', + address: '0x1f9840a85d5af5bf1d1762f925bdaddc4201f984', + decimals: 6, + }, + toChainId: CHAIN_IDS.LINEA_MAINNET, + }, + {}, + ); + const { container, getByRole, getByTestId } = renderWithProvider( + , + configureStore(mockStore), + ); + + expect(container).toMatchSnapshot(); + + expect(getByRole('button', { name: /ETH/u })).toBeInTheDocument(); + expect(getByRole('button', { name: /UNI/u })).toBeInTheDocument(); + + expect(getByTestId('from-amount')).toBeInTheDocument(); + expect(getByTestId('from-amount').closest('input')).not.toBeDisabled(); + expect(getByTestId('from-amount').closest('input')).toHaveValue(1); + await act(() => { + fireEvent.change(getByTestId('from-amount'), { target: { value: '2' } }); + }); + expect(getByTestId('from-amount').closest('input')).toHaveValue(2); + + expect(getByTestId('to-amount')).toBeInTheDocument(); + expect(getByTestId('to-amount').closest('input')).toBeDisabled(); + + expect(getByTestId('switch-tokens').closest('button')).not.toBeDisabled(); + }); + + it('should throw an error if token decimals are not defined', async () => { + const mockStore = createBridgeMockStore( + { + srcNetworkAllowlist: [CHAIN_IDS.MAINNET, CHAIN_IDS.LINEA_MAINNET], + destNetworkAllowlist: [CHAIN_IDS.LINEA_MAINNET], + }, + { + fromTokenInputValue: 1, + fromToken: { address: '0x3103910' }, + toToken: { + iconUrl: 'http://url', + symbol: 'UNI', + address: '0x1f9840a85d5af5bf1d1762f925bdaddc4201f984', + }, + toChainId: CHAIN_IDS.LINEA_MAINNET, + }, + {}, + ); + + expect(() => + renderWithProvider(, configureStore(mockStore)), + ).toThrow(); + }); +}); diff --git a/ui/pages/bridge/prepare/prepare-bridge-page.tsx b/ui/pages/bridge/prepare/prepare-bridge-page.tsx index 6c66499ac9b9..2fdb11289c5b 100644 --- a/ui/pages/bridge/prepare/prepare-bridge-page.tsx +++ b/ui/pages/bridge/prepare/prepare-bridge-page.tsx @@ -1,24 +1,163 @@ -import React from 'react'; -import { useSelector, shallowEqual } from 'react-redux'; -import { isEqual, shuffle } from 'lodash'; -import PrepareSwapPage from '../../swaps/prepare-swap-page/prepare-swap-page'; -import { getSelectedAccount, getTokenList } from '../../../selectors'; +import React, { useState } from 'react'; +import { useSelector, useDispatch } from 'react-redux'; +import classnames from 'classnames'; +import { + setFromChain, + setFromToken, + setFromTokenInputValue, + setToChain, + setToToken, + switchToAndFromTokens, +} from '../../../ducks/bridge/actions'; +import { + getFromAmount, + getFromChain, + getFromChains, + getFromToken, + getFromTokens, + getFromTopAssets, + getToAmount, + getToChain, + getToChains, + getToToken, + getToTokens, + getToTopAssets, +} from '../../../ducks/bridge/selectors'; +import { + Box, + ButtonIcon, + IconName, +} from '../../../components/component-library'; +import { useI18nContext } from '../../../hooks/useI18nContext'; +import { TokenBucketPriority } from '../../../../shared/constants/swaps'; +import { useTokensWithFiltering } from '../../../hooks/useTokensWithFiltering'; +import { setActiveNetwork } from '../../../store/actions'; +import { BlockSize } from '../../../helpers/constants/design-system'; +import { BridgeInputGroup } from './bridge-input-group'; -export const PrepareBridgePage = () => { - const selectedAccount = useSelector(getSelectedAccount, shallowEqual); - const { balance: ethBalance, address: selectedAccountAddress } = - selectedAccount; +const PrepareBridgePage = () => { + const dispatch = useDispatch(); - const tokenList = useSelector(getTokenList, isEqual); - const shuffledTokensList = shuffle(Object.values(tokenList)); + const t = useI18nContext(); + + const fromToken = useSelector(getFromToken); + const fromTokens = useSelector(getFromTokens); + const fromTopAssets = useSelector(getFromTopAssets); + + const toToken = useSelector(getToToken); + const toTokens = useSelector(getToTokens); + const toTopAssets = useSelector(getToTopAssets); + + const fromChains = useSelector(getFromChains); + const toChains = useSelector(getToChains); + const fromChain = useSelector(getFromChain); + const toChain = useSelector(getToChain); + + const fromAmount = useSelector(getFromAmount); + const toAmount = useSelector(getToAmount); + + const fromTokenListGenerator = useTokensWithFiltering( + fromTokens, + fromTopAssets, + TokenBucketPriority.owned, + fromChain?.chainId, + ); + const toTokenListGenerator = useTokensWithFiltering( + toTokens, + toTopAssets, + TokenBucketPriority.top, + toChain?.chainId, + ); + + const [rotateSwitchTokens, setRotateSwitchTokens] = useState(false); return ( -
- +
+ + { + dispatch(setFromTokenInputValue(e)); + }} + onAssetChange={(token) => dispatch(setFromToken(token))} + networkProps={{ + network: fromChain, + networks: fromChains, + onNetworkChange: (networkConfig) => { + dispatch( + setActiveNetwork( + networkConfig.rpcEndpoints[ + networkConfig.defaultRpcEndpointIndex + ].networkClientId, + ), + ); + dispatch(setFromChain(networkConfig.chainId)); + }, + }} + customTokenListGenerator={ + fromTokens && fromTopAssets ? fromTokenListGenerator : undefined + } + amountFieldProps={{ + testId: 'from-amount', + autoFocus: true, + value: fromAmount || undefined, + }} + /> + + + { + setRotateSwitchTokens(!rotateSwitchTokens); + const toChainClientId = + toChain?.defaultRpcEndpointIndex && toChain?.rpcEndpoints + ? toChain.rpcEndpoints?.[toChain.defaultRpcEndpointIndex] + .networkClientId + : undefined; + toChainClientId && dispatch(setActiveNetwork(toChainClientId)); + dispatch(switchToAndFromTokens({ fromChain })); + }} + /> + + + dispatch(setToToken(token))} + networkProps={{ + network: toChain, + networks: toChains, + onNetworkChange: (networkConfig) => { + dispatch(setToChain(networkConfig.chainId)); + }, + }} + customTokenListGenerator={ + toChain && toTokens && toTopAssets + ? toTokenListGenerator + : fromTokenListGenerator + } + amountFieldProps={{ + testId: 'to-amount', + readOnly: true, + disabled: true, + value: toAmount, + }} + /> +
); }; + +export default PrepareBridgePage; diff --git a/ui/pages/confirm-add-suggested-nft/confirm-add-suggested-nft.js b/ui/pages/confirm-add-suggested-nft/confirm-add-suggested-nft.js index 822db143d29a..dda856d64abd 100644 --- a/ui/pages/confirm-add-suggested-nft/confirm-add-suggested-nft.js +++ b/ui/pages/confirm-add-suggested-nft/confirm-add-suggested-nft.js @@ -1,7 +1,7 @@ import React, { useCallback, useContext, useEffect, useState } from 'react'; import { useDispatch, useSelector } from 'react-redux'; import { useHistory } from 'react-router-dom'; -import { ethErrors, serializeError } from 'eth-rpc-errors'; +import { providerErrors, serializeError } from '@metamask/rpc-errors'; import { getTokenTrackerLink } from '@metamask/etherscan-link'; import classnames from 'classnames'; import { PageContainerFooter } from '../../components/ui/page-container'; @@ -125,7 +125,7 @@ const ConfirmAddSuggestedNFT = () => { return dispatch( rejectPendingApproval( id, - serializeError(ethErrors.provider.userRejectedRequest()), + serializeError(providerErrors.userRejectedRequest()), ), ); }), @@ -421,7 +421,7 @@ const ConfirmAddSuggestedNFT = () => { rejectPendingApproval( id, serializeError( - ethErrors.provider.userRejectedRequest(), + providerErrors.userRejectedRequest(), ), ), ); diff --git a/ui/pages/confirm-add-suggested-token/confirm-add-suggested-token.js b/ui/pages/confirm-add-suggested-token/confirm-add-suggested-token.js index c3e1a3f73bf0..f099ea80bfd5 100644 --- a/ui/pages/confirm-add-suggested-token/confirm-add-suggested-token.js +++ b/ui/pages/confirm-add-suggested-token/confirm-add-suggested-token.js @@ -1,7 +1,7 @@ import React, { useCallback, useContext, useEffect, useMemo } from 'react'; import { useDispatch, useSelector } from 'react-redux'; import { useHistory } from 'react-router-dom'; -import { ethErrors, serializeError } from 'eth-rpc-errors'; +import { providerErrors, serializeError } from '@metamask/rpc-errors'; import { BannerAlert, Button, @@ -147,7 +147,7 @@ const ConfirmAddSuggestedToken = () => { dispatch( rejectPendingApproval( id, - serializeError(ethErrors.provider.userRejectedRequest()), + serializeError(providerErrors.userRejectedRequest()), ), ), ), diff --git a/ui/pages/confirmations/components/confirm/footer/footer.tsx b/ui/pages/confirmations/components/confirm/footer/footer.tsx index cc9b39609030..a37812899ec9 100644 --- a/ui/pages/confirmations/components/confirm/footer/footer.tsx +++ b/ui/pages/confirmations/components/confirm/footer/footer.tsx @@ -1,5 +1,5 @@ import { TransactionMeta } from '@metamask/transaction-controller'; -import { ethErrors, serializeError } from 'eth-rpc-errors'; +import { providerErrors, serializeError } from '@metamask/rpc-errors'; import React, { useCallback, useState } from 'react'; import { useDispatch, useSelector } from 'react-redux'; import { ConfirmAlertModal } from '../../../../../components/app/alert-system/confirm-alert-modal'; @@ -201,7 +201,7 @@ const Footer = () => { return; } - const error = ethErrors.provider.userRejectedRequest(); + const error = providerErrors.userRejectedRequest(); error.data = { location }; dispatch( diff --git a/ui/pages/confirmations/components/confirm/nav/nav.tsx b/ui/pages/confirmations/components/confirm/nav/nav.tsx index 6546b882b784..de0637a9f641 100644 --- a/ui/pages/confirmations/components/confirm/nav/nav.tsx +++ b/ui/pages/confirmations/components/confirm/nav/nav.tsx @@ -1,4 +1,4 @@ -import { ethErrors, serializeError } from 'eth-rpc-errors'; +import { providerErrors, serializeError } from '@metamask/rpc-errors'; import React, { useCallback, useMemo } from 'react'; import { useDispatch, useSelector } from 'react-redux'; import { useHistory } from 'react-router-dom'; @@ -78,7 +78,7 @@ const Nav = () => { dispatch( rejectPendingApproval( conf.id, - serializeError(ethErrors.provider.userRejectedRequest()), + serializeError(providerErrors.userRejectedRequest()), ), ); }); diff --git a/ui/pages/confirmations/components/confirm/title/title.test.tsx b/ui/pages/confirmations/components/confirm/title/title.test.tsx index 3c03343c2afb..3d4d6672940d 100644 --- a/ui/pages/confirmations/components/confirm/title/title.test.tsx +++ b/ui/pages/confirmations/components/confirm/title/title.test.tsx @@ -11,7 +11,10 @@ import { getMockTypedSignConfirmStateForRequest, } from '../../../../../../test/data/confirmations/helper'; import { unapprovedPersonalSignMsg } from '../../../../../../test/data/confirmations/personal_sign'; -import { permitSignatureMsg } from '../../../../../../test/data/confirmations/typed_sign'; +import { + permitNFTSignatureMsg, + permitSignatureMsg, +} from '../../../../../../test/data/confirmations/typed_sign'; import { renderWithConfirmContextProvider } from '../../../../../../test/lib/confirmations/render-helpers'; import { tEn } from '../../../../../../test/lib/i18n-helpers'; import { @@ -71,6 +74,21 @@ describe('ConfirmTitle', () => { ).toBeInTheDocument(); }); + it('should render the title and description for a NFT permit signature', () => { + const mockStore = configureMockStore([])( + getMockTypedSignConfirmStateForRequest(permitNFTSignatureMsg), + ); + const { getByText } = renderWithConfirmContextProvider( + , + mockStore, + ); + + expect(getByText('Withdrawal request')).toBeInTheDocument(); + expect( + getByText('This site wants permission to withdraw your NFTs'), + ).toBeInTheDocument(); + }); + it('should render the title and description for typed signature', () => { const mockStore = configureMockStore([])(getMockTypedSignConfirmState()); const { getByText } = renderWithConfirmContextProvider( diff --git a/ui/pages/confirmations/components/confirm/title/title.tsx b/ui/pages/confirmations/components/confirm/title/title.tsx index 2645feed8a41..969e9c05518d 100644 --- a/ui/pages/confirmations/components/confirm/title/title.tsx +++ b/ui/pages/confirmations/components/confirm/title/title.tsx @@ -3,6 +3,8 @@ import { TransactionType, } from '@metamask/transaction-controller'; import React, { memo, useMemo } from 'react'; + +import { TokenStandard } from '../../../../../../shared/constants/transaction'; import GeneralAlert from '../../../../../components/app/alert-system/general-alert/general-alert'; import { Box, Text } from '../../../../../components/component-library'; import { @@ -12,12 +14,11 @@ import { } from '../../../../../helpers/constants/design-system'; import useAlerts from '../../../../../hooks/useAlerts'; import { useI18nContext } from '../../../../../hooks/useI18nContext'; +import { TypedSignSignaturePrimaryTypes } from '../../../constants'; import { useConfirmContext } from '../../../context/confirm'; import { Confirmation, SignatureRequestType } from '../../../types/confirm'; -import { - isPermitSignatureRequest, - isSIWESignatureRequest, -} from '../../../utils'; +import { isSIWESignatureRequest } from '../../../utils'; +import { useTypedSignSignatureInfo } from '../../../hooks/useTypedSignSignatureInfo'; import { useIsNFT } from '../info/approve/hooks/use-is-nft'; import { useDecodedTransactionData } from '../info/hooks/useDecodedTransactionData'; import { getIsRevokeSetApprovalForAll } from '../info/utils'; @@ -51,6 +52,8 @@ function ConfirmBannerAlert({ ownerId }: { ownerId: string }) { type IntlFunction = (str: string) => string; +// todo: getTitle and getDescription can be merged to remove code duplication. + const getTitle = ( t: IntlFunction, confirmation?: Confirmation, @@ -58,6 +61,8 @@ const getTitle = ( customSpendingCap?: string, isRevokeSetApprovalForAll?: boolean, pending?: boolean, + primaryType?: keyof typeof TypedSignSignaturePrimaryTypes, + tokenStandard?: string, ) => { if (pending) { return ''; @@ -74,9 +79,13 @@ const getTitle = ( } return t('confirmTitleSignature'); case TransactionType.signTypedData: - return isPermitSignatureRequest(confirmation as SignatureRequestType) - ? t('confirmTitlePermitTokens') - : t('confirmTitleSignature'); + if (primaryType === TypedSignSignaturePrimaryTypes.PERMIT) { + if (tokenStandard === TokenStandard.ERC721) { + return t('setApprovalForAllRedesignedTitle'); + } + return t('confirmTitlePermitTokens'); + } + return t('confirmTitleSignature'); case TransactionType.tokenMethodApprove: if (isNFT) { return t('confirmTitleApproveTransaction'); @@ -104,6 +113,8 @@ const getDescription = ( customSpendingCap?: string, isRevokeSetApprovalForAll?: boolean, pending?: boolean, + primaryType?: keyof typeof TypedSignSignaturePrimaryTypes, + tokenStandard?: string, ) => { if (pending) { return ''; @@ -120,9 +131,13 @@ const getDescription = ( } return t('confirmTitleDescSign'); case TransactionType.signTypedData: - return isPermitSignatureRequest(confirmation as SignatureRequestType) - ? t('confirmTitleDescPermitSignature') - : t('confirmTitleDescSign'); + if (primaryType === TypedSignSignaturePrimaryTypes.PERMIT) { + if (tokenStandard === TokenStandard.ERC721) { + return t('confirmTitleDescApproveTransaction'); + } + return t('confirmTitleDescPermitSignature'); + } + return t('confirmTitleDescSign'); case TransactionType.tokenMethodApprove: if (isNFT) { return t('confirmTitleDescApproveTransaction'); @@ -150,6 +165,10 @@ const ConfirmTitle: React.FC = memo(() => { const { isNFT } = useIsNFT(currentConfirmation as TransactionMeta); + const { primaryType, tokenStandard } = useTypedSignSignatureInfo( + currentConfirmation as SignatureRequestType, + ); + const { customSpendingCap, pending: spendingCapPending } = useCurrentSpendingCap(currentConfirmation); @@ -175,6 +194,8 @@ const ConfirmTitle: React.FC = memo(() => { customSpendingCap, isRevokeSetApprovalForAll, spendingCapPending || revokePending, + primaryType, + tokenStandard, ), [ currentConfirmation, @@ -183,6 +204,8 @@ const ConfirmTitle: React.FC = memo(() => { isRevokeSetApprovalForAll, spendingCapPending, revokePending, + primaryType, + tokenStandard, ], ); @@ -195,6 +218,8 @@ const ConfirmTitle: React.FC = memo(() => { customSpendingCap, isRevokeSetApprovalForAll, spendingCapPending || revokePending, + primaryType, + tokenStandard, ), [ currentConfirmation, @@ -203,6 +228,8 @@ const ConfirmTitle: React.FC = memo(() => { isRevokeSetApprovalForAll, spendingCapPending, revokePending, + primaryType, + tokenStandard, ], ); diff --git a/ui/pages/confirmations/components/signature-request-original/signature-request-original.component.js b/ui/pages/confirmations/components/signature-request-original/signature-request-original.component.js index f9c9dbe9c0a1..026135a52685 100644 --- a/ui/pages/confirmations/components/signature-request-original/signature-request-original.component.js +++ b/ui/pages/confirmations/components/signature-request-original/signature-request-original.component.js @@ -2,7 +2,7 @@ import React, { Component } from 'react'; import PropTypes from 'prop-types'; import classnames from 'classnames'; import { ObjectInspector } from 'react-inspector'; -import { ethErrors, serializeError } from 'eth-rpc-errors'; +import { providerErrors, serializeError } from '@metamask/rpc-errors'; import { SubjectType } from '@metamask/permission-controller'; import LedgerInstructionField from '../ledger-instruction-field'; import { MESSAGE_TYPE } from '../../../../../shared/constants/app'; @@ -275,7 +275,7 @@ export default class SignatureRequestOriginal extends Component { await rejectPendingApproval( id, - serializeError(ethErrors.provider.userRejectedRequest()), + serializeError(providerErrors.userRejectedRequest()), ); clearConfirmTransaction(); history.push(mostRecentOverviewPage); @@ -304,7 +304,7 @@ export default class SignatureRequestOriginal extends Component { onCancel={async () => { await rejectPendingApproval( txData.id, - serializeError(ethErrors.provider.userRejectedRequest()), + serializeError(providerErrors.userRejectedRequest()), ); clearConfirmTransaction(); history.push(mostRecentOverviewPage); diff --git a/ui/pages/confirmations/components/signature-request-original/signature-request-original.container.js b/ui/pages/confirmations/components/signature-request-original/signature-request-original.container.js index 817f9f8699d4..0ac6b877fa72 100644 --- a/ui/pages/confirmations/components/signature-request-original/signature-request-original.container.js +++ b/ui/pages/confirmations/components/signature-request-original/signature-request-original.container.js @@ -11,6 +11,7 @@ import { } from '../../../../store/actions'; ///: BEGIN:ONLY_INCLUDE_IF(build-mmi) // eslint-disable-next-line import/order +import { getErrorMessage } from '../../../../../shared/modules/error'; import { mmiActionsFactory, setPersonalMessageInProgress, @@ -173,7 +174,7 @@ function mergeProps(stateProps, dispatchProps, ownProps) { } catch (err) { await dispatchProps.setWaitForConfirmDeepLinkDialog(true); await dispatchProps.showTransactionsFailedModal({ - errorMessage: err.message, + errorMessage: getErrorMessage(err), closeNotification: true, operationFailed: true, }); diff --git a/ui/pages/confirmations/components/signature-request-siwe/signature-request-siwe.js b/ui/pages/confirmations/components/signature-request-siwe/signature-request-siwe.js index 1ade6dd1a630..e1effe5c8ff3 100644 --- a/ui/pages/confirmations/components/signature-request-siwe/signature-request-siwe.js +++ b/ui/pages/confirmations/components/signature-request-siwe/signature-request-siwe.js @@ -4,7 +4,7 @@ import { useSelector, useDispatch } from 'react-redux'; import { useHistory } from 'react-router-dom'; import log from 'loglevel'; import { isValidSIWEOrigin } from '@metamask/controller-utils'; -import { ethErrors, serializeError } from 'eth-rpc-errors'; +import { providerErrors, serializeError } from '@metamask/rpc-errors'; import { BannerAlert, Text } from '../../../../components/component-library'; import Popover from '../../../../components/ui/popover'; import Checkbox from '../../../../components/ui/check-box'; @@ -102,7 +102,7 @@ export default function SignatureRequestSIWE({ txData, warnings }) { await dispatch( rejectPendingApproval( id, - serializeError(ethErrors.provider.userRejectedRequest()), + serializeError(providerErrors.userRejectedRequest()), ), ); } catch (e) { diff --git a/ui/pages/confirmations/components/signature-request/signature-request.js b/ui/pages/confirmations/components/signature-request/signature-request.js index f15c7045e2d7..ce6967b50f70 100644 --- a/ui/pages/confirmations/components/signature-request/signature-request.js +++ b/ui/pages/confirmations/components/signature-request/signature-request.js @@ -8,7 +8,7 @@ import { } from 'react-redux'; import PropTypes from 'prop-types'; import { memoize } from 'lodash'; -import { ethErrors, serializeError } from 'eth-rpc-errors'; +import { providerErrors, serializeError } from '@metamask/rpc-errors'; import { resolvePendingApproval, completedTx, @@ -176,7 +176,7 @@ const SignatureRequest = ({ txData, warnings }) => { await dispatch( rejectPendingApproval( id, - serializeError(ethErrors.provider.userRejectedRequest()), + serializeError(providerErrors.userRejectedRequest()), ), ); trackEvent({ diff --git a/ui/pages/confirmations/confirmation/confirmation.js b/ui/pages/confirmations/confirmation/confirmation.js index 12b2af503f7f..4bb1f4f7d203 100644 --- a/ui/pages/confirmations/confirmation/confirmation.js +++ b/ui/pages/confirmations/confirmation/confirmation.js @@ -14,6 +14,7 @@ import { produce } from 'immer'; import log from 'loglevel'; import { ApprovalType } from '@metamask/controller-utils'; import { DIALOG_APPROVAL_TYPES } from '@metamask/snaps-rpc-methods'; +import { CHAIN_SPEC_URL } from '../../../../shared/constants/network'; import fetchWithCache from '../../../../shared/lib/fetch-with-cache'; import { MetaMetricsEventCategory, @@ -372,7 +373,8 @@ export default function ConfirmationPage({ try { if (useSafeChainsListValidation) { const response = await fetchWithCache({ - url: 'https://chainid.network/chains.json', + url: CHAIN_SPEC_URL, + allowStale: true, cacheOptions: { cacheRefreshTime: DAY }, functionName: 'getSafeChainsList', }); diff --git a/ui/pages/confirmations/confirmation/templates/add-ethereum-chain.js b/ui/pages/confirmations/confirmation/templates/add-ethereum-chain.js index d14048897e39..998751c6e3a7 100644 --- a/ui/pages/confirmations/confirmation/templates/add-ethereum-chain.js +++ b/ui/pages/confirmations/confirmation/templates/add-ethereum-chain.js @@ -1,4 +1,4 @@ -import { ethErrors } from 'eth-rpc-errors'; +import { providerErrors } from '@metamask/rpc-errors'; import React from 'react'; import { RpcEndpointType } from '@metamask/network-controller'; @@ -564,7 +564,7 @@ function getValues(pendingApproval, t, actions, history, data) { onCancel: () => actions.rejectPendingApproval( pendingApproval.id, - ethErrors.provider.userRejectedRequest().serialize(), + providerErrors.userRejectedRequest().serialize(), ), networkDisplay: !originIsMetaMask, }; diff --git a/ui/pages/confirmations/confirmation/templates/switch-ethereum-chain.js b/ui/pages/confirmations/confirmation/templates/switch-ethereum-chain.js index c03d03d3891a..7dbdb8c9c757 100644 --- a/ui/pages/confirmations/confirmation/templates/switch-ethereum-chain.js +++ b/ui/pages/confirmations/confirmation/templates/switch-ethereum-chain.js @@ -1,4 +1,4 @@ -import { ethErrors } from 'eth-rpc-errors'; +import { providerErrors } from '@metamask/rpc-errors'; import { JustifyContent, SEVERITIES, @@ -85,7 +85,7 @@ function getValues(pendingApproval, t, actions) { onCancel: () => actions.rejectPendingApproval( pendingApproval.id, - ethErrors.provider.userRejectedRequest().serialize(), + providerErrors.userRejectedRequest().serialize(), ), networkDisplay: true, }; diff --git a/ui/pages/confirmations/constants/index.ts b/ui/pages/confirmations/constants/index.ts index 38fd05b714ba..7e26ce5c6d62 100644 --- a/ui/pages/confirmations/constants/index.ts +++ b/ui/pages/confirmations/constants/index.ts @@ -9,3 +9,8 @@ export const TYPED_SIGNATURE_VERSIONS = { }; export const SPENDING_CAP_UNLIMITED_MSG = 'UNLIMITED MESSAGE'; + +export const TypedSignSignaturePrimaryTypes = { + PERMIT: 'Permit', + ORDER: 'Order', +}; diff --git a/ui/pages/confirmations/hooks/useTypedSignSignatureInfo.test.js b/ui/pages/confirmations/hooks/useTypedSignSignatureInfo.test.js new file mode 100644 index 000000000000..38468749782d --- /dev/null +++ b/ui/pages/confirmations/hooks/useTypedSignSignatureInfo.test.js @@ -0,0 +1,27 @@ +import { renderHook } from '@testing-library/react-hooks'; + +import { TokenStandard } from '../../../../shared/constants/transaction'; +import { permitNFTSignatureMsg } from '../../../../test/data/confirmations/typed_sign'; +import { unapprovedPersonalSignMsg } from '../../../../test/data/confirmations/personal_sign'; +import { TypedSignSignaturePrimaryTypes } from '../constants'; +import { useTypedSignSignatureInfo } from './useTypedSignSignatureInfo'; + +describe('useTypedSignSignatureInfo', () => { + it('should return details for primaty type and token standard', () => { + const { result } = renderHook(() => + useTypedSignSignatureInfo(permitNFTSignatureMsg), + ); + expect(result.current.primaryType).toStrictEqual( + TypedSignSignaturePrimaryTypes.PERMIT, + ); + expect(result.current.tokenStandard).toStrictEqual(TokenStandard.ERC721); + }); + + it('should return empty object if confirmation is not typed sign', () => { + const { result } = renderHook(() => + useTypedSignSignatureInfo(unapprovedPersonalSignMsg), + ); + expect(result.current.primaryType).toBeUndefined(); + expect(result.current.tokenStandard).toBeUndefined(); + }); +}); diff --git a/ui/pages/confirmations/hooks/useTypedSignSignatureInfo.ts b/ui/pages/confirmations/hooks/useTypedSignSignatureInfo.ts new file mode 100644 index 000000000000..30d4e58f1525 --- /dev/null +++ b/ui/pages/confirmations/hooks/useTypedSignSignatureInfo.ts @@ -0,0 +1,53 @@ +import { useMemo } from 'react'; + +import { + isOrderSignatureRequest, + isPermitSignatureRequest, + isSignatureTransactionType, +} from '../utils'; +import { SignatureRequestType } from '../types/confirm'; +import { parseTypedDataMessage } from '../../../../shared/modules/transaction.utils'; +import { TokenStandard } from '../../../../shared/constants/transaction'; +import { MESSAGE_TYPE } from '../../../../shared/constants/app'; +import { TypedSignSignaturePrimaryTypes } from '../constants'; + +export const useTypedSignSignatureInfo = ( + confirmation: SignatureRequestType, +) => { + const primaryType = useMemo(() => { + if ( + !confirmation || + !isSignatureTransactionType(confirmation) || + confirmation?.type !== MESSAGE_TYPE.ETH_SIGN_TYPED_DATA + ) { + return undefined; + } + if (isPermitSignatureRequest(confirmation)) { + return TypedSignSignaturePrimaryTypes.PERMIT; + } else if (isOrderSignatureRequest(confirmation)) { + return TypedSignSignaturePrimaryTypes.ORDER; + } + return undefined; + }, [confirmation]); + + // here we are using presence of tokenId in typed message data to know if its NFT permit + // we can get contract details for verifyingContract but that is async process taking longer + // and result in confirmation page content loading late + const tokenStandard = useMemo(() => { + if (primaryType !== TypedSignSignaturePrimaryTypes.PERMIT) { + return undefined; + } + const { + message: { tokenId }, + } = parseTypedDataMessage(confirmation?.msgParams?.data as string); + if (tokenId !== undefined) { + return TokenStandard.ERC721; + } + return undefined; + }, [confirmation, primaryType]); + + return { + primaryType: primaryType as keyof typeof TypedSignSignaturePrimaryTypes, + tokenStandard, + }; +}; diff --git a/ui/pages/create-account/connect-hardware/index.test.tsx b/ui/pages/create-account/connect-hardware/index.test.tsx index 6e0d2627d4aa..0b8585fd0b5c 100644 --- a/ui/pages/create-account/connect-hardware/index.test.tsx +++ b/ui/pages/create-account/connect-hardware/index.test.tsx @@ -30,6 +30,10 @@ jest.mock('../../../selectors', () => ({ }, })); +jest.mock('../../../ducks/bridge/selectors', () => ({ + getAllBridgeableNetworks: () => [], +})); + const MOCK_RECENT_PAGE = '/home'; jest.mock('../../../ducks/history/history', () => ({ getMostRecentOverviewPage: jest diff --git a/ui/pages/error/error.component.js b/ui/pages/error/error.component.js index 57a8e40c6473..f7ab9c593d40 100644 --- a/ui/pages/error/error.component.js +++ b/ui/pages/error/error.component.js @@ -4,6 +4,7 @@ import PropTypes from 'prop-types'; // eslint-disable-next-line import/no-restricted-paths import { getEnvironmentType } from '../../../app/scripts/lib/util'; import { ENVIRONMENT_TYPE_POPUP } from '../../../shared/constants/app'; +import { getErrorMessage } from '../../../shared/modules/error'; import { SUPPORT_REQUEST_LINK } from '../../helpers/constants/common'; import { MetaMetricsContextProp, @@ -72,6 +73,7 @@ class ErrorPage extends PureComponent { const message = isPopup ? t('errorPagePopupMessage', [supportLink]) : t('errorPageMessage', [supportLink]); + const errorMessage = getErrorMessage(error); return (
@@ -81,8 +83,8 @@ class ErrorPage extends PureComponent {
{t('errorDetails')}
    - {error.message - ? this.renderErrorDetail(t('errorMessage', [error.message])) + {errorMessage + ? this.renderErrorDetail(t('errorMessage', [errorMessage])) : null} {error.code ? this.renderErrorDetail(t('errorCode', [error.code])) diff --git a/ui/pages/keychains/reveal-seed.js b/ui/pages/keychains/reveal-seed.js index 492f37545138..cf3e285eba64 100644 --- a/ui/pages/keychains/reveal-seed.js +++ b/ui/pages/keychains/reveal-seed.js @@ -2,6 +2,7 @@ import qrCode from 'qrcode-generator'; import React, { useContext, useEffect, useState } from 'react'; import { useDispatch, useSelector } from 'react-redux'; import { useHistory } from 'react-router-dom'; +import { getErrorMessage } from '../../../shared/modules/error'; import { MetaMetricsEventCategory, MetaMetricsEventKeyType, @@ -97,7 +98,7 @@ export default function RevealSeedPage() { reason: e.message, // 'incorrect_password', }, }); - setError(e.message); + setError(getErrorMessage(e)); }); }; diff --git a/ui/pages/onboarding-flow/privacy-settings/privacy-settings.js b/ui/pages/onboarding-flow/privacy-settings/privacy-settings.js index ee11f63caf2a..aed08f196957 100644 --- a/ui/pages/onboarding-flow/privacy-settings/privacy-settings.js +++ b/ui/pages/onboarding-flow/privacy-settings/privacy-settings.js @@ -1,6 +1,7 @@ import React, { useContext, useState, useEffect } from 'react'; import { useDispatch, useSelector } from 'react-redux'; import { useHistory } from 'react-router-dom'; +import classnames from 'classnames'; import { ButtonVariant } from '@metamask/snaps-sdk'; // TODO: Remove restricted import // eslint-disable-next-line import/no-restricted-paths @@ -78,6 +79,8 @@ import { } from '../../../../shared/constants/network'; import { Setting } from './setting'; +const ANIMATION_TIME = 500; + /** * Profile Syncing Setting props * @@ -232,14 +235,14 @@ export default function PrivacySettings() { setTimeout(() => { setHiddenClass(false); - }, 500); + }, ANIMATION_TIME); }; const handleBack = () => { setShowDetail(false); setTimeout(() => { setHiddenClass(true); - }, 500); + }, ANIMATION_TIME); }; const items = [ @@ -252,7 +255,10 @@ export default function PrivacySettings() { <>
    - {selectedItem && selectedItem.title} + {selectedItem?.title} @@ -397,7 +403,7 @@ export default function PrivacySettings() { className="privacy-settings__settings" data-testid="privacy-settings-settings" > - {selectedItem && selectedItem.id === 1 ? ( + {selectedItem?.id === 1 ? ( <> ) : null} - {selectedItem && selectedItem.id === 2 ? ( + {selectedItem?.id === 2 ? ( <> ) : null} - {selectedItem && selectedItem.id === 3 ? ( + {selectedItem?.id === 3 ? ( <>
    `; + +exports[`ConnectPage should render with defaults from the requested permissions 1`] = ` +
    +
    +
    +
    +
    +

    +

    + Connect with MetaMask +

    +

    + This site wants to + : +

    +

    +
    +
    +
    +
    +
    +
    + +
    +
    +

    + See your accounts and suggest transactions +

    +
    + + Requesting for Test Account + + +
    +
    + +
    +
    +
    + +
    +
    +

    + Use your enabled networks +

    +
    + + Requesting for + +
    Alerts"" + data-tooltipped="" + style="display: inline;" + > +
    +
    +
    +
    + Custom Mainnet RPC logo +
    +
    +
    +
    +
    +
    +
    + +
    +
    +
    + +
    +
    +
    +`; diff --git a/ui/pages/permissions-connect/connect-page/connect-page.test.tsx b/ui/pages/permissions-connect/connect-page/connect-page.test.tsx index d7c50c6aa501..ef705e474ad9 100644 --- a/ui/pages/permissions-connect/connect-page/connect-page.test.tsx +++ b/ui/pages/permissions-connect/connect-page/connect-page.test.tsx @@ -2,6 +2,11 @@ import React from 'react'; import { renderWithProvider } from '../../../../test/jest/rendering'; import mockState from '../../../../test/data/mock-state.json'; import configureStore from '../../../store/store'; +import { + CaveatTypes, + EndowmentTypes, + RestrictedMethods, +} from '../../../../shared/constants/permissions'; import { ConnectPage, ConnectPageRequest } from './connect-page'; const render = ( @@ -74,4 +79,36 @@ describe('ConnectPage', () => { expect(confirmButton).toBeDefined(); expect(cancelButton).toBeDefined(); }); + + it('should render with defaults from the requested permissions', () => { + const { container } = render({ + request: { + id: '1', + origin: 'https://test.dapp', + permissions: { + [RestrictedMethods.eth_accounts]: { + caveats: [ + { + type: CaveatTypes.restrictReturnedAccounts, + value: ['0x0dcd5d886577d5081b0c52e242ef29e70be3e7bc'], + }, + ], + }, + [EndowmentTypes.permittedChains]: { + caveats: [ + { + type: CaveatTypes.restrictNetworkSwitching, + value: ['0x1'], + }, + ], + }, + }, + }, + permissionsRequestId: '1', + rejectPermissionsRequest: jest.fn(), + approveConnection: jest.fn(), + activeTabOrigin: 'https://test.dapp', + }); + expect(container).toMatchSnapshot(); + }); }); diff --git a/ui/pages/permissions-connect/connect-page/connect-page.tsx b/ui/pages/permissions-connect/connect-page/connect-page.tsx index a30047fbd38a..0ae22b3d9e0f 100644 --- a/ui/pages/permissions-connect/connect-page/connect-page.tsx +++ b/ui/pages/permissions-connect/connect-page/connect-page.tsx @@ -34,10 +34,19 @@ import { MergedInternalAccount } from '../../../selectors/selectors.types'; import { mergeAccounts } from '../../../components/multichain/account-list-menu/account-list-menu'; import { TEST_CHAINS } from '../../../../shared/constants/network'; import PermissionsConnectFooter from '../../../components/app/permissions-connect-footer'; +import { + CaveatTypes, + EndowmentTypes, + RestrictedMethods, +} from '../../../../shared/constants/permissions'; export type ConnectPageRequest = { id: string; origin: string; + permissions?: Record< + string, + { caveats?: { type: string; value: string[] }[] } + >; }; type ConnectPageProps = { @@ -53,10 +62,23 @@ export const ConnectPage: React.FC = ({ permissionsRequestId, rejectPermissionsRequest, approveConnection, - activeTabOrigin, }) => { const t = useI18nContext(); + const ethAccountsPermission = + request?.permissions?.[RestrictedMethods.eth_accounts]; + const requestedAccounts = + ethAccountsPermission?.caveats?.find( + (caveat) => caveat.type === CaveatTypes.restrictReturnedAccounts, + )?.value || []; + + const permittedChainsPermission = + request?.permissions?.[EndowmentTypes.permittedChains]; + const requestedChainIds = + permittedChainsPermission?.caveats?.find( + (caveat) => caveat.type === CaveatTypes.restrictNetworkSwitching, + )?.value || []; + const networkConfigurations = useSelector(getNetworkConfigurationsByChainId); const [nonTestNetworks, testNetworks] = useMemo( () => @@ -70,7 +92,10 @@ export const ConnectPage: React.FC = ({ ), [networkConfigurations], ); - const defaultSelectedChainIds = nonTestNetworks.map(({ chainId }) => chainId); + const defaultSelectedChainIds = + requestedChainIds.length > 0 + ? requestedChainIds + : nonTestNetworks.map(({ chainId }) => chainId); const [selectedChainIds, setSelectedChainIds] = useState( defaultSelectedChainIds, ); @@ -84,7 +109,10 @@ export const ConnectPage: React.FC = ({ }, [accounts, internalAccounts]); const currentAccount = useSelector(getSelectedInternalAccount); - const defaultAccountsAddresses = [currentAccount?.address]; + const defaultAccountsAddresses = + requestedAccounts.length > 0 + ? requestedAccounts + : [currentAccount?.address]; const [selectedAccountAddresses, setSelectedAccountAddresses] = useState( defaultAccountsAddresses, ); @@ -117,7 +145,6 @@ export const ConnectPage: React.FC = ({ onSelectChainIds={setSelectedChainIds} selectedAccountAddresses={selectedAccountAddresses} selectedChainIds={selectedChainIds} - activeTabOrigin={activeTabOrigin} isConnectFlow /> diff --git a/ui/pages/permissions-connect/permissions-connect.component.js b/ui/pages/permissions-connect/permissions-connect.component.js index 417a82777b36..e32f85609406 100644 --- a/ui/pages/permissions-connect/permissions-connect.component.js +++ b/ui/pages/permissions-connect/permissions-connect.component.js @@ -1,7 +1,7 @@ import PropTypes from 'prop-types'; import React, { Component } from 'react'; import { Switch, Route } from 'react-router-dom'; -import { ethErrors, serializeError } from 'eth-rpc-errors'; +import { providerErrors, serializeError } from '@metamask/rpc-errors'; import { SubjectType } from '@metamask/permission-controller'; // TODO: Remove restricted import // eslint-disable-next-line import/no-restricted-paths @@ -443,7 +443,7 @@ export default class PermissionConnect extends Component { rejectSnapInstall={(requestId) => { rejectPendingApproval( requestId, - serializeError(ethErrors.provider.userRejectedRequest()), + serializeError(providerErrors.userRejectedRequest()), ); this.setState({ permissionsApproved: true }); }} @@ -469,7 +469,7 @@ export default class PermissionConnect extends Component { rejectSnapUpdate={(requestId) => { rejectPendingApproval( requestId, - serializeError(ethErrors.provider.userRejectedRequest()), + serializeError(providerErrors.userRejectedRequest()), ); this.setState({ permissionsApproved: false }); }} diff --git a/ui/pages/routes/routes.component.js b/ui/pages/routes/routes.component.js index a27d59e3b33b..d59c4b29e0c1 100644 --- a/ui/pages/routes/routes.component.js +++ b/ui/pages/routes/routes.component.js @@ -500,6 +500,17 @@ export default class Routes extends Component { hideAppHeader() { const { location } = this.props; + const isCrossChainSwapsPage = Boolean( + matchPath(location.pathname, { + path: `${CROSS_CHAIN_SWAP_ROUTE}`, + exact: false, + }), + ); + + if (isCrossChainSwapsPage) { + return true; + } + const isNotificationsPage = Boolean( matchPath(location.pathname, { path: `${NOTIFICATIONS_ROUTE}`, diff --git a/ui/pages/settings/networks-tab/networks-form/use-safe-chains.ts b/ui/pages/settings/networks-tab/networks-form/use-safe-chains.ts index 56b237e9fce4..3556c2196b31 100644 --- a/ui/pages/settings/networks-tab/networks-form/use-safe-chains.ts +++ b/ui/pages/settings/networks-tab/networks-form/use-safe-chains.ts @@ -3,6 +3,7 @@ import { useSelector } from 'react-redux'; import { useSafeChainsListValidationSelector } from '../../../../selectors'; import fetchWithCache from '../../../../../shared/lib/fetch-with-cache'; +import { CHAIN_SPEC_URL } from '../../../../../shared/constants/network'; import { DAY } from '../../../../../shared/constants/time'; export type SafeChain = { @@ -25,8 +26,9 @@ export const useSafeChains = () => { if (useSafeChainsListValidation) { useEffect(() => { fetchWithCache({ - url: 'https://chainid.network/chains.json', + url: CHAIN_SPEC_URL, functionName: 'getSafeChainsList', + allowStale: true, cacheOptions: { cacheRefreshTime: DAY }, }) .then((response) => { diff --git a/ui/store/actions.ts b/ui/store/actions.ts index 9c5ab7ebb45e..43c7fb189822 100644 --- a/ui/store/actions.ts +++ b/ui/store/actions.ts @@ -9,7 +9,8 @@ import { captureException } from '@sentry/browser'; import { capitalize, isEqual } from 'lodash'; import { ThunkAction } from 'redux-thunk'; import { Action, AnyAction } from 'redux'; -import { ethErrors, serializeError } from 'eth-rpc-errors'; +import { providerErrors, serializeError } from '@metamask/rpc-errors'; +import type { DataWithOptionalCause } from '@metamask/rpc-errors'; import type { Hex, Json } from '@metamask/utils'; import { AssetsContractController, @@ -111,6 +112,7 @@ import { import { decimalToHex } from '../../shared/modules/conversion.utils'; import { TxGasFees, PriorityLevels } from '../../shared/constants/gas'; import { + getErrorMessage, isErrorWithMessage, logErrorWithMessage, } from '../../shared/modules/error'; @@ -228,7 +230,7 @@ export function createNewVaultAndRestore( dispatch(hideLoadingIndication()); }) .catch((err) => { - dispatch(displayWarning(err.message)); + dispatch(displayWarning(err)); dispatch(hideLoadingIndication()); return Promise.reject(err); }); @@ -248,7 +250,7 @@ export function createNewVaultAndGetSeedPhrase( } catch (error) { dispatch(displayWarning(error)); if (isErrorWithMessage(error)) { - throw new Error(error.message); + throw new Error(getErrorMessage(error)); } else { throw error; } @@ -272,7 +274,7 @@ export function unlockAndGetSeedPhrase( } catch (error) { dispatch(displayWarning(error)); if (isErrorWithMessage(error)) { - throw new Error(error.message); + throw new Error(getErrorMessage(error)); } else { throw error; } @@ -375,7 +377,7 @@ export function resetAccount(): ThunkAction< dispatch(hideLoadingIndication()); if (err) { if (isErrorWithMessage(err)) { - dispatch(displayWarning(err.message)); + dispatch(displayWarning(err)); } reject(err); return; @@ -575,11 +577,12 @@ export function connectHardware( ); } catch (error) { logErrorWithMessage(error); + const message = getErrorMessage(error); if ( deviceName === HardwareDeviceNames.ledger && ledgerTransportType === LedgerTransportTypes.webhid && isErrorWithMessage(error) && - error.message.match('Failed to open the device') + message.match('Failed to open the device') ) { dispatch(displayWarning(t('ledgerDeviceOpenFailureMessage'))); throw new Error(t('ledgerDeviceOpenFailureMessage')); @@ -1378,10 +1381,7 @@ export function cancelTx( return new Promise((resolve, reject) => { callBackgroundMethod( 'rejectPendingApproval', - [ - String(txMeta.id), - ethErrors.provider.userRejectedRequest().serialize(), - ], + [String(txMeta.id), providerErrors.userRejectedRequest().serialize()], (error) => { if (error) { reject(error); @@ -1427,10 +1427,7 @@ export function cancelTxs( new Promise((resolve, reject) => { callBackgroundMethod( 'rejectPendingApproval', - [ - String(id), - ethErrors.provider.userRejectedRequest().serialize(), - ], + [String(id), providerErrors.userRejectedRequest().serialize()], (err) => { if (err) { reject(err); @@ -1666,7 +1663,7 @@ export function lockMetamask(): ThunkAction< return backgroundSetLocked() .then(() => forceUpdateMetamaskState(dispatch)) .catch((error) => { - dispatch(displayWarning(error.message)); + dispatch(displayWarning(getErrorMessage(error))); return Promise.reject(error); }) .then(() => { @@ -2059,15 +2056,17 @@ export function addNftVerifyOwnership( tokenID, ]); } catch (error) { - if ( - isErrorWithMessage(error) && - (error.message.includes('This NFT is not owned by the user') || - error.message.includes('Unable to verify ownership')) - ) { - throw error; - } else { - logErrorWithMessage(error); - dispatch(displayWarning(error)); + if (isErrorWithMessage(error)) { + const message = getErrorMessage(error); + if ( + message.includes('This NFT is not owned by the user') || + message.includes('Unable to verify ownership') + ) { + throw error; + } else { + logErrorWithMessage(error); + dispatch(displayWarning(error)); + } } } finally { await forceUpdateMetamaskState(dispatch); @@ -2810,7 +2809,8 @@ export function displayWarning(payload: unknown): PayloadAction { if (isErrorWithMessage(payload)) { return { type: actionConstants.DISPLAY_WARNING, - payload: payload.message, + payload: + (payload as DataWithOptionalCause)?.cause?.message || payload.message, }; } else if (typeof payload === 'string') { return { @@ -4061,7 +4061,7 @@ export function rejectAllMessages( ): ThunkAction { return async (dispatch: MetaMaskReduxDispatch) => { const userRejectionError = serializeError( - ethErrors.provider.userRejectedRequest(), + providerErrors.userRejectedRequest(), ); await Promise.all( messageList.map( diff --git a/ui/store/institutional/institution-background.test.js b/ui/store/institutional/institution-background.test.js index 7a3ca82e53eb..722bf7a3135c 100644 --- a/ui/store/institutional/institution-background.test.js +++ b/ui/store/institutional/institution-background.test.js @@ -11,6 +11,7 @@ import { setNoteToTraderMessage, setTypedMessageInProgress, setPersonalMessageInProgress, + logAndStoreApiRequest, } from './institution-background'; jest.mock('../actions', () => ({ @@ -173,4 +174,75 @@ describe('Institution Actions', () => { ); }); }); + + describe('#logAndStoreApiRequest', () => { + it('should call submitRequestToBackground with correct parameters', async () => { + const mockLogData = { + id: '123', + method: 'GET', + request: { + url: 'https://api.example.com/data', + headers: { 'Content-Type': 'application/json' }, + }, + response: { + status: 200, + body: '{"success": true}', + }, + timestamp: 1234567890, + }; + + await logAndStoreApiRequest(mockLogData); + + expect(submitRequestToBackground).toHaveBeenCalledWith( + 'logAndStoreApiRequest', + [mockLogData], + ); + }); + + it('should return the result from submitRequestToBackground', async () => { + const mockLogData = { + id: '456', + method: 'POST', + request: { + url: 'https://api.example.com/submit', + headers: { 'Content-Type': 'application/json' }, + body: '{"data": "test"}', + }, + response: { + status: 201, + body: '{"id": "789"}', + }, + timestamp: 1234567890, + }; + + submitRequestToBackground.mockResolvedValue('success'); + + const result = await logAndStoreApiRequest(mockLogData); + + expect(result).toBe('success'); + }); + + it('should throw an error if submitRequestToBackground fails', async () => { + const mockLogData = { + id: '789', + method: 'GET', + request: { + url: 'https://api.example.com/error', + headers: { 'Content-Type': 'application/json' }, + }, + response: { + status: 500, + body: '{"error": "Internal Server Error"}', + }, + timestamp: 1234567890, + }; + + const mockError = new Error('Background request failed'); + submitRequestToBackground.mockRejectedValue(mockError); + + await expect(logAndStoreApiRequest(mockLogData)).rejects.toThrow( + 'Background request failed', + ); + }); + }); }); diff --git a/ui/store/institutional/institution-background.ts b/ui/store/institutional/institution-background.ts index 1604953f9b99..fd42d069b8b7 100644 --- a/ui/store/institutional/institution-background.ts +++ b/ui/store/institutional/institution-background.ts @@ -1,6 +1,7 @@ import log from 'loglevel'; import { ThunkAction } from 'redux-thunk'; import { AnyAction } from 'redux'; +import { IApiCallLogEntry } from '@metamask-institutional/types'; import { forceUpdateMetamaskState, displayWarning, @@ -12,7 +13,10 @@ import { submitRequestToBackground, } from '../background-connection'; import { MetaMaskReduxDispatch, MetaMaskReduxState } from '../store'; -import { isErrorWithMessage } from '../../../shared/modules/error'; +import { + isErrorWithMessage, + getErrorMessage, +} from '../../../shared/modules/error'; import { ConnectionRequest } from '../../../shared/constants/mmi-controller'; export function showInteractiveReplacementTokenBanner({ @@ -34,8 +38,8 @@ export function showInteractiveReplacementTokenBanner({ // eslint-disable-next-line @typescript-eslint/no-explicit-any } catch (err: any) { if (err) { - dispatch(displayWarning(err.message)); - throw new Error(err.message); + dispatch(displayWarning(err)); + throw new Error(getErrorMessage(err)); } } }; @@ -80,7 +84,7 @@ export function setTypedMessageInProgress(msgId: string) { // eslint-disable-next-line @typescript-eslint/no-explicit-any } catch (error: any) { log.error(error); - dispatch(displayWarning(error.message)); + dispatch(displayWarning(error)); } finally { await forceUpdateMetamaskState(dispatch); dispatch(hideLoadingIndication()); @@ -97,7 +101,7 @@ export function setPersonalMessageInProgress(msgId: string) { // eslint-disable-next-line @typescript-eslint/no-explicit-any } catch (error: any) { log.error(error); - dispatch(displayWarning(error.message)); + dispatch(displayWarning(error)); } finally { await forceUpdateMetamaskState(dispatch); dispatch(hideLoadingIndication()); @@ -105,6 +109,12 @@ export function setPersonalMessageInProgress(msgId: string) { }; } +export async function logAndStoreApiRequest( + logData: IApiCallLogEntry, +): Promise { + return await submitRequestToBackground('logAndStoreApiRequest', [logData]); +} + /** * A factory that contains all MMI actions ready to use * Example usage: @@ -135,7 +145,7 @@ export function mmiActionsFactory() { } catch (error) { dispatch(displayWarning(error)); if (isErrorWithMessage(error)) { - throw new Error(error.message); + throw new Error(getErrorMessage(error)); } else { throw error; } @@ -157,7 +167,7 @@ export function mmiActionsFactory() { return () => { callBackgroundMethod(name, [payload], (err) => { if (isErrorWithMessage(err)) { - throw new Error(err.message); + throw new Error(getErrorMessage(err)); } }); }; diff --git a/yarn.lock b/yarn.lock index 94ecd006df3e..3c34b05fee52 100644 --- a/yarn.lock +++ b/yarn.lock @@ -4627,60 +4627,60 @@ __metadata: languageName: node linkType: hard -"@metamask-institutional/custody-controller@npm:^0.2.30, @metamask-institutional/custody-controller@npm:^0.2.31": - version: 0.2.31 - resolution: "@metamask-institutional/custody-controller@npm:0.2.31" +"@metamask-institutional/custody-controller@npm:^0.3.0": + version: 0.3.0 + resolution: "@metamask-institutional/custody-controller@npm:0.3.0" dependencies: "@ethereumjs/util": "npm:^8.0.5" - "@metamask-institutional/custody-keyring": "npm:^2.0.3" - "@metamask-institutional/sdk": "npm:^0.1.30" - "@metamask-institutional/types": "npm:^1.1.0" + "@metamask-institutional/custody-keyring": "npm:^2.1.0" + "@metamask-institutional/sdk": "npm:^0.2.0" + "@metamask-institutional/types": "npm:^1.2.0" "@metamask/obs-store": "npm:^9.0.0" - checksum: 10/f856c98db42a21639d9ec5d1c835bc302b5a1b3fb821aae8641f63a9400f8303b8fa578368a2f2d2a1ec0c148c070f809b8c0fa46fa3fd2fa29f80e0ec1da207 + checksum: 10/572e96d4b23566fb8dbf06ab0117c68c2d1db901deea69eee48d08f41ea3e1dbbbb3090c83cce6ff240ed8061e84df1b61befaf57da764b495eb0978d45fac42 languageName: node linkType: hard -"@metamask-institutional/custody-keyring@npm:^2.0.3": - version: 2.0.3 - resolution: "@metamask-institutional/custody-keyring@npm:2.0.3" +"@metamask-institutional/custody-keyring@npm:^2.1.0": + version: 2.1.0 + resolution: "@metamask-institutional/custody-keyring@npm:2.1.0" dependencies: "@ethereumjs/tx": "npm:^4.1.1" "@ethereumjs/util": "npm:^8.0.5" "@metamask-institutional/configuration-client": "npm:^2.0.1" - "@metamask-institutional/sdk": "npm:^0.1.30" - "@metamask-institutional/types": "npm:^1.1.0" + "@metamask-institutional/sdk": "npm:^0.2.0" + "@metamask-institutional/types": "npm:^1.2.0" "@metamask/obs-store": "npm:^9.0.0" crypto: "npm:^1.0.1" lodash.clonedeep: "npm:^4.5.0" - checksum: 10/987beeeed67fb92a436eb1318f48ec2cc0ceb1ae944b7f5b2e492dcdc28a4298c5a8d25a520022ac52f87a411f7341961100be47a9626fbb1674aed349d98737 + checksum: 10/78421e38fed4ad88412593a307fc13f220b0e5a83dee76de0032c835a7896bf23bb76030e4bb7d69bfa604db7a31faa6312ac64b05cc135d8afb723fb3660920 languageName: node linkType: hard -"@metamask-institutional/extension@npm:^0.3.27": - version: 0.3.27 - resolution: "@metamask-institutional/extension@npm:0.3.27" +"@metamask-institutional/extension@npm:^0.3.28": + version: 0.3.28 + resolution: "@metamask-institutional/extension@npm:0.3.28" dependencies: "@ethereumjs/util": "npm:^8.0.5" - "@metamask-institutional/custody-controller": "npm:^0.2.30" - "@metamask-institutional/custody-keyring": "npm:^2.0.3" + "@metamask-institutional/custody-controller": "npm:^0.3.0" + "@metamask-institutional/custody-keyring": "npm:^2.1.0" "@metamask-institutional/portfolio-dashboard": "npm:^1.4.1" - "@metamask-institutional/sdk": "npm:^0.1.30" - "@metamask-institutional/transaction-update": "npm:^0.2.5" - "@metamask-institutional/types": "npm:^1.1.0" + "@metamask-institutional/sdk": "npm:^0.2.0" + "@metamask-institutional/transaction-update": "npm:^0.2.6" + "@metamask-institutional/types": "npm:^1.2.0" jest-create-mock-instance: "npm:^2.0.0" jest-fetch-mock: "npm:3.0.3" lodash.clonedeep: "npm:^4.5.0" - checksum: 10/dc9eefe8045607cd415b9db4a8df833c9a523e9d06a3a0e49e4c6e85063924db1f117725a91c926f19ce26d0701fc175ea4ad38fb13a8a3b092434bcd7fd7882 + checksum: 10/a1f73c5281282ab1315ee19dd363330504300c036586ff64c98c176da8ac23046de8e8051956b4e15184faf0720bf324b81c406a1bf85295691c24f191b8f747 languageName: node linkType: hard -"@metamask-institutional/institutional-features@npm:^1.3.5": - version: 1.3.5 - resolution: "@metamask-institutional/institutional-features@npm:1.3.5" +"@metamask-institutional/institutional-features@npm:^1.3.6": + version: 1.3.6 + resolution: "@metamask-institutional/institutional-features@npm:1.3.6" dependencies: - "@metamask-institutional/custody-keyring": "npm:^2.0.3" + "@metamask-institutional/custody-keyring": "npm:^2.1.0" "@metamask/obs-store": "npm:^9.0.0" - checksum: 10/1a154dbbfc71c9fee43d755d901423e3ea17ad149679225481fdc2d73ae95960e1805a792dbe660dd778703614ea5fd7390314bd7099c8ede510db1d23bc08ab + checksum: 10/a6b53f1b0ba8554595498153cbc0d32bb1a2d8374ad6ff9b617fea4e10872120000d14d9916b48ff9bafbac5da954ada99dca5f88f3ba21d4fbb80590804444c languageName: node linkType: hard @@ -4698,17 +4698,17 @@ __metadata: languageName: node linkType: hard -"@metamask-institutional/sdk@npm:^0.1.30": - version: 0.1.30 - resolution: "@metamask-institutional/sdk@npm:0.1.30" +"@metamask-institutional/sdk@npm:^0.2.0": + version: 0.2.0 + resolution: "@metamask-institutional/sdk@npm:0.2.0" dependencies: "@metamask-institutional/simplecache": "npm:^1.1.0" - "@metamask-institutional/types": "npm:^1.1.0" + "@metamask-institutional/types": "npm:^1.2.0" "@types/jsonwebtoken": "npm:^9.0.1" - "@types/node": "npm:^20.11.17" + "@types/node": "npm:^20.14.9" bignumber.js: "npm:^9.1.1" jsonwebtoken: "npm:^9.0.0" - checksum: 10/3f36925fa9399a0ea06e2a64ea89accfb34f0a17581ab69652b4f325a948db10e88faebcca4f7c2d9f5f1f1c7f98bd8f970b7a489218dfd1be8cebc669a2f67e + checksum: 10/59f8b5eff176746ef3c9c406edda340ab04b37df1799d9b56e26fcede95441461d73d4be8b33f1dc3153cddea6baa876eba1232ca538da8f732a29801531a2f8 languageName: node linkType: hard @@ -4719,36 +4719,36 @@ __metadata: languageName: node linkType: hard -"@metamask-institutional/transaction-update@npm:^0.2.5": - version: 0.2.5 - resolution: "@metamask-institutional/transaction-update@npm:0.2.5" +"@metamask-institutional/transaction-update@npm:^0.2.6": + version: 0.2.6 + resolution: "@metamask-institutional/transaction-update@npm:0.2.6" dependencies: "@ethereumjs/util": "npm:^8.0.5" - "@metamask-institutional/custody-keyring": "npm:^2.0.3" - "@metamask-institutional/sdk": "npm:^0.1.30" - "@metamask-institutional/types": "npm:^1.1.0" - "@metamask-institutional/websocket-client": "npm:^0.2.5" + "@metamask-institutional/custody-keyring": "npm:^2.1.0" + "@metamask-institutional/sdk": "npm:^0.2.0" + "@metamask-institutional/types": "npm:^1.2.0" + "@metamask-institutional/websocket-client": "npm:^0.2.6" "@metamask/obs-store": "npm:^9.0.0" - checksum: 10/9dbcf7c38a03becf61ab013f78df225da1f6de12976f328e7809c0edda5ab9e1aeee2b4d5b9430c15d5dc9f7040fa703c560c58073d601110895388c1c15d7a8 + checksum: 10/815c6faaaed9af25ed21d1339790e82622bef81f3c578269afde908dc95d36cc64a549c58164e24f20d9941e8c05e883d02c8886b741e50e3cf83960a8cb00d2 languageName: node linkType: hard -"@metamask-institutional/types@npm:^1.1.0": - version: 1.1.0 - resolution: "@metamask-institutional/types@npm:1.1.0" - checksum: 10/76f3c8529e4fe549bcabe60c39a66dd1a526aa7ea16fe7949e960a884d2c9e5e2e65db4d1123e23eeaae46f88b10aafe365cc693f5f632ef1a8e407373fe2fdf +"@metamask-institutional/types@npm:^1.2.0": + version: 1.2.0 + resolution: "@metamask-institutional/types@npm:1.2.0" + checksum: 10/3e28224c12f1ad955f114de919dbf4abbef19bd19cca3a4544898061d79518a94baa14121ebf6e5c6972dd6b1d1ec8071ebc50a77480ad944c26a2be53af5290 languageName: node linkType: hard -"@metamask-institutional/websocket-client@npm:^0.2.5": - version: 0.2.5 - resolution: "@metamask-institutional/websocket-client@npm:0.2.5" +"@metamask-institutional/websocket-client@npm:^0.2.6": + version: 0.2.6 + resolution: "@metamask-institutional/websocket-client@npm:0.2.6" dependencies: - "@metamask-institutional/custody-keyring": "npm:^2.0.3" - "@metamask-institutional/sdk": "npm:^0.1.30" - "@metamask-institutional/types": "npm:^1.1.0" + "@metamask-institutional/custody-keyring": "npm:^2.1.0" + "@metamask-institutional/sdk": "npm:^0.2.0" + "@metamask-institutional/types": "npm:^1.2.0" mock-socket: "npm:^9.2.1" - checksum: 10/4743ccbb3a92a5b7ddccfd9f72741910bb93cc769023c8b9ee7944bb82f79938e45b10af5f7754b2898dc218c0e3874cb38aa628f96685fc69d956900723755d + checksum: 10/ba59b6d776fdc9d681ac0a294cd3eab961ba9d06d1ebd6a59fbe379cf640c421fdaaf53f6b6ab187ea3f1993b251292deb3c9d1fff8b6717fbd14f2512105190 languageName: node linkType: hard @@ -4981,10 +4981,10 @@ __metadata: languageName: node linkType: hard -"@metamask/bitcoin-wallet-snap@npm:^0.6.1": - version: 0.6.1 - resolution: "@metamask/bitcoin-wallet-snap@npm:0.6.1" - checksum: 10/9c595e328cd63efe62cdda4194efe44ab3da4a54a89007f485280924aa9e8ee37042bda0a07751f3ce01c2c3e4740b16cd130f07558aa84cd57b20a8d5f1d3a7 +"@metamask/bitcoin-wallet-snap@npm:^0.7.0": + version: 0.7.0 + resolution: "@metamask/bitcoin-wallet-snap@npm:0.7.0" + checksum: 10/be4eceef1715c5e6d33d095d5b4aaa974656d945ff0ed0304fdc1244eb8940eb8978f304378367642aa8fd60d6b375eecc2a4653c38ba62ec306c03955c96682 languageName: node linkType: hard @@ -5382,22 +5382,22 @@ __metadata: languageName: node linkType: hard -"@metamask/eth-snap-keyring@npm:^4.3.1, @metamask/eth-snap-keyring@npm:^4.3.6": - version: 4.3.6 - resolution: "@metamask/eth-snap-keyring@npm:4.3.6" +"@metamask/eth-snap-keyring@npm:^4.3.1, @metamask/eth-snap-keyring@npm:^4.3.6, @metamask/eth-snap-keyring@npm:^4.4.0": + version: 4.4.0 + resolution: "@metamask/eth-snap-keyring@npm:4.4.0" dependencies: "@ethereumjs/tx": "npm:^4.2.0" "@metamask/eth-sig-util": "npm:^7.0.3" - "@metamask/snaps-controllers": "npm:^9.7.0" - "@metamask/snaps-sdk": "npm:^6.5.1" - "@metamask/snaps-utils": "npm:^7.8.1" + "@metamask/snaps-controllers": "npm:^9.10.0" + "@metamask/snaps-sdk": "npm:^6.7.0" + "@metamask/snaps-utils": "npm:^8.3.0" "@metamask/superstruct": "npm:^3.1.0" "@metamask/utils": "npm:^9.2.1" "@types/uuid": "npm:^9.0.8" uuid: "npm:^9.0.1" peerDependencies: "@metamask/keyring-api": ^8.1.3 - checksum: 10/378dce125ba9e38b9ba7d9b7124383b4fd8d2782207dc69e1ae9e262beb83f22044eae5200986d4c353de29e5283c289e56b3acb88c8971a63f9365bdde3d5b4 + checksum: 10/fd9926ba3706506bd9a16d1c2501e7c6cd7b7e3e7ea332bc7f28e0fca1f67f4514da51e6f9f4541a7354a2363d04c09c445f61b98fdc366432e1def9c2f27d07 languageName: node linkType: hard @@ -6069,12 +6069,12 @@ __metadata: languageName: node linkType: hard -"@metamask/preinstalled-example-snap@npm:^0.1.0": - version: 0.1.0 - resolution: "@metamask/preinstalled-example-snap@npm:0.1.0" +"@metamask/preinstalled-example-snap@npm:^0.2.0": + version: 0.2.0 + resolution: "@metamask/preinstalled-example-snap@npm:0.2.0" dependencies: - "@metamask/snaps-sdk": "npm:^6.5.0" - checksum: 10/0540aa6c20b17171f3a3bcf9ea2a7be551d6abbf16de9bd55dce038c5602c62a3921c7e840b82a325b0db00f26b96f54568854bdcd091558bd3b8fa8c6188023 + "@metamask/snaps-sdk": "npm:^6.9.0" + checksum: 10/f8ad6f42c9bd7ce3b7fc9b45eecda6191320ff762b48c482ba4944a6d7a228682b833c15e56058f26ac7bb10417dfe9de340af1c8eb9bbe5dc03c665426ccb13 languageName: node linkType: hard @@ -6170,12 +6170,22 @@ __metadata: linkType: hard "@metamask/rpc-errors@npm:^6.0.0, @metamask/rpc-errors@npm:^6.2.1, @metamask/rpc-errors@npm:^6.3.1": - version: 6.3.1 - resolution: "@metamask/rpc-errors@npm:6.3.1" + version: 6.4.0 + resolution: "@metamask/rpc-errors@npm:6.4.0" + dependencies: + "@metamask/utils": "npm:^9.0.0" + fast-safe-stringify: "npm:^2.0.6" + checksum: 10/9a17525aa8ce9ac142a94c04000dba7f0635e8e155c6c045f57eca36cc78c255318cca2fad4571719a427dfd2df64b70bc6442989523a8de555480668d666ad5 + languageName: node + linkType: hard + +"@metamask/rpc-errors@npm:^7.0.0": + version: 7.0.0 + resolution: "@metamask/rpc-errors@npm:7.0.0" dependencies: "@metamask/utils": "npm:^9.0.0" fast-safe-stringify: "npm:^2.0.6" - checksum: 10/f968fb490b13b632c2ad4770a144d67cecdff8d539cb8b489c732b08dab7a62fae65d7a2908ce8c5b77260317aa618948a52463f093fa8d9f84aee1c5f6f5daf + checksum: 10/f25e2a5506d4d0d6193c88aef8f035ec189a1177f8aee8fa01c9a33d73b1536ca7b5eea2fb33a477768bbd2abaf16529e68f0b3cf714387e5d6c9178225354fd languageName: node linkType: hard @@ -6272,9 +6282,9 @@ __metadata: languageName: node linkType: hard -"@metamask/snaps-controllers@npm:^9.7.0": - version: 9.7.0 - resolution: "@metamask/snaps-controllers@npm:9.7.0" +"@metamask/snaps-controllers@npm:^9.10.0, @metamask/snaps-controllers@npm:^9.11.1": + version: 9.11.1 + resolution: "@metamask/snaps-controllers@npm:9.11.1" dependencies: "@metamask/approval-controller": "npm:^7.0.2" "@metamask/base-controller": "npm:^6.0.2" @@ -6286,9 +6296,9 @@ __metadata: "@metamask/post-message-stream": "npm:^8.1.1" "@metamask/rpc-errors": "npm:^6.3.1" "@metamask/snaps-registry": "npm:^3.2.1" - "@metamask/snaps-rpc-methods": "npm:^11.1.1" - "@metamask/snaps-sdk": "npm:^6.5.0" - "@metamask/snaps-utils": "npm:^8.1.1" + "@metamask/snaps-rpc-methods": "npm:^11.5.0" + "@metamask/snaps-sdk": "npm:^6.9.0" + "@metamask/snaps-utils": "npm:^8.4.1" "@metamask/utils": "npm:^9.2.1" "@xstate/fsm": "npm:^2.0.0" browserify-zlib: "npm:^0.2.0" @@ -6301,30 +6311,30 @@ __metadata: readable-web-to-node-stream: "npm:^3.0.2" tar-stream: "npm:^3.1.7" peerDependencies: - "@metamask/snaps-execution-environments": ^6.7.1 + "@metamask/snaps-execution-environments": ^6.9.1 peerDependenciesMeta: "@metamask/snaps-execution-environments": optional: true - checksum: 10/8a353819e60330ef3e338a40b1115d4c830b92b1cc0c92afb2b34bf46fbc906e6da5f905654e1d486cacd40b7025ec74d3cd01cb935090035ce9f1021ce5469f + checksum: 10/e9d47b62c39cf331d26a9e35dcf5c0452aff70980db31b42b56b11165d8d1dc7e3b5ad6b495644baa0276b18a7d9681bfb059388c4f2fb1b07c6bbc8b8da799b languageName: node linkType: hard -"@metamask/snaps-execution-environments@npm:^6.7.2": - version: 6.7.2 - resolution: "@metamask/snaps-execution-environments@npm:6.7.2" +"@metamask/snaps-execution-environments@npm:^6.9.1": + version: 6.9.1 + resolution: "@metamask/snaps-execution-environments@npm:6.9.1" dependencies: "@metamask/json-rpc-engine": "npm:^9.0.2" "@metamask/object-multiplex": "npm:^2.0.0" "@metamask/post-message-stream": "npm:^8.1.1" "@metamask/providers": "npm:^17.1.2" "@metamask/rpc-errors": "npm:^6.3.1" - "@metamask/snaps-sdk": "npm:^6.5.0" - "@metamask/snaps-utils": "npm:^8.1.1" + "@metamask/snaps-sdk": "npm:^6.8.0" + "@metamask/snaps-utils": "npm:^8.4.0" "@metamask/superstruct": "npm:^3.1.0" "@metamask/utils": "npm:^9.2.1" nanoid: "npm:^3.1.31" readable-stream: "npm:^3.6.2" - checksum: 10/4b8ec4c0f6e628feeffd92fe4378fd204d2ed78012a1ed5282b24b00c78cebc3b6d7cb1306903b045a2ca887ecc0adafb2c96da4a19f2730a268f4912b36bec3 + checksum: 10/87fb63e89780ebeb9083c93988167e671ceb3d1c77980a2cd32801f83d285669859bfd248197d3a2d683119b87554f1f835965549ad04587c8c2fa2f01fa1f18 languageName: node linkType: hard @@ -6340,67 +6350,36 @@ __metadata: languageName: node linkType: hard -"@metamask/snaps-rpc-methods@npm:^11.1.1": - version: 11.1.1 - resolution: "@metamask/snaps-rpc-methods@npm:11.1.1" +"@metamask/snaps-rpc-methods@npm:^11.5.0": + version: 11.5.0 + resolution: "@metamask/snaps-rpc-methods@npm:11.5.0" dependencies: "@metamask/key-tree": "npm:^9.1.2" "@metamask/permission-controller": "npm:^11.0.0" "@metamask/rpc-errors": "npm:^6.3.1" - "@metamask/snaps-sdk": "npm:^6.5.0" - "@metamask/snaps-utils": "npm:^8.1.1" + "@metamask/snaps-sdk": "npm:^6.9.0" + "@metamask/snaps-utils": "npm:^8.4.1" "@metamask/superstruct": "npm:^3.1.0" "@metamask/utils": "npm:^9.2.1" "@noble/hashes": "npm:^1.3.1" - checksum: 10/e23279dabc6f4ffe2c6c4a7003a624cd5e79b558d7981ec12c23e54a5da25cb7be9bc7bddfa8b2ce84af28a89b42076a2c14ab004b7a976a4426bf1e1de71b5b + checksum: 10/a89b79926d5204a70369cd70e5174290805e8f9ede8057a49e347bd0e680d88de40ddfc25b3e54f53a16c3080a736ab73b50ffe50623264564af13f8709a23d3 languageName: node linkType: hard -"@metamask/snaps-sdk@npm:^6.5.1": - version: 6.5.1 - resolution: "@metamask/snaps-sdk@npm:6.5.1" +"@metamask/snaps-sdk@npm:^6.9.0": + version: 6.9.0 + resolution: "@metamask/snaps-sdk@npm:6.9.0" dependencies: "@metamask/key-tree": "npm:^9.1.2" "@metamask/providers": "npm:^17.1.2" "@metamask/rpc-errors": "npm:^6.3.1" "@metamask/superstruct": "npm:^3.1.0" "@metamask/utils": "npm:^9.2.1" - checksum: 10/7831fb2ca61a32ad43e971de9307b221f6bd2f65c84a3286f350cfdd2396166c58db6cd2fac9711654a211c8dc2049e591a79ab720b3f5ad562e434f75e95d32 - languageName: node - linkType: hard - -"@metamask/snaps-utils@npm:8.1.1": - version: 8.1.1 - resolution: "@metamask/snaps-utils@npm:8.1.1" - dependencies: - "@babel/core": "npm:^7.23.2" - "@babel/types": "npm:^7.23.0" - "@metamask/base-controller": "npm:^6.0.2" - "@metamask/key-tree": "npm:^9.1.2" - "@metamask/permission-controller": "npm:^11.0.0" - "@metamask/rpc-errors": "npm:^6.3.1" - "@metamask/slip44": "npm:^4.0.0" - "@metamask/snaps-registry": "npm:^3.2.1" - "@metamask/snaps-sdk": "npm:^6.5.0" - "@metamask/superstruct": "npm:^3.1.0" - "@metamask/utils": "npm:^9.2.1" - "@noble/hashes": "npm:^1.3.1" - "@scure/base": "npm:^1.1.1" - chalk: "npm:^4.1.2" - cron-parser: "npm:^4.5.0" - fast-deep-equal: "npm:^3.1.3" - fast-json-stable-stringify: "npm:^2.1.0" - fast-xml-parser: "npm:^4.4.1" - marked: "npm:^12.0.1" - rfdc: "npm:^1.3.0" - semver: "npm:^7.5.4" - ses: "npm:^1.1.0" - validate-npm-package-name: "npm:^5.0.0" - checksum: 10/f4ceb52a1f9578993c88c82a67f4f041309af51c83ff5caa3fed080f36b54d14ea7da807ce1cf19a13600dd0e77c51af70398e8c7bb78f0ba99a037f4d22610f + checksum: 10/ea2c34c4451f671acc6c3c0ad0d46e770e8b7d0741c1d78a30bc36b883f09a10e9a428b8b564ecd0171da95fdf78bb8ac0de261423a1b35de5d22852300a24ee languageName: node linkType: hard -"@metamask/snaps-utils@npm:^7.4.0, @metamask/snaps-utils@npm:^7.8.1": +"@metamask/snaps-utils@npm:^7.4.0": version: 7.8.1 resolution: "@metamask/snaps-utils@npm:7.8.1" dependencies: @@ -6431,9 +6410,9 @@ __metadata: languageName: node linkType: hard -"@metamask/snaps-utils@patch:@metamask/snaps-utils@npm%3A8.1.1#~/.yarn/patches/@metamask-snaps-utils-npm-8.1.1-7d5dd6a26a.patch": - version: 8.1.1 - resolution: "@metamask/snaps-utils@patch:@metamask/snaps-utils@npm%3A8.1.1#~/.yarn/patches/@metamask-snaps-utils-npm-8.1.1-7d5dd6a26a.patch::version=8.1.1&hash=d09097" +"@metamask/snaps-utils@npm:^8.1.1, @metamask/snaps-utils@npm:^8.3.0, @metamask/snaps-utils@npm:^8.4.0, @metamask/snaps-utils@npm:^8.4.1": + version: 8.4.1 + resolution: "@metamask/snaps-utils@npm:8.4.1" dependencies: "@babel/core": "npm:^7.23.2" "@babel/types": "npm:^7.23.0" @@ -6443,7 +6422,7 @@ __metadata: "@metamask/rpc-errors": "npm:^6.3.1" "@metamask/slip44": "npm:^4.0.0" "@metamask/snaps-registry": "npm:^3.2.1" - "@metamask/snaps-sdk": "npm:^6.5.0" + "@metamask/snaps-sdk": "npm:^6.9.0" "@metamask/superstruct": "npm:^3.1.0" "@metamask/utils": "npm:^9.2.1" "@noble/hashes": "npm:^1.3.1" @@ -6458,7 +6437,7 @@ __metadata: semver: "npm:^7.5.4" ses: "npm:^1.1.0" validate-npm-package-name: "npm:^5.0.0" - checksum: 10/6b1d3d70c5ebee684d5b76bf911c66ebd122a0607cefcfc9fffd4bf6882a7acfca655d97be87c0f7f47e59a981b58234578ed8a123e554a36e6c48ff87492655 + checksum: 10/c68a2fe69dc835c2b996d621fd4698435475d419a85aa557aa000aae0ab7ebb68d2a52f0b28bbab94fff895ece9a94077e3910a21b16d904cff3b9419ca575b6 languageName: node linkType: hard @@ -6499,10 +6478,10 @@ __metadata: languageName: node linkType: hard -"@metamask/test-dapp@npm:^8.4.0": - version: 8.4.0 - resolution: "@metamask/test-dapp@npm:8.4.0" - checksum: 10/9d9c4df11c2b18c72b52e8743435ed0bd18815dd7a7aed43cf3a2cce1b9ef8926909890d00b4b624446f73b88c15e95bc0190c5437b9dad437a0e345a6b430ba +"@metamask/test-dapp@npm:8.7.0": + version: 8.7.0 + resolution: "@metamask/test-dapp@npm:8.7.0" + checksum: 10/c2559179d3372e5fc8d67a60c1e4056fad9809486eaff6a2aa9c351a2a613eeecc15885a5fd9b71b8f4139058fe168abeac06bd6bdb6d4a47fe0b9b4146923ab languageName: node linkType: hard @@ -10720,12 +10699,12 @@ __metadata: languageName: node linkType: hard -"@types/node@npm:*, @types/node@npm:>=12.12.47, @types/node@npm:>=13.7.0, @types/node@npm:^20, @types/node@npm:^20.11.17": - version: 20.12.7 - resolution: "@types/node@npm:20.12.7" +"@types/node@npm:*, @types/node@npm:>=12.12.47, @types/node@npm:>=13.7.0, @types/node@npm:^20, @types/node@npm:^20.14.9": + version: 20.16.11 + resolution: "@types/node@npm:20.16.11" dependencies: - undici-types: "npm:~5.26.4" - checksum: 10/b4a28a3b593a9bdca5650880b6a9acef46911d58cf7cfa57268f048e9a7157a7c3196421b96cea576850ddb732e3b54bc982c8eb5e1e5ef0635d4424c2fce801 + undici-types: "npm:~6.19.2" + checksum: 10/6d2f92b7b320c32ba0c2bc54d21651bd21690998a2e27f00d15019d4db3e0ec30fce85332efed5e37d4cda078ff93ea86ee3e92b76b7a25a9b92a52a039b60b2 languageName: node linkType: hard @@ -18452,6 +18431,13 @@ __metadata: languageName: node linkType: hard +"eth-chainlist@npm:~0.0.498": + version: 0.0.498 + resolution: "eth-chainlist@npm:0.0.498" + checksum: 10/a414c0e1f0a877f9ab8bf1cf775556308ddbb66618e368666d4dea9a0b949febedf8ca5440cf57419413404e7661f1e3d040802faf532d0e1618c40ecd334cbf + languageName: node + linkType: hard + "eth-eip712-util-browser@npm:^0.0.3": version: 0.0.3 resolution: "eth-eip712-util-browser@npm:0.0.3" @@ -26110,15 +26096,15 @@ __metadata: "@lgbot/madge": "npm:^6.2.0" "@lydell/node-pty": "npm:^1.0.1" "@material-ui/core": "npm:^4.11.0" - "@metamask-institutional/custody-controller": "npm:^0.2.31" - "@metamask-institutional/custody-keyring": "npm:^2.0.3" - "@metamask-institutional/extension": "npm:^0.3.27" - "@metamask-institutional/institutional-features": "npm:^1.3.5" + "@metamask-institutional/custody-controller": "npm:^0.3.0" + "@metamask-institutional/custody-keyring": "npm:^2.1.0" + "@metamask-institutional/extension": "npm:^0.3.28" + "@metamask-institutional/institutional-features": "npm:^1.3.6" "@metamask-institutional/portfolio-dashboard": "npm:^1.4.1" "@metamask-institutional/rpc-allowlist": "npm:^1.0.3" - "@metamask-institutional/sdk": "npm:^0.1.30" - "@metamask-institutional/transaction-update": "npm:^0.2.5" - "@metamask-institutional/types": "npm:^1.1.0" + "@metamask-institutional/sdk": "npm:^0.2.0" + "@metamask-institutional/transaction-update": "npm:^0.2.6" + "@metamask-institutional/types": "npm:^1.2.0" "@metamask/abi-utils": "npm:^2.0.2" "@metamask/account-watcher": "npm:^4.1.1" "@metamask/accounts-controller": "npm:^18.2.2" @@ -26129,7 +26115,7 @@ __metadata: "@metamask/assets-controllers": "patch:@metamask/assets-controllers@npm%3A38.3.0#~/.yarn/patches/@metamask-assets-controllers-npm-38.3.0-57b3d695bb.patch" "@metamask/auto-changelog": "npm:^2.1.0" "@metamask/base-controller": "npm:^7.0.0" - "@metamask/bitcoin-wallet-snap": "npm:^0.6.1" + "@metamask/bitcoin-wallet-snap": "npm:^0.7.0" "@metamask/browser-passworder": "npm:^4.3.0" "@metamask/build-utils": "npm:^3.0.0" "@metamask/contract-metadata": "npm:^2.5.0" @@ -26148,7 +26134,7 @@ __metadata: "@metamask/eth-ledger-bridge-keyring": "npm:^3.0.1" "@metamask/eth-query": "npm:^4.0.0" "@metamask/eth-sig-util": "npm:^7.0.1" - "@metamask/eth-snap-keyring": "npm:^4.3.6" + "@metamask/eth-snap-keyring": "npm:^4.4.0" "@metamask/eth-token-tracker": "npm:^8.0.0" "@metamask/eth-trezor-keyring": "npm:^3.1.3" "@metamask/etherscan-link": "npm:^3.0.0" @@ -26178,24 +26164,24 @@ __metadata: "@metamask/post-message-stream": "npm:^8.0.0" "@metamask/ppom-validator": "npm:0.34.0" "@metamask/preferences-controller": "npm:^13.0.2" - "@metamask/preinstalled-example-snap": "npm:^0.1.0" + "@metamask/preinstalled-example-snap": "npm:^0.2.0" "@metamask/profile-sync-controller": "npm:^0.9.7" "@metamask/providers": "npm:^14.0.2" "@metamask/queued-request-controller": "npm:^2.0.0" "@metamask/rate-limit-controller": "npm:^6.0.0" - "@metamask/rpc-errors": "npm:^6.2.1" + "@metamask/rpc-errors": "npm:^7.0.0" "@metamask/safe-event-emitter": "npm:^3.1.1" "@metamask/scure-bip39": "npm:^2.0.3" "@metamask/selected-network-controller": "npm:^18.0.1" "@metamask/signature-controller": "npm:^19.1.0" "@metamask/smart-transactions-controller": "npm:^13.0.0" - "@metamask/snaps-controllers": "npm:^9.7.0" - "@metamask/snaps-execution-environments": "npm:^6.7.2" - "@metamask/snaps-rpc-methods": "npm:^11.1.1" - "@metamask/snaps-sdk": "npm:^6.5.1" - "@metamask/snaps-utils": "patch:@metamask/snaps-utils@npm%3A8.1.1#~/.yarn/patches/@metamask-snaps-utils-npm-8.1.1-7d5dd6a26a.patch" + "@metamask/snaps-controllers": "npm:^9.11.1" + "@metamask/snaps-execution-environments": "npm:^6.9.1" + "@metamask/snaps-rpc-methods": "npm:^11.5.0" + "@metamask/snaps-sdk": "npm:^6.9.0" + "@metamask/snaps-utils": "npm:^8.4.1" "@metamask/test-bundler": "npm:^1.0.0" - "@metamask/test-dapp": "npm:^8.4.0" + "@metamask/test-dapp": "npm:8.7.0" "@metamask/transaction-controller": "npm:^37.2.0" "@metamask/user-operation-controller": "npm:^13.0.0" "@metamask/utils": "npm:^9.3.0" @@ -26334,10 +26320,10 @@ __metadata: eslint-plugin-react-hooks: "npm:^4.2.0" eslint-plugin-storybook: "npm:^0.6.15" eta: "npm:^3.2.0" + eth-chainlist: "npm:~0.0.498" eth-ens-namehash: "npm:^2.0.8" eth-lattice-keyring: "npm:^0.12.4" eth-method-registry: "npm:^4.0.0" - eth-rpc-errors: "npm:^4.0.2" ethereumjs-util: "npm:^7.0.10" ethers: "npm:5.7.0" extension-port-stream: "npm:^3.0.0" @@ -35502,6 +35488,13 @@ __metadata: languageName: node linkType: hard +"undici-types@npm:~6.19.2": + version: 6.19.8 + resolution: "undici-types@npm:6.19.8" + checksum: 10/cf0b48ed4fc99baf56584afa91aaffa5010c268b8842f62e02f752df209e3dea138b372a60a963b3b2576ed932f32329ce7ddb9cb5f27a6c83040d8cd74b7a70 + languageName: node + linkType: hard + "undici@npm:5.28.4": version: 5.28.4 resolution: "undici@npm:5.28.4"