Merge tag 'v4.5.5'

# Conflicts:
#	app/_locales/ja/messages.json
#	package-lock.json

messages.jsonのローカライズ
feature/default_network_editable
nyatla 7 years ago
commit cc246528b5
  1. 135
      .circleci/config.yml
  2. 4
      .eslintrc
  3. 2
      .gitignore
  4. 36
      CHANGELOG.md
  5. 61
      app/_locales/de/messages.json
  6. 13
      app/_locales/en/messages.json
  7. 2
      app/_locales/es/messages.json
  8. 2
      app/_locales/hn/messages.json
  9. 19
      app/_locales/index.json
  10. 2
      app/_locales/it/messages.json
  11. 48
      app/_locales/ja/messages.json
  12. 18
      app/_locales/nl/messages.json
  13. 2
      app/_locales/pt/messages.json
  14. 424
      app/_locales/ru/messages.json
  15. 2
      app/_locales/sl/messages.json
  16. 2
      app/_locales/th/messages.json
  17. 63
      app/_locales/zh_TW/messages.json
  18. 4
      app/home.html
  19. 10
      app/manifest.json
  20. 2
      app/notification.html
  21. 4
      app/popup.html
  22. 31
      app/scripts/background.js
  23. 2
      app/scripts/config.js
  24. 10
      app/scripts/contentscript.js
  25. 5
      app/scripts/controllers/blacklist.js
  26. 23
      app/scripts/controllers/currency.js
  27. 18
      app/scripts/controllers/infura.js
  28. 5
      app/scripts/controllers/preferences.js
  29. 15
      app/scripts/controllers/shapeshift.js
  30. 73
      app/scripts/controllers/transactions.js
  31. 2
      app/scripts/first-time-state.js
  32. 2
      app/scripts/inpage.js
  33. 27
      app/scripts/lib/extractEthjsErrorMessage.js
  34. 18
      app/scripts/lib/get-first-preferred-lang-code.js
  35. 33
      app/scripts/lib/getObjStructure.js
  36. 27
      app/scripts/lib/migrator/index.js
  37. 5
      app/scripts/lib/nonce-tracker.js
  38. 26
      app/scripts/lib/reportFailedTxToSentry.js
  39. 19
      app/scripts/lib/setupRaven.js
  40. 32
      app/scripts/lib/tx-gas-utils.js
  41. 35
      app/scripts/lib/tx-state-manager.js
  42. 29
      app/scripts/metamask-controller.js
  43. 7
      app/scripts/migrations/013.js
  44. 15
      app/scripts/migrations/015.js
  45. 22
      app/scripts/migrations/016.js
  46. 21
      app/scripts/migrations/017.js
  47. 39
      app/scripts/migrations/018.js
  48. 44
      app/scripts/migrations/019.js
  49. 17
      app/scripts/migrations/022.js
  50. 54
      app/scripts/migrations/023.js
  51. 41
      app/scripts/migrations/024.js
  52. 61
      app/scripts/migrations/025.js
  53. 3
      app/scripts/migrations/index.js
  54. 29
      app/scripts/migrations/template.js
  55. 78
      app/scripts/popup.js
  56. 84
      app/scripts/ui.js
  57. 5
      development/genStates.js
  58. 62
      development/metamaskbot-build-announce.js
  59. 55
      development/sentry-publish.js
  60. 3
      development/states/add-token.json
  61. 3
      development/states/confirm-new-ui.json
  62. 3
      development/states/confirm-sig-requests.json
  63. 3
      development/states/first-time.json
  64. 3
      development/states/send-edit.json
  65. 3
      development/states/send-new-ui.json
  66. 128
      development/states/tx-list-items.js
  67. 131
      development/verify-locale-strings.js
  68. 48
      docs/QA_Guide.md
  69. 9
      docs/translating-guide.md
  70. 629
      gulpfile.js
  71. 23
      mascara/server/index.js
  72. 2
      mascara/src/app/first-time/index.js
  73. 16
      mascara/src/background.js
  74. 0
      mascara/src/metamascara.js
  75. 14
      mascara/src/proxy.js
  76. 54
      mascara/src/ui.js
  77. 2
      mascara/ui/index.html
  78. 3
      old-ui/app/app.js
  79. 1
      old-ui/app/components/buy-button-subview.js
  80. 6
      old-ui/app/components/pending-tx.js
  81. 1
      old-ui/app/components/qr-code.js
  82. 4
      old-ui/app/components/range-slider.js
  83. 7
      old-ui/app/components/transaction-list-item.js
  84. 7
      old-ui/app/config.js
  85. 1
      old-ui/app/util.js
  86. 4
      old-ui/css.js
  87. 7874
      package-lock.json
  88. 153
      package.json
  89. 2
      test/base.conf.js
  90. 18
      test/e2e/func.js
  91. 145
      test/e2e/metamask.spec.js
  92. 2
      test/integration/lib/add-token.js
  93. 61
      test/integration/lib/tx-list-items.js
  94. 18
      test/screens/func.js
  95. 230
      test/screens/new-ui.js
  96. 99
      test/unit/migrations/023-test.js
  97. 49
      test/unit/migrations/024-test.js
  98. 49
      test/unit/migrations/025-test.js
  99. 17
      test/unit/migrations/template-test.js
  100. 33
      test/unit/migrator-test.js
  101. Some files were not shown because too many files have changed in this diff Show More

@ -6,12 +6,28 @@ workflows:
jobs:
- prep-deps-npm
- prep-deps-firefox
- prep-build:
requires:
- prep-deps-npm
- prep-scss:
requires:
- prep-deps-npm
- test-lint:
requires:
- prep-deps-npm
- test-e2e:
requires:
- prep-deps-npm
- prep-build
- job-screens:
requires:
- prep-deps-npm
- prep-build
- job-publish:
requires:
- prep-deps-npm
- prep-build
- job-screens
- test-unit:
requires:
- prep-deps-npm
@ -33,6 +49,16 @@ workflows:
- prep-deps-npm
- prep-deps-firefox
- prep-scss
- all-tests-pass:
requires:
- test-lint
- test-unit
- test-e2e
- job-screens
- test-integration-mascara-chrome
- test-integration-mascara-firefox
- test-integration-flat-chrome
- test-integration-flat-firefox
jobs:
prep-deps-npm:
@ -41,7 +67,7 @@ jobs:
steps:
- checkout
- restore_cache:
key: dependency-cache-{{ checksum "package-lock.json" }}
key: dependency-cache-{{ .Revision }}
- run:
name: Install deps via npm
command: npm install
@ -49,6 +75,10 @@ jobs:
key: dependency-cache-{{ checksum "package-lock.json" }}
paths:
- node_modules
- save_cache:
key: dependency-cache-{{ .Revision }}
paths:
- node_modules
prep-deps-firefox:
docker:
@ -65,6 +95,24 @@ jobs:
paths:
- firefox
prep-build:
docker:
- image: circleci/node:8-browsers
steps:
- checkout
- restore_cache:
key: dependency-cache-{{ .Revision }}
- run:
name: build:dist
command: npm run dist
- run:
name: build:debug
command: find dist/ -type f -exec md5sum {} \; | sort -k 2
- save_cache:
key: build-cache-{{ .Revision }}
paths:
- dist
- builds
prep-scss:
docker:
@ -72,7 +120,7 @@ jobs:
steps:
- checkout
- restore_cache:
key: dependency-cache-{{ checksum "package-lock.json" }}
key: dependency-cache-{{ .Revision }}
- run:
name: Get Scss Cache key
# this allows us to checksum against a whole directory
@ -91,18 +139,81 @@ jobs:
steps:
- checkout
- restore_cache:
key: dependency-cache-{{ checksum "package-lock.json" }}
key: dependency-cache-{{ .Revision }}
- run:
name: Test
command: npm run lint
test-e2e:
docker:
- image: circleci/node:8-browsers
steps:
- checkout
- restore_cache:
key: dependency-cache-{{ .Revision }}
- restore_cache:
key: build-cache-{{ .Revision }}
- run:
name: Test
command: npm run test:e2e
- store_artifacts:
path: test-artifacts
destination: test-artifacts
job-screens:
docker:
- image: circleci/node:8-browsers
steps:
- checkout
- restore_cache:
key: dependency-cache-{{ .Revision }}
- restore_cache:
key: build-cache-{{ .Revision }}
- run:
name: Test
command: npm run test:screens
- save_cache:
key: job-screens-{{ .Revision }}
paths:
- test-artifacts
job-publish:
docker:
- image: circleci/node:8-browsers
steps:
- checkout
- restore_cache:
key: dependency-cache-{{ .Revision }}
- restore_cache:
key: build-cache-{{ .Revision }}
- restore_cache:
key: job-screens-{{ .Revision }}
- store_artifacts:
path: dist/mascara
destination: builds/mascara
- store_artifacts:
path: dist/sourcemaps
destination: builds/sourcemaps
- store_artifacts:
path: builds
destination: builds
- store_artifacts:
path: test-artifacts
destination: test-artifacts
- run:
name: build:announce
command: ./development/metamaskbot-build-announce.js
- run:
name: sentry sourcemaps upload
command: npm run sentry:publish
test-unit:
docker:
- image: circleci/node:8-browsers
steps:
- checkout
- restore_cache:
key: dependency-cache-{{ checksum "package-lock.json" }}
key: dependency-cache-{{ .Revision }}
- run:
name: test:coverage
command: npm run test:coverage
@ -124,7 +235,7 @@ jobs:
&& sudo mv /usr/bin/firefox /usr/bin/firefox-old
&& sudo ln -s /opt/firefox58/firefox /usr/bin/firefox
- restore_cache:
key: dependency-cache-{{ checksum "package-lock.json" }}
key: dependency-cache-{{ .Revision }}
- run:
name: Get Scss Cache key
# this allows us to checksum against a whole directory
@ -143,7 +254,7 @@ jobs:
steps:
- checkout
- restore_cache:
key: dependency-cache-{{ checksum "package-lock.json" }}
key: dependency-cache-{{ .Revision }}
- run:
name: Get Scss Cache key
# this allows us to checksum against a whole directory
@ -171,7 +282,7 @@ jobs:
&& sudo mv /usr/bin/firefox /usr/bin/firefox-old
&& sudo ln -s /opt/firefox58/firefox /usr/bin/firefox
- restore_cache:
key: dependency-cache-{{ checksum "package-lock.json" }}
key: dependency-cache-{{ .Revision }}
- run:
name: Get Scss Cache key
# this allows us to checksum against a whole directory
@ -190,7 +301,7 @@ jobs:
steps:
- checkout
- restore_cache:
key: dependency-cache-{{ checksum "package-lock.json" }}
key: dependency-cache-{{ .Revision }}
- run:
name: Get Scss Cache key
# this allows us to checksum against a whole directory
@ -200,3 +311,11 @@ jobs:
- run:
name: test:integration:mascara
command: npm run test:mascara
all-tests-pass:
docker:
- image: circleci/node:8-browsers
steps:
- run:
name: All Tests Passed
command: echo 'weew - everything passed!'

@ -29,7 +29,8 @@
"plugins": [
"mocha",
"chai",
"react"
"react",
"json"
],
"globals": {
@ -41,6 +42,7 @@
},
"rules": {
"no-restricted-globals": ["error", "event"],
"accessor-pairs": 2,
"arrow-spacing": [2, { "before": true, "after": true }],
"block-spacing": [2, "always"],

2
.gitignore vendored

@ -28,6 +28,8 @@ test/background.js
test/bundle.js
test/test-bundle.js
test-artifacts
#ignore css output and sourcemaps
ui/app/css/output/

@ -2,6 +2,42 @@
## Current Master
## 4.5.5 Fri Apr 06 2018
- Graceful handling of unknown keys in txParams
- Fixes buggy handling of historical transactions with unknown keys in txParams
- Fix link for 'Learn More' in the Add Token Screen to open to a new tab.
- Fix Download State Logs button [#3791](https://github.com/MetaMask/metamask-extension/issues/3791)
- Enhanced migration error handling + reporting
## 4.5.4 (aborted) Thu Apr 05 2018
- Graceful handling of unknown keys in txParams
- Fix link for 'Learn More' in the Add Token Screen to open to a new tab.
- Fix Download State Logs button [#3791](https://github.com/MetaMask/metamask-extension/issues/3791)
- Fix migration error reporting
## 4.5.3 Wed Apr 04 2018
- Fix bug where checksum address are messing with balance issue [#3843](https://github.com/MetaMask/metamask-extension/issues/3843)
- new ui: fix the confirm transaction screen
## 4.5.2 Wed Apr 04 2018
- Fix overly strict validation where transactions were rejected with hex encoded "chainId"
## 4.5.1 Tue Apr 03 2018
- Fix default network (should be mainnet not Rinkeby)
- Fix Sentry automated error reporting endpoint
## 4.5.0 Mon Apr 02 2018
- (beta ui) Internationalization: Select your preferred language in the settings screen
- Internationalization: various locale improvements
- Fix bug where the "Reset account" feature would not clear the network cache.
- Increase maximum gas limit, to allow very gas heavy transactions, since block gas limits have been stable.
## 4.4.0 Mon Mar 26 2018
- Internationalization: Taiwanese, Thai, Slovenian

@ -14,9 +14,15 @@
"address": {
"message": "Adresse"
},
"addCustomToken": {
"message": "Eigenen Token hinzufügen"
},
"addToken": {
"message": "Token hinzufügen"
},
"addTokens": {
"message": "Token hinzufügen"
},
"amount": {
"message": "Betrag"
},
@ -31,9 +37,15 @@
"message": "MetaMask",
"description": "Der Name der Erweiterung"
},
"approved": {
"message": "Genehmigt"
},
"attemptingConnect": {
"message": "Versuch mit der Blockchain zu verbinden."
},
"attributions": {
"message": "Was wir verwenden"
},
"available": {
"message": "Verfügbar"
},
@ -43,6 +55,9 @@
"balance": {
"message": "Guthaben:"
},
"balances": {
"message": "Deine Guthaben"
},
"balanceIsInsufficientGas": {
"message": "Guthaben unzureichend für den aktuellen gesamten Gasbetrag"
},
@ -69,7 +84,7 @@
"message": "Auf Coinbase kaufen"
},
"buyCoinbaseExplainer": {
"message": "Coinbase ist die weltweit bekannteste Möglichkeit bitcoin, ethereum und litecoin zu kaufen und verkaufen."
"message": "Coinbase ist die weltweit bekannteste Art und Weise um bitcoin, ethereum und litecoin zu kaufen und verkaufen."
},
"ok": {
"message": "Ok"
@ -105,7 +120,7 @@
"message": "Zu Coinbase fortfahren"
},
"contractDeployment": {
"message": "Smart Contract ausführen"
"message": "Smart Contract Ausführung"
},
"conversionProgress": {
"message": "Umtausch in Arbeit"
@ -148,7 +163,7 @@
"description": "Börsentyp (Kryptowährungen)"
},
"currentConversion": {
"message": "Aktueller Umtausch"
"message": "Aktuelle Tauschwährung"
},
"currentNetwork": {
"message": "Aktuelles Netzwerk"
@ -185,7 +200,7 @@
"description": "Teilt dem Benutzer mit welchen Token er beim Einzahlen mit Shapeshift ausgewählt hat"
},
"depositEth": {
"message": "Eth einzahlen"
"message": "Eth kaufen"
},
"depositEther": {
"message": "Ether einzahlen"
@ -217,7 +232,7 @@
"done": {
"message": "Fertig"
},
"downloadStatelogs": {
"downloadStateLogs": {
"message": "Statelogs herunterladen"
},
"dropped": {
@ -274,7 +289,7 @@
"message": "Folge uns auf Twitter"
},
"from": {
"message": "von"
"message": "Von"
},
"fromToSame": {
"message": "Ziel- und Ursprungsadresse dürfen nicht identisch sein"
@ -347,14 +362,14 @@
"message": "Es erlaubt dir ether & Token zu halten und dient dir als Verbindung zu dezentralisierten Applikationen."
},
"import": {
"message": "Import",
"message": "Importieren",
"description": "Button um den Account aus einer ausgewählten Datei zu importieren"
},
"importAccount": {
"message": "Account importieren"
},
"importAccountMsg": {
"message":" Importierte Accounts werden nicht mit der Seed Wörterfolge deines ursprünglichen MetaMask Accounts verknüpft. Erfahre mehr über importierte Accounts."
"message":" Importierte Accounts werden nicht mit der Seed-Wörterfolge deines ursprünglichen MetaMask Accounts verknüpft. Erfahre mehr über importierte Accounts."
},
"importAnAccount": {
"message": "Einen Account importieren"
@ -441,10 +456,10 @@
"message": "Frei"
},
"loweCaseWords": {
"message": "Die Wörter der Seed Wörterfolgen sind alle kleingeschrieben"
"message": "Die Wörter der Seed-Wörterfolgen sind alle kleingeschrieben"
},
"mainnet": {
"message": "Ethereum Hauptnetzwerk (Main Net)"
"message": "Ethereum Main Net"
},
"message": {
"message": "Nachricht"
@ -541,7 +556,7 @@
"description": "Für den Import eine Accounts mit Hilfe eines Private Keys"
},
"pasteSeed": {
"message": "Füge deine Seed Wörterfolge hier ein!"
"message": "Füge deine Seed-Wörterfolge hier ein!"
},
"personalAddressDetected": {
"message": "Personalisierte Adresse identifiziert. Bitte füge die Token Contract Adresse ein."
@ -566,7 +581,7 @@
"message": "QR Code anzeigen"
},
"readdToken": {
"message": "Du kannst diesen Token zukünftig wieder hinzufügen indem du in den Menüpunkt \"Token hinzufügen\" in den Einstellungen deines Accounts gehst."
"message": "Du kannst diesen Token immer erneut hinzufügen, indem du in den Menüpunkt \"Token hinzufügen\" in den Einstellungen deines Accounts gehst."
},
"readMore": {
"message": "Hier mehr erfahren."
@ -590,7 +605,7 @@
"message": "Account zurücksetzten"
},
"restoreFromSeed": {
"message": "Mit Hilfe der Seed Wörterfolge wiederherstellen."
"message": "Mit Hilfe der Seed-Wörterfolge wiederherstellen."
},
"restoreVault": {
"message": "Vault wiederherstellen"
@ -605,13 +620,13 @@
"message": "Wallet Seed"
},
"revealSeedWords": {
"message": "Seed Wörterfolge anzeigen"
"message": "Seed-Wörterfolge anzeigen"
},
"revealSeedWordsWarning": {
"message": "Bitte niemals deine Seed Wörterfolge an einem öffentlichen Ort kenntlich machen. Mit diesen Wörtern können alle deine Accounts gestohlen werden."
"message": "Bitte niemals deine Seed-Wörterfolge an einem öffentlichen Ort kenntlich machen. Mit diesen Wörtern können alle deine Accounts gestohlen werden."
},
"revert": {
"message": "Zurück gehen"
"message": "Rückgängig machen"
},
"rinkeby": {
"message": "Rinkeby Testnetzwerk"
@ -623,7 +638,7 @@
"message": "Aktueller RPC"
},
"connectingToMainnet": {
"message": "Verbinde zum Ethereum Hauptnetzwerk (Main Net)"
"message": "Verbinde zum Ethereum Main Net"
},
"connectingToRopsten": {
"message": " Verbinde zum Ropsten Testnetzwerk"
@ -649,7 +664,7 @@
"description": "Prozess des Exportieren eines Accounts"
},
"saveSeedAsFile": {
"message": "Seed Wörterfolge als Datei speichern"
"message": "Seed-Wörterfolge als Datei speichern"
},
"search": {
"message": "Suche"
@ -661,7 +676,7 @@
"message": "Neues Passwort (min. 8 Zeichen)"
},
"seedPhraseReq": {
"message": "Seed Wörterfolgen bestehen aus 12 Wörtern"
"message": "Seed-Wörterfolgen bestehen aus 12 Wörtern"
},
"select": {
"message": "Auswählen"
@ -685,7 +700,7 @@
"message": "Token senden"
},
"onlySendToEtherAddress": {
"message": "ETH nur zu einer Ethereum Adresse senden."
"message": "ETH unbedingt nur zu einer Ethereum Adresse senden."
},
"sendTokensAnywhere": {
"message": "Token zu einer beliebigen Person mit einem Ethereumaccount senden"
@ -742,7 +757,7 @@
"message": "Einreichen"
},
"submitted": {
"message": "Eingereicht"
"message": "Abgeschickt"
},
"supportCenter": {
"message": "Gehe zu unserem Support Center"
@ -782,7 +797,7 @@
"message": "Tokensymbol"
},
"tokenWarning1": {
"message": "Behalte die Token die du mit deinem MetaMask Account gekauft hast im Auge. Wenn du Token mit einem anderen Account gekauft hast, werden diese hier nicht angezeigt."
"message": "Behalte die Token die du mit deinem MetaMask Account gekauft hast im Blick. Wenn du Token mit einem anderen Account gekauft hast, werden diese hier nicht angezeigt."
},
"total": {
"message": "Gesamt"
@ -853,7 +868,7 @@
"message": " Account einsehen"
},
"visitWebSite": {
"message": "Gehe zu unsere Webseite"
"message": "Gehe zu unserer Webseite"
},
"warning": {
"message": "Warnung"

@ -56,7 +56,7 @@
"message": "Balance:"
},
"balances": {
"message": "Your balances"
"message": "Token balance(s)"
},
"balanceIsInsufficientGas": {
"message": "Insufficient balance for current gas total"
@ -235,7 +235,7 @@
"done": {
"message": "Done"
},
"downloadStatelogs": {
"downloadStateLogs": {
"message": "Download State Logs"
},
"dropped": {
@ -671,6 +671,12 @@
"save": {
"message": "Save"
},
"reprice_title": {
"message": "Reprice Transaction"
},
"reprice_subtitle": {
"message": "Increase your gas price to attempt to overwrite and speed up your transaction"
},
"saveAsFile": {
"message": "Save as File",
"description": "Account export process"
@ -820,6 +826,9 @@
"transactions": {
"message": "transactions"
},
"transactionError": {
"message": "Transaction Error. Exception thrown in contract code."
},
"transactionMemo": {
"message": "Transaction memo (optional)"
},

@ -247,7 +247,7 @@
"done": {
"message": "Completo"
},
"downloadStatelogs": {
"downloadStateLogs": {
"message": "Descargar logs de estado"
},
"dropped": {

@ -223,7 +223,7 @@
"done": {
"message": "सपनन"
},
"downloadStatelogs": {
"downloadStateLogs": {
"message": "रय लग डउनलड कर"
},
"edit": {

@ -0,0 +1,19 @@
[
{ "code": "de", "name": "German" },
{ "code": "en", "name": "English" },
{ "code": "es", "name": "Spanish" },
{ "code": "fr", "name": "French" },
{ "code": "hn", "name": "Hindi" },
{ "code": "it", "name": "Italian" },
{ "code": "ja", "name": "Japanese" },
{ "code": "ko", "name": "Korean" },
{ "code": "nl", "name": "Dutch" },
{ "code": "ph", "name": "Tagalog" },
{ "code": "pt", "name": "Portuguese" },
{ "code": "ru", "name": "Russian" },
{ "code": "sl", "name": "Slovenian" },
{ "code": "th", "name": "Thai" },
{ "code": "vi", "name": "Vietnamese" },
{ "code": "zh_CN", "name": "Mandarin" },
{ "code": "zh_TW", "name": "Taiwanese" }
]

@ -223,7 +223,7 @@
"done": {
"message": "Finito"
},
"downloadStatelogs": {
"downloadStateLogs": {
"message": "Scarica i log di Stato"
},
"edit": {

@ -49,6 +49,9 @@
"balance": {
"message": "残高:"
},
"balances": {
"message": "トークン残高"
},
"balanceIsInsufficientGas": {
"message": "現在のガス総量に対して残高が不足しています"
},
@ -160,20 +163,20 @@
"message": "DENとは、あなたのパスワードが暗号化されたMetaMask内のストレージです。"
},
"deposit": {
"message": "振込"
"message": "振込"
},
"depositBTC": {
"message": "BTCを下記のアドレスへ振込んでください:"
},
"depositCoin": {
"message": "あなたの $1を下記のアドレスへ振込んでください",
"message": "$1を下記のアドレスへ振込んでください",
"description": "Tells the user what coin they have selected to deposit with shapeshift"
},
"depositEth": {
"message": "ETHを入金"
},
"depositEther": {
"message": "Etherを入金"
"message": "Etherを振込"
},
"depositFiat": {
"message": "法定通貨でデポジット"
@ -215,7 +218,7 @@
"message": "パスワードを入力"
},
"etherscanView": {
"message": "Etherscanでアカウントを参照"
"message": "Etherscanでアカウントを確認"
},
"exchangeRate": {
"message": "交換レート"
@ -293,7 +296,7 @@
"message": "トークンを隠す"
},
"hideTokenPrompt": {
"message": "トークンを隠しますか?"
"message": "トークンを隠しますか?"
},
"howToDeposit": {
"message": "どのようにEtherをデポジットしますか?"
@ -306,7 +309,7 @@
"message": "アカウントのインポート"
},
"importAccountMsg": {
"message":"追加したアカウントはMetaMaskのアカウントシードフレーズとは関連付けられません。インポートしたアカウントについての詳細は"
"message":"追加したアカウントはMetaMaskのアカウントパスフレーズとは関連付けられません。インポートしたアカウントについての詳細は"
},
"importAnAccount": {
"message": "アカウントをインポート"
@ -341,7 +344,7 @@
"description": "format for importing an account"
},
"keepTrackTokens": {
"message": "MetaMaskアカウントで入手したトークンを追跡でできます。"
"message": "MetaMaskアカウントで入手したトークンを検索できます。"
},
"kovan": {
"message": "Kovanテストネットワーク"
@ -386,6 +389,9 @@
"myAccounts": {
"message": "マイアカウント"
},
"mustSelectOne": {
"message": "一つ以上のトークンを選択してください。"
},
"needEtherInWallet": {
"message": "MetaMaskで分散型アプリケーションを使用するためには、このウォレットにEtherが必要です。"
},
@ -426,7 +432,7 @@
"message": "この名前にはアドレスが設定されていません。"
},
"noDeposits": {
"message": "振込みがありません。"
"message": "振込みがありません。"
},
"noTransactionHistory": {
"message": "トランザクション履歴がありません。"
@ -460,11 +466,14 @@
"description": "For importing an account from a private key"
},
"pasteSeed": {
"message": "シードをここにペーストして下さい!"
"message": "パスフレーズをここにペーストして下さい!"
},
"pleaseReviewTransaction": {
"message": "トランザクションを確認して下さい。"
},
"popularTokens": {
"message": "人気のトークン"
},
"privateKey": {
"message": "秘密鍵",
"description": "select this type of file to use to import an account"
@ -485,13 +494,13 @@
"message": "もっと読む"
},
"receive": {
"message": "受取"
"message": "受取"
},
"recipientAddress": {
"message": "受取人アドレス"
},
"refundAddress": {
"message": "受取アドレス"
"message": "受取アドレス"
},
"rejected": {
"message": "拒否されました"
@ -502,12 +511,18 @@
"restoreFromSeed": {
"message": "パスフレーズから復元する"
},
"restoreVault": {
"message": "ウォレットを復元する"
},
"required": {
"message": "必要です。"
},
"retryWithMoreGas": {
"message": "より高いガスプライスで再度試して下さい。"
},
"walletSeed": {
"message": "ウォレットのパスフレーズ"
},
"revealSeedWords": {
"message": "パスフレーズを表示"
},
@ -534,6 +549,9 @@
"selectService": {
"message": "サービスを選択"
},
"selectType": {
"message": "キーの種類"
},
"send": {
"message": "送信"
},
@ -555,8 +573,11 @@
"settings": {
"message": "設定"
},
"info": {
"message": "情報"
},
"shapeshiftBuy": {
"message": "Shapeshiftで取引"
"message": "Shapeshiftで交換"
},
"showPrivateKeys": {
"message": "秘密鍵を表示"
@ -616,6 +637,9 @@
"total": {
"message": "合計"
},
"transactions": {
"message": "トランザクション"
},
"transactionMemo": {
"message": "トランザクションメモ (オプション)"
},

@ -223,7 +223,7 @@
"done": {
"message": "Gedaan"
},
"downloadStatelogs": {
"downloadStateLogs": {
"message": "Staatslogboeken downloaden"
},
"edit": {
@ -299,7 +299,7 @@
"message": "De gaslimiet moet minstens 21000 zijn"
},
"generatingSeed": {
"message": "Zaad produceren ..."
"message": "Back-up woorden produceren ..."
},
"gasPrice": {
"message": "Gasprijs (GWEI)"
@ -432,7 +432,7 @@
"message": "Los"
},
"loweCaseWords": {
"message": "zaadwoorden hebben alleen kleine letters"
"message": "back-up woorden hebben alleen kleine letters"
},
"mainnet": {
"message": "belangrijkste ethereum-netwerk"
@ -532,7 +532,7 @@
"description": "Voor het importeren van een account vanaf een privésleutel"
},
"pasteSeed": {
"message": "Plak je zaadzin hier!"
"message": "Plak je back-up woorden hier!"
},
"personalAddressDetected": {
"message": "Persoonlijk adres gedetecteerd. Voer het tokencontractadres in."
@ -581,7 +581,7 @@
"message": "Account opnieuw instellen"
},
"restoreFromSeed": {
"message": "Herstel van zaaduitdrukking"
"message": "Herstel vanuit back-up woorden"
},
"required": {
"message": "Verplicht"
@ -590,10 +590,10 @@
"message": "Probeer hier opnieuw met een hogere gasprijs"
},
"revealSeedWords": {
"message": "Onthul zaadwoorden"
"message": "Onthul back-up woorden"
},
"revealSeedWordsWarning": {
"message": "Herstel je zaadwoorden niet op een openbare plaats! Deze woorden kunnen worden gebruikt om al uw accounts te stelen."
"message": "Zorg dat je back-up woorden niet op een openbare plaats bekijkt! Deze woorden kunnen worden gebruikt om al uw accounts opnieuw te genereren (en dus uw account te stelen)."
},
"revert": {
"message": "terugkeren"
@ -616,7 +616,7 @@
"description": "Account export proces"
},
"saveSeedAsFile": {
"message": "Bewaar zaadwoorden als bestand"
"message": "Bewaar back-up woorden als bestand"
},
"search": {
"message": "Zoeken"
@ -625,7 +625,7 @@
"message": "Voer hier je geheime twaalfwoordfrase in om je kluis te herstellen."
},
"seedPhraseReq": {
"message": "zaadzinnen zijn 12 woorden lang"
"message": "Back-up woorden zijn 12 woorden lang"
},
"select": {
"message": "kiezen"

@ -223,7 +223,7 @@
"done": {
"message": "Finalizado"
},
"downloadStatelogs": {
"downloadStateLogs": {
"message": "Descarregar Registos de Estado"
},
"edit": {

@ -3,13 +3,13 @@
"message": "Принять"
},
"account": {
"message": "Аккаунт"
"message": "Счет"
},
"accountDetails": {
"message": "Детали Аккаунта"
"message": "Детали счета"
},
"accountName": {
"message": "Имя Пользователя"
"message": "Название счета"
},
"address": {
"message": "Адрес"
@ -21,13 +21,13 @@
"message": "Добавить токен"
},
"addTokens": {
"message": "Добавить Токены"
"message": "Добавить токены"
},
"amount": {
"message": "Количество"
"message": "Сумма"
},
"amountPlusGas": {
"message": "Количество + газ"
"message": "Сумма + газ"
},
"appDescription": {
"message": "Расширение браузера для Ethereum",
@ -37,11 +37,14 @@
"message": "MetaMask",
"description": "The name of the application"
},
"approved": {
"message": "Одобрена"
},
"attemptingConnect": {
"message": "Попытка подключиться к блокчейн сети."
},
"attributions": {
"message": "Опознания"
"message": "Атрибуция"
},
"available": {
"message": "Доступный"
@ -53,13 +56,13 @@
"message": "Баланс:"
},
"balances": {
"message": "Ваши балансы"
"message": "Ваш баланс"
},
"balanceIsInsufficientGas": {
"message": "Недостаточный баланс для текущего объема газа"
},
"beta": {
"message": "БЕТА"
"message": "BETA"
},
"betweenMinAndMax": {
"message": "должно быть больше или равно $1 и меньше или равно $2.",
@ -69,10 +72,10 @@
"message": "Использовать Blockies Identicon"
},
"borrowDharma": {
"message": "Заимствовать с Dharma (бета)"
"message": "Взять в долг на Dharma (Beta)"
},
"builtInCalifornia": {
"message": "MetaMask спроектирован и построен в Калифорнии."
"message": "MetaMask спроектирован и разработан в Калифорнии."
},
"buy": {
"message": "Купить"
@ -81,7 +84,10 @@
"message": "Купить на Coinbase"
},
"buyCoinbaseExplainer": {
"message": "Coinbase - самый популярный в мире способ купить и продать биткойн, ethereum и litecoin."
"message": "Биржа Coinbase – это наиболее популярный способ купить или продать bitcoin, ethereum и litecoin."
},
"ok": {
"message": "ОК"
},
"cancel": {
"message": "Отмена"
@ -95,14 +101,17 @@
"confirm": {
"message": "Подтвердить"
},
"confirmed": {
"message": "Подтверждена"
},
"confirmContract": {
"message": "Подтвердить Контракт"
"message": "Подтвердить контракт"
},
"confirmPassword": {
"message": "Подтвердите Пароль"
"message": "Подтвердите пароль"
},
"confirmTransaction": {
"message": "Подтвердить Транзакцию"
"message": "Подтвердить транзакцию"
},
"continue": {
"message": "Продолжить"
@ -114,7 +123,7 @@
"message": "Развертывание контракта"
},
"conversionProgress": {
"message": "Выполняется конверсия"
"message": "Выполняется конвертация"
},
"copiedButton": {
"message": "Скопировано"
@ -126,7 +135,7 @@
"message": "Скопировано!"
},
"copiedSafe": {
"message": "Я скопировал его где-то в безопасности"
"message": "Я скопировал это в безопасное место"
},
"copy": {
"message": "Скопировать"
@ -138,29 +147,32 @@
"message": " Скопировать "
},
"copyPrivateKey": {
"message": "Это ваш личный ключ (нажмите, чтобы скопировать)"
"message": "Это ваш закрытый ключ (нажмите, чтобы скопировать)"
},
"create": {
"message": "Создать"
},
"createAccount": {
"message": "Регистрация"
"message": "Создать счет"
},
"createDen": {
"message": "Создать"
},
"crypto": {
"message": "Крипто",
"message": "Криптовалюта",
"description": "Exchange type (cryptocurrencies)"
},
"currentConversion": {
"message": "Текущая конверсия"
"message": "Текущая конвертация"
},
"currentNetwork": {
"message": "Текущая сеть"
},
"customGas": {
"message": "Настроить Газ"
"message": "Настроить газ"
},
"customToken": {
"message": "Пользовательский токен"
},
"customize": {
"message": "Настроить"
@ -169,112 +181,115 @@
"message": "Пользовательский RPC"
},
"decimalsMustZerotoTen": {
"message": "Десятичные числа должны быть не менее 0, и не более 36."
"message": "Количество десятичных разрядов должно быть минимум 0 и максимум 36."
},
"decimal": {
"message": "Десятичные значения точности"
"message": "Количество десятичных разрядов"
},
"defaultNetwork": {
"message": "Сеть по умолчанию для транзакций Ether - это Main Net."
"message": "Основная сеть Ethereum – это сеть по умолчанию для Ether транзакций."
},
"denExplainer": {
"message": "Ваш DEN - это ваше зашифрованное паролем хранилище в MetaMask."
"message": "DEN – это зашифрованное паролем хранилище внутри MetaMask."
},
"deposit": {
"message": "Депозит"
"message": "Пополнить"
},
"depositBTC": {
"message": "Депозит BTC по адресу:"
"message": "Отправьте ваш BTC на адрес ниже:"
},
"depositCoin": {
"message": "Депозит $1 по указанному ниже адресу",
"message": "Отправьте ваш $1 на адрес ниже",
"description": "Tells the user what coin they have selected to deposit with shapeshift"
},
"depositEth": {
"message": "Депозит Eth"
"message": "Пополнить Eth"
},
"depositEther": {
"message": "Депозит Эфир"
"message": "Пополнить Ether"
},
"depositFiat": {
"message": "Депозит с деньгами"
"message": "Пополнить деньгами"
},
"depositFromAccount": {
"message": "Депозит с другого счета"
"message": "Пополнить с другого счета"
},
"depositShapeShift": {
"message": "Депозит с ShapeShift"
"message": "Пополнить через ShapeShift"
},
"depositShapeShiftExplainer": {
"message": "Если у вас есть другие крипторесурсы, вы можете торговать и вносить Эфир непосредственно в кошелек MetaMask. Нет необходимости в аккаунте."
"message": "Если у вас есть другие криптовалюты, вы можете торговать и пополнять Ether напрямую в ваш MetaMask кошелек. Нет необходимости в счете."
},
"details": {
"message": "Детали"
},
"directDeposit": {
"message": "Прямой Депозит"
"message": "Прямое пополнение"
},
"directDepositEther": {
"message": "Прямой Депозит Эфира"
"message": "Прямое пополнение Ether"
},
"directDepositEtherExplainer": {
"message": "Если у вас уже есть Эфир, самый быстрый способ получить Эфир в вашем новом кошельке это прямым депозитом."
"message": "Если у вас уже есть Ether, то самый быстрый способ получить Ether в ваш новый кошелек – это прямое пополнение."
},
"done": {
"message": "Готово"
},
"downloadStatelogs": {
"message": "Загрузить логи статус"
"downloadStateLogs": {
"message": "Скачать журнал состояния"
},
"dropped": {
"message": "Отброшена"
},
"edit": {
"message": "Редактировать"
},
"editAccountName": {
"message": "Изменить Имя Аккаунта"
"message": "Редактировать название счета"
},
"emailUs": {
"message": "Свяжитесь с нами по электронной почте!"
},
"encryptNewDen": {
"message": "Шифруйте новый DEN"
"message": "Зашифровать ваш новый DEN"
},
"enterPassword": {
"message": "Введите пароль"
},
"enterPasswordConfirm": {
"message": "Введите свой пароль для подтверждения"
"message": "Введите ваш пароль для подтверждения"
},
"etherscanView": {
"message": "Просмотреть аккаунт на Etherscan"
"message": "Просмотреть счет на Etherscan"
},
"exchangeRate": {
"message": "Обменный Курс"
"message": "Обменный курс"
},
"exportPrivateKey": {
"message": "Экспорт закрытого ключа"
"message": "Экспортировать закрытый ключ"
},
"exportPrivateKeyWarning": {
"message": "Экспорт секретных ключей на свой страх и риск."
"message": "Вы экспортируете закрытые ключи на свой страх и риск."
},
"failed": {
"message": "Не смогли"
"message": "Неудачна"
},
"fiat": {
"message": "Бумажные деньги",
"message": "Валюта",
"description": "Exchange type"
},
"fileImportFail": {
"message": "Ошибка импорта файлов? Кликните сюда!",
"message": "Не работает импорт файла? Нажмите тут!",
"description": "Helps user import their account from a JSON file"
},
"followTwitter": {
"message": "Следуйте за нами на Twitter"
"message": "Читайте нас в Twitter"
},
"from": {
"message": "Из"
"message": "Отправитель"
},
"fromToSame": {
"message": "От и до адреса не могут быть одинаковым"
"message": "Адрес отправителя и получателя не могут быть одинаковыми"
},
"fromShapeShift": {
"message": "Из ShapeShift"
@ -284,37 +299,37 @@
"description": "Short indication of gas cost"
},
"gasFee": {
"message": "Плата за Газ"
"message": "Комиссия за газ"
},
"gasLimit": {
"message": "Газовый Предел"
"message": "Лимит газа"
},
"gasLimitCalculation": {
"message": "Мы рассчитываем предполагаемый предел газа на основе коэффициентов успешности сети."
"message": "Мы расчитываем предлагаемый лимит газа на основании успешных ставок в сети."
},
"gasLimitRequired": {
"message": "Требуется ограничение на Газ"
"message": "Установите лимит газа"
},
"gasLimitTooLow": {
"message": "Предел газа должен быть не менее 21000"
"message": "Лимит газа должен быть как минимум 21000"
},
"generatingSeed": {
"message": "Создание Семян ..."
"message": "Генерируем фразу..."
},
"gasPrice": {
"message": "Цена на Газ (GWEI)"
"message": "Цена за газ (GWEI)"
},
"gasPriceCalculation": {
"message": "Мы вычисляем предлагаемые цены на газ на основе коэффициентов успеха сети."
"message": "Мы расчитываем предлагаемые цены за газ на основании успешных ставок в сети."
},
"gasPriceRequired": {
"message": "Требуется цена на Газ"
"message": "Установите стоимость газа"
},
"getEther": {
"message": "Получить Эфир"
"message": "Получить Ether"
},
"getEtherFromFaucet": {
"message": "Получите Эфир из крана $1",
"message": "Получить Ether из крана для $1",
"description": "Displays network name for Ether faucet"
},
"greaterThanMin": {
@ -322,14 +337,14 @@
"description": "helper for inputting hex as decimal input"
},
"here": {
"message": "здесь",
"message": "тут",
"description": "as in -click here- for more information (goes with troubleTokenBalances)"
},
"hereList": {
"message": "Вот список !!!!"
"message": "Вот список!!!!"
},
"hide": {
"message": прятать"
"message": крыть"
},
"hideToken": {
"message": "Скрыть токен"
@ -338,33 +353,33 @@
"message": "Скрыть токен?"
},
"howToDeposit": {
"message": "Как бы вы хотели поместить Эфир?"
"message": "Как бы вы хотели пополнить Ether?"
},
"holdEther": {
"message": "Это позволяет вам использовать эфир и токены и служит мостом для децентрализованных приложений."
"message": "Позволяет вам хранить ether и токены и служит в качестве моста в децентрализированные приложения."
},
"import": {
"message": "Импортировать",
"description": "Button to import an account from a selected file"
},
"importAccount": {
"message": "Импорт Аккаунта"
"message": "Импортировать счет"
},
"importAccountMsg": {
"message": " Импортированные аккаунты не будут связаны с вашей первоначально созданным аккаунтом MetaMask. Подробнее о импортированных аккаунтах "
"message":" Импортированные счета не будут ассоциированы с вашей ключевой фразой, созданной MetaMask. Узнать больше про импорт счетов "
},
"importAnAccount": {
"message": "Импортировать аккаунт"
},
"importDen": {
"message": "Импорт существующих DEN"
"message": "Импортировать существующий DEN"
},
"imported": {
"message": "Импортирован",
"description": "status showing that an account has been fully loaded into the keyring"
},
"infoHelp": {
"message": "Информация и Помощь"
"message": "Информация и помощь"
},
"insufficientFunds": {
"message": "Недостаточно средств."
@ -373,35 +388,44 @@
"message": "Недостаточно токенов."
},
"invalidAddress": {
"message": "Недействительный адрес"
"message": "Неверный адрес"
},
"invalidAddressRecipient": {
"message": "Недопустимый адрес получателя."
"message": "Неверный адрес получателя"
},
"invalidGasParams": {
"message": "Недопустимые параметры Газа"
"message": "Неверные параметры газа"
},
"invalidInput": {
"message": "Неправильный ввод."
"message": "Неверный ввод."
},
"invalidRequest": {
"message": "Неверный Запрос"
"message": "Неверный запрос"
},
"invalidRPC": {
"message": "Недопустимый URI RPC"
"message": "Неверный RPC URI"
},
"jsonFail": {
"message": "Что-то пошло не так. Убедитесь, что ваш файл JSON правильно отформатирован."
"message": "Что-то пошло не так. Убедитесь, что ваш JSON файл правильно отформатирован."
},
"jsonFile": {
"message": "Файл JSON",
"message": "JSON файл",
"description": "format for importing an account"
},
"keepTrackTokens": {
"message": "Следите за купленными вами токенами с помощью аккаунта MetaMask."
},
"kovan": {
"message": "Kovan тестовая сеть"
"message": "Тестовая сеть Kovan"
},
"knowledgeDataBase": {
"message": "Посетите нашу базу знаний"
"message": "Посмотрите нашу Базу Знаний"
},
"max": {
"message": "Максимум"
},
"learnMore": {
"message": "Узнать больше."
},
"lessThanMax": {
"message": "должно быть меньше или равно $1.",
@ -410,29 +434,32 @@
"likeToAddTokens": {
"message": "Вы хотите добавить эти токены?"
},
"links": {
"message": "Ссылки"
},
"limit": {
"message": "Предел"
"message": "Лимит"
},
"loading": {
"message": "Загрузка..."
},
"loadingTokens": {
"message": "Загрузка токенов ..."
"message": "Загрузка токенов..."
},
"localhost": {
"message": "Локальный адрес 8545"
"message": "Localhost 8545"
},
"login": {
"message": "Авторизоваться"
"message": "Вход"
},
"logout": {
"message": "Выйти"
"message": "Выход"
},
"loose": {
"message": "Рыхлый"
"message": "Несвязанный"
},
"loweCaseWords": {
"message": "семенные слова имеют только символы нижнего регистра"
"message": "ключевая фраза может содержать только символы нижнего регистра"
},
"mainnet": {
"message": "Основная сеть Ethereum"
@ -441,19 +468,19 @@
"message": "Сообщение"
},
"metamaskDescription": {
"message": "MetaMask - это безопасное хранилище для Ethereum."
"message": "MetaMask – безопасный кошелек для Ethereum."
},
"min": {
"message": "Минимум"
},
"myAccounts": {
"message": "Мои Аккаунты"
"message": "Мои счета"
},
"mustSelectOne": {
"message": "Необходимо выбрать не менее 1 токена."
"message": "Необходимо выбрать как минимум 1 токен."
},
"needEtherInWallet": {
"message": "Чтобы взаимодействовать с децентрализованными приложениями с помощью MetaMask, вам понадобится Эфир в вашем кошельке."
"message": "Для взаимодействия с децентрализованными приложениями с помощью MetaMask нужен Ether в вашем кошельке."
},
"needImportFile": {
"message": "Вы должны выбрать файл для импорта.",
@ -464,60 +491,60 @@
"description": "Password and file needed to import an account"
},
"negativeETH": {
"message": "Невозможно отправить отрицательные количества ETH."
"message": "Невозможно отправить отрицательную сумму ETH."
},
"networks": {
"message": "Сети"
},
"newAccount": {
"message": "Новый Аккаунт"
"message": "Новый счет"
},
"newAccountNumberName": {
"message": "Аккаунт $1",
"message": "Счет $1",
"description": "Default name of next account to be created on create account screen"
},
"newContract": {
"message": "Новый Контракт"
"message": "Новый контракт"
},
"newPassword": {
"message": "Новый пароль (мин. 8 символов)"
},
"newRecipient": {
"message": "Новый Получатель"
"message": "Новый получатель"
},
"newRPC": {
"message": "Новый URL-адрес RPC"
"message": "Новый RPC URL"
},
"next": {
"message": "Далее"
},
"noAddressForName": {
"message": "Для этого имени не задан адрес."
"message": "Дла этого названия не установлен адрес."
},
"noDeposits": {
"message": "Не было получено никаких депозитов"
"message": "Пополнения не получены"
},
"noTransactionHistory": {
"message": "Нет истории транзакций."
},
"noTransactions": {
"message": "Нет Транзакций"
"message": "Нет транзакций"
},
"notStarted": {
"message": "Не Начался"
"message": "Не запущен"
},
"oldUI": {
"message": "Старый Интерфейс"
"message": "Старая версия интерфейса"
},
"oldUIMessage": {
"message": "Вы вернулись к старому интерфейсу. Вы можете вернуться к новому с помощью опции в раскрывающемся меню в правом верхнем углу."
"message": "Вы вернулись к старой версии интерфейса пользователя. Вы можете переключиться на новую с помощью опции выпадающего меню в правом верхнем углу."
},
"or": {
"message": "или",
"description": "choice between creating or importing a new account"
},
"passwordCorrect": {
"message": "Убедитесь, что ваш пароль правильный."
"message": "Убедитесь, что ваш пароль верный."
},
"passwordMismatch": {
"message": "пароли не совпадают",
@ -528,27 +555,30 @@
"description": "in password creation process, the password is not long enough to be secure"
},
"pastePrivateKey": {
"message": "Вставьте свою личную строку:",
"message": "Вставьте ваш закрытый ключ тут:",
"description": "For importing an account from a private key"
},
"pasteSeed": {
"message": "Вставьте здесь свою семенную фразу!"
"message": "Вставьте вашу ключевую фразу!"
},
"personalAddressDetected": {
"message": "Персональный адрес обнаружен. Введите адрес контракта токена."
"message": "Обнаружен персональный адрес. Введите адрес контракта токена."
},
"pleaseReviewTransaction": {
"message": "Проверьте транзакцию."
},
"popularTokens": {
"message": "Популярные токены"
},
"privacyMsg": {
"message": "Политика Конфиденциальности"
"message": "Политика конфиденциальности"
},
"privateKey": {
"message": "Закрытый ключ",
"description": "select this type of file to use to import an account"
},
"privateKeyWarning": {
"message": "Предупреждение: никогда не раскрывайте этот ключ. Любой, у кого есть ваши личные ключи, может украсть любые активы, хранящиеся в вашем аккаунте."
"message": "Предупреждение: Никогда не раскрывайте этот ключ. Любой, у кого есть ваши закрытые ключи, может украсть любые активы, хранящиеся на счету."
},
"privateNetwork": {
"message": "Частная сеть"
@ -557,126 +587,165 @@
"message": "Показать QR-код"
},
"readdToken": {
"message": "Вы можете добавить этот токен в будущем, перейдя в “Добавить токен” в меню параметров вашего аккаунта."
"message": "Вы можете в будущем добавить обратно этот токен, выбрав пункт меню “Добавить токен”."
},
"readMore": {
"message": "Подробнее читайте здесь."
"message": "Узнать больше тут."
},
"readMore2": {
"message": "Прочитайте больше."
"message": "Узнать больше."
},
"receive": {
"message": "Получить"
},
"recipientAddress": {
"message": "Адрес Получателя"
"message": "Адрес получателя"
},
"refundAddress": {
"message": "Ваш Адрес Возврата"
"message": "Ваш адрес для возврата средств"
},
"rejected": {
"message": "Отклонено"
"message": "Отклонена"
},
"resetAccount": {
"message": "Сбросить аккаунт"
},
"restoreFromSeed": {
"message": "Восстановить от семенной фразы"
"message": "Восстановить из ключевой фразы"
},
"restoreVault": {
"message": "Восстановить кошелек"
},
"required": {
"message": "Необходимо"
"message": "Обязательное поле"
},
"retryWithMoreGas": {
"message": "Повторите попытку с более высокой ценой на газ здесь"
"message": "Повторите попытку с большей ценой за газRetry with a higher gas price here"
},
"walletSeed": {
"message": "Ключевая фраза кошелька"
},
"revealSeedWords": {
"message": "Раскрыть семенные слова"
"message": "Показать ключевую фразу"
},
"revealSeedWordsWarning": {
"message": "Не восстанавливайте семенные слова в общественном месте! Эти слова могут использоваться для кражи всех ваших аккаунтах."
"message": "Не восстанавливайте ключевую фразу в общественном месте! Она может быть использована для кражи всех ваших счетов."
},
"revert": {
"message": "Откат"
"message": "Восстановить"
},
"rinkeby": {
"message": "Rinkeby тестовая сеть"
"message": "Тестовая сеть Rinkeby"
},
"ropsten": {
"message": "Ropsten тестовая сеть"
"message": "Тестовая сеть Ropsten"
},
"currentRpc": {
"message": "Current RPC"
},
"connectingToMainnet": {
"message": "Соединение с основной сетью Ethereum"
},
"connectingToRopsten": {
"message": "Соединение с тестовой сетью Ropsten"
},
"connectingToKovan": {
"message": "Соединение с тестовой сетью Kovan"
},
"connectingToRinkeby": {
"message": "Соединение с тестовой сетью Rinkeby"
},
"connectingToUnknown": {
"message": "Соединение с неизвестной сетью"
},
"sampleAccountName": {
"message": "Например, Мой новый аккаунт",
"message": "Например, Мой новый счет",
"description": "Help user understand concept of adding a human-readable name to their account"
},
"save": {
"message": "Сохранить"
},
"saveAsFile": {
"message": "Сохранить как Файл",
"message": "Сохранить в виде файла",
"description": "Account export process"
},
"saveSeedAsFile": {
"message": "Сохранить Семенные Слова Как Файл"
"message": "Сохранить ключевую фразу в виде файла"
},
"search": {
"message": "Поиск"
},
"secretPhrase": {
"message": "Введите свою секретную двенадцатисловную фразу здесь, чтобы восстановить хранилище."
"message": "Введите вашу ключевую фразу из 12 слов, чтобы восстановить кошелек."
},
"newPassword8Chars": {
"message": "Новый пароль (мин. 8 символов)"
},
"seedPhraseReq": {
"message": "семенные фразы длиной 12 слов"
"message": "ключевые фразы имеют длину 12 слов"
},
"select": {
"message": "Выбрать"
},
"selectCurrency": {
"message": "Выберите Валюту"
"message": "Выберите валюту"
},
"selectService": {
"message": "Выберите Сервис"
"message": "Выберите сервис"
},
"selectType": {
"message": "Выберите Тип"
"message": "Выберите тип"
},
"send": {
"message": "Послать"
"message": "Отправить"
},
"sendETH": {
"message": "Отправить ETH"
},
"sendTokens": {
"message": "Отправить Токены"
"message": "Отправить токены"
},
"onlySendToEtherAddress": {
"message": "Отправляйте ETH только на Ethereum адреса."
},
"searchTokens": {
"message": "Поиск токенов"
},
"sendTokensAnywhere": {
"message": "Отправить Токены кому-либо с аккаунтом Ethereum"
"message": "Отправить токены любому, у кого есть счет Ethereum"
},
"settings": {
"message": "Настройки"
},
"info": {
"message": "Информация"
},
"shapeshiftBuy": {
"message": "Купить с помощью Shapeshift"
"message": "Купить через Shapeshift"
},
"showPrivateKeys": {
"message": "Показать приватные ключи"
"message": "Показать закрытые ключи"
},
"showQRCode": {
"message": "Показать QR-код"
},
"sign": {
"message": "Знак"
"message": "Подпись"
},
"signed": {
"message": "Подписана"
},
"signMessage": {
"message": "Нодписать сообщение"
"message": "Подписать сообщение"
},
"signNotice": {
"message": "Подписание этого сообщения может иметь \nопасные побочные эффекты. Только подписывайте сообщения \nс сайтов, которым вы полностью доверяете своим аккаунтом. Этот опасный метод будет удален в будущей версии."
"message": "Подпись этого сообщения может иметь \nопасные побочные эффекты. Подписывайте только сообщения \nс сайтов, которым вы полностью доверяете свой аккаунт. Этот опасный метод будет удален в будущей версии."
},
"sigRequest": {
"message": "Запрос на подпись"
"message": "Запрос подписи"
},
"sigRequested": {
"message": "Подпись Запрошена"
"message": "Подпись запрошена"
},
"spaceBetween": {
"message": "между словами может быть только пробел"
@ -685,53 +754,59 @@
"message": "Статус"
},
"stateLogs": {
"message": "Логи Статуса"
"message": "Журнал состояния"
},
"stateLogsDescription": {
"message": "Логи статуса содержат ваши общедоступные адреса и отправленные транзакции."
"message": "Журнал состояния содержит ваши публичные адреса счетов и совершенные транзакции."
},
"stateLogError": {
"message": "Ошибка при получении журнала состояния."
},
"submit": {
"message": "Отправить"
},
"submitted": {
"message": "Отправлена"
},
"supportCenter": {
"message": "Посетите наш Центр поддержки"
"message": ерейти в наш Центр поддержки"
},
"symbolBetweenZeroTen": {
"message": "Символ должен быть от 0 до 10 символов."
},
"takesTooLong": {
"message": "Занимает слишком долго?"
"message": "Слишком долго?"
},
"terms": {
"message": "Условия Эксплуатации"
"message": "Условия пользования"
},
"testFaucet": {
"message": "Тестовый Кран"
"message": "Тестовый кран"
},
"to": {
"message": "К"
"message": "Получатель: "
},
"toETHviaShapeShift": {
"message": "$1 в ETH через ShapeShift",
"description": "system will fill in deposit type in start of message"
},
"tokenAddress": {
"message": "Адрес Токена"
"message": "Адрес токена"
},
"tokenAlreadyAdded": {
"message": "Токен уже добавлен."
"message": "Токен уже был добавлен."
},
"tokenBalance": {
"message": "Баланс Вашых Tокенов:"
"message": "Баланс ваших токенов:"
},
"tokenSelection": {
"message": "Поиск токенов или выбор из нашего списка популярных токенов."
"message": "Поищите токен или выберите из нашего списка популярных токенов."
},
"tokenSymbol": {
"message": "Символ Токена"
"message": "Символ токена"
},
"tokenWarning1": {
"message": "Следите за токенами, которые вы купили с помощью аккаунта MetaMask. Если вы купили токены, используя другой аккаунт, эти токены здесь не появятся."
"message": "Отслеживаются токены, купленные на счет в MetaMask. Если вы купили токены, используя другой счет, такие токены не будут тут отображены."
},
"total": {
"message": "Всего"
@ -740,35 +815,38 @@
"message": "транзакции"
},
"transactionMemo": {
"message": "Транзакционная записка (необязательно)"
"message": "Транзакционные данные (необязательный)"
},
"transactionNumber": {
"message": "Номер Транзакции"
"message": "Номер транзакции"
},
"transfers": {
"message": "Переводы"
},
"troubleTokenBalances": {
"message": "У нас были проблемы с загрузкой ваших токенов. Вы можете просмотреть их ",
"message": "Возникли проблемы при загрузке балансов токенов. Вы можете посмотреть их ",
"description": "Followed by a link (here) to view token balances"
},
"twelveWords": {
"message": "Эти 12 слов - единственный способ восстановить ваши учетные записи MetaMask.\nСохраните их где-нибудь в безопасности и в тайне."
"message": "Эти 12 слов являются единственной возможностью восстановить ваши счета в MetaMask.\nСохраните из в надежном секретном месте."
},
"typePassword": {
"message": "Введите Пароль"
"message": "Введите пароль"
},
"uiWelcome": {
"message": "Добро пожаловать в новый интерфейс (бета-версия)"
"message": "Новый интерфейс (Beta)"
},
"uiWelcomeMessage": {
"message": "Теперь вы используете новый интерфейс Metamask. Осмотритесь, попробуйте новые функции, такие как отправку токенов, и сообщите нам, есть ли у вас какие-либо проблемы."
"message": "Теперь вы используете новый интерфейс пользователя MetaMask. Осмотритесь, попробуйте новые функции, например, отправить токены и, если возникнут проблемы, сообщите нам."
},
"unapproved": {
"message": "Не одобрена"
},
"unavailable": {
"message": "Недоступен"
"message": "Недоступный"
},
"unknown": {
"message": "Неизвестный"
"message": "Неизвестно"
},
"unknownNetwork": {
"message": "Неизвестная частная сеть"
@ -777,7 +855,7 @@
"message": "Неизвестный идентификатор сети"
},
"uriErrorMsg": {
"message": "Для URI требуется соответствующий префикс HTTP / HTTPS."
"message": "Для URI требуется соответствующий префикс HTTP/HTTPS."
},
"usaOnly": {
"message": "Только США",
@ -787,19 +865,19 @@
"message": "Используется различными клиентами"
},
"useOldUI": {
"message": "Использовать старый интерфейс"
"message": "Использовать старый интерфейс пользователя"
},
"validFileImport": {
"message": ы должны выбрать действительный файл для импорта."
"message": ам нужно выбрать правильный файл для импорта."
},
"vaultCreated": {
"message": "Создано хранилище"
"message": "Кошелек был создан"
},
"viewAccount": {
"message": "Посмотреть аккаунт"
"message": "Посмотреть счет"
},
"visitWebSite": {
"message": осетите наш сайт"
"message": ерейти на наш сайт"
},
"warning": {
"message": "Предупреждение"
@ -811,7 +889,7 @@
"message": "Что это?"
},
"yourSigRequested": {
"message": "Ваша подпись запрашивается"
"message": "Запрашивается ваша подпись"
},
"youSign": {
"message": "Вы подписываете"

@ -223,7 +223,7 @@
"done": {
"message": "Končano"
},
"downloadStatelogs": {
"downloadStateLogs": {
"message": "Prenesi state dnevnike"
},
"edit": {

@ -223,7 +223,7 @@
"done": {
"message": "เสรจสน"
},
"downloadStatelogs": {
"downloadStateLogs": {
"message": "ดาวนโหลดลอกสถานะ"
},
"edit": {

@ -171,6 +171,9 @@
"customGas": {
"message": "自訂 Gas"
},
"customToken": {
"message": "自訂代幣"
},
"customize": {
"message": "自訂"
},
@ -184,7 +187,7 @@
"message": "小數點精度"
},
"defaultNetwork": {
"message": "預設Ether交易網路為主網(Main Net)。"
"message": "預設 Ether 交易網路為主網(Main Net)。"
},
"denExplainer": {
"message": "你的 DEN 是在你的 MetaMask 中的加密密碼儲存庫。"
@ -215,7 +218,7 @@
"message": "從 ShapeShift 存入"
},
"depositShapeShiftExplainer": {
"message": "如果你擁有其他加密貨幣,你可以直接交易並存入 Ether 到你的MetaMask錢包。不需要開帳戶。"
"message": "如果你擁有其他加密貨幣,你可以直接交易並存入 Ether 到你的 MetaMask 錢包。不需要開帳戶。"
},
"details": {
"message": "詳情"
@ -227,12 +230,12 @@
"message": "直接存入 Ether"
},
"directDepositEtherExplainer": {
"message": "如果你已經擁有了一些Ether,使用直接存入功能是讓你的新錢包最快取得Ether的方式。"
"message": "如果你已經擁有了一些 Ether,使用直接存入功能是讓你的新錢包最快取得 Ether 的方式。"
},
"done": {
"message": "完成"
},
"downloadStatelogs": {
"downloadStateLogs": {
"message": "下載狀態紀錄"
},
"dropped": {
@ -285,6 +288,9 @@
"message": "檔案導入失敗?點擊這裡!",
"description": "Helps user import their account from a JSON file"
},
"followTwitter": {
"message": "追蹤 Twitter"
},
"from": {
"message": "來源地址"
},
@ -313,6 +319,9 @@
"gasLimitTooLow": {
"message": "Gas 上限至少為 21000"
},
"generatingSeed": {
"message": "產生助憶詞中..."
},
"gasPrice": {
"message": "Gas 價格 (GWEI)"
},
@ -362,6 +371,9 @@
"importAccount": {
"message": "導入帳戶"
},
"importAccountMsg": {
"message":" 匯入的帳戶與您原有 MetaMask 帳戶的助憶詞並無關聯. 請查看與導入帳戶相關的資料 "
},
"importAnAccount": {
"message": "導入一個帳戶"
},
@ -400,12 +412,15 @@
"message": "無效的 RPC URI"
},
"jsonFail": {
"message": "有東西出錯了. 請確認你的 JSON 檔案格式正確."
"message": "有東西出錯了. 請確認你的 JSON 檔案格式正確"
},
"jsonFile": {
"message": "JSON 檔案",
"description": "format for importing an account"
},
"keepTrackTokens": {
"message": "持續追蹤您 MetaMask 帳戶中的代幣。"
},
"kovan": {
"message": "Kovan 測試網路"
},
@ -415,6 +430,9 @@
"max": {
"message": "最大值"
},
"learnMore": {
"message": "了解更多。"
},
"lessThanMax": {
"message": "必須小於等於 $1.",
"description": "helper for inputting hex as decimal input"
@ -437,17 +455,20 @@
"localhost": {
"message": "Localhost 8545"
},
"login": {
"message": "登入"
},
"logout": {
"message": "登出"
},
"loose": {
"message": "非Metamask帳號"
"message": "非 MetaMask 帳號"
},
"loweCaseWords": {
"message": "助憶詞僅包含小寫字元"
},
"mainnet": {
"message": "乙太坊網路"
"message": "乙太坊網路"
},
"message": {
"message": "訊息"
@ -465,7 +486,7 @@
"message": "必須選擇至少 1 代幣."
},
"needEtherInWallet": {
"message": "要使用 MetaMask 存取 DAPP時,您的錢包中需要有 Ether。"
"message": "要使用 MetaMask 存取 DAPP 時,您的錢包中需要有 Ether。"
},
"needImportFile": {
"message": "您必須選擇一個檔案來導入。",
@ -475,6 +496,9 @@
"message": "您必須為選擇好的檔案輸入密碼。",
"description": "Password and file needed to import an account"
},
"negativeETH": {
"message": "不能送出負值的 ETH。"
},
"networks": {
"message": "網路"
},
@ -525,6 +549,9 @@
"message": "或",
"description": "choice between creating or importing a new account"
},
"passwordCorrect": {
"message": "請確認您的密碼是正確的。"
},
"passwordMismatch": {
"message": "密碼不一致",
"description": "in password creation process, the two new password fields did not match"
@ -546,6 +573,12 @@
"pleaseReviewTransaction": {
"message": "請檢查你的交易。"
},
"popularTokens": {
"message": "常見的代幣"
},
"privacyMsg": {
"message": "隱私政策"
},
"privateKey": {
"message": "私鑰",
"description": "select this type of file to use to import an account"
@ -681,6 +714,9 @@
"onlySendToEtherAddress": {
"message": "只發送 ETH 到乙太坊地址."
},
"searchTokens": {
"message": "搜尋代幣"
},
"sendTokensAnywhere": {
"message": "發送代幣給擁有乙太坊帳戶的任何人"
},
@ -700,13 +736,16 @@
"message": "顯示 QR Code"
},
"sign": {
"message": "簽名"
"message": "簽署"
},
"signed": {
"message": "已簽署"
},
"signMessage": {
"message": "簽署訊息"
},
"signNotice": {
"message": "簽署此訊息可能會產生危險的副作用。 \n只從你完全信任的網站上簽名。這種危險的方法;將在未來的版本中被移除。"
"message": "簽署此訊息可能會產生危險地副作用。 \n只從你完全信任的網站上簽署。這種危險的方法;將在未來的版本中被移除。"
},
"sigRequest": {
"message": "請求簽署"
@ -767,7 +806,7 @@
"message": "代幣餘額:"
},
"tokenSelection": {
"message": "搜尋代幣或是從熱門代幣列表中選擇。"
"message": "搜尋代幣或是從常見代幣列表中選擇。"
},
"tokenSymbol": {
"message": "代幣代號"
@ -804,7 +843,7 @@
"message": "歡迎使用新版界面 (Beta)"
},
"uiWelcomeMessage": {
"message": "你現在正在使用新的 Metamask 界面。試試諸如發送代幣等新功能,有任何問題請告知我們。"
"message": "你現在正在使用新版 MetaMask 界面。試試諸如發送代幣等新功能吧,有任何問題請告知我們。"
},
"unapproved": {
"message": "未同意"

@ -3,10 +3,10 @@
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1 user-scalable=no">
<title>MetaMask Plugin</title>
<title>MetaMask</title>
</head>
<body>
<div id="app-content"></div>
<script src="./scripts/popup.js" type="text/javascript" charset="utf-8"></script>
<script src="./ui.js" type="text/javascript" charset="utf-8"></script>
</body>
</html>

@ -1,7 +1,7 @@
{
"name": "__MSG_appName__",
"short_name": "__MSG_appName__",
"version": "4.4.0",
"version": "4.5.5",
"manifest_version": 2,
"author": "https://metamask.io",
"description": "__MSG_appDescription__",
@ -27,8 +27,8 @@
"default_locale": "en",
"background": {
"scripts": [
"scripts/chromereload.js",
"scripts/background.js"
"chromereload.js",
"background.js"
],
"persistent": true
},
@ -48,7 +48,7 @@
"https://*/*"
],
"js": [
"scripts/contentscript.js"
"contentscript.js"
],
"run_at": "document_start",
"all_frames": true
@ -62,7 +62,7 @@
"https://*.infura.io/"
],
"web_accessible_resources": [
"scripts/inpage.js"
"inpage.js"
],
"externally_connectable": {
"matches": [

@ -11,6 +11,6 @@
</head>
<body class="notification" style="height:600px;">
<div id="app-content"></div>
<script src="./scripts/popup.js" type="text/javascript" charset="utf-8"></script>
<script src="./ui.js" type="text/javascript" charset="utf-8"></script>
</body>
</html>

@ -3,10 +3,10 @@
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1 user-scalable=no">
<title>MetaMask Plugin</title>
<title>MetaMask</title>
</head>
<body style="width:357px; height:600px;">
<div id="app-content"></div>
<script src="./scripts/popup.js" type="text/javascript" charset="utf-8"></script>
<script src="./ui.js" type="text/javascript" charset="utf-8"></script>
</body>
</html>

@ -19,10 +19,11 @@ const setupRaven = require('./lib/setupRaven')
const reportFailedTxToSentry = require('./lib/reportFailedTxToSentry')
const setupMetamaskMeshMetrics = require('./lib/setupMetamaskMeshMetrics')
const EdgeEncryptor = require('./edge-encryptor')
const getFirstPreferredLangCode = require('./lib/get-first-preferred-lang-code')
const getObjStructure = require('./lib/getObjStructure')
const STORAGE_KEY = 'metamask-config'
const METAMASK_DEBUG = 'GULP_METAMASK_DEBUG'
const METAMASK_DEBUG = process.env.METAMASK_DEBUG
window.log = log
log.setDefaultLevel(METAMASK_DEBUG ? 'debug' : 'warn')
@ -58,7 +59,8 @@ setupMetamaskMeshMetrics()
async function initialize () {
const initState = await loadStateFromPersistence()
await setupController(initState)
const initLangCode = await getFirstPreferredLangCode()
await setupController(initState, initLangCode)
log.debug('MetaMask initialization complete.')
}
@ -76,6 +78,16 @@ async function loadStateFromPersistence () {
diskStore.getState() ||
migrator.generateInitialState(firstTimeState)
// report migration errors to sentry
migrator.on('error', (err) => {
// get vault structure without secrets
const vaultStructure = getObjStructure(versionedData)
raven.captureException(err, {
// "extra" key is required by Sentry
extra: { vaultStructure },
})
})
// migrate data
versionedData = await migrator.migrateData(versionedData)
if (!versionedData) {
@ -83,13 +95,20 @@ async function loadStateFromPersistence () {
}
// write to disk
if (localStore.isSupported) localStore.set(versionedData)
if (localStore.isSupported) {
localStore.set(versionedData)
} else {
// throw in setTimeout so as to not block boot
setTimeout(() => {
throw new Error('MetaMask - Localstore not supported')
})
}
// return just the data
return versionedData.data
}
function setupController (initState) {
function setupController (initState, initLangCode) {
//
// MetaMask Controller
//
@ -101,6 +120,8 @@ function setupController (initState) {
showUnapprovedTx: triggerUi,
// initial state
initState,
// initial locale code
initLangCode,
// platform specific api
platform,
encryptor: isEdge ? new EdgeEncryptor() : undefined,

@ -13,7 +13,7 @@ const DEFAULT_RPC = 'rinkeby'
const OLD_UI_NETWORK_TYPE = 'network'
const BETA_UI_NETWORK_TYPE = 'networkBeta'
global.METAMASK_DEBUG = 'GULP_METAMASK_DEBUG'
global.METAMASK_DEBUG = process.env.METAMASK_DEBUG
module.exports = {
network: {

@ -7,8 +7,8 @@ const ObjectMultiplex = require('obj-multiplex')
const extension = require('extensionizer')
const PortStream = require('./lib/port-stream.js')
const inpageContent = fs.readFileSync(path.join(__dirname, '..', '..', 'dist', 'chrome', 'scripts', 'inpage.js')).toString()
const inpageSuffix = '//# sourceURL=' + extension.extension.getURL('scripts/inpage.js') + '\n'
const inpageContent = fs.readFileSync(path.join(__dirname, '..', '..', 'dist', 'chrome', 'inpage.js')).toString()
const inpageSuffix = '//# sourceURL=' + extension.extension.getURL('inpage.js') + '\n'
const inpageBundle = inpageContent + inpageSuffix
// Eventually this streaming injection could be replaced with:
@ -131,7 +131,11 @@ function documentElementCheck () {
}
function blacklistedDomainCheck () {
var blacklistedDomains = ['uscourts.gov', 'dropbox.com']
var blacklistedDomains = [
'uscourts.gov',
'dropbox.com',
'webbyawards.com',
]
var currentUrl = window.location.href
var currentRegex
for (let i = 0; i < blacklistedDomains.length; i++) {

@ -41,9 +41,9 @@ class BlacklistController {
scheduleUpdates () {
if (this._phishingUpdateIntervalRef) return
this.updatePhishingList()
this.updatePhishingList().catch(log.warn)
this._phishingUpdateIntervalRef = setInterval(() => {
this.updatePhishingList()
this.updatePhishingList().catch(log.warn)
}, POLLING_INTERVAL)
}
@ -57,4 +57,3 @@ class BlacklistController {
}
module.exports = BlacklistController

@ -43,20 +43,19 @@ class CurrencyController {
this.store.updateState({ conversionDate })
}
updateConversionRate () {
const currentCurrency = this.getCurrentCurrency()
return fetch(`https://api.infura.io/v1/ticker/eth${currentCurrency.toLowerCase()}`)
.then(response => response.json())
.then((parsedResponse) => {
async updateConversionRate () {
let currentCurrency
try {
currentCurrency = this.getCurrentCurrency()
const response = await fetch(`https://api.infura.io/v1/ticker/eth${currentCurrency.toLowerCase()}`)
const parsedResponse = await response.json()
this.setConversionRate(Number(parsedResponse.bid))
this.setConversionDate(Number(parsedResponse.timestamp))
}).catch((err) => {
if (err) {
console.warn('MetaMask - Failed to query currency conversion.')
this.setConversionRate(0)
this.setConversionDate('N/A')
}
})
} catch (err) {
log.warn(`MetaMask - Failed to query currency conversion:`, currentCurrency, err)
this.setConversionRate(0)
this.setConversionDate('N/A')
}
}
scheduleConversionInterval () {

@ -19,15 +19,13 @@ class InfuraController {
// Responsible for retrieving the status of Infura's nodes. Can return either
// ok, degraded, or down.
checkInfuraNetworkStatus () {
return fetch('https://api.infura.io/v1/status/metamask')
.then(response => response.json())
.then((parsedResponse) => {
this.store.updateState({
infuraNetworkStatus: parsedResponse,
})
return parsedResponse
})
async checkInfuraNetworkStatus () {
const response = await fetch('https://api.infura.io/v1/status/metamask')
const parsedResponse = await response.json()
this.store.updateState({
infuraNetworkStatus: parsedResponse,
})
return parsedResponse
}
scheduleInfuraNetworkCheck () {
@ -35,7 +33,7 @@ class InfuraController {
clearInterval(this.conversionInterval)
}
this.conversionInterval = setInterval(() => {
this.checkInfuraNetworkStatus()
this.checkInfuraNetworkStatus().catch(log.warn)
}, POLLING_INTERVAL)
}
}

@ -11,6 +11,7 @@ class PreferencesController {
tokens: [],
useBlockie: false,
featureFlags: {},
currentLocale: opts.initLangCode,
}, opts.initState)
this.store = new ObservableStore(initState)
}
@ -24,6 +25,10 @@ class PreferencesController {
return this.store.getState().useBlockie
}
setCurrentLocale (key) {
this.store.updateState({ currentLocale: key })
}
setSelectedAddress (_address) {
return new Promise((resolve, reject) => {
const address = normalizeAddress(_address)

@ -45,18 +45,19 @@ class ShapeshiftController {
})
}
updateTx (tx) {
const url = `https://shapeshift.io/txStat/${tx.depositAddress}`
return fetch(url)
.then((response) => {
return response.json()
}).then((json) => {
async updateTx (tx) {
try {
const url = `https://shapeshift.io/txStat/${tx.depositAddress}`
const response = await fetch(url)
const json = await response.json()
tx.response = json
if (tx.response.status === 'complete') {
tx.time = new Date().getTime()
}
return tx
})
} catch (err) {
log.warn(err)
}
}
saveTx (tx) {

@ -161,9 +161,11 @@ module.exports = class TransactionController extends EventEmitter {
this.emit(`${txMeta.id}:unapproved`, txMeta)
}
async newUnapprovedTransaction (txParams) {
async newUnapprovedTransaction (txParams, opts = {}) {
log.debug(`MetaMaskController newUnapprovedTransaction ${JSON.stringify(txParams)}`)
const initialTxMeta = await this.addUnapprovedTransaction(txParams)
initialTxMeta.origin = opts.origin
this.txStateManager.updateTx(initialTxMeta, '#newUnapprovedTransaction - adding the origin')
// listen for tx completion (success, fail)
return new Promise((resolve, reject) => {
this.txStateManager.once(`${initialTxMeta.id}:finished`, (finishedTxMeta) => {
@ -183,14 +185,15 @@ module.exports = class TransactionController extends EventEmitter {
async addUnapprovedTransaction (txParams) {
// validate
await this.txGasUtil.validateTxParams(txParams)
const normalizedTxParams = this._normalizeTxParams(txParams)
this._validateTxParams(normalizedTxParams)
// construct txMeta
const txMeta = this.txStateManager.generateTxMeta({txParams})
let txMeta = this.txStateManager.generateTxMeta({ txParams: normalizedTxParams })
this.addTx(txMeta)
this.emit('newUnapprovedTx', txMeta)
// add default tx params
try {
await this.addTxDefaults(txMeta)
txMeta = await this.addTxDefaults(txMeta)
} catch (error) {
console.log(error)
this.txStateManager.setTxStatusFailed(txMeta.id, error)
@ -273,12 +276,14 @@ module.exports = class TransactionController extends EventEmitter {
async signTransaction (txId) {
const txMeta = this.txStateManager.getTx(txId)
const txParams = txMeta.txParams
const fromAddress = txParams.from
// add network/chain id
txParams.chainId = ethUtil.addHexPrefix(this.getChainId().toString(16))
const chainId = this.getChainId()
const txParams = Object.assign({}, txMeta.txParams, { chainId })
// sign tx
const fromAddress = txParams.from
const ethTx = new Transaction(txParams)
await this.signEthTx(ethTx, fromAddress)
// set state to signed
this.txStateManager.setTxStatusSigned(txMeta.id)
const rawTx = ethUtil.bufferToHex(ethTx.serialize())
return rawTx
@ -309,6 +314,60 @@ module.exports = class TransactionController extends EventEmitter {
// PRIVATE METHODS
//
_normalizeTxParams (txParams) {
// functions that handle normalizing of that key in txParams
const whiteList = {
from: from => ethUtil.addHexPrefix(from).toLowerCase(),
to: to => ethUtil.addHexPrefix(txParams.to).toLowerCase(),
nonce: nonce => ethUtil.addHexPrefix(nonce),
value: value => ethUtil.addHexPrefix(value),
data: data => ethUtil.addHexPrefix(data),
gas: gas => ethUtil.addHexPrefix(gas),
gasPrice: gasPrice => ethUtil.addHexPrefix(gasPrice),
}
// apply only keys in the whiteList
const normalizedTxParams = {}
Object.keys(whiteList).forEach((key) => {
if (txParams[key]) normalizedTxParams[key] = whiteList[key](txParams[key])
})
return normalizedTxParams
}
_validateTxParams (txParams) {
this._validateFrom(txParams)
this._validateRecipient(txParams)
if ('value' in txParams) {
const value = txParams.value.toString()
if (value.includes('-')) {
throw new Error(`Invalid transaction value of ${txParams.value} not a positive number.`)
}
if (value.includes('.')) {
throw new Error(`Invalid transaction value of ${txParams.value} number must be in wei`)
}
}
}
_validateFrom (txParams) {
if ( !(typeof txParams.from === 'string') ) throw new Error(`Invalid from address ${txParams.from} not a string`)
if (!ethUtil.isValidAddress(txParams.from)) throw new Error('Invalid from address')
}
_validateRecipient (txParams) {
if (txParams.to === '0x' || txParams.to === null ) {
if (txParams.data) {
delete txParams.to
} else {
throw new Error('Invalid recipient address')
}
} else if ( txParams.to !== undefined && !ethUtil.isValidAddress(txParams.to) ) {
throw new Error('Invalid recipient address')
}
return txParams
}
_markNonceDuplicatesDropped (txId) {
this.txStateManager.setTxStatusConfirmed(txId)
// get the confirmed transactions nonce and from address

@ -1,6 +1,6 @@
// test and development environment variables
const env = process.env.METAMASK_ENV
const METAMASK_DEBUG = 'GULP_METAMASK_DEBUG'
const METAMASK_DEBUG = process.env.METAMASK_DEBUG
//
// The default state of MetaMask

@ -9,7 +9,7 @@ const setupDappAutoReload = require('./lib/auto-reload.js')
const MetamaskInpageProvider = require('./lib/inpage-provider.js')
restoreContextAfterImports()
const METAMASK_DEBUG = 'GULP_METAMASK_DEBUG'
const METAMASK_DEBUG = process.env.METAMASK_DEBUG
window.log = log
log.setDefaultLevel(METAMASK_DEBUG ? 'debug' : 'warn')

@ -0,0 +1,27 @@
const ethJsRpcSlug = 'Error: [ethjs-rpc] rpc error with payload '
const errorLabelPrefix = 'Error: '
module.exports = extractEthjsErrorMessage
//
// ethjs-rpc provides overly verbose error messages
// if we detect this type of message, we extract the important part
// Below is an example input and output
//
// Error: [ethjs-rpc] rpc error with payload {"id":3947817945380,"jsonrpc":"2.0","params":["0xf8eb8208708477359400830398539406012c8cf97bead5deae237070f9587f8e7a266d80b8843d7d3f5a0000000000000000000000000000000000000000000000000000000000081d1a000000000000000000000000000000000000000000000000001ff973cafa800000000000000000000000000000000000000000000000000000038d7ea4c68000000000000000000000000000000000000000000000000000000000000003f48025a04c32a9b630e0d9e7ff361562d850c86b7a884908135956a7e4a336fa0300d19ca06830776423f25218e8d19b267161db526e66895567147015b1f3fc47aef9a3c7"],"method":"eth_sendRawTransaction"} Error: replacement transaction underpriced
//
// Transaction Failed: replacement transaction underpriced
//
function extractEthjsErrorMessage(errorMessage) {
const isEthjsRpcError = errorMessage.includes(ethJsRpcSlug)
if (isEthjsRpcError) {
const payloadAndError = errorMessage.slice(ethJsRpcSlug.length)
const originalError = payloadAndError.slice(payloadAndError.indexOf(errorLabelPrefix) + errorLabelPrefix.length)
return originalError
} else {
return errorMessage
}
}

@ -0,0 +1,18 @@
const extension = require('extensionizer')
const promisify = require('pify')
const allLocales = require('../../_locales/index.json')
const existingLocaleCodes = allLocales.map(locale => locale.code.toLowerCase().replace('_', '-'))
async function getFirstPreferredLangCode () {
const userPreferredLocaleCodes = await promisify(
extension.i18n.getAcceptLanguages,
{ errorFirst: false }
)()
const firstPreferredLangCode = userPreferredLocaleCodes
.map(code => code.toLowerCase())
.find(code => existingLocaleCodes.includes(code))
return firstPreferredLangCode || 'en'
}
module.exports = getFirstPreferredLangCode

@ -0,0 +1,33 @@
const clone = require('clone')
module.exports = getObjStructure
// This will create an object that represents the structure of the given object
// it replaces all values with the result of their type
// {
// "data": {
// "CurrencyController": {
// "conversionDate": "number",
// "conversionRate": "number",
// "currentCurrency": "string"
// }
// }
function getObjStructure(obj) {
const structure = clone(obj)
return deepMap(structure, (value) => {
return value === null ? 'null' : typeof value
})
}
function deepMap(target = {}, visit) {
Object.entries(target).forEach(([key, value]) => {
if (typeof value === 'object' && value !== null) {
target[key] = deepMap(value, visit)
} else {
target[key] = visit(value)
}
})
return target
}

@ -1,6 +1,9 @@
class Migrator {
const EventEmitter = require('events')
class Migrator extends EventEmitter {
constructor (opts = {}) {
super()
const migrations = opts.migrations || []
// sort migrations by version
this.migrations = migrations.sort((a, b) => a.version - b.version)
@ -12,13 +15,29 @@ class Migrator {
// run all pending migrations on meta in place
async migrateData (versionedData = this.generateInitialState()) {
// get all migrations that have not yet been run
const pendingMigrations = this.migrations.filter(migrationIsPending)
// perform each migration
for (const index in pendingMigrations) {
const migration = pendingMigrations[index]
versionedData = await migration.migrate(versionedData)
if (!versionedData.data) throw new Error('Migrator - migration returned empty data')
if (versionedData.version !== undefined && versionedData.meta.version !== migration.version) throw new Error('Migrator - Migration did not update version number correctly')
try {
// attempt migration and validate
const migratedData = await migration.migrate(versionedData)
if (!migratedData.data) throw new Error('Migrator - migration returned empty data')
if (migratedData.version !== undefined && migratedData.meta.version !== migration.version) throw new Error('Migrator - Migration did not update version number correctly')
// accept the migration as good
versionedData = migratedData
} catch (err) {
// rewrite error message to add context without clobbering stack
const originalErrorMessage = err.message
err.message = `MetaMask Migration Error #${migration.version}: ${originalErrorMessage}`
console.warn(err.stack)
// emit error instead of throw so as to not break the run (gracefully fail)
this.emit('error', err)
// stop migrating and use state as is
return versionedData
}
}
return versionedData

@ -31,14 +31,13 @@ class NonceTracker {
const networkNonceResult = await this._getNetworkNextNonce(address)
const highestLocallyConfirmed = this._getHighestLocallyConfirmed(address)
const nextNetworkNonce = networkNonceResult.nonce
const highestLocalNonce = highestLocallyConfirmed
const highestSuggested = Math.max(nextNetworkNonce, highestLocalNonce)
const highestSuggested = Math.max(nextNetworkNonce, highestLocallyConfirmed)
const pendingTxs = this.getPendingTransactions(address)
const localNonceResult = this._getHighestContinuousFrom(pendingTxs, highestSuggested) || 0
nonceDetails.params = {
highestLocalNonce,
highestLocallyConfirmed,
highestSuggested,
nextNetworkNonce,
}

@ -1,5 +1,4 @@
const ethJsRpcSlug = 'Error: [ethjs-rpc] rpc error with payload '
const errorLabelPrefix = 'Error: '
const extractEthjsErrorMessage = require('./extractEthjsErrorMessage')
module.exports = reportFailedTxToSentry
@ -9,30 +8,9 @@ module.exports = reportFailedTxToSentry
//
function reportFailedTxToSentry({ raven, txMeta }) {
const errorMessage = extractErrorMessage(txMeta.err.message)
const errorMessage = 'Transaction Failed: ' + extractEthjsErrorMessage(txMeta.err.message)
raven.captureMessage(errorMessage, {
// "extra" key is required by Sentry
extra: txMeta,
})
}
//
// ethjs-rpc provides overly verbose error messages
// if we detect this type of message, we extract the important part
// Below is an example input and output
//
// Error: [ethjs-rpc] rpc error with payload {"id":3947817945380,"jsonrpc":"2.0","params":["0xf8eb8208708477359400830398539406012c8cf97bead5deae237070f9587f8e7a266d80b8843d7d3f5a0000000000000000000000000000000000000000000000000000000000081d1a000000000000000000000000000000000000000000000000001ff973cafa800000000000000000000000000000000000000000000000000000038d7ea4c68000000000000000000000000000000000000000000000000000000000000003f48025a04c32a9b630e0d9e7ff361562d850c86b7a884908135956a7e4a336fa0300d19ca06830776423f25218e8d19b267161db526e66895567147015b1f3fc47aef9a3c7"],"method":"eth_sendRawTransaction"} Error: replacement transaction underpriced
//
// Transaction Failed: replacement transaction underpriced
//
function extractErrorMessage(errorMessage) {
const isEthjsRpcError = errorMessage.includes(ethJsRpcSlug)
if (isEthjsRpcError) {
const payloadAndError = errorMessage.slice(ethJsRpcSlug.length)
const originalError = payloadAndError.slice(payloadAndError.indexOf(errorLabelPrefix) + errorLabelPrefix.length)
return `Transaction Failed: ${originalError}`
} else {
return `Transaction Failed: ${errorMessage}`
}
}

@ -1,5 +1,6 @@
const Raven = require('raven-js')
const METAMASK_DEBUG = 'GULP_METAMASK_DEBUG'
const METAMASK_DEBUG = process.env.METAMASK_DEBUG
const extractEthjsErrorMessage = require('./extractEthjsErrorMessage')
const PROD = 'https://3567c198f8a8412082d32655da2961d0@sentry.io/273505'
const DEV = 'https://f59f3dd640d2429d9d0e2445a87ea8e1@sentry.io/273496'
@ -21,8 +22,22 @@ function setupRaven(opts) {
const client = Raven.config(ravenTarget, {
release,
transport: function(opts) {
// modify report urls
const report = opts.data
// simplify certain complex error messages
report.exception.values.forEach(item => {
let errorMessage = item.value
// simplify ethjs error messages
errorMessage = extractEthjsErrorMessage(errorMessage)
// simplify 'Transaction Failed: known transaction'
if (errorMessage.indexOf('Transaction Failed: known transaction') === 0) {
// cut the hash from the error message
errorMessage = 'Transaction Failed: known transaction'
}
// finalize
item.value = errorMessage
})
// modify report urls
rewriteReportUrls(report)
// make request normally
client._makeRequest(opts)

@ -4,7 +4,7 @@ const {
BnMultiplyByFraction,
bnToHex,
} = require('./util')
const { addHexPrefix, isValidAddress } = require('ethereumjs-util')
const { addHexPrefix } = require('ethereumjs-util')
const SIMPLE_GAS_COST = '0x5208' // Hex for 21000, cost of a simple send.
/*
@ -52,7 +52,9 @@ module.exports = class TxGasUtil {
// if recipient has no code, gas is 21k max:
const recipient = txParams.to
const hasRecipient = Boolean(recipient)
const code = await this.query.getCode(recipient)
let code
if (recipient) code = await this.query.getCode(recipient)
if (hasRecipient && (!code || code === '0x')) {
txParams.gas = SIMPLE_GAS_COST
txMeta.simpleSend = true // Prevents buffer addition
@ -98,30 +100,4 @@ module.exports = class TxGasUtil {
// otherwise use blockGasLimit
return bnToHex(upperGasLimitBn)
}
async validateTxParams (txParams) {
this.validateRecipient(txParams)
if ('value' in txParams) {
const value = txParams.value.toString()
if (value.includes('-')) {
throw new Error(`Invalid transaction value of ${txParams.value} not a positive number.`)
}
if (value.includes('.')) {
throw new Error(`Invalid transaction value of ${txParams.value} number must be in wei`)
}
}
}
validateRecipient (txParams) {
if (txParams.to === '0x' || txParams.to === null ) {
if (txParams.data) {
delete txParams.to
} else {
throw new Error('Invalid recipient address')
}
} else if ( txParams.to !== undefined && !isValidAddress(txParams.to) ) {
throw new Error('Invalid recipient address')
}
return txParams
}
}

@ -38,11 +38,6 @@ module.exports = class TransactionStateManager extends EventEmitter {
}, opts)
}
// Returns the number of txs for the current network.
getTxCount () {
return this.getTxList().length
}
getTxList () {
const network = this.getNetwork()
const fullTxList = this.getFullTxList()
@ -88,7 +83,7 @@ module.exports = class TransactionStateManager extends EventEmitter {
txMeta.history.push(snapshot)
const transactions = this.getFullTxList()
const txCount = this.getTxCount()
const txCount = transactions.length
const txHistoryLimit = this.txHistoryLimit
// checks if the length of the tx history is
@ -111,12 +106,13 @@ module.exports = class TransactionStateManager extends EventEmitter {
}
updateTx (txMeta, note) {
// validate txParams
if (txMeta.txParams) {
Object.keys(txMeta.txParams).forEach((key) => {
const value = txMeta.txParams[key]
if (typeof value !== 'string') console.error(`${key}: ${value} in txParams is not a string`)
if (!ethUtil.isHexPrefixed(value)) console.error('is not hex prefixed, anything on txParams must be hex prefixed')
})
if (typeof txMeta.txParams.data === 'undefined') {
delete txMeta.txParams.data
}
this.validateTxParams(txMeta.txParams)
}
// create txMeta snapshot for history
@ -144,6 +140,23 @@ module.exports = class TransactionStateManager extends EventEmitter {
this.updateTx(txMeta, `txStateManager#updateTxParams`)
}
// validates txParams members by type
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})`)
if (!ethUtil.isHexPrefixed(value)) throw new Error(`${key} in txParams is not hex prefixed. got: (${value})`)
break
}
})
}
/*
Takes an object of fields to search for eg:
let thingsToLookFor = {

@ -57,7 +57,6 @@ module.exports = class MetamaskController extends EventEmitter {
this.defaultMaxListeners = 20
this.sendUpdate = debounce(this.privateSendUpdate.bind(this), 200)
this.opts = opts
const initState = opts.initState || {}
this.recordFirstTimeInfo(initState)
@ -82,6 +81,7 @@ module.exports = class MetamaskController extends EventEmitter {
// preferences controller
this.preferencesController = new PreferencesController({
initState: initState.PreferencesController,
initLangCode: opts.initLangCode,
})
// currency controller
@ -241,6 +241,11 @@ module.exports = class MetamaskController extends EventEmitter {
static: {
eth_syncing: false,
web3_clientVersion: `MetaMask/v${version}`,
eth_sendTransaction: (payload, next, end) => {
const origin = payload.origin
const txParams = payload.params[0]
nodeify(this.txController.newUnapprovedTransaction, this.txController)(txParams, { origin }, end)
},
},
// account mgmt
getAccounts: (cb) => {
@ -255,7 +260,6 @@ module.exports = class MetamaskController extends EventEmitter {
cb(null, result)
},
// tx signing
processTransaction: nodeify(async (txParams) => await this.txController.newUnapprovedTransaction(txParams), this),
// old style msg signing
processMessage: this.newUnsignedMessage.bind(this),
// personal_sign msg signing
@ -351,6 +355,7 @@ module.exports = class MetamaskController extends EventEmitter {
getState: (cb) => cb(null, this.getState()),
setCurrentCurrency: this.setCurrentCurrency.bind(this),
setUseBlockie: this.setUseBlockie.bind(this),
setCurrentLocale: this.setCurrentLocale.bind(this),
markAccountsFound: this.markAccountsFound.bind(this),
markPasswordForgotten: this.markPasswordForgotten.bind(this),
unMarkPasswordForgotten: this.unMarkPasswordForgotten.bind(this),
@ -365,7 +370,7 @@ module.exports = class MetamaskController extends EventEmitter {
placeSeedWords: this.placeSeedWords.bind(this),
verifySeedPhrase: nodeify(this.verifySeedPhrase, this),
clearSeedWordCache: this.clearSeedWordCache.bind(this),
resetAccount: this.resetAccount.bind(this),
resetAccount: nodeify(this.resetAccount, this),
importAccountWithStrategy: this.importAccountWithStrategy.bind(this),
// vault management
@ -583,10 +588,15 @@ module.exports = class MetamaskController extends EventEmitter {
/**
* ?
*/
resetAccount (cb) {
async resetAccount (cb) {
const selectedAddress = this.preferencesController.getSelectedAddress()
this.txController.wipeTransactions(selectedAddress)
cb(null, selectedAddress)
const networkController = this.networkController
const oldType = networkController.getProviderConfig().type
await networkController.setProviderType(oldType, true)
return selectedAddress
}
/**
@ -1029,6 +1039,15 @@ module.exports = class MetamaskController extends EventEmitter {
}
}
setCurrentLocale (key, cb) {
try {
this.preferencesController.setCurrentLocale(key)
cb(null)
} catch (err) {
cb(err)
}
}
recordFirstTimeInfo (initState) {
if (!('firstTimeInfo' in initState)) {
initState.firstTimeInfo = {

@ -27,8 +27,11 @@ module.exports = {
function transformState (state) {
const newState = state
if (newState.config.provider.type === 'testnet') {
newState.config.provider.type = 'ropsten'
const { config } = newState
if ( config && config.provider ) {
if (config.provider.type === 'testnet') {
newState.config.provider.type = 'ropsten'
}
}
return newState
}

@ -28,11 +28,14 @@ module.exports = {
function transformState (state) {
const newState = state
const transactions = newState.TransactionController.transactions
newState.TransactionController.transactions = transactions.map((txMeta) => {
if (!txMeta.err) return txMeta
else if (txMeta.err.message === 'Gave up submitting tx.') txMeta.status = 'failed'
return txMeta
})
const { TransactionController } = newState
if (TransactionController && TransactionController.transactions) {
const transactions = TransactionController.transactions
newState.TransactionController.transactions = transactions.map((txMeta) => {
if (!txMeta.err) return txMeta
else if (txMeta.err.message === 'Gave up submitting tx.') txMeta.status = 'failed'
return txMeta
})
}
return newState
}

@ -28,14 +28,18 @@ module.exports = {
function transformState (state) {
const newState = state
const transactions = newState.TransactionController.transactions
newState.TransactionController.transactions = transactions.map((txMeta) => {
if (!txMeta.err) return txMeta
if (txMeta.err === 'transaction with the same hash was already imported.') {
txMeta.status = 'submitted'
delete txMeta.err
}
return txMeta
})
const { TransactionController } = newState
if (TransactionController && TransactionController.transactions) {
const transactions = newState.TransactionController.transactions
newState.TransactionController.transactions = transactions.map((txMeta) => {
if (!txMeta.err) return txMeta
if (txMeta.err === 'transaction with the same hash was already imported.') {
txMeta.status = 'submitted'
delete txMeta.err
}
return txMeta
})
}
return newState
}

@ -27,14 +27,17 @@ module.exports = {
function transformState (state) {
const newState = state
const transactions = newState.TransactionController.transactions
newState.TransactionController.transactions = transactions.map((txMeta) => {
if (!txMeta.status === 'failed') return txMeta
if (txMeta.retryCount > 0 && txMeta.retryCount < 2) {
txMeta.status = 'submitted'
delete txMeta.err
}
return txMeta
})
const { TransactionController } = newState
if (TransactionController && TransactionController.transactions) {
const transactions = newState.TransactionController.transactions
newState.TransactionController.transactions = transactions.map((txMeta) => {
if (!txMeta.status === 'failed') return txMeta
if (txMeta.retryCount > 0 && txMeta.retryCount < 2) {
txMeta.status = 'submitted'
delete txMeta.err
}
return txMeta
})
}
return newState
}

@ -29,24 +29,27 @@ module.exports = {
function transformState (state) {
const newState = state
const transactions = newState.TransactionController.transactions
newState.TransactionController.transactions = transactions.map((txMeta) => {
// no history: initialize
if (!txMeta.history || txMeta.history.length === 0) {
const snapshot = txStateHistoryHelper.snapshotFromTxMeta(txMeta)
txMeta.history = [snapshot]
const { TransactionController } = newState
if (TransactionController && TransactionController.transactions) {
const transactions = newState.TransactionController.transactions
newState.TransactionController.transactions = transactions.map((txMeta) => {
// no history: initialize
if (!txMeta.history || txMeta.history.length === 0) {
const snapshot = txStateHistoryHelper.snapshotFromTxMeta(txMeta)
txMeta.history = [snapshot]
return txMeta
}
// has history: migrate
const newHistory = (
txStateHistoryHelper.migrateFromSnapshotsToDiffs(txMeta.history)
// remove empty diffs
.filter((entry) => {
return !Array.isArray(entry) || entry.length > 0
})
)
txMeta.history = newHistory
return txMeta
}
// has history: migrate
const newHistory = (
txStateHistoryHelper.migrateFromSnapshotsToDiffs(txMeta.history)
// remove empty diffs
.filter((entry) => {
return !Array.isArray(entry) || entry.length > 0
})
)
txMeta.history = newHistory
return txMeta
})
})
}
return newState
}

@ -29,32 +29,36 @@ module.exports = {
function transformState (state) {
const newState = state
const transactions = newState.TransactionController.transactions
const { TransactionController } = newState
if (TransactionController && TransactionController.transactions) {
newState.TransactionController.transactions = transactions.map((txMeta, _, txList) => {
if (txMeta.status !== 'submitted') return txMeta
const transactions = newState.TransactionController.transactions
const confirmedTxs = txList.filter((tx) => tx.status === 'confirmed')
.filter((tx) => tx.txParams.from === txMeta.txParams.from)
.filter((tx) => tx.metamaskNetworkId.from === txMeta.metamaskNetworkId.from)
const highestConfirmedNonce = getHighestNonce(confirmedTxs)
newState.TransactionController.transactions = transactions.map((txMeta, _, txList) => {
if (txMeta.status !== 'submitted') return txMeta
const pendingTxs = txList.filter((tx) => tx.status === 'submitted')
.filter((tx) => tx.txParams.from === txMeta.txParams.from)
.filter((tx) => tx.metamaskNetworkId.from === txMeta.metamaskNetworkId.from)
const highestContinuousNonce = getHighestContinuousFrom(pendingTxs, highestConfirmedNonce)
const confirmedTxs = txList.filter((tx) => tx.status === 'confirmed')
.filter((tx) => tx.txParams.from === txMeta.txParams.from)
.filter((tx) => tx.metamaskNetworkId.from === txMeta.metamaskNetworkId.from)
const highestConfirmedNonce = getHighestNonce(confirmedTxs)
const maxNonce = Math.max(highestContinuousNonce, highestConfirmedNonce)
const pendingTxs = txList.filter((tx) => tx.status === 'submitted')
.filter((tx) => tx.txParams.from === txMeta.txParams.from)
.filter((tx) => tx.metamaskNetworkId.from === txMeta.metamaskNetworkId.from)
const highestContinuousNonce = getHighestContinuousFrom(pendingTxs, highestConfirmedNonce)
if (parseInt(txMeta.txParams.nonce, 16) > maxNonce + 1) {
txMeta.status = 'failed'
txMeta.err = {
message: 'nonce too high',
note: 'migration 019 custom error',
const maxNonce = Math.max(highestContinuousNonce, highestConfirmedNonce)
if (parseInt(txMeta.txParams.nonce, 16) > maxNonce + 1) {
txMeta.status = 'failed'
txMeta.err = {
message: 'nonce too high',
note: 'migration 019 custom error',
}
}
}
return txMeta
})
return txMeta
})
}
return newState
}

@ -28,12 +28,15 @@ module.exports = {
function transformState (state) {
const newState = state
const transactions = newState.TransactionController.transactions
newState.TransactionController.transactions = transactions.map((txMeta) => {
if (txMeta.status !== 'submitted' || txMeta.submittedTime) return txMeta
txMeta.submittedTime = (new Date()).getTime()
return txMeta
})
const { TransactionController } = newState
if (TransactionController && TransactionController.transactions) {
const transactions = newState.TransactionController.transactions
newState.TransactionController.transactions = transactions.map((txMeta) => {
if (txMeta.status !== 'submitted' || txMeta.submittedTime) return txMeta
txMeta.submittedTime = (new Date()).getTime()
return txMeta
})
}
return newState
}

@ -0,0 +1,54 @@
const version = 23
/*
This migration removes transactions that are no longer usefull down to 40 total
*/
const clone = require('clone')
module.exports = {
version,
migrate: function (originalVersionedData) {
const versionedData = clone(originalVersionedData)
versionedData.meta.version = version
try {
const state = versionedData.data
const newState = transformState(state)
versionedData.data = newState
} catch (err) {
console.warn(`MetaMask Migration #${version}` + err.stack)
}
return Promise.resolve(versionedData)
},
}
function transformState (state) {
const newState = state
const { TransactionController } = newState
if (TransactionController && TransactionController.transactions) {
const transactions = newState.TransactionController.transactions
if (transactions.length <= 40) return newState
let reverseTxList = transactions.reverse()
let stripping = true
while (reverseTxList.length > 40 && stripping) {
let txIndex = reverseTxList.findIndex((txMeta) => {
return (txMeta.status === 'failed' ||
txMeta.status === 'rejected' ||
txMeta.status === 'confirmed' ||
txMeta.status === 'dropped')
})
if (txIndex < 0) stripping = false
else reverseTxList.splice(txIndex, 1)
}
newState.TransactionController.transactions = reverseTxList.reverse()
}
return newState
}

@ -0,0 +1,41 @@
const version = 24
/*
This migration ensures that the from address in txParams is to lower case for
all unapproved transactions
*/
const clone = require('clone')
module.exports = {
version,
migrate: async function (originalVersionedData) {
const versionedData = clone(originalVersionedData)
versionedData.meta.version = version
const state = versionedData.data
const newState = transformState(state)
versionedData.data = newState
return versionedData
},
}
function transformState (state) {
const newState = state
if (!newState.TransactionController) return newState
const transactions = newState.TransactionController.transactions
newState.TransactionController.transactions = transactions.map((txMeta, _, txList) => {
if (
txMeta.status === 'unapproved' &&
txMeta.txParams &&
txMeta.txParams.from
) {
txMeta.txParams.from = txMeta.txParams.from.toLowerCase()
}
return txMeta
})
return newState
}

@ -0,0 +1,61 @@
// next version number
const version = 25
/*
normalizes txParams on unconfirmed txs
*/
const ethUtil = require('ethereumjs-util')
const clone = require('clone')
module.exports = {
version,
migrate: async function (originalVersionedData) {
const versionedData = clone(originalVersionedData)
versionedData.meta.version = version
const state = versionedData.data
const newState = transformState(state)
versionedData.data = newState
return versionedData
},
}
function transformState (state) {
const newState = state
if (newState.TransactionController) {
if (newState.TransactionController.transactions) {
const transactions = newState.TransactionController.transactions
newState.TransactionController.transactions = transactions.map((txMeta) => {
if (txMeta.status !== 'unapproved') return txMeta
txMeta.txParams = normalizeTxParams(txMeta.txParams)
return txMeta
})
}
}
return newState
}
function normalizeTxParams (txParams) {
// functions that handle normalizing of that key in txParams
const whiteList = {
from: from => ethUtil.addHexPrefix(from).toLowerCase(),
to: to => ethUtil.addHexPrefix(txParams.to).toLowerCase(),
nonce: nonce => ethUtil.addHexPrefix(nonce),
value: value => ethUtil.addHexPrefix(value),
data: data => ethUtil.addHexPrefix(data),
gas: gas => ethUtil.addHexPrefix(gas),
gasPrice: gasPrice => ethUtil.addHexPrefix(gasPrice),
}
// apply only keys in the whiteList
const normalizedTxParams = {}
Object.keys(whiteList).forEach((key) => {
if (txParams[key]) normalizedTxParams[key] = whiteList[key](txParams[key])
})
return normalizedTxParams
}

@ -33,4 +33,7 @@ module.exports = [
require('./020'),
require('./021'),
require('./022'),
require('./023'),
require('./024'),
require('./025'),
]

@ -0,0 +1,29 @@
// next version number
const version = 0
/*
description of migration and what it does
*/
const clone = require('clone')
module.exports = {
version,
migrate: async function (originalVersionedData) {
const versionedData = clone(originalVersionedData)
versionedData.meta.version = version
const state = versionedData.data
const newState = transformState(state)
versionedData.data = newState
return versionedData
},
}
function transformState (state) {
const newState = state
// transform state here
return newState
}

@ -1,78 +0,0 @@
const injectCss = require('inject-css')
const OldMetaMaskUiCss = require('../../old-ui/css')
const NewMetaMaskUiCss = require('../../ui/css')
const startPopup = require('./popup-core')
const PortStream = require('./lib/port-stream.js')
const isPopupOrNotification = require('./lib/is-popup-or-notification')
const extension = require('extensionizer')
const ExtensionPlatform = require('./platforms/extension')
const NotificationManager = require('./lib/notification-manager')
const notificationManager = new NotificationManager()
const setupRaven = require('./lib/setupRaven')
// create platform global
global.platform = new ExtensionPlatform()
// setup sentry error reporting
const release = global.platform.getVersion()
setupRaven({ release })
// inject css
// const css = MetaMaskUiCss()
// injectCss(css)
// identify window type (popup, notification)
const windowType = isPopupOrNotification()
global.METAMASK_UI_TYPE = windowType
closePopupIfOpen(windowType)
// setup stream to background
const extensionPort = extension.runtime.connect({ name: windowType })
const connectionStream = new PortStream(extensionPort)
// start ui
const container = document.getElementById('app-content')
startPopup({ container, connectionStream }, (err, store) => {
if (err) return displayCriticalError(err)
// Code commented out until we begin auto adding users to NewUI
// const { isMascara, identities = {}, featureFlags = {} } = store.getState().metamask
// const firstTime = Object.keys(identities).length === 0
const { isMascara, featureFlags = {} } = store.getState().metamask
let betaUIState = featureFlags.betaUI
// Code commented out until we begin auto adding users to NewUI
// const useBetaCss = isMascara || firstTime || betaUIState
const useBetaCss = isMascara || betaUIState
let css = useBetaCss ? NewMetaMaskUiCss() : OldMetaMaskUiCss()
let deleteInjectedCss = injectCss(css)
let newBetaUIState
store.subscribe(() => {
const state = store.getState()
newBetaUIState = state.metamask.featureFlags.betaUI
if (newBetaUIState !== betaUIState) {
deleteInjectedCss()
betaUIState = newBetaUIState
css = betaUIState ? NewMetaMaskUiCss() : OldMetaMaskUiCss()
deleteInjectedCss = injectCss(css)
}
if (state.appState.shouldClose) notificationManager.closePopup()
})
})
function closePopupIfOpen (windowType) {
if (windowType !== 'notification') {
// should close only chrome popup
notificationManager.closePopup()
}
}
function displayCriticalError (err) {
container.innerHTML = '<div class="critical-error">The MetaMask app failed to load: please open and close MetaMask again to restart.</div>'
container.style.height = '80px'
log.error(err.stack)
throw err
}

@ -0,0 +1,84 @@
const injectCss = require('inject-css')
const OldMetaMaskUiCss = require('../../old-ui/css')
const NewMetaMaskUiCss = require('../../ui/css')
const startPopup = require('./popup-core')
const PortStream = require('./lib/port-stream.js')
const isPopupOrNotification = require('./lib/is-popup-or-notification')
const extension = require('extensionizer')
const ExtensionPlatform = require('./platforms/extension')
const NotificationManager = require('./lib/notification-manager')
const notificationManager = new NotificationManager()
const setupRaven = require('./lib/setupRaven')
start().catch(log.error)
async function start() {
// create platform global
global.platform = new ExtensionPlatform()
// setup sentry error reporting
const release = global.platform.getVersion()
setupRaven({ release })
// inject css
// const css = MetaMaskUiCss()
// injectCss(css)
// identify window type (popup, notification)
const windowType = isPopupOrNotification()
global.METAMASK_UI_TYPE = windowType
closePopupIfOpen(windowType)
// setup stream to background
const extensionPort = extension.runtime.connect({ name: windowType })
const connectionStream = new PortStream(extensionPort)
// start ui
const container = document.getElementById('app-content')
startPopup({ container, connectionStream }, (err, store) => {
if (err) return displayCriticalError(err)
// Code commented out until we begin auto adding users to NewUI
// const { isMascara, identities = {}, featureFlags = {} } = store.getState().metamask
// const firstTime = Object.keys(identities).length === 0
const { isMascara, featureFlags = {} } = store.getState().metamask
let betaUIState = featureFlags.betaUI
// Code commented out until we begin auto adding users to NewUI
// const useBetaCss = isMascara || firstTime || betaUIState
const useBetaCss = isMascara || betaUIState
let css = useBetaCss ? NewMetaMaskUiCss() : OldMetaMaskUiCss()
let deleteInjectedCss = injectCss(css)
let newBetaUIState
store.subscribe(() => {
const state = store.getState()
newBetaUIState = state.metamask.featureFlags.betaUI
if (newBetaUIState !== betaUIState) {
deleteInjectedCss()
betaUIState = newBetaUIState
css = betaUIState ? NewMetaMaskUiCss() : OldMetaMaskUiCss()
deleteInjectedCss = injectCss(css)
}
if (state.appState.shouldClose) notificationManager.closePopup()
})
})
function closePopupIfOpen (windowType) {
if (windowType !== 'notification') {
// should close only chrome popup
notificationManager.closePopup()
}
}
function displayCriticalError (err) {
container.innerHTML = '<div class="critical-error">The MetaMask app failed to load: please open and close MetaMask again to restart.</div>'
container.style.height = '80px'
log.error(err.stack)
throw err
}
}

@ -1,6 +1,8 @@
const fs = require('fs')
const path = require('path')
const promisify = require('pify')
const enLocaleMessages = require('../app/_locales/en/messages.json')
start().catch(console.error)
@ -12,6 +14,9 @@ async function start () {
const stateFilePath = path.join(__dirname, 'states', stateFileName)
const stateFileContent = await promisify(fs.readFile)(stateFilePath, 'utf8')
const state = JSON.parse(stateFileContent)
state.localeMessages = { en: enLocaleMessages, current: {} }
const stateName = stateFileName.split('.')[0].replace(/-/g, ' ', 'g')
states[stateName] = state
}))

@ -0,0 +1,62 @@
#!/usr/bin/env node
const request = require('request-promise')
const VERSION = require('../dist/chrome/manifest.json').version
start().catch(console.error)
async function start() {
const GITHUB_COMMENT_TOKEN = process.env.GITHUB_COMMENT_TOKEN
const CIRCLE_PULL_REQUEST = process.env.CIRCLE_PULL_REQUEST
console.log('CIRCLE_PULL_REQUEST', CIRCLE_PULL_REQUEST)
const CIRCLE_SHA1 = process.env.CIRCLE_SHA1
console.log('CIRCLE_SHA1', CIRCLE_SHA1)
const CIRCLE_BUILD_NUM = process.env.CIRCLE_BUILD_NUM
console.log('CIRCLE_BUILD_NUM', CIRCLE_BUILD_NUM)
if (!CIRCLE_PULL_REQUEST) {
console.warn(`No pull request detected for commit "${CIRCLE_SHA1}"`)
return
}
const CIRCLE_PR_NUMBER = CIRCLE_PULL_REQUEST.split('/').pop()
const SHORT_SHA1 = CIRCLE_SHA1.slice(0,7)
const BUILD_LINK_BASE = `https://${CIRCLE_BUILD_NUM}-42009758-gh.circle-artifacts.com/0`
const MASCARA = `${BUILD_LINK_BASE}/builds/mascara/home.html`
const CHROME = `${BUILD_LINK_BASE}/builds/metamask-chrome-${VERSION}.zip`
const FIREFOX = `${BUILD_LINK_BASE}/builds/metamask-firefox-${VERSION}.zip`
const EDGE = `${BUILD_LINK_BASE}/builds/metamask-edge-${VERSION}.zip`
const OPERA = `${BUILD_LINK_BASE}/builds/metamask-opera-${VERSION}.zip`
const WALKTHROUGH = `${BUILD_LINK_BASE}/test-artifacts/screens/walkthrough%20%28en%29.gif`
const commentBody = `
<details>
<summary>
Builds ready [${SHORT_SHA1}]:
<a href="${MASCARA}">mascara</a>,
<a href="${CHROME}">chrome</a>,
<a href="${FIREFOX}">firefox</a>,
<a href="${EDGE}">edge</a>,
<a href="${OPERA}">opera</a>
</summary>
<image src="${WALKTHROUGH}">
</details>
`
const JSON_PAYLOAD = JSON.stringify({ body: commentBody })
const POST_COMMENT_URI = `https://api.github.com/repos/metamask/metamask-extension/issues/${CIRCLE_PR_NUMBER}/comments`
console.log(`Announcement:\n${commentBody}`)
console.log(`Posting to: ${POST_COMMENT_URI}`)
await request({
method: 'POST',
uri: POST_COMMENT_URI,
body: JSON_PAYLOAD,
headers: {
'User-Agent': 'metamaskbot',
'Authorization': `token ${GITHUB_COMMENT_TOKEN}`,
},
})
}

@ -0,0 +1,55 @@
#!/usr/bin/env node
const pify = require('pify')
const exec = pify(require('child_process').exec, { multiArgs: true })
const VERSION = require('../dist/chrome/manifest.json').version
start().catch(console.error)
async function start(){
const authWorked = await checkIfAuthWorks()
if (!authWorked) {
console.log(`Sentry auth failed...`)
}
// check if version exists or not
const versionAlreadyExists = await checkIfVersionExists()
// abort if versions exists
if (versionAlreadyExists) {
console.log(`Version "${VERSION}" already exists on Sentry, aborting sourcemap upload.`)
return
}
// create sentry release
console.log(`creating Sentry release for "${VERSION}"...`)
await exec(`sentry-cli releases --org 'metamask' --project 'metamask' new ${VERSION}`)
console.log(`removing any existing files from Sentry release "${VERSION}"...`)
await exec(`sentry-cli releases --org 'metamask' --project 'metamask' files ${VERSION} delete --all`)
// upload sentry source and sourcemaps
console.log(`uploading source files Sentry release "${VERSION}"...`)
await exec(`for FILEPATH in ./dist/chrome/*.js; do [ -e $FILEPATH ] || continue; export FILE=\`basename $FILEPATH\` && echo uploading $FILE && sentry-cli releases --org 'metamask' --project 'metamask' files ${VERSION} upload $FILEPATH metamask/$FILE; done;`)
console.log(`uploading sourcemaps Sentry release "${VERSION}"...`)
await exec(`sentry-cli releases --org 'metamask' --project 'metamask' files ${VERSION} upload-sourcemaps ./dist/sourcemaps/ --url-prefix 'sourcemaps'`)
console.log('all done!')
}
async function checkIfAuthWorks() {
const itWorked = await doesNotFail(async () => {
await exec(`sentry-cli releases --org 'metamask' --project 'metamask' list`)
})
return itWorked
}
async function checkIfVersionExists() {
const versionAlreadyExists = await doesNotFail(async () => {
await exec(`sentry-cli releases --org 'metamask' --project 'metamask' info ${VERSION}`)
})
return versionAlreadyExists
}
async function doesNotFail(asyncFn) {
try {
await asyncFn()
return true
} catch (err) {
return false
}
}

@ -106,7 +106,8 @@
"errors": {},
"maxModeOn": false,
"editingTransactionId": null
}
},
"currentLocale": "en"
},
"appState": {
"menuOpen": false,

@ -128,7 +128,8 @@
"errors": {},
"maxModeOn": false,
"editingTransactionId": null
}
},
"currentLocale": "en"
},
"appState": {
"menuOpen": false,

@ -149,7 +149,8 @@
"errors": {},
"maxModeOn": false,
"editingTransactionId": null
}
},
"currentLocale": "en"
},
"appState": {
"menuOpen": false,

@ -36,7 +36,8 @@
},
"shapeShiftTxList": [],
"lostAccounts": [],
"tokens": []
"tokens": [],
"currentLocale": "en"
},
"appState": {
"menuOpen": false,

@ -128,7 +128,8 @@
"errors": {},
"maxModeOn": false,
"editingTransactionId": null
}
},
"currentLocale": "en"
},
"appState": {
"menuOpen": false,

@ -107,7 +107,8 @@
"errors": {},
"maxModeOn": false,
"editingTransactionId": null
}
},
"currentLocale": "en"
},
"appState": {
"menuOpen": false,

File diff suppressed because one or more lines are too long

@ -10,87 +10,88 @@
//
////////////////////////////////////////////////////////////////////////////////
var fs = require('fs')
var path = require('path')
const fs = require('fs')
const path = require('path')
const localeIndex = require('../app/_locales/index.json')
console.log('Locale Verification')
var locale = process.argv[2]
if (!locale || locale == '') {
console.log('Must enter a locale as argument. exitting')
process.exit(1)
const specifiedLocale = process.argv[2]
if (specifiedLocale) {
console.log(`Verifying selected locale "${specifiedLocale}":\n\n`)
const locale = localeIndex.find(localeMeta => localeMeta.code === specifiedLocale)
verifyLocale({ localeMeta })
} else {
console.log('Verifying all locales:\n\n')
localeIndex.forEach(localeMeta => {
verifyLocale({ localeMeta })
console.log('\n')
})
}
console.log("verifying for locale " + locale)
localeFilePath = path.join(process.cwd(), 'app', '_locales', locale, 'messages.json')
try {
localeObj = JSON.parse(fs.readFileSync(localeFilePath, 'utf8'));
} catch (e) {
if(e.code == 'ENOENT') {
console.log('Locale file not found')
} else {
console.log('Error opening your locale file: ', e)
function verifyLocale({ localeMeta }) {
const localeCode = localeMeta.code
const localeName = localeMeta.name
try {
const localeFilePath = path.join(process.cwd(), 'app', '_locales', localeCode, 'messages.json')
targetLocale = JSON.parse(fs.readFileSync(localeFilePath, 'utf8'));
} catch (e) {
if (e.code == 'ENOENT') {
console.log('Locale file not found')
} else {
console.log(`Error opening your locale ("${localeCode}") file: `, e)
}
process.exit(1)
}
process.exit(1)
}
englishFilePath = path.join(process.cwd(), 'app', '_locales', 'en', 'messages.json')
try {
englishObj = JSON.parse(fs.readFileSync(englishFilePath, 'utf8'));
} catch (e) {
if(e.code == 'ENOENT') {
console.log("English File not found")
} else {
console.log("Error opening english locale file: ", e)
try {
const englishFilePath = path.join(process.cwd(), 'app', '_locales', 'en', 'messages.json')
englishLocale = JSON.parse(fs.readFileSync(englishFilePath, 'utf8'));
} catch (e) {
if(e.code == 'ENOENT') {
console.log('English File not found')
} else {
console.log('Error opening english locale file: ', e)
}
process.exit(1)
}
process.exit(1)
}
console.log('\tverifying whether all your locale strings are contained in the english one')
// console.log(' verifying whether all your locale ("${localeCode}") strings are contained in the english one')
const extraItems = compareLocalesForMissingItems({ base: targetLocale, subject: englishLocale })
// console.log('\n verifying whether your locale ("${localeCode}") contains all english strings')
const missingItems = compareLocalesForMissingItems({ base: englishLocale, subject: targetLocale })
var counter = 0
var foundErrorA = false
var notFound = [];
Object.keys(localeObj).forEach(function(key){
if (!englishObj[key]) {
foundErrorA = true
notFound.push(key)
}
counter++
})
const englishEntryCount = Object.keys(englishLocale).length
const coveragePercent = 100 * (englishEntryCount - missingItems.length) / englishEntryCount
if (foundErrorA) {
console.log('\nThe following string(s) is(are) not found in the english locale:')
notFound.forEach(function(key) {
console.log(key)
})
} else {
console.log('\tall ' + counter +' strings declared in your locale were found in the english one')
}
console.log(`Status of **${localeName} (${localeCode})** ${coveragePercent.toFixed(2)}% coverage:`)
console.log('\n\tverifying whether your locale contains all english strings')
if (extraItems.length) {
console.log('\nMissing from english locale:')
extraItems.forEach(function(key) {
console.log(` - [ ] ${key}`)
})
} else {
// console.log(` all ${counter} strings declared in your locale ("${localeCode}") were found in the english one`)
}
var counter = 0
var foundErrorB = false
var notFound = [];
Object.keys(englishObj).forEach(function(key){
if (!localeObj[key]) {
foundErrorB = true
notFound.push(key)
if (missingItems.length) {
console.log(`\nMissing:`)
missingItems.forEach(function(key) {
console.log(` - [ ] ${key}`)
})
} else {
// console.log(` all ${counter} english strings were found in your locale ("${localeCode}")!`)
}
counter++
})
if (foundErrorB) {
console.log('\nThe following string(s) is(are) not found in the your locale:')
notFound.forEach(function(key) {
console.log(key)
})
} else {
console.log('\tall ' + counter +' english strings were found in your locale!')
if (!extraItems.length && !missingItems.length) {
console.log('Full coverage : )')
}
}
if (!foundErrorA && !foundErrorB) {
console.log('You are good to go')
function compareLocalesForMissingItems({ base, subject }) {
return Object.keys(base).filter((key) => !subject[key])
}

@ -0,0 +1,48 @@
# QA Guide
Steps to mark a full pass of QA complete.
* Browsers: Opera, Chrome, Firefox, Edge.
* OS: Ubuntu, Mac OSX, Windows
* Load older version of MetaMask and attempt to simulate updating the extension.
* Open Developer Console in background and popup, inspect errors.
* Watch the state logs
* Transactions (unapproved txs -> rejected/submitted -> confirmed)
* Nonces/LocalNonces
* Vault integrity
* create vault
* Log out
* Log in again
* Log out
* Restore from seed
* Create a second account
* Import a loose account (not related to HD Wallet)
* Import old existing vault seed phrase (pref with test Ether)
* Download State Logs, Priv key file, seed phrase file.
* Send Ether
* by address
* by ens name
* Web3 API Stability
* Create a contract from a Ðapp (remix)
* Load a Ðapp that reads using events/logs (ENS)
* Connect to MEW/MyCypto
* Send a transaction from any Ðapp
- MEW
- EtherDelta
- Leeroy
- Aragon
- (https://tmashuang.github.io/demo-dapp)
* Check account balances
* Token Management
* create a token with tokenfactory (http://tokenfactory.surge.sh/#/factory)
* Add that token to the token view
* Send that token to another metamask address.
* confirm the token arrived.
* Send a transaction and sign a message (https://danfinlay.github.io/js-eth-personal-sign-examples/) for each keyring type
* hd keyring
* imported keyring
* Change network from mainnet → ropsten → rinkeby → localhost (ganache)
* Ganache set blocktime to simulate retryTx in MetaMask
* Copy public key to clipboard
* Export private key
* Explore changes in master, target features that have been changed and break.

@ -6,9 +6,12 @@ The MetaMask browser extension supports new translations added in the form of ne
## Adding a new Language
Each supported language is represented by a folder in `app/_locales` whose name is that language's subtag ([look up a language subtag using this tool](https://r12a.github.io/app-subtags/)).
- Each supported language is represented by a folder in `app/_locales` whose name is that language's subtag (example: `app/_locales/es/`). (look up a language subtag using the [r12a "Find" tool](https://r12a.github.io/app-subtags/) or this [wikipedia list](https://en.wikipedia.org/wiki/List_of_ISO_639-1_codes)).
- Inside that folder there should be a `messages.json`.
- An easy way to start your translation is to first **make a copy** of `app/_locales/en/messages.json` (the english translation), and then **translate the `message` key** for each in-app message.
- **The `description` key** is just to add context for what the translation is about, it **does not need to be translated**.
- Add the language to the [locales index](https://github.com/MetaMask/metamask-extension/blob/master/app/_locales/index.json) `app/_locales/index.json`
Inside that folder there should be a `messages.json` file that follows the specified format. An easy way to start your translation is to first duplicate `app/_locales/en/messages.json` (the english translation), and then update the `message` key for each in-app message.
That's it! When MetaMask is loaded on a computer with that language set as the system language, they will see your translation instead of the default one.
@ -20,7 +23,7 @@ To automatically see if you are missing any phrases to translate, we have a scri
node development/verify-locale-strings.js $YOUR_LOCALE
```
Where `$YOUR_LOCALE` is your [locale string](https://r12a.github.io/app-subtags/), i.e. the name of your language folder.
Where `$YOUR_LOCALE` is your locale string (example: `es`), i.e. the name of your language folder.
To verify that your translation works in the app, you will need to [build a local copy](https://github.com/MetaMask/metamask-extension#building-locally) of MetaMask. You will need to change your browser language, your operating system language, and restart your browser (sorry it's so much work!).

@ -1,37 +1,55 @@
var watchify = require('watchify')
var browserify = require('browserify')
var disc = require('disc')
var gulp = require('gulp')
var source = require('vinyl-source-stream')
var buffer = require('vinyl-buffer')
var gutil = require('gulp-util')
var watch = require('gulp-watch')
var sourcemaps = require('gulp-sourcemaps')
var jsoneditor = require('gulp-json-editor')
var zip = require('gulp-zip')
var assign = require('lodash.assign')
var livereload = require('gulp-livereload')
var del = require('del')
var eslint = require('gulp-eslint')
var fs = require('fs')
var path = require('path')
var manifest = require('./app/manifest.json')
var gulpif = require('gulp-if')
var replace = require('gulp-replace')
var mkdirp = require('mkdirp')
var asyncEach = require('async/each')
var exec = require('child_process').exec
var sass = require('gulp-sass')
var autoprefixer = require('gulp-autoprefixer')
var gulpStylelint = require('gulp-stylelint')
var stylefmt = require('gulp-stylefmt')
var uglify = require('gulp-uglify-es').default
var babel = require('gulp-babel')
var disableDebugTools = gutil.env.disableDebugTools
var debug = gutil.env.debug
const watchify = require('watchify')
const browserify = require('browserify')
const envify = require('envify/custom')
const disc = require('disc')
const gulp = require('gulp')
const source = require('vinyl-source-stream')
const buffer = require('vinyl-buffer')
const gutil = require('gulp-util')
const watch = require('gulp-watch')
const sourcemaps = require('gulp-sourcemaps')
const jsoneditor = require('gulp-json-editor')
const zip = require('gulp-zip')
const assign = require('lodash.assign')
const livereload = require('gulp-livereload')
const del = require('del')
const eslint = require('gulp-eslint')
const fs = require('fs')
const path = require('path')
const manifest = require('./app/manifest.json')
const replace = require('gulp-replace')
const mkdirp = require('mkdirp')
const asyncEach = require('async/each')
const exec = require('child_process').exec
const sass = require('gulp-sass')
const autoprefixer = require('gulp-autoprefixer')
const gulpStylelint = require('gulp-stylelint')
const stylefmt = require('gulp-stylefmt')
const uglify = require('gulp-uglify-es').default
const babel = require('gulp-babel')
const debug = require('gulp-debug')
const pify = require('pify')
const gulpMultiProcess = require('gulp-multi-process')
const endOfStream = pify(require('end-of-stream'))
function gulpParallel (...args) {
return function spawnGulpChildProcess(cb) {
return gulpMultiProcess(args, cb, true)
}
}
const browserPlatforms = [
'firefox',
'chrome',
'edge',
'opera',
]
const commonPlatforms = [
// browser webapp
'mascara',
// browser extensions
...browserPlatforms
]
// browser reload
@ -41,65 +59,98 @@ gulp.task('dev:reload', function() {
})
})
// copy universal
// copy static
const copyTaskNames = []
const copyDevTaskNames = []
gulp.task('copy:locales', copyTask({
createCopyTasks('locales', {
source: './app/_locales/',
destinations: [
'./dist/firefox/_locales',
'./dist/chrome/_locales',
'./dist/edge/_locales',
'./dist/opera/_locales',
]
}))
gulp.task('copy:images', copyTask({
destinations: commonPlatforms.map(platform => `./dist/${platform}/_locales`),
})
createCopyTasks('images', {
source: './app/images/',
destinations: [
'./dist/firefox/images',
'./dist/chrome/images',
'./dist/edge/images',
'./dist/opera/images',
],
}))
gulp.task('copy:contractImages', copyTask({
destinations: commonPlatforms.map(platform => `./dist/${platform}/images`),
})
createCopyTasks('contractImages', {
source: './node_modules/eth-contract-metadata/images/',
destinations: [
'./dist/firefox/images/contract',
'./dist/chrome/images/contract',
'./dist/edge/images/contract',
'./dist/opera/images/contract',
],
}))
gulp.task('copy:fonts', copyTask({
destinations: commonPlatforms.map(platform => `./dist/${platform}/images/contract`),
})
createCopyTasks('fonts', {
source: './app/fonts/',
destinations: [
'./dist/firefox/fonts',
'./dist/chrome/fonts',
'./dist/edge/fonts',
'./dist/opera/fonts',
],
}))
gulp.task('copy:reload', copyTask({
destinations: commonPlatforms.map(platform => `./dist/${platform}/fonts`),
})
createCopyTasks('reload', {
devOnly: true,
source: './app/scripts/',
destinations: [
'./dist/firefox/scripts',
'./dist/chrome/scripts',
'./dist/edge/scripts',
'./dist/opera/scripts',
],
pattern: '/chromereload.js',
}))
gulp.task('copy:root', copyTask({
destinations: commonPlatforms.map(platform => `./dist/${platform}`),
})
createCopyTasks('html', {
source: './app/',
destinations: [
'./dist/firefox',
'./dist/chrome',
'./dist/edge',
'./dist/opera',
],
pattern: '/*',
}))
pattern: '/*.html',
destinations: commonPlatforms.map(platform => `./dist/${platform}`),
})
// copy extension
createCopyTasks('manifest', {
source: './app/',
pattern: '/*.json',
destinations: browserPlatforms.map(platform => `./dist/${platform}`),
})
// copy mascara
createCopyTasks('html:mascara', {
source: './mascara/',
pattern: 'proxy/index.html',
destinations: [`./dist/mascara/`],
})
function createCopyTasks(label, opts) {
if (!opts.devOnly) {
const copyTaskName = `copy:${label}`
copyTask(copyTaskName, opts)
copyTaskNames.push(copyTaskName)
}
const copyDevTaskName = `dev:copy:${label}`
copyTask(copyDevTaskName, Object.assign({ devMode: true }, opts))
copyDevTaskNames.push(copyDevTaskName)
}
function copyTask(taskName, opts){
const source = opts.source
const destination = opts.destination
const destinations = opts.destinations || [destination]
const pattern = opts.pattern || '/**/*'
const devMode = opts.devMode
return gulp.task(taskName, function () {
if (devMode) {
watch(source + pattern, (event) => {
livereload.changed(event.path)
performCopy()
})
}
return performCopy()
})
function performCopy() {
// stream from source
let stream = gulp.src(source + pattern, { base: source })
// copy to destinations
destinations.forEach(function(destination) {
stream = stream.pipe(gulp.dest(destination))
})
return stream
}
}
// manifest tinkering
gulp.task('manifest:chrome', function() {
return gulp.src('./dist/chrome/manifest.json')
@ -134,52 +185,40 @@ gulp.task('manifest:production', function() {
],{base: './dist/'})
// Exclude chromereload script in production:
.pipe(gulpif(!debug,jsoneditor(function(json) {
.pipe(jsoneditor(function(json) {
json.background.scripts = json.background.scripts.filter((script) => {
return !script.includes('chromereload')
})
return json
})))
}))
.pipe(gulp.dest('./dist/', { overwrite: true }))
})
const staticFiles = [
'locales',
'images',
'fonts',
'root'
]
var copyStrings = staticFiles.map(staticFile => `copy:${staticFile}`)
copyStrings.push('copy:contractImages')
if (debug) {
copyStrings.push('copy:reload')
}
gulp.task('copy', gulp.series(gulp.parallel(...copyStrings), 'manifest:production', 'manifest:chrome', 'manifest:opera'))
gulp.task('copy:watch', function(){
gulp.watch(['./app/{_locales,images}/*', './app/scripts/chromereload.js', './app/*.{html,json}'], gulp.series('copy'))
})
// record deps
gulp.task('deps', function (cb) {
exec('npm ls', (err, stdoutOutput, stderrOutput) => {
if (err) return cb(err)
const browsers = ['firefox','chrome','edge','opera']
asyncEach(browsers, (target, done) => {
fs.writeFile(`./dist/${target}/deps.txt`, stdoutOutput, done)
}, cb)
})
})
gulp.task('copy',
gulp.series(
gulp.parallel(...copyTaskNames),
'manifest:production',
'manifest:chrome',
'manifest:opera'
)
)
gulp.task('dev:copy',
gulp.series(
gulp.parallel(...copyDevTaskNames),
'manifest:chrome',
'manifest:opera'
)
)
// lint js
const lintTargets = ['app/**/*.json', 'app/**/*.js', '!app/scripts/vendor/**/*.js', 'ui/**/*.js', 'old-ui/**/*.js', 'mascara/src/*.js', 'mascara/server/*.js', '!node_modules/**', '!dist/firefox/**', '!docs/**', '!app/scripts/chromereload.js', '!mascara/test/jquery-3.1.0.min.js']
gulp.task('lint', function () {
// Ignoring node_modules, dist/firefox, and docs folders:
return gulp.src(['app/**/*.js', '!app/scripts/vendor/**/*.js', 'ui/**/*.js', 'mascara/src/*.js', 'mascara/server/*.js', '!node_modules/**', '!dist/firefox/**', '!docs/**', '!app/scripts/chromereload.js', '!mascara/test/jquery-3.1.0.min.js'])
return gulp.src(lintTargets)
.pipe(eslint(fs.readFileSync(path.join(__dirname, '.eslintrc'))))
// eslint.format() outputs the lint results to the console.
// Alternatively use eslint.formatEach() (see Docs).
@ -190,47 +229,55 @@ gulp.task('lint', function () {
});
gulp.task('lint:fix', function () {
return gulp.src(['app/**/*.js', 'ui/**/*.js', 'mascara/src/*.js', 'mascara/server/*.js', '!node_modules/**', '!dist/firefox/**', '!docs/**', '!app/scripts/chromereload.js', '!mascara/test/jquery-3.1.0.min.js'])
return gulp.src(lintTargets)
.pipe(eslint(Object.assign(fs.readFileSync(path.join(__dirname, '.eslintrc')), {fix: true})))
.pipe(eslint.format())
.pipe(eslint.failAfterError())
});
/*
gulp.task('default', ['lint'], function () {
// This will only run if the lint task is successful...
});
*/
// scss compilation and autoprefixing tasks
// build js
gulp.task('build:scss', createScssBuildTask({
src: 'ui/app/css/index.scss',
dest: 'ui/app/css/output',
devMode: false,
}))
const jsFiles = [
'inpage',
'contentscript',
'background',
'popup',
]
gulp.task('dev:scss', createScssBuildTask({
src: 'ui/app/css/index.scss',
dest: 'ui/app/css/output',
devMode: true,
pattern: 'ui/app/css/**/*.scss',
}))
// scss compilation and autoprefixing tasks
function createScssBuildTask({ src, dest, devMode, pattern }) {
return function () {
if (devMode) {
watch(pattern, async (event) => {
const stream = buildScss()
await endOfStream(stream)
livereload.changed(event.path)
})
}
return buildScss()
}
gulp.task('build:scss', function () {
return gulp.src('ui/app/css/index.scss')
.pipe(sourcemaps.init())
.pipe(sass().on('error', sass.logError))
.pipe(sourcemaps.write())
.pipe(autoprefixer())
.pipe(gulp.dest('ui/app/css/output'))
})
gulp.task('watch:scss', function() {
gulp.watch(['ui/app/css/**/*.scss'], gulp.series(['build:scss']))
})
function buildScss() {
return gulp.src(src)
.pipe(sourcemaps.init())
.pipe(sass().on('error', sass.logError))
.pipe(sourcemaps.write())
.pipe(autoprefixer())
.pipe(gulp.dest(dest))
}
}
gulp.task('lint-scss', function() {
return gulp
.src('ui/app/css/itcss/**/*.scss')
.pipe(gulpStylelint({
reporters: [
{formatter: 'string', console: true}
{ formatter: 'string', console: true }
],
fix: true,
}));
@ -242,46 +289,84 @@ gulp.task('fmt-scss', function () {
.pipe(gulp.dest('ui/app/css/itcss'));
});
// bundle tasks
// build js
var jsDevStrings = jsFiles.map(jsFile => `dev:js:${jsFile}`)
var jsBuildStrings = jsFiles.map(jsFile => `build:js:${jsFile}`)
const buildJsFiles = [
'inpage',
'contentscript',
'background',
'ui',
]
jsFiles.forEach((jsFile) => {
gulp.task(`dev:js:${jsFile}`, bundleTask({
watch: true,
label: jsFile,
filename: `${jsFile}.js`,
isBuild: false
}))
gulp.task(`build:js:${jsFile}`, bundleTask({
watch: false,
label: jsFile,
filename: `${jsFile}.js`,
isBuild: true
}))
})
// bundle tasks
createTasksForBuildJsExtension({ buildJsFiles, taskPrefix: 'dev:extension:js', devMode: true })
createTasksForBuildJsExtension({ buildJsFiles, taskPrefix: 'build:extension:js' })
createTasksForBuildJsMascara({ taskPrefix: 'build:mascara:js' })
createTasksForBuildJsMascara({ taskPrefix: 'dev:mascara:js', devMode: true })
function createTasksForBuildJsExtension({ buildJsFiles, taskPrefix, devMode, bundleTaskOpts = {} }) {
// inpage must be built before all other scripts:
const rootDir = './app/scripts'
const nonInpageFiles = buildJsFiles.filter(file => file !== 'inpage')
const buildPhase1 = ['inpage']
const buildPhase2 = nonInpageFiles
const destinations = browserPlatforms.map(platform => `./dist/${platform}`)
bundleTaskOpts = Object.assign({
buildSourceMaps: true,
sourceMapDir: devMode ? './' : '../sourcemaps',
minifyBuild: !devMode,
buildWithFullPaths: devMode,
watch: devMode,
devMode,
}, bundleTaskOpts)
createTasksForBuildJs({ rootDir, taskPrefix, bundleTaskOpts, destinations, buildPhase1, buildPhase2 })
}
// inpage must be built before all other scripts:
const firstDevString = jsDevStrings.shift()
gulp.task('dev:js', gulp.series(firstDevString, gulp.parallel(...jsDevStrings)))
function createTasksForBuildJsMascara({ taskPrefix, devMode, bundleTaskOpts = {} }) {
// inpage must be built before all other scripts:
const rootDir = './mascara/src/'
const buildPhase1 = ['ui', 'proxy', 'background', 'metamascara']
const destinations = ['./dist/mascara']
bundleTaskOpts = Object.assign({
buildSourceMaps: true,
sourceMapDir: './',
minifyBuild: !devMode,
buildWithFullPaths: devMode,
watch: devMode,
devMode,
}, bundleTaskOpts)
createTasksForBuildJs({ rootDir, taskPrefix, bundleTaskOpts, destinations, buildPhase1 })
}
function createTasksForBuildJs({ rootDir, taskPrefix, bundleTaskOpts, destinations, buildPhase1 = [], buildPhase2 = [] }) {
// bundle task for each file
const jsFiles = [].concat(buildPhase1, buildPhase2)
jsFiles.forEach((jsFile) => {
gulp.task(`${taskPrefix}:${jsFile}`, bundleTask(Object.assign({
label: jsFile,
filename: `${jsFile}.js`,
filepath: `${rootDir}/${jsFile}.js`,
destinations,
}, bundleTaskOpts)))
})
// compose into larger task
const subtasks = []
subtasks.push(gulp.parallel(buildPhase1.map(file => `${taskPrefix}:${file}`)))
if (buildPhase2.length) subtasks.push(gulp.parallel(buildPhase2.map(file => `${taskPrefix}:${file}`)))
// inpage must be built before all other scripts:
const firstBuildString = jsBuildStrings.shift()
gulp.task('build:js', gulp.series(firstBuildString, gulp.parallel(...jsBuildStrings)))
gulp.task(taskPrefix, gulp.series(subtasks))
}
// disc bundle analyzer tasks
jsFiles.forEach((jsFile) => {
gulp.task(`disc:${jsFile}`, discTask({ label: jsFile, filename: `${jsFile}.js` }))
buildJsFiles.forEach((jsFile) => {
gulp.task(`disc:${jsFile}`, discTask({ label: jsFile, filename: `${jsFile}.js` }))
})
gulp.task('disc', gulp.parallel(jsFiles.map(jsFile => `disc:${jsFile}`)))
gulp.task('disc', gulp.parallel(buildJsFiles.map(jsFile => `disc:${jsFile}`)))
// clean dist
gulp.task('clean', function clean() {
return del(['./dist/*'])
})
@ -293,40 +378,88 @@ gulp.task('zip:edge', zipTask('edge'))
gulp.task('zip:opera', zipTask('opera'))
gulp.task('zip', gulp.parallel('zip:chrome', 'zip:firefox', 'zip:edge', 'zip:opera'))
// set env var for production
gulp.task('apply-prod-environment', function(done) {
process.env.NODE_ENV = 'production'
done()
});
// high level tasks
gulp.task('dev', gulp.series('build:scss', 'dev:js', 'copy', gulp.parallel('watch:scss', 'copy:watch', 'dev:reload')))
gulp.task('dev',
gulp.series(
'clean',
'dev:scss',
gulp.parallel(
'dev:extension:js',
'dev:mascara:js',
'dev:copy',
'dev:reload'
)
)
)
gulp.task('dev:extension',
gulp.series(
'clean',
'dev:scss',
gulp.parallel(
'dev:extension:js',
'dev:copy',
'dev:reload'
)
)
)
gulp.task('dev:mascara',
gulp.series(
'clean',
'dev:scss',
gulp.parallel(
'dev:mascara:js',
'dev:copy',
'dev:reload'
)
)
)
gulp.task('build',
gulp.series(
'clean',
'build:scss',
gulpParallel(
'build:extension:js',
'build:mascara:js',
'copy'
)
)
)
gulp.task('build:extension',
gulp.series(
'clean',
'build:scss',
gulp.parallel(
'build:extension:js',
'copy'
)
)
)
gulp.task('build:mascara',
gulp.series(
'clean',
'build:scss',
gulp.parallel(
'build:mascara:js',
'copy'
)
)
)
gulp.task('build', gulp.series('clean', 'build:scss', gulp.parallel('build:js', 'copy')))
gulp.task('dist', gulp.series('apply-prod-environment', 'build', 'zip'))
gulp.task('dist',
gulp.series(
'build',
'zip'
)
)
// task generators
function copyTask(opts){
var source = opts.source
var destination = opts.destination
var destinations = opts.destinations || [ destination ]
var pattern = opts.pattern || '/**/*'
return performCopy
function performCopy(){
let stream = gulp.src(source + pattern, { base: source })
destinations.forEach(function(destination) {
stream = stream.pipe(gulp.dest(destination))
})
stream.pipe(gulpif(debug,livereload()))
return stream
}
}
function zipTask(target) {
return () => {
return gulp.src(`dist/${target}/**`)
@ -337,24 +470,48 @@ function zipTask(target) {
function generateBundler(opts, performBundle) {
const browserifyOpts = assign({}, watchify.args, {
entries: ['./app/scripts/'+opts.filename],
entries: [opts.filepath],
plugin: 'browserify-derequire',
debug: true,
fullPaths: debug,
debug: opts.buildSourceMaps,
fullPaths: opts.buildWithFullPaths,
})
let bundler = browserify(browserifyOpts)
// inject variables into bundle
bundler.transform(envify({
METAMASK_DEBUG: opts.devMode,
NODE_ENV: opts.devMode ? 'development' : 'production',
}))
// Minification
if (opts.minifyBuild) {
bundler.transform('uglifyify', {
global: true,
mangle: {
reserved: [ 'MetamaskInpageProvider' ]
},
})
}
if (opts.watch) {
bundler = watchify(bundler)
// on any file update, re-runs the bundler
bundler.on('update', performBundle)
bundler.on('update', async (ids) => {
const stream = performBundle()
await endOfStream(stream)
livereload.changed(`${ids}`)
})
}
return bundler
}
function discTask(opts) {
opts = Object.assign({
buildWithFullPaths: true,
}, opts)
const bundler = generateBundler(opts, performBundle)
// output build logs to terminal
bundler.on('log', gutil.log)
@ -363,9 +520,9 @@ function discTask(opts) {
function performBundle(){
// start "disc" build
let discDir = path.join(__dirname, 'disc')
const discDir = path.join(__dirname, 'disc')
mkdirp.sync(discDir)
let discPath = path.join(discDir, `${opts.label}.html`)
const discPath = path.join(discDir, `${opts.label}.html`)
return (
bundler.bundle()
@ -384,43 +541,45 @@ function bundleTask(opts) {
return performBundle
function performBundle(){
return (
bundler.bundle()
let buildStream = bundler.bundle()
// handle errors
buildStream.on('error', (err) => {
beep()
if (opts.watch) {
console.warn(err.stack)
} else {
throw err
}
})
// handle errors
.on('error', (err) => {
beep()
if (opts.watch) {
console.warn(err.stack)
} else {
throw err
}
})
// process bundles
buildStream = buildStream
// convert bundle stream to gulp vinyl stream
.pipe(source(opts.filename))
// inject variables into bundle
.pipe(replace('\'GULP_METAMASK_DEBUG\'', debug))
// buffer file contents (?)
.pipe(buffer())
// sourcemaps
// loads map from browserify file
.pipe(sourcemaps.init({ loadMaps: true }))
// Minification
.pipe(gulpif(opts.isBuild, uglify({
mangle: { reserved: [ 'MetamaskInpageProvider' ] },
})))
// writes .map file
.pipe(sourcemaps.write(debug ? './' : '../../sourcemaps'))
// write completed bundles
.pipe(gulp.dest('./dist/firefox/scripts'))
.pipe(gulp.dest('./dist/chrome/scripts'))
.pipe(gulp.dest('./dist/edge/scripts'))
.pipe(gulp.dest('./dist/opera/scripts'))
// finally, trigger live reload
.pipe(gulpif(debug, livereload()))
)
// Initialize Source Maps
if (opts.buildSourceMaps) {
buildStream = buildStream
// loads map from browserify file
.pipe(sourcemaps.init({ loadMaps: true }))
}
// Finalize Source Maps (writes .map file)
if (opts.buildSourceMaps) {
buildStream = buildStream
.pipe(sourcemaps.write(opts.sourceMapDir))
}
// write completed bundles
opts.destinations.forEach((dest) => {
buildStream = buildStream.pipe(gulp.dest(dest))
})
return buildStream
}
}

@ -1,7 +1,5 @@
const path = require('path')
const express = require('express')
const createBundle = require('./util').createBundle
const serveBundle = require('./util').serveBundle
const compression = require('compression')
module.exports = createMetamascaraServer
@ -9,27 +7,14 @@ module.exports = createMetamascaraServer
function createMetamascaraServer () {
// start bundlers
const metamascaraBundle = createBundle(path.join(__dirname, '/../src/mascara.js'))
const proxyBundle = createBundle(path.join(__dirname, '/../src/proxy.js'))
const uiBundle = createBundle(path.join(__dirname, '/../src/ui.js'))
const backgroundBuild = createBundle(path.join(__dirname, '/../src/background.js'))
// serve bundles
// setup server
const server = express()
server.use(compression())
// ui window
serveBundle(server, '/ui.js', uiBundle)
// serve assets
server.use(express.static(path.join(__dirname, '/../ui/'), { setHeaders: (res) => res.set('X-Frame-Options', 'DENY') }))
server.use(express.static(path.join(__dirname, '/../../dist/chrome')))
// metamascara
serveBundle(server, '/metamascara.js', metamascaraBundle)
// proxy
serveBundle(server, '/proxy/proxy.js', proxyBundle)
server.use('/proxy/', express.static(path.join(__dirname, '/../proxy')))
// background
serveBundle(server, '/background.js', backgroundBuild)
server.use(express.static(path.join(__dirname, '/../../dist/mascara')))
server.use(express.static(path.join(__dirname, '/../proxy')))
return server

@ -20,7 +20,6 @@ class FirstTimeFlow extends Component {
seedWords: PropTypes.string,
address: PropTypes.string,
noActiveNotices: PropTypes.bool,
goToBuyEtherView: PropTypes.func.isRequired,
};
static defaultProps = {
@ -171,4 +170,3 @@ export default connect(
openBuyEtherModal: () => dispatch(showModal({ name: 'DEPOSIT_ETHER'})),
})
)(FirstTimeFlow)

@ -30,15 +30,19 @@ global.addEventListener('activate', function (event) {
log.debug('inside:open')
// // state persistence
// state persistence
const dbController = new DbController({
key: STORAGE_KEY,
})
loadStateFromPersistence()
.then((initState) => setupController(initState))
.then(() => log.debug('MetaMask initialization complete.'))
.catch((err) => console.error('WHILE SETTING UP:', err))
start().catch(log.error)
async function start() {
log.debug('MetaMask initializing...')
const initState = await loadStateFromPersistence()
await setupController(initState)
log.debug('MetaMask initialization complete.')
}
//
// State and Persistence

@ -1,13 +1,13 @@
const createParentStream = require('iframe-stream').ParentStream
const SWcontroller = require('client-sw-ready-event/lib/sw-client.js')
const SwController = require('sw-controller')
const SwStream = require('sw-stream/lib/sw-stream.js')
const intervalDelay = Math.floor(Math.random() * (30000 - 1000)) + 1000
const background = new SWcontroller({
fileName: '/background.js',
letBeIdle: false,
wakeUpInterval: 30000,
intervalDelay,
const keepAliveDelay = Math.floor(Math.random() * (30000 - 1000)) + 1000
const background = new SwController({
fileName: './scripts/background.js',
keepAlive: true,
keepAliveInterval: 30000,
keepAliveDelay,
})
const pageStream = createParentStream()

@ -1,6 +1,6 @@
const injectCss = require('inject-css')
const SWcontroller = require('client-sw-ready-event/lib/sw-client.js')
const SwStream = require('sw-stream/lib/sw-stream.js')
const SwController = require('sw-controller')
const SwStream = require('sw-stream')
const MetaMaskUiCss = require('../../ui/css')
const MetamascaraPlatform = require('../../app/scripts/platforms/window')
const startPopup = require('../../app/scripts/popup-core')
@ -8,27 +8,44 @@ const startPopup = require('../../app/scripts/popup-core')
// create platform global
global.platform = new MetamascaraPlatform()
var css = MetaMaskUiCss()
injectCss(css)
const container = document.getElementById('app-content')
var name = 'popup'
const name = 'popup'
window.METAMASK_UI_TYPE = name
window.METAMASK_PLATFORM_TYPE = 'mascara'
const intervalDelay = Math.floor(Math.random() * (30000 - 1000)) + 1000
const keepAliveDelay = Math.floor(Math.random() * (30000 - 1000)) + 1000
const background = new SWcontroller({
fileName: '/background.js',
letBeIdle: false,
intervalDelay,
wakeUpInterval: 20000,
const swController = new SwController({
fileName: './background.js',
keepAlive: true,
keepAliveDelay,
keepAliveInterval: 20000,
})
swController.once('updatefound', windowReload)
swController.once('ready', async () => {
try {
swController.removeListener('updatefound', windowReload)
console.log('swController ready')
await timeout(1000)
console.log('connecting to app')
await connectApp()
console.log('app connected')
} catch (err) {
console.error(err)
}
})
console.log('starting service worker')
swController.startWorker()
// Setup listener for when the service worker is read
const connectApp = function (readSw) {
function connectApp() {
const connectionStream = SwStream({
serviceWorker: background.controller,
serviceWorker: swController.getWorker(),
context: name,
})
return new Promise((resolve, reject) => {
@ -43,19 +60,6 @@ const connectApp = function (readSw) {
})
})
}
background.on('ready', async (sw) => {
try {
background.removeListener('updatefound', connectApp)
await timeout(1000)
await connectApp(sw)
console.log('hello from cb ready event!')
} catch (e) {
console.error(e)
}
})
background.on('updatefound', windowReload)
background.startWorker()
function windowReload () {
if (window.METAMASK_SKIP_RELOAD) return

@ -7,6 +7,6 @@
</head>
<body>
<div id="app-content"></div>
<script src="./ui.js" type="text/javascript" charset="utf-8"></script>
<script src="./scripts/ui.js" type="text/javascript" charset="utf-8"></script>
</body>
</html>

@ -171,7 +171,7 @@ App.prototype.renderAppBar = function () {
h('img', {
height: 24,
width: 24,
src: '/images/icon-128.png',
src: './images/icon-128.png',
}),
h(NetworkIndicator, {
@ -581,7 +581,6 @@ App.prototype.renderPrimary = function () {
case 'qr':
log.debug('rendering show qr screen')
console.log(`QrView`, QrView);
return h('div', {
style: {
position: 'absolute',

@ -247,7 +247,6 @@ BuyButtonSubview.prototype.backButtonContext = function () {
if (this.props.context === 'confTx') {
this.props.dispatch(actions.showConfTxPage(false))
} else {
console.log(`actions.goHome`, actions.goHome);
this.props.dispatch(actions.goHome())
}
}

@ -62,8 +62,8 @@ PendingTx.prototype.render = function () {
const gasBn = hexToBn(gas)
// default to 8MM gas limit
const gasLimit = new BN(parseInt(blockGasLimit) || '8000000')
const safeGasLimitBN = this.bnMultiplyByFraction(gasLimit, 19, 20)
const saferGasLimitBN = this.bnMultiplyByFraction(gasLimit, 18, 20)
const safeGasLimitBN = this.bnMultiplyByFraction(gasLimit, 99, 100)
const saferGasLimitBN = this.bnMultiplyByFraction(gasLimit, 98, 100)
const safeGasLimit = safeGasLimitBN.toString(10)
// Gas Price
@ -311,7 +311,7 @@ PendingTx.prototype.render = function () {
style: {
fontSize: '0.9em',
},
}, 'Gas limit set dangerously high. Approving this transaction is likely to fail.')
}, 'Gas limit set dangerously high. Approving this transaction is liable to fail.')
: null,
]),

@ -25,7 +25,6 @@ function QrCodeView () {
QrCodeView.prototype.render = function () {
const props = this.props
const Qr = props.Qr
console.log(`QrCodeView Qr`, Qr);
const address = `${isHexPrefixed(Qr.data) ? 'ethereum:' : ''}${Qr.data}`
const qrImage = qrCode(4, 'M')
qrImage.addData(address)

@ -35,7 +35,7 @@ RangeSlider.prototype.render = function () {
step: increment,
style: range,
value: state.value || defaultValue,
onChange: mirrorInput ? this.mirrorInputs.bind(this, event) : onInput,
onChange: mirrorInput ? this.mirrorInputs.bind(this) : onInput,
}),
// Mirrored input for range
@ -47,7 +47,7 @@ RangeSlider.prototype.render = function () {
value: state.value || defaultValue,
step: increment,
style: input,
onChange: this.mirrorInputs.bind(this, event),
onChange: this.mirrorInputs.bind(this),
}) : null,
])
)

@ -30,7 +30,12 @@ function TransactionListItem () {
TransactionListItem.prototype.showRetryButton = function () {
const { transaction = {}, transactions } = this.props
const { status, submittedTime, txParams } = transaction
const { submittedTime, txParams } = transaction
if (!txParams) {
return false
}
const currentNonce = txParams.nonce
const currentNonceTxs = transactions.filter(tx => tx.txParams.nonce === currentNonce)
const currentNonceSubmittedTxs = currentNonceTxs.filter(tx => tx.status === 'submitted')

@ -42,7 +42,7 @@ ConfigScreen.prototype.render = function () {
// subtitle and nav
h('.section-title.flex-row.flex-center', [
h('i.fa.fa-arrow-left.fa-lg.cursor-pointer', {
onClick: (event) => {
onClick: () => {
state.dispatch(actions.goHome())
},
}),
@ -168,7 +168,6 @@ ConfigScreen.prototype.render = function () {
h('a', {
href: 'http://metamask.helpscoutdocs.com/article/36-resetting-an-account',
target: '_blank',
onClick (event) { this.navigateTo(event.target.href) },
}, 'Read more.'),
]),
h('br'),
@ -260,7 +259,3 @@ function currentProviderDisplay (metamaskState) {
h('span', value),
])
}
ConfigScreen.prototype.navigateTo = function (url) {
global.platform.openWindow({ url })
}

@ -231,6 +231,7 @@ function exportAsFile (filename, data) {
window.navigator.msSaveBlob(blob, filename)
} else {
const elem = window.document.createElement('a')
elem.target = '_blank'
elem.href = window.URL.createObjectURL(blob)
elem.download = filename
document.body.appendChild(elem)

@ -11,7 +11,7 @@ var cssFiles = {
'transitions.css': fs.readFileSync(path.join(__dirname, '/app/css/transitions.css'), 'utf8'),
'first-time.css': fs.readFileSync(path.join(__dirname, '../mascara/src/app/first-time/index.css'), 'utf8'),
'react-tooltip-component.css': fs.readFileSync(path.join(__dirname, '..', 'node_modules', 'react-tooltip-component', 'dist', 'react-tooltip-component.css'), 'utf8'),
'react-css': fs.readFileSync(path.join(__dirname, '..', 'node_modules', 'react-select', 'dist', 'react-select.css'), 'utf8'),
'react-css': fs.readFileSync(path.join(__dirname, '..', 'node_modules', 'react-select', 'dist', 'react-select.css'), 'utf8')
}
function bundleCss () {
@ -19,7 +19,7 @@ function bundleCss () {
var fileContent = cssFiles[fileName]
var output = String()
output += '/*========== ' + fileName + ' ==========*/\n\n'
output += '/ *========== ' + fileName + ' ========== * /\n\n'
output += fileContent
output += '\n\n'

7874
package-lock.json generated

File diff suppressed because it is too large Load Diff

@ -4,40 +4,39 @@
"public": false,
"private": true,
"scripts": {
"start": "npm run dev",
"dev": "gulp dev --debug",
"ui": "npm run test:flat:build:states && beefy development/ui-dev.js:bundle.js --live --open --index=./development/index.html --cwd ./",
"mock": "beefy development/mock-dev.js:bundle.js --live --open --index=./development/index.html --cwd ./",
"watch": "mocha watch --recursive \"test/unit/**/*.js\"",
"mascara": "gulp build && cross-env METAMASK_DEBUG=true node ./mascara/example/server",
"dist": "npm run dist:clear && npm install && gulp dist",
"dist:clear": "rm -rf node_modules/eth-contract-metadata && rm -rf node_modules/eth-phishing-detect",
"start": "gulp dev:extension",
"mascara": "gulp dev:mascara & node ./mascara/example/server",
"dist": "gulp dist",
"test": "npm run test:unit && npm run test:integration && npm run lint",
"test:unit": "cross-env METAMASK_ENV=test mocha --exit --require babel-core/register --require test/helper.js --recursive \"test/unit/**/*.js\"",
"test:single": "cross-env METAMASK_ENV=test mocha --require test/helper.js",
"test:integration": "npm run test:integration:build && npm run test:flat && npm run test:mascara",
"test:integration:build": "gulp build:scss",
"test:e2e": "shell-parallel -s 'npm run ganache:start' -x 'sleep 3 && npm run test:e2e:run'",
"test:e2e:run": "mocha test/e2e/metamask.spec --recursive",
"test:screens": "shell-parallel -s 'npm run ganache:start' -x 'sleep 3 && npm run test:screens:run'",
"test:screens:run": "node test/screens/new-ui.js",
"test:coverage": "nyc npm run test:unit && npm run test:coveralls-upload",
"test:coveralls-upload": "if [ $COVERALLS_REPO_TOKEN ]; then nyc report --reporter=text-lcov | coveralls; fi",
"test:flat": "npm run test:flat:build && karma start test/flat.conf.js",
"test:flat:build": "npm run test:flat:build:ui && npm run test:flat:build:tests",
"test:flat:build": "npm run test:flat:build:ui && npm run test:flat:build:tests && npm run test:flat:build:locales",
"test:flat:build:tests": "node test/integration/index.js",
"test:flat:build:states": "node development/genStates.js",
"test:flat:build:locales": "mkdirp dist/chrome && cp -R app/_locales dist/chrome/_locales",
"test:flat:build:ui": "npm run test:flat:build:states && browserify ./development/mock-dev.js -o ./development/bundle.js",
"test:mascara": "npm run test:mascara:build && karma start test/mascara.conf.js",
"test:mascara:build": "mkdirp dist/mascara && npm run test:mascara:build:ui && npm run test:mascara:build:background && npm run test:mascara:build:tests",
"test:mascara:build": "mkdirp dist/mascara && npm run test:mascara:build:ui && npm run test:mascara:build:background && npm run test:mascara:build:tests && npm run test:mascara:build:locales",
"test:mascara:build:ui": "browserify mascara/test/test-ui.js -o dist/mascara/ui.js",
"test:mascara:build:locales": "mkdirp dist/chrome && cp -R app/_locales dist/chrome/_locales",
"test:mascara:build:background": "browserify mascara/src/background.js -o dist/mascara/background.js",
"test:mascara:build:tests": "browserify test/integration/lib/first-time.js -o dist/mascara/tests.js",
"sentry": "export RELEASE=`cat app/manifest.json| jq -r .version` && npm run sentry:release && npm run sentry:upload",
"sentry:release": "npm run sentry:release:new && npm run sentry:release:clean",
"sentry:release:new": "sentry-cli releases --org 'metamask' --project 'metamask' new $RELEASE",
"sentry:release:clean": "sentry-cli releases --org 'metamask' --project 'metamask' files $RELEASE delete --all",
"sentry:upload": "npm run sentry:upload:source && npm run sentry:upload:maps",
"sentry:upload:source": "for FILEPATH in ./dist/chrome/scripts/*.js; do [ -e $FILEPATH ] || continue; export FILE=`basename $FILEPATH` && echo uploading $FILE && sentry-cli releases --org 'metamask' --project 'metamask' files $RELEASE upload $FILEPATH metamask/scripts/$FILE; done;",
"sentry:upload:maps": "sentry-cli releases --org 'metamask' --project 'metamask' files $RELEASE upload-sourcemaps ./dist/sourcemaps/ --url-prefix 'sourcemaps' --rewrite",
"ganache:start": "ganache-cli -m 'phrase upgrade clock rough situate wedding elder clever doctor stamp excess tent'",
"sentry:publish": "node ./development/sentry-publish.js",
"lint": "gulp lint",
"lint:fix": "gulp lint:fix",
"ui": "npm run test:flat:build:states && beefy development/ui-dev.js:bundle.js --live --open --index=./development/index.html --cwd ./",
"mock": "beefy development/mock-dev.js:bundle.js --live --open --index=./development/index.html --cwd ./",
"watch": "mocha watch --recursive \"test/unit/**/*.js\"",
"disc": "gulp disc --debug",
"announce": "node development/announcer.js",
"version:bump": "node development/run-version-bump.js",
@ -56,18 +55,17 @@
}
],
"reactify",
"envify",
"brfs"
]
},
"dependencies": {
"abi-decoder": "^1.0.9",
"abi-decoder": "^1.1.0",
"asmcrypto.js": "0.22.0",
"async": "^2.5.0",
"await-semaphore": "^0.1.1",
"babel-runtime": "^6.23.0",
"bignumber.js": "^4.1.0",
"bip39": "^2.2.0",
"bip39": "^2.5.0",
"bluebird": "^3.5.0",
"bn.js": "^4.11.7",
"boron": "^0.2.3",
@ -75,8 +73,7 @@
"browserify-derequire": "^0.9.4",
"browserify-unibabel": "^3.0.0",
"classnames": "^2.2.5",
"client-sw-ready-event": "^3.3.0",
"clone": "^2.1.1",
"clone": "^2.1.2",
"copy-to-clipboard": "^3.0.8",
"debounce": "^1.0.0",
"debounce-stream": "^2.0.0",
@ -84,46 +81,44 @@
"detect-node": "^2.0.3",
"disc": "^1.3.2",
"dnode": "^1.2.2",
"end-of-stream": "^1.1.0",
"end-of-stream": "^1.4.1",
"ensnare": "^1.0.0",
"eslint-plugin-react": "^7.4.0",
"eth-bin-to-ops": "^1.0.1",
"eth-block-tracker": "^2.3.0",
"eth-contract-metadata": "^1.1.5",
"eth-contract-metadata": "^1.7.0",
"eth-hd-keyring": "^1.2.1",
"eth-json-rpc-filters": "^1.2.5",
"eth-json-rpc-infura": "^3.0.0",
"eth-keyring-controller": "^2.1.4",
"eth-phishing-detect": "^1.1.4",
"eth-phishing-detect": "^1.1.13",
"eth-query": "^2.1.2",
"eth-sig-util": "^1.4.2",
"eth-token-tracker": "^1.1.4",
"ethereumjs-abi": "^0.6.4",
"ethereumjs-tx": "^1.3.0",
"ethereumjs-tx": "^1.3.4",
"ethereumjs-util": "github:ethereumjs/ethereumjs-util#ac5d0908536b447083ea422b435da27f26615de9",
"ethereumjs-wallet": "^0.6.0",
"etherscan-link": "^1.0.2",
"ethjs": "^0.2.8",
"ethjs-contract": "^0.1.9",
"ethjs-ens": "^2.0.0",
"ethjs-query": "^0.3.1",
"express": "^4.15.5",
"ethjs-query": "^0.3.4",
"express": "^4.16.3",
"extension-link-enabler": "^1.0.0",
"extensionizer": "^1.0.0",
"extensionizer": "^1.0.1",
"fast-json-patch": "^2.0.4",
"fast-levenshtein": "^2.0.6",
"fuse.js": "^3.2.0",
"gulp": "github:gulpjs/gulp#4.0",
"gulp-autoprefixer": "^5.0.0",
"gulp-eslint": "^4.0.0",
"gulp-sass": "^3.1.0",
"gulp-debug": "^3.2.0",
"gulp-sass": "^3.2.1",
"hat": "0.0.3",
"human-standard-token-abi": "^1.0.2",
"idb-global": "^2.1.0",
"identicon.js": "^2.3.1",
"identicon.js": "^2.3.2",
"iframe": "^1.0.0",
"iframe-stream": "^3.0.0",
"inject-css": "^0.1.1",
"inject-css": "^0.1.2",
"jazzicon": "^1.2.0",
"json-rpc-engine": "^3.6.1",
"json-rpc-middleware-stream": "^1.0.1",
@ -131,16 +126,16 @@
"lodash.memoize": "^4.1.2",
"lodash.shuffle": "^4.2.0",
"lodash.uniqby": "^4.7.0",
"loglevel": "^1.4.1",
"loglevel": "^1.6.1",
"metamascara": "^2.0.0",
"metamask-logo": "^2.1.2",
"metamask-logo": "^2.1.4",
"mkdirp": "^0.5.1",
"multiplex": "^6.7.0",
"number-to-bn": "^1.7.0",
"obj-multiplex": "^1.0.0",
"obs-store": "^3.0.0",
"once": "^1.3.3",
"percentile": "^1.2.0",
"pify": "^3.0.0",
"ping-pong-stream": "^1.0.0",
"pojo-migrator": "^2.1.0",
"polyfill-crypto.getrandomvalues": "^1.0.0",
@ -148,25 +143,25 @@
"promise-filter": "^1.1.0",
"promise-to-callback": "^1.0.0",
"pump": "^3.0.0",
"pumpify": "^1.3.4",
"pumpify": "^1.4.0",
"qrcode-npm": "0.0.3",
"ramda": "^0.24.1",
"raven-js": "^3.24.0",
"raven-js": "^3.24.1",
"react": "^15.6.2",
"react-addons-css-transition-group": "^15.6.0",
"react-dom": "^15.6.2",
"react-hyperscript": "^3.0.0",
"react-markdown": "^3.0.0",
"react-redux": "^5.0.5",
"react-select": "^1.0.0",
"react-simple-file-input": "^2.0.0",
"react-hyperscript": "^3.2.0",
"react-markdown": "^3.3.0",
"react-redux": "^5.0.7",
"react-select": "^1.2.1",
"react-simple-file-input": "^2.1.0",
"react-tippy": "^1.2.2",
"react-toggle-button": "^2.2.0",
"react-tooltip-component": "^0.3.0",
"react-transition-group": "^2.2.1",
"react-transition-group": "^2.3.0",
"react-trigger-change": "^1.0.2",
"reactify": "^1.1.1",
"readable-stream": "^2.3.3",
"readable-stream": "^2.3.6",
"recompose": "^0.25.0",
"redux": "^3.0.5",
"redux-logger": "^3.0.6",
@ -174,22 +169,23 @@
"request-promise": "^4.2.1",
"sandwich-expando": "^1.1.3",
"semaphore": "^1.0.5",
"semver": "^5.4.1",
"semver": "^5.5.0",
"shallow-copy": "0.0.1",
"sw-stream": "^2.0.0",
"textarea-caret": "^3.0.1",
"sw-controller": "^1.0.3",
"sw-stream": "^2.0.2",
"textarea-caret": "^3.1.0",
"through2": "^2.0.3",
"valid-url": "^1.0.9",
"vreme": "^3.0.2",
"web3": "^0.20.1",
"web3-provider-engine": "^13.5.6",
"web3": "^0.20.6",
"web3-provider-engine": "^13.8.0",
"web3-stream-provider": "^3.0.1",
"xtend": "^4.0.1"
},
"devDependencies": {
"@sentry/cli": "^1.30.3",
"babel-core": "^6.24.1",
"babel-eslint": "^8.0.0",
"babel-eslint": "^8.2.2",
"babel-plugin-transform-async-to-generator": "^6.24.1",
"babel-plugin-transform-runtime": "^6.23.0",
"babel-polyfill": "^6.23.0",
@ -198,10 +194,11 @@
"babel-register": "^6.7.2",
"babelify": "^8.0.0",
"beefy": "^2.1.5",
"brfs": "^1.4.3",
"brfs": "^1.5.0",
"browserify": "^16.1.1",
"chai": "^4.1.0",
"compression": "^1.7.1",
"chromedriver": "^2.37.0",
"compression": "^1.7.2",
"coveralls": "^3.0.0",
"cross-env": "^5.1.4",
"deep-freeze-strict": "^1.1.1",
@ -210,18 +207,21 @@
"enzyme": "^3.3.0",
"enzyme-adapter-react-15": "^1.0.5",
"eslint-plugin-chai": "0.0.1",
"eslint-plugin-mocha": "^4.9.0",
"eslint-plugin-react": "^7.4.0",
"eth-json-rpc-middleware": "^1.2.7",
"eslint-plugin-json": "^1.2.0",
"eslint-plugin-mocha": "^5.0.0",
"eslint-plugin-react": "^7.7.0",
"eth-json-rpc-middleware": "^1.6.0",
"fs-promise": "^2.0.3",
"gulp": "github:gulpjs/gulp#6d71a658c61edb3090221579d8f97dbe086ba2ed",
"ganache-cli": "^6.1.0",
"gifencoder": "^1.1.0",
"gulp": "github:gulpjs/gulp#4.0",
"gulp-babel": "^7.0.0",
"gulp-eslint": "^4.0.0",
"gulp-if": "^2.0.2",
"gulp-json-editor": "^2.2.1",
"gulp-eslint": "^4.0.2",
"gulp-json-editor": "^2.3.0",
"gulp-livereload": "^3.8.1",
"gulp-multi-process": "^1.3.1",
"gulp-replace": "^0.6.1",
"gulp-sourcemaps": "^2.6.0",
"gulp-sourcemaps": "^2.6.4",
"gulp-stylefmt": "^1.1.0",
"gulp-stylelint": "^7.0.0",
"gulp-uglify": "^3.0.0",
@ -229,8 +229,9 @@
"gulp-util": "^3.0.7",
"gulp-watch": "^5.0.0",
"gulp-zip": "^4.0.0",
"image-size": "^0.6.2",
"isomorphic-fetch": "^2.2.1",
"jsdom": "^11.1.0",
"jsdom": "^11.7.0",
"jsdom-global": "^3.0.2",
"jshint-stylish": "~2.2.1",
"karma": "^2.0.0",
@ -239,29 +240,33 @@
"karma-firefox-launcher": "^1.0.1",
"karma-qunit": "^1.2.1",
"lodash.assign": "^4.0.6",
"mocha": "^5.0.0",
"mocha": "^5.0.5",
"mocha-eslint": "^4.0.0",
"mocha-jsdom": "^1.1.0",
"mocha-sinon": "^2.0.0",
"nock": "^9.0.14",
"node-sass": "^4.7.2",
"nyc": "^11.0.3",
"nock": "^9.2.4",
"node-sass": "^4.8.3",
"nyc": "^11.6.0",
"open": "0.0.5",
"png-file-stream": "^1.0.0",
"prompt": "^1.0.0",
"qs": "^6.2.0",
"qunitjs": "^2.4.1",
"react-addons-test-utils": "^15.5.1",
"react-test-renderer": "^15.6.2",
"react-testutils-additions": "^15.2.0",
"react-testutils-additions": "^15.3.1",
"redux-test-utils": "^0.2.2",
"sinon": "^4.0.0",
"rimraf": "^2.6.2",
"selenium-webdriver": "^3.5.0",
"shell-parallel": "^1.0.3",
"sinon": "^5.0.0",
"stylelint-config-standard": "^18.2.0",
"tape": "^4.5.1",
"testem": "^2.0.0",
"uglifyify": "^4.0.2",
"tape": "^4.9.0",
"testem": "^2.2.1",
"uglifyify": "^4.0.5",
"vinyl-buffer": "^1.0.1",
"vinyl-source-stream": "^2.0.0",
"watchify": "^3.9.0"
"watchify": "^3.11.0"
},
"engines": {
"node": ">=0.8.0"

@ -19,11 +19,13 @@ module.exports = function(config) {
'test/integration/jquery-3.1.0.min.js',
{ pattern: 'dist/chrome/images/**/*.*', watched: false, included: false, served: true },
{ pattern: 'dist/chrome/fonts/**/*.*', watched: false, included: false, served: true },
{ pattern: 'dist/chrome/_locales/**/*.*', watched: false, included: false, served: true },
],
proxies: {
'/images/': '/base/dist/chrome/images/',
'/fonts/': '/base/dist/chrome/fonts/',
'/_locales/': '/base/dist/chrome/_locales/',
},
// test results reporter to use

@ -0,0 +1,18 @@
require('chromedriver')
const webdriver = require('selenium-webdriver')
exports.delay = function delay (time) {
return new Promise(resolve => setTimeout(resolve, time))
}
exports.buildWebDriver = function buildWebDriver (extPath) {
return new webdriver.Builder()
.withCapabilities({
chromeOptions: {
args: [`load-extension=${extPath}`],
},
})
.forBrowser('chrome')
.build()
}

@ -0,0 +1,145 @@
const fs = require('fs')
const mkdirp = require('mkdirp')
const path = require('path')
const assert = require('assert')
const pify = require('pify')
const webdriver = require('selenium-webdriver')
const By = webdriver.By
const { delay, buildWebDriver } = require('./func')
describe('Metamask popup page', function () {
let driver
this.seedPhase
this.accountAddress
this.timeout(0)
before(async function () {
const extPath = path.resolve('dist/chrome')
driver = buildWebDriver(extPath)
await driver.get('chrome://extensions-frame')
const elems = await driver.findElements(By.css('.extension-list-item-wrapper'))
const extensionId = await elems[1].getAttribute('id')
await driver.get(`chrome-extension://${extensionId}/popup.html`)
await delay(500)
})
afterEach(async function () {
if (this.currentTest.state === 'failed') {
await verboseReportOnFailure(this.currentTest)
}
})
after(async function () {
await driver.quit()
})
describe('#onboarding', () => {
it('should open Metamask.io', async function () {
const tabs = await driver.getAllWindowHandles()
await driver.switchTo().window(tabs[0])
await delay(300)
await setProviderType('localhost')
await delay(300)
})
it('should match title', async () => {
const title = await driver.getTitle()
assert.equal(title, 'MetaMask', 'title matches MetaMask')
})
it('should show privacy notice', async () => {
const privacy = await driver.findElement(By.css('.terms-header')).getText()
assert.equal(privacy, 'PRIVACY NOTICE', 'shows privacy notice')
driver.findElement(By.css('button')).click()
await delay(300)
})
it('should show terms of use', async () => {
await delay(300)
const terms = await driver.findElement(By.css('.terms-header')).getText()
assert.equal(terms, 'TERMS OF USE', 'shows terms of use')
await delay(300)
})
it('should be unable to continue without scolling throught the terms of use', async () => {
const button = await driver.findElement(By.css('button')).isEnabled()
assert.equal(button, false, 'disabled continue button')
const element = driver.findElement(By.linkText(
'Attributions'
))
await driver.executeScript('arguments[0].scrollIntoView(true)', element)
await delay(300)
})
it('should be able to continue when scrolled to the bottom of terms of use', async () => {
const button = await driver.findElement(By.css('button'))
const buttonEnabled = await button.isEnabled()
await delay(500)
assert.equal(buttonEnabled, true, 'enabled continue button')
await button.click()
await delay(300)
})
it('should accept password with length of eight', async () => {
const passwordBox = await driver.findElement(By.id('password-box'))
const passwordBoxConfirm = await driver.findElement(By.id('password-box-confirm'))
const button = driver.findElement(By.css('button'))
passwordBox.sendKeys('123456789')
passwordBoxConfirm.sendKeys('123456789')
await delay(500)
await button.click()
})
it('should show value was created and seed phrase', async () => {
await delay(700)
this.seedPhase = await driver.findElement(By.css('.twelve-word-phrase')).getText()
const continueAfterSeedPhrase = await driver.findElement(By.css('button'))
await continueAfterSeedPhrase.click()
await delay(300)
})
it('should show lock account', async () => {
await driver.findElement(By.css('.sandwich-expando')).click()
await delay(500)
await driver.findElement(By.css('#app-content > div > div:nth-child(3) > span > div > li:nth-child(3)')).click()
})
it('should accept account password after lock', async () => {
await delay(500)
await driver.findElement(By.id('password-box')).sendKeys('123456789')
await driver.findElement(By.css('button')).click()
await delay(500)
})
it('should show QR code option', async () => {
await delay(300)
await driver.findElement(By.css('.fa-ellipsis-h')).click()
await driver.findElement(By.css('#app-content > div > div.app-primary.from-right > div > div > div:nth-child(1) > flex-column > div.name-label > div > span > i > div > div > li:nth-child(3)')).click()
await delay(300)
})
it('should show the account address', async () => {
this.accountAddress = await driver.findElement(By.css('.ellip-address')).getText()
await driver.findElement(By.css('.fa-arrow-left')).click()
await delay(500)
})
})
async function setProviderType(type) {
await driver.executeScript('window.metamask.setProviderType(arguments[0])', type)
}
async function verboseReportOnFailure(test) {
const artifactDir = `./test-artifacts/${test.title}`
const filepathBase = `${artifactDir}/test-failure`
await pify(mkdirp)(artifactDir)
// capture screenshot
const screenshot = await driver.takeScreenshot()
await pify(fs.writeFile)(`${filepathBase}-screenshot.png`, screenshot, { encoding: 'base64' })
// capture dom source
const htmlSource = await driver.getPageSource()
await pify(fs.writeFile)(`${filepathBase}-dom.html`, htmlSource)
}
})

@ -75,7 +75,7 @@ async function runAddTokenFlowTest (assert, done) {
// Confirm Add token
assert.equal(
$('.add-token__description')[0].textContent,
'Would you like to add these tokens?',
'Token balance(s)',
'confirm add token rendered'
)
assert.ok($('button.btn-primary--lg')[0], 'confirm add token button found')

@ -0,0 +1,61 @@
const reactTriggerChange = require('../../lib/react-trigger-change')
const {
timeout,
queryAsync,
findAsync,
} = require('../../lib/util')
QUnit.module('tx list items')
QUnit.test('renders list items successfully', (assert) => {
const done = assert.async()
runTxListItemsTest(assert).then(done).catch((err) => {
assert.notOk(err, `Error was thrown: ${err.stack}`)
done()
})
})
async function runTxListItemsTest(assert, done) {
console.log('*** start runTxListItemsTest')
const selectState = await queryAsync($, 'select')
selectState.val('tx list items')
reactTriggerChange(selectState[0])
const metamaskLogo = await queryAsync($, '.left-menu-wrapper')
assert.ok(metamaskLogo[0], 'metamask logo present')
metamaskLogo[0].click()
const txListItems = await queryAsync($, '.tx-list-item')
assert.equal(txListItems.length, 8, 'all tx list items are rendered')
const unapprovedTx = txListItems[0]
assert.equal($(unapprovedTx).hasClass('tx-list-pending-item-container'), true, 'unapprovedTx has the correct class')
const retryTx = txListItems[1]
const retryTxLink = await findAsync($(retryTx), '.tx-list-item-retry-link')
assert.equal(retryTxLink[0].textContent, 'Increase the gas price on your transaction', 'retryTx has expected link')
const approvedTx = txListItems[2]
const approvedTxRenderedStatus = await findAsync($(approvedTx), '.tx-list-status')
assert.equal(approvedTxRenderedStatus[0].textContent, 'Approved', 'approvedTx has correct label')
const unapprovedMsg = txListItems[3]
const unapprovedMsgDescription = await findAsync($(unapprovedMsg), '.tx-list-account')
assert.equal(unapprovedMsgDescription[0].textContent, 'Signature Request', 'unapprovedMsg has correct description')
const failedTx = txListItems[4]
const failedTxRenderedStatus = await findAsync($(failedTx), '.tx-list-status')
assert.equal(failedTxRenderedStatus[0].textContent, 'Failed', 'failedTx has correct label')
const shapeShiftTx = txListItems[5]
const shapeShiftTxStatus = await findAsync($(shapeShiftTx), '.flex-column div:eq(1)')
assert.equal(shapeShiftTxStatus[0].textContent, 'No deposits received', 'shapeShiftTx has correct status')
const confirmedTokenTx = txListItems[6]
const confirmedTokenTxAddress = await findAsync($(confirmedTokenTx), '.tx-list-account')
assert.equal(confirmedTokenTxAddress[0].textContent, '0xe7884118...81a9', 'confirmedTokenTx has correct address')
const rejectedTx = txListItems[7]
const rejectedTxRenderedStatus = await findAsync($(rejectedTx), '.tx-list-status')
assert.equal(rejectedTxRenderedStatus[0].textContent, 'Rejected', 'rejectedTx has correct label')
}

@ -0,0 +1,18 @@
require('chromedriver')
const webdriver = require('selenium-webdriver')
exports.delay = function delay (time) {
return new Promise(resolve => setTimeout(resolve, time))
}
exports.buildWebDriver = function buildWebDriver (extPath) {
return new webdriver.Builder()
.withCapabilities({
chromeOptions: {
args: [`load-extension=${extPath}`],
},
})
.forBrowser('chrome')
.build()
}

@ -0,0 +1,230 @@
const path = require('path')
const fs = require('fs')
const pify = require('pify')
const mkdirp = require('mkdirp')
const rimraf = require('rimraf')
const webdriver = require('selenium-webdriver')
const endOfStream = require('end-of-stream')
const GIFEncoder = require('gifencoder')
const pngFileStream = require('png-file-stream')
const sizeOfPng = require('image-size/lib/types/png')
const By = webdriver.By
const { delay, buildWebDriver } = require('./func')
const localesIndex = require('../../app/_locales/index.json')
let driver
captureAllScreens().catch((err) => {
try {
console.error(err)
verboseReportOnFailure()
driver.quit()
} catch (err) {
console.error(err)
}
process.exit(1)
})
async function captureAllScreens() {
let screenshotCount = 0
// common names
let button
let tabs
let element
await cleanScreenShotDir()
// setup selenium and install extension
const extPath = path.resolve('dist/chrome')
driver = buildWebDriver(extPath)
await driver.get('chrome://extensions-frame')
const elems = await driver.findElements(By.css('.extension-list-item-wrapper'))
const extensionId = await elems[1].getAttribute('id')
await driver.get(`chrome-extension://${extensionId}/home.html`)
await delay(500)
tabs = await driver.getAllWindowHandles()
await driver.switchTo().window(tabs[0])
await delay(1000)
await setProviderType('localhost')
await delay(300)
// click try new ui
await driver.findElement(By.css('#app-content > div > div.app-primary.from-right > div > div.flex-row.flex-center.flex-grow > p')).click()
await delay(300)
// close metamask homepage and extra home.html
tabs = await driver.getAllWindowHandles()
// metamask homepage is opened on prod, not dev
if (tabs.length > 2) {
await driver.switchTo().window(tabs[2])
driver.close()
}
await driver.switchTo().window(tabs[1])
driver.close()
await driver.switchTo().window(tabs[0])
await delay(300)
await captureLanguageScreenShots('welcome-new-ui')
// setup account
await delay(1000)
await driver.findElement(By.css('body')).click()
await delay(300)
await captureLanguageScreenShots('welcome')
await driver.findElement(By.css('button')).click()
await captureLanguageScreenShots('create password')
const passwordBox = await driver.findElement(By.css('input[type=password]:nth-of-type(1)'))
const passwordBoxConfirm = await driver.findElement(By.css('input[type=password]:nth-of-type(2)'))
passwordBox.sendKeys('123456789')
passwordBoxConfirm.sendKeys('123456789')
await delay(500)
await captureLanguageScreenShots('choose-password-filled')
await driver.findElement(By.css('button')).click()
await delay(500)
await captureLanguageScreenShots('unique account image')
await driver.findElement(By.css('button')).click()
await delay(500)
await captureLanguageScreenShots('privacy note')
await driver.findElement(By.css('button')).click()
await delay(300)
await captureLanguageScreenShots('terms')
await delay(300)
element = driver.findElement(By.linkText('Attributions'))
await driver.executeScript('arguments[0].scrollIntoView(true)', element)
await delay(300)
await captureLanguageScreenShots('terms-scrolled')
await driver.findElement(By.css('button')).click()
await delay(300)
await captureLanguageScreenShots('secret backup phrase')
await driver.findElement(By.css('button')).click()
await delay(300)
await captureLanguageScreenShots('secret backup phrase')
await driver.findElement(By.css('.backup-phrase__reveal-button')).click()
await delay(300)
await captureLanguageScreenShots('secret backup phrase - reveal')
await driver.findElement(By.css('button')).click()
await delay(300)
await captureLanguageScreenShots('confirm secret backup phrase')
// finish up
console.log('building gif...')
await generateGif()
await driver.quit()
return
//
// await button.click()
// await delay(700)
// this.seedPhase = await driver.findElement(By.css('.twelve-word-phrase')).getText()
// await captureScreenShot('seed phrase')
//
// const continueAfterSeedPhrase = await driver.findElement(By.css('button'))
// await continueAfterSeedPhrase.click()
// await delay(300)
// await captureScreenShot('main screen')
//
// await driver.findElement(By.css('.sandwich-expando')).click()
// await delay(500)
// await captureScreenShot('menu')
// await driver.findElement(By.css('#app-content > div > div:nth-child(3) > span > div > li:nth-child(3)')).click()
// await captureScreenShot('main screen')
// it('should accept account password after lock', async () => {
// await delay(500)
// await driver.findElement(By.id('password-box')).sendKeys('123456789')
// await driver.findElement(By.css('button')).click()
// await delay(500)
// })
//
// it('should show QR code option', async () => {
// await delay(300)
// await driver.findElement(By.css('.fa-ellipsis-h')).click()
// await driver.findElement(By.css('#app-content > div > div.app-primary.from-right > div > div > div:nth-child(1) > flex-column > div.name-label > div > span > i > div > div > li:nth-child(3)')).click()
// await delay(300)
// })
//
// it('should show the account address', async () => {
// this.accountAddress = await driver.findElement(By.css('.ellip-address')).getText()
// await driver.findElement(By.css('.fa-arrow-left')).click()
// await delay(500)
// })
async function captureLanguageScreenShots(label) {
const nonEnglishLocales = localesIndex.filter(localeMeta => localeMeta.code !== 'en')
// take english shot
await captureScreenShot(`${label} (en)`)
for (let localeMeta of nonEnglishLocales) {
// set locale and take shot
await setLocale(localeMeta.code)
await delay(300)
await captureScreenShot(`${label} (${localeMeta.code})`)
}
// return locale to english
await setLocale('en')
await delay(300)
}
async function setLocale(code) {
await driver.executeScript('window.metamask.updateCurrentLocale(arguments[0])', code)
}
async function setProviderType(type) {
await driver.executeScript('window.metamask.setProviderType(arguments[0])', type)
}
// cleanup
await driver.quit()
async function cleanScreenShotDir() {
await pify(rimraf)(`./test-artifacts/screens/`)
}
async function captureScreenShot(label) {
const shotIndex = screenshotCount.toString().padStart(4, '0')
screenshotCount++
const artifactDir = `./test-artifacts/screens/`
await pify(mkdirp)(artifactDir)
// capture screenshot
const screenshot = await driver.takeScreenshot()
await pify(fs.writeFile)(`${artifactDir}/${shotIndex} - ${label}.png`, screenshot, { encoding: 'base64' })
}
async function generateGif(){
// calculate screenshot size
const screenshot = await driver.takeScreenshot()
const pngBuffer = Buffer.from(screenshot, 'base64')
const size = sizeOfPng.calculate(pngBuffer)
// read only the english pngs into gif
const encoder = new GIFEncoder(size.width, size.height)
const stream = pngFileStream('./test-artifacts/screens/* (en).png')
.pipe(encoder.createWriteStream({ repeat: 0, delay: 1000, quality: 10 }))
.pipe(fs.createWriteStream('./test-artifacts/screens/walkthrough (en).gif'))
// wait for end
await pify(endOfStream)(stream)
}
}
async function verboseReportOnFailure(test) {
const artifactDir = `./test-artifacts/${test.title}`
const filepathBase = `${artifactDir}/test-failure`
await pify(mkdirp)(artifactDir)
// capture screenshot
const screenshot = await driver.takeScreenshot()
await pify(fs.writeFile)(`${filepathBase}-screenshot.png`, screenshot, { encoding: 'base64' })
// capture dom source
const htmlSource = await driver.getPageSource()
await pify(fs.writeFile)(`${filepathBase}-dom.html`, htmlSource)
}

@ -0,0 +1,99 @@
const assert = require('assert')
const migration23 = require('../../../app/scripts/migrations/023')
const properTime = (new Date()).getTime()
const storage = {
"meta": {},
"data": {
"TransactionController": {
"transactions": [
]
},
},
}
const transactions = []
const transactions40 = []
const transactions20 = []
const txStates = [
'unapproved',
'approved',
'signed',
'submitted',
'confirmed',
'rejected',
'failed',
'dropped',
]
const deletableTxStates = [
'confirmed',
'rejected',
'failed',
'dropped',
]
let nonDeletableCount = 0
let status
while (transactions.length <= 100) {
status = txStates[Math.floor(Math.random() * Math.floor(txStates.length - 1))]
if (!deletableTxStates.find((s) => s === status)) nonDeletableCount++
transactions.push({status})
}
while (transactions40.length < 40) {
status = txStates[Math.floor(Math.random() * Math.floor(txStates.length - 1))]
transactions40.push({status})
}
while (transactions20.length < 20) {
status = txStates[Math.floor(Math.random() * Math.floor(txStates.length - 1))]
transactions20.push({status})
}
storage.data.TransactionController.transactions = transactions
describe('storage is migrated successfully and the proper transactions are remove from state', () => {
it('should remove transactions that are unneeded', (done) => {
migration23.migrate(storage)
.then((migratedData) => {
let leftoverNonDeletableTxCount = 0
const migratedTransactions = migratedData.data.TransactionController.transactions
migratedTransactions.forEach((tx) => {
if (!deletableTxStates.find((s) => s === tx.status)) {
leftoverNonDeletableTxCount++
}
})
assert.equal(leftoverNonDeletableTxCount, nonDeletableCount, 'migration shouldnt delete transactions we want to keep')
assert((migratedTransactions.length >= 40), `should be equal or greater to 40 if they are non deletable states got ${migratedTransactions.length} transactions`)
done()
}).catch(done)
})
it('should not remove any transactions because 40 is the expectable limit', (done) => {
storage.meta.version = 22
storage.data.TransactionController.transactions = transactions40
migration23.migrate(storage)
.then((migratedData) => {
const migratedTransactions = migratedData.data.TransactionController.transactions
assert.equal(migratedTransactions.length, 40, 'migration shouldnt delete when at limit')
done()
}).catch(done)
})
it('should not remove any transactions because 20 txs is under the expectable limit', (done) => {
storage.meta.version = 22
storage.data.TransactionController.transactions = transactions20
migration23.migrate(storage)
.then((migratedData) => {
const migratedTransactions = migratedData.data.TransactionController.transactions
assert.equal(migratedTransactions.length, 20, 'migration shouldnt delete when under limit')
done()
}).catch(done)
})
})

@ -0,0 +1,49 @@
const assert = require('assert')
const migration24 = require('../../../app/scripts/migrations/024')
const firstTimeState = {
meta: {},
data: require('../../../app/scripts/first-time-state'),
}
const properTime = (new Date()).getTime()
const storage = {
"meta": {},
"data": {
"TransactionController": {
"transactions": [
]
},
},
}
const transactions = []
while (transactions.length <= 10) {
transactions.push({ txParams: { from: '0x8aCce2391c0d510a6c5E5d8f819a678f79b7e675' }, status: 'unapproved' })
transactions.push({ txParams: { from: '0x8aCce2391c0d510a6c5E5d8f819a678f79b7e675' }, status: 'confirmed' })
}
storage.data.TransactionController.transactions = transactions
describe('storage is migrated successfully and the txParams.from are lowercase', () => {
it('should lowercase the from for unapproved txs', (done) => {
migration24.migrate(storage)
.then((migratedData) => {
const migratedTransactions = migratedData.data.TransactionController.transactions
migratedTransactions.forEach((tx) => {
if (tx.status === 'unapproved') assert.equal(tx.txParams.from, '0x8acce2391c0d510a6c5e5d8f819a678f79b7e675')
else assert.equal(tx.txParams.from, '0x8aCce2391c0d510a6c5E5d8f819a678f79b7e675')
})
done()
}).catch(done)
})
it('should migrate first time state', (done) => {
migration24.migrate(firstTimeState)
.then((migratedData) => {
assert.equal(migratedData.meta.version, 24)
done()
}).catch(done)
})
})

@ -0,0 +1,49 @@
const assert = require('assert')
const migration25 = require('../../../app/scripts/migrations/025')
const firstTimeState = {
meta: {},
data: require('../../../app/scripts/first-time-state'),
}
const storage = {
"meta": {},
"data": {
"TransactionController": {
"transactions": [
]
},
},
}
const transactions = []
while (transactions.length <= 10) {
transactions.push({ txParams: { from: '0x8aCce2391c0d510a6c5E5d8f819a678f79b7e675', random: 'stuff', chainId: 2 }, status: 'unapproved' })
transactions.push({ txParams: { from: '0x8aCce2391c0d510a6c5E5d8f819a678f79b7e675' }, status: 'confirmed' })
}
storage.data.TransactionController.transactions = transactions
describe('storage is migrated successfully and the txParams.from are lowercase', () => {
it('should lowercase the from for unapproved txs', (done) => {
migration25.migrate(storage)
.then((migratedData) => {
const migratedTransactions = migratedData.data.TransactionController.transactions
migratedTransactions.forEach((tx) => {
if (tx.status === 'unapproved') assert(!tx.txParams.random)
if (tx.status === 'unapproved') assert(!tx.txParams.chainId)
})
done()
}).catch(done)
})
it('should migrate first time state', (done) => {
migration25.migrate(firstTimeState)
.then((migratedData) => {
assert.equal(migratedData.meta.version, 25)
done()
}).catch(done)
})
})

@ -0,0 +1,17 @@
const assert = require('assert')
const migrationTemplate = require('../../../app/scripts/migrations/template')
const properTime = (new Date()).getTime()
const storage = {
meta: {},
data: {},
}
describe('storage is migrated successfully', () => {
it('should work', (done) => {
migrationTemplate.migrate(storage)
.then((migratedData) => {
assert.equal(migratedData.meta.version, 0)
done()
}).catch(done)
})
})

@ -1,7 +1,8 @@
const assert = require('assert')
const clone = require('clone')
const Migrator = require('../../app/scripts/lib/migrator/')
const migrations = [
const liveMigrations = require('../../app/scripts/migrations/')
const stubMigrations = [
{
version: 1,
migrate: (data) => {
@ -29,13 +30,39 @@ const migrations = [
},
]
const versionedData = {meta: {version: 0}, data: {hello: 'world'}}
const firstTimeState = {
meta: { version: 0 },
data: require('../../app/scripts/first-time-state'),
}
describe('Migrator', () => {
const migrator = new Migrator({ migrations })
const migrator = new Migrator({ migrations: stubMigrations })
it('migratedData version should be version 3', (done) => {
migrator.migrateData(versionedData)
.then((migratedData) => {
assert.equal(migratedData.meta.version, migrations[2].version)
assert.equal(migratedData.meta.version, stubMigrations[2].version)
done()
}).catch(done)
})
it('should match the last version in live migrations', (done) => {
const migrator = new Migrator({ migrations: liveMigrations })
migrator.migrateData(firstTimeState)
.then((migratedData) => {
const last = liveMigrations.length - 1
assert.equal(migratedData.meta.version, liveMigrations[last].version)
done()
}).catch(done)
})
it('should emit an error', function (done) {
this.timeout(15000)
const migrator = new Migrator({ migrations: [{ version: 1, migrate: async () => { throw new Error('test') } } ] })
migrator.on('error', () => done())
migrator.migrateData({ meta: {version: 0} })
.then((migratedData) => {
}).catch(done)
})
})

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

Loading…
Cancel
Save