mirror of
https://github.com/kremalicious/metamask-extension.git
synced 2024-12-23 09:52:26 +01:00
Merge branch 'e2e-tests' of https://github.com/tmashuang/metamask-extension into e2e-tests
This commit is contained in:
commit
025d8e7983
@ -15,6 +15,9 @@ workflows:
|
|||||||
- test-lint:
|
- test-lint:
|
||||||
requires:
|
requires:
|
||||||
- prep-deps-npm
|
- prep-deps-npm
|
||||||
|
- test-deps:
|
||||||
|
requires:
|
||||||
|
- prep-deps-npm
|
||||||
- test-e2e-chrome:
|
- test-e2e-chrome:
|
||||||
requires:
|
requires:
|
||||||
- prep-deps-npm
|
- prep-deps-npm
|
||||||
@ -48,6 +51,7 @@ workflows:
|
|||||||
- all-tests-pass:
|
- all-tests-pass:
|
||||||
requires:
|
requires:
|
||||||
- test-lint
|
- test-lint
|
||||||
|
- test-deps
|
||||||
- test-unit
|
- test-unit
|
||||||
- test-e2e-chrome
|
- test-e2e-chrome
|
||||||
- test-e2e-firefox
|
- test-e2e-firefox
|
||||||
@ -151,6 +155,17 @@ jobs:
|
|||||||
name: Test
|
name: Test
|
||||||
command: npm run lint
|
command: npm run lint
|
||||||
|
|
||||||
|
test-deps:
|
||||||
|
docker:
|
||||||
|
- image: circleci/node:8-browsers
|
||||||
|
steps:
|
||||||
|
- checkout
|
||||||
|
- restore_cache:
|
||||||
|
key: dependency-cache-{{ .Revision }}
|
||||||
|
- run:
|
||||||
|
name: Test
|
||||||
|
command: npx nsp check
|
||||||
|
|
||||||
test-e2e-chrome:
|
test-e2e-chrome:
|
||||||
docker:
|
docker:
|
||||||
- image: circleci/node:8-browsers
|
- image: circleci/node:8-browsers
|
||||||
|
@ -2,11 +2,14 @@
|
|||||||
|
|
||||||
## Current Master
|
## Current Master
|
||||||
|
|
||||||
|
## 4.6.0 Thu Apr 26 2018
|
||||||
|
|
||||||
- Correctly format currency conversion for locally selected preferred currency.
|
- Correctly format currency conversion for locally selected preferred currency.
|
||||||
- Improved performance of 3D fox logo.
|
- Improved performance of 3D fox logo.
|
||||||
- Fetch token prices based on contract address, not symbol
|
- Fetch token prices based on contract address, not symbol
|
||||||
- Fix bug that prevents setting language locale in settings.
|
- Fix bug that prevents setting language locale in settings.
|
||||||
- Show checksum addresses throughout the UI
|
- Show checksum addresses throughout the UI
|
||||||
|
- Allow transactions with a 0 gwei gas price
|
||||||
|
|
||||||
## 4.5.5 Fri Apr 06 2018
|
## 4.5.5 Fri Apr 06 2018
|
||||||
|
|
||||||
|
@ -12,12 +12,14 @@ For any new programmatic functionality, we like unit tests when possible, so if
|
|||||||
|
|
||||||
### PR Format
|
### PR Format
|
||||||
|
|
||||||
We use [waffle](https://waffle.io/) for project management, and it will automatically keep us organized if you do one simple thing:
|
|
||||||
|
|
||||||
If this PR closes the issue, add the line `Fixes #$ISSUE_NUMBER`. Ex. For closing issue 418, include the line `Fixes #418`.
|
If this PR closes the issue, add the line `Fixes #$ISSUE_NUMBER`. Ex. For closing issue 418, include the line `Fixes #418`.
|
||||||
|
|
||||||
If it doesn't close the issue but addresses it partially, just include a reference to the issue number, like `#418`.
|
If it doesn't close the issue but addresses it partially, just include a reference to the issue number, like `#418`.
|
||||||
|
|
||||||
|
Submit your PR against the `develop` branch. This is where we merge new features so they get some time to receive extra testing before being pushed to `master` for production.
|
||||||
|
|
||||||
|
If your PR is a hot-fix that needs to be published urgently, you may submit a PR against the `master` branch, but this PR will receive tighter scrutiny before merging.
|
||||||
|
|
||||||
## Before Merging
|
## Before Merging
|
||||||
|
|
||||||
Make sure you get a `:thumbsup`, `:+1`, or `LGTM` from another collaborator before merging.
|
Make sure you get a `:thumbsup`, `:+1`, or `LGTM` from another collaborator before merging.
|
||||||
|
@ -98,6 +98,9 @@
|
|||||||
"clickCopy": {
|
"clickCopy": {
|
||||||
"message": "Click to Copy"
|
"message": "Click to Copy"
|
||||||
},
|
},
|
||||||
|
"close": {
|
||||||
|
"message": "Close"
|
||||||
|
},
|
||||||
"confirm": {
|
"confirm": {
|
||||||
"message": "Confirm"
|
"message": "Confirm"
|
||||||
},
|
},
|
||||||
@ -259,6 +262,9 @@
|
|||||||
"enterPasswordConfirm": {
|
"enterPasswordConfirm": {
|
||||||
"message": "Enter your password to confirm"
|
"message": "Enter your password to confirm"
|
||||||
},
|
},
|
||||||
|
"enterPasswordContinue": {
|
||||||
|
"message": "Enter password to continue"
|
||||||
|
},
|
||||||
"passwordNotLongEnough": {
|
"passwordNotLongEnough": {
|
||||||
"message": "Password not long enough"
|
"message": "Password not long enough"
|
||||||
},
|
},
|
||||||
@ -331,6 +337,9 @@
|
|||||||
"gasPriceRequired": {
|
"gasPriceRequired": {
|
||||||
"message": "Gas Price Required"
|
"message": "Gas Price Required"
|
||||||
},
|
},
|
||||||
|
"generatingTransaction": {
|
||||||
|
"message": "Generating transaction"
|
||||||
|
},
|
||||||
"getEther": {
|
"getEther": {
|
||||||
"message": "Get Ether"
|
"message": "Get Ether"
|
||||||
},
|
},
|
||||||
@ -476,6 +485,9 @@
|
|||||||
"metamaskDescription": {
|
"metamaskDescription": {
|
||||||
"message": "MetaMask is a secure identity vault for Ethereum."
|
"message": "MetaMask is a secure identity vault for Ethereum."
|
||||||
},
|
},
|
||||||
|
"metamaskSeedWords": {
|
||||||
|
"message": "MetaMask Seed Words"
|
||||||
|
},
|
||||||
"min": {
|
"min": {
|
||||||
"message": "Minimum"
|
"message": "Minimum"
|
||||||
},
|
},
|
||||||
@ -549,6 +561,9 @@
|
|||||||
"message": "or",
|
"message": "or",
|
||||||
"description": "choice between creating or importing a new account"
|
"description": "choice between creating or importing a new account"
|
||||||
},
|
},
|
||||||
|
"password": {
|
||||||
|
"message": "Password"
|
||||||
|
},
|
||||||
"passwordCorrect": {
|
"passwordCorrect": {
|
||||||
"message": "Please make sure your password is correct."
|
"message": "Please make sure your password is correct."
|
||||||
},
|
},
|
||||||
@ -634,8 +649,17 @@
|
|||||||
"revealSeedWords": {
|
"revealSeedWords": {
|
||||||
"message": "Reveal Seed Words"
|
"message": "Reveal Seed Words"
|
||||||
},
|
},
|
||||||
|
"revealSeedWordsTitle": {
|
||||||
|
"message": "Seed Phrase"
|
||||||
|
},
|
||||||
|
"revealSeedWordsDescription": {
|
||||||
|
"message": "If you ever change browsers or move computers, you will need this seed phrase to access your accounts. Save them somewhere safe and secret."
|
||||||
|
},
|
||||||
|
"revealSeedWordsWarningTitle": {
|
||||||
|
"message": "DO NOT share this phrase with anyone!"
|
||||||
|
},
|
||||||
"revealSeedWordsWarning": {
|
"revealSeedWordsWarning": {
|
||||||
"message": "Do not recover your seed words in a public place! These words can be used to steal all your accounts."
|
"message": "These words can be used to steal all your accounts."
|
||||||
},
|
},
|
||||||
"revert": {
|
"revert": {
|
||||||
"message": "Revert"
|
"message": "Revert"
|
||||||
@ -677,6 +701,9 @@
|
|||||||
"reprice_subtitle": {
|
"reprice_subtitle": {
|
||||||
"message": "Increase your gas price to attempt to overwrite and speed up your transaction"
|
"message": "Increase your gas price to attempt to overwrite and speed up your transaction"
|
||||||
},
|
},
|
||||||
|
"saveAsCsvFile": {
|
||||||
|
"message": "Save as CSV File"
|
||||||
|
},
|
||||||
"saveAsFile": {
|
"saveAsFile": {
|
||||||
"message": "Save as File",
|
"message": "Save as File",
|
||||||
"description": "Account export process"
|
"description": "Account export process"
|
||||||
@ -909,7 +936,7 @@
|
|||||||
"youSign": {
|
"youSign": {
|
||||||
"message": "You are signing"
|
"message": "You are signing"
|
||||||
},
|
},
|
||||||
"generatingTransaction": {
|
"yourPrivateSeedPhrase": {
|
||||||
"message": "Generating transaction"
|
"message": "Your private seed phrase"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
24
app/images/copy-to-clipboard.svg
Normal file
24
app/images/copy-to-clipboard.svg
Normal file
@ -0,0 +1,24 @@
|
|||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<svg width="18px" height="17px" viewBox="0 0 18 17" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink">
|
||||||
|
<!-- Generator: sketchtool 49.3 (51167) - http://www.bohemiancoding.com/sketch -->
|
||||||
|
<title>374E58A5-C29E-4921-83E7-889FA06D6408</title>
|
||||||
|
<desc>Created with sketchtool.</desc>
|
||||||
|
<defs></defs>
|
||||||
|
<g id="Reveal-Seedphrase" stroke="none" stroke-width="1" fill="none" fill-rule="evenodd">
|
||||||
|
<g id="Seed-phrase-2" transform="translate(-39.000000, -379.000000)">
|
||||||
|
<g id="Group-2">
|
||||||
|
<g id="Group-8" transform="translate(16.000000, 248.000000)">
|
||||||
|
<g id="Group-6" transform="translate(23.336478, 120.000000)">
|
||||||
|
<g id="Group-5" transform="translate(0.408805, 11.000000)">
|
||||||
|
<g id="copy-to-clipboard">
|
||||||
|
<rect id="Rectangle-18" stroke="#3098DC" stroke-width="2" x="1" y="1" width="12.0220126" height="12"></rect>
|
||||||
|
<rect id="Rectangle-18-Copy-2" fill="#FFFFFF" x="2.1572327" y="2" width="14.0220126" height="14"></rect>
|
||||||
|
<rect id="Rectangle-18-Copy" stroke="#3098DC" stroke-width="2" x="4.23584906" y="4" width="12.0220126" height="12"></rect>
|
||||||
|
</g>
|
||||||
|
</g>
|
||||||
|
</g>
|
||||||
|
</g>
|
||||||
|
</g>
|
||||||
|
</g>
|
||||||
|
</g>
|
||||||
|
</svg>
|
After Width: | Height: | Size: 1.4 KiB |
@ -1,15 +1,26 @@
|
|||||||
<?xml version="1.0" encoding="utf-8"?>
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
<!-- Generator: Adobe Illustrator 16.0.0, SVG Export Plug-In . SVG Version: 6.00 Build 0) -->
|
<svg width="20px" height="18px" viewBox="0 0 20 18" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink">
|
||||||
<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">
|
<!-- Generator: sketchtool 49.3 (51167) - http://www.bohemiancoding.com/sketch -->
|
||||||
<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"
|
<title>50559280-0739-419A-8E87-3CDD16A6996A</title>
|
||||||
width="24.088px" height="24px" viewBox="138.01 0 24.088 24" enable-background="new 138.01 0 24.088 24" xml:space="preserve" fill="#F7861C">
|
<desc>Created with sketchtool.</desc>
|
||||||
<g>
|
<defs></defs>
|
||||||
<polygon fill="#F7861C" points="157.551,17.075 156.55,17.075 156.55,19.149 142.569,19.149 142.569,17.075 141.568,17.075
|
<g id="Reveal-Seedphrase" stroke="none" stroke-width="1" fill="none" fill-rule="evenodd">
|
||||||
141.568,20.145 141.955,20.145 141.955,20.15 157.006,20.15 157.006,20.145 157.551,20.145 "/>
|
<g id="Seed-phrase-2" transform="translate(-212.000000, -379.000000)" stroke="#259DE5" stroke-width="2">
|
||||||
<polygon fill="#F7861C" points="152.555,10.275 152.555,11.26 152.555,11.268 151.562,11.268 151.562,12.252 150.565,12.252
|
<g id="Group-2">
|
||||||
150.565,4.171 149.564,4.171 149.564,12.236 148.564,12.236 148.564,11.252 147.564,11.252 147.564,11.236 147.564,11.221
|
<g id="Group-8" transform="translate(16.000000, 248.000000)">
|
||||||
147.564,10.236 146.563,10.236 146.563,11.221 146.563,11.236 146.563,12.221 147.563,12.221 147.563,12.236 147.563,12.252
|
<g id="Group-6" transform="translate(23.336478, 120.000000)">
|
||||||
147.563,13.236 148.563,13.236 148.563,14.221 149.564,14.221 149.564,15.725 150.565,15.725 150.565,14.236 151.563,14.236
|
<g id="Group-3" transform="translate(174.000000, 11.000000)">
|
||||||
151.563,13.252 152.563,13.252 152.563,12.268 152.563,12.26 153.556,12.26 153.556,11.275 153.556,11.26 153.556,10.275 "/>
|
<g id="Group-4">
|
||||||
</g>
|
<g id="download">
|
||||||
</svg>
|
<polyline id="Path-5" points="0 11 0 17 17 17 17 11"></polyline>
|
||||||
|
<path d="M8.5,0 L8.5,11" id="Path-6"></path>
|
||||||
|
<polyline id="Path-7" points="3.1875 7 8.5 11 13.8125 7"></polyline>
|
||||||
|
</g>
|
||||||
|
</g>
|
||||||
|
</g>
|
||||||
|
</g>
|
||||||
|
</g>
|
||||||
|
</g>
|
||||||
|
</g>
|
||||||
|
</g>
|
||||||
|
</svg>
|
Before Width: | Height: | Size: 1.3 KiB After Width: | Height: | Size: 1.4 KiB |
22
app/images/warning.svg
Normal file
22
app/images/warning.svg
Normal file
@ -0,0 +1,22 @@
|
|||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<svg width="33px" height="32px" viewBox="0 0 33 32" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink">
|
||||||
|
<!-- Generator: Sketch 49.3 (51167) - http://www.bohemiancoding.com/sketch -->
|
||||||
|
<title>Group 7</title>
|
||||||
|
<desc>Created with Sketch.</desc>
|
||||||
|
<defs></defs>
|
||||||
|
<g id="Reveal-Seedphrase" stroke="none" stroke-width="1" fill="none" fill-rule="evenodd">
|
||||||
|
<g id="Seed-phrase-2" transform="translate(-29.000000, -155.000000)">
|
||||||
|
<g id="Group-2" transform="translate(0.000000, 132.000000)">
|
||||||
|
<g id="Group" transform="translate(28.000000, 19.000000)">
|
||||||
|
<g id="Group-19-Copy-2" transform="translate(0.000000, 3.000000)">
|
||||||
|
<g id="Group-7">
|
||||||
|
<path d="M20.1321134,3.85444772 L32.5721829,26.6020033 C33.367162,28.0556794 32.8331826,29.8785746 31.3795065,30.6735537 C30.9381289,30.9149321 30.4431378,31.0414403 29.9400695,31.0414403 L5.05993054,31.0414403 C3.40307629,31.0414403 2.05993054,29.6982946 2.05993054,28.0414403 C2.05993054,27.538372 2.18643873,27.0433809 2.42781712,26.6020033 L14.8678866,3.85444772 C15.6628657,2.40077162 17.4857609,1.86679221 18.939437,2.66177133 C19.442875,2.93708896 19.8567958,3.35100977 20.1321134,3.85444772 Z" id="Triangle-2-Copy" stroke="#FF001F" stroke-width="2"></path>
|
||||||
|
<rect id="Rectangle-5" fill="#FF001F" x="16" y="9" width="3" height="13"></rect>
|
||||||
|
<rect id="Rectangle-5-Copy" fill="#FF001F" x="16" y="24" width="3" height="3"></rect>
|
||||||
|
</g>
|
||||||
|
</g>
|
||||||
|
</g>
|
||||||
|
</g>
|
||||||
|
</g>
|
||||||
|
</g>
|
||||||
|
</svg>
|
After Width: | Height: | Size: 1.7 KiB |
@ -1,7 +1,7 @@
|
|||||||
{
|
{
|
||||||
"name": "__MSG_appName__",
|
"name": "__MSG_appName__",
|
||||||
"short_name": "__MSG_appName__",
|
"short_name": "__MSG_appName__",
|
||||||
"version": "4.5.5",
|
"version": "4.6.0",
|
||||||
"manifest_version": 2,
|
"manifest_version": 2,
|
||||||
"author": "https://metamask.io",
|
"author": "https://metamask.io",
|
||||||
"description": "__MSG_appDescription__",
|
"description": "__MSG_appDescription__",
|
||||||
|
@ -261,7 +261,11 @@ function setupController (initState, initLangCode) {
|
|||||||
controller.txController.on(`tx:status-update`, (txId, status) => {
|
controller.txController.on(`tx:status-update`, (txId, status) => {
|
||||||
if (status !== 'failed') return
|
if (status !== 'failed') return
|
||||||
const txMeta = controller.txController.txStateManager.getTx(txId)
|
const txMeta = controller.txController.txStateManager.getTx(txId)
|
||||||
reportFailedTxToSentry({ raven, txMeta })
|
try {
|
||||||
|
reportFailedTxToSentry({ raven, txMeta })
|
||||||
|
} catch (e) {
|
||||||
|
console.error(e)
|
||||||
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
// setup state persistence
|
// setup state persistence
|
||||||
|
@ -174,6 +174,7 @@ function blacklistedDomainCheck () {
|
|||||||
'uscourts.gov',
|
'uscourts.gov',
|
||||||
'dropbox.com',
|
'dropbox.com',
|
||||||
'webbyawards.com',
|
'webbyawards.com',
|
||||||
|
'cdn.shopify.com/s/javascripts/tricorder/xtld-read-only-frame.html',
|
||||||
]
|
]
|
||||||
var currentUrl = window.location.href
|
var currentUrl = window.location.href
|
||||||
var currentRegex
|
var currentRegex
|
||||||
|
@ -1,7 +1,7 @@
|
|||||||
const assert = require('assert')
|
const assert = require('assert')
|
||||||
const EventEmitter = require('events')
|
const EventEmitter = require('events')
|
||||||
const createMetamaskProvider = require('web3-provider-engine/zero.js')
|
const createMetamaskProvider = require('web3-provider-engine/zero.js')
|
||||||
const SubproviderFromProvider = require('web3-provider-engine/subproviders/web3.js')
|
const SubproviderFromProvider = require('web3-provider-engine/subproviders/provider.js')
|
||||||
const createInfuraProvider = require('eth-json-rpc-infura/src/createProvider')
|
const createInfuraProvider = require('eth-json-rpc-infura/src/createProvider')
|
||||||
const ObservableStore = require('obs-store')
|
const ObservableStore = require('obs-store')
|
||||||
const ComposedStore = require('obs-store/lib/composed')
|
const ComposedStore = require('obs-store/lib/composed')
|
||||||
|
@ -1,4 +1,5 @@
|
|||||||
const ObservableStore = require('obs-store')
|
const ObservableStore = require('obs-store')
|
||||||
|
const { warn } = require('loglevel')
|
||||||
|
|
||||||
// By default, poll every 3 minutes
|
// By default, poll every 3 minutes
|
||||||
const DEFAULT_INTERVAL = 180 * 1000
|
const DEFAULT_INTERVAL = 180 * 1000
|
||||||
@ -39,10 +40,13 @@ class TokenRatesController {
|
|||||||
*/
|
*/
|
||||||
async fetchExchangeRate (address) {
|
async fetchExchangeRate (address) {
|
||||||
try {
|
try {
|
||||||
const response = await fetch(`https://metamask.dev.balanc3.net/prices?from=${address}&to=ETH&autoConversion=false&summaryOnly=true`)
|
const response = await fetch(`https://metamask.balanc3.net/prices?from=${address}&to=ETH&autoConversion=false&summaryOnly=true`)
|
||||||
const json = await response.json()
|
const json = await response.json()
|
||||||
return json && json.length ? json[0].averagePrice : 0
|
return json && json.length ? json[0].averagePrice : 0
|
||||||
} catch (error) { }
|
} catch (error) {
|
||||||
|
warn(`MetaMask - TokenRatesController exchange rate fetch failed for ${address}.`, error)
|
||||||
|
return 0
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
92
app/scripts/controllers/transactions/README.md
Normal file
92
app/scripts/controllers/transactions/README.md
Normal file
@ -0,0 +1,92 @@
|
|||||||
|
# Transaction Controller
|
||||||
|
|
||||||
|
Transaction Controller is an aggregate of sub-controllers and trackers
|
||||||
|
exposed to the MetaMask controller.
|
||||||
|
|
||||||
|
- txStateManager
|
||||||
|
responsible for the state of a transaction and
|
||||||
|
storing the transaction
|
||||||
|
- pendingTxTracker
|
||||||
|
watching blocks for transactions to be include
|
||||||
|
and emitting confirmed events
|
||||||
|
- txGasUtil
|
||||||
|
gas calculations and safety buffering
|
||||||
|
- nonceTracker
|
||||||
|
calculating nonces
|
||||||
|
|
||||||
|
## Flow diagram of processing a transaction
|
||||||
|
|
||||||
|

|
||||||
|
|
||||||
|
## txMeta's & txParams
|
||||||
|
|
||||||
|
A txMeta is the "meta" object it has all the random bits of info we need about a transaction on it. txParams are sacred every thing on txParams gets signed so it must
|
||||||
|
be a valid key and be hex prefixed except for the network number. Extra stuff must go on the txMeta!
|
||||||
|
|
||||||
|
Here is a txMeta too look at:
|
||||||
|
|
||||||
|
```js
|
||||||
|
txMeta = {
|
||||||
|
"id": 2828415030114568, // unique id for this txMeta used for look ups
|
||||||
|
"time": 1524094064821, // time of creation
|
||||||
|
"status": "confirmed",
|
||||||
|
"metamaskNetworkId": "1524091532133", //the network id for the transaction
|
||||||
|
"loadingDefaults": false, // used to tell the ui when we are done calculatyig gass defaults
|
||||||
|
"txParams": { // the txParams object
|
||||||
|
"from": "0x8acce2391c0d510a6c5e5d8f819a678f79b7e675",
|
||||||
|
"to": "0x8acce2391c0d510a6c5e5d8f819a678f79b7e675",
|
||||||
|
"value": "0x0",
|
||||||
|
"gasPrice": "0x3b9aca00",
|
||||||
|
"gas": "0x7b0c",
|
||||||
|
"nonce": "0x0"
|
||||||
|
},
|
||||||
|
"history": [{ //debug
|
||||||
|
"id": 2828415030114568,
|
||||||
|
"time": 1524094064821,
|
||||||
|
"status": "unapproved",
|
||||||
|
"metamaskNetworkId": "1524091532133",
|
||||||
|
"loadingDefaults": true,
|
||||||
|
"txParams": {
|
||||||
|
"from": "0x8acce2391c0d510a6c5e5d8f819a678f79b7e675",
|
||||||
|
"to": "0x8acce2391c0d510a6c5e5d8f819a678f79b7e675",
|
||||||
|
"value": "0x0"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
[
|
||||||
|
{
|
||||||
|
"op": "add",
|
||||||
|
"path": "/txParams/gasPrice",
|
||||||
|
"value": "0x3b9aca00"
|
||||||
|
},
|
||||||
|
...], // I've removed most of history for this
|
||||||
|
"gasPriceSpecified": false, //whether or not the user/dapp has specified gasPrice
|
||||||
|
"gasLimitSpecified": false, //whether or not the user/dapp has specified gas
|
||||||
|
"estimatedGas": "5208",
|
||||||
|
"origin": "MetaMask", //debug
|
||||||
|
"nonceDetails": {
|
||||||
|
"params": {
|
||||||
|
"highestLocallyConfirmed": 0,
|
||||||
|
"highestSuggested": 0,
|
||||||
|
"nextNetworkNonce": 0
|
||||||
|
},
|
||||||
|
"local": {
|
||||||
|
"name": "local",
|
||||||
|
"nonce": 0,
|
||||||
|
"details": {
|
||||||
|
"startPoint": 0,
|
||||||
|
"highest": 0
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"network": {
|
||||||
|
"name": "network",
|
||||||
|
"nonce": 0,
|
||||||
|
"details": {
|
||||||
|
"baseCount": 0
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"rawTx": "0xf86980843b9aca00827b0c948acce2391c0d510a6c5e5d8f819a678f79b7e67580808602c5b5de66eea05c01a320b96ac730cb210ca56d2cb71fa360e1fc2c21fa5cf333687d18eb323fa02ed05987a6e5fd0f2459fcff80710b76b83b296454ad9a37594a0ccb4643ea90", // used for rebroadcast
|
||||||
|
"hash": "0xa45ba834b97c15e6ff4ed09badd04ecd5ce884b455eb60192cdc73bcc583972a",
|
||||||
|
"submittedTime": 1524094077902 // time of the attempt to submit the raw tx to the network, used in the ui to show the retry button
|
||||||
|
}
|
||||||
|
```
|
@ -3,28 +3,42 @@ const ObservableStore = require('obs-store')
|
|||||||
const ethUtil = require('ethereumjs-util')
|
const ethUtil = require('ethereumjs-util')
|
||||||
const Transaction = require('ethereumjs-tx')
|
const Transaction = require('ethereumjs-tx')
|
||||||
const EthQuery = require('ethjs-query')
|
const EthQuery = require('ethjs-query')
|
||||||
const TransactionStateManager = require('../lib/tx-state-manager')
|
const TransactionStateManager = require('./tx-state-manager')
|
||||||
const TxGasUtil = require('../lib/tx-gas-utils')
|
const TxGasUtil = require('./tx-gas-utils')
|
||||||
const PendingTransactionTracker = require('../lib/pending-tx-tracker')
|
const PendingTransactionTracker = require('./pending-tx-tracker')
|
||||||
const NonceTracker = require('../lib/nonce-tracker')
|
const NonceTracker = require('./nonce-tracker')
|
||||||
|
const txUtils = require('./lib/util')
|
||||||
const log = require('loglevel')
|
const log = require('loglevel')
|
||||||
|
|
||||||
/*
|
/**
|
||||||
Transaction Controller is an aggregate of sub-controllers and trackers
|
Transaction Controller is an aggregate of sub-controllers and trackers
|
||||||
composing them in a way to be exposed to the metamask controller
|
composing them in a way to be exposed to the metamask controller
|
||||||
- txStateManager
|
<br>- txStateManager
|
||||||
responsible for the state of a transaction and
|
responsible for the state of a transaction and
|
||||||
storing the transaction
|
storing the transaction
|
||||||
- pendingTxTracker
|
<br>- pendingTxTracker
|
||||||
watching blocks for transactions to be include
|
watching blocks for transactions to be include
|
||||||
and emitting confirmed events
|
and emitting confirmed events
|
||||||
- txGasUtil
|
<br>- txGasUtil
|
||||||
gas calculations and safety buffering
|
gas calculations and safety buffering
|
||||||
- nonceTracker
|
<br>- nonceTracker
|
||||||
calculating nonces
|
calculating nonces
|
||||||
|
|
||||||
|
|
||||||
|
@class
|
||||||
|
@param {object} - opts
|
||||||
|
@param {object} opts.initState - initial transaction list default is an empty array
|
||||||
|
@param {Object} opts.networkStore - an observable store for network number
|
||||||
|
@param {Object} opts.blockTracker - An instance of eth-blocktracker
|
||||||
|
@param {Object} opts.provider - A network provider.
|
||||||
|
@param {Function} opts.signTransaction - function the signs an ethereumjs-tx
|
||||||
|
@param {Function} [opts.getGasPrice] - optional gas price calculator
|
||||||
|
@param {Function} opts.signTransaction - ethTx signer that returns a rawTx
|
||||||
|
@param {Number} [opts.txHistoryLimit] - number *optional* for limiting how many transactions are in state
|
||||||
|
@param {Object} opts.preferencesStore
|
||||||
*/
|
*/
|
||||||
|
|
||||||
module.exports = class TransactionController extends EventEmitter {
|
class TransactionController extends EventEmitter {
|
||||||
constructor (opts) {
|
constructor (opts) {
|
||||||
super()
|
super()
|
||||||
this.networkStore = opts.networkStore || new ObservableStore({})
|
this.networkStore = opts.networkStore || new ObservableStore({})
|
||||||
@ -38,45 +52,19 @@ module.exports = class TransactionController extends EventEmitter {
|
|||||||
this.query = new EthQuery(this.provider)
|
this.query = new EthQuery(this.provider)
|
||||||
this.txGasUtil = new TxGasUtil(this.provider)
|
this.txGasUtil = new TxGasUtil(this.provider)
|
||||||
|
|
||||||
|
this._mapMethods()
|
||||||
this.txStateManager = new TransactionStateManager({
|
this.txStateManager = new TransactionStateManager({
|
||||||
initState: opts.initState,
|
initState: opts.initState,
|
||||||
txHistoryLimit: opts.txHistoryLimit,
|
txHistoryLimit: opts.txHistoryLimit,
|
||||||
getNetwork: this.getNetwork.bind(this),
|
getNetwork: this.getNetwork.bind(this),
|
||||||
})
|
})
|
||||||
|
this._onBootCleanUp()
|
||||||
this.txStateManager.getFilteredTxList({
|
|
||||||
status: 'unapproved',
|
|
||||||
loadingDefaults: true,
|
|
||||||
}).forEach((tx) => {
|
|
||||||
this.addTxDefaults(tx)
|
|
||||||
.then((txMeta) => {
|
|
||||||
txMeta.loadingDefaults = false
|
|
||||||
this.txStateManager.updateTx(txMeta, 'transactions: gas estimation for tx on boot')
|
|
||||||
}).catch((error) => {
|
|
||||||
this.txStateManager.setTxStatusFailed(tx.id, error)
|
|
||||||
})
|
|
||||||
})
|
|
||||||
|
|
||||||
this.txStateManager.getFilteredTxList({
|
|
||||||
status: 'approved',
|
|
||||||
}).forEach((txMeta) => {
|
|
||||||
const txSignError = new Error('Transaction found as "approved" during boot - possibly stuck during signing')
|
|
||||||
this.txStateManager.setTxStatusFailed(txMeta.id, txSignError)
|
|
||||||
})
|
|
||||||
|
|
||||||
|
|
||||||
this.store = this.txStateManager.store
|
this.store = this.txStateManager.store
|
||||||
this.txStateManager.on('tx:status-update', this.emit.bind(this, 'tx:status-update'))
|
|
||||||
this.nonceTracker = new NonceTracker({
|
this.nonceTracker = new NonceTracker({
|
||||||
provider: this.provider,
|
provider: this.provider,
|
||||||
getPendingTransactions: this.txStateManager.getPendingTransactions.bind(this.txStateManager),
|
getPendingTransactions: this.txStateManager.getPendingTransactions.bind(this.txStateManager),
|
||||||
getConfirmedTransactions: (address) => {
|
getConfirmedTransactions: this.txStateManager.getConfirmedTransactions.bind(this.txStateManager),
|
||||||
return this.txStateManager.getFilteredTxList({
|
|
||||||
from: address,
|
|
||||||
status: 'confirmed',
|
|
||||||
err: undefined,
|
|
||||||
})
|
|
||||||
},
|
|
||||||
})
|
})
|
||||||
|
|
||||||
this.pendingTxTracker = new PendingTransactionTracker({
|
this.pendingTxTracker = new PendingTransactionTracker({
|
||||||
@ -88,60 +76,14 @@ module.exports = class TransactionController extends EventEmitter {
|
|||||||
})
|
})
|
||||||
|
|
||||||
this.txStateManager.store.subscribe(() => this.emit('update:badge'))
|
this.txStateManager.store.subscribe(() => this.emit('update:badge'))
|
||||||
|
this._setupListners()
|
||||||
this.pendingTxTracker.on('tx:warning', (txMeta) => {
|
|
||||||
this.txStateManager.updateTx(txMeta, 'transactions/pending-tx-tracker#event: tx:warning')
|
|
||||||
})
|
|
||||||
this.pendingTxTracker.on('tx:confirmed', (txId) => this._markNonceDuplicatesDropped(txId))
|
|
||||||
this.pendingTxTracker.on('tx:failed', this.txStateManager.setTxStatusFailed.bind(this.txStateManager))
|
|
||||||
this.pendingTxTracker.on('tx:block-update', (txMeta, latestBlockNumber) => {
|
|
||||||
if (!txMeta.firstRetryBlockNumber) {
|
|
||||||
txMeta.firstRetryBlockNumber = latestBlockNumber
|
|
||||||
this.txStateManager.updateTx(txMeta, 'transactions/pending-tx-tracker#event: tx:block-update')
|
|
||||||
}
|
|
||||||
})
|
|
||||||
this.pendingTxTracker.on('tx:retry', (txMeta) => {
|
|
||||||
if (!('retryCount' in txMeta)) txMeta.retryCount = 0
|
|
||||||
txMeta.retryCount++
|
|
||||||
this.txStateManager.updateTx(txMeta, 'transactions/pending-tx-tracker#event: tx:retry')
|
|
||||||
})
|
|
||||||
|
|
||||||
this.blockTracker.on('block', this.pendingTxTracker.checkForTxInBlock.bind(this.pendingTxTracker))
|
|
||||||
// this is a little messy but until ethstore has been either
|
|
||||||
// removed or redone this is to guard against the race condition
|
|
||||||
this.blockTracker.on('latest', this.pendingTxTracker.resubmitPendingTxs.bind(this.pendingTxTracker))
|
|
||||||
this.blockTracker.on('sync', this.pendingTxTracker.queryPendingTxs.bind(this.pendingTxTracker))
|
|
||||||
// memstore is computed from a few different stores
|
// memstore is computed from a few different stores
|
||||||
this._updateMemstore()
|
this._updateMemstore()
|
||||||
this.txStateManager.store.subscribe(() => this._updateMemstore())
|
this.txStateManager.store.subscribe(() => this._updateMemstore())
|
||||||
this.networkStore.subscribe(() => this._updateMemstore())
|
this.networkStore.subscribe(() => this._updateMemstore())
|
||||||
this.preferencesStore.subscribe(() => this._updateMemstore())
|
this.preferencesStore.subscribe(() => this._updateMemstore())
|
||||||
}
|
}
|
||||||
|
/** @returns {number} the chainId*/
|
||||||
getState () {
|
|
||||||
return this.memStore.getState()
|
|
||||||
}
|
|
||||||
|
|
||||||
getNetwork () {
|
|
||||||
return this.networkStore.getState()
|
|
||||||
}
|
|
||||||
|
|
||||||
getSelectedAddress () {
|
|
||||||
return this.preferencesStore.getState().selectedAddress
|
|
||||||
}
|
|
||||||
|
|
||||||
getUnapprovedTxCount () {
|
|
||||||
return Object.keys(this.txStateManager.getUnapprovedTxList()).length
|
|
||||||
}
|
|
||||||
|
|
||||||
getPendingTxCount (account) {
|
|
||||||
return this.txStateManager.getPendingTransactions(account).length
|
|
||||||
}
|
|
||||||
|
|
||||||
getFilteredTxList (opts) {
|
|
||||||
return this.txStateManager.getFilteredTxList(opts)
|
|
||||||
}
|
|
||||||
|
|
||||||
getChainId () {
|
getChainId () {
|
||||||
const networkState = this.networkStore.getState()
|
const networkState = this.networkStore.getState()
|
||||||
const getChainId = parseInt(networkState)
|
const getChainId = parseInt(networkState)
|
||||||
@ -152,16 +94,30 @@ module.exports = class TransactionController extends EventEmitter {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
wipeTransactions (address) {
|
/**
|
||||||
this.txStateManager.wipeTransactions(address)
|
Adds a tx to the txlist
|
||||||
}
|
@emits ${txMeta.id}:unapproved
|
||||||
|
*/
|
||||||
// Adds a tx to the txlist
|
|
||||||
addTx (txMeta) {
|
addTx (txMeta) {
|
||||||
this.txStateManager.addTx(txMeta)
|
this.txStateManager.addTx(txMeta)
|
||||||
this.emit(`${txMeta.id}:unapproved`, txMeta)
|
this.emit(`${txMeta.id}:unapproved`, txMeta)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
Wipes the transactions for a given account
|
||||||
|
@param {string} address - hex string of the from address for txs being removed
|
||||||
|
*/
|
||||||
|
wipeTransactions (address) {
|
||||||
|
this.txStateManager.wipeTransactions(address)
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
add a new unapproved transaction to the pipeline
|
||||||
|
|
||||||
|
@returns {Promise<string>} the hash of the transaction after being submitted to the network
|
||||||
|
@param txParams {object} - txParams for the transaction
|
||||||
|
@param opts {object} - with the key origin to put the origin on the txMeta
|
||||||
|
*/
|
||||||
async newUnapprovedTransaction (txParams, opts = {}) {
|
async newUnapprovedTransaction (txParams, opts = {}) {
|
||||||
log.debug(`MetaMaskController newUnapprovedTransaction ${JSON.stringify(txParams)}`)
|
log.debug(`MetaMaskController newUnapprovedTransaction ${JSON.stringify(txParams)}`)
|
||||||
const initialTxMeta = await this.addUnapprovedTransaction(txParams)
|
const initialTxMeta = await this.addUnapprovedTransaction(txParams)
|
||||||
@ -184,17 +140,24 @@ module.exports = class TransactionController extends EventEmitter {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
Validates and generates a txMeta with defaults and puts it in txStateManager
|
||||||
|
store
|
||||||
|
|
||||||
|
@returns {txMeta}
|
||||||
|
*/
|
||||||
|
|
||||||
async addUnapprovedTransaction (txParams) {
|
async addUnapprovedTransaction (txParams) {
|
||||||
// validate
|
// validate
|
||||||
const normalizedTxParams = this._normalizeTxParams(txParams)
|
const normalizedTxParams = txUtils.normalizeTxParams(txParams)
|
||||||
this._validateTxParams(normalizedTxParams)
|
txUtils.validateTxParams(normalizedTxParams)
|
||||||
// construct txMeta
|
// construct txMeta
|
||||||
let txMeta = this.txStateManager.generateTxMeta({ txParams: normalizedTxParams })
|
let txMeta = this.txStateManager.generateTxMeta({ txParams: normalizedTxParams })
|
||||||
this.addTx(txMeta)
|
this.addTx(txMeta)
|
||||||
this.emit('newUnapprovedTx', txMeta)
|
this.emit('newUnapprovedTx', txMeta)
|
||||||
// add default tx params
|
// add default tx params
|
||||||
try {
|
try {
|
||||||
txMeta = await this.addTxDefaults(txMeta)
|
txMeta = await this.addTxGasDefaults(txMeta)
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.log(error)
|
console.log(error)
|
||||||
this.txStateManager.setTxStatusFailed(txMeta.id, error)
|
this.txStateManager.setTxStatusFailed(txMeta.id, error)
|
||||||
@ -206,21 +169,33 @@ module.exports = class TransactionController extends EventEmitter {
|
|||||||
|
|
||||||
return txMeta
|
return txMeta
|
||||||
}
|
}
|
||||||
|
/**
|
||||||
async addTxDefaults (txMeta) {
|
adds the tx gas defaults: gas && gasPrice
|
||||||
|
@param txMeta {Object} - the txMeta object
|
||||||
|
@returns {Promise<object>} resolves with txMeta
|
||||||
|
*/
|
||||||
|
async addTxGasDefaults (txMeta) {
|
||||||
const txParams = txMeta.txParams
|
const txParams = txMeta.txParams
|
||||||
// ensure value
|
// ensure value
|
||||||
|
txParams.value = txParams.value ? ethUtil.addHexPrefix(txParams.value) : '0x0'
|
||||||
txMeta.gasPriceSpecified = Boolean(txParams.gasPrice)
|
txMeta.gasPriceSpecified = Boolean(txParams.gasPrice)
|
||||||
let gasPrice = txParams.gasPrice
|
let gasPrice = txParams.gasPrice
|
||||||
if (!gasPrice) {
|
if (!gasPrice) {
|
||||||
gasPrice = this.getGasPrice ? this.getGasPrice() : await this.query.gasPrice()
|
gasPrice = this.getGasPrice ? this.getGasPrice() : await this.query.gasPrice()
|
||||||
}
|
}
|
||||||
txParams.gasPrice = ethUtil.addHexPrefix(gasPrice.toString(16))
|
txParams.gasPrice = ethUtil.addHexPrefix(gasPrice.toString(16))
|
||||||
txParams.value = txParams.value || '0x0'
|
|
||||||
// set gasLimit
|
// set gasLimit
|
||||||
return await this.txGasUtil.analyzeGasUsage(txMeta)
|
return await this.txGasUtil.analyzeGasUsage(txMeta)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
Creates a new txMeta with the same txParams as the original
|
||||||
|
to allow the user to resign the transaction with a higher gas values
|
||||||
|
@param originalTxId {number} - the id of the txMeta that
|
||||||
|
you want to attempt to retry
|
||||||
|
@return {txMeta}
|
||||||
|
*/
|
||||||
|
|
||||||
async retryTransaction (originalTxId) {
|
async retryTransaction (originalTxId) {
|
||||||
const originalTxMeta = this.txStateManager.getTx(originalTxId)
|
const originalTxMeta = this.txStateManager.getTx(originalTxId)
|
||||||
const lastGasPrice = originalTxMeta.txParams.gasPrice
|
const lastGasPrice = originalTxMeta.txParams.gasPrice
|
||||||
@ -234,15 +209,31 @@ module.exports = class TransactionController extends EventEmitter {
|
|||||||
return txMeta
|
return txMeta
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
updates the txMeta in the txStateManager
|
||||||
|
@param txMeta {Object} - the updated txMeta
|
||||||
|
*/
|
||||||
async updateTransaction (txMeta) {
|
async updateTransaction (txMeta) {
|
||||||
this.txStateManager.updateTx(txMeta, 'confTx: user updated transaction')
|
this.txStateManager.updateTx(txMeta, 'confTx: user updated transaction')
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
updates and approves the transaction
|
||||||
|
@param txMeta {Object}
|
||||||
|
*/
|
||||||
async updateAndApproveTransaction (txMeta) {
|
async updateAndApproveTransaction (txMeta) {
|
||||||
this.txStateManager.updateTx(txMeta, 'confTx: user approved transaction')
|
this.txStateManager.updateTx(txMeta, 'confTx: user approved transaction')
|
||||||
await this.approveTransaction(txMeta.id)
|
await this.approveTransaction(txMeta.id)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
sets the tx status to approved
|
||||||
|
auto fills the nonce
|
||||||
|
signs the transaction
|
||||||
|
publishes the transaction
|
||||||
|
if any of these steps fails the tx status will be set to failed
|
||||||
|
@param txId {number} - the tx's Id
|
||||||
|
*/
|
||||||
async approveTransaction (txId) {
|
async approveTransaction (txId) {
|
||||||
let nonceLock
|
let nonceLock
|
||||||
try {
|
try {
|
||||||
@ -274,7 +265,11 @@ module.exports = class TransactionController extends EventEmitter {
|
|||||||
throw err
|
throw err
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
/**
|
||||||
|
adds the chain id and signs the transaction and set the status to signed
|
||||||
|
@param txId {number} - the tx's Id
|
||||||
|
@returns - rawTx {string}
|
||||||
|
*/
|
||||||
async signTransaction (txId) {
|
async signTransaction (txId) {
|
||||||
const txMeta = this.txStateManager.getTx(txId)
|
const txMeta = this.txStateManager.getTx(txId)
|
||||||
// add network/chain id
|
// add network/chain id
|
||||||
@ -290,6 +285,12 @@ module.exports = class TransactionController extends EventEmitter {
|
|||||||
return rawTx
|
return rawTx
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
publishes the raw tx and sets the txMeta to submitted
|
||||||
|
@param txId {number} - the tx's Id
|
||||||
|
@param rawTx {string} - the hex string of the serialized signed transaction
|
||||||
|
@returns {Promise<void>}
|
||||||
|
*/
|
||||||
async publishTransaction (txId, rawTx) {
|
async publishTransaction (txId, rawTx) {
|
||||||
const txMeta = this.txStateManager.getTx(txId)
|
const txMeta = this.txStateManager.getTx(txId)
|
||||||
txMeta.rawTx = rawTx
|
txMeta.rawTx = rawTx
|
||||||
@ -299,11 +300,20 @@ module.exports = class TransactionController extends EventEmitter {
|
|||||||
this.txStateManager.setTxStatusSubmitted(txId)
|
this.txStateManager.setTxStatusSubmitted(txId)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
Convenience method for the ui thats sets the transaction to rejected
|
||||||
|
@param txId {number} - the tx's Id
|
||||||
|
@returns {Promise<void>}
|
||||||
|
*/
|
||||||
async cancelTransaction (txId) {
|
async cancelTransaction (txId) {
|
||||||
this.txStateManager.setTxStatusRejected(txId)
|
this.txStateManager.setTxStatusRejected(txId)
|
||||||
}
|
}
|
||||||
|
|
||||||
// receives a txHash records the tx as signed
|
/**
|
||||||
|
Sets the txHas on the txMeta
|
||||||
|
@param txId {number} - the tx's Id
|
||||||
|
@param txHash {string} - the hash for the txMeta
|
||||||
|
*/
|
||||||
setTxHash (txId, txHash) {
|
setTxHash (txId, txHash) {
|
||||||
// Add the tx hash to the persisted meta-tx object
|
// Add the tx hash to the persisted meta-tx object
|
||||||
const txMeta = this.txStateManager.getTx(txId)
|
const txMeta = this.txStateManager.getTx(txId)
|
||||||
@ -314,63 +324,92 @@ module.exports = class TransactionController extends EventEmitter {
|
|||||||
//
|
//
|
||||||
// PRIVATE METHODS
|
// PRIVATE METHODS
|
||||||
//
|
//
|
||||||
|
/** maps methods for convenience*/
|
||||||
|
_mapMethods () {
|
||||||
|
/** @returns the state in transaction controller */
|
||||||
|
this.getState = () => this.memStore.getState()
|
||||||
|
/** @returns the network number stored in networkStore */
|
||||||
|
this.getNetwork = () => this.networkStore.getState()
|
||||||
|
/** @returns the user selected address */
|
||||||
|
this.getSelectedAddress = () => this.preferencesStore.getState().selectedAddress
|
||||||
|
/** Returns an array of transactions whos status is unapproved */
|
||||||
|
this.getUnapprovedTxCount = () => Object.keys(this.txStateManager.getUnapprovedTxList()).length
|
||||||
|
/**
|
||||||
|
@returns a number that represents how many transactions have the status submitted
|
||||||
|
@param account {String} - hex prefixed account
|
||||||
|
*/
|
||||||
|
this.getPendingTxCount = (account) => this.txStateManager.getPendingTransactions(account).length
|
||||||
|
/** see txStateManager */
|
||||||
|
this.getFilteredTxList = (opts) => this.txStateManager.getFilteredTxList(opts)
|
||||||
|
}
|
||||||
|
|
||||||
_normalizeTxParams (txParams) {
|
/**
|
||||||
// functions that handle normalizing of that key in txParams
|
If transaction controller was rebooted with transactions that are uncompleted
|
||||||
const whiteList = {
|
in steps of the transaction signing or user confirmation process it will either
|
||||||
from: from => ethUtil.addHexPrefix(from).toLowerCase(),
|
transition txMetas to a failed state or try to redo those tasks.
|
||||||
to: to => ethUtil.addHexPrefix(txParams.to).toLowerCase(),
|
*/
|
||||||
nonce: nonce => ethUtil.addHexPrefix(nonce),
|
|
||||||
value: value => ethUtil.addHexPrefix(value),
|
|
||||||
data: data => ethUtil.addHexPrefix(data),
|
|
||||||
gas: gas => ethUtil.addHexPrefix(gas),
|
|
||||||
gasPrice: gasPrice => ethUtil.addHexPrefix(gasPrice),
|
|
||||||
}
|
|
||||||
|
|
||||||
// apply only keys in the whiteList
|
_onBootCleanUp () {
|
||||||
const normalizedTxParams = {}
|
this.txStateManager.getFilteredTxList({
|
||||||
Object.keys(whiteList).forEach((key) => {
|
status: 'unapproved',
|
||||||
if (txParams[key]) normalizedTxParams[key] = whiteList[key](txParams[key])
|
loadingDefaults: true,
|
||||||
|
}).forEach((tx) => {
|
||||||
|
this.addTxGasDefaults(tx)
|
||||||
|
.then((txMeta) => {
|
||||||
|
txMeta.loadingDefaults = false
|
||||||
|
this.txStateManager.updateTx(txMeta, 'transactions: gas estimation for tx on boot')
|
||||||
|
}).catch((error) => {
|
||||||
|
this.txStateManager.setTxStatusFailed(tx.id, error)
|
||||||
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
return normalizedTxParams
|
this.txStateManager.getFilteredTxList({
|
||||||
|
status: 'approved',
|
||||||
|
}).forEach((txMeta) => {
|
||||||
|
const txSignError = new Error('Transaction found as "approved" during boot - possibly stuck during signing')
|
||||||
|
this.txStateManager.setTxStatusFailed(txMeta.id, txSignError)
|
||||||
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
_validateTxParams (txParams) {
|
/**
|
||||||
this._validateFrom(txParams)
|
is called in constructor applies the listeners for pendingTxTracker txStateManager
|
||||||
this._validateRecipient(txParams)
|
and blockTracker
|
||||||
if ('value' in txParams) {
|
*/
|
||||||
const value = txParams.value.toString()
|
_setupListners () {
|
||||||
if (value.includes('-')) {
|
this.txStateManager.on('tx:status-update', this.emit.bind(this, 'tx:status-update'))
|
||||||
throw new Error(`Invalid transaction value of ${txParams.value} not a positive number.`)
|
this.pendingTxTracker.on('tx:warning', (txMeta) => {
|
||||||
|
this.txStateManager.updateTx(txMeta, 'transactions/pending-tx-tracker#event: tx:warning')
|
||||||
|
})
|
||||||
|
this.pendingTxTracker.on('tx:confirmed', (txId) => this.txStateManager.setTxStatusConfirmed(txId))
|
||||||
|
this.pendingTxTracker.on('tx:confirmed', (txId) => this._markNonceDuplicatesDropped(txId))
|
||||||
|
this.pendingTxTracker.on('tx:failed', this.txStateManager.setTxStatusFailed.bind(this.txStateManager))
|
||||||
|
this.pendingTxTracker.on('tx:block-update', (txMeta, latestBlockNumber) => {
|
||||||
|
if (!txMeta.firstRetryBlockNumber) {
|
||||||
|
txMeta.firstRetryBlockNumber = latestBlockNumber
|
||||||
|
this.txStateManager.updateTx(txMeta, 'transactions/pending-tx-tracker#event: tx:block-update')
|
||||||
}
|
}
|
||||||
|
})
|
||||||
|
this.pendingTxTracker.on('tx:retry', (txMeta) => {
|
||||||
|
if (!('retryCount' in txMeta)) txMeta.retryCount = 0
|
||||||
|
txMeta.retryCount++
|
||||||
|
this.txStateManager.updateTx(txMeta, 'transactions/pending-tx-tracker#event: tx:retry')
|
||||||
|
})
|
||||||
|
|
||||||
|
this.blockTracker.on('block', this.pendingTxTracker.checkForTxInBlock.bind(this.pendingTxTracker))
|
||||||
|
// this is a little messy but until ethstore has been either
|
||||||
|
// removed or redone this is to guard against the race condition
|
||||||
|
this.blockTracker.on('latest', this.pendingTxTracker.resubmitPendingTxs.bind(this.pendingTxTracker))
|
||||||
|
this.blockTracker.on('sync', this.pendingTxTracker.queryPendingTxs.bind(this.pendingTxTracker))
|
||||||
|
|
||||||
if (value.includes('.')) {
|
|
||||||
throw new Error(`Invalid transaction value of ${txParams.value} number must be in wei`)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
_validateFrom (txParams) {
|
/**
|
||||||
if ( !(typeof txParams.from === 'string') ) throw new Error(`Invalid from address ${txParams.from} not a string`)
|
Sets other txMeta statuses to dropped if the txMeta that has been confirmed has other transactions
|
||||||
if (!ethUtil.isValidAddress(txParams.from)) throw new Error('Invalid from address')
|
in the list have the same nonce
|
||||||
}
|
|
||||||
|
|
||||||
_validateRecipient (txParams) {
|
|
||||||
if (txParams.to === '0x' || txParams.to === null ) {
|
|
||||||
if (txParams.data) {
|
|
||||||
delete txParams.to
|
|
||||||
} else {
|
|
||||||
throw new Error('Invalid recipient address')
|
|
||||||
}
|
|
||||||
} else if ( txParams.to !== undefined && !ethUtil.isValidAddress(txParams.to) ) {
|
|
||||||
throw new Error('Invalid recipient address')
|
|
||||||
}
|
|
||||||
return txParams
|
|
||||||
}
|
|
||||||
|
|
||||||
|
@param txId {Number} - the txId of the transaction that has been confirmed in a block
|
||||||
|
*/
|
||||||
_markNonceDuplicatesDropped (txId) {
|
_markNonceDuplicatesDropped (txId) {
|
||||||
this.txStateManager.setTxStatusConfirmed(txId)
|
|
||||||
// get the confirmed transactions nonce and from address
|
// get the confirmed transactions nonce and from address
|
||||||
const txMeta = this.txStateManager.getTx(txId)
|
const txMeta = this.txStateManager.getTx(txId)
|
||||||
const { nonce, from } = txMeta.txParams
|
const { nonce, from } = txMeta.txParams
|
||||||
@ -385,6 +424,9 @@ module.exports = class TransactionController extends EventEmitter {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
Updates the memStore in transaction controller
|
||||||
|
*/
|
||||||
_updateMemstore () {
|
_updateMemstore () {
|
||||||
const unapprovedTxs = this.txStateManager.getUnapprovedTxList()
|
const unapprovedTxs = this.txStateManager.getUnapprovedTxList()
|
||||||
const selectedAddressTxList = this.txStateManager.getFilteredTxList({
|
const selectedAddressTxList = this.txStateManager.getFilteredTxList({
|
||||||
@ -394,3 +436,5 @@ module.exports = class TransactionController extends EventEmitter {
|
|||||||
this.memStore.updateState({ unapprovedTxs, selectedAddressTxList })
|
this.memStore.updateState({ unapprovedTxs, selectedAddressTxList })
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
module.exports = TransactionController
|
@ -1,6 +1,6 @@
|
|||||||
const jsonDiffer = require('fast-json-patch')
|
const jsonDiffer = require('fast-json-patch')
|
||||||
const clone = require('clone')
|
const clone = require('clone')
|
||||||
|
/** @module*/
|
||||||
module.exports = {
|
module.exports = {
|
||||||
generateHistoryEntry,
|
generateHistoryEntry,
|
||||||
replayHistory,
|
replayHistory,
|
||||||
@ -8,7 +8,11 @@ module.exports = {
|
|||||||
migrateFromSnapshotsToDiffs,
|
migrateFromSnapshotsToDiffs,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
converts non-initial history entries into diffs
|
||||||
|
@param longHistory {array}
|
||||||
|
@returns {array}
|
||||||
|
*/
|
||||||
function migrateFromSnapshotsToDiffs (longHistory) {
|
function migrateFromSnapshotsToDiffs (longHistory) {
|
||||||
return (
|
return (
|
||||||
longHistory
|
longHistory
|
||||||
@ -20,6 +24,17 @@ function migrateFromSnapshotsToDiffs (longHistory) {
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
generates an array of history objects sense the previous state.
|
||||||
|
The object has the keys opp(the operation preformed),
|
||||||
|
path(the key and if a nested object then each key will be seperated with a `/`)
|
||||||
|
value
|
||||||
|
with the first entry having the note
|
||||||
|
@param previousState {object} - the previous state of the object
|
||||||
|
@param newState {object} - the update object
|
||||||
|
@param note {string} - a optional note for the state change
|
||||||
|
@reurns {array}
|
||||||
|
*/
|
||||||
function generateHistoryEntry (previousState, newState, note) {
|
function generateHistoryEntry (previousState, newState, note) {
|
||||||
const entry = jsonDiffer.compare(previousState, newState)
|
const entry = jsonDiffer.compare(previousState, newState)
|
||||||
// Add a note to the first op, since it breaks if we append it to the entry
|
// Add a note to the first op, since it breaks if we append it to the entry
|
||||||
@ -27,11 +42,19 @@ function generateHistoryEntry (previousState, newState, note) {
|
|||||||
return entry
|
return entry
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
Recovers previous txMeta state obj
|
||||||
|
@return {object}
|
||||||
|
*/
|
||||||
function replayHistory (_shortHistory) {
|
function replayHistory (_shortHistory) {
|
||||||
const shortHistory = clone(_shortHistory)
|
const shortHistory = clone(_shortHistory)
|
||||||
return shortHistory.reduce((val, entry) => jsonDiffer.applyPatch(val, entry).newDocument)
|
return shortHistory.reduce((val, entry) => jsonDiffer.applyPatch(val, entry).newDocument)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
@param txMeta {Object}
|
||||||
|
@returns {object} a clone object of the txMeta with out history
|
||||||
|
*/
|
||||||
function snapshotFromTxMeta (txMeta) {
|
function snapshotFromTxMeta (txMeta) {
|
||||||
// create txMeta snapshot for history
|
// create txMeta snapshot for history
|
||||||
const snapshot = clone(txMeta)
|
const snapshot = clone(txMeta)
|
99
app/scripts/controllers/transactions/lib/util.js
Normal file
99
app/scripts/controllers/transactions/lib/util.js
Normal file
@ -0,0 +1,99 @@
|
|||||||
|
const {
|
||||||
|
addHexPrefix,
|
||||||
|
isValidAddress,
|
||||||
|
} = require('ethereumjs-util')
|
||||||
|
|
||||||
|
/**
|
||||||
|
@module
|
||||||
|
*/
|
||||||
|
module.exports = {
|
||||||
|
normalizeTxParams,
|
||||||
|
validateTxParams,
|
||||||
|
validateFrom,
|
||||||
|
validateRecipient,
|
||||||
|
getFinalStates,
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
// functions that handle normalizing of that key in txParams
|
||||||
|
const normalizers = {
|
||||||
|
from: from => addHexPrefix(from).toLowerCase(),
|
||||||
|
to: to => addHexPrefix(to).toLowerCase(),
|
||||||
|
nonce: nonce => addHexPrefix(nonce),
|
||||||
|
value: value => addHexPrefix(value),
|
||||||
|
data: data => addHexPrefix(data),
|
||||||
|
gas: gas => addHexPrefix(gas),
|
||||||
|
gasPrice: gasPrice => addHexPrefix(gasPrice),
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
normalizes txParams
|
||||||
|
@param txParams {object}
|
||||||
|
@returns {object} normalized txParams
|
||||||
|
*/
|
||||||
|
function normalizeTxParams (txParams) {
|
||||||
|
// apply only keys in the normalizers
|
||||||
|
const normalizedTxParams = {}
|
||||||
|
for (const key in normalizers) {
|
||||||
|
if (txParams[key]) normalizedTxParams[key] = normalizers[key](txParams[key])
|
||||||
|
}
|
||||||
|
return normalizedTxParams
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
validates txParams
|
||||||
|
@param txParams {object}
|
||||||
|
*/
|
||||||
|
function validateTxParams (txParams) {
|
||||||
|
validateFrom(txParams)
|
||||||
|
validateRecipient(txParams)
|
||||||
|
if ('value' in txParams) {
|
||||||
|
const value = txParams.value.toString()
|
||||||
|
if (value.includes('-')) {
|
||||||
|
throw new Error(`Invalid transaction value of ${txParams.value} not a positive number.`)
|
||||||
|
}
|
||||||
|
|
||||||
|
if (value.includes('.')) {
|
||||||
|
throw new Error(`Invalid transaction value of ${txParams.value} number must be in wei`)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
validates the from field in txParams
|
||||||
|
@param txParams {object}
|
||||||
|
*/
|
||||||
|
function validateFrom (txParams) {
|
||||||
|
if (!(typeof txParams.from === 'string')) throw new Error(`Invalid from address ${txParams.from} not a string`)
|
||||||
|
if (!isValidAddress(txParams.from)) throw new Error('Invalid from address')
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
validates the to field in txParams
|
||||||
|
@param txParams {object}
|
||||||
|
*/
|
||||||
|
function validateRecipient (txParams) {
|
||||||
|
if (txParams.to === '0x' || txParams.to === null) {
|
||||||
|
if (txParams.data) {
|
||||||
|
delete txParams.to
|
||||||
|
} else {
|
||||||
|
throw new Error('Invalid recipient address')
|
||||||
|
}
|
||||||
|
} else if (txParams.to !== undefined && !isValidAddress(txParams.to)) {
|
||||||
|
throw new Error('Invalid recipient address')
|
||||||
|
}
|
||||||
|
return txParams
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
@returns an {array} of states that can be considered final
|
||||||
|
*/
|
||||||
|
function getFinalStates () {
|
||||||
|
return [
|
||||||
|
'rejected', // the user has responded no!
|
||||||
|
'confirmed', // the tx has been included in a block.
|
||||||
|
'failed', // the tx failed for some reason, included on tx data.
|
||||||
|
'dropped', // the tx nonce was already used
|
||||||
|
]
|
||||||
|
}
|
||||||
|
|
@ -1,7 +1,15 @@
|
|||||||
const EthQuery = require('ethjs-query')
|
const EthQuery = require('ethjs-query')
|
||||||
const assert = require('assert')
|
const assert = require('assert')
|
||||||
const Mutex = require('await-semaphore').Mutex
|
const Mutex = require('await-semaphore').Mutex
|
||||||
|
/**
|
||||||
|
@param opts {Object}
|
||||||
|
@param {Object} opts.provider a ethereum provider
|
||||||
|
@param {Function} opts.getPendingTransactions a function that returns an array of txMeta
|
||||||
|
whosee status is `submitted`
|
||||||
|
@param {Function} opts.getConfirmedTransactions a function that returns an array of txMeta
|
||||||
|
whose status is `confirmed`
|
||||||
|
@class
|
||||||
|
*/
|
||||||
class NonceTracker {
|
class NonceTracker {
|
||||||
|
|
||||||
constructor ({ provider, getPendingTransactions, getConfirmedTransactions }) {
|
constructor ({ provider, getPendingTransactions, getConfirmedTransactions }) {
|
||||||
@ -12,6 +20,9 @@ class NonceTracker {
|
|||||||
this.lockMap = {}
|
this.lockMap = {}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
@returns {Promise<Object>} with the key releaseLock (the gloabl mutex)
|
||||||
|
*/
|
||||||
async getGlobalLock () {
|
async getGlobalLock () {
|
||||||
const globalMutex = this._lookupMutex('global')
|
const globalMutex = this._lookupMutex('global')
|
||||||
// await global mutex free
|
// await global mutex free
|
||||||
@ -19,8 +30,20 @@ class NonceTracker {
|
|||||||
return { releaseLock }
|
return { releaseLock }
|
||||||
}
|
}
|
||||||
|
|
||||||
// releaseLock must be called
|
/**
|
||||||
// releaseLock must be called after adding signed tx to pending transactions (or discarding)
|
* @typedef NonceDetails
|
||||||
|
* @property {number} highestLocallyConfirmed - A hex string of the highest nonce on a confirmed transaction.
|
||||||
|
* @property {number} nextNetworkNonce - The next nonce suggested by the eth_getTransactionCount method.
|
||||||
|
* @property {number} highetSuggested - The maximum between the other two, the number returned.
|
||||||
|
*/
|
||||||
|
|
||||||
|
/**
|
||||||
|
this will return an object with the `nextNonce` `nonceDetails` of type NonceDetails, and the releaseLock
|
||||||
|
Note: releaseLock must be called after adding a signed tx to pending transactions (or discarding).
|
||||||
|
|
||||||
|
@param address {string} the hex string for the address whose nonce we are calculating
|
||||||
|
@returns {Promise<NonceDetails>}
|
||||||
|
*/
|
||||||
async getNonceLock (address) {
|
async getNonceLock (address) {
|
||||||
// await global mutex free
|
// await global mutex free
|
||||||
await this._globalMutexFree()
|
await this._globalMutexFree()
|
||||||
@ -123,6 +146,17 @@ class NonceTracker {
|
|||||||
return highestNonce
|
return highestNonce
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
@typedef {object} highestContinuousFrom
|
||||||
|
@property {string} - name the name for how the nonce was calculated based on the data used
|
||||||
|
@property {number} - nonce the next suggested nonce
|
||||||
|
@property {object} - details the provided starting nonce that was used (for debugging)
|
||||||
|
*/
|
||||||
|
/**
|
||||||
|
@param txList {array} - list of txMeta's
|
||||||
|
@param startPoint {number} - the highest known locally confirmed nonce
|
||||||
|
@returns {highestContinuousFrom}
|
||||||
|
*/
|
||||||
_getHighestContinuousFrom (txList, startPoint) {
|
_getHighestContinuousFrom (txList, startPoint) {
|
||||||
const nonces = txList.map((txMeta) => {
|
const nonces = txList.map((txMeta) => {
|
||||||
const nonce = txMeta.txParams.nonce
|
const nonce = txMeta.txParams.nonce
|
||||||
@ -140,6 +174,10 @@ class NonceTracker {
|
|||||||
|
|
||||||
// this is a hotfix for the fact that the blockTracker will
|
// this is a hotfix for the fact that the blockTracker will
|
||||||
// change when the network changes
|
// change when the network changes
|
||||||
|
|
||||||
|
/**
|
||||||
|
@returns {Object} the current blockTracker
|
||||||
|
*/
|
||||||
_getBlockTracker () {
|
_getBlockTracker () {
|
||||||
return this.provider._blockTracker
|
return this.provider._blockTracker
|
||||||
}
|
}
|
@ -1,23 +1,24 @@
|
|||||||
const EventEmitter = require('events')
|
const EventEmitter = require('events')
|
||||||
|
const log = require('loglevel')
|
||||||
const EthQuery = require('ethjs-query')
|
const EthQuery = require('ethjs-query')
|
||||||
/*
|
/**
|
||||||
|
|
||||||
Utility class for tracking the transactions as they
|
|
||||||
go from a pending state to a confirmed (mined in a block) state
|
|
||||||
|
|
||||||
|
Event emitter utility class for tracking the transactions as they<br>
|
||||||
|
go from a pending state to a confirmed (mined in a block) state<br>
|
||||||
|
<br>
|
||||||
As well as continues broadcast while in the pending state
|
As well as continues broadcast while in the pending state
|
||||||
|
<br>
|
||||||
|
@param config {object} - non optional configuration object consists of:
|
||||||
|
@param {Object} config.provider - A network provider.
|
||||||
|
@param {Object} config.nonceTracker see nonce tracker
|
||||||
|
@param {function} config.getPendingTransactions a function for getting an array of transactions,
|
||||||
|
@param {function} config.publishTransaction a async function for publishing raw transactions,
|
||||||
|
|
||||||
~config is not optional~
|
|
||||||
requires a: {
|
|
||||||
provider: //,
|
|
||||||
nonceTracker: //see nonce tracker,
|
|
||||||
getPendingTransactions: //() a function for getting an array of transactions,
|
|
||||||
publishTransaction: //(rawTx) a async function for publishing raw transactions,
|
|
||||||
}
|
|
||||||
|
|
||||||
|
@class
|
||||||
*/
|
*/
|
||||||
|
|
||||||
module.exports = class PendingTransactionTracker extends EventEmitter {
|
class PendingTransactionTracker extends EventEmitter {
|
||||||
constructor (config) {
|
constructor (config) {
|
||||||
super()
|
super()
|
||||||
this.query = new EthQuery(config.provider)
|
this.query = new EthQuery(config.provider)
|
||||||
@ -29,8 +30,13 @@ module.exports = class PendingTransactionTracker extends EventEmitter {
|
|||||||
this._checkPendingTxs()
|
this._checkPendingTxs()
|
||||||
}
|
}
|
||||||
|
|
||||||
// checks if a signed tx is in a block and
|
/**
|
||||||
// if included sets the tx status as 'confirmed'
|
checks if a signed tx is in a block and
|
||||||
|
if it is included emits tx status as 'confirmed'
|
||||||
|
@param block {object}, a full block
|
||||||
|
@emits tx:confirmed
|
||||||
|
@emits tx:failed
|
||||||
|
*/
|
||||||
checkForTxInBlock (block) {
|
checkForTxInBlock (block) {
|
||||||
const signedTxList = this.getPendingTransactions()
|
const signedTxList = this.getPendingTransactions()
|
||||||
if (!signedTxList.length) return
|
if (!signedTxList.length) return
|
||||||
@ -52,6 +58,11 @@ module.exports = class PendingTransactionTracker extends EventEmitter {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
asks the network for the transaction to see if a block number is included on it
|
||||||
|
if we have skipped/missed blocks
|
||||||
|
@param object - oldBlock newBlock
|
||||||
|
*/
|
||||||
queryPendingTxs ({ oldBlock, newBlock }) {
|
queryPendingTxs ({ oldBlock, newBlock }) {
|
||||||
// check pending transactions on start
|
// check pending transactions on start
|
||||||
if (!oldBlock) {
|
if (!oldBlock) {
|
||||||
@ -63,7 +74,11 @@ module.exports = class PendingTransactionTracker extends EventEmitter {
|
|||||||
if (diff > 1) this._checkPendingTxs()
|
if (diff > 1) this._checkPendingTxs()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
Will resubmit any transactions who have not been confirmed in a block
|
||||||
|
@param block {object} - a block object
|
||||||
|
@emits tx:warning
|
||||||
|
*/
|
||||||
resubmitPendingTxs (block) {
|
resubmitPendingTxs (block) {
|
||||||
const pending = this.getPendingTransactions()
|
const pending = this.getPendingTransactions()
|
||||||
// only try resubmitting if their are transactions to resubmit
|
// only try resubmitting if their are transactions to resubmit
|
||||||
@ -100,6 +115,13 @@ module.exports = class PendingTransactionTracker extends EventEmitter {
|
|||||||
}))
|
}))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
resubmits the individual txMeta used in resubmitPendingTxs
|
||||||
|
@param txMeta {Object} - txMeta object
|
||||||
|
@param latestBlockNumber {string} - hex string for the latest block number
|
||||||
|
@emits tx:retry
|
||||||
|
@returns txHash {string}
|
||||||
|
*/
|
||||||
async _resubmitTx (txMeta, latestBlockNumber) {
|
async _resubmitTx (txMeta, latestBlockNumber) {
|
||||||
if (!txMeta.firstRetryBlockNumber) {
|
if (!txMeta.firstRetryBlockNumber) {
|
||||||
this.emit('tx:block-update', txMeta, latestBlockNumber)
|
this.emit('tx:block-update', txMeta, latestBlockNumber)
|
||||||
@ -123,7 +145,13 @@ module.exports = class PendingTransactionTracker extends EventEmitter {
|
|||||||
this.emit('tx:retry', txMeta)
|
this.emit('tx:retry', txMeta)
|
||||||
return txHash
|
return txHash
|
||||||
}
|
}
|
||||||
|
/**
|
||||||
|
Ask the network for the transaction to see if it has been include in a block
|
||||||
|
@param txMeta {Object} - the txMeta object
|
||||||
|
@emits tx:failed
|
||||||
|
@emits tx:confirmed
|
||||||
|
@emits tx:warning
|
||||||
|
*/
|
||||||
async _checkPendingTx (txMeta) {
|
async _checkPendingTx (txMeta) {
|
||||||
const txHash = txMeta.hash
|
const txHash = txMeta.hash
|
||||||
const txId = txMeta.id
|
const txId = txMeta.id
|
||||||
@ -162,8 +190,9 @@ module.exports = class PendingTransactionTracker extends EventEmitter {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// checks the network for signed txs and
|
/**
|
||||||
// if confirmed sets the tx status as 'confirmed'
|
checks the network for signed txs and releases the nonce global lock if it is
|
||||||
|
*/
|
||||||
async _checkPendingTxs () {
|
async _checkPendingTxs () {
|
||||||
const signedTxList = this.getPendingTransactions()
|
const signedTxList = this.getPendingTransactions()
|
||||||
// in order to keep the nonceTracker accurate we block it while updating pending transactions
|
// in order to keep the nonceTracker accurate we block it while updating pending transactions
|
||||||
@ -171,12 +200,17 @@ module.exports = class PendingTransactionTracker extends EventEmitter {
|
|||||||
try {
|
try {
|
||||||
await Promise.all(signedTxList.map((txMeta) => this._checkPendingTx(txMeta)))
|
await Promise.all(signedTxList.map((txMeta) => this._checkPendingTx(txMeta)))
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
console.error('PendingTransactionWatcher - Error updating pending transactions')
|
log.error('PendingTransactionWatcher - Error updating pending transactions')
|
||||||
console.error(err)
|
log.error(err)
|
||||||
}
|
}
|
||||||
nonceGlobalLock.releaseLock()
|
nonceGlobalLock.releaseLock()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
checks to see if a confirmed txMeta has the same nonce
|
||||||
|
@param txMeta {Object} - txMeta object
|
||||||
|
@returns {boolean}
|
||||||
|
*/
|
||||||
async _checkIfNonceIsTaken (txMeta) {
|
async _checkIfNonceIsTaken (txMeta) {
|
||||||
const address = txMeta.txParams.from
|
const address = txMeta.txParams.from
|
||||||
const completed = this.getCompletedTransactions(address)
|
const completed = this.getCompletedTransactions(address)
|
||||||
@ -185,5 +219,6 @@ module.exports = class PendingTransactionTracker extends EventEmitter {
|
|||||||
})
|
})
|
||||||
return sameNonce.length > 0
|
return sameNonce.length > 0
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
module.exports = PendingTransactionTracker
|
@ -3,22 +3,27 @@ const {
|
|||||||
hexToBn,
|
hexToBn,
|
||||||
BnMultiplyByFraction,
|
BnMultiplyByFraction,
|
||||||
bnToHex,
|
bnToHex,
|
||||||
} = require('./util')
|
} = require('../../lib/util')
|
||||||
const { addHexPrefix } = require('ethereumjs-util')
|
const { addHexPrefix } = require('ethereumjs-util')
|
||||||
const SIMPLE_GAS_COST = '0x5208' // Hex for 21000, cost of a simple send.
|
const SIMPLE_GAS_COST = '0x5208' // Hex for 21000, cost of a simple send.
|
||||||
|
|
||||||
/*
|
/**
|
||||||
tx-utils are utility methods for Transaction manager
|
tx-gas-utils are gas utility methods for Transaction manager
|
||||||
its passed ethquery
|
its passed ethquery
|
||||||
and used to do things like calculate gas of a tx.
|
and used to do things like calculate gas of a tx.
|
||||||
|
@param {Object} provider - A network provider.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
module.exports = class TxGasUtil {
|
class TxGasUtil {
|
||||||
|
|
||||||
constructor (provider) {
|
constructor (provider) {
|
||||||
this.query = new EthQuery(provider)
|
this.query = new EthQuery(provider)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
@param txMeta {Object} - the txMeta object
|
||||||
|
@returns {object} the txMeta object with the gas written to the txParams
|
||||||
|
*/
|
||||||
async analyzeGasUsage (txMeta) {
|
async analyzeGasUsage (txMeta) {
|
||||||
const block = await this.query.getBlockByNumber('latest', true)
|
const block = await this.query.getBlockByNumber('latest', true)
|
||||||
let estimatedGasHex
|
let estimatedGasHex
|
||||||
@ -38,6 +43,12 @@ module.exports = class TxGasUtil {
|
|||||||
return txMeta
|
return txMeta
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
Estimates the tx's gas usage
|
||||||
|
@param txMeta {Object} - the txMeta object
|
||||||
|
@param blockGasLimitHex {string} - hex string of the block's gas limit
|
||||||
|
@returns {string} the estimated gas limit as a hex string
|
||||||
|
*/
|
||||||
async estimateTxGas (txMeta, blockGasLimitHex) {
|
async estimateTxGas (txMeta, blockGasLimitHex) {
|
||||||
const txParams = txMeta.txParams
|
const txParams = txMeta.txParams
|
||||||
|
|
||||||
@ -70,6 +81,12 @@ module.exports = class TxGasUtil {
|
|||||||
return await this.query.estimateGas(txParams)
|
return await this.query.estimateGas(txParams)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
Writes the gas on the txParams in the txMeta
|
||||||
|
@param txMeta {Object} - the txMeta object to write to
|
||||||
|
@param blockGasLimitHex {string} - the block gas limit hex
|
||||||
|
@param estimatedGasHex {string} - the estimated gas hex
|
||||||
|
*/
|
||||||
setTxGas (txMeta, blockGasLimitHex, estimatedGasHex) {
|
setTxGas (txMeta, blockGasLimitHex, estimatedGasHex) {
|
||||||
txMeta.estimatedGas = addHexPrefix(estimatedGasHex)
|
txMeta.estimatedGas = addHexPrefix(estimatedGasHex)
|
||||||
const txParams = txMeta.txParams
|
const txParams = txMeta.txParams
|
||||||
@ -87,6 +104,13 @@ module.exports = class TxGasUtil {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
Adds a gas buffer with out exceeding the block gas limit
|
||||||
|
|
||||||
|
@param initialGasLimitHex {string} - the initial gas limit to add the buffer too
|
||||||
|
@param blockGasLimitHex {string} - the block gas limit
|
||||||
|
@returns {string} the buffered gas limit as a hex string
|
||||||
|
*/
|
||||||
addGasBuffer (initialGasLimitHex, blockGasLimitHex) {
|
addGasBuffer (initialGasLimitHex, blockGasLimitHex) {
|
||||||
const initialGasLimitBn = hexToBn(initialGasLimitHex)
|
const initialGasLimitBn = hexToBn(initialGasLimitHex)
|
||||||
const blockGasLimitBn = hexToBn(blockGasLimitHex)
|
const blockGasLimitBn = hexToBn(blockGasLimitHex)
|
||||||
@ -100,4 +124,6 @@ module.exports = class TxGasUtil {
|
|||||||
// otherwise use blockGasLimit
|
// otherwise use blockGasLimit
|
||||||
return bnToHex(upperGasLimitBn)
|
return bnToHex(upperGasLimitBn)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
module.exports = TxGasUtil
|
@ -1,22 +1,33 @@
|
|||||||
const extend = require('xtend')
|
const extend = require('xtend')
|
||||||
const EventEmitter = require('events')
|
const EventEmitter = require('events')
|
||||||
const ObservableStore = require('obs-store')
|
const ObservableStore = require('obs-store')
|
||||||
const createId = require('./random-id')
|
|
||||||
const ethUtil = require('ethereumjs-util')
|
const ethUtil = require('ethereumjs-util')
|
||||||
const txStateHistoryHelper = require('./tx-state-history-helper')
|
const txStateHistoryHelper = require('./lib/tx-state-history-helper')
|
||||||
|
const createId = require('../../lib/random-id')
|
||||||
// STATUS METHODS
|
const { getFinalStates } = require('./lib/util')
|
||||||
// statuses:
|
/**
|
||||||
// - `'unapproved'` the user has not responded
|
TransactionStateManager is responsible for the state of a transaction and
|
||||||
// - `'rejected'` the user has responded no!
|
storing the transaction
|
||||||
// - `'approved'` the user has approved the tx
|
it also has some convenience methods for finding subsets of transactions
|
||||||
// - `'signed'` the tx is signed
|
*
|
||||||
// - `'submitted'` the tx is sent to a server
|
*STATUS METHODS
|
||||||
// - `'confirmed'` the tx has been included in a block.
|
<br>statuses:
|
||||||
// - `'failed'` the tx failed for some reason, included on tx data.
|
<br> - `'unapproved'` the user has not responded
|
||||||
// - `'dropped'` the tx nonce was already used
|
<br> - `'rejected'` the user has responded no!
|
||||||
|
<br> - `'approved'` the user has approved the tx
|
||||||
module.exports = class TransactionStateManager extends EventEmitter {
|
<br> - `'signed'` the tx is signed
|
||||||
|
<br> - `'submitted'` the tx is sent to a server
|
||||||
|
<br> - `'confirmed'` the tx has been included in a block.
|
||||||
|
<br> - `'failed'` the tx failed for some reason, included on tx data.
|
||||||
|
<br> - `'dropped'` the tx nonce was already used
|
||||||
|
@param opts {object}
|
||||||
|
@param {object} [opts.initState={ transactions: [] }] initial transactions list with the key transaction {array}
|
||||||
|
@param {number} [opts.txHistoryLimit] limit for how many finished
|
||||||
|
transactions can hang around in state
|
||||||
|
@param {function} opts.getNetwork return network number
|
||||||
|
@class
|
||||||
|
*/
|
||||||
|
class TransactionStateManager extends EventEmitter {
|
||||||
constructor ({ initState, txHistoryLimit, getNetwork }) {
|
constructor ({ initState, txHistoryLimit, getNetwork }) {
|
||||||
super()
|
super()
|
||||||
|
|
||||||
@ -28,6 +39,10 @@ module.exports = class TransactionStateManager extends EventEmitter {
|
|||||||
this.getNetwork = getNetwork
|
this.getNetwork = getNetwork
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
@param opts {object} - the object to use when overwriting defaults
|
||||||
|
@returns {txMeta} the default txMeta object
|
||||||
|
*/
|
||||||
generateTxMeta (opts) {
|
generateTxMeta (opts) {
|
||||||
return extend({
|
return extend({
|
||||||
id: createId(),
|
id: createId(),
|
||||||
@ -38,17 +53,25 @@ module.exports = class TransactionStateManager extends EventEmitter {
|
|||||||
}, opts)
|
}, opts)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
@returns {array} of txMetas that have been filtered for only the current network
|
||||||
|
*/
|
||||||
getTxList () {
|
getTxList () {
|
||||||
const network = this.getNetwork()
|
const network = this.getNetwork()
|
||||||
const fullTxList = this.getFullTxList()
|
const fullTxList = this.getFullTxList()
|
||||||
return fullTxList.filter((txMeta) => txMeta.metamaskNetworkId === network)
|
return fullTxList.filter((txMeta) => txMeta.metamaskNetworkId === network)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
@returns {array} of all the txMetas in store
|
||||||
|
*/
|
||||||
getFullTxList () {
|
getFullTxList () {
|
||||||
return this.store.getState().transactions
|
return this.store.getState().transactions
|
||||||
}
|
}
|
||||||
|
|
||||||
// Returns the tx list
|
/**
|
||||||
|
@returns {array} the tx list whos status is unapproved
|
||||||
|
*/
|
||||||
getUnapprovedTxList () {
|
getUnapprovedTxList () {
|
||||||
const txList = this.getTxsByMetaData('status', 'unapproved')
|
const txList = this.getTxsByMetaData('status', 'unapproved')
|
||||||
return txList.reduce((result, tx) => {
|
return txList.reduce((result, tx) => {
|
||||||
@ -57,18 +80,37 @@ module.exports = class TransactionStateManager extends EventEmitter {
|
|||||||
}, {})
|
}, {})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
@param [address] {string} - hex prefixed address to sort the txMetas for [optional]
|
||||||
|
@returns {array} the tx list whos status is submitted if no address is provide
|
||||||
|
returns all txMetas who's status is submitted for the current network
|
||||||
|
*/
|
||||||
getPendingTransactions (address) {
|
getPendingTransactions (address) {
|
||||||
const opts = { status: 'submitted' }
|
const opts = { status: 'submitted' }
|
||||||
if (address) opts.from = address
|
if (address) opts.from = address
|
||||||
return this.getFilteredTxList(opts)
|
return this.getFilteredTxList(opts)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
@param [address] {string} - hex prefixed address to sort the txMetas for [optional]
|
||||||
|
@returns {array} the tx list whos status is confirmed if no address is provide
|
||||||
|
returns all txMetas who's status is confirmed for the current network
|
||||||
|
*/
|
||||||
getConfirmedTransactions (address) {
|
getConfirmedTransactions (address) {
|
||||||
const opts = { status: 'confirmed' }
|
const opts = { status: 'confirmed' }
|
||||||
if (address) opts.from = address
|
if (address) opts.from = address
|
||||||
return this.getFilteredTxList(opts)
|
return this.getFilteredTxList(opts)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
Adds the txMeta to the list of transactions in the store.
|
||||||
|
if the list is over txHistoryLimit it will remove a transaction that
|
||||||
|
is in its final state
|
||||||
|
it will allso add the key `history` to the txMeta with the snap shot of the original
|
||||||
|
object
|
||||||
|
@param txMeta {Object}
|
||||||
|
@returns {object} the txMeta
|
||||||
|
*/
|
||||||
addTx (txMeta) {
|
addTx (txMeta) {
|
||||||
this.once(`${txMeta.id}:signed`, function (txId) {
|
this.once(`${txMeta.id}:signed`, function (txId) {
|
||||||
this.removeAllListeners(`${txMeta.id}:rejected`)
|
this.removeAllListeners(`${txMeta.id}:rejected`)
|
||||||
@ -92,7 +134,9 @@ module.exports = class TransactionStateManager extends EventEmitter {
|
|||||||
// or rejected tx's.
|
// or rejected tx's.
|
||||||
// not tx's that are pending or unapproved
|
// not tx's that are pending or unapproved
|
||||||
if (txCount > txHistoryLimit - 1) {
|
if (txCount > txHistoryLimit - 1) {
|
||||||
let index = transactions.findIndex((metaTx) => metaTx.status === 'confirmed' || metaTx.status === 'rejected')
|
const index = transactions.findIndex((metaTx) => {
|
||||||
|
return getFinalStates().includes(metaTx.status)
|
||||||
|
})
|
||||||
if (index !== -1) {
|
if (index !== -1) {
|
||||||
transactions.splice(index, 1)
|
transactions.splice(index, 1)
|
||||||
}
|
}
|
||||||
@ -101,12 +145,21 @@ module.exports = class TransactionStateManager extends EventEmitter {
|
|||||||
this._saveTxList(transactions)
|
this._saveTxList(transactions)
|
||||||
return txMeta
|
return txMeta
|
||||||
}
|
}
|
||||||
// gets tx by Id and returns it
|
/**
|
||||||
|
@param txId {number}
|
||||||
|
@returns {object} the txMeta who matches the given id if none found
|
||||||
|
for the network returns undefined
|
||||||
|
*/
|
||||||
getTx (txId) {
|
getTx (txId) {
|
||||||
const txMeta = this.getTxsByMetaData('id', txId)[0]
|
const txMeta = this.getTxsByMetaData('id', txId)[0]
|
||||||
return txMeta
|
return txMeta
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
updates the txMeta in the list and adds a history entry
|
||||||
|
@param txMeta {Object} - the txMeta to update
|
||||||
|
@param [note] {string} - a not about the update for history
|
||||||
|
*/
|
||||||
updateTx (txMeta, note) {
|
updateTx (txMeta, note) {
|
||||||
// validate txParams
|
// validate txParams
|
||||||
if (txMeta.txParams) {
|
if (txMeta.txParams) {
|
||||||
@ -134,16 +187,23 @@ module.exports = class TransactionStateManager extends EventEmitter {
|
|||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
// merges txParams obj onto txData.txParams
|
/**
|
||||||
// use extend to ensure that all fields are filled
|
merges txParams obj onto txMeta.txParams
|
||||||
|
use extend to ensure that all fields are filled
|
||||||
|
@param txId {number} - the id of the txMeta
|
||||||
|
@param txParams {object} - the updated txParams
|
||||||
|
*/
|
||||||
updateTxParams (txId, txParams) {
|
updateTxParams (txId, txParams) {
|
||||||
const txMeta = this.getTx(txId)
|
const txMeta = this.getTx(txId)
|
||||||
txMeta.txParams = extend(txMeta.txParams, txParams)
|
txMeta.txParams = extend(txMeta.txParams, txParams)
|
||||||
this.updateTx(txMeta, `txStateManager#updateTxParams`)
|
this.updateTx(txMeta, `txStateManager#updateTxParams`)
|
||||||
}
|
}
|
||||||
|
|
||||||
// validates txParams members by type
|
/**
|
||||||
validateTxParams(txParams) {
|
validates txParams members by type
|
||||||
|
@param txParams {object} - txParams to validate
|
||||||
|
*/
|
||||||
|
validateTxParams (txParams) {
|
||||||
Object.keys(txParams).forEach((key) => {
|
Object.keys(txParams).forEach((key) => {
|
||||||
const value = txParams[key]
|
const value = txParams[key]
|
||||||
// validate types
|
// validate types
|
||||||
@ -159,17 +219,19 @@ module.exports = class TransactionStateManager extends EventEmitter {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
/*
|
/**
|
||||||
Takes an object of fields to search for eg:
|
@param opts {object} - an object of fields to search for eg:<br>
|
||||||
let thingsToLookFor = {
|
let <code>thingsToLookFor = {<br>
|
||||||
to: '0x0..',
|
to: '0x0..',<br>
|
||||||
from: '0x0..',
|
from: '0x0..',<br>
|
||||||
status: 'signed',
|
status: 'signed',<br>
|
||||||
err: undefined,
|
err: undefined,<br>
|
||||||
}
|
}<br></code>
|
||||||
and returns a list of tx with all
|
@param [initialList=this.getTxList()]
|
||||||
|
@returns a {array} of txMeta with all
|
||||||
options matching
|
options matching
|
||||||
|
*/
|
||||||
|
/*
|
||||||
****************HINT****************
|
****************HINT****************
|
||||||
| `err: undefined` is like looking |
|
| `err: undefined` is like looking |
|
||||||
| for a tx with no err |
|
| for a tx with no err |
|
||||||
@ -190,7 +252,14 @@ module.exports = class TransactionStateManager extends EventEmitter {
|
|||||||
})
|
})
|
||||||
return filteredTxList
|
return filteredTxList
|
||||||
}
|
}
|
||||||
|
/**
|
||||||
|
|
||||||
|
@param key {string} - the key to check
|
||||||
|
@param value - the value your looking for
|
||||||
|
@param [txList=this.getTxList()] {array} - the list to search. default is the txList
|
||||||
|
from txStateManager#getTxList
|
||||||
|
@returns {array} a list of txMetas who matches the search params
|
||||||
|
*/
|
||||||
getTxsByMetaData (key, value, txList = this.getTxList()) {
|
getTxsByMetaData (key, value, txList = this.getTxList()) {
|
||||||
return txList.filter((txMeta) => {
|
return txList.filter((txMeta) => {
|
||||||
if (txMeta.txParams[key]) {
|
if (txMeta.txParams[key]) {
|
||||||
@ -203,33 +272,51 @@ module.exports = class TransactionStateManager extends EventEmitter {
|
|||||||
|
|
||||||
// get::set status
|
// get::set status
|
||||||
|
|
||||||
// should return the status of the tx.
|
/**
|
||||||
|
@param txId {number} - the txMeta Id
|
||||||
|
@return {string} the status of the tx.
|
||||||
|
*/
|
||||||
getTxStatus (txId) {
|
getTxStatus (txId) {
|
||||||
const txMeta = this.getTx(txId)
|
const txMeta = this.getTx(txId)
|
||||||
return txMeta.status
|
return txMeta.status
|
||||||
}
|
}
|
||||||
|
|
||||||
// should update the status of the tx to 'rejected'.
|
/**
|
||||||
|
should update the status of the tx to 'rejected'.
|
||||||
|
@param txId {number} - the txMeta Id
|
||||||
|
*/
|
||||||
setTxStatusRejected (txId) {
|
setTxStatusRejected (txId) {
|
||||||
this._setTxStatus(txId, 'rejected')
|
this._setTxStatus(txId, 'rejected')
|
||||||
}
|
}
|
||||||
|
|
||||||
// should update the status of the tx to 'unapproved'.
|
/**
|
||||||
|
should update the status of the tx to 'unapproved'.
|
||||||
|
@param txId {number} - the txMeta Id
|
||||||
|
*/
|
||||||
setTxStatusUnapproved (txId) {
|
setTxStatusUnapproved (txId) {
|
||||||
this._setTxStatus(txId, 'unapproved')
|
this._setTxStatus(txId, 'unapproved')
|
||||||
}
|
}
|
||||||
// should update the status of the tx to 'approved'.
|
/**
|
||||||
|
should update the status of the tx to 'approved'.
|
||||||
|
@param txId {number} - the txMeta Id
|
||||||
|
*/
|
||||||
setTxStatusApproved (txId) {
|
setTxStatusApproved (txId) {
|
||||||
this._setTxStatus(txId, 'approved')
|
this._setTxStatus(txId, 'approved')
|
||||||
}
|
}
|
||||||
|
|
||||||
// should update the status of the tx to 'signed'.
|
/**
|
||||||
|
should update the status of the tx to 'signed'.
|
||||||
|
@param txId {number} - the txMeta Id
|
||||||
|
*/
|
||||||
setTxStatusSigned (txId) {
|
setTxStatusSigned (txId) {
|
||||||
this._setTxStatus(txId, 'signed')
|
this._setTxStatus(txId, 'signed')
|
||||||
}
|
}
|
||||||
|
|
||||||
// should update the status of the tx to 'submitted'.
|
/**
|
||||||
// and add a time stamp for when it was called
|
should update the status of the tx to 'submitted'.
|
||||||
|
and add a time stamp for when it was called
|
||||||
|
@param txId {number} - the txMeta Id
|
||||||
|
*/
|
||||||
setTxStatusSubmitted (txId) {
|
setTxStatusSubmitted (txId) {
|
||||||
const txMeta = this.getTx(txId)
|
const txMeta = this.getTx(txId)
|
||||||
txMeta.submittedTime = (new Date()).getTime()
|
txMeta.submittedTime = (new Date()).getTime()
|
||||||
@ -237,17 +324,29 @@ module.exports = class TransactionStateManager extends EventEmitter {
|
|||||||
this._setTxStatus(txId, 'submitted')
|
this._setTxStatus(txId, 'submitted')
|
||||||
}
|
}
|
||||||
|
|
||||||
// should update the status of the tx to 'confirmed'.
|
/**
|
||||||
|
should update the status of the tx to 'confirmed'.
|
||||||
|
@param txId {number} - the txMeta Id
|
||||||
|
*/
|
||||||
setTxStatusConfirmed (txId) {
|
setTxStatusConfirmed (txId) {
|
||||||
this._setTxStatus(txId, 'confirmed')
|
this._setTxStatus(txId, 'confirmed')
|
||||||
}
|
}
|
||||||
|
|
||||||
// should update the status dropped
|
/**
|
||||||
|
should update the status of the tx to 'dropped'.
|
||||||
|
@param txId {number} - the txMeta Id
|
||||||
|
*/
|
||||||
setTxStatusDropped (txId) {
|
setTxStatusDropped (txId) {
|
||||||
this._setTxStatus(txId, 'dropped')
|
this._setTxStatus(txId, 'dropped')
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
/**
|
||||||
|
should update the status of the tx to 'failed'.
|
||||||
|
and put the error on the txMeta
|
||||||
|
@param txId {number} - the txMeta Id
|
||||||
|
@param err {erroObject} - error object
|
||||||
|
*/
|
||||||
setTxStatusFailed (txId, err) {
|
setTxStatusFailed (txId, err) {
|
||||||
const txMeta = this.getTx(txId)
|
const txMeta = this.getTx(txId)
|
||||||
txMeta.err = {
|
txMeta.err = {
|
||||||
@ -258,6 +357,11 @@ module.exports = class TransactionStateManager extends EventEmitter {
|
|||||||
this._setTxStatus(txId, 'failed')
|
this._setTxStatus(txId, 'failed')
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
Removes transaction from the given address for the current network
|
||||||
|
from the txList
|
||||||
|
@param address {string} - hex string of the from address on the txParams to remove
|
||||||
|
*/
|
||||||
wipeTransactions (address) {
|
wipeTransactions (address) {
|
||||||
// network only tx
|
// network only tx
|
||||||
const txs = this.getFullTxList()
|
const txs = this.getFullTxList()
|
||||||
@ -273,9 +377,8 @@ module.exports = class TransactionStateManager extends EventEmitter {
|
|||||||
// PRIVATE METHODS
|
// PRIVATE METHODS
|
||||||
//
|
//
|
||||||
|
|
||||||
// Should find the tx in the tx list and
|
// STATUS METHODS
|
||||||
// update it.
|
// statuses:
|
||||||
// should set the status in txData
|
|
||||||
// - `'unapproved'` the user has not responded
|
// - `'unapproved'` the user has not responded
|
||||||
// - `'rejected'` the user has responded no!
|
// - `'rejected'` the user has responded no!
|
||||||
// - `'approved'` the user has approved the tx
|
// - `'approved'` the user has approved the tx
|
||||||
@ -283,6 +386,15 @@ module.exports = class TransactionStateManager extends EventEmitter {
|
|||||||
// - `'submitted'` the tx is sent to a server
|
// - `'submitted'` the tx is sent to a server
|
||||||
// - `'confirmed'` the tx has been included in a block.
|
// - `'confirmed'` the tx has been included in a block.
|
||||||
// - `'failed'` the tx failed for some reason, included on tx data.
|
// - `'failed'` the tx failed for some reason, included on tx data.
|
||||||
|
// - `'dropped'` the tx nonce was already used
|
||||||
|
|
||||||
|
/**
|
||||||
|
@param txId {number} - the txMeta Id
|
||||||
|
@param status {string} - the status to set on the txMeta
|
||||||
|
@emits tx:status-update - passes txId and status
|
||||||
|
@emits ${txMeta.id}:finished - if it is a finished state. Passes the txMeta
|
||||||
|
@emits update:badge
|
||||||
|
*/
|
||||||
_setTxStatus (txId, status) {
|
_setTxStatus (txId, status) {
|
||||||
const txMeta = this.getTx(txId)
|
const txMeta = this.getTx(txId)
|
||||||
txMeta.status = status
|
txMeta.status = status
|
||||||
@ -295,9 +407,14 @@ module.exports = class TransactionStateManager extends EventEmitter {
|
|||||||
this.emit('update:badge')
|
this.emit('update:badge')
|
||||||
}
|
}
|
||||||
|
|
||||||
// Saves the new/updated txList.
|
/**
|
||||||
|
Saves the new/updated txList.
|
||||||
|
@param transactions {array} - the list of transactions to save
|
||||||
|
*/
|
||||||
// Function is intended only for internal use
|
// Function is intended only for internal use
|
||||||
_saveTxList (transactions) {
|
_saveTxList (transactions) {
|
||||||
this.store.updateState({ transactions })
|
this.store.updateState({ transactions })
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
module.exports = TransactionStateManager
|
@ -101,6 +101,7 @@ ConfigManager.prototype.setShowSeedWords = function (should) {
|
|||||||
this.setData(data)
|
this.setData(data)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
ConfigManager.prototype.getShouldShowSeedWords = function () {
|
ConfigManager.prototype.getShouldShowSeedWords = function () {
|
||||||
var data = this.getData()
|
var data = this.getData()
|
||||||
return data.showSeedWords
|
return data.showSeedWords
|
||||||
@ -116,27 +117,6 @@ ConfigManager.prototype.getSeedWords = function () {
|
|||||||
var data = this.getData()
|
var data = this.getData()
|
||||||
return data.seedWords
|
return data.seedWords
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* Called to set the isRevealingSeedWords flag. This happens only when the user chooses to reveal
|
|
||||||
* the seed words and not during the first time flow.
|
|
||||||
* @param {boolean} reveal - Value to set the isRevealingSeedWords flag.
|
|
||||||
*/
|
|
||||||
ConfigManager.prototype.setIsRevealingSeedWords = function (reveal = false) {
|
|
||||||
const data = this.getData()
|
|
||||||
data.isRevealingSeedWords = reveal
|
|
||||||
this.setData(data)
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Returns the isRevealingSeedWords flag.
|
|
||||||
* @returns {boolean|undefined}
|
|
||||||
*/
|
|
||||||
ConfigManager.prototype.getIsRevealingSeedWords = function () {
|
|
||||||
const data = this.getData()
|
|
||||||
return data.isRevealingSeedWords
|
|
||||||
}
|
|
||||||
|
|
||||||
ConfigManager.prototype.setRpcTarget = function (rpcUrl) {
|
ConfigManager.prototype.setRpcTarget = function (rpcUrl) {
|
||||||
var config = this.getConfig()
|
var config = this.getConfig()
|
||||||
config.provider = {
|
config.provider = {
|
||||||
|
@ -23,22 +23,16 @@ function setupRaven(opts) {
|
|||||||
release,
|
release,
|
||||||
transport: function(opts) {
|
transport: function(opts) {
|
||||||
const report = opts.data
|
const report = opts.data
|
||||||
// simplify certain complex error messages
|
try {
|
||||||
report.exception.values.forEach(item => {
|
// handle error-like non-error exceptions
|
||||||
let errorMessage = item.value
|
nonErrorException(report)
|
||||||
// simplify ethjs error messages
|
// simplify certain complex error messages (e.g. Ethjs)
|
||||||
errorMessage = extractEthjsErrorMessage(errorMessage)
|
simplifyErrorMessages(report)
|
||||||
// simplify 'Transaction Failed: known transaction'
|
// modify report urls
|
||||||
if (errorMessage.indexOf('Transaction Failed: known transaction') === 0) {
|
rewriteReportUrls(report)
|
||||||
// cut the hash from the error message
|
} catch (err) {
|
||||||
errorMessage = 'Transaction Failed: known transaction'
|
console.warn(err)
|
||||||
}
|
}
|
||||||
// finalize
|
|
||||||
item.value = errorMessage
|
|
||||||
})
|
|
||||||
|
|
||||||
// modify report urls
|
|
||||||
rewriteReportUrls(report)
|
|
||||||
// make request normally
|
// make request normally
|
||||||
client._makeRequest(opts)
|
client._makeRequest(opts)
|
||||||
},
|
},
|
||||||
@ -48,15 +42,42 @@ function setupRaven(opts) {
|
|||||||
return Raven
|
return Raven
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function nonErrorException(report) {
|
||||||
|
// handle errors that lost their error-ness in serialization
|
||||||
|
if (report.message.includes('Non-Error exception captured with keys: message')) {
|
||||||
|
if (!(report.extra && report.extra.__serialized__)) return
|
||||||
|
report.message = `Non-Error Exception: ${report.extra.__serialized__.message}`
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function simplifyErrorMessages(report) {
|
||||||
|
if (report.exception && report.exception.values) {
|
||||||
|
report.exception.values.forEach(item => {
|
||||||
|
let errorMessage = item.value
|
||||||
|
// simplify ethjs error messages
|
||||||
|
errorMessage = extractEthjsErrorMessage(errorMessage)
|
||||||
|
// simplify 'Transaction Failed: known transaction'
|
||||||
|
if (errorMessage.indexOf('Transaction Failed: known transaction') === 0) {
|
||||||
|
// cut the hash from the error message
|
||||||
|
errorMessage = 'Transaction Failed: known transaction'
|
||||||
|
}
|
||||||
|
// finalize
|
||||||
|
item.value = errorMessage
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
function rewriteReportUrls(report) {
|
function rewriteReportUrls(report) {
|
||||||
// update request url
|
// update request url
|
||||||
report.request.url = toMetamaskUrl(report.request.url)
|
report.request.url = toMetamaskUrl(report.request.url)
|
||||||
// update exception stack trace
|
// update exception stack trace
|
||||||
report.exception.values.forEach(item => {
|
if (report.exception && report.exception.values) {
|
||||||
item.stacktrace.frames.forEach(frame => {
|
report.exception.values.forEach(item => {
|
||||||
frame.filename = toMetamaskUrl(frame.filename)
|
item.stacktrace.frames.forEach(frame => {
|
||||||
|
frame.filename = toMetamaskUrl(frame.filename)
|
||||||
|
})
|
||||||
})
|
})
|
||||||
})
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function toMetamaskUrl(origUrl) {
|
function toMetamaskUrl(origUrl) {
|
||||||
|
@ -309,7 +309,6 @@ module.exports = class MetamaskController extends EventEmitter {
|
|||||||
lostAccounts: this.configManager.getLostAccounts(),
|
lostAccounts: this.configManager.getLostAccounts(),
|
||||||
seedWords: this.configManager.getSeedWords(),
|
seedWords: this.configManager.getSeedWords(),
|
||||||
forgottenPassword: this.configManager.getPasswordForgotten(),
|
forgottenPassword: this.configManager.getPasswordForgotten(),
|
||||||
isRevealingSeedWords: Boolean(this.configManager.getIsRevealingSeedWords()),
|
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -351,7 +350,6 @@ module.exports = class MetamaskController extends EventEmitter {
|
|||||||
clearSeedWordCache: this.clearSeedWordCache.bind(this),
|
clearSeedWordCache: this.clearSeedWordCache.bind(this),
|
||||||
resetAccount: nodeify(this.resetAccount, this),
|
resetAccount: nodeify(this.resetAccount, this),
|
||||||
importAccountWithStrategy: this.importAccountWithStrategy.bind(this),
|
importAccountWithStrategy: this.importAccountWithStrategy.bind(this),
|
||||||
setIsRevealingSeedWords: this.configManager.setIsRevealingSeedWords.bind(this.configManager),
|
|
||||||
|
|
||||||
// vault management
|
// vault management
|
||||||
submitPassword: nodeify(keyringController.submitPassword, keyringController),
|
submitPassword: nodeify(keyringController.submitPassword, keyringController),
|
||||||
|
@ -7,7 +7,7 @@ This migration updates "transaction state history" to diffs style
|
|||||||
*/
|
*/
|
||||||
|
|
||||||
const clone = require('clone')
|
const clone = require('clone')
|
||||||
const txStateHistoryHelper = require('../lib/tx-state-history-helper')
|
const txStateHistoryHelper = require('../controllers/transactions/lib/tx-state-history-helper')
|
||||||
|
|
||||||
|
|
||||||
module.exports = {
|
module.exports = {
|
||||||
|
49
development/sourcemap-validator.js
Normal file
49
development/sourcemap-validator.js
Normal file
@ -0,0 +1,49 @@
|
|||||||
|
const fs = require('fs')
|
||||||
|
const { SourceMapConsumer } = require('source-map')
|
||||||
|
|
||||||
|
//
|
||||||
|
// Utility to help check if sourcemaps are working
|
||||||
|
//
|
||||||
|
// searches `dist/chrome/inpage.js` for "new Error" statements
|
||||||
|
// and prints their source lines using the sourcemaps.
|
||||||
|
// if not working it may error or print minified garbage
|
||||||
|
//
|
||||||
|
|
||||||
|
start()
|
||||||
|
|
||||||
|
async function start() {
|
||||||
|
const rawBuild = fs.readFileSync(__dirname + '/../dist/chrome/inpage.js', 'utf8')
|
||||||
|
const rawSourceMap = fs.readFileSync(__dirname + '/../dist/sourcemaps/inpage.js.map', 'utf8')
|
||||||
|
const consumer = await new SourceMapConsumer(rawSourceMap)
|
||||||
|
|
||||||
|
console.log('hasContentsOfAllSources:', consumer.hasContentsOfAllSources(), '\n')
|
||||||
|
console.log('sources:')
|
||||||
|
consumer.sources.map((sourcePath) => console.log(sourcePath))
|
||||||
|
|
||||||
|
console.log('\nexamining "new Error" statements:\n')
|
||||||
|
const sourceLines = rawBuild.split('\n')
|
||||||
|
sourceLines.map(line => indicesOf('new Error', line))
|
||||||
|
.forEach((errorIndices, lineIndex) => {
|
||||||
|
// if (errorIndex === null) return console.log('line does not contain "new Error"')
|
||||||
|
errorIndices.forEach((errorIndex) => {
|
||||||
|
const position = { line: lineIndex + 1, column: errorIndex }
|
||||||
|
const result = consumer.originalPositionFor(position)
|
||||||
|
if (!result.source) return console.warn(`!! missing source for position: ${position}`)
|
||||||
|
// filter out deps distributed minified without sourcemaps
|
||||||
|
if (result.source === 'node_modules/browserify/node_modules/browser-pack/_prelude.js') return // minified mess
|
||||||
|
if (result.source === 'node_modules/web3/dist/web3.min.js') return // minified mess
|
||||||
|
const sourceContent = consumer.sourceContentFor(result.source)
|
||||||
|
const sourceLines = sourceContent.split('\n')
|
||||||
|
const line = sourceLines[result.line-1]
|
||||||
|
console.log(`\n========================== ${result.source} ====================================\n`)
|
||||||
|
console.log(line)
|
||||||
|
console.log(`\n==============================================================================\n`)
|
||||||
|
})
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
function indicesOf(substring, string) {
|
||||||
|
var a=[],i=-1;
|
||||||
|
while((i=string.indexOf(substring,i+1)) >= 0) a.push(i);
|
||||||
|
return a;
|
||||||
|
}
|
BIN
docs/transaction-flow.png
Normal file
BIN
docs/transaction-flow.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 138 KiB |
22
gulpfile.js
22
gulpfile.js
@ -247,7 +247,7 @@ gulp.task('dev:scss', createScssBuildTask({
|
|||||||
src: 'ui/app/css/index.scss',
|
src: 'ui/app/css/index.scss',
|
||||||
dest: 'ui/app/css/output',
|
dest: 'ui/app/css/output',
|
||||||
devMode: true,
|
devMode: true,
|
||||||
pattern: 'ui/app/css/**/*.scss',
|
pattern: 'ui/app/**/*.scss',
|
||||||
}))
|
}))
|
||||||
|
|
||||||
function createScssBuildTask({ src, dest, devMode, pattern }) {
|
function createScssBuildTask({ src, dest, devMode, pattern }) {
|
||||||
@ -484,16 +484,6 @@ function generateBundler(opts, performBundle) {
|
|||||||
NODE_ENV: opts.devMode ? 'development' : 'production',
|
NODE_ENV: opts.devMode ? 'development' : 'production',
|
||||||
}))
|
}))
|
||||||
|
|
||||||
// Minification
|
|
||||||
if (opts.minifyBuild) {
|
|
||||||
bundler.transform('uglifyify', {
|
|
||||||
global: true,
|
|
||||||
mangle: {
|
|
||||||
reserved: [ 'MetamaskInpageProvider' ]
|
|
||||||
},
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
if (opts.watch) {
|
if (opts.watch) {
|
||||||
bundler = watchify(bundler)
|
bundler = watchify(bundler)
|
||||||
// on any file update, re-runs the bundler
|
// on any file update, re-runs the bundler
|
||||||
@ -567,6 +557,16 @@ function bundleTask(opts) {
|
|||||||
.pipe(sourcemaps.init({ loadMaps: true }))
|
.pipe(sourcemaps.init({ loadMaps: true }))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Minification
|
||||||
|
if (opts.minifyBuild) {
|
||||||
|
buildStream = buildStream
|
||||||
|
.pipe(uglify({
|
||||||
|
mangle: {
|
||||||
|
reserved: [ 'MetamaskInpageProvider' ]
|
||||||
|
},
|
||||||
|
}))
|
||||||
|
}
|
||||||
|
|
||||||
// Finalize Source Maps (writes .map file)
|
// Finalize Source Maps (writes .map file)
|
||||||
if (opts.buildSourceMaps) {
|
if (opts.buildSourceMaps) {
|
||||||
buildStream = buildStream
|
buildStream = buildStream
|
||||||
|
@ -70,10 +70,14 @@ class ImportAccountScreen extends Component {
|
|||||||
switch (this.state.selectedOption) {
|
switch (this.state.selectedOption) {
|
||||||
case OPTIONS.JSON_FILE:
|
case OPTIONS.JSON_FILE:
|
||||||
return importNewAccount('JSON File', [ jsonFile, password ])
|
return importNewAccount('JSON File', [ jsonFile, password ])
|
||||||
|
// JS runtime requires caught rejections but failures are handled by Redux
|
||||||
|
.catch()
|
||||||
.then(next)
|
.then(next)
|
||||||
case OPTIONS.PRIVATE_KEY:
|
case OPTIONS.PRIVATE_KEY:
|
||||||
default:
|
default:
|
||||||
return importNewAccount('Private Key', [ privateKey ])
|
return importNewAccount('Private Key', [ privateKey ])
|
||||||
|
// JS runtime requires caught rejections but failures are handled by Redux
|
||||||
|
.catch()
|
||||||
.then(next)
|
.then(next)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -8,7 +8,6 @@ import Identicon from '../../../../ui/app/components/identicon'
|
|||||||
import Breadcrumbs from './breadcrumbs'
|
import Breadcrumbs from './breadcrumbs'
|
||||||
import LoadingScreen from './loading-screen'
|
import LoadingScreen from './loading-screen'
|
||||||
import { DEFAULT_ROUTE, INITIALIZE_CONFIRM_SEED_ROUTE } from '../../../../ui/app/routes'
|
import { DEFAULT_ROUTE, INITIALIZE_CONFIRM_SEED_ROUTE } from '../../../../ui/app/routes'
|
||||||
import { confirmSeedWords } from '../../../../ui/app/actions'
|
|
||||||
|
|
||||||
const LockIcon = props => (
|
const LockIcon = props => (
|
||||||
<svg
|
<svg
|
||||||
@ -45,8 +44,6 @@ class BackupPhraseScreen extends Component {
|
|||||||
address: PropTypes.string.isRequired,
|
address: PropTypes.string.isRequired,
|
||||||
seedWords: PropTypes.string,
|
seedWords: PropTypes.string,
|
||||||
history: PropTypes.object,
|
history: PropTypes.object,
|
||||||
isRevealingSeedWords: PropTypes.bool,
|
|
||||||
clearSeedWords: PropTypes.func,
|
|
||||||
};
|
};
|
||||||
|
|
||||||
static defaultProps = {
|
static defaultProps = {
|
||||||
@ -61,14 +58,6 @@ class BackupPhraseScreen extends Component {
|
|||||||
}
|
}
|
||||||
|
|
||||||
componentWillMount () {
|
componentWillMount () {
|
||||||
this.checkSeedWords()
|
|
||||||
}
|
|
||||||
|
|
||||||
componentDidUpdate () {
|
|
||||||
this.checkSeedWords()
|
|
||||||
}
|
|
||||||
|
|
||||||
checkSeedWords () {
|
|
||||||
const { seedWords, history } = this.props
|
const { seedWords, history } = this.props
|
||||||
|
|
||||||
if (!seedWords) {
|
if (!seedWords) {
|
||||||
@ -103,29 +92,9 @@ class BackupPhraseScreen extends Component {
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
renderSubmitButton () {
|
|
||||||
const { isRevealingSeedWords, clearSeedWords, history } = this.props
|
|
||||||
const { isShowingSecret } = this.state
|
|
||||||
|
|
||||||
return isRevealingSeedWords
|
|
||||||
? <button
|
|
||||||
className="first-time-flow__button"
|
|
||||||
onClick={() => clearSeedWords().then(() => history.push(DEFAULT_ROUTE))}
|
|
||||||
disabled={!isShowingSecret}
|
|
||||||
>
|
|
||||||
Done
|
|
||||||
</button>
|
|
||||||
: <button
|
|
||||||
className="first-time-flow__button"
|
|
||||||
onClick={() => isShowingSecret && history.push(INITIALIZE_CONFIRM_SEED_ROUTE)}
|
|
||||||
disabled={!isShowingSecret}
|
|
||||||
>
|
|
||||||
Next
|
|
||||||
</button>
|
|
||||||
}
|
|
||||||
|
|
||||||
renderSecretScreen () {
|
renderSecretScreen () {
|
||||||
const { isRevealingSeedWords } = this.props
|
const { isShowingSecret } = this.state
|
||||||
|
const { history } = this.props
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="backup-phrase__content-wrapper">
|
<div className="backup-phrase__content-wrapper">
|
||||||
@ -152,8 +121,14 @@ class BackupPhraseScreen extends Component {
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div className="backup-phrase__next-button">
|
<div className="backup-phrase__next-button">
|
||||||
{ this.renderSubmitButton() }
|
<button
|
||||||
{ !isRevealingSeedWords && <Breadcrumbs total={3} currentIndex={1} />}
|
className="first-time-flow__button"
|
||||||
|
onClick={() => isShowingSecret && history.push(INITIALIZE_CONFIRM_SEED_ROUTE)}
|
||||||
|
disabled={!isShowingSecret}
|
||||||
|
>
|
||||||
|
Next
|
||||||
|
</button>
|
||||||
|
<Breadcrumbs total={3} currentIndex={1} />
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
)
|
)
|
||||||
@ -175,25 +150,13 @@ class BackupPhraseScreen extends Component {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const mapStateToProps = ({ metamask, appState }) => {
|
|
||||||
const { selectedAddress, seedWords, isRevealingSeedWords } = metamask
|
|
||||||
const { isLoading } = appState
|
|
||||||
|
|
||||||
return {
|
|
||||||
seedWords,
|
|
||||||
isRevealingSeedWords,
|
|
||||||
isLoading,
|
|
||||||
address: selectedAddress,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const mapDispatchToProps = dispatch => {
|
|
||||||
return {
|
|
||||||
clearSeedWords: () => dispatch(confirmSeedWords()),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
export default compose(
|
export default compose(
|
||||||
withRouter,
|
withRouter,
|
||||||
connect(mapStateToProps, mapDispatchToProps),
|
connect(
|
||||||
|
({ metamask: { selectedAddress, seedWords }, appState: { isLoading } }) => ({
|
||||||
|
seedWords,
|
||||||
|
isLoading,
|
||||||
|
address: selectedAddress,
|
||||||
|
})
|
||||||
|
)
|
||||||
)(BackupPhraseScreen)
|
)(BackupPhraseScreen)
|
||||||
|
@ -1,6 +1,6 @@
|
|||||||
MetaMask is beta software.
|
MetaMask is beta software.
|
||||||
|
|
||||||
When you log in to MetaMask, your current account is visible to every new site you visit.
|
When you log in to MetaMask, your current account's address is visible to every new site you visit. This can be used to look up your account balances of Ether and other tokens.
|
||||||
|
|
||||||
For your privacy, for now, please sign out of MetaMask when you're done using a site.
|
For your privacy, for now, please sign out of MetaMask when you're done using a site.
|
||||||
|
|
||||||
|
File diff suppressed because one or more lines are too long
@ -96,6 +96,8 @@ class JsonImportSubview extends Component {
|
|||||||
}
|
}
|
||||||
|
|
||||||
this.props.importNewAccount([ fileContents, password ])
|
this.props.importNewAccount([ fileContents, password ])
|
||||||
|
// JS runtime requires caught rejections but failures are handled by Redux
|
||||||
|
.catch()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -64,4 +64,6 @@ PrivateKeyImportView.prototype.createNewKeychain = function () {
|
|||||||
const input = document.getElementById('private-key-box')
|
const input = document.getElementById('private-key-box')
|
||||||
const privateKey = input.value
|
const privateKey = input.value
|
||||||
this.props.dispatch(actions.importNewAccount('Private Key', [ privateKey ]))
|
this.props.dispatch(actions.importNewAccount('Private Key', [ privateKey ]))
|
||||||
|
// JS runtime requires caught rejections but failures are handled by Redux
|
||||||
|
.catch()
|
||||||
}
|
}
|
||||||
|
@ -16,8 +16,7 @@ const addressSummary = util.addressSummary
|
|||||||
const nameForAddress = require('../../lib/contract-namer')
|
const nameForAddress = require('../../lib/contract-namer')
|
||||||
const BNInput = require('./bn-as-decimal-input')
|
const BNInput = require('./bn-as-decimal-input')
|
||||||
|
|
||||||
// corresponds with 0.1 GWEI
|
const MIN_GAS_PRICE_BN = new BN('0')
|
||||||
const MIN_GAS_PRICE_BN = new BN('100000000')
|
|
||||||
const MIN_GAS_LIMIT_BN = new BN('21000')
|
const MIN_GAS_LIMIT_BN = new BN('21000')
|
||||||
|
|
||||||
module.exports = PendingTx
|
module.exports = PendingTx
|
||||||
|
@ -138,7 +138,7 @@ ShapeshiftForm.prototype.renderMain = function () {
|
|||||||
width: '229px',
|
width: '229px',
|
||||||
height: '82px',
|
height: '82px',
|
||||||
},
|
},
|
||||||
}, this.props.warning)
|
}, this.props.warning + '')
|
||||||
: this.renderInfo(),
|
: this.renderInfo(),
|
||||||
|
|
||||||
this.renderRefundAddressForCoin(coin),
|
this.renderRefundAddressForCoin(coin),
|
||||||
|
3581
package-lock.json
generated
3581
package-lock.json
generated
File diff suppressed because it is too large
Load Diff
19
package.json
19
package.json
@ -78,6 +78,7 @@
|
|||||||
"classnames": "^2.2.5",
|
"classnames": "^2.2.5",
|
||||||
"clone": "^2.1.1",
|
"clone": "^2.1.1",
|
||||||
"copy-to-clipboard": "^3.0.8",
|
"copy-to-clipboard": "^3.0.8",
|
||||||
|
"css-loader": "^0.28.11",
|
||||||
"currency-formatter": "^1.4.2",
|
"currency-formatter": "^1.4.2",
|
||||||
"debounce": "^1.0.0",
|
"debounce": "^1.0.0",
|
||||||
"debounce-stream": "^2.0.0",
|
"debounce-stream": "^2.0.0",
|
||||||
@ -89,10 +90,9 @@
|
|||||||
"ensnare": "^1.0.0",
|
"ensnare": "^1.0.0",
|
||||||
"eslint-plugin-react": "^7.4.0",
|
"eslint-plugin-react": "^7.4.0",
|
||||||
"eth-bin-to-ops": "^1.0.1",
|
"eth-bin-to-ops": "^1.0.1",
|
||||||
"eth-block-tracker": "^2.3.0",
|
|
||||||
"eth-contract-metadata": "^1.1.5",
|
"eth-contract-metadata": "^1.1.5",
|
||||||
"eth-hd-keyring": "^1.2.1",
|
"eth-hd-keyring": "^1.2.1",
|
||||||
"eth-json-rpc-filters": "^1.2.5",
|
"eth-json-rpc-filters": "^1.2.6",
|
||||||
"eth-json-rpc-infura": "^3.0.0",
|
"eth-json-rpc-infura": "^3.0.0",
|
||||||
"eth-keyring-controller": "^2.2.0",
|
"eth-keyring-controller": "^2.2.0",
|
||||||
"eth-phishing-detect": "^1.1.4",
|
"eth-phishing-detect": "^1.1.4",
|
||||||
@ -113,6 +113,7 @@
|
|||||||
"extensionizer": "^1.0.0",
|
"extensionizer": "^1.0.0",
|
||||||
"fast-json-patch": "^2.0.4",
|
"fast-json-patch": "^2.0.4",
|
||||||
"fast-levenshtein": "^2.0.6",
|
"fast-levenshtein": "^2.0.6",
|
||||||
|
"file-loader": "^1.1.11",
|
||||||
"fuse.js": "^3.2.0",
|
"fuse.js": "^3.2.0",
|
||||||
"gulp": "github:gulpjs/gulp#4.0",
|
"gulp": "github:gulpjs/gulp#4.0",
|
||||||
"gulp-autoprefixer": "^5.0.0",
|
"gulp-autoprefixer": "^5.0.0",
|
||||||
@ -187,7 +188,7 @@
|
|||||||
"valid-url": "^1.0.9",
|
"valid-url": "^1.0.9",
|
||||||
"vreme": "^3.0.2",
|
"vreme": "^3.0.2",
|
||||||
"web3": "^0.20.1",
|
"web3": "^0.20.1",
|
||||||
"web3-provider-engine": "^13.8.0",
|
"web3-provider-engine": "^14.0.5",
|
||||||
"web3-stream-provider": "^3.0.1",
|
"web3-stream-provider": "^3.0.1",
|
||||||
"xtend": "^4.0.1"
|
"xtend": "^4.0.1"
|
||||||
},
|
},
|
||||||
@ -206,7 +207,7 @@
|
|||||||
"brfs": "^1.4.3",
|
"brfs": "^1.4.3",
|
||||||
"browserify": "^16.1.1",
|
"browserify": "^16.1.1",
|
||||||
"chai": "^4.1.0",
|
"chai": "^4.1.0",
|
||||||
"chromedriver": "^2.34.1",
|
"chromedriver": "2.36.0",
|
||||||
"compression": "^1.7.1",
|
"compression": "^1.7.1",
|
||||||
"coveralls": "^3.0.0",
|
"coveralls": "^3.0.0",
|
||||||
"cross-env": "^5.1.4",
|
"cross-env": "^5.1.4",
|
||||||
@ -219,9 +220,10 @@
|
|||||||
"eslint-plugin-json": "^1.2.0",
|
"eslint-plugin-json": "^1.2.0",
|
||||||
"eslint-plugin-mocha": "^5.0.0",
|
"eslint-plugin-mocha": "^5.0.0",
|
||||||
"eslint-plugin-react": "^7.4.0",
|
"eslint-plugin-react": "^7.4.0",
|
||||||
"eth-json-rpc-middleware": "^1.2.7",
|
"eth-json-rpc-middleware": "^1.6.0",
|
||||||
"fs-promise": "^2.0.3",
|
"fs-promise": "^2.0.3",
|
||||||
"ganache-cli": "^6.1.0",
|
"ganache-cli": "^6.1.0",
|
||||||
|
"ganache-core": "^2.1.0",
|
||||||
"geckodriver": "^1.11.0",
|
"geckodriver": "^1.11.0",
|
||||||
"gifencoder": "^1.1.0",
|
"gifencoder": "^1.1.0",
|
||||||
"gulp": "github:gulpjs/gulp#6d71a658c61edb3090221579d8f97dbe086ba2ed",
|
"gulp": "github:gulpjs/gulp#6d71a658c61edb3090221579d8f97dbe086ba2ed",
|
||||||
@ -257,8 +259,10 @@
|
|||||||
"mocha-sinon": "^2.0.0",
|
"mocha-sinon": "^2.0.0",
|
||||||
"nock": "^9.0.14",
|
"nock": "^9.0.14",
|
||||||
"node-sass": "^4.7.2",
|
"node-sass": "^4.7.2",
|
||||||
|
"nsp": "^3.2.1",
|
||||||
"nyc": "^11.0.3",
|
"nyc": "^11.0.3",
|
||||||
"open": "0.0.5",
|
"open": "0.0.5",
|
||||||
|
"path": "^0.12.7",
|
||||||
"png-file-stream": "^1.0.0",
|
"png-file-stream": "^1.0.0",
|
||||||
"prompt": "^1.0.0",
|
"prompt": "^1.0.0",
|
||||||
"qs": "^6.2.0",
|
"qs": "^6.2.0",
|
||||||
@ -268,14 +272,17 @@
|
|||||||
"react-test-renderer": "^15.6.2",
|
"react-test-renderer": "^15.6.2",
|
||||||
"react-testutils-additions": "^15.2.0",
|
"react-testutils-additions": "^15.2.0",
|
||||||
"redux-test-utils": "^0.2.2",
|
"redux-test-utils": "^0.2.2",
|
||||||
|
"resolve-url-loader": "^2.3.0",
|
||||||
"rimraf": "^2.6.2",
|
"rimraf": "^2.6.2",
|
||||||
|
"sass-loader": "^7.0.1",
|
||||||
"selenium-webdriver": "^3.5.0",
|
"selenium-webdriver": "^3.5.0",
|
||||||
"shell-parallel": "^1.0.3",
|
"shell-parallel": "^1.0.3",
|
||||||
"sinon": "^5.0.0",
|
"sinon": "^5.0.0",
|
||||||
|
"source-map": "^0.7.2",
|
||||||
|
"style-loader": "^0.21.0",
|
||||||
"stylelint-config-standard": "^18.2.0",
|
"stylelint-config-standard": "^18.2.0",
|
||||||
"tape": "^4.5.1",
|
"tape": "^4.5.1",
|
||||||
"testem": "^2.0.0",
|
"testem": "^2.0.0",
|
||||||
"uglifyify": "^4.0.5",
|
|
||||||
"vinyl-buffer": "^1.0.1",
|
"vinyl-buffer": "^1.0.1",
|
||||||
"vinyl-source-stream": "^2.0.0",
|
"vinyl-source-stream": "^2.0.0",
|
||||||
"watchify": "^3.9.0"
|
"watchify": "^3.9.0"
|
||||||
|
@ -26,9 +26,9 @@ describe('Metamask popup page', function () {
|
|||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
// after(async function () {
|
after(async function () {
|
||||||
// await driver.quit()
|
await driver.quit()
|
||||||
// })
|
})
|
||||||
|
|
||||||
describe('Setup', function () {
|
describe('Setup', function () {
|
||||||
|
|
||||||
|
@ -234,6 +234,7 @@ describe('', function () {
|
|||||||
await delay(1000)
|
await delay(1000)
|
||||||
})
|
})
|
||||||
|
|
||||||
|
// There is an issue with blank confirmation window, but the button is still there and the driver is able to clicked (?.?)
|
||||||
it('confirms transaction in MetaMask popup', async function () {
|
it('confirms transaction in MetaMask popup', async function () {
|
||||||
const windowHandles = await driver.getAllWindowHandles()
|
const windowHandles = await driver.getAllWindowHandles()
|
||||||
await driver.switchTo().window(windowHandles[2])
|
await driver.switchTo().window(windowHandles[2])
|
||||||
|
@ -23,6 +23,37 @@ global.ethQuery = {
|
|||||||
|
|
||||||
global.ethereumProvider = {}
|
global.ethereumProvider = {}
|
||||||
|
|
||||||
|
async function customizeGas (assert, price, limit, ethFee, usdFee) {
|
||||||
|
const sendGasOpenCustomizeModalButton = await queryAsync($, '.sliders-icon-container')
|
||||||
|
sendGasOpenCustomizeModalButton[0].click()
|
||||||
|
|
||||||
|
const customizeGasModal = await queryAsync($, '.send-v2__customize-gas')
|
||||||
|
assert.ok(customizeGasModal[0], 'should render the customize gas modal')
|
||||||
|
|
||||||
|
const customizeGasPriceInput = (await queryAsync($, '.send-v2__gas-modal-card')).first().find('input')
|
||||||
|
customizeGasPriceInput.val(price)
|
||||||
|
reactTriggerChange(customizeGasPriceInput[0])
|
||||||
|
const customizeGasLimitInput = (await queryAsync($, '.send-v2__gas-modal-card')).last().find('input')
|
||||||
|
customizeGasLimitInput.val(limit)
|
||||||
|
reactTriggerChange(customizeGasLimitInput[0])
|
||||||
|
|
||||||
|
const customizeGasSaveButton = await queryAsync($, '.send-v2__customize-gas__save')
|
||||||
|
customizeGasSaveButton[0].click()
|
||||||
|
const sendGasField = await queryAsync($, '.send-v2__gas-fee-display')
|
||||||
|
|
||||||
|
assert.equal(
|
||||||
|
(await findAsync(sendGasField, '.currency-display__input-wrapper > input')).val(),
|
||||||
|
ethFee,
|
||||||
|
'send gas field should show customized gas total'
|
||||||
|
)
|
||||||
|
|
||||||
|
assert.equal(
|
||||||
|
(await findAsync(sendGasField, '.currency-display__converted-value'))[0].textContent,
|
||||||
|
usdFee,
|
||||||
|
'send gas field should show customized gas total converted to USD'
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
async function runSendFlowTest(assert, done) {
|
async function runSendFlowTest(assert, done) {
|
||||||
console.log('*** start runSendFlowTest')
|
console.log('*** start runSendFlowTest')
|
||||||
const selectState = await queryAsync($, 'select')
|
const selectState = await queryAsync($, 'select')
|
||||||
@ -95,32 +126,8 @@ async function runSendFlowTest(assert, done) {
|
|||||||
'send gas field should show estimated gas total converted to USD'
|
'send gas field should show estimated gas total converted to USD'
|
||||||
)
|
)
|
||||||
|
|
||||||
const sendGasOpenCustomizeModalButton = await queryAsync($, '.sliders-icon-container')
|
await customizeGas(assert, 0, 21000, '0', '$0.00 USD')
|
||||||
sendGasOpenCustomizeModalButton[0].click()
|
await customizeGas(assert, 500, 60000, '0.003', '$3.60 USD')
|
||||||
|
|
||||||
const customizeGasModal = await queryAsync($, '.send-v2__customize-gas')
|
|
||||||
assert.ok(customizeGasModal[0], 'should render the customize gas modal')
|
|
||||||
|
|
||||||
const customizeGasPriceInput = (await queryAsync($, '.send-v2__gas-modal-card')).first().find('input')
|
|
||||||
customizeGasPriceInput.val(50)
|
|
||||||
reactTriggerChange(customizeGasPriceInput[0])
|
|
||||||
const customizeGasLimitInput = (await queryAsync($, '.send-v2__gas-modal-card')).last().find('input')
|
|
||||||
customizeGasLimitInput.val(60000)
|
|
||||||
reactTriggerChange(customizeGasLimitInput[0])
|
|
||||||
|
|
||||||
const customizeGasSaveButton = await queryAsync($, '.send-v2__customize-gas__save')
|
|
||||||
customizeGasSaveButton[0].click()
|
|
||||||
|
|
||||||
assert.equal(
|
|
||||||
(await findAsync(sendGasField, '.currency-display__input-wrapper > input')).val(),
|
|
||||||
'0.003',
|
|
||||||
'send gas field should show customized gas total'
|
|
||||||
)
|
|
||||||
assert.equal(
|
|
||||||
(await findAsync(sendGasField, '.currency-display__converted-value'))[0].textContent,
|
|
||||||
'$3.60 USD',
|
|
||||||
'send gas field should show customized gas total converted to USD'
|
|
||||||
)
|
|
||||||
|
|
||||||
const sendButton = await queryAsync($, 'button.btn-primary--lg.page-container__footer-button')
|
const sendButton = await queryAsync($, 'button.btn-primary--lg.page-container__footer-button')
|
||||||
assert.equal(sendButton[0].textContent, 'Next', 'next button rendered')
|
assert.equal(sendButton[0].textContent, 'Next', 'next button rendered')
|
||||||
|
@ -1,14 +1,28 @@
|
|||||||
const JsonRpcEngine = require('json-rpc-engine')
|
const JsonRpcEngine = require('json-rpc-engine')
|
||||||
const scaffoldMiddleware = require('eth-json-rpc-middleware/scaffold')
|
const scaffoldMiddleware = require('eth-json-rpc-middleware/scaffold')
|
||||||
const TestBlockchain = require('eth-block-tracker/test/util/testBlockMiddleware')
|
const providerAsMiddleware = require('eth-json-rpc-middleware/providerAsMiddleware')
|
||||||
|
const GanacheCore = require('ganache-core')
|
||||||
|
|
||||||
module.exports = {
|
module.exports = {
|
||||||
createEngineForTestData,
|
createEngineForTestData,
|
||||||
providerFromEngine,
|
providerFromEngine,
|
||||||
scaffoldMiddleware,
|
scaffoldMiddleware,
|
||||||
createTestProviderTools,
|
createTestProviderTools,
|
||||||
|
getTestSeed,
|
||||||
|
getTestAccounts,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function getTestSeed () {
|
||||||
|
return 'people carpet cluster attract ankle motor ozone mass dove original primary mask'
|
||||||
|
}
|
||||||
|
|
||||||
|
function getTestAccounts () {
|
||||||
|
return [
|
||||||
|
{ address: '0x88bb7F89eB5e5b30D3e15a57C68DBe03C6aCCB21', key: Buffer.from('254A8D551474F35CCC816388B4ED4D20B945C96B7EB857A68064CB9E9FB2C092', 'hex') },
|
||||||
|
{ address: '0x1fe9aAB565Be19629fF4e8541ca2102fb42D7724', key: Buffer.from('6BAB5A4F2A6911AF8EE2BD32C6C05F6643AC48EF6C939CDEAAAE6B1620805A9B', 'hex') },
|
||||||
|
{ address: '0xbda5c89aa6bA1b352194291AD6822C92AbC87c7B', key: Buffer.from('9B11D7F833648F26CE94D544855558D7053ECD396E4F4563968C232C012879B0', 'hex') },
|
||||||
|
]
|
||||||
|
}
|
||||||
|
|
||||||
function createEngineForTestData () {
|
function createEngineForTestData () {
|
||||||
return new JsonRpcEngine()
|
return new JsonRpcEngine()
|
||||||
@ -21,11 +35,13 @@ function providerFromEngine (engine) {
|
|||||||
|
|
||||||
function createTestProviderTools (opts = {}) {
|
function createTestProviderTools (opts = {}) {
|
||||||
const engine = createEngineForTestData()
|
const engine = createEngineForTestData()
|
||||||
const testBlockchain = new TestBlockchain()
|
|
||||||
// handle provided hooks
|
// handle provided hooks
|
||||||
engine.push(scaffoldMiddleware(opts.scaffold || {}))
|
engine.push(scaffoldMiddleware(opts.scaffold || {}))
|
||||||
// handle block tracker methods
|
// handle block tracker methods
|
||||||
engine.push(testBlockchain.createMiddleware())
|
engine.push(providerAsMiddleware(GanacheCore.provider({
|
||||||
|
mnemonic: getTestSeed(),
|
||||||
|
})))
|
||||||
|
// wrap in standard provider interface
|
||||||
const provider = providerFromEngine(engine)
|
const provider = providerFromEngine(engine)
|
||||||
return { provider, engine, testBlockchain }
|
return { provider, engine }
|
||||||
}
|
}
|
||||||
|
@ -6,6 +6,12 @@ const MetaMaskController = require('../../app/scripts/metamask-controller')
|
|||||||
const blacklistJSON = require('../stub/blacklist')
|
const blacklistJSON = require('../stub/blacklist')
|
||||||
const firstTimeState = require('../../app/scripts/first-time-state')
|
const firstTimeState = require('../../app/scripts/first-time-state')
|
||||||
|
|
||||||
|
const DEFAULT_LABEL = 'Account 1'
|
||||||
|
const TEST_SEED = 'debris dizzy just program just float decrease vacant alarm reduce speak stadium'
|
||||||
|
const TEST_ADDRESS = '0x0dcd5d886577d5081b0c52e242ef29e70be3e7bc'
|
||||||
|
const TEST_SEED_ALT = 'setup olympic issue mobile velvet surge alcohol burger horse view reopen gentle'
|
||||||
|
const TEST_ADDRESS_ALT = '0xc42edfcc21ed14dda456aa0756c153f7985d8813'
|
||||||
|
|
||||||
describe('MetaMaskController', function () {
|
describe('MetaMaskController', function () {
|
||||||
let metamaskController
|
let metamaskController
|
||||||
const sandbox = sinon.sandbox.create()
|
const sandbox = sinon.sandbox.create()
|
||||||
@ -87,17 +93,28 @@ describe('MetaMaskController', function () {
|
|||||||
|
|
||||||
describe('#createNewVaultAndRestore', function () {
|
describe('#createNewVaultAndRestore', function () {
|
||||||
it('should be able to call newVaultAndRestore despite a mistake.', async function () {
|
it('should be able to call newVaultAndRestore despite a mistake.', async function () {
|
||||||
|
|
||||||
const password = 'what-what-what'
|
const password = 'what-what-what'
|
||||||
const wrongSeed = 'debris dizzy just program just float decrease vacant alarm reduce speak stadiu'
|
await metamaskController.createNewVaultAndRestore(password, TEST_SEED.slice(0, -1)).catch((e) => null)
|
||||||
const rightSeed = 'debris dizzy just program just float decrease vacant alarm reduce speak stadium'
|
await metamaskController.createNewVaultAndRestore(password, TEST_SEED)
|
||||||
await metamaskController.createNewVaultAndRestore(password, wrongSeed)
|
|
||||||
.catch((e) => {
|
|
||||||
return
|
|
||||||
})
|
|
||||||
await metamaskController.createNewVaultAndRestore(password, rightSeed)
|
|
||||||
|
|
||||||
assert(metamaskController.keyringController.createNewVaultAndRestore.calledTwice)
|
assert(metamaskController.keyringController.createNewVaultAndRestore.calledTwice)
|
||||||
})
|
})
|
||||||
|
|
||||||
|
it('should clear previous identities after vault restoration', async () => {
|
||||||
|
await metamaskController.createNewVaultAndRestore('foobar1337', TEST_SEED)
|
||||||
|
assert.deepEqual(metamaskController.getState().identities, {
|
||||||
|
[TEST_ADDRESS]: { address: TEST_ADDRESS, name: DEFAULT_LABEL },
|
||||||
|
})
|
||||||
|
|
||||||
|
await metamaskController.keyringController.saveAccountLabel(TEST_ADDRESS, 'Account Foo')
|
||||||
|
assert.deepEqual(metamaskController.getState().identities, {
|
||||||
|
[TEST_ADDRESS]: { address: TEST_ADDRESS, name: 'Account Foo' },
|
||||||
|
})
|
||||||
|
|
||||||
|
await metamaskController.createNewVaultAndRestore('foobar1337', TEST_SEED_ALT)
|
||||||
|
assert.deepEqual(metamaskController.getState().identities, {
|
||||||
|
[TEST_ADDRESS_ALT]: { address: TEST_ADDRESS_ALT, name: DEFAULT_LABEL },
|
||||||
|
})
|
||||||
|
})
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
@ -1,5 +1,5 @@
|
|||||||
const assert = require('assert')
|
const assert = require('assert')
|
||||||
const NonceTracker = require('../../app/scripts/lib/nonce-tracker')
|
const NonceTracker = require('../../app/scripts/controllers/transactions/nonce-tracker')
|
||||||
const MockTxGen = require('../lib/mock-tx-gen')
|
const MockTxGen = require('../lib/mock-tx-gen')
|
||||||
let providerResultStub = {}
|
let providerResultStub = {}
|
||||||
|
|
||||||
|
@ -4,7 +4,7 @@ const EthTx = require('ethereumjs-tx')
|
|||||||
const ObservableStore = require('obs-store')
|
const ObservableStore = require('obs-store')
|
||||||
const clone = require('clone')
|
const clone = require('clone')
|
||||||
const { createTestProviderTools } = require('../stub/provider')
|
const { createTestProviderTools } = require('../stub/provider')
|
||||||
const PendingTransactionTracker = require('../../app/scripts/lib/pending-tx-tracker')
|
const PendingTransactionTracker = require('../../app/scripts/controllers/transactions/pending-tx-tracker')
|
||||||
const MockTxGen = require('../lib/mock-tx-gen')
|
const MockTxGen = require('../lib/mock-tx-gen')
|
||||||
const sinon = require('sinon')
|
const sinon = require('sinon')
|
||||||
const noop = () => true
|
const noop = () => true
|
||||||
|
@ -5,17 +5,16 @@ const EthjsQuery = require('ethjs-query')
|
|||||||
const ObservableStore = require('obs-store')
|
const ObservableStore = require('obs-store')
|
||||||
const sinon = require('sinon')
|
const sinon = require('sinon')
|
||||||
const TransactionController = require('../../app/scripts/controllers/transactions')
|
const TransactionController = require('../../app/scripts/controllers/transactions')
|
||||||
const TxGasUtils = require('../../app/scripts/lib/tx-gas-utils')
|
const TxGasUtils = require('../../app/scripts/controllers/transactions/tx-gas-utils')
|
||||||
const { createTestProviderTools } = require('../stub/provider')
|
const { createTestProviderTools, getTestAccounts } = require('../stub/provider')
|
||||||
|
|
||||||
const noop = () => true
|
const noop = () => true
|
||||||
const currentNetworkId = 42
|
const currentNetworkId = 42
|
||||||
const otherNetworkId = 36
|
const otherNetworkId = 36
|
||||||
const privKey = new Buffer('8718b9618a37d1fc78c436511fc6df3c8258d3250635bba617f33003270ec03e', 'hex')
|
|
||||||
|
|
||||||
|
|
||||||
describe('Transaction Controller', function () {
|
describe('Transaction Controller', function () {
|
||||||
let txController, provider, providerResultStub, testBlockchain
|
let txController, provider, providerResultStub, query, fromAccount
|
||||||
|
|
||||||
beforeEach(function () {
|
beforeEach(function () {
|
||||||
providerResultStub = {
|
providerResultStub = {
|
||||||
@ -24,9 +23,9 @@ describe('Transaction Controller', function () {
|
|||||||
// by default, all accounts are external accounts (not contracts)
|
// by default, all accounts are external accounts (not contracts)
|
||||||
eth_getCode: '0x',
|
eth_getCode: '0x',
|
||||||
}
|
}
|
||||||
const providerTools = createTestProviderTools({ scaffold: providerResultStub })
|
provider = createTestProviderTools({ scaffold: providerResultStub }).provider
|
||||||
provider = providerTools.provider
|
query = new EthjsQuery(provider)
|
||||||
testBlockchain = providerTools.testBlockchain
|
fromAccount = getTestAccounts()[0]
|
||||||
|
|
||||||
txController = new TransactionController({
|
txController = new TransactionController({
|
||||||
provider,
|
provider,
|
||||||
@ -34,7 +33,7 @@ describe('Transaction Controller', function () {
|
|||||||
txHistoryLimit: 10,
|
txHistoryLimit: 10,
|
||||||
blockTracker: { getCurrentBlock: noop, on: noop, once: noop },
|
blockTracker: { getCurrentBlock: noop, on: noop, once: noop },
|
||||||
signTransaction: (ethTx) => new Promise((resolve) => {
|
signTransaction: (ethTx) => new Promise((resolve) => {
|
||||||
ethTx.sign(privKey)
|
ethTx.sign(fromAccount.key)
|
||||||
resolve()
|
resolve()
|
||||||
}),
|
}),
|
||||||
})
|
})
|
||||||
@ -188,7 +187,7 @@ describe('Transaction Controller', function () {
|
|||||||
|
|
||||||
})
|
})
|
||||||
|
|
||||||
describe('#addTxDefaults', function () {
|
describe('#addTxGasDefaults', function () {
|
||||||
it('should add the tx defaults if their are none', function (done) {
|
it('should add the tx defaults if their are none', function (done) {
|
||||||
const txMeta = {
|
const txMeta = {
|
||||||
'txParams': {
|
'txParams': {
|
||||||
@ -199,7 +198,7 @@ describe('Transaction Controller', function () {
|
|||||||
providerResultStub.eth_gasPrice = '4a817c800'
|
providerResultStub.eth_gasPrice = '4a817c800'
|
||||||
providerResultStub.eth_getBlockByNumber = { gasLimit: '47b784' }
|
providerResultStub.eth_getBlockByNumber = { gasLimit: '47b784' }
|
||||||
providerResultStub.eth_estimateGas = '5209'
|
providerResultStub.eth_estimateGas = '5209'
|
||||||
txController.addTxDefaults(txMeta)
|
txController.addTxGasDefaults(txMeta)
|
||||||
.then((txMetaWithDefaults) => {
|
.then((txMetaWithDefaults) => {
|
||||||
assert(txMetaWithDefaults.txParams.value, '0x0', 'should have added 0x0 as the value')
|
assert(txMetaWithDefaults.txParams.value, '0x0', 'should have added 0x0 as the value')
|
||||||
assert(txMetaWithDefaults.txParams.gasPrice, 'should have added the gas price')
|
assert(txMetaWithDefaults.txParams.gasPrice, 'should have added the gas price')
|
||||||
@ -210,99 +209,6 @@ describe('Transaction Controller', function () {
|
|||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
describe('#_validateTxParams', function () {
|
|
||||||
it('does not throw for positive values', function () {
|
|
||||||
var sample = {
|
|
||||||
from: '0x1678a085c290ebd122dc42cba69373b5953b831d',
|
|
||||||
value: '0x01',
|
|
||||||
}
|
|
||||||
txController._validateTxParams(sample)
|
|
||||||
})
|
|
||||||
|
|
||||||
it('returns error for negative values', function () {
|
|
||||||
var sample = {
|
|
||||||
from: '0x1678a085c290ebd122dc42cba69373b5953b831d',
|
|
||||||
value: '-0x01',
|
|
||||||
}
|
|
||||||
try {
|
|
||||||
txController._validateTxParams(sample)
|
|
||||||
} catch (err) {
|
|
||||||
assert.ok(err, 'error')
|
|
||||||
}
|
|
||||||
})
|
|
||||||
})
|
|
||||||
|
|
||||||
describe('#_normalizeTxParams', () => {
|
|
||||||
it('should normalize txParams', () => {
|
|
||||||
let txParams = {
|
|
||||||
chainId: '0x1',
|
|
||||||
from: 'a7df1beDBF813f57096dF77FCd515f0B3900e402',
|
|
||||||
to: null,
|
|
||||||
data: '68656c6c6f20776f726c64',
|
|
||||||
random: 'hello world',
|
|
||||||
}
|
|
||||||
|
|
||||||
let normalizedTxParams = txController._normalizeTxParams(txParams)
|
|
||||||
|
|
||||||
assert(!normalizedTxParams.chainId, 'their should be no chainId')
|
|
||||||
assert(!normalizedTxParams.to, 'their should be no to address if null')
|
|
||||||
assert.equal(normalizedTxParams.from.slice(0, 2), '0x', 'from should be hexPrefixd')
|
|
||||||
assert.equal(normalizedTxParams.data.slice(0, 2), '0x', 'data should be hexPrefixd')
|
|
||||||
assert(!('random' in normalizedTxParams), 'their should be no random key in normalizedTxParams')
|
|
||||||
|
|
||||||
txParams.to = 'a7df1beDBF813f57096dF77FCd515f0B3900e402'
|
|
||||||
normalizedTxParams = txController._normalizeTxParams(txParams)
|
|
||||||
assert.equal(normalizedTxParams.to.slice(0, 2), '0x', 'to should be hexPrefixd')
|
|
||||||
|
|
||||||
})
|
|
||||||
})
|
|
||||||
|
|
||||||
describe('#_validateRecipient', () => {
|
|
||||||
it('removes recipient for txParams with 0x when contract data is provided', function () {
|
|
||||||
const zeroRecipientandDataTxParams = {
|
|
||||||
from: '0x1678a085c290ebd122dc42cba69373b5953b831d',
|
|
||||||
to: '0x',
|
|
||||||
data: 'bytecode',
|
|
||||||
}
|
|
||||||
const sanitizedTxParams = txController._validateRecipient(zeroRecipientandDataTxParams)
|
|
||||||
assert.deepEqual(sanitizedTxParams, { from: '0x1678a085c290ebd122dc42cba69373b5953b831d', data: 'bytecode' }, 'no recipient with 0x')
|
|
||||||
})
|
|
||||||
|
|
||||||
it('should error when recipient is 0x', function () {
|
|
||||||
const zeroRecipientTxParams = {
|
|
||||||
from: '0x1678a085c290ebd122dc42cba69373b5953b831d',
|
|
||||||
to: '0x',
|
|
||||||
}
|
|
||||||
assert.throws(() => { txController._validateRecipient(zeroRecipientTxParams) }, Error, 'Invalid recipient address')
|
|
||||||
})
|
|
||||||
})
|
|
||||||
|
|
||||||
|
|
||||||
describe('#_validateFrom', () => {
|
|
||||||
it('should error when from is not a hex string', function () {
|
|
||||||
|
|
||||||
// where from is undefined
|
|
||||||
const txParams = {}
|
|
||||||
assert.throws(() => { txController._validateFrom(txParams) }, Error, `Invalid from address ${txParams.from} not a string`)
|
|
||||||
|
|
||||||
// where from is array
|
|
||||||
txParams.from = []
|
|
||||||
assert.throws(() => { txController._validateFrom(txParams) }, Error, `Invalid from address ${txParams.from} not a string`)
|
|
||||||
|
|
||||||
// where from is a object
|
|
||||||
txParams.from = {}
|
|
||||||
assert.throws(() => { txController._validateFrom(txParams) }, Error, `Invalid from address ${txParams.from} not a string`)
|
|
||||||
|
|
||||||
// where from is a invalid address
|
|
||||||
txParams.from = 'im going to fail'
|
|
||||||
assert.throws(() => { txController._validateFrom(txParams) }, Error, `Invalid from address`)
|
|
||||||
|
|
||||||
// should run
|
|
||||||
txParams.from ='0x1678a085c290ebd122dc42cba69373b5953b831d'
|
|
||||||
txController._validateFrom(txParams)
|
|
||||||
})
|
|
||||||
})
|
|
||||||
|
|
||||||
describe('#addTx', function () {
|
describe('#addTx', function () {
|
||||||
it('should emit updates', function (done) {
|
it('should emit updates', function (done) {
|
||||||
const txMeta = {
|
const txMeta = {
|
||||||
@ -391,12 +297,12 @@ describe('Transaction Controller', function () {
|
|||||||
|
|
||||||
describe('#updateAndApproveTransaction', function () {
|
describe('#updateAndApproveTransaction', function () {
|
||||||
let txMeta
|
let txMeta
|
||||||
beforeEach(function () {
|
beforeEach(() => {
|
||||||
txMeta = {
|
txMeta = {
|
||||||
id: 1,
|
id: 1,
|
||||||
status: 'unapproved',
|
status: 'unapproved',
|
||||||
txParams: {
|
txParams: {
|
||||||
from: '0xc684832530fcbddae4b4230a47e991ddcec2831d',
|
from: fromAccount.address,
|
||||||
to: '0x1678a085c290ebd122dc42cba69373b5953b831d',
|
to: '0x1678a085c290ebd122dc42cba69373b5953b831d',
|
||||||
gasPrice: '0x77359400',
|
gasPrice: '0x77359400',
|
||||||
gas: '0x7b0d',
|
gas: '0x7b0d',
|
||||||
@ -405,11 +311,12 @@ describe('Transaction Controller', function () {
|
|||||||
metamaskNetworkId: currentNetworkId,
|
metamaskNetworkId: currentNetworkId,
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
it('should update and approve transactions', function () {
|
it('should update and approve transactions', async () => {
|
||||||
txController.txStateManager.addTx(txMeta)
|
txController.txStateManager.addTx(txMeta)
|
||||||
txController.updateAndApproveTransaction(txMeta)
|
const approvalPromise = txController.updateAndApproveTransaction(txMeta)
|
||||||
const tx = txController.txStateManager.getTx(1)
|
const tx = txController.txStateManager.getTx(1)
|
||||||
assert.equal(tx.status, 'approved')
|
assert.equal(tx.status, 'approved')
|
||||||
|
await approvalPromise
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
|
@ -1,14 +1,77 @@
|
|||||||
const assert = require('assert')
|
const assert = require('assert')
|
||||||
const TxGasUtils = require('../../app/scripts/lib/tx-gas-utils')
|
const Transaction = require('ethereumjs-tx')
|
||||||
const { createTestProviderTools } = require('../stub/provider')
|
const BN = require('bn.js')
|
||||||
|
|
||||||
describe('Tx Gas Util', function () {
|
|
||||||
let txGasUtil, provider, providerResultStub
|
const { hexToBn, bnToHex } = require('../../app/scripts/lib/util')
|
||||||
beforeEach(function () {
|
const TxUtils = require('../../app/scripts/controllers/transactions/tx-gas-utils')
|
||||||
providerResultStub = {}
|
|
||||||
provider = createTestProviderTools({ scaffold: providerResultStub }).provider
|
|
||||||
txGasUtil = new TxGasUtils({
|
describe('txUtils', function () {
|
||||||
provider,
|
let txUtils
|
||||||
|
|
||||||
|
before(function () {
|
||||||
|
txUtils = new TxUtils(new Proxy({}, {
|
||||||
|
get: (obj, name) => {
|
||||||
|
return () => {}
|
||||||
|
},
|
||||||
|
}))
|
||||||
|
})
|
||||||
|
|
||||||
|
describe('chain Id', function () {
|
||||||
|
it('prepares a transaction with the provided chainId', function () {
|
||||||
|
const txParams = {
|
||||||
|
to: '0x70ad465e0bab6504002ad58c744ed89c7da38524',
|
||||||
|
from: '0x69ad465e0bab6504002ad58c744ed89c7da38525',
|
||||||
|
value: '0x0',
|
||||||
|
gas: '0x7b0c',
|
||||||
|
gasPrice: '0x199c82cc00',
|
||||||
|
data: '0x',
|
||||||
|
nonce: '0x3',
|
||||||
|
chainId: 42,
|
||||||
|
}
|
||||||
|
const ethTx = new Transaction(txParams)
|
||||||
|
assert.equal(ethTx.getChainId(), 42, 'chainId is set from tx params')
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
describe('addGasBuffer', function () {
|
||||||
|
it('multiplies by 1.5, when within block gas limit', function () {
|
||||||
|
// naive estimatedGas: 0x16e360 (1.5 mil)
|
||||||
|
const inputHex = '0x16e360'
|
||||||
|
// dummy gas limit: 0x3d4c52 (4 mil)
|
||||||
|
const blockGasLimitHex = '0x3d4c52'
|
||||||
|
const output = txUtils.addGasBuffer(inputHex, blockGasLimitHex)
|
||||||
|
const inputBn = hexToBn(inputHex)
|
||||||
|
const outputBn = hexToBn(output)
|
||||||
|
const expectedBn = inputBn.muln(1.5)
|
||||||
|
assert(outputBn.eq(expectedBn), 'returns 1.5 the input value')
|
||||||
|
})
|
||||||
|
|
||||||
|
it('uses original estimatedGas, when above block gas limit', function () {
|
||||||
|
// naive estimatedGas: 0x16e360 (1.5 mil)
|
||||||
|
const inputHex = '0x16e360'
|
||||||
|
// dummy gas limit: 0x0f4240 (1 mil)
|
||||||
|
const blockGasLimitHex = '0x0f4240'
|
||||||
|
const output = txUtils.addGasBuffer(inputHex, blockGasLimitHex)
|
||||||
|
// const inputBn = hexToBn(inputHex)
|
||||||
|
const outputBn = hexToBn(output)
|
||||||
|
const expectedBn = hexToBn(inputHex)
|
||||||
|
assert(outputBn.eq(expectedBn), 'returns the original estimatedGas value')
|
||||||
|
})
|
||||||
|
|
||||||
|
it('buffers up to recommend gas limit recommended ceiling', function () {
|
||||||
|
// naive estimatedGas: 0x16e360 (1.5 mil)
|
||||||
|
const inputHex = '0x16e360'
|
||||||
|
// dummy gas limit: 0x1e8480 (2 mil)
|
||||||
|
const blockGasLimitHex = '0x1e8480'
|
||||||
|
const blockGasLimitBn = hexToBn(blockGasLimitHex)
|
||||||
|
const ceilGasLimitBn = blockGasLimitBn.muln(0.9)
|
||||||
|
const output = txUtils.addGasBuffer(inputHex, blockGasLimitHex)
|
||||||
|
// const inputBn = hexToBn(inputHex)
|
||||||
|
// const outputBn = hexToBn(output)
|
||||||
|
const expectedHex = bnToHex(ceilGasLimitBn)
|
||||||
|
assert.equal(output, expectedHex, 'returns the gas limit recommended ceiling value')
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
@ -1,6 +1,6 @@
|
|||||||
const assert = require('assert')
|
const assert = require('assert')
|
||||||
const clone = require('clone')
|
const clone = require('clone')
|
||||||
const txStateHistoryHelper = require('../../app/scripts/lib/tx-state-history-helper')
|
const txStateHistoryHelper = require('../../app/scripts/controllers/transactions/lib/tx-state-history-helper')
|
||||||
|
|
||||||
describe('deepCloneFromTxMeta', function () {
|
describe('deepCloneFromTxMeta', function () {
|
||||||
it('should clone deep', function () {
|
it('should clone deep', function () {
|
||||||
|
@ -1,5 +1,5 @@
|
|||||||
const assert = require('assert')
|
const assert = require('assert')
|
||||||
const txStateHistoryHelper = require('../../app/scripts/lib/tx-state-history-helper')
|
const txStateHistoryHelper = require('../../app/scripts/controllers/transactions/lib/tx-state-history-helper')
|
||||||
const testVault = require('../data/v17-long-history.json')
|
const testVault = require('../data/v17-long-history.json')
|
||||||
|
|
||||||
|
|
||||||
|
@ -1,8 +1,8 @@
|
|||||||
const assert = require('assert')
|
const assert = require('assert')
|
||||||
const clone = require('clone')
|
const clone = require('clone')
|
||||||
const ObservableStore = require('obs-store')
|
const ObservableStore = require('obs-store')
|
||||||
const TxStateManager = require('../../app/scripts/lib/tx-state-manager')
|
const TxStateManager = require('../../app/scripts/controllers/transactions/tx-state-manager')
|
||||||
const txStateHistoryHelper = require('../../app/scripts/lib/tx-state-history-helper')
|
const txStateHistoryHelper = require('../../app/scripts/controllers/transactions/lib/tx-state-history-helper')
|
||||||
const noop = () => true
|
const noop = () => true
|
||||||
|
|
||||||
describe('TransactionStateManager', function () {
|
describe('TransactionStateManager', function () {
|
||||||
|
@ -1,77 +1,98 @@
|
|||||||
const assert = require('assert')
|
const assert = require('assert')
|
||||||
const Transaction = require('ethereumjs-tx')
|
const txUtils = require('../../app/scripts/controllers/transactions/lib/util')
|
||||||
const BN = require('bn.js')
|
|
||||||
|
|
||||||
|
|
||||||
const { hexToBn, bnToHex } = require('../../app/scripts/lib/util')
|
|
||||||
const TxUtils = require('../../app/scripts/lib/tx-gas-utils')
|
|
||||||
|
|
||||||
|
|
||||||
describe('txUtils', function () {
|
describe('txUtils', function () {
|
||||||
let txUtils
|
describe('#validateTxParams', function () {
|
||||||
|
it('does not throw for positive values', function () {
|
||||||
before(function () {
|
var sample = {
|
||||||
txUtils = new TxUtils(new Proxy({}, {
|
from: '0x1678a085c290ebd122dc42cba69373b5953b831d',
|
||||||
get: (obj, name) => {
|
value: '0x01',
|
||||||
return () => {}
|
}
|
||||||
},
|
txUtils.validateTxParams(sample)
|
||||||
}))
|
})
|
||||||
})
|
|
||||||
|
it('returns error for negative values', function () {
|
||||||
describe('chain Id', function () {
|
var sample = {
|
||||||
it('prepares a transaction with the provided chainId', function () {
|
from: '0x1678a085c290ebd122dc42cba69373b5953b831d',
|
||||||
const txParams = {
|
value: '-0x01',
|
||||||
to: '0x70ad465e0bab6504002ad58c744ed89c7da38524',
|
}
|
||||||
from: '0x69ad465e0bab6504002ad58c744ed89c7da38525',
|
try {
|
||||||
value: '0x0',
|
txUtils.validateTxParams(sample)
|
||||||
gas: '0x7b0c',
|
} catch (err) {
|
||||||
gasPrice: '0x199c82cc00',
|
assert.ok(err, 'error')
|
||||||
data: '0x',
|
|
||||||
nonce: '0x3',
|
|
||||||
chainId: 42,
|
|
||||||
}
|
}
|
||||||
const ethTx = new Transaction(txParams)
|
|
||||||
assert.equal(ethTx.getChainId(), 42, 'chainId is set from tx params')
|
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
describe('addGasBuffer', function () {
|
describe('#normalizeTxParams', () => {
|
||||||
it('multiplies by 1.5, when within block gas limit', function () {
|
it('should normalize txParams', () => {
|
||||||
// naive estimatedGas: 0x16e360 (1.5 mil)
|
let txParams = {
|
||||||
const inputHex = '0x16e360'
|
chainId: '0x1',
|
||||||
// dummy gas limit: 0x3d4c52 (4 mil)
|
from: 'a7df1beDBF813f57096dF77FCd515f0B3900e402',
|
||||||
const blockGasLimitHex = '0x3d4c52'
|
to: null,
|
||||||
const output = txUtils.addGasBuffer(inputHex, blockGasLimitHex)
|
data: '68656c6c6f20776f726c64',
|
||||||
const inputBn = hexToBn(inputHex)
|
random: 'hello world',
|
||||||
const outputBn = hexToBn(output)
|
}
|
||||||
const expectedBn = inputBn.muln(1.5)
|
|
||||||
assert(outputBn.eq(expectedBn), 'returns 1.5 the input value')
|
|
||||||
})
|
|
||||||
|
|
||||||
it('uses original estimatedGas, when above block gas limit', function () {
|
let normalizedTxParams = txUtils.normalizeTxParams(txParams)
|
||||||
// naive estimatedGas: 0x16e360 (1.5 mil)
|
|
||||||
const inputHex = '0x16e360'
|
assert(!normalizedTxParams.chainId, 'their should be no chainId')
|
||||||
// dummy gas limit: 0x0f4240 (1 mil)
|
assert(!normalizedTxParams.to, 'their should be no to address if null')
|
||||||
const blockGasLimitHex = '0x0f4240'
|
assert.equal(normalizedTxParams.from.slice(0, 2), '0x', 'from should be hexPrefixd')
|
||||||
const output = txUtils.addGasBuffer(inputHex, blockGasLimitHex)
|
assert.equal(normalizedTxParams.data.slice(0, 2), '0x', 'data should be hexPrefixd')
|
||||||
// const inputBn = hexToBn(inputHex)
|
assert(!('random' in normalizedTxParams), 'their should be no random key in normalizedTxParams')
|
||||||
const outputBn = hexToBn(output)
|
|
||||||
const expectedBn = hexToBn(inputHex)
|
txParams.to = 'a7df1beDBF813f57096dF77FCd515f0B3900e402'
|
||||||
assert(outputBn.eq(expectedBn), 'returns the original estimatedGas value')
|
normalizedTxParams = txUtils.normalizeTxParams(txParams)
|
||||||
})
|
assert.equal(normalizedTxParams.to.slice(0, 2), '0x', 'to should be hexPrefixd')
|
||||||
|
|
||||||
it('buffers up to recommend gas limit recommended ceiling', function () {
|
|
||||||
// naive estimatedGas: 0x16e360 (1.5 mil)
|
|
||||||
const inputHex = '0x16e360'
|
|
||||||
// dummy gas limit: 0x1e8480 (2 mil)
|
|
||||||
const blockGasLimitHex = '0x1e8480'
|
|
||||||
const blockGasLimitBn = hexToBn(blockGasLimitHex)
|
|
||||||
const ceilGasLimitBn = blockGasLimitBn.muln(0.9)
|
|
||||||
const output = txUtils.addGasBuffer(inputHex, blockGasLimitHex)
|
|
||||||
// const inputBn = hexToBn(inputHex)
|
|
||||||
// const outputBn = hexToBn(output)
|
|
||||||
const expectedHex = bnToHex(ceilGasLimitBn)
|
|
||||||
assert.equal(output, expectedHex, 'returns the gas limit recommended ceiling value')
|
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
})
|
|
||||||
|
describe('#validateRecipient', () => {
|
||||||
|
it('removes recipient for txParams with 0x when contract data is provided', function () {
|
||||||
|
const zeroRecipientandDataTxParams = {
|
||||||
|
from: '0x1678a085c290ebd122dc42cba69373b5953b831d',
|
||||||
|
to: '0x',
|
||||||
|
data: 'bytecode',
|
||||||
|
}
|
||||||
|
const sanitizedTxParams = txUtils.validateRecipient(zeroRecipientandDataTxParams)
|
||||||
|
assert.deepEqual(sanitizedTxParams, { from: '0x1678a085c290ebd122dc42cba69373b5953b831d', data: 'bytecode' }, 'no recipient with 0x')
|
||||||
|
})
|
||||||
|
|
||||||
|
it('should error when recipient is 0x', function () {
|
||||||
|
const zeroRecipientTxParams = {
|
||||||
|
from: '0x1678a085c290ebd122dc42cba69373b5953b831d',
|
||||||
|
to: '0x',
|
||||||
|
}
|
||||||
|
assert.throws(() => { txUtils.validateRecipient(zeroRecipientTxParams) }, Error, 'Invalid recipient address')
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
|
||||||
|
describe('#validateFrom', () => {
|
||||||
|
it('should error when from is not a hex string', function () {
|
||||||
|
|
||||||
|
// where from is undefined
|
||||||
|
const txParams = {}
|
||||||
|
assert.throws(() => { txUtils.validateFrom(txParams) }, Error, `Invalid from address ${txParams.from} not a string`)
|
||||||
|
|
||||||
|
// where from is array
|
||||||
|
txParams.from = []
|
||||||
|
assert.throws(() => { txUtils.validateFrom(txParams) }, Error, `Invalid from address ${txParams.from} not a string`)
|
||||||
|
|
||||||
|
// where from is a object
|
||||||
|
txParams.from = {}
|
||||||
|
assert.throws(() => { txUtils.validateFrom(txParams) }, Error, `Invalid from address ${txParams.from} not a string`)
|
||||||
|
|
||||||
|
// where from is a invalid address
|
||||||
|
txParams.from = 'im going to fail'
|
||||||
|
assert.throws(() => { txUtils.validateFrom(txParams) }, Error, `Invalid from address`)
|
||||||
|
|
||||||
|
// should run
|
||||||
|
txParams.from ='0x1678a085c290ebd122dc42cba69373b5953b831d'
|
||||||
|
txUtils.validateFrom(txParams)
|
||||||
|
})
|
||||||
|
})
|
||||||
|
})
|
||||||
|
@ -1,4 +1,5 @@
|
|||||||
const abi = require('human-standard-token-abi')
|
const abi = require('human-standard-token-abi')
|
||||||
|
const pify = require('pify')
|
||||||
const getBuyEthUrl = require('../../app/scripts/lib/buy-eth-url')
|
const getBuyEthUrl = require('../../app/scripts/lib/buy-eth-url')
|
||||||
const { getTokenAddressFromTokenObject } = require('./util')
|
const { getTokenAddressFromTokenObject } = require('./util')
|
||||||
const ethUtil = require('ethereumjs-util')
|
const ethUtil = require('ethereumjs-util')
|
||||||
@ -83,7 +84,7 @@ var actions = {
|
|||||||
REVEAL_SEED_CONFIRMATION: 'REVEAL_SEED_CONFIRMATION',
|
REVEAL_SEED_CONFIRMATION: 'REVEAL_SEED_CONFIRMATION',
|
||||||
revealSeedConfirmation: revealSeedConfirmation,
|
revealSeedConfirmation: revealSeedConfirmation,
|
||||||
requestRevealSeed: requestRevealSeed,
|
requestRevealSeed: requestRevealSeed,
|
||||||
|
requestRevealSeedWords,
|
||||||
// unlock screen
|
// unlock screen
|
||||||
UNLOCK_IN_PROGRESS: 'UNLOCK_IN_PROGRESS',
|
UNLOCK_IN_PROGRESS: 'UNLOCK_IN_PROGRESS',
|
||||||
UNLOCK_FAILED: 'UNLOCK_FAILED',
|
UNLOCK_FAILED: 'UNLOCK_FAILED',
|
||||||
@ -345,11 +346,13 @@ function transitionBackward () {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function clearSeedWordCache () {
|
function confirmSeedWords () {
|
||||||
log.debug(`background.clearSeedWordCache`)
|
|
||||||
return dispatch => {
|
return dispatch => {
|
||||||
|
dispatch(actions.showLoadingIndication())
|
||||||
|
log.debug(`background.clearSeedWordCache`)
|
||||||
return new Promise((resolve, reject) => {
|
return new Promise((resolve, reject) => {
|
||||||
background.clearSeedWordCache((err, account) => {
|
background.clearSeedWordCache((err, account) => {
|
||||||
|
dispatch(actions.hideLoadingIndication())
|
||||||
if (err) {
|
if (err) {
|
||||||
dispatch(actions.displayWarning(err.message))
|
dispatch(actions.displayWarning(err.message))
|
||||||
return reject(err)
|
return reject(err)
|
||||||
@ -363,22 +366,6 @@ function clearSeedWordCache () {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function confirmSeedWords () {
|
|
||||||
return async dispatch => {
|
|
||||||
dispatch(actions.showLoadingIndication())
|
|
||||||
const account = await dispatch(clearSeedWordCache())
|
|
||||||
return dispatch(setIsRevealingSeedWords(false))
|
|
||||||
.then(() => {
|
|
||||||
dispatch(actions.hideLoadingIndication())
|
|
||||||
return account
|
|
||||||
})
|
|
||||||
.catch(() => {
|
|
||||||
dispatch(actions.hideLoadingIndication())
|
|
||||||
return account
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function createNewVaultAndRestore (password, seed) {
|
function createNewVaultAndRestore (password, seed) {
|
||||||
return (dispatch) => {
|
return (dispatch) => {
|
||||||
dispatch(actions.showLoadingIndication())
|
dispatch(actions.showLoadingIndication())
|
||||||
@ -441,6 +428,30 @@ function revealSeedConfirmation () {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function verifyPassword (password) {
|
||||||
|
return new Promise((resolve, reject) => {
|
||||||
|
background.submitPassword(password, error => {
|
||||||
|
if (error) {
|
||||||
|
return reject(error)
|
||||||
|
}
|
||||||
|
|
||||||
|
resolve(true)
|
||||||
|
})
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
function verifySeedPhrase () {
|
||||||
|
return new Promise((resolve, reject) => {
|
||||||
|
background.verifySeedPhrase((error, seedWords) => {
|
||||||
|
if (error) {
|
||||||
|
return reject(error)
|
||||||
|
}
|
||||||
|
|
||||||
|
resolve(seedWords)
|
||||||
|
})
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
function requestRevealSeed (password) {
|
function requestRevealSeed (password) {
|
||||||
return dispatch => {
|
return dispatch => {
|
||||||
dispatch(actions.showLoadingIndication())
|
dispatch(actions.showLoadingIndication())
|
||||||
@ -460,13 +471,29 @@ function requestRevealSeed (password) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
dispatch(actions.showNewVaultSeed(result))
|
dispatch(actions.showNewVaultSeed(result))
|
||||||
|
dispatch(actions.hideLoadingIndication())
|
||||||
resolve()
|
resolve()
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
.then(() => dispatch(setIsRevealingSeedWords(true)))
|
}
|
||||||
.then(() => dispatch(actions.hideLoadingIndication()))
|
}
|
||||||
.catch(() => dispatch(actions.hideLoadingIndication()))
|
|
||||||
|
function requestRevealSeedWords (password) {
|
||||||
|
return async dispatch => {
|
||||||
|
dispatch(actions.showLoadingIndication())
|
||||||
|
log.debug(`background.submitPassword`)
|
||||||
|
|
||||||
|
try {
|
||||||
|
await verifyPassword(password)
|
||||||
|
const seedWords = await verifySeedPhrase()
|
||||||
|
dispatch(actions.hideLoadingIndication())
|
||||||
|
return seedWords
|
||||||
|
} catch (error) {
|
||||||
|
dispatch(actions.hideLoadingIndication())
|
||||||
|
dispatch(actions.displayWarning(error.message))
|
||||||
|
throw new Error(error.message)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -497,31 +524,26 @@ function addNewKeyring (type, opts) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function importNewAccount (strategy, args) {
|
function importNewAccount (strategy, args) {
|
||||||
return (dispatch) => {
|
return async (dispatch) => {
|
||||||
dispatch(actions.showLoadingIndication('This may take a while, be patient.'))
|
let newState
|
||||||
log.debug(`background.importAccountWithStrategy`)
|
dispatch(actions.showLoadingIndication('This may take a while, please be patient.'))
|
||||||
return new Promise((resolve, reject) => {
|
try {
|
||||||
background.importAccountWithStrategy(strategy, args, (err) => {
|
log.debug(`background.importAccountWithStrategy`)
|
||||||
if (err) {
|
await pify(background.importAccountWithStrategy).call(background, strategy, args)
|
||||||
dispatch(actions.displayWarning(err.message))
|
log.debug(`background.getState`)
|
||||||
return reject(err)
|
newState = await pify(background.getState).call(background)
|
||||||
}
|
} catch (err) {
|
||||||
log.debug(`background.getState`)
|
dispatch(actions.hideLoadingIndication())
|
||||||
background.getState((err, newState) => {
|
dispatch(actions.displayWarning(err.message))
|
||||||
dispatch(actions.hideLoadingIndication())
|
throw err
|
||||||
if (err) {
|
}
|
||||||
dispatch(actions.displayWarning(err.message))
|
dispatch(actions.hideLoadingIndication())
|
||||||
return reject(err)
|
dispatch(actions.updateMetamaskState(newState))
|
||||||
}
|
dispatch({
|
||||||
dispatch(actions.updateMetamaskState(newState))
|
type: actions.SHOW_ACCOUNT_DETAIL,
|
||||||
dispatch({
|
value: newState.selectedAddress,
|
||||||
type: actions.SHOW_ACCOUNT_DETAIL,
|
|
||||||
value: newState.selectedAddress,
|
|
||||||
})
|
|
||||||
resolve(newState)
|
|
||||||
})
|
|
||||||
})
|
|
||||||
})
|
})
|
||||||
|
return newState
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -1923,11 +1945,3 @@ function updateNetworkEndpointType (networkEndpointType) {
|
|||||||
value: networkEndpointType,
|
value: networkEndpointType,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function setIsRevealingSeedWords (reveal) {
|
|
||||||
return dispatch => {
|
|
||||||
log.debug(`background.setIsRevealingSeedWords`)
|
|
||||||
background.setIsRevealingSeedWords(reveal)
|
|
||||||
return forceUpdateMetamaskState(dispatch)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
@ -24,12 +24,12 @@ const Initialized = require('./components/pages/initialized')
|
|||||||
const Settings = require('./components/pages/settings')
|
const Settings = require('./components/pages/settings')
|
||||||
const UnlockPage = require('./components/pages/unlock')
|
const UnlockPage = require('./components/pages/unlock')
|
||||||
const RestoreVaultPage = require('./components/pages/keychains/restore-vault')
|
const RestoreVaultPage = require('./components/pages/keychains/restore-vault')
|
||||||
const RevealSeedConfirmation = require('./keychains/hd/recover-seed/confirmation')
|
const RevealSeedConfirmation = require('./components/pages/keychains/reveal-seed')
|
||||||
const AddTokenPage = require('./components/pages/add-token')
|
const AddTokenPage = require('./components/pages/add-token')
|
||||||
const CreateAccountPage = require('./components/pages/create-account')
|
const CreateAccountPage = require('./components/pages/create-account')
|
||||||
const NoticeScreen = require('./components/pages/notice')
|
const NoticeScreen = require('./components/pages/notice')
|
||||||
|
|
||||||
const Loading = require('./components/loading')
|
const Loading = require('./components/loading-screen')
|
||||||
const NetworkIndicator = require('./components/network')
|
const NetworkIndicator = require('./components/network')
|
||||||
const Identicon = require('./components/identicon')
|
const Identicon = require('./components/identicon')
|
||||||
const ReactCSSTransitionGroup = require('react-addons-css-transition-group')
|
const ReactCSSTransitionGroup = require('react-addons-css-transition-group')
|
||||||
@ -56,20 +56,11 @@ const {
|
|||||||
|
|
||||||
class App extends Component {
|
class App extends Component {
|
||||||
componentWillMount () {
|
componentWillMount () {
|
||||||
const {
|
const { currentCurrency, setCurrentCurrencyToUSD } = this.props
|
||||||
currentCurrency,
|
|
||||||
setCurrentCurrencyToUSD,
|
|
||||||
isRevealingSeedWords,
|
|
||||||
clearSeedWords,
|
|
||||||
} = this.props
|
|
||||||
|
|
||||||
if (!currentCurrency) {
|
if (!currentCurrency) {
|
||||||
setCurrentCurrencyToUSD()
|
setCurrentCurrencyToUSD()
|
||||||
}
|
}
|
||||||
|
|
||||||
if (isRevealingSeedWords) {
|
|
||||||
clearSeedWords()
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
renderRoutes () {
|
renderRoutes () {
|
||||||
@ -144,6 +135,7 @@ class App extends Component {
|
|||||||
|
|
||||||
(isLoading || isLoadingNetwork) && h(Loading, {
|
(isLoading || isLoadingNetwork) && h(Loading, {
|
||||||
loadingMessage: loadMessage,
|
loadingMessage: loadMessage,
|
||||||
|
fullScreen: true,
|
||||||
}),
|
}),
|
||||||
|
|
||||||
// content
|
// content
|
||||||
@ -402,8 +394,6 @@ App.propTypes = {
|
|||||||
isMouseUser: PropTypes.bool,
|
isMouseUser: PropTypes.bool,
|
||||||
setMouseUserState: PropTypes.func,
|
setMouseUserState: PropTypes.func,
|
||||||
t: PropTypes.func,
|
t: PropTypes.func,
|
||||||
isRevealingSeedWords: PropTypes.bool,
|
|
||||||
clearSeedWords: PropTypes.func,
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function mapStateToProps (state) {
|
function mapStateToProps (state) {
|
||||||
@ -484,7 +474,6 @@ function mapDispatchToProps (dispatch, ownProps) {
|
|||||||
setCurrentCurrencyToUSD: () => dispatch(actions.setCurrentCurrency('usd')),
|
setCurrentCurrencyToUSD: () => dispatch(actions.setCurrentCurrency('usd')),
|
||||||
toggleAccountMenu: () => dispatch(actions.toggleAccountMenu()),
|
toggleAccountMenu: () => dispatch(actions.toggleAccountMenu()),
|
||||||
setMouseUserState: (isMouseUser) => dispatch(actions.setMouseUserState(isMouseUser)),
|
setMouseUserState: (isMouseUser) => dispatch(actions.setMouseUserState(isMouseUser)),
|
||||||
clearSeedWords: () => dispatch(actions.confirmSeedWords()),
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -6,7 +6,7 @@ const connect = require('react-redux').connect
|
|||||||
const actions = require('../actions')
|
const actions = require('../actions')
|
||||||
const CoinbaseForm = require('./coinbase-form')
|
const CoinbaseForm = require('./coinbase-form')
|
||||||
const ShapeshiftForm = require('./shapeshift-form')
|
const ShapeshiftForm = require('./shapeshift-form')
|
||||||
const Loading = require('./loading')
|
const Loading = require('./loading-screen')
|
||||||
const AccountPanel = require('./account-panel')
|
const AccountPanel = require('./account-panel')
|
||||||
const RadioList = require('./custom-radio-list')
|
const RadioList = require('./custom-radio-list')
|
||||||
const { getNetworkDisplayName } = require('../../../app/scripts/controllers/network/util')
|
const { getNetworkDisplayName } = require('../../../app/scripts/controllers/network/util')
|
||||||
|
@ -280,8 +280,7 @@ CustomizeGasModal.prototype.render = function () {
|
|||||||
h(GasModalCard, {
|
h(GasModalCard, {
|
||||||
value: convertedGasPrice,
|
value: convertedGasPrice,
|
||||||
min: forceGasMin || MIN_GAS_PRICE_GWEI,
|
min: forceGasMin || MIN_GAS_PRICE_GWEI,
|
||||||
// max: 1000,
|
step: 1,
|
||||||
step: multiplyCurrencies(MIN_GAS_PRICE_GWEI, 10),
|
|
||||||
onChange: value => this.convertAndSetGasPrice(value),
|
onChange: value => this.convertAndSetGasPrice(value),
|
||||||
title: this.context.t('gasPrice'),
|
title: this.context.t('gasPrice'),
|
||||||
copy: this.context.t('gasPriceCalculation'),
|
copy: this.context.t('gasPriceCalculation'),
|
||||||
@ -290,7 +289,6 @@ CustomizeGasModal.prototype.render = function () {
|
|||||||
h(GasModalCard, {
|
h(GasModalCard, {
|
||||||
value: convertedGasLimit,
|
value: convertedGasLimit,
|
||||||
min: 1,
|
min: 1,
|
||||||
// max: 100000,
|
|
||||||
step: 1,
|
step: 1,
|
||||||
onChange: value => this.convertAndSetGasLimit(value),
|
onChange: value => this.convertAndSetGasLimit(value),
|
||||||
title: this.context.t('gasLimit'),
|
title: this.context.t('gasLimit'),
|
||||||
|
@ -0,0 +1,45 @@
|
|||||||
|
const { Component } = require('react')
|
||||||
|
const PropTypes = require('prop-types')
|
||||||
|
const h = require('react-hyperscript')
|
||||||
|
const copyToClipboard = require('copy-to-clipboard')
|
||||||
|
const { exportAsFile } = require('../../util')
|
||||||
|
|
||||||
|
class ExportTextContainer extends Component {
|
||||||
|
render () {
|
||||||
|
const { text = '', filename = '' } = this.props
|
||||||
|
const { t } = this.context
|
||||||
|
|
||||||
|
return (
|
||||||
|
h('.export-text-container', [
|
||||||
|
h('.export-text-container__text-container', [
|
||||||
|
h('.export-text-container__text', text),
|
||||||
|
]),
|
||||||
|
h('.export-text-container__buttons-container', [
|
||||||
|
h('.export-text-container__button.export-text-container__button--copy', {
|
||||||
|
onClick: () => copyToClipboard(text),
|
||||||
|
}, [
|
||||||
|
h('img', { src: 'images/copy-to-clipboard.svg' }),
|
||||||
|
h('.export-text-container__button-text', t('copyToClipboard')),
|
||||||
|
]),
|
||||||
|
h('.export-text-container__button', {
|
||||||
|
onClick: () => exportAsFile(filename, text),
|
||||||
|
}, [
|
||||||
|
h('img', { src: 'images/download.svg' }),
|
||||||
|
h('.export-text-container__button-text', t('saveAsCsvFile')),
|
||||||
|
]),
|
||||||
|
]),
|
||||||
|
])
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
ExportTextContainer.propTypes = {
|
||||||
|
text: PropTypes.string,
|
||||||
|
filename: PropTypes.string,
|
||||||
|
}
|
||||||
|
|
||||||
|
ExportTextContainer.contextTypes = {
|
||||||
|
t: PropTypes.func,
|
||||||
|
}
|
||||||
|
|
||||||
|
module.exports = ExportTextContainer
|
@ -0,0 +1,52 @@
|
|||||||
|
.export-text-container {
|
||||||
|
display: flex;
|
||||||
|
justify-content: center;
|
||||||
|
flex-direction: column;
|
||||||
|
align-items: center;
|
||||||
|
border: 1px solid $alto;
|
||||||
|
border-radius: 4px;
|
||||||
|
font-weight: 400;
|
||||||
|
|
||||||
|
&__text-container {
|
||||||
|
width: 100%;
|
||||||
|
display: flex;
|
||||||
|
justify-content: center;
|
||||||
|
padding: 20px;
|
||||||
|
border-radius: 4px;
|
||||||
|
background: $alabaster;
|
||||||
|
}
|
||||||
|
|
||||||
|
&__text {
|
||||||
|
resize: none;
|
||||||
|
border: none;
|
||||||
|
background: $alabaster;
|
||||||
|
font-size: 20px;
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
&__buttons-container {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: row;
|
||||||
|
border-top: 1px solid $alto;
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
&__button {
|
||||||
|
padding: 10px;
|
||||||
|
flex: 1;
|
||||||
|
display: flex;
|
||||||
|
justify-content: center;
|
||||||
|
align-items: center;
|
||||||
|
font-size: 14px;
|
||||||
|
cursor: pointer;
|
||||||
|
color: $curious-blue;
|
||||||
|
|
||||||
|
&--copy {
|
||||||
|
border-right: 1px solid $alto;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
&__button-text {
|
||||||
|
padding-left: 10px;
|
||||||
|
}
|
||||||
|
}
|
2
ui/app/components/export-text-container/index.js
Normal file
2
ui/app/components/export-text-container/index.js
Normal file
@ -0,0 +1,2 @@
|
|||||||
|
const ExportTextContainer = require('./export-text-container.component')
|
||||||
|
module.exports = ExportTextContainer
|
2
ui/app/components/loading-screen/index.js
Normal file
2
ui/app/components/loading-screen/index.js
Normal file
@ -0,0 +1,2 @@
|
|||||||
|
const LoadingScreen = require('./loading-screen.component')
|
||||||
|
module.exports = LoadingScreen
|
@ -2,8 +2,9 @@ const { Component } = require('react')
|
|||||||
const h = require('react-hyperscript')
|
const h = require('react-hyperscript')
|
||||||
const PropTypes = require('prop-types')
|
const PropTypes = require('prop-types')
|
||||||
const classnames = require('classnames')
|
const classnames = require('classnames')
|
||||||
|
const Spinner = require('../spinner')
|
||||||
|
|
||||||
class LoadingIndicator extends Component {
|
class LoadingScreen extends Component {
|
||||||
renderMessage () {
|
renderMessage () {
|
||||||
const { loadingMessage } = this.props
|
const { loadingMessage } = this.props
|
||||||
return loadingMessage && h('span', loadingMessage)
|
return loadingMessage && h('span', loadingMessage)
|
||||||
@ -14,9 +15,9 @@ class LoadingIndicator extends Component {
|
|||||||
h('.loading-overlay', {
|
h('.loading-overlay', {
|
||||||
className: classnames({ 'loading-overlay--full-screen': this.props.fullScreen }),
|
className: classnames({ 'loading-overlay--full-screen': this.props.fullScreen }),
|
||||||
}, [
|
}, [
|
||||||
h('.flex-center.flex-column', [
|
h('.loading-overlay__container', [
|
||||||
h('img', {
|
h(Spinner, {
|
||||||
src: 'images/loading.svg',
|
color: '#F7C06C',
|
||||||
}),
|
}),
|
||||||
|
|
||||||
this.renderMessage(),
|
this.renderMessage(),
|
||||||
@ -26,9 +27,9 @@ class LoadingIndicator extends Component {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
LoadingIndicator.propTypes = {
|
LoadingScreen.propTypes = {
|
||||||
loadingMessage: PropTypes.string,
|
loadingMessage: PropTypes.string,
|
||||||
fullScreen: PropTypes.bool,
|
fullScreen: PropTypes.bool,
|
||||||
}
|
}
|
||||||
|
|
||||||
module.exports = LoadingIndicator
|
module.exports = LoadingScreen
|
@ -192,7 +192,7 @@ AddTokenScreen.prototype.attemptToAutoFillTokenParams = async function (address)
|
|||||||
if (symbol && decimals) {
|
if (symbol && decimals) {
|
||||||
this.setState({
|
this.setState({
|
||||||
customSymbol: symbol,
|
customSymbol: symbol,
|
||||||
customDecimals: decimals.toString(),
|
customDecimals: decimals,
|
||||||
autoFilled: true,
|
autoFilled: true,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
@ -105,6 +105,8 @@ class JsonImportSubview extends Component {
|
|||||||
}
|
}
|
||||||
|
|
||||||
this.props.importNewJsonAccount([ fileContents, password ])
|
this.props.importNewJsonAccount([ fileContents, password ])
|
||||||
|
// JS runtime requires caught rejections but failures are handled by Redux
|
||||||
|
.catch()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -91,5 +91,7 @@ PrivateKeyImportView.prototype.createNewKeychain = function () {
|
|||||||
const { importNewAccount, history } = this.props
|
const { importNewAccount, history } = this.props
|
||||||
|
|
||||||
importNewAccount('Private Key', [ privateKey ])
|
importNewAccount('Private Key', [ privateKey ])
|
||||||
|
// JS runtime requires caught rejections but failures are handled by Redux
|
||||||
|
.catch()
|
||||||
.then(() => history.push(DEFAULT_ROUTE))
|
.then(() => history.push(DEFAULT_ROUTE))
|
||||||
}
|
}
|
||||||
|
@ -21,7 +21,7 @@ const QrView = require('../../components/qr-code')
|
|||||||
|
|
||||||
// Routes
|
// Routes
|
||||||
const {
|
const {
|
||||||
REVEAL_SEED_ROUTE,
|
INITIALIZE_BACKUP_PHRASE_ROUTE,
|
||||||
RESTORE_VAULT_ROUTE,
|
RESTORE_VAULT_ROUTE,
|
||||||
CONFIRM_TRANSACTION_ROUTE,
|
CONFIRM_TRANSACTION_ROUTE,
|
||||||
NOTICE_ROUTE,
|
NOTICE_ROUTE,
|
||||||
@ -69,7 +69,7 @@ class Home extends Component {
|
|||||||
log.debug('rendering seed words')
|
log.debug('rendering seed words')
|
||||||
return h(Redirect, {
|
return h(Redirect, {
|
||||||
to: {
|
to: {
|
||||||
pathname: REVEAL_SEED_ROUTE,
|
pathname: INITIALIZE_BACKUP_PHRASE_ROUTE,
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
@ -2,11 +2,27 @@ const { Component } = require('react')
|
|||||||
const { connect } = require('react-redux')
|
const { connect } = require('react-redux')
|
||||||
const PropTypes = require('prop-types')
|
const PropTypes = require('prop-types')
|
||||||
const h = require('react-hyperscript')
|
const h = require('react-hyperscript')
|
||||||
const { exportAsFile } = require('../../../util')
|
const classnames = require('classnames')
|
||||||
const { requestRevealSeed, confirmSeedWords } = require('../../../actions')
|
|
||||||
|
const { requestRevealSeedWords } = require('../../../actions')
|
||||||
const { DEFAULT_ROUTE } = require('../../../routes')
|
const { DEFAULT_ROUTE } = require('../../../routes')
|
||||||
|
const ExportTextContainer = require('../../export-text-container')
|
||||||
|
|
||||||
|
const PASSWORD_PROMPT_SCREEN = 'PASSWORD_PROMPT_SCREEN'
|
||||||
|
const REVEAL_SEED_SCREEN = 'REVEAL_SEED_SCREEN'
|
||||||
|
|
||||||
class RevealSeedPage extends Component {
|
class RevealSeedPage extends Component {
|
||||||
|
constructor (props) {
|
||||||
|
super(props)
|
||||||
|
|
||||||
|
this.state = {
|
||||||
|
screen: PASSWORD_PROMPT_SCREEN,
|
||||||
|
password: '',
|
||||||
|
seedWords: null,
|
||||||
|
error: null,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
componentDidMount () {
|
componentDidMount () {
|
||||||
const passwordBox = document.getElementById('password-box')
|
const passwordBox = document.getElementById('password-box')
|
||||||
if (passwordBox) {
|
if (passwordBox) {
|
||||||
@ -14,182 +30,135 @@ class RevealSeedPage extends Component {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
checkConfirmation (event) {
|
handleSubmit (event) {
|
||||||
if (event.key === 'Enter') {
|
event.preventDefault()
|
||||||
event.preventDefault()
|
this.setState({ seedWords: null, error: null })
|
||||||
this.revealSeedWords()
|
this.props.requestRevealSeedWords(this.state.password)
|
||||||
}
|
.then(seedWords => this.setState({ seedWords, screen: REVEAL_SEED_SCREEN }))
|
||||||
|
.catch(error => this.setState({ error: error.message }))
|
||||||
}
|
}
|
||||||
|
|
||||||
revealSeedWords () {
|
renderWarning () {
|
||||||
const password = document.getElementById('password-box').value
|
|
||||||
this.props.requestRevealSeed(password)
|
|
||||||
}
|
|
||||||
|
|
||||||
renderSeed () {
|
|
||||||
const { seedWords, confirmSeedWords, history } = this.props
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
h('.initialize-screen.flex-column.flex-center.flex-grow', [
|
h('.page-container__warning-container', [
|
||||||
|
h('img.page-container__warning-icon', {
|
||||||
h('h3.flex-center.text-transform-uppercase', {
|
src: 'images/warning.svg',
|
||||||
style: {
|
|
||||||
background: '#EBEBEB',
|
|
||||||
color: '#AEAEAE',
|
|
||||||
marginTop: 36,
|
|
||||||
marginBottom: 8,
|
|
||||||
width: '100%',
|
|
||||||
fontSize: '20px',
|
|
||||||
padding: 6,
|
|
||||||
},
|
|
||||||
}, [
|
|
||||||
'Vault Created',
|
|
||||||
]),
|
|
||||||
|
|
||||||
h('div', {
|
|
||||||
style: {
|
|
||||||
fontSize: '1em',
|
|
||||||
marginTop: '10px',
|
|
||||||
textAlign: 'center',
|
|
||||||
},
|
|
||||||
}, [
|
|
||||||
h('span.error', 'These 12 words are the only way to restore your MetaMask accounts.\nSave them somewhere safe and secret.'),
|
|
||||||
]),
|
|
||||||
|
|
||||||
h('textarea.twelve-word-phrase', {
|
|
||||||
readOnly: true,
|
|
||||||
value: seedWords,
|
|
||||||
}),
|
}),
|
||||||
|
h('.page-container__warning-message', [
|
||||||
h('button.primary', {
|
h('.page-container__warning-title', [this.context.t('revealSeedWordsWarningTitle')]),
|
||||||
onClick: () => confirmSeedWords().then(() => history.push(DEFAULT_ROUTE)),
|
h('div', [this.context.t('revealSeedWordsWarning')]),
|
||||||
style: {
|
]),
|
||||||
margin: '24px',
|
|
||||||
fontSize: '0.9em',
|
|
||||||
marginBottom: '10px',
|
|
||||||
},
|
|
||||||
}, 'I\'ve copied it somewhere safe'),
|
|
||||||
|
|
||||||
h('button.primary', {
|
|
||||||
onClick: () => exportAsFile(`MetaMask Seed Words`, seedWords),
|
|
||||||
style: {
|
|
||||||
margin: '10px',
|
|
||||||
fontSize: '0.9em',
|
|
||||||
},
|
|
||||||
}, 'Save Seed Words As File'),
|
|
||||||
])
|
])
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
renderConfirmation () {
|
renderContent () {
|
||||||
const { history, warning, inProgress } = this.props
|
return this.state.screen === PASSWORD_PROMPT_SCREEN
|
||||||
|
? this.renderPasswordPromptContent()
|
||||||
|
: this.renderRevealSeedContent()
|
||||||
|
}
|
||||||
|
|
||||||
|
renderPasswordPromptContent () {
|
||||||
|
const { t } = this.context
|
||||||
|
|
||||||
return (
|
return (
|
||||||
h('.initialize-screen.flex-column.flex-center.flex-grow', {
|
h('form', {
|
||||||
style: { maxWidth: '420px' },
|
onSubmit: event => this.handleSubmit(event),
|
||||||
}, [
|
}, [
|
||||||
|
h('label.input-label', {
|
||||||
h('h3.flex-center.text-transform-uppercase', {
|
htmlFor: 'password-box',
|
||||||
style: {
|
}, t('enterPasswordContinue')),
|
||||||
background: '#EBEBEB',
|
h('.input-group', [
|
||||||
color: '#AEAEAE',
|
h('input.form-control', {
|
||||||
marginBottom: 24,
|
|
||||||
width: '100%',
|
|
||||||
fontSize: '20px',
|
|
||||||
padding: 6,
|
|
||||||
},
|
|
||||||
}, [
|
|
||||||
'Reveal Seed Words',
|
|
||||||
]),
|
|
||||||
|
|
||||||
h('.div', {
|
|
||||||
style: {
|
|
||||||
display: 'flex',
|
|
||||||
flexDirection: 'column',
|
|
||||||
padding: '20px',
|
|
||||||
justifyContent: 'center',
|
|
||||||
},
|
|
||||||
}, [
|
|
||||||
|
|
||||||
h('h4', 'Do not recover your seed words in a public place! These words can be used to steal all your accounts.'),
|
|
||||||
|
|
||||||
// confirmation
|
|
||||||
h('input.large-input.letter-spacey', {
|
|
||||||
type: 'password',
|
type: 'password',
|
||||||
|
placeholder: t('password'),
|
||||||
id: 'password-box',
|
id: 'password-box',
|
||||||
placeholder: 'Enter your password to confirm',
|
value: this.state.password,
|
||||||
onKeyPress: this.checkConfirmation.bind(this),
|
onChange: event => this.setState({ password: event.target.value }),
|
||||||
style: {
|
className: classnames({ 'form-control--error': this.state.error }),
|
||||||
width: 260,
|
|
||||||
marginTop: '12px',
|
|
||||||
},
|
|
||||||
}),
|
}),
|
||||||
|
|
||||||
h('.flex-row.flex-start', {
|
|
||||||
style: {
|
|
||||||
marginTop: 30,
|
|
||||||
width: '50%',
|
|
||||||
},
|
|
||||||
}, [
|
|
||||||
// cancel
|
|
||||||
h('button.primary', {
|
|
||||||
onClick: () => history.push(DEFAULT_ROUTE),
|
|
||||||
}, 'CANCEL'),
|
|
||||||
|
|
||||||
// submit
|
|
||||||
h('button.primary', {
|
|
||||||
style: { marginLeft: '10px' },
|
|
||||||
onClick: this.revealSeedWords.bind(this),
|
|
||||||
}, 'OK'),
|
|
||||||
|
|
||||||
]),
|
|
||||||
|
|
||||||
warning && (
|
|
||||||
h('span.error', {
|
|
||||||
style: {
|
|
||||||
margin: '20px',
|
|
||||||
},
|
|
||||||
}, warning.split('-'))
|
|
||||||
),
|
|
||||||
|
|
||||||
inProgress && (
|
|
||||||
h('span.in-progress-notification', 'Generating Seed...')
|
|
||||||
),
|
|
||||||
]),
|
]),
|
||||||
|
this.state.error && h('.reveal-seed__error', this.state.error),
|
||||||
|
])
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
renderRevealSeedContent () {
|
||||||
|
const { t } = this.context
|
||||||
|
|
||||||
|
return (
|
||||||
|
h('div', [
|
||||||
|
h('label.reveal-seed__label', t('yourPrivateSeedPhrase')),
|
||||||
|
h(ExportTextContainer, {
|
||||||
|
text: this.state.seedWords,
|
||||||
|
filename: t('metamaskSeedWords'),
|
||||||
|
}),
|
||||||
|
])
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
renderFooter () {
|
||||||
|
return this.state.screen === PASSWORD_PROMPT_SCREEN
|
||||||
|
? this.renderPasswordPromptFooter()
|
||||||
|
: this.renderRevealSeedFooter()
|
||||||
|
}
|
||||||
|
|
||||||
|
renderPasswordPromptFooter () {
|
||||||
|
return (
|
||||||
|
h('.page-container__footer', [
|
||||||
|
h('button.btn-secondary--lg.page-container__footer-button', {
|
||||||
|
onClick: () => this.props.history.push(DEFAULT_ROUTE),
|
||||||
|
}, this.context.t('cancel')),
|
||||||
|
h('button.btn-primary--lg.page-container__footer-button', {
|
||||||
|
onClick: event => this.handleSubmit(event),
|
||||||
|
disabled: this.state.password === '',
|
||||||
|
}, this.context.t('next')),
|
||||||
|
])
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
renderRevealSeedFooter () {
|
||||||
|
return (
|
||||||
|
h('.page-container__footer', [
|
||||||
|
h('button.btn-secondary--lg.page-container__footer-button', {
|
||||||
|
onClick: () => this.props.history.push(DEFAULT_ROUTE),
|
||||||
|
}, this.context.t('close')),
|
||||||
])
|
])
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
render () {
|
render () {
|
||||||
return this.props.seedWords
|
return (
|
||||||
? this.renderSeed()
|
h('.page-container', [
|
||||||
: this.renderConfirmation()
|
h('.page-container__header', [
|
||||||
|
h('.page-container__title', this.context.t('revealSeedWordsTitle')),
|
||||||
|
h('.page-container__subtitle', this.context.t('revealSeedWordsDescription')),
|
||||||
|
]),
|
||||||
|
h('.page-container__content', [
|
||||||
|
this.renderWarning(),
|
||||||
|
h('.reveal-seed__content', [
|
||||||
|
this.renderContent(),
|
||||||
|
]),
|
||||||
|
]),
|
||||||
|
this.renderFooter(),
|
||||||
|
])
|
||||||
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
RevealSeedPage.propTypes = {
|
RevealSeedPage.propTypes = {
|
||||||
requestRevealSeed: PropTypes.func,
|
requestRevealSeedWords: PropTypes.func,
|
||||||
confirmSeedWords: PropTypes.func,
|
|
||||||
seedWords: PropTypes.string,
|
|
||||||
inProgress: PropTypes.bool,
|
|
||||||
history: PropTypes.object,
|
history: PropTypes.object,
|
||||||
warning: PropTypes.string,
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const mapStateToProps = state => {
|
RevealSeedPage.contextTypes = {
|
||||||
const { appState: { warning }, metamask: { seedWords } } = state
|
t: PropTypes.func,
|
||||||
|
|
||||||
return {
|
|
||||||
warning,
|
|
||||||
seedWords,
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const mapDispatchToProps = dispatch => {
|
const mapDispatchToProps = dispatch => {
|
||||||
return {
|
return {
|
||||||
requestRevealSeed: password => dispatch(requestRevealSeed(password)),
|
requestRevealSeedWords: password => dispatch(requestRevealSeedWords(password)),
|
||||||
confirmSeedWords: () => dispatch(confirmSeedWords()),
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
module.exports = connect(mapStateToProps, mapDispatchToProps)(RevealSeedPage)
|
module.exports = connect(null, mapDispatchToProps)(RevealSeedPage)
|
||||||
|
@ -8,11 +8,11 @@ const abiDecoder = require('abi-decoder')
|
|||||||
abiDecoder.addABI(abi)
|
abiDecoder.addABI(abi)
|
||||||
const inherits = require('util').inherits
|
const inherits = require('util').inherits
|
||||||
const actions = require('../../actions')
|
const actions = require('../../actions')
|
||||||
const util = require('../../util')
|
const { getSymbolAndDecimals } = require('../../token-util')
|
||||||
const ConfirmSendEther = require('./confirm-send-ether')
|
const ConfirmSendEther = require('./confirm-send-ether')
|
||||||
const ConfirmSendToken = require('./confirm-send-token')
|
const ConfirmSendToken = require('./confirm-send-token')
|
||||||
const ConfirmDeployContract = require('./confirm-deploy-contract')
|
const ConfirmDeployContract = require('./confirm-deploy-contract')
|
||||||
const Loading = require('../loading')
|
const Loading = require('../loading-screen')
|
||||||
|
|
||||||
const TX_TYPES = {
|
const TX_TYPES = {
|
||||||
DEPLOY_CONTRACT: 'deploy_contract',
|
DEPLOY_CONTRACT: 'deploy_contract',
|
||||||
@ -26,6 +26,7 @@ function mapStateToProps (state) {
|
|||||||
const {
|
const {
|
||||||
conversionRate,
|
conversionRate,
|
||||||
identities,
|
identities,
|
||||||
|
tokens: existingTokens,
|
||||||
} = state.metamask
|
} = state.metamask
|
||||||
const accounts = state.metamask.accounts
|
const accounts = state.metamask.accounts
|
||||||
const selectedAddress = state.metamask.selectedAddress || Object.keys(accounts)[0]
|
const selectedAddress = state.metamask.selectedAddress || Object.keys(accounts)[0]
|
||||||
@ -33,6 +34,7 @@ function mapStateToProps (state) {
|
|||||||
conversionRate,
|
conversionRate,
|
||||||
identities,
|
identities,
|
||||||
selectedAddress,
|
selectedAddress,
|
||||||
|
existingTokens,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -66,6 +68,7 @@ PendingTx.prototype.componentDidUpdate = function (prevProps, prevState) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
PendingTx.prototype.setTokenData = async function () {
|
PendingTx.prototype.setTokenData = async function () {
|
||||||
|
const { existingTokens } = this.props
|
||||||
const txMeta = this.gatherTxMeta()
|
const txMeta = this.gatherTxMeta()
|
||||||
const txParams = txMeta.txParams || {}
|
const txParams = txMeta.txParams || {}
|
||||||
|
|
||||||
@ -89,30 +92,15 @@ PendingTx.prototype.setTokenData = async function () {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (isTokenTransaction) {
|
if (isTokenTransaction) {
|
||||||
const token = util.getContractAtAddress(txParams.to)
|
const { symbol, decimals } = await getSymbolAndDecimals(txParams.to, existingTokens)
|
||||||
const results = await Promise.all([
|
|
||||||
token.symbol(),
|
|
||||||
token.decimals(),
|
|
||||||
])
|
|
||||||
const [ symbol, decimals ] = results
|
|
||||||
|
|
||||||
if (symbol[0] && decimals[0]) {
|
this.setState({
|
||||||
this.setState({
|
transactionType: TX_TYPES.SEND_TOKEN,
|
||||||
transactionType: TX_TYPES.SEND_TOKEN,
|
tokenAddress: txParams.to,
|
||||||
tokenAddress: txParams.to,
|
tokenSymbol: symbol,
|
||||||
tokenSymbol: symbol[0],
|
tokenDecimals: decimals,
|
||||||
tokenDecimals: decimals[0],
|
isFetching: false,
|
||||||
isFetching: false,
|
})
|
||||||
})
|
|
||||||
} else {
|
|
||||||
this.setState({
|
|
||||||
transactionType: TX_TYPES.SEND_TOKEN,
|
|
||||||
tokenAddress: txParams.to,
|
|
||||||
tokenSymbol: null,
|
|
||||||
tokenDecimals: null,
|
|
||||||
isFetching: false,
|
|
||||||
})
|
|
||||||
}
|
|
||||||
} else {
|
} else {
|
||||||
this.setState({
|
this.setState({
|
||||||
transactionType: TX_TYPES.SEND_ETHER,
|
transactionType: TX_TYPES.SEND_ETHER,
|
||||||
|
@ -89,7 +89,6 @@ CurrencyDisplay.prototype.render = function () {
|
|||||||
} = this.props
|
} = this.props
|
||||||
|
|
||||||
const valueToRender = this.getValueToRender()
|
const valueToRender = this.getValueToRender()
|
||||||
|
|
||||||
const convertedValueToRender = this.getConvertedValueToRender(valueToRender)
|
const convertedValueToRender = this.getConvertedValueToRender(valueToRender)
|
||||||
|
|
||||||
return h('div', {
|
return h('div', {
|
||||||
@ -97,22 +96,24 @@ CurrencyDisplay.prototype.render = function () {
|
|||||||
style: {
|
style: {
|
||||||
borderColor: inError ? 'red' : null,
|
borderColor: inError ? 'red' : null,
|
||||||
},
|
},
|
||||||
onClick: () => this.currencyInput.focus(),
|
onClick: () => this.currencyInput && this.currencyInput.focus(),
|
||||||
}, [
|
}, [
|
||||||
|
|
||||||
h('div.currency-display__primary-row', [
|
h('div.currency-display__primary-row', [
|
||||||
|
|
||||||
h('div.currency-display__input-wrapper', [
|
h('div.currency-display__input-wrapper', [
|
||||||
|
|
||||||
h(CurrencyInput, {
|
h(readOnly ? 'input' : CurrencyInput, {
|
||||||
className: primaryBalanceClassName,
|
className: primaryBalanceClassName,
|
||||||
value: `${valueToRender}`,
|
value: `${valueToRender}`,
|
||||||
placeholder: '0',
|
placeholder: '0',
|
||||||
readOnly,
|
readOnly,
|
||||||
onInputChange: newValue => {
|
...(!readOnly ? {
|
||||||
handleChange(this.getAmount(newValue))
|
onInputChange: newValue => {
|
||||||
},
|
handleChange(this.getAmount(newValue))
|
||||||
inputRef: input => { this.currencyInput = input },
|
},
|
||||||
|
inputRef: input => { this.currencyInput = input },
|
||||||
|
} : {}),
|
||||||
}),
|
}),
|
||||||
|
|
||||||
h('span.currency-display__currency-symbol', primaryCurrency),
|
h('span.currency-display__currency-symbol', primaryCurrency),
|
||||||
|
@ -1,8 +1,8 @@
|
|||||||
const ethUtil = require('ethereumjs-util')
|
const ethUtil = require('ethereumjs-util')
|
||||||
const { conversionUtil, multiplyCurrencies } = require('../../conversion-util')
|
const { conversionUtil, multiplyCurrencies } = require('../../conversion-util')
|
||||||
|
|
||||||
const MIN_GAS_PRICE_HEX = (100000000).toString(16)
|
const MIN_GAS_PRICE_DEC = '0'
|
||||||
const MIN_GAS_PRICE_DEC = '100000000'
|
const MIN_GAS_PRICE_HEX = (parseInt(MIN_GAS_PRICE_DEC)).toString(16)
|
||||||
const MIN_GAS_LIMIT_DEC = '21000'
|
const MIN_GAS_LIMIT_DEC = '21000'
|
||||||
const MIN_GAS_LIMIT_HEX = (parseInt(MIN_GAS_LIMIT_DEC)).toString(16)
|
const MIN_GAS_LIMIT_HEX = (parseInt(MIN_GAS_LIMIT_DEC)).toString(16)
|
||||||
|
|
||||||
|
@ -55,6 +55,10 @@ function ShapeshiftForm () {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
ShapeshiftForm.prototype.getCoinPair = function () {
|
||||||
|
return `${this.state.depositCoin.toUpperCase()}_ETH`
|
||||||
|
}
|
||||||
|
|
||||||
ShapeshiftForm.prototype.componentWillMount = function () {
|
ShapeshiftForm.prototype.componentWillMount = function () {
|
||||||
this.props.shapeShiftSubview()
|
this.props.shapeShiftSubview()
|
||||||
}
|
}
|
||||||
@ -120,14 +124,12 @@ ShapeshiftForm.prototype.renderMetadata = function (label, value) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
ShapeshiftForm.prototype.renderMarketInfo = function () {
|
ShapeshiftForm.prototype.renderMarketInfo = function () {
|
||||||
const { depositCoin } = this.state
|
|
||||||
const coinPair = `${depositCoin}_eth`
|
|
||||||
const { tokenExchangeRates } = this.props
|
const { tokenExchangeRates } = this.props
|
||||||
const {
|
const {
|
||||||
limit,
|
limit,
|
||||||
rate,
|
rate,
|
||||||
minimum,
|
minimum,
|
||||||
} = tokenExchangeRates[coinPair] || {}
|
} = tokenExchangeRates[this.getCoinPair()] || {}
|
||||||
|
|
||||||
return h('div.shapeshift-form__metadata', {}, [
|
return h('div.shapeshift-form__metadata', {}, [
|
||||||
|
|
||||||
@ -172,10 +174,9 @@ ShapeshiftForm.prototype.renderQrCode = function () {
|
|||||||
|
|
||||||
ShapeshiftForm.prototype.render = function () {
|
ShapeshiftForm.prototype.render = function () {
|
||||||
const { coinOptions, btnClass, warning } = this.props
|
const { coinOptions, btnClass, warning } = this.props
|
||||||
const { depositCoin, errorMessage, showQrCode, depositAddress } = this.state
|
const { errorMessage, showQrCode, depositAddress } = this.state
|
||||||
const coinPair = `${depositCoin}_eth`
|
|
||||||
const { tokenExchangeRates } = this.props
|
const { tokenExchangeRates } = this.props
|
||||||
const token = tokenExchangeRates[coinPair]
|
const token = tokenExchangeRates[this.getCoinPair()]
|
||||||
|
|
||||||
return h('div.shapeshift-form-wrapper', [
|
return h('div.shapeshift-form-wrapper', [
|
||||||
showQrCode
|
showQrCode
|
||||||
|
2
ui/app/components/spinner/index.js
Normal file
2
ui/app/components/spinner/index.js
Normal file
@ -0,0 +1,2 @@
|
|||||||
|
const Spinner = require('./spinner.component')
|
||||||
|
module.exports = Spinner
|
78
ui/app/components/spinner/spinner.component.js
Normal file
78
ui/app/components/spinner/spinner.component.js
Normal file
@ -0,0 +1,78 @@
|
|||||||
|
import React from 'react'
|
||||||
|
import PropTypes from 'prop-types'
|
||||||
|
|
||||||
|
const Spinner = ({ className = '', color = '#000000' }) => {
|
||||||
|
return (
|
||||||
|
<div className={`spinner ${className}`}>
|
||||||
|
<svg className="lds-spinner" width="100%" height="100%" xmlns="http://www.w3.org/2000/svg" xmlnsXlink="http://www.w3.org/1999/xlink" viewBox="0 0 100 100" preserveAspectRatio="xMidYMid" style={{background: 'none'}}>
|
||||||
|
<g transform="rotate(0 50 50)">
|
||||||
|
<rect x={45} y={0} rx={0} ry={0} width={10} height={30} fill={color}>
|
||||||
|
<animate attributeName="opacity" values="1;0" dur="1s" begin="-0.9166666666666666s" repeatCount="indefinite" />
|
||||||
|
</rect>
|
||||||
|
</g>
|
||||||
|
<g transform="rotate(30 50 50)">
|
||||||
|
<rect x={45} y={0} rx={0} ry={0} width={10} height={30} fill={color}>
|
||||||
|
<animate attributeName="opacity" values="1;0" dur="1s" begin="-0.8333333333333334s" repeatCount="indefinite" />
|
||||||
|
</rect>
|
||||||
|
</g>
|
||||||
|
<g transform="rotate(60 50 50)">
|
||||||
|
<rect x={45} y={0} rx={0} ry={0} width={10} height={30} fill={color}>
|
||||||
|
<animate attributeName="opacity" values="1;0" dur="1s" begin="-0.75s" repeatCount="indefinite" />
|
||||||
|
</rect>
|
||||||
|
</g>
|
||||||
|
<g transform="rotate(90 50 50)">
|
||||||
|
<rect x={45} y={0} rx={0} ry={0} width={10} height={30} fill={color}>
|
||||||
|
<animate attributeName="opacity" values="1;0" dur="1s" begin="-0.6666666666666666s" repeatCount="indefinite" />
|
||||||
|
</rect>
|
||||||
|
</g>
|
||||||
|
<g transform="rotate(120 50 50)">
|
||||||
|
<rect x={45} y={0} rx={0} ry={0} width={10} height={30} fill={color}>
|
||||||
|
<animate attributeName="opacity" values="1;0" dur="1s" begin="-0.5833333333333334s" repeatCount="indefinite" />
|
||||||
|
</rect>
|
||||||
|
</g>
|
||||||
|
<g transform="rotate(150 50 50)">
|
||||||
|
<rect x={45} y={0} rx={0} ry={0} width={10} height={30} fill={color}>
|
||||||
|
<animate attributeName="opacity" values="1;0" dur="1s" begin="-0.5s" repeatCount="indefinite" />
|
||||||
|
</rect>
|
||||||
|
</g>
|
||||||
|
<g transform="rotate(180 50 50)">
|
||||||
|
<rect x={45} y={0} rx={0} ry={0} width={10} height={30} fill={color}>
|
||||||
|
<animate attributeName="opacity" values="1;0" dur="1s" begin="-0.4166666666666667s" repeatCount="indefinite" />
|
||||||
|
</rect>
|
||||||
|
</g>
|
||||||
|
<g transform="rotate(210 50 50)">
|
||||||
|
<rect x={45} y={0} rx={0} ry={0} width={10} height={30} fill={color}>
|
||||||
|
<animate attributeName="opacity" values="1;0" dur="1s" begin="-0.3333333333333333s" repeatCount="indefinite" />
|
||||||
|
</rect>
|
||||||
|
</g>
|
||||||
|
<g transform="rotate(240 50 50)">
|
||||||
|
<rect x={45} y={0} rx={0} ry={0} width={10} height={30} fill={color}>
|
||||||
|
<animate attributeName="opacity" values="1;0" dur="1s" begin="-0.25s" repeatCount="indefinite" />
|
||||||
|
</rect>
|
||||||
|
</g>
|
||||||
|
<g transform="rotate(270 50 50)">
|
||||||
|
<rect x={45} y={0} rx={0} ry={0} width={10} height={30} fill={color}>
|
||||||
|
<animate attributeName="opacity" values="1;0" dur="1s" begin="-0.16666666666666666s" repeatCount="indefinite" />
|
||||||
|
</rect>
|
||||||
|
</g>
|
||||||
|
<g transform="rotate(300 50 50)">
|
||||||
|
<rect x={45} y={0} rx={0} ry={0} width={10} height={30} fill={color}>
|
||||||
|
<animate attributeName="opacity" values="1;0" dur="1s" begin="-0.08333333333333333s" repeatCount="indefinite" />
|
||||||
|
</rect>
|
||||||
|
</g>
|
||||||
|
<g transform="rotate(330 50 50)">
|
||||||
|
<rect x={45} y={0} rx={0} ry={0} width={10} height={30} fill={color}>
|
||||||
|
<animate attributeName="opacity" values="1;0" dur="1s" begin="0s" repeatCount="indefinite" />
|
||||||
|
</rect>
|
||||||
|
</g>
|
||||||
|
</svg>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
Spinner.propTypes = {
|
||||||
|
className: PropTypes.string,
|
||||||
|
color: PropTypes.string,
|
||||||
|
}
|
||||||
|
|
||||||
|
module.exports = Spinner
|
@ -13,7 +13,7 @@ const SignatureRequest = require('./components/signature-request')
|
|||||||
// const PendingMsg = require('./components/pending-msg')
|
// const PendingMsg = require('./components/pending-msg')
|
||||||
// const PendingPersonalMsg = require('./components/pending-personal-msg')
|
// const PendingPersonalMsg = require('./components/pending-personal-msg')
|
||||||
// const PendingTypedMsg = require('./components/pending-typed-msg')
|
// const PendingTypedMsg = require('./components/pending-typed-msg')
|
||||||
const Loading = require('./components/loading')
|
const Loading = require('./components/loading-screen')
|
||||||
const { DEFAULT_ROUTE } = require('./routes')
|
const { DEFAULT_ROUTE } = require('./routes')
|
||||||
|
|
||||||
module.exports = compose(
|
module.exports = compose(
|
||||||
|
@ -61,3 +61,5 @@
|
|||||||
@import './welcome-screen.scss';
|
@import './welcome-screen.scss';
|
||||||
|
|
||||||
@import './sender-to-recipient.scss';
|
@import './sender-to-recipient.scss';
|
||||||
|
|
||||||
|
@import '../../../components/export-text-container/export-text-container.scss';
|
||||||
|
@ -26,4 +26,25 @@
|
|||||||
width: 100vw;
|
width: 100vw;
|
||||||
margin-top: 0;
|
margin-top: 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
&__container {
|
||||||
|
position: absolute;
|
||||||
|
top: 33%;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
justify-content: center;
|
||||||
|
align-items: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
&__message {
|
||||||
|
margin-top: 32px;
|
||||||
|
font-weight: 400;
|
||||||
|
font-size: 20px;
|
||||||
|
color: $manatee;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.spinner {
|
||||||
|
height: 58px;
|
||||||
|
width: 58px;
|
||||||
}
|
}
|
||||||
|
@ -1 +1,3 @@
|
|||||||
@import './unlock.scss';
|
@import './unlock.scss';
|
||||||
|
|
||||||
|
@import './reveal-seed.scss';
|
||||||
|
17
ui/app/css/itcss/components/pages/reveal-seed.scss
Normal file
17
ui/app/css/itcss/components/pages/reveal-seed.scss
Normal file
@ -0,0 +1,17 @@
|
|||||||
|
.reveal-seed {
|
||||||
|
&__content {
|
||||||
|
padding: 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
&__label {
|
||||||
|
padding-bottom: 10px;
|
||||||
|
font-weight: 400;
|
||||||
|
display: inline-block;
|
||||||
|
}
|
||||||
|
|
||||||
|
&__error {
|
||||||
|
color: $crimson;
|
||||||
|
font-size: 14px;
|
||||||
|
padding-top: 5px;
|
||||||
|
}
|
||||||
|
}
|
@ -207,6 +207,27 @@ input.large-input {
|
|||||||
&__content {
|
&__content {
|
||||||
height: 100%;
|
height: 100%;
|
||||||
overflow-y: auto;
|
overflow-y: auto;
|
||||||
|
min-height: 250px;
|
||||||
|
max-height: 400px;
|
||||||
|
}
|
||||||
|
|
||||||
|
&__warning-container {
|
||||||
|
background: $linen;
|
||||||
|
padding: 20px;
|
||||||
|
display: flex;
|
||||||
|
align-items: start;
|
||||||
|
}
|
||||||
|
|
||||||
|
&__warning-message {
|
||||||
|
padding-left: 15px;
|
||||||
|
}
|
||||||
|
|
||||||
|
&__warning-title {
|
||||||
|
font-weight: 500;
|
||||||
|
}
|
||||||
|
|
||||||
|
&__warning-icon {
|
||||||
|
padding-top: 5px;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -237,3 +258,49 @@ input.large-input {
|
|||||||
border-radius: 0;
|
border-radius: 0;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@media screen and (min-width: 576px) {
|
||||||
|
.page-container {
|
||||||
|
height: 600px;
|
||||||
|
flex: 0 0 auto;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.input-label {
|
||||||
|
padding-bottom: 10px;
|
||||||
|
font-weight: 400;
|
||||||
|
display: inline-block;
|
||||||
|
}
|
||||||
|
|
||||||
|
input.form-control {
|
||||||
|
padding-left: 10px;
|
||||||
|
font-size: 14px;
|
||||||
|
height: 40px;
|
||||||
|
border: 1px solid $alto;
|
||||||
|
border-radius: 3px;
|
||||||
|
width: 100%;
|
||||||
|
|
||||||
|
&::-webkit-input-placeholder {
|
||||||
|
font-weight: 100;
|
||||||
|
color: $dusty-gray;
|
||||||
|
}
|
||||||
|
|
||||||
|
&::-moz-placeholder {
|
||||||
|
font-weight: 100;
|
||||||
|
color: $dusty-gray;
|
||||||
|
}
|
||||||
|
|
||||||
|
&:-ms-input-placeholder {
|
||||||
|
font-weight: 100;
|
||||||
|
color: $dusty-gray;
|
||||||
|
}
|
||||||
|
|
||||||
|
&:-moz-placeholder {
|
||||||
|
font-weight: 100;
|
||||||
|
color: $dusty-gray;
|
||||||
|
}
|
||||||
|
|
||||||
|
&--error {
|
||||||
|
border: 1px solid $monzo;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
@ -54,6 +54,7 @@ $saffron: #f6c343;
|
|||||||
$dodger-blue: #3099f2;
|
$dodger-blue: #3099f2;
|
||||||
$zumthor: #edf7ff;
|
$zumthor: #edf7ff;
|
||||||
$ecstasy: #f7861c;
|
$ecstasy: #f7861c;
|
||||||
|
$linen: #fdf4f4;
|
||||||
|
|
||||||
/*
|
/*
|
||||||
Z-Indicies
|
Z-Indicies
|
||||||
|
@ -1,138 +0,0 @@
|
|||||||
const inherits = require('util').inherits
|
|
||||||
const Component = require('react').Component
|
|
||||||
const PropTypes = require('prop-types')
|
|
||||||
const connect = require('react-redux').connect
|
|
||||||
const h = require('react-hyperscript')
|
|
||||||
const actions = require('../../../actions')
|
|
||||||
const { withRouter } = require('react-router-dom')
|
|
||||||
const { compose } = require('recompose')
|
|
||||||
const {
|
|
||||||
DEFAULT_ROUTE,
|
|
||||||
INITIALIZE_BACKUP_PHRASE_ROUTE,
|
|
||||||
} = require('../../../routes')
|
|
||||||
|
|
||||||
RevealSeedConfirmation.contextTypes = {
|
|
||||||
t: PropTypes.func,
|
|
||||||
}
|
|
||||||
|
|
||||||
module.exports = compose(
|
|
||||||
withRouter,
|
|
||||||
connect(mapStateToProps)
|
|
||||||
)(RevealSeedConfirmation)
|
|
||||||
|
|
||||||
|
|
||||||
inherits(RevealSeedConfirmation, Component)
|
|
||||||
function RevealSeedConfirmation () {
|
|
||||||
Component.call(this)
|
|
||||||
}
|
|
||||||
|
|
||||||
function mapStateToProps (state) {
|
|
||||||
return {
|
|
||||||
warning: state.appState.warning,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
RevealSeedConfirmation.prototype.render = function () {
|
|
||||||
const props = this.props
|
|
||||||
|
|
||||||
return (
|
|
||||||
|
|
||||||
h('.initialize-screen.flex-column.flex-center.flex-grow', {
|
|
||||||
style: { maxWidth: '420px' },
|
|
||||||
}, [
|
|
||||||
|
|
||||||
h('h3.flex-center.text-transform-uppercase', {
|
|
||||||
style: {
|
|
||||||
background: '#EBEBEB',
|
|
||||||
color: '#AEAEAE',
|
|
||||||
marginBottom: 24,
|
|
||||||
width: '100%',
|
|
||||||
fontSize: '20px',
|
|
||||||
padding: 6,
|
|
||||||
},
|
|
||||||
}, [
|
|
||||||
'Reveal Seed Words',
|
|
||||||
]),
|
|
||||||
|
|
||||||
h('.div', {
|
|
||||||
style: {
|
|
||||||
display: 'flex',
|
|
||||||
flexDirection: 'column',
|
|
||||||
padding: '20px',
|
|
||||||
justifyContent: 'center',
|
|
||||||
},
|
|
||||||
}, [
|
|
||||||
|
|
||||||
h('h4', this.context.t('revealSeedWordsWarning')),
|
|
||||||
|
|
||||||
// confirmation
|
|
||||||
h('input.large-input.letter-spacey', {
|
|
||||||
type: 'password',
|
|
||||||
id: 'password-box',
|
|
||||||
placeholder: this.context.t('enterPasswordConfirm'),
|
|
||||||
onKeyPress: this.checkConfirmation.bind(this),
|
|
||||||
style: {
|
|
||||||
width: 260,
|
|
||||||
marginTop: '12px',
|
|
||||||
},
|
|
||||||
}),
|
|
||||||
|
|
||||||
h('.flex-row.flex-start', {
|
|
||||||
style: {
|
|
||||||
marginTop: 30,
|
|
||||||
width: '50%',
|
|
||||||
},
|
|
||||||
}, [
|
|
||||||
// cancel
|
|
||||||
h('button.primary', {
|
|
||||||
onClick: this.goHome.bind(this),
|
|
||||||
}, 'CANCEL'),
|
|
||||||
|
|
||||||
// submit
|
|
||||||
h('button.primary', {
|
|
||||||
style: { marginLeft: '10px' },
|
|
||||||
onClick: this.revealSeedWords.bind(this),
|
|
||||||
}, 'OK'),
|
|
||||||
|
|
||||||
]),
|
|
||||||
|
|
||||||
(props.warning) && (
|
|
||||||
h('span.error', {
|
|
||||||
style: {
|
|
||||||
margin: '20px',
|
|
||||||
},
|
|
||||||
}, props.warning.split('-'))
|
|
||||||
),
|
|
||||||
|
|
||||||
props.inProgress && (
|
|
||||||
h('span.in-progress-notification', this.context.t('generatingSeed'))
|
|
||||||
),
|
|
||||||
]),
|
|
||||||
])
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
RevealSeedConfirmation.prototype.componentDidMount = function () {
|
|
||||||
document.getElementById('password-box').focus()
|
|
||||||
}
|
|
||||||
|
|
||||||
RevealSeedConfirmation.prototype.goHome = function () {
|
|
||||||
this.props.dispatch(actions.showConfigPage(false))
|
|
||||||
this.props.dispatch(actions.confirmSeedWords())
|
|
||||||
.then(() => this.props.history.push(DEFAULT_ROUTE))
|
|
||||||
}
|
|
||||||
|
|
||||||
// create vault
|
|
||||||
|
|
||||||
RevealSeedConfirmation.prototype.checkConfirmation = function (event) {
|
|
||||||
if (event.key === 'Enter') {
|
|
||||||
event.preventDefault()
|
|
||||||
this.revealSeedWords()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
RevealSeedConfirmation.prototype.revealSeedWords = function () {
|
|
||||||
var password = document.getElementById('password-box').value
|
|
||||||
this.props.dispatch(actions.requestRevealSeed(password))
|
|
||||||
.then(() => this.props.history.push(INITIALIZE_BACKUP_PHRASE_ROUTE))
|
|
||||||
}
|
|
@ -493,7 +493,7 @@ SendTransactionScreen.prototype.renderFooter = function () {
|
|||||||
history,
|
history,
|
||||||
} = this.props
|
} = this.props
|
||||||
|
|
||||||
const missingTokenBalance = selectedToken && !tokenBalance
|
const missingTokenBalance = selectedToken && (tokenBalance === null || tokenBalance === undefined)
|
||||||
const noErrors = !amountError && toError === null
|
const noErrors = !amountError && toError === null
|
||||||
|
|
||||||
return h('div.page-container__footer', [
|
return h('div.page-container__footer', [
|
||||||
|
@ -1,14 +1,6 @@
|
|||||||
const abi = require('human-standard-token-abi')
|
const util = require('./util')
|
||||||
const Eth = require('ethjs-query')
|
|
||||||
const EthContract = require('ethjs-contract')
|
|
||||||
|
|
||||||
const tokenInfoGetter = function () {
|
|
||||||
if (typeof global.ethereumProvider === 'undefined') return
|
|
||||||
|
|
||||||
const eth = new Eth(global.ethereumProvider)
|
|
||||||
const contract = new EthContract(eth)
|
|
||||||
const TokenContract = contract(abi)
|
|
||||||
|
|
||||||
|
function tokenInfoGetter () {
|
||||||
const tokens = {}
|
const tokens = {}
|
||||||
|
|
||||||
return async (address) => {
|
return async (address) => {
|
||||||
@ -16,21 +8,38 @@ const tokenInfoGetter = function () {
|
|||||||
return tokens[address]
|
return tokens[address]
|
||||||
}
|
}
|
||||||
|
|
||||||
const contract = TokenContract.at(address)
|
tokens[address] = await getSymbolAndDecimals(address)
|
||||||
|
|
||||||
const result = await Promise.all([
|
|
||||||
contract.symbol(),
|
|
||||||
contract.decimals(),
|
|
||||||
])
|
|
||||||
|
|
||||||
const [ symbol = [], decimals = [] ] = result
|
|
||||||
|
|
||||||
tokens[address] = { symbol: symbol[0], decimals: decimals[0] }
|
|
||||||
|
|
||||||
return tokens[address]
|
return tokens[address]
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async function getSymbolAndDecimals (tokenAddress, existingTokens = []) {
|
||||||
|
const existingToken = existingTokens.find(({ address }) => tokenAddress === address)
|
||||||
|
if (existingToken) {
|
||||||
|
return existingToken
|
||||||
|
}
|
||||||
|
|
||||||
|
let result = []
|
||||||
|
try {
|
||||||
|
const token = util.getContractAtAddress(tokenAddress)
|
||||||
|
|
||||||
|
result = await Promise.all([
|
||||||
|
token.symbol(),
|
||||||
|
token.decimals(),
|
||||||
|
])
|
||||||
|
} catch (err) {
|
||||||
|
console.log(`symbol() and decimal() calls for token at address ${tokenAddress} resulted in error:`, err)
|
||||||
|
}
|
||||||
|
|
||||||
|
const [ symbol = [], decimals = [] ] = result
|
||||||
|
|
||||||
|
return {
|
||||||
|
symbol: symbol[0] || null,
|
||||||
|
decimals: decimals[0] && decimals[0].toString() || null,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
function calcTokenAmount (value, decimals) {
|
function calcTokenAmount (value, decimals) {
|
||||||
const multiplier = Math.pow(10, Number(decimals || 0))
|
const multiplier = Math.pow(10, Number(decimals || 0))
|
||||||
const amount = Number(value / multiplier)
|
const amount = Number(value / multiplier)
|
||||||
@ -42,4 +51,5 @@ function calcTokenAmount (value, decimals) {
|
|||||||
module.exports = {
|
module.exports = {
|
||||||
tokenInfoGetter,
|
tokenInfoGetter,
|
||||||
calcTokenAmount,
|
calcTokenAmount,
|
||||||
|
getSymbolAndDecimals,
|
||||||
}
|
}
|
||||||
|
@ -11,8 +11,9 @@ const getMessage = (locale, key, substitutions) => {
|
|||||||
const { current, en } = locale
|
const { current, en } = locale
|
||||||
const entry = current[key] || en[key]
|
const entry = current[key] || en[key]
|
||||||
if (!entry) {
|
if (!entry) {
|
||||||
log.error(`Translator - Unable to find value for "${key}"`)
|
|
||||||
// throw new Error(`Translator - Unable to find value for "${key}"`)
|
// throw new Error(`Translator - Unable to find value for "${key}"`)
|
||||||
|
log.error(`Translator - Unable to find value for "${key}"`)
|
||||||
|
return `[${key}]`
|
||||||
}
|
}
|
||||||
let phrase = entry.message
|
let phrase = entry.message
|
||||||
// perform substitutions
|
// perform substitutions
|
||||||
|
Loading…
Reference in New Issue
Block a user