mirror of
https://github.com/kremalicious/metamask-extension.git
synced 2024-11-23 02:10:12 +01:00
Merge pull request #1620 from MetaMask/i1616-AddTokenAdding
Add ability to add tokens to token list
This commit is contained in:
commit
ffd3773ef3
@ -4,6 +4,7 @@
|
||||
|
||||
- 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.
|
||||
- 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.
|
||||
|
@ -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) => {
|
||||
|
@ -293,6 +293,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),
|
||||
|
@ -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()
|
||||
}
|
||||
|
@ -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 Sybmol'),
|
||||
]),
|
||||
|
||||
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')
|
||||
@ -458,6 +459,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'})
|
||||
|
@ -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
|
||||
|
||||
@ -41,6 +42,7 @@ function navigateTo (url) {
|
||||
}
|
||||
|
||||
function urlFor (tokenAddress, address, network) {
|
||||
return `https://etherscan.io/token/${tokenAddress}?a=${address}`
|
||||
const prefix = prefixForNetwork(network)
|
||||
return `https://${prefix}etherscan.io/token/${tokenAddress}?a=${address}`
|
||||
}
|
||||
|
||||
|
@ -4,13 +4,14 @@ 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 = []
|
||||
for (const address in contracts) {
|
||||
const contract = contracts[address]
|
||||
if (contract.erc20) {
|
||||
contract.address = address
|
||||
tokens.push(contract)
|
||||
defaultTokens.push(contract)
|
||||
}
|
||||
}
|
||||
|
||||
@ -18,15 +19,18 @@ module.exports = TokenList
|
||||
|
||||
inherits(TokenList, Component)
|
||||
function TokenList () {
|
||||
this.state = { tokens, isLoading: true, network: null }
|
||||
this.state = {
|
||||
tokens: [],
|
||||
isLoading: true,
|
||||
network: null,
|
||||
}
|
||||
Component.call(this)
|
||||
}
|
||||
|
||||
TokenList.prototype.render = function () {
|
||||
const state = this.state
|
||||
const { tokens, isLoading, error } = state
|
||||
|
||||
const { userAddress } = this.props
|
||||
const { userAddress, network } = this.props
|
||||
|
||||
if (isLoading) {
|
||||
return this.message('Loading')
|
||||
@ -37,40 +41,65 @@ TokenList.prototype.render = function () {
|
||||
return this.message('There was a problem loading your token balances.')
|
||||
}
|
||||
|
||||
const network = this.props.network
|
||||
|
||||
const tokenViews = tokens.map((tokenData) => {
|
||||
tokenData.network = network
|
||||
tokenData.userAddress = userAddress
|
||||
return h(TokenCell, tokenData)
|
||||
})
|
||||
|
||||
return (
|
||||
return h('div', [
|
||||
h('ol', {
|
||||
style: {
|
||||
height: '302px',
|
||||
height: '260px',
|
||||
overflowY: 'auto',
|
||||
display: 'flex',
|
||||
flexDirection: 'column',
|
||||
},
|
||||
}, [h('style', `
|
||||
}, [
|
||||
h('style', `
|
||||
|
||||
li.token-cell {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
align-items: center;
|
||||
padding: 10px;
|
||||
}
|
||||
li.token-cell {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
align-items: center;
|
||||
padding: 10px;
|
||||
}
|
||||
|
||||
li.token-cell > h3 {
|
||||
margin-left: 12px;
|
||||
}
|
||||
li.token-cell > h3 {
|
||||
margin-left: 12px;
|
||||
}
|
||||
|
||||
li.token-cell:hover {
|
||||
background: white;
|
||||
cursor: pointer;
|
||||
}
|
||||
li.token-cell:hover {
|
||||
background: white;
|
||||
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) {
|
||||
@ -101,7 +130,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 +164,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 +176,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
|
||||
}
|
||||
|
||||
|
@ -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',
|
||||
|
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