From 60b23a3d23b9caa1c9ca8e455a0a0391e7033f56 Mon Sep 17 00:00:00 2001 From: Kevin Codex Date: Fri, 25 Sep 2026 23:54:31 +0800 Subject: [PATCH] feat(launchpad): MUSEWORLD as an official quote, with badge and USD price MUSEWORLD (Museworld's token) is now a known quote on Base. It is kept out of the launch form: launches paired with it are made inside Museworld by its agents. Its launches are listed with an official badge and a USD price. - Museworld blue badge (logo tile, symbol, check seal) in list rows, trending, activity feed, dashboard and token page; blue "official" pill on share cards - USD price: GITLAWB per MUSEWORLD from our index of its own openlaunch pool, spot checked against a 30-minute time-weighted average (same >25% rule as GITLAWB), times GITLAWB's USD price; ranks in USD sorts - "MUSEWORLD" is reserved for unlisted pairs, so a look-alike at another address shows by address --- app/public/museworld-mark.png | Bin 0 -> 4212 bytes .../app/t/[chain]/[token]/opengraph-image.tsx | 32 +++++--- app/src/app/t/[chain]/[token]/page.tsx | 5 +- app/src/components/launchpad/LaunchRow.tsx | 4 +- app/src/components/launchpad/LaunchTape.tsx | 4 +- app/src/components/launchpad/MeDashboard.tsx | 6 +- .../components/launchpad/MuseworldBadge.tsx | 40 ++++++++++ .../components/launchpad/TrendingStrip.tsx | 5 +- app/src/lib/launchpad/config.ts | 20 +++-- app/src/lib/launchpad/first-buy.ts | 2 +- app/src/lib/launchpad/museworld.test.ts | 73 ++++++++++++++++++ app/src/lib/launchpad/museworld.ts | 51 ++++++++++++ app/src/lib/launchpad/ogcard.ts | 5 +- app/src/lib/launchpad/queries.ts | 47 ++++++++++- app/src/lib/launchpad/unlisted-quote.test.ts | 23 +++--- app/src/lib/launchpad/unlisted-quote.ts | 6 +- 16 files changed, 279 insertions(+), 44 deletions(-) create mode 100644 app/public/museworld-mark.png create mode 100644 app/src/components/launchpad/MuseworldBadge.tsx create mode 100644 app/src/lib/launchpad/museworld.test.ts create mode 100644 app/src/lib/launchpad/museworld.ts diff --git a/app/public/museworld-mark.png b/app/public/museworld-mark.png new file mode 100644 index 0000000000000000000000000000000000000000..c0dbd87038e23d6be37ecef74cb63b82dd64745e GIT binary patch literal 4212 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 ou1s2T`e0x z-&e<36wm`-?T${L^6+&2Ri*2>umiuwh?lR%#N6lU?x7 z*6*7rq1iWz^*;C&)CRQXFfJ>+WcC!AxHv7vHV`UF3PzZH{q4LO2XodgzAY6Q7vDl~ zxXu#u)&CrCF^MNV4vc2_lYEM#Y_$hj_Fd|C(pVPF{C?cbz=Gh|t)+&%3^Ak0!xki7 z<{PRK(EE29Yz(c*8=m_P!1-bv1)mwCveW6c<$Cz)`p80;<%i%N&trn=A78jPU90t~ zT@R}@ol`+hji-LO@s}^9-?yKbesd-1se0}HJngBYX}t+7s6@EHe$ZyI{^WjIqF zdF7esh7rddbWIL`P6P#z_5XBM@&Jx!+^Rt40P#tQ=Zy&E>Kn5Xk4izUmC#Fv%EcPP zI34(1WVQI9qUNPT(pyMMK@DIR1vKy^Oq6}|_Ml~$QU+udK(zx!5H!FFfbRnI07wv^ z{XbfQXxINGkLPbwk6m!CBRFrf?hQ7Oi-AtkW%Fc0S+LE}g$o5VUSy%%*`7d;nbDPC z@*JD_90+Z$N~M4^i~U;t1_3eA>-8+%+J}XO`qaY0eDXTzm^f2{yi3?J;`sT}73Bhu zo!8Aud_vbyNee|H`5C&&=~>YtP+<4m25^_)Ci9sV;hpR|c0IrI%9ccKi(dxnSFiZr z$LUK`8CDgFhFIji!qA#6%#*+-CR2A07=Igv9xBuaPod7!rihet z0OkdPc{fUs|6=ripdLKs`ya5D0Z4iO0UhA>ISA;Jb{9Pwg5^9V^POScC30?d!q;3%FT+hX8kESeW_{cUf_VfP_Q2 z`s06(7js$stndaDTKPomhL|9OuBav9%BlKg*g2S%9M*y3Wy$+G@N4KN3>Q)}Ew{sV zxr-d59$_36aH~T{cmqOL{kcH);}v0Ih+$QjR`5qvK?{(M`U+OAT#&=oE23Ez#!VF; z`b2_V-gs?@@L>`rFQWa@;7-#QvLaqF4ELe!^Wd7(7eeB9F2eYR*k4$yO7;|v` zs+8*)D&b#>EnQQrteGtSVnOoK%C`LdUUBBA=6=mw)#p58Ih#km*7CkcrHmDAPa1C~ zH$}HnY@D-v4Ji$iZCPdQb;GCjgI|Wil2SqghUE8 | 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)); +/** Brand logos from public/, inlined once per process each (satori needs a data URL or absolute URL). */ +const brandLogos = new Map>(); +function logoDataUrl(publicPath: string): Promise { + let p = brandLogos.get(publicPath); + if (!p) { + p = readFile(path.join(process.cwd(), "public", publicPath)) + .then((b) => `data:image/png;base64,${b.toString("base64")}`) + .catch(() => null); + brandLogos.set(publicPath, p); + } + return p; } export const alt = "token on openlaunch.lol"; @@ -69,7 +75,9 @@ 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 glLogo = card?.quote?.kind === "gitlawb" ? await logoDataUrl(GITLAWB_LOGO_PATH) : null; + const mwLogo = card?.quote?.kind === "museworld" ? await logoDataUrl(MUSEWORLD_LOGO_PATH) : null; + const official = card?.quote?.kind === "museworld"; const logo = l ? await memo(`og-logo:${chain}:${token.toLowerCase()}`, 60_000, () => ownLogo(l.image_url)) : null; const h = hueOf(token); return new ImageResponse( @@ -117,8 +125,12 @@ export default async function TokenOg({ params }: { params: Promise<{ chain: str
{card.quote ? ( - - {card.quote.kind === "gitlawb" ? ( + + {card.quote.kind === "museworld" ? ( + + {mwLogo ? : null} + + ) : card.quote.kind === "gitlawb" ? ( {glLogo ? : null} @@ -127,7 +139,7 @@ export default async function TokenOg({ params }: { params: Promise<{ chain: str {card.quote.ticker} )} - priced in {card.quote.symbol} + {official ? `official · priced in ${card.quote.symbol}` : `priced in ${card.quote.symbol}`} ) : null} {[card.fee, "liquidity locked forever", card.age].map((t, i) => ( diff --git a/app/src/app/t/[chain]/[token]/page.tsx b/app/src/app/t/[chain]/[token]/page.tsx index f012ad0..bc60441 100644 --- a/app/src/app/t/[chain]/[token]/page.tsx +++ b/app/src/app/t/[chain]/[token]/page.tsx @@ -25,6 +25,8 @@ import { NATIVE, SWAP_SITES, TICK_SPACING, type Quote } from "@/lib/launchpad/co import UnlistedPairBadge from "@/components/launchpad/UnlistedPairBadge"; import { GITLAWB_SITE } from "@/lib/launchpad/gitlawb"; import GitlawbBadge from "@/components/launchpad/GitlawbBadge"; +import MuseworldBadge from "@/components/launchpad/MuseworldBadge"; +import { MUSEWORLD_SITE } from "@/lib/launchpad/museworld"; 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"; @@ -121,7 +123,7 @@ export default async function TokenPage({ params }: { params: Promise<{ chain: s

{l.name}

-
${l.symbol}·{quote.key === "gitlawb" ? : Paired with {quote.symbol}}{unlisted ? : null}·Launched {ago(l.block_time, now)} ago
+
${l.symbol}·{quote.key === "gitlawb" ? : quote.key === "museworld" ? : Paired with {quote.symbol}}{unlisted ? : null}·Launched {ago(l.block_time, now)} ago
@@ -173,6 +175,7 @@ export default async function TokenPage({ params }: { params: Promise<{ chain: s {unlisted ?

Paired with {quote.symbol}, a token openlaunch does not list. The launch contracts accept any ERC-20 as the pair; its name here is what its own contract reports, so check the pair token address above. Prices are in {quote.symbol} only, with no USD figure, and this launch is not ranked in Trending.

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

Paired with MUSEWORLD, the official token of Museworld, where AI agents launch their tokens. {mode === "free" ? "This pool has no trading fee." : mode === "burn" ? "Every trading fee on this pool is burned as MUSEWORLD." : "Trading fees on this pool are paid out in MUSEWORLD."} USD figures use the MUSEWORLD/GITLAWB pool on openlaunch (checked against its 30-minute average) and GITLAWB's own price.

: null} {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{GITLAWB_ORIGIN[chain]}. {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/LaunchRow.tsx b/app/src/components/launchpad/LaunchRow.tsx index 04997ec..1d6658e 100644 --- a/app/src/components/launchpad/LaunchRow.tsx +++ b/app/src/components/launchpad/LaunchRow.tsx @@ -8,7 +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 { QuoteBrandBadge } from "./MuseworldBadge"; import UnlistedPairBadge from "./UnlistedPairBadge"; import { marketUsd } from "@/lib/launchpad/market-format"; import { capDisplay } from "@/lib/launchpad/market-cap"; @@ -55,7 +55,7 @@ export default function LaunchRow({ l, rank, window = "all", hl = null, now, pop
{l.name} - {isGitlawbQuote(l.quote_key) ? : null} + {l.quote_key === "other" ? : null} {hl?.kind === "new" ? New : null}
diff --git a/app/src/components/launchpad/LaunchTape.tsx b/app/src/components/launchpad/LaunchTape.tsx index 6442d2a..68e2bef 100644 --- a/app/src/components/launchpad/LaunchTape.tsx +++ b/app/src/components/launchpad/LaunchTape.tsx @@ -5,7 +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 { QuoteBrandBadge } from "./MuseworldBadge"; import type { FeedItem } from "@/lib/launchpad/queries"; import { fmtQuote } from "@/lib/launchpad/math"; import { CHAIN_SHORT, shortAddr } from "@/lib/chainPublic"; @@ -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 f8ac253..1f15f3c 100644 --- a/app/src/components/launchpad/MeDashboard.tsx +++ b/app/src/components/launchpad/MeDashboard.tsx @@ -8,7 +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 { QuoteBrandBadge } from "./MuseworldBadge"; import UnlistedPairBadge from "./UnlistedPairBadge"; import FeeChip, { feeModeOf } from "./FeeChip"; import EditTokenSheet from "./EditTokenSheet"; @@ -247,7 +247,7 @@ function WalletDashboard({ address, isConnected }: { address: Address | undefine {l.name} {l.symbol} - {isGitlawbQuote(l.quote_key) ? : null} + {l.quote_key === "other" ? : null}
@@ -300,7 +300,7 @@ function WalletDashboard({ address, isConnected }: { address: Address | undefine {t.name} {t.symbol} - {isGitlawbQuote(t.quote_key) ? : null} + {t.quote_key === "other" ? : null}
diff --git a/app/src/components/launchpad/MuseworldBadge.tsx b/app/src/components/launchpad/MuseworldBadge.tsx new file mode 100644 index 0000000..6d1aab7 --- /dev/null +++ b/app/src/components/launchpad/MuseworldBadge.tsx @@ -0,0 +1,40 @@ +import { BadgeCheck } from "lucide-react"; +import { MUSEWORLD_BLUE, MUSEWORLD_LOGO_PATH, MUSEWORLD_SYMBOL } from "@/lib/launchpad/museworld"; +import GitlawbBadge, { isGitlawbQuote } from "./GitlawbBadge"; + +/** Museworld's logo tile (white "m" and sparkle on Museworld blue, transparent corners baked in), served from our origin. */ +export function MuseworldMark({ size = 16, className = "" }: { size?: number; className?: string }) { + // eslint-disable-next-line @next/next/no-img-element + return ; +} + +/** + * Official badge for tokens paired with MUSEWORLD, Museworld's token (lib/launchpad/museworld.ts). Same geometry as the + * GITLAWB badge so the two sit side by side in any row: "sm" is 20px tall and fits a text-sm line. Museworld blue with + * the logo tile flush left and a check seal on the right: the mark that says the pair is official, not just named so. + * The badge follows the server-resolved quote key, which comes from the quote ADDRESS, never from an on-chain name. + */ +export default function MuseworldBadge({ size = "sm", className = "", label = MUSEWORLD_SYMBOL }: { size?: "sm" | "md"; className?: string; label?: string }) { + return ( + + + {label} + + + ); +} + +export function isMuseworldQuote(quoteKey: string | null | undefined): boolean { + return quoteKey === "museworld"; +} + +/** The brand badge a quote earns, if any: GITLAWB's or Museworld's official one. One place for every list to call. */ +export function QuoteBrandBadge({ quoteKey, size = "sm", className = "" }: { quoteKey: string | null | undefined; size?: "sm" | "md"; className?: string }) { + if (isGitlawbQuote(quoteKey)) return ; + if (isMuseworldQuote(quoteKey)) return ; + return null; +} diff --git a/app/src/components/launchpad/TrendingStrip.tsx b/app/src/components/launchpad/TrendingStrip.tsx index 34cc02f..1a6dc22 100644 --- a/app/src/components/launchpad/TrendingStrip.tsx +++ b/app/src/components/launchpad/TrendingStrip.tsx @@ -4,7 +4,8 @@ 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 { isGitlawbQuote } from "./GitlawbBadge"; +import { QuoteBrandBadge, isMuseworldQuote } from "./MuseworldBadge"; import ChangeChip from "./ChangeChip"; import { useLive } from "./LiveProvider"; import type { LaunchRow } from "@/lib/launchpad/queries"; @@ -83,7 +84,7 @@ export default function TrendingStrip({ initial }: { initial: Snap }) {
0{index + 1} ranked - {isGitlawbQuote(row.quote_key) ? {CHAIN_SHORT[row.chain]} · : {CHAIN_SHORT[row.chain]} · {row.quote_symbol}} + {isGitlawbQuote(row.quote_key) || isMuseworldQuote(row.quote_key) ? {CHAIN_SHORT[row.chain]} · : {CHAIN_SHORT[row.chain]} · {row.quote_symbol}}
diff --git a/app/src/lib/launchpad/config.ts b/app/src/lib/launchpad/config.ts index e66aa1f..f8efa04 100644 --- a/app/src/lib/launchpad/config.ts +++ b/app/src/lib/launchpad/config.ts @@ -1,6 +1,7 @@ import { isAddress, type Address } from "viem"; import { CHAIN_KEYS, SITE_URL, isChainKey, type ChainKey } from "@/lib/chainPublic"; import { unlistedQuote } from "./unlisted-quote"; +import { MUSEWORLD_ADDRESS, MUSEWORLD_DECIMALS, MUSEWORLD_LOGO_PATH, MUSEWORLD_NAME, MUSEWORLD_SYMBOL } from "./museworld"; import { GITLAWB_ADDRESS, GITLAWB_ADDRESS_ROBINHOOD, GITLAWB_DECIMALS, GITLAWB_LOGO_PATH, GITLAWB_NAME, GITLAWB_SYMBOL } from "./gitlawb"; /** @@ -18,10 +19,10 @@ export const BPS = 10_000; export const MAX_RECIPIENTS = 7; /** - * key "stock" = a registry stock; "other" = an ERC-20 no list knows (an unlisted pair, unlisted-quote.ts). `decimalsKnown` + * key "museworld" = an official quote the form does not offer (museworld.ts); "stock" = a registry stock; "other" = an ERC-20 no list knows (an unlisted pair, unlisted-quote.ts). `decimalsKnown` * is false only for an unlisted quote whose decimals have not been read yet: its 18 is a placeholder and it must not trade. */ -export type Quote = { key: "eth" | "usdg" | "usdc" | "gitlawb" | "stock" | "other"; address: Address; symbol: string; decimals: number; usd: number | null /* fixed USD price (stables); live for stocks + GITLAWB (server-filled) */; name?: string; logo?: string | null; decimalsKnown?: boolean }; +export type Quote = { key: "eth" | "usdg" | "usdc" | "gitlawb" | "museworld" | "stock" | "other"; address: Address; symbol: string; decimals: number; usd: number | null /* fixed USD price (stables); live for stocks + GITLAWB (server-filled) */; name?: string; logo?: string | null; decimalsKnown?: boolean }; 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 }; @@ -32,6 +33,8 @@ const USDC_ARC: Quote = { key: "usdc", address: "0x36000000000000000000000000000 /** 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 }; +/** MUSEWORLD: usd is null here (client-safe static); the server fills the price from its own GITLAWB pool (queries.ts). */ +const MUSEWORLD: Quote = { key: "museworld", address: MUSEWORLD_ADDRESS, symbol: MUSEWORLD_SYMBOL, decimals: MUSEWORLD_DECIMALS, usd: null, name: MUSEWORLD_NAME, logo: MUSEWORLD_LOGO_PATH }; /** Canonical Uniswap v4 deployments (developers.uniswap.org/docs/protocols/v4/deployments). */ const V4_BY_CHAIN: Record = { @@ -66,6 +69,11 @@ const V4_BY_CHAIN: Record = { }; /** Quote assets offered per chain, first = default. */ const QUOTES_BY_CHAIN: Record = { base: [ETH, GITLAWB], robinhood: [USDG, ETH, GITLAWB_RH], arc: [USDC_ARC] }; +/** + * Official quotes: known, badged and priced wherever a launch paired with them is listed, but NEVER offered in the + * launch form (MUSEWORLD launches are made inside Museworld by its agents). Kept out of QUOTES_BY_CHAIN on purpose. + */ +const OFFICIAL_QUOTES: Record = { base: [MUSEWORLD], robinhood: [], arc: [] }; /** * What address(0) means on each chain. The factory is permissionless and documents address(0) as the native asset, @@ -89,10 +97,10 @@ export function quotesWithKey(key: Quote["key"]): { chain: ChainKey; address: st export function listedQuoteAddresses(chain: ChainKey): string[] { return allQuotes(chain).map((q) => q.address.toLowerCase()); } -/** The chain's offered quotes plus its native asset (already among them where the form offers it). */ +/** The chain's offered quotes plus its native asset (already among them where the form offers it), plus its official quotes. */ function allQuotes(chain: ChainKey): Quote[] { const offered = QUOTES_BY_CHAIN[chain]; - return offered.some((q) => q.address === NATIVE) ? offered : [...offered, NATIVE_QUOTES[chain]]; + return [...(offered.some((q) => q.address === NATIVE) ? offered : [...offered, NATIVE_QUOTES[chain]]), ...OFFICIAL_QUOTES[chain]]; } /** @@ -203,9 +211,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], usdc: [5_000, 10_000, 25_000, 100_000], gitlawb: [] /* derived from the live price */, stock: [] /* derived from the live price */, other: [] /* never offered */ }; +export const MCAP_PRESETS: Record = { eth: [1, 5, 10, 25], usdg: [5_000, 10_000, 25_000, 100_000], usdc: [5_000, 10_000, 25_000, 100_000], gitlawb: [] /* derived from the live price */, museworld: [] /* not offered in the form */, stock: [] /* derived from the live price */, other: [] /* never offered */ }; /** Buy amount presets per quote. */ -export const BUY_PRESETS: Record = { eth: ["0.01", "0.05", "0.1", "0.5"], usdg: ["5", "25", "100", "500"], usdc: ["5", "25", "100", "500"], gitlawb: ["100000", "500000", "1000000", "5000000"], stock: ["0.1", "0.5", "1", "5"], other: [] /* unknown token, unknown scale */ }; +export const BUY_PRESETS: Record = { eth: ["0.01", "0.05", "0.1", "0.5"], usdg: ["5", "25", "100", "500"], usdc: ["5", "25", "100", "500"], gitlawb: ["100000", "500000", "1000000", "5000000"], museworld: ["50000", "100000", "500000", "1000000"], stock: ["0.1", "0.5", "1", "5"], other: [] /* unknown token, unknown scale */ }; // NEXT_PUBLIC_* must be read as literal `process.env.X` expressions: Next inlines them at build time. const DEV_RPC: Record = { diff --git a/app/src/lib/launchpad/first-buy.ts b/app/src/lib/launchpad/first-buy.ts index f4ca46a..a36dbec 100644 --- a/app/src/lib/launchpad/first-buy.ts +++ b/app/src/lib/launchpad/first-buy.ts @@ -16,7 +16,7 @@ import type { Quote } from "./config"; export const SUGGESTED_BUY_USD = 25; /** Quick-pick amounts per quote; the first one is the suggestion (about $25 at the prices this was written at). */ -export const BUY_PRESETS: Record = { eth: ["0.01", "0.05", "0.1", "0.25"], usdg: ["25", "100", "250"], usdc: ["25", "100", "250"], gitlawb: ["500000", "1000000", "5000000"], stock: [], other: [] /* the form never offers an unlisted quote */ }; +export const BUY_PRESETS: Record = { eth: ["0.01", "0.05", "0.1", "0.25"], usdg: ["25", "100", "250"], usdc: ["25", "100", "250"], gitlawb: ["500000", "1000000", "5000000"], museworld: [] /* not offered in the form */, stock: [], other: [] /* the form never offers an unlisted quote */ }; export type NoSuggestion = "no-price" | "unknown-balance" | "insufficient" | "no-gas" | "declined"; /** `provisional`: shown as selected but not yet confirmed against a balance (no wallet yet, or the read is pending). */ diff --git a/app/src/lib/launchpad/museworld.test.ts b/app/src/lib/launchpad/museworld.test.ts new file mode 100644 index 0000000..4e5c80e --- /dev/null +++ b/app/src/lib/launchpad/museworld.test.ts @@ -0,0 +1,73 @@ +import { test } from "node:test"; +import assert from "node:assert/strict"; +import { readFileSync, existsSync } from "node:fs"; +import path from "node:path"; +import { CHAIN_KEYS } from "../chainKeys.ts"; +import { MCAP_PRESETS, launchpad, listedQuoteAddresses, quoteInfo, quoteUsdOf } from "./config.ts"; +import { MUSEWORLD_ADDRESS, MUSEWORLD_BLUE, MUSEWORLD_LOGO_PATH, timeWeightedPrice } from "./museworld.ts"; +import { cleanQuoteSymbol } from "./unlisted-quote.ts"; +import { quotePillOf } from "./ogcard.ts"; + +/** + * MUSEWORLD is an official quote (museworld.ts): badged and priced wherever its launches are listed, never offered in + * the launch form, since those launches are made inside Museworld by its agents. + */ +const src = (rel: string) => readFileSync(path.join(import.meta.dirname, rel), "utf8"); + +test("MUSEWORLD resolves on Base by address, with its logo; nowhere else", () => { + const q = quoteInfo("base", "0x882C8E35504D58979B1280FE76D6553F717029F8"); + assert.equal(q.key, "museworld"); + assert.equal(q.symbol, "MUSEWORLD"); + assert.equal(q.decimals, 18, "on-chain decimals() = 18; the SQL sorts assume 18 for it"); + assert.equal(q.logo, MUSEWORLD_LOGO_PATH); + assert.equal(quoteUsdOf(q, 4_000), null, "client-safe static: the server fills the price, never the ETH price"); + for (const k of CHAIN_KEYS.filter((k) => k !== "base")) assert.equal(quoteInfo(k, MUSEWORLD_ADDRESS).key, "other", `${k}: the same address is some other token`); + assert.ok(listedQuoteAddresses("base").includes(MUSEWORLD_ADDRESS.toLowerCase()), "known, so the indexer does not treat it as unlisted"); +}); + +test("MUSEWORLD is never offered in the launch form", () => { + for (const k of CHAIN_KEYS) assert.ok(!launchpad(k).quotes.some((q) => q.key === "museworld"), k); + assert.deepEqual(MCAP_PRESETS.museworld, []); + assert.match(src("../../app/api/quotes/route.ts"), /launchpad\(chain\)\.quotes/, "the form's quote API lists the offered quotes only"); +}); + +test("a fake MUSEWORLD at another address shows by address, not as ours", () => { + for (const fake of ["MUSEWORLD", "museworld", "$MUSEWORLD", "MUSEWORLD2", "MuseWorld.v2"]) assert.equal(cleanQuoteSymbol(fake), null, fake); + assert.equal(cleanQuoteSymbol("MUSE"), "MUSE", "a different name stays readable"); +}); + +test("time-weighted price: a one-block push barely moves a 30-minute average", () => { + const from = 0; + const to = 1800; + assert.equal(timeWeightedPrice([], 2, from, to), 2, "no swaps: the price before the window held throughout"); + assert.equal(timeWeightedPrice([], null, from, to), null, "nothing known"); + // steady at 1 for the window, pushed 10x for the last 2 seconds + const pushed = timeWeightedPrice([{ t: 1798, price: 10 }], 1, from, to)!; + assert.ok(pushed < 1.02, `pushed average ${pushed}`); + // half the window at 1, half at 3 + assert.equal(timeWeightedPrice([{ t: 900, price: 3 }], 1, from, to), 2); + // no baseline: the window starts at the first swap + assert.equal(timeWeightedPrice([{ t: 900, price: 3 }, { t: 1500, price: 1 }], null, from, to), (3 * 600 + 1 * 300) / 900); + // unordered input and junk prices are handled + assert.equal(timeWeightedPrice([{ t: 1500, price: 1 }, { t: 900, price: 3 }, { t: 1000, price: Number.NaN }, { t: 1100, price: 0 }], null, from, to), (3 * 600 + 1 * 300) / 900); + assert.equal(timeWeightedPrice([{ t: 10, price: 5 }], 1, 100, 100), null, "empty window"); +}); + +test("server: priced from its own GITLAWB pool, guarded by the average, fails soft, ranks in USD sorts", () => { + const q = src("queries.ts"); + assert.match(q, /if \(q\.key === "museworld"\) return \{ \.\.\.q, usd: museworldUsdNow \};/); + assert.match(q, /row\.quote !== GITLAWB_ADDRESS\.toLowerCase\(\)\) return null;/, "only the MUSEWORLD/GITLAWB pool may price it"); + assert.match(q, /reconcileGitlawbUsd\(spot > 0 \? spot : null, twap\)/); + assert.match(q, /perMuseworld \* gitlawbUsd/); + assert.match(q, /museworldUsdNow = null; \/\/ fail soft/); + assert.match(q, /WHEN \$\{quoteArms\("museworld"\)\} THEN \$\{museworldFactor\}::double precision/); +}); + +test("badge: Museworld blue with its logo, in every list and on the token page and share card", () => { + assert.equal(MUSEWORLD_BLUE, "#0866FF"); + assert.ok(existsSync(path.join(import.meta.dirname, "../../../public", MUSEWORLD_LOGO_PATH))); + for (const f of ["LaunchRow.tsx", "LaunchTape.tsx", "MeDashboard.tsx", "TrendingStrip.tsx"]) assert.match(src(`../../components/launchpad/${f}`), //); + assert.deepEqual(quotePillOf("museworld", "MUSEWORLD"), { symbol: "MUSEWORLD", ticker: "MW", kind: "museworld" }); + assert.match(src("../../app/t/[chain]/[token]/opengraph-image.tsx"), /official · priced in/); +}); diff --git a/app/src/lib/launchpad/museworld.ts b/app/src/lib/launchpad/museworld.ts new file mode 100644 index 0000000..163d5b6 --- /dev/null +++ b/app/src/lib/launchpad/museworld.ts @@ -0,0 +1,51 @@ +import type { Address } from "viem"; + +/** + * MUSEWORLD: Museworld's token (museworld.lol), an official quote. It is not offered in the launch form: launches + * paired with it are made inside Museworld, by its AI agents, straight against the factory. Here it is listed so those + * launches carry the Museworld badge and a USD price instead of showing as an unlisted pair (unlisted-quote.ts). + * + * MUSEWORLD was itself launched on openlaunch, paired with GITLAWB, and that pool is its deepest market. So its price + * needs no new feed: GITLAWB per MUSEWORLD from our own index of that pool, times GITLAWB's USD price. That leg is + * checked against a 30-minute time-weighted average (`timeWeightedPrice`), so one swap that pushes the pool for a block + * cannot rewrite the market caps, volumes or rankings of every MUSEWORLD-paired launch. Client-safe; pure. + */ +export const MUSEWORLD_ADDRESS = "0x882c8e35504d58979b1280fe76d6553f717029f8" as Address; // Base +export const MUSEWORLD_SYMBOL = "MUSEWORLD"; +export const MUSEWORLD_NAME = "Museworld"; +export const MUSEWORLD_DECIMALS = 18; +export const MUSEWORLD_SITE = "https://museworld.lol"; +/** The white "m" and sparkle on Museworld blue, as a 160px rounded tile (from the Museworld repo's DexScreener icon). */ +export const MUSEWORLD_LOGO_PATH = "/museworld-mark.png"; +/** The tile's ground: badges use the same blue so the tile and the pill read as one piece. */ +export const MUSEWORLD_BLUE = "#0866FF"; +/** Window of the time-weighted price. */ +export const MUSEWORLD_TWAP_WINDOW_S = 30 * 60; + +/** + * Time-weighted average of a step price over [from, to] (unix seconds). The pool price is a step function: `baseline` + * holds from `from` until the first point, and each point's price holds until the next one. Without a baseline the + * window starts at the first point. null when there is nothing to average or an input is unusable. + */ +export function timeWeightedPrice(points: { t: number; price: number }[], baseline: number | null, from: number, to: number): number | null { + if (!(to > from)) return null; + const steps = points.filter((p) => Number.isFinite(p.price) && p.price > 0 && p.t <= to).sort((a, b) => a.t - b.t); + let level = baseline !== null && Number.isFinite(baseline) && baseline > 0 ? baseline : null; + let at = from; + let area = 0; + let span = 0; + for (const p of steps) { + const t = Math.max(p.t, from); + if (level !== null && t > at) { + area += level * (t - at); + span += t - at; + } + level = p.price; + at = t; + } + if (level !== null && to > at) { + area += level * (to - at); + span += to - at; + } + return span > 0 ? area / span : null; +} diff --git a/app/src/lib/launchpad/ogcard.ts b/app/src/lib/launchpad/ogcard.ts index e60d7cc..c762bc8 100644 --- a/app/src/lib/launchpad/ogcard.ts +++ b/app/src/lib/launchpad/ogcard.ts @@ -2,7 +2,7 @@ import { CHAIN_LABELS, type ChainKey } from "../chainKeys.ts"; export type CardInput = { name: string; symbol: string; chain: ChainKey; 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 | null; fee: string; age: string; quote: { symbol: string; ticker: string; kind: "stock" | "gitlawb" } | null }; +export type Card = { title: string; symbol: string; chainLabel: string; mcap: string; change: string; up: boolean | null; fee: string; age: string; quote: { symbol: string; ticker: string; kind: "stock" | "gitlawb" | "museworld" } | null }; const DEAD = "0x000000000000000000000000000000000000dead"; @@ -49,9 +49,10 @@ export function shapeCard(l: CardInput, now: number): Card { }; } -/** "priced in" pill: registry stocks get a ticker tile, GITLAWB the Gitlawb tile; ETH / USDG / unknown ("?") get none. */ +/** "priced in" pill: registry stocks get a ticker tile, GITLAWB the Gitlawb tile, MUSEWORLD the blue Museworld pill; ETH / USDG / unlisted get none. */ export function quotePillOf(quoteKey: string, quoteSymbol: string): Card["quote"] { if (quoteKey === "gitlawb") return { symbol: "GITLAWB", ticker: "GL", kind: "gitlawb" }; + if (quoteKey === "museworld") return { symbol: "MUSEWORLD", ticker: "MW", kind: "museworld" }; const sym = quoteSymbol.trim(); 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 166d0f0..6cb5134 100644 --- a/app/src/lib/launchpad/queries.ts +++ b/app/src/lib/launchpad/queries.ts @@ -7,6 +7,8 @@ import { ensureRegistry, stockByAddress, stockList, stockUsdInUse } from "./stoc import { unlistedQuote, type QuoteTokenMeta } from "./unlisted-quote"; import { memo } from "./memo"; import { gitlawbUsd } from "./gitlawbServer"; +import { GITLAWB_ADDRESS, reconcileGitlawbUsd } from "./gitlawb"; +import { MUSEWORLD_ADDRESS, MUSEWORLD_TWAP_WINDOW_S, timeWeightedPrice } from "./museworld"; import { canonicalImageUrl } from "./images"; import { GRACE_HOURS, LIVE_WINDOW_HOURS, rankTrending, type LiveTier } from "./ranking"; import { SNIPER_BLOCKS } from "./holders"; @@ -102,6 +104,8 @@ let stockUsdNow = new Map(); /** Unlisted quotes' on-chain symbol / decimals (bb_quote_tokens, keyed `${chain_id}:${address}`), and each chain's stock tickers they may not borrow. */ let quoteTokensNow = new Map(); let stockSymbolsNow = new Map(); +/** MUSEWORLD USD for this request (filled by `withStocks` from its own GITLAWB pool; null = unknown → no USD, 0 weight in USD sorts). */ +let museworldUsdNow: number | null = null; /** GITLAWB USD for this request (filled by `withStocks`; null = unknown → no USD, 0 weight in USD sorts). */ let gitlawbUsdNow: number | null = null; @@ -109,12 +113,46 @@ let gitlawbUsdNow: number | null = null; function quoteInfo(chain: ChainKey, address: string): Quote { const q = staticQuoteInfo(chain, address); if (q.key === "gitlawb") return { ...q, usd: gitlawbUsdNow }; + if (q.key === "museworld") return { ...q, usd: museworldUsdNow }; if (q.key !== "other") return q; const st = stockByAddress(chain, address); if (st) return { 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 }; return unlistedQuote(address, quoteTokensNow.get(`${chainIdOf(chain)}:${address.toLowerCase()}`) ?? null, stockSymbolsNow.get(chain) ?? []); } +/** + * USD per MUSEWORLD (museworld.ts): GITLAWB per MUSEWORLD in its own openlaunch pool, times GITLAWB's USD price. The pool + * leg comes from our index: the spot (bb_launches) unless it strays >25% from the 30-minute time-weighted price of the + * swaps, then that average, the same rule as GITLAWB's own price. null when GITLAWB has no price, the launch row is not + * indexed, or it is not the GITLAWB pool it should be. + */ +async function readMuseworldUsd(gitlawbUsd: number | null): Promise { + const db = maybeDb(); + if (!db || gitlawbUsd === null || !(gitlawbUsd > 0)) return null; + const cid = chainIdOf("base"); + const token = MUSEWORLD_ADDRESS.toLowerCase(); + const now = Math.floor(Date.now() / 1000); + const from = now - MUSEWORLD_TWAP_WINDOW_S; + const [launch, before, recent] = await Promise.all([ + db<{ quote: string; sqrt_price_x96: string | null }[]>`SELECT quote, sqrt_price_x96 FROM bb_launches WHERE chain_id = ${cid} AND token = ${token}`, + db<{ sqrt_price_x96: string }[]>` + SELECT sqrt_price_x96 FROM bb_launch_swaps WHERE chain_id = ${cid} AND token = ${token} AND block_time < to_timestamp(${from}) + ORDER BY block_number DESC, log_index DESC LIMIT 1`, + db<{ t: number; sqrt_price_x96: string }[]>` + SELECT extract(epoch FROM block_time)::int AS t, sqrt_price_x96 FROM bb_launch_swaps + WHERE chain_id = ${cid} AND token = ${token} AND block_time >= to_timestamp(${from}) + ORDER BY block_number, log_index LIMIT 5000`, + ]); + const row = launch[0]; + if (!row?.sqrt_price_x96 || row.quote !== GITLAWB_ADDRESS.toLowerCase()) return null; + const spot = quotePerToken(BigInt(row.sqrt_price_x96), 18); // GITLAWB per MUSEWORLD + const baseline = before[0] ? quotePerToken(BigInt(before[0].sqrt_price_x96), 18) : null; + const twap = timeWeightedPrice(recent.map((r) => ({ t: Number(r.t), price: quotePerToken(BigInt(r.sqrt_price_x96), 18) })), baseline, from, now); + const { usd: perMuseworld, source, deviation } = reconcileGitlawbUsd(spot > 0 ? spot : null, twap); + if (source === "twap" && deviation !== null) console.warn(`[museworld] pool spot is ${Math.round(deviation * 100)}% off the 30m average; publishing the average`); + return perMuseworld === null ? null : perMuseworld * gitlawbUsd; +} + /** bb_quote_tokens is tiny (one row per unlisted quote) and changes only when the indexer reads a new one. */ async function loadQuoteTokens(): Promise> { const db = maybeDb(); @@ -132,6 +170,12 @@ async function withStocks(): Promise { } catch { /* fail soft: stocks show without USD */ } + try { + museworldUsdNow = await memo("museworld-usd", 15_000, () => readMuseworldUsd(gitlawbUsdNow)); + } catch (err) { + museworldUsdNow = null; // fail soft: MUSEWORLD pools show without USD + console.warn("[museworld] price read failed:", err instanceof Error ? err.message : err); + } stockSymbolsNow = new Map(CHAIN_KEYS.map((k) => [k, stockList(k).map((s) => s.symbol)])); try { quoteTokensNow = await memo("quote-tokens", 30_000, loadQuoteTokens); @@ -275,12 +319,13 @@ export async function listLaunchesPage(opts: ListOpts = {}): Promise { const stockArms = stockEntries.flatMap(([a, v]) => stockChains(a).map((k) => db`WHEN (l.chain_id = ${chainIdOf(k)} AND l.quote = ${a}) THEN ${v}::double precision`)); const stockCase = stockArms.length ? stockArms.reduce((acc, c) => db`${acc} ${c}`) : db``; const gitlawbFactor = gitlawbUsdNow !== null && gitlawbUsdNow > 0 ? gitlawbUsdNow : 0; // unknown → 0 weight, like an unknown stock + const museworldFactor = museworldUsdNow !== null && museworldUsdNow > 0 ? museworldUsdNow : 0; const stables = fixedUsdQuotes(); const stableCase = stables.length ? stables.map((s) => db`WHEN (l.chain_id = ${chainIdOf(s.chain)} AND l.quote = ${s.address}) THEN ${s.usd}::double precision`).reduce((acc, c) => db`${acc} ${c}`) : db``; // address(0) is ETH only where the chain's native asset is ETH; a native stable (Arc: USDC) is already in stableCase above const ethNativeArms = CHAIN_KEYS.filter((k) => NATIVE_QUOTES[k].key === "eth").map((k) => db`WHEN (l.chain_id = ${chainIdOf(k)} AND l.quote = ${NATIVE_ADDR}) THEN ${ethFactor}::double precision`); const ethNativeCase = ethNativeArms.length ? ethNativeArms.reduce((acc, c) => db`${acc} ${c}`) : db``; - const usdPerUnit = db`(CASE ${stableCase} ${stockCase} WHEN ${isGitlawb()} THEN ${gitlawbFactor}::double precision ${ethNativeCase} ELSE 0.0 END)`; + const usdPerUnit = db`(CASE ${stableCase} ${stockCase} WHEN ${isGitlawb()} THEN ${gitlawbFactor}::double precision WHEN ${quoteArms("museworld")} THEN ${museworldFactor}::double precision ${ethNativeCase} ELSE 0.0 END)`; const stockDecArms = stockEntries.flatMap(([a]) => stockChains(a).flatMap((k) => { const d = stockByAddress(k, a)!.decimals; return d !== 18 ? [db`WHEN (l.chain_id = ${chainIdOf(k)} AND l.quote = ${a}) THEN ${d}`] : []; })); const decCase = stockDecArms.length ? stockDecArms.reduce((acc, c) => db`${acc} ${c}`) : db``; const stableDec = stables.filter((s) => s.decimals !== 18); diff --git a/app/src/lib/launchpad/unlisted-quote.test.ts b/app/src/lib/launchpad/unlisted-quote.test.ts index 476bfe3..ae6b87b 100644 --- a/app/src/lib/launchpad/unlisted-quote.test.ts +++ b/app/src/lib/launchpad/unlisted-quote.test.ts @@ -8,29 +8,30 @@ import { capDisplay } from "./market-cap.ts"; import { fdvQuote } from "./math.ts"; /** - * The factory takes any ERC-20 as the quote (2026-09-25: SHEAF on Base, paired with MUSEWORLD). Such a pair is shown + * The factory takes any ERC-20 as the quote (first seen 2026-09-25: SHEAF on Base). Such a pair is shown * as unlisted: the token's own symbol when it is safe to show, its real decimals, never a USD price, never trending. */ -const MUSEWORLD = "0x882c8e35504d58979b1280fe76d6553f717029f8"; +/** Any ERC-20 no list knows (UNLISTED, the first one seen, is an official quote since; see museworld.test.ts). */ +const UNLISTED = "0x1234567890abcdef1234567890abcdef12345678"; const src = (rel: string) => readFileSync(path.join(import.meta.dirname, rel), "utf8"); test("an address no list knows is an unlisted quote: key other, labelled by address, never priced", () => { - const q = quoteInfo("base", MUSEWORLD); + const q = quoteInfo("base", UNLISTED); assert.equal(q.key, "other"); - assert.equal(q.symbol, "0x882c…29f8"); + assert.equal(q.symbol, "0x1234…5678"); assert.equal(q.decimalsKnown, false, "not read yet: 18 is a placeholder"); assert.equal(quoteUsdOf(q, 4_000), null, "never priced, above all never at the ETH price"); - assert.ok(!listedQuoteAddresses("base").includes(MUSEWORLD)); + assert.ok(!listedQuoteAddresses("base").includes(UNLISTED)); assert.ok(listedQuoteAddresses("base").includes(NATIVE), "the native asset is listed, so the indexer never reads address(0)"); }); test("the on-chain symbol and decimals are used once read", () => { - const q = unlistedQuote(MUSEWORLD, { symbol: "MUSEWORLD", name: "MUSWORLD", decimals: 18 }); - assert.deepEqual([q.key, q.symbol, q.decimals, q.decimalsKnown, q.usd], ["other", "MUSEWORLD", 18, true, null]); + const q = unlistedQuote(UNLISTED, { symbol: "PEPE", name: "Pepe", decimals: 18 }); + assert.deepEqual([q.key, q.symbol, q.decimals, q.decimalsKnown, q.usd], ["other", "PEPE", 18, true, null]); }); test("a 6-decimal unlisted quote prices with 6 decimals, not the 18 placeholder (10^12 apart)", () => { - const q = unlistedQuote(MUSEWORLD, { symbol: "MEME", name: null, decimals: 6 }); + const q = unlistedQuote(UNLISTED, { symbol: "MEME", name: null, decimals: 6 }); assert.equal(q.decimals, 6); // 1 MEME per token: sqrtPriceX96 for price (currency1 per currency0 raw) = 1e18 / 1e6 = 1e12 → sqrt = 1e6 · 2^96 const sqrt = 10n ** 6n * 2n ** 96n; @@ -42,7 +43,7 @@ test("a 6-decimal unlisted quote prices with 6 decimals, not the 18 placeholder test("unreadable or absurd decimals stay unknown", () => { for (const d of [null, -1, 1.5, MAX_QUOTE_DECIMALS + 1, 255, Number.NaN]) { assert.equal(validDecimals(d), false, String(d)); - assert.equal(unlistedQuote(MUSEWORLD, { symbol: "X", name: null, decimals: d as number | null }).decimalsKnown, false, String(d)); + assert.equal(unlistedQuote(UNLISTED, { symbol: "X", name: null, decimals: d as number | null }).decimalsKnown, false, String(d)); } for (const d of [0, 6, 8, 18, MAX_QUOTE_DECIMALS]) assert.equal(validDecimals(d), true, String(d)); }); @@ -50,7 +51,7 @@ test("unreadable or absurd decimals stay unknown", () => { test("a symbol that is, or dresses up as, a listed asset is replaced by the address", () => { for (const fake of ["USDC", "usdc", "$ETH", "WETH", "USDC.e", "USD+", "USDbC", "cbBTC", "GITLAWB", "gitlawb2", "U​SDC", " E T H "]) { assert.equal(cleanQuoteSymbol(fake), null, fake); - assert.equal(unlistedQuote(MUSEWORLD, { symbol: fake, name: null, decimals: 6 }).symbol, addressLabel(MUSEWORLD), fake); + assert.equal(unlistedQuote(UNLISTED, { symbol: fake, name: null, decimals: 6 }).symbol, addressLabel(UNLISTED), fake); } assert.equal(cleanQuoteSymbol("NVDAx", ["NVDAx"]), null, "the chain's stock tickers are reserved too"); }); @@ -63,7 +64,7 @@ test("look-alike letters from other scripts cannot spell a listed symbol", () => }); test("symbols are cleaned: ASCII word characters only, bounded, no control or markup", () => { - assert.equal(cleanQuoteSymbol("MUSEWORLD"), "MUSEWORLD"); + assert.equal(cleanQuoteSymbol("PEPE"), "PEPE"); assert.equal(cleanQuoteSymbol("$PEPE"), "PEPE"); assert.equal(cleanQuoteSymbol("DOGE"), "bDOGEb"); assert.equal(cleanQuoteSymbol("A\u0000B\nC"), "ABC"); diff --git a/app/src/lib/launchpad/unlisted-quote.ts b/app/src/lib/launchpad/unlisted-quote.ts index fc0adb5..e8d61fc 100644 --- a/app/src/lib/launchpad/unlisted-quote.ts +++ b/app/src/lib/launchpad/unlisted-quote.ts @@ -14,9 +14,9 @@ import type { Quote } from "./config"; */ /** Symbols an unlisted quote may never show as: the quotes we list anywhere, and the assets people take them for. */ -export const RESERVED_QUOTE_SYMBOLS = ["ETH", "WETH", "CBETH", "USDC", "USDG", "USDT", "USDBC", "DAI", "EURC", "BTC", "WBTC", "CBBTC", "GITLAWB"]; -/** Any symbol starting with these reads as a dollar or as our own token ("USDC.e", "USD+", "GITLAWB2"). */ -const RESERVED_PREFIXES = ["USD", "GITLAWB"]; +export const RESERVED_QUOTE_SYMBOLS = ["ETH", "WETH", "CBETH", "USDC", "USDG", "USDT", "USDBC", "DAI", "EURC", "BTC", "WBTC", "CBBTC", "GITLAWB", "MUSEWORLD"]; +/** Any symbol starting with these reads as a dollar or as one of our own tokens ("USDC.e", "USD+", "GITLAWB2", "MUSEWORLD.v2"). */ +const RESERVED_PREFIXES = ["USD", "GITLAWB", "MUSEWORLD"]; export const UNLISTED_SYMBOL_MAX = 12; /** Beyond this, 10^decimals leaves double precision and the tick maths is meaningless. */