1
0
mirror of https://github.com/kremalicious/metamask-extension.git synced 2024-12-23 09:52:26 +01:00

Merge pull request #9843 from MetaMask/Version-v8.1.4

Version v8.1.4 RC
This commit is contained in:
Mark Stacey 2020-11-16 16:53:52 -03:30 committed by GitHub
commit 2e73285f23
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
1020 changed files with 32596 additions and 22307 deletions

View File

@ -309,9 +309,11 @@ jobs:
path: test-artifacts
destination: test-artifacts
# important: generate sesify viz AFTER uploading builds as artifacts
- run:
name: build:sesify-viz
command: ./.circleci/scripts/create-sesify-viz
# 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

View File

@ -2,19 +2,19 @@ module.exports = {
root: true,
parser: '@babel/eslint-parser',
parserOptions: {
'sourceType': 'module',
'ecmaVersion': 2017,
'ecmaFeatures': {
'experimentalObjectRestSpread': true,
'impliedStrict': true,
'modules': true,
'blockBindings': true,
'arrowFunctions': true,
'objectLiteralShorthandMethods': true,
'objectLiteralShorthandProperties': true,
'templateStrings': true,
'classes': true,
'jsx': true,
sourceType: 'module',
ecmaVersion: 2017,
ecmaFeatures: {
experimentalObjectRestSpread: true,
impliedStrict: true,
modules: true,
blockBindings: true,
arrowFunctions: true,
objectLiteralShorthandMethods: true,
objectLiteralShorthandProperties: true,
templateStrings: true,
classes: true,
jsx: true,
},
},
@ -28,6 +28,9 @@ module.exports = {
'coverage/',
'app/scripts/chromereload.js',
'app/vendor/**',
'test/e2e/send-eth-with-private-key-test/**',
'nyc_output/**',
'.vscode/**',
],
extends: [
@ -38,11 +41,7 @@ module.exports = {
'plugin:react-hooks/recommended',
],
plugins: [
'@babel',
'react',
'import',
],
plugins: ['@babel', 'react', 'import', 'prettier'],
globals: {
document: 'readonly',
@ -50,6 +49,67 @@ module.exports = {
},
rules: {
// Prettier changes and reasoning
'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
// https://github.com/prettier/eslint-config-prettier#lines-around-comment
'lines-around-comment': [
'error',
{
beforeBlockComment: true,
afterLineComment: false,
allowBlockStart: true,
allowBlockEnd: true,
allowObjectStart: true,
allowObjectEnd: true,
allowArrayStart: true,
allowArrayEnd: true,
},
],
// Prettier has some opinions on mixed-operators, and there is ongoing work
// to make the output code clear. It is better today then it was when the first
// PR to add prettier. That being said, the workaround for keeping this rule enabled
// requires breaking parts of operations into different variables -- which I believe
// to be worse. https://github.com/prettier/eslint-config-prettier#no-mixed-operators
'no-mixed-operators': 'off',
// Prettier wraps single line functions with ternaries, etc in parens by default, but
// if the line is long enough it breaks it into a separate line and removes the parens.
// The second behavior conflicts with this rule. There is some guides on the repo about
// how you can keep it enabled:
// https://github.com/prettier/eslint-config-prettier#no-confusing-arrow
// However, in practice this conflicts with prettier adding parens around short lines,
// when autofixing in vscode and others.
'no-confusing-arrow': 'off',
// There is no configuration in prettier for how it stylizes regexes, which conflicts
// with wrap-regex.
'wrap-regex': 'off',
// Prettier handles all indentation automagically. it can be configured here
// https://prettier.io/docs/en/options.html#tab-width but the default matches our
// style.
indent: 'off',
// This rule conflicts with the way that prettier breaks code across multiple lines when
// it exceeds the maximum length. Prettier optimizes for readability while simultaneously
// maximizing the amount of code per line.
'function-paren-newline': 'off',
// This rule throws an error when there is a line break in an arrow function declaration
// but prettier breaks arrow function declarations to be as readable as possible while
// still conforming to the width rules.
'implicit-arrow-linebreak': 'off',
// This rule would result in an increase in white space in lines with generator functions,
// which impacts prettier's goal of maximizing code per line and readability. There is no
// current workaround.
'generator-star-spacing': 'off',
'default-param-last': 'off',
'require-atomic-updates': 'off',
'import/no-unassigned-import': 'off',
@ -57,29 +117,32 @@ module.exports = {
'react/no-unused-prop-types': 'error',
'react/no-unused-state': 'error',
'react/jsx-boolean-value': 'error',
'react/jsx-curly-brace-presence': ['error', { 'props': 'never', 'children': 'never' }],
'react/jsx-curly-brace-presence': [
'error',
{ props: 'never', children: 'never' },
],
'react/jsx-equals-spacing': 'error',
'react/no-deprecated': 'error',
'react/default-props-match-prop-types': 'error',
'react/jsx-closing-tag-location': 'error',
'react/jsx-closing-tag-location': [
'error',
{ selfClosing: 'tag-aligned', nonEmpty: 'tag-aligned' },
],
'react/jsx-no-duplicate-props': 'error',
'react/jsx-closing-bracket-location': 'error',
'react/jsx-first-prop-new-line': ['error', 'multiline'],
'react/jsx-max-props-per-line': ['error', { 'maximum': 1, 'when': 'multiline' }],
'react/jsx-tag-spacing': ['error', {
'closingSlash': 'never',
'beforeSelfClosing': 'always',
'afterOpening': 'never',
}],
'react/jsx-wrap-multilines': ['error', {
'declaration': 'parens-new-line',
'assignment': 'parens-new-line',
'return': 'parens-new-line',
'arrow': 'parens-new-line',
'condition': 'parens-new-line',
'logical': 'parens-new-line',
'prop': 'parens-new-line',
}],
'react/jsx-max-props-per-line': [
'error',
{ maximum: 1, when: 'multiline' },
],
'react/jsx-tag-spacing': [
'error',
{
closingSlash: 'never',
beforeSelfClosing: 'always',
afterOpening: 'never',
},
],
'no-invalid-this': 'off',
'@babel/no-invalid-this': 'error',
@ -93,67 +156,59 @@ module.exports = {
'node/no-unpublished-import': 'off',
'node/no-unpublished-require': 'off',
},
overrides: [{
files: [
'test/e2e/**/*.js',
],
rules: {
'mocha/no-hooks-for-single-case': 'off',
overrides: [
{
files: ['test/e2e/**/*.js'],
rules: {
'mocha/no-hooks-for-single-case': 'off',
},
},
}, {
files: [
'app/scripts/migrations/*.js',
'*.stories.js',
],
rules: {
'import/no-anonymous-default-export': ['error', { 'allowObject': true }],
{
files: ['app/scripts/migrations/*.js', '*.stories.js'],
rules: {
'import/no-anonymous-default-export': ['error', { allowObject: true }],
},
},
}, {
files: [
'app/scripts/migrations/*.js',
],
rules: {
'node/global-require': 'off',
{
files: ['app/scripts/migrations/*.js'],
rules: {
'node/global-require': 'off',
},
},
}, {
files: [
'test/**/*-test.js',
'test/**/*.spec.js',
],
rules: {
// Mocha will re-assign `this` in a test context
'@babel/no-invalid-this': 'off',
{
files: ['test/**/*-test.js', 'test/**/*.spec.js'],
rules: {
// Mocha will re-assign `this` in a test context
'@babel/no-invalid-this': 'off',
},
},
}, {
files: [
'development/**/*.js',
'test/e2e/benchmark.js',
'test/helper.js',
],
rules: {
'node/no-process-exit': 'off',
'node/shebang': 'off',
{
files: ['development/**/*.js', 'test/e2e/benchmark.js', 'test/helper.js'],
rules: {
'node/no-process-exit': 'off',
'node/shebang': 'off',
},
},
}, {
files: [
'.eslintrc.js',
'babel.config.js',
'nyc.config.js',
'stylelint.config.js',
'development/**/*.js',
'test/e2e/**/*.js',
'test/env.js',
'test/setup.js',
],
parserOptions: {
sourceType: 'script',
{
files: [
'.eslintrc.js',
'babel.config.js',
'nyc.config.js',
'stylelint.config.js',
'development/**/*.js',
'test/e2e/**/*.js',
'test/env.js',
'test/setup.js',
],
parserOptions: {
sourceType: 'script',
},
},
}],
],
settings: {
'react': {
'version': 'detect',
react: {
version: 'detect',
},
},
}

View File

@ -6,3 +6,4 @@ coverage/
app/vendor/**
.nyc_output/**
.vscode/**
test/e2e/send-eth-with-private-key-test/**

3
.prettierrc.yml Normal file
View File

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

View File

@ -3,6 +3,40 @@
## Current Develop Branch
- [#9612](https://github.com/MetaMask/metamask-extension/pull/9612): Update main-quote-summary designs/styles
## 8.1.4 Tue Nov 10 2020
- [#9687](https://github.com/MetaMask/metamask-extension/pull/9687): Allow speeding up of underpriced transactions
- [#9694](https://github.com/MetaMask/metamask-extension/pull/9694): normalize UI component font styles
- [#9695](https://github.com/MetaMask/metamask-extension/pull/9695): normalize app component font styles
- [#9696](https://github.com/MetaMask/metamask-extension/pull/9696): normalize deprecated itcss font styles
- [#9697](https://github.com/MetaMask/metamask-extension/pull/9697): normalize page font styles
- [#9740](https://github.com/MetaMask/metamask-extension/pull/9740): Standardize network settings page
- [#9750](https://github.com/MetaMask/metamask-extension/pull/9750): Make swap arrows accessible, make swaps advanced options accessible
- [#9766](https://github.com/MetaMask/metamask-extension/pull/9766): Use 1px borders on inputs and buttons
- [#9767](https://github.com/MetaMask/metamask-extension/pull/9767): Remove border radius from transfer button
- [#9764](https://github.com/MetaMask/metamask-extension/pull/9764): Update custom RPC network dropdown icons
- [#9763](https://github.com/MetaMask/metamask-extension/pull/9763): Add confirmation for network dropdown delete action
- [#9583](https://github.com/MetaMask/metamask-extension/pull/9583): Use `chainId` for incoming transactions controller
- [#9748](https://github.com/MetaMask/metamask-extension/pull/9748): Autofocus input, improve accessibility of restore page
- [#9778](https://github.com/MetaMask/metamask-extension/pull/9778): Shorten unit input width and use ellipses for overflow
- [#9746](https://github.com/MetaMask/metamask-extension/pull/9746): Make the login screen's Restore and Import links accessible
- [#9780](https://github.com/MetaMask/metamask-extension/pull/9780): Display decimal chain ID in network form
- [#9599](https://github.com/MetaMask/metamask-extension/pull/9599): Use MetaSwap API for gas price estimation in swaps
- [#9518](https://github.com/MetaMask/metamask-extension/pull/9518): Make all UI tabs accessible via keyboard
- [#9808](https://github.com/MetaMask/metamask-extension/pull/9808): Always allow overwriting invalid custom RPC chain ID
- [#9812](https://github.com/MetaMask/metamask-extension/pull/9812): Fix send header cancel button alignment
- [#9271](https://github.com/MetaMask/metamask-extension/pull/9271): Do not check popupIsOpen on Vivaldi
- [#9306](https://github.com/MetaMask/metamask-extension/pull/9306): Fix UI crash when dapp submits negative gas price
- [#9257](https://github.com/MetaMask/metamask-extension/pull/9257): Add sort and search to AddRecipient accounts list
- [#9824](https://github.com/MetaMask/metamask-extension/pull/9824): Move `externally_connectable` from base to Chrome manifest
- [#9815](https://github.com/MetaMask/metamask-extension/pull/9815): Add support for custom network RPC URL with basic auth
- [#9822](https://github.com/MetaMask/metamask-extension/pull/9822): Make QR code button focusable
- [#9832](https://github.com/MetaMask/metamask-extension/pull/9832): Warn instead of throw on duplicate web3
- [#9838](https://github.com/MetaMask/metamask-extension/pull/9838): @metamask/controllers@4.0.0
- [#9856](https://github.com/MetaMask/metamask-extension/pull/9856): Prevent user from getting stuck on opt in page
- [#9845](https://github.com/MetaMask/metamask-extension/pull/9845): Show a 'send eth' button on home screen in full screen mode
- [#9871](https://github.com/MetaMask/metamask-extension/pull/9871): Show send text upon hover in main asset list
- [#9880](https://github.com/MetaMask/metamask-extension/pull/9880): Properly detect U2F errors in hardware wallet
## 8.1.3 Mon Oct 26 2020
- [#9642](https://github.com/MetaMask/metamask-extension/pull/9642) Prevent excessive overflow from swap dropdowns
- [#9658](https://github.com/MetaMask/metamask-extension/pull/9658): Fix sorting Quote Source column of quote sort list

View File

@ -164,9 +164,6 @@
"cancel": {
"message": "ሰርዝ"
},
"cancelAttempt": {
"message": "ሙከራን ሰርዝ"
},
"cancellationGasFee": {
"message": "የስረዛ ነዳጅ ወጪ"
},
@ -308,9 +305,6 @@
"deleteNetworkDescription": {
"message": "ይህን አውታረ መረብ ለመሰረዝ እንደሚፈልጉ እርግጠኛ ነዎት?"
},
"deposit": {
"message": "ማጠራቀም"
},
"depositEther": {
"message": "Ether አስቀምጥ"
},
@ -737,7 +731,7 @@
"optionalBlockExplorerUrl": {
"message": "ኤክስፕሎረር URL አግድ (አማራጭ)"
},
"optionalSymbol": {
"optionalCurrencySymbol": {
"message": "ምልክት (አማራጭ)"
},
"orderOneHere": {
@ -985,9 +979,6 @@
"sentEther": {
"message": "የተላከ ether"
},
"sentTokens": {
"message": "የተላኩ ተለዋጭ ስሞች"
},
"separateEachWord": {
"message": "እያንዳንዱን ቃል በነጠላ ክፍት ቦታ ይለያዩ"
},

View File

@ -164,9 +164,6 @@
"cancel": {
"message": "إلغاء"
},
"cancelAttempt": {
"message": "إلغاء المحاولة"
},
"cancellationGasFee": {
"message": "رسوم الإلغاء بعملة جاس"
},
@ -308,9 +305,6 @@
"deleteNetworkDescription": {
"message": "هل أنت متأكد أنك تريد حذف هذه الشبكة؟"
},
"deposit": {
"message": "إيداع"
},
"depositEther": {
"message": "إيداع عملة إيثير"
},
@ -733,7 +727,7 @@
"optionalBlockExplorerUrl": {
"message": "العنوان الإلكتروني لمستكشف البلوكات (اختياري)"
},
"optionalSymbol": {
"optionalCurrencySymbol": {
"message": "الرمز (اختياري)"
},
"orderOneHere": {
@ -981,9 +975,6 @@
"sentEther": {
"message": "أرسل عملة إيثير"
},
"sentTokens": {
"message": "العملات الرمزية المرسلة"
},
"separateEachWord": {
"message": "افصل كل كلمة بمسافة واحدة"
},

View File

@ -164,9 +164,6 @@
"cancel": {
"message": "Отказ"
},
"cancelAttempt": {
"message": "Отмяна на опита"
},
"cancellationGasFee": {
"message": "Такса в газ за анулиране "
},
@ -308,9 +305,6 @@
"deleteNetworkDescription": {
"message": "Наистина ли искате да изтриете тази мрежа?"
},
"deposit": {
"message": "Депозит"
},
"depositEther": {
"message": "Депозирайте етер"
},
@ -736,7 +730,7 @@
"optionalBlockExplorerUrl": {
"message": "Блокиране на Explorer URL (по избор)"
},
"optionalSymbol": {
"optionalCurrencySymbol": {
"message": "Символ (по избор)"
},
"orderOneHere": {
@ -984,9 +978,6 @@
"sentEther": {
"message": "изпратен етер"
},
"sentTokens": {
"message": "изпратени жетони"
},
"separateEachWord": {
"message": "Отделете всяка дума с интервал"
},

View File

@ -164,9 +164,6 @@
"cancel": {
"message": "বাতিল করুন"
},
"cancelAttempt": {
"message": "প্রচেষ্টা বাতিল করুন"
},
"cancellationGasFee": {
"message": "বাতিল করার গ্যাস ফী"
},
@ -308,9 +305,6 @@
"deleteNetworkDescription": {
"message": "আপনি কি এই নেটওয়ার্কটি মোছার বিষয়ে নিশ্চিত?"
},
"deposit": {
"message": "জমা "
},
"depositEther": {
"message": "ইথার জমা করুন"
},
@ -740,7 +734,7 @@
"optionalBlockExplorerUrl": {
"message": "এক্সপ্লোরার URL ব্লক করুন (ঐচ্ছিক)"
},
"optionalSymbol": {
"optionalCurrencySymbol": {
"message": "প্রতীক (ঐচ্ছিক)"
},
"orderOneHere": {
@ -988,9 +982,6 @@
"sentEther": {
"message": "পাঠানো ইথার "
},
"sentTokens": {
"message": "টোকেনগুলি পাঠান"
},
"separateEachWord": {
"message": "প্রতিটি শব্দকে একটি স্পেস দিয়ে আলাদা করুন"
},

View File

@ -161,9 +161,6 @@
"cancel": {
"message": "Cancel·la"
},
"cancelAttempt": {
"message": "Cancel·la l'intent"
},
"cancellationGasFee": {
"message": "Preu de cancel·lació del gas"
},
@ -305,9 +302,6 @@
"deleteNetworkDescription": {
"message": "Estàs segur que vols eliminar aquesta xarxa?"
},
"deposit": {
"message": "Depòsit"
},
"depositEther": {
"message": "Diposita Ether"
},
@ -724,7 +718,7 @@
"optionalBlockExplorerUrl": {
"message": "Bloqueja l'URL d'Explorer (opcional)"
},
"optionalSymbol": {
"optionalCurrencySymbol": {
"message": "Símbol (opcional)"
},
"orderOneHere": {
@ -966,9 +960,6 @@
"sentEther": {
"message": "envia ether"
},
"sentTokens": {
"message": "fitxes enviades"
},
"separateEachWord": {
"message": "Separa cada paraula amb un sol espai"
},

View File

@ -118,9 +118,6 @@
"defaultNetwork": {
"message": "Výchozí síť pro Etherové transakce je Main Net."
},
"deposit": {
"message": "Vklad"
},
"depositEther": {
"message": "Vložit Ether"
},

View File

@ -164,9 +164,6 @@
"cancel": {
"message": "Afbryd"
},
"cancelAttempt": {
"message": "Annullér forsøg"
},
"cancellationGasFee": {
"message": "Gebyr for brændstofannullering"
},
@ -308,9 +305,6 @@
"deleteNetworkDescription": {
"message": "Er du sikker på, at du vil slette dette netværk?"
},
"deposit": {
"message": "Indbetal"
},
"depositEther": {
"message": "Indbetal Ether"
},
@ -724,7 +718,7 @@
"optionalBlockExplorerUrl": {
"message": "Blok-stifinder-URL (valgfrit)"
},
"optionalSymbol": {
"optionalCurrencySymbol": {
"message": "Symbol (valgfrit)"
},
"orderOneHere": {
@ -966,9 +960,6 @@
"sentEther": {
"message": "sendte ether"
},
"sentTokens": {
"message": "afsendte tokens"
},
"separateEachWord": {
"message": "Separer hvert ord med et enkelt mellemrum"
},

View File

@ -158,9 +158,6 @@
"cancel": {
"message": "Abbrechen"
},
"cancelAttempt": {
"message": "Versuch abbrechen"
},
"cancellationGasFee": {
"message": "Stornierungs-Gasgebühr"
},
@ -299,9 +296,6 @@
"deleteNetworkDescription": {
"message": "Sind Sie sicher, dass Sie dieses Netzwerk löschen möchten?"
},
"deposit": {
"message": "Einzahlen"
},
"depositEther": {
"message": "Ether einzahlen"
},
@ -957,9 +951,6 @@
"sentEther": {
"message": "Ether senden"
},
"sentTokens": {
"message": "gesendete Token"
},
"separateEachWord": {
"message": "Trennen Sie die Wörter mit einem einzelnen Leerzeichen voneinander"
},

View File

@ -161,9 +161,6 @@
"cancel": {
"message": "Ακύρωση"
},
"cancelAttempt": {
"message": "Ακύρωση Προσπάθειας"
},
"cancellationGasFee": {
"message": "Ακύρωση Χρέωσης Αερίου"
},
@ -305,9 +302,6 @@
"deleteNetworkDescription": {
"message": "Θέλετε σίγουρα να διαγράψετε αυτό το δίκτυο;"
},
"deposit": {
"message": "Κατάθεση"
},
"depositEther": {
"message": "Κατάθεση Ether"
},
@ -737,7 +731,7 @@
"optionalBlockExplorerUrl": {
"message": "Διεύθυνση URL Εξερευνητή Μπλοκ (προαιρετικό)"
},
"optionalSymbol": {
"optionalCurrencySymbol": {
"message": "Σύμβολο (προαιρετικό)"
},
"orderOneHere": {
@ -985,9 +979,6 @@
"sentEther": {
"message": "απεσταλμένα ether"
},
"sentTokens": {
"message": "αποστολή token"
},
"separateEachWord": {
"message": "Διαχωρίστε κάθε λέξη με ένα μόνο κενό"
},

View File

@ -239,9 +239,6 @@
"cancel": {
"message": "Cancel"
},
"cancelAttempt": {
"message": "Cancel Attempt"
},
"cancellationGasFee": {
"message": "Cancellation Gas Fee"
},
@ -477,9 +474,6 @@
"deleteNetworkDescription": {
"message": "Are you sure you want to delete this network?"
},
"deposit": {
"message": "Deposit"
},
"depositEther": {
"message": "Deposit Ether"
},
@ -875,7 +869,7 @@
"message": "Invalid IPFS Gateway: The value must be a valid URL"
},
"invalidNumber": {
"message": "Invalid number. Enter a decimal or hexadecimal number."
"message": "Invalid number. Enter a decimal or '0x'-prefixed hexadecimal number."
},
"invalidNumberLeadingZeros": {
"message": "Invalid number. Remove any leading zeros."
@ -1037,7 +1031,7 @@
"message": "Network Name"
},
"networkSettingsChainIdDescription": {
"message": "The chain ID is used for signing transactions. It must match the chain ID returned by the network. Enter a decimal or hexadecimal number starting with '0x'."
"message": "The chain ID is used for signing transactions. It must match the chain ID returned by the network. You can enter a decimal or '0x'-prefixed hexadecimal number, but we will display the number in decimal."
},
"networkSettingsDescription": {
"message": "Add and edit custom RPC networks"
@ -1156,8 +1150,8 @@
"optionalBlockExplorerUrl": {
"message": "Block Explorer URL (optional)"
},
"optionalSymbol": {
"message": "Symbol (optional)"
"optionalCurrencySymbol": {
"message": "Currency Symbol (optional)"
},
"orderOneHere": {
"message": "Order a Trezor or Ledger and keep your funds in cold storage"
@ -1458,9 +1452,6 @@
"sentEther": {
"message": "sent ether"
},
"sentTokens": {
"message": "sent tokens"
},
"separateEachWord": {
"message": "Separate each word with a single space"
},
@ -1827,6 +1818,9 @@
"swapSwapFrom": {
"message": "Swap from"
},
"swapSwapSwitch": {
"message": "Switch from and to tokens"
},
"swapSwapTo": {
"message": "Swap to"
},

View File

@ -136,9 +136,6 @@
"cancel": {
"message": "Cancelar"
},
"cancelAttempt": {
"message": "Intentar cancelar"
},
"cancellationGasFee": {
"message": "Comisión de Gas por cancelación"
},
@ -271,9 +268,6 @@
"deleteAccount": {
"message": "Eliminar Cuenta"
},
"deposit": {
"message": "Depositar"
},
"depositEther": {
"message": "Depositar Ether"
},
@ -580,7 +574,7 @@
"ofTextNofM": {
"message": "de"
},
"optionalSymbol": {
"optionalCurrencySymbol": {
"message": "Símbolo (opcional)"
},
"orderOneHere": {
@ -783,9 +777,6 @@
"sentEther": {
"message": "se mandó ether"
},
"sentTokens": {
"message": "se mandaron tokens"
},
"separateEachWord": {
"message": "Separar a cada palabra con un sólo espacio"
},

View File

@ -161,9 +161,6 @@
"cancel": {
"message": "Cancelar"
},
"cancelAttempt": {
"message": "Cancelar intento"
},
"cancellationGasFee": {
"message": "Tasa de cancelación de gas"
},
@ -305,9 +302,6 @@
"deleteNetworkDescription": {
"message": "¿Estás seguro de que deseas borrar esta red?"
},
"deposit": {
"message": "Depósito"
},
"depositEther": {
"message": "Depositar Ethers"
},
@ -725,7 +719,7 @@
"optionalBlockExplorerUrl": {
"message": "Bloquear la URL de Explorer (opcional)"
},
"optionalSymbol": {
"optionalCurrencySymbol": {
"message": "Símbolo (opcional)"
},
"orderOneHere": {
@ -973,9 +967,6 @@
"sentEther": {
"message": "Ethers enviados"
},
"sentTokens": {
"message": "tokens enviados"
},
"separateEachWord": {
"message": "Separa cada palabra con un solo espacio"
},

View File

@ -164,9 +164,6 @@
"cancel": {
"message": "Tühista"
},
"cancelAttempt": {
"message": "Tühista katse"
},
"cancellationGasFee": {
"message": "Tühistamise gaasitasu"
},
@ -308,9 +305,6 @@
"deleteNetworkDescription": {
"message": "Olete kindel, et soovite selle võrgu kustutada?"
},
"deposit": {
"message": "Sissemakse"
},
"depositEther": {
"message": "Eetri sissemakse"
},
@ -730,7 +724,7 @@
"optionalBlockExplorerUrl": {
"message": "Blokeeri Exploreri URL (valikuline)"
},
"optionalSymbol": {
"optionalCurrencySymbol": {
"message": "Sümbol (valikuline)"
},
"orderOneHere": {
@ -978,9 +972,6 @@
"sentEther": {
"message": "saadetud eeter"
},
"sentTokens": {
"message": "saadetud load"
},
"separateEachWord": {
"message": "Eraldage iga sõna ühe tühikuga"
},

View File

@ -164,9 +164,6 @@
"cancel": {
"message": "لغو"
},
"cancelAttempt": {
"message": "لغو تلاش"
},
"cancellationGasFee": {
"message": "لغو فیس گاز"
},
@ -308,9 +305,6 @@
"deleteNetworkDescription": {
"message": "آیا مطمئن هستید که این شبکه حذف شود؟"
},
"deposit": {
"message": "سپرده"
},
"depositEther": {
"message": "پرداخت ایتر"
},
@ -740,7 +734,7 @@
"optionalBlockExplorerUrl": {
"message": "بلاک کردن مرورگر URL (انتخابی)"
},
"optionalSymbol": {
"optionalCurrencySymbol": {
"message": "سمبول (انتخابی)"
},
"orderOneHere": {
@ -988,9 +982,6 @@
"sentEther": {
"message": "ایتر ارسال شد"
},
"sentTokens": {
"message": "رمزیاب های فرستاده شده"
},
"separateEachWord": {
"message": "هر کلمه را با یک فاصله واحد جدا سازید"
},

View File

@ -164,9 +164,6 @@
"cancel": {
"message": "Peruuta"
},
"cancelAttempt": {
"message": "Peruuta yritys"
},
"cancellationGasFee": {
"message": "Peruutuksen gas-maksu"
},
@ -308,9 +305,6 @@
"deleteNetworkDescription": {
"message": "Haluatko varmasti poistaa tämän verkon?"
},
"deposit": {
"message": "Talleta"
},
"depositEther": {
"message": "Talleta Etheriä"
},
@ -737,7 +731,7 @@
"optionalBlockExplorerUrl": {
"message": "Estä Explorerin URL-osoite (valinnainen)"
},
"optionalSymbol": {
"optionalCurrencySymbol": {
"message": "Symboli (valinnainen)"
},
"orderOneHere": {
@ -985,9 +979,6 @@
"sentEther": {
"message": "lähetä etheriä"
},
"sentTokens": {
"message": "lähetetyt poletit"
},
"separateEachWord": {
"message": "Erottele sanat toisistaan yhdellä välilyönnillä"
},

View File

@ -146,9 +146,6 @@
"cancel": {
"message": "Kanselahin"
},
"cancelAttempt": {
"message": "Kanselahin ang Pagtangka"
},
"cancellationGasFee": {
"message": "Gas Fee sa Pagkansela"
},
@ -284,9 +281,6 @@
"deleteNetworkDescription": {
"message": "Sigurado ka bang gusto mong i-delete ang network na ito?"
},
"deposit": {
"message": "Deposito"
},
"depositEther": {
"message": "Magdeposito ng Ether"
},
@ -671,7 +665,7 @@
"optionalBlockExplorerUrl": {
"message": "Block Explorer URL (opsyonal)"
},
"optionalSymbol": {
"optionalCurrencySymbol": {
"message": "Simbolo (opsyonal)"
},
"orderOneHere": {
@ -900,9 +894,6 @@
"sentEther": {
"message": "nagpadala ng ether"
},
"sentTokens": {
"message": "mga ipinadalang token"
},
"separateEachWord": {
"message": "Paghiwa-hiwalayin ang bawat salita gamit ang isang space"
},

View File

@ -155,9 +155,6 @@
"cancel": {
"message": "Annuler"
},
"cancelAttempt": {
"message": "Annuler la tentative."
},
"cancellationGasFee": {
"message": "Frais en gas de l'annulation"
},
@ -299,9 +296,6 @@
"deleteNetworkDescription": {
"message": "Êtes-vous sûr de vouloir supprimer ce réseau ?"
},
"deposit": {
"message": "Déposer"
},
"depositEther": {
"message": "Déposer de l'Ether"
},
@ -722,7 +716,7 @@
"optionalBlockExplorerUrl": {
"message": "Bloquer l'URL de l'explorateur (facultatif)"
},
"optionalSymbol": {
"optionalCurrencySymbol": {
"message": "Symbole (facultatif)"
},
"orderOneHere": {
@ -970,9 +964,6 @@
"sentEther": {
"message": "Ether envoyé"
},
"sentTokens": {
"message": "Jetons envoyés"
},
"separateEachWord": {
"message": "Separez chaque mot avec un espace simple"
},

View File

@ -164,9 +164,6 @@
"cancel": {
"message": "ביטול"
},
"cancelAttempt": {
"message": "בטל ניסיון"
},
"cancellationGasFee": {
"message": "עמלת דלק עבור ביטול"
},
@ -308,9 +305,6 @@
"deleteNetworkDescription": {
"message": "הנך בטוח/ה שברצונך למחוק רשת זו?"
},
"deposit": {
"message": "הפקדה"
},
"depositEther": {
"message": "הפקדת את'ר"
},
@ -737,7 +731,7 @@
"optionalBlockExplorerUrl": {
"message": "חסום כתובת URL של אקספלורר (אופציונלי)"
},
"optionalSymbol": {
"optionalCurrencySymbol": {
"message": "סמל (אופציונלי)"
},
"orderOneHere": {
@ -982,9 +976,6 @@
"sentEther": {
"message": "את'ר שנשלח"
},
"sentTokens": {
"message": "טוקנים שנשלחו"
},
"separateEachWord": {
"message": "יש להפריד כל מילה עם רווח יחיד"
},

View File

@ -164,9 +164,6 @@
"cancel": {
"message": "रद्द करें"
},
"cancelAttempt": {
"message": "प्रयास रद्द करें"
},
"cancellationGasFee": {
"message": "रद्दीकरण गैस शुल्क"
},
@ -308,9 +305,6 @@
"deleteNetworkDescription": {
"message": "क्या आप वाकई इस नेटवर्क को हटाना चाहते हैं?"
},
"deposit": {
"message": "जमा "
},
"depositEther": {
"message": "Ether जमा करें"
},
@ -737,7 +731,7 @@
"optionalBlockExplorerUrl": {
"message": "एक्सप्लोरर यूआरएल ब्लॉक (वैकल्पिक)"
},
"optionalSymbol": {
"optionalCurrencySymbol": {
"message": "सिम्बल (वैकल्पिक)"
},
"orderOneHere": {
@ -985,9 +979,6 @@
"sentEther": {
"message": "भेजे गए ether"
},
"sentTokens": {
"message": "भेजे गए टोकन"
},
"separateEachWord": {
"message": "प्रत्येक शब्द को एक स्पेस से अलग करें"
},

View File

@ -97,9 +97,6 @@
"defaultNetwork": {
"message": "ईथर लेनदेन के लिए डिफ़ॉल्ट नेटवर्क मुख्य नेट है।"
},
"deposit": {
"message": "जमा"
},
"depositEther": {
"message": "जमा - Ether"
},

View File

@ -164,9 +164,6 @@
"cancel": {
"message": "Odustani"
},
"cancelAttempt": {
"message": "Otkaži pokušaj"
},
"cancellationGasFee": {
"message": "Otkazivanje naknade za gorivo"
},
@ -308,9 +305,6 @@
"deleteNetworkDescription": {
"message": "Sigurno želite izbrisati ovu mrežu?"
},
"deposit": {
"message": "Polog"
},
"depositEther": {
"message": "Položi Ether"
},
@ -733,7 +727,7 @@
"optionalBlockExplorerUrl": {
"message": "Blokiraj Explorerov URL (neobavezno)"
},
"optionalSymbol": {
"optionalCurrencySymbol": {
"message": "Simbol (neobavezno)"
},
"orderOneHere": {
@ -981,9 +975,6 @@
"sentEther": {
"message": "pošalji ether"
},
"sentTokens": {
"message": "pošalji tokene"
},
"separateEachWord": {
"message": "Odvojite pojedinačne riječi jednim razmakom"
},

View File

@ -85,9 +85,6 @@
"cancel": {
"message": "Anile"
},
"cancelAttempt": {
"message": "Teste Anile"
},
"cancellationGasFee": {
"message": "Anilasyon Gaz Chaj"
},
@ -169,9 +166,6 @@
"defaultNetwork": {
"message": "Dfo rezo a pou tranzaksyon Ether se Mainnet."
},
"deposit": {
"message": "Depo"
},
"depositEther": {
"message": "Depo Ether"
},
@ -615,9 +609,6 @@
"sentEther": {
"message": "Voye ether"
},
"sentTokens": {
"message": "tokens deja voye"
},
"separateEachWord": {
"message": "Separe chak mo ak yon sèl espas"
},

View File

@ -164,9 +164,6 @@
"cancel": {
"message": "Mégse"
},
"cancelAttempt": {
"message": "Kísérlet megszakítása"
},
"cancellationGasFee": {
"message": "A törlés gázára"
},
@ -308,9 +305,6 @@
"deleteNetworkDescription": {
"message": "Biztosan törli ezt a hálózatot?"
},
"deposit": {
"message": "Befizetés"
},
"depositEther": {
"message": "Ether befizetése"
},
@ -733,7 +727,7 @@
"optionalBlockExplorerUrl": {
"message": "Explorer URL letiltása (nem kötelező)"
},
"optionalSymbol": {
"optionalCurrencySymbol": {
"message": "Szimbólum (opcionális)"
},
"orderOneHere": {
@ -981,9 +975,6 @@
"sentEther": {
"message": "elküldött ether"
},
"sentTokens": {
"message": "elküldött érmék"
},
"separateEachWord": {
"message": "Minden egyes szavat szóközzel válasszon el"
},

View File

@ -164,9 +164,6 @@
"cancel": {
"message": "Batal"
},
"cancelAttempt": {
"message": "Batalkan Percobaan"
},
"cancellationGasFee": {
"message": "Pembatalan Biaya Gas"
},
@ -721,7 +718,7 @@
"optionalBlockExplorerUrl": {
"message": "Blokir URL Penjelajah (opsional)"
},
"optionalSymbol": {
"optionalCurrencySymbol": {
"message": "Simbol (opsional)"
},
"orderOneHere": {
@ -969,9 +966,6 @@
"sentEther": {
"message": "kirim ether"
},
"sentTokens": {
"message": "token terkirim"
},
"separateEachWord": {
"message": "Pisahkan setiap kata dengan spasi tunggal"
},

View File

@ -226,9 +226,6 @@
"cancel": {
"message": "Annulla"
},
"cancelAttempt": {
"message": "Tentativo di Annullamento"
},
"cancellationGasFee": {
"message": "Costo di Annullamento in Gas"
},
@ -464,9 +461,6 @@
"deleteNetworkDescription": {
"message": "Sei sicuro di voler eliminare questa rete?"
},
"deposit": {
"message": "Deposita"
},
"depositEther": {
"message": "Deposita Ether"
},
@ -1056,7 +1050,7 @@
"optionalBlockExplorerUrl": {
"message": "URL del Block Explorer (opzionale)"
},
"optionalSymbol": {
"optionalCurrencySymbol": {
"message": "Simbolo (opzionale)"
},
"orderOneHere": {
@ -1358,9 +1352,6 @@
"sentEther": {
"message": "ether inviati"
},
"sentTokens": {
"message": "tokens inviati"
},
"separateEachWord": {
"message": "Separa ogni parola con un solo spazio"
},

View File

@ -169,9 +169,6 @@
"defaultNetwork": {
"message": "デフォルトのEther送受信ネットワークはメインネットです。"
},
"deposit": {
"message": "振込"
},
"depositEther": {
"message": "Etherを振込"
},

View File

@ -164,9 +164,6 @@
"cancel": {
"message": "ರದ್ದುಮಾಡಿ"
},
"cancelAttempt": {
"message": "ಪ್ರಯತ್ನವನ್ನು ರದ್ದುಪಡಿಸಿ"
},
"cancellationGasFee": {
"message": "ರದ್ದುಗೊಳಿಸುವ ಗ್ಯಾಸ್ ಶುಲ್ಕ"
},
@ -308,9 +305,6 @@
"deleteNetworkDescription": {
"message": "ನೀವು ಈ ನೆಟ್‌ವರ್ಕ್ ಅನ್ನು ಖಚಿತವಾಗಿ ಅಳಿಸಲು ಬಯಸುತ್ತೀರಾ?"
},
"deposit": {
"message": "ಠೇವಣಿ"
},
"depositEther": {
"message": "ಎಥರ್ ಠೇವಣಿ ಮಾಡಿ"
},
@ -740,7 +734,7 @@
"optionalBlockExplorerUrl": {
"message": "ಅನ್ವೇಷಕ URL ಅನ್ನು ನಿರ್ಬಂಧಿಸಿ (ಐಚ್ಛಿಕ)"
},
"optionalSymbol": {
"optionalCurrencySymbol": {
"message": "ಚಿಹ್ನೆ (ಐಚ್ಛಿಕ)"
},
"orderOneHere": {
@ -988,9 +982,6 @@
"sentEther": {
"message": "ಕಳುಹಿಸಲಾದ ಎಥರ್"
},
"sentTokens": {
"message": "ಕಳುಹಿಸಲಾದ ಟೋಕನ್‌ಗಳು"
},
"separateEachWord": {
"message": "ಒಂದು ಸ್ಪೇಸ್ ಮೂಲಕ ಪ್ರತಿ ಪದವನ್ನು ಬೇರ್ಪಡಿಸಿ"
},

View File

@ -161,9 +161,6 @@
"cancel": {
"message": "취소"
},
"cancelAttempt": {
"message": "취소 시도"
},
"cancellationGasFee": {
"message": "취소 가스 수수료"
},
@ -305,9 +302,6 @@
"deleteNetworkDescription": {
"message": "정말로 이 네트워크를 삭제하시겠습니까?"
},
"deposit": {
"message": "입금"
},
"depositEther": {
"message": "이더리움 입금하기"
},
@ -734,7 +728,7 @@
"optionalBlockExplorerUrl": {
"message": "익스플로러 URL 차단 (선택 사항)"
},
"optionalSymbol": {
"optionalCurrencySymbol": {
"message": "Symbol (선택)"
},
"orderOneHere": {
@ -979,9 +973,6 @@
"sentEther": {
"message": "전송된 이더"
},
"sentTokens": {
"message": "전송된 토큰"
},
"separateEachWord": {
"message": "각 단어는 공백 한칸으로 분리합니다"
},

View File

@ -164,9 +164,6 @@
"cancel": {
"message": "Atšaukti"
},
"cancelAttempt": {
"message": "Atšaukti mėginimą"
},
"cancellationGasFee": {
"message": "Dujų mokesčio atšaukimas"
},
@ -308,9 +305,6 @@
"deleteNetworkDescription": {
"message": "Ar tikrai norite panaikinti šį tinklą?"
},
"deposit": {
"message": "Indėlis"
},
"depositEther": {
"message": "Įnešti eterių"
},
@ -740,7 +734,7 @@
"optionalBlockExplorerUrl": {
"message": "Blokuoti naršyklės URL (pasirinktinai)"
},
"optionalSymbol": {
"optionalCurrencySymbol": {
"message": "Simbolis (nebūtinas)"
},
"orderOneHere": {
@ -988,9 +982,6 @@
"sentEther": {
"message": "siųsti eterių"
},
"sentTokens": {
"message": "išsiųsti žetonai"
},
"separateEachWord": {
"message": "Kiekvieną žodį atskirkite viengubu tarpu"
},

View File

@ -164,9 +164,6 @@
"cancel": {
"message": "Atcelt"
},
"cancelAttempt": {
"message": "Atcelt mēģinājumu"
},
"cancellationGasFee": {
"message": "Atcelšanas maksājums par Gas"
},
@ -308,9 +305,6 @@
"deleteNetworkDescription": {
"message": "Vai tiešām vēlaties dzēst šo tīklu?"
},
"deposit": {
"message": "Iemaksa"
},
"depositEther": {
"message": "Noguldīt Ether"
},
@ -736,7 +730,7 @@
"optionalBlockExplorerUrl": {
"message": "Bloķēt Explorer URL (pēc izvēles)"
},
"optionalSymbol": {
"optionalCurrencySymbol": {
"message": "Simbols (neobligāti)"
},
"orderOneHere": {
@ -984,9 +978,6 @@
"sentEther": {
"message": "nosūtītie ether"
},
"sentTokens": {
"message": "nosūtītie marķieri"
},
"separateEachWord": {
"message": "Atdaliet katru vārdu ar vienu atstarpi"
},

View File

@ -164,9 +164,6 @@
"cancel": {
"message": "Batal"
},
"cancelAttempt": {
"message": "Batalkan Percubaan"
},
"cancellationGasFee": {
"message": "Fi Gas Pembatalan"
},
@ -714,7 +711,7 @@
"optionalBlockExplorerUrl": {
"message": "Sekat URL Explorer (pilihan)"
},
"optionalSymbol": {
"optionalCurrencySymbol": {
"message": "Simbol (pilihan)"
},
"orderOneHere": {
@ -962,9 +959,6 @@
"sentEther": {
"message": "menghantar ether"
},
"sentTokens": {
"message": "token dihantar"
},
"separateEachWord": {
"message": "Pisahkan setiap perkataan dengan ruang tunggal"
},

View File

@ -94,9 +94,6 @@
"defaultNetwork": {
"message": "Het standaardnetwerk voor Ether-transacties is Main Net."
},
"deposit": {
"message": "Storting"
},
"depositEther": {
"message": "Stort Ether"
},

View File

@ -161,9 +161,6 @@
"cancel": {
"message": "Avbryt"
},
"cancelAttempt": {
"message": "Avbryt forsøk"
},
"cancellationGasFee": {
"message": "Kansellering gassavgift"
},
@ -305,9 +302,6 @@
"deleteNetworkDescription": {
"message": "Er du sikker på at du vil slette dette nettverket?"
},
"deposit": {
"message": "Innskudd"
},
"depositEther": {
"message": "Sett inn Ether "
},
@ -727,7 +721,7 @@
"optionalBlockExplorerUrl": {
"message": "Blokker Explorer URL (valgfritt)"
},
"optionalSymbol": {
"optionalCurrencySymbol": {
"message": "Symbol (valgfritt)"
},
"orderOneHere": {
@ -966,9 +960,6 @@
"sentEther": {
"message": "sendt ether"
},
"sentTokens": {
"message": "sendte tokener "
},
"separateEachWord": {
"message": "Del hvert ord med et enkelt mellomrom "
},

View File

@ -73,9 +73,6 @@
"defaultNetwork": {
"message": "Ang default network para sa Ether transactions ay ang Main Net."
},
"deposit": {
"message": "Deposito"
},
"depositEther": {
"message": "I-deposito ang Ether"
},

View File

@ -164,9 +164,6 @@
"cancel": {
"message": "Anuluj"
},
"cancelAttempt": {
"message": "Anuluj próbę"
},
"cancellationGasFee": {
"message": "Opłata za gaz za anulowanie"
},
@ -308,9 +305,6 @@
"deleteNetworkDescription": {
"message": "Czy na pewno chcesz usunąć tę sieć?"
},
"deposit": {
"message": "Zdeponuj"
},
"depositEther": {
"message": "Zdeponuj Eter"
},
@ -734,7 +728,7 @@
"optionalBlockExplorerUrl": {
"message": "Adres URL przeglądarki łańcucha bloków (opcjonalnie)"
},
"optionalSymbol": {
"optionalCurrencySymbol": {
"message": "Symbol (opcjonalnie)"
},
"orderOneHere": {
@ -982,9 +976,6 @@
"sentEther": {
"message": "wyślij eter"
},
"sentTokens": {
"message": "wysłane tokeny"
},
"separateEachWord": {
"message": "Oddziel słowa pojedynczą spacją"
},

View File

@ -97,9 +97,6 @@
"defaultNetwork": {
"message": "A rede pré definida para transações em Ether é a Main Net."
},
"deposit": {
"message": "Depósito"
},
"depositEther": {
"message": "Depositar Ether"
},

View File

@ -158,9 +158,6 @@
"cancel": {
"message": "Cancelar"
},
"cancelAttempt": {
"message": "Tentativa de cancelamento"
},
"cancellationGasFee": {
"message": "Tarifa de Gas de cancelamento"
},
@ -302,9 +299,6 @@
"deleteNetworkDescription": {
"message": "Tem certeza de que deseja excluir esta rede?"
},
"deposit": {
"message": "Depósito"
},
"depositEther": {
"message": "Depositar Ether"
},
@ -728,7 +722,7 @@
"optionalBlockExplorerUrl": {
"message": "URL exploradora de blocos (opcional)"
},
"optionalSymbol": {
"optionalCurrencySymbol": {
"message": "Símbolo (opcional)"
},
"orderOneHere": {
@ -976,9 +970,6 @@
"sentEther": {
"message": "ether enviado"
},
"sentTokens": {
"message": "tokens enviados"
},
"separateEachWord": {
"message": "Separe cada palavra com um único espaço"
},

View File

@ -45,9 +45,6 @@
"delete": {
"message": "Eliminar"
},
"deposit": {
"message": "Depósito"
},
"details": {
"message": "Detalhes"
},

View File

@ -164,9 +164,6 @@
"cancel": {
"message": "Anulare"
},
"cancelAttempt": {
"message": "Anulare încercare"
},
"cancellationGasFee": {
"message": "Taxă de anulare în gaz"
},
@ -308,9 +305,6 @@
"deleteNetworkDescription": {
"message": "Sigur vreți să ștergeți această rețea?"
},
"deposit": {
"message": "Depunere"
},
"depositEther": {
"message": "Depuneți Ether"
},
@ -727,7 +721,7 @@
"optionalBlockExplorerUrl": {
"message": "URL explorator bloc (opțional)"
},
"optionalSymbol": {
"optionalCurrencySymbol": {
"message": "Simbol (opțional)"
},
"orderOneHere": {
@ -975,9 +969,6 @@
"sentEther": {
"message": "trimiteți ether"
},
"sentTokens": {
"message": "tokenuri trimise"
},
"separateEachWord": {
"message": "Despărțiți fiecare cuvânt cu un spațiu"
},

View File

@ -163,9 +163,6 @@
"cancel": {
"message": "Отмена"
},
"cancelAttempt": {
"message": "Попытка отмены транзакции"
},
"cancellationGasFee": {
"message": "Комиссия за газ на отмену"
},
@ -330,9 +327,6 @@
"deleteNetworkDescription": {
"message": "Вы уверены, что хотите удалить эту сеть?"
},
"deposit": {
"message": "Пополнить"
},
"depositEther": {
"message": "Пополнить Ether"
},
@ -769,7 +763,7 @@
"optionalBlockExplorerUrl": {
"message": "URL блок-эксплорера (необязательно)"
},
"optionalSymbol": {
"optionalCurrencySymbol": {
"message": "Символ (необязательно)"
},
"orderOneHere": {
@ -1024,9 +1018,6 @@
"sentEther": {
"message": "Отправить Ether"
},
"sentTokens": {
"message": "Отправленные токены"
},
"separateEachWord": {
"message": "Разделяйте каждое слово одним пробелом"
},

View File

@ -158,9 +158,6 @@
"cancel": {
"message": "Zrušit"
},
"cancelAttempt": {
"message": "Zrušiť pokus"
},
"cancellationGasFee": {
"message": "Storno poplatok za GAS"
},
@ -302,9 +299,6 @@
"deleteNetworkDescription": {
"message": "Naozaj chcete túto sieť odstrániť?"
},
"deposit": {
"message": "Vklad"
},
"depositEther": {
"message": "Vložit Ether"
},
@ -709,7 +703,7 @@
"optionalBlockExplorerUrl": {
"message": "Blokovať URL Explorera (voliteľné)"
},
"optionalSymbol": {
"optionalCurrencySymbol": {
"message": "Symbol (voliteľné)"
},
"orderOneHere": {
@ -951,9 +945,6 @@
"sentEther": {
"message": "poslaný ether"
},
"sentTokens": {
"message": "poslané tokeny"
},
"separateEachWord": {
"message": "Každé slovo oddeľte jednou medzerou"
},

View File

@ -164,9 +164,6 @@
"cancel": {
"message": "Prekliči"
},
"cancelAttempt": {
"message": "Prekliči poskus"
},
"cancellationGasFee": {
"message": "Preklicani znesek gas"
},
@ -308,9 +305,6 @@
"deleteNetworkDescription": {
"message": "Ali ste prepričani, da želite izbrisati to omrežje?"
},
"deposit": {
"message": "Vplačaj"
},
"depositEther": {
"message": "Vplačilo ethra"
},
@ -725,7 +719,7 @@
"optionalBlockExplorerUrl": {
"message": "Blokiraj URL Explorerja (poljubno)"
},
"optionalSymbol": {
"optionalCurrencySymbol": {
"message": "Simbol (nezahtevano)"
},
"orderOneHere": {
@ -970,9 +964,6 @@
"sentEther": {
"message": "poslani ether"
},
"sentTokens": {
"message": "poslani žetoni"
},
"separateEachWord": {
"message": "Vsako besedo ločite z enim presledkom"
},

View File

@ -164,9 +164,6 @@
"cancel": {
"message": "Otkaži"
},
"cancelAttempt": {
"message": "Otkaži pokušaj"
},
"cancellationGasFee": {
"message": "Otkazivanje gas naknade"
},
@ -305,9 +302,6 @@
"deleteNetworkDescription": {
"message": "Da li ste sigurni da želite da izbrišete ovu mrežu?"
},
"deposit": {
"message": "Depozit"
},
"depositEther": {
"message": "Dajte depozit Ether-u"
},
@ -731,7 +725,7 @@
"optionalBlockExplorerUrl": {
"message": "Blokirajte URL Explorer-a (opciono)"
},
"optionalSymbol": {
"optionalCurrencySymbol": {
"message": "Simbol (opciono)"
},
"orderOneHere": {
@ -979,9 +973,6 @@
"sentEther": {
"message": "ether je poslat"
},
"sentTokens": {
"message": "poslati tokeni"
},
"separateEachWord": {
"message": "Razdvojite svaku reč jednim mestom razmaka"
},

View File

@ -161,9 +161,6 @@
"cancel": {
"message": "Avbryt"
},
"cancelAttempt": {
"message": "Avbryt försök"
},
"cancellationGasFee": {
"message": "Gasavgift för avbrytning"
},
@ -302,9 +299,6 @@
"deleteNetworkDescription": {
"message": "Är du säker på att du vill ta bort detta nätverk?"
},
"deposit": {
"message": "Deposition"
},
"depositEther": {
"message": "Sätt in Ether"
},
@ -724,7 +718,7 @@
"optionalBlockExplorerUrl": {
"message": "Block Explorer URL (valfritt)"
},
"optionalSymbol": {
"optionalCurrencySymbol": {
"message": "Symbol (frivillig)"
},
"orderOneHere": {
@ -972,9 +966,6 @@
"sentEther": {
"message": "skickat ether"
},
"sentTokens": {
"message": "skickade tokens"
},
"separateEachWord": {
"message": "Lägg in ett mellanslag mellan varje ord"
},

View File

@ -158,9 +158,6 @@
"cancel": {
"message": "Ghairi"
},
"cancelAttempt": {
"message": "Jaribio la Kubatilisha"
},
"cancellationGasFee": {
"message": "Ada ya Kubatilisha Gesi"
},
@ -302,9 +299,6 @@
"deleteNetworkDescription": {
"message": "Una uhakika unataka kufuta mtandao huu?"
},
"deposit": {
"message": "Fedha zilizopo kwenye akaunti"
},
"depositEther": {
"message": "Weka Ether"
},
@ -718,7 +712,7 @@
"optionalBlockExplorerUrl": {
"message": "URL ya Block Explorer URL (hiari)"
},
"optionalSymbol": {
"optionalCurrencySymbol": {
"message": "Ishara (hiari)"
},
"orderOneHere": {
@ -966,9 +960,6 @@
"sentEther": {
"message": "ether iliyotumwa"
},
"sentTokens": {
"message": "vianzio vilivyotumwa"
},
"separateEachWord": {
"message": "Tenganisha kila neno kwa nafasi moja"
},

View File

@ -136,9 +136,6 @@
"delete": {
"message": "நீக்கு"
},
"deposit": {
"message": "வைப்புத்தொகை"
},
"depositEther": {
"message": "வைப்புத்தொகை எதிர் "
},

View File

@ -142,9 +142,6 @@
"deleteNetwork": {
"message": "ลบเครือข่าย?"
},
"deposit": {
"message": "ฝาก"
},
"depositEther": {
"message": "การฝากอีเธอร์"
},

View File

@ -118,9 +118,6 @@
"defaultNetwork": {
"message": "Ether işlemleri için varsayılan ağ Main Net."
},
"deposit": {
"message": "Yatır"
},
"depositEther": {
"message": "Ether yatır"
},

View File

@ -164,9 +164,6 @@
"cancel": {
"message": "Скасувати"
},
"cancelAttempt": {
"message": "Відмінити спробу"
},
"cancellationGasFee": {
"message": "Вартість пального за скасування"
},
@ -308,9 +305,6 @@
"deleteNetworkDescription": {
"message": "Ви впевнені, що хочете видалити цю мережу?"
},
"deposit": {
"message": "Депозит"
},
"depositEther": {
"message": "Депонувати Ether"
},
@ -740,7 +734,7 @@
"optionalBlockExplorerUrl": {
"message": "Блокувати Explorer URL (не обов'язково)"
},
"optionalSymbol": {
"optionalCurrencySymbol": {
"message": "Символ (не обов'язково)"
},
"orderOneHere": {
@ -988,9 +982,6 @@
"sentEther": {
"message": "надісланий ефір"
},
"sentTokens": {
"message": "надіслані токени"
},
"separateEachWord": {
"message": "Відділіть кожне слово одним пробілом"
},

View File

@ -79,9 +79,6 @@
"defaultNetwork": {
"message": "Mạng lưới mặc định dùng cho các giao dịch Ether là Main Net (tiền ETH thật)."
},
"deposit": {
"message": "Ký gửi/nạp tiền"
},
"depositEther": {
"message": "Ký gửi Ether"
},

View File

@ -164,9 +164,6 @@
"cancel": {
"message": "取消"
},
"cancelAttempt": {
"message": "取消操作"
},
"cancellationGasFee": {
"message": "取消天然气费"
},
@ -308,9 +305,6 @@
"deleteNetworkDescription": {
"message": "您是否确认要删除该网络?"
},
"deposit": {
"message": "存入"
},
"depositEther": {
"message": "存入 Ether"
},
@ -722,7 +716,7 @@
"optionalBlockExplorerUrl": {
"message": "屏蔽管理器 URL选填"
},
"optionalSymbol": {
"optionalCurrencySymbol": {
"message": "符号(选填)"
},
"orderOneHere": {
@ -970,9 +964,6 @@
"sentEther": {
"message": "以太币已发送"
},
"sentTokens": {
"message": "代币已发送"
},
"separateEachWord": {
"message": "用空格分隔每个单词"
},

View File

@ -164,9 +164,6 @@
"cancel": {
"message": "取消"
},
"cancelAttempt": {
"message": "嘗試取消"
},
"cancellationGasFee": {
"message": "需要的手續費"
},
@ -308,9 +305,6 @@
"deleteNetworkDescription": {
"message": "你確定要刪除網路嗎?"
},
"deposit": {
"message": "存入"
},
"depositEther": {
"message": "存入乙太幣"
},
@ -731,7 +725,7 @@
"optionalBlockExplorerUrl": {
"message": "區塊鏈瀏覽器 URL非必要"
},
"optionalSymbol": {
"optionalCurrencySymbol": {
"message": "Symbol (可選)"
},
"orderOneHere": {
@ -967,9 +961,6 @@
"sentEther": {
"message": "發送乙太幣"
},
"sentTokens": {
"message": "發送代幣"
},
"separateEachWord": {
"message": "單詞之間請以空白間隔"
},

View File

@ -41,10 +41,6 @@
],
"default_locale": "en",
"description": "__MSG_appDescription__",
"externally_connectable": {
"matches": ["https://metamask.io/*"],
"ids": ["*"]
},
"icons": {
"16": "images/icon-16.png",
"19": "images/icon-19.png",
@ -68,6 +64,6 @@
"notifications"
],
"short_name": "__MSG_appName__",
"version": "8.1.3",
"version": "8.1.4",
"web_accessible_resources": ["inpage.js", "phishing.html"]
}

View File

@ -1,3 +1,7 @@
{
"externally_connectable": {
"matches": ["https://metamask.io/*"],
"ids": ["*"]
},
"minimum_chrome_version": "58"
}

View File

@ -2,10 +2,10 @@ 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) {
importAccount(strategy, args) {
try {
const importer = this.strategies[strategy]
const privateKeyHex = importer(...args)
@ -21,7 +21,7 @@ const accountImporter = {
throw new Error('Cannot import an empty key.')
}
const prefixed = ethUtil.addHexPrefix(privateKey)
const prefixed = addHexPrefix(privateKey)
const buffer = ethUtil.toBuffer(prefixed)
if (!ethUtil.isValidPrivate(buffer)) {
@ -43,10 +43,9 @@ const accountImporter = {
return walletToPrivateKey(wallet)
},
},
}
function walletToPrivateKey (wallet) {
function walletToPrivateKey(wallet) {
const privateKeyBuffer = wallet.getPrivateKey()
return ethUtil.bufferToHex(privateKeyBuffer)
}

View File

@ -60,9 +60,7 @@ const requestAccountTabIds = {}
// state persistence
const inTest = process.env.IN_TEST === 'true'
const localStore = inTest
? new ReadOnlyNetworkStore()
: new LocalStore()
const localStore = inTest ? new ReadOnlyNetworkStore() : new LocalStore()
let versionedData
if (inTest || process.env.METAMASK_DEBUG) {
@ -141,9 +139,9 @@ initialize().catch(log.error)
/**
* Initializes the MetaMask controller, and sets up all platform configuration.
* @returns {Promise} - Setup complete.
* @returns {Promise} Setup complete.
*/
async function initialize () {
async function initialize() {
const initState = await loadStateFromPersistence()
const initLangCode = await getFirstPreferredLangCode()
await setupController(initState, initLangCode)
@ -157,17 +155,17 @@ async function initialize () {
/**
* Loads any stored data, prioritizing the latest storage strategy.
* Migrates that data schema in case it was last loaded on an older version.
* @returns {Promise<MetaMaskState>} - Last data emitted from previous instance of MetaMask.
* @returns {Promise<MetaMaskState>} Last data emitted from previous instance of MetaMask.
*/
async function loadStateFromPersistence () {
async function loadStateFromPersistence() {
// migrations
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)
versionedData =
(await localStore.get()) || migrator.generateInitialState(firstTimeState)
// check if somehow state is empty
// this should never happen but new error reporting suggests that it has
@ -217,9 +215,9 @@ async function loadStateFromPersistence () {
*
* @param {Object} initState - The initial state to start the controller with, matches the state that is emitted from the controller.
* @param {string} initLangCode - The region code for the language preferred by the current user.
* @returns {Promise} - After setup is complete.
* @returns {Promise} After setup is complete.
*/
function setupController (initState, initLangCode) {
function setupController(initState, initLangCode) {
//
// MetaMask Controller
//
@ -249,7 +247,9 @@ function setupController (initState, initLangCode) {
setupEnsIpfsResolver({
getCurrentNetwork: controller.getCurrentNetwork,
getIpfsGateway: controller.preferencesController.getIpfsGateway.bind(controller.preferencesController),
getIpfsGateway: controller.preferencesController.getIpfsGateway.bind(
controller.preferencesController,
),
provider: controller.provider,
})
@ -267,14 +267,14 @@ function setupController (initState, initLangCode) {
/**
* Assigns the given state to the versioned object (with metadata), and returns that.
* @param {Object} state - The state object as emitted by the MetaMaskController.
* @returns {VersionedData} - The state object wrapped in an object that includes a metadata key.
* @returns {VersionedData} The state object wrapped in an object that includes a metadata key.
*/
function versionifyData (state) {
function versionifyData(state) {
versionedData.data = state
return versionedData
}
async function persistData (state) {
async function persistData(state) {
if (!state) {
throw new Error('MetaMask - updated state is missing')
}
@ -303,12 +303,14 @@ function setupController (initState, initLangCode) {
[ENVIRONMENT_TYPE_FULLSCREEN]: true,
}
const metamaskBlockedPorts = [
'trezor-connect',
]
const metamaskBlockedPorts = ['trezor-connect']
const isClientOpenStatus = () => {
return popupIsOpen || Boolean(Object.keys(openMetamaskTabsIDs).length) || notificationIsOpen
return (
popupIsOpen ||
Boolean(Object.keys(openMetamaskTabsIDs).length) ||
notificationIsOpen
)
}
/**
@ -323,7 +325,7 @@ function setupController (initState, initLangCode) {
* This method identifies trusted (MetaMask) interfaces, and connects them differently from untrusted (web pages).
* @param {Port} remotePort - The port provided by a new context.
*/
function connectRemote (remotePort) {
function connectRemote(remotePort) {
const processName = remotePort.name
const isMetaMaskInternalProcess = metamaskInternalProcessHash[processName]
@ -381,7 +383,7 @@ function setupController (initState, initLangCode) {
}
// communication with page or other extension
function connectExternal (remotePort) {
function connectExternal(remotePort) {
const portStream = new PortStream(remotePort)
controller.setupUntrustedCommunication(portStream, remotePort.sender)
}
@ -404,18 +406,30 @@ function setupController (initState, initLangCode) {
* 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 () {
function updateBadge() {
let label = ''
const unapprovedTxCount = controller.txController.getUnapprovedTxCount()
const { unapprovedMsgCount } = controller.messageManager
const { unapprovedPersonalMsgCount } = controller.personalMessageManager
const { unapprovedDecryptMsgCount } = controller.decryptMessageManager
const { unapprovedEncryptionPublicKeyMsgCount } = controller.encryptionPublicKeyManager
const {
unapprovedEncryptionPublicKeyMsgCount,
} = controller.encryptionPublicKeyManager
const { unapprovedTypedMessagesCount } = controller.typedMessageManager
const pendingPermissionRequests = Object.keys(controller.permissionsController.permissions.state.permissionsRequests).length
const waitingForUnlockCount = controller.appStateController.waitingForUnlock.length
const count = unapprovedTxCount + unapprovedMsgCount + unapprovedPersonalMsgCount + unapprovedDecryptMsgCount + unapprovedEncryptionPublicKeyMsgCount +
unapprovedTypedMessagesCount + pendingPermissionRequests + waitingForUnlockCount
const pendingPermissionRequests = Object.keys(
controller.permissionsController.permissions.state.permissionsRequests,
).length
const waitingForUnlockCount =
controller.appStateController.waitingForUnlock.length
const count =
unapprovedTxCount +
unapprovedMsgCount +
unapprovedPersonalMsgCount +
unapprovedDecryptMsgCount +
unapprovedEncryptionPublicKeyMsgCount +
unapprovedTypedMessagesCount +
pendingPermissionRequests +
waitingForUnlockCount
if (count) {
label = String(count)
}
@ -433,10 +447,18 @@ function setupController (initState, initLangCode) {
/**
* Opens the browser popup for user confirmation
*/
async function triggerUi () {
async function triggerUi() {
const tabs = await platform.getActiveTabs()
const currentlyActiveMetamaskTab = Boolean(tabs.find((tab) => openMetamaskTabsIDs[tab.id]))
if (!popupIsOpen && !currentlyActiveMetamaskTab) {
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
if ((isVivaldi || !popupIsOpen) && !currentlyActiveMetamaskTab) {
await notificationManager.showPopup()
}
}
@ -445,23 +467,24 @@ async function triggerUi () {
* Opens the browser popup for user confirmation of watchAsset
* then it waits until user interact with the UI
*/
async function openPopup () {
async function openPopup() {
await triggerUi()
await new Promise(
(resolve) => {
const interval = setInterval(() => {
if (!notificationIsOpen) {
clearInterval(interval)
resolve()
}
}, 1000)
},
)
await new Promise((resolve) => {
const interval = setInterval(() => {
if (!notificationIsOpen) {
clearInterval(interval)
resolve()
}
}, 1000)
})
}
// On first install, open a new tab with MetaMask
extension.runtime.onInstalled.addListener(({ reason }) => {
if (reason === 'install' && !(process.env.METAMASK_DEBUG || process.env.IN_TEST)) {
if (
reason === 'install' &&
!(process.env.METAMASK_DEBUG || process.env.IN_TEST)
) {
platform.openExtensionInBrowser()
}
})

View File

@ -9,7 +9,10 @@ import PortStream from 'extension-port-stream'
const fs = require('fs')
const path = require('path')
const inpageContent = fs.readFileSync(path.join(__dirname, '..', '..', 'dist', 'chrome', 'inpage.js'), 'utf8')
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
@ -30,7 +33,7 @@ if (shouldInjectProvider()) {
*
* @param {string} content - Code to be executed in the current document
*/
function injectScript (content) {
function injectScript(content) {
try {
const container = document.head || document.documentElement
const scriptTag = document.createElement('script')
@ -47,7 +50,7 @@ function injectScript (content) {
* Sets up the stream communication and submits site metadata
*
*/
async function start () {
async function start() {
await setupStreams()
await domIsReady()
}
@ -57,7 +60,7 @@ async function start () {
* browser extension and local per-page browser context.
*
*/
async function setupStreams () {
async function setupStreams() {
// the transport-specific streams for communication between inpage and background
const pageStream = new LocalMessageDuplexStream({
name: 'contentscript',
@ -73,17 +76,11 @@ async function setupStreams () {
const extensionMux = new ObjectMultiplex()
extensionMux.setMaxListeners(25)
pump(
pageMux,
pageStream,
pageMux,
(err) => logStreamDisconnectWarning('MetaMask Inpage Multiplex', err),
pump(pageMux, pageStream, pageMux, (err) =>
logStreamDisconnectWarning('MetaMask Inpage Multiplex', err),
)
pump(
extensionMux,
extensionStream,
extensionMux,
(err) => logStreamDisconnectWarning('MetaMask Background Multiplex', err),
pump(extensionMux, extensionStream, extensionMux, (err) =>
logStreamDisconnectWarning('MetaMask Background Multiplex', err),
)
// forward communication across inpage-background for these channels only
@ -95,14 +92,14 @@ async function setupStreams () {
phishingStream.once('data', redirectToPhishingWarning)
}
function forwardTrafficBetweenMuxers (channelName, muxA, muxB) {
function forwardTrafficBetweenMuxers(channelName, muxA, muxB) {
const channelA = muxA.createStream(channelName)
const channelB = muxB.createStream(channelName)
pump(
channelA,
channelB,
channelA,
(err) => logStreamDisconnectWarning(`MetaMask muxed traffic for channel "${channelName}" failed.`, err),
pump(channelA, channelB, channelA, (err) =>
logStreamDisconnectWarning(
`MetaMask muxed traffic for channel "${channelName}" failed.`,
err,
),
)
}
@ -112,7 +109,7 @@ function forwardTrafficBetweenMuxers (channelName, muxA, muxB) {
* @param {string} remoteLabel - Remote stream name
* @param {Error} err - Stream connection error
*/
function logStreamDisconnectWarning (remoteLabel, err) {
function logStreamDisconnectWarning(remoteLabel, err) {
let warningMsg = `MetamaskContentscript - lost connection to ${remoteLabel}`
if (err) {
warningMsg += `\n${err.stack}`
@ -123,19 +120,23 @@ function logStreamDisconnectWarning (remoteLabel, err) {
/**
* Determines if the provider should be injected
*
* @returns {boolean} {@code true} - if the provider should be injected
* @returns {boolean} {@code true} Whether the provider should be injected
*/
function shouldInjectProvider () {
return doctypeCheck() && suffixCheck() &&
documentElementCheck() && !blockedDomainCheck()
function shouldInjectProvider() {
return (
doctypeCheck() &&
suffixCheck() &&
documentElementCheck() &&
!blockedDomainCheck()
)
}
/**
* Checks the doctype of the current document if it exists
*
* @returns {boolean} {@code true} - if the doctype is html or if none exists
* @returns {boolean} {@code true} if the doctype is html or if none exists
*/
function doctypeCheck () {
function doctypeCheck() {
const { doctype } = window.document
if (doctype) {
return doctype.name === 'html'
@ -150,13 +151,10 @@ function doctypeCheck () {
* that we should not inject the provider into. This check is indifferent of
* query parameters in the location.
*
* @returns {boolean} - whether or not the extension of the current document is prohibited
* @returns {boolean} whether or not the extension of the current document is prohibited
*/
function suffixCheck () {
const prohibitedTypes = [
/\.xml$/u,
/\.pdf$/u,
]
function suffixCheck() {
const prohibitedTypes = [/\.xml$/u, /\.pdf$/u]
const currentUrl = window.location.pathname
for (let i = 0; i < prohibitedTypes.length; i++) {
if (prohibitedTypes[i].test(currentUrl)) {
@ -169,9 +167,9 @@ function suffixCheck () {
/**
* Checks the documentElement of the current document
*
* @returns {boolean} {@code true} - if the documentElement is an html node or if none exists
* @returns {boolean} {@code true} if the documentElement is an html node or if none exists
*/
function documentElementCheck () {
function documentElementCheck() {
const documentElement = document.documentElement.nodeName
if (documentElement) {
return documentElement.toLowerCase() === 'html'
@ -182,9 +180,9 @@ function documentElementCheck () {
/**
* Checks if the current domain is blocked
*
* @returns {boolean} {@code true} - if the current domain is blocked
* @returns {boolean} {@code true} if the current domain is blocked
*/
function blockedDomainCheck () {
function blockedDomainCheck() {
const blockedDomains = [
'uscourts.gov',
'dropbox.com',
@ -201,7 +199,10 @@ function blockedDomainCheck () {
let currentRegex
for (let i = 0; i < blockedDomains.length; i++) {
const blockedDomain = blockedDomains[i].replace('.', '\\.')
currentRegex = new RegExp(`(?:https?:\\/\\/)(?:(?!${blockedDomain}).)*$`, 'u')
currentRegex = new RegExp(
`(?:https?:\\/\\/)(?:(?!${blockedDomain}).)*$`,
'u',
)
if (!currentRegex.test(currentUrl)) {
return true
}
@ -212,7 +213,7 @@ function blockedDomainCheck () {
/**
* Redirects the current page to a phishing information page
*/
function redirectToPhishingWarning () {
function redirectToPhishingWarning() {
console.log('MetaMask - routing to Phishing Warning component')
const extensionURL = extension.runtime.getURL('phishing.html')
window.location.href = `${extensionURL}#${querystring.stringify({
@ -224,11 +225,13 @@ function redirectToPhishingWarning () {
/**
* Returns a promise that resolves when the DOM is loaded (does not wait for images to load)
*/
async function domIsReady () {
async function domIsReady() {
// already loaded
if (['interactive', 'complete'].includes(document.readyState)) {
return undefined
}
// wait for load
return new Promise((resolve) => window.addEventListener('DOMContentLoaded', resolve, { once: true }))
return new Promise((resolve) =>
window.addEventListener('DOMContentLoaded', resolve, { once: true }),
)
}

View File

@ -21,14 +21,13 @@ export const ALERT_TYPES = {
}
const defaultState = {
alertEnabledness: Object.keys(ALERT_TYPES)
.reduce(
(alertEnabledness, alertType) => {
alertEnabledness[alertType] = true
return alertEnabledness
},
{},
),
alertEnabledness: Object.keys(ALERT_TYPES).reduce(
(alertEnabledness, alertType) => {
alertEnabledness[alertType] = true
return alertEnabledness
},
{},
),
unconnectedAccountAlertShownOrigins: {},
}
@ -37,12 +36,11 @@ const defaultState = {
* alert related state
*/
export default class AlertController {
/**
* @constructor
* @param {AlertControllerOptions} [opts] - Controller configuration parameters
*/
constructor (opts = {}) {
constructor(opts = {}) {
const { initState, preferencesStore } = opts
const state = {
...defaultState,
@ -56,14 +54,17 @@ export default class AlertController {
preferencesStore.subscribe(({ selectedAddress }) => {
const currentState = this.store.getState()
if (currentState.unconnectedAccountAlertShownOrigins && this.selectedAddress !== selectedAddress) {
if (
currentState.unconnectedAccountAlertShownOrigins &&
this.selectedAddress !== selectedAddress
) {
this.selectedAddress = selectedAddress
this.store.updateState({ unconnectedAccountAlertShownOrigins: {} })
}
})
}
setAlertEnabledness (alertId, enabledness) {
setAlertEnabledness(alertId, enabledness) {
let { alertEnabledness } = this.store.getState()
alertEnabledness = { ...alertEnabledness }
alertEnabledness[alertId] = enabledness
@ -74,9 +75,11 @@ export default class AlertController {
* Sets the "switch to connected" alert as shown for the given origin
* @param {string} origin - The origin the alert has been shown for
*/
setUnconnectedAccountAlertShown (origin) {
setUnconnectedAccountAlertShown(origin) {
let { unconnectedAccountAlertShownOrigins } = this.store.getState()
unconnectedAccountAlertShownOrigins = { ...unconnectedAccountAlertShownOrigins }
unconnectedAccountAlertShownOrigins = {
...unconnectedAccountAlertShownOrigins,
}
unconnectedAccountAlertShownOrigins[origin] = true
this.store.updateState({ unconnectedAccountAlertShownOrigins })
}

View File

@ -2,12 +2,11 @@ import EventEmitter from 'events'
import ObservableStore from 'obs-store'
export default class AppStateController extends EventEmitter {
/**
* @constructor
* @param opts
* @param {Object} opts
*/
constructor (opts = {}) {
constructor(opts = {}) {
const {
addUnlockListener,
isUnlocked,
@ -23,7 +22,8 @@ export default class AppStateController extends EventEmitter {
timeoutMinutes: 0,
connectedStatusPopoverHasBeenShown: true,
swapsWelcomeMessageHasBeenShown: false,
defaultHomeActiveTabName: null, ...initState,
defaultHomeActiveTabName: null,
...initState,
})
this.timer = null
@ -53,7 +53,7 @@ export default class AppStateController extends EventEmitter {
* @returns {Promise<void>} A promise that resolves when the extension is
* unlocked, or immediately if the extension is already unlocked.
*/
getUnlockPromise (shouldShowUnlockRequest) {
getUnlockPromise(shouldShowUnlockRequest) {
return new Promise((resolve) => {
if (this.isUnlocked()) {
resolve()
@ -72,7 +72,7 @@ export default class AppStateController extends EventEmitter {
* @param {boolean} shouldShowUnlockRequest - Whether the extension notification
* popup should be opened.
*/
waitForUnlock (resolve, shouldShowUnlockRequest) {
waitForUnlock(resolve, shouldShowUnlockRequest) {
this.waitingForUnlock.push({ resolve })
this.emit('updateBadge')
if (shouldShowUnlockRequest) {
@ -83,7 +83,7 @@ export default class AppStateController extends EventEmitter {
/**
* Drains the waitingForUnlock queue, resolving all the related Promises.
*/
handleUnlock () {
handleUnlock() {
if (this.waitingForUnlock.length > 0) {
while (this.waitingForUnlock.length > 0) {
this.waitingForUnlock.shift().resolve()
@ -96,7 +96,7 @@ export default class AppStateController extends EventEmitter {
* Sets the default home tab
* @param {string} [defaultHomeActiveTabName] - the tab name
*/
setDefaultHomeActiveTabName (defaultHomeActiveTabName) {
setDefaultHomeActiveTabName(defaultHomeActiveTabName) {
this.store.updateState({
defaultHomeActiveTabName,
})
@ -105,7 +105,7 @@ export default class AppStateController extends EventEmitter {
/**
* Record that the user has seen the connected status info popover
*/
setConnectedStatusPopoverHasBeenShown () {
setConnectedStatusPopoverHasBeenShown() {
this.store.updateState({
connectedStatusPopoverHasBeenShown: true,
})
@ -114,7 +114,7 @@ export default class AppStateController extends EventEmitter {
/**
* Record that the user has seen the swap screen welcome message
*/
setSwapsWelcomeMessageHasBeenShown () {
setSwapsWelcomeMessageHasBeenShown() {
this.store.updateState({
swapsWelcomeMessageHasBeenShown: true,
})
@ -124,7 +124,7 @@ export default class AppStateController extends EventEmitter {
* Sets the last active time to the current time
* @returns {void}
*/
setLastActiveTime () {
setLastActiveTime() {
this._resetTimer()
}
@ -134,7 +134,7 @@ export default class AppStateController extends EventEmitter {
* @returns {void}
* @private
*/
_setInactiveTimeout (timeoutMinutes) {
_setInactiveTimeout(timeoutMinutes) {
this.store.updateState({
timeoutMinutes,
})
@ -151,7 +151,7 @@ export default class AppStateController extends EventEmitter {
* @returns {void}
* @private
*/
_resetTimer () {
_resetTimer() {
const { timeoutMinutes } = this.store.getState()
if (this.timer) {
@ -162,6 +162,9 @@ export default class AppStateController extends EventEmitter {
return
}
this.timer = setTimeout(() => this.onInactiveTimeout(), timeoutMinutes * 60 * 1000)
this.timer = setTimeout(
() => this.onInactiveTimeout(),
timeoutMinutes * 60 * 1000,
)
}
}

View File

@ -12,13 +12,12 @@ import ObservableStore from 'obs-store'
* a cache of account balances in local storage
*/
export default class CachedBalancesController {
/**
* Creates a new controller instance
*
* @param {CachedBalancesOptions} [opts] Controller configuration parameters
* @param {CachedBalancesOptions} [opts] - Controller configuration parameters
*/
constructor (opts = {}) {
constructor(opts = {}) {
const { accountTracker, getNetwork } = opts
this.accountTracker = accountTracker
@ -37,15 +36,18 @@ export default class CachedBalancesController {
* @param {Object} obj - The the recently updated accounts object for the current network
* @returns {Promise<void>}
*/
async updateCachedBalances ({ accounts }) {
async updateCachedBalances({ accounts }) {
const network = await this.getNetwork()
const balancesToCache = await this._generateBalancesToCache(accounts, network)
const balancesToCache = await this._generateBalancesToCache(
accounts,
network,
)
this.store.updateState({
cachedBalances: balancesToCache,
})
}
_generateBalancesToCache (newAccounts, currentNetwork) {
_generateBalancesToCache(newAccounts, currentNetwork) {
const { cachedBalances } = this.store.getState()
const currentNetworkBalancesToCache = { ...cachedBalances[currentNetwork] }
@ -68,7 +70,7 @@ export default class CachedBalancesController {
* Removes cachedBalances
*/
clearCachedBalances () {
clearCachedBalances() {
this.store.updateState({ cachedBalances: {} })
}
@ -80,7 +82,7 @@ export default class CachedBalancesController {
* @private
*
*/
_registerUpdates () {
_registerUpdates() {
const update = this.updateCachedBalances.bind(this)
this.accountTracker.store.subscribe(update)
}

View File

@ -6,20 +6,25 @@ import { MAINNET } from './network/enums'
// By default, poll every 3 minutes
const DEFAULT_INTERVAL = 180 * 1000
const SINGLE_CALL_BALANCES_ADDRESS = '0xb1f8e55c7f64d203c1400b9d8555d050f94adf39'
const SINGLE_CALL_BALANCES_ADDRESS =
'0xb1f8e55c7f64d203c1400b9d8555d050f94adf39'
/**
* A controller that polls for token exchange
* rates based on a user's current token list
*/
export default class DetectTokensController {
/**
* Creates a DetectTokensController
*
* @param {Object} [config] - Options to configure controller
*/
constructor ({ interval = DEFAULT_INTERVAL, preferences, network, keyringMemStore } = {}) {
constructor({
interval = DEFAULT_INTERVAL,
preferences,
network,
keyringMemStore,
} = {}) {
this.preferences = preferences
this.interval = interval
this.network = network
@ -29,7 +34,7 @@ export default class DetectTokensController {
/**
* For each token in eth-contract-metadata, find check selectedAddress balance.
*/
async detectNewTokens () {
async detectNewTokens() {
if (!this.isActive) {
return
}
@ -40,7 +45,10 @@ export default class DetectTokensController {
const tokensToDetect = []
this.web3.setProvider(this._network._provider)
for (const contractAddress in contracts) {
if (contracts[contractAddress].erc20 && !(this.tokenAddresses.includes(contractAddress.toLowerCase()))) {
if (
contracts[contractAddress].erc20 &&
!this.tokenAddresses.includes(contractAddress.toLowerCase())
) {
tokensToDetect.push(contractAddress)
}
}
@ -49,20 +57,29 @@ export default class DetectTokensController {
try {
result = await this._getTokenBalances(tokensToDetect)
} catch (error) {
warn(`MetaMask - DetectTokensController single call balance fetch failed`, error)
warn(
`MetaMask - DetectTokensController single call balance fetch failed`,
error,
)
return
}
tokensToDetect.forEach((tokenAddress, index) => {
const balance = result[index]
if (balance && !balance.isZero()) {
this._preferences.addToken(tokenAddress, contracts[tokenAddress].symbol, contracts[tokenAddress].decimals)
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)
async _getTokenBalances(tokens) {
const ethContract = this.web3.eth
.contract(SINGLE_CALL_BALANCES_ABI)
.at(SINGLE_CALL_BALANCES_ADDRESS)
return new Promise((resolve, reject) => {
ethContract.balances([this.selectedAddress], tokens, (error, result) => {
if (error) {
@ -78,7 +95,7 @@ export default class DetectTokensController {
* in case of address change or user session initialization.
*
*/
restartTokenDetection () {
restartTokenDetection() {
if (!(this.isActive && this.selectedAddress)) {
return
}
@ -90,7 +107,7 @@ export default class DetectTokensController {
/**
* @type {Number}
*/
set interval (interval) {
set interval(interval) {
this._handle && clearInterval(this._handle)
if (!interval) {
return
@ -104,7 +121,7 @@ export default class DetectTokensController {
* In setter when selectedAddress is changed, detectNewTokens and restart polling
* @type {Object}
*/
set preferences (preferences) {
set preferences(preferences) {
if (!preferences) {
return
}
@ -129,7 +146,7 @@ export default class DetectTokensController {
/**
* @type {Object}
*/
set network (network) {
set network(network) {
if (!network) {
return
}
@ -141,7 +158,7 @@ export default class DetectTokensController {
* In setter when isUnlocked is updated to true, detectNewTokens and restart polling
* @type {Object}
*/
set keyringMemStore (keyringMemStore) {
set keyringMemStore(keyringMemStore) {
if (!keyringMemStore) {
return
}
@ -160,7 +177,7 @@ export default class DetectTokensController {
* Internal isActive state
* @type {Object}
*/
get isActive () {
get isActive() {
return this.isOpen && this.isUnlocked
}
/* eslint-enable accessor-pairs */

View File

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

View File

@ -8,7 +8,7 @@ const ZERO_ADDRESS = '0x0000000000000000000000000000000000000000'
const ZERO_X_ERROR_ADDRESS = '0x'
export default class EnsController {
constructor ({ ens, provider, networkStore } = {}) {
constructor({ ens, provider, networkStore } = {}) {
const initState = {
ensResolutionsByAddress: {},
}
@ -38,11 +38,11 @@ export default class EnsController {
})
}
reverseResolveAddress (address) {
reverseResolveAddress(address) {
return this._reverseResolveAddress(ethUtil.toChecksumAddress(address))
}
async _reverseResolveAddress (address) {
async _reverseResolveAddress(address) {
if (!this._ens) {
return undefined
}
@ -68,7 +68,10 @@ export default class EnsController {
return undefined
}
if (registeredAddress === ZERO_ADDRESS || registeredAddress === ZERO_X_ERROR_ADDRESS) {
if (
registeredAddress === ZERO_ADDRESS ||
registeredAddress === ZERO_X_ERROR_ADDRESS
) {
return undefined
}
@ -80,7 +83,7 @@ export default class EnsController {
return domain
}
_updateResolutionsByAddress (address, domain) {
_updateResolutionsByAddress(address, domain) {
const oldState = this.store.getState()
this.store.putState({
ensResolutionsByAddress: {

View File

@ -6,30 +6,49 @@ import { bnToHex } from '../lib/util'
import fetchWithTimeout from '../lib/fetch-with-timeout'
import {
ROPSTEN,
RINKEBY,
KOVAN,
TRANSACTION_CATEGORIES,
TRANSACTION_STATUSES,
} from '../../../shared/constants/transaction'
import {
CHAIN_ID_TO_NETWORK_ID_MAP,
CHAIN_ID_TO_TYPE_MAP,
GOERLI,
GOERLI_CHAIN_ID,
KOVAN,
KOVAN_CHAIN_ID,
MAINNET,
NETWORK_TYPE_TO_ID_MAP,
MAINNET_CHAIN_ID,
RINKEBY,
RINKEBY_CHAIN_ID,
ROPSTEN,
ROPSTEN_CHAIN_ID,
} from './network/enums'
const fetch = fetchWithTimeout({
timeout: 30000,
})
export default class IncomingTransactionsController {
/**
* This controller is responsible for retrieving incoming transactions. Etherscan is polled once every block to check
* for new incoming transactions for the current selected account on the current network
*
* Note that only the built-in Infura networks are supported (i.e. anything in `INFURA_PROVIDER_TYPES`). We will not
* attempt to retrieve incoming transactions on any custom RPC endpoints.
*/
const etherscanSupportedNetworks = [
GOERLI_CHAIN_ID,
KOVAN_CHAIN_ID,
MAINNET_CHAIN_ID,
RINKEBY_CHAIN_ID,
ROPSTEN_CHAIN_ID,
]
constructor (opts = {}) {
const {
blockTracker,
networkController,
preferencesController,
} = opts
export default class IncomingTransactionsController {
constructor(opts = {}) {
const { blockTracker, networkController, preferencesController } = opts
this.blockTracker = blockTracker
this.networkController = networkController
this.preferencesController = preferencesController
this.getCurrentNetwork = () => networkController.getProviderConfig().type
this._onLatestBlock = async (newBlockNumberHex) => {
const selectedAddress = this.preferencesController.getSelectedAddress()
@ -43,54 +62,66 @@ export default class IncomingTransactionsController {
const initState = {
incomingTransactions: {},
incomingTxLastFetchedBlocksByNetwork: {
[ROPSTEN]: null,
[RINKEBY]: null,
[KOVAN]: null,
[GOERLI]: null,
[KOVAN]: null,
[MAINNET]: null,
}, ...opts.initState,
[RINKEBY]: null,
[ROPSTEN]: null,
},
...opts.initState,
}
this.store = new ObservableStore(initState)
this.preferencesController.store.subscribe(pairwise((prevState, currState) => {
const { featureFlags: { showIncomingTransactions: prevShowIncomingTransactions } = {} } = prevState
const { featureFlags: { showIncomingTransactions: currShowIncomingTransactions } = {} } = currState
this.preferencesController.store.subscribe(
pairwise((prevState, currState) => {
const {
featureFlags: {
showIncomingTransactions: prevShowIncomingTransactions,
} = {},
} = prevState
const {
featureFlags: {
showIncomingTransactions: currShowIncomingTransactions,
} = {},
} = currState
if (currShowIncomingTransactions === prevShowIncomingTransactions) {
return
}
if (currShowIncomingTransactions === prevShowIncomingTransactions) {
return
}
if (prevShowIncomingTransactions && !currShowIncomingTransactions) {
this.stop()
return
}
if (prevShowIncomingTransactions && !currShowIncomingTransactions) {
this.stop()
return
}
this.start()
}))
this.start()
}),
)
this.preferencesController.store.subscribe(pairwise(async (prevState, currState) => {
const { selectedAddress: prevSelectedAddress } = prevState
const { selectedAddress: currSelectedAddress } = currState
this.preferencesController.store.subscribe(
pairwise(async (prevState, currState) => {
const { selectedAddress: prevSelectedAddress } = prevState
const { selectedAddress: currSelectedAddress } = currState
if (currSelectedAddress === prevSelectedAddress) {
return
}
if (currSelectedAddress === prevSelectedAddress) {
return
}
await this._update({
address: currSelectedAddress,
})
}))
await this._update({
address: currSelectedAddress,
})
}),
)
this.networkController.on('networkDidChange', async (newType) => {
this.networkController.on('networkDidChange', async () => {
const address = this.preferencesController.getSelectedAddress()
await this._update({
address,
networkType: newType,
})
})
}
start () {
start() {
const { featureFlags = {} } = this.preferencesController.store.getState()
const { showIncomingTransactions } = featureFlags
@ -102,33 +133,45 @@ export default class IncomingTransactionsController {
this.blockTracker.addListener('latest', this._onLatestBlock)
}
stop () {
stop() {
this.blockTracker.removeListener('latest', this._onLatestBlock)
}
async _update ({ address, newBlockNumberDec, networkType } = {}) {
async _update({ address, newBlockNumberDec } = {}) {
const chainId = this.networkController.getCurrentChainId()
if (!etherscanSupportedNetworks.includes(chainId)) {
return
}
try {
const dataForUpdate = await this._getDataForUpdate({ address, newBlockNumberDec, networkType })
await this._updateStateWithNewTxData(dataForUpdate)
const dataForUpdate = await this._getDataForUpdate({
address,
chainId,
newBlockNumberDec,
})
this._updateStateWithNewTxData(dataForUpdate)
} catch (err) {
log.error(err)
}
}
async _getDataForUpdate ({ address, newBlockNumberDec, networkType } = {}) {
async _getDataForUpdate({ address, chainId, newBlockNumberDec } = {}) {
const {
incomingTransactions: currentIncomingTxs,
incomingTxLastFetchedBlocksByNetwork: currentBlocksByNetwork,
} = this.store.getState()
const network = networkType || this.getCurrentNetwork()
const lastFetchBlockByCurrentNetwork = currentBlocksByNetwork[network]
const lastFetchBlockByCurrentNetwork =
currentBlocksByNetwork[CHAIN_ID_TO_TYPE_MAP[chainId]]
let blockToFetchFrom = lastFetchBlockByCurrentNetwork || newBlockNumberDec
if (blockToFetchFrom === undefined) {
blockToFetchFrom = parseInt(this.blockTracker.getCurrentBlock(), 16)
}
const { latestIncomingTxBlockNumber, txs: newTxs } = await this._fetchAll(address, blockToFetchFrom, network)
const { latestIncomingTxBlockNumber, txs: newTxs } = await this._fetchAll(
address,
blockToFetchFrom,
chainId,
)
return {
latestIncomingTxBlockNumber,
@ -136,17 +179,17 @@ export default class IncomingTransactionsController {
currentIncomingTxs,
currentBlocksByNetwork,
fetchedBlockNumber: blockToFetchFrom,
network,
chainId,
}
}
async _updateStateWithNewTxData ({
_updateStateWithNewTxData({
latestIncomingTxBlockNumber,
newTxs,
currentIncomingTxs,
currentBlocksByNetwork,
fetchedBlockNumber,
network,
chainId,
}) {
const newLatestBlockHashByNetwork = latestIncomingTxBlockNumber
? parseInt(latestIncomingTxBlockNumber, 10) + 1
@ -161,28 +204,23 @@ export default class IncomingTransactionsController {
this.store.updateState({
incomingTxLastFetchedBlocksByNetwork: {
...currentBlocksByNetwork,
[network]: newLatestBlockHashByNetwork,
[CHAIN_ID_TO_TYPE_MAP[chainId]]: newLatestBlockHashByNetwork,
},
incomingTransactions: newIncomingTransactions,
})
}
async _fetchAll (address, fromBlock, networkType) {
const fetchedTxResponse = await this._fetchTxs(address, fromBlock, networkType)
async _fetchAll(address, fromBlock, chainId) {
const fetchedTxResponse = await this._fetchTxs(address, fromBlock, chainId)
return this._processTxFetchResponse(fetchedTxResponse)
}
async _fetchTxs (address, fromBlock, networkType) {
let etherscanSubdomain = 'api'
const currentNetworkID = NETWORK_TYPE_TO_ID_MAP[networkType]?.networkId
async _fetchTxs(address, fromBlock, chainId) {
const etherscanSubdomain =
chainId === MAINNET_CHAIN_ID
? 'api'
: `api-${CHAIN_ID_TO_TYPE_MAP[chainId]}`
if (!currentNetworkID) {
return {}
}
if (networkType !== MAINNET) {
etherscanSubdomain = `api-${networkType}`
}
const apiUrl = `https://${etherscanSubdomain}.etherscan.io`
let url = `${apiUrl}/api?module=account&action=txlist&address=${address}&tag=latest&page=1`
@ -195,22 +233,26 @@ export default class IncomingTransactionsController {
return {
...parsedResponse,
address,
currentNetworkID,
chainId,
}
}
_processTxFetchResponse ({ status, result = [], address, currentNetworkID }) {
_processTxFetchResponse({ status, result = [], address, chainId }) {
if (status === '1' && Array.isArray(result) && result.length > 0) {
const remoteTxList = {}
const remoteTxs = []
result.forEach((tx) => {
if (!remoteTxList[tx.hash]) {
remoteTxs.push(this._normalizeTxFromEtherscan(tx, currentNetworkID))
remoteTxs.push(this._normalizeTxFromEtherscan(tx, chainId))
remoteTxList[tx.hash] = 1
}
})
const incomingTxs = remoteTxs.filter((tx) => tx.txParams.to && tx.txParams.to.toLowerCase() === address.toLowerCase())
const incomingTxs = remoteTxs.filter(
(tx) =>
tx.txParams.to &&
tx.txParams.to.toLowerCase() === address.toLowerCase(),
)
incomingTxs.sort((a, b) => (a.time < b.time ? -1 : 1))
let latestIncomingTxBlockNumber = null
@ -218,7 +260,8 @@ export default class IncomingTransactionsController {
if (
tx.blockNumber &&
(!latestIncomingTxBlockNumber ||
parseInt(latestIncomingTxBlockNumber, 10) < parseInt(tx.blockNumber, 10))
parseInt(latestIncomingTxBlockNumber, 10) <
parseInt(tx.blockNumber, 10))
) {
latestIncomingTxBlockNumber = tx.blockNumber
}
@ -234,13 +277,16 @@ export default class IncomingTransactionsController {
}
}
_normalizeTxFromEtherscan (txMeta, currentNetworkID) {
_normalizeTxFromEtherscan(txMeta, chainId) {
const time = parseInt(txMeta.timeStamp, 10) * 1000
const status = txMeta.isError === '0' ? 'confirmed' : 'failed'
const status =
txMeta.isError === '0'
? TRANSACTION_STATUSES.CONFIRMED
: TRANSACTION_STATUSES.FAILED
return {
blockNumber: txMeta.blockNumber,
id: createId(),
metamaskNetworkId: currentNetworkID,
metamaskNetworkId: CHAIN_ID_TO_NETWORK_ID_MAP[chainId],
status,
time,
txParams: {
@ -252,12 +298,12 @@ export default class IncomingTransactionsController {
value: bnToHex(new BN(txMeta.value)),
},
hash: txMeta.hash,
transactionCategory: 'incoming',
transactionCategory: TRANSACTION_CATEGORIES.INCOMING,
}
}
}
function pairwise (fn) {
function pairwise(fn) {
let first = true
let cache
return (value) => {

View File

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

View File

@ -10,7 +10,7 @@ import createInfuraMiddleware from 'eth-json-rpc-infura'
import BlockTracker from 'eth-block-tracker'
import * as networkEnums from './enums'
export default function createInfuraClient ({ network, projectId }) {
export default function createInfuraClient({ network, projectId }) {
const infuraMiddleware = createInfuraMiddleware({
network,
projectId,
@ -32,7 +32,7 @@ export default function createInfuraClient ({ network, projectId }) {
return { networkMiddleware, blockTracker }
}
function createNetworkAndChainIdMiddleware ({ network }) {
function createNetworkAndChainIdMiddleware({ network }) {
let chainId
let netId

View File

@ -9,16 +9,12 @@ import providerFromMiddleware from 'eth-json-rpc-middleware/providerFromMiddlewa
import BlockTracker from 'eth-block-tracker'
const inTest = process.env.IN_TEST === 'true'
const blockTrackerOpts = inTest
? { pollingInterval: 1000 }
: {}
const blockTrackerOpts = inTest ? { pollingInterval: 1000 } : {}
const getTestMiddlewares = () => {
return inTest
? [createEstimateGasDelayTestMiddleware()]
: []
return inTest ? [createEstimateGasDelayTestMiddleware()] : []
}
export default function createJsonRpcClient ({ rpcUrl, chainId }) {
export default function createJsonRpcClient({ rpcUrl, chainId }) {
const fetchMiddleware = createFetchMiddleware({ rpcUrl })
const blockProvider = providerFromMiddleware(fetchMiddleware)
const blockTracker = new BlockTracker({
@ -39,7 +35,7 @@ export default function createJsonRpcClient ({ rpcUrl, chainId }) {
return { networkMiddleware, blockTracker }
}
function createChainIdMiddleware (chainId) {
function createChainIdMiddleware(chainId) {
return (req, res, next, end) => {
if (req.method === 'eth_chainId') {
res.result = chainId
@ -53,7 +49,7 @@ function createChainIdMiddleware (chainId) {
* For use in tests only.
* Adds a delay to `eth_estimateGas` calls.
*/
function createEstimateGasDelayTestMiddleware () {
function createEstimateGasDelayTestMiddleware() {
return createAsyncMiddleware(async (req, _, next) => {
if (req.method === 'eth_estimateGas') {
await new Promise((resolve) => setTimeout(resolve, 2000))

View File

@ -1,9 +1,12 @@
import mergeMiddleware from 'json-rpc-engine/src/mergeMiddleware'
import createScaffoldMiddleware from 'json-rpc-engine/src/createScaffoldMiddleware'
import createWalletSubprovider from 'eth-json-rpc-middleware/wallet'
import { createPendingNonceMiddleware, createPendingTxMiddleware } from './middleware/pending'
import {
createPendingNonceMiddleware,
createPendingTxMiddleware,
} from './middleware/pending'
export default function createMetamaskMiddleware ({
export default function createMetamaskMiddleware({
version,
getAccounts,
processTransaction,

View File

@ -22,13 +22,7 @@ export const KOVAN_DISPLAY_NAME = 'Kovan'
export const MAINNET_DISPLAY_NAME = 'Ethereum Mainnet'
export const GOERLI_DISPLAY_NAME = 'Goerli'
export const INFURA_PROVIDER_TYPES = [
ROPSTEN,
RINKEBY,
KOVAN,
MAINNET,
GOERLI,
]
export const INFURA_PROVIDER_TYPES = [ROPSTEN, RINKEBY, KOVAN, MAINNET, GOERLI]
export const NETWORK_TYPE_TO_ID_MAP = {
[ROPSTEN]: { networkId: ROPSTEN_NETWORK_ID, chainId: ROPSTEN_CHAIN_ID },
@ -57,3 +51,17 @@ export const NETWORK_TO_NAME_MAP = {
[GOERLI_CHAIN_ID]: GOERLI_DISPLAY_NAME,
[MAINNET_CHAIN_ID]: MAINNET_DISPLAY_NAME,
}
export const CHAIN_ID_TO_TYPE_MAP = Object.entries(
NETWORK_TYPE_TO_ID_MAP,
).reduce((chainIdToTypeMap, [networkType, { chainId }]) => {
chainIdToTypeMap[chainId] = networkType
return chainIdToTypeMap
}, {})
export const CHAIN_ID_TO_NETWORK_ID_MAP = Object.values(
NETWORK_TYPE_TO_ID_MAP,
).reduce((chainIdToNetworkIdMap, { chainId, networkId }) => {
chainIdToNetworkIdMap[chainId] = networkId
return chainIdToNetworkIdMap
}, {})

View File

@ -1,7 +1,7 @@
import createAsyncMiddleware from 'json-rpc-engine/src/createAsyncMiddleware'
import { formatTxMetaForRpcResult } from '../util'
export function createPendingNonceMiddleware ({ getPendingNonce }) {
export function createPendingNonceMiddleware({ getPendingNonce }) {
return createAsyncMiddleware(async (req, res, next) => {
const { method, params } = req
if (method !== 'eth_getTransactionCount') {
@ -17,7 +17,7 @@ export function createPendingNonceMiddleware ({ getPendingNonce }) {
})
}
export function createPendingTxMiddleware ({ getPendingTransactionByHash }) {
export function createPendingTxMiddleware({ getPendingTransactionByHash }) {
return createAsyncMiddleware(async (req, res, next) => {
const { method, params } = req
if (method !== 'eth_getTransactionByHash') {

View File

@ -5,7 +5,10 @@ import ComposedStore from 'obs-store/lib/composed'
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 {
createSwappableProxy,
createEventEmitterProxy,
} from 'swappable-obj-proxy'
import EthQuery from 'eth-query'
import createMetamaskMiddleware from './createMetamaskMiddleware'
import createInfuraClient from './createInfuraClient'
@ -40,8 +43,7 @@ const defaultProviderConfig = {
}
export default class NetworkController extends EventEmitter {
constructor (opts = {}) {
constructor(opts = {}) {
super()
// create stores
@ -72,7 +74,7 @@ export default class NetworkController extends EventEmitter {
* @throws {Error} if the project ID is not a valid string
* @return {void}
*/
setInfuraProjectId (projectId) {
setInfuraProjectId(projectId) {
if (!projectId || typeof projectId !== 'string') {
throw new Error('Invalid Infura project ID')
}
@ -80,7 +82,7 @@ export default class NetworkController extends EventEmitter {
this._infuraProjectId = projectId
}
initializeProvider (providerParams) {
initializeProvider(providerParams) {
this._baseProviderParams = providerParams
const { type, rpcUrl, chainId } = this.getProviderConfig()
this._configureProvider({ type, rpcUrl, chainId })
@ -88,41 +90,45 @@ export default class NetworkController extends EventEmitter {
}
// return the proxies so the references will always be good
getProviderAndBlockTracker () {
getProviderAndBlockTracker() {
const provider = this._providerProxy
const blockTracker = this._blockTrackerProxy
return { provider, blockTracker }
}
verifyNetwork () {
verifyNetwork() {
// Check network when restoring connectivity:
if (this.isNetworkLoading()) {
this.lookupNetwork()
}
}
getNetworkState () {
getNetworkState() {
return this.networkStore.getState()
}
setNetworkState (network) {
setNetworkState(network) {
this.networkStore.putState(network)
}
isNetworkLoading () {
isNetworkLoading() {
return this.getNetworkState() === 'loading'
}
lookupNetwork () {
lookupNetwork() {
// Prevent firing when provider is not defined.
if (!this._provider) {
log.warn('NetworkController - lookupNetwork aborted due to missing provider')
log.warn(
'NetworkController - lookupNetwork aborted due to missing provider',
)
return
}
const chainId = this.getCurrentChainId()
if (!chainId) {
log.warn('NetworkController - lookupNetwork aborted due to missing chainId')
log.warn(
'NetworkController - lookupNetwork aborted due to missing chainId',
)
this.setNetworkState('loading')
return
}
@ -143,12 +149,12 @@ export default class NetworkController extends EventEmitter {
})
}
getCurrentChainId () {
getCurrentChainId() {
const { type, chainId: configChainId } = this.getProviderConfig()
return NETWORK_TYPE_TO_ID_MAP[type]?.chainId || configChainId
}
setRpcTarget (rpcUrl, chainId, ticker = 'ETH', nickname = '', rpcPrefs) {
setRpcTarget(rpcUrl, chainId, ticker = 'ETH', nickname = '', rpcPrefs) {
this.setProviderConfig({
type: 'rpc',
rpcUrl,
@ -159,26 +165,33 @@ export default class NetworkController extends EventEmitter {
})
}
async setProviderType (type, rpcUrl = '', ticker = 'ETH', nickname = '') {
assert.notEqual(type, 'rpc', `NetworkController - cannot call "setProviderType" with type 'rpc'. use "setRpcTarget"`)
assert(INFURA_PROVIDER_TYPES.includes(type), `NetworkController - Unknown rpc type "${type}"`)
async setProviderType(type, rpcUrl = '', ticker = 'ETH', nickname = '') {
assert.notEqual(
type,
'rpc',
`NetworkController - cannot call "setProviderType" with type 'rpc'. use "setRpcTarget"`,
)
assert(
INFURA_PROVIDER_TYPES.includes(type),
`NetworkController - Unknown rpc type "${type}"`,
)
const { chainId } = NETWORK_TYPE_TO_ID_MAP[type]
this.setProviderConfig({ type, rpcUrl, chainId, ticker, nickname })
}
resetConnection () {
resetConnection() {
this.setProviderConfig(this.getProviderConfig())
}
/**
* Sets the provider config and switches the network.
*/
setProviderConfig (config) {
setProviderConfig(config) {
this.providerStore.updateState(config)
this._switchNetwork(config)
}
getProviderConfig () {
getProviderConfig() {
return this.providerStore.getState()
}
@ -186,26 +199,28 @@ export default class NetworkController extends EventEmitter {
// Private
//
_switchNetwork (opts) {
_switchNetwork(opts) {
this.setNetworkState('loading')
this._configureProvider(opts)
this.emit('networkDidChange', opts.type)
}
_configureProvider ({ type, rpcUrl, chainId }) {
_configureProvider({ type, rpcUrl, chainId }) {
// infura type-based endpoints
const isInfura = INFURA_PROVIDER_TYPES.includes(type)
if (isInfura) {
this._configureInfuraProvider(type, this._infuraProjectId)
// url-based rpc endpoints
// url-based rpc endpoints
} else if (type === 'rpc') {
this._configureStandardProvider(rpcUrl, chainId)
} else {
throw new Error(`NetworkController - _configureProvider - unknown type "${type}"`)
throw new Error(
`NetworkController - _configureProvider - unknown type "${type}"`,
)
}
}
_configureInfuraProvider (type, projectId) {
_configureInfuraProvider(type, projectId) {
log.info('NetworkController - configureInfuraProvider', type)
const networkClient = createInfuraClient({
network: type,
@ -214,14 +229,16 @@ export default class NetworkController extends EventEmitter {
this._setNetworkClient(networkClient)
}
_configureStandardProvider (rpcUrl, chainId) {
_configureStandardProvider(rpcUrl, chainId) {
log.info('NetworkController - configureStandardProvider', rpcUrl)
const networkClient = createJsonRpcClient({ rpcUrl, chainId })
this._setNetworkClient(networkClient)
}
_setNetworkClient ({ networkMiddleware, blockTracker }) {
const metamaskMiddleware = createMetamaskMiddleware(this._baseProviderParams)
_setNetworkClient({ networkMiddleware, blockTracker }) {
const metamaskMiddleware = createMetamaskMiddleware(
this._baseProviderParams,
)
const engine = new JsonRpcEngine()
engine.push(metamaskMiddleware)
engine.push(networkMiddleware)
@ -229,7 +246,7 @@ export default class NetworkController extends EventEmitter {
this._setProviderAndBlockTracker({ provider, blockTracker })
}
_setProviderAndBlockTracker ({ provider, blockTracker }) {
_setProviderAndBlockTracker({ provider, blockTracker }) {
// update or intialize proxies
if (this._providerProxy) {
this._providerProxy.setTarget(provider)
@ -239,7 +256,9 @@ export default class NetworkController extends EventEmitter {
if (this._blockTrackerProxy) {
this._blockTrackerProxy.setTarget(blockTracker)
} else {
this._blockTrackerProxy = createEventEmitterProxy(blockTracker, { eventFilter: 'skipInternal' })
this._blockTrackerProxy = createEventEmitterProxy(blockTracker, {
eventFilter: 'skipInternal',
})
}
// set new provider and blockTracker
this._provider = provider

View File

@ -2,21 +2,23 @@ import { NETWORK_TO_NAME_MAP } from './enums'
export const getNetworkDisplayName = (key) => NETWORK_TO_NAME_MAP[key]
export function formatTxMetaForRpcResult (txMeta) {
export function formatTxMetaForRpcResult(txMeta) {
return {
'blockHash': txMeta.txReceipt ? txMeta.txReceipt.blockHash : null,
'blockNumber': txMeta.txReceipt ? txMeta.txReceipt.blockNumber : null,
'from': txMeta.txParams.from,
'gas': txMeta.txParams.gas,
'gasPrice': txMeta.txParams.gasPrice,
'hash': txMeta.hash,
'input': txMeta.txParams.data || '0x',
'nonce': txMeta.txParams.nonce,
'to': txMeta.txParams.to,
'transactionIndex': txMeta.txReceipt ? txMeta.txReceipt.transactionIndex : null,
'value': txMeta.txParams.value || '0x0',
'v': txMeta.v,
'r': txMeta.r,
's': txMeta.s,
blockHash: txMeta.txReceipt ? txMeta.txReceipt.blockHash : null,
blockNumber: txMeta.txReceipt ? txMeta.txReceipt.blockNumber : null,
from: txMeta.txParams.from,
gas: txMeta.txParams.gas,
gasPrice: txMeta.txParams.gasPrice,
hash: txMeta.hash,
input: txMeta.txParams.data || '0x',
nonce: txMeta.txParams.nonce,
to: txMeta.txParams.to,
transactionIndex: txMeta.txReceipt
? txMeta.txReceipt.transactionIndex
: null,
value: txMeta.txParams.value || '0x0',
v: txMeta.v,
r: txMeta.r,
s: txMeta.s,
}
}

View File

@ -17,18 +17,17 @@ import log from 'loglevel'
* state related to onboarding
*/
export default class OnboardingController {
/**
* Creates a new controller instance
*
* @param {OnboardingOptions} [opts] Controller configuration parameters
*/
constructor (opts = {}) {
constructor(opts = {}) {
const initialTransientState = {
onboardingTabs: {},
}
const initState = {
seedPhraseBackedUp: true,
seedPhraseBackedUp: null,
...opts.initState,
...initialTransientState,
}
@ -46,7 +45,7 @@ export default class OnboardingController {
})
}
setSeedPhraseBackedUp (newSeedPhraseBackUpState) {
setSeedPhraseBackedUp(newSeedPhraseBackUpState) {
this.store.updateState({
seedPhraseBackedUp: newSeedPhraseBackUpState,
})
@ -65,7 +64,9 @@ export default class OnboardingController {
}
const onboardingTabs = { ...this.store.getState().onboardingTabs }
if (!onboardingTabs[location] || onboardingTabs[location] !== tabId) {
log.debug(`Registering onboarding tab at location '${location}' with tabId '${tabId}'`)
log.debug(
`Registering onboarding tab at location '${location}' with tabId '${tabId}'`,
)
onboardingTabs[location] = tabId
this.store.updateState({ onboardingTabs })
}

View File

@ -1,4 +1,3 @@
export const WALLET_PREFIX = 'wallet_'
export const HISTORY_STORE_KEY = 'permissionsHistory'
@ -23,9 +22,7 @@ export const NOTIFICATION_NAMES = {
accountsChanged: 'wallet_accountsChanged',
}
export const LOG_IGNORE_METHODS = [
'wallet_sendDomainMetadata',
]
export const LOG_IGNORE_METHODS = ['wallet_sendDomainMetadata']
export const LOG_METHOD_TYPES = {
restricted: 'restricted',

View File

@ -24,8 +24,7 @@ import {
} from './enums'
export class PermissionsController {
constructor (
constructor(
{
getKeyringAccounts,
getRestrictedMethods,
@ -38,7 +37,6 @@ export class PermissionsController {
restoredPermissions = {},
restoredState = {},
) {
// additional top-level store key set in _initializeMetadataStore
this.store = new ObservableStore({
[LOG_STORE_KEY]: restoredState[LOG_STORE_KEY] || [],
@ -75,8 +73,7 @@ export class PermissionsController {
})
}
createMiddleware ({ origin, extensionId }) {
createMiddleware({ origin, extensionId }) {
if (typeof origin !== 'string' || !origin.length) {
throw new Error('Must provide non-empty string origin.')
}
@ -91,20 +88,26 @@ export class PermissionsController {
engine.push(this.permissionsLog.createMiddleware())
engine.push(createPermissionsMethodMiddleware({
addDomainMetadata: this.addDomainMetadata.bind(this),
getAccounts: this.getAccounts.bind(this, origin),
getUnlockPromise: () => this._getUnlockPromise(true),
hasPermission: this.hasPermission.bind(this, origin),
notifyAccountsChanged: this.notifyAccountsChanged.bind(this, origin),
requestAccountsPermission: this._requestPermissions.bind(
this, { origin }, { eth_accounts: {} },
),
}))
engine.push(
createPermissionsMethodMiddleware({
addDomainMetadata: this.addDomainMetadata.bind(this),
getAccounts: this.getAccounts.bind(this, origin),
getUnlockPromise: () => this._getUnlockPromise(true),
hasPermission: this.hasPermission.bind(this, origin),
notifyAccountsChanged: this.notifyAccountsChanged.bind(this, origin),
requestAccountsPermission: this._requestPermissions.bind(
this,
{ origin },
{ eth_accounts: {} },
),
}),
)
engine.push(this.permissions.providerMiddlewareFunction.bind(
this.permissions, { origin },
))
engine.push(
this.permissions.providerMiddlewareFunction.bind(this.permissions, {
origin,
}),
)
return asMiddleware(engine)
}
@ -114,7 +117,7 @@ export class PermissionsController {
* @param {string} origin - The requesting origin
* @returns {Promise<string>} The permissions request ID
*/
async requestAccountsPermissionWithId (origin) {
async requestAccountsPermissionWithId(origin) {
const id = nanoid()
this._requestPermissions({ origin }, { eth_accounts: {} }, id)
return id
@ -127,16 +130,19 @@ export class PermissionsController {
*
* @param {string} origin - The origin string.
*/
getAccounts (origin) {
getAccounts(origin) {
return new Promise((resolve, _) => {
const req = { method: 'eth_accounts' }
const res = {}
this.permissions.providerMiddlewareFunction(
{ origin }, req, res, () => undefined, _end,
{ origin },
req,
res,
() => undefined,
_end,
)
function _end () {
function _end() {
if (res.error || !Array.isArray(res.result)) {
resolve([])
} else {
@ -153,7 +159,7 @@ export class PermissionsController {
* @param {string} permission - The permission to check for.
* @returns {boolean} Whether the origin has the permission.
*/
hasPermission (origin, permission) {
hasPermission(origin, permission) {
return Boolean(this.permissions.getPermission(origin, permission))
}
@ -162,7 +168,7 @@ export class PermissionsController {
*
* @returns {Object} identities
*/
_getIdentities () {
_getIdentities() {
return this.preferences.getState().identities
}
@ -175,9 +181,8 @@ export class PermissionsController {
* @returns {Promise<IOcapLdCapability[]>} A Promise that resolves with the
* approved permissions, or rejects with an error.
*/
_requestPermissions (domain, permissions, id) {
_requestPermissions(domain, permissions, id) {
return new Promise((resolve, reject) => {
// rpc-cap assigns an id to the request if there is none, as expected by
// requestUserApproval below
const req = {
@ -188,10 +193,14 @@ export class PermissionsController {
const res = {}
this.permissions.providerMiddlewareFunction(
domain, req, res, () => undefined, _end,
domain,
req,
res,
() => undefined,
_end,
)
function _end (_err) {
function _end(_err) {
const err = _err || res.error
if (err) {
reject(err)
@ -211,8 +220,7 @@ export class PermissionsController {
* @param {Object} approved - The request object approved by the user
* @param {Array} accounts - The accounts to expose, if any
*/
async approvePermissionsRequest (approved, accounts) {
async approvePermissionsRequest(approved, accounts) {
const { id } = approved.metadata
const approval = this.pendingApprovals.get(id)
@ -222,28 +230,29 @@ export class PermissionsController {
}
try {
if (Object.keys(approved.permissions).length === 0) {
approval.reject(ethErrors.rpc.invalidRequest({
message: 'Must request at least one permission.',
}))
approval.reject(
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,
approved.permissions,
accounts,
)
approval.resolve(approved.permissions)
}
} catch (err) {
// if finalization fails, reject the request
approval.reject(ethErrors.rpc.invalidRequest({
message: err.message, data: err,
}))
approval.reject(
ethErrors.rpc.invalidRequest({
message: err.message,
data: err,
}),
)
}
this._removePendingApproval(id)
@ -256,7 +265,7 @@ export class PermissionsController {
*
* @param {string} id - The id of the request rejected by the user
*/
async rejectPermissionsRequest (id) {
async rejectPermissionsRequest(id) {
const approval = this.pendingApprovals.get(id)
if (!approval) {
@ -277,8 +286,7 @@ export class PermissionsController {
* @param {string} origin - The origin to expose the account to.
* @param {string} account - The new account to expose.
*/
async addPermittedAccount (origin, account) {
async addPermittedAccount(origin, account) {
const domains = this.permissions.getDomains()
if (!domains[origin]) {
throw new Error('Unrecognized domain')
@ -294,7 +302,8 @@ export class PermissionsController {
}
this.permissions.updateCaveatFor(
origin, 'eth_accounts',
origin,
'eth_accounts',
CAVEAT_NAMES.exposedAccounts,
[...oldPermittedAccounts, account],
)
@ -315,8 +324,7 @@ export class PermissionsController {
* @param {string} origin - The origin to remove the account from.
* @param {string} account - The account to remove.
*/
async removePermittedAccount (origin, account) {
async removePermittedAccount(origin, account) {
const domains = this.permissions.getDomains()
if (!domains[origin]) {
throw new Error('Unrecognized domain')
@ -331,15 +339,16 @@ export class PermissionsController {
throw new Error('Account is not permitted for origin')
}
let newPermittedAccounts = oldPermittedAccounts
.filter((acc) => acc !== account)
let newPermittedAccounts = oldPermittedAccounts.filter(
(acc) => acc !== account,
)
if (newPermittedAccounts.length === 0) {
this.removePermissionsFor({ [origin]: ['eth_accounts'] })
} else {
this.permissions.updateCaveatFor(
origin, 'eth_accounts',
origin,
'eth_accounts',
CAVEAT_NAMES.exposedAccounts,
newPermittedAccounts,
)
@ -358,14 +367,19 @@ export class PermissionsController {
*
* @param {string} account - The account to remove.
*/
async removeAllAccountPermissions (account) {
async removeAllAccountPermissions(account) {
this.validatePermittedAccounts([account])
const domains = this.permissions.getDomains()
const connectedOrigins = Object.keys(domains)
.filter((origin) => this._getPermittedAccounts(origin).includes(account))
const connectedOrigins = Object.keys(domains).filter((origin) =>
this._getPermittedAccounts(origin).includes(account),
)
await Promise.all(connectedOrigins.map((origin) => this.removePermittedAccount(origin, account)))
await Promise.all(
connectedOrigins.map((origin) =>
this.removePermittedAccount(origin, account),
),
)
}
/**
@ -378,15 +392,13 @@ export class PermissionsController {
* @param {string[]} requestedAccounts - The accounts to expose, if any.
* @returns {Object} The finalized permissions request object.
*/
async finalizePermissionsRequest (requestedPermissions, requestedAccounts) {
async finalizePermissionsRequest(requestedPermissions, requestedAccounts) {
const finalizedPermissions = cloneDeep(requestedPermissions)
const finalizedAccounts = cloneDeep(requestedAccounts)
const { eth_accounts: ethAccounts } = finalizedPermissions
if (ethAccounts) {
this.validatePermittedAccounts(finalizedAccounts)
if (!ethAccounts.caveats) {
@ -394,9 +406,11 @@ export class PermissionsController {
}
// caveat names are unique, and we will only construct this caveat here
ethAccounts.caveats = ethAccounts.caveats.filter((c) => (
c.name !== CAVEAT_NAMES.exposedAccounts && c.name !== CAVEAT_NAMES.primaryAccountOnly
))
ethAccounts.caveats = ethAccounts.caveats.filter(
(c) =>
c.name !== CAVEAT_NAMES.exposedAccounts &&
c.name !== CAVEAT_NAMES.primaryAccountOnly,
)
ethAccounts.caveats.push({
type: CAVEAT_TYPES.limitResponseLength,
@ -420,7 +434,7 @@ export class PermissionsController {
*
* @param {string[]} accounts - An array of addresses.
*/
validatePermittedAccounts (accounts) {
validatePermittedAccounts(accounts) {
if (!Array.isArray(accounts) || accounts.length === 0) {
throw new Error('Must provide non-empty array of account(s).')
}
@ -441,8 +455,7 @@ export class PermissionsController {
* @param {string} origin - The origin of the domain to notify.
* @param {Array<string>} newAccounts - The currently permitted accounts.
*/
notifyAccountsChanged (origin, newAccounts) {
notifyAccountsChanged(origin, newAccounts) {
if (typeof origin !== 'string' || !origin) {
throw new Error(`Invalid origin: '${origin}'`)
}
@ -459,9 +472,7 @@ export class PermissionsController {
// if the accounts changed from the perspective of the dapp,
// update "last seen" time for the origin and account(s)
// exception: no accounts -> no times to update
this.permissionsLog.updateAccountsHistory(
origin, newAccounts,
)
this.permissionsLog.updateAccountsHistory(origin, newAccounts)
// NOTE:
// we don't check for accounts changing in the notifyAllDomains case,
@ -475,17 +486,14 @@ export class PermissionsController {
* Should only be called after confirming that the permissions exist, to
* avoid sending unnecessary notifications.
*
* @param {Object} domains { origin: [permissions] } - The map of domain
* origins to permissions to remove.
* @param {Object} domains - The map of domain origins to permissions to remove.
* e.g. { origin: [permissions] }
*/
removePermissionsFor (domains) {
removePermissionsFor(domains) {
Object.entries(domains).forEach(([origin, perms]) => {
this.permissions.removePermissionsFor(
origin,
perms.map((methodName) => {
if (methodName === 'eth_accounts') {
this.notifyAccountsChanged(origin, [])
}
@ -499,7 +507,7 @@ export class PermissionsController {
/**
* Removes all known domains and their related permissions.
*/
clearPermissions () {
clearPermissions() {
this.permissions.clearDomains()
this._notifyAllDomains({
method: NOTIFICATION_NAMES.accountsChanged,
@ -517,8 +525,7 @@ export class PermissionsController {
* @param {string} origin - The origin whose domain metadata to store.
* @param {Object} metadata - The domain's metadata that will be stored.
*/
addDomainMetadata (origin, metadata) {
addDomainMetadata(origin, metadata) {
const oldMetadataState = this.store.getState()[METADATA_STORE_KEY]
const newMetadataState = { ...oldMetadataState }
@ -541,7 +548,10 @@ export class PermissionsController {
lastUpdated: Date.now(),
}
if (!newMetadataState[origin].extensionId && !newMetadataState[origin].host) {
if (
!newMetadataState[origin].extensionId &&
!newMetadataState[origin].host
) {
newMetadataState[origin].host = new URL(origin).host
}
@ -557,8 +567,7 @@ export class PermissionsController {
*
* @param {Object} restoredState - The restored permissions controller state.
*/
_initializeMetadataStore (restoredState) {
_initializeMetadataStore(restoredState) {
const metadataState = restoredState[METADATA_STORE_KEY] || {}
const newMetadataState = this._trimDomainMetadata(metadataState)
@ -574,8 +583,7 @@ export class PermissionsController {
* @param {Object} metadataState - The metadata store state object to trim.
* @returns {Object} The new metadata state object.
*/
_trimDomainMetadata (metadataState) {
_trimDomainMetadata(metadataState) {
const newMetadataState = { ...metadataState }
const origins = Object.keys(metadataState)
const permissionsDomains = this.permissions.getDomains()
@ -593,7 +601,7 @@ export class PermissionsController {
* Replaces the existing domain metadata with the passed-in object.
* @param {Object} newMetadataState - The new metadata to set.
*/
_setDomainMetadata (newMetadataState) {
_setDomainMetadata(newMetadataState) {
this.store.updateState({ [METADATA_STORE_KEY]: newMetadataState })
}
@ -603,11 +611,10 @@ export class PermissionsController {
* @param {string} origin - The origin to obtain permitted accounts for
* @returns {Array<string>|null} The list of permitted accounts
*/
_getPermittedAccounts (origin) {
_getPermittedAccounts(origin) {
const permittedAccounts = this.permissions
.getPermission(origin, 'eth_accounts')
?.caveats
?.find((caveat) => caveat.name === CAVEAT_NAMES.exposedAccounts)
?.caveats?.find((caveat) => caveat.name === CAVEAT_NAMES.exposedAccounts)
?.value
return permittedAccounts || null
@ -622,8 +629,7 @@ export class PermissionsController {
*
* @param {string} account - The newly selected account's address.
*/
async _handleAccountSelected (account) {
async _handleAccountSelected(account) {
if (typeof account !== 'string') {
throw new Error('Selected account should be a non-empty string.')
}
@ -631,20 +637,20 @@ export class PermissionsController {
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
const ethAccounts = permissions.find(
(permission) => permission.parentCapability === 'eth_accounts',
)
const exposedAccounts = ethAccounts?.caveats.find(
(caveat) => caveat.name === 'exposedAccounts',
)?.value
return exposedAccounts?.includes(account)
})
.map(([domain]) => domain)
await Promise.all(
connectedDomains
.map(
(origin) => this._handleConnectedAccountSelected(origin),
),
connectedDomains.map((origin) =>
this._handleConnectedAccountSelected(origin),
),
)
}
@ -656,7 +662,7 @@ export class PermissionsController {
*
* @param {string} origin - The origin
*/
async _handleConnectedAccountSelected (origin) {
async _handleConnectedAccountSelected(origin) {
const permittedAccounts = await this.getAccounts(origin)
this.notifyAccountsChanged(origin, permittedAccounts)
@ -669,8 +675,7 @@ export class PermissionsController {
* @param {Function} resolve - The function resolving the pending approval Promise.
* @param {Function} reject - The function rejecting the pending approval Promise.
*/
_addPendingApproval (id, origin, resolve, reject) {
_addPendingApproval(id, origin, resolve, reject) {
if (
this.pendingApprovalOrigins.has(origin) ||
this.pendingApprovals.has(id)
@ -688,7 +693,7 @@ export class PermissionsController {
* Removes the pending approval with the given id.
* @param {string} id - The id of the pending approval to remove.
*/
_removePendingApproval (id) {
_removePendingApproval(id) {
const { origin } = this.pendingApprovals.get(id)
this.pendingApprovalOrigins.delete(origin)
this.pendingApprovals.delete(id)
@ -698,51 +703,54 @@ export class PermissionsController {
* A convenience method for retrieving a login object
* or creating a new one if needed.
*
* @param {string} origin = The origin string representing the domain.
* @param {string} origin - The origin string representing the domain.
*/
_initializePermissions (restoredState) {
_initializePermissions(restoredState) {
// these permission requests are almost certainly stale
const initState = { ...restoredState, permissionsRequests: [] }
this.permissions = new RpcCap({
this.permissions = new RpcCap(
{
// Supports passthrough methods:
safeMethods: SAFE_METHODS,
// Supports passthrough methods:
safeMethods: SAFE_METHODS,
// optional prefix for internal methods
methodPrefix: WALLET_PREFIX,
// optional prefix for internal methods
methodPrefix: WALLET_PREFIX,
restrictedMethods: this._restrictedMethods,
restrictedMethods: this._restrictedMethods,
/**
* A promise-returning callback used to determine whether to approve
* permissions requests or not.
*
* Currently only returns a boolean, but eventually should return any
* specific parameters or amendments to the permissions.
*
* @param {string} req - The internal rpc-cap user request object.
*/
requestUserApproval: async (req) => {
const {
metadata: { id, origin },
} = req
/**
* A promise-returning callback used to determine whether to approve
* permissions requests or not.
*
* Currently only returns a boolean, but eventually should return any
* specific parameters or amendments to the permissions.
*
* @param {string} req - The internal rpc-cap user request object.
*/
requestUserApproval: async (req) => {
const { metadata: { id, origin } } = req
if (this.pendingApprovalOrigins.has(origin)) {
throw ethErrors.rpc.resourceUnavailable(
'Permissions request already pending; please wait.',
)
}
if (this.pendingApprovalOrigins.has(origin)) {
throw ethErrors.rpc.resourceUnavailable(
'Permissions request already pending; please wait.',
)
}
this._showPermissionRequest()
this._showPermissionRequest()
return new Promise((resolve, reject) => {
this._addPendingApproval(id, origin, resolve, reject)
})
return new Promise((resolve, reject) => {
this._addPendingApproval(id, origin, resolve, reject)
})
},
},
}, initState)
initState,
)
}
}
export function addInternalMethodPrefix (method) {
export function addInternalMethodPrefix(method) {
return WALLET_PREFIX + method
}

View File

@ -14,8 +14,7 @@ import {
* and permissions-related methods.
*/
export default class PermissionsLogController {
constructor ({ restrictedMethods, store }) {
constructor({ restrictedMethods, store }) {
this.restrictedMethods = restrictedMethods
this.store = store
}
@ -25,7 +24,7 @@ export default class PermissionsLogController {
*
* @returns {Array<Object>} The activity log.
*/
getActivityLog () {
getActivityLog() {
return this.store.getState()[LOG_STORE_KEY] || []
}
@ -34,7 +33,7 @@ export default class PermissionsLogController {
*
* @param {Array<Object>} logs - The new activity log array.
*/
updateActivityLog (logs) {
updateActivityLog(logs) {
this.store.updateState({ [LOG_STORE_KEY]: logs })
}
@ -43,7 +42,7 @@ export default class PermissionsLogController {
*
* @returns {Object} The permissions history log.
*/
getHistory () {
getHistory() {
return this.store.getState()[HISTORY_STORE_KEY] || {}
}
@ -52,7 +51,7 @@ export default class PermissionsLogController {
*
* @param {Object} history - The new permissions history log object.
*/
updateHistory (history) {
updateHistory(history) {
this.store.updateState({ [HISTORY_STORE_KEY]: history })
}
@ -63,8 +62,7 @@ export default class PermissionsLogController {
* @param {string} origin - The origin that the accounts are exposed to.
* @param {Array<string>} accounts - The accounts.
*/
updateAccountsHistory (origin, accounts) {
updateAccountsHistory(origin, accounts) {
if (accounts.length === 0) {
return
}
@ -88,9 +86,8 @@ export default class PermissionsLogController {
*
* @returns {JsonRpcEngineMiddleware} The permissions log middleware.
*/
createMiddleware () {
createMiddleware() {
return (req, res, next, _end) => {
let activityEntry, requestedMethods
const { origin, method } = req
const isInternal = method.startsWith(WALLET_PREFIX)
@ -100,7 +97,6 @@ export default class PermissionsLogController {
!LOG_IGNORE_METHODS.includes(method) &&
(isInternal || this.restrictedMethods.includes(method))
) {
activityEntry = this.logRequest(req, isInternal)
if (method === `${WALLET_PREFIX}requestPermissions`) {
@ -109,7 +105,6 @@ export default class PermissionsLogController {
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)
@ -122,7 +117,6 @@ export default class PermissionsLogController {
// call next with a return handler for capturing the response
next((cb) => {
const time = Date.now()
this.logResponse(activityEntry, res, time)
@ -130,7 +124,10 @@ export default class PermissionsLogController {
// any permissions or accounts changes will be recorded on the response,
// so we only log permissions history here
this.logPermissionsHistory(
requestedMethods, origin, res.result, time,
requestedMethods,
origin,
res.result,
time,
method === 'eth_requestAccounts',
)
}
@ -145,13 +142,13 @@ export default class PermissionsLogController {
* @param {Object} request - The request object.
* @param {boolean} isInternal - Whether the request is internal.
*/
logRequest (request, isInternal) {
logRequest(request, isInternal) {
const activityEntry = {
id: request.id,
method: request.method,
methodType: (
isInternal ? LOG_METHOD_TYPES.internal : LOG_METHOD_TYPES.restricted
),
methodType: isInternal
? LOG_METHOD_TYPES.internal
: LOG_METHOD_TYPES.restricted,
origin: request.origin,
request: cloneDeep(request),
requestTime: Date.now(),
@ -171,8 +168,7 @@ export default class PermissionsLogController {
* @param {Object} response - The response object.
* @param {number} time - Output from Date.now()
*/
logResponse (entry, response, time) {
logResponse(entry, response, time) {
if (!entry || !response) {
return
}
@ -188,8 +184,7 @@ export default class PermissionsLogController {
*
* @param {Object} entry - The activity log entry.
*/
commitNewActivity (entry) {
commitNewActivity(entry) {
const logs = this.getActivityLog()
// add new entry to end of log
@ -212,32 +207,31 @@ export default class PermissionsLogController {
* @param {string} time - The time of the request, i.e. Date.now().
* @param {boolean} isEthRequestAccounts - Whether the permissions request was 'eth_requestAccounts'.
*/
logPermissionsHistory (
requestedMethods, origin, result,
time, isEthRequestAccounts,
logPermissionsHistory(
requestedMethods,
origin,
result,
time,
isEthRequestAccounts,
) {
let accounts, newEntries
if (isEthRequestAccounts) {
accounts = result
const accountToTimeMap = getAccountToTimeMap(accounts, time)
newEntries = {
'eth_accounts': {
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
// accounts were last seen or approved by the origin.
newEntries = result
.map((perm) => {
if (perm.parentCapability === 'eth_accounts') {
accounts = this.getAccountsFromPermission(perm)
}
@ -245,13 +239,10 @@ export default class PermissionsLogController {
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)
acc[method] = {
@ -280,8 +271,7 @@ export default class PermissionsLogController {
* @param {string} origin - The requesting origin.
* @param {Object} newEntries - The new entries to commit.
*/
commitNewHistory (origin, newEntries) {
commitNewHistory(origin, newEntries) {
// a simple merge updates most permissions
const history = this.getHistory()
const newOriginHistory = {
@ -291,19 +281,16 @@ export default class PermissionsLogController {
// eth_accounts requires special handling, because of information
// we store about the accounts
const existingEthAccountsEntry = (
const existingEthAccountsEntry =
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 = (
const lastApproved =
newEthAccountsEntry.lastApproved ||
existingEthAccountsEntry.lastApproved
)
// merge old and new eth_accounts history entries
newOriginHistory.eth_accounts = {
@ -326,7 +313,7 @@ export default class PermissionsLogController {
* @param {Object} request - The request object.
* @returns {Array<string>} The names of the requested permissions.
*/
getRequestedMethods (request) {
getRequestedMethods(request) {
if (
!request.params ||
!request.params[0] ||
@ -345,20 +332,17 @@ export default class PermissionsLogController {
* @param {Object} perm - The permissions object.
* @returns {Array<string>} The permitted accounts.
*/
getAccountsFromPermission (perm) {
getAccountsFromPermission(perm) {
if (perm.parentCapability !== 'eth_accounts' || !perm.caveats) {
return []
}
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)
}
@ -377,8 +361,6 @@ export default class PermissionsLogController {
* @param {number} time - A time, e.g. Date.now().
* @returns {Object} A string:number map of addresses to time.
*/
function getAccountToTimeMap (accounts, time) {
return accounts.reduce(
(acc, account) => ({ ...acc, [account]: time }), {},
)
function getAccountToTimeMap(accounts, time) {
return accounts.reduce((acc, account) => ({ ...acc, [account]: time }), {})
}

View File

@ -4,7 +4,7 @@ import { ethErrors } from 'eth-json-rpc-errors'
/**
* Create middleware for handling certain methods and preprocessing permissions requests.
*/
export default function createPermissionsMethodMiddleware ({
export default function createPermissionsMethodMiddleware({
addDomainMetadata,
getAccounts,
getUnlockPromise,
@ -12,26 +12,21 @@ export default function createPermissionsMethodMiddleware ({
notifyAccountsChanged,
requestAccountsPermission,
}) {
let isProcessingRequestAccounts = false
return createAsyncMiddleware(async (req, res, next) => {
let responseHandler
switch (req.method) {
// Intercepting eth_accounts requests for backwards compatibility:
// The getAccounts call below wraps the rpc-cap middleware, and returns
// an empty array in case of errors (such as 4100:unauthorized)
case 'eth_accounts': {
res.result = await getAccounts()
return
}
case 'eth_requestAccounts': {
if (isProcessingRequestAccounts) {
res.error = ethErrors.rpc.resourceUnavailable(
'Already processing eth_requestAccounts. Please wait.',
@ -79,7 +74,6 @@ export default function createPermissionsMethodMiddleware ({
// custom method for getting metadata from the requesting domain,
// sent automatically by the inpage provider when it's initialized
case 'wallet_sendDomainMetadata': {
if (typeof req.domainMetadata?.name === 'string') {
addDomainMetadata(req.origin, req.domainMetadata)
}
@ -89,11 +83,8 @@ export default function createPermissionsMethodMiddleware ({
// register return handler to send accountsChanged notification
case 'wallet_requestPermissions': {
if ('eth_accounts' in req.params?.[0]) {
responseHandler = async () => {
if (Array.isArray(res.result)) {
for (const permission of res.result) {
if (permission.parentCapability === 'eth_accounts') {

View File

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

View File

@ -9,28 +9,27 @@ import { addInternalMethodPrefix } from './permissions'
import { NETWORK_TYPE_TO_ID_MAP } from './network/enums'
export default class PreferencesController {
/**
*
* @typedef {Object} PreferencesController
* @param {Object} opts - Overrides the defaults for the initial state of this.store
* @property {object} store The stored object containing a users preferences, stored in local storage
* @property {array} store.frequentRpcList A list of custom rpcs to provide the user
* @property {array} store.tokens The tokens the user wants display in their token lists
* @property {object} store.accountTokens The tokens stored per account and then per network type
* @property {object} store.assetImages Contains assets objects related to assets added
* @property {Object} store The stored object containing a users preferences, stored in local storage
* @property {Array} store.frequentRpcList A list of custom rpcs to provide the user
* @property {Array} store.tokens The tokens the user wants display in their token lists
* @property {Object} store.accountTokens The tokens stored per account and then per network type
* @property {Object} store.assetImages Contains assets objects related to assets added
* @property {boolean} store.useBlockie The users preference for blockie identicons within the UI
* @property {boolean} store.useNonceField The users preference for nonce field within the UI
* @property {object} store.featureFlags A key-boolean map, where keys refer to features and booleans to whether the
* @property {Object} store.featureFlags A key-boolean map, where keys refer to features and booleans to whether the
* user wishes to see that feature.
*
* Feature flags can be set by the global function `setPreference(feature, enabled)`, and so should not expose any sensitive behavior.
* @property {object} store.knownMethodData Contains all data methods known by the user
* @property {Object} store.knownMethodData Contains all data methods known by the user
* @property {string} store.currentLocale The preferred language locale key
* @property {string} store.selectedAddress A hex string that matches the currently selected address in the app
*
*/
constructor (opts = {}) {
constructor(opts = {}) {
const initState = {
frequentRpcListDetail: [],
accountTokens: {},
@ -66,7 +65,8 @@ export default class PreferencesController {
metaMetricsSendCount: 0,
// ENS decentralized website resolution
ipfsGateway: 'dweb.link', ...opts.initState,
ipfsGateway: 'dweb.link',
...opts.initState,
}
this.network = opts.network
@ -86,7 +86,7 @@ export default class PreferencesController {
* Sets the {@code forgottenPassword} state property
* @param {boolean} forgottenPassword - whether or not the user has forgotten their password
*/
setPasswordForgotten (forgottenPassword) {
setPasswordForgotten(forgottenPassword) {
this.store.updateState({ forgottenPassword })
}
@ -96,7 +96,7 @@ export default class PreferencesController {
* @param {boolean} val - Whether or not the user prefers blockie indicators
*
*/
setUseBlockie (val) {
setUseBlockie(val) {
this.store.updateState({ useBlockie: val })
}
@ -106,7 +106,7 @@ export default class PreferencesController {
* @param {boolean} val - Whether or not the user prefers to set nonce
*
*/
setUseNonceField (val) {
setUseNonceField(val) {
this.store.updateState({ useNonceField: val })
}
@ -116,7 +116,7 @@ export default class PreferencesController {
* @param {boolean} val - Whether or not the user prefers phishing domain protection
*
*/
setUsePhishDetect (val) {
setUsePhishDetect(val) {
this.store.updateState({ usePhishDetect: val })
}
@ -124,14 +124,19 @@ export default class PreferencesController {
* Setter for the `participateInMetaMetrics` property
*
* @param {boolean} bool - Whether or not the user wants to participate in MetaMetrics
* @returns {string|null} - the string of the new metametrics id, or null if not set
* @returns {string|null} the string of the new metametrics id, or null if not set
*
*/
setParticipateInMetaMetrics (bool) {
setParticipateInMetaMetrics(bool) {
this.store.updateState({ participateInMetaMetrics: bool })
let metaMetricsId = null
if (bool && !this.store.getState().metaMetricsId) {
metaMetricsId = bufferToHex(sha3(String(Date.now()) + String(Math.round(Math.random() * Number.MAX_SAFE_INTEGER))))
metaMetricsId = bufferToHex(
sha3(
String(Date.now()) +
String(Math.round(Math.random() * Number.MAX_SAFE_INTEGER)),
),
)
this.store.updateState({ metaMetricsId })
} else if (bool === false) {
this.store.updateState({ metaMetricsId })
@ -139,11 +144,11 @@ export default class PreferencesController {
return metaMetricsId
}
getParticipateInMetaMetrics () {
getParticipateInMetaMetrics() {
return this.store.getState().participateInMetaMetrics
}
setMetaMetricsSendCount (val) {
setMetaMetricsSendCount(val) {
this.store.updateState({ metaMetricsSendCount: val })
}
@ -153,19 +158,19 @@ export default class PreferencesController {
* @param {string} type - Indicates the type of first time flow - create or import - the user wishes to follow
*
*/
setFirstTimeFlowType (type) {
setFirstTimeFlowType(type) {
this.store.updateState({ firstTimeFlowType: type })
}
getSuggestedTokens () {
getSuggestedTokens() {
return this.store.getState().suggestedTokens
}
getAssetImages () {
getAssetImages() {
return this.store.getState().assetImages
}
addSuggestedERC20Asset (tokenOpts) {
addSuggestedERC20Asset(tokenOpts) {
this._validateERC20AssetParams(tokenOpts)
const suggested = this.getSuggestedTokens()
const { rawAddress, symbol, decimals, image } = tokenOpts
@ -181,7 +186,7 @@ export default class PreferencesController {
* @param {string} fourBytePrefix - Four-byte method signature
* @param {string} methodData - Corresponding data method
*/
addKnownMethodData (fourBytePrefix, methodData) {
addKnownMethodData(fourBytePrefix, methodData) {
const { knownMethodData } = this.store.getState()
knownMethodData[fourBytePrefix] = methodData
this.store.updateState({ knownMethodData })
@ -190,12 +195,12 @@ export default class PreferencesController {
/**
* RPC engine middleware for requesting new asset added
*
* @param req
* @param res
* @param {Function} - next
* @param {Function} - end
* @param {any} req
* @param {any} res
* @param {Function} next
* @param {Function} end
*/
async requestWatchAsset (req, res, next, end) {
async requestWatchAsset(req, res, next, end) {
if (
req.method === 'metamask_watchAsset' ||
req.method === addInternalMethodPrefix('watchAsset')
@ -227,8 +232,10 @@ export default class PreferencesController {
* @param {string} key - he preferred language locale key
*
*/
setCurrentLocale (key) {
const textDirection = (['ar', 'dv', 'fa', 'he', 'ku'].includes(key)) ? 'rtl' : 'auto'
setCurrentLocale(key) {
const textDirection = ['ar', 'dv', 'fa', 'he', 'ku'].includes(key)
? 'rtl'
: 'auto'
this.store.updateState({
currentLocale: key,
textDirection,
@ -243,7 +250,7 @@ export default class PreferencesController {
* @param {string[]} addresses - An array of hex addresses
*
*/
setAddresses (addresses) {
setAddresses(addresses) {
const oldIdentities = this.store.getState().identities
const oldAccountTokens = this.store.getState().accountTokens
@ -264,9 +271,9 @@ export default class PreferencesController {
* Removes an address from state
*
* @param {string} address - A hex address
* @returns {string} - the address that was removed
* @returns {string} the address that was removed
*/
removeAddress (address) {
removeAddress(address) {
const { identities } = this.store.getState()
const { accountTokens } = this.store.getState()
if (!identities[address]) {
@ -291,7 +298,7 @@ export default class PreferencesController {
* @param {string[]} addresses - An array of hex addresses
*
*/
addAddresses (addresses) {
addAddresses(addresses) {
const { identities, accountTokens } = this.store.getState()
addresses.forEach((address) => {
// skip if already exists
@ -312,10 +319,9 @@ export default class PreferencesController {
* Removes any unknown identities, and returns the resulting selected address.
*
* @param {Array<string>} addresses - known to the vault.
* @returns {Promise<string>} - selectedAddress the selected address.
* @returns {Promise<string>} selectedAddress the selected address.
*/
syncAddresses (addresses) {
syncAddresses(addresses) {
if (!Array.isArray(addresses) || addresses.length === 0) {
throw new Error('Expected non-empty array of addresses.')
}
@ -332,7 +338,6 @@ export default class PreferencesController {
// Identities are no longer present.
if (Object.keys(newlyLost).length > 0) {
// store lost accounts
Object.keys(newlyLost).forEach((key) => {
lostIdentities[key] = newlyLost[key]
@ -353,7 +358,7 @@ export default class PreferencesController {
return selected
}
removeSuggestedTokens () {
removeSuggestedTokens() {
return new Promise((resolve) => {
this.store.updateState({ suggestedTokens: {} })
resolve({})
@ -364,10 +369,10 @@ export default class PreferencesController {
* Setter for the `selectedAddress` property
*
* @param {string} _address - A new hex address for an account
* @returns {Promise<void>} - Promise resolves with tokens
* @returns {Promise<void>} Promise resolves with tokens
*
*/
setSelectedAddress (_address) {
setSelectedAddress(_address) {
const address = normalizeAddress(_address)
this._updateTokens(address)
@ -385,10 +390,10 @@ export default class PreferencesController {
/**
* Getter for the `selectedAddress` property
*
* @returns {string} - The hex address for the currently selected account
* @returns {string} The hex address for the currently selected account
*
*/
getSelectedAddress () {
getSelectedAddress() {
return this.store.getState().selectedAddress
}
@ -409,11 +414,11 @@ export default class PreferencesController {
*
* @param {string} rawAddress - Hex address of the token contract. May or may not be a checksum address.
* @param {string} symbol - The symbol of the token
* @param {number} decimals - The number of decimals the token uses.
* @returns {Promise<array>} - Promises the new array of AddedToken objects.
* @param {number} decimals - The number of decimals the token uses.
* @returns {Promise<array>} Promises the new array of AddedToken objects.
*
*/
async addToken (rawAddress, symbol, decimals, image) {
async addToken(rawAddress, symbol, decimals, image) {
const address = normalizeAddress(rawAddress)
const newEntry = { address, symbol, decimals }
const { tokens } = this.store.getState()
@ -437,10 +442,10 @@ export default class PreferencesController {
* Removes a specified token from the tokens array.
*
* @param {string} rawAddress - Hex address of the token contract to remove.
* @returns {Promise<array>} - The new array of AddedToken objects
* @returns {Promise<array>} The new array of AddedToken objects
*
*/
removeToken (rawAddress) {
removeToken(rawAddress) {
const { tokens } = this.store.getState()
const assetImages = this.getAssetImages()
const updatedTokens = tokens.filter((token) => token.address !== rawAddress)
@ -452,10 +457,10 @@ export default class PreferencesController {
/**
* A getter for the `tokens` property
*
* @returns {array} - The current array of AddedToken objects
* @returns {Array} The current array of AddedToken objects
*
*/
getTokens () {
getTokens() {
return this.store.getState().tokens
}
@ -465,9 +470,11 @@ export default class PreferencesController {
* @param {string} label - the custom label for the account
* @returns {Promise<string>}
*/
setAccountLabel (account, label) {
setAccountLabel(account, label) {
if (!account) {
throw new Error(`setAccountLabel requires a valid address, got ${String(account)}`)
throw new Error(
`setAccountLabel requires a valid address, got ${String(account)}`,
)
}
const address = normalizeAddress(account)
const { identities } = this.store.getState()
@ -488,7 +495,7 @@ export default class PreferencesController {
* @param {Object} [newRpcDetails.rpcPrefs] - Optional RPC preferences, such as the block explorer URL
*
*/
async updateRpc (newRpcDetails) {
async updateRpc(newRpcDetails) {
const rpcList = this.getFrequentRpcListDetail()
const index = rpcList.findIndex((element) => {
return element.rpcUrl === newRpcDetails.rpcUrl
@ -497,7 +504,6 @@ export default class PreferencesController {
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
@ -506,13 +512,17 @@ export default class PreferencesController {
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)
const provider = new ethers.providers.JsonRpcProvider(
rpcDetail.rpcUrl,
)
try {
addressBookKey = await provider.send('net_version')
assert(typeof addressBookKey === 'string')
} catch (error) {
log.debug(error)
log.warn(`Failed to get networkId from ${rpcDetail.rpcUrl}; skipping address book migration`)
log.warn(
`Failed to get networkId from ${rpcDetail.rpcUrl}; skipping address book migration`,
)
}
}
@ -521,10 +531,12 @@ export default class PreferencesController {
// on both networks, since we don't know which network each contact is intended for.
let duplicate = false
const builtInProviderNetworkIds = Object.values(NETWORK_TYPE_TO_ID_MAP)
.map((ids) => ids.networkId)
const otherRpcEntries = rpcList
.filter((entry) => entry.rpcUrl !== newRpcDetails.rpcUrl)
const builtInProviderNetworkIds = Object.values(
NETWORK_TYPE_TO_ID_MAP,
).map((ids) => ids.networkId)
const otherRpcEntries = rpcList.filter(
(entry) => entry.rpcUrl !== newRpcDetails.rpcUrl,
)
if (
builtInProviderNetworkIds.includes(addressBookKey) ||
otherRpcEntries.some((entry) => entry.chainId === addressBookKey)
@ -532,7 +544,11 @@ export default class PreferencesController {
duplicate = true
}
this.migrateAddressBookState(addressBookKey, updatedRpc.chainId, duplicate)
this.migrateAddressBookState(
addressBookKey,
updatedRpc.chainId,
duplicate,
)
}
rpcList[index] = updatedRpc
this.store.updateState({ frequentRpcListDetail: rpcList })
@ -552,7 +568,13 @@ export default class PreferencesController {
* @param {Object} [rpcPrefs] - Optional RPC preferences, such as the block explorer URL
*
*/
addToFrequentRpcList (rpcUrl, chainId, ticker = 'ETH', nickname = '', rpcPrefs = {}) {
addToFrequentRpcList(
rpcUrl,
chainId,
ticker = 'ETH',
nickname = '',
rpcPrefs = {},
) {
const rpcList = this.getFrequentRpcListDetail()
const index = rpcList.findIndex((element) => {
@ -574,10 +596,10 @@ export default class PreferencesController {
* Removes custom RPC url from state.
*
* @param {string} url - The RPC url to remove from frequentRpcList.
* @returns {Promise<array>} - Promise resolving to updated frequentRpcList.
* @returns {Promise<array>} Promise resolving to updated frequentRpcList.
*
*/
removeFromFrequentRpcList (url) {
removeFromFrequentRpcList(url) {
const rpcList = this.getFrequentRpcListDetail()
const index = rpcList.findIndex((element) => {
return element.rpcUrl === url
@ -592,10 +614,10 @@ export default class PreferencesController {
/**
* Getter for the `frequentRpcListDetail` property.
*
* @returns {array<array>} - An array of rpc urls.
* @returns {array<array>} An array of rpc urls.
*
*/
getFrequentRpcListDetail () {
getFrequentRpcListDetail() {
return this.store.getState().frequentRpcListDetail
}
@ -604,10 +626,10 @@ export default class PreferencesController {
*
* @param {string} feature - A key that corresponds to a UI feature.
* @param {boolean} activated - Indicates whether or not the UI feature should be displayed
* @returns {Promise<object>} - Promises a new object; the updated featureFlags object.
* @returns {Promise<object>} Promises a new object; the updated featureFlags object.
*
*/
setFeatureFlag (feature, activated) {
setFeatureFlag(feature, activated) {
const currentFeatureFlags = this.store.getState().featureFlags
const updatedFeatureFlags = {
...currentFeatureFlags,
@ -624,9 +646,9 @@ export default class PreferencesController {
* found in the settings page.
* @param {string} preference - The preference to enable or disable.
* @param {boolean} value - Indicates whether or not the preference should be enabled or disabled.
* @returns {Promise<object>} - Promises a new object; the updated preferences object.
* @returns {Promise<object>} Promises a new object; the updated preferences object.
*/
setPreference (preference, value) {
setPreference(preference, value) {
const currentPreferences = this.getPreferences()
const updatedPreferences = {
...currentPreferences,
@ -639,9 +661,9 @@ export default class PreferencesController {
/**
* A getter for the `preferences` property
* @returns {Object} - A key-boolean map of user-selected preferences.
* @returns {Object} A key-boolean map of user-selected preferences.
*/
getPreferences () {
getPreferences() {
return this.store.getState().preferences
}
@ -649,25 +671,25 @@ export default class PreferencesController {
* Sets the completedOnboarding state to true, indicating that the user has completed the
* onboarding process.
*/
completeOnboarding () {
completeOnboarding() {
this.store.updateState({ completedOnboarding: true })
return Promise.resolve(true)
}
/**
* A getter for the `ipfsGateway` property
* @returns {string} - The current IPFS gateway domain
* @returns {string} The current IPFS gateway domain
*/
getIpfsGateway () {
getIpfsGateway() {
return this.store.getState().ipfsGateway
}
/**
* A setter for the `ipfsGateway` property
* @param {string} domain - The new IPFS gateway domain
* @returns {Promise<string>} - A promise of the update IPFS gateway domain
* @returns {Promise<string>} A promise of the update IPFS gateway domain
*/
setIpfsGateway (domain) {
setIpfsGateway(domain) {
this.store.updateState({ ipfsGateway: domain })
return Promise.resolve(domain)
}
@ -681,7 +703,7 @@ export default class PreferencesController {
*
*
*/
_subscribeProviderType () {
_subscribeProviderType() {
this.network.providerStore.subscribe(() => {
const { tokens } = this._getTokenRelatedStates()
this.store.updateState({ tokens })
@ -691,11 +713,15 @@ export default class PreferencesController {
/**
* Updates `accountTokens` and `tokens` of current account and network according to it.
*
* @param {array} tokens - Array of tokens to be updated.
* @param {Array} tokens - Array of tokens to be updated.
*
*/
_updateAccountTokens (tokens, assetImages) {
const { accountTokens, providerType, selectedAddress } = this._getTokenRelatedStates()
_updateAccountTokens(tokens, assetImages) {
const {
accountTokens,
providerType,
selectedAddress,
} = this._getTokenRelatedStates()
accountTokens[selectedAddress][providerType] = tokens
this.store.updateState({ accountTokens, tokens, assetImages })
}
@ -706,7 +732,7 @@ export default class PreferencesController {
* @param {string} selectedAddress - Account address to be updated with.
*
*/
_updateTokens (selectedAddress) {
_updateTokens(selectedAddress) {
const { tokens } = this._getTokenRelatedStates(selectedAddress)
this.store.updateState({ tokens })
}
@ -714,11 +740,11 @@ export default class PreferencesController {
/**
* A getter for `tokens` and `accountTokens` related states.
*
* @param {string} [selectedAddress] A new hex address for an account
* @returns {Object.<array, object, string, string>} - States to interact with tokens in `accountTokens`
* @param {string} [selectedAddress] - A new hex address for an account
* @returns {Object.<array, object, string, string>} States to interact with tokens in `accountTokens`
*
*/
_getTokenRelatedStates (selectedAddress) {
_getTokenRelatedStates(selectedAddress) {
const { accountTokens } = this.store.getState()
if (!selectedAddress) {
// eslint-disable-next-line no-param-reassign
@ -738,11 +764,11 @@ export default class PreferencesController {
/**
* Handle the suggestion of an ERC20 asset through `watchAsset`
* *
* @param {Promise} promise - Promise according to addition of ERC20 token
* @param {Object} tokenMetadata - Token metadata
*
*/
async _handleWatchAssetERC20 (options) {
const { address, symbol, decimals, image } = options
async _handleWatchAssetERC20(tokenMetadata) {
const { address, symbol, decimals, image } = tokenMetadata
const rawAddress = address
try {
this._validateERC20AssetParams({ rawAddress, symbol, decimals })
@ -752,7 +778,9 @@ export default class PreferencesController {
const tokenOpts = { rawAddress, decimals, symbol, image }
this.addSuggestedERC20Asset(tokenOpts)
return this.openPopup().then(() => {
const tokenAddresses = this.getTokens().filter((token) => token.address === normalizeAddress(rawAddress))
const tokenAddresses = this.getTokens().filter(
(token) => token.address === normalizeAddress(rawAddress),
)
return tokenAddresses.length > 0
})
}
@ -765,17 +793,21 @@ export default class PreferencesController {
* doesn't fulfill requirements
*
*/
_validateERC20AssetParams (opts) {
_validateERC20AssetParams(opts) {
const { rawAddress, symbol, decimals } = opts
if (!rawAddress || !symbol || typeof decimals === 'undefined') {
throw new Error(`Cannot suggest token without address, symbol, and decimals`)
throw new Error(
`Cannot suggest token without address, symbol, and decimals`,
)
}
if (!(symbol.length < 7)) {
throw new Error(`Invalid symbol ${symbol} more than six characters`)
}
const numDecimals = parseInt(decimals, 10)
if (isNaN(numDecimals) || numDecimals > 36 || numDecimals < 0) {
throw new Error(`Invalid decimals ${decimals} must be at least 0, and not over 36`)
throw new Error(
`Invalid decimals ${decimals} must be at least 0, and not over 36`,
)
}
if (!isValidAddress(rawAddress)) {
throw new Error(`Invalid address ${rawAddress}`)

View File

@ -2,7 +2,7 @@ import { ethers } from 'ethers'
import log from 'loglevel'
import BigNumber from 'bignumber.js'
import ObservableStore from 'obs-store'
import { mapValues } from 'lodash'
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'
@ -28,17 +28,14 @@ const MAX_GAS_LIMIT = 2500000
// 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
function calculateGasEstimateWithRefund (maxGas = MAX_GAS_LIMIT, estimatedRefund = 0, estimatedGas = 0) {
const maxGasMinusRefund = new BigNumber(
maxGas,
10,
)
.minus(estimatedRefund, 10)
function calculateGasEstimateWithRefund(
maxGas = MAX_GAS_LIMIT,
estimatedRefund = 0,
estimatedGas = 0,
) {
const maxGasMinusRefund = new BigNumber(maxGas, 10).minus(estimatedRefund, 10)
const gasEstimateWithRefund = maxGasMinusRefund.lt(
estimatedGas,
16,
)
const gasEstimateWithRefund = maxGasMinusRefund.lt(estimatedGas, 16)
? maxGasMinusRefund.toString(16)
: estimatedGas
@ -68,7 +65,7 @@ const initialState = {
}
export default class SwapsController {
constructor ({
constructor({
getBufferedGasLimit,
networkController,
provider,
@ -108,18 +105,26 @@ export default class SwapsController {
// that quotes will no longer be available after 1 or 2 minutes. When fetchAndSetQuotes is first called it, receives fetch that parameters are stored in
// state. These stored parameters are used on subsequent calls made during polling.
// Note: we stop polling after 3 requests, until new quotes are explicitly asked for. The logic that enforces that maximum is in the body of fetchAndSetQuotes
pollForNewQuotes () {
pollForNewQuotes() {
this.pollingTimeout = setTimeout(() => {
const { swapsState } = this.store.getState()
this.fetchAndSetQuotes(swapsState.fetchParams, swapsState.fetchParams.metaData, true)
this.fetchAndSetQuotes(
swapsState.fetchParams,
swapsState.fetchParams?.metaData,
true,
)
}, QUOTE_POLLING_INTERVAL)
}
stopPollingForQuotes () {
stopPollingForQuotes() {
clearTimeout(this.pollingTimeout)
}
async fetchAndSetQuotes (fetchParams, fetchParamsMetaData = {}, isPolledRequest) {
async fetchAndSetQuotes(
fetchParams,
fetchParamsMetaData = {},
isPolledRequest,
) {
if (!fetchParams) {
return null
}
@ -150,7 +155,10 @@ export default class SwapsController {
const quotesLastFetched = Date.now()
let approvalRequired = false
if (fetchParams.sourceToken !== ETH_SWAPS_TOKEN_ADDRESS && Object.values(newQuotes).length) {
if (
fetchParams.sourceToken !== ETH_SWAPS_TOKEN_ADDRESS &&
Object.values(newQuotes).length
) {
const allowance = await this._getERC20Allowance(
fetchParams.sourceToken,
fetchParams.fromAddress,
@ -167,7 +175,9 @@ export default class SwapsController {
approvalNeeded: null,
}))
} else if (!isPolledRequest) {
const { gasLimit: approvalGas } = await this.timedoutGasReturn(Object.values(newQuotes)[0].approvalNeeded)
const { gasLimit: approvalGas } = await this.timedoutGasReturn(
Object.values(newQuotes)[0].approvalNeeded,
)
newQuotes = mapValues(newQuotes, (quote) => ({
...quote,
@ -190,13 +200,12 @@ export default class SwapsController {
if (Object.values(newQuotes).length === 0) {
this.setSwapsErrorKey(QUOTES_NOT_AVAILABLE_ERROR)
} else {
const topQuoteData = await this._findTopQuoteAndCalculateSavings(newQuotes)
if (topQuoteData.topAggId) {
topAggId = topQuoteData.topAggId
newQuotes[topAggId].isBestQuote = topQuoteData.isBest
newQuotes[topAggId].savings = topQuoteData.savings
}
const [
_topAggId,
quotesWithSavingsAndFeeData,
] = await this._findTopQuoteAndCalculateSavings(newQuotes)
topAggId = _topAggId
newQuotes = quotesWithSavingsAndFeeData
}
// If a newer call has been made, don't update state with old information
@ -235,32 +244,34 @@ export default class SwapsController {
return [newQuotes, topAggId]
}
safeRefetchQuotes () {
safeRefetchQuotes() {
const { swapsState } = this.store.getState()
if (!this.pollingTimeout && swapsState.fetchParams) {
this.fetchAndSetQuotes(swapsState.fetchParams)
}
}
setSelectedQuoteAggId (selectedAggId) {
setSelectedQuoteAggId(selectedAggId) {
const { swapsState } = this.store.getState()
this.store.updateState({ swapsState: { ...swapsState, selectedAggId } })
}
setSwapsTokens (tokens) {
setSwapsTokens(tokens) {
const { swapsState } = this.store.getState()
this.store.updateState({ swapsState: { ...swapsState, tokens } })
}
setSwapsErrorKey (errorKey) {
setSwapsErrorKey(errorKey) {
const { swapsState } = this.store.getState()
this.store.updateState({ swapsState: { ...swapsState, errorKey } })
}
async getAllQuotesWithGasEstimates (quotes) {
async getAllQuotesWithGasEstimates(quotes) {
const quoteGasData = await Promise.all(
Object.values(quotes).map(async (quote) => {
const { gasLimit, simulationFails } = await this.timedoutGasReturn(quote.trade)
const { gasLimit, simulationFails } = await this.timedoutGasReturn(
quote.trade,
)
return [gasLimit, simulationFails, quote.aggregator]
}),
)
@ -268,7 +279,11 @@ export default class SwapsController {
const newQuotes = {}
quoteGasData.forEach(([gasLimit, simulationFails, aggId]) => {
if (gasLimit && !simulationFails) {
const gasEstimateWithRefund = calculateGasEstimateWithRefund(quotes[aggId].maxGas, quotes[aggId].estimatedRefund, gasLimit)
const gasEstimateWithRefund = calculateGasEstimateWithRefund(
quotes[aggId].maxGas,
quotes[aggId].estimatedRefund,
gasLimit,
)
newQuotes[aggId] = {
...quotes[aggId],
@ -285,7 +300,7 @@ export default class SwapsController {
return newQuotes
}
timedoutGasReturn (tradeTxParams) {
timedoutGasReturn(tradeTxParams) {
return new Promise((resolve) => {
let gasTimedOut = false
@ -321,7 +336,7 @@ export default class SwapsController {
})
}
async setInitialGasEstimate (initialAggId) {
async setInitialGasEstimate(initialAggId) {
const { swapsState } = this.store.getState()
const quoteToUpdate = { ...swapsState.quotes[initialAggId] }
@ -332,64 +347,73 @@ export default class SwapsController {
} = await this.timedoutGasReturn(quoteToUpdate.trade)
if (newGasEstimate && !simulationFails) {
const gasEstimateWithRefund = calculateGasEstimateWithRefund(quoteToUpdate.maxGas, quoteToUpdate.estimatedRefund, newGasEstimate)
const gasEstimateWithRefund = calculateGasEstimateWithRefund(
quoteToUpdate.maxGas,
quoteToUpdate.estimatedRefund,
newGasEstimate,
)
quoteToUpdate.gasEstimate = newGasEstimate
quoteToUpdate.gasEstimateWithRefund = gasEstimateWithRefund
}
this.store.updateState({
swapsState: { ...swapsState, quotes: { ...swapsState.quotes, [initialAggId]: quoteToUpdate } },
swapsState: {
...swapsState,
quotes: { ...swapsState.quotes, [initialAggId]: quoteToUpdate },
},
})
}
setApproveTxId (approveTxId) {
setApproveTxId(approveTxId) {
const { swapsState } = this.store.getState()
this.store.updateState({ swapsState: { ...swapsState, approveTxId } })
}
setTradeTxId (tradeTxId) {
setTradeTxId(tradeTxId) {
const { swapsState } = this.store.getState()
this.store.updateState({ swapsState: { ...swapsState, tradeTxId } })
}
setQuotesLastFetched (quotesLastFetched) {
setQuotesLastFetched(quotesLastFetched) {
const { swapsState } = this.store.getState()
this.store.updateState({ swapsState: { ...swapsState, quotesLastFetched } })
}
setSwapsTxGasPrice (gasPrice) {
setSwapsTxGasPrice(gasPrice) {
const { swapsState } = this.store.getState()
this.store.updateState({
swapsState: { ...swapsState, customGasPrice: gasPrice },
})
}
setSwapsTxGasLimit (gasLimit) {
setSwapsTxGasLimit(gasLimit) {
const { swapsState } = this.store.getState()
this.store.updateState({
swapsState: { ...swapsState, customMaxGas: gasLimit },
})
}
setCustomApproveTxData (data) {
setCustomApproveTxData(data) {
const { swapsState } = this.store.getState()
this.store.updateState({
swapsState: { ...swapsState, customApproveTxData: data },
})
}
setBackgroundSwapRouteState (routeState) {
setBackgroundSwapRouteState(routeState) {
const { swapsState } = this.store.getState()
this.store.updateState({ swapsState: { ...swapsState, routeState } })
}
setSwapsLiveness (swapsFeatureIsLive) {
setSwapsLiveness(swapsFeatureIsLive) {
const { swapsState } = this.store.getState()
this.store.updateState({ swapsState: { ...swapsState, swapsFeatureIsLive } })
this.store.updateState({
swapsState: { ...swapsState, swapsFeatureIsLive },
})
}
resetPostFetchState () {
resetPostFetchState() {
const { swapsState } = this.store.getState()
this.store.updateState({
@ -403,21 +427,25 @@ export default class SwapsController {
clearTimeout(this.pollingTimeout)
}
resetSwapsState () {
resetSwapsState() {
const { swapsState } = this.store.getState()
this.store.updateState({
swapsState: { ...initialState.swapsState, tokens: swapsState.tokens, swapsFeatureIsLive: swapsState.swapsFeatureIsLive },
swapsState: {
...initialState.swapsState,
tokens: swapsState.tokens,
swapsFeatureIsLive: swapsState.swapsFeatureIsLive,
},
})
clearTimeout(this.pollingTimeout)
}
async _getEthersGasPrice () {
async _getEthersGasPrice() {
const ethersGasPrice = await this.ethersProvider.getGasPrice()
return ethersGasPrice.toHexString()
}
async _findTopQuoteAndCalculateSavings (quotes = {}) {
async _findTopQuoteAndCalculateSavings(quotes = {}) {
const tokenConversionRates = this.tokenRatesStore.getState()
.contractExchangeRates
const {
@ -429,15 +457,14 @@ export default class SwapsController {
return {}
}
const usedGasPrice = customGasPrice || await this._getEthersGasPrice()
const newQuotes = cloneDeep(quotes)
let topAggId = ''
let ethTradeValueOfBestQuote = null
let ethFeeForBestQuote = null
const allEthTradeValues = []
const allEthFees = []
const usedGasPrice = customGasPrice || (await this._getEthersGasPrice())
Object.values(quotes).forEach((quote) => {
let topAggId = null
let overallValueOfBestQuoteForSorting = null
Object.values(newQuotes).forEach((quote) => {
const {
aggregator,
approvalNeeded,
@ -449,6 +476,7 @@ export default class SwapsController {
sourceAmount,
sourceToken,
trade,
fee: metaMaskFee,
} = quote
const tradeGasLimitForCalculation = gasEstimate
@ -468,8 +496,10 @@ export default class SwapsController {
// It always includes any external fees charged by the quote source. In
// addition, if the source asset is ETH, trade.value includes the amount
// of swapped ETH.
const totalWeiCost = new BigNumber(gasTotalInWeiHex, 16)
.plus(trade.value, 16)
const totalWeiCost = new BigNumber(gasTotalInWeiHex, 16).plus(
trade.value,
16,
)
const totalEthCost = conversionUtil(totalWeiCost, {
fromCurrency: 'ETH',
@ -482,81 +512,122 @@ export default class SwapsController {
// The total fee is aggregator/exchange fees plus gas fees.
// If the swap is from ETH, subtract the sourceAmount from the total cost.
// Otherwise, the total fee is simply trade.value plus gas fees.
const ethFee = sourceToken === ETH_SWAPS_TOKEN_ADDRESS
? conversionUtil(
totalWeiCost.minus(sourceAmount, 10), // sourceAmount is in wei
{
fromCurrency: 'ETH',
fromDenomination: 'WEI',
toDenomination: 'ETH',
fromNumericBase: 'BN',
numberOfDecimals: 6,
},
)
: totalEthCost
const ethFee =
sourceToken === ETH_SWAPS_TOKEN_ADDRESS
? conversionUtil(
totalWeiCost.minus(sourceAmount, 10), // sourceAmount is in wei
{
fromCurrency: 'ETH',
fromDenomination: 'WEI',
toDenomination: 'ETH',
fromNumericBase: 'BN',
numberOfDecimals: 6,
},
)
: totalEthCost
const decimalAdjustedDestinationAmount = calcTokenAmount(
destinationAmount,
destinationTokenInfo.decimals,
)
const tokenPercentageOfPreFeeDestAmount = new BigNumber(100, 10)
.minus(metaMaskFee, 10)
.div(100)
const destinationAmountBeforeMetaMaskFee = decimalAdjustedDestinationAmount.div(
tokenPercentageOfPreFeeDestAmount,
)
const metaMaskFeeInTokens = destinationAmountBeforeMetaMaskFee.minus(
decimalAdjustedDestinationAmount,
)
const tokenConversionRate = tokenConversionRates[destinationToken]
const ethValueOfTrade =
destinationToken === ETH_SWAPS_TOKEN_ADDRESS
? calcTokenAmount(destinationAmount, 18).minus(totalEthCost, 10)
: new BigNumber(tokenConversionRate || 1, 10)
.times(
calcTokenAmount(
destinationAmount,
destinationTokenInfo.decimals,
),
10,
)
.minus(tokenConversionRate ? totalEthCost : 0, 10)
const conversionRateForSorting = tokenConversionRate || 1
// collect values for savings calculation
allEthTradeValues.push(ethValueOfTrade)
allEthFees.push(ethFee)
const ethValueOfTokens = decimalAdjustedDestinationAmount.times(
conversionRateForSorting,
10,
)
const conversionRateForCalculations =
destinationToken === ETH_SWAPS_TOKEN_ADDRESS ? 1 : tokenConversionRate
const overallValueOfQuoteForSorting =
conversionRateForCalculations === undefined
? ethValueOfTokens
: ethValueOfTokens.minus(ethFee, 10)
quote.ethFee = ethFee.toString(10)
if (conversionRateForCalculations !== undefined) {
quote.ethValueOfTokens = ethValueOfTokens.toString(10)
quote.overallValueOfQuote = overallValueOfQuoteForSorting.toString(10)
quote.metaMaskFeeInEth = metaMaskFeeInTokens
.times(conversionRateForCalculations)
.toString(10)
}
if (
ethTradeValueOfBestQuote === null ||
ethValueOfTrade.gt(ethTradeValueOfBestQuote)
overallValueOfBestQuoteForSorting === null ||
overallValueOfQuoteForSorting.gt(overallValueOfBestQuoteForSorting)
) {
topAggId = aggregator
ethTradeValueOfBestQuote = ethValueOfTrade
ethFeeForBestQuote = ethFee
overallValueOfBestQuoteForSorting = overallValueOfQuoteForSorting
}
})
const isBest =
quotes[topAggId].destinationToken === ETH_SWAPS_TOKEN_ADDRESS ||
Boolean(tokenConversionRates[quotes[topAggId]?.destinationToken])
newQuotes[topAggId].destinationToken === ETH_SWAPS_TOKEN_ADDRESS ||
Boolean(tokenConversionRates[newQuotes[topAggId]?.destinationToken])
let savings = null
if (isBest) {
const bestQuote = newQuotes[topAggId]
savings = {}
const {
ethFee: medianEthFee,
metaMaskFeeInEth: medianMetaMaskFee,
ethValueOfTokens: medianEthValueOfTokens,
} = getMedianEthValueQuote(Object.values(newQuotes))
// Performance savings are calculated as:
// valueForBestTrade - medianValueOfAllTrades
savings.performance = ethTradeValueOfBestQuote.minus(
getMedian(allEthTradeValues),
// (ethValueOfTokens for the best trade) - (ethValueOfTokens for the media trade)
savings.performance = new BigNumber(bestQuote.ethValueOfTokens, 10).minus(
medianEthValueOfTokens,
10,
)
// Performance savings are calculated as:
// medianFeeOfAllTrades - feeForBestTrade
savings.fee = getMedian(allEthFees).minus(
ethFeeForBestQuote,
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)
// Total savings are the sum of performance and fee savings
savings.total = savings.performance.plus(savings.fee, 10).toString(10)
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
newQuotes[topAggId].isBestQuote = true
newQuotes[topAggId].savings = savings
}
return { topAggId, isBest, savings }
return [topAggId, newQuotes]
}
async _getERC20Allowance (contractAddress, walletAddress) {
async _getERC20Allowance(contractAddress, walletAddress) {
const contract = new ethers.Contract(
contractAddress, abi, this.ethersProvider,
contractAddress,
abi,
this.ethersProvider,
)
return await contract.allowance(walletAddress, METASWAP_ADDRESS)
}
@ -569,7 +640,7 @@ export default class SwapsController {
* If the browser goes offline, the interval is cleared and swaps are disabled
* until the value can be fetched again.
*/
_setupSwapsLivenessFetching () {
_setupSwapsLivenessFetching() {
const TEN_MINUTES_MS = 10 * 60 * 1000
let intervalId = null
@ -577,7 +648,10 @@ export default class SwapsController {
if (window.navigator.onLine && intervalId === null) {
// Set the interval first to prevent race condition between listener and
// initial call to this function.
intervalId = setInterval(this._fetchAndSetSwapsLiveness.bind(this), TEN_MINUTES_MS)
intervalId = setInterval(
this._fetchAndSetSwapsLiveness.bind(this),
TEN_MINUTES_MS,
)
this._fetchAndSetSwapsLiveness()
}
}
@ -608,7 +682,7 @@ export default class SwapsController {
* Only updates state if the fetched/computed flag value differs from current
* state.
*/
async _fetchAndSetSwapsLiveness () {
async _fetchAndSetSwapsLiveness() {
const { swapsState } = this.store.getState()
const { swapsFeatureIsLive: oldSwapsFeatureIsLive } = swapsState
let swapsFeatureIsLive = false
@ -637,7 +711,9 @@ export default class SwapsController {
}
if (!successfullyFetched) {
log.error('Failed to fetch swaps feature flag 3 times. Setting to false and trying again next interval.')
log.error(
'Failed to fetch swaps feature flag 3 times. Setting to false and trying again next interval.',
)
}
if (swapsFeatureIsLive !== oldSwapsFeatureIsLive) {
@ -647,36 +723,123 @@ export default class SwapsController {
}
/**
* Calculates the median of a sample of BigNumber values.
* Calculates the median overallValueOfQuote of a sample of quotes.
*
* @param {import('bignumber.js').BigNumber[]} values - A sample of BigNumber
* values. The array will be sorted in place.
* @returns {import('bignumber.js').BigNumber} The median of the sample.
* @param {Array} quotes - A sample of quote objects with overallValueOfQuote, ethFee, metaMaskFeeInEth, and ethValueOfTokens properties
* @returns {Object} An object with the ethValueOfTokens, ethFee, and metaMaskFeeInEth of the quote with the median overallValueOfQuote
*/
function getMedian (values) {
if (!Array.isArray(values) || values.length === 0) {
function getMedianEthValueQuote(_quotes) {
if (!Array.isArray(_quotes) || _quotes.length === 0) {
throw new Error('Expected non-empty array param.')
}
values.sort((a, b) => {
if (a.equals(b)) {
const quotes = [..._quotes]
quotes.sort((quoteA, quoteB) => {
const overallValueOfQuoteA = new BigNumber(quoteA.overallValueOfQuote, 10)
const overallValueOfQuoteB = new BigNumber(quoteB.overallValueOfQuote, 10)
if (overallValueOfQuoteA.equals(overallValueOfQuoteB)) {
return 0
}
return a.lessThan(b) ? -1 : 1
return overallValueOfQuoteA.lessThan(overallValueOfQuoteB) ? -1 : 1
})
if (values.length % 2 === 1) {
// return middle value
return values[(values.length - 1) / 2]
if (quotes.length % 2 === 1) {
// return middle values
const medianOverallValue =
quotes[(quotes.length - 1) / 2].overallValueOfQuote
const quotesMatchingMedianQuoteValue = quotes.filter(
(quote) => medianOverallValue === quote.overallValueOfQuote,
)
return meansOfQuotesFeesAndValue(quotesMatchingMedianQuoteValue)
}
// return mean of middle two values
const upperIndex = values.length / 2
return values[upperIndex]
.plus(values[upperIndex - 1])
.dividedBy(2)
const upperIndex = quotes.length / 2
const lowerIndex = upperIndex - 1
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)
.plus(feesAndValueAtLowerIndex.ethFee, 10)
.dividedBy(2)
.toString(10),
metaMaskFeeInEth: new BigNumber(
feesAndValueAtUpperIndex.metaMaskFeeInEth,
10,
)
.plus(feesAndValueAtLowerIndex.metaMaskFeeInEth, 10)
.dividedBy(2)
.toString(10),
ethValueOfTokens: new BigNumber(
feesAndValueAtUpperIndex.ethValueOfTokens,
10,
)
.plus(feesAndValueAtLowerIndex.ethValueOfTokens, 10)
.dividedBy(2)
.toString(10),
}
}
/**
* Calculates the arithmetic mean for each of three properties - ethFee, metaMaskFeeInEth and ethValueOfTokens - across
* an array of objects containing those properties.
*
* @param {Array} quotes - A sample of quote objects with overallValueOfQuote, ethFee, metaMaskFeeInEth and
* ethValueOfTokens properties
* @returns {Object} An object with the arithmetic mean each of the ethFee, metaMaskFeeInEth and ethValueOfTokens of
* the passed quote objects
*/
function meansOfQuotesFeesAndValue(quotes) {
const feeAndValueSumsAsBigNumbers = quotes.reduce(
(feeAndValueSums, quote) => ({
ethFee: feeAndValueSums.ethFee.plus(quote.ethFee, 10),
metaMaskFeeInEth: feeAndValueSums.metaMaskFeeInEth.plus(
quote.metaMaskFeeInEth,
10,
),
ethValueOfTokens: feeAndValueSums.ethValueOfTokens.plus(
quote.ethValueOfTokens,
10,
),
}),
{
ethFee: new BigNumber(0, 10),
metaMaskFeeInEth: new BigNumber(0, 10),
ethValueOfTokens: new BigNumber(0, 10),
},
)
return {
ethFee: feeAndValueSumsAsBigNumbers.ethFee
.div(quotes.length, 10)
.toString(10),
metaMaskFeeInEth: feeAndValueSumsAsBigNumbers.metaMaskFeeInEth
.div(quotes.length, 10)
.toString(10),
ethValueOfTokens: feeAndValueSumsAsBigNumbers.ethValueOfTokens
.div(quotes.length, 10)
.toString(10),
}
}
export const utils = {
getMedian,
getMedianEthValueQuote,
meansOfQuotesFeesAndValue,
}

View File

@ -18,7 +18,7 @@ import createMetamaskMiddleware from './network/createMetamaskMiddleware'
const SYNC_TIMEOUT = 60 * 1000 // one minute
export default class ThreeBoxController {
constructor (opts = {}) {
constructor(opts = {}) {
const {
preferencesController,
keyringController,
@ -41,16 +41,22 @@ export default class ThreeBoxController {
const accounts = await this.keyringController.getAccounts()
if (isUnlocked && accounts[0]) {
const appKeyAddress = await this.keyringController.getAppKeyAddress(accounts[0], 'wallet://3box.metamask.io')
const appKeyAddress = await this.keyringController.getAppKeyAddress(
accounts[0],
'wallet://3box.metamask.io',
)
return [appKeyAddress]
}
return []
},
processPersonalMessage: async (msgParams) => {
const accounts = await this.keyringController.getAccounts()
return keyringController.signPersonalMessage({ ...msgParams, from: accounts[0] }, {
withAppKeyOrigin: 'wallet://3box.metamask.io',
})
return keyringController.signPersonalMessage(
{ ...msgParams, from: accounts[0] },
{
withAppKeyOrigin: 'wallet://3box.metamask.io',
},
)
},
})
@ -65,14 +71,16 @@ export default class ThreeBoxController {
}
this.store = new ObservableStore(initState)
this.registeringUpdates = false
this.lastMigration = migrations.sort((a, b) => a.version - b.version).slice(-1)[0]
this.lastMigration = migrations
.sort((a, b) => a.version - b.version)
.slice(-1)[0]
if (initState.threeBoxSyncingAllowed) {
this.init()
}
}
async init () {
async init() {
const accounts = await this.keyringController.getAccounts()
this.address = accounts[0]
if (this.address && !(this.box && this.store.getState().threeBoxSynced)) {
@ -80,7 +88,7 @@ export default class ThreeBoxController {
}
}
async _update3Box () {
async _update3Box() {
try {
const { threeBoxSyncingAllowed, threeBoxSynced } = this.store.getState()
if (threeBoxSyncingAllowed && threeBoxSynced) {
@ -99,7 +107,7 @@ export default class ThreeBoxController {
}
}
_createProvider (providerOpts) {
_createProvider(providerOpts) {
const metamaskMiddleware = createMetamaskMiddleware(providerOpts)
const engine = new JsonRpcEngine()
engine.push(createOriginMiddleware({ origin: '3Box' }))
@ -108,7 +116,7 @@ export default class ThreeBoxController {
return provider
}
_waitForOnSyncDone () {
_waitForOnSyncDone() {
return new Promise((resolve) => {
this.box.onSyncDone(() => {
log.debug('3Box box sync done')
@ -117,9 +125,12 @@ export default class ThreeBoxController {
})
}
async new3Box () {
async new3Box() {
const accounts = await this.keyringController.getAccounts()
this.address = await this.keyringController.getAppKeyAddress(accounts[0], 'wallet://3box.metamask.io')
this.address = await this.keyringController.getAppKeyAddress(
accounts[0],
'wallet://3box.metamask.io',
)
let backupExists
try {
const threeBoxConfig = await Box.getConfig(this.address)
@ -170,20 +181,22 @@ export default class ThreeBoxController {
}
}
async getLastUpdated () {
async getLastUpdated() {
const res = await this.space.private.get('metamaskBackup')
const parsedRes = JSON.parse(res || '{}')
return parsedRes.lastUpdated
}
async migrateBackedUpState (backedUpState) {
async migrateBackedUpState(backedUpState) {
const migrator = new Migrator({ migrations })
const { preferences, addressBook } = JSON.parse(backedUpState)
const formattedStateBackup = {
PreferencesController: preferences,
AddressBookController: addressBook,
}
const initialMigrationState = migrator.generateInitialState(formattedStateBackup)
const initialMigrationState = migrator.generateInitialState(
formattedStateBackup,
)
const migratedState = await migrator.migrateData(initialMigrationState)
return {
preferences: migratedState.data.PreferencesController,
@ -191,31 +204,30 @@ export default class ThreeBoxController {
}
}
async restoreFromThreeBox () {
async restoreFromThreeBox() {
const backedUpState = await this.space.private.get('metamaskBackup')
const {
preferences,
addressBook,
} = await this.migrateBackedUpState(backedUpState)
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()
}
turnThreeBoxSyncingOn () {
turnThreeBoxSyncingOn() {
this._registerUpdates()
}
turnThreeBoxSyncingOff () {
turnThreeBoxSyncingOff() {
this.box.logout()
}
setShowRestorePromptToFalse () {
setShowRestorePromptToFalse() {
this.store.updateState({ showRestorePrompt: false })
}
setThreeBoxSyncingPermission (newThreeboxSyncingState) {
setThreeBoxSyncingPermission(newThreeboxSyncingState) {
if (this.store.getState().threeBoxDisabled) {
return
}
@ -232,11 +244,11 @@ export default class ThreeBoxController {
}
}
getThreeBoxSyncingState () {
getThreeBoxSyncingState() {
return this.store.getState().threeBoxSyncingAllowed
}
_registerUpdates () {
_registerUpdates() {
if (!this.registeringUpdates) {
const updatePreferences = this._update3Box.bind(this)
this.preferencesController.store.subscribe(updatePreferences)

View File

@ -11,13 +11,12 @@ const DEFAULT_INTERVAL = 180 * 1000
* rates based on a user's current token list
*/
export default class TokenRatesController {
/**
* Creates a TokenRatesController
*
* @param {Object} [config] - Options to configure controller
*/
constructor ({ currency, preferences } = {}) {
constructor({ currency, preferences } = {}) {
this.store = new ObservableStore()
this.currency = currency
this.preferences = preferences
@ -26,21 +25,32 @@ export default class TokenRatesController {
/**
* Updates exchange rates for all tokens
*/
async updateExchangeRates () {
async updateExchangeRates() {
const contractExchangeRates = {}
const nativeCurrency = this.currency ? this.currency.state.nativeCurrency.toLowerCase() : 'eth'
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}`
if (this._tokens.length > 0) {
try {
const response = await window.fetch(`https://api.coingecko.com/api/v3/simple/token_price/ethereum?${query}`)
const response = await window.fetch(
`https://api.coingecko.com/api/v3/simple/token_price/ethereum?${query}`,
)
const prices = await response.json()
this._tokens.forEach((token) => {
const price = prices[token.address.toLowerCase()] || prices[ethUtil.toChecksumAddress(token.address)]
contractExchangeRates[normalizeAddress(token.address)] = price ? price[nativeCurrency] : 0
const price =
prices[token.address.toLowerCase()] ||
prices[ethUtil.toChecksumAddress(token.address)]
contractExchangeRates[normalizeAddress(token.address)] = price
? price[nativeCurrency]
: 0
})
} catch (error) {
log.warn(`MetaMask - TokenRatesController exchange rate fetch failed.`, error)
log.warn(
`MetaMask - TokenRatesController exchange rate fetch failed.`,
error,
)
}
}
this.store.putState({ contractExchangeRates })
@ -50,7 +60,7 @@ export default class TokenRatesController {
/**
* @type {Object}
*/
set preferences (preferences) {
set preferences(preferences) {
this._preferences && this._preferences.unsubscribe()
if (!preferences) {
return
@ -65,13 +75,13 @@ export default class TokenRatesController {
/**
* @type {Array}
*/
set tokens (tokens) {
set tokens(tokens) {
this._tokens = tokens
this.updateExchangeRates()
}
/* eslint-enable accessor-pairs */
start (interval = DEFAULT_INTERVAL) {
start(interval = DEFAULT_INTERVAL) {
this._handle && clearInterval(this._handle)
if (!interval) {
return
@ -82,7 +92,7 @@ export default class TokenRatesController {
this.updateExchangeRates()
}
stop () {
stop() {
this._handle && clearInterval(this._handle)
}
}

View File

@ -1,14 +0,0 @@
const TRANSACTION_TYPE_CANCEL = 'cancel'
const TRANSACTION_TYPE_RETRY = 'retry'
const TRANSACTION_TYPE_STANDARD = 'standard'
const TRANSACTION_STATUS_APPROVED = 'approved'
const TRANSACTION_STATUS_CONFIRMED = 'confirmed'
export {
TRANSACTION_TYPE_CANCEL,
TRANSACTION_TYPE_RETRY,
TRANSACTION_TYPE_STANDARD,
TRANSACTION_STATUS_APPROVED,
TRANSACTION_STATUS_CONFIRMED,
}

View File

@ -9,29 +9,24 @@ import { ethers } from 'ethers'
import NonceTracker from 'nonce-tracker'
import log from 'loglevel'
import BigNumber from 'bignumber.js'
import {
TOKEN_METHOD_APPROVE,
TOKEN_METHOD_TRANSFER,
TOKEN_METHOD_TRANSFER_FROM,
SEND_ETHER_ACTION_KEY,
DEPLOY_CONTRACT_ACTION_KEY,
CONTRACT_INTERACTION_KEY,
SWAP,
} from '../../../../ui/app/helpers/constants/transactions'
import cleanErrorStack from '../../lib/cleanErrorStack'
import { hexToBn, bnToHex, BnMultiplyByFraction } from '../../lib/util'
import {
hexToBn,
bnToHex,
BnMultiplyByFraction,
addHexPrefix,
} from '../../lib/util'
import { TRANSACTION_NO_CONTRACT_ERROR_KEY } from '../../../../ui/app/helpers/constants/error-keys'
import { getSwapsTokensReceivedFromTxMeta } from '../../../../ui/app/pages/swaps/swaps.util'
import {
TRANSACTION_CATEGORIES,
TRANSACTION_STATUSES,
TRANSACTION_TYPES,
} from '../../../../shared/constants/transaction'
import TransactionStateManager from './tx-state-manager'
import TxGasUtil from './tx-gas-utils'
import PendingTransactionTracker from './pending-tx-tracker'
import * as txUtils from './lib/util'
import {
TRANSACTION_TYPE_CANCEL,
TRANSACTION_TYPE_RETRY,
TRANSACTION_TYPE_STANDARD,
TRANSACTION_STATUS_APPROVED,
} from './enums'
const hstInterface = new ethers.utils.Interface(abi)
@ -53,20 +48,20 @@ const MAX_MEMSTORE_TX_LIST_SIZE = 100 // Number of transactions (by unique nonce
calculating nonces
@class
@param {Object} - opts
@param {Object} opts.initState - initial transaction list default is an empty array
@param {Object} opts.networkStore - an observable store for network number
@param {Object} opts.blockTracker - An instance of eth-blocktracker
@param {Object} opts.provider - A network provider.
@param {Function} opts.signTransaction - function the signs an ethereumjs-tx
@param {Object} opts.getPermittedAccounts - get accounts that an origin has permissions for
@param {Function} opts.signTransaction - ethTx signer that returns a rawTx
@param {number} [opts.txHistoryLimit] - number *optional* for limiting how many transactions are in state
@param {Object} opts.preferencesStore
@param {Object} opts
@param {Object} opts.initState - initial transaction list default is an empty array
@param {Object} opts.networkStore - an observable store for network number
@param {Object} opts.blockTracker - An instance of eth-blocktracker
@param {Object} opts.provider - A network provider.
@param {Function} opts.signTransaction - function the signs an ethereumjs-tx
@param {Object} opts.getPermittedAccounts - get accounts that an origin has permissions for
@param {Function} opts.signTransaction - ethTx signer that returns a rawTx
@param {number} [opts.txHistoryLimit] - number *optional* for limiting how many transactions are in state
@param {Object} opts.preferencesStore
*/
export default class TransactionController extends EventEmitter {
constructor (opts) {
constructor(opts) {
super()
this.networkStore = opts.networkStore || new ObservableStore({})
this._getCurrentChainId = opts.getCurrentChainId
@ -95,8 +90,12 @@ export default class TransactionController extends EventEmitter {
this.nonceTracker = new NonceTracker({
provider: this.provider,
blockTracker: this.blockTracker,
getPendingTransactions: this.txStateManager.getPendingTransactions.bind(this.txStateManager),
getConfirmedTransactions: this.txStateManager.getConfirmedTransactions.bind(this.txStateManager),
getPendingTransactions: this.txStateManager.getPendingTransactions.bind(
this.txStateManager,
),
getConfirmedTransactions: this.txStateManager.getConfirmedTransactions.bind(
this.txStateManager,
),
})
this.pendingTxTracker = new PendingTransactionTracker({
@ -109,7 +108,9 @@ export default class TransactionController extends EventEmitter {
return [...pending, ...approved]
},
approveTransaction: this.approveTransaction.bind(this),
getCompletedTransactions: this.txStateManager.getConfirmedTransactions.bind(this.txStateManager),
getCompletedTransactions: this.txStateManager.getConfirmedTransactions.bind(
this.txStateManager,
),
})
this.txStateManager.store.subscribe(() => this.emit('update:badge'))
@ -132,7 +133,7 @@ export default class TransactionController extends EventEmitter {
*
* @returns {number} The numerical chainId.
*/
getChainId () {
getChainId() {
const networkState = this.networkStore.getState()
const chainId = this._getCurrentChainId()
const integerChainId = parseInt(chainId, 16)
@ -146,7 +147,7 @@ export default class TransactionController extends EventEmitter {
Adds a tx to the txlist
@emits ${txMeta.id}:unapproved
*/
addTx (txMeta) {
addTx(txMeta) {
this.txStateManager.addTx(txMeta)
this.emit(`${txMeta.id}:unapproved`, txMeta)
}
@ -155,37 +156,62 @@ export default class TransactionController extends EventEmitter {
Wipes the transactions for a given account
@param {string} address - hex string of the from address for txs being removed
*/
wipeTransactions (address) {
wipeTransactions(address) {
this.txStateManager.wipeTransactions(address)
}
/**
* Add a new unapproved transaction to the pipeline
*
* @returns {Promise<string>} - the hash of the transaction after being submitted to the network
* @param {Object} txParams - txParams for the transaction
* @param {Object} opts - with the key origin to put the origin on the txMeta
*/
async newUnapprovedTransaction (txParams, opts = {}) {
* Add a new unapproved transaction to the pipeline
*
* @returns {Promise<string>} the hash of the transaction after being submitted to the network
* @param {Object} txParams - txParams for the transaction
* @param {Object} opts - with the key origin to put the origin on the txMeta
*/
async newUnapprovedTransaction(txParams, opts = {}) {
log.debug(
`MetaMaskController newUnapprovedTransaction ${JSON.stringify(txParams)}`,
)
log.debug(`MetaMaskController newUnapprovedTransaction ${JSON.stringify(txParams)}`)
const initialTxMeta = await this.addUnapprovedTransaction(txParams, opts.origin)
const initialTxMeta = await this.addUnapprovedTransaction(
txParams,
opts.origin,
)
// listen for tx completion (success, fail)
return new Promise((resolve, reject) => {
this.txStateManager.once(`${initialTxMeta.id}:finished`, (finishedTxMeta) => {
switch (finishedTxMeta.status) {
case 'submitted':
return resolve(finishedTxMeta.hash)
case 'rejected':
return reject(cleanErrorStack(ethErrors.provider.userRejectedRequest('MetaMask Tx Signature: User denied transaction signature.')))
case 'failed':
return reject(cleanErrorStack(ethErrors.rpc.internal(finishedTxMeta.err.message)))
default:
return reject(cleanErrorStack(ethErrors.rpc.internal(`MetaMask Tx Signature: Unknown problem: ${JSON.stringify(finishedTxMeta.txParams)}`)))
}
})
this.txStateManager.once(
`${initialTxMeta.id}:finished`,
(finishedTxMeta) => {
switch (finishedTxMeta.status) {
case TRANSACTION_STATUSES.SUBMITTED:
return resolve(finishedTxMeta.hash)
case TRANSACTION_STATUSES.REJECTED:
return reject(
cleanErrorStack(
ethErrors.provider.userRejectedRequest(
'MetaMask Tx Signature: User denied transaction signature.',
),
),
)
case TRANSACTION_STATUSES.FAILED:
return reject(
cleanErrorStack(
ethErrors.rpc.internal(finishedTxMeta.err.message),
),
)
default:
return reject(
cleanErrorStack(
ethErrors.rpc.internal(
`MetaMask Tx Signature: Unknown problem: ${JSON.stringify(
finishedTxMeta.txParams,
)}`,
),
),
)
}
},
)
})
}
@ -195,8 +221,7 @@ export default class TransactionController extends EventEmitter {
*
* @returns {txMeta}
*/
async addUnapprovedTransaction (txParams, origin) {
async addUnapprovedTransaction(txParams, origin) {
// validate
const normalizedTxParams = txUtils.normalizeTxParams(txParams)
@ -210,7 +235,7 @@ export default class TransactionController extends EventEmitter {
*/
let txMeta = this.txStateManager.generateTxMeta({
txParams: normalizedTxParams,
type: TRANSACTION_TYPE_STANDARD,
type: TRANSACTION_TYPES.STANDARD,
})
if (origin === 'metamask') {
@ -236,12 +261,15 @@ export default class TransactionController extends EventEmitter {
txMeta.origin = origin
const { transactionCategory, getCodeResponse } = await this._determineTransactionCategory(txParams)
const {
transactionCategory,
getCodeResponse,
} = await this._determineTransactionCategory(txParams)
txMeta.transactionCategory = transactionCategory
// ensure value
txMeta.txParams.value = txMeta.txParams.value
? ethUtil.addHexPrefix(txMeta.txParams.value)
? addHexPrefix(txMeta.txParams.value)
: '0x0'
this.addTx(txMeta)
@ -267,11 +295,14 @@ export default class TransactionController extends EventEmitter {
/**
* Adds the tx gas defaults: gas && gasPrice
* @param {Object} txMeta - the txMeta object
* @returns {Promise<object>} - resolves with txMeta
* @returns {Promise<object>} resolves with txMeta
*/
async addTxGasDefaults (txMeta, getCodeResponse) {
async addTxGasDefaults(txMeta, getCodeResponse) {
const defaultGasPrice = await this._getDefaultGasPrice(txMeta)
const { gasLimit: defaultGasLimit, simulationFails } = await this._getDefaultGasLimit(txMeta, getCodeResponse)
const {
gasLimit: defaultGasLimit,
simulationFails,
} = await this._getDefaultGasLimit(txMeta, getCodeResponse)
// eslint-disable-next-line no-param-reassign
txMeta = this.txStateManager.getTx(txMeta.id)
@ -292,13 +323,13 @@ export default class TransactionController extends EventEmitter {
* @param {Object} txMeta - The txMeta object
* @returns {Promise<string|undefined>} The default gas price
*/
async _getDefaultGasPrice (txMeta) {
async _getDefaultGasPrice(txMeta) {
if (txMeta.txParams.gasPrice) {
return undefined
}
const gasPrice = await this.query.gasPrice()
return ethUtil.addHexPrefix(gasPrice.toString(16))
return addHexPrefix(gasPrice.toString(16))
}
/**
@ -307,16 +338,18 @@ export default class TransactionController extends EventEmitter {
* @param {string} getCodeResponse - The transaction category code response, used for debugging purposes
* @returns {Promise<Object>} Object containing the default gas limit, or the simulation failure object
*/
async _getDefaultGasLimit (txMeta, getCodeResponse) {
async _getDefaultGasLimit(txMeta, getCodeResponse) {
if (txMeta.txParams.gas) {
return {}
} else if (
txMeta.txParams.to &&
txMeta.transactionCategory === SEND_ETHER_ACTION_KEY
txMeta.transactionCategory === TRANSACTION_CATEGORIES.SENT_ETHER
) {
// if there's data in the params, but there's no contract code, it's not a valid transaction
if (txMeta.txParams.data) {
const err = new Error('TxGasUtil - Trying to call a function on a non-contract address')
const err = new Error(
'TxGasUtil - Trying to call a function on a non-contract address',
)
// set error key so ui can display localized error message
err.errorKey = TRANSACTION_NO_CONTRACT_ERROR_KEY
@ -329,10 +362,17 @@ export default class TransactionController extends EventEmitter {
return { gasLimit: SIMPLE_GAS_COST }
}
const { blockGasLimit, estimatedGasHex, simulationFails } = await this.txGasUtil.analyzeGasUsage(txMeta)
const {
blockGasLimit,
estimatedGasHex,
simulationFails,
} = await this.txGasUtil.analyzeGasUsage(txMeta)
// add additional gas buffer to our estimation for safety
const gasLimit = this.txGasUtil.addGasBuffer(ethUtil.addHexPrefix(estimatedGasHex), blockGasLimit)
const gasLimit = this.txGasUtil.addGasBuffer(
addHexPrefix(estimatedGasHex),
blockGasLimit,
)
return { gasLimit, simulationFails }
}
@ -344,12 +384,14 @@ export default class TransactionController extends EventEmitter {
* @param {string} [customGasPrice] - the hex value to use for the cancel transaction
* @returns {txMeta}
*/
async createCancelTransaction (originalTxId, customGasPrice) {
async createCancelTransaction(originalTxId, customGasPrice) {
const originalTxMeta = this.txStateManager.getTx(originalTxId)
const { txParams } = originalTxMeta
const { gasPrice: lastGasPrice, from, nonce } = txParams
const newGasPrice = customGasPrice || bnToHex(BnMultiplyByFraction(hexToBn(lastGasPrice), 11, 10))
const newGasPrice =
customGasPrice ||
bnToHex(BnMultiplyByFraction(hexToBn(lastGasPrice), 11, 10))
const newTxMeta = this.txStateManager.generateTxMeta({
txParams: {
from,
@ -361,8 +403,8 @@ export default class TransactionController extends EventEmitter {
},
lastGasPrice,
loadingDefaults: false,
status: TRANSACTION_STATUS_APPROVED,
type: TRANSACTION_TYPE_CANCEL,
status: TRANSACTION_STATUSES.APPROVED,
type: TRANSACTION_TYPES.CANCEL,
})
this.addTx(newTxMeta)
@ -380,12 +422,14 @@ export default class TransactionController extends EventEmitter {
* @param {string} [customGasLimit] - The new custom gas limt, in hex
* @returns {txMeta}
*/
async createSpeedUpTransaction (originalTxId, customGasPrice, customGasLimit) {
async createSpeedUpTransaction(originalTxId, customGasPrice, customGasLimit) {
const originalTxMeta = this.txStateManager.getTx(originalTxId)
const { txParams } = originalTxMeta
const { gasPrice: lastGasPrice } = txParams
const newGasPrice = customGasPrice || bnToHex(BnMultiplyByFraction(hexToBn(lastGasPrice), 11, 10))
const newGasPrice =
customGasPrice ||
bnToHex(BnMultiplyByFraction(hexToBn(lastGasPrice), 11, 10))
const newTxMeta = this.txStateManager.generateTxMeta({
txParams: {
@ -394,8 +438,8 @@ export default class TransactionController extends EventEmitter {
},
lastGasPrice,
loadingDefaults: false,
status: TRANSACTION_STATUS_APPROVED,
type: TRANSACTION_TYPE_RETRY,
status: TRANSACTION_STATUSES.APPROVED,
type: TRANSACTION_TYPES.RETRY,
})
if (customGasLimit) {
@ -411,7 +455,7 @@ export default class TransactionController extends EventEmitter {
updates the txMeta in the txStateManager
@param {Object} txMeta - the updated txMeta
*/
async updateTransaction (txMeta) {
async updateTransaction(txMeta) {
this.txStateManager.updateTx(txMeta, 'confTx: user updated transaction')
}
@ -419,7 +463,7 @@ export default class TransactionController extends EventEmitter {
updates and approves the transaction
@param {Object} txMeta
*/
async updateAndApproveTransaction (txMeta) {
async updateAndApproveTransaction(txMeta) {
this.txStateManager.updateTx(txMeta, 'confTx: user approved transaction')
await this.approveTransaction(txMeta.id)
}
@ -432,7 +476,7 @@ export default class TransactionController extends EventEmitter {
if any of these steps fails the tx status will be set to failed
@param {number} txId - the tx's Id
*/
async approveTransaction (txId) {
async approveTransaction(txId) {
// TODO: Move this safety out of this function.
// Since this transaction is async,
// we need to keep track of what is currently being signed,
@ -456,10 +500,13 @@ export default class TransactionController extends EventEmitter {
// add nonce to txParams
// if txMeta has lastGasPrice then it is a retry at same nonce with higher
// gas price transaction and their for the nonce should not be calculated
const nonce = txMeta.lastGasPrice ? txMeta.txParams.nonce : nonceLock.nextNonce
const customOrNonce = (customNonceValue === 0) ? customNonceValue : customNonceValue || nonce
const nonce = txMeta.lastGasPrice
? txMeta.txParams.nonce
: nonceLock.nextNonce
const customOrNonce =
customNonceValue === 0 ? customNonceValue : customNonceValue || nonce
txMeta.txParams.nonce = ethUtil.addHexPrefix(customOrNonce.toString(16))
txMeta.txParams.nonce = addHexPrefix(customOrNonce.toString(16))
// add nonce debugging information to txMeta
txMeta.nonceDetails = nonceLock.nonceDetails
if (customNonceValue) {
@ -492,9 +539,9 @@ export default class TransactionController extends EventEmitter {
/**
adds the chain id and signs the transaction and set the status to signed
@param {number} txId - the tx's Id
@returns {string} - rawTx
@returns {string} rawTx
*/
async signTransaction (txId) {
async signTransaction(txId) {
const txMeta = this.txStateManager.getTx(txId)
// add network/chain id
const chainId = this.getChainId()
@ -510,7 +557,10 @@ export default class TransactionController extends EventEmitter {
txMeta.s = ethUtil.bufferToHex(ethTx.s)
txMeta.v = ethUtil.bufferToHex(ethTx.v)
this.txStateManager.updateTx(txMeta, 'transactions#signTransaction: add r, s, v values')
this.txStateManager.updateTx(
txMeta,
'transactions#signTransaction: add r, s, v values',
)
// set state to signed
this.txStateManager.setTxStatusSigned(txMeta.id)
@ -524,10 +574,10 @@ export default class TransactionController extends EventEmitter {
@param {string} rawTx - the hex string of the serialized signed transaction
@returns {Promise<void>}
*/
async publishTransaction (txId, rawTx) {
async publishTransaction(txId, rawTx) {
const txMeta = this.txStateManager.getTx(txId)
txMeta.rawTx = rawTx
if (txMeta.transactionCategory === SWAP) {
if (txMeta.transactionCategory === TRANSACTION_CATEGORIES.SWAP) {
const preTxBalance = await this.query.getBalance(txMeta.txParams.from)
txMeta.preTxBalance = preTxBalance.toString(16)
}
@ -537,8 +587,8 @@ export default class TransactionController extends EventEmitter {
txHash = await this.query.sendRawTransaction(rawTx)
} catch (error) {
if (error.message.toLowerCase().includes('known transaction')) {
txHash = ethUtil.sha3(ethUtil.addHexPrefix(rawTx)).toString('hex')
txHash = ethUtil.addHexPrefix(txHash)
txHash = ethUtil.sha3(addHexPrefix(rawTx)).toString('hex')
txHash = addHexPrefix(txHash)
} else {
throw error
}
@ -554,7 +604,7 @@ export default class TransactionController extends EventEmitter {
* @param {number} txId - The tx's ID
* @returns {Promise<void>}
*/
async confirmTransaction (txId, txReceipt) {
async confirmTransaction(txId, txReceipt) {
// get the txReceipt before marking the transaction confirmed
// to ensure the receipt is gotten before the ui revives the tx
const txMeta = this.txStateManager.getTx(txId)
@ -566,9 +616,10 @@ export default class TransactionController extends EventEmitter {
try {
// It seems that sometimes the numerical values being returned from
// this.query.getTransactionReceipt are BN instances and not strings.
const gasUsed = typeof txReceipt.gasUsed === 'string'
? txReceipt.gasUsed
: txReceipt.gasUsed.toString(16)
const gasUsed =
typeof txReceipt.gasUsed === 'string'
? txReceipt.gasUsed
: txReceipt.gasUsed.toString(16)
txMeta.txReceipt = {
...txReceipt,
@ -577,9 +628,12 @@ export default class TransactionController extends EventEmitter {
this.txStateManager.setTxStatusConfirmed(txId)
this._markNonceDuplicatesDropped(txId)
this.txStateManager.updateTx(txMeta, 'transactions#confirmTransaction - add txReceipt')
this.txStateManager.updateTx(
txMeta,
'transactions#confirmTransaction - add txReceipt',
)
if (txMeta.transactionCategory === SWAP) {
if (txMeta.transactionCategory === TRANSACTION_CATEGORIES.SWAP) {
const postTxBalance = await this.query.getBalance(txMeta.txParams.from)
const latestTxMeta = this.txStateManager.getTx(txId)
@ -589,11 +643,13 @@ export default class TransactionController extends EventEmitter {
latestTxMeta.postTxBalance = postTxBalance.toString(16)
this.txStateManager.updateTx(latestTxMeta, 'transactions#confirmTransaction - add postTxBalance')
this.txStateManager.updateTx(
latestTxMeta,
'transactions#confirmTransaction - add postTxBalance',
)
this._trackSwapsMetrics(latestTxMeta, approvalTxMeta)
}
} catch (err) {
log.error(err)
}
@ -604,7 +660,7 @@ export default class TransactionController extends EventEmitter {
@param {number} txId - the tx's Id
@returns {Promise<void>}
*/
async cancelTransaction (txId) {
async cancelTransaction(txId) {
this.txStateManager.setTxStatusRejected(txId)
}
@ -613,7 +669,7 @@ export default class TransactionController extends EventEmitter {
@param {number} txId - the tx's Id
@param {string} txHash - the hash for the txMeta
*/
setTxHash (txId, txHash) {
setTxHash(txId, txHash) {
// Add the tx hash to the persisted meta-tx object
const txMeta = this.txStateManager.getTx(txId)
txMeta.hash = txHash
@ -624,32 +680,35 @@ export default class TransactionController extends EventEmitter {
// PRIVATE METHODS
//
/** maps methods for convenience*/
_mapMethods () {
/** @returns {Object} - the state in transaction controller */
_mapMethods() {
/** @returns {Object} the state in transaction controller */
this.getState = () => this.memStore.getState()
/** @returns {string|number} - the network number stored in networkStore */
/** @returns {string|number} the network number stored in networkStore */
this.getNetwork = () => this.networkStore.getState()
/** @returns {string} - the user selected address */
this.getSelectedAddress = () => this.preferencesStore.getState().selectedAddress
/** @returns {string} the user selected address */
this.getSelectedAddress = () =>
this.preferencesStore.getState().selectedAddress
/** @returns {array} - transactions whos status is unapproved */
this.getUnapprovedTxCount = () => Object.keys(this.txStateManager.getUnapprovedTxList()).length
/** @returns {Array} transactions whos status is unapproved */
this.getUnapprovedTxCount = () =>
Object.keys(this.txStateManager.getUnapprovedTxList()).length
/**
@returns {number} - number of transactions that have the status submitted
@returns {number} number of transactions that have the status submitted
@param {string} account - hex prefixed account
*/
this.getPendingTxCount = (account) => this.txStateManager.getPendingTransactions(account).length
this.getPendingTxCount = (account) =>
this.txStateManager.getPendingTransactions(account).length
/** see txStateManager */
this.getFilteredTxList = (opts) => this.txStateManager.getFilteredTxList(opts)
this.getFilteredTxList = (opts) =>
this.txStateManager.getFilteredTxList(opts)
}
// called once on startup
async _updatePendingTxsAfterFirstBlock () {
async _updatePendingTxsAfterFirstBlock() {
// wait for first block so we know we're ready
await this.blockTracker.getLatestBlock()
// get status update for all pending transactions (for the current network)
@ -662,49 +721,78 @@ export default class TransactionController extends EventEmitter {
transition txMetas to a failed state or try to redo those tasks.
*/
_onBootCleanUp () {
this.txStateManager.getFilteredTxList({
status: 'unapproved',
loadingDefaults: true,
}).forEach((tx) => {
_onBootCleanUp() {
this.txStateManager
.getFilteredTxList({
status: TRANSACTION_STATUSES.UNAPPROVED,
loadingDefaults: true,
})
.forEach((tx) => {
this.addTxGasDefaults(tx)
.then((txMeta) => {
txMeta.loadingDefaults = false
this.txStateManager.updateTx(
txMeta,
'transactions: gas estimation for tx on boot',
)
})
.catch((error) => {
const txMeta = this.txStateManager.getTx(tx.id)
txMeta.loadingDefaults = false
this.txStateManager.updateTx(
txMeta,
'failed to estimate gas during boot cleanup.',
)
this.txStateManager.setTxStatusFailed(txMeta.id, error)
})
})
this.addTxGasDefaults(tx)
.then((txMeta) => {
txMeta.loadingDefaults = false
this.txStateManager.updateTx(txMeta, 'transactions: gas estimation for tx on boot')
}).catch((error) => {
const txMeta = this.txStateManager.getTx(tx.id)
txMeta.loadingDefaults = false
this.txStateManager.updateTx(txMeta, 'failed to estimate gas during boot cleanup.')
this.txStateManager.setTxStatusFailed(txMeta.id, error)
})
})
this.txStateManager.getFilteredTxList({
status: TRANSACTION_STATUS_APPROVED,
}).forEach((txMeta) => {
const txSignError = new Error('Transaction found as "approved" during boot - possibly stuck during signing')
this.txStateManager.setTxStatusFailed(txMeta.id, txSignError)
})
this.txStateManager
.getFilteredTxList({
status: TRANSACTION_STATUSES.APPROVED,
})
.forEach((txMeta) => {
const txSignError = new Error(
'Transaction found as "approved" during boot - possibly stuck during signing',
)
this.txStateManager.setTxStatusFailed(txMeta.id, txSignError)
})
}
/**
is called in constructor applies the listeners for pendingTxTracker txStateManager
and blockTracker
*/
_setupListeners () {
this.txStateManager.on('tx:status-update', this.emit.bind(this, 'tx:status-update'))
_setupListeners() {
this.txStateManager.on(
'tx:status-update',
this.emit.bind(this, 'tx:status-update'),
)
this._setupBlockTrackerListener()
this.pendingTxTracker.on('tx:warning', (txMeta) => {
this.txStateManager.updateTx(txMeta, 'transactions/pending-tx-tracker#event: tx:warning')
this.txStateManager.updateTx(
txMeta,
'transactions/pending-tx-tracker#event: tx:warning',
)
})
this.pendingTxTracker.on('tx:failed', this.txStateManager.setTxStatusFailed.bind(this.txStateManager))
this.pendingTxTracker.on('tx:confirmed', (txId, transactionReceipt) => this.confirmTransaction(txId, transactionReceipt))
this.pendingTxTracker.on('tx:dropped', this.txStateManager.setTxStatusDropped.bind(this.txStateManager))
this.pendingTxTracker.on(
'tx:failed',
this.txStateManager.setTxStatusFailed.bind(this.txStateManager),
)
this.pendingTxTracker.on('tx:confirmed', (txId, transactionReceipt) =>
this.confirmTransaction(txId, transactionReceipt),
)
this.pendingTxTracker.on(
'tx:dropped',
this.txStateManager.setTxStatusDropped.bind(this.txStateManager),
)
this.pendingTxTracker.on('tx:block-update', (txMeta, latestBlockNumber) => {
if (!txMeta.firstRetryBlockNumber) {
txMeta.firstRetryBlockNumber = latestBlockNumber
this.txStateManager.updateTx(txMeta, 'transactions/pending-tx-tracker#event: tx:block-update')
this.txStateManager.updateTx(
txMeta,
'transactions/pending-tx-tracker#event: tx:block-update',
)
}
})
this.pendingTxTracker.on('tx:retry', (txMeta) => {
@ -712,7 +800,10 @@ export default class TransactionController extends EventEmitter {
txMeta.retryCount = 0
}
txMeta.retryCount += 1
this.txStateManager.updateTx(txMeta, 'transactions/pending-tx-tracker#event: tx:retry')
this.txStateManager.updateTx(
txMeta,
'transactions/pending-tx-tracker#event: tx:retry',
)
})
}
@ -720,7 +811,7 @@ export default class TransactionController extends EventEmitter {
Returns a "type" for a transaction out of the following list: simpleSend, tokenTransfer, tokenApprove,
contractDeployment, contractMethodCall
*/
async _determineTransactionCategory (txParams) {
async _determineTransactionCategory(txParams) {
const { data, to } = txParams
let name
try {
@ -730,16 +821,16 @@ export default class TransactionController extends EventEmitter {
}
const tokenMethodName = [
TOKEN_METHOD_APPROVE,
TOKEN_METHOD_TRANSFER,
TOKEN_METHOD_TRANSFER_FROM,
TRANSACTION_CATEGORIES.TOKEN_METHOD_APPROVE,
TRANSACTION_CATEGORIES.TOKEN_METHOD_TRANSFER,
TRANSACTION_CATEGORIES.TOKEN_METHOD_TRANSFER_FROM,
].find((methodName) => methodName === name && name.toLowerCase())
let result
if (txParams.data && tokenMethodName) {
result = tokenMethodName
} else if (txParams.data && !to) {
result = DEPLOY_CONTRACT_ACTION_KEY
result = TRANSACTION_CATEGORIES.DEPLOY_CONTRACT
}
let code
@ -753,7 +844,9 @@ export default class TransactionController extends EventEmitter {
const codeIsEmpty = !code || code === '0x' || code === '0x0'
result = codeIsEmpty ? SEND_ETHER_ACTION_KEY : CONTRACT_INTERACTION_KEY
result = codeIsEmpty
? TRANSACTION_CATEGORIES.SENT_ETHER
: TRANSACTION_CATEGORIES.CONTRACT_INTERACTION
}
return { transactionCategory: result, getCodeResponse: code }
@ -765,7 +858,7 @@ export default class TransactionController extends EventEmitter {
@param {number} txId - the txId of the transaction that has been confirmed in a block
*/
_markNonceDuplicatesDropped (txId) {
_markNonceDuplicatesDropped(txId) {
// get the confirmed transactions nonce and from address
const txMeta = this.txStateManager.getTx(txId)
const { nonce, from } = txMeta.txParams
@ -779,12 +872,15 @@ export default class TransactionController extends EventEmitter {
return
}
otherTxMeta.replacedBy = txMeta.hash
this.txStateManager.updateTx(txMeta, 'transactions/pending-tx-tracker#event: tx:confirmed reference to confirmed txHash with same nonce')
this.txStateManager.updateTx(
txMeta,
'transactions/pending-tx-tracker#event: tx:confirmed reference to confirmed txHash with same nonce',
)
this.txStateManager.setTxStatusDropped(otherTxMeta.id)
})
}
_setupBlockTrackerListener () {
_setupBlockTrackerListener() {
let listenersAreActive = false
const latestBlockHandler = this._onLatestBlock.bind(this)
const { blockTracker, txStateManager } = this
@ -792,7 +888,7 @@ export default class TransactionController extends EventEmitter {
txStateManager.on('tx:status-update', updateSubscription)
updateSubscription()
function updateSubscription () {
function updateSubscription() {
const pendingTxs = txStateManager.getPendingTransactions()
if (!listenersAreActive && pendingTxs.length > 0) {
blockTracker.on('latest', latestBlockHandler)
@ -804,7 +900,7 @@ export default class TransactionController extends EventEmitter {
}
}
async _onLatestBlock (blockNumber) {
async _onLatestBlock(blockNumber) {
try {
await this.pendingTxTracker.updatePendingTxs()
} catch (err) {
@ -820,13 +916,15 @@ export default class TransactionController extends EventEmitter {
/**
Updates the memStore in transaction controller
*/
_updateMemstore () {
_updateMemstore() {
const unapprovedTxs = this.txStateManager.getUnapprovedTxList()
const currentNetworkTxList = this.txStateManager.getTxList(MAX_MEMSTORE_TX_LIST_SIZE)
const currentNetworkTxList = this.txStateManager.getTxList(
MAX_MEMSTORE_TX_LIST_SIZE,
)
this.memStore.updateState({ unapprovedTxs, currentNetworkTxList })
}
_trackSwapsMetrics (txMeta, approvalTxMeta) {
_trackSwapsMetrics(txMeta, approvalTxMeta) {
if (this._getParticipateInMetrics() && txMeta.swapMetaData) {
if (txMeta.txReceipt.status === '0x0') {
this._trackMetaMetricsEvent({
@ -851,19 +949,18 @@ export default class TransactionController extends EventEmitter {
approvalTxMeta,
)
const quoteVsExecutionRatio = `${
(new BigNumber(tokensReceived, 10))
.div(txMeta.swapMetaData.token_to_amount, 10)
.times(100)
.round(2)
}%`
const quoteVsExecutionRatio = `${new BigNumber(tokensReceived, 10)
.div(txMeta.swapMetaData.token_to_amount, 10)
.times(100)
.round(2)}%`
const estimatedVsUsedGasRatio = `${
(new BigNumber(txMeta.txReceipt.gasUsed, 16))
.div(txMeta.swapMetaData.estimated_gas, 10)
.times(100)
.round(2)
}%`
const estimatedVsUsedGasRatio = `${new BigNumber(
txMeta.txReceipt.gasUsed,
16,
)
.div(txMeta.swapMetaData.estimated_gas, 10)
.times(100)
.round(2)}%`
this._trackMetaMetricsEvent({
event: 'Swap Completed',

View File

@ -3,13 +3,13 @@ import { cloneDeep } from 'lodash'
/**
converts non-initial history entries into diffs
@param {array} longHistory
@returns {array}
@param {Array} longHistory
@returns {Array}
*/
export function migrateFromSnapshotsToDiffs (longHistory) {
export function migrateFromSnapshotsToDiffs(longHistory) {
return (
longHistory
// convert non-initial history entries into diffs
// convert non-initial history entries into diffs
.map((entry, index) => {
if (index === 0) {
return entry
@ -29,9 +29,9 @@ export function migrateFromSnapshotsToDiffs (longHistory) {
@param {Object} previousState - the previous state of the object
@param {Object} newState - the update object
@param {string} [note] - a optional note for the state change
@returns {array}
@returns {Array}
*/
export function generateHistoryEntry (previousState, newState, note) {
export function generateHistoryEntry(previousState, newState, note) {
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]) {
@ -48,9 +48,11 @@ export function generateHistoryEntry (previousState, newState, note) {
Recovers previous txMeta state obj
@returns {Object}
*/
export function replayHistory (_shortHistory) {
export function replayHistory(_shortHistory) {
const shortHistory = cloneDeep(_shortHistory)
return shortHistory.reduce((val, entry) => jsonDiffer.applyPatch(val, entry).newDocument)
return shortHistory.reduce(
(val, entry) => jsonDiffer.applyPatch(val, entry).newDocument,
)
}
/**
@ -58,7 +60,7 @@ export function replayHistory (_shortHistory) {
* @param {Object} txMeta - the tx metadata object
* @returns {Object} a deep clone without history
*/
export function snapshotFromTxMeta (txMeta) {
export function snapshotFromTxMeta(txMeta) {
const shallow = { ...txMeta }
delete shallow.history
return cloneDeep(shallow)

View File

@ -1,8 +1,11 @@
import { addHexPrefix, isValidAddress } from 'ethereumjs-util'
import { isValidAddress } from 'ethereumjs-util'
import { addHexPrefix } from '../../../lib/util'
import { TRANSACTION_STATUSES } from '../../../../../shared/constants/transaction'
const normalizers = {
from: (from) => addHexPrefix(from),
to: (to, lowerCase) => (lowerCase ? addHexPrefix(to).toLowerCase() : addHexPrefix(to)),
to: (to, lowerCase) =>
lowerCase ? addHexPrefix(to).toLowerCase() : addHexPrefix(to),
nonce: (nonce) => addHexPrefix(nonce),
value: (value) => addHexPrefix(value),
data: (data) => addHexPrefix(data),
@ -17,7 +20,7 @@ const normalizers = {
* Default: true
* @returns {Object} the normalized tx params
*/
export function normalizeTxParams (txParams, lowerCase = true) {
export function normalizeTxParams(txParams, lowerCase = true) {
// apply only keys in the normalizers
const normalizedTxParams = {}
for (const key in normalizers) {
@ -33,17 +36,21 @@ export function normalizeTxParams (txParams, lowerCase = true) {
* @param {Object} txParams - the tx params
* @throws {Error} if the tx params contains invalid fields
*/
export function validateTxParams (txParams) {
export function validateTxParams(txParams) {
validateFrom(txParams)
validateRecipient(txParams)
if ('value' in txParams) {
const value = txParams.value.toString()
if (value.includes('-')) {
throw new Error(`Invalid transaction value of ${txParams.value} not a positive number.`)
throw new Error(
`Invalid transaction value of ${txParams.value} not a positive number.`,
)
}
if (value.includes('.')) {
throw new Error(`Invalid transaction value of ${txParams.value} number must be in wei`)
throw new Error(
`Invalid transaction value of ${txParams.value} number must be in wei`,
)
}
}
}
@ -53,7 +60,7 @@ export function validateTxParams (txParams) {
* @param {Object} txParams
* @throws {Error} if the from address isn't valid
*/
export function validateFrom (txParams) {
export function validateFrom(txParams) {
if (!(typeof txParams.from === 'string')) {
throw new Error(`Invalid from address ${txParams.from} not a string`)
}
@ -68,7 +75,7 @@ export function validateFrom (txParams) {
* @returns {Object} the tx params
* @throws {Error} if the recipient is invalid OR there isn't tx data
*/
export function validateRecipient (txParams) {
export function validateRecipient(txParams) {
if (txParams.to === '0x' || txParams.to === null) {
if (txParams.data) {
delete txParams.to
@ -85,11 +92,11 @@ export function validateRecipient (txParams) {
* Returns a list of final states
* @returns {string[]} the states that can be considered final states
*/
export function getFinalStates () {
export function getFinalStates() {
return [
'rejected', // the user has responded no!
'confirmed', // the tx has been included in a block.
'failed', // the tx failed for some reason, included on tx data.
'dropped', // the tx nonce was already used
TRANSACTION_STATUSES.REJECTED, // the user has responded no!
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
]
}

View File

@ -1,6 +1,7 @@
import EventEmitter from 'safe-event-emitter'
import log from 'loglevel'
import EthQuery from 'ethjs-query'
import { TRANSACTION_STATUSES } from '../../../../shared/constants/transaction'
/**
@ -11,15 +12,14 @@ import EthQuery from 'ethjs-query'
<br>
@param {Object} config - non optional configuration object consists of:
@param {Object} config.provider - A network provider.
@param {Object} config.nonceTracker see nonce tracker
@param {function} config.getPendingTransactions a function for getting an array of transactions,
@param {function} config.publishTransaction a async function for publishing raw transactions,
@param {Object} config.nonceTracker - see nonce tracker
@param {Function} config.getPendingTransactions - a function for getting an array of transactions,
@param {Function} config.publishTransaction - a async function for publishing raw transactions,
@class
*/
export default class PendingTransactionTracker extends EventEmitter {
/**
* We wait this many blocks before emitting a 'tx:dropped' event
*
@ -37,9 +37,9 @@ export default class PendingTransactionTracker extends EventEmitter {
*/
droppedBlocksBufferByHash = new Map()
constructor (config) {
constructor(config) {
super()
this.query = config.query || (new EthQuery(config.provider))
this.query = config.query || new EthQuery(config.provider)
this.nonceTracker = config.nonceTracker
this.getPendingTransactions = config.getPendingTransactions
this.getCompletedTransactions = config.getCompletedTransactions
@ -51,14 +51,18 @@ export default class PendingTransactionTracker extends EventEmitter {
/**
checks the network for signed txs and releases the nonce global lock if it is
*/
async updatePendingTxs () {
async updatePendingTxs() {
// in order to keep the nonceTracker accurate we block it while updating pending transactions
const nonceGlobalLock = await this.nonceTracker.getGlobalLock()
try {
const pendingTxs = this.getPendingTransactions()
await Promise.all(pendingTxs.map((txMeta) => this._checkPendingTx(txMeta)))
await Promise.all(
pendingTxs.map((txMeta) => this._checkPendingTx(txMeta)),
)
} catch (err) {
log.error('PendingTransactionTracker - Error updating pending transactions')
log.error(
'PendingTransactionTracker - Error updating pending transactions',
)
log.error(err)
}
nonceGlobalLock.releaseLock()
@ -70,7 +74,7 @@ export default class PendingTransactionTracker extends EventEmitter {
* @emits tx:warning
* @returns {Promise<void>}
*/
async resubmitPendingTxs (blockNumber) {
async resubmitPendingTxs(blockNumber) {
const pending = this.getPendingTransactions()
if (!pending.length) {
return
@ -79,18 +83,20 @@ export default class PendingTransactionTracker extends EventEmitter {
try {
await this._resubmitTx(txMeta, blockNumber)
} catch (err) {
const errorMessage = err.value?.message?.toLowerCase() || err.message.toLowerCase()
const isKnownTx = (
const errorMessage =
err.value?.message?.toLowerCase() || err.message.toLowerCase()
const isKnownTx =
// geth
errorMessage.includes('replacement transaction underpriced') ||
errorMessage.includes('known transaction') ||
// parity
errorMessage.includes('gas price too low to replace') ||
errorMessage.includes('transaction with the same hash was already imported') ||
errorMessage.includes(
'transaction with the same hash was already imported',
) ||
// other
errorMessage.includes('gateway timeout') ||
errorMessage.includes('nonce too low')
)
// ignore resubmit warnings, return early
if (isKnownTx) {
return
@ -117,13 +123,16 @@ export default class PendingTransactionTracker extends EventEmitter {
* @emits tx:retry
* @private
*/
async _resubmitTx (txMeta, latestBlockNumber) {
async _resubmitTx(txMeta, latestBlockNumber) {
if (!txMeta.firstRetryBlockNumber) {
this.emit('tx:block-update', txMeta, latestBlockNumber)
}
const firstRetryBlockNumber = txMeta.firstRetryBlockNumber || latestBlockNumber
const txBlockDistance = Number.parseInt(latestBlockNumber, 16) - Number.parseInt(firstRetryBlockNumber, 16)
const firstRetryBlockNumber =
txMeta.firstRetryBlockNumber || latestBlockNumber
const txBlockDistance =
Number.parseInt(latestBlockNumber, 16) -
Number.parseInt(firstRetryBlockNumber, 16)
const retryCount = txMeta.retryCount || 0
@ -155,19 +164,21 @@ export default class PendingTransactionTracker extends EventEmitter {
* @emits tx:warning
* @private
*/
async _checkPendingTx (txMeta) {
async _checkPendingTx(txMeta) {
const txHash = txMeta.hash
const txId = txMeta.id
// Only check submitted txs
if (txMeta.status !== 'submitted') {
if (txMeta.status !== TRANSACTION_STATUSES.SUBMITTED) {
return
}
// extra check in case there was an uncaught error during the
// signature and submission process
if (!txHash) {
const noTxHashErr = new Error('We had an error while submitting this transaction, please try again.')
const noTxHashErr = new Error(
'We had an error while submitting this transaction, please try again.',
)
noTxHashErr.name = 'NoTxHashError'
this.emit('tx:failed', txId, noTxHashErr)
@ -206,8 +217,11 @@ export default class PendingTransactionTracker extends EventEmitter {
* @returns {Promise<boolean>}
* @private
*/
async _checkIfTxWasDropped (txMeta) {
const { hash: txHash, txParams: { nonce, from } } = txMeta
async _checkIfTxWasDropped(txMeta) {
const {
hash: txHash,
txParams: { nonce, from },
} = txMeta
const networkNextNonce = await this.query.getTransactionCount(from)
if (parseInt(nonce, 16) >= networkNextNonce.toNumber()) {
@ -235,14 +249,16 @@ export default class PendingTransactionTracker extends EventEmitter {
* @returns {Promise<boolean>}
* @private
*/
async _checkIfNonceIsTaken (txMeta) {
async _checkIfNonceIsTaken(txMeta) {
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
// and if that is the case, don't consider the transaction to have taken its own nonce
(other) => !(other.id === txMeta.id) && other.txParams.nonce === txMeta.txParams.nonce,
(other) =>
!(other.id === txMeta.id) &&
other.txParams.nonce === txMeta.txParams.nonce,
)
}
}

View File

@ -20,8 +20,7 @@ and used to do things like calculate gas of a tx.
*/
export default class TxGasUtil {
constructor (provider) {
constructor(provider) {
this.query = new EthQuery(provider)
}
@ -29,7 +28,7 @@ export default class TxGasUtil {
@param {Object} txMeta - the txMeta object
@returns {GasAnalysisResult} The result of the gas analysis
*/
async analyzeGasUsage (txMeta) {
async analyzeGasUsage(txMeta) {
const block = await this.query.getBlockByNumber('latest', false)
// fallback to block gasLimit
@ -54,9 +53,9 @@ export default class TxGasUtil {
/**
Estimates the tx's gas usage
@param {Object} txMeta - the txMeta object
@returns {string} - the estimated gas limit as a hex string
@returns {string} the estimated gas limit as a hex string
*/
async estimateTxGas (txMeta) {
async estimateTxGas(txMeta) {
const { txParams } = txMeta
// estimate tx gas requirements
@ -68,9 +67,9 @@ export default class TxGasUtil {
@param {string} initialGasLimitHex - the initial gas limit to add the buffer too
@param {string} blockGasLimitHex - the block gas limit
@returns {string} - the buffered gas limit as a hex string
@returns {string} the buffered gas limit as a hex string
*/
addGasBuffer (initialGasLimitHex, blockGasLimitHex, multiplier = 1.5) {
addGasBuffer(initialGasLimitHex, blockGasLimitHex, multiplier = 1.5) {
const initialGasLimitBn = hexToBn(initialGasLimitHex)
const blockGasLimitBn = hexToBn(blockGasLimitHex)
const upperGasLimitBn = blockGasLimitBn.muln(0.9)
@ -88,11 +87,19 @@ export default class TxGasUtil {
return bnToHex(upperGasLimitBn)
}
async getBufferedGasLimit (txMeta, multiplier) {
const { blockGasLimit, estimatedGasHex, simulationFails } = await this.analyzeGasUsage(txMeta)
async getBufferedGasLimit(txMeta, multiplier) {
const {
blockGasLimit,
estimatedGasHex,
simulationFails,
} = await this.analyzeGasUsage(txMeta)
// add additional gas buffer to our estimation for safety
const gasLimit = this.addGasBuffer(ethUtil.addHexPrefix(estimatedGasHex), blockGasLimit, multiplier)
const gasLimit = this.addGasBuffer(
ethUtil.addHexPrefix(estimatedGasHex),
blockGasLimit,
multiplier,
)
return { gasLimit, simulationFails }
}
}

View File

@ -2,57 +2,55 @@ import EventEmitter from 'safe-event-emitter'
import ObservableStore from 'obs-store'
import log from 'loglevel'
import createId from '../../lib/random-id'
import { generateHistoryEntry, replayHistory, snapshotFromTxMeta } from './lib/tx-state-history-helpers'
import { TRANSACTION_STATUSES } from '../../../../shared/constants/transaction'
import {
generateHistoryEntry,
replayHistory,
snapshotFromTxMeta,
} from './lib/tx-state-history-helpers'
import { getFinalStates, normalizeTxParams } from './lib/util'
/**
TransactionStateManager is responsible for the state of a transaction and
storing the transaction
it also has some convenience methods for finding subsets of transactions
*
*STATUS METHODS
<br>statuses:
<br> - `'unapproved'` the user has not responded
<br> - `'rejected'` the user has responded no!
<br> - `'approved'` the user has approved the tx
<br> - `'signed'` the tx is signed
<br> - `'submitted'` the tx is sent to a server
<br> - `'confirmed'` the tx has been included in a block.
<br> - `'failed'` the tx failed for some reason, included on tx data.
<br> - `'dropped'` the tx nonce was already used
@param {Object} opts
@param {Object} [opts.initState={ transactions: [] }] initial transactions list with the key transaction {array}
@param {number} [opts.txHistoryLimit] limit for how many finished
transactions can hang around in state
@param {function} opts.getNetwork return network number
@class
*/
* TransactionStatuses reimported from the shared transaction constants file
* @typedef {import('../../../../shared/constants/transaction').TransactionStatuses} TransactionStatuses
*/
/**
* TransactionStateManager is responsible for the state of a transaction and
* storing the transaction. It also has some convenience methods for finding
* subsets of transactions.
* @param {Object} opts
* @param {Object} [opts.initState={ transactions: [] }] - initial transactions list with the key transaction {Array}
* @param {number} [opts.txHistoryLimit] - limit for how many finished
* transactions can hang around in state
* @param {Function} opts.getNetwork - return network number
* @class
*/
export default class TransactionStateManager extends EventEmitter {
constructor ({ initState, txHistoryLimit, getNetwork }) {
constructor({ initState, txHistoryLimit, getNetwork }) {
super()
this.store = new ObservableStore(
{ transactions: [], ...initState },
)
this.store = new ObservableStore({ transactions: [], ...initState })
this.txHistoryLimit = txHistoryLimit
this.getNetwork = getNetwork
}
/**
@param {Object} opts - the object to use when overwriting defaults
@returns {txMeta} - the default txMeta object
*/
generateTxMeta (opts) {
* @param {Object} opts - the object to use when overwriting defaults
* @returns {txMeta} the default txMeta object
*/
generateTxMeta(opts) {
const netId = this.getNetwork()
if (netId === 'loading') {
throw new Error('MetaMask is having trouble connecting to the network')
}
return {
id: createId(),
time: (new Date()).getTime(),
status: 'unapproved',
time: new Date().getTime(),
status: TRANSACTION_STATUSES.UNAPPROVED,
metamaskNetworkId: netId,
loadingDefaults: true, ...opts,
loadingDefaults: true,
...opts,
}
}
@ -61,10 +59,10 @@ export default class TransactionStateManager extends EventEmitter {
*
* The list is iterated backwards as new transactions are pushed onto it.
*
* @param {number} [limit] a limit for the number of transactions to return
* @param {number} [limit] - a limit for the number of transactions to return
* @returns {Object[]} The {@code txMeta}s, filtered to the current network
*/
getTxList (limit) {
getTxList(limit) {
const network = this.getNetwork()
const fullTxList = this.getFullTxList()
@ -93,17 +91,20 @@ export default class TransactionStateManager extends EventEmitter {
}
/**
@returns {array} - of all the txMetas in store
*/
getFullTxList () {
* @returns {Array} of all the txMetas in store
*/
getFullTxList() {
return this.store.getState().transactions
}
/**
@returns {array} - the tx list whose status is unapproved
*/
getUnapprovedTxList () {
const txList = this.getTxsByMetaData('status', 'unapproved')
* @returns {Array} the tx list with unapproved status
*/
getUnapprovedTxList() {
const txList = this.getTxsByMetaData(
'status',
TRANSACTION_STATUSES.UNAPPROVED,
)
return txList.reduce((result, tx) => {
result[tx.id] = tx
return result
@ -111,12 +112,12 @@ export default class TransactionStateManager extends EventEmitter {
}
/**
@param [address] {string} - hex prefixed address to sort the txMetas for [optional]
@returns {array} - the tx list whose status is approved if no address is provide
returns all txMetas who's status is approved for the current network
*/
getApprovedTransactions (address) {
const opts = { status: 'approved' }
* @param {string} [address] - hex prefixed address to sort the txMetas for [optional]
* @returns {Array} the tx list with approved status if no address is provide
* returns all txMetas with approved statuses for the current network
*/
getApprovedTransactions(address) {
const opts = { status: TRANSACTION_STATUSES.APPROVED }
if (address) {
opts.from = address
}
@ -124,12 +125,12 @@ export default class TransactionStateManager extends EventEmitter {
}
/**
@param [address] {string} - hex prefixed address to sort the txMetas for [optional]
@returns {array} - the tx list whose status is submitted if no address is provide
returns all txMetas who's status is submitted for the current network
*/
getPendingTransactions (address) {
const opts = { status: 'submitted' }
* @param {string} [address] - hex prefixed address to sort the txMetas for [optional]
* @returns {Array} the tx list submitted status if no address is provide
* returns all txMetas with submitted statuses for the current network
*/
getPendingTransactions(address) {
const opts = { status: TRANSACTION_STATUSES.SUBMITTED }
if (address) {
opts.from = address
}
@ -137,12 +138,12 @@ export default class TransactionStateManager extends EventEmitter {
}
/**
@param [address] {string} - hex prefixed address to sort the txMetas for [optional]
@returns {array} - the tx list whose status is confirmed if no address is provide
@param {string} [address] - hex prefixed address to sort the txMetas for [optional]
@returns {Array} the tx list whose status is confirmed if no address is provide
returns all txMetas who's status is confirmed for the current network
*/
getConfirmedTransactions (address) {
const opts = { status: 'confirmed' }
getConfirmedTransactions(address) {
const opts = { status: TRANSACTION_STATUSES.CONFIRMED }
if (address) {
opts.from = address
}
@ -150,15 +151,15 @@ export default class TransactionStateManager extends EventEmitter {
}
/**
Adds the txMeta to the list of transactions in the store.
if the list is over txHistoryLimit it will remove a transaction that
is in its final state
it will also add the key `history` to the txMeta with the snap shot of the original
object
@param {Object} txMeta
@returns {Object} - the txMeta
*/
addTx (txMeta) {
* Adds the txMeta to the list of transactions in the store.
* if the list is over txHistoryLimit it will remove a transaction that
* is in its final state.
* it will also add the key `history` to the txMeta with the snap shot of
* the original object
* @param {Object} txMeta
* @returns {Object} the txMeta
*/
addTx(txMeta) {
// normalize and validate txParams if present
if (txMeta.txParams) {
txMeta.txParams = this.normalizeAndValidateTxParams(txMeta.txParams)
@ -193,8 +194,9 @@ export default class TransactionStateManager extends EventEmitter {
transactions.splice(index, 1)
}
}
const newTxIndex = transactions
.findIndex((currentTxMeta) => currentTxMeta.time > txMeta.time)
const newTxIndex = transactions.findIndex(
(currentTxMeta) => currentTxMeta.time > txMeta.time,
)
newTxIndex === -1
? transactions.push(txMeta)
@ -204,21 +206,21 @@ export default class TransactionStateManager extends EventEmitter {
}
/**
@param {number} txId
@returns {Object} - the txMeta who matches the given id if none found
for the network returns undefined
*/
getTx (txId) {
* @param {number} txId
* @returns {Object} the txMeta who matches the given id if none found
* for the network returns undefined
*/
getTx(txId) {
const txMeta = this.getTxsByMetaData('id', txId)[0]
return txMeta
}
/**
updates the txMeta in the list and adds a history entry
@param {Object} txMeta - the txMeta to update
@param {string} [note] - a note about the update for history
*/
updateTx (txMeta, note) {
* updates the txMeta in the list and adds a history entry
* @param {Object} txMeta - the txMeta to update
* @param {string} [note] - a note about the update for history
*/
updateTx(txMeta, note) {
// normalize and validate txParams if present
if (txMeta.txParams) {
txMeta.txParams = this.normalizeAndValidateTxParams(txMeta.txParams)
@ -243,12 +245,12 @@ export default class TransactionStateManager extends EventEmitter {
}
/**
merges txParams obj onto txMeta.txParams
use extend to ensure that all fields are filled
@param {number} txId - the id of the txMeta
@param {Object} txParams - the updated txParams
*/
updateTxParams (txId, txParams) {
* merges txParams obj onto txMeta.txParams use extend to ensure
* that all fields are filled
* @param {number} txId - the id of the txMeta
* @param {Object} txParams - the updated txParams
*/
updateTxParams(txId, txParams) {
const txMeta = this.getTx(txId)
txMeta.txParams = { ...txMeta.txParams, ...txParams }
this.updateTx(txMeta, `txStateManager#updateTxParams`)
@ -258,7 +260,7 @@ export default class TransactionStateManager extends EventEmitter {
* normalize and validate txParams members
* @param {Object} txParams - txParams
*/
normalizeAndValidateTxParams (txParams) {
normalizeAndValidateTxParams(txParams) {
if (typeof txParams.data === 'undefined') {
delete txParams.data
}
@ -269,22 +271,26 @@ export default class TransactionStateManager extends EventEmitter {
}
/**
validates txParams members by type
@param {Object} txParams - txParams to validate
*/
validateTxParams (txParams) {
* validates txParams members by type
* @param {Object} txParams - txParams to validate
*/
validateTxParams(txParams) {
Object.keys(txParams).forEach((key) => {
const value = txParams[key]
// validate types
switch (key) {
case 'chainId':
if (typeof value !== 'number' && typeof value !== 'string') {
throw new Error(`${key} in txParams is not a Number or hex string. got: (${value})`)
throw new Error(
`${key} in txParams is not a Number or hex string. got: (${value})`,
)
}
break
default:
if (typeof value !== 'string') {
throw new Error(`${key} in txParams is not a string. got: (${value})`)
throw new Error(
`${key} in txParams is not a string. got: (${value})`,
)
}
break
}
@ -301,8 +307,8 @@ export default class TransactionStateManager extends EventEmitter {
}<br></code>
optionally the values of the keys can be functions for situations like where
you want all but one status.
@param [initialList=this.getTxList()]
@returns {array} - array of txMeta with all
@param {Array} [initialList=this.getTxList()]
@returns {Array} array of txMeta with all
options matching
*/
/*
@ -319,7 +325,7 @@ export default class TransactionStateManager extends EventEmitter {
or for filtering for all txs from one account
and that have been 'confirmed'
*/
getFilteredTxList (opts, initialList) {
getFilteredTxList(opts, initialList) {
let filteredTxList = initialList
Object.keys(opts).forEach((key) => {
filteredTxList = this.getTxsByMetaData(key, opts[key], filteredTxList)
@ -328,14 +334,13 @@ export default class TransactionStateManager extends EventEmitter {
}
/**
@param {string} key - the key to check
@param value - the value your looking for can also be a function that returns a bool
@param [txList=this.getTxList()] {array} - the list to search. default is the txList
from txStateManager#getTxList
@returns {array} - a list of txMetas who matches the search params
*/
getTxsByMetaData (key, value, txList = this.getTxList()) {
* @param {string} key - the key to check
* @param {any} value - the value your looking for can also be a function that returns a bool
* @param {Array} [txList=this.getTxList()] - the list to search. default is the txList
* from txStateManager#getTxList
* @returns {Array} a list of txMetas who matches the search params
*/
getTxsByMetaData(key, value, txList = this.getTxList()) {
const filter = typeof value === 'function' ? value : (v) => v === value
return txList.filter((txMeta) => {
@ -349,82 +354,81 @@ export default class TransactionStateManager extends EventEmitter {
// get::set status
/**
@param {number} txId - the txMeta Id
@returns {string} - the status of the tx.
*/
getTxStatus (txId) {
* @param {number} txId - the txMeta Id
* @returns {string} the status of the tx.
*/
getTxStatus(txId) {
const txMeta = this.getTx(txId)
return txMeta.status
}
/**
should update the status of the tx to 'rejected'.
@param {number} txId - the txMeta Id
*/
setTxStatusRejected (txId) {
* Update the status of the tx to 'rejected'.
* @param {number} txId - the txMeta Id
*/
setTxStatusRejected(txId) {
this._setTxStatus(txId, 'rejected')
this._removeTx(txId)
}
/**
should update the status of the tx to 'unapproved'.
@param {number} txId - the txMeta Id
*/
setTxStatusUnapproved (txId) {
this._setTxStatus(txId, 'unapproved')
* Update the status of the tx to 'unapproved'.
* @param {number} txId - the txMeta Id
*/
setTxStatusUnapproved(txId) {
this._setTxStatus(txId, TRANSACTION_STATUSES.UNAPPROVED)
}
/**
should update the status of the tx to 'approved'.
@param {number} txId - the txMeta Id
*/
setTxStatusApproved (txId) {
this._setTxStatus(txId, 'approved')
* Update the status of the tx to 'approved'.
* @param {number} txId - the txMeta Id
*/
setTxStatusApproved(txId) {
this._setTxStatus(txId, TRANSACTION_STATUSES.APPROVED)
}
/**
should update the status of the tx to 'signed'.
@param {number} txId - the txMeta Id
*/
setTxStatusSigned (txId) {
this._setTxStatus(txId, 'signed')
* Update the status of the tx to 'signed'.
* @param {number} txId - the txMeta Id
*/
setTxStatusSigned(txId) {
this._setTxStatus(txId, TRANSACTION_STATUSES.SIGNED)
}
/**
should update the status of the tx to 'submitted'.
and add a time stamp for when it was called
@param {number} txId - the txMeta Id
*/
setTxStatusSubmitted (txId) {
* Update the status of the tx to 'submitted' and add a time stamp
* for when it was called
* @param {number} txId - the txMeta Id
*/
setTxStatusSubmitted(txId) {
const txMeta = this.getTx(txId)
txMeta.submittedTime = (new Date()).getTime()
txMeta.submittedTime = new Date().getTime()
this.updateTx(txMeta, 'txStateManager - add submitted time stamp')
this._setTxStatus(txId, 'submitted')
this._setTxStatus(txId, TRANSACTION_STATUSES.SUBMITTED)
}
/**
should update the status of the tx to 'confirmed'.
@param {number} txId - the txMeta Id
*/
setTxStatusConfirmed (txId) {
this._setTxStatus(txId, 'confirmed')
* Update the status of the tx to 'confirmed'.
* @param {number} txId - the txMeta Id
*/
setTxStatusConfirmed(txId) {
this._setTxStatus(txId, TRANSACTION_STATUSES.CONFIRMED)
}
/**
should update the status of the tx to 'dropped'.
@param {number} txId - the txMeta Id
*/
setTxStatusDropped (txId) {
this._setTxStatus(txId, 'dropped')
* Update the status of the tx to 'dropped'.
* @param {number} txId - the txMeta Id
*/
setTxStatusDropped(txId) {
this._setTxStatus(txId, TRANSACTION_STATUSES.DROPPED)
}
/**
should update the status of the tx to 'failed'.
and put the error on the txMeta
@param {number} txId - the txMeta Id
@param {erroObject} err - error object
*/
setTxStatusFailed (txId, err) {
* Updates the status of the tx to 'failed' and put the error on the txMeta
* @param {number} txId - the txMeta Id
* @param {erroObject} err - error object
*/
setTxStatusFailed(txId, err) {
const error = err || new Error('Internal metamask failure')
const txMeta = this.getTx(txId)
@ -434,48 +438,45 @@ export default class TransactionStateManager extends EventEmitter {
stack: error.stack,
}
this.updateTx(txMeta, 'transactions:tx-state-manager#fail - add error')
this._setTxStatus(txId, 'failed')
this._setTxStatus(txId, TRANSACTION_STATUSES.FAILED)
}
/**
Removes transaction from the given address for the current network
from the txList
@param {string} address - hex string of the from address on the txParams to remove
*/
wipeTransactions (address) {
* Removes transaction from the given address for the current network
* from the txList
* @param {string} address - hex string of the from address on the txParams
* to remove
*/
wipeTransactions(address) {
// network only tx
const txs = this.getFullTxList()
const network = this.getNetwork()
// Filter out the ones from the current account and network
const otherAccountTxs = txs.filter((txMeta) => !(txMeta.txParams.from === address && txMeta.metamaskNetworkId === network))
const otherAccountTxs = txs.filter(
(txMeta) =>
!(
txMeta.txParams.from === address &&
txMeta.metamaskNetworkId === network
),
)
// Update state
this._saveTxList(otherAccountTxs)
}
//
// PRIVATE METHODS
//
// STATUS METHODS
// statuses:
// - `'unapproved'` the user has not responded
// - `'rejected'` the user has responded no!
// - `'approved'` the user has approved the tx
// - `'signed'` the tx is signed
// - `'submitted'` the tx is sent to a server
// - `'confirmed'` the tx has been included in a block.
// - `'failed'` the tx failed for some reason, included on tx data.
// - `'dropped'` the tx nonce was already used
/**
@param {number} txId - the txMeta Id
@param {string} status - the status to set on the txMeta
@emits tx:status-update - passes txId and status
@emits ${txMeta.id}:finished - if it is a finished state. Passes the txMeta
@emits update:badge
*/
_setTxStatus (txId, status) {
* @param {number} txId - the txMeta Id
* @param {TransactionStatuses[keyof TransactionStatuses]} status - the status to set on the txMeta
* @emits tx:status-update - passes txId and status
* @emits ${txMeta.id}:finished - if it is a finished state. Passes the txMeta
* @emits update:badge
*/
_setTxStatus(txId, status) {
const txMeta = this.getTx(txId)
if (!txMeta) {
@ -487,7 +488,13 @@ export default class TransactionStateManager extends EventEmitter {
this.updateTx(txMeta, `txStateManager: setting status to ${status}`)
this.emit(`${txMeta.id}:${status}`, txId)
this.emit(`tx:status-update`, txId, status)
if (['submitted', 'rejected', 'failed'].includes(status)) {
if (
[
TRANSACTION_STATUSES.SUBMITTED,
TRANSACTION_STATUSES.REJECTED,
TRANSACTION_STATUSES.FAILED,
].includes(status)
) {
this.emit(`${txMeta.id}:finished`, txMeta)
}
this.emit('update:badge')
@ -497,15 +504,14 @@ export default class TransactionStateManager extends EventEmitter {
}
/**
Saves the new/updated txList.
@param {array} transactions - the list of transactions to save
*/
// Function is intended only for internal use
_saveTxList (transactions) {
* Saves the new/updated txList. Intended only for internal use
* @param {Array} transactions - the list of transactions to save
*/
_saveTxList(transactions) {
this.store.updateState({ transactions })
}
_removeTx (txId) {
_removeTx(txId) {
const transactionList = this.getFullTxList()
this._saveTxList(transactionList.filter((txMeta) => txMeta.id !== txId))
}
@ -513,10 +519,11 @@ export default class TransactionStateManager extends EventEmitter {
/**
* Filters out the unapproved transactions
*/
clearUnapprovedTxs () {
clearUnapprovedTxs() {
const transactions = this.getFullTxList()
const nonUnapprovedTxs = transactions.filter((tx) => tx.status !== 'unapproved')
const nonUnapprovedTxs = transactions.filter(
(tx) => tx.status !== TRANSACTION_STATUSES.UNAPPROVED,
)
this._saveTxList(nonUnapprovedTxs)
}
}

View File

@ -1,4 +1,3 @@
/**
* @typedef {Object} FirstTimeState
* @property {Object} config Initial configuration parameters

View File

@ -60,13 +60,13 @@ initProvider({
// TODO:deprecate:2020
// Setup web3
if (typeof window.web3 !== 'undefined') {
throw new Error(`MetaMask detected another web3.
if (typeof window.web3 === 'undefined') {
// proxy web3, assign to window, and set up site auto reload
setupWeb3(log)
} else {
log.warn(`MetaMask detected another web3.
MetaMask will not work reliably with another web3 extension.
This usually happens if you have two MetaMasks installed,
or MetaMask and another web3 extension. Please remove one
and try again.`)
}
// proxy web3, assign to window, and set up site auto reload
setupWeb3(log)

View File

@ -5,14 +5,13 @@ import ObservableStore from 'obs-store'
* structure of child stores based on configuration
*/
export default class ComposableObservableStore extends ObservableStore {
/**
* Create a new store
*
* @param {Object} [initState] - The initial store state
* @param {Object} [config] - Map of internal state keys to child stores
*/
constructor (initState, config) {
constructor(initState, config) {
super(initState)
this.updateStructure(config)
}
@ -22,7 +21,7 @@ export default class ComposableObservableStore extends ObservableStore {
*
* @param {Object} [config] - Map of internal state keys to child stores
*/
updateStructure (config) {
updateStructure(config) {
this.config = config
this.removeAllListeners()
for (const key in config) {
@ -38,14 +37,16 @@ export default class ComposableObservableStore extends ObservableStore {
* Merges all child store state into a single object rather than
* returning an object keyed by child store class name
*
* @returns {Object} - Object containing merged child store state
* @returns {Object} Object containing merged child store state
*/
getFlatState () {
getFlatState() {
let flatState = {}
for (const key in this.config) {
if (Object.prototype.hasOwnProperty.call(this.config, key)) {
const controller = this.config[key]
const state = controller.getState ? controller.getState() : controller.state
const state = controller.getState
? controller.getState()
: controller.state
flatState = { ...flatState, ...state }
}
}

View File

@ -14,7 +14,12 @@ import log from 'loglevel'
import pify from 'pify'
import Web3 from 'web3'
import SINGLE_CALL_BALANCES_ABI from 'single-call-balance-checker-abi'
import { MAINNET_CHAIN_ID, RINKEBY_CHAIN_ID, ROPSTEN_CHAIN_ID, KOVAN_CHAIN_ID } from '../controllers/network/enums'
import {
MAINNET_CHAIN_ID,
RINKEBY_CHAIN_ID,
ROPSTEN_CHAIN_ID,
KOVAN_CHAIN_ID,
} from '../controllers/network/enums'
import {
SINGLE_CALL_BALANCES_ADDRESS,
@ -42,14 +47,13 @@ import { bnToHex } from './util'
*
*/
export default class AccountTracker {
/**
* @param {Object} opts - Options for initializing the controller
* @param {Object} opts.provider - An EIP-1193 provider instance that uses the current global network
* @param {Object} opts.blockTracker - A block tracker, which emits events for each new block
* @param {Function} opts.getCurrentChainId - A function that returns the `chainId` for the current global network
*/
constructor (opts = {}) {
constructor(opts = {}) {
const initState = {
accounts: {},
currentBlockGasLimit: '',
@ -71,7 +75,7 @@ export default class AccountTracker {
this.web3 = new Web3(this._provider)
}
start () {
start() {
// remove first to avoid double add
this._blockTracker.removeListener('latest', this._updateForBlock)
// add listener
@ -80,7 +84,7 @@ export default class AccountTracker {
this._updateAccounts()
}
stop () {
stop() {
// remove listener
this._blockTracker.removeListener('latest', this._updateForBlock)
}
@ -92,11 +96,11 @@ export default class AccountTracker {
* Once this AccountTracker's accounts are up to date with those referenced by the passed addresses, each
* of these accounts are given an updated balance via EthQuery.
*
* @param {array} address - The array of hex addresses for accounts with which this AccountTracker's accounts should be
* @param {Array} address - The array of hex addresses for accounts with which this AccountTracker's accounts should be
* in sync
*
*/
syncWithAddresses (addresses) {
syncWithAddresses(addresses) {
const { accounts } = this.store.getState()
const locals = Object.keys(accounts)
@ -122,10 +126,10 @@ export default class AccountTracker {
* Adds new addresses to track the balances of
* given a balance as long this._currentBlockNumber is defined.
*
* @param {array} addresses - An array of hex addresses of new accounts to track
* @param {Array} addresses - An array of hex addresses of new accounts to track
*
*/
addAccounts (addresses) {
addAccounts(addresses) {
const { accounts } = this.store.getState()
// add initial state for addresses
addresses.forEach((address) => {
@ -143,10 +147,10 @@ export default class AccountTracker {
/**
* Removes accounts from being tracked
*
* @param {array} an - array of hex addresses to stop tracking
* @param {Array} an - array of hex addresses to stop tracking
*
*/
removeAccount (addresses) {
removeAccount(addresses) {
const { accounts } = this.store.getState()
// remove each state object
addresses.forEach((address) => {
@ -160,7 +164,7 @@ export default class AccountTracker {
* Removes all addresses and associated balances
*/
clearAccounts () {
clearAccounts() {
this.store.updateState({ accounts: {} })
}
@ -173,7 +177,7 @@ export default class AccountTracker {
* @fires 'block' The updated state, if all account updates are successful
*
*/
async _updateForBlock (blockNumber) {
async _updateForBlock(blockNumber) {
this._currentBlockNumber = blockNumber
// block gasLimit polling shouldn't be in account-tracker shouldn't be here...
@ -195,29 +199,41 @@ export default class AccountTracker {
* balanceChecker is deployed on main eth (test)nets and requires a single call
* for all other networks, calls this._updateAccount for each account in this.store
*
* @returns {Promise} - after all account balances updated
* @returns {Promise} after all account balances updated
*
*/
async _updateAccounts () {
async _updateAccounts() {
const { accounts } = this.store.getState()
const addresses = Object.keys(accounts)
const chainId = this.getCurrentChainId()
switch (chainId) {
case MAINNET_CHAIN_ID:
await this._updateAccountsViaBalanceChecker(addresses, SINGLE_CALL_BALANCES_ADDRESS)
await this._updateAccountsViaBalanceChecker(
addresses,
SINGLE_CALL_BALANCES_ADDRESS,
)
break
case RINKEBY_CHAIN_ID:
await this._updateAccountsViaBalanceChecker(addresses, SINGLE_CALL_BALANCES_ADDRESS_RINKEBY)
await this._updateAccountsViaBalanceChecker(
addresses,
SINGLE_CALL_BALANCES_ADDRESS_RINKEBY,
)
break
case ROPSTEN_CHAIN_ID:
await this._updateAccountsViaBalanceChecker(addresses, SINGLE_CALL_BALANCES_ADDRESS_ROPSTEN)
await this._updateAccountsViaBalanceChecker(
addresses,
SINGLE_CALL_BALANCES_ADDRESS_ROPSTEN,
)
break
case KOVAN_CHAIN_ID:
await this._updateAccountsViaBalanceChecker(addresses, SINGLE_CALL_BALANCES_ADDRESS_KOVAN)
await this._updateAccountsViaBalanceChecker(
addresses,
SINGLE_CALL_BALANCES_ADDRESS_KOVAN,
)
break
default:
@ -230,10 +246,10 @@ export default class AccountTracker {
*
* @private
* @param {string} address - A hex address of a the account to be updated
* @returns {Promise} - after the account balance is updated
* @returns {Promise} after the account balance is updated
*
*/
async _updateAccount (address) {
async _updateAccount(address) {
// query balance
const balance = await this._query.getBalance(address)
const result = { address, balance }
@ -252,15 +268,20 @@ export default class AccountTracker {
* @param {*} addresses
* @param {*} deployedContractAddress
*/
async _updateAccountsViaBalanceChecker (addresses, deployedContractAddress) {
async _updateAccountsViaBalanceChecker(addresses, deployedContractAddress) {
const { accounts } = this.store.getState()
this.web3.setProvider(this._provider)
const ethContract = this.web3.eth.contract(SINGLE_CALL_BALANCES_ABI).at(deployedContractAddress)
const ethContract = this.web3.eth
.contract(SINGLE_CALL_BALANCES_ABI)
.at(deployedContractAddress)
const ethBalance = ['0x0']
ethContract.balances(addresses, ethBalance, (error, result) => {
if (error) {
log.warn(`MetaMask - Account Tracker single call balance fetch failed`, error)
log.warn(
`MetaMask - Account Tracker single call balance fetch failed`,
error,
)
Promise.all(addresses.map(this._updateAccount.bind(this)))
return
}
@ -271,5 +292,4 @@ export default class AccountTracker {
this.store.updateState({ accounts })
})
}
}

View File

@ -2,13 +2,13 @@
* Gives the caller a url at which the user can acquire eth, depending on the network they are in
*
* @param {Object} opts - Options required to determine the correct url
* @param {string} opts.network The network for which to return a url
* @param {string} opts.address The address the bought ETH should be sent to. Only relevant if network === '1'.
* @returns {string|undefined} - The url at which the user can access ETH, while in the given network. If the passed
* @param {string} opts.network - The network for which to return a url
* @param {string} opts.address - The address the bought ETH should be sent to. Only relevant if network === '1'.
* @returns {string|undefined} The url at which the user can access ETH, while in the given network. If the passed
* network does not match any of the specified cases, or if no network is given, returns undefined.
*
*/
export default function getBuyEthUrl ({ network, address, service }) {
export default function getBuyEthUrl({ network, address, service }) {
// default service by network if not specified
if (!service) {
// eslint-disable-next-line no-param-reassign
@ -33,7 +33,7 @@ export default function getBuyEthUrl ({ network, address, service }) {
}
}
function getDefaultServiceForNetwork (network) {
function getDefaultServiceForNetwork(network) {
switch (network) {
case '1':
return 'wyre'
@ -46,6 +46,8 @@ function getDefaultServiceForNetwork (network) {
case '5':
return 'goerli-faucet'
default:
throw new Error(`No default cryptocurrency exchange or faucet for networkId: "${network}"`)
throw new Error(
`No default cryptocurrency exchange or faucet for networkId: "${network}"`,
)
}
}

View File

@ -1,14 +1,14 @@
/**
* Returns error without stack trace for better UI display
* @param {Error} err - error
* @returns {Error} - Error with clean stack trace.
* @returns {Error} Error with clean stack trace.
*/
export default function cleanErrorStack (err) {
export default function cleanErrorStack(err) {
let { name } = err
name = (name === undefined) ? 'Error' : String(name)
name = name === undefined ? 'Error' : String(name)
let msg = err.message
msg = (msg === undefined) ? '' : String(msg)
msg = msg === undefined ? '' : String(msg)
if (name === '') {
err.stack = err.message

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