mirror of
https://github.com/kremalicious/metamask-extension.git
synced 2024-12-23 09:52:26 +01:00
Merge pull request #3982 from MetaMask/i3981-contract-rates
Fetch token prices based on contract address
This commit is contained in:
commit
e4eb69dcc2
@ -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') {
|
||||
|
77
app/scripts/controllers/token-rates.js
Normal file
77
app/scripts/controllers/token-rates.js
Normal file
@ -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
|
||||
}
|
||||
}
|
||||
|
29
test/unit/token-rates-controller.js
Normal file
29
test/unit/token-rates-controller.js
Normal file
@ -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…
Reference in New Issue
Block a user