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
25 changes: 23 additions & 2 deletions app/components/trade/DepositTrigger.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import { useWalletCompat } from "@/hooks/useWalletCompat";
import { useUserAccount } from "@/hooks/useUserAccount";
import { useSlabState } from "@/components/providers/SlabProvider";
import { useTokenMeta } from "@/hooks/useTokenMeta";
import { useWalletAtaBalance } from "@/hooks/useWalletAtaBalance";
import { formatTokenAmount } from "@/lib/format";
import { DepositWithdrawCard } from "./DepositWithdrawCard";
import { isMockMode } from "@/lib/mock-mode";
Expand All @@ -31,6 +32,23 @@ export const DepositTrigger: FC<{ slabAddress: string }> = ({ slabAddress }) =>
const capital = userAccount?.account.capital ?? 0n;
const prevCapitalRef = useRef(capital);

// The bar at the top of the trade panel now shows the user's PHANTOM
// (or whichever wallet) USDC balance — what they have available to
// deposit — NOT what's already deposited into this market. Account
// balance (capital) is shown alongside the order-form Size row as
// "Account Bal:" instead.
//
// Pass `capital` as the refresh trigger so a deposit (which decreases
// wallet ATA balance and increases capital) re-fetches the wallet
// balance. Without this the bar would keep showing the pre-deposit
// number until the user reloaded the page.
const { balance: walletBalance, decimals: walletDecimals } = useWalletAtaBalance(
config?.collateralMint ?? null,
capital,
);
const displayBalance = walletBalance ?? 0n;
const displayDecimals = walletDecimals ?? decimals;

// Read localStorage on mount
useEffect(() => {
const stored = localStorage.getItem(lsKey(slabAddress));
Expand Down Expand Up @@ -65,9 +83,12 @@ export const DepositTrigger: FC<{ slabAddress: string }> = ({ slabAddress }) =>
onClick={() => setExpanded(!expanded)}
>
<div className="flex items-center gap-2">
<span className="text-[9px] uppercase tracking-[0.15em] text-[var(--text-dim)]">Account</span>
{/* Label and value share the same brightness so the whole
"Wallet Balance: X USDC" line reads as one unit. The
earlier "Account" label was at --text-dim, which made
users squint to read which balance this was. */}
<span className="text-[11px] font-medium text-[var(--text)]" style={{ fontFamily: "var(--font-mono)", fontVariantNumeric: "tabular-nums" }}>
{formatTokenAmount(capital, decimals)} {symbol}
Wallet Balance: {formatTokenAmount(displayBalance, displayDecimals)} {symbol}
</span>
</div>
<div className="flex items-center gap-1.5">
Expand Down
2 changes: 1 addition & 1 deletion app/components/trade/TradeForm.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -704,7 +704,7 @@ export const TradeForm: FC<{ slabAddress: string }> = ({ slabAddress }) => {
<div className="mb-1.5 flex items-center justify-between">
<label className="text-[10px] uppercase tracking-[0.15em] text-[var(--text)]">Size<InfoIcon tooltip="Position size — enter in contracts (tokens) or USD. Both fields sync automatically." /></label>
<span className="text-[10px] text-[var(--text)] whitespace-nowrap min-w-0 shrink-0" style={{ fontFamily: "var(--font-mono)", fontVariantNumeric: "tabular-nums" }}>
Bal: {userAccount ? formatPerc(capital, decimals) : (walletAtaBalance !== null ? formatPerc(walletAtaBalance, decimals) : "—")} {collateralSymbol}
Account Bal: {userAccount ? formatPerc(capital, decimals) : "0"} {collateralSymbol}
</span>
</div>
<div className="grid grid-cols-2 gap-1.5">
Expand Down
76 changes: 76 additions & 0 deletions app/hooks/useWalletAtaBalance.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,76 @@
"use client";

import { useState, useEffect } from "react";
import type { PublicKey } from "@solana/web3.js";
import { getAssociatedTokenAddressSync } from "@solana/spl-token";
import { useWalletCompat, useConnectionCompat } from "@/hooks/useWalletCompat";

export interface WalletAtaBalance {
/** Raw atomic balance from the user's ATA, or null if no ATA / not connected. */
balance: bigint | null;
/** On-chain decimals from the ATA, or null if unavailable. Useful for
* tokens where TokenMetadata fails (cross-network, missing). */
decimals: number | null;
}

/** Fetches the user's associated token account balance for a given mint.
* Returns `{ balance: null }` when the wallet is disconnected, the mint
* is null, or the ATA doesn't exist yet.
*
* Re-fetch trigger: pass any value as `refreshTrigger` whose identity
* changes when an external event invalidates the cached balance.
* The canonical use is the in-market `capital` value — a deposit
* decreases the wallet ATA balance and increases capital; passing
* capital as the trigger forces a re-read of the ATA so the bar
* reflects the post-deposit state instead of the stale pre-deposit
* number. Without this, only publicKey/mint/connection changes would
* trigger a re-read, none of which fire after a deposit/withdraw. */
export function useWalletAtaBalance(
mint: PublicKey | null | undefined,
refreshTrigger?: unknown,
): WalletAtaBalance {
const { publicKey } = useWalletCompat();
const { connection } = useConnectionCompat();
const [state, setState] = useState<WalletAtaBalance>({
balance: null,
decimals: null,
});

useEffect(() => {
if (!publicKey || !mint) {
setState({ balance: null, decimals: null });
return;
}
let cancelled = false;
(async () => {
try {
const ata = getAssociatedTokenAddressSync(mint, publicKey);
const info = await connection.getTokenAccountBalance(ata);
if (cancelled) return;
// RPC returns amount as a string. The happy-path zero balance
// is "0" (truthy in JS). The else branch only fires for the
// unusual empty-string / falsy case from a malformed response;
// even then we want to preserve any decimals the RPC did
// hand back rather than silently drop them.
const onChainDecimals =
info.value.decimals !== undefined ? info.value.decimals : null;
if (info.value.amount) {
setState({
balance: BigInt(info.value.amount),
decimals: onChainDecimals,
});
} else {
setState({ balance: null, decimals: onChainDecimals });
}
} catch {
// ATA may not exist yet (user hasn't received this token), keep null.
if (!cancelled) setState({ balance: null, decimals: null });
}
})();
return () => {
cancelled = true;
};
}, [publicKey, mint, connection, refreshTrigger]);

return state;
}
Loading