From a238d313e251156b2e6f312790164044513014f7 Mon Sep 17 00:00:00 2001 From: StrathCole <7449529+StrathCole@users.noreply.github.com> Date: Tue, 17 Sep 2024 21:55:44 +0200 Subject: [PATCH] Add SDR calculation (#30) * - implement SDR price calculation from sdr basket settings * - added log output * - only calculate SDR if not provided from price server * - wrong type of variable * - fixed typo - add fixed price for USD * - type fixed * - move logic from feeder to price server * - move to adjustPrices * - fixed invalid structure of prices * - allow sdr basket being passed by env * - make SDR a correct fiat pair * - rework parsing of website * - remove logs * - bump version --- feeder/package-lock.json | 4 +- feeder/package.json | 2 +- price-server/config/default-sample.js | 8 +++ price-server/config/docker.js | 10 +++ price-server/package-lock.json | 4 +- price-server/package.json | 2 +- .../src/provider/fiat/FiatProvider.ts | 63 ++++++++++++++++++- price-server/src/provider/fiat/quoter/IMF.ts | 24 ++++--- 8 files changed, 102 insertions(+), 15 deletions(-) diff --git a/feeder/package-lock.json b/feeder/package-lock.json index 7a333aee..132b3ed3 100644 --- a/feeder/package-lock.json +++ b/feeder/package-lock.json @@ -1,12 +1,12 @@ { "name": "@classic-terra/oracle-feeder", - "version": "3.1.4", + "version": "3.1.5", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "@classic-terra/oracle-feeder", - "version": "3.1.4", + "version": "3.1.5", "license": "Apache-2.0", "dependencies": { "@terra-money/terra.js": "^3.1.7", diff --git a/feeder/package.json b/feeder/package.json index 9e46939c..07a8137a 100644 --- a/feeder/package.json +++ b/feeder/package.json @@ -1,6 +1,6 @@ { "name": "@classic-terra/oracle-feeder", - "version": "3.1.4", + "version": "3.1.5", "main": "src/index.ts", "license": "Apache-2.0", "scripts": { diff --git a/price-server/config/default-sample.js b/price-server/config/default-sample.js index a8f002f5..a55a2569 100644 --- a/price-server/config/default-sample.js +++ b/price-server/config/default-sample.js @@ -587,4 +587,12 @@ module.exports = { apiKey: '', // necessary }, }, + sdrBasket: { + // to calculate SDR value if not available from fiat providers + USD: '0.57813', + EUR: '0.37379', + JPY: '13.452', + CNY: '1.0993', + GBP: '0.080870', + }, } diff --git a/price-server/config/docker.js b/price-server/config/docker.js index f284a485..4d79786c 100644 --- a/price-server/config/docker.js +++ b/price-server/config/docker.js @@ -57,4 +57,14 @@ module.exports = { apiKey: process.env.FIAT_PROVIDER_ALPHA_VANTAGE_API_KEY || '', // necessary }, }, + sdrBasket: process.env.SDR_BASKET + ? JSON.parse(process.env.SDR_BASKET) + : { + // to calculate SDR value if not available from fiat providers + USD: '0.57813', + EUR: '0.37379', + JPY: '13.452', + CNY: '1.0993', + GBP: '0.080870', + }, } diff --git a/price-server/package-lock.json b/price-server/package-lock.json index 13d457a1..d754468b 100644 --- a/price-server/package-lock.json +++ b/price-server/package-lock.json @@ -1,12 +1,12 @@ { "name": "@classic-terra/price-server", - "version": "3.1.4", + "version": "3.1.5", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "@classic-terra/price-server", - "version": "3.1.4", + "version": "3.1.5", "license": "Apache-2.0", "dependencies": { "@opentelemetry/api": "^1.0.2", diff --git a/price-server/package.json b/price-server/package.json index 7053989e..26c7a6a1 100644 --- a/price-server/package.json +++ b/price-server/package.json @@ -1,6 +1,6 @@ { "name": "@classic-terra/price-server", - "version": "3.1.4", + "version": "3.1.5", "main": "src/main.ts", "license": "Apache-2.0", "scripts": { diff --git a/price-server/src/provider/fiat/FiatProvider.ts b/price-server/src/provider/fiat/FiatProvider.ts index 084878c0..b57c79f7 100644 --- a/price-server/src/provider/fiat/FiatProvider.ts +++ b/price-server/src/provider/fiat/FiatProvider.ts @@ -1,6 +1,9 @@ import * as config from 'config' -import { Provider, ProviderOptions } from 'provider/base' +import { PriceBySymbol, Provider, ProviderOptions } from 'provider/base' +import * as logger from 'lib/logger' import { CurrencyLayer, AlphaVantage, Fixer, ExchangeRate, Fer, Frankfurter, Fastforex, IMF } from './quoter' +import BigNumber from 'bignumber.js' +import { getBaseCurrency } from 'lib/currency' class FiatProvider extends Provider { constructor(options: ProviderOptions) { @@ -32,6 +35,54 @@ class FiatProvider extends Provider { await this.tick(Date.now()) } + protected calculateSDR(prices: PriceBySymbol): BigNumber | undefined { + if (!config.sdrBasket) { + logger.error(`calculateSDR: config.sdrBasket not found`) + return undefined + } + + const priceList = Object.keys(prices).map((symbol) => ({ + denom: getBaseCurrency(symbol), + price: prices[symbol].toFixed(8), + })) + + // check if all prices from the basket are available + for (const denom of Object.keys(config.sdrBasket)) { + if (denom === 'USD') { + continue + } + + if (!priceList.find((p) => p.denom === denom)) { + logger.error(`calculateSDR price for ${denom} not found`) + return undefined + } + } + + // calculate SDR price + let sdrPrice: BigNumber | undefined = undefined + + try { + sdrPrice = Object.entries(config.sdrBasket).reduce((acc, [denom, weight]: [string, string]) => { + const price = denom === 'USD' ? BigNumber(1) : priceList.find((p) => p.denom === denom)?.price || BigNumber(0) + if (!price) { + throw new Error(`price for ${denom} not found`) + } + return acc.plus(new BigNumber(price).times(weight)) + }, new BigNumber(0)) + } catch (err) { + logger.error(`getPrices: error calculating SDR price: ${err.message}`) + return undefined + } + + if (!sdrPrice) { + logger.error(`getPrices: error calculating SDR price`) + return undefined + } + + logger.info(`getPrices: calculated SDR price: ${sdrPrice.toString()}`) + return sdrPrice + } + protected adjustPrices(): void { for (const symbol of this.symbols) { delete this.priceBySymbol[symbol] @@ -46,6 +97,16 @@ class FiatProvider extends Provider { } } } + + if (!this.priceBySymbol['SDR/USD']) { + logger.info(`No SDR price found, falling back to calculation.`) + const sdrPrice = this.calculateSDR(this.priceBySymbol) + if (sdrPrice && sdrPrice.isNaN() === false) { + this.priceBySymbol['SDR/USD'] = sdrPrice + } else { + logger.error(`No SDR price found, calculation failed.`) + } + } } } diff --git a/price-server/src/provider/fiat/quoter/IMF.ts b/price-server/src/provider/fiat/quoter/IMF.ts index 9b49d0fe..898995f1 100644 --- a/price-server/src/provider/fiat/quoter/IMF.ts +++ b/price-server/src/provider/fiat/quoter/IMF.ts @@ -9,16 +9,24 @@ const SDR_VALUATION_URL = 'https://www.imf.org/external/np/fin/data/rms_sdrv.asp async function fetchQuote() { const text = await fetch(SDR_VALUATION_URL).then((res) => res.text()) - const doc = parse(text) - const tds = doc.querySelectorAll('.tightest td') - const idx = tds.findIndex((el) => el.structuredText === ' SDR1 = US$') - - if (idx === -1) { - throw new Error('cannot find SDR/USD element from HTML document') + let idx = -1 + const tables = text.match(/]*>([\s\S]*?)<\/table>/gi) || [] + for (const table of tables) { + const doc = parse(table.trim()) + const tds = doc.querySelectorAll('td') + + idx = tds.findIndex((el) => { + return el.structuredText.trim() === 'SDR1 = US$' + }) + + if (idx !== -1) { + // sample format: ' 1.32149 2' + return num(tds[idx + 1].structuredText.split(' ')[1]) + } } - // sample format: ' 1.32149 2' - return num(tds[idx + 1].structuredText.split(' ')[1]) + // nothing found + throw new Error('cannot find SDR/USD element from HTML document') } // fetchQuote().then(console.log).catch(console.error) // For test