mirror of
https://github.com/kremalicious/metamask-extension.git
synced 2024-12-23 09:52:26 +01:00
Merge branch 'nonce-tracker' of https://github.com/MetaMask/metamask-plugin into nonce-tracker
This commit is contained in:
commit
a670e54973
27
CHANGELOG.md
27
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
|
||||
|
||||
|
@ -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",
|
||||
|
@ -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) => {
|
||||
|
@ -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()
|
||||
@ -36,7 +34,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
|
||||
@ -162,13 +163,15 @@ module.exports = class TransactionController extends EventEmitter {
|
||||
const txParams = txMeta.txParams
|
||||
// ensure value
|
||||
txParams.value = txParams.value || '0x0'
|
||||
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 () {
|
||||
@ -429,10 +432,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) {
|
||||
@ -443,15 +460,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
|
||||
|
@ -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)
|
||||
|
38
app/scripts/migrations/015.js
Normal file
38
app/scripts/migrations/015.js
Normal file
@ -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('./013'),
|
||||
require('./014'),
|
||||
require('./015'),
|
||||
]
|
||||
|
12
package.json
12
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",
|
||||
|
@ -18,9 +18,10 @@ describe('Transaction Controller', function () {
|
||||
txController = new TransactionController({
|
||||
networkStore: new ObservableStore(currentNetworkId),
|
||||
txHistoryLimit: 10,
|
||||
blockTracker: { getCurrentBlock: noop, on: noop },
|
||||
blockTracker: { getCurrentBlock: noop, on: noop, once: noop },
|
||||
provider: { sendAsync: noop },
|
||||
ethQuery: new EthQuery({ sendAsync: noop }),
|
||||
ethStore: { getState: noop },
|
||||
signTransaction: (ethTx) => new Promise((resolve) => {
|
||||
ethTx.sign(privKey)
|
||||
resolve()
|
||||
@ -269,7 +270,7 @@ describe('Transaction Controller', function () {
|
||||
})
|
||||
|
||||
|
||||
it('does not overwrite set values', function (done) {
|
||||
it('does not overwrite set values', function () {
|
||||
this.timeout(15000)
|
||||
const wrongValue = '0x05'
|
||||
|
||||
@ -288,9 +289,7 @@ describe('Transaction Controller', function () {
|
||||
const pubStub = sinon.stub(txController.txProviderUtils, 'publishTransaction')
|
||||
.callsArgWithAsync(1, null, originalValue)
|
||||
|
||||
txController.approveTransaction(txMeta.id).then((err) => {
|
||||
assert.ifError(err, 'should not error')
|
||||
|
||||
return txController.approveTransaction(txMeta.id).then(() => {
|
||||
const result = txController.getTx(txMeta.id)
|
||||
const params = result.txParams
|
||||
|
||||
@ -302,7 +301,6 @@ describe('Transaction Controller', function () {
|
||||
priceStub.restore()
|
||||
signStub.restore()
|
||||
pubStub.restore()
|
||||
done()
|
||||
})
|
||||
})
|
||||
})
|
||||
@ -318,4 +316,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,
|
||||
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()
|
||||
}
|
||||
|
@ -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',
|
||||
|
@ -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,
|
||||
|
219
ui/app/add-token.js
Normal file
219
ui/app/add-token.js
Normal file
@ -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')
|
||||
// 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,11 +151,6 @@ 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,
|
||||
@ -165,7 +161,6 @@ App.prototype.renderAppBar = function () {
|
||||
},
|
||||
}),
|
||||
]),
|
||||
]),
|
||||
|
||||
// metamask name
|
||||
props.isUnlocked && h('h1', {
|
||||
@ -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
|
||||
|
@ -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.
|
||||
|
@ -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.',
|
||||
return h('span', {
|
||||
style: {
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
flexDirection: 'row',
|
||||
},
|
||||
onClick: (event) => this.props.onClick(event),
|
||||
}, [
|
||||
h('img', {
|
||||
title: 'Attempting to connect to blockchain.',
|
||||
style: {
|
||||
width: '27px',
|
||||
marginRight: '-27px',
|
||||
},
|
||||
src: 'images/loading.svg',
|
||||
})
|
||||
}),
|
||||
h('i.fa.fa-sort-desc'),
|
||||
])
|
||||
|
||||
} else if (providerName === 'mainnet') {
|
||||
hoverText = 'Main Ethereum Network'
|
||||
iconName = 'ethereum-network'
|
||||
|
@ -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,
|
||||
}),
|
||||
|
@ -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}`
|
||||
}
|
||||
|
||||
|
@ -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,21 +41,22 @@ 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', `
|
||||
}, [
|
||||
h('style', `
|
||||
|
||||
li.token-cell {
|
||||
display: flex;
|
||||
@ -69,8 +74,32 @@ TokenList.prototype.render = function () {
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
`)].concat(tokenViews.length ? tokenViews : this.message('No Tokens Found.')))
|
||||
)
|
||||
`),
|
||||
...tokenViews,
|
||||
tokenViews.length ? null : this.message('No Tokens Found.'),
|
||||
]),
|
||||
this.addTokenButtonElement(),
|
||||
])
|
||||
}
|
||||
|
||||
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
|
||||
}
|
||||
|
||||
|
@ -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', {
|
||||
|
@ -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 (
|
||||
|
||||
|
@ -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',
|
||||
|
@ -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)
|
||||
|
21
ui/lib/etherscan-prefix-for-network.js
Normal file
21
ui/lib/etherscan-prefix-for-network.js
Normal file
@ -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) {
|
||||
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}`
|
||||
}
|
||||
|
Loading…
Reference in New Issue
Block a user