Merge pull request #10397 from MetaMask/Version-v9.0.5

Version v9.0.5 RC
feature/default_network_editable
Mark Stacey 4 years ago committed by GitHub
commit 4b75135864
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
  1. 27
      .circleci/config.yml
  2. 19
      .circleci/scripts/chrome-install.sh
  3. 13
      .circleci/scripts/create-sesify-viz
  4. 24
      .circleci/scripts/deps-install.sh
  5. 14
      .eslintrc.js
  6. 2
      .gitignore
  7. 2
      .nvmrc
  8. 1
      .prettierrc.yml
  9. 57
      .storybook/i18n.js
  10. 58
      .storybook/locales.js
  11. 1
      .storybook/main.js
  12. 40
      .storybook/preview.js
  13. 1
      .yarnrc
  14. 15
      CHANGELOG.md
  15. 4
      README.md
  16. 3
      app/_locales/am/messages.json
  17. 3
      app/_locales/ar/messages.json
  18. 3
      app/_locales/bg/messages.json
  19. 3
      app/_locales/bn/messages.json
  20. 3
      app/_locales/ca/messages.json
  21. 3
      app/_locales/cs/messages.json
  22. 3
      app/_locales/da/messages.json
  23. 3
      app/_locales/de/messages.json
  24. 3
      app/_locales/el/messages.json
  25. 31
      app/_locales/en/messages.json
  26. 3
      app/_locales/es/messages.json
  27. 3
      app/_locales/es_419/messages.json
  28. 3
      app/_locales/et/messages.json
  29. 3
      app/_locales/fa/messages.json
  30. 3
      app/_locales/fi/messages.json
  31. 3
      app/_locales/fil/messages.json
  32. 3
      app/_locales/fr/messages.json
  33. 3
      app/_locales/he/messages.json
  34. 6
      app/_locales/hi/messages.json
  35. 3
      app/_locales/hn/messages.json
  36. 3
      app/_locales/hr/messages.json
  37. 3
      app/_locales/ht/messages.json
  38. 3
      app/_locales/hu/messages.json
  39. 1381
      app/_locales/id/messages.json
  40. 3
      app/_locales/it/messages.json
  41. 3
      app/_locales/ja/messages.json
  42. 3
      app/_locales/kn/messages.json
  43. 1445
      app/_locales/ko/messages.json
  44. 3
      app/_locales/lt/messages.json
  45. 3
      app/_locales/lv/messages.json
  46. 3
      app/_locales/ms/messages.json
  47. 3
      app/_locales/nl/messages.json
  48. 3
      app/_locales/no/messages.json
  49. 3
      app/_locales/ph/messages.json
  50. 3
      app/_locales/pl/messages.json
  51. 3
      app/_locales/pt/messages.json
  52. 3
      app/_locales/pt_BR/messages.json
  53. 3
      app/_locales/ro/messages.json
  54. 3
      app/_locales/ru/messages.json
  55. 3
      app/_locales/sk/messages.json
  56. 3
      app/_locales/sl/messages.json
  57. 3
      app/_locales/sr/messages.json
  58. 3
      app/_locales/sv/messages.json
  59. 3
      app/_locales/sw/messages.json
  60. 3
      app/_locales/ta/messages.json
  61. 3
      app/_locales/th/messages.json
  62. 3
      app/_locales/tr/messages.json
  63. 3
      app/_locales/uk/messages.json
  64. 3
      app/_locales/vi/messages.json
  65. 3
      app/_locales/zh_CN/messages.json
  66. 3
      app/_locales/zh_TW/messages.json
  67. 2
      app/manifest/_base.json
  68. 48
      app/scripts/account-import-strategies/index.js
  69. 280
      app/scripts/background.js
  70. 8
      app/scripts/constants/contracts.js
  71. 176
      app/scripts/contentscript.js
  72. 56
      app/scripts/controllers/alert.js
  73. 68
      app/scripts/controllers/app-state.js
  74. 40
      app/scripts/controllers/cached-balances.js
  75. 112
      app/scripts/controllers/detect-tokens.js
  76. 12
      app/scripts/controllers/ens/ens.js
  77. 68
      app/scripts/controllers/ens/index.js
  78. 176
      app/scripts/controllers/incoming-transactions.js
  79. 128
      app/scripts/controllers/metametrics.js
  80. 36
      app/scripts/controllers/network/createInfuraClient.js
  81. 48
      app/scripts/controllers/network/createJsonRpcClient.js
  82. 10
      app/scripts/controllers/network/createMetamaskMiddleware.js
  83. 2
      app/scripts/controllers/network/index.js
  84. 38
      app/scripts/controllers/network/middleware/pending.js
  85. 198
      app/scripts/controllers/network/network.js
  86. 6
      app/scripts/controllers/network/util.js
  87. 36
      app/scripts/controllers/onboarding.js
  88. 24
      app/scripts/controllers/permissions/enums.js
  89. 296
      app/scripts/controllers/permissions/index.js
  90. 130
      app/scripts/controllers/permissions/permissionsLog.js
  91. 62
      app/scripts/controllers/permissions/permissionsMethodMiddleware.js
  92. 26
      app/scripts/controllers/permissions/restrictedMethods.js
  93. 420
      app/scripts/controllers/preferences.js
  94. 475
      app/scripts/controllers/swaps.js
  95. 199
      app/scripts/controllers/threebox.js
  96. 70
      app/scripts/controllers/token-rates.js
  97. 554
      app/scripts/controllers/transactions/index.js
  98. 28
      app/scripts/controllers/transactions/lib/tx-state-history-helpers.js
  99. 44
      app/scripts/controllers/transactions/lib/util.js
  100. 132
      app/scripts/controllers/transactions/pending-tx-tracker.js
  101. Some files were not shown because too many files have changed in this diff Show More

@ -3,10 +3,10 @@ version: 2.1
executors:
node-browsers:
docker:
- image: circleci/node@sha256:e16740707de2ebed45c05d507f33ef204902349c7356d720610b5ec6a35d3d88
- image: circleci/node:14-browsers
node-browsers-medium-plus:
docker:
- image: circleci/node@sha256:e16740707de2ebed45c05d507f33ef204902349c7356d720610b5ec6a35d3d88
- image: circleci/node:14-browsers
resource_class: medium+
environment:
NODE_OPTIONS: --max_old_space_size=2048
@ -90,6 +90,7 @@ workflows:
requires:
- prep-deps
- prep-build
- prep-build-storybook
- benchmark
- all-tests-pass
- job-publish-release:
@ -214,7 +215,7 @@ jobs:
- persist_to_workspace:
root: .
paths:
- .out
- storybook-build
test-lint:
executor: node-browsers
@ -262,6 +263,9 @@ jobs:
executor: node-browsers
steps:
- checkout
- run:
name: Re-Install Chrome
command: ./.circleci/scripts/chrome-install.sh
- attach_workspace:
at: .
- run:
@ -286,6 +290,9 @@ jobs:
executor: node-browsers
steps:
- checkout
- run:
name: Re-Install Chrome
command: ./.circleci/scripts/chrome-install.sh
- attach_workspace:
at: .
- run:
@ -361,9 +368,12 @@ jobs:
destination: test-artifacts
benchmark:
executor: node-browsers
executor: node-browsers-medium-plus
steps:
- checkout
- run:
name: Re-Install Chrome
command: ./.circleci/scripts/chrome-install.sh
- attach_workspace:
at: .
- run:
@ -404,15 +414,12 @@ jobs:
- store_artifacts:
path: test-artifacts
destination: test-artifacts
# important: generate sesify viz AFTER uploading builds as artifacts
# Temporarily disabled until we can update to a version of `sesify` with
# this fix included: https://github.com/LavaMoat/LavaMoat/pull/121
# - run:
# name: build:sesify-viz
# command: ./.circleci/scripts/create-sesify-viz
- store_artifacts:
path: build-artifacts
destination: build-artifacts
- store_artifacts:
path: storybook-build
destination: storybook
- run:
name: build:announce
command: ./development/metamaskbot-build-announce.js

@ -0,0 +1,19 @@
#!/usr/bin/env bash
set -e
set -u
set -o pipefail
CHROME_VERSION='79.0.3945.117-1'
CHROME_BINARY="google-chrome-stable_${CHROME_VERSION}_amd64.deb"
CHROME_BINARY_URL="http://dl.google.com/linux/chrome/deb/pool/main/g/google-chrome-stable/${CHROME_BINARY}"
wget -O "${CHROME_BINARY}" -t 5 "${CHROME_BINARY_URL}"
(sudo dpkg -i "${CHROME_BINARY}" || sudo apt-get -fy install)
rm -rf "${CHROME_BINARY}"
sudo sed -i 's|HERE/chrome"|HERE/chrome" --disable-setuid-sandbox --no-sandbox|g' "/opt/google/chrome/google-chrome"
printf '%s\n' "CHROME ${CHROME_VERSION} configured"

@ -1,13 +0,0 @@
#!/usr/bin/env bash
set -x
set -e
set -u
set -o pipefail
# prepare artifacts dir
mkdir -p ./build-artifacts/deps-viz/
# generate viz
SESIFY_AUTOGEN=1 yarn build scripts:core:prod:background
npx sesify-viz --deps sesify/deps-background.json --config sesify/background.json --dest ./build-artifacts/deps-viz/background

@ -5,7 +5,7 @@ set -x
# Exit immediately if a command exits with a non-zero status.
set -e
yarn --frozen-lockfile --ignore-scripts --har
yarn --frozen-lockfile --har
# Move HAR file into directory with consistent name so that we can cache it
mkdir -p build-artifacts/yarn-install-har
@ -15,23 +15,5 @@ then
mv ./*.har build-artifacts/yarn-install-har/
fi
# run each in subshell so directory change does not persist
# scripts can be any of:
# preinstall
# install
# postinstall
# for build
(cd node_modules/node-sass && yarn run postinstall)
(cd node_modules/optipng-bin && yarn run postinstall)
(cd node_modules/gifsicle && yarn run postinstall)
(cd node_modules/jpegtran-bin && yarn run postinstall)
# for test
(cd node_modules/scrypt && yarn run install)
(cd node_modules/weak && yarn run install)
(cd node_modules/chromedriver && yarn run install)
(cd node_modules/geckodriver && yarn run postinstall)
# for release
(cd node_modules/@sentry/cli && yarn run install)
# use @lavamoat/allow-scripts instead of manually running install scripts so directory change does not persist
yarn allow-scripts

@ -53,13 +53,6 @@ module.exports = {
'prettier/prettier': 'error',
// Our usage of spaces before *named* function parens is unusual, and
// doesn't match the prettier spec. prettier does not offer an option
// to configure this
'space-before-function-paren': [
'error',
{ anonymous: 'always', named: 'never' },
],
// Our eslint config has the default setting for this as error. This
// include beforeBlockComment: true, but in order to match the prettier
// spec you have to enable before and after blocks, objects and arrays
@ -147,7 +140,10 @@ module.exports = {
'no-invalid-this': 'off',
'@babel/no-invalid-this': 'error',
'@babel/semi': ['error', 'never'],
// prettier handles these
semi: 'off',
'@babel/semi': 'off',
'mocha/no-setup-in-describe': 'off',
'node/no-process-env': 'off',
@ -213,4 +209,4 @@ module.exports = {
version: 'detect',
},
},
}
};

2
.gitignore vendored

@ -26,7 +26,7 @@ temp
.DS_Store
app/.DS_Store
.out/
storybook-build/
coverage/
dist
builds/

@ -1 +1 @@
v10.18.1
v14

@ -1,3 +1,2 @@
singleQuote: true
semi: false
trailingComma: all

@ -0,0 +1,57 @@
import React, { Component, createContext, useMemo } from 'react';
import PropTypes from 'prop-types';
import { getMessage } from '../ui/app/helpers/utils/i18n-helper';
import { I18nContext } from '../ui/app/contexts/i18n';
export { I18nContext }
export const I18nProvider = (props) => {
const { currentLocale, current, en } = props
const t = useMemo(() => {
return (key, ...args) =>
getMessage(currentLocale, current, key, ...args) ||
getMessage(currentLocale, en, key, ...args);
}, [currentLocale, current, en]);
return (
<I18nContext.Provider value={t}>{props.children}</I18nContext.Provider>
);
};
I18nProvider.propTypes = {
currentLocale: PropTypes.string,
current: PropTypes.object,
en: PropTypes.object,
children: PropTypes.node,
};
I18nProvider.defaultProps = {
children: undefined,
};
export class LegacyI18nProvider extends Component {
static propTypes = {
children: PropTypes.node,
};
static defaultProps = {
children: undefined,
};
static contextType = I18nContext;
static childContextTypes = {
t: PropTypes.func,
};
getChildContext() {
return {
t: this.context,
};
}
render() {
return this.props.children;
}
}

@ -0,0 +1,58 @@
import * as am from '../app/_locales/am/messages.json';
import * as ar from '../app/_locales/ar/messages.json';
import * as bg from '../app/_locales/bg/messages.json';
import * as bn from '../app/_locales/bn/messages.json';
import * as ca from '../app/_locales/ca/messages.json';
import * as cs from '../app/_locales/cs/messages.json';
import * as da from '../app/_locales/da/messages.json';
import * as de from '../app/_locales/de/messages.json';
import * as el from '../app/_locales/el/messages.json';
import * as en from '../app/_locales/en/messages.json';
import * as es from '../app/_locales/es/messages.json';
import * as es_419 from '../app/_locales/es_419/messages.json';
import * as et from '../app/_locales/et/messages.json';
import * as fa from '../app/_locales/fa/messages.json';
import * as fi from '../app/_locales/fi/messages.json';
import * as fil from '../app/_locales/fil/messages.json';
import * as fr from '../app/_locales/fr/messages.json';
import * as gu from '../app/_locales/gu/messages.json';
import * as he from '../app/_locales/he/messages.json';
import * as hi from '../app/_locales/hi/messages.json';
import * as hn from '../app/_locales/hn/messages.json';
import * as hr from '../app/_locales/hr/messages.json';
import * as ht from '../app/_locales/ht/messages.json';
import * as hu from '../app/_locales/hu/messages.json';
import * as id from '../app/_locales/id/messages.json';
import * as it from '../app/_locales/it/messages.json';
import * as ja from '../app/_locales/ja/messages.json';
import * as kn from '../app/_locales/kn/messages.json';
import * as ko from '../app/_locales/ko/messages.json';
import * as lt from '../app/_locales/lt/messages.json';
import * as lv from '../app/_locales/lv/messages.json';
import * as ml from '../app/_locales/ml/messages.json';
import * as mr from '../app/_locales/mr/messages.json';
import * as ms from '../app/_locales/ms/messages.json';
import * as nl from '../app/_locales/nl/messages.json';
import * as no from '../app/_locales/no/messages.json';
import * as ph from '../app/_locales/ph/messages.json';
import * as pl from '../app/_locales/pl/messages.json';
import * as pt from '../app/_locales/pt/messages.json';
import * as pt_BR from '../app/_locales/pt_BR/messages.json';
import * as pt_PT from '../app/_locales/pt_PT/messages.json';
import * as ro from '../app/_locales/ro/messages.json';
import * as ru from '../app/_locales/ru/messages.json';
import * as sk from '../app/_locales/sk/messages.json';
import * as sl from '../app/_locales/sl/messages.json';
import * as sr from '../app/_locales/sr/messages.json';
import * as sv from '../app/_locales/sv/messages.json';
import * as sw from '../app/_locales/sw/messages.json';
import * as ta from '../app/_locales/ta/messages.json';
import * as te from '../app/_locales/te/messages.json';
import * as th from '../app/_locales/th/messages.json';
import * as tr from '../app/_locales/tr/messages.json';
import * as uk from '../app/_locales/uk/messages.json';
import * as vi from '../app/_locales/vi/messages.json';
import * as zh_CN from '../app/_locales/zh_CN/messages.json';
import * as zh_TW from '../app/_locales/zh_TW/messages.json';
export { am, ar, bg, bn, ca, cs, da, de, el, en, es, es_419, et, fa, fi, fil, fr, gu, he, hi, hn, hr, ht, hu, id, it, ja, kn, ko, lt, lv, ml, mr, ms, nl, no, ph, pl, pt, pt_BR, pt_PT, ro, ru, sk, sl, sr, sv, sw, ta, te, th, tr, uk, vi, zh_CN, zh_TW };

@ -8,6 +8,7 @@ module.exports = {
'@storybook/addon-knobs',
'@storybook/addon-actions',
'@storybook/addon-backgrounds',
'@storybook/addon-toolbars',
],
webpackFinal: async (config) => {
config.module.strictExportPresence = true

@ -1,11 +1,12 @@
import React from 'react'
import { addDecorator, addParameters } from '@storybook/react'
import { withKnobs } from '@storybook/addon-knobs'
import { I18nProvider, LegacyI18nProvider } from '../ui/app/contexts/i18n'
import { Provider } from 'react-redux'
import configureStore from '../ui/app/store/store'
import '../ui/app/css/index.scss'
import en from '../app/_locales/en/messages'
import localeList from '../app/_locales/index.json'
import * as allLocales from './locales'
import { I18nProvider, LegacyI18nProvider } from './i18n'
addParameters({
backgrounds: {
@ -17,6 +18,20 @@ addParameters({
}
})
export const globalTypes = {
locale: {
name: 'Locale',
description: 'internationalization locale',
defaultValue: 'en',
toolbar: {
icon: 'globe',
items: localeList.map(({ code, name }) => {
return { value: code, right: code, title: name }
})
},
},
};
const styles = {
height: '100vh',
display: 'flex',
@ -25,17 +40,19 @@ const styles = {
}
const store = configureStore({
metamask: { metamask: { currentLocale: 'en' } },
localeMessages: {
current: en,
en: en,
},
metamask: { metamask: { } },
})
const metamaskDecorator = story => (
const metamaskDecorator = (story, context) => {
const currentLocale = context.globals.locale
const current = allLocales[currentLocale]
return (
<Provider store={store}>
<I18nProvider>
<I18nProvider
currentLocale={currentLocale}
current={current}
en={allLocales.en}
>
<LegacyI18nProvider>
<div style={styles}>
{ story() }
@ -43,7 +60,8 @@ const metamaskDecorator = story => (
</LegacyI18nProvider>
</I18nProvider>
</Provider>
)
)
}
addDecorator(withKnobs)
addDecorator(metamaskDecorator)

@ -0,0 +1 @@
ignore-scripts true

@ -2,6 +2,21 @@
## Current Develop Branch
## 9.0.5 Mon Feb 08 2021
- [#10278](https://github.com/MetaMask/metamask-extension/pull/10278): Allow editing transaction amount after clicking max
- [#10214](https://github.com/MetaMask/metamask-extension/pull/10214): Standardize size, shape and color of network color indicators
- [#10298](https://github.com/MetaMask/metamask-extension/pull/10298): Use network primary currency instead of always defaulting to ETH in the confirm approve screen
- [#10300](https://github.com/MetaMask/metamask-extension/pull/10300): Add origin to signature request confirmation page
- [#10296](https://github.com/MetaMask/metamask-extension/pull/10296): Add origin to transaction confirmation
- [#10266](https://github.com/MetaMask/metamask-extension/pull/10266): Update `ko` localized messages
- [#10263](https://github.com/MetaMask/metamask-extension/pull/10263): Update `id` localized messages
- [#10347](https://github.com/MetaMask/metamask-extension/pull/10347): Require click of "Continue" button to interact with swap screen if there is a price impact warning for present swap
- [#10373](https://github.com/MetaMask/metamask-extension/pull/10373): Change copy of submit button on swaps screen
- [#10346](https://github.com/MetaMask/metamask-extension/pull/10346): Swaps token sources/verification messaging update
- [#10378](https://github.com/MetaMask/metamask-extension/pull/10378): Stop showing the window.web3 in-app popup if the dapp is just using web3.currentProvider
- [#10326](https://github.com/MetaMask/metamask-extension/pull/10326): Throw error when attempting to get an encryption key via eth_getEncryptionPublicKey when connected to Ledger HW
- [#10386](https://github.com/MetaMask/metamask-extension/pull/10386): Make action buttons on message components in swaps flow accessible
## 9.0.4 Fri Jan 22 2021
- [#10285](https://github.com/MetaMask/metamask-extension/pull/10285): Update @metamask/contract-metadata from v1.21.0 to 1.22.0
- [#10174](https://github.com/MetaMask/metamask-extension/pull/10174): Move fox to bottom of 'About' page

@ -17,10 +17,10 @@ To learn how to contribute to the MetaMask project itself, visit our [Internal D
## Building locally
- Install [Node.js](https://nodejs.org) version 10
- Install [Node.js](https://nodejs.org) version 14
- If you are using [nvm](https://github.com/creationix/nvm#installation) (recommended) running `nvm use` will automatically choose the right node version for you.
- Install [Yarn](https://yarnpkg.com/en/docs/install)
- Install dependencies: `yarn`
- Install dependencies: `yarn setup` (not the usual install command)
- Copy the `.metamaskrc.dist` file to `.metamaskrc`
- Replace the `INFURA_PROJECT_ID` value with your own personal [Infura Project ID](https://infura.io/docs).
- If debugging MetaMetrics, you'll need to add a value for `SEGMENT_WRITE_KEY` [Segment write key](https://segment.com/docs/connections/find-writekey/).

@ -1253,9 +1253,6 @@
"yourPrivateSeedPhrase": {
"message": "የግል ዘር ሐረግዎ"
},
"yourSigRequested": {
"message": "ፊርማዎ እየተጠየቀ ነው"
},
"zeroGasPriceOnSpeedUpError": {
"message": "በስፒድ አፕ ላይ ዜሮ የነዳጅ ዋጋ"
}

@ -1249,9 +1249,6 @@
"yourPrivateSeedPhrase": {
"message": "عبارة الأمان الشخصية الخاصة بك"
},
"yourSigRequested": {
"message": "جاري طلب توقيعك"
},
"zeroGasPriceOnSpeedUpError": {
"message": "سعر الغاز صفر عند التعجيل"
}

@ -1252,9 +1252,6 @@
"yourPrivateSeedPhrase": {
"message": "Вашата лична фраза зародиш"
},
"yourSigRequested": {
"message": "Изисква се вашият подпис"
},
"zeroGasPriceOnSpeedUpError": {
"message": "Нулева цена на газ при ускоряване"
}

@ -1256,9 +1256,6 @@
"yourPrivateSeedPhrase": {
"message": "আপনর বযকিগত সড ফজ"
},
"yourSigRequested": {
"message": "আপনর সষরর অনধ জ হয়"
},
"zeroGasPriceOnSpeedUpError": {
"message": "সড আপ এ শয গর ময"
}

@ -1225,9 +1225,6 @@
"yourPrivateSeedPhrase": {
"message": "La teva frase privada de seeds"
},
"yourSigRequested": {
"message": "Es necessita la teva firma"
},
"zeroGasPriceOnSpeedUpError": {
"message": "Gas a cost zero accelerant-se"
}

@ -464,8 +464,5 @@
},
"youSign": {
"message": "Podepisujete"
},
"yourSigRequested": {
"message": "Je vyžadován váš podpis"
}
}

@ -1225,9 +1225,6 @@
"yourPrivateSeedPhrase": {
"message": "Din private seed-sætning"
},
"yourSigRequested": {
"message": "Din signatur er ønsket"
},
"zeroGasPriceOnSpeedUpError": {
"message": "Nul brændstofpris på fremskynding"
}

@ -1213,9 +1213,6 @@
"yourPrivateSeedPhrase": {
"message": "Ihr privater Seed-Schlüssel"
},
"yourSigRequested": {
"message": "Deine Unterschrift wird angefordert"
},
"zeroGasPriceOnSpeedUpError": {
"message": "Keine Gaskosten bei Beschleunigung"
}

@ -1250,9 +1250,6 @@
"yourPrivateSeedPhrase": {
"message": "Η προσωπική σας φράση φύτρου"
},
"yourSigRequested": {
"message": "Ζητείται η υπογραφή σας"
},
"zeroGasPriceOnSpeedUpError": {
"message": "Μηδενική τιμή καυσίμου κατά την επιτάχυνση"
}

@ -364,6 +364,9 @@
"contactsSettingsDescription": {
"message": "Add, edit, remove, and manage your contacts"
},
"continue": {
"message": "Continue"
},
"continueToWyre": {
"message": "Continue to Wyre"
},
@ -1672,9 +1675,6 @@
"swapFinalizing": {
"message": "Finalizing..."
},
"swapGetQuotes": {
"message": "Get quotes"
},
"swapHighSlippageWarning": {
"message": "Slippage amount is very high. Make sure you know what you are doing!"
},
@ -1713,7 +1713,7 @@
"message": "MetaMask fee"
},
"swapMetaMaskFeeDescription": {
"message": "We find the best price from the top liquidity sources, every time. A fee of $1% is automatically factored into each quote, which supports ongoing development to make MetaMask even better.",
"message": "We find the best price from the top liquidity sources, every time. A fee of $1% is automatically factored into this quote.",
"description": "Provides information about the fee that metamask takes for swaps. $1 is a decimal number."
},
"swapNQuotes": {
@ -1735,6 +1735,9 @@
"message": "You are about to swap $1 $2 (~$3) for $4 $5 (~$6).",
"description": "This message represents the price slippage for the swap. $1 and $4 are a number (ex: 2.89), $2 and $5 are symbols (ex: ETH), and $3 and $6 are fiat currency amounts."
},
"swapPriceDifferenceAcknowledgement": {
"message": "I'm aware"
},
"swapPriceDifferenceTitle": {
"message": "Price difference of ~$1%",
"description": "$1 is a number (ex: 1.23) that represents the price difference."
@ -1792,6 +1795,9 @@
"swapRequestForQuotation": {
"message": "Request for quotation"
},
"swapReviewSwap": {
"message": "Review Swap"
},
"swapSearchForAToken": {
"message": "Search for a token"
},
@ -1839,6 +1845,17 @@
"message": "Swap $1 to $2",
"description": "Used in the transaction display list to describe a swap. $1 and $2 are the symbols of tokens in involved in a swap."
},
"swapTokenVerificationMessage": {
"message": "Always confirm the token address on $1.",
"description": "Points the user to Etherscan as a place they can verify information about a token. $1 is replaced with the translation for \"Etherscan\" followed by an info icon that shows more info on hover."
},
"swapTokenVerificationOnlyOneSource": {
"message": "Only verified on 1 source."
},
"swapTokenVerificationSources": {
"message": "Verified on $1 sources.",
"description": "Indicates the number of token information sources that recognize the symbol + address. $1 is a decimal number."
},
"swapTransactionComplete": {
"message": "Transaction complete"
},
@ -2086,6 +2103,9 @@
"viewAccount": {
"message": "View Account"
},
"viewAllDetails": {
"message": "View all details"
},
"viewContact": {
"message": "View Contact"
},
@ -2139,9 +2159,6 @@
"yourPrivateSeedPhrase": {
"message": "Your private seed phrase"
},
"yourSigRequested": {
"message": "Your signature is being requested"
},
"zeroGasPriceOnSpeedUpError": {
"message": "Zero gas price on speed up"
}

@ -985,9 +985,6 @@
"yourPrivateSeedPhrase": {
"message": "Tu frase semilla privada"
},
"yourSigRequested": {
"message": "Tu firma ya fue solicitada"
},
"zeroGasPriceOnSpeedUpError": {
"message": "No hubo precio de gas al agilizar"
}

@ -1235,9 +1235,6 @@
"yourPrivateSeedPhrase": {
"message": "Tu frase de inicialización privada"
},
"yourSigRequested": {
"message": "Su firma se está procesando"
},
"zeroGasPriceOnSpeedUpError": {
"message": "El precio del gas es cero en aceleración"
}

@ -1246,9 +1246,6 @@
"yourPrivateSeedPhrase": {
"message": "Teie privaatne seemnefraas"
},
"yourSigRequested": {
"message": "Taotletakse teie allkirja"
},
"zeroGasPriceOnSpeedUpError": {
"message": "Null gaasihind kiirendamisel"
}

@ -1256,9 +1256,6 @@
"yourPrivateSeedPhrase": {
"message": "عبارت بازیاب شخصی شما"
},
"yourSigRequested": {
"message": "امضاء شما درخواست میشود"
},
"zeroGasPriceOnSpeedUpError": {
"message": "قیمت صفر گاز بسوی سرعت"
}

@ -1253,9 +1253,6 @@
"yourPrivateSeedPhrase": {
"message": "Sinun yksityinen siemenlauseesi"
},
"yourSigRequested": {
"message": "Allekirjoitustasi pyydetään"
},
"zeroGasPriceOnSpeedUpError": {
"message": "Polttoaine ei maksa kiihdytyksen yhteydessä"
}

@ -1147,9 +1147,6 @@
"yourPrivateSeedPhrase": {
"message": "Ang iyong pribadong seed phrase"
},
"yourSigRequested": {
"message": "Hinihiling ang iyong signature"
},
"zeroGasPriceOnSpeedUpError": {
"message": "Zero ang presyo ng gas sa speed up"
}

@ -1232,9 +1232,6 @@
"yourPrivateSeedPhrase": {
"message": "Votre phrase Seed privée"
},
"yourSigRequested": {
"message": "Votre signature est demandée"
},
"zeroGasPriceOnSpeedUpError": {
"message": "Prix de l'essence zéro sur l'accélération"
}

@ -1250,9 +1250,6 @@
"yourPrivateSeedPhrase": {
"message": "ה-seed phrase הפרטי שלך"
},
"yourSigRequested": {
"message": "נדרשת חתימתך"
},
"zeroGasPriceOnSpeedUpError": {
"message": "מחיר גז אפס על האצה"
}

@ -1657,9 +1657,6 @@
"swapFinalizing": {
"message": "अिम रप दि रह..."
},
"swapGetQuotes": {
"message": "उदधरण पत कर"
},
"swapHighSlippageWarning": {
"message": "सिज रि बहत अधिक ह। सिित करि आप जनति आप क कर रह!"
},
@ -2096,9 +2093,6 @@
"yourPrivateSeedPhrase": {
"message": "आपकिड फ"
},
"yourSigRequested": {
"message": "आपक हसषर क अनध कि रह"
},
"zeroGasPriceOnSpeedUpError": {
"message": "जस मय म"
}

@ -423,8 +423,5 @@
},
"youSign": {
"message": "आप हसषर कर रह"
},
"yourSigRequested": {
"message": "आपक हसषर अनध कि रह"
}
}

@ -1246,9 +1246,6 @@
"yourPrivateSeedPhrase": {
"message": "Vaša privatna početna rečenica"
},
"yourSigRequested": {
"message": "Vaš se potpis zahtijeva"
},
"zeroGasPriceOnSpeedUpError": {
"message": "Nulta cijena goriva kod ubrzavanja"
}

@ -780,8 +780,5 @@
},
"yourPrivateSeedPhrase": {
"message": "Seed fraz prive ou a"
},
"yourSigRequested": {
"message": "Yo mande siyati ou"
}
}

@ -1246,9 +1246,6 @@
"yourPrivateSeedPhrase": {
"message": "Az ön privát seed mondata"
},
"yourSigRequested": {
"message": "Szükség van az aláírására"
},
"zeroGasPriceOnSpeedUpError": {
"message": "Nulla gázár a gyorsuláshoz"
}

File diff suppressed because it is too large Load Diff

@ -1718,9 +1718,6 @@
"yourPrivateSeedPhrase": {
"message": "La tua frase seed privata"
},
"yourSigRequested": {
"message": "E' richiesta la tua firma"
},
"zeroGasPriceOnSpeedUpError": {
"message": "Prezzo del gas maggiore di zero"
}

@ -474,8 +474,5 @@
},
"youSign": {
"message": "署名しています。"
},
"yourSigRequested": {
"message": "あなたの署名がリクエストされています。"
}
}

@ -1256,9 +1256,6 @@
"yourPrivateSeedPhrase": {
"message": "ನಿಮ ಖಸಗಿ"
},
"yourSigRequested": {
"message": "ನಿಮ ಸಹಿಯನಿಿಸಲಿ"
},
"zeroGasPriceOnSpeedUpError": {
"message": "ವಗ ಹಿದಕಯ ಗ"
}

File diff suppressed because it is too large Load Diff

@ -1256,9 +1256,6 @@
"yourPrivateSeedPhrase": {
"message": "Jūsų asmeninė atkūrimo frazė"
},
"yourSigRequested": {
"message": "Prašoma jūsų parašo"
},
"zeroGasPriceOnSpeedUpError": {
"message": "Nustatykite nulinę dujų kainą greitėjant"
}

@ -1252,9 +1252,6 @@
"yourPrivateSeedPhrase": {
"message": "Jūsu privātā atkopšanas frāze"
},
"yourSigRequested": {
"message": "Nepieciešams jūsu paraksts"
},
"zeroGasPriceOnSpeedUpError": {
"message": "Gas nulles cena pie paātrinājuma"
}

@ -1230,9 +1230,6 @@
"yourPrivateSeedPhrase": {
"message": "Ungkapan benih peribadi anda"
},
"yourSigRequested": {
"message": "Tandatangan anda sedang diminta"
},
"zeroGasPriceOnSpeedUpError": {
"message": "Sifar harga gas untuk pencepatan"
}

@ -410,8 +410,5 @@
},
"youSign": {
"message": "U ondertekent"
},
"yourSigRequested": {
"message": "Uw handtekening wordt aangevraagd"
}
}

@ -1228,9 +1228,6 @@
"yourPrivateSeedPhrase": {
"message": "Din private frøfrase"
},
"yourSigRequested": {
"message": "Det bes om signaturen din "
},
"zeroGasPriceOnSpeedUpError": {
"message": "Null bensinpris for fremskynding"
}

@ -252,8 +252,5 @@
},
"youSign": {
"message": "Ikaw ay nagsa-sign"
},
"yourSigRequested": {
"message": "Hinihiling ang iyong signature"
}
}

@ -1244,9 +1244,6 @@
"yourPrivateSeedPhrase": {
"message": "Twoja prywatna fraza seed"
},
"yourSigRequested": {
"message": "Twój podpis jest wymagany"
},
"zeroGasPriceOnSpeedUpError": {
"message": "Przyspieszenie z zerową ceną gazu"
}

@ -420,8 +420,5 @@
},
"youSign": {
"message": "Está a assinar"
},
"yourSigRequested": {
"message": "A sua assinatura está a ser pedida"
}
}

@ -1238,9 +1238,6 @@
"yourPrivateSeedPhrase": {
"message": "Sua frase-semente particular"
},
"yourSigRequested": {
"message": "Sua assinatura está sendo solicitada"
},
"zeroGasPriceOnSpeedUpError": {
"message": "Preço de Gas zero na agilização"
}

@ -1237,9 +1237,6 @@
"yourPrivateSeedPhrase": {
"message": "Expresia dvs. seed privată"
},
"yourSigRequested": {
"message": "Semnătura dvs. este solicitată"
},
"zeroGasPriceOnSpeedUpError": {
"message": "Preț gas zero la accelerare"
}

@ -1292,8 +1292,5 @@
},
"yourPrivateSeedPhrase": {
"message": "Ваша сид-фраза"
},
"yourSigRequested": {
"message": "Запрашивается ваша подпись"
}
}

@ -1213,9 +1213,6 @@
"yourPrivateSeedPhrase": {
"message": "Vaša súkromná seed fráza"
},
"yourSigRequested": {
"message": "Je vyžadován váš podpis"
},
"zeroGasPriceOnSpeedUpError": {
"message": "Nulová cena za GAS pri zrýchlení"
}

@ -1235,9 +1235,6 @@
"yourPrivateSeedPhrase": {
"message": "Vaš zasebni seed phrase"
},
"yourSigRequested": {
"message": "Zahtevan je bil vaš podpis"
},
"zeroGasPriceOnSpeedUpError": {
"message": "Ničelni gas price na pospešitvi"
}

@ -1241,9 +1241,6 @@
"yourPrivateSeedPhrase": {
"message": "Vaša privatna šifra za oporavak naloga (seed phrase)"
},
"yourSigRequested": {
"message": "Zahteva se vaš potpis"
},
"zeroGasPriceOnSpeedUpError": {
"message": "Nulta cena gasa ubrzava"
}

@ -1231,9 +1231,6 @@
"yourPrivateSeedPhrase": {
"message": "Din privata seedphrase"
},
"yourSigRequested": {
"message": "En begäran om din signatur har skickats"
},
"zeroGasPriceOnSpeedUpError": {
"message": "Inget gaspris vid uppsnabbning"
}

@ -1234,9 +1234,6 @@
"yourPrivateSeedPhrase": {
"message": "Kirai chako kianzio cha binafsi"
},
"yourSigRequested": {
"message": "Saini yako inaombwa"
},
"zeroGasPriceOnSpeedUpError": {
"message": "Bei ya gesi sifuri kwenye kuongeza kasi"
}

@ -552,8 +552,5 @@
},
"youSign": {
"message": "நகளிிகள"
},
"yourSigRequested": {
"message": "உஙகளபமரபபடிறத"
}
}

@ -573,8 +573,5 @@
},
"youSign": {
"message": "คณกำลงเซนชอ"
},
"yourSigRequested": {
"message": "ลายเซนของคณกำลงไดบการรองขอ"
}
}

@ -486,8 +486,5 @@
},
"youSign": {
"message": "İmzalıyorsunuz"
},
"yourSigRequested": {
"message": "İmzanız isteniyor"
}
}

@ -1256,9 +1256,6 @@
"yourPrivateSeedPhrase": {
"message": "Ваша секретна seed-фраза"
},
"yourSigRequested": {
"message": "Надійшов запит вашого підпису"
},
"zeroGasPriceOnSpeedUpError": {
"message": "Ціна пального на прискорення - нуль"
}

@ -303,8 +303,5 @@
},
"youSign": {
"message": "Bạn đang ký nhận"
},
"yourSigRequested": {
"message": "Chữ ký của bạn đang được yêu cầu"
}
}

@ -1241,9 +1241,6 @@
"yourPrivateSeedPhrase": {
"message": "你的私有助记词"
},
"yourSigRequested": {
"message": "正在请求你的签名"
},
"zeroGasPriceOnSpeedUpError": {
"message": "Gas 价格加速上涨"
}

@ -1244,9 +1244,6 @@
"yourPrivateSeedPhrase": {
"message": "您的助憶詞"
},
"yourSigRequested": {
"message": "正在請求您的簽署"
},
"zeroGasPriceOnSpeedUpError": {
"message": "加速的 Gas 價格為 0"
}

@ -78,6 +78,6 @@
"notifications"
],
"short_name": "__MSG_appName__",
"version": "9.0.4",
"version": "9.0.5",
"web_accessible_resources": ["inpage.js", "phishing.html"]
}

@ -1,53 +1,53 @@
import log from 'loglevel'
import Wallet from 'ethereumjs-wallet'
import importers from 'ethereumjs-wallet/thirdparty'
import ethUtil from 'ethereumjs-util'
import { addHexPrefix } from '../lib/util'
import log from 'loglevel';
import Wallet from 'ethereumjs-wallet';
import importers from 'ethereumjs-wallet/thirdparty';
import ethUtil from 'ethereumjs-util';
import { addHexPrefix } from '../lib/util';
const accountImporter = {
importAccount(strategy, args) {
try {
const importer = this.strategies[strategy]
const privateKeyHex = importer(...args)
return Promise.resolve(privateKeyHex)
const importer = this.strategies[strategy];
const privateKeyHex = importer(...args);
return Promise.resolve(privateKeyHex);
} catch (e) {
return Promise.reject(e)
return Promise.reject(e);
}
},
strategies: {
'Private Key': (privateKey) => {
if (!privateKey) {
throw new Error('Cannot import an empty key.')
throw new Error('Cannot import an empty key.');
}
const prefixed = addHexPrefix(privateKey)
const buffer = ethUtil.toBuffer(prefixed)
const prefixed = addHexPrefix(privateKey);
const buffer = ethUtil.toBuffer(prefixed);
if (!ethUtil.isValidPrivate(buffer)) {
throw new Error('Cannot import invalid private key.')
throw new Error('Cannot import invalid private key.');
}
const stripped = ethUtil.stripHexPrefix(prefixed)
return stripped
const stripped = ethUtil.stripHexPrefix(prefixed);
return stripped;
},
'JSON File': (input, password) => {
let wallet
let wallet;
try {
wallet = importers.fromEtherWallet(input, password)
wallet = importers.fromEtherWallet(input, password);
} catch (e) {
log.debug('Attempt to import as EtherWallet format failed, trying V3')
wallet = Wallet.fromV3(input, password, true)
log.debug('Attempt to import as EtherWallet format failed, trying V3');
wallet = Wallet.fromV3(input, password, true);
}
return walletToPrivateKey(wallet)
return walletToPrivateKey(wallet);
},
},
}
};
function walletToPrivateKey(wallet) {
const privateKeyBuffer = wallet.getPrivateKey()
return ethUtil.bufferToHex(privateKeyBuffer)
const privateKeyBuffer = wallet.getPrivateKey();
return ethUtil.bufferToHex(privateKeyBuffer);
}
export default accountImporter
export default accountImporter;

@ -3,68 +3,68 @@
*/
// these need to run before anything else
/* eslint-disable import/first,import/order */
import setupFetchDebugging from './lib/setupFetchDebugging'
import setupFetchDebugging from './lib/setupFetchDebugging';
/* eslint-enable import/order */
setupFetchDebugging()
setupFetchDebugging();
// polyfills
import 'abortcontroller-polyfill/dist/polyfill-patch-fetch'
import 'abortcontroller-polyfill/dist/polyfill-patch-fetch';
import endOfStream from 'end-of-stream'
import pump from 'pump'
import debounce from 'debounce-stream'
import log from 'loglevel'
import extension from 'extensionizer'
import { storeAsStream, storeTransformStream } from '@metamask/obs-store'
import PortStream from 'extension-port-stream'
import { captureException } from '@sentry/browser'
import endOfStream from 'end-of-stream';
import pump from 'pump';
import debounce from 'debounce-stream';
import log from 'loglevel';
import extension from 'extensionizer';
import { storeAsStream, storeTransformStream } from '@metamask/obs-store';
import PortStream from 'extension-port-stream';
import { captureException } from '@sentry/browser';
import {
ENVIRONMENT_TYPE_POPUP,
ENVIRONMENT_TYPE_NOTIFICATION,
ENVIRONMENT_TYPE_FULLSCREEN,
} from '../../shared/constants/app'
import migrations from './migrations'
import Migrator from './lib/migrator'
import ExtensionPlatform from './platforms/extension'
import LocalStore from './lib/local-store'
import ReadOnlyNetworkStore from './lib/network-store'
import createStreamSink from './lib/createStreamSink'
import NotificationManager from './lib/notification-manager'
import MetamaskController from './metamask-controller'
import rawFirstTimeState from './first-time-state'
import getFirstPreferredLangCode from './lib/get-first-preferred-lang-code'
import getObjStructure from './lib/getObjStructure'
import setupEnsIpfsResolver from './lib/ens-ipfs/setup'
} from '../../shared/constants/app';
import migrations from './migrations';
import Migrator from './lib/migrator';
import ExtensionPlatform from './platforms/extension';
import LocalStore from './lib/local-store';
import ReadOnlyNetworkStore from './lib/network-store';
import createStreamSink from './lib/createStreamSink';
import NotificationManager from './lib/notification-manager';
import MetamaskController from './metamask-controller';
import rawFirstTimeState from './first-time-state';
import getFirstPreferredLangCode from './lib/get-first-preferred-lang-code';
import getObjStructure from './lib/getObjStructure';
import setupEnsIpfsResolver from './lib/ens-ipfs/setup';
/* eslint-enable import/first */
const { sentry } = global
const firstTimeState = { ...rawFirstTimeState }
const { sentry } = global;
const firstTimeState = { ...rawFirstTimeState };
log.setDefaultLevel(process.env.METAMASK_DEBUG ? 'debug' : 'warn')
log.setDefaultLevel(process.env.METAMASK_DEBUG ? 'debug' : 'warn');
const platform = new ExtensionPlatform()
const platform = new ExtensionPlatform();
const notificationManager = new NotificationManager()
global.METAMASK_NOTIFIER = notificationManager
const notificationManager = new NotificationManager();
global.METAMASK_NOTIFIER = notificationManager;
let popupIsOpen = false
let notificationIsOpen = false
const openMetamaskTabsIDs = {}
const requestAccountTabIds = {}
let popupIsOpen = false;
let notificationIsOpen = false;
const openMetamaskTabsIDs = {};
const requestAccountTabIds = {};
// state persistence
const inTest = process.env.IN_TEST === 'true'
const localStore = inTest ? new ReadOnlyNetworkStore() : new LocalStore()
let versionedData
const inTest = process.env.IN_TEST === 'true';
const localStore = inTest ? new ReadOnlyNetworkStore() : new LocalStore();
let versionedData;
if (inTest || process.env.METAMASK_DEBUG) {
global.metamaskGetState = localStore.get.bind(localStore)
global.metamaskGetState = localStore.get.bind(localStore);
}
// initialization flow
initialize().catch(log.error)
initialize().catch(log.error);
/**
* An object representing a transaction, in whatever state it is in.
@ -139,10 +139,10 @@ initialize().catch(log.error)
* @returns {Promise} Setup complete.
*/
async function initialize() {
const initState = await loadStateFromPersistence()
const initLangCode = await getFirstPreferredLangCode()
await setupController(initState, initLangCode)
log.debug('MetaMask initialization complete.')
const initState = await loadStateFromPersistence();
const initLangCode = await getFirstPreferredLangCode();
await setupController(initState, initLangCode);
log.debug('MetaMask initialization complete.');
}
//
@ -156,13 +156,13 @@ async function initialize() {
*/
async function loadStateFromPersistence() {
// migrations
const migrator = new Migrator({ migrations })
migrator.on('error', console.warn)
const migrator = new Migrator({ migrations });
migrator.on('error', console.warn);
// read from disk
// first from preferred, async API:
versionedData =
(await localStore.get()) || migrator.generateInitialState(firstTimeState)
(await localStore.get()) || migrator.generateInitialState(firstTimeState);
// check if somehow state is empty
// this should never happen but new error reporting suggests that it has
@ -170,38 +170,38 @@ async function loadStateFromPersistence() {
// https://github.com/metamask/metamask-extension/issues/3919
if (versionedData && !versionedData.data) {
// unable to recover, clear state
versionedData = migrator.generateInitialState(firstTimeState)
sentry.captureMessage('MetaMask - Empty vault found - unable to recover')
versionedData = migrator.generateInitialState(firstTimeState);
sentry.captureMessage('MetaMask - Empty vault found - unable to recover');
}
// report migration errors to sentry
migrator.on('error', (err) => {
// get vault structure without secrets
const vaultStructure = getObjStructure(versionedData)
const vaultStructure = getObjStructure(versionedData);
sentry.captureException(err, {
// "extra" key is required by Sentry
extra: { vaultStructure },
})
})
});
});
// migrate data
versionedData = await migrator.migrateData(versionedData)
versionedData = await migrator.migrateData(versionedData);
if (!versionedData) {
throw new Error('MetaMask - migrator returned undefined')
throw new Error('MetaMask - migrator returned undefined');
}
// write to disk
if (localStore.isSupported) {
localStore.set(versionedData)
localStore.set(versionedData);
} else {
// throw in setTimeout so as to not block boot
setTimeout(() => {
throw new Error('MetaMask - Localstore not supported')
})
throw new Error('MetaMask - Localstore not supported');
});
}
// return just the data
return versionedData.data
return versionedData.data;
}
/**
@ -232,12 +232,12 @@ function setupController(initState, initLangCode) {
platform,
extension,
getRequestAccountTabIds: () => {
return requestAccountTabIds
return requestAccountTabIds;
},
getOpenMetamaskTabsIds: () => {
return openMetamaskTabsIDs
return openMetamaskTabsIDs;
},
})
});
setupEnsIpfsResolver({
getCurrentNetwork: controller.getCurrentNetwork,
@ -245,7 +245,7 @@ function setupController(initState, initLangCode) {
controller.preferencesController,
),
provider: controller.provider,
})
});
// setup state persistence
pump(
@ -254,9 +254,9 @@ function setupController(initState, initLangCode) {
storeTransformStream(versionifyData),
createStreamSink(persistData),
(error) => {
log.error('MetaMask - Persistence pipeline failed', error)
log.error('MetaMask - Persistence pipeline failed', error);
},
)
);
/**
* Assigns the given state to the versioned object (with metadata), and returns that.
@ -264,32 +264,32 @@ function setupController(initState, initLangCode) {
* @returns {VersionedData} The state object wrapped in an object that includes a metadata key.
*/
function versionifyData(state) {
versionedData.data = state
return versionedData
versionedData.data = state;
return versionedData;
}
let dataPersistenceFailing = false
let dataPersistenceFailing = false;
async function persistData(state) {
if (!state) {
throw new Error('MetaMask - updated state is missing')
throw new Error('MetaMask - updated state is missing');
}
if (!state.data) {
throw new Error('MetaMask - updated state does not have data')
throw new Error('MetaMask - updated state does not have data');
}
if (localStore.isSupported) {
try {
await localStore.set(state)
await localStore.set(state);
if (dataPersistenceFailing) {
dataPersistenceFailing = false
dataPersistenceFailing = false;
}
} catch (err) {
// log error so we dont break the pipeline
if (!dataPersistenceFailing) {
dataPersistenceFailing = true
captureException(err)
dataPersistenceFailing = true;
captureException(err);
}
log.error('error setting state in local store:', err)
log.error('error setting state in local store:', err);
}
}
}
@ -297,24 +297,24 @@ function setupController(initState, initLangCode) {
//
// connect to other contexts
//
extension.runtime.onConnect.addListener(connectRemote)
extension.runtime.onConnectExternal.addListener(connectExternal)
extension.runtime.onConnect.addListener(connectRemote);
extension.runtime.onConnectExternal.addListener(connectExternal);
const metamaskInternalProcessHash = {
[ENVIRONMENT_TYPE_POPUP]: true,
[ENVIRONMENT_TYPE_NOTIFICATION]: true,
[ENVIRONMENT_TYPE_FULLSCREEN]: true,
}
};
const metamaskBlockedPorts = ['trezor-connect']
const metamaskBlockedPorts = ['trezor-connect'];
const isClientOpenStatus = () => {
return (
popupIsOpen ||
Boolean(Object.keys(openMetamaskTabsIDs).length) ||
notificationIsOpen
)
}
);
};
/**
* A runtime.Port object, as provided by the browser:
@ -329,99 +329,99 @@ function setupController(initState, initLangCode) {
* @param {Port} remotePort - The port provided by a new context.
*/
function connectRemote(remotePort) {
const processName = remotePort.name
const isMetaMaskInternalProcess = metamaskInternalProcessHash[processName]
const processName = remotePort.name;
const isMetaMaskInternalProcess = metamaskInternalProcessHash[processName];
if (metamaskBlockedPorts.includes(remotePort.name)) {
return
return;
}
if (isMetaMaskInternalProcess) {
const portStream = new PortStream(remotePort)
const portStream = new PortStream(remotePort);
// communication with popup
controller.isClientOpen = true
controller.setupTrustedCommunication(portStream, remotePort.sender)
controller.isClientOpen = true;
controller.setupTrustedCommunication(portStream, remotePort.sender);
if (processName === ENVIRONMENT_TYPE_POPUP) {
popupIsOpen = true
popupIsOpen = true;
endOfStream(portStream, () => {
popupIsOpen = false
controller.isClientOpen = isClientOpenStatus()
})
popupIsOpen = false;
controller.isClientOpen = isClientOpenStatus();
});
}
if (processName === ENVIRONMENT_TYPE_NOTIFICATION) {
notificationIsOpen = true
notificationIsOpen = true;
endOfStream(portStream, () => {
notificationIsOpen = false
controller.isClientOpen = isClientOpenStatus()
})
notificationIsOpen = false;
controller.isClientOpen = isClientOpenStatus();
});
}
if (processName === ENVIRONMENT_TYPE_FULLSCREEN) {
const tabId = remotePort.sender.tab.id
openMetamaskTabsIDs[tabId] = true
const tabId = remotePort.sender.tab.id;
openMetamaskTabsIDs[tabId] = true;
endOfStream(portStream, () => {
delete openMetamaskTabsIDs[tabId]
controller.isClientOpen = isClientOpenStatus()
})
delete openMetamaskTabsIDs[tabId];
controller.isClientOpen = isClientOpenStatus();
});
}
} else {
if (remotePort.sender && remotePort.sender.tab && remotePort.sender.url) {
const tabId = remotePort.sender.tab.id
const url = new URL(remotePort.sender.url)
const { origin } = url
const tabId = remotePort.sender.tab.id;
const url = new URL(remotePort.sender.url);
const { origin } = url;
remotePort.onMessage.addListener((msg) => {
if (msg.data && msg.data.method === 'eth_requestAccounts') {
requestAccountTabIds[origin] = tabId
requestAccountTabIds[origin] = tabId;
}
})
});
}
connectExternal(remotePort)
connectExternal(remotePort);
}
}
// communication with page or other extension
function connectExternal(remotePort) {
const portStream = new PortStream(remotePort)
controller.setupUntrustedCommunication(portStream, remotePort.sender)
const portStream = new PortStream(remotePort);
controller.setupUntrustedCommunication(portStream, remotePort.sender);
}
//
// User Interface setup
//
updateBadge()
controller.txController.on('update:badge', updateBadge)
controller.messageManager.on('updateBadge', updateBadge)
controller.personalMessageManager.on('updateBadge', updateBadge)
controller.decryptMessageManager.on('updateBadge', updateBadge)
controller.encryptionPublicKeyManager.on('updateBadge', updateBadge)
controller.typedMessageManager.on('updateBadge', updateBadge)
controller.approvalController.subscribe(updateBadge)
controller.appStateController.on('updateBadge', updateBadge)
updateBadge();
controller.txController.on('update:badge', updateBadge);
controller.messageManager.on('updateBadge', updateBadge);
controller.personalMessageManager.on('updateBadge', updateBadge);
controller.decryptMessageManager.on('updateBadge', updateBadge);
controller.encryptionPublicKeyManager.on('updateBadge', updateBadge);
controller.typedMessageManager.on('updateBadge', updateBadge);
controller.approvalController.subscribe(updateBadge);
controller.appStateController.on('updateBadge', updateBadge);
/**
* Updates the Web Extension's "badge" number, on the little fox in the toolbar.
* The number reflects the current number of pending transactions or message signatures needing user approval.
*/
function updateBadge() {
let label = ''
const unapprovedTxCount = controller.txController.getUnapprovedTxCount()
const { unapprovedMsgCount } = controller.messageManager
const { unapprovedPersonalMsgCount } = controller.personalMessageManager
const { unapprovedDecryptMsgCount } = controller.decryptMessageManager
let label = '';
const unapprovedTxCount = controller.txController.getUnapprovedTxCount();
const { unapprovedMsgCount } = controller.messageManager;
const { unapprovedPersonalMsgCount } = controller.personalMessageManager;
const { unapprovedDecryptMsgCount } = controller.decryptMessageManager;
const {
unapprovedEncryptionPublicKeyMsgCount,
} = controller.encryptionPublicKeyManager
const { unapprovedTypedMessagesCount } = controller.typedMessageManager
const pendingApprovalCount = controller.approvalController.getTotalApprovalCount()
} = controller.encryptionPublicKeyManager;
const { unapprovedTypedMessagesCount } = controller.typedMessageManager;
const pendingApprovalCount = controller.approvalController.getTotalApprovalCount();
const waitingForUnlockCount =
controller.appStateController.waitingForUnlock.length
controller.appStateController.waitingForUnlock.length;
const count =
unapprovedTxCount +
unapprovedMsgCount +
@ -430,15 +430,15 @@ function setupController(initState, initLangCode) {
unapprovedEncryptionPublicKeyMsgCount +
unapprovedTypedMessagesCount +
pendingApprovalCount +
waitingForUnlockCount
waitingForUnlockCount;
if (count) {
label = String(count)
label = String(count);
}
extension.browserAction.setBadgeText({ text: label })
extension.browserAction.setBadgeBackgroundColor({ color: '#037DD6' })
extension.browserAction.setBadgeText({ text: label });
extension.browserAction.setBadgeBackgroundColor({ color: '#037DD6' });
}
return Promise.resolve()
return Promise.resolve();
}
//
@ -449,18 +449,18 @@ function setupController(initState, initLangCode) {
* Opens the browser popup for user confirmation
*/
async function triggerUi() {
const tabs = await platform.getActiveTabs()
const tabs = await platform.getActiveTabs();
const currentlyActiveMetamaskTab = Boolean(
tabs.find((tab) => openMetamaskTabsIDs[tab.id]),
)
);
// Vivaldi is not closing port connection on popup close, so popupIsOpen does not work correctly
// To be reviewed in the future if this behaviour is fixed - also the way we determine isVivaldi variable might change at some point
const isVivaldi =
tabs.length > 0 &&
tabs[0].extData &&
tabs[0].extData.indexOf('vivaldi_tab') > -1
tabs[0].extData.indexOf('vivaldi_tab') > -1;
if ((isVivaldi || !popupIsOpen) && !currentlyActiveMetamaskTab) {
await notificationManager.showPopup()
await notificationManager.showPopup();
}
}
@ -469,15 +469,15 @@ async function triggerUi() {
* then it waits until user interact with the UI
*/
async function openPopup() {
await triggerUi()
await triggerUi();
await new Promise((resolve) => {
const interval = setInterval(() => {
if (!notificationIsOpen) {
clearInterval(interval)
resolve()
clearInterval(interval);
resolve();
}
}, 1000)
})
}, 1000);
});
}
// On first install, open a new tab with MetaMask
@ -486,6 +486,6 @@ extension.runtime.onInstalled.addListener(({ reason }) => {
reason === 'install' &&
!(process.env.METAMASK_DEBUG || process.env.IN_TEST)
) {
platform.openExtensionInBrowser()
platform.openExtensionInBrowser();
}
})
});

@ -1,8 +1,8 @@
export const SINGLE_CALL_BALANCES_ADDRESS =
'0xb1f8e55c7f64d203c1400b9d8555d050f94adf39'
'0xb1f8e55c7f64d203c1400b9d8555d050f94adf39';
export const SINGLE_CALL_BALANCES_ADDRESS_RINKEBY =
'0x9f510b19f1ad66f0dcf6e45559fab0d6752c1db7'
'0x9f510b19f1ad66f0dcf6e45559fab0d6752c1db7';
export const SINGLE_CALL_BALANCES_ADDRESS_ROPSTEN =
'0xb8e671734ce5c8d7dfbbea5574fa4cf39f7a54a4'
'0xb8e671734ce5c8d7dfbbea5574fa4cf39f7a54a4';
export const SINGLE_CALL_BALANCES_ADDRESS_KOVAN =
'0xb1d3fbb2f83aecd196f474c16ca5d9cffa0d0ffc'
'0xb1d3fbb2f83aecd196f474c16ca5d9cffa0d0ffc';

@ -1,35 +1,35 @@
import querystring from 'querystring'
import pump from 'pump'
import LocalMessageDuplexStream from 'post-message-stream'
import ObjectMultiplex from 'obj-multiplex'
import extension from 'extensionizer'
import PortStream from 'extension-port-stream'
import { obj as createThoughStream } from 'through2'
import querystring from 'querystring';
import pump from 'pump';
import LocalMessageDuplexStream from 'post-message-stream';
import ObjectMultiplex from 'obj-multiplex';
import extension from 'extensionizer';
import PortStream from 'extension-port-stream';
import { obj as createThoughStream } from 'through2';
// These require calls need to use require to be statically recognized by browserify
const fs = require('fs')
const path = require('path')
const fs = require('fs');
const path = require('path');
const inpageContent = fs.readFileSync(
path.join(__dirname, '..', '..', 'dist', 'chrome', 'inpage.js'),
'utf8',
)
const inpageSuffix = `//# sourceURL=${extension.runtime.getURL('inpage.js')}\n`
const inpageBundle = inpageContent + inpageSuffix
);
const inpageSuffix = `//# sourceURL=${extension.runtime.getURL('inpage.js')}\n`;
const inpageBundle = inpageContent + inpageSuffix;
const CONTENT_SCRIPT = 'metamask-contentscript'
const INPAGE = 'metamask-inpage'
const PROVIDER = 'metamask-provider'
const CONTENT_SCRIPT = 'metamask-contentscript';
const INPAGE = 'metamask-inpage';
const PROVIDER = 'metamask-provider';
// TODO:LegacyProvider: Delete
const LEGACY_CONTENT_SCRIPT = 'contentscript'
const LEGACY_INPAGE = 'inpage'
const LEGACY_PROVIDER = 'provider'
const LEGACY_PUBLIC_CONFIG = 'publicConfig'
const LEGACY_CONTENT_SCRIPT = 'contentscript';
const LEGACY_INPAGE = 'inpage';
const LEGACY_PROVIDER = 'provider';
const LEGACY_PUBLIC_CONFIG = 'publicConfig';
if (shouldInjectProvider()) {
injectScript(inpageBundle)
setupStreams()
injectScript(inpageBundle);
setupStreams();
}
/**
@ -39,14 +39,14 @@ if (shouldInjectProvider()) {
*/
function injectScript(content) {
try {
const container = document.head || document.documentElement
const scriptTag = document.createElement('script')
scriptTag.setAttribute('async', 'false')
scriptTag.textContent = content
container.insertBefore(scriptTag, container.children[0])
container.removeChild(scriptTag)
const container = document.head || document.documentElement;
const scriptTag = document.createElement('script');
scriptTag.setAttribute('async', 'false');
scriptTag.textContent = content;
container.insertBefore(scriptTag, container.children[0]);
container.removeChild(scriptTag);
} catch (error) {
console.error('MetaMask: Provider injection failed.', error)
console.error('MetaMask: Provider injection failed.', error);
}
}
@ -60,81 +60,81 @@ async function setupStreams() {
const pageStream = new LocalMessageDuplexStream({
name: CONTENT_SCRIPT,
target: INPAGE,
})
const extensionPort = extension.runtime.connect({ name: CONTENT_SCRIPT })
const extensionStream = new PortStream(extensionPort)
});
const extensionPort = extension.runtime.connect({ name: CONTENT_SCRIPT });
const extensionStream = new PortStream(extensionPort);
// create and connect channel muxers
// so we can handle the channels individually
const pageMux = new ObjectMultiplex()
pageMux.setMaxListeners(25)
const extensionMux = new ObjectMultiplex()
extensionMux.setMaxListeners(25)
extensionMux.ignoreStream(LEGACY_PUBLIC_CONFIG) // TODO:LegacyProvider: Delete
const pageMux = new ObjectMultiplex();
pageMux.setMaxListeners(25);
const extensionMux = new ObjectMultiplex();
extensionMux.setMaxListeners(25);
extensionMux.ignoreStream(LEGACY_PUBLIC_CONFIG); // TODO:LegacyProvider: Delete
pump(pageMux, pageStream, pageMux, (err) =>
logStreamDisconnectWarning('MetaMask Inpage Multiplex', err),
)
);
pump(extensionMux, extensionStream, extensionMux, (err) => {
logStreamDisconnectWarning('MetaMask Background Multiplex', err)
notifyInpageOfStreamFailure()
})
logStreamDisconnectWarning('MetaMask Background Multiplex', err);
notifyInpageOfStreamFailure();
});
// forward communication across inpage-background for these channels only
forwardTrafficBetweenMuxes(PROVIDER, pageMux, extensionMux)
forwardTrafficBetweenMuxes(PROVIDER, pageMux, extensionMux);
// connect "phishing" channel to warning system
const phishingStream = extensionMux.createStream('phishing')
phishingStream.once('data', redirectToPhishingWarning)
const phishingStream = extensionMux.createStream('phishing');
phishingStream.once('data', redirectToPhishingWarning);
// TODO:LegacyProvider: Delete
// handle legacy provider
const legacyPageStream = new LocalMessageDuplexStream({
name: LEGACY_CONTENT_SCRIPT,
target: LEGACY_INPAGE,
})
});
const legacyPageMux = new ObjectMultiplex()
legacyPageMux.setMaxListeners(25)
const legacyExtensionMux = new ObjectMultiplex()
legacyExtensionMux.setMaxListeners(25)
const legacyPageMux = new ObjectMultiplex();
legacyPageMux.setMaxListeners(25);
const legacyExtensionMux = new ObjectMultiplex();
legacyExtensionMux.setMaxListeners(25);
pump(legacyPageMux, legacyPageStream, legacyPageMux, (err) =>
logStreamDisconnectWarning('MetaMask Legacy Inpage Multiplex', err),
)
);
pump(
legacyExtensionMux,
extensionStream,
getNotificationTransformStream(),
legacyExtensionMux,
(err) => {
logStreamDisconnectWarning('MetaMask Background Legacy Multiplex', err)
notifyInpageOfStreamFailure()
logStreamDisconnectWarning('MetaMask Background Legacy Multiplex', err);
notifyInpageOfStreamFailure();
},
)
);
forwardNamedTrafficBetweenMuxes(
LEGACY_PROVIDER,
PROVIDER,
legacyPageMux,
legacyExtensionMux,
)
);
forwardTrafficBetweenMuxes(
LEGACY_PUBLIC_CONFIG,
legacyPageMux,
legacyExtensionMux,
)
);
}
function forwardTrafficBetweenMuxes(channelName, muxA, muxB) {
const channelA = muxA.createStream(channelName)
const channelB = muxB.createStream(channelName)
const channelA = muxA.createStream(channelName);
const channelB = muxB.createStream(channelName);
pump(channelA, channelB, channelA, (error) =>
console.debug(
`MetaMask: Muxed traffic for channel "${channelName}" failed.`,
error,
),
)
);
}
// TODO:LegacyProvider: Delete
@ -144,14 +144,14 @@ function forwardNamedTrafficBetweenMuxes(
muxA,
muxB,
) {
const channelA = muxA.createStream(channelAName)
const channelB = muxB.createStream(channelBName)
const channelA = muxA.createStream(channelAName);
const channelB = muxB.createStream(channelBName);
pump(channelA, channelB, channelA, (error) =>
console.debug(
`MetaMask: Muxed traffic between channels "${channelAName}" and "${channelBName}" failed.`,
error,
),
)
);
}
// TODO:LegacyProvider: Delete
@ -159,13 +159,13 @@ function getNotificationTransformStream() {
return createThoughStream((chunk, _, cb) => {
if (chunk?.name === PROVIDER) {
if (chunk.data?.method === 'metamask_accountsChanged') {
chunk.data.method = 'wallet_accountsChanged'
chunk.data.result = chunk.data.params
delete chunk.data.params
chunk.data.method = 'wallet_accountsChanged';
chunk.data.result = chunk.data.params;
delete chunk.data.params;
}
}
cb(null, chunk)
})
cb(null, chunk);
});
}
/**
@ -178,7 +178,7 @@ function logStreamDisconnectWarning(remoteLabel, error) {
console.debug(
`MetaMask: Content script lost connection to "${remoteLabel}".`,
error,
)
);
}
/**
@ -200,7 +200,7 @@ function notifyInpageOfStreamFailure() {
},
},
window.location.origin,
)
);
}
/**
@ -214,7 +214,7 @@ function shouldInjectProvider() {
suffixCheck() &&
documentElementCheck() &&
!blockedDomainCheck()
)
);
}
/**
@ -223,11 +223,11 @@ function shouldInjectProvider() {
* @returns {boolean} {@code true} if the doctype is html or if none exists
*/
function doctypeCheck() {
const { doctype } = window.document
const { doctype } = window.document;
if (doctype) {
return doctype.name === 'html'
return doctype.name === 'html';
}
return true
return true;
}
/**
@ -240,14 +240,14 @@ function doctypeCheck() {
* @returns {boolean} whether or not the extension of the current document is prohibited
*/
function suffixCheck() {
const prohibitedTypes = [/\.xml$/u, /\.pdf$/u]
const currentUrl = window.location.pathname
const prohibitedTypes = [/\.xml$/u, /\.pdf$/u];
const currentUrl = window.location.pathname;
for (let i = 0; i < prohibitedTypes.length; i++) {
if (prohibitedTypes[i].test(currentUrl)) {
return false
return false;
}
}
return true
return true;
}
/**
@ -256,11 +256,11 @@ function suffixCheck() {
* @returns {boolean} {@code true} if the documentElement is an html node or if none exists
*/
function documentElementCheck() {
const documentElement = document.documentElement.nodeName
const documentElement = document.documentElement.nodeName;
if (documentElement) {
return documentElement.toLowerCase() === 'html'
return documentElement.toLowerCase() === 'html';
}
return true
return true;
}
/**
@ -280,30 +280,30 @@ function blockedDomainCheck() {
'ani.gamer.com.tw',
'blueskybooking.com',
'sharefile.com',
]
const currentUrl = window.location.href
let currentRegex
];
const currentUrl = window.location.href;
let currentRegex;
for (let i = 0; i < blockedDomains.length; i++) {
const blockedDomain = blockedDomains[i].replace('.', '\\.')
const blockedDomain = blockedDomains[i].replace('.', '\\.');
currentRegex = new RegExp(
`(?:https?:\\/\\/)(?:(?!${blockedDomain}).)*$`,
'u',
)
);
if (!currentRegex.test(currentUrl)) {
return true
return true;
}
}
return false
return false;
}
/**
* Redirects the current page to a phishing information page
*/
function redirectToPhishingWarning() {
console.debug('MetaMask: Routing to Phishing Warning component.')
const extensionURL = extension.runtime.getURL('phishing.html')
console.debug('MetaMask: Routing to Phishing Warning component.');
const extensionURL = extension.runtime.getURL('phishing.html');
window.location.href = `${extensionURL}#${querystring.stringify({
hostname: window.location.hostname,
href: window.location.href,
})}`
})}`;
}

@ -1,8 +1,8 @@
import { ObservableStore } from '@metamask/obs-store'
import { ObservableStore } from '@metamask/obs-store';
import {
TOGGLEABLE_ALERT_TYPES,
WEB3_SHIM_USAGE_ALERT_STATES,
} from '../../../shared/constants/alerts'
} from '../../../shared/constants/alerts';
/**
* @typedef {Object} AlertControllerInitState
@ -21,14 +21,14 @@ import {
const defaultState = {
alertEnabledness: TOGGLEABLE_ALERT_TYPES.reduce(
(alertEnabledness, alertType) => {
alertEnabledness[alertType] = true
return alertEnabledness
alertEnabledness[alertType] = true;
return alertEnabledness;
},
{},
),
unconnectedAccountAlertShownOrigins: {},
web3ShimUsageOrigins: {},
}
};
/**
* Controller responsible for maintaining alert-related state.
@ -39,36 +39,36 @@ export default class AlertController {
* @param {AlertControllerOptions} [opts] - Controller configuration parameters
*/
constructor(opts = {}) {
const { initState = {}, preferencesStore } = opts
const { initState = {}, preferencesStore } = opts;
const state = {
...defaultState,
alertEnabledness: {
...defaultState.alertEnabledness,
...initState.alertEnabledness,
},
}
};
this.store = new ObservableStore(state)
this.store = new ObservableStore(state);
this.selectedAddress = preferencesStore.getState().selectedAddress
this.selectedAddress = preferencesStore.getState().selectedAddress;
preferencesStore.subscribe(({ selectedAddress }) => {
const currentState = this.store.getState()
const currentState = this.store.getState();
if (
currentState.unconnectedAccountAlertShownOrigins &&
this.selectedAddress !== selectedAddress
) {
this.selectedAddress = selectedAddress
this.store.updateState({ unconnectedAccountAlertShownOrigins: {} })
this.selectedAddress = selectedAddress;
this.store.updateState({ unconnectedAccountAlertShownOrigins: {} });
}
})
});
}
setAlertEnabledness(alertId, enabledness) {
let { alertEnabledness } = this.store.getState()
alertEnabledness = { ...alertEnabledness }
alertEnabledness[alertId] = enabledness
this.store.updateState({ alertEnabledness })
let { alertEnabledness } = this.store.getState();
alertEnabledness = { ...alertEnabledness };
alertEnabledness[alertId] = enabledness;
this.store.updateState({ alertEnabledness });
}
/**
@ -76,12 +76,12 @@ export default class AlertController {
* @param {string} origin - The origin the alert has been shown for
*/
setUnconnectedAccountAlertShown(origin) {
let { unconnectedAccountAlertShownOrigins } = this.store.getState()
let { unconnectedAccountAlertShownOrigins } = this.store.getState();
unconnectedAccountAlertShownOrigins = {
...unconnectedAccountAlertShownOrigins,
}
unconnectedAccountAlertShownOrigins[origin] = true
this.store.updateState({ unconnectedAccountAlertShownOrigins })
};
unconnectedAccountAlertShownOrigins[origin] = true;
this.store.updateState({ unconnectedAccountAlertShownOrigins });
}
/**
@ -92,7 +92,7 @@ export default class AlertController {
* origin, or undefined.
*/
getWeb3ShimUsageState(origin) {
return this.store.getState().web3ShimUsageOrigins[origin]
return this.store.getState().web3ShimUsageOrigins[origin];
}
/**
@ -101,7 +101,7 @@ export default class AlertController {
* @param {string} origin - The origin the that used the web3 shim.
*/
setWeb3ShimUsageRecorded(origin) {
this._setWeb3ShimUsageState(origin, WEB3_SHIM_USAGE_ALERT_STATES.RECORDED)
this._setWeb3ShimUsageState(origin, WEB3_SHIM_USAGE_ALERT_STATES.RECORDED);
}
/**
@ -111,7 +111,7 @@ export default class AlertController {
* dismissed for.
*/
setWeb3ShimUsageAlertDismissed(origin) {
this._setWeb3ShimUsageState(origin, WEB3_SHIM_USAGE_ALERT_STATES.DISMISSED)
this._setWeb3ShimUsageState(origin, WEB3_SHIM_USAGE_ALERT_STATES.DISMISSED);
}
/**
@ -120,11 +120,11 @@ export default class AlertController {
* @param {number} value - The state value to set.
*/
_setWeb3ShimUsageState(origin, value) {
let { web3ShimUsageOrigins } = this.store.getState()
let { web3ShimUsageOrigins } = this.store.getState();
web3ShimUsageOrigins = {
...web3ShimUsageOrigins,
}
web3ShimUsageOrigins[origin] = value
this.store.updateState({ web3ShimUsageOrigins })
};
web3ShimUsageOrigins[origin] = value;
this.store.updateState({ web3ShimUsageOrigins });
}
}

@ -1,5 +1,5 @@
import EventEmitter from 'events'
import { ObservableStore } from '@metamask/obs-store'
import EventEmitter from 'events';
import { ObservableStore } from '@metamask/obs-store';
export default class AppStateController extends EventEmitter {
/**
@ -14,34 +14,34 @@ export default class AppStateController extends EventEmitter {
onInactiveTimeout,
showUnlockRequest,
preferencesStore,
} = opts
super()
} = opts;
super();
this.onInactiveTimeout = onInactiveTimeout || (() => undefined)
this.onInactiveTimeout = onInactiveTimeout || (() => undefined);
this.store = new ObservableStore({
timeoutMinutes: 0,
connectedStatusPopoverHasBeenShown: true,
swapsWelcomeMessageHasBeenShown: false,
defaultHomeActiveTabName: null,
...initState,
})
this.timer = null
});
this.timer = null;
this.isUnlocked = isUnlocked
this.waitingForUnlock = []
addUnlockListener(this.handleUnlock.bind(this))
this.isUnlocked = isUnlocked;
this.waitingForUnlock = [];
addUnlockListener(this.handleUnlock.bind(this));
this._showUnlockRequest = showUnlockRequest
this._showUnlockRequest = showUnlockRequest;
preferencesStore.subscribe(({ preferences }) => {
const currentState = this.store.getState()
const currentState = this.store.getState();
if (currentState.timeoutMinutes !== preferences.autoLockTimeLimit) {
this._setInactiveTimeout(preferences.autoLockTimeLimit)
this._setInactiveTimeout(preferences.autoLockTimeLimit);
}
})
});
const { preferences } = preferencesStore.getState()
this._setInactiveTimeout(preferences.autoLockTimeLimit)
const { preferences } = preferencesStore.getState();
this._setInactiveTimeout(preferences.autoLockTimeLimit);
}
/**
@ -56,11 +56,11 @@ export default class AppStateController extends EventEmitter {
getUnlockPromise(shouldShowUnlockRequest) {
return new Promise((resolve) => {
if (this.isUnlocked()) {
resolve()
resolve();
} else {
this.waitForUnlock(resolve, shouldShowUnlockRequest)
this.waitForUnlock(resolve, shouldShowUnlockRequest);
}
})
});
}
/**
@ -73,10 +73,10 @@ export default class AppStateController extends EventEmitter {
* popup should be opened.
*/
waitForUnlock(resolve, shouldShowUnlockRequest) {
this.waitingForUnlock.push({ resolve })
this.emit('updateBadge')
this.waitingForUnlock.push({ resolve });
this.emit('updateBadge');
if (shouldShowUnlockRequest) {
this._showUnlockRequest()
this._showUnlockRequest();
}
}
@ -86,9 +86,9 @@ export default class AppStateController extends EventEmitter {
handleUnlock() {
if (this.waitingForUnlock.length > 0) {
while (this.waitingForUnlock.length > 0) {
this.waitingForUnlock.shift().resolve()
this.waitingForUnlock.shift().resolve();
}
this.emit('updateBadge')
this.emit('updateBadge');
}
}
@ -99,7 +99,7 @@ export default class AppStateController extends EventEmitter {
setDefaultHomeActiveTabName(defaultHomeActiveTabName) {
this.store.updateState({
defaultHomeActiveTabName,
})
});
}
/**
@ -108,7 +108,7 @@ export default class AppStateController extends EventEmitter {
setConnectedStatusPopoverHasBeenShown() {
this.store.updateState({
connectedStatusPopoverHasBeenShown: true,
})
});
}
/**
@ -117,7 +117,7 @@ export default class AppStateController extends EventEmitter {
setSwapsWelcomeMessageHasBeenShown() {
this.store.updateState({
swapsWelcomeMessageHasBeenShown: true,
})
});
}
/**
@ -125,7 +125,7 @@ export default class AppStateController extends EventEmitter {
* @returns {void}
*/
setLastActiveTime() {
this._resetTimer()
this._resetTimer();
}
/**
@ -137,9 +137,9 @@ export default class AppStateController extends EventEmitter {
_setInactiveTimeout(timeoutMinutes) {
this.store.updateState({
timeoutMinutes,
})
});
this._resetTimer()
this._resetTimer();
}
/**
@ -152,19 +152,19 @@ export default class AppStateController extends EventEmitter {
* @private
*/
_resetTimer() {
const { timeoutMinutes } = this.store.getState()
const { timeoutMinutes } = this.store.getState();
if (this.timer) {
clearTimeout(this.timer)
clearTimeout(this.timer);
}
if (!timeoutMinutes) {
return
return;
}
this.timer = setTimeout(
() => this.onInactiveTimeout(),
timeoutMinutes * 60 * 1000,
)
);
}
}

@ -1,4 +1,4 @@
import { ObservableStore } from '@metamask/obs-store'
import { ObservableStore } from '@metamask/obs-store';
/**
* @typedef {Object} CachedBalancesOptions
@ -18,15 +18,15 @@ export default class CachedBalancesController {
* @param {CachedBalancesOptions} [opts] - Controller configuration parameters
*/
constructor(opts = {}) {
const { accountTracker, getNetwork } = opts
const { accountTracker, getNetwork } = opts;
this.accountTracker = accountTracker
this.getNetwork = getNetwork
this.accountTracker = accountTracker;
this.getNetwork = getNetwork;
const initState = { cachedBalances: {}, ...opts.initState }
this.store = new ObservableStore(initState)
const initState = { cachedBalances: {}, ...opts.initState };
this.store = new ObservableStore(initState);
this._registerUpdates()
this._registerUpdates();
}
/**
@ -37,33 +37,33 @@ export default class CachedBalancesController {
* @returns {Promise<void>}
*/
async updateCachedBalances({ accounts }) {
const network = await this.getNetwork()
const network = await this.getNetwork();
const balancesToCache = await this._generateBalancesToCache(
accounts,
network,
)
);
this.store.updateState({
cachedBalances: balancesToCache,
})
});
}
_generateBalancesToCache(newAccounts, currentNetwork) {
const { cachedBalances } = this.store.getState()
const currentNetworkBalancesToCache = { ...cachedBalances[currentNetwork] }
const { cachedBalances } = this.store.getState();
const currentNetworkBalancesToCache = { ...cachedBalances[currentNetwork] };
Object.keys(newAccounts).forEach((accountID) => {
const account = newAccounts[accountID]
const account = newAccounts[accountID];
if (account.balance) {
currentNetworkBalancesToCache[accountID] = account.balance
currentNetworkBalancesToCache[accountID] = account.balance;
}
})
});
const balancesToCache = {
...cachedBalances,
[currentNetwork]: currentNetworkBalancesToCache,
}
};
return balancesToCache
return balancesToCache;
}
/**
@ -71,7 +71,7 @@ export default class CachedBalancesController {
*/
clearCachedBalances() {
this.store.updateState({ cachedBalances: {} })
this.store.updateState({ cachedBalances: {} });
}
/**
@ -83,7 +83,7 @@ export default class CachedBalancesController {
*
*/
_registerUpdates() {
const update = this.updateCachedBalances.bind(this)
this.accountTracker.store.subscribe(update)
const update = this.updateCachedBalances.bind(this);
this.accountTracker.store.subscribe(update);
}
}

@ -1,12 +1,12 @@
import Web3 from 'web3'
import contracts from '@metamask/contract-metadata'
import { warn } from 'loglevel'
import SINGLE_CALL_BALANCES_ABI from 'single-call-balance-checker-abi'
import { MAINNET_CHAIN_ID } from '../../../shared/constants/network'
import { SINGLE_CALL_BALANCES_ADDRESS } from '../constants/contracts'
import Web3 from 'web3';
import contracts from '@metamask/contract-metadata';
import { warn } from 'loglevel';
import SINGLE_CALL_BALANCES_ABI from 'single-call-balance-checker-abi';
import { MAINNET_CHAIN_ID } from '../../../shared/constants/network';
import { SINGLE_CALL_BALANCES_ADDRESS } from '../constants/contracts';
// By default, poll every 3 minutes
const DEFAULT_INTERVAL = 180 * 1000
const DEFAULT_INTERVAL = 180 * 1000;
/**
* A controller that polls for token exchange
@ -24,10 +24,10 @@ export default class DetectTokensController {
network,
keyringMemStore,
} = {}) {
this.preferences = preferences
this.interval = interval
this.network = network
this.keyringMemStore = keyringMemStore
this.preferences = preferences;
this.interval = interval;
this.network = network;
this.keyringMemStore = keyringMemStore;
}
/**
@ -35,59 +35,59 @@ export default class DetectTokensController {
*/
async detectNewTokens() {
if (!this.isActive) {
return
return;
}
if (this._network.store.getState().provider.chainId !== MAINNET_CHAIN_ID) {
return
return;
}
const tokensToDetect = []
this.web3.setProvider(this._network._provider)
const tokensToDetect = [];
this.web3.setProvider(this._network._provider);
for (const contractAddress in contracts) {
if (
contracts[contractAddress].erc20 &&
!this.tokenAddresses.includes(contractAddress.toLowerCase()) &&
!this.hiddenTokens.includes(contractAddress.toLowerCase())
) {
tokensToDetect.push(contractAddress)
tokensToDetect.push(contractAddress);
}
}
let result
let result;
try {
result = await this._getTokenBalances(tokensToDetect)
result = await this._getTokenBalances(tokensToDetect);
} catch (error) {
warn(
`MetaMask - DetectTokensController single call balance fetch failed`,
error,
)
return
);
return;
}
tokensToDetect.forEach((tokenAddress, index) => {
const balance = result[index]
const balance = result[index];
if (balance && !balance.isZero()) {
this._preferences.addToken(
tokenAddress,
contracts[tokenAddress].symbol,
contracts[tokenAddress].decimals,
)
);
}
})
});
}
async _getTokenBalances(tokens) {
const ethContract = this.web3.eth
.contract(SINGLE_CALL_BALANCES_ABI)
.at(SINGLE_CALL_BALANCES_ADDRESS)
.at(SINGLE_CALL_BALANCES_ADDRESS);
return new Promise((resolve, reject) => {
ethContract.balances([this.selectedAddress], tokens, (error, result) => {
if (error) {
return reject(error)
return reject(error);
}
return resolve(result)
})
})
return resolve(result);
});
});
}
/**
@ -97,10 +97,10 @@ export default class DetectTokensController {
*/
restartTokenDetection() {
if (!(this.isActive && this.selectedAddress)) {
return
return;
}
this.detectNewTokens()
this.interval = DEFAULT_INTERVAL
this.detectNewTokens();
this.interval = DEFAULT_INTERVAL;
}
/* eslint-disable accessor-pairs */
@ -108,13 +108,13 @@ export default class DetectTokensController {
* @type {Number}
*/
set interval(interval) {
this._handle && clearInterval(this._handle)
this._handle && clearInterval(this._handle);
if (!interval) {
return
return;
}
this._handle = setInterval(() => {
this.detectNewTokens()
}, interval)
this.detectNewTokens();
}, interval);
}
/**
@ -123,26 +123,26 @@ export default class DetectTokensController {
*/
set preferences(preferences) {
if (!preferences) {
return
return;
}
this._preferences = preferences
const currentTokens = preferences.store.getState().tokens
this._preferences = preferences;
const currentTokens = preferences.store.getState().tokens;
this.tokenAddresses = currentTokens
? currentTokens.map((token) => token.address)
: []
this.hiddenTokens = preferences.store.getState().hiddenTokens
: [];
this.hiddenTokens = preferences.store.getState().hiddenTokens;
preferences.store.subscribe(({ tokens = [], hiddenTokens = [] }) => {
this.tokenAddresses = tokens.map((token) => {
return token.address
})
this.hiddenTokens = hiddenTokens
})
return token.address;
});
this.hiddenTokens = hiddenTokens;
});
preferences.store.subscribe(({ selectedAddress }) => {
if (this.selectedAddress !== selectedAddress) {
this.selectedAddress = selectedAddress
this.restartTokenDetection()
this.selectedAddress = selectedAddress;
this.restartTokenDetection();
}
})
});
}
/**
@ -150,10 +150,10 @@ export default class DetectTokensController {
*/
set network(network) {
if (!network) {
return
return;
}
this._network = network
this.web3 = new Web3(network._provider)
this._network = network;
this.web3 = new Web3(network._provider);
}
/**
@ -162,17 +162,17 @@ export default class DetectTokensController {
*/
set keyringMemStore(keyringMemStore) {
if (!keyringMemStore) {
return
return;
}
this._keyringMemStore = keyringMemStore
this._keyringMemStore = keyringMemStore;
this._keyringMemStore.subscribe(({ isUnlocked }) => {
if (this.isUnlocked !== isUnlocked) {
this.isUnlocked = isUnlocked
this.isUnlocked = isUnlocked;
if (isUnlocked) {
this.restartTokenDetection()
this.restartTokenDetection();
}
}
})
});
}
/**
@ -180,7 +180,7 @@ export default class DetectTokensController {
* @type {Object}
*/
get isActive() {
return this.isOpen && this.isUnlocked
return this.isOpen && this.isUnlocked;
}
/* eslint-enable accessor-pairs */
}

@ -1,23 +1,23 @@
import EthJsEns from 'ethjs-ens'
import ensNetworkMap from 'ethereum-ens-network-map'
import EthJsEns from 'ethjs-ens';
import ensNetworkMap from 'ethereum-ens-network-map';
export default class Ens {
static getNetworkEnsSupport(network) {
return Boolean(ensNetworkMap[network])
return Boolean(ensNetworkMap[network]);
}
constructor({ network, provider } = {}) {
this._ethJsEns = new EthJsEns({
network,
provider,
})
});
}
lookup(ensName) {
return this._ethJsEns.lookup(ensName)
return this._ethJsEns.lookup(ensName);
}
reverse(address) {
return this._ethJsEns.reverse(address)
return this._ethJsEns.reverse(address);
}
}

@ -1,95 +1,95 @@
import punycode from 'punycode/punycode'
import ethUtil from 'ethereumjs-util'
import { ObservableStore } from '@metamask/obs-store'
import log from 'loglevel'
import Ens from './ens'
import punycode from 'punycode/punycode';
import ethUtil from 'ethereumjs-util';
import { ObservableStore } from '@metamask/obs-store';
import log from 'loglevel';
import Ens from './ens';
const ZERO_ADDRESS = '0x0000000000000000000000000000000000000000'
const ZERO_X_ERROR_ADDRESS = '0x'
const ZERO_ADDRESS = '0x0000000000000000000000000000000000000000';
const ZERO_X_ERROR_ADDRESS = '0x';
export default class EnsController {
constructor({ ens, provider, networkStore } = {}) {
const initState = {
ensResolutionsByAddress: {},
}
};
this._ens = ens
this._ens = ens;
if (!this._ens) {
const network = networkStore.getState()
const network = networkStore.getState();
if (Ens.getNetworkEnsSupport(network)) {
this._ens = new Ens({
network,
provider,
})
});
}
}
this.store = new ObservableStore(initState)
this.store = new ObservableStore(initState);
networkStore.subscribe((network) => {
this.store.putState(initState)
this.store.putState(initState);
if (Ens.getNetworkEnsSupport(network)) {
this._ens = new Ens({
network,
provider,
})
});
} else {
delete this._ens
delete this._ens;
}
})
});
}
reverseResolveAddress(address) {
return this._reverseResolveAddress(ethUtil.toChecksumAddress(address))
return this._reverseResolveAddress(ethUtil.toChecksumAddress(address));
}
async _reverseResolveAddress(address) {
if (!this._ens) {
return undefined
return undefined;
}
const state = this.store.getState()
const state = this.store.getState();
if (state.ensResolutionsByAddress[address]) {
return state.ensResolutionsByAddress[address]
return state.ensResolutionsByAddress[address];
}
let domain
let domain;
try {
domain = await this._ens.reverse(address)
domain = await this._ens.reverse(address);
} catch (error) {
log.debug(error)
return undefined
log.debug(error);
return undefined;
}
let registeredAddress
let registeredAddress;
try {
registeredAddress = await this._ens.lookup(domain)
registeredAddress = await this._ens.lookup(domain);
} catch (error) {
log.debug(error)
return undefined
log.debug(error);
return undefined;
}
if (
registeredAddress === ZERO_ADDRESS ||
registeredAddress === ZERO_X_ERROR_ADDRESS
) {
return undefined
return undefined;
}
if (ethUtil.toChecksumAddress(registeredAddress) !== address) {
return undefined
return undefined;
}
this._updateResolutionsByAddress(address, punycode.toASCII(domain))
return domain
this._updateResolutionsByAddress(address, punycode.toASCII(domain));
return domain;
}
_updateResolutionsByAddress(address, domain) {
const oldState = this.store.getState()
const oldState = this.store.getState();
this.store.putState({
ensResolutionsByAddress: {
...oldState.ensResolutionsByAddress,
[address]: domain,
},
})
});
}
}

@ -1,14 +1,14 @@
import { ObservableStore } from '@metamask/obs-store'
import log from 'loglevel'
import BN from 'bn.js'
import createId from '../lib/random-id'
import { bnToHex } from '../lib/util'
import getFetchWithTimeout from '../../../shared/modules/fetch-with-timeout'
import { ObservableStore } from '@metamask/obs-store';
import log from 'loglevel';
import BN from 'bn.js';
import createId from '../lib/random-id';
import { bnToHex } from '../lib/util';
import getFetchWithTimeout from '../../../shared/modules/fetch-with-timeout';
import {
TRANSACTION_CATEGORIES,
TRANSACTION_STATUSES,
} from '../../../shared/constants/transaction'
} from '../../../shared/constants/transaction';
import {
CHAIN_ID_TO_NETWORK_ID_MAP,
CHAIN_ID_TO_TYPE_MAP,
@ -22,9 +22,9 @@ import {
RINKEBY_CHAIN_ID,
ROPSTEN,
ROPSTEN_CHAIN_ID,
} from '../../../shared/constants/network'
} from '../../../shared/constants/network';
const fetchWithTimeout = getFetchWithTimeout(30000)
const fetchWithTimeout = getFetchWithTimeout(30000);
/**
* This controller is responsible for retrieving incoming transactions. Etherscan is polled once every block to check
@ -39,23 +39,23 @@ const etherscanSupportedNetworks = [
MAINNET_CHAIN_ID,
RINKEBY_CHAIN_ID,
ROPSTEN_CHAIN_ID,
]
];
export default class IncomingTransactionsController {
constructor(opts = {}) {
const { blockTracker, networkController, preferencesController } = opts
this.blockTracker = blockTracker
this.networkController = networkController
this.preferencesController = preferencesController
const { blockTracker, networkController, preferencesController } = opts;
this.blockTracker = blockTracker;
this.networkController = networkController;
this.preferencesController = preferencesController;
this._onLatestBlock = async (newBlockNumberHex) => {
const selectedAddress = this.preferencesController.getSelectedAddress()
const newBlockNumberDec = parseInt(newBlockNumberHex, 16)
const selectedAddress = this.preferencesController.getSelectedAddress();
const newBlockNumberDec = parseInt(newBlockNumberHex, 16);
await this._update({
address: selectedAddress,
newBlockNumberDec,
})
}
});
};
const initState = {
incomingTransactions: {},
@ -67,8 +67,8 @@ export default class IncomingTransactionsController {
[ROPSTEN]: null,
},
...opts.initState,
}
this.store = new ObservableStore(initState)
};
this.store = new ObservableStore(initState);
this.preferencesController.store.subscribe(
pairwise((prevState, currState) => {
@ -76,79 +76,79 @@ export default class IncomingTransactionsController {
featureFlags: {
showIncomingTransactions: prevShowIncomingTransactions,
} = {},
} = prevState
} = prevState;
const {
featureFlags: {
showIncomingTransactions: currShowIncomingTransactions,
} = {},
} = currState
} = currState;
if (currShowIncomingTransactions === prevShowIncomingTransactions) {
return
return;
}
if (prevShowIncomingTransactions && !currShowIncomingTransactions) {
this.stop()
return
this.stop();
return;
}
this.start()
this.start();
}),
)
);
this.preferencesController.store.subscribe(
pairwise(async (prevState, currState) => {
const { selectedAddress: prevSelectedAddress } = prevState
const { selectedAddress: currSelectedAddress } = currState
const { selectedAddress: prevSelectedAddress } = prevState;
const { selectedAddress: currSelectedAddress } = currState;
if (currSelectedAddress === prevSelectedAddress) {
return
return;
}
await this._update({
address: currSelectedAddress,
})
});
}),
)
);
this.networkController.on('networkDidChange', async () => {
const address = this.preferencesController.getSelectedAddress()
const address = this.preferencesController.getSelectedAddress();
await this._update({
address,
})
})
});
});
}
start() {
const { featureFlags = {} } = this.preferencesController.store.getState()
const { showIncomingTransactions } = featureFlags
const { featureFlags = {} } = this.preferencesController.store.getState();
const { showIncomingTransactions } = featureFlags;
if (!showIncomingTransactions) {
return
return;
}
this.blockTracker.removeListener('latest', this._onLatestBlock)
this.blockTracker.addListener('latest', this._onLatestBlock)
this.blockTracker.removeListener('latest', this._onLatestBlock);
this.blockTracker.addListener('latest', this._onLatestBlock);
}
stop() {
this.blockTracker.removeListener('latest', this._onLatestBlock)
this.blockTracker.removeListener('latest', this._onLatestBlock);
}
async _update({ address, newBlockNumberDec } = {}) {
const chainId = this.networkController.getCurrentChainId()
const chainId = this.networkController.getCurrentChainId();
if (!etherscanSupportedNetworks.includes(chainId)) {
return
return;
}
try {
const dataForUpdate = await this._getDataForUpdate({
address,
chainId,
newBlockNumberDec,
})
this._updateStateWithNewTxData(dataForUpdate)
});
this._updateStateWithNewTxData(dataForUpdate);
} catch (err) {
log.error(err)
log.error(err);
}
}
@ -156,20 +156,20 @@ export default class IncomingTransactionsController {
const {
incomingTransactions: currentIncomingTxs,
incomingTxLastFetchedBlocksByNetwork: currentBlocksByNetwork,
} = this.store.getState()
} = this.store.getState();
const lastFetchBlockByCurrentNetwork =
currentBlocksByNetwork[CHAIN_ID_TO_TYPE_MAP[chainId]]
let blockToFetchFrom = lastFetchBlockByCurrentNetwork || newBlockNumberDec
currentBlocksByNetwork[CHAIN_ID_TO_TYPE_MAP[chainId]];
let blockToFetchFrom = lastFetchBlockByCurrentNetwork || newBlockNumberDec;
if (blockToFetchFrom === undefined) {
blockToFetchFrom = parseInt(this.blockTracker.getCurrentBlock(), 16)
blockToFetchFrom = parseInt(this.blockTracker.getCurrentBlock(), 16);
}
const { latestIncomingTxBlockNumber, txs: newTxs } = await this._fetchAll(
address,
blockToFetchFrom,
chainId,
)
);
return {
latestIncomingTxBlockNumber,
@ -178,7 +178,7 @@ export default class IncomingTransactionsController {
currentBlocksByNetwork,
fetchedBlockNumber: blockToFetchFrom,
chainId,
}
};
}
_updateStateWithNewTxData({
@ -191,13 +191,13 @@ export default class IncomingTransactionsController {
}) {
const newLatestBlockHashByNetwork = latestIncomingTxBlockNumber
? parseInt(latestIncomingTxBlockNumber, 10) + 1
: fetchedBlockNumber + 1
: fetchedBlockNumber + 1;
const newIncomingTransactions = {
...currentIncomingTxs,
}
};
newTxs.forEach((tx) => {
newIncomingTransactions[tx.hash] = tx
})
newIncomingTransactions[tx.hash] = tx;
});
this.store.updateState({
incomingTxLastFetchedBlocksByNetwork: {
@ -205,53 +205,53 @@ export default class IncomingTransactionsController {
[CHAIN_ID_TO_TYPE_MAP[chainId]]: newLatestBlockHashByNetwork,
},
incomingTransactions: newIncomingTransactions,
})
});
}
async _fetchAll(address, fromBlock, chainId) {
const fetchedTxResponse = await this._fetchTxs(address, fromBlock, chainId)
return this._processTxFetchResponse(fetchedTxResponse)
const fetchedTxResponse = await this._fetchTxs(address, fromBlock, chainId);
return this._processTxFetchResponse(fetchedTxResponse);
}
async _fetchTxs(address, fromBlock, chainId) {
const etherscanSubdomain =
chainId === MAINNET_CHAIN_ID
? 'api'
: `api-${CHAIN_ID_TO_TYPE_MAP[chainId]}`
: `api-${CHAIN_ID_TO_TYPE_MAP[chainId]}`;
const apiUrl = `https://${etherscanSubdomain}.etherscan.io`
let url = `${apiUrl}/api?module=account&action=txlist&address=${address}&tag=latest&page=1`
const apiUrl = `https://${etherscanSubdomain}.etherscan.io`;
let url = `${apiUrl}/api?module=account&action=txlist&address=${address}&tag=latest&page=1`;
if (fromBlock) {
url += `&startBlock=${parseInt(fromBlock, 10)}`
url += `&startBlock=${parseInt(fromBlock, 10)}`;
}
const response = await fetchWithTimeout(url)
const parsedResponse = await response.json()
const response = await fetchWithTimeout(url);
const parsedResponse = await response.json();
return {
...parsedResponse,
address,
chainId,
}
};
}
_processTxFetchResponse({ status, result = [], address, chainId }) {
if (status === '1' && Array.isArray(result) && result.length > 0) {
const remoteTxList = {}
const remoteTxs = []
const remoteTxList = {};
const remoteTxs = [];
result.forEach((tx) => {
if (!remoteTxList[tx.hash]) {
remoteTxs.push(this._normalizeTxFromEtherscan(tx, chainId))
remoteTxList[tx.hash] = 1
remoteTxs.push(this._normalizeTxFromEtherscan(tx, chainId));
remoteTxList[tx.hash] = 1;
}
})
});
const incomingTxs = remoteTxs.filter(
(tx) => tx.txParams?.to?.toLowerCase() === address.toLowerCase(),
)
incomingTxs.sort((a, b) => (a.time < b.time ? -1 : 1))
);
incomingTxs.sort((a, b) => (a.time < b.time ? -1 : 1));
let latestIncomingTxBlockNumber = null
let latestIncomingTxBlockNumber = null;
incomingTxs.forEach((tx) => {
if (
tx.blockNumber &&
@ -259,26 +259,26 @@ export default class IncomingTransactionsController {
parseInt(latestIncomingTxBlockNumber, 10) <
parseInt(tx.blockNumber, 10))
) {
latestIncomingTxBlockNumber = tx.blockNumber
latestIncomingTxBlockNumber = tx.blockNumber;
}
})
});
return {
latestIncomingTxBlockNumber,
txs: incomingTxs,
}
};
}
return {
latestIncomingTxBlockNumber: null,
txs: [],
}
};
}
_normalizeTxFromEtherscan(txMeta, chainId) {
const time = parseInt(txMeta.timeStamp, 10) * 1000
const time = parseInt(txMeta.timeStamp, 10) * 1000;
const status =
txMeta.isError === '0'
? TRANSACTION_STATUSES.CONFIRMED
: TRANSACTION_STATUSES.FAILED
: TRANSACTION_STATUSES.FAILED;
return {
blockNumber: txMeta.blockNumber,
id: createId(),
@ -295,22 +295,22 @@ export default class IncomingTransactionsController {
},
hash: txMeta.hash,
transactionCategory: TRANSACTION_CATEGORIES.INCOMING,
}
};
}
}
function pairwise(fn) {
let first = true
let cache
let first = true;
let cache;
return (value) => {
try {
if (first) {
first = false
return fn(value, value)
first = false;
return fn(value, value);
}
return fn(cache, value)
return fn(cache, value);
} finally {
cache = value
}
cache = value;
}
};
}

@ -1,11 +1,11 @@
import { merge, omit } from 'lodash'
import { ObservableStore } from '@metamask/obs-store'
import { bufferToHex, sha3 } from 'ethereumjs-util'
import { ENVIRONMENT_TYPE_BACKGROUND } from '../../../shared/constants/app'
import { merge, omit } from 'lodash';
import { ObservableStore } from '@metamask/obs-store';
import { bufferToHex, sha3 } from 'ethereumjs-util';
import { ENVIRONMENT_TYPE_BACKGROUND } from '../../../shared/constants/app';
import {
METAMETRICS_ANONYMOUS_ID,
METAMETRICS_BACKGROUND_PAGE_OBJECT,
} from '../../../shared/constants/metametrics'
} from '../../../shared/constants/metametrics';
/**
* Used to determine whether or not to attach a user's metametrics id
@ -25,10 +25,10 @@ const trackableSendCounts = {
5000: true,
10000: true,
25000: true,
}
};
export function sendCountIsTrackable(sendCount) {
return Boolean(trackableSendCounts[sendCount])
return Boolean(trackableSendCounts[sendCount]);
}
/**
@ -82,30 +82,30 @@ export default class MetaMetricsController {
environment,
initState,
}) {
const prefState = preferencesStore.getState()
this.chainId = getCurrentChainId()
this.network = getNetworkIdentifier()
this.locale = prefState.currentLocale.replace('_', '-')
const prefState = preferencesStore.getState();
this.chainId = getCurrentChainId();
this.network = getNetworkIdentifier();
this.locale = prefState.currentLocale.replace('_', '-');
this.version =
environment === 'production' ? version : `${version}-${environment}`
environment === 'production' ? version : `${version}-${environment}`;
this.store = new ObservableStore({
participateInMetaMetrics: null,
metaMetricsId: null,
metaMetricsSendCount: 0,
...initState,
})
});
preferencesStore.subscribe(({ currentLocale }) => {
this.locale = currentLocale.replace('_', '-')
})
this.locale = currentLocale.replace('_', '-');
});
onNetworkDidChange(() => {
this.chainId = getCurrentChainId()
this.network = getNetworkIdentifier()
})
this.segment = segment
this.segmentLegacy = segmentLegacy
this.chainId = getCurrentChainId();
this.network = getNetworkIdentifier();
});
this.segment = segment;
this.segmentLegacy = segmentLegacy;
}
generateMetaMetricsId() {
@ -114,7 +114,7 @@ export default class MetaMetricsController {
String(Date.now()) +
String(Math.round(Math.random() * Number.MAX_SAFE_INTEGER)),
),
)
);
}
/**
@ -126,22 +126,22 @@ export default class MetaMetricsController {
* if not set
*/
setParticipateInMetaMetrics(participateInMetaMetrics) {
let { metaMetricsId } = this.state
let { metaMetricsId } = this.state;
if (participateInMetaMetrics && !metaMetricsId) {
metaMetricsId = this.generateMetaMetricsId()
metaMetricsId = this.generateMetaMetricsId();
} else if (participateInMetaMetrics === false) {
metaMetricsId = null
metaMetricsId = null;
}
this.store.updateState({ participateInMetaMetrics, metaMetricsId })
return metaMetricsId
this.store.updateState({ participateInMetaMetrics, metaMetricsId });
return metaMetricsId;
}
get state() {
return this.store.getState()
return this.store.getState();
}
setMetaMetricsSendCount(val) {
this.store.updateState({ metaMetricsSendCount: val })
this.store.updateState({ metaMetricsSendCount: val });
}
/**
@ -162,7 +162,7 @@ export default class MetaMetricsController {
userAgent: window.navigator.userAgent,
page,
referrer,
}
};
}
/**
@ -185,7 +185,7 @@ export default class MetaMetricsController {
page,
referrer,
environmentType = ENVIRONMENT_TYPE_BACKGROUND,
} = rawPayload
} = rawPayload;
return {
event,
properties: {
@ -206,7 +206,7 @@ export default class MetaMetricsController {
environment_type: environmentType,
},
context: this._buildContext(referrer, page),
}
};
}
/**
@ -225,20 +225,20 @@ export default class MetaMetricsController {
metaMetricsId: metaMetricsIdOverride,
matomoEvent,
flushImmediately,
} = options || {}
let idType = 'userId'
let idValue = this.state.metaMetricsId
let excludeMetaMetricsId = options?.excludeMetaMetricsId ?? false
} = options || {};
let idType = 'userId';
let idValue = this.state.metaMetricsId;
let excludeMetaMetricsId = options?.excludeMetaMetricsId ?? false;
// This is carried over from the old implementation, and will likely need
// to be updated to work with the new tracking plan. I think we should use
// a config setting for this instead of trying to match the event name
const isSendFlow = Boolean(payload.event.match(/^send|^confirm/iu))
const isSendFlow = Boolean(payload.event.match(/^send|^confirm/iu));
if (
isSendFlow &&
this.state.metaMetricsSendCount &&
!sendCountIsTrackable(this.state.metaMetricsSendCount + 1)
) {
excludeMetaMetricsId = true
excludeMetaMetricsId = true;
}
// If we are tracking sensitive data we will always use the anonymousId
// property as well as our METAMETRICS_ANONYMOUS_ID. This prevents us from
@ -251,12 +251,12 @@ export default class MetaMetricsController {
// case we will track the opt in event to the user's id. In all other cases
// we use the metaMetricsId from state.
if (excludeMetaMetricsId || (isOptIn && !metaMetricsIdOverride)) {
idType = 'anonymousId'
idValue = METAMETRICS_ANONYMOUS_ID
idType = 'anonymousId';
idValue = METAMETRICS_ANONYMOUS_ID;
} else if (isOptIn && metaMetricsIdOverride) {
idValue = metaMetricsIdOverride
idValue = metaMetricsIdOverride;
}
payload[idType] = idValue
payload[idType] = idValue;
// Promises will only resolve when the event is sent to segment. For any
// event that relies on this promise being fulfilled before performing UI
@ -269,20 +269,20 @@ export default class MetaMetricsController {
// that seemingly breaks with lockdown enabled. Creating a new error
// here prevents the system from freezing when the network request to
// segment fails for any reason.
const safeError = new Error(err.message)
safeError.stack = err.stack
return reject(safeError)
}
return resolve()
const safeError = new Error(err.message);
safeError.stack = err.stack;
return reject(safeError);
}
return resolve();
};
const target = matomoEvent === true ? this.segmentLegacy : this.segment
const target = matomoEvent === true ? this.segmentLegacy : this.segment;
target.track(payload, callback)
target.track(payload, callback);
if (flushImmediately) {
target.flush()
target.flush();
}
})
});
}
/**
@ -293,15 +293,15 @@ export default class MetaMetricsController {
*/
trackPage({ name, params, environmentType, page, referrer }, options) {
if (this.state.participateInMetaMetrics === false) {
return
return;
}
if (this.state.participateInMetaMetrics === null && !options?.isOptInPath) {
return
return;
}
const { metaMetricsId } = this.state
const idTrait = metaMetricsId ? 'userId' : 'anonymousId'
const idValue = metaMetricsId ?? METAMETRICS_ANONYMOUS_ID
const { metaMetricsId } = this.state;
const idTrait = metaMetricsId ? 'userId' : 'anonymousId';
const idValue = metaMetricsId ?? METAMETRICS_ANONYMOUS_ID;
this.segment.page({
[idTrait]: idValue,
name,
@ -313,7 +313,7 @@ export default class MetaMetricsController {
environment_type: environmentType,
},
context: this._buildContext(referrer, page),
})
});
}
/**
@ -328,16 +328,16 @@ export default class MetaMetricsController {
async trackEvent(payload, options) {
// event and category are required fields for all payloads
if (!payload.event || !payload.category) {
throw new Error('Must specify event and category.')
throw new Error('Must specify event and category.');
}
if (!this.state.participateInMetaMetrics && !options?.isOptIn) {
return
return;
}
// We might track multiple events if sensitiveProperties is included, this array will hold
// the promises returned from this._track.
const events = []
const events = [];
if (payload.sensitiveProperties) {
// sensitiveProperties will only be tracked using the anonymousId property and generic id
@ -346,13 +346,13 @@ export default class MetaMetricsController {
if (options?.excludeMetaMetricsId === true) {
throw new Error(
'sensitiveProperties was specified in an event payload that also set the excludeMetaMetricsId flag',
)
);
}
const combinedProperties = merge(
payload.sensitiveProperties,
payload.properties,
)
);
events.push(
this._track(
@ -362,11 +362,11 @@ export default class MetaMetricsController {
}),
{ ...options, excludeMetaMetricsId: true },
),
)
);
}
events.push(this._track(this._buildEventPayload(payload), options))
events.push(this._track(this._buildEventPayload(payload), options));
await Promise.all(events)
await Promise.all(events);
}
}

@ -1,14 +1,14 @@
import { createScaffoldMiddleware, mergeMiddleware } from 'json-rpc-engine'
import createBlockReRefMiddleware from 'eth-json-rpc-middleware/block-ref'
import createRetryOnEmptyMiddleware from 'eth-json-rpc-middleware/retryOnEmpty'
import createBlockCacheMiddleware from 'eth-json-rpc-middleware/block-cache'
import createInflightMiddleware from 'eth-json-rpc-middleware/inflight-cache'
import createBlockTrackerInspectorMiddleware from 'eth-json-rpc-middleware/block-tracker-inspector'
import providerFromMiddleware from 'eth-json-rpc-middleware/providerFromMiddleware'
import createInfuraMiddleware from 'eth-json-rpc-infura'
import BlockTracker from 'eth-block-tracker'
import { createScaffoldMiddleware, mergeMiddleware } from 'json-rpc-engine';
import createBlockReRefMiddleware from 'eth-json-rpc-middleware/block-ref';
import createRetryOnEmptyMiddleware from 'eth-json-rpc-middleware/retryOnEmpty';
import createBlockCacheMiddleware from 'eth-json-rpc-middleware/block-cache';
import createInflightMiddleware from 'eth-json-rpc-middleware/inflight-cache';
import createBlockTrackerInspectorMiddleware from 'eth-json-rpc-middleware/block-tracker-inspector';
import providerFromMiddleware from 'eth-json-rpc-middleware/providerFromMiddleware';
import createInfuraMiddleware from 'eth-json-rpc-infura';
import BlockTracker from 'eth-block-tracker';
import { NETWORK_TYPE_TO_ID_MAP } from '../../../../shared/constants/network'
import { NETWORK_TYPE_TO_ID_MAP } from '../../../../shared/constants/network';
export default function createInfuraClient({ network, projectId }) {
const infuraMiddleware = createInfuraMiddleware({
@ -16,9 +16,9 @@ export default function createInfuraClient({ network, projectId }) {
projectId,
maxAttempts: 5,
source: 'metamask',
})
const infuraProvider = providerFromMiddleware(infuraMiddleware)
const blockTracker = new BlockTracker({ provider: infuraProvider })
});
const infuraProvider = providerFromMiddleware(infuraMiddleware);
const blockTracker = new BlockTracker({ provider: infuraProvider });
const networkMiddleware = mergeMiddleware([
createNetworkAndChainIdMiddleware({ network }),
@ -28,19 +28,19 @@ export default function createInfuraClient({ network, projectId }) {
createRetryOnEmptyMiddleware({ blockTracker, provider: infuraProvider }),
createBlockTrackerInspectorMiddleware({ blockTracker }),
infuraMiddleware,
])
return { networkMiddleware, blockTracker }
]);
return { networkMiddleware, blockTracker };
}
function createNetworkAndChainIdMiddleware({ network }) {
if (!NETWORK_TYPE_TO_ID_MAP[network]) {
throw new Error(`createInfuraClient - unknown network "${network}"`)
throw new Error(`createInfuraClient - unknown network "${network}"`);
}
const { chainId, networkId } = NETWORK_TYPE_TO_ID_MAP[network]
const { chainId, networkId } = NETWORK_TYPE_TO_ID_MAP[network];
return createScaffoldMiddleware({
eth_chainId: chainId,
net_version: networkId,
})
});
}

@ -1,25 +1,25 @@
import { createAsyncMiddleware, mergeMiddleware } from 'json-rpc-engine'
import createFetchMiddleware from 'eth-json-rpc-middleware/fetch'
import createBlockRefRewriteMiddleware from 'eth-json-rpc-middleware/block-ref-rewrite'
import createBlockCacheMiddleware from 'eth-json-rpc-middleware/block-cache'
import createInflightMiddleware from 'eth-json-rpc-middleware/inflight-cache'
import createBlockTrackerInspectorMiddleware from 'eth-json-rpc-middleware/block-tracker-inspector'
import providerFromMiddleware from 'eth-json-rpc-middleware/providerFromMiddleware'
import BlockTracker from 'eth-block-tracker'
import { createAsyncMiddleware, mergeMiddleware } from 'json-rpc-engine';
import createFetchMiddleware from 'eth-json-rpc-middleware/fetch';
import createBlockRefRewriteMiddleware from 'eth-json-rpc-middleware/block-ref-rewrite';
import createBlockCacheMiddleware from 'eth-json-rpc-middleware/block-cache';
import createInflightMiddleware from 'eth-json-rpc-middleware/inflight-cache';
import createBlockTrackerInspectorMiddleware from 'eth-json-rpc-middleware/block-tracker-inspector';
import providerFromMiddleware from 'eth-json-rpc-middleware/providerFromMiddleware';
import BlockTracker from 'eth-block-tracker';
const inTest = process.env.IN_TEST === 'true'
const blockTrackerOpts = inTest ? { pollingInterval: 1000 } : {}
const inTest = process.env.IN_TEST === 'true';
const blockTrackerOpts = inTest ? { pollingInterval: 1000 } : {};
const getTestMiddlewares = () => {
return inTest ? [createEstimateGasDelayTestMiddleware()] : []
}
return inTest ? [createEstimateGasDelayTestMiddleware()] : [];
};
export default function createJsonRpcClient({ rpcUrl, chainId }) {
const fetchMiddleware = createFetchMiddleware({ rpcUrl })
const blockProvider = providerFromMiddleware(fetchMiddleware)
const fetchMiddleware = createFetchMiddleware({ rpcUrl });
const blockProvider = providerFromMiddleware(fetchMiddleware);
const blockTracker = new BlockTracker({
...blockTrackerOpts,
provider: blockProvider,
})
});
const networkMiddleware = mergeMiddleware([
...getTestMiddlewares(),
@ -29,19 +29,19 @@ export default function createJsonRpcClient({ rpcUrl, chainId }) {
createInflightMiddleware(),
createBlockTrackerInspectorMiddleware({ blockTracker }),
fetchMiddleware,
])
]);
return { networkMiddleware, blockTracker }
return { networkMiddleware, blockTracker };
}
function createChainIdMiddleware(chainId) {
return (req, res, next, end) => {
if (req.method === 'eth_chainId') {
res.result = chainId
return end()
}
return next()
res.result = chainId;
return end();
}
return next();
};
}
/**
@ -51,8 +51,8 @@ function createChainIdMiddleware(chainId) {
function createEstimateGasDelayTestMiddleware() {
return createAsyncMiddleware(async (req, _, next) => {
if (req.method === 'eth_estimateGas') {
await new Promise((resolve) => setTimeout(resolve, 2000))
await new Promise((resolve) => setTimeout(resolve, 2000));
}
return next()
})
return next();
});
}

@ -1,9 +1,9 @@
import { createScaffoldMiddleware, mergeMiddleware } from 'json-rpc-engine'
import createWalletSubprovider from 'eth-json-rpc-middleware/wallet'
import { createScaffoldMiddleware, mergeMiddleware } from 'json-rpc-engine';
import createWalletSubprovider from 'eth-json-rpc-middleware/wallet';
import {
createPendingNonceMiddleware,
createPendingTxMiddleware,
} from './middleware/pending'
} from './middleware/pending';
export default function createMetamaskMiddleware({
version,
@ -38,6 +38,6 @@ export default function createMetamaskMiddleware({
}),
createPendingNonceMiddleware({ getPendingNonce }),
createPendingTxMiddleware({ getPendingTransactionByHash }),
])
return metamaskMiddleware
]);
return metamaskMiddleware;
}

@ -1 +1 @@
export { default } from './network'
export { default } from './network';

@ -1,35 +1,35 @@
import { createAsyncMiddleware } from 'json-rpc-engine'
import { formatTxMetaForRpcResult } from '../util'
import { createAsyncMiddleware } from 'json-rpc-engine';
import { formatTxMetaForRpcResult } from '../util';
export function createPendingNonceMiddleware({ getPendingNonce }) {
return createAsyncMiddleware(async (req, res, next) => {
const { method, params } = req
const { method, params } = req;
if (method !== 'eth_getTransactionCount') {
next()
return
next();
return;
}
const [param, blockRef] = params
const [param, blockRef] = params;
if (blockRef !== 'pending') {
next()
return
next();
return;
}
res.result = await getPendingNonce(param)
})
res.result = await getPendingNonce(param);
});
}
export function createPendingTxMiddleware({ getPendingTransactionByHash }) {
return createAsyncMiddleware(async (req, res, next) => {
const { method, params } = req
const { method, params } = req;
if (method !== 'eth_getTransactionByHash') {
next()
return
next();
return;
}
const [hash] = params
const txMeta = getPendingTransactionByHash(hash)
const [hash] = params;
const txMeta = getPendingTransactionByHash(hash);
if (!txMeta) {
next()
return
next();
return;
}
res.result = formatTxMetaForRpcResult(txMeta)
})
res.result = formatTxMetaForRpcResult(txMeta);
});
}

@ -1,14 +1,14 @@
import assert from 'assert'
import EventEmitter from 'events'
import { ComposedStore, ObservableStore } from '@metamask/obs-store'
import { JsonRpcEngine } from 'json-rpc-engine'
import providerFromEngine from 'eth-json-rpc-middleware/providerFromEngine'
import log from 'loglevel'
import assert from 'assert';
import EventEmitter from 'events';
import { ComposedStore, ObservableStore } from '@metamask/obs-store';
import { JsonRpcEngine } from 'json-rpc-engine';
import providerFromEngine from 'eth-json-rpc-middleware/providerFromEngine';
import log from 'loglevel';
import {
createSwappableProxy,
createEventEmitterProxy,
} from 'swappable-obj-proxy'
import EthQuery from 'eth-query'
} from 'swappable-obj-proxy';
import EthQuery from 'eth-query';
import {
RINKEBY,
MAINNET,
@ -17,63 +17,63 @@ import {
NETWORK_TYPE_TO_ID_MAP,
MAINNET_CHAIN_ID,
RINKEBY_CHAIN_ID,
} from '../../../../shared/constants/network'
} from '../../../../shared/constants/network';
import {
isPrefixedFormattedHexString,
isSafeChainId,
} from '../../../../shared/modules/utils'
import createMetamaskMiddleware from './createMetamaskMiddleware'
import createInfuraClient from './createInfuraClient'
import createJsonRpcClient from './createJsonRpcClient'
} from '../../../../shared/modules/utils';
import createMetamaskMiddleware from './createMetamaskMiddleware';
import createInfuraClient from './createInfuraClient';
import createJsonRpcClient from './createJsonRpcClient';
const env = process.env.METAMASK_ENV
const env = process.env.METAMASK_ENV;
let defaultProviderConfigOpts
let defaultProviderConfigOpts;
if (process.env.IN_TEST === 'true') {
defaultProviderConfigOpts = {
type: NETWORK_TYPE_RPC,
rpcUrl: 'http://localhost:8545',
chainId: '0x539',
nickname: 'Localhost 8545',
}
};
} else if (process.env.METAMASK_DEBUG || env === 'test') {
defaultProviderConfigOpts = { type: RINKEBY, chainId: RINKEBY_CHAIN_ID }
defaultProviderConfigOpts = { type: RINKEBY, chainId: RINKEBY_CHAIN_ID };
} else {
defaultProviderConfigOpts = { type: MAINNET, chainId: MAINNET_CHAIN_ID }
defaultProviderConfigOpts = { type: MAINNET, chainId: MAINNET_CHAIN_ID };
}
const defaultProviderConfig = {
ticker: 'ETH',
...defaultProviderConfigOpts,
}
};
export default class NetworkController extends EventEmitter {
constructor(opts = {}) {
super()
super();
// create stores
this.providerStore = new ObservableStore(
opts.provider || { ...defaultProviderConfig },
)
);
this.previousProviderStore = new ObservableStore(
this.providerStore.getState(),
)
this.networkStore = new ObservableStore('loading')
);
this.networkStore = new ObservableStore('loading');
this.store = new ComposedStore({
provider: this.providerStore,
previousProviderStore: this.previousProviderStore,
network: this.networkStore,
})
});
// provider and block tracker
this._provider = null
this._blockTracker = null
this._provider = null;
this._blockTracker = null;
// provider and block tracker proxies - because the network changes
this._providerProxy = null
this._blockTrackerProxy = null
this._providerProxy = null;
this._blockTrackerProxy = null;
this.on('networkDidChange', this.lookupNetwork)
this.on('networkDidChange', this.lookupNetwork);
}
/**
@ -85,43 +85,43 @@ export default class NetworkController extends EventEmitter {
*/
setInfuraProjectId(projectId) {
if (!projectId || typeof projectId !== 'string') {
throw new Error('Invalid Infura project ID')
throw new Error('Invalid Infura project ID');
}
this._infuraProjectId = projectId
this._infuraProjectId = projectId;
}
initializeProvider(providerParams) {
this._baseProviderParams = providerParams
const { type, rpcUrl, chainId } = this.getProviderConfig()
this._configureProvider({ type, rpcUrl, chainId })
this.lookupNetwork()
this._baseProviderParams = providerParams;
const { type, rpcUrl, chainId } = this.getProviderConfig();
this._configureProvider({ type, rpcUrl, chainId });
this.lookupNetwork();
}
// return the proxies so the references will always be good
getProviderAndBlockTracker() {
const provider = this._providerProxy
const blockTracker = this._blockTrackerProxy
return { provider, blockTracker }
const provider = this._providerProxy;
const blockTracker = this._blockTrackerProxy;
return { provider, blockTracker };
}
verifyNetwork() {
// Check network when restoring connectivity:
if (this.isNetworkLoading()) {
this.lookupNetwork()
this.lookupNetwork();
}
}
getNetworkState() {
return this.networkStore.getState()
return this.networkStore.getState();
}
setNetworkState(network) {
this.networkStore.putState(network)
this.networkStore.putState(network);
}
isNetworkLoading() {
return this.getNetworkState() === 'loading'
return this.getNetworkState() === 'loading';
}
lookupNetwork() {
@ -129,49 +129,49 @@ export default class NetworkController extends EventEmitter {
if (!this._provider) {
log.warn(
'NetworkController - lookupNetwork aborted due to missing provider',
)
return
);
return;
}
const chainId = this.getCurrentChainId()
const chainId = this.getCurrentChainId();
if (!chainId) {
log.warn(
'NetworkController - lookupNetwork aborted due to missing chainId',
)
this.setNetworkState('loading')
return
);
this.setNetworkState('loading');
return;
}
// Ping the RPC endpoint so we can confirm that it works
const ethQuery = new EthQuery(this._provider)
const initialNetwork = this.getNetworkState()
const ethQuery = new EthQuery(this._provider);
const initialNetwork = this.getNetworkState();
ethQuery.sendAsync({ method: 'net_version' }, (err, networkVersion) => {
const currentNetwork = this.getNetworkState()
const currentNetwork = this.getNetworkState();
if (initialNetwork === currentNetwork) {
if (err) {
this.setNetworkState('loading')
return
this.setNetworkState('loading');
return;
}
this.setNetworkState(networkVersion)
this.setNetworkState(networkVersion);
}
})
});
}
getCurrentChainId() {
const { type, chainId: configChainId } = this.getProviderConfig()
return NETWORK_TYPE_TO_ID_MAP[type]?.chainId || configChainId
const { type, chainId: configChainId } = this.getProviderConfig();
return NETWORK_TYPE_TO_ID_MAP[type]?.chainId || configChainId;
}
setRpcTarget(rpcUrl, chainId, ticker = 'ETH', nickname = '', rpcPrefs) {
assert.ok(
isPrefixedFormattedHexString(chainId),
`Invalid chain ID "${chainId}": invalid hex string.`,
)
);
assert.ok(
isSafeChainId(parseInt(chainId, 16)),
`Invalid chain ID "${chainId}": numerical value greater than max safe value.`,
)
);
this.setProviderConfig({
type: NETWORK_TYPE_RPC,
rpcUrl,
@ -179,7 +179,7 @@ export default class NetworkController extends EventEmitter {
ticker,
nickname,
rpcPrefs,
})
});
}
async setProviderType(type, rpcUrl = '', ticker = 'ETH', nickname = '') {
@ -187,41 +187,41 @@ export default class NetworkController extends EventEmitter {
type,
NETWORK_TYPE_RPC,
`NetworkController - cannot call "setProviderType" with type "${NETWORK_TYPE_RPC}". Use "setRpcTarget"`,
)
);
assert.ok(
INFURA_PROVIDER_TYPES.includes(type),
`Unknown Infura provider type "${type}".`,
)
const { chainId } = NETWORK_TYPE_TO_ID_MAP[type]
this.setProviderConfig({ type, rpcUrl, chainId, ticker, nickname })
);
const { chainId } = NETWORK_TYPE_TO_ID_MAP[type];
this.setProviderConfig({ type, rpcUrl, chainId, ticker, nickname });
}
resetConnection() {
this.setProviderConfig(this.getProviderConfig())
this.setProviderConfig(this.getProviderConfig());
}
/**
* Sets the provider config and switches the network.
*/
setProviderConfig(config) {
this.previousProviderStore.updateState(this.getProviderConfig())
this.providerStore.updateState(config)
this._switchNetwork(config)
this.previousProviderStore.updateState(this.getProviderConfig());
this.providerStore.updateState(config);
this._switchNetwork(config);
}
rollbackToPreviousProvider() {
const config = this.previousProviderStore.getState()
this.providerStore.updateState(config)
this._switchNetwork(config)
const config = this.previousProviderStore.getState();
this.providerStore.updateState(config);
this._switchNetwork(config);
}
getProviderConfig() {
return this.providerStore.getState()
return this.providerStore.getState();
}
getNetworkIdentifier() {
const provider = this.providerStore.getState()
return provider.type === NETWORK_TYPE_RPC ? provider.rpcUrl : provider.type
const provider = this.providerStore.getState();
return provider.type === NETWORK_TYPE_RPC ? provider.rpcUrl : provider.type;
}
//
@ -229,68 +229,68 @@ export default class NetworkController extends EventEmitter {
//
_switchNetwork(opts) {
this.setNetworkState('loading')
this._configureProvider(opts)
this.emit('networkDidChange', opts.type)
this.setNetworkState('loading');
this._configureProvider(opts);
this.emit('networkDidChange', opts.type);
}
_configureProvider({ type, rpcUrl, chainId }) {
// infura type-based endpoints
const isInfura = INFURA_PROVIDER_TYPES.includes(type)
const isInfura = INFURA_PROVIDER_TYPES.includes(type);
if (isInfura) {
this._configureInfuraProvider(type, this._infuraProjectId)
this._configureInfuraProvider(type, this._infuraProjectId);
// url-based rpc endpoints
} else if (type === NETWORK_TYPE_RPC) {
this._configureStandardProvider(rpcUrl, chainId)
this._configureStandardProvider(rpcUrl, chainId);
} else {
throw new Error(
`NetworkController - _configureProvider - unknown type "${type}"`,
)
);
}
}
_configureInfuraProvider(type, projectId) {
log.info('NetworkController - configureInfuraProvider', type)
log.info('NetworkController - configureInfuraProvider', type);
const networkClient = createInfuraClient({
network: type,
projectId,
})
this._setNetworkClient(networkClient)
});
this._setNetworkClient(networkClient);
}
_configureStandardProvider(rpcUrl, chainId) {
log.info('NetworkController - configureStandardProvider', rpcUrl)
const networkClient = createJsonRpcClient({ rpcUrl, chainId })
this._setNetworkClient(networkClient)
log.info('NetworkController - configureStandardProvider', rpcUrl);
const networkClient = createJsonRpcClient({ rpcUrl, chainId });
this._setNetworkClient(networkClient);
}
_setNetworkClient({ networkMiddleware, blockTracker }) {
const metamaskMiddleware = createMetamaskMiddleware(
this._baseProviderParams,
)
const engine = new JsonRpcEngine()
engine.push(metamaskMiddleware)
engine.push(networkMiddleware)
const provider = providerFromEngine(engine)
this._setProviderAndBlockTracker({ provider, blockTracker })
);
const engine = new JsonRpcEngine();
engine.push(metamaskMiddleware);
engine.push(networkMiddleware);
const provider = providerFromEngine(engine);
this._setProviderAndBlockTracker({ provider, blockTracker });
}
_setProviderAndBlockTracker({ provider, blockTracker }) {
// update or intialize proxies
if (this._providerProxy) {
this._providerProxy.setTarget(provider)
this._providerProxy.setTarget(provider);
} else {
this._providerProxy = createSwappableProxy(provider)
this._providerProxy = createSwappableProxy(provider);
}
if (this._blockTrackerProxy) {
this._blockTrackerProxy.setTarget(blockTracker)
this._blockTrackerProxy.setTarget(blockTracker);
} else {
this._blockTrackerProxy = createEventEmitterProxy(blockTracker, {
eventFilter: 'skipInternal',
})
});
}
// set new provider and blockTracker
this._provider = provider
this._blockTracker = blockTracker
this._provider = provider;
this._blockTracker = blockTracker;
}
}

@ -1,6 +1,6 @@
import { NETWORK_TO_NAME_MAP } from '../../../../shared/constants/network'
import { NETWORK_TO_NAME_MAP } from '../../../../shared/constants/network';
export const getNetworkDisplayName = (key) => NETWORK_TO_NAME_MAP[key]
export const getNetworkDisplayName = (key) => NETWORK_TO_NAME_MAP[key];
export function formatTxMetaForRpcResult(txMeta) {
return {
@ -20,5 +20,5 @@ export function formatTxMetaForRpcResult(txMeta) {
v: txMeta.v,
r: txMeta.r,
s: txMeta.s,
}
};
}

@ -1,5 +1,5 @@
import { ObservableStore } from '@metamask/obs-store'
import log from 'loglevel'
import { ObservableStore } from '@metamask/obs-store';
import log from 'loglevel';
/**
* @typedef {Object} InitState
@ -25,30 +25,30 @@ export default class OnboardingController {
constructor(opts = {}) {
const initialTransientState = {
onboardingTabs: {},
}
};
const initState = {
seedPhraseBackedUp: null,
...opts.initState,
...initialTransientState,
}
this.store = new ObservableStore(initState)
this.preferencesController = opts.preferencesController
this.completedOnboarding = this.preferencesController.store.getState().completedOnboarding
};
this.store = new ObservableStore(initState);
this.preferencesController = opts.preferencesController;
this.completedOnboarding = this.preferencesController.store.getState().completedOnboarding;
this.preferencesController.store.subscribe(({ completedOnboarding }) => {
if (completedOnboarding !== this.completedOnboarding) {
this.completedOnboarding = completedOnboarding
this.completedOnboarding = completedOnboarding;
if (completedOnboarding) {
this.store.updateState(initialTransientState)
this.store.updateState(initialTransientState);
}
}
})
});
}
setSeedPhraseBackedUp(newSeedPhraseBackUpState) {
this.store.updateState({
seedPhraseBackedUp: newSeedPhraseBackUpState,
})
});
}
/**
@ -59,16 +59,16 @@ export default class OnboardingController {
*/
registerOnboarding = async (location, tabId) => {
if (this.completedOnboarding) {
log.debug('Ignoring registerOnboarding; user already onboarded')
return
log.debug('Ignoring registerOnboarding; user already onboarded');
return;
}
const onboardingTabs = { ...this.store.getState().onboardingTabs }
const onboardingTabs = { ...this.store.getState().onboardingTabs };
if (!onboardingTabs[location] || onboardingTabs[location] !== tabId) {
log.debug(
`Registering onboarding tab at location '${location}' with tabId '${tabId}'`,
)
onboardingTabs[location] = tabId
this.store.updateState({ onboardingTabs })
}
);
onboardingTabs[location] = tabId;
this.store.updateState({ onboardingTabs });
}
};
}

@ -1,37 +1,37 @@
export const APPROVAL_TYPE = 'wallet_requestPermissions'
export const APPROVAL_TYPE = 'wallet_requestPermissions';
export const WALLET_PREFIX = 'wallet_'
export const WALLET_PREFIX = 'wallet_';
export const HISTORY_STORE_KEY = 'permissionsHistory'
export const HISTORY_STORE_KEY = 'permissionsHistory';
export const LOG_STORE_KEY = 'permissionsLog'
export const LOG_STORE_KEY = 'permissionsLog';
export const METADATA_STORE_KEY = 'domainMetadata'
export const METADATA_STORE_KEY = 'domainMetadata';
export const METADATA_CACHE_MAX_SIZE = 100
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',
chainChanged: 'metamask_chainChanged',
}
};
export const LOG_IGNORE_METHODS = [
'wallet_registerOnboarding',
'wallet_watchAsset',
]
];
export const LOG_METHOD_TYPES = {
restricted: 'restricted',
internal: 'internal',
}
};
export const LOG_LIMIT = 100
export const LOG_LIMIT = 100;
export const SAFE_METHODS = [
'eth_blockNumber',
@ -90,4 +90,4 @@ export const SAFE_METHODS = [
'wallet_watchAsset',
'web3_clientVersion',
'web3_sha3',
]
];

@ -1,12 +1,12 @@
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 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
@ -17,13 +17,13 @@ import {
HISTORY_STORE_KEY,
NOTIFICATION_NAMES,
CAVEAT_TYPES,
} from './enums'
} from './enums';
import createPermissionsMethodMiddleware from './permissionsMethodMiddleware'
import PermissionsLogController from './permissionsLog'
import createPermissionsMethodMiddleware from './permissionsMethodMiddleware';
import PermissionsLogController from './permissionsLog';
// instanbul ignore next
const noop = () => undefined
const noop = () => undefined;
export class PermissionsController {
constructor(
@ -44,56 +44,56 @@ export class PermissionsController {
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.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.approvals = approvals;
this._initializePermissions(restoredPermissions);
this._lastSelectedAddress = preferences.getState().selectedAddress;
this.preferences = preferences;
this._initializeMetadataStore(restoredState)
this._initializeMetadataStore(restoredState);
preferences.subscribe(async ({ selectedAddress }) => {
if (selectedAddress && selectedAddress !== this._lastSelectedAddress) {
this._lastSelectedAddress = selectedAddress
await this._handleAccountSelected(selectedAddress)
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.')
throw new Error('Must provide non-empty string origin.');
}
const metadataState = this.store.getState()[METADATA_STORE_KEY]
const metadataState = this.store.getState()[METADATA_STORE_KEY];
if (extensionId && metadataState[origin]?.extensionId !== extensionId) {
this.addDomainMetadata(origin, { extensionId })
this.addDomainMetadata(origin, { extensionId });
}
const engine = new JsonRpcEngine()
const engine = new JsonRpcEngine();
engine.push(this.permissionsLog.createMiddleware())
engine.push(this.permissionsLog.createMiddleware());
engine.push(
createPermissionsMethodMiddleware({
@ -108,15 +108,15 @@ export class PermissionsController {
{ eth_accounts: {} },
),
}),
)
);
engine.push(
this.permissions.providerMiddlewareFunction.bind(this.permissions, {
origin,
}),
)
);
return engine.asMiddleware()
return engine.asMiddleware();
}
/**
@ -125,9 +125,9 @@ export class PermissionsController {
* @returns {Promise<string>} The permissions request ID
*/
async requestAccountsPermissionWithId(origin) {
const id = nanoid()
this._requestPermissions({ origin }, { eth_accounts: {} }, id)
return id
const id = nanoid();
this._requestPermissions({ origin }, { eth_accounts: {} }, id);
return id;
}
/**
@ -139,24 +139,24 @@ export class PermissionsController {
*/
getAccounts(origin) {
return new Promise((resolve, _) => {
const req = { method: 'eth_accounts' }
const res = {}
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([])
resolve([]);
} else {
resolve(res.result)
resolve(res.result);
}
}
})
});
}
/**
@ -167,7 +167,7 @@ export class PermissionsController {
* @returns {boolean} Whether the origin has the permission.
*/
hasPermission(origin, permission) {
return Boolean(this.permissions.getPermission(origin, permission))
return Boolean(this.permissions.getPermission(origin, permission));
}
/**
@ -176,7 +176,7 @@ export class PermissionsController {
* @returns {Object} identities
*/
_getIdentities() {
return this.preferences.getState().identities
return this.preferences.getState().identities;
}
/**
@ -196,20 +196,20 @@ export class PermissionsController {
id,
method: 'wallet_requestPermissions',
params: [permissions],
}
const res = {}
};
const res = {};
this.permissions.providerMiddlewareFunction(domain, req, res, noop, _end)
this.permissions.providerMiddlewareFunction(domain, req, res, noop, _end);
function _end(_err) {
const err = _err || res.error
const err = _err || res.error;
if (err) {
reject(err)
reject(err);
} else {
resolve(res.result)
resolve(res.result);
}
}
})
});
}
/**
@ -222,11 +222,11 @@ export class PermissionsController {
* @param {Array} accounts - The accounts to expose, if any
*/
async approvePermissionsRequest(approved, accounts) {
const { id } = approved.metadata
const { id } = approved.metadata;
if (!this.approvals.has({ id })) {
log.debug(`Permissions request with id '${id}' not found.`)
return
log.debug(`Permissions request with id '${id}' not found.`);
return;
}
try {
@ -236,15 +236,15 @@ export class PermissionsController {
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.resolve(id, approved.permissions)
);
this.approvals.resolve(id, approved.permissions);
}
} catch (err) {
// if finalization fails, reject the request
@ -254,7 +254,7 @@ export class PermissionsController {
message: err.message,
data: err,
}),
)
);
}
}
@ -267,11 +267,11 @@ export class PermissionsController {
*/
async rejectPermissionsRequest(id) {
if (!this.approvals.has({ id })) {
log.debug(`Permissions request with id '${id}' not found.`)
return
log.debug(`Permissions request with id '${id}' not found.`);
return;
}
this.approvals.reject(id, ethErrors.provider.userRejectedRequest())
this.approvals.reject(id, ethErrors.provider.userRejectedRequest());
}
/**
@ -284,18 +284,18 @@ export class PermissionsController {
* @param {string} account - The new account to expose.
*/
async addPermittedAccount(origin, account) {
const domains = this.permissions.getDomains()
const domains = this.permissions.getDomains();
if (!domains[origin]) {
throw new Error('Unrecognized domain')
throw new Error('Unrecognized domain');
}
this.validatePermittedAccounts([account])
this.validatePermittedAccounts([account]);
const oldPermittedAccounts = this._getPermittedAccounts(origin)
const oldPermittedAccounts = this._getPermittedAccounts(origin);
if (!oldPermittedAccounts) {
throw new Error(`Origin does not have 'eth_accounts' permission`)
throw new Error(`Origin does not have 'eth_accounts' permission`);
} else if (oldPermittedAccounts.includes(account)) {
throw new Error('Account is already permitted for origin')
throw new Error('Account is already permitted for origin');
}
this.permissions.updateCaveatFor(
@ -303,11 +303,11 @@ export class PermissionsController {
'eth_accounts',
CAVEAT_NAMES.exposedAccounts,
[...oldPermittedAccounts, account],
)
);
const permittedAccounts = await this.getAccounts(origin)
const permittedAccounts = await this.getAccounts(origin);
this.notifyAccountsChanged(origin, permittedAccounts)
this.notifyAccountsChanged(origin, permittedAccounts);
}
/**
@ -322,38 +322,38 @@ export class PermissionsController {
* @param {string} account - The account to remove.
*/
async removePermittedAccount(origin, account) {
const domains = this.permissions.getDomains()
const domains = this.permissions.getDomains();
if (!domains[origin]) {
throw new Error('Unrecognized domain')
throw new Error('Unrecognized domain');
}
this.validatePermittedAccounts([account])
this.validatePermittedAccounts([account]);
const oldPermittedAccounts = this._getPermittedAccounts(origin)
const oldPermittedAccounts = this._getPermittedAccounts(origin);
if (!oldPermittedAccounts) {
throw new Error(`Origin does not have 'eth_accounts' permission`)
throw new Error(`Origin does not have 'eth_accounts' permission`);
} else if (!oldPermittedAccounts.includes(account)) {
throw new Error('Account is not permitted for origin')
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'] })
this.removePermissionsFor({ [origin]: ['eth_accounts'] });
} else {
this.permissions.updateCaveatFor(
origin,
'eth_accounts',
CAVEAT_NAMES.exposedAccounts,
newPermittedAccounts,
)
);
newPermittedAccounts = await this.getAccounts(origin)
newPermittedAccounts = await this.getAccounts(origin);
}
this.notifyAccountsChanged(origin, newPermittedAccounts)
this.notifyAccountsChanged(origin, newPermittedAccounts);
}
/**
@ -365,18 +365,18 @@ export class PermissionsController {
* @param {string} account - The account to remove.
*/
async removeAllAccountPermissions(account) {
this.validatePermittedAccounts([account])
this.validatePermittedAccounts([account]);
const domains = this.permissions.getDomains()
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),
),
)
);
}
/**
@ -390,16 +390,16 @@ export class PermissionsController {
* @returns {Object} The finalized permissions request object.
*/
async finalizePermissionsRequest(requestedPermissions, requestedAccounts) {
const finalizedPermissions = cloneDeep(requestedPermissions)
const finalizedAccounts = cloneDeep(requestedAccounts)
const finalizedPermissions = cloneDeep(requestedPermissions);
const finalizedAccounts = cloneDeep(requestedAccounts);
const { eth_accounts: ethAccounts } = finalizedPermissions
const { eth_accounts: ethAccounts } = finalizedPermissions;
if (ethAccounts) {
this.validatePermittedAccounts(finalizedAccounts)
this.validatePermittedAccounts(finalizedAccounts);
if (!ethAccounts.caveats) {
ethAccounts.caveats = []
ethAccounts.caveats = [];
}
// caveat names are unique, and we will only construct this caveat here
@ -407,22 +407,22 @@ export class PermissionsController {
(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
return finalizedPermissions;
}
/**
@ -433,16 +433,16 @@ export class PermissionsController {
*/
validatePermittedAccounts(accounts) {
if (!Array.isArray(accounts) || accounts.length === 0) {
throw new Error('Must provide non-empty array of account(s).')
throw new Error('Must provide non-empty array of account(s).');
}
// assert accounts exist
const allIdentities = this._getIdentities()
const allIdentities = this._getIdentities();
accounts.forEach((acc) => {
if (!allIdentities[acc]) {
throw new Error(`Unknown account: ${acc}`)
throw new Error(`Unknown account: ${acc}`);
}
})
});
}
/**
@ -454,11 +454,11 @@ export class PermissionsController {
*/
notifyAccountsChanged(origin, newAccounts) {
if (typeof origin !== 'string' || !origin) {
throw new Error(`Invalid origin: '${origin}'`)
throw new Error(`Invalid origin: '${origin}'`);
}
if (!Array.isArray(newAccounts)) {
throw new Error('Invalid accounts', newAccounts)
throw new Error('Invalid accounts', newAccounts);
}
// We do not share accounts when the extension is locked.
@ -466,8 +466,8 @@ export class PermissionsController {
this._notifyDomain(origin, {
method: NOTIFICATION_NAMES.accountsChanged,
params: newAccounts,
})
this.permissionsLog.updateAccountsHistory(origin, newAccounts)
});
this.permissionsLog.updateAccountsHistory(origin, newAccounts);
}
// NOTE:
@ -491,26 +491,26 @@ export class PermissionsController {
origin,
perms.map((methodName) => {
if (methodName === 'eth_accounts') {
this.notifyAccountsChanged(origin, [])
this.notifyAccountsChanged(origin, []);
}
return { parentCapability: methodName }
return { parentCapability: methodName };
}),
)
})
);
});
}
/**
* Removes all known domains and their related permissions.
*/
clearPermissions() {
this.permissions.clearDomains()
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: [],
})
});
}
/**
@ -524,18 +524,18 @@ export class PermissionsController {
* @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 }
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 permissionsDomains = this.permissions.getDomains();
const oldOrigin = this._pendingSiteMetadata.values().next().value
this._pendingSiteMetadata.delete(oldOrigin)
const oldOrigin = this._pendingSiteMetadata.values().next().value;
this._pendingSiteMetadata.delete(oldOrigin);
if (!permissionsDomains[oldOrigin]) {
delete newMetadataState[oldOrigin]
delete newMetadataState[oldOrigin];
}
}
@ -544,17 +544,17 @@ export class PermissionsController {
...oldMetadataState[origin],
...metadata,
lastUpdated: Date.now(),
}
};
if (
!newMetadataState[origin].extensionId &&
!newMetadataState[origin].host
) {
newMetadataState[origin].host = new URL(origin).host
newMetadataState[origin].host = new URL(origin).host;
}
this._pendingSiteMetadata.add(origin)
this._setDomainMetadata(newMetadataState)
this._pendingSiteMetadata.add(origin);
this._setDomainMetadata(newMetadataState);
}
/**
@ -566,11 +566,11 @@ export class PermissionsController {
* @param {Object} restoredState - The restored permissions controller state.
*/
_initializeMetadataStore(restoredState) {
const metadataState = restoredState[METADATA_STORE_KEY] || {}
const newMetadataState = this._trimDomainMetadata(metadataState)
const metadataState = restoredState[METADATA_STORE_KEY] || {};
const newMetadataState = this._trimDomainMetadata(metadataState);
this._pendingSiteMetadata = new Set()
this._setDomainMetadata(newMetadataState)
this._pendingSiteMetadata = new Set();
this._setDomainMetadata(newMetadataState);
}
/**
@ -582,17 +582,17 @@ export class PermissionsController {
* @returns {Object} The new metadata state object.
*/
_trimDomainMetadata(metadataState) {
const newMetadataState = { ...metadataState }
const origins = Object.keys(metadataState)
const permissionsDomains = this.permissions.getDomains()
const newMetadataState = { ...metadataState };
const origins = Object.keys(metadataState);
const permissionsDomains = this.permissions.getDomains();
origins.forEach((origin) => {
if (!permissionsDomains[origin]) {
delete newMetadataState[origin]
delete newMetadataState[origin];
}
})
});
return newMetadataState
return newMetadataState;
}
/**
@ -600,7 +600,7 @@ export class PermissionsController {
* @param {Object} newMetadataState - The new metadata to set.
*/
_setDomainMetadata(newMetadataState) {
this.store.updateState({ [METADATA_STORE_KEY]: newMetadataState })
this.store.updateState({ [METADATA_STORE_KEY]: newMetadataState });
}
/**
@ -613,9 +613,9 @@ export class PermissionsController {
const permittedAccounts = this.permissions
.getPermission(origin, 'eth_accounts')
?.caveats?.find((caveat) => caveat.name === CAVEAT_NAMES.exposedAccounts)
?.value
?.value;
return permittedAccounts || null
return permittedAccounts || null;
}
/**
@ -629,27 +629,27 @@ export class PermissionsController {
*/
async _handleAccountSelected(account) {
if (typeof account !== 'string') {
throw new Error('Selected account should be a non-empty string.')
throw new Error('Selected account should be a non-empty string.');
}
const domains = this.permissions.getDomains() || {}
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)
)?.value;
return exposedAccounts?.includes(account);
})
.map(([domain]) => domain)
.map(([domain]) => domain);
await Promise.all(
connectedDomains.map((origin) =>
this._handleConnectedAccountSelected(origin),
),
)
);
}
/**
@ -661,9 +661,9 @@ export class PermissionsController {
* @param {string} origin - The origin
*/
async _handleConnectedAccountSelected(origin) {
const permittedAccounts = await this.getAccounts(origin)
const permittedAccounts = await this.getAccounts(origin);
this.notifyAccountsChanged(origin, permittedAccounts)
this.notifyAccountsChanged(origin, permittedAccounts);
}
/**
@ -674,7 +674,7 @@ export class PermissionsController {
*/
_initializePermissions(restoredState) {
// these permission requests are almost certainly stale
const initState = { ...restoredState, permissionsRequests: [] }
const initState = { ...restoredState, permissionsRequests: [] };
this.permissions = new RpcCap(
{
@ -698,16 +698,16 @@ export class PermissionsController {
requestUserApproval: async (req) => {
const {
metadata: { id, origin },
} = req
} = req;
return this.approvals.addAndShowApprovalRequest({
id,
origin,
type: APPROVAL_TYPE,
})
});
},
},
initState,
)
);
}
}

@ -1,5 +1,5 @@
import { cloneDeep } from 'lodash'
import { CAVEAT_NAMES } from '../../../../shared/constants/permissions'
import { cloneDeep } from 'lodash';
import { CAVEAT_NAMES } from '../../../../shared/constants/permissions';
import {
HISTORY_STORE_KEY,
LOG_IGNORE_METHODS,
@ -7,7 +7,7 @@ import {
LOG_METHOD_TYPES,
LOG_STORE_KEY,
WALLET_PREFIX,
} from './enums'
} from './enums';
/**
* Controller with middleware for logging requests and responses to restricted
@ -15,8 +15,8 @@ import {
*/
export default class PermissionsLogController {
constructor({ restrictedMethods, store }) {
this.restrictedMethods = restrictedMethods
this.store = store
this.restrictedMethods = restrictedMethods;
this.store = store;
}
/**
@ -25,7 +25,7 @@ export default class PermissionsLogController {
* @returns {Array<Object>} The activity log.
*/
getActivityLog() {
return this.store.getState()[LOG_STORE_KEY] || []
return this.store.getState()[LOG_STORE_KEY] || [];
}
/**
@ -34,7 +34,7 @@ export default class PermissionsLogController {
* @param {Array<Object>} logs - The new activity log array.
*/
updateActivityLog(logs) {
this.store.updateState({ [LOG_STORE_KEY]: logs })
this.store.updateState({ [LOG_STORE_KEY]: logs });
}
/**
@ -43,7 +43,7 @@ export default class PermissionsLogController {
* @returns {Object} The permissions history log.
*/
getHistory() {
return this.store.getState()[HISTORY_STORE_KEY] || {}
return this.store.getState()[HISTORY_STORE_KEY] || {};
}
/**
@ -52,7 +52,7 @@ export default class PermissionsLogController {
* @param {Object} history - The new permissions history log object.
*/
updateHistory(history) {
this.store.updateState({ [HISTORY_STORE_KEY]: history })
this.store.updateState({ [HISTORY_STORE_KEY]: history });
}
/**
@ -65,16 +65,16 @@ export default class PermissionsLogController {
*/
updateAccountsHistory(origin, accounts) {
if (accounts.length === 0) {
return
return;
}
const accountToTimeMap = getAccountToTimeMap(accounts, Date.now())
const accountToTimeMap = getAccountToTimeMap(accounts, Date.now());
this.commitNewHistory(origin, {
eth_accounts: {
accounts: accountToTimeMap,
},
})
});
}
/**
@ -89,37 +89,37 @@ export default class PermissionsLogController {
*/
createMiddleware() {
return (req, res, next, _end) => {
let activityEntry, requestedMethods
const { origin, method } = req
const isInternal = method.startsWith(WALLET_PREFIX)
let activityEntry, requestedMethods;
const { origin, method } = req;
const isInternal = method.startsWith(WALLET_PREFIX);
// we only log certain methods
if (
!LOG_IGNORE_METHODS.includes(method) &&
(isInternal || this.restrictedMethods.includes(method))
) {
activityEntry = this.logRequest(req, isInternal)
activityEntry = this.logRequest(req, isInternal);
if (method === `${WALLET_PREFIX}requestPermissions`) {
// get the corresponding methods from the requested permissions so
// that we can record permissions history
requestedMethods = this.getRequestedMethods(req)
requestedMethods = this.getRequestedMethods(req);
}
} else if (method === 'eth_requestAccounts') {
// eth_requestAccounts is a special case; we need to extract the accounts
// from it
activityEntry = this.logRequest(req, isInternal)
requestedMethods = ['eth_accounts']
activityEntry = this.logRequest(req, isInternal);
requestedMethods = ['eth_accounts'];
} else {
// no-op
next()
return
next();
return;
}
// call next with a return handler for capturing the response
next((cb) => {
const time = Date.now()
this.logResponse(activityEntry, res, time)
const time = Date.now();
this.logResponse(activityEntry, res, time);
if (requestedMethods && !res.error && res.result) {
// any permissions or accounts changes will be recorded on the response,
@ -130,11 +130,11 @@ export default class PermissionsLogController {
res.result,
time,
method === 'eth_requestAccounts',
)
}
cb()
})
);
}
cb();
});
};
}
/**
@ -156,9 +156,9 @@ export default class PermissionsLogController {
response: null,
responseTime: null,
success: null,
}
this.commitNewActivity(activityEntry)
return activityEntry
};
this.commitNewActivity(activityEntry);
return activityEntry;
}
/**
@ -171,12 +171,12 @@ export default class PermissionsLogController {
*/
logResponse(entry, response, time) {
if (!entry || !response) {
return
return;
}
entry.response = cloneDeep(response)
entry.responseTime = time
entry.success = !response.error
entry.response = cloneDeep(response);
entry.responseTime = time;
entry.success = !response.error;
}
/**
@ -186,17 +186,17 @@ export default class PermissionsLogController {
* @param {Object} entry - The activity log entry.
*/
commitNewActivity(entry) {
const logs = this.getActivityLog()
const logs = this.getActivityLog();
// add new entry to end of log
logs.push(entry)
logs.push(entry);
// remove oldest log if exceeding size limit
if (logs.length > LOG_LIMIT) {
logs.shift()
logs.shift();
}
this.updateActivityLog(logs)
this.updateActivityLog(logs);
}
/**
@ -215,18 +215,18 @@ export default class PermissionsLogController {
time,
isEthRequestAccounts,
) {
let accounts, newEntries
let accounts, newEntries;
if (isEthRequestAccounts) {
accounts = result
const accountToTimeMap = getAccountToTimeMap(accounts, time)
accounts = result;
const accountToTimeMap = getAccountToTimeMap(accounts, time);
newEntries = {
eth_accounts: {
accounts: accountToTimeMap,
lastApproved: time,
},
}
};
} else {
// Records new "lastApproved" times for the granted permissions, if any.
// Special handling for eth_accounts, in order to record the time the
@ -234,33 +234,33 @@ export default class PermissionsLogController {
newEntries = result
.map((perm) => {
if (perm.parentCapability === 'eth_accounts') {
accounts = this.getAccountsFromPermission(perm)
accounts = this.getAccountsFromPermission(perm);
}
return perm.parentCapability
return perm.parentCapability;
})
.reduce((acc, method) => {
// all approved permissions will be included in the response,
// not just the newly requested ones
if (requestedMethods.includes(method)) {
if (method === 'eth_accounts') {
const accountToTimeMap = getAccountToTimeMap(accounts, time)
const accountToTimeMap = getAccountToTimeMap(accounts, time);
acc[method] = {
lastApproved: time,
accounts: accountToTimeMap,
}
};
} else {
acc[method] = { lastApproved: time }
acc[method] = { lastApproved: time };
}
}
return acc
}, {})
return acc;
}, {});
}
if (Object.keys(newEntries).length > 0) {
this.commitNewHistory(origin, newEntries)
this.commitNewHistory(origin, newEntries);
}
}
@ -274,24 +274,24 @@ export default class PermissionsLogController {
*/
commitNewHistory(origin, newEntries) {
// a simple merge updates most permissions
const history = this.getHistory()
const history = this.getHistory();
const newOriginHistory = {
...history[origin],
...newEntries,
}
};
// eth_accounts requires special handling, because of information
// we store about the accounts
const existingEthAccountsEntry =
history[origin] && history[origin].eth_accounts
const newEthAccountsEntry = newEntries.eth_accounts
history[origin] && history[origin].eth_accounts;
const newEthAccountsEntry = newEntries.eth_accounts;
if (existingEthAccountsEntry && newEthAccountsEntry) {
// we may intend to update just the accounts, not the permission
// itself
const lastApproved =
newEthAccountsEntry.lastApproved ||
existingEthAccountsEntry.lastApproved
existingEthAccountsEntry.lastApproved;
// merge old and new eth_accounts history entries
newOriginHistory.eth_accounts = {
@ -300,12 +300,12 @@ export default class PermissionsLogController {
...existingEthAccountsEntry.accounts,
...newEthAccountsEntry.accounts,
},
}
};
}
history[origin] = newOriginHistory
history[origin] = newOriginHistory;
this.updateHistory(history)
this.updateHistory(history);
}
/**
@ -321,9 +321,9 @@ export default class PermissionsLogController {
typeof request.params[0] !== 'object' ||
Array.isArray(request.params[0])
) {
return null
return null;
}
return Object.keys(request.params[0])
return Object.keys(request.params[0]);
}
/**
@ -335,21 +335,21 @@ export default class PermissionsLogController {
*/
getAccountsFromPermission(perm) {
if (perm.parentCapability !== 'eth_accounts' || !perm.caveats) {
return []
return [];
}
const accounts = new Set()
const accounts = new Set();
for (const caveat of perm.caveats) {
if (
caveat.name === CAVEAT_NAMES.exposedAccounts &&
Array.isArray(caveat.value)
) {
for (const value of caveat.value) {
accounts.add(value)
accounts.add(value);
}
}
}
return [...accounts]
return [...accounts];
}
}
@ -363,5 +363,5 @@ export default class PermissionsLogController {
* @returns {Object} A string:number map of addresses to time.
*/
function getAccountToTimeMap(accounts, time) {
return accounts.reduce((acc, account) => ({ ...acc, [account]: time }), {})
return accounts.reduce((acc, account) => ({ ...acc, [account]: time }), {});
}

@ -1,5 +1,5 @@
import { createAsyncMiddleware } from 'json-rpc-engine'
import { ethErrors } from 'eth-rpc-errors'
import { createAsyncMiddleware } from 'json-rpc-engine';
import { ethErrors } from 'eth-rpc-errors';
/**
* Create middleware for handling certain methods and preprocessing permissions requests.
@ -12,73 +12,73 @@ export default function createPermissionsMethodMiddleware({
notifyAccountsChanged,
requestAccountsPermission,
}) {
let isProcessingRequestAccounts = false
let isProcessingRequestAccounts = false;
return createAsyncMiddleware(async (req, res, next) => {
let responseHandler
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
res.result = await getAccounts();
return;
}
case 'eth_requestAccounts': {
if (isProcessingRequestAccounts) {
res.error = ethErrors.rpc.resourceUnavailable(
'Already processing eth_requestAccounts. Please wait.',
)
return
);
return;
}
if (hasPermission('eth_accounts')) {
isProcessingRequestAccounts = true
await getUnlockPromise()
isProcessingRequestAccounts = false
isProcessingRequestAccounts = true;
await getUnlockPromise();
isProcessingRequestAccounts = false;
}
// first, just try to get accounts
let accounts = await getAccounts()
let accounts = await getAccounts();
if (accounts.length > 0) {
res.result = accounts
return
res.result = accounts;
return;
}
// if no accounts, request the accounts permission
try {
await requestAccountsPermission()
await requestAccountsPermission();
} catch (err) {
res.error = err
return
res.error = err;
return;
}
// get the accounts again
accounts = await getAccounts()
accounts = await getAccounts();
/* istanbul ignore else: too hard to induce, see below comment */
if (accounts.length > 0) {
res.result = accounts
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
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)
addDomainMetadata(req.origin, req.params);
}
res.result = true
return
res.result = true;
return;
}
// register return handler to send accountsChanged notification
@ -88,25 +88,25 @@ export default function createPermissionsMethodMiddleware({
if (Array.isArray(res.result)) {
for (const permission of res.result) {
if (permission.parentCapability === 'eth_accounts') {
notifyAccountsChanged(await getAccounts())
notifyAccountsChanged(await getAccounts());
}
}
}
};
}
}
break
break;
}
default:
break
break;
}
// when this promise resolves, the response is on its way back
// eslint-disable-next-line node/callback-return
await next()
await next();
if (responseHandler) {
responseHandler()
responseHandler();
}
})
});
}

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

@ -1,13 +1,13 @@
import { strict as assert } from 'assert'
import { ObservableStore } from '@metamask/obs-store'
import { ethErrors } from 'eth-rpc-errors'
import { normalize as normalizeAddress } from 'eth-sig-util'
import { isValidAddress } from 'ethereumjs-util'
import ethers from 'ethers'
import log from 'loglevel'
import { LISTED_CONTRACT_ADDRESSES } from '../../../shared/constants/tokens'
import { NETWORK_TYPE_TO_ID_MAP } from '../../../shared/constants/network'
import { isPrefixedFormattedHexString } from '../../../shared/modules/utils'
import { strict as assert } from 'assert';
import { ObservableStore } from '@metamask/obs-store';
import { ethErrors } from 'eth-rpc-errors';
import { normalize as normalizeAddress } from 'eth-sig-util';
import { isValidAddress } from 'ethereumjs-util';
import ethers from 'ethers';
import log from 'loglevel';
import { LISTED_CONTRACT_ADDRESSES } from '../../../shared/constants/tokens';
import { NETWORK_TYPE_TO_ID_MAP } from '../../../shared/constants/network';
import { isPrefixedFormattedHexString } from '../../../shared/modules/utils';
export default class PreferencesController {
/**
@ -65,18 +65,18 @@ export default class PreferencesController {
// ENS decentralized website resolution
ipfsGateway: 'dweb.link',
...opts.initState,
}
};
this.network = opts.network
this.store = new ObservableStore(initState)
this.store.setMaxListeners(12)
this.openPopup = opts.openPopup
this.migrateAddressBookState = opts.migrateAddressBookState
this._subscribeProviderType()
this.network = opts.network;
this.store = new ObservableStore(initState);
this.store.setMaxListeners(12);
this.openPopup = opts.openPopup;
this.migrateAddressBookState = opts.migrateAddressBookState;
this._subscribeProviderType();
global.setPreference = (key, value) => {
return this.setFeatureFlag(key, value)
}
return this.setFeatureFlag(key, value);
};
}
// PUBLIC METHODS
@ -85,7 +85,7 @@ export default class PreferencesController {
* @param {boolean} forgottenPassword - whether or not the user has forgotten their password
*/
setPasswordForgotten(forgottenPassword) {
this.store.updateState({ forgottenPassword })
this.store.updateState({ forgottenPassword });
}
/**
@ -95,7 +95,7 @@ export default class PreferencesController {
*
*/
setUseBlockie(val) {
this.store.updateState({ useBlockie: val })
this.store.updateState({ useBlockie: val });
}
/**
@ -105,7 +105,7 @@ export default class PreferencesController {
*
*/
setUseNonceField(val) {
this.store.updateState({ useNonceField: val })
this.store.updateState({ useNonceField: val });
}
/**
@ -115,7 +115,7 @@ export default class PreferencesController {
*
*/
setUsePhishDetect(val) {
this.store.updateState({ usePhishDetect: val })
this.store.updateState({ usePhishDetect: val });
}
/**
@ -125,15 +125,15 @@ export default class PreferencesController {
*
*/
setFirstTimeFlowType(type) {
this.store.updateState({ firstTimeFlowType: type })
this.store.updateState({ firstTimeFlowType: type });
}
getSuggestedTokens() {
return this.store.getState().suggestedTokens
return this.store.getState().suggestedTokens;
}
getAssetImages() {
return this.store.getState().assetImages
return this.store.getState().assetImages;
}
/**
@ -143,9 +143,9 @@ export default class PreferencesController {
* @param {string} methodData - Corresponding data method
*/
addKnownMethodData(fourBytePrefix, methodData) {
const { knownMethodData } = this.store.getState()
knownMethodData[fourBytePrefix] = methodData
this.store.updateState({ knownMethodData })
const { knownMethodData } = this.store.getState();
knownMethodData[fourBytePrefix] = methodData;
this.store.updateState({ knownMethodData });
}
/**
@ -154,15 +154,15 @@ export default class PreferencesController {
* @param {Object} req - The watchAsset JSON-RPC request object.
*/
async requestWatchAsset(req) {
const { type, options } = req.params
const { type, options } = req.params;
switch (type) {
case 'ERC20':
return await this._handleWatchAssetERC20(options)
return await this._handleWatchAssetERC20(options);
default:
throw ethErrors.rpc.invalidParams(
`Asset of type "${type}" not supported.`,
)
);
}
}
@ -175,12 +175,12 @@ export default class PreferencesController {
setCurrentLocale(key) {
const textDirection = ['ar', 'dv', 'fa', 'he', 'ku'].includes(key)
? 'rtl'
: 'auto'
: 'auto';
this.store.updateState({
currentLocale: key,
textDirection,
})
return textDirection
});
return textDirection;
}
/**
@ -191,26 +191,26 @@ export default class PreferencesController {
*
*/
setAddresses(addresses) {
const oldIdentities = this.store.getState().identities
const oldAccountTokens = this.store.getState().accountTokens
const oldAccountHiddenTokens = this.store.getState().accountHiddenTokens
const oldIdentities = this.store.getState().identities;
const oldAccountTokens = this.store.getState().accountTokens;
const oldAccountHiddenTokens = this.store.getState().accountHiddenTokens;
const identities = addresses.reduce((ids, address, index) => {
const oldId = oldIdentities[address] || {}
ids[address] = { name: `Account ${index + 1}`, address, ...oldId }
return ids
}, {})
const oldId = oldIdentities[address] || {};
ids[address] = { name: `Account ${index + 1}`, address, ...oldId };
return ids;
}, {});
const accountTokens = addresses.reduce((tokens, address) => {
const oldTokens = oldAccountTokens[address] || {}
tokens[address] = oldTokens
return tokens
}, {})
const oldTokens = oldAccountTokens[address] || {};
tokens[address] = oldTokens;
return tokens;
}, {});
const accountHiddenTokens = addresses.reduce((hiddenTokens, address) => {
const oldHiddenTokens = oldAccountHiddenTokens[address] || {}
hiddenTokens[address] = oldHiddenTokens
return hiddenTokens
}, {})
this.store.updateState({ identities, accountTokens, accountHiddenTokens })
const oldHiddenTokens = oldAccountHiddenTokens[address] || {};
hiddenTokens[address] = oldHiddenTokens;
return hiddenTokens;
}, {});
this.store.updateState({ identities, accountTokens, accountHiddenTokens });
}
/**
@ -224,23 +224,23 @@ export default class PreferencesController {
identities,
accountTokens,
accountHiddenTokens,
} = this.store.getState()
} = this.store.getState();
if (!identities[address]) {
throw new Error(`${address} can't be deleted cause it was not found`)
throw new Error(`${address} can't be deleted cause it was not found`);
}
delete identities[address]
delete accountTokens[address]
delete accountHiddenTokens[address]
this.store.updateState({ identities, accountTokens, accountHiddenTokens })
delete identities[address];
delete accountTokens[address];
delete accountHiddenTokens[address];
this.store.updateState({ identities, accountTokens, accountHiddenTokens });
// If the selected account is no longer valid,
// select an arbitrary other account:
if (address === this.getSelectedAddress()) {
const selected = Object.keys(identities)[0]
this.setSelectedAddress(selected)
const selected = Object.keys(identities)[0];
this.setSelectedAddress(selected);
}
return address
return address;
}
/**
@ -254,20 +254,20 @@ export default class PreferencesController {
identities,
accountTokens,
accountHiddenTokens,
} = this.store.getState()
} = this.store.getState();
addresses.forEach((address) => {
// skip if already exists
if (identities[address]) {
return
return;
}
// add missing identity
const identityCount = Object.keys(identities).length
const identityCount = Object.keys(identities).length;
accountTokens[address] = {}
accountHiddenTokens[address] = {}
identities[address] = { name: `Account ${identityCount + 1}`, address }
})
this.store.updateState({ identities, accountTokens, accountHiddenTokens })
accountTokens[address] = {};
accountHiddenTokens[address] = {};
identities[address] = { name: `Account ${identityCount + 1}`, address };
});
this.store.updateState({ identities, accountTokens, accountHiddenTokens });
}
/**
@ -279,46 +279,46 @@ export default class PreferencesController {
*/
syncAddresses(addresses) {
if (!Array.isArray(addresses) || addresses.length === 0) {
throw new Error('Expected non-empty array of addresses.')
throw new Error('Expected non-empty array of addresses.');
}
const { identities, lostIdentities } = this.store.getState()
const { identities, lostIdentities } = this.store.getState();
const newlyLost = {}
const newlyLost = {};
Object.keys(identities).forEach((identity) => {
if (!addresses.includes(identity)) {
newlyLost[identity] = identities[identity]
delete identities[identity]
newlyLost[identity] = identities[identity];
delete identities[identity];
}
})
});
// Identities are no longer present.
if (Object.keys(newlyLost).length > 0) {
// store lost accounts
Object.keys(newlyLost).forEach((key) => {
lostIdentities[key] = newlyLost[key]
})
lostIdentities[key] = newlyLost[key];
});
}
this.store.updateState({ identities, lostIdentities })
this.addAddresses(addresses)
this.store.updateState({ identities, lostIdentities });
this.addAddresses(addresses);
// If the selected account is no longer valid,
// select an arbitrary other account:
let selected = this.getSelectedAddress()
let selected = this.getSelectedAddress();
if (!addresses.includes(selected)) {
selected = addresses[0]
this.setSelectedAddress(selected)
selected = addresses[0];
this.setSelectedAddress(selected);
}
return selected
return selected;
}
removeSuggestedTokens() {
return new Promise((resolve) => {
this.store.updateState({ suggestedTokens: {} })
resolve({})
})
this.store.updateState({ suggestedTokens: {} });
resolve({});
});
}
/**
@ -329,18 +329,18 @@ export default class PreferencesController {
*
*/
setSelectedAddress(_address) {
const address = normalizeAddress(_address)
this._updateTokens(address)
const address = normalizeAddress(_address);
this._updateTokens(address);
const { identities, tokens } = this.store.getState()
const selectedIdentity = identities[address]
const { identities, tokens } = this.store.getState();
const selectedIdentity = identities[address];
if (!selectedIdentity) {
throw new Error(`Identity for '${address} not found`)
throw new Error(`Identity for '${address} not found`);
}
selectedIdentity.lastSelected = Date.now()
this.store.updateState({ identities, selectedAddress: address })
return Promise.resolve(tokens)
selectedIdentity.lastSelected = Date.now();
this.store.updateState({ identities, selectedAddress: address });
return Promise.resolve(tokens);
}
/**
@ -350,7 +350,7 @@ export default class PreferencesController {
*
*/
getSelectedAddress() {
return this.store.getState().selectedAddress
return this.store.getState().selectedAddress;
}
/**
@ -375,26 +375,26 @@ export default class PreferencesController {
*
*/
async addToken(rawAddress, symbol, decimals, image) {
const address = normalizeAddress(rawAddress)
const newEntry = { address, symbol, decimals }
const { tokens, hiddenTokens } = this.store.getState()
const assetImages = this.getAssetImages()
const address = normalizeAddress(rawAddress);
const newEntry = { address, symbol, decimals };
const { tokens, hiddenTokens } = this.store.getState();
const assetImages = this.getAssetImages();
const updatedHiddenTokens = hiddenTokens.filter(
(tokenAddress) => tokenAddress !== rawAddress.toLowerCase(),
)
);
const previousEntry = tokens.find((token) => {
return token.address === address
})
const previousIndex = tokens.indexOf(previousEntry)
return token.address === address;
});
const previousIndex = tokens.indexOf(previousEntry);
if (previousEntry) {
tokens[previousIndex] = newEntry
tokens[previousIndex] = newEntry;
} else {
tokens.push(newEntry)
tokens.push(newEntry);
}
assetImages[address] = image
this._updateAccountTokens(tokens, assetImages, updatedHiddenTokens)
return Promise.resolve(tokens)
assetImages[address] = image;
this._updateAccountTokens(tokens, assetImages, updatedHiddenTokens);
return Promise.resolve(tokens);
}
/**
@ -405,13 +405,15 @@ export default class PreferencesController {
*
*/
removeToken(rawAddress) {
const { tokens, hiddenTokens } = this.store.getState()
const assetImages = this.getAssetImages()
const updatedTokens = tokens.filter((token) => token.address !== rawAddress)
const updatedHiddenTokens = [...hiddenTokens, rawAddress.toLowerCase()]
delete assetImages[rawAddress]
this._updateAccountTokens(updatedTokens, assetImages, updatedHiddenTokens)
return Promise.resolve(updatedTokens)
const { tokens, hiddenTokens } = this.store.getState();
const assetImages = this.getAssetImages();
const updatedTokens = tokens.filter(
(token) => token.address !== rawAddress,
);
const updatedHiddenTokens = [...hiddenTokens, rawAddress.toLowerCase()];
delete assetImages[rawAddress];
this._updateAccountTokens(updatedTokens, assetImages, updatedHiddenTokens);
return Promise.resolve(updatedTokens);
}
/**
@ -421,7 +423,7 @@ export default class PreferencesController {
*
*/
getTokens() {
return this.store.getState().tokens
return this.store.getState().tokens;
}
/**
@ -434,14 +436,14 @@ export default class PreferencesController {
if (!account) {
throw new Error(
`setAccountLabel requires a valid address, got ${String(account)}`,
)
);
}
const address = normalizeAddress(account)
const { identities } = this.store.getState()
identities[address] = identities[address] || {}
identities[address].name = label
this.store.updateState({ identities })
return Promise.resolve(label)
const address = normalizeAddress(account);
const { identities } = this.store.getState();
identities[address] = identities[address] || {};
identities[address].name = label;
this.store.updateState({ identities });
return Promise.resolve(label);
}
/**
@ -456,33 +458,33 @@ export default class PreferencesController {
*
*/
async updateRpc(newRpcDetails) {
const rpcList = this.getFrequentRpcListDetail()
const rpcList = this.getFrequentRpcListDetail();
const index = rpcList.findIndex((element) => {
return element.rpcUrl === newRpcDetails.rpcUrl
})
return element.rpcUrl === newRpcDetails.rpcUrl;
});
if (index > -1) {
const rpcDetail = rpcList[index]
const updatedRpc = { ...rpcDetail, ...newRpcDetails }
const rpcDetail = rpcList[index];
const updatedRpc = { ...rpcDetail, ...newRpcDetails };
if (rpcDetail.chainId !== updatedRpc.chainId) {
// When the chainId is changed, associated address book entries should
// also be migrated. The address book entries are keyed by the `network` state,
// which for custom networks is the chainId with a fallback to the networkId
// if the chainId is not set.
let addressBookKey = rpcDetail.chainId
let addressBookKey = rpcDetail.chainId;
if (!addressBookKey) {
// We need to find the networkId to determine what these addresses were keyed by
const provider = new ethers.providers.JsonRpcProvider(
rpcDetail.rpcUrl,
)
);
try {
addressBookKey = await provider.send('net_version')
assert(typeof addressBookKey === 'string')
addressBookKey = await provider.send('net_version');
assert(typeof addressBookKey === 'string');
} catch (error) {
log.debug(error)
log.debug(error);
log.warn(
`Failed to get networkId from ${rpcDetail.rpcUrl}; skipping address book migration`,
)
);
}
}
@ -490,31 +492,37 @@ export default class PreferencesController {
// value. In this case, the contact book entries are duplicated so that they remain
// on both networks, since we don't know which network each contact is intended for.
let duplicate = false
let duplicate = false;
const builtInProviderNetworkIds = Object.values(
NETWORK_TYPE_TO_ID_MAP,
).map((ids) => ids.networkId)
).map((ids) => ids.networkId);
const otherRpcEntries = rpcList.filter(
(entry) => entry.rpcUrl !== newRpcDetails.rpcUrl,
)
);
if (
builtInProviderNetworkIds.includes(addressBookKey) ||
otherRpcEntries.some((entry) => entry.chainId === addressBookKey)
) {
duplicate = true
duplicate = true;
}
this.migrateAddressBookState(
addressBookKey,
updatedRpc.chainId,
duplicate,
)
);
}
rpcList[index] = updatedRpc
this.store.updateState({ frequentRpcListDetail: rpcList })
rpcList[index] = updatedRpc;
this.store.updateState({ frequentRpcListDetail: rpcList });
} else {
const { rpcUrl, chainId, ticker, nickname, rpcPrefs = {} } = newRpcDetails
this.addToFrequentRpcList(rpcUrl, chainId, ticker, nickname, rpcPrefs)
const {
rpcUrl,
chainId,
ticker,
nickname,
rpcPrefs = {},
} = newRpcDetails;
this.addToFrequentRpcList(rpcUrl, chainId, ticker, nickname, rpcPrefs);
}
}
@ -535,21 +543,21 @@ export default class PreferencesController {
nickname = '',
rpcPrefs = {},
) {
const rpcList = this.getFrequentRpcListDetail()
const rpcList = this.getFrequentRpcListDetail();
const index = rpcList.findIndex((element) => {
return element.rpcUrl === rpcUrl
})
return element.rpcUrl === rpcUrl;
});
if (index !== -1) {
rpcList.splice(index, 1)
rpcList.splice(index, 1);
}
if (!isPrefixedFormattedHexString(chainId)) {
throw new Error(`Invalid chainId: "${chainId}"`)
throw new Error(`Invalid chainId: "${chainId}"`);
}
rpcList.push({ rpcUrl, chainId, ticker, nickname, rpcPrefs })
this.store.updateState({ frequentRpcListDetail: rpcList })
rpcList.push({ rpcUrl, chainId, ticker, nickname, rpcPrefs });
this.store.updateState({ frequentRpcListDetail: rpcList });
}
/**
@ -560,15 +568,15 @@ export default class PreferencesController {
*
*/
removeFromFrequentRpcList(url) {
const rpcList = this.getFrequentRpcListDetail()
const rpcList = this.getFrequentRpcListDetail();
const index = rpcList.findIndex((element) => {
return element.rpcUrl === url
})
return element.rpcUrl === url;
});
if (index !== -1) {
rpcList.splice(index, 1)
rpcList.splice(index, 1);
}
this.store.updateState({ frequentRpcListDetail: rpcList })
return Promise.resolve(rpcList)
this.store.updateState({ frequentRpcListDetail: rpcList });
return Promise.resolve(rpcList);
}
/**
@ -578,7 +586,7 @@ export default class PreferencesController {
*
*/
getFrequentRpcListDetail() {
return this.store.getState().frequentRpcListDetail
return this.store.getState().frequentRpcListDetail;
}
/**
@ -590,15 +598,15 @@ export default class PreferencesController {
*
*/
setFeatureFlag(feature, activated) {
const currentFeatureFlags = this.store.getState().featureFlags
const currentFeatureFlags = this.store.getState().featureFlags;
const updatedFeatureFlags = {
...currentFeatureFlags,
[feature]: activated,
}
};
this.store.updateState({ featureFlags: updatedFeatureFlags })
this.store.updateState({ featureFlags: updatedFeatureFlags });
return Promise.resolve(updatedFeatureFlags)
return Promise.resolve(updatedFeatureFlags);
}
/**
@ -609,14 +617,14 @@ export default class PreferencesController {
* @returns {Promise<object>} Promises a new object; the updated preferences object.
*/
setPreference(preference, value) {
const currentPreferences = this.getPreferences()
const currentPreferences = this.getPreferences();
const updatedPreferences = {
...currentPreferences,
[preference]: value,
}
};
this.store.updateState({ preferences: updatedPreferences })
return Promise.resolve(updatedPreferences)
this.store.updateState({ preferences: updatedPreferences });
return Promise.resolve(updatedPreferences);
}
/**
@ -624,7 +632,7 @@ export default class PreferencesController {
* @returns {Object} A key-boolean map of user-selected preferences.
*/
getPreferences() {
return this.store.getState().preferences
return this.store.getState().preferences;
}
/**
@ -632,8 +640,8 @@ export default class PreferencesController {
* onboarding process.
*/
completeOnboarding() {
this.store.updateState({ completedOnboarding: true })
return Promise.resolve(true)
this.store.updateState({ completedOnboarding: true });
return Promise.resolve(true);
}
/**
@ -641,7 +649,7 @@ export default class PreferencesController {
* @returns {string} The current IPFS gateway domain
*/
getIpfsGateway() {
return this.store.getState().ipfsGateway
return this.store.getState().ipfsGateway;
}
/**
@ -650,8 +658,8 @@ export default class PreferencesController {
* @returns {Promise<string>} A promise of the update IPFS gateway domain
*/
setIpfsGateway(domain) {
this.store.updateState({ ipfsGateway: domain })
return Promise.resolve(domain)
this.store.updateState({ ipfsGateway: domain });
return Promise.resolve(domain);
}
//
@ -665,9 +673,9 @@ export default class PreferencesController {
*/
_subscribeProviderType() {
this.network.providerStore.subscribe(() => {
const { tokens, hiddenTokens } = this._getTokenRelatedStates()
this._updateAccountTokens(tokens, this.getAssetImages(), hiddenTokens)
})
const { tokens, hiddenTokens } = this._getTokenRelatedStates();
this._updateAccountTokens(tokens, this.getAssetImages(), hiddenTokens);
});
}
/**
@ -684,16 +692,16 @@ export default class PreferencesController {
providerType,
selectedAddress,
accountHiddenTokens,
} = this._getTokenRelatedStates()
accountTokens[selectedAddress][providerType] = tokens
accountHiddenTokens[selectedAddress][providerType] = hiddenTokens
} = this._getTokenRelatedStates();
accountTokens[selectedAddress][providerType] = tokens;
accountHiddenTokens[selectedAddress][providerType] = hiddenTokens;
this.store.updateState({
accountTokens,
tokens,
assetImages,
accountHiddenTokens,
hiddenTokens,
})
});
}
/**
@ -705,8 +713,8 @@ export default class PreferencesController {
_updateTokens(selectedAddress) {
const { tokens, hiddenTokens } = this._getTokenRelatedStates(
selectedAddress,
)
this.store.updateState({ tokens, hiddenTokens })
);
this.store.updateState({ tokens, hiddenTokens });
}
/**
@ -717,26 +725,26 @@ export default class PreferencesController {
*
*/
_getTokenRelatedStates(selectedAddress) {
const { accountTokens, accountHiddenTokens } = this.store.getState()
const { accountTokens, accountHiddenTokens } = this.store.getState();
if (!selectedAddress) {
// eslint-disable-next-line no-param-reassign
selectedAddress = this.store.getState().selectedAddress
selectedAddress = this.store.getState().selectedAddress;
}
const providerType = this.network.providerStore.getState().type
const providerType = this.network.providerStore.getState().type;
if (!(selectedAddress in accountTokens)) {
accountTokens[selectedAddress] = {}
accountTokens[selectedAddress] = {};
}
if (!(selectedAddress in accountHiddenTokens)) {
accountHiddenTokens[selectedAddress] = {}
accountHiddenTokens[selectedAddress] = {};
}
if (!(providerType in accountTokens[selectedAddress])) {
accountTokens[selectedAddress][providerType] = []
accountTokens[selectedAddress][providerType] = [];
}
if (!(providerType in accountHiddenTokens[selectedAddress])) {
accountHiddenTokens[selectedAddress][providerType] = []
accountHiddenTokens[selectedAddress][providerType] = [];
}
const tokens = accountTokens[selectedAddress][providerType]
const hiddenTokens = accountHiddenTokens[selectedAddress][providerType]
const tokens = accountTokens[selectedAddress][providerType];
const hiddenTokens = accountHiddenTokens[selectedAddress][providerType];
return {
tokens,
accountTokens,
@ -744,7 +752,7 @@ export default class PreferencesController {
accountHiddenTokens,
providerType,
selectedAddress,
}
};
}
/**
@ -754,17 +762,17 @@ export default class PreferencesController {
*
*/
async _handleWatchAssetERC20(tokenMetadata) {
this._validateERC20AssetParams(tokenMetadata)
this._validateERC20AssetParams(tokenMetadata);
const address = normalizeAddress(tokenMetadata.address)
const { symbol, decimals, image } = tokenMetadata
this._addSuggestedERC20Asset(address, symbol, decimals, image)
const address = normalizeAddress(tokenMetadata.address);
const { symbol, decimals, image } = tokenMetadata;
this._addSuggestedERC20Asset(address, symbol, decimals, image);
await this.openPopup()
await this.openPopup();
const tokenAddresses = this.getTokens().filter(
(token) => token.address === address,
)
return tokenAddresses.length > 0
);
return tokenAddresses.length > 0;
}
/**
@ -779,24 +787,24 @@ export default class PreferencesController {
if (!address || !symbol || typeof decimals === 'undefined') {
throw ethErrors.rpc.invalidParams(
`Must specify address, symbol, and decimals.`,
)
);
}
if (typeof symbol !== 'string') {
throw ethErrors.rpc.invalidParams(`Invalid symbol: not a string.`)
throw ethErrors.rpc.invalidParams(`Invalid symbol: not a string.`);
}
if (!(symbol.length < 7)) {
throw ethErrors.rpc.invalidParams(
`Invalid symbol "${symbol}": longer than 6 characters.`,
)
);
}
const numDecimals = parseInt(decimals, 10)
const numDecimals = parseInt(decimals, 10);
if (isNaN(numDecimals) || numDecimals > 36 || numDecimals < 0) {
throw ethErrors.rpc.invalidParams(
`Invalid decimals "${decimals}": must be 0 <= 36.`,
)
);
}
if (!isValidAddress(address)) {
throw ethErrors.rpc.invalidParams(`Invalid address "${address}".`)
throw ethErrors.rpc.invalidParams(`Invalid address "${address}".`);
}
}
@ -807,9 +815,9 @@ export default class PreferencesController {
decimals,
image,
unlisted: !LISTED_CONTRACT_ADDRESSES.includes(address),
}
const suggested = this.getSuggestedTokens()
suggested[address] = newEntry
this.store.updateState({ suggestedTokens: suggested })
};
const suggested = this.getSuggestedTokens();
suggested[address] = newEntry;
this.store.updateState({ suggestedTokens: suggested });
}
}

@ -1,54 +1,57 @@
import { ethers } from 'ethers'
import log from 'loglevel'
import BigNumber from 'bignumber.js'
import { ObservableStore } from '@metamask/obs-store'
import { mapValues, cloneDeep } from 'lodash'
import abi from 'human-standard-token-abi'
import { calcTokenAmount } from '../../../ui/app/helpers/utils/token-util'
import { calcGasTotal } from '../../../ui/app/pages/send/send.utils'
import { conversionUtil } from '../../../ui/app/helpers/utils/conversion-util'
import { ethers } from 'ethers';
import log from 'loglevel';
import BigNumber from 'bignumber.js';
import { ObservableStore } from '@metamask/obs-store';
import { mapValues, cloneDeep } from 'lodash';
import abi from 'human-standard-token-abi';
import { calcTokenAmount } from '../../../ui/app/helpers/utils/token-util';
import { calcGasTotal } from '../../../ui/app/pages/send/send.utils';
import { conversionUtil } from '../../../ui/app/helpers/utils/conversion-util';
import {
ETH_SWAPS_TOKEN_ADDRESS,
DEFAULT_ERC20_APPROVE_GAS,
QUOTES_EXPIRED_ERROR,
QUOTES_NOT_AVAILABLE_ERROR,
SWAPS_FETCH_ORDER_CONFLICT,
} from '../../../ui/app/helpers/constants/swaps'
} from '../../../ui/app/helpers/constants/swaps';
import {
fetchTradesInfo as defaultFetchTradesInfo,
fetchSwapsFeatureLiveness as defaultFetchSwapsFeatureLiveness,
fetchSwapsQuoteRefreshTime as defaultFetchSwapsQuoteRefreshTime,
} from '../../../ui/app/pages/swaps/swaps.util'
} from '../../../ui/app/pages/swaps/swaps.util';
const METASWAP_ADDRESS = '0x881d40237659c251811cec9c364ef91dc08d300c'
const METASWAP_ADDRESS = '0x881d40237659c251811cec9c364ef91dc08d300c';
// The MAX_GAS_LIMIT is a number that is higher than the maximum gas costs we have observed on any aggregator
const MAX_GAS_LIMIT = 2500000
const MAX_GAS_LIMIT = 2500000;
// To ensure that our serves are not spammed if MetaMask is left idle, we limit the number of fetches for quotes that are made on timed intervals.
// 3 seems to be an appropriate balance of giving users the time they need when MetaMask is not left idle, and turning polling off when it is.
const POLL_COUNT_LIMIT = 3
const POLL_COUNT_LIMIT = 3;
// If for any reason the MetaSwap API fails to provide a refresh time,
// provide a reasonable fallback to avoid further errors
const FALLBACK_QUOTE_REFRESH_TIME = 60000
const FALLBACK_QUOTE_REFRESH_TIME = 60000;
// This is the amount of time to wait, after successfully fetching quotes
// and their gas estimates, before fetching for new quotes
const QUOTE_POLLING_DIFFERENCE_INTERVAL = 10 * 1000
const QUOTE_POLLING_DIFFERENCE_INTERVAL = 10 * 1000;
function calculateGasEstimateWithRefund(
maxGas = MAX_GAS_LIMIT,
estimatedRefund = 0,
estimatedGas = 0,
) {
const maxGasMinusRefund = new BigNumber(maxGas, 10).minus(estimatedRefund, 10)
const maxGasMinusRefund = new BigNumber(maxGas, 10).minus(
estimatedRefund,
10,
);
const gasEstimateWithRefund = maxGasMinusRefund.lt(estimatedGas, 16)
? maxGasMinusRefund.toString(16)
: estimatedGas
: estimatedGas;
return gasEstimateWithRefund
return gasEstimateWithRefund;
}
const initialState = {
@ -69,7 +72,7 @@ const initialState = {
swapsFeatureIsLive: false,
swapsQuoteRefreshTime: FALLBACK_QUOTE_REFRESH_TIME,
},
}
};
export default class SwapsController {
constructor({
@ -84,46 +87,46 @@ export default class SwapsController {
}) {
this.store = new ObservableStore({
swapsState: { ...initialState.swapsState },
})
});
this._fetchTradesInfo = fetchTradesInfo
this._fetchSwapsFeatureLiveness = fetchSwapsFeatureLiveness
this._fetchSwapsQuoteRefreshTime = fetchSwapsQuoteRefreshTime
this._fetchTradesInfo = fetchTradesInfo;
this._fetchSwapsFeatureLiveness = fetchSwapsFeatureLiveness;
this._fetchSwapsQuoteRefreshTime = fetchSwapsQuoteRefreshTime;
this.getBufferedGasLimit = getBufferedGasLimit
this.tokenRatesStore = tokenRatesStore
this.getBufferedGasLimit = getBufferedGasLimit;
this.tokenRatesStore = tokenRatesStore;
this.pollCount = 0
this.getProviderConfig = getProviderConfig
this.pollCount = 0;
this.getProviderConfig = getProviderConfig;
this.indexOfNewestCallInFlight = 0
this.indexOfNewestCallInFlight = 0;
this.ethersProvider = new ethers.providers.Web3Provider(provider)
this._currentNetwork = networkController.store.getState().network
this.ethersProvider = new ethers.providers.Web3Provider(provider);
this._currentNetwork = networkController.store.getState().network;
networkController.on('networkDidChange', (network) => {
if (network !== 'loading' && network !== this._currentNetwork) {
this._currentNetwork = network
this.ethersProvider = new ethers.providers.Web3Provider(provider)
this._currentNetwork = network;
this.ethersProvider = new ethers.providers.Web3Provider(provider);
}
})
});
this._setupSwapsLivenessFetching()
this._setupSwapsLivenessFetching();
}
// Sets the refresh rate for quote updates from the MetaSwap API
async _setSwapsQuoteRefreshTime() {
// Default to fallback time unless API returns valid response
let swapsQuoteRefreshTime = FALLBACK_QUOTE_REFRESH_TIME
let swapsQuoteRefreshTime = FALLBACK_QUOTE_REFRESH_TIME;
try {
swapsQuoteRefreshTime = await this._fetchSwapsQuoteRefreshTime()
swapsQuoteRefreshTime = await this._fetchSwapsQuoteRefreshTime();
} catch (e) {
console.error('Request for swaps quote refresh time failed: ', e)
console.error('Request for swaps quote refresh time failed: ', e);
}
const { swapsState } = this.store.getState()
const { swapsState } = this.store.getState();
this.store.updateState({
swapsState: { ...swapsState, swapsQuoteRefreshTime },
})
});
}
// Once quotes are fetched, we poll for new ones to keep the quotes up to date. Market and aggregator contract conditions can change fast enough
@ -133,20 +136,20 @@ export default class SwapsController {
pollForNewQuotes() {
const {
swapsState: { swapsQuoteRefreshTime },
} = this.store.getState()
} = this.store.getState();
this.pollingTimeout = setTimeout(() => {
const { swapsState } = this.store.getState()
const { swapsState } = this.store.getState();
this.fetchAndSetQuotes(
swapsState.fetchParams,
swapsState.fetchParams?.metaData,
true,
)
}, swapsQuoteRefreshTime - QUOTE_POLLING_DIFFERENCE_INTERVAL)
);
}, swapsQuoteRefreshTime - QUOTE_POLLING_DIFFERENCE_INTERVAL);
}
stopPollingForQuotes() {
clearTimeout(this.pollingTimeout)
clearTimeout(this.pollingTimeout);
}
async fetchAndSetQuotes(
@ -155,37 +158,37 @@ export default class SwapsController {
isPolledRequest,
) {
if (!fetchParams) {
return null
return null;
}
// Every time we get a new request that is not from the polling, we reset the poll count so we can poll for up to three more sets of quotes with these new params.
if (!isPolledRequest) {
this.pollCount = 0
this.pollCount = 0;
}
// If there are any pending poll requests, clear them so that they don't get call while this new fetch is in process
clearTimeout(this.pollingTimeout)
clearTimeout(this.pollingTimeout);
if (!isPolledRequest) {
this.setSwapsErrorKey('')
this.setSwapsErrorKey('');
}
const indexOfCurrentCall = this.indexOfNewestCallInFlight + 1
this.indexOfNewestCallInFlight = indexOfCurrentCall
const indexOfCurrentCall = this.indexOfNewestCallInFlight + 1;
this.indexOfNewestCallInFlight = indexOfCurrentCall;
let [newQuotes] = await Promise.all([
this._fetchTradesInfo(fetchParams),
this._setSwapsQuoteRefreshTime(),
])
]);
newQuotes = mapValues(newQuotes, (quote) => ({
...quote,
sourceTokenInfo: fetchParamsMetaData.sourceTokenInfo,
destinationTokenInfo: fetchParamsMetaData.destinationTokenInfo,
}))
}));
const quotesLastFetched = Date.now()
const quotesLastFetched = Date.now();
let approvalRequired = false
let approvalRequired = false;
if (
fetchParams.sourceToken !== ETH_SWAPS_TOKEN_ADDRESS &&
Object.values(newQuotes).length
@ -193,22 +196,22 @@ export default class SwapsController {
const allowance = await this._getERC20Allowance(
fetchParams.sourceToken,
fetchParams.fromAddress,
)
);
// For a user to be able to swap a token, they need to have approved the MetaSwap contract to withdraw that token.
// _getERC20Allowance() returns the amount of the token they have approved for withdrawal. If that amount is greater
// than 0, it means that approval has already occured and is not needed. Otherwise, for tokens to be swapped, a new
// call of the ERC-20 approve method is required.
approvalRequired = allowance.eq(0)
approvalRequired = allowance.eq(0);
if (!approvalRequired) {
newQuotes = mapValues(newQuotes, (quote) => ({
...quote,
approvalNeeded: null,
}))
}));
} else if (!isPolledRequest) {
const { gasLimit: approvalGas } = await this.timedoutGasReturn(
Object.values(newQuotes)[0].approvalNeeded,
)
);
newQuotes = mapValues(newQuotes, (quote) => ({
...quote,
@ -216,39 +219,39 @@ export default class SwapsController {
...quote.approvalNeeded,
gas: approvalGas || DEFAULT_ERC20_APPROVE_GAS,
},
}))
}));
}
}
let topAggId = null
let topAggId = null;
// We can reduce time on the loading screen by only doing this after the
// loading screen and best quote have rendered.
if (!approvalRequired && !fetchParams?.balanceError) {
newQuotes = await this.getAllQuotesWithGasEstimates(newQuotes)
newQuotes = await this.getAllQuotesWithGasEstimates(newQuotes);
}
if (Object.values(newQuotes).length === 0) {
this.setSwapsErrorKey(QUOTES_NOT_AVAILABLE_ERROR)
this.setSwapsErrorKey(QUOTES_NOT_AVAILABLE_ERROR);
} else {
const [
_topAggId,
quotesWithSavingsAndFeeData,
] = await this._findTopQuoteAndCalculateSavings(newQuotes)
topAggId = _topAggId
newQuotes = quotesWithSavingsAndFeeData
] = await this._findTopQuoteAndCalculateSavings(newQuotes);
topAggId = _topAggId;
newQuotes = quotesWithSavingsAndFeeData;
}
// If a newer call has been made, don't update state with old information
// Prevents timing conflicts between fetches
if (this.indexOfNewestCallInFlight !== indexOfCurrentCall) {
throw new Error(SWAPS_FETCH_ORDER_CONFLICT)
throw new Error(SWAPS_FETCH_ORDER_CONFLICT);
}
const { swapsState } = this.store.getState()
let { selectedAggId } = swapsState
const { swapsState } = this.store.getState();
let { selectedAggId } = swapsState;
if (!newQuotes[selectedAggId]) {
selectedAggId = null
selectedAggId = null;
}
this.store.updateState({
@ -260,41 +263,41 @@ export default class SwapsController {
selectedAggId,
topAggId,
},
})
});
// We only want to do up to a maximum of three requests from polling.
this.pollCount += 1
this.pollCount += 1;
if (this.pollCount < POLL_COUNT_LIMIT + 1) {
this.pollForNewQuotes()
this.pollForNewQuotes();
} else {
this.resetPostFetchState()
this.setSwapsErrorKey(QUOTES_EXPIRED_ERROR)
return null
this.resetPostFetchState();
this.setSwapsErrorKey(QUOTES_EXPIRED_ERROR);
return null;
}
return [newQuotes, topAggId]
return [newQuotes, topAggId];
}
safeRefetchQuotes() {
const { swapsState } = this.store.getState()
const { swapsState } = this.store.getState();
if (!this.pollingTimeout && swapsState.fetchParams) {
this.fetchAndSetQuotes(swapsState.fetchParams)
this.fetchAndSetQuotes(swapsState.fetchParams);
}
}
setSelectedQuoteAggId(selectedAggId) {
const { swapsState } = this.store.getState()
this.store.updateState({ swapsState: { ...swapsState, selectedAggId } })
const { swapsState } = this.store.getState();
this.store.updateState({ swapsState: { ...swapsState, selectedAggId } });
}
setSwapsTokens(tokens) {
const { swapsState } = this.store.getState()
this.store.updateState({ swapsState: { ...swapsState, tokens } })
const { swapsState } = this.store.getState();
this.store.updateState({ swapsState: { ...swapsState, tokens } });
}
setSwapsErrorKey(errorKey) {
const { swapsState } = this.store.getState()
this.store.updateState({ swapsState: { ...swapsState, errorKey } })
const { swapsState } = this.store.getState();
this.store.updateState({ swapsState: { ...swapsState, errorKey } });
}
async getAllQuotesWithGasEstimates(quotes) {
@ -302,43 +305,43 @@ export default class SwapsController {
Object.values(quotes).map(async (quote) => {
const { gasLimit, simulationFails } = await this.timedoutGasReturn(
quote.trade,
)
return [gasLimit, simulationFails, quote.aggregator]
);
return [gasLimit, simulationFails, quote.aggregator];
}),
)
);
const newQuotes = {}
const newQuotes = {};
quoteGasData.forEach(([gasLimit, simulationFails, aggId]) => {
if (gasLimit && !simulationFails) {
const gasEstimateWithRefund = calculateGasEstimateWithRefund(
quotes[aggId].maxGas,
quotes[aggId].estimatedRefund,
gasLimit,
)
);
newQuotes[aggId] = {
...quotes[aggId],
gasEstimate: gasLimit,
gasEstimateWithRefund,
}
};
} else if (quotes[aggId].approvalNeeded) {
// If gas estimation fails, but an ERC-20 approve is needed, then we do not add any estimate property to the quote object
// Such quotes will rely on the maxGas and averageGas properties from the api
newQuotes[aggId] = quotes[aggId]
newQuotes[aggId] = quotes[aggId];
}
// If gas estimation fails and no approval is needed, then we filter that quote out, so that it is not shown to the user
})
return newQuotes
});
return newQuotes;
}
timedoutGasReturn(tradeTxParams) {
return new Promise((resolve) => {
let gasTimedOut = false
let gasTimedOut = false;
const gasTimeout = setTimeout(() => {
gasTimedOut = true
resolve({ gasLimit: null, simulationFails: true })
}, 5000)
gasTimedOut = true;
resolve({ gasLimit: null, simulationFails: true });
}, 5000);
// Remove gas from params that will be passed to the `estimateGas` call
// Including it can cause the estimate to fail if the actual gas needed
@ -348,44 +351,44 @@ export default class SwapsController {
from: tradeTxParams.from,
to: tradeTxParams.to,
value: tradeTxParams.value,
}
};
this.getBufferedGasLimit({ txParams: tradeTxParamsForGasEstimate }, 1)
.then(({ gasLimit, simulationFails }) => {
if (!gasTimedOut) {
clearTimeout(gasTimeout)
resolve({ gasLimit, simulationFails })
clearTimeout(gasTimeout);
resolve({ gasLimit, simulationFails });
}
})
.catch((e) => {
log.error(e)
log.error(e);
if (!gasTimedOut) {
clearTimeout(gasTimeout)
resolve({ gasLimit: null, simulationFails: true })
clearTimeout(gasTimeout);
resolve({ gasLimit: null, simulationFails: true });
}
})
})
});
});
}
async setInitialGasEstimate(initialAggId) {
const { swapsState } = this.store.getState()
const { swapsState } = this.store.getState();
const quoteToUpdate = { ...swapsState.quotes[initialAggId] }
const quoteToUpdate = { ...swapsState.quotes[initialAggId] };
const {
gasLimit: newGasEstimate,
simulationFails,
} = await this.timedoutGasReturn(quoteToUpdate.trade)
} = await this.timedoutGasReturn(quoteToUpdate.trade);
if (newGasEstimate && !simulationFails) {
const gasEstimateWithRefund = calculateGasEstimateWithRefund(
quoteToUpdate.maxGas,
quoteToUpdate.estimatedRefund,
newGasEstimate,
)
);
quoteToUpdate.gasEstimate = newGasEstimate
quoteToUpdate.gasEstimateWithRefund = gasEstimateWithRefund
quoteToUpdate.gasEstimate = newGasEstimate;
quoteToUpdate.gasEstimateWithRefund = gasEstimateWithRefund;
}
this.store.updateState({
@ -393,59 +396,61 @@ export default class SwapsController {
...swapsState,
quotes: { ...swapsState.quotes, [initialAggId]: quoteToUpdate },
},
})
});
}
setApproveTxId(approveTxId) {
const { swapsState } = this.store.getState()
this.store.updateState({ swapsState: { ...swapsState, approveTxId } })
const { swapsState } = this.store.getState();
this.store.updateState({ swapsState: { ...swapsState, approveTxId } });
}
setTradeTxId(tradeTxId) {
const { swapsState } = this.store.getState()
this.store.updateState({ swapsState: { ...swapsState, tradeTxId } })
const { swapsState } = this.store.getState();
this.store.updateState({ swapsState: { ...swapsState, tradeTxId } });
}
setQuotesLastFetched(quotesLastFetched) {
const { swapsState } = this.store.getState()
this.store.updateState({ swapsState: { ...swapsState, quotesLastFetched } })
const { swapsState } = this.store.getState();
this.store.updateState({
swapsState: { ...swapsState, quotesLastFetched },
});
}
setSwapsTxGasPrice(gasPrice) {
const { swapsState } = this.store.getState()
const { swapsState } = this.store.getState();
this.store.updateState({
swapsState: { ...swapsState, customGasPrice: gasPrice },
})
});
}
setSwapsTxGasLimit(gasLimit) {
const { swapsState } = this.store.getState()
const { swapsState } = this.store.getState();
this.store.updateState({
swapsState: { ...swapsState, customMaxGas: gasLimit },
})
});
}
setCustomApproveTxData(data) {
const { swapsState } = this.store.getState()
const { swapsState } = this.store.getState();
this.store.updateState({
swapsState: { ...swapsState, customApproveTxData: data },
})
});
}
setBackgroundSwapRouteState(routeState) {
const { swapsState } = this.store.getState()
this.store.updateState({ swapsState: { ...swapsState, routeState } })
const { swapsState } = this.store.getState();
this.store.updateState({ swapsState: { ...swapsState, routeState } });
}
setSwapsLiveness(swapsFeatureIsLive) {
const { swapsState } = this.store.getState()
const { swapsState } = this.store.getState();
this.store.updateState({
swapsState: { ...swapsState, swapsFeatureIsLive },
})
});
}
resetPostFetchState() {
const { swapsState } = this.store.getState()
const { swapsState } = this.store.getState();
this.store.updateState({
swapsState: {
@ -455,12 +460,12 @@ export default class SwapsController {
swapsFeatureIsLive: swapsState.swapsFeatureIsLive,
swapsQuoteRefreshTime: swapsState.swapsQuoteRefreshTime,
},
})
clearTimeout(this.pollingTimeout)
});
clearTimeout(this.pollingTimeout);
}
resetSwapsState() {
const { swapsState } = this.store.getState()
const { swapsState } = this.store.getState();
this.store.updateState({
swapsState: {
@ -469,33 +474,33 @@ export default class SwapsController {
swapsFeatureIsLive: swapsState.swapsFeatureIsLive,
swapsQuoteRefreshTime: swapsState.swapsQuoteRefreshTime,
},
})
clearTimeout(this.pollingTimeout)
});
clearTimeout(this.pollingTimeout);
}
async _getEthersGasPrice() {
const ethersGasPrice = await this.ethersProvider.getGasPrice()
return ethersGasPrice.toHexString()
const ethersGasPrice = await this.ethersProvider.getGasPrice();
return ethersGasPrice.toHexString();
}
async _findTopQuoteAndCalculateSavings(quotes = {}) {
const tokenConversionRates = this.tokenRatesStore.getState()
.contractExchangeRates
.contractExchangeRates;
const {
swapsState: { customGasPrice },
} = this.store.getState()
} = this.store.getState();
const numQuotes = Object.keys(quotes).length
const numQuotes = Object.keys(quotes).length;
if (!numQuotes) {
return {}
return {};
}
const newQuotes = cloneDeep(quotes)
const newQuotes = cloneDeep(quotes);
const usedGasPrice = customGasPrice || (await this._getEthersGasPrice())
const usedGasPrice = customGasPrice || (await this._getEthersGasPrice());
let topAggId = null
let overallValueOfBestQuoteForSorting = null
let topAggId = null;
let overallValueOfBestQuoteForSorting = null;
Object.values(newQuotes).forEach((quote) => {
const {
@ -510,20 +515,20 @@ export default class SwapsController {
sourceToken,
trade,
fee: metaMaskFee,
} = quote
} = quote;
const tradeGasLimitForCalculation = gasEstimate
? new BigNumber(gasEstimate, 16)
: new BigNumber(averageGas || MAX_GAS_LIMIT, 10)
: new BigNumber(averageGas || MAX_GAS_LIMIT, 10);
const totalGasLimitForCalculation = tradeGasLimitForCalculation
.plus(approvalNeeded?.gas || '0x0', 16)
.toString(16)
.toString(16);
const gasTotalInWeiHex = calcGasTotal(
totalGasLimitForCalculation,
usedGasPrice,
)
);
// trade.value is a sum of different values depending on the transaction.
// It always includes any external fees charged by the quote source. In
@ -532,7 +537,7 @@ export default class SwapsController {
const totalWeiCost = new BigNumber(gasTotalInWeiHex, 16).plus(
trade.value,
16,
)
);
const totalEthCost = conversionUtil(totalWeiCost, {
fromCurrency: 'ETH',
@ -540,7 +545,7 @@ export default class SwapsController {
toDenomination: 'ETH',
fromNumericBase: 'BN',
numberOfDecimals: 6,
})
});
// The total fee is aggregator/exchange fees plus gas fees.
// If the swap is from ETH, subtract the sourceAmount from the total cost.
@ -557,103 +562,103 @@ export default class SwapsController {
numberOfDecimals: 6,
},
)
: totalEthCost
: totalEthCost;
const decimalAdjustedDestinationAmount = calcTokenAmount(
destinationAmount,
destinationTokenInfo.decimals,
)
);
const tokenPercentageOfPreFeeDestAmount = new BigNumber(100, 10)
.minus(metaMaskFee, 10)
.div(100)
.div(100);
const destinationAmountBeforeMetaMaskFee = decimalAdjustedDestinationAmount.div(
tokenPercentageOfPreFeeDestAmount,
)
);
const metaMaskFeeInTokens = destinationAmountBeforeMetaMaskFee.minus(
decimalAdjustedDestinationAmount,
)
);
const tokenConversionRate = tokenConversionRates[destinationToken]
const conversionRateForSorting = tokenConversionRate || 1
const tokenConversionRate = tokenConversionRates[destinationToken];
const conversionRateForSorting = tokenConversionRate || 1;
const ethValueOfTokens = decimalAdjustedDestinationAmount.times(
conversionRateForSorting,
10,
)
);
const conversionRateForCalculations =
destinationToken === ETH_SWAPS_TOKEN_ADDRESS ? 1 : tokenConversionRate
destinationToken === ETH_SWAPS_TOKEN_ADDRESS ? 1 : tokenConversionRate;
const overallValueOfQuoteForSorting =
conversionRateForCalculations === undefined
? ethValueOfTokens
: ethValueOfTokens.minus(ethFee, 10)
: ethValueOfTokens.minus(ethFee, 10);
quote.ethFee = ethFee.toString(10)
quote.ethFee = ethFee.toString(10);
if (conversionRateForCalculations !== undefined) {
quote.ethValueOfTokens = ethValueOfTokens.toString(10)
quote.overallValueOfQuote = overallValueOfQuoteForSorting.toString(10)
quote.ethValueOfTokens = ethValueOfTokens.toString(10);
quote.overallValueOfQuote = overallValueOfQuoteForSorting.toString(10);
quote.metaMaskFeeInEth = metaMaskFeeInTokens
.times(conversionRateForCalculations)
.toString(10)
.toString(10);
}
if (
overallValueOfBestQuoteForSorting === null ||
overallValueOfQuoteForSorting.gt(overallValueOfBestQuoteForSorting)
) {
topAggId = aggregator
overallValueOfBestQuoteForSorting = overallValueOfQuoteForSorting
topAggId = aggregator;
overallValueOfBestQuoteForSorting = overallValueOfQuoteForSorting;
}
})
});
const isBest =
newQuotes[topAggId].destinationToken === ETH_SWAPS_TOKEN_ADDRESS ||
Boolean(tokenConversionRates[newQuotes[topAggId]?.destinationToken])
Boolean(tokenConversionRates[newQuotes[topAggId]?.destinationToken]);
let savings = null
let savings = null;
if (isBest) {
const bestQuote = newQuotes[topAggId]
const bestQuote = newQuotes[topAggId];
savings = {}
savings = {};
const {
ethFee: medianEthFee,
metaMaskFeeInEth: medianMetaMaskFee,
ethValueOfTokens: medianEthValueOfTokens,
} = getMedianEthValueQuote(Object.values(newQuotes))
} = getMedianEthValueQuote(Object.values(newQuotes));
// Performance savings are calculated as:
// (ethValueOfTokens for the best trade) - (ethValueOfTokens for the media trade)
savings.performance = new BigNumber(bestQuote.ethValueOfTokens, 10).minus(
medianEthValueOfTokens,
10,
)
);
// Fee savings are calculated as:
// (fee for the median trade) - (fee for the best trade)
savings.fee = new BigNumber(medianEthFee).minus(bestQuote.ethFee, 10)
savings.fee = new BigNumber(medianEthFee).minus(bestQuote.ethFee, 10);
savings.metaMaskFee = bestQuote.metaMaskFeeInEth
savings.metaMaskFee = bestQuote.metaMaskFeeInEth;
// Total savings are calculated as:
// performance savings + fee savings - metamask fee
savings.total = savings.performance
.plus(savings.fee)
.minus(savings.metaMaskFee)
.toString(10)
savings.performance = savings.performance.toString(10)
savings.fee = savings.fee.toString(10)
savings.medianMetaMaskFee = medianMetaMaskFee
.toString(10);
savings.performance = savings.performance.toString(10);
savings.fee = savings.fee.toString(10);
savings.medianMetaMaskFee = medianMetaMaskFee;
newQuotes[topAggId].isBestQuote = true
newQuotes[topAggId].savings = savings
newQuotes[topAggId].isBestQuote = true;
newQuotes[topAggId].savings = savings;
}
return [topAggId, newQuotes]
return [topAggId, newQuotes];
}
async _getERC20Allowance(contractAddress, walletAddress) {
@ -661,8 +666,8 @@ export default class SwapsController {
contractAddress,
abi,
this.ethersProvider,
)
return await contract.allowance(walletAddress, METASWAP_ADDRESS)
);
return await contract.allowance(walletAddress, METASWAP_ADDRESS);
}
/**
@ -674,8 +679,8 @@ export default class SwapsController {
* until the value can be fetched again.
*/
_setupSwapsLivenessFetching() {
const TEN_MINUTES_MS = 10 * 60 * 1000
let intervalId = null
const TEN_MINUTES_MS = 10 * 60 * 1000;
let intervalId = null;
const fetchAndSetupInterval = () => {
if (window.navigator.onLine && intervalId === null) {
@ -684,25 +689,25 @@ export default class SwapsController {
intervalId = setInterval(
this._fetchAndSetSwapsLiveness.bind(this),
TEN_MINUTES_MS,
)
this._fetchAndSetSwapsLiveness()
}
);
this._fetchAndSetSwapsLiveness();
}
};
window.addEventListener('online', fetchAndSetupInterval)
window.addEventListener('online', fetchAndSetupInterval);
window.addEventListener('offline', () => {
if (intervalId !== null) {
clearInterval(intervalId)
intervalId = null
clearInterval(intervalId);
intervalId = null;
const { swapsState } = this.store.getState()
const { swapsState } = this.store.getState();
if (swapsState.swapsFeatureIsLive) {
this.setSwapsLiveness(false)
this.setSwapsLiveness(false);
}
}
})
});
fetchAndSetupInterval()
fetchAndSetupInterval();
}
/**
@ -716,41 +721,41 @@ export default class SwapsController {
* state.
*/
async _fetchAndSetSwapsLiveness() {
const { swapsState } = this.store.getState()
const { swapsFeatureIsLive: oldSwapsFeatureIsLive } = swapsState
let swapsFeatureIsLive = false
let successfullyFetched = false
let numAttempts = 0
const { swapsState } = this.store.getState();
const { swapsFeatureIsLive: oldSwapsFeatureIsLive } = swapsState;
let swapsFeatureIsLive = false;
let successfullyFetched = false;
let numAttempts = 0;
const fetchAndIncrementNumAttempts = async () => {
try {
swapsFeatureIsLive = Boolean(await this._fetchSwapsFeatureLiveness())
successfullyFetched = true
swapsFeatureIsLive = Boolean(await this._fetchSwapsFeatureLiveness());
successfullyFetched = true;
} catch (err) {
log.error(err)
numAttempts += 1
}
log.error(err);
numAttempts += 1;
}
};
await fetchAndIncrementNumAttempts()
await fetchAndIncrementNumAttempts();
// The loop conditions are modified by fetchAndIncrementNumAttempts.
// eslint-disable-next-line no-unmodified-loop-condition
while (!successfullyFetched && numAttempts < 3) {
await new Promise((resolve) => {
setTimeout(resolve, 5000) // 5 seconds
})
await fetchAndIncrementNumAttempts()
setTimeout(resolve, 5000); // 5 seconds
});
await fetchAndIncrementNumAttempts();
}
if (!successfullyFetched) {
log.error(
'Failed to fetch swaps feature flag 3 times. Setting to false and trying again next interval.',
)
);
}
if (swapsFeatureIsLive !== oldSwapsFeatureIsLive) {
this.setSwapsLiveness(swapsFeatureIsLive)
this.setSwapsLiveness(swapsFeatureIsLive);
}
}
}
@ -763,50 +768,50 @@ export default class SwapsController {
*/
function getMedianEthValueQuote(_quotes) {
if (!Array.isArray(_quotes) || _quotes.length === 0) {
throw new Error('Expected non-empty array param.')
throw new Error('Expected non-empty array param.');
}
const quotes = [..._quotes]
const quotes = [..._quotes];
quotes.sort((quoteA, quoteB) => {
const overallValueOfQuoteA = new BigNumber(quoteA.overallValueOfQuote, 10)
const overallValueOfQuoteB = new BigNumber(quoteB.overallValueOfQuote, 10)
const overallValueOfQuoteA = new BigNumber(quoteA.overallValueOfQuote, 10);
const overallValueOfQuoteB = new BigNumber(quoteB.overallValueOfQuote, 10);
if (overallValueOfQuoteA.equals(overallValueOfQuoteB)) {
return 0
return 0;
}
return overallValueOfQuoteA.lessThan(overallValueOfQuoteB) ? -1 : 1
})
return overallValueOfQuoteA.lessThan(overallValueOfQuoteB) ? -1 : 1;
});
if (quotes.length % 2 === 1) {
// return middle values
const medianOverallValue =
quotes[(quotes.length - 1) / 2].overallValueOfQuote
quotes[(quotes.length - 1) / 2].overallValueOfQuote;
const quotesMatchingMedianQuoteValue = quotes.filter(
(quote) => medianOverallValue === quote.overallValueOfQuote,
)
return meansOfQuotesFeesAndValue(quotesMatchingMedianQuoteValue)
);
return meansOfQuotesFeesAndValue(quotesMatchingMedianQuoteValue);
}
// return mean of middle two values
const upperIndex = quotes.length / 2
const lowerIndex = upperIndex - 1
const upperIndex = quotes.length / 2;
const lowerIndex = upperIndex - 1;
const overallValueAtUpperIndex = quotes[upperIndex].overallValueOfQuote
const overallValueAtLowerIndex = quotes[lowerIndex].overallValueOfQuote
const overallValueAtUpperIndex = quotes[upperIndex].overallValueOfQuote;
const overallValueAtLowerIndex = quotes[lowerIndex].overallValueOfQuote;
const quotesMatchingUpperIndexValue = quotes.filter(
(quote) => overallValueAtUpperIndex === quote.overallValueOfQuote,
)
);
const quotesMatchingLowerIndexValue = quotes.filter(
(quote) => overallValueAtLowerIndex === quote.overallValueOfQuote,
)
);
const feesAndValueAtUpperIndex = meansOfQuotesFeesAndValue(
quotesMatchingUpperIndexValue,
)
);
const feesAndValueAtLowerIndex = meansOfQuotesFeesAndValue(
quotesMatchingLowerIndexValue,
)
);
return {
ethFee: new BigNumber(feesAndValueAtUpperIndex.ethFee, 10)
@ -827,7 +832,7 @@ function getMedianEthValueQuote(_quotes) {
.plus(feesAndValueAtLowerIndex.ethValueOfTokens, 10)
.dividedBy(2)
.toString(10),
}
};
}
/**
@ -857,7 +862,7 @@ function meansOfQuotesFeesAndValue(quotes) {
metaMaskFeeInEth: new BigNumber(0, 10),
ethValueOfTokens: new BigNumber(0, 10),
},
)
);
return {
ethFee: feeAndValueSumsAsBigNumbers.ethFee
@ -869,10 +874,10 @@ function meansOfQuotesFeesAndValue(quotes) {
ethValueOfTokens: feeAndValueSumsAsBigNumbers.ethValueOfTokens
.div(quotes.length, 10)
.toString(10),
}
};
}
export const utils = {
getMedianEthValueQuote,
meansOfQuotesFeesAndValue,
}
};

@ -1,21 +1,21 @@
import { ObservableStore } from '@metamask/obs-store'
import { ObservableStore } from '@metamask/obs-store';
/* eslint-disable import/first,import/order */
const Box = process.env.IN_TEST
? require('../../../development/mock-3box')
: require('3box')
: require('3box');
/* eslint-enable import/order */
import log from 'loglevel'
import { JsonRpcEngine } from 'json-rpc-engine'
import providerFromEngine from 'eth-json-rpc-middleware/providerFromEngine'
import Migrator from '../lib/migrator'
import migrations from '../migrations'
import createOriginMiddleware from '../lib/createOriginMiddleware'
import createMetamaskMiddleware from './network/createMetamaskMiddleware'
import log from 'loglevel';
import { JsonRpcEngine } from 'json-rpc-engine';
import providerFromEngine from 'eth-json-rpc-middleware/providerFromEngine';
import Migrator from '../lib/migrator';
import migrations from '../migrations';
import createOriginMiddleware from '../lib/createOriginMiddleware';
import createMetamaskMiddleware from './network/createMetamaskMiddleware';
/* eslint-enable import/first */
const SYNC_TIMEOUT = 60 * 1000 // one minute
const SYNC_TIMEOUT = 60 * 1000; // one minute
export default class ThreeBoxController {
constructor(opts = {}) {
@ -25,40 +25,40 @@ export default class ThreeBoxController {
addressBookController,
version,
getKeyringControllerState,
} = opts
} = opts;
this.preferencesController = preferencesController
this.addressBookController = addressBookController
this.keyringController = keyringController
this.preferencesController = preferencesController;
this.addressBookController = addressBookController;
this.keyringController = keyringController;
this.provider = this._createProvider({
version,
getAccounts: async ({ origin }) => {
if (origin !== '3Box') {
return []
return [];
}
const { isUnlocked } = getKeyringControllerState()
const { isUnlocked } = getKeyringControllerState();
const accounts = await this.keyringController.getAccounts()
const accounts = await this.keyringController.getAccounts();
if (isUnlocked && accounts[0]) {
const appKeyAddress = await this.keyringController.getAppKeyAddress(
accounts[0],
'wallet://3box.metamask.io',
)
return [appKeyAddress]
);
return [appKeyAddress];
}
return []
return [];
},
processPersonalMessage: async (msgParams) => {
const accounts = await this.keyringController.getAccounts()
const accounts = await this.keyringController.getAccounts();
return keyringController.signPersonalMessage(
{ ...msgParams, from: accounts[0] },
{
withAppKeyOrigin: 'wallet://3box.metamask.io',
},
)
);
},
})
});
const initState = {
threeBoxSyncingAllowed: false,
@ -68,193 +68,196 @@ export default class ThreeBoxController {
threeBoxAddress: null,
threeBoxSynced: false,
threeBoxDisabled: false,
}
this.store = new ObservableStore(initState)
this.registeringUpdates = false
};
this.store = new ObservableStore(initState);
this.registeringUpdates = false;
this.lastMigration = migrations
.sort((a, b) => a.version - b.version)
.slice(-1)[0]
.slice(-1)[0];
if (initState.threeBoxSyncingAllowed) {
this.init()
this.init();
}
}
async init() {
const accounts = await this.keyringController.getAccounts()
this.address = accounts[0]
const accounts = await this.keyringController.getAccounts();
this.address = accounts[0];
if (this.address && !(this.box && this.store.getState().threeBoxSynced)) {
await this.new3Box()
await this.new3Box();
}
}
async _update3Box() {
try {
const { threeBoxSyncingAllowed, threeBoxSynced } = this.store.getState()
const { threeBoxSyncingAllowed, threeBoxSynced } = this.store.getState();
if (threeBoxSyncingAllowed && threeBoxSynced) {
const newState = {
preferences: this.preferencesController.store.getState(),
addressBook: this.addressBookController.state,
lastUpdated: Date.now(),
lastMigration: this.lastMigration,
}
};
await this.space.private.set('metamaskBackup', JSON.stringify(newState))
await this.setShowRestorePromptToFalse()
await this.space.private.set(
'metamaskBackup',
JSON.stringify(newState),
);
await this.setShowRestorePromptToFalse();
}
} catch (error) {
console.error(error)
console.error(error);
}
}
_createProvider(providerOpts) {
const metamaskMiddleware = createMetamaskMiddleware(providerOpts)
const engine = new JsonRpcEngine()
engine.push(createOriginMiddleware({ origin: '3Box' }))
engine.push(metamaskMiddleware)
const provider = providerFromEngine(engine)
return provider
const metamaskMiddleware = createMetamaskMiddleware(providerOpts);
const engine = new JsonRpcEngine();
engine.push(createOriginMiddleware({ origin: '3Box' }));
engine.push(metamaskMiddleware);
const provider = providerFromEngine(engine);
return provider;
}
_waitForOnSyncDone() {
return new Promise((resolve) => {
this.box.onSyncDone(() => {
log.debug('3Box box sync done')
return resolve()
})
})
log.debug('3Box box sync done');
return resolve();
});
});
}
async new3Box() {
const accounts = await this.keyringController.getAccounts()
const accounts = await this.keyringController.getAccounts();
this.address = await this.keyringController.getAppKeyAddress(
accounts[0],
'wallet://3box.metamask.io',
)
let backupExists
);
let backupExists;
try {
const threeBoxConfig = await Box.getConfig(this.address)
backupExists = threeBoxConfig.spaces && threeBoxConfig.spaces.metamask
const threeBoxConfig = await Box.getConfig(this.address);
backupExists = threeBoxConfig.spaces && threeBoxConfig.spaces.metamask;
} catch (e) {
if (e.message.match(/^Error: Invalid response \(404\)/u)) {
backupExists = false
backupExists = false;
} else {
throw e
throw e;
}
}
if (this.getThreeBoxSyncingState() || backupExists) {
this.store.updateState({ threeBoxSynced: false })
this.store.updateState({ threeBoxSynced: false });
let timedOut = false
let timedOut = false;
const syncTimeout = setTimeout(() => {
log.error(`3Box sync timed out after ${SYNC_TIMEOUT} ms`)
timedOut = true
log.error(`3Box sync timed out after ${SYNC_TIMEOUT} ms`);
timedOut = true;
this.store.updateState({
threeBoxDisabled: true,
threeBoxSyncingAllowed: false,
})
}, SYNC_TIMEOUT)
});
}, SYNC_TIMEOUT);
try {
this.box = await Box.openBox(this.address, this.provider)
await this._waitForOnSyncDone()
this.box = await Box.openBox(this.address, this.provider);
await this._waitForOnSyncDone();
this.space = await this.box.openSpace('metamask', {
onSyncDone: async () => {
const stateUpdate = {
threeBoxSynced: true,
threeBoxAddress: this.address,
}
};
if (timedOut) {
log.info(`3Box sync completed after timeout; no longer disabled`)
stateUpdate.threeBoxDisabled = false
log.info(`3Box sync completed after timeout; no longer disabled`);
stateUpdate.threeBoxDisabled = false;
}
clearTimeout(syncTimeout)
this.store.updateState(stateUpdate)
clearTimeout(syncTimeout);
this.store.updateState(stateUpdate);
log.debug('3Box space sync done')
log.debug('3Box space sync done');
},
})
});
} catch (e) {
console.error(e)
throw e
console.error(e);
throw e;
}
}
}
async getLastUpdated() {
const res = await this.space.private.get('metamaskBackup')
const parsedRes = JSON.parse(res || '{}')
return parsedRes.lastUpdated
const res = await this.space.private.get('metamaskBackup');
const parsedRes = JSON.parse(res || '{}');
return parsedRes.lastUpdated;
}
async migrateBackedUpState(backedUpState) {
const migrator = new Migrator({ migrations })
const { preferences, addressBook } = JSON.parse(backedUpState)
const migrator = new Migrator({ migrations });
const { preferences, addressBook } = JSON.parse(backedUpState);
const formattedStateBackup = {
PreferencesController: preferences,
AddressBookController: addressBook,
}
};
const initialMigrationState = migrator.generateInitialState(
formattedStateBackup,
)
const migratedState = await migrator.migrateData(initialMigrationState)
);
const migratedState = await migrator.migrateData(initialMigrationState);
return {
preferences: migratedState.data.PreferencesController,
addressBook: migratedState.data.AddressBookController,
}
};
}
async restoreFromThreeBox() {
const backedUpState = await this.space.private.get('metamaskBackup')
const backedUpState = await this.space.private.get('metamaskBackup');
const { preferences, addressBook } = await this.migrateBackedUpState(
backedUpState,
)
this.store.updateState({ threeBoxLastUpdated: backedUpState.lastUpdated })
preferences && this.preferencesController.store.updateState(preferences)
addressBook && this.addressBookController.update(addressBook, true)
this.setShowRestorePromptToFalse()
);
this.store.updateState({ threeBoxLastUpdated: backedUpState.lastUpdated });
preferences && this.preferencesController.store.updateState(preferences);
addressBook && this.addressBookController.update(addressBook, true);
this.setShowRestorePromptToFalse();
}
turnThreeBoxSyncingOn() {
this._registerUpdates()
this._registerUpdates();
}
turnThreeBoxSyncingOff() {
this.box.logout()
this.box.logout();
}
setShowRestorePromptToFalse() {
this.store.updateState({ showRestorePrompt: false })
this.store.updateState({ showRestorePrompt: false });
}
setThreeBoxSyncingPermission(newThreeboxSyncingState) {
if (this.store.getState().threeBoxDisabled) {
return
return;
}
this.store.updateState({
threeBoxSyncingAllowed: newThreeboxSyncingState,
})
});
if (newThreeboxSyncingState && this.box) {
this.turnThreeBoxSyncingOn()
this.turnThreeBoxSyncingOn();
}
if (!newThreeboxSyncingState && this.box) {
this.turnThreeBoxSyncingOff()
this.turnThreeBoxSyncingOff();
}
}
getThreeBoxSyncingState() {
return this.store.getState().threeBoxSyncingAllowed
return this.store.getState().threeBoxSyncingAllowed;
}
_registerUpdates() {
if (!this.registeringUpdates) {
const updatePreferences = this._update3Box.bind(this)
this.preferencesController.store.subscribe(updatePreferences)
const updateAddressBook = this._update3Box.bind(this)
this.addressBookController.subscribe(updateAddressBook)
this.registeringUpdates = true
const updatePreferences = this._update3Box.bind(this);
this.preferencesController.store.subscribe(updatePreferences);
const updateAddressBook = this._update3Box.bind(this);
this.addressBookController.subscribe(updateAddressBook);
this.registeringUpdates = true;
}
}
}

@ -1,13 +1,13 @@
import { ObservableStore } from '@metamask/obs-store'
import log from 'loglevel'
import { normalize as normalizeAddress } from 'eth-sig-util'
import ethUtil from 'ethereumjs-util'
import getFetchWithTimeout from '../../../shared/modules/fetch-with-timeout'
import { ObservableStore } from '@metamask/obs-store';
import log from 'loglevel';
import { normalize as normalizeAddress } from 'eth-sig-util';
import ethUtil from 'ethereumjs-util';
import getFetchWithTimeout from '../../../shared/modules/fetch-with-timeout';
const fetchWithTimeout = getFetchWithTimeout(30000)
const fetchWithTimeout = getFetchWithTimeout(30000);
// By default, poll every 3 minutes
const DEFAULT_INTERVAL = 180 * 1000
const DEFAULT_INTERVAL = 180 * 1000;
/**
* A controller that polls for token exchange
@ -20,43 +20,43 @@ export default class TokenRatesController {
* @param {Object} [config] - Options to configure controller
*/
constructor({ currency, preferences } = {}) {
this.store = new ObservableStore()
this.currency = currency
this.preferences = preferences
this.store = new ObservableStore();
this.currency = currency;
this.preferences = preferences;
}
/**
* Updates exchange rates for all tokens
*/
async updateExchangeRates() {
const contractExchangeRates = {}
const contractExchangeRates = {};
const nativeCurrency = this.currency
? this.currency.state.nativeCurrency.toLowerCase()
: 'eth'
const pairs = this._tokens.map((token) => token.address).join(',')
const query = `contract_addresses=${pairs}&vs_currencies=${nativeCurrency}`
: 'eth';
const pairs = this._tokens.map((token) => token.address).join(',');
const query = `contract_addresses=${pairs}&vs_currencies=${nativeCurrency}`;
if (this._tokens.length > 0) {
try {
const response = await fetchWithTimeout(
`https://api.coingecko.com/api/v3/simple/token_price/ethereum?${query}`,
)
const prices = await response.json()
);
const prices = await response.json();
this._tokens.forEach((token) => {
const price =
prices[token.address.toLowerCase()] ||
prices[ethUtil.toChecksumAddress(token.address)]
prices[ethUtil.toChecksumAddress(token.address)];
contractExchangeRates[normalizeAddress(token.address)] = price
? price[nativeCurrency]
: 0
})
: 0;
});
} catch (error) {
log.warn(
`MetaMask - TokenRatesController exchange rate fetch failed.`,
error,
)
);
}
}
this.store.putState({ contractExchangeRates })
this.store.putState({ contractExchangeRates });
}
/* eslint-disable accessor-pairs */
@ -64,38 +64,38 @@ export default class TokenRatesController {
* @type {Object}
*/
set preferences(preferences) {
this._preferences && this._preferences.unsubscribe()
this._preferences && this._preferences.unsubscribe();
if (!preferences) {
return
return;
}
this._preferences = preferences
this.tokens = preferences.getState().tokens
this._preferences = preferences;
this.tokens = preferences.getState().tokens;
preferences.subscribe(({ tokens = [] }) => {
this.tokens = tokens
})
this.tokens = tokens;
});
}
/**
* @type {Array}
*/
set tokens(tokens) {
this._tokens = tokens
this.updateExchangeRates()
this._tokens = tokens;
this.updateExchangeRates();
}
/* eslint-enable accessor-pairs */
start(interval = DEFAULT_INTERVAL) {
this._handle && clearInterval(this._handle)
this._handle && clearInterval(this._handle);
if (!interval) {
return
return;
}
this._handle = setInterval(() => {
this.updateExchangeRates()
}, interval)
this.updateExchangeRates()
this.updateExchangeRates();
}, interval);
this.updateExchangeRates();
}
stop() {
this._handle && clearInterval(this._handle)
this._handle && clearInterval(this._handle);
}
}

File diff suppressed because it is too large Load Diff

@ -1,5 +1,5 @@
import jsonDiffer from 'fast-json-patch'
import { cloneDeep } from 'lodash'
import jsonDiffer from 'fast-json-patch';
import { cloneDeep } from 'lodash';
/**
converts non-initial history entries into diffs
@ -12,11 +12,11 @@ export function migrateFromSnapshotsToDiffs(longHistory) {
// convert non-initial history entries into diffs
.map((entry, index) => {
if (index === 0) {
return entry
return entry;
}
return generateHistoryEntry(longHistory[index - 1], entry)
return generateHistoryEntry(longHistory[index - 1], entry);
})
)
);
}
/**
@ -32,16 +32,16 @@ export function migrateFromSnapshotsToDiffs(longHistory) {
@returns {Array}
*/
export function generateHistoryEntry(previousState, newState, note) {
const entry = jsonDiffer.compare(previousState, newState)
const entry = jsonDiffer.compare(previousState, newState);
// Add a note to the first op, since it breaks if we append it to the entry
if (entry[0]) {
if (note) {
entry[0].note = note
entry[0].note = note;
}
entry[0].timestamp = Date.now()
entry[0].timestamp = Date.now();
}
return entry
return entry;
}
/**
@ -49,10 +49,10 @@ export function generateHistoryEntry(previousState, newState, note) {
@returns {Object}
*/
export function replayHistory(_shortHistory) {
const shortHistory = cloneDeep(_shortHistory)
const shortHistory = cloneDeep(_shortHistory);
return shortHistory.reduce(
(val, entry) => jsonDiffer.applyPatch(val, entry).newDocument,
)
);
}
/**
@ -61,7 +61,7 @@ export function replayHistory(_shortHistory) {
* @returns {Object} a deep clone without history
*/
export function snapshotFromTxMeta(txMeta) {
const shallow = { ...txMeta }
delete shallow.history
return cloneDeep(shallow)
const shallow = { ...txMeta };
delete shallow.history;
return cloneDeep(shallow);
}

@ -1,7 +1,7 @@
import { isValidAddress } from 'ethereumjs-util'
import { ethErrors } from 'eth-rpc-errors'
import { addHexPrefix } from '../../../lib/util'
import { TRANSACTION_STATUSES } from '../../../../../shared/constants/transaction'
import { isValidAddress } from 'ethereumjs-util';
import { ethErrors } from 'eth-rpc-errors';
import { addHexPrefix } from '../../../lib/util';
import { TRANSACTION_STATUSES } from '../../../../../shared/constants/transaction';
const normalizers = {
from: (from) => addHexPrefix(from),
@ -12,7 +12,7 @@ const normalizers = {
data: (data) => addHexPrefix(data),
gas: (gas) => addHexPrefix(gas),
gasPrice: (gasPrice) => addHexPrefix(gasPrice),
}
};
/**
* Normalizes the given txParams
@ -23,13 +23,13 @@ const normalizers = {
*/
export function normalizeTxParams(txParams, lowerCase = true) {
// apply only keys in the normalizers
const normalizedTxParams = {}
const normalizedTxParams = {};
for (const key in normalizers) {
if (txParams[key]) {
normalizedTxParams[key] = normalizers[key](txParams[key], lowerCase)
normalizedTxParams[key] = normalizers[key](txParams[key], lowerCase);
}
}
return normalizedTxParams
return normalizedTxParams;
}
/**
@ -41,28 +41,28 @@ export function validateTxParams(txParams) {
if (!txParams || typeof txParams !== 'object' || Array.isArray(txParams)) {
throw ethErrors.rpc.invalidParams(
'Invalid transaction params: must be an object.',
)
);
}
if (!txParams.to && !txParams.data) {
throw ethErrors.rpc.invalidParams(
'Invalid transaction params: must specify "data" for contract deployments, or "to" (and optionally "data") for all other types of transactions.',
)
);
}
validateFrom(txParams)
validateRecipient(txParams)
validateFrom(txParams);
validateRecipient(txParams);
if ('value' in txParams) {
const value = txParams.value.toString()
const value = txParams.value.toString();
if (value.includes('-')) {
throw ethErrors.rpc.invalidParams(
`Invalid transaction value "${txParams.value}": not a positive number.`,
)
);
}
if (value.includes('.')) {
throw ethErrors.rpc.invalidParams(
`Invalid transaction value of "${txParams.value}": number must be in wei.`,
)
);
}
}
}
@ -76,10 +76,10 @@ export function validateFrom(txParams) {
if (!(typeof txParams.from === 'string')) {
throw ethErrors.rpc.invalidParams(
`Invalid "from" address "${txParams.from}": not a string.`,
)
);
}
if (!isValidAddress(txParams.from)) {
throw ethErrors.rpc.invalidParams('Invalid "from" address.')
throw ethErrors.rpc.invalidParams('Invalid "from" address.');
}
}
@ -92,14 +92,14 @@ export function validateFrom(txParams) {
export function validateRecipient(txParams) {
if (txParams.to === '0x' || txParams.to === null) {
if (txParams.data) {
delete txParams.to
delete txParams.to;
} else {
throw ethErrors.rpc.invalidParams('Invalid "to" address.')
throw ethErrors.rpc.invalidParams('Invalid "to" address.');
}
} else if (txParams.to !== undefined && !isValidAddress(txParams.to)) {
throw ethErrors.rpc.invalidParams('Invalid "to" address.')
throw ethErrors.rpc.invalidParams('Invalid "to" address.');
}
return txParams
return txParams;
}
/**
@ -112,5 +112,5 @@ export function getFinalStates() {
TRANSACTION_STATUSES.CONFIRMED, // the tx has been included in a block.
TRANSACTION_STATUSES.FAILED, // the tx failed for some reason, included on tx data.
TRANSACTION_STATUSES.DROPPED, // the tx nonce was already used
]
];
}

@ -1,7 +1,7 @@
import EventEmitter from 'safe-event-emitter'
import log from 'loglevel'
import EthQuery from 'ethjs-query'
import { TRANSACTION_STATUSES } from '../../../../shared/constants/transaction'
import EventEmitter from 'safe-event-emitter';
import log from 'loglevel';
import EthQuery from 'ethjs-query';
import { TRANSACTION_STATUSES } from '../../../../shared/constants/transaction';
/**
@ -27,7 +27,7 @@ export default class PendingTransactionTracker extends EventEmitter {
*
* @type {number}
*/
DROPPED_BUFFER_COUNT = 3
DROPPED_BUFFER_COUNT = 3;
/**
* A map of transaction hashes to the number of blocks we've seen
@ -35,17 +35,17 @@ export default class PendingTransactionTracker extends EventEmitter {
*
* @type {Map<String, number>}
*/
droppedBlocksBufferByHash = new Map()
droppedBlocksBufferByHash = new Map();
constructor(config) {
super()
this.query = config.query || new EthQuery(config.provider)
this.nonceTracker = config.nonceTracker
this.getPendingTransactions = config.getPendingTransactions
this.getCompletedTransactions = config.getCompletedTransactions
this.publishTransaction = config.publishTransaction
this.approveTransaction = config.approveTransaction
this.confirmTransaction = config.confirmTransaction
super();
this.query = config.query || new EthQuery(config.provider);
this.nonceTracker = config.nonceTracker;
this.getPendingTransactions = config.getPendingTransactions;
this.getCompletedTransactions = config.getCompletedTransactions;
this.publishTransaction = config.publishTransaction;
this.approveTransaction = config.approveTransaction;
this.confirmTransaction = config.confirmTransaction;
}
/**
@ -53,19 +53,19 @@ export default class PendingTransactionTracker extends EventEmitter {
*/
async updatePendingTxs() {
// in order to keep the nonceTracker accurate we block it while updating pending transactions
const nonceGlobalLock = await this.nonceTracker.getGlobalLock()
const nonceGlobalLock = await this.nonceTracker.getGlobalLock();
try {
const pendingTxs = this.getPendingTransactions()
const pendingTxs = this.getPendingTransactions();
await Promise.all(
pendingTxs.map((txMeta) => this._checkPendingTx(txMeta)),
)
);
} catch (err) {
log.error(
'PendingTransactionTracker - Error updating pending transactions',
)
log.error(err)
);
log.error(err);
}
nonceGlobalLock.releaseLock()
nonceGlobalLock.releaseLock();
}
/**
@ -75,16 +75,16 @@ export default class PendingTransactionTracker extends EventEmitter {
* @returns {Promise<void>}
*/
async resubmitPendingTxs(blockNumber) {
const pending = this.getPendingTransactions()
const pending = this.getPendingTransactions();
if (!pending.length) {
return
return;
}
for (const txMeta of pending) {
try {
await this._resubmitTx(txMeta, blockNumber)
await this._resubmitTx(txMeta, blockNumber);
} catch (err) {
const errorMessage =
err.value?.message?.toLowerCase() || err.message.toLowerCase()
err.value?.message?.toLowerCase() || err.message.toLowerCase();
const isKnownTx =
// geth
errorMessage.includes('replacement transaction underpriced') ||
@ -96,17 +96,17 @@ export default class PendingTransactionTracker extends EventEmitter {
) ||
// other
errorMessage.includes('gateway timeout') ||
errorMessage.includes('nonce too low')
errorMessage.includes('nonce too low');
// ignore resubmit warnings, return early
if (isKnownTx) {
return
return;
}
// encountered real error - transition to error state
txMeta.warning = {
error: errorMessage,
message: 'There was an error when resubmitting this transaction.',
}
this.emit('tx:warning', txMeta, err)
};
this.emit('tx:warning', txMeta, err);
}
}
}
@ -125,33 +125,33 @@ export default class PendingTransactionTracker extends EventEmitter {
*/
async _resubmitTx(txMeta, latestBlockNumber) {
if (!txMeta.firstRetryBlockNumber) {
this.emit('tx:block-update', txMeta, latestBlockNumber)
this.emit('tx:block-update', txMeta, latestBlockNumber);
}
const firstRetryBlockNumber =
txMeta.firstRetryBlockNumber || latestBlockNumber
txMeta.firstRetryBlockNumber || latestBlockNumber;
const txBlockDistance =
Number.parseInt(latestBlockNumber, 16) -
Number.parseInt(firstRetryBlockNumber, 16)
Number.parseInt(firstRetryBlockNumber, 16);
const retryCount = txMeta.retryCount || 0
const retryCount = txMeta.retryCount || 0;
// Exponential backoff to limit retries at publishing
if (txBlockDistance <= Math.pow(2, retryCount) - 1) {
return undefined
return undefined;
}
// Only auto-submit already-signed txs:
if (!('rawTx' in txMeta)) {
return this.approveTransaction(txMeta.id)
return this.approveTransaction(txMeta.id);
}
const { rawTx } = txMeta
const txHash = await this.publishTransaction(rawTx)
const { rawTx } = txMeta;
const txHash = await this.publishTransaction(rawTx);
// Increment successful tries:
this.emit('tx:retry', txMeta)
return txHash
this.emit('tx:retry', txMeta);
return txHash;
}
/**
@ -165,12 +165,12 @@ export default class PendingTransactionTracker extends EventEmitter {
* @private
*/
async _checkPendingTx(txMeta) {
const txHash = txMeta.hash
const txId = txMeta.id
const txHash = txMeta.hash;
const txId = txMeta.id;
// Only check submitted txs
if (txMeta.status !== TRANSACTION_STATUSES.SUBMITTED) {
return
return;
}
// extra check in case there was an uncaught error during the
@ -178,35 +178,35 @@ export default class PendingTransactionTracker extends EventEmitter {
if (!txHash) {
const noTxHashErr = new Error(
'We had an error while submitting this transaction, please try again.',
)
noTxHashErr.name = 'NoTxHashError'
this.emit('tx:failed', txId, noTxHashErr)
);
noTxHashErr.name = 'NoTxHashError';
this.emit('tx:failed', txId, noTxHashErr);
return
return;
}
if (await this._checkIfNonceIsTaken(txMeta)) {
this.emit('tx:dropped', txId)
return
this.emit('tx:dropped', txId);
return;
}
try {
const transactionReceipt = await this.query.getTransactionReceipt(txHash)
const transactionReceipt = await this.query.getTransactionReceipt(txHash);
if (transactionReceipt?.blockNumber) {
this.emit('tx:confirmed', txId, transactionReceipt)
return
this.emit('tx:confirmed', txId, transactionReceipt);
return;
}
} catch (err) {
txMeta.warning = {
error: err.message,
message: 'There was a problem loading this transaction.',
}
this.emit('tx:warning', txMeta, err)
return
};
this.emit('tx:warning', txMeta, err);
return;
}
if (await this._checkIfTxWasDropped(txMeta)) {
this.emit('tx:dropped', txId)
this.emit('tx:dropped', txId);
}
}
@ -221,26 +221,26 @@ export default class PendingTransactionTracker extends EventEmitter {
const {
hash: txHash,
txParams: { nonce, from },
} = txMeta
const networkNextNonce = await this.query.getTransactionCount(from)
} = txMeta;
const networkNextNonce = await this.query.getTransactionCount(from);
if (parseInt(nonce, 16) >= networkNextNonce.toNumber()) {
return false
return false;
}
if (!this.droppedBlocksBufferByHash.has(txHash)) {
this.droppedBlocksBufferByHash.set(txHash, 0)
this.droppedBlocksBufferByHash.set(txHash, 0);
}
const currentBlockBuffer = this.droppedBlocksBufferByHash.get(txHash)
const currentBlockBuffer = this.droppedBlocksBufferByHash.get(txHash);
if (currentBlockBuffer < this.DROPPED_BUFFER_COUNT) {
this.droppedBlocksBufferByHash.set(txHash, currentBlockBuffer + 1)
return false
this.droppedBlocksBufferByHash.set(txHash, currentBlockBuffer + 1);
return false;
}
this.droppedBlocksBufferByHash.delete(txHash)
return true
this.droppedBlocksBufferByHash.delete(txHash);
return true;
}
/**
@ -250,8 +250,8 @@ export default class PendingTransactionTracker extends EventEmitter {
* @private
*/
async _checkIfNonceIsTaken(txMeta) {
const address = txMeta.txParams.from
const completed = this.getCompletedTransactions(address)
const address = txMeta.txParams.from;
const completed = this.getCompletedTransactions(address);
return completed.some(
// This is called while the transaction is in-flight, so it is possible that the
// list of completed transactions now includes the transaction we were looking at
@ -259,6 +259,6 @@ export default class PendingTransactionTracker extends EventEmitter {
(other) =>
!(other.id === txMeta.id) &&
other.txParams.nonce === txMeta.txParams.nonce,
)
);
}
}

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

Loading…
Cancel
Save