Merge remote-tracking branch 'origin/develop' into master-sync

* origin/develop: (100 commits)
  Switch Flask horizontal logos (#13113)
  Update `@babel/runtime` patch to fix lockdown error (#13109)
  Use promisified background in setUseNonceField (#13107)
  Fix account name duplicates (#12867)
  Choose accounts refactor (#13039)
  Fix permissions-connect-footer "learn more" link (#13092)
  Feat/collectibles the return (#12970)
  Subject metadata cleanup (#13090)
  Fix merge conflict typo
  Bump just-safe-set from 2.1.0 to 2.2.3 (#13049)
  Fix typo in German translation (#13040)
  Using EIP-1559 V2 for swaps (#12966)
  Make restore vault a form so an user can submit via keyboard (#12989)
  Remove legacy node parent detection (#12814)
  Add stories for Home notification component (#13035)
  Update Redux DevTools README instructions (#13038)
  Jestify app/scripts/controller/network/**/*.test.js (#12985)
  Fix order of account list (#12999)
  Changes in gas loading animation in EIP-1559 V2 (#13016)
  Add crowdin configuration and github action (#12552)
  ...
feature/default_network_editable
Mark Stacey 3 years ago
commit 0c18366a87
  1. 4
      .circleci/config.yml
  2. 31
      .eslintrc.js
  3. 31
      .github/workflows/crowdin_action.yml
  4. 4
      .metamaskrc.dist
  5. 10
      .mocharc.js
  6. 5
      .mocharc.lax.js
  7. BIN
      .storybook/images/catnip-spicywright.png
  8. 8
      .storybook/initial-states/approval-screens/token-approval.js
  9. 6
      .storybook/manager.js
  10. 14
      .storybook/metamask-storybook-theme.js
  11. 27
      .storybook/preview-head.html
  12. 11
      .storybook/preview.js
  13. 37
      .storybook/test-data.js
  14. 22
      README.md
  15. 2
      app/_locales/de/messages.json
  16. 150
      app/_locales/en/messages.json
  17. 6
      app/_locales/es/messages.json
  18. 6
      app/_locales/es_419/messages.json
  19. 6
      app/_locales/hi/messages.json
  20. 6
      app/_locales/id/messages.json
  21. 6
      app/_locales/it/messages.json
  22. 6
      app/_locales/ja/messages.json
  23. 6
      app/_locales/ko/messages.json
  24. 6
      app/_locales/ph/messages.json
  25. 6
      app/_locales/pt_BR/messages.json
  26. 6
      app/_locales/ru/messages.json
  27. 6
      app/_locales/tl/messages.json
  28. 6
      app/_locales/vi/messages.json
  29. 6
      app/_locales/zh_CN/messages.json
  30. 16
      app/build-types/flask/images/logo/metamask-logo-horizontal-dark.svg
  31. 16
      app/build-types/flask/images/logo/metamask-logo-horizontal.svg
  32. 1
      app/images/curve-high.svg
  33. 1
      app/images/curve-low.svg
  34. 1
      app/images/curve-medium.svg
  35. 11
      app/scripts/background.js
  36. 12
      app/scripts/controllers/app-state.js
  37. 2
      app/scripts/controllers/network/createJsonRpcClient.js
  38. 89
      app/scripts/controllers/network/network-controller.test.js
  39. 2
      app/scripts/controllers/network/network.js
  40. 56
      app/scripts/controllers/network/pending-middleware.test.js
  41. 13
      app/scripts/controllers/network/util.test.js
  42. 71
      app/scripts/controllers/permissions/background-api.js
  43. 181
      app/scripts/controllers/permissions/background-api.test.js
  44. 39
      app/scripts/controllers/permissions/caveat-mutators.js
  45. 32
      app/scripts/controllers/permissions/caveat-mutators.test.js
  46. 78
      app/scripts/controllers/permissions/enums.js
  47. 724
      app/scripts/controllers/permissions/index.js
  48. 39
      app/scripts/controllers/permissions/permission-log.js
  49. 353
      app/scripts/controllers/permissions/permission-log.test.js
  50. 1562
      app/scripts/controllers/permissions/permissions-controller.test.js
  51. 950
      app/scripts/controllers/permissions/permissions-middleware.test.js
  52. 112
      app/scripts/controllers/permissions/permissionsMethodMiddleware.js
  53. 174
      app/scripts/controllers/permissions/restricted-methods.test.js
  54. 40
      app/scripts/controllers/permissions/restrictedMethods.js
  55. 84
      app/scripts/controllers/permissions/selectors.js
  56. 116
      app/scripts/controllers/permissions/selectors.test.js
  57. 258
      app/scripts/controllers/permissions/specifications.js
  58. 340
      app/scripts/controllers/permissions/specifications.test.js
  59. 7
      app/scripts/controllers/preferences.js
  60. 25
      app/scripts/controllers/preferences.test.js
  61. 14
      app/scripts/controllers/transactions/index.js
  62. 1
      app/scripts/controllers/transactions/lib/tx-state-history-helpers.js
  63. 8
      app/scripts/controllers/transactions/lib/tx-state-history-helpers.test.js
  64. 15
      app/scripts/controllers/transactions/pending-tx-tracker.js
  65. 8
      app/scripts/controllers/transactions/tx-state-manager.test.js
  66. 54
      app/scripts/lib/ComposableObservableStore.test.js
  67. 2
      app/scripts/lib/buy-eth-url.js
  68. 30
      app/scripts/lib/buy-eth-url.test.js
  69. 26
      app/scripts/lib/cleanErrorStack.test.js
  70. 21
      app/scripts/lib/createMetaRPCHandler.js
  71. 74
      app/scripts/lib/createMetaRPCHandler.test.js
  72. 1
      app/scripts/lib/ens-ipfs/setup.js
  73. 71
      app/scripts/lib/message-manager.test.js
  74. 42
      app/scripts/lib/metaRPCClientFactory.test.js
  75. 43
      app/scripts/lib/migrator/index.test.js
  76. 38
      app/scripts/lib/nodeify.js
  77. 74
      app/scripts/lib/nodeify.test.js
  78. 88
      app/scripts/lib/personal-message-manager.test.js
  79. 57
      app/scripts/lib/rpc-method-middleware/createMethodMiddleware.js
  80. 33
      app/scripts/lib/rpc-method-middleware/handlers/eth-accounts.js
  81. 10
      app/scripts/lib/rpc-method-middleware/handlers/index.js
  82. 108
      app/scripts/lib/rpc-method-middleware/handlers/request-accounts.js
  83. 58
      app/scripts/lib/rpc-method-middleware/handlers/send-metadata.js
  84. 73
      app/scripts/lib/seed-phrase-verifier.test.js
  85. 8
      app/scripts/lib/segment.js
  86. 44
      app/scripts/lib/typed-message-manager.test.js
  87. 169
      app/scripts/lib/util.test.js
  88. 973
      app/scripts/metamask-controller.js
  89. 73
      app/scripts/metamask-controller.test.js
  90. 10
      app/scripts/migrations/048.test.js
  91. 161
      app/scripts/migrations/068.js
  92. 450
      app/scripts/migrations/068.test.js
  93. 41
      app/scripts/migrations/069.js
  94. 102
      app/scripts/migrations/069.test.js
  95. 4
      app/scripts/migrations/index.js
  96. 13
      crowdin.yml
  97. 1
      development/build/index.js
  98. 13
      development/build/manifest.js
  99. 8
      development/build/scripts.js
  100. 11
      development/build/transforms/remove-fenced-code.js
  101. Some files were not shown because too many files have changed in this diff Show More

@ -660,8 +660,8 @@ jobs:
- attach_workspace:
at: .
- run:
name: test:coverage
command: yarn test:coverage
name: test:coverage:mocha
command: yarn test:coverage:mocha
- run:
name: test:coverage:jest
command: yarn test:coverage:jest

@ -22,6 +22,7 @@ module.exports = {
ignorePatterns: [
'!.eslintrc.js',
'!.mocharc.js',
'node_modules/**',
'dist/**',
'builds/**',
@ -86,6 +87,27 @@ module.exports = {
'node/no-process-env': 'off',
// TODO: remove this override
'padding-line-between-statements': [
'error',
{
blankLine: 'always',
prev: 'directive',
next: '*',
},
{
blankLine: 'any',
prev: 'directive',
next: 'directive',
},
// Disabled temporarily to reduce conflicts while PR queue is large
// {
// blankLine: 'always',
// prev: ['multiline-block-like', 'multiline-expression'],
// next: ['multiline-block-like', 'multiline-expression'],
// },
],
// TODO: re-enable these rules
'node/no-sync': 'off',
'node/no-unpublished-import': 'off',
@ -136,8 +158,11 @@ module.exports = {
'ui/__mocks__/*.js',
'shared/**/*.test.js',
'development/**/*.test.js',
'app/scripts/lib/**/*.test.js',
'app/scripts/migrations/*.test.js',
'app/scripts/platforms/*.test.js',
'app/scripts/controllers/network/**/*.test.js',
'app/scripts/controllers/permissions/*.test.js',
],
extends: ['@metamask/eslint-config-mocha'],
rules: {
@ -160,8 +185,11 @@ module.exports = {
'ui/__mocks__/*.js',
'shared/**/*.test.js',
'development/**/*.test.js',
'app/scripts/lib/**/*.test.js',
'app/scripts/migrations/*.test.js',
'app/scripts/platforms/*.test.js',
'app/scripts/controllers/network/**/*.test.js',
'app/scripts/controllers/permissions/*.test.js',
],
extends: ['@metamask/eslint-config-jest'],
rules: {
@ -184,7 +212,9 @@ module.exports = {
{
files: [
'.eslintrc.js',
'.mocharc.js',
'babel.config.js',
'jest.config.js',
'nyc.config.js',
'stylelint.config.js',
'app/scripts/lockdown-run.js',
@ -195,7 +225,6 @@ module.exports = {
'test/setup.js',
'test/helpers/protect-intrinsics-helpers.js',
'test/lib/wait-until-called.js',
'jest.config.js',
],
parserOptions: {
sourceType: 'script',

@ -0,0 +1,31 @@
name: Crowdin Action
permissions:
contents: write
pull-requests: write
on:
push:
branches:
- develop
schedule:
- cron: "0 */12 * * *"
jobs:
synchronize-with-crowdin:
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@v2
- name: crowdin action
uses: crowdin/github-action@d0622816ed4f4744db27d04374b2cef6867f7bed
with:
upload_translations: true
download_translations: true
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
CROWDIN_PROJECT_ID: ${{ secrets.CROWDIN_PROJECT_ID }}
CROWDIN_PERSONAL_TOKEN: ${{ secrets.CROWDIN_PERSONAL_TOKEN }}

@ -2,3 +2,7 @@
PASSWORD=METAMASK PASSWORD
INFURA_PROJECT_ID=00000000000
SEGMENT_WRITE_KEY=
ONBOARDING_V2=
EIP_1559_V2=
SWAPS_USE_DEV_APIS=
COLLECTIBLES_V1=

@ -1,7 +1,13 @@
module.exports = {
// TODO: Remove the `exit` setting, it can hide broken tests.
exit: true,
ignore: ['./app/scripts/migrations/*.test.js', './app/scripts/platforms/*.test.js'],
ignore: [
'./app/scripts/lib/**/*.test.js',
'./app/scripts/migrations/*.test.js',
'./app/scripts/platforms/*.test.js',
'./app/scripts/controllers/network/**/*.test.js',
'./app/scripts/controllers/permissions/*.test.js',
],
recursive: true,
require: ['test/env.js', 'test/setup.js'],
}
};

@ -1,5 +0,0 @@
const baseConfig = require('./.mocharc');
module.exports = Object.assign({}, baseConfig, {
ignore: [...baseConfig.ignore, './app/scripts/controllers/permissions/*.test.js']
});

Binary file not shown.

After

Width:  |  Height:  |  Size: 59 KiB

@ -46,11 +46,11 @@ export const currentNetworkTxListSample = {
]
}
export const domainMetadata = {
export const subjectMetadata = {
"https://metamask.github.io": {
"origin": "https://metamask.github.io",
"name": "E2E Test Dapp",
"icon": "https://metamask.github.io/test-dapp/metamask-fox.svg",
"lastUpdated": 1620723443380,
"host": "metamask.github.io"
"iconUrl": "https://metamask.github.io/test-dapp/metamask-fox.svg",
"subjectType": "website"
}
}

@ -0,0 +1,6 @@
import { addons } from '@storybook/addons';
import MetaMaskStorybookTheme from './metamask-storybook-theme';
addons.setConfig({
theme: MetaMaskStorybookTheme,
});

@ -0,0 +1,14 @@
// .storybook/YourTheme.js
import { create } from '@storybook/theming';
import logo from '../app/images/logo/metamask-logo-horizontal.svg';
export default create({
base: 'light',
brandTitle: 'MetaMask Storybook',
brandImage: logo,
// Typography
fontBase: 'Euclid, Roboto, Helvetica, Arial, sans-serif',
fontCode: 'Inconsolata, monospace',
});

@ -0,0 +1,27 @@
<link rel="preconnect" href="https://fonts.googleapis.com" />
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin />
<link
href="https://fonts.googleapis.com/css2?family=Inconsolata&display=swap"
rel="stylesheet"
/>
<style>
* {
--gray-pre-bg: #f8f8f8;
--font-family-monospace: Inconsolata, monospace;
--font-size-code: 0.875rem;
}
.docblock-source {
background: var(--gray-pre-bg) !important;
}
.docblock-source code {
font-family: var(--font-family-monospace) !important;
font-size: var(--font-size-code) !important;
}
.docblock-source code * {
font-family: var(--font-family-monospace) !important;
font-size: var(--font-size-code) !important;
}
</style>

@ -1,7 +1,6 @@
import React, { useEffect } from 'react';
import { addDecorator, addParameters } from '@storybook/react';
import { action } from '@storybook/addon-actions';
import { withKnobs } from '@storybook/addon-knobs';
import { Provider } from 'react-redux';
import configureStore from '../ui/store/store';
import '../ui/css/index.scss';
@ -13,6 +12,7 @@ import testData from './test-data.js';
import { Router } from 'react-router-dom';
import { createBrowserHistory } from 'history';
import { _setBackgroundConnection } from '../ui/store/actions';
import MetaMaskStorybookTheme from './metamask-storybook-theme';
addParameters({
backgrounds: {
@ -22,6 +22,14 @@ addParameters({
{ name: 'dark', value: '#333333' },
],
},
docs: {
theme: MetaMaskStorybookTheme,
},
options: {
storySort: {
order: ['Getting Started', 'Components', ['UI', 'App'], 'Pages'],
},
},
});
export const globalTypes = {
@ -77,5 +85,4 @@ const metamaskDecorator = (story, context) => {
);
};
addDecorator(withKnobs);
addDecorator(metamaskDecorator);

@ -1013,34 +1013,25 @@ const state = {
goerli: null,
mainnet: 10902989,
},
permissionsRequests: [],
permissionsDescriptions: {},
domains: {
subjects: {
'https://app.uniswap.org': {
permissions: [
{
'@context': ['https://github.com/MetaMask/rpc-cap'],
permissions: {
'eth_accounts': {
invoker: 'https://app.uniswap.org',
parentCapability: 'eth_accounts',
id: 'a7342e4b-beae-4525-a36c-c0635fd03359',
date: 1620710693178,
caveats: [
{
type: 'limitResponseLength',
value: 1,
name: 'primaryAccountOnly',
},
{
type: 'filterResponse',
type: 'restrictReturnedAccounts',
value: ['0x64a845a5b02460acf8a3d84503b0d68d028b4bb4'],
name: 'exposedAccounts',
},
],
},
],
},
},
permissionsLog: [
},
permissionActivityLog: [
{
id: 522690215,
method: 'eth_accounts',
@ -1171,7 +1162,7 @@ const state = {
success: true,
},
],
permissionsHistory: {
permissionHistory: {
'https://metamask.github.io': {
eth_accounts: {
lastApproved: 1620710693213,
@ -1181,18 +1172,18 @@ const state = {
},
},
},
domainMetadata: {
subjectMetadata: {
'https://metamask.github.io': {
name: 'E2E Test Dapp',
icon: 'https://metamask.github.io/test-dapp/metamask-fox.svg',
lastUpdated: 1620723443380,
host: 'metamask.github.io',
origin: 'https://metamask.github.io',
iconUrl: 'https://metamask.github.io/test-dapp/metamask-fox.svg',
subjectType: 'website',
},
'https://app.uniswap.org': {
name: 'Uniswap',
icon: './UNI.png',
lastUpdated: 1620723443380,
host: 'app.uniswap.org',
origin: 'https://app.uniswap.org',
iconUrl: './UNI.png',
subjectType: 'website',
},
},
threeBoxSyncingAllowed: false,

@ -33,13 +33,22 @@ See the [build system readme](./development/build/README.md) for build system us
To start a development build (e.g. with logging and file watching) run `yarn start`.
To start the [React DevTools](https://github.com/facebook/react-devtools) and [Redux DevTools Extension](https://github.com/reduxjs/redux-devtools/tree/main/extension)
alongside the app, use `yarn start:dev`.
- React DevTools will open in a separate window; no browser extension is required
- Redux DevTools will need to be installed as a browser extension. Open the Redux Remote Devtools to access Redux state logs. This can be done by either right clicking within the web browser to bring up the context menu, expanding the Redux DevTools panel and clicking Open Remote DevTools OR clicking the Redux DevTools extension icon and clicking Open Remote DevTools.
- You will also need to check the "Use custom (local) server" checkbox in the Remote DevTools Settings, using the default server configuration (host `localhost`, port `8000`, secure connection checkbox unchecked)
#### React and Redux DevTools
[Test site](https://metamask.github.io/test-dapp/) can be used to execute different user flows.
To start the [React DevTools](https://github.com/facebook/react-devtools), run `yarn devtools:react` with a development build installed in a browser. This will open in a separate window; no browser extension is required.
To start the [Redux DevTools Extension](https://github.com/reduxjs/redux-devtools/tree/main/extension):
- Install the package `remotedev-server` globally (e.g. `yarn global add remotedev-server`)
- Install the Redux Devtools extension.
- Open the Redux DevTools extension and check the "Use custom (local) server" checkbox in the Remote DevTools Settings, using the default server configuration (host `localhost`, port `8000`, secure connection checkbox unchecked).
Then run the command `yarn devtools:redux` with a development build installed in a browser. This will enable you to use the Redux DevTools extension to inspect MetaMask.
To create a development build and run both of these tools simultaneously, run `yarn start:dev`.
#### Test Dapp
[This test site](https://metamask.github.io/test-dapp/) can be used to execute different user flows.
### Running Unit Tests and Linting
@ -61,6 +70,7 @@ Whenever you change dependencies (adding, removing, or updating, either in `pack
* `yarn.lock`:
* Run `yarn setup` again after your changes to ensure `yarn.lock` has been properly updated.
* Run `yarn yarn-deduplicate` to remove duplicate dependencies from the lockfile.
* The `allow-scripts` configuration in `package.json`
* Run `yarn allow-scripts auto` to update the `allow-scripts` configuration automatically. This config determines whether the package's install/postinstall scripts are allowed to run. Review each new package to determine whether the install script needs to run or not, testing if necessary.
* Unfortunately, `yarn allow-scripts auto` will behave inconsistently on different platforms. macOS and Windows users may see extraneous changes relating to optional dependencies.

@ -736,7 +736,7 @@
"message": "Zurücksetzen"
},
"resetAccount": {
"message": "Account zurücksetzten"
"message": "Account zurücksetzen"
},
"resetAccountDescription": {
"message": "Durch das Zurücksetzen Ihres Kontos wird Ihr Transaktionsverlauf gelöscht."

@ -73,6 +73,10 @@
"accountName": {
"message": "Account Name"
},
"accountNameDuplicate": {
"message": "This account name already exists",
"description": "This is an error message shown when the user enters a new account name that matches an existing account name"
},
"accountOptions": {
"message": "Account Options"
},
@ -139,9 +143,6 @@
"addNFT": {
"message": "Add NFT"
},
"addNFTLowerCase": {
"message": "add NFT"
},
"addNetwork": {
"message": "Add Network"
},
@ -167,7 +168,7 @@
"message": "Advanced"
},
"advancedBaseGasFeeToolTip": {
"message": "Any difference between your max base fee and the current base fee will be refunded after completion."
"message": "When your transaction gets included in the block, any difference between your max base fee and the actual base fee will be refunded. Total amount is calculated as max base fee (in GWEI) * gas limit."
},
"advancedGasFeeModalTitle": {
"message": "Advanced gas fee"
@ -320,6 +321,9 @@
"balanceOutdated": {
"message": "Balance may be outdated"
},
"baseFee": {
"message": "Base fee"
},
"basic": {
"message": "Basic"
},
@ -565,6 +569,9 @@
"contract": {
"message": "Contract"
},
"contractAddress": {
"message": "Contract address"
},
"contractAddressError": {
"message": "You are sending tokens to the token's contract address. This may result in the loss of these tokens."
},
@ -637,6 +644,10 @@
"customGas": {
"message": "Customize Gas"
},
"customGasSettingToolTipMessage": {
"message": "Use $1 to customise the gas price. This can be confusing if you aren’t familiar. Interact at your own risk.",
"description": "$1 is key 'advanced' (text: 'Advanced') separated here so that it can be passed in with bold fontweight"
},
"customGasSubTitle": {
"message": "Increasing fee may decrease processing times, but it is not guaranteed."
},
@ -649,6 +660,10 @@
"dappSuggested": {
"message": "Site suggested"
},
"dappSuggestedGasSettingToolTipMessage": {
"message": "$1 has suggested this price.",
"description": "$1 is url for the dapp that has suggested gas settings"
},
"dappSuggestedShortLabel": {
"message": "Site"
},
@ -706,6 +721,9 @@
"depositEther": {
"message": "Deposit Ether"
},
"description": {
"message": "Description"
},
"details": {
"message": "Details"
},
@ -799,12 +817,28 @@
"editGasLimitOutOfBounds": {
"message": "Gas limit must be at least $1"
},
"editGasLimitOutOfBoundsV2": {
"message": "Gas limit must be greater than $1 and less than $2",
"description": "$1 is the minimum limit for gas and $2 is the maximum limit"
},
"editGasLimitTooltip": {
"message": "Gas limit is the maximum units of gas you are willing to use. Units of gas are a multiplier to “Max priority fee” and “Max fee”."
},
"editGasLow": {
"message": "Low"
},
"editGasMaxBaseFeeGWEIImbalance": {
"message": "Max base fee cannot be lower than priority fee"
},
"editGasMaxBaseFeeHigh": {
"message": "Max base fee is higher than necessary"
},
"editGasMaxBaseFeeLow": {
"message": "Max base fee is low for current network conditions"
},
"editGasMaxBaseFeeMultiplierImbalance": {
"message": "Multiplier is low relative to Priority fee"
},
"editGasMaxFeeHigh": {
"message": "Max fee is higher than necessary"
},
@ -820,12 +854,21 @@
"editGasMaxPriorityFeeBelowMinimum": {
"message": "Max priority fee must be greater than 0 GWEI"
},
"editGasMaxPriorityFeeBelowMinimumV2": {
"message": "Priority fee must be greater than 0."
},
"editGasMaxPriorityFeeHigh": {
"message": "Max priority fee is higher than necessary. You may pay more than needed."
},
"editGasMaxPriorityFeeHighV2": {
"message": "Priority fee is higher than necessary. You may pay more than needed"
},
"editGasMaxPriorityFeeLow": {
"message": "Max priority fee is low for current network conditions"
},
"editGasMaxPriorityFeeLowV2": {
"message": "Priority fee is low for current network conditions"
},
"editGasMaxPriorityFeeTooltip": {
"message": "Max priority fee (aka “miner tip”) goes directly to miners and incentivizes them to prioritize your transaction. You’ll most often pay your max setting"
},
@ -872,6 +915,9 @@
"editPermission": {
"message": "Edit Permission"
},
"enableAutoDetect": {
"message": " Enable Autodetect"
},
"enableFromSettings": {
"message": " Enable it from Settings."
},
@ -993,7 +1039,7 @@
"message": "Backup gas price is provided as the main gas estimation service is unavailable right now."
},
"eth_accounts": {
"message": "View the addresses of your permitted accounts (required)",
"message": "See address, account balance, activity and initiate transactions",
"description": "The description for the `eth_accounts` permission"
},
"ethereumPublicAddress": {
@ -1071,6 +1117,18 @@
"flaskExperimentalText5": {
"message": "Using Flask gives you much greater discretion in using the power of MetaMask, and that discretion is yours. Do you accept these risks as well as extra responsibility for your wallet's safety?"
},
"flaskSnapSettingsCardButtonCta": {
"message": "See details",
"description": "Call to action a user can take to see more information about the Snap that is installed"
},
"flaskSnapSettingsCardDateAddedOn": {
"message": "Added on",
"description": "Start of the sentence describing when and where snap was added"
},
"flaskSnapSettingsCardFrom": {
"message": "from",
"description": "Part of the sentence describing when and where snap was added"
},
"followUsOnTwitter": {
"message": "Follow us on Twitter"
},
@ -1116,6 +1174,9 @@
"message": "Gas limit must be at least $1",
"description": "$1 is the custom gas limit, in decimal."
},
"gasLimitV2": {
"message": "Gas limit"
},
"gasOption": {
"message": "Gas option"
},
@ -1250,6 +1311,16 @@
"high": {
"message": "Aggressive"
},
"highGasSettingToolTipDialog": {
"message": "High probability, even in volatile markets"
},
"highGasSettingToolTipMessage": {
"message": "Use $1 to cover surges in network traffic due to things like popular NFT drops.",
"description": "$1 is key 'high' (text: 'Aggressive') separated here so that it can be passed in with bold fontweight"
},
"highLowercase": {
"message": "high"
},
"history": {
"message": "History"
},
@ -1289,6 +1360,9 @@
"importMyWallet": {
"message": "Import My Wallet"
},
"importNFTs": {
"message": "Import NFTs"
},
"importTokenQuestion": {
"message": "Import token?"
},
@ -1424,6 +1498,9 @@
"learnMore": {
"message": "learn more"
},
"learnMoreUpperCase": {
"message": "Learn more"
},
"learnScamRisk": {
"message": "scams and security risks."
},
@ -1481,6 +1558,9 @@
"likeToImportTokens": {
"message": "Would you like to import these tokens?"
},
"link": {
"message": "Link"
},
"links": {
"message": "Links"
},
@ -1505,6 +1585,13 @@
"low": {
"message": "Low"
},
"lowGasSettingToolTipMessage": {
"message": "Use $1 to wait for a cheaper price. Time estimates are much less accurate as prices are somewhat unpredicible.",
"description": "$1 is key 'low' separated here so that it can be passed in with bold fontweight"
},
"lowLowercase": {
"message": "low"
},
"lowPriorityMessage": {
"message": "Future transactions will queue after this one. This price was last seen was some time ago."
},
@ -1533,6 +1620,10 @@
"medium": {
"message": "Market"
},
"mediumGasSettingToolTipMessage": {
"message": "Use $1 for fast processing at current market price.",
"description": "$1 is key 'medium' (text: 'Market') separated here so that it can be passed in with bold fontweight"
},
"memo": {
"message": "memo"
},
@ -1694,6 +1785,17 @@
"networkStatus": {
"message": "Network status"
},
"networkStatusBaseFeeTooltip": {
"message": "The base fee is set by the network and changes every 13-14 seconds. Our $1 and $2 options account for sudden increases.",
"description": "$1 and $2 are bold text for Medium and Aggressive respectively."
},
"networkStatusPriorityFeeTooltip": {
"message": "Range of priority fees (aka “miner tip”). This goes to miners and incentivizes them to prioritize your transaction."
},
"networkStatusStabilityFeeTooltip": {
"message": "Gas fees are $1 relative to the past 72 hours.",
"description": "$1 is networks stability value - stable, low, high"
},
"networkURL": {
"message": "Network URL"
},
@ -1729,10 +1831,10 @@
"message": "New Contract"
},
"newNFTsDetected": {
"message": "New NFTs detected"
"message": "New! NFT detection"
},
"newNFTsDetectedInfo": {
"message": "One or more new NFTs were detected in your wallet."
"message": "Allow MetaMask to automatically detect NFTs from Opensea and display in your MetaMask wallet."
},
"newNetworkAdded": {
"message": "“$1” was successfully added!"
@ -1774,6 +1876,9 @@
"noAlreadyHaveSeed": {
"message": "No, I already have a Secret Recovery Phrase"
},
"noConversionDateAvailable": {
"message": "No Currency Conversion Date Available"
},
"noConversionRateAvailable": {
"message": "No Conversion Rate Available"
},
@ -2034,15 +2139,9 @@
"message": "You have (1) pending transaction.",
"description": "$1 is count of pending transactions"
},
"permissionCheckedIconDescription": {
"message": "You have approved this permission"
},
"permissionRequest": {
"message": "Permission request"
},
"permissionUncheckedIconDescription": {
"message": "You have not approved this permission"
},
"permissions": {
"message": "Permissions"
},
@ -2067,6 +2166,9 @@
"message": "Select native to prioritize displaying values in the native currency of the chain (e.g. ETH). Select Fiat to prioritize displaying values in your selected fiat currency."
},
"priorityFee": {
"message": "Priority fee"
},
"priorityFeeProperCase": {
"message": "Priority Fee"
},
"privacyMsg": {
@ -2172,6 +2274,9 @@
"removeAccountDescription": {
"message": "This account will be removed from your wallet. Please make sure you have the original Secret Recovery Phrase or private key for this imported account before continuing. You can import or create accounts again from the account drop-down. "
},
"removeNFT": {
"message": "Remove NFT"
},
"requestsAwaitingAcknowledgement": {
"message": "requests waiting to be acknowledged"
},
@ -2342,7 +2447,7 @@
"message": "Select a higher gas fee to accelerate the processing of your transaction.*"
},
"selectAccounts": {
"message": "Select account(s)"
"message": "Select the account(s) to use on this site"
},
"selectAll": {
"message": "Select all"
@ -2360,7 +2465,7 @@
"message": "Select HD Path"
},
"selectNFTPrivacyPreference": {
"message": "Select NFT privacy preference"
"message": "Turn on NFT detection in Settings"
},
"selectPathHelp": {
"message": "If you don't see the accounts you expect, try switching the HD path."
@ -2490,6 +2595,9 @@
"somethingWentWrong": {
"message": "Oops! Something went wrong."
},
"source": {
"message": "Source"
},
"speedUp": {
"message": "Speed Up"
},
@ -2530,6 +2638,9 @@
"stable": {
"message": "Stable"
},
"stableLowercase": {
"message": "stable"
},
"stateLogError": {
"message": "Error in retrieving state logs."
},
@ -2813,6 +2924,12 @@
"swapSourceInfo": {
"message": "We search multiple liquidity sources (exchanges, aggregators and professional market makers) to find the best rates and lowest network fees."
},
"swapSuggested": {
"message": "Swap suggested"
},
"swapSuggestedGasSettingToolTipMessage": {
"message": "Swaps are complex and time sensitive transactions. We recommend this gas fee for a good balance between cost and confidence of a successful Swap."
},
"swapSwapFrom": {
"message": "Swap from"
},
@ -3267,6 +3384,9 @@
"message": "View $1 on Etherscan",
"description": "$1 is the action type. e.g (Account, Transaction, Swap)"
},
"viewOnOpensea": {
"message": "View on Opensea"
},
"viewinExplorer": {
"message": "View $1 in Explorer",
"description": "$1 is the action type. e.g (Account, Transaction, Swap)"

@ -1343,12 +1343,6 @@
"pending": {
"message": "Pendiente"
},
"permissionCheckedIconDescription": {
"message": "Aprobó este permiso"
},
"permissionUncheckedIconDescription": {
"message": "No aprobó este permiso"
},
"permissions": {
"message": "Permisos"
},

@ -1343,12 +1343,6 @@
"pending": {
"message": "Pendiente"
},
"permissionCheckedIconDescription": {
"message": "Aprobó este permiso"
},
"permissionUncheckedIconDescription": {
"message": "No aprobó este permiso"
},
"permissions": {
"message": "Permisos"
},

@ -1343,12 +1343,6 @@
"pending": {
"message": "लित"
},
"permissionCheckedIconDescription": {
"message": "आपन इस अनमति अनित कर दि"
},
"permissionUncheckedIconDescription": {
"message": "आपन इस अनमति अनित नहि"
},
"permissions": {
"message": "अनमति"
},

@ -1343,12 +1343,6 @@
"pending": {
"message": "Tunda"
},
"permissionCheckedIconDescription": {
"message": "Anda telah menyetujui izin ini"
},
"permissionUncheckedIconDescription": {
"message": "Anda belum menyetujui izin ini"
},
"permissions": {
"message": "Izin"
},

@ -1102,12 +1102,6 @@
"pending": {
"message": "in corso"
},
"permissionCheckedIconDescription": {
"message": "Hai approvato questo permesso"
},
"permissionUncheckedIconDescription": {
"message": "Non hai approvato questo permesso"
},
"permissions": {
"message": "Permessi"
},

@ -1343,12 +1343,6 @@
"pending": {
"message": "処理"
},
"permissionCheckedIconDescription": {
"message": "この許可の承認が完了しました。"
},
"permissionUncheckedIconDescription": {
"message": "この許可の承認が完了していません。"
},
"permissions": {
"message": "許可"
},

@ -1343,12 +1343,6 @@
"pending": {
"message": "보류 중"
},
"permissionCheckedIconDescription": {
"message": "이 권한을 승인했습니다."
},
"permissionUncheckedIconDescription": {
"message": "이 권한을 승인하지 않았습니다."
},
"permissions": {
"message": "권한"
},

@ -1343,12 +1343,6 @@
"pending": {
"message": "Nakabinbin"
},
"permissionCheckedIconDescription": {
"message": "Inaprubahan mo ang pahintulot na ito"
},
"permissionUncheckedIconDescription": {
"message": "Hindi mo inaprubahan ang pahintulot na ito"
},
"permissions": {
"message": "Mga Pahintulot"
},

@ -1343,12 +1343,6 @@
"pending": {
"message": "Pendente"
},
"permissionCheckedIconDescription": {
"message": "Você aprovou esta permissão"
},
"permissionUncheckedIconDescription": {
"message": "Você não aprovou esta permissão"
},
"permissions": {
"message": "Permissões"
},

@ -1343,12 +1343,6 @@
"pending": {
"message": "В ожидании"
},
"permissionCheckedIconDescription": {
"message": "Вы одобрили это разрешение"
},
"permissionUncheckedIconDescription": {
"message": "Вы не одобрили это разрешение"
},
"permissions": {
"message": "Разрешения"
},

@ -1093,12 +1093,6 @@
"pending": {
"message": "Nakabinbin"
},
"permissionCheckedIconDescription": {
"message": "Inaprubahan mo ang pahintulot na ito"
},
"permissionUncheckedIconDescription": {
"message": "Hindi mo inaprubahan ang pahintulot na ito"
},
"permissions": {
"message": "Mga Pahintulot"
},

@ -1343,12 +1343,6 @@
"pending": {
"message": "Đang chờ xử lý"
},
"permissionCheckedIconDescription": {
"message": "Bạn đã phê duyệt quyền này"
},
"permissionUncheckedIconDescription": {
"message": "Bạn chưa phê duyệt quyền này"
},
"permissions": {
"message": "Quyền"
},

@ -1144,12 +1144,6 @@
"pending": {
"message": "待处理"
},
"permissionCheckedIconDescription": {
"message": "您已同意该权限"
},
"permissionUncheckedIconDescription": {
"message": "您还未同意该权限"
},
"permissions": {
"message": "权限"
},

@ -34,14 +34,14 @@
<path d="M256.683 108.955L251.485 105.202L259.802 97.5905L253.46 92.5859L261.777 86.2258L256.267 82.0553L265 39.5158L251.901 0L167.587 31.5918H97.4127L13.0993 0L0 39.5158L8.8368 82.0553L3.22283 86.2258L11.5398 92.5859L5.19812 97.5905L13.5151 105.202L8.31699 108.955L20.2727 123.031L2.18321 179.542L18.9211 237.199L77.8678 220.934L113.111 203.731L110.231 227.044L112.903 224.583L152.097 224.479L154.696 226.773L151.889 203.731L187.132 220.934L246.079 237.199L262.921 179.542L244.727 123.031L256.683 108.955Z" fill="url(#paint13_linear)" fill-opacity="0.1" style="mix-blend-mode:color-dodge"/>
<path d="M256.683 108.955L251.485 105.202L259.802 97.5905L253.46 92.5859L261.777 86.2258L256.267 82.0553L265 39.5158L251.901 0L167.587 31.5918H97.4127L13.0993 0L0 39.5158L8.8368 82.0553L3.22283 86.2258L11.5398 92.5859L5.19812 97.5905L13.5151 105.202L8.31699 108.955L20.2727 123.031L2.18321 179.542L18.9211 237.199L77.8678 220.934L113.111 203.731L117.997 200.186H125.275H139.829H147.107L151.889 203.731L187.132 220.934L246.079 237.199L262.921 179.542L244.727 123.031L256.683 108.955Z" fill="url(#paint14_radial)" style="mix-blend-mode:overlay"/>
</g>
<path d="M1166.17 120.732C1159.42 116.127 1151.72 112.742 1144.35 108.759C1139.75 106.295 1134.84 103.855 1130.83 100.47C1124.08 94.944 1125.3 83.5933 1132.67 78.6647C1143.11 71.9189 1160.02 75.603 1161.86 89.7167C1161.86 90.0154 1162.16 90.339 1162.48 90.339H1178.15C1178.44 90.339 1178.77 90.0403 1178.77 89.7167C1177.85 79.8844 1174.16 71.9189 1167.41 66.6916C1160.94 61.763 1153.27 59 1145.27 59C1104.11 59 1100.42 102.611 1122.54 116.426C1125 117.969 1146.82 129.021 1154.49 133.626C1162.16 138.53 1164.62 147.143 1161.24 153.888C1158.17 160.335 1150.18 164.318 1142.48 164.019C1133.89 163.721 1127.12 158.792 1124.98 151.424C1124.68 150.204 1124.36 147.74 1124.36 146.52C1124.36 146.222 1124.06 145.898 1123.73 145.898H1106.85C1106.55 145.898 1106.23 146.197 1106.23 146.52C1106.23 158.792 1109.29 165.563 1117.58 171.711C1125.25 177.536 1133.87 180 1142.78 180C1165.82 180 1177.8 166.807 1180.26 153.291C1182.13 140.372 1178.14 128.399 1166.17 120.732Z" fill="#24292E"/>
<path d="M433.192 61.4634H425.522H417.229C416.931 61.4634 416.607 61.7621 416.607 61.7621L402.786 107.514C402.487 108.136 401.865 108.136 401.566 107.514L388.044 61.7621C388.044 61.4634 387.745 61.4634 387.421 61.4634H379.129H371.758H361.623C361.299 61.4634 361 61.7621 361 62.0608V178.754C361 179.053 361.299 179.377 361.623 179.377H378.506C378.805 179.377 379.129 179.078 379.129 178.754V90.0145C379.129 89.3922 380.05 89.0935 380.349 89.7158L394.469 135.766L395.39 138.828C395.39 139.126 395.689 139.126 396.013 139.126H408.912C409.211 139.126 409.535 138.828 409.535 138.828L410.456 135.766L424.576 89.7158C424.576 89.0935 425.497 89.4171 425.497 90.0145V178.754C425.497 179.053 425.796 179.377 426.12 179.377H443.003C443.302 179.377 443.626 179.078 443.626 178.754V62.0608C443.626 61.7621 443.327 61.4385 443.003 61.4385L433.192 61.4634Z" fill="#24292E"/>
<path d="M907.506 61.4634C907.207 61.4634 906.883 61.7621 906.883 61.7621L893.063 107.514C892.764 108.136 892.141 108.136 891.842 107.514L878.022 61.7621C878.022 61.4634 877.723 61.4634 877.399 61.4634H851.6C851.301 61.4634 850.978 61.7621 850.978 62.0857V178.779C850.978 179.078 851.276 179.402 851.6 179.402H868.484C868.783 179.402 869.106 179.103 869.106 178.779V90.0145C869.106 89.3922 870.028 89.0935 870.327 89.7158L884.446 135.766L885.368 138.828C885.368 139.126 885.667 139.126 885.99 139.126H898.89C899.189 139.126 899.512 138.828 899.512 138.828L900.434 135.766L914.553 89.7158C914.852 89.0935 915.773 89.0935 915.773 90.0145V178.754C915.773 179.053 916.072 179.377 916.396 179.377H933.28C933.579 179.377 933.902 179.078 933.902 178.754V62.0608C933.902 61.7621 933.604 61.4385 933.28 61.4385L907.506 61.4634Z" fill="#24292E"/>
<path d="M690.01 61.4648H658.359H641.475H610.148C609.849 61.4648 609.525 61.7635 609.525 62.0871V76.5245C609.525 76.8232 609.824 77.1468 610.148 77.1468H640.877V178.482C640.877 178.781 641.176 179.104 641.5 179.104H658.384C658.683 179.104 659.006 178.806 659.006 178.482V77.1219H689.711C690.01 77.1219 690.333 76.8232 690.333 76.4996V62.0623C690.632 61.7635 690.309 61.4648 690.01 61.4648Z" fill="#24292E"/>
<path d="M789.545 179.377H804.91C805.208 179.377 805.532 179.078 805.532 178.456L773.582 61.4637C773.582 61.165 773.284 61.165 772.96 61.165H767.133H756.699H751.17C750.872 61.165 750.548 61.4637 750.548 61.4637L718.897 178.456C718.897 178.755 719.196 179.377 719.52 179.377H734.884C735.183 179.377 735.507 179.078 735.507 179.078L744.721 145.001C744.721 144.703 745.02 144.703 745.343 144.703H779.435C779.733 144.703 780.057 145.001 780.057 145.001L789.271 179.078C788.922 179.054 789.246 179.377 789.545 179.377ZM749.004 127.776L761.281 82.3232C761.58 81.7009 762.202 81.7009 762.501 82.3232L774.778 127.776C774.778 128.075 774.479 128.697 774.155 128.697H749.577C749.303 128.398 749.004 128.1 749.004 127.776Z" fill="#24292E"/>
<path d="M1051.59 179.377H1066.96C1067.26 179.377 1067.58 179.078 1067.58 178.456L1035.63 61.4637C1035.63 61.165 1035.33 61.165 1035.01 61.165H1029.18H1018.75H1012.92C1012.62 61.165 1012.3 61.4637 1012.3 61.4637L980.646 178.456C980.646 178.755 980.944 179.377 981.268 179.377H996.633C996.932 179.377 997.255 179.078 997.255 179.078L1006.47 145.001C1006.47 144.703 1006.77 144.703 1007.09 144.703H1041.18C1041.48 144.703 1041.81 145.001 1041.81 145.001L1051.02 179.078C1050.97 179.054 1051.27 179.377 1051.59 179.377ZM1011.03 127.776L1023.3 82.3232C1023.6 81.7009 1024.22 81.7009 1024.52 82.3232L1036.8 127.776C1036.8 128.075 1036.5 128.697 1036.18 128.697H1011.6C1011.35 128.398 1011.03 128.1 1011.03 127.776Z" fill="#24292E"/>
<path d="M512.132 162.176V125.934C512.132 125.635 512.431 125.311 512.755 125.311H557.604C557.903 125.311 558.227 125.013 558.227 124.689V110.252C558.227 109.953 557.928 109.629 557.604 109.629H512.755C512.456 109.629 512.132 109.331 512.132 109.007V77.7427C512.132 77.444 512.431 77.1204 512.755 77.1204H563.755C564.054 77.1204 564.377 76.8217 564.377 76.4981V62.0608C564.377 61.7621 564.079 61.4385 563.755 61.4385H512.132H494.626C494.327 61.4385 494.003 61.7372 494.003 62.0608V77.0955V109.331V124.988V162.45V178.406C494.003 178.705 494.302 179.028 494.626 179.028H512.132H566.195C566.494 179.028 566.818 178.73 566.818 178.406V163.048C566.818 162.749 566.519 162.425 566.195 162.425H512.755C512.456 162.799 512.132 162.475 512.132 162.176Z" fill="#24292E"/>
<path d="M1320.39 178.132L1262.02 117.645C1261.72 117.346 1261.72 117.022 1262.02 116.724L1314.54 62.3844C1314.83 62.0857 1314.54 61.4634 1314.24 61.4634H1292.72C1292.42 61.4634 1292.42 61.4634 1292.42 61.7621L1247.87 108.136C1247.57 108.435 1246.95 108.136 1246.95 107.837V62.0608C1246.95 61.7621 1246.65 61.4385 1246.33 61.4385H1229.44C1229.15 61.4385 1228.82 61.7372 1228.82 62.0608V178.754C1228.82 179.053 1229.12 179.377 1229.44 179.377H1246.33C1246.63 179.377 1246.95 179.078 1246.95 178.754V127.178C1246.95 126.556 1247.57 126.257 1247.87 126.88L1298.25 179.078L1298.55 179.377H1320.06C1320.69 179.377 1320.99 178.754 1320.39 178.132Z" fill="#24292E"/>
<path d="M1166.17 120.732C1159.42 116.127 1151.72 112.742 1144.35 108.759C1139.75 106.295 1134.84 103.855 1130.83 100.47C1124.08 94.944 1125.3 83.5933 1132.67 78.6647C1143.11 71.9189 1160.02 75.603 1161.86 89.7167C1161.86 90.0154 1162.16 90.339 1162.48 90.339H1178.15C1178.44 90.339 1178.77 90.0403 1178.77 89.7167C1177.85 79.8844 1174.16 71.9189 1167.41 66.6916C1160.94 61.763 1153.27 59 1145.27 59C1104.11 59 1100.42 102.611 1122.54 116.426C1125 117.969 1146.82 129.021 1154.49 133.626C1162.16 138.53 1164.62 147.143 1161.24 153.888C1158.17 160.335 1150.18 164.318 1142.48 164.019C1133.89 163.721 1127.12 158.792 1124.98 151.424C1124.68 150.204 1124.36 147.74 1124.36 146.52C1124.36 146.222 1124.06 145.898 1123.73 145.898H1106.85C1106.55 145.898 1106.23 146.197 1106.23 146.52C1106.23 158.792 1109.29 165.563 1117.58 171.711C1125.25 177.536 1133.87 180 1142.78 180C1165.82 180 1177.8 166.807 1180.26 153.291C1182.13 140.372 1178.14 128.399 1166.17 120.732Z" fill="white"/>
<path d="M433.192 61.4634H425.522H417.229C416.931 61.4634 416.607 61.7621 416.607 61.7621L402.786 107.514C402.487 108.136 401.865 108.136 401.566 107.514L388.044 61.7621C388.044 61.4634 387.745 61.4634 387.421 61.4634H379.129H371.758H361.623C361.299 61.4634 361 61.7621 361 62.0608V178.754C361 179.053 361.299 179.377 361.623 179.377H378.506C378.805 179.377 379.129 179.078 379.129 178.754V90.0145C379.129 89.3922 380.05 89.0935 380.349 89.7158L394.469 135.766L395.39 138.828C395.39 139.126 395.689 139.126 396.013 139.126H408.912C409.211 139.126 409.535 138.828 409.535 138.828L410.456 135.766L424.576 89.7158C424.576 89.0935 425.497 89.4171 425.497 90.0145V178.754C425.497 179.053 425.796 179.377 426.12 179.377H443.003C443.302 179.377 443.626 179.078 443.626 178.754V62.0608C443.626 61.7621 443.327 61.4385 443.003 61.4385L433.192 61.4634Z" fill="white"/>
<path d="M907.506 61.4634C907.207 61.4634 906.883 61.7621 906.883 61.7621L893.063 107.514C892.764 108.136 892.141 108.136 891.842 107.514L878.022 61.7621C878.022 61.4634 877.723 61.4634 877.399 61.4634H851.6C851.301 61.4634 850.978 61.7621 850.978 62.0857V178.779C850.978 179.078 851.276 179.402 851.6 179.402H868.484C868.783 179.402 869.106 179.103 869.106 178.779V90.0145C869.106 89.3922 870.028 89.0935 870.327 89.7158L884.446 135.766L885.368 138.828C885.368 139.126 885.667 139.126 885.99 139.126H898.89C899.189 139.126 899.512 138.828 899.512 138.828L900.434 135.766L914.553 89.7158C914.852 89.0935 915.773 89.0935 915.773 90.0145V178.754C915.773 179.053 916.072 179.377 916.396 179.377H933.28C933.579 179.377 933.902 179.078 933.902 178.754V62.0608C933.902 61.7621 933.604 61.4385 933.28 61.4385L907.506 61.4634Z" fill="white"/>
<path d="M690.01 61.4648H658.359H641.475H610.148C609.849 61.4648 609.525 61.7635 609.525 62.0871V76.5245C609.525 76.8232 609.824 77.1468 610.148 77.1468H640.877V178.482C640.877 178.781 641.176 179.104 641.5 179.104H658.384C658.683 179.104 659.006 178.806 659.006 178.482V77.1219H689.711C690.01 77.1219 690.333 76.8232 690.333 76.4996V62.0623C690.632 61.7635 690.309 61.4648 690.01 61.4648Z" fill="white"/>
<path d="M789.545 179.377H804.91C805.208 179.377 805.532 179.078 805.532 178.456L773.582 61.4637C773.582 61.165 773.284 61.165 772.96 61.165H767.133H756.699H751.17C750.872 61.165 750.548 61.4637 750.548 61.4637L718.897 178.456C718.897 178.755 719.196 179.377 719.52 179.377H734.884C735.183 179.377 735.507 179.078 735.507 179.078L744.721 145.001C744.721 144.703 745.02 144.703 745.343 144.703H779.435C779.733 144.703 780.057 145.001 780.057 145.001L789.271 179.078C788.922 179.054 789.246 179.377 789.545 179.377ZM749.004 127.776L761.281 82.3232C761.58 81.7009 762.202 81.7009 762.501 82.3232L774.778 127.776C774.778 128.075 774.479 128.697 774.155 128.697H749.577C749.303 128.398 749.004 128.1 749.004 127.776Z" fill="white"/>
<path d="M1051.59 179.377H1066.96C1067.26 179.377 1067.58 179.078 1067.58 178.456L1035.63 61.4637C1035.63 61.165 1035.33 61.165 1035.01 61.165H1029.18H1018.75H1012.92C1012.62 61.165 1012.3 61.4637 1012.3 61.4637L980.646 178.456C980.646 178.755 980.944 179.377 981.268 179.377H996.633C996.932 179.377 997.255 179.078 997.255 179.078L1006.47 145.001C1006.47 144.703 1006.77 144.703 1007.09 144.703H1041.18C1041.48 144.703 1041.81 145.001 1041.81 145.001L1051.02 179.078C1050.97 179.054 1051.27 179.377 1051.59 179.377ZM1011.03 127.776L1023.3 82.3232C1023.6 81.7009 1024.22 81.7009 1024.52 82.3232L1036.8 127.776C1036.8 128.075 1036.5 128.697 1036.18 128.697H1011.6C1011.35 128.398 1011.03 128.1 1011.03 127.776Z" fill="white"/>
<path d="M512.132 162.176V125.934C512.132 125.635 512.431 125.311 512.755 125.311H557.604C557.903 125.311 558.227 125.013 558.227 124.689V110.252C558.227 109.953 557.928 109.629 557.604 109.629H512.755C512.456 109.629 512.132 109.331 512.132 109.007V77.7427C512.132 77.444 512.431 77.1204 512.755 77.1204H563.755C564.054 77.1204 564.377 76.8217 564.377 76.4981V62.0608C564.377 61.7621 564.079 61.4385 563.755 61.4385H512.132H494.626C494.327 61.4385 494.003 61.7372 494.003 62.0608V77.0955V109.331V124.988V162.45V178.406C494.003 178.705 494.302 179.028 494.626 179.028H512.132H566.195C566.494 179.028 566.818 178.73 566.818 178.406V163.048C566.818 162.749 566.519 162.425 566.195 162.425H512.755C512.456 162.799 512.132 162.475 512.132 162.176Z" fill="white"/>
<path d="M1320.39 178.132L1262.02 117.645C1261.72 117.346 1261.72 117.022 1262.02 116.724L1314.54 62.3844C1314.83 62.0857 1314.54 61.4634 1314.24 61.4634H1292.72C1292.42 61.4634 1292.42 61.4634 1292.42 61.7621L1247.87 108.136C1247.57 108.435 1246.95 108.136 1246.95 107.837V62.0608C1246.95 61.7621 1246.65 61.4385 1246.33 61.4385H1229.44C1229.15 61.4385 1228.82 61.7372 1228.82 62.0608V178.754C1228.82 179.053 1229.12 179.377 1229.44 179.377H1246.33C1246.63 179.377 1246.95 179.078 1246.95 178.754V127.178C1246.95 126.556 1247.57 126.257 1247.87 126.88L1298.25 179.078L1298.55 179.377H1320.06C1320.69 179.377 1320.99 178.754 1320.39 178.132Z" fill="white"/>
<rect x="1338" y="27" width="194" height="84" rx="12" fill="#24292E"/>
<path d="M1358 50.6376H1380.89V58.7139H1366.33V65.515H1377.7V73.5913H1366.33V88.3624H1358V50.6376Z" fill="white"/>
<path d="M1386.55 50.6376H1394.87V80.2861H1410.28V88.3624H1386.55V50.6376Z" fill="white"/>

Before

Width:  |  Height:  |  Size: 16 KiB

After

Width:  |  Height:  |  Size: 16 KiB

@ -34,14 +34,14 @@
<path d="M256.683 108.955L251.485 105.202L259.802 97.5905L253.46 92.5859L261.777 86.2258L256.267 82.0553L265 39.5158L251.901 0L167.587 31.5918H97.4127L13.0993 0L0 39.5158L8.8368 82.0553L3.22283 86.2258L11.5398 92.5859L5.19812 97.5905L13.5151 105.202L8.31699 108.955L20.2727 123.031L2.18321 179.542L18.9211 237.199L77.8678 220.934L113.111 203.731L110.231 227.044L112.903 224.583L152.097 224.479L154.696 226.773L151.889 203.731L187.132 220.934L246.079 237.199L262.921 179.542L244.727 123.031L256.683 108.955Z" fill="url(#paint13_linear)" fill-opacity="0.1" style="mix-blend-mode:color-dodge"/>
<path d="M256.683 108.955L251.485 105.202L259.802 97.5905L253.46 92.5859L261.777 86.2258L256.267 82.0553L265 39.5158L251.901 0L167.587 31.5918H97.4127L13.0993 0L0 39.5158L8.8368 82.0553L3.22283 86.2258L11.5398 92.5859L5.19812 97.5905L13.5151 105.202L8.31699 108.955L20.2727 123.031L2.18321 179.542L18.9211 237.199L77.8678 220.934L113.111 203.731L117.997 200.186H125.275H139.829H147.107L151.889 203.731L187.132 220.934L246.079 237.199L262.921 179.542L244.727 123.031L256.683 108.955Z" fill="url(#paint14_radial)" style="mix-blend-mode:overlay"/>
</g>
<path d="M1166.17 120.732C1159.42 116.127 1151.72 112.742 1144.35 108.759C1139.75 106.295 1134.84 103.855 1130.83 100.47C1124.08 94.944 1125.3 83.5933 1132.67 78.6647C1143.11 71.9189 1160.02 75.603 1161.86 89.7167C1161.86 90.0154 1162.16 90.339 1162.48 90.339H1178.15C1178.44 90.339 1178.77 90.0403 1178.77 89.7167C1177.85 79.8844 1174.16 71.9189 1167.41 66.6916C1160.94 61.763 1153.27 59 1145.27 59C1104.11 59 1100.42 102.611 1122.54 116.426C1125 117.969 1146.82 129.021 1154.49 133.626C1162.16 138.53 1164.62 147.143 1161.24 153.888C1158.17 160.335 1150.18 164.318 1142.48 164.019C1133.89 163.721 1127.12 158.792 1124.98 151.424C1124.68 150.204 1124.36 147.74 1124.36 146.52C1124.36 146.222 1124.06 145.898 1123.73 145.898H1106.85C1106.55 145.898 1106.23 146.197 1106.23 146.52C1106.23 158.792 1109.29 165.563 1117.58 171.711C1125.25 177.536 1133.87 180 1142.78 180C1165.82 180 1177.8 166.807 1180.26 153.291C1182.13 140.372 1178.14 128.399 1166.17 120.732Z" fill="white"/>
<path d="M433.192 61.4634H425.522H417.229C416.931 61.4634 416.607 61.7621 416.607 61.7621L402.786 107.514C402.487 108.136 401.865 108.136 401.566 107.514L388.044 61.7621C388.044 61.4634 387.745 61.4634 387.421 61.4634H379.129H371.758H361.623C361.299 61.4634 361 61.7621 361 62.0608V178.754C361 179.053 361.299 179.377 361.623 179.377H378.506C378.805 179.377 379.129 179.078 379.129 178.754V90.0145C379.129 89.3922 380.05 89.0935 380.349 89.7158L394.469 135.766L395.39 138.828C395.39 139.126 395.689 139.126 396.013 139.126H408.912C409.211 139.126 409.535 138.828 409.535 138.828L410.456 135.766L424.576 89.7158C424.576 89.0935 425.497 89.4171 425.497 90.0145V178.754C425.497 179.053 425.796 179.377 426.12 179.377H443.003C443.302 179.377 443.626 179.078 443.626 178.754V62.0608C443.626 61.7621 443.327 61.4385 443.003 61.4385L433.192 61.4634Z" fill="white"/>
<path d="M907.506 61.4634C907.207 61.4634 906.883 61.7621 906.883 61.7621L893.063 107.514C892.764 108.136 892.141 108.136 891.842 107.514L878.022 61.7621C878.022 61.4634 877.723 61.4634 877.399 61.4634H851.6C851.301 61.4634 850.978 61.7621 850.978 62.0857V178.779C850.978 179.078 851.276 179.402 851.6 179.402H868.484C868.783 179.402 869.106 179.103 869.106 178.779V90.0145C869.106 89.3922 870.028 89.0935 870.327 89.7158L884.446 135.766L885.368 138.828C885.368 139.126 885.667 139.126 885.99 139.126H898.89C899.189 139.126 899.512 138.828 899.512 138.828L900.434 135.766L914.553 89.7158C914.852 89.0935 915.773 89.0935 915.773 90.0145V178.754C915.773 179.053 916.072 179.377 916.396 179.377H933.28C933.579 179.377 933.902 179.078 933.902 178.754V62.0608C933.902 61.7621 933.604 61.4385 933.28 61.4385L907.506 61.4634Z" fill="white"/>
<path d="M690.01 61.4648H658.359H641.475H610.148C609.849 61.4648 609.525 61.7635 609.525 62.0871V76.5245C609.525 76.8232 609.824 77.1468 610.148 77.1468H640.877V178.482C640.877 178.781 641.176 179.104 641.5 179.104H658.384C658.683 179.104 659.006 178.806 659.006 178.482V77.1219H689.711C690.01 77.1219 690.333 76.8232 690.333 76.4996V62.0623C690.632 61.7635 690.309 61.4648 690.01 61.4648Z" fill="white"/>
<path d="M789.545 179.377H804.91C805.208 179.377 805.532 179.078 805.532 178.456L773.582 61.4637C773.582 61.165 773.284 61.165 772.96 61.165H767.133H756.699H751.17C750.872 61.165 750.548 61.4637 750.548 61.4637L718.897 178.456C718.897 178.755 719.196 179.377 719.52 179.377H734.884C735.183 179.377 735.507 179.078 735.507 179.078L744.721 145.001C744.721 144.703 745.02 144.703 745.343 144.703H779.435C779.733 144.703 780.057 145.001 780.057 145.001L789.271 179.078C788.922 179.054 789.246 179.377 789.545 179.377ZM749.004 127.776L761.281 82.3232C761.58 81.7009 762.202 81.7009 762.501 82.3232L774.778 127.776C774.778 128.075 774.479 128.697 774.155 128.697H749.577C749.303 128.398 749.004 128.1 749.004 127.776Z" fill="white"/>
<path d="M1051.59 179.377H1066.96C1067.26 179.377 1067.58 179.078 1067.58 178.456L1035.63 61.4637C1035.63 61.165 1035.33 61.165 1035.01 61.165H1029.18H1018.75H1012.92C1012.62 61.165 1012.3 61.4637 1012.3 61.4637L980.646 178.456C980.646 178.755 980.944 179.377 981.268 179.377H996.633C996.932 179.377 997.255 179.078 997.255 179.078L1006.47 145.001C1006.47 144.703 1006.77 144.703 1007.09 144.703H1041.18C1041.48 144.703 1041.81 145.001 1041.81 145.001L1051.02 179.078C1050.97 179.054 1051.27 179.377 1051.59 179.377ZM1011.03 127.776L1023.3 82.3232C1023.6 81.7009 1024.22 81.7009 1024.52 82.3232L1036.8 127.776C1036.8 128.075 1036.5 128.697 1036.18 128.697H1011.6C1011.35 128.398 1011.03 128.1 1011.03 127.776Z" fill="white"/>
<path d="M512.132 162.176V125.934C512.132 125.635 512.431 125.311 512.755 125.311H557.604C557.903 125.311 558.227 125.013 558.227 124.689V110.252C558.227 109.953 557.928 109.629 557.604 109.629H512.755C512.456 109.629 512.132 109.331 512.132 109.007V77.7427C512.132 77.444 512.431 77.1204 512.755 77.1204H563.755C564.054 77.1204 564.377 76.8217 564.377 76.4981V62.0608C564.377 61.7621 564.079 61.4385 563.755 61.4385H512.132H494.626C494.327 61.4385 494.003 61.7372 494.003 62.0608V77.0955V109.331V124.988V162.45V178.406C494.003 178.705 494.302 179.028 494.626 179.028H512.132H566.195C566.494 179.028 566.818 178.73 566.818 178.406V163.048C566.818 162.749 566.519 162.425 566.195 162.425H512.755C512.456 162.799 512.132 162.475 512.132 162.176Z" fill="white"/>
<path d="M1320.39 178.132L1262.02 117.645C1261.72 117.346 1261.72 117.022 1262.02 116.724L1314.54 62.3844C1314.83 62.0857 1314.54 61.4634 1314.24 61.4634H1292.72C1292.42 61.4634 1292.42 61.4634 1292.42 61.7621L1247.87 108.136C1247.57 108.435 1246.95 108.136 1246.95 107.837V62.0608C1246.95 61.7621 1246.65 61.4385 1246.33 61.4385H1229.44C1229.15 61.4385 1228.82 61.7372 1228.82 62.0608V178.754C1228.82 179.053 1229.12 179.377 1229.44 179.377H1246.33C1246.63 179.377 1246.95 179.078 1246.95 178.754V127.178C1246.95 126.556 1247.57 126.257 1247.87 126.88L1298.25 179.078L1298.55 179.377H1320.06C1320.69 179.377 1320.99 178.754 1320.39 178.132Z" fill="white"/>
<path d="M1166.17 120.732C1159.42 116.127 1151.72 112.742 1144.35 108.759C1139.75 106.295 1134.84 103.855 1130.83 100.47C1124.08 94.944 1125.3 83.5933 1132.67 78.6647C1143.11 71.9189 1160.02 75.603 1161.86 89.7167C1161.86 90.0154 1162.16 90.339 1162.48 90.339H1178.15C1178.44 90.339 1178.77 90.0403 1178.77 89.7167C1177.85 79.8844 1174.16 71.9189 1167.41 66.6916C1160.94 61.763 1153.27 59 1145.27 59C1104.11 59 1100.42 102.611 1122.54 116.426C1125 117.969 1146.82 129.021 1154.49 133.626C1162.16 138.53 1164.62 147.143 1161.24 153.888C1158.17 160.335 1150.18 164.318 1142.48 164.019C1133.89 163.721 1127.12 158.792 1124.98 151.424C1124.68 150.204 1124.36 147.74 1124.36 146.52C1124.36 146.222 1124.06 145.898 1123.73 145.898H1106.85C1106.55 145.898 1106.23 146.197 1106.23 146.52C1106.23 158.792 1109.29 165.563 1117.58 171.711C1125.25 177.536 1133.87 180 1142.78 180C1165.82 180 1177.8 166.807 1180.26 153.291C1182.13 140.372 1178.14 128.399 1166.17 120.732Z" fill="#24292E"/>
<path d="M433.192 61.4634H425.522H417.229C416.931 61.4634 416.607 61.7621 416.607 61.7621L402.786 107.514C402.487 108.136 401.865 108.136 401.566 107.514L388.044 61.7621C388.044 61.4634 387.745 61.4634 387.421 61.4634H379.129H371.758H361.623C361.299 61.4634 361 61.7621 361 62.0608V178.754C361 179.053 361.299 179.377 361.623 179.377H378.506C378.805 179.377 379.129 179.078 379.129 178.754V90.0145C379.129 89.3922 380.05 89.0935 380.349 89.7158L394.469 135.766L395.39 138.828C395.39 139.126 395.689 139.126 396.013 139.126H408.912C409.211 139.126 409.535 138.828 409.535 138.828L410.456 135.766L424.576 89.7158C424.576 89.0935 425.497 89.4171 425.497 90.0145V178.754C425.497 179.053 425.796 179.377 426.12 179.377H443.003C443.302 179.377 443.626 179.078 443.626 178.754V62.0608C443.626 61.7621 443.327 61.4385 443.003 61.4385L433.192 61.4634Z" fill="#24292E"/>
<path d="M907.506 61.4634C907.207 61.4634 906.883 61.7621 906.883 61.7621L893.063 107.514C892.764 108.136 892.141 108.136 891.842 107.514L878.022 61.7621C878.022 61.4634 877.723 61.4634 877.399 61.4634H851.6C851.301 61.4634 850.978 61.7621 850.978 62.0857V178.779C850.978 179.078 851.276 179.402 851.6 179.402H868.484C868.783 179.402 869.106 179.103 869.106 178.779V90.0145C869.106 89.3922 870.028 89.0935 870.327 89.7158L884.446 135.766L885.368 138.828C885.368 139.126 885.667 139.126 885.99 139.126H898.89C899.189 139.126 899.512 138.828 899.512 138.828L900.434 135.766L914.553 89.7158C914.852 89.0935 915.773 89.0935 915.773 90.0145V178.754C915.773 179.053 916.072 179.377 916.396 179.377H933.28C933.579 179.377 933.902 179.078 933.902 178.754V62.0608C933.902 61.7621 933.604 61.4385 933.28 61.4385L907.506 61.4634Z" fill="#24292E"/>
<path d="M690.01 61.4648H658.359H641.475H610.148C609.849 61.4648 609.525 61.7635 609.525 62.0871V76.5245C609.525 76.8232 609.824 77.1468 610.148 77.1468H640.877V178.482C640.877 178.781 641.176 179.104 641.5 179.104H658.384C658.683 179.104 659.006 178.806 659.006 178.482V77.1219H689.711C690.01 77.1219 690.333 76.8232 690.333 76.4996V62.0623C690.632 61.7635 690.309 61.4648 690.01 61.4648Z" fill="#24292E"/>
<path d="M789.545 179.377H804.91C805.208 179.377 805.532 179.078 805.532 178.456L773.582 61.4637C773.582 61.165 773.284 61.165 772.96 61.165H767.133H756.699H751.17C750.872 61.165 750.548 61.4637 750.548 61.4637L718.897 178.456C718.897 178.755 719.196 179.377 719.52 179.377H734.884C735.183 179.377 735.507 179.078 735.507 179.078L744.721 145.001C744.721 144.703 745.02 144.703 745.343 144.703H779.435C779.733 144.703 780.057 145.001 780.057 145.001L789.271 179.078C788.922 179.054 789.246 179.377 789.545 179.377ZM749.004 127.776L761.281 82.3232C761.58 81.7009 762.202 81.7009 762.501 82.3232L774.778 127.776C774.778 128.075 774.479 128.697 774.155 128.697H749.577C749.303 128.398 749.004 128.1 749.004 127.776Z" fill="#24292E"/>
<path d="M1051.59 179.377H1066.96C1067.26 179.377 1067.58 179.078 1067.58 178.456L1035.63 61.4637C1035.63 61.165 1035.33 61.165 1035.01 61.165H1029.18H1018.75H1012.92C1012.62 61.165 1012.3 61.4637 1012.3 61.4637L980.646 178.456C980.646 178.755 980.944 179.377 981.268 179.377H996.633C996.932 179.377 997.255 179.078 997.255 179.078L1006.47 145.001C1006.47 144.703 1006.77 144.703 1007.09 144.703H1041.18C1041.48 144.703 1041.81 145.001 1041.81 145.001L1051.02 179.078C1050.97 179.054 1051.27 179.377 1051.59 179.377ZM1011.03 127.776L1023.3 82.3232C1023.6 81.7009 1024.22 81.7009 1024.52 82.3232L1036.8 127.776C1036.8 128.075 1036.5 128.697 1036.18 128.697H1011.6C1011.35 128.398 1011.03 128.1 1011.03 127.776Z" fill="#24292E"/>
<path d="M512.132 162.176V125.934C512.132 125.635 512.431 125.311 512.755 125.311H557.604C557.903 125.311 558.227 125.013 558.227 124.689V110.252C558.227 109.953 557.928 109.629 557.604 109.629H512.755C512.456 109.629 512.132 109.331 512.132 109.007V77.7427C512.132 77.444 512.431 77.1204 512.755 77.1204H563.755C564.054 77.1204 564.377 76.8217 564.377 76.4981V62.0608C564.377 61.7621 564.079 61.4385 563.755 61.4385H512.132H494.626C494.327 61.4385 494.003 61.7372 494.003 62.0608V77.0955V109.331V124.988V162.45V178.406C494.003 178.705 494.302 179.028 494.626 179.028H512.132H566.195C566.494 179.028 566.818 178.73 566.818 178.406V163.048C566.818 162.749 566.519 162.425 566.195 162.425H512.755C512.456 162.799 512.132 162.475 512.132 162.176Z" fill="#24292E"/>
<path d="M1320.39 178.132L1262.02 117.645C1261.72 117.346 1261.72 117.022 1262.02 116.724L1314.54 62.3844C1314.83 62.0857 1314.54 61.4634 1314.24 61.4634H1292.72C1292.42 61.4634 1292.42 61.4634 1292.42 61.7621L1247.87 108.136C1247.57 108.435 1246.95 108.136 1246.95 107.837V62.0608C1246.95 61.7621 1246.65 61.4385 1246.33 61.4385H1229.44C1229.15 61.4385 1228.82 61.7372 1228.82 62.0608V178.754C1228.82 179.053 1229.12 179.377 1229.44 179.377H1246.33C1246.63 179.377 1246.95 179.078 1246.95 178.754V127.178C1246.95 126.556 1247.57 126.257 1247.87 126.88L1298.25 179.078L1298.55 179.377H1320.06C1320.69 179.377 1320.99 178.754 1320.39 178.132Z" fill="#24292E"/>
<rect x="1338" y="27" width="194" height="84" rx="12" fill="#24292E"/>
<path d="M1358 50.6376H1380.89V58.7139H1366.33V65.515H1377.7V73.5913H1366.33V88.3624H1358V50.6376Z" fill="white"/>
<path d="M1386.55 50.6376H1394.87V80.2861H1410.28V88.3624H1386.55V50.6376Z" fill="white"/>

Before

Width:  |  Height:  |  Size: 16 KiB

After

Width:  |  Height:  |  Size: 16 KiB

@ -0,0 +1 @@
<svg width="136" height="31" fill="none" xmlns="http://www.w3.org/2000/svg"><path d="M91.201 8.705h1.03l2.39 5.65 2.4-5.65h1.01l-3.02 7.1h-.78l-3.03-7.1Zm9.327 7.2a2.73 2.73 0 0 1-1.06-.2 2.431 2.431 0 0 1-1.35-1.37 2.91 2.91 0 0 1-.18-1.03c0-.367.063-.707.19-1.02.126-.32.303-.597.53-.83.233-.234.51-.417.83-.55.32-.134.67-.2 1.05-.2.326 0 .633.056.92.17.286.106.536.27.75.49.22.213.393.476.52.79.126.306.19.66.19 1.06v.13c0 .033-.004.076-.01.13h-4.1c.006.233.053.45.14.65.093.2.213.373.36.52.153.146.333.263.54.35.213.08.443.12.69.12.386 0 .703-.077.95-.23a1.82 1.82 0 0 0 .61-.64l.68.47a2.48 2.48 0 0 1-.91.87c-.374.213-.82.32-1.34.32Zm1.51-3.13a1.601 1.601 0 0 0-.19-.55c-.087-.16-.2-.297-.34-.41a1.431 1.431 0 0 0-.46-.26 1.645 1.645 0 0 0-.54-.09 1.73 1.73 0 0 0-1.04.35 1.5 1.5 0 0 0-.38.41c-.107.16-.18.343-.22.55h3.17Zm2.1-1.97h.86v.97c.047-.16.12-.304.22-.43a1.556 1.556 0 0 1 .74-.52c.147-.047.294-.07.44-.07.134 0 .264.013.39.04v.89a.78.78 0 0 0-.23-.06 1.342 1.342 0 0 0-.24-.02c-.16 0-.32.036-.48.11-.153.066-.293.17-.42.31-.12.14-.22.32-.3.54-.08.213-.12.466-.12.76v2.48h-.86v-5Zm4.032 7.09 1.09-2.35-2.19-4.74h.95l1.72 3.8 1.71-3.8h.96l-3.28 7.09h-.96Zm7.287-2.09v-7.5h.86v3.27c.173-.3.4-.52.68-.66s.583-.21.91-.21c.28 0 .536.046.77.14.233.093.43.23.59.41.166.173.296.386.39.64.093.246.14.523.14.83v3.08h-.85v-2.95c0-.42-.11-.75-.33-.99-.214-.247-.497-.37-.85-.37-.2 0-.39.04-.57.12-.174.08-.327.2-.46.36-.127.153-.23.343-.31.57-.074.226-.11.486-.11.78v2.48h-.86Zm6.328-6.34a.599.599 0 0 1-.62-.61c0-.167.06-.31.18-.43s.267-.18.44-.18a.602.602 0 0 1 .6.61c0 .173-.056.32-.17.44a.581.581 0 0 1-.43.17Zm-.44 1.34h.86v5h-.86v-5Zm4.671 4.19c.246 0 .473-.044.68-.13.213-.087.393-.207.54-.36.146-.16.26-.347.34-.56.086-.214.13-.447.13-.7 0-.247-.044-.477-.13-.69a1.588 1.588 0 0 0-.34-.56 1.542 1.542 0 0 0-.54-.36 1.637 1.637 0 0 0-.68-.14c-.254 0-.484.046-.69.14a1.548 1.548 0 0 0-.53.36c-.147.153-.264.34-.35.56-.08.213-.12.443-.12.69 0 .253.04.486.12.7.086.213.203.4.35.56.146.153.323.273.53.36.206.086.436.13.69.13Zm-.05 3c-.54 0-1.014-.1-1.42-.3-.407-.2-.717-.44-.93-.72l.6-.6c.2.253.443.456.73.61.293.153.633.23 1.02.23.213 0 .42-.034.62-.1.2-.067.376-.174.53-.32.16-.14.286-.32.38-.54.093-.214.14-.47.14-.77v-.57a2.032 2.032 0 0 1-.72.63c-.307.166-.647.25-1.02.25-.347 0-.67-.064-.97-.19-.3-.134-.56-.314-.78-.54a2.623 2.623 0 0 1-.52-.81 2.8 2.8 0 0 1-.18-1.01c0-.354.06-.684.18-.99.126-.314.3-.584.52-.81.22-.234.48-.414.78-.54.3-.134.623-.2.97-.2.373 0 .713.083 1.02.25.306.16.546.366.72.62v-.77h.86v4.71c0 .42-.067.783-.2 1.09-.127.313-.304.57-.53.77-.227.206-.494.36-.8.46-.307.106-.64.16-1 .16Zm4.149-2.19v-7.5h.86v3.27c.173-.3.4-.52.68-.66s.583-.21.91-.21c.28 0 .537.046.77.14.233.093.43.23.59.41.167.173.297.386.39.64.093.246.14.523.14.83v3.08h-.85v-2.95c0-.42-.11-.75-.33-.99-.213-.247-.497-.37-.85-.37-.2 0-.39.04-.57.12-.173.08-.327.2-.46.36-.127.153-.23.343-.31.57-.073.226-.11.486-.11.78v2.48h-.86Z" fill="#F66A0A"/><path opacity=".3" d="M19.506 22.805c-10.763 7.42-19.416 8-19.416 8h110.25s-8.653-.58-19.417-8c-10.763-7.42-23.363-22-35.708-22-12.345 0-24.945 14.58-35.709 22Z" fill="#037DD6"/><mask id="a" style="mask-type:alpha" maskUnits="userSpaceOnUse" x="0" y="0" width="111" height="31"><path d="M19.506 22.672c-10.763 7.42-19.416 8-19.416 8h110.25s-8.653-.58-19.417-8c-10.763-7.42-23.363-22-35.708-22-12.345 0-24.945 14.58-35.709 22Z" fill="#EAF6FF"/></mask><g mask="url(#a)"><path fill="#F66A0A" stroke="#fff" stroke-width="2" d="M91.986-5.143h20.706v39.25H91.986z"/></g></svg>

After

Width:  |  Height:  |  Size: 3.4 KiB

@ -0,0 +1 @@
<svg width="125" height="31" fill="none" xmlns="http://www.w3.org/2000/svg"><path d="M14.233 8.738h.93v6.22h3.43v.88h-4.36v-7.1Zm7.412 7.2c-.38 0-.73-.067-1.05-.2a2.679 2.679 0 0 1-.83-.56 2.569 2.569 0 0 1-.55-.82c-.126-.32-.19-.66-.19-1.02s.064-.697.19-1.01a2.54 2.54 0 0 1 .55-.83c.234-.233.51-.417.83-.55.32-.14.67-.21 1.05-.21.374 0 .72.07 1.04.21.32.133.597.317.83.55.234.233.414.51.54.83.134.313.2.65.2 1.01s-.066.7-.2 1.02a2.44 2.44 0 0 1-.54.82 2.68 2.68 0 0 1-.83.56c-.32.133-.666.2-1.04.2Zm0-.8c.26 0 .497-.047.71-.14.214-.093.394-.22.54-.38.154-.167.27-.357.35-.57.087-.22.13-.457.13-.71 0-.247-.043-.48-.13-.7-.08-.22-.196-.41-.35-.57a1.52 1.52 0 0 0-.54-.39 1.754 1.754 0 0 0-.71-.14c-.26 0-.496.047-.71.14a1.619 1.619 0 0 0-.55.39c-.153.16-.273.35-.36.57-.08.22-.12.453-.12.7 0 .253.04.49.12.71.087.213.207.403.36.57.154.16.337.287.55.38.214.093.45.14.71.14Zm6.825-2.89-1.23 3.59h-.76l-1.7-5h.9l1.21 3.65 1.25-3.65h.66l1.25 3.65 1.21-3.65h.91l-1.7 5h-.76l-1.24-3.59Z" fill="#F66A0A"/><path opacity=".3" d="M33.96 22.838c-10.764 7.42-19.417 8-19.417 8h110.25s-8.653-.58-19.416-8c-10.764-7.42-23.364-22-35.709-22-12.345 0-24.945 14.58-35.709 22Z" fill="#037DD6"/><mask id="a" style="mask-type:alpha" maskUnits="userSpaceOnUse" x="14" y="0" width="111" height="31"><path d="M33.96 22.705c-10.764 7.42-19.417 8-19.417 8h110.25s-8.653-.58-19.416-8c-10.764-7.42-23.364-22-35.709-22-12.345 0-24.945 14.58-35.709 22Z" fill="#EAF6FF"/></mask><g mask="url(#a)"><path fill="#F66A0A" stroke="#fff" stroke-width="2" d="M12.793 16.838h20.706v17.303H12.793z"/></g></svg>

After

Width:  |  Height:  |  Size: 1.5 KiB

@ -0,0 +1 @@
<svg width="111" height="49" fill="none" xmlns="http://www.w3.org/2000/svg"><path d="m38 4.334 2.63 3.54 2.65-3.54h.84v7.1h-.92v-5.52l-2.56 3.44-2.56-3.44v5.52h-.93v-7.1H38Zm9.99 7.2a2.73 2.73 0 0 1-1.06-.2 2.431 2.431 0 0 1-1.35-1.37 2.91 2.91 0 0 1-.18-1.03c0-.367.063-.707.19-1.02.126-.32.303-.597.53-.83.233-.234.51-.417.83-.55.32-.134.67-.2 1.05-.2.326 0 .633.056.92.17.286.106.536.27.75.49.22.213.393.476.52.79.126.306.19.66.19 1.06v.13c0 .033-.004.076-.01.13h-4.1c.006.233.053.45.14.65.093.2.213.373.36.52.153.146.333.263.54.35.213.08.443.12.69.12.386 0 .703-.077.95-.23.253-.16.456-.374.61-.64l.68.47c-.227.366-.53.656-.91.87-.374.213-.82.32-1.34.32Zm1.51-3.13a1.579 1.579 0 0 0-.19-.55c-.087-.16-.2-.297-.34-.41a1.419 1.419 0 0 0-.46-.26 1.639 1.639 0 0 0-.54-.09 1.729 1.729 0 0 0-1.04.35 1.5 1.5 0 0 0-.38.41c-.107.16-.18.343-.22.55h3.17Zm4.16 3.13c-.353 0-.68-.067-.98-.2a2.57 2.57 0 0 1-.77-.56 2.73 2.73 0 0 1-.52-.83 2.8 2.8 0 0 1-.18-1.01c0-.36.06-.697.18-1.01.127-.314.3-.587.52-.82a2.359 2.359 0 0 1 1.75-.77c.38 0 .727.086 1.04.26.314.166.554.37.72.61v-3.27h.86v7.5h-.86v-.77a2.08 2.08 0 0 1-.72.62 2.18 2.18 0 0 1-1.04.25Zm.13-.79a1.607 1.607 0 0 0 1.22-.52c.154-.167.27-.36.35-.58.087-.22.13-.457.13-.71 0-.254-.043-.49-.13-.71-.08-.22-.196-.41-.35-.57a1.544 1.544 0 0 0-.53-.39 1.658 1.658 0 0 0-.69-.14c-.253 0-.486.046-.7.14a1.648 1.648 0 0 0-.54.39 1.82 1.82 0 0 0-.35.57c-.08.22-.12.456-.12.71 0 .253.04.49.12.71.087.22.204.413.35.58.154.16.334.286.54.38.214.093.447.14.7.14Zm4.55-5.65a.599.599 0 0 1-.62-.61c0-.167.06-.31.18-.43s.266-.18.44-.18c.172 0 .316.06.43.18.112.12.17.263.17.43 0 .173-.058.32-.17.44a.583.583 0 0 1-.43.17Zm-.44 1.34h.86v5h-.86v-5Zm4.26 5.1c-.274 0-.524-.047-.75-.14-.227-.1-.42-.237-.58-.41a1.917 1.917 0 0 1-.38-.63 2.617 2.617 0 0 1-.13-.85v-3.07h.86v2.94c0 .42.1.753.3 1 .206.246.483.37.83.37.193 0 .373-.04.54-.12.173-.087.32-.207.44-.36.126-.16.226-.354.3-.58.073-.227.11-.484.11-.77v-2.48h.86v5h-.86v-.77a1.654 1.654 0 0 1-.66.66c-.267.14-.56.21-.88.21Zm10.46-3.04c0-.407-.083-.737-.25-.99-.166-.254-.413-.38-.74-.38-.4 0-.726.156-.98.47-.246.313-.376.74-.39 1.28v2.56h-.86v-2.94c0-.407-.083-.737-.25-.99-.16-.254-.403-.38-.73-.38-.406 0-.74.163-1 .49-.253.326-.38.773-.38 1.34v2.48h-.86v-5h.86v.77a1.63 1.63 0 0 1 .61-.63c.26-.16.56-.24.9-.24.374 0 .69.096.95.29.267.186.464.446.59.78.134-.327.344-.587.63-.78.294-.194.63-.29 1.01-.29.274 0 .517.05.73.15.22.093.404.23.55.41.154.173.27.386.35.64.08.246.12.523.12.83v3.07h-.86v-2.94Z" fill="#037DD6"/><path opacity=".3" d="M19.506 40.566c-10.763 7.42-19.416 8-19.416 8h110.25s-8.653-.58-19.417-8c-10.763-7.42-23.363-22-35.708-22-12.345 0-24.945 14.58-35.709 22Z" fill="#037DD6"/><mask id="a" style="mask-type:alpha" maskUnits="userSpaceOnUse" x="0" y="18" width="111" height="31"><path d="M19.506 40.434c-10.763 7.42-19.416 8-19.416 8h110.25s-8.653-.58-19.417-8c-10.763-7.42-23.363-22-35.708-22-12.345 0-24.945 14.58-35.709 22Z" fill="#EAF6FF"/></mask><g mask="url(#a)"><path fill="#037DD6" stroke="#fff" stroke-width="2" d="M36.047 12.619H73.39v39.25H36.047z"/></g></svg>

After

Width:  |  Height:  |  Size: 3.0 KiB

@ -56,7 +56,7 @@ const openMetamaskTabsIDs = {};
const requestAccountTabIds = {};
// state persistence
const inTest = process.env.IN_TEST === 'true';
const inTest = process.env.IN_TEST;
const localStore = inTest ? new ReadOnlyNetworkStore() : new LocalStore();
let versionedData;
@ -533,13 +533,8 @@ function setupController(initState, initLangCode) {
),
);
// We're specifcally avoid using approvalController directly for better
// Error support during rejection
Object.keys(
controller.permissionsController.approvals.state.pendingApprovals,
).forEach((approvalId) =>
controller.permissionsController.rejectPermissionsRequest(approvalId),
);
// Finally, reject all approvals managed by the ApprovalController
controller.approvalController.clear();
updateBadge();
}

@ -31,6 +31,7 @@ export default class AppStateController extends EventEmitter {
fullScreenGasPollTokens: [],
recoveryPhraseReminderHasBeenShown: false,
recoveryPhraseReminderLastShown: new Date().getTime(),
collectiblesDetectionNoticeDismissed: false,
showTestnetMessageInDropdown: true,
trezorModel: null,
...initState,
@ -252,4 +253,15 @@ export default class AppStateController extends EventEmitter {
setTrezorModel(trezorModel) {
this.store.updateState({ trezorModel });
}
/**
* A setter for the `collectiblesDetectionNoticeDismissed` property
*/
setCollectiblesDetectionNoticeDismissed(
collectiblesDetectionNoticeDismissed,
) {
this.store.updateState({
collectiblesDetectionNoticeDismissed,
});
}
}

@ -10,7 +10,7 @@ import {
import { PollingBlockTracker } from 'eth-block-tracker';
import { SECOND } from '../../../../shared/constants/time';
const inTest = process.env.IN_TEST === 'true';
const inTest = process.env.IN_TEST;
const blockTrackerOpts = inTest ? { pollingInterval: SECOND } : {};
const getTestMiddlewares = () => {
return inTest ? [createEstimateGasDelayTestMiddleware()] : [];

@ -1,10 +1,9 @@
import { strict as assert } from 'assert';
import sinon from 'sinon';
import { getNetworkDisplayName } from './util';
import NetworkController, { NETWORK_EVENTS } from './network';
describe('NetworkController', function () {
describe('controller', function () {
describe('NetworkController', () => {
describe('controller', () => {
let networkController;
let getLatestBlockStub;
let setProviderTypeAndWait;
@ -13,7 +12,7 @@ describe('NetworkController', function () {
getAccounts: noop,
};
beforeEach(function () {
beforeEach(() => {
networkController = new NetworkController();
getLatestBlockStub = sinon
.stub(networkController, 'getLatestBlock')
@ -28,118 +27,108 @@ describe('NetworkController', function () {
});
});
afterEach(function () {
afterEach(() => {
getLatestBlockStub.reset();
});
describe('#provider', function () {
it('provider should be updatable without reassignment', function () {
describe('#provider', () => {
it('provider should be updatable without reassignment', () => {
networkController.initializeProvider(networkControllerProviderConfig);
const providerProxy = networkController.getProviderAndBlockTracker()
.provider;
assert.equal(providerProxy.test, undefined);
expect(providerProxy.test).toBeUndefined();
providerProxy.setTarget({ test: true });
assert.equal(providerProxy.test, true);
expect(providerProxy.test).toStrictEqual(true);
});
});
describe('#getNetworkState', function () {
it('should return "loading" when new', function () {
describe('#getNetworkState', () => {
it('should return "loading" when new', () => {
const networkState = networkController.getNetworkState();
assert.equal(networkState, 'loading', 'network is loading');
expect(networkState).toStrictEqual('loading');
});
});
describe('#setNetworkState', function () {
it('should update the network', function () {
describe('#setNetworkState', () => {
it('should update the network', () => {
networkController.setNetworkState('1');
const networkState = networkController.getNetworkState();
assert.equal(networkState, '1', 'network is 1');
expect(networkState).toStrictEqual('1');
});
});
describe('#setProviderType', function () {
it('should update provider.type', function () {
describe('#setProviderType', () => {
it('should update provider.type', () => {
networkController.initializeProvider(networkControllerProviderConfig);
networkController.setProviderType('mainnet');
const { type } = networkController.getProviderConfig();
assert.equal(type, 'mainnet', 'provider type is updated');
expect(type).toStrictEqual('mainnet');
});
it('should set the network to loading', function () {
it('should set the network to loading', () => {
networkController.initializeProvider(networkControllerProviderConfig);
const spy = sinon.spy(networkController, 'setNetworkState');
networkController.setProviderType('mainnet');
assert.equal(
spy.callCount,
1,
'should have called setNetworkState 2 times',
);
assert.ok(
spy.calledOnceWithExactly('loading'),
'should have called with "loading" first',
);
expect(spy.callCount).toStrictEqual(1);
expect(spy.calledOnceWithExactly('loading')).toStrictEqual(true);
});
});
describe('#getEIP1559Compatibility', function () {
it('should return false when baseFeePerGas is not in the block header', async function () {
describe('#getEIP1559Compatibility', () => {
it('should return false when baseFeePerGas is not in the block header', async () => {
networkController.initializeProvider(networkControllerProviderConfig);
const supportsEIP1559 = await networkController.getEIP1559Compatibility();
assert.equal(supportsEIP1559, false);
expect(supportsEIP1559).toStrictEqual(false);
});
it('should return true when baseFeePerGas is in block header', async function () {
it('should return true when baseFeePerGas is in block header', async () => {
networkController.initializeProvider(networkControllerProviderConfig);
getLatestBlockStub.callsFake(() =>
Promise.resolve({ baseFeePerGas: '0xa ' }),
);
const supportsEIP1559 = await networkController.getEIP1559Compatibility();
assert.equal(supportsEIP1559, true);
expect(supportsEIP1559).toStrictEqual(true);
});
it('should store EIP1559 support in state to reduce calls to getLatestBlock', async function () {
it('should store EIP1559 support in state to reduce calls to getLatestBlock', async () => {
networkController.initializeProvider(networkControllerProviderConfig);
getLatestBlockStub.callsFake(() =>
Promise.resolve({ baseFeePerGas: '0xa ' }),
);
await networkController.getEIP1559Compatibility();
const supportsEIP1559 = await networkController.getEIP1559Compatibility();
assert.equal(getLatestBlockStub.calledOnce, true);
assert.equal(supportsEIP1559, true);
expect(getLatestBlockStub.calledOnce).toStrictEqual(true);
expect(supportsEIP1559).toStrictEqual(true);
});
it('should clear stored EIP1559 support when changing networks', async function () {
it('should clear stored EIP1559 support when changing networks', async () => {
networkController.initializeProvider(networkControllerProviderConfig);
networkController.consoleThis = true;
getLatestBlockStub.callsFake(() =>
Promise.resolve({ baseFeePerGas: '0xa ' }),
);
await networkController.getEIP1559Compatibility();
assert.equal(
expect(
networkController.networkDetails.getState().EIPS[1559],
true,
);
).toStrictEqual(true);
getLatestBlockStub.callsFake(() => Promise.resolve({}));
await setProviderTypeAndWait('mainnet');
assert.equal(
expect(
networkController.networkDetails.getState().EIPS[1559],
undefined,
);
).toBeUndefined();
await networkController.getEIP1559Compatibility();
assert.equal(
expect(
networkController.networkDetails.getState().EIPS[1559],
false,
);
assert.equal(getLatestBlockStub.calledTwice, true);
).toStrictEqual(false);
expect(getLatestBlockStub.calledTwice).toStrictEqual(true);
});
});
});
describe('utils', function () {
it('getNetworkDisplayName should return the correct network name', function () {
describe('utils', () => {
it('getNetworkDisplayName should return the correct network name', () => {
const tests = [
{
input: '3',
@ -188,7 +177,7 @@ describe('NetworkController', function () {
];
tests.forEach(({ input, expected }) =>
assert.equal(getNetworkDisplayName(input), expected),
expect(getNetworkDisplayName(input)).toStrictEqual(expected),
);
});
});

@ -33,7 +33,7 @@ const env = process.env.METAMASK_ENV;
const fetchWithTimeout = getFetchWithTimeout(SECOND * 30);
let defaultProviderConfigOpts;
if (process.env.IN_TEST === 'true') {
if (process.env.IN_TEST) {
defaultProviderConfigOpts = {
type: NETWORK_TYPE_RPC,
rpcUrl: 'http://localhost:8545',

@ -1,4 +1,3 @@
import { strict as assert } from 'assert';
import { GAS_LIMITS } from '../../../../shared/constants/gas';
import { TRANSACTION_ENVELOPE_TYPES } from '../../../../shared/constants/transaction';
import { txMetaStub } from '../../../../test/stub/tx-meta-stub';
@ -7,25 +6,35 @@ import {
createPendingTxMiddleware,
} from './middleware/pending';
describe('PendingNonceMiddleware', function () {
describe('#createPendingNonceMiddleware', function () {
describe('PendingNonceMiddleware', () => {
describe('#createPendingNonceMiddleware', () => {
const getPendingNonce = async () => '0x2';
const address = '0xF231D46dD78806E1DD93442cf33C7671f8538748';
const pendingNonceMiddleware = createPendingNonceMiddleware({
getPendingNonce,
});
it('should call next if not a eth_getTransactionCount request', function (done) {
it('should call next if not a eth_getTransactionCount request', () => {
const req = { method: 'eth_getBlockByNumber' };
const res = {};
pendingNonceMiddleware(req, res, () => done());
const next = jest.fn();
pendingNonceMiddleware(req, res, next);
expect(next).toHaveBeenCalledTimes(1);
});
it('should call next if not a "pending" block request', function (done) {
it('should call next if not a "pending" block request', () => {
const req = { method: 'eth_getTransactionCount', params: [address] };
const res = {};
pendingNonceMiddleware(req, res, () => done());
const next = jest.fn();
pendingNonceMiddleware(req, res, next);
expect(next).toHaveBeenCalledTimes(1);
});
it('should fill the result with a the "pending" nonce', function (done) {
it('should fill the result with a the "pending" nonce', () => {
const req = {
method: 'eth_getTransactionCount',
params: [address, 'pending'],
@ -35,17 +44,16 @@ describe('PendingNonceMiddleware', function () {
req,
res,
() => {
done(new Error('should not have called next'));
return new Error('should not have called next');
},
() => {
assert(res.result === '0x2');
done();
expect(res.result).toStrictEqual('0x2');
},
);
});
});
describe('#createPendingTxMiddleware', function () {
describe('#createPendingTxMiddleware', () => {
let returnUndefined = true;
const getPendingTransactionByHash = () =>
returnUndefined ? undefined : txMetaStub;
@ -72,19 +80,24 @@ describe('PendingNonceMiddleware', function () {
r: '0x5f973e540f2d3c2f06d3725a626b75247593cb36477187ae07ecfe0a4db3cf57',
s: '0x0259b52ee8c58baaa385fb05c3f96116e58de89bcc165cb3bfdfc708672fed8a',
};
it('should call next if not a eth_getTransactionByHash request', function (done) {
it('should call next if not a eth_getTransactionByHash request', () => {
const req = { method: 'eth_getBlockByNumber' };
const res = {};
pendingTxMiddleware(req, res, () => done());
const next = jest.fn();
pendingTxMiddleware(req, res, next);
expect(next).toHaveBeenCalledTimes(1);
});
it('should call next if no pending txMeta is in history', function (done) {
it('should call next if no pending txMeta is in history', () => {
const req = { method: 'eth_getTransactionByHash', params: [address] };
const res = {};
pendingTxMiddleware(req, res, () => done());
const next = jest.fn();
pendingTxMiddleware(req, res, next);
expect(next).toHaveBeenCalledTimes(1);
});
it('should fill the result with a the "pending" tx the result should match the rpc spec', function (done) {
it('should fill the result with a the "pending" tx the result should match the rpc spec', () => {
returnUndefined = false;
const req = {
method: 'eth_getTransactionByHash',
@ -95,15 +108,10 @@ describe('PendingNonceMiddleware', function () {
req,
res,
() => {
done(new Error('should not have called next'));
return new Error('should not have called next');
},
() => {
assert.deepStrictEqual(
res.result,
spec,
new Error('result does not match the spec object'),
);
done();
expect(res.result).toStrictEqual(spec);
},
);
});

@ -1,4 +1,3 @@
import { strict as assert } from 'assert';
import {
TRANSACTION_STATUSES,
TRANSACTION_TYPES,
@ -7,9 +6,9 @@ import {
import { formatTxMetaForRpcResult } from './util';
describe('network utils', function () {
describe('formatTxMetaForRpcResult', function () {
it('should correctly format the tx meta object (EIP-1559)', function () {
describe('network utils', () => {
describe('formatTxMetaForRpcResult', () => {
it('should correctly format the tx meta object (EIP-1559)', () => {
const txMeta = {
id: 1,
status: TRANSACTION_STATUSES.UNAPPROVED,
@ -54,10 +53,10 @@ describe('network utils', function () {
value: '0x0',
};
const result = formatTxMetaForRpcResult(txMeta);
assert.deepEqual(result, expectedResult);
expect(result).toStrictEqual(expectedResult);
});
it('should correctly format the tx meta object (non EIP-1559)', function () {
it('should correctly format the tx meta object (non EIP-1559)', () => {
const txMeta = {
id: 1,
status: TRANSACTION_STATUSES.UNAPPROVED,
@ -99,7 +98,7 @@ describe('network utils', function () {
value: '0x0',
};
const result = formatTxMetaForRpcResult(txMeta);
assert.deepEqual(result, expectedResult);
expect(result).toStrictEqual(expectedResult);
});
});
});

@ -0,0 +1,71 @@
import {
CaveatTypes,
RestrictedMethods,
} from '../../../../shared/constants/permissions';
export function getPermissionBackgroundApiMethods(permissionController) {
return {
addPermittedAccount: (origin, account) => {
const existing = permissionController.getCaveat(
origin,
RestrictedMethods.eth_accounts,
CaveatTypes.restrictReturnedAccounts,
);
if (existing.value.includes(account)) {
throw new Error(
`eth_accounts permission for origin "${origin}" already permits account "${account}".`,
);
}
permissionController.updateCaveat(
origin,
RestrictedMethods.eth_accounts,
CaveatTypes.restrictReturnedAccounts,
[...existing.value, account],
);
},
removePermittedAccount: (origin, account) => {
const existing = permissionController.getCaveat(
origin,
RestrictedMethods.eth_accounts,
CaveatTypes.restrictReturnedAccounts,
);
if (!existing.value.includes(account)) {
throw new Error(
`eth_accounts permission for origin "${origin}" already does not permit account "${account}".`,
);
}
const remainingAccounts = existing.value.filter(
(existingAccount) => existingAccount !== account,
);
if (remainingAccounts.length === 0) {
permissionController.revokePermission(
origin,
RestrictedMethods.eth_accounts,
);
} else {
permissionController.updateCaveat(
origin,
RestrictedMethods.eth_accounts,
CaveatTypes.restrictReturnedAccounts,
remainingAccounts,
);
}
},
requestAccountsPermissionWithId: async (origin) => {
const [, { id }] = await permissionController.requestPermissions(
{ origin },
{
eth_accounts: {},
},
);
return id;
},
};
}

@ -0,0 +1,181 @@
import {
CaveatTypes,
RestrictedMethods,
} from '../../../../shared/constants/permissions';
import { getPermissionBackgroundApiMethods } from './background-api';
describe('permission background API methods', () => {
describe('addPermittedAccount', () => {
it('adds a permitted account', () => {
const permissionController = {
getCaveat: jest.fn().mockImplementationOnce(() => {
return { type: CaveatTypes.restrictReturnedAccounts, value: ['0x1'] };
}),
updateCaveat: jest.fn(),
};
getPermissionBackgroundApiMethods(
permissionController,
).addPermittedAccount('foo.com', '0x2');
expect(permissionController.getCaveat).toHaveBeenCalledTimes(1);
expect(permissionController.getCaveat).toHaveBeenCalledWith(
'foo.com',
RestrictedMethods.eth_accounts,
CaveatTypes.restrictReturnedAccounts,
);
expect(permissionController.updateCaveat).toHaveBeenCalledTimes(1);
expect(permissionController.updateCaveat).toHaveBeenCalledWith(
'foo.com',
RestrictedMethods.eth_accounts,
CaveatTypes.restrictReturnedAccounts,
['0x1', '0x2'],
);
});
it('throws if the specified account is already permitted', () => {
const permissionController = {
getCaveat: jest.fn().mockImplementationOnce(() => {
return { type: CaveatTypes.restrictReturnedAccounts, value: ['0x1'] };
}),
updateCaveat: jest.fn(),
};
expect(() =>
getPermissionBackgroundApiMethods(
permissionController,
).addPermittedAccount('foo.com', '0x1'),
).toThrow(
`eth_accounts permission for origin "foo.com" already permits account "0x1".`,
);
expect(permissionController.getCaveat).toHaveBeenCalledTimes(1);
expect(permissionController.getCaveat).toHaveBeenCalledWith(
'foo.com',
RestrictedMethods.eth_accounts,
CaveatTypes.restrictReturnedAccounts,
);
expect(permissionController.updateCaveat).not.toHaveBeenCalled();
});
});
describe('removePermittedAccount', () => {
it('removes a permitted account', () => {
const permissionController = {
getCaveat: jest.fn().mockImplementationOnce(() => {
return {
type: CaveatTypes.restrictReturnedAccounts,
value: ['0x1', '0x2'],
};
}),
revokePermission: jest.fn(),
updateCaveat: jest.fn(),
};
getPermissionBackgroundApiMethods(
permissionController,
).removePermittedAccount('foo.com', '0x2');
expect(permissionController.getCaveat).toHaveBeenCalledTimes(1);
expect(permissionController.getCaveat).toHaveBeenCalledWith(
'foo.com',
RestrictedMethods.eth_accounts,
CaveatTypes.restrictReturnedAccounts,
);
expect(permissionController.revokePermission).not.toHaveBeenCalled();
expect(permissionController.updateCaveat).toHaveBeenCalledTimes(1);
expect(permissionController.updateCaveat).toHaveBeenCalledWith(
'foo.com',
RestrictedMethods.eth_accounts,
CaveatTypes.restrictReturnedAccounts,
['0x1'],
);
});
it('revokes the accounts permission if the removed account is the only permitted account', () => {
const permissionController = {
getCaveat: jest.fn().mockImplementationOnce(() => {
return {
type: CaveatTypes.restrictReturnedAccounts,
value: ['0x1'],
};
}),
revokePermission: jest.fn(),
updateCaveat: jest.fn(),
};
getPermissionBackgroundApiMethods(
permissionController,
).removePermittedAccount('foo.com', '0x1');
expect(permissionController.getCaveat).toHaveBeenCalledTimes(1);
expect(permissionController.getCaveat).toHaveBeenCalledWith(
'foo.com',
RestrictedMethods.eth_accounts,
CaveatTypes.restrictReturnedAccounts,
);
expect(permissionController.revokePermission).toHaveBeenCalledTimes(1);
expect(permissionController.revokePermission).toHaveBeenCalledWith(
'foo.com',
RestrictedMethods.eth_accounts,
);
expect(permissionController.updateCaveat).not.toHaveBeenCalled();
});
it('throws if the specified account is not permitted', () => {
const permissionController = {
getCaveat: jest.fn().mockImplementationOnce(() => {
return { type: CaveatTypes.restrictReturnedAccounts, value: ['0x1'] };
}),
revokePermission: jest.fn(),
updateCaveat: jest.fn(),
};
expect(() =>
getPermissionBackgroundApiMethods(
permissionController,
).removePermittedAccount('foo.com', '0x2'),
).toThrow(
`eth_accounts permission for origin "foo.com" already does not permit account "0x2".`,
);
expect(permissionController.getCaveat).toHaveBeenCalledTimes(1);
expect(permissionController.getCaveat).toHaveBeenCalledWith(
'foo.com',
RestrictedMethods.eth_accounts,
CaveatTypes.restrictReturnedAccounts,
);
expect(permissionController.revokePermission).not.toHaveBeenCalled();
expect(permissionController.updateCaveat).not.toHaveBeenCalled();
});
});
describe('requestAccountsPermissionWithId', () => {
it('request an accounts permission and returns the request id', async () => {
const permissionController = {
requestPermissions: jest.fn().mockImplementationOnce(async () => {
return [null, { id: 'arbitraryId' }];
}),
};
const id = await getPermissionBackgroundApiMethods(
permissionController,
).requestAccountsPermissionWithId('foo.com');
expect(permissionController.requestPermissions).toHaveBeenCalledTimes(1);
expect(permissionController.requestPermissions).toHaveBeenCalledWith(
{ origin: 'foo.com' },
{ eth_accounts: {} },
);
expect(id).toStrictEqual('arbitraryId');
});
});
});

@ -0,0 +1,39 @@
import { CaveatMutatorOperation } from '@metamask/snap-controllers';
import { CaveatTypes } from '../../../../shared/constants/permissions';
/**
* Factories that construct caveat mutator functions that are passed to
* PermissionController.updatePermissionsByCaveat.
*/
export const CaveatMutatorFactories = {
[CaveatTypes.restrictReturnedAccounts]: {
removeAccount,
},
};
/**
* Removes the target account from the value arrays of all
* `restrictReturnedAccounts` caveats. No-ops if the target account is not in
* the array, and revokes the parent permission if it's the only account in
* the array.
*
* @param {string} targetAccount - The address of the account to remove from
* all accounts permissions.
* @param {string[]} existingAccounts - The account address array from the
* account permissions.
*/
function removeAccount(targetAccount, existingAccounts) {
const newAccounts = existingAccounts.filter(
(address) => address !== targetAccount,
);
if (newAccounts.length === existingAccounts.length) {
return { operation: CaveatMutatorOperation.noop };
} else if (newAccounts.length > 0) {
return {
operation: CaveatMutatorOperation.updateValue,
value: newAccounts,
};
}
return { operation: CaveatMutatorOperation.revokePermission };
}

@ -0,0 +1,32 @@
import { CaveatMutatorOperation } from '@metamask/snap-controllers';
import { CaveatTypes } from '../../../../shared/constants/permissions';
import { CaveatMutatorFactories } from './caveat-mutators';
describe('caveat mutators', () => {
describe('restrictReturnedAccounts', () => {
const { removeAccount } = CaveatMutatorFactories[
CaveatTypes.restrictReturnedAccounts
];
describe('removeAccount', () => {
it('returns the no-op operation if the target account is not permitted', () => {
expect(removeAccount('0x2', ['0x1'])).toStrictEqual({
operation: CaveatMutatorOperation.noop,
});
});
it('returns the update operation and a new value if the target account is permitted', () => {
expect(removeAccount('0x2', ['0x1', '0x2'])).toStrictEqual({
operation: CaveatMutatorOperation.updateValue,
value: ['0x1'],
});
});
it('returns the revoke permission operation the target account is the only permitted account', () => {
expect(removeAccount('0x1', ['0x1'])).toStrictEqual({
operation: CaveatMutatorOperation.revokePermission,
});
});
});
});
});

@ -1,20 +1,5 @@
export const APPROVAL_TYPE = 'wallet_requestPermissions';
export const WALLET_PREFIX = 'wallet_';
export const HISTORY_STORE_KEY = 'permissionsHistory';
export const LOG_STORE_KEY = 'permissionsLog';
export const METADATA_STORE_KEY = 'domainMetadata';
export const METADATA_CACHE_MAX_SIZE = 100;
export const CAVEAT_TYPES = {
limitResponseLength: 'limitResponseLength',
filterResponse: 'filterResponse',
};
export const NOTIFICATION_NAMES = {
accountsChanged: 'metamask_accountsChanged',
unlockStateChanged: 'metamask_unlockStateChanged',
@ -31,64 +16,7 @@ export const LOG_METHOD_TYPES = {
internal: 'internal',
};
/**
* The permission activity log size limit.
*/
export const LOG_LIMIT = 100;
export const SAFE_METHODS = [
'eth_blockNumber',
'eth_call',
'eth_chainId',
'eth_coinbase',
'eth_decrypt',
'eth_estimateGas',
'eth_feeHistory',
'eth_gasPrice',
'eth_getBalance',
'eth_getBlockByHash',
'eth_getBlockByNumber',
'eth_getBlockTransactionCountByHash',
'eth_getBlockTransactionCountByNumber',
'eth_getCode',
'eth_getEncryptionPublicKey',
'eth_getFilterChanges',
'eth_getFilterLogs',
'eth_getLogs',
'eth_getProof',
'eth_getStorageAt',
'eth_getTransactionByBlockHashAndIndex',
'eth_getTransactionByBlockNumberAndIndex',
'eth_getTransactionByHash',
'eth_getTransactionCount',
'eth_getTransactionReceipt',
'eth_getUncleByBlockHashAndIndex',
'eth_getUncleByBlockNumberAndIndex',
'eth_getUncleCountByBlockHash',
'eth_getUncleCountByBlockNumber',
'eth_getWork',
'eth_hashrate',
'eth_mining',
'eth_newBlockFilter',
'eth_newFilter',
'eth_newPendingTransactionFilter',
'eth_protocolVersion',
'eth_sendRawTransaction',
'eth_sendTransaction',
'eth_sign',
'eth_signTypedData',
'eth_signTypedData_v1',
'eth_signTypedData_v3',
'eth_signTypedData_v4',
'eth_submitHashrate',
'eth_submitWork',
'eth_syncing',
'eth_uninstallFilter',
'metamask_getProviderState',
'metamask_watchAsset',
'net_listening',
'net_peerCount',
'net_version',
'personal_ecRecover',
'personal_sign',
'wallet_watchAsset',
'web3_clientVersion',
'web3_sha3',
];

@ -1,718 +1,6 @@
import nanoid from 'nanoid';
import { JsonRpcEngine } from 'json-rpc-engine';
import { ObservableStore } from '@metamask/obs-store';
import log from 'loglevel';
import { CapabilitiesController as RpcCap } from 'rpc-cap';
import { ethErrors } from 'eth-rpc-errors';
import { cloneDeep } from 'lodash';
import { CAVEAT_NAMES } from '../../../../shared/constants/permissions';
import {
APPROVAL_TYPE,
SAFE_METHODS, // methods that do not require any permissions to use
WALLET_PREFIX,
METADATA_STORE_KEY,
METADATA_CACHE_MAX_SIZE,
LOG_STORE_KEY,
HISTORY_STORE_KEY,
NOTIFICATION_NAMES,
CAVEAT_TYPES,
} from './enums';
import createPermissionsMethodMiddleware from './permissionsMethodMiddleware';
import PermissionsLogController from './permissionsLog';
// instanbul ignore next
const noop = () => undefined;
export class PermissionsController {
constructor(
{
approvals,
getKeyringAccounts,
getRestrictedMethods,
getUnlockPromise,
isUnlocked,
notifyDomain,
notifyAllDomains,
preferences,
} = {},
restoredPermissions = {},
restoredState = {},
) {
// additional top-level store key set in _initializeMetadataStore
this.store = new ObservableStore({
[LOG_STORE_KEY]: restoredState[LOG_STORE_KEY] || [],
[HISTORY_STORE_KEY]: restoredState[HISTORY_STORE_KEY] || {},
});
this.getKeyringAccounts = getKeyringAccounts;
this._getUnlockPromise = getUnlockPromise;
this._notifyDomain = notifyDomain;
this._notifyAllDomains = notifyAllDomains;
this._isUnlocked = isUnlocked;
this._restrictedMethods = getRestrictedMethods({
getKeyringAccounts: this.getKeyringAccounts.bind(this),
getIdentities: this._getIdentities.bind(this),
});
this.permissionsLog = new PermissionsLogController({
restrictedMethods: Object.keys(this._restrictedMethods),
store: this.store,
});
/**
* @type {import('@metamask/controllers').ApprovalController}
* @public
*/
this.approvals = approvals;
this._initializePermissions(restoredPermissions);
this._lastSelectedAddress = preferences.getState().selectedAddress;
this.preferences = preferences;
this._initializeMetadataStore(restoredState);
preferences.subscribe(async ({ selectedAddress }) => {
if (selectedAddress && selectedAddress !== this._lastSelectedAddress) {
this._lastSelectedAddress = selectedAddress;
await this._handleAccountSelected(selectedAddress);
}
});
}
createMiddleware({ origin, extensionId }) {
if (typeof origin !== 'string' || !origin.length) {
throw new Error('Must provide non-empty string origin.');
}
const metadataState = this.store.getState()[METADATA_STORE_KEY];
if (extensionId && metadataState[origin]?.extensionId !== extensionId) {
this.addDomainMetadata(origin, { extensionId });
}
const engine = new JsonRpcEngine();
engine.push(this.permissionsLog.createMiddleware());
engine.push(
createPermissionsMethodMiddleware({
addDomainMetadata: this.addDomainMetadata.bind(this),
getAccounts: this.getAccounts.bind(this, origin),
getUnlockPromise: () => this._getUnlockPromise(true),
hasPermission: this.hasPermission.bind(this, origin),
notifyAccountsChanged: this.notifyAccountsChanged.bind(this, origin),
requestAccountsPermission: this._requestPermissions.bind(
this,
{ origin },
{ eth_accounts: {} },
),
}),
);
engine.push(
this.permissions.providerMiddlewareFunction.bind(this.permissions, {
origin,
}),
);
return engine.asMiddleware();
}
/**
* Request {@code eth_accounts} permissions
* @param {string} origin - The requesting origin
* @returns {Promise<string>} The permissions request ID
*/
async requestAccountsPermissionWithId(origin) {
const id = nanoid();
this._requestPermissions({ origin }, { eth_accounts: {} }, id).then(
async () => {
const permittedAccounts = await this.getAccounts(origin);
this.notifyAccountsChanged(origin, permittedAccounts);
},
);
return id;
}
/**
* Returns the accounts that should be exposed for the given origin domain,
* if any. This method exists for when a trusted context needs to know
* which accounts are exposed to a given domain.
*
* @param {string} origin - The origin string.
*/
getAccounts(origin) {
return new Promise((resolve, _) => {
const req = { method: 'eth_accounts' };
const res = {};
this.permissions.providerMiddlewareFunction(
{ origin },
req,
res,
noop,
_end,
);
function _end() {
if (res.error || !Array.isArray(res.result)) {
resolve([]);
} else {
resolve(res.result);
}
}
});
}
/**
* Returns whether the given origin has the given permission.
*
* @param {string} origin - The origin to check.
* @param {string} permission - The permission to check for.
* @returns {boolean} Whether the origin has the permission.
*/
hasPermission(origin, permission) {
return Boolean(this.permissions.getPermission(origin, permission));
}
/**
* Gets the identities from the preferences controller store
*
* @returns {Object} identities
*/
_getIdentities() {
return this.preferences.getState().identities;
}
/**
* Submits a permissions request to rpc-cap. Internal, background use only.
*
* @param {IOriginMetadata} domain - The external domain metadata.
* @param {IRequestedPermissions} permissions - The requested permissions.
* @param {string} [id] - The desired id of the permissions request, if any.
* @returns {Promise<IOcapLdCapability[]>} A Promise that resolves with the
* approved permissions, or rejects with an error.
*/
_requestPermissions(domain, permissions, id) {
return new Promise((resolve, reject) => {
// rpc-cap assigns an id to the request if there is none, as expected by
// requestUserApproval below
const req = {
id,
method: 'wallet_requestPermissions',
params: [permissions],
};
const res = {};
this.permissions.providerMiddlewareFunction(domain, req, res, noop, _end);
function _end(_err) {
const err = _err || res.error;
if (err) {
reject(err);
} else {
resolve(res.result);
}
}
});
}
/**
* User approval callback. Resolves the Promise for the permissions request
* waited upon by rpc-cap, see requestUserApproval in _initializePermissions.
* The request will be rejected if finalizePermissionsRequest fails.
* Idempotent for a given request id.
*
* @param {Object} approved - The request object approved by the user
* @param {Array} accounts - The accounts to expose, if any
*/
async approvePermissionsRequest(approved, accounts) {
const { id } = approved.metadata;
if (!this.approvals.has({ id })) {
log.debug(`Permissions request with id '${id}' not found.`);
return;
}
try {
if (Object.keys(approved.permissions).length === 0) {
this.approvals.reject(
id,
ethErrors.rpc.invalidRequest({
message: 'Must request at least one permission.',
}),
);
} else {
// attempt to finalize the request and resolve it,
// settings caveats as necessary
approved.permissions = await this.finalizePermissionsRequest(
approved.permissions,
accounts,
);
this.approvals.accept(id, approved.permissions);
}
} catch (err) {
// if finalization fails, reject the request
this.approvals.reject(
id,
ethErrors.rpc.invalidRequest({
message: err.message,
data: err,
}),
);
}
}
/**
* User rejection callback. Rejects the Promise for the permissions request
* waited upon by rpc-cap, see requestUserApproval in _initializePermissions.
* Idempotent for a given id.
*
* @param {string} id - The id of the request rejected by the user
*/
async rejectPermissionsRequest(id) {
if (!this.approvals.has({ id })) {
log.debug(`Permissions request with id '${id}' not found.`);
return;
}
this.approvals.reject(id, ethErrors.provider.userRejectedRequest());
}
/**
* Expose an account to the given origin. Changes the eth_accounts
* permissions and emits accountsChanged.
*
* Throws error if the origin or account is invalid, or if the update fails.
*
* @param {string} origin - The origin to expose the account to.
* @param {string} account - The new account to expose.
*/
async addPermittedAccount(origin, account) {
const domains = this.permissions.getDomains();
if (!domains[origin]) {
throw new Error('Unrecognized domain');
}
this.validatePermittedAccounts([account]);
const oldPermittedAccounts = this._getPermittedAccounts(origin);
if (oldPermittedAccounts.length === 0) {
throw new Error(`Origin does not have 'eth_accounts' permission`);
} else if (oldPermittedAccounts.includes(account)) {
throw new Error('Account is already permitted for origin');
}
this.permissions.updateCaveatFor(
origin,
'eth_accounts',
CAVEAT_NAMES.exposedAccounts,
[...oldPermittedAccounts, account],
);
const permittedAccounts = await this.getAccounts(origin);
this.notifyAccountsChanged(origin, permittedAccounts);
}
/**
* Removes an exposed account from the given origin. Changes the eth_accounts
* permission and emits accountsChanged.
* If origin only has a single permitted account, removes the eth_accounts
* permission from the origin.
*
* Throws error if the origin or account is invalid, or if the update fails.
*
* @param {string} origin - The origin to remove the account from.
* @param {string} account - The account to remove.
*/
async removePermittedAccount(origin, account) {
const domains = this.permissions.getDomains();
if (!domains[origin]) {
throw new Error('Unrecognized domain');
}
this.validatePermittedAccounts([account]);
const oldPermittedAccounts = this._getPermittedAccounts(origin);
if (oldPermittedAccounts.length === 0) {
throw new Error(`Origin does not have 'eth_accounts' permission`);
} else if (!oldPermittedAccounts.includes(account)) {
throw new Error('Account is not permitted for origin');
}
let newPermittedAccounts = oldPermittedAccounts.filter(
(acc) => acc !== account,
);
if (newPermittedAccounts.length === 0) {
this.removePermissionsFor({ [origin]: ['eth_accounts'] });
} else {
this.permissions.updateCaveatFor(
origin,
'eth_accounts',
CAVEAT_NAMES.exposedAccounts,
newPermittedAccounts,
);
newPermittedAccounts = await this.getAccounts(origin);
}
this.notifyAccountsChanged(origin, newPermittedAccounts);
}
/**
* Remove all permissions associated with a particular account. Any eth_accounts
* permissions left with no permitted accounts will be removed as well.
*
* Throws error if the account is invalid, or if the update fails.
*
* @param {string} account - The account to remove.
*/
async removeAllAccountPermissions(account) {
this.validatePermittedAccounts([account]);
const domains = this.permissions.getDomains();
const connectedOrigins = Object.keys(domains).filter((origin) =>
this._getPermittedAccounts(origin).includes(account),
);
await Promise.all(
connectedOrigins.map((origin) =>
this.removePermittedAccount(origin, account),
),
);
}
/**
* Finalizes a permissions request. Throws if request validation fails.
* Clones the passed-in parameters to prevent inadvertent modification.
* Sets (adds or replaces) caveats for the following permissions:
* - eth_accounts: the permitted accounts caveat
*
* @param {Object} requestedPermissions - The requested permissions.
* @param {string[]} requestedAccounts - The accounts to expose, if any.
* @returns {Object} The finalized permissions request object.
*/
async finalizePermissionsRequest(requestedPermissions, requestedAccounts) {
const finalizedPermissions = cloneDeep(requestedPermissions);
const finalizedAccounts = cloneDeep(requestedAccounts);
const { eth_accounts: ethAccounts } = finalizedPermissions;
if (ethAccounts) {
this.validatePermittedAccounts(finalizedAccounts);
if (!ethAccounts.caveats) {
ethAccounts.caveats = [];
}
// caveat names are unique, and we will only construct this caveat here
ethAccounts.caveats = ethAccounts.caveats.filter(
(c) =>
c.name !== CAVEAT_NAMES.exposedAccounts &&
c.name !== CAVEAT_NAMES.primaryAccountOnly,
);
ethAccounts.caveats.push({
type: CAVEAT_TYPES.limitResponseLength,
value: 1,
name: CAVEAT_NAMES.primaryAccountOnly,
});
ethAccounts.caveats.push({
type: CAVEAT_TYPES.filterResponse,
value: finalizedAccounts,
name: CAVEAT_NAMES.exposedAccounts,
});
}
return finalizedPermissions;
}
/**
* Validate an array of accounts representing accounts to be exposed
* to a domain. Throws error if validation fails.
*
* @param {string[]} accounts - An array of addresses.
*/
validatePermittedAccounts(accounts) {
if (!Array.isArray(accounts) || accounts.length === 0) {
throw new Error('Must provide non-empty array of account(s).');
}
// assert accounts exist
const allIdentities = this._getIdentities();
accounts.forEach((acc) => {
if (!allIdentities[acc]) {
throw new Error(`Unknown account: ${acc}`);
}
});
}
/**
* Notify a domain that its permitted accounts have changed.
* Also updates the accounts history log.
*
* @param {string} origin - The origin of the domain to notify.
* @param {Array<string>} newAccounts - The currently permitted accounts.
*/
notifyAccountsChanged(origin, newAccounts) {
if (typeof origin !== 'string' || !origin) {
throw new Error(`Invalid origin: '${origin}'`);
}
if (!Array.isArray(newAccounts)) {
throw new Error('Invalid accounts', newAccounts);
}
// We do not share accounts when the extension is locked.
if (this._isUnlocked()) {
this._notifyDomain(origin, {
method: NOTIFICATION_NAMES.accountsChanged,
params: newAccounts,
});
this.permissionsLog.updateAccountsHistory(origin, newAccounts);
}
// NOTE:
// We don't check for accounts changing in the notifyAllDomains case,
// because the log only records when accounts were last seen, and the
// the accounts only change for all domains at once when permissions are
// removed.
}
/**
* Removes the given permissions for the given domain.
* Should only be called after confirming that the permissions exist, to
* avoid sending unnecessary notifications.
*
* @param {Object} domains - The map of domain origins to permissions to remove.
* e.g. { origin: [permissions] }
*/
removePermissionsFor(domains) {
Object.entries(domains).forEach(([origin, perms]) => {
this.permissions.removePermissionsFor(
origin,
perms.map((methodName) => {
if (methodName === 'eth_accounts') {
this.notifyAccountsChanged(origin, []);
}
return { parentCapability: methodName };
}),
);
});
}
/**
* Removes all known domains and their related permissions.
*/
clearPermissions() {
this.permissions.clearDomains();
// It's safe to notify that no accounts are available, regardless of
// extension lock state
this._notifyAllDomains({
method: NOTIFICATION_NAMES.accountsChanged,
params: [],
});
}
/**
* Stores domain metadata for the given origin (domain).
* Deletes metadata for domains without permissions in a FIFO manner, once
* more than 100 distinct origins have been added since boot.
* Metadata is never deleted for domains with permissions, to prevent a
* degraded user experience, since metadata cannot yet be requested on demand.
*
* @param {string} origin - The origin whose domain metadata to store.
* @param {Object} metadata - The domain's metadata that will be stored.
*/
addDomainMetadata(origin, metadata) {
const oldMetadataState = this.store.getState()[METADATA_STORE_KEY];
const newMetadataState = { ...oldMetadataState };
// delete pending metadata origin from queue, and delete its metadata if
// it doesn't have any permissions
if (this._pendingSiteMetadata.size >= METADATA_CACHE_MAX_SIZE) {
const permissionsDomains = this.permissions.getDomains();
const oldOrigin = this._pendingSiteMetadata.values().next().value;
this._pendingSiteMetadata.delete(oldOrigin);
if (!permissionsDomains[oldOrigin]) {
delete newMetadataState[oldOrigin];
}
}
// add new metadata to store after popping
newMetadataState[origin] = {
...oldMetadataState[origin],
...metadata,
lastUpdated: Date.now(),
};
if (
!newMetadataState[origin].extensionId &&
!newMetadataState[origin].host
) {
newMetadataState[origin].host = new URL(origin).host;
}
this._pendingSiteMetadata.add(origin);
this._setDomainMetadata(newMetadataState);
}
/**
* Removes all domains without permissions from the restored metadata state,
* and rehydrates the metadata store.
*
* Requires PermissionsController._initializePermissions to have been called first.
*
* @param {Object} restoredState - The restored permissions controller state.
*/
_initializeMetadataStore(restoredState) {
const metadataState = restoredState[METADATA_STORE_KEY] || {};
const newMetadataState = this._trimDomainMetadata(metadataState);
this._pendingSiteMetadata = new Set();
this._setDomainMetadata(newMetadataState);
}
/**
* Trims the given metadataState object by removing metadata for all origins
* without permissions.
* Returns a new object; does not mutate the argument.
*
* @param {Object} metadataState - The metadata store state object to trim.
* @returns {Object} The new metadata state object.
*/
_trimDomainMetadata(metadataState) {
const newMetadataState = { ...metadataState };
const origins = Object.keys(metadataState);
const permissionsDomains = this.permissions.getDomains();
origins.forEach((origin) => {
if (!permissionsDomains[origin]) {
delete newMetadataState[origin];
}
});
return newMetadataState;
}
/**
* Replaces the existing domain metadata with the passed-in object.
* @param {Object} newMetadataState - The new metadata to set.
*/
_setDomainMetadata(newMetadataState) {
this.store.updateState({ [METADATA_STORE_KEY]: newMetadataState });
}
/**
* Get current set of permitted accounts for the given origin
*
* @param {string} origin - The origin to obtain permitted accounts for
* @returns {Array<string>} The list of permitted accounts
*/
_getPermittedAccounts(origin) {
const permittedAccounts = this.permissions
.getPermission(origin, 'eth_accounts')
?.caveats?.find((caveat) => caveat.name === CAVEAT_NAMES.exposedAccounts)
?.value;
return permittedAccounts || [];
}
/**
* When a new account is selected in the UI, emit accountsChanged to each origin
* where the selected account is exposed.
*
* Note: This will emit "false positive" accountsChanged events, but they are
* handled by the inpage provider.
*
* @param {string} account - The newly selected account's address.
*/
async _handleAccountSelected(account) {
if (typeof account !== 'string') {
throw new Error('Selected account should be a non-empty string.');
}
const domains = this.permissions.getDomains() || {};
const connectedDomains = Object.entries(domains)
.filter(([_, { permissions }]) => {
const ethAccounts = permissions.find(
(permission) => permission.parentCapability === 'eth_accounts',
);
const exposedAccounts = ethAccounts?.caveats.find(
(caveat) => caveat.name === 'exposedAccounts',
)?.value;
return exposedAccounts?.includes(account);
})
.map(([domain]) => domain);
await Promise.all(
connectedDomains.map((origin) =>
this._handleConnectedAccountSelected(origin),
),
);
}
/**
* When a new account is selected in the UI, emit accountsChanged to 'origin'
*
* Note: This will emit "false positive" accountsChanged events, but they are
* handled by the inpage provider.
*
* @param {string} origin - The origin
*/
async _handleConnectedAccountSelected(origin) {
const permittedAccounts = await this.getAccounts(origin);
this.notifyAccountsChanged(origin, permittedAccounts);
}
/**
* A convenience method for retrieving a login object
* or creating a new one if needed.
*
* @param {string} origin - The origin string representing the domain.
*/
_initializePermissions(restoredState) {
// these permission requests are almost certainly stale
const initState = { ...restoredState, permissionsRequests: [] };
this.permissions = new RpcCap(
{
// Supports passthrough methods:
safeMethods: SAFE_METHODS,
// optional prefix for internal methods
methodPrefix: WALLET_PREFIX,
restrictedMethods: this._restrictedMethods,
/**
* A promise-returning callback used to determine whether to approve
* permissions requests or not.
*
* Currently only returns a boolean, but eventually should return any
* specific parameters or amendments to the permissions.
*
* @param {string} req - The internal rpc-cap user request object.
*/
requestUserApproval: async (req) => {
const {
metadata: { id, origin },
} = req;
return this.approvals.addAndShowApprovalRequest({
id,
origin,
type: APPROVAL_TYPE,
});
},
},
initState,
);
}
}
export * from './caveat-mutators';
export * from './background-api';
export * from './enums';
export * from './permission-log';
export * from './specifications';
export * from './selectors';

@ -1,11 +1,10 @@
import { ObservableStore } from '@metamask/obs-store';
import stringify from 'fast-safe-stringify';
import { CAVEAT_NAMES } from '../../../../shared/constants/permissions';
import { CaveatTypes } from '../../../../shared/constants/permissions';
import {
HISTORY_STORE_KEY,
LOG_IGNORE_METHODS,
LOG_LIMIT,
LOG_METHOD_TYPES,
LOG_STORE_KEY,
WALLET_PREFIX,
} from './enums';
@ -13,51 +12,59 @@ import {
* Controller with middleware for logging requests and responses to restricted
* and permissions-related methods.
*/
export default class PermissionsLogController {
constructor({ restrictedMethods, store }) {
export class PermissionLogController {
/**
* @param {{ restrictedMethods: Set<string>, initState: Record<string, unknown> }} options - Options bag.
*/
constructor({ restrictedMethods, initState }) {
this.restrictedMethods = restrictedMethods;
this.store = store;
this.store = new ObservableStore({
permissionHistory: {},
permissionActivityLog: [],
...initState,
});
}
/**
* Get the activity log.
* Get the restricted method activity log.
*
* @returns {Array<Object>} The activity log.
*/
getActivityLog() {
return this.store.getState()[LOG_STORE_KEY] || [];
return this.store.getState().permissionActivityLog;
}
/**
* Update the activity log.
* Update the restricted method activity log.
*
* @param {Array<Object>} logs - The new activity log array.
*/
updateActivityLog(logs) {
this.store.updateState({ [LOG_STORE_KEY]: logs });
this.store.updateState({ permissionActivityLog: logs });
}
/**
* Get the permissions history log.
* Get the permission history log.
*
* @returns {Object} The permissions history log.
*/
getHistory() {
return this.store.getState()[HISTORY_STORE_KEY] || {};
return this.store.getState().permissionHistory;
}
/**
* Update the permissions history log.
* Update the permission history log.
*
* @param {Object} history - The new permissions history log object.
*/
updateHistory(history) {
this.store.updateState({ [HISTORY_STORE_KEY]: history });
this.store.updateState({ permissionHistory: history });
}
/**
* Updates the exposed account history for the given origin.
* Sets the 'last seen' time to Date.now() for the given accounts.
* Does **not** update the 'lastApproved' time for the permission itself.
* Returns if the accounts array is empty.
*
* @param {string} origin - The origin that the accounts are exposed to.
@ -96,7 +103,7 @@ export default class PermissionsLogController {
// we only log certain methods
if (
!LOG_IGNORE_METHODS.includes(method) &&
(isInternal || this.restrictedMethods.includes(method))
(isInternal || this.restrictedMethods.has(method))
) {
activityEntry = this.logRequest(req, isInternal);
@ -341,7 +348,7 @@ export default class PermissionsLogController {
const accounts = new Set();
for (const caveat of perm.caveats) {
if (
caveat.name === CAVEAT_NAMES.exposedAccounts &&
caveat.type === CaveatTypes.restrictReturnedAccounts &&
Array.isArray(caveat.value)
) {
for (const value of caveat.value) {

@ -1,23 +1,15 @@
import { strict as assert } from 'assert';
import { ObservableStore } from '@metamask/obs-store';
import nanoid from 'nanoid';
import { useFakeTimers } from 'sinon';
import {
constants,
getters,
noop,
} from '../../../../test/mocks/permission-controller';
import { validateActivityEntry } from '../../../../test/helpers/permission-controller-helpers';
import PermissionsLogController from './permissionsLog';
import stringify from 'fast-safe-stringify';
import { constants, getters, noop } from '../../../../test/mocks/permissions';
import { PermissionLogController } from './permission-log';
import { LOG_LIMIT, LOG_METHOD_TYPES } from './enums';
const { PERMS, RPC_REQUESTS } = getters;
const {
ACCOUNTS,
EXPECTED_HISTORIES,
DOMAINS,
SUBJECTS,
PERM_NAMES,
REQUEST_IDS,
RESTRICTED_METHODS,
@ -25,10 +17,10 @@ const {
let clock;
const initPermLog = () => {
return new PermissionsLogController({
store: new ObservableStore(),
const initPermLog = (initState = {}) => {
return new PermissionLogController({
restrictedMethods: RESTRICTED_METHODS,
initState,
});
};
@ -59,21 +51,21 @@ const getSavedMockNext = (arr) => (handler) => {
arr.push(handler);
};
describe('permissions log', function () {
describe('activity log', function () {
describe('PermissionLogController', () => {
describe('restricted method activity log', () => {
let permLog, logMiddleware;
beforeEach(function () {
beforeEach(() => {
permLog = initPermLog();
logMiddleware = initMiddleware(permLog);
});
it('records activity for restricted methods', function () {
it('records activity for restricted methods', () => {
let log, req, res;
// test_method, success
req = RPC_REQUESTS.test_method(DOMAINS.a.origin);
req = RPC_REQUESTS.test_method(SUBJECTS.a.origin);
req.id = REQUEST_IDS.a;
res = { foo: 'bar' };
@ -82,7 +74,7 @@ describe('permissions log', function () {
log = permLog.getActivityLog();
const entry1 = log[0];
assert.equal(log.length, 1, 'log should have single entry');
expect(log).toHaveLength(1);
validateActivityEntry(
entry1,
{ ...req },
@ -93,7 +85,7 @@ describe('permissions log', function () {
// eth_accounts, failure
req = RPC_REQUESTS.eth_accounts(DOMAINS.b.origin);
req = RPC_REQUESTS.eth_accounts(SUBJECTS.b.origin);
req.id = REQUEST_IDS.b;
res = { error: new Error('Unauthorized.') };
@ -102,7 +94,7 @@ describe('permissions log', function () {
log = permLog.getActivityLog();
const entry2 = log[1];
assert.equal(log.length, 2, 'log should have 2 entries');
expect(log).toHaveLength(2);
validateActivityEntry(
entry2,
{ ...req },
@ -113,7 +105,7 @@ describe('permissions log', function () {
// eth_requestAccounts, success
req = RPC_REQUESTS.eth_requestAccounts(DOMAINS.c.origin);
req = RPC_REQUESTS.eth_requestAccounts(SUBJECTS.c.origin);
req.id = REQUEST_IDS.c;
res = { result: ACCOUNTS.c.permitted };
@ -122,7 +114,7 @@ describe('permissions log', function () {
log = permLog.getActivityLog();
const entry3 = log[2];
assert.equal(log.length, 3, 'log should have 3 entries');
expect(log).toHaveLength(3);
validateActivityEntry(
entry3,
{ ...req },
@ -133,7 +125,7 @@ describe('permissions log', function () {
// test_method, no response
req = RPC_REQUESTS.test_method(DOMAINS.a.origin);
req = RPC_REQUESTS.test_method(SUBJECTS.a.origin);
req.id = REQUEST_IDS.a;
res = null;
@ -142,7 +134,7 @@ describe('permissions log', function () {
log = permLog.getActivityLog();
const entry4 = log[3];
assert.equal(log.length, 4, 'log should have 4 entries');
expect(log).toHaveLength(4);
validateActivityEntry(
entry4,
{ ...req },
@ -152,14 +144,13 @@ describe('permissions log', function () {
);
// validate final state
assert.equal(entry1, log[0], 'first log entry should remain');
assert.equal(entry2, log[1], 'second log entry should remain');
assert.equal(entry3, log[2], 'third log entry should remain');
assert.equal(entry4, log[3], 'fourth log entry should remain');
expect(entry1).toStrictEqual(log[0]);
expect(entry2).toStrictEqual(log[1]);
expect(entry3).toStrictEqual(log[2]);
expect(entry4).toStrictEqual(log[3]);
});
it('handles responses added out of order', function () {
it('handles responses added out of order', () => {
let log;
const handlerArray = [];
@ -168,7 +159,7 @@ describe('permissions log', function () {
const id2 = nanoid();
const id3 = nanoid();
const req = RPC_REQUESTS.test_method(DOMAINS.a.origin);
const req = RPC_REQUESTS.test_method(SUBJECTS.a.origin);
// get make requests
req.id = id1;
@ -185,19 +176,15 @@ describe('permissions log', function () {
// verify log state
log = permLog.getActivityLog();
assert.equal(log.length, 3, 'log should have 3 entries');
expect(log).toHaveLength(3);
const entry1 = log[0];
const entry2 = log[1];
const entry3 = log[2];
assert.ok(
entry1.id === id1 &&
entry1.response === null &&
entry2.id === id2 &&
entry2.response === null &&
entry3.id === id3 &&
entry3.response === null,
'all entries should be in correct order and without responses',
);
// all entries should be in correct order, without responses
expect(entry1).toMatchObject({ id: id1, response: null });
expect(entry2).toMatchObject({ id: id2, response: null });
expect(entry3).toMatchObject({ id: id3, response: null });
// call response handlers
for (const i of [1, 2, 0]) {
@ -206,7 +193,7 @@ describe('permissions log', function () {
// verify log state again
log = permLog.getActivityLog();
assert.equal(log.length, 3, 'log should have 3 entries');
expect(log).toHaveLength(3);
// verify all entries
log = permLog.getActivityLog();
@ -236,8 +223,8 @@ describe('permissions log', function () {
);
});
it('handles a lack of response', function () {
let req = RPC_REQUESTS.test_method(DOMAINS.a.origin);
it('handles a lack of response', () => {
let req = RPC_REQUESTS.test_method(SUBJECTS.a.origin);
req.id = REQUEST_IDS.a;
let res = { foo: 'bar' };
@ -247,7 +234,7 @@ describe('permissions log', function () {
let log = permLog.getActivityLog();
const entry1 = log[0];
assert.equal(log.length, 1, 'log should have single entry');
expect(log).toHaveLength(1);
validateActivityEntry(
entry1,
{ ...req },
@ -257,7 +244,7 @@ describe('permissions log', function () {
);
// next request should be handled as normal
req = RPC_REQUESTS.eth_accounts(DOMAINS.b.origin);
req = RPC_REQUESTS.eth_accounts(SUBJECTS.b.origin);
req.id = REQUEST_IDS.b;
res = { result: ACCOUNTS.b.permitted };
@ -265,7 +252,7 @@ describe('permissions log', function () {
log = permLog.getActivityLog();
const entry2 = log[1];
assert.equal(log.length, 2, 'log should have 2 entries');
expect(log).toHaveLength(2);
validateActivityEntry(
entry2,
{ ...req },
@ -275,32 +262,32 @@ describe('permissions log', function () {
);
// validate final state
assert.equal(entry1, log[0], 'first log entry remains');
assert.equal(entry2, log[1], 'second log entry remains');
expect(entry1).toStrictEqual(log[0]);
expect(entry2).toStrictEqual(log[1]);
});
it('ignores expected methods', function () {
it('ignores expected methods', () => {
let log = permLog.getActivityLog();
assert.equal(log.length, 0, 'log should be empty');
expect(log).toHaveLength(0);
const res = { foo: 'bar' };
const req1 = RPC_REQUESTS.metamask_sendDomainMetadata(
DOMAINS.c.origin,
SUBJECTS.c.origin,
'foobar',
);
const req2 = RPC_REQUESTS.custom(DOMAINS.b.origin, 'eth_getBlockNumber');
const req3 = RPC_REQUESTS.custom(DOMAINS.b.origin, 'net_version');
const req2 = RPC_REQUESTS.custom(SUBJECTS.b.origin, 'eth_getBlockNumber');
const req3 = RPC_REQUESTS.custom(SUBJECTS.b.origin, 'net_version');
logMiddleware(req1, res);
logMiddleware(req2, res);
logMiddleware(req3, res);
log = permLog.getActivityLog();
assert.equal(log.length, 0, 'log should still be empty');
expect(log).toHaveLength(0);
});
it('enforces log limit', function () {
const req = RPC_REQUESTS.test_method(DOMAINS.a.origin);
it('enforces log limit', () => {
const req = RPC_REQUESTS.test_method(SUBJECTS.a.origin);
const res = { foo: 'bar' };
// max out log
@ -312,11 +299,7 @@ describe('permissions log', function () {
// check last entry valid
let log = permLog.getActivityLog();
assert.equal(
log.length,
LOG_LIMIT,
'log should have LOG_LIMIT num entries',
);
expect(log).toHaveLength(LOG_LIMIT);
validateActivityEntry(
log[LOG_LIMIT - 1],
@ -335,11 +318,7 @@ describe('permissions log', function () {
// check log length
log = permLog.getActivityLog();
assert.equal(
log.length,
LOG_LIMIT,
'log should have LOG_LIMIT num entries',
);
expect(log).toHaveLength(LOG_LIMIT);
// check first and last entries
validateActivityEntry(
@ -360,24 +339,22 @@ describe('permissions log', function () {
});
});
describe('permissions history', function () {
describe('permission history log', () => {
let permLog, logMiddleware;
beforeEach(function () {
beforeEach(() => {
permLog = initPermLog();
logMiddleware = initMiddleware(permLog);
initClock();
});
afterEach(function () {
afterEach(() => {
tearDownClock();
});
it('only updates history on responses', function () {
let permHistory;
it('only updates history on responses', () => {
const req = RPC_REQUESTS.requestPermission(
DOMAINS.a.origin,
SUBJECTS.a.origin,
PERM_NAMES.test_method,
);
const res = { result: [PERMS.granted.test_method()] };
@ -385,27 +362,19 @@ describe('permissions log', function () {
// noop => no response
logMiddleware({ ...req }, { ...res }, noop);
permHistory = permLog.getHistory();
assert.deepEqual(permHistory, {}, 'history should not have been updated');
expect(permLog.getHistory()).toStrictEqual({});
// response => records granted permissions
logMiddleware({ ...req }, { ...res });
permHistory = permLog.getHistory();
assert.equal(
Object.keys(permHistory).length,
1,
'history should have single origin',
);
assert.ok(
Boolean(permHistory[DOMAINS.a.origin]),
'history should have expected origin',
);
const permHistory = permLog.getHistory();
expect(Object.keys(permHistory)).toHaveLength(1);
expect(permHistory[SUBJECTS.a.origin]).toBeDefined();
});
it('ignores malformed permissions requests', function () {
it('ignores malformed permissions requests', () => {
const req = RPC_REQUESTS.requestPermission(
DOMAINS.a.origin,
SUBJECTS.a.origin,
PERM_NAMES.test_method,
);
delete req.params;
@ -414,18 +383,12 @@ describe('permissions log', function () {
// no params => no response
logMiddleware({ ...req }, { ...res });
assert.deepEqual(
permLog.getHistory(),
{},
'history should not have been updated',
);
expect(permLog.getHistory()).toStrictEqual({});
});
it('records and updates account history as expected', async function () {
let permHistory;
it('records and updates account history as expected', async () => {
const req = RPC_REQUESTS.requestPermission(
DOMAINS.a.origin,
SUBJECTS.a.origin,
PERM_NAMES.eth_accounts,
);
const res = {
@ -434,15 +397,7 @@ describe('permissions log', function () {
logMiddleware({ ...req }, { ...res });
// validate history
permHistory = permLog.getHistory();
assert.deepEqual(
permHistory,
EXPECTED_HISTORIES.case1[0],
'should have correct history',
);
expect(permLog.getHistory()).toStrictEqual(EXPECTED_HISTORIES.case1[0]);
// mock permission requested again, with another approved account
@ -452,18 +407,12 @@ describe('permissions log', function () {
logMiddleware({ ...req }, { ...res });
permHistory = permLog.getHistory();
assert.deepEqual(
permHistory,
EXPECTED_HISTORIES.case1[1],
'should have correct history',
);
expect(permLog.getHistory()).toStrictEqual(EXPECTED_HISTORIES.case1[1]);
});
it('handles eth_accounts response without caveats', async function () {
it('handles eth_accounts response without caveats', async () => {
const req = RPC_REQUESTS.requestPermission(
DOMAINS.a.origin,
SUBJECTS.a.origin,
PERM_NAMES.eth_accounts,
);
const res = {
@ -473,18 +422,12 @@ describe('permissions log', function () {
logMiddleware({ ...req }, { ...res });
// validate history
assert.deepEqual(
permLog.getHistory(),
EXPECTED_HISTORIES.case2[0],
'should have expected history',
);
expect(permLog.getHistory()).toStrictEqual(EXPECTED_HISTORIES.case2[0]);
});
it('handles extra caveats for eth_accounts', async function () {
it('handles extra caveats for eth_accounts', async () => {
const req = RPC_REQUESTS.requestPermission(
DOMAINS.a.origin,
SUBJECTS.a.origin,
PERM_NAMES.eth_accounts,
);
const res = {
@ -494,20 +437,14 @@ describe('permissions log', function () {
logMiddleware({ ...req }, { ...res });
// validate history
assert.deepEqual(
permLog.getHistory(),
EXPECTED_HISTORIES.case1[0],
'should have correct history',
);
expect(permLog.getHistory()).toStrictEqual(EXPECTED_HISTORIES.case1[0]);
});
// wallet_requestPermissions returns all permissions approved for the
// requesting origin, including old ones
it('handles unrequested permissions on the response', async function () {
it('handles unrequested permissions on the response', async () => {
const req = RPC_REQUESTS.requestPermission(
DOMAINS.a.origin,
SUBJECTS.a.origin,
PERM_NAMES.eth_accounts,
);
const res = {
@ -519,18 +456,12 @@ describe('permissions log', function () {
logMiddleware({ ...req }, { ...res });
// validate history
assert.deepEqual(
permLog.getHistory(),
EXPECTED_HISTORIES.case1[0],
'should have correct history',
);
expect(permLog.getHistory()).toStrictEqual(EXPECTED_HISTORIES.case1[0]);
});
it('does not update history if no new permissions are approved', async function () {
it('does not update history if no new permissions are approved', async () => {
let req = RPC_REQUESTS.requestPermission(
DOMAINS.a.origin,
SUBJECTS.a.origin,
PERM_NAMES.test_method,
);
let res = {
@ -539,20 +470,14 @@ describe('permissions log', function () {
logMiddleware({ ...req }, { ...res });
// validate history
assert.deepEqual(
permLog.getHistory(),
EXPECTED_HISTORIES.case4[0],
'should have correct history',
);
expect(permLog.getHistory()).toStrictEqual(EXPECTED_HISTORIES.case4[0]);
// new permission requested, but not approved
clock.tick(1);
req = RPC_REQUESTS.requestPermission(
DOMAINS.a.origin,
SUBJECTS.a.origin,
PERM_NAMES.eth_accounts,
);
res = {
@ -561,18 +486,11 @@ describe('permissions log', function () {
logMiddleware({ ...req }, { ...res });
// validate history
assert.deepEqual(
permLog.getHistory(),
EXPECTED_HISTORIES.case4[0],
'should have same history as before',
);
// history should be unmodified
expect(permLog.getHistory()).toStrictEqual(EXPECTED_HISTORIES.case4[0]);
});
it('records and updates history for multiple origins, regardless of response order', async function () {
let permHistory;
it('records and updates history for multiple origins, regardless of response order', async () => {
// make first round of requests
const round1 = [];
@ -581,7 +499,7 @@ describe('permissions log', function () {
// first origin
round1.push({
req: RPC_REQUESTS.requestPermission(
DOMAINS.a.origin,
SUBJECTS.a.origin,
PERM_NAMES.test_method,
),
res: {
@ -592,7 +510,7 @@ describe('permissions log', function () {
// second origin
round1.push({
req: RPC_REQUESTS.requestPermission(
DOMAINS.b.origin,
SUBJECTS.b.origin,
PERM_NAMES.eth_accounts,
),
res: {
@ -602,7 +520,7 @@ describe('permissions log', function () {
// third origin
round1.push({
req: RPC_REQUESTS.requestPermissions(DOMAINS.c.origin, {
req: RPC_REQUESTS.requestPermissions(SUBJECTS.c.origin, {
[PERM_NAMES.test_method]: {},
[PERM_NAMES.eth_accounts]: {},
}),
@ -623,14 +541,7 @@ describe('permissions log', function () {
handlers1[i](noop);
}
// validate history
permHistory = permLog.getHistory();
assert.deepEqual(
permHistory,
EXPECTED_HISTORIES.case3[0],
'should have expected history',
);
expect(permLog.getHistory()).toStrictEqual(EXPECTED_HISTORIES.case3[0]);
// make next round of requests
@ -642,7 +553,7 @@ describe('permissions log', function () {
// first origin
round2.push({
req: RPC_REQUESTS.requestPermission(
DOMAINS.a.origin,
SUBJECTS.a.origin,
PERM_NAMES.test_method,
),
res: {
@ -654,7 +565,7 @@ describe('permissions log', function () {
// third origin
round2.push({
req: RPC_REQUESTS.requestPermissions(DOMAINS.c.origin, {
req: RPC_REQUESTS.requestPermissions(SUBJECTS.c.origin, {
[PERM_NAMES.eth_accounts]: {},
}),
res: {
@ -667,14 +578,90 @@ describe('permissions log', function () {
logMiddleware({ ...x.req }, { ...x.res });
});
// validate history
permHistory = permLog.getHistory();
expect(permLog.getHistory()).toStrictEqual(EXPECTED_HISTORIES.case3[1]);
});
});
assert.deepEqual(
permHistory,
EXPECTED_HISTORIES.case3[1],
'should have expected history',
);
describe('updateAccountsHistory', () => {
beforeEach(() => {
initClock();
});
afterEach(() => {
tearDownClock();
});
it('does nothing if the list of accounts is empty', () => {
const permLog = initPermLog();
permLog.updateAccountsHistory('foo.com', []);
expect(permLog.getHistory()).toStrictEqual({});
});
it('updates the account history', () => {
const permLog = initPermLog({
permissionHistory: {
'foo.com': {
[PERM_NAMES.eth_accounts]: {
accounts: {
'0x1': 1,
},
lastApproved: 1,
},
},
},
});
clock.tick(1);
permLog.updateAccountsHistory('foo.com', ['0x1', '0x2']);
expect(permLog.getHistory()).toStrictEqual({
'foo.com': {
[PERM_NAMES.eth_accounts]: {
accounts: {
'0x1': 2,
'0x2': 2,
},
lastApproved: 1,
},
},
});
});
});
});
/**
* Validates an activity log entry with respect to a request, response, and
* relevant metadata.
*
* @param {Object} entry - The activity log entry to validate.
* @param {Object} req - The request that generated the entry.
* @param {Object} [res] - The response for the request, if any.
* @param {'restricted'|'internal'} methodType - The method log controller method type of the request.
* @param {boolean} success - Whether the request succeeded or not.
*/
function validateActivityEntry(entry, req, res, methodType, success) {
expect(entry).toBeDefined();
expect(entry.id).toStrictEqual(req.id);
expect(entry.method).toStrictEqual(req.method);
expect(entry.origin).toStrictEqual(req.origin);
expect(entry.methodType).toStrictEqual(methodType);
expect(entry.request).toStrictEqual(stringify(req, null, 2));
expect(Number.isInteger(entry.requestTime)).toBe(true);
if (res) {
expect(Number.isInteger(entry.responseTime)).toBe(true);
expect(entry.requestTime <= entry.responseTime).toBe(true);
expect(entry.success).toStrictEqual(success);
expect(entry.response).toStrictEqual(stringify(res, null, 2));
} else {
expect(entry.requestTime > 0).toBe(true);
expect(entry).toMatchObject({
response: null,
responseTime: null,
success: null,
});
}
}

@ -1,950 +0,0 @@
import { strict as assert } from 'assert';
import sinon from 'sinon';
import {
constants,
getters,
getPermControllerOpts,
getPermissionsMiddleware,
} from '../../../../test/mocks/permission-controller';
import {
getUserApprovalPromise,
grantPermissions,
} from '../../../../test/helpers/permission-controller-helpers';
import { METADATA_STORE_KEY } from './enums';
import { PermissionsController } from '.';
const { CAVEATS, ERRORS, PERMS, RPC_REQUESTS } = getters;
const { ACCOUNTS, DOMAINS, PERM_NAMES } = constants;
const initPermController = () => {
return new PermissionsController({
...getPermControllerOpts(),
});
};
const createApprovalSpies = (permController) => {
sinon.spy(permController.approvals, '_add');
};
const getNextApprovalId = (permController) => {
return permController.approvals._approvals.keys().next().value;
};
const validatePermission = (perm, name, origin, caveats) => {
assert.equal(
name,
perm.parentCapability,
'should have expected permission name',
);
assert.equal(origin, perm.invoker, 'should have expected permission origin');
if (caveats) {
assert.deepEqual(
caveats,
perm.caveats,
'should have expected permission caveats',
);
} else {
assert.ok(!perm.caveats, 'should not have any caveats');
}
};
describe('permissions middleware', function () {
describe('wallet_requestPermissions', function () {
let permController;
beforeEach(function () {
permController = initPermController();
permController.notifyAccountsChanged = sinon.fake();
});
it('grants permissions on user approval', async function () {
createApprovalSpies(permController);
const aMiddleware = getPermissionsMiddleware(
permController,
DOMAINS.a.origin,
);
const req = RPC_REQUESTS.requestPermission(
DOMAINS.a.origin,
PERM_NAMES.eth_accounts,
);
const res = {};
const userApprovalPromise = getUserApprovalPromise(permController);
const pendingApproval = assert.doesNotReject(
aMiddleware(req, res),
'should not reject permissions request',
);
await userApprovalPromise;
assert.ok(
permController.approvals._add.calledOnce,
'should have added single approval request',
);
const id = getNextApprovalId(permController);
const approvedReq = PERMS.approvedRequest(
id,
PERMS.requests.eth_accounts(),
);
await permController.approvePermissionsRequest(
approvedReq,
ACCOUNTS.a.permitted,
);
await pendingApproval;
assert.ok(
res.result && !res.error,
'response should have result and no error',
);
assert.equal(
res.result.length,
1,
'origin should have single approved permission',
);
validatePermission(
res.result[0],
PERM_NAMES.eth_accounts,
DOMAINS.a.origin,
CAVEATS.eth_accounts(ACCOUNTS.a.permitted),
);
const aAccounts = await permController.getAccounts(DOMAINS.a.origin);
assert.deepEqual(
aAccounts,
[ACCOUNTS.a.primary],
'origin should have correct accounts',
);
assert.ok(
permController.notifyAccountsChanged.calledOnceWith(
DOMAINS.a.origin,
aAccounts,
),
'expected notification call should have been made',
);
});
it('handles serial approved requests that overwrite existing permissions', async function () {
const aMiddleware = getPermissionsMiddleware(
permController,
DOMAINS.a.origin,
);
// create first request
const req1 = RPC_REQUESTS.requestPermission(
DOMAINS.a.origin,
PERM_NAMES.eth_accounts,
);
const res1 = {};
// send, approve, and validate first request
// note use of ACCOUNTS.a.permitted
let userApprovalPromise = getUserApprovalPromise(permController);
const pendingApproval1 = assert.doesNotReject(
aMiddleware(req1, res1),
'should not reject permissions request',
);
await userApprovalPromise;
const id1 = getNextApprovalId(permController);
const approvedReq1 = PERMS.approvedRequest(
id1,
PERMS.requests.eth_accounts(),
);
await permController.approvePermissionsRequest(
approvedReq1,
ACCOUNTS.a.permitted,
);
await pendingApproval1;
assert.ok(
res1.result && !res1.error,
'response should have result and no error',
);
assert.equal(
res1.result.length,
1,
'origin should have single approved permission',
);
validatePermission(
res1.result[0],
PERM_NAMES.eth_accounts,
DOMAINS.a.origin,
CAVEATS.eth_accounts(ACCOUNTS.a.permitted),
);
const accounts1 = await permController.getAccounts(DOMAINS.a.origin);
assert.deepEqual(
accounts1,
[ACCOUNTS.a.primary],
'origin should have correct accounts',
);
assert.ok(
permController.notifyAccountsChanged.calledOnceWith(
DOMAINS.a.origin,
accounts1,
),
'expected notification call should have been made',
);
// create second request
const requestedPerms2 = {
...PERMS.requests.eth_accounts(),
...PERMS.requests.test_method(),
};
const req2 = RPC_REQUESTS.requestPermissions(DOMAINS.a.origin, {
...requestedPerms2,
});
const res2 = {};
// send, approve, and validate second request
// note use of ACCOUNTS.b.permitted
userApprovalPromise = getUserApprovalPromise(permController);
const pendingApproval2 = assert.doesNotReject(
aMiddleware(req2, res2),
'should not reject permissions request',
);
await userApprovalPromise;
const id2 = getNextApprovalId(permController);
const approvedReq2 = PERMS.approvedRequest(id2, { ...requestedPerms2 });
await permController.approvePermissionsRequest(
approvedReq2,
ACCOUNTS.b.permitted,
);
await pendingApproval2;
assert.ok(
res2.result && !res2.error,
'response should have result and no error',
);
assert.equal(
res2.result.length,
2,
'origin should have single approved permission',
);
validatePermission(
res2.result[0],
PERM_NAMES.eth_accounts,
DOMAINS.a.origin,
CAVEATS.eth_accounts(ACCOUNTS.b.permitted),
);
validatePermission(
res2.result[1],
PERM_NAMES.test_method,
DOMAINS.a.origin,
);
const accounts2 = await permController.getAccounts(DOMAINS.a.origin);
assert.deepEqual(
accounts2,
[ACCOUNTS.b.primary],
'origin should have correct accounts',
);
assert.equal(
permController.notifyAccountsChanged.callCount,
2,
'should have called notification method 2 times in total',
);
assert.ok(
permController.notifyAccountsChanged.lastCall.calledWith(
DOMAINS.a.origin,
accounts2,
),
'expected notification call should have been made',
);
});
it('rejects permissions on user rejection', async function () {
createApprovalSpies(permController);
const aMiddleware = getPermissionsMiddleware(
permController,
DOMAINS.a.origin,
);
const req = RPC_REQUESTS.requestPermission(
DOMAINS.a.origin,
PERM_NAMES.eth_accounts,
);
const res = {};
const expectedError = ERRORS.rejectPermissionsRequest.rejection();
const userApprovalPromise = getUserApprovalPromise(permController);
const requestRejection = assert.rejects(
aMiddleware(req, res),
expectedError,
'request should be rejected with correct error',
);
await userApprovalPromise;
assert.ok(
permController.approvals._add.calledOnce,
'should have added single approval request',
);
const id = getNextApprovalId(permController);
await permController.rejectPermissionsRequest(id);
await requestRejection;
assert.ok(
!res.result && res.error && res.error.message === expectedError.message,
'response should have expected error and no result',
);
const aAccounts = await permController.getAccounts(DOMAINS.a.origin);
assert.deepEqual(
aAccounts,
[],
'origin should have have correct accounts',
);
assert.ok(
permController.notifyAccountsChanged.notCalled,
'should not have called notification method',
);
});
it('rejects requests with unknown permissions', async function () {
createApprovalSpies(permController);
const aMiddleware = getPermissionsMiddleware(
permController,
DOMAINS.a.origin,
);
const req = RPC_REQUESTS.requestPermissions(DOMAINS.a.origin, {
...PERMS.requests.does_not_exist(),
...PERMS.requests.test_method(),
});
const res = {};
const expectedError = ERRORS.rejectPermissionsRequest.methodNotFound(
PERM_NAMES.does_not_exist,
);
await assert.rejects(
aMiddleware(req, res),
expectedError,
'request should be rejected with correct error',
);
assert.ok(
permController.approvals._add.notCalled,
'no approval requests should have been added',
);
assert.ok(
!res.result && res.error && res.error.message === expectedError.message,
'response should have expected error and no result',
);
assert.ok(
permController.notifyAccountsChanged.notCalled,
'should not have called notification method',
);
});
it('accepts only a single pending permissions request per origin', async function () {
createApprovalSpies(permController);
// two middlewares for two origins
const aMiddleware = getPermissionsMiddleware(
permController,
DOMAINS.a.origin,
);
const bMiddleware = getPermissionsMiddleware(
permController,
DOMAINS.b.origin,
);
// create and start processing first request for first origin
const reqA1 = RPC_REQUESTS.requestPermission(
DOMAINS.a.origin,
PERM_NAMES.test_method,
);
const resA1 = {};
let userApprovalPromise = getUserApprovalPromise(permController);
const requestApproval1 = assert.doesNotReject(
aMiddleware(reqA1, resA1),
'should not reject permissions request',
);
await userApprovalPromise;
// create and start processing first request for second origin
const reqB1 = RPC_REQUESTS.requestPermission(
DOMAINS.b.origin,
PERM_NAMES.test_method,
);
const resB1 = {};
userApprovalPromise = getUserApprovalPromise(permController);
const requestApproval2 = assert.doesNotReject(
bMiddleware(reqB1, resB1),
'should not reject permissions request',
);
await userApprovalPromise;
assert.ok(
permController.approvals._add.calledTwice,
'should have added two approval requests',
);
// create and start processing second request for first origin,
// which should throw
const reqA2 = RPC_REQUESTS.requestPermission(
DOMAINS.a.origin,
PERM_NAMES.test_method,
);
const resA2 = {};
userApprovalPromise = getUserApprovalPromise(permController);
const expectedError = ERRORS.pendingApprovals.requestAlreadyPending(
DOMAINS.a.origin,
);
const requestApprovalFail = assert.rejects(
aMiddleware(reqA2, resA2),
expectedError,
'request should be rejected with correct error',
);
await userApprovalPromise;
await requestApprovalFail;
assert.ok(
!resA2.result &&
resA2.error &&
resA2.error.message === expectedError.message,
'response should have expected error and no result',
);
assert.equal(
permController.approvals._add.callCount,
3,
'should have attempted to create three pending approvals',
);
assert.equal(
permController.approvals._approvals.size,
2,
'should only have created two pending approvals',
);
// now, remaining pending requests should be approved without issue
for (const id of permController.approvals._approvals.keys()) {
await permController.approvePermissionsRequest(
PERMS.approvedRequest(id, PERMS.requests.test_method()),
);
}
await requestApproval1;
await requestApproval2;
assert.ok(
resA1.result && !resA1.error,
'first response should have result and no error',
);
assert.equal(
resA1.result.length,
1,
'first origin should have single approved permission',
);
assert.ok(
resB1.result && !resB1.error,
'second response should have result and no error',
);
assert.equal(
resB1.result.length,
1,
'second origin should have single approved permission',
);
});
});
describe('restricted methods', function () {
let permController;
beforeEach(function () {
permController = initPermController();
});
it('prevents restricted method access for unpermitted domain', async function () {
const aMiddleware = getPermissionsMiddleware(
permController,
DOMAINS.a.origin,
);
const req = RPC_REQUESTS.test_method(DOMAINS.a.origin);
const res = {};
const expectedError = ERRORS.rpcCap.unauthorized();
await assert.rejects(
aMiddleware(req, res),
expectedError,
'request should be rejected with correct error',
);
assert.ok(
!res.result && res.error && res.error.code === expectedError.code,
'response should have expected error and no result',
);
});
it('allows restricted method access for permitted domain', async function () {
const bMiddleware = getPermissionsMiddleware(
permController,
DOMAINS.b.origin,
);
grantPermissions(
permController,
DOMAINS.b.origin,
PERMS.finalizedRequests.test_method(),
);
const req = RPC_REQUESTS.test_method(DOMAINS.b.origin, true);
const res = {};
await assert.doesNotReject(bMiddleware(req, res), 'should not reject');
assert.ok(
res.result && res.result === 1,
'response should have correct result',
);
});
});
describe('eth_accounts', function () {
let permController;
beforeEach(function () {
permController = initPermController();
});
it('returns empty array for non-permitted domain', async function () {
const aMiddleware = getPermissionsMiddleware(
permController,
DOMAINS.a.origin,
);
const req = RPC_REQUESTS.eth_accounts(DOMAINS.a.origin);
const res = {};
await assert.doesNotReject(aMiddleware(req, res), 'should not reject');
assert.ok(
res.result && !res.error,
'response should have result and no error',
);
assert.deepEqual(res.result, [], 'response should have correct result');
});
it('returns correct accounts for permitted domain', async function () {
const aMiddleware = getPermissionsMiddleware(
permController,
DOMAINS.a.origin,
);
grantPermissions(
permController,
DOMAINS.a.origin,
PERMS.finalizedRequests.eth_accounts(ACCOUNTS.a.permitted),
);
const req = RPC_REQUESTS.eth_accounts(DOMAINS.a.origin);
const res = {};
await assert.doesNotReject(aMiddleware(req, res), 'should not reject');
assert.ok(
res.result && !res.error,
'response should have result and no error',
);
assert.deepEqual(
res.result,
[ACCOUNTS.a.primary],
'response should have correct result',
);
});
});
describe('eth_requestAccounts', function () {
let permController;
beforeEach(function () {
permController = initPermController();
});
it('requests accounts for unpermitted origin, and approves on user approval', async function () {
createApprovalSpies(permController);
const userApprovalPromise = getUserApprovalPromise(permController);
const aMiddleware = getPermissionsMiddleware(
permController,
DOMAINS.a.origin,
);
const req = RPC_REQUESTS.eth_requestAccounts(DOMAINS.a.origin);
const res = {};
const pendingApproval = assert.doesNotReject(
aMiddleware(req, res),
'should not reject permissions request',
);
await userApprovalPromise;
assert.ok(
permController.approvals._add.calledOnce,
'should have added single approval request',
);
const id = getNextApprovalId(permController);
const approvedReq = PERMS.approvedRequest(
id,
PERMS.requests.eth_accounts(),
);
await permController.approvePermissionsRequest(
approvedReq,
ACCOUNTS.a.permitted,
);
// wait for permission to be granted
await pendingApproval;
const perms = permController.permissions.getPermissionsForDomain(
DOMAINS.a.origin,
);
assert.equal(
perms.length,
1,
'domain should have correct number of permissions',
);
validatePermission(
perms[0],
PERM_NAMES.eth_accounts,
DOMAINS.a.origin,
CAVEATS.eth_accounts(ACCOUNTS.a.permitted),
);
// we should also see the accounts on the response
assert.ok(
res.result && !res.error,
'response should have result and no error',
);
assert.deepEqual(
res.result,
[ACCOUNTS.a.primary],
'result should have correct accounts',
);
// we should also be able to get the accounts independently
const aAccounts = await permController.getAccounts(DOMAINS.a.origin);
assert.deepEqual(
aAccounts,
[ACCOUNTS.a.primary],
'origin should have have correct accounts',
);
});
it('requests accounts for unpermitted origin, and rejects on user rejection', async function () {
createApprovalSpies(permController);
const userApprovalPromise = getUserApprovalPromise(permController);
const aMiddleware = getPermissionsMiddleware(
permController,
DOMAINS.a.origin,
);
const req = RPC_REQUESTS.eth_requestAccounts(DOMAINS.a.origin);
const res = {};
const expectedError = ERRORS.rejectPermissionsRequest.rejection();
const requestRejection = assert.rejects(
aMiddleware(req, res),
expectedError,
'request should be rejected with correct error',
);
await userApprovalPromise;
assert.ok(
permController.approvals._add.calledOnce,
'should have added single approval request',
);
const id = getNextApprovalId(permController);
await permController.rejectPermissionsRequest(id);
await requestRejection;
assert.ok(
!res.result && res.error && res.error.message === expectedError.message,
'response should have expected error and no result',
);
const aAccounts = await permController.getAccounts(DOMAINS.a.origin);
assert.deepEqual(
aAccounts,
[],
'origin should have have correct accounts',
);
});
it('directly returns accounts for permitted domain', async function () {
const cMiddleware = getPermissionsMiddleware(
permController,
DOMAINS.c.origin,
);
grantPermissions(
permController,
DOMAINS.c.origin,
PERMS.finalizedRequests.eth_accounts(ACCOUNTS.c.permitted),
);
const req = RPC_REQUESTS.eth_requestAccounts(DOMAINS.c.origin);
const res = {};
await assert.doesNotReject(cMiddleware(req, res), 'should not reject');
assert.ok(
res.result && !res.error,
'response should have result and no error',
);
assert.deepEqual(
res.result,
[ACCOUNTS.c.primary],
'response should have correct result',
);
});
it('rejects new requests when request already pending', async function () {
let unlock;
const unlockPromise = new Promise((resolve) => {
unlock = resolve;
});
permController.getUnlockPromise = () => unlockPromise;
const cMiddleware = getPermissionsMiddleware(
permController,
DOMAINS.c.origin,
);
grantPermissions(
permController,
DOMAINS.c.origin,
PERMS.finalizedRequests.eth_accounts(ACCOUNTS.c.permitted),
);
const req = RPC_REQUESTS.eth_requestAccounts(DOMAINS.c.origin);
const res = {};
// this will block until we resolve the unlock Promise
const requestApproval = assert.doesNotReject(
cMiddleware(req, res),
'should not reject',
);
// this will reject because of the already pending request
await assert.rejects(
cMiddleware({ ...req }, {}),
ERRORS.eth_requestAccounts.requestAlreadyPending(DOMAINS.c.origin),
);
// now unlock and let through the first request
unlock();
await requestApproval;
assert.ok(
res.result && !res.error,
'response should have result and no error',
);
assert.deepEqual(
res.result,
[ACCOUNTS.c.primary],
'response should have correct result',
);
});
});
describe('metamask_sendDomainMetadata', function () {
let permController, clock;
beforeEach(function () {
permController = initPermController();
clock = sinon.useFakeTimers(1);
});
afterEach(function () {
clock.restore();
});
it('records domain metadata', async function () {
const name = 'BAZ';
const cMiddleware = getPermissionsMiddleware(
permController,
DOMAINS.c.origin,
);
const req = RPC_REQUESTS.metamask_sendDomainMetadata(
DOMAINS.c.origin,
name,
);
const res = {};
await assert.doesNotReject(cMiddleware(req, res), 'should not reject');
assert.ok(res.result, 'result should be true');
const metadataStore = permController.store.getState()[METADATA_STORE_KEY];
assert.deepEqual(
metadataStore,
{
[DOMAINS.c.origin]: {
name,
host: DOMAINS.c.host,
lastUpdated: 1,
},
},
'metadata should have been added to store',
);
});
it('records domain metadata and preserves extensionId', async function () {
const extensionId = 'fooExtension';
const name = 'BAZ';
const cMiddleware = getPermissionsMiddleware(
permController,
DOMAINS.c.origin,
extensionId,
);
const req = RPC_REQUESTS.metamask_sendDomainMetadata(
DOMAINS.c.origin,
name,
);
const res = {};
await assert.doesNotReject(cMiddleware(req, res), 'should not reject');
assert.ok(res.result, 'result should be true');
const metadataStore = permController.store.getState()[METADATA_STORE_KEY];
assert.deepEqual(
metadataStore,
{ [DOMAINS.c.origin]: { name, extensionId, lastUpdated: 1 } },
'metadata should have been added to store',
);
});
it('should not record domain metadata if no name', async function () {
const name = null;
const cMiddleware = getPermissionsMiddleware(
permController,
DOMAINS.c.origin,
);
const req = RPC_REQUESTS.metamask_sendDomainMetadata(
DOMAINS.c.origin,
name,
);
const res = {};
await assert.doesNotReject(cMiddleware(req, res), 'should not reject');
assert.ok(res.result, 'result should be true');
const metadataStore = permController.store.getState()[METADATA_STORE_KEY];
assert.deepEqual(
metadataStore,
{},
'metadata should not have been added to store',
);
});
it('should not record domain metadata if no metadata', async function () {
const cMiddleware = getPermissionsMiddleware(
permController,
DOMAINS.c.origin,
);
const req = RPC_REQUESTS.metamask_sendDomainMetadata(DOMAINS.c.origin);
delete req.domainMetadata;
const res = {};
await assert.doesNotReject(cMiddleware(req, res), 'should not reject');
assert.ok(res.result, 'result should be true');
const metadataStore = permController.store.getState()[METADATA_STORE_KEY];
assert.deepEqual(
metadataStore,
{},
'metadata should not have been added to store',
);
});
});
});

@ -1,112 +0,0 @@
import { createAsyncMiddleware } from 'json-rpc-engine';
import { ethErrors } from 'eth-rpc-errors';
/**
* Create middleware for handling certain methods and preprocessing permissions requests.
*/
export default function createPermissionsMethodMiddleware({
addDomainMetadata,
getAccounts,
getUnlockPromise,
hasPermission,
notifyAccountsChanged,
requestAccountsPermission,
}) {
let isProcessingRequestAccounts = false;
return createAsyncMiddleware(async (req, res, next) => {
let responseHandler;
switch (req.method) {
// Intercepting eth_accounts requests for backwards compatibility:
// The getAccounts call below wraps the rpc-cap middleware, and returns
// an empty array in case of errors (such as 4100:unauthorized)
case 'eth_accounts': {
res.result = await getAccounts();
return;
}
case 'eth_requestAccounts': {
if (isProcessingRequestAccounts) {
res.error = ethErrors.rpc.resourceUnavailable(
'Already processing eth_requestAccounts. Please wait.',
);
return;
}
if (hasPermission('eth_accounts')) {
isProcessingRequestAccounts = true;
await getUnlockPromise();
isProcessingRequestAccounts = false;
}
// first, just try to get accounts
let accounts = await getAccounts();
if (accounts.length > 0) {
res.result = accounts;
return;
}
// if no accounts, request the accounts permission
try {
await requestAccountsPermission();
} catch (err) {
res.error = err;
return;
}
// get the accounts again
accounts = await getAccounts();
/* istanbul ignore else: too hard to induce, see below comment */
if (accounts.length > 0) {
res.result = accounts;
} else {
// this should never happen, because it should be caught in the
// above catch clause
res.error = ethErrors.rpc.internal(
'Accounts unexpectedly unavailable. Please report this bug.',
);
}
return;
}
// custom method for getting metadata from the requesting domain,
// sent automatically by the inpage provider when it's initialized
case 'metamask_sendDomainMetadata': {
if (typeof req.params?.name === 'string') {
addDomainMetadata(req.origin, req.params);
}
res.result = true;
return;
}
// register return handler to send accountsChanged notification
case 'wallet_requestPermissions': {
if ('eth_accounts' in req.params?.[0]) {
responseHandler = async () => {
if (Array.isArray(res.result)) {
for (const permission of res.result) {
if (permission.parentCapability === 'eth_accounts') {
notifyAccountsChanged(await getAccounts());
}
}
}
};
}
break;
}
default:
break;
}
// when this promise resolves, the response is on its way back
// eslint-disable-next-line node/callback-return
await next();
if (responseHandler) {
responseHandler();
}
});
}

@ -1,174 +0,0 @@
import { strict as assert } from 'assert';
import pify from 'pify';
import getRestrictedMethods from './restrictedMethods';
describe('restricted methods', function () {
describe('eth_accounts', function () {
it('should handle getKeyringAccounts error', async function () {
const restrictedMethods = getRestrictedMethods({
getKeyringAccounts: async () => {
throw new Error('foo');
},
});
const ethAccountsMethod = pify(restrictedMethods.eth_accounts.method);
const res = {};
const fooError = new Error('foo');
await assert.rejects(
ethAccountsMethod(null, res, null),
fooError,
'Should reject with expected error',
);
assert.deepEqual(
res,
{ error: fooError },
'response should have expected error and no result',
);
});
it('should handle missing identity for first account when sorting', async function () {
const restrictedMethods = getRestrictedMethods({
getIdentities: () => {
return { '0x7e57e2': {} };
},
getKeyringAccounts: async () => ['0x7e57e2', '0x7e57e3'],
});
const ethAccountsMethod = pify(restrictedMethods.eth_accounts.method);
const res = {};
await assert.rejects(ethAccountsMethod(null, res, null));
assert.ok(res.error instanceof Error, 'result should have error');
assert.deepEqual(
Object.keys(res),
['error'],
'result should only contain error',
);
});
it('should handle missing identity for second account when sorting', async function () {
const restrictedMethods = getRestrictedMethods({
getIdentities: () => {
return { '0x7e57e3': {} };
},
getKeyringAccounts: async () => ['0x7e57e2', '0x7e57e3'],
});
const ethAccountsMethod = pify(restrictedMethods.eth_accounts.method);
const res = {};
await assert.rejects(ethAccountsMethod(null, res, null));
assert.ok(res.error instanceof Error, 'result should have error');
assert.deepEqual(
Object.keys(res),
['error'],
'result should only contain error',
);
});
it('should return accounts in keyring order when none are selected', async function () {
const keyringAccounts = ['0x7e57e2', '0x7e57e3', '0x7e57e4', '0x7e57e5'];
const restrictedMethods = getRestrictedMethods({
getIdentities: () => {
return keyringAccounts.reduce((identities, address) => {
identities[address] = {};
return identities;
}, {});
},
getKeyringAccounts: async () => [...keyringAccounts],
});
const ethAccountsMethod = pify(restrictedMethods.eth_accounts.method);
const res = {};
await ethAccountsMethod(null, res, null);
assert.deepEqual(
res,
{ result: keyringAccounts },
'should return accounts in correct order',
);
});
it('should return accounts in keyring order when all have same last selected time', async function () {
const keyringAccounts = ['0x7e57e2', '0x7e57e3', '0x7e57e4', '0x7e57e5'];
const restrictedMethods = getRestrictedMethods({
getIdentities: () => {
return keyringAccounts.reduce((identities, address) => {
identities[address] = { lastSelected: 1000 };
return identities;
}, {});
},
getKeyringAccounts: async () => [...keyringAccounts],
});
const ethAccountsMethod = pify(restrictedMethods.eth_accounts.method);
const res = {};
await ethAccountsMethod(null, res, null);
assert.deepEqual(
res,
{ result: keyringAccounts },
'should return accounts in correct order',
);
});
it('should return accounts sorted by last selected (descending)', async function () {
const keyringAccounts = ['0x7e57e2', '0x7e57e3', '0x7e57e4', '0x7e57e5'];
const expectedResult = keyringAccounts.slice().reverse();
const restrictedMethods = getRestrictedMethods({
getIdentities: () => {
return keyringAccounts.reduce((identities, address, index) => {
identities[address] = { lastSelected: index * 1000 };
return identities;
}, {});
},
getKeyringAccounts: async () => [...keyringAccounts],
});
const ethAccountsMethod = pify(restrictedMethods.eth_accounts.method);
const res = {};
await ethAccountsMethod(null, res, null);
assert.deepEqual(
res,
{ result: expectedResult },
'should return accounts in correct order',
);
});
it('should return accounts sorted by last selected (descending) with unselected accounts last, in keyring order', async function () {
const keyringAccounts = [
'0x7e57e2',
'0x7e57e3',
'0x7e57e4',
'0x7e57e5',
'0x7e57e6',
];
const expectedResult = [
'0x7e57e4',
'0x7e57e2',
'0x7e57e3',
'0x7e57e5',
'0x7e57e6',
];
const restrictedMethods = getRestrictedMethods({
getIdentities: () => {
return {
'0x7e57e2': { lastSelected: 1000 },
'0x7e57e3': {},
'0x7e57e4': { lastSelected: 2000 },
'0x7e57e5': {},
'0x7e57e6': {},
};
},
getKeyringAccounts: async () => [...keyringAccounts],
});
const ethAccountsMethod = pify(restrictedMethods.eth_accounts.method);
const res = {};
await ethAccountsMethod(null, res, null);
assert.deepEqual(
res,
{ result: expectedResult },
'should return accounts in correct order',
);
});
});
});

@ -1,40 +0,0 @@
export default function getRestrictedMethods({
getIdentities,
getKeyringAccounts,
}) {
return {
eth_accounts: {
method: async (_, res, __, end) => {
try {
const accounts = await getKeyringAccounts();
const identities = getIdentities();
res.result = accounts.sort((firstAddress, secondAddress) => {
if (!identities[firstAddress]) {
throw new Error(`Missing identity for address ${firstAddress}`);
} else if (!identities[secondAddress]) {
throw new Error(`Missing identity for address ${secondAddress}`);
} else if (
identities[firstAddress].lastSelected ===
identities[secondAddress].lastSelected
) {
return 0;
} else if (identities[firstAddress].lastSelected === undefined) {
return 1;
} else if (identities[secondAddress].lastSelected === undefined) {
return -1;
}
return (
identities[secondAddress].lastSelected -
identities[firstAddress].lastSelected
);
});
end();
} catch (err) {
res.error = err;
end(err);
}
},
},
};
}

@ -0,0 +1,84 @@
import { createSelector } from 'reselect';
import { CaveatTypes } from '../../../../shared/constants/permissions';
/**
* This file contains selectors for PermissionController selector event
* subscriptions, used to detect whenever a subject's accounts change so that
* we can notify the subject via the `accountsChanged` provider event.
*/
/**
* @param {Record<string, Record<string, unknown>>} state - The
* PermissionController state.
* @returns {Record<string, unknown>} The PermissionController subjects.
*/
const getSubjects = (state) => state.subjects;
/**
* Get the permitted accounts for each subject, keyed by origin.
* The values of the returned map are immutable values from the
* PermissionController state.
*
* @returns {Map<string, string[]>} The current origin:accounts[] map.
*/
export const getPermittedAccountsByOrigin = createSelector(
getSubjects,
(subjects) => {
return Object.values(subjects).reduce((originToAccountsMap, subject) => {
const caveat = subject.permissions?.eth_accounts?.caveats.find(
({ type }) => type === CaveatTypes.restrictReturnedAccounts,
);
if (caveat) {
originToAccountsMap.set(subject.origin, caveat.value);
}
return originToAccountsMap;
}, new Map());
},
);
/**
* Given the current and previous exposed accounts for each PermissionController
* subject, returns a new map containing all accounts that have changed.
* The values of each map must be immutable values directly from the
* PermissionController state, or an empty array instantiated in this
* function.
*
* @param {Map<string, string[]>} newAccountsMap - The new origin:accounts[] map.
* @param {Map<string, string[]>} [previousAccountsMap] - The previous origin:accounts[] map.
* @returns {Map<string, string[]>} The origin:accounts[] map of changed accounts.
*/
export const getChangedAccounts = (newAccountsMap, previousAccountsMap) => {
if (previousAccountsMap === undefined) {
return newAccountsMap;
}
const changedAccounts = new Map();
if (newAccountsMap === previousAccountsMap) {
return changedAccounts;
}
const newOrigins = new Set([...newAccountsMap.keys()]);
for (const origin of previousAccountsMap.keys()) {
const newAccounts = newAccountsMap.get(origin) ?? [];
// The values of these maps are references to immutable values, which is why
// a strict equality check is enough for diffing. The values are either from
// PermissionController state, or an empty array initialized in the previous
// call to this function. `newAccountsMap` will never contain any empty
// arrays.
if (previousAccountsMap.get(origin) !== newAccounts) {
changedAccounts.set(origin, newAccounts);
}
newOrigins.delete(origin);
}
// By now, newOrigins is either empty or contains some number of previously
// unencountered origins, and all of their accounts have "changed".
for (const origin of newOrigins.keys()) {
changedAccounts.set(origin, newAccountsMap.get(origin));
}
return changedAccounts;
};

@ -0,0 +1,116 @@
import { cloneDeep } from 'lodash';
import { getChangedAccounts, getPermittedAccountsByOrigin } from './selectors';
describe('PermissionController selectors', () => {
describe('getChangedAccounts', () => {
it('returns the new value if the previous value is undefined', () => {
const newAccounts = new Map([['foo.bar', ['0x1']]]);
expect(getChangedAccounts(newAccounts)).toBe(newAccounts);
});
it('returns an empty map if the new and previous values are the same', () => {
const newAccounts = new Map([['foo.bar', ['0x1']]]);
expect(getChangedAccounts(newAccounts, newAccounts)).toStrictEqual(
new Map(),
);
});
it('returns a new map of the changed accounts if the new and previous values differ', () => {
// We set this on the new and previous value under the key 'foo.bar' to
// check that identical values are excluded.
const identicalValue = ['0x1'];
const previousAccounts = new Map([
['bar.baz', ['0x1']], // included: different accounts
['fizz.buzz', ['0x1']], // included: removed in new value
]);
previousAccounts.set('foo.bar', identicalValue);
const newAccounts = new Map([
['bar.baz', ['0x1', '0x2']], // included: different accounts
['baz.fizz', ['0x3']], // included: brand new
]);
newAccounts.set('foo.bar', identicalValue);
expect(getChangedAccounts(newAccounts, previousAccounts)).toStrictEqual(
new Map([
['bar.baz', ['0x1', '0x2']],
['fizz.buzz', []],
['baz.fizz', ['0x3']],
]),
);
});
});
describe('getPermittedAccountsByOrigin', () => {
it('memoizes and gets permitted accounts by origin', () => {
const state1 = {
subjects: {
'foo.bar': {
origin: 'foo.bar',
permissions: {
eth_accounts: {
caveats: [{ type: 'restrictReturnedAccounts', value: ['0x1'] }],
},
},
},
'bar.baz': {
origin: 'bar.baz',
permissions: {
eth_accounts: {
caveats: [{ type: 'restrictReturnedAccounts', value: ['0x2'] }],
},
},
},
'baz.bizz': {
origin: 'baz.fizz',
permissions: {
eth_accounts: {
caveats: [
{ type: 'restrictReturnedAccounts', value: ['0x1', '0x2'] },
],
},
},
},
'no.accounts': {
// we shouldn't see this in the result
permissions: {
foobar: {},
},
},
},
};
const expected1 = new Map([
['foo.bar', ['0x1']],
['bar.baz', ['0x2']],
['baz.fizz', ['0x1', '0x2']],
]);
const selected1 = getPermittedAccountsByOrigin(state1);
expect(selected1).toStrictEqual(expected1);
// The selector should return the memoized value if state.subjects is
// the same object
expect(selected1).toBe(getPermittedAccountsByOrigin(state1));
// If we mutate the state, the selector return value should be different
// from the first.
const state2 = cloneDeep(state1);
delete state2.subjects['foo.bar'];
const expected2 = new Map([
['bar.baz', ['0x2']],
['baz.fizz', ['0x1', '0x2']],
]);
const selected2 = getPermittedAccountsByOrigin(state2);
expect(selected2).toStrictEqual(expected2);
expect(selected2).not.toBe(selected1);
// Since we didn't mutate the state at this point, the value should once
// again be the memoized.
expect(selected2).toBe(getPermittedAccountsByOrigin(state2));
});
});
});

@ -0,0 +1,258 @@
import { constructPermission } from '@metamask/snap-controllers';
import {
CaveatTypes,
RestrictedMethods,
} from '../../../../shared/constants/permissions';
/**
* This file contains the specifications of the permissions and caveats
* that are recognized by our permission system. See the PermissionController
* README in @metamask/snap-controllers for details.
*/
/**
* The "keys" of all of permissions recognized by the PermissionController.
* Permission keys and names have distinct meanings in the permission system.
*/
const PermissionKeys = Object.freeze({
...RestrictedMethods,
});
/**
* Factory functions for all caveat types recognized by the
* PermissionController.
*/
const CaveatFactories = Object.freeze({
[CaveatTypes.restrictReturnedAccounts]: (accounts) => {
return { type: CaveatTypes.restrictReturnedAccounts, value: accounts };
},
});
/**
* A PreferencesController identity object.
*
* @typedef {Object} Identity
* @property {string} address - The address of the identity.
* @property {string} name - The name of the identity.
* @property {number} [lastSelected] - Unix timestamp of when the identity was
* last selected in the UI.
*/
/**
* Gets the specifications for all caveats that will be recognized by the
* PermissionController.
*
* @param {{
* getIdentities: () => Record<string, Identity>,
* }} options - Options bag.
*/
export const getCaveatSpecifications = ({ getIdentities }) => {
return {
[CaveatTypes.restrictReturnedAccounts]: {
type: CaveatTypes.restrictReturnedAccounts,
decorator: (method, caveat) => {
return async (args) => {
const result = await method(args);
return result
.filter((account) => caveat.value.includes(account))
.slice(0, 1);
};
},
validator: (caveat, _origin, _target) =>
validateCaveatAccounts(caveat.value, getIdentities),
},
};
};
/**
* Gets the specifications for all permissions that will be recognized by the
* PermissionController.
*
* @param {{
* getAllAccounts: () => Promise<string[]>,
* getIdentities: () => Record<string, Identity>,
* }} options - Options bag.
* @param options.getAllAccounts - A function that returns all Ethereum accounts
* in the current MetaMask instance.
* @param options.getIdentities - A function that returns the
* `PreferencesController` identity objects for all Ethereum accounts in the
* current MetaMask instance.
*/
export const getPermissionSpecifications = ({
getAllAccounts,
getIdentities,
}) => {
return {
[PermissionKeys.eth_accounts]: {
targetKey: PermissionKeys.eth_accounts,
allowedCaveats: [CaveatTypes.restrictReturnedAccounts],
factory: (permissionOptions, requestData) => {
if (Array.isArray(permissionOptions.caveats)) {
throw new Error(
`${PermissionKeys.eth_accounts} error: Received unexpected caveats. Any permitted caveats will be added automatically.`,
);
}
// This value will be further validated as part of the caveat.
if (!requestData.approvedAccounts) {
throw new Error(
`${PermissionKeys.eth_accounts} error: No approved accounts specified.`,
);
}
return constructPermission({
...permissionOptions,
caveats: [
CaveatFactories[CaveatTypes.restrictReturnedAccounts](
requestData.approvedAccounts,
),
],
});
},
methodImplementation: async (_args) => {
const accounts = await getAllAccounts();
const identities = getIdentities();
return accounts.sort((firstAddress, secondAddress) => {
if (!identities[firstAddress]) {
throw new Error(`Missing identity for address: "${firstAddress}".`);
} else if (!identities[secondAddress]) {
throw new Error(
`Missing identity for address: "${secondAddress}".`,
);
} else if (
identities[firstAddress].lastSelected ===
identities[secondAddress].lastSelected
) {
return 0;
} else if (identities[firstAddress].lastSelected === undefined) {
return 1;
} else if (identities[secondAddress].lastSelected === undefined) {
return -1;
}
return (
identities[secondAddress].lastSelected -
identities[firstAddress].lastSelected
);
});
},
validator: (permission, _origin, _target) => {
const { caveats } = permission;
if (
!caveats ||
caveats.length !== 1 ||
caveats[0].type !== CaveatTypes.restrictReturnedAccounts
) {
throw new Error(
`${PermissionKeys.eth_accounts} error: Invalid caveats. There must be a single caveat of type "${CaveatTypes.restrictReturnedAccounts}".`,
);
}
},
},
};
};
/**
* Validates the accounts associated with a caveat. In essence, ensures that
* the accounts value is an array of non-empty strings, and that each string
* corresponds to a PreferencesController identity.
*
* @param {string[]} accounts - The accounts associated with the caveat.
* @param {() => Record<string, Identity>} getIdentities - Gets all
* PreferencesController identities.
*/
function validateCaveatAccounts(accounts, getIdentities) {
if (!Array.isArray(accounts) || accounts.length === 0) {
throw new Error(
`${PermissionKeys.eth_accounts} error: Expected non-empty array of Ethereum addresses.`,
);
}
const identities = getIdentities();
accounts.forEach((address) => {
if (!address || typeof address !== 'string') {
throw new Error(
`${PermissionKeys.eth_accounts} error: Expected an array of Ethereum addresses. Received: "${address}".`,
);
}
if (!identities[address]) {
throw new Error(
`${PermissionKeys.eth_accounts} error: Received unrecognized address: "${address}".`,
);
}
});
}
/**
* All unrestricted methods recognized by the PermissionController.
* Unrestricted methods are ignored by the permission system, but every
* JSON-RPC request seen by the permission system must correspond to a
* restricted or unrestricted method, or the request will be rejected with a
* "method not found" error.
*/
export const unrestrictedMethods = Object.freeze([
'eth_blockNumber',
'eth_call',
'eth_chainId',
'eth_coinbase',
'eth_decrypt',
'eth_estimateGas',
'eth_feeHistory',
'eth_gasPrice',
'eth_getBalance',
'eth_getBlockByHash',
'eth_getBlockByNumber',
'eth_getBlockTransactionCountByHash',
'eth_getBlockTransactionCountByNumber',
'eth_getCode',
'eth_getEncryptionPublicKey',
'eth_getFilterChanges',
'eth_getFilterLogs',
'eth_getLogs',
'eth_getProof',
'eth_getStorageAt',
'eth_getTransactionByBlockHashAndIndex',
'eth_getTransactionByBlockNumberAndIndex',
'eth_getTransactionByHash',
'eth_getTransactionCount',
'eth_getTransactionReceipt',
'eth_getUncleByBlockHashAndIndex',
'eth_getUncleByBlockNumberAndIndex',
'eth_getUncleCountByBlockHash',
'eth_getUncleCountByBlockNumber',
'eth_getWork',
'eth_hashrate',
'eth_mining',
'eth_newBlockFilter',
'eth_newFilter',
'eth_newPendingTransactionFilter',
'eth_protocolVersion',
'eth_sendRawTransaction',
'eth_sendTransaction',
'eth_sign',
'eth_signTypedData',
'eth_signTypedData_v1',
'eth_signTypedData_v3',
'eth_signTypedData_v4',
'eth_submitHashrate',
'eth_submitWork',
'eth_syncing',
'eth_uninstallFilter',
'metamask_getProviderState',
'metamask_watchAsset',
'net_listening',
'net_peerCount',
'net_version',
'personal_ecRecover',
'personal_sign',
'wallet_watchAsset',
'web3_clientVersion',
'web3_sha3',
]);

@ -0,0 +1,340 @@
import {
CaveatTypes,
RestrictedMethods,
} from '../../../../shared/constants/permissions';
import {
getCaveatSpecifications,
getPermissionSpecifications,
unrestrictedMethods,
} from './specifications';
// Note: This causes Date.now() to return the number 1.
jest.useFakeTimers('modern').setSystemTime(1);
describe('PermissionController specifications', () => {
describe('caveat specifications', () => {
it('getCaveatSpecifications returns the expected specifications object', () => {
const caveatSpecifications = getCaveatSpecifications({});
expect(Object.keys(caveatSpecifications)).toHaveLength(1);
expect(
caveatSpecifications[CaveatTypes.restrictReturnedAccounts].type,
).toStrictEqual(CaveatTypes.restrictReturnedAccounts);
});
describe('restrictReturnedAccounts', () => {
describe('decorator', () => {
it('returns the first array member included in the caveat value', async () => {
const getIdentities = jest.fn();
const { decorator } = getCaveatSpecifications({ getIdentities })[
CaveatTypes.restrictReturnedAccounts
];
const method = async () => ['0x1', '0x2', '0x3'];
const caveat = {
type: CaveatTypes.restrictReturnedAccounts,
value: ['0x1', '0x2'],
};
const decorated = decorator(method, caveat);
expect(await decorated()).toStrictEqual(['0x1']);
});
it('returns an empty array if no array members are included in the caveat value', async () => {
const getIdentities = jest.fn();
const { decorator } = getCaveatSpecifications({ getIdentities })[
CaveatTypes.restrictReturnedAccounts
];
const method = async () => ['0x1', '0x2', '0x3'];
const caveat = {
type: CaveatTypes.restrictReturnedAccounts,
value: ['0x5'],
};
const decorated = decorator(method, caveat);
expect(await decorated()).toStrictEqual([]);
});
it('returns an empty array if the method result is an empty array', async () => {
const getIdentities = jest.fn();
const { decorator } = getCaveatSpecifications({ getIdentities })[
CaveatTypes.restrictReturnedAccounts
];
const method = async () => [];
const caveat = {
type: CaveatTypes.restrictReturnedAccounts,
value: ['0x1', '0x2'],
};
const decorated = decorator(method, caveat);
expect(await decorated()).toStrictEqual([]);
});
});
describe('validator', () => {
it('rejects invalid array values', () => {
const getIdentities = jest.fn();
const { validator } = getCaveatSpecifications({ getIdentities })[
CaveatTypes.restrictReturnedAccounts
];
[null, 'foo', {}, []].forEach((invalidValue) => {
expect(() => validator({ value: invalidValue })).toThrow(
/Expected non-empty array of Ethereum addresses\.$/u,
);
});
});
it('rejects falsy or non-string addresses', () => {
const getIdentities = jest.fn();
const { validator } = getCaveatSpecifications({ getIdentities })[
CaveatTypes.restrictReturnedAccounts
];
[[{}], [[]], [null], ['']].forEach((invalidValue) => {
expect(() => validator({ value: invalidValue })).toThrow(
/Expected an array of Ethereum addresses. Received:/u,
);
});
});
it('rejects addresses that have no corresponding identity', () => {
const getIdentities = jest.fn().mockImplementationOnce(() => {
return {
'0x1': true,
'0x3': true,
};
});
const { validator } = getCaveatSpecifications({ getIdentities })[
CaveatTypes.restrictReturnedAccounts
];
expect(() => validator({ value: ['0x1', '0x2', '0x3'] })).toThrow(
/Received unrecognized address:/u,
);
});
});
});
});
describe('permission specifications', () => {
it('getPermissionSpecifications returns the expected specifications object', () => {
const permissionSpecifications = getPermissionSpecifications({});
expect(Object.keys(permissionSpecifications)).toHaveLength(1);
expect(
permissionSpecifications[RestrictedMethods.eth_accounts].targetKey,
).toStrictEqual(RestrictedMethods.eth_accounts);
});
describe('eth_accounts', () => {
describe('factory', () => {
it('constructs a valid eth_accounts permission', () => {
const getIdentities = jest.fn();
const getAllAccounts = jest.fn();
const { factory } = getPermissionSpecifications({
getIdentities,
getAllAccounts,
})[RestrictedMethods.eth_accounts];
expect(
factory(
{ invoker: 'foo.bar', target: 'eth_accounts' },
{ approvedAccounts: ['0x1'] },
),
).toStrictEqual({
caveats: [
{
type: CaveatTypes.restrictReturnedAccounts,
value: ['0x1'],
},
],
date: 1,
id: expect.any(String),
invoker: 'foo.bar',
parentCapability: 'eth_accounts',
});
});
it('throws an error if no approvedAccounts are specified', () => {
const getIdentities = jest.fn();
const getAllAccounts = jest.fn();
const { factory } = getPermissionSpecifications({
getIdentities,
getAllAccounts,
})[RestrictedMethods.eth_accounts];
expect(() =>
factory(
{ invoker: 'foo.bar', target: 'eth_accounts' },
{}, // no approvedAccounts
),
).toThrow(/No approved accounts specified\.$/u);
});
it('throws an error if any caveats are specified directly', () => {
const getIdentities = jest.fn();
const getAllAccounts = jest.fn();
const { factory } = getPermissionSpecifications({
getIdentities,
getAllAccounts,
})[RestrictedMethods.eth_accounts];
expect(() =>
factory(
{
caveats: [
{
type: CaveatTypes.restrictReturnedAccounts,
value: ['0x1', '0x2'],
},
],
invoker: 'foo.bar',
target: 'eth_accounts',
},
{ approvedAccounts: ['0x1'] },
),
).toThrow(/Received unexpected caveats./u);
});
});
describe('methodImplementation', () => {
it('returns the keyring accounts in lastSelected order', async () => {
const getIdentities = jest.fn().mockImplementationOnce(() => {
return {
'0x1': {
lastSelected: 1,
},
'0x2': {},
'0x3': {
lastSelected: 3,
},
'0x4': {
lastSelected: 3,
},
};
});
const getAllAccounts = jest
.fn()
.mockImplementationOnce(() => ['0x1', '0x2', '0x3', '0x4']);
const { methodImplementation } = getPermissionSpecifications({
getIdentities,
getAllAccounts,
})[RestrictedMethods.eth_accounts];
expect(await methodImplementation()).toStrictEqual([
'0x3',
'0x4',
'0x1',
'0x2',
]);
});
it('throws if a keyring account is missing an address (case 1)', async () => {
const getIdentities = jest.fn().mockImplementationOnce(() => {
return {
'0x2': {
lastSelected: 3,
},
'0x3': {
lastSelected: 3,
},
};
});
const getAllAccounts = jest
.fn()
.mockImplementationOnce(() => ['0x1', '0x2', '0x3']);
const { methodImplementation } = getPermissionSpecifications({
getIdentities,
getAllAccounts,
})[RestrictedMethods.eth_accounts];
await expect(() => methodImplementation()).rejects.toThrow(
'Missing identity for address: "0x1".',
);
});
it('throws if a keyring account is missing an address (case 2)', async () => {
const getIdentities = jest.fn().mockImplementationOnce(() => {
return {
'0x1': {
lastSelected: 1,
},
'0x3': {
lastSelected: 3,
},
};
});
const getAllAccounts = jest
.fn()
.mockImplementationOnce(() => ['0x1', '0x2', '0x3']);
const { methodImplementation } = getPermissionSpecifications({
getIdentities,
getAllAccounts,
})[RestrictedMethods.eth_accounts];
await expect(() => methodImplementation()).rejects.toThrow(
'Missing identity for address: "0x2".',
);
});
});
describe('validator', () => {
it('accepts valid permissions', () => {
const getIdentities = jest.fn();
const getAllAccounts = jest.fn();
const { validator } = getPermissionSpecifications({
getIdentities,
getAllAccounts,
})[RestrictedMethods.eth_accounts];
expect(() =>
validator({
caveats: [
{
type: CaveatTypes.restrictReturnedAccounts,
value: ['0x1', '0x2'],
},
],
date: 1,
id: expect.any(String),
invoker: 'foo.bar',
parentCapability: 'eth_accounts',
}),
).not.toThrow();
});
it('rejects invalid caveats', () => {
const getIdentities = jest.fn();
const getAllAccounts = jest.fn();
const { validator } = getPermissionSpecifications({
getIdentities,
getAllAccounts,
})[RestrictedMethods.eth_accounts];
[null, [], [1, 2], [{ type: 'foobar' }]].forEach(
(invalidCaveatsValue) => {
expect(() =>
validator({
caveats: invalidCaveatsValue,
date: 1,
id: expect.any(String),
invoker: 'foo.bar',
parentCapability: 'eth_accounts',
}),
).toThrow(/Invalid caveats./u);
},
);
});
});
});
});
describe('unrestricted methods', () => {
it('defines the unrestricted methods', () => {
expect(Array.isArray(unrestrictedMethods)).toBe(true);
expect(Object.isFrozen(unrestrictedMethods)).toBe(true);
});
});
});

@ -3,7 +3,10 @@ import { ObservableStore } from '@metamask/obs-store';
import { normalize as normalizeAddress } from 'eth-sig-util';
import { ethers } from 'ethers';
import log from 'loglevel';
import { NETWORK_TYPE_TO_ID_MAP } from '../../../shared/constants/network';
import {
IPFS_DEFAULT_GATEWAY_URL,
NETWORK_TYPE_TO_ID_MAP,
} from '../../../shared/constants/network';
import { isPrefixedFormattedHexString } from '../../../shared/modules/network.utils';
import { LEDGER_TRANSPORT_TYPES } from '../../../shared/constants/hardware-wallets';
import { NETWORK_EVENTS } from './network';
@ -61,7 +64,7 @@ export default class PreferencesController {
hideZeroBalanceTokens: false,
},
// ENS decentralized website resolution
ipfsGateway: 'dweb.link',
ipfsGateway: IPFS_DEFAULT_GATEWAY_URL,
infuraBlocked: null,
ledgerTransportType: window.navigator.hid
? LEDGER_TRANSPORT_TYPES.WEBHID

@ -31,6 +31,7 @@ describe('preferences controller', function () {
.callsFake(() => ({ type: 'mainnet' }));
preferencesController = new PreferencesController({
initLangCode: 'en_US',
migrateAddressBookState,
network,
provider,
@ -41,6 +42,30 @@ describe('preferences controller', function () {
sinon.restore();
});
describe('useBlockie', function () {
it('defaults useBlockie to false', function () {
assert.equal(preferencesController.store.getState().useBlockie, false);
});
it('setUseBlockie to true', function () {
preferencesController.setUseBlockie(true);
assert.equal(preferencesController.store.getState().useBlockie, true);
});
});
describe('setCurrentLocale', function () {
it('checks the default currentLocale', function () {
const { currentLocale } = preferencesController.store.getState();
assert.equal(currentLocale, 'en_US');
});
it('sets current locale in preferences controller', function () {
preferencesController.setCurrentLocale('ja');
const { currentLocale } = preferencesController.store.getState();
assert.equal(currentLocale, 'ja');
});
});
describe('setAddresses', function () {
it('should keep a map of addresses to names and addresses in the store', function () {
preferencesController.setAddresses(['0xda22le', '0x7e57e2']);

@ -977,7 +977,7 @@ export default class TransactionController extends EventEmitter {
* @param {number} txId - The tx's ID
* @returns {Promise<void>}
*/
async confirmTransaction(txId, txReceipt, baseFeePerGas) {
async confirmTransaction(txId, txReceipt, baseFeePerGas, blockTimestamp) {
// get the txReceipt before marking the transaction confirmed
// to ensure the receipt is gotten before the ui revives the tx
const txMeta = this.txStateManager.getTransaction(txId);
@ -1002,6 +1002,9 @@ export default class TransactionController extends EventEmitter {
if (baseFeePerGas) {
txMeta.baseFeePerGas = baseFeePerGas;
}
if (blockTimestamp) {
txMeta.blockTimestamp = blockTimestamp;
}
this.txStateManager.setTxStatusConfirmed(txId);
this._markNonceDuplicatesDropped(txId);
@ -1183,8 +1186,13 @@ export default class TransactionController extends EventEmitter {
});
this.pendingTxTracker.on(
'tx:confirmed',
(txId, transactionReceipt, baseFeePerGas) =>
this.confirmTransaction(txId, transactionReceipt, baseFeePerGas),
(txId, transactionReceipt, baseFeePerGas, blockTimestamp) =>
this.confirmTransaction(
txId,
transactionReceipt,
baseFeePerGas,
blockTimestamp,
),
);
this.pendingTxTracker.on('tx:dropped', (txId) => {
this._dropTransaction(txId);

@ -38,7 +38,6 @@ export function generateHistoryEntry(previousState, newState, note) {
if (note) {
entry[0].note = note;
}
entry[0].timestamp = Date.now();
}
return entry;

@ -119,9 +119,9 @@ describe('Transaction state history helper', function () {
},
};
const before = new Date().getTime();
const timeBefore = new Date().getTime();
const result = generateHistoryEntry(prevState, nextState, note);
const after = new Date().getTime();
const timeAfter = new Date().getTime();
assert.ok(Array.isArray(result));
assert.equal(result.length, 3);
@ -134,7 +134,9 @@ describe('Transaction state history helper', function () {
assert.equal(result[0].path, expectedEntry1.path);
assert.equal(result[0].value, expectedEntry1.value);
assert.equal(result[0].note, note);
assert.ok(result[0].timestamp >= before && result[0].timestamp <= after);
assert.ok(
result[0].timestamp >= timeBefore && result[0].timestamp <= timeAfter,
);
const expectedEntry2 = {
op: 'replace',

@ -164,6 +164,7 @@ export default class PendingTransactionTracker extends EventEmitter {
* @emits tx:warning
* @private
*/
async _checkPendingTx(txMeta) {
const txHash = txMeta.hash;
const txId = txMeta.id;
@ -193,11 +194,21 @@ export default class PendingTransactionTracker extends EventEmitter {
try {
const transactionReceipt = await this.query.getTransactionReceipt(txHash);
if (transactionReceipt?.blockNumber) {
const { baseFeePerGas } = await this.query.getBlockByHash(
const {
baseFeePerGas,
timestamp: blockTimestamp,
} = await this.query.getBlockByHash(
transactionReceipt?.blockHash,
false,
);
this.emit('tx:confirmed', txId, transactionReceipt, baseFeePerGas);
this.emit(
'tx:confirmed',
txId,
transactionReceipt,
baseFeePerGas,
blockTimestamp,
);
return;
}
} catch (err) {

@ -845,9 +845,9 @@ describe('TransactionStateManager', function () {
);
// modify value and updateTransaction
updatedTx.txParams.gasPrice = desiredGasPrice;
const before = new Date().getTime();
const timeBefore = new Date().getTime();
txStateManager.updateTransaction(updatedTx);
const after = new Date().getTime();
const timeAfter = new Date().getTime();
// check updated value
const result = txStateManager.getTransaction('1');
assert.equal(
@ -888,8 +888,8 @@ describe('TransactionStateManager', function () {
'two history items (initial + diff) value',
);
assert.ok(
result.history[1][0].timestamp >= before &&
result.history[1][0].timestamp <= after,
result.history[1][0].timestamp >= timeBefore &&
result.history[1][0].timestamp <= timeAfter,
);
});

@ -1,4 +1,3 @@
import { strict as assert } from 'assert';
import { ObservableStore } from '@metamask/obs-store';
import {
BaseController,
@ -48,17 +47,17 @@ class ExampleController extends BaseControllerV2 {
}
}
describe('ComposableObservableStore', function () {
it('should register initial state', function () {
describe('ComposableObservableStore', () => {
it('should register initial state', () => {
const controllerMessenger = new ControllerMessenger();
const store = new ComposableObservableStore({
controllerMessenger,
state: 'state',
});
assert.strictEqual(store.getState(), 'state');
expect(store.getState()).toStrictEqual('state');
});
it('should register initial structure', function () {
it('should register initial structure', () => {
const controllerMessenger = new ControllerMessenger();
const testStore = new ObservableStore();
const store = new ComposableObservableStore({
@ -66,28 +65,28 @@ describe('ComposableObservableStore', function () {
controllerMessenger,
});
testStore.putState('state');
assert.deepEqual(store.getState(), { TestStore: 'state' });
expect(store.getState()).toStrictEqual({ TestStore: 'state' });
});
it('should update structure with observable store', function () {
it('should update structure with observable store', () => {
const controllerMessenger = new ControllerMessenger();
const testStore = new ObservableStore();
const store = new ComposableObservableStore({ controllerMessenger });
store.updateStructure({ TestStore: testStore });
testStore.putState('state');
assert.deepEqual(store.getState(), { TestStore: 'state' });
expect(store.getState()).toStrictEqual({ TestStore: 'state' });
});
it('should update structure with BaseController-based controller', function () {
it('should update structure with BaseController-based controller', () => {
const controllerMessenger = new ControllerMessenger();
const oldExampleController = new OldExampleController();
const store = new ComposableObservableStore({ controllerMessenger });
store.updateStructure({ OldExample: oldExampleController });
oldExampleController.updateBaz('state');
assert.deepEqual(store.getState(), { OldExample: { baz: 'state' } });
expect(store.getState()).toStrictEqual({ OldExample: { baz: 'state' } });
});
it('should update structure with BaseControllerV2-based controller', function () {
it('should update structure with BaseControllerV2-based controller', () => {
const controllerMessenger = new ControllerMessenger();
const exampleController = new ExampleController({
messenger: controllerMessenger,
@ -95,11 +94,10 @@ describe('ComposableObservableStore', function () {
const store = new ComposableObservableStore({ controllerMessenger });
store.updateStructure({ Example: exampleController });
exampleController.updateBar('state');
console.log(exampleController.state);
assert.deepEqual(store.getState(), { Example: { bar: 'state' } });
expect(store.getState()).toStrictEqual({ Example: { bar: 'state' } });
});
it('should update structure with all three types of stores', function () {
it('should update structure with all three types of stores', () => {
const controllerMessenger = new ControllerMessenger();
const exampleStore = new ObservableStore();
const exampleController = new ExampleController({
@ -115,14 +113,14 @@ describe('ComposableObservableStore', function () {
exampleStore.putState('state');
exampleController.updateBar('state');
oldExampleController.updateBaz('state');
assert.deepEqual(store.getState(), {
expect(store.getState()).toStrictEqual({
Example: { bar: 'state' },
OldExample: { baz: 'state' },
Store: 'state',
});
});
it('should return flattened state', function () {
it('should return flattened state', () => {
const controllerMessenger = new ControllerMessenger();
const fooStore = new ObservableStore({ foo: 'foo' });
const barController = new ExampleController({
@ -142,46 +140,48 @@ describe('ComposableObservableStore', function () {
BazStore: bazController.state,
},
});
assert.deepEqual(store.getFlatState(), {
expect(store.getFlatState()).toStrictEqual({
foo: 'foo',
bar: 'bar',
baz: 'baz',
});
});
it('should return empty flattened state when not configured', function () {
it('should return empty flattened state when not configured', () => {
const controllerMessenger = new ControllerMessenger();
const store = new ComposableObservableStore({ controllerMessenger });
assert.deepEqual(store.getFlatState(), {});
expect(store.getFlatState()).toStrictEqual({});
});
it('should throw if the controller messenger is omitted and the config includes a BaseControllerV2 controller', function () {
it('should throw if the controller messenger is omitted and the config includes a BaseControllerV2 controller', () => {
const controllerMessenger = new ControllerMessenger();
const exampleController = new ExampleController({
messenger: controllerMessenger,
});
assert.throws(
expect(
() =>
new ComposableObservableStore({
config: {
Example: exampleController,
},
}),
);
).toThrow(`Cannot read property 'subscribe' of undefined`);
});
it('should throw if the controller messenger is omitted and updateStructure called with a BaseControllerV2 controller', function () {
it('should throw if the controller messenger is omitted and updateStructure called with a BaseControllerV2 controller', () => {
const controllerMessenger = new ControllerMessenger();
const exampleController = new ExampleController({
messenger: controllerMessenger,
});
const store = new ComposableObservableStore({});
assert.throws(() => store.updateStructure({ Example: exampleController }));
expect(() => store.updateStructure({ Example: exampleController })).toThrow(
`Cannot read property 'subscribe' of undefined`,
);
});
it('should throw if initialized with undefined config entry', function () {
it('should throw if initialized with undefined config entry', () => {
const controllerMessenger = new ControllerMessenger();
assert.throws(
expect(
() =>
new ComposableObservableStore({
config: {
@ -189,6 +189,6 @@ describe('ComposableObservableStore', function () {
},
controllerMessenger,
}),
);
).toThrow(`Undefined 'Example'`);
});
});

@ -27,7 +27,7 @@ const createWyrePurchaseUrl = async (address) => {
const response = await fetchWithTimeout(fiatOnRampUrlApi, {
method: 'GET',
headers: {
'Accept': 'application/json',
Accept: 'application/json',
'Content-Type': 'application/json',
},
});

@ -1,4 +1,3 @@
import { strict as assert } from 'assert';
import nock from 'nock';
import {
KOVAN_CHAIN_ID,
@ -27,8 +26,8 @@ const KOVAN = {
chainId: KOVAN_CHAIN_ID,
};
describe('buy-eth-url', function () {
it('returns Wyre url with an ETH address for Ethereum mainnet', async function () {
describe('buy-eth-url', () => {
it('returns Wyre url with an ETH address for Ethereum mainnet', async () => {
nock(SWAPS_API_V2_BASE_URL)
.get(
`/networks/1/fiatOnRampUrl?serviceName=wyre&destinationAddress=${ETH_ADDRESS}`,
@ -37,43 +36,40 @@ describe('buy-eth-url', function () {
url: `https://pay.sendwyre.com/purchase?accountId=${WYRE_ACCOUNT_ID}&utm_campaign=${WYRE_ACCOUNT_ID}&destCurrency=ETH&utm_medium=widget&paymentMethod=debit-card&reservation=MLZVUF8FMXZUMARJC23B&dest=ethereum%3A${ETH_ADDRESS}&utm_source=checkout`,
});
const wyreUrl = await getBuyEthUrl(MAINNET);
assert.equal(
wyreUrl,
expect(wyreUrl).toStrictEqual(
`https://pay.sendwyre.com/purchase?accountId=${WYRE_ACCOUNT_ID}&utm_campaign=${WYRE_ACCOUNT_ID}&destCurrency=ETH&utm_medium=widget&paymentMethod=debit-card&reservation=MLZVUF8FMXZUMARJC23B&dest=ethereum%3A${ETH_ADDRESS}&utm_source=checkout`,
);
nock.cleanAll();
});
it('returns a fallback Wyre url if /orders/reserve API call fails', async function () {
it('returns a fallback Wyre url if /orders/reserve API call fails', async () => {
const wyreUrl = await getBuyEthUrl(MAINNET);
assert.equal(
wyreUrl,
expect(wyreUrl).toStrictEqual(
`https://pay.sendwyre.com/purchase?dest=ethereum:${ETH_ADDRESS}&destCurrency=ETH&accountId=${WYRE_ACCOUNT_ID}&paymentMethod=debit-card`,
);
});
it('returns Transak url with an ETH address for Ethereum mainnet', async function () {
it('returns Transak url with an ETH address for Ethereum mainnet', async () => {
const transakUrl = await getBuyEthUrl({ ...MAINNET, service: 'transak' });
assert.equal(
transakUrl,
expect(transakUrl).toStrictEqual(
`https://global.transak.com/?apiKey=${TRANSAK_API_KEY}&hostURL=https%3A%2F%2Fmetamask.io&defaultCryptoCurrency=ETH&walletAddress=${ETH_ADDRESS}`,
);
});
it('returns metamask ropsten faucet for network 3', async function () {
it('returns metamask ropsten faucet for network 3', async () => {
const ropstenUrl = await getBuyEthUrl(ROPSTEN);
assert.equal(ropstenUrl, 'https://faucet.metamask.io/');
expect(ropstenUrl).toStrictEqual('https://faucet.metamask.io/');
});
it('returns rinkeby dapp for network 4', async function () {
it('returns rinkeby dapp for network 4', async () => {
const rinkebyUrl = await getBuyEthUrl(RINKEBY);
assert.equal(rinkebyUrl, 'https://www.rinkeby.io/');
expect(rinkebyUrl).toStrictEqual('https://www.rinkeby.io/');
});
it('returns kovan github test faucet for network 42', async function () {
it('returns kovan github test faucet for network 42', async () => {
const kovanUrl = await getBuyEthUrl(KOVAN);
assert.equal(kovanUrl, 'https://github.com/kovan-testnet/faucet');
expect(kovanUrl).toStrictEqual('https://github.com/kovan-testnet/faucet');
});
});

@ -1,34 +1,36 @@
import { strict as assert } from 'assert';
import cleanErrorStack from './cleanErrorStack';
describe('Clean Error Stack', function () {
describe('Clean Error Stack', () => {
const testMessage = 'Test Message';
const testError = new Error(testMessage);
const undefinedErrorName = new Error(testMessage);
const blankErrorName = new Error(testMessage);
const blankMsgError = new Error();
beforeEach(function () {
beforeEach(() => {
undefinedErrorName.name = undefined;
blankErrorName.name = '';
});
it('tests error with message', function () {
assert.equal(cleanErrorStack(testError).toString(), 'Error: Test Message');
it('tests error with message', () => {
expect(cleanErrorStack(testError).toString()).toStrictEqual(
'Error: Test Message',
);
});
it('tests error with undefined name', function () {
assert.equal(
cleanErrorStack(undefinedErrorName).toString(),
it('tests error with undefined name', () => {
expect(cleanErrorStack(undefinedErrorName).toString()).toStrictEqual(
'Error: Test Message',
);
});
it('tests error with blank name', function () {
assert.equal(cleanErrorStack(blankErrorName).toString(), 'Test Message');
it('tests error with blank name', () => {
expect(cleanErrorStack(blankErrorName).toString()).toStrictEqual(
'Test Message',
);
});
it('tests error with blank message', function () {
assert.equal(cleanErrorStack(blankMsgError).toString(), 'Error');
it('tests error with blank message', () => {
expect(cleanErrorStack(blankMsgError).toString()).toStrictEqual('Error');
});
});

@ -1,7 +1,7 @@
import { ethErrors, serializeError } from 'eth-rpc-errors';
const createMetaRPCHandler = (api, outStream) => {
return (data) => {
return async (data) => {
if (outStream._writableState.ended) {
return;
}
@ -15,14 +15,26 @@ const createMetaRPCHandler = (api, outStream) => {
});
return;
}
api[data.method](...data.params, (err, result) => {
let result;
let error;
try {
result = await api[data.method](...data.params);
} catch (err) {
error = err;
}
if (outStream._writableState.ended) {
if (error) {
console.error(error);
}
return;
}
if (err) {
if (error) {
outStream.write({
jsonrpc: '2.0',
error: serializeError(err, { shouldIncludeStack: true }),
error: serializeError(error, { shouldIncludeStack: true }),
id: data.id,
});
} else {
@ -32,7 +44,6 @@ const createMetaRPCHandler = (api, outStream) => {
id: data.id,
});
}
});
};
};

@ -1,13 +1,11 @@
import { strict as assert } from 'assert';
import { obj as createThoughStream } from 'through2';
import createMetaRPCHandler from './createMetaRPCHandler';
describe('createMetaRPCHandler', function () {
it('can call the api when handler receives a JSON-RPC request', function (done) {
describe('createMetaRPCHandler', () => {
it('can call the api when handler receives a JSON-RPC request', () => {
const api = {
foo: (param1) => {
assert.strictEqual(param1, 'bar');
done();
expect(param1).toStrictEqual('bar');
},
};
const streamTest = createThoughStream();
@ -18,11 +16,31 @@ describe('createMetaRPCHandler', function () {
params: ['bar'],
});
});
it('can write the response to the outstream when api callback is called', function (done) {
it('can write the response to the outstream', () => {
const api = {
foo: (param1, cb) => {
assert.strictEqual(param1, 'bar');
cb(null, 'foobarbaz');
foo: (param1) => {
expect(param1).toStrictEqual('bar');
return 'foobarbaz';
},
};
const streamTest = createThoughStream();
const handler = createMetaRPCHandler(api, streamTest);
handler({
id: 1,
method: 'foo',
params: ['bar'],
});
streamTest.on('data', (data) => {
expect(data.result).toStrictEqual('foobarbaz');
streamTest.end();
});
});
it('can write an async response to the outstream', () => {
const api = {
foo: async (param1) => {
expect(param1).toStrictEqual('bar');
await new Promise((resolve) => setTimeout(() => resolve(), 100));
return 'foobarbaz';
},
};
const streamTest = createThoughStream();
@ -33,16 +51,15 @@ describe('createMetaRPCHandler', function () {
params: ['bar'],
});
streamTest.on('data', (data) => {
assert.strictEqual(data.result, 'foobarbaz');
expect(data.result).toStrictEqual('foobarbaz');
streamTest.end();
done();
});
});
it('can write the error to the outstream when api callback is called with an error', function (done) {
it('can write the error to the outstream when method throws an error', () => {
const api = {
foo: (param1, cb) => {
assert.strictEqual(param1, 'bar');
cb(new Error('foo-error'));
foo: (param1) => {
expect(param1).toStrictEqual('bar');
throw new Error('foo-error');
},
};
const streamTest = createThoughStream();
@ -53,45 +70,46 @@ describe('createMetaRPCHandler', function () {
params: ['bar'],
});
streamTest.on('data', (data) => {
assert.strictEqual(data.error.message, 'foo-error');
expect(data.error.message).toStrictEqual('foo-error');
streamTest.end();
done();
});
});
it('can not throw an error for writing an error after end', function (done) {
it('can not throw an error for writing an error after end', () => {
const api = {
foo: (param1, cb) => {
assert.strictEqual(param1, 'bar');
cb(new Error('foo-error'));
foo: (param1) => {
expect(param1).toStrictEqual('bar');
throw new Error('foo-error');
},
};
const streamTest = createThoughStream();
const handler = createMetaRPCHandler(api, streamTest);
streamTest.end();
expect(() => {
handler({
id: 1,
method: 'foo',
params: ['bar'],
});
done();
}).not.toThrow();
});
it('can not throw an error for write after end', function (done) {
it('can not throw an error for write after end', () => {
const api = {
foo: (param1, cb) => {
assert.strictEqual(param1, 'bar');
cb(undefined, {
foo: (param1) => {
expect(param1).toStrictEqual('bar');
return {
foo: 'bar',
});
};
},
};
const streamTest = createThoughStream();
const handler = createMetaRPCHandler(api, streamTest);
streamTest.end();
expect(() => {
handler({
id: 1,
method: 'foo',
params: ['bar'],
});
done();
}).not.toThrow();
});
});

@ -50,6 +50,7 @@ export default function setupEnsIpfsResolver({
async function attemptResolve({ tabId, name, pathname, search, fragment }) {
const ipfsGateway = getIpfsGateway();
extension.tabs.update(tabId, { url: `loading.html` });
let url = `https://app.ens.domains/name/${name}`;
try {

@ -1,27 +1,25 @@
import { strict as assert } from 'assert';
import sinon from 'sinon';
import { TRANSACTION_STATUSES } from '../../../shared/constants/transaction';
import MessageManager from './message-manager';
describe('Message Manager', function () {
describe('Message Manager', () => {
let messageManager;
beforeEach(function () {
beforeEach(() => {
messageManager = new MessageManager({
metricsEvent: sinon.fake(),
metricsEvent: jest.fn(),
});
});
describe('#getMsgList', function () {
it('when new should return empty array', function () {
describe('#getMsgList', () => {
it('when new should return empty array', () => {
const result = messageManager.messages;
assert.ok(Array.isArray(result));
assert.equal(result.length, 0);
expect(Array.isArray(result)).toStrictEqual(true);
expect(result).toHaveLength(0);
});
});
describe('#addMsg', function () {
it('adds a Msg returned in getMsgList', function () {
describe('#addMsg', () => {
it('adds a Msg returned in getMsgList', () => {
const Msg = {
id: 1,
status: TRANSACTION_STATUSES.APPROVED,
@ -29,14 +27,14 @@ describe('Message Manager', function () {
};
messageManager.addMsg(Msg);
const result = messageManager.messages;
assert.ok(Array.isArray(result));
assert.equal(result.length, 1);
assert.equal(result[0].id, 1);
expect(Array.isArray(result)).toStrictEqual(true);
expect(result).toHaveLength(1);
expect(result[0].id).toStrictEqual(1);
});
});
describe('#setMsgStatusApproved', function () {
it('sets the Msg status to approved', function () {
describe('#setMsgStatusApproved', () => {
it('sets the Msg status to approved', () => {
const Msg = {
id: 1,
status: 'unapproved',
@ -45,14 +43,14 @@ describe('Message Manager', function () {
messageManager.addMsg(Msg);
messageManager.setMsgStatusApproved(1);
const result = messageManager.messages;
assert.ok(Array.isArray(result));
assert.equal(result.length, 1);
assert.equal(result[0].status, TRANSACTION_STATUSES.APPROVED);
expect(Array.isArray(result)).toStrictEqual(true);
expect(result).toHaveLength(1);
expect(result[0].status).toStrictEqual(TRANSACTION_STATUSES.APPROVED);
});
});
describe('#rejectMsg', function () {
it('sets the Msg status to rejected', function () {
describe('#rejectMsg', () => {
it('sets the Msg status to rejected', () => {
const Msg = {
id: 1,
status: 'unapproved',
@ -61,14 +59,14 @@ describe('Message Manager', function () {
messageManager.addMsg(Msg);
messageManager.rejectMsg(1);
const result = messageManager.messages;
assert.ok(Array.isArray(result));
assert.equal(result.length, 1);
assert.equal(result[0].status, TRANSACTION_STATUSES.REJECTED);
expect(Array.isArray(result)).toStrictEqual(true);
expect(result).toHaveLength(1);
expect(result[0].status).toStrictEqual(TRANSACTION_STATUSES.REJECTED);
});
});
describe('#_updateMsg', function () {
it('replaces the Msg with the same id', function () {
describe('#_updateMsg', () => {
it('replaces the Msg with the same id', () => {
messageManager.addMsg({
id: '1',
status: 'unapproved',
@ -86,12 +84,12 @@ describe('Message Manager', function () {
metamaskNetworkId: 'unit test',
});
const result = messageManager.getMsg('1');
assert.equal(result.hash, 'foo');
expect(result.hash).toStrictEqual('foo');
});
});
describe('#getUnapprovedMsgs', function () {
it('returns unapproved Msgs in a hash', function () {
describe('#getUnapprovedMsgs', () => {
it('returns unapproved Msgs in a hash', () => {
messageManager.addMsg({
id: '1',
status: 'unapproved',
@ -103,14 +101,14 @@ describe('Message Manager', function () {
metamaskNetworkId: 'unit test',
});
const result = messageManager.getUnapprovedMsgs();
assert.equal(typeof result, 'object');
assert.equal(result['1'].status, 'unapproved');
assert.equal(result['2'], undefined);
expect(typeof result).toStrictEqual('object');
expect(result['1'].status).toStrictEqual('unapproved');
expect(result['2']).toBeUndefined();
});
});
describe('#getMsg', function () {
it('returns a Msg with the requested id', function () {
describe('#getMsg', () => {
it('returns a Msg with the requested id', () => {
messageManager.addMsg({
id: '1',
status: 'unapproved',
@ -121,9 +119,8 @@ describe('Message Manager', function () {
status: TRANSACTION_STATUSES.APPROVED,
metamaskNetworkId: 'unit test',
});
assert.equal(messageManager.getMsg('1').status, 'unapproved');
assert.equal(
messageManager.getMsg('2').status,
expect(messageManager.getMsg('1').status).toStrictEqual('unapproved');
expect(messageManager.getMsg('2').status).toStrictEqual(
TRANSACTION_STATUSES.APPROVED,
);
});

@ -1,24 +1,21 @@
import { strict as assert } from 'assert';
import { obj as createThoughStream } from 'through2';
import metaRPCClientFactory from './metaRPCClientFactory';
describe('metaRPCClientFactory', function () {
it('should be able to make an rpc request with the method', function (done) {
describe('metaRPCClientFactory', () => {
it('should be able to make an rpc request with the method', () => {
const streamTest = createThoughStream((chunk) => {
assert.strictEqual(chunk.method, 'foo');
done();
expect(chunk.method).toStrictEqual('foo');
});
const metaRPCClient = metaRPCClientFactory(streamTest);
metaRPCClient.foo();
});
it('should be able to make an rpc request/response with the method and params and node-style callback', function (done) {
it('should be able to make an rpc request/response with the method and params and node-style callback', () => {
const streamTest = createThoughStream();
const metaRPCClient = metaRPCClientFactory(streamTest);
// make a "foo" method call
metaRPCClient.foo('bar', (_, result) => {
assert.strictEqual(result, 'foobarbaz');
done();
expect(result).toStrictEqual('foobarbaz');
});
// fake a response
@ -30,15 +27,14 @@ describe('metaRPCClientFactory', function () {
});
});
});
it('should be able to make an rpc request/error with the method and params and node-style callback', function (done) {
it('should be able to make an rpc request/error with the method and params and node-style callback', () => {
const streamTest = createThoughStream();
const metaRPCClient = metaRPCClientFactory(streamTest);
// make a "foo" method call
metaRPCClient.foo('bar', (err) => {
assert.strictEqual(err.message, 'foo-message');
assert.strictEqual(err.code, 1);
done();
expect(err.message).toStrictEqual('foo-message');
expect(err.code).toStrictEqual(1);
});
metaRPCClient.requests.forEach((_, key) => {
@ -53,17 +49,16 @@ describe('metaRPCClientFactory', function () {
});
});
it('should be able to make an rpc request/response with the method and params and node-style callback with multiple instances of metaRPCClientFactory and the same connectionStream', function (done) {
it('should be able to make an rpc request/response with the method and params and node-style callback with multiple instances of metaRPCClientFactory and the same connectionStream', () => {
const streamTest = createThoughStream();
const metaRPCClient = metaRPCClientFactory(streamTest);
const metaRPCClient2 = metaRPCClientFactory(streamTest);
// make a "foo" method call, followed by "baz" call on metaRPCClient2
metaRPCClient.foo('bar', (_, result) => {
assert.strictEqual(result, 'foobarbaz');
expect(result).toStrictEqual('foobarbaz');
metaRPCClient2.baz('bar', (err) => {
assert.strictEqual(err, null);
done();
expect(err).toBeNull();
});
});
@ -86,13 +81,12 @@ describe('metaRPCClientFactory', function () {
});
});
it('should be able to handle notifications', function (done) {
it('should be able to handle notifications', () => {
const streamTest = createThoughStream();
const metaRPCClient = metaRPCClientFactory(streamTest);
metaRPCClient.onNotification((notification) => {
assert(notification.method, 'foobarbaz');
done();
expect(notification.method).toStrictEqual('foobarbaz');
});
// send a notification
@ -103,13 +97,12 @@ describe('metaRPCClientFactory', function () {
});
});
it('should be able to handle errors with no id', function (done) {
it('should be able to handle errors with no id', () => {
const streamTest = createThoughStream();
const metaRPCClient = metaRPCClientFactory(streamTest);
metaRPCClient.onUncaughtError((error) => {
assert(error.code, 1);
done();
expect(error.code).toStrictEqual(1);
});
streamTest.write({
@ -121,13 +114,12 @@ describe('metaRPCClientFactory', function () {
});
});
it('should be able to handle errors with null id', function (done) {
it('should be able to handle errors with null id', () => {
const streamTest = createThoughStream();
const metaRPCClient = metaRPCClientFactory(streamTest);
metaRPCClient.onUncaughtError((error) => {
assert(error.code, 1);
done();
expect(error.code).toStrictEqual(1);
});
streamTest.write({

@ -1,5 +1,5 @@
/* eslint-disable jest/no-conditional-expect */
import fs from 'fs';
import { strict as assert } from 'assert';
import { cloneDeep } from 'lodash';
import liveMigrations from '../../migrations';
import data from '../../first-time-state';
@ -39,11 +39,11 @@ const firstTimeState = {
data,
};
describe('migrations', function () {
describe('liveMigrations require list', function () {
describe('migrations', () => {
describe('liveMigrations require list', () => {
let migrationNumbers;
before(function () {
beforeAll(() => {
const fileNames = fs.readdirSync('./app/scripts/migrations/');
migrationNumbers = fileNames
.reduce((acc, filename) => {
@ -56,21 +56,19 @@ describe('migrations', function () {
.map((num) => parseInt(num, 10));
});
it('should include all migrations', function () {
it('should include all migrations', () => {
migrationNumbers.forEach((num) => {
const migration = liveMigrations.find((m) => m.version === num);
assert(
migration,
`migration not included in 'migrations/index.js': ${num}`,
);
expect(migration.version).toStrictEqual(num);
});
});
it('should have tests for all migrations', function () {
it('should have tests for all migrations', () => {
const fileNames = fs.readdirSync('./app/scripts/migrations/');
const testNumbers = fileNames
.reduce((acc, filename) => {
const name = filename.split('.test.')[0];
// eslint-disable-next-line jest/no-if
if (/^\d+$/u.test(name)) {
acc.push(name);
}
@ -80,30 +78,31 @@ describe('migrations', function () {
migrationNumbers.forEach((num) => {
if (num >= 33) {
assert.ok(
testNumbers.includes(num),
`no test found for migration: ${num}`,
);
expect(testNumbers).toContain(num);
}
});
});
});
describe('Migrator', function () {
it('migratedData version should be version 3', async function () {
describe('Migrator', () => {
it('migratedData version should be version 3', async () => {
const migrator = new Migrator({ migrations: stubMigrations });
const migratedData = await migrator.migrateData(versionedData);
assert.equal(migratedData.meta.version, stubMigrations[2].version);
expect(migratedData.meta.version).toStrictEqual(
stubMigrations[2].version,
);
});
it('should match the last version in live migrations', async function () {
it('should match the last version in live migrations', async () => {
const migrator = new Migrator({ migrations: liveMigrations });
const migratedData = await migrator.migrateData(firstTimeState);
const last = liveMigrations.length - 1;
assert.equal(migratedData.meta.version, liveMigrations[last].version);
expect(migratedData.meta.version).toStrictEqual(
liveMigrations[last].version,
);
});
it('should emit an error', async function () {
it('should emit an error', async () => {
const migrator = new Migrator({
migrations: [
{
@ -114,7 +113,9 @@ describe('migrations', function () {
},
],
});
await assert.rejects(migrator.migrateData({ meta: { version: 0 } }));
await expect(async () => {
await migrator.migrateData({ meta: { version: 0 } });
}).rejects.toThrow('Error: MetaMask Migration Error #1: test');
});
});
});

@ -1,38 +0,0 @@
import promiseToCallback from 'promise-to-callback';
const callbackNoop = function (err) {
if (err) {
throw err;
}
};
/**
* A generator that returns a function which, when passed a promise, can treat that promise as a node style callback.
* The prime advantage being that callbacks are better for error handling.
*
* @param {Function} fn - The function to handle as a callback
* @param {Object} context - The context in which the fn is to be called, most often a this reference
*
*/
export default function nodeify(fn, context) {
return function (...args) {
const lastArg = args[args.length - 1];
const lastArgIsCallback = typeof lastArg === 'function';
let callback;
if (lastArgIsCallback) {
callback = lastArg;
args.pop();
} else {
callback = callbackNoop;
}
// call the provided function and ensure result is a promise
let result;
try {
result = Promise.resolve(fn.apply(context, args));
} catch (err) {
result = Promise.reject(err);
}
// wire up promise resolution to callback
promiseToCallback(result)(callback);
};
}

@ -1,74 +0,0 @@
import { strict as assert } from 'assert';
import nodeify from './nodeify';
describe('nodeify', function () {
const obj = {
foo: 'bar',
promiseFunc(a) {
const solution = this.foo + a;
return Promise.resolve(solution);
},
};
it('should retain original context', function (done) {
const nodified = nodeify(obj.promiseFunc, obj);
nodified('baz', (err, res) => {
if (!err) {
assert.equal(res, 'barbaz');
done();
return;
}
done(new Error(err.toString()));
});
});
it('no callback - should allow the last argument to not be a function', function (done) {
const nodified = nodeify(obj.promiseFunc, obj);
try {
nodified('baz');
done();
} catch (err) {
done(
new Error(
'should not have thrown if the last argument is not a function',
),
);
}
});
it('sync functions - returns value', function (done) {
const nodified = nodeify(() => 42);
try {
nodified((err, result) => {
if (err) {
done(new Error(`should not have thrown any error: ${err.message}`));
return;
}
assert.equal(42, result, 'got expected result');
});
done();
} catch (err) {
done(new Error(`should not have thrown any error: ${err.message}`));
}
});
it('sync functions - handles errors', function (done) {
const nodified = nodeify(() => {
throw new Error('boom!');
});
try {
nodified((err, result) => {
if (result) {
done(new Error('should not have returned any result'));
return;
}
assert.ok(err, 'got expected error');
assert.ok(err.message.includes('boom!'), 'got expected error message');
});
done();
} catch (err) {
done(new Error(`should not have thrown any error: ${err.message}`));
}
});
});

@ -1,25 +1,25 @@
import { strict as assert } from 'assert';
import sinon from 'sinon';
import { TRANSACTION_STATUSES } from '../../../shared/constants/transaction';
import PersonalMessageManager from './personal-message-manager';
describe('Personal Message Manager', function () {
describe('Personal Message Manager', () => {
let messageManager;
beforeEach(function () {
messageManager = new PersonalMessageManager({ metricsEvent: sinon.fake() });
beforeEach(() => {
messageManager = new PersonalMessageManager({
metricsEvent: jest.fn(),
});
});
describe('#getMsgList', function () {
it('when new should return empty array', function () {
describe('#getMsgList', () => {
it('when new should return empty array', () => {
const result = messageManager.messages;
assert.ok(Array.isArray(result));
assert.equal(result.length, 0);
expect(Array.isArray(result)).toStrictEqual(true);
expect(result).toHaveLength(0);
});
});
describe('#addMsg', function () {
it('adds a Msg returned in getMsgList', function () {
describe('#addMsg', () => {
it('adds a Msg returned in getMsgList', () => {
const Msg = {
id: 1,
status: TRANSACTION_STATUSES.APPROVED,
@ -27,14 +27,14 @@ describe('Personal Message Manager', function () {
};
messageManager.addMsg(Msg);
const result = messageManager.messages;
assert.ok(Array.isArray(result));
assert.equal(result.length, 1);
assert.equal(result[0].id, 1);
expect(Array.isArray(result)).toStrictEqual(true);
expect(result).toHaveLength(1);
expect(result[0].id).toStrictEqual(1);
});
});
describe('#setMsgStatusApproved', function () {
it('sets the Msg status to approved', function () {
describe('#setMsgStatusApproved', () => {
it('sets the Msg status to approved', () => {
const Msg = {
id: 1,
status: TRANSACTION_STATUSES.UNAPPROVED,
@ -43,14 +43,14 @@ describe('Personal Message Manager', function () {
messageManager.addMsg(Msg);
messageManager.setMsgStatusApproved(1);
const result = messageManager.messages;
assert.ok(Array.isArray(result));
assert.equal(result.length, 1);
assert.equal(result[0].status, TRANSACTION_STATUSES.APPROVED);
expect(Array.isArray(result)).toStrictEqual(true);
expect(result).toHaveLength(1);
expect(result[0].status).toStrictEqual(TRANSACTION_STATUSES.APPROVED);
});
});
describe('#rejectMsg', function () {
it('sets the Msg status to rejected', function () {
describe('#rejectMsg', () => {
it('sets the Msg status to rejected', () => {
const Msg = {
id: 1,
status: TRANSACTION_STATUSES.UNAPPROVED,
@ -59,14 +59,14 @@ describe('Personal Message Manager', function () {
messageManager.addMsg(Msg);
messageManager.rejectMsg(1);
const result = messageManager.messages;
assert.ok(Array.isArray(result));
assert.equal(result.length, 1);
assert.equal(result[0].status, TRANSACTION_STATUSES.REJECTED);
expect(Array.isArray(result)).toStrictEqual(true);
expect(result).toHaveLength(1);
expect(result[0].status).toStrictEqual(TRANSACTION_STATUSES.REJECTED);
});
});
describe('#_updateMsg', function () {
it('replaces the Msg with the same id', function () {
describe('#_updateMsg', () => {
it('replaces the Msg with the same id', () => {
messageManager.addMsg({
id: '1',
status: TRANSACTION_STATUSES.UNAPPROVED,
@ -84,12 +84,12 @@ describe('Personal Message Manager', function () {
metamaskNetworkId: 'unit test',
});
const result = messageManager.getMsg('1');
assert.equal(result.hash, 'foo');
expect(result.hash).toStrictEqual('foo');
});
});
describe('#getUnapprovedMsgs', function () {
it('returns unapproved Msgs in a hash', function () {
describe('#getUnapprovedMsgs', () => {
it('returns unapproved Msgs in a hash', () => {
messageManager.addMsg({
id: '1',
status: TRANSACTION_STATUSES.UNAPPROVED,
@ -101,14 +101,14 @@ describe('Personal Message Manager', function () {
metamaskNetworkId: 'unit test',
});
const result = messageManager.getUnapprovedMsgs();
assert.equal(typeof result, 'object');
assert.equal(result['1'].status, TRANSACTION_STATUSES.UNAPPROVED);
assert.equal(result['2'], undefined);
expect(typeof result).toStrictEqual('object');
expect(result['1'].status).toStrictEqual(TRANSACTION_STATUSES.UNAPPROVED);
expect(result['2']).toBeUndefined();
});
});
describe('#getMsg', function () {
it('returns a Msg with the requested id', function () {
describe('#getMsg', () => {
it('returns a Msg with the requested id', () => {
messageManager.addMsg({
id: '1',
status: TRANSACTION_STATUSES.UNAPPROVED,
@ -119,34 +119,32 @@ describe('Personal Message Manager', function () {
status: TRANSACTION_STATUSES.APPROVED,
metamaskNetworkId: 'unit test',
});
assert.equal(
messageManager.getMsg('1').status,
expect(messageManager.getMsg('1').status).toStrictEqual(
TRANSACTION_STATUSES.UNAPPROVED,
);
assert.equal(
messageManager.getMsg('2').status,
expect(messageManager.getMsg('2').status).toStrictEqual(
TRANSACTION_STATUSES.APPROVED,
);
});
});
describe('#normalizeMsgData', function () {
it('converts text to a utf8 hex string', function () {
describe('#normalizeMsgData', () => {
it('converts text to a utf8 hex string', () => {
const input = 'hello';
const output = messageManager.normalizeMsgData(input);
assert.equal(output, '0x68656c6c6f', 'predictably hex encoded');
expect(output).toStrictEqual('0x68656c6c6f');
});
it('tolerates a hex prefix', function () {
it('tolerates a hex prefix', () => {
const input = '0x12';
const output = messageManager.normalizeMsgData(input);
assert.equal(output, '0x12', 'un modified');
expect(output).toStrictEqual('0x12');
});
it('tolerates normal hex', function () {
it('tolerates normal hex', () => {
const input = '12';
const output = messageManager.normalizeMsgData(input);
assert.equal(output, '0x12', 'adds prefix');
expect(output).toStrictEqual('0x12');
});
});
});

@ -1,8 +1,12 @@
import { permissionRpcMethods } from '@metamask/snap-controllers';
import { selectHooks } from '@metamask/rpc-methods';
import { ethErrors } from 'eth-rpc-errors';
import { UNSUPPORTED_RPC_METHODS } from '../../../../shared/constants/network';
import handlers from './handlers';
import localHandlers from './handlers';
const handlerMap = handlers.reduce((map, handler) => {
const allHandlers = [...localHandlers, ...permissionRpcMethods.handlers];
const handlerMap = allHandlers.reduce((map, handler) => {
for (const methodName of handler.methodNames) {
map.set(methodName, handler);
}
@ -10,23 +14,17 @@ const handlerMap = handlers.reduce((map, handler) => {
}, new Map());
/**
* Returns a middleware that implements the RPC methods defined in the handlers
* directory.
*
* The purpose of this middleware is to create portable RPC method
* implementations that are decoupled from the rest of our background
* architecture.
* Creates a json-rpc-engine middleware of RPC method implementations.
*
* Handlers consume functions that hook into the background, and only depend
* on their signatures, not e.g. controller internals.
*
* Eventually, we'll want to extract this middleware into its own package.
*
* @param {Object} opts - The middleware options
* @param {Record<string, unknown>} hooks - Required "hooks" into our
* controllers.
* @returns {(req: Object, res: Object, next: Function, end: Function) => void}
*/
export default function createMethodMiddleware(opts) {
return function methodMiddleware(req, res, next, end) {
export default function createMethodMiddleware(hooks) {
return async function methodMiddleware(req, res, next, end) {
// Reject unsupported methods.
if (UNSUPPORTED_RPC_METHODS.has(req.method)) {
return end(ethErrors.rpc.methodNotSupported());
@ -35,29 +33,20 @@ export default function createMethodMiddleware(opts) {
const handler = handlerMap.get(req.method);
if (handler) {
const { implementation, hookNames } = handler;
return implementation(req, res, next, end, selectHooks(opts, hookNames));
try {
// Implementations may or may not be async, so we must await them.
return await implementation(
req,
res,
next,
end,
selectHooks(hooks, hookNames),
);
} catch (error) {
return end(error);
}
}
return next();
};
}
/**
* Returns the subset of the specified `hooks` that are included in the
* `hookNames` object. This is a Principle of Least Authority (POLA) measure
* to ensure that each RPC method implementation only has access to the
* API "hooks" it needs to do its job.
*
* @param {Record<string, unknown>} hooks - The hooks to select from.
* @param {Record<string, true>} hookNames - The names of the hooks to select.
* @returns {Record<string, unknown> | undefined} The selected hooks.
*/
function selectHooks(hooks, hookNames) {
if (hookNames) {
return Object.keys(hookNames).reduce((hookSubset, hookName) => {
hookSubset[hookName] = hooks[hookName];
return hookSubset;
}, {});
}
return undefined;
}

@ -0,0 +1,33 @@
import { MESSAGE_TYPE } from '../../../../../shared/constants/app';
/**
* A wrapper for `eth_accounts` that returns an empty array when permission is denied.
*/
const requestEthereumAccounts = {
methodNames: [MESSAGE_TYPE.ETH_ACCOUNTS],
implementation: ethAccountsHandler,
hookNames: {
getAccounts: true,
},
};
export default requestEthereumAccounts;
/**
* @typedef {Record<string, Function>} EthAccountsOptions
* @property {Function} getAccounts - Gets the accounts for the requesting
* origin.
*/
/**
*
* @param {import('json-rpc-engine').JsonRpcRequest<unknown>} req - The JSON-RPC request object.
* @param {import('json-rpc-engine').JsonRpcResponse<true>} res - The JSON-RPC response object.
* @param {Function} _next - The json-rpc-engine 'next' callback.
* @param {Function} end - The json-rpc-engine 'end' callback.
* @param {EthAccountsOptions} options - The RPC method hooks.
*/
async function ethAccountsHandler(_req, res, _next, end, { getAccounts }) {
res.result = await getAccounts();
return end();
}

@ -1,14 +1,20 @@
import addEthereumChain from './add-ethereum-chain';
import switchEthereumChain from './switch-ethereum-chain';
import ethAccounts from './eth-accounts';
import getProviderState from './get-provider-state';
import logWeb3ShimUsage from './log-web3-shim-usage';
import requestAccounts from './request-accounts';
import sendMetadata from './send-metadata';
import switchEthereumChain from './switch-ethereum-chain';
import watchAsset from './watch-asset';
const handlers = [
addEthereumChain,
switchEthereumChain,
ethAccounts,
getProviderState,
logWeb3ShimUsage,
requestAccounts,
sendMetadata,
switchEthereumChain,
watchAsset,
];
export default handlers;

@ -0,0 +1,108 @@
import { ethErrors } from 'eth-rpc-errors';
import { MESSAGE_TYPE } from '../../../../../shared/constants/app';
/**
* This method attempts to retrieve the Ethereum accounts available to the
* requester, or initiate a request for account access if none are currently
* available. It is essentially a wrapper of wallet_requestPermissions that
* only errors if the user rejects the request. We maintain the method for
* backwards compatibility reasons.
*/
const requestEthereumAccounts = {
methodNames: [MESSAGE_TYPE.ETH_REQUEST_ACCOUNTS],
implementation: requestEthereumAccountsHandler,
hookNames: {
origin: true,
getAccounts: true,
getUnlockPromise: true,
hasPermission: true,
requestAccountsPermission: true,
},
};
export default requestEthereumAccounts;
// Used to rate-limit pending requests to one per origin
const locks = new Set();
/**
* @typedef {Record<string, string | Function>} RequestEthereumAccountsOptions
* @property {string} origin - The requesting origin.
* @property {Function} getAccounts - Gets the accounts for the requesting
* origin.
* @property {Function} getUnlockPromise - Gets a promise that resolves when
* the extension unlocks.
* @property {Function} hasPermission - Returns whether the requesting origin
* has the specified permission.
* @property {Function} requestAccountsPermission - Requests the `eth_accounts`
* permission for the requesting origin.
*/
/**
*
* @param {import('json-rpc-engine').JsonRpcRequest<unknown>} _req - The JSON-RPC request object.
* @param {import('json-rpc-engine').JsonRpcResponse<true>} res - The JSON-RPC response object.
* @param {Function} _next - The json-rpc-engine 'next' callback.
* @param {Function} end - The json-rpc-engine 'end' callback.
* @param {RequestEthereumAccountsOptions} options - The RPC method hooks.
*/
async function requestEthereumAccountsHandler(
_req,
res,
_next,
end,
{
origin,
getAccounts,
getUnlockPromise,
hasPermission,
requestAccountsPermission,
},
) {
if (locks.has(origin)) {
res.error = ethErrors.rpc.resourceUnavailable(
`Already processing ${MESSAGE_TYPE.ETH_REQUEST_ACCOUNTS}. Please wait.`,
);
return end();
}
if (hasPermission(MESSAGE_TYPE.ETH_ACCOUNTS)) {
// We wait for the extension to unlock in this case only, because permission
// requests are handled when the extension is unlocked, regardless of the
// lock state when they were received.
try {
locks.add(origin);
await getUnlockPromise();
res.result = await getAccounts();
end();
} catch (error) {
end(error);
} finally {
locks.delete(origin);
}
return undefined;
}
// If no accounts, request the accounts permission
try {
await requestAccountsPermission();
} catch (err) {
res.error = err;
return end();
}
// Get the approved accounts
const accounts = await getAccounts();
/* istanbul ignore else: too hard to induce, see below comment */
if (accounts.length > 0) {
res.result = accounts;
} else {
// This should never happen, because it should be caught in the
// above catch clause
res.error = ethErrors.rpc.internal(
'Accounts unexpectedly unavailable. Please report this bug.',
);
}
return end();
}

@ -0,0 +1,58 @@
import { ethErrors } from 'eth-rpc-errors';
import { MESSAGE_TYPE } from '../../../../../shared/constants/app';
/**
* This internal method is used by our external provider to send metadata about
* permission subjects so that we can e.g. display a proper name and icon in
* our UI.
*/
const sendMetadata = {
methodNames: [MESSAGE_TYPE.SEND_METADATA],
implementation: sendMetadataHandler,
hookNames: {
addSubjectMetadata: true,
subjectType: true,
},
};
export default sendMetadata;
/**
* @typedef {Record<string, Function>} SendMetadataOptions
* @property {Function} addSubjectMetadata - A function that records subject
* metadata, bound to the requesting origin.
* @property {string} subjectType - The type of the requesting origin / subject.
*/
/**
* @param {import('json-rpc-engine').JsonRpcRequest<unknown>} req - The JSON-RPC request object.
* @param {import('json-rpc-engine').JsonRpcResponse<true>} res - The JSON-RPC response object.
* @param {Function} _next - The json-rpc-engine 'next' callback.
* @param {Function} end - The json-rpc-engine 'end' callback.
* @param {SendMetadataOptions} options
*/
function sendMetadataHandler(
req,
res,
_next,
end,
{ addSubjectMetadata, subjectType },
) {
const { origin, params } = req;
if (params && typeof params === 'object' && !Array.isArray(params)) {
const { icon = null, name = null, ...remainingParams } = params;
addSubjectMetadata({
...remainingParams,
iconUrl: icon,
name,
subjectType,
origin,
});
} else {
return end(ethErrors.rpc.invalidParams({ data: params }));
}
res.result = true;
return end();
}

@ -1,127 +1,118 @@
import { strict as assert } from 'assert';
/**
* @jest-environment node
* https://github.com/facebook/jest/issues/7780
*/
import { cloneDeep } from 'lodash';
import KeyringController from 'eth-keyring-controller';
import firstTimeState from '../first-time-state';
import mockEncryptor from '../../../test/lib/mock-encryptor';
import seedPhraseVerifier from './seed-phrase-verifier';
describe('SeedPhraseVerifier', function () {
describe('verifyAccounts', function () {
describe('SeedPhraseVerifier', () => {
describe('verifyAccounts', () => {
const password = 'passw0rd1';
const hdKeyTree = 'HD Key Tree';
let keyringController;
let primaryKeyring;
beforeEach(async function () {
beforeEach(async () => {
keyringController = new KeyringController({
initState: cloneDeep(firstTimeState),
encryptor: mockEncryptor,
});
assert(keyringController);
expect.any(keyringController);
await keyringController.createNewVaultAndKeychain(password);
primaryKeyring = keyringController.getKeyringsByType(hdKeyTree)[0];
});
it('should be able to verify created account with seed words', async function () {
it('should be able to verify created account with seed words', async () => {
const createdAccounts = await primaryKeyring.getAccounts();
assert.equal(createdAccounts.length, 1);
expect(createdAccounts).toHaveLength(1);
const serialized = await primaryKeyring.serialize();
const seedWords = serialized.mnemonic;
assert.notEqual(seedWords.length, 0);
expect(seedWords).not.toHaveLength(0);
await seedPhraseVerifier.verifyAccounts(createdAccounts, seedWords);
});
it('should be able to verify created account (upper case) with seed words', async function () {
it('should be able to verify created account (upper case) with seed words', async () => {
const createdAccounts = await primaryKeyring.getAccounts();
assert.equal(createdAccounts.length, 1);
expect(createdAccounts).toHaveLength(1);
const upperCaseAccounts = [createdAccounts[0].toUpperCase()];
const serialized = await primaryKeyring.serialize();
const seedWords = serialized.mnemonic;
assert.notEqual(seedWords.length, 0);
expect(seedWords).not.toHaveLength(0);
await seedPhraseVerifier.verifyAccounts(upperCaseAccounts, seedWords);
});
it('should be able to verify created account (lower case) with seed words', async function () {
it('should be able to verify created account (lower case) with seed words', async () => {
const createdAccounts = await primaryKeyring.getAccounts();
assert.equal(createdAccounts.length, 1);
expect(createdAccounts).toHaveLength(1);
const lowerCaseAccounts = [createdAccounts[0].toLowerCase()];
const serialized = await primaryKeyring.serialize();
const seedWords = serialized.mnemonic;
assert.notEqual(seedWords.length, 0);
expect(seedWords).not.toHaveLength(0);
await seedPhraseVerifier.verifyAccounts(lowerCaseAccounts, seedWords);
});
it('should return error with good but different seed words', async function () {
it('should return error with good but different seed words', async () => {
const createdAccounts = await primaryKeyring.getAccounts();
assert.equal(createdAccounts.length, 1);
expect(createdAccounts).toHaveLength(1);
await primaryKeyring.serialize();
const seedWords =
'debris dizzy just program just float decrease vacant alarm reduce speak stadium';
try {
await expect(async () => {
await seedPhraseVerifier.verifyAccounts(createdAccounts, seedWords);
assert.fail('Should reject');
} catch (err) {
assert.ok(
err.message.indexOf('Not identical accounts!') >= 0,
'Wrong error message',
);
}
}).rejects.toThrow('Not identical accounts!');
});
it('should return error with undefined existing accounts', async function () {
it('should return error with undefined existing accounts', async () => {
const createdAccounts = await primaryKeyring.getAccounts();
assert.equal(createdAccounts.length, 1);
expect(createdAccounts).toHaveLength(1);
await primaryKeyring.serialize();
const seedWords =
'debris dizzy just program just float decrease vacant alarm reduce speak stadium';
try {
await expect(async () => {
await seedPhraseVerifier.verifyAccounts(undefined, seedWords);
assert.fail('Should reject');
} catch (err) {
assert.equal(err.message, 'No created accounts defined.');
}
}).rejects.toThrow('No created accounts defined.');
});
it('should return error with empty accounts array', async function () {
it('should return error with empty accounts array', async () => {
const createdAccounts = await primaryKeyring.getAccounts();
assert.equal(createdAccounts.length, 1);
expect(createdAccounts).toHaveLength(1);
await primaryKeyring.serialize();
const seedWords =
'debris dizzy just program just float decrease vacant alarm reduce speak stadium';
try {
await expect(async () => {
await seedPhraseVerifier.verifyAccounts([], seedWords);
assert.fail('Should reject');
} catch (err) {
assert.equal(err.message, 'No created accounts defined.');
}
}).rejects.toThrow('No created accounts defined.');
});
it('should be able to verify more than one created account with seed words', async function () {
it('should be able to verify more than one created account with seed words', async () => {
await keyringController.addNewAccount(primaryKeyring);
await keyringController.addNewAccount(primaryKeyring);
const createdAccounts = await primaryKeyring.getAccounts();
assert.equal(createdAccounts.length, 3);
expect(createdAccounts).toHaveLength(3);
const serialized = await primaryKeyring.serialize();
const seedWords = serialized.mnemonic;
assert.notEqual(seedWords.length, 0);
expect(seedWords).not.toHaveLength(0);
await seedPhraseVerifier.verifyAccounts(createdAccounts, seedWords);
});

@ -33,10 +33,7 @@ const SEGMENT_FLUSH_INTERVAL = SECOND * 5;
* @param {number} flushInterval - ms interval to flush queue and send to segment
* @returns {SegmentInterface}
*/
export const createSegmentMock = (
flushAt = SEGMENT_FLUSH_AT,
flushInterval = SEGMENT_FLUSH_INTERVAL,
) => {
export const createSegmentMock = (flushAt = SEGMENT_FLUSH_AT) => {
const segmentMock = {
// Internal queue to keep track of events and properly mimic segment's
// queueing behavior.
@ -77,8 +74,7 @@ export const createSegmentMock = (
// noop
},
};
// Mimic the flushInterval behavior with an interval
setInterval(segmentMock.flush, flushInterval);
return segmentMock;
};

@ -1,9 +1,8 @@
import { strict as assert } from 'assert';
import sinon from 'sinon';
import { TRANSACTION_STATUSES } from '../../../shared/constants/transaction';
import TypedMessageManager from './typed-message-manager';
describe('Typed Message Manager', function () {
describe('Typed Message Manager', () => {
let typedMessageManager,
msgParamsV1,
msgParamsV3,
@ -14,7 +13,7 @@ describe('Typed Message Manager', function () {
const address = '0xc42edfcc21ed14dda456aa0756c153f7985d8813';
beforeEach(async function () {
beforeEach(async () => {
typedMessageManager = new TypedMessageManager({
getCurrentChainId: sinon.fake.returns('0x1'),
metricsEvent: sinon.fake(),
@ -81,47 +80,48 @@ describe('Typed Message Manager', function () {
numberMsgId = parseInt(msgId, 10);
});
it('supports version 1 of signedTypedData', function () {
it('supports version 1 of signedTypedData', () => {
typedMessageManager.addUnapprovedMessage(msgParamsV1, null, 'V1');
assert.equal(
messages[messages.length - 1].msgParams.data,
expect(messages[messages.length - 1].msgParams.data).toStrictEqual(
msgParamsV1.data,
);
});
it('has params address', function () {
assert.equal(typedMsgs[msgId].msgParams.from, address);
it('has params address', () => {
expect(typedMsgs[msgId].msgParams.from).toStrictEqual(address);
});
it('adds to unapproved messages and sets status to unapproved', function () {
assert.equal(typedMsgs[msgId].status, TRANSACTION_STATUSES.UNAPPROVED);
it('adds to unapproved messages and sets status to unapproved', () => {
expect(typedMsgs[msgId].status).toStrictEqual(
TRANSACTION_STATUSES.UNAPPROVED,
);
});
it('validates params', function () {
assert.doesNotThrow(() => {
it('validates params', async () => {
await expect(() => {
typedMessageManager.validateParams(messages[0].msgParams);
}, 'Does not throw with valid parameters');
}).not.toThrow();
});
it('gets unapproved by id', function () {
it('gets unapproved by id', () => {
const getMsg = typedMessageManager.getMsg(numberMsgId);
assert.equal(getMsg.id, numberMsgId);
expect(getMsg.id).toStrictEqual(numberMsgId);
});
it('approves messages', async function () {
it('approves messages', async () => {
const messageMetaMaskId = messages[0].msgParams;
typedMessageManager.approveMessage(messageMetaMaskId);
assert.equal(messages[0].status, TRANSACTION_STATUSES.APPROVED);
expect(messages[0].status).toStrictEqual(TRANSACTION_STATUSES.APPROVED);
});
it('sets msg status to signed and adds a raw sig to message details', function () {
it('sets msg status to signed and adds a raw sig to message details', () => {
typedMessageManager.setMsgStatusSigned(numberMsgId, 'raw sig');
assert.equal(messages[0].status, TRANSACTION_STATUSES.SIGNED);
assert.equal(messages[0].rawSig, 'raw sig');
expect(messages[0].status).toStrictEqual(TRANSACTION_STATUSES.SIGNED);
expect(messages[0].rawSig).toStrictEqual('raw sig');
});
it('rejects message', function () {
it('rejects message', () => {
typedMessageManager.rejectMsg(numberMsgId);
assert.equal(messages[0].status, TRANSACTION_STATUSES.REJECTED);
expect(messages[0].status).toStrictEqual(TRANSACTION_STATUSES.REJECTED);
});
});

@ -1,5 +1,3 @@
import { strict as assert } from 'assert';
import sinon from 'sinon';
import { isPrefixedFormattedHexString } from '../../../shared/modules/network.utils';
import {
ENVIRONMENT_TYPE_POPUP,
@ -13,201 +11,154 @@ import {
} from '../../../shared/constants/app';
import { getEnvironmentType, getPlatform } from './util';
describe('app utils', function () {
describe('getEnvironmentType', function () {
it('should return popup type', function () {
describe('app utils', () => {
describe('getEnvironmentType', () => {
it('should return popup type', () => {
const environmentType = getEnvironmentType(
'http://extension-id/popup.html',
);
assert.equal(environmentType, ENVIRONMENT_TYPE_POPUP);
expect(environmentType).toStrictEqual(ENVIRONMENT_TYPE_POPUP);
});
it('should return notification type', function () {
it('should return notification type', () => {
const environmentType = getEnvironmentType(
'http://extension-id/notification.html',
);
assert.equal(environmentType, ENVIRONMENT_TYPE_NOTIFICATION);
expect(environmentType).toStrictEqual(ENVIRONMENT_TYPE_NOTIFICATION);
});
it('should return fullscreen type for home.html', function () {
it('should return fullscreen type for home.html', () => {
const environmentType = getEnvironmentType(
'http://extension-id/home.html',
);
assert.equal(environmentType, ENVIRONMENT_TYPE_FULLSCREEN);
expect(environmentType).toStrictEqual(ENVIRONMENT_TYPE_FULLSCREEN);
});
it('should return fullscreen type for phishing.html', function () {
it('should return fullscreen type for phishing.html', () => {
const environmentType = getEnvironmentType(
'http://extension-id/phishing.html',
);
assert.equal(environmentType, ENVIRONMENT_TYPE_FULLSCREEN);
expect(environmentType).toStrictEqual(ENVIRONMENT_TYPE_FULLSCREEN);
});
it('should return background type', function () {
it('should return background type', () => {
const environmentType = getEnvironmentType(
'http://extension-id/_generated_background_page.html',
);
assert.equal(environmentType, ENVIRONMENT_TYPE_BACKGROUND);
expect(environmentType).toStrictEqual(ENVIRONMENT_TYPE_BACKGROUND);
});
it('should return the correct type for a URL with a hash fragment', function () {
it('should return the correct type for a URL with a hash fragment', () => {
const environmentType = getEnvironmentType(
'http://extension-id/popup.html#hash',
);
assert.equal(environmentType, ENVIRONMENT_TYPE_POPUP);
expect(environmentType).toStrictEqual(ENVIRONMENT_TYPE_POPUP);
});
it('should return the correct type for a URL with query parameters', function () {
it('should return the correct type for a URL with query parameters', () => {
const environmentType = getEnvironmentType(
'http://extension-id/popup.html?param=foo',
);
assert.equal(environmentType, ENVIRONMENT_TYPE_POPUP);
expect(environmentType).toStrictEqual(ENVIRONMENT_TYPE_POPUP);
});
it('should return the correct type for a URL with query parameters and a hash fragment', function () {
it('should return the correct type for a URL with query parameters and a hash fragment', () => {
const environmentType = getEnvironmentType(
'http://extension-id/popup.html?param=foo#hash',
);
assert.equal(environmentType, ENVIRONMENT_TYPE_POPUP);
expect(environmentType).toStrictEqual(ENVIRONMENT_TYPE_POPUP);
});
});
describe('isPrefixedFormattedHexString', function () {
it('should return true for valid hex strings', function () {
assert.equal(
isPrefixedFormattedHexString('0x1'),
true,
'should return true',
);
describe('isPrefixedFormattedHexString', () => {
it('should return true for valid hex strings', () => {
expect(isPrefixedFormattedHexString('0x1')).toStrictEqual(true);
assert.equal(
isPrefixedFormattedHexString('0xa'),
true,
'should return true',
);
expect(isPrefixedFormattedHexString('0xa')).toStrictEqual(true);
assert.equal(
expect(
isPrefixedFormattedHexString('0xabcd1123fae909aad87452'),
true,
'should return true',
);
).toStrictEqual(true);
});
it('should return false for invalid hex strings', function () {
assert.equal(
isPrefixedFormattedHexString('0x'),
false,
'should return false',
);
it('should return false for invalid hex strings', () => {
expect(isPrefixedFormattedHexString('0x')).toStrictEqual(false);
assert.equal(
isPrefixedFormattedHexString('0x0'),
false,
'should return false',
);
expect(isPrefixedFormattedHexString('0x0')).toStrictEqual(false);
assert.equal(
isPrefixedFormattedHexString('0x01'),
false,
'should return false',
);
expect(isPrefixedFormattedHexString('0x01')).toStrictEqual(false);
assert.equal(
isPrefixedFormattedHexString(' 0x1'),
false,
'should return false',
);
expect(isPrefixedFormattedHexString(' 0x1')).toStrictEqual(false);
assert.equal(
isPrefixedFormattedHexString('0x1 '),
false,
'should return false',
);
expect(isPrefixedFormattedHexString('0x1 ')).toStrictEqual(false);
assert.equal(
isPrefixedFormattedHexString('0x1afz'),
false,
'should return false',
);
expect(isPrefixedFormattedHexString('0x1afz')).toStrictEqual(false);
assert.equal(
isPrefixedFormattedHexString('z'),
false,
'should return false',
);
expect(isPrefixedFormattedHexString('z')).toStrictEqual(false);
assert.equal(
isPrefixedFormattedHexString(2),
false,
'should return false',
);
expect(isPrefixedFormattedHexString(2)).toStrictEqual(false);
assert.equal(
isPrefixedFormattedHexString(['0x1']),
false,
'should return false',
);
expect(isPrefixedFormattedHexString(['0x1'])).toStrictEqual(false);
assert.equal(
isPrefixedFormattedHexString(),
false,
'should return false',
);
expect(isPrefixedFormattedHexString()).toStrictEqual(false);
});
});
describe('getPlatform', function () {
const setBrowserSpecificWindow = (browser) => {
describe('getPlatform', () => {
let userAgent, setBrowserSpecificWindow;
beforeEach(() => {
userAgent = jest.spyOn(window.navigator, 'userAgent', 'get');
setBrowserSpecificWindow = (browser) => {
switch (browser) {
case 'firefox': {
sinon.stub(window, 'navigator').value({
userAgent:
userAgent.mockReturnValue(
'Mozilla/5.0 (Macintosh; Intel Mac OS X 10.15; rv:95.0) Gecko/20100101 Firefox/95.0',
});
);
break;
}
case 'edge': {
sinon.stub(window, 'navigator').value({
userAgent:
userAgent.mockReturnValue(
'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/95.0.4638.54 Safari/537.36 Edg/95.0.1020.30',
});
);
break;
}
case 'opera': {
sinon.stub(window, 'navigator').value({
userAgent:
userAgent.mockReturnValue(
'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/94.0.4606.81 Safari/537.36 OPR/80.0.4170.63',
});
);
break;
}
default: {
sinon.stub(window, 'navigator').value({
userAgent:
userAgent.mockReturnValue(
'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/94.0.4606.81 Safari/537.36',
});
);
break;
}
}
};
});
it('should detect Firefox', function () {
it('should detect Firefox', () => {
setBrowserSpecificWindow('firefox');
assert.equal(getPlatform(), PLATFORM_FIREFOX);
expect(getPlatform()).toStrictEqual(PLATFORM_FIREFOX);
});
it('should detect Edge', function () {
it('should detect Edge', () => {
setBrowserSpecificWindow('edge');
assert.equal(getPlatform(), PLATFORM_EDGE);
expect(getPlatform()).toStrictEqual(PLATFORM_EDGE);
});
it('should detect Opera', function () {
it('should detect Opera', () => {
setBrowserSpecificWindow('opera');
assert.equal(getPlatform(), PLATFORM_OPERA);
expect(getPlatform()).toStrictEqual(PLATFORM_OPERA);
});
it('should detect Chrome', function () {
it('should detect Chrome', () => {
setBrowserSpecificWindow('chrome');
assert.equal(getPlatform(), PLATFORM_CHROME);
expect(getPlatform()).toStrictEqual(PLATFORM_CHROME);
});
});
});

File diff suppressed because it is too large Load Diff

@ -33,22 +33,32 @@ const firstTimeState = {
const ganacheServer = new Ganache();
const threeBoxSpies = {
_registerUpdates: sinon.spy(),
init: sinon.stub(),
getLastUpdated: sinon.stub(),
getThreeBoxSyncingState: sinon.stub().returns(true),
restoreFromThreeBox: sinon.stub(),
setShowRestorePromptToFalse: sinon.stub(),
setThreeBoxSyncingPermission: sinon.stub(),
turnThreeBoxSyncingOn: sinon.stub(),
_registerUpdates: sinon.spy(),
};
class ThreeBoxControllerMock {
constructor() {
this._registerUpdates = threeBoxSpies._registerUpdates;
this.init = threeBoxSpies.init;
this.getLastUpdated = threeBoxSpies.getLastUpdated;
this.getThreeBoxSyncingState = threeBoxSpies.getThreeBoxSyncingState;
this.restoreFromThreeBox = threeBoxSpies.restoreFromThreeBox;
this.setShowRestorePromptToFalse =
threeBoxSpies.setShowRestorePromptToFalse;
this.setThreeBoxSyncingPermission =
threeBoxSpies.setThreeBoxSyncingPermission;
this.store = {
subscribe: () => undefined,
getState: () => ({}),
};
this.init = threeBoxSpies.init;
this.getThreeBoxSyncingState = threeBoxSpies.getThreeBoxSyncingState;
this.turnThreeBoxSyncingOn = threeBoxSpies.turnThreeBoxSyncingOn;
this._registerUpdates = threeBoxSpies._registerUpdates;
}
}
@ -423,35 +433,10 @@ describe('MetaMaskController', function () {
});
describe('#getApi', function () {
it('getState', function (done) {
let state;
it('getState', function () {
const getApi = metamaskController.getApi();
getApi.getState((err, res) => {
if (err) {
done(err);
} else {
state = res;
}
});
const state = getApi.getState();
assert.deepEqual(state, metamaskController.getState());
done();
});
});
describe('preferencesController', function () {
it('defaults useBlockie to false', function () {
assert.equal(
metamaskController.preferencesController.store.getState().useBlockie,
false,
);
});
it('setUseBlockie to true', function () {
metamaskController.setUseBlockie(true, noop);
assert.equal(
metamaskController.preferencesController.store.getState().useBlockie,
true,
);
});
});
@ -768,10 +753,7 @@ describe('MetaMaskController', function () {
sinon.stub(metamaskController.preferencesController, 'removeAddress');
sinon.stub(metamaskController.accountTracker, 'removeAccount');
sinon.stub(metamaskController.keyringController, 'removeAccount');
sinon.stub(
metamaskController.permissionsController,
'removeAllAccountPermissions',
);
sinon.stub(metamaskController, 'removeAllAccountPermissions');
ret = await metamaskController.removeAccount(addressToRemove);
});
@ -780,7 +762,7 @@ describe('MetaMaskController', function () {
metamaskController.keyringController.removeAccount.restore();
metamaskController.accountTracker.removeAccount.restore();
metamaskController.preferencesController.removeAddress.restore();
metamaskController.permissionsController.removeAllAccountPermissions.restore();
metamaskController.removeAllAccountPermissions.restore();
});
it('should call preferencesController.removeAddress', async function () {
@ -804,9 +786,9 @@ describe('MetaMaskController', function () {
),
);
});
it('should call permissionsController.removeAllAccountPermissions', async function () {
it('should call metamaskController.removeAllAccountPermissions', async function () {
assert(
metamaskController.permissionsController.removeAllAccountPermissions.calledWith(
metamaskController.removeAllAccountPermissions.calledWith(
addressToRemove,
),
);
@ -816,21 +798,6 @@ describe('MetaMaskController', function () {
});
});
describe('#setCurrentLocale', function () {
it('checks the default currentLocale', function () {
const preferenceCurrentLocale = metamaskController.preferencesController.store.getState()
.currentLocale;
assert.equal(preferenceCurrentLocale, 'en_US');
});
it('sets current locale in preferences controller', function () {
metamaskController.setCurrentLocale('ja', noop);
const preferenceCurrentLocale = metamaskController.preferencesController.store.getState()
.currentLocale;
assert.equal(preferenceCurrentLocale, 'ja');
});
});
describe('#newUnsignedMessage', function () {
let msgParams, metamaskMsgs, messages, msgId;

@ -351,13 +351,13 @@ describe('migration #48', () => {
data: {
AddressBookController: {
addressBook: {
'1': {
1: {
address1: {
chainId: '1',
foo: 'bar',
},
},
'100': {
100: {
address1: {
chainId: '100',
foo: 'bar',
@ -416,7 +416,7 @@ describe('migration #48', () => {
data: {
AddressBookController: {
addressBook: {
'2': {
2: {
address1: {
chainId: '2',
key2: 'kaplar',
@ -489,7 +489,7 @@ describe('migration #48', () => {
AddressBookController: {
addressBook: {
'0x1': { foo: { bar: 'baz' } },
'kaplar': { foo: { bar: 'baz' } },
kaplar: { foo: { bar: 'baz' } },
},
bar: {
baz: 'buzz',
@ -505,7 +505,7 @@ describe('migration #48', () => {
AddressBookController: {
addressBook: {
'0x1': { foo: { bar: 'baz' } },
'kaplar': { foo: { bar: 'baz' } },
kaplar: { foo: { bar: 'baz' } },
},
bar: {
baz: 'buzz',

@ -0,0 +1,161 @@
import { cloneDeep } from 'lodash';
const version = 68;
/**
* Transforms the PermissionsController and PermissionsMetadata substates
* to match the new permission system.
*/
export default {
version,
async migrate(originalVersionedData) {
const versionedData = cloneDeep(originalVersionedData);
versionedData.meta.version = version;
const state = versionedData.data;
const newState = transformState(state);
versionedData.data = newState;
return versionedData;
},
};
function transformState(state) {
const {
PermissionsController = {},
PermissionsMetadata = {},
...remainingState
} = state;
const {
domainMetadata = {},
permissionsHistory = {},
permissionsLog = [],
} = PermissionsMetadata;
return {
...remainingState,
PermissionController: getPermissionControllerState(PermissionsController),
PermissionLogController: {
permissionActivityLog: permissionsLog,
permissionHistory: permissionsHistory,
},
SubjectMetadataController: getSubjectMetadataControllerState(
domainMetadata,
),
};
}
function getPermissionControllerState(PermissionsController) {
const { domains = {} } = PermissionsController;
/**
* Example existing domain entry. Every existing domain will have a single
* eth_accounts permission, which simplifies the transform.
*
* 'https://metamask.github.io': {
* permissions: [
* {
* '@context': ['https://github.com/MetaMask/rpc-cap'],
* 'caveats': [
* {
* name: 'primaryAccountOnly',
* type: 'limitResponseLength',
* value: 1,
* },
* {
* name: 'exposedAccounts',
* type: 'filterResponse',
* value: ['0x0c97a5c81e50a02ff8be73cc3f0a0569e61f4ed8'],
* },
* ],
* 'date': 1616006369498,
* 'id': '3d0bdc27-e8e4-4fb0-a24b-340d61f6a3fa',
* 'invoker': 'https://metamask.github.io',
* 'parentCapability': 'eth_accounts',
* },
* ],
* },
*/
const ETH_ACCOUNTS = 'eth_accounts';
const NEW_CAVEAT_TYPE = 'restrictReturnedAccounts';
const OLD_CAVEAT_NAME = 'exposedAccounts';
const subjects = Object.entries(domains).reduce(
(transformed, [origin, domainEntry]) => {
const {
permissions: [ethAccountsPermission],
} = domainEntry;
// There are two caveats for each eth_accounts permission, but we only
// need the value of one of them in the new permission system.
const oldCaveat = ethAccountsPermission.caveats.find(
(caveat) => caveat.name === OLD_CAVEAT_NAME,
);
const newPermission = {
...ethAccountsPermission,
caveats: [{ type: NEW_CAVEAT_TYPE, value: oldCaveat.value }],
};
// We never used this, and just omit it in the new system.
delete newPermission['@context'];
transformed[origin] = {
origin,
permissions: {
[ETH_ACCOUNTS]: newPermission,
},
};
return transformed;
},
{},
);
return {
subjects,
};
}
function getSubjectMetadataControllerState(domainMetadata) {
/**
* Example existing domainMetadata entry.
*
* "https://www.youtube.com": {
* "host": "www.youtube.com",
* "icon": null,
* "lastUpdated": 1637697914908,
* "name": "YouTube"
* }
*/
const subjectMetadata = Object.entries(domainMetadata).reduce(
(transformed, [origin, metadata]) => {
const {
name = null,
icon = null,
extensionId = null,
...other
} = metadata;
// We're getting rid of these.
delete other.lastUpdated;
delete other.host;
if (origin) {
transformed[origin] = {
name,
iconUrl: icon,
extensionId,
...other,
origin,
};
}
return transformed;
},
{},
);
return {
subjectMetadata,
};
}

@ -0,0 +1,450 @@
import migration68 from './068';
describe('migration #68', () => {
it('should update the version metadata', async () => {
const oldStorage = {
meta: {
version: 67,
},
data: {},
};
const newStorage = await migration68.migrate(oldStorage);
expect(newStorage.meta).toStrictEqual({
version: 68,
});
});
it('should migrate all data', async () => {
const oldStorage = {
meta: {
version: 67,
},
data: getOldState(),
};
const newStorage = await migration68.migrate(oldStorage);
expect(newStorage).toMatchObject({
meta: {
version: 68,
},
data: {
FooController: { a: 'b' },
PermissionController: { subjects: expect.any(Object) },
PermissionLogController: {
permissionActivityLog: expect.any(Object),
permissionHistory: expect.any(Object),
},
SubjectMetadataController: { subjectMetadata: expect.any(Object) },
},
});
expect(newStorage.PermissionsController).toBeUndefined();
expect(newStorage.PermissionsMetadata).toBeUndefined();
});
it('should migrate the PermissionsController state', async () => {
const oldStorage = {
meta: {},
data: {
PermissionsController: getOldState().PermissionsController,
},
};
const newStorage = await migration68.migrate(oldStorage);
const { PermissionController } = newStorage.data;
expect(PermissionController).toStrictEqual({
subjects: {
'https://faucet.metamask.io': {
origin: 'https://faucet.metamask.io',
permissions: {
eth_accounts: {
caveats: [
{
type: 'restrictReturnedAccounts',
value: ['0xc42edfcc21ed14dda456aa0756c153f7985d8813'],
},
],
date: 1597334833084,
id: 'e01bada4-ddc7-47b6-be67-d4603733e0e9',
invoker: 'https://faucet.metamask.io',
parentCapability: 'eth_accounts',
},
},
},
'https://metamask.github.io': {
origin: 'https://metamask.github.io',
permissions: {
eth_accounts: {
caveats: [
{
type: 'restrictReturnedAccounts',
value: ['0x0dcd5d886577d5081b0c52e242ef29e70be3e7bc'],
},
],
date: 1616006369498,
id: '3d0bdc27-e8e4-4fb0-a24b-340d61f6a3fa',
invoker: 'https://metamask.github.io',
parentCapability: 'eth_accounts',
},
},
},
'https://xdai.io': {
origin: 'https://xdai.io',
permissions: {
eth_accounts: {
caveats: [
{
type: 'restrictReturnedAccounts',
value: ['0x0dcd5d886577d5081b0c52e242ef29e70be3e7bc'],
},
],
date: 1605908022382,
id: '88c5de24-11a9-4f1e-9651-b072f4c11928',
invoker: 'https://xdai.io',
parentCapability: 'eth_accounts',
},
},
},
},
});
});
it('should migrate the PermissionsMetadata state', async () => {
const oldStorage = {
meta: {},
data: {
PermissionsMetadata: getOldState().PermissionsMetadata,
},
};
const newStorage = await migration68.migrate(oldStorage);
const {
PermissionLogController,
SubjectMetadataController,
} = newStorage.data;
const expected = getOldState().PermissionsMetadata;
expect(PermissionLogController.permissionHistory).toStrictEqual(
expected.permissionsHistory,
);
expect(PermissionLogController.permissionActivityLog).toStrictEqual(
expected.permissionsLog,
);
expect(SubjectMetadataController).toStrictEqual({
subjectMetadata: {
'https://1inch.exchange': {
iconUrl: 'https://1inch.exchange/assets/favicon/favicon-32x32.png',
name: 'DEX Aggregator - 1inch.exchange',
origin: 'https://1inch.exchange',
extensionId: null,
},
'https://ascii-tree-generator.com': {
iconUrl: 'https://ascii-tree-generator.com/favicon.ico',
name: 'ASCII Tree Generator',
origin: 'https://ascii-tree-generator.com',
extensionId: null,
},
'https://caniuse.com': {
iconUrl: 'https://caniuse.com/img/favicon-128.png',
name: 'Can I use... Support tables for HTML5, CSS3, etc',
origin: 'https://caniuse.com',
extensionId: null,
},
'https://core-geth.org': {
iconUrl: 'https://core-geth.org/icons/icon-48x48.png',
name: 'core-geth.org',
origin: 'https://core-geth.org',
extensionId: null,
},
'https://docs.npmjs.com': {
iconUrl: 'https://docs.npmjs.com/favicon-32x32.png',
name: 'package-locks | npm Docs',
origin: 'https://docs.npmjs.com',
extensionId: null,
},
},
});
});
it('should handle domain metadata edge cases', async () => {
const oldStorage = {
meta: {},
data: {
PermissionsMetadata: {
domainMetadata: {
'foo.bar': {
// no name
icon: 'fooIcon',
extensionId: 'fooExtension', // non-null
origin: null, // should get overwritten
extraProperty: 'bar', // should be preserved
},
},
},
},
};
const newStorage = await migration68.migrate(oldStorage);
expect(
newStorage.data.SubjectMetadataController.subjectMetadata,
).toStrictEqual({
'foo.bar': {
name: null, // replaced with null
iconUrl: 'fooIcon', // preserved value, changed name
extensionId: 'fooExtension', // preserved
origin: 'foo.bar', // overwritten with correct origin
extraProperty: 'bar', // preserved
},
});
});
});
function getOldState() {
return {
FooController: { a: 'b' }, // just to ensure it's not touched
PermissionsController: {
domains: {
'https://faucet.metamask.io': {
permissions: [
{
'@context': ['https://github.com/MetaMask/rpc-cap'],
caveats: [
{
name: 'primaryAccountOnly',
type: 'limitResponseLength',
value: 1,
},
{
name: 'exposedAccounts',
type: 'filterResponse',
value: ['0xc42edfcc21ed14dda456aa0756c153f7985d8813'],
},
],
date: 1597334833084,
id: 'e01bada4-ddc7-47b6-be67-d4603733e0e9',
invoker: 'https://faucet.metamask.io',
parentCapability: 'eth_accounts',
},
],
},
'https://metamask.github.io': {
permissions: [
{
'@context': ['https://github.com/MetaMask/rpc-cap'],
caveats: [
{
name: 'primaryAccountOnly',
type: 'limitResponseLength',
value: 1,
},
{
name: 'exposedAccounts',
type: 'filterResponse',
value: ['0x0dcd5d886577d5081b0c52e242ef29e70be3e7bc'],
},
],
date: 1616006369498,
id: '3d0bdc27-e8e4-4fb0-a24b-340d61f6a3fa',
invoker: 'https://metamask.github.io',
parentCapability: 'eth_accounts',
},
],
},
'https://xdai.io': {
permissions: [
{
'@context': ['https://github.com/MetaMask/rpc-cap'],
caveats: [
{
name: 'primaryAccountOnly',
type: 'limitResponseLength',
value: 1,
},
{
name: 'exposedAccounts',
type: 'filterResponse',
value: ['0x0dcd5d886577d5081b0c52e242ef29e70be3e7bc'],
},
],
date: 1605908022382,
id: '88c5de24-11a9-4f1e-9651-b072f4c11928',
invoker: 'https://xdai.io',
parentCapability: 'eth_accounts',
},
],
},
},
permissionsDescriptions: {},
permissionsRequests: [],
},
PermissionsMetadata: {
domainMetadata: {
'https://1inch.exchange': {
host: '1inch.exchange',
icon: 'https://1inch.exchange/assets/favicon/favicon-32x32.png',
lastUpdated: 1605489265143,
name: 'DEX Aggregator - 1inch.exchange',
},
'https://ascii-tree-generator.com': {
host: 'ascii-tree-generator.com',
icon: 'https://ascii-tree-generator.com/favicon.ico',
lastUpdated: 1637721988618,
name: 'ASCII Tree Generator',
},
'https://caniuse.com': {
host: 'caniuse.com',
icon: 'https://caniuse.com/img/favicon-128.png',
lastUpdated: 1637692936599,
name: 'Can I use... Support tables for HTML5, CSS3, etc',
},
'https://core-geth.org': {
host: 'core-geth.org',
icon: 'https://core-geth.org/icons/icon-48x48.png',
lastUpdated: 1637692093173,
name: 'core-geth.org',
},
'https://docs.npmjs.com': {
host: 'docs.npmjs.com',
icon: 'https://docs.npmjs.com/favicon-32x32.png',
lastUpdated: 1637721451476,
name: 'package-locks | npm Docs',
},
},
permissionsHistory: {
'https://opensea.io': {
eth_accounts: {
accounts: {
'0xc42edfcc21ed14dda456aa0756c153f7985d8813': 1617399873696,
},
lastApproved: 1617399873696,
},
},
'https://faucet.metamask.io': {
eth_accounts: {
accounts: {
'0x0dcd5d886577d5081b0c52e242ef29e70be3e7bc': 1620369333736,
},
lastApproved: 1610405614031,
},
},
'https://metamask.github.io': {
eth_accounts: {
accounts: {
'0x0dcd5d886577d5081b0c52e242ef29e70be3e7bc': 1620759882723,
'0xf9eab18b7db3adf8cd6bd5f4aed9e1d5e0e7f926': 1616005950557,
},
lastApproved: 1620759882723,
},
},
'https://xdai.io': {
eth_accounts: {
accounts: {
'0x0dcd5d886577d5081b0c52e242ef29e70be3e7bc': 1620369333736,
},
lastApproved: 1605908022384,
},
},
},
permissionsLog: [
{
id: 3642448888,
method: 'eth_accounts',
methodType: 'restricted',
origin: 'https://metamask.github.io',
request: {
id: 3642448888,
jsonrpc: '2.0',
method: 'eth_accounts',
origin: 'https://metamask.github.io',
tabId: 489,
},
requestTime: 1615325885561,
response: {
id: 3642448888,
jsonrpc: '2.0',
result: [],
},
responseTime: 1615325885561,
success: true,
},
{
id: 2960964763,
method: 'wallet_getPermissions',
methodType: 'internal',
origin: 'https://metamask.github.io',
request: {
id: 2960964763,
jsonrpc: '2.0',
method: 'wallet_getPermissions',
origin: 'https://metamask.github.io',
tabId: 145,
},
requestTime: 1620759866273,
response: {
id: 2960964763,
jsonrpc: '2.0',
result: [
{
'@context': ['https://github.com/MetaMask/rpc-cap'],
caveats: [
{
name: 'primaryAccountOnly',
type: 'limitResponseLength',
value: 1,
},
{
name: 'exposedAccounts',
type: 'filterResponse',
value: ['0x0dcd5d886577d5081b0c52e242ef29e70be3e7bc'],
},
],
date: 1616006369498,
id: '3d0bdc27-e8e4-4fb0-a24b-340d61f6a3fa',
invoker: 'https://metamask.github.io',
parentCapability: 'eth_accounts',
},
],
},
responseTime: 1620759866273,
success: true,
},
{
id: 2960964764,
method: 'eth_accounts',
methodType: 'restricted',
origin: 'https://metamask.github.io',
request: {
id: 2960964764,
jsonrpc: '2.0',
method: 'eth_accounts',
origin: 'https://metamask.github.io',
tabId: 145,
},
requestTime: 1620759866280,
response: {
id: 2960964764,
jsonrpc: '2.0',
result: [],
},
responseTime: 1620759866280,
success: true,
},
{
id: 519616456,
method: 'eth_accounts',
methodType: 'restricted',
origin: 'http://localhost:9011',
request:
'{\n "method": "eth_accounts",\n "jsonrpc": "2.0",\n "id": 519616456,\n "origin": "http://localhost:9011",\n "tabId": 1020\n}',
requestTime: 1636479612050,
response:
'{\n "id": 519616456,\n "jsonrpc": "2.0",\n "result": []\n}',
responseTime: 1636479612051,
success: true,
},
],
},
};
}

@ -0,0 +1,41 @@
import { cloneDeep } from 'lodash';
import { SUBJECT_TYPES } from '../../../shared/constants/app';
const version = 69;
/**
* Adds the `subjectType` property to all subject metadata.
*/
export default {
version,
async migrate(originalVersionedData) {
const versionedData = cloneDeep(originalVersionedData);
versionedData.meta.version = version;
const state = versionedData.data;
const newState = transformState(state);
versionedData.data = newState;
return versionedData;
},
};
function transformState(state) {
if (typeof state?.SubjectMetadataController?.subjectMetadata === 'object') {
const {
SubjectMetadataController: { subjectMetadata },
} = state;
// mutate SubjectMetadataController.subjectMetadata in place
Object.values(subjectMetadata).forEach((metadata) => {
if (
metadata &&
typeof metadata === 'object' &&
!Array.isArray(metadata)
) {
metadata.subjectType = metadata.extensionId
? SUBJECT_TYPES.EXTENSION
: SUBJECT_TYPES.WEBSITE;
}
});
}
return state;
}

@ -0,0 +1,102 @@
import { SUBJECT_TYPES } from '../../../shared/constants/app';
import migration69 from './069';
describe('migration #69', () => {
it('should update the version metadata', async () => {
const oldStorage = {
meta: {
version: 68,
},
data: {},
};
const newStorage = await migration69.migrate(oldStorage);
expect(newStorage.meta).toStrictEqual({
version: 69,
});
});
it('should migrate all data', async () => {
const oldStorage = {
meta: {
version: 68,
},
data: {
FooController: { a: 'b' },
SubjectMetadataController: {
subjectMetadata: {
'https://1inch.exchange': {
iconUrl:
'https://1inch.exchange/assets/favicon/favicon-32x32.png',
name: 'DEX Aggregator - 1inch.exchange',
origin: 'https://1inch.exchange',
extensionId: null,
},
'https://ascii-tree-generator.com': {
iconUrl: 'https://ascii-tree-generator.com/favicon.ico',
name: 'ASCII Tree Generator',
origin: 'https://ascii-tree-generator.com',
extensionId: 'ascii-tree-generator-extension',
},
'https://null.com': null,
'https://foo.com': 'bad data',
'https://bar.com': ['bad data'],
},
},
},
};
const newStorage = await migration69.migrate(oldStorage);
expect(newStorage).toStrictEqual({
meta: {
version: 69,
},
data: {
FooController: { a: 'b' },
SubjectMetadataController: {
subjectMetadata: {
'https://1inch.exchange': {
iconUrl:
'https://1inch.exchange/assets/favicon/favicon-32x32.png',
name: 'DEX Aggregator - 1inch.exchange',
origin: 'https://1inch.exchange',
extensionId: null,
subjectType: SUBJECT_TYPES.WEBSITE,
},
'https://ascii-tree-generator.com': {
iconUrl: 'https://ascii-tree-generator.com/favicon.ico',
name: 'ASCII Tree Generator',
origin: 'https://ascii-tree-generator.com',
extensionId: 'ascii-tree-generator-extension',
subjectType: SUBJECT_TYPES.EXTENSION,
},
'https://null.com': null,
'https://foo.com': 'bad data',
'https://bar.com': ['bad data'],
},
},
},
});
});
it('should handle missing SubjectMetadataController', async () => {
const oldStorage = {
meta: {
version: 68,
},
data: {
FooController: { a: 'b' },
},
};
const newStorage = await migration69.migrate(oldStorage);
expect(newStorage).toStrictEqual({
meta: {
version: 69,
},
data: {
FooController: { a: 'b' },
},
});
});
});

@ -71,6 +71,8 @@ import m064 from './064';
import m065 from './065';
import m066 from './066';
import m067 from './067';
import m068 from './068';
import m069 from './069';
const migrations = [
m002,
@ -139,6 +141,8 @@ const migrations = [
m065,
m066,
m067,
m068,
m069,
];
export default migrations;

@ -0,0 +1,13 @@
"project_id_env": CROWDIN_PROJECT_ID
"api_token_env": CROWDIN_PERSONAL_TOKEN
"base_path" : "."
"base_url" : "https://metamask.crowdin.com"
"preserve_hierarchy": true
files: [
{
"source" : "app/_locales/en/messages.json",
"translation" : "/app/_locales/%two_letters_code%/%original_file_name%",
}
]

@ -23,6 +23,7 @@ const { BuildType, getBrowserVersionMap } = require('./utils');
// Packages required dynamically via browserify configuration in dependencies
// Required for LavaMoat policy generation
require('loose-envify');
require('globalthis');
require('@babel/plugin-proposal-object-rest-spread');
require('@babel/plugin-transform-runtime');
require('@babel/plugin-proposal-class-properties');

@ -1,6 +1,6 @@
const { promises: fs } = require('fs');
const path = require('path');
const { merge, cloneDeep } = require('lodash');
const { mergeWith, cloneDeep } = require('lodash');
const baseManifest = require('../../app/manifest/_base.json');
@ -28,11 +28,12 @@ function createManifestTasks({
`${platform}.json`,
),
);
const result = merge(
const result = mergeWith(
cloneDeep(baseManifest),
platformModifications,
browserVersionMap[platform],
await getBuildModifications(buildType, platform),
customArrayMerge,
);
const dir = path.join('.', 'dist', platform);
await fs.mkdir(dir, { recursive: true });
@ -99,6 +100,14 @@ function createManifestTasks({
);
};
}
// helper for merging obj value
function customArrayMerge(objValue, srcValue) {
if (Array.isArray(objValue)) {
return [...new Set([...objValue, ...srcValue])];
}
return undefined;
}
}
// helper for reading and deserializing json from fs

@ -366,6 +366,7 @@ function createFactoredBuild({
minify,
reloadOnChange,
shouldLintFenceFiles,
testing,
});
// set bundle entries
@ -543,6 +544,7 @@ function createNormalBundle({
minify,
reloadOnChange,
shouldLintFenceFiles,
testing,
});
// set bundle entries
@ -599,6 +601,7 @@ function setupBundlerDefaults(
minify,
reloadOnChange,
shouldLintFenceFiles,
testing,
},
) {
const { bundlerOpts } = buildConfiguration;
@ -620,8 +623,9 @@ function setupBundlerDefaults(
});
// Ensure react-devtools are not included in non-dev builds
if (!devMode) {
if (!devMode || testing) {
bundlerOpts.manualIgnore.push('react-devtools');
bundlerOpts.manualIgnore.push('remote-redux-devtools');
}
// Inject environment variables via node-style `process.env`
@ -779,7 +783,7 @@ function getEnvironmentVariables({ buildType, devMode, testing }) {
METAMASK_VERSION: version,
METAMASK_BUILD_TYPE: buildType,
NODE_ENV: devMode ? ENVIRONMENT.DEVELOPMENT : ENVIRONMENT.PRODUCTION,
IN_TEST: testing ? 'true' : false,
IN_TEST: testing,
PUBNUB_SUB_KEY: process.env.PUBNUB_SUB_KEY || '',
PUBNUB_PUB_KEY: process.env.PUBNUB_PUB_KEY || '',
CONF: devMode ? metamaskrc : {},

@ -3,7 +3,7 @@ const { PassThrough, Transform } = require('stream');
const { BuildType } = require('../utils');
const { lintTransformedFile } = require('./utils');
const hasOwnProperty = (obj, key) => Reflect.hasOwnProperty.call(obj, key);
const hasKey = (obj, key) => Reflect.hasOwnProperty.call(obj, key);
module.exports = {
createRemoveFencedCodeTransform,
@ -90,7 +90,7 @@ function createRemoveFencedCodeTransform(
buildType,
shouldLintTransformedFiles = true,
) {
if (!hasOwnProperty(BuildType, buildType)) {
if (!hasKey(BuildType, buildType)) {
throw new Error(
`Code fencing transform received unrecognized build type "${buildType}".`,
);
@ -140,7 +140,7 @@ const CommandValidators = {
}
params.forEach((param) => {
if (!hasOwnProperty(BuildType, param)) {
if (!hasKey(BuildType, param)) {
throw new Error(
getInvalidParamsMessage(
filePath,
@ -250,7 +250,7 @@ function removeFencedCode(filePath, typeOfCurrentBuild, fileContent) {
// The first element of a RegEx match array is the input
const [, terminus, command, parameters] = directiveMatches;
if (!hasOwnProperty(DirectiveTerminuses, terminus)) {
if (!hasKey(DirectiveTerminuses, terminus)) {
throw new Error(
getInvalidFenceLineMessage(
filePath,
@ -259,7 +259,8 @@ function removeFencedCode(filePath, typeOfCurrentBuild, fileContent) {
),
);
}
if (!hasOwnProperty(DirectiveCommands, command)) {
if (!hasKey(DirectiveCommands, command)) {
throw new Error(
getInvalidFenceLineMessage(
filePath,

Some files were not shown because too many files have changed in this diff Show More

Loading…
Cancel
Save