diff --git a/lib/block-listeners/polkadot-node-subscription.js b/lib/block-listeners/polkadot-node-subscription.js index cae08cd8b5..1cdc2e73e8 100644 --- a/lib/block-listeners/polkadot-node-subscription.js +++ b/lib/block-listeners/polkadot-node-subscription.js @@ -52,7 +52,6 @@ class PolkadotNodeSubscription { const blockHeight = blockHeader.number.toNumber() if (this.height < blockHeight) { this.height = blockHeight - console.log(`\x1b[36mNew kusama block #${blockHeight}\x1b[0m`) this.newBlockHandler(blockHeight) } }) diff --git a/lib/reducers/emoneyV0-reducers.js b/lib/reducers/emoneyV0-reducers.js index f0bddc57a8..af3a24d816 100644 --- a/lib/reducers/emoneyV0-reducers.js +++ b/lib/reducers/emoneyV0-reducers.js @@ -1,106 +1,105 @@ const terraV3Reducers = require('./terraV3-reducers') const BigNumber = require('bignumber.js') -const fetch = require('node-fetch') const _ = require('lodash') -const EMoneyAPIUrl = `https://api.e-money.com/v1/` -const exchangeAPIURL = `https://api.exchangeratesapi.io/latest?` -const Sentry = require('@sentry/node') -async function totalBackedValueReducer(totalBackedValue) { - const exchangeRates = await fetchTokenExchangeRates() - const token = `e`.concat(totalBackedValue.denom.substring(1)) - const ticker = totalBackedValue.denom.substring(1).toUpperCase() - // First we calculate the fiat net value of the token's total supply in it counterpart - // fiat currency - const fiatValue = exchangeRates[token] - ? BigNumber(totalBackedValue.amount) - .div(1000000) - .times(exchangeRates[token][ticker]) - .toNumber() - : null - // Now that we have its net fiat value, we transform this value into euro - const { rates } = await fetchFiatExchangeRates(`EUR`, ticker) - const eurRate = ticker === `EUR` ? 1 : rates[ticker] - return (totalBackedValue = { - ...totalBackedValue, - amount: exchangeRates[token] - ? BigNumber(totalBackedValue.amount).times( - Object.values(exchangeRates[token])[0] - ) - : BigNumber(totalBackedValue.amount), - // The total net EUR value of the token's total supply equals its net value in its - // counterpart fiat currency times this fiat currency's EUR rate - eurValue: fiatValue * eurRate - }) -} +async function totalBackedValueReducer( + totalBackedValue, + exchangeRates, + reducers +) { + const aggregatingCurrency = `EUR` + const lunieCoin = reducers.coinReducer(totalBackedValue) -async function fetchTokenExchangeRates() { - return await fetch(`${EMoneyAPIUrl}rates.json`) - .then(r => r.json()) - .catch(err => { - Sentry.withScope(function(scope) { - scope.setExtra('fetch', `${EMoneyAPIUrl}rates.json`) - Sentry.captureException(err) - }) - }) + // The total net EUR value of the token's total supply + const fiatValue = BigNumber(lunieCoin.amount) + .times(exchangeRates[lunieCoin.denom][aggregatingCurrency]) + .toNumber() + return { + ...lunieCoin, + eurValue: fiatValue + } } -async function fetchFiatExchangeRates(selectedFiatCurrency, ticker) { - return await fetch( - `${exchangeAPIURL}base=${selectedFiatCurrency}&symbols=${ticker}` +async function getTotalNetworkAnnualRewards(inflations, totalBackedValues) { + // Now we need to multiply each total supply of backed tokens with its corresponding + // inflation + const totalBackedValueDictionary = _.keyBy( + totalBackedValues.map(totalBackedValue => ({ + ...totalBackedValue, + denom: totalBackedValue.denom.toLowerCase() // inflation response uses lower case denoms + })), + 'denom' ) - .then(r => r.json()) - .catch(err => { - Sentry.withScope(function(scope) { - scope.setExtra( - 'fetch', - `${exchangeAPIURL}base=${selectedFiatCurrency}&symbols=${ticker}` - ) - Sentry.captureException(err) - }) - }) + const rewardsSumInEur = inflations.reduce((sum, inflation) => { + return BigNumber(sum).plus( + BigNumber(inflation.inflation).times( + totalBackedValueDictionary[inflation.denom].eurValue // we use the eur value to be able to sum up the individual token values + ) + ) + }, 0) + + return rewardsSumInEur } async function expectedRewardsPerToken( validator, commission, - inflations, - totalBackedValues + totalNetworkAnnualRewards, + exchangeRates ) { + const aggregatingCurrency = 'EUR' + const stakingToken = 'NGM' + const percentOfAllEligibleStake = validator.votingPower const totalStakeToValidator = validator.tokens // used to answer the question "How many rewards per ONE token invested?" const percentOfAllRewardsPerToken = BigNumber(percentOfAllEligibleStake) .times(BigNumber(1).minus(BigNumber(commission))) .div(BigNumber(totalStakeToValidator)) - // Now we need to multiply each total supply of backed tokens with its corresponding - // inflation - const totalBackedValueDictionary = _.keyBy(totalBackedValues, 'denom') - const rewardsSumInEur = inflations.reduce((sum, inflation) => { - return BigNumber(sum).plus( - BigNumber(inflation.inflation).times( - totalBackedValueDictionary[inflation.denom].eurValue // we use the eur value to be able to sum up the individual token values - ) - ) - }, 0) - // now we calculate the total rewards in eur per token delegated to the validator - const totalEURGainsPerTokenInvested = BigNumber(rewardsSumInEur).times( - percentOfAllRewardsPerToken - ) + const totalEURGainsPerTokenInvested = BigNumber( + totalNetworkAnnualRewards + ).times(percentOfAllRewardsPerToken) // How many NGM tokens can we buy with the total gain in EUR we make in a year's time? // 0.50€ is the price the NGM tokens will be first sold. Therefore, this is the official value // until they reach an exchange - const pricePerNGM = 0.5 - const ngmGains = totalEURGainsPerTokenInvested / pricePerNGM + const pricePerNGM = exchangeRates[stakingToken][aggregatingCurrency] + const ngmGains = totalEURGainsPerTokenInvested.div(pricePerNGM) return ngmGains.toFixed(4) // we don't need more then a precision of 2 (0.1 = 10%) } +function calculateTokenExchangeRates( + supportedFiatCurrencies, + emoneyTokenExchangeRates, + fiatExchangeRates, + reducers +) { + return Object.entries(emoneyTokenExchangeRates).reduce( + (all, [denom, emoneyTokenToFiatExchangeRate]) => { + const [fiatCurrency, rate] = Object.entries( + emoneyTokenToFiatExchangeRate + )[0] // TODO dangerous if there will be more rates from the API directly + // precalculate the exchange rates for all denom currency pairs + supportedFiatCurrencies.forEach(supportedCurrency => { + all[reducers.denomLookup(denom)] = + all[reducers.denomLookup(denom)] || {} + all[reducers.denomLookup(denom)][supportedCurrency] = + supportedCurrency === fiatCurrency + ? rate + : rate * fiatExchangeRates[fiatCurrency][supportedCurrency] + }) + return all + }, + {} + ) +} + module.exports = { ...terraV3Reducers, expectedRewardsPerToken, - fetchTokenExchangeRates, - totalBackedValueReducer + totalBackedValueReducer, + getTotalNetworkAnnualRewards, + calculateTokenExchangeRates } diff --git a/lib/source/emoneyV0-source.js b/lib/source/emoneyV0-source.js index 601a98def9..c891df2bbe 100644 --- a/lib/source/emoneyV0-source.js +++ b/lib/source/emoneyV0-source.js @@ -1,9 +1,10 @@ const TerraV3API = require('./terraV3-source') const CosmosV0API = require('./cosmosV0-source') -const BigNumber = require('bignumber.js') const fetch = require('node-fetch') -const apiURL = `https://api.exchangeratesapi.io/latest?` +const Sentry = require('@sentry/node') +const fiatExchangeRateApi = `https://api.exchangeratesapi.io/latest?` +const EMoneyAPIUrl = `https://beta-api.e-money.com/v1/` const gasPrices = [ { denom: 'echf', @@ -34,6 +35,51 @@ class EMoneyV0API extends TerraV3API { this.gasPrices = gasPrices } + // additional block handling by network + async newBlockHandler(block, store) { + try { + // update exchange rates every 5 minutes + if ( + !store.exchangeRatesUpdating && + (!store.exchangeRatesUpdate || + Date.now() - new Date(store.exchangeRatesUpdate).getTime() > + 5 * 60 * 1000) + ) { + console.log('Updating Emoney token exchange rates') + store.exchangeRatesUpdating = true + + const [fiatExchangeRates, tokenExchangeRates] = await Promise.all([ + this.fetchFiatExchangeRates(), + this.fetchEmoneyTokenExchangeRates() + ]) + this.store.exchangeRates = this.reducers.calculateTokenExchangeRates( + supportedFiatCurrencies, + tokenExchangeRates, + fiatExchangeRates, + this.reducers + ) + + await Promise.all([ + this.getTokensInflations(), + this.getTotalBackedValues() + ]).then(async ([inflatations, totalBackedValues]) => { + store.totalNetworkAnnualRewards = await this.reducers.getTotalNetworkAnnualRewards( + inflatations, + totalBackedValues + ) + }) + + // eslint-disable-next-line require-atomic-updates + store.exchangeRatesUpdating = false + // eslint-disable-next-line require-atomic-updates + store.exchangeRatesUpdate = Date.now() + } + } catch (error) { + console.error('EMoney block handler failed', error) + Sentry.captureException(error) + } + } + // Here we query for the current inflation rates for all the backed tokens async getTokensInflations() { const inflations = await this.get(`inflation/current`) @@ -50,11 +96,16 @@ class EMoneyV0API extends TerraV3API { const mapTotalBackedValues = async () => { return Promise.all( totalBackedValues.map(totalBackedValue => - this.reducers.totalBackedValueReducer(totalBackedValue) + this.reducers.totalBackedValueReducer( + totalBackedValue, + this.store.exchangeRates, + this.reducers + ) ) ) } - return await mapTotalBackedValues() + const resolvedBackedValues = await mapTotalBackedValues() + return resolvedBackedValues } getAnnualProvision() { @@ -66,14 +117,12 @@ class EMoneyV0API extends TerraV3API { } async getExpectedReturns(validator) { - const inflations = await this.getTokensInflations() - const totalBackedValues = await this.getTotalBackedValues() // Right now we are displaying only the EUR value of expected returns const expectedReturns = this.reducers.expectedRewardsPerToken( validator, validator.commission, - inflations, - totalBackedValues + this.store.totalNetworkAnnualRewards, + this.store.exchangeRates ) return expectedReturns } @@ -96,87 +145,51 @@ class EMoneyV0API extends TerraV3API { ) } - // When e-Money goes live they will count with a trading platform where the value - // for the different backed tokens will be changing slightly. - // They will provide with an API for us to query these values. - // For now we will assume a 1:1 ratio and treat each token like it were the real - // fiat currency it represents. - const denom = balance.denom.substring(1).toUpperCase() - - // To handle the NGM balance, first we convert to EUR value and then to the selected - // fiat currency value - if (denom === 'NGM') { - const eurValue = this.reducers.coinReducer(balance).amount * 0.5 // 0.50€ is the price the NGM tokens will be first sold. Therefore, the official value until they reach an exchange - if (selectedFiatCurrency === `EUR`) { - return { - amount: parseFloat(eurValue).toFixed(2), - denom: `EUR`, - symbol: `€` - } - } else { - const { rates } = await this.fetchExchangeRates( - selectedFiatCurrency, - `EUR` - ) - const fiatValue = eurValue / rates[`EUR`] - const currencySign = this.getCurrencySign(selectedFiatCurrency) - return { - amount: parseFloat(fiatValue).toFixed(2), - denom: selectedFiatCurrency, - symbol: currencySign - } - } - // For all other balances we use the public API https://exchangeratesapi.io/ to add all balances into - // a single fiat currency value, the selectedFiatCurrency - } else { - const { rates } = await this.fetchExchangeRates( - selectedFiatCurrency, - denom - ) - let totalAsFiat = 0 - // Here we check if the balance we are currently calculating the total fiat value from is - // the same currency we want the fiat value to be displayed in. - // In that case, we simply add it, don't need to convert it. - if (denom !== selectedFiatCurrency) { - totalAsFiat = this.reducers.coinReducer(balance).amount / rates[denom] - } else { - totalAsFiat = this.reducers.coinReducer(balance).amount - } - // Now we do total value in the selected currency times the token fiat value - const totalFiatValue = await this.convertFiatValueToTokenFiatValue( - totalAsFiat, - denom - ) - // Finally we get the proper currency sign to display - const currencySign = this.getCurrencySign(selectedFiatCurrency) - return { - amount: parseFloat(totalFiatValue).toFixed(2), - denom: selectedFiatCurrency, - symbol: currencySign - } + // e-Money acts like a trading platform where the value for the different backed tokens will be changing slightly. + // They are providing us with an API for us to query the exchange rate from their token to the value of that token + // For aggregating all the values of the different tokens we normalize to one currency using the public API https://exchangeratesapi.io/ + const lunieCoin = this.reducers.coinReducer(balance) + const exchangeRate = this.store.exchangeRates[lunieCoin.denom][ + selectedFiatCurrency + ] + const totalAsFiat = lunieCoin.amount * exchangeRate + + // Finally we get the proper currency sign to display + const currencySign = this.getCurrencySign(selectedFiatCurrency) + return { + amount: parseFloat(totalAsFiat).toFixed(2), + denom: selectedFiatCurrency, + symbol: currencySign } } - async fetchExchangeRates(selectedFiatCurrency, ticker) { - return await fetch( - `${apiURL}base=${selectedFiatCurrency}&symbols=${ticker}` - ) + async fetchEmoneyTokenExchangeRates() { + const rates = await fetch(`${EMoneyAPIUrl}rates.json`) .then(r => r.json()) - .catch(error => console.error(error)) + .catch(err => { + Sentry.withScope(function(scope) { + scope.setExtra('fetch', `${EMoneyAPIUrl}rates.json`) + Sentry.captureException(err) + }) + }) + rates['NGM'] = { EUR: 0.5 } + return rates } - async convertFiatValueToTokenFiatValue(totalAsFiat, denom) { - const tokenExchangeRates = await this.reducers.fetchTokenExchangeRates() - const tokenExchangeRatesArray = Object.values(tokenExchangeRates) - const filterDenomFromExchangeRates = tokenExchangeRatesArray.filter( - rate => Object.keys(rate)[0] === denom - )[0] - const selectedFiatCurrencyExchangeRate = Object.values( - filterDenomFromExchangeRates - )[0] - return BigNumber(totalAsFiat) - .times(selectedFiatCurrencyExchangeRate) - .toNumber() + async fetchFiatExchangeRates() { + let all = {} + await Promise.all( + Array.from(supportedFiatCurrencies).map(async fiatCurrency => { + const { rates } = await fetch( + `${fiatExchangeRateApi}base=${fiatCurrency}` + ) + .then(r => r.json()) + .catch(error => console.error(error)) + all[fiatCurrency] = rates + }) + ) + + return all } getCurrencySign(currency) { @@ -195,6 +208,14 @@ class EMoneyV0API extends TerraV3API { return `?` } } + + async getBalancesFromAddress(address, fiatCurrency) { + return TerraV3API.prototype.getBalancesFromAddress.call( + this, + address, + fiatCurrency + ) + } } module.exports = EMoneyV0API