diff --git a/README.md b/README.md index ebfc391d8..bd5b086e7 100644 --- a/README.md +++ b/README.md @@ -270,6 +270,7 @@ The file `common.ts` with type [`AppConfig`](src/config/types.ts) contains impor - `ui` - `priceChart`: use `tradingView` chart or `native` chart for token pair price history. You need to provide a backend with price history endpoint to support `native` view. - `useGradientBranding`: Flag to enable gradient styles for buttons. + - `tradeCount`: Display the amount of trades in the explorer page. #### Gas token different than native token diff --git a/e2e/screenshots/simulator/recurring/Recurring_limit_limit/simulator-input-price.png b/e2e/screenshots/simulator/recurring/Recurring_limit_limit/simulator-input-price.png index cefd7a622..0eaeb2525 100644 Binary files a/e2e/screenshots/simulator/recurring/Recurring_limit_limit/simulator-input-price.png and b/e2e/screenshots/simulator/recurring/Recurring_limit_limit/simulator-input-price.png differ diff --git a/e2e/screenshots/strategy/disposable/Disposable_buy_limit/create/form.png b/e2e/screenshots/strategy/disposable/Disposable_buy_limit/create/form.png index a462fe328..70d1bb744 100644 Binary files a/e2e/screenshots/strategy/disposable/Disposable_buy_limit/create/form.png and b/e2e/screenshots/strategy/disposable/Disposable_buy_limit/create/form.png differ diff --git a/e2e/screenshots/strategy/disposable/Disposable_buy_limit/create/my-strategy.png b/e2e/screenshots/strategy/disposable/Disposable_buy_limit/create/my-strategy.png index c9ad933cb..37b5fa7d0 100644 Binary files a/e2e/screenshots/strategy/disposable/Disposable_buy_limit/create/my-strategy.png and b/e2e/screenshots/strategy/disposable/Disposable_buy_limit/create/my-strategy.png differ diff --git a/e2e/screenshots/strategy/disposable/Disposable_buy_limit/deposit/form.png b/e2e/screenshots/strategy/disposable/Disposable_buy_limit/deposit/form.png index 3178226d4..ac5d38ff2 100644 Binary files a/e2e/screenshots/strategy/disposable/Disposable_buy_limit/deposit/form.png and b/e2e/screenshots/strategy/disposable/Disposable_buy_limit/deposit/form.png differ diff --git a/e2e/screenshots/strategy/disposable/Disposable_buy_range/create/my-strategy.png b/e2e/screenshots/strategy/disposable/Disposable_buy_range/create/my-strategy.png index ae30b64bd..86eb5b967 100644 Binary files a/e2e/screenshots/strategy/disposable/Disposable_buy_range/create/my-strategy.png and b/e2e/screenshots/strategy/disposable/Disposable_buy_range/create/my-strategy.png differ diff --git a/e2e/screenshots/strategy/disposable/Disposable_buy_range/deposit/form.png b/e2e/screenshots/strategy/disposable/Disposable_buy_range/deposit/form.png index a3ed96b5f..4af601d62 100644 Binary files a/e2e/screenshots/strategy/disposable/Disposable_buy_range/deposit/form.png and b/e2e/screenshots/strategy/disposable/Disposable_buy_range/deposit/form.png differ diff --git a/e2e/screenshots/strategy/disposable/Disposable_sell_limit/create/my-strategy.png b/e2e/screenshots/strategy/disposable/Disposable_sell_limit/create/my-strategy.png index 917016517..67b3a8274 100644 Binary files a/e2e/screenshots/strategy/disposable/Disposable_sell_limit/create/my-strategy.png and b/e2e/screenshots/strategy/disposable/Disposable_sell_limit/create/my-strategy.png differ diff --git a/e2e/screenshots/strategy/disposable/Disposable_sell_range/create/my-strategy.png b/e2e/screenshots/strategy/disposable/Disposable_sell_range/create/my-strategy.png index e83f4bdb3..d34571598 100644 Binary files a/e2e/screenshots/strategy/disposable/Disposable_sell_range/create/my-strategy.png and b/e2e/screenshots/strategy/disposable/Disposable_sell_range/create/my-strategy.png differ diff --git a/e2e/screenshots/strategy/overlapping/Overlapping/create/my-strategy.png b/e2e/screenshots/strategy/overlapping/Overlapping/create/my-strategy.png index 822ddddab..bb0d75f32 100644 Binary files a/e2e/screenshots/strategy/overlapping/Overlapping/create/my-strategy.png and b/e2e/screenshots/strategy/overlapping/Overlapping/create/my-strategy.png differ diff --git a/e2e/screenshots/strategy/recurring/Recurring_limit_limit/create/my-strategy.png b/e2e/screenshots/strategy/recurring/Recurring_limit_limit/create/my-strategy.png index 8fb8bf669..07caf7b2c 100644 Binary files a/e2e/screenshots/strategy/recurring/Recurring_limit_limit/create/my-strategy.png and b/e2e/screenshots/strategy/recurring/Recurring_limit_limit/create/my-strategy.png differ diff --git a/e2e/screenshots/strategy/recurring/Recurring_limit_limit/deposit/form.png b/e2e/screenshots/strategy/recurring/Recurring_limit_limit/deposit/form.png index 8aa67cd3a..b447aae49 100644 Binary files a/e2e/screenshots/strategy/recurring/Recurring_limit_limit/deposit/form.png and b/e2e/screenshots/strategy/recurring/Recurring_limit_limit/deposit/form.png differ diff --git a/e2e/screenshots/strategy/recurring/Recurring_limit_range/create/form.png b/e2e/screenshots/strategy/recurring/Recurring_limit_range/create/form.png index a1a290162..71b8a378f 100644 Binary files a/e2e/screenshots/strategy/recurring/Recurring_limit_range/create/form.png and b/e2e/screenshots/strategy/recurring/Recurring_limit_range/create/form.png differ diff --git a/e2e/screenshots/strategy/recurring/Recurring_limit_range/create/my-strategy.png b/e2e/screenshots/strategy/recurring/Recurring_limit_range/create/my-strategy.png index e1944b3ca..df7c0dc0c 100644 Binary files a/e2e/screenshots/strategy/recurring/Recurring_limit_range/create/my-strategy.png and b/e2e/screenshots/strategy/recurring/Recurring_limit_range/create/my-strategy.png differ diff --git a/e2e/screenshots/strategy/recurring/Recurring_range_limit/create/form.png b/e2e/screenshots/strategy/recurring/Recurring_range_limit/create/form.png index b832884cd..78cdd4d14 100644 Binary files a/e2e/screenshots/strategy/recurring/Recurring_range_limit/create/form.png and b/e2e/screenshots/strategy/recurring/Recurring_range_limit/create/form.png differ diff --git a/e2e/screenshots/strategy/recurring/Recurring_range_limit/create/my-strategy.png b/e2e/screenshots/strategy/recurring/Recurring_range_limit/create/my-strategy.png index f696adbf7..fabb27839 100644 Binary files a/e2e/screenshots/strategy/recurring/Recurring_range_limit/create/my-strategy.png and b/e2e/screenshots/strategy/recurring/Recurring_range_limit/create/my-strategy.png differ diff --git a/e2e/screenshots/strategy/recurring/Recurring_range_range/create/my-strategy.png b/e2e/screenshots/strategy/recurring/Recurring_range_range/create/my-strategy.png index 2e0e6b7c0..756ca3684 100644 Binary files a/e2e/screenshots/strategy/recurring/Recurring_range_range/create/my-strategy.png and b/e2e/screenshots/strategy/recurring/Recurring_range_range/create/my-strategy.png differ diff --git a/e2e/screenshots/strategy/recurring/Recurring_range_range/deposit/form.png b/e2e/screenshots/strategy/recurring/Recurring_range_range/deposit/form.png index ebcc5628d..b101b3a0e 100644 Binary files a/e2e/screenshots/strategy/recurring/Recurring_range_range/deposit/form.png and b/e2e/screenshots/strategy/recurring/Recurring_range_range/deposit/form.png differ diff --git a/src/components/debug/DebugTransferNFT.tsx b/src/components/debug/DebugTransferNFT.tsx index d9c6b3c3d..3157a4dbf 100644 --- a/src/components/debug/DebugTransferNFT.tsx +++ b/src/components/debug/DebugTransferNFT.tsx @@ -31,7 +31,9 @@ export const DebugTransferNFT = () => { inputId ); await tx?.wait(); - await cache.invalidateQueries({ queryKey: QueryKey.strategies(user) }); + await cache.invalidateQueries({ + queryKey: QueryKey.strategiesByUser(user), + }); setIsSuccess(true); } catch (e) { console.error('failed to transfer NFT', e); diff --git a/src/components/explorer/ExplorerHeader.tsx b/src/components/explorer/ExplorerHeader.tsx new file mode 100644 index 000000000..0b73bd2c3 --- /dev/null +++ b/src/components/explorer/ExplorerHeader.tsx @@ -0,0 +1,337 @@ +import { buttonStyles } from 'components/common/button/buttonStyles'; +import { TokensOverlap } from 'components/common/tokensOverlap'; +import { useTokens } from 'hooks/useTokens'; +import { Strategy, useGetStrategyList } from 'libs/queries'; +import { + PairTrade, + Trending, + useTrending, +} from 'libs/queries/extApi/tradeCount'; +import { Link } from 'libs/routing'; +import { Token } from 'libs/tokens'; +import { CSSProperties, useEffect, useRef } from 'react'; +import { toPairSlug } from 'utils/pairSearch'; + +const getTrendingPairs = ( + tokensMap: Map, + trending?: Trending +) => { + if (!trending) return { isLoading: true, data: [] }; + const pairs: Record = {}; + for (const trade of trending?.pairCount ?? []) { + pairs[trade.pairAddresses] ||= trade; + } + const list = Object.values(pairs) + .filter((pair) => !!pair.pairTrades_24h) + .sort((a, b) => b.pairTrades - a.pairTrades) + .splice(0, 3); + + // If there are less than 3, pick the remaining best + if (list.length < 3) { + const remaining = Object.values(pairs) + .filter((pair) => !!pair.pairTrades_24h) + .sort((a, b) => b.pairTrades - a.pairTrades) + .splice(0, 3 - list.length); + list.push(...remaining); + } + + // Sort again in case we had to add more + const data = list + .sort((a, b) => b.pairTrades - a.pairTrades) + .map((pair) => ({ + pairAddress: pair.pairAddresses, + base: tokensMap.get(pair.token0.toLowerCase())!, + quote: tokensMap.get(pair.token1.toLowerCase())!, + trades: pair.pairTrades, + })); + return { isLoading: false, data }; +}; + +interface StrategyWithTradeCount extends Strategy { + trades: number; +} +const useTrendStrategies = ( + trending?: Trending +): { isLoading: boolean; data: StrategyWithTradeCount[] } => { + const trades = trending?.tradeCount ?? []; + const list = trades + .filter((t) => !!t.strategyTrades_24h) + .sort((a, b) => b.strategyTrades - a.strategyTrades) + .splice(0, 3); + + // If there are less than 3, pick the remaining best + if (list.length < 3) { + const remaining = trades + .filter((t) => !!t.strategyTrades_24h) + .sort((a, b) => b.strategyTrades - a.strategyTrades) + .splice(0, 3 - list.length); + list.push(...remaining); + } + + const record: Record = {}; + for (const item of list) { + record[item.id] = item.strategyTrades; + } + const ids = list.map((s) => s.id); + const query = useGetStrategyList(ids); + if (query.isLoading) return { isLoading: true, data: [] }; + + const data = (query.data ?? []).map((strategy) => ({ + ...strategy, + trades: record[strategy.id], + })); + return { isLoading: false, data }; +}; + +export const ExplorerHeader = () => { + const { tokensMap } = useTokens(); + const { data: trending, isLoading, isError } = useTrending(); + const trendingStrategies = useTrendStrategies(trending); + const trendingPairs = getTrendingPairs(tokensMap, trending); + const strategies = trendingStrategies.data; + const pairs = trendingPairs.data; + + const strategiesLoading = trendingStrategies.isLoading || isLoading; + const pairLoading = trendingPairs.isLoading || isLoading; + if (isError) return; + return ( +
+
+

+ Total Trades +

+ +
+ + Create Strategy + + + Trade + +
+
+
+

Popular Pairs

+ + + + + + + + + {pairLoading && + [1, 2, 3].map((id) => ( + + + + + ))} + {pairs.map(({ pairAddress, base, quote, trades }) => ( + + + + + ))} + +
Token PairTrades
+ + + +
+ +
+ + {base?.symbol} + / + {quote?.symbol} +
+ +
+ + {formatter.format(trades)} + +
+
+
+

+ Trending Strategies +

+ + + + + + + + + {strategiesLoading && + [1, 2, 3].map((id) => ( + + + + + ))} + {strategies.map(({ id, idDisplay, base, quote, trades }) => ( + + + + + ))} + +
IDTrades
+ + + +
+ +
+ + {idDisplay} +
+ +
+ + {formatter.format(trades)} + +
+
+
+ ); +}; + +const Loading = (style: CSSProperties) => ( +
+
+
+); + +const formatter = new Intl.NumberFormat(undefined, { + maximumFractionDigits: 0, +}); + +interface TradesProps { + trades?: number; +} + +const Trades = ({ trades }: TradesProps) => { + const ref = useRef(null); + const anims = useRef[]>(null); + const lastTrades = useRef(0); + const initDelta = 60; + + useEffect(() => { + if (typeof trades !== 'number') return; + let tradesChanged = false; + const start = async () => { + const from = lastTrades.current || trades - initDelta; + const to = trades; + const letters = ref.current!.children; + // Initial animation + if (!lastTrades.current) { + const initAnims: Promise[] = []; + const next = formatter.format(from).split(''); + for (let i = 0; i < next.length; i++) { + const v = next[i]; + if (!'0123456789'.includes(v)) continue; + const anim = letters[i]?.animate( + [{ transform: `translateY(-${v}0%)` }], + { + duration: 1000, + delay: i * 100, + fill: 'forwards', + easing: 'cubic-bezier(1,-0.54,.65,1.46)', + } + ); + if (anim) initAnims.push(anim.finished); + } + await Promise.allSettled(initAnims); + } + // Wait for lingering animations if any + await Promise.allSettled(anims.current ?? []); + anims.current = []; + let previous = formatter.format(from - 1).split(''); + for (let value = from; value <= to; value++) { + const next = formatter.format(value).split(''); + for (let i = 0; i < next.length; i++) { + if (tradesChanged) return; + const v = next[i]; + if (!'0123456789'.includes(v)) continue; + if (previous[i] === next[i]) continue; + const anim = letters[i].animate( + [{ transform: `translateY(-${v}0%)` }], + { + duration: 1000, + delay: 2000, + fill: 'forwards', + easing: 'cubic-bezier(1,.11,.55,.79)', + } + ); + anims.current.push(anim.finished); + } + previous = next; + await Promise.allSettled(anims.current ?? []); + lastTrades.current = value; + } + }; + start(); + return () => { + tradesChanged = true; + }; + }, [trades]); + + if (typeof trades !== 'number') { + return ; + } + + const initial = trades ? formatter.format(trades - initDelta) : '0'; + return ( +

+ {initial.split('').map((v, i) => { + if (!'0123456789'.includes(v)) return {v}; + return ( + + 0 + 1 + 2 + 3 + 4 + 5 + 6 + 7 + 8 + 9 + + ); + })} +

+ ); +}; diff --git a/src/components/strategies/MyStrategiesHeader.module.css b/src/components/strategies/MyStrategiesHeader.module.css new file mode 100644 index 000000000..169ed9b42 --- /dev/null +++ b/src/components/strategies/MyStrategiesHeader.module.css @@ -0,0 +1,32 @@ +@media (prefers-reduced-motion: no-preference) { + .header [role="rowheader"], .header [role="cell"], .header a { + animation: slideup 200ms var(--delay, 0ms) var(--ease-out) backwards; + } + .header [role="row"]:nth-child(1) [role="rowheader"] { + --delay: 0ms; + } + .header [role="row"]:nth-child(1) [role="cell"] { + --delay: 50ms; + } + .header [role="row"]:nth-child(2) [role="rowheader"] { + --delay: 100ms; + } + .header [role="row"]:nth-child(2) [role="cell"] { + --delay: 150ms; + } + .header [role="row"]:nth-child(3) [role="rowheader"] { + --delay: 200ms; + } + .header [role="row"]:nth-child(3) [role="cell"] { + --delay: 250ms; + } + .header a { + --delay: 300ms; + } + @keyframes slideup { + from { + opacity: 0; + transform: translateY(20px); + } + } +} diff --git a/src/components/strategies/MyStrategiesHeader.tsx b/src/components/strategies/MyStrategiesHeader.tsx new file mode 100644 index 000000000..9a38d4306 --- /dev/null +++ b/src/components/strategies/MyStrategiesHeader.tsx @@ -0,0 +1,84 @@ +import { Link } from '@tanstack/react-router'; +import { buttonStyles } from 'components/common/button/buttonStyles'; +import { Tooltip } from 'components/common/tooltip/Tooltip'; +import { useFiatCurrency } from 'hooks/useFiatCurrency'; +import { useStrategyCtx } from 'hooks/useStrategies'; +import { SafeDecimal } from 'libs/safedecimal'; +import { useMemo } from 'react'; +import { carbonEvents } from 'services/events'; +import { cn, prettifyNumber } from 'utils/helpers'; +import style from './MyStrategiesHeader.module.css'; + +export const MyStrategiesHeader = () => { + const { strategies } = useStrategyCtx(); + const { selectedFiatCurrency: currentCurrency } = useFiatCurrency(); + + const netWorth = useMemo(() => { + const total = strategies.reduce((acc, strategy) => { + return acc.add(strategy.fiatBudget.total); + }, new SafeDecimal(0)); + return prettifyNumber(total, { currentCurrency }); + }, [strategies, currentCurrency]); + + const totalTrade = useMemo(() => { + const total = strategies.reduce((acc, strategy) => { + return acc.add(strategy.tradeCount); + }, new SafeDecimal(0)); + return prettifyNumber(total, { isInteger: true }); + }, [strategies]); + + const totalTrade24h = useMemo(() => { + const total = strategies.reduce((acc, strategy) => { + return acc.add(strategy.tradeCount24h); + }, new SafeDecimal(0)); + return prettifyNumber(total, { isInteger: true }); + }, [strategies]); + + return ( +
+
+
+

+ Net Worth + +

+

+ {netWorth} +

+
+
+

+ Total Trades +

+

+ {totalTrade} +

+
+
+

+ Trades (Last 24h) +

+

+ {totalTrade24h} +

+
+
+ carbonEvents.strategy.newStrategyCreateClick(undefined)} + > + Create Strategy + +
+ ); +}; diff --git a/src/components/strategies/common/utils.ts b/src/components/strategies/common/utils.ts index ec9614337..5e94802d8 100644 --- a/src/components/strategies/common/utils.ts +++ b/src/components/strategies/common/utils.ts @@ -4,6 +4,7 @@ import { Token } from 'libs/tokens'; import { formatNumber } from 'utils/helpers'; import { BaseOrder } from './types'; import { endOfDay, getUnixTime, startOfDay, subDays } from 'date-fns'; +import { StrategyType } from 'libs/routing'; type StrategyOrderInput = | { min: string; max: string } @@ -35,7 +36,7 @@ export const isDisposableStrategy = (strategy: Strategy) => { return false; }; -export const getStrategyType = (strategy: Strategy) => { +export const getStrategyType = (strategy: Strategy): StrategyType => { if (isOverlappingStrategy(strategy)) return 'overlapping'; if (isDisposableStrategy(strategy)) return 'disposable'; return 'recurring'; diff --git a/src/components/strategies/edit/EditStrategyForm.tsx b/src/components/strategies/edit/EditStrategyForm.tsx index 1d099b4ae..0a30d02f2 100644 --- a/src/components/strategies/edit/EditStrategyForm.tsx +++ b/src/components/strategies/edit/EditStrategyForm.tsx @@ -158,7 +158,7 @@ export const EditStrategyForm: FC = (props) => { console.log('tx hash', tx.hash); await tx.wait(); cache.invalidateQueries({ - queryKey: QueryKey.strategies(user), + queryKey: QueryKey.strategiesByUser(user), }); carbonEvents.strategyEdit.strategyEditPrices({ ...strategyEventData, diff --git a/src/components/strategies/overview/StrategyCreateFirst.tsx b/src/components/strategies/overview/StrategyCreateFirst.tsx index b807b2987..1afce494c 100644 --- a/src/components/strategies/overview/StrategyCreateFirst.tsx +++ b/src/components/strategies/overview/StrategyCreateFirst.tsx @@ -2,7 +2,7 @@ import { StrategyBlockCreate } from 'components/strategies/overview/strategyBloc export const StrategyCreateFirst = () => { return ( -
+
{ await tx.wait(); void cache.invalidateQueries({ - queryKey: QueryKey.strategies(user), + queryKey: QueryKey.strategiesByUser(user), }); console.log('tx confirmed'); successEventsCb?.(); diff --git a/src/components/strategies/usePauseStrategy.ts b/src/components/strategies/usePauseStrategy.ts index 0cbcc520e..9b22e17cd 100644 --- a/src/components/strategies/usePauseStrategy.ts +++ b/src/components/strategies/usePauseStrategy.ts @@ -52,7 +52,7 @@ export const usePauseStrategy = () => { await tx.wait(); void cache.invalidateQueries({ - queryKey: QueryKey.strategies(user), + queryKey: QueryKey.strategiesByUser(user), }); console.log('tx confirmed'); successEventsCb?.(); diff --git a/src/config/blast/common.ts b/src/config/blast/common.ts index d76fd25ea..f5ab8597e 100644 --- a/src/config/blast/common.ts +++ b/src/config/blast/common.ts @@ -130,5 +130,6 @@ export const commonConfig: AppConfig = { ui: { priceChart: 'tradingView', useGradientBranding: true, + tradeCount: false, }, }; diff --git a/src/config/celo/common.ts b/src/config/celo/common.ts index 32b9f8c1d..1bdb04ce7 100644 --- a/src/config/celo/common.ts +++ b/src/config/celo/common.ts @@ -141,5 +141,6 @@ export const commonConfig: AppConfig = { ui: { priceChart: 'native', useGradientBranding: true, + tradeCount: true, }, }; diff --git a/src/config/configSchema.ts b/src/config/configSchema.ts index 529778896..5177216be 100644 --- a/src/config/configSchema.ts +++ b/src/config/configSchema.ts @@ -107,5 +107,6 @@ export const AppConfigSchema = v.object({ ui: v.object({ priceChart: v.union([v.literal('native'), v.literal('tradingView')]), useGradientBranding: v.optional(v.boolean()), + tradeCount: v.optional(v.boolean()), }), }); diff --git a/src/config/ethereum/common.ts b/src/config/ethereum/common.ts index e2f2675c1..0d264d814 100644 --- a/src/config/ethereum/common.ts +++ b/src/config/ethereum/common.ts @@ -216,5 +216,6 @@ export const commonConfig: AppConfig = { ui: { priceChart: 'native', useGradientBranding: true, + tradeCount: false, }, }; diff --git a/src/config/sei/common.ts b/src/config/sei/common.ts index 340acfff7..befecdfee 100644 --- a/src/config/sei/common.ts +++ b/src/config/sei/common.ts @@ -117,5 +117,6 @@ export const commonConfig: AppConfig = { ui: { priceChart: 'native', useGradientBranding: true, + tradeCount: true, }, }; diff --git a/src/hooks/useStrategies.tsx b/src/hooks/useStrategies.tsx index 108924874..406607b19 100644 --- a/src/hooks/useStrategies.tsx +++ b/src/hooks/useStrategies.tsx @@ -111,21 +111,20 @@ export const useStrategiesWithFiat = ( const price = priceQueries[i].data?.[selectedFiatCurrency]; prices[address] = price; } + const tradeCountQuery = useTradeCount(); - const tradeCount: Record = {}; - for (const item of tradeCountQuery.data ?? []) { - tradeCount[item.strategyId] = item.tradeCount; - } const result = strategies.map((strategy) => { const basePrice = new SafeDecimal(prices[strategy.base.address] ?? 0); const quotePrice = new SafeDecimal(prices[strategy.quote.address] ?? 0); const base = basePrice.times(strategy.order1.balance); const quote = quotePrice.times(strategy.order0.balance); const total = base.plus(quote); + const trades = tradeCountQuery.data[strategy.id]; return { ...strategy, fiatBudget: { base, quote, total }, - tradeCount: tradeCount[strategy.id] ?? 0, + tradeCount: trades?.tradeCount ?? 0, + tradeCount24h: trades?.tradeCount24h ?? 0, }; }); return { diff --git a/src/libs/queries/extApi/tokenPrice.ts b/src/libs/queries/extApi/tokenPrice.ts index 1a5d976d6..99588e77e 100644 --- a/src/libs/queries/extApi/tokenPrice.ts +++ b/src/libs/queries/extApi/tokenPrice.ts @@ -37,7 +37,15 @@ export const useGetMultipleTokenPrices = (addresses: string[] = []) => { queries: addresses.map((address) => { return { queryKey: QueryKey.tokenPrice(address), - queryFn: () => carbonApi.getMarketRate(address, availableCurrencies), + queryFn: () => { + return carbonApi + .getMarketRate(address, availableCurrencies) + .catch((err) => { + // See comment above + console.error(err); + return {} as FiatPriceDict; + }); + }, enabled: !!address && availableCurrencies.length > 0, refetchInterval: FIVE_MIN_IN_MS, staleTime: FIVE_MIN_IN_MS, diff --git a/src/libs/queries/extApi/tradeCount.ts b/src/libs/queries/extApi/tradeCount.ts index 2c44ee63e..b02b34e3e 100644 --- a/src/libs/queries/extApi/tradeCount.ts +++ b/src/libs/queries/extApi/tradeCount.ts @@ -3,15 +3,57 @@ import { QueryKey } from 'libs/queries/queryKey'; import { ONE_HOUR_IN_MS } from 'utils/time'; import { carbonApi } from 'utils/carbonApi'; -export interface TradeCount { - strategyId: string; - tradeCount: number; +export interface StrategyTrade { + id: string; + strategyTrades: number; + strategyTrades_24h: number; + token0: string; + token1: string; + symbol0: string; + symbol1: string; + pairSymbol: string; + pairAddresses: string; +} +export interface PairTrade { + pairId: string; + pairTrades: number; + pairTrades_24h: number; + token0: string; + token1: string; + symbol0: string; + symbol1: string; + pairSymbol: string; + pairAddresses: string; } -export const useTradeCount = () => { +export interface Trending { + totalTradeCount: number; + tradeCount: StrategyTrade[]; + pairCount: PairTrade[]; +} + +export const useTrending = () => { return useQuery({ - queryKey: QueryKey.tradeCount(), - queryFn: carbonApi.getTradeCount, + queryKey: QueryKey.trending(), + queryFn: carbonApi.getTrending, staleTime: ONE_HOUR_IN_MS, + refetchInterval: 120_000, }); }; + +interface StrategyTradeCount { + tradeCount: number; + tradeCount24h: number; +} + +export const useTradeCount = () => { + const query = useTrending(); + const tradeCount: Record = {}; + for (const item of query.data?.tradeCount ?? []) { + tradeCount[item.id] = { + tradeCount: item.strategyTrades, + tradeCount24h: item.strategyTrades_24h, + }; + } + return { data: tradeCount, isPending: query.isPending }; +}; diff --git a/src/libs/queries/queryKey.ts b/src/libs/queries/queryKey.ts index 20fdcc647..a787a590e 100644 --- a/src/libs/queries/queryKey.ts +++ b/src/libs/queries/queryKey.ts @@ -39,10 +39,11 @@ export namespace QueryKey { 'token-price-history', params, ]; - export const tradeCount = () => [...extAPI, 'trade-count']; + export const trending = () => [...extAPI, 'trending']; export const strategy = (id: string) => [...sdk, 'strategy', id]; - export const strategies = (user?: string) => [ + export const strategyList = (ids: string[]) => [...sdk, 'strategy', ...ids]; + export const strategiesByUser = (user?: string) => [ ...sdk, 'strategies', 'user', diff --git a/src/libs/queries/sdk/strategy.ts b/src/libs/queries/sdk/strategy.ts index 4fbab5726..071ff90e3 100644 --- a/src/libs/queries/sdk/strategy.ts +++ b/src/libs/queries/sdk/strategy.ts @@ -49,6 +49,7 @@ export interface StrategyWithFiat extends Strategy { base: SafeDecimal; }; tradeCount: number; + tradeCount24h: number; } interface StrategiesHelperProps { @@ -163,7 +164,7 @@ export const useGetUserStrategies = ({ user }: Props) => { const isZeroAddress = address === config.addresses.tokens.ZERO; return useQuery({ - queryKey: QueryKey.strategies(address), + queryKey: QueryKey.strategiesByUser(address), queryFn: async () => { if (!address || !isValidAddress || isZeroAddress) return []; const strategies = await carbonSDK.getUserStrategies(address); @@ -180,6 +181,28 @@ export const useGetUserStrategies = ({ user }: Props) => { }); }; +export const useGetStrategyList = (ids: string[]) => { + const { tokens, getTokenById, importTokens } = useTokens(); + const { Token } = useContract(); + + return useQuery({ + queryKey: QueryKey.strategyList(ids), + queryFn: async () => { + const getStrategies = ids.map((id) => carbonSDK.getStrategy(id)); + const strategies = await Promise.all(getStrategies); + return buildStrategiesHelper({ + strategies, + getTokenById, + importTokens, + Token, + }); + }, + enabled: tokens.length > 0, + staleTime: ONE_DAY_IN_MS, + retry: false, + }); +}; + export const useGetStrategy = (id: string) => { const { tokens, getTokenById, importTokens } = useTokens(); const { Token } = useContract(); diff --git a/src/pages/explorer/index.tsx b/src/pages/explorer/index.tsx index 80cd9c631..9118f87fc 100644 --- a/src/pages/explorer/index.tsx +++ b/src/pages/explorer/index.tsx @@ -7,9 +7,11 @@ import { } from 'components/explorer'; import { StrategyProvider, useStrategyCtx } from 'hooks/useStrategies'; import { ExplorerTabs } from 'components/explorer/ExplorerTabs'; +import { ExplorerHeader } from 'components/explorer/ExplorerHeader'; import { useEffect, useState } from 'react'; import { explorerEvents } from 'services/events/explorerEvents'; import { lsService } from 'services/localeStorage'; +import config from 'config'; const url = '/explore/$type'; export const ExplorerPage = () => { @@ -33,6 +35,7 @@ export const ExplorerPage = () => { + {config.ui.tradeCount && }
{slug && } diff --git a/src/pages/strategies/index.tsx b/src/pages/strategies/index.tsx index c85c6aeaf..a267db39c 100644 --- a/src/pages/strategies/index.tsx +++ b/src/pages/strategies/index.tsx @@ -9,16 +9,14 @@ import { StrategySearch } from 'components/strategies/overview/StrategySearch'; import { useGetUserStrategies } from 'libs/queries'; import { Page } from 'components/common/page'; import { useMemo } from 'react'; -import { Outlet, useRouterState, useMatchRoute, Link } from 'libs/routing'; +import { Outlet, useRouterState, useMatchRoute } from 'libs/routing'; import { ReactComponent as IconPieChart } from 'assets/icons/piechart.svg'; import { ReactComponent as IconOverview } from 'assets/icons/overview.svg'; import { ReactComponent as IconActivity } from 'assets/icons/activity.svg'; import { StrategyProvider } from 'hooks/useStrategies'; -import { cn } from 'utils/helpers'; -import { carbonEvents } from 'services/events'; -import { buttonStyles } from 'components/common/button/buttonStyles'; import { StrategyFilterSort } from 'components/strategies/overview/StrategyFilterSort'; import { StrategySelectLayout } from 'components/strategies/StrategySelectLayout'; +import { MyStrategiesHeader } from 'components/strategies/MyStrategiesHeader'; export const StrategiesPage = () => { const { pathname } = useRouterState().location; @@ -54,11 +52,20 @@ export const StrategiesPage = () => { }, ]; + if (!user) { + return ( + + + + ); + } + return ( - {user && ( -
+
+ +
{showFilter && ( <> @@ -67,30 +74,17 @@ export const StrategiesPage = () => { )} - - carbonEvents.strategy.newStrategyCreateClick(undefined) - } - > - Create Strategy -
- )} - {/* Hidden tag to target in E2E */} - {query.isFetching && ( - - )} - {user ? : } + {/* Hidden tag to target in E2E */} + {query.isFetching && ( + + )} + +
); diff --git a/src/utils/carbonApi.ts b/src/utils/carbonApi.ts index e5ca2cbf9..e26ee46c1 100644 --- a/src/utils/carbonApi.ts +++ b/src/utils/carbonApi.ts @@ -13,7 +13,7 @@ import { ServerActivityMeta, } from 'libs/queries/extApi/activity'; import { lsService } from 'services/localeStorage'; -import { TradeCount } from 'libs/queries/extApi/tradeCount'; +import { Trending } from 'libs/queries/extApi/tradeCount'; // Only ETH is supported as network currency by the API const NETWORK_CURRENCY = @@ -93,7 +93,7 @@ const carbonApi = { getActivityMeta: async (params: QueryActivityParams) => { return get('activity/meta', params); }, - getTradeCount: () => get('analytics/trades_count'), + getTrending: () => get('analytics/trending'), }; export { carbonApi }; diff --git a/src/utils/helpers/number.test.ts b/src/utils/helpers/number.test.ts index dfab5d691..33761ac8a 100644 --- a/src/utils/helpers/number.test.ts +++ b/src/utils/helpers/number.test.ts @@ -235,7 +235,7 @@ describe('Test helpers', () => { expect(prettifyNumber(19999.999999999986138278)).toEqual('19,999.99'); }); - test('Check rounding is correct - math.round', () => { + test('Check rounding is correct - option.round', () => { expect( prettifyNumber(18999.999999999851769955, { round: true }) ).toEqual('19,000.00'); @@ -243,6 +243,12 @@ describe('Test helpers', () => { prettifyNumber(19999.999999999986138278, { round: true }) ).toEqual('20,000.00'); }); + test('Check trunc is correct - option.isInteger', () => { + expect( + prettifyNumber(18999.999999999851769955, { isInteger: true }) + ).toEqual('18,999'); + expect(prettifyNumber(0, { isInteger: true })).toEqual('0'); + }); test('Check rounding is correct - currentCurrency is USD', () => { expect( diff --git a/src/utils/helpers/number.ts b/src/utils/helpers/number.ts index 41434892c..5922470f5 100644 --- a/src/utils/helpers/number.ts +++ b/src/utils/helpers/number.ts @@ -111,6 +111,7 @@ interface PrettifyNumberOptions { highPrecision?: boolean; locale?: string; round?: boolean; + isInteger?: boolean; decimals?: number; noSubscript?: boolean; } @@ -125,6 +126,10 @@ const getIntlOptions = (value: SafeDecimal, options: PrettifyNumberOptions) => { // @ts-ignore: TS52072 roundingMode is not yet supported in TypeScript 5.2 intlOptions.roundingMode = 'halfExpand'; } + if (options.isInteger) { + // @ts-ignore: TS52072 roundingMode is not yet supported in TypeScript 5.2 + intlOptions.roundingMode = 'trunc'; + } // Currency if (options.currentCurrency) { @@ -155,14 +160,18 @@ export function prettifyNumber( // Force value to be positive if (num.lte(0)) { - intlOptions.minimumFractionDigits = Math.min(options.decimals ?? 2, 2); - intlOptions.maximumFractionDigits = Math.min(options.decimals ?? 2, 2); + const min = options.isInteger ? 0 : 2; + const max = options.isInteger ? 0 : 2; + intlOptions.minimumFractionDigits = Math.min(options.decimals ?? min, min); + intlOptions.maximumFractionDigits = Math.min(options.decimals ?? max, max); return Intl.NumberFormat(locale, intlOptions).format(0); } if (num.gte(1)) { - intlOptions.minimumFractionDigits = Math.min(options.decimals ?? 2, 2); - intlOptions.maximumFractionDigits = Math.max(options.decimals ?? 2, 2); + const min = options.isInteger ? 0 : 2; + const max = options.isInteger ? 0 : 2; + intlOptions.minimumFractionDigits = Math.min(options.decimals ?? min, min); + intlOptions.maximumFractionDigits = Math.max(options.decimals ?? max, max); } else if (num.gte(0.001)) { intlOptions.minimumFractionDigits = Math.min(options.decimals ?? 2, 2); intlOptions.maximumFractionDigits = Math.max(options.decimals ?? 6, 2);