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. 17
      app/scripts/controllers/currency.js
  27. 10
      app/scripts/controllers/infura.js
  28. 5
      app/scripts/controllers/preferences.js
  29. 13
      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. 5
      app/scripts/migrations/013.js
  44. 5
      app/scripts/migrations/015.js
  45. 4
      app/scripts/migrations/016.js
  46. 3
      app/scripts/migrations/017.js
  47. 3
      app/scripts/migrations/018.js
  48. 4
      app/scripts/migrations/019.js
  49. 3
      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. 119
      development/verify-locale-strings.js
  68. 48
      docs/QA_Guide.md
  69. 9
      docs/translating-guide.md
  70. 591
      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. 16266
      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: jobs:
- prep-deps-npm - prep-deps-npm
- prep-deps-firefox - prep-deps-firefox
- prep-build:
requires:
- prep-deps-npm
- prep-scss: - prep-scss:
requires: requires:
- prep-deps-npm - prep-deps-npm
- test-lint: - test-lint:
requires: requires:
- prep-deps-npm - 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: - test-unit:
requires: requires:
- prep-deps-npm - prep-deps-npm
@ -33,6 +49,16 @@ workflows:
- prep-deps-npm - prep-deps-npm
- prep-deps-firefox - prep-deps-firefox
- prep-scss - 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: jobs:
prep-deps-npm: prep-deps-npm:
@ -41,7 +67,7 @@ jobs:
steps: steps:
- checkout - checkout
- restore_cache: - restore_cache:
key: dependency-cache-{{ checksum "package-lock.json" }} key: dependency-cache-{{ .Revision }}
- run: - run:
name: Install deps via npm name: Install deps via npm
command: npm install command: npm install
@ -49,6 +75,10 @@ jobs:
key: dependency-cache-{{ checksum "package-lock.json" }} key: dependency-cache-{{ checksum "package-lock.json" }}
paths: paths:
- node_modules - node_modules
- save_cache:
key: dependency-cache-{{ .Revision }}
paths:
- node_modules
prep-deps-firefox: prep-deps-firefox:
docker: docker:
@ -65,6 +95,24 @@ jobs:
paths: paths:
- firefox - 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: prep-scss:
docker: docker:
@ -72,7 +120,7 @@ jobs:
steps: steps:
- checkout - checkout
- restore_cache: - restore_cache:
key: dependency-cache-{{ checksum "package-lock.json" }} key: dependency-cache-{{ .Revision }}
- run: - run:
name: Get Scss Cache key name: Get Scss Cache key
# this allows us to checksum against a whole directory # this allows us to checksum against a whole directory
@ -91,18 +139,81 @@ jobs:
steps: steps:
- checkout - checkout
- restore_cache: - restore_cache:
key: dependency-cache-{{ checksum "package-lock.json" }} key: dependency-cache-{{ .Revision }}
- run: - run:
name: Test name: Test
command: npm run lint 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: test-unit:
docker: docker:
- image: circleci/node:8-browsers - image: circleci/node:8-browsers
steps: steps:
- checkout - checkout
- restore_cache: - restore_cache:
key: dependency-cache-{{ checksum "package-lock.json" }} key: dependency-cache-{{ .Revision }}
- run: - run:
name: test:coverage name: test:coverage
command: npm run test:coverage command: npm run test:coverage
@ -124,7 +235,7 @@ jobs:
&& sudo mv /usr/bin/firefox /usr/bin/firefox-old && sudo mv /usr/bin/firefox /usr/bin/firefox-old
&& sudo ln -s /opt/firefox58/firefox /usr/bin/firefox && sudo ln -s /opt/firefox58/firefox /usr/bin/firefox
- restore_cache: - restore_cache:
key: dependency-cache-{{ checksum "package-lock.json" }} key: dependency-cache-{{ .Revision }}
- run: - run:
name: Get Scss Cache key name: Get Scss Cache key
# this allows us to checksum against a whole directory # this allows us to checksum against a whole directory
@ -143,7 +254,7 @@ jobs:
steps: steps:
- checkout - checkout
- restore_cache: - restore_cache:
key: dependency-cache-{{ checksum "package-lock.json" }} key: dependency-cache-{{ .Revision }}
- run: - run:
name: Get Scss Cache key name: Get Scss Cache key
# this allows us to checksum against a whole directory # this allows us to checksum against a whole directory
@ -171,7 +282,7 @@ jobs:
&& sudo mv /usr/bin/firefox /usr/bin/firefox-old && sudo mv /usr/bin/firefox /usr/bin/firefox-old
&& sudo ln -s /opt/firefox58/firefox /usr/bin/firefox && sudo ln -s /opt/firefox58/firefox /usr/bin/firefox
- restore_cache: - restore_cache:
key: dependency-cache-{{ checksum "package-lock.json" }} key: dependency-cache-{{ .Revision }}
- run: - run:
name: Get Scss Cache key name: Get Scss Cache key
# this allows us to checksum against a whole directory # this allows us to checksum against a whole directory
@ -190,7 +301,7 @@ jobs:
steps: steps:
- checkout - checkout
- restore_cache: - restore_cache:
key: dependency-cache-{{ checksum "package-lock.json" }} key: dependency-cache-{{ .Revision }}
- run: - run:
name: Get Scss Cache key name: Get Scss Cache key
# this allows us to checksum against a whole directory # this allows us to checksum against a whole directory
@ -200,3 +311,11 @@ jobs:
- run: - run:
name: test:integration:mascara name: test:integration:mascara
command: npm run test: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": [ "plugins": [
"mocha", "mocha",
"chai", "chai",
"react" "react",
"json"
], ],
"globals": { "globals": {
@ -41,6 +42,7 @@
}, },
"rules": { "rules": {
"no-restricted-globals": ["error", "event"],
"accessor-pairs": 2, "accessor-pairs": 2,
"arrow-spacing": [2, { "before": true, "after": true }], "arrow-spacing": [2, { "before": true, "after": true }],
"block-spacing": [2, "always"], "block-spacing": [2, "always"],

2
.gitignore vendored

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

@ -2,6 +2,42 @@
## Current Master ## 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 ## 4.4.0 Mon Mar 26 2018
- Internationalization: Taiwanese, Thai, Slovenian - Internationalization: Taiwanese, Thai, Slovenian

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

@ -56,7 +56,7 @@
"message": "Balance:" "message": "Balance:"
}, },
"balances": { "balances": {
"message": "Your balances" "message": "Token balance(s)"
}, },
"balanceIsInsufficientGas": { "balanceIsInsufficientGas": {
"message": "Insufficient balance for current gas total" "message": "Insufficient balance for current gas total"
@ -235,7 +235,7 @@
"done": { "done": {
"message": "Done" "message": "Done"
}, },
"downloadStatelogs": { "downloadStateLogs": {
"message": "Download State Logs" "message": "Download State Logs"
}, },
"dropped": { "dropped": {
@ -671,6 +671,12 @@
"save": { "save": {
"message": "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": { "saveAsFile": {
"message": "Save as File", "message": "Save as File",
"description": "Account export process" "description": "Account export process"
@ -820,6 +826,9 @@
"transactions": { "transactions": {
"message": "transactions" "message": "transactions"
}, },
"transactionError": {
"message": "Transaction Error. Exception thrown in contract code."
},
"transactionMemo": { "transactionMemo": {
"message": "Transaction memo (optional)" "message": "Transaction memo (optional)"
}, },

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

@ -223,7 +223,7 @@
"done": { "done": {
"message": "सपनन" "message": "सपनन"
}, },
"downloadStatelogs": { "downloadStateLogs": {
"message": "रय लग डउनलड कर" "message": "रय लग डउनलड कर"
}, },
"edit": { "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": { "done": {
"message": "Finito" "message": "Finito"
}, },
"downloadStatelogs": { "downloadStateLogs": {
"message": "Scarica i log di Stato" "message": "Scarica i log di Stato"
}, },
"edit": { "edit": {

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

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

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

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

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

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

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

@ -3,10 +3,10 @@
<head> <head>
<meta charset="utf-8"> <meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1 user-scalable=no"> <meta name="viewport" content="width=device-width, initial-scale=1 user-scalable=no">
<title>MetaMask Plugin</title> <title>MetaMask</title>
</head> </head>
<body> <body>
<div id="app-content"></div> <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> </body>
</html> </html>

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

@ -11,6 +11,6 @@
</head> </head>
<body class="notification" style="height:600px;"> <body class="notification" style="height:600px;">
<div id="app-content"></div> <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> </body>
</html> </html>

@ -3,10 +3,10 @@
<head> <head>
<meta charset="utf-8"> <meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1 user-scalable=no"> <meta name="viewport" content="width=device-width, initial-scale=1 user-scalable=no">
<title>MetaMask Plugin</title> <title>MetaMask</title>
</head> </head>
<body style="width:357px; height:600px;"> <body style="width:357px; height:600px;">
<div id="app-content"></div> <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> </body>
</html> </html>

@ -19,10 +19,11 @@ const setupRaven = require('./lib/setupRaven')
const reportFailedTxToSentry = require('./lib/reportFailedTxToSentry') const reportFailedTxToSentry = require('./lib/reportFailedTxToSentry')
const setupMetamaskMeshMetrics = require('./lib/setupMetamaskMeshMetrics') const setupMetamaskMeshMetrics = require('./lib/setupMetamaskMeshMetrics')
const EdgeEncryptor = require('./edge-encryptor') 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 STORAGE_KEY = 'metamask-config'
const METAMASK_DEBUG = 'GULP_METAMASK_DEBUG' const METAMASK_DEBUG = process.env.METAMASK_DEBUG
window.log = log window.log = log
log.setDefaultLevel(METAMASK_DEBUG ? 'debug' : 'warn') log.setDefaultLevel(METAMASK_DEBUG ? 'debug' : 'warn')
@ -58,7 +59,8 @@ setupMetamaskMeshMetrics()
async function initialize () { async function initialize () {
const initState = await loadStateFromPersistence() const initState = await loadStateFromPersistence()
await setupController(initState) const initLangCode = await getFirstPreferredLangCode()
await setupController(initState, initLangCode)
log.debug('MetaMask initialization complete.') log.debug('MetaMask initialization complete.')
} }
@ -76,6 +78,16 @@ async function loadStateFromPersistence () {
diskStore.getState() || diskStore.getState() ||
migrator.generateInitialState(firstTimeState) 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 // migrate data
versionedData = await migrator.migrateData(versionedData) versionedData = await migrator.migrateData(versionedData)
if (!versionedData) { if (!versionedData) {
@ -83,13 +95,20 @@ async function loadStateFromPersistence () {
} }
// write to disk // 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 just the data
return versionedData.data return versionedData.data
} }
function setupController (initState) { function setupController (initState, initLangCode) {
// //
// MetaMask Controller // MetaMask Controller
// //
@ -101,6 +120,8 @@ function setupController (initState) {
showUnapprovedTx: triggerUi, showUnapprovedTx: triggerUi,
// initial state // initial state
initState, initState,
// initial locale code
initLangCode,
// platform specific api // platform specific api
platform, platform,
encryptor: isEdge ? new EdgeEncryptor() : undefined, encryptor: isEdge ? new EdgeEncryptor() : undefined,

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

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

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

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

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

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

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

@ -161,9 +161,11 @@ module.exports = class TransactionController extends EventEmitter {
this.emit(`${txMeta.id}:unapproved`, txMeta) this.emit(`${txMeta.id}:unapproved`, txMeta)
} }
async newUnapprovedTransaction (txParams) { async newUnapprovedTransaction (txParams, opts = {}) {
log.debug(`MetaMaskController newUnapprovedTransaction ${JSON.stringify(txParams)}`) log.debug(`MetaMaskController newUnapprovedTransaction ${JSON.stringify(txParams)}`)
const initialTxMeta = await this.addUnapprovedTransaction(txParams) const initialTxMeta = await this.addUnapprovedTransaction(txParams)
initialTxMeta.origin = opts.origin
this.txStateManager.updateTx(initialTxMeta, '#newUnapprovedTransaction - adding the origin')
// listen for tx completion (success, fail) // listen for tx completion (success, fail)
return new Promise((resolve, reject) => { return new Promise((resolve, reject) => {
this.txStateManager.once(`${initialTxMeta.id}:finished`, (finishedTxMeta) => { this.txStateManager.once(`${initialTxMeta.id}:finished`, (finishedTxMeta) => {
@ -183,14 +185,15 @@ module.exports = class TransactionController extends EventEmitter {
async addUnapprovedTransaction (txParams) { async addUnapprovedTransaction (txParams) {
// validate // validate
await this.txGasUtil.validateTxParams(txParams) const normalizedTxParams = this._normalizeTxParams(txParams)
this._validateTxParams(normalizedTxParams)
// construct txMeta // construct txMeta
const txMeta = this.txStateManager.generateTxMeta({txParams}) let txMeta = this.txStateManager.generateTxMeta({ txParams: normalizedTxParams })
this.addTx(txMeta) this.addTx(txMeta)
this.emit('newUnapprovedTx', txMeta) this.emit('newUnapprovedTx', txMeta)
// add default tx params // add default tx params
try { try {
await this.addTxDefaults(txMeta) txMeta = await this.addTxDefaults(txMeta)
} catch (error) { } catch (error) {
console.log(error) console.log(error)
this.txStateManager.setTxStatusFailed(txMeta.id, error) this.txStateManager.setTxStatusFailed(txMeta.id, error)
@ -273,12 +276,14 @@ module.exports = class TransactionController extends EventEmitter {
async signTransaction (txId) { async signTransaction (txId) {
const txMeta = this.txStateManager.getTx(txId) const txMeta = this.txStateManager.getTx(txId)
const txParams = txMeta.txParams
const fromAddress = txParams.from
// add network/chain id // 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) const ethTx = new Transaction(txParams)
await this.signEthTx(ethTx, fromAddress) await this.signEthTx(ethTx, fromAddress)
// set state to signed
this.txStateManager.setTxStatusSigned(txMeta.id) this.txStateManager.setTxStatusSigned(txMeta.id)
const rawTx = ethUtil.bufferToHex(ethTx.serialize()) const rawTx = ethUtil.bufferToHex(ethTx.serialize())
return rawTx return rawTx
@ -309,6 +314,60 @@ module.exports = class TransactionController extends EventEmitter {
// PRIVATE METHODS // 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) { _markNonceDuplicatesDropped (txId) {
this.txStateManager.setTxStatusConfirmed(txId) this.txStateManager.setTxStatusConfirmed(txId)
// get the confirmed transactions nonce and from address // get the confirmed transactions nonce and from address

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

@ -9,7 +9,7 @@ const setupDappAutoReload = require('./lib/auto-reload.js')
const MetamaskInpageProvider = require('./lib/inpage-provider.js') const MetamaskInpageProvider = require('./lib/inpage-provider.js')
restoreContextAfterImports() restoreContextAfterImports()
const METAMASK_DEBUG = 'GULP_METAMASK_DEBUG' const METAMASK_DEBUG = process.env.METAMASK_DEBUG
window.log = log window.log = log
log.setDefaultLevel(METAMASK_DEBUG ? 'debug' : 'warn') 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 = {}) { constructor (opts = {}) {
super()
const migrations = opts.migrations || [] const migrations = opts.migrations || []
// sort migrations by version // sort migrations by version
this.migrations = migrations.sort((a, b) => a.version - b.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 // run all pending migrations on meta in place
async migrateData (versionedData = this.generateInitialState()) { async migrateData (versionedData = this.generateInitialState()) {
// get all migrations that have not yet been run
const pendingMigrations = this.migrations.filter(migrationIsPending) const pendingMigrations = this.migrations.filter(migrationIsPending)
// perform each migration
for (const index in pendingMigrations) { for (const index in pendingMigrations) {
const migration = pendingMigrations[index] const migration = pendingMigrations[index]
versionedData = await migration.migrate(versionedData) try {
if (!versionedData.data) throw new Error('Migrator - migration returned empty data') // attempt migration and validate
if (versionedData.version !== undefined && versionedData.meta.version !== migration.version) throw new Error('Migrator - Migration did not update version number correctly') 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 return versionedData

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

@ -1,5 +1,4 @@
const ethJsRpcSlug = 'Error: [ethjs-rpc] rpc error with payload ' const extractEthjsErrorMessage = require('./extractEthjsErrorMessage')
const errorLabelPrefix = 'Error: '
module.exports = reportFailedTxToSentry module.exports = reportFailedTxToSentry
@ -9,30 +8,9 @@ module.exports = reportFailedTxToSentry
// //
function reportFailedTxToSentry({ raven, txMeta }) { function reportFailedTxToSentry({ raven, txMeta }) {
const errorMessage = extractErrorMessage(txMeta.err.message) const errorMessage = 'Transaction Failed: ' + extractEthjsErrorMessage(txMeta.err.message)
raven.captureMessage(errorMessage, { raven.captureMessage(errorMessage, {
// "extra" key is required by Sentry // "extra" key is required by Sentry
extra: txMeta, 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 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 PROD = 'https://3567c198f8a8412082d32655da2961d0@sentry.io/273505'
const DEV = 'https://f59f3dd640d2429d9d0e2445a87ea8e1@sentry.io/273496' const DEV = 'https://f59f3dd640d2429d9d0e2445a87ea8e1@sentry.io/273496'
@ -21,8 +22,22 @@ function setupRaven(opts) {
const client = Raven.config(ravenTarget, { const client = Raven.config(ravenTarget, {
release, release,
transport: function(opts) { transport: function(opts) {
// modify report urls
const report = opts.data 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) rewriteReportUrls(report)
// make request normally // make request normally
client._makeRequest(opts) client._makeRequest(opts)

@ -4,7 +4,7 @@ const {
BnMultiplyByFraction, BnMultiplyByFraction,
bnToHex, bnToHex,
} = require('./util') } = 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. 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: // if recipient has no code, gas is 21k max:
const recipient = txParams.to const recipient = txParams.to
const hasRecipient = Boolean(recipient) 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')) { if (hasRecipient && (!code || code === '0x')) {
txParams.gas = SIMPLE_GAS_COST txParams.gas = SIMPLE_GAS_COST
txMeta.simpleSend = true // Prevents buffer addition txMeta.simpleSend = true // Prevents buffer addition
@ -98,30 +100,4 @@ module.exports = class TxGasUtil {
// otherwise use blockGasLimit // otherwise use blockGasLimit
return bnToHex(upperGasLimitBn) 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) }, opts)
} }
// Returns the number of txs for the current network.
getTxCount () {
return this.getTxList().length
}
getTxList () { getTxList () {
const network = this.getNetwork() const network = this.getNetwork()
const fullTxList = this.getFullTxList() const fullTxList = this.getFullTxList()
@ -88,7 +83,7 @@ module.exports = class TransactionStateManager extends EventEmitter {
txMeta.history.push(snapshot) txMeta.history.push(snapshot)
const transactions = this.getFullTxList() const transactions = this.getFullTxList()
const txCount = this.getTxCount() const txCount = transactions.length
const txHistoryLimit = this.txHistoryLimit const txHistoryLimit = this.txHistoryLimit
// checks if the length of the tx history is // checks if the length of the tx history is
@ -111,12 +106,13 @@ module.exports = class TransactionStateManager extends EventEmitter {
} }
updateTx (txMeta, note) { updateTx (txMeta, note) {
// validate txParams
if (txMeta.txParams) { if (txMeta.txParams) {
Object.keys(txMeta.txParams).forEach((key) => { if (typeof txMeta.txParams.data === 'undefined') {
const value = txMeta.txParams[key] delete txMeta.txParams.data
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')
}) this.validateTxParams(txMeta.txParams)
} }
// create txMeta snapshot for history // create txMeta snapshot for history
@ -144,6 +140,23 @@ module.exports = class TransactionStateManager extends EventEmitter {
this.updateTx(txMeta, `txStateManager#updateTxParams`) 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: Takes an object of fields to search for eg:
let thingsToLookFor = { let thingsToLookFor = {

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

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

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

@ -28,7 +28,10 @@ module.exports = {
function transformState (state) { function transformState (state) {
const newState = state const newState = state
const { TransactionController } = newState
if (TransactionController && TransactionController.transactions) {
const transactions = newState.TransactionController.transactions const transactions = newState.TransactionController.transactions
newState.TransactionController.transactions = transactions.map((txMeta) => { newState.TransactionController.transactions = transactions.map((txMeta) => {
if (!txMeta.err) return txMeta if (!txMeta.err) return txMeta
if (txMeta.err === 'transaction with the same hash was already imported.') { if (txMeta.err === 'transaction with the same hash was already imported.') {
@ -37,5 +40,6 @@ function transformState (state) {
} }
return txMeta return txMeta
}) })
}
return newState return newState
} }

@ -27,6 +27,8 @@ module.exports = {
function transformState (state) { function transformState (state) {
const newState = state const newState = state
const { TransactionController } = newState
if (TransactionController && TransactionController.transactions) {
const transactions = newState.TransactionController.transactions const transactions = newState.TransactionController.transactions
newState.TransactionController.transactions = transactions.map((txMeta) => { newState.TransactionController.transactions = transactions.map((txMeta) => {
if (!txMeta.status === 'failed') return txMeta if (!txMeta.status === 'failed') return txMeta
@ -36,5 +38,6 @@ function transformState (state) {
} }
return txMeta return txMeta
}) })
}
return newState return newState
} }

@ -29,6 +29,8 @@ module.exports = {
function transformState (state) { function transformState (state) {
const newState = state const newState = state
const { TransactionController } = newState
if (TransactionController && TransactionController.transactions) {
const transactions = newState.TransactionController.transactions const transactions = newState.TransactionController.transactions
newState.TransactionController.transactions = transactions.map((txMeta) => { newState.TransactionController.transactions = transactions.map((txMeta) => {
// no history: initialize // no history: initialize
@ -48,5 +50,6 @@ function transformState (state) {
txMeta.history = newHistory txMeta.history = newHistory
return txMeta return txMeta
}) })
}
return newState return newState
} }

@ -29,6 +29,9 @@ module.exports = {
function transformState (state) { function transformState (state) {
const newState = state const newState = state
const { TransactionController } = newState
if (TransactionController && TransactionController.transactions) {
const transactions = newState.TransactionController.transactions const transactions = newState.TransactionController.transactions
newState.TransactionController.transactions = transactions.map((txMeta, _, txList) => { newState.TransactionController.transactions = transactions.map((txMeta, _, txList) => {
@ -55,6 +58,7 @@ function transformState (state) {
} }
return txMeta return txMeta
}) })
}
return newState return newState
} }

@ -28,6 +28,8 @@ module.exports = {
function transformState (state) { function transformState (state) {
const newState = state const newState = state
const { TransactionController } = newState
if (TransactionController && TransactionController.transactions) {
const transactions = newState.TransactionController.transactions const transactions = newState.TransactionController.transactions
newState.TransactionController.transactions = transactions.map((txMeta) => { newState.TransactionController.transactions = transactions.map((txMeta) => {
@ -35,5 +37,6 @@ function transformState (state) {
txMeta.submittedTime = (new Date()).getTime() txMeta.submittedTime = (new Date()).getTime()
return txMeta return txMeta
}) })
}
return newState 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('./020'),
require('./021'), require('./021'),
require('./022'), 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 fs = require('fs')
const path = require('path') const path = require('path')
const promisify = require('pify') const promisify = require('pify')
const enLocaleMessages = require('../app/_locales/en/messages.json')
start().catch(console.error) start().catch(console.error)
@ -12,6 +14,9 @@ async function start () {
const stateFilePath = path.join(__dirname, 'states', stateFileName) const stateFilePath = path.join(__dirname, 'states', stateFileName)
const stateFileContent = await promisify(fs.readFile)(stateFilePath, 'utf8') const stateFileContent = await promisify(fs.readFile)(stateFilePath, 'utf8')
const state = JSON.parse(stateFileContent) const state = JSON.parse(stateFileContent)
state.localeMessages = { en: enLocaleMessages, current: {} }
const stateName = stateFileName.split('.')[0].replace(/-/g, ' ', 'g') const stateName = stateFileName.split('.')[0].replace(/-/g, ' ', 'g')
states[stateName] = state 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": {}, "errors": {},
"maxModeOn": false, "maxModeOn": false,
"editingTransactionId": null "editingTransactionId": null
} },
"currentLocale": "en"
}, },
"appState": { "appState": {
"menuOpen": false, "menuOpen": false,

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

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

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

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

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

File diff suppressed because one or more lines are too long

@ -10,87 +10,88 @@
// //
//////////////////////////////////////////////////////////////////////////////// ////////////////////////////////////////////////////////////////////////////////
var fs = require('fs') const fs = require('fs')
var path = require('path') const path = require('path')
const localeIndex = require('../app/_locales/index.json')
console.log('Locale Verification') console.log('Locale Verification')
var locale = process.argv[2] const specifiedLocale = process.argv[2]
if (!locale || locale == '') { if (specifiedLocale) {
console.log('Must enter a locale as argument. exitting') console.log(`Verifying selected locale "${specifiedLocale}":\n\n`)
process.exit(1) 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 { function verifyLocale({ localeMeta }) {
localeObj = JSON.parse(fs.readFileSync(localeFilePath, 'utf8')); const localeCode = localeMeta.code
} catch (e) { const localeName = localeMeta.name
if(e.code == 'ENOENT') {
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') console.log('Locale file not found')
} else { } else {
console.log('Error opening your locale file: ', e) 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 {
try { const englishFilePath = path.join(process.cwd(), 'app', '_locales', 'en', 'messages.json')
englishObj = JSON.parse(fs.readFileSync(englishFilePath, 'utf8')); englishLocale = JSON.parse(fs.readFileSync(englishFilePath, 'utf8'));
} catch (e) { } catch (e) {
if(e.code == 'ENOENT') { if(e.code == 'ENOENT') {
console.log("English File not found") console.log('English File not found')
} else { } else {
console.log("Error opening english locale file: ", e) 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')
var counter = 0
var foundErrorA = false
var notFound = [];
Object.keys(localeObj).forEach(function(key){
if (!englishObj[key]) {
foundErrorA = true
notFound.push(key)
} }
counter++
})
if (foundErrorA) { // console.log(' verifying whether all your locale ("${localeCode}") strings are contained in the english one')
console.log('\nThe following string(s) is(are) not found in the english locale:') const extraItems = compareLocalesForMissingItems({ base: targetLocale, subject: englishLocale })
notFound.forEach(function(key) { // console.log('\n verifying whether your locale ("${localeCode}") contains all english strings')
console.log(key) const missingItems = compareLocalesForMissingItems({ base: englishLocale, subject: targetLocale })
})
} else {
console.log('\tall ' + counter +' strings declared in your locale were found in the english one')
}
console.log('\n\tverifying whether your locale contains all english strings') const englishEntryCount = Object.keys(englishLocale).length
const coveragePercent = 100 * (englishEntryCount - missingItems.length) / englishEntryCount
var counter = 0 console.log(`Status of **${localeName} (${localeCode})** ${coveragePercent.toFixed(2)}% coverage:`)
var foundErrorB = false
var notFound = []; if (extraItems.length) {
Object.keys(englishObj).forEach(function(key){ console.log('\nMissing from english locale:')
if (!localeObj[key]) { extraItems.forEach(function(key) {
foundErrorB = true console.log(` - [ ] ${key}`)
notFound.push(key) })
} else {
// console.log(` all ${counter} strings declared in your locale ("${localeCode}") were found in the english one`)
} }
counter++
})
if (foundErrorB) { if (missingItems.length) {
console.log('\nThe following string(s) is(are) not found in the your locale:') console.log(`\nMissing:`)
notFound.forEach(function(key) { missingItems.forEach(function(key) {
console.log(key) console.log(` - [ ] ${key}`)
}) })
} else { } else {
console.log('\tall ' + counter +' english strings were found in your locale!') // console.log(` all ${counter} english strings were found in your locale ("${localeCode}")!`)
}
if (!extraItems.length && !missingItems.length) {
console.log('Full coverage : )')
}
} }
if (!foundErrorA && !foundErrorB) { function compareLocalesForMissingItems({ base, subject }) {
console.log('You are good to go') 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 ## 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. 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 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!). 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') const watchify = require('watchify')
var browserify = require('browserify') const browserify = require('browserify')
var disc = require('disc') const envify = require('envify/custom')
var gulp = require('gulp') const disc = require('disc')
var source = require('vinyl-source-stream') const gulp = require('gulp')
var buffer = require('vinyl-buffer') const source = require('vinyl-source-stream')
var gutil = require('gulp-util') const buffer = require('vinyl-buffer')
var watch = require('gulp-watch') const gutil = require('gulp-util')
var sourcemaps = require('gulp-sourcemaps') const watch = require('gulp-watch')
var jsoneditor = require('gulp-json-editor') const sourcemaps = require('gulp-sourcemaps')
var zip = require('gulp-zip') const jsoneditor = require('gulp-json-editor')
var assign = require('lodash.assign') const zip = require('gulp-zip')
var livereload = require('gulp-livereload') const assign = require('lodash.assign')
var del = require('del') const livereload = require('gulp-livereload')
var eslint = require('gulp-eslint') const del = require('del')
var fs = require('fs') const eslint = require('gulp-eslint')
var path = require('path') const fs = require('fs')
var manifest = require('./app/manifest.json') const path = require('path')
var gulpif = require('gulp-if') const manifest = require('./app/manifest.json')
var replace = require('gulp-replace') const replace = require('gulp-replace')
var mkdirp = require('mkdirp') const mkdirp = require('mkdirp')
var asyncEach = require('async/each') const asyncEach = require('async/each')
var exec = require('child_process').exec const exec = require('child_process').exec
var sass = require('gulp-sass') const sass = require('gulp-sass')
var autoprefixer = require('gulp-autoprefixer') const autoprefixer = require('gulp-autoprefixer')
var gulpStylelint = require('gulp-stylelint') const gulpStylelint = require('gulp-stylelint')
var stylefmt = require('gulp-stylefmt') const stylefmt = require('gulp-stylefmt')
var uglify = require('gulp-uglify-es').default const uglify = require('gulp-uglify-es').default
var babel = require('gulp-babel') const babel = require('gulp-babel')
const debug = require('gulp-debug')
const pify = require('pify')
var disableDebugTools = gutil.env.disableDebugTools const gulpMultiProcess = require('gulp-multi-process')
var debug = gutil.env.debug 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 // 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/', source: './app/_locales/',
destinations: [ destinations: commonPlatforms.map(platform => `./dist/${platform}/_locales`),
'./dist/firefox/_locales', })
'./dist/chrome/_locales', createCopyTasks('images', {
'./dist/edge/_locales',
'./dist/opera/_locales',
]
}))
gulp.task('copy:images', copyTask({
source: './app/images/', source: './app/images/',
destinations: [ destinations: commonPlatforms.map(platform => `./dist/${platform}/images`),
'./dist/firefox/images', })
'./dist/chrome/images', createCopyTasks('contractImages', {
'./dist/edge/images',
'./dist/opera/images',
],
}))
gulp.task('copy:contractImages', copyTask({
source: './node_modules/eth-contract-metadata/images/', source: './node_modules/eth-contract-metadata/images/',
destinations: [ destinations: commonPlatforms.map(platform => `./dist/${platform}/images/contract`),
'./dist/firefox/images/contract', })
'./dist/chrome/images/contract', createCopyTasks('fonts', {
'./dist/edge/images/contract',
'./dist/opera/images/contract',
],
}))
gulp.task('copy:fonts', copyTask({
source: './app/fonts/', source: './app/fonts/',
destinations: [ destinations: commonPlatforms.map(platform => `./dist/${platform}/fonts`),
'./dist/firefox/fonts', })
'./dist/chrome/fonts', createCopyTasks('reload', {
'./dist/edge/fonts', devOnly: true,
'./dist/opera/fonts',
],
}))
gulp.task('copy:reload', copyTask({
source: './app/scripts/', source: './app/scripts/',
destinations: [
'./dist/firefox/scripts',
'./dist/chrome/scripts',
'./dist/edge/scripts',
'./dist/opera/scripts',
],
pattern: '/chromereload.js', pattern: '/chromereload.js',
})) destinations: commonPlatforms.map(platform => `./dist/${platform}`),
gulp.task('copy:root', copyTask({ })
createCopyTasks('html', {
source: './app/', source: './app/',
destinations: [ pattern: '/*.html',
'./dist/firefox', destinations: commonPlatforms.map(platform => `./dist/${platform}`),
'./dist/chrome', })
'./dist/edge',
'./dist/opera', // copy extension
],
pattern: '/*', 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() { gulp.task('manifest:chrome', function() {
return gulp.src('./dist/chrome/manifest.json') return gulp.src('./dist/chrome/manifest.json')
@ -134,52 +185,40 @@ gulp.task('manifest:production', function() {
],{base: './dist/'}) ],{base: './dist/'})
// Exclude chromereload script in production: // Exclude chromereload script in production:
.pipe(gulpif(!debug,jsoneditor(function(json) { .pipe(jsoneditor(function(json) {
json.background.scripts = json.background.scripts.filter((script) => { json.background.scripts = json.background.scripts.filter((script) => {
return !script.includes('chromereload') return !script.includes('chromereload')
}) })
return json return json
}))) }))
.pipe(gulp.dest('./dist/', { overwrite: true })) .pipe(gulp.dest('./dist/', { overwrite: true }))
}) })
const staticFiles = [ gulp.task('copy',
'locales', gulp.series(
'images', gulp.parallel(...copyTaskNames),
'fonts', 'manifest:production',
'root' 'manifest:chrome',
] 'manifest:opera'
)
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) { gulp.task('dev:copy',
exec('npm ls', (err, stdoutOutput, stderrOutput) => { gulp.series(
if (err) return cb(err) gulp.parallel(...copyDevTaskNames),
const browsers = ['firefox','chrome','edge','opera'] 'manifest:chrome',
asyncEach(browsers, (target, done) => { 'manifest:opera'
fs.writeFile(`./dist/${target}/deps.txt`, stdoutOutput, done) )
}, cb) )
})
})
// lint js // 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 () { gulp.task('lint', function () {
// Ignoring node_modules, dist/firefox, and docs folders: // 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')))) .pipe(eslint(fs.readFileSync(path.join(__dirname, '.eslintrc'))))
// eslint.format() outputs the lint results to the console. // eslint.format() outputs the lint results to the console.
// Alternatively use eslint.formatEach() (see Docs). // Alternatively use eslint.formatEach() (see Docs).
@ -190,47 +229,55 @@ gulp.task('lint', function () {
}); });
gulp.task('lint:fix', 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(Object.assign(fs.readFileSync(path.join(__dirname, '.eslintrc')), {fix: true})))
.pipe(eslint.format()) .pipe(eslint.format())
.pipe(eslint.failAfterError()) .pipe(eslint.failAfterError())
}); });
/* // scss compilation and autoprefixing tasks
gulp.task('default', ['lint'], function () {
// This will only run if the lint task is successful...
});
*/
// build js gulp.task('build:scss', createScssBuildTask({
src: 'ui/app/css/index.scss',
dest: 'ui/app/css/output',
devMode: false,
}))
const jsFiles = [ gulp.task('dev:scss', createScssBuildTask({
'inpage', src: 'ui/app/css/index.scss',
'contentscript', dest: 'ui/app/css/output',
'background', devMode: true,
'popup', 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 () { function buildScss() {
return gulp.src('ui/app/css/index.scss') return gulp.src(src)
.pipe(sourcemaps.init()) .pipe(sourcemaps.init())
.pipe(sass().on('error', sass.logError)) .pipe(sass().on('error', sass.logError))
.pipe(sourcemaps.write()) .pipe(sourcemaps.write())
.pipe(autoprefixer()) .pipe(autoprefixer())
.pipe(gulp.dest('ui/app/css/output')) .pipe(gulp.dest(dest))
}) }
gulp.task('watch:scss', function() { }
gulp.watch(['ui/app/css/**/*.scss'], gulp.series(['build:scss']))
})
gulp.task('lint-scss', function() { gulp.task('lint-scss', function() {
return gulp return gulp
.src('ui/app/css/itcss/**/*.scss') .src('ui/app/css/itcss/**/*.scss')
.pipe(gulpStylelint({ .pipe(gulpStylelint({
reporters: [ reporters: [
{formatter: 'string', console: true} { formatter: 'string', console: true }
], ],
fix: true, fix: true,
})); }));
@ -242,46 +289,84 @@ gulp.task('fmt-scss', function () {
.pipe(gulp.dest('ui/app/css/itcss')); .pipe(gulp.dest('ui/app/css/itcss'));
}); });
// build js
const buildJsFiles = [
'inpage',
'contentscript',
'background',
'ui',
]
// bundle tasks // 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 })
}
var jsDevStrings = jsFiles.map(jsFile => `dev:js:${jsFile}`) function createTasksForBuildJsMascara({ taskPrefix, devMode, bundleTaskOpts = {} }) {
var jsBuildStrings = jsFiles.map(jsFile => `build:js:${jsFile}`) // 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 })
}
jsFiles.forEach((jsFile) => { function createTasksForBuildJs({ rootDir, taskPrefix, bundleTaskOpts, destinations, buildPhase1 = [], buildPhase2 = [] }) {
gulp.task(`dev:js:${jsFile}`, bundleTask({ // bundle task for each file
watch: true, const jsFiles = [].concat(buildPhase1, buildPhase2)
jsFiles.forEach((jsFile) => {
gulp.task(`${taskPrefix}:${jsFile}`, bundleTask(Object.assign({
label: jsFile, label: jsFile,
filename: `${jsFile}.js`, filename: `${jsFile}.js`,
isBuild: false filepath: `${rootDir}/${jsFile}.js`,
})) destinations,
gulp.task(`build:js:${jsFile}`, bundleTask({ }, bundleTaskOpts)))
watch: false, })
label: jsFile, // compose into larger task
filename: `${jsFile}.js`, const subtasks = []
isBuild: true 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 firstDevString = jsDevStrings.shift()
gulp.task('dev:js', gulp.series(firstDevString, gulp.parallel(...jsDevStrings)))
// inpage must be built before all other scripts: gulp.task(taskPrefix, gulp.series(subtasks))
const firstBuildString = jsBuildStrings.shift() }
gulp.task('build:js', gulp.series(firstBuildString, gulp.parallel(...jsBuildStrings)))
// disc bundle analyzer tasks // disc bundle analyzer tasks
jsFiles.forEach((jsFile) => { buildJsFiles.forEach((jsFile) => {
gulp.task(`disc:${jsFile}`, discTask({ label: jsFile, filename: `${jsFile}.js` })) 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 // clean dist
gulp.task('clean', function clean() { gulp.task('clean', function clean() {
return del(['./dist/*']) return del(['./dist/*'])
}) })
@ -293,40 +378,88 @@ gulp.task('zip:edge', zipTask('edge'))
gulp.task('zip:opera', zipTask('opera')) gulp.task('zip:opera', zipTask('opera'))
gulp.task('zip', gulp.parallel('zip:chrome', 'zip:firefox', 'zip:edge', 'zip: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 // 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.task('dist', gulp.series('apply-prod-environment', 'build', 'zip')) gulp.series(
'build',
'zip'
)
)
// task generators // 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) { function zipTask(target) {
return () => { return () => {
return gulp.src(`dist/${target}/**`) return gulp.src(`dist/${target}/**`)
@ -337,24 +470,48 @@ function zipTask(target) {
function generateBundler(opts, performBundle) { function generateBundler(opts, performBundle) {
const browserifyOpts = assign({}, watchify.args, { const browserifyOpts = assign({}, watchify.args, {
entries: ['./app/scripts/'+opts.filename], entries: [opts.filepath],
plugin: 'browserify-derequire', plugin: 'browserify-derequire',
debug: true, debug: opts.buildSourceMaps,
fullPaths: debug, fullPaths: opts.buildWithFullPaths,
}) })
let bundler = browserify(browserifyOpts) 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) { if (opts.watch) {
bundler = watchify(bundler) bundler = watchify(bundler)
// on any file update, re-runs the 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 return bundler
} }
function discTask(opts) { function discTask(opts) {
opts = Object.assign({
buildWithFullPaths: true,
}, opts)
const bundler = generateBundler(opts, performBundle) const bundler = generateBundler(opts, performBundle)
// output build logs to terminal // output build logs to terminal
bundler.on('log', gutil.log) bundler.on('log', gutil.log)
@ -363,9 +520,9 @@ function discTask(opts) {
function performBundle(){ function performBundle(){
// start "disc" build // start "disc" build
let discDir = path.join(__dirname, 'disc') const discDir = path.join(__dirname, 'disc')
mkdirp.sync(discDir) mkdirp.sync(discDir)
let discPath = path.join(discDir, `${opts.label}.html`) const discPath = path.join(discDir, `${opts.label}.html`)
return ( return (
bundler.bundle() bundler.bundle()
@ -384,12 +541,10 @@ function bundleTask(opts) {
return performBundle return performBundle
function performBundle(){ function performBundle(){
return ( let buildStream = bundler.bundle()
bundler.bundle()
// handle errors // handle errors
.on('error', (err) => { buildStream.on('error', (err) => {
beep() beep()
if (opts.watch) { if (opts.watch) {
console.warn(err.stack) console.warn(err.stack)
@ -397,30 +552,34 @@ function bundleTask(opts) {
throw err throw err
} }
}) })
// process bundles
buildStream = buildStream
// convert bundle stream to gulp vinyl stream // convert bundle stream to gulp vinyl stream
.pipe(source(opts.filename)) .pipe(source(opts.filename))
// inject variables into bundle
.pipe(replace('\'GULP_METAMASK_DEBUG\'', debug))
// buffer file contents (?) // buffer file contents (?)
.pipe(buffer()) .pipe(buffer())
// sourcemaps
// Initialize Source Maps
if (opts.buildSourceMaps) {
buildStream = buildStream
// loads map from browserify file // loads map from browserify file
.pipe(sourcemaps.init({ loadMaps: true })) .pipe(sourcemaps.init({ loadMaps: true }))
// Minification }
.pipe(gulpif(opts.isBuild, uglify({
mangle: { reserved: [ 'MetamaskInpageProvider' ] }, // Finalize Source Maps (writes .map file)
}))) if (opts.buildSourceMaps) {
// writes .map file buildStream = buildStream
.pipe(sourcemaps.write(debug ? './' : '../../sourcemaps')) .pipe(sourcemaps.write(opts.sourceMapDir))
}
// write completed bundles // write completed bundles
.pipe(gulp.dest('./dist/firefox/scripts')) opts.destinations.forEach((dest) => {
.pipe(gulp.dest('./dist/chrome/scripts')) buildStream = buildStream.pipe(gulp.dest(dest))
.pipe(gulp.dest('./dist/edge/scripts')) })
.pipe(gulp.dest('./dist/opera/scripts'))
// finally, trigger live reload return buildStream
.pipe(gulpif(debug, livereload()))
)
} }
} }

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

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

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

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

@ -1,6 +1,6 @@
const injectCss = require('inject-css') const injectCss = require('inject-css')
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 SwStream = require('sw-stream')
const MetaMaskUiCss = require('../../ui/css') const MetaMaskUiCss = require('../../ui/css')
const MetamascaraPlatform = require('../../app/scripts/platforms/window') const MetamascaraPlatform = require('../../app/scripts/platforms/window')
const startPopup = require('../../app/scripts/popup-core') const startPopup = require('../../app/scripts/popup-core')
@ -8,27 +8,44 @@ const startPopup = require('../../app/scripts/popup-core')
// create platform global // create platform global
global.platform = new MetamascaraPlatform() global.platform = new MetamascaraPlatform()
var css = MetaMaskUiCss() var css = MetaMaskUiCss()
injectCss(css) injectCss(css)
const container = document.getElementById('app-content') const container = document.getElementById('app-content')
var name = 'popup' const name = 'popup'
window.METAMASK_UI_TYPE = name window.METAMASK_UI_TYPE = name
window.METAMASK_PLATFORM_TYPE = 'mascara' 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({ const swController = new SwController({
fileName: '/background.js', fileName: './background.js',
letBeIdle: false, keepAlive: true,
intervalDelay, keepAliveDelay,
wakeUpInterval: 20000, 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 // Setup listener for when the service worker is read
const connectApp = function (readSw) { function connectApp() {
const connectionStream = SwStream({ const connectionStream = SwStream({
serviceWorker: background.controller, serviceWorker: swController.getWorker(),
context: name, context: name,
}) })
return new Promise((resolve, reject) => { 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 () { function windowReload () {
if (window.METAMASK_SKIP_RELOAD) return if (window.METAMASK_SKIP_RELOAD) return

@ -7,6 +7,6 @@
</head> </head>
<body> <body>
<div id="app-content"></div> <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> </body>
</html> </html>

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

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

@ -62,8 +62,8 @@ PendingTx.prototype.render = function () {
const gasBn = hexToBn(gas) const gasBn = hexToBn(gas)
// default to 8MM gas limit // default to 8MM gas limit
const gasLimit = new BN(parseInt(blockGasLimit) || '8000000') const gasLimit = new BN(parseInt(blockGasLimit) || '8000000')
const safeGasLimitBN = this.bnMultiplyByFraction(gasLimit, 19, 20) const safeGasLimitBN = this.bnMultiplyByFraction(gasLimit, 99, 100)
const saferGasLimitBN = this.bnMultiplyByFraction(gasLimit, 18, 20) const saferGasLimitBN = this.bnMultiplyByFraction(gasLimit, 98, 100)
const safeGasLimit = safeGasLimitBN.toString(10) const safeGasLimit = safeGasLimitBN.toString(10)
// Gas Price // Gas Price
@ -311,7 +311,7 @@ PendingTx.prototype.render = function () {
style: { style: {
fontSize: '0.9em', 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, : null,
]), ]),

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

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

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

@ -42,7 +42,7 @@ ConfigScreen.prototype.render = function () {
// subtitle and nav // subtitle and nav
h('.section-title.flex-row.flex-center', [ h('.section-title.flex-row.flex-center', [
h('i.fa.fa-arrow-left.fa-lg.cursor-pointer', { h('i.fa.fa-arrow-left.fa-lg.cursor-pointer', {
onClick: (event) => { onClick: () => {
state.dispatch(actions.goHome()) state.dispatch(actions.goHome())
}, },
}), }),
@ -168,7 +168,6 @@ ConfigScreen.prototype.render = function () {
h('a', { h('a', {
href: 'http://metamask.helpscoutdocs.com/article/36-resetting-an-account', href: 'http://metamask.helpscoutdocs.com/article/36-resetting-an-account',
target: '_blank', target: '_blank',
onClick (event) { this.navigateTo(event.target.href) },
}, 'Read more.'), }, 'Read more.'),
]), ]),
h('br'), h('br'),
@ -260,7 +259,3 @@ function currentProviderDisplay (metamaskState) {
h('span', value), 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) window.navigator.msSaveBlob(blob, filename)
} else { } else {
const elem = window.document.createElement('a') const elem = window.document.createElement('a')
elem.target = '_blank'
elem.href = window.URL.createObjectURL(blob) elem.href = window.URL.createObjectURL(blob)
elem.download = filename elem.download = filename
document.body.appendChild(elem) document.body.appendChild(elem)

@ -11,7 +11,7 @@ var cssFiles = {
'transitions.css': fs.readFileSync(path.join(__dirname, '/app/css/transitions.css'), 'utf8'), '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'), '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-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 () { function bundleCss () {
@ -19,7 +19,7 @@ function bundleCss () {
var fileContent = cssFiles[fileName] var fileContent = cssFiles[fileName]
var output = String() var output = String()
output += '/*========== ' + fileName + ' ==========*/\n\n' output += '/ *========== ' + fileName + ' ========== * /\n\n'
output += fileContent output += fileContent
output += '\n\n' output += '\n\n'

16266
package-lock.json generated

File diff suppressed because it is too large Load Diff

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

@ -19,11 +19,13 @@ module.exports = function(config) {
'test/integration/jquery-3.1.0.min.js', 'test/integration/jquery-3.1.0.min.js',
{ pattern: 'dist/chrome/images/**/*.*', watched: false, included: false, served: true }, { pattern: 'dist/chrome/images/**/*.*', watched: false, included: false, served: true },
{ pattern: 'dist/chrome/fonts/**/*.*', 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: { proxies: {
'/images/': '/base/dist/chrome/images/', '/images/': '/base/dist/chrome/images/',
'/fonts/': '/base/dist/chrome/fonts/', '/fonts/': '/base/dist/chrome/fonts/',
'/_locales/': '/base/dist/chrome/_locales/',
}, },
// test results reporter to use // 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 // Confirm Add token
assert.equal( assert.equal(
$('.add-token__description')[0].textContent, $('.add-token__description')[0].textContent,
'Would you like to add these tokens?', 'Token balance(s)',
'confirm add token rendered' 'confirm add token rendered'
) )
assert.ok($('button.btn-primary--lg')[0], 'confirm add token button found') 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 assert = require('assert')
const clone = require('clone') const clone = require('clone')
const Migrator = require('../../app/scripts/lib/migrator/') const Migrator = require('../../app/scripts/lib/migrator/')
const migrations = [ const liveMigrations = require('../../app/scripts/migrations/')
const stubMigrations = [
{ {
version: 1, version: 1,
migrate: (data) => { migrate: (data) => {
@ -29,13 +30,39 @@ const migrations = [
}, },
] ]
const versionedData = {meta: {version: 0}, data: {hello: 'world'}} const versionedData = {meta: {version: 0}, data: {hello: 'world'}}
const firstTimeState = {
meta: { version: 0 },
data: require('../../app/scripts/first-time-state'),
}
describe('Migrator', () => { describe('Migrator', () => {
const migrator = new Migrator({ migrations }) const migrator = new Migrator({ migrations: stubMigrations })
it('migratedData version should be version 3', (done) => { it('migratedData version should be version 3', (done) => {
migrator.migrateData(versionedData) migrator.migrateData(versionedData)
.then((migratedData) => { .then((migratedData) => {
assert.equal(migratedData.meta.version, migrations[2].version) assert.equal(migratedData.meta.version, stubMigrations[2].version)
done() done()
}).catch(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