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:
requires:
- prep-deps-npm
- test-deps:
requires:
- prep-deps-npm
- test-e2e-chrome:
requires:
- prep-deps-npm
@ -48,6 +51,7 @@ workflows:
- all-tests-pass:
requires:
- test-lint
- test-deps
- test-unit
- test-e2e-chrome
- test-e2e-firefox
@ -151,6 +155,17 @@ jobs:
name: Test
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:
docker:
- image: circleci/node:8-browsers

View File

@ -2,11 +2,14 @@
## Current Master
## 4.6.0 Thu Apr 26 2018
- Correctly format currency conversion for locally selected preferred currency.
- Improved performance of 3D fox logo.
- Fetch token prices based on contract address, not symbol
- Fix bug that prevents setting language locale in settings.
- Show checksum addresses throughout the UI
- Allow transactions with a 0 gwei gas price
## 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
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 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
Make sure you get a `:thumbsup`, `:+1`, or `LGTM` from another collaborator before merging.

View File

@ -98,6 +98,9 @@
"clickCopy": {
"message": "Click to Copy"
},
"close": {
"message": "Close"
},
"confirm": {
"message": "Confirm"
},
@ -259,6 +262,9 @@
"enterPasswordConfirm": {
"message": "Enter your password to confirm"
},
"enterPasswordContinue": {
"message": "Enter password to continue"
},
"passwordNotLongEnough": {
"message": "Password not long enough"
},
@ -331,6 +337,9 @@
"gasPriceRequired": {
"message": "Gas Price Required"
},
"generatingTransaction": {
"message": "Generating transaction"
},
"getEther": {
"message": "Get Ether"
},
@ -476,6 +485,9 @@
"metamaskDescription": {
"message": "MetaMask is a secure identity vault for Ethereum."
},
"metamaskSeedWords": {
"message": "MetaMask Seed Words"
},
"min": {
"message": "Minimum"
},
@ -549,6 +561,9 @@
"message": "or",
"description": "choice between creating or importing a new account"
},
"password": {
"message": "Password"
},
"passwordCorrect": {
"message": "Please make sure your password is correct."
},
@ -634,8 +649,17 @@
"revealSeedWords": {
"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": {
"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": {
"message": "Revert"
@ -677,6 +701,9 @@
"reprice_subtitle": {
"message": "Increase your gas price to attempt to overwrite and speed up your transaction"
},
"saveAsCsvFile": {
"message": "Save as CSV File"
},
"saveAsFile": {
"message": "Save as File",
"description": "Account export process"
@ -909,7 +936,7 @@
"youSign": {
"message": "You are signing"
},
"generatingTransaction": {
"message": "Generating transaction"
"yourPrivateSeedPhrase": {
"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"?>
<!-- Generator: Adobe Illustrator 16.0.0, SVG Export Plug-In . SVG Version: 6.00 Build 0) -->
<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">
<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"
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">
<g>
<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
141.568,20.145 141.955,20.145 141.955,20.15 157.006,20.15 157.006,20.145 157.551,20.145 "/>
<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
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
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
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
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>
<?xml version="1.0" encoding="UTF-8"?>
<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">
<!-- Generator: sketchtool 49.3 (51167) - http://www.bohemiancoding.com/sketch -->
<title>50559280-0739-419A-8E87-3CDD16A6996A</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(-212.000000, -379.000000)" stroke="#259DE5" stroke-width="2">
<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-3" transform="translate(174.000000, 11.000000)">
<g id="Group-4">
<g id="download">
<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__",
"short_name": "__MSG_appName__",
"version": "4.5.5",
"version": "4.6.0",
"manifest_version": 2,
"author": "https://metamask.io",
"description": "__MSG_appDescription__",

View File

@ -261,7 +261,11 @@ function setupController (initState, initLangCode) {
controller.txController.on(`tx:status-update`, (txId, status) => {
if (status !== 'failed') return
const txMeta = controller.txController.txStateManager.getTx(txId)
try {
reportFailedTxToSentry({ raven, txMeta })
} catch (e) {
console.error(e)
}
})
// setup state persistence

View File

@ -174,6 +174,7 @@ function blacklistedDomainCheck () {
'uscourts.gov',
'dropbox.com',
'webbyawards.com',
'cdn.shopify.com/s/javascripts/tricorder/xtld-read-only-frame.html',
]
var currentUrl = window.location.href
var currentRegex

View File

@ -1,7 +1,7 @@
const assert = require('assert')
const EventEmitter = require('events')
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 ObservableStore = require('obs-store')
const ComposedStore = require('obs-store/lib/composed')

View File

@ -1,4 +1,5 @@
const ObservableStore = require('obs-store')
const { warn } = require('loglevel')
// By default, poll every 3 minutes
const DEFAULT_INTERVAL = 180 * 1000
@ -39,10 +40,13 @@ class TokenRatesController {
*/
async fetchExchangeRate (address) {
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()
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 Transaction = require('ethereumjs-tx')
const EthQuery = require('ethjs-query')
const TransactionStateManager = require('../lib/tx-state-manager')
const TxGasUtil = require('../lib/tx-gas-utils')
const PendingTransactionTracker = require('../lib/pending-tx-tracker')
const NonceTracker = require('../lib/nonce-tracker')
const TransactionStateManager = require('./tx-state-manager')
const TxGasUtil = require('./tx-gas-utils')
const PendingTransactionTracker = require('./pending-tx-tracker')
const NonceTracker = require('./nonce-tracker')
const txUtils = require('./lib/util')
const log = require('loglevel')
/*
/**
Transaction Controller is an aggregate of sub-controllers and trackers
composing them in a way to be exposed to the metamask controller
- txStateManager
<br>- txStateManager
responsible for the state of a transaction and
storing the transaction
- pendingTxTracker
<br>- pendingTxTracker
watching blocks for transactions to be include
and emitting confirmed events
- txGasUtil
<br>- txGasUtil
gas calculations and safety buffering
- nonceTracker
<br>- nonceTracker
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) {
super()
this.networkStore = opts.networkStore || new ObservableStore({})
@ -38,45 +52,19 @@ module.exports = class TransactionController extends EventEmitter {
this.query = new EthQuery(this.provider)
this.txGasUtil = new TxGasUtil(this.provider)
this._mapMethods()
this.txStateManager = new TransactionStateManager({
initState: opts.initState,
txHistoryLimit: opts.txHistoryLimit,
getNetwork: this.getNetwork.bind(this),
})
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._onBootCleanUp()
this.store = this.txStateManager.store
this.txStateManager.on('tx:status-update', this.emit.bind(this, 'tx:status-update'))
this.nonceTracker = new NonceTracker({
provider: this.provider,
getPendingTransactions: this.txStateManager.getPendingTransactions.bind(this.txStateManager),
getConfirmedTransactions: (address) => {
return this.txStateManager.getFilteredTxList({
from: address,
status: 'confirmed',
err: undefined,
})
},
getConfirmedTransactions: this.txStateManager.getConfirmedTransactions.bind(this.txStateManager),
})
this.pendingTxTracker = new PendingTransactionTracker({
@ -88,60 +76,14 @@ module.exports = class TransactionController extends EventEmitter {
})
this.txStateManager.store.subscribe(() => this.emit('update:badge'))
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))
this._setupListners()
// memstore is computed from a few different stores
this._updateMemstore()
this.txStateManager.store.subscribe(() => this._updateMemstore())
this.networkStore.subscribe(() => this._updateMemstore())
this.preferencesStore.subscribe(() => this._updateMemstore())
}
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)
}
/** @returns {number} the chainId*/
getChainId () {
const networkState = this.networkStore.getState()
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
/**
Adds a tx to the txlist
@emits ${txMeta.id}:unapproved
*/
addTx (txMeta) {
this.txStateManager.addTx(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 = {}) {
log.debug(`MetaMaskController newUnapprovedTransaction ${JSON.stringify(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) {
// validate
const normalizedTxParams = this._normalizeTxParams(txParams)
this._validateTxParams(normalizedTxParams)
const normalizedTxParams = txUtils.normalizeTxParams(txParams)
txUtils.validateTxParams(normalizedTxParams)
// construct txMeta
let txMeta = this.txStateManager.generateTxMeta({ txParams: normalizedTxParams })
this.addTx(txMeta)
this.emit('newUnapprovedTx', txMeta)
// add default tx params
try {
txMeta = await this.addTxDefaults(txMeta)
txMeta = await this.addTxGasDefaults(txMeta)
} catch (error) {
console.log(error)
this.txStateManager.setTxStatusFailed(txMeta.id, error)
@ -206,21 +169,33 @@ module.exports = class TransactionController extends EventEmitter {
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
// ensure value
txParams.value = txParams.value ? ethUtil.addHexPrefix(txParams.value) : '0x0'
txMeta.gasPriceSpecified = Boolean(txParams.gasPrice)
let gasPrice = txParams.gasPrice
if (!gasPrice) {
gasPrice = this.getGasPrice ? this.getGasPrice() : await this.query.gasPrice()
}
txParams.gasPrice = ethUtil.addHexPrefix(gasPrice.toString(16))
txParams.value = txParams.value || '0x0'
// set gasLimit
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) {
const originalTxMeta = this.txStateManager.getTx(originalTxId)
const lastGasPrice = originalTxMeta.txParams.gasPrice
@ -234,15 +209,31 @@ module.exports = class TransactionController extends EventEmitter {
return txMeta
}
/**
updates the txMeta in the txStateManager
@param txMeta {Object} - the updated txMeta
*/
async updateTransaction (txMeta) {
this.txStateManager.updateTx(txMeta, 'confTx: user updated transaction')
}
/**
updates and approves the transaction
@param txMeta {Object}
*/
async updateAndApproveTransaction (txMeta) {
this.txStateManager.updateTx(txMeta, 'confTx: user approved transaction')
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) {
let nonceLock
try {
@ -274,7 +265,11 @@ module.exports = class TransactionController extends EventEmitter {
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) {
const txMeta = this.txStateManager.getTx(txId)
// add network/chain id
@ -290,6 +285,12 @@ module.exports = class TransactionController extends EventEmitter {
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) {
const txMeta = this.txStateManager.getTx(txId)
txMeta.rawTx = rawTx
@ -299,11 +300,20 @@ module.exports = class TransactionController extends EventEmitter {
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) {
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) {
// Add the tx hash to the persisted meta-tx object
const txMeta = this.txStateManager.getTx(txId)
@ -314,63 +324,92 @@ module.exports = class TransactionController extends EventEmitter {
//
// PRIVATE METHODS
//
_normalizeTxParams (txParams) {
// functions that handle normalizing of that key in txParams
const whiteList = {
from: from => ethUtil.addHexPrefix(from).toLowerCase(),
to: to => ethUtil.addHexPrefix(txParams.to).toLowerCase(),
nonce: nonce => ethUtil.addHexPrefix(nonce),
value: value => ethUtil.addHexPrefix(value),
data: data => ethUtil.addHexPrefix(data),
gas: gas => ethUtil.addHexPrefix(gas),
gasPrice: gasPrice => ethUtil.addHexPrefix(gasPrice),
/** 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)
}
// apply only keys in the whiteList
const normalizedTxParams = {}
Object.keys(whiteList).forEach((key) => {
if (txParams[key]) normalizedTxParams[key] = whiteList[key](txParams[key])
/**
If transaction controller was rebooted with transactions that are uncompleted
in steps of the transaction signing or user confirmation process it will either
transition txMetas to a failed state or try to redo those tasks.
*/
_onBootCleanUp () {
this.txStateManager.getFilteredTxList({
status: 'unapproved',
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)
this._validateRecipient(txParams)
if ('value' in txParams) {
const value = txParams.value.toString()
if (value.includes('-')) {
throw new Error(`Invalid transaction value of ${txParams.value} not a positive number.`)
/**
is called in constructor applies the listeners for pendingTxTracker txStateManager
and blockTracker
*/
_setupListners () {
this.txStateManager.on('tx:status-update', this.emit.bind(this, 'tx:status-update'))
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`)
if (!ethUtil.isValidAddress(txParams.from)) throw new Error('Invalid from address')
}
_validateRecipient (txParams) {
if (txParams.to === '0x' || txParams.to === null ) {
if (txParams.data) {
delete txParams.to
} else {
throw new Error('Invalid recipient address')
}
} else if ( txParams.to !== undefined && !ethUtil.isValidAddress(txParams.to) ) {
throw new Error('Invalid recipient address')
}
return txParams
}
/**
Sets other txMeta statuses to dropped if the txMeta that has been confirmed has other transactions
in the list have the same nonce
@param txId {Number} - the txId of the transaction that has been confirmed in a block
*/
_markNonceDuplicatesDropped (txId) {
this.txStateManager.setTxStatusConfirmed(txId)
// get the confirmed transactions nonce and from address
const txMeta = this.txStateManager.getTx(txId)
const { nonce, from } = txMeta.txParams
@ -385,6 +424,9 @@ module.exports = class TransactionController extends EventEmitter {
})
}
/**
Updates the memStore in transaction controller
*/
_updateMemstore () {
const unapprovedTxs = this.txStateManager.getUnapprovedTxList()
const selectedAddressTxList = this.txStateManager.getFilteredTxList({
@ -394,3 +436,5 @@ module.exports = class TransactionController extends EventEmitter {
this.memStore.updateState({ unapprovedTxs, selectedAddressTxList })
}
}
module.exports = TransactionController

View File

@ -1,6 +1,6 @@
const jsonDiffer = require('fast-json-patch')
const clone = require('clone')
/** @module*/
module.exports = {
generateHistoryEntry,
replayHistory,
@ -8,7 +8,11 @@ module.exports = {
migrateFromSnapshotsToDiffs,
}
/**
converts non-initial history entries into diffs
@param longHistory {array}
@returns {array}
*/
function migrateFromSnapshotsToDiffs (longHistory) {
return (
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) {
const entry = jsonDiffer.compare(previousState, newState)
// 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
}
/**
Recovers previous txMeta state obj
@return {object}
*/
function replayHistory (_shortHistory) {
const shortHistory = clone(_shortHistory)
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) {
// create txMeta snapshot for history
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 assert = require('assert')
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 {
constructor ({ provider, getPendingTransactions, getConfirmedTransactions }) {
@ -12,6 +20,9 @@ class NonceTracker {
this.lockMap = {}
}
/**
@returns {Promise<Object>} with the key releaseLock (the gloabl mutex)
*/
async getGlobalLock () {
const globalMutex = this._lookupMutex('global')
// await global mutex free
@ -19,8 +30,20 @@ class NonceTracker {
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) {
// await global mutex free
await this._globalMutexFree()
@ -123,6 +146,17 @@ class NonceTracker {
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) {
const nonces = txList.map((txMeta) => {
const nonce = txMeta.txParams.nonce
@ -140,6 +174,10 @@ class NonceTracker {
// this is a hotfix for the fact that the blockTracker will
// change when the network changes
/**
@returns {Object} the current blockTracker
*/
_getBlockTracker () {
return this.provider._blockTracker
}

View File

@ -1,23 +1,24 @@
const EventEmitter = require('events')
const log = require('loglevel')
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
<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) {
super()
this.query = new EthQuery(config.provider)
@ -29,8 +30,13 @@ module.exports = class PendingTransactionTracker extends EventEmitter {
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) {
const signedTxList = this.getPendingTransactions()
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 }) {
// check pending transactions on start
if (!oldBlock) {
@ -63,7 +74,11 @@ module.exports = class PendingTransactionTracker extends EventEmitter {
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) {
const pending = this.getPendingTransactions()
// 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) {
if (!txMeta.firstRetryBlockNumber) {
this.emit('tx:block-update', txMeta, latestBlockNumber)
@ -123,7 +145,13 @@ module.exports = class PendingTransactionTracker extends EventEmitter {
this.emit('tx:retry', txMeta)
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) {
const txHash = txMeta.hash
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 () {
const signedTxList = this.getPendingTransactions()
// 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 {
await Promise.all(signedTxList.map((txMeta) => this._checkPendingTx(txMeta)))
} catch (err) {
console.error('PendingTransactionWatcher - Error updating pending transactions')
console.error(err)
log.error('PendingTransactionWatcher - Error updating pending transactions')
log.error(err)
}
nonceGlobalLock.releaseLock()
}
/**
checks to see if a confirmed txMeta has the same nonce
@param txMeta {Object} - txMeta object
@returns {boolean}
*/
async _checkIfNonceIsTaken (txMeta) {
const address = txMeta.txParams.from
const completed = this.getCompletedTransactions(address)
@ -185,5 +219,6 @@ module.exports = class PendingTransactionTracker extends EventEmitter {
})
return sameNonce.length > 0
}
}
module.exports = PendingTransactionTracker

View File

@ -3,22 +3,27 @@ const {
hexToBn,
BnMultiplyByFraction,
bnToHex,
} = require('./util')
} = require('../../lib/util')
const { addHexPrefix } = require('ethereumjs-util')
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
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) {
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) {
const block = await this.query.getBlockByNumber('latest', true)
let estimatedGasHex
@ -38,6 +43,12 @@ module.exports = class TxGasUtil {
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) {
const txParams = txMeta.txParams
@ -70,6 +81,12 @@ module.exports = class TxGasUtil {
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) {
txMeta.estimatedGas = addHexPrefix(estimatedGasHex)
const txParams = txMeta.txParams
@ -87,6 +104,13 @@ module.exports = class TxGasUtil {
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) {
const initialGasLimitBn = hexToBn(initialGasLimitHex)
const blockGasLimitBn = hexToBn(blockGasLimitHex)
@ -101,3 +125,5 @@ module.exports = class TxGasUtil {
return bnToHex(upperGasLimitBn)
}
}
module.exports = TxGasUtil

View File

@ -1,22 +1,33 @@
const extend = require('xtend')
const EventEmitter = require('events')
const ObservableStore = require('obs-store')
const createId = require('./random-id')
const ethUtil = require('ethereumjs-util')
const txStateHistoryHelper = require('./tx-state-history-helper')
// STATUS METHODS
// statuses:
// - `'unapproved'` the user has not responded
// - `'rejected'` the user has responded no!
// - `'approved'` the user has approved the tx
// - `'signed'` the tx is signed
// - `'submitted'` the tx is sent to a server
// - `'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
module.exports = class TransactionStateManager extends EventEmitter {
const txStateHistoryHelper = require('./lib/tx-state-history-helper')
const createId = require('../../lib/random-id')
const { getFinalStates } = require('./lib/util')
/**
TransactionStateManager is responsible for the state of a transaction and
storing the transaction
it also has some convenience methods for finding subsets of transactions
*
*STATUS METHODS
<br>statuses:
<br> - `'unapproved'` the user has not responded
<br> - `'rejected'` the user has responded no!
<br> - `'approved'` the user has approved the tx
<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 }) {
super()
@ -28,6 +39,10 @@ module.exports = class TransactionStateManager extends EventEmitter {
this.getNetwork = getNetwork
}
/**
@param opts {object} - the object to use when overwriting defaults
@returns {txMeta} the default txMeta object
*/
generateTxMeta (opts) {
return extend({
id: createId(),
@ -38,17 +53,25 @@ module.exports = class TransactionStateManager extends EventEmitter {
}, opts)
}
/**
@returns {array} of txMetas that have been filtered for only the current network
*/
getTxList () {
const network = this.getNetwork()
const fullTxList = this.getFullTxList()
return fullTxList.filter((txMeta) => txMeta.metamaskNetworkId === network)
}
/**
@returns {array} of all the txMetas in store
*/
getFullTxList () {
return this.store.getState().transactions
}
// Returns the tx list
/**
@returns {array} the tx list whos status is unapproved
*/
getUnapprovedTxList () {
const txList = this.getTxsByMetaData('status', 'unapproved')
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) {
const opts = { status: 'submitted' }
if (address) opts.from = address
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) {
const opts = { status: 'confirmed' }
if (address) opts.from = address
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) {
this.once(`${txMeta.id}:signed`, function (txId) {
this.removeAllListeners(`${txMeta.id}:rejected`)
@ -92,7 +134,9 @@ module.exports = class TransactionStateManager extends EventEmitter {
// or rejected tx's.
// not tx's that are pending or unapproved
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) {
transactions.splice(index, 1)
}
@ -101,12 +145,21 @@ module.exports = class TransactionStateManager extends EventEmitter {
this._saveTxList(transactions)
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) {
const txMeta = this.getTxsByMetaData('id', txId)[0]
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) {
// validate 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) {
const txMeta = this.getTx(txId)
txMeta.txParams = extend(txMeta.txParams, txParams)
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) => {
const value = txParams[key]
// validate types
@ -159,17 +219,19 @@ module.exports = class TransactionStateManager extends EventEmitter {
})
}
/*
Takes an object of fields to search for eg:
let thingsToLookFor = {
to: '0x0..',
from: '0x0..',
status: 'signed',
err: undefined,
}
and returns a list of tx with all
/**
@param opts {object} - an object of fields to search for eg:<br>
let <code>thingsToLookFor = {<br>
to: '0x0..',<br>
from: '0x0..',<br>
status: 'signed',<br>
err: undefined,<br>
}<br></code>
@param [initialList=this.getTxList()]
@returns a {array} of txMeta with all
options matching
*/
/*
****************HINT****************
| `err: undefined` is like looking |
| for a tx with no err |
@ -190,7 +252,14 @@ module.exports = class TransactionStateManager extends EventEmitter {
})
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()) {
return txList.filter((txMeta) => {
if (txMeta.txParams[key]) {
@ -203,33 +272,51 @@ module.exports = class TransactionStateManager extends EventEmitter {
// 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) {
const txMeta = this.getTx(txId)
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) {
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) {
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) {
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) {
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) {
const txMeta = this.getTx(txId)
txMeta.submittedTime = (new Date()).getTime()
@ -237,17 +324,29 @@ module.exports = class TransactionStateManager extends EventEmitter {
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) {
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) {
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) {
const txMeta = this.getTx(txId)
txMeta.err = {
@ -258,6 +357,11 @@ module.exports = class TransactionStateManager extends EventEmitter {
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) {
// network only tx
const txs = this.getFullTxList()
@ -273,9 +377,8 @@ module.exports = class TransactionStateManager extends EventEmitter {
// PRIVATE METHODS
//
// Should find the tx in the tx list and
// update it.
// should set the status in txData
// STATUS METHODS
// statuses:
// - `'unapproved'` the user has not responded
// - `'rejected'` the user has responded no!
// - `'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
// - `'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
/**
@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) {
const txMeta = this.getTx(txId)
txMeta.status = status
@ -295,9 +407,14 @@ module.exports = class TransactionStateManager extends EventEmitter {
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
_saveTxList (transactions) {
this.store.updateState({ transactions })
}
}
module.exports = TransactionStateManager

View File

@ -101,6 +101,7 @@ ConfigManager.prototype.setShowSeedWords = function (should) {
this.setData(data)
}
ConfigManager.prototype.getShouldShowSeedWords = function () {
var data = this.getData()
return data.showSeedWords
@ -116,27 +117,6 @@ ConfigManager.prototype.getSeedWords = function () {
var data = this.getData()
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) {
var config = this.getConfig()
config.provider = {

View File

@ -23,7 +23,35 @@ function setupRaven(opts) {
release,
transport: function(opts) {
const report = opts.data
// simplify certain complex error messages
try {
// handle error-like non-error exceptions
nonErrorException(report)
// simplify certain complex error messages (e.g. Ethjs)
simplifyErrorMessages(report)
// modify report urls
rewriteReportUrls(report)
} catch (err) {
console.warn(err)
}
// make request normally
client._makeRequest(opts)
},
})
client.install()
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
@ -36,27 +64,20 @@ function setupRaven(opts) {
// finalize
item.value = errorMessage
})
// modify report urls
rewriteReportUrls(report)
// make request normally
client._makeRequest(opts)
},
})
client.install()
return Raven
}
}
function rewriteReportUrls(report) {
// update request url
report.request.url = toMetamaskUrl(report.request.url)
// update exception stack trace
if (report.exception && report.exception.values) {
report.exception.values.forEach(item => {
item.stacktrace.frames.forEach(frame => {
frame.filename = toMetamaskUrl(frame.filename)
})
})
}
}
function toMetamaskUrl(origUrl) {

View File

@ -309,7 +309,6 @@ module.exports = class MetamaskController extends EventEmitter {
lostAccounts: this.configManager.getLostAccounts(),
seedWords: this.configManager.getSeedWords(),
forgottenPassword: this.configManager.getPasswordForgotten(),
isRevealingSeedWords: Boolean(this.configManager.getIsRevealingSeedWords()),
},
}
}
@ -351,7 +350,6 @@ module.exports = class MetamaskController extends EventEmitter {
clearSeedWordCache: this.clearSeedWordCache.bind(this),
resetAccount: nodeify(this.resetAccount, this),
importAccountWithStrategy: this.importAccountWithStrategy.bind(this),
setIsRevealingSeedWords: this.configManager.setIsRevealingSeedWords.bind(this.configManager),
// vault management
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 txStateHistoryHelper = require('../lib/tx-state-history-helper')
const txStateHistoryHelper = require('../controllers/transactions/lib/tx-state-history-helper')
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',
dest: 'ui/app/css/output',
devMode: true,
pattern: 'ui/app/css/**/*.scss',
pattern: 'ui/app/**/*.scss',
}))
function createScssBuildTask({ src, dest, devMode, pattern }) {
@ -484,16 +484,6 @@ function generateBundler(opts, performBundle) {
NODE_ENV: opts.devMode ? 'development' : 'production',
}))
// Minification
if (opts.minifyBuild) {
bundler.transform('uglifyify', {
global: true,
mangle: {
reserved: [ 'MetamaskInpageProvider' ]
},
})
}
if (opts.watch) {
bundler = watchify(bundler)
// on any file update, re-runs the bundler
@ -567,6 +557,16 @@ function bundleTask(opts) {
.pipe(sourcemaps.init({ loadMaps: true }))
}
// Minification
if (opts.minifyBuild) {
buildStream = buildStream
.pipe(uglify({
mangle: {
reserved: [ 'MetamaskInpageProvider' ]
},
}))
}
// Finalize Source Maps (writes .map file)
if (opts.buildSourceMaps) {
buildStream = buildStream

View File

@ -70,10 +70,14 @@ class ImportAccountScreen extends Component {
switch (this.state.selectedOption) {
case OPTIONS.JSON_FILE:
return importNewAccount('JSON File', [ jsonFile, password ])
// JS runtime requires caught rejections but failures are handled by Redux
.catch()
.then(next)
case OPTIONS.PRIVATE_KEY:
default:
return importNewAccount('Private Key', [ privateKey ])
// JS runtime requires caught rejections but failures are handled by Redux
.catch()
.then(next)
}
}

View File

@ -8,7 +8,6 @@ import Identicon from '../../../../ui/app/components/identicon'
import Breadcrumbs from './breadcrumbs'
import LoadingScreen from './loading-screen'
import { DEFAULT_ROUTE, INITIALIZE_CONFIRM_SEED_ROUTE } from '../../../../ui/app/routes'
import { confirmSeedWords } from '../../../../ui/app/actions'
const LockIcon = props => (
<svg
@ -45,8 +44,6 @@ class BackupPhraseScreen extends Component {
address: PropTypes.string.isRequired,
seedWords: PropTypes.string,
history: PropTypes.object,
isRevealingSeedWords: PropTypes.bool,
clearSeedWords: PropTypes.func,
};
static defaultProps = {
@ -61,14 +58,6 @@ class BackupPhraseScreen extends Component {
}
componentWillMount () {
this.checkSeedWords()
}
componentDidUpdate () {
this.checkSeedWords()
}
checkSeedWords () {
const { seedWords, history } = this.props
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 () {
const { isRevealingSeedWords } = this.props
const { isShowingSecret } = this.state
const { history } = this.props
return (
<div className="backup-phrase__content-wrapper">
@ -152,8 +121,14 @@ class BackupPhraseScreen extends Component {
</div>
</div>
<div className="backup-phrase__next-button">
{ this.renderSubmitButton() }
{ !isRevealingSeedWords && <Breadcrumbs total={3} currentIndex={1} />}
<button
className="first-time-flow__button"
onClick={() => isShowingSecret && history.push(INITIALIZE_CONFIRM_SEED_ROUTE)}
disabled={!isShowingSecret}
>
Next
</button>
<Breadcrumbs total={3} currentIndex={1} />
</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(
withRouter,
connect(mapStateToProps, mapDispatchToProps),
connect(
({ metamask: { selectedAddress, seedWords }, appState: { isLoading } }) => ({
seedWords,
isLoading,
address: selectedAddress,
})
)
)(BackupPhraseScreen)

View File

@ -1,6 +1,6 @@
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.

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 ])
// 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 privateKey = input.value
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 BNInput = require('./bn-as-decimal-input')
// corresponds with 0.1 GWEI
const MIN_GAS_PRICE_BN = new BN('100000000')
const MIN_GAS_PRICE_BN = new BN('0')
const MIN_GAS_LIMIT_BN = new BN('21000')
module.exports = PendingTx

View File

@ -138,7 +138,7 @@ ShapeshiftForm.prototype.renderMain = function () {
width: '229px',
height: '82px',
},
}, this.props.warning)
}, this.props.warning + '')
: this.renderInfo(),
this.renderRefundAddressForCoin(coin),

3579
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@ -78,6 +78,7 @@
"classnames": "^2.2.5",
"clone": "^2.1.1",
"copy-to-clipboard": "^3.0.8",
"css-loader": "^0.28.11",
"currency-formatter": "^1.4.2",
"debounce": "^1.0.0",
"debounce-stream": "^2.0.0",
@ -89,10 +90,9 @@
"ensnare": "^1.0.0",
"eslint-plugin-react": "^7.4.0",
"eth-bin-to-ops": "^1.0.1",
"eth-block-tracker": "^2.3.0",
"eth-contract-metadata": "^1.1.5",
"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-keyring-controller": "^2.2.0",
"eth-phishing-detect": "^1.1.4",
@ -113,6 +113,7 @@
"extensionizer": "^1.0.0",
"fast-json-patch": "^2.0.4",
"fast-levenshtein": "^2.0.6",
"file-loader": "^1.1.11",
"fuse.js": "^3.2.0",
"gulp": "github:gulpjs/gulp#4.0",
"gulp-autoprefixer": "^5.0.0",
@ -187,7 +188,7 @@
"valid-url": "^1.0.9",
"vreme": "^3.0.2",
"web3": "^0.20.1",
"web3-provider-engine": "^13.8.0",
"web3-provider-engine": "^14.0.5",
"web3-stream-provider": "^3.0.1",
"xtend": "^4.0.1"
},
@ -206,7 +207,7 @@
"brfs": "^1.4.3",
"browserify": "^16.1.1",
"chai": "^4.1.0",
"chromedriver": "^2.34.1",
"chromedriver": "2.36.0",
"compression": "^1.7.1",
"coveralls": "^3.0.0",
"cross-env": "^5.1.4",
@ -219,9 +220,10 @@
"eslint-plugin-json": "^1.2.0",
"eslint-plugin-mocha": "^5.0.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",
"ganache-cli": "^6.1.0",
"ganache-core": "^2.1.0",
"geckodriver": "^1.11.0",
"gifencoder": "^1.1.0",
"gulp": "github:gulpjs/gulp#6d71a658c61edb3090221579d8f97dbe086ba2ed",
@ -257,8 +259,10 @@
"mocha-sinon": "^2.0.0",
"nock": "^9.0.14",
"node-sass": "^4.7.2",
"nsp": "^3.2.1",
"nyc": "^11.0.3",
"open": "0.0.5",
"path": "^0.12.7",
"png-file-stream": "^1.0.0",
"prompt": "^1.0.0",
"qs": "^6.2.0",
@ -268,14 +272,17 @@
"react-test-renderer": "^15.6.2",
"react-testutils-additions": "^15.2.0",
"redux-test-utils": "^0.2.2",
"resolve-url-loader": "^2.3.0",
"rimraf": "^2.6.2",
"sass-loader": "^7.0.1",
"selenium-webdriver": "^3.5.0",
"shell-parallel": "^1.0.3",
"sinon": "^5.0.0",
"source-map": "^0.7.2",
"style-loader": "^0.21.0",
"stylelint-config-standard": "^18.2.0",
"tape": "^4.5.1",
"testem": "^2.0.0",
"uglifyify": "^4.0.5",
"vinyl-buffer": "^1.0.1",
"vinyl-source-stream": "^2.0.0",
"watchify": "^3.9.0"

View File

@ -26,9 +26,9 @@ describe('Metamask popup page', function () {
}
})
// after(async function () {
// await driver.quit()
// })
after(async function () {
await driver.quit()
})
describe('Setup', function () {

View File

@ -234,6 +234,7 @@ describe('', function () {
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 () {
const windowHandles = await driver.getAllWindowHandles()
await driver.switchTo().window(windowHandles[2])

View File

@ -23,6 +23,37 @@ global.ethQuery = {
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) {
console.log('*** start runSendFlowTest')
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'
)
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(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'
)
await customizeGas(assert, 0, 21000, '0', '$0.00 USD')
await customizeGas(assert, 500, 60000, '0.003', '$3.60 USD')
const sendButton = await queryAsync($, 'button.btn-primary--lg.page-container__footer-button')
assert.equal(sendButton[0].textContent, 'Next', 'next button rendered')

View File

@ -1,14 +1,28 @@
const JsonRpcEngine = require('json-rpc-engine')
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 = {
createEngineForTestData,
providerFromEngine,
scaffoldMiddleware,
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 () {
return new JsonRpcEngine()
@ -21,11 +35,13 @@ function providerFromEngine (engine) {
function createTestProviderTools (opts = {}) {
const engine = createEngineForTestData()
const testBlockchain = new TestBlockchain()
// handle provided hooks
engine.push(scaffoldMiddleware(opts.scaffold || {}))
// handle block tracker methods
engine.push(testBlockchain.createMiddleware())
engine.push(providerAsMiddleware(GanacheCore.provider({
mnemonic: getTestSeed(),
})))
// wrap in standard provider interface
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 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 () {
let metamaskController
const sandbox = sinon.sandbox.create()
@ -87,17 +93,28 @@ describe('MetaMaskController', function () {
describe('#createNewVaultAndRestore', function () {
it('should be able to call newVaultAndRestore despite a mistake.', async function () {
const password = 'what-what-what'
const wrongSeed = 'debris dizzy just program just float decrease vacant alarm reduce speak stadiu'
const rightSeed = 'debris dizzy just program just float decrease vacant alarm reduce speak stadium'
await metamaskController.createNewVaultAndRestore(password, wrongSeed)
.catch((e) => {
return
})
await metamaskController.createNewVaultAndRestore(password, rightSeed)
await metamaskController.createNewVaultAndRestore(password, TEST_SEED.slice(0, -1)).catch((e) => null)
await metamaskController.createNewVaultAndRestore(password, TEST_SEED)
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 NonceTracker = require('../../app/scripts/lib/nonce-tracker')
const NonceTracker = require('../../app/scripts/controllers/transactions/nonce-tracker')
const MockTxGen = require('../lib/mock-tx-gen')
let providerResultStub = {}

View File

@ -4,7 +4,7 @@ const EthTx = require('ethereumjs-tx')
const ObservableStore = require('obs-store')
const clone = require('clone')
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 sinon = require('sinon')
const noop = () => true

View File

@ -5,17 +5,16 @@ const EthjsQuery = require('ethjs-query')
const ObservableStore = require('obs-store')
const sinon = require('sinon')
const TransactionController = require('../../app/scripts/controllers/transactions')
const TxGasUtils = require('../../app/scripts/lib/tx-gas-utils')
const { createTestProviderTools } = require('../stub/provider')
const TxGasUtils = require('../../app/scripts/controllers/transactions/tx-gas-utils')
const { createTestProviderTools, getTestAccounts } = require('../stub/provider')
const noop = () => true
const currentNetworkId = 42
const otherNetworkId = 36
const privKey = new Buffer('8718b9618a37d1fc78c436511fc6df3c8258d3250635bba617f33003270ec03e', 'hex')
describe('Transaction Controller', function () {
let txController, provider, providerResultStub, testBlockchain
let txController, provider, providerResultStub, query, fromAccount
beforeEach(function () {
providerResultStub = {
@ -24,9 +23,9 @@ describe('Transaction Controller', function () {
// by default, all accounts are external accounts (not contracts)
eth_getCode: '0x',
}
const providerTools = createTestProviderTools({ scaffold: providerResultStub })
provider = providerTools.provider
testBlockchain = providerTools.testBlockchain
provider = createTestProviderTools({ scaffold: providerResultStub }).provider
query = new EthjsQuery(provider)
fromAccount = getTestAccounts()[0]
txController = new TransactionController({
provider,
@ -34,7 +33,7 @@ describe('Transaction Controller', function () {
txHistoryLimit: 10,
blockTracker: { getCurrentBlock: noop, on: noop, once: noop },
signTransaction: (ethTx) => new Promise((resolve) => {
ethTx.sign(privKey)
ethTx.sign(fromAccount.key)
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) {
const txMeta = {
'txParams': {
@ -199,7 +198,7 @@ describe('Transaction Controller', function () {
providerResultStub.eth_gasPrice = '4a817c800'
providerResultStub.eth_getBlockByNumber = { gasLimit: '47b784' }
providerResultStub.eth_estimateGas = '5209'
txController.addTxDefaults(txMeta)
txController.addTxGasDefaults(txMeta)
.then((txMetaWithDefaults) => {
assert(txMetaWithDefaults.txParams.value, '0x0', 'should have added 0x0 as the value')
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 () {
it('should emit updates', function (done) {
const txMeta = {
@ -391,12 +297,12 @@ describe('Transaction Controller', function () {
describe('#updateAndApproveTransaction', function () {
let txMeta
beforeEach(function () {
beforeEach(() => {
txMeta = {
id: 1,
status: 'unapproved',
txParams: {
from: '0xc684832530fcbddae4b4230a47e991ddcec2831d',
from: fromAccount.address,
to: '0x1678a085c290ebd122dc42cba69373b5953b831d',
gasPrice: '0x77359400',
gas: '0x7b0d',
@ -405,11 +311,12 @@ describe('Transaction Controller', function () {
metamaskNetworkId: currentNetworkId,
}
})
it('should update and approve transactions', function () {
it('should update and approve transactions', async () => {
txController.txStateManager.addTx(txMeta)
txController.updateAndApproveTransaction(txMeta)
const approvalPromise = txController.updateAndApproveTransaction(txMeta)
const tx = txController.txStateManager.getTx(1)
assert.equal(tx.status, 'approved')
await approvalPromise
})
})

View File

@ -1,14 +1,77 @@
const assert = require('assert')
const TxGasUtils = require('../../app/scripts/lib/tx-gas-utils')
const { createTestProviderTools } = require('../stub/provider')
const Transaction = require('ethereumjs-tx')
const BN = require('bn.js')
describe('Tx Gas Util', function () {
let txGasUtil, provider, providerResultStub
beforeEach(function () {
providerResultStub = {}
provider = createTestProviderTools({ scaffold: providerResultStub }).provider
txGasUtil = new TxGasUtils({
provider,
const { hexToBn, bnToHex } = require('../../app/scripts/lib/util')
const TxUtils = require('../../app/scripts/controllers/transactions/tx-gas-utils')
describe('txUtils', function () {
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 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 () {
it('should clone deep', function () {

View File

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

View File

@ -1,8 +1,8 @@
const assert = require('assert')
const clone = require('clone')
const ObservableStore = require('obs-store')
const TxStateManager = require('../../app/scripts/lib/tx-state-manager')
const txStateHistoryHelper = require('../../app/scripts/lib/tx-state-history-helper')
const TxStateManager = require('../../app/scripts/controllers/transactions/tx-state-manager')
const txStateHistoryHelper = require('../../app/scripts/controllers/transactions/lib/tx-state-history-helper')
const noop = () => true
describe('TransactionStateManager', function () {

View File

@ -1,77 +1,98 @@
const assert = require('assert')
const Transaction = require('ethereumjs-tx')
const BN = require('bn.js')
const { hexToBn, bnToHex } = require('../../app/scripts/lib/util')
const TxUtils = require('../../app/scripts/lib/tx-gas-utils')
const txUtils = require('../../app/scripts/controllers/transactions/lib/util')
describe('txUtils', function () {
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,
describe('#validateTxParams', function () {
it('does not throw for positive values', function () {
var sample = {
from: '0x1678a085c290ebd122dc42cba69373b5953b831d',
value: '0x01',
}
txUtils.validateTxParams(sample)
})
it('returns error for negative values', function () {
var sample = {
from: '0x1678a085c290ebd122dc42cba69373b5953b831d',
value: '-0x01',
}
try {
txUtils.validateTxParams(sample)
} catch (err) {
assert.ok(err, 'error')
}
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')
describe('#normalizeTxParams', () => {
it('should normalize txParams', () => {
let txParams = {
chainId: '0x1',
from: 'a7df1beDBF813f57096dF77FCd515f0B3900e402',
to: null,
data: '68656c6c6f20776f726c64',
random: 'hello world',
}
let normalizedTxParams = txUtils.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 = txUtils.normalizeTxParams(txParams)
assert.equal(normalizedTxParams.to.slice(0, 2), '0x', 'to should be hexPrefixd')
})
})
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')
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('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')
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 pify = require('pify')
const getBuyEthUrl = require('../../app/scripts/lib/buy-eth-url')
const { getTokenAddressFromTokenObject } = require('./util')
const ethUtil = require('ethereumjs-util')
@ -83,7 +84,7 @@ var actions = {
REVEAL_SEED_CONFIRMATION: 'REVEAL_SEED_CONFIRMATION',
revealSeedConfirmation: revealSeedConfirmation,
requestRevealSeed: requestRevealSeed,
requestRevealSeedWords,
// unlock screen
UNLOCK_IN_PROGRESS: 'UNLOCK_IN_PROGRESS',
UNLOCK_FAILED: 'UNLOCK_FAILED',
@ -345,11 +346,13 @@ function transitionBackward () {
}
}
function clearSeedWordCache () {
log.debug(`background.clearSeedWordCache`)
function confirmSeedWords () {
return dispatch => {
dispatch(actions.showLoadingIndication())
log.debug(`background.clearSeedWordCache`)
return new Promise((resolve, reject) => {
background.clearSeedWordCache((err, account) => {
dispatch(actions.hideLoadingIndication())
if (err) {
dispatch(actions.displayWarning(err.message))
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) {
return (dispatch) => {
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) {
return dispatch => {
dispatch(actions.showLoadingIndication())
@ -460,13 +471,29 @@ function requestRevealSeed (password) {
}
dispatch(actions.showNewVaultSeed(result))
dispatch(actions.hideLoadingIndication())
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) {
return (dispatch) => {
dispatch(actions.showLoadingIndication('This may take a while, be patient.'))
return async (dispatch) => {
let newState
dispatch(actions.showLoadingIndication('This may take a while, please be patient.'))
try {
log.debug(`background.importAccountWithStrategy`)
return new Promise((resolve, reject) => {
background.importAccountWithStrategy(strategy, args, (err) => {
if (err) {
dispatch(actions.displayWarning(err.message))
return reject(err)
}
await pify(background.importAccountWithStrategy).call(background, strategy, args)
log.debug(`background.getState`)
background.getState((err, newState) => {
newState = await pify(background.getState).call(background)
} catch (err) {
dispatch(actions.hideLoadingIndication())
if (err) {
dispatch(actions.displayWarning(err.message))
return reject(err)
throw err
}
dispatch(actions.hideLoadingIndication())
dispatch(actions.updateMetamaskState(newState))
dispatch({
type: actions.SHOW_ACCOUNT_DETAIL,
value: newState.selectedAddress,
})
resolve(newState)
})
})
})
return newState
}
}
@ -1923,11 +1945,3 @@ function updateNetworkEndpointType (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 UnlockPage = require('./components/pages/unlock')
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 CreateAccountPage = require('./components/pages/create-account')
const NoticeScreen = require('./components/pages/notice')
const Loading = require('./components/loading')
const Loading = require('./components/loading-screen')
const NetworkIndicator = require('./components/network')
const Identicon = require('./components/identicon')
const ReactCSSTransitionGroup = require('react-addons-css-transition-group')
@ -56,20 +56,11 @@ const {
class App extends Component {
componentWillMount () {
const {
currentCurrency,
setCurrentCurrencyToUSD,
isRevealingSeedWords,
clearSeedWords,
} = this.props
const { currentCurrency, setCurrentCurrencyToUSD } = this.props
if (!currentCurrency) {
setCurrentCurrencyToUSD()
}
if (isRevealingSeedWords) {
clearSeedWords()
}
}
renderRoutes () {
@ -144,6 +135,7 @@ class App extends Component {
(isLoading || isLoadingNetwork) && h(Loading, {
loadingMessage: loadMessage,
fullScreen: true,
}),
// content
@ -402,8 +394,6 @@ App.propTypes = {
isMouseUser: PropTypes.bool,
setMouseUserState: PropTypes.func,
t: PropTypes.func,
isRevealingSeedWords: PropTypes.bool,
clearSeedWords: PropTypes.func,
}
function mapStateToProps (state) {
@ -484,7 +474,6 @@ function mapDispatchToProps (dispatch, ownProps) {
setCurrentCurrencyToUSD: () => dispatch(actions.setCurrentCurrency('usd')),
toggleAccountMenu: () => dispatch(actions.toggleAccountMenu()),
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 CoinbaseForm = require('./coinbase-form')
const ShapeshiftForm = require('./shapeshift-form')
const Loading = require('./loading')
const Loading = require('./loading-screen')
const AccountPanel = require('./account-panel')
const RadioList = require('./custom-radio-list')
const { getNetworkDisplayName } = require('../../../app/scripts/controllers/network/util')

View File

@ -280,8 +280,7 @@ CustomizeGasModal.prototype.render = function () {
h(GasModalCard, {
value: convertedGasPrice,
min: forceGasMin || MIN_GAS_PRICE_GWEI,
// max: 1000,
step: multiplyCurrencies(MIN_GAS_PRICE_GWEI, 10),
step: 1,
onChange: value => this.convertAndSetGasPrice(value),
title: this.context.t('gasPrice'),
copy: this.context.t('gasPriceCalculation'),
@ -290,7 +289,6 @@ CustomizeGasModal.prototype.render = function () {
h(GasModalCard, {
value: convertedGasLimit,
min: 1,
// max: 100000,
step: 1,
onChange: value => this.convertAndSetGasLimit(value),
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 PropTypes = require('prop-types')
const classnames = require('classnames')
const Spinner = require('../spinner')
class LoadingIndicator extends Component {
class LoadingScreen extends Component {
renderMessage () {
const { loadingMessage } = this.props
return loadingMessage && h('span', loadingMessage)
@ -14,9 +15,9 @@ class LoadingIndicator extends Component {
h('.loading-overlay', {
className: classnames({ 'loading-overlay--full-screen': this.props.fullScreen }),
}, [
h('.flex-center.flex-column', [
h('img', {
src: 'images/loading.svg',
h('.loading-overlay__container', [
h(Spinner, {
color: '#F7C06C',
}),
this.renderMessage(),
@ -26,9 +27,9 @@ class LoadingIndicator extends Component {
}
}
LoadingIndicator.propTypes = {
LoadingScreen.propTypes = {
loadingMessage: PropTypes.string,
fullScreen: PropTypes.bool,
}
module.exports = LoadingIndicator
module.exports = LoadingScreen

View File

@ -192,7 +192,7 @@ AddTokenScreen.prototype.attemptToAutoFillTokenParams = async function (address)
if (symbol && decimals) {
this.setState({
customSymbol: symbol,
customDecimals: decimals.toString(),
customDecimals: decimals,
autoFilled: true,
})
}

View File

@ -105,6 +105,8 @@ class JsonImportSubview extends Component {
}
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
importNewAccount('Private Key', [ privateKey ])
// JS runtime requires caught rejections but failures are handled by Redux
.catch()
.then(() => history.push(DEFAULT_ROUTE))
}

View File

@ -21,7 +21,7 @@ const QrView = require('../../components/qr-code')
// Routes
const {
REVEAL_SEED_ROUTE,
INITIALIZE_BACKUP_PHRASE_ROUTE,
RESTORE_VAULT_ROUTE,
CONFIRM_TRANSACTION_ROUTE,
NOTICE_ROUTE,
@ -69,7 +69,7 @@ class Home extends Component {
log.debug('rendering seed words')
return h(Redirect, {
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 PropTypes = require('prop-types')
const h = require('react-hyperscript')
const { exportAsFile } = require('../../../util')
const { requestRevealSeed, confirmSeedWords } = require('../../../actions')
const classnames = require('classnames')
const { requestRevealSeedWords } = require('../../../actions')
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 {
constructor (props) {
super(props)
this.state = {
screen: PASSWORD_PROMPT_SCREEN,
password: '',
seedWords: null,
error: null,
}
}
componentDidMount () {
const passwordBox = document.getElementById('password-box')
if (passwordBox) {
@ -14,182 +30,135 @@ class RevealSeedPage extends Component {
}
}
checkConfirmation (event) {
if (event.key === 'Enter') {
handleSubmit (event) {
event.preventDefault()
this.revealSeedWords()
}
this.setState({ seedWords: null, error: null })
this.props.requestRevealSeedWords(this.state.password)
.then(seedWords => this.setState({ seedWords, screen: REVEAL_SEED_SCREEN }))
.catch(error => this.setState({ error: error.message }))
}
revealSeedWords () {
const password = document.getElementById('password-box').value
this.props.requestRevealSeed(password)
}
renderSeed () {
const { seedWords, confirmSeedWords, history } = this.props
renderWarning () {
return (
h('.initialize-screen.flex-column.flex-center.flex-grow', [
h('h3.flex-center.text-transform-uppercase', {
style: {
background: '#EBEBEB',
color: '#AEAEAE',
marginTop: 36,
marginBottom: 8,
width: '100%',
fontSize: '20px',
padding: 6,
},
}, [
'Vault Created',
]),
h('div', {
style: {
fontSize: '1em',
marginTop: '10px',
textAlign: 'center',
},
}, [
h('span.error', 'These 12 words are the only way to restore your MetaMask accounts.\nSave them somewhere safe and secret.'),
]),
h('textarea.twelve-word-phrase', {
readOnly: true,
value: seedWords,
h('.page-container__warning-container', [
h('img.page-container__warning-icon', {
src: 'images/warning.svg',
}),
h('button.primary', {
onClick: () => confirmSeedWords().then(() => history.push(DEFAULT_ROUTE)),
style: {
margin: '24px',
fontSize: '0.9em',
marginBottom: '10px',
},
}, 'I\'ve copied it somewhere safe'),
h('button.primary', {
onClick: () => exportAsFile(`MetaMask Seed Words`, seedWords),
style: {
margin: '10px',
fontSize: '0.9em',
},
}, 'Save Seed Words As File'),
h('.page-container__warning-message', [
h('.page-container__warning-title', [this.context.t('revealSeedWordsWarningTitle')]),
h('div', [this.context.t('revealSeedWordsWarning')]),
]),
])
)
}
renderConfirmation () {
const { history, warning, inProgress } = this.props
renderContent () {
return this.state.screen === PASSWORD_PROMPT_SCREEN
? this.renderPasswordPromptContent()
: this.renderRevealSeedContent()
}
renderPasswordPromptContent () {
const { t } = this.context
return (
h('.initialize-screen.flex-column.flex-center.flex-grow', {
style: { maxWidth: '420px' },
h('form', {
onSubmit: event => this.handleSubmit(event),
}, [
h('h3.flex-center.text-transform-uppercase', {
style: {
background: '#EBEBEB',
color: '#AEAEAE',
marginBottom: 24,
width: '100%',
fontSize: '20px',
padding: 6,
},
}, [
'Reveal Seed Words',
]),
h('.div', {
style: {
display: 'flex',
flexDirection: 'column',
padding: '20px',
justifyContent: 'center',
},
}, [
h('h4', 'Do not recover your seed words in a public place! These words can be used to steal all your accounts.'),
// confirmation
h('input.large-input.letter-spacey', {
h('label.input-label', {
htmlFor: 'password-box',
}, t('enterPasswordContinue')),
h('.input-group', [
h('input.form-control', {
type: 'password',
placeholder: t('password'),
id: 'password-box',
placeholder: 'Enter your password to confirm',
onKeyPress: this.checkConfirmation.bind(this),
style: {
width: 260,
marginTop: '12px',
},
value: this.state.password,
onChange: event => this.setState({ password: event.target.value }),
className: classnames({ 'form-control--error': this.state.error }),
}),
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'),
]),
this.state.error && h('.reveal-seed__error', this.state.error),
])
)
}
warning && (
h('span.error', {
style: {
margin: '20px',
},
}, warning.split('-'))
),
renderRevealSeedContent () {
const { t } = this.context
inProgress && (
h('span.in-progress-notification', 'Generating Seed...')
),
]),
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 () {
return this.props.seedWords
? this.renderSeed()
: this.renderConfirmation()
return (
h('.page-container', [
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 = {
requestRevealSeed: PropTypes.func,
confirmSeedWords: PropTypes.func,
seedWords: PropTypes.string,
inProgress: PropTypes.bool,
requestRevealSeedWords: PropTypes.func,
history: PropTypes.object,
warning: PropTypes.string,
}
const mapStateToProps = state => {
const { appState: { warning }, metamask: { seedWords } } = state
return {
warning,
seedWords,
}
RevealSeedPage.contextTypes = {
t: PropTypes.func,
}
const mapDispatchToProps = dispatch => {
return {
requestRevealSeed: password => dispatch(requestRevealSeed(password)),
confirmSeedWords: () => dispatch(confirmSeedWords()),
requestRevealSeedWords: password => dispatch(requestRevealSeedWords(password)),
}
}
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)
const inherits = require('util').inherits
const actions = require('../../actions')
const util = require('../../util')
const { getSymbolAndDecimals } = require('../../token-util')
const ConfirmSendEther = require('./confirm-send-ether')
const ConfirmSendToken = require('./confirm-send-token')
const ConfirmDeployContract = require('./confirm-deploy-contract')
const Loading = require('../loading')
const Loading = require('../loading-screen')
const TX_TYPES = {
DEPLOY_CONTRACT: 'deploy_contract',
@ -26,6 +26,7 @@ function mapStateToProps (state) {
const {
conversionRate,
identities,
tokens: existingTokens,
} = state.metamask
const accounts = state.metamask.accounts
const selectedAddress = state.metamask.selectedAddress || Object.keys(accounts)[0]
@ -33,6 +34,7 @@ function mapStateToProps (state) {
conversionRate,
identities,
selectedAddress,
existingTokens,
}
}
@ -66,6 +68,7 @@ PendingTx.prototype.componentDidUpdate = function (prevProps, prevState) {
}
PendingTx.prototype.setTokenData = async function () {
const { existingTokens } = this.props
const txMeta = this.gatherTxMeta()
const txParams = txMeta.txParams || {}
@ -89,30 +92,15 @@ PendingTx.prototype.setTokenData = async function () {
}
if (isTokenTransaction) {
const token = util.getContractAtAddress(txParams.to)
const results = await Promise.all([
token.symbol(),
token.decimals(),
])
const [ symbol, decimals ] = results
const { symbol, decimals } = await getSymbolAndDecimals(txParams.to, existingTokens)
if (symbol[0] && decimals[0]) {
this.setState({
transactionType: TX_TYPES.SEND_TOKEN,
tokenAddress: txParams.to,
tokenSymbol: symbol[0],
tokenDecimals: decimals[0],
tokenSymbol: symbol,
tokenDecimals: decimals,
isFetching: false,
})
} else {
this.setState({
transactionType: TX_TYPES.SEND_TOKEN,
tokenAddress: txParams.to,
tokenSymbol: null,
tokenDecimals: null,
isFetching: false,
})
}
} else {
this.setState({
transactionType: TX_TYPES.SEND_ETHER,

View File

@ -89,7 +89,6 @@ CurrencyDisplay.prototype.render = function () {
} = this.props
const valueToRender = this.getValueToRender()
const convertedValueToRender = this.getConvertedValueToRender(valueToRender)
return h('div', {
@ -97,22 +96,24 @@ CurrencyDisplay.prototype.render = function () {
style: {
borderColor: inError ? 'red' : null,
},
onClick: () => this.currencyInput.focus(),
onClick: () => this.currencyInput && this.currencyInput.focus(),
}, [
h('div.currency-display__primary-row', [
h('div.currency-display__input-wrapper', [
h(CurrencyInput, {
h(readOnly ? 'input' : CurrencyInput, {
className: primaryBalanceClassName,
value: `${valueToRender}`,
placeholder: '0',
readOnly,
...(!readOnly ? {
onInputChange: newValue => {
handleChange(this.getAmount(newValue))
},
inputRef: input => { this.currencyInput = input },
} : {}),
}),
h('span.currency-display__currency-symbol', primaryCurrency),

View File

@ -1,8 +1,8 @@
const ethUtil = require('ethereumjs-util')
const { conversionUtil, multiplyCurrencies } = require('../../conversion-util')
const MIN_GAS_PRICE_HEX = (100000000).toString(16)
const MIN_GAS_PRICE_DEC = '100000000'
const MIN_GAS_PRICE_DEC = '0'
const MIN_GAS_PRICE_HEX = (parseInt(MIN_GAS_PRICE_DEC)).toString(16)
const MIN_GAS_LIMIT_DEC = '21000'
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 () {
this.props.shapeShiftSubview()
}
@ -120,14 +124,12 @@ ShapeshiftForm.prototype.renderMetadata = function (label, value) {
}
ShapeshiftForm.prototype.renderMarketInfo = function () {
const { depositCoin } = this.state
const coinPair = `${depositCoin}_eth`
const { tokenExchangeRates } = this.props
const {
limit,
rate,
minimum,
} = tokenExchangeRates[coinPair] || {}
} = tokenExchangeRates[this.getCoinPair()] || {}
return h('div.shapeshift-form__metadata', {}, [
@ -172,10 +174,9 @@ ShapeshiftForm.prototype.renderQrCode = function () {
ShapeshiftForm.prototype.render = function () {
const { coinOptions, btnClass, warning } = this.props
const { depositCoin, errorMessage, showQrCode, depositAddress } = this.state
const coinPair = `${depositCoin}_eth`
const { errorMessage, showQrCode, depositAddress } = this.state
const { tokenExchangeRates } = this.props
const token = tokenExchangeRates[coinPair]
const token = tokenExchangeRates[this.getCoinPair()]
return h('div.shapeshift-form-wrapper', [
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 PendingPersonalMsg = require('./components/pending-personal-msg')
// const PendingTypedMsg = require('./components/pending-typed-msg')
const Loading = require('./components/loading')
const Loading = require('./components/loading-screen')
const { DEFAULT_ROUTE } = require('./routes')
module.exports = compose(

View File

@ -61,3 +61,5 @@
@import './welcome-screen.scss';
@import './sender-to-recipient.scss';
@import '../../../components/export-text-container/export-text-container.scss';

View File

@ -26,4 +26,25 @@
width: 100vw;
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 './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 {
height: 100%;
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;
}
}
@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;
$zumthor: #edf7ff;
$ecstasy: #f7861c;
$linen: #fdf4f4;
/*
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,
} = this.props
const missingTokenBalance = selectedToken && !tokenBalance
const missingTokenBalance = selectedToken && (tokenBalance === null || tokenBalance === undefined)
const noErrors = !amountError && toError === null
return h('div.page-container__footer', [

View File

@ -1,14 +1,6 @@
const abi = require('human-standard-token-abi')
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)
const util = require('./util')
function tokenInfoGetter () {
const tokens = {}
return async (address) => {
@ -16,18 +8,35 @@ const tokenInfoGetter = function () {
return tokens[address]
}
const contract = TokenContract.at(address)
tokens[address] = await getSymbolAndDecimals(address)
const result = await Promise.all([
contract.symbol(),
contract.decimals(),
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
tokens[address] = { symbol: symbol[0], decimals: decimals[0] }
return tokens[address]
return {
symbol: symbol[0] || null,
decimals: decimals[0] && decimals[0].toString() || null,
}
}
@ -42,4 +51,5 @@ function calcTokenAmount (value, decimals) {
module.exports = {
tokenInfoGetter,
calcTokenAmount,
getSymbolAndDecimals,
}

View File

@ -11,8 +11,9 @@ const getMessage = (locale, key, substitutions) => {
const { current, en } = locale
const entry = current[key] || en[key]
if (!entry) {
log.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
// perform substitutions