1
0
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:
Thomas 2018-05-03 14:38:49 -07:00
commit 025d8e7983
81 changed files with 4497 additions and 1905 deletions

View File

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

View File

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

View File

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

View File

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

View 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

View File

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

View File

@ -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__",

View File

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

View File

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

View File

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

View File

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

View 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
![transaction-flow](../../../../docs/transaction-flow.png)
## 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
}
```

View File

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

View File

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

View 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
]
}

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View 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

Binary file not shown.

After

Width:  |  Height:  |  Size: 138 KiB

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

File diff suppressed because it is too large Load Diff

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -0,0 +1,2 @@
const ExportTextContainer = require('./export-text-container.component')
module.exports = ExportTextContainer

View File

@ -0,0 +1,2 @@
const LoadingScreen = require('./loading-screen.component')
module.exports = LoadingScreen

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -0,0 +1,2 @@
const Spinner = require('./spinner.component')
module.exports = Spinner

View 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

View File

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

View File

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

View File

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

View File

@ -1 +1,3 @@
@import './unlock.scss'; @import './unlock.scss';
@import './reveal-seed.scss';

View 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;
}
}

View File

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

View File

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

View File

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

View File

@ -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', [

View File

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

View File

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