Merge branch 'develop' into account-tracker-network-change

feature/default_network_editable
kumavis 6 years ago committed by GitHub
commit 59ab595b5e
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
  1. 2
      .babelrc
  2. 19
      .circleci/config.yml
  3. 2
      .circleci/scripts/firefox-install
  4. 1
      .eslintignore
  5. 1
      .gitignore
  6. 3
      .yo-rc.json
  7. 32
      CHANGELOG.md
  8. 6
      README.md
  9. 2
      app/_locales/cs/messages.json
  10. 2
      app/_locales/de/messages.json
  11. 94
      app/_locales/en/messages.json
  12. 2
      app/_locales/es/messages.json
  13. 2
      app/_locales/fr/messages.json
  14. 822
      app/_locales/ko/messages.json
  15. 2
      app/_locales/ru/messages.json
  16. 2
      app/_locales/tml/messages.json
  17. 2
      app/_locales/tr/messages.json
  18. 9
      app/_locales/zh_CN/messages.json
  19. 3
      app/images/arrow-popout.svg
  20. 34
      app/images/ledger-logo.svg
  21. 3
      app/manifest.json
  22. 7
      app/phishing.html
  23. 21
      app/scripts/background.js
  24. 2
      app/scripts/contentscript.js
  25. 2
      app/scripts/controllers/network/createMetamaskMiddleware.js
  26. 138
      app/scripts/controllers/preferences.js
  27. 12
      app/scripts/controllers/transactions/enums.js
  28. 50
      app/scripts/controllers/transactions/index.js
  29. 1
      app/scripts/controllers/transactions/tx-state-manager.js
  30. 25
      app/scripts/inpage.js
  31. 21
      app/scripts/lib/account-tracker.js
  32. 6
      app/scripts/lib/auto-reload.js
  33. 254
      app/scripts/lib/config-manager.js
  34. 4
      app/scripts/lib/ipfsContent.js
  35. 74
      app/scripts/lib/typed-message-manager.js
  36. 230
      app/scripts/metamask-controller.js
  37. 50
      app/scripts/migrations/_multi-keyring.js
  38. 2
      app/scripts/notice-controller.js
  39. 5
      app/scripts/phishing-detect.js
  40. 1
      development/states/add-token.json
  41. 1
      development/states/confirm-new-ui.json
  42. 1
      development/states/confirm-sig-requests.json
  43. 1
      development/states/currency-localization.json
  44. 1
      development/states/first-time.json
  45. 2
      development/states/send-edit.json
  46. 2
      development/states/send-new-ui.json
  47. 2
      development/states/send.json
  48. 1
      development/states/tx-list-items.json
  49. 1
      docs/adding-new-networks.md
  50. 11
      gulpfile.js
  51. 8
      mascara/src/app/first-time/create-password-screen.js
  52. 6
      mascara/src/app/first-time/index.css
  53. 5
      old-ui/app/account-detail.js
  54. 202
      old-ui/app/add-suggested-token.js
  55. 2
      old-ui/app/add-token.js
  56. 6
      old-ui/app/app.js
  57. 11
      old-ui/app/components/app-bar.js
  58. 3
      old-ui/app/components/pending-typed-msg-details.js
  59. 27
      old-ui/app/components/typed-message-renderer.js
  60. 11169
      package-lock.json
  61. 30
      package.json
  62. 1251
      test/data/mock-state.json
  63. 6
      test/e2e/beta/from-import-beta-ui.spec.js
  64. 219
      test/e2e/beta/metamask-beta-ui.spec.js
  65. 10
      test/e2e/metamask.spec.js
  66. 4
      test/integration/lib/add-token.js
  67. 2
      test/integration/lib/confirm-sig-requests.js
  68. 8
      test/integration/lib/currency-localization.js
  69. 6
      test/integration/lib/send-new-ui.js
  70. 27
      test/integration/lib/tx-list-items.js
  71. 9
      test/lib/mock-config-manager.js
  72. 42
      test/lib/render-helpers.js
  73. 20
      test/lib/shallow-with-store.js
  74. 33
      test/unit/app/cleanErrorStack.spec.js
  75. 105
      test/unit/app/controllers/metamask-controller-test.js
  76. 7
      test/unit/app/controllers/notice-controller-test.js
  77. 140
      test/unit/app/controllers/preferences-controller-test.js
  78. 3
      test/unit/components/balance-component-test.js
  79. 112
      test/unit/config-manager-test.js
  80. 2
      test/unit/responsive/components/dropdown-test.js
  81. 2
      test/unit/ui/add-token.spec.js
  82. 36
      test/unit/ui/app/components/identicon.spec.js
  83. 69
      test/unit/ui/app/components/token-cell.spec.js
  84. 175
      test/unit/ui/app/selectors.spec.js
  85. 26
      test/unit/ui/etherscan-prefix-for-network.spec.js
  86. 33
      ui/app/account-and-transaction-details.js
  87. 92
      ui/app/actions.js
  88. 83
      ui/app/app.js
  89. 48
      ui/app/components/balance-component.js
  90. 5
      ui/app/components/button/button.component.js
  91. 267
      ui/app/components/buy-button-subview.js
  92. 25
      ui/app/components/card/card.component.js
  93. 1
      ui/app/components/card/index.js
  94. 11
      ui/app/components/card/index.scss
  95. 25
      ui/app/components/card/tests/card.component.test.js
  96. 12
      ui/app/components/confirm-page-container/confirm-page-container-content/confirm-page-container-content.component.js
  97. 1
      ui/app/components/confirm-page-container/confirm-page-container-content/confirm-page-container-error/index.js
  98. 4
      ui/app/components/confirm-page-container/confirm-page-container-content/confirm-page-container-summary/confirm-page-container-summary.component.js
  99. 1
      ui/app/components/confirm-page-container/confirm-page-container-content/index.js
  100. 2
      ui/app/components/confirm-page-container/confirm-page-container-content/index.scss
  101. Some files were not shown because too many files have changed in this diff Show More

@ -1,4 +1,4 @@
{
"presets": [["env"], "react", "stage-0"],
"presets": [["env", { "targets": { "browsers": [">0.25%", "not ie 11", "not op_mini all"] } } ], "react", "stage-0"],
"plugins": ["transform-runtime", "transform-async-to-generator", "transform-class-properties"]
}

@ -39,6 +39,10 @@ workflows:
- test-unit:
requires:
- prep-deps-npm
- test-mozilla-lint:
requires:
- prep-deps-npm
- prep-build
- test-integration-mascara-chrome:
requires:
- prep-deps-npm
@ -59,6 +63,7 @@ workflows:
requires:
- test-lint
- test-unit
- test-mozilla-lint
- test-e2e-chrome
- test-e2e-firefox
- test-e2e-beta-chrome
@ -299,8 +304,8 @@ jobs:
- run:
name: github gh-pages docs publish
command: >
git config user.name metamaskbot
git config user.email admin@metamask.io
git config user.name metamaskbot &&
git config user.email admin@metamask.io &&
gh-pages -d docs/jsdocs
test-unit:
@ -313,6 +318,16 @@ jobs:
- run:
name: test:coverage
command: npm run test:coverage
test-mozilla-lint:
docker:
- image: circleci/node:8.11.3-browsers
steps:
- checkout
- attach_workspace:
at: .
- run:
name: test:mozilla-lint
command: npm run mozilla-lint
test-integration-flat-firefox:
docker:

@ -4,7 +4,7 @@ set -e
set -u
set -o pipefail
FIREFOX_VERSION='61.0.2'
FIREFOX_VERSION='62.0'
FIREFOX_BINARY="firefox-${FIREFOX_VERSION}.tar.bz2"
FIREFOX_BINARY_URL="https://ftp.mozilla.org/pub/firefox/releases/${FIREFOX_VERSION}/linux-x86_64/en-US/${FIREFOX_BINARY}"
FIREFOX_PATH='/opt/firefox'

@ -3,6 +3,7 @@ dist/**
builds/**
test-builds/**
docs/**
coverage/
development/bundle.js
development/states.js

1
.gitignore vendored

@ -21,6 +21,7 @@ temp
.DS_Store
app/.DS_Store
coverage/
dist
builds/
disc/

@ -1,3 +0,0 @@
{
"generator-mocha": {}
}

@ -2,11 +2,37 @@
## Current Develop Branch
## 4.12.0 Thursday September 27 2018
- Reintroduces changes from 4.10.0
## 4.11.1 Tuesday September 25 2018
- Adds Ledger support.
## 4.11.0 Monday September 24 2018
- Identical to 4.9.3. A rollback version to give time to fix bugs in the 4.10.x branch.
## 4.10.0 Mon Sep 17 2018
- [#4803](https://github.com/MetaMask/metamask-extension/pull/4803): Implement EIP-712: Sign typed data, but continue to support v1.
- [#4898](https://github.com/MetaMask/metamask-extension/pull/4898): Restore multiple consecutive accounts with balances.
- [#4279](https://github.com/MetaMask/metamask-extension/pull/4279): New BlockTracker and Json-Rpc-Engine based Provider.
- [#5050](https://github.com/MetaMask/metamask-extension/pull/5050): Add Ledger hardware wallet support.
- [#4919](https://github.com/MetaMask/metamask-extension/pull/4919): Refactor and Redesign Transaction List.
- [#5182](https://github.com/MetaMask/metamask-extension/pull/5182): Add Transaction Details to the Transaction List view.
- [#5229](https://github.com/MetaMask/metamask-extension/pull/5229): Clear old seed words when importing new seed words.
- [#5264](https://github.com/MetaMask/metamask-extension/pull/5264): Improve click area for adjustment arrows buttons.
- [#4606](https://github.com/MetaMask/metamask-extension/pull/4606): Add new metamask_watchAsset method.
- [#5189](https://github.com/MetaMask/metamask-extension/pull/5189): Fix bug where Ropsten loading message is shown when connecting to Kovan.
- [#5256](https://github.com/MetaMask/metamask-extension/pull/5256): Add mock EIP-1102 support
## 4.9.3 Wed Aug 15 2018
- (#4897)[https://github.com/MetaMask/metamask-extension/pull/4897]: QR code scan for recipient addresses.
- (#4961)[https://github.com/MetaMask/metamask-extension/pull/4961]: Add a download seed phrase link.
- (#5060)[https://github.com/MetaMask/metamask-extension/pull/5060]: Fix bug where gas was not updating properly.
- [#4897](https://github.com/MetaMask/metamask-extension/pull/4897): QR code scan for recipient addresses.
- [#4961](https://github.com/MetaMask/metamask-extension/pull/4961): Add a download seed phrase link.
- [#5060](https://github.com/MetaMask/metamask-extension/pull/5060): Fix bug where gas was not updating properly.
## 4.9.2 Mon Aug 09 2018

@ -37,6 +37,12 @@ If you're a web dapp developer, we've got two types of guides for you:
Uncompressed builds can be found in `/dist`, compressed builds can be found in `/builds` once they're built.
## Contributing
You can read [our internal docs here](https://metamask.github.io/metamask-extension/).
You can re-generate the docs locally by running `npm run doc`, and contributors can update the hosted docs by running `npm run publish-docs`.
### Running Tests
Requires `mocha` installed. Run `npm install -g mocha`.

@ -796,7 +796,7 @@
"message": "Testovací faucet"
},
"to": {
"message": "Komu: "
"message": "Komu"
},
"toETHviaShapeShift": {
"message": "$1 na ETH přes ShapeShift",

@ -775,7 +775,7 @@
"message": "Testfaucet"
},
"to": {
"message": "An:"
"message": "An"
},
"toETHviaShapeShift": {
"message": "$1 an ETH via ShapeShift",

@ -17,6 +17,9 @@
"accountSelectionRequired": {
"message": "You need to select an account!"
},
"activityLog": {
"message": "activity log"
},
"address": {
"message": "Address"
},
@ -29,6 +32,9 @@
"addTokens": {
"message": "Add Tokens"
},
"addSuggestedTokens": {
"message": "Add Suggested Tokens"
},
"addAcquiredTokens": {
"message": "Add the tokens you've acquired using MetaMask"
},
@ -55,6 +61,12 @@
"attemptingConnect": {
"message": "Attempting to connect to blockchain."
},
"attemptToCancel": {
"message": "Attempt to Cancel?"
},
"attemptToCancelDescription": {
"message": "Attempting to cancel does not guarantee your original transaction will be cancelled. If cancelled, you are still required to pay a transaction fee to the network."
},
"attributions": {
"message": "Attributions"
},
@ -110,6 +122,12 @@
"cancel": {
"message": "Cancel"
},
"cancelAttempt": {
"message": "Cancel Attempt"
},
"cancellationGasFee": {
"message": "Cancellation Gas Fee"
},
"classicInterface": {
"message": "Use classic interface"
},
@ -210,6 +228,9 @@
"currentConversion": {
"message": "Current Conversion"
},
"currentLanguage": {
"message": "Current Language"
},
"currentNetwork": {
"message": "Current Network"
},
@ -451,6 +472,9 @@
"hideTokenPrompt": {
"message": "Hide Token?"
},
"history": {
"message": "History"
},
"howToDeposit": {
"message": "How would you like to deposit Ether?"
},
@ -587,6 +611,9 @@
"metamaskSeedWords": {
"message": "MetaMask Seed Words"
},
"metamaskVersion": {
"message": "MetaMask Version"
},
"min": {
"message": "Minimum"
},
@ -651,7 +678,7 @@
"message": "No transaction history."
},
"noTransactions": {
"message": "No Transactions"
"message": "You have no transactions"
},
"notFound": {
"message": "Not Found"
@ -702,6 +729,9 @@
"pasteSeed": {
"message": "Paste your seed phrase here!"
},
"pending": {
"message": "pending"
},
"personalAddressDetected": {
"message": "Personal address detected. Input the token contract address."
},
@ -730,6 +760,9 @@
"qrCode": {
"message": "Show QR Code"
},
"queue": {
"message": "Queue"
},
"readdToken": {
"message": "You can add this token back in the future by going go to “Add token” in your accounts options menu."
},
@ -845,6 +878,9 @@
"save": {
"message": "Save"
},
"speedUp": {
"message": "speed up"
},
"speedUpTitle": {
"message": "Speed Up Transaction"
},
@ -870,6 +906,12 @@
"secretPhrase": {
"message": "Enter your secret twelve word phrase here to restore your vault."
},
"showHexData": {
"message": "Show Hex Data"
},
"showHexDataDescription": {
"message": "Select this to show the hex data field on the send screen"
},
"newPassword8Chars": {
"message": "New Password (min 8 chars)"
},
@ -882,6 +924,9 @@
"selectCurrency": {
"message": "Select Currency"
},
"selectLocale": {
"message": "Select Locale"
},
"selectService": {
"message": "Select Service"
},
@ -897,6 +942,12 @@
"sendTokens": {
"message": "Send Tokens"
},
"sentEther": {
"message": "sent ether"
},
"sentTokens": {
"message": "sent tokens"
},
"separateEachWord": {
"message": "Separate each word with a single space"
},
@ -910,6 +961,9 @@
"orderOneHere": {
"message": "Order a Trezor or Ledger and keep your funds in cold storage"
},
"outgoing": {
"message": "Outgoing"
},
"searchTokens": {
"message": "Search Tokens"
},
@ -973,6 +1027,9 @@
"sign": {
"message": "Sign"
},
"signatureRequest": {
"message": "Signature Request"
},
"signed": {
"message": "Signed"
},
@ -1025,7 +1082,7 @@
"message": "Test Faucet"
},
"to": {
"message": "To: "
"message": "To"
},
"toETHviaShapeShift": {
"message": "$1 to ETH via ShapeShift",
@ -1055,6 +1112,30 @@
"total": {
"message": "Total"
},
"transaction": {
"message": "transaction"
},
"transactionConfirmed": {
"message": "Transaction confirmed on $2."
},
"transactionCreated": {
"message": "Transaction created with a value of $1 on $2."
},
"transactionWithNonce": {
"message": "Transaction $1"
},
"transactionDropped": {
"message": "Transaction dropped on $2."
},
"transactionSubmitted": {
"message": "Transaction submitted on $2."
},
"transactionUpdated": {
"message": "Transaction updated on $2."
},
"transactionUpdatedGas": {
"message": "Transaction updated with a gas price of $1 on $2."
},
"transactions": {
"message": "transactions"
},
@ -1101,6 +1182,9 @@
"unavailable": {
"message": "Unavailable"
},
"units": {
"message": "units"
},
"unknown": {
"message": "Unknown"
},
@ -1128,6 +1212,9 @@
"unlockMessage": {
"message": "The decentralized web awaits"
},
"updatedWithDate": {
"message": "Updated $1"
},
"uriErrorMsg": {
"message": "URIs require the appropriate HTTP/HTTPS prefix."
},
@ -1168,6 +1255,9 @@
"whatsThis": {
"message": "What's this?"
},
"yesLetsTry": {
"message": "Yes, let's try"
},
"youNeedToAllowCameraAccess": {
"message": "You need to allow camera access to use this feature."
},

@ -772,7 +772,7 @@
"message": "Probar Faucet"
},
"to": {
"message": "Para:"
"message": "Para"
},
"toETHviaShapeShift": {
"message": "$1 a ETH via ShapeShift",

@ -490,7 +490,7 @@
"message": "Sélectionner un service"
},
"send": {
"message": "Envoyé"
"message": "Envoyer"
},
"sendTokens": {
"message": "Envoyer des jetons"

File diff suppressed because it is too large Load Diff

@ -784,7 +784,7 @@
"message": "Тестовый кран"
},
"to": {
"message": "Получатель: "
"message": "Получатель"
},
"toETHviaShapeShift": {
"message": "$1 в ETH через ShapeShift",

@ -796,7 +796,7 @@
"message": "சதன"
},
"to": {
"message": "பநர: "
"message": "பநர"
},
"toETHviaShapeShift": {
"message": "$ 1 மதல ETH வர வடிவம",

@ -796,7 +796,7 @@
"message": "Test Musluğu"
},
"to": {
"message": "Kime: "
"message": "Kime"
},
"toETHviaShapeShift": {
"message": "ShapeShift üstünden $1'dan ETH'e",

@ -23,6 +23,9 @@
"addTokens": {
"message": "添加代币"
},
"addAcquiredTokens": {
"message": "在Metamask上添加已用的代币"
},
"amount": {
"message": "数量"
},
@ -116,6 +119,9 @@
"confirmTransaction": {
"message": "确认交易"
},
"connectHardwareWallet": {
"message": "链接硬件钱包"
},
"continue": {
"message": "继续"
},
@ -714,6 +720,9 @@
"search": {
"message": "搜索"
},
"searchResults": {
"message": "搜索结果"
},
"secretPhrase": {
"message": "输入12位助记词以恢复金库."
},

@ -0,0 +1,3 @@
<svg width="10" height="10" viewBox="0 0 10 10" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M8.67589 0.641872C8.65169 0.642635 8.62756 0.644749 8.6036 0.648202H4.79279C4.55863 0.644896 4.34082 0.767704 4.22278 0.969601C4.10473 1.1715 4.10473 1.4212 4.22278 1.6231C4.34082 1.825 4.55863 1.9478 4.79279 1.9445H7.12113L0.437932 8.61587C0.268309 8.77843 0.19998 9.01984 0.259298 9.24697C0.318616 9.47411 0.496311 9.65149 0.723852 9.71071C0.951393 9.76992 1.19322 9.70171 1.35608 9.53239L8.03927 2.86102V5.18524C8.03596 5.41898 8.15899 5.6364 8.36124 5.75424C8.56349 5.87208 8.81364 5.87208 9.0159 5.75424C9.21815 5.6364 9.34118 5.41898 9.33787 5.18524V1.37863C9.36404 1.18976 9.30558 0.998955 9.17804 0.857009C9.0505 0.715062 8.86682 0.636369 8.67589 0.641872Z" fill="#359BDD"/>
</svg>

After

Width:  |  Height:  |  Size: 795 B

@ -1 +1,33 @@
<svg id="Layer_1" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 1916.3 516.8" width="2500" height="674"><style>.st0{fill:#333745}</style><g id="squares_1_"><path class="st0" d="M578.2 392.7V24.3h25.6v344.1h175.3v24.3H578.2zm327.5 5.1c-39.7 0-70.4-12.8-93.4-37.1-21.7-24.3-33.3-58.8-33.3-103.6 0-43.5 10.2-79.3 32-104.9 21.7-26.9 49.9-39.7 87-39.7 32 0 57.6 11.5 76.8 33.3 19.2 23 28.1 53.7 28.1 92.1v20.5H804.6c0 37.1 9 66.5 26.9 85.7 16.6 20.5 42.2 29.4 74.2 29.4 15.3 0 29.4-1.3 40.9-3.8 11.5-2.6 26.9-6.4 44.8-14.1v24.3c-15.3 6.4-29.4 11.5-42.2 14.1-14.3 2.6-28.9 3.9-43.5 3.8zM898 135.6c-26.9 0-47.3 9-64 25.6-15.3 17.9-25.6 42.2-28.1 75.5h168.9c0-32-6.4-56.3-20.5-74.2-12.8-18-32-26.9-56.3-26.9zm238-21.8c19.2 0 37.1 3.8 51.2 10.2 14.1 7.7 26.9 19.2 38.4 37.1h1.3c-1.3-21.7-1.3-42.2-1.3-62.7V0h24.3v392.7h-16.6l-6.4-42.2c-20.5 30.7-51.2 47.3-89.6 47.3s-66.5-11.5-87-35.8c-20.5-23-29.4-57.6-29.4-102.3 0-47.3 10.2-83.2 29.4-108.7 19.2-25.6 48.6-37.2 85.7-37.2zm0 21.8c-29.4 0-52.4 10.2-67.8 32-15.3 20.5-23 51.2-23 92.1 0 78 30.7 116.4 90.8 116.4 30.7 0 53.7-9 67.8-26.9 14.1-17.9 21.7-47.3 21.7-89.6v-3.8c0-42.2-7.7-72.9-21.7-90.8-12.8-20.5-35.8-29.4-67.8-29.4zm379.9-16.6v17.9l-56.3 3.8c15.3 19.2 23 39.7 23 61.4 0 26.9-9 47.3-26.9 64-17.9 16.6-40.9 24.3-70.4 24.3-12.8 0-21.7 0-25.6-1.3-10.2 5.1-17.9 11.5-23 17.9-5.1 7.7-7.7 14.1-7.7 23s3.8 15.3 10.2 19.2c6.4 3.8 17.9 6.4 33.3 6.4h47.3c29.4 0 52.4 6.4 67.8 17.9s24.3 29.4 24.3 53.7c0 29.4-11.5 51.2-34.5 66.5-23 15.3-56.3 23-99.8 23-34.5 0-61.4-6.4-80.6-20.5-19.2-12.8-28.1-32-28.1-55 0-19.2 6.4-34.5 17.9-47.3s28.1-20.5 47.3-25.6c-7.7-3.8-15.3-9-19.2-15.3-5-6.2-7.7-13.8-7.7-21.7 0-17.9 11.5-34.5 34.5-48.6-15.3-6.4-28.1-16.6-37.1-30.7-9-14.1-12.8-30.7-12.8-48.6 0-26.9 9-49.9 25.6-66.5 17.9-16.6 40.9-24.3 70.4-24.3 17.9 0 32 1.3 42.2 5.1h85.7v1.3h.2zm-222.6 319.8c0 37.1 28.1 56.3 84.4 56.3 71.6 0 107.5-23 107.5-69.1 0-16.6-5.1-28.1-16.6-35.8-11.5-7.7-29.4-11.5-55-11.5h-44.8c-49.9 1.2-75.5 20.4-75.5 60.1zm21.8-235.4c0 21.7 6.4 37.1 19.2 49.9 12.8 11.5 29.4 17.9 51.2 17.9 23 0 40.9-6.4 52.4-17.9 12.8-11.5 17.9-28.1 17.9-49.9 0-23-6.4-40.9-19.2-52.4-12.8-11.5-29.4-17.9-52.4-17.9-21.7 0-39.7 6.4-51.2 19.2-12.8 11.4-17.9 29.3-17.9 51.1z"/><path class="st0" d="M1640 397.8c-39.7 0-70.4-12.8-93.4-37.1-21.7-24.3-33.3-58.8-33.3-103.6 0-43.5 10.2-79.3 32-104.9 21.7-26.9 49.9-39.7 87-39.7 32 0 57.6 11.5 76.8 33.3 19.2 23 28.1 53.7 28.1 92.1v20.5h-197c0 37.1 9 66.5 26.9 85.7 16.6 20.5 42.2 29.4 74.2 29.4 15.3 0 29.4-1.3 40.9-3.8 11.5-2.6 26.9-6.4 44.8-14.1v24.3c-15.3 6.4-29.4 11.5-42.2 14.1-14.1 2.6-28.2 3.8-44.8 3.8zm-6.4-262.2c-26.9 0-47.3 9-64 25.6-15.3 17.9-25.6 42.2-28.1 75.5h168.9c0-32-6.4-56.3-20.5-74.2-12.8-18-32-26.9-56.3-26.9zm245.6-21.8c11.5 0 24.3 1.3 37.1 3.8l-5.1 24.3c-11.8-2.6-23.8-3.9-35.8-3.8-23 0-42.2 10.2-57.6 29.4-15.3 20.5-23 44.8-23 75.5v149.7h-25.6V119h21.7l2.6 49.9h1.3c11.5-20.5 23-34.5 35.8-42.2 15.4-9 30.7-12.9 48.6-12.9zM333.9 12.8h-183v245.6h245.6V76.7c.1-34.5-28.1-63.9-62.6-63.9zm-239.2 0H64c-34.5 0-64 28.1-64 64v30.7h94.7V12.8zM0 165h94.7v94.7H0V165zm301.9 245.6h30.7c34.5 0 64-28.1 64-64V316h-94.7v94.6zm-151-94.6h94.7v94.7h-94.7V316zM0 316v30.7c0 34.5 28.1 64 64 64h30.7V316H0z"/></g></svg>
<svg version="1.1" id="Layer_1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px"
viewBox="0 0 197.4 48.6" style="enable-background:new 0 0 197.4 48.6;" xml:space="preserve">
<style type="text/css">
.st0{fill:#1D2028;}
</style>
<title>Fichier 8</title>
<g>
<path class="st0" d="M34.1,0H15.5v25.1h25.1V6.5C40.6,2.9,37.7,0,34.1,0z"/>
<path class="st0" d="M9.7,0H6.5C2.9,0,0,2.9,0,6.5v3.2h9.7V0z"/>
<rect y="15.5" class="st0" width="9.7" height="9.7"/>
<path class="st0" d="M31,40.6h3.2c3.6,0,6.5-2.9,6.5-6.5V31H31V40.6z"/>
<rect x="15.5" y="31" class="st0" width="9.7" height="9.7"/>
<path class="st0" d="M0,31v3.2c0,3.6,2.9,6.5,6.5,6.5h3.2V31H0z"/>
<g>
<polygon class="st0" points="65.4,2.6 61.6,2.6 61.6,38.1 81.7,38.1 81.7,34.7 65.4,34.7 "/>
<path class="st0" d="M93.9,12c-7.4,0-12.6,5.5-12.6,13.4c0,0.3,0,0.6,0,0.9c0.1,3.4,1.6,6.6,4.1,9c2.4,2.2,5.5,3.5,8.8,3.5
c0.2,0,0.3,0,0.5,0c3.5,0,6.8-1.3,9.4-3.5l0.1-0.1l-1.7-2.8l-0.2,0.1c-2.1,1.9-4.7,3-7.5,3c-4.6,0-9.3-3-9.6-9.7h19.2v-0.2
c0,0,0.1-1.2,0.1-1.8C104.5,16.6,100.3,12,93.9,12z M85.3,22.6c0.8-4.5,4.1-7.4,8.4-7.4c3.2,0,6.7,1.9,7,7.4H85.3z"/>
<path class="st0" d="M126.5,15c0,0.4,0,0.9,0,1.3c-1.6-2.7-4.6-4.4-7.7-4.4c-0.1,0-0.2,0-0.3,0c-6.8,0-11.5,5.4-11.5,13.3
c0,8,4.5,13.4,11.2,13.4c5.3,0,7.7-3.2,8.5-4.6c0,0.4,0,0.8,0,1.1v2.8h3.6V2.6h-3.7V15H126.5z M118.7,35.3c-4.7,0-7.8-4-7.8-10
c0-5.8,3.3-9.9,7.9-9.9c3.9,0,7.8,3.1,7.8,9.9C126.6,32.7,122.5,35.3,118.7,35.3z"/>
<path class="st0" d="M152.2,15.5c0,0.1,0,0.2,0,0.2c-0.7-1.2-2.9-3.8-8.2-3.8c-6.7,0-11.1,5.1-11.1,12.9s4.6,13.1,11.4,13.1
c3.7,0,6.2-1.3,7.9-4c0,0.4,0,0.8,0,1.2v2.3c0,4.9-3.1,7.7-8.6,7.7c-2.3,0-4.7-0.6-6.8-1.7l-0.2-0.1l-1.4,3.1l0.2,0.1
c2.6,1.3,5.5,2,8.3,2c5.9,0,12.2-3,12.2-11.3V12.6h-3.7L152.2,15.5L152.2,15.5z M144.8,34.6c-4.9,0-8.1-3.8-8.1-9.7
c0-6,2.8-9.4,7.6-9.4c5.3,0,7.8,3.1,7.8,9.4C152.2,31.1,149.6,34.6,144.8,34.6z"/>
<path class="st0" d="M171,12c-7.4,0-12.5,5.5-12.5,13.3c0,0.3,0,0.6,0,0.9c0.1,3.4,1.6,6.6,4.1,9c2.4,2.2,5.5,3.5,8.8,3.5
c0.2,0,0.3,0,0.5,0c3.5,0,6.8-1.3,9.4-3.5l0.1-0.1l-1.8-2.8l-0.2,0.1c-2.1,1.9-4.7,3-7.5,3c-4.6,0-9.3-3-9.6-9.7h19.3v-0.2
c0,0,0.1-1.2,0.1-1.8C181.7,16.6,177.5,12,171,12z M162.5,22.6c0.8-4.5,4.1-7.4,8.4-7.4c3.2,0,6.7,1.9,7,7.4H162.5z"/>
<path class="st0" d="M197.3,12.5c-0.5-0.1-0.9-0.1-1.4-0.2c-3.5,0-6.4,2.2-7.9,5.9c0-0.3,0-0.7,0-1.1v-4.6h-3.7l0.1,25.3V38h3.8
V27.3c0-1.6,0.2-3.3,0.7-4.8c1.2-3.9,3.9-6.4,7.1-6.4c0.4,0,0.8,0,1.2,0.1h0.2v-3.7L197.3,12.5z"/>
</g>
</g>
</svg>

Before

Width:  |  Height:  |  Size: 3.1 KiB

After

Width:  |  Height:  |  Size: 2.5 KiB

@ -1,7 +1,7 @@
{
"name": "__MSG_appName__",
"short_name": "__MSG_appName__",
"version": "4.9.3",
"version": "4.12.0",
"manifest_version": 2,
"author": "https://metamask.io",
"description": "__MSG_appDescription__",
@ -63,7 +63,6 @@
"activeTab",
"webRequest",
"*://*.eth/",
"*://*.test/",
"notifications"
],
"web_accessible_resources": [

@ -25,9 +25,9 @@
a {
color: white;
}
</style>
<script>
(function(i,s,o,g,r,a,m){i['GoogleAnalyticsObject']=r;i[r]=i[r]||function(){
(i[r].q=i[r].q||[]).push(arguments)},i[r].l=1*new Date();a=s.createElement(o),
@ -43,9 +43,9 @@
ga('create', 'UA-68598031-1', 'auto' {'allowLinker':true});
ga('send', 'pageview');
ga('require', 'linker');
ga('linker:autoLink', ['harrydenley.com', 'metamask.io'], false, true);
ga('linker:autoLink', ['metamask.io'], false, true);
</script>
<script src="phishing-detect.js"></script>
</head>
<body>
@ -55,6 +55,7 @@
<h3>ATTENTION</h3>
<p>MetaMask believes this domain could currently compromise your security and has prevented you from interacting with it.</p>
<p>This is because the site tested positive on the <a href="https://github.com/metamask/eth-phishing-detect">Ethereum Phishing Detector</a>. This includes outright malicious websites and legitimate websites that have been compromised by a malicious actor.</p>
<p id="esdbLink"></p>
<p>You can turn MetaMask off to interact with this site, but it is advised not to.</p>
<p>If you think this domain is incorrectly flagged or if a blocked legitimate website has resolved its security issues, <a href="https://github.com/metamask/eth-phishing-detect/issues/new">please file an issue</a>.</p>

@ -256,6 +256,7 @@ function setupController (initState, initLangCode) {
showUnconfirmedMessage: triggerUi,
unlockAccountMessage: triggerUi,
showUnapprovedTx: triggerUi,
showWatchAssetUi: showWatchAssetUi,
// initial state
initState,
// initial locale code
@ -405,6 +406,7 @@ function setupController (initState, initLangCode) {
controller.txController.on('update:badge', updateBadge)
controller.messageManager.on('updateBadge', updateBadge)
controller.personalMessageManager.on('updateBadge', updateBadge)
controller.typedMessageManager.on('updateBadge', updateBadge)
/**
* Updates the Web Extension's "badge" number, on the little fox in the toolbar.
@ -443,9 +445,28 @@ function triggerUi () {
})
}
/**
* Opens the browser popup for user confirmation of watchAsset
* then it waits until user interact with the UI
*/
function showWatchAssetUi () {
triggerUi()
return new Promise(
(resolve) => {
var interval = setInterval(() => {
if (!notificationIsOpen) {
clearInterval(interval)
resolve()
}
}, 1000)
}
)
}
// On first install, open a window to MetaMask website to how-it-works.
extension.runtime.onInstalled.addListener(function (details) {
if ((details.reason === 'install') && (!METAMASK_DEBUG)) {
extension.tabs.create({url: 'https://metamask.io/#how-it-works'})
}
})

@ -199,5 +199,5 @@ function blacklistedDomainCheck () {
function redirectToPhishingWarning () {
console.log('MetaMask - routing to Phishing Warning component')
const extensionURL = extension.runtime.getURL('phishing.html')
window.location.href = extensionURL
window.location.href = extensionURL + '#' + window.location.hostname
}

@ -38,6 +38,6 @@ function createPendingNonceMiddleware ({ getPendingNonce }) {
const address = req.params[0]
const blockRef = req.params[1]
if (blockRef !== 'pending') return next()
req.result = await getPendingNonce(address)
res.result = await getPendingNonce(address)
})
}

@ -1,5 +1,6 @@
const ObservableStore = require('obs-store')
const normalizeAddress = require('eth-sig-util').normalize
const { isValidAddress } = require('ethereumjs-util')
const extend = require('xtend')
@ -14,6 +15,7 @@ class PreferencesController {
* @property {string} store.currentAccountTab Indicates the selected tab in the ui
* @property {array} store.tokens The tokens the user wants display in their token lists
* @property {object} store.accountTokens The tokens stored per account and then per network type
* @property {object} store.assetImages Contains assets objects related to assets added
* @property {boolean} store.useBlockie The users preference for blockie identicons within the UI
* @property {object} store.featureFlags A key-boolean map, where keys refer to features and booleans to whether the
* user wishes to see that feature
@ -26,21 +28,42 @@ class PreferencesController {
frequentRpcList: [],
currentAccountTab: 'history',
accountTokens: {},
assetImages: {},
tokens: [],
suggestedTokens: {},
useBlockie: false,
featureFlags: {},
currentLocale: opts.initLangCode,
identities: {},
lostIdentities: {},
seedWords: null,
forgottenPassword: false,
}, opts.initState)
this.diagnostics = opts.diagnostics
this.network = opts.network
this.store = new ObservableStore(initState)
this.showWatchAssetUi = opts.showWatchAssetUi
this._subscribeProviderType()
}
// PUBLIC METHODS
/**
* Sets the {@code forgottenPassword} state property
* @param {boolean} forgottenPassword whether or not the user has forgotten their password
*/
setPasswordForgotten (forgottenPassword) {
this.store.updateState({ forgottenPassword })
}
/**
* Sets the {@code seedWords} seed words
* @param {string|null} seedWords the seed words
*/
setSeedWords (seedWords) {
this.store.updateState({ seedWords })
}
/**
* Setter for the `useBlockie` property
*
@ -51,6 +74,53 @@ class PreferencesController {
this.store.updateState({ useBlockie: val })
}
getSuggestedTokens () {
return this.store.getState().suggestedTokens
}
getAssetImages () {
return this.store.getState().assetImages
}
addSuggestedERC20Asset (tokenOpts) {
this._validateERC20AssetParams(tokenOpts)
const suggested = this.getSuggestedTokens()
const { rawAddress, symbol, decimals, image } = tokenOpts
const address = normalizeAddress(rawAddress)
const newEntry = { address, symbol, decimals, image }
suggested[address] = newEntry
this.store.updateState({ suggestedTokens: suggested })
}
/**
* RPC engine middleware for requesting new asset added
*
* @param req
* @param res
* @param {Function} - next
* @param {Function} - end
*/
async requestWatchAsset (req, res, next, end) {
if (req.method === 'metamask_watchAsset') {
const { type, options } = req.params
switch (type) {
case 'ERC20':
const result = await this._handleWatchAssetERC20(options)
if (result instanceof Error) {
end(result)
} else {
res.result = result
end()
}
break
default:
end(new Error(`Asset of type ${type} not supported`))
}
} else {
next()
}
}
/**
* Getter for the `useBlockie` property
*
@ -186,6 +256,13 @@ class PreferencesController {
return selected
}
removeSuggestedTokens () {
return new Promise((resolve, reject) => {
this.store.updateState({ suggestedTokens: {} })
resolve({})
})
}
/**
* Setter for the `selectedAddress` property
*
@ -232,11 +309,11 @@ class PreferencesController {
* @returns {Promise<array>} Promises the new array of AddedToken objects.
*
*/
async addToken (rawAddress, symbol, decimals) {
async addToken (rawAddress, symbol, decimals, image) {
const address = normalizeAddress(rawAddress)
const newEntry = { address, symbol, decimals }
const tokens = this.store.getState().tokens
const assetImages = this.getAssetImages()
const previousEntry = tokens.find((token, index) => {
return token.address === address
})
@ -247,7 +324,8 @@ class PreferencesController {
} else {
tokens.push(newEntry)
}
this._updateAccountTokens(tokens)
assetImages[address] = image
this._updateAccountTokens(tokens, assetImages)
return Promise.resolve(tokens)
}
@ -260,8 +338,10 @@ class PreferencesController {
*/
removeToken (rawAddress) {
const tokens = this.store.getState().tokens
const assetImages = this.getAssetImages()
const updatedTokens = tokens.filter(token => token.address !== rawAddress)
this._updateAccountTokens(updatedTokens)
delete assetImages[rawAddress]
this._updateAccountTokens(updatedTokens, assetImages)
return Promise.resolve(updatedTokens)
}
@ -322,7 +402,7 @@ class PreferencesController {
/**
* Returns an updated rpcList based on the passed url and the current list.
* The returned list will have a max length of 2. If the _url currently exists it the list, it will be moved to the
* The returned list will have a max length of 3. If the _url currently exists it the list, it will be moved to the
* end of the list. The current list is modified and returned as a promise.
*
* @param {string} _url The rpc url to add to the frequentRpcList.
@ -338,7 +418,7 @@ class PreferencesController {
if (_url !== 'http://localhost:8545') {
rpcList.push(_url)
}
if (rpcList.length > 2) {
if (rpcList.length > 3) {
rpcList.shift()
}
return Promise.resolve(rpcList)
@ -387,6 +467,7 @@ class PreferencesController {
//
// PRIVATE METHODS
//
/**
* Subscription to network provider type.
*
@ -405,10 +486,10 @@ class PreferencesController {
* @param {array} tokens Array of tokens to be updated.
*
*/
_updateAccountTokens (tokens) {
_updateAccountTokens (tokens, assetImages) {
const { accountTokens, providerType, selectedAddress } = this._getTokenRelatedStates()
accountTokens[selectedAddress][providerType] = tokens
this.store.updateState({ accountTokens, tokens })
this.store.updateState({ accountTokens, tokens, assetImages })
}
/**
@ -438,6 +519,47 @@ class PreferencesController {
const tokens = accountTokens[selectedAddress][providerType]
return { tokens, accountTokens, providerType, selectedAddress }
}
/**
* Handle the suggestion of an ERC20 asset through `watchAsset`
* *
* @param {Promise} promise Promise according to addition of ERC20 token
*
*/
async _handleWatchAssetERC20 (options) {
const { address, symbol, decimals, image } = options
const rawAddress = address
try {
this._validateERC20AssetParams({ rawAddress, symbol, decimals })
} catch (err) {
return err
}
const tokenOpts = { rawAddress, decimals, symbol, image }
this.addSuggestedERC20Asset(tokenOpts)
return this.showWatchAssetUi().then(() => {
const tokenAddresses = this.getTokens().filter(token => token.address === normalizeAddress(rawAddress))
return tokenAddresses.length > 0
})
}
/**
* Validates that the passed options for suggested token have all required properties.
*
* @param {Object} opts The options object to validate
* @throws {string} Throw a custom error indicating that address, symbol and/or decimals
* doesn't fulfill requirements
*
*/
_validateERC20AssetParams (opts) {
const { rawAddress, symbol, decimals } = opts
if (!rawAddress || !symbol || !decimals) throw new Error(`Cannot suggest token without address, symbol, and decimals`)
if (!(symbol.length < 6)) throw new Error(`Invalid symbol ${symbol} more than five characters`)
const numDecimals = parseInt(decimals, 10)
if (isNaN(numDecimals) || numDecimals > 36 || numDecimals < 0) {
throw new Error(`Invalid decimals ${decimals} must be at least 0, and not over 36`)
}
if (!isValidAddress(rawAddress)) throw new Error(`Invalid address ${rawAddress}`)
}
}
module.exports = PreferencesController

@ -0,0 +1,12 @@
const TRANSACTION_TYPE_CANCEL = 'cancel'
const TRANSACTION_TYPE_RETRY = 'retry'
const TRANSACTION_TYPE_STANDARD = 'standard'
const TRANSACTION_STATUS_APPROVED = 'approved'
module.exports = {
TRANSACTION_TYPE_CANCEL,
TRANSACTION_TYPE_RETRY,
TRANSACTION_TYPE_STANDARD,
TRANSACTION_STATUS_APPROVED,
}

@ -11,6 +11,14 @@ const txUtils = require('./lib/util')
const cleanErrorStack = require('../../lib/cleanErrorStack')
const log = require('loglevel')
const recipientBlacklistChecker = require('./lib/recipient-blacklist-checker')
const {
TRANSACTION_TYPE_CANCEL,
TRANSACTION_TYPE_RETRY,
TRANSACTION_TYPE_STANDARD,
TRANSACTION_STATUS_APPROVED,
} = require('./enums')
const { hexToBn, bnToHex, BnMultiplyByFraction } = require('../../lib/util')
/**
Transaction Controller is an aggregate of sub-controllers and trackers
@ -160,7 +168,10 @@ class TransactionController extends EventEmitter {
const normalizedTxParams = txUtils.normalizeTxParams(txParams)
txUtils.validateTxParams(normalizedTxParams)
// construct txMeta
let txMeta = this.txStateManager.generateTxMeta({ txParams: normalizedTxParams })
let txMeta = this.txStateManager.generateTxMeta({
txParams: normalizedTxParams,
type: TRANSACTION_TYPE_STANDARD,
})
this.addTx(txMeta)
this.emit('newUnapprovedTx', txMeta)
@ -214,12 +225,47 @@ class TransactionController extends EventEmitter {
txParams: originalTxMeta.txParams,
lastGasPrice,
loadingDefaults: false,
type: TRANSACTION_TYPE_RETRY,
})
this.addTx(txMeta)
this.emit('newUnapprovedTx', txMeta)
return txMeta
}
/**
* Creates a new approved transaction to attempt to cancel a previously submitted transaction. The
* new transaction contains the same nonce as the previous, is a basic ETH transfer of 0x value to
* the sender's address, and has a higher gasPrice than that of the previous transaction.
* @param {number} originalTxId - the id of the txMeta that you want to attempt to cancel
* @param {string=} customGasPrice - the hex value to use for the cancel transaction
* @returns {txMeta}
*/
async createCancelTransaction (originalTxId, customGasPrice) {
const originalTxMeta = this.txStateManager.getTx(originalTxId)
const { txParams } = originalTxMeta
const { gasPrice: lastGasPrice, from, nonce } = txParams
const newGasPrice = customGasPrice || bnToHex(BnMultiplyByFraction(hexToBn(lastGasPrice), 11, 10))
const newTxMeta = this.txStateManager.generateTxMeta({
txParams: {
from,
to: from,
nonce,
gas: '0x5208',
value: '0x0',
gasPrice: newGasPrice,
},
lastGasPrice,
loadingDefaults: false,
status: TRANSACTION_STATUS_APPROVED,
type: TRANSACTION_TYPE_CANCEL,
})
this.addTx(newTxMeta)
await this.approveTransaction(newTxMeta.id)
return newTxMeta
}
/**
updates the txMeta in the txStateManager
@param txMeta {Object} - the updated txMeta
@ -393,7 +439,7 @@ class TransactionController extends EventEmitter {
})
this.txStateManager.getFilteredTxList({
status: 'approved',
status: TRANSACTION_STATUS_APPROVED,
}).forEach((txMeta) => {
const txSignError = new Error('Transaction found as "approved" during boot - possibly stuck during signing')
this.txStateManager.setTxStatusFailed(txMeta.id, txSignError)

@ -353,6 +353,7 @@ class TransactionStateManager extends EventEmitter {
const txMeta = this.getTx(txId)
txMeta.err = {
message: err.toString(),
rpc: err.value,
stack: err.stack,
}
this.updateTx(txMeta)

@ -9,6 +9,11 @@ restoreContextAfterImports()
log.setDefaultLevel(process.env.METAMASK_DEBUG ? 'debug' : 'warn')
console.warn('ATTENTION: In an effort to improve user privacy, MetaMask will ' +
'stop exposing user accounts to dapps by default beginning November 2nd, 2018. ' +
'Dapps should call provider.enable() in order to view and use accounts. Please see ' +
'https://bit.ly/2QQHXvF for complete information and up-to-date example code.')
//
// setup plugin communication
//
@ -22,6 +27,25 @@ var metamaskStream = new LocalMessageDuplexStream({
// compose the inpage provider
var inpageProvider = new MetamaskInpageProvider(metamaskStream)
// Augment the provider with its enable method
inpageProvider.enable = function (options = {}) {
return new Promise((resolve, reject) => {
if (options.mockRejection) {
reject('User rejected account access')
} else {
inpageProvider.sendAsync({ method: 'eth_accounts', params: [] }, (error, response) => {
if (error) {
reject(error)
} else {
resolve(response.result)
}
})
}
})
}
window.ethereum = inpageProvider
//
// setup web3
//
@ -33,6 +57,7 @@ if (typeof window.web3 !== 'undefined') {
or MetaMask and another web3 extension. Please remove one
and try again.`)
}
var web3 = new Web3(inpageProvider)
web3.setProvider = function () {
log.debug('MetaMask - overrode web3.setProvider')

@ -43,10 +43,27 @@ class AccountTracker {
this._provider = opts.provider
this._query = pify(new EthQuery(this._provider))
this._blockTracker = opts.blockTracker
// subscribe to latest block
this._blockTracker.on('latest', this._updateForBlock.bind(this))
// blockTracker.currentBlock may be null
this._currentBlockNumber = this._blockTracker.getCurrentBlock()
this._blockTracker.once('latest', blockNumber => {
this._currentBlockNumber = blockNumber
})
// bind function for easier listener syntax
this._updateForBlock = this._updateForBlock.bind(this)
}
start () {
// remove first to avoid double add
this._blockTracker.removeListener('latest', this._updateForBlock)
// add listener
this._blockTracker.addListener('latest', this._updateForBlock)
// fetch account balances
this._updateAccounts()
}
stop () {
// remove listener
this._blockTracker.removeListener('latest', this._updateForBlock)
}
/**

@ -2,18 +2,12 @@ module.exports = setupDappAutoReload
function setupDappAutoReload (web3, observable) {
// export web3 as a global, checking for usage
let hasBeenWarned = false
let reloadInProgress = false
let lastTimeUsed
let lastSeenNetwork
global.web3 = new Proxy(web3, {
get: (_web3, key) => {
// show warning once on web3 access
if (!hasBeenWarned && key !== 'currentProvider') {
console.warn('MetaMask: web3 will be deprecated in the near future in favor of the ethereumProvider \nhttps://github.com/MetaMask/faq/blob/master/detecting_metamask.md#web3-deprecation')
hasBeenWarned = true
}
// get the time of use
lastTimeUsed = Date.now()
// return value normally

@ -1,254 +0,0 @@
const ethUtil = require('ethereumjs-util')
const normalize = require('eth-sig-util').normalize
const {
MAINNET_RPC_URL,
ROPSTEN_RPC_URL,
KOVAN_RPC_URL,
RINKEBY_RPC_URL,
} = require('../controllers/network/enums')
/* The config-manager is a convenience object
* wrapping a pojo-migrator.
*
* It exists mostly to allow the creation of
* convenience methods to access and persist
* particular portions of the state.
*/
module.exports = ConfigManager
function ConfigManager (opts) {
// ConfigManager is observable and will emit updates
this._subs = []
this.store = opts.store
}
ConfigManager.prototype.setConfig = function (config) {
var data = this.getData()
data.config = config
this.setData(data)
this._emitUpdates(config)
}
ConfigManager.prototype.getConfig = function () {
var data = this.getData()
return data.config
}
ConfigManager.prototype.setData = function (data) {
this.store.putState(data)
}
ConfigManager.prototype.getData = function () {
return this.store.getState()
}
ConfigManager.prototype.setPasswordForgotten = function (passwordForgottenState) {
const data = this.getData()
data.forgottenPassword = passwordForgottenState
this.setData(data)
}
ConfigManager.prototype.getPasswordForgotten = function (passwordForgottenState) {
const data = this.getData()
return data.forgottenPassword
}
ConfigManager.prototype.setWallet = function (wallet) {
var data = this.getData()
data.wallet = wallet
this.setData(data)
}
ConfigManager.prototype.setVault = function (encryptedString) {
var data = this.getData()
data.vault = encryptedString
this.setData(data)
}
ConfigManager.prototype.getVault = function () {
var data = this.getData()
return data.vault
}
ConfigManager.prototype.getKeychains = function () {
return this.getData().keychains || []
}
ConfigManager.prototype.setKeychains = function (keychains) {
var data = this.getData()
data.keychains = keychains
this.setData(data)
}
ConfigManager.prototype.getSelectedAccount = function () {
var config = this.getConfig()
return config.selectedAccount
}
ConfigManager.prototype.setSelectedAccount = function (address) {
var config = this.getConfig()
config.selectedAccount = ethUtil.addHexPrefix(address)
this.setConfig(config)
}
ConfigManager.prototype.getWallet = function () {
return this.getData().wallet
}
// Takes a boolean
ConfigManager.prototype.setShowSeedWords = function (should) {
var data = this.getData()
data.showSeedWords = should
this.setData(data)
}
ConfigManager.prototype.getShouldShowSeedWords = function () {
var data = this.getData()
return data.showSeedWords
}
ConfigManager.prototype.setSeedWords = function (words) {
var data = this.getData()
data.seedWords = words
this.setData(data)
}
ConfigManager.prototype.getSeedWords = function () {
var data = this.getData()
return data.seedWords
}
ConfigManager.prototype.setRpcTarget = function (rpcUrl) {
var config = this.getConfig()
config.provider = {
type: 'rpc',
rpcTarget: rpcUrl,
}
this.setConfig(config)
}
ConfigManager.prototype.setProviderType = function (type) {
var config = this.getConfig()
config.provider = {
type: type,
}
this.setConfig(config)
}
ConfigManager.prototype.useEtherscanProvider = function () {
var config = this.getConfig()
config.provider = {
type: 'etherscan',
}
this.setConfig(config)
}
ConfigManager.prototype.getProvider = function () {
var config = this.getConfig()
return config.provider
}
ConfigManager.prototype.getCurrentRpcAddress = function () {
var provider = this.getProvider()
if (!provider) return null
switch (provider.type) {
case 'mainnet':
return MAINNET_RPC_URL
case 'ropsten':
return ROPSTEN_RPC_URL
case 'kovan':
return KOVAN_RPC_URL
case 'rinkeby':
return RINKEBY_RPC_URL
default:
return provider && provider.rpcTarget ? provider.rpcTarget : RINKEBY_RPC_URL
}
}
//
// Tx
//
ConfigManager.prototype.getTxList = function () {
var data = this.getData()
if (data.transactions !== undefined) {
return data.transactions
} else {
return []
}
}
ConfigManager.prototype.setTxList = function (txList) {
var data = this.getData()
data.transactions = txList
this.setData(data)
}
// wallet nickname methods
ConfigManager.prototype.getWalletNicknames = function () {
var data = this.getData()
const nicknames = ('walletNicknames' in data) ? data.walletNicknames : {}
return nicknames
}
ConfigManager.prototype.nicknameForWallet = function (account) {
const address = normalize(account)
const nicknames = this.getWalletNicknames()
return nicknames[address]
}
ConfigManager.prototype.setNicknameForWallet = function (account, nickname) {
const address = normalize(account)
const nicknames = this.getWalletNicknames()
nicknames[address] = nickname
var data = this.getData()
data.walletNicknames = nicknames
this.setData(data)
}
// observable
ConfigManager.prototype.getSalt = function () {
var data = this.getData()
return data.salt
}
ConfigManager.prototype.setSalt = function (salt) {
var data = this.getData()
data.salt = salt
this.setData(data)
}
ConfigManager.prototype.subscribe = function (fn) {
this._subs.push(fn)
var unsubscribe = this.unsubscribe.bind(this, fn)
return unsubscribe
}
ConfigManager.prototype.unsubscribe = function (fn) {
var index = this._subs.indexOf(fn)
if (index !== -1) this._subs.splice(index, 1)
}
ConfigManager.prototype._emitUpdates = function (state) {
this._subs.forEach(function (handler) {
handler(state)
})
}
ConfigManager.prototype.setLostAccounts = function (lostAccounts) {
var data = this.getData()
data.lostAccounts = lostAccounts
this.setData(data)
}
ConfigManager.prototype.getLostAccounts = function () {
var data = this.getData()
return data.lostAccounts || []
}

@ -5,6 +5,8 @@ module.exports = function (provider) {
function ipfsContent (details) {
const name = details.url.substring(7, details.url.length - 1)
let clearTime = null
if (/^.+\.eth$/.test(name) === false) return
extension.tabs.query({active: true}, tab => {
extension.tabs.update(tab.id, { url: 'loading.html' })
@ -34,7 +36,7 @@ module.exports = function (provider) {
return { cancel: true }
}
extension.webRequest.onErrorOccurred.addListener(ipfsContent, {urls: ['*://*.eth/', '*://*.test/']})
extension.webRequest.onErrorOccurred.addListener(ipfsContent, {urls: ['*://*.eth/']})
return {
remove () {

@ -4,6 +4,7 @@ const createId = require('./random-id')
const assert = require('assert')
const sigUtil = require('eth-sig-util')
const log = require('loglevel')
const jsonschema = require('jsonschema')
/**
* Represents, and contains data about, an 'eth_signTypedData' type signature request. These are created when a
@ -17,7 +18,7 @@ const log = require('loglevel')
* @property {Object} msgParams.from The address that is making the signature request.
* @property {string} msgParams.data A hex string conversion of the raw buffer data of the signature request
* @property {number} time The epoch time at which the this message was created
* @property {string} status Indicates whether the signature request is 'unapproved', 'approved', 'signed' or 'rejected'
* @property {string} status Indicates whether the signature request is 'unapproved', 'approved', 'signed', 'rejected', or 'errored'
* @property {string} type The json-prc signing method for which a signature request has been made. A 'Message' will
* always have a 'eth_signTypedData' type.
*
@ -26,17 +27,10 @@ const log = require('loglevel')
module.exports = class TypedMessageManager extends EventEmitter {
/**
* Controller in charge of managing - storing, adding, removing, updating - TypedMessage.
*
* @typedef {Object} TypedMessage
* @param {Object} opts @deprecated
* @property {Object} memStore The observable store where TypedMessage are saved.
* @property {Object} memStore.unapprovedTypedMessages A collection of all TypedMessages in the 'unapproved' state
* @property {number} memStore.unapprovedTypedMessagesCount The count of all TypedMessages in this.memStore.unapprobedMsgs
* @property {array} messages Holds all messages that have been created by this TypedMessage
*
*/
constructor (opts) {
constructor ({ networkController }) {
super()
this.networkController = networkController
this.memStore = new ObservableStore({
unapprovedTypedMessages: {},
unapprovedTypedMessagesCount: 0,
@ -76,15 +70,17 @@ module.exports = class TypedMessageManager extends EventEmitter {
* @returns {promise} When the message has been signed or rejected
*
*/
addUnapprovedMessageAsync (msgParams, req) {
addUnapprovedMessageAsync (msgParams, req, version) {
return new Promise((resolve, reject) => {
const msgId = this.addUnapprovedMessage(msgParams, req)
const msgId = this.addUnapprovedMessage(msgParams, req, version)
this.once(`${msgId}:finished`, (data) => {
switch (data.status) {
case 'signed':
return resolve(data.rawSig)
case 'rejected':
return reject(new Error('MetaMask Message Signature: User denied message signature.'))
case 'errored':
return reject(new Error(`MetaMask Message Signature: ${data.error}`))
default:
return reject(new Error(`MetaMask Message Signature: Unknown problem: ${JSON.stringify(msgParams)}`))
}
@ -102,7 +98,8 @@ module.exports = class TypedMessageManager extends EventEmitter {
* @returns {number} The id of the newly created TypedMessage.
*
*/
addUnapprovedMessage (msgParams, req) {
addUnapprovedMessage (msgParams, req, version) {
msgParams.version = version
this.validateParams(msgParams)
// add origin from request
if (req) msgParams.origin = req.origin
@ -132,14 +129,33 @@ module.exports = class TypedMessageManager extends EventEmitter {
*
*/
validateParams (params) {
assert.equal(typeof params, 'object', 'Params should ben an object.')
assert.ok('data' in params, 'Params must include a data field.')
assert.ok('from' in params, 'Params must include a from field.')
assert.ok(Array.isArray(params.data), 'Data should be an array.')
assert.equal(typeof params.from, 'string', 'From field must be a string.')
assert.doesNotThrow(() => {
sigUtil.typedSignatureHash(params.data)
}, 'Expected EIP712 typed data')
switch (params.version) {
case 'V1':
assert.equal(typeof params, 'object', 'Params should ben an object.')
assert.ok('data' in params, 'Params must include a data field.')
assert.ok('from' in params, 'Params must include a from field.')
assert.ok(Array.isArray(params.data), 'Data should be an array.')
assert.equal(typeof params.from, 'string', 'From field must be a string.')
assert.doesNotThrow(() => {
sigUtil.typedSignatureHash(params.data)
}, 'Expected EIP712 typed data')
break
case 'V3':
let data
assert.equal(typeof params, 'object', 'Params should be an object.')
assert.ok('data' in params, 'Params must include a data field.')
assert.ok('from' in params, 'Params must include a from field.')
assert.equal(typeof params.from, 'string', 'From field must be a string.')
assert.equal(typeof params.data, 'string', 'Data must be passed as a valid JSON string.')
assert.doesNotThrow(() => { data = JSON.parse(params.data) }, 'Data must be passed as a valid JSON string.')
const validation = jsonschema.validate(data, sigUtil.TYPED_MESSAGE_SCHEMA)
assert.ok(data.primaryType in data.types, `Primary type of "${data.primaryType}" has no type definition.`)
assert.equal(validation.errors.length, 0, 'Data must conform to EIP-712 schema. See https://git.io/fNtcx.')
const chainId = data.domain.chainId
const activeChainId = parseInt(this.networkController.getNetworkState())
chainId && assert.equal(chainId, activeChainId, `Provided chainId (${chainId}) must match the active chainId (${activeChainId})`)
break
}
}
/**
@ -214,6 +230,7 @@ module.exports = class TypedMessageManager extends EventEmitter {
*/
prepMsgForSigning (msgParams) {
delete msgParams.metamaskId
delete msgParams.version
return Promise.resolve(msgParams)
}
@ -227,6 +244,19 @@ module.exports = class TypedMessageManager extends EventEmitter {
this._setMsgStatus(msgId, 'rejected')
}
/**
* Sets a TypedMessage status to 'errored' via a call to this._setMsgStatus.
*
* @param {number} msgId The id of the TypedMessage to error
*
*/
errorMessage (msgId, error) {
const msg = this.getMsg(msgId)
msg.error = error
this._updateMsg(msg)
this._setMsgStatus(msgId, 'errored')
}
//
// PRIVATE METHODS
//
@ -250,7 +280,7 @@ module.exports = class TypedMessageManager extends EventEmitter {
msg.status = status
this._updateMsg(msg)
this.emit(`${msgId}:${status}`, msg)
if (status === 'rejected' || status === 'signed') {
if (status === 'rejected' || status === 'signed' || status === 'errored') {
this.emit(`${msgId}:finished`, msg)
}
}

@ -36,7 +36,6 @@ const TransactionController = require('./controllers/transactions')
const BalancesController = require('./controllers/computed-balances')
const TokenRatesController = require('./controllers/token-rates')
const DetectTokensController = require('./controllers/detect-tokens')
const ConfigManager = require('./lib/config-manager')
const nodeify = require('./lib/nodeify')
const accountImporter = require('./account-import-strategies')
const getBuyEthUrl = require('./lib/buy-eth-url')
@ -50,6 +49,8 @@ const log = require('loglevel')
const TrezorKeyring = require('eth-trezor-keyring')
const LedgerBridgeKeyring = require('eth-ledger-bridge-keyring')
const EthQuery = require('eth-query')
const ethUtil = require('ethereumjs-util')
const sigUtil = require('eth-sig-util')
module.exports = class MetamaskController extends EventEmitter {
@ -67,6 +68,10 @@ module.exports = class MetamaskController extends EventEmitter {
const initState = opts.initState || {}
this.recordFirstTimeInfo(initState)
// this keeps track of how many "controllerStream" connections are open
// the only thing that uses controller connections are open metamask UI instances
this.activeControllerConnections = 0
// platform-specific api
this.platform = opts.platform
@ -79,15 +84,11 @@ module.exports = class MetamaskController extends EventEmitter {
// network store
this.networkController = new NetworkController(initState.NetworkController)
// config manager
this.configManager = new ConfigManager({
store: this.store,
})
// preferences controller
this.preferencesController = new PreferencesController({
initState: initState.PreferencesController,
initLangCode: opts.initLangCode,
showWatchAssetUi: opts.showWatchAssetUi,
network: this.networkController,
})
@ -127,6 +128,16 @@ module.exports = class MetamaskController extends EventEmitter {
provider: this.provider,
blockTracker: this.blockTracker,
})
// start and stop polling for balances based on activeControllerConnections
this.on('controllerConnectionChanged', (activeControllerConnections) => {
if (activeControllerConnections > 0) {
this.accountTracker.start()
} else {
this.accountTracker.stop()
}
})
// ensure accountTracker updates balances after network change
this.networkController.on('networkDidChange', () => {
this.accountTracker._updateAccounts()
@ -141,19 +152,7 @@ module.exports = class MetamaskController extends EventEmitter {
encryptor: opts.encryptor || undefined,
})
// If only one account exists, make sure it is selected.
this.keyringController.memStore.subscribe((state) => {
const addresses = state.keyrings.reduce((res, keyring) => {
return res.concat(keyring.accounts)
}, [])
if (addresses.length === 1) {
const address = addresses[0]
this.preferencesController.setSelectedAddress(address)
}
// ensure preferences + identities controller know about all addresses
this.preferencesController.addAddresses(addresses)
this.accountTracker.syncWithAddresses(addresses)
})
this.keyringController.memStore.subscribe((s) => this._onKeyringControllerUpdate(s))
// detect tokens controller
this.detectTokensController = new DetectTokensController({
@ -180,7 +179,7 @@ module.exports = class MetamaskController extends EventEmitter {
blockTracker: this.blockTracker,
getGasPrice: this.getGasPrice.bind(this),
})
this.txController.on('newUnapprovedTx', opts.showUnapprovedTx.bind(opts))
this.txController.on('newUnapprovedTx', () => opts.showUnapprovedTx())
this.txController.on(`tx:status-update`, (txId, status) => {
if (status === 'confirmed' || status === 'failed') {
@ -214,7 +213,7 @@ module.exports = class MetamaskController extends EventEmitter {
this.networkController.lookupNetwork()
this.messageManager = new MessageManager()
this.personalMessageManager = new PersonalMessageManager()
this.typedMessageManager = new TypedMessageManager()
this.typedMessageManager = new TypedMessageManager({ networkController: this.networkController })
this.publicConfigStore = this.initPublicConfigStore()
this.store.updateStructure({
@ -259,6 +258,7 @@ module.exports = class MetamaskController extends EventEmitter {
eth_syncing: false,
web3_clientVersion: `MetaMask/v${version}`,
},
version,
// account mgmt
getAccounts: async () => {
const isUnlocked = this.keyringController.memStore.getState().isUnlocked
@ -275,7 +275,7 @@ module.exports = class MetamaskController extends EventEmitter {
// msg signing
processEthSignMessage: this.newUnsignedMessage.bind(this),
processPersonalMessage: this.newUnsignedPersonalMessage.bind(this),
processTypedMessage: this.newUnsignedTypedMessage.bind(this),
getPendingNonce: this.getPendingNonce.bind(this),
}
const providerProxy = this.networkController.initializeProvider(providerOpts)
return providerProxy
@ -317,18 +317,15 @@ module.exports = class MetamaskController extends EventEmitter {
* @returns {Object} status
*/
getState () {
const wallet = this.configManager.getWallet()
const vault = this.keyringController.store.getState().vault
const isInitialized = (!!wallet || !!vault)
const isInitialized = !!vault
return {
...{ isInitialized },
...this.memStore.getFlatState(),
...this.configManager.getConfig(),
...{
lostAccounts: this.configManager.getLostAccounts(),
seedWords: this.configManager.getSeedWords(),
forgottenPassword: this.configManager.getPasswordForgotten(),
// TODO: Remove usages of lost accounts
lostAccounts: [],
},
}
}
@ -390,6 +387,7 @@ module.exports = class MetamaskController extends EventEmitter {
setSelectedAddress: nodeify(preferencesController.setSelectedAddress, preferencesController),
addToken: nodeify(preferencesController.addToken, preferencesController),
removeToken: nodeify(preferencesController.removeToken, preferencesController),
removeSuggestedTokens: nodeify(preferencesController.removeSuggestedTokens, preferencesController),
setCurrentAccountTab: nodeify(preferencesController.setCurrentAccountTab, preferencesController),
setAccountLabel: nodeify(preferencesController.setAccountLabel, preferencesController),
setFeatureFlag: nodeify(preferencesController.setFeatureFlag, preferencesController),
@ -409,6 +407,7 @@ module.exports = class MetamaskController extends EventEmitter {
updateTransaction: nodeify(txController.updateTransaction, txController),
updateAndApproveTransaction: nodeify(txController.updateAndApproveTransaction, txController),
retryTransaction: nodeify(this.retryTransaction, this),
createCancelTransaction: nodeify(this.createCancelTransaction, this),
getFilteredTxList: nodeify(txController.getFilteredTxList, txController),
isNonceTaken: nodeify(txController.isNonceTaken, txController),
estimateGas: nodeify(this.estimateGas, this),
@ -728,7 +727,7 @@ module.exports = class MetamaskController extends EventEmitter {
this.verifySeedPhrase()
.then((seedWords) => {
this.configManager.setSeedWords(seedWords)
this.preferencesController.setSeedWords(seedWords)
return cb(null, seedWords)
})
.catch((err) => {
@ -777,7 +776,7 @@ module.exports = class MetamaskController extends EventEmitter {
* @param {function} cb Callback function called with the current address.
*/
clearSeedWordCache (cb) {
this.configManager.setSeedWords(null)
this.preferencesController.setSeedWords(null)
cb(null, this.preferencesController.getSelectedAddress())
}
@ -985,22 +984,31 @@ module.exports = class MetamaskController extends EventEmitter {
* @param {Object} msgParams - The params passed to eth_signTypedData.
* @returns {Object} Full state update.
*/
signTypedMessage (msgParams) {
log.info('MetaMaskController - signTypedMessage')
async signTypedMessage (msgParams) {
log.info('MetaMaskController - eth_signTypedData')
const msgId = msgParams.metamaskId
// sets the status op the message to 'approved'
// and removes the metamaskId for signing
return this.typedMessageManager.approveMessage(msgParams)
.then((cleanMsgParams) => {
// signs the message
return this.keyringController.signTypedMessage(cleanMsgParams)
})
.then((rawSig) => {
// tells the listener that the message has been signed
// and can be returned to the dapp
this.typedMessageManager.setMsgStatusSigned(msgId, rawSig)
return this.getState()
})
const version = msgParams.version
try {
const cleanMsgParams = await this.typedMessageManager.approveMessage(msgParams)
const address = sigUtil.normalize(cleanMsgParams.from)
const keyring = await this.keyringController.getKeyringForAccount(address)
const wallet = keyring._getWalletForAccount(address)
const privKey = ethUtil.toBuffer(wallet.getPrivateKey())
let signature
switch (version) {
case 'V1':
signature = sigUtil.signTypedDataLegacy(privKey, { data: cleanMsgParams.data })
break
case 'V3':
signature = sigUtil.signTypedData(privKey, { data: JSON.parse(cleanMsgParams.data) })
break
}
this.typedMessageManager.setMsgStatusSigned(msgId, signature)
return this.getState()
} catch (error) {
log.info('MetaMaskController - eth_signTypedData failed.', error)
this.typedMessageManager.errorMessage(msgId, error)
}
}
/**
@ -1038,35 +1046,16 @@ module.exports = class MetamaskController extends EventEmitter {
/**
* A legacy method used to record user confirmation that they understand
* that some of their accounts have been recovered but should be backed up.
* This function no longer does anything and will be removed.
*
* @deprecated
* @param {Function} cb - A callback function called with a full state update.
*/
markAccountsFound (cb) {
this.configManager.setLostAccounts([])
this.sendUpdate()
// TODO Remove me
cb(null, this.getState())
}
/**
* A legacy method (probably dead code) that was used when we swapped out our
* key management library that we depended on.
*
* Described in:
* https://medium.com/metamask/metamask-3-migration-guide-914b79533cdd
*
* @deprecated
* @param {} migratorOutput
*/
restoreOldLostAccounts (migratorOutput) {
const { lostAccounts } = migratorOutput
if (lostAccounts) {
this.configManager.setLostAccounts(lostAccounts.map(acct => acct.address))
return this.importLostAccounts(migratorOutput)
}
return Promise.resolve(migratorOutput)
}
/**
* An account object
* @typedef Account
@ -1111,6 +1100,19 @@ module.exports = class MetamaskController extends EventEmitter {
return state
}
/**
* Allows a user to attempt to cancel a previously submitted transaction by creating a new
* transaction.
* @param {number} originalTxId - the id of the txMeta that you want to attempt to cancel
* @param {string=} customGasPrice - the hex value to use for the cancel transaction
* @returns {object} MetaMask state
*/
async createCancelTransaction (originalTxId, customGasPrice, cb) {
await this.txController.createCancelTransaction(originalTxId, customGasPrice)
const state = await this.getState()
return state
}
estimateGas (estimateGasParams) {
return new Promise((resolve, reject) => {
return this.txController.txGasUtil.query.estimateGas(estimateGasParams, (err, res) => {
@ -1132,7 +1134,7 @@ module.exports = class MetamaskController extends EventEmitter {
* @param {Function} cb - A callback function called when complete.
*/
markPasswordForgotten (cb) {
this.configManager.setPasswordForgotten(true)
this.preferencesController.setPasswordForgotten(true)
this.sendUpdate()
cb()
}
@ -1142,7 +1144,7 @@ module.exports = class MetamaskController extends EventEmitter {
* @param {Function} cb - A callback function called when complete.
*/
unMarkPasswordForgotten (cb) {
this.configManager.setPasswordForgotten(false)
this.preferencesController.setPasswordForgotten(false)
this.sendUpdate()
cb()
}
@ -1213,18 +1215,28 @@ module.exports = class MetamaskController extends EventEmitter {
setupControllerConnection (outStream) {
const api = this.getApi()
const dnode = Dnode(api)
// report new active controller connection
this.activeControllerConnections++
this.emit('controllerConnectionChanged', this.activeControllerConnections)
// connect dnode api to remote connection
pump(
outStream,
dnode,
outStream,
(err) => {
// report new active controller connection
this.activeControllerConnections--
this.emit('controllerConnectionChanged', this.activeControllerConnections)
// report any error
if (err) log.error(err)
}
)
dnode.on('remote', (remote) => {
// push updates to popup
const sendUpdate = remote.sendUpdate.bind(remote)
const sendUpdate = (update) => remote.sendUpdate(update)
this.on('update', sendUpdate)
// remove update listener once the connection ends
dnode.on('end', () => this.removeListener('update', sendUpdate))
})
}
@ -1246,6 +1258,10 @@ module.exports = class MetamaskController extends EventEmitter {
engine.push(createOriginMiddleware({ origin }))
engine.push(createLoggerMiddleware({ origin }))
engine.push(filterMiddleware)
engine.push(this.preferencesController.requestWatchAsset.bind(this.preferencesController))
engine.push(this.createTypedDataMiddleware('eth_signTypedData', 'V1').bind(this))
engine.push(this.createTypedDataMiddleware('eth_signTypedData_v1', 'V1').bind(this))
engine.push(this.createTypedDataMiddleware('eth_signTypedData_v3', 'V3', true).bind(this))
engine.push(createProviderMiddleware({ provider: this.provider }))
// setup connection
@ -1273,15 +1289,45 @@ module.exports = class MetamaskController extends EventEmitter {
* @param {*} outStream - The stream to provide public config over.
*/
setupPublicConfig (outStream) {
const configStream = asStream(this.publicConfigStore)
pump(
asStream(this.publicConfigStore),
configStream,
outStream,
(err) => {
configStream.destroy()
if (err) log.error(err)
}
)
}
/**
* Handle a KeyringController update
* @param {object} state the KC state
* @return {Promise<void>}
* @private
*/
async _onKeyringControllerUpdate (state) {
const {isUnlocked, keyrings} = state
const addresses = keyrings.reduce((acc, {accounts}) => acc.concat(accounts), [])
if (!addresses.length) {
return
}
// Ensure preferences + identities controller know about all addresses
this.preferencesController.addAddresses(addresses)
this.accountTracker.syncWithAddresses(addresses)
const wasLocked = !isUnlocked
if (wasLocked) {
const oldSelectedAddress = this.preferencesController.getSelectedAddress()
if (!addresses.includes(oldSelectedAddress)) {
const address = addresses[0]
await this.preferencesController.setSelectedAddress(address)
}
}
}
/**
* A method for emitting the full MetaMask state to all registered listeners.
* @private
@ -1324,6 +1370,19 @@ module.exports = class MetamaskController extends EventEmitter {
return '0x' + percentileNumBn.mul(GWEI_BN).toString(16)
}
/**
* Returns the nonce that will be associated with a transaction once approved
* @param address {string} - The hex string address for the transaction
* @returns Promise<number>
*/
async getPendingNonce (address) {
const { nonceDetails, releaseLock} = await this.txController.nonceTracker.getNonceLock(address)
const pendingNonce = nonceDetails.params.highestSuggested
releaseLock()
return pendingNonce
}
//=============================================================================
// CONFIG
//=============================================================================
@ -1428,6 +1487,7 @@ module.exports = class MetamaskController extends EventEmitter {
}
}
// TODO: Replace isClientOpen methods with `controllerConnectionChanged` events.
/**
* A method for recording whether the MetaMask user interface is open or not.
* @private
@ -1448,4 +1508,34 @@ module.exports = class MetamaskController extends EventEmitter {
set isClientOpenAndUnlocked (active) {
this.tokenRatesController.isActive = active
}
/**
* Creates RPC engine middleware for processing eth_signTypedData requests
*
* @param {Object} req - request object
* @param {Object} res - response object
* @param {Function} - next
* @param {Function} - end
*/
createTypedDataMiddleware (methodName, version, reverse) {
return async (req, res, next, end) => {
const { method, params } = req
if (method === methodName) {
const promise = this.typedMessageManager.addUnapprovedMessageAsync({
data: reverse ? params[1] : params[0],
from: reverse ? params[0] : params[1],
}, req, version)
this.sendUpdate()
this.opts.showUnconfirmedMessage()
try {
res.result = await promise
end()
} catch (error) {
end(error)
}
} else {
next()
}
}
}
}

@ -1,50 +0,0 @@
const version = 5
/*
This is an incomplete migration bc it requires post-decrypted data
which we dont have access to at the time of this writing.
*/
const ObservableStore = require('obs-store')
const ConfigManager = require('../../app/scripts/lib/config-manager')
const IdentityStoreMigrator = require('../../app/scripts/lib/idStore-migrator')
const KeyringController = require('eth-keyring-controller')
const password = 'obviously not correct'
module.exports = {
version,
migrate: function (versionedData) {
versionedData.meta.version = version
const store = new ObservableStore(versionedData.data)
const configManager = new ConfigManager({ store })
const idStoreMigrator = new IdentityStoreMigrator({ configManager })
const keyringController = new KeyringController({
configManager: configManager,
})
// attempt to migrate to multiVault
return idStoreMigrator.migratedVaultForPassword(password)
.then((result) => {
// skip if nothing to migrate
if (!result) return Promise.resolve(versionedData)
delete versionedData.data.wallet
// create new keyrings
const privKeys = result.lostAccounts.map(acct => acct.privateKey)
return Promise.all([
keyringController.restoreKeyring(result.serialized),
keyringController.restoreKeyring({ type: 'Simple Key Pair', data: privKeys }),
]).then(() => {
return keyringController.persistAllKeyrings(password)
}).then(() => {
// copy result on to state object
versionedData.data = store.get()
return Promise.resolve(versionedData)
})
})
},
}

@ -7,7 +7,7 @@ const uniqBy = require('lodash.uniqby')
module.exports = class NoticeController extends EventEmitter {
constructor (opts) {
constructor (opts = {}) {
super()
this.noticePoller = null
this.firstVersion = opts.firstVersion

@ -0,0 +1,5 @@
window.onload = function() {
if (window.location.pathname === '/phishing.html') {
document.getElementById('esdbLink').innerHTML = '<b>To read more about this scam, navigate to: <a href="https://etherscamdb.info/domain/' + window.location.hash.substring(1) + '"> https://etherscamdb.info/domain/' + window.location.hash.substring(1) + '</a></b>'
}
}

@ -123,6 +123,7 @@
"modalState": {},
"previousModalState": {}
},
"sidebar": {},
"transForward": true,
"isLoading": false,
"warning": null,

@ -141,6 +141,7 @@
"accountDetail": {
"subview": "transactions"
},
"sidebar": {},
"modal": {
"modalState": {},
"previousModalState": {}

@ -162,6 +162,7 @@
"accountDetail": {
"subview": "transactions"
},
"sidebar": {},
"modal": {
"modalState": {},
"previousModalState": {}

@ -120,6 +120,7 @@
"accountDetail": {
"subview": "transactions"
},
"sidebar": {},
"modal": {
"modalState": {},
"previousModalState": {}

@ -48,6 +48,7 @@
"accountDetail": {
"subview": "transactions"
},
"sidebar": {},
"transForward": true,
"isLoading": false,
"warning": null,

@ -22,6 +22,7 @@
"name": "Send Account 4"
}
},
"assetImages": {},
"unapprovedTxs": {},
"currentCurrency": "USD",
"conversionRate": 1200.88200327,
@ -141,6 +142,7 @@
"accountDetail": {
"subview": "transactions"
},
"sidebar": {},
"modal": {
"modalState": {},
"previousModalState": {}

@ -61,6 +61,7 @@
"name": "Address Book Account 1"
}
],
"assetImages": {},
"tokens": [],
"transactions": {},
"selectedAddressTxList": [],
@ -120,6 +121,7 @@
"accountDetail": {
"subview": "transactions"
},
"sidebar": {},
"modal": {
"modalState": {},
"previousModalState": {}

@ -21,6 +21,7 @@
"name": "Account 4"
}
},
"assetImages": {},
"unapprovedTxs": {},
"currentCurrency": "USD",
"conversionRate": 16.88200327,
@ -99,6 +100,7 @@
"accountExport": "none",
"privateKey": ""
},
"sidebar": {},
"transForward": true,
"isLoading": false,
"warning": null,

@ -118,6 +118,7 @@
"modalState": {},
"previousModalState": {}
},
"sidebar": {},
"transForward": true,
"isLoading": false,
"warning": null,

@ -5,7 +5,6 @@ To add another network to our dropdown menu, make sure the following files are a
```
app/scripts/config.js
app/scripts/lib/buy-eth-url.js
app/scripts/lib/config-manager.js
ui/app/app.js
ui/app/components/buy-button-subview.js
ui/app/components/drop-menu-item.js

@ -229,11 +229,12 @@ function createScssBuildTask ({ src, dest, devMode, pattern }) {
await endOfStream(stream)
livereload.changed(event.path)
})
return buildScssWithSourceMaps()
}
return buildScss()
}
function buildScss () {
function buildScssWithSourceMaps () {
return gulp.src(src)
.pipe(sourcemaps.init())
.pipe(sass().on('error', sass.logError))
@ -241,6 +242,13 @@ function createScssBuildTask ({ src, dest, devMode, pattern }) {
.pipe(autoprefixer())
.pipe(gulp.dest(dest))
}
function buildScss () {
return gulp.src(src)
.pipe(sass().on('error', sass.logError))
.pipe(autoprefixer())
.pipe(gulp.dest(dest))
}
}
gulp.task('lint-scss', function () {
@ -267,6 +275,7 @@ const buildJsFiles = [
'contentscript',
'background',
'ui',
'phishing-detect',
]
// bundle tasks

@ -63,7 +63,9 @@ class CreatePasswordScreen extends Component {
return password === confirmPassword
}
createAccount = () => {
createAccount = (event) => {
event.preventDefault()
if (!this.isValid()) {
return
}
@ -127,7 +129,7 @@ class CreatePasswordScreen extends Component {
It allows you to hold ether & tokens, and interact with decentralized applications.
</div>
</div>}
<div className="create-password">
<form className="create-password">
<div className="create-password__title">
Create Password
</div>
@ -188,7 +190,7 @@ class CreatePasswordScreen extends Component {
</a>
{ */ }
<Breadcrumbs total={3} currentIndex={0} />
</div>
</form>
</div>
</div>
)

@ -17,6 +17,12 @@
font-family: Roboto;
}
@media screen and (min-height: 576px) {
.first-time-flow {
height: 100vh;
}
}
.alpha-warning__container {
display: flex;
justify-content: center;

@ -32,6 +32,7 @@ function mapStateToProps (state) {
currentCurrency: state.metamask.currentCurrency,
currentAccountTab: state.metamask.currentAccountTab,
tokens: state.metamask.tokens,
suggestedTokens: state.metamask.suggestedTokens,
computedBalances: state.metamask.computedBalances,
}
}
@ -49,6 +50,10 @@ AccountDetailScreen.prototype.render = function () {
var account = props.accounts[selected]
const { network, conversionRate, currentCurrency } = props
if (Object.keys(props.suggestedTokens).length > 0) {
this.props.dispatch(actions.showAddSuggestedTokenPage())
}
return (
h('.account-detail-section.full-flex-height', [

@ -0,0 +1,202 @@
const inherits = require('util').inherits
const Component = require('react').Component
const h = require('react-hyperscript')
const connect = require('react-redux').connect
const actions = require('../../ui/app/actions')
const Tooltip = require('./components/tooltip.js')
const ethUtil = require('ethereumjs-util')
const Copyable = require('./components/copyable')
const addressSummary = require('./util').addressSummary
module.exports = connect(mapStateToProps)(AddSuggestedTokenScreen)
function mapStateToProps (state) {
return {
identities: state.metamask.identities,
suggestedTokens: state.metamask.suggestedTokens,
}
}
inherits(AddSuggestedTokenScreen, Component)
function AddSuggestedTokenScreen () {
this.state = {
warning: null,
}
Component.call(this)
}
AddSuggestedTokenScreen.prototype.render = function () {
const state = this.state
const props = this.props
const { warning } = state
const key = Object.keys(props.suggestedTokens)[0]
const { address, symbol, decimals } = props.suggestedTokens[key]
return (
h('.flex-column.flex-grow', [
// subtitle and nav
h('.section-title.flex-row.flex-center', [
h('h2.page-subtitle', 'Add Suggested Token'),
]),
h('.error', {
style: {
display: warning ? 'block' : 'none',
padding: '0 20px',
textAlign: 'center',
},
}, warning),
// conf view
h('.flex-column.flex-justify-center.flex-grow.select-none', [
h('.flex-space-around', {
style: {
padding: '20px',
},
}, [
h('div', [
h(Tooltip, {
position: 'top',
title: 'The contract of the actual token contract. Click for more info.',
}, [
h('a', {
style: { fontWeight: 'bold', paddingRight: '10px'},
href: 'https://support.metamask.io/kb/article/24-what-is-a-token-contract-address',
target: '_blank',
}, [
h('span', 'Token Contract Address '),
h('i.fa.fa-question-circle'),
]),
]),
]),
h('div', {
style: { display: 'flex' },
}, [
h(Copyable, {
value: ethUtil.toChecksumAddress(address),
}, [
h('span#token-address', {
style: {
width: 'inherit',
flex: '1 0 auto',
height: '30px',
margin: '8px',
display: 'flex',
},
}, addressSummary(address, 24, 4, false)),
]),
]),
h('div', [
h('span', {
style: { fontWeight: 'bold', paddingRight: '10px'},
}, 'Token Symbol'),
]),
h('div', { style: {display: 'flex'} }, [
h('p#token_symbol', {
style: {
width: 'inherit',
flex: '1 0 auto',
height: '30px',
margin: '8px',
},
}, symbol),
]),
h('div', [
h('span', {
style: { fontWeight: 'bold', paddingRight: '10px'},
}, 'Decimals of Precision'),
]),
h('div', { style: {display: 'flex'} }, [
h('p#token_decimals', {
type: 'number',
style: {
width: 'inherit',
flex: '1 0 auto',
height: '30px',
margin: '8px',
},
}, decimals),
]),
h('button', {
style: {
alignSelf: 'center',
margin: '8px',
},
onClick: (event) => {
this.props.dispatch(actions.removeSuggestedTokens())
},
}, 'Cancel'),
h('button', {
style: {
alignSelf: 'center',
margin: '8px',
},
onClick: (event) => {
const valid = this.validateInputs({ address, symbol, decimals })
if (!valid) return
this.props.dispatch(actions.addToken(address.trim(), symbol.trim(), decimals))
.then(() => {
this.props.dispatch(actions.removeSuggestedTokens())
})
},
}, 'Add'),
]),
]),
])
)
}
AddSuggestedTokenScreen.prototype.componentWillMount = function () {
if (typeof global.ethereumProvider === 'undefined') return
}
AddSuggestedTokenScreen.prototype.validateInputs = function (opts) {
let msg = ''
const identitiesList = Object.keys(this.props.identities)
const { address, symbol, decimals } = opts
const standardAddress = ethUtil.addHexPrefix(address).toLowerCase()
const validAddress = ethUtil.isValidAddress(address)
if (!validAddress) {
msg += 'Address is invalid.'
}
const validDecimals = decimals >= 0 && decimals <= 36
if (!validDecimals) {
msg += 'Decimals must be at least 0, and not over 36. '
}
const symbolLen = symbol.trim().length
const validSymbol = symbolLen > 0 && symbolLen < 10
if (!validSymbol) {
msg += 'Symbol must be between 0 and 10 characters.'
}
const ownAddress = identitiesList.includes(standardAddress)
if (ownAddress) {
msg = 'Personal address detected. Input the token contract address.'
}
const isValid = validAddress && validDecimals && !ownAddress
if (!isValid) {
this.setState({
warning: msg,
})
} else {
this.setState({ warning: null })
}
return isValid
}

@ -196,7 +196,7 @@ AddTokenScreen.prototype.validateInputs = function () {
msg += 'Address is invalid.'
}
const validDecimals = decimals >= 0 && decimals < 36
const validDecimals = decimals >= 0 && decimals <= 36
if (!validDecimals) {
msg += 'Decimals must be at least 0, and not over 36. '
}

@ -23,6 +23,7 @@ const generateLostAccountsNotice = require('../lib/lost-accounts-notice')
// other views
const ConfigScreen = require('./config')
const AddTokenScreen = require('./add-token')
const AddSuggestedTokenScreen = require('./add-suggested-token')
const Import = require('./accounts/import')
const InfoScreen = require('./info')
const NewUiAnnouncement = require('./new-ui-annoucement')
@ -74,6 +75,7 @@ function mapStateToProps (state) {
lostAccounts: state.metamask.lostAccounts,
frequentRpcList: state.metamask.frequentRpcList || [],
featureFlags,
suggestedTokens: state.metamask.suggestedTokens,
// state needed to get account dropdown temporarily rendering from app bar
identities,
@ -236,6 +238,10 @@ App.prototype.renderPrimary = function () {
log.debug('rendering add-token screen from unlock screen.')
return h(AddTokenScreen, {key: 'add-token'})
case 'add-suggested-token':
log.debug('rendering add-suggested-token screen from unlock screen.')
return h(AddSuggestedTokenScreen, {key: 'add-suggested-token'})
case 'config':
log.debug('rendering config screen')
return h(ConfigScreen, {key: 'config'})

@ -350,11 +350,14 @@ module.exports = class AppBar extends Component {
}
}
renderCommonRpc (rpcList, {rpcTarget}) {
renderCommonRpc (rpcList, provider) {
const {dispatch} = this.props
const reversedRpcList = rpcList.slice().reverse()
return rpcList.map((rpc) => {
if ((rpc === LOCALHOST_RPC_URL) || (rpc === rpcTarget)) {
return reversedRpcList.map((rpc) => {
const currentRpcTarget = provider.type === 'rpc' && rpc === provider.rpcTarget
if ((rpc === LOCALHOST_RPC_URL) || currentRpcTarget) {
return null
} else {
return h(DropdownMenuItem, {
@ -364,7 +367,7 @@ module.exports = class AppBar extends Component {
}, [
h('i.fa.fa-question-circle.fa-lg.menu-icon'),
rpc,
rpcTarget === rpc
currentRpcTarget
? h('.check', '✓')
: null,
])

@ -21,7 +21,7 @@ PendingMsgDetails.prototype.render = function () {
var identity = state.identities[address] || { address: address }
var account = state.accounts[address] || { address: address }
var { data } = msgParams
var { data, version } = msgParams
return (
h('div', {
@ -48,6 +48,7 @@ PendingMsgDetails.prototype.render = function () {
h('label.font-small', { style: { display: 'block' } }, 'YOU ARE SIGNING'),
h(TypedMessageRenderer, {
value: data,
version,
style: {
height: '215px',
},

@ -2,6 +2,7 @@ const Component = require('react').Component
const h = require('react-hyperscript')
const inherits = require('util').inherits
const extend = require('xtend')
const { ObjectInspector } = require('react-inspector')
module.exports = TypedMessageRenderer
@ -12,8 +13,16 @@ function TypedMessageRenderer () {
TypedMessageRenderer.prototype.render = function () {
const props = this.props
const { value, style } = props
const text = renderTypedData(value)
const { value, version, style } = props
let text
switch (version) {
case 'V1':
text = renderTypedData(value)
break
case 'V3':
text = renderTypedDataV3(value)
break
}
const defaultStyle = extend({
width: '315px',
@ -44,3 +53,17 @@ function renderTypedData (values) {
])
})
}
function renderTypedDataV3 (values) {
const { domain, message } = JSON.parse(values)
return [
domain ? h('div', [
h('h1', 'Domain'),
h(ObjectInspector, { data: domain, expandLevel: 1, name: 'domain' }),
]) : '',
message ? h('div', [
h('h1', 'Message'),
h(ObjectInspector, { data: message, expandLevel: 1, name: 'message' }),
]) : '',
]
}

11169
package-lock.json generated

File diff suppressed because it is too large Load Diff

@ -8,6 +8,7 @@
"mascara": "gulp dev:mascara & node ./mascara/example/server",
"dist": "gulp dist",
"doc": "jsdoc -c development/tools/.jsdoc.json",
"publish-docs": "gh-pages -d docs/jsdocs",
"test": "npm run test:unit && npm run test:integration && npm run lint",
"watch:test:unit": "nodemon --exec \"npm run test:unit\" ./test ./app ./ui",
"test:unit": "cross-env METAMASK_ENV=test mocha --exit --require test/setup.js --recursive \"test/unit/**/*.js\" \"ui/app/**/*.test.js\" && dot-only-hunter",
@ -22,7 +23,7 @@
"test:e2e:run:firefox": "SELENIUM_BROWSER=firefox mocha test/e2e/metamask.spec --bail --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 --reporter=text --reporter=html npm run test:unit && npm run test:coveralls-upload",
"test:coveralls-upload": "if [ $COVERALLS_REPO_TOKEN ]; then nyc report --reporter=text-lcov | coveralls; fi",
"test:flat": "npm run test:flat:build && karma start test/flat.conf.js",
"test:flat:build": "npm run test:flat:build:ui && npm run test:flat:build:tests && npm run test:flat:build:locales",
@ -40,6 +41,7 @@
"sentry:publish": "node ./development/sentry-publish.js",
"lint": "eslint .",
"lint:fix": "eslint . --fix",
"mozilla-lint": "addons-linter dist/firefox",
"ui": "npm run test:flat:build:states && beefy development/ui-dev.js:bundle.js --live --open --index=./development/index.html --cwd ./",
"mock": "beefy development/mock-dev.js:bundle.js --live --open --index=./development/index.html --cwd ./",
"watch": "mocha watch --recursive \"test/unit/**/*.js\"",
@ -57,7 +59,11 @@
[
"env",
{
"debug": true
"browsers": [
">0.25%",
"not ie 11",
"not op_mini all"
]
}
],
"stage-0"
@ -105,7 +111,7 @@
"ensnare": "^1.0.0",
"eslint-plugin-react": "^7.4.0",
"eth-bin-to-ops": "^1.0.1",
"eth-block-tracker": "^4.0.1",
"eth-block-tracker": "^4.0.2",
"eth-contract-metadata": "github:MetaMask/eth-contract-metadata#master",
"eth-ens-namehash": "^2.0.8",
"eth-hd-keyring": "^1.2.2",
@ -117,7 +123,7 @@
"eth-method-registry": "^1.0.0",
"eth-phishing-detect": "^1.1.4",
"eth-query": "^2.1.2",
"eth-sig-util": "^1.4.2",
"eth-sig-util": "^2.0.2",
"eth-token-tracker": "^1.1.4",
"eth-trezor-keyring": "^0.1.0",
"ethereumjs-abi": "^0.6.4",
@ -143,29 +149,30 @@
"gulp-eslint": "^4.0.0",
"gulp-sass": "^4.0.0",
"hat": "0.0.3",
"human-standard-token-abi": "^1.0.2",
"human-standard-token-abi": "^2.0.0",
"idb-global": "^2.1.0",
"identicon.js": "^2.3.1",
"iframe": "^1.0.0",
"iframe-stream": "^3.0.0",
"inject-css": "^0.1.1",
"jazzicon": "^1.2.0",
"json-rpc-engine": "^3.7.3",
"json-rpc-engine": "^3.7.4",
"json-rpc-middleware-stream": "^1.0.1",
"jsonschema": "^1.2.4",
"lodash.debounce": "^4.0.8",
"lodash.memoize": "^4.1.2",
"lodash.shuffle": "^4.2.0",
"lodash.uniqby": "^4.7.0",
"loglevel": "^1.4.1",
"metamascara": "^2.0.0",
"metamask-inpage-provider": "^1.0.0",
"metamask-inpage-provider": "^1.1.1",
"metamask-logo": "^2.1.4",
"mkdirp": "^0.5.1",
"multihashes": "^0.4.12",
"multiplex": "^6.7.0",
"number-to-bn": "^1.7.0",
"obj-multiplex": "^1.0.0",
"obs-store": "^3.0.0",
"obs-store": "^3.0.2",
"percentile": "^1.2.0",
"pify": "^3.0.0",
"ping-pong-stream": "^1.0.0",
@ -184,7 +191,9 @@
"react-addons-css-transition-group": "^15.6.0",
"react-dom": "^15.6.2",
"react-hyperscript": "^3.0.0",
"react-inspector": "^2.3.0",
"react-markdown": "^3.0.0",
"react-media": "^1.8.0",
"react-redux": "^5.0.5",
"react-router-dom": "^4.2.2",
"react-select": "^1.0.0",
@ -222,6 +231,7 @@
"@storybook/addon-info": "^3.4.2",
"@storybook/addon-knobs": "^3.4.2",
"@storybook/react": "^3.4.2",
"addons-linter": "^1.3.4",
"babel-core": "^6.24.1",
"babel-eslint": "^8.0.0",
"babel-plugin-transform-async-to-generator": "^6.24.1",
@ -246,8 +256,8 @@
"del": "^3.0.0",
"dot-only-hunter": "^1.0.3",
"envify": "^4.0.0",
"enzyme": "^3.3.0",
"enzyme-adapter-react-15": "^1.0.5",
"enzyme": "^3.4.4",
"enzyme-adapter-react-15": "^1.0.6",
"eslint-plugin-chai": "0.0.1",
"eslint-plugin-json": "^1.2.0",
"eslint-plugin-mocha": "^5.0.0",

File diff suppressed because it is too large Load Diff

@ -314,12 +314,12 @@ describe('Using MetaMask with an existing account', function () {
})
it('finds the transaction in the transactions list', async function () {
const transactions = await findElements(driver, By.css('.tx-list-item'))
const transactions = await findElements(driver, By.css('.transaction-list-item'))
assert.equal(transactions.length, 1)
const txValues = await findElements(driver, By.css('.tx-list-value'))
const txValues = await findElements(driver, By.css('.transaction-list-item__amount--primary'))
assert.equal(txValues.length, 1)
assert.equal(await txValues[0].getText(), '1 ETH')
assert.equal(await txValues[0].getText(), '-1 ETH')
})
})

@ -225,19 +225,9 @@ describe('MetaMask', function () {
await delay(regularDelayMs)
}
await clickWordAndWait(words[0])
await clickWordAndWait(words[1])
await clickWordAndWait(words[2])
await clickWordAndWait(words[3])
await clickWordAndWait(words[4])
await clickWordAndWait(words[5])
await clickWordAndWait(words[6])
await clickWordAndWait(words[7])
await clickWordAndWait(words[8])
await clickWordAndWait(words[9])
await clickWordAndWait(words[10])
await clickWordAndWait(words[11])
for (let i = 0; i < 12; i++) {
await clickWordAndWait(words[i])
}
} catch (e) {
if (count > 2) {
throw e
@ -414,12 +404,12 @@ describe('MetaMask', function () {
})
it('finds the transaction in the transactions list', async function () {
const transactions = await findElements(driver, By.css('.tx-list-item'))
const transactions = await findElements(driver, By.css('.transaction-list-item'))
assert.equal(transactions.length, 1)
if (process.env.SELENIUM_BROWSER !== 'firefox') {
const txValues = await findElement(driver, By.css('.tx-list-value'))
await driver.wait(until.elementTextMatches(txValues, /1\sETH/), 10000)
const txValues = await findElement(driver, By.css('.transaction-list-item__amount--primary'))
await driver.wait(until.elementTextMatches(txValues, /-1\sETH/), 10000)
}
})
})
@ -457,14 +447,11 @@ describe('MetaMask', function () {
})
it('finds the transaction in the transactions list', async function () {
const transactions = await findElements(driver, By.css('.tx-list-item'))
const transactions = await findElements(driver, By.css('.transaction-list-item'))
assert.equal(transactions.length, 2)
const txStatuses = await findElements(driver, By.css('.tx-list-status'))
await driver.wait(until.elementTextMatches(txStatuses[0], /Confirmed/))
const txValues = await findElement(driver, By.css('.tx-list-value'))
await driver.wait(until.elementTextMatches(txValues, /3\sETH/), 10000)
const txValues = await findElement(driver, By.css('.transaction-list-item__amount--primary'))
await driver.wait(until.elementTextMatches(txValues, /-3\sETH/), 10000)
})
})
@ -487,9 +474,9 @@ describe('MetaMask', function () {
await driver.switchTo().window(extension)
await delay(regularDelayMs)
const txListItem = await findElement(driver, By.xpath(`//span[contains(text(), 'Contract Deployment')]`))
const txListItem = await findElement(driver, By.xpath(`//div[contains(text(), 'Contract Deployment')]`))
await txListItem.click()
await delay(regularDelayMs)
await delay(largeDelayMs)
})
it('displays the contract creation data', async () => {
@ -511,13 +498,15 @@ describe('MetaMask', function () {
it('confirms a deploy contract transaction', async () => {
const confirmButton = await findElement(driver, By.xpath(`//button[contains(text(), 'Confirm')]`))
await confirmButton.click()
await delay(regularDelayMs)
await delay(largeDelayMs)
const txStatuses = await findElements(driver, By.css('.tx-list-status'))
await driver.wait(until.elementTextMatches(txStatuses[0], /Confirmed/))
driver.wait(async () => {
const confirmedTxes = await findElements(driver, By.css('.transaction-list__completed-transactions .transaction-list-item'))
return confirmedTxes.length === 3
}, 10000)
const txAccounts = await findElements(driver, By.css('.tx-list-account'))
assert.equal(await txAccounts[0].getText(), 'Contract Deployment')
const txAction = await findElements(driver, By.css('.transaction-list-item__action'))
await driver.wait(until.elementTextMatches(txAction[0], /Contract\sDeployment/), 10000)
await delay(regularDelayMs)
})
@ -538,9 +527,9 @@ describe('MetaMask', function () {
await driver.switchTo().window(extension)
await delay(largeDelayMs)
await findElements(driver, By.css('.tx-list-pending-item-container'))
const [txListValue] = await findElements(driver, By.css('.tx-list-value'))
await driver.wait(until.elementTextMatches(txListValue, /4\sETH/), 10000)
await findElements(driver, By.css('.transaction-list-item'))
const [txListValue] = await findElements(driver, By.css('.transaction-list-item__amount--primary'))
await driver.wait(until.elementTextMatches(txListValue, /-4\sETH/), 10000)
await txListValue.click()
await delay(regularDelayMs)
@ -568,15 +557,17 @@ describe('MetaMask', function () {
await confirmButton.click()
await delay(regularDelayMs)
const txStatuses = await findElements(driver, By.css('.tx-list-status'))
await driver.wait(until.elementTextMatches(txStatuses[0], /Confirmed/))
driver.wait(async () => {
const confirmedTxes = await findElements(driver, By.css('.transaction-list__completed-transactions .transaction-list-item'))
return confirmedTxes.length === 4
}, 10000)
const txValues = await findElement(driver, By.css('.tx-list-value'))
await driver.wait(until.elementTextMatches(txValues, /4\sETH/), 10000)
const txValues = await findElements(driver, By.css('.transaction-list-item__amount--primary'))
await driver.wait(until.elementTextMatches(txValues[0], /-4\sETH/), 10000)
const txAccounts = await findElements(driver, By.css('.tx-list-account'))
const firstTxAddress = await txAccounts[0].getText()
assert(firstTxAddress.match(/^0x\w{8}\.{3}\w{4}$/))
// const txAccounts = await findElements(driver, By.css('.tx-list-account'))
// const firstTxAddress = await txAccounts[0].getText()
// assert(firstTxAddress.match(/^0x\w{8}\.{3}\w{4}$/))
})
it('calls and confirms a contract method where ETH is received', async () => {
@ -590,7 +581,7 @@ describe('MetaMask', function () {
await driver.switchTo().window(extension)
await delay(regularDelayMs)
const txListItem = await findElement(driver, By.css('.tx-list-item'))
const txListItem = await findElement(driver, By.css('.transaction-list-item'))
await txListItem.click()
await delay(regularDelayMs)
@ -598,18 +589,20 @@ describe('MetaMask', function () {
await confirmButton.click()
await delay(regularDelayMs)
const txStatuses = await findElements(driver, By.css('.tx-list-status'))
await driver.wait(until.elementTextMatches(txStatuses[0], /Confirmed/))
driver.wait(async () => {
const confirmedTxes = await findElements(driver, By.css('.transaction-list__completed-transactions .transaction-list-item'))
return confirmedTxes.length === 5
}, 10000)
const txValues = await findElement(driver, By.css('.tx-list-value'))
await driver.wait(until.elementTextMatches(txValues, /0\sETH/), 10000)
const txValues = await findElement(driver, By.css('.transaction-list-item__amount--primary'))
await driver.wait(until.elementTextMatches(txValues, /-0\sETH/), 10000)
await closeAllWindowHandlesExcept(driver, [extension, dapp])
await driver.switchTo().window(extension)
})
it('renders the correct ETH balance', async () => {
const balance = await findElement(driver, By.css('.tx-view .balance-display .token-amount'))
const balance = await findElement(driver, By.css('.transaction-view-balance__primary-balance'))
await delay(regularDelayMs)
if (process.env.SELENIUM_BROWSER !== 'firefox') {
await driver.wait(until.elementTextMatches(balance, /^92.*ETH.*$/), 10000)
@ -654,18 +647,17 @@ describe('MetaMask', function () {
await closeAllWindowHandlesExcept(driver, [extension, dapp])
await delay(regularDelayMs)
await driver.switchTo().window(extension)
await delay(regularDelayMs)
await delay(largeDelayMs)
})
it('clicks on the Add Token button', async () => {
const addToken = await findElement(driver, By.xpath(`//button[contains(text(), 'Add Token')]`))
const addToken = await driver.findElement(By.css('.wallet-view__add-token-button'))
await addToken.click()
await delay(regularDelayMs)
})
it('picks the newly created Test token', async () => {
const addCustomToken = await findElement(driver, By.xpath("//div[contains(text(), 'Custom Token')]"))
const addCustomToken = await findElement(driver, By.xpath("//li[contains(text(), 'Custom Token')]"))
await addCustomToken.click()
await delay(regularDelayMs)
@ -683,7 +675,7 @@ describe('MetaMask', function () {
})
it('renders the balance for the new token', async () => {
const balance = await findElement(driver, By.css('.tx-view .balance-display .token-amount'))
const balance = await findElement(driver, By.css('.transaction-view-balance .transaction-view-balance__token-balance'))
await driver.wait(until.elementTextMatches(balance, /^100\s*TST\s*$/))
const tokenAmount = await balance.getText()
assert.ok(/^100\s*TST\s*$/.test(tokenAmount))
@ -752,21 +744,25 @@ describe('MetaMask', function () {
})
it('finds the transaction in the transactions list', async function () {
const transactions = await findElements(driver, By.css('.tx-list-item'))
const transactions = await findElements(driver, By.css('.transaction-list-item'))
assert.equal(transactions.length, 1)
const txValues = await findElements(driver, By.css('.tx-list-value'))
const txValues = await findElements(driver, By.css('.transaction-list-item__amount--primary'))
assert.equal(txValues.length, 1)
// test cancelled on firefox until https://github.com/mozilla/geckodriver/issues/906 is resolved,
// or possibly until we use latest version of firefox in the tests
if (process.env.SELENIUM_BROWSER !== 'firefox') {
await driver.wait(until.elementTextMatches(txValues[0], /50\sTST/), 10000)
await driver.wait(until.elementTextMatches(txValues[0], /-50\sTST/), 10000)
}
const txStatuses = await findElements(driver, By.css('.tx-list-status'))
const tx = await driver.wait(until.elementTextMatches(txStatuses[0], /Confirmed|Failed/), 10000)
assert.equal(await tx.getText(), 'Confirmed')
driver.wait(async () => {
const confirmedTxes = await findElements(driver, By.css('.transaction-list__completed-transactions .transaction-list-item'))
return confirmedTxes.length === 1
}, 10000)
const txStatuses = await findElements(driver, By.css('.transaction-list-item__action'))
const tx = await driver.wait(until.elementTextMatches(txStatuses[0], /Sent\sToken|Failed/), 10000)
assert.equal(await tx.getText(), 'Sent Tokens')
})
})
@ -789,9 +785,9 @@ describe('MetaMask', function () {
await driver.switchTo().window(extension)
await delay(largeDelayMs)
await findElements(driver, By.css('.tx-list-pending-item-container'))
const [txListValue] = await findElements(driver, By.css('.tx-list-value'))
await driver.wait(until.elementTextMatches(txListValue, /7\sTST/), 10000)
await findElements(driver, By.css('.transaction-list__pending-transactions'))
const [txListValue] = await findElements(driver, By.css('.transaction-list-item__amount--primary'))
await driver.wait(until.elementTextMatches(txListValue, /-7\sTST/), 10000)
await txListValue.click()
await delay(regularDelayMs)
@ -838,25 +834,28 @@ describe('MetaMask', function () {
})
it('finds the transaction in the transactions list', async function () {
const transactions = await findElements(driver, By.css('.tx-list-item'))
assert.equal(transactions.length, 2)
driver.wait(async () => {
const confirmedTxes = await findElements(driver, By.css('.transaction-list__completed-transactions .transaction-list-item'))
return confirmedTxes.length === 2
}, 10000)
const txValues = await findElements(driver, By.css('.tx-list-value'))
await driver.wait(until.elementTextMatches(txValues[0], /7\sTST/))
const txStatuses = await findElements(driver, By.css('.tx-list-status'))
await driver.wait(until.elementTextMatches(txStatuses[0], /Confirmed/))
const txValues = await findElements(driver, By.css('.transaction-list-item__amount--primary'))
await driver.wait(until.elementTextMatches(txValues[0], /-7\sTST/))
const txStatuses = await findElements(driver, By.css('.transaction-list-item__action'))
await driver.wait(until.elementTextMatches(txStatuses[0], /Sent\sToken/))
const walletBalance = await findElement(driver, By.css('.wallet-balance'))
await walletBalance.click()
const tokenListItems = await findElements(driver, By.css('.token-list-item'))
await tokenListItems[0].click()
await delay(regularDelayMs)
// test cancelled on firefox until https://github.com/mozilla/geckodriver/issues/906 is resolved,
// or possibly until we use latest version of firefox in the tests
if (process.env.SELENIUM_BROWSER !== 'firefox') {
const tokenBalanceAmount = await findElement(driver, By.css('.token-balance__amount'))
assert.equal(await tokenBalanceAmount.getText(), '43')
const tokenBalanceAmount = await findElement(driver, By.css('.transaction-view-balance__token-balance'))
assert.equal(await tokenBalanceAmount.getText(), '43 TST')
}
})
})
@ -880,9 +879,14 @@ describe('MetaMask', function () {
await driver.switchTo().window(extension)
await delay(regularDelayMs)
const [txListItem] = await findElements(driver, By.css('.tx-list-item'))
const [txListValue] = await findElements(driver, By.css('.tx-list-value'))
await driver.wait(until.elementTextMatches(txListValue, /0\sETH/))
driver.wait(async () => {
const pendingTxes = await findElements(driver, By.css('.transaction-list__pending-transactions .transaction-list-item'))
return pendingTxes.length === 1
}, 10000)
const [txListItem] = await findElements(driver, By.css('.transaction-list-item'))
const [txListValue] = await findElements(driver, By.css('.transaction-list-item__amount--primary'))
await driver.wait(until.elementTextMatches(txListValue, /-7\sTST/))
await txListItem.click()
await delay(regularDelayMs)
})
@ -953,10 +957,15 @@ describe('MetaMask', function () {
})
it('finds the transaction in the transactions list', async function () {
const txValues = await findElements(driver, By.css('.tx-list-value'))
await driver.wait(until.elementTextMatches(txValues[0], /0\sETH/))
const txStatuses = await findElements(driver, By.css('.tx-list-status'))
await driver.wait(until.elementTextMatches(txStatuses[0], /Confirmed/))
driver.wait(async () => {
const confirmedTxes = await findElements(driver, By.css('.transaction-list__completed-transactions .transaction-list-item'))
return confirmedTxes.length === 3
}, 10000)
const txValues = await findElements(driver, By.css('.transaction-list-item__amount--primary'))
await driver.wait(until.elementTextMatches(txValues[0], /-7\sTST/))
const txStatuses = await findElements(driver, By.css('.transaction-list-item__action'))
await driver.wait(until.elementTextMatches(txStatuses[0], /Approve/))
})
})
@ -1006,9 +1015,69 @@ describe('MetaMask', function () {
})
it('renders the balance for the chosen token', async () => {
const balance = await findElement(driver, By.css('.tx-view .balance-display .token-amount'))
const balance = await findElement(driver, By.css('.transaction-view-balance__token-balance'))
await driver.wait(until.elementTextMatches(balance, /0\sBAT/))
await delay(regularDelayMs)
})
})
describe('Stores custom RPC history', () => {
const customRpcUrls = [
'https://mainnet.infura.io/1',
'https://mainnet.infura.io/2',
'https://mainnet.infura.io/3',
'https://mainnet.infura.io/4',
]
customRpcUrls.forEach(customRpcUrl => {
it('creates custom RPC: ' + customRpcUrl, async () => {
const networkDropdown = await findElement(driver, By.css('.network-name'))
await networkDropdown.click()
await delay(regularDelayMs)
const customRpcButton = await findElement(driver, By.xpath(`//span[contains(text(), 'Custom RPC')]`))
await customRpcButton.click()
await delay(regularDelayMs)
const customRpcInput = await findElement(driver, By.css('input[placeholder="New RPC URL"]'))
await customRpcInput.clear()
await customRpcInput.sendKeys(customRpcUrl)
const customRpcSave = await findElement(driver, By.css('.settings-tab__rpc-save-button'))
await customRpcSave.click()
await delay(largeDelayMs * 2)
})
})
it('selects another provider', async () => {
const networkDropdown = await findElement(driver, By.css('.network-name'))
await networkDropdown.click()
await delay(regularDelayMs)
const customRpcButton = await findElement(driver, By.xpath(`//span[contains(text(), 'Main Ethereum Network')]`))
await customRpcButton.click()
await delay(largeDelayMs * 2)
})
it('finds 3 recent RPCs in history', async () => {
const networkDropdown = await findElement(driver, By.css('.network-name'))
await networkDropdown.click()
await delay(regularDelayMs)
// oldest selected RPC is not found
await assertElementNotPresent(webdriver, driver, By.xpath(`//span[contains(text(), '${customRpcUrls[0]}')]`))
// only recent 3 are found and in correct order (most recent at the top)
const customRpcs = await findElements(driver, By.xpath(`//span[contains(text(), 'https://mainnet.infura.io/')]`))
assert.equal(customRpcs.length, 3)
for (let i = 0; i < customRpcs.length; i++) {
const linkText = await customRpcs[i].getText()
const rpcUrl = customRpcUrls[customRpcUrls.length - i - 1]
assert.notEqual(linkText.indexOf(rpcUrl), -1)
}
})
})
})

@ -201,17 +201,17 @@ describe('Metamask popup page', function () {
})
it('balance renders', async function () {
await delay(200)
await delay(500)
const balance = await driver.findElement(By.css('#app-content > div > div.app-primary.from-right > div > div > div.flex-row > div.ether-balance.ether-balance-amount > div > div > div:nth-child(1) > div:nth-child(1)'))
assert.equal(await balance.getText(), '100.000')
await delay(200)
})
it('sends transaction', async function () {
const sendButton = await driver.findElement(By.css('#app-content > div > div.app-primary.from-right > div > div > div.flex-row > button:nth-child(4)'))
assert.equal(await sendButton.getText(), 'SEND')
await sendButton.click()
await delay(200)
const sendButton = await driver.findElement(By.css('#app-content > div > div.app-primary.from-right > div > div > div.flex-row > button:nth-child(4)'))
assert.equal(await sendButton.getText(), 'SEND')
await sendButton.click()
await delay(200)
})
it('adds recipient address and amount', async function () {

@ -86,7 +86,7 @@ async function runAddTokenFlowTest (assert, done) {
$('button.btn-primary.btn--large')[0].click()
// Verify added token image
let heroBalance = await queryAsync($, '.hero-balance')
let heroBalance = await queryAsync($, '.transaction-view-balance__balance-container')
assert.ok(heroBalance, 'rendered hero balance')
assert.ok(tokenImageUrl.indexOf(heroBalance.find('img').attr('src')) > -1, 'token added')
@ -134,7 +134,7 @@ async function runAddTokenFlowTest (assert, done) {
// $('button.btn-primary--lg')[0].click()
// Verify added token image
heroBalance = await queryAsync($, '.hero-balance')
heroBalance = await queryAsync($, '.transaction-view-balance__balance-container')
assert.ok(heroBalance, 'rendered hero balance')
assert.ok(heroBalance.find('.identicon')[0], 'token added')
}

@ -19,7 +19,7 @@ async function runConfirmSigRequestsTest (assert, done) {
selectState.val('confirm sig requests')
reactTriggerChange(selectState[0])
const pendingRequestItem = $.find('.tx-list-item.tx-list-pending-item-container.tx-list-clickable')
const pendingRequestItem = $.find('.transaction-list-item .transaction-list-item__grid')
if (pendingRequestItem[0]) {
pendingRequestItem[0].click()

@ -22,8 +22,8 @@ async function runCurrencyLocalizationTest (assert, done) {
await timeout(1000)
reactTriggerChange(selectState[0])
await timeout(1000)
const txView = await queryAsync($, '.tx-view')
const heroBalance = await findAsync($(txView), '.hero-balance')
const fiatAmount = await findAsync($(heroBalance), '.fiat-amount')
assert.equal(fiatAmount[0].textContent, '₱102,707.97')
const txView = await queryAsync($, '.transaction-view')
const heroBalance = await findAsync($(txView), '.transaction-view-balance__balance')
const fiatAmount = await findAsync($(heroBalance), '.transaction-view-balance__secondary-balance')
assert.equal(fiatAmount[0].textContent, '₱102,707.97 PHP')
}

@ -58,7 +58,7 @@ async function runSendFlowTest (assert, done) {
selectState.val('send new ui')
reactTriggerChange(selectState[0])
const sendScreenButton = await queryAsync($, 'button.btn-primary.hero-balance-button')
const sendScreenButton = await queryAsync($, 'button.btn-primary.transaction-view-balance__button')
assert.ok(sendScreenButton[1], 'send screen button present')
sendScreenButton[1].click()
@ -124,10 +124,10 @@ async function runSendFlowTest (assert, done) {
selectState.val('send edit')
reactTriggerChange(selectState[0])
const confirmFromName = (await queryAsync($, '.sender-to-recipient__sender-name')).first()
const confirmFromName = (await queryAsync($, '.sender-to-recipient__name')).first()
assert.equal(confirmFromName[0].textContent, 'Send Account 4', 'confirm screen should show correct from name')
const confirmToName = (await queryAsync($, '.sender-to-recipient__recipient-name')).last()
const confirmToName = (await queryAsync($, '.sender-to-recipient__name')).last()
assert.equal(confirmToName[0].textContent, 'Send Account 3', 'confirm screen should show correct to name')
const confirmScreenRowFiats = await queryAsync($, '.confirm-detail-row__fiat')

@ -29,26 +29,25 @@ async function runTxListItemsTest (assert, done) {
assert.ok(metamaskLogo[0], 'metamask logo present')
metamaskLogo[0].click()
const txListItems = await queryAsync($, '.tx-list-item')
const txListItems = await queryAsync($, '.transaction-list-item')
assert.equal(txListItems.length, 8, 'all tx list items are rendered')
const unapprovedTx = txListItems[0]
assert.equal($(unapprovedTx).hasClass('tx-list-pending-item-container'), true, 'unapprovedTx has the correct class')
const retryTx = txListItems[1]
const retryTxLink = await findAsync($(retryTx), '.tx-list-item-retry-container span')
assert.equal(retryTxLink[0].textContent, 'Taking too long? Increase the gas price on your transaction', 'retryTx has expected link')
const retryTxGrid = await findAsync($(txListItems[1]), '.transaction-list-item__grid')
retryTxGrid[0].click()
const retryTxDetails = await findAsync($, '.transaction-list-item-details')
const headerButtons = await findAsync($(retryTxDetails[0]), '.transaction-list-item-details__header-button')
assert.equal(headerButtons[0].textContent, 'speed up')
const approvedTx = txListItems[2]
const approvedTxRenderedStatus = await findAsync($(approvedTx), '.tx-list-status')
assert.equal(approvedTxRenderedStatus[0].textContent, 'Approved', 'approvedTx has correct label')
const approvedTxRenderedStatus = await findAsync($(approvedTx), '.transaction-list-item__status')
assert.equal(approvedTxRenderedStatus[0].textContent, 'pending', 'approvedTx has correct label')
const unapprovedMsg = txListItems[3]
const unapprovedMsgDescription = await findAsync($(unapprovedMsg), '.tx-list-account')
const unapprovedMsgDescription = await findAsync($(unapprovedMsg), '.transaction-list-item__action')
assert.equal(unapprovedMsgDescription[0].textContent, 'Signature Request', 'unapprovedMsg has correct description')
const failedTx = txListItems[4]
const failedTxRenderedStatus = await findAsync($(failedTx), '.tx-list-status')
const failedTxRenderedStatus = await findAsync($(failedTx), '.transaction-list-item__status')
assert.equal(failedTxRenderedStatus[0].textContent, 'Failed', 'failedTx has correct label')
const shapeShiftTx = txListItems[5]
@ -56,10 +55,10 @@ async function runTxListItemsTest (assert, done) {
assert.equal(shapeShiftTxStatus[0].textContent, 'No deposits received', 'shapeShiftTx has correct status')
const confirmedTokenTx = txListItems[6]
const confirmedTokenTxAddress = await findAsync($(confirmedTokenTx), '.tx-list-account')
assert.equal(confirmedTokenTxAddress[0].textContent, '0xE7884118...81a9', 'confirmedTokenTx has correct address')
const confirmedTokenTxAddress = await findAsync($(confirmedTokenTx), '.transaction-list-item__status')
assert.equal(confirmedTokenTxAddress[0].textContent, 'Confirmed', 'confirmedTokenTx has correct address')
const rejectedTx = txListItems[7]
const rejectedTxRenderedStatus = await findAsync($(rejectedTx), '.tx-list-status')
const rejectedTxRenderedStatus = await findAsync($(rejectedTx), '.transaction-list-item__status')
assert.equal(rejectedTxRenderedStatus[0].textContent, 'Rejected', 'rejectedTx has correct label')
}

@ -1,9 +0,0 @@
const ObservableStore = require('obs-store')
const clone = require('clone')
const ConfigManager = require('../../app/scripts/lib/config-manager')
const firstTimeState = require('../../app/scripts/first-time-state')
module.exports = function () {
const store = new ObservableStore(clone(firstTimeState))
return new ConfigManager({ store })
}

@ -0,0 +1,42 @@
const { shallow, mount } = require('enzyme')
import { BrowserRouter } from 'react-router-dom'
import { shape } from 'prop-types'
module.exports = {
shallowWithStore,
mountWithStore,
mountWithRouter,
}
function shallowWithStore (component, store) {
const context = {
store,
}
return shallow(component, {context})
}
function mountWithStore (component, store) {
const context = {
store,
}
return mount(component, {context})
}
function mountWithRouter (node) {
// Instantiate router context
const router = {
history: new BrowserRouter().history,
route: {
location: {},
match: {},
},
}
const createContext = () => ({
context: { router, t: () => {} },
childContextTypes: { router: shape({}), t: () => {} },
})
return mount(node, createContext())
}

@ -1,20 +0,0 @@
const { shallow, mount } = require('enzyme')
module.exports = {
shallowWithStore,
mountWithStore,
}
function shallowWithStore (component, store) {
const context = {
store,
}
return shallow(component, {context})
}
function mountWithStore (component, store) {
const context = {
store,
}
return mount(component, {context})
}

@ -0,0 +1,33 @@
const assert = require('assert')
const cleanErrorStack = require('../../../app/scripts/lib/cleanErrorStack')
describe('Clean Error Stack', () => {
const testMessage = 'Test Message'
const testError = new Error(testMessage)
const undefinedErrorName = new Error(testMessage)
const blankErrorName = new Error(testMessage)
const blankMsgError = new Error()
beforeEach(() => {
undefinedErrorName.name = undefined
blankErrorName.name = ''
})
it('tests error with message', () => {
assert.equal(cleanErrorStack(testError), 'Error: Test Message')
})
it('tests error with undefined name', () => {
assert.equal(cleanErrorStack(undefinedErrorName).toString(), 'Error: Test Message')
})
it('tests error with blank name', () => {
assert.equal(cleanErrorStack(blankErrorName).toString(), 'Test Message')
})
it('tests error with blank message', () => {
assert.equal(cleanErrorStack(blankMsgError), 'Error')
})
})

@ -584,22 +584,18 @@ describe('MetaMaskController', function () {
})
describe('#clearSeedWordCache', function () {
it('should set seed words to null', function (done) {
sandbox.stub(metamaskController.preferencesController, 'setSeedWords')
metamaskController.clearSeedWordCache((err) => {
if (err) {
done(err)
}
it('should have set seed words', function () {
metamaskController.configManager.setSeedWords('test words')
const getConfigSeed = metamaskController.configManager.getSeedWords()
assert.equal(getConfigSeed, 'test words')
})
it('should clear config seed phrase', function () {
metamaskController.configManager.setSeedWords('test words')
metamaskController.clearSeedWordCache((err, result) => {
if (err) console.log(err)
assert.ok(metamaskController.preferencesController.setSeedWords.calledOnce)
assert.deepEqual(metamaskController.preferencesController.setSeedWords.args, [[null]])
done()
})
const getConfigSeed = metamaskController.configManager.getSeedWords()
assert.equal(getConfigSeed, null)
})
})
describe('#setCurrentLocale', function () {
@ -793,24 +789,95 @@ describe('MetaMaskController', function () {
describe('#markAccountsFound', function () {
it('adds lost accounts to config manager data', function () {
metamaskController.markAccountsFound(noop)
const configManagerData = metamaskController.configManager.getData()
assert.deepEqual(configManagerData.lostAccounts, [])
const state = metamaskController.getState()
assert.deepEqual(state.lostAccounts, [])
})
})
describe('#markPasswordForgotten', function () {
it('adds and sets forgottenPassword to config data to true', function () {
metamaskController.markPasswordForgotten(noop)
const configManagerData = metamaskController.configManager.getData()
assert.equal(configManagerData.forgottenPassword, true)
const state = metamaskController.getState()
assert.equal(state.forgottenPassword, true)
})
})
describe('#unMarkPasswordForgotten', function () {
it('adds and sets forgottenPassword to config data to false', function () {
metamaskController.unMarkPasswordForgotten(noop)
const configManagerData = metamaskController.configManager.getData()
assert.equal(configManagerData.forgottenPassword, false)
const state = metamaskController.getState()
assert.equal(state.forgottenPassword, false)
})
})
describe('#_onKeyringControllerUpdate', function () {
it('should do nothing if there are no keyrings in state', async function () {
const addAddresses = sinon.fake()
const syncWithAddresses = sinon.fake()
sandbox.replace(metamaskController, 'preferencesController', {
addAddresses,
})
sandbox.replace(metamaskController, 'accountTracker', {
syncWithAddresses,
})
const oldState = metamaskController.getState()
await metamaskController._onKeyringControllerUpdate({keyrings: []})
assert.ok(addAddresses.notCalled)
assert.ok(syncWithAddresses.notCalled)
assert.deepEqual(metamaskController.getState(), oldState)
})
it('should update selected address if keyrings was locked', async function () {
const addAddresses = sinon.fake()
const getSelectedAddress = sinon.fake.returns('0x42')
const setSelectedAddress = sinon.fake()
const syncWithAddresses = sinon.fake()
sandbox.replace(metamaskController, 'preferencesController', {
addAddresses,
getSelectedAddress,
setSelectedAddress,
})
sandbox.replace(metamaskController, 'accountTracker', {
syncWithAddresses,
})
const oldState = metamaskController.getState()
await metamaskController._onKeyringControllerUpdate({
isUnlocked: false,
keyrings: [{
accounts: ['0x1', '0x2'],
}],
})
assert.deepEqual(addAddresses.args, [[['0x1', '0x2']]])
assert.deepEqual(syncWithAddresses.args, [[['0x1', '0x2']]])
assert.deepEqual(setSelectedAddress.args, [['0x1']])
assert.deepEqual(metamaskController.getState(), oldState)
})
it('should NOT update selected address if already unlocked', async function () {
const addAddresses = sinon.fake()
const syncWithAddresses = sinon.fake()
sandbox.replace(metamaskController, 'preferencesController', {
addAddresses,
})
sandbox.replace(metamaskController, 'accountTracker', {
syncWithAddresses,
})
const oldState = metamaskController.getState()
await metamaskController._onKeyringControllerUpdate({
isUnlocked: true,
keyrings: [{
accounts: ['0x1', '0x2'],
}],
})
assert.deepEqual(addAddresses.args, [[['0x1', '0x2']]])
assert.deepEqual(syncWithAddresses.args, [[['0x1', '0x2']]])
assert.deepEqual(metamaskController.getState(), oldState)
})
})

@ -1,16 +1,11 @@
const assert = require('assert')
const configManagerGen = require('../../../lib/mock-config-manager')
const NoticeController = require('../../../../app/scripts/notice-controller')
describe('notice-controller', function () {
var noticeController
beforeEach(function () {
// simple localStorage polyfill
const configManager = configManagerGen()
noticeController = new NoticeController({
configManager: configManager,
})
noticeController = new NoticeController()
})
describe('notices', function () {

@ -1,6 +1,7 @@
const assert = require('assert')
const ObservableStore = require('obs-store')
const PreferencesController = require('../../../../app/scripts/controllers/preferences')
const sinon = require('sinon')
describe('preferences controller', function () {
let preferencesController
@ -339,5 +340,144 @@ describe('preferences controller', function () {
assert.deepEqual(tokensSecond, initialTokensSecond, 'tokens equal for same network')
})
})
describe('on watchAsset', function () {
var stubNext, stubEnd, stubHandleWatchAssetERC20, asy, req, res
const sandbox = sinon.createSandbox()
beforeEach(() => {
req = {params: {}}
res = {}
asy = {next: () => {}, end: () => {}}
stubNext = sandbox.stub(asy, 'next')
stubEnd = sandbox.stub(asy, 'end').returns(0)
stubHandleWatchAssetERC20 = sandbox.stub(preferencesController, '_handleWatchAssetERC20')
})
after(() => {
sandbox.restore()
})
it('shouldn not do anything if method not corresponds', async function () {
const asy = {next: () => {}, end: () => {}}
var stubNext = sandbox.stub(asy, 'next')
var stubEnd = sandbox.stub(asy, 'end').returns(0)
req.method = 'metamask'
await preferencesController.requestWatchAsset(req, res, asy.next, asy.end)
sandbox.assert.notCalled(stubEnd)
sandbox.assert.called(stubNext)
})
it('should do something if method is supported', async function () {
const asy = {next: () => {}, end: () => {}}
var stubNext = sandbox.stub(asy, 'next')
var stubEnd = sandbox.stub(asy, 'end').returns(0)
req.method = 'metamask_watchAsset'
req.params.type = 'someasset'
await preferencesController.requestWatchAsset(req, res, asy.next, asy.end)
sandbox.assert.called(stubEnd)
sandbox.assert.notCalled(stubNext)
})
it('should through error if method is supported but asset type is not', async function () {
req.method = 'metamask_watchAsset'
req.params.type = 'someasset'
await preferencesController.requestWatchAsset(req, res, asy.next, asy.end)
sandbox.assert.called(stubEnd)
sandbox.assert.notCalled(stubHandleWatchAssetERC20)
sandbox.assert.notCalled(stubNext)
assert.deepEqual(res, {})
})
it('should trigger handle add asset if type supported', async function () {
const asy = {next: () => {}, end: () => {}}
req.method = 'metamask_watchAsset'
req.params.type = 'ERC20'
await preferencesController.requestWatchAsset(req, res, asy.next, asy.end)
sandbox.assert.called(stubHandleWatchAssetERC20)
})
})
describe('on watchAsset of type ERC20', function () {
var req
const sandbox = sinon.createSandbox()
beforeEach(() => {
req = {params: {type: 'ERC20'}}
})
after(() => {
sandbox.restore()
})
it('should add suggested token', async function () {
const address = '0xabcdef1234567'
const symbol = 'ABBR'
const decimals = 5
const image = 'someimage'
req.params.options = { address, symbol, decimals, image }
sandbox.stub(preferencesController, '_validateERC20AssetParams').returns(true)
preferencesController.showWatchAssetUi = async () => {}
await preferencesController._handleWatchAssetERC20(req.params.options)
const suggested = preferencesController.getSuggestedTokens()
assert.equal(Object.keys(suggested).length, 1, `one token added ${Object.keys(suggested)}`)
assert.equal(suggested[address].address, address, 'set address correctly')
assert.equal(suggested[address].symbol, symbol, 'set symbol correctly')
assert.equal(suggested[address].decimals, decimals, 'set decimals correctly')
assert.equal(suggested[address].image, image, 'set image correctly')
})
it('should add token correctly if user confirms', async function () {
const address = '0xabcdef1234567'
const symbol = 'ABBR'
const decimals = 5
const image = 'someimage'
req.params.options = { address, symbol, decimals, image }
sandbox.stub(preferencesController, '_validateERC20AssetParams').returns(true)
preferencesController.showWatchAssetUi = async () => {
await preferencesController.addToken(address, symbol, decimals, image)
}
await preferencesController._handleWatchAssetERC20(req.params.options)
const tokens = preferencesController.getTokens()
assert.equal(tokens.length, 1, `one token added`)
const added = tokens[0]
assert.equal(added.address, address, 'set address correctly')
assert.equal(added.symbol, symbol, 'set symbol correctly')
assert.equal(added.decimals, decimals, 'set decimals correctly')
const assetImages = preferencesController.getAssetImages()
assert.ok(assetImages[address], `set image correctly`)
})
})
describe('setPasswordForgotten', function () {
it('should default to false', function () {
const state = preferencesController.store.getState()
assert.equal(state.forgottenPassword, false)
})
it('should set the forgottenPassword property in state', function () {
assert.equal(preferencesController.store.getState().forgottenPassword, false)
preferencesController.setPasswordForgotten(true)
assert.equal(preferencesController.store.getState().forgottenPassword, true)
})
})
describe('setSeedWords', function () {
it('should default to null', function () {
const state = preferencesController.store.getState()
assert.equal(state.seedWords, null)
})
it('should set the seedWords property in state', function () {
assert.equal(preferencesController.store.getState().seedWords, null)
preferencesController.setSeedWords('foo bar baz')
assert.equal(preferencesController.store.getState().seedWords, 'foo bar baz')
})
})
})

@ -1,7 +1,7 @@
const assert = require('assert')
const h = require('react-hyperscript')
const { createMockStore } = require('redux-test-utils')
const { shallowWithStore } = require('../../lib/shallow-with-store')
const { shallowWithStore } = require('../../lib/render-helpers')
const BalanceComponent = require('../../../ui/app/components/balance-component')
const mockState = {
metamask: {
@ -42,4 +42,3 @@ describe('BalanceComponent', function () {
})
})

@ -1,112 +0,0 @@
const assert = require('assert')
const configManagerGen = require('../lib/mock-config-manager')
describe('config-manager', function () {
var configManager
beforeEach(function () {
configManager = configManagerGen()
})
describe('#setConfig', function () {
it('should set the config key', function () {
var testConfig = {
provider: {
type: 'rpc',
rpcTarget: 'foobar',
},
}
configManager.setConfig(testConfig)
var result = configManager.getData()
assert.equal(result.config.provider.type, testConfig.provider.type)
assert.equal(result.config.provider.rpcTarget, testConfig.provider.rpcTarget)
})
it('setting wallet should not overwrite config', function () {
var testConfig = {
provider: {
type: 'rpc',
rpcTarget: 'foobar',
},
}
configManager.setConfig(testConfig)
var testWallet = {
name: 'this is my fake wallet',
}
configManager.setWallet(testWallet)
var result = configManager.getData()
assert.equal(result.wallet.name, testWallet.name, 'wallet name is set')
assert.equal(result.config.provider.rpcTarget, testConfig.provider.rpcTarget)
testConfig.provider.type = 'something else!'
configManager.setConfig(testConfig)
result = configManager.getData()
assert.equal(result.wallet.name, testWallet.name, 'wallet name is set')
assert.equal(result.config.provider.rpcTarget, testConfig.provider.rpcTarget)
assert.equal(result.config.provider.type, testConfig.provider.type)
})
})
describe('wallet nicknames', function () {
it('should return null when no nicknames are saved', function () {
var nick = configManager.nicknameForWallet('0x0')
assert.equal(nick, null, 'no nickname returned')
})
it('should persist nicknames', function () {
var account = '0x0'
var nick1 = 'foo'
var nick2 = 'bar'
configManager.setNicknameForWallet(account, nick1)
var result1 = configManager.nicknameForWallet(account)
assert.equal(result1, nick1)
configManager.setNicknameForWallet(account, nick2)
var result2 = configManager.nicknameForWallet(account)
assert.equal(result2, nick2)
})
})
describe('rpc manipulations', function () {
it('changing rpc should return a different rpc', function () {
var firstRpc = 'first'
var secondRpc = 'second'
configManager.setRpcTarget(firstRpc)
var firstResult = configManager.getCurrentRpcAddress()
assert.equal(firstResult, firstRpc)
configManager.setRpcTarget(secondRpc)
var secondResult = configManager.getCurrentRpcAddress()
assert.equal(secondResult, secondRpc)
})
})
describe('transactions', function () {
beforeEach(function () {
configManager.setTxList([])
})
describe('#getTxList', function () {
it('when new should return empty array', function () {
var result = configManager.getTxList()
assert.ok(Array.isArray(result))
assert.equal(result.length, 0)
})
})
describe('#setTxList', function () {
it('saves the submitted data to the tx list', function () {
var target = [{ foo: 'bar' }]
configManager.setTxList(target)
var result = configManager.getTxList()
assert.equal(result[0].foo, 'bar')
})
})
})
})

@ -6,7 +6,7 @@ const path = require('path')
const Dropdown = require(path.join(__dirname, '..', '..', '..', '..', 'ui', 'app', 'components', 'dropdowns', 'index.js')).Dropdown
const { createMockStore } = require('redux-test-utils')
const { mountWithStore } = require('../../../lib/shallow-with-store')
const { mountWithStore } = require('../../../lib/render-helpers')
const mockState = {
metamask: {

@ -1,7 +1,7 @@
const assert = require('assert')
const { createMockStore } = require('redux-test-utils')
const h = require('react-hyperscript')
const { shallowWithStore } = require('../../lib/shallow-with-store')
const { shallowWithStore } = require('../../lib/render-helpers')
const AddTokenScreen = require('../../../old-ui/app/add-token')
describe('Add Token Screen', function () {

@ -0,0 +1,36 @@
import React from 'react'
import assert from 'assert'
import thunk from 'redux-thunk'
import configureMockStore from 'redux-mock-store'
import { mount } from 'enzyme'
import IdenticonComponent from '../../../../../ui/app/components/identicon'
describe('Identicon Component', () => {
const state = {
metamask: {
useBlockie: false,
},
}
const middlewares = [thunk]
const mockStore = configureMockStore(middlewares)
const store = mockStore(state)
it('renders default eth_logo identicon with no props', () => {
const wrapper = mount(<IdenticonComponent store={store}/>)
assert.equal(wrapper.find('img.balance-icon').prop('src'), './images/eth_logo.svg')
})
it('renders custom image and add className props', () => {
const wrapper = mount(<IdenticonComponent store={store} className={'test-image'} image={'test-image'} />)
assert.equal(wrapper.find('img.test-image').prop('className'), 'test-image identicon')
assert.equal(wrapper.find('img.test-image').prop('src'), 'test-image')
})
it('renders div with address prop', () => {
const wrapper = mount(<IdenticonComponent store={store} className={'test-address'} address={'0xTest'} />)
assert.equal(wrapper.find('div.test-address').prop('className'), 'test-address identicon')
})
})

@ -0,0 +1,69 @@
import React from 'react'
import assert from 'assert'
import thunk from 'redux-thunk'
import { Provider } from 'react-redux'
import configureMockStore from 'redux-mock-store'
import { mount } from 'enzyme'
import TokenCell from '../../../../../ui/app/components/token-cell'
import Identicon from '../../../../../ui/app/components/identicon'
describe('Token Cell', () => {
let wrapper
const state = {
metamask: {
network: 'test',
currentCurrency: 'usd',
selectedTokenAddress: '0xToken',
selectedAddress: '0xAddress',
contractExchangeRates: {
'0xAnotherToken': 0.015,
},
conversionRate: 7.00,
},
appState: {
sidebar: {
isOpen: true,
},
},
}
const middlewares = [thunk]
const mockStore = configureMockStore(middlewares)
const store = mockStore(state)
beforeEach(() => {
wrapper = mount(
<Provider store={store}>
<TokenCell
address={'0xAnotherToken'}
symbol={'TEST'}
string={'5.000'}
network={22}
currentCurrency={'usd'}
image={'./test-image'}
/>
</Provider>
)
})
it('renders Identicon with props from token cell', () => {
assert.equal(wrapper.find(Identicon).prop('address'), '0xAnotherToken')
assert.equal(wrapper.find(Identicon).prop('network'), 'test')
assert.equal(wrapper.find(Identicon).prop('image'), './test-image')
})
it('renders token balance', () => {
assert.equal(wrapper.find('.token-list-item__token-balance').text(), '5.000')
})
it('renders token symbol', () => {
assert.equal(wrapper.find('.token-list-item__token-symbol').text(), 'TEST')
})
it('renders converted fiat amount', () => {
assert.equal(wrapper.find('.token-list-item__fiat-amount').text(), '0.52 USD')
})
})

@ -0,0 +1,175 @@
const assert = require('assert')
const selectors = require('../../../../ui/app/selectors')
const mockState = require('../../../data/mock-state.json')
const Eth = require('ethjs')
const { createTestProviderTools } = require('../../../stub/provider')
const provider = createTestProviderTools({ scaffold: {}}).provider
describe('Selectors', function () {
describe('#getSelectedAddress', function () {
let state
beforeEach(function () {
state = {
metamask: {
accounts: {
'0x0dcd5d886577d5081b0c52e242ef29e70be3e7bc': {
'balance': '0x0',
'address': '0x0dcd5d886577d5081b0c52e242ef29e70be3e7bc',
},
},
},
}
})
it('returns first account if selectedAddress is undefined', function () {
assert.equal(selectors.getSelectedAddress(state), '0x0dcd5d886577d5081b0c52e242ef29e70be3e7bc')
})
it('returns selectedAddress', function () {
assert.equal(selectors.getSelectedAddress(mockState), '0x0dcd5d886577d5081b0c52e242ef29e70be3e7bc')
})
})
it('returns selected identity', function () {
const identity = selectors.getSelectedIdentity(mockState)
assert.equal(identity.address, '0x0dcd5d886577d5081b0c52e242ef29e70be3e7bc')
assert.equal(identity.name, 'Test Account')
})
it('returns selected account', function () {
const account = selectors.getSelectedAccount(mockState)
assert.equal(account.balance, '0x0')
assert.equal(account.address, '0x0dcd5d886577d5081b0c52e242ef29e70be3e7bc')
})
it('returns selected token from first token list', function () {
const token = selectors.getSelectedToken(mockState)
assert.equal(token.address, '0x108cf70c7d384c552f42c07c41c0e1e46d77ea0d')
assert.equal(token.symbol, 'TEST')
assert.equal(token.decimals, '0')
})
describe('#getSelectedTokenExchangeRate', function () {
it('returns token exchange rate for first token', function () {
const tokenRate = selectors.getSelectedTokenExchangeRate(mockState)
assert.equal(tokenRate, '0.00039345803819379796')
})
})
describe('#getTokenExchangeRate', function () {
let missingTokenRate
beforeEach(function () {
missingTokenRate = {
metamask: {
'contractExchangeRates': {},
},
}
})
it('returns 0 token exchange rate for a token not in state', function () {
const tokenRate = selectors.getTokenExchangeRate(missingTokenRate, '0xd8f6a2ffb0fc5952d16c9768b71cfd35b6399aa5')
assert.equal(tokenRate, 0)
})
it('returns token exchange rate for specified token in state', function () {
const tokenRate = selectors.getTokenExchangeRate(mockState, '0xd8f6a2ffb0fc5952d16c9768b71cfd35b6399aa5')
assert.equal(tokenRate, 0.00008189274407698049)
})
})
it('returns conversionRate from state', function () {
assert.equal(selectors.conversionRateSelector(mockState), 556.12)
})
it('returns address book from state', function () {
const addressBook = selectors.getAddressBook(mockState)
assert.equal(addressBook[0].address, '0xc42edfcc21ed14dda456aa0756c153f7985d8813')
assert.equal(addressBook[0].name, '')
})
it('returns accounts with balance, address, and name from identity and accounts in state', function () {
const accountsWithSendEther = selectors.accountsWithSendEtherInfoSelector(mockState)
assert.equal(accountsWithSendEther.length, 2)
assert.equal(accountsWithSendEther[0].balance, '0x0')
assert.equal(accountsWithSendEther[0].address, '0x0dcd5d886577d5081b0c52e242ef29e70be3e7bc')
assert.equal(accountsWithSendEther[0].name, 'Test Account')
})
it('returns selected account with balance, address, and name from accountsWithSendEtherInfoSelector', function () {
const currentAccountwithSendEther = selectors.getCurrentAccountWithSendEtherInfo(mockState)
assert.equal(currentAccountwithSendEther.balance, '0x0')
assert.equal(currentAccountwithSendEther.address, '0x0dcd5d886577d5081b0c52e242ef29e70be3e7bc')
assert.equal(currentAccountwithSendEther.name, 'Test Account')
})
describe('#transactionSelector', function () {
it('returns transactions from state', function () {
selectors.transactionsSelector(mockState)
})
})
it('#getGasIsLoading', () => {
const gasIsLoading = selectors.getGasIsLoading(mockState)
assert.equal(gasIsLoading, false)
})
describe('Send From', () => {
it('#getSendFrom', () => {
const sendFrom = selectors.getSendFrom(mockState)
assert.equal(sendFrom, '0xc42edfcc21ed14dda456aa0756c153f7985d8813')
})
it('#getForceGasMin', () => {
const forceGasMin = selectors.getForceGasMin(mockState)
assert.equal(forceGasMin, null)
})
it('#getSendAmount', () => {
const sendAmount = selectors.getSendAmount(mockState)
assert.equal(sendAmount, '1bc16d674ec80000')
})
it('#getSendMaxModeState', () => {
const sendMaxModeState = selectors.getSendMaxModeState(mockState)
assert.equal(sendMaxModeState, false)
})
})
it('#getCurrentCurrency', () => {
const currentCurrency = selectors.getCurrentCurrency(mockState)
assert.equal(currentCurrency, 'usd')
})
it('#getSelectedTokenToFiatRate', () => {
const selectedTokenToFiatRate = selectors.getSelectedTokenToFiatRate(mockState)
assert.equal(selectedTokenToFiatRate, '0.21880988420033493')
})
describe('#getSelectedTokenContract', () => {
beforeEach(() => {
global.eth = new Eth(provider)
})
it('', () => {
const selectedTokenContract = selectors.getSelectedTokenContract(mockState)
assert(selectedTokenContract.abi)
})
})
it('#getCurrentViewContext', () => {
const currentViewContext = selectors.getCurrentViewContext(mockState)
assert.equal(currentViewContext, '0x0dcd5d886577d5081b0c52e242ef29e70be3e7bc')
})
it('#getTotalUnapprovedCount', () => {
const totalUnapprovedCount = selectors.getTotalUnapprovedCount(mockState)
assert.equal(totalUnapprovedCount, 1)
})
})

@ -0,0 +1,26 @@
const assert = require('assert')
const etherscanNetworkPrefix = require('../../../ui/lib/etherscan-prefix-for-network')
describe('Etherscan Network Prefix', () => {
it('returns empy string as default value', () => {
assert.equal(etherscanNetworkPrefix(), '')
})
it('returns empty string as a prefix for networkId of 1', () => {
assert.equal(etherscanNetworkPrefix(1), '')
})
it('returns ropsten as prefix for networkId of 3', () => {
assert.equal(etherscanNetworkPrefix(3), 'ropsten.')
})
it('returns rinkeby as prefix for networkId of 4', () => {
assert.equal(etherscanNetworkPrefix(4), 'rinkeby.')
})
it('returs kovan as prefix for networkId of 42', () => {
assert.equal(etherscanNetworkPrefix(42), 'kovan.')
})
})

@ -1,33 +0,0 @@
const Component = require('react').Component
const h = require('react-hyperscript')
const inherits = require('util').inherits
// Main Views
const TxView = require('./components/tx-view')
const WalletView = require('./components/wallet-view')
module.exports = AccountAndTransactionDetails
inherits(AccountAndTransactionDetails, Component)
function AccountAndTransactionDetails () {
Component.call(this)
}
AccountAndTransactionDetails.prototype.render = function () {
return h('div.account-and-transaction-details', [
// wallet
h(WalletView, {
style: {
},
responsiveDisplayClassname: '.lap-visible',
}, [
]),
// transaction
h(TxView, {
style: {
},
}, [
]),
])
}

@ -227,11 +227,14 @@ var actions = {
SET_PROVIDER_TYPE: 'SET_PROVIDER_TYPE',
showConfigPage,
SHOW_ADD_TOKEN_PAGE: 'SHOW_ADD_TOKEN_PAGE',
SHOW_ADD_SUGGESTED_TOKEN_PAGE: 'SHOW_ADD_SUGGESTED_TOKEN_PAGE',
showAddTokenPage,
showAddSuggestedTokenPage,
addToken,
addTokens,
removeToken,
updateTokens,
removeSuggestedTokens,
UPDATE_TOKENS: 'UPDATE_TOKENS',
setRpcTarget: setRpcTarget,
setProviderType: setProviderType,
@ -312,6 +315,8 @@ var actions = {
CLEAR_PENDING_TOKENS: 'CLEAR_PENDING_TOKENS',
setPendingTokens,
clearPendingTokens,
createCancelTransaction,
}
module.exports = actions
@ -410,12 +415,18 @@ function createNewVaultAndRestore (password, seed) {
log.debug(`background.createNewVaultAndRestore`)
return new Promise((resolve, reject) => {
background.createNewVaultAndRestore(password, seed, err => {
background.clearSeedWordCache((err) => {
if (err) {
return reject(err)
}
resolve()
background.createNewVaultAndRestore(password, seed, (err) => {
if (err) {
return reject(err)
}
resolve()
})
})
})
.then(() => dispatch(actions.unMarkPasswordForgotten()))
@ -1147,6 +1158,10 @@ function updateAndApproveTx (txData) {
return txData
})
.catch((err) => {
dispatch(actions.hideLoadingIndication())
return Promise.reject(err)
})
}
}
@ -1589,11 +1604,18 @@ function showAddTokenPage (transitionForward = true) {
}
}
function addToken (address, symbol, decimals) {
function showAddSuggestedTokenPage (transitionForward = true) {
return {
type: actions.SHOW_ADD_SUGGESTED_TOKEN_PAGE,
value: transitionForward,
}
}
function addToken (address, symbol, decimals, image) {
return (dispatch) => {
dispatch(actions.showLoadingIndication())
return new Promise((resolve, reject) => {
background.addToken(address, symbol, decimals, (err, tokens) => {
background.addToken(address, symbol, decimals, image, (err, tokens) => {
dispatch(actions.hideLoadingIndication())
if (err) {
dispatch(actions.displayWarning(err.message))
@ -1643,6 +1665,27 @@ function addTokens (tokens) {
}
}
function removeSuggestedTokens () {
return (dispatch) => {
dispatch(actions.showLoadingIndication())
return new Promise((resolve, reject) => {
background.removeSuggestedTokens((err, suggestedTokens) => {
dispatch(actions.hideLoadingIndication())
if (err) {
dispatch(actions.displayWarning(err.message))
}
dispatch(actions.clearPendingTokens())
if (global.METAMASK_UI_TYPE === ENVIRONMENT_TYPE_NOTIFICATION) {
return global.platform.closeCurrentWindow()
}
resolve(suggestedTokens)
})
})
.then(() => updateMetamaskStateFromBackground())
.then(suggestedTokens => dispatch(actions.updateMetamaskState({...suggestedTokens})))
}
}
function updateTokens (newTokens) {
return {
type: actions.UPDATE_TOKENS,
@ -1650,6 +1693,12 @@ function updateTokens (newTokens) {
}
}
function clearPendingTokens () {
return {
type: actions.CLEAR_PENDING_TOKENS,
}
}
function goBackToInitView () {
return {
type: actions.BACK_TO_INIT_MENU,
@ -1725,6 +1774,29 @@ function retryTransaction (txId) {
}
}
function createCancelTransaction (txId, customGasPrice) {
log.debug('background.cancelTransaction')
let newTxId
return dispatch => {
return new Promise((resolve, reject) => {
background.createCancelTransaction(txId, customGasPrice, (err, newState) => {
if (err) {
dispatch(actions.displayWarning(err.message))
reject(err)
}
const { selectedAddressTxList } = newState
const { id } = selectedAddressTxList[selectedAddressTxList.length - 1]
newTxId = id
resolve(newState)
})
})
.then(newState => dispatch(actions.updateMetamaskState(newState)))
.then(() => newTxId)
}
}
//
// config
//
@ -1812,9 +1884,13 @@ function hideModal (payload) {
}
}
function showSidebar () {
function showSidebar ({ transitionName, type }) {
return {
type: actions.SIDEBAR_OPEN,
value: {
transitionName,
type,
},
}
}
@ -2310,9 +2386,3 @@ function setPendingTokens (pendingTokens) {
payload: tokens,
}
}
function clearPendingTokens () {
return {
type: actions.CLEAR_PENDING_TOKENS,
}
}

@ -15,22 +15,22 @@ const SendTransactionScreen = require('./components/send/send.container')
const ConfirmTransaction = require('./components/pages/confirm-transaction')
// slideout menu
const WalletView = require('./components/wallet-view')
const Sidebar = require('./components/sidebars').default
// other views
const Home = require('./components/pages/home')
import Home from './components/pages/home'
import Settings from './components/pages/settings'
const Authenticated = require('./components/pages/authenticated')
const Initialized = require('./components/pages/initialized')
const Settings = require('./components/pages/settings')
const RestoreVaultPage = require('./components/pages/keychains/restore-vault').default
const RevealSeedConfirmation = require('./components/pages/keychains/reveal-seed')
const AddTokenPage = require('./components/pages/add-token')
const ConfirmAddTokenPage = require('./components/pages/confirm-add-token')
const ConfirmAddSuggestedTokenPage = require('./components/pages/confirm-add-suggested-token')
const CreateAccountPage = require('./components/pages/create-account')
const NoticeScreen = require('./components/pages/notice')
const Loading = require('./components/loading-screen')
const ReactCSSTransitionGroup = require('react-addons-css-transition-group')
const NetworkDropdown = require('./components/dropdowns/network-dropdown')
const AccountMenu = require('./components/account-menu')
@ -51,6 +51,7 @@ const {
RESTORE_VAULT_ROUTE,
ADD_TOKEN_ROUTE,
CONFIRM_ADD_TOKEN_ROUTE,
CONFIRM_ADD_SUGGESTED_TOKEN_ROUTE,
NEW_ACCOUNT_ROUTE,
SEND_ROUTE,
CONFIRM_TRANSACTION_ROUTE,
@ -85,6 +86,7 @@ class App extends Component {
h(Authenticated, { path: SEND_ROUTE, exact, component: SendTransactionScreen }),
h(Authenticated, { path: ADD_TOKEN_ROUTE, exact, component: AddTokenPage }),
h(Authenticated, { path: CONFIRM_ADD_TOKEN_ROUTE, exact, component: ConfirmAddTokenPage }),
h(Authenticated, { path: CONFIRM_ADD_SUGGESTED_TOKEN_ROUTE, exact, component: ConfirmAddSuggestedTokenPage }),
h(Authenticated, { path: NEW_ACCOUNT_ROUTE, component: CreateAccountPage }),
h(Authenticated, { path: DEFAULT_ROUTE, exact, component: Home }),
])
@ -102,6 +104,7 @@ class App extends Component {
frequentRpcList,
currentView,
setMouseUserState,
sidebar,
} = this.props
const isLoadingNetwork = network === 'loading' && currentView.name !== 'config'
const loadMessage = loadingMessage || isLoadingNetwork ?
@ -134,7 +137,12 @@ class App extends Component {
h(AppHeader),
// sidebar
this.renderSidebar(),
h(Sidebar, {
sidebarOpen: sidebar.isOpen,
hideSidebar: this.props.hideSidebar,
transitionName: sidebar.transitionName,
type: sidebar.type,
}),
// network dropdown
h(NetworkDropdown, {
@ -144,61 +152,18 @@ class App extends Component {
h(AccountMenu),
(isLoading || isLoadingNetwork) && h(Loading, {
loadingMessage: loadMessage,
}),
h('div.main-container-wrapper', [
(isLoading || isLoadingNetwork) && h(Loading, {
loadingMessage: loadMessage,
}),
// content
this.renderRoutes(),
// content
this.renderRoutes(),
]),
])
)
}
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,
])
}
toggleMetamaskActive () {
if (!this.props.isUnlocked) {
// currently inactive: redirect to password box
@ -225,7 +190,7 @@ class App extends Component {
} else if (providerName === 'ropsten') {
name = this.context.t('connectingToRopsten')
} else if (providerName === 'kovan') {
name = this.context.t('connectingToRopsten')
name = this.context.t('connectingToKovan')
} else if (providerName === 'rinkeby') {
name = this.context.t('connectingToRinkeby')
} else {
@ -267,7 +232,7 @@ App.propTypes = {
provider: PropTypes.object,
frequentRpcList: PropTypes.array,
currentView: PropTypes.object,
sidebarOpen: PropTypes.bool,
sidebar: PropTypes.object,
alertOpen: PropTypes.bool,
hideSidebar: PropTypes.func,
isMascara: PropTypes.bool,
@ -303,7 +268,7 @@ function mapStateToProps (state) {
const { appState, metamask } = state
const {
networkDropdownOpen,
sidebarOpen,
sidebar,
alertOpen,
alertMessage,
isLoading,
@ -330,7 +295,7 @@ function mapStateToProps (state) {
return {
// state from plugin
networkDropdownOpen,
sidebarOpen,
sidebar,
alertOpen,
alertMessage,
isLoading,

@ -4,8 +4,8 @@ const h = require('react-hyperscript')
const inherits = require('util').inherits
const TokenBalance = require('./token-balance')
const Identicon = require('./identicon')
const currencyFormatter = require('currency-formatter')
const currencies = require('currency-formatter/currencies')
import CurrencyDisplay from './currency-display'
const { getAssetImages, conversionRateSelector, getCurrentCurrency} = require('../selectors')
const { formatBalance, generateBalanceObject } = require('../util')
@ -20,8 +20,9 @@ function mapStateToProps (state) {
return {
account,
network,
conversionRate: state.metamask.conversionRate,
currentCurrency: state.metamask.currentCurrency,
conversionRate: conversionRateSelector(state),
currentCurrency: getCurrentCurrency(state),
assetImages: getAssetImages(state),
}
}
@ -32,7 +33,9 @@ function BalanceComponent () {
BalanceComponent.prototype.render = function () {
const props = this.props
const { token, network } = props
const { token, network, assetImages } = props
const address = token && token.address
const image = assetImages && address ? assetImages[token.address] : undefined
return h('div.balance-container', {}, [
@ -43,8 +46,9 @@ BalanceComponent.prototype.render = function () {
// }),
h(Identicon, {
diameter: 50,
address: token && token.address,
address,
network,
image,
}),
token ? this.renderTokenBalance() : this.renderBalance(),
@ -80,38 +84,12 @@ BalanceComponent.prototype.renderBalance = function () {
style: {},
}, this.getTokenBalance(formattedBalance, shorten)),
showFiat ? this.renderFiatValue(formattedBalance) : null,
showFiat && h(CurrencyDisplay, {
value: balanceValue,
}),
])
}
BalanceComponent.prototype.renderFiatValue = function (formattedBalance) {
const { conversionRate, currentCurrency } = this.props
const fiatDisplayNumber = this.getFiatDisplayNumber(formattedBalance, conversionRate)
const fiatPrefix = currentCurrency === 'USD' ? '$' : ''
return this.renderFiatAmount(fiatDisplayNumber, currentCurrency, fiatPrefix)
}
BalanceComponent.prototype.renderFiatAmount = function (fiatDisplayNumber, fiatSuffix, fiatPrefix) {
const shouldNotRenderFiat = fiatDisplayNumber === 'N/A' || Number(fiatDisplayNumber) === 0
if (shouldNotRenderFiat) return null
const upperCaseFiatSuffix = fiatSuffix.toUpperCase()
const display = currencies.find(currency => currency.code === upperCaseFiatSuffix)
? currencyFormatter.format(Number(fiatDisplayNumber), {
code: upperCaseFiatSuffix,
})
: `${fiatPrefix}${fiatDisplayNumber} ${upperCaseFiatSuffix}`
return h('div.fiat-amount', {
style: {},
}, display)
}
BalanceComponent.prototype.getTokenBalance = function (formattedBalance, shorten) {
const balanceObj = generateBalanceObject(formattedBalance, shorten ? 1 : 3)

@ -6,6 +6,7 @@ const CLASSNAME_DEFAULT = 'btn-default'
const CLASSNAME_PRIMARY = 'btn-primary'
const CLASSNAME_SECONDARY = 'btn-secondary'
const CLASSNAME_CONFIRM = 'btn-confirm'
const CLASSNAME_RAISED = 'btn-raised'
const CLASSNAME_LARGE = 'btn--large'
const typeHash = {
@ -13,6 +14,7 @@ const typeHash = {
primary: CLASSNAME_PRIMARY,
secondary: CLASSNAME_SECONDARY,
confirm: CLASSNAME_CONFIRM,
raised: CLASSNAME_RAISED,
}
export default class Button extends Component {
@ -20,7 +22,7 @@ export default class Button extends Component {
type: PropTypes.string,
large: PropTypes.bool,
className: PropTypes.string,
children: PropTypes.string,
children: PropTypes.oneOfType([PropTypes.string, PropTypes.node]),
}
render () {
@ -29,6 +31,7 @@ export default class Button extends Component {
return (
<button
className={classnames(
'button',
typeHash[type],
large && CLASSNAME_LARGE,
className

@ -1,267 +0,0 @@
const Component = require('react').Component
const PropTypes = require('prop-types')
const h = require('react-hyperscript')
const inherits = require('util').inherits
const connect = require('react-redux').connect
const actions = require('../actions')
const CoinbaseForm = require('./coinbase-form')
const ShapeshiftForm = require('./shapeshift-form')
const Loading = require('./loading-screen')
const AccountPanel = require('./account-panel')
const RadioList = require('./custom-radio-list')
const { getNetworkDisplayName } = require('../../../app/scripts/controllers/network/util')
BuyButtonSubview.contextTypes = {
t: PropTypes.func,
}
module.exports = connect(mapStateToProps)(BuyButtonSubview)
function mapStateToProps (state) {
return {
identity: state.appState.identity,
account: state.metamask.accounts[state.appState.buyView.buyAddress],
warning: state.appState.warning,
buyView: state.appState.buyView,
network: state.metamask.network,
provider: state.metamask.provider,
context: state.appState.currentView.context,
isSubLoading: state.appState.isSubLoading,
}
}
inherits(BuyButtonSubview, Component)
function BuyButtonSubview () {
Component.call(this)
}
BuyButtonSubview.prototype.render = function () {
return (
h('div', {
style: {
width: '100%',
},
}, [
this.headerSubview(),
this.primarySubview(),
])
)
}
BuyButtonSubview.prototype.headerSubview = function () {
const props = this.props
const isLoading = props.isSubLoading
return (
h('.flex-column', {
style: {
alignItems: 'center',
},
}, [
// header bar (back button, label)
h('.flex-row', {
style: {
alignItems: 'center',
justifyContent: 'center',
},
}, [
h('i.fa.fa-arrow-left.fa-lg.cursor-pointer.color-orange', {
onClick: this.backButtonContext.bind(this),
style: {
position: 'absolute',
left: '10px',
},
}),
h('h2.text-transform-uppercase.flex-center', {
style: {
width: '100vw',
background: 'rgb(235, 235, 235)',
color: 'rgb(174, 174, 174)',
paddingTop: '4px',
paddingBottom: '4px',
},
}, this.context.t('depositEth')),
]),
// loading indication
h('div', {
style: {
position: 'absolute',
top: '57vh',
left: '49vw',
},
}, [
isLoading && h(Loading),
]),
// account panel
h('div', {
style: {
width: '80%',
},
}, [
h(AccountPanel, {
showFullAddress: true,
identity: props.identity,
account: props.account,
}),
]),
h('.flex-row', {
style: {
alignItems: 'center',
justifyContent: 'center',
},
}, [
h('h3.text-transform-uppercase.flex-center', {
style: {
paddingLeft: '15px',
width: '100vw',
background: 'rgb(235, 235, 235)',
color: 'rgb(174, 174, 174)',
paddingTop: '4px',
paddingBottom: '4px',
},
}, this.context.t('selectService')),
]),
])
)
}
BuyButtonSubview.prototype.primarySubview = function () {
const props = this.props
const network = props.network
switch (network) {
case 'loading':
return
case '1':
return this.mainnetSubview()
// Ropsten, Rinkeby, Kovan
case '3':
case '4':
case '42':
const networkName = getNetworkDisplayName(network)
const label = `${networkName} ${this.context.t('testFaucet')}`
return (
h('div.flex-column', {
style: {
alignItems: 'center',
margin: '20px 50px',
},
}, [
h('button.text-transform-uppercase', {
onClick: () => this.props.dispatch(actions.buyEth({ network })),
style: {
marginTop: '15px',
},
}, label),
// Kovan only: Dharma loans beta
network === '42' ? (
h('button.text-transform-uppercase', {
onClick: () => this.navigateTo('https://borrow.dharma.io/'),
style: {
marginTop: '15px',
},
}, this.context.t('borrowDharma'))
) : null,
])
)
default:
return (
h('h2.error', this.context.t('unknownNetworkId'))
)
}
}
BuyButtonSubview.prototype.mainnetSubview = function () {
const props = this.props
return (
h('.flex-column', {
style: {
alignItems: 'center',
},
}, [
h('.flex-row.selected-exchange', {
style: {
position: 'relative',
right: '35px',
marginTop: '20px',
marginBottom: '20px',
},
}, [
h(RadioList, {
defaultFocus: props.buyView.subview,
labels: [
'Coinbase',
'ShapeShift',
],
subtext: {
'Coinbase': `${this.context.t('crypto')}/${this.context.t('fiat')} (${this.context.t('usaOnly')})`,
'ShapeShift': this.context.t('crypto'),
},
onClick: this.radioHandler.bind(this),
}),
]),
h('h3.text-transform-uppercase', {
style: {
paddingLeft: '15px',
fontFamily: 'Montserrat Light',
width: '100vw',
background: 'rgb(235, 235, 235)',
color: 'rgb(174, 174, 174)',
paddingTop: '4px',
paddingBottom: '4px',
},
}, props.buyView.subview),
this.formVersionSubview(),
])
)
}
BuyButtonSubview.prototype.formVersionSubview = function () {
const network = this.props.network
if (network === '1') {
if (this.props.buyView.formView.coinbase) {
return h(CoinbaseForm, this.props)
} else if (this.props.buyView.formView.shapeshift) {
return h(ShapeshiftForm, this.props)
}
}
}
BuyButtonSubview.prototype.navigateTo = function (url) {
global.platform.openWindow({ url })
}
BuyButtonSubview.prototype.backButtonContext = function () {
if (this.props.context === 'confTx') {
this.props.dispatch(actions.showConfTxPage({transForward: false}))
} else {
this.props.dispatch(actions.goHome())
}
}
BuyButtonSubview.prototype.radioHandler = function (event) {
switch (event.target.title) {
case 'Coinbase':
return this.props.dispatch(actions.coinBaseSubview())
case 'ShapeShift':
return this.props.dispatch(actions.shapeShiftSubview(this.props.provider.type))
}
}

@ -0,0 +1,25 @@
import React, { PureComponent } from 'react'
import PropTypes from 'prop-types'
import classnames from 'classnames'
export default class Card extends PureComponent {
static propTypes = {
className: PropTypes.string,
overrideClassName: PropTypes.bool,
title: PropTypes.string,
children: PropTypes.node,
}
render () {
const { className, overrideClassName, title } = this.props
return (
<div className={classnames({ 'card': !overrideClassName }, className)}>
<div className="card__title">
{ title }
</div>
{ this.props.children }
</div>
)
}
}

@ -0,0 +1 @@
export { default } from './card.component'

@ -0,0 +1,11 @@
.card {
border-radius: 4px;
box-shadow: 0px 2px 4px rgba(0, 0, 0, 0.08);
padding: 8px;
&__title {
border-bottom: 1px solid #d8d8d8;
padding-bottom: 4px;
text-transform: capitalize;
}
}

@ -0,0 +1,25 @@
import React from 'react'
import assert from 'assert'
import { shallow } from 'enzyme'
import Card from '../card.component'
describe('Card Component', () => {
it('should render a card with a title and child element', () => {
const wrapper = shallow(
<Card
title="Test"
className="card-test-class"
>
<div className="child-test-class">Child</div>
</Card>
)
assert.ok(wrapper.hasClass('card-test-class'))
const title = wrapper.find('.card__title')
assert.ok(title)
assert.equal(title.text(), 'Test')
const child = wrapper.find('.child-test-class')
assert.ok(child)
assert.equal(child.text(), 'Child')
})
})

@ -2,11 +2,8 @@ import React, { Component } from 'react'
import PropTypes from 'prop-types'
import classnames from 'classnames'
import { Tabs, Tab } from '../../tabs'
import {
ConfirmPageContainerSummary,
ConfirmPageContainerError,
ConfirmPageContainerWarning,
} from './'
import { ConfirmPageContainerSummary, ConfirmPageContainerWarning } from './'
import ErrorMessage from '../../error-message'
export default class ConfirmPageContainerContent extends Component {
static propTypes = {
@ -18,6 +15,7 @@ export default class ConfirmPageContainerContent extends Component {
hideSubtitle: PropTypes.bool,
identiconAddress: PropTypes.string,
nonce: PropTypes.string,
assetImage: PropTypes.string,
subtitle: PropTypes.oneOfType([PropTypes.string, PropTypes.number]),
summaryComponent: PropTypes.node,
title: PropTypes.oneOfType([PropTypes.string, PropTypes.number]),
@ -60,6 +58,7 @@ export default class ConfirmPageContainerContent extends Component {
hideSubtitle,
identiconAddress,
nonce,
assetImage,
summaryComponent,
detailsComponent,
dataComponent,
@ -85,6 +84,7 @@ export default class ConfirmPageContainerContent extends Component {
hideSubtitle={hideSubtitle}
identiconAddress={identiconAddress}
nonce={nonce}
assetImage={assetImage}
/>
)
}
@ -92,7 +92,7 @@ export default class ConfirmPageContainerContent extends Component {
{
(errorKey || errorMessage) && (
<div className="confirm-page-container-content__error-container">
<ConfirmPageContainerError
<ErrorMessage
errorMessage={errorMessage}
errorKey={errorKey}
/>

@ -4,7 +4,7 @@ import classnames from 'classnames'
import Identicon from '../../../identicon'
const ConfirmPageContainerSummary = props => {
const { action, title, subtitle, hideSubtitle, className, identiconAddress, nonce } = props
const { action, title, subtitle, hideSubtitle, className, identiconAddress, nonce, assetImage } = props
return (
<div className={classnames('confirm-page-container-summary', className)}>
@ -27,6 +27,7 @@ const ConfirmPageContainerSummary = props => {
className="confirm-page-container-summary__identicon"
diameter={36}
address={identiconAddress}
image={assetImage}
/>
)
}
@ -51,6 +52,7 @@ ConfirmPageContainerSummary.propTypes = {
className: PropTypes.string,
identiconAddress: PropTypes.string,
nonce: PropTypes.string,
assetImage: PropTypes.string,
}
export default ConfirmPageContainerSummary

@ -1,4 +1,3 @@
export { default } from './confirm-page-container-content.component'
export { default as ConfirmPageContainerSummary } from './confirm-page-container-summary'
export { default as ConfirmPageContainerError } from './confirm-page-container-error'
export { default as ConfirmPageContainerWarning } from './confirm-page-container-warning'

@ -1,5 +1,3 @@
@import './confirm-page-container-error/index';
@import './confirm-page-container-warning/index';
@import './confirm-page-container-summary/index';

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

Loading…
Cancel
Save