diff --git a/CHANGELOG.md b/CHANGELOG.md index 37f334d23..567479862 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -3,6 +3,7 @@ ## Current Master - 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. ## 3.7.8 2017-6-12 diff --git a/app/scripts/controllers/preferences.js b/app/scripts/controllers/preferences.js index aa8e05fcc..e45224593 100644 --- a/app/scripts/controllers/preferences.js +++ b/app/scripts/controllers/preferences.js @@ -8,13 +8,11 @@ class PreferencesController { const initState = extend({ frequentRpcList: [], currentAccountTab: 'history', + tokens: [], }, opts.initState) this.store = new ObservableStore(initState) } - - // - // PUBLIC METHODS - // +// PUBLIC METHODS setSelectedAddress (_address) { return new Promise((resolve, reject) => { @@ -28,6 +26,29 @@ class PreferencesController { return this.store.getState().selectedAddress } + addToken (rawAddress, symbol, decimals) { + const address = normalizeAddress(rawAddress) + const newEntry = { address, symbol, decimals } + + const tokens = this.store.getState().tokens + const previousIndex = tokens.find((token, index) => { + return token.address === address + }) + + if (previousIndex) { + tokens[previousIndex] = newEntry + } else { + tokens.push(newEntry) + } + + this.store.updateState({ tokens }) + return Promise.resolve() + } + + getTokens () { + return this.store.getState().tokens + } + updateFrequentRpcList (_url) { return this.addToFrequentRpcList(_url) .then((rpcList) => { diff --git a/app/scripts/metamask-controller.js b/app/scripts/metamask-controller.js index 410693df4..e4267381d 100644 --- a/app/scripts/metamask-controller.js +++ b/app/scripts/metamask-controller.js @@ -275,6 +275,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), diff --git a/ui/app/account-detail.js b/ui/app/account-detail.js index 19f2cba59..bed05a7fb 100644 --- a/ui/app/account-detail.js +++ b/ui/app/account-detail.js @@ -35,6 +35,7 @@ function mapStateToProps (state) { conversionRate: state.metamask.conversionRate, currentCurrency: state.metamask.currentCurrency, currentAccountTab: state.metamask.currentAccountTab, + tokens: state.metamask.tokens, } } @@ -273,13 +274,14 @@ 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, + tokens, addToken: () => this.props.dispatch(actions.showAddTokenPage()), }) default: diff --git a/ui/app/actions.js b/ui/app/actions.js index d17d4610e..6ff28f32f 100644 --- a/ui/app/actions.js +++ b/ui/app/actions.js @@ -124,6 +124,7 @@ var actions = { showConfigPage, SHOW_ADD_TOKEN_PAGE: 'SHOW_ADD_TOKEN_PAGE', showAddTokenPage, + addToken, setRpcTarget: setRpcTarget, setDefaultRpcTarget: setDefaultRpcTarget, setProviderType: setProviderType, @@ -636,6 +637,19 @@ function showAddTokenPage (transitionForward = true) { } } +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)) + } + dispatch(actions.goHome()) + }) + } +} + function goBackToInitView () { return { type: actions.BACK_TO_INIT_MENU, diff --git a/ui/app/add-token.js b/ui/app/add-token.js index fdbb5fe53..025cfacb5 100644 --- a/ui/app/add-token.js +++ b/ui/app/add-token.js @@ -30,10 +30,8 @@ function AddTokenScreen () { } AddTokenScreen.prototype.render = function () { - const props = this.props - const state = this.state - const { warning, address, symbol, decimals } = state + const { warning, symbol, decimals } = state return ( h('.flex-column.flex-grow', [ @@ -138,12 +136,12 @@ AddTokenScreen.prototype.render = function () { style: { alignSelf: 'center', }, - onClick (event) { + onClick: (event) => { const valid = this.validateInputs() if (!valid) return - const { address, symbol, decimals } = state - this.props.dispatch(addToken(address.trim(), symbol.trim(), decimals)) + const { address, symbol, decimals } = this.state + this.props.dispatch(actions.addToken(address.trim(), symbol.trim(), decimals)) }, }, 'Add'), ]), diff --git a/ui/app/components/token-list.js b/ui/app/components/token-list.js index d230ce74a..100e596ed 100644 --- a/ui/app/components/token-list.js +++ b/ui/app/components/token-list.js @@ -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,22 +19,23 @@ module.exports = TokenList inherits(TokenList, Component) function TokenList () { - this.state = { tokens, isLoading: true, network: null } + this.state = { + tokens: null, + isLoading: true, + network: null, + } Component.call(this) } TokenList.prototype.render = function () { const state = this.state - const { tokens, isLoading } = state - - const { userAddress } = this.props + const { isLoading, tokens } = state + const { userAddress, network } = this.props if (isLoading) { return this.message('Loading') } - const network = this.props.network - const tokenViews = tokens.map((tokenData) => { tokenData.network = network tokenData.userAddress = userAddress @@ -120,7 +122,7 @@ TokenList.prototype.createFreshTokenTracker = function () { this.tracker = new TokenTracker({ userAddress, provider: global.ethereumProvider, - tokens: tokens, + tokens: uniqueMergeTokens(defaultTokens, this.props.tokens), pollingInterval: 8000, }) @@ -149,7 +151,12 @@ TokenList.prototype.componentWillUpdate = function (nextProps) { } TokenList.prototype.updateBalances = function (tokenData) { - const heldTokens = tokenData.filter(token => token.balance !== '0' && token.string !== '0.000') + const desired = this.props.tokens.map(token => token.address) + const heldTokens = tokenData.filter(token => { + const held = token.balance !== '0' && token.string !== '0.000' + const preferred = desired.includes(normalizeAddress(token.address)) + return held || preferred + }) this.setState({ tokens: heldTokens, isLoading: false }) } @@ -158,3 +165,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 +} +