14.05.2026 rip
This commit is contained in:
@@ -26,9 +26,9 @@ export function RouterProvider() {
|
||||
</Route>
|
||||
|
||||
<Route path={ROUTES.CONVERTER} element={<ConverterPage />} />
|
||||
<Route path={ROUTES.WALLET} element={<WalletPage />} />
|
||||
|
||||
<Route element={<ProtectedRoute />}>
|
||||
<Route path={ROUTES.WALLET} element={<WalletPage />} />
|
||||
<Route path={ROUTES.SWAP} element={<SwapPage />} />
|
||||
<Route path={ROUTES.PROFILE} element={<ProfilePage />} />
|
||||
<Route path={ROUTES.SEED_PHRASE} element={<SeedPhrasePage />} />
|
||||
|
||||
36
src/features/wallet/api/walletApi.ts
Normal file
36
src/features/wallet/api/walletApi.ts
Normal file
@@ -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<string, FormattedAmount>
|
||||
}
|
||||
|
||||
export interface PriceEntry {
|
||||
usd: number
|
||||
}
|
||||
|
||||
export const CHAINS: Chain[] = ['ETH', 'BSC', 'BTC', 'TRX', 'SOL']
|
||||
|
||||
export async function getWalletBalance(chain: Chain): Promise<WalletBalanceData> {
|
||||
const res = await api.get<{ success: boolean; data: WalletBalanceData }>(`/api/wallets/${chain}/balance`)
|
||||
return res.data
|
||||
}
|
||||
|
||||
export async function getPrices(symbols: string[]): Promise<Record<string, PriceEntry>> {
|
||||
const res = await api.get<{ success: boolean; data: Record<string, PriceEntry> }>(
|
||||
`/api/prices?symbols=${symbols.join(',')}`
|
||||
)
|
||||
return res.data
|
||||
}
|
||||
3
src/features/wallet/index.ts
Normal file
3
src/features/wallet/index.ts
Normal file
@@ -0,0 +1,3 @@
|
||||
export { useAllWalletBalances, usePrices } from './model/useWalletData'
|
||||
export type { Chain, FormattedAmount, WalletBalanceData, PriceEntry } from './api/walletApi'
|
||||
export { CHAINS } from './api/walletApi'
|
||||
20
src/features/wallet/model/useWalletData.ts
Normal file
20
src/features/wallet/model/useWalletData.ts
Normal file
@@ -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,
|
||||
})
|
||||
}
|
||||
69
src/widgets/token-table/model/useTokenRows.ts
Normal file
69
src/widgets/token-table/model/useTokenRows.ts
Normal file
@@ -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<string, ChainSource> = {
|
||||
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<string, WalletBalanceData> = {}
|
||||
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 }
|
||||
}
|
||||
@@ -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 {
|
||||
|
||||
@@ -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<boolean[]>(TOKENS.map((t) => t.fav))
|
||||
const { rows, isLoading } = useTokenRows()
|
||||
const [favs, setFavs] = useState<boolean[]>(() => 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 (
|
||||
<>
|
||||
<div className={styles.wrap}>
|
||||
<div className={`${styles.wrap} ${isLoading ? styles.loading : ''}`}>
|
||||
{/* Desktop table */}
|
||||
<table className={styles.table}>
|
||||
<thead>
|
||||
@@ -31,7 +32,7 @@ export function TokenTable() {
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{TOKENS.map((t, i) => (
|
||||
{rows.map((t, i) => (
|
||||
<tr key={t.ticker}>
|
||||
<td>
|
||||
<button
|
||||
@@ -81,7 +82,7 @@ export function TokenTable() {
|
||||
|
||||
{/* Mobile card list */}
|
||||
<div className={styles.mobileList}>
|
||||
{TOKENS.map((t, i) => (
|
||||
{rows.map((t, i) => (
|
||||
<div key={t.ticker} className={styles.card}>
|
||||
<button
|
||||
className={`${styles.star} ${favs[i] ? styles.starOn : ''}`}
|
||||
|
||||
Reference in New Issue
Block a user