mirror of
https://github.com/kremalicious/metamask-extension.git
synced 2024-12-23 09:52:26 +01:00
Merge branch 'develop' into master-sync
This commit is contained in:
commit
460f08f008
@ -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
|
||||
|
31
.eslintrc.js
31
.eslintrc.js
@ -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',
|
||||
|
31
.github/workflows/crowdin_action.yml
vendored
Normal file
31
.github/workflows/crowdin_action.yml
vendored
Normal file
@ -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=
|
||||
|
10
.mocharc.js
10
.mocharc.js
@ -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']
|
||||
});
|
@ -46,7 +46,7 @@ export const currentNetworkTxListSample = {
|
||||
]
|
||||
}
|
||||
|
||||
export const domainMetadata = {
|
||||
export const subjectMetadata = {
|
||||
"https://metamask.github.io": {
|
||||
"name": "E2E Test Dapp",
|
||||
"icon": "https://metamask.github.io/test-dapp/metamask-fox.svg",
|
||||
|
6
.storybook/manager.js
Normal file
6
.storybook/manager.js
Normal file
@ -0,0 +1,6 @@
|
||||
import { addons } from '@storybook/addons';
|
||||
import MetaMaskStorybookTheme from './metamask-storybook-theme';
|
||||
|
||||
addons.setConfig({
|
||||
theme: MetaMaskStorybookTheme,
|
||||
});
|
14
.storybook/metamask-storybook-theme.js
Normal file
14
.storybook/metamask-storybook-theme.js
Normal file
@ -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',
|
||||
});
|
27
.storybook/preview-head.html
Normal file
27
.storybook/preview-head.html
Normal file
@ -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,7 +1172,7 @@ const state = {
|
||||
},
|
||||
},
|
||||
},
|
||||
domainMetadata: {
|
||||
subjectMetadata: {
|
||||
'https://metamask.github.io': {
|
||||
name: 'E2E Test Dapp',
|
||||
icon: 'https://metamask.github.io/test-dapp/metamask-fox.svg',
|
||||
|
22
README.md
22
README.md
@ -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."
|
||||
|
@ -167,7 +167,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 +320,9 @@
|
||||
"balanceOutdated": {
|
||||
"message": "Balance may be outdated"
|
||||
},
|
||||
"baseFee": {
|
||||
"message": "Base fee"
|
||||
},
|
||||
"basic": {
|
||||
"message": "Basic"
|
||||
},
|
||||
@ -637,6 +640,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 +656,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"
|
||||
},
|
||||
@ -799,12 +810,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 +847,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"
|
||||
},
|
||||
@ -993,7 +1029,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 +1107,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 +1164,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 +1301,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"
|
||||
},
|
||||
@ -1505,6 +1566,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 +1601,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 +1766,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"
|
||||
},
|
||||
@ -1774,6 +1857,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 +2120,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 +2147,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": {
|
||||
@ -2530,6 +2613,9 @@
|
||||
"stable": {
|
||||
"message": "Stable"
|
||||
},
|
||||
"stableLowercase": {
|
||||
"message": "stable"
|
||||
},
|
||||
"stateLogError": {
|
||||
"message": "Error in retrieving state logs."
|
||||
},
|
||||
@ -2813,6 +2899,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"
|
||||
},
|
||||
|
@ -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": "权限"
|
||||
},
|
||||
|
1
app/images/curve-high.svg
Normal file
1
app/images/curve-high.svg
Normal file
@ -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 |
1
app/images/curve-low.svg
Normal file
1
app/images/curve-low.svg
Normal file
@ -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 |
1
app/images/curve-medium.svg
Normal file
1
app/images/curve-medium.svg
Normal file
@ -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();
|
||||
}
|
||||
|
@ -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);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
71
app/scripts/controllers/permissions/background-api.js
Normal file
71
app/scripts/controllers/permissions/background-api.js
Normal file
@ -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;
|
||||
},
|
||||
};
|
||||
}
|
181
app/scripts/controllers/permissions/background-api.test.js
Normal file
181
app/scripts/controllers/permissions/background-api.test.js
Normal file
@ -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');
|
||||
});
|
||||
});
|
||||
});
|
39
app/scripts/controllers/permissions/caveat-mutators.js
Normal file
39
app/scripts/controllers/permissions/caveat-mutators.js
Normal file
@ -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 };
|
||||
}
|
32
app/scripts/controllers/permissions/caveat-mutators.test.js
Normal file
32
app/scripts/controllers/permissions/caveat-mutators.test.js
Normal file
@ -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,
|
||||
});
|
||||
}
|
||||
}
|
File diff suppressed because it is too large
Load Diff
@ -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);
|
||||
}
|
||||
},
|
||||
},
|
||||
};
|
||||
}
|
84
app/scripts/controllers/permissions/selectors.js
Normal file
84
app/scripts/controllers/permissions/selectors.js
Normal file
@ -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;
|
||||
};
|
116
app/scripts/controllers/permissions/selectors.test.js
Normal file
116
app/scripts/controllers/permissions/selectors.test.js
Normal file
@ -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));
|
||||
});
|
||||
});
|
||||
});
|
258
app/scripts/controllers/permissions/specifications.js
Normal file
258
app/scripts/controllers/permissions/specifications.js
Normal file
@ -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',
|
||||
]);
|
340
app/scripts/controllers/permissions/specifications.test.js
Normal file
340
app/scripts/controllers/permissions/specifications.test.js
Normal file
@ -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);
|
||||
});
|
||||
});
|
||||
});
|
@ -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 undefined name', function () {
|
||||
assert.equal(
|
||||
cleanErrorStack(undefinedErrorName).toString(),
|
||||
it('tests error with message', () => {
|
||||
expect(cleanErrorStack(testError).toString()).toStrictEqual(
|
||||
'Error: Test Message',
|
||||
);
|
||||
});
|
||||
|
||||
it('tests error with blank name', function () {
|
||||
assert.equal(cleanErrorStack(blankErrorName).toString(), 'Test Message');
|
||||
it('tests error with undefined name', () => {
|
||||
expect(cleanErrorStack(undefinedErrorName).toString()).toStrictEqual(
|
||||
'Error: Test Message',
|
||||
);
|
||||
});
|
||||
|
||||
it('tests error with blank message', function () {
|
||||
assert.equal(cleanErrorStack(blankMsgError).toString(), 'Error');
|
||||
it('tests error with blank name', () => {
|
||||
expect(cleanErrorStack(blankErrorName).toString()).toStrictEqual(
|
||||
'Test Message',
|
||||
);
|
||||
});
|
||||
|
||||
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,24 +15,35 @@ const createMetaRPCHandler = (api, outStream) => {
|
||||
});
|
||||
return;
|
||||
}
|
||||
api[data.method](...data.params, (err, result) => {
|
||||
if (outStream._writableState.ended) {
|
||||
return;
|
||||
|
||||
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);
|
||||
}
|
||||
if (err) {
|
||||
outStream.write({
|
||||
jsonrpc: '2.0',
|
||||
error: serializeError(err, { shouldIncludeStack: true }),
|
||||
id: data.id,
|
||||
});
|
||||
} else {
|
||||
outStream.write({
|
||||
jsonrpc: '2.0',
|
||||
result,
|
||||
id: data.id,
|
||||
});
|
||||
}
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
if (error) {
|
||||
outStream.write({
|
||||
jsonrpc: '2.0',
|
||||
error: serializeError(error, { shouldIncludeStack: true }),
|
||||
id: data.id,
|
||||
});
|
||||
} else {
|
||||
outStream.write({
|
||||
jsonrpc: '2.0',
|
||||
result,
|
||||
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,11 @@ 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();
|
||||
@ -33,16 +31,16 @@ 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 an async response to the outstream', () => {
|
||||
const api = {
|
||||
foo: (param1, cb) => {
|
||||
assert.strictEqual(param1, 'bar');
|
||||
cb(new Error('foo-error'));
|
||||
foo: async (param1) => {
|
||||
expect(param1).toStrictEqual('bar');
|
||||
await new Promise((resolve) => setTimeout(() => resolve(), 100));
|
||||
return 'foobarbaz';
|
||||
},
|
||||
};
|
||||
const streamTest = createThoughStream();
|
||||
@ -53,45 +51,65 @@ describe('createMetaRPCHandler', function () {
|
||||
params: ['bar'],
|
||||
});
|
||||
streamTest.on('data', (data) => {
|
||||
assert.strictEqual(data.error.message, 'foo-error');
|
||||
expect(data.result).toStrictEqual('foobarbaz');
|
||||
streamTest.end();
|
||||
done();
|
||||
});
|
||||
});
|
||||
it('can not throw an error for writing an error after end', 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();
|
||||
const handler = createMetaRPCHandler(api, streamTest);
|
||||
handler({
|
||||
id: 1,
|
||||
method: 'foo',
|
||||
params: ['bar'],
|
||||
});
|
||||
streamTest.on('data', (data) => {
|
||||
expect(data.error.message).toStrictEqual('foo-error');
|
||||
streamTest.end();
|
||||
});
|
||||
});
|
||||
it('can not throw an error for writing an error after end', () => {
|
||||
const api = {
|
||||
foo: (param1) => {
|
||||
expect(param1).toStrictEqual('bar');
|
||||
throw new Error('foo-error');
|
||||
},
|
||||
};
|
||||
const streamTest = createThoughStream();
|
||||
const handler = createMetaRPCHandler(api, streamTest);
|
||||
streamTest.end();
|
||||
handler({
|
||||
id: 1,
|
||||
method: 'foo',
|
||||
params: ['bar'],
|
||||
});
|
||||
done();
|
||||
expect(() => {
|
||||
handler({
|
||||
id: 1,
|
||||
method: 'foo',
|
||||
params: ['bar'],
|
||||
});
|
||||
}).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();
|
||||
handler({
|
||||
id: 1,
|
||||
method: 'foo',
|
||||
params: ['bar'],
|
||||
});
|
||||
done();
|
||||
expect(() => {
|
||||
handler({
|
||||
id: 1,
|
||||
method: 'foo',
|
||||
params: ['bar'],
|
||||
});
|
||||
}).not.toThrow();
|
||||
});
|
||||
});
|
||||
|
@ -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() });
|
||||
});
|
||||
|
||||
describe('#getMsgList', function () {
|
||||
it('when new should return empty array', function () {
|
||||
const result = messageManager.messages;
|
||||
assert.ok(Array.isArray(result));
|
||||
assert.equal(result.length, 0);
|
||||
beforeEach(() => {
|
||||
messageManager = new PersonalMessageManager({
|
||||
metricsEvent: jest.fn(),
|
||||
});
|
||||
});
|
||||
|
||||
describe('#addMsg', function () {
|
||||
it('adds a Msg returned in getMsgList', function () {
|
||||
describe('#getMsgList', () => {
|
||||
it('when new should return empty array', () => {
|
||||
const result = messageManager.messages;
|
||||
expect(Array.isArray(result)).toStrictEqual(true);
|
||||
expect(result).toHaveLength(0);
|
||||
});
|
||||
});
|
||||
|
||||
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,57 @@
|
||||
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 { params } = req;
|
||||
if (params && typeof params === 'object' && !Array.isArray(params)) {
|
||||
const { icon = null, name = null, ...remainingParams } = params;
|
||||
|
||||
addSubjectMetadata({
|
||||
...remainingParams,
|
||||
iconUrl: icon,
|
||||
name,
|
||||
subjectType,
|
||||
});
|
||||
} 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) => {
|
||||
switch (browser) {
|
||||
case 'firefox': {
|
||||
sinon.stub(window, 'navigator').value({
|
||||
userAgent:
|
||||
describe('getPlatform', () => {
|
||||
let userAgent, setBrowserSpecificWindow;
|
||||
|
||||
beforeEach(() => {
|
||||
userAgent = jest.spyOn(window.navigator, 'userAgent', 'get');
|
||||
|
||||
setBrowserSpecificWindow = (browser) => {
|
||||
switch (browser) {
|
||||
case 'firefox': {
|
||||
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:
|
||||
);
|
||||
break;
|
||||
}
|
||||
case 'edge': {
|
||||
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:
|
||||
);
|
||||
break;
|
||||
}
|
||||
case 'opera': {
|
||||
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:
|
||||
);
|
||||
|
||||
break;
|
||||
}
|
||||
default: {
|
||||
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;
|
||||
);
|
||||
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 = {
|
||||
init: sinon.stub(),
|
||||
getThreeBoxSyncingState: sinon.stub().returns(true),
|
||||
turnThreeBoxSyncingOn: sinon.stub(),
|
||||
_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(),
|
||||
};
|
||||
|
||||
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',
|
||||
|
161
app/scripts/migrations/068.js
Normal file
161
app/scripts/migrations/068.js
Normal file
@ -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,
|
||||
};
|
||||
}
|
450
app/scripts/migrations/068.test.js
Normal file
450
app/scripts/migrations/068.test.js
Normal file
@ -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,
|
||||
},
|
||||
],
|
||||
},
|
||||
};
|
||||
}
|
41
app/scripts/migrations/069.js
Normal file
41
app/scripts/migrations/069.js
Normal file
@ -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;
|
||||
}
|
102
app/scripts/migrations/069.test.js
Normal file
102
app/scripts/migrations/069.test.js
Normal file
@ -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;
|
||||
|
13
crowdin.yml
Normal file
13
crowdin.yml
Normal file
@ -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,
|
||||
|
@ -240,7 +240,7 @@ async function start() {
|
||||
body: JSON_PAYLOAD,
|
||||
headers: {
|
||||
'User-Agent': 'metamaskbot',
|
||||
'Authorization': `token ${GITHUB_COMMENT_TOKEN}`,
|
||||
Authorization: `token ${GITHUB_COMMENT_TOKEN}`,
|
||||
},
|
||||
});
|
||||
if (!response.ok) {
|
||||
|
@ -1,6 +1,6 @@
|
||||
# Add Custom Build to Firefox
|
||||
|
||||
Go to the url `about:debugging`.
|
||||
Go to the url `about:debugging#addons`.
|
||||
|
||||
Click the button `Load Temporary Add-On`.
|
||||
|
||||
|
@ -1,5 +1,9 @@
|
||||
module.exports = {
|
||||
collectCoverageFrom: ['<rootDir>/ui/**/*.js', '<rootDir>/shared/**/*.js'],
|
||||
collectCoverageFrom: [
|
||||
'<rootDir>/app/scripts/controllers/permissions/*.js',
|
||||
'<rootDir>/shared/**/*.js',
|
||||
'<rootDir>/ui/**/*.js',
|
||||
],
|
||||
coverageDirectory: './jest-coverage/main',
|
||||
coveragePathIgnorePatterns: ['.stories.js', '.snap'],
|
||||
coverageReporters: ['html', 'text-summary'],
|
||||
@ -10,6 +14,12 @@ module.exports = {
|
||||
lines: 43,
|
||||
statements: 43,
|
||||
},
|
||||
'./app/scripts/controllers/permissions/*.js': {
|
||||
branches: 100,
|
||||
functions: 100,
|
||||
lines: 100,
|
||||
statements: 100,
|
||||
},
|
||||
},
|
||||
// TODO: enable resetMocks
|
||||
// resetMocks: true,
|
||||
@ -19,8 +29,11 @@ module.exports = {
|
||||
testMatch: [
|
||||
'<rootDir>/ui/**/*.test.js',
|
||||
'<rootDir>/shared/**/*.test.js',
|
||||
'<rootDir>/app/scripts/lib/**/*.test.js',
|
||||
'<rootDir>/app/scripts/migrations/*.test.js',
|
||||
'<rootDir>/app/scripts/platforms/*.test.js',
|
||||
'<rootDir>app/scripts/controllers/network/**/*.test.js',
|
||||
'<rootDir>/app/scripts/controllers/permissions/*.test.js',
|
||||
],
|
||||
testTimeout: 2500,
|
||||
transform: {
|
||||
|
@ -548,7 +548,6 @@
|
||||
"@metamask/contract-metadata": true,
|
||||
"abort-controller": true,
|
||||
"async-mutex": true,
|
||||
"await-semaphore": true,
|
||||
"buffer": true,
|
||||
"eth-ens-namehash": true,
|
||||
"eth-json-rpc-infura": true,
|
||||
@ -558,11 +557,9 @@
|
||||
"eth-query": true,
|
||||
"eth-rpc-errors": true,
|
||||
"eth-sig-util": true,
|
||||
"ethereumjs-tx": true,
|
||||
"ethereumjs-util": true,
|
||||
"ethereumjs-wallet": true,
|
||||
"ethers": true,
|
||||
"ethjs-query": true,
|
||||
"ethjs-unit": true,
|
||||
"ethjs-util": true,
|
||||
"events": true,
|
||||
@ -628,6 +625,16 @@
|
||||
"mersenne-twister": true
|
||||
}
|
||||
},
|
||||
"@metamask/key-tree": {
|
||||
"packages": {
|
||||
"bip39": true,
|
||||
"buffer": true,
|
||||
"crypto-browserify": true,
|
||||
"is-buffer": true,
|
||||
"keccak": true,
|
||||
"secp256k1": true
|
||||
}
|
||||
},
|
||||
"@metamask/logo": {
|
||||
"globals": {
|
||||
"addEventListener": true,
|
||||
@ -642,6 +649,16 @@
|
||||
"gl-vec3": true
|
||||
}
|
||||
},
|
||||
"@metamask/object-multiplex": {
|
||||
"globals": {
|
||||
"console.warn": true
|
||||
},
|
||||
"packages": {
|
||||
"end-of-stream": true,
|
||||
"once": true,
|
||||
"readable-stream": true
|
||||
}
|
||||
},
|
||||
"@metamask/obs-store": {
|
||||
"globals": {
|
||||
"localStorage": true
|
||||
@ -652,6 +669,25 @@
|
||||
"through2": true
|
||||
}
|
||||
},
|
||||
"@metamask/post-message-stream": {
|
||||
"globals": {
|
||||
"addEventListener": true,
|
||||
"location.origin": true,
|
||||
"onmessage": "write",
|
||||
"postMessage": true,
|
||||
"removeEventListener": true
|
||||
},
|
||||
"packages": {
|
||||
"readable-stream": true
|
||||
}
|
||||
},
|
||||
"@metamask/rpc-methods": {
|
||||
"packages": {
|
||||
"@metamask/key-tree": true,
|
||||
"@metamask/snap-controllers": true,
|
||||
"eth-rpc-errors": true
|
||||
}
|
||||
},
|
||||
"@metamask/safe-event-emitter": {
|
||||
"globals": {
|
||||
"setTimeout": true
|
||||
@ -660,6 +696,33 @@
|
||||
"events": true
|
||||
}
|
||||
},
|
||||
"@metamask/snap-controllers": {
|
||||
"globals": {
|
||||
"Worker": true,
|
||||
"clearTimeout": true,
|
||||
"console.error": true,
|
||||
"console.log": true,
|
||||
"console.warn": true,
|
||||
"fetch": true,
|
||||
"setTimeout": true
|
||||
},
|
||||
"packages": {
|
||||
"@metamask/controllers": true,
|
||||
"@metamask/object-multiplex": true,
|
||||
"@metamask/obs-store": true,
|
||||
"@metamask/post-message-stream": true,
|
||||
"@metamask/safe-event-emitter": true,
|
||||
"@metamask/snap-workers": true,
|
||||
"deep-freeze-strict": true,
|
||||
"eth-rpc-errors": true,
|
||||
"fast-deep-equal": true,
|
||||
"immer": true,
|
||||
"json-rpc-engine": true,
|
||||
"json-rpc-middleware-stream": true,
|
||||
"nanoid": true,
|
||||
"pump": true
|
||||
}
|
||||
},
|
||||
"@ngraveio/bc-ur": {
|
||||
"packages": {
|
||||
"@apocentre/alias-sampling": true,
|
||||
@ -1069,6 +1132,7 @@
|
||||
},
|
||||
"bip39": {
|
||||
"packages": {
|
||||
"buffer": true,
|
||||
"create-hash": true,
|
||||
"pbkdf2": true,
|
||||
"randombytes": true,
|
||||
@ -1902,13 +1966,6 @@
|
||||
"ethereumjs-util": true
|
||||
}
|
||||
},
|
||||
"ethereumjs-tx": {
|
||||
"packages": {
|
||||
"buffer": true,
|
||||
"ethereum-common": true,
|
||||
"ethereumjs-util": true
|
||||
}
|
||||
},
|
||||
"ethereumjs-util": {
|
||||
"packages": {
|
||||
"assert": true,
|
||||
@ -2183,11 +2240,6 @@
|
||||
"has-symbols": true
|
||||
}
|
||||
},
|
||||
"get-params": {
|
||||
"globals": {
|
||||
"GetParams": "write"
|
||||
}
|
||||
},
|
||||
"graphql-request": {
|
||||
"globals": {
|
||||
"fetch": true
|
||||
@ -2826,11 +2878,6 @@
|
||||
"process": true
|
||||
}
|
||||
},
|
||||
"jsan": {
|
||||
"globals": {
|
||||
"console.warn": true
|
||||
}
|
||||
},
|
||||
"jsbi": {
|
||||
"globals": {
|
||||
"define": true
|
||||
@ -2849,7 +2896,11 @@
|
||||
}
|
||||
},
|
||||
"json-rpc-middleware-stream": {
|
||||
"globals": {
|
||||
"setTimeout": true
|
||||
},
|
||||
"packages": {
|
||||
"@metamask/safe-event-emitter": true,
|
||||
"readable-stream": true
|
||||
}
|
||||
},
|
||||
@ -3562,10 +3613,11 @@
|
||||
"crypto": true,
|
||||
"msCrypto": true,
|
||||
"navigator": true
|
||||
},
|
||||
}
|
||||
},
|
||||
"no-case": {
|
||||
"packages": {
|
||||
"buffer": true,
|
||||
"crypto-browserify": true
|
||||
"lower-case": true
|
||||
}
|
||||
},
|
||||
"no-case": {
|
||||
@ -4415,35 +4467,6 @@
|
||||
"@babel/runtime": true
|
||||
}
|
||||
},
|
||||
"redux-devtools-core": {
|
||||
"globals": {
|
||||
"ErrorUtils": true,
|
||||
"console": true,
|
||||
"devToolsOptions": true,
|
||||
"onerror": "write",
|
||||
"serializeState": true
|
||||
},
|
||||
"packages": {
|
||||
"get-params": true,
|
||||
"jsan": true,
|
||||
"lodash": true,
|
||||
"nanoid": true,
|
||||
"remotedev-serialize": true
|
||||
}
|
||||
},
|
||||
"redux-devtools-instrument": {
|
||||
"globals": {
|
||||
"chrome": true,
|
||||
"console.error": true,
|
||||
"process": true,
|
||||
"setTimeout": true
|
||||
},
|
||||
"packages": {
|
||||
"lodash": true,
|
||||
"process": true,
|
||||
"symbol-observable": true
|
||||
}
|
||||
},
|
||||
"regenerator-runtime": {
|
||||
"globals": {
|
||||
"regeneratorRuntime": "write"
|
||||
@ -4460,21 +4483,6 @@
|
||||
"url": true
|
||||
}
|
||||
},
|
||||
"remote-redux-devtools": {
|
||||
"globals": {
|
||||
"console.log": true,
|
||||
"console.warn": true,
|
||||
"fetch": true,
|
||||
"setTimeout": true
|
||||
},
|
||||
"packages": {
|
||||
"jsan": true,
|
||||
"redux-devtools-core": true,
|
||||
"redux-devtools-instrument": true,
|
||||
"rn-host-detect": true,
|
||||
"socketcluster-client": true
|
||||
}
|
||||
},
|
||||
"retimer": {
|
||||
"globals": {
|
||||
"clearTimeout": true,
|
||||
@ -4500,22 +4508,6 @@
|
||||
"buffer": true
|
||||
}
|
||||
},
|
||||
"rn-host-detect": {
|
||||
"globals": {
|
||||
"__DEV__": true,
|
||||
"__fbBatchedBridgeConfig": true,
|
||||
"console": true
|
||||
}
|
||||
},
|
||||
"rpc-cap": {
|
||||
"packages": {
|
||||
"@metamask/controllers": true,
|
||||
"eth-rpc-errors": true,
|
||||
"is-subset": true,
|
||||
"json-rpc-engine": true,
|
||||
"uuid": true
|
||||
}
|
||||
},
|
||||
"safe-buffer": {
|
||||
"packages": {
|
||||
"buffer": true
|
||||
@ -4535,16 +4527,6 @@
|
||||
"truncate-utf8-bytes": true
|
||||
}
|
||||
},
|
||||
"sc-channel": {
|
||||
"packages": {
|
||||
"component-emitter": true
|
||||
}
|
||||
},
|
||||
"sc-formatter": {
|
||||
"globals": {
|
||||
"Buffer": true
|
||||
}
|
||||
},
|
||||
"scheduler": {
|
||||
"globals": {
|
||||
"MessageChannel": true,
|
||||
@ -4701,29 +4683,6 @@
|
||||
"uuid": true
|
||||
}
|
||||
},
|
||||
"socketcluster-client": {
|
||||
"globals": {
|
||||
"WebSocket": true,
|
||||
"WorkerGlobalScope": true,
|
||||
"addEventListener": true,
|
||||
"clearTimeout": true,
|
||||
"localStorage": true,
|
||||
"location": true,
|
||||
"removeEventListener": true,
|
||||
"setTimeout": true
|
||||
},
|
||||
"packages": {
|
||||
"buffer": true,
|
||||
"clone": true,
|
||||
"component-emitter": true,
|
||||
"linked-list": true,
|
||||
"querystring-es3": true,
|
||||
"sc-channel": true,
|
||||
"sc-errors": true,
|
||||
"sc-formatter": true,
|
||||
"uuid": true
|
||||
}
|
||||
},
|
||||
"sort-keys": {
|
||||
"packages": {
|
||||
"is-plain-obj": true
|
||||
|
@ -548,7 +548,6 @@
|
||||
"@metamask/contract-metadata": true,
|
||||
"abort-controller": true,
|
||||
"async-mutex": true,
|
||||
"await-semaphore": true,
|
||||
"buffer": true,
|
||||
"eth-ens-namehash": true,
|
||||
"eth-json-rpc-infura": true,
|
||||
@ -558,11 +557,9 @@
|
||||
"eth-query": true,
|
||||
"eth-rpc-errors": true,
|
||||
"eth-sig-util": true,
|
||||
"ethereumjs-tx": true,
|
||||
"ethereumjs-util": true,
|
||||
"ethereumjs-wallet": true,
|
||||
"ethers": true,
|
||||
"ethjs-query": true,
|
||||
"ethjs-unit": true,
|
||||
"ethjs-util": true,
|
||||
"events": true,
|
||||
@ -628,6 +625,16 @@
|
||||
"mersenne-twister": true
|
||||
}
|
||||
},
|
||||
"@metamask/key-tree": {
|
||||
"packages": {
|
||||
"bip39": true,
|
||||
"buffer": true,
|
||||
"crypto-browserify": true,
|
||||
"is-buffer": true,
|
||||
"keccak": true,
|
||||
"secp256k1": true
|
||||
}
|
||||
},
|
||||
"@metamask/logo": {
|
||||
"globals": {
|
||||
"addEventListener": true,
|
||||
@ -642,6 +649,16 @@
|
||||
"gl-vec3": true
|
||||
}
|
||||
},
|
||||
"@metamask/object-multiplex": {
|
||||
"globals": {
|
||||
"console.warn": true
|
||||
},
|
||||
"packages": {
|
||||
"end-of-stream": true,
|
||||
"once": true,
|
||||
"readable-stream": true
|
||||
}
|
||||
},
|
||||
"@metamask/obs-store": {
|
||||
"globals": {
|
||||
"localStorage": true
|
||||
@ -652,6 +669,25 @@
|
||||
"through2": true
|
||||
}
|
||||
},
|
||||
"@metamask/post-message-stream": {
|
||||
"globals": {
|
||||
"addEventListener": true,
|
||||
"location.origin": true,
|
||||
"onmessage": "write",
|
||||
"postMessage": true,
|
||||
"removeEventListener": true
|
||||
},
|
||||
"packages": {
|
||||
"readable-stream": true
|
||||
}
|
||||
},
|
||||
"@metamask/rpc-methods": {
|
||||
"packages": {
|
||||
"@metamask/key-tree": true,
|
||||
"@metamask/snap-controllers": true,
|
||||
"eth-rpc-errors": true
|
||||
}
|
||||
},
|
||||
"@metamask/safe-event-emitter": {
|
||||
"globals": {
|
||||
"setTimeout": true
|
||||
@ -660,6 +696,33 @@
|
||||
"events": true
|
||||
}
|
||||
},
|
||||
"@metamask/snap-controllers": {
|
||||
"globals": {
|
||||
"Worker": true,
|
||||
"clearTimeout": true,
|
||||
"console.error": true,
|
||||
"console.log": true,
|
||||
"console.warn": true,
|
||||
"fetch": true,
|
||||
"setTimeout": true
|
||||
},
|
||||
"packages": {
|
||||
"@metamask/controllers": true,
|
||||
"@metamask/object-multiplex": true,
|
||||
"@metamask/obs-store": true,
|
||||
"@metamask/post-message-stream": true,
|
||||
"@metamask/safe-event-emitter": true,
|
||||
"@metamask/snap-workers": true,
|
||||
"deep-freeze-strict": true,
|
||||
"eth-rpc-errors": true,
|
||||
"fast-deep-equal": true,
|
||||
"immer": true,
|
||||
"json-rpc-engine": true,
|
||||
"json-rpc-middleware-stream": true,
|
||||
"nanoid": true,
|
||||
"pump": true
|
||||
}
|
||||
},
|
||||
"@ngraveio/bc-ur": {
|
||||
"packages": {
|
||||
"@apocentre/alias-sampling": true,
|
||||
@ -1069,6 +1132,7 @@
|
||||
},
|
||||
"bip39": {
|
||||
"packages": {
|
||||
"buffer": true,
|
||||
"create-hash": true,
|
||||
"pbkdf2": true,
|
||||
"randombytes": true,
|
||||
@ -1902,13 +1966,6 @@
|
||||
"ethereumjs-util": true
|
||||
}
|
||||
},
|
||||
"ethereumjs-tx": {
|
||||
"packages": {
|
||||
"buffer": true,
|
||||
"ethereum-common": true,
|
||||
"ethereumjs-util": true
|
||||
}
|
||||
},
|
||||
"ethereumjs-util": {
|
||||
"packages": {
|
||||
"assert": true,
|
||||
@ -2183,11 +2240,6 @@
|
||||
"has-symbols": true
|
||||
}
|
||||
},
|
||||
"get-params": {
|
||||
"globals": {
|
||||
"GetParams": "write"
|
||||
}
|
||||
},
|
||||
"graphql-request": {
|
||||
"globals": {
|
||||
"fetch": true
|
||||
@ -2826,11 +2878,6 @@
|
||||
"process": true
|
||||
}
|
||||
},
|
||||
"jsan": {
|
||||
"globals": {
|
||||
"console.warn": true
|
||||
}
|
||||
},
|
||||
"jsbi": {
|
||||
"globals": {
|
||||
"define": true
|
||||
@ -2849,7 +2896,11 @@
|
||||
}
|
||||
},
|
||||
"json-rpc-middleware-stream": {
|
||||
"globals": {
|
||||
"setTimeout": true
|
||||
},
|
||||
"packages": {
|
||||
"@metamask/safe-event-emitter": true,
|
||||
"readable-stream": true
|
||||
}
|
||||
},
|
||||
@ -3562,10 +3613,11 @@
|
||||
"crypto": true,
|
||||
"msCrypto": true,
|
||||
"navigator": true
|
||||
},
|
||||
}
|
||||
},
|
||||
"no-case": {
|
||||
"packages": {
|
||||
"buffer": true,
|
||||
"crypto-browserify": true
|
||||
"lower-case": true
|
||||
}
|
||||
},
|
||||
"no-case": {
|
||||
@ -4415,35 +4467,6 @@
|
||||
"@babel/runtime": true
|
||||
}
|
||||
},
|
||||
"redux-devtools-core": {
|
||||
"globals": {
|
||||
"ErrorUtils": true,
|
||||
"console": true,
|
||||
"devToolsOptions": true,
|
||||
"onerror": "write",
|
||||
"serializeState": true
|
||||
},
|
||||
"packages": {
|
||||
"get-params": true,
|
||||
"jsan": true,
|
||||
"lodash": true,
|
||||
"nanoid": true,
|
||||
"remotedev-serialize": true
|
||||
}
|
||||
},
|
||||
"redux-devtools-instrument": {
|
||||
"globals": {
|
||||
"chrome": true,
|
||||
"console.error": true,
|
||||
"process": true,
|
||||
"setTimeout": true
|
||||
},
|
||||
"packages": {
|
||||
"lodash": true,
|
||||
"process": true,
|
||||
"symbol-observable": true
|
||||
}
|
||||
},
|
||||
"regenerator-runtime": {
|
||||
"globals": {
|
||||
"regeneratorRuntime": "write"
|
||||
@ -4460,21 +4483,6 @@
|
||||
"url": true
|
||||
}
|
||||
},
|
||||
"remote-redux-devtools": {
|
||||
"globals": {
|
||||
"console.log": true,
|
||||
"console.warn": true,
|
||||
"fetch": true,
|
||||
"setTimeout": true
|
||||
},
|
||||
"packages": {
|
||||
"jsan": true,
|
||||
"redux-devtools-core": true,
|
||||
"redux-devtools-instrument": true,
|
||||
"rn-host-detect": true,
|
||||
"socketcluster-client": true
|
||||
}
|
||||
},
|
||||
"retimer": {
|
||||
"globals": {
|
||||
"clearTimeout": true,
|
||||
@ -4500,22 +4508,6 @@
|
||||
"buffer": true
|
||||
}
|
||||
},
|
||||
"rn-host-detect": {
|
||||
"globals": {
|
||||
"__DEV__": true,
|
||||
"__fbBatchedBridgeConfig": true,
|
||||
"console": true
|
||||
}
|
||||
},
|
||||
"rpc-cap": {
|
||||
"packages": {
|
||||
"@metamask/controllers": true,
|
||||
"eth-rpc-errors": true,
|
||||
"is-subset": true,
|
||||
"json-rpc-engine": true,
|
||||
"uuid": true
|
||||
}
|
||||
},
|
||||
"safe-buffer": {
|
||||
"packages": {
|
||||
"buffer": true
|
||||
@ -4535,16 +4527,6 @@
|
||||
"truncate-utf8-bytes": true
|
||||
}
|
||||
},
|
||||
"sc-channel": {
|
||||
"packages": {
|
||||
"component-emitter": true
|
||||
}
|
||||
},
|
||||
"sc-formatter": {
|
||||
"globals": {
|
||||
"Buffer": true
|
||||
}
|
||||
},
|
||||
"scheduler": {
|
||||
"globals": {
|
||||
"MessageChannel": true,
|
||||
@ -4701,29 +4683,6 @@
|
||||
"uuid": true
|
||||
}
|
||||
},
|
||||
"socketcluster-client": {
|
||||
"globals": {
|
||||
"WebSocket": true,
|
||||
"WorkerGlobalScope": true,
|
||||
"addEventListener": true,
|
||||
"clearTimeout": true,
|
||||
"localStorage": true,
|
||||
"location": true,
|
||||
"removeEventListener": true,
|
||||
"setTimeout": true
|
||||
},
|
||||
"packages": {
|
||||
"buffer": true,
|
||||
"clone": true,
|
||||
"component-emitter": true,
|
||||
"linked-list": true,
|
||||
"querystring-es3": true,
|
||||
"sc-channel": true,
|
||||
"sc-errors": true,
|
||||
"sc-formatter": true,
|
||||
"uuid": true
|
||||
}
|
||||
},
|
||||
"sort-keys": {
|
||||
"packages": {
|
||||
"is-plain-obj": true
|
||||
|
@ -548,7 +548,6 @@
|
||||
"@metamask/contract-metadata": true,
|
||||
"abort-controller": true,
|
||||
"async-mutex": true,
|
||||
"await-semaphore": true,
|
||||
"buffer": true,
|
||||
"eth-ens-namehash": true,
|
||||
"eth-json-rpc-infura": true,
|
||||
@ -558,11 +557,9 @@
|
||||
"eth-query": true,
|
||||
"eth-rpc-errors": true,
|
||||
"eth-sig-util": true,
|
||||
"ethereumjs-tx": true,
|
||||
"ethereumjs-util": true,
|
||||
"ethereumjs-wallet": true,
|
||||
"ethers": true,
|
||||
"ethjs-query": true,
|
||||
"ethjs-unit": true,
|
||||
"ethjs-util": true,
|
||||
"events": true,
|
||||
@ -628,6 +625,16 @@
|
||||
"mersenne-twister": true
|
||||
}
|
||||
},
|
||||
"@metamask/key-tree": {
|
||||
"packages": {
|
||||
"bip39": true,
|
||||
"buffer": true,
|
||||
"crypto-browserify": true,
|
||||
"is-buffer": true,
|
||||
"keccak": true,
|
||||
"secp256k1": true
|
||||
}
|
||||
},
|
||||
"@metamask/logo": {
|
||||
"globals": {
|
||||
"addEventListener": true,
|
||||
@ -642,6 +649,16 @@
|
||||
"gl-vec3": true
|
||||
}
|
||||
},
|
||||
"@metamask/object-multiplex": {
|
||||
"globals": {
|
||||
"console.warn": true
|
||||
},
|
||||
"packages": {
|
||||
"end-of-stream": true,
|
||||
"once": true,
|
||||
"readable-stream": true
|
||||
}
|
||||
},
|
||||
"@metamask/obs-store": {
|
||||
"globals": {
|
||||
"localStorage": true
|
||||
@ -652,6 +669,25 @@
|
||||
"through2": true
|
||||
}
|
||||
},
|
||||
"@metamask/post-message-stream": {
|
||||
"globals": {
|
||||
"addEventListener": true,
|
||||
"location.origin": true,
|
||||
"onmessage": "write",
|
||||
"postMessage": true,
|
||||
"removeEventListener": true
|
||||
},
|
||||
"packages": {
|
||||
"readable-stream": true
|
||||
}
|
||||
},
|
||||
"@metamask/rpc-methods": {
|
||||
"packages": {
|
||||
"@metamask/key-tree": true,
|
||||
"@metamask/snap-controllers": true,
|
||||
"eth-rpc-errors": true
|
||||
}
|
||||
},
|
||||
"@metamask/safe-event-emitter": {
|
||||
"globals": {
|
||||
"setTimeout": true
|
||||
@ -660,6 +696,33 @@
|
||||
"events": true
|
||||
}
|
||||
},
|
||||
"@metamask/snap-controllers": {
|
||||
"globals": {
|
||||
"Worker": true,
|
||||
"clearTimeout": true,
|
||||
"console.error": true,
|
||||
"console.log": true,
|
||||
"console.warn": true,
|
||||
"fetch": true,
|
||||
"setTimeout": true
|
||||
},
|
||||
"packages": {
|
||||
"@metamask/controllers": true,
|
||||
"@metamask/object-multiplex": true,
|
||||
"@metamask/obs-store": true,
|
||||
"@metamask/post-message-stream": true,
|
||||
"@metamask/safe-event-emitter": true,
|
||||
"@metamask/snap-workers": true,
|
||||
"deep-freeze-strict": true,
|
||||
"eth-rpc-errors": true,
|
||||
"fast-deep-equal": true,
|
||||
"immer": true,
|
||||
"json-rpc-engine": true,
|
||||
"json-rpc-middleware-stream": true,
|
||||
"nanoid": true,
|
||||
"pump": true
|
||||
}
|
||||
},
|
||||
"@ngraveio/bc-ur": {
|
||||
"packages": {
|
||||
"@apocentre/alias-sampling": true,
|
||||
@ -1069,6 +1132,7 @@
|
||||
},
|
||||
"bip39": {
|
||||
"packages": {
|
||||
"buffer": true,
|
||||
"create-hash": true,
|
||||
"pbkdf2": true,
|
||||
"randombytes": true,
|
||||
@ -1902,13 +1966,6 @@
|
||||
"ethereumjs-util": true
|
||||
}
|
||||
},
|
||||
"ethereumjs-tx": {
|
||||
"packages": {
|
||||
"buffer": true,
|
||||
"ethereum-common": true,
|
||||
"ethereumjs-util": true
|
||||
}
|
||||
},
|
||||
"ethereumjs-util": {
|
||||
"packages": {
|
||||
"assert": true,
|
||||
@ -2183,11 +2240,6 @@
|
||||
"has-symbols": true
|
||||
}
|
||||
},
|
||||
"get-params": {
|
||||
"globals": {
|
||||
"GetParams": "write"
|
||||
}
|
||||
},
|
||||
"graphql-request": {
|
||||
"globals": {
|
||||
"fetch": true
|
||||
@ -2826,11 +2878,6 @@
|
||||
"process": true
|
||||
}
|
||||
},
|
||||
"jsan": {
|
||||
"globals": {
|
||||
"console.warn": true
|
||||
}
|
||||
},
|
||||
"jsbi": {
|
||||
"globals": {
|
||||
"define": true
|
||||
@ -2849,7 +2896,11 @@
|
||||
}
|
||||
},
|
||||
"json-rpc-middleware-stream": {
|
||||
"globals": {
|
||||
"setTimeout": true
|
||||
},
|
||||
"packages": {
|
||||
"@metamask/safe-event-emitter": true,
|
||||
"readable-stream": true
|
||||
}
|
||||
},
|
||||
@ -3562,10 +3613,11 @@
|
||||
"crypto": true,
|
||||
"msCrypto": true,
|
||||
"navigator": true
|
||||
},
|
||||
}
|
||||
},
|
||||
"no-case": {
|
||||
"packages": {
|
||||
"buffer": true,
|
||||
"crypto-browserify": true
|
||||
"lower-case": true
|
||||
}
|
||||
},
|
||||
"no-case": {
|
||||
@ -4415,35 +4467,6 @@
|
||||
"@babel/runtime": true
|
||||
}
|
||||
},
|
||||
"redux-devtools-core": {
|
||||
"globals": {
|
||||
"ErrorUtils": true,
|
||||
"console": true,
|
||||
"devToolsOptions": true,
|
||||
"onerror": "write",
|
||||
"serializeState": true
|
||||
},
|
||||
"packages": {
|
||||
"get-params": true,
|
||||
"jsan": true,
|
||||
"lodash": true,
|
||||
"nanoid": true,
|
||||
"remotedev-serialize": true
|
||||
}
|
||||
},
|
||||
"redux-devtools-instrument": {
|
||||
"globals": {
|
||||
"chrome": true,
|
||||
"console.error": true,
|
||||
"process": true,
|
||||
"setTimeout": true
|
||||
},
|
||||
"packages": {
|
||||
"lodash": true,
|
||||
"process": true,
|
||||
"symbol-observable": true
|
||||
}
|
||||
},
|
||||
"regenerator-runtime": {
|
||||
"globals": {
|
||||
"regeneratorRuntime": "write"
|
||||
@ -4460,21 +4483,6 @@
|
||||
"url": true
|
||||
}
|
||||
},
|
||||
"remote-redux-devtools": {
|
||||
"globals": {
|
||||
"console.log": true,
|
||||
"console.warn": true,
|
||||
"fetch": true,
|
||||
"setTimeout": true
|
||||
},
|
||||
"packages": {
|
||||
"jsan": true,
|
||||
"redux-devtools-core": true,
|
||||
"redux-devtools-instrument": true,
|
||||
"rn-host-detect": true,
|
||||
"socketcluster-client": true
|
||||
}
|
||||
},
|
||||
"retimer": {
|
||||
"globals": {
|
||||
"clearTimeout": true,
|
||||
@ -4500,22 +4508,6 @@
|
||||
"buffer": true
|
||||
}
|
||||
},
|
||||
"rn-host-detect": {
|
||||
"globals": {
|
||||
"__DEV__": true,
|
||||
"__fbBatchedBridgeConfig": true,
|
||||
"console": true
|
||||
}
|
||||
},
|
||||
"rpc-cap": {
|
||||
"packages": {
|
||||
"@metamask/controllers": true,
|
||||
"eth-rpc-errors": true,
|
||||
"is-subset": true,
|
||||
"json-rpc-engine": true,
|
||||
"uuid": true
|
||||
}
|
||||
},
|
||||
"safe-buffer": {
|
||||
"packages": {
|
||||
"buffer": true
|
||||
@ -4535,16 +4527,6 @@
|
||||
"truncate-utf8-bytes": true
|
||||
}
|
||||
},
|
||||
"sc-channel": {
|
||||
"packages": {
|
||||
"component-emitter": true
|
||||
}
|
||||
},
|
||||
"sc-formatter": {
|
||||
"globals": {
|
||||
"Buffer": true
|
||||
}
|
||||
},
|
||||
"scheduler": {
|
||||
"globals": {
|
||||
"MessageChannel": true,
|
||||
@ -4701,29 +4683,6 @@
|
||||
"uuid": true
|
||||
}
|
||||
},
|
||||
"socketcluster-client": {
|
||||
"globals": {
|
||||
"WebSocket": true,
|
||||
"WorkerGlobalScope": true,
|
||||
"addEventListener": true,
|
||||
"clearTimeout": true,
|
||||
"localStorage": true,
|
||||
"location": true,
|
||||
"removeEventListener": true,
|
||||
"setTimeout": true
|
||||
},
|
||||
"packages": {
|
||||
"buffer": true,
|
||||
"clone": true,
|
||||
"component-emitter": true,
|
||||
"linked-list": true,
|
||||
"querystring-es3": true,
|
||||
"sc-channel": true,
|
||||
"sc-errors": true,
|
||||
"sc-formatter": true,
|
||||
"uuid": true
|
||||
}
|
||||
},
|
||||
"sort-keys": {
|
||||
"packages": {
|
||||
"is-plain-obj": true
|
||||
|
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue
Block a user