Merge pull request #3982 from MetaMask/i3981-contract-rates

Fetch token prices based on contract address
feature/default_network_editable
Dan Finlay 7 years ago committed by GitHub
commit e4eb69dcc2
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
  1. 3
      CHANGELOG.md
  2. 3
      app/scripts/background.js
  3. 77
      app/scripts/controllers/token-rates.js
  4. 16
      app/scripts/metamask-controller.js
  5. 29
      test/unit/token-rates-controller.js
  6. 24
      ui/app/actions.js
  7. 8
      ui/app/components/pending-tx/confirm-send-token.js
  8. 1
      ui/app/components/send/send-v2-container.js
  9. 20
      ui/app/components/token-cell.js
  10. 18
      ui/app/components/tx-list-item.js
  11. 10
      ui/app/reducers/metamask.js
  12. 19
      ui/app/selectors.js
  13. 11
      ui/app/send-v2.js

@ -2,7 +2,8 @@
## Current Master
- Improved performance of 3D fox logo.
- Improved performance of 3D fox logo
- Fetch token prices based on contract address, not symbol
- Fix bug that prevents setting language locale in settings.
## 4.5.5 Fri Apr 06 2018

@ -199,6 +199,7 @@ function setupController (initState, initLangCode) {
if (isMetaMaskInternalProcess) {
// communication with popup
popupIsOpen = popupIsOpen || (remotePort.name === 'popup')
controller.isClientOpen = true
controller.setupTrustedCommunication(portStream, 'MetaMask')
// record popup as closed
if (remotePort.sender.url.match(/home.html$/)) {
@ -210,6 +211,8 @@ function setupController (initState, initLangCode) {
if (remotePort.sender.url.match(/home.html$/)) {
openMetamaskTabsIDs[remotePort.sender.tab.id] = false
}
controller.isClientOpen = popupIsOpen ||
Object.keys(openMetamaskTabsIDs).some(key => openMetamaskTabsIDs[key])
})
}
if (remotePort.name === 'notification') {

@ -0,0 +1,77 @@
const ObservableStore = require('obs-store')
// By default, poll every 3 minutes
const DEFAULT_INTERVAL = 180 * 1000
/**
* A controller that polls for token exchange
* rates based on a user's current token list
*/
class TokenRatesController {
/**
* Creates a TokenRatesController
*
* @param {Object} [config] - Options to configure controller
*/
constructor ({ interval = DEFAULT_INTERVAL, preferences } = {}) {
this.store = new ObservableStore()
this.preferences = preferences
this.interval = interval
}
/**
* Updates exchange rates for all tokens
*/
async updateExchangeRates () {
if (!this.isActive) { return }
const contractExchangeRates = {}
for (const i in this._tokens) {
const address = this._tokens[i].address
contractExchangeRates[address] = await this.fetchExchangeRate(address)
}
this.store.putState({ contractExchangeRates })
}
/**
* Fetches a token exchange rate by address
*
* @param {String} address - Token contract address
*/
async fetchExchangeRate (address) {
try {
const response = await fetch(`https://exchanges.balanc3.net/prices?from=${address}&to=ETH&autoConversion=false&summaryOnly=true`)
const json = await response.json()
return json && json.length ? json[0].averagePrice : 0
} catch (error) { }
}
/**
* @type {Number} - Interval used to poll for exchange rates
*/
set interval (interval) {
this._handle && clearInterval(this._handle)
if (!interval) { return }
this._handle = setInterval(() => { this.updateExchangeRates() }, interval)
}
/**
* @type {Object} - Preferences controller instance
*/
set preferences (preferences) {
this._preferences && this._preferences.unsubscribe()
if (!preferences) { return }
this._preferences = preferences
this.tokens = preferences.getState().tokens
preferences.subscribe(({ tokens = [] }) => { this.tokens = tokens })
}
/**
* @type {Array} - Array of token objects with contract addresses
*/
set tokens (tokens) {
this._tokens = tokens
this.updateExchangeRates()
}
}
module.exports = TokenRatesController

@ -34,6 +34,7 @@ const PersonalMessageManager = require('./lib/personal-message-manager')
const TypedMessageManager = require('./lib/typed-message-manager')
const TransactionController = require('./controllers/transactions')
const BalancesController = require('./controllers/computed-balances')
const TokenRatesController = require('./controllers/token-rates')
const ConfigManager = require('./lib/config-manager')
const nodeify = require('./lib/nodeify')
const accountImporter = require('./account-import-strategies')
@ -105,6 +106,11 @@ module.exports = class MetamaskController extends EventEmitter {
this.provider = this.initializeProvider()
this.blockTracker = this.provider._blockTracker
// token exchange rate tracker
this.tokenRatesController = new TokenRatesController({
preferences: this.preferencesController.store,
})
this.recentBlocksController = new RecentBlocksController({
blockTracker: this.blockTracker,
provider: this.provider,
@ -202,6 +208,7 @@ module.exports = class MetamaskController extends EventEmitter {
AccountTracker: this.accountTracker.store,
TxController: this.txController.memStore,
BalancesController: this.balancesController.store,
TokenRatesController: this.tokenRatesController.store,
MessageManager: this.messageManager.memStore,
PersonalMessageManager: this.personalMessageManager.memStore,
TypesMessageManager: this.typedMessageManager.memStore,
@ -263,6 +270,7 @@ module.exports = class MetamaskController extends EventEmitter {
// memStore -> transform -> publicConfigStore
this.on('update', (memState) => {
this.isClientOpenAndUnlocked = memState.isUnlocked && this._isClientOpen
const publicState = selectPublicState(memState)
publicConfigStore.putState(publicState)
})
@ -1024,4 +1032,12 @@ module.exports = class MetamaskController extends EventEmitter {
}
}
set isClientOpen (open) {
this._isClientOpen = open
this.isClientOpenAndUnlocked = this.getState().isUnlocked && open
}
set isClientOpenAndUnlocked (active) {
this.tokenRatesController.isActive = active
}
}

@ -0,0 +1,29 @@
const assert = require('assert')
const sinon = require('sinon')
const TokenRatesController = require('../../app/scripts/controllers/token-rates')
const ObservableStore = require('obs-store')
describe('TokenRatesController', () => {
it('should listen for preferences store updates', () => {
const preferences = new ObservableStore({ tokens: [] })
const controller = new TokenRatesController({ preferences })
preferences.putState({ tokens: ['foo'] })
assert.deepEqual(controller._tokens, ['foo'])
})
it('should poll on correct interval', async () => {
const stub = sinon.stub(global, 'setInterval')
new TokenRatesController({ interval: 1337 }) // eslint-disable-line no-new
assert.strictEqual(stub.getCall(0).args[1], 1337)
stub.restore()
})
it('should fetch each token rate based on address', async () => {
const controller = new TokenRatesController()
controller.isActive = true
controller.fetchExchangeRate = address => address
controller.tokens = [{ address: 'foo' }, { address: 'bar' }]
await controller.updateExchangeRates()
assert.deepEqual(controller.store.getState().contractExchangeRates, { foo: 'foo', bar: 'bar' })
})
})

@ -221,8 +221,6 @@ var actions = {
coinBaseSubview: coinBaseSubview,
SHAPESHIFT_SUBVIEW: 'SHAPESHIFT_SUBVIEW',
shapeShiftSubview: shapeShiftSubview,
UPDATE_TOKEN_EXCHANGE_RATE: 'UPDATE_TOKEN_EXCHANGE_RATE',
updateTokenExchangeRate,
PAIR_UPDATE: 'PAIR_UPDATE',
pairUpdate: pairUpdate,
coinShiftRquest: coinShiftRquest,
@ -1752,28 +1750,6 @@ function shapeShiftRequest (query, options, cb) {
}
}
function updateTokenExchangeRate (token = '') {
const pair = `${token.toLowerCase()}_eth`
return dispatch => {
if (!token) {
return
}
shapeShiftRequest('marketinfo', { pair }, marketinfo => {
if (!marketinfo.error) {
dispatch({
type: actions.UPDATE_TOKEN_EXCHANGE_RATE,
payload: {
pair,
marketinfo,
},
})
}
})
}
}
function setFeatureFlag (feature, activated, notificationType) {
return (dispatch) => {
dispatch(actions.showLoadingIndication())

@ -48,7 +48,7 @@ module.exports = compose(
function mapStateToProps (state, ownProps) {
const { token: { symbol }, txData } = ownProps
const { token: { address }, txData } = ownProps
const { txParams } = txData || {}
const tokenData = txParams.data && abiDecoder.decodeMethod(txParams.data)
@ -59,7 +59,7 @@ function mapStateToProps (state, ownProps) {
} = state.metamask
const accounts = state.metamask.accounts
const selectedAddress = getSelectedAddress(state)
const tokenExchangeRate = getTokenExchangeRate(state, symbol)
const tokenExchangeRate = getTokenExchangeRate(state, address)
const { balance } = accounts[selectedAddress]
return {
conversionRate,
@ -75,12 +75,9 @@ function mapStateToProps (state, ownProps) {
}
function mapDispatchToProps (dispatch, ownProps) {
const { token: { symbol } } = ownProps
return {
backToAccountDetail: address => dispatch(actions.backToAccountDetail(address)),
cancelTransaction: ({ id }) => dispatch(actions.cancelTx({ id })),
updateTokenExchangeRate: () => dispatch(actions.updateTokenExchangeRate(symbol)),
editTransaction: txMeta => {
const { token: { address } } = ownProps
const { txParams = {}, id } = txMeta
@ -203,7 +200,6 @@ ConfirmSendToken.prototype.componentWillMount = function () {
.balanceOf(selectedAddress)
.then(usersToken => {
})
this.props.updateTokenExchangeRate()
this.updateComponentSendErrors({})
}

@ -66,7 +66,6 @@ function mapDispatchToProps (dispatch) {
showCustomizeGasModal: () => dispatch(actions.showModal({ name: 'CUSTOMIZE_GAS' })),
estimateGas: params => dispatch(actions.estimateGas(params)),
getGasPrice: () => dispatch(actions.getGasPrice()),
updateTokenExchangeRate: token => dispatch(actions.updateTokenExchangeRate(token)),
signTokenTx: (tokenAddress, toAddress, amount, txData) => (
dispatch(actions.signTokenTx(tokenAddress, toAddress, amount, txData))
),

@ -16,7 +16,7 @@ function mapStateToProps (state) {
currentCurrency: state.metamask.currentCurrency,
selectedTokenAddress: state.metamask.selectedTokenAddress,
userAddress: selectors.getSelectedAddress(state),
tokenExchangeRates: state.metamask.tokenExchangeRates,
contractExchangeRates: state.metamask.contractExchangeRates,
conversionRate: state.metamask.conversionRate,
sidebarOpen: state.appState.sidebarOpen,
}
@ -25,7 +25,6 @@ function mapStateToProps (state) {
function mapDispatchToProps (dispatch) {
return {
setSelectedToken: address => dispatch(actions.setSelectedToken(address)),
updateTokenExchangeRate: token => dispatch(actions.updateTokenExchangeRate(token)),
hideSidebar: () => dispatch(actions.hideSidebar()),
}
}
@ -41,15 +40,6 @@ function TokenCell () {
}
}
TokenCell.prototype.componentWillMount = function () {
const {
updateTokenExchangeRate,
symbol,
} = this.props
updateTokenExchangeRate(symbol)
}
TokenCell.prototype.render = function () {
const { tokenMenuOpen } = this.state
const props = this.props
@ -60,7 +50,7 @@ TokenCell.prototype.render = function () {
network,
setSelectedToken,
selectedTokenAddress,
tokenExchangeRates,
contractExchangeRates,
conversionRate,
hideSidebar,
sidebarOpen,
@ -68,15 +58,13 @@ TokenCell.prototype.render = function () {
// userAddress,
} = props
const pair = `${symbol.toLowerCase()}_eth`
let currentTokenToFiatRate
let currentTokenInFiat
let formattedFiat = ''
if (tokenExchangeRates[pair]) {
if (contractExchangeRates[address]) {
currentTokenToFiatRate = multiplyCurrencies(
tokenExchangeRates[pair].rate,
contractExchangeRates[address],
conversionRate
)
currentTokenInFiat = conversionUtil(string, {

@ -27,7 +27,7 @@ function mapStateToProps (state) {
return {
tokens: state.metamask.tokens,
currentCurrency: getCurrentCurrency(state),
tokenExchangeRates: state.metamask.tokenExchangeRates,
contractExchangeRates: state.metamask.contractExchangeRates,
selectedAddressTxList: state.metamask.selectedAddressTxList,
}
}
@ -142,31 +142,29 @@ TxListItem.prototype.getTokenInfo = async function () {
({ decimals, symbol } = await tokenInfoGetter(toAddress))
}
return { decimals, symbol }
return { decimals, symbol, address: toAddress }
}
TxListItem.prototype.getSendTokenTotal = async function () {
const {
txParams = {},
conversionRate,
tokenExchangeRates,
contractExchangeRates,
currentCurrency,
} = this.props
const decodedData = txParams.data && abiDecoder.decodeMethod(txParams.data)
const { params = [] } = decodedData || {}
const { value } = params[1] || {}
const { decimals, symbol } = await this.getTokenInfo()
const { decimals, symbol, address } = await this.getTokenInfo()
const total = calcTokenAmount(value, decimals)
const pair = symbol && `${symbol.toLowerCase()}_eth`
let tokenToFiatRate
let totalInFiat
if (tokenExchangeRates[pair]) {
if (contractExchangeRates[address]) {
tokenToFiatRate = multiplyCurrencies(
tokenExchangeRates[pair].rate,
contractExchangeRates[address],
conversionRate
)
@ -220,7 +218,6 @@ TxListItem.prototype.resubmit = function () {
TxListItem.prototype.render = function () {
const {
transactionStatus,
transactionAmount,
onClick,
transactionId,
dateString,
@ -229,7 +226,6 @@ TxListItem.prototype.render = function () {
txParams,
} = this.props
const { total, fiatTotal, isTokenTx } = this.state
const showFiatTotal = transactionAmount !== '0x0' && fiatTotal
return h(`div${className || ''}`, {
key: transactionId,
@ -288,7 +284,7 @@ TxListItem.prototype.render = function () {
h('span.tx-list-value', total),
showFiatTotal && h('span.tx-list-fiat-value', fiatTotal),
fiatTotal && h('span.tx-list-fiat-value', fiatTotal),
]),
]),

@ -24,6 +24,7 @@ function reduceMetamask (state, action) {
frequentRpcList: [],
addressBook: [],
selectedTokenAddress: null,
contractExchangeRates: {},
tokenExchangeRates: {},
tokens: [],
send: {
@ -176,15 +177,6 @@ function reduceMetamask (state, action) {
conversionDate: action.value.conversionDate,
})
case actions.UPDATE_TOKEN_EXCHANGE_RATE:
const { payload: { pair, marketinfo } } = action
return extend(metamaskState, {
tokenExchangeRates: {
...metamaskState.tokenExchangeRates,
[pair]: marketinfo,
},
})
case actions.UPDATE_TOKENS:
return extend(metamaskState, {
tokens: action.newTokens,

@ -62,22 +62,15 @@ function getSelectedToken (state) {
}
function getSelectedTokenExchangeRate (state) {
const tokenExchangeRates = state.metamask.tokenExchangeRates
const contractExchangeRates = state.metamask.contractExchangeRates
const selectedToken = getSelectedToken(state) || {}
const { symbol = '' } = selectedToken
const pair = `${symbol.toLowerCase()}_eth`
const { rate: tokenExchangeRate = 0 } = tokenExchangeRates[pair] || {}
return tokenExchangeRate
const { address } = selectedToken
return contractExchangeRates[address] || 0
}
function getTokenExchangeRate (state, tokenSymbol) {
const pair = `${tokenSymbol.toLowerCase()}_eth`
const tokenExchangeRates = state.metamask.tokenExchangeRates
const { rate: tokenExchangeRate = 0 } = tokenExchangeRates[pair] || {}
return tokenExchangeRate
function getTokenExchangeRate (state, address) {
const contractExchangeRates = state.metamask.contractExchangeRates
return contractExchangeRates[address] || 0
}
function conversionRateSelector (state) {

@ -88,17 +88,6 @@ SendTransactionScreen.prototype.updateSendTokenBalance = function (usersToken) {
}
SendTransactionScreen.prototype.componentWillMount = function () {
const {
updateTokenExchangeRate,
selectedToken = {},
} = this.props
const { symbol } = selectedToken || {}
if (symbol) {
updateTokenExchangeRate(symbol)
}
this.updateGas()
}

Loading…
Cancel
Save