From e89d2b81ce90855a5c2d4e921356a5e039fd41d7 Mon Sep 17 00:00:00 2001 From: Kevin Codex Date: Tue, 8 Sep 2026 16:14:25 +0800 Subject: [PATCH 01/10] GITLAWB as a quote asset on Base Launches on Base can now be priced in GITLAWB (Gitlawb's token), next to ETH and Coinbase tokenized stocks. Same contracts, same ERC-20 quote path (Permit2, salt ordering); trading fees are paid in GITLAWB, or burned when no beneficiary is named. - lib/launchpad/gitlawb.ts: token facts, the WETH/GITLAWB Uniswap v4 pool key (dynamic-fee hook, tick spacing 200; pool id asserted = keccak(abi.encode(key))), USD from sqrtPriceX96 x ETH/USD with a sanity window, dollar->GITLAWB presets, tile - lib/launchpad/gitlawbServer.ts: StateView.getSlot0 read, 60s cache, fail-soft - config: quote key "gitlawb", Base quotes [ETH, GITLAWB], presets, quoteKeyOf() - queries: live USD factor in SQL sorts/volume/mcap + totals, gitlawb list filter - launch form: GITLAWB chip with live price, dollar start-cap presets, compact units - token page + share card: "priced in GITLAWB" note/pill with the Gitlawb mark - fmtQuote: 18-dec amounts >= 100K read as 1.2M (GITLAWB is millions per dollar) - copy: rules (new FAQ entry), agents, llms.txt, README - tests: gitlawb.test (pool id, price, presets), filter + card cases (256 total); contracts/test/LaunchFactory.gitlawb.fork.t.sol proves launch/buy/sell/collect on live Base with the 1% fee burned as GITLAWB --- README.md | 3 +- app/src/app/agents/page.tsx | 4 +- app/src/app/api/quotes/route.ts | 6 +- app/src/app/llms.txt/route.ts | 3 +- app/src/app/rules/page.tsx | 3 +- .../app/t/[chain]/[token]/opengraph-image.tsx | 12 +- app/src/app/t/[chain]/[token]/page.tsx | 6 +- app/src/components/launchpad/LaunchForm.tsx | 61 ++++++++-- app/src/components/launchpad/TradePanel.tsx | 2 +- .../components/sections/rules-guide.test.ts | 4 +- app/src/lib/launchpad/config.ts | 16 ++- app/src/lib/launchpad/creator.test.ts | 3 +- app/src/lib/launchpad/gitlawb.test.ts | 46 ++++++++ app/src/lib/launchpad/gitlawb.ts | 64 +++++++++++ app/src/lib/launchpad/gitlawbServer.ts | 39 +++++++ app/src/lib/launchpad/math.ts | 3 +- app/src/lib/launchpad/ogcard.ts | 7 +- app/src/lib/launchpad/queries.ts | 22 ++-- app/src/lib/launchpad/search.test.ts | 2 + app/src/lib/launchpad/search.ts | 6 +- .../test/LaunchFactory.gitlawb.fork.t.sol | 107 ++++++++++++++++++ 21 files changed, 376 insertions(+), 43 deletions(-) create mode 100644 app/src/lib/launchpad/gitlawb.test.ts create mode 100644 app/src/lib/launchpad/gitlawb.ts create mode 100644 app/src/lib/launchpad/gitlawbServer.ts create mode 100644 contracts/test/LaunchFactory.gitlawb.fork.t.sol diff --git a/README.md b/README.md index a10be2dc..b3621d3c 100644 --- a/README.md +++ b/README.md @@ -32,7 +32,8 @@ Creators choose a 0 / 1 / 3 % trading fee that is pushed **in full** to benefici - **Rug-proof liquidity.** The position NFT lives in a locker with no owner, no pause, no upgrade path and no transfer function. Proven with an adversarial fork suite against the live deployment. - **Two chains, one set of addresses.** Base and Robinhood Chain, same contracts, same rules. -- **Any quote asset.** ETH, USDG, or a **tokenized stock**: Coinbase's B20 stocks on Base +- **Any quote asset.** ETH, USDG, **GITLAWB** ([Gitlawb](https://gitlawb.com)'s token on Base, priced from its + Uniswap v4 WETH pool; fees paid or burned in GITLAWB), or a **tokenized stock**: Coinbase's B20 stocks on Base (NVDAc, AAPLc, TSLAc, …, priced from Chainlink on-chain feeds) and Robinhood Stock Tokens on Robinhood Chain. - **Trade in-page.** Buys and sells go straight to the Uniswap v4 pool through the Universal Router; the site never touches funds. diff --git a/app/src/app/agents/page.tsx b/app/src/app/agents/page.tsx index 930470c2..87a970f2 100644 --- a/app/src/app/agents/page.tsx +++ b/app/src/app/agents/page.tsx @@ -73,7 +73,7 @@ export default function AgentsPage() {
-
quote
ETH uses address zero. For USDG on Robinhood Chain, use 0x5fc5360D0400a0Fd4f2af552ADD042D716F1d168 and call findSalt(...) first so the token address sorts above the quote.
+
quote
ETH uses address zero. For GITLAWB on Base, use 0x5F980Dcfc4c0fa3911554cf5ab288ed0eb13DBa3; for USDG on Robinhood Chain, 0x5fc5360D0400a0Fd4f2af552ADD042D716F1d168. For any ERC-20 quote call findSalt(...) first so the token address sorts above the quote.
supply
0 means the default 1B supply.
startTick
Sets the opening market cap. A multiple of 200; 184200 ≈ 10 ETH FDV for 1B supply.
lpFee
In pips: 0, 10000 = 1%, or 30000 = 3%.
@@ -106,7 +106,7 @@ export default function AgentsPage() {
03

Trade & collect

Standard pools. Direct contract calls.

-
Swap

Speak Uniswap v4

Pools are plain Uniswap v4, with tick spacing 200 and no hook. The quote can be ETH, USDG or a supported issuer-registry stock token. Get the actual pool key with poolKeyOf(token) on the factory. Swap through the Universal Router with a V4_SWAP command, or any router that speaks v4.

+
Swap

Speak Uniswap v4

Pools are plain Uniswap v4, with tick spacing 200 and no hook. The quote can be ETH, GITLAWB (Base), USDG (Robinhood Chain) or a supported issuer-registry stock token. Get the actual pool key with poolKeyOf(token) on the factory. Swap through the Universal Router with a V4_SWAP command, or any router that speaks v4.

Collect

Pay out accrued fees

Anyone may call collect(tokenId) on that chain's locker to pay out accrued fees. Base locker: {locker}. Use the matching chain address above.

Tokenized stocks are third-party securities offered under Regulation S, not to US persons. The site recognizes them from issuer registries, not on-chain names.

diff --git a/app/src/app/api/quotes/route.ts b/app/src/app/api/quotes/route.ts index 2ede9816..64975d99 100644 --- a/app/src/app/api/quotes/route.ts +++ b/app/src/app/api/quotes/route.ts @@ -5,12 +5,13 @@ import { ensureRegistry, stockList, stockPrices } from "@/lib/launchpad/stocksSe import { searchStocks } from "@/lib/launchpad/stocks"; import { searchBaseStocks, stockTileSvg } from "@/lib/launchpad/baseStocks"; import { memo } from "@/lib/launchpad/memo"; +import { gitlawbUsd } from "@/lib/launchpad/gitlawbServer"; export const dynamic = "force-dynamic"; /** * GET /api/quotes?chain=[&q=AAPL] → quote assets the launch form may offer on that chain: - * the fixed ones (ETH, USDG) plus tokenized stocks from that chain's registry (with live USD): + * the fixed ones (ETH, USDG, GITLAWB on Base — with GITLAWB's live USD) plus tokenized stocks from that chain's registry (with live USD): * Coinbase tokenized stocks on Base, Robinhood Stock Tokens on Robinhood Chain. * Only registry addresses are ever labelled as stocks. */ @@ -19,7 +20,8 @@ export async function GET(req: Request) { const chain = u.searchParams.get("chain"); if (!isChainKey(chain)) return NextResponse.json({ error: "bad chain" }, { status: 400 }); const q = (u.searchParams.get("q") ?? "").slice(0, 12); - const fixed = launchpad(chain).quotes.map((x) => ({ ...x, usd: x.usd })); + const gl = chain === "base" ? await gitlawbUsd() : null; + const fixed = launchpad(chain).quotes.map((x) => ({ ...x, usd: x.key === "gitlawb" ? gl : x.usd })); const stocks = await memo(`quotes:stocks:${chain}:${q.toUpperCase()}`, 30_000, async () => { let hits: { address: string; symbol: string; name: string; decimals: number; logo: string | null }[]; if (chain === "base") hits = searchBaseStocks(q, 13).map((s) => ({ ...s, logo: stockTileSvg(s.symbol) })); diff --git a/app/src/app/llms.txt/route.ts b/app/src/app/llms.txt/route.ts index 1be3ef9e..acd68e7f 100644 --- a/app/src/app/llms.txt/route.ts +++ b/app/src/app/llms.txt/route.ts @@ -35,7 +35,7 @@ ${verificationLine("Robinhood Chain (4663)", r, [ ## What a launch does 1. deploys a fixed-supply ERC-20 (1,000,000,000; no mint/pause/blacklist/tax/owner) -2. initializes a Uniswap v4 pool quote/token (quote = ETH, or USDG on Robinhood Chain; tick spacing 200, no hook) at the chosen start tick +2. initializes a Uniswap v4 pool quote/token (quote = ETH, GITLAWB on Base, USDG on Robinhood Chain, or a registry stock token; tick spacing 200, no hook) at the chosen start tick 3. mints one single-sided position holding 100% of supply to an ownerless locker (no withdraw path exists) 4. registers fee routing: lpFee 0 | 10000 (1%) | 30000 (3%) pips; recipients [] = fees burned, else {payout,bps}[] summing to 10000. Fixed forever. @@ -47,6 +47,7 @@ Robinhood Chain (4663): LaunchFactory ${r.factory ?? "(not deployed yet)"} · La Uniswap v4: Base PoolManager 0x498581fF718922c3f8e6A244956aF099B2652b2b, Universal Router 0x6fF5693b99212Da76ad316178A184AB56D299b43; Robinhood PoolManager 0x8366a39CC670B4001A1121B8F6A443A643e40951, Universal Router 0x8876789976decbfcbbbe364623c63652db8c0904 USDG (Robinhood, 6 dec): 0x5fc5360D0400a0Fd4f2af552ADD042D716F1d168 +GITLAWB (Base, 18 dec): 0x5F980Dcfc4c0fa3911554cf5ab288ed0eb13DBa3 (Gitlawb's token; fees paid in GITLAWB, burned when recipients = []) ## Launch (contract call) factory.launch((name, symbol, metadataURI, quote, supply=0, startTick, lpFee, salt, recipients)) diff --git a/app/src/app/rules/page.tsx b/app/src/app/rules/page.tsx index a9b4df32..735b1349 100644 --- a/app/src/app/rules/page.tsx +++ b/app/src/app/rules/page.tsx @@ -12,7 +12,7 @@ export const metadata: Metadata = { title: "How it works", description: "What a const STEPS = [ { title: "Your token is deployed", description: "A plain ERC-20 with EIP-2612 permit. Fixed supply, 1 billion by default. No mint, no pause, no blacklist, no transfer tax, no owner.", value: "1B", label: "default supply" }, - { title: "A market opens", description: "A Uniswap v4 pool pairs your token with ETH, a tokenized stock (Coinbase stocks on Base, Robinhood Stock Tokens on Robinhood Chain), or USDG on Robinhood Chain. No hook. The pool starts at the market cap you pick.", value: "v4", label: "Uniswap pool" }, + { title: "A market opens", description: "A Uniswap v4 pool pairs your token with ETH, GITLAWB on Base, a tokenized stock (Coinbase stocks on Base, Robinhood Stock Tokens on Robinhood Chain), or USDG on Robinhood Chain. No hook. The pool starts at the market cap you pick.", value: "v4", label: "Uniswap pool" }, { title: "The liquidity position is locked", description: "100% of the supply goes into one single-sided position at launch. Its NFT is minted to an ownerless locker that has no function to withdraw, transfer or shrink it. Ever.", value: "100%", label: "deposited at launch" }, { title: "Fee routing is written in", description: "The trading fee you choose (0%, 1% or 3%) goes 100% to the beneficiaries you name, or is burned if you name none. Fixed at launch, unchangeable.", value: "0 / 1 / 3%", label: "your trading fee" }, ] as const; @@ -108,6 +108,7 @@ export default function RulesPage() {
How does the price move?

Price follows a single-sided Uniswap v4 curve: the first buyers get the most tokens per unit of the quote asset, and every buy moves the price up. Sells move it down.

There is no anti-snipe mechanism. Bots can buy in the first block like anyone else.

What can change after launch?

Nothing on-chain can be edited after launch: not the name, not the fee, not the beneficiaries. Metadata (image, description, links) is stored by this site and can change; the token itself cannot.

+
What is a GITLAWB-quoted pool?

GITLAWB is Gitlawb's token on Base, an ordinary ERC-20 with no transfer restrictions and no issuer switch. Pick it as the quote and buyers pay in GITLAWB, the creator's trading fee is paid in GITLAWB, and a launch with no beneficiary burns GITLAWB on every trade.

Dollar figures for these pools come from the Uniswap v4 WETH/GITLAWB pool price and ETH/USD. They are for display and sorting only; nothing on-chain depends on them.

What should I know about stock-quoted pools?

Stock-quoted pools use tokenized stocks as the quote asset: Coinbase tokenized stocks (B20) on Base, recognised here only from Base's official list and priced from Chainlink's on-chain feeds; Robinhood Stock Tokens on Robinhood Chain, recognised only from Robinhood's own registry.

Both are securities issued by third parties under Regulation S and are not offered to US persons. Coinbase's also exclude the UK, Canada, Australia, Singapore and Switzerland. Their issuers can pause transfers or freeze wallets in restricted jurisdictions. Holding or trading them is subject to the issuer's terms, not ours.

diff --git a/app/src/app/t/[chain]/[token]/opengraph-image.tsx b/app/src/app/t/[chain]/[token]/opengraph-image.tsx index 25a97610..9139bb5d 100644 --- a/app/src/app/t/[chain]/[token]/opengraph-image.tsx +++ b/app/src/app/t/[chain]/[token]/opengraph-image.tsx @@ -106,9 +106,15 @@ export default async function TokenOg({ params }: { params: Promise<{ chain: str
{card.quote ? ( - 4 ? 7 : card.quote.ticker.length > 3 ? 8 : 10, fontWeight: 800, letterSpacing: -0.5, whiteSpace: "nowrap", overflow: "hidden" }}> - {card.quote.ticker} - + {card.quote.kind === "gitlawb" ? ( + + + + ) : ( + 4 ? 7 : card.quote.ticker.length > 3 ? 8 : 10, fontWeight: 800, letterSpacing: -0.5, whiteSpace: "nowrap", overflow: "hidden" }}> + {card.quote.ticker} + + )} priced in {card.quote.symbol} ) : null} diff --git a/app/src/app/t/[chain]/[token]/page.tsx b/app/src/app/t/[chain]/[token]/page.tsx index e5b0591c..61a4af4c 100644 --- a/app/src/app/t/[chain]/[token]/page.tsx +++ b/app/src/app/t/[chain]/[token]/page.tsx @@ -21,7 +21,8 @@ import { memo } from "@/lib/launchpad/memo"; import { ago, nowMs } from "@/lib/launchpad/time"; import { getLaunch, getSwaps } from "@/lib/launchpad/queries"; import { ethUsd } from "@/lib/launchpad/ethPrice"; -import { NATIVE, TICK_SPACING, uniswapSwapUrl, type Quote } from "@/lib/launchpad/config"; +import { NATIVE, TICK_SPACING, quoteKeyOf, uniswapSwapUrl, type Quote } from "@/lib/launchpad/config"; +import { GITLAWB_SITE } from "@/lib/launchpad/gitlawb"; import { fmtCompact, fmtPrice, fmtQuote, fmtUsd, pipsToPct } from "@/lib/launchpad/math"; import { marketCount as count } from "@/lib/launchpad/token-market"; import { marketUsd } from "@/lib/launchpad/market-format"; @@ -55,7 +56,7 @@ export default async function TokenPage({ params }: { params: Promise<{ chain: s const l = await getLaunch(chain, token, usd); if (!l) notFound(); const quote: Quote = { - key: l.quote === NATIVE ? "eth" : l.quote_symbol === "USDG" ? "usdg" : "stock", + key: quoteKeyOf(chain, l.quote), address: l.quote as Address, symbol: l.quote_symbol, decimals: l.quote_decimals, @@ -140,6 +141,7 @@ export default async function TokenPage({ params }: { params: Promise<{ chain: s
{stockQuote ?

Paired with {stockQuote.name} ({stockQuote.symbol}), a third-party tokenized stock. These securities are not offered to US persons. The quote asset is identified from the issuer registry, not its token name.

: null} + {quote.key === "gitlawb" ?

Paired with GITLAWB, Gitlawb's token on Base. Trading fees on this pool are paid in GITLAWB{feeRoute.includes("burned") ? ", and burned" : ""}. USD figures use the Uniswap v4 WETH/GITLAWB pool price.

: null} } />
diff --git a/app/src/components/launchpad/LaunchForm.tsx b/app/src/components/launchpad/LaunchForm.tsx index 5b8e067d..cbe7c0f2 100644 --- a/app/src/components/launchpad/LaunchForm.tsx +++ b/app/src/components/launchpad/LaunchForm.tsx @@ -15,6 +15,7 @@ import { BPS, DEFAULT_SUPPLY, FEE_PRESETS, MCAP_PRESETS, TICK_SPACING, launchpad import { fdvForStartTick, fmtCompact, fmtUsd, initialBuyPreview, minOut, startTickForFdv, tickToTokensPerQuote, units } from "@/lib/launchpad/math"; import { encodeV4ExactInSingle, type PoolKey } from "@/lib/launchpad/swap"; import { stockMcapPresets } from "@/lib/launchpad/stocks"; +import { GITLAWB_SITE, gitlawbMcapPresets } 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"; @@ -43,7 +44,7 @@ type Beneficiary = "burn" | "me" | "custom"; const FIRST_BUY_SLIPPAGE_BPS = 300; // Other buyers can trade between the launch and this separate buy. const PERMIT_EXPIRY_S = 30 * 24 * 3600; const GAS_RESERVE_WEI = 500_000_000_000_000n; // 0.0005 ETH kept back so the buy itself can pay for gas -const BUY_PRESETS: Record = { eth: ["0.01", "0.05", "0.1", "0.25"], usdg: ["25", "100", "250"], stock: [] }; +const BUY_PRESETS: Record = { eth: ["0.01", "0.05", "0.1", "0.25"], usdg: ["25", "100", "250"], gitlawb: ["500000", "1000000", "5000000"], stock: [] }; function randomSalt(): Hex { const b = new Uint8Array(32); @@ -131,8 +132,28 @@ export default function LaunchForm({ ethUsd, initialChain = "base" }: { ethUsd: const [stock, setStock] = useState(null); const [stockQ, setStockQ] = useState(""); const [stockHits, setStockHits] = useState([]); - const quote: Quote = quoteKey === "stock" && stock ? stock : (cfg.quotes.find((q) => q.key === quoteKey) ?? cfg.quotes[0]); + // GITLAWB's USD is live (server reads the v4 pool); the static config carries null + const [gitlawbUsd, setGitlawbUsd] = useState(null); + const staticQuote: Quote = quoteKey === "stock" && stock ? stock : (cfg.quotes.find((q) => q.key === quoteKey) ?? cfg.quotes[0]); + const quote: Quote = staticQuote.key === "gitlawb" ? { ...staticQuote, usd: gitlawbUsd } : staticQuote; const quoteUsd = quoteUsdOf(quote, ethUsd); + useEffect(() => { + if (quoteKey !== "gitlawb") return; + let alive = true; + (async () => { + try { + const res = await fetch(`/api/quotes?chain=${chain}`, { cache: "no-store" }); + const d = (await res.json()) as { fixed?: Quote[] }; + const usd = d.fixed?.find((q) => q.key === "gitlawb")?.usd ?? null; + if (alive) setGitlawbUsd(usd); + } catch { + if (alive) setGitlawbUsd(null); + } + })(); + return () => { + alive = false; + }; + }, [chain, quoteKey]); // stock search (per-chain registry via our server; only registry addresses are ever offered) useEffect(() => { if (quoteKey !== "stock") return; @@ -176,12 +197,12 @@ export default function LaunchForm({ ethUsd, initialChain = "base" }: { ethUsd: const metaKeyRef = useRef(null); const [phase, setPhase] = useState({ k: "idle" }); - const presets = quote.key === "stock" ? stockMcapPresets(quote.usd ?? 0) : MCAP_PRESETS[quote.key]; + 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); 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}` : `${v.toFixed(quote.key === "stock" ? 3 : 2)} ${quote.symbol}`); + const fmtMcap = (v: number) => (quote.decimals <= 6 ? `${v.toLocaleString("en-US", { maximumFractionDigits: 0 })} ${quote.symbol}` : quote.key === "gitlawb" ? `${fmtCompact(v, 1)} ${quote.symbol}` : `${v.toFixed(quote.key === "stock" ? 3 : 2)} ${quote.symbol}`); const symbolClean = symbol.trim().toUpperCase(); const initialBuyRaw = parseBuyAmount(initialBuy, quote.decimals); @@ -190,6 +211,7 @@ export default function LaunchForm({ ethUsd, initialChain = "base" }: { ethUsd: if (!/^[A-Z0-9]{1,10}$/.test(symbolClean)) errors.push("Symbol: 1–10 letters or digits."); if (startTick === null) errors.push("Starting market cap must be a positive number."); if (quoteKey === "stock" && !stock) errors.push("Pick a stock to price the token in."); + if (quoteKey === "gitlawb" && quote.usd === null && !customMcap.trim()) errors.push("GITLAWB price unavailable right now: enter a custom starting market cap in GITLAWB, or retry."); if (image && !/^https:\/\//.test(image.trim())) errors.push("Image must be an https URL."); if (website && !/^https:\/\//.test(website.trim())) errors.push("Website must be an https URL."); if (feePips > 0 && beneficiary === "custom" && !isAddress(customAddr.trim())) errors.push("Beneficiary: enter a valid address."); @@ -357,12 +379,12 @@ export default function LaunchForm({ ethUsd, initialChain = "base" }: { ethUsd: aria-pressed={active} >
{CHAIN_LABELS[k]}
-
{k === "base" ? "Priced in ETH or a Coinbase tokenized stock. Gas ≈ cents." : ok ? "Priced in USDG (dollars), ETH or a Robinhood Stock Token. Gas ≈ cents." : "Coming soon."}
+
{k === "base" ? "Priced in ETH, GITLAWB or a Coinbase tokenized stock. Gas ≈ cents." : ok ? "Priced in USDG (dollars), ETH or a Robinhood Stock Token. Gas ≈ cents." : "Coming soon."}
); })} - {/* every chain offers at least one fixed quote plus tokenized stocks */} + {/* every chain offers at least one fixed quote plus tokenized stocks; Base also offers GITLAWB */} {cfg.quotes.length > 0 ? (
Priced in @@ -384,10 +406,27 @@ export default function LaunchForm({ ethUsd, initialChain = "base" }: { ethUsd: ))}
- {quote.key === "usdg" ? "Buyers pay with USDG; market cap and fees are in dollars." : quote.key === "stock" ? (chain === "base" ? "Buyers pay with a Coinbase tokenized stock; fees are paid in that stock." : "Buyers pay with a Robinhood Stock Token; fees are paid in that stock.") : "Buyers pay with ETH."} + {quote.key === "usdg" ? "Buyers pay with USDG; market cap and fees are in dollars." : quote.key === "gitlawb" ? "Buyers pay with GITLAWB; fees are paid in GITLAWB, or burned." : quote.key === "stock" ? (chain === "base" ? "Buyers pay with a Coinbase tokenized stock; fees are paid in that stock." : "Buyers pay with a Robinhood Stock Token; fees are paid in that stock.") : "Buyers pay with ETH."} ) : null} + {quoteKey === "gitlawb" ? ( +
+ + {quote.logo ? ( + // eslint-disable-next-line @next/next/no-img-element + + ) : null} + {quote.symbol} + {quote.name} + {quote.usd ? {fmtUsd(quote.usd)} : price loading…} + +

+ GITLAWB is Gitlawb's token on Base: an ordinary ERC-20, no transfer restrictions, no issuer switch. Name no beneficiary and every trading fee burns GITLAWB. Price from the Uniswap v4 WETH/GITLAWB pool.{" "} + gitlawb.com ↗ +

+
+ ) : null} {quoteKey === "stock" ? (
@@ -508,7 +547,7 @@ export default function LaunchForm({ ethUsd, initialChain = "base" }: { ethUsd: }} 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" ? `$${fmtCompact(v * (quote.usd ?? 0), 0)}` : quote.decimals <= 6 ? `$${fmtCompact(v, 0)}` : `${v} ${quote.symbol}`} + {quote.key === "stock" || quote.key === "gitlawb" ? `$${fmtCompact(v * (quote.usd ?? 0), 0)}` : quote.decimals <= 6 ? `$${fmtCompact(v, 0)}` : `${v} ${quote.symbol}`} ); })} @@ -527,8 +566,8 @@ export default function LaunchForm({ ethUsd, initialChain = "base" }: { ethUsd: {fdvPreview !== null && tokensPerEth !== null ? (

Opens at {fmtMcap(fdvPreview)} - {previewMcapUsd && quote.key !== "usdg" ? ≈ {previewMcapUsd} : null} fully diluted. The first {quote.symbol} buys about{" "} - {fmtCompact(tokensPerEth, 0)} tokens, then the price climbs along the curve. + {previewMcapUsd && quote.key !== "usdg" ? ≈ {previewMcapUsd} : null} 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} @@ -614,7 +653,7 @@ export default function LaunchForm({ ethUsd, initialChain = "base" }: { ethUsd: onClick={() => setInitialBuy(active ? "" : v)} 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"}`} > - {v} {quote.symbol} + {quote.key === "gitlawb" ? fmtCompact(Number(v), 0) : v} {quote.symbol} ); })} diff --git a/app/src/components/launchpad/TradePanel.tsx b/app/src/components/launchpad/TradePanel.tsx index 7acda594..f97aaa4a 100644 --- a/app/src/components/launchpad/TradePanel.tsx +++ b/app/src/components/launchpad/TradePanel.tsx @@ -200,7 +200,7 @@ export default function TradePanel({ chain, token, symbol, poolKey, quote, ethUs {side === "buy" ? quote.symbol : symbol}
- {side === "buy" ? BUY_PRESETS[quote.key].map((p) => ) : [25, 50, 100].map((pct) => )} + {side === "buy" ? BUY_PRESETS[quote.key].map((p) => ) : [25, 50, 100].map((pct) => )}
diff --git a/app/src/components/sections/rules-guide.test.ts b/app/src/components/sections/rules-guide.test.ts index 3365cbda..f42a7ab0 100644 --- a/app/src/components/sections/rules-guide.test.ts +++ b/app/src/components/sections/rules-guide.test.ts @@ -12,7 +12,9 @@ test("the rules guide retains native navigation and visible risk disclosures", ( for (const anchor of ["launchpad", "fees", "immutable", "know", "contracts"]) { assert.ok(page.includes(`id="${anchor}"`), `missing section: ${anchor}`); } - assert.equal((page.match(/
/g) ?? []).length, 3); + assert.equal((page.match(/
/g) ?? []).length, 4); + assert.match(page, /GITLAWB-quoted pool/); + assert.match(page, /burns GITLAWB on every trade/); assert.match(page, /a locked pool does not make a token valuable/); assert.match(page, /Nothing is refundable/); assert.match(page, /no anti-snipe mechanism/); diff --git a/app/src/lib/launchpad/config.ts b/app/src/lib/launchpad/config.ts index 0aba3547..fa6eb93a 100644 --- a/app/src/lib/launchpad/config.ts +++ b/app/src/lib/launchpad/config.ts @@ -1,5 +1,6 @@ import { isAddress, type Address } from "viem"; import { CHAIN_KEYS, SITE_URL, type ChainKey } from "@/lib/chainPublic"; +import { GITLAWB_ADDRESS, GITLAWB_DECIMALS, GITLAWB_NAME, GITLAWB_SYMBOL, gitlawbTileSvg } from "./gitlawb"; /** * CLIENT-SAFE launchpad config, per chain. Only NEXT_PUBLIC_* vars are read here. @@ -13,12 +14,14 @@ export const DEAD = "0x000000000000000000000000000000000000dEaD" as const; export const NATIVE = "0x0000000000000000000000000000000000000000" as const; export const BPS = 10_000; -export type Quote = { key: "eth" | "usdg" | "stock"; address: Address; symbol: string; decimals: number; usd: number | null /* fixed USD price (stables); live for stocks */; name?: string; logo?: string | null }; +export type Quote = { key: "eth" | "usdg" | "gitlawb" | "stock"; address: Address; symbol: string; decimals: number; usd: number | null /* fixed USD price (stables); live for stocks + GITLAWB (server-filled) */; name?: string; logo?: string | null }; export type V4 = { poolManager: Address; positionManager: Address; stateView: Address; quoter: Address; universalRouter: Address; permit2: Address; swapLayout: "v1" | "v2" }; export type ChainLaunchpad = { key: ChainKey; factory: Address | null; locker: Address | null; v4: V4; quotes: Quote[]; configured: boolean }; const ETH: Quote = { key: "eth", address: NATIVE, symbol: "ETH", decimals: 18, usd: null }; const USDG: Quote = { key: "usdg", address: "0x5fc5360D0400a0Fd4f2af552ADD042D716F1d168", symbol: "USDG", decimals: 6, usd: 1 }; +/** GITLAWB on Base: usd is null here (client-safe static); the server fills the live price (gitlawbServer.ts). */ +const GITLAWB: Quote = { key: "gitlawb", address: GITLAWB_ADDRESS as Address, symbol: GITLAWB_SYMBOL, decimals: GITLAWB_DECIMALS, usd: null, name: GITLAWB_NAME, logo: gitlawbTileSvg() }; /** Canonical Uniswap v4 deployments (developers.uniswap.org/docs/protocols/v4/deployments). */ const V4_BY_CHAIN: Record = { @@ -42,7 +45,7 @@ const V4_BY_CHAIN: Record = { }, }; /** Quote assets offered per chain, first = default. */ -const QUOTES_BY_CHAIN: Record = { base: [ETH], robinhood: [USDG, ETH] }; +const QUOTES_BY_CHAIN: Record = { base: [ETH, GITLAWB], robinhood: [USDG, ETH] }; function addr(v: string | undefined): Address | null { const raw = (v ?? "").trim(); @@ -85,6 +88,11 @@ export function quoteInfo(key: ChainKey, address: string): Quote { return QUOTES_BY_CHAIN[key].find((q) => q.address.toLowerCase() === a) ?? { key: "stock", address: address as Address, symbol: "?", decimals: 18, usd: null }; } +/** Quote key for a stored quote address: a fixed quote's key, else "stock" (= some ERC20, priced only if a registry knows it). */ +export function quoteKeyOf(key: ChainKey, address: string): Quote["key"] { + return quoteInfo(key, address).key; +} + export { quoteUsdOf } from "./math"; /** LP fee presets offered in the form (pips). */ @@ -95,9 +103,9 @@ export const FEE_PRESETS = [ ] as const; /** Starting market cap presets per quote (fully diluted, in quote units). */ -export const MCAP_PRESETS: Record = { eth: [1, 5, 10, 25], usdg: [5_000, 10_000, 25_000, 100_000], stock: [] /* derived from the live price */ }; +export const MCAP_PRESETS: Record = { eth: [1, 5, 10, 25], usdg: [5_000, 10_000, 25_000, 100_000], gitlawb: [] /* derived from the live price */, stock: [] /* derived from the live price */ }; /** Buy amount presets per quote. */ -export const BUY_PRESETS: Record = { eth: ["0.01", "0.05", "0.1", "0.5"], usdg: ["5", "25", "100", "500"], stock: ["0.1", "0.5", "1", "5"] }; +export const BUY_PRESETS: Record = { eth: ["0.01", "0.05", "0.1", "0.5"], usdg: ["5", "25", "100", "500"], gitlawb: ["100000", "500000", "1000000", "5000000"], stock: ["0.1", "0.5", "1", "5"] }; /** Browser RPC: dev override per chain, else our same-origin proxy (→ Alchemy/public, key stays server-side). */ export function browserRpc(key: ChainKey): string { diff --git a/app/src/lib/launchpad/creator.test.ts b/app/src/lib/launchpad/creator.test.ts index 37505211..d7adb0db 100644 --- a/app/src/lib/launchpad/creator.test.ts +++ b/app/src/lib/launchpad/creator.test.ts @@ -60,7 +60,8 @@ test("share card shaping", () => { const now = Date.parse("2026-09-06T12:00:00Z"); const c = shapeCard({ name: "Dollar Dog", symbol: "DDOG", chain: "robinhood", fdv_usd: 12345, fdv_quote: 12345, quote_symbol: "USDG", change_from_launch: 0.234, lp_fee: 10000, recipients: [{ payout: DEAD, bps: 10000 }], block_time: "2026-09-06T09:30:00Z" }, now); assert.deepEqual(c, { title: "Dollar Dog", symbol: "DDOG", chainLabel: "Robinhood Chain", mcap: "$12.3K", change: "+23%", up: true, fee: "1% fee, burned", age: "2h old", quote: null }); - assert.deepEqual(shapeCard({ name: "Base Stock Test", symbol: "BSTK", chain: "base", fdv_usd: 25206, fdv_quote: 109.6, quote_symbol: "NVDAc", change_from_launch: 0, lp_fee: 10000, recipients: [{ payout: "0x00000000000000000000000000000000000c0ffe", bps: 10000 }], block_time: "2026-09-06T09:30:00Z" }, now).quote, { symbol: "NVDAc", ticker: "NVDA" }, "stock quotes get a ticker tile on the card"); + assert.deepEqual(shapeCard({ name: "Base Stock Test", symbol: "BSTK", chain: "base", fdv_usd: 25206, fdv_quote: 109.6, quote_symbol: "NVDAc", change_from_launch: 0, lp_fee: 10000, recipients: [{ payout: "0x00000000000000000000000000000000000c0ffe", bps: 10000 }], block_time: "2026-09-06T09:30:00Z" }, now).quote, { symbol: "NVDAc", ticker: "NVDA", kind: "stock" }, "stock quotes get a ticker tile on the card"); + assert.deepEqual(shapeCard({ name: "Lawb Fan", symbol: "LAWB", chain: "base", fdv_usd: 9800, fdv_quote: 580_000_000, quote_symbol: "GITLAWB", change_from_launch: 0, lp_fee: 10000, recipients: [], block_time: "2026-09-06T09:30:00Z" }, now).quote, { symbol: "GITLAWB", ticker: "GL", kind: "gitlawb" }, "GITLAWB quotes get the Gitlawb mark"); assert.equal(feeLabel(0, []), "0% fee"); assert.equal(feeLabel(30000, [{ payout: "0x1", bps: 10000 }]), "3% fee → beneficiary"); assert.equal(ageLabel("2026-09-06T11:59:30Z", now), "1m old"); diff --git a/app/src/lib/launchpad/gitlawb.test.ts b/app/src/lib/launchpad/gitlawb.test.ts new file mode 100644 index 00000000..93818f7a --- /dev/null +++ b/app/src/lib/launchpad/gitlawb.test.ts @@ -0,0 +1,46 @@ +import { test } from "node:test"; +import assert from "node:assert/strict"; +import { encodeAbiParameters, keccak256 } from "viem"; +import { DYNAMIC_FEE_FLAG, GITLAWB_ADDRESS, GITLAWB_POOL_ID, GITLAWB_POOL_KEY, gitlawbMcapPresets, gitlawbTileSvg, gitlawbUsdFromSqrtPrice, isGitlawbAddress } from "./gitlawb.ts"; + +test("pool id = keccak256(abi.encode(poolKey)) for the WETH/GITLAWB dynamic-fee pool", () => { + const k = GITLAWB_POOL_KEY; + const id = keccak256( + encodeAbiParameters( + [{ type: "address" }, { type: "address" }, { type: "uint24" }, { type: "int24" }, { type: "address" }], + [k.currency0 as `0x${string}`, k.currency1 as `0x${string}`, k.fee, k.tickSpacing, k.hooks as `0x${string}`], + ), + ); + assert.equal(id, GITLAWB_POOL_ID); + assert.equal(k.fee, DYNAMIC_FEE_FLAG); + assert.ok(BigInt(k.currency0) < BigInt(k.currency1), "WETH sorts below GITLAWB → GITLAWB is currency1"); + assert.equal(GITLAWB_ADDRESS, GITLAWB_ADDRESS.toLowerCase(), "stored lowercase (DB quote column is lowercase)"); +}); + +test("gitlawbUsdFromSqrtPrice: live reading ≈ $1.68e-5 at tick 188117 / ETH $2,476; rejects garbage", () => { + // sqrtPriceX96 read from StateView on 2026-09-08 (tick 188117 → ~1.477e8 GITLAWB per ETH) + const sqrt = 0x2f799c757751a02dcfbdb3537a4an; + const usd = gitlawbUsdFromSqrtPrice(sqrt, 2476); + assert.ok(usd !== null); + assert.ok(Math.abs(usd - 1.676e-5) / 1.676e-5 < 0.01, `≈ $1.676e-5, got ${usd}`); + assert.equal(gitlawbUsdFromSqrtPrice(sqrt, null), null, "no ETH price → no USD"); + assert.equal(gitlawbUsdFromSqrtPrice(0n, 2476), null, "zero price"); + assert.equal(gitlawbUsdFromSqrtPrice(-1n, 2476), null, "negative"); + assert.equal(gitlawbUsdFromSqrtPrice(2n ** 96n, 2476), null, "1 GITLAWB = 1 ETH is outside the sanity window"); + assert.equal(gitlawbUsdFromSqrtPrice(2n ** 96n * 10n ** 9n, 2476), null, "absurdly cheap is rejected too"); +}); + +test("presets convert dollar targets into GITLAWB units; empty without a price", () => { + assert.deepEqual(gitlawbMcapPresets(null), []); + assert.deepEqual(gitlawbMcapPresets(0), []); + const p = gitlawbMcapPresets(0.00002); + assert.deepEqual(p, [250_000_000, 500_000_000, 1_250_000_000, 5_000_000_000]); +}); + +test("tile is an inline SVG data URL; address check is case-insensitive", () => { + const t = gitlawbTileSvg(); + assert.ok(t.startsWith("data:image/svg+xml,")); + assert.ok(decodeURIComponent(t).includes(" 0) || sqrtPriceX96 <= 0n) return null; + const sqrt = Number(sqrtPriceX96) / Number(Q96); + const gitlawbPerEth = sqrt * sqrt; // 18/18 decimals → no scaling + if (!Number.isFinite(gitlawbPerEth) || gitlawbPerEth <= 0) return null; + const usd = ethUsd / gitlawbPerEth; + // sanity window: $1e-9 … $1 per GITLAWB (spot is ~$1e-5; a 1e4× move either way is not a real reading) + if (!(usd > 1e-9 && usd < 1)) return null; + return usd; +} + +/** Starting-market-cap presets in whole GITLAWB for the usual dollar targets; empty while the price is unknown. */ +export function gitlawbMcapPresets(usdPrice: number | null, usdTargets = [5_000, 10_000, 25_000, 100_000]): number[] { + if (usdPrice === null || !(usdPrice > 0)) return []; + return usdTargets.map((u) => Math.round(u / usdPrice)); +} + +/** Gitlawb mark (ring + planet + satellite) on a navy tile, as an inline data URL (same shape as the stock tiles). */ +export function gitlawbTileSvg(): string { + const svg = ``; + return `data:image/svg+xml,${encodeURIComponent(svg).replace(/%20/g, " ")}`; +} + +export function isGitlawbAddress(address: string): boolean { + return address.toLowerCase() === GITLAWB_ADDRESS; +} diff --git a/app/src/lib/launchpad/gitlawbServer.ts b/app/src/lib/launchpad/gitlawbServer.ts new file mode 100644 index 00000000..e5566b55 --- /dev/null +++ b/app/src/lib/launchpad/gitlawbServer.ts @@ -0,0 +1,39 @@ +import "server-only"; +import { publicClient } from "@/lib/chain"; +import { STATE_VIEW_ABI } from "./abi"; +import { launchpad } from "./config"; +import { ethUsd } from "./ethPrice"; +import { GITLAWB_POOL_ID, gitlawbUsdFromSqrtPrice } from "./gitlawb"; + +/** + * USD per GITLAWB, server-side: StateView.getSlot0(WETH/GITLAWB pool) × ETH/USD, cached 60s. + * Fails soft: a transient RPC error keeps the last good price; with none, GITLAWB-quoted launches + * simply show no USD (and rank with 0 weight in USD sorts) until the next successful read. + */ +let cached: { at: number; usd: number | null } = { at: 0, usd: null }; +let inflight: Promise | null = null; +const TTL = 60_000; + +async function read(): Promise { + const [eth, slot0] = await Promise.all([ + ethUsd(), + publicClient("base").readContract({ address: launchpad("base").v4.stateView, abi: STATE_VIEW_ABI, functionName: "getSlot0", args: [GITLAWB_POOL_ID] }), + ]); + return gitlawbUsdFromSqrtPrice(slot0[0], eth); +} + +export async function gitlawbUsd(): Promise { + if (Date.now() - cached.at < TTL) return cached.usd; + inflight ??= read() + .then((usd) => { + cached = { at: Date.now(), usd: usd ?? cached.usd }; + return cached.usd; + }) + .catch((err) => { + console.warn("[gitlawb] price read failed:", err instanceof Error ? err.message : err); + cached = { at: Date.now(), usd: cached.usd }; + return cached.usd; + }) + .finally(() => (inflight = null)); + return inflight; +} diff --git a/app/src/lib/launchpad/math.ts b/app/src/lib/launchpad/math.ts index 5b5d54ad..1a5302be 100644 --- a/app/src/lib/launchpad/math.ts +++ b/app/src/lib/launchpad/math.ts @@ -166,7 +166,8 @@ export function pipsToPct(pips: number): string { /** "12.5 USDG" / "0.05 ETH" from a raw amount. */ export function fmtQuote(raw: bigint | string, decimals: number, symbol: string): string { const v = units(raw, decimals); - return `${decimals <= 6 ? (v >= 100 ? v.toFixed(0) : v.toFixed(2)).replace(/\.00$/, "") : fmtEth(v)} ${symbol}`; + // 18-dec quotes with big unit counts (GITLAWB: millions per dollar) read as 1.2M, never 1200000.0000 + return `${decimals <= 6 ? (v >= 100 ? v.toFixed(0) : v.toFixed(2)).replace(/\.00$/, "") : Math.abs(v) >= 100_000 ? fmtCompact(v, 2) : fmtEth(v)} ${symbol}`; } export const WAD_ = WAD; diff --git a/app/src/lib/launchpad/ogcard.ts b/app/src/lib/launchpad/ogcard.ts index 6f0cd951..a1607eff 100644 --- a/app/src/lib/launchpad/ogcard.ts +++ b/app/src/lib/launchpad/ogcard.ts @@ -1,6 +1,6 @@ /** Pure shaping for the per-token share card (node --test loads this). */ export type CardInput = { name: string; symbol: string; chain: "base" | "robinhood"; fdv_usd: number | null; fdv_quote: number; quote_symbol: string; change_from_launch: number; lp_fee: number; recipients: { payout: string; bps: number }[]; block_time: string }; -export type Card = { title: string; symbol: string; chainLabel: string; mcap: string; change: string; up: boolean; fee: string; age: string; quote: { symbol: string; ticker: string } | null }; +export type Card = { title: string; symbol: string; chainLabel: string; mcap: string; change: string; up: boolean; fee: string; age: string; quote: { symbol: string; ticker: string; kind: "stock" | "gitlawb" } | null }; const DEAD = "0x000000000000000000000000000000000000dead"; @@ -41,11 +41,12 @@ export function shapeCard(l: CardInput, now: number): Card { }; } -/** Stock quotes get a "priced in" pill with a ticker tile; ETH / USDG / unknown ("?") do not. */ +/** Stock quotes get a "priced in" pill with a ticker tile, GITLAWB one with the Gitlawb mark; ETH / USDG / unknown ("?") get none. */ export function stockQuoteOf(quoteSymbol: string): Card["quote"] { const sym = quoteSymbol.trim(); if (!sym || sym === "?" || sym === "ETH" || sym === "USDG") return null; - return { symbol: sym.slice(0, 12), ticker: sym.replace(/c$/, "").toUpperCase().slice(0, 5) }; + if (sym === "GITLAWB") return { symbol: "GITLAWB", ticker: "GL", kind: "gitlawb" }; + return { symbol: sym.slice(0, 12), ticker: sym.replace(/c$/, "").toUpperCase().slice(0, 5), kind: "stock" }; } /** Social previews show ~125 chars: clamp on a word boundary with an ellipsis; short text is untouched. */ diff --git a/app/src/lib/launchpad/queries.ts b/app/src/lib/launchpad/queries.ts index 25644df9..9f39bef3 100644 --- a/app/src/lib/launchpad/queries.ts +++ b/app/src/lib/launchpad/queries.ts @@ -3,6 +3,8 @@ import { maybeDb } from "@/lib/db"; import { CHAIN_KEYS, chainIdOf, chainKeyOf, type ChainKey } from "@/lib/chainPublic"; import { quoteInfo as staticQuoteInfo, quoteUsdOf, type Quote } from "./config"; import { ensureRegistry, stockByAddress, stockUsdInUse } from "./stocksServer"; +import { gitlawbUsd } from "./gitlawbServer"; +import { GITLAWB_ADDRESS } from "./gitlawb"; import { canonicalImageUrl } from "./images"; import { rankTrending } from "./trending"; import { imagePublicBase } from "./imageStore"; @@ -79,22 +81,25 @@ type Raw = Omit(); +/** GITLAWB USD for this request (filled by `withStocks`; null = unknown → no USD, 0 weight in USD sorts). */ +let gitlawbUsdNow: number | null = null; -/** Server-side quote resolution: static ETH/USDG, else a registry stock, else unknown. */ +/** Server-side quote resolution: static ETH/USDG/GITLAWB (GITLAWB gets the live price), else a registry stock, else unknown. */ function quoteInfo(chain: ChainKey, address: string): Quote { const q = staticQuoteInfo(chain, address); + if (q.key === "gitlawb") return { ...q, usd: gitlawbUsdNow }; if (q.symbol !== "?") return q; const st = stockByAddress(chain, address); return st ? { key: "stock", address: st.address as Quote["address"], symbol: st.symbol, decimals: st.decimals, usd: stockUsdNow.get(st.address) ?? null, name: st.name, logo: st.logo } : q; } -/** Warm the stock registry + prices for the stocks in use, so `shape()` can stay synchronous. */ +/** Warm the stock registry + prices for the stocks in use (and the GITLAWB price), so `shape()` can stay synchronous. */ async function withStocks(): Promise { try { await ensureRegistry(); // robinhood registry (fail-soft); the Base list is static - stockUsdNow = await stockUsdInUse(); + [stockUsdNow, gitlawbUsdNow] = await Promise.all([stockUsdInUse(), gitlawbUsd()]); } catch { - /* fail soft: stocks show without USD */ + /* fail soft: stocks / GITLAWB show without USD */ } } @@ -185,11 +190,13 @@ export async function listLaunchesPage(opts: ListOpts = {}): Promise { if (opts.filter === "fee0") conds.push(db`l.lp_fee = 0`); if (opts.filter === "burn") conds.push(db`l.lp_fee > 0 AND jsonb_array_length(l.recipients) = 1 AND lower(l.recipients->0->>'payout') = ${DEAD_ADDR}`); if (opts.filter === "usdg") conds.push(db`l.quote = ${USDG_ADDR}`); + if (opts.filter === "gitlawb") conds.push(db`l.quote = ${GITLAWB_ADDRESS}`); if (opts.filter === "today") conds.push(db`l.block_time > now() - interval '24 hours'`); const where = conds.length ? db`WHERE ${conds.reduce((a, c) => db`${a} AND ${c}`)}` : db``; // per-row USD factor and quote decimals (USDG is the only non-18-dec quote we list) const stockCase = stockEntries.length ? stockEntries.map(([a, v]) => db`WHEN l.quote = ${a} THEN ${v}::double precision`).reduce((acc, c) => db`${acc} ${c}`) : db``; - const usdPerUnit = db`(CASE WHEN l.quote = ${USDG_ADDR} THEN 1.0 ${stockCase} WHEN l.quote = ${NATIVE_ADDR} THEN ${ethFactor}::double precision ELSE 0.0 END)`; + const gitlawbFactor = gitlawbUsdNow !== null && gitlawbUsdNow > 0 ? gitlawbUsdNow : 0; // unknown → 0 weight, like an unknown stock + const usdPerUnit = db`(CASE WHEN l.quote = ${USDG_ADDR} THEN 1.0 ${stockCase} WHEN l.quote = ${GITLAWB_ADDRESS} THEN ${gitlawbFactor}::double precision WHEN l.quote = ${NATIVE_ADDR} THEN ${ethFactor}::double precision ELSE 0.0 END)`; const stockDec = stockEntries.map(([a]) => [a, stockByAddress("base", a)?.decimals ?? stockByAddress("robinhood", a)?.decimals ?? 18] as [string, number]).filter(([, d]) => d !== 18); const decCase = stockDec.length ? stockDec.map(([a, d]) => db`WHEN l.quote = ${a} THEN ${d}`).reduce((acc, c) => db`${acc} ${c}`) : db``; const qd = db`(CASE WHEN l.quote = ${USDG_ADDR} THEN 6 ${decCase} ELSE 18 END)`; @@ -284,7 +291,7 @@ export type LaunchTotals = { volume_usd: number; fees_burned_usd: number; fees_to_creators_usd: number; - by_chain: Record; + by_chain: Record; }; export async function getLaunchTotals(ethUsd: number | null = null): Promise { @@ -294,7 +301,7 @@ export async function getLaunchTotals(ethUsd: number | null = null): Promise { assert.equal(matchesFilter(row({ lp_fee: 10_000, recipients: [{ payout: "0xabc", bps: 10000 }] }), "burn"), false); assert.equal(matchesFilter(row(), "burn"), false, "0% pools are not 'burn'"); assert.equal(matchesFilter(row({ quote_symbol: "USDG" }), "usdg"), true); + assert.equal(matchesFilter(row({ quote_symbol: "GITLAWB" }), "gitlawb"), true); + assert.equal(matchesFilter(row({ quote_symbol: "ETH" }), "gitlawb"), false); assert.equal(matchesFilter(row(), "today"), true); assert.equal(matchesFilter(row({ block_time: new Date(Date.now() - 3 * 86_400_000).toISOString() }), "today"), false); assert.equal(matchesFilter(row(), null), true); diff --git a/app/src/lib/launchpad/search.ts b/app/src/lib/launchpad/search.ts index e44a6ff2..e6860765 100644 --- a/app/src/lib/launchpad/search.ts +++ b/app/src/lib/launchpad/search.ts @@ -1,13 +1,14 @@ /** Pure search / filter helpers (client + server; node --test loads this directly). */ -export type LaunchFilter = "fee0" | "burn" | "usdg" | "today"; +export type LaunchFilter = "fee0" | "burn" | "usdg" | "gitlawb" | "today"; export const FILTERS: { key: LaunchFilter; label: string; title: string }[] = [ { key: "fee0", label: "0% fee", title: "feeless pools" }, { key: "burn", label: "fees burned", title: "no beneficiary; every fee is burned" }, { key: "usdg", label: "USDG", title: "priced in USDG (Robinhood Chain)" }, + { key: "gitlawb", label: "GITLAWB", title: "priced in GITLAWB (Base)" }, { key: "today", label: "today", title: "launched in the last 24 hours" }, ]; export function isFilter(v: unknown): v is LaunchFilter { - return v === "fee0" || v === "burn" || v === "usdg" || v === "today"; + return v === "fee0" || v === "burn" || v === "usdg" || v === "gitlawb" || v === "today"; } /** Trim, collapse whitespace, strip a leading $ (people type $SYM), lowercase. */ @@ -34,6 +35,7 @@ export function matchesFilter(l: Matchable, f: LaunchFilter | null, now = Date.n if (f === "fee0") return l.lp_fee === 0; if (f === "burn") return l.lp_fee > 0 && l.recipients.length === 1 && l.recipients[0].payout.toLowerCase() === DEAD; if (f === "usdg") return l.quote_symbol === "USDG"; + if (f === "gitlawb") return l.quote_symbol === "GITLAWB"; return now - new Date(l.block_time).getTime() < 86_400_000; } diff --git a/contracts/test/LaunchFactory.gitlawb.fork.t.sol b/contracts/test/LaunchFactory.gitlawb.fork.t.sol new file mode 100644 index 00000000..f7ac0b06 --- /dev/null +++ b/contracts/test/LaunchFactory.gitlawb.fork.t.sol @@ -0,0 +1,107 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.26; + +import {Test} from "forge-std/Test.sol"; +import {IPoolManager} from "@uniswap/v4-core/src/interfaces/IPoolManager.sol"; +import {PoolKey} from "@uniswap/v4-core/src/types/PoolKey.sol"; +import {TickMath} from "@uniswap/v4-core/src/libraries/TickMath.sol"; +import {SwapParams} from "@uniswap/v4-core/src/types/PoolOperation.sol"; +import {PoolSwapTest} from "@uniswap/v4-core/src/test/PoolSwapTest.sol"; +import {LaunchFactory} from "src/LaunchFactory.sol"; +import {LaunchLocker} from "src/LaunchLocker.sol"; +import {LaunchToken} from "src/LaunchToken.sol"; + +interface IERC20Meta { + function decimals() external view returns (uint8); + function symbol() external view returns (string memory); + function balanceOf(address) external view returns (uint256); + function approve(address, uint256) external returns (bool); + function transfer(address, uint256) external returns (bool); +} + +/// Can a launch be quoted in GITLAWB (Gitlawb's token on Base)? Against the LIVE Base +/// factory: launch a token priced in GITLAWB with NO beneficiary, buy it with GITLAWB, +/// sell some back, collect — and prove the 1% fee was BURNED as GITLAWB (sent to 0x…dEaD). +/// GITLAWB is borrowed from the Uniswap v4 PoolManager (which provably holds it: the deepest +/// GITLAWB market is a v4 pool) via prank — no mocks anywhere. +/// FORK_TESTS=true BASE_RPC_URL=… forge test --match-contract LaunchGitlawbQuote -vv +contract LaunchGitlawbQuote is Test { + address constant FACTORY = 0x815542E8b392389A1389E22E588E4B62A67Ade72; + address constant LOCKER = 0xcd1680D26922fcd9CabFbb8a56bA40C333fD842a; + address constant PM = 0x498581fF718922c3f8e6A244956aF099B2652b2b; + address constant GITLAWB = 0x5F980Dcfc4c0fa3911554cf5ab288ed0eb13DBa3; // Gitlawb (18 dec) + address constant DEAD = 0x000000000000000000000000000000000000dEaD; + + bool forked; + LaunchFactory factory = LaunchFactory(FACTORY); + PoolSwapTest router; + address creator = makeAddr("gitlawb-creator"); + address buyer = makeAddr("gitlawb-buyer"); + + function setUp() public { + if (!vm.envOr("FORK_TESTS", false)) return; + vm.createSelectFork(vm.envOr("BASE_RPC_URL", string("https://mainnet.base.org"))); + forked = true; + router = new PoolSwapTest(IPoolManager(PM)); + vm.deal(creator, 1 ether); + vm.deal(buyer, 1 ether); + // borrow 200M GITLAWB (~$3.4K at $1.7e-5) from the PoolManager for the buyer + uint256 pmBal = IERC20Meta(GITLAWB).balanceOf(PM); + assertGt(pmBal, 200_000_000e18, "PoolManager holds GITLAWB (the v4 WETH/GITLAWB pool)"); + vm.prank(PM); + IERC20Meta(GITLAWB).transfer(buyer, 200_000_000e18); + } + + function test_fork_launchQuotedInGITLAWB_buySellBurn() public { + if (!forked) return; + assertEq(IERC20Meta(GITLAWB).decimals(), 18); + assertEq(IERC20Meta(GITLAWB).symbol(), "GITLAWB"); + LaunchFactory.LaunchParams memory p; + p.name = "Lawb Fan Token"; + p.symbol = "LAWBFAN"; + p.quote = GITLAWB; + p.lpFee = 10_000; + // 1B supply at an FDV of 600M GITLAWB (≈ $10k): tokens per GITLAWB = 1e9/6e8 ≈ 1.667 → tick ≈ ln(1.667)/ln(1.0001) ≈ 5,108 → 5,200 + p.startTick = 5_200; + // recipients empty = fees burned + (bytes32 salt,) = factory.findSalt(creator, keccak256("lawbfan"), p.name, p.symbol, 0, "", GITLAWB, 64); + p.salt = salt; + vm.prank(creator); + (address token, uint256 tokenId) = factory.launch(p); + assertGt(uint160(token), uint160(GITLAWB), "token sorts above the quote"); + PoolKey memory key = factory.poolKeyOf(token); + + uint256 deadBefore = IERC20Meta(GITLAWB).balanceOf(DEAD); + + // buy with 100M GITLAWB + vm.startPrank(buyer); + IERC20Meta(GITLAWB).approve(address(router), 100_000_000e18); + router.swap( + key, + SwapParams({zeroForOne: true, amountSpecified: -100_000_000e18, sqrtPriceLimitX96: TickMath.MIN_SQRT_PRICE + 1}), + PoolSwapTest.TestSettings({takeClaims: false, settleUsingBurn: false}), + "" + ); + uint256 got = LaunchToken(token).balanceOf(buyer); + assertGt(got, 100_000_000e18, "more than 100M tokens for 100M GITLAWB at about 1.6 tokens per GITLAWB"); + // sell half back for GITLAWB + LaunchToken(token).approve(address(router), got / 2); + uint256 glBefore = IERC20Meta(GITLAWB).balanceOf(buyer); + router.swap( + key, + SwapParams({ + zeroForOne: false, amountSpecified: -int256(got / 2), sqrtPriceLimitX96: TickMath.MAX_SQRT_PRICE - 1 + }), + PoolSwapTest.TestSettings({takeClaims: false, settleUsingBurn: false}), + "" + ); + vm.stopPrank(); + assertGt(IERC20Meta(GITLAWB).balanceOf(buyer), glBefore, "got GITLAWB back"); + + // fees: 1% of the GITLAWB that came in, burned (no beneficiary) → lands at 0x…dEaD + (uint256 quoteOut,) = LaunchLocker(payable(LOCKER)).collect(tokenId); + assertApproxEqRel(quoteOut, 1_000_000e18, 2e16, "about 1% of 100M GITLAWB collected"); + assertEq(IERC20Meta(GITLAWB).balanceOf(DEAD) - deadBefore, quoteOut, "every collected GITLAWB fee was burned"); + assertEq(IERC20Meta(GITLAWB).balanceOf(creator), 0, "creator got nothing: no beneficiary"); + } +} From 32ecff2772fecea6ad4b078a95ba1ee0ec403853 Mon Sep 17 00:00:00 2001 From: Kevin Codex Date: Tue, 8 Sep 2026 16:20:26 +0800 Subject: [PATCH 02/10] GITLAWB badge for tokens paired with GITLAWB The visible perk of picking GITLAWB as the quote: a navy badge with the Gitlawb mark wherever the token shows up. - components/launchpad/GitlawbBadge.tsx (+ GitlawbMark, isGitlawbQuote) - placements: launch list rows, trending cards, activity feed launch items (feed launch items now carry quote_symbol), token page header ("Paired with GITLAWB"), creator dashboard rows, launch-form preview card; share card already had the pill - launch form: "Pair with GITLAWB, get the badge" nudge next to the quote chips on Base, and the GITLAWB info block explains where the badge appears - dark mode: light hairline border so the badge stays legible on the dark ground --- app/src/app/t/[chain]/[token]/page.tsx | 3 +- app/src/components/launchpad/GitlawbBadge.tsx | 33 +++++++++++++++++++ app/src/components/launchpad/LaunchForm.tsx | 13 ++++++-- app/src/components/launchpad/LaunchRow.tsx | 2 ++ app/src/components/launchpad/LaunchTape.tsx | 2 ++ app/src/components/launchpad/MeDashboard.tsx | 3 ++ .../components/launchpad/TrendingStrip.tsx | 3 +- app/src/lib/launchpad/queries.ts | 4 +-- 8 files changed, 57 insertions(+), 6 deletions(-) create mode 100644 app/src/components/launchpad/GitlawbBadge.tsx diff --git a/app/src/app/t/[chain]/[token]/page.tsx b/app/src/app/t/[chain]/[token]/page.tsx index 61a4af4c..8e3c6718 100644 --- a/app/src/app/t/[chain]/[token]/page.tsx +++ b/app/src/app/t/[chain]/[token]/page.tsx @@ -23,6 +23,7 @@ import { getLaunch, getSwaps } from "@/lib/launchpad/queries"; import { ethUsd } from "@/lib/launchpad/ethPrice"; import { NATIVE, TICK_SPACING, quoteKeyOf, uniswapSwapUrl, type Quote } from "@/lib/launchpad/config"; import { GITLAWB_SITE } from "@/lib/launchpad/gitlawb"; +import GitlawbBadge from "@/components/launchpad/GitlawbBadge"; import { fmtCompact, fmtPrice, fmtQuote, fmtUsd, pipsToPct } from "@/lib/launchpad/math"; import { marketCount as count } from "@/lib/launchpad/token-market"; import { marketUsd } from "@/lib/launchpad/market-format"; @@ -90,7 +91,7 @@ export default async function TokenPage({ params }: { params: Promise<{ chain: s

{l.name}

-
${l.symbol}·Paired with {quote.symbol}·Launched {ago(l.block_time, now)} ago
+
${l.symbol}·{quote.key === "gitlawb" ? : Paired with {quote.symbol}}·Launched {ago(l.block_time, now)} ago
diff --git a/app/src/components/launchpad/GitlawbBadge.tsx b/app/src/components/launchpad/GitlawbBadge.tsx new file mode 100644 index 00000000..0bfde482 --- /dev/null +++ b/app/src/components/launchpad/GitlawbBadge.tsx @@ -0,0 +1,33 @@ +import { GITLAWB_SYMBOL } from "@/lib/launchpad/gitlawb"; + +/** The Gitlawb mark (ring + planet + satellite), inline so it renders in server and client components alike. */ +export function GitlawbMark({ size = 12, className = "" }: { size?: number; className?: string }) { + return ( + + ); +} + +/** + * Badge for tokens paired with GITLAWB. Shown wherever a token appears (list, trending, feed, token page, + * dashboard, share card) — the visible perk of picking GITLAWB as the quote asset. + */ +export default function GitlawbBadge({ size = "sm", className = "", label = GITLAWB_SYMBOL }: { size?: "sm" | "md"; className?: string; label?: string }) { + return ( + + + {label} + + ); +} + +/** True for a launch whose quote is GITLAWB (quote symbols come from the server registry, never from on-chain names). */ +export function isGitlawbQuote(quoteSymbol: string | null | undefined): boolean { + return quoteSymbol === GITLAWB_SYMBOL; +} diff --git a/app/src/components/launchpad/LaunchForm.tsx b/app/src/components/launchpad/LaunchForm.tsx index cbe7c0f2..75061748 100644 --- a/app/src/components/launchpad/LaunchForm.tsx +++ b/app/src/components/launchpad/LaunchForm.tsx @@ -8,6 +8,7 @@ import { isAddress, maxUint160, maxUint256, parseEventLogs, parseUnits, zeroAddr import TokenAvatar from "./TokenAvatar"; import ImageUpload from "./ImageUpload"; import FeeChip from "./FeeChip"; +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"; @@ -408,6 +409,11 @@ export default function LaunchForm({ ethUsd, initialChain = "base" }: { ethUsd: {quote.key === "usdg" ? "Buyers pay with USDG; market cap and fees are in dollars." : quote.key === "gitlawb" ? "Buyers pay with GITLAWB; fees are paid in GITLAWB, or burned." : quote.key === "stock" ? (chain === "base" ? "Buyers pay with a Coinbase tokenized stock; fees are paid in that stock." : "Buyers pay with a Robinhood Stock Token; fees are paid in that stock.") : "Buyers pay with ETH."} + {chain === "base" && quote.key !== "gitlawb" ? ( + + ) : null}
) : null} {quoteKey === "gitlawb" ? ( @@ -422,7 +428,7 @@ export default function LaunchForm({ ethUsd, initialChain = "base" }: { ethUsd: {quote.usd ? {fmtUsd(quote.usd)} : price loading…}

- GITLAWB is Gitlawb's token on Base: an ordinary ERC-20, no transfer restrictions, no issuer switch. Name no beneficiary and every trading fee burns GITLAWB. Price from the Uniswap v4 WETH/GITLAWB pool.{" "} + Your token carries the badge on the launch list, trending, the activity feed, its page and its share card. GITLAWB is Gitlawb's token on Base: an ordinary ERC-20, no transfer restrictions, no issuer switch. Name no beneficiary and every trading fee burns GITLAWB. Price from the Uniswap v4 WETH/GITLAWB pool.{" "} gitlawb.com ↗

@@ -726,7 +732,10 @@ export default function LaunchForm({ ethUsd, initialChain = "base" }: { ethUsd:
{name.trim() || "Your token"}
{symbolClean || "TICKER"}
- +
+ {quote.key === "gitlawb" ? : null} + +
{description.trim() ?

{description.trim()}

: null}
diff --git a/app/src/components/launchpad/LaunchRow.tsx b/app/src/components/launchpad/LaunchRow.tsx index 197907df..de263c72 100644 --- a/app/src/components/launchpad/LaunchRow.tsx +++ b/app/src/components/launchpad/LaunchRow.tsx @@ -8,6 +8,7 @@ import { fmtQuote, fmtUsd, pipsToPct } from "@/lib/launchpad/math"; import { CHAIN_SHORT } from "@/lib/chainPublic"; import { ago } from "@/lib/launchpad/time"; import ChangeChip from "./ChangeChip"; +import GitlawbBadge, { isGitlawbQuote } from "./GitlawbBadge"; import { marketUsd } from "@/lib/launchpad/market-format"; const columns = "md:grid-cols-[minmax(0,1fr)_6.5rem_5.5rem_6rem_5rem_2.5rem]"; @@ -49,6 +50,7 @@ export default function LaunchRow({ l, rank, window = "all", hl = null, now, pop
{l.name} + {isGitlawbQuote(l.quote_symbol) ? : null} {hl?.kind === "new" ? New : null}
diff --git a/app/src/components/launchpad/LaunchTape.tsx b/app/src/components/launchpad/LaunchTape.tsx index fe3a62a0..7ae5731c 100644 --- a/app/src/components/launchpad/LaunchTape.tsx +++ b/app/src/components/launchpad/LaunchTape.tsx @@ -5,6 +5,7 @@ import { useEffect, useRef, useState } from "react"; import { Activity, ArrowDownLeft, ArrowUpRight, Plus } from "lucide-react"; import { useLive } from "./LiveProvider"; import TokenAvatar from "./TokenAvatar"; +import GitlawbBadge, { isGitlawbQuote } from "./GitlawbBadge"; import type { FeedItem } from "@/lib/launchpad/queries"; import { fmtQuote } from "@/lib/launchpad/math"; import { CHAIN_SHORT, shortAddr } from "@/lib/chainPublic"; @@ -60,6 +61,7 @@ export default function LaunchTape({ initial }: { initial: FeedItem[] }) {
diff --git a/app/src/components/launchpad/MeDashboard.tsx b/app/src/components/launchpad/MeDashboard.tsx index 35b1153b..37cf3d35 100644 --- a/app/src/components/launchpad/MeDashboard.tsx +++ b/app/src/components/launchpad/MeDashboard.tsx @@ -8,6 +8,7 @@ import { getPublicClient, getWalletClient } from "wagmi/actions"; import type { Address } from "viem"; import TokenAvatar from "./TokenAvatar"; import ChainBadge from "./ChainBadge"; +import GitlawbBadge, { isGitlawbQuote } from "./GitlawbBadge"; import FeeChip, { feeModeOf } from "./FeeChip"; import EditTokenSheet from "./EditTokenSheet"; import { toast } from "./TxToasts"; @@ -246,6 +247,7 @@ function WalletDashboard({ address, isConnected }: { address: Address | undefine {l.name} {l.symbol} + {isGitlawbQuote(l.quote_symbol) ? : null}
@@ -295,6 +297,7 @@ function WalletDashboard({ address, isConnected }: { address: Address | undefine {t.name} {t.symbol} + {isGitlawbQuote(t.quote_symbol) ? : null}
{t.my_buys} buys · {t.my_sells} sells · mc {t.fdv_usd !== null ? fmtUsd(t.fdv_usd, { compact: true }) : "—"} diff --git a/app/src/components/launchpad/TrendingStrip.tsx b/app/src/components/launchpad/TrendingStrip.tsx index f42b6204..2a95dec3 100644 --- a/app/src/components/launchpad/TrendingStrip.tsx +++ b/app/src/components/launchpad/TrendingStrip.tsx @@ -4,6 +4,7 @@ import Link from "next/link"; import { useEffect, useRef, useState } from "react"; import { ArrowUpRight, ChartNoAxesCombined } from "lucide-react"; import TokenAvatar from "./TokenAvatar"; +import GitlawbBadge, { isGitlawbQuote } from "./GitlawbBadge"; import ChangeChip from "./ChangeChip"; import { useLive } from "./LiveProvider"; import type { LaunchRow } from "@/lib/launchpad/queries"; @@ -81,7 +82,7 @@ export default function TrendingStrip({ initial }: { initial: Snap }) {
0{index + 1} ranked - {CHAIN_SHORT[row.chain]} · {row.quote_symbol} + {isGitlawbQuote(row.quote_symbol) ? {CHAIN_SHORT[row.chain]} · : {CHAIN_SHORT[row.chain]} · {row.quote_symbol}}
diff --git a/app/src/lib/launchpad/queries.ts b/app/src/lib/launchpad/queries.ts index 9f39bef3..75b9f686 100644 --- a/app/src/lib/launchpad/queries.ts +++ b/app/src/lib/launchpad/queries.ts @@ -256,7 +256,7 @@ export async function getSwaps(chain: ChainKey, token: string, quoteDecimals: nu /** Home tape: launches + trades across chains, newest first. */ export type FeedItem = - | { kind: "launch"; chain: ChainKey; at: string; tx_hash: string; token: string; name: string; symbol: string; launcher: string; lp_fee: number; image_url: string | null } + | { kind: "launch"; chain: ChainKey; at: string; tx_hash: string; token: string; name: string; symbol: string; launcher: string; lp_fee: number; quote_symbol: string; image_url: string | null } | { kind: "swap"; chain: ChainKey; at: string; tx_hash: string; token: string; name: string; symbol: string; trader: string | null; is_buy: boolean; is_dev: boolean; quote_wei: string; quote_symbol: string; quote_decimals: number; usd: number | null; image_url: string | null }; export async function getLaunchFeed(limit = 24, ethUsd: number | null = null): Promise { @@ -276,7 +276,7 @@ export async function getLaunchFeed(limit = 24, ethUsd: number | null = null): P ) x ORDER BY at DESC LIMIT ${n}`; // each arm pre-limited on an indexed time column: the union never scans the whole swaps table return rows.map((r) => { const chain = chainKeyOf(r.chain_id) ?? "base"; - if (r.kind === "launch") return { kind: "launch", chain, at: r.at, tx_hash: r.tx_hash, token: r.token, name: r.name, symbol: r.symbol, launcher: r.who ?? "", lp_fee: r.lp_fee ?? 0, image_url: canonicalImageUrl(r.image_url, imagePublicBase()) }; + if (r.kind === "launch") return { kind: "launch", chain, at: r.at, tx_hash: r.tx_hash, token: r.token, name: r.name, symbol: r.symbol, launcher: r.who ?? "", lp_fee: r.lp_fee ?? 0, quote_symbol: quoteInfo(chain, r.quote).symbol, image_url: canonicalImageUrl(r.image_url, imagePublicBase()) }; const q = quoteInfo(chain, r.quote); const qu = quoteUsd(q, ethUsd); const wei = r.quote_wei ?? "0"; From 9d6df844b2afecf5d2d073f9860db4214071fdc5 Mon Sep 17 00:00:00 2001 From: Kevin Codex Date: Tue, 8 Sep 2026 16:25:03 +0800 Subject: [PATCH 03/10] review: scope GITLAWB SQL matches to Base, cap price staleness at 15 min - listLaunchesPage: the gitlawb filter and the USD factor CASE now require chain_id = Base, so a same-address token on another chain is never labelled or priced as GITLAWB - gitlawbServer: a last-good price survives transient RPC failures / out-of-window readings for at most 15 minutes; after that GITLAWB pools show no USD instead of a frozen price --- app/src/lib/launchpad/gitlawbServer.ts | 19 ++++++++++++++----- app/src/lib/launchpad/queries.ts | 5 +++-- 2 files changed, 17 insertions(+), 7 deletions(-) diff --git a/app/src/lib/launchpad/gitlawbServer.ts b/app/src/lib/launchpad/gitlawbServer.ts index e5566b55..2ac5e04c 100644 --- a/app/src/lib/launchpad/gitlawbServer.ts +++ b/app/src/lib/launchpad/gitlawbServer.ts @@ -7,12 +7,19 @@ import { GITLAWB_POOL_ID, gitlawbUsdFromSqrtPrice } from "./gitlawb"; /** * USD per GITLAWB, server-side: StateView.getSlot0(WETH/GITLAWB pool) × ETH/USD, cached 60s. - * Fails soft: a transient RPC error keeps the last good price; with none, GITLAWB-quoted launches - * simply show no USD (and rank with 0 weight in USD sorts) until the next successful read. + * Fails soft: a transient RPC error (or an out-of-window reading) keeps the last good price for up to + * 15 minutes; after that, GITLAWB-quoted launches simply show no USD (and rank with 0 weight in USD + * sorts) until the next successful read. */ -let cached: { at: number; usd: number | null } = { at: 0, usd: null }; +let cached: { at: number; usd: number | null; goodAt: number } = { at: 0, usd: null, goodAt: 0 }; let inflight: Promise | null = null; const TTL = 60_000; +/** A last-good price is reused through transient failures for at most this long; after that GITLAWB shows no USD. */ +const MAX_STALE = 15 * 60_000; + +function lastGood(now: number): number | null { + return now - cached.goodAt <= MAX_STALE ? cached.usd : null; +} async function read(): Promise { const [eth, slot0] = await Promise.all([ @@ -26,12 +33,14 @@ export async function gitlawbUsd(): Promise { if (Date.now() - cached.at < TTL) return cached.usd; inflight ??= read() .then((usd) => { - cached = { at: Date.now(), usd: usd ?? cached.usd }; + const now = Date.now(); + cached = usd !== null ? { at: now, usd, goodAt: now } : { at: now, usd: lastGood(now), goodAt: cached.goodAt }; return cached.usd; }) .catch((err) => { console.warn("[gitlawb] price read failed:", err instanceof Error ? err.message : err); - cached = { at: Date.now(), usd: cached.usd }; + const now = Date.now(); + cached = { at: now, usd: lastGood(now), goodAt: cached.goodAt }; return cached.usd; }) .finally(() => (inflight = null)); diff --git a/app/src/lib/launchpad/queries.ts b/app/src/lib/launchpad/queries.ts index 75b9f686..4e1edee5 100644 --- a/app/src/lib/launchpad/queries.ts +++ b/app/src/lib/launchpad/queries.ts @@ -161,6 +161,7 @@ export const VOLUME_WINDOWS: VolumeWindow[] = ["1h", "24h", "all"]; export type ListOpts = { sort?: LaunchSort; window?: VolumeWindow; chain?: ChainKey | null; filter?: LaunchFilter | null; limit?: number; offset?: number; launcher?: string; ethUsd?: number | null }; const USDG_ADDR = "0x5fc5360d0400a0fd4f2af552add042d716f1d168"; +const BASE_ID = chainIdOf("base"); // GITLAWB is a Base quote only: the same address on another chain is not GITLAWB const NATIVE_ADDR = "0x0000000000000000000000000000000000000000"; const DEAD_ADDR = "0x000000000000000000000000000000000000dead"; @@ -190,13 +191,13 @@ export async function listLaunchesPage(opts: ListOpts = {}): Promise { if (opts.filter === "fee0") conds.push(db`l.lp_fee = 0`); if (opts.filter === "burn") conds.push(db`l.lp_fee > 0 AND jsonb_array_length(l.recipients) = 1 AND lower(l.recipients->0->>'payout') = ${DEAD_ADDR}`); if (opts.filter === "usdg") conds.push(db`l.quote = ${USDG_ADDR}`); - if (opts.filter === "gitlawb") conds.push(db`l.quote = ${GITLAWB_ADDRESS}`); + if (opts.filter === "gitlawb") conds.push(db`l.chain_id = ${BASE_ID} AND l.quote = ${GITLAWB_ADDRESS}`); if (opts.filter === "today") conds.push(db`l.block_time > now() - interval '24 hours'`); const where = conds.length ? db`WHERE ${conds.reduce((a, c) => db`${a} AND ${c}`)}` : db``; // per-row USD factor and quote decimals (USDG is the only non-18-dec quote we list) const stockCase = stockEntries.length ? stockEntries.map(([a, v]) => db`WHEN l.quote = ${a} THEN ${v}::double precision`).reduce((acc, c) => db`${acc} ${c}`) : db``; const gitlawbFactor = gitlawbUsdNow !== null && gitlawbUsdNow > 0 ? gitlawbUsdNow : 0; // unknown → 0 weight, like an unknown stock - const usdPerUnit = db`(CASE WHEN l.quote = ${USDG_ADDR} THEN 1.0 ${stockCase} WHEN l.quote = ${GITLAWB_ADDRESS} THEN ${gitlawbFactor}::double precision WHEN l.quote = ${NATIVE_ADDR} THEN ${ethFactor}::double precision ELSE 0.0 END)`; + const usdPerUnit = db`(CASE WHEN l.quote = ${USDG_ADDR} THEN 1.0 ${stockCase} WHEN l.chain_id = ${BASE_ID} AND l.quote = ${GITLAWB_ADDRESS} THEN ${gitlawbFactor}::double precision WHEN l.quote = ${NATIVE_ADDR} THEN ${ethFactor}::double precision ELSE 0.0 END)`; const stockDec = stockEntries.map(([a]) => [a, stockByAddress("base", a)?.decimals ?? stockByAddress("robinhood", a)?.decimals ?? 18] as [string, number]).filter(([, d]) => d !== 18); const decCase = stockDec.length ? stockDec.map(([a, d]) => db`WHEN l.quote = ${a} THEN ${d}`).reduce((acc, c) => db`${acc} ${c}`) : db``; const qd = db`(CASE WHEN l.quote = ${USDG_ADDR} THEN 6 ${decCase} ELSE 18 END)`; From c15a0255ad037af83e3e47108feb6b79812c1275 Mon Sep 17 00:00:00 2001 From: Kevin Codex Date: Tue, 8 Sep 2026 16:34:18 +0800 Subject: [PATCH 04/10] GITLAWB badge: use the real Gitlawb logo The mark is now gitlawb.com's own logo.png (web main branch, byte-identical to the live site), trimmed and downscaled to public/gitlawb-mark.png (160px, 6KB) and served from our origin. Badge and tiles sit on the logo's black ground with white text; the share card embeds the PNG as a data URL. Drops the hand-drawn SVG mark. --- app/public/gitlawb-mark.png | Bin 0 -> 6041 bytes .../app/t/[chain]/[token]/opengraph-image.tsx | 16 ++++++++++-- app/src/components/launchpad/GitlawbBadge.tsx | 23 ++++++++---------- app/src/components/launchpad/LaunchForm.tsx | 2 +- app/src/lib/launchpad/config.ts | 4 +-- app/src/lib/launchpad/gitlawb.test.ts | 10 ++++---- app/src/lib/launchpad/gitlawb.ts | 13 ++++++---- 7 files changed, 40 insertions(+), 28 deletions(-) create mode 100644 app/public/gitlawb-mark.png diff --git a/app/public/gitlawb-mark.png b/app/public/gitlawb-mark.png new file mode 100644 index 0000000000000000000000000000000000000000..15c898872bef7e25d4801eb5d8ede6d1b92df285 GIT binary patch literal 6041 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 literal 0 HcmV?d00001 diff --git a/app/src/app/t/[chain]/[token]/opengraph-image.tsx b/app/src/app/t/[chain]/[token]/opengraph-image.tsx index 9139bb5d..8e30139b 100644 --- a/app/src/app/t/[chain]/[token]/opengraph-image.tsx +++ b/app/src/app/t/[chain]/[token]/opengraph-image.tsx @@ -11,6 +11,17 @@ import { BRAND, BRAND_TLD } from "@/lib/brand"; import { isOwnImageUrl } from "@/lib/launchpad/images"; import { imagePublicBase, readImage } from "@/lib/launchpad/imageStore"; import sharp from "sharp"; +import { readFile } from "node:fs/promises"; +import path from "node:path"; +import { GITLAWB_LOGO_BG, GITLAWB_LOGO_PATH } from "@/lib/launchpad/gitlawb"; + +/** Gitlawb's logo from public/, inlined once per process (satori needs a data URL or absolute URL). */ +let gitlawbLogo: Promise | null = null; +function gitlawbLogoDataUrl(): Promise { + return (gitlawbLogo ??= readFile(path.join(process.cwd(), "public", GITLAWB_LOGO_PATH)) + .then((b) => `data:image/png;base64,${b.toString("base64")}`) + .catch(() => null)); +} export const alt = "token on openlaunch.lol"; export const size = { width: 1200, height: 630 }; @@ -58,6 +69,7 @@ export default async function TokenOg({ params }: { params: Promise<{ chain: str const [usd, fonts] = await Promise.all([ethUsd(), loadOgFonts()]); const l = isChainKey(chain) && isAddress(token) ? await memo(`og:${chain}:${token.toLowerCase()}`, 30_000, () => getLaunch(chain, token, usd)) : null; const card = l ? shapeCard(l, nowMs()) : null; + const glLogo = card?.quote?.kind === "gitlawb" ? await gitlawbLogoDataUrl() : null; const logo = l ? await memo(`og-logo:${chain}:${token.toLowerCase()}`, 60_000, () => ownLogo(l.image_url)) : null; const h = hueOf(token); return new ImageResponse( @@ -107,8 +119,8 @@ export default async function TokenOg({ params }: { params: Promise<{ chain: str {card.quote ? ( {card.quote.kind === "gitlawb" ? ( - - + + {glLogo ? : null} ) : ( 4 ? 7 : card.quote.ticker.length > 3 ? 8 : 10, fontWeight: 800, letterSpacing: -0.5, whiteSpace: "nowrap", overflow: "hidden" }}> diff --git a/app/src/components/launchpad/GitlawbBadge.tsx b/app/src/components/launchpad/GitlawbBadge.tsx index 0bfde482..8c2465e6 100644 --- a/app/src/components/launchpad/GitlawbBadge.tsx +++ b/app/src/components/launchpad/GitlawbBadge.tsx @@ -1,27 +1,24 @@ -import { GITLAWB_SYMBOL } from "@/lib/launchpad/gitlawb"; +import { GITLAWB_LOGO_BG, GITLAWB_LOGO_PATH, GITLAWB_SYMBOL } from "@/lib/launchpad/gitlawb"; -/** The Gitlawb mark (ring + planet + satellite), inline so it renders in server and client components alike. */ -export function GitlawbMark({ size = 12, className = "" }: { size?: number; className?: string }) { - return ( - - ); +/** Gitlawb's logo (the real asset from gitlawb.com, served from our origin) as a small rounded tile. */ +export function GitlawbMark({ size = 16, className = "" }: { size?: number; className?: string }) { + // eslint-disable-next-line @next/next/no-img-element + return ; } /** * Badge for tokens paired with GITLAWB. Shown wherever a token appears (list, trending, feed, token page, - * dashboard, share card) — the visible perk of picking GITLAWB as the quote asset. + * dashboard, share card) — the visible perk of picking GITLAWB as the quote asset. Black like the logo's own + * ground, white text, a light hairline in dark mode so it still reads as a chip on a dark page. */ export default function GitlawbBadge({ size = "sm", className = "", label = GITLAWB_SYMBOL }: { size?: "sm" | "md"; className?: string; label?: string }) { return ( - + {label} ); diff --git a/app/src/components/launchpad/LaunchForm.tsx b/app/src/components/launchpad/LaunchForm.tsx index 75061748..e2947e12 100644 --- a/app/src/components/launchpad/LaunchForm.tsx +++ b/app/src/components/launchpad/LaunchForm.tsx @@ -421,7 +421,7 @@ export default function LaunchForm({ ethUsd, initialChain = "base" }: { ethUsd: {quote.logo ? ( // eslint-disable-next-line @next/next/no-img-element - + ) : null} {quote.symbol} {quote.name} diff --git a/app/src/lib/launchpad/config.ts b/app/src/lib/launchpad/config.ts index fa6eb93a..5d89e9d9 100644 --- a/app/src/lib/launchpad/config.ts +++ b/app/src/lib/launchpad/config.ts @@ -1,6 +1,6 @@ import { isAddress, type Address } from "viem"; import { CHAIN_KEYS, SITE_URL, type ChainKey } from "@/lib/chainPublic"; -import { GITLAWB_ADDRESS, GITLAWB_DECIMALS, GITLAWB_NAME, GITLAWB_SYMBOL, gitlawbTileSvg } from "./gitlawb"; +import { GITLAWB_ADDRESS, GITLAWB_DECIMALS, GITLAWB_LOGO_PATH, GITLAWB_NAME, GITLAWB_SYMBOL } from "./gitlawb"; /** * CLIENT-SAFE launchpad config, per chain. Only NEXT_PUBLIC_* vars are read here. @@ -21,7 +21,7 @@ export type ChainLaunchpad = { key: ChainKey; factory: Address | null; locker: A const ETH: Quote = { key: "eth", address: NATIVE, symbol: "ETH", decimals: 18, usd: null }; const USDG: Quote = { key: "usdg", address: "0x5fc5360D0400a0Fd4f2af552ADD042D716F1d168", symbol: "USDG", decimals: 6, usd: 1 }; /** GITLAWB on Base: usd is null here (client-safe static); the server fills the live price (gitlawbServer.ts). */ -const GITLAWB: Quote = { key: "gitlawb", address: GITLAWB_ADDRESS as Address, symbol: GITLAWB_SYMBOL, decimals: GITLAWB_DECIMALS, usd: null, name: GITLAWB_NAME, logo: gitlawbTileSvg() }; +const GITLAWB: Quote = { key: "gitlawb", address: GITLAWB_ADDRESS as Address, symbol: GITLAWB_SYMBOL, decimals: GITLAWB_DECIMALS, usd: null, name: GITLAWB_NAME, logo: GITLAWB_LOGO_PATH }; /** Canonical Uniswap v4 deployments (developers.uniswap.org/docs/protocols/v4/deployments). */ const V4_BY_CHAIN: Record = { diff --git a/app/src/lib/launchpad/gitlawb.test.ts b/app/src/lib/launchpad/gitlawb.test.ts index 93818f7a..a5896ee9 100644 --- a/app/src/lib/launchpad/gitlawb.test.ts +++ b/app/src/lib/launchpad/gitlawb.test.ts @@ -1,7 +1,8 @@ import { test } from "node:test"; import assert from "node:assert/strict"; import { encodeAbiParameters, keccak256 } from "viem"; -import { DYNAMIC_FEE_FLAG, GITLAWB_ADDRESS, GITLAWB_POOL_ID, GITLAWB_POOL_KEY, gitlawbMcapPresets, gitlawbTileSvg, gitlawbUsdFromSqrtPrice, isGitlawbAddress } from "./gitlawb.ts"; +import { existsSync } from "node:fs"; +import { DYNAMIC_FEE_FLAG, GITLAWB_ADDRESS, GITLAWB_LOGO_PATH, GITLAWB_POOL_ID, GITLAWB_POOL_KEY, gitlawbMcapPresets, gitlawbUsdFromSqrtPrice, isGitlawbAddress } from "./gitlawb.ts"; test("pool id = keccak256(abi.encode(poolKey)) for the WETH/GITLAWB dynamic-fee pool", () => { const k = GITLAWB_POOL_KEY; @@ -37,10 +38,9 @@ test("presets convert dollar targets into GITLAWB units; empty without a price", assert.deepEqual(p, [250_000_000, 500_000_000, 1_250_000_000, 5_000_000_000]); }); -test("tile is an inline SVG data URL; address check is case-insensitive", () => { - const t = gitlawbTileSvg(); - assert.ok(t.startsWith("data:image/svg+xml,")); - assert.ok(decodeURIComponent(t).includes(" { + assert.ok(GITLAWB_LOGO_PATH.startsWith("/"), "same-origin path"); + assert.ok(existsSync(new URL(`../../../public${GITLAWB_LOGO_PATH}`, import.meta.url)), "public/gitlawb-mark.png exists"); assert.ok(isGitlawbAddress("0x5F980Dcfc4c0fa3911554cf5ab288ed0eb13DBa3")); assert.ok(!isGitlawbAddress("0x5F980Dcfc4c0fa3911554cf5ab288ed0eb13DBa4")); }); diff --git a/app/src/lib/launchpad/gitlawb.ts b/app/src/lib/launchpad/gitlawb.ts index 3b6dbdb4..05582299 100644 --- a/app/src/lib/launchpad/gitlawb.ts +++ b/app/src/lib/launchpad/gitlawb.ts @@ -53,11 +53,14 @@ export function gitlawbMcapPresets(usdPrice: number | null, usdTargets = [5_000, return usdTargets.map((u) => Math.round(u / usdPrice)); } -/** Gitlawb mark (ring + planet + satellite) on a navy tile, as an inline data URL (same shape as the stock tiles). */ -export function gitlawbTileSvg(): string { - const svg = ``; - return `data:image/svg+xml,${encodeURIComponent(svg).replace(/%20/g, " ")}`; -} +/** + * Gitlawb's logo (the white branch-and-key mark on black), served from our own origin. + * Source: gitlawb.com's `public/logo.png` (web main branch, byte-identical to the live site), trimmed and + * downscaled to 160px in `public/gitlawb-mark.png`. + */ +export const GITLAWB_LOGO_PATH = "/gitlawb-mark.png"; +/** The logo's own ground — badges and tiles use the same colour so the mark never sits in a box of a different shade. */ +export const GITLAWB_LOGO_BG = "#000000"; export function isGitlawbAddress(address: string): boolean { return address.toLowerCase() === GITLAWB_ADDRESS; From 8c53c98533c8eae6eb534b32b9317a775e13f291 Mon Sep 17 00:00:00 2001 From: Kevin Codex Date: Tue, 8 Sep 2026 16:35:56 +0800 Subject: [PATCH 05/10] GITLAWB badge: bigger mark, tighter pill (18px in 22px, 22px in 28px) --- app/src/components/launchpad/GitlawbBadge.tsx | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/app/src/components/launchpad/GitlawbBadge.tsx b/app/src/components/launchpad/GitlawbBadge.tsx index 8c2465e6..4c562b0b 100644 --- a/app/src/components/launchpad/GitlawbBadge.tsx +++ b/app/src/components/launchpad/GitlawbBadge.tsx @@ -14,11 +14,11 @@ export function GitlawbMark({ size = 16, className = "" }: { size?: number; clas export default function GitlawbBadge({ size = "sm", className = "", label = GITLAWB_SYMBOL }: { size?: "sm" | "md"; className?: string; label?: string }) { return ( - + {label} ); From 81e53e81573ab9648918bafc068f81d2b28c111d Mon Sep 17 00:00:00 2001 From: Kevin Codex Date: Tue, 8 Sep 2026 16:39:18 +0800 Subject: [PATCH 06/10] GITLAWB badge: planet logo as a navy PNG tile public/gitlawb-mark.png is now generated from the web repo's planet logo: the mark recoloured white, the blue satellite kept, cropped and squared on a navy ground with rounded corners baked into the alpha. Badge pill is the same navy so tile and pill read as one piece. --- app/public/gitlawb-mark.png | Bin 6041 -> 7240 bytes app/scripts/smoke.sh | 2 +- app/src/components/launchpad/GitlawbBadge.tsx | 10 +++++----- app/src/lib/launchpad/gitlawb.ts | 10 +++++----- 4 files changed, 11 insertions(+), 11 deletions(-) diff --git a/app/public/gitlawb-mark.png b/app/public/gitlawb-mark.png index 15c898872bef7e25d4801eb5d8ede6d1b92df285..0a7df3953a2e275135491b064bd6f77f27f78554 100644 GIT binary patch literal 7240 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 diff --git a/app/scripts/smoke.sh b/app/scripts/smoke.sh index a6009309..b9d0ecd1 100755 --- a/app/scripts/smoke.sh +++ b/app/scripts/smoke.sh @@ -4,7 +4,7 @@ set -u B="${1:-https://openlaunch.lol}" fail=0 check() { code=$(curl -s -o /dev/null -w '%{http_code}' "$B$1"); ok=0; case "$1" in /api/launch/meta/*) { [ "$code" = 200 ] || [ "$code" = 404 ]; } && ok=1;; /t/0x*) { [ "$code" = 307 ] || [ "$code" = 308 ]; } && ok=1;; *) [ "$code" = 200 ] && ok=1;; esac; [ $ok = 1 ] && echo " ok $code $1" || { echo " FAIL $code $1"; fail=1; }; } -for p in / /me /feed "/api/posts?feed=1" "/api/quotes?chain=robinhood&q=AAPL" "/?chain=robinhood" /launch "/launch?chain=robinhood" /rules /agents /llms.txt /api/health "/api/launch/live?sort=trending&window=1h" "/api/launch/live?sort=new&chain=robinhood" /api/launch/feed /api/presence; do check "$p"; done +for p in / /me /feed "/api/posts?feed=1" "/api/quotes?chain=robinhood&q=AAPL" "/api/quotes?chain=base" "/?filter=gitlawb" /gitlawb-mark.png "/?chain=robinhood" /launch "/launch?chain=robinhood" /rules /agents /llms.txt /api/health "/api/launch/live?sort=trending&window=1h" "/api/launch/live?sort=new&chain=robinhood" /api/launch/feed /api/presence; do check "$p"; done for c in base robinhood; do T=$(curl -s "$B/api/launch/list?chain=$c&limit=1" | python3 -c 'import json,sys; l=json.load(sys.stdin)["launches"]; print(l[0]["token"] if l else "")') if [ -n "$T" ]; then check "/t/$c/$T"; check "/t/$c/$T/opengraph-image"; check "/api/launch/meta/$T?chain=$c"; check "/t/$T"; else echo " skip no launches on $c"; fi diff --git a/app/src/components/launchpad/GitlawbBadge.tsx b/app/src/components/launchpad/GitlawbBadge.tsx index 4c562b0b..7f0e76d3 100644 --- a/app/src/components/launchpad/GitlawbBadge.tsx +++ b/app/src/components/launchpad/GitlawbBadge.tsx @@ -1,20 +1,20 @@ import { GITLAWB_LOGO_BG, GITLAWB_LOGO_PATH, GITLAWB_SYMBOL } from "@/lib/launchpad/gitlawb"; -/** Gitlawb's logo (the real asset from gitlawb.com, served from our origin) as a small rounded tile. */ +/** Gitlawb's logo tile (navy, white mark, blue satellite; transparent corners baked in), served from our origin. */ export function GitlawbMark({ size = 16, className = "" }: { size?: number; className?: string }) { // eslint-disable-next-line @next/next/no-img-element - return ; + return ; } /** * Badge for tokens paired with GITLAWB. Shown wherever a token appears (list, trending, feed, token page, - * dashboard, share card) — the visible perk of picking GITLAWB as the quote asset. Black like the logo's own - * ground, white text, a light hairline in dark mode so it still reads as a chip on a dark page. + * dashboard, share card) — the visible perk of picking GITLAWB as the quote asset. Navy like the logo tile, + * white text, a light hairline in dark mode so it still reads as a chip on a dark page. */ export default function GitlawbBadge({ size = "sm", className = "", label = GITLAWB_SYMBOL }: { size?: "sm" | "md"; className?: string; label?: string }) { return ( diff --git a/app/src/lib/launchpad/gitlawb.ts b/app/src/lib/launchpad/gitlawb.ts index 05582299..311ff0b1 100644 --- a/app/src/lib/launchpad/gitlawb.ts +++ b/app/src/lib/launchpad/gitlawb.ts @@ -54,13 +54,13 @@ export function gitlawbMcapPresets(usdPrice: number | null, usdTargets = [5_000, } /** - * Gitlawb's logo (the white branch-and-key mark on black), served from our own origin. - * Source: gitlawb.com's `public/logo.png` (web main branch, byte-identical to the live site), trimmed and - * downscaled to 160px in `public/gitlawb-mark.png`. + * Gitlawb's logo (planet, orbit ring, blue satellite) as a navy rounded tile with the mark in white, served + * from our own origin. Generated from the web repo's `public/logo.png` by recolouring the mark and cropping + * (see the commit that added `public/gitlawb-mark.png`); 160px, transparent corners. */ export const GITLAWB_LOGO_PATH = "/gitlawb-mark.png"; -/** The logo's own ground — badges and tiles use the same colour so the mark never sits in a box of a different shade. */ -export const GITLAWB_LOGO_BG = "#000000"; +/** The tile's ground — badges use the same navy so the tile and the pill read as one piece. */ +export const GITLAWB_LOGO_BG = "#0a1020"; export function isGitlawbAddress(address: string): boolean { return address.toLowerCase() === GITLAWB_ADDRESS; From bc3034f2a1b8789648edc8eaadc410cd3ec75d05 Mon Sep 17 00:00:00 2001 From: Kevin Codex Date: Tue, 8 Sep 2026 16:44:29 +0800 Subject: [PATCH 07/10] GITLAWB: review pass + final logo tile MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Logo: public/gitlawb-mark.png is Gitlawb's branch-and-key mark (web main logo.png, as on gitlawb.com) with strokes boldened a touch so they survive 18–22px badges, cropped to the mark, 160px, transparent rounded corners. Badge is the same black with white text. Review fixes (automated review + own pass): - gitlawbServer: stale-while-revalidate (fresh 60s, background refresh, only a cold process waits, capped at 4s; 8s read timeout; last-good kept 15 min) so the price read never sits on a request's critical path; decoupled from the stock lookup in withStocks() - gitlawb.ts: pool id derived via poolIdOf (test pins it to the on-chain id), price via sqrtPriceToTokensPerQuote, presets via stockMcapPresets; implausible readings are used but logged instead of silently dropping the price - rows carry quote_key (LaunchRow, FeedItem); badge, list filter and share-card pill branch on it instead of on the display symbol - chain-specific filter chips (USDG, GITLAWB) hidden when another chain is selected; switching chain drops a filter that no longer applies - token page blurb branches on the typed fee mode (0% pools no longer claim fees are paid) - launch page passes gitlawbUsd server-side (no client fetch, no double error while loading) - one 18-dec formatter (fmtQuoteUnits) shared by fmtQuote, TradePanel and LaunchForm: ETH-style below 100K units, compact above (999,999 → 1M), stables 2dp - smoke.sh covers /api/quotes?chain=base, the gitlawb filter and the logo asset --- app/public/gitlawb-mark.png | Bin 7240 -> 5665 bytes app/src/app/launch/page.tsx | 5 +- app/src/app/t/[chain]/[token]/page.tsx | 2 +- app/src/components/launchpad/GitlawbBadge.tsx | 12 +-- app/src/components/launchpad/LaunchForm.tsx | 34 ++------- app/src/components/launchpad/LaunchList.tsx | 4 +- app/src/components/launchpad/LaunchRow.tsx | 2 +- app/src/components/launchpad/LaunchTape.tsx | 2 +- app/src/components/launchpad/MeDashboard.tsx | 4 +- app/src/components/launchpad/TradePanel.tsx | 12 +-- .../components/launchpad/TrendingStrip.tsx | 2 +- app/src/lib/launchpad/creator.test.ts | 8 +- app/src/lib/launchpad/gitlawb.test.ts | 28 +++---- app/src/lib/launchpad/gitlawb.ts | 59 +++++++------- app/src/lib/launchpad/gitlawbServer.ts | 72 ++++++++++-------- app/src/lib/launchpad/math.test.ts | 13 +++- app/src/lib/launchpad/math.ts | 19 ++++- app/src/lib/launchpad/ogcard.ts | 12 +-- app/src/lib/launchpad/queries.ts | 17 +++-- app/src/lib/launchpad/search.test.ts | 6 +- app/src/lib/launchpad/search.ts | 13 ++-- 21 files changed, 169 insertions(+), 157 deletions(-) diff --git a/app/public/gitlawb-mark.png b/app/public/gitlawb-mark.png index 0a7df3953a2e275135491b064bd6f77f27f78554..3277a571bdcbc4d1182527bf97be94eb2c5fc5d8 100644 GIT binary patch literal 5665 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 Hu0mjf;i1zw literal 7240 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 Wo~;fm;g0YC0000 }) { const sp = await searchParams; - const usd = await ethUsd(); + const [usd, gitlawb] = await Promise.all([ethUsd(), gitlawbUsd()]); return ( <>
@@ -20,7 +21,7 @@ export default async function LaunchPage({ searchParams }: { searchParams: Promi

Launch a token

Pick a chain, fill this in, sign once, done. No platform fee. You only pay gas.

- +
); diff --git a/app/src/app/t/[chain]/[token]/page.tsx b/app/src/app/t/[chain]/[token]/page.tsx index 8e3c6718..7d3b065b 100644 --- a/app/src/app/t/[chain]/[token]/page.tsx +++ b/app/src/app/t/[chain]/[token]/page.tsx @@ -142,7 +142,7 @@ export default async function TokenPage({ params }: { params: Promise<{ chain: s
{stockQuote ?

Paired with {stockQuote.name} ({stockQuote.symbol}), a third-party tokenized stock. These securities are not offered to US persons. The quote asset is identified from the issuer registry, not its token name.

: null} - {quote.key === "gitlawb" ?

Paired with GITLAWB, Gitlawb's token on Base. Trading fees on this pool are paid in GITLAWB{feeRoute.includes("burned") ? ", and burned" : ""}. USD figures use the Uniswap v4 WETH/GITLAWB pool price.

: null} + {quote.key === "gitlawb" ?

Paired with GITLAWB, Gitlawb's token on Base. {mode === "free" ? "This pool has no trading fee." : mode === "burn" ? "Every trading fee on this pool is burned as GITLAWB." : "Trading fees on this pool are paid out in GITLAWB."} USD figures use the Uniswap v4 WETH/GITLAWB pool price.

: null} } /> diff --git a/app/src/components/launchpad/GitlawbBadge.tsx b/app/src/components/launchpad/GitlawbBadge.tsx index 7f0e76d3..b81ad0ff 100644 --- a/app/src/components/launchpad/GitlawbBadge.tsx +++ b/app/src/components/launchpad/GitlawbBadge.tsx @@ -1,6 +1,6 @@ import { GITLAWB_LOGO_BG, GITLAWB_LOGO_PATH, GITLAWB_SYMBOL } from "@/lib/launchpad/gitlawb"; -/** Gitlawb's logo tile (navy, white mark, blue satellite; transparent corners baked in), served from our origin. */ +/** Gitlawb's logo tile (black, white branch-and-key mark; transparent corners baked in), served from our origin. */ export function GitlawbMark({ size = 16, className = "" }: { size?: number; className?: string }) { // eslint-disable-next-line @next/next/no-img-element return ; @@ -8,13 +8,13 @@ export function GitlawbMark({ size = 16, className = "" }: { size?: number; clas /** * Badge for tokens paired with GITLAWB. Shown wherever a token appears (list, trending, feed, token page, - * dashboard, share card) — the visible perk of picking GITLAWB as the quote asset. Navy like the logo tile, + * dashboard, share card) — the visible perk of picking GITLAWB as the quote asset. Black like the logo tile, * white text, a light hairline in dark mode so it still reads as a chip on a dark page. */ export default function GitlawbBadge({ size = "sm", className = "", label = GITLAWB_SYMBOL }: { size?: "sm" | "md"; className?: string; label?: string }) { return ( @@ -24,7 +24,7 @@ export default function GitlawbBadge({ size = "sm", className = "", label = GITL ); } -/** True for a launch whose quote is GITLAWB (quote symbols come from the server registry, never from on-chain names). */ -export function isGitlawbQuote(quoteSymbol: string | null | undefined): boolean { - return quoteSymbol === GITLAWB_SYMBOL; +/** True for a row whose server-resolved quote key is GITLAWB (keys come from the quote address, never from on-chain names). */ +export function isGitlawbQuote(quoteKey: string | null | undefined): boolean { + return quoteKey === "gitlawb"; } diff --git a/app/src/components/launchpad/LaunchForm.tsx b/app/src/components/launchpad/LaunchForm.tsx index e2947e12..d1e18cf7 100644 --- a/app/src/components/launchpad/LaunchForm.tsx +++ b/app/src/components/launchpad/LaunchForm.tsx @@ -13,7 +13,7 @@ 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 { fdvForStartTick, fmtCompact, fmtUsd, initialBuyPreview, minOut, startTickForFdv, tickToTokensPerQuote, units } from "@/lib/launchpad/math"; +import { fdvForStartTick, fmtCompact, fmtQuoteUnits, fmtUsd, initialBuyPreview, minOut, startTickForFdv, tickToTokensPerQuote, units } from "@/lib/launchpad/math"; import { encodeV4ExactInSingle, type PoolKey } from "@/lib/launchpad/swap"; import { stockMcapPresets } from "@/lib/launchpad/stocks"; import { GITLAWB_SITE, gitlawbMcapPresets } from "@/lib/launchpad/gitlawb"; @@ -123,7 +123,7 @@ async function firstBuy(ctx: FirstBuyCtx, tokenAddr: Address, launchHash: Hex, a return received > 0n ? { hash: h, out: received, exact: true } : { hash: h, out, exact: false }; } -export default function LaunchForm({ ethUsd, initialChain = "base" }: { ethUsd: number | null; initialChain?: ChainKey }) { +export default function LaunchForm({ ethUsd, gitlawbUsd = null, initialChain = "base" }: { ethUsd: number | null; gitlawbUsd?: number | null; initialChain?: ChainKey }) { const router = useRouter(); const [chain, setChain] = useState(initialChain); const cfg = launchpad(chain); @@ -133,28 +133,10 @@ export default function LaunchForm({ ethUsd, initialChain = "base" }: { ethUsd: const [stock, setStock] = useState(null); const [stockQ, setStockQ] = useState(""); const [stockHits, setStockHits] = useState([]); - // GITLAWB's USD is live (server reads the v4 pool); the static config carries null - const [gitlawbUsd, setGitlawbUsd] = useState(null); + // GITLAWB's USD is live: the page fetched it server-side (same as ethUsd); the static config carries null const staticQuote: Quote = quoteKey === "stock" && stock ? stock : (cfg.quotes.find((q) => q.key === quoteKey) ?? cfg.quotes[0]); const quote: Quote = staticQuote.key === "gitlawb" ? { ...staticQuote, usd: gitlawbUsd } : staticQuote; const quoteUsd = quoteUsdOf(quote, ethUsd); - useEffect(() => { - if (quoteKey !== "gitlawb") return; - let alive = true; - (async () => { - try { - const res = await fetch(`/api/quotes?chain=${chain}`, { cache: "no-store" }); - const d = (await res.json()) as { fixed?: Quote[] }; - const usd = d.fixed?.find((q) => q.key === "gitlawb")?.usd ?? null; - if (alive) setGitlawbUsd(usd); - } catch { - if (alive) setGitlawbUsd(null); - } - })(); - return () => { - alive = false; - }; - }, [chain, quoteKey]); // stock search (per-chain registry via our server; only registry addresses are ever offered) useEffect(() => { if (quoteKey !== "stock") return; @@ -203,7 +185,7 @@ export default function LaunchForm({ ethUsd, initialChain = "base" }: { ethUsd: 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 === "gitlawb" ? `${fmtCompact(v, 1)} ${quote.symbol}` : `${v.toFixed(quote.key === "stock" ? 3 : 2)} ${quote.symbol}`); + 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 symbolClean = symbol.trim().toUpperCase(); const initialBuyRaw = parseBuyAmount(initialBuy, quote.decimals); @@ -212,7 +194,7 @@ export default function LaunchForm({ ethUsd, initialChain = "base" }: { ethUsd: if (!/^[A-Z0-9]{1,10}$/.test(symbolClean)) errors.push("Symbol: 1–10 letters or digits."); if (startTick === null) errors.push("Starting market cap must be a positive number."); if (quoteKey === "stock" && !stock) errors.push("Pick a stock to price the token in."); - if (quoteKey === "gitlawb" && quote.usd === null && !customMcap.trim()) errors.push("GITLAWB price unavailable right now: enter a custom starting market cap in GITLAWB, or retry."); + if (quoteKey === "gitlawb" && quote.usd === null && !customMcap.trim() && startTick === null) errors.push("GITLAWB price unavailable right now: enter a custom starting market cap in GITLAWB, or reload."); if (image && !/^https:\/\//.test(image.trim())) errors.push("Image must be an https URL."); if (website && !/^https:\/\//.test(website.trim())) errors.push("Website must be an https URL."); if (feePips > 0 && beneficiary === "custom" && !isAddress(customAddr.trim())) errors.push("Beneficiary: enter a valid address."); @@ -425,7 +407,7 @@ export default function LaunchForm({ ethUsd, initialChain = "base" }: { ethUsd: ) : null} {quote.symbol} {quote.name} - {quote.usd ? {fmtUsd(quote.usd)} : price loading…} + {quote.usd ? {fmtUsd(quote.usd)} : price unavailable}

Your token carries the badge on the launch list, trending, the activity feed, its page and its share card. GITLAWB is Gitlawb's token on Base: an ordinary ERC-20, no transfer restrictions, no issuer switch. Name no beneficiary and every trading fee burns GITLAWB. Price from the Uniswap v4 WETH/GITLAWB pool.{" "} @@ -659,7 +641,7 @@ export default function LaunchForm({ ethUsd, initialChain = "base" }: { ethUsd: onClick={() => setInitialBuy(active ? "" : v)} 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 === "gitlawb" ? fmtCompact(Number(v), 0) : v} {quote.symbol} + {fmtQuoteUnits(Number(v), quote.decimals)} {quote.symbol} ); })} @@ -675,7 +657,7 @@ export default function LaunchForm({ ethUsd, initialChain = "base" }: { ethUsd: {quote.symbol} {initialBuyRaw && buyBalance !== undefined ? ( - balance {fmtCompact(units(buyBalance, quote.decimals), quote.decimals <= 8 ? 2 : 4)} + balance {fmtQuoteUnits(units(buyBalance, quote.decimals), quote.decimals)} ) : null} {buyPreview ? ( diff --git a/app/src/components/launchpad/LaunchList.tsx b/app/src/components/launchpad/LaunchList.tsx index 6a797861..ff98c3f2 100644 --- a/app/src/components/launchpad/LaunchList.tsx +++ b/app/src/components/launchpad/LaunchList.tsx @@ -238,7 +238,7 @@ export default function LaunchList({ initial, initialHasMore = false, initialSor

- { const value = values[0]; if (value) pick(sort, window_, value === "all" ? null : value as ChainKey); }}> + { const value = values[0]; if (!value) return; const c = value === "all" ? null : (value as ChainKey); const keep = !filter || FILTERS.some((f) => f.key === filter && (!f.chain || !c || f.chain === c)); pick(sort, window_, c, keep ? filter : null); }}> {CHAIN_FILTERS.map((c) => {c.label})} {showWindow ? { if (values[0]) pick(sort, values[0] as VolumeWindow); }}> @@ -248,7 +248,7 @@ export default function LaunchList({ initial, initialHasMore = false, initialSor
diff --git a/app/src/components/launchpad/LaunchRow.tsx b/app/src/components/launchpad/LaunchRow.tsx index de263c72..a63596b0 100644 --- a/app/src/components/launchpad/LaunchRow.tsx +++ b/app/src/components/launchpad/LaunchRow.tsx @@ -50,7 +50,7 @@ export default function LaunchRow({ l, rank, window = "all", hl = null, now, pop
{l.name} - {isGitlawbQuote(l.quote_symbol) ? : null} + {isGitlawbQuote(l.quote_key) ? : null} {hl?.kind === "new" ? New : null}
diff --git a/app/src/components/launchpad/LaunchTape.tsx b/app/src/components/launchpad/LaunchTape.tsx index 7ae5731c..78d717b4 100644 --- a/app/src/components/launchpad/LaunchTape.tsx +++ b/app/src/components/launchpad/LaunchTape.tsx @@ -61,7 +61,7 @@ export default function LaunchTape({ initial }: { initial: FeedItem[] }) {
diff --git a/app/src/components/launchpad/MeDashboard.tsx b/app/src/components/launchpad/MeDashboard.tsx index 37cf3d35..162a9b50 100644 --- a/app/src/components/launchpad/MeDashboard.tsx +++ b/app/src/components/launchpad/MeDashboard.tsx @@ -247,7 +247,7 @@ function WalletDashboard({ address, isConnected }: { address: Address | undefine {l.name} {l.symbol} - {isGitlawbQuote(l.quote_symbol) ? : null} + {isGitlawbQuote(l.quote_key) ? : null}
@@ -297,7 +297,7 @@ function WalletDashboard({ address, isConnected }: { address: Address | undefine {t.name} {t.symbol} - {isGitlawbQuote(t.quote_symbol) ? : null} + {isGitlawbQuote(t.quote_key) ? : null}
{t.my_buys} buys · {t.my_sells} sells · mc {t.fdv_usd !== null ? fmtUsd(t.fdv_usd, { compact: true }) : "—"} diff --git a/app/src/components/launchpad/TradePanel.tsx b/app/src/components/launchpad/TradePanel.tsx index f97aaa4a..efdf60ef 100644 --- a/app/src/components/launchpad/TradePanel.tsx +++ b/app/src/components/launchpad/TradePanel.tsx @@ -11,7 +11,7 @@ import { btn } from "@/components/ui"; import { toast } from "./TxToasts"; import { ERC20_MIN_ABI, PERMIT2_ABI, UNIVERSAL_ROUTER_ABI, V4_QUOTER_ABI } from "@/lib/launchpad/abi"; import { BUY_PRESETS, launchpad, quoteUsdOf, type Quote } from "@/lib/launchpad/config"; -import { fmtCompact, fmtUsd, minOut, units, pipsToPct } from "@/lib/launchpad/math"; +import { fmtCompact, fmtQuoteUnits, fmtUsd, minOut, units, pipsToPct } from "@/lib/launchpad/math"; import { encodeV4ExactInSingle, type PoolKey } from "@/lib/launchpad/swap"; import { CHAINS, CHAIN_LABELS, BUILDER_DATA_SUFFIX, explorerTx, type ChainKey } from "@/lib/chainPublic"; import { tradeQuoteKey } from "@/lib/launchpad/token-market"; @@ -38,12 +38,6 @@ type Phase = const SLIPPAGE_BPS = 100; // 1% const PERMIT_EXPIRY_S = 30 * 24 * 3600; -function fmtEthLocal(v: number): string { - if (!Number.isFinite(v) || v === 0) return "0"; - const abs = Math.abs(v); - const digits = abs >= 100 ? 1 : abs >= 1 ? 3 : abs >= 0.01 ? 4 : abs >= 0.0001 ? 6 : 8; - return v.toFixed(digits).replace(/\.?0+$/, ""); -} export default function TradePanel({ chain, token, symbol, poolKey, quote, ethUsd, onTraded }: { chain: ChainKey; token: Address; symbol: string; poolKey: PoolKey; quote: Quote; ethUsd: number | null; onTraded?: () => void }) { const CHAIN = CHAINS[chain]; @@ -52,7 +46,7 @@ export default function TradePanel({ chain, token, symbol, poolKey, quote, ethUs const configured = launchpad(chain).configured; const isNative = quote.key === "eth"; const quoteUsd = quoteUsdOf(quote, ethUsd); - const fmtQ = (raw: bigint) => `${quote.decimals <= 6 ? units(raw, quote.decimals).toFixed(2).replace(/\.00$/, "") : fmtEthLocal(units(raw, quote.decimals))} ${quote.symbol}`; + const fmtQ = (raw: bigint) => `${fmtQuoteUnits(units(raw, quote.decimals), quote.decimals)} ${quote.symbol}`; const router = useRouter(); const config = useConfig(); const { address, isConnected, chainId } = useAccount(); @@ -200,7 +194,7 @@ export default function TradePanel({ chain, token, symbol, poolKey, quote, ethUs {side === "buy" ? quote.symbol : symbol}
- {side === "buy" ? BUY_PRESETS[quote.key].map((p) => ) : [25, 50, 100].map((pct) => )} + {side === "buy" ? BUY_PRESETS[quote.key].map((p) => ) : [25, 50, 100].map((pct) => )}
diff --git a/app/src/components/launchpad/TrendingStrip.tsx b/app/src/components/launchpad/TrendingStrip.tsx index 2a95dec3..9963922a 100644 --- a/app/src/components/launchpad/TrendingStrip.tsx +++ b/app/src/components/launchpad/TrendingStrip.tsx @@ -82,7 +82,7 @@ export default function TrendingStrip({ initial }: { initial: Snap }) {
0{index + 1} ranked - {isGitlawbQuote(row.quote_symbol) ? {CHAIN_SHORT[row.chain]} · : {CHAIN_SHORT[row.chain]} · {row.quote_symbol}} + {isGitlawbQuote(row.quote_key) ? {CHAIN_SHORT[row.chain]} · : {CHAIN_SHORT[row.chain]} · {row.quote_symbol}}
diff --git a/app/src/lib/launchpad/creator.test.ts b/app/src/lib/launchpad/creator.test.ts index d7adb0db..eb763109 100644 --- a/app/src/lib/launchpad/creator.test.ts +++ b/app/src/lib/launchpad/creator.test.ts @@ -58,14 +58,14 @@ test("isNonce accepts 32 hex chars only", () => { test("share card shaping", () => { const now = Date.parse("2026-09-06T12:00:00Z"); - const c = shapeCard({ name: "Dollar Dog", symbol: "DDOG", chain: "robinhood", fdv_usd: 12345, fdv_quote: 12345, quote_symbol: "USDG", change_from_launch: 0.234, lp_fee: 10000, recipients: [{ payout: DEAD, bps: 10000 }], block_time: "2026-09-06T09:30:00Z" }, now); + const c = shapeCard({ name: "Dollar Dog", symbol: "DDOG", chain: "robinhood", fdv_usd: 12345, fdv_quote: 12345, quote_key: "usdg", quote_symbol: "USDG", change_from_launch: 0.234, lp_fee: 10000, recipients: [{ payout: DEAD, bps: 10000 }], block_time: "2026-09-06T09:30:00Z" }, now); assert.deepEqual(c, { title: "Dollar Dog", symbol: "DDOG", chainLabel: "Robinhood Chain", mcap: "$12.3K", change: "+23%", up: true, fee: "1% fee, burned", age: "2h old", quote: null }); - assert.deepEqual(shapeCard({ name: "Base Stock Test", symbol: "BSTK", chain: "base", fdv_usd: 25206, fdv_quote: 109.6, quote_symbol: "NVDAc", change_from_launch: 0, lp_fee: 10000, recipients: [{ payout: "0x00000000000000000000000000000000000c0ffe", bps: 10000 }], block_time: "2026-09-06T09:30:00Z" }, now).quote, { symbol: "NVDAc", ticker: "NVDA", kind: "stock" }, "stock quotes get a ticker tile on the card"); - assert.deepEqual(shapeCard({ name: "Lawb Fan", symbol: "LAWB", chain: "base", fdv_usd: 9800, fdv_quote: 580_000_000, quote_symbol: "GITLAWB", change_from_launch: 0, lp_fee: 10000, recipients: [], block_time: "2026-09-06T09:30:00Z" }, now).quote, { symbol: "GITLAWB", ticker: "GL", kind: "gitlawb" }, "GITLAWB quotes get the Gitlawb mark"); + assert.deepEqual(shapeCard({ name: "Base Stock Test", symbol: "BSTK", chain: "base", fdv_usd: 25206, fdv_quote: 109.6, quote_key: "stock", quote_symbol: "NVDAc", change_from_launch: 0, lp_fee: 10000, recipients: [{ payout: "0x00000000000000000000000000000000000c0ffe", bps: 10000 }], block_time: "2026-09-06T09:30:00Z" }, now).quote, { symbol: "NVDAc", ticker: "NVDA", kind: "stock" }, "stock quotes get a ticker tile on the card"); + assert.deepEqual(shapeCard({ name: "Lawb Fan", symbol: "LAWB", chain: "base", fdv_usd: 9800, fdv_quote: 580_000_000, quote_key: "gitlawb", quote_symbol: "GITLAWB", change_from_launch: 0, lp_fee: 10000, recipients: [], block_time: "2026-09-06T09:30:00Z" }, now).quote, { symbol: "GITLAWB", ticker: "GL", kind: "gitlawb" }, "GITLAWB quotes get the Gitlawb mark"); assert.equal(feeLabel(0, []), "0% fee"); assert.equal(feeLabel(30000, [{ payout: "0x1", bps: 10000 }]), "3% fee → beneficiary"); assert.equal(ageLabel("2026-09-06T11:59:30Z", now), "1m old"); - assert.equal(shapeCard({ name: "X", symbol: "X", chain: "base", fdv_usd: null, fdv_quote: 2.5, quote_symbol: "ETH", change_from_launch: -0.5, lp_fee: 0, recipients: [], block_time: "2026-09-01T00:00:00Z" }, now).mcap, "2.50 ETH"); + assert.equal(shapeCard({ name: "X", symbol: "X", chain: "base", fdv_usd: null, fdv_quote: 2.5, quote_key: "eth", quote_symbol: "ETH", change_from_launch: -0.5, lp_fee: 0, recipients: [], block_time: "2026-09-01T00:00:00Z" }, now).mcap, "2.50 ETH"); }); test("clampSocial: ≤125 chars on a word boundary with an ellipsis; short text untouched; whitespace collapsed", () => { diff --git a/app/src/lib/launchpad/gitlawb.test.ts b/app/src/lib/launchpad/gitlawb.test.ts index a5896ee9..e4a82ac5 100644 --- a/app/src/lib/launchpad/gitlawb.test.ts +++ b/app/src/lib/launchpad/gitlawb.test.ts @@ -1,41 +1,37 @@ import { test } from "node:test"; import assert from "node:assert/strict"; -import { encodeAbiParameters, keccak256 } from "viem"; import { existsSync } from "node:fs"; -import { DYNAMIC_FEE_FLAG, GITLAWB_ADDRESS, GITLAWB_LOGO_PATH, GITLAWB_POOL_ID, GITLAWB_POOL_KEY, gitlawbMcapPresets, gitlawbUsdFromSqrtPrice, isGitlawbAddress } from "./gitlawb.ts"; +import { GITLAWB_ADDRESS, GITLAWB_LOGO_PATH, GITLAWB_POOL_ID, GITLAWB_POOL_KEY, gitlawbMcapPresets, gitlawbUsdFromSqrtPrice, isGitlawbAddress } from "./gitlawb.ts"; -test("pool id = keccak256(abi.encode(poolKey)) for the WETH/GITLAWB dynamic-fee pool", () => { +test("the derived pool id is the on-chain WETH/GITLAWB pool (Initialize at Base block 43,202,530)", () => { const k = GITLAWB_POOL_KEY; - const id = keccak256( - encodeAbiParameters( - [{ type: "address" }, { type: "address" }, { type: "uint24" }, { type: "int24" }, { type: "address" }], - [k.currency0 as `0x${string}`, k.currency1 as `0x${string}`, k.fee, k.tickSpacing, k.hooks as `0x${string}`], - ), - ); - assert.equal(id, GITLAWB_POOL_ID); - assert.equal(k.fee, DYNAMIC_FEE_FLAG); + assert.equal(GITLAWB_POOL_ID, "0xec33256bf1ded407a57fd3c1965e7556e42ac14db09bc4e6fef57d5e2eb0b0b9"); + assert.equal(k.fee, 0x800000, "dynamic-fee flag"); assert.ok(BigInt(k.currency0) < BigInt(k.currency1), "WETH sorts below GITLAWB → GITLAWB is currency1"); assert.equal(GITLAWB_ADDRESS, GITLAWB_ADDRESS.toLowerCase(), "stored lowercase (DB quote column is lowercase)"); }); -test("gitlawbUsdFromSqrtPrice: live reading ≈ $1.68e-5 at tick 188117 / ETH $2,476; rejects garbage", () => { +test("gitlawbUsdFromSqrtPrice: live reading ≈ $1.68e-5 at tick 188117 / ETH $2,476; rejects garbage; flags implausible", () => { // sqrtPriceX96 read from StateView on 2026-09-08 (tick 188117 → ~1.477e8 GITLAWB per ETH) const sqrt = 0x2f799c757751a02dcfbdb3537a4an; - const usd = gitlawbUsdFromSqrtPrice(sqrt, 2476); + const flagged: number[] = []; + const usd = gitlawbUsdFromSqrtPrice(sqrt, 2476, (v) => flagged.push(v)); assert.ok(usd !== null); assert.ok(Math.abs(usd - 1.676e-5) / 1.676e-5 < 0.01, `≈ $1.676e-5, got ${usd}`); + assert.deepEqual(flagged, [], "a normal reading is not flagged"); assert.equal(gitlawbUsdFromSqrtPrice(sqrt, null), null, "no ETH price → no USD"); assert.equal(gitlawbUsdFromSqrtPrice(0n, 2476), null, "zero price"); assert.equal(gitlawbUsdFromSqrtPrice(-1n, 2476), null, "negative"); - assert.equal(gitlawbUsdFromSqrtPrice(2n ** 96n, 2476), null, "1 GITLAWB = 1 ETH is outside the sanity window"); - assert.equal(gitlawbUsdFromSqrtPrice(2n ** 96n * 10n ** 9n, 2476), null, "absurdly cheap is rejected too"); + const big = gitlawbUsdFromSqrtPrice(2n ** 96n, 2476, (v) => flagged.push(v)); + assert.equal(big, 2476, "1 GITLAWB = 1 ETH is still a reading…"); + assert.deepEqual(flagged, [2476], "…but it is reported as implausible"); }); test("presets convert dollar targets into GITLAWB units; empty without a price", () => { assert.deepEqual(gitlawbMcapPresets(null), []); assert.deepEqual(gitlawbMcapPresets(0), []); const p = gitlawbMcapPresets(0.00002); - assert.deepEqual(p, [250_000_000, 500_000_000, 1_250_000_000, 5_000_000_000]); + assert.deepEqual(p, [250_000_000, 500_000_000, 1_250_000_000, 5_000_000_000], "same $5K/$10K/$25K/$100K ladder as stocks, in whole GITLAWB"); }); test("logo asset ships with the app; address check is case-insensitive", () => { diff --git a/app/src/lib/launchpad/gitlawb.ts b/app/src/lib/launchpad/gitlawb.ts index 311ff0b1..b57d3293 100644 --- a/app/src/lib/launchpad/gitlawb.ts +++ b/app/src/lib/launchpad/gitlawb.ts @@ -9,58 +9,61 @@ * tick spacing 200), read on-chain through StateView.getSlot0(poolId) and multiplied by ETH/USD. * Both sides are 18-dec, so no decimal adjustment. Display + sorting only, never used on-chain. */ +import type { Address } from "viem"; +import { poolIdOf, sqrtPriceToTokensPerQuote } from "./math.ts"; +import { stockMcapPresets } from "./stocks.ts"; + export const GITLAWB_ADDRESS = "0x5f980dcfc4c0fa3911554cf5ab288ed0eb13dba3"; export const GITLAWB_SYMBOL = "GITLAWB"; export const GITLAWB_NAME = "Gitlawb"; export const GITLAWB_DECIMALS = 18; export const GITLAWB_SITE = "https://gitlawb.com"; -export const BASE_WETH = "0x4200000000000000000000000000000000000006"; -/** Uniswap v4 dynamic-fee flag (fee field = 0x800000 means "the hook sets the fee"). */ -export const DYNAMIC_FEE_FLAG = 0x800000; -/** The WETH/GITLAWB v4 pool key on Base (Initialize event at block 43,202,530). */ +/** + * The WETH/GITLAWB v4 pool key on Base (Initialize event at block 43,202,530): a dynamic-fee pool + * (fee 0x800000 = "the hook sets the fee"), tick spacing 200. Its id is derived, never pasted; + * gitlawb.test.ts pins it to the on-chain id. + */ export const GITLAWB_POOL_KEY = { - currency0: BASE_WETH, - currency1: GITLAWB_ADDRESS, - fee: DYNAMIC_FEE_FLAG, + currency0: "0x4200000000000000000000000000000000000006" as Address, // WETH + currency1: GITLAWB_ADDRESS as Address, + fee: 0x800000, tickSpacing: 200, - hooks: "0xbb7784a4d481184283ed89619a3e3ed143e1adc0", + hooks: "0xbb7784a4d481184283ed89619a3e3ed143e1adc0" as Address, } as const; -/** keccak256(abi.encode(GITLAWB_POOL_KEY)) — asserted by gitlawb.test.ts. */ -export const GITLAWB_POOL_ID = "0xec33256bf1ded407a57fd3c1965e7556e42ac14db09bc4e6fef57d5e2eb0b0b9"; +export const GITLAWB_POOL_ID = poolIdOf(GITLAWB_POOL_KEY); -const Q96 = 2n ** 96n; +/** Outside this band a reading is still used but logged: spot is ~$1e-5, so either end means the pool or ETH/USD moved 1,000× or more. */ +const PLAUSIBLE_USD: [number, number] = [1e-8, 1e-2]; /** - * USD per GITLAWB from the pool's sqrtPriceX96 (price of currency1 in currency0 = GITLAWB per WETH) - * and ETH/USD. null when either input is unusable, or the result is absurd (a pool that is empty or - * being manipulated must not price the site: sorting falls back to "unknown" = 0 weight). + * USD per GITLAWB from the pool's sqrtPriceX96 (GITLAWB per WETH; both 18-dec) and ETH/USD. + * null only when an input is unusable. An implausible reading is accepted (the site must not go + * unpriced because the token moved) but reported through `onImplausible` so it shows up in logs. */ -export function gitlawbUsdFromSqrtPrice(sqrtPriceX96: bigint, ethUsd: number | null): number | null { +export function gitlawbUsdFromSqrtPrice(sqrtPriceX96: bigint, ethUsd: number | null, onImplausible?: (usd: number) => void): number | null { if (ethUsd === null || !(ethUsd > 0) || sqrtPriceX96 <= 0n) return null; - const sqrt = Number(sqrtPriceX96) / Number(Q96); - const gitlawbPerEth = sqrt * sqrt; // 18/18 decimals → no scaling + const gitlawbPerEth = sqrtPriceToTokensPerQuote(sqrtPriceX96, 18); if (!Number.isFinite(gitlawbPerEth) || gitlawbPerEth <= 0) return null; const usd = ethUsd / gitlawbPerEth; - // sanity window: $1e-9 … $1 per GITLAWB (spot is ~$1e-5; a 1e4× move either way is not a real reading) - if (!(usd > 1e-9 && usd < 1)) return null; + if (!Number.isFinite(usd) || usd <= 0) return null; + if (usd < PLAUSIBLE_USD[0] || usd > PLAUSIBLE_USD[1]) onImplausible?.(usd); return usd; } -/** Starting-market-cap presets in whole GITLAWB for the usual dollar targets; empty while the price is unknown. */ -export function gitlawbMcapPresets(usdPrice: number | null, usdTargets = [5_000, 10_000, 25_000, 100_000]): number[] { - if (usdPrice === null || !(usdPrice > 0)) return []; - return usdTargets.map((u) => Math.round(u / usdPrice)); +/** Starting-market-cap presets in whole GITLAWB for the same dollar targets the stock form uses; empty while the price is unknown. */ +export function gitlawbMcapPresets(usdPrice: number | null): number[] { + return stockMcapPresets(usdPrice ?? 0).map(Math.round); } /** - * Gitlawb's logo (planet, orbit ring, blue satellite) as a navy rounded tile with the mark in white, served - * from our own origin. Generated from the web repo's `public/logo.png` by recolouring the mark and cropping - * (see the commit that added `public/gitlawb-mark.png`); 160px, transparent corners. + * Gitlawb's logo (the white branch-and-key mark on black, as on gitlawb.com) as a rounded tile, served from + * our own origin. `public/gitlawb-mark.png` is generated from the web repo's `public/logo.png`: strokes + * boldened slightly so they survive 18–22px badges, cropped to the mark, 160px, transparent corners. */ export const GITLAWB_LOGO_PATH = "/gitlawb-mark.png"; -/** The tile's ground — badges use the same navy so the tile and the pill read as one piece. */ -export const GITLAWB_LOGO_BG = "#0a1020"; +/** The tile's ground — badges use the same black so the tile and the pill read as one piece. */ +export const GITLAWB_LOGO_BG = "#000000"; export function isGitlawbAddress(address: string): boolean { return address.toLowerCase() === GITLAWB_ADDRESS; diff --git a/app/src/lib/launchpad/gitlawbServer.ts b/app/src/lib/launchpad/gitlawbServer.ts index 2ac5e04c..a44c63f4 100644 --- a/app/src/lib/launchpad/gitlawbServer.ts +++ b/app/src/lib/launchpad/gitlawbServer.ts @@ -6,43 +6,53 @@ import { ethUsd } from "./ethPrice"; import { GITLAWB_POOL_ID, gitlawbUsdFromSqrtPrice } from "./gitlawb"; /** - * USD per GITLAWB, server-side: StateView.getSlot0(WETH/GITLAWB pool) × ETH/USD, cached 60s. - * Fails soft: a transient RPC error (or an out-of-window reading) keeps the last good price for up to - * 15 minutes; after that, GITLAWB-quoted launches simply show no USD (and rank with 0 weight in USD - * sorts) until the next successful read. + * USD per GITLAWB, server-side: StateView.getSlot0(WETH/GITLAWB pool) × ETH/USD. + * + * Stale-while-revalidate so it never sits on a request's critical path once warm: a fresh value is + * served for 60s; after that the cached value is returned immediately and ONE refresh runs in the + * background. Only a cold process (no value yet) awaits the read, and that wait is capped at 4s. + * Failures keep the last good price for up to 15 minutes; after that GITLAWB pools simply show no + * USD (and rank with 0 weight in USD sorts) until a read succeeds. Never throws. */ -let cached: { at: number; usd: number | null; goodAt: number } = { at: 0, usd: null, goodAt: 0 }; -let inflight: Promise | null = null; -const TTL = 60_000; -/** A last-good price is reused through transient failures for at most this long; after that GITLAWB shows no USD. */ -const MAX_STALE = 15 * 60_000; - -function lastGood(now: number): number | null { - return now - cached.goodAt <= MAX_STALE ? cached.usd : null; -} +let usdNow: number | null = null; +let goodAt = 0; // when usdNow was last read successfully +let refreshedAt = 0; // when the last refresh attempt finished +let inflight: Promise | null = null; +const FRESH_MS = 60_000; +const MAX_STALE_MS = 15 * 60_000; +const COLD_WAIT_MS = 4_000; +const READ_TIMEOUT_MS = 8_000; async function read(): Promise { - const [eth, slot0] = await Promise.all([ - ethUsd(), - publicClient("base").readContract({ address: launchpad("base").v4.stateView, abi: STATE_VIEW_ABI, functionName: "getSlot0", args: [GITLAWB_POOL_ID] }), + const timeout = new Promise((_, reject) => setTimeout(() => reject(new Error(`timeout after ${READ_TIMEOUT_MS}ms`)), READ_TIMEOUT_MS).unref?.()); + const [eth, slot0] = await Promise.race([ + Promise.all([ethUsd(), publicClient("base").readContract({ address: launchpad("base").v4.stateView, abi: STATE_VIEW_ABI, functionName: "getSlot0", args: [GITLAWB_POOL_ID] })]), + timeout, ]); - return gitlawbUsdFromSqrtPrice(slot0[0], eth); + return gitlawbUsdFromSqrtPrice(slot0[0], eth, (usd) => console.warn(`[gitlawb] implausible price reading: $${usd} per GITLAWB (used anyway)`)); } -export async function gitlawbUsd(): Promise { - if (Date.now() - cached.at < TTL) return cached.usd; - inflight ??= read() +function refresh(): Promise { + return (inflight ??= read() .then((usd) => { - const now = Date.now(); - cached = usd !== null ? { at: now, usd, goodAt: now } : { at: now, usd: lastGood(now), goodAt: cached.goodAt }; - return cached.usd; - }) - .catch((err) => { - console.warn("[gitlawb] price read failed:", err instanceof Error ? err.message : err); - const now = Date.now(); - cached = { at: now, usd: lastGood(now), goodAt: cached.goodAt }; - return cached.usd; + if (usd !== null) { + usdNow = usd; + goodAt = Date.now(); + } }) - .finally(() => (inflight = null)); - return inflight; + .catch((err) => console.warn("[gitlawb] price read failed:", err instanceof Error ? err.message : err)) + .finally(() => { + refreshedAt = Date.now(); + inflight = null; + })); +} + +export async function gitlawbUsd(): Promise { + const now = Date.now(); + if (now - refreshedAt >= FRESH_MS) { + const p = refresh(); + // cold start: nothing to serve yet → wait briefly for the first read (bounded) + if (goodAt === 0) await Promise.race([p, new Promise((r) => setTimeout(r, COLD_WAIT_MS).unref?.())]); + } + return goodAt > 0 && Date.now() - goodAt <= MAX_STALE_MS ? usdNow : null; } diff --git a/app/src/lib/launchpad/math.test.ts b/app/src/lib/launchpad/math.test.ts index a68af5ee..f7657644 100644 --- a/app/src/lib/launchpad/math.test.ts +++ b/app/src/lib/launchpad/math.test.ts @@ -1,6 +1,6 @@ import { test } from "node:test"; import assert from "node:assert/strict"; -import { fdvForStartTick, fmtCompact, fmtEth, fmtPrice, fmtUsd, initialBuyPreview, minOut, poolIdOf, quoteUsdOf, startTickForFdv, sqrtPriceToTokensPerQuote, tickToTokensPerQuote } from "./math.ts"; +import { fdvForStartTick, fmtCompact, fmtEth, fmtQuoteUnits, fmtPrice, fmtUsd, initialBuyPreview, minOut, poolIdOf, quoteUsdOf, startTickForFdv, sqrtPriceToTokensPerQuote, tickToTokensPerQuote } from "./math.ts"; import { encodeV4ExactInSingle } from "./swap.ts"; test("startTickForFdv: 10 ETH FDV on 1B supply ≈ tick 184200 (1 ETH = 100M tokens)", () => { @@ -125,3 +125,14 @@ test("initialBuyPreview: the LP fee reduces output; quote decimals are honoured" assert.ok(u.pctOfSupply > 0.9 && u.pctOfSupply < 1.0, `got ${u.pctOfSupply}`); assert.ok(u.fdvAfter > 1_000 && u.fdvAfter < 1_100, `fdv after ${u.fdvAfter}`); }); + +test("fmtQuoteUnits: stables 2dp, 18-dec ETH-style below 100K, compact above with suffix promotion", () => { + assert.equal(fmtQuoteUnits(12.5, 6), "12.50"); + assert.equal(fmtQuoteUnits(250, 6), "250"); + assert.equal(fmtQuoteUnits(0.0421, 18), "0.0421"); + assert.equal(fmtQuoteUnits(99_999.99999999999, 18), "100K", "float noise at the boundary still compacts"); + assert.equal(fmtQuoteUnits(150_000, 18), "150K"); + assert.equal(fmtQuoteUnits(999_999, 18), "1M", "no 1000.00K"); + assert.equal(fmtQuoteUnits(1_234_567, 18), "1.23M"); + assert.equal(fmtQuoteUnits(594_540_000, 18), "594.54M"); +}); diff --git a/app/src/lib/launchpad/math.ts b/app/src/lib/launchpad/math.ts index 1a5302be..52df89db 100644 --- a/app/src/lib/launchpad/math.ts +++ b/app/src/lib/launchpad/math.ts @@ -163,11 +163,22 @@ export function pipsToPct(pips: number): string { return `${Number.isInteger(pct) ? pct : pct.toFixed(2)}%`; } -/** "12.5 USDG" / "0.05 ETH" from a raw amount. */ +const COMPACT = new Intl.NumberFormat("en-US", { notation: "compact", maximumFractionDigits: 2 }); + +/** + * Whole quote units for display. ≤6-dec quotes (stables) show 2 decimals; 18-dec quotes show ETH-style + * precision below 100K units and compact above (GITLAWB: millions per dollar → "1.2M", never + * "1200000.0000"; 999,999 → "1M", not "1000.00K"). + */ +export function fmtQuoteUnits(v: number, decimals: number): string { + if (!Number.isFinite(v)) return "—"; + if (decimals <= 6) return (Math.abs(v) >= 100 ? v.toFixed(0) : v.toFixed(2)).replace(/\.00$/, ""); + return Math.round(Math.abs(v)) >= 100_000 ? COMPACT.format(v) : fmtEth(v); +} + +/** "12.5 USDG" / "0.05 ETH" / "1.2M GITLAWB" from a raw amount. */ export function fmtQuote(raw: bigint | string, decimals: number, symbol: string): string { - const v = units(raw, decimals); - // 18-dec quotes with big unit counts (GITLAWB: millions per dollar) read as 1.2M, never 1200000.0000 - return `${decimals <= 6 ? (v >= 100 ? v.toFixed(0) : v.toFixed(2)).replace(/\.00$/, "") : Math.abs(v) >= 100_000 ? fmtCompact(v, 2) : fmtEth(v)} ${symbol}`; + return `${fmtQuoteUnits(units(raw, decimals), decimals)} ${symbol}`; } export const WAD_ = WAD; diff --git a/app/src/lib/launchpad/ogcard.ts b/app/src/lib/launchpad/ogcard.ts index a1607eff..dcc9dec4 100644 --- a/app/src/lib/launchpad/ogcard.ts +++ b/app/src/lib/launchpad/ogcard.ts @@ -1,5 +1,5 @@ /** Pure shaping for the per-token share card (node --test loads this). */ -export type CardInput = { name: string; symbol: string; chain: "base" | "robinhood"; fdv_usd: number | null; fdv_quote: number; quote_symbol: string; change_from_launch: number; lp_fee: number; recipients: { payout: string; bps: number }[]; block_time: string }; +export type CardInput = { name: string; symbol: string; chain: "base" | "robinhood"; fdv_usd: number | null; fdv_quote: number; quote_key: string; quote_symbol: string; change_from_launch: number; lp_fee: number; recipients: { payout: string; bps: number }[]; block_time: string }; export type Card = { title: string; symbol: string; chainLabel: string; mcap: string; change: string; up: boolean; fee: string; age: string; quote: { symbol: string; ticker: string; kind: "stock" | "gitlawb" } | null }; const DEAD = "0x000000000000000000000000000000000000dead"; @@ -37,15 +37,15 @@ export function shapeCard(l: CardInput, now: number): Card { up: pct >= 0, fee: feeLabel(l.lp_fee, l.recipients), age: ageLabel(l.block_time, now), - quote: stockQuoteOf(l.quote_symbol), + quote: quotePillOf(l.quote_key, l.quote_symbol), }; } -/** Stock quotes get a "priced in" pill with a ticker tile, GITLAWB one with the Gitlawb mark; ETH / USDG / unknown ("?") get none. */ -export function stockQuoteOf(quoteSymbol: string): Card["quote"] { +/** "priced in" pill: registry stocks get a ticker tile, GITLAWB the Gitlawb tile; ETH / USDG / unknown ("?") get none. */ +export function quotePillOf(quoteKey: string, quoteSymbol: string): Card["quote"] { + if (quoteKey === "gitlawb") return { symbol: "GITLAWB", ticker: "GL", kind: "gitlawb" }; const sym = quoteSymbol.trim(); - if (!sym || sym === "?" || sym === "ETH" || sym === "USDG") return null; - if (sym === "GITLAWB") return { symbol: "GITLAWB", ticker: "GL", kind: "gitlawb" }; + if (quoteKey !== "stock" || !sym || sym === "?") return null; return { symbol: sym.slice(0, 12), ticker: sym.replace(/c$/, "").toUpperCase().slice(0, 5), kind: "stock" }; } diff --git a/app/src/lib/launchpad/queries.ts b/app/src/lib/launchpad/queries.ts index 4e1edee5..c13da3cf 100644 --- a/app/src/lib/launchpad/queries.ts +++ b/app/src/lib/launchpad/queries.ts @@ -25,6 +25,7 @@ export type LaunchRow = { token_id: number; launcher: string; quote: string; + quote_key: Quote["key"]; // eth | usdg | gitlawb | stock (= any other ERC20) quote_symbol: string; quote_decimals: number; pool_id: string; @@ -70,7 +71,7 @@ export type LaunchRow = { volume_24h_usd: number | null; }; -type Raw = Omit & { +type Raw = Omit & { token_id: bigint; block_number: bigint; tick: number | null; @@ -95,11 +96,12 @@ function quoteInfo(chain: ChainKey, address: string): Quote { /** Warm the stock registry + prices for the stocks in use (and the GITLAWB price), so `shape()` can stay synchronous. */ async function withStocks(): Promise { + gitlawbUsdNow = await gitlawbUsd(); // never throws; served from cache once warm (stale-while-revalidate) try { await ensureRegistry(); // robinhood registry (fail-soft); the Base list is static - [stockUsdNow, gitlawbUsdNow] = await Promise.all([stockUsdInUse(), gitlawbUsd()]); + stockUsdNow = await stockUsdInUse(); } catch { - /* fail soft: stocks / GITLAWB show without USD */ + /* fail soft: stocks show without USD */ } } @@ -123,6 +125,7 @@ function shape(raw: Raw & { last_swap_block?: bigint; last_swap_log?: number; lo return { ...r, chain, + quote_key: q.key, quote_symbol: q.symbol, quote_decimals: q.decimals, token_id: Number(r.token_id), @@ -257,8 +260,8 @@ export async function getSwaps(chain: ChainKey, token: string, quoteDecimals: nu /** Home tape: launches + trades across chains, newest first. */ export type FeedItem = - | { kind: "launch"; chain: ChainKey; at: string; tx_hash: string; token: string; name: string; symbol: string; launcher: string; lp_fee: number; quote_symbol: string; image_url: string | null } - | { kind: "swap"; chain: ChainKey; at: string; tx_hash: string; token: string; name: string; symbol: string; trader: string | null; is_buy: boolean; is_dev: boolean; quote_wei: string; quote_symbol: string; quote_decimals: number; usd: number | null; image_url: string | null }; + | { kind: "launch"; chain: ChainKey; at: string; tx_hash: string; token: string; name: string; symbol: string; launcher: string; lp_fee: number; quote_key: Quote["key"]; image_url: string | null } + | { kind: "swap"; chain: ChainKey; at: string; tx_hash: string; token: string; name: string; symbol: string; trader: string | null; is_buy: boolean; is_dev: boolean; quote_wei: string; quote_key: Quote["key"]; quote_symbol: string; quote_decimals: number; usd: number | null; image_url: string | null }; export async function getLaunchFeed(limit = 24, ethUsd: number | null = null): Promise { const db = maybeDb(); @@ -277,11 +280,11 @@ export async function getLaunchFeed(limit = 24, ethUsd: number | null = null): P ) x ORDER BY at DESC LIMIT ${n}`; // each arm pre-limited on an indexed time column: the union never scans the whole swaps table return rows.map((r) => { const chain = chainKeyOf(r.chain_id) ?? "base"; - if (r.kind === "launch") return { kind: "launch", chain, at: r.at, tx_hash: r.tx_hash, token: r.token, name: r.name, symbol: r.symbol, launcher: r.who ?? "", lp_fee: r.lp_fee ?? 0, quote_symbol: quoteInfo(chain, r.quote).symbol, image_url: canonicalImageUrl(r.image_url, imagePublicBase()) }; + if (r.kind === "launch") return { kind: "launch", chain, at: r.at, tx_hash: r.tx_hash, token: r.token, name: r.name, symbol: r.symbol, launcher: r.who ?? "", lp_fee: r.lp_fee ?? 0, quote_key: quoteInfo(chain, r.quote).key, image_url: canonicalImageUrl(r.image_url, imagePublicBase()) }; const q = quoteInfo(chain, r.quote); const qu = quoteUsd(q, ethUsd); const wei = r.quote_wei ?? "0"; - return { kind: "swap", chain, at: r.at, tx_hash: r.tx_hash, token: r.token, name: r.name, symbol: r.symbol, trader: r.who, is_buy: Boolean(r.is_buy), is_dev: Boolean(r.is_dev), quote_wei: wei, quote_symbol: q.symbol, quote_decimals: q.decimals, usd: qu === null ? null : units(wei, q.decimals) * qu, image_url: canonicalImageUrl(r.image_url, imagePublicBase()) }; + return { kind: "swap", chain, at: r.at, tx_hash: r.tx_hash, token: r.token, name: r.name, symbol: r.symbol, trader: r.who, is_buy: Boolean(r.is_buy), is_dev: Boolean(r.is_dev), quote_wei: wei, quote_key: q.key, quote_symbol: q.symbol, quote_decimals: q.decimals, usd: qu === null ? null : units(wei, q.decimals) * qu, image_url: canonicalImageUrl(r.image_url, imagePublicBase()) }; }); } diff --git a/app/src/lib/launchpad/search.test.ts b/app/src/lib/launchpad/search.test.ts index cddc1137..257f16db 100644 --- a/app/src/lib/launchpad/search.test.ts +++ b/app/src/lib/launchpad/search.test.ts @@ -38,9 +38,9 @@ test("filters: fee0 / burn / usdg / today", () => { assert.equal(matchesFilter(row({ lp_fee: 10_000 }), "burn"), true, "1% + dead recipient = burn"); assert.equal(matchesFilter(row({ lp_fee: 10_000, recipients: [{ payout: "0xabc", bps: 10000 }] }), "burn"), false); assert.equal(matchesFilter(row(), "burn"), false, "0% pools are not 'burn'"); - assert.equal(matchesFilter(row({ quote_symbol: "USDG" }), "usdg"), true); - assert.equal(matchesFilter(row({ quote_symbol: "GITLAWB" }), "gitlawb"), true); - assert.equal(matchesFilter(row({ quote_symbol: "ETH" }), "gitlawb"), false); + assert.equal(matchesFilter(row({ quote_key: "usdg" }), "usdg"), true); + assert.equal(matchesFilter(row({ quote_key: "gitlawb" }), "gitlawb"), true); + assert.equal(matchesFilter(row({ quote_key: "eth" }), "gitlawb"), false); assert.equal(matchesFilter(row(), "today"), true); assert.equal(matchesFilter(row({ block_time: new Date(Date.now() - 3 * 86_400_000).toISOString() }), "today"), false); assert.equal(matchesFilter(row(), null), true); diff --git a/app/src/lib/launchpad/search.ts b/app/src/lib/launchpad/search.ts index e6860765..5c599476 100644 --- a/app/src/lib/launchpad/search.ts +++ b/app/src/lib/launchpad/search.ts @@ -1,10 +1,11 @@ /** Pure search / filter helpers (client + server; node --test loads this directly). */ export type LaunchFilter = "fee0" | "burn" | "usdg" | "gitlawb" | "today"; -export const FILTERS: { key: LaunchFilter; label: string; title: string }[] = [ +/** `chain`: the filter only makes sense on that chain (its quote is not offered elsewhere) → hidden when another chain is selected. */ +export const FILTERS: { key: LaunchFilter; label: string; title: string; chain?: "base" | "robinhood" }[] = [ { key: "fee0", label: "0% fee", title: "feeless pools" }, { key: "burn", label: "fees burned", title: "no beneficiary; every fee is burned" }, - { key: "usdg", label: "USDG", title: "priced in USDG (Robinhood Chain)" }, - { key: "gitlawb", label: "GITLAWB", title: "priced in GITLAWB (Base)" }, + { key: "usdg", label: "USDG", title: "priced in USDG (Robinhood Chain)", chain: "robinhood" }, + { key: "gitlawb", label: "GITLAWB", title: "priced in GITLAWB (Base)", chain: "base" }, { key: "today", label: "today", title: "launched in the last 24 hours" }, ]; export function isFilter(v: unknown): v is LaunchFilter { @@ -20,7 +21,7 @@ export function isAddressQuery(q: string): boolean { } const DEAD = "0x000000000000000000000000000000000000dead"; -type Matchable = { name: string; symbol: string; token: string; lp_fee: number; quote_symbol: string; block_time: string; recipients: { payout: string; bps: number }[] }; +type Matchable = { name: string; symbol: string; token: string; lp_fee: number; quote_key: string; block_time: string; recipients: { payout: string; bps: number }[] }; /** Client-side match over an already-loaded row. Name/symbol prefix or substring, or exact address. */ export function matchesQuery(l: Matchable, q: string): boolean { @@ -34,8 +35,8 @@ export function matchesFilter(l: Matchable, f: LaunchFilter | null, now = Date.n if (!f) return true; if (f === "fee0") return l.lp_fee === 0; if (f === "burn") return l.lp_fee > 0 && l.recipients.length === 1 && l.recipients[0].payout.toLowerCase() === DEAD; - if (f === "usdg") return l.quote_symbol === "USDG"; - if (f === "gitlawb") return l.quote_symbol === "GITLAWB"; + if (f === "usdg") return l.quote_key === "usdg"; + if (f === "gitlawb") return l.quote_key === "gitlawb"; return now - new Date(l.block_time).getTime() < 86_400_000; } From f26d5a69eb8efce9e5247397afeaa8aad72dc9ed Mon Sep 17 00:00:00 2001 From: Kevin Codex Date: Tue, 8 Sep 2026 16:53:10 +0800 Subject: [PATCH 08/10] GITLAWB badge: fit the text line (20px) so rows with the badge keep the same height as rows without it --- app/src/app/t/[chain]/[token]/page.tsx | 2 +- app/src/components/launchpad/GitlawbBadge.tsx | 7 ++++--- 2 files changed, 5 insertions(+), 4 deletions(-) diff --git a/app/src/app/t/[chain]/[token]/page.tsx b/app/src/app/t/[chain]/[token]/page.tsx index 7d3b065b..02750562 100644 --- a/app/src/app/t/[chain]/[token]/page.tsx +++ b/app/src/app/t/[chain]/[token]/page.tsx @@ -91,7 +91,7 @@ export default async function TokenPage({ params }: { params: Promise<{ chain: s

{l.name}

-
${l.symbol}·{quote.key === "gitlawb" ? : Paired with {quote.symbol}}·Launched {ago(l.block_time, now)} ago
+
${l.symbol}·{quote.key === "gitlawb" ? : Paired with {quote.symbol}}·Launched {ago(l.block_time, now)} ago
diff --git a/app/src/components/launchpad/GitlawbBadge.tsx b/app/src/components/launchpad/GitlawbBadge.tsx index b81ad0ff..9fdbf1d0 100644 --- a/app/src/components/launchpad/GitlawbBadge.tsx +++ b/app/src/components/launchpad/GitlawbBadge.tsx @@ -8,17 +8,18 @@ export function GitlawbMark({ size = 16, className = "" }: { size?: number; clas /** * Badge for tokens paired with GITLAWB. Shown wherever a token appears (list, trending, feed, token page, - * dashboard, share card) — the visible perk of picking GITLAWB as the quote asset. Black like the logo tile, + * dashboard, share card) — the visible perk of picking GITLAWB as the quote asset. "sm" is 20px tall so it sits + * inside a text-sm line without changing row heights. Black like the logo tile, * white text, a light hairline in dark mode so it still reads as a chip on a dark page. */ export default function GitlawbBadge({ size = "sm", className = "", label = GITLAWB_SYMBOL }: { size?: "sm" | "md"; className?: string; label?: string }) { return ( - + {label} ); From 822d18019fb180859aa22f201a7a21d5246bf561 Mon Sep 17 00:00:00 2001 From: Kevin Codex Date: Tue, 8 Sep 2026 17:15:01 +0800 Subject: [PATCH 09/10] GITLAWB on Robinhood Chain too GITLAWB exists on Robinhood Chain as a LayerZero V2 OFT (0xd1b0d44E4f6ed940fcC7A9F59Bf30Daf62cCFe3D, community repo contracts.ts), minted 1:1 against GITLAWB locked in the GitlawbOFTAdapter on Base (peers verified both ways on-chain). So the Robinhood side needs no bridge work: the OFT is now a fixed quote next to USDG and ETH. - config: robinhood quotes [USDG, ETH, GITLAWB]; same key, same badge, same live USD (the Base pool price; the two are one supply kept within arbitrage by the bridge) - queries: filter + USD factor match per chain (Base address on Base, OFT on Robinhood) - filter chip offered on both chains; launch form nudge on any chain that offers GITLAWB - copy: form, token page, rules, agents, llms.txt, README mention the bridged token - contracts/test/LaunchFactory.gitlawbRobinhood.fork.t.sol: pins endpoint + Base peer and proves launch/buy/sell/collect with the fee burned as GITLAWB on live Robinhood Chain --- README.md | 4 +- app/src/app/agents/page.tsx | 4 +- app/src/app/api/quotes/route.ts | 4 +- app/src/app/llms.txt/route.ts | 3 +- app/src/app/rules/page.tsx | 4 +- app/src/app/t/[chain]/[token]/page.tsx | 2 +- app/src/components/launchpad/LaunchForm.tsx | 6 +- app/src/lib/launchpad/config.ts | 7 +- app/src/lib/launchpad/gitlawb.test.ts | 6 +- app/src/lib/launchpad/gitlawb.ts | 21 ++- app/src/lib/launchpad/queries.ts | 11 +- app/src/lib/launchpad/search.ts | 2 +- .../LaunchFactory.gitlawbRobinhood.fork.t.sol | 120 ++++++++++++++++++ 13 files changed, 166 insertions(+), 28 deletions(-) create mode 100644 contracts/test/LaunchFactory.gitlawbRobinhood.fork.t.sol diff --git a/README.md b/README.md index b3621d3c..66383a67 100644 --- a/README.md +++ b/README.md @@ -32,8 +32,8 @@ Creators choose a 0 / 1 / 3 % trading fee that is pushed **in full** to benefici - **Rug-proof liquidity.** The position NFT lives in a locker with no owner, no pause, no upgrade path and no transfer function. Proven with an adversarial fork suite against the live deployment. - **Two chains, one set of addresses.** Base and Robinhood Chain, same contracts, same rules. -- **Any quote asset.** ETH, USDG, **GITLAWB** ([Gitlawb](https://gitlawb.com)'s token on Base, priced from its - Uniswap v4 WETH pool; fees paid or burned in GITLAWB), or a **tokenized stock**: Coinbase's B20 stocks on Base +- **Any quote asset.** ETH, USDG, **GITLAWB** ([Gitlawb](https://gitlawb.com)'s token, on Base and bridged 1:1 to + Robinhood Chain; priced from its Base Uniswap v4 WETH pool; fees paid or burned in GITLAWB), or a **tokenized stock**: Coinbase's B20 stocks on Base (NVDAc, AAPLc, TSLAc, …, priced from Chainlink on-chain feeds) and Robinhood Stock Tokens on Robinhood Chain. - **Trade in-page.** Buys and sells go straight to the Uniswap v4 pool through the Universal Router; the site never touches funds. diff --git a/app/src/app/agents/page.tsx b/app/src/app/agents/page.tsx index 87a970f2..79a47b61 100644 --- a/app/src/app/agents/page.tsx +++ b/app/src/app/agents/page.tsx @@ -73,7 +73,7 @@ export default function AgentsPage() {
-
quote
ETH uses address zero. For GITLAWB on Base, use 0x5F980Dcfc4c0fa3911554cf5ab288ed0eb13DBa3; for USDG on Robinhood Chain, 0x5fc5360D0400a0Fd4f2af552ADD042D716F1d168. For any ERC-20 quote call findSalt(...) first so the token address sorts above the quote.
+
quote
ETH uses address zero. For GITLAWB use 0x5F980Dcfc4c0fa3911554cf5ab288ed0eb13DBa3 on Base or 0xd1b0d44E4f6ed940fcC7A9F59Bf30Daf62cCFe3D on Robinhood Chain; for USDG on Robinhood Chain, 0x5fc5360D0400a0Fd4f2af552ADD042D716F1d168. For any ERC-20 quote call findSalt(...) first so the token address sorts above the quote.
supply
0 means the default 1B supply.
startTick
Sets the opening market cap. A multiple of 200; 184200 ≈ 10 ETH FDV for 1B supply.
lpFee
In pips: 0, 10000 = 1%, or 30000 = 3%.
@@ -106,7 +106,7 @@ export default function AgentsPage() {
03

Trade & collect

Standard pools. Direct contract calls.

-
Swap

Speak Uniswap v4

Pools are plain Uniswap v4, with tick spacing 200 and no hook. The quote can be ETH, GITLAWB (Base), USDG (Robinhood Chain) or a supported issuer-registry stock token. Get the actual pool key with poolKeyOf(token) on the factory. Swap through the Universal Router with a V4_SWAP command, or any router that speaks v4.

+
Swap

Speak Uniswap v4

Pools are plain Uniswap v4, with tick spacing 200 and no hook. The quote can be ETH, GITLAWB (both chains), USDG (Robinhood Chain) or a supported issuer-registry stock token. Get the actual pool key with poolKeyOf(token) on the factory. Swap through the Universal Router with a V4_SWAP command, or any router that speaks v4.

Collect

Pay out accrued fees

Anyone may call collect(tokenId) on that chain's locker to pay out accrued fees. Base locker: {locker}. Use the matching chain address above.

Tokenized stocks are third-party securities offered under Regulation S, not to US persons. The site recognizes them from issuer registries, not on-chain names.

diff --git a/app/src/app/api/quotes/route.ts b/app/src/app/api/quotes/route.ts index 64975d99..0adda648 100644 --- a/app/src/app/api/quotes/route.ts +++ b/app/src/app/api/quotes/route.ts @@ -11,7 +11,7 @@ export const dynamic = "force-dynamic"; /** * GET /api/quotes?chain=[&q=AAPL] → quote assets the launch form may offer on that chain: - * the fixed ones (ETH, USDG, GITLAWB on Base — with GITLAWB's live USD) plus tokenized stocks from that chain's registry (with live USD): + * the fixed ones (ETH, USDG, GITLAWB — with GITLAWB's live USD) plus tokenized stocks from that chain's registry (with live USD): * Coinbase tokenized stocks on Base, Robinhood Stock Tokens on Robinhood Chain. * Only registry addresses are ever labelled as stocks. */ @@ -20,7 +20,7 @@ export async function GET(req: Request) { const chain = u.searchParams.get("chain"); if (!isChainKey(chain)) return NextResponse.json({ error: "bad chain" }, { status: 400 }); const q = (u.searchParams.get("q") ?? "").slice(0, 12); - const gl = chain === "base" ? await gitlawbUsd() : null; + const gl = await gitlawbUsd(); // one price for both chains: the Robinhood token is the Base token bridged 1:1 const fixed = launchpad(chain).quotes.map((x) => ({ ...x, usd: x.key === "gitlawb" ? gl : x.usd })); const stocks = await memo(`quotes:stocks:${chain}:${q.toUpperCase()}`, 30_000, async () => { let hits: { address: string; symbol: string; name: string; decimals: number; logo: string | null }[]; diff --git a/app/src/app/llms.txt/route.ts b/app/src/app/llms.txt/route.ts index acd68e7f..f3644cc3 100644 --- a/app/src/app/llms.txt/route.ts +++ b/app/src/app/llms.txt/route.ts @@ -35,7 +35,7 @@ ${verificationLine("Robinhood Chain (4663)", r, [ ## What a launch does 1. deploys a fixed-supply ERC-20 (1,000,000,000; no mint/pause/blacklist/tax/owner) -2. initializes a Uniswap v4 pool quote/token (quote = ETH, GITLAWB on Base, USDG on Robinhood Chain, or a registry stock token; tick spacing 200, no hook) at the chosen start tick +2. initializes a Uniswap v4 pool quote/token (quote = ETH, GITLAWB, USDG on Robinhood Chain, or a registry stock token; tick spacing 200, no hook) at the chosen start tick 3. mints one single-sided position holding 100% of supply to an ownerless locker (no withdraw path exists) 4. registers fee routing: lpFee 0 | 10000 (1%) | 30000 (3%) pips; recipients [] = fees burned, else {payout,bps}[] summing to 10000. Fixed forever. @@ -48,6 +48,7 @@ Uniswap v4: Base PoolManager 0x498581fF718922c3f8e6A244956aF099B2652b2b, Univers Robinhood PoolManager 0x8366a39CC670B4001A1121B8F6A443A643e40951, Universal Router 0x8876789976decbfcbbbe364623c63652db8c0904 USDG (Robinhood, 6 dec): 0x5fc5360D0400a0Fd4f2af552ADD042D716F1d168 GITLAWB (Base, 18 dec): 0x5F980Dcfc4c0fa3911554cf5ab288ed0eb13DBa3 (Gitlawb's token; fees paid in GITLAWB, burned when recipients = []) +GITLAWB (Robinhood, 18 dec): 0xd1b0d44E4f6ed940fcC7A9F59Bf30Daf62cCFe3D (LayerZero OFT of the Base token, 1:1) ## Launch (contract call) factory.launch((name, symbol, metadataURI, quote, supply=0, startTick, lpFee, salt, recipients)) diff --git a/app/src/app/rules/page.tsx b/app/src/app/rules/page.tsx index 735b1349..00465e5a 100644 --- a/app/src/app/rules/page.tsx +++ b/app/src/app/rules/page.tsx @@ -12,7 +12,7 @@ export const metadata: Metadata = { title: "How it works", description: "What a const STEPS = [ { title: "Your token is deployed", description: "A plain ERC-20 with EIP-2612 permit. Fixed supply, 1 billion by default. No mint, no pause, no blacklist, no transfer tax, no owner.", value: "1B", label: "default supply" }, - { title: "A market opens", description: "A Uniswap v4 pool pairs your token with ETH, GITLAWB on Base, a tokenized stock (Coinbase stocks on Base, Robinhood Stock Tokens on Robinhood Chain), or USDG on Robinhood Chain. No hook. The pool starts at the market cap you pick.", value: "v4", label: "Uniswap pool" }, + { title: "A market opens", description: "A Uniswap v4 pool pairs your token with ETH, GITLAWB, a tokenized stock (Coinbase stocks on Base, Robinhood Stock Tokens on Robinhood Chain), or USDG on Robinhood Chain. No hook. The pool starts at the market cap you pick.", value: "v4", label: "Uniswap pool" }, { title: "The liquidity position is locked", description: "100% of the supply goes into one single-sided position at launch. Its NFT is minted to an ownerless locker that has no function to withdraw, transfer or shrink it. Ever.", value: "100%", label: "deposited at launch" }, { title: "Fee routing is written in", description: "The trading fee you choose (0%, 1% or 3%) goes 100% to the beneficiaries you name, or is burned if you name none. Fixed at launch, unchangeable.", value: "0 / 1 / 3%", label: "your trading fee" }, ] as const; @@ -108,7 +108,7 @@ export default function RulesPage() {
How does the price move?

Price follows a single-sided Uniswap v4 curve: the first buyers get the most tokens per unit of the quote asset, and every buy moves the price up. Sells move it down.

There is no anti-snipe mechanism. Bots can buy in the first block like anyone else.

What can change after launch?

Nothing on-chain can be edited after launch: not the name, not the fee, not the beneficiaries. Metadata (image, description, links) is stored by this site and can change; the token itself cannot.

-
What is a GITLAWB-quoted pool?

GITLAWB is Gitlawb's token on Base, an ordinary ERC-20 with no transfer restrictions and no issuer switch. Pick it as the quote and buyers pay in GITLAWB, the creator's trading fee is paid in GITLAWB, and a launch with no beneficiary burns GITLAWB on every trade.

Dollar figures for these pools come from the Uniswap v4 WETH/GITLAWB pool price and ETH/USD. They are for display and sorting only; nothing on-chain depends on them.

+
What is a GITLAWB-quoted pool?

GITLAWB is Gitlawb's token on Base, bridged 1:1 to Robinhood Chain over LayerZero (one supply, two chains): an ordinary ERC-20 with no transfer restrictions and no issuer switch. Pick it as the quote and buyers pay in GITLAWB, the creator's trading fee is paid in GITLAWB, and a launch with no beneficiary burns GITLAWB on every trade.

Dollar figures for these pools come from the Uniswap v4 WETH/GITLAWB pool price on Base and ETH/USD, on both chains. They are for display and sorting only; nothing on-chain depends on them.

What should I know about stock-quoted pools?

Stock-quoted pools use tokenized stocks as the quote asset: Coinbase tokenized stocks (B20) on Base, recognised here only from Base's official list and priced from Chainlink's on-chain feeds; Robinhood Stock Tokens on Robinhood Chain, recognised only from Robinhood's own registry.

Both are securities issued by third parties under Regulation S and are not offered to US persons. Coinbase's also exclude the UK, Canada, Australia, Singapore and Switzerland. Their issuers can pause transfers or freeze wallets in restricted jurisdictions. Holding or trading them is subject to the issuer's terms, not ours.

diff --git a/app/src/app/t/[chain]/[token]/page.tsx b/app/src/app/t/[chain]/[token]/page.tsx index 02750562..dae716ad 100644 --- a/app/src/app/t/[chain]/[token]/page.tsx +++ b/app/src/app/t/[chain]/[token]/page.tsx @@ -142,7 +142,7 @@ export default async function TokenPage({ params }: { params: Promise<{ chain: s
{stockQuote ?

Paired with {stockQuote.name} ({stockQuote.symbol}), a third-party tokenized stock. These securities are not offered to US persons. The quote asset is identified from the issuer registry, not its token name.

: null} - {quote.key === "gitlawb" ?

Paired with GITLAWB, Gitlawb's token on Base. {mode === "free" ? "This pool has no trading fee." : mode === "burn" ? "Every trading fee on this pool is burned as GITLAWB." : "Trading fees on this pool are paid out in GITLAWB."} USD figures use the Uniswap v4 WETH/GITLAWB pool price.

: null} + {quote.key === "gitlawb" ?

Paired with GITLAWB, Gitlawb's token{chain === "robinhood" ? " (bridged 1:1 from Base over LayerZero; one supply, two chains)" : " on Base"}. {mode === "free" ? "This pool has no trading fee." : mode === "burn" ? "Every trading fee on this pool is burned as GITLAWB." : "Trading fees on this pool are paid out in GITLAWB."} USD figures use the Uniswap v4 WETH/GITLAWB pool price on Base.

: null} } />
diff --git a/app/src/components/launchpad/LaunchForm.tsx b/app/src/components/launchpad/LaunchForm.tsx index d1e18cf7..b3898bd0 100644 --- a/app/src/components/launchpad/LaunchForm.tsx +++ b/app/src/components/launchpad/LaunchForm.tsx @@ -362,7 +362,7 @@ export default function LaunchForm({ ethUsd, gitlawbUsd = null, initialChain = " aria-pressed={active} >
{CHAIN_LABELS[k]}
-
{k === "base" ? "Priced in ETH, GITLAWB or a Coinbase tokenized stock. Gas ≈ cents." : ok ? "Priced in USDG (dollars), ETH or a Robinhood Stock Token. Gas ≈ cents." : "Coming soon."}
+
{k === "base" ? "Priced in ETH, GITLAWB or a Coinbase tokenized stock. Gas ≈ cents." : ok ? "Priced in USDG (dollars), ETH, GITLAWB or a Robinhood Stock Token. Gas ≈ cents." : "Coming soon."}
); })} @@ -391,7 +391,7 @@ export default function LaunchForm({ ethUsd, gitlawbUsd = null, initialChain = " {quote.key === "usdg" ? "Buyers pay with USDG; market cap and fees are in dollars." : quote.key === "gitlawb" ? "Buyers pay with GITLAWB; fees are paid in GITLAWB, or burned." : quote.key === "stock" ? (chain === "base" ? "Buyers pay with a Coinbase tokenized stock; fees are paid in that stock." : "Buyers pay with a Robinhood Stock Token; fees are paid in that stock.") : "Buyers pay with ETH."} - {chain === "base" && quote.key !== "gitlawb" ? ( + {cfg.quotes.some((q) => q.key === "gitlawb") && quote.key !== "gitlawb" ? ( @@ -410,7 +410,7 @@ export default function LaunchForm({ ethUsd, gitlawbUsd = null, initialChain = " {quote.usd ? {fmtUsd(quote.usd)} : price unavailable}

- Your token carries the badge on the launch list, trending, the activity feed, its page and its share card. GITLAWB is Gitlawb's token on Base: an ordinary ERC-20, no transfer restrictions, no issuer switch. Name no beneficiary and every trading fee burns GITLAWB. Price from the Uniswap v4 WETH/GITLAWB pool.{" "} + Your token carries the badge on the launch list, trending, the activity feed, its page and its share card. GITLAWB is Gitlawb's token{chain === "robinhood" ? ", bridged 1:1 from Base to Robinhood Chain over LayerZero" : " on Base"}: an ordinary ERC-20, no transfer restrictions, no issuer switch. Name no beneficiary and every trading fee burns GITLAWB. Price from the Uniswap v4 WETH/GITLAWB pool on Base.{" "} gitlawb.com ↗

diff --git a/app/src/lib/launchpad/config.ts b/app/src/lib/launchpad/config.ts index 5d89e9d9..af19d014 100644 --- a/app/src/lib/launchpad/config.ts +++ b/app/src/lib/launchpad/config.ts @@ -1,6 +1,6 @@ import { isAddress, type Address } from "viem"; import { CHAIN_KEYS, SITE_URL, type ChainKey } from "@/lib/chainPublic"; -import { GITLAWB_ADDRESS, GITLAWB_DECIMALS, GITLAWB_LOGO_PATH, GITLAWB_NAME, GITLAWB_SYMBOL } from "./gitlawb"; +import { GITLAWB_ADDRESS, GITLAWB_ADDRESS_ROBINHOOD, GITLAWB_DECIMALS, GITLAWB_LOGO_PATH, GITLAWB_NAME, GITLAWB_SYMBOL } from "./gitlawb"; /** * CLIENT-SAFE launchpad config, per chain. Only NEXT_PUBLIC_* vars are read here. @@ -20,8 +20,9 @@ export type ChainLaunchpad = { key: ChainKey; factory: Address | null; locker: A const ETH: Quote = { key: "eth", address: NATIVE, symbol: "ETH", decimals: 18, usd: null }; const USDG: Quote = { key: "usdg", address: "0x5fc5360D0400a0Fd4f2af552ADD042D716F1d168", symbol: "USDG", decimals: 6, usd: 1 }; -/** GITLAWB on Base: usd is null here (client-safe static); the server fills the live price (gitlawbServer.ts). */ +/** GITLAWB: usd is null here (client-safe static); the server fills the live price (gitlawbServer.ts). Robinhood's is the LayerZero OFT of the Base token. */ const GITLAWB: Quote = { key: "gitlawb", address: GITLAWB_ADDRESS as Address, symbol: GITLAWB_SYMBOL, decimals: GITLAWB_DECIMALS, usd: null, name: GITLAWB_NAME, logo: GITLAWB_LOGO_PATH }; +const GITLAWB_RH: Quote = { ...GITLAWB, address: GITLAWB_ADDRESS_ROBINHOOD as Address }; /** Canonical Uniswap v4 deployments (developers.uniswap.org/docs/protocols/v4/deployments). */ const V4_BY_CHAIN: Record = { @@ -45,7 +46,7 @@ const V4_BY_CHAIN: Record = { }, }; /** Quote assets offered per chain, first = default. */ -const QUOTES_BY_CHAIN: Record = { base: [ETH, GITLAWB], robinhood: [USDG, ETH] }; +const QUOTES_BY_CHAIN: Record = { base: [ETH, GITLAWB], robinhood: [USDG, ETH, GITLAWB_RH] }; function addr(v: string | undefined): Address | null { const raw = (v ?? "").trim(); diff --git a/app/src/lib/launchpad/gitlawb.test.ts b/app/src/lib/launchpad/gitlawb.test.ts index e4a82ac5..66bc0f3f 100644 --- a/app/src/lib/launchpad/gitlawb.test.ts +++ b/app/src/lib/launchpad/gitlawb.test.ts @@ -1,7 +1,7 @@ import { test } from "node:test"; import assert from "node:assert/strict"; import { existsSync } from "node:fs"; -import { GITLAWB_ADDRESS, GITLAWB_LOGO_PATH, GITLAWB_POOL_ID, GITLAWB_POOL_KEY, gitlawbMcapPresets, gitlawbUsdFromSqrtPrice, isGitlawbAddress } from "./gitlawb.ts"; +import { GITLAWB_ADDRESS, GITLAWB_ADDRESS_ROBINHOOD, GITLAWB_ADDRESSES, GITLAWB_LOGO_PATH, GITLAWB_POOL_ID, GITLAWB_POOL_KEY, gitlawbMcapPresets, gitlawbUsdFromSqrtPrice, isGitlawbAddress } from "./gitlawb.ts"; test("the derived pool id is the on-chain WETH/GITLAWB pool (Initialize at Base block 43,202,530)", () => { const k = GITLAWB_POOL_KEY; @@ -39,4 +39,8 @@ test("logo asset ships with the app; address check is case-insensitive", () => { assert.ok(existsSync(new URL(`../../../public${GITLAWB_LOGO_PATH}`, import.meta.url)), "public/gitlawb-mark.png exists"); assert.ok(isGitlawbAddress("0x5F980Dcfc4c0fa3911554cf5ab288ed0eb13DBa3")); assert.ok(!isGitlawbAddress("0x5F980Dcfc4c0fa3911554cf5ab288ed0eb13DBa4")); + assert.ok(isGitlawbAddress("0xd1b0d44E4f6ed940fcC7A9F59Bf30Daf62cCFe3D", "robinhood"), "the Robinhood OFT (community repo contracts.ts)"); + assert.ok(!isGitlawbAddress("0xd1b0d44E4f6ed940fcC7A9F59Bf30Daf62cCFe3D", "base"), "the Robinhood address is not GITLAWB on Base"); + assert.equal(GITLAWB_ADDRESSES.robinhood, GITLAWB_ADDRESS_ROBINHOOD); + assert.equal(GITLAWB_ADDRESS_ROBINHOOD, GITLAWB_ADDRESS_ROBINHOOD.toLowerCase()); }); diff --git a/app/src/lib/launchpad/gitlawb.ts b/app/src/lib/launchpad/gitlawb.ts index b57d3293..803dd9fb 100644 --- a/app/src/lib/launchpad/gitlawb.ts +++ b/app/src/lib/launchpad/gitlawb.ts @@ -1,9 +1,10 @@ /** - * GITLAWB as a quote asset on Base — pure logic (node --test loads this directly). + * GITLAWB as a quote asset on Base and Robinhood Chain — pure logic (node --test loads this directly). * - * GITLAWB is Gitlawb's token: an ordinary 18-decimal ERC-20 on Base, no transfer restrictions, - * no issuer switch. Pools quoted in it work exactly like any other ERC-20 quote (Permit2 path); - * trading fees are paid out in GITLAWB, or burned when the launch names no beneficiary. + * GITLAWB is Gitlawb's token: an ordinary 18-decimal ERC-20 on Base (bridged 1:1 to Robinhood Chain + * over LayerZero OFT), no transfer restrictions, no issuer switch. Pools quoted in it work exactly like + * any other ERC-20 quote (Permit2 path); trading fees are paid out in GITLAWB, or burned when the + * launch names no beneficiary. * * USD comes from the deepest GITLAWB market: the Uniswap v4 WETH/GITLAWB pool (dynamic-fee hook, * tick spacing 200), read on-chain through StateView.getSlot0(poolId) and multiplied by ETH/USD. @@ -14,6 +15,14 @@ import { poolIdOf, sqrtPriceToTokensPerQuote } from "./math.ts"; import { stockMcapPresets } from "./stocks.ts"; export const GITLAWB_ADDRESS = "0x5f980dcfc4c0fa3911554cf5ab288ed0eb13dba3"; +/** + * GITLAWB on Robinhood Chain (4663): a LayerZero V2 OFT, minted 1:1 against GITLAWB locked in the + * GitlawbOFTAdapter on Base (0x3308384bc308d09b3eba9a8f11cc36e993a273a4). Same token, one supply, two + * chains — see the community repo's contracts.ts. Priced from the Base pool (the deep market); the + * bridge keeps the two within arbitrage of each other. + */ +export const GITLAWB_ADDRESS_ROBINHOOD = "0xd1b0d44e4f6ed940fcc7a9f59bf30daf62ccfe3d"; +export const GITLAWB_ADDRESSES: Record<"base" | "robinhood", string> = { base: GITLAWB_ADDRESS, robinhood: GITLAWB_ADDRESS_ROBINHOOD }; export const GITLAWB_SYMBOL = "GITLAWB"; export const GITLAWB_NAME = "Gitlawb"; export const GITLAWB_DECIMALS = 18; @@ -65,6 +74,6 @@ export const GITLAWB_LOGO_PATH = "/gitlawb-mark.png"; /** The tile's ground — badges use the same black so the tile and the pill read as one piece. */ export const GITLAWB_LOGO_BG = "#000000"; -export function isGitlawbAddress(address: string): boolean { - return address.toLowerCase() === GITLAWB_ADDRESS; +export function isGitlawbAddress(address: string, chain: "base" | "robinhood" = "base"): boolean { + return address.toLowerCase() === GITLAWB_ADDRESSES[chain]; } diff --git a/app/src/lib/launchpad/queries.ts b/app/src/lib/launchpad/queries.ts index c13da3cf..1d1146d3 100644 --- a/app/src/lib/launchpad/queries.ts +++ b/app/src/lib/launchpad/queries.ts @@ -4,7 +4,7 @@ import { CHAIN_KEYS, chainIdOf, chainKeyOf, type ChainKey } from "@/lib/chainPub import { quoteInfo as staticQuoteInfo, quoteUsdOf, type Quote } from "./config"; import { ensureRegistry, stockByAddress, stockUsdInUse } from "./stocksServer"; import { gitlawbUsd } from "./gitlawbServer"; -import { GITLAWB_ADDRESS } from "./gitlawb"; +import { GITLAWB_ADDRESS, GITLAWB_ADDRESS_ROBINHOOD } from "./gitlawb"; import { canonicalImageUrl } from "./images"; import { rankTrending } from "./trending"; import { imagePublicBase } from "./imageStore"; @@ -164,7 +164,8 @@ export const VOLUME_WINDOWS: VolumeWindow[] = ["1h", "24h", "all"]; export type ListOpts = { sort?: LaunchSort; window?: VolumeWindow; chain?: ChainKey | null; filter?: LaunchFilter | null; limit?: number; offset?: number; launcher?: string; ethUsd?: number | null }; const USDG_ADDR = "0x5fc5360d0400a0fd4f2af552add042d716f1d168"; -const BASE_ID = chainIdOf("base"); // GITLAWB is a Base quote only: the same address on another chain is not GITLAWB +const BASE_ID = chainIdOf("base"); +const RH_ID = chainIdOf("robinhood"); const NATIVE_ADDR = "0x0000000000000000000000000000000000000000"; const DEAD_ADDR = "0x000000000000000000000000000000000000dead"; @@ -194,13 +195,15 @@ export async function listLaunchesPage(opts: ListOpts = {}): Promise { if (opts.filter === "fee0") conds.push(db`l.lp_fee = 0`); if (opts.filter === "burn") conds.push(db`l.lp_fee > 0 AND jsonb_array_length(l.recipients) = 1 AND lower(l.recipients->0->>'payout') = ${DEAD_ADDR}`); if (opts.filter === "usdg") conds.push(db`l.quote = ${USDG_ADDR}`); - if (opts.filter === "gitlawb") conds.push(db`l.chain_id = ${BASE_ID} AND l.quote = ${GITLAWB_ADDRESS}`); + // GITLAWB has a different address per chain; each match is chain-scoped so a same-address token elsewhere is never GITLAWB + const isGitlawb = () => db`((l.chain_id = ${BASE_ID} AND l.quote = ${GITLAWB_ADDRESS}) OR (l.chain_id = ${RH_ID} AND l.quote = ${GITLAWB_ADDRESS_ROBINHOOD}))`; + if (opts.filter === "gitlawb") conds.push(db`${isGitlawb()}`); if (opts.filter === "today") conds.push(db`l.block_time > now() - interval '24 hours'`); const where = conds.length ? db`WHERE ${conds.reduce((a, c) => db`${a} AND ${c}`)}` : db``; // per-row USD factor and quote decimals (USDG is the only non-18-dec quote we list) const stockCase = stockEntries.length ? stockEntries.map(([a, v]) => db`WHEN l.quote = ${a} THEN ${v}::double precision`).reduce((acc, c) => db`${acc} ${c}`) : db``; const gitlawbFactor = gitlawbUsdNow !== null && gitlawbUsdNow > 0 ? gitlawbUsdNow : 0; // unknown → 0 weight, like an unknown stock - const usdPerUnit = db`(CASE WHEN l.quote = ${USDG_ADDR} THEN 1.0 ${stockCase} WHEN l.chain_id = ${BASE_ID} AND l.quote = ${GITLAWB_ADDRESS} THEN ${gitlawbFactor}::double precision WHEN l.quote = ${NATIVE_ADDR} THEN ${ethFactor}::double precision ELSE 0.0 END)`; + const usdPerUnit = db`(CASE WHEN l.quote = ${USDG_ADDR} THEN 1.0 ${stockCase} WHEN ${isGitlawb()} THEN ${gitlawbFactor}::double precision WHEN l.quote = ${NATIVE_ADDR} THEN ${ethFactor}::double precision ELSE 0.0 END)`; const stockDec = stockEntries.map(([a]) => [a, stockByAddress("base", a)?.decimals ?? stockByAddress("robinhood", a)?.decimals ?? 18] as [string, number]).filter(([, d]) => d !== 18); const decCase = stockDec.length ? stockDec.map(([a, d]) => db`WHEN l.quote = ${a} THEN ${d}`).reduce((acc, c) => db`${acc} ${c}`) : db``; const qd = db`(CASE WHEN l.quote = ${USDG_ADDR} THEN 6 ${decCase} ELSE 18 END)`; diff --git a/app/src/lib/launchpad/search.ts b/app/src/lib/launchpad/search.ts index 5c599476..4937adcf 100644 --- a/app/src/lib/launchpad/search.ts +++ b/app/src/lib/launchpad/search.ts @@ -5,7 +5,7 @@ export const FILTERS: { key: LaunchFilter; label: string; title: string; chain?: { key: "fee0", label: "0% fee", title: "feeless pools" }, { key: "burn", label: "fees burned", title: "no beneficiary; every fee is burned" }, { key: "usdg", label: "USDG", title: "priced in USDG (Robinhood Chain)", chain: "robinhood" }, - { key: "gitlawb", label: "GITLAWB", title: "priced in GITLAWB (Base)", chain: "base" }, + { key: "gitlawb", label: "GITLAWB", title: "priced in GITLAWB (Base or Robinhood Chain)" }, { key: "today", label: "today", title: "launched in the last 24 hours" }, ]; export function isFilter(v: unknown): v is LaunchFilter { diff --git a/contracts/test/LaunchFactory.gitlawbRobinhood.fork.t.sol b/contracts/test/LaunchFactory.gitlawbRobinhood.fork.t.sol new file mode 100644 index 00000000..c5c953d8 --- /dev/null +++ b/contracts/test/LaunchFactory.gitlawbRobinhood.fork.t.sol @@ -0,0 +1,120 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.26; + +import {Test} from "forge-std/Test.sol"; +import {IPoolManager} from "@uniswap/v4-core/src/interfaces/IPoolManager.sol"; +import {PoolKey} from "@uniswap/v4-core/src/types/PoolKey.sol"; +import {TickMath} from "@uniswap/v4-core/src/libraries/TickMath.sol"; +import {SwapParams} from "@uniswap/v4-core/src/types/PoolOperation.sol"; +import {PoolSwapTest} from "@uniswap/v4-core/src/test/PoolSwapTest.sol"; +import {LaunchFactory} from "src/LaunchFactory.sol"; +import {LaunchLocker} from "src/LaunchLocker.sol"; +import {LaunchToken} from "src/LaunchToken.sol"; + +interface IERC20Meta { + function decimals() external view returns (uint8); + function symbol() external view returns (string memory); + function balanceOf(address) external view returns (uint256); + function approve(address, uint256) external returns (bool); + function transfer(address, uint256) external returns (bool); +} + +interface IOFT { + function endpoint() external view returns (address); + function peers(uint32 eid) external view returns (bytes32); +} + +/// GITLAWB on Robinhood Chain is the LayerZero OFT of the Base token. Against the LIVE Robinhood +/// factory: launch a token priced in that GITLAWB with NO beneficiary, buy, sell some back, collect, +/// and prove the 1% fee was BURNED as GITLAWB. Also pins the bridge wiring: the OFT's LayerZero +/// endpoint and its Base peer (the GitlawbOFTAdapter that locks the Base token). +/// FORK_TESTS=true ROBINHOOD_RPC_URL=... forge test --match-contract LaunchGitlawbRobinhoodQuote -vv +contract LaunchGitlawbRobinhoodQuote is Test { + address constant FACTORY = 0x815542E8b392389A1389E22E588E4B62A67Ade72; + address constant LOCKER = 0xcd1680D26922fcd9CabFbb8a56bA40C333fD842a; + address constant PM = 0x8366a39CC670B4001A1121B8F6A443A643e40951; + address constant GITLAWB = 0xd1b0d44E4f6ed940fcC7A9F59Bf30Daf62cCFe3D; // LayerZero OFT (18 dec) + address constant LZ_ENDPOINT_ROBINHOOD = 0x6F475642a6e85809B1c36Fa62763669b1b48DD5B; + address constant BASE_OFT_ADAPTER = 0x3308384bc308D09b3Eba9A8F11cC36e993A273A4; + uint32 constant BASE_EID = 30184; + address constant DEAD = 0x000000000000000000000000000000000000dEaD; + + bool forked; + LaunchFactory factory = LaunchFactory(FACTORY); + PoolSwapTest router; + address creator = makeAddr("gitlawb-rh-creator"); + address buyer = makeAddr("gitlawb-rh-buyer"); + + function setUp() public { + if (!vm.envOr("FORK_TESTS", false)) return; + vm.createSelectFork(vm.envOr("ROBINHOOD_RPC_URL", string("https://rpc.mainnet.chain.robinhood.com"))); + forked = true; + router = new PoolSwapTest(IPoolManager(PM)); + vm.deal(creator, 1 ether); + vm.deal(buyer, 1 ether); + // borrow 20M GITLAWB from the PoolManager (it holds tens of millions across GITLAWB pools) + uint256 pmBal = IERC20Meta(GITLAWB).balanceOf(PM); + assertGt(pmBal, 20_000_000e18, "PoolManager holds GITLAWB on Robinhood Chain"); + vm.prank(PM); + IERC20Meta(GITLAWB).transfer(buyer, 20_000_000e18); + } + + function test_fork_bridgeWiring() public view { + if (!forked) return; + assertEq(IERC20Meta(GITLAWB).symbol(), "GITLAWB"); + assertEq(IERC20Meta(GITLAWB).decimals(), 18); + assertEq(IOFT(GITLAWB).endpoint(), LZ_ENDPOINT_ROBINHOOD, "LayerZero V2 endpoint on Robinhood Chain"); + assertEq(address(uint160(uint256(IOFT(GITLAWB).peers(BASE_EID)))), BASE_OFT_ADAPTER, "peer on Base = GitlawbOFTAdapter"); + } + + function test_fork_launchQuotedInGITLAWB_buySellBurn() public { + if (!forked) return; + LaunchFactory.LaunchParams memory p; + p.name = "Lawb Fan RH"; + p.symbol = "LAWBRH"; + p.quote = GITLAWB; + p.lpFee = 10_000; + // 1B supply at an FDV of 600M GITLAWB: tokens per GITLAWB ~ 1.667 -> tick ~ 5,108 -> 5,200 + p.startTick = 5_200; + // recipients empty = fees burned + (bytes32 salt,) = factory.findSalt(creator, keccak256("lawbrh"), p.name, p.symbol, 0, "", GITLAWB, 64); + p.salt = salt; + vm.prank(creator); + (address token, uint256 tokenId) = factory.launch(p); + assertGt(uint160(token), uint160(GITLAWB), "token sorts above the quote"); + PoolKey memory key = factory.poolKeyOf(token); + + uint256 deadBefore = IERC20Meta(GITLAWB).balanceOf(DEAD); + + // buy with 10M GITLAWB + vm.startPrank(buyer); + IERC20Meta(GITLAWB).approve(address(router), 10_000_000e18); + router.swap( + key, + SwapParams({zeroForOne: true, amountSpecified: -10_000_000e18, sqrtPriceLimitX96: TickMath.MIN_SQRT_PRICE + 1}), + PoolSwapTest.TestSettings({takeClaims: false, settleUsingBurn: false}), + "" + ); + uint256 got = LaunchToken(token).balanceOf(buyer); + assertGt(got, 10_000_000e18, "more than 10M tokens for 10M GITLAWB at about 1.6 tokens per GITLAWB"); + // sell half back + LaunchToken(token).approve(address(router), got / 2); + uint256 glBefore = IERC20Meta(GITLAWB).balanceOf(buyer); + router.swap( + key, + SwapParams({ + zeroForOne: false, amountSpecified: -int256(got / 2), sqrtPriceLimitX96: TickMath.MAX_SQRT_PRICE - 1 + }), + PoolSwapTest.TestSettings({takeClaims: false, settleUsingBurn: false}), + "" + ); + vm.stopPrank(); + assertGt(IERC20Meta(GITLAWB).balanceOf(buyer), glBefore, "got GITLAWB back"); + + // fees: 1% of the GITLAWB that came in, burned (no beneficiary) + (uint256 quoteOut,) = LaunchLocker(payable(LOCKER)).collect(tokenId); + assertApproxEqRel(quoteOut, 100_000e18, 2e16, "about 1% of 10M GITLAWB collected"); + assertEq(IERC20Meta(GITLAWB).balanceOf(DEAD) - deadBefore, quoteOut, "every collected GITLAWB fee was burned"); + assertEq(IERC20Meta(GITLAWB).balanceOf(creator), 0, "creator got nothing: no beneficiary"); + } +} From 7321a315f8d50c0e8ec48fefce5d9bad1427e51b Mon Sep 17 00:00:00 2001 From: Kevin Codex Date: Tue, 8 Sep 2026 17:35:39 +0800 Subject: [PATCH 10/10] CodeRabbit review (PR #16): TWAP cross-check for the GITLAWB price, accurate fee wording - gitlawbServer: the v4 spot is now reconciled against a 30-minute time-weighted average tick from the Uniswap v3 WETH/GITLAWB pool (observe([1800, 0]), on-chain, no off-chain service). Spot is published unless it strays >25% from the TWAP, then the TWAP is published and the event is logged. A single-block push of the v4 price can no longer rewrite market caps, USD volume or trending scores. Last-good retention on failure unchanged. Replaces the fixed dollar band. Pure helpers (twapTick, gitlawbUsdFromTick, reconcileGitlawbUsd) with tests. - rules page: fees go to the beneficiaries named at launch by their shares, or are burned when none is named (not "the creator's fee") - badge tooltip: "any trading fee is paid, or burned, in GITLAWB" (true for 0% pools too) --- app/src/app/rules/page.tsx | 2 +- app/src/components/launchpad/GitlawbBadge.tsx | 2 +- app/src/lib/launchpad/gitlawb.test.ts | 30 ++++++++--- app/src/lib/launchpad/gitlawb.ts | 52 +++++++++++++++---- app/src/lib/launchpad/gitlawbServer.ts | 35 ++++++++++--- 5 files changed, 94 insertions(+), 27 deletions(-) diff --git a/app/src/app/rules/page.tsx b/app/src/app/rules/page.tsx index 00465e5a..995258c8 100644 --- a/app/src/app/rules/page.tsx +++ b/app/src/app/rules/page.tsx @@ -108,7 +108,7 @@ export default function RulesPage() {
How does the price move?

Price follows a single-sided Uniswap v4 curve: the first buyers get the most tokens per unit of the quote asset, and every buy moves the price up. Sells move it down.

There is no anti-snipe mechanism. Bots can buy in the first block like anyone else.

What can change after launch?

Nothing on-chain can be edited after launch: not the name, not the fee, not the beneficiaries. Metadata (image, description, links) is stored by this site and can change; the token itself cannot.

-
What is a GITLAWB-quoted pool?

GITLAWB is Gitlawb's token on Base, bridged 1:1 to Robinhood Chain over LayerZero (one supply, two chains): an ordinary ERC-20 with no transfer restrictions and no issuer switch. Pick it as the quote and buyers pay in GITLAWB, the creator's trading fee is paid in GITLAWB, and a launch with no beneficiary burns GITLAWB on every trade.

Dollar figures for these pools come from the Uniswap v4 WETH/GITLAWB pool price on Base and ETH/USD, on both chains. They are for display and sorting only; nothing on-chain depends on them.

+
What is a GITLAWB-quoted pool?

GITLAWB is Gitlawb's token on Base, bridged 1:1 to Robinhood Chain over LayerZero (one supply, two chains): an ordinary ERC-20 with no transfer restrictions and no issuer switch. Pick it as the quote and buyers pay in GITLAWB; any trading fee is paid in GITLAWB to the beneficiaries named at launch (by their shares), and a launch that names none burns GITLAWB on every trade.

Dollar figures for these pools come from the Uniswap v4 WETH/GITLAWB pool price on Base, cross-checked against a 30-minute on-chain average, and ETH/USD, on both chains. They are for display and sorting only; nothing on-chain depends on them.

What should I know about stock-quoted pools?

Stock-quoted pools use tokenized stocks as the quote asset: Coinbase tokenized stocks (B20) on Base, recognised here only from Base's official list and priced from Chainlink's on-chain feeds; Robinhood Stock Tokens on Robinhood Chain, recognised only from Robinhood's own registry.

Both are securities issued by third parties under Regulation S and are not offered to US persons. Coinbase's also exclude the UK, Canada, Australia, Singapore and Switzerland. Their issuers can pause transfers or freeze wallets in restricted jurisdictions. Holding or trading them is subject to the issuer's terms, not ours.

diff --git a/app/src/components/launchpad/GitlawbBadge.tsx b/app/src/components/launchpad/GitlawbBadge.tsx index 9fdbf1d0..94383b4d 100644 --- a/app/src/components/launchpad/GitlawbBadge.tsx +++ b/app/src/components/launchpad/GitlawbBadge.tsx @@ -17,7 +17,7 @@ export default function GitlawbBadge({ size = "sm", className = "", label = GITL {label} diff --git a/app/src/lib/launchpad/gitlawb.test.ts b/app/src/lib/launchpad/gitlawb.test.ts index 66bc0f3f..54a9829a 100644 --- a/app/src/lib/launchpad/gitlawb.test.ts +++ b/app/src/lib/launchpad/gitlawb.test.ts @@ -1,7 +1,7 @@ import { test } from "node:test"; import assert from "node:assert/strict"; import { existsSync } from "node:fs"; -import { GITLAWB_ADDRESS, GITLAWB_ADDRESS_ROBINHOOD, GITLAWB_ADDRESSES, GITLAWB_LOGO_PATH, GITLAWB_POOL_ID, GITLAWB_POOL_KEY, gitlawbMcapPresets, gitlawbUsdFromSqrtPrice, isGitlawbAddress } from "./gitlawb.ts"; +import { GITLAWB_ADDRESS, GITLAWB_ADDRESS_ROBINHOOD, GITLAWB_ADDRESSES, GITLAWB_LOGO_PATH, GITLAWB_V3_POOL, gitlawbUsdFromTick, reconcileGitlawbUsd, twapTick, GITLAWB_POOL_ID, GITLAWB_POOL_KEY, gitlawbMcapPresets, gitlawbUsdFromSqrtPrice, isGitlawbAddress } from "./gitlawb.ts"; test("the derived pool id is the on-chain WETH/GITLAWB pool (Initialize at Base block 43,202,530)", () => { const k = GITLAWB_POOL_KEY; @@ -11,20 +11,16 @@ test("the derived pool id is the on-chain WETH/GITLAWB pool (Initialize at Base assert.equal(GITLAWB_ADDRESS, GITLAWB_ADDRESS.toLowerCase(), "stored lowercase (DB quote column is lowercase)"); }); -test("gitlawbUsdFromSqrtPrice: live reading ≈ $1.68e-5 at tick 188117 / ETH $2,476; rejects garbage; flags implausible", () => { +test("gitlawbUsdFromSqrtPrice: live reading ≈ $1.68e-5 at tick 188117 / ETH $2,476; rejects garbage", () => { // sqrtPriceX96 read from StateView on 2026-09-08 (tick 188117 → ~1.477e8 GITLAWB per ETH) const sqrt = 0x2f799c757751a02dcfbdb3537a4an; - const flagged: number[] = []; - const usd = gitlawbUsdFromSqrtPrice(sqrt, 2476, (v) => flagged.push(v)); + const usd = gitlawbUsdFromSqrtPrice(sqrt, 2476); assert.ok(usd !== null); assert.ok(Math.abs(usd - 1.676e-5) / 1.676e-5 < 0.01, `≈ $1.676e-5, got ${usd}`); - assert.deepEqual(flagged, [], "a normal reading is not flagged"); assert.equal(gitlawbUsdFromSqrtPrice(sqrt, null), null, "no ETH price → no USD"); assert.equal(gitlawbUsdFromSqrtPrice(0n, 2476), null, "zero price"); assert.equal(gitlawbUsdFromSqrtPrice(-1n, 2476), null, "negative"); - const big = gitlawbUsdFromSqrtPrice(2n ** 96n, 2476, (v) => flagged.push(v)); - assert.equal(big, 2476, "1 GITLAWB = 1 ETH is still a reading…"); - assert.deepEqual(flagged, [2476], "…but it is reported as implausible"); + assert.equal(gitlawbUsdFromSqrtPrice(2n ** 96n, 2476), 2476, "1 GITLAWB = 1 ETH is still a reading; the TWAP guard decides whether it is published"); }); test("presets convert dollar targets into GITLAWB units; empty without a price", () => { @@ -44,3 +40,21 @@ test("logo asset ships with the app; address check is case-insensitive", () => { assert.equal(GITLAWB_ADDRESSES.robinhood, GITLAWB_ADDRESS_ROBINHOOD); assert.equal(GITLAWB_ADDRESS_ROBINHOOD, GITLAWB_ADDRESS_ROBINHOOD.toLowerCase()); }); + +test("v3 TWAP cross-check: tick from cumulatives, USD from tick, and spot/TWAP reconciliation", () => { + // observe([1800, 0]) on the v3 WETH/GITLAWB pool, 2026-09-08: avg tick 188,260 (v4 spot tick was 188,117 → 1.4% apart) + const t = twapTick(1897758471224n, 1898097340718n, 1800); + assert.equal(t, 188260); + assert.equal(twapTick(-10n, -25n, 10), -2, "negative deltas floor toward -inf like Uniswap"); + assert.equal(twapTick(0n, 20n, 10), 2); + const twap = gitlawbUsdFromTick(t, 2476); + assert.ok(twap !== null && Math.abs(twap - 1.652e-5) / 1.652e-5 < 0.01, `≈ $1.65e-5, got ${twap}`); + assert.equal(gitlawbUsdFromTick(t, null), null); + assert.equal(GITLAWB_V3_POOL, GITLAWB_V3_POOL.toLowerCase()); + + assert.deepEqual(reconcileGitlawbUsd(1.0e-5, 1.1e-5), { usd: 1.0e-5, source: "spot", deviation: Math.abs(1.0e-5 - 1.1e-5) / 1.1e-5 }, "within 25%: spot"); + assert.deepEqual(reconcileGitlawbUsd(5e-5, 1e-5), { usd: 1e-5, source: "twap", deviation: 4 }, "spot pushed 5×: TWAP wins"); + assert.deepEqual(reconcileGitlawbUsd(null, 1e-5), { usd: 1e-5, source: "twap", deviation: null }); + assert.deepEqual(reconcileGitlawbUsd(1e-5, null), { usd: 1e-5, source: "spot", deviation: null }); + assert.deepEqual(reconcileGitlawbUsd(null, null), { usd: null, source: null, deviation: null }); +}); diff --git a/app/src/lib/launchpad/gitlawb.ts b/app/src/lib/launchpad/gitlawb.ts index 803dd9fb..47cfeaff 100644 --- a/app/src/lib/launchpad/gitlawb.ts +++ b/app/src/lib/launchpad/gitlawb.ts @@ -42,22 +42,56 @@ export const GITLAWB_POOL_KEY = { } as const; export const GITLAWB_POOL_ID = poolIdOf(GITLAWB_POOL_KEY); -/** Outside this band a reading is still used but logged: spot is ~$1e-5, so either end means the pool or ETH/USD moved 1,000× or more. */ -const PLAUSIBLE_USD: [number, number] = [1e-8, 1e-2]; +/** + * Manipulation cross-check: the Uniswap v3 WETH/GITLAWB 1% pool on Base (token0 WETH, token1 GITLAWB) + * keeps an on-chain oracle (observe), so a 30-minute time-weighted average tick is available without + * any off-chain service. The v4 spot is the primary price (deepest market); when it strays more than + * MAX_SPOT_DEVIATION from the TWAP, the TWAP is used instead, so a single-block price push in the v4 + * pool cannot rewrite market caps, USD volume or trending scores site-wide. + */ +export const GITLAWB_V3_POOL = "0x72c12f0cc0e0c6e97ff9869717a482aff97e3b30" as Address; +export const TWAP_WINDOW_S = 1800; +export const MAX_SPOT_DEVIATION = 0.25; + +/** Time-weighted average tick from two tickCumulative readings `windowS` apart (Uniswap v3 semantics: floor toward -inf). */ +export function twapTick(cumulativeThen: bigint, cumulativeNow: bigint, windowS: number): number { + const delta = cumulativeNow - cumulativeThen; + const w = BigInt(windowS); + let tick = delta / w; + if (delta < 0n && delta % w !== 0n) tick -= 1n; + return Number(tick); +} + +/** USD per GITLAWB from a WETH/GITLAWB tick (GITLAWB per WETH = 1.0001^tick; both 18-dec) and ETH/USD. */ +export function gitlawbUsdFromTick(tick: number, ethUsd: number | null): number | null { + if (ethUsd === null || !(ethUsd > 0) || !Number.isFinite(tick)) return null; + const usd = ethUsd / Math.pow(1.0001, tick); + return Number.isFinite(usd) && usd > 0 ? usd : null; +} + +/** + * Pick the price to publish: the spot unless it deviates from the TWAP by more than `maxDeviation`, + * in which case the TWAP wins. With only one side available that side is used; with neither, null. + */ +export function reconcileGitlawbUsd(spot: number | null, twap: number | null, maxDeviation = MAX_SPOT_DEVIATION): { usd: number | null; source: "spot" | "twap" | null; deviation: number | null } { + if (spot === null && twap === null) return { usd: null, source: null, deviation: null }; + if (spot === null) return { usd: twap, source: "twap", deviation: null }; + if (twap === null) return { usd: spot, source: "spot", deviation: null }; + const deviation = Math.abs(spot - twap) / twap; + return deviation > maxDeviation ? { usd: twap, source: "twap", deviation } : { usd: spot, source: "spot", deviation }; +} /** - * USD per GITLAWB from the pool's sqrtPriceX96 (GITLAWB per WETH; both 18-dec) and ETH/USD. - * null only when an input is unusable. An implausible reading is accepted (the site must not go - * unpriced because the token moved) but reported through `onImplausible` so it shows up in logs. + * USD per GITLAWB from the v4 pool's sqrtPriceX96 (GITLAWB per WETH; both 18-dec) and ETH/USD. + * null only when an input is unusable. Manipulation resistance comes from reconcileGitlawbUsd() + * (spot vs the v3 TWAP), not from a fixed dollar band. */ -export function gitlawbUsdFromSqrtPrice(sqrtPriceX96: bigint, ethUsd: number | null, onImplausible?: (usd: number) => void): number | null { +export function gitlawbUsdFromSqrtPrice(sqrtPriceX96: bigint, ethUsd: number | null): number | null { if (ethUsd === null || !(ethUsd > 0) || sqrtPriceX96 <= 0n) return null; const gitlawbPerEth = sqrtPriceToTokensPerQuote(sqrtPriceX96, 18); if (!Number.isFinite(gitlawbPerEth) || gitlawbPerEth <= 0) return null; const usd = ethUsd / gitlawbPerEth; - if (!Number.isFinite(usd) || usd <= 0) return null; - if (usd < PLAUSIBLE_USD[0] || usd > PLAUSIBLE_USD[1]) onImplausible?.(usd); - return usd; + return Number.isFinite(usd) && usd > 0 ? usd : null; } /** Starting-market-cap presets in whole GITLAWB for the same dollar targets the stock form uses; empty while the price is unknown. */ diff --git a/app/src/lib/launchpad/gitlawbServer.ts b/app/src/lib/launchpad/gitlawbServer.ts index a44c63f4..1cf437ba 100644 --- a/app/src/lib/launchpad/gitlawbServer.ts +++ b/app/src/lib/launchpad/gitlawbServer.ts @@ -3,16 +3,21 @@ import { publicClient } from "@/lib/chain"; import { STATE_VIEW_ABI } from "./abi"; import { launchpad } from "./config"; import { ethUsd } from "./ethPrice"; -import { GITLAWB_POOL_ID, gitlawbUsdFromSqrtPrice } from "./gitlawb"; +import { GITLAWB_POOL_ID, GITLAWB_V3_POOL, TWAP_WINDOW_S, gitlawbUsdFromSqrtPrice, gitlawbUsdFromTick, reconcileGitlawbUsd, twapTick } from "./gitlawb"; /** - * USD per GITLAWB, server-side: StateView.getSlot0(WETH/GITLAWB pool) × ETH/USD. + * USD per GITLAWB, server-side. Two on-chain readings, one ETH/USD: + * - spot: StateView.getSlot0 on the Uniswap v4 WETH/GITLAWB pool (the deep market) + * - TWAP: observe([1800, 0]) on the Uniswap v3 WETH/GITLAWB pool (30-minute time-weighted average) + * The spot is published unless it strays >25% from the TWAP, in which case the TWAP is published: a + * single-block push of the v4 price cannot rewrite market caps, USD volume or trending scores. When + * only one side reads, that side is used (logged when it is the unguarded spot). * * Stale-while-revalidate so it never sits on a request's critical path once warm: a fresh value is * served for 60s; after that the cached value is returned immediately and ONE refresh runs in the - * background. Only a cold process (no value yet) awaits the read, and that wait is capped at 4s. - * Failures keep the last good price for up to 15 minutes; after that GITLAWB pools simply show no - * USD (and rank with 0 weight in USD sorts) until a read succeeds. Never throws. + * background. Only a cold process (no value yet) awaits the read, capped at 4s. Failures keep the last + * good price for up to 15 minutes; after that GITLAWB pools simply show no USD (and rank with 0 weight + * in USD sorts) until a read succeeds. Never throws. */ let usdNow: number | null = null; let goodAt = 0; // when usdNow was last read successfully @@ -23,13 +28,27 @@ const MAX_STALE_MS = 15 * 60_000; const COLD_WAIT_MS = 4_000; const READ_TIMEOUT_MS = 8_000; +const V3_OBSERVE_ABI = [ + { type: "function", name: "observe", stateMutability: "view", inputs: [{ name: "secondsAgos", type: "uint32[]" }], outputs: [{ name: "tickCumulatives", type: "int56[]" }, { name: "secondsPerLiquidityCumulativeX128s", type: "uint160[]" }] }, +] as const; + async function read(): Promise { + const client = publicClient("base"); const timeout = new Promise((_, reject) => setTimeout(() => reject(new Error(`timeout after ${READ_TIMEOUT_MS}ms`)), READ_TIMEOUT_MS).unref?.()); - const [eth, slot0] = await Promise.race([ - Promise.all([ethUsd(), publicClient("base").readContract({ address: launchpad("base").v4.stateView, abi: STATE_VIEW_ABI, functionName: "getSlot0", args: [GITLAWB_POOL_ID] })]), + const [eth, slot0, observed] = await Promise.race([ + Promise.all([ + ethUsd(), + client.readContract({ address: launchpad("base").v4.stateView, abi: STATE_VIEW_ABI, functionName: "getSlot0", args: [GITLAWB_POOL_ID] }).then((r) => r[0] as bigint, () => null), + client.readContract({ address: GITLAWB_V3_POOL, abi: V3_OBSERVE_ABI, functionName: "observe", args: [[TWAP_WINDOW_S, 0]] }).then((r) => r[0] as readonly bigint[], () => null), + ]), timeout, ]); - return gitlawbUsdFromSqrtPrice(slot0[0], eth, (usd) => console.warn(`[gitlawb] implausible price reading: $${usd} per GITLAWB (used anyway)`)); + const spot = slot0 === null ? null : gitlawbUsdFromSqrtPrice(slot0, eth); + const twap = observed === null || observed.length < 2 ? null : gitlawbUsdFromTick(twapTick(observed[0], observed[1], TWAP_WINDOW_S), eth); + const { usd, source, deviation } = reconcileGitlawbUsd(spot, twap); + if (source === "twap" && spot !== null) console.warn(`[gitlawb] v4 spot $${spot} is ${Math.round((deviation ?? 0) * 100)}% off the 30m v3 TWAP $${twap}; publishing the TWAP`); + else if (source === "spot" && twap === null) console.warn("[gitlawb] v3 TWAP unavailable; publishing the unguarded v4 spot"); + return usd; } function refresh(): Promise {