From 5a43f8323d564aa80294b13b52e526646d814d29 Mon Sep 17 00:00:00 2001 From: Judith Lao Date: Mon, 23 May 2022 11:50:07 -0400 Subject: [PATCH] feat: add dataloader to ref-finance call, chore: refactor and update usn actions --- .../src/redux/slices/tokenFiatValues/index.js | 112 ++++------------- .../frontend/src/utils/fiatValueManager.js | 117 ++++++++++++++++++ packages/frontend/src/utils/ref-finance.js | 27 ---- yarn.lock | 24 +++- 4 files changed, 160 insertions(+), 120 deletions(-) create mode 100644 packages/frontend/src/utils/fiatValueManager.js delete mode 100644 packages/frontend/src/utils/ref-finance.js diff --git a/packages/frontend/src/redux/slices/tokenFiatValues/index.js b/packages/frontend/src/redux/slices/tokenFiatValues/index.js index 519236208c..4170e69f12 100644 --- a/packages/frontend/src/redux/slices/tokenFiatValues/index.js +++ b/packages/frontend/src/redux/slices/tokenFiatValues/index.js @@ -1,102 +1,28 @@ import { createSlice, createAsyncThunk } from '@reduxjs/toolkit'; -import DataLoader from 'dataloader'; import merge from 'lodash.merge'; -import Cache from 'node-cache'; +import cloneDeep from 'lodash.clonedeep'; import { createSelector } from 'reselect'; -import { stringifyUrl } from 'query-string'; -import sendJson from '../../../tmp_fetch_send_json'; -import { fetchTokenPrices, fetchTokenWhiteList } from '../../../utils/ref-finance'; +import FiatValueManager from '../../../utils/fiatValueManager'; import handleAsyncThunkStatus from '../../reducerStatus/handleAsyncThunkStatus'; import initialStatusState from '../../reducerStatus/initialState/initialStatusState'; const SLICE_NAME = 'tokenFiatValues'; -const COINGECKO_PRICE_URL = 'https://api.coingecko.com/api/v3/simple/price'; - -function wrapNodeCacheForDataloader(cache) { - return { - get: (...args) => { - return cache.get(...args); - }, - - set: (...args) => { - return cache.set(...args); - }, - - delete: (...args) => { - return cache.del(...args); - }, - - clear: (...args) => { - return cache.flushAll(...args); - } - - }; -} - -class FiatValueManager { - constructor(DataLoader) { - this.fiatValueDataLoader = DataLoader; - } - - async getPrice(tokens = ['near']) { - const byTokenName = {}; - - const prices = await this.fiatValueDataLoader.loadMany(tokens); - tokens.forEach((tokenName, ndx) => byTokenName[tokenName] = prices[ndx]); - - return byTokenName; - } -} - -const fiatValueDataLoader = new DataLoader( - async (tokenIds) => { - const tokenFiatValues = await sendJson( - 'GET', - stringifyUrl({ - url: COINGECKO_PRICE_URL, - query: { - ids: tokenIds.join(','), - vs_currencies: 'usd,eur,cny', - include_last_updated_at: true - } - }) - ); - - return tokenIds.map((id) => tokenFiatValues[id]); - }, - { - /* 0 checkperiod means we only purge values from the cache on attempting to read an expired value - Which allows us to avoid having to call `.close()` on the cache to allow node to exit cleanly */ - cacheMap: wrapNodeCacheForDataloader(new Cache({ stdTTL: 30, checkperiod: 0, useClones: false })) - } -); +const fiatValueManager = new FiatValueManager(); const fetchTokenFiatValues = createAsyncThunk( `${SLICE_NAME}/fetchTokenFiatValues`, async () => { - const fiatValueManager = new FiatValueManager(fiatValueDataLoader); - - const [coinGeckoTokenFiatValues, refFinanceTokenFiatValues] = await Promise.allSettled([fiatValueManager.getPrice(['near']), fetchTokenPrices()]); - - const last_updated_at = Date.now() / 1000; - const otherTokenFiatValues = Object.keys(refFinanceTokenFiatValues).reduce((acc, curr) => { - return ({ - ...acc, - [curr]: { - usd: +Number(refFinanceTokenFiatValues[curr]?.price).toFixed(2) || null, - last_updated_at - } - }); - }, {}); - - return merge({}, coinGeckoTokenFiatValues, otherTokenFiatValues); - } + const [coinGeckoTokenFiatValues, refFinanceTokenFiatValues] = await Promise.all( + [fiatValueManager.getPrice(['near', 'usn']), fiatValueManager.fetchTokenPrices()] + ); + return merge({}, coinGeckoTokenFiatValues, refFinanceTokenFiatValues); + } ); const getTokenWhiteList = createAsyncThunk( `${SLICE_NAME}/getTokenWhiteList`, - async (account_id) => fetchTokenWhiteList(account_id) + async (account_id) => fiatValueManager.fetchTokenWhiteList(account_id) ); @@ -112,10 +38,22 @@ const tokenFiatValuesSlice = createSlice({ builder.addCase(fetchTokenFiatValues.fulfilled, (state, action) => { // Payload of .fulfilled is in the same shape as the store; just merge it! // { near: { usd: x, lastUpdatedTimestamp: 1212312321, ... } - + const beenUpdatedTokens = {}; + const tokens = cloneDeep(state).tokens; + Object.keys(action.payload).map((token) => { + const previousLastUpdatedAt = tokens[token] ? tokens[token].last_updated_at : 0; + const previousPrice = tokens[token] ? tokens[token].usd : 0; + const fetchedLastUpdatedAt = action.payload[token].last_updated_at; + const fetchedPrice = action.payload[token].usd; + if (fetchedLastUpdatedAt > previousLastUpdatedAt && fetchedPrice !== previousPrice) { + beenUpdatedTokens[token] = action.payload[token]; + } + }); // Using merge instead of `assign()` so in the future we don't blow away previously loaded token // prices when we load new ones with different token names - merge(state.tokens, action.payload); + if (Object.keys(beenUpdatedTokens).length) { + merge(state.tokens, beenUpdatedTokens); + } }); builder.addCase(getTokenWhiteList.fulfilled, (state, action) => { state.tokenWhiteList = action.payload; @@ -147,11 +85,11 @@ export const selectNearTokenFiatValueUSD = createSelector(selectNearTokenFiatDat export const selectUSDNTokenFiatData = createSelector( selectAllTokenFiatValues, - ({ tokens }) => tokens.tether || {} + ({ tokens }) => tokens.usn || {} ); export const selectUSDNTokenFiatValueUSD = createSelector( selectUSDNTokenFiatData, - (tether) => tether.usd + (usn) => usn.usd ); export const selectTokensFiatValueUSD = createSelector(selectAllTokenFiatValues, ({ tokens }) => tokens || {}); diff --git a/packages/frontend/src/utils/fiatValueManager.js b/packages/frontend/src/utils/fiatValueManager.js new file mode 100644 index 0000000000..084281c1db --- /dev/null +++ b/packages/frontend/src/utils/fiatValueManager.js @@ -0,0 +1,117 @@ +import { Contract } from 'near-api-js'; +import DataLoader from 'dataloader'; +import Cache from 'node-cache'; +import { stringifyUrl } from 'query-string'; +import sendJson from '../tmp_fetch_send_json'; +import { REF_FINANCE_API_ENDPOINT, REF_FINANCE_CONTRACT} from '../config'; +import { wallet } from './wallet'; + +const COINGECKO_PRICE_URL = 'https://api.coingecko.com/api/v3/simple/price'; + +function wrapNodeCacheForDataloader(cache) { + return { + get: (...args) => { + return cache.get(...args); + }, + + set: (...args) => { + return cache.set(...args); + }, + + delete: (...args) => { + return cache.del(...args); + }, + + clear: (...args) => { + return cache.flushAll(...args); + } + }; +} + +export default class FiatValueManager { + constructor() { + this.coinGeckoFiatValueDataLoader = new DataLoader( + async (tokenIds) => { + try { + const tokenFiatValues = await sendJson( + 'GET', + stringifyUrl({ + url: COINGECKO_PRICE_URL, + query: { + ids: tokenIds.join(','), + vs_currencies: 'usd,eur,cny', + include_last_updated_at: true + } + }) + ); + return tokenIds.map((id) => tokenFiatValues[id]); + } catch (error) { + console.error(`Failed to fetch coingecko prices: ${error}`); + // DataLoader must be constructed with a function which accepts + // Array and returns Promise> of the same length + // as the Array of keys + return new Promise((resolve, reject) => { + return [].fill({}, tokenIds.length); + }); + } + }, + { + /* 0 checkperiod means we only purge values from the cache on attempting to read an expired value + Which allows us to avoid having to call `.close()` on the cache to allow node to exit cleanly */ + cacheMap: wrapNodeCacheForDataloader(new Cache({ stdTTL: 30, checkperiod: 0, useClones: false })) + } + ); + this.refFinanceDataLoader = new DataLoader( + async (dummyToken) => { + try { + const refFinanceTokenFiatValues = await sendJson( + 'GET', + REF_FINANCE_API_ENDPOINT + '/list-token-price' + ); + return [refFinanceTokenFiatValues]; + } catch (error) { + console.error(`Failed to fetch ref-finance prices: ${error}`); + return new Promise((resolve, reject) => [{}]); + } + }, + { + cacheMap: wrapNodeCacheForDataloader(new Cache({ stdTTL: 30, checkperiod: 0, useClones: false })) + } + ) + }; + + async getPrice(tokens = ['near', 'usn']) { + const byTokenName = {}; + const prices = await this.fiatValueDataLoader.loadMany(tokens); + tokens.forEach((tokenName, ndx) => byTokenName[tokenName] = prices[ndx]); + return byTokenName; + }; + + async fetchTokenPrices(dummyToken = ['near']) { + const [prices] = await this.refFinanceDataLoader.loadMany(dummyToken); + const last_updated_at = Date.now() / 1000; + const formattedValues = Object.keys(prices).reduce((acc, curr) => { + return ({ + ...acc, + [curr]: { + usd: +Number(prices[curr]?.price).toFixed(2) || null, + last_updated_at + } + }); + }, {}); + return formattedValues; + }; + + async fetchTokenWhiteList(accountId) { + try { + const account = wallet.getAccountBasic(accountId); + const contract = new Contract(account, REF_FINANCE_CONTRACT, {viewMethods: ['get_whitelisted_tokens']}); + const whiteListedTokens = await contract.get_whitelisted_tokens(); + + return whiteListedTokens; + } catch (error) { + console.error(`Failed to fetch whitelisted tokens: ${error}`); + return []; + } + }; +} diff --git a/packages/frontend/src/utils/ref-finance.js b/packages/frontend/src/utils/ref-finance.js deleted file mode 100644 index 26897e1451..0000000000 --- a/packages/frontend/src/utils/ref-finance.js +++ /dev/null @@ -1,27 +0,0 @@ -import { Contract } from 'near-api-js'; - -import { REF_FINANCE_API_ENDPOINT, REF_FINANCE_CONTRACT} from '../config'; -import sendJson from '../tmp_fetch_send_json'; -import { wallet } from './wallet'; - -export const fetchTokenPrices = async () => { - try { - return sendJson('GET', REF_FINANCE_API_ENDPOINT + '/list-token-price'); - } catch (error) { - console.error(`Failed to fetch token prices: ${error}`); - return {}; - } -}; - -export const fetchTokenWhiteList = async (accountId) => { - try { - const account = wallet.getAccountBasic(accountId); - const contract = new Contract(account, REF_FINANCE_CONTRACT, {viewMethods: ['get_whitelisted_tokens']}); - const whiteListedTokens = await contract.get_whitelisted_tokens(); - - return whiteListedTokens; - } catch (error) { - console.error(`Failed to fetch whitelisted tokens: ${error}`); - return []; - } -}; diff --git a/yarn.lock b/yarn.lock index 7c7740c1f2..8fb33ac9fc 100644 --- a/yarn.lock +++ b/yarn.lock @@ -3948,16 +3948,16 @@ clone-deep@^4.0.1: kind-of "^6.0.2" shallow-clone "^3.0.0" +clone@2.x, clone@^2.1.1: + version "2.1.2" + resolved "https://registry.yarnpkg.com/clone/-/clone-2.1.2.tgz#1b7f4b9f591f1e8f83670401600345a02887435f" + integrity sha1-G39Ln1kfHo+DZwQBYANFoCiHQ18= + clone@^1.0.2: version "1.0.4" resolved "https://registry.yarnpkg.com/clone/-/clone-1.0.4.tgz#da309cc263df15994c688ca902179ca3c7cd7c7e" integrity sha1-2jCcwmPfFZlMaIypAheco8fNfH4= -clone@^2.1.1: - version "2.1.2" - resolved "https://registry.yarnpkg.com/clone/-/clone-2.1.2.tgz#1b7f4b9f591f1e8f83670401600345a02887435f" - integrity sha1-G39Ln1kfHo+DZwQBYANFoCiHQ18= - cmd-shim@^4.1.0: version "4.1.0" resolved "https://registry.yarnpkg.com/cmd-shim/-/cmd-shim-4.1.0.tgz#b3a904a6743e9fede4148c6f3800bf2a08135bdd" @@ -4754,6 +4754,11 @@ data-urls@^2.0.0: whatwg-mimetype "^2.3.0" whatwg-url "^8.0.0" +dataloader@^2.1.0: + version "2.1.0" + resolved "https://registry.yarnpkg.com/dataloader/-/dataloader-2.1.0.tgz#c69c538235e85e7ac6c6c444bae8ecabf5de9df7" + integrity sha512-qTcEYLen3r7ojZNgVUaRggOI+KM7jrKxXeSHhogh/TWxYMeONEMqY+hmkobiYQozsGIyg9OYVzO4ZIfoB4I0pQ== + date-fns@^2.16.1: version "2.28.0" resolved "https://registry.yarnpkg.com/date-fns/-/date-fns-2.28.0.tgz#9570d656f5fc13143e50c975a3b6bbeb46cd08b2" @@ -9199,6 +9204,13 @@ node-addon-api@^1.7.1: resolved "https://registry.yarnpkg.com/node-addon-api/-/node-addon-api-1.7.2.tgz#3df30b95720b53c24e59948b49532b662444f54d" integrity sha512-ibPK3iA+vaY1eEjESkQkM0BbCqFOaZMiXRTtdB0u7b4djtY6JnsjvPdUHVMg6xQt3B8fpTTWHI9A+ADjM9frzg== +node-cache@^5.1.2: + version "5.1.2" + resolved "https://registry.yarnpkg.com/node-cache/-/node-cache-5.1.2.tgz#f264dc2ccad0a780e76253a694e9fd0ed19c398d" + integrity sha512-t1QzWwnk4sjLWaQAS8CHgOJ+RAfmHpxFWmc36IWTiWHQfs0w5JDMBS1b1ZxQteo0vVVuWJvIUKHDkkeK7vIGCg== + dependencies: + clone "2.x" + node-fetch@2.6.7, node-fetch@^2.4.1, node-fetch@^2.6.0, node-fetch@^2.6.1, node-fetch@^2.6.2, node-fetch@^2.6.6: version "2.6.7" resolved "https://registry.yarnpkg.com/node-fetch/-/node-fetch-2.6.7.tgz#24de9fba827e3b4ae44dc8b20256a379160052ad" @@ -13665,4 +13677,4 @@ yazl@^2.5.1: yn@3.1.1: version "3.1.1" resolved "https://registry.yarnpkg.com/yn/-/yn-3.1.1.tgz#1e87401a09d767c1d5eab26a6e4c185182d2eb50" - integrity sha512-Ux4ygGWsu2c7isFWe8Yu1YluJmqVhxqK2cLXNQA5AcC3QfbGNpM7fu0Y8b/z16pXLnFxZYvWhd3fhBY9DLmC6Q== \ No newline at end of file + integrity sha512-Ux4ygGWsu2c7isFWe8Yu1YluJmqVhxqK2cLXNQA5AcC3QfbGNpM7fu0Y8b/z16pXLnFxZYvWhd3fhBY9DLmC6Q==