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

* origin/develop: (82 commits)
  Add links to release headers (#10808)
  Removing unnecessary params from withFixtures function call. (#10831)
  Fix _getPermittedAccounts type safety (#10819)
  Refactoring send-edit.spec.js to use fixtures (#10792)
  Refactoring address-book.spec.js to use fixtures (#10804)
  Update the changelog when creating an RC (#10795)
  Update changelog headers and fix dates (#10805)
  Add support for locators into driver abstraction (#10802)
  remove node-sass dependency (#10797)
  Add release header when updating changelog (#10794)
  Ensure correct primary currency image is displayed on home screen and token list (#10777)
  upgrade eslint deps (#10789)
  only applies rules to the appropriate files (#10788)
  Set the BSC_CONTRACT_ADDRESS to lowercase (#10800)
  Prevent duplicate changelog entries (#10786)
  Add changelog entries under release candidate header (#10784)
  Remove script for creating master sync PR (#10791)
  Remove date from changelog release header (#10790)
  Remove useless negation (#10787)
  Refactoring ethereum-on.spec.js to use fixtures (#10778)
  ...
feature/default_network_editable
Mark Stacey 4 years ago
commit 2d49efd3d6
  1. 17
      .circleci/config.yml
  2. 3
      .circleci/scripts/release-bump-changelog-version.sh
  3. 10
      .circleci/scripts/release-bump-manifest-version.sh
  4. 34
      .circleci/scripts/release-commit-version-bump.sh
  5. 52
      .circleci/scripts/release-create-master-pr.sh
  6. 126
      .eslintrc.js
  7. 1
      .gitignore
  8. 1
      .storybook/test-data.js
  9. 813
      CHANGELOG.md
  10. 9
      app/_locales/am/messages.json
  11. 9
      app/_locales/ar/messages.json
  12. 9
      app/_locales/bg/messages.json
  13. 9
      app/_locales/bn/messages.json
  14. 9
      app/_locales/ca/messages.json
  15. 6
      app/_locales/cs/messages.json
  16. 9
      app/_locales/da/messages.json
  17. 9
      app/_locales/de/messages.json
  18. 9
      app/_locales/el/messages.json
  19. 12
      app/_locales/en/messages.json
  20. 9
      app/_locales/es/messages.json
  21. 9
      app/_locales/es_419/messages.json
  22. 9
      app/_locales/et/messages.json
  23. 9
      app/_locales/fa/messages.json
  24. 9
      app/_locales/fi/messages.json
  25. 9
      app/_locales/fil/messages.json
  26. 9
      app/_locales/fr/messages.json
  27. 9
      app/_locales/he/messages.json
  28. 9
      app/_locales/hi/messages.json
  29. 6
      app/_locales/hn/messages.json
  30. 9
      app/_locales/hr/messages.json
  31. 6
      app/_locales/ht/messages.json
  32. 9
      app/_locales/hu/messages.json
  33. 9
      app/_locales/id/messages.json
  34. 9
      app/_locales/it/messages.json
  35. 9
      app/_locales/ja/messages.json
  36. 9
      app/_locales/kn/messages.json
  37. 9
      app/_locales/ko/messages.json
  38. 9
      app/_locales/lt/messages.json
  39. 9
      app/_locales/lv/messages.json
  40. 9
      app/_locales/ms/messages.json
  41. 6
      app/_locales/nl/messages.json
  42. 6
      app/_locales/no/messages.json
  43. 9
      app/_locales/pl/messages.json
  44. 6
      app/_locales/pt/messages.json
  45. 9
      app/_locales/pt_BR/messages.json
  46. 9
      app/_locales/ro/messages.json
  47. 9
      app/_locales/ru/messages.json
  48. 9
      app/_locales/sk/messages.json
  49. 9
      app/_locales/sl/messages.json
  50. 9
      app/_locales/sr/messages.json
  51. 9
      app/_locales/sv/messages.json
  52. 9
      app/_locales/sw/messages.json
  53. 6
      app/_locales/ta/messages.json
  54. 6
      app/_locales/th/messages.json
  55. 9
      app/_locales/tl/messages.json
  56. 6
      app/_locales/tr/messages.json
  57. 9
      app/_locales/uk/messages.json
  58. 9
      app/_locales/vi/messages.json
  59. 9
      app/_locales/zh_CN/messages.json
  60. 9
      app/_locales/zh_TW/messages.json
  61. 15
      app/background.html
  62. 9
      app/manifest/_base.json
  63. 2
      app/scripts/account-import-strategies/account-import-strategies.test.js
  64. 22
      app/scripts/background.js
  65. 4
      app/scripts/controllers/cached-balances.test.js
  66. 8
      app/scripts/controllers/detect-tokens.test.js
  67. 2
      app/scripts/controllers/ens/index.test.js
  68. 293
      app/scripts/controllers/incoming-transactions.js
  69. 902
      app/scripts/controllers/incoming-transactions.test.js
  70. 12
      app/scripts/controllers/metametrics.test.js
  71. 4
      app/scripts/controllers/network/createInfuraClient.js
  72. 4
      app/scripts/controllers/network/createJsonRpcClient.js
  73. 4
      app/scripts/controllers/network/network-controller.test.js
  74. 4
      app/scripts/controllers/network/pending-middleware.test.js
  75. 8
      app/scripts/controllers/permissions/index.js
  76. 18
      app/scripts/controllers/permissions/permissions-controller.test.js
  77. 18
      app/scripts/controllers/permissions/permissions-log-controller.test.js
  78. 15
      app/scripts/controllers/permissions/permissions-middleware.test.js
  79. 6
      app/scripts/controllers/permissions/permissionsLog.js
  80. 2
      app/scripts/controllers/permissions/restricted-methods.test.js
  81. 11
      app/scripts/controllers/preferences.js
  82. 25
      app/scripts/controllers/preferences.test.js
  83. 12
      app/scripts/controllers/swaps.test.js
  84. 2
      app/scripts/controllers/token-rates-controller.test.js
  85. 175
      app/scripts/controllers/transactions/index.js
  86. 300
      app/scripts/controllers/transactions/index.test.js
  87. 4
      app/scripts/controllers/transactions/lib/tx-state-history-helpers.test.js
  88. 60
      app/scripts/controllers/transactions/lib/util.js
  89. 2
      app/scripts/controllers/transactions/lib/util.test.js
  90. 4
      app/scripts/controllers/transactions/pending-tx-tracker.test.js
  91. 4
      app/scripts/controllers/transactions/tx-gas-utils.test.js
  92. 633
      app/scripts/controllers/transactions/tx-state-manager.js
  93. 727
      app/scripts/controllers/transactions/tx-state-manager.test.js
  94. 2
      app/scripts/lib/ComposableObservableStore.test.js
  95. 36
      app/scripts/lib/buy-eth-url.js
  96. 16
      app/scripts/lib/buy-eth-url.test.js
  97. 2
      app/scripts/lib/cleanErrorStack.test.js
  98. 33
      app/scripts/lib/createMetaRPCHandler.js
  99. 61
      app/scripts/lib/createMetaRPCHandler.test.js
  100. 2
      app/scripts/lib/decrypt-message-manager.js
  101. Some files were not shown because too many files have changed in this diff Show More

@ -123,12 +123,18 @@ jobs:
- checkout
- attach_workspace:
at: .
- run:
name: Bump manifest version
command: .circleci/scripts/release-bump-manifest-version.sh
- run:
name: Update changelog
command: yarn update-changelog
- run:
name: Commit changes
command: .circleci/scripts/release-commit-version-bump.sh
- run:
name: Create GitHub Pull Request for version
command: |
.circleci/scripts/release-bump-changelog-version.sh
.circleci/scripts/release-bump-manifest-version.sh
.circleci/scripts/release-create-release-pr.sh
command: .circleci/scripts/release-create-release-pr.sh
prep-deps:
executor: node-browsers
@ -470,9 +476,6 @@ jobs:
name: Create GitHub release
command: |
.circleci/scripts/release-create-gh-release.sh
- run:
name: Create GitHub Pull Request to sync master with develop
command: .circleci/scripts/release-create-master-pr.sh
job-publish-storybook:
executor: node-browsers

@ -21,13 +21,12 @@ version="${CIRCLE_BRANCH/Version-v/}"
if ! grep --quiet --fixed-strings "$version" CHANGELOG.md
then
printf '%s\n' 'Adding this release to CHANGELOG.md'
date_str="$(date '+%a %b %d %Y')"
cp CHANGELOG.md{,.bak}
update_headers=$(cat <<END
/## Current Develop Branch/ {
print "## Current Develop Branch\n";
print "## ${version} ${date_str}";
print "## ${version}";
next;
}
{

@ -26,14 +26,4 @@ yarn prettier --write app/manifest/_base.json
if [[ -z $(git status --porcelain) ]]
then
printf '%s\n' 'App manifest version already set'
exit 0
fi
git \
-c user.name='MetaMask Bot' \
-c user.email='metamaskbot@users.noreply.github.com' \
commit --message "${CIRCLE_BRANCH/-/ }" \
CHANGELOG.md app/manifest/_base.json
repo_slug="$CIRCLE_PROJECT_USERNAME/$CIRCLE_PROJECT_REPONAME"
git push "https://$GITHUB_TOKEN_USER:$GITHUB_TOKEN@github.com/$repo_slug" "$CIRCLE_BRANCH"

@ -0,0 +1,34 @@
#!/usr/bin/env bash
set -e
set -u
set -o pipefail
if [[ "${CI:-}" != 'true' ]]
then
printf '%s\n' 'CI environment variable must be set to true'
exit 1
fi
if [[ "${CIRCLECI:-}" != 'true' ]]
then
printf '%s\n' 'CIRCLECI environment variable must be set to true'
exit 1
fi
printf '%s\n' 'Commit the manifest version and changelog if the manifest has changed'
if git diff --quiet app/manifest/_base.json;
then
printf '%s\n' 'No manifest changes to commit'
exit 0
fi
git \
-c user.name='MetaMask Bot' \
-c user.email='metamaskbot@users.noreply.github.com' \
commit --message "${CIRCLE_BRANCH/-/ }" \
CHANGELOG.md app/manifest/_base.json
repo_slug="$CIRCLE_PROJECT_USERNAME/$CIRCLE_PROJECT_REPONAME"
git push "https://$GITHUB_TOKEN_USER:$GITHUB_TOKEN@github.com/$repo_slug" "$CIRCLE_BRANCH"

@ -1,52 +0,0 @@
#!/usr/bin/env bash
set -e
set -u
set -o pipefail
if [[ "${CI:-}" != 'true' ]]
then
printf '%s\n' 'CI environment variable must be set to true'
exit 1
fi
if [[ "${CIRCLECI:-}" != 'true' ]]
then
printf '%s\n' 'CIRCLECI environment variable must be set to true'
exit 1
fi
if [[ -z "${GITHUB_TOKEN:-}" ]]
then
printf '%s\n' 'GITHUB_TOKEN environment variable must be set'
exit 1
fi
function install_github_cli ()
{
printf '%s\n' 'Installing hub CLI'
pushd "$(mktemp -d)"
curl -sSL 'https://github.com/github/hub/releases/download/v2.11.2/hub-linux-amd64-2.11.2.tgz' | tar xz
PATH="$PATH:$PWD/hub-linux-amd64-2.11.2/bin"
popd
}
base_branch='develop'
if [[ -n "${CI_PULL_REQUEST:-}" ]]
then
printf '%s\n' 'CI_PULL_REQUEST is set, pull request already exists for this build'
exit 0
fi
install_github_cli
printf '%s\n' "Creating a Pull Request to sync 'master' with 'develop'"
if ! hub pull-request \
--message "Master => develop" --message 'Merge latest release back into develop' \
--base "$CIRCLE_PROJECT_USERNAME:$base_branch" \
--head "$CIRCLE_PROJECT_USERNAME:$CIRCLE_BRANCH";
then
printf '%s\n' 'Pull Request already exists'
fi

@ -26,7 +26,7 @@ module.exports = {
'test-*/**',
'docs/**',
'coverage/',
'app/scripts/chromereload.js',
'development/chromereload.js',
'app/vendor/**',
'test/e2e/send-eth-with-private-key-test/**',
'nyc_output/**',
@ -37,12 +37,10 @@ module.exports = {
extends: [
'@metamask/eslint-config',
'@metamask/eslint-config/config/nodejs',
'@metamask/eslint-config/config/mocha',
'plugin:react/recommended',
'plugin:react-hooks/recommended',
'prettier',
],
plugins: ['@babel', 'react', 'import', 'prettier'],
plugins: ['@babel', 'import', 'prettier'],
globals: {
document: 'readonly',
@ -53,90 +51,10 @@ module.exports = {
// Prettier changes and reasoning
'prettier/prettier': 'error',
// Our eslint config has the default setting for this as error. This
// include beforeBlockComment: true, but in order to match the prettier
// spec you have to enable before and after blocks, objects and arrays
// https://github.com/prettier/eslint-config-prettier#lines-around-comment
'lines-around-comment': [
'error',
{
beforeBlockComment: true,
afterLineComment: false,
allowBlockStart: true,
allowBlockEnd: true,
allowObjectStart: true,
allowObjectEnd: true,
allowArrayStart: true,
allowArrayEnd: true,
},
],
// Prettier has some opinions on mixed-operators, and there is ongoing work
// to make the output code clear. It is better today then it was when the first
// PR to add prettier. That being said, the workaround for keeping this rule enabled
// requires breaking parts of operations into different variables -- which I believe
// to be worse. https://github.com/prettier/eslint-config-prettier#no-mixed-operators
'no-mixed-operators': 'off',
// Prettier wraps single line functions with ternaries, etc in parens by default, but
// if the line is long enough it breaks it into a separate line and removes the parens.
// The second behavior conflicts with this rule. There is some guides on the repo about
// how you can keep it enabled:
// https://github.com/prettier/eslint-config-prettier#no-confusing-arrow
// However, in practice this conflicts with prettier adding parens around short lines,
// when autofixing in vscode and others.
'no-confusing-arrow': 'off',
// There is no configuration in prettier for how it stylizes regexes, which conflicts
// with wrap-regex.
'wrap-regex': 'off',
// Prettier handles all indentation automagically. it can be configured here
// https://prettier.io/docs/en/options.html#tab-width but the default matches our
// style.
indent: 'off',
// This rule conflicts with the way that prettier breaks code across multiple lines when
// it exceeds the maximum length. Prettier optimizes for readability while simultaneously
// maximizing the amount of code per line.
'function-paren-newline': 'off',
// This rule throws an error when there is a line break in an arrow function declaration
// but prettier breaks arrow function declarations to be as readable as possible while
// still conforming to the width rules.
'implicit-arrow-linebreak': 'off',
// This rule would result in an increase in white space in lines with generator functions,
// which impacts prettier's goal of maximizing code per line and readability. There is no
// current workaround.
'generator-star-spacing': 'off',
'default-param-last': 'off',
'require-atomic-updates': 'off',
'import/no-unassigned-import': 'off',
'prefer-object-spread': 'error',
'react/no-unused-prop-types': 'error',
'react/no-unused-state': 'error',
'react/jsx-boolean-value': 'error',
'react/jsx-curly-brace-presence': [
'error',
{ props: 'never', children: 'never' },
],
'react/jsx-equals-spacing': 'error',
'react/no-deprecated': 'error',
'react/default-props-match-prop-types': 'error',
'react/jsx-closing-tag-location': [
'error',
{ selfClosing: 'tag-aligned', nonEmpty: 'tag-aligned' },
],
'react/jsx-no-duplicate-props': 'error',
'react/jsx-closing-bracket-location': 'error',
'react/jsx-first-prop-new-line': ['error', 'multiline'],
'react/jsx-max-props-per-line': [
'error',
{ maximum: 1, when: 'multiline' },
],
'react/jsx-tag-spacing': [
'error',
{
closingSlash: 'never',
beforeSelfClosing: 'always',
afterOpening: 'never',
},
],
'default-param-last': 'off',
'require-atomic-updates': 'off',
'no-invalid-this': 'off',
'@babel/no-invalid-this': 'error',
@ -145,7 +63,6 @@ module.exports = {
semi: 'off',
'@babel/semi': 'off',
'mocha/no-setup-in-describe': 'off',
'node/no-process-env': 'off',
// TODO: re-enable these rules
@ -155,9 +72,28 @@ module.exports = {
},
overrides: [
{
files: ['test/e2e/**/*.js'],
files: ['ui/**/*.js', 'test/lib/render-helpers.js'],
plugins: ['react'],
extends: ['plugin:react/recommended', 'plugin:react-hooks/recommended'],
rules: {
'react/no-unused-prop-types': 'error',
'react/no-unused-state': 'error',
'react/jsx-boolean-value': 'error',
'react/jsx-curly-brace-presence': [
'error',
{ props: 'never', children: 'never' },
],
'react/no-deprecated': 'error',
'react/default-props-match-prop-types': 'error',
'react/jsx-no-duplicate-props': 'error',
},
},
{
files: ['test/e2e/**/*.spec.js'],
extends: ['@metamask/eslint-config/config/mocha'],
rules: {
'mocha/no-hooks-for-single-case': 'off',
'mocha/no-setup-in-describe': 'off',
},
},
{
@ -173,14 +109,18 @@ module.exports = {
},
},
{
files: ['test/**/*-test.js', 'test/**/*.spec.js'],
files: ['**/*.test.js'],
extends: ['@metamask/eslint-config/config/mocha'],
rules: {
// Mocha will re-assign `this` in a test context
'@babel/no-invalid-this': 'off',
'mocha/no-setup-in-describe': 'off',
},
},
{
files: ['development/**/*.js', 'test/e2e/benchmark.js', 'test/helper.js'],
files: [
'development/**/*.js',
'test/e2e/benchmark.js',
'test/helpers/setup-helper.js',
],
rules: {
'node/no-process-exit': 'off',
'node/shebang': 'off',

1
.gitignore vendored

@ -8,6 +8,7 @@ audit.json
app/bower_components
test/bower_components
package
.eslintcache
# IDEs
.idea

@ -5,7 +5,6 @@ const state = {
isInitialized: true,
isUnlocked: true,
featureFlags: { sendHexData: true },
rpcUrl: 'https://rawtestrpc.metamask.io/',
identities: {
'0xfdea65c8e26263f6d9a1b5de9555d2931a33b825': {
address: '0xfdea65c8e26263f6d9a1b5de9555d2931a33b825',

File diff suppressed because it is too large Load Diff

@ -505,9 +505,6 @@
"importWallet": {
"message": "ቋት አስመጣ"
},
"importYourExisting": {
"message": "ባለ 12 ቃል የዘር ሐረግን በመጠቀም ነባር ቋትዎን ያስመጡ"
},
"imported": {
"message": "ከውጭ የመጣ",
"description": "status showing that an account has been fully loaded into the keyring"
@ -892,9 +889,6 @@
"secretBackupPhraseWarning": {
"message": "ማስጠንቀቂያ፡ የመጠባበቂያ ምዕራፍዎን በጭራሽ አይግለጹ። ይህን ሐረገ የያዘ ማንኛውም ሰው የእርስዎን Ether እስከወዲያኛው ሊወስደው ይችላል።"
},
"secretPhrase": {
"message": "ካዝናዎን ወደነበረበት ለመመለስ ሚስጥራዊ ባለ አስራ ሁለት ቃል ሐረግዎን ያስገቡ።"
},
"securityAndPrivacy": {
"message": "ደህንነት እና ግላዊነት"
},
@ -934,9 +928,6 @@
"sendAmount": {
"message": "መጠኑን ላክ"
},
"sendETH": {
"message": "ETH ላክ"
},
"sendTokens": {
"message": "ተለዋጭ ስሞችን ላክ"
},

@ -501,9 +501,6 @@
"importWallet": {
"message": "استيراد المحفظة"
},
"importYourExisting": {
"message": "قم باستيراد محفظتك الحالية باستخدام جملة بذرية مكونة من 12 كلمة"
},
"imported": {
"message": "المستوردة",
"description": "status showing that an account has been fully loaded into the keyring"
@ -888,9 +885,6 @@
"secretBackupPhraseWarning": {
"message": "تحذير: لا تكشف مطلقاً عن عبارة الدعم الخاصة بك. يمكن لأي شخص بهذه العبارة أن يستحوذ على الأثير الخاص بك إلى الأبد."
},
"secretPhrase": {
"message": "أدخل جملتك السرية المكونة من اثني عشر كلمة هنا لاستعادة خزنتك."
},
"securityAndPrivacy": {
"message": "الأمن والخصوصية"
},
@ -930,9 +924,6 @@
"sendAmount": {
"message": "إرسال المبلغ"
},
"sendETH": {
"message": "إرسال عملة إيثيريوم"
},
"sendTokens": {
"message": "إرسال عملات رمزية"
},

@ -501,9 +501,6 @@
"importWallet": {
"message": "Импортиране на портфейла"
},
"importYourExisting": {
"message": "Импортирайте съществуващия си портфейл с помощта на фраза зародиш с 12 думи"
},
"imported": {
"message": "Импортирани",
"description": "status showing that an account has been fully loaded into the keyring"
@ -891,9 +888,6 @@
"secretBackupPhraseWarning": {
"message": "ВНИМАНИЕ: Никога не разкривайте резервната си фраза. Всеки с тази фраза може да вземе вашия етер завинаги."
},
"secretPhrase": {
"message": "Въведете своята тайна фраза от дванадесет думи тук, за да възстановите портфейла си."
},
"securityAndPrivacy": {
"message": "Сигурност и поверителност"
},
@ -933,9 +927,6 @@
"sendAmount": {
"message": "Изпратете сумата"
},
"sendETH": {
"message": "Изпрати ETH"
},
"sendTokens": {
"message": "Изпращане на жетони"
},

@ -505,9 +505,6 @@
"importWallet": {
"message": "ওয়ট আমদি করন"
},
"importYourExisting": {
"message": "একটি 12 শবর সড ফজ বযবহর কর আপনর বিযমন ওয়ট আমদি করন"
},
"imported": {
"message": "আমদিত",
"description": "status showing that an account has been fully loaded into the keyring"
@ -895,9 +892,6 @@
"secretBackupPhraseWarning": {
"message": "সতরকত: কখনও আপনর বকআপ ফজ পরকশ করবন ন। এই ফজ পউ আপনর ইথর চিরকর জনয নিিরবন।"
},
"secretPhrase": {
"message": "আপনর ভলট রির করত এখ আপনর গপন ব শবর ফজ লিন।"
},
"securityAndPrivacy": {
"message": "নিপত এবপনয়ত"
},
@ -937,9 +931,6 @@
"sendAmount": {
"message": "প অরথরি"
},
"sendETH": {
"message": "ETH পন"
},
"sendTokens": {
"message": "টনগিন"
},

@ -492,9 +492,6 @@
"importWallet": {
"message": "Importar Moneder"
},
"importYourExisting": {
"message": "Importa el teu moneder utilitzant la frase de recuperació de 12 paraules"
},
"imported": {
"message": "Importats",
"description": "status showing that an account has been fully loaded into the keyring"
@ -873,9 +870,6 @@
"secretBackupPhraseWarning": {
"message": "ATENCIÓ: No divulguis mai la teva frase de recuperació. Qualsevol amb aquesta frase pot utilitzar el teu Ether per sempre."
},
"secretPhrase": {
"message": "Introdueix aquí la teva frase secreta de dotze paraules per a recuperar la teva caixa forta."
},
"securityAndPrivacy": {
"message": "Seguretat i privacitat"
},
@ -915,9 +909,6 @@
"sendAmount": {
"message": "Enviar Quantitat"
},
"sendETH": {
"message": "Envia ETH"
},
"sendTokens": {
"message": "Enviar Fitxes"
},

@ -344,9 +344,6 @@
"searchTokens": {
"message": "Hledat tokeny"
},
"secretPhrase": {
"message": "Zadejte svých 12 slov tajné fráze k obnovení trezoru."
},
"seedPhraseReq": {
"message": "klíčové fráze mají 12 slov"
},
@ -356,9 +353,6 @@
"send": {
"message": "Odeslat"
},
"sendETH": {
"message": "Odeslat ETH"
},
"sendTokens": {
"message": "Odeslat tokeny"
},

@ -498,9 +498,6 @@
"importWallet": {
"message": "Importér pung"
},
"importYourExisting": {
"message": "Importér din eksisterende pung med en 12-ords seedfrase"
},
"imported": {
"message": "Importeret",
"description": "status showing that an account has been fully loaded into the keyring"
@ -876,9 +873,6 @@
"secretBackupPhraseWarning": {
"message": "ADVARSEL: Afslør aldrig din backup-frase. Enhver med denne frase kan tage din Ether for altid."
},
"secretPhrase": {
"message": "Indtast din hemmelige tolv ord lange sætning for at gendanne din vault."
},
"securityAndPrivacy": {
"message": "Sikkerhed & Privatliv"
},
@ -915,9 +909,6 @@
"sendAmount": {
"message": "Send Beløb"
},
"sendETH": {
"message": "Vælg ETH"
},
"sendTokens": {
"message": "Send tokens"
},

@ -493,9 +493,6 @@
"importWallet": {
"message": "Wallet importieren"
},
"importYourExisting": {
"message": "Importieren Sie Ihre bestehende Wallet mit einem 12-Wort-Seed-Schlüssel"
},
"imported": {
"message": "Importiert",
"description": "status showing that an account has been fully loaded into the keyring"
@ -864,9 +861,6 @@
"secretBackupPhraseWarning": {
"message": "WARNUNG: Legen Sie niemals Ihre Sicherungsphrase offen. Mit dieser Phrase kann sich jeder Ihr Ether für immer aneignen."
},
"secretPhrase": {
"message": "Gib die 12 Wörter deiner geheimem Wörterfolge ein um deinen Vault wiederherzustellen."
},
"securityAndPrivacy": {
"message": "Sicherheit & Datenschutz"
},
@ -906,9 +900,6 @@
"sendAmount": {
"message": "Betrag senden"
},
"sendETH": {
"message": "ETH senden"
},
"sendTokens": {
"message": "Token senden"
},

@ -502,9 +502,6 @@
"importWallet": {
"message": "Εισαγωγή Πορτοφολιού"
},
"importYourExisting": {
"message": "Εισαγάγετε το υπάρχον πορτοφόλι σας χρησιμοποιώντας μια φράση φύτρου 12 λέξεων"
},
"imported": {
"message": "Έγινε εισαγωγή",
"description": "status showing that an account has been fully loaded into the keyring"
@ -892,9 +889,6 @@
"secretBackupPhraseWarning": {
"message": "ΠΡΟΕΙΔΟΠΟΙΗΣΗ: Ποτέ μην αποκαλύπτετε την εφεδρική φράση. Όποιος έχει αυτή τη φράση μπορεί να πάρει τα Ether σας για πάντα."
},
"secretPhrase": {
"message": "Εισαγάγετε εδώ τη μυστική φράση δώδεκα λέξεων για να επαναφέρετε το χρηματοκιβώτιό σας."
},
"securityAndPrivacy": {
"message": "Ασφάλεια και Απόρρητο"
},
@ -934,9 +928,6 @@
"sendAmount": {
"message": "Αποστολή Ποσού"
},
"sendETH": {
"message": "Στείλτε ETH"
},
"sendTokens": {
"message": "Στείλτε Tokens"
},

@ -869,7 +869,7 @@
"message": "Import wallet"
},
"importYourExisting": {
"message": "Import your existing wallet using a 12 word seed phrase"
"message": "Import your existing wallet using a seed phrase"
},
"imported": {
"message": "Imported",
@ -1060,7 +1060,7 @@
"message": "MetaMask would like to gather usage data to better understand how our users interact with the extension. This data will be used to continually improve the usability and user experience of our product and the Ethereum ecosystem."
},
"mismatchedChain": {
"message": "This network details for this Chain ID do not match our records. We recommend that you $1 before proceeding.",
"message": "The network details for this chain ID do not match our records. We recommend that you $1 before proceeding.",
"description": "$1 is a clickable link with text defined by the 'mismatchedChainLinkText' key"
},
"mismatchedChainLinkText": {
@ -1451,7 +1451,7 @@
"message": "WARNING: Never disclose your backup phrase. Anyone with this phrase can take your Ether forever."
},
"secretPhrase": {
"message": "Enter your secret twelve word phrase here to restore your vault."
"message": "Enter your secret phrase here to restore your vault."
},
"securityAndPrivacy": {
"message": "Security & Privacy"
@ -1507,9 +1507,6 @@
"sendAmount": {
"message": "Send Amount"
},
"sendETH": {
"message": "Send ETH"
},
"sendSpecifiedTokens": {
"message": "Send $1",
"description": "Symbol of the specified token"
@ -2218,6 +2215,9 @@
"walletSeed": {
"message": "Seed phrase"
},
"walletSeedRestore": {
"message": "Wallet Seed"
},
"web3ShimUsageNotification": {
"message": "We noticed that the current website tried to use the removed window.web3 API. If the site appears to be broken, please click $1 for more information.",
"description": "$1 is a clickable link."

@ -796,9 +796,6 @@
"importWallet": {
"message": "Importar Monedero"
},
"importYourExisting": {
"message": "Importa tu monedero existente usando la frase semilla de 12 palabras"
},
"imported": {
"message": "Importado",
"description": "status showing that an account has been fully loaded into the keyring"
@ -1355,9 +1352,6 @@
"secretBackupPhraseWarning": {
"message": "ADVERTENCIA: Nunca revele su frase de respaldo. Cualquiera con esta frase puede tomar su Ether para siempre."
},
"secretPhrase": {
"message": "Ingresa tu frase de doce (12) palabras para restaurar tu bóveda."
},
"securityAndPrivacy": {
"message": "Seguridad y Privacidad"
},
@ -1409,9 +1403,6 @@
"sendAmount": {
"message": "Enviar cantidad"
},
"sendETH": {
"message": "Enviar ETH"
},
"sendSpecifiedTokens": {
"message": "Enviar $1",
"description": "Symbol of the specified token"

@ -796,9 +796,6 @@
"importWallet": {
"message": "Importar billetera"
},
"importYourExisting": {
"message": "Importa tu billetera existente con una frase semilla de 12 palabras"
},
"imported": {
"message": "Importado",
"description": "status showing that an account has been fully loaded into the keyring"
@ -1355,9 +1352,6 @@
"secretBackupPhraseWarning": {
"message": "ADVERTENCIA: Nunca reveles tu frase de respaldo. Cualquier persona que tenga acceso a esta frase puede llevarse tus Ether permanentemente."
},
"secretPhrase": {
"message": "Ingresa tu frase secreta de doce palabras para restaurar tu almacén."
},
"securityAndPrivacy": {
"message": "Seguridad y privacidad"
},
@ -1409,9 +1403,6 @@
"sendAmount": {
"message": "Enviar monto"
},
"sendETH": {
"message": "Enviar ETH"
},
"sendSpecifiedTokens": {
"message": "Enviar $1",
"description": "Symbol of the specified token"

@ -501,9 +501,6 @@
"importWallet": {
"message": "Importige rahakott"
},
"importYourExisting": {
"message": "Importige 12-sõnalise seemnefraasi abil olemasolev rahakott"
},
"imported": {
"message": "Imporditud",
"description": "status showing that an account has been fully loaded into the keyring"
@ -885,9 +882,6 @@
"secretBackupPhraseWarning": {
"message": "HOIATUS! Ärge avaldage kunagi oma varundusfraasi. Selle fraasiga on võimalik teie eeter igaveseks ära võtta."
},
"secretPhrase": {
"message": "Sisestage hoidla taastamiseks oma salajane 12-sõnaline fraas."
},
"securityAndPrivacy": {
"message": "Turvalisus ja privaatsus"
},
@ -927,9 +921,6 @@
"sendAmount": {
"message": "Saatke kogus"
},
"sendETH": {
"message": "Saada ETH"
},
"sendTokens": {
"message": "Saada lube"
},

@ -505,9 +505,6 @@
"importWallet": {
"message": "وارد سازی کیف"
},
"importYourExisting": {
"message": "کیف موجود تان را با استفاده از عبارت رمزیاب 12 کلمه یی وارد کنید"
},
"imported": {
"message": "وارد شده",
"description": "status showing that an account has been fully loaded into the keyring"
@ -895,9 +892,6 @@
"secretBackupPhraseWarning": {
"message": "هشدار: هرگز عبارت پشتیبان تان را به کسی فاش نسازید. هرکسیکه این عبارت را داشته باشد ایتر شما را برای همیشه خواهد گرفت."
},
"secretPhrase": {
"message": "جهت بازیابی خزانه تان عبارت دوازده کلمه یی تان را اینجا وارد کنید."
},
"securityAndPrivacy": {
"message": "امنیت و حریم خصوصی"
},
@ -937,9 +931,6 @@
"sendAmount": {
"message": "ارسال مبلغ"
},
"sendETH": {
"message": "ارسال ETH"
},
"sendTokens": {
"message": "رمزیاب ها را ارسال کنید"
},

@ -505,9 +505,6 @@
"importWallet": {
"message": "Tuo kukkaro"
},
"importYourExisting": {
"message": "Tuo nykyinen lompakkosi käyttäen 12 sanan \n siemenlausetta"
},
"imported": {
"message": "Tuotu",
"description": "status showing that an account has been fully loaded into the keyring"
@ -892,9 +889,6 @@
"secretBackupPhraseWarning": {
"message": "VAROITUS: älä koskaan kerro varmuuskopiolausettasi kenellekään. Kuka tahansa tämän lauseen omaava voi napata Etherisi pysyvästi."
},
"secretPhrase": {
"message": "Palauta holvisi syöttämällä tähän salainen kahdentoista sanan tekstisi."
},
"securityAndPrivacy": {
"message": "Turva & yksityisyys"
},
@ -934,9 +928,6 @@
"sendAmount": {
"message": "Lähetä summa"
},
"sendETH": {
"message": "Lähetä ETH:iä"
},
"sendTokens": {
"message": "Lähetä tietueita"
},

@ -462,9 +462,6 @@
"importWallet": {
"message": "Mag-import ng Wallet"
},
"importYourExisting": {
"message": "I-import ang kasalukuyan mong wallet gamit ang isang seed phrase na may 12 salita"
},
"imported": {
"message": "Na-import",
"description": "status showing that an account has been fully loaded into the keyring"
@ -807,9 +804,6 @@
"secretBackupPhraseWarning": {
"message": "BABALA: Huwag ibunyag ang iyong backup phrase. Mananakaw ng kahit sinong may ganitong parirala ang iyong Ether at hindi na ito maibabalik."
},
"secretPhrase": {
"message": "Ilagay ang iyong lihim na pariralang may labindalawang salita para ma-restore ang iyong vault."
},
"securityAndPrivacy": {
"message": "Seguridad at Privacy"
},
@ -849,9 +843,6 @@
"sendAmount": {
"message": "Magpadala ng Halaga"
},
"sendETH": {
"message": "Magpadala ng ETH"
},
"sendTokens": {
"message": "Magpadala ng Mga Token"
},

@ -496,9 +496,6 @@
"importWallet": {
"message": "Importer le portefeuille"
},
"importYourExisting": {
"message": "Importez votre portefeuille existant à l'aide d'une phrase mnémotechnique de 12 mots"
},
"imported": {
"message": "Importé",
"description": "status showing that an account has been fully loaded into the keyring"
@ -877,9 +874,6 @@
"secretBackupPhraseWarning": {
"message": "AVERTISSEMENT : ne révélez jamais votre phrase de sauvegarde. N'importe qui avec cette phrase peut voler votre Ether pour toujours."
},
"secretPhrase": {
"message": "Entrez vos 12 mots secrets de votre phrase Seed pour restaurer votre coffre."
},
"securityAndPrivacy": {
"message": "Sécurité et confidentialité"
},
@ -919,9 +913,6 @@
"sendAmount": {
"message": "Envoyer le montant"
},
"sendETH": {
"message": "Envoyer des ETH"
},
"sendTokens": {
"message": "Envoyer des jetons"
},

@ -505,9 +505,6 @@
"importWallet": {
"message": "ייבא ארנק"
},
"importYourExisting": {
"message": "יבא/י את הארנק הקיים שלך באמצעות seed phrase בן 12 מילים"
},
"imported": {
"message": "מיובאות",
"description": "status showing that an account has been fully loaded into the keyring"
@ -889,9 +886,6 @@
"secretBackupPhraseWarning": {
"message": "אזהרה: לעולם אין לחשוף את צירוף הגיבוי שלך. כל מי שברשותו צירוף זה יכול לקחת את האת'ר שלך לצמיתות."
},
"secretPhrase": {
"message": "הזנ/י את הצירוף הסודי שלך של שתים-עשרה המילים כאן כדי לשחזר את הכספת שלך."
},
"securityAndPrivacy": {
"message": "אבטחה ופרטיות"
},
@ -931,9 +925,6 @@
"sendAmount": {
"message": "שלח סכום"
},
"sendETH": {
"message": "שלח/י ETH"
},
"sendTokens": {
"message": "שלח טוקנים"
},

@ -787,9 +787,6 @@
"importWallet": {
"message": "वट आयत कर"
},
"importYourExisting": {
"message": "12 शबद कडफ उपयग करक अपनट क आयत कर"
},
"imported": {
"message": "आयित",
"description": "status showing that an account has been fully loaded into the keyring"
@ -1346,9 +1343,6 @@
"secretBackupPhraseWarning": {
"message": "चवन: कभ अपनकअप वश क न कर। इस वश कथ कई भ आपक Ether क हमिए ल सकत।"
},
"secretPhrase": {
"message": "अपनिनरित करनिए अपनत बरह शबद वश क यह दरज कर।"
},
"securityAndPrivacy": {
"message": "सरक और गपनयत"
},
@ -1400,9 +1394,6 @@
"sendAmount": {
"message": "रि"
},
"sendETH": {
"message": "ETH भ"
},
"sendSpecifiedTokens": {
"message": "$1 भ",
"description": "Symbol of the specified token"

@ -324,9 +324,6 @@
"search": {
"message": "खज कर"
},
"secretPhrase": {
"message": "अपनत बरह शबद वश यह अपनिनरित करनिए दरज कर।"
},
"seedPhraseReq": {
"message": "बज वश 12 शबद ल"
},
@ -336,9 +333,6 @@
"send": {
"message": "भ"
},
"sendETH": {
"message": "भ ETH"
},
"sendTokens": {
"message": "भकन"
},

@ -501,9 +501,6 @@
"importWallet": {
"message": "Uvezi novčanik"
},
"importYourExisting": {
"message": "Uvezite svoj postojeći novčanik početnom rečenicom od 12 riječi"
},
"imported": {
"message": "Uvezeno",
"description": "status showing that an account has been fully loaded into the keyring"
@ -888,9 +885,6 @@
"secretBackupPhraseWarning": {
"message": "UPOZORENJE: nikada ne otkrivajte svoju alternativnu rečenicu. Bilo tko ovom rečenicom može zauvijek preuzeti vaš Ether."
},
"secretPhrase": {
"message": "Ovdje upišite svoju tajnu rečenicu od 12 riječi kako biste obnovili svoj sef."
},
"securityAndPrivacy": {
"message": "Sigurnost i privatnost"
},
@ -930,9 +924,6 @@
"sendAmount": {
"message": "Odaberi iznos"
},
"sendETH": {
"message": "Pošalji ETH"
},
"sendTokens": {
"message": "Pošalji tokene"
},

@ -561,9 +561,6 @@
"searchTokens": {
"message": "Rechèch Tokens"
},
"secretPhrase": {
"message": "Antre fraz sekrè douz mo ou a pou w restore kòf ou a."
},
"seedPhraseReq": {
"message": "Seed fraz yo se 12 long mo"
},
@ -585,9 +582,6 @@
"send": {
"message": "Voye"
},
"sendETH": {
"message": "Voye ETH"
},
"sendTokens": {
"message": "Voye Tokens"
},

@ -501,9 +501,6 @@
"importWallet": {
"message": "Pénztárca importálása"
},
"importYourExisting": {
"message": "Importálja meglévő pénztárcáját a 12 szóból álló seed mondat segítségével"
},
"imported": {
"message": "Importált",
"description": "status showing that an account has been fully loaded into the keyring"
@ -888,9 +885,6 @@
"secretBackupPhraseWarning": {
"message": "FIGYELEM: Senkise se adja meg a biztonsági szakaszát. Ennek tulajdonosa örökre elviheti Ether-jeit."
},
"secretPhrase": {
"message": "Tárolód helyreállításához írd be titkos tizenkét szavas szókapcsolatodat ide."
},
"securityAndPrivacy": {
"message": "Biztonság és adatvédelem"
},
@ -930,9 +924,6 @@
"sendAmount": {
"message": "Összeg küldése"
},
"sendETH": {
"message": "ETH küldése"
},
"sendTokens": {
"message": "Token küldése"
},

@ -787,9 +787,6 @@
"importWallet": {
"message": "Impor dompet"
},
"importYourExisting": {
"message": "Impor dompet Anda yang ada menggunakan frasa pemulihan 12 kata"
},
"imported": {
"message": "Diimpor",
"description": "status showing that an account has been fully loaded into the keyring"
@ -1346,9 +1343,6 @@
"secretBackupPhraseWarning": {
"message": "PERINGATAN: Jangan pernah ungkapkan frasa cadangan Anda. Siapa pun yang memiliki frasa ini dapat mengambil Ether Anda selamanya."
},
"secretPhrase": {
"message": "Masukkan frasa kata dua belas rahasia Anda di sini untuk memulihkan vault Anda."
},
"securityAndPrivacy": {
"message": "Keamanan & Privasi"
},
@ -1400,9 +1394,6 @@
"sendAmount": {
"message": "Kirim Jumlah"
},
"sendETH": {
"message": "Kirim ETH"
},
"sendSpecifiedTokens": {
"message": "Kirim $1",
"description": "Symbol of the specified token"

@ -799,9 +799,6 @@
"importWallet": {
"message": "Importa Portafoglio"
},
"importYourExisting": {
"message": "Importa il tuo portafoglio esistente usando la tua frase seed a 12 parole"
},
"imported": {
"message": "Importato",
"description": "status showing that an account has been fully loaded into the keyring"
@ -1361,9 +1358,6 @@
"secretBackupPhraseWarning": {
"message": "ATTENZIONE: Non dire mai a nessuno questa frase di backup. Chiunque con questa frase può rubare i tuoi Ether per sempre."
},
"secretPhrase": {
"message": "Inserisci la tua frase segreta di dodici parole per ripristinare la cassaforte."
},
"securityAndPrivacy": {
"message": "Sicurezza & Privacy"
},
@ -1415,9 +1409,6 @@
"sendAmount": {
"message": "Invia Importo"
},
"sendETH": {
"message": "Invia ETH"
},
"sendSpecifiedTokens": {
"message": "Invia $1",
"description": "Symbol of the specified token"

@ -796,9 +796,6 @@
"importWallet": {
"message": "ウォレットのインポート"
},
"importYourExisting": {
"message": "12単語のシードフレーズを使用して既存のウォレットをインポートします"
},
"imported": {
"message": "インポート済",
"description": "status showing that an account has been fully loaded into the keyring"
@ -1355,9 +1352,6 @@
"secretBackupPhraseWarning": {
"message": "警告:シードフレーズは絶対に公開しないでください。シードフレーズを使うと、誰でもアカウントからETHを盗み出せます。"
},
"secretPhrase": {
"message": "アカウント情報を復元するには、12単語で構成されたシードフレーズを入力してください。"
},
"securityAndPrivacy": {
"message": "セキュリティとプライバシー"
},
@ -1409,9 +1403,6 @@
"sendAmount": {
"message": "送金額"
},
"sendETH": {
"message": "ETHの送金"
},
"sendSpecifiedTokens": {
"message": "$1 を送る",
"description": "Symbol of the specified token"

@ -505,9 +505,6 @@
"importWallet": {
"message": "ವ ಅನ ಆಮದಿ"
},
"importYourExisting": {
"message": "12 ಪದದ ಸ ಅನ ಬಳಸಿಿಮ ಅಸಿವದಲಿವ ವ ಅನ ಆಮದಿ"
},
"imported": {
"message": "ಆಮದಡಲಿ",
"description": "status showing that an account has been fully loaded into the keyring"
@ -895,9 +892,6 @@
"secretBackupPhraseWarning": {
"message": "ಎಚಚರಿ: ನಿಮ ಬಕಪ ಅನಿ ಬಹಿರಗಪಡಿಸಬಿ. ಈ ಫ ಅನಿವ ಯದರಿಮ ಎಥರ ಅನವತವಿಳಬಹ."
},
"secretPhrase": {
"message": "ನಿಮ ವ ಅನ ಮರಿಸಲಿಮ ರಹಸಯ ಹನರಡ ಪದದ ಫ ಅನ ಇಲಿ ನಮಿಿ."
},
"securityAndPrivacy": {
"message": "ಭದರತ ಮತಯತ"
},
@ -937,9 +931,6 @@
"sendAmount": {
"message": "ಮತವನ ಕಳಿಿ"
},
"sendETH": {
"message": "ETH ಕಳಿಿ"
},
"sendTokens": {
"message": "ಟಕನಗಳನ ಕಳಿಿ"
},

@ -787,9 +787,6 @@
"importWallet": {
"message": "지갑 가져오기"
},
"importYourExisting": {
"message": "12단어 시드 구문을 사용하여 지갑 가져오기"
},
"imported": {
"message": "가져옴",
"description": "status showing that an account has been fully loaded into the keyring"
@ -1343,9 +1340,6 @@
"secretBackupPhraseWarning": {
"message": "경고: 백업 구문은 절대로 공개하지 마세요. 이 구문이 있는 사람은 귀하의 Ether를 영원히 소유할 수 있습니다."
},
"secretPhrase": {
"message": "금고를 복구하려면 비밀 12단어 구문을 여기에 입력하세요."
},
"securityAndPrivacy": {
"message": "보안 및 개인정보 보호"
},
@ -1397,9 +1391,6 @@
"sendAmount": {
"message": "금액 보내기"
},
"sendETH": {
"message": "ETH 보내기"
},
"sendSpecifiedTokens": {
"message": "$1 보내기",
"description": "Symbol of the specified token"

@ -505,9 +505,6 @@
"importWallet": {
"message": "Importuoti slaptažodinę"
},
"importYourExisting": {
"message": "Importuoti turimą piniginę naudojant 12 žodžių atkūrimo frazę"
},
"imported": {
"message": "Importuota",
"description": "status showing that an account has been fully loaded into the keyring"
@ -895,9 +892,6 @@
"secretBackupPhraseWarning": {
"message": "ĮSPĖJIMAS. Niekada neatskleiskite savo atsarginės frazės. Bet kas, žinantis šią frazę, gali visiems laikams pasiimti jūsų eterius."
},
"secretPhrase": {
"message": "Savo saugyklai atkurti įveskite slaptą dvylikos žodžių frazę."
},
"securityAndPrivacy": {
"message": "Sauga ir privatumas"
},
@ -937,9 +931,6 @@
"sendAmount": {
"message": "Siųsti sumą"
},
"sendETH": {
"message": "Siųsti ETH"
},
"sendTokens": {
"message": "Siųsti žetonus"
},

@ -501,9 +501,6 @@
"importWallet": {
"message": "Importēt maku"
},
"importYourExisting": {
"message": "Importējiet esošo maku, izmantojot 12 vārdu atkopšanas frāzi"
},
"imported": {
"message": "Importēts",
"description": "status showing that an account has been fully loaded into the keyring"
@ -891,9 +888,6 @@
"secretBackupPhraseWarning": {
"message": "BRĪDINĀJUMS! Nekādā gadījumā neizpaudiet savu rezerves frāzi. Ikviens, kam pieejama šī frāze, var uz visiem laikiem pārņemt jūsu Ether."
},
"secretPhrase": {
"message": "Ievadiet šeit slepeno divpadsmit vārdu frāzi, lai atjaunotu savu seifu."
},
"securityAndPrivacy": {
"message": "Drošība un konfidencialitāte"
},
@ -933,9 +927,6 @@
"sendAmount": {
"message": "Nosūtītā summa"
},
"sendETH": {
"message": "Sūtīt ETH"
},
"sendTokens": {
"message": "Nosūtīt marķierus"
},

@ -491,9 +491,6 @@
"importWallet": {
"message": "Import Dompet"
},
"importYourExisting": {
"message": "Import dompet sedia ada anda menggunakan frasa benih 12 perkataan"
},
"imported": {
"message": "Diimport",
"description": "status showing that an account has been fully loaded into the keyring"
@ -872,9 +869,6 @@
"secretBackupPhraseWarning": {
"message": "AMARAN: Jangan sesekali dedahkan frasa sandaran anda. Sesiapa yang memperoleh frasa ini boleh mengambil Ether anda selama-lamanya."
},
"secretPhrase": {
"message": "Masukkan ungkapan rahsia dua belas perkataan di sini untuk memulihkan kekubah anda."
},
"securityAndPrivacy": {
"message": "Keselamatan & Privasi"
},
@ -914,9 +908,6 @@
"sendAmount": {
"message": "Hantar Amaun"
},
"sendETH": {
"message": "Hantar ETH"
},
"sendTokens": {
"message": "Hantar Token"
},

@ -311,9 +311,6 @@
"search": {
"message": "Zoeken"
},
"secretPhrase": {
"message": "Voer hier je geheime twaalfwoordfrase in om je kluis te herstellen."
},
"seedPhraseReq": {
"message": "Back-up woorden zijn 12 woorden lang"
},
@ -323,9 +320,6 @@
"send": {
"message": "Sturen"
},
"sendETH": {
"message": "Verzend ETH"
},
"sendTokens": {
"message": "Stuur tokens"
},

@ -492,9 +492,6 @@
"importWallet": {
"message": "Importér lommebok "
},
"importYourExisting": {
"message": "Importer din eksisterende lommebok ved å bruk en tolvords seed-frase."
},
"imported": {
"message": "Importert",
"description": "status showing that an account has been fully loaded into the keyring"
@ -879,9 +876,6 @@
"secretBackupPhraseWarning": {
"message": "ADVARSEL: Du må aldri avsløre gjenopprettingsfrasen din. Alle som har denne frasen kan ta fra deg Etheren din for alltid."
},
"secretPhrase": {
"message": "Skriv inn den tolv ord lange frasen her for å gjenopprette hvelvet ditt. "
},
"securityAndPrivacy": {
"message": "Sikkerhet og personvern"
},

@ -505,9 +505,6 @@
"importWallet": {
"message": "Importuj portfel"
},
"importYourExisting": {
"message": "Zaimportuj istniejący portfel, wprowadzając 12 słów frazy seed"
},
"imported": {
"message": "Zaimportowane",
"description": "status showing that an account has been fully loaded into the keyring"
@ -889,9 +886,6 @@
"secretBackupPhraseWarning": {
"message": "OSTRZEŻENIE: Nigdy nie ujawniaj swojej frazy zapasowej. Każdy, kto pozna tę frazę, może bezpowrotnie odebrać Ci kryptowalutę Ether."
},
"secretPhrase": {
"message": "Żeby otworzyć schowek, wpisz tutaj swoją frazę dwunastu słów."
},
"securityAndPrivacy": {
"message": "Bezpieczeństwo i prywatność"
},
@ -931,9 +925,6 @@
"sendAmount": {
"message": "Wyślij kwotę"
},
"sendETH": {
"message": "Wyślij ETH"
},
"sendTokens": {
"message": "Wyślij tokeny"
},

@ -321,9 +321,6 @@
"search": {
"message": "Procurar"
},
"secretPhrase": {
"message": "Introduza a sua frase secreta de 12 palavras para recuperar o seu ."
},
"seedPhraseReq": {
"message": "seed phrases are 12 words long"
},
@ -333,9 +330,6 @@
"send": {
"message": "Enviar"
},
"sendETH": {
"message": "Enviar ETH"
},
"sendTokens": {
"message": "Enviar Tokens"
},

@ -499,9 +499,6 @@
"importWallet": {
"message": "Importar Carteira"
},
"importYourExisting": {
"message": "Importe sua carteira existente usando uma frase-semente de 12 palavras"
},
"imported": {
"message": "Importado",
"description": "status showing that an account has been fully loaded into the keyring"
@ -883,9 +880,6 @@
"secretBackupPhraseWarning": {
"message": "ATENÇÃO: Nunca revele sua frase de backup a ninguém. Qualquer pessoa com essa frase pode obter seu Ether para sempre."
},
"secretPhrase": {
"message": "Digite sua frase secreta de doze palavras aqui para restaurar seu cofre."
},
"securityAndPrivacy": {
"message": "Segurança & Privacidade"
},
@ -925,9 +919,6 @@
"sendAmount": {
"message": "Enviar Quantia"
},
"sendETH": {
"message": "Enviar ETH"
},
"sendTokens": {
"message": "Enviar Tokens"
},

@ -495,9 +495,6 @@
"importWallet": {
"message": "Importați portofel"
},
"importYourExisting": {
"message": "Importați portofelul existent folosind o frază seed de 12 cuvinte"
},
"imported": {
"message": "Importate",
"description": "status showing that an account has been fully loaded into the keyring"
@ -882,9 +879,6 @@
"secretBackupPhraseWarning": {
"message": "ATENȚIE: Nu dezvăluiți niciodată expresia dvs. de rezervă. Oricine are această expresie vă poate lua Ether-ul pentru totdeauna."
},
"secretPhrase": {
"message": "Introduceți aici expresia dvs. secretă din 12 cuvinte pentru a restabili seiful."
},
"securityAndPrivacy": {
"message": "Securitate și confidențialitate"
},
@ -924,9 +918,6 @@
"sendAmount": {
"message": "Suma trimisă"
},
"sendETH": {
"message": "Trimitere ETH"
},
"sendTokens": {
"message": "Trimiteți indicative"
},

@ -787,9 +787,6 @@
"importWallet": {
"message": "Импортировать кошелек"
},
"importYourExisting": {
"message": "Импортируйте существующий кошелек, используя начальную фразу из 12 слов"
},
"imported": {
"message": "Импортированный",
"description": "status showing that an account has been fully loaded into the keyring"
@ -1346,9 +1343,6 @@
"secretBackupPhraseWarning": {
"message": "ПРЕДУПРЕЖДЕНИЕ: Никогда не разглашайте резервную фразу. Любой, у кого есть эта фраза, может забрать ваш Ether навсегда."
},
"secretPhrase": {
"message": "Введите здесь свою секретную фразу из двенадцати слов, чтобы восстановить свой сейф."
},
"securityAndPrivacy": {
"message": "Безопасность и конфиденциальность"
},
@ -1400,9 +1394,6 @@
"sendAmount": {
"message": "Отправить сумму"
},
"sendETH": {
"message": "Отправить ETH"
},
"sendSpecifiedTokens": {
"message": "Отправить $1",
"description": "Symbol of the specified token"

@ -492,9 +492,6 @@
"importWallet": {
"message": "Importovať Peňaženku"
},
"importYourExisting": {
"message": "Importujte svoju existujúcu peňaženku pomocou 12-slovnej seed frázy"
},
"imported": {
"message": "Importováno",
"description": "status showing that an account has been fully loaded into the keyring"
@ -858,9 +855,6 @@
"secretBackupPhraseWarning": {
"message": "UPOZORNENIE: Nikdy nezverejňujte svoju backup frázu. Každý, kto má túto frázu, môže navždy vziať váš Ether."
},
"secretPhrase": {
"message": "Zadejte svých 12 slov tajné fráze k obnovení trezoru."
},
"securityAndPrivacy": {
"message": "Bezpečnosť a súkromie"
},
@ -900,9 +894,6 @@
"sendAmount": {
"message": "Poslať sumu"
},
"sendETH": {
"message": "Odeslat ETH"
},
"sendTokens": {
"message": "Odeslat tokeny"
},

@ -496,9 +496,6 @@
"importWallet": {
"message": "Uvozi denarnico"
},
"importYourExisting": {
"message": "Uvozite svojo obstoječo denarnico s pomočjo 12-besednega gesla seed phrase"
},
"imported": {
"message": "Uvoženo",
"description": "status showing that an account has been fully loaded into the keyring"
@ -877,9 +874,6 @@
"secretBackupPhraseWarning": {
"message": "OPOZORILO: Nikoli nikomur ne razkrijte varnostne kopije. Kdorkoli lahko tem geslom vedno prevzame vaš Ether."
},
"secretPhrase": {
"message": "Vnesite vaših dvanajst besed za obnovitev vaših računov."
},
"securityAndPrivacy": {
"message": "Varnost in zasebnost"
},
@ -919,9 +913,6 @@
"sendAmount": {
"message": "Pošlji znesek"
},
"sendETH": {
"message": "Pošlji ETH"
},
"sendTokens": {
"message": "Pošlji žetone"
},

@ -502,9 +502,6 @@
"importWallet": {
"message": "Uvezite novčanik"
},
"importYourExisting": {
"message": "Uvezite vaš postojeći novčanik koristeći seed frazu sa 12 reči"
},
"imported": {
"message": "Увезени",
"description": "status showing that an account has been fully loaded into the keyring"
@ -886,9 +883,6 @@
"secretBackupPhraseWarning": {
"message": "UPOZORENJE: Nikada ne otkrivajte svoju rezervnu frazu. Svako sa ovom frazom može zauvek da Vam uzme Vaš Ether."
},
"secretPhrase": {
"message": "Unesite ovde svoj tajni izraz od dvanaest reči kako biste povratili svoj trezor."
},
"securityAndPrivacy": {
"message": "Bezbednost i privatnost"
},
@ -928,9 +922,6 @@
"sendAmount": {
"message": "Pošaljite iznos"
},
"sendETH": {
"message": "Pošalji ETH"
},
"sendTokens": {
"message": "Pošalji tokene"
},

@ -495,9 +495,6 @@
"importWallet": {
"message": "Importera plånbok"
},
"importYourExisting": {
"message": "Importera din existerande plånbok med hjälp av en 12 ord lång seedfras"
},
"imported": {
"message": "Importerade",
"description": "status showing that an account has been fully loaded into the keyring"
@ -879,9 +876,6 @@
"secretBackupPhraseWarning": {
"message": "VARNING: avslöja aldrig din backup-fras. Någon som känner till denna fras kan ta dina Ether för alltid."
},
"secretPhrase": {
"message": "Ange din tolv ord långa hemliga fras här för att återställa ditt valv."
},
"securityAndPrivacy": {
"message": "Säkerhet och integritet"
},
@ -921,9 +915,6 @@
"sendAmount": {
"message": "Skicka belopp"
},
"sendETH": {
"message": "Skicka ETH"
},
"sendTokens": {
"message": "Skicka tokens"
},

@ -492,9 +492,6 @@
"importWallet": {
"message": "Hamisha Waleti"
},
"importYourExisting": {
"message": "Hamisha waleti iliyopo kwa kutumia kirai kianzio cha maneno 12"
},
"imported": {
"message": "Zilizoingizwa",
"description": "status showing that an account has been fully loaded into the keyring"
@ -873,9 +870,6 @@
"secretBackupPhraseWarning": {
"message": "ONYO: Kamwe usiweke wazi kirai chako cha hifadhi mbadala. Mtu yeyote mwenye kirai hiki anaweza kuchukua Ether yako daima."
},
"secretPhrase": {
"message": "Ingiza hapa kirai chako cha siri cha maneno kumi na mawili ili urejeshe vault yako."
},
"securityAndPrivacy": {
"message": "Ulinzi na Faragha"
},
@ -915,9 +909,6 @@
"sendAmount": {
"message": "Tuma Kiasi"
},
"sendETH": {
"message": "Tuma ETH"
},
"sendTokens": {
"message": "Tuma Vianzio"
},

@ -426,9 +426,6 @@
"searchTokens": {
"message": "தடலகன"
},
"secretPhrase": {
"message": "உஙகளடகதபதறக இங உஙகள ரகசிய பனிரணடர உளிடவ."
},
"seedPhraseReq": {
"message": "விியஙகள 12 வகளடவ"
},
@ -438,9 +435,6 @@
"send": {
"message": "அன"
},
"sendETH": {
"message": "ETH ஐ அன"
},
"sendTokens": {
"message": "டகனகள அனபவ"
},

@ -420,9 +420,6 @@
"search": {
"message": "คนหา"
},
"secretPhrase": {
"message": "ปอนกลมคำสบสองคำเพอกนตเซฟของคณ"
},
"seedPhraseReq": {
"message": "กลมคำชดมความยาว 12 คำ"
},
@ -441,9 +438,6 @@
"sendAmount": {
"message": "สงจำนวนเงนน"
},
"sendETH": {
"message": "สงอเธอร"
},
"sendTokens": {
"message": "สงโทเคน"
},

@ -787,9 +787,6 @@
"importWallet": {
"message": "I-import ang wallet"
},
"importYourExisting": {
"message": "I-import ang iyong kasalukuyang wallet gamit ang 12 salita na seed phrase"
},
"imported": {
"message": "Na-import",
"description": "status showing that an account has been fully loaded into the keyring"
@ -1343,9 +1340,6 @@
"secretBackupPhraseWarning": {
"message": "BABALA: Huwag kailanman ipaalam ang iyong phrase sa pag-back up. Ang sinumang may phrase na ito ay maaaring angkinin ang iyong Ether."
},
"secretPhrase": {
"message": "Ilagay ang iyong labindalawang lihim na phrase dito para ma-restore ang iyong vault."
},
"securityAndPrivacy": {
"message": "Seguridad at Pagkapribado"
},
@ -1397,9 +1391,6 @@
"sendAmount": {
"message": "Halaga ng Ipapadala"
},
"sendETH": {
"message": "Magpadala ng ETH"
},
"sendSpecifiedTokens": {
"message": "Magpadala ng $1",
"description": "Symbol of the specified token"

@ -366,9 +366,6 @@
"searchTokens": {
"message": "Jeton ara"
},
"secretPhrase": {
"message": "Kasanızı geri getirmek için gizli 12 kelimelik ifadenizi giriniz."
},
"seedPhraseReq": {
"message": "Kaynak ifadeleri 12 kelimedir."
},
@ -378,9 +375,6 @@
"send": {
"message": "Gönder"
},
"sendETH": {
"message": "ETH Gönder"
},
"sendTokens": {
"message": "Jeton Gönder"
},

@ -505,9 +505,6 @@
"importWallet": {
"message": "Імпортувати гаманець"
},
"importYourExisting": {
"message": "Імпортуйте ваш гаманець, що існує, використовуючи початкову фразу з 12 слів"
},
"imported": {
"message": "Імпортовано",
"description": "status showing that an account has been fully loaded into the keyring"
@ -895,9 +892,6 @@
"secretBackupPhraseWarning": {
"message": "ЗАСТЕРЕЖЕННЯ: Ніколи не розголошуйте вашу резервну фразу. Будь-хто з цією фразою зможе забрати ваш Ether назавжди."
},
"secretPhrase": {
"message": "Введіть секретну фразу з дванадцяти слів, щоб відновити своє сховище."
},
"securityAndPrivacy": {
"message": "Безпека й конфіденційність"
},
@ -937,9 +931,6 @@
"sendAmount": {
"message": "Надіслати суму"
},
"sendETH": {
"message": "Надіслати ETH"
},
"sendTokens": {
"message": "Надіслати токени"
},

@ -787,9 +787,6 @@
"importWallet": {
"message": "Nhập ví"
},
"importYourExisting": {
"message": "Nhập ví hiện có của bạn bằng cụm mật khẩu gốc gồm 12 từ"
},
"imported": {
"message": "Đã nhập",
"description": "status showing that an account has been fully loaded into the keyring"
@ -1346,9 +1343,6 @@
"secretBackupPhraseWarning": {
"message": "CẢNH BÁO: Tuyệt đối không để lộ cụm mật khẩu sao lưu của bạn. Bất kỳ ai có cụm mật khẩu này cũng có thể lấy Ether của bạn vĩnh viễn."
},
"secretPhrase": {
"message": "Nhập cụm mật khẩu bí mật gồm 12 từ vào đây để khôi phục két của bạn."
},
"securityAndPrivacy": {
"message": "Bảo mật và quyền riêng tư"
},
@ -1400,9 +1394,6 @@
"sendAmount": {
"message": "Gửi khoản tiền"
},
"sendETH": {
"message": "Gửi ETH"
},
"sendSpecifiedTokens": {
"message": "Gửi $1",
"description": "Symbol of the specified token"

@ -796,9 +796,6 @@
"importWallet": {
"message": "导入钱包"
},
"importYourExisting": {
"message": "使用 12 个单词的账户助记词导入您现有的钱包账户。"
},
"imported": {
"message": "已导入",
"description": "status showing that an account has been fully loaded into the keyring"
@ -1355,9 +1352,6 @@
"secretBackupPhraseWarning": {
"message": "警告:切勿向他人透露您的账户助记词。任何人一旦持有该账户助记词,即可控制您的 Ether。"
},
"secretPhrase": {
"message": "输入 12 个单词组成的账户助记词恢复您的账户。"
},
"securityAndPrivacy": {
"message": "安全与隐私"
},
@ -1409,9 +1403,6 @@
"sendAmount": {
"message": "发送数额"
},
"sendETH": {
"message": "发送 ETH"
},
"sendSpecifiedTokens": {
"message": "发送 $1",
"description": "Symbol of the specified token"

@ -514,9 +514,6 @@
"importWallet": {
"message": "匯入錢包"
},
"importYourExisting": {
"message": "使用 12 字的助記詞來匯入你現有的錢包"
},
"imported": {
"message": "已匯入私鑰",
"description": "status showing that an account has been fully loaded into the keyring"
@ -889,9 +886,6 @@
"secretBackupPhraseWarning": {
"message": "警告: 絕對不要洩漏您的助憶詞。任何人只要得知助憶詞代表他可以竊取您所有的以太幣和代幣。"
},
"secretPhrase": {
"message": "輸入您的12個助憶詞以回復金庫"
},
"securityAndPrivacy": {
"message": "安全&隱私"
},
@ -925,9 +919,6 @@
"sendAmount": {
"message": "發送數量"
},
"sendETH": {
"message": "發送以太幣"
},
"sendTokens": {
"message": "發送代幣"
},

@ -0,0 +1,15 @@
<!doctype html>
<html>
<head>
<meta charset="utf-8">
</head>
<body>
<script src="./globalthis.js" type="text/javascript" charset="utf-8"></script>
<script src="./initSentry.js" type="text/javascript" charset="utf-8"></script>
<script src="./lockdown.js" type="text/javascript" charset="utf-8"></script>
<script src="./runLockdown.js" type="text/javascript" charset="utf-8"></script>
<script src="./bg-libs.js" type="text/javascript" charset="utf-8"></script>
<script src="./background.js" type="text/javascript" charset="utf-8"></script>
<script src="./chromereload.js" type="text/javascript" charset="utf-8"></script>
</body>
</html>

@ -1,14 +1,7 @@
{
"author": "https://metamask.io",
"background": {
"scripts": [
"globalthis.js",
"initSentry.js",
"lockdown.js",
"runLockdown.js",
"bg-libs.js",
"background.js"
],
"page": "background.html",
"persistent": true
},
"browser_action": {

@ -1,6 +1,6 @@
import assert from 'assert';
import ethUtil from 'ethereumjs-util';
import accountImporter from '../../../app/scripts/account-import-strategies';
import accountImporter from '.';
describe('Account Import Strategies', function () {
const privkey =

@ -1,12 +1,6 @@
/**
* @file The entry point for the web extension singleton process.
*/
// these need to run before anything else
/* eslint-disable import/first,import/order */
import setupFetchDebugging from './lib/setupFetchDebugging';
/* eslint-enable import/order */
setupFetchDebugging();
// polyfills
import 'abortcontroller-polyfill/dist/polyfill-patch-fetch';
@ -70,21 +64,7 @@ if (inTest || process.env.METAMASK_DEBUG) {
initialize().catch(log.error);
/**
* An object representing a transaction, in whatever state it is in.
* @typedef TransactionMeta
*
* @property {number} id - An internally unique tx identifier.
* @property {number} time - Time the tx was first suggested, in unix epoch time (ms).
* @property {string} status - The current transaction status (unapproved, signed, submitted, dropped, failed, rejected), as defined in `tx-state-manager.js`.
* @property {string} metamaskNetworkId - The transaction's network ID, used for EIP-155 compliance.
* @property {boolean} loadingDefaults - TODO: Document
* @property {Object} txParams - The tx params as passed to the network provider.
* @property {Object[]} history - A history of mutations to this TransactionMeta object.
* @property {string} origin - A string representing the interface that suggested the transaction.
* @property {Object} nonceDetails - A metadata object containing information used to derive the suggested nonce, useful for debugging nonce issues.
* @property {string} rawTx - A hex string of the final signed transaction, ready to submit to the network.
* @property {string} hash - A hex string of the transaction hash, used to identify the transaction on the network.
* @property {number} submittedTime - The time the transaction was submitted to the network, in Unix epoch time (ms).
* @typedef {import('../../shared/constants/transaction').TransactionMeta} TransactionMeta
*/
/**

@ -1,7 +1,7 @@
import assert from 'assert';
import sinon from 'sinon';
import CachedBalancesController from '../../../../app/scripts/controllers/cached-balances';
import { KOVAN_CHAIN_ID } from '../../../../shared/constants/network';
import { KOVAN_CHAIN_ID } from '../../../shared/constants/network';
import CachedBalancesController from './cached-balances';
describe('CachedBalancesController', function () {
describe('updateCachedBalances', function () {

@ -4,10 +4,10 @@ import { ObservableStore } from '@metamask/obs-store';
import contracts from '@metamask/contract-metadata';
import BigNumber from 'bignumber.js';
import DetectTokensController from '../../../../app/scripts/controllers/detect-tokens';
import NetworkController from '../../../../app/scripts/controllers/network/network';
import PreferencesController from '../../../../app/scripts/controllers/preferences';
import { MAINNET, ROPSTEN } from '../../../../shared/constants/network';
import { MAINNET, ROPSTEN } from '../../../shared/constants/network';
import DetectTokensController from './detect-tokens';
import NetworkController from './network';
import PreferencesController from './preferences';
describe('DetectTokensController', function () {
const sandbox = sinon.createSandbox();

@ -1,6 +1,6 @@
import assert from 'assert';
import sinon from 'sinon';
import EnsController from '../../../../app/scripts/controllers/ens';
import EnsController from '.';
const ZERO_ADDRESS = '0x0000000000000000000000000000000000000000';
const ZERO_X_ERROR_ADDRESS = '0x';

@ -1,32 +1,48 @@
import { ObservableStore } from '@metamask/obs-store';
import log from 'loglevel';
import BN from 'bn.js';
import createId from '../lib/random-id';
import createId from '../../../shared/modules/random-id';
import { bnToHex } from '../lib/util';
import getFetchWithTimeout from '../../../shared/modules/fetch-with-timeout';
import {
TRANSACTION_CATEGORIES,
TRANSACTION_TYPES,
TRANSACTION_STATUSES,
} from '../../../shared/constants/transaction';
import {
CHAIN_ID_TO_NETWORK_ID_MAP,
CHAIN_ID_TO_TYPE_MAP,
GOERLI,
GOERLI_CHAIN_ID,
KOVAN,
KOVAN_CHAIN_ID,
MAINNET,
MAINNET_CHAIN_ID,
RINKEBY,
RINKEBY_CHAIN_ID,
ROPSTEN,
ROPSTEN_CHAIN_ID,
} from '../../../shared/constants/network';
import { NETWORK_EVENTS } from './network';
const fetchWithTimeout = getFetchWithTimeout(30000);
/**
* @typedef {import('../../../shared/constants/transaction').TransactionMeta} TransactionMeta
*/
/**
* A transaction object in the format returned by the Etherscan API.
*
* Note that this is not an exhaustive type definiton; only the properties we use are defined
*
* @typedef {Object} EtherscanTransaction
* @property {string} blockNumber - The number of the block this transaction was found in, in decimal
* @property {string} from - The hex-prefixed address of the sender
* @property {string} gas - The gas limit, in decimal WEI
* @property {string} gasPrice - The gas price, in decimal WEI
* @property {string} hash - The hex-prefixed transaction hash
* @property {string} isError - Whether the transaction was confirmed or failed (0 for confirmed, 1 for failed)
* @property {string} nonce - The transaction nonce, in decimal
* @property {string} timeStamp - The timestamp for the transaction, in seconds
* @property {string} to - The hex-prefixed address of the recipient
* @property {string} value - The amount of ETH sent in this transaction, in decimal WEI
*/
/**
* 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
@ -44,35 +60,37 @@ const etherscanSupportedNetworks = [
export default class IncomingTransactionsController {
constructor(opts = {}) {
const { blockTracker, networkController, preferencesController } = opts;
const {
blockTracker,
onNetworkDidChange,
getCurrentChainId,
preferencesController,
} = opts;
this.blockTracker = blockTracker;
this.networkController = networkController;
this.getCurrentChainId = getCurrentChainId;
this.preferencesController = preferencesController;
this._onLatestBlock = async (newBlockNumberHex) => {
const selectedAddress = this.preferencesController.getSelectedAddress();
const newBlockNumberDec = parseInt(newBlockNumberHex, 16);
await this._update({
address: selectedAddress,
newBlockNumberDec,
});
await this._update(selectedAddress, newBlockNumberDec);
};
const initState = {
incomingTransactions: {},
incomingTxLastFetchedBlocksByNetwork: {
[GOERLI]: null,
[KOVAN]: null,
[MAINNET]: null,
[RINKEBY]: null,
[ROPSTEN]: null,
incomingTxLastFetchedBlockByChainId: {
[GOERLI_CHAIN_ID]: null,
[KOVAN_CHAIN_ID]: null,
[MAINNET_CHAIN_ID]: null,
[RINKEBY_CHAIN_ID]: null,
[ROPSTEN_CHAIN_ID]: null,
},
...opts.initState,
};
this.store = new ObservableStore(initState);
this.preferencesController.store.subscribe(
pairwise((prevState, currState) => {
previousValueComparator((prevState, currState) => {
const {
featureFlags: {
showIncomingTransactions: prevShowIncomingTransactions,
@ -94,29 +112,24 @@ export default class IncomingTransactionsController {
}
this.start();
}),
}, this.preferencesController.store.getState()),
);
this.preferencesController.store.subscribe(
pairwise(async (prevState, currState) => {
previousValueComparator(async (prevState, currState) => {
const { selectedAddress: prevSelectedAddress } = prevState;
const { selectedAddress: currSelectedAddress } = currState;
if (currSelectedAddress === prevSelectedAddress) {
return;
}
await this._update({
address: currSelectedAddress,
});
}),
await this._update(currSelectedAddress);
}, this.preferencesController.store.getState()),
);
this.networkController.on(NETWORK_EVENTS.NETWORK_DID_CHANGE, async () => {
onNetworkDidChange(async () => {
const address = this.preferencesController.getSelectedAddress();
await this._update({
address,
});
await this._update(address);
});
}
@ -136,85 +149,79 @@ export default class IncomingTransactionsController {
this.blockTracker.removeListener('latest', this._onLatestBlock);
}
async _update({ address, newBlockNumberDec } = {}) {
const chainId = this.networkController.getCurrentChainId();
if (!etherscanSupportedNetworks.includes(chainId)) {
/**
* Determines the correct block number to begin looking for new transactions
* from, fetches the transactions and then saves them and the next block
* number to begin fetching from in state. Block numbers and transactions are
* stored per chainId.
* @private
* @param {string} address - address to lookup transactions for
* @param {number} [newBlockNumberDec] - block number to begin fetching from
* @returns {void}
*/
async _update(address, newBlockNumberDec) {
const chainId = this.getCurrentChainId();
if (!etherscanSupportedNetworks.includes(chainId) || !address) {
return;
}
try {
const dataForUpdate = await this._getDataForUpdate({
const currentState = this.store.getState();
const currentBlock = parseInt(this.blockTracker.getCurrentBlock(), 16);
const mostRecentlyFetchedBlock =
currentState.incomingTxLastFetchedBlockByChainId[chainId];
const blockToFetchFrom =
mostRecentlyFetchedBlock ?? newBlockNumberDec ?? currentBlock;
const newIncomingTxs = await this._getNewIncomingTransactions(
address,
blockToFetchFrom,
chainId,
newBlockNumberDec,
);
let newMostRecentlyFetchedBlock = blockToFetchFrom;
newIncomingTxs.forEach((tx) => {
if (
tx.blockNumber &&
parseInt(newMostRecentlyFetchedBlock, 10) <
parseInt(tx.blockNumber, 10)
) {
newMostRecentlyFetchedBlock = parseInt(tx.blockNumber, 10);
}
});
this.store.updateState({
incomingTxLastFetchedBlockByChainId: {
...currentState.incomingTxLastFetchedBlockByChainId,
[chainId]: newMostRecentlyFetchedBlock + 1,
},
incomingTransactions: newIncomingTxs.reduce(
(transactions, tx) => {
transactions[tx.hash] = tx;
return transactions;
},
{
...currentState.incomingTransactions,
},
),
});
this._updateStateWithNewTxData(dataForUpdate);
} catch (err) {
log.error(err);
}
}
async _getDataForUpdate({ address, chainId, newBlockNumberDec } = {}) {
const {
incomingTransactions: currentIncomingTxs,
incomingTxLastFetchedBlocksByNetwork: currentBlocksByNetwork,
} = this.store.getState();
const lastFetchBlockByCurrentNetwork =
currentBlocksByNetwork[CHAIN_ID_TO_TYPE_MAP[chainId]];
let blockToFetchFrom = lastFetchBlockByCurrentNetwork || newBlockNumberDec;
if (blockToFetchFrom === undefined) {
blockToFetchFrom = parseInt(this.blockTracker.getCurrentBlock(), 16);
}
const { latestIncomingTxBlockNumber, txs: newTxs } = await this._fetchAll(
address,
blockToFetchFrom,
chainId,
);
return {
latestIncomingTxBlockNumber,
newTxs,
currentIncomingTxs,
currentBlocksByNetwork,
fetchedBlockNumber: blockToFetchFrom,
chainId,
};
}
_updateStateWithNewTxData({
latestIncomingTxBlockNumber,
newTxs,
currentIncomingTxs,
currentBlocksByNetwork,
fetchedBlockNumber,
chainId,
}) {
const newLatestBlockHashByNetwork = latestIncomingTxBlockNumber
? parseInt(latestIncomingTxBlockNumber, 10) + 1
: fetchedBlockNumber + 1;
const newIncomingTransactions = {
...currentIncomingTxs,
};
newTxs.forEach((tx) => {
newIncomingTransactions[tx.hash] = tx;
});
this.store.updateState({
incomingTxLastFetchedBlocksByNetwork: {
...currentBlocksByNetwork,
[CHAIN_ID_TO_TYPE_MAP[chainId]]: newLatestBlockHashByNetwork,
},
incomingTransactions: newIncomingTransactions,
});
}
async _fetchAll(address, fromBlock, chainId) {
const fetchedTxResponse = await this._fetchTxs(address, fromBlock, chainId);
return this._processTxFetchResponse(fetchedTxResponse);
}
async _fetchTxs(address, fromBlock, chainId) {
/**
* fetches transactions for the given address and chain, via etherscan, then
* processes the data into the necessary shape for usage in this controller.
*
* @private
* @param {string} [address] - Address to fetch transactions for
* @param {number} [fromBlock] - Block to look for transactions at
* @param {string} [chainId] - The chainId for the current network
* @returns {TransactionMeta[]}
*/
async _getNewIncomingTransactions(address, fromBlock, chainId) {
const etherscanSubdomain =
chainId === MAINNET_CHAIN_ID
? 'api'
@ -227,16 +234,8 @@ export default class IncomingTransactionsController {
url += `&startBlock=${parseInt(fromBlock, 10)}`;
}
const response = await fetchWithTimeout(url);
const parsedResponse = await response.json();
return {
...parsedResponse,
address,
chainId,
};
}
_processTxFetchResponse({ status, result = [], address, chainId }) {
const { status, result } = await response.json();
let newIncomingTxs = [];
if (status === '1' && Array.isArray(result) && result.length > 0) {
const remoteTxList = {};
const remoteTxs = [];
@ -247,70 +246,70 @@ export default class IncomingTransactionsController {
}
});
const incomingTxs = remoteTxs.filter(
newIncomingTxs = remoteTxs.filter(
(tx) => tx.txParams?.to?.toLowerCase() === address.toLowerCase(),
);
incomingTxs.sort((a, b) => (a.time < b.time ? -1 : 1));
let latestIncomingTxBlockNumber = null;
incomingTxs.forEach((tx) => {
if (
tx.blockNumber &&
(!latestIncomingTxBlockNumber ||
parseInt(latestIncomingTxBlockNumber, 10) <
parseInt(tx.blockNumber, 10))
) {
latestIncomingTxBlockNumber = tx.blockNumber;
}
});
return {
latestIncomingTxBlockNumber,
txs: incomingTxs,
};
newIncomingTxs.sort((a, b) => (a.time < b.time ? -1 : 1));
}
return {
latestIncomingTxBlockNumber: null,
txs: [],
};
return newIncomingTxs;
}
_normalizeTxFromEtherscan(txMeta, chainId) {
const time = parseInt(txMeta.timeStamp, 10) * 1000;
/**
* Transmutes a EtherscanTransaction into a TransactionMeta
* @param {EtherscanTransaction} etherscanTransaction - the transaction to normalize
* @param {string} chainId - The chainId of the current network
* @returns {TransactionMeta}
*/
_normalizeTxFromEtherscan(etherscanTransaction, chainId) {
const time = parseInt(etherscanTransaction.timeStamp, 10) * 1000;
const status =
txMeta.isError === '0'
etherscanTransaction.isError === '0'
? TRANSACTION_STATUSES.CONFIRMED
: TRANSACTION_STATUSES.FAILED;
return {
blockNumber: txMeta.blockNumber,
blockNumber: etherscanTransaction.blockNumber,
id: createId(),
chainId,
metamaskNetworkId: CHAIN_ID_TO_NETWORK_ID_MAP[chainId],
status,
time,
txParams: {
from: txMeta.from,
gas: bnToHex(new BN(txMeta.gas)),
gasPrice: bnToHex(new BN(txMeta.gasPrice)),
nonce: bnToHex(new BN(txMeta.nonce)),
to: txMeta.to,
value: bnToHex(new BN(txMeta.value)),
from: etherscanTransaction.from,
gas: bnToHex(new BN(etherscanTransaction.gas)),
gasPrice: bnToHex(new BN(etherscanTransaction.gasPrice)),
nonce: bnToHex(new BN(etherscanTransaction.nonce)),
to: etherscanTransaction.to,
value: bnToHex(new BN(etherscanTransaction.value)),
},
hash: txMeta.hash,
transactionCategory: TRANSACTION_CATEGORIES.INCOMING,
hash: etherscanTransaction.hash,
type: TRANSACTION_TYPES.INCOMING,
};
}
}
function pairwise(fn) {
/**
* Returns a function with arity 1 that caches the argument that the function
* is called with and invokes the comparator with both the cached, previous,
* value and the current value. If specified, the initialValue will be passed
* in as the previous value on the first invocation of the returned method.
* @template A
* @params {A=} type of compared value
* @param {(prevValue: A, nextValue: A) => void} comparator - method to compare
* previous and next values.
* @param {A} [initialValue] - initial value to supply to prevValue
* on first call of the method.
* @returns {void}
*/
function previousValueComparator(comparator, initialValue) {
let first = true;
let cache;
return (value) => {
try {
if (first) {
first = false;
return fn(value, value);
return comparator(initialValue ?? value, value);
}
return fn(cache, value);
return comparator(cache, value);
} finally {
cache = value;
}

@ -1,14 +1,14 @@
import { strict as assert } from 'assert';
import sinon from 'sinon';
import MetaMetricsController from '../../../../app/scripts/controllers/metametrics';
import { ENVIRONMENT_TYPE_BACKGROUND } from '../../../../shared/constants/app';
import { createSegmentMock } from '../../../../app/scripts/lib/segment';
import { ENVIRONMENT_TYPE_BACKGROUND } from '../../../shared/constants/app';
import { createSegmentMock } from '../lib/segment';
import {
METAMETRICS_ANONYMOUS_ID,
METAMETRICS_BACKGROUND_PAGE_OBJECT,
} from '../../../../shared/constants/metametrics';
import waitUntilCalled from '../../../lib/wait-until-called';
import { NETWORK_EVENTS } from '../../../../app/scripts/controllers/network';
} from '../../../shared/constants/metametrics';
import waitUntilCalled from '../../../test/lib/wait-until-called';
import MetaMetricsController from './metametrics';
import { NETWORK_EVENTS } from './network';
const segment = createSegmentMock(2, 10000);
const segmentLegacy = createSegmentMock(2, 10000);

@ -6,7 +6,7 @@ import createInflightMiddleware from 'eth-json-rpc-middleware/inflight-cache';
import createBlockTrackerInspectorMiddleware from 'eth-json-rpc-middleware/block-tracker-inspector';
import providerFromMiddleware from 'eth-json-rpc-middleware/providerFromMiddleware';
import createInfuraMiddleware from 'eth-json-rpc-infura';
import BlockTracker from 'eth-block-tracker';
import { PollingBlockTracker } from 'eth-block-tracker';
import { NETWORK_TYPE_TO_ID_MAP } from '../../../../shared/constants/network';
@ -18,7 +18,7 @@ export default function createInfuraClient({ network, projectId }) {
source: 'metamask',
});
const infuraProvider = providerFromMiddleware(infuraMiddleware);
const blockTracker = new BlockTracker({ provider: infuraProvider });
const blockTracker = new PollingBlockTracker({ provider: infuraProvider });
const networkMiddleware = mergeMiddleware([
createNetworkAndChainIdMiddleware({ network }),

@ -5,7 +5,7 @@ import createBlockCacheMiddleware from 'eth-json-rpc-middleware/block-cache';
import createInflightMiddleware from 'eth-json-rpc-middleware/inflight-cache';
import createBlockTrackerInspectorMiddleware from 'eth-json-rpc-middleware/block-tracker-inspector';
import providerFromMiddleware from 'eth-json-rpc-middleware/providerFromMiddleware';
import BlockTracker from 'eth-block-tracker';
import { PollingBlockTracker } from 'eth-block-tracker';
const inTest = process.env.IN_TEST === 'true';
const blockTrackerOpts = inTest ? { pollingInterval: 1000 } : {};
@ -16,7 +16,7 @@ const getTestMiddlewares = () => {
export default function createJsonRpcClient({ rpcUrl, chainId }) {
const fetchMiddleware = createFetchMiddleware({ rpcUrl });
const blockProvider = providerFromMiddleware(fetchMiddleware);
const blockTracker = new BlockTracker({
const blockTracker = new PollingBlockTracker({
...blockTrackerOpts,
provider: blockProvider,
});

@ -1,7 +1,7 @@
import { strict as assert } from 'assert';
import sinon from 'sinon';
import NetworkController from '../../../../../app/scripts/controllers/network';
import { getNetworkDisplayName } from '../../../../../app/scripts/controllers/network/util';
import { getNetworkDisplayName } from './util';
import NetworkController from './network';
describe('NetworkController', function () {
describe('controller', function () {

@ -1,9 +1,9 @@
import assert from 'assert';
import { txMetaStub } from '../../../../test/stub/tx-meta-stub';
import {
createPendingNonceMiddleware,
createPendingTxMiddleware,
} from '../../../../../app/scripts/controllers/network/middleware/pending';
import { txMetaStub } from './stubs';
} from './middleware/pending';
describe('PendingNonceMiddleware', function () {
describe('#createPendingNonceMiddleware', function () {

@ -297,7 +297,7 @@ export class PermissionsController {
this.validatePermittedAccounts([account]);
const oldPermittedAccounts = this._getPermittedAccounts(origin);
if (!oldPermittedAccounts) {
if (oldPermittedAccounts.length === 0) {
throw new Error(`Origin does not have 'eth_accounts' permission`);
} else if (oldPermittedAccounts.includes(account)) {
throw new Error('Account is already permitted for origin');
@ -335,7 +335,7 @@ export class PermissionsController {
this.validatePermittedAccounts([account]);
const oldPermittedAccounts = this._getPermittedAccounts(origin);
if (!oldPermittedAccounts) {
if (oldPermittedAccounts.length === 0) {
throw new Error(`Origin does not have 'eth_accounts' permission`);
} else if (!oldPermittedAccounts.includes(account)) {
throw new Error('Account is not permitted for origin');
@ -612,7 +612,7 @@ export class PermissionsController {
* Get current set of permitted accounts for the given origin
*
* @param {string} origin - The origin to obtain permitted accounts for
* @returns {Array<string>|null} The list of permitted accounts
* @returns {Array<string>} The list of permitted accounts
*/
_getPermittedAccounts(origin) {
const permittedAccounts = this.permissions
@ -620,7 +620,7 @@ export class PermissionsController {
?.caveats?.find((caveat) => caveat.name === CAVEAT_NAMES.exposedAccounts)
?.value;
return permittedAccounts || null;
return permittedAccounts || [];
}
/**

@ -2,22 +2,20 @@ import { strict as assert } from 'assert';
import { find } from 'lodash';
import sinon from 'sinon';
import {
METADATA_STORE_KEY,
METADATA_CACHE_MAX_SIZE,
} from '../../../../../app/scripts/controllers/permissions/enums';
import { PermissionsController } from '../../../../../app/scripts/controllers/permissions';
import { getRequestUserApprovalHelper, grantPermissions } from './helpers';
import {
constants,
getters,
getNotifyDomain,
getNotifyAllDomains,
getPermControllerOpts,
} from './mocks';
} from '../../../../test/mocks/permission-controller';
import {
getRequestUserApprovalHelper,
grantPermissions,
} from '../../../../test/helpers/permission-controller-helpers';
import { METADATA_STORE_KEY, METADATA_CACHE_MAX_SIZE } from './enums';
import { PermissionsController } from '.';
const { ERRORS, NOTIFICATIONS, PERMS } = getters;

@ -3,16 +3,14 @@ import { ObservableStore } from '@metamask/obs-store';
import nanoid from 'nanoid';
import { useFakeTimers } from 'sinon';
import PermissionsLogController from '../../../../../app/scripts/controllers/permissions/permissionsLog';
import {
LOG_LIMIT,
LOG_METHOD_TYPES,
} from '../../../../../app/scripts/controllers/permissions/enums';
import { validateActivityEntry } from './helpers';
import { constants, getters, noop } from './mocks';
constants,
getters,
noop,
} from '../../../../test/mocks/permission-controller';
import { validateActivityEntry } from '../../../../test/helpers/permission-controller-helpers';
import PermissionsLogController from './permissionsLog';
import { LOG_LIMIT, LOG_METHOD_TYPES } from './enums';
const { PERMS, RPC_REQUESTS } = getters;
@ -50,7 +48,7 @@ const initMiddleware = (permLog) => {
const initClock = () => {
// useFakeTimers, is in fact, not a react-hook
// eslint-disable-next-line
clock = useFakeTimers(1)
clock = useFakeTimers(1);
};
const tearDownClock = () => {

@ -1,18 +1,19 @@
import { strict as assert } from 'assert';
import sinon from 'sinon';
import { METADATA_STORE_KEY } from '../../../../../app/scripts/controllers/permissions/enums';
import { PermissionsController } from '../../../../../app/scripts/controllers/permissions';
import { getUserApprovalPromise, grantPermissions } from './helpers';
import {
constants,
getters,
getPermControllerOpts,
getPermissionsMiddleware,
} from './mocks';
} from '../../../../test/mocks/permission-controller';
import {
getUserApprovalPromise,
grantPermissions,
} from '../../../../test/helpers/permission-controller-helpers';
import { METADATA_STORE_KEY } from './enums';
import { PermissionsController } from '.';
const { CAVEATS, ERRORS, PERMS, RPC_REQUESTS } = getters;

@ -1,4 +1,4 @@
import { cloneDeep } from 'lodash';
import stringify from 'fast-safe-stringify';
import { CAVEAT_NAMES } from '../../../../shared/constants/permissions';
import {
HISTORY_STORE_KEY,
@ -151,7 +151,7 @@ export default class PermissionsLogController {
? LOG_METHOD_TYPES.internal
: LOG_METHOD_TYPES.restricted,
origin: request.origin,
request: cloneDeep(request),
request: stringify(request, null, 2),
requestTime: Date.now(),
response: null,
responseTime: null,
@ -174,7 +174,7 @@ export default class PermissionsLogController {
return;
}
entry.response = cloneDeep(response);
entry.response = stringify(response, null, 2);
entry.responseTime = time;
entry.success = !response.error;
}

@ -1,7 +1,7 @@
import { strict as assert } from 'assert';
import pify from 'pify';
import getRestrictedMethods from '../../../../../app/scripts/controllers/permissions/restrictedMethods';
import getRestrictedMethods from './restrictedMethods';
describe('restricted methods', function () {
describe('eth_accounts', function () {

@ -378,7 +378,7 @@ export default class PreferencesController {
*/
async addToken(rawAddress, symbol, decimals, image) {
const address = normalizeAddress(rawAddress);
const newEntry = { address, symbol, decimals };
const newEntry = { address, symbol, decimals: Number(decimals) };
const { tokens, hiddenTokens } = this.store.getState();
const assetImages = this.getAssetImages();
const updatedHiddenTokens = hiddenTokens.filter(
@ -793,9 +793,14 @@ export default class PreferencesController {
if (typeof symbol !== 'string') {
throw ethErrors.rpc.invalidParams(`Invalid symbol: not a string.`);
}
if (!(symbol.length < 7)) {
if (!(symbol.length > 0)) {
throw ethErrors.rpc.invalidParams(
`Invalid symbol "${symbol}": longer than 6 characters.`,
`Invalid symbol "${symbol}": shorter than a character.`,
);
}
if (!(symbol.length < 12)) {
throw ethErrors.rpc.invalidParams(
`Invalid symbol "${symbol}": longer than 11 characters.`,
);
}
const numDecimals = parseInt(decimals, 10);

@ -1,10 +1,10 @@
import assert from 'assert';
import sinon from 'sinon';
import PreferencesController from '../../../../app/scripts/controllers/preferences';
import {
MAINNET_CHAIN_ID,
RINKEBY_CHAIN_ID,
} from '../../../../shared/constants/network';
} from '../../../shared/constants/network';
import PreferencesController from './preferences';
describe('preferences controller', function () {
let preferencesController;
@ -539,6 +539,14 @@ describe('preferences controller', function () {
decimals: 0,
}),
);
assert.doesNotThrow(() =>
validate({
address: '0xd26114cd6EE289AccF82350c8d8487fedB8A0C07',
symbol: 'ABCDEFGHIJK',
decimals: 0,
}),
);
assert.throws(
() => validate({ symbol: 'ABC', decimals: 0 }),
'missing address should fail',
@ -563,10 +571,19 @@ describe('preferences controller', function () {
() =>
validate({
address: '0xd26114cd6EE289AccF82350c8d8487fedB8A0C07',
symbol: 'ABCDEFGHI',
symbol: 'ABCDEFGHIJKLM',
decimals: 0,
}),
'long symbol should fail',
);
assert.throws(
() =>
validate({
address: '0xd26114cd6EE289AccF82350c8d8487fedB8A0C07',
symbol: '',
decimals: 0,
}),
'invalid symbol should fail',
'empty symbol should fail',
);
assert.throws(
() =>

@ -9,13 +9,11 @@ import {
ROPSTEN_NETWORK_ID,
MAINNET_NETWORK_ID,
MAINNET_CHAIN_ID,
} from '../../../../shared/constants/network';
import { ETH_SWAPS_TOKEN_OBJECT } from '../../../../shared/constants/swaps';
import { createTestProviderTools } from '../../../stub/provider';
import SwapsController, {
utils,
} from '../../../../app/scripts/controllers/swaps';
import { NETWORK_EVENTS } from '../../../../app/scripts/controllers/network';
} from '../../../shared/constants/network';
import { ETH_SWAPS_TOKEN_OBJECT } from '../../../shared/constants/swaps';
import { createTestProviderTools } from '../../../test/stub/provider';
import SwapsController, { utils } from './swaps';
import { NETWORK_EVENTS } from './network';
const MOCK_FETCH_PARAMS = {
slippage: 3,

@ -1,7 +1,7 @@
import assert from 'assert';
import sinon from 'sinon';
import { ObservableStore } from '@metamask/obs-store';
import TokenRatesController from '../../../../app/scripts/controllers/token-rates';
import TokenRatesController from './token-rates';
describe('TokenRatesController', function () {
let nativeCurrency;

@ -19,7 +19,6 @@ import {
import { TRANSACTION_NO_CONTRACT_ERROR_KEY } from '../../../../ui/app/helpers/constants/error-keys';
import { getSwapsTokensReceivedFromTxMeta } from '../../../../ui/app/pages/swaps/swaps.util';
import {
TRANSACTION_CATEGORIES,
TRANSACTION_STATUSES,
TRANSACTION_TYPES,
} from '../../../../shared/constants/transaction';
@ -151,8 +150,8 @@ export default class TransactionController extends EventEmitter {
Adds a tx to the txlist
@emits ${txMeta.id}:unapproved
*/
addTx(txMeta) {
this.txStateManager.addTx(txMeta);
addTransaction(txMeta) {
this.txStateManager.addTransaction(txMeta);
this.emit(`${txMeta.id}:unapproved`, txMeta);
}
@ -235,11 +234,10 @@ export default class TransactionController extends EventEmitter {
`generateTxMeta` adds the default txMeta properties to the passed object.
These include the tx's `id`. As we use the id for determining order of
txes in the tx-state-manager, it is necessary to call the asynchronous
method `this._determineTransactionCategory` after `generateTxMeta`.
method `this._determineTransactionType` after `generateTxMeta`.
*/
let txMeta = this.txStateManager.generateTxMeta({
txParams: normalizedTxParams,
type: TRANSACTION_TYPES.STANDARD,
});
if (origin === 'metamask') {
@ -265,33 +263,38 @@ export default class TransactionController extends EventEmitter {
txMeta.origin = origin;
const {
transactionCategory,
getCodeResponse,
} = await this._determineTransactionCategory(txParams);
txMeta.transactionCategory = transactionCategory;
const { type, getCodeResponse } = await this._determineTransactionType(
txParams,
);
txMeta.type = type;
// ensure value
txMeta.txParams.value = txMeta.txParams.value
? addHexPrefix(txMeta.txParams.value)
: '0x0';
this.addTx(txMeta);
this.addTransaction(txMeta);
this.emit('newUnapprovedTx', txMeta);
try {
txMeta = await this.addTxGasDefaults(txMeta, getCodeResponse);
} catch (error) {
log.warn(error);
txMeta = this.txStateManager.getTx(txMeta.id);
txMeta = this.txStateManager.getTransaction(txMeta.id);
txMeta.loadingDefaults = false;
this.txStateManager.updateTx(txMeta, 'Failed to calculate gas defaults.');
this.txStateManager.updateTransaction(
txMeta,
'Failed to calculate gas defaults.',
);
throw error;
}
txMeta.loadingDefaults = false;
// save txMeta
this.txStateManager.updateTx(txMeta, 'Added new unapproved transaction.');
this.txStateManager.updateTransaction(
txMeta,
'Added new unapproved transaction.',
);
return txMeta;
}
@ -309,7 +312,7 @@ export default class TransactionController extends EventEmitter {
} = await this._getDefaultGasLimit(txMeta, getCodeResponse);
// eslint-disable-next-line no-param-reassign
txMeta = this.txStateManager.getTx(txMeta.id);
txMeta = this.txStateManager.getTransaction(txMeta.id);
if (simulationFails) {
txMeta.simulationFails = simulationFails;
}
@ -347,7 +350,7 @@ export default class TransactionController extends EventEmitter {
return {};
} else if (
txMeta.txParams.to &&
txMeta.transactionCategory === TRANSACTION_CATEGORIES.SENT_ETHER
txMeta.type === TRANSACTION_TYPES.SENT_ETHER
) {
// if there's data in the params, but there's no contract code, it's not a valid transaction
if (txMeta.txParams.data) {
@ -388,8 +391,8 @@ export default class TransactionController extends EventEmitter {
* @param {string} [customGasPrice] - the hex value to use for the cancel transaction
* @returns {txMeta}
*/
async createCancelTransaction(originalTxId, customGasPrice) {
const originalTxMeta = this.txStateManager.getTx(originalTxId);
async createCancelTransaction(originalTxId, customGasPrice, customGasLimit) {
const originalTxMeta = this.txStateManager.getTransaction(originalTxId);
const { txParams } = originalTxMeta;
const { gasPrice: lastGasPrice, from, nonce } = txParams;
@ -401,7 +404,7 @@ export default class TransactionController extends EventEmitter {
from,
to: from,
nonce,
gas: '0x5208',
gas: customGasLimit || '0x5208',
value: '0x0',
gasPrice: newGasPrice,
},
@ -411,7 +414,7 @@ export default class TransactionController extends EventEmitter {
type: TRANSACTION_TYPES.CANCEL,
});
this.addTx(newTxMeta);
this.addTransaction(newTxMeta);
await this.approveTransaction(newTxMeta.id);
return newTxMeta;
}
@ -427,7 +430,7 @@ export default class TransactionController extends EventEmitter {
* @returns {txMeta}
*/
async createSpeedUpTransaction(originalTxId, customGasPrice, customGasLimit) {
const originalTxMeta = this.txStateManager.getTx(originalTxId);
const originalTxMeta = this.txStateManager.getTransaction(originalTxId);
const { txParams } = originalTxMeta;
const { gasPrice: lastGasPrice } = txParams;
@ -450,7 +453,7 @@ export default class TransactionController extends EventEmitter {
newTxMeta.txParams.gas = customGasLimit;
}
this.addTx(newTxMeta);
this.addTransaction(newTxMeta);
await this.approveTransaction(newTxMeta.id);
return newTxMeta;
}
@ -460,7 +463,10 @@ export default class TransactionController extends EventEmitter {
@param {Object} txMeta - the updated txMeta
*/
async updateTransaction(txMeta) {
this.txStateManager.updateTx(txMeta, 'confTx: user updated transaction');
this.txStateManager.updateTransaction(
txMeta,
'confTx: user updated transaction',
);
}
/**
@ -468,7 +474,10 @@ export default class TransactionController extends EventEmitter {
@param {Object} txMeta
*/
async updateAndApproveTransaction(txMeta) {
this.txStateManager.updateTx(txMeta, 'confTx: user approved transaction');
this.txStateManager.updateTransaction(
txMeta,
'confTx: user approved transaction',
);
await this.approveTransaction(txMeta.id);
}
@ -495,7 +504,7 @@ export default class TransactionController extends EventEmitter {
// approve
this.txStateManager.setTxStatusApproved(txId);
// get next nonce
const txMeta = this.txStateManager.getTx(txId);
const txMeta = this.txStateManager.getTransaction(txId);
const fromAddress = txMeta.txParams.from;
// wait for a nonce
let { customNonceValue } = txMeta;
@ -516,7 +525,10 @@ export default class TransactionController extends EventEmitter {
if (customNonceValue) {
txMeta.nonceDetails.customNonceValue = customNonceValue;
}
this.txStateManager.updateTx(txMeta, 'transactions#approveTransaction');
this.txStateManager.updateTransaction(
txMeta,
'transactions#approveTransaction',
);
// sign transaction
const rawTx = await this.signTransaction(txId);
await this.publishTransaction(txId, rawTx);
@ -546,7 +558,7 @@ export default class TransactionController extends EventEmitter {
@returns {string} rawTx
*/
async signTransaction(txId) {
const txMeta = this.txStateManager.getTx(txId);
const txMeta = this.txStateManager.getTransaction(txId);
// add network/chain id
const chainId = this.getChainId();
const txParams = { ...txMeta.txParams, chainId };
@ -561,7 +573,7 @@ export default class TransactionController extends EventEmitter {
txMeta.s = ethUtil.bufferToHex(ethTx.s);
txMeta.v = ethUtil.bufferToHex(ethTx.v);
this.txStateManager.updateTx(
this.txStateManager.updateTransaction(
txMeta,
'transactions#signTransaction: add r, s, v values',
);
@ -579,13 +591,16 @@ export default class TransactionController extends EventEmitter {
@returns {Promise<void>}
*/
async publishTransaction(txId, rawTx) {
const txMeta = this.txStateManager.getTx(txId);
const txMeta = this.txStateManager.getTransaction(txId);
txMeta.rawTx = rawTx;
if (txMeta.transactionCategory === TRANSACTION_CATEGORIES.SWAP) {
if (txMeta.type === TRANSACTION_TYPES.SWAP) {
const preTxBalance = await this.query.getBalance(txMeta.txParams.from);
txMeta.preTxBalance = preTxBalance.toString(16);
}
this.txStateManager.updateTx(txMeta, 'transactions#publishTransaction');
this.txStateManager.updateTransaction(
txMeta,
'transactions#publishTransaction',
);
let txHash;
try {
txHash = await this.query.sendRawTransaction(rawTx);
@ -611,7 +626,7 @@ export default class TransactionController extends EventEmitter {
async confirmTransaction(txId, txReceipt) {
// get the txReceipt before marking the transaction confirmed
// to ensure the receipt is gotten before the ui revives the tx
const txMeta = this.txStateManager.getTx(txId);
const txMeta = this.txStateManager.getTransaction(txId);
if (!txMeta) {
return;
@ -632,22 +647,22 @@ export default class TransactionController extends EventEmitter {
this.txStateManager.setTxStatusConfirmed(txId);
this._markNonceDuplicatesDropped(txId);
this.txStateManager.updateTx(
this.txStateManager.updateTransaction(
txMeta,
'transactions#confirmTransaction - add txReceipt',
);
if (txMeta.transactionCategory === TRANSACTION_CATEGORIES.SWAP) {
if (txMeta.type === TRANSACTION_TYPES.SWAP) {
const postTxBalance = await this.query.getBalance(txMeta.txParams.from);
const latestTxMeta = this.txStateManager.getTx(txId);
const latestTxMeta = this.txStateManager.getTransaction(txId);
const approvalTxMeta = latestTxMeta.approvalTxId
? this.txStateManager.getTx(latestTxMeta.approvalTxId)
? this.txStateManager.getTransaction(latestTxMeta.approvalTxId)
: null;
latestTxMeta.postTxBalance = postTxBalance.toString(16);
this.txStateManager.updateTx(
this.txStateManager.updateTransaction(
latestTxMeta,
'transactions#confirmTransaction - add postTxBalance',
);
@ -675,9 +690,9 @@ export default class TransactionController extends EventEmitter {
*/
setTxHash(txId, txHash) {
// Add the tx hash to the persisted meta-tx object
const txMeta = this.txStateManager.getTx(txId);
const txMeta = this.txStateManager.getTransaction(txId);
txMeta.hash = txHash;
this.txStateManager.updateTx(txMeta, 'transactions#setTxHash');
this.txStateManager.updateTransaction(txMeta, 'transactions#setTxHash');
}
//
@ -707,8 +722,7 @@ export default class TransactionController extends EventEmitter {
this.txStateManager.getPendingTransactions(account).length;
/** see txStateManager */
this.getFilteredTxList = (opts) =>
this.txStateManager.getFilteredTxList(opts);
this.getTransactions = (opts) => this.txStateManager.getTransactions(opts);
}
// called once on startup
@ -727,23 +741,25 @@ export default class TransactionController extends EventEmitter {
_onBootCleanUp() {
this.txStateManager
.getFilteredTxList({
status: TRANSACTION_STATUSES.UNAPPROVED,
loadingDefaults: true,
.getTransactions({
searchCriteria: {
status: TRANSACTION_STATUSES.UNAPPROVED,
loadingDefaults: true,
},
})
.forEach((tx) => {
this.addTxGasDefaults(tx)
.then((txMeta) => {
txMeta.loadingDefaults = false;
this.txStateManager.updateTx(
this.txStateManager.updateTransaction(
txMeta,
'transactions: gas estimation for tx on boot',
);
})
.catch((error) => {
const txMeta = this.txStateManager.getTx(tx.id);
const txMeta = this.txStateManager.getTransaction(tx.id);
txMeta.loadingDefaults = false;
this.txStateManager.updateTx(
this.txStateManager.updateTransaction(
txMeta,
'failed to estimate gas during boot cleanup.',
);
@ -752,8 +768,10 @@ export default class TransactionController extends EventEmitter {
});
this.txStateManager
.getFilteredTxList({
status: TRANSACTION_STATUSES.APPROVED,
.getTransactions({
searchCriteria: {
status: TRANSACTION_STATUSES.APPROVED,
},
})
.forEach((txMeta) => {
const txSignError = new Error(
@ -774,7 +792,7 @@ export default class TransactionController extends EventEmitter {
);
this._setupBlockTrackerListener();
this.pendingTxTracker.on('tx:warning', (txMeta) => {
this.txStateManager.updateTx(
this.txStateManager.updateTransaction(
txMeta,
'transactions/pending-tx-tracker#event: tx:warning',
);
@ -793,7 +811,7 @@ export default class TransactionController extends EventEmitter {
this.pendingTxTracker.on('tx:block-update', (txMeta, latestBlockNumber) => {
if (!txMeta.firstRetryBlockNumber) {
txMeta.firstRetryBlockNumber = latestBlockNumber;
this.txStateManager.updateTx(
this.txStateManager.updateTransaction(
txMeta,
'transactions/pending-tx-tracker#event: tx:block-update',
);
@ -804,7 +822,7 @@ export default class TransactionController extends EventEmitter {
txMeta.retryCount = 0;
}
txMeta.retryCount += 1;
this.txStateManager.updateTx(
this.txStateManager.updateTransaction(
txMeta,
'transactions/pending-tx-tracker#event: tx:retry',
);
@ -812,10 +830,27 @@ export default class TransactionController extends EventEmitter {
}
/**
Returns a "type" for a transaction out of the following list: simpleSend, tokenTransfer, tokenApprove,
contractDeployment, contractMethodCall
*/
async _determineTransactionCategory(txParams) {
* @typedef { 'transfer' | 'approve' | 'transferfrom' | 'contractInteraction'| 'sentEther' } InferrableTransactionTypes
*/
/**
* @typedef {Object} InferTransactionTypeResult
* @property {InferrableTransactionTypes} type - The type of transaction
* @property {string} getCodeResponse - The contract code, in hex format if
* it exists. '0x0' or '0x' are also indicators of non-existent contract
* code
*/
/**
* Determines the type of the transaction by analyzing the txParams.
* This method will return one of the types defined in shared/constants/transactions
* It will never return TRANSACTION_TYPE_CANCEL or TRANSACTION_TYPE_RETRY as these
* represent specific events that we control from the extension and are added manually
* at transaction creation.
* @param {Object} txParams - Parameters for the transaction
* @returns {InferTransactionTypeResult}
*/
async _determineTransactionType(txParams) {
const { data, to } = txParams;
let name;
try {
@ -825,16 +860,16 @@ export default class TransactionController extends EventEmitter {
}
const tokenMethodName = [
TRANSACTION_CATEGORIES.TOKEN_METHOD_APPROVE,
TRANSACTION_CATEGORIES.TOKEN_METHOD_TRANSFER,
TRANSACTION_CATEGORIES.TOKEN_METHOD_TRANSFER_FROM,
TRANSACTION_TYPES.TOKEN_METHOD_APPROVE,
TRANSACTION_TYPES.TOKEN_METHOD_TRANSFER,
TRANSACTION_TYPES.TOKEN_METHOD_TRANSFER_FROM,
].find((methodName) => methodName === name && name.toLowerCase());
let result;
if (data && tokenMethodName) {
result = tokenMethodName;
} else if (data && !to) {
result = TRANSACTION_CATEGORIES.DEPLOY_CONTRACT;
result = TRANSACTION_TYPES.DEPLOY_CONTRACT;
}
let code;
@ -849,11 +884,11 @@ export default class TransactionController extends EventEmitter {
const codeIsEmpty = !code || code === '0x' || code === '0x0';
result = codeIsEmpty
? TRANSACTION_CATEGORIES.SENT_ETHER
: TRANSACTION_CATEGORIES.CONTRACT_INTERACTION;
? TRANSACTION_TYPES.SENT_ETHER
: TRANSACTION_TYPES.CONTRACT_INTERACTION;
}
return { transactionCategory: result, getCodeResponse: code };
return { type: result, getCodeResponse: code };
}
/**
@ -864,9 +899,11 @@ export default class TransactionController extends EventEmitter {
*/
_markNonceDuplicatesDropped(txId) {
// get the confirmed transactions nonce and from address
const txMeta = this.txStateManager.getTx(txId);
const txMeta = this.txStateManager.getTransaction(txId);
const { nonce, from } = txMeta.txParams;
const sameNonceTxs = this.txStateManager.getFilteredTxList({ nonce, from });
const sameNonceTxs = this.txStateManager.getTransactions({
searchCriteria: { nonce, from },
});
if (!sameNonceTxs.length) {
return;
}
@ -876,7 +913,7 @@ export default class TransactionController extends EventEmitter {
return;
}
otherTxMeta.replacedBy = txMeta.hash;
this.txStateManager.updateTx(
this.txStateManager.updateTransaction(
txMeta,
'transactions/pending-tx-tracker#event: tx:confirmed reference to confirmed txHash with same nonce',
);
@ -922,9 +959,9 @@ export default class TransactionController extends EventEmitter {
*/
_updateMemstore() {
const unapprovedTxs = this.txStateManager.getUnapprovedTxList();
const currentNetworkTxList = this.txStateManager.getTxList(
MAX_MEMSTORE_TX_LIST_SIZE,
);
const currentNetworkTxList = this.txStateManager.getTransactions({
limit: MAX_MEMSTORE_TX_LIST_SIZE,
});
this.memStore.updateState({ unapprovedTxs, currentNetworkTxList });
}

@ -4,23 +4,25 @@ import ethUtil from 'ethereumjs-util';
import EthTx from 'ethereumjs-tx';
import { ObservableStore } from '@metamask/obs-store';
import sinon from 'sinon';
import TransactionController from '../../../../../app/scripts/controllers/transactions';
import {
createTestProviderTools,
getTestAccounts,
} from '../../../../stub/provider';
} from '../../../../test/stub/provider';
import {
TRANSACTION_CATEGORIES,
TRANSACTION_STATUSES,
TRANSACTION_TYPES,
} from '../../../../../shared/constants/transaction';
import { METAMASK_CONTROLLER_EVENTS } from '../../../../../app/scripts/metamask-controller';
} from '../../../../shared/constants/transaction';
import { METAMASK_CONTROLLER_EVENTS } from '../../metamask-controller';
import TransactionController from '.';
const noop = () => true;
const currentNetworkId = '42';
const currentChainId = '0x2a';
const VALID_ADDRESS = '0x0000000000000000000000000000000000000000';
const VALID_ADDRESS_TWO = '0x0000000000000000000000000000000000000001';
describe('Transaction Controller', function () {
let txController, provider, providerResultStub, fromAccount;
@ -82,26 +84,35 @@ describe('Transaction Controller', function () {
describe('#getUnapprovedTxCount', function () {
it('should return the number of unapproved txs', function () {
txController.txStateManager._saveTxList([
txController.txStateManager._addTransactionsToState([
{
id: 1,
status: TRANSACTION_STATUSES.UNAPPROVED,
metamaskNetworkId: currentNetworkId,
txParams: {},
txParams: {
to: VALID_ADDRESS,
from: VALID_ADDRESS_TWO,
},
history: [{}],
},
{
id: 2,
status: TRANSACTION_STATUSES.UNAPPROVED,
metamaskNetworkId: currentNetworkId,
txParams: {},
txParams: {
to: VALID_ADDRESS,
from: VALID_ADDRESS_TWO,
},
history: [{}],
},
{
id: 3,
status: TRANSACTION_STATUSES.UNAPPROVED,
metamaskNetworkId: currentNetworkId,
txParams: {},
txParams: {
to: VALID_ADDRESS,
from: VALID_ADDRESS_TWO,
},
history: [{}],
},
]);
@ -112,26 +123,35 @@ describe('Transaction Controller', function () {
describe('#getPendingTxCount', function () {
it('should return the number of pending txs', function () {
txController.txStateManager._saveTxList([
txController.txStateManager._addTransactionsToState([
{
id: 1,
status: TRANSACTION_STATUSES.SUBMITTED,
metamaskNetworkId: currentNetworkId,
txParams: {},
txParams: {
to: VALID_ADDRESS,
from: VALID_ADDRESS_TWO,
},
history: [{}],
},
{
id: 2,
status: TRANSACTION_STATUSES.SUBMITTED,
metamaskNetworkId: currentNetworkId,
txParams: {},
txParams: {
to: VALID_ADDRESS,
from: VALID_ADDRESS_TWO,
},
history: [{}],
},
{
id: 3,
status: TRANSACTION_STATUSES.SUBMITTED,
metamaskNetworkId: currentNetworkId,
txParams: {},
txParams: {
to: VALID_ADDRESS,
from: VALID_ADDRESS_TWO,
},
history: [{}],
},
]);
@ -147,7 +167,7 @@ describe('Transaction Controller', function () {
from: address,
to: '0xc684832530fcbddae4b4230a47e991ddcec2831d',
};
txController.txStateManager._saveTxList([
txController.txStateManager._addTransactionsToState([
{
id: 0,
status: TRANSACTION_STATUSES.CONFIRMED,
@ -233,17 +253,19 @@ describe('Transaction Controller', function () {
txParams,
history: [{}],
};
txController.txStateManager._saveTxList([txMeta]);
txController.txStateManager._addTransactionsToState([txMeta]);
stub = sinon
.stub(txController, 'addUnapprovedTransaction')
.callsFake(() => {
txController.emit('newUnapprovedTx', txMeta);
return Promise.resolve(txController.txStateManager.addTx(txMeta));
return Promise.resolve(
txController.txStateManager.addTransaction(txMeta),
);
});
});
afterEach(function () {
txController.txStateManager._saveTxList([]);
txController.txStateManager._addTransactionsToState([]);
stub.restore();
});
@ -313,7 +335,7 @@ describe('Transaction Controller', function () {
'should have added 0x0 as the value',
);
const memTxMeta = txController.txStateManager.getTx(txMeta.id);
const memTxMeta = txController.txStateManager.getTransaction(txMeta.id);
assert.deepEqual(txMeta, memTxMeta);
});
@ -354,12 +376,15 @@ describe('Transaction Controller', function () {
describe('#addTxGasDefaults', function () {
it('should add the tx defaults if their are none', async function () {
txController.txStateManager._saveTxList([
txController.txStateManager._addTransactionsToState([
{
id: 1,
status: TRANSACTION_STATUSES.UNAPPROVED,
metamaskNetworkId: currentNetworkId,
txParams: {},
txParams: {
to: VALID_ADDRESS,
from: VALID_ADDRESS_TWO,
},
history: [{}],
},
]);
@ -387,13 +412,16 @@ describe('Transaction Controller', function () {
});
});
describe('#addTx', function () {
describe('#addTransaction', function () {
it('should emit updates', function (done) {
const txMeta = {
id: '1',
status: TRANSACTION_STATUSES.UNAPPROVED,
metamaskNetworkId: currentNetworkId,
txParams: {},
txParams: {
to: VALID_ADDRESS,
from: VALID_ADDRESS_TWO,
},
};
const eventNames = [
@ -420,7 +448,7 @@ describe('Transaction Controller', function () {
done();
})
.catch(done);
txController.addTx(txMeta);
txController.addTransaction(txMeta);
});
});
@ -432,6 +460,8 @@ describe('Transaction Controller', function () {
status: TRANSACTION_STATUSES.UNAPPROVED,
metamaskNetworkId: currentNetworkId,
txParams: {
to: VALID_ADDRESS_TWO,
from: VALID_ADDRESS,
nonce: originalValue,
gas: originalValue,
gasPrice: originalValue,
@ -441,7 +471,7 @@ describe('Transaction Controller', function () {
this.timeout(15000);
const wrongValue = '0x05';
txController.addTx(txMeta);
txController.addTransaction(txMeta);
providerResultStub.eth_gasPrice = wrongValue;
providerResultStub.eth_estimateGas = '0x5209';
@ -457,7 +487,7 @@ describe('Transaction Controller', function () {
});
await txController.approveTransaction(txMeta.id);
const result = txController.txStateManager.getTx(txMeta.id);
const result = txController.txStateManager.getTransaction(txMeta.id);
const params = result.txParams;
assert.equal(params.gas, originalValue, 'gas unmodified');
@ -475,12 +505,15 @@ describe('Transaction Controller', function () {
describe('#sign replay-protected tx', function () {
it('prepares a tx with the chainId set', async function () {
txController.addTx(
txController.addTransaction(
{
id: '1',
status: TRANSACTION_STATUSES.UNAPPROVED,
metamaskNetworkId: currentNetworkId,
txParams: {},
txParams: {
to: VALID_ADDRESS,
from: VALID_ADDRESS_TWO,
},
},
noop,
);
@ -504,9 +537,9 @@ describe('Transaction Controller', function () {
},
metamaskNetworkId: currentNetworkId,
};
txController.txStateManager.addTx(txMeta);
txController.txStateManager.addTransaction(txMeta);
const approvalPromise = txController.updateAndApproveTransaction(txMeta);
const tx = txController.txStateManager.getTx(1);
const tx = txController.txStateManager.getTransaction(1);
assert.equal(tx.status, TRANSACTION_STATUSES.APPROVED);
await approvalPromise;
});
@ -521,53 +554,74 @@ describe('Transaction Controller', function () {
describe('#cancelTransaction', function () {
it('should emit a status change to rejected', function (done) {
txController.txStateManager._saveTxList([
txController.txStateManager._addTransactionsToState([
{
id: 0,
status: TRANSACTION_STATUSES.UNAPPROVED,
txParams: {},
txParams: {
to: VALID_ADDRESS,
from: VALID_ADDRESS_TWO,
},
metamaskNetworkId: currentNetworkId,
history: [{}],
},
{
id: 1,
status: TRANSACTION_STATUSES.REJECTED,
txParams: {},
txParams: {
to: VALID_ADDRESS,
from: VALID_ADDRESS_TWO,
},
metamaskNetworkId: currentNetworkId,
history: [{}],
},
{
id: 2,
status: TRANSACTION_STATUSES.APPROVED,
txParams: {},
txParams: {
to: VALID_ADDRESS,
from: VALID_ADDRESS_TWO,
},
metamaskNetworkId: currentNetworkId,
history: [{}],
},
{
id: 3,
status: TRANSACTION_STATUSES.SIGNED,
txParams: {},
txParams: {
to: VALID_ADDRESS,
from: VALID_ADDRESS_TWO,
},
metamaskNetworkId: currentNetworkId,
history: [{}],
},
{
id: 4,
status: TRANSACTION_STATUSES.SUBMITTED,
txParams: {},
txParams: {
to: VALID_ADDRESS,
from: VALID_ADDRESS_TWO,
},
metamaskNetworkId: currentNetworkId,
history: [{}],
},
{
id: 5,
status: TRANSACTION_STATUSES.CONFIRMED,
txParams: {},
txParams: {
to: VALID_ADDRESS,
from: VALID_ADDRESS_TWO,
},
metamaskNetworkId: currentNetworkId,
history: [{}],
},
{
id: 6,
status: TRANSACTION_STATUSES.FAILED,
txParams: {},
txParams: {
to: VALID_ADDRESS,
from: VALID_ADDRESS_TWO,
},
metamaskNetworkId: currentNetworkId,
history: [{}],
},
@ -592,13 +646,13 @@ describe('Transaction Controller', function () {
});
describe('#createSpeedUpTransaction', function () {
let addTxSpy;
let addTransactionSpy;
let approveTransactionSpy;
let txParams;
let expectedTxParams;
beforeEach(function () {
addTxSpy = sinon.spy(txController, 'addTx');
addTransactionSpy = sinon.spy(txController, 'addTransaction');
approveTransactionSpy = sinon.spy(txController, 'approveTransaction');
txParams = {
@ -608,7 +662,7 @@ describe('Transaction Controller', function () {
gas: '0x5209',
gasPrice: '0xa',
};
txController.txStateManager._saveTxList([
txController.txStateManager._addTransactionsToState([
{
id: 1,
status: TRANSACTION_STATUSES.SUBMITTED,
@ -622,18 +676,18 @@ describe('Transaction Controller', function () {
});
afterEach(function () {
addTxSpy.restore();
addTransactionSpy.restore();
approveTransactionSpy.restore();
});
it('should call this.addTx and this.approveTransaction with the expected args', async function () {
it('should call this.addTransaction and this.approveTransaction with the expected args', async function () {
await txController.createSpeedUpTransaction(1);
assert.equal(addTxSpy.callCount, 1);
assert.equal(addTransactionSpy.callCount, 1);
const addTxArgs = addTxSpy.getCall(0).args[0];
assert.deepEqual(addTxArgs.txParams, expectedTxParams);
const addTransactionArgs = addTransactionSpy.getCall(0).args[0];
assert.deepEqual(addTransactionArgs.txParams, expectedTxParams);
const { lastGasPrice, type } = addTxArgs;
const { lastGasPrice, type } = addTransactionArgs;
assert.deepEqual(
{ lastGasPrice, type },
{
@ -675,7 +729,10 @@ describe('Transaction Controller', function () {
txMeta = {
id: 1,
status: TRANSACTION_STATUSES.UNAPPROVED,
txParams: {},
txParams: {
to: VALID_ADDRESS,
from: VALID_ADDRESS_TWO,
},
metamaskNetworkId: currentNetworkId,
};
providerResultStub.eth_sendRawTransaction = hash;
@ -684,9 +741,9 @@ describe('Transaction Controller', function () {
it('should publish a tx, updates the rawTx when provided a one', async function () {
const rawTx =
'0x477b2e6553c917af0db0388ae3da62965ff1a184558f61b749d1266b2e6d024c';
txController.txStateManager.addTx(txMeta);
txController.txStateManager.addTransaction(txMeta);
await txController.publishTransaction(txMeta.id, rawTx);
const publishedTx = txController.txStateManager.getTx(1);
const publishedTx = txController.txStateManager.getTransaction(1);
assert.equal(publishedTx.hash, hash);
assert.equal(publishedTx.status, TRANSACTION_STATUSES.SUBMITTED);
});
@ -697,9 +754,9 @@ describe('Transaction Controller', function () {
};
const rawTx =
'0xf86204831e848082520894f231d46dd78806e1dd93442cf33c7671f853874880802ca05f973e540f2d3c2f06d3725a626b75247593cb36477187ae07ecfe0a4db3cf57a00259b52ee8c58baaa385fb05c3f96116e58de89bcc165cb3bfdfc708672fed8a';
txController.txStateManager.addTx(txMeta);
txController.txStateManager.addTransaction(txMeta);
await txController.publishTransaction(txMeta.id, rawTx);
const publishedTx = txController.txStateManager.getTx(1);
const publishedTx = txController.txStateManager.getTransaction(1);
assert.equal(
publishedTx.hash,
'0x2cc5a25744486f7383edebbf32003e5a66e18135799593d6b5cdd2bb43674f09',
@ -710,62 +767,92 @@ describe('Transaction Controller', function () {
describe('#_markNonceDuplicatesDropped', function () {
it('should mark all nonce duplicates as dropped without marking the confirmed transaction as dropped', function () {
txController.txStateManager._saveTxList([
txController.txStateManager._addTransactionsToState([
{
id: 1,
status: TRANSACTION_STATUSES.CONFIRMED,
metamaskNetworkId: currentNetworkId,
history: [{}],
txParams: { nonce: '0x01' },
txParams: {
to: VALID_ADDRESS_TWO,
from: VALID_ADDRESS,
nonce: '0x01',
},
},
{
id: 2,
status: TRANSACTION_STATUSES.SUBMITTED,
metamaskNetworkId: currentNetworkId,
history: [{}],
txParams: { nonce: '0x01' },
txParams: {
to: VALID_ADDRESS_TWO,
from: VALID_ADDRESS,
nonce: '0x01',
},
},
{
id: 3,
status: TRANSACTION_STATUSES.SUBMITTED,
metamaskNetworkId: currentNetworkId,
history: [{}],
txParams: { nonce: '0x01' },
txParams: {
to: VALID_ADDRESS_TWO,
from: VALID_ADDRESS,
nonce: '0x01',
},
},
{
id: 4,
status: TRANSACTION_STATUSES.SUBMITTED,
metamaskNetworkId: currentNetworkId,
history: [{}],
txParams: { nonce: '0x01' },
txParams: {
to: VALID_ADDRESS_TWO,
from: VALID_ADDRESS,
nonce: '0x01',
},
},
{
id: 5,
status: TRANSACTION_STATUSES.SUBMITTED,
metamaskNetworkId: currentNetworkId,
history: [{}],
txParams: { nonce: '0x01' },
txParams: {
to: VALID_ADDRESS_TWO,
from: VALID_ADDRESS,
nonce: '0x01',
},
},
{
id: 6,
status: TRANSACTION_STATUSES.SUBMITTED,
metamaskNetworkId: currentNetworkId,
history: [{}],
txParams: { nonce: '0x01' },
txParams: {
to: VALID_ADDRESS_TWO,
from: VALID_ADDRESS,
nonce: '0x01',
},
},
{
id: 7,
status: TRANSACTION_STATUSES.SUBMITTED,
metamaskNetworkId: currentNetworkId,
history: [{}],
txParams: { nonce: '0x01' },
txParams: {
to: VALID_ADDRESS_TWO,
from: VALID_ADDRESS,
nonce: '0x01',
},
},
]);
txController._markNonceDuplicatesDropped(1);
const confirmedTx = txController.txStateManager.getTx(1);
const droppedTxs = txController.txStateManager.getFilteredTxList({
nonce: '0x01',
status: TRANSACTION_STATUSES.DROPPED,
const confirmedTx = txController.txStateManager.getTransaction(1);
const droppedTxs = txController.txStateManager.getTransactions({
searchCriteria: {
nonce: '0x01',
status: TRANSACTION_STATUSES.DROPPED,
},
});
assert.equal(
confirmedTx.status,
@ -776,76 +863,76 @@ describe('Transaction Controller', function () {
});
});
describe('#_determineTransactionCategory', function () {
it('should return a simple send transactionCategory when to is truthy but data is falsy', async function () {
const result = await txController._determineTransactionCategory({
describe('#_determineTransactionType', function () {
it('should return a simple send type when to is truthy but data is falsy', async function () {
const result = await txController._determineTransactionType({
to: '0xabc',
data: '',
});
assert.deepEqual(result, {
transactionCategory: TRANSACTION_CATEGORIES.SENT_ETHER,
type: TRANSACTION_TYPES.SENT_ETHER,
getCodeResponse: null,
});
});
it('should return a token transfer transactionCategory when data is for the respective method call', async function () {
const result = await txController._determineTransactionCategory({
it('should return a token transfer type when data is for the respective method call', async function () {
const result = await txController._determineTransactionType({
to: '0xabc',
data:
'0xa9059cbb0000000000000000000000002f318C334780961FB129D2a6c30D0763d9a5C970000000000000000000000000000000000000000000000000000000000000000a',
});
assert.deepEqual(result, {
transactionCategory: TRANSACTION_CATEGORIES.TOKEN_METHOD_TRANSFER,
type: TRANSACTION_TYPES.TOKEN_METHOD_TRANSFER,
getCodeResponse: undefined,
});
});
it('should return a token approve transactionCategory when data is for the respective method call', async function () {
const result = await txController._determineTransactionCategory({
it('should return a token approve type when data is for the respective method call', async function () {
const result = await txController._determineTransactionType({
to: '0xabc',
data:
'0x095ea7b30000000000000000000000002f318C334780961FB129D2a6c30D0763d9a5C9700000000000000000000000000000000000000000000000000000000000000005',
});
assert.deepEqual(result, {
transactionCategory: TRANSACTION_CATEGORIES.TOKEN_METHOD_APPROVE,
type: TRANSACTION_TYPES.TOKEN_METHOD_APPROVE,
getCodeResponse: undefined,
});
});
it('should return a contract deployment transactionCategory when to is falsy and there is data', async function () {
const result = await txController._determineTransactionCategory({
it('should return a contract deployment type when to is falsy and there is data', async function () {
const result = await txController._determineTransactionType({
to: '',
data: '0xabd',
});
assert.deepEqual(result, {
transactionCategory: TRANSACTION_CATEGORIES.DEPLOY_CONTRACT,
type: TRANSACTION_TYPES.DEPLOY_CONTRACT,
getCodeResponse: undefined,
});
});
it('should return a simple send transactionCategory with a 0x getCodeResponse when there is data and but the to address is not a contract address', async function () {
const result = await txController._determineTransactionCategory({
it('should return a simple send type with a 0x getCodeResponse when there is data and but the to address is not a contract address', async function () {
const result = await txController._determineTransactionType({
to: '0x9e673399f795D01116e9A8B2dD2F156705131ee9',
data: '0xabd',
});
assert.deepEqual(result, {
transactionCategory: TRANSACTION_CATEGORIES.SENT_ETHER,
type: TRANSACTION_TYPES.SENT_ETHER,
getCodeResponse: '0x',
});
});
it('should return a simple send transactionCategory with a null getCodeResponse when to is truthy and there is data and but getCode returns an error', async function () {
const result = await txController._determineTransactionCategory({
it('should return a simple send type with a null getCodeResponse when to is truthy and there is data and but getCode returns an error', async function () {
const result = await txController._determineTransactionType({
to: '0xabc',
data: '0xabd',
});
assert.deepEqual(result, {
transactionCategory: TRANSACTION_CATEGORIES.SENT_ETHER,
type: TRANSACTION_TYPES.SENT_ETHER,
getCodeResponse: null,
});
});
it('should return a contract interaction transactionCategory with the correct getCodeResponse when to is truthy and there is data and it is not a token transaction', async function () {
it('should return a contract interaction type with the correct getCodeResponse when to is truthy and there is data and it is not a token transaction', async function () {
const _providerResultStub = {
// 1 gwei
eth_gasPrice: '0x0de0b6b3a7640000',
@ -875,17 +962,17 @@ describe('Transaction Controller', function () {
}),
getParticipateInMetrics: () => false,
});
const result = await _txController._determineTransactionCategory({
const result = await _txController._determineTransactionType({
to: '0x9e673399f795D01116e9A8B2dD2F156705131ee9',
data: 'abd',
});
assert.deepEqual(result, {
transactionCategory: TRANSACTION_CATEGORIES.CONTRACT_INTERACTION,
type: TRANSACTION_TYPES.CONTRACT_INTERACTION,
getCodeResponse: '0x0a',
});
});
it('should return a contract interaction transactionCategory with the correct getCodeResponse when to is a contract address and data is falsy', async function () {
it('should return a contract interaction type with the correct getCodeResponse when to is a contract address and data is falsy', async function () {
const _providerResultStub = {
// 1 gwei
eth_gasPrice: '0x0de0b6b3a7640000',
@ -915,12 +1002,12 @@ describe('Transaction Controller', function () {
}),
getParticipateInMetrics: () => false,
});
const result = await _txController._determineTransactionCategory({
const result = await _txController._determineTransactionType({
to: '0x9e673399f795D01116e9A8B2dD2F156705131ee9',
data: '',
});
assert.deepEqual(result, {
transactionCategory: TRANSACTION_CATEGORIES.CONTRACT_INTERACTION,
type: TRANSACTION_TYPES.CONTRACT_INTERACTION,
getCodeResponse: '0x0a',
});
});
@ -928,53 +1015,74 @@ describe('Transaction Controller', function () {
describe('#getPendingTransactions', function () {
it('should show only submitted and approved transactions as pending transaction', function () {
txController.txStateManager._saveTxList([
txController.txStateManager._addTransactionsToState([
{
id: 1,
status: TRANSACTION_STATUSES.UNAPPROVED,
metamaskNetworkId: currentNetworkId,
txParams: {},
txParams: {
to: VALID_ADDRESS,
from: VALID_ADDRESS_TWO,
},
},
{
id: 2,
status: TRANSACTION_STATUSES.REJECTED,
metamaskNetworkId: currentNetworkId,
txParams: {},
txParams: {
to: VALID_ADDRESS,
from: VALID_ADDRESS_TWO,
},
history: [{}],
},
{
id: 3,
status: TRANSACTION_STATUSES.APPROVED,
metamaskNetworkId: currentNetworkId,
txParams: {},
txParams: {
to: VALID_ADDRESS,
from: VALID_ADDRESS_TWO,
},
history: [{}],
},
{
id: 4,
status: TRANSACTION_STATUSES.SIGNED,
metamaskNetworkId: currentNetworkId,
txParams: {},
txParams: {
to: VALID_ADDRESS,
from: VALID_ADDRESS_TWO,
},
history: [{}],
},
{
id: 5,
status: TRANSACTION_STATUSES.SUBMITTED,
metamaskNetworkId: currentNetworkId,
txParams: {},
txParams: {
to: VALID_ADDRESS,
from: VALID_ADDRESS_TWO,
},
history: [{}],
},
{
id: 6,
status: TRANSACTION_STATUSES.CONFIRMED,
metamaskNetworkId: currentNetworkId,
txParams: {},
txParams: {
to: VALID_ADDRESS,
from: VALID_ADDRESS_TWO,
},
history: [{}],
},
{
id: 7,
status: TRANSACTION_STATUSES.FAILED,
metamaskNetworkId: currentNetworkId,
txParams: {},
txParams: {
to: VALID_ADDRESS,
from: VALID_ADDRESS_TWO,
},
history: [{}],
},
]);

@ -1,11 +1,11 @@
import { strict as assert } from 'assert';
import testData from '../../../../../test/data/mock-tx-history.json';
import {
snapshotFromTxMeta,
migrateFromSnapshotsToDiffs,
replayHistory,
generateHistoryEntry,
} from '../../../../../app/scripts/controllers/transactions/lib/tx-state-history-helpers';
import testData from '../../../../data/mock-tx-history.json';
} from './tx-state-history-helpers';
describe('Transaction state history helper', function () {
describe('#snapshotFromTxMeta', function () {

@ -14,6 +14,12 @@ const normalizers = {
gasPrice: (gasPrice) => addHexPrefix(gasPrice),
};
export function normalizeAndValidateTxParams(txParams, lowerCase = true) {
const normalizedTxParams = normalizeTxParams(txParams, lowerCase);
validateTxParams(normalizedTxParams);
return normalizedTxParams;
}
/**
* Normalizes the given txParams
* @param {Object} txParams - The transaction params
@ -49,22 +55,48 @@ export function validateTxParams(txParams) {
);
}
validateFrom(txParams);
validateRecipient(txParams);
if ('value' in txParams) {
const value = txParams.value.toString();
if (value.includes('-')) {
throw ethErrors.rpc.invalidParams(
`Invalid transaction value "${txParams.value}": not a positive number.`,
);
}
Object.entries(txParams).forEach(([key, value]) => {
// validate types
switch (key) {
case 'from':
validateFrom(txParams);
break;
case 'to':
validateRecipient(txParams);
break;
case 'value':
if (typeof value !== 'string') {
throw ethErrors.rpc.invalidParams(
`Invalid transaction params: ${key} is not a string. got: (${value})`,
);
}
if (value.toString().includes('-')) {
throw ethErrors.rpc.invalidParams(
`Invalid transaction value "${value}": not a positive number.`,
);
}
if (value.includes('.')) {
throw ethErrors.rpc.invalidParams(
`Invalid transaction value of "${txParams.value}": number must be in wei.`,
);
if (value.toString().includes('.')) {
throw ethErrors.rpc.invalidParams(
`Invalid transaction value of "${value}": number must be in wei.`,
);
}
break;
case 'chainId':
if (typeof value !== 'number' && typeof value !== 'string') {
throw ethErrors.rpc.invalidParams(
`Invalid transaction params: ${key} is not a Number or hex string. got: (${value})`,
);
}
break;
default:
if (typeof value !== 'string') {
throw ethErrors.rpc.invalidParams(
`Invalid transaction params: ${key} is not a string. got: (${value})`,
);
}
}
}
});
}
/**

@ -1,5 +1,5 @@
import { strict as assert } from 'assert';
import * as txUtils from '../../../../../app/scripts/controllers/transactions/lib/util';
import * as txUtils from './util';
describe('txUtils', function () {
describe('#validateTxParams', function () {

@ -1,8 +1,8 @@
import { strict as assert } from 'assert';
import sinon from 'sinon';
import BN from 'bn.js';
import PendingTransactionTracker from '../../../../../app/scripts/controllers/transactions/pending-tx-tracker';
import { TRANSACTION_STATUSES } from '../../../../../shared/constants/transaction';
import { TRANSACTION_STATUSES } from '../../../../shared/constants/transaction';
import PendingTransactionTracker from './pending-tx-tracker';
describe('PendingTransactionTracker', function () {
describe('#resubmitPendingTxs', function () {

@ -1,7 +1,7 @@
import { strict as assert } from 'assert';
import Transaction from 'ethereumjs-tx';
import { hexToBn, bnToHex } from '../../../../../app/scripts/lib/util';
import TxUtils from '../../../../../app/scripts/controllers/transactions/tx-gas-utils';
import { hexToBn, bnToHex } from '../../lib/util';
import TxUtils from './tx-gas-utils';
describe('txUtils', function () {
let txUtils;

@ -1,7 +1,8 @@
import EventEmitter from 'safe-event-emitter';
import { ObservableStore } from '@metamask/obs-store';
import log from 'loglevel';
import createId from '../../lib/random-id';
import { keyBy, mapValues, omitBy, pickBy, sortBy } from 'lodash';
import createId from '../../../../shared/modules/random-id';
import { TRANSACTION_STATUSES } from '../../../../shared/constants/transaction';
import { METAMASK_CONTROLLER_EVENTS } from '../../metamask-controller';
import { transactionMatchesNetwork } from '../../../../shared/modules/transaction.utils';
@ -10,11 +11,29 @@ import {
replayHistory,
snapshotFromTxMeta,
} from './lib/tx-state-history-helpers';
import { getFinalStates, normalizeTxParams } from './lib/util';
import { getFinalStates, normalizeAndValidateTxParams } from './lib/util';
/**
* TransactionStatuses reimported from the shared transaction constants file
* @typedef {import('../../../../shared/constants/transaction').TransactionStatuses} TransactionStatuses
* @typedef {import(
* '../../../../shared/constants/transaction'
* ).TransactionStatusString} TransactionStatusString
*/
/**
* @typedef {import('../../../../shared/constants/transaction').TxParams} TxParams
*/
/**
* @typedef {import(
* '../../../../shared/constants/transaction'
* ).TransactionMeta} TransactionMeta
*/
/**
* @typedef {Object} TransactionState
* @property {Record<string, TransactionMeta>} transactions - TransactionMeta
* keyed by the transaction's id.
*/
/**
@ -22,7 +41,8 @@ import { getFinalStates, normalizeTxParams } from './lib/util';
* storing the transaction. It also has some convenience methods for finding
* subsets of transactions.
* @param {Object} opts
* @param {Object} [opts.initState={ transactions: [] }] - initial transactions list with the key transaction {Array}
* @param {TransactionState} [opts.initState={ transactions: {} }] - initial
* transactions list keyed by id
* @param {number} [opts.txHistoryLimit] - limit for how many finished
* transactions can hang around in state
* @param {Function} opts.getNetwork - return network number
@ -32,15 +52,25 @@ export default class TransactionStateManager extends EventEmitter {
constructor({ initState, txHistoryLimit, getNetwork, getCurrentChainId }) {
super();
this.store = new ObservableStore({ transactions: [], ...initState });
this.store = new ObservableStore({
transactions: {},
...initState,
});
this.txHistoryLimit = txHistoryLimit;
this.getNetwork = getNetwork;
this.getCurrentChainId = getCurrentChainId;
}
/**
* @param {Object} opts - the object to use when overwriting defaults
* @returns {txMeta} the default txMeta object
* Generates a TransactionMeta object consisting of the fields required for
* use throughout the extension. The argument here will override everything
* in the resulting transaction meta.
*
* TODO: Don't overwrite everything?
*
* @param {Partial<TransactionMeta>} opts - the object to use when
* overwriting default keys of the TransactionMeta
* @returns {TransactionMeta} the default txMeta object
*/
generateTxMeta(opts) {
const netId = this.getNetwork();
@ -60,100 +90,70 @@ export default class TransactionStateManager extends EventEmitter {
}
/**
* Returns the full tx list for the current network
*
* The list is iterated backwards as new transactions are pushed onto it.
* Get an object containing all unapproved transactions for the current
* network. This is the only transaction fetching method that returns an
* object, so it doesn't use getTransactions like everything else.
*
* @param {number} [limit] - a limit for the number of transactions to return
* @returns {Object[]} The {@code txMeta}s, filtered to the current network
*/
getTxList(limit) {
const network = this.getNetwork();
const chainId = this.getCurrentChainId();
const fullTxList = this.getFullTxList();
const nonces = new Set();
const txs = [];
for (let i = fullTxList.length - 1; i > -1; i--) {
const txMeta = fullTxList[i];
if (transactionMatchesNetwork(txMeta, chainId, network) === false) {
continue;
}
if (limit !== undefined) {
const { nonce } = txMeta.txParams;
if (!nonces.has(nonce)) {
if (nonces.size < limit) {
nonces.add(nonce);
} else {
continue;
}
}
}
txs.unshift(txMeta);
}
return txs;
}
/**
* @returns {Array} of all the txMetas in store
*/
getFullTxList() {
return this.store.getState().transactions;
}
/**
* @returns {Array} the tx list with unapproved status
* @returns {Record<string, TransactionMeta>} Unapproved transactions keyed
* by id
*/
getUnapprovedTxList() {
const txList = this.getTxsByMetaData(
'status',
TRANSACTION_STATUSES.UNAPPROVED,
const chainId = this.getCurrentChainId();
const network = this.getNetwork();
return pickBy(
this.store.getState().transactions,
(transaction) =>
transaction.status === TRANSACTION_STATUSES.UNAPPROVED &&
transactionMatchesNetwork(transaction, chainId, network),
);
return txList.reduce((result, tx) => {
result[tx.id] = tx;
return result;
}, {});
}
/**
* @param {string} [address] - hex prefixed address to sort the txMetas for [optional]
* @returns {Array} the tx list with approved status if no address is provide
* returns all txMetas with approved statuses for the current network
* Get all approved transactions for the current network. If an address is
* provided, the list will be further refined to only those transactions
* originating from the supplied address.
*
* @param {string} [address] - hex prefixed address to find transactions for.
* @returns {TransactionMeta[]} the filtered list of transactions
*/
getApprovedTransactions(address) {
const opts = { status: TRANSACTION_STATUSES.APPROVED };
const searchCriteria = { status: TRANSACTION_STATUSES.APPROVED };
if (address) {
opts.from = address;
searchCriteria.from = address;
}
return this.getFilteredTxList(opts);
return this.getTransactions({ searchCriteria });
}
/**
* @param {string} [address] - hex prefixed address to sort the txMetas for [optional]
* @returns {Array} the tx list submitted status if no address is provide
* returns all txMetas with submitted statuses for the current network
* Get all pending transactions for the current network. If an address is
* provided, the list will be further refined to only those transactions
* originating from the supplied address.
*
* @param {string} [address] - hex prefixed address to find transactions for.
* @returns {TransactionMeta[]} the filtered list of transactions
*/
getPendingTransactions(address) {
const opts = { status: TRANSACTION_STATUSES.SUBMITTED };
const searchCriteria = { status: TRANSACTION_STATUSES.SUBMITTED };
if (address) {
opts.from = address;
searchCriteria.from = address;
}
return this.getFilteredTxList(opts);
return this.getTransactions({ searchCriteria });
}
/**
@param {string} [address] - hex prefixed address to sort the txMetas for [optional]
@returns {Array} the tx list whose status is confirmed if no address is provide
returns all txMetas who's status is confirmed for the current network
*/
* Get all confirmed transactions for the current network. If an address is
* provided, the list will be further refined to only those transactions
* originating from the supplied address.
*
* @param {string} [address] - hex prefixed address to find transactions for.
* @returns {TransactionMeta[]} the filtered list of transactions
*/
getConfirmedTransactions(address) {
const opts = { status: TRANSACTION_STATUSES.CONFIRMED };
const searchCriteria = { status: TRANSACTION_STATUSES.CONFIRMED };
if (address) {
opts.from = address;
searchCriteria.from = address;
}
return this.getFilteredTxList(opts);
return this.getTransactions({ searchCriteria });
}
/**
@ -162,13 +162,14 @@ export default class TransactionStateManager extends EventEmitter {
* is in its final state.
* it will also add the key `history` to the txMeta with the snap shot of
* the original object
* @param {Object} txMeta
* @returns {Object} the txMeta
* @param {TransactionMeta} txMeta - The TransactionMeta object to add.
* @returns {TransactionMeta} The same TransactionMeta, but with validated
* txParams and transaction history.
*/
addTx(txMeta) {
addTransaction(txMeta) {
// normalize and validate txParams if present
if (txMeta.txParams) {
txMeta.txParams = this.normalizeAndValidateTxParams(txMeta.txParams);
txMeta.txParams = normalizeAndValidateTxParams(txMeta.txParams, false);
}
this.once(`${txMeta.id}:signed`, () => {
@ -183,42 +184,43 @@ export default class TransactionStateManager extends EventEmitter {
const snapshot = snapshotFromTxMeta(txMeta);
txMeta.history.push(snapshot);
const transactions = this.getFullTxList();
const transactions = this.getTransactions({
filterToCurrentNetwork: false,
});
const txCount = transactions.length;
const { txHistoryLimit } = this;
// checks if the length of the tx history is
// longer then desired persistence limit
// and then if it is removes only confirmed
// or rejected tx's.
// not tx's that are pending or unapproved
// checks if the length of the tx history is longer then desired persistence
// limit and then if it is removes the oldest confirmed or rejected tx.
// Pending or unapproved transactions will not be removed by this
// operation.
//
// TODO: we are already limiting what we send to the UI, and in the future
// we will send UI only collected groups of transactions *per page* so at
// some point in the future, this persistence limit can be adjusted. When
// we do that I think we should figure out a better storage solution for
// transaction history entries.
if (txCount > txHistoryLimit - 1) {
const index = transactions.findIndex((metaTx) => {
return getFinalStates().includes(metaTx.status);
});
if (index !== -1) {
transactions.splice(index, 1);
this._deleteTransaction(transactions[index].id);
}
}
const newTxIndex = transactions.findIndex(
(currentTxMeta) => currentTxMeta.time > txMeta.time,
);
newTxIndex === -1
? transactions.push(txMeta)
: transactions.splice(newTxIndex, 0, txMeta);
this._saveTxList(transactions);
this._addTransactionsToState([txMeta]);
return txMeta;
}
/**
* @param {number} txId
* @returns {Object} the txMeta who matches the given id if none found
* @returns {TransactionMeta} the txMeta who matches the given id if none found
* for the network returns undefined
*/
getTx(txId) {
const txMeta = this.getTxsByMetaData('id', txId)[0];
return txMeta;
getTransaction(txId) {
const { transactions } = this.store.getState();
return transactions[txId];
}
/**
@ -226,10 +228,10 @@ export default class TransactionStateManager extends EventEmitter {
* @param {Object} txMeta - the txMeta to update
* @param {string} [note] - a note about the update for history
*/
updateTx(txMeta, note) {
updateTransaction(txMeta, note) {
// normalize and validate txParams if present
if (txMeta.txParams) {
txMeta.txParams = this.normalizeAndValidateTxParams(txMeta.txParams);
txMeta.txParams = normalizeAndValidateTxParams(txMeta.txParams, false);
}
// create txMeta snapshot for history
@ -244,232 +246,277 @@ export default class TransactionStateManager extends EventEmitter {
// commit txMeta to state
const txId = txMeta.id;
const txList = this.getFullTxList();
const index = txList.findIndex((txData) => txData.id === txId);
txList[index] = txMeta;
this._saveTxList(txList);
this.store.updateState({
transactions: {
...this.store.getState().transactions,
[txId]: txMeta,
},
});
}
/**
* merges txParams obj onto txMeta.txParams use extend to ensure
* that all fields are filled
* @param {number} txId - the id of the txMeta
* @param {Object} txParams - the updated txParams
* SearchCriteria can search in any key in TxParams or the base
* TransactionMeta. This type represents any key on either of those two
* types.
* @typedef {TxParams[keyof TxParams] | TransactionMeta[keyof TransactionMeta]} SearchableKeys
*/
updateTxParams(txId, txParams) {
const txMeta = this.getTx(txId);
txMeta.txParams = { ...txMeta.txParams, ...txParams };
this.updateTx(txMeta, `txStateManager#updateTxParams`);
}
/**
* normalize and validate txParams members
* @param {Object} txParams - txParams
* Predicates can either be strict values, which is shorthand for using
* strict equality, or a method that receives he value of the specified key
* and returns a boolean.
* @typedef {(v: unknown) => boolean | unknown} FilterPredicate
*/
normalizeAndValidateTxParams(txParams) {
if (typeof txParams.data === 'undefined') {
delete txParams.data;
}
// eslint-disable-next-line no-param-reassign
txParams = normalizeTxParams(txParams, false);
this.validateTxParams(txParams);
return txParams;
}
/**
* validates txParams members by type
* @param {Object} txParams - txParams to validate
* Retrieve a list of transactions from state. By default this will return
* the full list of Transactions for the currently selected chain/network.
* Additional options can be provided to change what is included in the final
* list.
*
* @param opts - options to change filter behavior
* @param {Record<SearchableKeys, FilterPredicate>} [opts.searchCriteria] -
* an object with keys that match keys in TransactionMeta or TxParams, and
* values that are predicates. Predicates can either be strict values,
* which is shorthand for using strict equality, or a method that receives
* the value of the specified key and returns a boolean. The transaction
* list will be filtered to only those items that the predicate returns
* truthy for. **HINT**: `err: undefined` is like looking for a tx with no
* err. so you can also search txs that don't have something as well by
* setting the value as undefined.
* @param {TransactionMeta[]} [opts.initialList] - If provided the filtering
* will occur on the provided list. By default this will be the full list
* from state sorted by time ASC.
* @param {boolean} [opts.filterToCurrentNetwork=true] - Filter transaction
* list to only those that occurred on the current chain or network.
* Defaults to true.
* @param {number} [opts.limit] - limit the number of transactions returned
* to N unique nonces.
* @returns {TransactionMeta[]} The TransactionMeta objects that all provided
* predicates return truthy for.
*/
validateTxParams(txParams) {
Object.keys(txParams).forEach((key) => {
const value = txParams[key];
// validate types
switch (key) {
case 'chainId':
if (typeof value !== 'number' && typeof value !== 'string') {
throw new Error(
`${key} in txParams is not a Number or hex string. got: (${value})`,
);
}
break;
default:
if (typeof value !== 'string') {
throw new Error(
`${key} in txParams is not a string. got: (${value})`,
);
}
break;
}
});
}
/**
@param {Object} opts - an object of fields to search for eg:<br>
let <code>thingsToLookFor = {<br>
to: '0x0..',<br>
from: '0x0..',<br>
status: 'signed', \\ (status) => status !== 'rejected' give me all txs who's status is not rejected<br>
err: undefined,<br>
}<br></code>
optionally the values of the keys can be functions for situations like where
you want all but one status.
@param {Array} [initialList=this.getTxList()]
@returns {Array} array of txMeta with all
options matching
*/
/*
****************HINT****************
| `err: undefined` is like looking |
| for a tx with no err |
| so you can also search txs that |
| dont have something as well by |
| setting the value as undefined |
************************************
this is for things like filtering a the tx list
for only tx's from 1 account
or for filtering for all txs from one account
and that have been 'confirmed'
*/
getFilteredTxList(opts, initialList) {
let filteredTxList = initialList;
Object.keys(opts).forEach((key) => {
filteredTxList = this.getTxsByMetaData(key, opts[key], filteredTxList);
getTransactions({
searchCriteria = {},
initialList,
filterToCurrentNetwork = true,
limit,
} = {}) {
const chainId = this.getCurrentChainId();
const network = this.getNetwork();
// searchCriteria is an object that might have values that aren't predicate
// methods. When providing any other value type (string, number, etc), we
// consider this shorthand for "check the value at key for strict equality
// with the provided value". To conform this object to be only methods, we
// mapValues (lodash) such that every value on the object is a method that
// returns a boolean.
const predicateMethods = mapValues(searchCriteria, (predicate) => {
return typeof predicate === 'function'
? predicate
: (v) => v === predicate;
});
return filteredTxList;
}
/**
* @param {string} key - the key to check
* @param {any} value - the value your looking for can also be a function that returns a bool
* @param {Array} [txList=this.getTxList()] - the list to search. default is the txList
* from txStateManager#getTxList
* @returns {Array} a list of txMetas who matches the search params
*/
getTxsByMetaData(key, value, txList = this.getTxList()) {
const filter = typeof value === 'function' ? value : (v) => v === value;
// If an initial list is provided we need to change it back into an object
// first, so that it matches the shape of our state. This is done by the
// lodash keyBy method. This is the edge case for this method, typically
// initialList will be undefined.
const transactionsToFilter = initialList
? keyBy(initialList, 'id')
: this.store.getState().transactions;
// Combine sortBy and pickBy to transform our state object into an array of
// matching transactions that are sorted by time.
const filteredTransactions = sortBy(
pickBy(transactionsToFilter, (transaction) => {
// default matchesCriteria to the value of transactionMatchesNetwork
// when filterToCurrentNetwork is true.
if (
filterToCurrentNetwork &&
transactionMatchesNetwork(transaction, chainId, network) === false
) {
return false;
}
// iterate over the predicateMethods keys to check if the transaction
// matches the searchCriteria
for (const [key, predicate] of Object.entries(predicateMethods)) {
// We return false early as soon as we know that one of the specified
// search criteria do not match the transaction. This prevents
// needlessly checking all criteria when we already know the criteria
// are not fully satisfied. We check both txParams and the base
// object as predicate keys can be either.
if (key in transaction.txParams) {
if (predicate(transaction.txParams[key]) === false) {
return false;
}
} else if (predicate(transaction[key]) === false) {
return false;
}
}
return txList.filter((txMeta) => {
if (key in txMeta.txParams) {
return filter(txMeta.txParams[key]);
return true;
}),
'time',
);
if (limit !== undefined) {
// We need to have all transactions of a given nonce in order to display
// necessary details in the UI. We use the size of this set to determine
// whether we have reached the limit provided, thus ensuring that all
// transactions of nonces we include will be sent to the UI.
const nonces = new Set();
const txs = [];
// By default, the transaction list we filter from is sorted by time ASC.
// To ensure that filtered results prefers the newest transactions we
// iterate from right to left, inserting transactions into front of a new
// array. The original order is preserved, but we ensure that newest txs
// are preferred.
for (let i = filteredTransactions.length - 1; i > -1; i--) {
const txMeta = filteredTransactions[i];
const { nonce } = txMeta.txParams;
if (!nonces.has(nonce)) {
if (nonces.size < limit) {
nonces.add(nonce);
} else {
continue;
}
}
// Push transaction into the beginning of our array to ensure the
// original order is preserved.
txs.unshift(txMeta);
}
return filter(txMeta[key]);
});
}
// get::set status
/**
* @param {number} txId - the txMeta Id
* @returns {string} the status of the tx.
*/
getTxStatus(txId) {
const txMeta = this.getTx(txId);
return txMeta.status;
return txs;
}
return filteredTransactions;
}
/**
* Update the status of the tx to 'rejected'.
* @param {number} txId - the txMeta Id
* Update status of the TransactionMeta with provided id to 'rejected'.
* After setting the status, the TransactionMeta is deleted from state.
*
* TODO: Should we show historically rejected transactions somewhere in the
* UI? Seems like it could be valuable for information purposes. Of course
* only after limit issues are reduced.
*
* @param {number} txId - the target TransactionMeta's Id
*/
setTxStatusRejected(txId) {
this._setTxStatus(txId, 'rejected');
this._removeTx(txId);
this._setTransactionStatus(txId, 'rejected');
this._deleteTransaction(txId);
}
/**
* Update the status of the tx to 'unapproved'.
* @param {number} txId - the txMeta Id
* Update status of the TransactionMeta with provided id to 'unapproved'
*
* @param {number} txId - the target TransactionMeta's Id
*/
setTxStatusUnapproved(txId) {
this._setTxStatus(txId, TRANSACTION_STATUSES.UNAPPROVED);
this._setTransactionStatus(txId, TRANSACTION_STATUSES.UNAPPROVED);
}
/**
* Update the status of the tx to 'approved'.
* @param {number} txId - the txMeta Id
* Update status of the TransactionMeta with provided id to 'approved'
*
* @param {number} txId - the target TransactionMeta's Id
*/
setTxStatusApproved(txId) {
this._setTxStatus(txId, TRANSACTION_STATUSES.APPROVED);
this._setTransactionStatus(txId, TRANSACTION_STATUSES.APPROVED);
}
/**
* Update the status of the tx to 'signed'.
* @param {number} txId - the txMeta Id
* Update status of the TransactionMeta with provided id to 'signed'
*
* @param {number} txId - the target TransactionMeta's Id
*/
setTxStatusSigned(txId) {
this._setTxStatus(txId, TRANSACTION_STATUSES.SIGNED);
this._setTransactionStatus(txId, TRANSACTION_STATUSES.SIGNED);
}
/**
* Update the status of the tx to 'submitted' and add a time stamp
* for when it was called
* @param {number} txId - the txMeta Id
* Update status of the TransactionMeta with provided id to 'submitted'
* and sets the 'submittedTime' property with the current Unix epoch time.
*
* @param {number} txId - the target TransactionMeta's Id
*/
setTxStatusSubmitted(txId) {
const txMeta = this.getTx(txId);
const txMeta = this.getTransaction(txId);
txMeta.submittedTime = new Date().getTime();
this.updateTx(txMeta, 'txStateManager - add submitted time stamp');
this._setTxStatus(txId, TRANSACTION_STATUSES.SUBMITTED);
this.updateTransaction(txMeta, 'txStateManager - add submitted time stamp');
this._setTransactionStatus(txId, TRANSACTION_STATUSES.SUBMITTED);
}
/**
* Update the status of the tx to 'confirmed'.
* @param {number} txId - the txMeta Id
* Update status of the TransactionMeta with provided id to 'confirmed'
*
* @param {number} txId - the target TransactionMeta's Id
*/
setTxStatusConfirmed(txId) {
this._setTxStatus(txId, TRANSACTION_STATUSES.CONFIRMED);
this._setTransactionStatus(txId, TRANSACTION_STATUSES.CONFIRMED);
}
/**
* Update the status of the tx to 'dropped'.
* @param {number} txId - the txMeta Id
* Update status of the TransactionMeta with provided id to 'dropped'
*
* @param {number} txId - the target TransactionMeta's Id
*/
setTxStatusDropped(txId) {
this._setTxStatus(txId, TRANSACTION_STATUSES.DROPPED);
this._setTransactionStatus(txId, TRANSACTION_STATUSES.DROPPED);
}
/**
* Updates the status of the tx to 'failed' and put the error on the txMeta
* @param {number} txId - the txMeta Id
* @param {erroObject} err - error object
* Update status of the TransactionMeta with provided id to 'failed' and put
* the error on the TransactionMeta object.
*
* @param {number} txId - the target TransactionMeta's Id
* @param {Error} err - error object
*/
setTxStatusFailed(txId, err) {
const error = err || new Error('Internal metamask failure');
const txMeta = this.getTx(txId);
const txMeta = this.getTransaction(txId);
txMeta.err = {
message: error.toString(),
rpc: error.value,
stack: error.stack,
};
this.updateTx(txMeta, 'transactions:tx-state-manager#fail - add error');
this._setTxStatus(txId, TRANSACTION_STATUSES.FAILED);
this.updateTransaction(
txMeta,
'transactions:tx-state-manager#fail - add error',
);
this._setTransactionStatus(txId, TRANSACTION_STATUSES.FAILED);
}
/**
* Removes transaction from the given address for the current network
* from the txList
* Removes all transactions for the given address on the current network,
* preferring chainId for comparison over networkId.
*
* @param {string} address - hex string of the from address on the txParams
* to remove
*/
wipeTransactions(address) {
// network only tx
const txs = this.getFullTxList();
const { transactions } = this.store.getState();
const network = this.getNetwork();
const chainId = this.getCurrentChainId();
// Filter out the ones from the current account and network
const otherAccountTxs = txs.filter(
(txMeta) =>
!(
txMeta.txParams.from === address &&
transactionMatchesNetwork(txMeta, chainId, network)
),
);
// Update state
this._saveTxList(otherAccountTxs);
this.store.updateState({
transactions: omitBy(
transactions,
(transaction) =>
transaction.txParams.from === address &&
transactionMatchesNetwork(transaction, chainId, network),
),
});
}
/**
* Filters out the unapproved transactions from state
*/
clearUnapprovedTxs() {
this.store.updateState({
transactions: omitBy(
this.store.getState().transactions,
(transaction) => transaction.status === TRANSACTION_STATUSES.UNAPPROVED,
),
});
}
//
@ -477,14 +524,37 @@ export default class TransactionStateManager extends EventEmitter {
//
/**
* @param {number} txId - the txMeta Id
* @param {TransactionStatuses[keyof TransactionStatuses]} status - the status to set on the txMeta
* @emits tx:status-update - passes txId and status
* @emits ${txMeta.id}:finished - if it is a finished state. Passes the txMeta
* @emits 'updateBadge'
* Updates a transaction's status in state, and then emits events that are
* subscribed to elsewhere. See below for best guesses on where and how these
* events are received.
* @param {number} txId - the TransactionMeta Id
* @param {TransactionStatusString} status - the status to set on the
* TransactionMeta
* @emits txMeta.id:txMeta.status - every time a transaction's status changes
* we emit the change passing along the id. This does not appear to be used
* outside of this file, which only listens to this to unsubscribe listeners
* of :rejected and :signed statuses when the inverse status changes. Likely
* safe to drop.
* @emits tx:status-update - every time a transaction's status changes we
* emit this event and pass txId and status. This event is subscribed to in
* the TransactionController and re-broadcast by the TransactionController.
* It is used internally within the TransactionController to try and update
* pending transactions on each new block (from blockTracker). It's also
* subscribed to in metamask-controller to display a browser notification on
* confirmed or failed transactions.
* @emits txMeta.id:finished - When a transaction moves to a finished state
* this event is emitted, which is used in the TransactionController to pass
* along details of the transaction to the dapp that suggested them. This
* pattern is replicated across all of the message managers and can likely
* be supplemented or replaced by the ApprovalController.
* @emits updateBadge - When the number of transactions changes in state,
* the badge in the browser extension bar should be updated to reflect the
* number of pending transactions. This particular emit doesn't appear to
* bubble up anywhere that is actually used. TransactionController emits
* this *anytime the state changes*, so this is probably superfluous.
*/
_setTxStatus(txId, status) {
const txMeta = this.getTx(txId);
_setTransactionStatus(txId, status) {
const txMeta = this.getTransaction(txId);
if (!txMeta) {
return;
@ -492,7 +562,10 @@ export default class TransactionStateManager extends EventEmitter {
txMeta.status = status;
try {
this.updateTx(txMeta, `txStateManager: setting status to ${status}`);
this.updateTransaction(
txMeta,
`txStateManager: setting status to ${status}`,
);
this.emit(`${txMeta.id}:${status}`, txId);
this.emit(`tx:status-update`, txId, status);
if (
@ -511,26 +584,32 @@ export default class TransactionStateManager extends EventEmitter {
}
/**
* Saves the new/updated txList. Intended only for internal use
* @param {Array} transactions - the list of transactions to save
* Adds one or more transactions into state. This is not intended for
* external use.
*
* @private
* @param {TransactionMeta[]} transactions - the list of transactions to save
*/
_saveTxList(transactions) {
this.store.updateState({ transactions });
}
_removeTx(txId) {
const transactionList = this.getFullTxList();
this._saveTxList(transactionList.filter((txMeta) => txMeta.id !== txId));
_addTransactionsToState(transactions) {
this.store.updateState({
transactions: transactions.reduce((result, newTx) => {
result[newTx.id] = newTx;
return result;
}, this.store.getState().transactions),
});
}
/**
* Filters out the unapproved transactions
* removes one transaction from state. This is not intended for external use.
*
* @private
* @param {number} targetTransactionId - the transaction to delete
*/
clearUnapprovedTxs() {
const transactions = this.getFullTxList();
const nonUnapprovedTxs = transactions.filter(
(tx) => tx.status !== TRANSACTION_STATUSES.UNAPPROVED,
);
this._saveTxList(nonUnapprovedTxs);
_deleteTransaction(targetTransactionId) {
const { transactions } = this.store.getState();
delete transactions[targetTransactionId];
this.store.updateState({
transactions,
});
}
}

@ -1,6 +1,6 @@
import assert from 'assert';
import { ObservableStore } from '@metamask/obs-store';
import ComposableObservableStore from '../../../app/scripts/lib/ComposableObservableStore';
import ComposableObservableStore from './ComposableObservableStore';
describe('ComposableObservableStore', function () {
it('should register initial state', function () {

@ -1,18 +1,26 @@
import {
GOERLI_CHAIN_ID,
KOVAN_CHAIN_ID,
MAINNET_CHAIN_ID,
RINKEBY_CHAIN_ID,
ROPSTEN_CHAIN_ID,
} from '../../../shared/constants/network';
/**
* Gives the caller a url at which the user can acquire eth, depending on the network they are in
*
* @param {Object} opts - Options required to determine the correct url
* @param {string} opts.network - The network for which to return a url
* @param {string} opts.address - The address the bought ETH should be sent to. Only relevant if network === '1'.
* @returns {string|undefined} The url at which the user can access ETH, while in the given network. If the passed
* network does not match any of the specified cases, or if no network is given, returns undefined.
* @param {string} opts.chainId - The chainId for which to return a url
* @param {string} opts.address - The address the bought ETH should be sent to. Only relevant if chainId === '0x1'.
* @returns {string|undefined} The url at which the user can access ETH, while in the given chain. If the passed
* chainId does not match any of the specified cases, or if no chainId is given, returns undefined.
*
*/
export default function getBuyEthUrl({ network, address, service }) {
export default function getBuyEthUrl({ chainId, address, service }) {
// default service by network if not specified
if (!service) {
// eslint-disable-next-line no-param-reassign
service = getDefaultServiceForNetwork(network);
service = getDefaultServiceForChain(chainId);
}
switch (service) {
@ -33,21 +41,21 @@ export default function getBuyEthUrl({ network, address, service }) {
}
}
function getDefaultServiceForNetwork(network) {
switch (network) {
case '1':
function getDefaultServiceForChain(chainId) {
switch (chainId) {
case MAINNET_CHAIN_ID:
return 'wyre';
case '3':
case ROPSTEN_CHAIN_ID:
return 'metamask-faucet';
case '4':
case RINKEBY_CHAIN_ID:
return 'rinkeby-faucet';
case '42':
case KOVAN_CHAIN_ID:
return 'kovan-faucet';
case '5':
case GOERLI_CHAIN_ID:
return 'goerli-faucet';
default:
throw new Error(
`No default cryptocurrency exchange or faucet for networkId: "${network}"`,
`No default cryptocurrency exchange or faucet for chainId: "${chainId}"`,
);
}
}

@ -1,20 +1,26 @@
import assert from 'assert';
import getBuyEthUrl from '../../../app/scripts/lib/buy-eth-url';
import {
KOVAN_CHAIN_ID,
MAINNET_CHAIN_ID,
RINKEBY_CHAIN_ID,
ROPSTEN_CHAIN_ID,
} from '../../../shared/constants/network';
import getBuyEthUrl from './buy-eth-url';
describe('buy-eth-url', function () {
const mainnet = {
network: '1',
chainId: MAINNET_CHAIN_ID,
amount: 5,
address: '0x0dcd5d886577d5081b0c52e242ef29e70be3e7bc',
};
const ropsten = {
network: '3',
chainId: ROPSTEN_CHAIN_ID,
};
const rinkeby = {
network: '4',
chainId: RINKEBY_CHAIN_ID,
};
const kovan = {
network: '42',
chainId: KOVAN_CHAIN_ID,
};
it('returns wyre url with address for network 1', function () {

@ -1,5 +1,5 @@
import assert from 'assert';
import cleanErrorStack from '../../../app/scripts/lib/cleanErrorStack';
import cleanErrorStack from './cleanErrorStack';
describe('Clean Error Stack', function () {
const testMessage = 'Test Message';

@ -0,0 +1,33 @@
import { ethErrors, serializeError } from 'eth-rpc-errors';
const createMetaRPCHandler = (api, outStream) => {
return (data) => {
if (!api[data.method]) {
outStream.write({
jsonrpc: '2.0',
error: ethErrors.rpc.methodNotFound({
message: `${data.method} not found`,
}),
id: data.id,
});
return;
}
api[data.method](...data.params, (err, result) => {
if (err) {
outStream.write({
jsonrpc: '2.0',
error: serializeError(err, { shouldIncludeStack: true }),
id: data.id,
});
} else {
outStream.write({
jsonrpc: '2.0',
result,
id: data.id,
});
}
});
};
};
export default createMetaRPCHandler;

@ -0,0 +1,61 @@
import assert from 'assert';
import { obj as createThoughStream } from 'through2';
import createMetaRPCHandler from './createMetaRPCHandler';
describe('createMetaRPCHandler', function () {
it('can call the api when handler receives a JSON-RPC request', function (done) {
const api = {
foo: (param1) => {
assert.strictEqual(param1, 'bar');
done();
},
};
const streamTest = createThoughStream();
const handler = createMetaRPCHandler(api, streamTest);
handler({
id: 1,
method: 'foo',
params: ['bar'],
});
});
it('can write the response to the outstream when api callback is called', function (done) {
const api = {
foo: (param1, cb) => {
assert.strictEqual(param1, 'bar');
cb(null, 'foobarbaz');
},
};
const streamTest = createThoughStream();
const handler = createMetaRPCHandler(api, streamTest);
handler({
id: 1,
method: 'foo',
params: ['bar'],
});
streamTest.on('data', (data) => {
assert.strictEqual(data.result, 'foobarbaz');
streamTest.end();
done();
});
});
it('can write the error to the outstream when api callback is called with an error', function (done) {
const api = {
foo: (param1, cb) => {
assert.strictEqual(param1, 'bar');
cb(new Error('foo-error'));
},
};
const streamTest = createThoughStream();
const handler = createMetaRPCHandler(api, streamTest);
handler({
id: 1,
method: 'foo',
params: ['bar'],
});
streamTest.on('data', (data) => {
assert.strictEqual(data.error.message, 'foo-error');
streamTest.end();
done();
});
});
});

@ -5,8 +5,8 @@ import { ethErrors } from 'eth-rpc-errors';
import log from 'loglevel';
import { MESSAGE_TYPE } from '../../../shared/constants/app';
import { METAMASK_CONTROLLER_EVENTS } from '../metamask-controller';
import createId from '../../../shared/modules/random-id';
import { addHexPrefix } from './util';
import createId from './random-id';
const hexRe = /^[0-9A-Fa-f]+$/gu;

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

Loading…
Cancel
Save