From 68b77871b8958e6ba014d240df02fa3a6dde6147 Mon Sep 17 00:00:00 2001 From: fiqriBTI Date: Wed, 27 Aug 2025 14:47:11 +0700 Subject: [PATCH 1/5] feat: add tabs ui --- .../liquidity-pools/detail/[poolId]/page.tsx | 350 ++++++++++++------ .../liquidityPool/components/DataTable.tsx | 69 +++- src/features/liquidityPool/services.ts | 2 + 3 files changed, 298 insertions(+), 123 deletions(-) diff --git a/src/app/(walletConnected)/liquidity-pools/detail/[poolId]/page.tsx b/src/app/(walletConnected)/liquidity-pools/detail/[poolId]/page.tsx index 76c9563..5fc9430 100644 --- a/src/app/(walletConnected)/liquidity-pools/detail/[poolId]/page.tsx +++ b/src/app/(walletConnected)/liquidity-pools/detail/[poolId]/page.tsx @@ -1,6 +1,5 @@ 'use client' -import { Loader2 } from 'lucide-react' import Image from 'next/image' import Link from 'next/link' import { useRouter } from 'next/navigation' @@ -13,6 +12,7 @@ import { LuArrowUpDown } from 'react-icons/lu' import { CopyButton } from '@/components/common/CopyButton' import { Button, buttonVariants } from '@/components/ui/button' +import { Tabs, TabsContent, TabsList, TabsTrigger } from '@/components/ui/tabs' import { Tooltip, TooltipContent, TooltipTrigger } from '@/components/ui/tooltip' import { getFeeTierColor } from '@/features/liquidityPool/components/Columns' import PoolDetailSkeleton from '@/features/liquidityPool/components/PoolDetailSkeleton' @@ -127,7 +127,10 @@ export default function PoolDetail({ params }: { params: { poolId: string } }) { }) const transactionData = getTransactionsByPoolId.data ? getTransactionsByPoolId.data.data : [] - const transactionColumn = getTransactionListColumns(baseMint?.symbol ?? '', quoteMint?.symbol ?? '') + const transactionColumn = getTransactionListColumns( + baseMint?.symbol ?? '', + quoteMint?.symbol ?? '' + ) const mintAPoolAmount = useMemo(() => { if (!poolDetailData) return 0 @@ -167,8 +170,10 @@ export default function PoolDetail({ params }: { params: { poolId: string } }) { ? '$0' : `$${(isPoolData(poolDetailData) ? poolDetailData.day.volume : poolDetailData.volume24h).toFixed(3)}` - const mintAImage = baseMint?.logoURI && baseMint?.logoURI !== '' ? baseMint.logoURI : '/icon-placeholder.svg' - const mintBImage = quoteMint?.logoURI && quoteMint?.logoURI !== '' ? quoteMint.logoURI : '/icon-placeholder.svg' + const mintAImage = + baseMint?.logoURI && baseMint?.logoURI !== '' ? baseMint.logoURI : '/icon-placeholder.svg' + const mintBImage = + quoteMint?.logoURI && quoteMint?.logoURI !== '' ? quoteMint.logoURI : '/icon-placeholder.svg' useEffect(() => { if (getTransactionsByPoolId.isSuccess) { @@ -270,115 +275,238 @@ export default function PoolDetail({ params }: { params: { poolId: string } }) {
-
-
-

Pool balances

- - - - - -

- Total value of tokens currently held in this pool, displayed per token. -

-
-
-
- -
-
- -
- -
- -
- -
-
-

Links

-
-
-
- {`${baseMint?.name} { - e.currentTarget.src = '/icon-placeholder.svg' - }} - /> -

{baseMint?.symbol}

-
-
- - {shortenAddress(baseMint?.address ?? '', 7)} - - -
-
-
-
- {`${quoteMint?.name} { - e.currentTarget.src = '/icon-placeholder.svg' - }} - /> -

{quoteMint?.symbol}

+ + + Pool Stats + My Stats + + +
+
+

Pool balances

+ + + + + +

+ Total value of tokens currently held in this pool, displayed per token. +

+
+
-
- - {shortenAddress(quoteMint?.address ?? '', 7)} - - + +
+
+ +
+ +
+ +
+ +
+
+

+ Links +

+
+
+
+ {`${baseMint?.name} { + e.currentTarget.src = '/icon-placeholder.svg' + }} + /> +

{baseMint?.symbol}

+
+
+ + {shortenAddress(baseMint?.address ?? '', 7)} + + +
+
+
+
+ {`${quoteMint?.name} { + e.currentTarget.src = '/icon-placeholder.svg' + }} + /> +

{quoteMint?.symbol}

+
+
+ + {shortenAddress(quoteMint?.address ?? '', 7)} + + +
+
+
+
+
+ +
+
+

Pool balances

+ + + + + +

+ Total value of tokens currently held in this pool, displayed per token. +

+
+
-
-
-
+ +
+
+ +
+ +
+ +
+ +
+
+

+ Links +

+
+
+
+ {`${baseMint?.name} { + e.currentTarget.src = '/icon-placeholder.svg' + }} + /> +

{baseMint?.symbol}

+
+
+ + {shortenAddress(baseMint?.address ?? '', 7)} + + +
+
+
+
+ {`${quoteMint?.name} { + e.currentTarget.src = '/icon-placeholder.svg' + }} + /> +

{quoteMint?.symbol}

+
+
+ + {shortenAddress(quoteMint?.address ?? '', 7)} + + +
+
+
+
+ +

Transactions

diff --git a/src/features/liquidityPool/components/DataTable.tsx b/src/features/liquidityPool/components/DataTable.tsx index 14e3c80..23369b1 100644 --- a/src/features/liquidityPool/components/DataTable.tsx +++ b/src/features/liquidityPool/components/DataTable.tsx @@ -37,9 +37,22 @@ import { DropdownMenuLabel } from '@/components/ui/dropdown-menu' import { Input } from '@/components/ui/input' -import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from '@/components/ui/select' +import { + Select, + SelectContent, + SelectItem, + SelectTrigger, + SelectValue +} from '@/components/ui/select' import { Skeleton } from '@/components/ui/skeleton' -import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow } from '@/components/ui/table' +import { + Table, + TableBody, + TableCell, + TableHead, + TableHeader, + TableRow +} from '@/components/ui/table' import { Tooltip, TooltipContent, TooltipProvider, TooltipTrigger } from '@/components/ui/tooltip' import { useIsMobile } from '@/hooks/isMobile' import { useDebounce } from '@/hooks/useDebounce' @@ -78,7 +91,11 @@ interface AdvancedFilters { } // Enhanced global filter function -function enhancedGlobalFilterFn(row: any, columnId: string, filterValue: string) { +function enhancedGlobalFilterFn( + row: any, + columnId: string, + filterValue: string +) { const { mintA, mintB, id } = row.original as PoolListProps const search = filterValue.toLowerCase() @@ -175,7 +192,13 @@ function TableSkeleton() { ) } -function EmptyState({ hasFilters, onClearFilters }: { hasFilters: boolean; onClearFilters: () => void }) { +function EmptyState({ + hasFilters, + onClearFilters +}: { + hasFilters: boolean + onClearFilters: () => void +}) { return (
@@ -364,7 +387,8 @@ export function DataTable({ const filteredPools = table.getFilteredRowModel().rows.length const currentPage = table.getState().pagination.pageIndex + 1 const totalPages = table.getPageCount() - const startIndex = table.getState().pagination.pageIndex * table.getState().pagination.pageSize + 1 + const startIndex = + table.getState().pagination.pageIndex * table.getState().pagination.pageSize + 1 const endIndex = Math.min(startIndex + table.getState().pagination.pageSize - 1, filteredPools) if (isLoading) { @@ -395,7 +419,9 @@ export function DataTable({
setAdvancedFilters((prev) => ({ ...prev, type: value }))} + onValueChange={(value: FilterType) => + setAdvancedFilters((prev) => ({ ...prev, type: value })) + } > @@ -500,7 +528,12 @@ export function DataTable({ - @@ -728,7 +761,12 @@ export function DataTable({ )} -
@@ -739,7 +777,9 @@ export function DataTable({

- {filteredPools === totalPools ? `${totalPools} total pools` : `${filteredPools} of ${totalPools} pools`} + {filteredPools === totalPools + ? `${totalPools} total pools` + : `${filteredPools} of ${totalPools} pools`}

{enablePagination && filteredPools > 0 && (

@@ -784,11 +824,16 @@ export function DataTable({ {headerGroup.headers.map((header) => ( - {header.isPlaceholder ? null : flexRender(header.column.columnDef.header, header.getContext())} + {header.isPlaceholder + ? null + : flexRender(header.column.columnDef.header, header.getContext())} ))} Actions diff --git a/src/features/liquidityPool/services.ts b/src/features/liquidityPool/services.ts index 7233c11..46e8004 100644 --- a/src/features/liquidityPool/services.ts +++ b/src/features/liquidityPool/services.ts @@ -266,6 +266,8 @@ export const useGetPoolById = ({ poolId }: { poolId: string }) => { }) } +export const useGetUserPoolStatsById = () => {} + // Pool refresh mutation export const useRefreshPools = () => { const queryClient = useQueryClient() From e47dc1d94deba05cc29263a0594ae9df90edf861 Mon Sep 17 00:00:00 2001 From: fiqriBTI Date: Thu, 28 Aug 2025 13:47:57 +0700 Subject: [PATCH 2/5] feat: resolve tvl value by adding mint a and b price --- .../liquidity-pools/detail/[poolId]/page.tsx | 78 +++++- .../components/PoolDetailSkeleton.tsx | 82 ++++--- src/features/liquidityPool/onchain.ts | 230 +++++++++++------- src/features/liquidityPool/services.ts | 15 +- 4 files changed, 267 insertions(+), 138 deletions(-) diff --git a/src/app/(walletConnected)/liquidity-pools/detail/[poolId]/page.tsx b/src/app/(walletConnected)/liquidity-pools/detail/[poolId]/page.tsx index 5fc9430..c045487 100644 --- a/src/app/(walletConnected)/liquidity-pools/detail/[poolId]/page.tsx +++ b/src/app/(walletConnected)/liquidity-pools/detail/[poolId]/page.tsx @@ -12,10 +12,14 @@ import { LuArrowUpDown } from 'react-icons/lu' import { CopyButton } from '@/components/common/CopyButton' import { Button, buttonVariants } from '@/components/ui/button' +import { Skeleton } from '@/components/ui/skeleton' import { Tabs, TabsContent, TabsList, TabsTrigger } from '@/components/ui/tabs' import { Tooltip, TooltipContent, TooltipTrigger } from '@/components/ui/tooltip' import { getFeeTierColor } from '@/features/liquidityPool/components/Columns' -import PoolDetailSkeleton from '@/features/liquidityPool/components/PoolDetailSkeleton' +import { + InitialPoolDetailSkeleton, + PoolDetailTransactionSkeleton +} from '@/features/liquidityPool/components/PoolDetailSkeleton' import { getTransactionListColumns } from '@/features/liquidityPool/components/TransactionColumns' import { TransactionDataTable } from '@/features/liquidityPool/components/TransactionDataTable' import { useGetPoolById, useGetTransactionsByPoolId } from '@/features/liquidityPool/services' @@ -31,12 +35,14 @@ function PoolAmountBar({ mintAAmount, mintBAmount, mintASymbol, - mintBSymbol + mintBSymbol, + isLoading }: { mintAAmount: number mintBAmount: number mintASymbol: string mintBSymbol: string + isLoading?: boolean }) { const total = mintAAmount + mintBAmount const mintAPercentage = (mintAAmount / total) * 100 @@ -48,6 +54,18 @@ function PoolAmountBar({ return value.toFixed(3) } + if (isLoading) { + return ( +

+
+ + +
+ +
+ ) + } + return (
@@ -106,7 +124,9 @@ export default function PoolDetail({ params }: { params: { poolId: string } }) { const router = useRouter() const poolId = params.poolId - const [isReversed, setIsReversed] = useState(false) + const [isReversed, setIsReversed] = useState(false) + const [isMyStats, setIsMyStats] = useState(false) + const [isInitialLoad, setIsInitialLoad] = useState(true) const isMobile = useIsMobile() const onReverse = () => { @@ -123,7 +143,8 @@ export default function PoolDetail({ params }: { params: { poolId: string } }) { const getTransactionsByPoolId = useGetTransactionsByPoolId({ poolId, baseMint, - quoteMint + quoteMint, + isFilteredByOwner: isMyStats }) const transactionData = getTransactionsByPoolId.data ? getTransactionsByPoolId.data.data : [] @@ -187,7 +208,13 @@ export default function PoolDetail({ params }: { params: { poolId: string } }) { } }, [getTransactionsByPoolId.error, getTransactionsByPoolId.isError]) - if (getPoolById.isLoading || getTransactionsByPoolId.isLoading) + useEffect(() => { + if (!(getPoolById.isLoading || getTransactionsByPoolId.isLoading)) { + setIsInitialLoad(false) + } + }, [getPoolById.isLoading, getTransactionsByPoolId.isLoading]) + + if (isInitialLoad && (getPoolById.isLoading || getTransactionsByPoolId.isLoading)) return (
- +
) @@ -275,10 +302,33 @@ export default function PoolDetail({ params }: { params: { poolId: string } }) {
- - - Pool Stats - My Stats + setIsMyStats(value === 'my-stats')} + > + + + Pool Stats + + + My Stats +
@@ -306,6 +356,7 @@ export default function PoolDetail({ params }: { params: { poolId: string } }) { mintBAmount={mintBPoolAmount} mintASymbol={baseMint?.symbol ?? ''} mintBSymbol={quoteMint?.symbol ?? ''} + isLoading={getPoolById.isLoading} />
@@ -419,6 +470,7 @@ export default function PoolDetail({ params }: { params: { poolId: string } }) { mintBAmount={mintBPoolAmount} mintASymbol={baseMint?.symbol ?? ''} mintBSymbol={quoteMint?.symbol ?? ''} + isLoading={getPoolById.isLoading} />
@@ -510,7 +562,11 @@ export default function PoolDetail({ params }: { params: { poolId: string } }) {

Transactions

- + {getTransactionsByPoolId.isLoading ? ( + + ) : ( + + )}
) diff --git a/src/features/liquidityPool/components/PoolDetailSkeleton.tsx b/src/features/liquidityPool/components/PoolDetailSkeleton.tsx index ec9ef6f..c2d7ebb 100644 --- a/src/features/liquidityPool/components/PoolDetailSkeleton.tsx +++ b/src/features/liquidityPool/components/PoolDetailSkeleton.tsx @@ -1,68 +1,76 @@ import { Skeleton } from '@/components/ui/skeleton' -export default function PoolDetailSkeleton() { +export function PoolDetailTransactionSkeleton() { + return ( +
+ {/* Table header */} +
+ {['Time', 'Type', 'USD', 'USDC', 'Wallet'].map((col) => ( + + ))} +
+ {/* Table rows */} + {[...Array(8)].map((_, row) => ( +
+ {[...Array(5)].map((_, col) => ( + + ))} +
+ ))} +
+ ) +} + +export function InitialPoolDetailSkeleton() { return (
{/* Header */}
-
- - - - +
+ + + +
-
- - +
+ +
- {/* Pool stats card */} -
-
+
+
- +
+ + +
+
-
{[...Array(4)].map((_, i) => (
- +
))}
{/* Links */} -
+
{[...Array(2)].map((_, i) => (
- - - + + +
))}
{/* Transactions table */} -
- -
- {/* Table header */} -
- {['Time', 'Type', 'USD', 'USDC', 'Wallet'].map((col) => ( - - ))} -
- {/* Table rows */} - {[...Array(8)].map((_, row) => ( -
- {[...Array(5)].map((_, col) => ( - - ))} -
- ))} -
+
+ +
) diff --git a/src/features/liquidityPool/onchain.ts b/src/features/liquidityPool/onchain.ts index 6fec617..77d73f2 100644 --- a/src/features/liquidityPool/onchain.ts +++ b/src/features/liquidityPool/onchain.ts @@ -1,10 +1,19 @@ import { struct, u8, blob } from '@bbachain/buffer-layout' -import { getAccount, getMint } from '@bbachain/spl-token' +import { + ASSOCIATED_TOKEN_PROGRAM_ID, + getAccount, + getMint, + TOKEN_PROGRAM_ID +} from '@bbachain/spl-token' import { PROGRAM_ID as TOKEN_SWAP_PROGRAM_ID } from '@bbachain/spl-token-swap' import { Connection, PublicKey } from '@bbachain/web3.js' +import axios from 'axios' +import ENDPOINTS from '@/constants/endpoint' import { getTokenByAddress, generateTokenDisplayName } from '@/staticData/tokens' +import { getCoinGeckoId } from '../swap/utils' + import { MintInfo } from './types' // Layout for parsing TokenSwap account data @@ -16,92 +25,97 @@ const uint64 = (property: string = 'uint64') => { return blob(8, property) } - interface RawTokenSwap { - version: number - isInitialized: boolean - bumpSeed: number - poolTokenProgramId: Uint8Array - tokenAccountA: Uint8Array - tokenAccountB: Uint8Array - tokenPool: Uint8Array - mintA: Uint8Array - mintB: Uint8Array - feeAccount: Uint8Array - tradeFeeNumerator: Uint8Array - tradeFeeDenominator: Uint8Array - ownerTradeFeeNumerator: Uint8Array - ownerTradeFeeDenominator: Uint8Array - ownerWithdrawFeeNumerator: Uint8Array - ownerWithdrawFeeDenominator: Uint8Array - hostFeeNumerator: Uint8Array - hostFeeDenominator: Uint8Array - curveType: number - curveParameters: Uint8Array - } +interface RawTokenSwap { + version: number + isInitialized: boolean + bumpSeed: number + poolTokenProgramId: Uint8Array + tokenAccountA: Uint8Array + tokenAccountB: Uint8Array + tokenPool: Uint8Array + mintA: Uint8Array + mintB: Uint8Array + feeAccount: Uint8Array + tradeFeeNumerator: Uint8Array + tradeFeeDenominator: Uint8Array + ownerTradeFeeNumerator: Uint8Array + ownerTradeFeeDenominator: Uint8Array + ownerWithdrawFeeNumerator: Uint8Array + ownerWithdrawFeeDenominator: Uint8Array + hostFeeNumerator: Uint8Array + hostFeeDenominator: Uint8Array + curveType: number + curveParameters: Uint8Array +} - export const TokenSwapLayout = struct([ - u8('version'), - u8('isInitialized'), - u8('bumpSeed'), - publicKey('poolTokenProgramId'), - publicKey('tokenAccountA'), - publicKey('tokenAccountB'), - publicKey('tokenPool'), - publicKey('mintA'), - publicKey('mintB'), - publicKey('feeAccount'), - uint64('tradeFeeNumerator'), - uint64('tradeFeeDenominator'), - uint64('ownerTradeFeeNumerator'), - uint64('ownerTradeFeeDenominator'), - uint64('ownerWithdrawFeeNumerator'), - uint64('ownerWithdrawFeeDenominator'), - uint64('hostFeeNumerator'), - uint64('hostFeeDenominator'), - u8('curveType'), - blob(32, 'curveParameters') - ]) - - export interface OnchainPoolData { - address: string - programId: string - swapData: RawTokenSwap - mintA: MintInfo - mintB: MintInfo - tokenAccountA: string - tokenAccountB: string - reserveA: bigint - reserveB: bigint - feeRate: number - tvl: number - volume24h: number - fees24h: number - apr24h: number - } +export const TokenSwapLayout = struct([ + u8('version'), + u8('isInitialized'), + u8('bumpSeed'), + publicKey('poolTokenProgramId'), + publicKey('tokenAccountA'), + publicKey('tokenAccountB'), + publicKey('tokenPool'), + publicKey('mintA'), + publicKey('mintB'), + publicKey('feeAccount'), + uint64('tradeFeeNumerator'), + uint64('tradeFeeDenominator'), + uint64('ownerTradeFeeNumerator'), + uint64('ownerTradeFeeDenominator'), + uint64('ownerWithdrawFeeNumerator'), + uint64('ownerWithdrawFeeDenominator'), + uint64('hostFeeNumerator'), + uint64('hostFeeDenominator'), + u8('curveType'), + blob(32, 'curveParameters') +]) + +export interface OnchainPoolData { + address: string + programId: string + swapData: RawTokenSwap + mintA: MintInfo + mintB: MintInfo + tokenAccountA: string + tokenAccountB: string + reserveA: bigint + reserveB: bigint + feeRate: number + tvl: number + volume24h: number + fees24h: number + apr24h: number +} - /** - * Fetch all pool accounts from the Token Swap Program - */ - export async function getPoolAccounts(connection: Connection): Promise> { - try { - const accounts = await connection.getProgramAccounts(TOKEN_SWAP_PROGRAM_ID, { - filters: [ - { - dataSize: TokenSwapLayout.span // Only get accounts with correct data size - } - ] - }) - return accounts - } catch (error) { - console.error('Error fetching pool accounts:', error) - throw new Error('Failed to fetch pool accounts from onchain') - } +/** + * Fetch all pool accounts from the Token Swap Program + */ +export async function getPoolAccounts( + connection: Connection +): Promise> { + try { + const accounts = await connection.getProgramAccounts(TOKEN_SWAP_PROGRAM_ID, { + filters: [ + { + dataSize: TokenSwapLayout.span // Only get accounts with correct data size + } + ] + }) + return accounts + } catch (error) { + console.error('Error fetching pool accounts:', error) + throw new Error('Failed to fetch pool accounts from onchain') } +} /** * Parse raw pool account data into structured format */ -export function parsePoolData(pubkey: PublicKey, accountData: Buffer): RawTokenSwap & { address: string } { +export function parsePoolData( + pubkey: PublicKey, + accountData: Buffer +): RawTokenSwap & { address: string } { try { const data = new Uint8Array(accountData.buffer, accountData.byteOffset, accountData.byteLength) const swapData = TokenSwapLayout.decode(data) @@ -119,7 +133,10 @@ export function parsePoolData(pubkey: PublicKey, accountData: Buffer): RawTokenS /** * Get token mint information for a given mint address */ -export async function getTokenMintInfo(connection: Connection, mintAddress: PublicKey): Promise { +export async function getTokenMintInfo( + connection: Connection, + mintAddress: PublicKey +): Promise { try { const addressStr = mintAddress.toBase58() @@ -165,7 +182,10 @@ export async function getTokenMintInfo(connection: Connection, mintAddress: Publ /** * Get token account balance for reserve calculation */ -export async function getTokenAccountBalance(connection: Connection, tokenAccount: PublicKey): Promise { +export async function getTokenAccountBalance( + connection: Connection, + tokenAccount: PublicKey +): Promise { try { const account = await getAccount(connection, tokenAccount) return account.amount @@ -206,8 +226,8 @@ export function calculateTVL( reserveB: bigint, mintA: MintInfo, mintB: MintInfo, - priceA: number = 1, - priceB: number = 1 + priceA: number, + priceB: number ): number { const balanceA = Number(reserveA) / Math.pow(10, mintA.decimals) const balanceB = Number(reserveB) / Math.pow(10, mintB.decimals) @@ -240,6 +260,18 @@ export function calculatePoolMetrics( } } +async function getCoinGeckoTokenPrice(id: string | undefined) { + if (!id || id === '') return 1 + const res = await axios.get(ENDPOINTS.COIN_GECKO.GET_SIMPLE_PRICE, { + params: { + ids: id, + vs_currencies: 'usd' + } + }) + const usdRate = res.data[id].usd + return usdRate +} + /** * Process a single pool account into OnchainPoolData */ @@ -264,8 +296,14 @@ export async function processPoolAccount( const tokenAccountBKey = new PublicKey(parsedData.tokenAccountB) // Convert fee numerator/denominator from Uint8Array to bigint - const tradeFeeNumerator = new DataView(parsedData.tradeFeeNumerator.buffer).getBigUint64(0, true) - const tradeFeeDenominator = new DataView(parsedData.tradeFeeDenominator.buffer).getBigUint64(0, true) + const tradeFeeNumerator = new DataView(parsedData.tradeFeeNumerator.buffer).getBigUint64( + 0, + true + ) + const tradeFeeDenominator = new DataView(parsedData.tradeFeeDenominator.buffer).getBigUint64( + 0, + true + ) // Get mint information for both tokens const [mintA, mintB] = await Promise.all([ @@ -279,9 +317,11 @@ export async function processPoolAccount( getTokenAccountBalance(connection, tokenAccountBKey) ]) + const mintAPrice = await getCoinGeckoTokenPrice(getCoinGeckoId(mintA.address)) + const mintBPrice = await getCoinGeckoTokenPrice(getCoinGeckoId(mintB.address)) // Calculate metrics const feeRate = calculateFeeRate(tradeFeeNumerator, tradeFeeDenominator) - const tvl = calculateTVL(reserveA, reserveB, mintA, mintB) + const tvl = calculateTVL(reserveA, reserveB, mintA, mintB, mintAPrice, mintBPrice) const metrics = calculatePoolMetrics(tvl, feeRate) return { @@ -336,3 +376,19 @@ export async function getAllPoolsFromOnchain(connection: Connection): Promise { export const useGetTransactionsByPoolId = ({ poolId, baseMint, - quoteMint + quoteMint, + isFilteredByOwner }: { poolId: string baseMint: MintInfo | undefined quoteMint: MintInfo | undefined + isFilteredByOwner?: boolean }) => { + const { publicKey: ownerAddress } = useWallet() + const isValidParams = !!poolId?.trim() && !!baseMint?.address?.trim() && !!quoteMint?.address?.trim() @@ -374,7 +378,8 @@ export const useGetTransactionsByPoolId = ({ SERVICES_KEY.POOL.GET_TRANSACTIONS_BY_POOL_ID, poolId, baseMint?.address, - quoteMint?.address + quoteMint?.address, + isFilteredByOwner ? ownerAddress?.toBase58() : null ], enabled: isValidParams && areTokenPricesReady, ...CACHE_CONFIG, @@ -390,7 +395,7 @@ export const useGetTransactionsByPoolId = ({ console.log('Transaction count:', transactionData?.length) - const responseData = transactionData + let responseData = transactionData .map((tx) => { const parsedData = processTransactionData(tx, baseMint, quoteMint) if (!parsedData) return null @@ -406,6 +411,10 @@ export const useGetTransactionsByPoolId = ({ }) .filter((item): item is TransactionListProps => item !== null) + if (isFilteredByOwner && ownerAddress) { + responseData = responseData.filter((item) => item.wallet === ownerAddress.toBase58()) + } + return { message: 'Successfully get transaction data', data: responseData } } catch (error) { console.error('Failed to fetch transactions:', error) From c0cff029d06975a34427c3139cbf1c70216b0f95 Mon Sep 17 00:00:00 2001 From: fiqriBTI Date: Fri, 29 Aug 2025 15:25:20 +0700 Subject: [PATCH 3/5] feat: get user stats in pool detail service --- .../liquidity-pools/detail/[poolId]/page.tsx | 105 +++++------------- src/constants/service.ts | 1 + src/features/liquidityPool/onchain.ts | 89 +++++++++++---- src/features/liquidityPool/services.ts | 62 ++++++++++- 4 files changed, 156 insertions(+), 101 deletions(-) diff --git a/src/app/(walletConnected)/liquidity-pools/detail/[poolId]/page.tsx b/src/app/(walletConnected)/liquidity-pools/detail/[poolId]/page.tsx index c045487..b4f2ee0 100644 --- a/src/app/(walletConnected)/liquidity-pools/detail/[poolId]/page.tsx +++ b/src/app/(walletConnected)/liquidity-pools/detail/[poolId]/page.tsx @@ -22,7 +22,11 @@ import { } from '@/features/liquidityPool/components/PoolDetailSkeleton' import { getTransactionListColumns } from '@/features/liquidityPool/components/TransactionColumns' import { TransactionDataTable } from '@/features/liquidityPool/components/TransactionDataTable' -import { useGetPoolById, useGetTransactionsByPoolId } from '@/features/liquidityPool/services' +import { + useGetPoolById, + useGetTransactionsByPoolId, + useGetUserPoolStatsById +} from '@/features/liquidityPool/services' import { PoolData } from '@/features/liquidityPool/types' import { useIsMobile } from '@/hooks/isMobile' import { cn, getExplorerAddress, shortenAddress } from '@/lib/utils' @@ -179,6 +183,13 @@ export default function PoolDetail({ params }: { params: { poolId: string } }) { return Number(reserve) / Math.pow(10, decimals) }, [poolDetailData, isReversed]) + const getUserStats = useGetUserPoolStatsById({ + poolId, + reserveA: mintAPoolAmount, + reserveB: mintBPoolAmount, + isUserStats: isMyStats + }) + const apr7Day = !poolDetailData ? '0.00%' : `${(isPoolData(poolDetailData) ? poolDetailData.week.apr : poolDetailData.apr24h).toFixed(2)}%` @@ -214,7 +225,10 @@ export default function PoolDetail({ params }: { params: { poolId: string } }) { } }, [getPoolById.isLoading, getTransactionsByPoolId.isLoading]) - if (isInitialLoad && (getPoolById.isLoading || getTransactionsByPoolId.isLoading)) + if ( + isInitialLoad && + (getPoolById.isLoading || getTransactionsByPoolId.isLoading || getUserStats.isLoading) + ) return (



-
-

- Links -

-
-
-
- {`${baseMint?.name} { - e.currentTarget.src = '/icon-placeholder.svg' - }} - /> -

{baseMint?.symbol}

-
-
- - {shortenAddress(baseMint?.address ?? '', 7)} - - -
-
-
-
- {`${quoteMint?.name} { - e.currentTarget.src = '/icon-placeholder.svg' - }} - /> -

{quoteMint?.symbol}

-
-
- - {shortenAddress(quoteMint?.address ?? '', 7)} - - -
-
-
-
diff --git a/src/constants/service.ts b/src/constants/service.ts index ea9ea9e..65736a4 100644 --- a/src/constants/service.ts +++ b/src/constants/service.ts @@ -44,6 +44,7 @@ const POOL_SERVICE_KEY = { GET_POOL_BY_ID: 'get-pool-by-id', GET_POOL_BY_MINT: 'get-pool-by-mint', GET_POOL_STATS: 'get-pool-stats', + GET_USER_POOL_STATS: 'get-user-pool-stats', GET_TRANSACTIONS_BY_POOL_ID: 'get-transations-by-pool-id', DEPOSIT_LIQUIDITY: 'deposit-liquidity' } as const diff --git a/src/features/liquidityPool/onchain.ts b/src/features/liquidityPool/onchain.ts index 77d73f2..852a174 100644 --- a/src/features/liquidityPool/onchain.ts +++ b/src/features/liquidityPool/onchain.ts @@ -2,14 +2,16 @@ import { struct, u8, blob } from '@bbachain/buffer-layout' import { ASSOCIATED_TOKEN_PROGRAM_ID, getAccount, + getAssociatedTokenAddress, getMint, TOKEN_PROGRAM_ID } from '@bbachain/spl-token' -import { PROGRAM_ID as TOKEN_SWAP_PROGRAM_ID } from '@bbachain/spl-token-swap' +import { PROGRAM_ID as TOKEN_SWAP_PROGRAM_ID, TokenSwap } from '@bbachain/spl-token-swap' import { Connection, PublicKey } from '@bbachain/web3.js' import axios from 'axios' import ENDPOINTS from '@/constants/endpoint' +import { getTokenAccounts } from '@/lib/tokenAccount' import { getTokenByAddress, generateTokenDisplayName } from '@/staticData/tokens' import { getCoinGeckoId } from '../swap/utils' @@ -260,16 +262,44 @@ export function calculatePoolMetrics( } } -async function getCoinGeckoTokenPrice(id: string | undefined) { +const cache = new Map() + +const STALE_TIME = 60000 // 1 minute +const REFETCH_INTERVAL = 300000 // 5 minutes + +async function getCoinGeckoTokenPrice(id: string | undefined): Promise { if (!id || id === '') return 1 - const res = await axios.get(ENDPOINTS.COIN_GECKO.GET_SIMPLE_PRICE, { - params: { - ids: id, - vs_currencies: 'usd' + + const now = Date.now() + const cached = cache.get(id) + + // If cached and not stale + if (cached && now - cached.timestamp < STALE_TIME) { + return cached.price + } + + try { + const res = await axios.get(ENDPOINTS.COIN_GECKO.GET_SIMPLE_PRICE, { + params: { + ids: id, + vs_currencies: 'usd' + } + }) + + const usdRate = res.data[id]?.usd ?? 1 + + // Update cache + cache.set(id, { price: usdRate, timestamp: now }) + + return usdRate + } catch (error) { + // If fetch fails and we have a recent cached value, return it + if (cached && now - cached.timestamp < REFETCH_INTERVAL) { + return cached.price } - }) - const usdRate = res.data[id].usd - return usdRate + // Otherwise fallback to default + return 1 + } } /** @@ -377,18 +407,29 @@ export async function getAllPoolsFromOnchain(connection: Connection): Promise { + const tokenSwapState = await TokenSwap.fromAccountAddress(connection, lpMintAddress) + const mintInfo = await getMint(connection, tokenSwapState.poolMint) + const decimals = mintInfo.decimals + const rawSupply = mintInfo.supply + const totalSupply = Number(rawSupply) / Math.pow(10, decimals) + return totalSupply +} + +export const getUserLPTokens = async ( + connection: Connection, + lpMintAddress: PublicKey, + userPublicKey: PublicKey +): Promise => { + const tokenSwapState = await TokenSwap.fromAccountAddress(connection, lpMintAddress) + const userTokenAccountAddress = await getAssociatedTokenAddress( + tokenSwapState.poolMint, + userPublicKey + ) + const tokenAccountInfo = await getAccount(connection, userTokenAccountAddress) + const mintInfo = await getMint(connection, tokenSwapState.poolMint) + const decimals = mintInfo.decimals + const rawAmount = tokenAccountInfo.amount + const userBalance = Number(rawAmount) / Math.pow(10, decimals) + return userBalance +} diff --git a/src/features/liquidityPool/services.ts b/src/features/liquidityPool/services.ts index ef69cf4..8ff58cd 100644 --- a/src/features/liquidityPool/services.ts +++ b/src/features/liquidityPool/services.ts @@ -36,9 +36,16 @@ import { import { useGetCoinGeckoTokenPrice } from '../swap/services' import { getCoinGeckoId } from '../swap/utils' +import { getLPTokenData } from '../tokens/utils' import { TransactionListProps } from './components/TransactionColumns' -import { getAllPoolsFromOnchain, OnchainPoolData } from './onchain' +import { + getAllPoolsFromOnchain, + getTotalLPSupply, + getUserLPTokens, + OnchainPoolData, + parsePoolData +} from './onchain' import { MintInfo, PoolData, @@ -279,7 +286,58 @@ export const useGetPoolById = ({ poolId }: { poolId: string }) => { }) } -export const useGetUserPoolStatsById = () => {} +export const useGetUserPoolStatsById = ({ + poolId, + reserveA, + reserveB, + isUserStats +}: { + poolId: string + reserveA: number + reserveB: number + isUserStats: boolean +}) => { + const { connection } = useConnection() + const { publicKey: ownerAddress } = useWallet() + + return useQuery({ + enabled: + Boolean(poolId) && + reserveA > 0 && + reserveB > 0 && + isUserStats === true && + Boolean(ownerAddress), + queryKey: [ + SERVICES_KEY.POOL.GET_USER_POOL_STATS, + poolId, + ownerAddress?.toBase58(), + reserveA, + reserveB + ], + queryFn: async () => { + if (!ownerAddress) { + throw new Error('Wallet not connected. Please connect your wallet to create a pool.') + } + + const lpMint = new PublicKey(poolId) + const userLPToken = await getUserLPTokens(connection, lpMint, ownerAddress) + const lpTokenSupply = await getTotalLPSupply(connection, lpMint) + + const userShare = userLPToken / lpTokenSupply + const userMintAReserve = userShare * reserveA + const userMintBReserve = userShare * reserveB + const userReserveTotal = userMintAReserve + userMintBReserve + + return { + userShare: userShare * 100, + userLPToken, + userMintAReserve, + userMintBReserve, + userReserveTotal + } + } + }) +} // Pool refresh mutation export const useRefreshPools = () => { From 0e9c8847f21b00ddc112f3e7c8eebada5a955eb0 Mon Sep 17 00:00:00 2001 From: fiqriBTI Date: Fri, 29 Aug 2025 23:13:17 +0700 Subject: [PATCH 4/5] feat: add fee earnings on user stats and improve ui responsiveness --- .../liquidity-pools/detail/[poolId]/page.tsx | 52 +++++++++++++------ src/features/liquidityPool/services.ts | 34 ++++++++++-- 2 files changed, 67 insertions(+), 19 deletions(-) diff --git a/src/app/(walletConnected)/liquidity-pools/detail/[poolId]/page.tsx b/src/app/(walletConnected)/liquidity-pools/detail/[poolId]/page.tsx index b4f2ee0..d88eb68 100644 --- a/src/app/(walletConnected)/liquidity-pools/detail/[poolId]/page.tsx +++ b/src/app/(walletConnected)/liquidity-pools/detail/[poolId]/page.tsx @@ -93,12 +93,14 @@ function StatsItem({ title, info, content, - className + className, + isLoading }: { title: string info: string content: string className?: string + isLoading?: boolean }) { return (
@@ -119,7 +121,11 @@ function StatsItem({ -

{content}

+ {isLoading ? ( + + ) : ( +

{content}

+ )}
) } @@ -187,6 +193,14 @@ export default function PoolDetail({ params }: { params: { poolId: string } }) { poolId, reserveA: mintAPoolAmount, reserveB: mintBPoolAmount, + mintA: baseMint, + mintB: quoteMint, + feeRate: poolDetailData?.feeRate ?? 0, + volume24h: !poolDetailData + ? 0 + : isPoolData(poolDetailData) + ? poolDetailData.day.volume + : poolDetailData.volume24h, isUserStats: isMyStats }) @@ -244,7 +258,7 @@ export default function PoolDetail({ params }: { params: { poolId: string } }) { ) return ( -
+
-
+
-
+
-
+
-
+
-
+
-
+
-
+
-
+
diff --git a/src/features/liquidityPool/services.ts b/src/features/liquidityPool/services.ts index 8ff58cd..2c0ee28 100644 --- a/src/features/liquidityPool/services.ts +++ b/src/features/liquidityPool/services.ts @@ -34,7 +34,7 @@ import { getWBBAMintAddress } from '@/staticData/tokens' -import { useGetCoinGeckoTokenPrice } from '../swap/services' +import { useGetCoinGeckoTokenPrice, useGetUserBalanceByMint } from '../swap/services' import { getCoinGeckoId } from '../swap/utils' import { getLPTokenData } from '../tokens/utils' @@ -290,15 +290,35 @@ export const useGetUserPoolStatsById = ({ poolId, reserveA, reserveB, + mintA, + mintB, + volume24h, + feeRate, isUserStats }: { poolId: string reserveA: number reserveB: number + mintA: MintInfo | undefined + mintB: MintInfo | undefined + volume24h: number + feeRate: number isUserStats: boolean }) => { const { connection } = useConnection() const { publicKey: ownerAddress } = useWallet() + const baseUSDValue = useGetCoinGeckoTokenPrice({ + coinGeckoId: mintA ? getCoinGeckoId(mintA.address) : '' + }) + + const quoteUSDValue = useGetCoinGeckoTokenPrice({ + coinGeckoId: mintB ? getCoinGeckoId(mintB.address) : '' + }) + + const baseInitialPrice = baseUSDValue.data ?? 0 + const quoteInitialPrice = quoteUSDValue.data ?? 0 + + const areTokenPricesReady = baseUSDValue.isSuccess && quoteUSDValue.isSuccess return useQuery({ enabled: @@ -306,7 +326,8 @@ export const useGetUserPoolStatsById = ({ reserveA > 0 && reserveB > 0 && isUserStats === true && - Boolean(ownerAddress), + Boolean(ownerAddress) && + areTokenPricesReady, queryKey: [ SERVICES_KEY.POOL.GET_USER_POOL_STATS, poolId, @@ -326,14 +347,19 @@ export const useGetUserPoolStatsById = ({ const userShare = userLPToken / lpTokenSupply const userMintAReserve = userShare * reserveA const userMintBReserve = userShare * reserveB - const userReserveTotal = userMintAReserve + userMintBReserve + const userReserveTotal = + userMintAReserve * baseInitialPrice + userMintBReserve * quoteInitialPrice + + const totalDailyFees = volume24h * feeRate + const dailyFeeEarnings = totalDailyFees * userShare return { userShare: userShare * 100, userLPToken, userMintAReserve, userMintBReserve, - userReserveTotal + userReserveTotal, + dailyFeeEarnings } } }) From c0e12ef65e85b30f517b37314ba6d5d0fc93ee23 Mon Sep 17 00:00:00 2001 From: fiqriBTI Date: Sat, 30 Aug 2025 17:47:41 +0700 Subject: [PATCH 5/5] feat: integrate get user stats service to deposit page --- .../liquidity-pools/deposit/[poolId]/page.tsx | 112 ++++++++++++++---- 1 file changed, 87 insertions(+), 25 deletions(-) diff --git a/src/app/(walletConnected)/liquidity-pools/deposit/[poolId]/page.tsx b/src/app/(walletConnected)/liquidity-pools/deposit/[poolId]/page.tsx index a8d3831..d8c9bc6 100644 --- a/src/app/(walletConnected)/liquidity-pools/deposit/[poolId]/page.tsx +++ b/src/app/(walletConnected)/liquidity-pools/deposit/[poolId]/page.tsx @@ -13,7 +13,11 @@ import { Button } from '@/components/ui/button' import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card' import LPSlippageDialog from '@/features/liquidityPool/components/LPSlippageDialog' import LPSuccessDialog from '@/features/liquidityPool/components/LPSuccessDialog' -import { useDepositToPool, useGetPoolById } from '@/features/liquidityPool/services' +import { + useDepositToPool, + useGetPoolById, + useGetUserPoolStatsById +} from '@/features/liquidityPool/services' import { PoolData } from '@/features/liquidityPool/types' import SwapItem from '@/features/swap/components/SwapItem' import { useGetUserBalanceByMint, useGetCoinGeckoTokenPrice } from '@/features/swap/services' @@ -92,14 +96,22 @@ export default function LiquidityPoolDeposit({ params }: { params: { poolId: str totalValue: string } | null>(null) - const getMintABalance = useGetUserBalanceByMint({ mintAddress: poolDetailData?.mintA.address ?? '' }) - const getMintBBalance = useGetUserBalanceByMint({ mintAddress: poolDetailData?.mintB.address ?? '' }) + const getMintABalance = useGetUserBalanceByMint({ + mintAddress: poolDetailData?.mintA.address ?? '' + }) + const getMintBBalance = useGetUserBalanceByMint({ + mintAddress: poolDetailData?.mintB.address ?? '' + }) // Get token prices from CoinGecko const getMintATokenPrice = useGetCoinGeckoTokenPrice({ - coinGeckoId: poolDetailData?.mintA.address ? getCoinGeckoId(poolDetailData.mintA.address) : undefined + coinGeckoId: poolDetailData?.mintA.address + ? getCoinGeckoId(poolDetailData.mintA.address) + : undefined }) const getMintBTokenPrice = useGetCoinGeckoTokenPrice({ - coinGeckoId: poolDetailData?.mintB.address ? getCoinGeckoId(poolDetailData.mintB.address) : undefined + coinGeckoId: poolDetailData?.mintB.address + ? getCoinGeckoId(poolDetailData.mintB.address) + : undefined }) // Convert balance from daltons to UI units using token decimals @@ -122,6 +134,33 @@ export default function LiquidityPoolDeposit({ params }: { params: { poolId: str const depositMutation = useDepositToPool() + const mintAPoolAmount = poolDetailData + ? isPoolData(poolDetailData) + ? poolDetailData.mintAmountA + : Number(poolDetailData.reserveA) / Math.pow(10, poolDetailData.mintA.decimals) + : 0 + + const mintBPoolAmount = poolDetailData + ? isPoolData(poolDetailData) + ? poolDetailData.mintAmountB + : Number(poolDetailData.reserveB) / Math.pow(10, poolDetailData.mintB.decimals) + : 0 + + const getUserStats = useGetUserPoolStatsById({ + poolId, + reserveA: mintAPoolAmount, + reserveB: mintBPoolAmount, + mintA: poolDetailData?.mintA, + mintB: poolDetailData?.mintB, + feeRate: poolDetailData?.feeRate ?? 0, + volume24h: !poolDetailData + ? 0 + : isPoolData(poolDetailData) + ? poolDetailData.day.volume + : poolDetailData.volume24h, + isUserStats: true + }) + // Auto-calculate optimal ratio useEffect(() => { if (!poolDetailData || isCalculating) return @@ -131,8 +170,12 @@ export default function LiquidityPoolDeposit({ params }: { params: { poolId: str let reserveA: bigint, reserveB: bigint if (isPoolData(poolDetailData)) { - reserveA = BigInt(Math.floor(poolDetailData.mintAmountA * Math.pow(10, poolDetailData.mintA.decimals))) - reserveB = BigInt(Math.floor(poolDetailData.mintAmountB * Math.pow(10, poolDetailData.mintB.decimals))) + reserveA = BigInt( + Math.floor(poolDetailData.mintAmountA * Math.pow(10, poolDetailData.mintA.decimals)) + ) + reserveB = BigInt( + Math.floor(poolDetailData.mintAmountB * Math.pow(10, poolDetailData.mintB.decimals)) + ) } else { reserveA = pool.reserveA || BigInt(0) reserveB = pool.reserveB || BigInt(0) @@ -207,8 +250,12 @@ export default function LiquidityPoolDeposit({ params }: { params: { poolId: str mintB: poolDetailData.mintB, tokenAccountA: '', tokenAccountB: '', - reserveA: BigInt(Math.floor(poolDetailData.mintAmountA * Math.pow(10, poolDetailData.mintA.decimals))), - reserveB: BigInt(Math.floor(poolDetailData.mintAmountB * Math.pow(10, poolDetailData.mintB.decimals))), + reserveA: BigInt( + Math.floor(poolDetailData.mintAmountA * Math.pow(10, poolDetailData.mintA.decimals)) + ), + reserveB: BigInt( + Math.floor(poolDetailData.mintAmountB * Math.pow(10, poolDetailData.mintB.decimals)) + ), feeRate: poolDetailData.feeRate, tvl: poolDetailData.tvl, volume24h: poolDetailData.day?.volume ?? 0, @@ -252,7 +299,9 @@ export default function LiquidityPoolDeposit({ params }: { params: { poolId: str