Maintain token prices using a background service

feature/default_network_editable
bitpshr 7 years ago
parent a350e80fee
commit d0447f9058
  1. 76
      app/scripts/controllers/token-rates.js
  2. 7
      app/scripts/metamask-controller.js
  3. 28
      test/unit/token-rates-controller.js
  4. 58
      ui/app/actions.js
  5. 1
      ui/app/components/send/send-v2-container.js
  6. 17
      ui/app/reducers/metamask.js
  7. 11
      ui/app/send-v2.js

@ -0,0 +1,76 @@
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 () {
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')
@ -104,6 +105,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,
@ -201,6 +207,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,

@ -0,0 +1,28 @@
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.fetchExchangeRate = address => address
controller.tokens = [{ address: 'foo' }, { address: 'bar' }]
await controller.updateExchangeRates()
assert.deepEqual(controller.store.getState().contractExchangeRates, { foo: 'foo', bar: 'bar' })
})
})

@ -220,10 +220,6 @@ var actions = {
coinBaseSubview: coinBaseSubview,
SHAPESHIFT_SUBVIEW: 'SHAPESHIFT_SUBVIEW',
shapeShiftSubview: shapeShiftSubview,
UPDATE_CONTRACT_EXCHANGE_RATES: 'UPDATE_CONTRACT_EXCHANGE_RATES',
UPDATE_CONTRACT_EXCHANGE_RATE: 'UPDATE_CONTRACT_EXCHANGE_RATE',
updateContractExchangeRates,
updateContractExchangeRate,
PAIR_UPDATE: 'PAIR_UPDATE',
pairUpdate: pairUpdate,
coinShiftRquest: coinShiftRquest,
@ -1082,12 +1078,9 @@ function unlockMetamask (account) {
}
function updateMetamaskState (newState) {
return async dispatch => {
await dispatch({
type: actions.UPDATE_METAMASK_STATE,
value: newState,
})
dispatch(updateContractExchangeRates())
return {
type: actions.UPDATE_METAMASK_STATE,
value: newState,
}
}
@ -1300,12 +1293,9 @@ function addTokens (tokens) {
}
function updateTokens (newTokens) {
return async dispatch => {
await dispatch({
type: actions.UPDATE_TOKENS,
newTokens,
})
dispatch(updateContractExchangeRates())
return {
type: actions.UPDATE_TOKENS,
newTokens,
}
}
@ -1759,42 +1749,6 @@ function shapeShiftRequest (query, options, cb) {
}
}
async function fetchContractRate (address) {
try {
const response = await fetch(`https://exchanges.balanc3.net/prices?from=${address}&to=ETH&autoConversion=false&summaryOnly=true`)
const json = await response.json()
const rate = json && json.length ? json[0].averagePrice : 0
return { address, rate }
} catch (error) { }
}
function updateContractExchangeRates () {
return async (dispatch, getState) => {
const { metamask: { tokens = [] } } = getState()
const newExchangeRates = {}
for (const i in tokens) {
const address = tokens[i].address
newExchangeRates[address] = (await fetchContractRate(address)).rate
}
dispatch({
type: actions.UPDATE_CONTRACT_EXCHANGE_RATES,
payload: { newExchangeRates },
})
}
}
function updateContractExchangeRate (address) {
return async dispatch => {
const { address, rate } = await fetchContractRate(address)
dispatch({
type: actions.UPDATE_CONTRACT_EXCHANGE_RATE,
payload: { address, rate },
})
}
}
function setFeatureFlag (feature, activated, notificationType) {
return (dispatch) => {
dispatch(actions.showLoadingIndication())

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

@ -177,23 +177,6 @@ function reduceMetamask (state, action) {
conversionDate: action.value.conversionDate,
})
case actions.UPDATE_CONTRACT_EXCHANGE_RATES:
const { payload: { newExchangeRates } } = action
return {
...metamaskState,
contractExchangeRates: newExchangeRates,
}
case actions.UPDATE_CONTRACT_EXCHANGE_RATE:
const { payload: { address, rate } } = action
return {
...metamaskState,
contractExchangeRates: {
...metamaskState.contractExchangeRates,
[address]: rate,
},
}
case actions.UPDATE_TOKENS:
return extend(metamaskState, {
tokens: action.newTokens,

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

Loading…
Cancel
Save