Skip to content
Merged
Show file tree
Hide file tree
Changes from 2 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
9 changes: 6 additions & 3 deletions app/src/app/api/boost/route.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,8 @@ import { rateLimited } from "@/lib/launchpad/editServer";
import { clientIp } from "@/lib/profiles/http";
import { ethUsd } from "@/lib/launchpad/ethPrice";
import { BOOST_CHAIN } from "@/lib/launchpad/boost";
import { boostEnabled, boostLaneOrNull, getTokenBoost } from "@/lib/launchpad/boostServer";
import { boostEnabled, boostLaneOrNull, getBoostLane, getTokenBoost } from "@/lib/launchpad/boostServer";
import { adminWallets } from "@/lib/launchpad/postsServer";

export const dynamic = "force-dynamic";

Expand All @@ -27,8 +28,10 @@ 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
const lane = await boostLaneOrNull(await ethUsd());
// the same few-second cache pages and polls read: a caller polling this cannot add a database pass per request.
// An admin wallet may ask for it fresh (the moderation panel after a hide), like the reported-posts queue's gate.
const fresh = u.searchParams.get("fresh") === "1" && adminWallets().has((u.searchParams.get("wallet") ?? "").toLowerCase());
Comment thread
coderabbitai[bot] marked this conversation as resolved.
Outdated
const lane = fresh ? await getBoostLane(await ethUsd(), { fresh: true }) : await boostLaneOrNull(await ethUsd());
if (!lane) return NextResponse.json({ error: "could not load boosts" }, { status: 502 });
return NextResponse.json(lane, { headers: NO_STORE });
} catch (err) {
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
5 changes: 3 additions & 2 deletions app/src/components/launchpad/BoostAdmin.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -26,14 +26,15 @@ export default function BoostAdmin() {
const [target, setTarget] = useState("");
const [msg, setMsg] = useState<{ ok: boolean; text: string } | null>(null);
// a failed or unreadable answer (a proxy's HTML page) keeps the last list on screen; it never throws from the effect
// an admin wallet reads the lane fresh: right after a hide the other machine's few-second caches may still hold it
const load = useCallback(async () => {
try {
const res = await fetch(`/api/boost?chain=${BOOST_CHAIN}`, { cache: "no-store" });
const res = await fetch(`/api/boost?chain=${BOOST_CHAIN}${address ? `&fresh=1&wallet=${address}` : ""}`, { cache: "no-store" });
if (res.ok) setLane((await res.json()) as BoostLane);
} catch {
// keep the last lane
}
}, []);
}, [address]);
useEffect(() => {
const id = setTimeout(() => void load(), 0);
return () => clearTimeout(id);
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
41 changes: 38 additions & 3 deletions app/src/components/launchpad/boost-ui.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -345,9 +345,14 @@ 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
assert.match(boostRoute, /const lane = await boostLaneOrNull\(await ethUsd\(\)\);/);
assert.doesNotMatch(boostRoute, /getBoostLane/);
// a public read of the lane comes from the same few-second cache: polling it adds no database pass; only an admin
// wallet gets it fresh (the moderation panel after a hide)
assert.match(boostRoute, /const fresh = u\.searchParams\.get\("fresh"\) === "1" && adminWallets\(\)\.has\(\(u\.searchParams\.get\("wallet"\) \?\? ""\)\.toLowerCase\(\)\);/);
assert.match(boostRoute, /const lane = fresh \? await getBoostLane\(await ethUsd\(\), \{ fresh: true \}\) : await boostLaneOrNull\(await ethUsd\(\)\);/);
assert.equal((boostRoute.match(/getBoostLane\(/g) ?? []).length, 1);
assert.match(server, /const ranked = opts\.fresh \? rankLane\(await laneTotals\(BOOST_LANE_SIZE \* 4\), BOOST_LANE_SIZE \* 4\) : await rankedLane\(\);/);
assert.match(admin, /fetch\(`\/api\/boost\?chain=\$\{BOOST_CHAIN\}\$\{address \? `&fresh=1&wallet=\$\{address\}` : ""\}`/);
assert.doesNotMatch(admin, /justHidden|JUST_HIDDEN/, "no timer guess: the admin's read is simply fresh");
});

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 +387,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);
});
20 changes: 20 additions & 0 deletions app/src/lib/launchpad/boost.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,8 @@
*/
import { TWIG_ADDRESS, TWIG_DECIMALS, TWIG_SYMBOL } from "./twig.ts";
import { CHAIN_IDS } from "../chainKeys.ts";
import { BRAND_DOMAIN, BRAND_X } from "../brand.ts";
import { intentUrl } from "../profiles/xpost.ts";

/** Boosts exist on Base only: TWIG has not been bridged anywhere. */
export const BOOST_CHAIN = "base" as const;
Expand Down Expand Up @@ -239,6 +241,24 @@ export function topBoosters(rows: readonly BoosterTotal[]): Map<string, string>
return new Map([...best].map(([token, r]) => [token, r.booster]));
}

/* ─────────────────────────────── results and sharing ─────────────────────────────── */

/** The token page shows its clicks from the lane once there are this many: a fresh boost never reads "3 clicks". */
export const BOOST_PANEL_CLICKS_MIN = 10;

/**
* The post a booster can share once a boost is confirmed: what was sent and where it went, nothing about price. True
* whether or not the boost brought the token into the lane yet.
*/
export function boostShareText(symbol: string, raw: bigint | string): string {
return `Boosted $${symbol} with ${fmtBoostTwig(raw)} on ${BRAND_DOMAIN}. The TWIG went to 0x…dEaD.`;
}

/** The X intent for that post: the token's page as its link, via the official account (the one intent builder, xpost.ts). */
export function boostShareUrl(symbol: string, raw: bigint | string, pageUrl: string): string {
return intentUrl(boostShareText(symbol, raw), { url: pageUrl, via: BRAND_X });
}

/* ─────────────────────────────── views and clicks ─────────────────────────────── */

/**
Expand Down
Loading
Loading