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

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
16 changes: 13 additions & 3 deletions app/src/app/api/boost/mod/route.ts
Original file line number Diff line number Diff line change
@@ -1,16 +1,26 @@
import { NextResponse } from "next/server";
import { rateLimited } from "@/lib/launchpad/editServer";
import { clientIp, readJson } from "@/lib/profiles/http";
import { moderateBoost } from "@/lib/launchpad/boostServer";
import { getBoostLane, moderateBoost } from "@/lib/launchpad/boostServer";
import { ethUsd } from "@/lib/launchpad/ethPrice";

export const dynamic = "force-dynamic";

/** POST {action: hide|unhide, token, wallet, nonce, ts, signature}: an admin wallet keeps a Base token out of the Boosted lane, or lets it back. */
/**
* POST {action: hide|unhide, token, wallet, nonce, ts, signature}: an admin wallet keeps a Base token out of the Boosted
* lane, or lets it back. A signed, single-use request, so its answer may carry the lane read fresh (both few-second
* caches skipped): the panel shows the result at once, whichever machine its next read reaches. `lane` is null when
* that read fails; the write itself has succeeded.
*/
export async function POST(req: Request) {
if (rateLimited(`boost-mod:ip:${clientIp(req)}`, 30)) return NextResponse.json({ error: "slow down" }, { status: 429 });
const b = await readJson(req);
if (!b) return NextResponse.json({ error: "bad json" }, { status: 400 });
const r = await moderateBoost({ action: b.action, token: b.token, wallet: b.wallet, nonce: b.nonce, ts: b.ts, signature: b.signature });
if (!r.ok) return NextResponse.json({ error: r.error }, { status: r.status });
return NextResponse.json(r);
const lane = await getBoostLane(await ethUsd(), { fresh: true }).catch((err) => {
console.warn("[boost] fresh lane after moderation failed:", err instanceof Error ? err.message : err);
return null;
});
return NextResponse.json({ ...r, lane });
}
3 changes: 2 additions & 1 deletion app/src/app/api/boost/route.ts
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,8 @@ export async function GET(req: Request) {
if (!isAddress(token)) return NextResponse.json({ error: "bad token" }, { status: 400 });
return NextResponse.json(await getTokenBoost(token), { headers: NO_STORE });
}
// the same few-second cache pages and polls read: a caller polling this cannot add a database pass per request
// the same few-second cache pages and polls read: a caller polling this cannot add a database pass per request.
// The only fresh read is the signed moderation answer (api/boost/mod), right after a hide or unhide.
const lane = await boostLaneOrNull(await ethUsd());
if (!lane) return NextResponse.json({ error: "could not load boosts" }, { status: 502 });
return NextResponse.json(lane, { headers: NO_STORE });
Expand Down
2 changes: 1 addition & 1 deletion app/src/app/llms.txt/route.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@ To boost a launch: TWIG (${BOOST_TOKEN}).transfer(0x…dEaD, amount) with amount
(whole = a count of whole TWIG, at least ${BOOST_FLOOR_TWIG}; token_id = the launch's position id, the list API's token_id field,
greater than 0 and less than 10^11). The last 12 digits name the launch; a transfer outside these bounds is not a boost.
Then POST ${SITE_URL}/api/launch/sync?chain=base&tx=0x… to show it at once. Burns are final.
- GET ${SITE_URL}/api/boost?chain=base (the lane: twig, boosts, last_at, expires_at, clicks, top_booster, launch; entry)
- GET ${SITE_URL}/api/boost?chain=base (the lane: twig, boosts, last_at, expires_at, clicks, top_booster, launch; names)
- GET ${SITE_URL}/api/boost?chain=base&token=0x… (one token: twig in the window, place, hidden)
`;
}
Expand Down
3 changes: 2 additions & 1 deletion app/src/app/t/[chain]/[token]/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ import TokenTrades from "@/components/launchpad/TokenTrades";
import LaunchReceipt from "@/components/launchpad/LaunchReceipt";
import TokenAbout from "@/components/launchpad/TokenAbout";
import NamesProvider from "@/components/profile/NamesProvider";
import { intentUrl } from "@/lib/profiles/xpost";
import { namesFor, type NameEntry } from "@/lib/profiles/server";
import { getHolderPanel } from "@/lib/launchpad/holdersServer";
import { memo } from "@/lib/launchpad/memo";
Expand Down Expand Up @@ -220,7 +221,7 @@ export default async function TokenPage({ params, searchParams }: { params: Prom
<WatchButton token={{ chain, token: l.token, name: l.name, symbol: l.symbol }} labelled />
{/* the token's links as one bar, the Watch button's height: share, copy the address, the explorer */}
<div className="inline-flex h-10 max-w-full items-stretch overflow-hidden rounded-xl border border-line bg-card text-xs font-medium text-body sm:h-9">
<a href={`https://x.com/intent/post?text=${encodeURIComponent(shareText)}&url=${encodeURIComponent(`${SITE_URL}/t/${chain}/${l.token}`)}`} target="_blank" rel="noreferrer" className={`inline-flex ${segment}`} aria-label="Share token on X"><Share2 size={13} aria-hidden="true" /><span className="hidden sm:inline">Share</span></a>
<a href={intentUrl(shareText, { url: `${SITE_URL}/t/${chain}/${l.token}` })} target="_blank" rel="noreferrer" className={`inline-flex ${segment}`} aria-label="Share token on X"><Share2 size={13} aria-hidden="true" /><span className="hidden sm:inline">Share</span></a>
<CopyChip value={l.token} className={`inline-flex ${segment} border-l border-line font-code`} />
{/* phones keep one row of actions; the explorer link is also in About & contracts */}
<a href={explorerAddress(chain, l.token)} target="_blank" rel="noreferrer" className={`hidden sm:inline-flex ${segment} border-l border-line`}>{explorerName(chain)}<ArrowUpRight size={13} aria-hidden="true" /></a>
Expand Down
13 changes: 9 additions & 4 deletions app/src/components/launchpad/BoostAdmin.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -48,12 +48,17 @@ export default function BoostAdmin() {
const signature = await wallet.signMessage({ message: buildBoostModMessage({ action, token, wallet: address, nonce: n, ts }) });
const res = await fetch("/api/boost/mod", { method: "POST", headers: { "content-type": "application/json" }, body: JSON.stringify({ action, token, wallet: address, nonce: n, ts, signature }) });
// an error page that is not JSON (a 502 from the proxy) still reports the HTTP failure, not a parse error
const d = (await res.json().catch(() => ({}))) as { error?: string };
const d = (await res.json().catch(() => ({}))) as { error?: string; lane?: BoostLane | null };
if (!res.ok) throw new Error(d.error ?? `request failed (HTTP ${res.status})`);
setMsg({ ok: true, text: `${action === "hide" ? "Hidden" : "Shown again"}: ${token}` });
// a hidden card leaves the list at once; the next read (fresh on the machine that took the write) confirms it
if (action === "hide") setLane((l) => (l?.enabled ? { ...l, items: l.items.filter((b) => b.token !== token.toLowerCase()) } : l));
await load();
// the signed answer carries the lane read fresh, so the list is right at once, whichever machine answers next;
// without it (that read failed) the card still leaves at once and a cached read follows
if (d.lane) {
setLane(d.lane);
} else {
if (action === "hide") setLane((l) => (l?.enabled ? { ...l, items: l.items.filter((b) => b.token !== token.toLowerCase()) } : l));
await load();
}
} catch (e) {
setMsg({ ok: false, text: friendlyError(e) });
}
Expand Down
5 changes: 4 additions & 1 deletion app/src/components/launchpad/BoostDialog.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@ import { DEAD } from "@/lib/launchpad/config";
import { TWIG_WRAP_URL } from "@/lib/launchpad/twig";
import { fmtUsd } from "@/lib/launchpad/math";
import { friendlyError } from "@/lib/errors";
import { BUILDER_DATA_SUFFIX, CHAINS, explorerTx } from "@/lib/chainPublic";
import { BUILDER_DATA_SUFFIX, CHAINS, SITE_URL, explorerTx } from "@/lib/chainPublic";
import {
BOOST_CHAIN,
BOOST_COPY,
Expand All @@ -33,6 +33,7 @@ import {
isLaunchTag,
maxBoostWhole,
parseBoostWhole,
boostShareUrl,
} from "@/lib/launchpad/boost";
import { toast } from "./TxToasts";
import { TwigMark } from "./TwigBadge";
Expand Down Expand Up @@ -150,6 +151,8 @@ export default function BoostDialog({ token, tokenId, symbol, name, twigUsd, cur
<p className="text-xs leading-relaxed text-muted">{fmtBoostTwig(boostAmount(phase.whole, tag))} went to 0x…dEaD. It counts for the next 24 hours and adds up with any other boost on {symbol}; the lane shows the 5 largest totals.</p>
<div className="flex flex-wrap gap-2">
<a href={explorerTx(BOOST_CHAIN, phase.hash)} target="_blank" rel="noreferrer" className={btn.secondarySm}>View transaction<ArrowUpRight size={13} aria-hidden="true" /></a>
{/* the booster's own post: what was sent and where it went, the token's page as its link */}
<a href={boostShareUrl(symbol, boostAmount(phase.whole, tag), `${SITE_URL}/t/${BOOST_CHAIN}/${token}`)} target="_blank" rel="noreferrer" className={btn.secondarySm}>Share on X<ArrowUpRight size={13} aria-hidden="true" /></a>
<Link href="/#boosted" onClick={onClose} className={btn.secondarySm}>See the lane</Link>
<button type="button" onClick={onClose} className={btn.primarySm}>Done</button>
</div>
Expand Down
17 changes: 13 additions & 4 deletions app/src/components/launchpad/BoostPanel.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,21 +2,21 @@

import dynamic from "next/dynamic";
import { useCallback, useEffect, useRef, useState } from "react";
import { ArrowUpRight } from "lucide-react";
import { ArrowUpRight, MousePointerClick } from "lucide-react";
import { btn } from "@/components/ui";
import { useLive } from "./LiveProvider";
import { TwigMark } from "./TwigBadge";
import type { TokenBoost } from "@/lib/launchpad/boostServer";
import { BOOST_CHAIN, BOOST_COPY, boostTimeLeft, fmtBoostTwig } from "@/lib/launchpad/boost";
import { BOOST_CHAIN, BOOST_COPY, BOOST_PANEL_CLICKS_MIN, boostTimeLeft, fmtBoostTwig } from "@/lib/launchpad/boost";
import { explorerTx } from "@/lib/chainPublic";
import type { PendingBoost } from "./BoostDialog";

// the dialog (wallet reads, the send flow) loads on first use, not with the page
const BoostDialog = dynamic(() => import("./BoostDialog"), { ssr: false });

/**
* A Base token's boost line on its page: whether it is in the Boosted lane, with how much TWIG and for how long, and
* the button that opens the dialog. Nothing while BOOST is off. A token an admin hid says so and offers no button, so
* A Base token's boost line on its page: whether it is in the Boosted lane, with how much TWIG and for how long, what
* the lane brought it (clicks in the same 24 hours, from BOOST_PANEL_CLICKS_MIN up), and the button that opens the dialog. Nothing while BOOST is off. A token an admin hid says so and offers no button, so
* nobody sends TWIG for a place it cannot get. `autoOpen` comes from /me's "Boost" link (?boost=1).
*/
export default function BoostPanel({ token, tokenId, symbol, name, initial, twigUsd, serverNow, autoOpen = false }: { token: string; tokenId: number; symbol: string; name: string; initial: TokenBoost; twigUsd: number | null; serverNow: number; autoOpen?: boolean }) {
Expand Down Expand Up @@ -51,6 +51,8 @@ export default function BoostPanel({ token, tokenId, symbol, name, initial, twig
if (!state.enabled) return null;
const left = state.last_at ? boostTimeLeft(state.last_at, now) : null;
const active = left !== null && BigInt(state.twig) > 0n;
// absent from a machine on an older build mid-deploy: no line, never "undefined"
const clicks = state.clicks ?? 0;
return (
<section id="boost" aria-labelledby="boost-heading" className="scroll-mt-24 rounded-2xl border border-line bg-card px-4 py-3.5 sm:px-5">
<div className="flex items-center justify-between gap-3">
Expand All @@ -68,6 +70,13 @@ export default function BoostPanel({ token, tokenId, symbol, name, initial, twig
<>Not boosted. Sending TWIG to 0x…dEaD puts {symbol} in the Boosted lane for 24 hours.</>
)}
</p>
{/* what the lane brought it: only while boosted, and not until it reads like a result */}
{active && clicks >= BOOST_PANEL_CLICKS_MIN ? (
<p className="mt-1 flex items-center gap-1.5 text-xs text-body">
<MousePointerClick size={12} aria-hidden="true" className="shrink-0 text-muted" />
<span><span className="font-mono text-ink tnum">{clicks.toLocaleString("en-US")}</span> clicks from the lane in the last 24 hours</span>
</p>
) : null}
<div className="mt-3 flex flex-wrap items-center gap-x-3 gap-y-2">
<button type="button" onClick={() => setOpen(true)} className={btn.secondarySm}>{pending ? "Boost sent: check it" : "Boost with TWIG"}</button>
{active && state.last_tx ? <a href={explorerTx(BOOST_CHAIN, state.last_tx)} target="_blank" rel="noreferrer" className="inline-flex items-center gap-0.5 text-[11px] text-muted hover:text-ink">Latest burn<ArrowUpRight size={11} aria-hidden="true" /></a> : null}
Expand Down
46 changes: 43 additions & 3 deletions app/src/components/launchpad/boost-ui.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -189,7 +189,7 @@ test("CodeRabbit round 1: the done screen, admin errors and the agent formula",
assert.doesNotMatch(dialog, /settle\([^)]*, true\)/);
assert.match(dialog, /const showsAfter = \(w: bigint\): boolean => !tagOk \|\| boostShowsNow\(entry, current, boostAmount\(w, tag\), inLane\);/);
// an error page that is not JSON still reports the HTTP failure; a bad lane read never throws from the effect
assert.match(admin, /const d = \(await res\.json\(\)\.catch\(\(\) => \(\{\}\)\)\) as \{ error\?: string \};/);
assert.match(admin, /const d = \(await res\.json\(\)\.catch\(\(\) => \(\{\}\)\)\) as \{ error\?: string; lane\?: BoostLane \| null \};/);
assert.match(admin, /throw new Error\(d\.error \?\? `request failed \(HTTP \$\{res\.status\}\)`\);/);
assert.match(admin, /const load = useCallback\(async \(\) => \{\n\s*try \{/);
// agents get the bounds the decoder enforces (isLaunchTag: 0 < tag < 10^11; whole TWIG >= the floor)
Expand Down Expand Up @@ -345,9 +345,19 @@ test("the lane invites a boost: a header button, every open place a button, and
assert.match(server, /items, names: await creditNames\(\[\.\.\.boosters\.values\(\)\]\) \};/);
assert.match(server, /const known = await namesFor\(wallets\)\.catch\(\(\) => null\);\n\s*return known \? Object\.fromEntries\(wallets\.map\(\(w\) => \[w, known\[w\] \?\? null\]\)\) : \{\};/);
assert.match(lane, /<NamesProvider names=\{lane\.names \?\? NO_NAMES\}>/);
// a public read of the lane comes from the same few-second cache: polling it adds no database pass
// a public read of the lane comes from the same few-second cache: polling it adds no database pass, and no query
// parameter skips it (CodeRabbit #102: a wallet in the URL is not proof of an admin)
assert.match(boostRoute, /const lane = await boostLaneOrNull\(await ethUsd\(\)\);/);
assert.doesNotMatch(boostRoute, /getBoostLane/);
assert.doesNotMatch(boostRoute, /getBoostLane|searchParams\.get\("fresh"\)|adminWallets/);
// the only fresh read is the answer to a signed, single-use moderation request, after the write
const modRoute = read("../../app/api/boost/mod/route.ts");
assert.ok(modRoute.indexOf("if (!r.ok) return") < modRoute.indexOf("getBoostLane(await ethUsd(), { fresh: true })"), "fresh only after a verified write");
assert.match(modRoute, /return NextResponse\.json\(\{ \.\.\.r, lane \}\);/);
assert.match(server, /const ranked = opts\.fresh \? rankLane\(await laneTotals\(BOOST_LANE_SIZE \* 4\), BOOST_LANE_SIZE \* 4\) : await rankedLane\(\);/);
// the panel takes that lane; its own reads are the cached ones everyone gets
assert.match(admin, /if \(d\.lane\) \{\n\s*setLane\(d\.lane\);\n\s*\} else \{/);
assert.match(admin, /fetch\(`\/api\/boost\?chain=\$\{BOOST_CHAIN\}`, \{ cache: "no-store" \}\)/);
assert.doesNotMatch(admin, /fresh=1|justHidden|JUST_HIDDEN/);
});

test("the picker only navigates: the boost happens on the token's page, with every check the dialog has", () => {
Expand Down Expand Up @@ -382,3 +392,33 @@ test("a card credits the wallet that sent the most TWIG for it; a failed read sh
assert.match(body, /\} catch \(err\) \{\n\s*console\.warn\("\[boost\] lane boosters read failed:"[^\n]*\n\s*return new Map\(\);/);
assert.match(lane, /\{b\.top_booster \? \(/);
});


test("the token page shows what the lane brought: clicks in the same 24 hours, from 10 up, only while boosted", () => {
assert.match(panel, /const clicks = state\.clicks \?\? 0;/);
assert.match(panel, /\{active && clicks >= BOOST_PANEL_CLICKS_MIN \? \(/);
assert.match(panel, /clicks from the lane in the last 24 hours/);
// the lane card's own count (laneClicks: same window, never throws), and only for a token boosted in the window
const body = server.slice(server.indexOf("export async function getTokenBoost("), server.indexOf("/* ─────────────────────────────── views and clicks"));
assert.match(body, /const clicks = row\?\.last_at \? \(\(await laneClicks\(\[t\]\)\)\.get\(t\) \?\? 0\) : 0;/);
assert.doesNotMatch(body, /FROM bb_boost_clicks/, "one click count for the card and the page, not two copies");
});

test("after a confirmed boost the booster can share it on X: its own post, in a new tab", () => {
const done = dialog.slice(dialog.indexOf('{phase.k === "done" ? ('), dialog.indexOf(") : ("));
assert.match(done, /<a href=\{boostShareUrl\(symbol, boostAmount\(phase\.whole, tag\), `\$\{SITE_URL\}\/t\/\$\{BOOST_CHAIN\}\/\$\{token\}`\)\} target="_blank" rel="noreferrer" className=\{btn\.secondarySm\}>Share on X/);
assert.equal((dialog.match(/boostShareUrl\(/g) ?? []).length, 1, "only on the done screen");
});

test("the agents' notes name the lane's fields as they are", () => {
assert.match(llms, /\(the lane: twig, boosts, last_at, expires_at, clicks, top_booster, launch; names\)/);
assert.doesNotMatch(llms, /launch; entry\)/);
});



test("one X intent builder: the booster's post and the token page's Share both go through intentUrl", () => {
assert.match(read("../../lib/launchpad/boost.ts"), /return intentUrl\(boostShareText\(symbol, raw\), \{ url: pageUrl, via: BRAND_X \}\);/);
assert.match(tokenPage, /<a href=\{intentUrl\(shareText, \{ url: `\$\{SITE_URL\}\/t\/\$\{chain\}\/\$\{l\.token\}` \}\)\} target="_blank"/);
for (const f of ["../../lib/launchpad/boost.ts", "../../app/t/[chain]/[token]/page.tsx"]) assert.doesNotMatch(read(f), /https:\/\/x\.com\/intent/, f);
});
20 changes: 20 additions & 0 deletions app/src/lib/launchpad/boost.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,9 @@ import {
BOOST_CLICK_SLOT_MS,
BOOST_CLICKS_KEEP_DAYS,
BOOST_FROM_FLOOR,
BOOST_PANEL_CLICKS_MIN,
boostShareText,
boostShareUrl,
boostEntryLine,
topBoosters,
BOOST_SOURCES,
Expand Down Expand Up @@ -345,3 +348,20 @@ test("a card credits the wallet that sent the most TWIG in the window; a tie goe
assert.equal(picked.get("0xt3"), "0x999");
assert.equal(topBoosters([]).size, 0);
});

test("a booster's post says what was sent and where it went, links the token's page, and tags the official account", () => {
const raw = 1_000_000n * E18 + ID;
assert.equal(boostShareText("MUSEWORLD", raw), "Boosted $MUSEWORLD with 1M TWIG on openlaunch.lol. The TWIG went to 0x…dEaD.");
assert.equal(boostShareText("BSTR", (426_248n * E18).toString()), "Boosted $BSTR with 426.25K TWIG on openlaunch.lol. The TWIG went to 0x…dEaD.");
const url = new URL(boostShareUrl("A&B #1", raw, "https://openlaunch.lol/t/base/0xabc"));
assert.equal(url.origin + url.pathname, "https://x.com/intent/post", "the same intent the token page's Share uses");
assert.equal(url.searchParams.get("text"), "Boosted $A&B #1 with 1M TWIG on openlaunch.lol. The TWIG went to 0x…dEaD.", "a symbol never breaks out of its parameter");
assert.equal(url.searchParams.get("url"), "https://openlaunch.lol/t/base/0xabc");
assert.equal(url.searchParams.get("via"), "openlaunch_lol");
const banned = /\b(moon|rally|buyback|guarantee[ds]?|pump|gem|100x|profit|invest(ment)?|returns?|yield|airdrop|reward)\b/i;
assert.doesNotMatch(boostShareText("X", raw), banned);
});

test("the token page shows its clicks from the lane only once they read like a result", () => {
assert.equal(BOOST_PANEL_CLICKS_MIN, 10);
});
Loading
Loading