From 79f1ee371b10f8e312d4f3cf735a18c769f0d8fa Mon Sep 17 00:00:00 2001 From: rassadin11 Date: Thu, 14 May 2026 15:50:22 +0300 Subject: [PATCH] 14.05.2026 rip --- src/app/providers/RouterProvider.tsx | 2 +- src/features/wallet/api/walletApi.ts | 36 ++++++++++ src/features/wallet/index.ts | 3 + src/features/wallet/model/useWalletData.ts | 20 ++++++ src/widgets/token-table/model/useTokenRows.ts | 69 +++++++++++++++++++ .../token-table/ui/TokenTable.module.css | 6 ++ src/widgets/token-table/ui/TokenTable.tsx | 11 +-- tsconfig.tsbuildinfo | 2 +- 8 files changed, 142 insertions(+), 7 deletions(-) create mode 100644 src/features/wallet/api/walletApi.ts create mode 100644 src/features/wallet/index.ts create mode 100644 src/features/wallet/model/useWalletData.ts create mode 100644 src/widgets/token-table/model/useTokenRows.ts diff --git a/src/app/providers/RouterProvider.tsx b/src/app/providers/RouterProvider.tsx index 79bd560..f69725f 100644 --- a/src/app/providers/RouterProvider.tsx +++ b/src/app/providers/RouterProvider.tsx @@ -26,9 +26,9 @@ export function RouterProvider() { } /> + } /> }> - } /> } /> } /> } /> diff --git a/src/features/wallet/api/walletApi.ts b/src/features/wallet/api/walletApi.ts new file mode 100644 index 0000000..74ec8a0 --- /dev/null +++ b/src/features/wallet/api/walletApi.ts @@ -0,0 +1,36 @@ +import { api } from '@shared/api/base' + +export type Chain = 'ETH' | 'BSC' | 'BTC' | 'TRX' | 'SOL' + +export interface FormattedAmount { + raw: string + formatted: string + decimals: number + usdPrice: number + usdValue: number +} + +export interface WalletBalanceData { + chain: Chain + address: string + native: FormattedAmount + tokens: Record +} + +export interface PriceEntry { + usd: number +} + +export const CHAINS: Chain[] = ['ETH', 'BSC', 'BTC', 'TRX', 'SOL'] + +export async function getWalletBalance(chain: Chain): Promise { + const res = await api.get<{ success: boolean; data: WalletBalanceData }>(`/api/wallets/${chain}/balance`) + return res.data +} + +export async function getPrices(symbols: string[]): Promise> { + const res = await api.get<{ success: boolean; data: Record }>( + `/api/prices?symbols=${symbols.join(',')}` + ) + return res.data +} diff --git a/src/features/wallet/index.ts b/src/features/wallet/index.ts new file mode 100644 index 0000000..2e351d0 --- /dev/null +++ b/src/features/wallet/index.ts @@ -0,0 +1,3 @@ +export { useAllWalletBalances, usePrices } from './model/useWalletData' +export type { Chain, FormattedAmount, WalletBalanceData, PriceEntry } from './api/walletApi' +export { CHAINS } from './api/walletApi' diff --git a/src/features/wallet/model/useWalletData.ts b/src/features/wallet/model/useWalletData.ts new file mode 100644 index 0000000..f773f8a --- /dev/null +++ b/src/features/wallet/model/useWalletData.ts @@ -0,0 +1,20 @@ +import { useQuery, useQueries } from '@tanstack/react-query' +import { getWalletBalance, getPrices, CHAINS } from '../api/walletApi' + +export function useAllWalletBalances() { + return useQueries({ + queries: CHAINS.map(chain => ({ + queryKey: ['wallet', 'balance', chain], + queryFn: () => getWalletBalance(chain), + staleTime: 30_000, + })), + }) +} + +export function usePrices(symbols: string[]) { + return useQuery({ + queryKey: ['wallet', 'prices', symbols.join(',')], + queryFn: () => getPrices(symbols), + staleTime: 5 * 60 * 1000, + }) +} diff --git a/src/widgets/token-table/model/useTokenRows.ts b/src/widgets/token-table/model/useTokenRows.ts new file mode 100644 index 0000000..536829e --- /dev/null +++ b/src/widgets/token-table/model/useTokenRows.ts @@ -0,0 +1,69 @@ +import { useAllWalletBalances, usePrices } from '@features/wallet' +import { CHAINS } from '@features/wallet' +import { TOKENS } from './tokens' +import type { WalletBalanceData } from '@features/wallet' + +const PRICE_SYMBOLS = ['BTC', 'ETH', 'SOL', 'TRX', 'ARB'] + +type ChainSource = + | { chain: 'BTC' | 'ETH' | 'SOL' | 'TRX'; type: 'native' } + | { chain: 'ETH'; type: 'token'; symbol: string } + +const CHAIN_MAP: Record = { + BTC: { chain: 'BTC', type: 'native' }, + ETH: { chain: 'ETH', type: 'native' }, + SOL: { chain: 'SOL', type: 'native' }, + TRX: { chain: 'TRX', type: 'native' }, + ARB: { chain: 'ETH', type: 'token', symbol: 'ARB' }, +} + +function formatUsd(value: number): string { + return `$${value.toLocaleString('en-US', { minimumFractionDigits: 2, maximumFractionDigits: 2 })}` +} + +function formatPrice(value: number): string { + if (value >= 1) { + return `$${value.toLocaleString('en-US', { minimumFractionDigits: 2, maximumFractionDigits: 2 })}` + } + return `$${value.toLocaleString('en-US', { minimumFractionDigits: 4, maximumFractionDigits: 6 })}` +} + +export function useTokenRows() { + const balanceResults = useAllWalletBalances() + const pricesQuery = usePrices(PRICE_SYMBOLS) + + const isLoading = balanceResults.some(r => r.isLoading) || pricesQuery.isLoading + + const balanceByChain: Record = {} + CHAINS.forEach((chain, i) => { + const data = balanceResults[i].data + if (data) balanceByChain[chain] = data + }) + + const rows = TOKENS.map(t => { + const src = CHAIN_MAP[t.ticker] + const chainData = src ? balanceByChain[src.chain] : undefined + + let bal = t.bal + let usd = t.usd + + if (chainData) { + const amount = + src.type === 'native' + ? chainData.native + : chainData.tokens[src.type === 'token' ? src.symbol : t.ticker] + + if (amount) { + bal = amount.formatted + usd = formatUsd(amount.usdValue) + } + } + + const priceEntry = pricesQuery.data?.[t.ticker] + const price = priceEntry ? formatPrice(priceEntry.usd) : t.price + + return { ...t, price, bal, usd } + }) + + return { rows, isLoading } +} diff --git a/src/widgets/token-table/ui/TokenTable.module.css b/src/widgets/token-table/ui/TokenTable.module.css index bd0f4b3..e25c0c4 100644 --- a/src/widgets/token-table/ui/TokenTable.module.css +++ b/src/widgets/token-table/ui/TokenTable.module.css @@ -3,6 +3,12 @@ border: 1px solid var(--glass-border); border-radius: 20px; overflow: hidden; + transition: opacity 0.2s; +} + +.loading { + opacity: 0.6; + pointer-events: none; } .table { diff --git a/src/widgets/token-table/ui/TokenTable.tsx b/src/widgets/token-table/ui/TokenTable.tsx index a9c21f3..8bde73d 100644 --- a/src/widgets/token-table/ui/TokenTable.tsx +++ b/src/widgets/token-table/ui/TokenTable.tsx @@ -1,9 +1,10 @@ import { useState } from 'react' -import { TOKENS } from '../model/tokens' +import { useTokenRows } from '../model/useTokenRows' import styles from './TokenTable.module.css' export function TokenTable() { - const [favs, setFavs] = useState(TOKENS.map((t) => t.fav)) + const { rows, isLoading } = useTokenRows() + const [favs, setFavs] = useState(() => rows.map((t) => t.fav)) function toggleFav(i: number) { setFavs((prev) => prev.map((v, idx) => (idx === i ? !v : v))) @@ -17,7 +18,7 @@ export function TokenTable() { return ( <> -
+
{/* Desktop table */} @@ -31,7 +32,7 @@ export function TokenTable() { - {TOKENS.map((t, i) => ( + {rows.map((t, i) => (