From db6e948d9f23d41a34d492d40f2119e893b89916 Mon Sep 17 00:00:00 2001 From: Timofei Domnikov Date: Thu, 26 Jan 2023 17:42:01 +0300 Subject: [PATCH] KEEP-1125: load usd prices from data service --- .size-limit.json | 2 +- src/_core/usdPrices.tsx | 100 +++++++++++++++++++++++ src/assets/constants.ts | 9 -- src/background.ts | 4 + src/controllers/assetInfo.ts | 72 ++++++---------- src/popup.tsx | 9 +- src/ui/components/ui/UsdAmount/index.tsx | 22 +++-- src/ui/services/Background.ts | 7 ++ 8 files changed, 157 insertions(+), 68 deletions(-) create mode 100644 src/_core/usdPrices.tsx diff --git a/.size-limit.json b/.size-limit.json index 63ea39292..aabc00f3b 100644 --- a/.size-limit.json +++ b/.size-limit.json @@ -17,7 +17,7 @@ "dist/build/popup.js", "dist/build/vendors*.js" ], - "limit": "438 kB" + "limit": "439 kB" }, { "name": "contentscript", diff --git a/src/_core/usdPrices.tsx b/src/_core/usdPrices.tsx new file mode 100644 index 000000000..cfa7720b5 --- /dev/null +++ b/src/_core/usdPrices.tsx @@ -0,0 +1,100 @@ +import { + createContext, + ReactNode, + useCallback, + useContext, + useEffect, + useMemo, + useRef, + useState, +} from 'react'; +import invariant from 'tiny-invariant'; + +import { usePopupSelector } from '../popup/store/react'; +import Background from '../ui/services/Background'; +import { startPolling } from './polling'; +import { useDebouncedValue } from './useDebouncedValue'; + +const USD_PRICES_UPDATE_INTERVAL = 5000; + +const UsdPricesContext = createContext< + ((assetIds: string[]) => (() => void) | undefined) | null +>(null); + +export function UsdPricesProvider({ children }: { children: ReactNode }) { + const lastUpdatedAssetIdsTimestampsRef = useRef>({}); + const [observedAssetIds, setObservedAssetIds] = useState([]); + const observedAssetIdsDebounced = useDebouncedValue(observedAssetIds, 100); + + useEffect(() => { + const idsToUpdate = Array.from(new Set(observedAssetIdsDebounced.flat())); + + if (idsToUpdate.length === 0) { + return; + } + + return startPolling(USD_PRICES_UPDATE_INTERVAL, async () => { + const currentTime = new Date().getTime(); + + const areAllAssetsUpToDate = idsToUpdate.every(id => { + const timestamp = lastUpdatedAssetIdsTimestampsRef.current[id]; + + if (timestamp == null) { + return false; + } + + return currentTime - timestamp < USD_PRICES_UPDATE_INTERVAL; + }); + + if (!areAllAssetsUpToDate) { + await Background.updateUsdPricesByAssetIds(idsToUpdate); + + const updatedTime = new Date().getTime(); + + for (const id of idsToUpdate) { + lastUpdatedAssetIdsTimestampsRef.current[id] = updatedTime; + } + } + }); + }, [observedAssetIdsDebounced]); + + const observe = useCallback((assetIds: string[]) => { + setObservedAssetIds(ids => [...ids, assetIds]); + + return () => { + setObservedAssetIds(prev => prev.filter(ids => ids !== assetIds)); + }; + }, []); + + return ( + + {children} + + ); +} + +export function useUsdPrices(assetIds: string[]) { + const currentNetwork = usePopupSelector(state => state.currentNetwork); + const isMainnet = currentNetwork === 'mainnet'; + + const observe = useContext(UsdPricesContext); + invariant(observe); + + useEffect(() => { + if (!isMainnet) { + return; + } + + return observe(assetIds); + }, [observe, assetIds, isMainnet]); + + const usdPrices = usePopupSelector(state => state.usdPrices); + + return useMemo(() => { + const assetIdsSet = new Set(assetIds); + + return Object.fromEntries( + Object.entries(usdPrices).filter(([id]) => assetIdsSet.has(id)) + ); + }, [assetIds, usdPrices]); +} diff --git a/src/assets/constants.ts b/src/assets/constants.ts index b9d5771b9..51f78b755 100644 --- a/src/assets/constants.ts +++ b/src/assets/constants.ts @@ -98,15 +98,6 @@ export const assetIds: Record> = { }, }; -export const stablecoinAssetIds = new Set([ - '2thtesXvnVMcCnih9iZbJL3d2NQZMfzENJo8YFj6r5jU', - '34N9YcEETLWn93qYQ64EsP1x89tSruJU44RrEMSXXEPJ', - '6XtHjpXbs9RRJP2Sr9GUyVqzACcby9TkThHXnjVC5CDJ', - '8DLiYZjo3UUaRBTHU7Ayoqg4ihwb6YH1AfXrrhdjQ7K1', - '8zUYbdB8Q6mDhpcXYv52ji8ycfj4SDX4gJXS7YY3dA4R', - 'DG2xFkPdDwKUoBkzGAhQtLpSGzfXLiCYPEzeKH2Ad24p', -]); - export const defaultAssetTickers = { B1dG9exXzJdFASDF2MwCE7TYJE5My4UgVRx43nqDbF6s: 'ABTCLPC', '4NyYnDGopZvEAQ3TcBDJrJFWSiA2xzuAw83Ms8jT7WuK': 'ABTCLPM', diff --git a/src/background.ts b/src/background.ts index 91e803702..8f716d05a 100644 --- a/src/background.ts +++ b/src/background.ts @@ -532,6 +532,10 @@ class BackgroundService extends EventEmitter { updateAssets: this.assetInfoController.updateAssets.bind( this.assetInfoController ), + updateUsdPricesByAssetIds: + this.assetInfoController.updateUsdPricesByAssetIds.bind( + this.assetInfoController + ), toggleAssetFavorite: this.assetInfoController.toggleAssetFavorite.bind( this.assetInfoController ), diff --git a/src/controllers/assetInfo.ts b/src/controllers/assetInfo.ts index fa41ca9fa..30a1169c0 100644 --- a/src/controllers/assetInfo.ts +++ b/src/controllers/assetInfo.ts @@ -4,7 +4,7 @@ import { NetworkName } from 'networks/types'; import ObservableStore from 'obs-store'; import Browser from 'webextension-polyfill'; -import { defaultAssetTickers, stablecoinAssetIds } from '../assets/constants'; +import { defaultAssetTickers } from '../assets/constants'; import { ExtensionStorage, StorageLocalState } from '../storage/storage'; import { NetworkController } from './network'; import { RemoteConfigController } from './remoteConfig'; @@ -30,12 +30,8 @@ const SUSPICIOUS_LIST_URL = const SUSPICIOUS_PERIOD_IN_MINUTES = 60; const MAX_AGE = 60 * 60 * 1000; -const MARKETDATA_URL = 'https://marketdata.wavesplatform.com/'; -const MARKETDATA_USD_ASSET_ID = 'DG2xFkPdDwKUoBkzGAhQtLpSGzfXLiCYPEzeKH2Ad24p'; -const MARKETDATA_PERIOD_IN_MINUTES = 10; - -const STATIC_SERVICE_URL = 'https://api.keeper-wallet.app'; -const SWAPSERVICE_URL = 'https://swap-api.keeper-wallet.app'; +const DATA_SERVICE_URL = 'https://api.keeper-wallet.app'; +const SWAP_SERVICE_URL = 'https://swap-api.keeper-wallet.app'; const INFO_PERIOD_IN_MINUTES = 60; const SWAPPABLE_ASSETS_UPDATE_PERIOD_IN_MINUTES = 240; @@ -123,19 +119,12 @@ export class AssetInfoController { this.updateSuspiciousAssets(); } - if (Object.keys(initState.usdPrices).length === 0) { - this.updateUsdPrices(); - } - this.updateInfo(); this.updateSwappableAssetIdsByVendor(); Browser.alarms.create('updateSuspiciousAssets', { periodInMinutes: SUSPICIOUS_PERIOD_IN_MINUTES, }); - Browser.alarms.create('updateUsdPrices', { - periodInMinutes: MARKETDATA_PERIOD_IN_MINUTES, - }); Browser.alarms.create('updateInfo', { periodInMinutes: INFO_PERIOD_IN_MINUTES, }); @@ -148,9 +137,6 @@ export class AssetInfoController { case 'updateSuspiciousAssets': this.updateSuspiciousAssets(); break; - case 'updateUsdPrices': - this.updateUsdPrices(); - break; case 'updateInfo': this.updateInfo(); break; @@ -392,49 +378,39 @@ export class AssetInfoController { } } - async updateUsdPrices() { - const { usdPrices } = this.store.getState(); + async updateUsdPricesByAssetIds(assetIds: string[]) { const network = this.getNetwork(); - if (!usdPrices || network === NetworkName.Mainnet) { - const resp = await fetch(new URL('/api/tickers', MARKETDATA_URL)); + if (assetIds.length === 0 || network !== NetworkName.Mainnet) { + return; + } - if (resp.ok) { - const tickers = (await resp.json()) as Array<{ - '24h_close': string; - amountAssetID: string; - priceAssetID: string; - }>; + const { usdPrices } = this.store.getState(); - // eslint-disable-next-line @typescript-eslint/no-shadow - const usdPrices = tickers.reduce>( - (acc, ticker) => { - if ( - !stablecoinAssetIds.has(ticker.amountAssetID) && - ticker.priceAssetID === MARKETDATA_USD_ASSET_ID - ) { - acc[ticker.amountAssetID] = ticker['24h_close']; - } + const response = await fetch(new URL('/api/v1/rates', DATA_SERVICE_URL), { + method: 'POST', + body: JSON.stringify({ ids: assetIds }), + }); - return acc; - }, - {} - ); + if (!response.ok) { + throw response; + } - stablecoinAssetIds.forEach(ticker => { - usdPrices[ticker] = '1'; - }); + const updatedUsdPrices: Record = await response.json(); - this.store.updateState({ usdPrices }); - } - } + this.store.updateState({ + usdPrices: { + ...usdPrices, + ...updatedUsdPrices, + }, + }); } async updateInfo() { const network = this.getNetwork(); if (network === NetworkName.Mainnet) { - const resp = await fetch(new URL('/api/v1/assets', STATIC_SERVICE_URL)); + const resp = await fetch(new URL('/api/v1/assets', DATA_SERVICE_URL)); if (resp.ok) { const assets = (await resp.json()) as Array<{ @@ -463,7 +439,7 @@ export class AssetInfoController { } async updateSwappableAssetIdsByVendor() { - const resp = await fetch(new URL('/assets', SWAPSERVICE_URL)); + const resp = await fetch(new URL('/assets', SWAP_SERVICE_URL)); if (resp.ok) { const swappableAssetIdsByVendor = (await resp.json()) as Record< string, diff --git a/src/popup.tsx b/src/popup.tsx index 401d81ee8..942929796 100644 --- a/src/popup.tsx +++ b/src/popup.tsx @@ -13,6 +13,7 @@ import invariant from 'tiny-invariant'; import Browser from 'webextension-polyfill'; import { SignProvider } from './_core/signContext'; +import { UsdPricesProvider } from './_core/usdPrices'; import type { UiApi } from './background'; import { i18nextInit } from './i18n/init'; import { @@ -58,9 +59,11 @@ Promise.all([ - - - + + + + + diff --git a/src/ui/components/ui/UsdAmount/index.tsx b/src/ui/components/ui/UsdAmount/index.tsx index 3b841fae5..4081efd20 100644 --- a/src/ui/components/ui/UsdAmount/index.tsx +++ b/src/ui/components/ui/UsdAmount/index.tsx @@ -1,5 +1,9 @@ import BigNumber from '@waves/bignumber'; import { usePopupSelector } from 'popup/store/react'; +import { useMemo } from 'react'; + +import { useUsdPrices } from '../../../../_core/usdPrices'; +import { Loader } from '../loader'; interface Props { id: string; @@ -8,18 +12,22 @@ interface Props { } export function UsdAmount({ id, tokens, className }: Props) { - const usdPrices = usePopupSelector(state => state.usdPrices); - const currentNetwork = usePopupSelector(state => state.currentNetwork); const isMainnet = currentNetwork === 'mainnet'; - if (!usdPrices || !isMainnet) { + const usdPrices = useUsdPrices(useMemo(() => [id], [id])); + + if (!isMainnet) { return null; } - return !usdPrices[id] || usdPrices[id] === '1' ? null : ( -

{`≈ $${new BigNumber(usdPrices[id]) - .mul(tokens) - .toFixed(2)}`}

+ if (usdPrices[id] == null) { + return ; + } + + return ( +

+ ≈ ${new BigNumber(usdPrices[id]).mul(tokens).toFixed(2)} +

); } diff --git a/src/ui/services/Background.ts b/src/ui/services/Background.ts index 89c41c29a..2b0047d8a 100644 --- a/src/ui/services/Background.ts +++ b/src/ui/services/Background.ts @@ -334,6 +334,13 @@ class Background { return await this.background!.updateAssets(assetIds, options); } + async updateUsdPricesByAssetIds(assetIds: string[]) { + await this.initPromise; + this._connect(); + // eslint-disable-next-line @typescript-eslint/no-non-null-assertion + return await this.background!.updateUsdPricesByAssetIds(assetIds); + } + async setAddress(address: string, name: string): Promise { await this.initPromise; this._connect();