Skip to content

Commit bcb9e4f

Browse files
authored
Merge pull request #102 from Twigpine/feat/boost-results
feat(boost): clicks from the lane on the token page, Share on X after a boost
2 parents 8a66abf + c4a749d commit bcb9e4f

14 files changed

Lines changed: 153 additions & 28 deletions

File tree

‎app/src/app/api/boost/mod/route.ts‎

Lines changed: 13 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,16 +1,26 @@
11
import { NextResponse } from "next/server";
22
import { rateLimited } from "@/lib/launchpad/editServer";
33
import { clientIp, readJson } from "@/lib/profiles/http";
4-
import { moderateBoost } from "@/lib/launchpad/boostServer";
4+
import { getBoostLane, moderateBoost } from "@/lib/launchpad/boostServer";
5+
import { ethUsd } from "@/lib/launchpad/ethPrice";
56

67
export const dynamic = "force-dynamic";
78

8-
/** 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. */
9+
/**
10+
* POST {action: hide|unhide, token, wallet, nonce, ts, signature}: an admin wallet keeps a Base token out of the Boosted
11+
* lane, or lets it back. A signed, single-use request, so its answer may carry the lane read fresh (both few-second
12+
* caches skipped): the panel shows the result at once, whichever machine its next read reaches. `lane` is null when
13+
* that read fails; the write itself has succeeded.
14+
*/
915
export async function POST(req: Request) {
1016
if (rateLimited(`boost-mod:ip:${clientIp(req)}`, 30)) return NextResponse.json({ error: "slow down" }, { status: 429 });
1117
const b = await readJson(req);
1218
if (!b) return NextResponse.json({ error: "bad json" }, { status: 400 });
1319
const r = await moderateBoost({ action: b.action, token: b.token, wallet: b.wallet, nonce: b.nonce, ts: b.ts, signature: b.signature });
1420
if (!r.ok) return NextResponse.json({ error: r.error }, { status: r.status });
15-
return NextResponse.json(r);
21+
const lane = await getBoostLane(await ethUsd(), { fresh: true }).catch((err) => {
22+
console.warn("[boost] fresh lane after moderation failed:", err instanceof Error ? err.message : err);
23+
return null;
24+
});
25+
return NextResponse.json({ ...r, lane });
1626
}

‎app/src/app/api/boost/route.ts‎

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -27,7 +27,8 @@ export async function GET(req: Request) {
2727
if (!isAddress(token)) return NextResponse.json({ error: "bad token" }, { status: 400 });
2828
return NextResponse.json(await getTokenBoost(token), { headers: NO_STORE });
2929
}
30-
// the same few-second cache pages and polls read: a caller polling this cannot add a database pass per request
30+
// the same few-second cache pages and polls read: a caller polling this cannot add a database pass per request.
31+
// The only fresh read is the signed moderation answer (api/boost/mod), right after a hide or unhide.
3132
const lane = await boostLaneOrNull(await ethUsd());
3233
if (!lane) return NextResponse.json({ error: "could not load boosts" }, { status: 502 });
3334
return NextResponse.json(lane, { headers: NO_STORE });

‎app/src/app/llms.txt/route.ts‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,7 @@ To boost a launch: TWIG (${BOOST_TOKEN}).transfer(0x…dEaD, amount) with amount
1515
(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,
1616
greater than 0 and less than 10^11). The last 12 digits name the launch; a transfer outside these bounds is not a boost.
1717
Then POST ${SITE_URL}/api/launch/sync?chain=base&tx=0x… to show it at once. Burns are final.
18-
- GET ${SITE_URL}/api/boost?chain=base (the lane: twig, boosts, last_at, expires_at, clicks, top_booster, launch; entry)
18+
- GET ${SITE_URL}/api/boost?chain=base (the lane: twig, boosts, last_at, expires_at, clicks, top_booster, launch; names)
1919
- GET ${SITE_URL}/api/boost?chain=base&token=0x… (one token: twig in the window, place, hidden)
2020
`;
2121
}

‎app/src/app/t/[chain]/[token]/page.tsx‎

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@ import TokenTrades from "@/components/launchpad/TokenTrades";
1919
import LaunchReceipt from "@/components/launchpad/LaunchReceipt";
2020
import TokenAbout from "@/components/launchpad/TokenAbout";
2121
import NamesProvider from "@/components/profile/NamesProvider";
22+
import { intentUrl } from "@/lib/profiles/xpost";
2223
import { namesFor, type NameEntry } from "@/lib/profiles/server";
2324
import { getHolderPanel } from "@/lib/launchpad/holdersServer";
2425
import { memo } from "@/lib/launchpad/memo";
@@ -220,7 +221,7 @@ export default async function TokenPage({ params, searchParams }: { params: Prom
220221
<WatchButton token={{ chain, token: l.token, name: l.name, symbol: l.symbol }} labelled />
221222
{/* the token's links as one bar, the Watch button's height: share, copy the address, the explorer */}
222223
<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">
223-
<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>
224+
<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>
224225
<CopyChip value={l.token} className={`inline-flex ${segment} border-l border-line font-code`} />
225226
{/* phones keep one row of actions; the explorer link is also in About & contracts */}
226227
<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>

‎app/src/components/launchpad/BoostAdmin.tsx‎

Lines changed: 9 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -48,12 +48,17 @@ export default function BoostAdmin() {
4848
const signature = await wallet.signMessage({ message: buildBoostModMessage({ action, token, wallet: address, nonce: n, ts }) });
4949
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 }) });
5050
// an error page that is not JSON (a 502 from the proxy) still reports the HTTP failure, not a parse error
51-
const d = (await res.json().catch(() => ({}))) as { error?: string };
51+
const d = (await res.json().catch(() => ({}))) as { error?: string; lane?: BoostLane | null };
5252
if (!res.ok) throw new Error(d.error ?? `request failed (HTTP ${res.status})`);
5353
setMsg({ ok: true, text: `${action === "hide" ? "Hidden" : "Shown again"}: ${token}` });
54-
// a hidden card leaves the list at once; the next read (fresh on the machine that took the write) confirms it
55-
if (action === "hide") setLane((l) => (l?.enabled ? { ...l, items: l.items.filter((b) => b.token !== token.toLowerCase()) } : l));
56-
await load();
54+
// the signed answer carries the lane read fresh, so the list is right at once, whichever machine answers next;
55+
// without it (that read failed) the card still leaves at once and a cached read follows
56+
if (d.lane) {
57+
setLane(d.lane);
58+
} else {
59+
if (action === "hide") setLane((l) => (l?.enabled ? { ...l, items: l.items.filter((b) => b.token !== token.toLowerCase()) } : l));
60+
await load();
61+
}
5762
} catch (e) {
5863
setMsg({ ok: false, text: friendlyError(e) });
5964
}

‎app/src/components/launchpad/BoostDialog.tsx‎

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,7 @@ import { DEAD } from "@/lib/launchpad/config";
1616
import { TWIG_WRAP_URL } from "@/lib/launchpad/twig";
1717
import { fmtUsd } from "@/lib/launchpad/math";
1818
import { friendlyError } from "@/lib/errors";
19-
import { BUILDER_DATA_SUFFIX, CHAINS, explorerTx } from "@/lib/chainPublic";
19+
import { BUILDER_DATA_SUFFIX, CHAINS, SITE_URL, explorerTx } from "@/lib/chainPublic";
2020
import {
2121
BOOST_CHAIN,
2222
BOOST_COPY,
@@ -33,6 +33,7 @@ import {
3333
isLaunchTag,
3434
maxBoostWhole,
3535
parseBoostWhole,
36+
boostShareUrl,
3637
} from "@/lib/launchpad/boost";
3738
import { toast } from "./TxToasts";
3839
import { TwigMark } from "./TwigBadge";
@@ -150,6 +151,8 @@ export default function BoostDialog({ token, tokenId, symbol, name, twigUsd, cur
150151
<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>
151152
<div className="flex flex-wrap gap-2">
152153
<a href={explorerTx(BOOST_CHAIN, phase.hash)} target="_blank" rel="noreferrer" className={btn.secondarySm}>View transaction<ArrowUpRight size={13} aria-hidden="true" /></a>
154+
{/* the booster's own post: what was sent and where it went, the token's page as its link */}
155+
<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>
153156
<Link href="/#boosted" onClick={onClose} className={btn.secondarySm}>See the lane</Link>
154157
<button type="button" onClick={onClose} className={btn.primarySm}>Done</button>
155158
</div>

‎app/src/components/launchpad/BoostPanel.tsx‎

Lines changed: 13 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -2,21 +2,21 @@
22

33
import dynamic from "next/dynamic";
44
import { useCallback, useEffect, useRef, useState } from "react";
5-
import { ArrowUpRight } from "lucide-react";
5+
import { ArrowUpRight, MousePointerClick } from "lucide-react";
66
import { btn } from "@/components/ui";
77
import { useLive } from "./LiveProvider";
88
import { TwigMark } from "./TwigBadge";
99
import type { TokenBoost } from "@/lib/launchpad/boostServer";
10-
import { BOOST_CHAIN, BOOST_COPY, boostTimeLeft, fmtBoostTwig } from "@/lib/launchpad/boost";
10+
import { BOOST_CHAIN, BOOST_COPY, BOOST_PANEL_CLICKS_MIN, boostTimeLeft, fmtBoostTwig } from "@/lib/launchpad/boost";
1111
import { explorerTx } from "@/lib/chainPublic";
1212
import type { PendingBoost } from "./BoostDialog";
1313

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

1717
/**
18-
* 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
19-
* the button that opens the dialog. Nothing while BOOST is off. A token an admin hid says so and offers no button, so
18+
* 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
19+
* 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
2020
* nobody sends TWIG for a place it cannot get. `autoOpen` comes from /me's "Boost" link (?boost=1).
2121
*/
2222
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 }) {
@@ -51,6 +51,8 @@ export default function BoostPanel({ token, tokenId, symbol, name, initial, twig
5151
if (!state.enabled) return null;
5252
const left = state.last_at ? boostTimeLeft(state.last_at, now) : null;
5353
const active = left !== null && BigInt(state.twig) > 0n;
54+
// absent from a machine on an older build mid-deploy: no line, never "undefined"
55+
const clicks = state.clicks ?? 0;
5456
return (
5557
<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">
5658
<div className="flex items-center justify-between gap-3">
@@ -68,6 +70,13 @@ export default function BoostPanel({ token, tokenId, symbol, name, initial, twig
6870
<>Not boosted. Sending TWIG to 0x…dEaD puts {symbol} in the Boosted lane for 24 hours.</>
6971
)}
7072
</p>
73+
{/* what the lane brought it: only while boosted, and not until it reads like a result */}
74+
{active && clicks >= BOOST_PANEL_CLICKS_MIN ? (
75+
<p className="mt-1 flex items-center gap-1.5 text-xs text-body">
76+
<MousePointerClick size={12} aria-hidden="true" className="shrink-0 text-muted" />
77+
<span><span className="font-mono text-ink tnum">{clicks.toLocaleString("en-US")}</span> clicks from the lane in the last 24 hours</span>
78+
</p>
79+
) : null}
7180
<div className="mt-3 flex flex-wrap items-center gap-x-3 gap-y-2">
7281
<button type="button" onClick={() => setOpen(true)} className={btn.secondarySm}>{pending ? "Boost sent: check it" : "Boost with TWIG"}</button>
7382
{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}

‎app/src/components/launchpad/boost-ui.test.ts‎

Lines changed: 43 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -189,7 +189,7 @@ test("CodeRabbit round 1: the done screen, admin errors and the agent formula",
189189
assert.doesNotMatch(dialog, /settle\([^)]*, true\)/);
190190
assert.match(dialog, /const showsAfter = \(w: bigint\): boolean => !tagOk \|\| boostShowsNow\(entry, current, boostAmount\(w, tag\), inLane\);/);
191191
// an error page that is not JSON still reports the HTTP failure; a bad lane read never throws from the effect
192-
assert.match(admin, /const d = \(await res\.json\(\)\.catch\(\(\) => \(\{\}\)\)\) as \{ error\?: string \};/);
192+
assert.match(admin, /const d = \(await res\.json\(\)\.catch\(\(\) => \(\{\}\)\)\) as \{ error\?: string; lane\?: BoostLane \| null \};/);
193193
assert.match(admin, /throw new Error\(d\.error \?\? `request failed \(HTTP \$\{res\.status\}\)`\);/);
194194
assert.match(admin, /const load = useCallback\(async \(\) => \{\n\s*try \{/);
195195
// agents get the bounds the decoder enforces (isLaunchTag: 0 < tag < 10^11; whole TWIG >= the floor)
@@ -345,9 +345,19 @@ test("the lane invites a boost: a header button, every open place a button, and
345345
assert.match(server, /items, names: await creditNames\(\[\.\.\.boosters\.values\(\)\]\) \};/);
346346
assert.match(server, /const known = await namesFor\(wallets\)\.catch\(\(\) => null\);\n\s*return known \? Object\.fromEntries\(wallets\.map\(\(w\) => \[w, known\[w\] \?\? null\]\)\) : \{\};/);
347347
assert.match(lane, /<NamesProvider names=\{lane\.names \?\? NO_NAMES\}>/);
348-
// a public read of the lane comes from the same few-second cache: polling it adds no database pass
348+
// a public read of the lane comes from the same few-second cache: polling it adds no database pass, and no query
349+
// parameter skips it (CodeRabbit #102: a wallet in the URL is not proof of an admin)
349350
assert.match(boostRoute, /const lane = await boostLaneOrNull\(await ethUsd\(\)\);/);
350-
assert.doesNotMatch(boostRoute, /getBoostLane/);
351+
assert.doesNotMatch(boostRoute, /getBoostLane|searchParams\.get\("fresh"\)|adminWallets/);
352+
// the only fresh read is the answer to a signed, single-use moderation request, after the write
353+
const modRoute = read("../../app/api/boost/mod/route.ts");
354+
assert.ok(modRoute.indexOf("if (!r.ok) return") < modRoute.indexOf("getBoostLane(await ethUsd(), { fresh: true })"), "fresh only after a verified write");
355+
assert.match(modRoute, /return NextResponse\.json\(\{ \.\.\.r, lane \}\);/);
356+
assert.match(server, /const ranked = opts\.fresh \? rankLane\(await laneTotals\(BOOST_LANE_SIZE \* 4\), BOOST_LANE_SIZE \* 4\) : await rankedLane\(\);/);
357+
// the panel takes that lane; its own reads are the cached ones everyone gets
358+
assert.match(admin, /if \(d\.lane\) \{\n\s*setLane\(d\.lane\);\n\s*\} else \{/);
359+
assert.match(admin, /fetch\(`\/api\/boost\?chain=\$\{BOOST_CHAIN\}`, \{ cache: "no-store" \}\)/);
360+
assert.doesNotMatch(admin, /fresh=1|justHidden|JUST_HIDDEN/);
351361
});
352362

353363
test("the picker only navigates: the boost happens on the token's page, with every check the dialog has", () => {
@@ -382,3 +392,33 @@ test("a card credits the wallet that sent the most TWIG for it; a failed read sh
382392
assert.match(body, /\} catch \(err\) \{\n\s*console\.warn\("\[boost\] lane boosters read failed:"[^\n]*\n\s*return new Map\(\);/);
383393
assert.match(lane, /\{b\.top_booster \? \(/);
384394
});
395+
396+
397+
test("the token page shows what the lane brought: clicks in the same 24 hours, from 10 up, only while boosted", () => {
398+
assert.match(panel, /const clicks = state\.clicks \?\? 0;/);
399+
assert.match(panel, /\{active && clicks >= BOOST_PANEL_CLICKS_MIN \? \(/);
400+
assert.match(panel, /clicks from the lane in the last 24 hours/);
401+
// the lane card's own count (laneClicks: same window, never throws), and only for a token boosted in the window
402+
const body = server.slice(server.indexOf("export async function getTokenBoost("), server.indexOf("/* ─────────────────────────────── views and clicks"));
403+
assert.match(body, /const clicks = row\?\.last_at \? \(\(await laneClicks\(\[t\]\)\)\.get\(t\) \?\? 0\) : 0;/);
404+
assert.doesNotMatch(body, /FROM bb_boost_clicks/, "one click count for the card and the page, not two copies");
405+
});
406+
407+
test("after a confirmed boost the booster can share it on X: its own post, in a new tab", () => {
408+
const done = dialog.slice(dialog.indexOf('{phase.k === "done" ? ('), dialog.indexOf(") : ("));
409+
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/);
410+
assert.equal((dialog.match(/boostShareUrl\(/g) ?? []).length, 1, "only on the done screen");
411+
});
412+
413+
test("the agents' notes name the lane's fields as they are", () => {
414+
assert.match(llms, /\(the lane: twig, boosts, last_at, expires_at, clicks, top_booster, launch; names\)/);
415+
assert.doesNotMatch(llms, /launch; entry\)/);
416+
});
417+
418+
419+
420+
test("one X intent builder: the booster's post and the token page's Share both go through intentUrl", () => {
421+
assert.match(read("../../lib/launchpad/boost.ts"), /return intentUrl\(boostShareText\(symbol, raw\), \{ url: pageUrl, via: BRAND_X \}\);/);
422+
assert.match(tokenPage, /<a href=\{intentUrl\(shareText, \{ url: `\$\{SITE_URL\}\/t\/\$\{chain\}\/\$\{l\.token\}` \}\)\} target="_blank"/);
423+
for (const f of ["../../lib/launchpad/boost.ts", "../../app/t/[chain]/[token]/page.tsx"]) assert.doesNotMatch(read(f), /https:\/\/x\.com\/intent/, f);
424+
});

‎app/src/lib/launchpad/boost.test.ts‎

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,9 @@ import {
3434
BOOST_CLICK_SLOT_MS,
3535
BOOST_CLICKS_KEEP_DAYS,
3636
BOOST_FROM_FLOOR,
37+
BOOST_PANEL_CLICKS_MIN,
38+
boostShareText,
39+
boostShareUrl,
3740
boostEntryLine,
3841
topBoosters,
3942
BOOST_SOURCES,
@@ -345,3 +348,20 @@ test("a card credits the wallet that sent the most TWIG in the window; a tie goe
345348
assert.equal(picked.get("0xt3"), "0x999");
346349
assert.equal(topBoosters([]).size, 0);
347350
});
351+
352+
test("a booster's post says what was sent and where it went, links the token's page, and tags the official account", () => {
353+
const raw = 1_000_000n * E18 + ID;
354+
assert.equal(boostShareText("MUSEWORLD", raw), "Boosted $MUSEWORLD with 1M TWIG on openlaunch.lol. The TWIG went to 0x…dEaD.");
355+
assert.equal(boostShareText("BSTR", (426_248n * E18).toString()), "Boosted $BSTR with 426.25K TWIG on openlaunch.lol. The TWIG went to 0x…dEaD.");
356+
const url = new URL(boostShareUrl("A&B #1", raw, "https://openlaunch.lol/t/base/0xabc"));
357+
assert.equal(url.origin + url.pathname, "https://x.com/intent/post", "the same intent the token page's Share uses");
358+
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");
359+
assert.equal(url.searchParams.get("url"), "https://openlaunch.lol/t/base/0xabc");
360+
assert.equal(url.searchParams.get("via"), "openlaunch_lol");
361+
const banned = /\b(moon|rally|buyback|guarantee[ds]?|pump|gem|100x|profit|invest(ment)?|returns?|yield|airdrop|reward)\b/i;
362+
assert.doesNotMatch(boostShareText("X", raw), banned);
363+
});
364+
365+
test("the token page shows its clicks from the lane only once they read like a result", () => {
366+
assert.equal(BOOST_PANEL_CLICKS_MIN, 10);
367+
});

0 commit comments

Comments
 (0)