Merge branch 'master' into nonce-tracker

feature/default_network_editable
kumavis 8 years ago committed by GitHub
commit d228f46254
  1. 27
      CHANGELOG.md
  2. 2
      app/manifest.json
  3. 29
      app/scripts/controllers/preferences.js
  4. 65
      app/scripts/controllers/transactions.js
  5. 12
      app/scripts/metamask-controller.js
  6. 38
      app/scripts/migrations/015.js
  7. 1
      app/scripts/migrations/index.js
  8. 12
      package.json
  9. 40
      test/unit/tx-controller-test.js
  10. 10
      ui/app/account-detail.js
  11. 9
      ui/app/accounts/import/index.js
  12. 27
      ui/app/actions.js
  13. 219
      ui/app/add-token.js
  14. 54
      ui/app/app.js
  15. 2
      ui/app/components/ens-input.js
  16. 23
      ui/app/components/network.js
  17. 2
      ui/app/components/pending-tx.js
  18. 42
      ui/app/components/token-cell.js
  19. 109
      ui/app/components/token-list.js
  20. 28
      ui/app/config.js
  21. 2
      ui/app/keychains/hd/create-vault-complete.js
  22. 10
      ui/app/reducers/app.js
  23. 4
      ui/app/send.js
  24. 21
      ui/lib/etherscan-prefix-for-network.js
  25. 21
      ui/lib/explorer-link.js

@ -2,13 +2,40 @@
## Current Master ## Current Master
## 3.8.3 2017-7-6
- Re-enable default token list.
- Add origin header to dapp-bound requests to allow providers to throttle sites.
- Fix bug that could sometimes resubmit a transaction that had been stalled due to low balance after balance was restored.
## 3.8.2 2017-7-3
- No longer show network loading indication on config screen, to allow selecting custom RPCs.
- Visually indicate that network spinner is a menu.
- Indicate what network is being searched for when disconnected.
## 3.8.1 2017-6-30
- Temporarily disabled loading popular tokens by default to improve performance.
- Remove SEND token button until a better token sending form can be built, due to some precision issues.
- Fix precision bug in token balances.
- Cache token symbol and precisions to reduce network load.
- Transpile some newer JavaScript, restores compatibility with some older browsers.
## 3.8.0 2017-6-28
- No longer stop rebroadcasting transactions
- Add list of popular tokens held to the account detail view. - Add list of popular tokens held to the account detail view.
- Add ability to add Tokens to token list.
- Add a warning to JSON file import. - Add a warning to JSON file import.
- Add "send" link to token list, which goes to TokenFactory.
- Fix bug where slowly mined txs would sometimes be incorrectly marked as failed. - Fix bug where slowly mined txs would sometimes be incorrectly marked as failed.
- Fix bug where badge count did not reflect personal_sign pending messages. - Fix bug where badge count did not reflect personal_sign pending messages.
- Seed word confirmation wording is now scarier. - Seed word confirmation wording is now scarier.
- Fix error for invalid seed words. - Fix error for invalid seed words.
- Prevent users from submitting two duplicate transactions by disabling submit. - Prevent users from submitting two duplicate transactions by disabling submit.
- Allow Dapps to specify gas price as hex string.
- Add button for copying state logs to clipboard.
## 3.7.8 2017-6-12 ## 3.7.8 2017-6-12

@ -1,7 +1,7 @@
{ {
"name": "MetaMask", "name": "MetaMask",
"short_name": "Metamask", "short_name": "Metamask",
"version": "3.7.8", "version": "3.8.3",
"manifest_version": 2, "manifest_version": 2,
"author": "https://metamask.io", "author": "https://metamask.io",
"description": "Ethereum Browser Extension", "description": "Ethereum Browser Extension",

@ -8,13 +8,11 @@ class PreferencesController {
const initState = extend({ const initState = extend({
frequentRpcList: [], frequentRpcList: [],
currentAccountTab: 'history', currentAccountTab: 'history',
tokens: [],
}, opts.initState) }, opts.initState)
this.store = new ObservableStore(initState) this.store = new ObservableStore(initState)
} }
// PUBLIC METHODS
//
// PUBLIC METHODS
//
setSelectedAddress (_address) { setSelectedAddress (_address) {
return new Promise((resolve, reject) => { return new Promise((resolve, reject) => {
@ -28,6 +26,29 @@ class PreferencesController {
return this.store.getState().selectedAddress return this.store.getState().selectedAddress
} }
addToken (rawAddress, symbol, decimals) {
const address = normalizeAddress(rawAddress)
const newEntry = { address, symbol, decimals }
const tokens = this.store.getState().tokens
const previousIndex = tokens.find((token, index) => {
return token.address === address
})
if (previousIndex) {
tokens[previousIndex] = newEntry
} else {
tokens.push(newEntry)
}
this.store.updateState({ tokens })
return Promise.resolve()
}
getTokens () {
return this.store.getState().tokens
}
updateFrequentRpcList (_url) { updateFrequentRpcList (_url) {
return this.addToFrequentRpcList(_url) return this.addToFrequentRpcList(_url)
.then((rpcList) => { .then((rpcList) => {

@ -8,8 +8,6 @@ const TxProviderUtil = require('../lib/tx-utils')
const createId = require('../lib/random-id') const createId = require('../lib/random-id')
const NonceTracker = require('../lib/nonce-tracker') const NonceTracker = require('../lib/nonce-tracker')
const RETRY_LIMIT = 200
module.exports = class TransactionController extends EventEmitter { module.exports = class TransactionController extends EventEmitter {
constructor (opts) { constructor (opts) {
super() super()
@ -37,7 +35,10 @@ module.exports = class TransactionController extends EventEmitter {
this.query = opts.ethQuery this.query = opts.ethQuery
this.txProviderUtils = new TxProviderUtil(this.query) this.txProviderUtils = new TxProviderUtil(this.query)
this.blockTracker.on('rawBlock', this.checkForTxInBlock.bind(this)) this.blockTracker.on('rawBlock', this.checkForTxInBlock.bind(this))
this.blockTracker.on('latest', this.resubmitPendingTxs.bind(this)) // this is a little messy but until ethstore has been either
// removed or redone this is to guard against the race condition
// where ethStore hasent been populated by the results yet
this.blockTracker.once('latest', () => this.blockTracker.on('latest', this.resubmitPendingTxs.bind(this)))
this.blockTracker.on('sync', this.queryPendingTxs.bind(this)) this.blockTracker.on('sync', this.queryPendingTxs.bind(this))
this.signEthTx = opts.signTransaction this.signEthTx = opts.signTransaction
this.ethStore = opts.ethStore this.ethStore = opts.ethStore
@ -163,13 +164,15 @@ module.exports = class TransactionController extends EventEmitter {
const txParams = txMeta.txParams const txParams = txMeta.txParams
// ensure value // ensure value
txParams.value = txParams.value || '0x0' txParams.value = txParams.value || '0x0'
this.query.gasPrice((err, gasPrice) => { if (!txParams.gasPrice) {
if (err) return cb(err) this.query.gasPrice((err, gasPrice) => {
// set gasPrice if (err) return cb(err)
txParams.gasPrice = gasPrice // set gasPrice
// set gasLimit txParams.gasPrice = gasPrice
this.txProviderUtils.analyzeGasUsage(txMeta, cb) })
}) }
// set gasLimit
this.txProviderUtils.analyzeGasUsage(txMeta, cb)
} }
getUnapprovedTxList () { getUnapprovedTxList () {
@ -430,10 +433,24 @@ module.exports = class TransactionController extends EventEmitter {
// only try resubmitting if their are transactions to resubmit // only try resubmitting if their are transactions to resubmit
if (!pending.length) return if (!pending.length) return
const resubmit = denodeify(this._resubmitTx.bind(this)) const resubmit = denodeify(this._resubmitTx.bind(this))
Promise.all(pending.map(txMeta => resubmit(txMeta))) pending.forEach((txMeta) => resubmit(txMeta)
.catch((reason) => { .catch((reason) => {
log.info('Problem resubmitting tx', reason) /*
}) Dont marked as failed if the error is a "known" transaction warning
"there is already a transaction with the same sender-nonce
but higher/same gas price"
*/
const errorMessage = reason.message.toLowerCase()
const isKnownTx = (
// geth
errorMessage === 'replacement transaction underpriced'
|| errorMessage.startsWith('known transaction')
// parity
|| errorMessage === 'gas price too low to replace'
)
// ignore resubmit warnings, return early
if (!isKnownTx) this.setTxStatusFailed(txMeta.id, reason.message)
}))
} }
_resubmitTx (txMeta, cb) { _resubmitTx (txMeta, cb) {
@ -444,15 +461,25 @@ module.exports = class TransactionController extends EventEmitter {
const gtBalance = Number.parseInt(txMeta.txParams.value) > Number.parseInt(balance) const gtBalance = Number.parseInt(txMeta.txParams.value) > Number.parseInt(balance)
if (!('retryCount' in txMeta)) txMeta.retryCount = 0 if (!('retryCount' in txMeta)) txMeta.retryCount = 0
// if the value of the transaction is greater then the balance // if the value of the transaction is greater then the balance, fail.
// or the nonce of the transaction is lower then the accounts nonce if (gtBalance) {
// dont resubmit the tx const message = 'Insufficient balance.'
if (gtBalance || txNonce < nonce) return cb() this.setTxStatusFailed(txMeta.id, message)
cb()
return log.error(message)
}
// if the nonce of the transaction is lower then the accounts nonce, fail.
if (txNonce < nonce) {
const message = 'Invalid nonce.'
this.setTxStatusFailed(txMeta.id, message)
cb()
return log.error(message)
}
// Only auto-submit already-signed txs: // Only auto-submit already-signed txs:
if (!('rawTx' in txMeta)) return cb() if (!('rawTx' in txMeta)) return cb()
if (txMeta.retryCount > RETRY_LIMIT) return
// Increment a try counter. // Increment a try counter.
txMeta.retryCount++ txMeta.retryCount++
const rawTx = txMeta.rawTx const rawTx = txMeta.rawTx

@ -184,7 +184,9 @@ module.exports = class MetamaskController extends EventEmitter {
eth_syncing: false, eth_syncing: false,
web3_clientVersion: `MetaMask/v${version}`, web3_clientVersion: `MetaMask/v${version}`,
}, },
// rpc data source
rpcUrl: this.networkController.getCurrentRpcAddress(), rpcUrl: this.networkController.getCurrentRpcAddress(),
originHttpHeaderKey: 'X-Metamask-Origin',
// account mgmt // account mgmt
getAccounts: (cb) => { getAccounts: (cb) => {
const isUnlocked = this.keyringController.memStore.getState().isUnlocked const isUnlocked = this.keyringController.memStore.getState().isUnlocked
@ -293,6 +295,7 @@ module.exports = class MetamaskController extends EventEmitter {
// PreferencesController // PreferencesController
setSelectedAddress: nodeify(preferencesController.setSelectedAddress).bind(preferencesController), setSelectedAddress: nodeify(preferencesController.setSelectedAddress).bind(preferencesController),
addToken: nodeify(preferencesController.addToken).bind(preferencesController),
setCurrentAccountTab: nodeify(preferencesController.setCurrentAccountTab).bind(preferencesController), setCurrentAccountTab: nodeify(preferencesController.setCurrentAccountTab).bind(preferencesController),
setDefaultRpc: nodeify(this.setDefaultRpc).bind(this), setDefaultRpc: nodeify(this.setDefaultRpc).bind(this),
setCustomRpc: nodeify(this.setCustomRpc).bind(this), setCustomRpc: nodeify(this.setCustomRpc).bind(this),
@ -355,8 +358,13 @@ module.exports = class MetamaskController extends EventEmitter {
} }
setupProviderConnection (outStream, originDomain) { setupProviderConnection (outStream, originDomain) {
streamIntoProvider(outStream, this.provider, logger) streamIntoProvider(outStream, this.provider, onRequest, onResponse)
function logger (err, request, response) { // append dapp origin domain to request
function onRequest (request) {
request.origin = originDomain
}
// log rpc activity
function onResponse (err, request, response) {
if (err) return console.error(err) if (err) return console.error(err)
if (response.error) { if (response.error) {
console.error('Error in RPC response:\n', response.error) console.error('Error in RPC response:\n', response.error)

@ -0,0 +1,38 @@
const version = 15
/*
This migration sets transactions with the 'Gave up submitting tx.' err message
to a 'failed' stated
*/
const clone = require('clone')
module.exports = {
version,
migrate: function (originalVersionedData) {
const versionedData = clone(originalVersionedData)
versionedData.meta.version = version
try {
const state = versionedData.data
const newState = transformState(state)
versionedData.data = newState
} catch (err) {
console.warn(`MetaMask Migration #${version}` + err.stack)
}
return Promise.resolve(versionedData)
},
}
function transformState (state) {
const newState = state
const transactions = newState.TransactionController.transactions
newState.TransactionController.transactions = transactions.map((txMeta) => {
if (!txMeta.err) return txMeta
else if (txMeta.err.message === 'Gave up submitting tx.') txMeta.status = 'failed'
return txMeta
})
return newState
}

@ -25,4 +25,5 @@ module.exports = [
require('./012'), require('./012'),
require('./013'), require('./013'),
require('./014'), require('./014'),
require('./015'),
] ]

@ -68,7 +68,7 @@
"eth-query": "^2.1.2", "eth-query": "^2.1.2",
"eth-sig-util": "^1.1.1", "eth-sig-util": "^1.1.1",
"eth-simple-keyring": "^1.1.1", "eth-simple-keyring": "^1.1.1",
"eth-token-tracker": "^1.0.9", "eth-token-tracker": "^1.1.2",
"ethereumjs-tx": "^1.3.0", "ethereumjs-tx": "^1.3.0",
"ethereumjs-util": "ethereumjs/ethereumjs-util#ac5d0908536b447083ea422b435da27f26615de9", "ethereumjs-util": "ethereumjs/ethereumjs-util#ac5d0908536b447083ea422b435da27f26615de9",
"ethereumjs-wallet": "^0.6.0", "ethereumjs-wallet": "^0.6.0",
@ -104,7 +104,7 @@
"qrcode-npm": "0.0.3", "qrcode-npm": "0.0.3",
"react": "^15.0.2", "react": "^15.0.2",
"react-addons-css-transition-group": "^15.0.2", "react-addons-css-transition-group": "^15.0.2",
"react-dom": "^15.0.2", "react-dom": "^15.5.4",
"react-hyperscript": "^2.2.2", "react-hyperscript": "^2.2.2",
"react-markdown": "^2.3.0", "react-markdown": "^2.3.0",
"react-redux": "^4.4.5", "react-redux": "^4.4.5",
@ -124,9 +124,9 @@
"through2": "^2.0.1", "through2": "^2.0.1",
"valid-url": "^1.0.9", "valid-url": "^1.0.9",
"vreme": "^3.0.2", "vreme": "^3.0.2",
"web3": "0.18.2", "web3": "0.19.1",
"web3-provider-engine": "^13.0.3", "web3-provider-engine": "^13.1.1",
"web3-stream-provider": "^2.0.6", "web3-stream-provider": "^3.0.1",
"xtend": "^4.0.1" "xtend": "^4.0.1"
}, },
"devDependencies": { "devDependencies": {
@ -142,7 +142,6 @@
"brfs": "^1.4.3", "brfs": "^1.4.3",
"browserify": "^13.0.0", "browserify": "^13.0.0",
"chai": "^3.5.0", "chai": "^3.5.0",
"clone": "^1.0.2",
"deep-freeze-strict": "^1.1.1", "deep-freeze-strict": "^1.1.1",
"del": "^2.2.0", "del": "^2.2.0",
"envify": "^4.0.0", "envify": "^4.0.0",
@ -174,7 +173,6 @@
"qs": "^6.2.0", "qs": "^6.2.0",
"qunit": "^0.9.1", "qunit": "^0.9.1",
"react-addons-test-utils": "^15.5.1", "react-addons-test-utils": "^15.5.1",
"react-dom": "^15.5.4",
"react-test-renderer": "^15.5.4", "react-test-renderer": "^15.5.4",
"react-testutils-additions": "^15.2.0", "react-testutils-additions": "^15.2.0",
"sinon": "^1.17.3", "sinon": "^1.17.3",

@ -21,6 +21,7 @@ describe('Transaction Controller', function () {
blockTracker: { getCurrentBlock: noop, on: noop }, blockTracker: { getCurrentBlock: noop, on: noop },
provider: { sendAsync: noop }, provider: { sendAsync: noop },
ethQuery: new EthQuery({ sendAsync: noop }), ethQuery: new EthQuery({ sendAsync: noop }),
ethStore: { getState: noop },
signTransaction: (ethTx) => new Promise((resolve) => { signTransaction: (ethTx) => new Promise((resolve) => {
ethTx.sign(privKey) ethTx.sign(privKey)
resolve() resolve()
@ -318,4 +319,43 @@ describe('Transaction Controller', function () {
}) })
}) })
}) })
describe('#_resubmitTx with a too-low balance', function () {
it('should fail the transaction', function (done) {
const from = '0xda0da0'
const txMeta = {
id: 1,
status: 'submitted',
metamaskNetworkId: currentNetworkId,
txParams: {
from,
nonce: '0x1',
value: '0xfffff',
},
}
const lowBalance = '0x0'
const fakeStoreState = { accounts: {} }
fakeStoreState.accounts[from] = {
balance: lowBalance,
nonce: '0x0',
}
// Stubbing out current account state:
const getStateStub = sinon.stub(txController.ethStore, 'getState')
.returns(fakeStoreState)
// Adding the fake tx:
txController.addTx(clone(txMeta))
txController._resubmitTx(txMeta, function (err) {
assert.ifError(err, 'should not throw an error')
const updatedMeta = txController.getTx(txMeta.id)
assert.notEqual(updatedMeta.status, txMeta.status, 'status changed.')
assert.equal(updatedMeta.status, 'failed', 'tx set to failed.')
done()
})
})
})
}) })

@ -35,6 +35,7 @@ function mapStateToProps (state) {
conversionRate: state.metamask.conversionRate, conversionRate: state.metamask.conversionRate,
currentCurrency: state.metamask.currentCurrency, currentCurrency: state.metamask.currentCurrency,
currentAccountTab: state.metamask.currentAccountTab, currentAccountTab: state.metamask.currentAccountTab,
tokens: state.metamask.tokens,
} }
} }
@ -273,11 +274,16 @@ AccountDetailScreen.prototype.tabSections = function () {
AccountDetailScreen.prototype.tabSwitchView = function () { AccountDetailScreen.prototype.tabSwitchView = function () {
const props = this.props const props = this.props
const { address, network } = props const { address, network } = props
const { currentAccountTab } = this.props const { currentAccountTab, tokens } = this.props
switch (currentAccountTab) { switch (currentAccountTab) {
case 'tokens': case 'tokens':
return h(TokenList, { userAddress: address, network }) return h(TokenList, {
userAddress: address,
network,
tokens,
addToken: () => this.props.dispatch(actions.showAddTokenPage()),
})
default: default:
return this.transactionList() return this.transactionList()
} }

@ -2,6 +2,7 @@ const inherits = require('util').inherits
const Component = require('react').Component const Component = require('react').Component
const h = require('react-hyperscript') const h = require('react-hyperscript')
const connect = require('react-redux').connect const connect = require('react-redux').connect
const actions = require('../../actions')
import Select from 'react-select' import Select from 'react-select'
// Subviews // Subviews
@ -37,6 +38,14 @@ AccountImportSubview.prototype.render = function () {
style: { style: {
}, },
}, [ }, [
h('.section-title.flex-row.flex-center', [
h('i.fa.fa-arrow-left.fa-lg.cursor-pointer', {
onClick: (event) => {
props.dispatch(actions.goHome())
},
}),
h('h2.page-subtitle', 'Import Accounts'),
]),
h('div', { h('div', {
style: { style: {
padding: '10px', padding: '10px',

@ -121,7 +121,10 @@ var actions = {
SET_PROVIDER_TYPE: 'SET_PROVIDER_TYPE', SET_PROVIDER_TYPE: 'SET_PROVIDER_TYPE',
USE_ETHERSCAN_PROVIDER: 'USE_ETHERSCAN_PROVIDER', USE_ETHERSCAN_PROVIDER: 'USE_ETHERSCAN_PROVIDER',
useEtherscanProvider: useEtherscanProvider, useEtherscanProvider: useEtherscanProvider,
showConfigPage: showConfigPage, showConfigPage,
SHOW_ADD_TOKEN_PAGE: 'SHOW_ADD_TOKEN_PAGE',
showAddTokenPage,
addToken,
setRpcTarget: setRpcTarget, setRpcTarget: setRpcTarget,
setDefaultRpcTarget: setDefaultRpcTarget, setDefaultRpcTarget: setDefaultRpcTarget,
setProviderType: setProviderType, setProviderType: setProviderType,
@ -627,6 +630,28 @@ function showConfigPage (transitionForward = true) {
} }
} }
function showAddTokenPage (transitionForward = true) {
return {
type: actions.SHOW_ADD_TOKEN_PAGE,
value: transitionForward,
}
}
function addToken (address, symbol, decimals) {
return (dispatch) => {
dispatch(actions.showLoadingIndication())
background.addToken(address, symbol, decimals, (err) => {
dispatch(actions.hideLoadingIndication())
if (err) {
return dispatch(actions.displayWarning(err.message))
}
setTimeout(() => {
dispatch(actions.goHome())
}, 250)
})
}
}
function goBackToInitView () { function goBackToInitView () {
return { return {
type: actions.BACK_TO_INIT_MENU, type: actions.BACK_TO_INIT_MENU,

@ -0,0 +1,219 @@
const inherits = require('util').inherits
const Component = require('react').Component
const h = require('react-hyperscript')
const connect = require('react-redux').connect
const actions = require('./actions')
const ethUtil = require('ethereumjs-util')
const abi = require('human-standard-token-abi')
const Eth = require('ethjs-query')
const EthContract = require('ethjs-contract')
const emptyAddr = '0x0000000000000000000000000000000000000000'
module.exports = connect(mapStateToProps)(AddTokenScreen)
function mapStateToProps (state) {
return {
}
}
inherits(AddTokenScreen, Component)
function AddTokenScreen () {
this.state = {
warning: null,
address: null,
symbol: 'TOKEN',
decimals: 18,
}
Component.call(this)
}
AddTokenScreen.prototype.render = function () {
const state = this.state
const props = this.props
const { warning, symbol, decimals } = state
return (
h('.flex-column.flex-grow', [
// subtitle and nav
h('.section-title.flex-row.flex-center', [
h('i.fa.fa-arrow-left.fa-lg.cursor-pointer', {
onClick: (event) => {
props.dispatch(actions.goHome())
},
}),
h('h2.page-subtitle', 'Add Token'),
]),
h('.error', {
style: {
display: warning ? 'block' : 'none',
padding: '0 20px',
textAlign: 'center',
},
}, warning),
// conf view
h('.flex-column.flex-justify-center.flex-grow.select-none', [
h('.flex-space-around', {
style: {
padding: '20px',
},
}, [
h('div', [
h('span', {
style: { fontWeight: 'bold', paddingRight: '10px'},
}, 'Token Address'),
]),
h('section.flex-row.flex-center', [
h('input#token-address', {
name: 'address',
placeholder: 'Token Address',
onChange: this.tokenAddressDidChange.bind(this),
style: {
width: 'inherit',
flex: '1 0 auto',
height: '30px',
margin: '8px',
},
}),
]),
h('div', [
h('span', {
style: { fontWeight: 'bold', paddingRight: '10px'},
}, 'Token Symbol'),
]),
h('div', { style: {display: 'flex'} }, [
h('input#token_symbol', {
placeholder: `Like "ETH"`,
value: symbol,
style: {
width: 'inherit',
flex: '1 0 auto',
height: '30px',
margin: '8px',
},
onChange: (event) => {
var element = event.target
var symbol = element.value
this.setState({ symbol })
},
}),
]),
h('div', [
h('span', {
style: { fontWeight: 'bold', paddingRight: '10px'},
}, 'Decimals of Precision'),
]),
h('div', { style: {display: 'flex'} }, [
h('input#token_decimals', {
value: decimals,
type: 'number',
min: 0,
max: 36,
style: {
width: 'inherit',
flex: '1 0 auto',
height: '30px',
margin: '8px',
},
onChange: (event) => {
var element = event.target
var decimals = element.value.trim()
this.setState({ decimals })
},
}),
]),
h('button', {
style: {
alignSelf: 'center',
},
onClick: (event) => {
const valid = this.validateInputs()
if (!valid) return
const { address, symbol, decimals } = this.state
this.props.dispatch(actions.addToken(address.trim(), symbol.trim(), decimals))
},
}, 'Add'),
]),
]),
])
)
}
AddTokenScreen.prototype.componentWillMount = function () {
if (typeof global.ethereumProvider === 'undefined') return
this.eth = new Eth(global.ethereumProvider)
this.contract = new EthContract(this.eth)
this.TokenContract = this.contract(abi)
}
AddTokenScreen.prototype.tokenAddressDidChange = function (event) {
const el = event.target
const address = el.value.trim()
if (ethUtil.isValidAddress(address) && address !== emptyAddr) {
this.setState({ address })
this.attemptToAutoFillTokenParams(address)
}
}
AddTokenScreen.prototype.validateInputs = function () {
let msg = ''
const state = this.state
const { address, symbol, decimals } = state
const validAddress = ethUtil.isValidAddress(address)
if (!validAddress) {
msg += 'Address is invalid. '
}
const validDecimals = decimals >= 0 && decimals < 36
if (!validDecimals) {
msg += 'Decimals must be at least 0, and not over 36. '
}
const symbolLen = symbol.trim().length
const validSymbol = symbolLen > 0 && symbolLen < 10
if (!validSymbol) {
msg += 'Symbol must be between 0 and 10 characters.'
}
const isValid = validAddress && validDecimals
if (!isValid) {
this.setState({
warning: msg,
})
} else {
this.setState({ warning: null })
}
return isValid
}
AddTokenScreen.prototype.attemptToAutoFillTokenParams = async function (address) {
const contract = this.TokenContract.at(address)
const results = await Promise.all([
contract.symbol(),
contract.decimals(),
])
const [ symbol, decimals ] = results
if (symbol && decimals) {
console.log('SETTING SYMBOL AND DECIMALS', { symbol, decimals })
this.setState({ symbol: symbol[0], decimals: decimals[0].toString() })
}
}

@ -19,6 +19,7 @@ const NoticeScreen = require('./components/notice')
const generateLostAccountsNotice = require('../lib/lost-accounts-notice') const generateLostAccountsNotice = require('../lib/lost-accounts-notice')
// other views // other views
const ConfigScreen = require('./config') const ConfigScreen = require('./config')
const AddTokenScreen = require('./add-token')
const Import = require('./accounts/import') const Import = require('./accounts/import')
const InfoScreen = require('./info') const InfoScreen = require('./info')
const Loading = require('./components/loading') const Loading = require('./components/loading')
@ -65,9 +66,9 @@ function mapStateToProps (state) {
App.prototype.render = function () { App.prototype.render = function () {
var props = this.props var props = this.props
const { isLoading, loadingMessage, transForward, network } = props const { isLoading, loadingMessage, transForward, network } = props
const isLoadingNetwork = network === 'loading' const isLoadingNetwork = network === 'loading' && props.currentView.name !== 'config'
const loadMessage = loadingMessage || isLoadingNetwork ? const loadMessage = loadingMessage || isLoadingNetwork ?
'Searching for Network' : null `Connecting to ${this.getNetworkName()}` : null
log.debug('Main ui render function') log.debug('Main ui render function')
@ -135,7 +136,7 @@ App.prototype.renderAppBar = function () {
}, },
}, [ }, [
h('div', { h('div.left-menu-section', {
style: { style: {
display: 'flex', display: 'flex',
flexDirection: 'row', flexDirection: 'row',
@ -150,21 +151,15 @@ App.prototype.renderAppBar = function () {
src: '/images/icon-128.png', src: '/images/icon-128.png',
}), }),
h('#network-spacer.flex-center', { h(NetworkIndicator, {
style: { network: this.props.network,
marginRight: '-72px', provider: this.props.provider,
onClick: (event) => {
event.preventDefault()
event.stopPropagation()
this.setState({ isNetworkMenuOpen: !isNetworkMenuOpen })
}, },
}, [ }),
h(NetworkIndicator, {
network: this.props.network,
provider: this.props.provider,
onClick: (event) => {
event.preventDefault()
event.stopPropagation()
this.setState({ isNetworkMenuOpen: !isNetworkMenuOpen })
},
}),
]),
]), ]),
// metamask name // metamask name
@ -458,6 +453,10 @@ App.prototype.renderPrimary = function () {
log.debug('rendering confirm tx screen') log.debug('rendering confirm tx screen')
return h(ConfirmTxScreen, {key: 'confirm-tx'}) return h(ConfirmTxScreen, {key: 'confirm-tx'})
case 'add-token':
log.debug('rendering add-token screen from unlock screen.')
return h(AddTokenScreen, {key: 'add-token'})
case 'config': case 'config':
log.debug('rendering config screen') log.debug('rendering config screen')
return h(ConfigScreen, {key: 'config'}) return h(ConfigScreen, {key: 'config'})
@ -550,6 +549,27 @@ App.prototype.renderCustomOption = function (provider) {
} }
} }
App.prototype.getNetworkName = function () {
const { provider } = this.props
const providerName = provider.type
let name
if (providerName === 'mainnet') {
name = 'Main Ethereum Network'
} else if (providerName === 'ropsten') {
name = 'Ropsten Test Network'
} else if (providerName === 'kovan') {
name = 'Kovan Test Network'
} else if (providerName === 'rinkeby') {
name = 'Rinkeby Test Network'
} else {
name = 'Unknown Private Network'
}
return name
}
App.prototype.renderCommonRpc = function (rpcList, provider) { App.prototype.renderCommonRpc = function (rpcList, provider) {
const { rpcTarget } = provider const { rpcTarget } = provider
const props = this.props const props = this.props

@ -41,7 +41,6 @@ EnsInput.prototype.render = function () {
this.checkName() this.checkName()
}, },
}) })
return h('div', { return h('div', {
style: { width: '100%' }, style: { width: '100%' },
}, [ }, [
@ -55,6 +54,7 @@ EnsInput.prototype.render = function () {
return h('option', { return h('option', {
value: identity.address, value: identity.address,
label: identity.name, label: identity.name,
key: identity.address,
}) })
}), }),
// Corresponds to previously sent-to addresses. // Corresponds to previously sent-to addresses.

@ -22,15 +22,24 @@ Network.prototype.render = function () {
let iconName, hoverText let iconName, hoverText
if (networkNumber === 'loading') { if (networkNumber === 'loading') {
return h('img.network-indicator', { return h('span', {
title: 'Attempting to connect to blockchain.',
onClick: (event) => this.props.onClick(event),
style: { style: {
width: '27px', display: 'flex',
marginRight: '-27px', alignItems: 'center',
flexDirection: 'row',
}, },
src: 'images/loading.svg', onClick: (event) => this.props.onClick(event),
}) }, [
h('img', {
title: 'Attempting to connect to blockchain.',
style: {
width: '27px',
},
src: 'images/loading.svg',
}),
h('i.fa.fa-sort-desc'),
])
} else if (providerName === 'mainnet') { } else if (providerName === 'mainnet') {
hoverText = 'Main Ethereum Network' hoverText = 'Main Ethereum Network'
iconName = 'ethereum-network' iconName = 'ethereum-network'

@ -315,7 +315,7 @@ PendingTx.prototype.render = function () {
// Accept Button // Accept Button
h('input.confirm.btn-green', { h('input.confirm.btn-green', {
type: 'submit', type: 'submit',
value: 'ACCEPT', value: 'SUBMIT',
style: { marginLeft: '10px' }, style: { marginLeft: '10px' },
disabled: insufficientBalance || !this.state.valid || !isValidAddress || this.state.submitting, disabled: insufficientBalance || !this.state.valid || !isValidAddress || this.state.submitting,
}), }),

@ -2,6 +2,7 @@ const Component = require('react').Component
const h = require('react-hyperscript') const h = require('react-hyperscript')
const inherits = require('util').inherits const inherits = require('util').inherits
const Identicon = require('./identicon') const Identicon = require('./identicon')
const prefixForNetwork = require('../../lib/etherscan-prefix-for-network')
module.exports = TokenCell module.exports = TokenCell
@ -17,12 +18,7 @@ TokenCell.prototype.render = function () {
return ( return (
h('li.token-cell', { h('li.token-cell', {
style: { cursor: network === '1' ? 'pointer' : 'default' }, style: { cursor: network === '1' ? 'pointer' : 'default' },
onClick: (event) => { onClick: this.view.bind(this, address, userAddress, network),
const url = urlFor(address, userAddress, network)
if (url) {
navigateTo(url)
}
},
}, [ }, [
h(Identicon, { h(Identicon, {
@ -32,15 +28,45 @@ TokenCell.prototype.render = function () {
}), }),
h('h3', `${string || 0} ${symbol}`), h('h3', `${string || 0} ${symbol}`),
h('span', { style: { flex: '1 0 auto' } }),
/*
h('button', {
onClick: this.send.bind(this, address),
}, 'SEND'),
*/
]) ])
) )
} }
TokenCell.prototype.send = function (address, event) {
event.preventDefault()
event.stopPropagation()
const url = tokenFactoryFor(address)
if (url) {
navigateTo(url)
}
}
TokenCell.prototype.view = function (address, userAddress, network, event) {
const url = etherscanLinkFor(address, userAddress, network)
if (url) {
navigateTo(url)
}
}
function navigateTo (url) { function navigateTo (url) {
global.platform.openWindow({ url }) global.platform.openWindow({ url })
} }
function urlFor (tokenAddress, address, network) { function etherscanLinkFor (tokenAddress, address, network) {
return `https://etherscan.io/token/${tokenAddress}?a=${address}` const prefix = prefixForNetwork(network)
return `https://${prefix}etherscan.io/token/${tokenAddress}?a=${address}`
}
function tokenFactoryFor (tokenAddress) {
return `https://tokenfactory.surge.sh/#/token/${tokenAddress}`
} }

@ -3,14 +3,15 @@ const h = require('react-hyperscript')
const inherits = require('util').inherits const inherits = require('util').inherits
const TokenTracker = require('eth-token-tracker') const TokenTracker = require('eth-token-tracker')
const TokenCell = require('./token-cell.js') const TokenCell = require('./token-cell.js')
const contracts = require('eth-contract-metadata') const normalizeAddress = require('eth-sig-util').normalize
const tokens = [] const defaultTokens = []
const contracts = require('eth-contract-metadata')
for (const address in contracts) { for (const address in contracts) {
const contract = contracts[address] const contract = contracts[address]
if (contract.erc20) { if (contract.erc20) {
contract.address = address contract.address = address
tokens.push(contract) defaultTokens.push(contract)
} }
} }
@ -18,15 +19,18 @@ module.exports = TokenList
inherits(TokenList, Component) inherits(TokenList, Component)
function TokenList () { function TokenList () {
this.state = { tokens, isLoading: true, network: null } this.state = {
tokens: [],
isLoading: true,
network: null,
}
Component.call(this) Component.call(this)
} }
TokenList.prototype.render = function () { TokenList.prototype.render = function () {
const state = this.state const state = this.state
const { tokens, isLoading, error } = state const { tokens, isLoading, error } = state
const { userAddress, network } = this.props
const { userAddress } = this.props
if (isLoading) { if (isLoading) {
return this.message('Loading') return this.message('Loading')
@ -37,40 +41,65 @@ TokenList.prototype.render = function () {
return this.message('There was a problem loading your token balances.') return this.message('There was a problem loading your token balances.')
} }
const network = this.props.network
const tokenViews = tokens.map((tokenData) => { const tokenViews = tokens.map((tokenData) => {
tokenData.network = network tokenData.network = network
tokenData.userAddress = userAddress tokenData.userAddress = userAddress
return h(TokenCell, tokenData) return h(TokenCell, tokenData)
}) })
return ( return h('div', [
h('ol', { h('ol', {
style: { style: {
height: '302px', height: '260px',
overflowY: 'auto', overflowY: 'auto',
display: 'flex',
flexDirection: 'column',
}, },
}, [h('style', ` }, [
h('style', `
li.token-cell {
display: flex; li.token-cell {
flex-direction: row; display: flex;
align-items: center; flex-direction: row;
padding: 10px; align-items: center;
} padding: 10px;
}
li.token-cell > h3 {
margin-left: 12px; li.token-cell > h3 {
} margin-left: 12px;
}
li.token-cell:hover {
background: white; li.token-cell:hover {
cursor: pointer; background: white;
} cursor: pointer;
}
`),
...tokenViews,
tokenViews.length ? null : this.message('No Tokens Found.'),
]),
this.addTokenButtonElement(),
])
}
`)].concat(tokenViews.length ? tokenViews : this.message('No Tokens Found.'))) TokenList.prototype.addTokenButtonElement = function () {
) return h('div', [
h('div.footer.hover-white.pointer', {
key: 'reveal-account-bar',
onClick: () => {
this.props.addToken()
},
style: {
display: 'flex',
height: '40px',
padding: '10px',
justifyContent: 'center',
alignItems: 'center',
},
}, [
h('i.fa.fa-plus.fa-lg'),
]),
])
} }
TokenList.prototype.message = function (body) { TokenList.prototype.message = function (body) {
@ -80,6 +109,7 @@ TokenList.prototype.message = function (body) {
height: '250px', height: '250px',
alignItems: 'center', alignItems: 'center',
justifyContent: 'center', justifyContent: 'center',
padding: '30px',
}, },
}, body) }, body)
} }
@ -101,7 +131,7 @@ TokenList.prototype.createFreshTokenTracker = function () {
this.tracker = new TokenTracker({ this.tracker = new TokenTracker({
userAddress, userAddress,
provider: global.ethereumProvider, provider: global.ethereumProvider,
tokens: tokens, tokens: uniqueMergeTokens(defaultTokens, this.props.tokens),
pollingInterval: 8000, pollingInterval: 8000,
}) })
@ -135,8 +165,10 @@ TokenList.prototype.componentWillUpdate = function (nextProps) {
} }
} }
TokenList.prototype.updateBalances = function (tokenData) { TokenList.prototype.updateBalances = function (tokens) {
const heldTokens = tokenData.filter(token => token.balance !== '0' && token.string !== '0.000') const heldTokens = tokens.filter(token => {
return token.balance !== '0' && token.string !== '0.000'
})
this.setState({ tokens: heldTokens, isLoading: false }) this.setState({ tokens: heldTokens, isLoading: false })
} }
@ -145,3 +177,16 @@ TokenList.prototype.componentWillUnmount = function () {
this.tracker.stop() this.tracker.stop()
} }
function uniqueMergeTokens (tokensA, tokensB) {
const uniqueAddresses = []
const result = []
tokensA.concat(tokensB).forEach((token) => {
const normal = normalizeAddress(token.address)
if (!uniqueAddresses.includes(normal)) {
uniqueAddresses.push(normal)
result.push(token)
}
})
return result
}

@ -5,6 +5,7 @@ const connect = require('react-redux').connect
const actions = require('./actions') const actions = require('./actions')
const currencies = require('./conversion.json').rows const currencies = require('./conversion.json').rows
const validUrl = require('valid-url') const validUrl = require('valid-url')
const copyToClipboard = require('copy-to-clipboard')
module.exports = connect(mapStateToProps)(ConfigScreen) module.exports = connect(mapStateToProps)(ConfigScreen)
@ -85,8 +86,35 @@ ConfigScreen.prototype.render = function () {
}, },
}, 'Save'), }, 'Save'),
]), ]),
h('hr.horizontal-line'), h('hr.horizontal-line'),
currentConversionInformation(metamaskState, state), currentConversionInformation(metamaskState, state),
h('hr.horizontal-line'),
h('div', {
style: {
marginTop: '20px',
},
}, [
h('p', {
style: {
fontFamily: 'Montserrat Light',
fontSize: '13px',
},
}, `State logs contain your public account addresses and sent transactions.`),
h('br'),
h('button', {
style: {
alignSelf: 'center',
},
onClick (event) {
copyToClipboard(window.logState())
},
}, 'Copy State Logs'),
]),
h('hr.horizontal-line'), h('hr.horizontal-line'),
h('div', { h('div', {

@ -20,7 +20,7 @@ function mapStateToProps (state) {
CreateVaultCompleteScreen.prototype.render = function () { CreateVaultCompleteScreen.prototype.render = function () {
var state = this.props var state = this.props
var seed = state.seed || state.cachedSeed var seed = state.seed || state.cachedSeed || ''
return ( return (

@ -103,7 +103,17 @@ function reduceApp (state, action) {
transForward: action.value, transForward: action.value,
}) })
case actions.SHOW_ADD_TOKEN_PAGE:
return extend(appState, {
currentView: {
name: 'add-token',
context: appState.currentView.context,
},
transForward: action.value,
})
case actions.SHOW_IMPORT_PAGE: case actions.SHOW_IMPORT_PAGE:
return extend(appState, { return extend(appState, {
currentView: { currentView: {
name: 'import-menu', name: 'import-menu',

@ -189,7 +189,7 @@ SendTransactionScreen.prototype.render = function () {
style: { style: {
textTransform: 'uppercase', textTransform: 'uppercase',
}, },
}, 'Send'), }, 'Next'),
]), ]),
@ -244,7 +244,7 @@ SendTransactionScreen.prototype.recipientDidChange = function (recipient, nickna
SendTransactionScreen.prototype.onSubmit = function () { SendTransactionScreen.prototype.onSubmit = function () {
const state = this.state || {} const state = this.state || {}
const recipient = state.recipient || document.querySelector('input[name="address"]').value const recipient = state.recipient || document.querySelector('input[name="address"]').value.replace(/^[.\s]+|[.\s]+$/g, '')
const nickname = state.nickname || ' ' const nickname = state.nickname || ' '
const input = document.querySelector('input[name="amount"]').value const input = document.querySelector('input[name="amount"]').value
const value = util.normalizeEthStringToWei(input) const value = util.normalizeEthStringToWei(input)

@ -0,0 +1,21 @@
module.exports = function (network) {
const net = parseInt(network)
let prefix
switch (net) {
case 1: // main net
prefix = ''
break
case 3: // ropsten test net
prefix = 'ropsten.'
break
case 4: // rinkeby test net
prefix = 'rinkeby.'
break
case 42: // kovan test net
prefix = 'kovan.'
break
default:
prefix = ''
}
return prefix
}

@ -1,21 +1,6 @@
const prefixForNetwork = require('./etherscan-prefix-for-network')
module.exports = function (hash, network) { module.exports = function (hash, network) {
const net = parseInt(network) const prefix = prefixForNetwork(network)
let prefix
switch (net) {
case 1: // main net
prefix = ''
break
case 3: // ropsten test net
prefix = 'ropsten.'
break
case 4: // rinkeby test net
prefix = 'rinkeby.'
break
case 42: // kovan test net
prefix = 'kovan.'
break
default:
prefix = ''
}
return `http://${prefix}etherscan.io/tx/${hash}` return `http://${prefix}etherscan.io/tx/${hash}`
} }

Loading…
Cancel
Save