Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
157 changes: 122 additions & 35 deletions apps/web/src/features/pools/components/gm-pool-row.tsx
Original file line number Diff line number Diff line change
@@ -1,8 +1,11 @@
import { useEffect } from "react"
import { Skeleton } from "@workspace/ui/components/skeleton"
import { usePoolRowData } from "../hooks/use-pool-row-data"
import {
getComposition,
getEstimatedApy,
getFundingRatePerHourPct,
getOpenInterestUsd,
getPoolTvlUsd,
rawToDisplay,
} from "../lib/pool-math"
Expand All @@ -17,6 +20,63 @@ import { useWalletStore } from "@/features/wallet/store/wallet-store"
type GmPoolRowProps = {
market: PoolMarketConfig
variant: "desktop" | "mobile"
onMetricsChange?: (marketToken: string, metrics: PoolRowMetrics) => void
}

export type PoolRowMetrics = {
tvlUsd: number
openInterestUsd: number
apy: number | null
}

function formatCompactUsd(value: number) {
return formatUsd(value, { compact: true })
}

function ValueCell({
value,
title,
isLoading,
className = "",
}: {
value: string
title?: string
isLoading?: boolean
className?: string
}) {
if (isLoading) {
return <Skeleton className="ml-auto h-4 w-20 max-w-full" />
}

return (
<span
className={`block min-w-0 truncate tabular-nums ${className}`}
title={title ?? value}
>
{value}
</span>
)
}

function MobileStat({
label,
value,
title,
isLoading,
}: {
label: string
value: string
title?: string
isLoading?: boolean
}) {
return (
<div className="min-w-0">
<dt className="truncate text-muted-foreground">{label}</dt>
<dd className="mt-1 min-w-0 font-mono text-foreground">
<ValueCell value={value} title={title} isLoading={isLoading} />
</dd>
</div>
)
}

function PoolIdentity({ market }: { market: PoolMarketConfig }) {
Expand All @@ -34,27 +94,44 @@ function PoolIdentity({ market }: { market: PoolMarketConfig }) {
)
}

export function GmPoolRow({ market, variant }: GmPoolRowProps) {
export function GmPoolRow({ market, variant, onMetricsChange }: GmPoolRowProps) {
const address = useWalletStore((state) => state.address)
const isConnected = useWalletStore((state) => state.status === "connected")
const { data, isLoading } = usePoolRowData(market)
const poolValue = data?.poolValue
const composition = getComposition(poolValue)
const tvlUsd = getPoolTvlUsd(poolValue)
const apy = getEstimatedApy(poolValue)
const openInterestUsd =
rawToDisplay(data?.openInterest?.long) + rawToDisplay(data?.openInterest?.short)
const funding = rawToDisplay(data?.fundingInfo?.fundingFactorPerSecond)
const openInterestUsd = getOpenInterestUsd(data?.openInterest)
// Display funding as an hourly percentage, matching the trade page convention.
const funding = getFundingRatePerHourPct(data?.fundingInfo?.fundingFactorPerSecond)
const userGmBalance = data?.userGmBalance ?? 0n
const hasUserGm = userGmBalance > 0n
const hasFailures = (data?.failures.length ?? 0) > 0
const failureTitle = hasFailures ? `Unavailable reads: ${data?.failures.join(", ")}` : undefined
const tvlLabel = formatCompactUsd(tvlUsd)
const tvlTitle = formatUsd(tvlUsd)
const openInterestLabel = formatCompactUsd(openInterestUsd)
const openInterestTitle = formatUsd(openInterestUsd)
const fundingLabel = funding === 0 ? "—" : formatPct(funding, { decimals: 4 })
const apyLabel = apy == null ? "Est. pending" : `${formatPct(apy, { sign: false })} est.`
const userGmLabel = formatToken(rawToDisplay(userGmBalance), "GM", { decimals: 4 })
const userGmTitle = formatToken(Number(formatSorobanAmount(userGmBalance, 7, 7)), "GM", {
decimals: 7,
})

useEffect(() => {
onMetricsChange?.(market.marketToken, { tvlUsd, openInterestUsd, apy })
}, [apy, market.marketToken, onMetricsChange, openInterestUsd, tvlUsd])

if (variant === "mobile") {
return (
<article className="rounded-lg border border-border bg-card p-4">
<article className="min-w-0 rounded-lg border border-border bg-card p-4">
<div className="flex items-start justify-between gap-3">
<PoolIdentity market={market} />
{isLoading ? <Skeleton className="h-5 w-20" /> : <p className="text-sm">{formatUsd(tvlUsd)}</p>}
<p className="min-w-20 max-w-28 text-right font-mono text-sm">
<ValueCell value={tvlLabel} title={tvlTitle} isLoading={isLoading} />
</p>
</div>
<div className="mt-4">
<PoolCompositionBar
Expand All @@ -65,26 +142,27 @@ export function GmPoolRow({ market, variant }: GmPoolRowProps) {
/>
</div>
<dl className="mt-4 grid grid-cols-2 gap-3 text-[13px]">
<div>
<dt className="text-muted-foreground">Open interest</dt>
<dd className="mt-1 text-foreground">{formatUsd(openInterestUsd)}</dd>
</div>
<div>
<dt className="text-muted-foreground">APY</dt>
<dd className="mt-1 text-foreground">{apy == null ? "Est. pending" : `${formatPct(apy, { sign: false })} est.`}</dd>
</div>
<div>
<dt className="text-muted-foreground">Your GM</dt>
<dd className="mt-1 text-foreground">{formatToken(rawToDisplay(userGmBalance), "GM", { decimals: 4 })}</dd>
</div>
<div>
<dt className="text-muted-foreground">Funding</dt>
<dd className="mt-1 text-foreground">{funding === 0 ? "—" : formatPct(funding, { decimals: 4 })}</dd>
</div>
<MobileStat
label="Open interest"
value={openInterestLabel}
title={openInterestTitle}
isLoading={isLoading}
/>
<MobileStat label="APY" value={apyLabel} isLoading={isLoading} />
<MobileStat
label="Your GM"
value={userGmLabel}
title={userGmTitle}
isLoading={isLoading}
/>
<MobileStat label="Funding / hr" value={fundingLabel} isLoading={isLoading} />
</dl>
{hasFailures ? (
<p className="mt-3 text-[12px] text-yellow-600 dark:text-yellow-400">
Some live reads are unavailable.
<p
className="mt-3 inline-flex max-w-full items-center rounded border border-yellow-500/25 bg-yellow-500/10 px-2 py-1 text-[12px] text-yellow-700 dark:text-yellow-300"
title={failureTitle}
>
Partial data
</p>
) : null}
<div className="mt-4">
Expand All @@ -101,12 +179,12 @@ export function GmPoolRow({ market, variant }: GmPoolRowProps) {
}

return (
<tr className="border-b border-border last:border-0">
<tr className="border-b border-border last:border-0 odd:bg-muted/15 hover:bg-muted/30">
<td className="px-5 py-4">
<PoolIdentity market={market} />
</td>
<td className="px-5 py-4 text-right font-mono text-sm">
{isLoading ? <Skeleton className="ml-auto h-4 w-20" /> : formatUsd(tvlUsd)}
<td className="px-4 py-4 text-right font-mono text-sm">
<ValueCell value={tvlLabel} title={tvlTitle} isLoading={isLoading} />
</td>
<td className="px-5 py-4">
<PoolCompositionBar
Expand All @@ -116,15 +194,21 @@ export function GmPoolRow({ market, variant }: GmPoolRowProps) {
shortSymbol={market.shortSymbol}
/>
</td>
<td className="px-5 py-4 text-right font-mono text-sm">{formatUsd(openInterestUsd)}</td>
<td className="px-5 py-4 text-right font-mono text-sm">
{funding === 0 ? "—" : formatPct(funding, { decimals: 4 })}
<td className="px-4 py-4 text-right font-mono text-sm">
<ValueCell
value={openInterestLabel}
title={openInterestTitle}
isLoading={isLoading}
/>
</td>
<td className="px-4 py-4 text-right font-mono text-sm">
<ValueCell value={fundingLabel} isLoading={isLoading} />
</td>
<td className="px-5 py-4 text-right font-mono text-sm">
{apy == null ? "Est. pending" : `${formatPct(apy, { sign: false })} est.`}
<td className="px-4 py-4 text-right font-mono text-sm">
<ValueCell value={apyLabel} isLoading={isLoading} />
</td>
<td className="px-5 py-4 text-right font-mono text-sm">
{formatToken(Number(formatSorobanAmount(userGmBalance, 7, 4)), "GM", { decimals: 4 })}
<td className="px-4 py-4 text-right font-mono text-sm">
<ValueCell value={userGmLabel} title={userGmTitle} isLoading={isLoading} />
</td>
<td className="px-5 py-4 text-right">
<PoolActions
Expand All @@ -135,7 +219,10 @@ export function GmPoolRow({ market, variant }: GmPoolRowProps) {
userGmBalance={userGmBalance}
/>
{hasFailures ? (
<p className="mt-2 text-right text-[11px] text-yellow-600 dark:text-yellow-400">
<p
className="mt-2 text-right text-[11px] text-yellow-700 dark:text-yellow-300"
title={failureTitle}
>
Partial data
</p>
) : null}
Expand Down
Loading
Loading