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
5 changes: 2 additions & 3 deletions app/src/app/t/[chain]/[token]/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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";

Expand Down Expand Up @@ -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);
Expand Down Expand Up @@ -171,7 +170,7 @@ export default async function TokenPage({ params }: { params: Promise<{ chain: s
</div>
</div>
</main>
<MobileBuyBar symbol={l.symbol} mcap={fdvUsd !== null ? fmtUsd(fdvUsd, { compact: true }) : fdvQuoteLabel} />
<MobileBuyBar symbol={l.symbol} mcap={capDisplay(l.fdv_quote, l.quote_usd, { key: l.quote_key, symbol: l.quote_symbol, decimals: l.quote_decimals }).compact} />
</>
);
}
Expand Down
34 changes: 19 additions & 15 deletions app/src/components/launchpad/LaunchForm.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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";
Expand Down Expand Up @@ -203,12 +203,17 @@ export default function LaunchForm({ ethUsd, gitlawbUsd = null, initialChain = "
const metaKeyRef = useRef<Hex | null>(null);
const [phase, setPhase] = useState<Phase>({ 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 onChain = chainId === CHAIN.id;
const symbolClean = symbol.trim().toUpperCase();
Expand Down Expand Up @@ -358,7 +363,6 @@ export default function LaunchForm({ ethUsd, gitlawbUsd = null, initialChain = "
}
}

const previewMcapUsd = fdvPreview !== null && quoteUsd ? fmtUsd(fdvPreview * quoteUsd, { compact: true }) : null;

return (
<div className="grid lg:grid-cols-[minmax(0,1fr)_22rem] gap-6 lg:gap-8 items-start">
Expand Down Expand Up @@ -581,7 +585,7 @@ export default function LaunchForm({ ethUsd, gitlawbUsd = null, initialChain = "
</div>
<div className="flex flex-wrap gap-2">
{presets.map((v) => {
const active = !customMcap.trim() && (mcapPick ?? presets[2] ?? presets[0]) === v;
const active = !customMcap.trim() && pickedPreset === v;
return (
<button
type="button"
Expand All @@ -592,7 +596,7 @@ export default function LaunchForm({ ethUsd, gitlawbUsd = null, initialChain = "
}}
className={`h-11 px-4 rounded-xl border font-mono text-sm font-bold tnum ${active ? "bg-ink text-inverse border-ink" : "bg-card text-ink border-line-strong hover:border-ink/40"}`}
>
{quote.key === "stock" || quote.key === "gitlawb" ? `$${fmtCompact(v * (quote.usd ?? 0), 0)}` : quote.decimals <= 6 ? `$${fmtCompact(v, 0)}` : `${v} ${quote.symbol}`}
{capChipLabel(v, entry, quote)}
</button>
);
})}
Expand All @@ -603,15 +607,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}`}
/>
<span className="absolute right-3 top-1/2 -translate-y-1/2 text-xs font-mono text-muted">{quote.symbol}</span>
<span className="absolute right-3 top-1/2 -translate-y-1/2 text-xs font-mono text-muted">{entry.unit === "usd" ? "USD" : quote.symbol}</span>
</div>
</div>
{fdvPreview !== null && tokensPerEth !== null ? (
<p className="text-sm text-body">
Opens at <span className="font-mono font-bold text-ink tnum">{fmtMcap(fdvPreview)}</span>
{previewMcapUsd && quote.key !== "usdg" ? <span className="font-mono text-muted tnum"> ≈ {previewMcapUsd}</span> : null} fully diluted. The first {quote.key === "gitlawb" ? "1M " : ""}{quote.symbol} buys about{" "}
Opens at <span className="font-mono font-bold text-ink tnum">{cap(fdvPreview).main}</span>
<span className="font-mono text-muted tnum"> · {cap(fdvPreview).detail}</span> fully diluted. The first {quote.key === "gitlawb" ? "1M " : ""}{quote.symbol} buys about{" "}
<span className="font-mono font-bold text-ink tnum">{fmtCompact(tokensPerEth * (quote.key === "gitlawb" ? 1e6 : 1), 0)}</span> tokens, then the price climbs along the curve.
</p>
) : null}
Expand Down Expand Up @@ -722,7 +726,7 @@ export default function LaunchForm({ ethUsd, gitlawbUsd = null, initialChain = "
<p className="text-sm text-body">
Estimated buy: about <span className="font-mono font-bold text-ink tnum">{fmtCompact(buyPreview.tokensOut, 0)}</span> {symbolClean || "tokens"}{" "}
<span className="font-mono text-muted tnum">({fmtPct(buyPreview.pctOfSupply)} of supply{buyUsd ? ` · ≈ ${fmtUsd(buyUsd)}` : ""})</span>. Estimated market cap after your buy:{" "}
<span className="font-mono font-bold text-ink tnum">{fmtMcap(buyPreview.fdvAfter)}</span>. Includes price impact and the pool fee; the exact amount is quoted on-chain right before the buy.
<span className="font-mono font-bold text-ink tnum">{cap(buyPreview.fdvAfter).main}</span><span className="font-mono text-muted tnum"> · {cap(buyPreview.fdvAfter).detail}</span>. Includes price impact and the pool fee; the exact amount is quoted on-chain right before the buy.
</p>
) : suggestion.reason === "insufficient" ? (
<p className={helper}>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.</p>
Expand Down Expand Up @@ -787,7 +791,7 @@ export default function LaunchForm({ ethUsd, gitlawbUsd = null, initialChain = "
</div>
{description.trim() ? <p className="mt-3 text-sm text-body line-clamp-3">{description.trim()}</p> : null}
<dl className="mt-4 grid grid-cols-2 gap-2">
<Mini k="Opens at" v={fdvPreview !== null ? fmtMcap(fdvPreview) : "—"} sub={quote.key !== "usdg" ? previewMcapUsd : CHAIN_LABELS[chain]} />
<Mini k="Opens at" v={fdvPreview !== null ? cap(fdvPreview).main : "—"} sub={fdvPreview !== null ? cap(fdvPreview).detail : CHAIN_LABELS[chain]} />
<Mini k="First buy" v={initialBuyRaw ? `${initialBuy.trim()} ${quote.symbol}` : "none"} sub={buyPreview ? `${buySource === "suggested" ? "suggested · " : ""}~${fmtPct(buyPreview.pctOfSupply)} of supply` : "pool opens untouched"} />
<Mini k="Trading fee" v={FEE_PRESETS.find((f) => f.pips === feePips)?.label ?? "—"} sub={feePips === 0 ? "free pool" : beneficiary === "burn" ? "burned" : "to beneficiary"} />
<Mini k="Platform fee" v="0" sub="always" accent />
Expand Down
11 changes: 6 additions & 5 deletions app/src/components/launchpad/LaunchRow.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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]";
Expand Down Expand Up @@ -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 = <time dateTime={l.block_time} title={new Date(l.block_time).toUTCString()} suppressHydrationWarning>{ago(l.block_time, now)}</time>;
Expand All @@ -66,8 +67,8 @@ export default function LaunchRow({ l, rank, window = "all", hl = null, now, pop
<div className="min-w-0 text-right">
<span className="mb-0.5 block text-[10px] text-muted md:sr-only">Market cap</span>
<span key={hl?.at ?? "rest"} className={`block truncate font-mono text-sm font-bold text-ink tnum ${pop ? "bb-pop" : ""}`} title={capDetail}>{capLabel}</span>
<span className="hidden truncate font-mono text-[10px] text-muted tnum md:block" title={fdvQuoteLabel}>{l.fdv_usd !== null ? fdvQuoteLabel : l.quote_symbol}</span>
<span className="mt-0.5 block md:hidden"><span className="sr-only">Change since launch </span><ChangeChip v={l.change_from_launch} plain /></span>
<span className="hidden truncate font-mono text-[10px] text-muted tnum md:block" title={capDetail}>{cap.detail}</span>
<span className="mt-0.5 block md:hidden"><span className="block truncate font-mono text-[10px] text-muted tnum" title={capDetail}>{cap.detail}</span><span className="sr-only">Change since launch </span><ChangeChip v={l.change_from_launch} plain /></span>
</div>
<div className="hidden min-w-0 text-right md:block"><span className="sr-only">Change since launch </span><ChangeChip v={l.change_from_launch} plain /></div>
<div className="hidden min-w-0 text-right font-mono text-xs text-body tnum md:block" title={volumeDetail}><span className="sr-only">Volume {window} </span><span className="block truncate">{volLabel}</span></div>
Expand Down
5 changes: 3 additions & 2 deletions app/src/components/launchpad/MeDashboard.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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";
Expand Down Expand Up @@ -249,7 +250,7 @@ function WalletDashboard({ address, isConnected }: { address: Address | undefine
</div>
<div className={styles.tokenMeta}>
<FeeChip lpFee={l.lp_fee} mode={feeModeOf(l.lp_fee, l.recipients)} />
<span>mc {l.fdv_usd !== null ? fmtUsd(l.fdv_usd, { compact: true }) : "—"}</span>
<span>mc {capDisplay(l.fdv_quote, l.quote_usd, { key: l.quote_key, symbol: l.quote_symbol, decimals: l.quote_decimals }).compact}</span>
<span>· {l.buys + l.sells} trades</span>
{now ? <span suppressHydrationWarning>· {ago(l.block_time, now)} ago</span> : null}
</div>
Expand Down Expand Up @@ -298,7 +299,7 @@ function WalletDashboard({ address, isConnected }: { address: Address | undefine
{isGitlawbQuote(t.quote_key) ? <GitlawbBadge /> : null}
</div>
<div className={styles.tokenMeta}>
{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 }).compact}
</div>
</div>
<div className={styles.holdingValue}>
Expand Down
3 changes: 2 additions & 1 deletion app/src/components/launchpad/TrendingStrip.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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";
Expand Down Expand Up @@ -90,7 +91,7 @@ export default function TrendingStrip({ initial }: { initial: Snap }) {
<div className="min-w-0"><p className="truncate text-[13px] font-semibold text-ink">{row.name}</p><p className="truncate font-mono text-[10px] text-muted">{row.symbol}</p></div>
</div>
<div className="mt-3 flex min-w-0 items-baseline justify-between gap-2">
<span className="truncate font-mono text-sm font-bold text-ink tnum"><span className="sr-only">Market cap </span>{row.fdv_usd !== null ? marketUsd(row.fdv_usd) : "—"}</span>
<span className="truncate font-mono text-sm font-bold text-ink tnum"><span className="sr-only">Market cap </span>{capDisplay(row.fdv_quote, row.quote_usd, { key: row.quote_key, symbol: row.quote_symbol, decimals: row.quote_decimals }).compact}</span>
<ChangeChip v={row.change_from_launch} plain />
</div>
<div className="mt-2 flex items-center justify-between gap-2 border-t border-line pt-2 text-[10px] text-muted">
Expand Down
29 changes: 29 additions & 0 deletions app/src/components/launchpad/market-cap-sites.test.ts
Original file line number Diff line number Diff line change
@@ -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\}</g) ?? []).length, 2, "one rendered detail line for desktop, one for mobile");
assert.match(row, /md:hidden"><span className="block truncate[^"]*" title=\{capDetail\}>\{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><span[^>]*> · \{cap\(buyPreview\.fdvAfter\)\.detail\}/);
assert.doesNotMatch(form, /fmtMcap\(/, "the old quote-first formatter is gone");
});
Loading
Loading