From 78788a774f8eef15b0230416a0fe1fa28f9515f0 Mon Sep 17 00:00:00 2001 From: Kevin Codex Date: Fri, 11 Sep 2026 12:27:07 +0800 Subject: [PATCH 1/2] market cap in dollars everywhere; the launch form takes the starting cap in dollars The list, strip, chart and dashboard already led with a dollar market cap, but the launch form did not: an ETH launch picked "1 / 5 / 10 / 25 ETH" while every other quote picked "$5K / $10K / $25K / $100K", the custom cap was in quote units, and "Opens at" led with the quote figure. One rule now, in lib/launchpad/market-cap.ts: market cap is a dollar figure, the quote figure is the detail, and a quote with no USD price shows its own figure marked "no USD price" instead of a dash. Form: the same four dollar targets for every priced quote ($25K default, about the old 10 ETH), a dollar custom input, dollars first on "Opens at", the buy preview and the preview card, with the quote figure after. The tick is still computed from the quote-denominated cap, so the dollar figure shown is the one the chosen tick produces (within one 2% tick step). Without a price the form falls back to quote units as before. A preset pick no longer carries across quotes. Rows, strip, dashboard and the mobile buy bar use the one display helper; the row's small line shows the quote figure (or the "no USD price" mark). API unchanged. Tests for the entry mode, presets, pick carry-over, the tick round-trip and every label. --- app/src/app/t/[chain]/[token]/page.tsx | 5 +- app/src/components/launchpad/LaunchForm.tsx | 33 ++++++---- app/src/components/launchpad/LaunchRow.tsx | 9 +-- app/src/components/launchpad/MeDashboard.tsx | 5 +- .../components/launchpad/TrendingStrip.tsx | 3 +- app/src/lib/launchpad/market-cap.test.ts | 65 +++++++++++++++++++ app/src/lib/launchpad/market-cap.ts | 63 ++++++++++++++++++ 7 files changed, 159 insertions(+), 24 deletions(-) create mode 100644 app/src/lib/launchpad/market-cap.test.ts create mode 100644 app/src/lib/launchpad/market-cap.ts diff --git a/app/src/app/t/[chain]/[token]/page.tsx b/app/src/app/t/[chain]/[token]/page.tsx index cc973142..0bcf7ec0 100644 --- a/app/src/app/t/[chain]/[token]/page.tsx +++ b/app/src/app/t/[chain]/[token]/page.tsx @@ -32,6 +32,7 @@ import { jsonLdHtml, tokenCanonical } from "@/lib/seo"; import { stockByAddress } from "@/lib/launchpad/stocksServer"; import { BRAND_DOMAIN, BRAND_X } from "@/lib/brand"; import { clampSocial } from "@/lib/launchpad/ogcard"; +import { capDisplay } from "@/lib/launchpad/market-cap"; export const dynamic = "force-dynamic"; @@ -71,9 +72,7 @@ export default async function TokenPage({ params }: { params: Promise<{ chain: s const mode = feeModeOf(l.lp_fee, l.recipients); const poolKey = { currency0: l.quote as Address, currency1: l.token as Address, fee: l.lp_fee, tickSpacing: TICK_SPACING, hooks: NATIVE as Address }; const priceUsd = l.price_usd; - const fdvUsd = l.fdv_usd; const chainLabel = CHAIN_LABELS[chain]; - const fdvQuoteLabel = fmtQuote(BigInt(Math.round(l.fdv_quote * 10 ** quote.decimals)), quote.decimals, quote.symbol); const shareText = `${l.name} ($${l.symbol}) on ${chainLabel}. ${l.lp_fee === 0 ? "0% fee" : mode === "burn" ? "fees burned" : "no platform fee"}, liquidity locked forever`; const supplyLabel = fmtCompact(Number(BigInt(l.supply)) / 1e18, 0); @@ -171,7 +170,7 @@ export default async function TokenPage({ params }: { params: Promise<{ chain: s - + ); } diff --git a/app/src/components/launchpad/LaunchForm.tsx b/app/src/components/launchpad/LaunchForm.tsx index ef366d22..da054856 100644 --- a/app/src/components/launchpad/LaunchForm.tsx +++ b/app/src/components/launchpad/LaunchForm.tsx @@ -12,12 +12,12 @@ import GitlawbBadge from "./GitlawbBadge"; import { toast } from "./TxToasts"; import { btn, card, helper, input, label } from "@/components/ui"; import { ERC20_MIN_ABI, ERC20_TRANSFER_EVENT, LAUNCH_FACTORY_ABI, PERMIT2_ABI, UNIVERSAL_ROUTER_ABI, V4_QUOTER_ABI } from "@/lib/launchpad/abi"; -import { BPS, DEFAULT_SUPPLY, FEE_PRESETS, MCAP_PRESETS, TICK_SPACING, launchpad, quoteUsdOf, type Quote } from "@/lib/launchpad/config"; +import { BPS, DEFAULT_SUPPLY, FEE_PRESETS, TICK_SPACING, launchpad, quoteUsdOf, type Quote } from "@/lib/launchpad/config"; +import { capChipLabel, capDisplay, capEntry, capPick, capPresets, capToQuote } from "@/lib/launchpad/market-cap"; import { fdvForStartTick, fmtCompact, fmtQuoteUnits, fmtUsd, initialBuyPreview, minOut, startTickForFdv, tickToTokensPerQuote, units } from "@/lib/launchpad/math"; import { BUY_PRESETS, defaultFirstBuy, suggestFirstBuy } from "@/lib/launchpad/first-buy"; import { encodeV4ExactInSingle, type PoolKey } from "@/lib/launchpad/swap"; -import { stockMcapPresets } from "@/lib/launchpad/stocks"; -import { GITLAWB_SITE, gitlawbMcapPresets } from "@/lib/launchpad/gitlawb"; +import { GITLAWB_SITE } from "@/lib/launchpad/gitlawb"; import { CHAINS, CHAIN_LABELS, CHAIN_KEYS, BUILDER_DATA_SUFFIX, explorerTx, shortAddr, type ChainKey } from "@/lib/chainPublic"; import { friendlyError } from "@/lib/errors"; import { Spinner } from "@/components/Skeleton"; @@ -203,12 +203,18 @@ export default function LaunchForm({ ethUsd, gitlawbUsd = null, initialChain = " const metaKeyRef = useRef(null); const [phase, setPhase] = useState({ k: "idle" }); - const presets = quote.key === "stock" ? stockMcapPresets(quote.usd ?? 0) : quote.key === "gitlawb" ? gitlawbMcapPresets(quote.usd) : MCAP_PRESETS[quote.key]; - const mcap = customMcap.trim() ? Number(customMcap) : (mcapPick ?? presets[2] ?? 0); + // Starting cap: entered in dollars whenever the quote has a USD price, else in quote units (lib/launchpad/market-cap.ts). + // The tick is always computed from the quote-denominated cap; every figure shown leads with dollars. + const entry = capEntry(quoteUsd); + const presets = capPresets(entry, quote.key); + const pickedPreset = capPick(mcapPick, presets); + const mcapEntered = customMcap.trim() ? Number(customMcap) : (pickedPreset ?? 0); + const mcap = mcapEntered > 0 && Number.isFinite(mcapEntered) ? capToQuote(mcapEntered, entry) : 0; const startTick = mcap > 0 && Number.isFinite(mcap) ? startTickForFdv(mcap, quote.decimals) : null; const fdvPreview = startTick !== null ? fdvForStartTick(startTick, quote.decimals) : null; const tokensPerEth = startTick !== null ? tickToTokensPerQuote(startTick, quote.decimals) : null; - const fmtMcap = (v: number) => (quote.decimals <= 6 ? `${v.toLocaleString("en-US", { maximumFractionDigits: 0 })} ${quote.symbol}` : quote.key === "stock" ? `${v.toFixed(3)} ${quote.symbol}` : `${fmtQuoteUnits(v, quote.decimals)} ${quote.symbol}`); + const cap = (v: number) => capDisplay(v, quoteUsd, quote); + const fmtMcap = (v: number) => cap(v).main; const onChain = chainId === CHAIN.id; const symbolClean = symbol.trim().toUpperCase(); @@ -358,7 +364,6 @@ export default function LaunchForm({ ethUsd, gitlawbUsd = null, initialChain = " } } - const previewMcapUsd = fdvPreview !== null && quoteUsd ? fmtUsd(fdvPreview * quoteUsd, { compact: true }) : null; return (
@@ -581,7 +586,7 @@ export default function LaunchForm({ ethUsd, gitlawbUsd = null, initialChain = "
{presets.map((v) => { - const active = !customMcap.trim() && (mcapPick ?? presets[2] ?? presets[0]) === v; + const active = !customMcap.trim() && pickedPreset === v; return ( ); })} @@ -603,15 +608,15 @@ export default function LaunchForm({ ethUsd, gitlawbUsd = null, initialChain = " onChange={(e) => setCustomMcap(e.target.value.replace(/[^0-9.]/g, ""))} placeholder="custom" inputMode="decimal" - aria-label={`custom starting market cap in ${quote.symbol}`} + aria-label={`custom starting market cap in ${entry.unit === "usd" ? "USD" : quote.symbol}`} /> - {quote.symbol} + {entry.unit === "usd" ? "USD" : quote.symbol}
{fdvPreview !== null && tokensPerEth !== null ? (

- Opens at {fmtMcap(fdvPreview)} - {previewMcapUsd && quote.key !== "usdg" ? ≈ {previewMcapUsd} : null} fully diluted. The first {quote.key === "gitlawb" ? "1M " : ""}{quote.symbol} buys about{" "} + Opens at {cap(fdvPreview).main} + · {cap(fdvPreview).detail} fully diluted. The first {quote.key === "gitlawb" ? "1M " : ""}{quote.symbol} buys about{" "} {fmtCompact(tokensPerEth * (quote.key === "gitlawb" ? 1e6 : 1), 0)} tokens, then the price climbs along the curve.

) : null} @@ -787,7 +792,7 @@ export default function LaunchForm({ ethUsd, gitlawbUsd = null, initialChain = " {description.trim() ?

{description.trim()}

: null}
- + f.pips === feePips)?.label ?? "—"} sub={feePips === 0 ? "free pool" : beneficiary === "burn" ? "burned" : "to beneficiary"} /> diff --git a/app/src/components/launchpad/LaunchRow.tsx b/app/src/components/launchpad/LaunchRow.tsx index 99c6e65b..ff8a054f 100644 --- a/app/src/components/launchpad/LaunchRow.tsx +++ b/app/src/components/launchpad/LaunchRow.tsx @@ -10,6 +10,7 @@ import { ago } from "@/lib/launchpad/time"; import ChangeChip from "./ChangeChip"; import GitlawbBadge, { isGitlawbQuote } from "./GitlawbBadge"; import { marketUsd } from "@/lib/launchpad/market-format"; +import { capDisplay } from "@/lib/launchpad/market-cap"; import type { LiveTier } from "@/lib/launchpad/ranking"; const columns = "md:grid-cols-[minmax(0,1fr)_6.5rem_5.5rem_6rem_5rem_2.5rem]"; @@ -37,9 +38,9 @@ export default function LaunchRow({ l, rank, window = "all", hl = null, now, pop const volUsd = window === "1h" ? l.volume_1h_usd : window === "24h" ? l.volume_24h_usd : l.volume_usd; const volLabel = volUsd !== null ? marketUsd(volUsd) : fmtQuote(volRaw, l.quote_decimals, l.quote_symbol); const volumeDetail = volUsd !== null ? fmtUsd(volUsd) : volLabel; - const fdvQuoteLabel = fmtQuote(BigInt(Math.round(l.fdv_quote * 10 ** l.quote_decimals)), l.quote_decimals, l.quote_symbol); - const capLabel = l.fdv_usd !== null ? marketUsd(l.fdv_usd) : fdvQuoteLabel; - const capDetail = l.fdv_usd !== null ? fmtUsd(l.fdv_usd) : fdvQuoteLabel; + const cap = capDisplay(l.fdv_quote, l.quote_usd, { key: l.quote_key, symbol: l.quote_symbol, decimals: l.quote_decimals }); // dollars lead; the quote figure is the detail + const capLabel = cap.main; + const capDetail = cap.usd !== null ? `${fmtUsd(cap.usd)} · ${cap.detail}` : `${cap.main} · ${cap.detail}`; const feeLabel = mode === "free" ? "0% trading fee" : `${pipsToPct(l.lp_fee)} fee → ${mode === "burn" ? "burned" : mode === "split" ? "split" : "beneficiary"}`; const flash = hl ? `bb-row-${hl.kind}` : ""; const age = ; @@ -66,7 +67,7 @@ export default function LaunchRow({ l, rank, window = "all", hl = null, now, pop
Market cap {capLabel} - {l.fdv_usd !== null ? fdvQuoteLabel : l.quote_symbol} + {cap.detail} Change since launch
Change since launch
diff --git a/app/src/components/launchpad/MeDashboard.tsx b/app/src/components/launchpad/MeDashboard.tsx index 7fc92602..86ed6a25 100644 --- a/app/src/components/launchpad/MeDashboard.tsx +++ b/app/src/components/launchpad/MeDashboard.tsx @@ -17,6 +17,7 @@ import { LAUNCH_LOCKER_ABI, ERC20_MIN_ABI } from "@/lib/launchpad/abi"; import { launchpad } from "@/lib/launchpad/config"; import { earnedRaw, feeShareBps, holdingUsd, isBurnOnly } from "@/lib/launchpad/creator"; import { fmtCompact, fmtQuote, fmtUsd } from "@/lib/launchpad/math"; +import { capDisplay } from "@/lib/launchpad/market-cap"; import type { LaunchRow, WalletTrade } from "@/lib/launchpad/queries"; import type { EditFields } from "@/lib/launchpad/editAuth"; import { ago } from "@/lib/launchpad/time"; @@ -249,7 +250,7 @@ function WalletDashboard({ address, isConnected }: { address: Address | undefine
- mc {l.fdv_usd !== null ? fmtUsd(l.fdv_usd, { compact: true }) : "—"} + mc {capDisplay(l.fdv_quote, l.quote_usd, { key: l.quote_key, symbol: l.quote_symbol, decimals: l.quote_decimals }).main} · {l.buys + l.sells} trades {now ? · {ago(l.block_time, now)} ago : null}
@@ -298,7 +299,7 @@ function WalletDashboard({ address, isConnected }: { address: Address | undefine {isGitlawbQuote(t.quote_key) ? : null}
- {t.my_buys} buys · {t.my_sells} sells · mc {t.fdv_usd !== null ? fmtUsd(t.fdv_usd, { compact: true }) : "—"} + {t.my_buys} buys · {t.my_sells} sells · mc {capDisplay(t.fdv_quote, t.quote_usd, { key: t.quote_key, symbol: t.quote_symbol, decimals: t.quote_decimals }).main}
diff --git a/app/src/components/launchpad/TrendingStrip.tsx b/app/src/components/launchpad/TrendingStrip.tsx index 31c9d93b..ac4ba6af 100644 --- a/app/src/components/launchpad/TrendingStrip.tsx +++ b/app/src/components/launchpad/TrendingStrip.tsx @@ -10,6 +10,7 @@ import { useLive } from "./LiveProvider"; import type { LaunchRow } from "@/lib/launchpad/queries"; import { fmtQuote } from "@/lib/launchpad/math"; import { marketUsd } from "@/lib/launchpad/market-format"; +import { capDisplay } from "@/lib/launchpad/market-cap"; import { CHAIN_SHORT } from "@/lib/chainPublic"; import { stickyKing } from "@/lib/launchpad/ranking"; import { launchKey, refreshInPlace } from "@/lib/launchpad/list-state"; @@ -90,7 +91,7 @@ export default function TrendingStrip({ initial }: { initial: Snap }) {

{row.name}

{row.symbol}

- Market cap {row.fdv_usd !== null ? marketUsd(row.fdv_usd) : "—"} + Market cap {capDisplay(row.fdv_quote, row.quote_usd, { key: row.quote_key, symbol: row.quote_symbol, decimals: row.quote_decimals }).main}
diff --git a/app/src/lib/launchpad/market-cap.test.ts b/app/src/lib/launchpad/market-cap.test.ts new file mode 100644 index 00000000..c8444069 --- /dev/null +++ b/app/src/lib/launchpad/market-cap.test.ts @@ -0,0 +1,65 @@ +import { test } from "node:test"; +import assert from "node:assert/strict"; +import { MCAP_DEFAULT_INDEX, MCAP_USD_TARGETS, capChipLabel, capDisplay, capEntry, capPick, capPresets, capQuoteLabel, capToQuote } from "./market-cap.ts"; +import { fdvForStartTick, startTickForFdv } from "./math.ts"; + +const eth = { key: "eth" as const, symbol: "ETH", decimals: 18 }; +const usdg = { key: "usdg" as const, symbol: "USDG", decimals: 6 }; +const nvda = { key: "stock" as const, symbol: "NVDA", decimals: 18 }; +const gitlawb = { key: "gitlawb" as const, symbol: "GITLAWB", decimals: 18 }; + +test("capEntry: dollars whenever the quote has a positive price, quote units otherwise", () => { + assert.deepEqual(capEntry(2464.49), { unit: "usd", quoteUsd: 2464.49 }); + assert.deepEqual(capEntry(1), { unit: "usd", quoteUsd: 1 }); + assert.deepEqual(capEntry(null), { unit: "quote" }); + assert.deepEqual(capEntry(0), { unit: "quote" }); + assert.deepEqual(capEntry(Number.NaN), { unit: "quote" }); +}); + +test("capPresets: the same four dollar targets for every priced quote; a quote's own units without a price", () => { + const usd = capEntry(2464.49); + for (const k of ["eth", "usdg", "gitlawb", "stock"] as const) assert.deepEqual(capPresets(usd, k), MCAP_USD_TARGETS); + assert.deepEqual(capPresets(capEntry(null), "eth"), [1, 5, 10, 25], "ETH without a price: the old ETH presets"); + assert.deepEqual(capPresets(capEntry(null), "usdg"), [5_000, 10_000, 25_000, 100_000]); + assert.deepEqual(capPresets(capEntry(null), "stock"), [], "a stock without a price has no presets: the form asks for a custom cap"); + assert.deepEqual(capPresets(capEntry(null), "gitlawb"), []); + assert.equal(MCAP_USD_TARGETS[MCAP_DEFAULT_INDEX], 25_000); +}); + +test("capPick: a pick survives only among the presets it was made from; otherwise the default", () => { + assert.equal(capPick(100_000, MCAP_USD_TARGETS), 100_000); + assert.equal(capPick(null, MCAP_USD_TARGETS), 25_000); + assert.equal(capPick(25_000, [1, 5, 10, 25]), 10, "a $25K pick does not become 25,000 ETH after switching to an unpriced quote"); + assert.equal(capPick(10, [1, 5, 10, 25]), 10); + assert.equal(capPick(10, []), null, "no presets, no pick: the form needs a custom cap"); +}); + +test("capToQuote feeds the tick maths: $25K at $2,500/ETH is 10 ETH, and the tick round-trips within one spacing step", () => { + const usd = capEntry(2500); + assert.equal(capToQuote(25_000, usd), 10); + assert.equal(capToQuote(10, capEntry(null)), 10, "quote mode is the identity"); + const tick = startTickForFdv(capToQuote(25_000, usd), 18); + const shown = fdvForStartTick(tick, 18) * 2500; + assert.ok(Math.abs(shown / 25_000 - 1) < 0.02, `the cap the tick actually produces is shown, within one 2% tick step of the target: $${shown.toFixed(0)}`); + const usdgTick = startTickForFdv(capToQuote(10_000, capEntry(1)), 6); + const usdgShown = fdvForStartTick(usdgTick, 6); + assert.ok(Math.abs(usdgShown / 10_000 - 1) < 0.02); +}); + +test("labels: chips in dollars or quote units; quote text per quote kind", () => { + assert.equal(capChipLabel(25_000, capEntry(2500), eth), "$25K"); + assert.equal(capChipLabel(100_000, capEntry(1), usdg), "$100K"); + assert.equal(capChipLabel(10, capEntry(null), eth), "10 ETH"); + assert.equal(capQuoteLabel(10.1234, eth), "10.123 ETH"); + assert.equal(capQuoteLabel(25_000, usdg), "25,000 USDG"); + assert.equal(capQuoteLabel(0.11208, nvda), "0.112 NVDA"); + assert.equal(capQuoteLabel(1_250_000, gitlawb), "1.25M GITLAWB"); +}); + +test("capDisplay: dollars lead with the quote figure as detail; without a price the quote figure leads and says so", () => { + assert.deepEqual(capDisplay(10, 2500, eth), { main: "$25K", detail: "10 ETH", usd: 25_000 }); + assert.deepEqual(capDisplay(25_000, 1, usdg), { main: "$25K", detail: "25,000 USDG", usd: 25_000 }); + assert.deepEqual(capDisplay(0.112, 223.05, nvda), { main: "$24.98", detail: "0.112 NVDA", usd: 0.112 * 223.05 }, "under $1,000 the figure keeps cents"); + assert.deepEqual(capDisplay(10, null, eth), { main: "10 ETH", detail: "no USD price", usd: null }); + assert.deepEqual(capDisplay(0.5, 0, nvda), { main: "0.500 NVDA", detail: "no USD price", usd: null }); +}); diff --git a/app/src/lib/launchpad/market-cap.ts b/app/src/lib/launchpad/market-cap.ts new file mode 100644 index 00000000..4570bb87 --- /dev/null +++ b/app/src/lib/launchpad/market-cap.ts @@ -0,0 +1,63 @@ +/** + * Market cap is a dollar figure everywhere; the quote-denominated figure is a detail. Pure, node --test loads it directly. + * + * The launch form lets the creator enter the starting cap in dollars whenever the quote has a USD price (ETH, USDG, + * GITLAWB and stocks with a live price) and converts to quote units for the tick maths; when the quote has no price + * the form falls back to quote units, as before. The same four dollar targets apply to every priced quote, so an + * ETH launch no longer opens at "10 ETH" while a USDG launch opens at "$10K". + */ + +import { MCAP_PRESETS, type Quote } from "./config"; +import { fmtCompact, fmtQuoteUnits } from "./math"; +import { marketUsd } from "./market-format"; + +export const MCAP_USD_TARGETS = [5_000, 10_000, 25_000, 100_000]; +export const MCAP_DEFAULT_INDEX = 2; // $25K, about what the old 10 ETH default was + +/** How the form takes the starting cap: in dollars (with the quote's USD price) or in quote units (no price). */ +export type CapEntry = { unit: "usd"; quoteUsd: number } | { unit: "quote" }; + +export function capEntry(quoteUsd: number | null): CapEntry { + return quoteUsd !== null && Number.isFinite(quoteUsd) && quoteUsd > 0 ? { unit: "usd", quoteUsd } : { unit: "quote" }; +} + +/** Preset caps in the entry unit: the dollar targets, or the quote-unit presets a quote has without a price (none for stocks/GITLAWB). */ +export function capPresets(entry: CapEntry, quoteKey: Quote["key"]): number[] { + return entry.unit === "usd" ? MCAP_USD_TARGETS : MCAP_PRESETS[quoteKey]; +} + +/** The preset to use: the creator's pick when it belongs to this quote's presets (a pick does not carry across quotes), else the default. */ +export function capPick(pick: number | null, presets: number[]): number | null { + if (pick !== null && presets.includes(pick)) return pick; + return presets[MCAP_DEFAULT_INDEX] ?? presets[0] ?? null; +} + +/** A cap in the entry unit → quote units, which is what the start tick is computed from. */ +export function capToQuote(v: number, entry: CapEntry): number { + return entry.unit === "usd" ? v / entry.quoteUsd : v; +} + +/** Preset chip text: "$25K" in dollar mode, "10 ETH" in quote mode. */ +export function capChipLabel(v: number, entry: CapEntry, quote: Pick): string { + return entry.unit === "usd" ? `$${fmtCompact(v, 0)}` : `${fmtQuoteUnits(v, quote.decimals)} ${quote.symbol}`; +} + +/** A quote-denominated cap as quote text: "9.87 ETH", "25,000 USDG", "0.112 NVDA". */ +export function capQuoteLabel(fdvQuote: number, quote: Pick): string { + if (quote.decimals <= 6) return `${fdvQuote.toLocaleString("en-US", { maximumFractionDigits: 0 })} ${quote.symbol}`; + if (quote.key === "stock") return `${fdvQuote.toFixed(3)} ${quote.symbol}`; + return `${fmtQuoteUnits(fdvQuote, quote.decimals)} ${quote.symbol}`; +} + +/** + * How a cap is shown anywhere on the site: the dollar figure leads and the quote figure is the detail; without a USD + * price the quote figure leads and the detail says so. Never a bare dash. + */ +export function capDisplay(fdvQuote: number, quoteUsd: number | null, quote: Pick): { main: string; detail: string; usd: number | null } { + const quoteText = capQuoteLabel(fdvQuote, quote); + if (quoteUsd !== null && Number.isFinite(quoteUsd) && quoteUsd > 0) { + const usd = fdvQuote * quoteUsd; + return { main: marketUsd(usd), detail: quoteText, usd }; + } + return { main: quoteText, detail: "no USD price", usd: null }; +} From f1db129ef374ebf2287aaeb0cf0c9a77253b7143 Mon Sep 17 00:00:00 2001 From: Kevin Codex Date: Fri, 11 Sep 2026 13:02:40 +0800 Subject: [PATCH 2/2] market cap (CodeRabbit, PR #27): the "no USD price" mark reaches every site; fractional USDG - One-string sites (trending strip, dashboard token lists, the mobile buy bar) rendered only the main figure, so an unpriced quote showed "10 ETH" with nothing saying it is not dollars. The display helper now also returns `compact`: the dollar figure when priced, else the quote figure with the mark; those sites use it. - The post-buy estimate in the launch form showed only the main figure; it now carries the detail like "Opens at". The home row showed the detail only from md up; the mobile block has it too. - A 6-decimal quote's detail rounded to whole units, so a 0.5 USDG cap read "1 USDG"; below 100 units it keeps two decimals now. - Tests: helper cases for compact and fractional USDG; a source test pins every site to render the detail or the compact form, and the form to have dropped the old quote-first formatter. --- app/src/app/t/[chain]/[token]/page.tsx | 2 +- app/src/components/launchpad/LaunchForm.tsx | 3 +- app/src/components/launchpad/LaunchRow.tsx | 2 +- app/src/components/launchpad/MeDashboard.tsx | 4 +-- .../components/launchpad/TrendingStrip.tsx | 2 +- .../launchpad/market-cap-sites.test.ts | 29 +++++++++++++++++++ app/src/lib/launchpad/market-cap.test.ts | 16 ++++++---- app/src/lib/launchpad/market-cap.ts | 15 ++++++---- 8 files changed, 55 insertions(+), 18 deletions(-) create mode 100644 app/src/components/launchpad/market-cap-sites.test.ts diff --git a/app/src/app/t/[chain]/[token]/page.tsx b/app/src/app/t/[chain]/[token]/page.tsx index 0bcf7ec0..f1aede6c 100644 --- a/app/src/app/t/[chain]/[token]/page.tsx +++ b/app/src/app/t/[chain]/[token]/page.tsx @@ -170,7 +170,7 @@ export default async function TokenPage({ params }: { params: Promise<{ chain: s
- + ); } diff --git a/app/src/components/launchpad/LaunchForm.tsx b/app/src/components/launchpad/LaunchForm.tsx index da054856..1da9d121 100644 --- a/app/src/components/launchpad/LaunchForm.tsx +++ b/app/src/components/launchpad/LaunchForm.tsx @@ -214,7 +214,6 @@ export default function LaunchForm({ ethUsd, gitlawbUsd = null, initialChain = " const fdvPreview = startTick !== null ? fdvForStartTick(startTick, quote.decimals) : null; const tokensPerEth = startTick !== null ? tickToTokensPerQuote(startTick, quote.decimals) : null; const cap = (v: number) => capDisplay(v, quoteUsd, quote); - const fmtMcap = (v: number) => cap(v).main; const onChain = chainId === CHAIN.id; const symbolClean = symbol.trim().toUpperCase(); @@ -727,7 +726,7 @@ export default function LaunchForm({ ethUsd, gitlawbUsd = null, initialChain = "

Estimated buy: about {fmtCompact(buyPreview.tokensOut, 0)} {symbolClean || "tokens"}{" "} ({fmtPct(buyPreview.pctOfSupply)} of supply{buyUsd ? ` · ≈ ${fmtUsd(buyUsd)}` : ""}). Estimated market cap after your buy:{" "} - {fmtMcap(buyPreview.fdvAfter)}. Includes price impact and the pool fee; the exact amount is quoted on-chain right before the buy. + {cap(buyPreview.fdvAfter).main} · {cap(buyPreview.fdvAfter).detail}. Includes price impact and the pool fee; the exact amount is quoted on-chain right before the buy.

) : suggestion.reason === "insufficient" ? (

Suggested {fmtQuoteUnits(Number(defaultFirstBuy(quote)), quote.decimals)} {quote.symbol}, but this wallet holds only gas. The launch stays free; you can buy on the token page later.

diff --git a/app/src/components/launchpad/LaunchRow.tsx b/app/src/components/launchpad/LaunchRow.tsx index ff8a054f..b9afa041 100644 --- a/app/src/components/launchpad/LaunchRow.tsx +++ b/app/src/components/launchpad/LaunchRow.tsx @@ -68,7 +68,7 @@ export default function LaunchRow({ l, rank, window = "all", hl = null, now, pop Market cap {capLabel} {cap.detail} - Change since launch + {cap.detail}Change since launch
Change since launch
Volume {window} {volLabel}
diff --git a/app/src/components/launchpad/MeDashboard.tsx b/app/src/components/launchpad/MeDashboard.tsx index 86ed6a25..ce5f96c1 100644 --- a/app/src/components/launchpad/MeDashboard.tsx +++ b/app/src/components/launchpad/MeDashboard.tsx @@ -250,7 +250,7 @@ function WalletDashboard({ address, isConnected }: { address: Address | undefine
- mc {capDisplay(l.fdv_quote, l.quote_usd, { key: l.quote_key, symbol: l.quote_symbol, decimals: l.quote_decimals }).main} + mc {capDisplay(l.fdv_quote, l.quote_usd, { key: l.quote_key, symbol: l.quote_symbol, decimals: l.quote_decimals }).compact} · {l.buys + l.sells} trades {now ? · {ago(l.block_time, now)} ago : null}
@@ -299,7 +299,7 @@ function WalletDashboard({ address, isConnected }: { address: Address | undefine {isGitlawbQuote(t.quote_key) ? : null}
- {t.my_buys} buys · {t.my_sells} sells · mc {capDisplay(t.fdv_quote, t.quote_usd, { key: t.quote_key, symbol: t.quote_symbol, decimals: t.quote_decimals }).main} + {t.my_buys} buys · {t.my_sells} sells · mc {capDisplay(t.fdv_quote, t.quote_usd, { key: t.quote_key, symbol: t.quote_symbol, decimals: t.quote_decimals }).compact}
diff --git a/app/src/components/launchpad/TrendingStrip.tsx b/app/src/components/launchpad/TrendingStrip.tsx index ac4ba6af..34cc02f9 100644 --- a/app/src/components/launchpad/TrendingStrip.tsx +++ b/app/src/components/launchpad/TrendingStrip.tsx @@ -91,7 +91,7 @@ export default function TrendingStrip({ initial }: { initial: Snap }) {

{row.name}

{row.symbol}

- Market cap {capDisplay(row.fdv_quote, row.quote_usd, { key: row.quote_key, symbol: row.quote_symbol, decimals: row.quote_decimals }).main} + Market cap {capDisplay(row.fdv_quote, row.quote_usd, { key: row.quote_key, symbol: row.quote_symbol, decimals: row.quote_decimals }).compact}
diff --git a/app/src/components/launchpad/market-cap-sites.test.ts b/app/src/components/launchpad/market-cap-sites.test.ts new file mode 100644 index 00000000..6d3fdd15 --- /dev/null +++ b/app/src/components/launchpad/market-cap-sites.test.ts @@ -0,0 +1,29 @@ +import assert from "node:assert/strict"; +import { readFileSync } from "node:fs"; +import test from "node:test"; + +/** Every market-cap site renders the dollar-first display and never drops the "no USD price" mark (CodeRabbit, PR #27). */ +const read = (p: string) => readFileSync(new URL(p, import.meta.url), "utf8"); + +test("the home row shows the quote detail on every width", () => { + const row = read("./LaunchRow.tsx"); + assert.match(row, /const cap = capDisplay\(l\.fdv_quote, l\.quote_usd/); + assert.equal((row.match(/>\{cap\.detail\}\{cap\.detail\}<\/span>/, "the mobile block carries the detail"); +}); + +test("one-string sites use the compact form, which carries the mark for an unpriced quote", () => { + for (const [file, expected] of [["./TrendingStrip.tsx", 1], ["./MeDashboard.tsx", 2], ["../../app/t/[chain]/[token]/page.tsx", 1]] as const) { + const src = read(file); + assert.equal((src.match(/capDisplay\([^)]*\)\.compact/g) ?? []).length, expected, `${file} uses .compact`); + assert.doesNotMatch(src, /capDisplay\([^)]*\)\.main/, `${file} never renders .main alone`); + } +}); + +test("the launch form renders main + detail for the opening cap, the preview card and the post-buy estimate", () => { + const form = read("./LaunchForm.tsx"); + assert.equal((form.match(/cap\(fdvPreview\)\.main/g) ?? []).length, 2); + assert.equal((form.match(/cap\(fdvPreview\)\.detail/g) ?? []).length, 2); + assert.match(form, /cap\(buyPreview\.fdvAfter\)\.main\}<\/span>]*> · \{cap\(buyPreview\.fdvAfter\)\.detail\}/); + assert.doesNotMatch(form, /fmtMcap\(/, "the old quote-first formatter is gone"); +}); diff --git a/app/src/lib/launchpad/market-cap.test.ts b/app/src/lib/launchpad/market-cap.test.ts index c8444069..b378696c 100644 --- a/app/src/lib/launchpad/market-cap.test.ts +++ b/app/src/lib/launchpad/market-cap.test.ts @@ -1,6 +1,6 @@ import { test } from "node:test"; import assert from "node:assert/strict"; -import { MCAP_DEFAULT_INDEX, MCAP_USD_TARGETS, capChipLabel, capDisplay, capEntry, capPick, capPresets, capQuoteLabel, capToQuote } from "./market-cap.ts"; +import { MCAP_DEFAULT_INDEX, MCAP_USD_TARGETS, NO_USD_PRICE, capChipLabel, capDisplay, capEntry, capPick, capPresets, capQuoteLabel, capToQuote } from "./market-cap.ts"; import { fdvForStartTick, startTickForFdv } from "./math.ts"; const eth = { key: "eth" as const, symbol: "ETH", decimals: 18 }; @@ -52,14 +52,18 @@ test("labels: chips in dollars or quote units; quote text per quote kind", () => assert.equal(capChipLabel(10, capEntry(null), eth), "10 ETH"); assert.equal(capQuoteLabel(10.1234, eth), "10.123 ETH"); assert.equal(capQuoteLabel(25_000, usdg), "25,000 USDG"); + assert.equal(capQuoteLabel(0.5, usdg), "0.5 USDG", "fractional USDG keeps its cents (CodeRabbit, PR #27)"); + assert.equal(capQuoteLabel(99.99, usdg), "99.99 USDG"); + assert.equal(capQuoteLabel(100.4, usdg), "100 USDG", "from 100 units up, whole units"); assert.equal(capQuoteLabel(0.11208, nvda), "0.112 NVDA"); assert.equal(capQuoteLabel(1_250_000, gitlawb), "1.25M GITLAWB"); }); test("capDisplay: dollars lead with the quote figure as detail; without a price the quote figure leads and says so", () => { - assert.deepEqual(capDisplay(10, 2500, eth), { main: "$25K", detail: "10 ETH", usd: 25_000 }); - assert.deepEqual(capDisplay(25_000, 1, usdg), { main: "$25K", detail: "25,000 USDG", usd: 25_000 }); - assert.deepEqual(capDisplay(0.112, 223.05, nvda), { main: "$24.98", detail: "0.112 NVDA", usd: 0.112 * 223.05 }, "under $1,000 the figure keeps cents"); - assert.deepEqual(capDisplay(10, null, eth), { main: "10 ETH", detail: "no USD price", usd: null }); - assert.deepEqual(capDisplay(0.5, 0, nvda), { main: "0.500 NVDA", detail: "no USD price", usd: null }); + assert.deepEqual(capDisplay(10, 2500, eth), { main: "$25K", detail: "10 ETH", compact: "$25K", usd: 25_000 }); + assert.deepEqual(capDisplay(25_000, 1, usdg), { main: "$25K", detail: "25,000 USDG", compact: "$25K", usd: 25_000 }); + assert.deepEqual(capDisplay(0.112, 223.05, nvda), { main: "$24.98", detail: "0.112 NVDA", compact: "$24.98", usd: 0.112 * 223.05 }, "under $1,000 the figure keeps cents"); + assert.deepEqual(capDisplay(10, null, eth), { main: "10 ETH", detail: NO_USD_PRICE, compact: "10 ETH · no USD price", usd: null }, "one-string sites still carry the mark"); + assert.deepEqual(capDisplay(0.5, 0, nvda), { main: "0.500 NVDA", detail: NO_USD_PRICE, compact: "0.500 NVDA · no USD price", usd: null }); + assert.deepEqual(capDisplay(0.5, 1, usdg), { main: "$0.5", detail: "0.5 USDG", compact: "$0.5", usd: 0.5 }); }); diff --git a/app/src/lib/launchpad/market-cap.ts b/app/src/lib/launchpad/market-cap.ts index 4570bb87..433f9e0d 100644 --- a/app/src/lib/launchpad/market-cap.ts +++ b/app/src/lib/launchpad/market-cap.ts @@ -44,20 +44,25 @@ export function capChipLabel(v: number, entry: CapEntry, quote: Pick): string { - if (quote.decimals <= 6) return `${fdvQuote.toLocaleString("en-US", { maximumFractionDigits: 0 })} ${quote.symbol}`; + if (quote.decimals <= 6) return `${fdvQuote.toLocaleString("en-US", { maximumFractionDigits: Math.abs(fdvQuote) < 100 ? 2 : 0 })} ${quote.symbol}`; // a 0.5 USDG cap is "0.5 USDG", not "1 USDG" if (quote.key === "stock") return `${fdvQuote.toFixed(3)} ${quote.symbol}`; return `${fmtQuoteUnits(fdvQuote, quote.decimals)} ${quote.symbol}`; } +export const NO_USD_PRICE = "no USD price"; + /** * How a cap is shown anywhere on the site: the dollar figure leads and the quote figure is the detail; without a USD - * price the quote figure leads and the detail says so. Never a bare dash. + * price the quote figure leads and the detail says so. Never a bare dash. `compact` is the one-string form for sites + * with room for a single figure: the dollar figure when priced, else the quote figure with the mark, so an unpriced + * cap is never shown as if it were dollars. */ -export function capDisplay(fdvQuote: number, quoteUsd: number | null, quote: Pick): { main: string; detail: string; usd: number | null } { +export function capDisplay(fdvQuote: number, quoteUsd: number | null, quote: Pick): { main: string; detail: string; compact: string; usd: number | null } { const quoteText = capQuoteLabel(fdvQuote, quote); if (quoteUsd !== null && Number.isFinite(quoteUsd) && quoteUsd > 0) { const usd = fdvQuote * quoteUsd; - return { main: marketUsd(usd), detail: quoteText, usd }; + const main = marketUsd(usd); + return { main, detail: quoteText, compact: main, usd }; } - return { main: quoteText, detail: "no USD price", usd: null }; + return { main: quoteText, detail: NO_USD_PRICE, compact: `${quoteText} · ${NO_USD_PRICE}`, usd: null }; }