diff --git a/packages/bento-web/pages/_app.tsx b/packages/bento-web/pages/_app.tsx
index c46a3eca..833247d3 100644
--- a/packages/bento-web/pages/_app.tsx
+++ b/packages/bento-web/pages/_app.tsx
@@ -16,6 +16,7 @@ import styled from 'styled-components';
import { LoadingProgress } from '@/components/LoadingProgress';
import { NavigationBar } from '@/components/NavigationBar';
+import { PricingsProvider } from '@/hooks/pricings';
import { GlobalStyle } from '@/styles/GlobalStyle';
Analytics.initialize();
@@ -65,22 +66,24 @@ const App = ({ Component, pageProps }: AppProps) => {
-
-
-
-
-
-
-
-
-
-
-
-
-
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
);
};
diff --git a/packages/bento-web/src/dashboard/utils/useMultipleRequests.ts b/packages/bento-web/src/dashboard/utils/useMultipleRequests.ts
index bdca857a..274a6774 100644
--- a/packages/bento-web/src/dashboard/utils/useMultipleRequests.ts
+++ b/packages/bento-web/src/dashboard/utils/useMultipleRequests.ts
@@ -19,7 +19,6 @@ export const useMultipleRequests = (
...responsesRef.current[requestKey],
isLoading: true,
};
- setCount((prev) => prev + 1);
axios
.get(requestKey)
diff --git a/packages/bento-web/src/dashboard/utils/useNFTBalances.ts b/packages/bento-web/src/dashboard/utils/useNFTBalances.ts
index 3f36e8b8..341456ef 100644
--- a/packages/bento-web/src/dashboard/utils/useNFTBalances.ts
+++ b/packages/bento-web/src/dashboard/utils/useNFTBalances.ts
@@ -1,10 +1,11 @@
import { Wallet, safeAsyncFlatMap, safePromiseAll } from '@bento/common';
import { OpenSea, OpenSeaAsset } from '@bento/core';
-import { priceFromCoinGecko } from '@bento/core';
import chunk from 'lodash.chunk';
import groupBy from 'lodash.groupby';
import { useEffect, useState } from 'react';
+import { useCachedPricings } from '@/hooks/pricings';
+
import { NFTWalletBalance } from '@/dashboard/types/WalletBalance';
const CHUNK_SIZE = 5;
@@ -14,6 +15,7 @@ type Options = {
};
export const useNFTBalances = ({ wallets }: Options) => {
+ const { getCachedPrice } = useCachedPricings();
const [openSeaNFTBalance, setOpenSeaNFTBalance] = useState<
NFTWalletBalance[]
>([]);
@@ -63,8 +65,8 @@ export const useNFTBalances = ({ wallets }: Options) => {
};
main();
- priceFromCoinGecko('ethereum').then(setEthereumPrice);
- }, [JSON.stringify(wallets)]);
+ setEthereumPrice(getCachedPrice('ethereum'));
+ }, [getCachedPrice, JSON.stringify(wallets)]);
useEffect(() => {
const flattedAssets = Object.values(fetchedAssets)
diff --git a/packages/bento-web/src/dashboard/utils/useWalletBalances.ts b/packages/bento-web/src/dashboard/utils/useWalletBalances.ts
index 2d2554b4..b48ea3c0 100644
--- a/packages/bento-web/src/dashboard/utils/useWalletBalances.ts
+++ b/packages/bento-web/src/dashboard/utils/useWalletBalances.ts
@@ -1,7 +1,8 @@
import { Wallet } from '@bento/common';
-import { pricesFromCoinGecko } from '@bento/core';
import produce from 'immer';
-import { useCallback, useEffect, useMemo, useState } from 'react';
+import { useEffect, useMemo, useState } from 'react';
+
+import { useCachedPricings } from '@/hooks/pricings';
import { KEYS_BY_NETWORK } from '@/constants/networks';
import {
@@ -25,6 +26,8 @@ type Options = {
};
export const useWalletBalances = ({ wallets }: Options) => {
+ const { getCachedPrice } = useCachedPricings();
+
const calculatedRequests = useMemo(() => {
// TODO: Clean this thing up
const data: PartialRecord =
@@ -67,48 +70,24 @@ export const useWalletBalances = ({ wallets }: Options) => {
useInterval(refetch, 60 * 1_000);
const balances = useMemo(() => result.flatMap((v) => v.data ?? []), [result]);
-
- const coinGeckoIds = useMemo(
- () => balances.flatMap((v) => v.coinGeckoId || []),
- [balances],
- );
-
- const [coinGeckoPricesByIds, setCoinGeckoPricesByIds] = useState<
- Record
- >({});
-
- const fetchPrices = useCallback(() => {
- if (!coinGeckoIds.length) {
- return;
- }
-
- pricesFromCoinGecko(coinGeckoIds)
- .then(setCoinGeckoPricesByIds)
- .catch((error) => {
- console.error(error);
- });
- }, [JSON.stringify(coinGeckoIds)]);
+ const [balancesWithPrices, setBalancesWithPrices] = useState<
+ (EVMWalletBalance | CosmosSDKWalletBalance | SolanaWalletBalance)[]
+ >([]);
useEffect(() => {
- fetchPrices();
- }, [fetchPrices]);
- useInterval(fetchPrices, 60 * 1_000);
-
- const balancesWithPrices = useMemo(
- () =>
- produce(balances, (draft) => {
- draft.forEach((token) => {
- if (typeof token.price === 'undefined') {
- if (!!token.coinGeckoId) {
- token.price = coinGeckoPricesByIds[token.coinGeckoId] ?? 0;
- } else {
- token.price = 0;
- }
+ const result = produce(balances, (draft) => {
+ draft.forEach((token) => {
+ if (typeof token.price === 'undefined') {
+ if (!!token.coinGeckoId) {
+ token.price = getCachedPrice(token.coinGeckoId);
+ } else {
+ token.price = 0;
}
- });
- }),
- [balances, JSON.stringify(coinGeckoPricesByIds)],
- );
+ }
+ });
+ });
+ setBalancesWithPrices(result);
+ }, [balances, getCachedPrice]);
return {
balances: balancesWithPrices,
diff --git a/packages/bento-web/src/hooks/pricings.tsx b/packages/bento-web/src/hooks/pricings.tsx
new file mode 100644
index 00000000..3d8e852c
--- /dev/null
+++ b/packages/bento-web/src/hooks/pricings.tsx
@@ -0,0 +1,150 @@
+import { safePromiseAll } from '@bento/common';
+import { pricesFromCoinGecko } from '@bento/core';
+import { atom, useAtom, useAtomValue, useSetAtom } from 'jotai';
+import React, { useCallback } from 'react';
+
+import { useInterval } from '@/hooks/useInterval';
+
+import { Config } from '@/utils';
+
+import { useLazyEffect } from './useLazyEffect';
+
+const SECONDS = 1_000;
+const CACHE_TIME = 60 * SECONDS;
+
+type ValueVO = [v: number, t: number];
+const buildCacheStore = (prefix: string) => ({
+ get: async (key: string): Promise => {
+ const v = localStorage.getItem(prefix);
+ if (!v) {
+ return null;
+ }
+ return JSON.parse(v)[key] || null;
+ },
+ set: async (key: string, value: T) => {
+ const v = localStorage.getItem(prefix);
+ const prev = !v ? {} : JSON.parse(v);
+ localStorage.setItem(prefix, JSON.stringify({ ...prev, [key]: value }));
+ },
+});
+const CacheStore = buildCacheStore('@pricings');
+
+type PricingMap = Record;
+export const pricingsAtom = atom({});
+export const coinGeckoIdsAtom = atom([]);
+
+const useCoinGeckoPrices = () => {
+ const coinGeckoIds = useAtomValue(coinGeckoIdsAtom);
+ const [prices, setPrices] = useAtom(pricingsAtom);
+
+ const fetchPrices = useCallback(async () => {
+ if (!coinGeckoIds.length) {
+ return;
+ }
+
+ if (Config.ENVIRONMENT !== 'production') {
+ console.log('hit');
+ }
+
+ const cachedIds: string[] = [];
+ const cachedPrices = await safePromiseAll(
+ coinGeckoIds.map(async (coinGeckoId) => {
+ const cachedValue = await CacheStore.get(coinGeckoId).catch(() => null);
+ if (!cachedValue) {
+ return [coinGeckoId, 0];
+ }
+ const [value, cachedAt] = cachedValue as ValueVO;
+
+ if (cachedAt >= Date.now() - CACHE_TIME) {
+ cachedIds.push(coinGeckoId);
+ return [coinGeckoId, value];
+ }
+
+ return [coinGeckoId, 0];
+ }),
+ );
+ let cachedPricesObject: PricingMap | null = null;
+ if (cachedPrices.length > 0) {
+ cachedPricesObject = Object.fromEntries(cachedPrices);
+ setPrices(cachedPricesObject as PricingMap);
+ }
+
+ const cachedIdsSet = new Set(cachedIds);
+ const uncachedIds = coinGeckoIds.filter((id) => !cachedIdsSet.has(id));
+
+ const fetchedPrices =
+ uncachedIds.length === 0
+ ? []
+ : await pricesFromCoinGecko(uncachedIds).then((prices) =>
+ Object.entries(prices),
+ );
+ await safePromiseAll(
+ fetchedPrices.map(([key, price]) =>
+ CacheStore.set(key, [price, Date.now()]),
+ ),
+ );
+
+ const fetchedPricesObject = Object.fromEntries(fetchedPrices);
+
+ setPrices({
+ ...cachedPricesObject,
+ ...fetchedPricesObject,
+ });
+ }, [JSON.stringify(coinGeckoIds)]);
+
+ useLazyEffect(
+ () => {
+ fetchPrices();
+ },
+ [fetchPrices],
+ 2_000,
+ );
+
+ useInterval(fetchPrices, CACHE_TIME);
+
+ return prices;
+};
+
+interface GetCachedPrice {
+ (coinGeckoId: string): number;
+ (coinGeckoIds: string[]): number[];
+}
+const GetCachedPriceContext = React.createContext(((
+ v: string | string[],
+) => (typeof v === 'string' ? 0 : [0])) as GetCachedPrice);
+
+export const PricingsProvider: React.FC = ({
+ children,
+}) => {
+ useCoinGeckoPrices();
+
+ const [prices] = useAtom(pricingsAtom);
+ const setCoinGeckoIds = useSetAtom(coinGeckoIdsAtom);
+
+ const getCachedPrice = useCallback(
+ (coinGeckoId: string | string[]) => {
+ const single = typeof coinGeckoId === 'string';
+ const ids = single ? [coinGeckoId] : coinGeckoId;
+ const res = ids.map((id) => {
+ if (id in prices) {
+ return prices[id];
+ }
+ setCoinGeckoIds((prev) => (prev.includes(id) ? prev : [...prev, id]));
+ return 0;
+ });
+ return single ? res[0] : res;
+ },
+ [prices],
+ ) as GetCachedPrice;
+
+ return (
+
+ {children}
+
+ );
+};
+
+export const useCachedPricings = () => {
+ const getCachedPrice = React.useContext(GetCachedPriceContext);
+ return { getCachedPrice };
+};
diff --git a/packages/bento-web/src/hooks/useLazyEffect.ts b/packages/bento-web/src/hooks/useLazyEffect.ts
new file mode 100644
index 00000000..19c242c3
--- /dev/null
+++ b/packages/bento-web/src/hooks/useLazyEffect.ts
@@ -0,0 +1,28 @@
+import {
+ DependencyList,
+ EffectCallback,
+ useCallback,
+ useEffect,
+ useRef,
+} from 'react';
+
+import { debounce } from '@/utils/debounce';
+
+export function useLazyEffect(
+ effect: EffectCallback,
+ deps: DependencyList = [],
+ wait: number,
+) {
+ const cleanUp = useRef void)>();
+ const effectRef = useRef();
+ effectRef.current = useCallback(effect, deps);
+ const lazyEffect = useCallback(
+ debounce(() => (cleanUp.current = effectRef.current?.()), wait),
+ [],
+ );
+ useEffect(lazyEffect, deps);
+ useEffect(() => {
+ return () =>
+ cleanUp.current instanceof Function ? cleanUp.current() : undefined;
+ }, []);
+}
diff --git a/packages/bento-web/src/utils/debounce.ts b/packages/bento-web/src/utils/debounce.ts
new file mode 100644
index 00000000..5ce2ee21
--- /dev/null
+++ b/packages/bento-web/src/utils/debounce.ts
@@ -0,0 +1,22 @@
+import { Config } from './Config';
+
+export const debounce = (
+ func: (...args: Arguments) => any,
+ wait: number,
+ immediate?: boolean,
+) => {
+ let timeout: NodeJS.Timeout | null = null;
+ return function (...args: Arguments) {
+ if (timeout) {
+ clearTimeout(timeout);
+ }
+ if (Config.ENVIRONMENT !== 'production') {
+ console.log('debounce');
+ }
+ timeout = setTimeout(function () {
+ timeout = null;
+ if (!immediate) func(...args);
+ }, wait);
+ if (immediate && !timeout) func(...args);
+ };
+};