aboutsummaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
authorkumavis <kumavis@users.noreply.github.com>2017-07-08 05:21:43 +0800
committerGitHub <noreply@github.com>2017-07-08 05:21:43 +0800
commitd228f46254783885e9762a2f96ecb9c3077c5fb3 (patch)
treeb0e0fde9f209c3728f7a1e36d3c4417ea7a3852d
parent51ff6d74e884b599f78b5454b33f2bc1a046f0b2 (diff)
parent678cfd21755bab430d28a8f0d0fb44a81ecefade (diff)
downloadtangerine-wallet-browser-d228f46254783885e9762a2f96ecb9c3077c5fb3.tar
tangerine-wallet-browser-d228f46254783885e9762a2f96ecb9c3077c5fb3.tar.gz
tangerine-wallet-browser-d228f46254783885e9762a2f96ecb9c3077c5fb3.tar.bz2
tangerine-wallet-browser-d228f46254783885e9762a2f96ecb9c3077c5fb3.tar.lz
tangerine-wallet-browser-d228f46254783885e9762a2f96ecb9c3077c5fb3.tar.xz
tangerine-wallet-browser-d228f46254783885e9762a2f96ecb9c3077c5fb3.tar.zst
tangerine-wallet-browser-d228f46254783885e9762a2f96ecb9c3077c5fb3.zip
Merge branch 'master' into nonce-tracker
-rw-r--r--CHANGELOG.md27
-rw-r--r--app/manifest.json2
-rw-r--r--app/scripts/controllers/preferences.js29
-rw-r--r--app/scripts/controllers/transactions.js65
-rw-r--r--app/scripts/metamask-controller.js12
-rw-r--r--app/scripts/migrations/015.js38
-rw-r--r--app/scripts/migrations/index.js1
-rw-r--r--package.json12
-rw-r--r--test/unit/tx-controller-test.js40
-rw-r--r--ui/app/account-detail.js10
-rw-r--r--ui/app/accounts/import/index.js9
-rw-r--r--ui/app/actions.js27
-rw-r--r--ui/app/add-token.js219
-rw-r--r--ui/app/app.js54
-rw-r--r--ui/app/components/ens-input.js2
-rw-r--r--ui/app/components/network.js23
-rw-r--r--ui/app/components/pending-tx.js2
-rw-r--r--ui/app/components/token-cell.js42
-rw-r--r--ui/app/components/token-list.js109
-rw-r--r--ui/app/config.js28
-rw-r--r--ui/app/keychains/hd/create-vault-complete.js2
-rw-r--r--ui/app/reducers/app.js10
-rw-r--r--ui/app/send.js4
-rw-r--r--ui/lib/etherscan-prefix-for-network.js21
-rw-r--r--ui/lib/explorer-link.js21
25 files changed, 686 insertions, 123 deletions
diff --git a/CHANGELOG.md b/CHANGELOG.md
index 96dc79d9a..3966ea1bb 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -2,13 +2,40 @@
## 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 ability to add Tokens to token list.
- 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 badge count did not reflect personal_sign pending messages.
- Seed word confirmation wording is now scarier.
- Fix error for invalid seed words.
- 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
diff --git a/app/manifest.json b/app/manifest.json
index 7ae20158c..aafc33e66 100644
--- a/app/manifest.json
+++ b/app/manifest.json
@@ -1,7 +1,7 @@
{
"name": "MetaMask",
"short_name": "Metamask",
- "version": "3.7.8",
+ "version": "3.8.3",
"manifest_version": 2,
"author": "https://metamask.io",
"description": "Ethereum Browser Extension",
diff --git a/app/scripts/controllers/preferences.js b/app/scripts/controllers/preferences.js
index aa8e05fcc..e45224593 100644
--- a/app/scripts/controllers/preferences.js
+++ b/app/scripts/controllers/preferences.js
@@ -8,13 +8,11 @@ class PreferencesController {
const initState = extend({
frequentRpcList: [],
currentAccountTab: 'history',
+ tokens: [],
}, opts.initState)
this.store = new ObservableStore(initState)
}
-
- //
- // PUBLIC METHODS
- //
+// PUBLIC METHODS
setSelectedAddress (_address) {
return new Promise((resolve, reject) => {
@@ -28,6 +26,29 @@ class PreferencesController {
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) {
return this.addToFrequentRpcList(_url)
.then((rpcList) => {
diff --git a/app/scripts/controllers/transactions.js b/app/scripts/controllers/transactions.js
index 2b40f9456..6bf980a22 100644
--- a/app/scripts/controllers/transactions.js
+++ b/app/scripts/controllers/transactions.js
@@ -8,8 +8,6 @@ const TxProviderUtil = require('../lib/tx-utils')
const createId = require('../lib/random-id')
const NonceTracker = require('../lib/nonce-tracker')
-const RETRY_LIMIT = 200
-
module.exports = class TransactionController extends EventEmitter {
constructor (opts) {
super()
@@ -37,7 +35,10 @@ module.exports = class TransactionController extends EventEmitter {
this.query = opts.ethQuery
this.txProviderUtils = new TxProviderUtil(this.query)
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.signEthTx = opts.signTransaction
this.ethStore = opts.ethStore
@@ -163,13 +164,15 @@ module.exports = class TransactionController extends EventEmitter {
const txParams = txMeta.txParams
// ensure value
txParams.value = txParams.value || '0x0'
- this.query.gasPrice((err, gasPrice) => {
- if (err) return cb(err)
- // set gasPrice
- txParams.gasPrice = gasPrice
- // set gasLimit
- this.txProviderUtils.analyzeGasUsage(txMeta, cb)
- })
+ if (!txParams.gasPrice) {
+ this.query.gasPrice((err, gasPrice) => {
+ if (err) return cb(err)
+ // set gasPrice
+ txParams.gasPrice = gasPrice
+ })
+ }
+ // set gasLimit
+ this.txProviderUtils.analyzeGasUsage(txMeta, cb)
}
getUnapprovedTxList () {
@@ -430,10 +433,24 @@ module.exports = class TransactionController extends EventEmitter {
// only try resubmitting if their are transactions to resubmit
if (!pending.length) return
const resubmit = denodeify(this._resubmitTx.bind(this))
- Promise.all(pending.map(txMeta => resubmit(txMeta)))
+ pending.forEach((txMeta) => resubmit(txMeta)
.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) {
@@ -444,15 +461,25 @@ module.exports = class TransactionController extends EventEmitter {
const gtBalance = Number.parseInt(txMeta.txParams.value) > Number.parseInt(balance)
if (!('retryCount' in txMeta)) txMeta.retryCount = 0
- // if the value of the transaction is greater then the balance
- // or the nonce of the transaction is lower then the accounts nonce
- // dont resubmit the tx
- if (gtBalance || txNonce < nonce) return cb()
+ // if the value of the transaction is greater then the balance, fail.
+ if (gtBalance) {
+ const message = 'Insufficient balance.'
+ 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:
if (!('rawTx' in txMeta)) return cb()
- if (txMeta.retryCount > RETRY_LIMIT) return
-
// Increment a try counter.
txMeta.retryCount++
const rawTx = txMeta.rawTx
diff --git a/app/scripts/metamask-controller.js b/app/scripts/metamask-controller.js
index be64a45c7..573594b39 100644
--- a/app/scripts/metamask-controller.js
+++ b/app/scripts/metamask-controller.js
@@ -184,7 +184,9 @@ module.exports = class MetamaskController extends EventEmitter {
eth_syncing: false,
web3_clientVersion: `MetaMask/v${version}`,
},
+ // rpc data source
rpcUrl: this.networkController.getCurrentRpcAddress(),
+ originHttpHeaderKey: 'X-Metamask-Origin',
// account mgmt
getAccounts: (cb) => {
const isUnlocked = this.keyringController.memStore.getState().isUnlocked
@@ -293,6 +295,7 @@ module.exports = class MetamaskController extends EventEmitter {
// PreferencesController
setSelectedAddress: nodeify(preferencesController.setSelectedAddress).bind(preferencesController),
+ addToken: nodeify(preferencesController.addToken).bind(preferencesController),
setCurrentAccountTab: nodeify(preferencesController.setCurrentAccountTab).bind(preferencesController),
setDefaultRpc: nodeify(this.setDefaultRpc).bind(this),
setCustomRpc: nodeify(this.setCustomRpc).bind(this),
@@ -355,8 +358,13 @@ module.exports = class MetamaskController extends EventEmitter {
}
setupProviderConnection (outStream, originDomain) {
- streamIntoProvider(outStream, this.provider, logger)
- function logger (err, request, response) {
+ streamIntoProvider(outStream, this.provider, onRequest, onResponse)
+ // 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 (response.error) {
console.error('Error in RPC response:\n', response.error)
diff --git a/app/scripts/migrations/015.js b/app/scripts/migrations/015.js
new file mode 100644
index 000000000..4b839580b
--- /dev/null
+++ b/app/scripts/migrations/015.js
@@ -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
+}
diff --git a/app/scripts/migrations/index.js b/app/scripts/migrations/index.js
index fb1ad7863..651ee6a9c 100644
--- a/app/scripts/migrations/index.js
+++ b/app/scripts/migrations/index.js
@@ -25,4 +25,5 @@ module.exports = [
require('./012'),
require('./013'),
require('./014'),
+ require('./015'),
]
diff --git a/package.json b/package.json
index 82792351d..16149f7b7 100644
--- a/package.json
+++ b/package.json
@@ -68,7 +68,7 @@
"eth-query": "^2.1.2",
"eth-sig-util": "^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-util": "ethereumjs/ethereumjs-util#ac5d0908536b447083ea422b435da27f26615de9",
"ethereumjs-wallet": "^0.6.0",
@@ -104,7 +104,7 @@
"qrcode-npm": "0.0.3",
"react": "^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-markdown": "^2.3.0",
"react-redux": "^4.4.5",
@@ -124,9 +124,9 @@
"through2": "^2.0.1",
"valid-url": "^1.0.9",
"vreme": "^3.0.2",
- "web3": "0.18.2",
- "web3-provider-engine": "^13.0.3",
- "web3-stream-provider": "^2.0.6",
+ "web3": "0.19.1",
+ "web3-provider-engine": "^13.1.1",
+ "web3-stream-provider": "^3.0.1",
"xtend": "^4.0.1"
},
"devDependencies": {
@@ -142,7 +142,6 @@
"brfs": "^1.4.3",
"browserify": "^13.0.0",
"chai": "^3.5.0",
- "clone": "^1.0.2",
"deep-freeze-strict": "^1.1.1",
"del": "^2.2.0",
"envify": "^4.0.0",
@@ -174,7 +173,6 @@
"qs": "^6.2.0",
"qunit": "^0.9.1",
"react-addons-test-utils": "^15.5.1",
- "react-dom": "^15.5.4",
"react-test-renderer": "^15.5.4",
"react-testutils-additions": "^15.2.0",
"sinon": "^1.17.3",
diff --git a/test/unit/tx-controller-test.js b/test/unit/tx-controller-test.js
index 8ce6a5a65..c9eff5d01 100644
--- a/test/unit/tx-controller-test.js
+++ b/test/unit/tx-controller-test.js
@@ -21,6 +21,7 @@ describe('Transaction Controller', function () {
blockTracker: { getCurrentBlock: noop, on: noop },
provider: { sendAsync: noop },
ethQuery: new EthQuery({ sendAsync: noop }),
+ ethStore: { getState: noop },
signTransaction: (ethTx) => new Promise((resolve) => {
ethTx.sign(privKey)
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()
+ })
+ })
+ })
})
+
diff --git a/ui/app/account-detail.js b/ui/app/account-detail.js
index 836032b3c..bed05a7fb 100644
--- a/ui/app/account-detail.js
+++ b/ui/app/account-detail.js
@@ -35,6 +35,7 @@ function mapStateToProps (state) {
conversionRate: state.metamask.conversionRate,
currentCurrency: state.metamask.currentCurrency,
currentAccountTab: state.metamask.currentAccountTab,
+ tokens: state.metamask.tokens,
}
}
@@ -273,11 +274,16 @@ AccountDetailScreen.prototype.tabSections = function () {
AccountDetailScreen.prototype.tabSwitchView = function () {
const props = this.props
const { address, network } = props
- const { currentAccountTab } = this.props
+ const { currentAccountTab, tokens } = this.props
switch (currentAccountTab) {
case 'tokens':
- return h(TokenList, { userAddress: address, network })
+ return h(TokenList, {
+ userAddress: address,
+ network,
+ tokens,
+ addToken: () => this.props.dispatch(actions.showAddTokenPage()),
+ })
default:
return this.transactionList()
}
diff --git a/ui/app/accounts/import/index.js b/ui/app/accounts/import/index.js
index a0f0f9bdb..97b387229 100644
--- a/ui/app/accounts/import/index.js
+++ b/ui/app/accounts/import/index.js
@@ -2,6 +2,7 @@ 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')
import Select from 'react-select'
// Subviews
@@ -37,6 +38,14 @@ AccountImportSubview.prototype.render = function () {
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', {
style: {
padding: '10px',
diff --git a/ui/app/actions.js b/ui/app/actions.js
index b6b5d6eb1..d99291e46 100644
--- a/ui/app/actions.js
+++ b/ui/app/actions.js
@@ -121,7 +121,10 @@ var actions = {
SET_PROVIDER_TYPE: 'SET_PROVIDER_TYPE',
USE_ETHERSCAN_PROVIDER: 'USE_ETHERSCAN_PROVIDER',
useEtherscanProvider: useEtherscanProvider,
- showConfigPage: showConfigPage,
+ showConfigPage,
+ SHOW_ADD_TOKEN_PAGE: 'SHOW_ADD_TOKEN_PAGE',
+ showAddTokenPage,
+ addToken,
setRpcTarget: setRpcTarget,
setDefaultRpcTarget: setDefaultRpcTarget,
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 () {
return {
type: actions.BACK_TO_INIT_MENU,
diff --git a/ui/app/add-token.js b/ui/app/add-token.js
new file mode 100644
index 000000000..15ef7a852
--- /dev/null
+++ b/ui/app/add-token.js
@@ -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() })
+ }
+}
+
diff --git a/ui/app/app.js b/ui/app/app.js
index d444a8349..1a63002e1 100644
--- a/ui/app/app.js
+++ b/ui/app/app.js
@@ -19,6 +19,7 @@ const NoticeScreen = require('./components/notice')
const generateLostAccountsNotice = require('../lib/lost-accounts-notice')
// other views
const ConfigScreen = require('./config')
+const AddTokenScreen = require('./add-token')
const Import = require('./accounts/import')
const InfoScreen = require('./info')
const Loading = require('./components/loading')
@@ -65,9 +66,9 @@ function mapStateToProps (state) {
App.prototype.render = function () {
var props = this.props
const { isLoading, loadingMessage, transForward, network } = props
- const isLoadingNetwork = network === 'loading'
+ const isLoadingNetwork = network === 'loading' && props.currentView.name !== 'config'
const loadMessage = loadingMessage || isLoadingNetwork ?
- 'Searching for Network' : null
+ `Connecting to ${this.getNetworkName()}` : null
log.debug('Main ui render function')
@@ -135,7 +136,7 @@ App.prototype.renderAppBar = function () {
},
}, [
- h('div', {
+ h('div.left-menu-section', {
style: {
display: 'flex',
flexDirection: 'row',
@@ -150,21 +151,15 @@ App.prototype.renderAppBar = function () {
src: '/images/icon-128.png',
}),
- h('#network-spacer.flex-center', {
- style: {
- marginRight: '-72px',
+ h(NetworkIndicator, {
+ network: this.props.network,
+ 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
@@ -458,6 +453,10 @@ App.prototype.renderPrimary = function () {
log.debug('rendering confirm tx screen')
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':
log.debug('rendering config screen')
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) {
const { rpcTarget } = provider
const props = this.props
diff --git a/ui/app/components/ens-input.js b/ui/app/components/ens-input.js
index 16c50db84..3a33ebf74 100644
--- a/ui/app/components/ens-input.js
+++ b/ui/app/components/ens-input.js
@@ -41,7 +41,6 @@ EnsInput.prototype.render = function () {
this.checkName()
},
})
-
return h('div', {
style: { width: '100%' },
}, [
@@ -55,6 +54,7 @@ EnsInput.prototype.render = function () {
return h('option', {
value: identity.address,
label: identity.name,
+ key: identity.address,
})
}),
// Corresponds to previously sent-to addresses.
diff --git a/ui/app/components/network.js b/ui/app/components/network.js
index 31a8fc17c..d5d3e18cd 100644
--- a/ui/app/components/network.js
+++ b/ui/app/components/network.js
@@ -22,15 +22,24 @@ Network.prototype.render = function () {
let iconName, hoverText
if (networkNumber === 'loading') {
- return h('img.network-indicator', {
- title: 'Attempting to connect to blockchain.',
- onClick: (event) => this.props.onClick(event),
+ return h('span', {
style: {
- width: '27px',
- marginRight: '-27px',
+ display: 'flex',
+ 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') {
hoverText = 'Main Ethereum Network'
iconName = 'ethereum-network'
diff --git a/ui/app/components/pending-tx.js b/ui/app/components/pending-tx.js
index f33a5d948..d7d602f31 100644
--- a/ui/app/components/pending-tx.js
+++ b/ui/app/components/pending-tx.js
@@ -315,7 +315,7 @@ PendingTx.prototype.render = function () {
// Accept Button
h('input.confirm.btn-green', {
type: 'submit',
- value: 'ACCEPT',
+ value: 'SUBMIT',
style: { marginLeft: '10px' },
disabled: insufficientBalance || !this.state.valid || !isValidAddress || this.state.submitting,
}),
diff --git a/ui/app/components/token-cell.js b/ui/app/components/token-cell.js
index d3a895d36..19d7139bb 100644
--- a/ui/app/components/token-cell.js
+++ b/ui/app/components/token-cell.js
@@ -2,6 +2,7 @@ const Component = require('react').Component
const h = require('react-hyperscript')
const inherits = require('util').inherits
const Identicon = require('./identicon')
+const prefixForNetwork = require('../../lib/etherscan-prefix-for-network')
module.exports = TokenCell
@@ -17,12 +18,7 @@ TokenCell.prototype.render = function () {
return (
h('li.token-cell', {
style: { cursor: network === '1' ? 'pointer' : 'default' },
- onClick: (event) => {
- const url = urlFor(address, userAddress, network)
- if (url) {
- navigateTo(url)
- }
- },
+ onClick: this.view.bind(this, address, userAddress, network),
}, [
h(Identicon, {
@@ -32,15 +28,45 @@ TokenCell.prototype.render = function () {
}),
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) {
global.platform.openWindow({ url })
}
-function urlFor (tokenAddress, address, network) {
- return `https://etherscan.io/token/${tokenAddress}?a=${address}`
+function etherscanLinkFor (tokenAddress, address, network) {
+ const prefix = prefixForNetwork(network)
+ return `https://${prefix}etherscan.io/token/${tokenAddress}?a=${address}`
+}
+
+function tokenFactoryFor (tokenAddress) {
+ return `https://tokenfactory.surge.sh/#/token/${tokenAddress}`
}
diff --git a/ui/app/components/token-list.js b/ui/app/components/token-list.js
index 633d3ccfe..20cfa897e 100644
--- a/ui/app/components/token-list.js
+++ b/ui/app/components/token-list.js
@@ -3,14 +3,15 @@ const h = require('react-hyperscript')
const inherits = require('util').inherits
const TokenTracker = require('eth-token-tracker')
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) {
const contract = contracts[address]
if (contract.erc20) {
contract.address = address
- tokens.push(contract)
+ defaultTokens.push(contract)
}
}
@@ -18,15 +19,18 @@ module.exports = TokenList
inherits(TokenList, Component)
function TokenList () {
- this.state = { tokens, isLoading: true, network: null }
+ this.state = {
+ tokens: [],
+ isLoading: true,
+ network: null,
+ }
Component.call(this)
}
TokenList.prototype.render = function () {
const state = this.state
const { tokens, isLoading, error } = state
-
- const { userAddress } = this.props
+ const { userAddress, network } = this.props
if (isLoading) {
return this.message('Loading')
@@ -37,40 +41,65 @@ TokenList.prototype.render = function () {
return this.message('There was a problem loading your token balances.')
}
- const network = this.props.network
-
const tokenViews = tokens.map((tokenData) => {
tokenData.network = network
tokenData.userAddress = userAddress
return h(TokenCell, tokenData)
})
- return (
+ return h('div', [
h('ol', {
style: {
- height: '302px',
+ height: '260px',
overflowY: 'auto',
+ display: 'flex',
+ flexDirection: 'column',
},
- }, [h('style', `
-
- li.token-cell {
- display: flex;
- flex-direction: row;
- align-items: center;
- padding: 10px;
- }
-
- li.token-cell > h3 {
- margin-left: 12px;
- }
-
- li.token-cell:hover {
- background: white;
- cursor: pointer;
- }
+ }, [
+ h('style', `
+
+ li.token-cell {
+ display: flex;
+ flex-direction: row;
+ align-items: center;
+ padding: 10px;
+ }
+
+ li.token-cell > h3 {
+ margin-left: 12px;
+ }
+
+ li.token-cell:hover {
+ 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) {
@@ -80,6 +109,7 @@ TokenList.prototype.message = function (body) {
height: '250px',
alignItems: 'center',
justifyContent: 'center',
+ padding: '30px',
},
}, body)
}
@@ -101,7 +131,7 @@ TokenList.prototype.createFreshTokenTracker = function () {
this.tracker = new TokenTracker({
userAddress,
provider: global.ethereumProvider,
- tokens: tokens,
+ tokens: uniqueMergeTokens(defaultTokens, this.props.tokens),
pollingInterval: 8000,
})
@@ -135,8 +165,10 @@ TokenList.prototype.componentWillUpdate = function (nextProps) {
}
}
-TokenList.prototype.updateBalances = function (tokenData) {
- const heldTokens = tokenData.filter(token => token.balance !== '0' && token.string !== '0.000')
+TokenList.prototype.updateBalances = function (tokens) {
+ const heldTokens = tokens.filter(token => {
+ return token.balance !== '0' && token.string !== '0.000'
+ })
this.setState({ tokens: heldTokens, isLoading: false })
}
@@ -145,3 +177,16 @@ TokenList.prototype.componentWillUnmount = function () {
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
+}
+
diff --git a/ui/app/config.js b/ui/app/config.js
index d7be26757..62785c49b 100644
--- a/ui/app/config.js
+++ b/ui/app/config.js
@@ -5,6 +5,7 @@ const connect = require('react-redux').connect
const actions = require('./actions')
const currencies = require('./conversion.json').rows
const validUrl = require('valid-url')
+const copyToClipboard = require('copy-to-clipboard')
module.exports = connect(mapStateToProps)(ConfigScreen)
@@ -85,8 +86,35 @@ ConfigScreen.prototype.render = function () {
},
}, 'Save'),
]),
+
h('hr.horizontal-line'),
+
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('div', {
diff --git a/ui/app/keychains/hd/create-vault-complete.js b/ui/app/keychains/hd/create-vault-complete.js
index 9741155f7..a318a9b50 100644
--- a/ui/app/keychains/hd/create-vault-complete.js
+++ b/ui/app/keychains/hd/create-vault-complete.js
@@ -20,7 +20,7 @@ function mapStateToProps (state) {
CreateVaultCompleteScreen.prototype.render = function () {
var state = this.props
- var seed = state.seed || state.cachedSeed
+ var seed = state.seed || state.cachedSeed || ''
return (
diff --git a/ui/app/reducers/app.js b/ui/app/reducers/app.js
index deacad0a7..2fcc9bfe0 100644
--- a/ui/app/reducers/app.js
+++ b/ui/app/reducers/app.js
@@ -103,7 +103,17 @@ function reduceApp (state, action) {
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:
+
return extend(appState, {
currentView: {
name: 'import-menu',
diff --git a/ui/app/send.js b/ui/app/send.js
index fd6994145..a21a219eb 100644
--- a/ui/app/send.js
+++ b/ui/app/send.js
@@ -189,7 +189,7 @@ SendTransactionScreen.prototype.render = function () {
style: {
textTransform: 'uppercase',
},
- }, 'Send'),
+ }, 'Next'),
]),
@@ -244,7 +244,7 @@ SendTransactionScreen.prototype.recipientDidChange = function (recipient, nickna
SendTransactionScreen.prototype.onSubmit = function () {
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 input = document.querySelector('input[name="amount"]').value
const value = util.normalizeEthStringToWei(input)
diff --git a/ui/lib/etherscan-prefix-for-network.js b/ui/lib/etherscan-prefix-for-network.js
new file mode 100644
index 000000000..2c1904f1c
--- /dev/null
+++ b/ui/lib/etherscan-prefix-for-network.js
@@ -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
+}
diff --git a/ui/lib/explorer-link.js b/ui/lib/explorer-link.js
index e11249551..3b82ecd5f 100644
--- a/ui/lib/explorer-link.js
+++ b/ui/lib/explorer-link.js
@@ -1,21 +1,6 @@
+const prefixForNetwork = require('./etherscan-prefix-for-network')
+
module.exports = function (hash, 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 = ''
- }
+ const prefix = prefixForNetwork(network)
return `http://${prefix}etherscan.io/tx/${hash}`
}