mirror of
https://github.com/kremalicious/metamask-extension.git
synced 2024-12-23 09:52:26 +01:00
Merge branch 'master' into nonce-tracker
This commit is contained in:
commit
d228f46254
27
CHANGELOG.md
27
CHANGELOG.md
@ -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)
|
||||||
|
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('./012'),
|
||||||
require('./013'),
|
require('./013'),
|
||||||
require('./014'),
|
require('./014'),
|
||||||
|
require('./015'),
|
||||||
]
|
]
|
||||||
|
12
package.json
12
package.json
@ -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,
|
||||||
|
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')
|
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 {
|
li.token-cell {
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: row;
|
flex-direction: row;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
padding: 10px;
|
padding: 10px;
|
||||||
}
|
}
|
||||||
|
|
||||||
li.token-cell > h3 {
|
li.token-cell > h3 {
|
||||||
margin-left: 12px;
|
margin-left: 12px;
|
||||||
}
|
}
|
||||||
|
|
||||||
li.token-cell:hover {
|
li.token-cell:hover {
|
||||||
background: white;
|
background: white;
|
||||||
cursor: pointer;
|
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) {
|
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)
|
||||||
|
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) {
|
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…
Reference in New Issue
Block a user