Merge branch 'master' into testing

feature/default_network_editable
Thomas 7 years ago
commit f82c51c2c4
  1. 108
      .circleci/config.yml
  2. 4
      .eslintrc
  3. 36
      CHANGELOG.md
  4. 2
      app/_locales/de/messages.json
  5. 5
      app/_locales/en/messages.json
  6. 2
      app/_locales/es/messages.json
  7. 2
      app/_locales/hn/messages.json
  8. 1
      app/_locales/index.json
  9. 99
      app/_locales/ja/messages.json
  10. 2
      app/_locales/nl/messages.json
  11. 424
      app/_locales/ru/messages.json
  12. 2
      app/_locales/sl/messages.json
  13. 2
      app/_locales/th/messages.json
  14. 912
      app/_locales/tr/messages.json
  15. 2
      app/_locales/zh_TW/messages.json
  16. 4
      app/manifest.json
  17. 58
      app/scripts/background.js
  18. 2
      app/scripts/config.js
  19. 6
      app/scripts/contentscript.js
  20. 5
      app/scripts/controllers/blacklist.js
  21. 23
      app/scripts/controllers/currency.js
  22. 18
      app/scripts/controllers/infura.js
  23. 15
      app/scripts/controllers/shapeshift.js
  24. 61
      app/scripts/controllers/transactions.js
  25. 2
      app/scripts/first-time-state.js
  26. 2
      app/scripts/inpage.js
  27. 6
      app/scripts/lib/get-first-preferred-lang-code.js
  28. 33
      app/scripts/lib/getObjStructure.js
  29. 3
      app/scripts/lib/is-popup-or-notification.js
  30. 27
      app/scripts/lib/migrator/index.js
  31. 2
      app/scripts/lib/setupRaven.js
  32. 34
      app/scripts/lib/tx-gas-utils.js
  33. 22
      app/scripts/lib/tx-state-manager.js
  34. 7
      app/scripts/migrations/013.js
  35. 15
      app/scripts/migrations/015.js
  36. 22
      app/scripts/migrations/016.js
  37. 21
      app/scripts/migrations/017.js
  38. 39
      app/scripts/migrations/018.js
  39. 44
      app/scripts/migrations/019.js
  40. 17
      app/scripts/migrations/022.js
  41. 38
      app/scripts/migrations/023.js
  42. 41
      app/scripts/migrations/024.js
  43. 61
      app/scripts/migrations/025.js
  44. 2
      app/scripts/migrations/index.js
  45. 29
      app/scripts/migrations/template.js
  46. 62
      development/metamaskbot-build-announce.js
  47. 21
      development/mock-dev.js
  48. 55
      development/sentry-publish.js
  49. 0
      development/states/tx-list-items.json
  50. 133
      development/verify-locale-strings.js
  51. 48
      docs/QA_Guide.md
  52. 9
      docs/translating-guide.md
  53. 51
      gulpfile.js
  54. 151
      mascara/src/app/first-time/confirm-seed-screen.js
  55. 279
      mascara/src/app/first-time/create-password-screen.js
  56. 17
      mascara/src/app/first-time/import-seed-phrase-screen.js
  57. 187
      mascara/src/app/first-time/index.js
  58. 105
      mascara/src/app/first-time/notice-screen.js
  59. 147
      mascara/src/app/first-time/seed-screen.js
  60. 18
      mascara/src/app/first-time/unique-image-screen.js
  61. 1
      old-ui/app/app.js
  62. 1
      old-ui/app/components/buy-button-subview.js
  63. 1
      old-ui/app/components/qr-code.js
  64. 4
      old-ui/app/components/range-slider.js
  65. 2
      old-ui/app/components/transaction-list-item.js
  66. 2
      old-ui/app/config.js
  67. 1
      old-ui/app/util.js
  68. 3603
      package-lock.json
  69. 36
      package.json
  70. 6
      test/e2e/metamask.spec.js
  71. 2
      test/integration/lib/add-token.js
  72. 18
      test/integration/lib/confirm-sig-requests.js
  73. 3
      test/integration/lib/mascara-first-time.js
  74. 30
      test/integration/lib/send-new-ui.js
  75. 18
      test/screens/func.js
  76. 230
      test/screens/new-ui.js
  77. 49
      test/unit/migrations/024-test.js
  78. 49
      test/unit/migrations/025-test.js
  79. 17
      test/unit/migrations/template-test.js
  80. 33
      test/unit/migrator-test.js
  81. 96
      test/unit/tx-controller-test.js
  82. 18
      test/unit/tx-gas-util-test.js
  83. 187
      ui/app/actions.js
  84. 930
      ui/app/app.js
  85. 54
      ui/app/components/account-menu/index.js
  86. 8
      ui/app/components/dropdowns/network-dropdown.js
  87. 4
      ui/app/components/ens-input.js
  88. 44
      ui/app/components/pages/add-token.js
  89. 34
      ui/app/components/pages/authenticated.js
  90. 0
      ui/app/components/pages/create-account/import-account/index.js
  91. 16
      ui/app/components/pages/create-account/import-account/json.js
  92. 24
      ui/app/components/pages/create-account/import-account/private-key.js
  93. 0
      ui/app/components/pages/create-account/import-account/seed.js
  94. 81
      ui/app/components/pages/create-account/index.js
  95. 30
      ui/app/components/pages/create-account/new-account.js
  96. 332
      ui/app/components/pages/home.js
  97. 25
      ui/app/components/pages/initialized.js
  98. 177
      ui/app/components/pages/keychains/restore-vault.js
  99. 195
      ui/app/components/pages/keychains/reveal-seed.js
  100. 28
      ui/app/components/pages/metamask-route.js
  101. Some files were not shown because too many files have changed in this diff Show More

@ -17,8 +17,17 @@ workflows:
- prep-deps-npm - prep-deps-npm
- test-e2e: - test-e2e:
requires: requires:
- prep-deps-npm
- prep-build
- job-screens:
requires:
- prep-deps-npm
- prep-build - prep-build
- job-publish:
requires:
- prep-deps-npm - prep-deps-npm
- prep-build
- job-screens
- test-unit: - test-unit:
requires: requires:
- prep-deps-npm - prep-deps-npm
@ -45,6 +54,7 @@ workflows:
- test-lint - test-lint
- test-unit - test-unit
- test-e2e - test-e2e
- job-screens
- test-integration-mascara-chrome - test-integration-mascara-chrome
- test-integration-mascara-firefox - test-integration-mascara-firefox
- test-integration-flat-chrome - test-integration-flat-chrome
@ -57,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
@ -65,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:
@ -87,7 +101,7 @@ jobs:
steps: steps:
- checkout - checkout
- restore_cache: - restore_cache:
key: dependency-cache-{{ checksum "package-lock.json" }} key: dependency-cache-{{ .Revision }}
- run: - run:
name: build:dist name: build:dist
command: npm run dist command: npm run dist
@ -98,32 +112,7 @@ jobs:
key: build-cache-{{ .Revision }} key: build-cache-{{ .Revision }}
paths: paths:
- dist - dist
- store_artifacts: - builds
path: dist/mascara
destination: builds/mascara
- store_artifacts:
path: builds
destination: builds
- run:
name: build:announce
command: |
CIRCLE_PR_NUMBER="${CIRCLE_PR_NUMBER:-${CIRCLE_PULL_REQUEST##*/}}"
SHORT_SHA1=$(echo $CIRCLE_SHA1 | cut -c 1-7)
BUILD_LINK_BASE="https://$CIRCLE_BUILD_NUM-42009758-gh.circle-artifacts.com/0/builds"
VERSION=$(node -p 'require("./dist/chrome/manifest.json").version')
MASCARA="$BUILD_LINK_BASE/mascara/home.html"
CHROME="$BUILD_LINK_BASE/metamask-chrome-$VERSION.zip"
FIREFOX="$BUILD_LINK_BASE/metamask-firefox-$VERSION.zip"
OPERA="$BUILD_LINK_BASE/metamask-opera-$VERSION.zip"
EDGE="$BUILD_LINK_BASE/metamask-edge-$VERSION.zip"
COMMENT_MAIN="Builds ready [$SHORT_SHA1]: [mascara][mascara], [chrome][chrome], [firefox][firefox], [edge][edge], [opera][opera]"
COMMENT_LINKS="[mascara]:$MASCARA\n[chrome]:$CHROME\n[firefox]:$FIREFOX\n[opera]:$OPERA\n[edge]:$EDGE\n"
COMMENT_BODY="$COMMENT_MAIN\n\n$COMMENT_LINKS"
JSON_PAYLOAD="{\"body\":\"$COMMENT_BODY\"}"
POST_COMMENT_URI="https://api.github.com/repos/metamask/metamask-extension/issues/$CIRCLE_PR_NUMBER/comments"
echo "Announcement:\n$COMMENT_BODY"
echo "Posting to $POST_COMMENT_URI"
curl -d "$JSON_PAYLOAD" -H "Authorization: token $GITHUB_COMMENT_TOKEN" $POST_COMMENT_URI
prep-scss: prep-scss:
docker: docker:
@ -131,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
@ -150,7 +139,7 @@ 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
@ -161,7 +150,7 @@ jobs:
steps: steps:
- checkout - checkout
- restore_cache: - restore_cache:
key: dependency-cache-{{ checksum "package-lock.json" }} key: dependency-cache-{{ .Revision }}
- restore_cache: - restore_cache:
key: build-cache-{{ .Revision }} key: build-cache-{{ .Revision }}
- run: - run:
@ -171,13 +160,60 @@ jobs:
path: test-artifacts path: test-artifacts
destination: 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
@ -199,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
@ -218,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
@ -246,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
@ -265,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

@ -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,9 +2,43 @@
## Current Master ## Current Master
- Improved performance of 3D fox logo.
## 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. - 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. - Increase maximum gas limit, to allow very gas heavy transactions, since block gas limits have been stable.
- Allow user to select language on settings page
## 4.4.0 Mon Mar 26 2018 ## 4.4.0 Mon Mar 26 2018

@ -232,7 +232,7 @@
"done": { "done": {
"message": "Fertig" "message": "Fertig"
}, },
"downloadStatelogs": { "downloadStateLogs": {
"message": "Statelogs herunterladen" "message": "Statelogs herunterladen"
}, },
"dropped": { "dropped": {

@ -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"
@ -826,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": {

@ -13,6 +13,7 @@
{ "code": "ru", "name": "Russian" }, { "code": "ru", "name": "Russian" },
{ "code": "sl", "name": "Slovenian" }, { "code": "sl", "name": "Slovenian" },
{ "code": "th", "name": "Thai" }, { "code": "th", "name": "Thai" },
{ "code": "tr", "name": "Turkish" },
{ "code": "vi", "name": "Vietnamese" }, { "code": "vi", "name": "Vietnamese" },
{ "code": "zh_CN", "name": "Mandarin" }, { "code": "zh_CN", "name": "Mandarin" },
{ "code": "zh_TW", "name": "Taiwanese" } { "code": "zh_TW", "name": "Taiwanese" }

@ -20,6 +20,9 @@
"addToken": { "addToken": {
"message": "トークンを追加" "message": "トークンを追加"
}, },
"addTokens": {
"message": "トークンを追加"
},
"amount": { "amount": {
"message": "金額" "message": "金額"
}, },
@ -46,6 +49,9 @@
"balance": { "balance": {
"message": "残高:" "message": "残高:"
}, },
"balances": {
"message": "トークン残高"
},
"balanceIsInsufficientGas": { "balanceIsInsufficientGas": {
"message": "現在のガス総量に対して残高が不足しています" "message": "現在のガス総量に対して残高が不足しています"
}, },
@ -63,10 +69,10 @@
"message": "購入" "message": "購入"
}, },
"buyCoinbase": { "buyCoinbase": {
"message": "Coinbaseで購入" "message": "Coinbaseのサイトで購入"
}, },
"buyCoinbaseExplainer": { "buyCoinbaseExplainer": {
"message": "Coinbaseは、世界的なBitcoin、Ethereum、そしてLitecoinの取引所です。" "message": "Etherを購入できます。Coinbaseは、世界的なBitcoin、Ethereum、そしてLitecoinの取引所です。"
}, },
"cancel": { "cancel": {
"message": "キャンセル" "message": "キャンセル"
@ -90,7 +96,7 @@
"message": "トランザクションの確認" "message": "トランザクションの確認"
}, },
"continueToCoinbase": { "continueToCoinbase": {
"message": "Coinbaseで続行" "message": "Coinbaseを開く"
}, },
"contractDeployment": { "contractDeployment": {
"message": "コントラクトのデプロイ" "message": "コントラクトのデプロイ"
@ -138,6 +144,9 @@
"customGas": { "customGas": {
"message": "ガスのカスタマイズ" "message": "ガスのカスタマイズ"
}, },
"customToken": {
"message": "カスタムトークン"
},
"customize": { "customize": {
"message": "カスタマイズ" "message": "カスタマイズ"
}, },
@ -154,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": "法定通貨でデポジット"
@ -176,10 +185,10 @@
"message": "別のアカウントから入金" "message": "別のアカウントから入金"
}, },
"depositShapeShift": { "depositShapeShift": {
"message": "ShapeShiftで入金" "message": "ShapeShiftで交換"
}, },
"depositShapeShiftExplainer": { "depositShapeShiftExplainer": {
"message": "他の暗号通貨をEtherと交換してMetaMaskのウォレットへ入金できます。アカウント作成は不要です。" "message": "他の暗号通貨とEtherを交換して、MetaMaskのウォレットへ入金できます。アカウント作成は不要です。"
}, },
"details": { "details": {
"message": "詳細" "message": "詳細"
@ -188,10 +197,10 @@
"message": "ダイレクトデポジット" "message": "ダイレクトデポジット"
}, },
"directDepositEther": { "directDepositEther": {
"message": "Etherを直接受け取り" "message": "Etherを直接入金"
}, },
"directDepositEtherExplainer": { "directDepositEtherExplainer": {
"message": "Etherをすでにお持ちなら、MetaMaskの新しいウォレットにEtherを送信することができます。" "message": "既にEtherをお持ちなら、MetaMaskの新しいウォレットにEtherを送信することができます。"
}, },
"done": { "done": {
"message": "完了" "message": "完了"
@ -209,7 +218,7 @@
"message": "パスワードを入力" "message": "パスワードを入力"
}, },
"etherscanView": { "etherscanView": {
"message": "Etherscanでアカウントを参照" "message": "Etherscanでアカウントを確認"
}, },
"exchangeRate": { "exchangeRate": {
"message": "交換レート" "message": "交換レート"
@ -266,7 +275,7 @@
"message": "必要ガスプライス" "message": "必要ガスプライス"
}, },
"getEther": { "getEther": {
"message": "Etherをゲット" "message": "Etherを取得する"
}, },
"getEtherFromFaucet": { "getEtherFromFaucet": {
"message": "フォーセットで $1のEtherを得ることができます。", "message": "フォーセットで $1のEtherを得ることができます。",
@ -287,7 +296,7 @@
"message": "トークンを隠す" "message": "トークンを隠す"
}, },
"hideTokenPrompt": { "hideTokenPrompt": {
"message": "トークンを隠しますか?" "message": "トークンを隠しますか?"
}, },
"howToDeposit": { "howToDeposit": {
"message": "どのようにEtherをデポジットしますか?" "message": "どのようにEtherをデポジットしますか?"
@ -300,7 +309,7 @@
"message": "アカウントのインポート" "message": "アカウントのインポート"
}, },
"importAccountMsg": { "importAccountMsg": {
"message":"追加したアカウントはMetaMaskのアカウントシードフレーズとは関連付けられません。インポートしたアカウントについての詳細は" "message":"追加したアカウントはMetaMaskのアカウントパスフレーズとは関連付けられません。インポートしたアカウントについての詳細は"
}, },
"importAnAccount": { "importAnAccount": {
"message": "アカウントをインポート" "message": "アカウントをインポート"
@ -334,13 +343,22 @@
"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テストネットワーク"
}, },
"learnMore": {
"message": "詳細"
},
"lessThanMax": { "lessThanMax": {
"message": " $1以下にして下さい。", "message": " $1以下にして下さい。",
"description": "helper for inputting hex as decimal input" "description": "helper for inputting hex as decimal input"
}, },
"likeToAddTokens": {
"message": "トークンを追加しますか?"
},
"limit": { "limit": {
"message": "リミット" "message": "リミット"
}, },
@ -371,8 +389,11 @@
"myAccounts": { "myAccounts": {
"message": "マイアカウント" "message": "マイアカウント"
}, },
"mustSelectOne": {
"message": "一つ以上のトークンを選択してください。"
},
"needEtherInWallet": { "needEtherInWallet": {
"message": "MetaMaskを使って分散型アプリケーションを使用するためには、このウォレットにEtherが必要です。" "message": "MetaMask分散型アプリケーションを使用するためには、このウォレットにEtherが必要です。"
}, },
"needImportFile": { "needImportFile": {
"message": "インポートするファイルを選択してください。", "message": "インポートするファイルを選択してください。",
@ -411,7 +432,7 @@
"message": "この名前にはアドレスが設定されていません。" "message": "この名前にはアドレスが設定されていません。"
}, },
"noDeposits": { "noDeposits": {
"message": "デポジットがありません。" "message": "振込みがありません。"
}, },
"noTransactionHistory": { "noTransactionHistory": {
"message": "トランザクション履歴がありません。" "message": "トランザクション履歴がありません。"
@ -445,10 +466,13 @@
"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": "秘密鍵",
@ -470,13 +494,13 @@
"message": "もっと読む" "message": "もっと読む"
}, },
"receive": { "receive": {
"message": "受" "message": "受取"
}, },
"recipientAddress": { "recipientAddress": {
"message": "受取人アドレス" "message": "受取人アドレス"
}, },
"refundAddress": { "refundAddress": {
"message": "あなたの返金先アドレス" "message": "受取アドレス"
}, },
"rejected": { "rejected": {
"message": "拒否されました" "message": "拒否されました"
@ -487,12 +511,18 @@
"restoreFromSeed": { "restoreFromSeed": {
"message": "パスフレーズから復元する" "message": "パスフレーズから復元する"
}, },
"restoreVault": {
"message": "ウォレットを復元する"
},
"required": { "required": {
"message": "必要です。" "message": "必要です。"
}, },
"retryWithMoreGas": { "retryWithMoreGas": {
"message": "より高いガスプライスで再度試して下さい。" "message": "より高いガスプライスで再度試して下さい。"
}, },
"walletSeed": {
"message": "ウォレットのパスフレーズ"
},
"revealSeedWords": { "revealSeedWords": {
"message": "パスフレーズを表示" "message": "パスフレーズを表示"
}, },
@ -519,23 +549,35 @@
"selectService": { "selectService": {
"message": "サービスを選択" "message": "サービスを選択"
}, },
"selectType": {
"message": "キーの種類"
},
"send": { "send": {
"message": "送信" "message": "送信"
}, },
"sendETH": {
"message": "ETHの送信"
},
"sendTokens": { "sendTokens": {
"message": "トークンを送る" "message": "トークンを送る"
}, },
"onlySendToEtherAddress": { "onlySendToEtherAddress": {
"message": "ETHはイーサリウムアカウントのみに送信できます。" "message": "ETHはイーサリウムアカウントのみに送信できます。"
}, },
"searchTokens": {
"message": "トークンの検索"
},
"sendTokensAnywhere": { "sendTokensAnywhere": {
"message": "イーサリアムアカウントを持っている人にトークンを送る" "message": "イーサリアムアカウントを持っている人にトークンを送る"
}, },
"settings": { "settings": {
"message": "設定" "message": "設定"
}, },
"info": {
"message": "情報"
},
"shapeshiftBuy": { "shapeshiftBuy": {
"message": "Shapeshiftで買う" "message": "Shapeshiftで交換"
}, },
"showPrivateKeys": { "showPrivateKeys": {
"message": "秘密鍵を表示" "message": "秘密鍵を表示"
@ -565,13 +607,13 @@
"message": "送信" "message": "送信"
}, },
"takesTooLong": { "takesTooLong": {
"message": "長くかかりすぎていますか?" "message": "送信に時間がかかりますか?"
}, },
"testFaucet": { "testFaucet": {
"message": "Faucetをテスト" "message": "Faucetをテスト"
}, },
"to": { "to": {
"message": "先" "message": "送信先"
}, },
"toETHviaShapeShift": { "toETHviaShapeShift": {
"message": "ShapeShiftで $1をETHにする", "message": "ShapeShiftで $1をETHにする",
@ -595,6 +637,9 @@
"total": { "total": {
"message": "合計" "message": "合計"
}, },
"transactions": {
"message": "トランザクション"
},
"transactionMemo": { "transactionMemo": {
"message": "トランザクションメモ (オプション)" "message": "トランザクションメモ (オプション)"
}, },
@ -609,7 +654,7 @@
"description": "Followed by a link (here) to view token balances" "description": "Followed by a link (here) to view token balances"
}, },
"typePassword": { "typePassword": {
"message": "パスワードタイプ" "message": "パスワードの入力"
}, },
"uiWelcome": { "uiWelcome": {
"message": "新UIへようこそ!(ベータ版)" "message": "新UIへようこそ!(ベータ版)"
@ -646,7 +691,7 @@
"message": "警告" "message": "警告"
}, },
"whatsThis": { "whatsThis": {
"message": "これは何でしょう?" "message": "この機能について"
}, },
"yourSigRequested": { "yourSigRequested": {
"message": "あなたの署名がリクエストされています。" "message": "あなたの署名がリクエストされています。"

@ -223,7 +223,7 @@
"done": { "done": {
"message": "Gedaan" "message": "Gedaan"
}, },
"downloadStatelogs": { "downloadStateLogs": {
"message": "Staatslogboeken downloaden" "message": "Staatslogboeken downloaden"
}, },
"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": {

@ -0,0 +1,912 @@
{
"accept": {
"message": "Kabul et"
},
"account": {
"message": "Hesap"
},
"accountDetails": {
"message": "Hesap Detayları"
},
"accountName": {
"message": "Hesap İsmi"
},
"address": {
"message": "Adres"
},
"addCustomToken": {
"message": "Özel jeton ekle"
},
"addToken": {
"message": "Jeton ekle"
},
"addTokens": {
"message": "Jetonlar ekle"
},
"amount": {
"message": "Tutar"
},
"amountPlusGas": {
"message": "Tutar + Gas"
},
"appDescription": {
"message": "Ethereum Tarayıcı Uzantısı",
"description": "Uygulama açıklaması"
},
"appName": {
"message": "MetaMask",
"description": "Uygulama ismi"
},
"approved": {
"message": "Onaylandı"
},
"attemptingConnect": {
"message": "Blockchain'e bağlanmayı deniyor"
},
"attributions": {
"message": "Atıflar"
},
"available": {
"message": "Müsait"
},
"back": {
"message": "Geri"
},
"balance": {
"message": "Bakiye:"
},
"balances": {
"message": "Jeton bakiyesi"
},
"balanceIsInsufficientGas": {
"message": "Toplam gas için yetersiz bakiye"
},
"beta": {
"message": "BETA"
},
"betweenMinAndMax": {
"message": "$1'e eşit veya daha büyük olmalı ve $2'den küçük veya eşit olmalı",
"description": "Onaltılık sayının ondalık sayı olarak girişi için yardımcı"
},
"blockiesIdenticon": {
"message": "Blockies Identicon kullan"
},
"borrowDharma": {
"message": "Dharma (Beta) ile ödünç al"
},
"builtInCalifornia": {
"message": "MetaMask California'da tasarlandı ve yaratıldı"
},
"buy": {
"message": "Satın al"
},
"buyCoinbase": {
"message": "Coinbase'de satın al"
},
"buyCoinbaseExplainer": {
"message": "Coinbase bitcoin, ethereum, and litecoin alıp satmanın dünyadaki en popüler yolu"
},
"ok": {
"message": "Tamam"
},
"cancel": {
"message": "Vazgeç"
},
"classicInterface": {
"message": "Klasik arayüzü kullan"
},
"clickCopy": {
"message": "Kopyalamak için tıkla"
},
"confirm": {
"message": "Onayla"
},
"confirmed": {
"message": "Onaylandı"
},
"confirmContract": {
"message": "Sözleşmeyi onayla"
},
"confirmPassword": {
"message": "Şifreyi onayla"
},
"confirmTransaction": {
"message": "İşlemi onayla"
},
"continue": {
"message": "Devam et"
},
"continueToCoinbase": {
"message": "Coinbase'e devam et"
},
"contractDeployment": {
"message": "Sözleşme kurulumu"
},
"conversionProgress": {
"message": "Çevirim devam ediyor"
},
"copiedButton": {
"message": "Kopyalandı"
},
"copiedClipboard": {
"message": "Panoya kopyalandı"
},
"copiedExclamation": {
"message": "Kopyalandı!"
},
"copiedSafe": {
"message": "Güvenli bir yere kopyaladım"
},
"copy": {
"message": "Kopyala"
},
"copyToClipboard": {
"message": "Panoya kopyala"
},
"copyButton": {
"message": " Kopyala "
},
"copyPrivateKey": {
"message": "Bu sizin özel anahtarınız (kopyalamak için tıklayın)"
},
"create": {
"message": "Yarat"
},
"createAccount": {
"message": "Hesap Oluştur"
},
"createDen": {
"message": "Yarat"
},
"crypto": {
"message": "Kripto",
"description": "Kambiyo tipi (kripto para)"
},
"currentConversion": {
"message": "Geçerli çevirme"
},
"currentNetwork": {
"message": "Geçerli Ağ"
},
"customGas": {
"message": "Gas'i özelleştir"
},
"customToken": {
"message": "Özel Jeton"
},
"customize": {
"message": "Özelleştir"
},
"customRPC": {
"message": "Özel RPC"
},
"decimalsMustZerotoTen": {
"message": "Ondalıklar en azından 0 olmalı ve 36'dan büyük olmamalı."
},
"decimal": {
"message": "Ondalık hassasiyeti"
},
"defaultNetwork": {
"message": "Ether işlemleri için varsayılan ağ Main Net."
},
"denExplainer": {
"message": "DEN'iniz MetaMask içersinde parola-şifrelenmiş deponuzdur."
},
"deposit": {
"message": "Yatır"
},
"depositBTC": {
"message": "BTC'inizi aşağıdaki adrese yatırın:"
},
"depositCoin": {
"message": "$1'nızı aşağıdaki adrese yatırın",
"description": "Kullanıcıya hangi jetonu seçtiyse onu yatırmasını shapeshift ile söyler."
},
"depositEth": {
"message": "Eth yatır"
},
"depositEther": {
"message": "Ether yatır"
},
"depositFiat": {
"message": "Para yatır"
},
"depositFromAccount": {
"message": "Başka bir hesaptan yatır"
},
"depositShapeShift": {
"message": "ShapeShift ile yatır"
},
"depositShapeShiftExplainer": {
"message": "Eğer başka kripto paralara sahipseniz, MetaMask cüzdanınıza direk olarak Ether yatırabilirsiniz. Hesaba gerek yoktur."
},
"details": {
"message": "Ayrıntılar"
},
"directDeposit": {
"message": "Direk Yatırma"
},
"directDepositEther": {
"message": "Direk Ether Yatırma"
},
"directDepositEtherExplainer": {
"message": "Eğer çoktan Etheriniz varsa, yeni hesabınıza Ether aktarmanın en kolay yolu direk yatırmadır."
},
"done": {
"message": "Bitti"
},
"downloadStateLogs": {
"message": "Durum kayıtlarını indir"
},
"dropped": {
"message": "Bırakıldı"
},
"edit": {
"message": "Düzenle"
},
"editAccountName": {
"message": "Hesap ismini düzenle"
},
"emailUs": {
"message": "Bize e-posta atın!"
},
"encryptNewDen": {
"message": "Yeni DEN'inizi şifreleyin"
},
"enterPassword": {
"message": "Parolanızı girin"
},
"enterPasswordConfirm": {
"message": "Onaylamak için parolanızı girin"
},
"passwordNotLongEnough": {
"message": "Parola yeterince uzun değil"
},
"passwordsDontMatch": {
"message": "Parolalar eşleşmiyor"
},
"etherscanView": {
"message": "Hesabı Etherscan üzerinde izle"
},
"exchangeRate": {
"message": "Döviz kuru"
},
"exportPrivateKey": {
"message": "Özel anahtarı ver"
},
"exportPrivateKeyWarning": {
"message": "Özel anahtarınızı vermek sizin sorumluluğunuzdadır."
},
"failed": {
"message": "Başarısız oldu"
},
"fiat": {
"message": "Para",
"description": "Döviz türü"
},
"fileImportFail": {
"message": "Dosya alma çalışmıyor mu? Buraya tıklayın!",
"description": "Kullanıcıların hesaplarını JSON dosyasından almalarına yardım eder"
},
"followTwitter": {
"message": "Bizi twitter'da takip edin"
},
"from": {
"message": "Kimden"
},
"fromToSame": {
"message": "Kimden ve kime adresi aynı olamaz"
},
"fromShapeShift": {
"message": "ShapeShift'den"
},
"gas": {
"message": "Gas",
"description": "Gas maliyetinin kısa indikatörü"
},
"gasFee": {
"message": "Gas Ücreti"
},
"gasLimit": {
"message": "Gas Limiti"
},
"gasLimitCalculation": {
"message": "Önerilen gas limitini ağ başarı oranını baz alarak hesaplıyoruz."
},
"gasLimitRequired": {
"message": "Gas limiti gereklidir"
},
"gasLimitTooLow": {
"message": "Gas limiti en az 21000 olmalıdır"
},
"generatingSeed": {
"message": "Kaynak Oluşturuyor..."
},
"gasPrice": {
"message": "Gas Fiyatı (GWEI)"
},
"gasPriceCalculation": {
"message": "Önerilen gas fiyatını ağ başarı oranını baz alarak hesaplıyoruz."
},
"gasPriceRequired": {
"message": "Gas Fiyatı Gereklidir"
},
"getEther": {
"message": "Ether Al"
},
"getEtherFromFaucet": {
"message": "Musluktan $1 karşılığı Ether alın",
"description": "Ether musluğunun ağ ismini gösterir"
},
"greaterThanMin": {
"message": "must be greater than or equal to $1.",
"description": "helper for inputting hex as decimal input"
},
"here": {
"message": "burada",
"description": "daha fazla bilgi için -buraya tıklayın- (troubleTokenBalances ile gidiyor)"
},
"hereList": {
"message": "İşte bir liste!!!!"
},
"hide": {
"message": "Gizle"
},
"hideToken": {
"message": "Jetonu gizle"
},
"hideTokenPrompt": {
"message": "Jetonu gizle?"
},
"howToDeposit": {
"message": "Ether'i nasıl yatırmak istersiniz?"
},
"holdEther": {
"message": "Ether ve jeton tutmanızı sağlar ve merkezi olmayan uygulamalar ve sizin aranızda köprü vazifesi görür."
},
"import": {
"message": "Al",
"description": "Seçilen dosyadan hesap alma düğmesi. "
},
"importAccount": {
"message": "Hesap Al"
},
"importAccountMsg": {
"message":" Alınan hesaplar orjinal kaynakifadenizle yarattığınız MetaMask hesabınızla ilişkilendirilmez. Alınan hesaplar ile ilgili daha fazla bilgi edinin "
},
"importAnAccount": {
"message": "Hesap al"
},
"importDen": {
"message": "Varolan DEN al"
},
"imported": {
"message": "Alındı",
"description": "Hesabın keyringe başarı ile alındığını gösteren durum"
},
"infoHelp": {
"message": "Bilgi ve yardım"
},
"insufficientFunds": {
"message": "Yetersiz kaynak."
},
"insufficientTokens": {
"message": "Yetersiz Jeton."
},
"invalidAddress": {
"message": "Geçersiz adres"
},
"invalidAddressRecipient": {
"message": "Alıcı adresi geçersiz"
},
"invalidGasParams": {
"message": "Geçersiz gas parametreleri"
},
"invalidInput": {
"message": "Geçersiz giriş."
},
"invalidRequest": {
"message": "Geçersiz istek"
},
"invalidRPC": {
"message": "Geçersiz RPC URI"
},
"jsonFail": {
"message": "Birşeyler yanlış gitti. JSON dosyanızın düzgün derlendiğinden emin olun."
},
"jsonFile": {
"message": "JSON Dosyası",
"description": "Hesap alımı için düzenle"
},
"keepTrackTokens": {
"message": "MetaMask hesabınızla satın aldığınız jetonların kaydını tutun."
},
"kovan": {
"message": "Kovan Test Ağı"
},
"knowledgeDataBase": {
"message": "Bilgi veritabanımızı ziyaret edin"
},
"max": {
"message": "Maksimum"
},
"learnMore": {
"message": "Daha fazla bilgi."
},
"lessThanMax": {
"message": "$1'den az veya eşit olmalıdır.",
"description": "Onaltılık sayıyı ondalık olarak girmek için yardımcı"
},
"likeToAddTokens": {
"message": "Bu jetonlara adres eklemek ister misiniz?"
},
"links": {
"message": "Bağlantılar"
},
"limit": {
"message": "Limit"
},
"loading": {
"message": "Yükleniyor..."
},
"loadingTokens": {
"message": "Jetonlar yükleniyor..."
},
"localhost": {
"message": "Localhost 8545"
},
"login": {
"message": "Giriş yap"
},
"logout": {
"message": "Çıkış"
},
"loose": {
"message": "Gevşek"
},
"loweCaseWords": {
"message": "kaynak kelimeleri sadece küçük harflerden oluşabilir."
},
"mainnet": {
"message": "Main Ethereum Ağı"
},
"message": {
"message": "Mesaj"
},
"metamaskDescription": {
"message": "MetaMask Ethereum için güvenli bir kimlik kasasıdır."
},
"min": {
"message": "Minimum"
},
"myAccounts": {
"message": "Hesaplarım"
},
"mustSelectOne": {
"message": "En az bir jeton seçilmeli"
},
"needEtherInWallet": {
"message": "MetaMask kullanarak merkezi olamayan uygulamalarla etkileşmek için cüzdanınızda Ether bulunmalıdır."
},
"needImportFile": {
"message": "Almak için bir dosya seçmelisiniz.",
"description": "Kullanıcı bir hesap alır ve devam etmek içinbir dosya seçmelidir."
},
"needImportPassword": {
"message": "Seçilen dosya için bir parola girmelisiniz.",
"description": "Hesap almak için parola ve dosya gerekiyor."
},
"negativeETH": {
"message": "Negatif ETH miktarları gönderilemez."
},
"networks": {
"message": "Ağlar"
},
"newAccount": {
"message": "Yeni Hesap"
},
"newAccountNumberName": {
"message": "Hesap $1",
"description": "Hesap yaratma ekranındaki bir sonraki hesabın varsayılan ismi"
},
"newContract": {
"message": "Yeni Sözleşme"
},
"newPassword": {
"message": "Yeni Parola (min 8 karakter)"
},
"newRecipient": {
"message": "Yeni alıcı"
},
"newRPC": {
"message": "Yeni RPC URL"
},
"next": {
"message": "Sonraki"
},
"noAddressForName": {
"message": "Bu isim için bir adres tanımlanmamış."
},
"noDeposits": {
"message": "Yatırma alınmadı"
},
"noTransactionHistory": {
"message": "İşlem geçmişi yok."
},
"noTransactions": {
"message": "İşlem yok"
},
"notStarted": {
"message": "Başlamadı"
},
"oldUI": {
"message": "Eski UI"
},
"oldUIMessage": {
"message": "Eski UI'a döndünüz. Yeni UI'a üst sağ sekme menüsündeki seçenek ile dönebilirsiniz."
},
"or": {
"message": "veya",
"description": "Yeni bir hesap yaratmak veya almak arasındaki seçim"
},
"passwordCorrect": {
"message": "Lütfen parolanın doğru olduğuna emin olun."
},
"passwordMismatch": {
"message": "parolalar eşleşmiyor",
"description": "parola yaratma işleminde, iki yeni parola alanı eşleşmiyor."
},
"passwordShort": {
"message": "parola yeterince uzun değil",
"description": "parola yaratma işleminde, parola güvenli olacak kadar uzun değil."
},
"pastePrivateKey": {
"message": "Özel anahtar dizinizi buraya yapıştırın:",
"description": "Özel anahtardan hesap almak için"
},
"pasteSeed": {
"message": "Kaynak ifadenizi buraya yapıştırın!"
},
"personalAddressDetected": {
"message": "Kişisel adres tespit edilidi. Jeton sözleşme adresini girin."
},
"pleaseReviewTransaction": {
"message": "Lütfen işleminizi gözden geçirin."
},
"popularTokens": {
"message": "Popüler Jetonlar"
},
"privacyMsg": {
"message": "Gizlilik Şartları"
},
"privateKey": {
"message": "Özel Anahtar",
"description": "Hesap alırken bu tip bir dosya seçin"
},
"privateKeyWarning": {
"message": "Uyarı: Bu anahtarı kimse ile paylaşmayın. Özel anahtarlarınıza sahip herkes hesaplarınızıdaki tüm varlığınızı çalabilir."
},
"privateNetwork": {
"message": "Özel Ağ"
},
"qrCode": {
"message": "QR Kodunu göster"
},
"readdToken": {
"message": "Gelecekte Bu jetonu hesap seçenekleri menüsünde “Jeton ekle”'ye giderek geri ekleyebilirsiniz."
},
"readMore": {
"message": "Burada daha fazla okuyun."
},
"readMore2": {
"message": "Daha fazla okuyun."
},
"receive": {
"message": "Al"
},
"recipientAddress": {
"message": "Alıcı adresi"
},
"refundAddress": {
"message": "İade adresiniz"
},
"rejected": {
"message": "Rededildi"
},
"resetAccount": {
"message": "Hesabı sıfıla"
},
"restoreFromSeed": {
"message": "Kaynak ifadeden geri getir. Restore from seed phrase"
},
"restoreVault": {
"message": "Kasayı geri getir"
},
"required": {
"message": "Gerekli"
},
"retryWithMoreGas": {
"message": "Daha yüksek bir gas fiyatı ile tekrar dene"
},
"walletSeed": {
"message": "Cüzdan Kaynağı"
},
"revealSeedWords": {
"message": "Kaynak kelimelerini göster"
},
"revealSeedWordsWarning": {
"message": "Açık bir yerde kaynak kelimeliriniz geri getirmeyin! Bu kelimeler tüm hesaplarınızı çalmak için kullanılabilir."
},
"revert": {
"message": "Geri döndür"
},
"rinkeby": {
"message": "Rinkeby Test Ağı"
},
"ropsten": {
"message": "Ropsten Test Ağı"
},
"currentRpc": {
"message": "Geçerli RPC"
},
"connectingToMainnet": {
"message": "Main Ethereum Ağına bağlanıyor"
},
"connectingToRopsten": {
"message": "Ropsten Test Ağına bağlanıyor"
},
"connectingToKovan": {
"message": "Kovan Test Ağına bağlanıyor"
},
"connectingToRinkeby": {
"message": "Rinkeby Test Ağına bağlanıyor"
},
"connectingToUnknown": {
"message": "Bilinmeyen Ağa bağlanıyor"
},
"sampleAccountName": {
"message": "E.g. Yeni hesabım",
"description": "Kullanıcının hesabına okunabilir isim ekleme konseptini anlamasına yardımcı olmak."
},
"save": {
"message": "Kaydet"
},
"reprice_title": {
"message": "İşlemi Yeniden Fiyatlandır"
},
"reprice_subtitle": {
"message": "İşlemizi hızlandırmayı denemek için gas fiyatınızı yükseltin."
},
"saveAsFile": {
"message": "Dosya olarak kaydet",
"description": "Hesap verme işlemi"
},
"saveSeedAsFile": {
"message": "Kaynak Kelimelerini Dosya olarak Kaydet"
},
"search": {
"message": "Ara"
},
"secretPhrase": {
"message": "Kasanızı geri getirmek için gizli 12 kelimelik ifadenizi giriniz."
},
"newPassword8Chars": {
"message": "Yeni Parola (minumum 8 karakter)"
},
"seedPhraseReq": {
"message": "Kaynak ifadeleri 12 kelimedir."
},
"select": {
"message": "Seç"
},
"selectCurrency": {
"message": "Döviz Seç"
},
"selectService": {
"message": "Servis Seç"
},
"selectType": {
"message": "Tip Seç"
},
"send": {
"message": "Gönder"
},
"sendETH": {
"message": "ETH Gönder"
},
"sendTokens": {
"message": "Jeton Gönder"
},
"onlySendToEtherAddress": {
"message": "Ethereum adresine sadece ETH gönder."
},
"searchTokens": {
"message": "Jeton ara"
},
"sendTokensAnywhere": {
"message": "Ethereum hesabı olan birine Jeton gönder"
},
"settings": {
"message": "Ayarlar"
},
"info": {
"message": "Bilgi"
},
"shapeshiftBuy": {
"message": "Shapeshift ile satın al"
},
"showPrivateKeys": {
"message": "Özel anahtarları göster"
},
"showQRCode": {
"message": "QR Kodu göster"
},
"sign": {
"message": "İmza"
},
"signed": {
"message": "İmzalandı"
},
"signMessage": {
"message": "Mesajı İmzala"
},
"signNotice": {
"message": "Bu mesajı imzalamanın tehlikeli \nyan etkileri olabilir. Tamamen güvendiğiniz sitelerden \ngelen mesajları hesabınızla imzalayınız.\n Bu tehlikeli metod gelecek versiyonlarda çıkarılacaktır. "
},
"sigRequest": {
"message": "İmza isteği"
},
"sigRequested": {
"message": "İmza isteniyor"
},
"spaceBetween": {
"message": "Kelimeler arası sadece bir boşluk olabilir."
},
"status": {
"message": "Durum"
},
"stateLogs": {
"message": "Durum Kayıtları"
},
"stateLogsDescription": {
"message": "Durum kayıtları açık hesap adresinizi ve gönderilen işlemleri içerir."
},
"stateLogError": {
"message": "Durum kayıtlarını alma hatası"
},
"submit": {
"message": "Gönder"
},
"submitted": {
"message": "Gönderildi"
},
"supportCenter": {
"message": "Destek merkezimizi ziyaret edin"
},
"symbolBetweenZeroTen": {
"message": "Sembol 0 ve 10 karakter aralığında olmalıdır."
},
"takesTooLong": {
"message": "Çok mu uzun sürüyor?"
},
"terms": {
"message": "Kullanım şartları"
},
"testFaucet": {
"message": "Test Musluğu"
},
"to": {
"message": "Kime: "
},
"toETHviaShapeShift": {
"message": "ShapeShift üstünden $1'dan ETH'e",
"description": "system will fill in deposit type in start of message"
},
"tokenAddress": {
"message": "Jeton Adresi"
},
"tokenAlreadyAdded": {
"message": "Jeton çoktan eklenmiş."
},
"tokenBalance": {
"message": "Jeton bakiyeniz:"
},
"tokenSelection": {
"message": "Jeton arayın veya popüler jeton listemizden seçin."
},
"tokenSymbol": {
"message": "Jeton Sembolü"
},
"tokenWarning1": {
"message": "MetaMask hesabınızla aldığınız jetonların kaydını tutun. Başka bir hesapla jetonlar satın aldıysanız, o jetonlar burada gözükmeyecektir."
},
"total": {
"message": "Toplam"
},
"transactions": {
"message": "işlemler"
},
"transactionError": {
"message": "İşlem Hatası. Sözleşme kodundan kural dışı durum fırlatıldı."
},
"transactionMemo": {
"message": "İşlem notu (opsiyonel)"
},
"transactionNumber": {
"message": "İşlem numarası"
},
"transfers": {
"message": "Transferler"
},
"troubleTokenBalances": {
"message": "Jeton bakiyelerinizi yüklerken sorun yaşadık. Buradan izleyebilirsiniz ",
"description": "Jeton bakiyelerini görmek için bir link (burası) ile takip ediliyor"
},
"twelveWords": {
"message": "MetaMask hesaplarınızı geri getirmenin tek yolu bu 12 kelimedir.\nBu kelimeleri güvenli ve gizli bir yerde saklayın."
},
"typePassword": {
"message": "Parolanızı girin"
},
"uiWelcome": {
"message": "Yeni UI (Beta)'ya hoşgeldiniz"
},
"uiWelcomeMessage": {
"message": "Şu anda yeni Metamask UI kullanmaktasınız. Gözatın, jeton gönderme gibi yeni özellikleri deneyin ve herhangi bir sorunlar karşılaşırsanız bize haber verin"
},
"unapproved": {
"message": "Onaylanmadı"
},
"unavailable": {
"message": "Mevcut değil"
},
"unknown": {
"message": "Bilinmeyen"
},
"unknownNetwork": {
"message": "Bilinmeyen özel ağ"
},
"unknownNetworkId": {
"message": "Bilinmeyen ağ IDsi"
},
"uriErrorMsg": {
"message": "URIler için HTTP/HTTPS öneki gerekmektedir."
},
"usaOnly": {
"message": "Sadece ABD",
"description": "Bu dövizi sadece ABD ikamet edenler kullanabilir"
},
"usedByClients": {
"message": "Farklı istemciler tarafından kullanılmakta"
},
"useOldUI": {
"message": "Eski UI kullan"
},
"validFileImport": {
"message": "Almak için geçerli bir dosya seçmelisiniz"
},
"vaultCreated": {
"message": "Kasa Yaratıldı"
},
"viewAccount": {
"message": "Hesabı İncele"
},
"visitWebSite": {
"message": "Web sitemizi ziyaret edin"
},
"warning": {
"message": "Uyarı"
},
"welcomeBeta": {
"message": "MetaMask Beta'ya Hoşgeldiniz"
},
"whatsThis": {
"message": "Bu nedir?"
},
"yourSigRequested": {
"message": "İmzanız isteniyor"
},
"youSign": {
"message": "İmzalıyorsunuz"
}
}

@ -235,7 +235,7 @@
"done": { "done": {
"message": "完成" "message": "完成"
}, },
"downloadStatelogs": { "downloadStateLogs": {
"message": "下載狀態紀錄" "message": "下載狀態紀錄"
}, },
"dropped": { "dropped": {

@ -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__",
@ -69,4 +69,4 @@
"https://metamask.io/*" "https://metamask.io/*"
] ]
} }
} }

@ -20,9 +20,10 @@ 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 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')
@ -77,6 +78,38 @@ async function loadStateFromPersistence () {
diskStore.getState() || diskStore.getState() ||
migrator.generateInitialState(firstTimeState) migrator.generateInitialState(firstTimeState)
// check if somehow state is empty
// this should never happen but new error reporting suggests that it has
// for a small number of users
// https://github.com/metamask/metamask-extension/issues/3919
if (versionedData && !versionedData.data) {
// try to recover from diskStore incase only localStore is bad
const diskStoreState = diskStore.getState()
if (diskStoreState && diskStoreState.data) {
// we were able to recover (though it might be old)
versionedData = diskStoreState
const vaultStructure = getObjStructure(versionedData)
raven.captureMessage('MetaMask - Empty vault found - recovered from diskStore', {
// "extra" key is required by Sentry
extra: { vaultStructure },
})
} else {
// unable to recover, clear state
versionedData = migrator.generateInitialState(firstTimeState)
raven.captureMessage('MetaMask - Empty vault found - unable to recover')
}
}
// 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) {
@ -84,7 +117,14 @@ 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
@ -94,7 +134,7 @@ function setupController (initState, initLangCode) {
// //
// MetaMask Controller // MetaMask Controller
// //
const controller = new MetamaskController({ const controller = new MetamaskController({
// User confirmation callbacks: // User confirmation callbacks:
showUnconfirmedMessage: triggerUi, showUnconfirmedMessage: triggerUi,
@ -122,9 +162,9 @@ function setupController (initState, initLangCode) {
asStream(controller.store), asStream(controller.store),
debounce(1000), debounce(1000),
storeTransform(versionifyData), storeTransform(versionifyData),
storeTransform(syncDataWithExtension), storeTransform(persistData),
(error) => { (error) => {
log.error('pump hit error', error) log.error('MetaMask - Persistence pipeline failed', error)
} }
) )
@ -133,7 +173,13 @@ function setupController (initState, initLangCode) {
return versionedData return versionedData
} }
function syncDataWithExtension(state) { function persistData(state) {
if (!state) {
throw new Error('MetaMask - updated state is missing', state)
}
if (!state.data) {
throw new Error('MetaMask - updated state does not have data', state)
}
if (localStore.isSupported) { if (localStore.isSupported) {
localStore.set(state) localStore.set(state)
.catch((err) => { .catch((err) => {

@ -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: {

@ -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:`, currentCurrency, err) 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)
} }
} }

@ -45,18 +45,19 @@ class ShapeshiftController {
}) })
} }
updateTx (tx) { async updateTx (tx) {
const url = `https://shapeshift.io/txStat/${tx.depositAddress}` try {
return fetch(url) const url = `https://shapeshift.io/txStat/${tx.depositAddress}`
.then((response) => { const response = await fetch(url)
return response.json() const json = await 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) {

@ -185,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)
@ -313,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')

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

@ -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
}

@ -3,7 +3,8 @@ module.exports = function isPopupOrNotification () {
// if (url.match(/popup.html$/) || url.match(/home.html$/)) { // if (url.match(/popup.html$/) || url.match(/home.html$/)) {
// Below regexes needed for feature toggles (e.g. see line ~340 in ui/app/app.js) // Below regexes needed for feature toggles (e.g. see line ~340 in ui/app/app.js)
// Revert below regexes to above commented out regexes before merge to master // Revert below regexes to above commented out regexes before merge to master
if (url.match(/popup.html(?:\?.+)*$/) || url.match(/home.html(?:\?.+)*$/)) { if (url.match(/popup.html(?:\?.+)*$/) ||
url.match(/home.html(?:\?.+)*$/) || url.match(/home.html(?:#.*)*$/)) {
return 'popup' return 'popup'
} else { } else {
return 'notification' return 'notification'

@ -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

@ -1,5 +1,5 @@
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 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'

@ -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
}
}

@ -92,8 +92,10 @@ module.exports = class TransactionStateManager extends EventEmitter {
// or rejected tx's. // or rejected tx's.
// not tx's that are pending or unapproved // not tx's that are pending or unapproved
if (txCount > txHistoryLimit - 1) { if (txCount > txHistoryLimit - 1) {
const index = transactions.findIndex((metaTx) => metaTx.status === 'confirmed' || metaTx.status === 'rejected') let index = transactions.findIndex((metaTx) => metaTx.status === 'confirmed' || metaTx.status === 'rejected')
transactions.splice(index, 1) if (index !== -1) {
transactions.splice(index, 1)
}
} }
transactions.push(txMeta) transactions.push(txMeta)
this._saveTxList(transactions) this._saveTxList(transactions)
@ -108,6 +110,10 @@ module.exports = class TransactionStateManager extends EventEmitter {
updateTx (txMeta, note) { updateTx (txMeta, note) {
// validate txParams // validate txParams
if (txMeta.txParams) { if (txMeta.txParams) {
if (typeof txMeta.txParams.data === 'undefined') {
delete txMeta.txParams.data
}
this.validateTxParams(txMeta.txParams) this.validateTxParams(txMeta.txParams)
} }
@ -140,8 +146,16 @@ module.exports = class TransactionStateManager extends EventEmitter {
validateTxParams(txParams) { validateTxParams(txParams) {
Object.keys(txParams).forEach((key) => { Object.keys(txParams).forEach((key) => {
const value = txParams[key] const value = txParams[key]
if (typeof value !== 'string') throw new Error(`${key}: ${value} in txParams is not a string`) // validate types
if (!ethUtil.isHexPrefixed(value)) throw new Error('is not hex prefixed, everything on txParams must be hex prefixed') 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
}
}) })
} }

@ -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
newState.config.provider.type = 'ropsten' if ( config && config.provider ) {
if (config.provider.type === 'testnet') {
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
newState.TransactionController.transactions = transactions.map((txMeta) => { if (TransactionController && TransactionController.transactions) {
if (!txMeta.err) return txMeta const transactions = TransactionController.transactions
else if (txMeta.err.message === 'Gave up submitting tx.') txMeta.status = 'failed' newState.TransactionController.transactions = transactions.map((txMeta) => {
return txMeta if (!txMeta.err) return txMeta
}) else if (txMeta.err.message === 'Gave up submitting tx.') txMeta.status = 'failed'
return txMeta
})
}
return newState return newState
} }

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

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

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

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

@ -28,12 +28,15 @@ 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) {
newState.TransactionController.transactions = transactions.map((txMeta) => { const transactions = newState.TransactionController.transactions
if (txMeta.status !== 'submitted' || txMeta.submittedTime) return txMeta
txMeta.submittedTime = (new Date()).getTime() newState.TransactionController.transactions = transactions.map((txMeta) => {
return txMeta if (txMeta.status !== 'submitted' || txMeta.submittedTime) return txMeta
}) txMeta.submittedTime = (new Date()).getTime()
return txMeta
})
}
return newState return newState
} }

@ -28,23 +28,27 @@ module.exports = {
function transformState (state) { function transformState (state) {
const newState = state const newState = state
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() 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 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
}

@ -34,4 +34,6 @@ module.exports = [
require('./021'), require('./021'),
require('./022'), require('./022'),
require('./023'), 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
}

@ -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}`,
},
})
}

@ -36,15 +36,28 @@ log.setLevel('debug')
// //
const qs = require('qs') const qs = require('qs')
let queryString = qs.parse(window.location.href.split('#')[1]) const routerPath = window.location.href.split('#')[1]
let selectedView = queryString.view || 'first time' let queryString = {}
let selectedView
if (routerPath) {
queryString = qs.parse(routerPath.split('?')[1])
}
selectedView = queryString.view || 'first time'
const firstState = states[selectedView] const firstState = states[selectedView]
updateQueryParams(selectedView) updateQueryParams(selectedView)
function updateQueryParams(newView) { function updateQueryParams (newView) {
queryString.view = newView queryString.view = newView
const params = qs.stringify(queryString) const params = qs.stringify(queryString)
window.location.href = window.location.href.split('#')[0] + `#${params}` const locationPaths = window.location.href.split('#')
const routerPath = locationPaths[1] || ''
const newPath = locationPaths[0] + '#' + routerPath.split('?')[0] + `?${params}`
if (window.location.href !== newPath) {
window.location.href = newPath
}
} }
// //

@ -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
}
}

@ -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') {
console.log('Locale file not found') try {
} else { const localeFilePath = path.join(process.cwd(), 'app', '_locales', localeCode, 'messages.json')
console.log('Error opening your locale file: ', e) targetLocale = JSON.parse(fs.readFileSync(localeFilePath, 'utf8'));
} catch (e) {
if (e.code == 'ENOENT') {
console.log('Locale file not found')
} else {
console.log(`Error opening your locale ("${localeCode}") file: `, e)
}
process.exit(1)
} }
process.exit(1)
}
englishFilePath = path.join(process.cwd(), 'app', '_locales', 'en', 'messages.json') try {
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') // console.log(' verifying whether all your locale ("${localeCode}") strings are contained in the english one')
const extraItems = compareLocalesForMissingItems({ base: targetLocale, subject: englishLocale })
// console.log('\n verifying whether your locale ("${localeCode}") contains all english strings')
const missingItems = compareLocalesForMissingItems({ base: englishLocale, subject: targetLocale })
var counter = 0 const englishEntryCount = Object.keys(englishLocale).length
var foundErrorA = false const coveragePercent = 100 * (englishEntryCount - missingItems.length) / englishEntryCount
var notFound = [];
Object.keys(localeObj).forEach(function(key){
if (!englishObj[key]) {
foundErrorA = true
notFound.push(key)
}
counter++
})
if (foundErrorA) { console.log(`Status of **${localeName} (${localeCode})** ${coveragePercent.toFixed(2)}% coverage:`)
console.log('\nThe following string(s) is(are) not found in the english locale:')
notFound.forEach(function(key) {
console.log(key)
})
} else {
console.log('\tall ' + counter +' strings declared in your locale were found in the english one')
}
console.log('\n\tverifying whether your locale contains all english strings') if (extraItems.length) {
console.log('\nMissing from english locale:')
extraItems.forEach(function(key) {
console.log(` - [ ] ${key}`)
})
} else {
// console.log(` all ${counter} strings declared in your locale ("${localeCode}") were found in the english one`)
}
var counter = 0 if (missingItems.length) {
var foundErrorB = false console.log(`\nMissing:`)
var notFound = []; missingItems.forEach(function(key) {
Object.keys(englishObj).forEach(function(key){ console.log(` - [ ] ${key}`)
if (!localeObj[key]) { })
foundErrorB = true } else {
notFound.push(key) // console.log(` all ${counter} english strings were found in your locale ("${localeCode}")!`)
} }
counter++
})
if (foundErrorB) { if (!extraItems.length && !missingItems.length) {
console.log('\nThe following string(s) is(are) not found in the your locale:') console.log('Full coverage : )')
notFound.forEach(function(key) { }
console.log(key)
})
} else {
console.log('\tall ' + counter +' english strings were found in your locale!')
} }
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,5 +1,6 @@
const watchify = require('watchify') const watchify = require('watchify')
const browserify = require('browserify') const browserify = require('browserify')
const envify = require('envify/custom')
const disc = require('disc') const disc = require('disc')
const gulp = require('gulp') const gulp = require('gulp')
const source = require('vinyl-source-stream') const source = require('vinyl-source-stream')
@ -28,8 +29,14 @@ const uglify = require('gulp-uglify-es').default
const babel = require('gulp-babel') const babel = require('gulp-babel')
const debug = require('gulp-debug') const debug = require('gulp-debug')
const pify = require('pify') const pify = require('pify')
const gulpMultiProcess = require('gulp-multi-process')
const endOfStream = pify(require('end-of-stream')) const endOfStream = pify(require('end-of-stream'))
function gulpParallel (...args) {
return function spawnGulpChildProcess(cb) {
return gulpMultiProcess(args, cb, true)
}
}
const browserPlatforms = [ const browserPlatforms = [
'firefox', 'firefox',
@ -207,9 +214,11 @@ gulp.task('dev:copy',
// 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).
@ -220,7 +229,7 @@ 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())
@ -369,12 +378,6 @@ 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 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.task('dev',
@ -418,7 +421,7 @@ gulp.task('build',
gulp.series( gulp.series(
'clean', 'clean',
'build:scss', 'build:scss',
gulp.parallel( gulpParallel(
'build:extension:js', 'build:extension:js',
'build:mascara:js', 'build:mascara:js',
'copy' 'copy'
@ -450,7 +453,6 @@ gulp.task('build:mascara',
gulp.task('dist', gulp.task('dist',
gulp.series( gulp.series(
'apply-prod-environment',
'build', 'build',
'zip' 'zip'
) )
@ -476,6 +478,22 @@ function generateBundler(opts, performBundle) {
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
@ -539,12 +557,9 @@ function bundleTask(opts) {
buildStream = buildStream 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\'', opts.devMode))
// buffer file contents (?) // buffer file contents (?)
.pipe(buffer()) .pipe(buffer())
// Initialize Source Maps // Initialize Source Maps
if (opts.buildSourceMaps) { if (opts.buildSourceMaps) {
buildStream = buildStream buildStream = buildStream
@ -552,16 +567,6 @@ function bundleTask(opts) {
.pipe(sourcemaps.init({ loadMaps: true })) .pipe(sourcemaps.init({ loadMaps: true }))
} }
// Minification
if (opts.minifyBuild) {
buildStream = buildStream
.pipe(uglify({
mangle: {
reserved: [ 'MetamaskInpageProvider' ]
},
}))
}
// Finalize Source Maps (writes .map file) // Finalize Source Maps (writes .map file)
if (opts.buildSourceMaps) { if (opts.buildSourceMaps) {
buildStream = buildStream buildStream = buildStream

@ -0,0 +1,151 @@
import React, { Component } from 'react'
import PropTypes from 'prop-types'
import { connect } from 'react-redux'
import { withRouter } from 'react-router-dom'
import classnames from 'classnames'
import shuffle from 'lodash.shuffle'
import { compose } from 'recompose'
import Identicon from '../../../../ui/app/components/identicon'
import { confirmSeedWords, showModal } from '../../../../ui/app/actions'
import Breadcrumbs from './breadcrumbs'
import LoadingScreen from './loading-screen'
import { DEFAULT_ROUTE } from '../../../../ui/app/routes'
class ConfirmSeedScreen extends Component {
static propTypes = {
isLoading: PropTypes.bool,
address: PropTypes.string,
seedWords: PropTypes.string,
confirmSeedWords: PropTypes.func,
history: PropTypes.object,
openBuyEtherModal: PropTypes.func,
};
static defaultProps = {
seedWords: '',
}
constructor (props) {
super(props)
const { seedWords } = props
this.state = {
selectedSeeds: [],
shuffledSeeds: seedWords && shuffle(seedWords.split(' ')) || [],
}
}
componentWillMount () {
const { seedWords, history } = this.props
if (!seedWords) {
history.push(DEFAULT_ROUTE)
}
}
handleClick () {
const { confirmSeedWords, history, openBuyEtherModal } = this.props
confirmSeedWords()
.then(() => {
history.push(DEFAULT_ROUTE)
openBuyEtherModal()
})
}
render () {
const { seedWords } = this.props
const { selectedSeeds, shuffledSeeds } = this.state
const isValid = seedWords === selectedSeeds.map(([_, seed]) => seed).join(' ')
return (
<div className="first-time-flow">
{
this.props.isLoading
? <LoadingScreen loadingMessage="Creating your new account" />
: (
<div className="first-view-main-wrapper">
<div className="first-view-main">
<div className="backup-phrase">
<Identicon address={this.props.address} diameter={70} />
<div className="backup-phrase__content-wrapper">
<div>
<div className="backup-phrase__title">
Confirm your Secret Backup Phrase
</div>
<div className="backup-phrase__body-text">
Please select each phrase in order to make sure it is correct.
</div>
<div className="backup-phrase__confirm-secret">
{selectedSeeds.map(([_, word], i) => (
<button
key={i}
className="backup-phrase__confirm-seed-option"
>
{word}
</button>
))}
</div>
<div className="backup-phrase__confirm-seed-options">
{shuffledSeeds.map((word, i) => {
const isSelected = selectedSeeds
.filter(([index, seed]) => seed === word && index === i)
.length
return (
<button
key={i}
className={classnames('backup-phrase__confirm-seed-option', {
'backup-phrase__confirm-seed-option--selected': isSelected,
})}
onClick={() => {
if (!isSelected) {
this.setState({
selectedSeeds: [...selectedSeeds, [i, word]],
})
} else {
this.setState({
selectedSeeds: selectedSeeds
.filter(([index, seed]) => !(seed === word && index === i)),
})
}
}}
>
{word}
</button>
)
})}
</div>
<button
className="first-time-flow__button"
onClick={() => isValid && this.handleClick()}
disabled={!isValid}
>
Confirm
</button>
</div>
</div>
<Breadcrumbs total={3} currentIndex={1} />
</div>
</div>
</div>
)
}
</div>
)
}
}
export default compose(
withRouter,
connect(
({ metamask: { selectedAddress, seedWords }, appState: { isLoading } }) => ({
seedWords,
isLoading,
address: selectedAddress,
}),
dispatch => ({
confirmSeedWords: () => dispatch(confirmSeedWords()),
openBuyEtherModal: () => dispatch(showModal({ name: 'DEPOSIT_ETHER'})),
})
)
)(ConfirmSeedScreen)

@ -1,20 +1,26 @@
import EventEmitter from 'events'
import React, { Component } from 'react' import React, { Component } from 'react'
import PropTypes from 'prop-types' import PropTypes from 'prop-types'
import {connect} from 'react-redux' import {connect} from 'react-redux'
import classnames from 'classnames' import { withRouter } from 'react-router-dom'
import {createNewVaultAndKeychain} from '../../../../ui/app/actions' import { compose } from 'recompose'
import LoadingScreen from './loading-screen' import { createNewVaultAndKeychain } from '../../../../ui/app/actions'
import Breadcrumbs from './breadcrumbs' import Breadcrumbs from './breadcrumbs'
import EventEmitter from 'events'
import Mascot from '../../../../ui/app/components/mascot' import Mascot from '../../../../ui/app/components/mascot'
import classnames from 'classnames'
import {
INITIALIZE_UNIQUE_IMAGE_ROUTE,
INITIALIZE_IMPORT_WITH_SEED_PHRASE_ROUTE,
INITIALIZE_NOTICE_ROUTE,
} from '../../../../ui/app/routes'
class CreatePasswordScreen extends Component { class CreatePasswordScreen extends Component {
static propTypes = { static propTypes = {
isLoading: PropTypes.bool.isRequired, isLoading: PropTypes.bool.isRequired,
createAccount: PropTypes.func.isRequired, createAccount: PropTypes.func.isRequired,
goToImportWithSeedPhrase: PropTypes.func.isRequired, history: PropTypes.object.isRequired,
goToImportAccount: PropTypes.func.isRequired, isInitialized: PropTypes.bool,
next: PropTypes.func.isRequired, isUnlocked: PropTypes.bool,
isMascara: PropTypes.bool.isRequired, isMascara: PropTypes.bool.isRequired,
} }
@ -23,13 +29,21 @@ class CreatePasswordScreen extends Component {
confirmPassword: '', confirmPassword: '',
} }
constructor () { constructor (props) {
super() super(props)
this.animationEventEmitter = new EventEmitter() this.animationEventEmitter = new EventEmitter()
} }
componentWillMount () {
const { isInitialized, history } = this.props
if (isInitialized) {
history.push(INITIALIZE_NOTICE_ROUTE)
}
}
isValid () { isValid () {
const {password, confirmPassword} = this.state const { password, confirmPassword } = this.state
if (!password || !confirmPassword) { if (!password || !confirmPassword) {
return false return false
@ -47,93 +61,182 @@ class CreatePasswordScreen extends Component {
return return
} }
const {password} = this.state const { password } = this.state
const {createAccount, next} = this.props const { createAccount, history } = this.props
this.setState({ isLoading: true })
createAccount(password) createAccount(password)
.then(next) .then(() => history.push(INITIALIZE_UNIQUE_IMAGE_ROUTE))
}
renderFields () {
const { isMascara, history } = this.props
return (
<div className={classnames({ 'first-view-main-wrapper': !isMascara })}>
<div className={classnames({
'first-view-main': !isMascara,
'first-view-main__mascara': isMascara,
})}>
{isMascara && <div className="mascara-info first-view-phone-invisible">
<Mascot
animationEventEmitter={this.animationEventEmitter}
width="225"
height="225"
/>
<div className="info">
MetaMask is a secure identity vault for Ethereum.
</div>
<div className="info">
It allows you to hold ether & tokens, and interact with decentralized applications.
</div>
</div>}
<div className="create-password">
<div className="create-password__title">
Create Password
</div>
<input
className="first-time-flow__input"
type="password"
placeholder="New Password (min 8 characters)"
onChange={e => this.setState({password: e.target.value})}
/>
<input
className="first-time-flow__input create-password__confirm-input"
type="password"
placeholder="Confirm Password"
onChange={e => this.setState({confirmPassword: e.target.value})}
/>
<button
className="first-time-flow__button"
disabled={!this.isValid()}
onClick={this.createAccount}
>
Create
</button>
<a
href=""
className="first-time-flow__link create-password__import-link"
onClick={e => {
e.preventDefault()
history.push(INITIALIZE_IMPORT_WITH_SEED_PHRASE_ROUTE)
}}
>
Import with seed phrase
</a>
{ /* }
<a
href=""
className="first-time-flow__link create-password__import-link"
onClick={e => {
e.preventDefault()
history.push(INITIALIZE_IMPORT_ACCOUNT_ROUTE)
}}
>
Import an account
</a>
{ */ }
<Breadcrumbs total={3} currentIndex={0} />
</div>
</div>
</div>
)
} }
render () { render () {
const { isLoading, goToImportWithSeedPhrase, isMascara } = this.props const { history, isMascara } = this.props
return isLoading return (
? <LoadingScreen loadingMessage="Creating your new account" /> <div className={classnames({ 'first-view-main-wrapper': !isMascara })}>
: ( <div className={classnames({
<div className={classnames({ 'first-view-main-wrapper': !isMascara })}> 'first-view-main': !isMascara,
<div className={classnames({ 'first-view-main__mascara': isMascara,
'first-view-main': !isMascara, })}>
'first-view-main__mascara': isMascara, {isMascara && <div className="mascara-info first-view-phone-invisible">
})}> <Mascot
{isMascara && <div className="mascara-info first-view-phone-invisible"> animationEventEmitter={this.animationEventEmitter}
<Mascot width="225"
animationEventEmitter={this.animationEventEmitter} height="225"
width="225" />
height="225" <div className="info">
/> MetaMask is a secure identity vault for Ethereum.
<div className="info"> </div>
MetaMask is a secure identity vault for Ethereum. <div className="info">
</div> It allows you to hold ether & tokens, and interact with decentralized applications.
<div className="info">
It allows you to hold ether & tokens, and interact with decentralized applications.
</div>
</div>}
<div className="create-password">
<div className="create-password__title">
Create Password
</div>
<input
className="first-time-flow__input"
type="password"
placeholder="New Password (min 8 characters)"
onChange={e => this.setState({password: e.target.value})}
/>
<input
className="first-time-flow__input create-password__confirm-input"
type="password"
placeholder="Confirm Password"
onChange={e => this.setState({confirmPassword: e.target.value})}
/>
<button
className="first-time-flow__button"
disabled={!this.isValid()}
onClick={this.createAccount}
>
Create
</button>
<a
href=""
className="first-time-flow__link create-password__import-link"
onClick={e => {
e.preventDefault()
goToImportWithSeedPhrase()
}}
>
Import with seed phrase
</a>
{ /* }
<a
href=""
className="first-time-flow__link create-password__import-link"
onClick={e => {
e.preventDefault()
goToImportAccount()
}}
>
Import an account
</a>
{ */ }
<Breadcrumbs total={3} currentIndex={0} />
</div> </div>
</div>}
<div className="create-password">
<div className="create-password__title">
Create Password
</div>
<input
className="first-time-flow__input"
type="password"
placeholder="New Password (min 8 characters)"
onChange={e => this.setState({password: e.target.value})}
/>
<input
className="first-time-flow__input create-password__confirm-input"
type="password"
placeholder="Confirm Password"
onChange={e => this.setState({confirmPassword: e.target.value})}
/>
<button
className="first-time-flow__button"
disabled={!this.isValid()}
onClick={this.createAccount}
>
Create
</button>
<a
href=""
className="first-time-flow__link create-password__import-link"
onClick={e => {
e.preventDefault()
history.push(INITIALIZE_IMPORT_WITH_SEED_PHRASE_ROUTE)
}}
>
Import with seed phrase
</a>
{ /* }
<a
href=""
className="first-time-flow__link create-password__import-link"
onClick={e => {
e.preventDefault()
history.push(INITIALIZE_IMPORT_ACCOUNT_ROUTE)
}}
>
Import an account
</a>
{ */ }
<Breadcrumbs total={3} currentIndex={0} />
</div> </div>
</div> </div>
) </div>
)
}
}
const mapStateToProps = ({ metamask, appState }) => {
const { isInitialized, isUnlocked, isMascara, noActiveNotices } = metamask
const { isLoading } = appState
return {
isLoading,
isInitialized,
isUnlocked,
isMascara,
noActiveNotices,
} }
} }
export default connect( export default compose(
({ appState: { isLoading }, metamask: { isMascara } }) => ({ isLoading, isMascara }), withRouter,
dispatch => ({ connect(
createAccount: password => dispatch(createNewVaultAndKeychain(password)), mapStateToProps,
}) dispatch => ({
createAccount: password => dispatch(createNewVaultAndKeychain(password)),
})
)
)(CreatePasswordScreen) )(CreatePasswordScreen)

@ -8,16 +8,16 @@ import {
displayWarning, displayWarning,
unMarkPasswordForgotten, unMarkPasswordForgotten,
} from '../../../../ui/app/actions' } from '../../../../ui/app/actions'
import { DEFAULT_ROUTE, INITIALIZE_NOTICE_ROUTE } from '../../../../ui/app/routes'
class ImportSeedPhraseScreen extends Component { class ImportSeedPhraseScreen extends Component {
static propTypes = { static propTypes = {
warning: PropTypes.string, warning: PropTypes.string,
back: PropTypes.func.isRequired,
next: PropTypes.func.isRequired,
createNewVaultAndRestore: PropTypes.func.isRequired, createNewVaultAndRestore: PropTypes.func.isRequired,
hideWarning: PropTypes.func.isRequired, hideWarning: PropTypes.func.isRequired,
displayWarning: PropTypes.func, displayWarning: PropTypes.func,
leaveImportSeedScreenState: PropTypes.func, leaveImportSeedScreenState: PropTypes.func,
history: PropTypes.object,
}; };
state = { state = {
@ -64,20 +64,21 @@ class ImportSeedPhraseScreen extends Component {
const { password, seedPhrase } = this.state const { password, seedPhrase } = this.state
const { const {
createNewVaultAndRestore, createNewVaultAndRestore,
next,
displayWarning, displayWarning,
leaveImportSeedScreenState, leaveImportSeedScreenState,
history,
} = this.props } = this.props
leaveImportSeedScreenState() leaveImportSeedScreenState()
createNewVaultAndRestore(password, this.parseSeedPhrase(seedPhrase)) createNewVaultAndRestore(password, this.parseSeedPhrase(seedPhrase))
.then(next) .then(() => history.push(INITIALIZE_NOTICE_ROUTE))
} }
render () { render () {
const { seedPhrase, password, confirmPassword } = this.state const { seedPhrase, password, confirmPassword } = this.state
const { warning } = this.props const { warning, isLoading } = this.props
const importDisabled = warning || !seedPhrase || !password || !confirmPassword const importDisabled = warning || !seedPhrase || !password || !confirmPassword || isLoading
return ( return (
<div className="first-view-main-wrapper"> <div className="first-view-main-wrapper">
<div className="first-view-main"> <div className="first-view-main">
@ -86,7 +87,7 @@ class ImportSeedPhraseScreen extends Component {
className="import-account__back-button" className="import-account__back-button"
onClick={e => { onClick={e => {
e.preventDefault() e.preventDefault()
this.props.back() this.props.history.goBack()
}} }}
href="#" href="#"
> >
@ -152,7 +153,7 @@ class ImportSeedPhraseScreen extends Component {
} }
export default connect( export default connect(
({ appState: { warning } }) => ({ warning }), ({ appState: { warning, isLoading } }) => ({ warning, isLoading }),
dispatch => ({ dispatch => ({
leaveImportSeedScreenState: () => { leaveImportSeedScreenState: () => {
dispatch(unMarkPasswordForgotten()) dispatch(unMarkPasswordForgotten())

@ -1,17 +1,26 @@
import React, { Component } from 'react' import React, { Component } from 'react'
import PropTypes from 'prop-types' import PropTypes from 'prop-types'
import {connect} from 'react-redux' import {connect} from 'react-redux'
import { withRouter, Switch, Route } from 'react-router-dom'
import { compose } from 'recompose'
import CreatePasswordScreen from './create-password-screen' import CreatePasswordScreen from './create-password-screen'
import UniqueImageScreen from './unique-image-screen' import UniqueImageScreen from './unique-image-screen'
import NoticeScreen from './notice-screen' import NoticeScreen from './notice-screen'
import BackupPhraseScreen from './backup-phrase-screen' import BackupPhraseScreen from './seed-screen'
import ImportAccountScreen from './import-account-screen' import ImportAccountScreen from './import-account-screen'
import ImportSeedPhraseScreen from './import-seed-phrase-screen' import ImportSeedPhraseScreen from './import-seed-phrase-screen'
import ConfirmSeed from './confirm-seed-screen'
import { import {
onboardingBuyEthView, INITIALIZE_ROUTE,
unMarkPasswordForgotten, INITIALIZE_IMPORT_ACCOUNT_ROUTE,
showModal, INITIALIZE_UNIQUE_IMAGE_ROUTE,
} from '../../../../ui/app/actions' INITIALIZE_IMPORT_WITH_SEED_PHRASE_ROUTE,
INITIALIZE_NOTICE_ROUTE,
INITIALIZE_BACKUP_PHRASE_ROUTE,
INITIALIZE_CONFIRM_SEED_ROUTE,
INITIALIZE_CREATE_PASSWORD_ROUTE,
} from '../../../../ui/app/routes'
import WelcomeScreen from '../../../../ui/app/welcome-screen'
class FirstTimeFlow extends Component { class FirstTimeFlow extends Component {
@ -20,6 +29,10 @@ 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,
isUnlocked: PropTypes.bool,
history: PropTypes.object,
welcomeScreenSeen: PropTypes.bool,
}; };
static defaultProps = { static defaultProps = {
@ -28,145 +41,53 @@ class FirstTimeFlow extends Component {
noActiveNotices: false, noActiveNotices: false,
}; };
static SCREEN_TYPE = {
CREATE_PASSWORD: 'create_password',
IMPORT_ACCOUNT: 'import_account',
IMPORT_SEED_PHRASE: 'import_seed_phrase',
UNIQUE_IMAGE: 'unique_image',
NOTICE: 'notice',
BACK_UP_PHRASE: 'back_up_phrase',
CONFIRM_BACK_UP_PHRASE: 'confirm_back_up_phrase',
LOADING: 'loading',
};
constructor (props) {
super(props)
this.state = {
screenType: this.getScreenType(),
}
}
setScreenType (screenType) {
this.setState({ screenType })
}
getScreenType () {
const {
isInitialized,
seedWords,
noActiveNotices,
forgottenPassword,
} = this.props
const {SCREEN_TYPE} = FirstTimeFlow
// return SCREEN_TYPE.NOTICE
if (forgottenPassword) {
return SCREEN_TYPE.IMPORT_SEED_PHRASE
}
if (!isInitialized) {
return SCREEN_TYPE.CREATE_PASSWORD
}
if (!noActiveNotices) {
return SCREEN_TYPE.NOTICE
}
if (seedWords) {
return SCREEN_TYPE.BACK_UP_PHRASE
}
};
renderScreen () {
const {SCREEN_TYPE} = FirstTimeFlow
const {
openBuyEtherModal,
address,
restoreCreatePasswordScreen,
forgottenPassword,
leaveImportSeedScreenState,
} = this.props
switch (this.state.screenType) {
case SCREEN_TYPE.CREATE_PASSWORD:
return (
<CreatePasswordScreen
next={() => this.setScreenType(SCREEN_TYPE.UNIQUE_IMAGE)}
goToImportAccount={() => this.setScreenType(SCREEN_TYPE.IMPORT_ACCOUNT)}
goToImportWithSeedPhrase={() => this.setScreenType(SCREEN_TYPE.IMPORT_SEED_PHRASE)}
/>
)
case SCREEN_TYPE.IMPORT_ACCOUNT:
return (
<ImportAccountScreen
back={() => this.setScreenType(SCREEN_TYPE.CREATE_PASSWORD)}
next={() => this.setScreenType(SCREEN_TYPE.NOTICE)}
/>
)
case SCREEN_TYPE.IMPORT_SEED_PHRASE:
return (
<ImportSeedPhraseScreen
back={() => {
leaveImportSeedScreenState()
this.setScreenType(SCREEN_TYPE.CREATE_PASSWORD)
}}
next={() => {
const newScreenType = forgottenPassword ? null : SCREEN_TYPE.NOTICE
this.setScreenType(newScreenType)
}}
/>
)
case SCREEN_TYPE.UNIQUE_IMAGE:
return (
<UniqueImageScreen
next={() => this.setScreenType(SCREEN_TYPE.NOTICE)}
/>
)
case SCREEN_TYPE.NOTICE:
return (
<NoticeScreen
next={() => this.setScreenType(SCREEN_TYPE.BACK_UP_PHRASE)}
/>
)
case SCREEN_TYPE.BACK_UP_PHRASE:
return (
<BackupPhraseScreen
next={() => openBuyEtherModal()}
/>
)
default:
return <noscript />
}
}
render () { render () {
return ( return (
<div className="first-time-flow"> <div className="first-time-flow">
{this.renderScreen()} <Switch>
<Route exact path={INITIALIZE_IMPORT_ACCOUNT_ROUTE} component={ImportAccountScreen} />
<Route
exact
path={INITIALIZE_IMPORT_WITH_SEED_PHRASE_ROUTE}
component={ImportSeedPhraseScreen}
/>
<Route exact path={INITIALIZE_UNIQUE_IMAGE_ROUTE} component={UniqueImageScreen} />
<Route exact path={INITIALIZE_NOTICE_ROUTE} component={NoticeScreen} />
<Route exact path={INITIALIZE_BACKUP_PHRASE_ROUTE} component={BackupPhraseScreen} />
<Route exact path={INITIALIZE_CONFIRM_SEED_ROUTE} component={ConfirmSeed} />
<Route exact path={INITIALIZE_CREATE_PASSWORD_ROUTE} component={CreatePasswordScreen} />
<Route exact path={INITIALIZE_ROUTE} component={WelcomeScreen} />
</Switch>
</div> </div>
) )
} }
} }
export default connect( const mapStateToProps = ({ metamask }) => {
({ const {
metamask: { isInitialized,
isInitialized, seedWords,
seedWords, noActiveNotices,
noActiveNotices, selectedAddress,
selectedAddress, forgottenPassword,
forgottenPassword, isMascara,
} isUnlocked,
}) => ({ welcomeScreenSeen,
} = metamask
return {
isMascara,
isInitialized, isInitialized,
seedWords, seedWords,
noActiveNotices, noActiveNotices,
address: selectedAddress, address: selectedAddress,
forgottenPassword, forgottenPassword,
}), isUnlocked,
dispatch => ({ welcomeScreenSeen,
leaveImportSeedScreenState: () => dispatch(unMarkPasswordForgotten()), }
openBuyEtherModal: () => dispatch(showModal({ name: 'DEPOSIT_ETHER'})), }
})
export default compose(
withRouter,
connect(mapStateToProps)
)(FirstTimeFlow) )(FirstTimeFlow)

@ -1,11 +1,14 @@
import React, { Component } from 'react' import React, { Component } from 'react'
import PropTypes from 'prop-types' import PropTypes from 'prop-types'
import Markdown from 'react-markdown' import Markdown from 'react-markdown'
import {connect} from 'react-redux' import { connect } from 'react-redux'
import { withRouter } from 'react-router-dom'
import { compose } from 'recompose'
import debounce from 'lodash.debounce' import debounce from 'lodash.debounce'
import {markNoticeRead} from '../../../../ui/app/actions' import { markNoticeRead } from '../../../../ui/app/actions'
import Identicon from '../../../../ui/app/components/identicon' import Identicon from '../../../../ui/app/components/identicon'
import Breadcrumbs from './breadcrumbs' import Breadcrumbs from './breadcrumbs'
import { INITIALIZE_BACKUP_PHRASE_ROUTE } from '../../../../ui/app/routes'
import LoadingScreen from './loading-screen' import LoadingScreen from './loading-screen'
class NoticeScreen extends Component { class NoticeScreen extends Component {
@ -16,8 +19,15 @@ class NoticeScreen extends Component {
date: PropTypes.string, date: PropTypes.string,
body: PropTypes.string, body: PropTypes.string,
}), }),
next: PropTypes.func.isRequired, location: PropTypes.shape({
state: PropTypes.shape({
next: PropTypes.func.isRequired,
}),
}),
markNoticeRead: PropTypes.func, markNoticeRead: PropTypes.func,
history: PropTypes.object,
isLoading: PropTypes.bool,
noActiveNotices: PropTypes.bool,
}; };
static defaultProps = { static defaultProps = {
@ -29,17 +39,24 @@ class NoticeScreen extends Component {
} }
componentDidMount () { componentDidMount () {
if (this.props.noActiveNotices) {
this.props.history.push(INITIALIZE_BACKUP_PHRASE_ROUTE)
}
this.onScroll() this.onScroll()
} }
acceptTerms = () => { acceptTerms = () => {
const { markNoticeRead, lastUnreadNotice, next } = this.props const { markNoticeRead, lastUnreadNotice, history } = this.props
const defer = markNoticeRead(lastUnreadNotice) markNoticeRead(lastUnreadNotice)
.then(() => this.setState({ atBottom: false })) .then(hasActiveNotices => {
if (!hasActiveNotices) {
if ((/terms/gi).test(lastUnreadNotice.title)) { history.push(INITIALIZE_BACKUP_PHRASE_ROUTE)
defer.then(next) } else {
} this.setState({ atBottom: false })
this.onScroll()
}
})
} }
onScroll = debounce(() => { onScroll = debounce(() => {
@ -64,27 +81,29 @@ class NoticeScreen extends Component {
isLoading isLoading
? <LoadingScreen /> ? <LoadingScreen />
: ( : (
<div className="first-view-main-wrapper"> <div className="first-time-flow">
<div className="first-view-main"> <div className="first-view-main-wrapper">
<div <div className="first-view-main">
className="tou" <div
onScroll={this.onScroll} className="tou"
> onScroll={this.onScroll}
<Identicon address={address} diameter={70} />
<div className="tou__title">{title}</div>
<Markdown
className="tou__body markdown"
source={body}
skipHtml
/>
<button
className="first-time-flow__button"
onClick={atBottom && this.acceptTerms}
disabled={!atBottom}
> >
Accept <Identicon address={address} diameter={70} />
</button> <div className="tou__title">{title}</div>
<Breadcrumbs total={3} currentIndex={2} /> <Markdown
className="tou__body markdown"
source={body}
skipHtml
/>
<button
className="first-time-flow__button"
onClick={atBottom && this.acceptTerms}
disabled={!atBottom}
>
Accept
</button>
<Breadcrumbs total={3} currentIndex={2} />
</div>
</div> </div>
</div> </div>
</div> </div>
@ -93,12 +112,24 @@ class NoticeScreen extends Component {
} }
} }
export default connect( const mapStateToProps = ({ metamask, appState }) => {
({ metamask: { selectedAddress, lastUnreadNotice }, appState: { isLoading } }) => ({ const { selectedAddress, lastUnreadNotice, noActiveNotices } = metamask
lastUnreadNotice, const { isLoading } = appState
return {
address: selectedAddress, address: selectedAddress,
}), lastUnreadNotice,
dispatch => ({ noActiveNotices,
markNoticeRead: notice => dispatch(markNoticeRead(notice)), isLoading,
}) }
}
export default compose(
withRouter,
connect(
mapStateToProps,
dispatch => ({
markNoticeRead: notice => dispatch(markNoticeRead(notice)),
})
)
)(NoticeScreen) )(NoticeScreen)

@ -1,13 +1,13 @@
import React, { Component } from 'react' import React, { Component } from 'react'
import PropTypes from 'prop-types' import PropTypes from 'prop-types'
import {connect} from 'react-redux' import { connect } from 'react-redux'
import classnames from 'classnames' import classnames from 'classnames'
import shuffle from 'lodash.shuffle' import { withRouter } from 'react-router-dom'
import {compose, onlyUpdateForPropTypes} from 'recompose' import { compose } from 'recompose'
import Identicon from '../../../../ui/app/components/identicon' import Identicon from '../../../../ui/app/components/identicon'
import {confirmSeedWords} from '../../../../ui/app/actions'
import Breadcrumbs from './breadcrumbs' import Breadcrumbs from './breadcrumbs'
import LoadingScreen from './loading-screen' import LoadingScreen from './loading-screen'
import { DEFAULT_ROUTE, INITIALIZE_CONFIRM_SEED_ROUTE } from '../../../../ui/app/routes'
const LockIcon = props => ( const LockIcon = props => (
<svg <svg
@ -36,34 +36,32 @@ const LockIcon = props => (
/> />
</g> </g>
</svg> </svg>
); )
class BackupPhraseScreen extends Component { class BackupPhraseScreen extends Component {
static propTypes = { static propTypes = {
isLoading: PropTypes.bool.isRequired, isLoading: PropTypes.bool.isRequired,
address: PropTypes.string.isRequired, address: PropTypes.string.isRequired,
seedWords: PropTypes.string.isRequired, seedWords: PropTypes.string,
next: PropTypes.func.isRequired, history: PropTypes.object,
confirmSeedWords: PropTypes.func.isRequired,
}; };
static defaultProps = { static defaultProps = {
seedWords: '' seedWords: '',
}; }
static PAGE = {
SECRET: 'secret',
CONFIRM: 'confirm'
};
constructor(props) { constructor (props) {
const {seedWords} = props
super(props) super(props)
this.state = { this.state = {
isShowingSecret: false, isShowingSecret: false,
page: BackupPhraseScreen.PAGE.SECRET, }
selectedSeeds: [], }
shuffledSeeds: seedWords && shuffle(seedWords.split(' ')),
componentWillMount () {
const { seedWords, history } = this.props
if (!seedWords) {
history.push(DEFAULT_ROUTE)
} }
} }
@ -73,7 +71,7 @@ class BackupPhraseScreen extends Component {
return ( return (
<div className="backup-phrase__secret"> <div className="backup-phrase__secret">
<div className={classnames('backup-phrase__secret-words', { <div className={classnames('backup-phrase__secret-words', {
'backup-phrase__secret-words--hidden': !isShowingSecret 'backup-phrase__secret-words--hidden': !isShowingSecret,
})}> })}>
{this.props.seedWords} {this.props.seedWords}
</div> </div>
@ -96,6 +94,7 @@ class BackupPhraseScreen extends Component {
renderSecretScreen () { renderSecretScreen () {
const { isShowingSecret } = this.state const { isShowingSecret } = this.state
const { history } = this.props
return ( return (
<div className="backup-phrase__content-wrapper"> <div className="backup-phrase__content-wrapper">
@ -124,10 +123,7 @@ class BackupPhraseScreen extends Component {
<div className="backup-phrase__next-button"> <div className="backup-phrase__next-button">
<button <button
className="first-time-flow__button" className="first-time-flow__button"
onClick={() => isShowingSecret && this.setState({ onClick={() => isShowingSecret && history.push(INITIALIZE_CONFIRM_SEED_ROUTE)}
isShowingSecret: false,
page: BackupPhraseScreen.PAGE.CONFIRM,
})}
disabled={!isShowingSecret} disabled={!isShowingSecret}
> >
Next Next
@ -138,99 +134,6 @@ class BackupPhraseScreen extends Component {
) )
} }
renderConfirmationScreen() {
const { seedWords, confirmSeedWords, next } = this.props;
const { selectedSeeds, shuffledSeeds } = this.state;
const isValid = seedWords === selectedSeeds.map(([_, seed]) => seed).join(' ')
return (
<div className="backup-phrase__content-wrapper">
<div>
<div className="backup-phrase__title">Confirm your Secret Backup Phrase</div>
<div className="backup-phrase__body-text">
Please select each phrase in order to make sure it is correct.
</div>
<div className="backup-phrase__confirm-secret">
{selectedSeeds.map(([_, word], i) => (
<button
key={i}
className="backup-phrase__confirm-seed-option"
>
{word}
</button>
))}
</div>
<div className="backup-phrase__confirm-seed-options">
{shuffledSeeds.map((word, i) => {
const isSelected = selectedSeeds
.filter(([index, seed]) => seed === word && index === i)
.length
return (
<button
key={i}
className={classnames('backup-phrase__confirm-seed-option', {
'backup-phrase__confirm-seed-option--selected': isSelected
})}
onClick={() => {
if (!isSelected) {
this.setState({
selectedSeeds: [...selectedSeeds, [i, word]]
})
} else {
this.setState({
selectedSeeds: selectedSeeds
.filter(([index, seed]) => !(seed === word && index === i))
})
}
}}
>
{word}
</button>
)
})}
</div>
<button
className="first-time-flow__button"
onClick={() => isValid && confirmSeedWords().then(next)}
disabled={!isValid}
>
Confirm
</button>
</div>
</div>
)
}
renderBack () {
return this.state.page === BackupPhraseScreen.PAGE.CONFIRM
? (
<a
className="backup-phrase__back-button"
onClick={e => {
e.preventDefault()
this.setState({
page: BackupPhraseScreen.PAGE.SECRET
})
}}
href="#"
>
{`< Back`}
</a>
)
: null
}
renderContent () {
switch (this.state.page) {
case BackupPhraseScreen.PAGE.CONFIRM:
return this.renderConfirmationScreen()
case BackupPhraseScreen.PAGE.SECRET:
default:
return this.renderSecretScreen()
}
}
render () { render () {
return this.props.isLoading return this.props.isLoading
? <LoadingScreen loadingMessage="Creating your new account" /> ? <LoadingScreen loadingMessage="Creating your new account" />
@ -238,9 +141,8 @@ class BackupPhraseScreen extends Component {
<div className="first-view-main-wrapper"> <div className="first-view-main-wrapper">
<div className="first-view-main"> <div className="first-view-main">
<div className="backup-phrase"> <div className="backup-phrase">
{this.renderBack()}
<Identicon address={this.props.address} diameter={70} /> <Identicon address={this.props.address} diameter={70} />
{this.renderContent()} {this.renderSecretScreen()}
</div> </div>
</div> </div>
</div> </div>
@ -249,15 +151,12 @@ class BackupPhraseScreen extends Component {
} }
export default compose( export default compose(
onlyUpdateForPropTypes, withRouter,
connect( connect(
({ metamask: { selectedAddress, seedWords }, appState: { isLoading } }) => ({ ({ metamask: { selectedAddress, seedWords }, appState: { isLoading } }) => ({
seedWords, seedWords,
isLoading, isLoading,
address: selectedAddress, address: selectedAddress,
}),
dispatch => ({
confirmSeedWords: () => dispatch(confirmSeedWords()),
}) })
) )
)(BackupPhraseScreen) )(BackupPhraseScreen)

@ -1,13 +1,16 @@
import React, { Component } from 'react' import React, { Component } from 'react'
import PropTypes from 'prop-types' import PropTypes from 'prop-types'
import { withRouter } from 'react-router-dom'
import { compose } from 'recompose'
import {connect} from 'react-redux' import {connect} from 'react-redux'
import Identicon from '../../../../ui/app/components/identicon' import Identicon from '../../../../ui/app/components/identicon'
import Breadcrumbs from './breadcrumbs' import Breadcrumbs from './breadcrumbs'
import { INITIALIZE_NOTICE_ROUTE } from '../../../../ui/app/routes'
class UniqueImageScreen extends Component { class UniqueImageScreen extends Component {
static propTypes = { static propTypes = {
address: PropTypes.string, address: PropTypes.string,
next: PropTypes.func.isRequired, history: PropTypes.object,
} }
render () { render () {
@ -25,7 +28,7 @@ class UniqueImageScreen extends Component {
</div> </div>
<button <button
className="first-time-flow__button" className="first-time-flow__button"
onClick={this.props.next} onClick={() => this.props.history.push(INITIALIZE_NOTICE_ROUTE)}
> >
Next Next
</button> </button>
@ -37,8 +40,11 @@ class UniqueImageScreen extends Component {
} }
} }
export default connect( export default compose(
({ metamask: { selectedAddress } }) => ({ withRouter,
address: selectedAddress, connect(
}) ({ metamask: { selectedAddress } }) => ({
address: selectedAddress,
})
)
)(UniqueImageScreen) )(UniqueImageScreen)

@ -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())
} }
} }

@ -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,7 @@ 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) { if (!txParams) {
return false return false

@ -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())
}, },
}), }),

@ -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)

3603
package-lock.json generated

File diff suppressed because it is too large Load Diff

@ -5,14 +5,17 @@
"private": true, "private": true,
"scripts": { "scripts": {
"start": "gulp dev:extension", "start": "gulp dev:extension",
"mascara": "gulp dev:mascara & cross-env METAMASK_DEBUG=true node ./mascara/example/server", "mascara": "gulp dev:mascara & node ./mascara/example/server",
"dist": "gulp dist", "dist": "gulp dist",
"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": "METAMASK_ENV=test mocha test/e2e/metamask.spec --recursive", "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",
@ -27,13 +30,8 @@
"test:mascara:build:locales": "mkdirp dist/chrome && cp -R app/_locales dist/chrome/_locales", "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 ./", "ui": "npm run test:flat:build:states && beefy development/ui-dev.js:bundle.js --live --open --index=./development/index.html --cwd ./",
@ -57,7 +55,6 @@
} }
], ],
"reactify", "reactify",
"envify",
"brfs" "brfs"
] ]
}, },
@ -103,7 +100,7 @@
"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.3.4",
"ethjs-contract": "^0.1.9", "ethjs-contract": "^0.1.9",
"ethjs-ens": "^2.0.0", "ethjs-ens": "^2.0.0",
"ethjs-query": "^0.3.4", "ethjs-query": "^0.3.4",
@ -117,7 +114,7 @@
"gulp-autoprefixer": "^5.0.0", "gulp-autoprefixer": "^5.0.0",
"gulp-debug": "^3.2.0", "gulp-debug": "^3.2.0",
"gulp-eslint": "^4.0.0", "gulp-eslint": "^4.0.0",
"gulp-sass": "^3.1.0", "gulp-sass": "^4.0.0",
"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",
@ -134,13 +131,12 @@
"lodash.uniqby": "^4.7.0", "lodash.uniqby": "^4.7.0",
"loglevel": "^1.4.1", "loglevel": "^1.4.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", "pify": "^3.0.0",
"ping-pong-stream": "^1.0.0", "ping-pong-stream": "^1.0.0",
@ -149,6 +145,7 @@
"post-message-stream": "^3.0.0", "post-message-stream": "^3.0.0",
"promise-filter": "^1.1.0", "promise-filter": "^1.1.0",
"promise-to-callback": "^1.0.0", "promise-to-callback": "^1.0.0",
"prop-types": "^15.6.1",
"pump": "^3.0.0", "pump": "^3.0.0",
"pumpify": "^1.3.4", "pumpify": "^1.3.4",
"qrcode-npm": "0.0.3", "qrcode-npm": "0.0.3",
@ -160,6 +157,7 @@
"react-hyperscript": "^3.0.0", "react-hyperscript": "^3.0.0",
"react-markdown": "^3.0.0", "react-markdown": "^3.0.0",
"react-redux": "^5.0.5", "react-redux": "^5.0.5",
"react-router-dom": "^4.2.2",
"react-select": "^1.0.0", "react-select": "^1.0.0",
"react-simple-file-input": "^2.0.0", "react-simple-file-input": "^2.0.0",
"react-tippy": "^1.2.2", "react-tippy": "^1.2.2",
@ -214,15 +212,19 @@
"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-json": "^1.2.0",
"eslint-plugin-mocha": "^5.0.0", "eslint-plugin-mocha": "^5.0.0",
"eslint-plugin-react": "^7.4.0", "eslint-plugin-react": "^7.4.0",
"eth-json-rpc-middleware": "^1.2.7", "eth-json-rpc-middleware": "^1.2.7",
"fs-promise": "^2.0.3", "fs-promise": "^2.0.3",
"ganache-cli": "^6.1.0",
"gifencoder": "^1.1.0",
"gulp": "github:gulpjs/gulp#6d71a658c61edb3090221579d8f97dbe086ba2ed", "gulp": "github:gulpjs/gulp#6d71a658c61edb3090221579d8f97dbe086ba2ed",
"gulp-babel": "^7.0.0", "gulp-babel": "^7.0.0",
"gulp-eslint": "^4.0.0", "gulp-eslint": "^4.0.0",
"gulp-json-editor": "^2.2.1", "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.0",
"gulp-stylefmt": "^1.1.0", "gulp-stylefmt": "^1.1.0",
@ -232,6 +234,7 @@
"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.2.0", "jsdom": "^11.2.0",
"jsdom-global": "^3.0.2", "jsdom-global": "^3.0.2",
@ -250,6 +253,7 @@
"node-sass": "^4.7.2", "node-sass": "^4.7.2",
"nyc": "^11.0.3", "nyc": "^11.0.3",
"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",
@ -257,12 +261,14 @@
"react-test-renderer": "^15.6.2", "react-test-renderer": "^15.6.2",
"react-testutils-additions": "^15.2.0", "react-testutils-additions": "^15.2.0",
"redux-test-utils": "^0.2.2", "redux-test-utils": "^0.2.2",
"rimraf": "^2.6.2",
"selenium-webdriver": "^3.5.0", "selenium-webdriver": "^3.5.0",
"shell-parallel": "^1.0.3",
"sinon": "^5.0.0", "sinon": "^5.0.0",
"stylelint-config-standard": "^18.2.0", "stylelint-config-standard": "^18.2.0",
"tape": "^4.5.1", "tape": "^4.5.1",
"testem": "^2.0.0", "testem": "^2.0.0",
"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.9.0"

@ -38,6 +38,8 @@ describe('Metamask popup page', function () {
const tabs = await driver.getAllWindowHandles() const tabs = await driver.getAllWindowHandles()
await driver.switchTo().window(tabs[0]) await driver.switchTo().window(tabs[0])
await delay(300) await delay(300)
await setProviderType('localhost')
await delay(300)
}) })
it('should match title', async () => { it('should match title', async () => {
@ -124,6 +126,10 @@ describe('Metamask popup page', function () {
}) })
}) })
async function setProviderType(type) {
await driver.executeScript('window.metamask.setProviderType(arguments[0])', type)
}
async function verboseReportOnFailure(test) { async function verboseReportOnFailure(test) {
const artifactDir = `./test-artifacts/${test.title}` const artifactDir = `./test-artifacts/${test.title}`
const filepathBase = `${artifactDir}/test-failure` const filepathBase = `${artifactDir}/test-failure`

@ -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')

@ -21,11 +21,22 @@ async function runConfirmSigRequestsTest(assert, done) {
selectState.val('confirm sig requests') selectState.val('confirm sig requests')
reactTriggerChange(selectState[0]) reactTriggerChange(selectState[0])
// await timeout(1000000)
const pendingRequestItem = $.find('.tx-list-item.tx-list-pending-item-container.tx-list-clickable')
if (pendingRequestItem[0]) {
pendingRequestItem[0].click()
}
let confirmSigHeadline = await queryAsync($, '.request-signature__headline') let confirmSigHeadline = await queryAsync($, '.request-signature__headline')
assert.equal(confirmSigHeadline[0].textContent, 'Your signature is being requested') assert.equal(confirmSigHeadline[0].textContent, 'Your signature is being requested')
let confirmSigMessage = await queryAsync($, '.request-signature__notice')
assert.ok(confirmSigMessage[0].textContent.match(/^Signing\sthis\smessage/))
let confirmSigRowValue = await queryAsync($, '.request-signature__row-value') let confirmSigRowValue = await queryAsync($, '.request-signature__row-value')
assert.ok(confirmSigRowValue[0].textContent.match(/^\#\sTerms\sof\sUse/)) assert.equal(confirmSigRowValue[0].textContent, '0x879a053d4800c6354e76c7985a865d2922c82fb5b3f4577b2fe08b998954f2e0')
let confirmSigSignButton = await queryAsync($, 'button.btn-primary--lg') let confirmSigSignButton = await queryAsync($, 'button.btn-primary--lg')
confirmSigSignButton[0].click() confirmSigSignButton[0].click()
@ -33,11 +44,8 @@ async function runConfirmSigRequestsTest(assert, done) {
confirmSigHeadline = await queryAsync($, '.request-signature__headline') confirmSigHeadline = await queryAsync($, '.request-signature__headline')
assert.equal(confirmSigHeadline[0].textContent, 'Your signature is being requested') assert.equal(confirmSigHeadline[0].textContent, 'Your signature is being requested')
let confirmSigMessage = await queryAsync($, '.request-signature__notice')
assert.ok(confirmSigMessage[0].textContent.match(/^Signing\sthis\smessage/))
confirmSigRowValue = await queryAsync($, '.request-signature__row-value') confirmSigRowValue = await queryAsync($, '.request-signature__row-value')
assert.equal(confirmSigRowValue[0].textContent, '0x879a053d4800c6354e76c7985a865d2922c82fb5b3f4577b2fe08b998954f2e0') assert.ok(confirmSigRowValue[0].textContent.match(/^\#\sTerms\sof\sUse/))
confirmSigSignButton = await queryAsync($, 'button.btn-primary--lg') confirmSigSignButton = await queryAsync($, 'button.btn-primary--lg')
confirmSigSignButton[0].click() confirmSigSignButton[0].click()

@ -13,6 +13,9 @@ async function runFirstTimeUsageTest (assert, done) {
await skipNotices(app) await skipNotices(app)
const welcomeButton = (await findAsync(app, '.welcome-screen__button'))[0]
welcomeButton.click()
// Scroll through terms // Scroll through terms
const title = (await findAsync(app, '.create-password__title')).text() const title = (await findAsync(app, '.create-password__title')).text()
assert.equal(title, 'Create Password', 'create password screen') assert.equal(title, 'Create Password', 'create password screen')

@ -53,7 +53,7 @@ async function runSendFlowTest(assert, done) {
assert.equal(sendFromDropdownList.children().length, 4, 'send from dropdown shows all accounts') assert.equal(sendFromDropdownList.children().length, 4, 'send from dropdown shows all accounts')
sendFromDropdownList.children()[1].click() sendFromDropdownList.children()[1].click()
sendFromFieldItemAddress = await queryAsync($, '.account-list-item__account-name') sendFromFieldItemAddress = await queryAsync($, '.account-list-item__account-name')
assert.equal(sendFromFieldItemAddress[0].textContent, 'Send Account 2', 'send from field dropdown changes account name') assert.equal(sendFromFieldItemAddress[0].textContent, 'Send Account 2', 'send from field dropdown changes account name')
let sendToFieldInput = await queryAsync($, '.send-v2__to-autocomplete__input') let sendToFieldInput = await queryAsync($, '.send-v2__to-autocomplete__input')
@ -164,17 +164,27 @@ async function runSendFlowTest(assert, done) {
const sendButtonInEdit = await queryAsync($, '.btn-primary--lg.page-container__footer-button') const sendButtonInEdit = await queryAsync($, '.btn-primary--lg.page-container__footer-button')
assert.equal(sendButtonInEdit[0].textContent, 'Next', 'next button in edit rendered') assert.equal(sendButtonInEdit[0].textContent, 'Next', 'next button in edit rendered')
sendButtonInEdit[0].click()
// TODO: Need a way to mock background so that we can test correct transition from editing to confirm selectState.val('send new ui')
selectState.val('confirm new ui')
reactTriggerChange(selectState[0]) reactTriggerChange(selectState[0])
const confirmScreenConfirmButton = await queryAsync($, '.btn-confirm.page-container__footer-button')
console.log(`+++++++++++++++++++++++++++++++= confirmScreenConfirmButton[0]`, confirmScreenConfirmButton[0]);
confirmScreenConfirmButton[0].click()
const txView = await queryAsync($, '.tx-view') const cancelButtonInEdit = await queryAsync($, '.btn-secondary--lg.page-container__footer-button')
console.log(`++++++++++++++++++++++++++++++++ txView[0]`, txView[0]); cancelButtonInEdit[0].click()
// sendButtonInEdit[0].click()
// // TODO: Need a way to mock background so that we can test correct transition from editing to confirm
// selectState.val('confirm new ui')
// reactTriggerChange(selectState[0])
// const confirmScreenConfirmButton = await queryAsync($, '.btn-confirm.page-container__footer-button')
// console.log(`+++++++++++++++++++++++++++++++= confirmScreenConfirmButton[0]`, confirmScreenConfirmButton[0]);
// confirmScreenConfirmButton[0].click()
// await timeout(10000000)
// const txView = await queryAsync($, '.tx-view')
// console.log(`++++++++++++++++++++++++++++++++ txView[0]`, txView[0]);
assert.ok(txView[0], 'Should return to the account details screen after confirming') // assert.ok(txView[0], 'Should return to the account details screen after confirming')
} }

@ -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,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)
})
}) })

@ -162,7 +162,7 @@ describe('Transaction Controller', function () {
describe('#addUnapprovedTransaction', function () { describe('#addUnapprovedTransaction', function () {
it('should add an unapproved transaction and return a valid txMeta', function (done) { it('should add an unapproved transaction and return a valid txMeta', function (done) {
txController.addUnapprovedTransaction({}) txController.addUnapprovedTransaction({ from: '0x1678a085c290ebd122dc42cba69373b5953b831d' })
.then((txMeta) => { .then((txMeta) => {
assert(('id' in txMeta), 'should have a id') assert(('id' in txMeta), 'should have a id')
assert(('time' in txMeta), 'should have a time stamp') assert(('time' in txMeta), 'should have a time stamp')
@ -182,7 +182,7 @@ describe('Transaction Controller', function () {
assert(txMetaFromEmit, 'txMeta is falsey') assert(txMetaFromEmit, 'txMeta is falsey')
done() done()
}) })
txController.addUnapprovedTransaction({}) txController.addUnapprovedTransaction({ from: '0x1678a085c290ebd122dc42cba69373b5953b831d' })
.catch(done) .catch(done)
}) })
@ -210,29 +210,99 @@ describe('Transaction Controller', function () {
}) })
}) })
describe('#validateTxParams', function () { describe('#_validateTxParams', function () {
it('does not throw for positive values', function (done) { it('does not throw for positive values', function () {
var sample = { var sample = {
from: '0x1678a085c290ebd122dc42cba69373b5953b831d',
value: '0x01', value: '0x01',
} }
txController.txGasUtil.validateTxParams(sample).then(() => { txController._validateTxParams(sample)
done()
}).catch(done)
}) })
it('returns error for negative values', function (done) { it('returns error for negative values', function () {
var sample = { var sample = {
from: '0x1678a085c290ebd122dc42cba69373b5953b831d',
value: '-0x01', value: '-0x01',
} }
txController.txGasUtil.validateTxParams(sample) try {
.then(() => done('expected to thrown on negativity values but didn\'t')) txController._validateTxParams(sample)
.catch((err) => { } catch (err) {
assert.ok(err, 'error') assert.ok(err, 'error')
done() }
}) })
})
describe('#_normalizeTxParams', () => {
it('should normalize txParams', () => {
let txParams = {
chainId: '0x1',
from: 'a7df1beDBF813f57096dF77FCd515f0B3900e402',
to: null,
data: '68656c6c6f20776f726c64',
random: 'hello world',
}
let normalizedTxParams = txController._normalizeTxParams(txParams)
assert(!normalizedTxParams.chainId, 'their should be no chainId')
assert(!normalizedTxParams.to, 'their should be no to address if null')
assert.equal(normalizedTxParams.from.slice(0, 2), '0x', 'from should be hexPrefixd')
assert.equal(normalizedTxParams.data.slice(0, 2), '0x', 'data should be hexPrefixd')
assert(!('random' in normalizedTxParams), 'their should be no random key in normalizedTxParams')
txParams.to = 'a7df1beDBF813f57096dF77FCd515f0B3900e402'
normalizedTxParams = txController._normalizeTxParams(txParams)
assert.equal(normalizedTxParams.to.slice(0, 2), '0x', 'to should be hexPrefixd')
}) })
}) })
describe('#_validateRecipient', () => {
it('removes recipient for txParams with 0x when contract data is provided', function () {
const zeroRecipientandDataTxParams = {
from: '0x1678a085c290ebd122dc42cba69373b5953b831d',
to: '0x',
data: 'bytecode',
}
const sanitizedTxParams = txController._validateRecipient(zeroRecipientandDataTxParams)
assert.deepEqual(sanitizedTxParams, { from: '0x1678a085c290ebd122dc42cba69373b5953b831d', data: 'bytecode' }, 'no recipient with 0x')
})
it('should error when recipient is 0x', function () {
const zeroRecipientTxParams = {
from: '0x1678a085c290ebd122dc42cba69373b5953b831d',
to: '0x',
}
assert.throws(() => { txController._validateRecipient(zeroRecipientTxParams) }, Error, 'Invalid recipient address')
})
})
describe('#_validateFrom', () => {
it('should error when from is not a hex string', function () {
// where from is undefined
const txParams = {}
assert.throws(() => { txController._validateFrom(txParams) }, Error, `Invalid from address ${txParams.from} not a string`)
// where from is array
txParams.from = []
assert.throws(() => { txController._validateFrom(txParams) }, Error, `Invalid from address ${txParams.from} not a string`)
// where from is a object
txParams.from = {}
assert.throws(() => { txController._validateFrom(txParams) }, Error, `Invalid from address ${txParams.from} not a string`)
// where from is a invalid address
txParams.from = 'im going to fail'
assert.throws(() => { txController._validateFrom(txParams) }, Error, `Invalid from address`)
// should run
txParams.from ='0x1678a085c290ebd122dc42cba69373b5953b831d'
txController._validateFrom(txParams)
})
})
describe('#addTx', function () { describe('#addTx', function () {
it('should emit updates', function (done) { it('should emit updates', function (done) {
const txMeta = { const txMeta = {

@ -11,22 +11,4 @@ describe('Tx Gas Util', function () {
provider, provider,
}) })
}) })
it('removes recipient for txParams with 0x when contract data is provided', function () {
const zeroRecipientandDataTxParams = {
from: '0x1678a085c290ebd122dc42cba69373b5953b831d',
to: '0x',
data: 'bytecode',
}
const sanitizedTxParams = txGasUtil.validateRecipient(zeroRecipientandDataTxParams)
assert.deepEqual(sanitizedTxParams, { from: '0x1678a085c290ebd122dc42cba69373b5953b831d', data: 'bytecode' }, 'no recipient with 0x')
})
it('should error when recipient is 0x', function () {
const zeroRecipientTxParams = {
from: '0x1678a085c290ebd122dc42cba69373b5953b831d',
to: '0x',
}
assert.throws(() => { txGasUtil.validateRecipient(zeroRecipientTxParams) }, Error, 'Invalid recipient address')
})
}) })

@ -347,7 +347,7 @@ function transitionBackward () {
} }
function confirmSeedWords () { function confirmSeedWords () {
return (dispatch) => { return dispatch => {
dispatch(actions.showLoadingIndication()) dispatch(actions.showLoadingIndication())
log.debug(`background.clearSeedWordCache`) log.debug(`background.clearSeedWordCache`)
return new Promise((resolve, reject) => { return new Promise((resolve, reject) => {
@ -355,7 +355,7 @@ function confirmSeedWords () {
dispatch(actions.hideLoadingIndication()) dispatch(actions.hideLoadingIndication())
if (err) { if (err) {
dispatch(actions.displayWarning(err.message)) dispatch(actions.displayWarning(err.message))
reject(err) return reject(err)
} }
log.info('Seed word cache cleared. ' + account) log.info('Seed word cache cleared. ' + account)
@ -571,35 +571,47 @@ function signMsg (msgData) {
return (dispatch) => { return (dispatch) => {
dispatch(actions.showLoadingIndication()) dispatch(actions.showLoadingIndication())
log.debug(`actions calling background.signMessage`) return new Promise((resolve, reject) => {
background.signMessage(msgData, (err, newState) => { log.debug(`actions calling background.signMessage`)
log.debug('signMessage called back') background.signMessage(msgData, (err, newState) => {
dispatch(actions.updateMetamaskState(newState)) log.debug('signMessage called back')
dispatch(actions.hideLoadingIndication()) dispatch(actions.updateMetamaskState(newState))
dispatch(actions.hideLoadingIndication())
if (err) log.error(err) if (err) {
if (err) return dispatch(actions.displayWarning(err.message)) log.error(err)
dispatch(actions.displayWarning(err.message))
return reject(err)
}
dispatch(actions.completedTx(msgData.metamaskId)) dispatch(actions.completedTx(msgData.metamaskId))
return resolve(msgData)
})
}) })
} }
} }
function signPersonalMsg (msgData) { function signPersonalMsg (msgData) {
log.debug('action - signPersonalMsg') log.debug('action - signPersonalMsg')
return (dispatch) => { return dispatch => {
dispatch(actions.showLoadingIndication()) dispatch(actions.showLoadingIndication())
log.debug(`actions calling background.signPersonalMessage`) return new Promise((resolve, reject) => {
background.signPersonalMessage(msgData, (err, newState) => { log.debug(`actions calling background.signPersonalMessage`)
log.debug('signPersonalMessage called back') background.signPersonalMessage(msgData, (err, newState) => {
dispatch(actions.updateMetamaskState(newState)) log.debug('signPersonalMessage called back')
dispatch(actions.hideLoadingIndication()) dispatch(actions.updateMetamaskState(newState))
dispatch(actions.hideLoadingIndication())
if (err) log.error(err) if (err) {
if (err) return dispatch(actions.displayWarning(err.message)) log.error(err)
dispatch(actions.displayWarning(err.message))
return reject(err)
}
dispatch(actions.completedTx(msgData.metamaskId)) dispatch(actions.completedTx(msgData.metamaskId))
return resolve(msgData)
})
}) })
} }
} }
@ -609,16 +621,22 @@ function signTypedMsg (msgData) {
return (dispatch) => { return (dispatch) => {
dispatch(actions.showLoadingIndication()) dispatch(actions.showLoadingIndication())
log.debug(`actions calling background.signTypedMessage`) return new Promise((resolve, reject) => {
background.signTypedMessage(msgData, (err, newState) => { log.debug(`actions calling background.signTypedMessage`)
log.debug('signTypedMessage called back') background.signTypedMessage(msgData, (err, newState) => {
dispatch(actions.updateMetamaskState(newState)) log.debug('signTypedMessage called back')
dispatch(actions.hideLoadingIndication()) dispatch(actions.updateMetamaskState(newState))
dispatch(actions.hideLoadingIndication())
if (err) log.error(err) if (err) {
if (err) return dispatch(actions.displayWarning(err.message)) log.error(err)
dispatch(actions.displayWarning(err.message))
return reject(err)
}
dispatch(actions.completedTx(msgData.metamaskId)) dispatch(actions.completedTx(msgData.metamaskId))
return resolve(msgData)
})
}) })
} }
} }
@ -798,17 +816,24 @@ function updateTransaction (txData) {
function updateAndApproveTx (txData) { function updateAndApproveTx (txData) {
log.info('actions: updateAndApproveTx: ' + JSON.stringify(txData)) log.info('actions: updateAndApproveTx: ' + JSON.stringify(txData))
return (dispatch) => { return (dispatch) => {
log.debug(`actions calling background.updateAndApproveTx.`) log.debug(`actions calling background.updateAndApproveTx`)
background.updateAndApproveTransaction(txData, (err) => {
dispatch(actions.hideLoadingIndication()) return new Promise((resolve, reject) => {
dispatch(actions.updateTransactionParams(txData.id, txData.txParams)) background.updateAndApproveTransaction(txData, err => {
dispatch(actions.clearSend()) dispatch(actions.hideLoadingIndication())
if (err) { dispatch(actions.updateTransactionParams(txData.id, txData.txParams))
dispatch(actions.txError(err)) dispatch(actions.clearSend())
dispatch(actions.goHome())
return log.error(err.message) if (err) {
} dispatch(actions.txError(err))
dispatch(actions.completedTx(txData.id)) dispatch(actions.goHome())
log.error(err.message)
reject(err)
}
dispatch(actions.completedTx(txData.id))
resolve(txData)
})
}) })
} }
} }
@ -836,29 +861,77 @@ function txError (err) {
} }
function cancelMsg (msgData) { function cancelMsg (msgData) {
log.debug(`background.cancelMessage`) return dispatch => {
background.cancelMessage(msgData.id) dispatch(actions.showLoadingIndication())
return actions.completedTx(msgData.id)
return new Promise((resolve, reject) => {
log.debug(`background.cancelMessage`)
background.cancelMessage(msgData.id, (err, newState) => {
dispatch(actions.updateMetamaskState(newState))
dispatch(actions.hideLoadingIndication())
if (err) {
return reject(err)
}
dispatch(actions.completedTx(msgData.id))
return resolve(msgData)
})
})
}
} }
function cancelPersonalMsg (msgData) { function cancelPersonalMsg (msgData) {
const id = msgData.id return dispatch => {
background.cancelPersonalMessage(id) dispatch(actions.showLoadingIndication())
return actions.completedTx(id)
return new Promise((resolve, reject) => {
const id = msgData.id
background.cancelPersonalMessage(id, (err, newState) => {
dispatch(actions.updateMetamaskState(newState))
dispatch(actions.hideLoadingIndication())
if (err) {
return reject(err)
}
dispatch(actions.completedTx(id))
return resolve(msgData)
})
})
}
} }
function cancelTypedMsg (msgData) { function cancelTypedMsg (msgData) {
const id = msgData.id return dispatch => {
background.cancelTypedMessage(id) dispatch(actions.showLoadingIndication())
return actions.completedTx(id)
return new Promise((resolve, reject) => {
const id = msgData.id
background.cancelTypedMessage(id, (err, newState) => {
dispatch(actions.updateMetamaskState(newState))
dispatch(actions.hideLoadingIndication())
if (err) {
return reject(err)
}
dispatch(actions.completedTx(id))
return resolve(msgData)
})
})
}
} }
function cancelTx (txData) { function cancelTx (txData) {
return (dispatch) => { return dispatch => {
log.debug(`background.cancelTransaction`) log.debug(`background.cancelTransaction`)
background.cancelTransaction(txData.id, () => { return new Promise((resolve, reject) => {
dispatch(actions.clearSend()) background.cancelTransaction(txData.id, () => {
dispatch(actions.completedTx(txData.id)) dispatch(actions.clearSend())
dispatch(actions.completedTx(txData.id))
resolve(txData)
})
}) })
} }
} }
@ -1249,12 +1322,13 @@ function markNoticeRead (notice) {
dispatch(actions.displayWarning(err)) dispatch(actions.displayWarning(err))
return reject(err) return reject(err)
} }
if (notice) { if (notice) {
dispatch(actions.showNotice(notice)) dispatch(actions.showNotice(notice))
resolve() resolve(true)
} else { } else {
dispatch(actions.clearNotices()) dispatch(actions.clearNotices())
resolve() resolve(false)
} }
}) })
}) })
@ -1300,7 +1374,7 @@ function retryTransaction (txId) {
function setProviderType (type) { function setProviderType (type) {
return (dispatch) => { return (dispatch) => {
log.debug(`background.setProviderType`) log.debug(`background.setProviderType`, type)
background.setProviderType(type, (err, result) => { background.setProviderType(type, (err, result) => {
if (err) { if (err) {
log.error(err) log.error(err)
@ -1321,13 +1395,14 @@ function updateProviderType (type) {
} }
function setRpcTarget (newRpc) { function setRpcTarget (newRpc) {
log.debug(`background.setRpcTarget: ${newRpc}`)
return (dispatch) => { return (dispatch) => {
log.debug(`background.setRpcTarget: ${newRpc}`)
background.setCustomRpc(newRpc, (err, result) => { background.setCustomRpc(newRpc, (err, result) => {
if (err) { if (err) {
log.error(err) log.error(err)
return dispatch(self.displayWarning('Had a problem changing networks!')) return dispatch(self.displayWarning('Had a problem changing networks!'))
} }
dispatch(actions.setSelectedToken())
}) })
} }
} }
@ -1772,7 +1847,7 @@ function forceUpdateMetamaskState (dispatch) {
} }
dispatch(actions.updateMetamaskState(newState)) dispatch(actions.updateMetamaskState(newState))
resolve() resolve(newState)
}) })
}) })
} }

@ -1,62 +1,421 @@
const inherits = require('util').inherits const { Component } = require('react')
const Component = require('react').Component const PropTypes = require('prop-types')
const connect = require('react-redux').connect const connect = require('react-redux').connect
const { Route, Switch, withRouter } = require('react-router-dom')
const { compose } = require('recompose')
const h = require('react-hyperscript') const h = require('react-hyperscript')
const PropTypes = require('prop-types')
const actions = require('./actions') const actions = require('./actions')
const classnames = require('classnames') const classnames = require('classnames')
// mascara
const MascaraFirstTime = require('../../mascara/src/app/first-time').default
const MascaraBuyEtherScreen = require('../../mascara/src/app/first-time/buy-ether-screen').default
// init // init
const OldUIInitializeMenuScreen = require('./first-time/init-menu') const InitializeScreen = require('../../mascara/src/app/first-time').default
const InitializeMenuScreen = MascaraFirstTime
const NewKeyChainScreen = require('./new-keychain')
const WelcomeScreen = require('./welcome-screen').default
// accounts // accounts
const MainContainer = require('./main-container')
const SendTransactionScreen2 = require('./components/send/send-v2-container') const SendTransactionScreen2 = require('./components/send/send-v2-container')
const ConfirmTxScreen = require('./conf-tx') const ConfirmTxScreen = require('./conf-tx')
// notice
const NoticeScreen = require('./components/notice')
const generateLostAccountsNotice = require('../lib/lost-accounts-notice')
// slideout menu // slideout menu
const WalletView = require('./components/wallet-view') const WalletView = require('./components/wallet-view')
// other views // other views
const Settings = require('./settings') const Home = require('./components/pages/home')
const AddTokenScreen = require('./add-token') const Authenticated = require('./components/pages/authenticated')
const Import = require('./accounts/import') const Initialized = require('./components/pages/initialized')
const NewAccount = require('./accounts/new-account') const Settings = require('./components/pages/settings')
const UnlockPage = require('./components/pages/unlock')
const RestoreVaultPage = require('./components/pages/keychains/restore-vault')
const RevealSeedConfirmation = require('./keychains/hd/recover-seed/confirmation')
const AddTokenPage = require('./components/pages/add-token')
const CreateAccountPage = require('./components/pages/create-account')
const NoticeScreen = require('./components/pages/notice')
const Loading = require('./components/loading') const Loading = require('./components/loading')
const NetworkIndicator = require('./components/network') const NetworkIndicator = require('./components/network')
const Identicon = require('./components/identicon') const Identicon = require('./components/identicon')
const BuyView = require('./components/buy-button-subview')
const HDCreateVaultComplete = require('./keychains/hd/create-vault-complete')
const HDRestoreVaultScreen = require('./keychains/hd/restore-vault')
const RevealSeedConfirmation = require('./keychains/hd/recover-seed/confirmation')
const ReactCSSTransitionGroup = require('react-addons-css-transition-group') const ReactCSSTransitionGroup = require('react-addons-css-transition-group')
const NetworkDropdown = require('./components/dropdowns/network-dropdown') const NetworkDropdown = require('./components/dropdowns/network-dropdown')
const AccountMenu = require('./components/account-menu') const AccountMenu = require('./components/account-menu')
const QrView = require('./components/qr-code')
// Global Modals // Global Modals
const Modal = require('./components/modals/index').Modal const Modal = require('./components/modals/index').Modal
App.contextTypes = { // Routes
t: PropTypes.func, const {
} DEFAULT_ROUTE,
UNLOCK_ROUTE,
SETTINGS_ROUTE,
REVEAL_SEED_ROUTE,
RESTORE_VAULT_ROUTE,
ADD_TOKEN_ROUTE,
NEW_ACCOUNT_ROUTE,
SEND_ROUTE,
CONFIRM_TRANSACTION_ROUTE,
INITIALIZE_ROUTE,
NOTICE_ROUTE,
} = require('./routes')
class App extends Component {
componentWillMount () {
const { currentCurrency, setCurrentCurrencyToUSD } = this.props
if (!currentCurrency) {
setCurrentCurrencyToUSD()
}
}
renderRoutes () {
const exact = true
return (
h(Switch, [
h(Route, { path: INITIALIZE_ROUTE, component: InitializeScreen }),
h(Initialized, { path: REVEAL_SEED_ROUTE, exact, component: RevealSeedConfirmation }),
h(Initialized, { path: UNLOCK_ROUTE, exact, component: UnlockPage }),
h(Initialized, { path: SETTINGS_ROUTE, component: Settings }),
h(Initialized, { path: RESTORE_VAULT_ROUTE, exact, component: RestoreVaultPage }),
h(Initialized, { path: NOTICE_ROUTE, exact, component: NoticeScreen }),
h(Authenticated, { path: CONFIRM_TRANSACTION_ROUTE, component: ConfirmTxScreen }),
h(Authenticated, { path: SEND_ROUTE, exact, component: SendTransactionScreen2 }),
h(Authenticated, { path: ADD_TOKEN_ROUTE, exact, component: AddTokenPage }),
h(Authenticated, { path: NEW_ACCOUNT_ROUTE, component: CreateAccountPage }),
h(Authenticated, { path: DEFAULT_ROUTE, exact, component: Home }),
])
)
}
render () {
const {
isLoading,
loadingMessage,
network,
isMouseUser,
provider,
frequentRpcList,
currentView,
setMouseUserState,
} = this.props
const isLoadingNetwork = network === 'loading' && currentView.name !== 'config'
const loadMessage = loadingMessage || isLoadingNetwork ?
this.getConnectingLabel() : null
log.debug('Main ui render function')
return (
h('.flex-column.full-height', {
className: classnames({ 'mouse-user-styles': isMouseUser }),
style: {
overflowX: 'hidden',
position: 'relative',
alignItems: 'center',
},
tabIndex: '0',
onClick: () => setMouseUserState(true),
onKeyDown: (e) => {
if (e.keyCode === 9) {
setMouseUserState(false)
}
},
}, [
// global modal
h(Modal, {}, []),
// app bar
this.renderAppBar(),
// sidebar
this.renderSidebar(),
module.exports = connect(mapStateToProps, mapDispatchToProps)(App) // network dropdown
h(NetworkDropdown, {
provider,
frequentRpcList,
}, []),
h(AccountMenu),
inherits(App, Component) (isLoading || isLoadingNetwork) && h(Loading, {
function App () { Component.call(this) } loadingMessage: loadMessage,
}),
// this.renderLoadingIndicator({ isLoading, isLoadingNetwork, loadMessage }),
// content
this.renderRoutes(),
])
)
}
renderGlobalModal () {
return h(Modal, {
ref: 'modalRef',
}, [
// h(BuyOptions, {}, []),
])
}
renderSidebar () {
return h('div', [
h('style', `
.sidebar-enter {
transition: transform 300ms ease-in-out;
transform: translateX(-100%);
}
.sidebar-enter.sidebar-enter-active {
transition: transform 300ms ease-in-out;
transform: translateX(0%);
}
.sidebar-leave {
transition: transform 200ms ease-out;
transform: translateX(0%);
}
.sidebar-leave.sidebar-leave-active {
transition: transform 200ms ease-out;
transform: translateX(-100%);
}
`),
h(ReactCSSTransitionGroup, {
transitionName: 'sidebar',
transitionEnterTimeout: 300,
transitionLeaveTimeout: 200,
}, [
// A second instance of Walletview is used for non-mobile viewports
this.props.sidebarOpen ? h(WalletView, {
responsiveDisplayClassname: '.sidebar',
style: {},
}) : undefined,
]),
// overlay
// TODO: add onClick for overlay to close sidebar
this.props.sidebarOpen ? h('div.sidebar-overlay', {
style: {},
onClick: () => {
this.props.hideSidebar()
},
}, []) : undefined,
])
}
renderAppBar () {
const {
isUnlocked,
network,
provider,
networkDropdownOpen,
showNetworkDropdown,
hideNetworkDropdown,
isInitialized,
welcomeScreenSeen,
isPopup,
betaUI,
} = this.props
if (window.METAMASK_UI_TYPE === 'notification') {
return null
}
const props = this.props
const {isMascara, isOnboarding} = props
// Do not render header if user is in mascara onboarding
if (isMascara && isOnboarding) {
return null
}
// Do not render header if user is in mascara buy ether
if (isMascara && props.currentView.name === 'buyEth') {
return null
}
return (
h('.full-width', {
style: {},
}, [
(isInitialized || welcomeScreenSeen || isPopup || !betaUI) && h('.app-header.flex-row.flex-space-between', {
className: classnames({
'app-header--initialized': !isOnboarding,
}),
}, [
h('div.app-header-contents', {}, [
h('div.left-menu-wrapper', {
onClick: () => props.history.push(DEFAULT_ROUTE),
}, [
// mini logo
h('img.metafox-icon', {
height: 42,
width: 42,
src: '/images/metamask-fox.svg',
}),
// metamask name
h('.flex-row', [
h('h1', this.context.t('appName')),
h('div.beta-label', this.context.t('beta')),
]),
]),
betaUI && isInitialized && h('div.header__right-actions', [
h('div.network-component-wrapper', {
style: {},
}, [
// Network Indicator
h(NetworkIndicator, {
network,
provider,
disabled: this.props.location.pathname === CONFIRM_TRANSACTION_ROUTE,
onClick: (event) => {
event.preventDefault()
event.stopPropagation()
return networkDropdownOpen === false
? showNetworkDropdown()
: hideNetworkDropdown()
},
}),
]),
isUnlocked && h('div.account-menu__icon', { onClick: this.props.toggleAccountMenu }, [
h(Identicon, {
address: this.props.selectedAddress,
diameter: 32,
}),
]),
]),
]),
]),
!isInitialized && !isPopup && betaUI && h('.alpha-warning__container', {}, [
h('h2', {
className: classnames({
'alpha-warning': welcomeScreenSeen,
'alpha-warning-welcome-screen': !welcomeScreenSeen,
}),
}, 'Please be aware that this version is still under development'),
]),
])
)
}
renderLoadingIndicator ({ isLoading, isLoadingNetwork, loadMessage }) {
const { isMascara } = this.props
return isMascara
? null
: h(Loading, {
isLoading: isLoading || isLoadingNetwork,
loadingMessage: loadMessage,
})
}
toggleMetamaskActive () {
if (!this.props.isUnlocked) {
// currently inactive: redirect to password box
var passwordBox = document.querySelector('input[type=password]')
if (!passwordBox) return
passwordBox.focus()
} else {
// currently active: deactivate
this.props.dispatch(actions.lockMetamask(false))
}
}
getConnectingLabel = function () {
const { provider } = this.props
const providerName = provider.type
let name
if (providerName === 'mainnet') {
name = this.context.t('connectingToMainnet')
} else if (providerName === 'ropsten') {
name = this.context.t('connectingToRopsten')
} else if (providerName === 'kovan') {
name = this.context.t('connectingToRopsten')
} else if (providerName === 'rinkeby') {
name = this.context.t('connectingToRinkeby')
} else {
name = this.context.t('connectingToUnknown')
}
return name
}
getNetworkName () {
const { provider } = this.props
const providerName = provider.type
let name
if (providerName === 'mainnet') {
name = this.context.t('mainnet')
} else if (providerName === 'ropsten') {
name = this.context.t('ropsten')
} else if (providerName === 'kovan') {
name = this.context.t('kovan')
} else if (providerName === 'rinkeby') {
name = this.context.t('rinkeby')
} else {
name = this.context.t('unknownNetwork')
}
return name
}
}
App.propTypes = {
currentCurrency: PropTypes.string,
setCurrentCurrencyToUSD: PropTypes.func,
isLoading: PropTypes.bool,
loadingMessage: PropTypes.string,
network: PropTypes.string,
provider: PropTypes.object,
frequentRpcList: PropTypes.array,
currentView: PropTypes.object,
sidebarOpen: PropTypes.bool,
hideSidebar: PropTypes.func,
isMascara: PropTypes.bool,
isOnboarding: PropTypes.bool,
isUnlocked: PropTypes.bool,
networkDropdownOpen: PropTypes.bool,
showNetworkDropdown: PropTypes.func,
hideNetworkDropdown: PropTypes.func,
history: PropTypes.object,
location: PropTypes.object,
dispatch: PropTypes.func,
toggleAccountMenu: PropTypes.func,
selectedAddress: PropTypes.string,
noActiveNotices: PropTypes.bool,
lostAccounts: PropTypes.array,
isInitialized: PropTypes.bool,
forgottenPassword: PropTypes.bool,
activeAddress: PropTypes.string,
unapprovedTxs: PropTypes.object,
seedWords: PropTypes.string,
unapprovedMsgCount: PropTypes.number,
unapprovedPersonalMsgCount: PropTypes.number,
unapprovedTypedMessagesCount: PropTypes.number,
welcomeScreenSeen: PropTypes.bool,
isPopup: PropTypes.bool,
betaUI: PropTypes.bool,
isMouseUser: PropTypes.bool,
setMouseUserState: PropTypes.func,
t: PropTypes.func,
}
function mapStateToProps (state) { function mapStateToProps (state) {
const { appState, metamask } = state
const {
networkDropdownOpen,
sidebarOpen,
isLoading,
loadingMessage,
} = appState
const { const {
identities, identities,
accounts, accounts,
@ -65,17 +424,23 @@ function mapStateToProps (state) {
isInitialized, isInitialized,
noActiveNotices, noActiveNotices,
seedWords, seedWords,
} = state.metamask unapprovedTxs,
lastUnreadNotice,
lostAccounts,
unapprovedMsgCount,
unapprovedPersonalMsgCount,
unapprovedTypedMessagesCount,
} = metamask
const selected = address || Object.keys(accounts)[0] const selected = address || Object.keys(accounts)[0]
return { return {
// state from plugin // state from plugin
networkDropdownOpen: state.appState.networkDropdownOpen, networkDropdownOpen,
sidebarOpen: state.appState.sidebarOpen, sidebarOpen,
isLoading: state.appState.isLoading, isLoading,
loadingMessage: state.appState.loadingMessage, loadingMessage,
noActiveNotices: state.metamask.noActiveNotices, noActiveNotices,
isInitialized: state.metamask.isInitialized, isInitialized,
isUnlocked: state.metamask.isUnlocked, isUnlocked: state.metamask.isUnlocked,
selectedAddress: state.metamask.selectedAddress, selectedAddress: state.metamask.selectedAddress,
currentView: state.appState.currentView, currentView: state.appState.currentView,
@ -85,14 +450,17 @@ function mapStateToProps (state) {
isOnboarding: Boolean(!noActiveNotices || seedWords || !isInitialized), isOnboarding: Boolean(!noActiveNotices || seedWords || !isInitialized),
isPopup: state.metamask.isPopup, isPopup: state.metamask.isPopup,
seedWords: state.metamask.seedWords, seedWords: state.metamask.seedWords,
unapprovedTxs: state.metamask.unapprovedTxs, unapprovedTxs,
unapprovedMsgs: state.metamask.unapprovedMsgs, unapprovedMsgs: state.metamask.unapprovedMsgs,
unapprovedMsgCount,
unapprovedPersonalMsgCount,
unapprovedTypedMessagesCount,
menuOpen: state.appState.menuOpen, menuOpen: state.appState.menuOpen,
network: state.metamask.network, network: state.metamask.network,
provider: state.metamask.provider, provider: state.metamask.provider,
forgottenPassword: state.metamask.forgottenPassword, forgottenPassword: state.appState.forgottenPassword,
lastUnreadNotice: state.metamask.lastUnreadNotice, lastUnreadNotice,
lostAccounts: state.metamask.lostAccounts, lostAccounts,
frequentRpcList: state.metamask.frequentRpcList || [], frequentRpcList: state.metamask.frequentRpcList || [],
currentCurrency: state.metamask.currentCurrency, currentCurrency: state.metamask.currentCurrency,
isMouseUser: state.appState.isMouseUser, isMouseUser: state.appState.isMouseUser,
@ -120,479 +488,11 @@ function mapDispatchToProps (dispatch, ownProps) {
} }
} }
App.prototype.componentWillMount = function () { App.contextTypes = {
if (!this.props.currentCurrency) { t: PropTypes.func,
this.props.setCurrentCurrencyToUSD()
}
}
App.prototype.render = function () {
var props = this.props
const {
isLoading,
loadingMessage,
network,
isMouseUser,
setMouseUserState,
} = props
const isLoadingNetwork = network === 'loading' && props.currentView.name !== 'config'
const loadMessage = loadingMessage || isLoadingNetwork ?
this.getConnectingLabel() : null
log.debug('Main ui render function')
return (
h('.flex-column.full-height', {
className: classnames({ 'mouse-user-styles': isMouseUser }),
style: {
overflowX: 'hidden',
position: 'relative',
alignItems: 'center',
},
tabIndex: '0',
onClick: () => setMouseUserState(true),
onKeyDown: (e) => {
if (e.keyCode === 9) {
setMouseUserState(false)
}
},
}, [
// global modal
h(Modal, {}, []),
// app bar
this.renderAppBar(),
// sidebar
this.renderSidebar(),
// network dropdown
h(NetworkDropdown, {
provider: this.props.provider,
frequentRpcList: this.props.frequentRpcList,
}, []),
h(AccountMenu),
(isLoading || isLoadingNetwork) && h(Loading, {
loadingMessage: loadMessage,
}),
// this.renderLoadingIndicator({ isLoading, isLoadingNetwork, loadMessage }),
// content
this.renderPrimary(),
])
)
}
App.prototype.renderGlobalModal = function () {
return h(Modal, {
ref: 'modalRef',
}, [
// h(BuyOptions, {}, []),
])
}
App.prototype.renderSidebar = function () {
return h('div', {
}, [
h('style', `
.sidebar-enter {
transition: transform 300ms ease-in-out;
transform: translateX(-100%);
}
.sidebar-enter.sidebar-enter-active {
transition: transform 300ms ease-in-out;
transform: translateX(0%);
}
.sidebar-leave {
transition: transform 200ms ease-out;
transform: translateX(0%);
}
.sidebar-leave.sidebar-leave-active {
transition: transform 200ms ease-out;
transform: translateX(-100%);
}
`),
h(ReactCSSTransitionGroup, {
transitionName: 'sidebar',
transitionEnterTimeout: 300,
transitionLeaveTimeout: 200,
}, [
// A second instance of Walletview is used for non-mobile viewports
this.props.sidebarOpen ? h(WalletView, {
responsiveDisplayClassname: '.sidebar',
style: {},
}) : undefined,
]),
// overlay
// TODO: add onClick for overlay to close sidebar
this.props.sidebarOpen ? h('div.sidebar-overlay', {
style: {},
onClick: () => {
this.props.hideSidebar()
},
}, []) : undefined,
])
}
App.prototype.renderAppBar = function () {
const {
isUnlocked,
network,
provider,
networkDropdownOpen,
showNetworkDropdown,
hideNetworkDropdown,
currentView,
isInitialized,
betaUI,
isPopup,
welcomeScreenSeen,
} = this.props
if (window.METAMASK_UI_TYPE === 'notification') {
return null
}
const props = this.props
const {isMascara, isOnboarding} = props
// Do not render header if user is in mascara onboarding
if (isMascara && isOnboarding) {
return null
}
// Do not render header if user is in mascara buy ether
if (isMascara && props.currentView.name === 'buyEth') {
return null
}
return (
h('.full-width', {
style: {},
}, [
(isInitialized || welcomeScreenSeen || isPopup || !betaUI) && h('.app-header.flex-row.flex-space-between', {
className: classnames({
'app-header--initialized': !isOnboarding,
}),
}, [
h('div.app-header-contents', {}, [
h('div.left-menu-wrapper', {
onClick: () => {
props.dispatch(actions.backToAccountDetail(props.activeAddress))
},
}, [
// mini logo
h('img.metafox-icon', {
height: 42,
width: 42,
src: './images/metamask-fox.svg',
}),
// metamask name
h('.flex-row', [
h('h1', this.context.t('appName')),
h('div.beta-label', this.context.t('beta')),
]),
]),
betaUI && isInitialized && h('div.header__right-actions', [
h('div.network-component-wrapper', {
style: {},
}, [
// Network Indicator
h(NetworkIndicator, {
network,
provider,
disabled: currentView.name === 'confTx',
onClick: (event) => {
event.preventDefault()
event.stopPropagation()
return networkDropdownOpen === false
? showNetworkDropdown()
: hideNetworkDropdown()
},
}),
]),
isUnlocked && h('div.account-menu__icon', { onClick: this.props.toggleAccountMenu }, [
h(Identicon, {
address: this.props.selectedAddress,
diameter: 32,
}),
]),
]),
]),
]),
!isInitialized && !isPopup && betaUI && h('.alpha-warning__container', {}, [
h('h2', {
className: classnames({
'alpha-warning': welcomeScreenSeen,
'alpha-warning-welcome-screen': !welcomeScreenSeen,
}),
}, 'Please be aware that this version is still under development'),
]),
])
)
}
App.prototype.renderLoadingIndicator = function ({ isLoading, isLoadingNetwork, loadMessage }) {
const { isMascara } = this.props
return isMascara
? null
: h(Loading, {
isLoading: isLoading || isLoadingNetwork,
loadingMessage: loadMessage,
})
}
App.prototype.renderBackButton = function (style, justArrow = false) {
var props = this.props
return (
h('.flex-row', {
key: 'leftArrow',
style: style,
onClick: () => props.dispatch(actions.goBackToInitView()),
}, [
h('i.fa.fa-arrow-left.cursor-pointer'),
justArrow ? null : h('div.cursor-pointer', {
style: {
marginLeft: '3px',
},
onClick: () => props.dispatch(actions.goBackToInitView()),
}, 'BACK'),
])
)
}
App.prototype.renderPrimary = function () {
log.debug('rendering primary')
var props = this.props
const {
isMascara,
isOnboarding,
betaUI,
isRevealingSeedWords,
welcomeScreenSeen,
Qr,
isInitialized,
isUnlocked,
} = props
const isMascaraOnboarding = isMascara && isOnboarding
const isBetaUIOnboarding = betaUI && isOnboarding
if (!welcomeScreenSeen && betaUI && !isInitialized && !isUnlocked) {
return h(WelcomeScreen)
}
if (isMascaraOnboarding || isBetaUIOnboarding) {
return h(MascaraFirstTime)
}
// notices
if (!props.noActiveNotices && !betaUI) {
log.debug('rendering notice screen for unread notices.')
return h(NoticeScreen, {
notice: props.lastUnreadNotice,
key: 'NoticeScreen',
onConfirm: () => props.dispatch(actions.markNoticeRead(props.lastUnreadNotice)),
})
} else if (props.lostAccounts && props.lostAccounts.length > 0) {
log.debug('rendering notice screen for lost accounts view.')
return h(NoticeScreen, {
notice: generateLostAccountsNotice(props.lostAccounts),
key: 'LostAccountsNotice',
onConfirm: () => props.dispatch(actions.markAccountsFound()),
})
}
if (props.isInitialized && props.forgottenPassword) {
log.debug('rendering restore vault screen')
return h(HDRestoreVaultScreen, {key: 'HDRestoreVaultScreen'})
} else if (!props.isInitialized && !props.isUnlocked && !isRevealingSeedWords) {
log.debug('rendering menu screen')
return !betaUI
? h(OldUIInitializeMenuScreen, {key: 'menuScreenInit'})
: h(InitializeMenuScreen, {key: 'menuScreenInit'})
}
// show unlock screen
if (!props.isUnlocked) {
return h(MainContainer, {
currentViewName: props.currentView.name,
isUnlocked: props.isUnlocked,
})
}
// show seed words screen
if (props.seedWords) {
log.debug('rendering seed words')
return h(HDCreateVaultComplete, {key: 'HDCreateVaultComplete'})
}
// show current view
switch (props.currentView.name) {
case 'accountDetail':
log.debug('rendering main container')
return h(MainContainer, {key: 'account-detail'})
case 'sendTransaction':
log.debug('rendering send tx screen')
// Going to leave this here until we are ready to delete SendTransactionScreen v1
// const SendComponentToRender = checkFeatureToggle('send-v2')
// ? SendTransactionScreen2
// : SendTransactionScreen
return h(SendTransactionScreen2, {key: 'send-transaction'})
case 'sendToken':
log.debug('rendering send token screen')
// Going to leave this here until we are ready to delete SendTransactionScreen v1
// const SendTokenComponentToRender = checkFeatureToggle('send-v2')
// ? SendTransactionScreen2
// : SendTokenScreen
return h(SendTransactionScreen2, {key: 'sendToken'})
case 'newKeychain':
log.debug('rendering new keychain screen')
return h(NewKeyChainScreen, {key: 'new-keychain'})
case 'confTx':
log.debug('rendering confirm tx screen')
return h(ConfirmTxScreen, {key: 'confirm-tx'})
case 'add-token':
log.debug('rendering add-token screen from unlock screen.')
return h(AddTokenScreen, {key: 'add-token'})
case 'config':
log.debug('rendering config screen')
return h(Settings, {key: 'config'})
case 'import-menu':
log.debug('rendering import screen')
return h(Import, {key: 'import-menu'})
case 'new-account-page':
log.debug('rendering new account screen')
return h(NewAccount, {key: 'new-account'})
case 'reveal-seed-conf':
log.debug('rendering reveal seed confirmation screen')
return h(RevealSeedConfirmation, {key: 'reveal-seed-conf'})
case 'info':
log.debug('rendering info screen')
return h(Settings, {key: 'info', tab: 'info'})
case 'buyEth':
log.debug('rendering buy ether screen')
return h(BuyView, {key: 'buyEthView'})
case 'onboardingBuyEth':
log.debug('rendering onboarding buy ether screen')
return h(MascaraBuyEtherScreen, {key: 'buyEthView'})
case 'qr':
log.debug('rendering show qr screen')
return h('div', {
style: {
position: 'absolute',
height: '100%',
top: '0px',
left: '0px',
},
}, [
h('i.fa.fa-arrow-left.fa-lg.cursor-pointer.color-orange', {
onClick: () => props.dispatch(actions.backToAccountDetail(props.activeAddress)),
style: {
marginLeft: '10px',
marginTop: '50px',
},
}),
h('div', {
style: {
position: 'absolute',
left: '44px',
width: '285px',
},
}, [
h(QrView, {key: 'qr', Qr}),
]),
])
default:
log.debug('rendering default, account detail screen')
return h(MainContainer, {key: 'account-detail'})
}
}
App.prototype.toggleMetamaskActive = function () {
if (!this.props.isUnlocked) {
// currently inactive: redirect to password box
var passwordBox = document.querySelector('input[type=password]')
if (!passwordBox) return
passwordBox.focus()
} else {
// currently active: deactivate
this.props.dispatch(actions.lockMetamask(false))
}
}
App.prototype.getConnectingLabel = function () {
const { provider } = this.props
const providerName = provider.type
let name
if (providerName === 'mainnet') {
name = this.context.t('connectingToMainnet')
} else if (providerName === 'ropsten') {
name = this.context.t('connectingToRopsten')
} else if (providerName === 'kovan') {
name = this.context.t('connectingToRopsten')
} else if (providerName === 'rinkeby') {
name = this.context.t('connectingToRinkeby')
} else {
name = this.context.t('connectingToUnknown')
}
return name
} }
App.prototype.getNetworkName = function () { module.exports = compose(
const { provider } = this.props withRouter,
const providerName = provider.type connect(mapStateToProps, mapDispatchToProps)
)(App)
let name
if (providerName === 'mainnet') {
name = this.context.t('mainnet')
} else if (providerName === 'ropsten') {
name = this.context.t('ropsten')
} else if (providerName === 'kovan') {
name = this.context.t('kovan')
} else if (providerName === 'rinkeby') {
name = this.context.t('rinkeby')
} else {
name = this.context.t('unknownNetwork')
}
return name
}

@ -1,20 +1,31 @@
const inherits = require('util').inherits const inherits = require('util').inherits
const Component = require('react').Component const Component = require('react').Component
const PropTypes = require('prop-types')
const connect = require('react-redux').connect const connect = require('react-redux').connect
const { compose } = require('recompose')
const { withRouter } = require('react-router-dom')
const PropTypes = require('prop-types')
const h = require('react-hyperscript') const h = require('react-hyperscript')
const actions = require('../../actions') const actions = require('../../actions')
const { Menu, Item, Divider, CloseArea } = require('../dropdowns/components/menu') const { Menu, Item, Divider, CloseArea } = require('../dropdowns/components/menu')
const Identicon = require('../identicon') const Identicon = require('../identicon')
const { formatBalance } = require('../../util') const { formatBalance } = require('../../util')
const {
SETTINGS_ROUTE,
INFO_ROUTE,
NEW_ACCOUNT_ROUTE,
IMPORT_ACCOUNT_ROUTE,
DEFAULT_ROUTE,
} = require('../../routes')
module.exports = compose(
withRouter,
connect(mapStateToProps, mapDispatchToProps)
)(AccountMenu)
AccountMenu.contextTypes = { AccountMenu.contextTypes = {
t: PropTypes.func, t: PropTypes.func,
} }
module.exports = connect(mapStateToProps, mapDispatchToProps)(AccountMenu)
inherits(AccountMenu, Component) inherits(AccountMenu, Component)
function AccountMenu () { Component.call(this) } function AccountMenu () { Component.call(this) }
@ -25,7 +36,6 @@ function mapStateToProps (state) {
keyrings: state.metamask.keyrings, keyrings: state.metamask.keyrings,
identities: state.metamask.identities, identities: state.metamask.identities,
accounts: state.metamask.accounts, accounts: state.metamask.accounts,
} }
} }
@ -48,11 +58,6 @@ function mapDispatchToProps (dispatch) {
dispatch(actions.hideSidebar()) dispatch(actions.hideSidebar())
dispatch(actions.toggleAccountMenu()) dispatch(actions.toggleAccountMenu())
}, },
showNewAccountPage: (formToSelect) => {
dispatch(actions.showNewAccountPage(formToSelect))
dispatch(actions.hideSidebar())
dispatch(actions.toggleAccountMenu())
},
showInfoPage: () => { showInfoPage: () => {
dispatch(actions.showInfoPage()) dispatch(actions.showInfoPage())
dispatch(actions.hideSidebar()) dispatch(actions.hideSidebar())
@ -65,10 +70,8 @@ AccountMenu.prototype.render = function () {
const { const {
isAccountMenuOpen, isAccountMenuOpen,
toggleAccountMenu, toggleAccountMenu,
showNewAccountPage,
lockMetamask, lockMetamask,
showConfigPage, history,
showInfoPage,
} = this.props } = this.props
return h(Menu, { className: 'account-menu', isShowing: isAccountMenuOpen }, [ return h(Menu, { className: 'account-menu', isShowing: isAccountMenuOpen }, [
@ -78,30 +81,45 @@ AccountMenu.prototype.render = function () {
}, [ }, [
this.context.t('myAccounts'), this.context.t('myAccounts'),
h('button.account-menu__logout-button', { h('button.account-menu__logout-button', {
onClick: lockMetamask, onClick: () => {
lockMetamask()
history.push(DEFAULT_ROUTE)
},
}, this.context.t('logout')), }, this.context.t('logout')),
]), ]),
h(Divider), h(Divider),
h('div.account-menu__accounts', this.renderAccounts()), h('div.account-menu__accounts', this.renderAccounts()),
h(Divider), h(Divider),
h(Item, { h(Item, {
onClick: () => showNewAccountPage('CREATE'), onClick: () => {
toggleAccountMenu()
history.push(NEW_ACCOUNT_ROUTE)
},
icon: h('img.account-menu__item-icon', { src: 'images/plus-btn-white.svg' }), icon: h('img.account-menu__item-icon', { src: 'images/plus-btn-white.svg' }),
text: this.context.t('createAccount'), text: this.context.t('createAccount'),
}), }),
h(Item, { h(Item, {
onClick: () => showNewAccountPage('IMPORT'), onClick: () => {
toggleAccountMenu()
history.push(IMPORT_ACCOUNT_ROUTE)
},
icon: h('img.account-menu__item-icon', { src: 'images/import-account.svg' }), icon: h('img.account-menu__item-icon', { src: 'images/import-account.svg' }),
text: this.context.t('importAccount'), text: this.context.t('importAccount'),
}), }),
h(Divider), h(Divider),
h(Item, { h(Item, {
onClick: showInfoPage, onClick: () => {
toggleAccountMenu()
history.push(INFO_ROUTE)
},
icon: h('img', { src: 'images/mm-info-icon.svg' }), icon: h('img', { src: 'images/mm-info-icon.svg' }),
text: this.context.t('infoHelp'), text: this.context.t('infoHelp'),
}), }),
h(Item, { h(Item, {
onClick: showConfigPage, onClick: () => {
toggleAccountMenu()
history.push(SETTINGS_ROUTE)
},
icon: h('img.account-menu__item-icon', { src: 'images/settings.svg' }), icon: h('img.account-menu__item-icon', { src: 'images/settings.svg' }),
text: this.context.t('settings'), text: this.context.t('settings'),
}), }),

@ -203,18 +203,18 @@ NetworkDropdown.prototype.render = function () {
{ {
key: 'default', key: 'default',
closeMenu: () => this.props.hideNetworkDropdown(), closeMenu: () => this.props.hideNetworkDropdown(),
onClick: () => props.setRpcTarget('http://localhost:8545'), onClick: () => props.setProviderType('localhost'),
style: dropdownMenuItemStyle, style: dropdownMenuItemStyle,
}, },
[ [
activeNetwork === 'http://localhost:8545' ? h('i.fa.fa-check') : h('.network-check__transparent', '✓'), providerType === 'localhost' ? h('i.fa.fa-check') : h('.network-check__transparent', '✓'),
h(NetworkDropdownIcon, { h(NetworkDropdownIcon, {
isSelected: activeNetwork === 'http://localhost:8545', isSelected: providerType === 'localhost',
innerBorder: '1px solid #9b9b9b', innerBorder: '1px solid #9b9b9b',
}), }),
h('span.network-name-item', { h('span.network-name-item', {
style: { style: {
color: activeNetwork === 'http://localhost:8545' ? '#ffffff' : '#9b9b9b', color: providerType === 'localhost' ? '#ffffff' : '#9b9b9b',
}, },
}, this.context.t('localhost')), }, this.context.t('localhost')),
] ]

@ -32,10 +32,10 @@ EnsInput.prototype.render = function () {
const network = this.props.network const network = this.props.network
const networkHasEnsSupport = getNetworkEnsSupport(network) const networkHasEnsSupport = getNetworkEnsSupport(network)
if (!networkHasEnsSupport) return
props.onChange(recipient) props.onChange(recipient)
if (!networkHasEnsSupport) return
if (recipient.match(ensRE) === null) { if (recipient.match(ensRE) === null) {
return this.setState({ return this.setState({
loadingEns: false, loadingEns: false,

@ -7,8 +7,8 @@ const connect = require('react-redux').connect
const R = require('ramda') const R = require('ramda')
const Fuse = require('fuse.js') const Fuse = require('fuse.js')
const contractMap = require('eth-contract-metadata') const contractMap = require('eth-contract-metadata')
const TokenBalance = require('./components/token-balance') const TokenBalance = require('../../components/token-balance')
const Identicon = require('./components/identicon') const Identicon = require('../../components/identicon')
const contractList = Object.entries(contractMap) const contractList = Object.entries(contractMap)
.map(([ _, tokenData]) => tokenData) .map(([ _, tokenData]) => tokenData)
.filter(tokenData => Boolean(tokenData.erc20)) .filter(tokenData => Boolean(tokenData.erc20))
@ -24,9 +24,10 @@ const fuse = new Fuse(contractList, {
{ name: 'symbol', weight: 0.5 }, { name: 'symbol', weight: 0.5 },
], ],
}) })
const actions = require('./actions') const actions = require('../../actions')
const ethUtil = require('ethereumjs-util') const ethUtil = require('ethereumjs-util')
const { tokenInfoGetter } = require('./token-util') const { tokenInfoGetter } = require('../../token-util')
const { DEFAULT_ROUTE } = require('../../routes')
const emptyAddr = '0x0000000000000000000000000000000000000000' const emptyAddr = '0x0000000000000000000000000000000000000000'
@ -47,7 +48,6 @@ function mapStateToProps (state) {
function mapDispatchToProps (dispatch) { function mapDispatchToProps (dispatch) {
return { return {
goHome: () => dispatch(actions.goHome()),
addTokens: tokens => dispatch(actions.addTokens(tokens)), addTokens: tokens => dispatch(actions.addTokens(tokens)),
} }
} }
@ -56,6 +56,7 @@ inherits(AddTokenScreen, Component)
function AddTokenScreen () { function AddTokenScreen () {
this.state = { this.state = {
isShowingConfirmation: false, isShowingConfirmation: false,
isShowingInfoBox: true,
customAddress: '', customAddress: '',
customSymbol: '', customSymbol: '',
customDecimals: '', customDecimals: '',
@ -295,7 +296,7 @@ AddTokenScreen.prototype.renderConfirmation = function () {
selectedTokens, selectedTokens,
} = this.state } = this.state
const { addTokens, goHome } = this.props const { addTokens, history } = this.props
const customToken = { const customToken = {
address, address,
@ -310,9 +311,6 @@ AddTokenScreen.prototype.renderConfirmation = function () {
return ( return (
h('div.add-token', [ h('div.add-token', [
h('div.add-token__wrapper', [ h('div.add-token__wrapper', [
h('div.add-token__title-container.add-token__confirmation-title', [
h('div.add-token__description', this.context.t('likeToAddTokens')),
]),
h('div.add-token__content-container.add-token__confirmation-content', [ h('div.add-token__content-container.add-token__confirmation-content', [
h('div.add-token__description.add-token__confirmation-description', this.context.t('balances')), h('div.add-token__description.add-token__confirmation-description', this.context.t('balances')),
h('div.add-token__confirmation-token-list', h('div.add-token__confirmation-token-list',
@ -335,7 +333,7 @@ AddTokenScreen.prototype.renderConfirmation = function () {
onClick: () => this.setState({ isShowingConfirmation: false }), onClick: () => this.setState({ isShowingConfirmation: false }),
}, this.context.t('back')), }, this.context.t('back')),
h('button.btn-primary--lg', { h('button.btn-primary--lg', {
onClick: () => addTokens(tokens).then(goHome), onClick: () => addTokens(tokens).then(() => history.push(DEFAULT_ROUTE)),
}, this.context.t('addTokens')), }, this.context.t('addTokens')),
]), ]),
]) ])
@ -347,18 +345,23 @@ AddTokenScreen.prototype.displayTab = function (selectedTab) {
} }
AddTokenScreen.prototype.renderTabs = function () { AddTokenScreen.prototype.renderTabs = function () {
const { displayedTab, errors } = this.state const { isShowingInfoBox, displayedTab, errors } = this.state
return displayedTab === 'CUSTOM_TOKEN' return displayedTab === 'CUSTOM_TOKEN'
? this.renderCustomForm() ? this.renderCustomForm()
: h('div', [ : h('div', [
h('div.add-token__wrapper', [ h('div.add-token__wrapper', [
h('div.add-token__content-container', [ h('div.add-token__content-container', [
h('div.add-token__info-box', [ isShowingInfoBox && h('div.add-token__info-box', [
h('div.add-token__info-box__close'), h('div.add-token__info-box__close', {
onClick: () => this.setState({ isShowingInfoBox: false }),
}),
h('div.add-token__info-box__title', this.context.t('whatsThis')), h('div.add-token__info-box__title', this.context.t('whatsThis')),
h('div.add-token__info-box__copy', this.context.t('keepTrackTokens')), h('div.add-token__info-box__copy', this.context.t('keepTrackTokens')),
h('div.add-token__info-box__copy--blue', this.context.t('learnMore')), h('a.add-token__info-box__copy--blue', {
href: 'http://metamask.helpscoutdocs.com/article/16-managing-erc20-tokens',
target: '_blank',
}, this.context.t('learnMore')),
]), ]),
h('div.add-token__input-container', [ h('div.add-token__input-container', [
h('input.add-token__input', { h('input.add-token__input', {
@ -379,23 +382,24 @@ AddTokenScreen.prototype.render = function () {
isShowingConfirmation, isShowingConfirmation,
displayedTab, displayedTab,
} = this.state } = this.state
const { goHome } = this.props const { history } = this.props
return h('div.add-token', [ return h('div.add-token', [
h('div.add-token__header', [ h('div.add-token__header', [
h('div.add-token__header__cancel', { h('div.add-token__header__cancel', {
onClick: () => goHome(), onClick: () => history.push(DEFAULT_ROUTE),
}, [ }, [
h('i.fa.fa-angle-left.fa-lg'), h('i.fa.fa-angle-left.fa-lg'),
h('span', this.context.t('cancel')), h('span', this.context.t('cancel')),
]), ]),
h('div.add-token__header__title', this.context.t('addTokens')), h('div.add-token__header__title', this.context.t('addTokens')),
isShowingConfirmation && h('div.add-token__header__subtitle', this.context.t('likeToAddTokens')),
!isShowingConfirmation && h('div.add-token__header__tabs', [ !isShowingConfirmation && h('div.add-token__header__tabs', [
h('div.add-token__header__tabs__tab', { h('div.add-token__header__tabs__tab', {
className: classnames('add-token__header__tabs__tab', { className: classnames('add-token__header__tabs__tab', {
'add-token__header__tabs__selected': displayedTab === 'SEARCH', 'add-token__header__tabs__selected': displayedTab === 'SEARCH',
'add-token__header__tabs__unselected cursor-pointer': displayedTab !== 'SEARCH', 'add-token__header__tabs__unselected': displayedTab !== 'SEARCH',
}), }),
onClick: () => this.displayTab('SEARCH'), onClick: () => this.displayTab('SEARCH'),
}, this.context.t('search')), }, this.context.t('search')),
@ -403,21 +407,21 @@ AddTokenScreen.prototype.render = function () {
h('div.add-token__header__tabs__tab', { h('div.add-token__header__tabs__tab', {
className: classnames('add-token__header__tabs__tab', { className: classnames('add-token__header__tabs__tab', {
'add-token__header__tabs__selected': displayedTab === 'CUSTOM_TOKEN', 'add-token__header__tabs__selected': displayedTab === 'CUSTOM_TOKEN',
'add-token__header__tabs__unselected cursor-pointer': displayedTab !== 'CUSTOM_TOKEN', 'add-token__header__tabs__unselected': displayedTab !== 'CUSTOM_TOKEN',
}), }),
onClick: () => this.displayTab('CUSTOM_TOKEN'), onClick: () => this.displayTab('CUSTOM_TOKEN'),
}, this.context.t('customToken')), }, this.context.t('customToken')),
]), ]),
]), ]),
//
isShowingConfirmation isShowingConfirmation
? this.renderConfirmation() ? this.renderConfirmation()
: this.renderTabs(), : this.renderTabs(),
!isShowingConfirmation && h('div.add-token__buttons', [ !isShowingConfirmation && h('div.add-token__buttons', [
h('button.btn-secondary--lg.add-token__cancel-button', { h('button.btn-secondary--lg.add-token__cancel-button', {
onClick: goHome, onClick: () => history.push(DEFAULT_ROUTE),
}, this.context.t('cancel')), }, this.context.t('cancel')),
h('button.btn-primary--lg.add-token__confirm-button', { h('button.btn-primary--lg.add-token__confirm-button', {
onClick: this.onNext, onClick: this.onNext,

@ -0,0 +1,34 @@
const { connect } = require('react-redux')
const PropTypes = require('prop-types')
const { Redirect } = require('react-router-dom')
const h = require('react-hyperscript')
const MetamaskRoute = require('./metamask-route')
const { UNLOCK_ROUTE, INITIALIZE_ROUTE } = require('../../routes')
const Authenticated = props => {
const { isUnlocked, isInitialized } = props
switch (true) {
case isUnlocked && isInitialized:
return h(MetamaskRoute, { ...props })
case !isInitialized:
return h(Redirect, { to: { pathname: INITIALIZE_ROUTE } })
default:
return h(Redirect, { to: { pathname: UNLOCK_ROUTE } })
}
}
Authenticated.propTypes = {
isUnlocked: PropTypes.bool,
isInitialized: PropTypes.bool,
}
const mapStateToProps = state => {
const { metamask: { isUnlocked, isInitialized } } = state
return {
isUnlocked,
isInitialized,
}
}
module.exports = connect(mapStateToProps)(Authenticated)

@ -1,11 +1,12 @@
const Component = require('react').Component const Component = require('react').Component
const PropTypes = require('prop-types') const PropTypes = require('prop-types')
const h = require('react-hyperscript') const h = require('react-hyperscript')
const { withRouter } = require('react-router-dom')
const { compose } = require('recompose')
const connect = require('react-redux').connect const connect = require('react-redux').connect
const actions = require('../../actions') const actions = require('../../../../actions')
const FileInput = require('react-simple-file-input').default const FileInput = require('react-simple-file-input').default
const { DEFAULT_ROUTE } = require('../../../../routes')
const HELP_LINK = 'https://support.metamask.io/kb/article/7-importing-accounts' const HELP_LINK = 'https://support.metamask.io/kb/article/7-importing-accounts'
class JsonImportSubview extends Component { class JsonImportSubview extends Component {
@ -51,7 +52,7 @@ class JsonImportSubview extends Component {
h('div.new-account-create-form__buttons', {}, [ h('div.new-account-create-form__buttons', {}, [
h('button.btn-secondary.new-account-create-form__button', { h('button.btn-secondary.new-account-create-form__button', {
onClick: () => this.props.goHome(), onClick: () => this.props.history.push(DEFAULT_ROUTE),
}, [ }, [
this.context.t('cancel'), this.context.t('cancel'),
]), ]),
@ -112,6 +113,7 @@ JsonImportSubview.propTypes = {
goHome: PropTypes.func, goHome: PropTypes.func,
displayWarning: PropTypes.func, displayWarning: PropTypes.func,
importNewJsonAccount: PropTypes.func, importNewJsonAccount: PropTypes.func,
history: PropTypes.object,
t: PropTypes.func, t: PropTypes.func,
} }
@ -133,5 +135,7 @@ JsonImportSubview.contextTypes = {
t: PropTypes.func, t: PropTypes.func,
} }
module.exports = connect(mapStateToProps, mapDispatchToProps)(JsonImportSubview) module.exports = compose(
withRouter,
connect(mapStateToProps, mapDispatchToProps)
)(JsonImportSubview)

@ -1,15 +1,21 @@
const inherits = require('util').inherits const inherits = require('util').inherits
const Component = require('react').Component const Component = require('react').Component
const h = require('react-hyperscript') const h = require('react-hyperscript')
const { withRouter } = require('react-router-dom')
const { compose } = require('recompose')
const PropTypes = require('prop-types') const PropTypes = require('prop-types')
const connect = require('react-redux').connect const connect = require('react-redux').connect
const actions = require('../../actions') const actions = require('../../../../actions')
const { DEFAULT_ROUTE } = require('../../../../routes')
PrivateKeyImportView.contextTypes = { PrivateKeyImportView.contextTypes = {
t: PropTypes.func, t: PropTypes.func,
} }
module.exports = connect(mapStateToProps, mapDispatchToProps)(PrivateKeyImportView) module.exports = compose(
withRouter,
connect(mapStateToProps, mapDispatchToProps)
)(PrivateKeyImportView)
function mapStateToProps (state) { function mapStateToProps (state) {
@ -20,9 +26,8 @@ function mapStateToProps (state) {
function mapDispatchToProps (dispatch) { function mapDispatchToProps (dispatch) {
return { return {
goHome: () => dispatch(actions.goHome()),
importNewAccount: (strategy, [ privateKey ]) => { importNewAccount: (strategy, [ privateKey ]) => {
dispatch(actions.importNewAccount(strategy, [ privateKey ])) return dispatch(actions.importNewAccount(strategy, [ privateKey ]))
}, },
displayWarning: () => dispatch(actions.displayWarning(null)), displayWarning: () => dispatch(actions.displayWarning(null)),
} }
@ -30,11 +35,12 @@ function mapDispatchToProps (dispatch) {
inherits(PrivateKeyImportView, Component) inherits(PrivateKeyImportView, Component)
function PrivateKeyImportView () { function PrivateKeyImportView () {
this.createKeyringOnEnter = this.createKeyringOnEnter.bind(this)
Component.call(this) Component.call(this)
} }
PrivateKeyImportView.prototype.render = function () { PrivateKeyImportView.prototype.render = function () {
const { error, goHome } = this.props const { error } = this.props
return ( return (
h('div.new-account-import-form__private-key', [ h('div.new-account-import-form__private-key', [
@ -46,7 +52,7 @@ PrivateKeyImportView.prototype.render = function () {
h('input.new-account-import-form__input-password', { h('input.new-account-import-form__input-password', {
type: 'password', type: 'password',
id: 'private-key-box', id: 'private-key-box',
onKeyPress: () => this.createKeyringOnEnter(), onKeyPress: e => this.createKeyringOnEnter(e),
}), }),
]), ]),
@ -54,7 +60,7 @@ PrivateKeyImportView.prototype.render = function () {
h('div.new-account-import-form__buttons', {}, [ h('div.new-account-import-form__buttons', {}, [
h('button.btn-secondary--lg.new-account-create-form__button', { h('button.btn-secondary--lg.new-account-create-form__button', {
onClick: () => goHome(), onClick: () => this.props.history.push(DEFAULT_ROUTE),
}, [ }, [
this.context.t('cancel'), this.context.t('cancel'),
]), ]),
@ -82,6 +88,8 @@ PrivateKeyImportView.prototype.createKeyringOnEnter = function (event) {
PrivateKeyImportView.prototype.createNewKeychain = function () { PrivateKeyImportView.prototype.createNewKeychain = function () {
const input = document.getElementById('private-key-box') const input = document.getElementById('private-key-box')
const privateKey = input.value const privateKey = input.value
const { importNewAccount, history } = this.props
this.props.importNewAccount('Private Key', [ privateKey ]) importNewAccount('Private Key', [ privateKey ])
.then(() => history.push(DEFAULT_ROUTE))
} }

@ -0,0 +1,81 @@
const Component = require('react').Component
const { Switch, Route, matchPath } = require('react-router-dom')
const PropTypes = require('prop-types')
const h = require('react-hyperscript')
const connect = require('react-redux').connect
const actions = require('../../../actions')
const { getCurrentViewContext } = require('../../../selectors')
const classnames = require('classnames')
const NewAccountCreateForm = require('./new-account')
const NewAccountImportForm = require('./import-account')
const { NEW_ACCOUNT_ROUTE, IMPORT_ACCOUNT_ROUTE } = require('../../../routes')
class CreateAccountPage extends Component {
renderTabs () {
const { history, location } = this.props
return h('div.new-account__tabs', [
h('div.new-account__tabs__tab', {
className: classnames('new-account__tabs__tab', {
'new-account__tabs__selected': matchPath(location.pathname, {
path: NEW_ACCOUNT_ROUTE, exact: true,
}),
}),
onClick: () => history.push(NEW_ACCOUNT_ROUTE),
}, 'Create'),
h('div.new-account__tabs__tab', {
className: classnames('new-account__tabs__tab', {
'new-account__tabs__selected': matchPath(location.pathname, {
path: IMPORT_ACCOUNT_ROUTE, exact: true,
}),
}),
onClick: () => history.push(IMPORT_ACCOUNT_ROUTE),
}, 'Import'),
])
}
render () {
return h('div.new-account', {}, [
h('div.new-account__header', [
h('div.new-account__title', 'New Account'),
this.renderTabs(),
]),
h('div.new-account__form', [
h(Switch, [
h(Route, {
exact: true,
path: NEW_ACCOUNT_ROUTE,
component: NewAccountCreateForm,
}),
h(Route, {
exact: true,
path: IMPORT_ACCOUNT_ROUTE,
component: NewAccountImportForm,
}),
]),
]),
])
}
}
CreateAccountPage.propTypes = {
location: PropTypes.object,
history: PropTypes.object,
}
const mapStateToProps = state => ({
displayedForm: getCurrentViewContext(state),
})
const mapDispatchToProps = dispatch => ({
displayForm: form => dispatch(actions.setNewAccountForm(form)),
showQrView: (selected, identity) => dispatch(actions.showQrView(selected, identity)),
showExportPrivateKeyModal: () => {
dispatch(actions.showModal({ name: 'EXPORT_PRIVATE_KEY' }))
},
hideModal: () => dispatch(actions.hideModal()),
saveAccountLabel: (address, label) => dispatch(actions.saveAccountLabel(address, label)),
})
module.exports = connect(mapStateToProps, mapDispatchToProps)(CreateAccountPage)

@ -2,7 +2,8 @@ const { Component } = require('react')
const PropTypes = require('prop-types') const PropTypes = require('prop-types')
const h = require('react-hyperscript') const h = require('react-hyperscript')
const connect = require('react-redux').connect const connect = require('react-redux').connect
const actions = require('../../actions') const actions = require('../../../actions')
const { DEFAULT_ROUTE } = require('../../../routes')
class NewAccountCreateForm extends Component { class NewAccountCreateForm extends Component {
constructor (props, context) { constructor (props, context) {
@ -19,7 +20,7 @@ class NewAccountCreateForm extends Component {
render () { render () {
const { newAccountName, defaultAccountName } = this.state const { newAccountName, defaultAccountName } = this.state
const { history, createAccount } = this.props
return h('div.new-account-create-form', [ return h('div.new-account-create-form', [
@ -38,13 +39,16 @@ class NewAccountCreateForm extends Component {
h('div.new-account-create-form__buttons', {}, [ h('div.new-account-create-form__buttons', {}, [
h('button.btn-secondary--lg.new-account-create-form__button', { h('button.btn-secondary--lg.new-account-create-form__button', {
onClick: () => this.props.goHome(), onClick: () => history.push(DEFAULT_ROUTE),
}, [ }, [
this.context.t('cancel'), this.context.t('cancel'),
]), ]),
h('button.btn-primary--lg.new-account-create-form__button', { h('button.btn-primary--lg.new-account-create-form__button', {
onClick: () => this.props.createAccount(newAccountName || defaultAccountName), onClick: () => {
createAccount(newAccountName || defaultAccountName)
.then(() => history.push(DEFAULT_ROUTE))
},
}, [ }, [
this.context.t('create'), this.context.t('create'),
]), ]),
@ -59,8 +63,8 @@ NewAccountCreateForm.propTypes = {
hideModal: PropTypes.func, hideModal: PropTypes.func,
showImportPage: PropTypes.func, showImportPage: PropTypes.func,
createAccount: PropTypes.func, createAccount: PropTypes.func,
goHome: PropTypes.func,
numberOfExistingAccounts: PropTypes.number, numberOfExistingAccounts: PropTypes.number,
history: PropTypes.object,
t: PropTypes.func, t: PropTypes.func,
} }
@ -77,23 +81,17 @@ const mapStateToProps = state => {
const mapDispatchToProps = dispatch => { const mapDispatchToProps = dispatch => {
return { return {
toCoinbase: (address) => { toCoinbase: address => dispatch(actions.buyEth({ network: '1', address, amount: 0 })),
dispatch(actions.buyEth({ network: '1', address, amount: 0 })) hideModal: () => dispatch(actions.hideModal()),
}, createAccount: newAccountName => {
hideModal: () => { return dispatch(actions.addNewAccount())
dispatch(actions.hideModal()) .then(newAccountAddress => {
},
createAccount: (newAccountName) => {
dispatch(actions.addNewAccount())
.then((newAccountAddress) => {
if (newAccountName) { if (newAccountName) {
dispatch(actions.saveAccountLabel(newAccountAddress, newAccountName)) dispatch(actions.saveAccountLabel(newAccountAddress, newAccountName))
} }
dispatch(actions.goHome())
}) })
}, },
showImportPage: () => dispatch(actions.showImportPage()), showImportPage: () => dispatch(actions.showImportPage()),
goHome: () => dispatch(actions.goHome()),
} }
} }

@ -0,0 +1,332 @@
const { Component } = require('react')
const PropTypes = require('prop-types')
const connect = require('../../metamask-connect')
const { Redirect, withRouter } = require('react-router-dom')
const { compose } = require('recompose')
const h = require('react-hyperscript')
const actions = require('../../actions')
// init
const NewKeyChainScreen = require('../../new-keychain')
// mascara
const MascaraBuyEtherScreen = require('../../../../mascara/src/app/first-time/buy-ether-screen').default
// accounts
const MainContainer = require('../../main-container')
// other views
const BuyView = require('../../components/buy-button-subview')
const QrView = require('../../components/qr-code')
// Routes
const {
REVEAL_SEED_ROUTE,
RESTORE_VAULT_ROUTE,
CONFIRM_TRANSACTION_ROUTE,
NOTICE_ROUTE,
} = require('../../routes')
class Home extends Component {
componentDidMount () {
const {
history,
unapprovedTxs = {},
unapprovedMsgCount = 0,
unapprovedPersonalMsgCount = 0,
unapprovedTypedMessagesCount = 0,
} = this.props
// unapprovedTxs and unapproved messages
if (Object.keys(unapprovedTxs).length ||
unapprovedTypedMessagesCount + unapprovedMsgCount + unapprovedPersonalMsgCount > 0) {
history.push(CONFIRM_TRANSACTION_ROUTE)
}
}
render () {
log.debug('rendering primary')
const {
noActiveNotices,
lostAccounts,
forgottenPassword,
currentView,
activeAddress,
seedWords,
} = this.props
// notices
if (!noActiveNotices || (lostAccounts && lostAccounts.length > 0)) {
return h(Redirect, {
to: {
pathname: NOTICE_ROUTE,
},
})
}
// seed words
if (seedWords) {
log.debug('rendering seed words')
return h(Redirect, {
to: {
pathname: REVEAL_SEED_ROUTE,
},
})
}
if (forgottenPassword) {
log.debug('rendering restore vault screen')
return h(Redirect, {
to: {
pathname: RESTORE_VAULT_ROUTE,
},
})
}
// if (!props.noActiveNotices) {
// log.debug('rendering notice screen for unread notices.')
// return h(NoticeScreen, {
// notice: props.lastUnreadNotice,
// key: 'NoticeScreen',
// onConfirm: () => props.dispatch(actions.markNoticeRead(props.lastUnreadNotice)),
// })
// } else if (props.lostAccounts && props.lostAccounts.length > 0) {
// log.debug('rendering notice screen for lost accounts view.')
// return h(NoticeScreen, {
// notice: generateLostAccountsNotice(props.lostAccounts),
// key: 'LostAccountsNotice',
// onConfirm: () => props.dispatch(actions.markAccountsFound()),
// })
// }
// if (props.seedWords) {
// log.debug('rendering seed words')
// return h(HDCreateVaultComplete, {key: 'HDCreateVaultComplete'})
// }
// show initialize screen
// if (!isInitialized || forgottenPassword) {
// // show current view
// log.debug('rendering an initialize screen')
// // switch (props.currentView.name) {
// // case 'restoreVault':
// // log.debug('rendering restore vault screen')
// // return h(HDRestoreVaultScreen, {key: 'HDRestoreVaultScreen'})
// // default:
// // log.debug('rendering menu screen')
// // return h(InitializeScreen, {key: 'menuScreenInit'})
// // }
// }
// // show unlock screen
// if (!props.isUnlocked) {
// return h(MainContainer, {
// currentViewName: props.currentView.name,
// isUnlocked: props.isUnlocked,
// })
// }
// show current view
switch (currentView.name) {
case 'accountDetail':
log.debug('rendering main container')
return h(MainContainer, {key: 'account-detail'})
// case 'sendTransaction':
// log.debug('rendering send tx screen')
// // Going to leave this here until we are ready to delete SendTransactionScreen v1
// // const SendComponentToRender = checkFeatureToggle('send-v2')
// // ? SendTransactionScreen2
// // : SendTransactionScreen
// return h(SendTransactionScreen2, {key: 'send-transaction'})
// case 'sendToken':
// log.debug('rendering send token screen')
// // Going to leave this here until we are ready to delete SendTransactionScreen v1
// // const SendTokenComponentToRender = checkFeatureToggle('send-v2')
// // ? SendTransactionScreen2
// // : SendTokenScreen
// return h(SendTransactionScreen2, {key: 'sendToken'})
case 'newKeychain':
log.debug('rendering new keychain screen')
return h(NewKeyChainScreen, {key: 'new-keychain'})
// case 'confTx':
// log.debug('rendering confirm tx screen')
// return h(Redirect, {
// to: {
// pathname: CONFIRM_TRANSACTION_ROUTE,
// },
// })
// return h(ConfirmTxScreen, {key: 'confirm-tx'})
// case 'add-token':
// log.debug('rendering add-token screen from unlock screen.')
// return h(AddTokenScreen, {key: 'add-token'})
// case 'config':
// log.debug('rendering config screen')
// return h(Settings, {key: 'config'})
// case 'import-menu':
// log.debug('rendering import screen')
// return h(Import, {key: 'import-menu'})
// case 'reveal-seed-conf':
// log.debug('rendering reveal seed confirmation screen')
// return h(RevealSeedConfirmation, {key: 'reveal-seed-conf'})
// case 'info':
// log.debug('rendering info screen')
// return h(Settings, {key: 'info', tab: 'info'})
case 'buyEth':
log.debug('rendering buy ether screen')
return h(BuyView, {key: 'buyEthView'})
case 'onboardingBuyEth':
log.debug('rendering onboarding buy ether screen')
return h(MascaraBuyEtherScreen, {key: 'buyEthView'})
case 'qr':
log.debug('rendering show qr screen')
return h('div', {
style: {
position: 'absolute',
height: '100%',
top: '0px',
left: '0px',
},
}, [
h('i.fa.fa-arrow-left.fa-lg.cursor-pointer.color-orange', {
onClick: () => this.props.dispatch(actions.backToAccountDetail(activeAddress)),
style: {
marginLeft: '10px',
marginTop: '50px',
},
}),
h('div', {
style: {
position: 'absolute',
left: '44px',
width: '285px',
},
}, [
h(QrView, {key: 'qr'}),
]),
])
default:
log.debug('rendering default, account detail screen')
return h(MainContainer, {key: 'account-detail'})
}
}
}
Home.propTypes = {
currentCurrency: PropTypes.string,
isLoading: PropTypes.bool,
loadingMessage: PropTypes.string,
network: PropTypes.string,
provider: PropTypes.object,
frequentRpcList: PropTypes.array,
currentView: PropTypes.object,
sidebarOpen: PropTypes.bool,
isMascara: PropTypes.bool,
isOnboarding: PropTypes.bool,
isUnlocked: PropTypes.bool,
networkDropdownOpen: PropTypes.bool,
history: PropTypes.object,
dispatch: PropTypes.func,
selectedAddress: PropTypes.string,
noActiveNotices: PropTypes.bool,
lostAccounts: PropTypes.array,
isInitialized: PropTypes.bool,
forgottenPassword: PropTypes.bool,
activeAddress: PropTypes.string,
unapprovedTxs: PropTypes.object,
seedWords: PropTypes.string,
unapprovedMsgCount: PropTypes.number,
unapprovedPersonalMsgCount: PropTypes.number,
unapprovedTypedMessagesCount: PropTypes.number,
welcomeScreenSeen: PropTypes.bool,
isPopup: PropTypes.bool,
isMouseUser: PropTypes.bool,
t: PropTypes.func,
}
function mapStateToProps (state) {
const { appState, metamask } = state
const {
networkDropdownOpen,
sidebarOpen,
isLoading,
loadingMessage,
} = appState
const {
accounts,
address,
isInitialized,
noActiveNotices,
seedWords,
unapprovedTxs,
lastUnreadNotice,
lostAccounts,
unapprovedMsgCount,
unapprovedPersonalMsgCount,
unapprovedTypedMessagesCount,
} = metamask
const selected = address || Object.keys(accounts)[0]
return {
// state from plugin
networkDropdownOpen,
sidebarOpen,
isLoading,
loadingMessage,
noActiveNotices,
isInitialized,
isUnlocked: state.metamask.isUnlocked,
selectedAddress: state.metamask.selectedAddress,
currentView: state.appState.currentView,
activeAddress: state.appState.activeAddress,
transForward: state.appState.transForward,
isMascara: state.metamask.isMascara,
isOnboarding: Boolean(!noActiveNotices || seedWords || !isInitialized),
isPopup: state.metamask.isPopup,
seedWords: state.metamask.seedWords,
unapprovedTxs,
unapprovedMsgs: state.metamask.unapprovedMsgs,
unapprovedMsgCount,
unapprovedPersonalMsgCount,
unapprovedTypedMessagesCount,
menuOpen: state.appState.menuOpen,
network: state.metamask.network,
provider: state.metamask.provider,
forgottenPassword: state.appState.forgottenPassword,
lastUnreadNotice,
lostAccounts,
frequentRpcList: state.metamask.frequentRpcList || [],
currentCurrency: state.metamask.currentCurrency,
isMouseUser: state.appState.isMouseUser,
isRevealingSeedWords: state.metamask.isRevealingSeedWords,
Qr: state.appState.Qr,
welcomeScreenSeen: state.metamask.welcomeScreenSeen,
// state needed to get account dropdown temporarily rendering from app bar
selected,
}
}
module.exports = compose(
withRouter,
connect(mapStateToProps)
)(Home)

@ -0,0 +1,25 @@
const { connect } = require('react-redux')
const PropTypes = require('prop-types')
const { Redirect } = require('react-router-dom')
const h = require('react-hyperscript')
const { INITIALIZE_ROUTE } = require('../../routes')
const MetamaskRoute = require('./metamask-route')
const Initialized = props => {
return props.isInitialized
? h(MetamaskRoute, { ...props })
: h(Redirect, { to: { pathname: INITIALIZE_ROUTE } })
}
Initialized.propTypes = {
isInitialized: PropTypes.bool,
}
const mapStateToProps = state => {
const { metamask: { isInitialized } } = state
return {
isInitialized,
}
}
module.exports = connect(mapStateToProps)(Initialized)

@ -0,0 +1,177 @@
const { withRouter } = require('react-router-dom')
const PropTypes = require('prop-types')
const { compose } = require('recompose')
const PersistentForm = require('../../../../lib/persistent-form')
const connect = require('../../../metamask-connect')
const h = require('react-hyperscript')
const { createNewVaultAndRestore, unMarkPasswordForgotten } = require('../../../actions')
const { DEFAULT_ROUTE } = require('../../../routes')
class RestoreVaultPage extends PersistentForm {
constructor (props) {
super(props)
this.state = {
error: null,
}
}
createOnEnter (event) {
if (event.key === 'Enter') {
this.createNewVaultAndRestore()
}
}
cancel () {
this.props.unMarkPasswordForgotten()
.then(this.props.history.push(DEFAULT_ROUTE))
}
createNewVaultAndRestore () {
this.setState({ error: null })
// check password
var passwordBox = document.getElementById('password-box')
var password = passwordBox.value
var passwordConfirmBox = document.getElementById('password-box-confirm')
var passwordConfirm = passwordConfirmBox.value
if (password.length < 8) {
this.setState({ error: 'Password not long enough' })
return
}
if (password !== passwordConfirm) {
this.setState({ error: 'Passwords don\'t match' })
return
}
// check seed
var seedBox = document.querySelector('textarea.twelve-word-phrase')
var seed = seedBox.value.trim()
if (seed.split(' ').length !== 12) {
this.setState({ error: 'Seed phrases are 12 words long' })
return
}
// submit
this.props.createNewVaultAndRestore(password, seed)
.then(() => this.props.history.push(DEFAULT_ROUTE))
.catch(({ message }) => {
this.setState({ error: message })
log.error(message)
})
}
render () {
const { error } = this.state
this.persistentFormParentId = 'restore-vault-form'
return (
h('.initialize-screen.flex-column.flex-center.flex-grow', [
h('h3.flex-center.text-transform-uppercase', {
style: {
background: '#EBEBEB',
color: '#AEAEAE',
marginBottom: 24,
width: '100%',
fontSize: '20px',
padding: 6,
},
}, [
this.props.t('restoreVault'),
]),
// wallet seed entry
h('h3', 'Wallet Seed'),
h('textarea.twelve-word-phrase.letter-spacey', {
dataset: {
persistentFormId: 'wallet-seed',
},
placeholder: this.props.t('secretPhrase'),
}),
// password
h('input.large-input.letter-spacey', {
type: 'password',
id: 'password-box',
placeholder: this.props.t('newPassword8Chars'),
dataset: {
persistentFormId: 'password',
},
style: {
width: 260,
marginTop: 12,
},
}),
// confirm password
h('input.large-input.letter-spacey', {
type: 'password',
id: 'password-box-confirm',
placeholder: this.props.t('confirmPassword'),
onKeyPress: this.createOnEnter.bind(this),
dataset: {
persistentFormId: 'password-confirmation',
},
style: {
width: 260,
marginTop: 16,
},
}),
error && (
h('span.error.in-progress-notification', error)
),
// submit
h('.flex-row.flex-space-between', {
style: {
marginTop: 30,
width: '50%',
},
}, [
// cancel
h('button.primary', {
onClick: () => this.cancel(),
}, this.props.t('cancel')),
// submit
h('button.primary', {
onClick: this.createNewVaultAndRestore.bind(this),
}, this.props.t('ok')),
]),
])
)
}
}
RestoreVaultPage.propTypes = {
history: PropTypes.object,
}
const mapStateToProps = state => {
const { appState: { warning, forgottenPassword } } = state
return {
warning,
forgottenPassword,
}
}
const mapDispatchToProps = dispatch => {
return {
createNewVaultAndRestore: (password, seed) => {
return dispatch(createNewVaultAndRestore(password, seed))
},
unMarkPasswordForgotten: () => dispatch(unMarkPasswordForgotten()),
}
}
module.exports = compose(
withRouter,
connect(mapStateToProps, mapDispatchToProps)
)(RestoreVaultPage)

@ -0,0 +1,195 @@
const { Component } = require('react')
const { connect } = require('react-redux')
const PropTypes = require('prop-types')
const h = require('react-hyperscript')
const { exportAsFile } = require('../../../util')
const { requestRevealSeed, confirmSeedWords } = require('../../../actions')
const { DEFAULT_ROUTE } = require('../../../routes')
class RevealSeedPage extends Component {
componentDidMount () {
const passwordBox = document.getElementById('password-box')
if (passwordBox) {
passwordBox.focus()
}
}
checkConfirmation (event) {
if (event.key === 'Enter') {
event.preventDefault()
this.revealSeedWords()
}
}
revealSeedWords () {
const password = document.getElementById('password-box').value
this.props.requestRevealSeed(password)
}
renderSeed () {
const { seedWords, confirmSeedWords, history } = this.props
return (
h('.initialize-screen.flex-column.flex-center.flex-grow', [
h('h3.flex-center.text-transform-uppercase', {
style: {
background: '#EBEBEB',
color: '#AEAEAE',
marginTop: 36,
marginBottom: 8,
width: '100%',
fontSize: '20px',
padding: 6,
},
}, [
'Vault Created',
]),
h('div', {
style: {
fontSize: '1em',
marginTop: '10px',
textAlign: 'center',
},
}, [
h('span.error', 'These 12 words are the only way to restore your MetaMask accounts.\nSave them somewhere safe and secret.'),
]),
h('textarea.twelve-word-phrase', {
readOnly: true,
value: seedWords,
}),
h('button.primary', {
onClick: () => confirmSeedWords().then(() => history.push(DEFAULT_ROUTE)),
style: {
margin: '24px',
fontSize: '0.9em',
marginBottom: '10px',
},
}, 'I\'ve copied it somewhere safe'),
h('button.primary', {
onClick: () => exportAsFile(`MetaMask Seed Words`, seedWords),
style: {
margin: '10px',
fontSize: '0.9em',
},
}, 'Save Seed Words As File'),
])
)
}
renderConfirmation () {
const { history, warning, inProgress } = this.props
return (
h('.initialize-screen.flex-column.flex-center.flex-grow', {
style: { maxWidth: '420px' },
}, [
h('h3.flex-center.text-transform-uppercase', {
style: {
background: '#EBEBEB',
color: '#AEAEAE',
marginBottom: 24,
width: '100%',
fontSize: '20px',
padding: 6,
},
}, [
'Reveal Seed Words',
]),
h('.div', {
style: {
display: 'flex',
flexDirection: 'column',
padding: '20px',
justifyContent: 'center',
},
}, [
h('h4', 'Do not recover your seed words in a public place! These words can be used to steal all your accounts.'),
// confirmation
h('input.large-input.letter-spacey', {
type: 'password',
id: 'password-box',
placeholder: 'Enter your password to confirm',
onKeyPress: this.checkConfirmation.bind(this),
style: {
width: 260,
marginTop: '12px',
},
}),
h('.flex-row.flex-start', {
style: {
marginTop: 30,
width: '50%',
},
}, [
// cancel
h('button.primary', {
onClick: () => history.push(DEFAULT_ROUTE),
}, 'CANCEL'),
// submit
h('button.primary', {
style: { marginLeft: '10px' },
onClick: this.revealSeedWords.bind(this),
}, 'OK'),
]),
warning && (
h('span.error', {
style: {
margin: '20px',
},
}, warning.split('-'))
),
inProgress && (
h('span.in-progress-notification', 'Generating Seed...')
),
]),
])
)
}
render () {
return this.props.seedWords
? this.renderSeed()
: this.renderConfirmation()
}
}
RevealSeedPage.propTypes = {
requestRevealSeed: PropTypes.func,
confirmSeedWords: PropTypes.func,
seedWords: PropTypes.string,
inProgress: PropTypes.bool,
history: PropTypes.object,
warning: PropTypes.string,
}
const mapStateToProps = state => {
const { appState: { warning }, metamask: { seedWords } } = state
return {
warning,
seedWords,
}
}
const mapDispatchToProps = dispatch => {
return {
requestRevealSeed: password => dispatch(requestRevealSeed(password)),
confirmSeedWords: () => dispatch(confirmSeedWords()),
}
}
module.exports = connect(mapStateToProps, mapDispatchToProps)(RevealSeedPage)

@ -0,0 +1,28 @@
const { connect } = require('react-redux')
const PropTypes = require('prop-types')
const { Route } = require('react-router-dom')
const h = require('react-hyperscript')
const MetamaskRoute = ({ component, mascaraComponent, isMascara, ...props }) => {
return (
h(Route, {
...props,
component: isMascara && mascaraComponent ? mascaraComponent : component,
})
)
}
MetamaskRoute.propTypes = {
component: PropTypes.func,
mascaraComponent: PropTypes.func,
isMascara: PropTypes.bool,
}
const mapStateToProps = state => {
const { metamask: { isMascara } } = state
return {
isMascara,
}
}
module.exports = connect(mapStateToProps)(MetamaskRoute)

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

Loading…
Cancel
Save