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 path: test-artifacts
destination: test-artifacts destination: test-artifacts
# important: generate sesify viz AFTER uploading builds as artifacts # important: generate sesify viz AFTER uploading builds as artifacts
- run: # Temporarily disabled until we can update to a version of `sesify` with
name: build:sesify-viz # this fix included: https://github.com/LavaMoat/LavaMoat/pull/121
command: ./.circleci/scripts/create-sesify-viz # - run:
# name: build:sesify-viz
# command: ./.circleci/scripts/create-sesify-viz
- store_artifacts: - store_artifacts:
path: build-artifacts path: build-artifacts
destination: build-artifacts destination: build-artifacts

View File

@ -2,19 +2,19 @@ module.exports = {
root: true, root: true,
parser: '@babel/eslint-parser', parser: '@babel/eslint-parser',
parserOptions: { parserOptions: {
'sourceType': 'module', sourceType: 'module',
'ecmaVersion': 2017, ecmaVersion: 2017,
'ecmaFeatures': { ecmaFeatures: {
'experimentalObjectRestSpread': true, experimentalObjectRestSpread: true,
'impliedStrict': true, impliedStrict: true,
'modules': true, modules: true,
'blockBindings': true, blockBindings: true,
'arrowFunctions': true, arrowFunctions: true,
'objectLiteralShorthandMethods': true, objectLiteralShorthandMethods: true,
'objectLiteralShorthandProperties': true, objectLiteralShorthandProperties: true,
'templateStrings': true, templateStrings: true,
'classes': true, classes: true,
'jsx': true, jsx: true,
}, },
}, },
@ -28,6 +28,9 @@ module.exports = {
'coverage/', 'coverage/',
'app/scripts/chromereload.js', 'app/scripts/chromereload.js',
'app/vendor/**', 'app/vendor/**',
'test/e2e/send-eth-with-private-key-test/**',
'nyc_output/**',
'.vscode/**',
], ],
extends: [ extends: [
@ -38,11 +41,7 @@ module.exports = {
'plugin:react-hooks/recommended', 'plugin:react-hooks/recommended',
], ],
plugins: [ plugins: ['@babel', 'react', 'import', 'prettier'],
'@babel',
'react',
'import',
],
globals: { globals: {
document: 'readonly', document: 'readonly',
@ -50,6 +49,67 @@ module.exports = {
}, },
rules: { 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', 'default-param-last': 'off',
'require-atomic-updates': 'off', 'require-atomic-updates': 'off',
'import/no-unassigned-import': 'off', 'import/no-unassigned-import': 'off',
@ -57,29 +117,32 @@ module.exports = {
'react/no-unused-prop-types': 'error', 'react/no-unused-prop-types': 'error',
'react/no-unused-state': 'error', 'react/no-unused-state': 'error',
'react/jsx-boolean-value': '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/jsx-equals-spacing': 'error',
'react/no-deprecated': 'error', 'react/no-deprecated': 'error',
'react/default-props-match-prop-types': '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-no-duplicate-props': 'error',
'react/jsx-closing-bracket-location': 'error', 'react/jsx-closing-bracket-location': 'error',
'react/jsx-first-prop-new-line': ['error', 'multiline'], 'react/jsx-first-prop-new-line': ['error', 'multiline'],
'react/jsx-max-props-per-line': ['error', { 'maximum': 1, 'when': 'multiline' }], 'react/jsx-max-props-per-line': [
'react/jsx-tag-spacing': ['error', { 'error',
'closingSlash': 'never', { maximum: 1, when: 'multiline' },
'beforeSelfClosing': 'always', ],
'afterOpening': 'never', 'react/jsx-tag-spacing': [
}], 'error',
'react/jsx-wrap-multilines': ['error', { {
'declaration': 'parens-new-line', closingSlash: 'never',
'assignment': 'parens-new-line', beforeSelfClosing: 'always',
'return': 'parens-new-line', afterOpening: 'never',
'arrow': 'parens-new-line', },
'condition': 'parens-new-line', ],
'logical': 'parens-new-line',
'prop': 'parens-new-line',
}],
'no-invalid-this': 'off', 'no-invalid-this': 'off',
'@babel/no-invalid-this': 'error', '@babel/no-invalid-this': 'error',
@ -93,67 +156,59 @@ module.exports = {
'node/no-unpublished-import': 'off', 'node/no-unpublished-import': 'off',
'node/no-unpublished-require': 'off', 'node/no-unpublished-require': 'off',
}, },
overrides: [
overrides: [{ {
files: [ files: ['test/e2e/**/*.js'],
'test/e2e/**/*.js', rules: {
], 'mocha/no-hooks-for-single-case': 'off',
rules: { },
'mocha/no-hooks-for-single-case': 'off',
}, },
}, { {
files: [ files: ['app/scripts/migrations/*.js', '*.stories.js'],
'app/scripts/migrations/*.js', rules: {
'*.stories.js', 'import/no-anonymous-default-export': ['error', { allowObject: true }],
], },
rules: {
'import/no-anonymous-default-export': ['error', { 'allowObject': true }],
}, },
}, { {
files: [ files: ['app/scripts/migrations/*.js'],
'app/scripts/migrations/*.js', rules: {
], 'node/global-require': 'off',
rules: { },
'node/global-require': 'off',
}, },
}, { {
files: [ files: ['test/**/*-test.js', 'test/**/*.spec.js'],
'test/**/*-test.js', rules: {
'test/**/*.spec.js', // Mocha will re-assign `this` in a test context
], '@babel/no-invalid-this': 'off',
rules: { },
// Mocha will re-assign `this` in a test context
'@babel/no-invalid-this': 'off',
}, },
}, { {
files: [ files: ['development/**/*.js', 'test/e2e/benchmark.js', 'test/helper.js'],
'development/**/*.js', rules: {
'test/e2e/benchmark.js', 'node/no-process-exit': 'off',
'test/helper.js', 'node/shebang': 'off',
], },
rules: {
'node/no-process-exit': 'off',
'node/shebang': 'off',
}, },
}, { {
files: [ files: [
'.eslintrc.js', '.eslintrc.js',
'babel.config.js', 'babel.config.js',
'nyc.config.js', 'nyc.config.js',
'stylelint.config.js', 'stylelint.config.js',
'development/**/*.js', 'development/**/*.js',
'test/e2e/**/*.js', 'test/e2e/**/*.js',
'test/env.js', 'test/env.js',
'test/setup.js', 'test/setup.js',
], ],
parserOptions: { parserOptions: {
sourceType: 'script', sourceType: 'script',
},
}, },
}], ],
settings: { settings: {
'react': { react: {
'version': 'detect', version: 'detect',
}, },
}, },
} }

View File

@ -6,3 +6,4 @@ coverage/
app/vendor/** app/vendor/**
.nyc_output/** .nyc_output/**
.vscode/** .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 ## Current Develop Branch
- [#9612](https://github.com/MetaMask/metamask-extension/pull/9612): Update main-quote-summary designs/styles - [#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 ## 8.1.3 Mon Oct 26 2020
- [#9642](https://github.com/MetaMask/metamask-extension/pull/9642) Prevent excessive overflow from swap dropdowns - [#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 - [#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": { "cancel": {
"message": "ሰርዝ" "message": "ሰርዝ"
}, },
"cancelAttempt": {
"message": "ሙከራን ሰርዝ"
},
"cancellationGasFee": { "cancellationGasFee": {
"message": "የስረዛ ነዳጅ ወጪ" "message": "የስረዛ ነዳጅ ወጪ"
}, },
@ -308,9 +305,6 @@
"deleteNetworkDescription": { "deleteNetworkDescription": {
"message": "ይህን አውታረ መረብ ለመሰረዝ እንደሚፈልጉ እርግጠኛ ነዎት?" "message": "ይህን አውታረ መረብ ለመሰረዝ እንደሚፈልጉ እርግጠኛ ነዎት?"
}, },
"deposit": {
"message": "ማጠራቀም"
},
"depositEther": { "depositEther": {
"message": "Ether አስቀምጥ" "message": "Ether አስቀምጥ"
}, },
@ -737,7 +731,7 @@
"optionalBlockExplorerUrl": { "optionalBlockExplorerUrl": {
"message": "ኤክስፕሎረር URL አግድ (አማራጭ)" "message": "ኤክስፕሎረር URL አግድ (አማራጭ)"
}, },
"optionalSymbol": { "optionalCurrencySymbol": {
"message": "ምልክት (አማራጭ)" "message": "ምልክት (አማራጭ)"
}, },
"orderOneHere": { "orderOneHere": {
@ -985,9 +979,6 @@
"sentEther": { "sentEther": {
"message": "የተላከ ether" "message": "የተላከ ether"
}, },
"sentTokens": {
"message": "የተላኩ ተለዋጭ ስሞች"
},
"separateEachWord": { "separateEachWord": {
"message": "እያንዳንዱን ቃል በነጠላ ክፍት ቦታ ይለያዩ" "message": "እያንዳንዱን ቃል በነጠላ ክፍት ቦታ ይለያዩ"
}, },

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -79,9 +79,6 @@
"defaultNetwork": { "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)." "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": { "depositEther": {
"message": "Ký gửi Ether" "message": "Ký gửi Ether"
}, },

View File

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

View File

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

View File

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

View File

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

View File

@ -2,10 +2,10 @@ import log from 'loglevel'
import Wallet from 'ethereumjs-wallet' import Wallet from 'ethereumjs-wallet'
import importers from 'ethereumjs-wallet/thirdparty' import importers from 'ethereumjs-wallet/thirdparty'
import ethUtil from 'ethereumjs-util' import ethUtil from 'ethereumjs-util'
import { addHexPrefix } from '../lib/util'
const accountImporter = { const accountImporter = {
importAccount(strategy, args) {
importAccount (strategy, args) {
try { try {
const importer = this.strategies[strategy] const importer = this.strategies[strategy]
const privateKeyHex = importer(...args) const privateKeyHex = importer(...args)
@ -21,7 +21,7 @@ const accountImporter = {
throw new Error('Cannot import an empty key.') throw new Error('Cannot import an empty key.')
} }
const prefixed = ethUtil.addHexPrefix(privateKey) const prefixed = addHexPrefix(privateKey)
const buffer = ethUtil.toBuffer(prefixed) const buffer = ethUtil.toBuffer(prefixed)
if (!ethUtil.isValidPrivate(buffer)) { if (!ethUtil.isValidPrivate(buffer)) {
@ -43,10 +43,9 @@ const accountImporter = {
return walletToPrivateKey(wallet) return walletToPrivateKey(wallet)
}, },
}, },
} }
function walletToPrivateKey (wallet) { function walletToPrivateKey(wallet) {
const privateKeyBuffer = wallet.getPrivateKey() const privateKeyBuffer = wallet.getPrivateKey()
return ethUtil.bufferToHex(privateKeyBuffer) return ethUtil.bufferToHex(privateKeyBuffer)
} }

View File

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

View File

@ -9,7 +9,10 @@ import PortStream from 'extension-port-stream'
const fs = require('fs') const fs = require('fs')
const path = require('path') 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 inpageSuffix = `//# sourceURL=${extension.runtime.getURL('inpage.js')}\n`
const inpageBundle = inpageContent + inpageSuffix const inpageBundle = inpageContent + inpageSuffix
@ -30,7 +33,7 @@ if (shouldInjectProvider()) {
* *
* @param {string} content - Code to be executed in the current document * @param {string} content - Code to be executed in the current document
*/ */
function injectScript (content) { function injectScript(content) {
try { try {
const container = document.head || document.documentElement const container = document.head || document.documentElement
const scriptTag = document.createElement('script') const scriptTag = document.createElement('script')
@ -47,7 +50,7 @@ function injectScript (content) {
* Sets up the stream communication and submits site metadata * Sets up the stream communication and submits site metadata
* *
*/ */
async function start () { async function start() {
await setupStreams() await setupStreams()
await domIsReady() await domIsReady()
} }
@ -57,7 +60,7 @@ async function start () {
* browser extension and local per-page browser context. * browser extension and local per-page browser context.
* *
*/ */
async function setupStreams () { async function setupStreams() {
// the transport-specific streams for communication between inpage and background // the transport-specific streams for communication between inpage and background
const pageStream = new LocalMessageDuplexStream({ const pageStream = new LocalMessageDuplexStream({
name: 'contentscript', name: 'contentscript',
@ -73,17 +76,11 @@ async function setupStreams () {
const extensionMux = new ObjectMultiplex() const extensionMux = new ObjectMultiplex()
extensionMux.setMaxListeners(25) extensionMux.setMaxListeners(25)
pump( pump(pageMux, pageStream, pageMux, (err) =>
pageMux, logStreamDisconnectWarning('MetaMask Inpage Multiplex', err),
pageStream,
pageMux,
(err) => logStreamDisconnectWarning('MetaMask Inpage Multiplex', err),
) )
pump( pump(extensionMux, extensionStream, extensionMux, (err) =>
extensionMux, logStreamDisconnectWarning('MetaMask Background Multiplex', err),
extensionStream,
extensionMux,
(err) => logStreamDisconnectWarning('MetaMask Background Multiplex', err),
) )
// forward communication across inpage-background for these channels only // forward communication across inpage-background for these channels only
@ -95,14 +92,14 @@ async function setupStreams () {
phishingStream.once('data', redirectToPhishingWarning) phishingStream.once('data', redirectToPhishingWarning)
} }
function forwardTrafficBetweenMuxers (channelName, muxA, muxB) { function forwardTrafficBetweenMuxers(channelName, muxA, muxB) {
const channelA = muxA.createStream(channelName) const channelA = muxA.createStream(channelName)
const channelB = muxB.createStream(channelName) const channelB = muxB.createStream(channelName)
pump( pump(channelA, channelB, channelA, (err) =>
channelA, logStreamDisconnectWarning(
channelB, `MetaMask muxed traffic for channel "${channelName}" failed.`,
channelA, err,
(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 {string} remoteLabel - Remote stream name
* @param {Error} err - Stream connection error * @param {Error} err - Stream connection error
*/ */
function logStreamDisconnectWarning (remoteLabel, err) { function logStreamDisconnectWarning(remoteLabel, err) {
let warningMsg = `MetamaskContentscript - lost connection to ${remoteLabel}` let warningMsg = `MetamaskContentscript - lost connection to ${remoteLabel}`
if (err) { if (err) {
warningMsg += `\n${err.stack}` warningMsg += `\n${err.stack}`
@ -123,19 +120,23 @@ function logStreamDisconnectWarning (remoteLabel, err) {
/** /**
* Determines if the provider should be injected * 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 () { function shouldInjectProvider() {
return doctypeCheck() && suffixCheck() && return (
documentElementCheck() && !blockedDomainCheck() doctypeCheck() &&
suffixCheck() &&
documentElementCheck() &&
!blockedDomainCheck()
)
} }
/** /**
* Checks the doctype of the current document if it exists * 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 const { doctype } = window.document
if (doctype) { if (doctype) {
return doctype.name === 'html' return doctype.name === 'html'
@ -150,13 +151,10 @@ function doctypeCheck () {
* that we should not inject the provider into. This check is indifferent of * that we should not inject the provider into. This check is indifferent of
* query parameters in the location. * 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 () { function suffixCheck() {
const prohibitedTypes = [ const prohibitedTypes = [/\.xml$/u, /\.pdf$/u]
/\.xml$/u,
/\.pdf$/u,
]
const currentUrl = window.location.pathname const currentUrl = window.location.pathname
for (let i = 0; i < prohibitedTypes.length; i++) { for (let i = 0; i < prohibitedTypes.length; i++) {
if (prohibitedTypes[i].test(currentUrl)) { if (prohibitedTypes[i].test(currentUrl)) {
@ -169,9 +167,9 @@ function suffixCheck () {
/** /**
* Checks the documentElement of the current document * 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 const documentElement = document.documentElement.nodeName
if (documentElement) { if (documentElement) {
return documentElement.toLowerCase() === 'html' return documentElement.toLowerCase() === 'html'
@ -182,9 +180,9 @@ function documentElementCheck () {
/** /**
* Checks if the current domain is blocked * 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 = [ const blockedDomains = [
'uscourts.gov', 'uscourts.gov',
'dropbox.com', 'dropbox.com',
@ -201,7 +199,10 @@ function blockedDomainCheck () {
let currentRegex let currentRegex
for (let i = 0; i < blockedDomains.length; i++) { for (let i = 0; i < blockedDomains.length; i++) {
const blockedDomain = blockedDomains[i].replace('.', '\\.') const blockedDomain = blockedDomains[i].replace('.', '\\.')
currentRegex = new RegExp(`(?:https?:\\/\\/)(?:(?!${blockedDomain}).)*$`, 'u') currentRegex = new RegExp(
`(?:https?:\\/\\/)(?:(?!${blockedDomain}).)*$`,
'u',
)
if (!currentRegex.test(currentUrl)) { if (!currentRegex.test(currentUrl)) {
return true return true
} }
@ -212,7 +213,7 @@ function blockedDomainCheck () {
/** /**
* Redirects the current page to a phishing information page * Redirects the current page to a phishing information page
*/ */
function redirectToPhishingWarning () { function redirectToPhishingWarning() {
console.log('MetaMask - routing to Phishing Warning component') console.log('MetaMask - routing to Phishing Warning component')
const extensionURL = extension.runtime.getURL('phishing.html') const extensionURL = extension.runtime.getURL('phishing.html')
window.location.href = `${extensionURL}#${querystring.stringify({ 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) * 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 // already loaded
if (['interactive', 'complete'].includes(document.readyState)) { if (['interactive', 'complete'].includes(document.readyState)) {
return undefined return undefined
} }
// wait for load // 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 = { const defaultState = {
alertEnabledness: Object.keys(ALERT_TYPES) alertEnabledness: Object.keys(ALERT_TYPES).reduce(
.reduce( (alertEnabledness, alertType) => {
(alertEnabledness, alertType) => { alertEnabledness[alertType] = true
alertEnabledness[alertType] = true return alertEnabledness
return alertEnabledness },
}, {},
{}, ),
),
unconnectedAccountAlertShownOrigins: {}, unconnectedAccountAlertShownOrigins: {},
} }
@ -37,12 +36,11 @@ const defaultState = {
* alert related state * alert related state
*/ */
export default class AlertController { export default class AlertController {
/** /**
* @constructor * @constructor
* @param {AlertControllerOptions} [opts] - Controller configuration parameters * @param {AlertControllerOptions} [opts] - Controller configuration parameters
*/ */
constructor (opts = {}) { constructor(opts = {}) {
const { initState, preferencesStore } = opts const { initState, preferencesStore } = opts
const state = { const state = {
...defaultState, ...defaultState,
@ -56,14 +54,17 @@ export default class AlertController {
preferencesStore.subscribe(({ selectedAddress }) => { preferencesStore.subscribe(({ selectedAddress }) => {
const currentState = this.store.getState() const currentState = this.store.getState()
if (currentState.unconnectedAccountAlertShownOrigins && this.selectedAddress !== selectedAddress) { if (
currentState.unconnectedAccountAlertShownOrigins &&
this.selectedAddress !== selectedAddress
) {
this.selectedAddress = selectedAddress this.selectedAddress = selectedAddress
this.store.updateState({ unconnectedAccountAlertShownOrigins: {} }) this.store.updateState({ unconnectedAccountAlertShownOrigins: {} })
} }
}) })
} }
setAlertEnabledness (alertId, enabledness) { setAlertEnabledness(alertId, enabledness) {
let { alertEnabledness } = this.store.getState() let { alertEnabledness } = this.store.getState()
alertEnabledness = { ...alertEnabledness } alertEnabledness = { ...alertEnabledness }
alertEnabledness[alertId] = enabledness alertEnabledness[alertId] = enabledness
@ -74,9 +75,11 @@ export default class AlertController {
* Sets the "switch to connected" alert as shown for the given origin * Sets the "switch to connected" alert as shown for the given origin
* @param {string} origin - The origin the alert has been shown for * @param {string} origin - The origin the alert has been shown for
*/ */
setUnconnectedAccountAlertShown (origin) { setUnconnectedAccountAlertShown(origin) {
let { unconnectedAccountAlertShownOrigins } = this.store.getState() let { unconnectedAccountAlertShownOrigins } = this.store.getState()
unconnectedAccountAlertShownOrigins = { ...unconnectedAccountAlertShownOrigins } unconnectedAccountAlertShownOrigins = {
...unconnectedAccountAlertShownOrigins,
}
unconnectedAccountAlertShownOrigins[origin] = true unconnectedAccountAlertShownOrigins[origin] = true
this.store.updateState({ unconnectedAccountAlertShownOrigins }) this.store.updateState({ unconnectedAccountAlertShownOrigins })
} }

View File

@ -2,12 +2,11 @@ import EventEmitter from 'events'
import ObservableStore from 'obs-store' import ObservableStore from 'obs-store'
export default class AppStateController extends EventEmitter { export default class AppStateController extends EventEmitter {
/** /**
* @constructor * @constructor
* @param opts * @param {Object} opts
*/ */
constructor (opts = {}) { constructor(opts = {}) {
const { const {
addUnlockListener, addUnlockListener,
isUnlocked, isUnlocked,
@ -23,7 +22,8 @@ export default class AppStateController extends EventEmitter {
timeoutMinutes: 0, timeoutMinutes: 0,
connectedStatusPopoverHasBeenShown: true, connectedStatusPopoverHasBeenShown: true,
swapsWelcomeMessageHasBeenShown: false, swapsWelcomeMessageHasBeenShown: false,
defaultHomeActiveTabName: null, ...initState, defaultHomeActiveTabName: null,
...initState,
}) })
this.timer = null this.timer = null
@ -53,7 +53,7 @@ export default class AppStateController extends EventEmitter {
* @returns {Promise<void>} A promise that resolves when the extension is * @returns {Promise<void>} A promise that resolves when the extension is
* unlocked, or immediately if the extension is already unlocked. * unlocked, or immediately if the extension is already unlocked.
*/ */
getUnlockPromise (shouldShowUnlockRequest) { getUnlockPromise(shouldShowUnlockRequest) {
return new Promise((resolve) => { return new Promise((resolve) => {
if (this.isUnlocked()) { if (this.isUnlocked()) {
resolve() resolve()
@ -72,7 +72,7 @@ export default class AppStateController extends EventEmitter {
* @param {boolean} shouldShowUnlockRequest - Whether the extension notification * @param {boolean} shouldShowUnlockRequest - Whether the extension notification
* popup should be opened. * popup should be opened.
*/ */
waitForUnlock (resolve, shouldShowUnlockRequest) { waitForUnlock(resolve, shouldShowUnlockRequest) {
this.waitingForUnlock.push({ resolve }) this.waitingForUnlock.push({ resolve })
this.emit('updateBadge') this.emit('updateBadge')
if (shouldShowUnlockRequest) { if (shouldShowUnlockRequest) {
@ -83,7 +83,7 @@ export default class AppStateController extends EventEmitter {
/** /**
* Drains the waitingForUnlock queue, resolving all the related Promises. * Drains the waitingForUnlock queue, resolving all the related Promises.
*/ */
handleUnlock () { handleUnlock() {
if (this.waitingForUnlock.length > 0) { if (this.waitingForUnlock.length > 0) {
while (this.waitingForUnlock.length > 0) { while (this.waitingForUnlock.length > 0) {
this.waitingForUnlock.shift().resolve() this.waitingForUnlock.shift().resolve()
@ -96,7 +96,7 @@ export default class AppStateController extends EventEmitter {
* Sets the default home tab * Sets the default home tab
* @param {string} [defaultHomeActiveTabName] - the tab name * @param {string} [defaultHomeActiveTabName] - the tab name
*/ */
setDefaultHomeActiveTabName (defaultHomeActiveTabName) { setDefaultHomeActiveTabName(defaultHomeActiveTabName) {
this.store.updateState({ this.store.updateState({
defaultHomeActiveTabName, defaultHomeActiveTabName,
}) })
@ -105,7 +105,7 @@ export default class AppStateController extends EventEmitter {
/** /**
* Record that the user has seen the connected status info popover * Record that the user has seen the connected status info popover
*/ */
setConnectedStatusPopoverHasBeenShown () { setConnectedStatusPopoverHasBeenShown() {
this.store.updateState({ this.store.updateState({
connectedStatusPopoverHasBeenShown: true, connectedStatusPopoverHasBeenShown: true,
}) })
@ -114,7 +114,7 @@ export default class AppStateController extends EventEmitter {
/** /**
* Record that the user has seen the swap screen welcome message * Record that the user has seen the swap screen welcome message
*/ */
setSwapsWelcomeMessageHasBeenShown () { setSwapsWelcomeMessageHasBeenShown() {
this.store.updateState({ this.store.updateState({
swapsWelcomeMessageHasBeenShown: true, swapsWelcomeMessageHasBeenShown: true,
}) })
@ -124,7 +124,7 @@ export default class AppStateController extends EventEmitter {
* Sets the last active time to the current time * Sets the last active time to the current time
* @returns {void} * @returns {void}
*/ */
setLastActiveTime () { setLastActiveTime() {
this._resetTimer() this._resetTimer()
} }
@ -134,7 +134,7 @@ export default class AppStateController extends EventEmitter {
* @returns {void} * @returns {void}
* @private * @private
*/ */
_setInactiveTimeout (timeoutMinutes) { _setInactiveTimeout(timeoutMinutes) {
this.store.updateState({ this.store.updateState({
timeoutMinutes, timeoutMinutes,
}) })
@ -151,7 +151,7 @@ export default class AppStateController extends EventEmitter {
* @returns {void} * @returns {void}
* @private * @private
*/ */
_resetTimer () { _resetTimer() {
const { timeoutMinutes } = this.store.getState() const { timeoutMinutes } = this.store.getState()
if (this.timer) { if (this.timer) {
@ -162,6 +162,9 @@ export default class AppStateController extends EventEmitter {
return 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 * a cache of account balances in local storage
*/ */
export default class CachedBalancesController { export default class CachedBalancesController {
/** /**
* Creates a new controller instance * 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 const { accountTracker, getNetwork } = opts
this.accountTracker = accountTracker this.accountTracker = accountTracker
@ -37,15 +36,18 @@ export default class CachedBalancesController {
* @param {Object} obj - The the recently updated accounts object for the current network * @param {Object} obj - The the recently updated accounts object for the current network
* @returns {Promise<void>} * @returns {Promise<void>}
*/ */
async updateCachedBalances ({ accounts }) { async updateCachedBalances({ accounts }) {
const network = await this.getNetwork() const network = await this.getNetwork()
const balancesToCache = await this._generateBalancesToCache(accounts, network) const balancesToCache = await this._generateBalancesToCache(
accounts,
network,
)
this.store.updateState({ this.store.updateState({
cachedBalances: balancesToCache, cachedBalances: balancesToCache,
}) })
} }
_generateBalancesToCache (newAccounts, currentNetwork) { _generateBalancesToCache(newAccounts, currentNetwork) {
const { cachedBalances } = this.store.getState() const { cachedBalances } = this.store.getState()
const currentNetworkBalancesToCache = { ...cachedBalances[currentNetwork] } const currentNetworkBalancesToCache = { ...cachedBalances[currentNetwork] }
@ -68,7 +70,7 @@ export default class CachedBalancesController {
* Removes cachedBalances * Removes cachedBalances
*/ */
clearCachedBalances () { clearCachedBalances() {
this.store.updateState({ cachedBalances: {} }) this.store.updateState({ cachedBalances: {} })
} }
@ -80,7 +82,7 @@ export default class CachedBalancesController {
* @private * @private
* *
*/ */
_registerUpdates () { _registerUpdates() {
const update = this.updateCachedBalances.bind(this) const update = this.updateCachedBalances.bind(this)
this.accountTracker.store.subscribe(update) this.accountTracker.store.subscribe(update)
} }

View File

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

View File

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

View File

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

View File

@ -6,30 +6,49 @@ import { bnToHex } from '../lib/util'
import fetchWithTimeout from '../lib/fetch-with-timeout' import fetchWithTimeout from '../lib/fetch-with-timeout'
import { import {
ROPSTEN, TRANSACTION_CATEGORIES,
RINKEBY, TRANSACTION_STATUSES,
KOVAN, } from '../../../shared/constants/transaction'
import {
CHAIN_ID_TO_NETWORK_ID_MAP,
CHAIN_ID_TO_TYPE_MAP,
GOERLI, GOERLI,
GOERLI_CHAIN_ID,
KOVAN,
KOVAN_CHAIN_ID,
MAINNET, MAINNET,
NETWORK_TYPE_TO_ID_MAP, MAINNET_CHAIN_ID,
RINKEBY,
RINKEBY_CHAIN_ID,
ROPSTEN,
ROPSTEN_CHAIN_ID,
} from './network/enums' } from './network/enums'
const fetch = fetchWithTimeout({ const fetch = fetchWithTimeout({
timeout: 30000, 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 = {}) { export default class IncomingTransactionsController {
const { constructor(opts = {}) {
blockTracker, const { blockTracker, networkController, preferencesController } = opts
networkController,
preferencesController,
} = opts
this.blockTracker = blockTracker this.blockTracker = blockTracker
this.networkController = networkController this.networkController = networkController
this.preferencesController = preferencesController this.preferencesController = preferencesController
this.getCurrentNetwork = () => networkController.getProviderConfig().type
this._onLatestBlock = async (newBlockNumberHex) => { this._onLatestBlock = async (newBlockNumberHex) => {
const selectedAddress = this.preferencesController.getSelectedAddress() const selectedAddress = this.preferencesController.getSelectedAddress()
@ -43,54 +62,66 @@ export default class IncomingTransactionsController {
const initState = { const initState = {
incomingTransactions: {}, incomingTransactions: {},
incomingTxLastFetchedBlocksByNetwork: { incomingTxLastFetchedBlocksByNetwork: {
[ROPSTEN]: null,
[RINKEBY]: null,
[KOVAN]: null,
[GOERLI]: null, [GOERLI]: null,
[KOVAN]: null,
[MAINNET]: null, [MAINNET]: null,
}, ...opts.initState, [RINKEBY]: null,
[ROPSTEN]: null,
},
...opts.initState,
} }
this.store = new ObservableStore(initState) this.store = new ObservableStore(initState)
this.preferencesController.store.subscribe(pairwise((prevState, currState) => { this.preferencesController.store.subscribe(
const { featureFlags: { showIncomingTransactions: prevShowIncomingTransactions } = {} } = prevState pairwise((prevState, currState) => {
const { featureFlags: { showIncomingTransactions: currShowIncomingTransactions } = {} } = currState const {
featureFlags: {
showIncomingTransactions: prevShowIncomingTransactions,
} = {},
} = prevState
const {
featureFlags: {
showIncomingTransactions: currShowIncomingTransactions,
} = {},
} = currState
if (currShowIncomingTransactions === prevShowIncomingTransactions) { if (currShowIncomingTransactions === prevShowIncomingTransactions) {
return return
} }
if (prevShowIncomingTransactions && !currShowIncomingTransactions) { if (prevShowIncomingTransactions && !currShowIncomingTransactions) {
this.stop() this.stop()
return return
} }
this.start() this.start()
})) }),
)
this.preferencesController.store.subscribe(pairwise(async (prevState, currState) => { this.preferencesController.store.subscribe(
const { selectedAddress: prevSelectedAddress } = prevState pairwise(async (prevState, currState) => {
const { selectedAddress: currSelectedAddress } = currState const { selectedAddress: prevSelectedAddress } = prevState
const { selectedAddress: currSelectedAddress } = currState
if (currSelectedAddress === prevSelectedAddress) { if (currSelectedAddress === prevSelectedAddress) {
return return
} }
await this._update({ await this._update({
address: currSelectedAddress, address: currSelectedAddress,
}) })
})) }),
)
this.networkController.on('networkDidChange', async (newType) => { this.networkController.on('networkDidChange', async () => {
const address = this.preferencesController.getSelectedAddress() const address = this.preferencesController.getSelectedAddress()
await this._update({ await this._update({
address, address,
networkType: newType,
}) })
}) })
} }
start () { start() {
const { featureFlags = {} } = this.preferencesController.store.getState() const { featureFlags = {} } = this.preferencesController.store.getState()
const { showIncomingTransactions } = featureFlags const { showIncomingTransactions } = featureFlags
@ -102,33 +133,45 @@ export default class IncomingTransactionsController {
this.blockTracker.addListener('latest', this._onLatestBlock) this.blockTracker.addListener('latest', this._onLatestBlock)
} }
stop () { stop() {
this.blockTracker.removeListener('latest', this._onLatestBlock) 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 { try {
const dataForUpdate = await this._getDataForUpdate({ address, newBlockNumberDec, networkType }) const dataForUpdate = await this._getDataForUpdate({
await this._updateStateWithNewTxData(dataForUpdate) address,
chainId,
newBlockNumberDec,
})
this._updateStateWithNewTxData(dataForUpdate)
} catch (err) { } catch (err) {
log.error(err) log.error(err)
} }
} }
async _getDataForUpdate ({ address, newBlockNumberDec, networkType } = {}) { async _getDataForUpdate({ address, chainId, newBlockNumberDec } = {}) {
const { const {
incomingTransactions: currentIncomingTxs, incomingTransactions: currentIncomingTxs,
incomingTxLastFetchedBlocksByNetwork: currentBlocksByNetwork, incomingTxLastFetchedBlocksByNetwork: currentBlocksByNetwork,
} = this.store.getState() } = this.store.getState()
const network = networkType || this.getCurrentNetwork() const lastFetchBlockByCurrentNetwork =
const lastFetchBlockByCurrentNetwork = currentBlocksByNetwork[network] currentBlocksByNetwork[CHAIN_ID_TO_TYPE_MAP[chainId]]
let blockToFetchFrom = lastFetchBlockByCurrentNetwork || newBlockNumberDec let blockToFetchFrom = lastFetchBlockByCurrentNetwork || newBlockNumberDec
if (blockToFetchFrom === undefined) { if (blockToFetchFrom === undefined) {
blockToFetchFrom = parseInt(this.blockTracker.getCurrentBlock(), 16) 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 { return {
latestIncomingTxBlockNumber, latestIncomingTxBlockNumber,
@ -136,17 +179,17 @@ export default class IncomingTransactionsController {
currentIncomingTxs, currentIncomingTxs,
currentBlocksByNetwork, currentBlocksByNetwork,
fetchedBlockNumber: blockToFetchFrom, fetchedBlockNumber: blockToFetchFrom,
network, chainId,
} }
} }
async _updateStateWithNewTxData ({ _updateStateWithNewTxData({
latestIncomingTxBlockNumber, latestIncomingTxBlockNumber,
newTxs, newTxs,
currentIncomingTxs, currentIncomingTxs,
currentBlocksByNetwork, currentBlocksByNetwork,
fetchedBlockNumber, fetchedBlockNumber,
network, chainId,
}) { }) {
const newLatestBlockHashByNetwork = latestIncomingTxBlockNumber const newLatestBlockHashByNetwork = latestIncomingTxBlockNumber
? parseInt(latestIncomingTxBlockNumber, 10) + 1 ? parseInt(latestIncomingTxBlockNumber, 10) + 1
@ -161,28 +204,23 @@ export default class IncomingTransactionsController {
this.store.updateState({ this.store.updateState({
incomingTxLastFetchedBlocksByNetwork: { incomingTxLastFetchedBlocksByNetwork: {
...currentBlocksByNetwork, ...currentBlocksByNetwork,
[network]: newLatestBlockHashByNetwork, [CHAIN_ID_TO_TYPE_MAP[chainId]]: newLatestBlockHashByNetwork,
}, },
incomingTransactions: newIncomingTransactions, incomingTransactions: newIncomingTransactions,
}) })
} }
async _fetchAll (address, fromBlock, networkType) { async _fetchAll(address, fromBlock, chainId) {
const fetchedTxResponse = await this._fetchTxs(address, fromBlock, networkType) const fetchedTxResponse = await this._fetchTxs(address, fromBlock, chainId)
return this._processTxFetchResponse(fetchedTxResponse) return this._processTxFetchResponse(fetchedTxResponse)
} }
async _fetchTxs (address, fromBlock, networkType) { async _fetchTxs(address, fromBlock, chainId) {
let etherscanSubdomain = 'api' const etherscanSubdomain =
const currentNetworkID = NETWORK_TYPE_TO_ID_MAP[networkType]?.networkId 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` const apiUrl = `https://${etherscanSubdomain}.etherscan.io`
let url = `${apiUrl}/api?module=account&action=txlist&address=${address}&tag=latest&page=1` let url = `${apiUrl}/api?module=account&action=txlist&address=${address}&tag=latest&page=1`
@ -195,22 +233,26 @@ export default class IncomingTransactionsController {
return { return {
...parsedResponse, ...parsedResponse,
address, address,
currentNetworkID, chainId,
} }
} }
_processTxFetchResponse ({ status, result = [], address, currentNetworkID }) { _processTxFetchResponse({ status, result = [], address, chainId }) {
if (status === '1' && Array.isArray(result) && result.length > 0) { if (status === '1' && Array.isArray(result) && result.length > 0) {
const remoteTxList = {} const remoteTxList = {}
const remoteTxs = [] const remoteTxs = []
result.forEach((tx) => { result.forEach((tx) => {
if (!remoteTxList[tx.hash]) { if (!remoteTxList[tx.hash]) {
remoteTxs.push(this._normalizeTxFromEtherscan(tx, currentNetworkID)) remoteTxs.push(this._normalizeTxFromEtherscan(tx, chainId))
remoteTxList[tx.hash] = 1 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)) incomingTxs.sort((a, b) => (a.time < b.time ? -1 : 1))
let latestIncomingTxBlockNumber = null let latestIncomingTxBlockNumber = null
@ -218,7 +260,8 @@ export default class IncomingTransactionsController {
if ( if (
tx.blockNumber && tx.blockNumber &&
(!latestIncomingTxBlockNumber || (!latestIncomingTxBlockNumber ||
parseInt(latestIncomingTxBlockNumber, 10) < parseInt(tx.blockNumber, 10)) parseInt(latestIncomingTxBlockNumber, 10) <
parseInt(tx.blockNumber, 10))
) { ) {
latestIncomingTxBlockNumber = tx.blockNumber 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 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 { return {
blockNumber: txMeta.blockNumber, blockNumber: txMeta.blockNumber,
id: createId(), id: createId(),
metamaskNetworkId: currentNetworkID, metamaskNetworkId: CHAIN_ID_TO_NETWORK_ID_MAP[chainId],
status, status,
time, time,
txParams: { txParams: {
@ -252,12 +298,12 @@ export default class IncomingTransactionsController {
value: bnToHex(new BN(txMeta.value)), value: bnToHex(new BN(txMeta.value)),
}, },
hash: txMeta.hash, hash: txMeta.hash,
transactionCategory: 'incoming', transactionCategory: TRANSACTION_CATEGORIES.INCOMING,
} }
} }
} }
function pairwise (fn) { function pairwise(fn) {
let first = true let first = true
let cache let cache
return (value) => { return (value) => {

View File

@ -1,4 +1,8 @@
export const SINGLE_CALL_BALANCES_ADDRESS = '0xb1f8e55c7f64d203c1400b9d8555d050f94adf39' export const SINGLE_CALL_BALANCES_ADDRESS =
export const SINGLE_CALL_BALANCES_ADDRESS_RINKEBY = '0x9f510b19f1ad66f0dcf6e45559fab0d6752c1db7' '0xb1f8e55c7f64d203c1400b9d8555d050f94adf39'
export const SINGLE_CALL_BALANCES_ADDRESS_ROPSTEN = '0xb8e671734ce5c8d7dfbbea5574fa4cf39f7a54a4' export const SINGLE_CALL_BALANCES_ADDRESS_RINKEBY =
export const SINGLE_CALL_BALANCES_ADDRESS_KOVAN = '0xb1d3fbb2f83aecd196f474c16ca5d9cffa0d0ffc' '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 BlockTracker from 'eth-block-tracker'
import * as networkEnums from './enums' import * as networkEnums from './enums'
export default function createInfuraClient ({ network, projectId }) { export default function createInfuraClient({ network, projectId }) {
const infuraMiddleware = createInfuraMiddleware({ const infuraMiddleware = createInfuraMiddleware({
network, network,
projectId, projectId,
@ -32,7 +32,7 @@ export default function createInfuraClient ({ network, projectId }) {
return { networkMiddleware, blockTracker } return { networkMiddleware, blockTracker }
} }
function createNetworkAndChainIdMiddleware ({ network }) { function createNetworkAndChainIdMiddleware({ network }) {
let chainId let chainId
let netId let netId

View File

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

View File

@ -1,9 +1,12 @@
import mergeMiddleware from 'json-rpc-engine/src/mergeMiddleware' import mergeMiddleware from 'json-rpc-engine/src/mergeMiddleware'
import createScaffoldMiddleware from 'json-rpc-engine/src/createScaffoldMiddleware' import createScaffoldMiddleware from 'json-rpc-engine/src/createScaffoldMiddleware'
import createWalletSubprovider from 'eth-json-rpc-middleware/wallet' 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, version,
getAccounts, getAccounts,
processTransaction, processTransaction,

View File

@ -22,13 +22,7 @@ export const KOVAN_DISPLAY_NAME = 'Kovan'
export const MAINNET_DISPLAY_NAME = 'Ethereum Mainnet' export const MAINNET_DISPLAY_NAME = 'Ethereum Mainnet'
export const GOERLI_DISPLAY_NAME = 'Goerli' export const GOERLI_DISPLAY_NAME = 'Goerli'
export const INFURA_PROVIDER_TYPES = [ export const INFURA_PROVIDER_TYPES = [ROPSTEN, RINKEBY, KOVAN, MAINNET, GOERLI]
ROPSTEN,
RINKEBY,
KOVAN,
MAINNET,
GOERLI,
]
export const NETWORK_TYPE_TO_ID_MAP = { export const NETWORK_TYPE_TO_ID_MAP = {
[ROPSTEN]: { networkId: ROPSTEN_NETWORK_ID, chainId: ROPSTEN_CHAIN_ID }, [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, [GOERLI_CHAIN_ID]: GOERLI_DISPLAY_NAME,
[MAINNET_CHAIN_ID]: MAINNET_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 createAsyncMiddleware from 'json-rpc-engine/src/createAsyncMiddleware'
import { formatTxMetaForRpcResult } from '../util' import { formatTxMetaForRpcResult } from '../util'
export function createPendingNonceMiddleware ({ getPendingNonce }) { export function createPendingNonceMiddleware({ getPendingNonce }) {
return createAsyncMiddleware(async (req, res, next) => { return createAsyncMiddleware(async (req, res, next) => {
const { method, params } = req const { method, params } = req
if (method !== 'eth_getTransactionCount') { 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) => { return createAsyncMiddleware(async (req, res, next) => {
const { method, params } = req const { method, params } = req
if (method !== 'eth_getTransactionByHash') { if (method !== 'eth_getTransactionByHash') {

View File

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

View File

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

View File

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

View File

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

View File

@ -14,8 +14,7 @@ import {
* and permissions-related methods. * and permissions-related methods.
*/ */
export default class PermissionsLogController { export default class PermissionsLogController {
constructor({ restrictedMethods, store }) {
constructor ({ restrictedMethods, store }) {
this.restrictedMethods = restrictedMethods this.restrictedMethods = restrictedMethods
this.store = store this.store = store
} }
@ -25,7 +24,7 @@ export default class PermissionsLogController {
* *
* @returns {Array<Object>} The activity log. * @returns {Array<Object>} The activity log.
*/ */
getActivityLog () { getActivityLog() {
return this.store.getState()[LOG_STORE_KEY] || [] return this.store.getState()[LOG_STORE_KEY] || []
} }
@ -34,7 +33,7 @@ export default class PermissionsLogController {
* *
* @param {Array<Object>} logs - The new activity log array. * @param {Array<Object>} logs - The new activity log array.
*/ */
updateActivityLog (logs) { updateActivityLog(logs) {
this.store.updateState({ [LOG_STORE_KEY]: logs }) this.store.updateState({ [LOG_STORE_KEY]: logs })
} }
@ -43,7 +42,7 @@ export default class PermissionsLogController {
* *
* @returns {Object} The permissions history log. * @returns {Object} The permissions history log.
*/ */
getHistory () { getHistory() {
return this.store.getState()[HISTORY_STORE_KEY] || {} return this.store.getState()[HISTORY_STORE_KEY] || {}
} }
@ -52,7 +51,7 @@ export default class PermissionsLogController {
* *
* @param {Object} history - The new permissions history log object. * @param {Object} history - The new permissions history log object.
*/ */
updateHistory (history) { updateHistory(history) {
this.store.updateState({ [HISTORY_STORE_KEY]: 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 {string} origin - The origin that the accounts are exposed to.
* @param {Array<string>} accounts - The accounts. * @param {Array<string>} accounts - The accounts.
*/ */
updateAccountsHistory (origin, accounts) { updateAccountsHistory(origin, accounts) {
if (accounts.length === 0) { if (accounts.length === 0) {
return return
} }
@ -88,9 +86,8 @@ export default class PermissionsLogController {
* *
* @returns {JsonRpcEngineMiddleware} The permissions log middleware. * @returns {JsonRpcEngineMiddleware} The permissions log middleware.
*/ */
createMiddleware () { createMiddleware() {
return (req, res, next, _end) => { return (req, res, next, _end) => {
let activityEntry, requestedMethods let activityEntry, requestedMethods
const { origin, method } = req const { origin, method } = req
const isInternal = method.startsWith(WALLET_PREFIX) const isInternal = method.startsWith(WALLET_PREFIX)
@ -100,7 +97,6 @@ export default class PermissionsLogController {
!LOG_IGNORE_METHODS.includes(method) && !LOG_IGNORE_METHODS.includes(method) &&
(isInternal || this.restrictedMethods.includes(method)) (isInternal || this.restrictedMethods.includes(method))
) { ) {
activityEntry = this.logRequest(req, isInternal) activityEntry = this.logRequest(req, isInternal)
if (method === `${WALLET_PREFIX}requestPermissions`) { if (method === `${WALLET_PREFIX}requestPermissions`) {
@ -109,7 +105,6 @@ export default class PermissionsLogController {
requestedMethods = this.getRequestedMethods(req) requestedMethods = this.getRequestedMethods(req)
} }
} else if (method === 'eth_requestAccounts') { } else if (method === 'eth_requestAccounts') {
// eth_requestAccounts is a special case; we need to extract the accounts // eth_requestAccounts is a special case; we need to extract the accounts
// from it // from it
activityEntry = this.logRequest(req, isInternal) activityEntry = this.logRequest(req, isInternal)
@ -122,7 +117,6 @@ export default class PermissionsLogController {
// call next with a return handler for capturing the response // call next with a return handler for capturing the response
next((cb) => { next((cb) => {
const time = Date.now() const time = Date.now()
this.logResponse(activityEntry, res, time) this.logResponse(activityEntry, res, time)
@ -130,7 +124,10 @@ export default class PermissionsLogController {
// any permissions or accounts changes will be recorded on the response, // any permissions or accounts changes will be recorded on the response,
// so we only log permissions history here // so we only log permissions history here
this.logPermissionsHistory( this.logPermissionsHistory(
requestedMethods, origin, res.result, time, requestedMethods,
origin,
res.result,
time,
method === 'eth_requestAccounts', method === 'eth_requestAccounts',
) )
} }
@ -145,13 +142,13 @@ export default class PermissionsLogController {
* @param {Object} request - The request object. * @param {Object} request - The request object.
* @param {boolean} isInternal - Whether the request is internal. * @param {boolean} isInternal - Whether the request is internal.
*/ */
logRequest (request, isInternal) { logRequest(request, isInternal) {
const activityEntry = { const activityEntry = {
id: request.id, id: request.id,
method: request.method, method: request.method,
methodType: ( methodType: isInternal
isInternal ? LOG_METHOD_TYPES.internal : LOG_METHOD_TYPES.restricted ? LOG_METHOD_TYPES.internal
), : LOG_METHOD_TYPES.restricted,
origin: request.origin, origin: request.origin,
request: cloneDeep(request), request: cloneDeep(request),
requestTime: Date.now(), requestTime: Date.now(),
@ -171,8 +168,7 @@ export default class PermissionsLogController {
* @param {Object} response - The response object. * @param {Object} response - The response object.
* @param {number} time - Output from Date.now() * @param {number} time - Output from Date.now()
*/ */
logResponse (entry, response, time) { logResponse(entry, response, time) {
if (!entry || !response) { if (!entry || !response) {
return return
} }
@ -188,8 +184,7 @@ export default class PermissionsLogController {
* *
* @param {Object} entry - The activity log entry. * @param {Object} entry - The activity log entry.
*/ */
commitNewActivity (entry) { commitNewActivity(entry) {
const logs = this.getActivityLog() const logs = this.getActivityLog()
// add new entry to end of log // 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 {string} time - The time of the request, i.e. Date.now().
* @param {boolean} isEthRequestAccounts - Whether the permissions request was 'eth_requestAccounts'. * @param {boolean} isEthRequestAccounts - Whether the permissions request was 'eth_requestAccounts'.
*/ */
logPermissionsHistory ( logPermissionsHistory(
requestedMethods, origin, result, requestedMethods,
time, isEthRequestAccounts, origin,
result,
time,
isEthRequestAccounts,
) { ) {
let accounts, newEntries let accounts, newEntries
if (isEthRequestAccounts) { if (isEthRequestAccounts) {
accounts = result accounts = result
const accountToTimeMap = getAccountToTimeMap(accounts, time) const accountToTimeMap = getAccountToTimeMap(accounts, time)
newEntries = { newEntries = {
'eth_accounts': { eth_accounts: {
accounts: accountToTimeMap, accounts: accountToTimeMap,
lastApproved: time, lastApproved: time,
}, },
} }
} else { } else {
// Records new "lastApproved" times for the granted permissions, if any. // Records new "lastApproved" times for the granted permissions, if any.
// Special handling for eth_accounts, in order to record the time the // Special handling for eth_accounts, in order to record the time the
// accounts were last seen or approved by the origin. // accounts were last seen or approved by the origin.
newEntries = result newEntries = result
.map((perm) => { .map((perm) => {
if (perm.parentCapability === 'eth_accounts') { if (perm.parentCapability === 'eth_accounts') {
accounts = this.getAccountsFromPermission(perm) accounts = this.getAccountsFromPermission(perm)
} }
@ -245,13 +239,10 @@ export default class PermissionsLogController {
return perm.parentCapability return perm.parentCapability
}) })
.reduce((acc, method) => { .reduce((acc, method) => {
// all approved permissions will be included in the response, // all approved permissions will be included in the response,
// not just the newly requested ones // not just the newly requested ones
if (requestedMethods.includes(method)) { if (requestedMethods.includes(method)) {
if (method === 'eth_accounts') { if (method === 'eth_accounts') {
const accountToTimeMap = getAccountToTimeMap(accounts, time) const accountToTimeMap = getAccountToTimeMap(accounts, time)
acc[method] = { acc[method] = {
@ -280,8 +271,7 @@ export default class PermissionsLogController {
* @param {string} origin - The requesting origin. * @param {string} origin - The requesting origin.
* @param {Object} newEntries - The new entries to commit. * @param {Object} newEntries - The new entries to commit.
*/ */
commitNewHistory (origin, newEntries) { commitNewHistory(origin, newEntries) {
// a simple merge updates most permissions // a simple merge updates most permissions
const history = this.getHistory() const history = this.getHistory()
const newOriginHistory = { const newOriginHistory = {
@ -291,19 +281,16 @@ export default class PermissionsLogController {
// eth_accounts requires special handling, because of information // eth_accounts requires special handling, because of information
// we store about the accounts // we store about the accounts
const existingEthAccountsEntry = ( const existingEthAccountsEntry =
history[origin] && history[origin].eth_accounts history[origin] && history[origin].eth_accounts
)
const newEthAccountsEntry = newEntries.eth_accounts const newEthAccountsEntry = newEntries.eth_accounts
if (existingEthAccountsEntry && newEthAccountsEntry) { if (existingEthAccountsEntry && newEthAccountsEntry) {
// we may intend to update just the accounts, not the permission // we may intend to update just the accounts, not the permission
// itself // itself
const lastApproved = ( const lastApproved =
newEthAccountsEntry.lastApproved || newEthAccountsEntry.lastApproved ||
existingEthAccountsEntry.lastApproved existingEthAccountsEntry.lastApproved
)
// merge old and new eth_accounts history entries // merge old and new eth_accounts history entries
newOriginHistory.eth_accounts = { newOriginHistory.eth_accounts = {
@ -326,7 +313,7 @@ export default class PermissionsLogController {
* @param {Object} request - The request object. * @param {Object} request - The request object.
* @returns {Array<string>} The names of the requested permissions. * @returns {Array<string>} The names of the requested permissions.
*/ */
getRequestedMethods (request) { getRequestedMethods(request) {
if ( if (
!request.params || !request.params ||
!request.params[0] || !request.params[0] ||
@ -345,20 +332,17 @@ export default class PermissionsLogController {
* @param {Object} perm - The permissions object. * @param {Object} perm - The permissions object.
* @returns {Array<string>} The permitted accounts. * @returns {Array<string>} The permitted accounts.
*/ */
getAccountsFromPermission (perm) { getAccountsFromPermission(perm) {
if (perm.parentCapability !== 'eth_accounts' || !perm.caveats) { if (perm.parentCapability !== 'eth_accounts' || !perm.caveats) {
return [] return []
} }
const accounts = new Set() const accounts = new Set()
for (const caveat of perm.caveats) { for (const caveat of perm.caveats) {
if ( if (
caveat.name === CAVEAT_NAMES.exposedAccounts && caveat.name === CAVEAT_NAMES.exposedAccounts &&
Array.isArray(caveat.value) Array.isArray(caveat.value)
) { ) {
for (const value of caveat.value) { for (const value of caveat.value) {
accounts.add(value) accounts.add(value)
} }
@ -377,8 +361,6 @@ export default class PermissionsLogController {
* @param {number} time - A time, e.g. Date.now(). * @param {number} time - A time, e.g. Date.now().
* @returns {Object} A string:number map of addresses to time. * @returns {Object} A string:number map of addresses to time.
*/ */
function getAccountToTimeMap (accounts, time) { function getAccountToTimeMap(accounts, time) {
return accounts.reduce( return accounts.reduce((acc, account) => ({ ...acc, [account]: time }), {})
(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. * Create middleware for handling certain methods and preprocessing permissions requests.
*/ */
export default function createPermissionsMethodMiddleware ({ export default function createPermissionsMethodMiddleware({
addDomainMetadata, addDomainMetadata,
getAccounts, getAccounts,
getUnlockPromise, getUnlockPromise,
@ -12,26 +12,21 @@ export default function createPermissionsMethodMiddleware ({
notifyAccountsChanged, notifyAccountsChanged,
requestAccountsPermission, requestAccountsPermission,
}) { }) {
let isProcessingRequestAccounts = false let isProcessingRequestAccounts = false
return createAsyncMiddleware(async (req, res, next) => { return createAsyncMiddleware(async (req, res, next) => {
let responseHandler let responseHandler
switch (req.method) { switch (req.method) {
// Intercepting eth_accounts requests for backwards compatibility: // Intercepting eth_accounts requests for backwards compatibility:
// The getAccounts call below wraps the rpc-cap middleware, and returns // The getAccounts call below wraps the rpc-cap middleware, and returns
// an empty array in case of errors (such as 4100:unauthorized) // an empty array in case of errors (such as 4100:unauthorized)
case 'eth_accounts': { case 'eth_accounts': {
res.result = await getAccounts() res.result = await getAccounts()
return return
} }
case 'eth_requestAccounts': { case 'eth_requestAccounts': {
if (isProcessingRequestAccounts) { if (isProcessingRequestAccounts) {
res.error = ethErrors.rpc.resourceUnavailable( res.error = ethErrors.rpc.resourceUnavailable(
'Already processing eth_requestAccounts. Please wait.', 'Already processing eth_requestAccounts. Please wait.',
@ -79,7 +74,6 @@ export default function createPermissionsMethodMiddleware ({
// custom method for getting metadata from the requesting domain, // custom method for getting metadata from the requesting domain,
// sent automatically by the inpage provider when it's initialized // sent automatically by the inpage provider when it's initialized
case 'wallet_sendDomainMetadata': { case 'wallet_sendDomainMetadata': {
if (typeof req.domainMetadata?.name === 'string') { if (typeof req.domainMetadata?.name === 'string') {
addDomainMetadata(req.origin, req.domainMetadata) addDomainMetadata(req.origin, req.domainMetadata)
} }
@ -89,11 +83,8 @@ export default function createPermissionsMethodMiddleware ({
// register return handler to send accountsChanged notification // register return handler to send accountsChanged notification
case 'wallet_requestPermissions': { case 'wallet_requestPermissions': {
if ('eth_accounts' in req.params?.[0]) { if ('eth_accounts' in req.params?.[0]) {
responseHandler = async () => { responseHandler = async () => {
if (Array.isArray(res.result)) { if (Array.isArray(res.result)) {
for (const permission of res.result) { for (const permission of res.result) {
if (permission.parentCapability === 'eth_accounts') { if (permission.parentCapability === 'eth_accounts') {

View File

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

View File

@ -9,28 +9,27 @@ import { addInternalMethodPrefix } from './permissions'
import { NETWORK_TYPE_TO_ID_MAP } from './network/enums' import { NETWORK_TYPE_TO_ID_MAP } from './network/enums'
export default class PreferencesController { export default class PreferencesController {
/** /**
* *
* @typedef {Object} PreferencesController * @typedef {Object} PreferencesController
* @param {Object} opts - Overrides the defaults for the initial state of this.store * @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 {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.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 {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.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.assetImages Contains assets objects related to assets added
* @property {boolean} store.useBlockie The users preference for blockie identicons within the UI * @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 {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. * 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. * 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.currentLocale The preferred language locale key
* @property {string} store.selectedAddress A hex string that matches the currently selected address in the app * @property {string} store.selectedAddress A hex string that matches the currently selected address in the app
* *
*/ */
constructor (opts = {}) { constructor(opts = {}) {
const initState = { const initState = {
frequentRpcListDetail: [], frequentRpcListDetail: [],
accountTokens: {}, accountTokens: {},
@ -66,7 +65,8 @@ export default class PreferencesController {
metaMetricsSendCount: 0, metaMetricsSendCount: 0,
// ENS decentralized website resolution // ENS decentralized website resolution
ipfsGateway: 'dweb.link', ...opts.initState, ipfsGateway: 'dweb.link',
...opts.initState,
} }
this.network = opts.network this.network = opts.network
@ -86,7 +86,7 @@ export default class PreferencesController {
* Sets the {@code forgottenPassword} state property * Sets the {@code forgottenPassword} state property
* @param {boolean} forgottenPassword - whether or not the user has forgotten their password * @param {boolean} forgottenPassword - whether or not the user has forgotten their password
*/ */
setPasswordForgotten (forgottenPassword) { setPasswordForgotten(forgottenPassword) {
this.store.updateState({ forgottenPassword }) this.store.updateState({ forgottenPassword })
} }
@ -96,7 +96,7 @@ export default class PreferencesController {
* @param {boolean} val - Whether or not the user prefers blockie indicators * @param {boolean} val - Whether or not the user prefers blockie indicators
* *
*/ */
setUseBlockie (val) { setUseBlockie(val) {
this.store.updateState({ useBlockie: 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 * @param {boolean} val - Whether or not the user prefers to set nonce
* *
*/ */
setUseNonceField (val) { setUseNonceField(val) {
this.store.updateState({ useNonceField: 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 * @param {boolean} val - Whether or not the user prefers phishing domain protection
* *
*/ */
setUsePhishDetect (val) { setUsePhishDetect(val) {
this.store.updateState({ usePhishDetect: val }) this.store.updateState({ usePhishDetect: val })
} }
@ -124,14 +124,19 @@ export default class PreferencesController {
* Setter for the `participateInMetaMetrics` property * Setter for the `participateInMetaMetrics` property
* *
* @param {boolean} bool - Whether or not the user wants to participate in MetaMetrics * @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 }) this.store.updateState({ participateInMetaMetrics: bool })
let metaMetricsId = null let metaMetricsId = null
if (bool && !this.store.getState().metaMetricsId) { 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 }) this.store.updateState({ metaMetricsId })
} else if (bool === false) { } else if (bool === false) {
this.store.updateState({ metaMetricsId }) this.store.updateState({ metaMetricsId })
@ -139,11 +144,11 @@ export default class PreferencesController {
return metaMetricsId return metaMetricsId
} }
getParticipateInMetaMetrics () { getParticipateInMetaMetrics() {
return this.store.getState().participateInMetaMetrics return this.store.getState().participateInMetaMetrics
} }
setMetaMetricsSendCount (val) { setMetaMetricsSendCount(val) {
this.store.updateState({ metaMetricsSendCount: 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 * @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 }) this.store.updateState({ firstTimeFlowType: type })
} }
getSuggestedTokens () { getSuggestedTokens() {
return this.store.getState().suggestedTokens return this.store.getState().suggestedTokens
} }
getAssetImages () { getAssetImages() {
return this.store.getState().assetImages return this.store.getState().assetImages
} }
addSuggestedERC20Asset (tokenOpts) { addSuggestedERC20Asset(tokenOpts) {
this._validateERC20AssetParams(tokenOpts) this._validateERC20AssetParams(tokenOpts)
const suggested = this.getSuggestedTokens() const suggested = this.getSuggestedTokens()
const { rawAddress, symbol, decimals, image } = tokenOpts const { rawAddress, symbol, decimals, image } = tokenOpts
@ -181,7 +186,7 @@ export default class PreferencesController {
* @param {string} fourBytePrefix - Four-byte method signature * @param {string} fourBytePrefix - Four-byte method signature
* @param {string} methodData - Corresponding data method * @param {string} methodData - Corresponding data method
*/ */
addKnownMethodData (fourBytePrefix, methodData) { addKnownMethodData(fourBytePrefix, methodData) {
const { knownMethodData } = this.store.getState() const { knownMethodData } = this.store.getState()
knownMethodData[fourBytePrefix] = methodData knownMethodData[fourBytePrefix] = methodData
this.store.updateState({ knownMethodData }) this.store.updateState({ knownMethodData })
@ -190,12 +195,12 @@ export default class PreferencesController {
/** /**
* RPC engine middleware for requesting new asset added * RPC engine middleware for requesting new asset added
* *
* @param req * @param {any} req
* @param res * @param {any} res
* @param {Function} - next * @param {Function} next
* @param {Function} - end * @param {Function} end
*/ */
async requestWatchAsset (req, res, next, end) { async requestWatchAsset(req, res, next, end) {
if ( if (
req.method === 'metamask_watchAsset' || req.method === 'metamask_watchAsset' ||
req.method === addInternalMethodPrefix('watchAsset') req.method === addInternalMethodPrefix('watchAsset')
@ -227,8 +232,10 @@ export default class PreferencesController {
* @param {string} key - he preferred language locale key * @param {string} key - he preferred language locale key
* *
*/ */
setCurrentLocale (key) { setCurrentLocale(key) {
const textDirection = (['ar', 'dv', 'fa', 'he', 'ku'].includes(key)) ? 'rtl' : 'auto' const textDirection = ['ar', 'dv', 'fa', 'he', 'ku'].includes(key)
? 'rtl'
: 'auto'
this.store.updateState({ this.store.updateState({
currentLocale: key, currentLocale: key,
textDirection, textDirection,
@ -243,7 +250,7 @@ export default class PreferencesController {
* @param {string[]} addresses - An array of hex addresses * @param {string[]} addresses - An array of hex addresses
* *
*/ */
setAddresses (addresses) { setAddresses(addresses) {
const oldIdentities = this.store.getState().identities const oldIdentities = this.store.getState().identities
const oldAccountTokens = this.store.getState().accountTokens const oldAccountTokens = this.store.getState().accountTokens
@ -264,9 +271,9 @@ export default class PreferencesController {
* Removes an address from state * Removes an address from state
* *
* @param {string} address - A hex address * @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 { identities } = this.store.getState()
const { accountTokens } = this.store.getState() const { accountTokens } = this.store.getState()
if (!identities[address]) { if (!identities[address]) {
@ -291,7 +298,7 @@ export default class PreferencesController {
* @param {string[]} addresses - An array of hex addresses * @param {string[]} addresses - An array of hex addresses
* *
*/ */
addAddresses (addresses) { addAddresses(addresses) {
const { identities, accountTokens } = this.store.getState() const { identities, accountTokens } = this.store.getState()
addresses.forEach((address) => { addresses.forEach((address) => {
// skip if already exists // skip if already exists
@ -312,10 +319,9 @@ export default class PreferencesController {
* Removes any unknown identities, and returns the resulting selected address. * Removes any unknown identities, and returns the resulting selected address.
* *
* @param {Array<string>} addresses - known to the vault. * @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) { if (!Array.isArray(addresses) || addresses.length === 0) {
throw new Error('Expected non-empty array of addresses.') throw new Error('Expected non-empty array of addresses.')
} }
@ -332,7 +338,6 @@ export default class PreferencesController {
// Identities are no longer present. // Identities are no longer present.
if (Object.keys(newlyLost).length > 0) { if (Object.keys(newlyLost).length > 0) {
// store lost accounts // store lost accounts
Object.keys(newlyLost).forEach((key) => { Object.keys(newlyLost).forEach((key) => {
lostIdentities[key] = newlyLost[key] lostIdentities[key] = newlyLost[key]
@ -353,7 +358,7 @@ export default class PreferencesController {
return selected return selected
} }
removeSuggestedTokens () { removeSuggestedTokens() {
return new Promise((resolve) => { return new Promise((resolve) => {
this.store.updateState({ suggestedTokens: {} }) this.store.updateState({ suggestedTokens: {} })
resolve({}) resolve({})
@ -364,10 +369,10 @@ export default class PreferencesController {
* Setter for the `selectedAddress` property * Setter for the `selectedAddress` property
* *
* @param {string} _address - A new hex address for an account * @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) const address = normalizeAddress(_address)
this._updateTokens(address) this._updateTokens(address)
@ -385,10 +390,10 @@ export default class PreferencesController {
/** /**
* Getter for the `selectedAddress` property * 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 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} rawAddress - Hex address of the token contract. May or may not be a checksum address.
* @param {string} symbol - The symbol of the token * @param {string} symbol - The symbol of the token
* @param {number} decimals - The number of decimals the token uses. * @param {number} decimals - The number of decimals the token uses.
* @returns {Promise<array>} - Promises the new array of AddedToken objects. * @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 address = normalizeAddress(rawAddress)
const newEntry = { address, symbol, decimals } const newEntry = { address, symbol, decimals }
const { tokens } = this.store.getState() const { tokens } = this.store.getState()
@ -437,10 +442,10 @@ export default class PreferencesController {
* Removes a specified token from the tokens array. * Removes a specified token from the tokens array.
* *
* @param {string} rawAddress - Hex address of the token contract to remove. * @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 { tokens } = this.store.getState()
const assetImages = this.getAssetImages() const assetImages = this.getAssetImages()
const updatedTokens = tokens.filter((token) => token.address !== rawAddress) const updatedTokens = tokens.filter((token) => token.address !== rawAddress)
@ -452,10 +457,10 @@ export default class PreferencesController {
/** /**
* A getter for the `tokens` property * 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 return this.store.getState().tokens
} }
@ -465,9 +470,11 @@ export default class PreferencesController {
* @param {string} label - the custom label for the account * @param {string} label - the custom label for the account
* @returns {Promise<string>} * @returns {Promise<string>}
*/ */
setAccountLabel (account, label) { setAccountLabel(account, label) {
if (!account) { 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 address = normalizeAddress(account)
const { identities } = this.store.getState() 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 * @param {Object} [newRpcDetails.rpcPrefs] - Optional RPC preferences, such as the block explorer URL
* *
*/ */
async updateRpc (newRpcDetails) { async updateRpc(newRpcDetails) {
const rpcList = this.getFrequentRpcListDetail() const rpcList = this.getFrequentRpcListDetail()
const index = rpcList.findIndex((element) => { const index = rpcList.findIndex((element) => {
return element.rpcUrl === newRpcDetails.rpcUrl return element.rpcUrl === newRpcDetails.rpcUrl
@ -497,7 +504,6 @@ export default class PreferencesController {
const rpcDetail = rpcList[index] const rpcDetail = rpcList[index]
const updatedRpc = { ...rpcDetail, ...newRpcDetails } const updatedRpc = { ...rpcDetail, ...newRpcDetails }
if (rpcDetail.chainId !== updatedRpc.chainId) { if (rpcDetail.chainId !== updatedRpc.chainId) {
// When the chainId is changed, associated address book entries should // When the chainId is changed, associated address book entries should
// also be migrated. The address book entries are keyed by the `network` state, // 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 // 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 let addressBookKey = rpcDetail.chainId
if (!addressBookKey) { if (!addressBookKey) {
// We need to find the networkId to determine what these addresses were keyed by // 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 { try {
addressBookKey = await provider.send('net_version') addressBookKey = await provider.send('net_version')
assert(typeof addressBookKey === 'string') assert(typeof addressBookKey === 'string')
} catch (error) { } catch (error) {
log.debug(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. // on both networks, since we don't know which network each contact is intended for.
let duplicate = false let duplicate = false
const builtInProviderNetworkIds = Object.values(NETWORK_TYPE_TO_ID_MAP) const builtInProviderNetworkIds = Object.values(
.map((ids) => ids.networkId) NETWORK_TYPE_TO_ID_MAP,
const otherRpcEntries = rpcList ).map((ids) => ids.networkId)
.filter((entry) => entry.rpcUrl !== newRpcDetails.rpcUrl) const otherRpcEntries = rpcList.filter(
(entry) => entry.rpcUrl !== newRpcDetails.rpcUrl,
)
if ( if (
builtInProviderNetworkIds.includes(addressBookKey) || builtInProviderNetworkIds.includes(addressBookKey) ||
otherRpcEntries.some((entry) => entry.chainId === addressBookKey) otherRpcEntries.some((entry) => entry.chainId === addressBookKey)
@ -532,7 +544,11 @@ export default class PreferencesController {
duplicate = true duplicate = true
} }
this.migrateAddressBookState(addressBookKey, updatedRpc.chainId, duplicate) this.migrateAddressBookState(
addressBookKey,
updatedRpc.chainId,
duplicate,
)
} }
rpcList[index] = updatedRpc rpcList[index] = updatedRpc
this.store.updateState({ frequentRpcListDetail: rpcList }) 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 * @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 rpcList = this.getFrequentRpcListDetail()
const index = rpcList.findIndex((element) => { const index = rpcList.findIndex((element) => {
@ -574,10 +596,10 @@ export default class PreferencesController {
* Removes custom RPC url from state. * Removes custom RPC url from state.
* *
* @param {string} url - The RPC url to remove from frequentRpcList. * @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 rpcList = this.getFrequentRpcListDetail()
const index = rpcList.findIndex((element) => { const index = rpcList.findIndex((element) => {
return element.rpcUrl === url return element.rpcUrl === url
@ -592,10 +614,10 @@ export default class PreferencesController {
/** /**
* Getter for the `frequentRpcListDetail` property. * 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 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 {string} feature - A key that corresponds to a UI feature.
* @param {boolean} activated - Indicates whether or not the UI feature should be displayed * @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 currentFeatureFlags = this.store.getState().featureFlags
const updatedFeatureFlags = { const updatedFeatureFlags = {
...currentFeatureFlags, ...currentFeatureFlags,
@ -624,9 +646,9 @@ export default class PreferencesController {
* found in the settings page. * found in the settings page.
* @param {string} preference - The preference to enable or disable. * @param {string} preference - The preference to enable or disable.
* @param {boolean} value - Indicates whether or not the preference should be enabled or disabled. * @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 currentPreferences = this.getPreferences()
const updatedPreferences = { const updatedPreferences = {
...currentPreferences, ...currentPreferences,
@ -639,9 +661,9 @@ export default class PreferencesController {
/** /**
* A getter for the `preferences` property * 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 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 * Sets the completedOnboarding state to true, indicating that the user has completed the
* onboarding process. * onboarding process.
*/ */
completeOnboarding () { completeOnboarding() {
this.store.updateState({ completedOnboarding: true }) this.store.updateState({ completedOnboarding: true })
return Promise.resolve(true) return Promise.resolve(true)
} }
/** /**
* A getter for the `ipfsGateway` property * 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 return this.store.getState().ipfsGateway
} }
/** /**
* A setter for the `ipfsGateway` property * A setter for the `ipfsGateway` property
* @param {string} domain - The new IPFS gateway domain * @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 }) this.store.updateState({ ipfsGateway: domain })
return Promise.resolve(domain) return Promise.resolve(domain)
} }
@ -681,7 +703,7 @@ export default class PreferencesController {
* *
* *
*/ */
_subscribeProviderType () { _subscribeProviderType() {
this.network.providerStore.subscribe(() => { this.network.providerStore.subscribe(() => {
const { tokens } = this._getTokenRelatedStates() const { tokens } = this._getTokenRelatedStates()
this.store.updateState({ tokens }) this.store.updateState({ tokens })
@ -691,11 +713,15 @@ export default class PreferencesController {
/** /**
* Updates `accountTokens` and `tokens` of current account and network according to it. * 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) { _updateAccountTokens(tokens, assetImages) {
const { accountTokens, providerType, selectedAddress } = this._getTokenRelatedStates() const {
accountTokens,
providerType,
selectedAddress,
} = this._getTokenRelatedStates()
accountTokens[selectedAddress][providerType] = tokens accountTokens[selectedAddress][providerType] = tokens
this.store.updateState({ accountTokens, tokens, assetImages }) this.store.updateState({ accountTokens, tokens, assetImages })
} }
@ -706,7 +732,7 @@ export default class PreferencesController {
* @param {string} selectedAddress - Account address to be updated with. * @param {string} selectedAddress - Account address to be updated with.
* *
*/ */
_updateTokens (selectedAddress) { _updateTokens(selectedAddress) {
const { tokens } = this._getTokenRelatedStates(selectedAddress) const { tokens } = this._getTokenRelatedStates(selectedAddress)
this.store.updateState({ tokens }) this.store.updateState({ tokens })
} }
@ -714,11 +740,11 @@ export default class PreferencesController {
/** /**
* A getter for `tokens` and `accountTokens` related states. * A getter for `tokens` and `accountTokens` related states.
* *
* @param {string} [selectedAddress] A new hex address for an account * @param {string} [selectedAddress] - A new hex address for an account
* @returns {Object.<array, object, string, string>} - States to interact with tokens in `accountTokens` * @returns {Object.<array, object, string, string>} States to interact with tokens in `accountTokens`
* *
*/ */
_getTokenRelatedStates (selectedAddress) { _getTokenRelatedStates(selectedAddress) {
const { accountTokens } = this.store.getState() const { accountTokens } = this.store.getState()
if (!selectedAddress) { if (!selectedAddress) {
// eslint-disable-next-line no-param-reassign // eslint-disable-next-line no-param-reassign
@ -738,11 +764,11 @@ export default class PreferencesController {
/** /**
* Handle the suggestion of an ERC20 asset through `watchAsset` * 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) { async _handleWatchAssetERC20(tokenMetadata) {
const { address, symbol, decimals, image } = options const { address, symbol, decimals, image } = tokenMetadata
const rawAddress = address const rawAddress = address
try { try {
this._validateERC20AssetParams({ rawAddress, symbol, decimals }) this._validateERC20AssetParams({ rawAddress, symbol, decimals })
@ -752,7 +778,9 @@ export default class PreferencesController {
const tokenOpts = { rawAddress, decimals, symbol, image } const tokenOpts = { rawAddress, decimals, symbol, image }
this.addSuggestedERC20Asset(tokenOpts) this.addSuggestedERC20Asset(tokenOpts)
return this.openPopup().then(() => { 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 return tokenAddresses.length > 0
}) })
} }
@ -765,17 +793,21 @@ export default class PreferencesController {
* doesn't fulfill requirements * doesn't fulfill requirements
* *
*/ */
_validateERC20AssetParams (opts) { _validateERC20AssetParams(opts) {
const { rawAddress, symbol, decimals } = opts const { rawAddress, symbol, decimals } = opts
if (!rawAddress || !symbol || typeof decimals === 'undefined') { 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)) { if (!(symbol.length < 7)) {
throw new Error(`Invalid symbol ${symbol} more than six characters`) throw new Error(`Invalid symbol ${symbol} more than six characters`)
} }
const numDecimals = parseInt(decimals, 10) const numDecimals = parseInt(decimals, 10)
if (isNaN(numDecimals) || numDecimals > 36 || numDecimals < 0) { 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)) { if (!isValidAddress(rawAddress)) {
throw new Error(`Invalid address ${rawAddress}`) throw new Error(`Invalid address ${rawAddress}`)

View File

@ -2,7 +2,7 @@ import { ethers } from 'ethers'
import log from 'loglevel' import log from 'loglevel'
import BigNumber from 'bignumber.js' import BigNumber from 'bignumber.js'
import ObservableStore from 'obs-store' import ObservableStore from 'obs-store'
import { mapValues } from 'lodash' import { mapValues, cloneDeep } from 'lodash'
import abi from 'human-standard-token-abi' import abi from 'human-standard-token-abi'
import { calcTokenAmount } from '../../../ui/app/helpers/utils/token-util' import { calcTokenAmount } from '../../../ui/app/helpers/utils/token-util'
import { calcGasTotal } from '../../../ui/app/pages/send/send.utils' 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. // 3 seems to be an appropriate balance of giving users the time they need when MetaMask is not left idle, and turning polling off when it is.
const POLL_COUNT_LIMIT = 3 const POLL_COUNT_LIMIT = 3
function calculateGasEstimateWithRefund (maxGas = MAX_GAS_LIMIT, estimatedRefund = 0, estimatedGas = 0) { function calculateGasEstimateWithRefund(
const maxGasMinusRefund = new BigNumber( maxGas = MAX_GAS_LIMIT,
maxGas, estimatedRefund = 0,
10, estimatedGas = 0,
) ) {
.minus(estimatedRefund, 10) const maxGasMinusRefund = new BigNumber(maxGas, 10).minus(estimatedRefund, 10)
const gasEstimateWithRefund = maxGasMinusRefund.lt( const gasEstimateWithRefund = maxGasMinusRefund.lt(estimatedGas, 16)
estimatedGas,
16,
)
? maxGasMinusRefund.toString(16) ? maxGasMinusRefund.toString(16)
: estimatedGas : estimatedGas
@ -68,7 +65,7 @@ const initialState = {
} }
export default class SwapsController { export default class SwapsController {
constructor ({ constructor({
getBufferedGasLimit, getBufferedGasLimit,
networkController, networkController,
provider, 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 // 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. // 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 // 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(() => { this.pollingTimeout = setTimeout(() => {
const { swapsState } = this.store.getState() const { swapsState } = this.store.getState()
this.fetchAndSetQuotes(swapsState.fetchParams, swapsState.fetchParams.metaData, true) this.fetchAndSetQuotes(
swapsState.fetchParams,
swapsState.fetchParams?.metaData,
true,
)
}, QUOTE_POLLING_INTERVAL) }, QUOTE_POLLING_INTERVAL)
} }
stopPollingForQuotes () { stopPollingForQuotes() {
clearTimeout(this.pollingTimeout) clearTimeout(this.pollingTimeout)
} }
async fetchAndSetQuotes (fetchParams, fetchParamsMetaData = {}, isPolledRequest) { async fetchAndSetQuotes(
fetchParams,
fetchParamsMetaData = {},
isPolledRequest,
) {
if (!fetchParams) { if (!fetchParams) {
return null return null
} }
@ -150,7 +155,10 @@ export default class SwapsController {
const quotesLastFetched = Date.now() const quotesLastFetched = Date.now()
let approvalRequired = false let approvalRequired = false
if (fetchParams.sourceToken !== ETH_SWAPS_TOKEN_ADDRESS && Object.values(newQuotes).length) { if (
fetchParams.sourceToken !== ETH_SWAPS_TOKEN_ADDRESS &&
Object.values(newQuotes).length
) {
const allowance = await this._getERC20Allowance( const allowance = await this._getERC20Allowance(
fetchParams.sourceToken, fetchParams.sourceToken,
fetchParams.fromAddress, fetchParams.fromAddress,
@ -167,7 +175,9 @@ export default class SwapsController {
approvalNeeded: null, approvalNeeded: null,
})) }))
} else if (!isPolledRequest) { } 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) => ({ newQuotes = mapValues(newQuotes, (quote) => ({
...quote, ...quote,
@ -190,13 +200,12 @@ export default class SwapsController {
if (Object.values(newQuotes).length === 0) { if (Object.values(newQuotes).length === 0) {
this.setSwapsErrorKey(QUOTES_NOT_AVAILABLE_ERROR) this.setSwapsErrorKey(QUOTES_NOT_AVAILABLE_ERROR)
} else { } else {
const topQuoteData = await this._findTopQuoteAndCalculateSavings(newQuotes) const [
_topAggId,
if (topQuoteData.topAggId) { quotesWithSavingsAndFeeData,
topAggId = topQuoteData.topAggId ] = await this._findTopQuoteAndCalculateSavings(newQuotes)
newQuotes[topAggId].isBestQuote = topQuoteData.isBest topAggId = _topAggId
newQuotes[topAggId].savings = topQuoteData.savings newQuotes = quotesWithSavingsAndFeeData
}
} }
// If a newer call has been made, don't update state with old information // 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] return [newQuotes, topAggId]
} }
safeRefetchQuotes () { safeRefetchQuotes() {
const { swapsState } = this.store.getState() const { swapsState } = this.store.getState()
if (!this.pollingTimeout && swapsState.fetchParams) { if (!this.pollingTimeout && swapsState.fetchParams) {
this.fetchAndSetQuotes(swapsState.fetchParams) this.fetchAndSetQuotes(swapsState.fetchParams)
} }
} }
setSelectedQuoteAggId (selectedAggId) { setSelectedQuoteAggId(selectedAggId) {
const { swapsState } = this.store.getState() const { swapsState } = this.store.getState()
this.store.updateState({ swapsState: { ...swapsState, selectedAggId } }) this.store.updateState({ swapsState: { ...swapsState, selectedAggId } })
} }
setSwapsTokens (tokens) { setSwapsTokens(tokens) {
const { swapsState } = this.store.getState() const { swapsState } = this.store.getState()
this.store.updateState({ swapsState: { ...swapsState, tokens } }) this.store.updateState({ swapsState: { ...swapsState, tokens } })
} }
setSwapsErrorKey (errorKey) { setSwapsErrorKey(errorKey) {
const { swapsState } = this.store.getState() const { swapsState } = this.store.getState()
this.store.updateState({ swapsState: { ...swapsState, errorKey } }) this.store.updateState({ swapsState: { ...swapsState, errorKey } })
} }
async getAllQuotesWithGasEstimates (quotes) { async getAllQuotesWithGasEstimates(quotes) {
const quoteGasData = await Promise.all( const quoteGasData = await Promise.all(
Object.values(quotes).map(async (quote) => { 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] return [gasLimit, simulationFails, quote.aggregator]
}), }),
) )
@ -268,7 +279,11 @@ export default class SwapsController {
const newQuotes = {} const newQuotes = {}
quoteGasData.forEach(([gasLimit, simulationFails, aggId]) => { quoteGasData.forEach(([gasLimit, simulationFails, aggId]) => {
if (gasLimit && !simulationFails) { 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] = { newQuotes[aggId] = {
...quotes[aggId], ...quotes[aggId],
@ -285,7 +300,7 @@ export default class SwapsController {
return newQuotes return newQuotes
} }
timedoutGasReturn (tradeTxParams) { timedoutGasReturn(tradeTxParams) {
return new Promise((resolve) => { return new Promise((resolve) => {
let gasTimedOut = false let gasTimedOut = false
@ -321,7 +336,7 @@ export default class SwapsController {
}) })
} }
async setInitialGasEstimate (initialAggId) { async setInitialGasEstimate(initialAggId) {
const { swapsState } = this.store.getState() const { swapsState } = this.store.getState()
const quoteToUpdate = { ...swapsState.quotes[initialAggId] } const quoteToUpdate = { ...swapsState.quotes[initialAggId] }
@ -332,64 +347,73 @@ export default class SwapsController {
} = await this.timedoutGasReturn(quoteToUpdate.trade) } = await this.timedoutGasReturn(quoteToUpdate.trade)
if (newGasEstimate && !simulationFails) { if (newGasEstimate && !simulationFails) {
const gasEstimateWithRefund = calculateGasEstimateWithRefund(quoteToUpdate.maxGas, quoteToUpdate.estimatedRefund, newGasEstimate) const gasEstimateWithRefund = calculateGasEstimateWithRefund(
quoteToUpdate.maxGas,
quoteToUpdate.estimatedRefund,
newGasEstimate,
)
quoteToUpdate.gasEstimate = newGasEstimate quoteToUpdate.gasEstimate = newGasEstimate
quoteToUpdate.gasEstimateWithRefund = gasEstimateWithRefund quoteToUpdate.gasEstimateWithRefund = gasEstimateWithRefund
} }
this.store.updateState({ 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() const { swapsState } = this.store.getState()
this.store.updateState({ swapsState: { ...swapsState, approveTxId } }) this.store.updateState({ swapsState: { ...swapsState, approveTxId } })
} }
setTradeTxId (tradeTxId) { setTradeTxId(tradeTxId) {
const { swapsState } = this.store.getState() const { swapsState } = this.store.getState()
this.store.updateState({ swapsState: { ...swapsState, tradeTxId } }) this.store.updateState({ swapsState: { ...swapsState, tradeTxId } })
} }
setQuotesLastFetched (quotesLastFetched) { setQuotesLastFetched(quotesLastFetched) {
const { swapsState } = this.store.getState() const { swapsState } = this.store.getState()
this.store.updateState({ swapsState: { ...swapsState, quotesLastFetched } }) this.store.updateState({ swapsState: { ...swapsState, quotesLastFetched } })
} }
setSwapsTxGasPrice (gasPrice) { setSwapsTxGasPrice(gasPrice) {
const { swapsState } = this.store.getState() const { swapsState } = this.store.getState()
this.store.updateState({ this.store.updateState({
swapsState: { ...swapsState, customGasPrice: gasPrice }, swapsState: { ...swapsState, customGasPrice: gasPrice },
}) })
} }
setSwapsTxGasLimit (gasLimit) { setSwapsTxGasLimit(gasLimit) {
const { swapsState } = this.store.getState() const { swapsState } = this.store.getState()
this.store.updateState({ this.store.updateState({
swapsState: { ...swapsState, customMaxGas: gasLimit }, swapsState: { ...swapsState, customMaxGas: gasLimit },
}) })
} }
setCustomApproveTxData (data) { setCustomApproveTxData(data) {
const { swapsState } = this.store.getState() const { swapsState } = this.store.getState()
this.store.updateState({ this.store.updateState({
swapsState: { ...swapsState, customApproveTxData: data }, swapsState: { ...swapsState, customApproveTxData: data },
}) })
} }
setBackgroundSwapRouteState (routeState) { setBackgroundSwapRouteState(routeState) {
const { swapsState } = this.store.getState() const { swapsState } = this.store.getState()
this.store.updateState({ swapsState: { ...swapsState, routeState } }) this.store.updateState({ swapsState: { ...swapsState, routeState } })
} }
setSwapsLiveness (swapsFeatureIsLive) { setSwapsLiveness(swapsFeatureIsLive) {
const { swapsState } = this.store.getState() const { swapsState } = this.store.getState()
this.store.updateState({ swapsState: { ...swapsState, swapsFeatureIsLive } }) this.store.updateState({
swapsState: { ...swapsState, swapsFeatureIsLive },
})
} }
resetPostFetchState () { resetPostFetchState() {
const { swapsState } = this.store.getState() const { swapsState } = this.store.getState()
this.store.updateState({ this.store.updateState({
@ -403,21 +427,25 @@ export default class SwapsController {
clearTimeout(this.pollingTimeout) clearTimeout(this.pollingTimeout)
} }
resetSwapsState () { resetSwapsState() {
const { swapsState } = this.store.getState() const { swapsState } = this.store.getState()
this.store.updateState({ this.store.updateState({
swapsState: { ...initialState.swapsState, tokens: swapsState.tokens, swapsFeatureIsLive: swapsState.swapsFeatureIsLive }, swapsState: {
...initialState.swapsState,
tokens: swapsState.tokens,
swapsFeatureIsLive: swapsState.swapsFeatureIsLive,
},
}) })
clearTimeout(this.pollingTimeout) clearTimeout(this.pollingTimeout)
} }
async _getEthersGasPrice () { async _getEthersGasPrice() {
const ethersGasPrice = await this.ethersProvider.getGasPrice() const ethersGasPrice = await this.ethersProvider.getGasPrice()
return ethersGasPrice.toHexString() return ethersGasPrice.toHexString()
} }
async _findTopQuoteAndCalculateSavings (quotes = {}) { async _findTopQuoteAndCalculateSavings(quotes = {}) {
const tokenConversionRates = this.tokenRatesStore.getState() const tokenConversionRates = this.tokenRatesStore.getState()
.contractExchangeRates .contractExchangeRates
const { const {
@ -429,15 +457,14 @@ export default class SwapsController {
return {} return {}
} }
const usedGasPrice = customGasPrice || await this._getEthersGasPrice() const newQuotes = cloneDeep(quotes)
let topAggId = '' const usedGasPrice = customGasPrice || (await this._getEthersGasPrice())
let ethTradeValueOfBestQuote = null
let ethFeeForBestQuote = null
const allEthTradeValues = []
const allEthFees = []
Object.values(quotes).forEach((quote) => { let topAggId = null
let overallValueOfBestQuoteForSorting = null
Object.values(newQuotes).forEach((quote) => {
const { const {
aggregator, aggregator,
approvalNeeded, approvalNeeded,
@ -449,6 +476,7 @@ export default class SwapsController {
sourceAmount, sourceAmount,
sourceToken, sourceToken,
trade, trade,
fee: metaMaskFee,
} = quote } = quote
const tradeGasLimitForCalculation = gasEstimate const tradeGasLimitForCalculation = gasEstimate
@ -468,8 +496,10 @@ export default class SwapsController {
// It always includes any external fees charged by the quote source. In // It always includes any external fees charged by the quote source. In
// addition, if the source asset is ETH, trade.value includes the amount // addition, if the source asset is ETH, trade.value includes the amount
// of swapped ETH. // of swapped ETH.
const totalWeiCost = new BigNumber(gasTotalInWeiHex, 16) const totalWeiCost = new BigNumber(gasTotalInWeiHex, 16).plus(
.plus(trade.value, 16) trade.value,
16,
)
const totalEthCost = conversionUtil(totalWeiCost, { const totalEthCost = conversionUtil(totalWeiCost, {
fromCurrency: 'ETH', fromCurrency: 'ETH',
@ -482,81 +512,122 @@ export default class SwapsController {
// The total fee is aggregator/exchange fees plus gas fees. // The total fee is aggregator/exchange fees plus gas fees.
// If the swap is from ETH, subtract the sourceAmount from the total cost. // If the swap is from ETH, subtract the sourceAmount from the total cost.
// Otherwise, the total fee is simply trade.value plus gas fees. // Otherwise, the total fee is simply trade.value plus gas fees.
const ethFee = sourceToken === ETH_SWAPS_TOKEN_ADDRESS const ethFee =
? conversionUtil( sourceToken === ETH_SWAPS_TOKEN_ADDRESS
totalWeiCost.minus(sourceAmount, 10), // sourceAmount is in wei ? conversionUtil(
{ totalWeiCost.minus(sourceAmount, 10), // sourceAmount is in wei
fromCurrency: 'ETH', {
fromDenomination: 'WEI', fromCurrency: 'ETH',
toDenomination: 'ETH', fromDenomination: 'WEI',
fromNumericBase: 'BN', toDenomination: 'ETH',
numberOfDecimals: 6, fromNumericBase: 'BN',
}, numberOfDecimals: 6,
) },
: totalEthCost )
: 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 tokenConversionRate = tokenConversionRates[destinationToken]
const ethValueOfTrade = const conversionRateForSorting = tokenConversionRate || 1
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)
// collect values for savings calculation const ethValueOfTokens = decimalAdjustedDestinationAmount.times(
allEthTradeValues.push(ethValueOfTrade) conversionRateForSorting,
allEthFees.push(ethFee) 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 ( if (
ethTradeValueOfBestQuote === null || overallValueOfBestQuoteForSorting === null ||
ethValueOfTrade.gt(ethTradeValueOfBestQuote) overallValueOfQuoteForSorting.gt(overallValueOfBestQuoteForSorting)
) { ) {
topAggId = aggregator topAggId = aggregator
ethTradeValueOfBestQuote = ethValueOfTrade overallValueOfBestQuoteForSorting = overallValueOfQuoteForSorting
ethFeeForBestQuote = ethFee
} }
}) })
const isBest = const isBest =
quotes[topAggId].destinationToken === ETH_SWAPS_TOKEN_ADDRESS || newQuotes[topAggId].destinationToken === ETH_SWAPS_TOKEN_ADDRESS ||
Boolean(tokenConversionRates[quotes[topAggId]?.destinationToken]) Boolean(tokenConversionRates[newQuotes[topAggId]?.destinationToken])
let savings = null let savings = null
if (isBest) { if (isBest) {
const bestQuote = newQuotes[topAggId]
savings = {} savings = {}
const {
ethFee: medianEthFee,
metaMaskFeeInEth: medianMetaMaskFee,
ethValueOfTokens: medianEthValueOfTokens,
} = getMedianEthValueQuote(Object.values(newQuotes))
// Performance savings are calculated as: // Performance savings are calculated as:
// valueForBestTrade - medianValueOfAllTrades // (ethValueOfTokens for the best trade) - (ethValueOfTokens for the media trade)
savings.performance = ethTradeValueOfBestQuote.minus( savings.performance = new BigNumber(bestQuote.ethValueOfTokens, 10).minus(
getMedian(allEthTradeValues), medianEthValueOfTokens,
10, 10,
) )
// Performance savings are calculated as: // Fee savings are calculated as:
// medianFeeOfAllTrades - feeForBestTrade // (fee for the median trade) - (fee for the best trade)
savings.fee = getMedian(allEthFees).minus( savings.fee = new BigNumber(medianEthFee).minus(bestQuote.ethFee, 10)
ethFeeForBestQuote,
10,
)
// Total savings are the sum of performance and fee savings savings.metaMaskFee = bestQuote.metaMaskFeeInEth
savings.total = savings.performance.plus(savings.fee, 10).toString(10)
// 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.performance = savings.performance.toString(10)
savings.fee = savings.fee.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( const contract = new ethers.Contract(
contractAddress, abi, this.ethersProvider, contractAddress,
abi,
this.ethersProvider,
) )
return await contract.allowance(walletAddress, METASWAP_ADDRESS) 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 * If the browser goes offline, the interval is cleared and swaps are disabled
* until the value can be fetched again. * until the value can be fetched again.
*/ */
_setupSwapsLivenessFetching () { _setupSwapsLivenessFetching() {
const TEN_MINUTES_MS = 10 * 60 * 1000 const TEN_MINUTES_MS = 10 * 60 * 1000
let intervalId = null let intervalId = null
@ -577,7 +648,10 @@ export default class SwapsController {
if (window.navigator.onLine && intervalId === null) { if (window.navigator.onLine && intervalId === null) {
// Set the interval first to prevent race condition between listener and // Set the interval first to prevent race condition between listener and
// initial call to this function. // 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() this._fetchAndSetSwapsLiveness()
} }
} }
@ -608,7 +682,7 @@ export default class SwapsController {
* Only updates state if the fetched/computed flag value differs from current * Only updates state if the fetched/computed flag value differs from current
* state. * state.
*/ */
async _fetchAndSetSwapsLiveness () { async _fetchAndSetSwapsLiveness() {
const { swapsState } = this.store.getState() const { swapsState } = this.store.getState()
const { swapsFeatureIsLive: oldSwapsFeatureIsLive } = swapsState const { swapsFeatureIsLive: oldSwapsFeatureIsLive } = swapsState
let swapsFeatureIsLive = false let swapsFeatureIsLive = false
@ -637,7 +711,9 @@ export default class SwapsController {
} }
if (!successfullyFetched) { 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) { 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 * @param {Array} quotes - A sample of quote objects with overallValueOfQuote, ethFee, metaMaskFeeInEth, and ethValueOfTokens properties
* values. The array will be sorted in place. * @returns {Object} An object with the ethValueOfTokens, ethFee, and metaMaskFeeInEth of the quote with the median overallValueOfQuote
* @returns {import('bignumber.js').BigNumber} The median of the sample.
*/ */
function getMedian (values) { function getMedianEthValueQuote(_quotes) {
if (!Array.isArray(values) || values.length === 0) { if (!Array.isArray(_quotes) || _quotes.length === 0) {
throw new Error('Expected non-empty array param.') throw new Error('Expected non-empty array param.')
} }
values.sort((a, b) => { const quotes = [..._quotes]
if (a.equals(b)) {
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 0
} }
return a.lessThan(b) ? -1 : 1 return overallValueOfQuoteA.lessThan(overallValueOfQuoteB) ? -1 : 1
}) })
if (values.length % 2 === 1) { if (quotes.length % 2 === 1) {
// return middle value // return middle values
return values[(values.length - 1) / 2] 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 // return mean of middle two values
const upperIndex = values.length / 2 const upperIndex = quotes.length / 2
return values[upperIndex] const lowerIndex = upperIndex - 1
.plus(values[upperIndex - 1])
.dividedBy(2) 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 = { export const utils = {
getMedian, getMedianEthValueQuote,
meansOfQuotesFeesAndValue,
} }

View File

@ -18,7 +18,7 @@ import createMetamaskMiddleware from './network/createMetamaskMiddleware'
const SYNC_TIMEOUT = 60 * 1000 // one minute const SYNC_TIMEOUT = 60 * 1000 // one minute
export default class ThreeBoxController { export default class ThreeBoxController {
constructor (opts = {}) { constructor(opts = {}) {
const { const {
preferencesController, preferencesController,
keyringController, keyringController,
@ -41,16 +41,22 @@ export default class ThreeBoxController {
const accounts = await this.keyringController.getAccounts() const accounts = await this.keyringController.getAccounts()
if (isUnlocked && accounts[0]) { 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 [appKeyAddress]
} }
return [] return []
}, },
processPersonalMessage: async (msgParams) => { processPersonalMessage: async (msgParams) => {
const accounts = await this.keyringController.getAccounts() const accounts = await this.keyringController.getAccounts()
return keyringController.signPersonalMessage({ ...msgParams, from: accounts[0] }, { return keyringController.signPersonalMessage(
withAppKeyOrigin: 'wallet://3box.metamask.io', { ...msgParams, from: accounts[0] },
}) {
withAppKeyOrigin: 'wallet://3box.metamask.io',
},
)
}, },
}) })
@ -65,14 +71,16 @@ export default class ThreeBoxController {
} }
this.store = new ObservableStore(initState) this.store = new ObservableStore(initState)
this.registeringUpdates = false 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) { if (initState.threeBoxSyncingAllowed) {
this.init() this.init()
} }
} }
async init () { async init() {
const accounts = await this.keyringController.getAccounts() const accounts = await this.keyringController.getAccounts()
this.address = accounts[0] this.address = accounts[0]
if (this.address && !(this.box && this.store.getState().threeBoxSynced)) { if (this.address && !(this.box && this.store.getState().threeBoxSynced)) {
@ -80,7 +88,7 @@ export default class ThreeBoxController {
} }
} }
async _update3Box () { async _update3Box() {
try { try {
const { threeBoxSyncingAllowed, threeBoxSynced } = this.store.getState() const { threeBoxSyncingAllowed, threeBoxSynced } = this.store.getState()
if (threeBoxSyncingAllowed && threeBoxSynced) { if (threeBoxSyncingAllowed && threeBoxSynced) {
@ -99,7 +107,7 @@ export default class ThreeBoxController {
} }
} }
_createProvider (providerOpts) { _createProvider(providerOpts) {
const metamaskMiddleware = createMetamaskMiddleware(providerOpts) const metamaskMiddleware = createMetamaskMiddleware(providerOpts)
const engine = new JsonRpcEngine() const engine = new JsonRpcEngine()
engine.push(createOriginMiddleware({ origin: '3Box' })) engine.push(createOriginMiddleware({ origin: '3Box' }))
@ -108,7 +116,7 @@ export default class ThreeBoxController {
return provider return provider
} }
_waitForOnSyncDone () { _waitForOnSyncDone() {
return new Promise((resolve) => { return new Promise((resolve) => {
this.box.onSyncDone(() => { this.box.onSyncDone(() => {
log.debug('3Box box sync done') log.debug('3Box box sync done')
@ -117,9 +125,12 @@ export default class ThreeBoxController {
}) })
} }
async new3Box () { async new3Box() {
const accounts = await this.keyringController.getAccounts() const accounts = await this.keyringController.getAccounts()
this.address = await this.keyringController.getAppKeyAddress(accounts[0], 'wallet://3box.metamask.io') this.address = await this.keyringController.getAppKeyAddress(
accounts[0],
'wallet://3box.metamask.io',
)
let backupExists let backupExists
try { try {
const threeBoxConfig = await Box.getConfig(this.address) 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 res = await this.space.private.get('metamaskBackup')
const parsedRes = JSON.parse(res || '{}') const parsedRes = JSON.parse(res || '{}')
return parsedRes.lastUpdated return parsedRes.lastUpdated
} }
async migrateBackedUpState (backedUpState) { async migrateBackedUpState(backedUpState) {
const migrator = new Migrator({ migrations }) const migrator = new Migrator({ migrations })
const { preferences, addressBook } = JSON.parse(backedUpState) const { preferences, addressBook } = JSON.parse(backedUpState)
const formattedStateBackup = { const formattedStateBackup = {
PreferencesController: preferences, PreferencesController: preferences,
AddressBookController: addressBook, AddressBookController: addressBook,
} }
const initialMigrationState = migrator.generateInitialState(formattedStateBackup) const initialMigrationState = migrator.generateInitialState(
formattedStateBackup,
)
const migratedState = await migrator.migrateData(initialMigrationState) const migratedState = await migrator.migrateData(initialMigrationState)
return { return {
preferences: migratedState.data.PreferencesController, 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 backedUpState = await this.space.private.get('metamaskBackup')
const { const { preferences, addressBook } = await this.migrateBackedUpState(
preferences, backedUpState,
addressBook, )
} = await this.migrateBackedUpState(backedUpState)
this.store.updateState({ threeBoxLastUpdated: backedUpState.lastUpdated }) this.store.updateState({ threeBoxLastUpdated: backedUpState.lastUpdated })
preferences && this.preferencesController.store.updateState(preferences) preferences && this.preferencesController.store.updateState(preferences)
addressBook && this.addressBookController.update(addressBook, true) addressBook && this.addressBookController.update(addressBook, true)
this.setShowRestorePromptToFalse() this.setShowRestorePromptToFalse()
} }
turnThreeBoxSyncingOn () { turnThreeBoxSyncingOn() {
this._registerUpdates() this._registerUpdates()
} }
turnThreeBoxSyncingOff () { turnThreeBoxSyncingOff() {
this.box.logout() this.box.logout()
} }
setShowRestorePromptToFalse () { setShowRestorePromptToFalse() {
this.store.updateState({ showRestorePrompt: false }) this.store.updateState({ showRestorePrompt: false })
} }
setThreeBoxSyncingPermission (newThreeboxSyncingState) { setThreeBoxSyncingPermission(newThreeboxSyncingState) {
if (this.store.getState().threeBoxDisabled) { if (this.store.getState().threeBoxDisabled) {
return return
} }
@ -232,11 +244,11 @@ export default class ThreeBoxController {
} }
} }
getThreeBoxSyncingState () { getThreeBoxSyncingState() {
return this.store.getState().threeBoxSyncingAllowed return this.store.getState().threeBoxSyncingAllowed
} }
_registerUpdates () { _registerUpdates() {
if (!this.registeringUpdates) { if (!this.registeringUpdates) {
const updatePreferences = this._update3Box.bind(this) const updatePreferences = this._update3Box.bind(this)
this.preferencesController.store.subscribe(updatePreferences) 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 * rates based on a user's current token list
*/ */
export default class TokenRatesController { export default class TokenRatesController {
/** /**
* Creates a TokenRatesController * Creates a TokenRatesController
* *
* @param {Object} [config] - Options to configure controller * @param {Object} [config] - Options to configure controller
*/ */
constructor ({ currency, preferences } = {}) { constructor({ currency, preferences } = {}) {
this.store = new ObservableStore() this.store = new ObservableStore()
this.currency = currency this.currency = currency
this.preferences = preferences this.preferences = preferences
@ -26,21 +25,32 @@ export default class TokenRatesController {
/** /**
* Updates exchange rates for all tokens * Updates exchange rates for all tokens
*/ */
async updateExchangeRates () { async updateExchangeRates() {
const contractExchangeRates = {} 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 pairs = this._tokens.map((token) => token.address).join(',')
const query = `contract_addresses=${pairs}&vs_currencies=${nativeCurrency}` const query = `contract_addresses=${pairs}&vs_currencies=${nativeCurrency}`
if (this._tokens.length > 0) { if (this._tokens.length > 0) {
try { 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() const prices = await response.json()
this._tokens.forEach((token) => { this._tokens.forEach((token) => {
const price = prices[token.address.toLowerCase()] || prices[ethUtil.toChecksumAddress(token.address)] const price =
contractExchangeRates[normalizeAddress(token.address)] = price ? price[nativeCurrency] : 0 prices[token.address.toLowerCase()] ||
prices[ethUtil.toChecksumAddress(token.address)]
contractExchangeRates[normalizeAddress(token.address)] = price
? price[nativeCurrency]
: 0
}) })
} catch (error) { } catch (error) {
log.warn(`MetaMask - TokenRatesController exchange rate fetch failed.`, error) log.warn(
`MetaMask - TokenRatesController exchange rate fetch failed.`,
error,
)
} }
} }
this.store.putState({ contractExchangeRates }) this.store.putState({ contractExchangeRates })
@ -50,7 +60,7 @@ export default class TokenRatesController {
/** /**
* @type {Object} * @type {Object}
*/ */
set preferences (preferences) { set preferences(preferences) {
this._preferences && this._preferences.unsubscribe() this._preferences && this._preferences.unsubscribe()
if (!preferences) { if (!preferences) {
return return
@ -65,13 +75,13 @@ export default class TokenRatesController {
/** /**
* @type {Array} * @type {Array}
*/ */
set tokens (tokens) { set tokens(tokens) {
this._tokens = tokens this._tokens = tokens
this.updateExchangeRates() this.updateExchangeRates()
} }
/* eslint-enable accessor-pairs */ /* eslint-enable accessor-pairs */
start (interval = DEFAULT_INTERVAL) { start(interval = DEFAULT_INTERVAL) {
this._handle && clearInterval(this._handle) this._handle && clearInterval(this._handle)
if (!interval) { if (!interval) {
return return
@ -82,7 +92,7 @@ export default class TokenRatesController {
this.updateExchangeRates() this.updateExchangeRates()
} }
stop () { stop() {
this._handle && clearInterval(this._handle) 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 NonceTracker from 'nonce-tracker'
import log from 'loglevel' import log from 'loglevel'
import BigNumber from 'bignumber.js' 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 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 { TRANSACTION_NO_CONTRACT_ERROR_KEY } from '../../../../ui/app/helpers/constants/error-keys'
import { getSwapsTokensReceivedFromTxMeta } from '../../../../ui/app/pages/swaps/swaps.util' 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 TransactionStateManager from './tx-state-manager'
import TxGasUtil from './tx-gas-utils' import TxGasUtil from './tx-gas-utils'
import PendingTransactionTracker from './pending-tx-tracker' import PendingTransactionTracker from './pending-tx-tracker'
import * as txUtils from './lib/util' 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) 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 calculating nonces
@class @class
@param {Object} - opts @param {Object} opts
@param {Object} opts.initState - initial transaction list default is an empty array @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.networkStore - an observable store for network number
@param {Object} opts.blockTracker - An instance of eth-blocktracker @param {Object} opts.blockTracker - An instance of eth-blocktracker
@param {Object} opts.provider - A network provider. @param {Object} opts.provider - A network provider.
@param {Function} opts.signTransaction - function the signs an ethereumjs-tx @param {Function} opts.signTransaction - function the signs an ethereumjs-tx
@param {Object} opts.getPermittedAccounts - get accounts that an origin has permissions for @param {Object} opts.getPermittedAccounts - get accounts that an origin has permissions for
@param {Function} opts.signTransaction - ethTx signer that returns a rawTx @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 {number} [opts.txHistoryLimit] - number *optional* for limiting how many transactions are in state
@param {Object} opts.preferencesStore @param {Object} opts.preferencesStore
*/ */
export default class TransactionController extends EventEmitter { export default class TransactionController extends EventEmitter {
constructor (opts) { constructor(opts) {
super() super()
this.networkStore = opts.networkStore || new ObservableStore({}) this.networkStore = opts.networkStore || new ObservableStore({})
this._getCurrentChainId = opts.getCurrentChainId this._getCurrentChainId = opts.getCurrentChainId
@ -95,8 +90,12 @@ export default class TransactionController extends EventEmitter {
this.nonceTracker = new NonceTracker({ this.nonceTracker = new NonceTracker({
provider: this.provider, provider: this.provider,
blockTracker: this.blockTracker, blockTracker: this.blockTracker,
getPendingTransactions: this.txStateManager.getPendingTransactions.bind(this.txStateManager), getPendingTransactions: this.txStateManager.getPendingTransactions.bind(
getConfirmedTransactions: this.txStateManager.getConfirmedTransactions.bind(this.txStateManager), this.txStateManager,
),
getConfirmedTransactions: this.txStateManager.getConfirmedTransactions.bind(
this.txStateManager,
),
}) })
this.pendingTxTracker = new PendingTransactionTracker({ this.pendingTxTracker = new PendingTransactionTracker({
@ -109,7 +108,9 @@ export default class TransactionController extends EventEmitter {
return [...pending, ...approved] return [...pending, ...approved]
}, },
approveTransaction: this.approveTransaction.bind(this), 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')) this.txStateManager.store.subscribe(() => this.emit('update:badge'))
@ -132,7 +133,7 @@ export default class TransactionController extends EventEmitter {
* *
* @returns {number} The numerical chainId. * @returns {number} The numerical chainId.
*/ */
getChainId () { getChainId() {
const networkState = this.networkStore.getState() const networkState = this.networkStore.getState()
const chainId = this._getCurrentChainId() const chainId = this._getCurrentChainId()
const integerChainId = parseInt(chainId, 16) const integerChainId = parseInt(chainId, 16)
@ -146,7 +147,7 @@ export default class TransactionController extends EventEmitter {
Adds a tx to the txlist Adds a tx to the txlist
@emits ${txMeta.id}:unapproved @emits ${txMeta.id}:unapproved
*/ */
addTx (txMeta) { addTx(txMeta) {
this.txStateManager.addTx(txMeta) this.txStateManager.addTx(txMeta)
this.emit(`${txMeta.id}:unapproved`, txMeta) this.emit(`${txMeta.id}:unapproved`, txMeta)
} }
@ -155,37 +156,62 @@ export default class TransactionController extends EventEmitter {
Wipes the transactions for a given account Wipes the transactions for a given account
@param {string} address - hex string of the from address for txs being removed @param {string} address - hex string of the from address for txs being removed
*/ */
wipeTransactions (address) { wipeTransactions(address) {
this.txStateManager.wipeTransactions(address) this.txStateManager.wipeTransactions(address)
} }
/** /**
* Add a new unapproved transaction to the pipeline * Add a new unapproved transaction to the pipeline
* *
* @returns {Promise<string>} - the hash of the transaction after being submitted to the network * @returns {Promise<string>} the hash of the transaction after being submitted to the network
* @param {Object} txParams - txParams for the transaction * @param {Object} txParams - txParams for the transaction
* @param {Object} opts - with the key origin to put the origin on the txMeta * @param {Object} opts - with the key origin to put the origin on the txMeta
*/ */
async newUnapprovedTransaction (txParams, opts = {}) { async newUnapprovedTransaction(txParams, opts = {}) {
log.debug(
`MetaMaskController newUnapprovedTransaction ${JSON.stringify(txParams)}`,
)
log.debug(`MetaMaskController newUnapprovedTransaction ${JSON.stringify(txParams)}`) const initialTxMeta = await this.addUnapprovedTransaction(
txParams,
const initialTxMeta = await this.addUnapprovedTransaction(txParams, opts.origin) opts.origin,
)
// listen for tx completion (success, fail) // listen for tx completion (success, fail)
return new Promise((resolve, reject) => { return new Promise((resolve, reject) => {
this.txStateManager.once(`${initialTxMeta.id}:finished`, (finishedTxMeta) => { this.txStateManager.once(
switch (finishedTxMeta.status) { `${initialTxMeta.id}:finished`,
case 'submitted': (finishedTxMeta) => {
return resolve(finishedTxMeta.hash) switch (finishedTxMeta.status) {
case 'rejected': case TRANSACTION_STATUSES.SUBMITTED:
return reject(cleanErrorStack(ethErrors.provider.userRejectedRequest('MetaMask Tx Signature: User denied transaction signature.'))) return resolve(finishedTxMeta.hash)
case 'failed': case TRANSACTION_STATUSES.REJECTED:
return reject(cleanErrorStack(ethErrors.rpc.internal(finishedTxMeta.err.message))) return reject(
default: cleanErrorStack(
return reject(cleanErrorStack(ethErrors.rpc.internal(`MetaMask Tx Signature: Unknown problem: ${JSON.stringify(finishedTxMeta.txParams)}`))) 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} * @returns {txMeta}
*/ */
async addUnapprovedTransaction (txParams, origin) { async addUnapprovedTransaction(txParams, origin) {
// validate // validate
const normalizedTxParams = txUtils.normalizeTxParams(txParams) const normalizedTxParams = txUtils.normalizeTxParams(txParams)
@ -210,7 +235,7 @@ export default class TransactionController extends EventEmitter {
*/ */
let txMeta = this.txStateManager.generateTxMeta({ let txMeta = this.txStateManager.generateTxMeta({
txParams: normalizedTxParams, txParams: normalizedTxParams,
type: TRANSACTION_TYPE_STANDARD, type: TRANSACTION_TYPES.STANDARD,
}) })
if (origin === 'metamask') { if (origin === 'metamask') {
@ -236,12 +261,15 @@ export default class TransactionController extends EventEmitter {
txMeta.origin = origin txMeta.origin = origin
const { transactionCategory, getCodeResponse } = await this._determineTransactionCategory(txParams) const {
transactionCategory,
getCodeResponse,
} = await this._determineTransactionCategory(txParams)
txMeta.transactionCategory = transactionCategory txMeta.transactionCategory = transactionCategory
// ensure value // ensure value
txMeta.txParams.value = txMeta.txParams.value txMeta.txParams.value = txMeta.txParams.value
? ethUtil.addHexPrefix(txMeta.txParams.value) ? addHexPrefix(txMeta.txParams.value)
: '0x0' : '0x0'
this.addTx(txMeta) this.addTx(txMeta)
@ -267,11 +295,14 @@ export default class TransactionController extends EventEmitter {
/** /**
* Adds the tx gas defaults: gas && gasPrice * Adds the tx gas defaults: gas && gasPrice
* @param {Object} txMeta - the txMeta object * @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 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 // eslint-disable-next-line no-param-reassign
txMeta = this.txStateManager.getTx(txMeta.id) txMeta = this.txStateManager.getTx(txMeta.id)
@ -292,13 +323,13 @@ export default class TransactionController extends EventEmitter {
* @param {Object} txMeta - The txMeta object * @param {Object} txMeta - The txMeta object
* @returns {Promise<string|undefined>} The default gas price * @returns {Promise<string|undefined>} The default gas price
*/ */
async _getDefaultGasPrice (txMeta) { async _getDefaultGasPrice(txMeta) {
if (txMeta.txParams.gasPrice) { if (txMeta.txParams.gasPrice) {
return undefined return undefined
} }
const gasPrice = await this.query.gasPrice() 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 * @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 * @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) { if (txMeta.txParams.gas) {
return {} return {}
} else if ( } else if (
txMeta.txParams.to && 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 there's data in the params, but there's no contract code, it's not a valid transaction
if (txMeta.txParams.data) { 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 // set error key so ui can display localized error message
err.errorKey = TRANSACTION_NO_CONTRACT_ERROR_KEY err.errorKey = TRANSACTION_NO_CONTRACT_ERROR_KEY
@ -329,10 +362,17 @@ export default class TransactionController extends EventEmitter {
return { gasLimit: SIMPLE_GAS_COST } 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 // 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 } 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 * @param {string} [customGasPrice] - the hex value to use for the cancel transaction
* @returns {txMeta} * @returns {txMeta}
*/ */
async createCancelTransaction (originalTxId, customGasPrice) { async createCancelTransaction(originalTxId, customGasPrice) {
const originalTxMeta = this.txStateManager.getTx(originalTxId) const originalTxMeta = this.txStateManager.getTx(originalTxId)
const { txParams } = originalTxMeta const { txParams } = originalTxMeta
const { gasPrice: lastGasPrice, from, nonce } = txParams 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({ const newTxMeta = this.txStateManager.generateTxMeta({
txParams: { txParams: {
from, from,
@ -361,8 +403,8 @@ export default class TransactionController extends EventEmitter {
}, },
lastGasPrice, lastGasPrice,
loadingDefaults: false, loadingDefaults: false,
status: TRANSACTION_STATUS_APPROVED, status: TRANSACTION_STATUSES.APPROVED,
type: TRANSACTION_TYPE_CANCEL, type: TRANSACTION_TYPES.CANCEL,
}) })
this.addTx(newTxMeta) this.addTx(newTxMeta)
@ -380,12 +422,14 @@ export default class TransactionController extends EventEmitter {
* @param {string} [customGasLimit] - The new custom gas limt, in hex * @param {string} [customGasLimit] - The new custom gas limt, in hex
* @returns {txMeta} * @returns {txMeta}
*/ */
async createSpeedUpTransaction (originalTxId, customGasPrice, customGasLimit) { async createSpeedUpTransaction(originalTxId, customGasPrice, customGasLimit) {
const originalTxMeta = this.txStateManager.getTx(originalTxId) const originalTxMeta = this.txStateManager.getTx(originalTxId)
const { txParams } = originalTxMeta const { txParams } = originalTxMeta
const { gasPrice: lastGasPrice } = txParams 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({ const newTxMeta = this.txStateManager.generateTxMeta({
txParams: { txParams: {
@ -394,8 +438,8 @@ export default class TransactionController extends EventEmitter {
}, },
lastGasPrice, lastGasPrice,
loadingDefaults: false, loadingDefaults: false,
status: TRANSACTION_STATUS_APPROVED, status: TRANSACTION_STATUSES.APPROVED,
type: TRANSACTION_TYPE_RETRY, type: TRANSACTION_TYPES.RETRY,
}) })
if (customGasLimit) { if (customGasLimit) {
@ -411,7 +455,7 @@ export default class TransactionController extends EventEmitter {
updates the txMeta in the txStateManager updates the txMeta in the txStateManager
@param {Object} txMeta - the updated txMeta @param {Object} txMeta - the updated txMeta
*/ */
async updateTransaction (txMeta) { async updateTransaction(txMeta) {
this.txStateManager.updateTx(txMeta, 'confTx: user updated transaction') this.txStateManager.updateTx(txMeta, 'confTx: user updated transaction')
} }
@ -419,7 +463,7 @@ export default class TransactionController extends EventEmitter {
updates and approves the transaction updates and approves the transaction
@param {Object} txMeta @param {Object} txMeta
*/ */
async updateAndApproveTransaction (txMeta) { async updateAndApproveTransaction(txMeta) {
this.txStateManager.updateTx(txMeta, 'confTx: user approved transaction') this.txStateManager.updateTx(txMeta, 'confTx: user approved transaction')
await this.approveTransaction(txMeta.id) 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 if any of these steps fails the tx status will be set to failed
@param {number} txId - the tx's Id @param {number} txId - the tx's Id
*/ */
async approveTransaction (txId) { async approveTransaction(txId) {
// TODO: Move this safety out of this function. // TODO: Move this safety out of this function.
// Since this transaction is async, // Since this transaction is async,
// we need to keep track of what is currently being signed, // 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 // add nonce to txParams
// if txMeta has lastGasPrice then it is a retry at same nonce with higher // 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 // gas price transaction and their for the nonce should not be calculated
const nonce = txMeta.lastGasPrice ? txMeta.txParams.nonce : nonceLock.nextNonce const nonce = txMeta.lastGasPrice
const customOrNonce = (customNonceValue === 0) ? customNonceValue : customNonceValue || nonce ? 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 // add nonce debugging information to txMeta
txMeta.nonceDetails = nonceLock.nonceDetails txMeta.nonceDetails = nonceLock.nonceDetails
if (customNonceValue) { 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 adds the chain id and signs the transaction and set the status to signed
@param {number} txId - the tx's Id @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) const txMeta = this.txStateManager.getTx(txId)
// add network/chain id // add network/chain id
const chainId = this.getChainId() const chainId = this.getChainId()
@ -510,7 +557,10 @@ export default class TransactionController extends EventEmitter {
txMeta.s = ethUtil.bufferToHex(ethTx.s) txMeta.s = ethUtil.bufferToHex(ethTx.s)
txMeta.v = ethUtil.bufferToHex(ethTx.v) 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 // set state to signed
this.txStateManager.setTxStatusSigned(txMeta.id) 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 @param {string} rawTx - the hex string of the serialized signed transaction
@returns {Promise<void>} @returns {Promise<void>}
*/ */
async publishTransaction (txId, rawTx) { async publishTransaction(txId, rawTx) {
const txMeta = this.txStateManager.getTx(txId) const txMeta = this.txStateManager.getTx(txId)
txMeta.rawTx = rawTx txMeta.rawTx = rawTx
if (txMeta.transactionCategory === SWAP) { if (txMeta.transactionCategory === TRANSACTION_CATEGORIES.SWAP) {
const preTxBalance = await this.query.getBalance(txMeta.txParams.from) const preTxBalance = await this.query.getBalance(txMeta.txParams.from)
txMeta.preTxBalance = preTxBalance.toString(16) txMeta.preTxBalance = preTxBalance.toString(16)
} }
@ -537,8 +587,8 @@ export default class TransactionController extends EventEmitter {
txHash = await this.query.sendRawTransaction(rawTx) txHash = await this.query.sendRawTransaction(rawTx)
} catch (error) { } catch (error) {
if (error.message.toLowerCase().includes('known transaction')) { if (error.message.toLowerCase().includes('known transaction')) {
txHash = ethUtil.sha3(ethUtil.addHexPrefix(rawTx)).toString('hex') txHash = ethUtil.sha3(addHexPrefix(rawTx)).toString('hex')
txHash = ethUtil.addHexPrefix(txHash) txHash = addHexPrefix(txHash)
} else { } else {
throw error throw error
} }
@ -554,7 +604,7 @@ export default class TransactionController extends EventEmitter {
* @param {number} txId - The tx's ID * @param {number} txId - The tx's ID
* @returns {Promise<void>} * @returns {Promise<void>}
*/ */
async confirmTransaction (txId, txReceipt) { async confirmTransaction(txId, txReceipt) {
// get the txReceipt before marking the transaction confirmed // get the txReceipt before marking the transaction confirmed
// to ensure the receipt is gotten before the ui revives the tx // to ensure the receipt is gotten before the ui revives the tx
const txMeta = this.txStateManager.getTx(txId) const txMeta = this.txStateManager.getTx(txId)
@ -566,9 +616,10 @@ export default class TransactionController extends EventEmitter {
try { try {
// It seems that sometimes the numerical values being returned from // It seems that sometimes the numerical values being returned from
// this.query.getTransactionReceipt are BN instances and not strings. // this.query.getTransactionReceipt are BN instances and not strings.
const gasUsed = typeof txReceipt.gasUsed === 'string' const gasUsed =
? txReceipt.gasUsed typeof txReceipt.gasUsed === 'string'
: txReceipt.gasUsed.toString(16) ? txReceipt.gasUsed
: txReceipt.gasUsed.toString(16)
txMeta.txReceipt = { txMeta.txReceipt = {
...txReceipt, ...txReceipt,
@ -577,9 +628,12 @@ export default class TransactionController extends EventEmitter {
this.txStateManager.setTxStatusConfirmed(txId) this.txStateManager.setTxStatusConfirmed(txId)
this._markNonceDuplicatesDropped(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 postTxBalance = await this.query.getBalance(txMeta.txParams.from)
const latestTxMeta = this.txStateManager.getTx(txId) const latestTxMeta = this.txStateManager.getTx(txId)
@ -589,11 +643,13 @@ export default class TransactionController extends EventEmitter {
latestTxMeta.postTxBalance = postTxBalance.toString(16) 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) this._trackSwapsMetrics(latestTxMeta, approvalTxMeta)
} }
} catch (err) { } catch (err) {
log.error(err) log.error(err)
} }
@ -604,7 +660,7 @@ export default class TransactionController extends EventEmitter {
@param {number} txId - the tx's Id @param {number} txId - the tx's Id
@returns {Promise<void>} @returns {Promise<void>}
*/ */
async cancelTransaction (txId) { async cancelTransaction(txId) {
this.txStateManager.setTxStatusRejected(txId) this.txStateManager.setTxStatusRejected(txId)
} }
@ -613,7 +669,7 @@ export default class TransactionController extends EventEmitter {
@param {number} txId - the tx's Id @param {number} txId - the tx's Id
@param {string} txHash - the hash for the txMeta @param {string} txHash - the hash for the txMeta
*/ */
setTxHash (txId, txHash) { setTxHash(txId, txHash) {
// Add the tx hash to the persisted meta-tx object // Add the tx hash to the persisted meta-tx object
const txMeta = this.txStateManager.getTx(txId) const txMeta = this.txStateManager.getTx(txId)
txMeta.hash = txHash txMeta.hash = txHash
@ -624,32 +680,35 @@ export default class TransactionController extends EventEmitter {
// PRIVATE METHODS // PRIVATE METHODS
// //
/** maps methods for convenience*/ /** maps methods for convenience*/
_mapMethods () { _mapMethods() {
/** @returns {Object} the state in transaction controller */
/** @returns {Object} - the state in transaction controller */
this.getState = () => this.memStore.getState() 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() this.getNetwork = () => this.networkStore.getState()
/** @returns {string} - the user selected address */ /** @returns {string} the user selected address */
this.getSelectedAddress = () => this.preferencesStore.getState().selectedAddress this.getSelectedAddress = () =>
this.preferencesStore.getState().selectedAddress
/** @returns {array} - transactions whos status is unapproved */ /** @returns {Array} transactions whos status is unapproved */
this.getUnapprovedTxCount = () => Object.keys(this.txStateManager.getUnapprovedTxList()).length 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 @param {string} account - hex prefixed account
*/ */
this.getPendingTxCount = (account) => this.txStateManager.getPendingTransactions(account).length this.getPendingTxCount = (account) =>
this.txStateManager.getPendingTransactions(account).length
/** see txStateManager */ /** see txStateManager */
this.getFilteredTxList = (opts) => this.txStateManager.getFilteredTxList(opts) this.getFilteredTxList = (opts) =>
this.txStateManager.getFilteredTxList(opts)
} }
// called once on startup // called once on startup
async _updatePendingTxsAfterFirstBlock () { async _updatePendingTxsAfterFirstBlock() {
// wait for first block so we know we're ready // wait for first block so we know we're ready
await this.blockTracker.getLatestBlock() await this.blockTracker.getLatestBlock()
// get status update for all pending transactions (for the current network) // 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. transition txMetas to a failed state or try to redo those tasks.
*/ */
_onBootCleanUp () { _onBootCleanUp() {
this.txStateManager.getFilteredTxList({ this.txStateManager
status: 'unapproved', .getFilteredTxList({
loadingDefaults: true, status: TRANSACTION_STATUSES.UNAPPROVED,
}).forEach((tx) => { 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) this.txStateManager
.then((txMeta) => { .getFilteredTxList({
txMeta.loadingDefaults = false status: TRANSACTION_STATUSES.APPROVED,
this.txStateManager.updateTx(txMeta, 'transactions: gas estimation for tx on boot') })
}).catch((error) => { .forEach((txMeta) => {
const txMeta = this.txStateManager.getTx(tx.id) const txSignError = new Error(
txMeta.loadingDefaults = false 'Transaction found as "approved" during boot - possibly stuck during signing',
this.txStateManager.updateTx(txMeta, 'failed to estimate gas during boot cleanup.') )
this.txStateManager.setTxStatusFailed(txMeta.id, error) this.txStateManager.setTxStatusFailed(txMeta.id, txSignError)
}) })
})
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)
})
} }
/** /**
is called in constructor applies the listeners for pendingTxTracker txStateManager is called in constructor applies the listeners for pendingTxTracker txStateManager
and blockTracker and blockTracker
*/ */
_setupListeners () { _setupListeners() {
this.txStateManager.on('tx:status-update', this.emit.bind(this, 'tx:status-update')) this.txStateManager.on(
'tx:status-update',
this.emit.bind(this, 'tx:status-update'),
)
this._setupBlockTrackerListener() this._setupBlockTrackerListener()
this.pendingTxTracker.on('tx:warning', (txMeta) => { 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(
this.pendingTxTracker.on('tx:confirmed', (txId, transactionReceipt) => this.confirmTransaction(txId, transactionReceipt)) 'tx:failed',
this.pendingTxTracker.on('tx:dropped', this.txStateManager.setTxStatusDropped.bind(this.txStateManager)) 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) => { this.pendingTxTracker.on('tx:block-update', (txMeta, latestBlockNumber) => {
if (!txMeta.firstRetryBlockNumber) { if (!txMeta.firstRetryBlockNumber) {
txMeta.firstRetryBlockNumber = latestBlockNumber 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) => { this.pendingTxTracker.on('tx:retry', (txMeta) => {
@ -712,7 +800,10 @@ export default class TransactionController extends EventEmitter {
txMeta.retryCount = 0 txMeta.retryCount = 0
} }
txMeta.retryCount += 1 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, Returns a "type" for a transaction out of the following list: simpleSend, tokenTransfer, tokenApprove,
contractDeployment, contractMethodCall contractDeployment, contractMethodCall
*/ */
async _determineTransactionCategory (txParams) { async _determineTransactionCategory(txParams) {
const { data, to } = txParams const { data, to } = txParams
let name let name
try { try {
@ -730,16 +821,16 @@ export default class TransactionController extends EventEmitter {
} }
const tokenMethodName = [ const tokenMethodName = [
TOKEN_METHOD_APPROVE, TRANSACTION_CATEGORIES.TOKEN_METHOD_APPROVE,
TOKEN_METHOD_TRANSFER, TRANSACTION_CATEGORIES.TOKEN_METHOD_TRANSFER,
TOKEN_METHOD_TRANSFER_FROM, TRANSACTION_CATEGORIES.TOKEN_METHOD_TRANSFER_FROM,
].find((methodName) => methodName === name && name.toLowerCase()) ].find((methodName) => methodName === name && name.toLowerCase())
let result let result
if (txParams.data && tokenMethodName) { if (txParams.data && tokenMethodName) {
result = tokenMethodName result = tokenMethodName
} else if (txParams.data && !to) { } else if (txParams.data && !to) {
result = DEPLOY_CONTRACT_ACTION_KEY result = TRANSACTION_CATEGORIES.DEPLOY_CONTRACT
} }
let code let code
@ -753,7 +844,9 @@ export default class TransactionController extends EventEmitter {
const codeIsEmpty = !code || code === '0x' || code === '0x0' 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 } 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 @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 // get the confirmed transactions nonce and from address
const txMeta = this.txStateManager.getTx(txId) const txMeta = this.txStateManager.getTx(txId)
const { nonce, from } = txMeta.txParams const { nonce, from } = txMeta.txParams
@ -779,12 +872,15 @@ export default class TransactionController extends EventEmitter {
return return
} }
otherTxMeta.replacedBy = txMeta.hash 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) this.txStateManager.setTxStatusDropped(otherTxMeta.id)
}) })
} }
_setupBlockTrackerListener () { _setupBlockTrackerListener() {
let listenersAreActive = false let listenersAreActive = false
const latestBlockHandler = this._onLatestBlock.bind(this) const latestBlockHandler = this._onLatestBlock.bind(this)
const { blockTracker, txStateManager } = this const { blockTracker, txStateManager } = this
@ -792,7 +888,7 @@ export default class TransactionController extends EventEmitter {
txStateManager.on('tx:status-update', updateSubscription) txStateManager.on('tx:status-update', updateSubscription)
updateSubscription() updateSubscription()
function updateSubscription () { function updateSubscription() {
const pendingTxs = txStateManager.getPendingTransactions() const pendingTxs = txStateManager.getPendingTransactions()
if (!listenersAreActive && pendingTxs.length > 0) { if (!listenersAreActive && pendingTxs.length > 0) {
blockTracker.on('latest', latestBlockHandler) blockTracker.on('latest', latestBlockHandler)
@ -804,7 +900,7 @@ export default class TransactionController extends EventEmitter {
} }
} }
async _onLatestBlock (blockNumber) { async _onLatestBlock(blockNumber) {
try { try {
await this.pendingTxTracker.updatePendingTxs() await this.pendingTxTracker.updatePendingTxs()
} catch (err) { } catch (err) {
@ -820,13 +916,15 @@ export default class TransactionController extends EventEmitter {
/** /**
Updates the memStore in transaction controller Updates the memStore in transaction controller
*/ */
_updateMemstore () { _updateMemstore() {
const unapprovedTxs = this.txStateManager.getUnapprovedTxList() 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 }) this.memStore.updateState({ unapprovedTxs, currentNetworkTxList })
} }
_trackSwapsMetrics (txMeta, approvalTxMeta) { _trackSwapsMetrics(txMeta, approvalTxMeta) {
if (this._getParticipateInMetrics() && txMeta.swapMetaData) { if (this._getParticipateInMetrics() && txMeta.swapMetaData) {
if (txMeta.txReceipt.status === '0x0') { if (txMeta.txReceipt.status === '0x0') {
this._trackMetaMetricsEvent({ this._trackMetaMetricsEvent({
@ -851,19 +949,18 @@ export default class TransactionController extends EventEmitter {
approvalTxMeta, approvalTxMeta,
) )
const quoteVsExecutionRatio = `${ const quoteVsExecutionRatio = `${new BigNumber(tokensReceived, 10)
(new BigNumber(tokensReceived, 10)) .div(txMeta.swapMetaData.token_to_amount, 10)
.div(txMeta.swapMetaData.token_to_amount, 10) .times(100)
.times(100) .round(2)}%`
.round(2)
}%`
const estimatedVsUsedGasRatio = `${ const estimatedVsUsedGasRatio = `${new BigNumber(
(new BigNumber(txMeta.txReceipt.gasUsed, 16)) txMeta.txReceipt.gasUsed,
.div(txMeta.swapMetaData.estimated_gas, 10) 16,
.times(100) )
.round(2) .div(txMeta.swapMetaData.estimated_gas, 10)
}%` .times(100)
.round(2)}%`
this._trackMetaMetricsEvent({ this._trackMetaMetricsEvent({
event: 'Swap Completed', event: 'Swap Completed',

View File

@ -3,13 +3,13 @@ import { cloneDeep } from 'lodash'
/** /**
converts non-initial history entries into diffs converts non-initial history entries into diffs
@param {array} longHistory @param {Array} longHistory
@returns {array} @returns {Array}
*/ */
export function migrateFromSnapshotsToDiffs (longHistory) { export function migrateFromSnapshotsToDiffs(longHistory) {
return ( return (
longHistory longHistory
// convert non-initial history entries into diffs // convert non-initial history entries into diffs
.map((entry, index) => { .map((entry, index) => {
if (index === 0) { if (index === 0) {
return entry return entry
@ -29,9 +29,9 @@ export function migrateFromSnapshotsToDiffs (longHistory) {
@param {Object} previousState - the previous state of the object @param {Object} previousState - the previous state of the object
@param {Object} newState - the update object @param {Object} newState - the update object
@param {string} [note] - a optional note for the state change @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) const entry = jsonDiffer.compare(previousState, newState)
// Add a note to the first op, since it breaks if we append it to the entry // Add a note to the first op, since it breaks if we append it to the entry
if (entry[0]) { if (entry[0]) {
@ -48,9 +48,11 @@ export function generateHistoryEntry (previousState, newState, note) {
Recovers previous txMeta state obj Recovers previous txMeta state obj
@returns {Object} @returns {Object}
*/ */
export function replayHistory (_shortHistory) { export function replayHistory(_shortHistory) {
const shortHistory = cloneDeep(_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 * @param {Object} txMeta - the tx metadata object
* @returns {Object} a deep clone without history * @returns {Object} a deep clone without history
*/ */
export function snapshotFromTxMeta (txMeta) { export function snapshotFromTxMeta(txMeta) {
const shallow = { ...txMeta } const shallow = { ...txMeta }
delete shallow.history delete shallow.history
return cloneDeep(shallow) 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 = { const normalizers = {
from: (from) => addHexPrefix(from), 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), nonce: (nonce) => addHexPrefix(nonce),
value: (value) => addHexPrefix(value), value: (value) => addHexPrefix(value),
data: (data) => addHexPrefix(data), data: (data) => addHexPrefix(data),
@ -17,7 +20,7 @@ const normalizers = {
* Default: true * Default: true
* @returns {Object} the normalized tx params * @returns {Object} the normalized tx params
*/ */
export function normalizeTxParams (txParams, lowerCase = true) { export function normalizeTxParams(txParams, lowerCase = true) {
// apply only keys in the normalizers // apply only keys in the normalizers
const normalizedTxParams = {} const normalizedTxParams = {}
for (const key in normalizers) { for (const key in normalizers) {
@ -33,17 +36,21 @@ export function normalizeTxParams (txParams, lowerCase = true) {
* @param {Object} txParams - the tx params * @param {Object} txParams - the tx params
* @throws {Error} if the tx params contains invalid fields * @throws {Error} if the tx params contains invalid fields
*/ */
export function validateTxParams (txParams) { export function validateTxParams(txParams) {
validateFrom(txParams) validateFrom(txParams)
validateRecipient(txParams) validateRecipient(txParams)
if ('value' in txParams) { if ('value' in txParams) {
const value = txParams.value.toString() const value = txParams.value.toString()
if (value.includes('-')) { 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('.')) { 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 * @param {Object} txParams
* @throws {Error} if the from address isn't valid * @throws {Error} if the from address isn't valid
*/ */
export function validateFrom (txParams) { export function validateFrom(txParams) {
if (!(typeof txParams.from === 'string')) { if (!(typeof txParams.from === 'string')) {
throw new Error(`Invalid from address ${txParams.from} not a 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 * @returns {Object} the tx params
* @throws {Error} if the recipient is invalid OR there isn't tx data * @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.to === '0x' || txParams.to === null) {
if (txParams.data) { if (txParams.data) {
delete txParams.to delete txParams.to
@ -85,11 +92,11 @@ export function validateRecipient (txParams) {
* Returns a list of final states * Returns a list of final states
* @returns {string[]} the states that can be considered final states * @returns {string[]} the states that can be considered final states
*/ */
export function getFinalStates () { export function getFinalStates() {
return [ return [
'rejected', // the user has responded no! TRANSACTION_STATUSES.REJECTED, // the user has responded no!
'confirmed', // the tx has been included in a block. TRANSACTION_STATUSES.CONFIRMED, // the tx has been included in a block.
'failed', // the tx failed for some reason, included on tx data. TRANSACTION_STATUSES.FAILED, // the tx failed for some reason, included on tx data.
'dropped', // the tx nonce was already used TRANSACTION_STATUSES.DROPPED, // the tx nonce was already used
] ]
} }

View File

@ -1,6 +1,7 @@
import EventEmitter from 'safe-event-emitter' import EventEmitter from 'safe-event-emitter'
import log from 'loglevel' import log from 'loglevel'
import EthQuery from 'ethjs-query' import EthQuery from 'ethjs-query'
import { TRANSACTION_STATUSES } from '../../../../shared/constants/transaction'
/** /**
@ -11,15 +12,14 @@ import EthQuery from 'ethjs-query'
<br> <br>
@param {Object} config - non optional configuration object consists of: @param {Object} config - non optional configuration object consists of:
@param {Object} config.provider - A network provider. @param {Object} config.provider - A network provider.
@param {Object} config.nonceTracker see nonce tracker @param {Object} config.nonceTracker - see nonce tracker
@param {function} config.getPendingTransactions a function for getting an array of transactions, @param {Function} config.getPendingTransactions - a function for getting an array of transactions,
@param {function} config.publishTransaction a async function for publishing raw transactions, @param {Function} config.publishTransaction - a async function for publishing raw transactions,
@class @class
*/ */
export default class PendingTransactionTracker extends EventEmitter { export default class PendingTransactionTracker extends EventEmitter {
/** /**
* We wait this many blocks before emitting a 'tx:dropped' event * We wait this many blocks before emitting a 'tx:dropped' event
* *
@ -37,9 +37,9 @@ export default class PendingTransactionTracker extends EventEmitter {
*/ */
droppedBlocksBufferByHash = new Map() droppedBlocksBufferByHash = new Map()
constructor (config) { constructor(config) {
super() super()
this.query = config.query || (new EthQuery(config.provider)) this.query = config.query || new EthQuery(config.provider)
this.nonceTracker = config.nonceTracker this.nonceTracker = config.nonceTracker
this.getPendingTransactions = config.getPendingTransactions this.getPendingTransactions = config.getPendingTransactions
this.getCompletedTransactions = config.getCompletedTransactions 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 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 // in order to keep the nonceTracker accurate we block it while updating pending transactions
const nonceGlobalLock = await this.nonceTracker.getGlobalLock() const nonceGlobalLock = await this.nonceTracker.getGlobalLock()
try { try {
const pendingTxs = this.getPendingTransactions() const pendingTxs = this.getPendingTransactions()
await Promise.all(pendingTxs.map((txMeta) => this._checkPendingTx(txMeta))) await Promise.all(
pendingTxs.map((txMeta) => this._checkPendingTx(txMeta)),
)
} catch (err) { } catch (err) {
log.error('PendingTransactionTracker - Error updating pending transactions') log.error(
'PendingTransactionTracker - Error updating pending transactions',
)
log.error(err) log.error(err)
} }
nonceGlobalLock.releaseLock() nonceGlobalLock.releaseLock()
@ -70,7 +74,7 @@ export default class PendingTransactionTracker extends EventEmitter {
* @emits tx:warning * @emits tx:warning
* @returns {Promise<void>} * @returns {Promise<void>}
*/ */
async resubmitPendingTxs (blockNumber) { async resubmitPendingTxs(blockNumber) {
const pending = this.getPendingTransactions() const pending = this.getPendingTransactions()
if (!pending.length) { if (!pending.length) {
return return
@ -79,18 +83,20 @@ export default class PendingTransactionTracker extends EventEmitter {
try { try {
await this._resubmitTx(txMeta, blockNumber) await this._resubmitTx(txMeta, blockNumber)
} catch (err) { } catch (err) {
const errorMessage = err.value?.message?.toLowerCase() || err.message.toLowerCase() const errorMessage =
const isKnownTx = ( err.value?.message?.toLowerCase() || err.message.toLowerCase()
const isKnownTx =
// geth // geth
errorMessage.includes('replacement transaction underpriced') || errorMessage.includes('replacement transaction underpriced') ||
errorMessage.includes('known transaction') || errorMessage.includes('known transaction') ||
// parity // parity
errorMessage.includes('gas price too low to replace') || 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 // other
errorMessage.includes('gateway timeout') || errorMessage.includes('gateway timeout') ||
errorMessage.includes('nonce too low') errorMessage.includes('nonce too low')
)
// ignore resubmit warnings, return early // ignore resubmit warnings, return early
if (isKnownTx) { if (isKnownTx) {
return return
@ -117,13 +123,16 @@ export default class PendingTransactionTracker extends EventEmitter {
* @emits tx:retry * @emits tx:retry
* @private * @private
*/ */
async _resubmitTx (txMeta, latestBlockNumber) { async _resubmitTx(txMeta, latestBlockNumber) {
if (!txMeta.firstRetryBlockNumber) { if (!txMeta.firstRetryBlockNumber) {
this.emit('tx:block-update', txMeta, latestBlockNumber) this.emit('tx:block-update', txMeta, latestBlockNumber)
} }
const firstRetryBlockNumber = txMeta.firstRetryBlockNumber || latestBlockNumber const firstRetryBlockNumber =
const txBlockDistance = Number.parseInt(latestBlockNumber, 16) - Number.parseInt(firstRetryBlockNumber, 16) txMeta.firstRetryBlockNumber || latestBlockNumber
const txBlockDistance =
Number.parseInt(latestBlockNumber, 16) -
Number.parseInt(firstRetryBlockNumber, 16)
const retryCount = txMeta.retryCount || 0 const retryCount = txMeta.retryCount || 0
@ -155,19 +164,21 @@ export default class PendingTransactionTracker extends EventEmitter {
* @emits tx:warning * @emits tx:warning
* @private * @private
*/ */
async _checkPendingTx (txMeta) { async _checkPendingTx(txMeta) {
const txHash = txMeta.hash const txHash = txMeta.hash
const txId = txMeta.id const txId = txMeta.id
// Only check submitted txs // Only check submitted txs
if (txMeta.status !== 'submitted') { if (txMeta.status !== TRANSACTION_STATUSES.SUBMITTED) {
return return
} }
// extra check in case there was an uncaught error during the // extra check in case there was an uncaught error during the
// signature and submission process // signature and submission process
if (!txHash) { 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' noTxHashErr.name = 'NoTxHashError'
this.emit('tx:failed', txId, noTxHashErr) this.emit('tx:failed', txId, noTxHashErr)
@ -206,8 +217,11 @@ export default class PendingTransactionTracker extends EventEmitter {
* @returns {Promise<boolean>} * @returns {Promise<boolean>}
* @private * @private
*/ */
async _checkIfTxWasDropped (txMeta) { async _checkIfTxWasDropped(txMeta) {
const { hash: txHash, txParams: { nonce, from } } = txMeta const {
hash: txHash,
txParams: { nonce, from },
} = txMeta
const networkNextNonce = await this.query.getTransactionCount(from) const networkNextNonce = await this.query.getTransactionCount(from)
if (parseInt(nonce, 16) >= networkNextNonce.toNumber()) { if (parseInt(nonce, 16) >= networkNextNonce.toNumber()) {
@ -235,14 +249,16 @@ export default class PendingTransactionTracker extends EventEmitter {
* @returns {Promise<boolean>} * @returns {Promise<boolean>}
* @private * @private
*/ */
async _checkIfNonceIsTaken (txMeta) { async _checkIfNonceIsTaken(txMeta) {
const address = txMeta.txParams.from const address = txMeta.txParams.from
const completed = this.getCompletedTransactions(address) const completed = this.getCompletedTransactions(address)
return completed.some( return completed.some(
// This is called while the transaction is in-flight, so it is possible that the // 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 // 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 // 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 { export default class TxGasUtil {
constructor(provider) {
constructor (provider) {
this.query = new EthQuery(provider) this.query = new EthQuery(provider)
} }
@ -29,7 +28,7 @@ export default class TxGasUtil {
@param {Object} txMeta - the txMeta object @param {Object} txMeta - the txMeta object
@returns {GasAnalysisResult} The result of the gas analysis @returns {GasAnalysisResult} The result of the gas analysis
*/ */
async analyzeGasUsage (txMeta) { async analyzeGasUsage(txMeta) {
const block = await this.query.getBlockByNumber('latest', false) const block = await this.query.getBlockByNumber('latest', false)
// fallback to block gasLimit // fallback to block gasLimit
@ -54,9 +53,9 @@ export default class TxGasUtil {
/** /**
Estimates the tx's gas usage Estimates the tx's gas usage
@param {Object} txMeta - the txMeta object @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 const { txParams } = txMeta
// estimate tx gas requirements // 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} initialGasLimitHex - the initial gas limit to add the buffer too
@param {string} blockGasLimitHex - the block gas limit @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 initialGasLimitBn = hexToBn(initialGasLimitHex)
const blockGasLimitBn = hexToBn(blockGasLimitHex) const blockGasLimitBn = hexToBn(blockGasLimitHex)
const upperGasLimitBn = blockGasLimitBn.muln(0.9) const upperGasLimitBn = blockGasLimitBn.muln(0.9)
@ -88,11 +87,19 @@ export default class TxGasUtil {
return bnToHex(upperGasLimitBn) return bnToHex(upperGasLimitBn)
} }
async getBufferedGasLimit (txMeta, multiplier) { async getBufferedGasLimit(txMeta, multiplier) {
const { blockGasLimit, estimatedGasHex, simulationFails } = await this.analyzeGasUsage(txMeta) const {
blockGasLimit,
estimatedGasHex,
simulationFails,
} = await this.analyzeGasUsage(txMeta)
// add additional gas buffer to our estimation for safety // 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 } return { gasLimit, simulationFails }
} }
} }

View File

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

View File

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

View File

@ -60,13 +60,13 @@ initProvider({
// TODO:deprecate:2020 // TODO:deprecate:2020
// Setup web3 // Setup web3
if (typeof window.web3 !== 'undefined') { if (typeof window.web3 === 'undefined') {
throw new Error(`MetaMask detected another web3. // 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. MetaMask will not work reliably with another web3 extension.
This usually happens if you have two MetaMasks installed, This usually happens if you have two MetaMasks installed,
or MetaMask and another web3 extension. Please remove one or MetaMask and another web3 extension. Please remove one
and try again.`) 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 * structure of child stores based on configuration
*/ */
export default class ComposableObservableStore extends ObservableStore { export default class ComposableObservableStore extends ObservableStore {
/** /**
* Create a new store * Create a new store
* *
* @param {Object} [initState] - The initial store state * @param {Object} [initState] - The initial store state
* @param {Object} [config] - Map of internal state keys to child stores * @param {Object} [config] - Map of internal state keys to child stores
*/ */
constructor (initState, config) { constructor(initState, config) {
super(initState) super(initState)
this.updateStructure(config) this.updateStructure(config)
} }
@ -22,7 +21,7 @@ export default class ComposableObservableStore extends ObservableStore {
* *
* @param {Object} [config] - Map of internal state keys to child stores * @param {Object} [config] - Map of internal state keys to child stores
*/ */
updateStructure (config) { updateStructure(config) {
this.config = config this.config = config
this.removeAllListeners() this.removeAllListeners()
for (const key in config) { 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 * Merges all child store state into a single object rather than
* returning an object keyed by child store class name * 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 = {} let flatState = {}
for (const key in this.config) { for (const key in this.config) {
if (Object.prototype.hasOwnProperty.call(this.config, key)) { if (Object.prototype.hasOwnProperty.call(this.config, key)) {
const controller = 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 } flatState = { ...flatState, ...state }
} }
} }

View File

@ -14,7 +14,12 @@ import log from 'loglevel'
import pify from 'pify' import pify from 'pify'
import Web3 from 'web3' import Web3 from 'web3'
import SINGLE_CALL_BALANCES_ABI from 'single-call-balance-checker-abi' 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 { import {
SINGLE_CALL_BALANCES_ADDRESS, SINGLE_CALL_BALANCES_ADDRESS,
@ -42,14 +47,13 @@ import { bnToHex } from './util'
* *
*/ */
export default class AccountTracker { export default class AccountTracker {
/** /**
* @param {Object} opts - Options for initializing the controller * @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.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 {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 * @param {Function} opts.getCurrentChainId - A function that returns the `chainId` for the current global network
*/ */
constructor (opts = {}) { constructor(opts = {}) {
const initState = { const initState = {
accounts: {}, accounts: {},
currentBlockGasLimit: '', currentBlockGasLimit: '',
@ -71,7 +75,7 @@ export default class AccountTracker {
this.web3 = new Web3(this._provider) this.web3 = new Web3(this._provider)
} }
start () { start() {
// remove first to avoid double add // remove first to avoid double add
this._blockTracker.removeListener('latest', this._updateForBlock) this._blockTracker.removeListener('latest', this._updateForBlock)
// add listener // add listener
@ -80,7 +84,7 @@ export default class AccountTracker {
this._updateAccounts() this._updateAccounts()
} }
stop () { stop() {
// remove listener // remove listener
this._blockTracker.removeListener('latest', this._updateForBlock) 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 * 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. * 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 * in sync
* *
*/ */
syncWithAddresses (addresses) { syncWithAddresses(addresses) {
const { accounts } = this.store.getState() const { accounts } = this.store.getState()
const locals = Object.keys(accounts) const locals = Object.keys(accounts)
@ -122,10 +126,10 @@ export default class AccountTracker {
* Adds new addresses to track the balances of * Adds new addresses to track the balances of
* given a balance as long this._currentBlockNumber is defined. * 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() const { accounts } = this.store.getState()
// add initial state for addresses // add initial state for addresses
addresses.forEach((address) => { addresses.forEach((address) => {
@ -143,10 +147,10 @@ export default class AccountTracker {
/** /**
* Removes accounts from being tracked * 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() const { accounts } = this.store.getState()
// remove each state object // remove each state object
addresses.forEach((address) => { addresses.forEach((address) => {
@ -160,7 +164,7 @@ export default class AccountTracker {
* Removes all addresses and associated balances * Removes all addresses and associated balances
*/ */
clearAccounts () { clearAccounts() {
this.store.updateState({ accounts: {} }) this.store.updateState({ accounts: {} })
} }
@ -173,7 +177,7 @@ export default class AccountTracker {
* @fires 'block' The updated state, if all account updates are successful * @fires 'block' The updated state, if all account updates are successful
* *
*/ */
async _updateForBlock (blockNumber) { async _updateForBlock(blockNumber) {
this._currentBlockNumber = blockNumber this._currentBlockNumber = blockNumber
// block gasLimit polling shouldn't be in account-tracker shouldn't be here... // 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 * 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 * 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 { accounts } = this.store.getState()
const addresses = Object.keys(accounts) const addresses = Object.keys(accounts)
const chainId = this.getCurrentChainId() const chainId = this.getCurrentChainId()
switch (chainId) { switch (chainId) {
case MAINNET_CHAIN_ID: case MAINNET_CHAIN_ID:
await this._updateAccountsViaBalanceChecker(addresses, SINGLE_CALL_BALANCES_ADDRESS) await this._updateAccountsViaBalanceChecker(
addresses,
SINGLE_CALL_BALANCES_ADDRESS,
)
break break
case RINKEBY_CHAIN_ID: case RINKEBY_CHAIN_ID:
await this._updateAccountsViaBalanceChecker(addresses, SINGLE_CALL_BALANCES_ADDRESS_RINKEBY) await this._updateAccountsViaBalanceChecker(
addresses,
SINGLE_CALL_BALANCES_ADDRESS_RINKEBY,
)
break break
case ROPSTEN_CHAIN_ID: case ROPSTEN_CHAIN_ID:
await this._updateAccountsViaBalanceChecker(addresses, SINGLE_CALL_BALANCES_ADDRESS_ROPSTEN) await this._updateAccountsViaBalanceChecker(
addresses,
SINGLE_CALL_BALANCES_ADDRESS_ROPSTEN,
)
break break
case KOVAN_CHAIN_ID: case KOVAN_CHAIN_ID:
await this._updateAccountsViaBalanceChecker(addresses, SINGLE_CALL_BALANCES_ADDRESS_KOVAN) await this._updateAccountsViaBalanceChecker(
addresses,
SINGLE_CALL_BALANCES_ADDRESS_KOVAN,
)
break break
default: default:
@ -230,10 +246,10 @@ export default class AccountTracker {
* *
* @private * @private
* @param {string} address - A hex address of a the account to be updated * @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 // query balance
const balance = await this._query.getBalance(address) const balance = await this._query.getBalance(address)
const result = { address, balance } const result = { address, balance }
@ -252,15 +268,20 @@ export default class AccountTracker {
* @param {*} addresses * @param {*} addresses
* @param {*} deployedContractAddress * @param {*} deployedContractAddress
*/ */
async _updateAccountsViaBalanceChecker (addresses, deployedContractAddress) { async _updateAccountsViaBalanceChecker(addresses, deployedContractAddress) {
const { accounts } = this.store.getState() const { accounts } = this.store.getState()
this.web3.setProvider(this._provider) 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'] const ethBalance = ['0x0']
ethContract.balances(addresses, ethBalance, (error, result) => { ethContract.balances(addresses, ethBalance, (error, result) => {
if (error) { 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))) Promise.all(addresses.map(this._updateAccount.bind(this)))
return return
} }
@ -271,5 +292,4 @@ export default class AccountTracker {
this.store.updateState({ accounts }) 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 * 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 {Object} opts - Options required to determine the correct url
* @param {string} opts.network The network for which to return a 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'. * @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 * @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. * 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 // default service by network if not specified
if (!service) { if (!service) {
// eslint-disable-next-line no-param-reassign // 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) { switch (network) {
case '1': case '1':
return 'wyre' return 'wyre'
@ -46,6 +46,8 @@ function getDefaultServiceForNetwork (network) {
case '5': case '5':
return 'goerli-faucet' return 'goerli-faucet'
default: 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 * Returns error without stack trace for better UI display
* @param {Error} err - error * @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 let { name } = err
name = (name === undefined) ? 'Error' : String(name) name = name === undefined ? 'Error' : String(name)
let msg = err.message let msg = err.message
msg = (msg === undefined) ? '' : String(msg) msg = msg === undefined ? '' : String(msg)
if (name === '') { if (name === '') {
err.stack = err.message err.stack = err.message

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