17 Commits
Author SHA1 Message Date
Kevin Codex 869ebb91dc fix(profiles): cap zalgo text, keep tags only in real flags, review leftovers
Deploy blockers (both from 658b50d's cleaning rewrite)
- Length: the grapheme cut alone let one letter carry thousands of combining
  marks ("a" + 3000 x U+0301 saved as a 3,000-character name; a bio kept
  4,001). Each character now keeps at most 4 combining marks and 16 code
  points (the longest emoji, a family with skin tones, is 11), and a field at
  most 4 code points per allowed character. Real diacritics and emoji are
  kept; an emoji at the limit is kept or dropped whole, never split.
- Tags: tag characters survive only inside the three subdivision flags emoji
  have (England, Scotland, Wales). Any other black flag + tags rendered as a
  plain flag while carrying hidden ASCII ("ignoreallprevinstruc" got
  through), the kind of text that steers an LLM reading names or bios.
- Pasted line breaks and tabs in a one-line field become a space instead of
  gluing words together.
- Tests: zalgo name and bio under their caps, fake flags stripped, the three
  real flags unchanged, 👨‍💻 / 🏳️‍🌈 / a skin-tone family at the exact limit,
  and the idempotency fuzz still holds.

Review leftovers
- migrate.mjs: a failed concurrent build names the index and the likely
  cause (55P03: an older transaction is still open, or another session holds
  the table; 57014: past the 20 min timeout). Still exits 1; retrying the
  deploy is safe.
- The availability check takes the form's X handle, so a verified owner's
  claim matches the save's rule (only while keeping that handle).
- The as-sent signature candidate counts only when the fields hold no C0/C1
  controls, bidi marks, overrides or isolates, or line / paragraph
  separators (the bio may keep line breaks); otherwise only the cleaned text
  is accepted.
- approve_x takes each released wallet's profile lock (in wallet order)
  before touching its rows, as that wallet's own verify does.
- deleteProfile's doc says when a name is held (kept a day or more).
2026-10-09 11:47:34 +08:00
Kevin Codex 658b50db9f fix(profiles): ultra review + CodeRabbit on 6c88cef
Signing and text
- Cleaning is idempotent: one pass could NFC-compose across a character it
  removed, so a name like "Jose<ZWSP><acute>" cleaned differently twice and
  the server's rebuilt message never matched the signature (401 forever).
  It now repeats until nothing changes, keeps the emoji joiner and
  presentation selectors only inside emoji (and tags only inside a
  subdivision flag), and cuts at whole graphemes, never half an emoji.
- The save signature is checked over the fields exactly as sent, or else
  over their cleaned form; the stored values are always the cleaned ones.
- Smart-wallet code checks cache only "has code on this chain" (permanent)
  and never "no code": a cached "deployed nowhere" (seedable with a junk
  signature) was the window for an old owner to sign through ERC-6492, and
  a partial list sent multi-chain wallets to the wrong chain.

Deploy safety
- The three new bb_launch_swaps indexes move to db/concurrent-indexes.sql,
  built by migrate.mjs after the schema transaction commits, with CREATE
  INDEX CONCURRENTLY (an invalid leftover is dropped and rebuilt). Inside the
  transaction they held the exclusive lock an existing ALTER on that table
  already takes, so every read and write on swaps queued behind the builds.

Profiles and moderation
- A hidden (not deleted) profile can't be saved: its owner sees "Create
  profile" and the blank form would overwrite the real row and drop X.
- Approving a claim releases any other wallet still holding the account's
  binding (was a 23505 → 502 on every retry); verifying does the same and
  now also clears their account facts and open codes.
- Usernames can't start with 0x (they read as short addresses), and digit /
  underscore look-alikes of reserved names are reserved ("0penlaunch_team").
- The availability check knows a verified owner may claim their own handle;
  that claim now requires keeping the verification in the same save.
- Coming back after a delete under the name the delete let go of is no
  rename (no clock, the clock is not restarted).
- Verification posts never @-mention the username (it is not an X handle).
- X down + a link from another account: an honest error, nothing queued.
- Re-check: only X saying "gone" pauses; X's sources disagreeing, or a new
  handle with no account id (a rename), is unsure, and unsure reads are
  retried in about an hour instead of after the full 1 or 7 days.
- Only a duplicate nonce is a replay; other DB errors are errors.

Indexer
- applyLaunchTx gives attribution the receipt it already holds.
- attributeBacklog's start block comes off the block_time index (the old
  min() walked 597K rows / 99 ms per poll per chain on a 600K-row table;
  now 4 buffers), an empty pass rests 5 minutes, and it reads 5x the batch
  so rows waiting out their retry gap no longer block the rows behind.
- healSwapTraders reads each transaction once, and rests unreadable ones.

Client
- Saved and verified names go straight into the names store (marked as
  this browser's own, so no older page render overwrites them); lookups
  time out after 10 s; the cache size check runs before answers land.
- The token page seeds no names when the lookup failed; /u seeds the owner
  from the profile it read.
- The /me card retries a failed lookup; "Being checked" is a status, not a
  button into a code the server will not take; a spent code is cleared.
- Wallet trade rows are keyed by chain, tx and log index.
2026-10-09 08:42:17 +08:00
Kevin Codex 27599317d0 fix(profiles): review round 2 (signing chain, execution-window attribution, bounded backlog)
- signatures: a plain wallet is recovered locally (no RPC); a smart wallet
  is checked on the chain it signed on (Coinbase Smart Wallet and Safe
  bind the chain id), only where it is deployed or nowhere yet; deployed
  only elsewhere → "switch your wallet to <chain>". Round 1 verified on
  the first chain with code, which locked out wallets signing elsewhere.
- attribution: a swap counts for a 4337 account only inside its
  operation's execution (after the EntryPoint's BeforeExecution, before
  its UserOperationEvent); a swap made during bundle validation stays on
  the sender. The 7702 rule is dropped (relayers keep the trade, as
  before this branch). Checked on live Base v0.6 / v0.7 / v0.8 traffic.
- backlog: the last day is always retried (a failed live lookup heals
  without the flag); full history still needs LAUNCH_ATTRIBUTE_BACKLOG=1;
  a row whose evidence stays unreadable for 3 tries is marked 'unread'
  so it cannot stall the queue.
- deleting no longer restarts the rename clock; coming back follows the
  same clock as a rename. Moderation reaches deleted profiles.
- the username check never treats the retired-name placeholder as you.
- migrate: SET LOCAL lock_timeout 10s, so an ALTER never queues behind a
  long read (and blocks reads behind it); a timeout fails the release.
2026-10-07 18:48:45 +08:00
Kevin Codex 34f0b0407b arc: final review round (live fees, silent skips, unindexed chains)
- Arc gas reserves sized to launch-day fees with margin: the base fee ran
  ~80 gwei plus ~30 gwei tips (measured), four times the 20 gwei floor the
  reserves were sized at, so a first buy right after a launch could have
  failed for gas. Launch reserve 1 USDC, swap reserve 0.2 USDC; blurb updated.
- the single-token holder skip logs an alert and says what stays off; the
  "holds only gas" copy is right where the quote is the gas token
- /api/health flags a configured chain the indexer has never run for; the
  secrets script requires the Arc locker, deploy block and RPC once the Arc
  factory is set, so Arc cannot go live unindexed
- the sync-lag alert threshold is per chain (block times differ ~20x) with
  the global and per-chain env overrides
- LAUNCH_SYNC_CONFIRMATIONS_ARC stays pushable as an override
2026-09-16 21:32:06 +08:00
Kevin Codex 2366a6ed0e arc: third review round
- sync errors are stored with every URL redacted: the message reaches
  /api/health unauthenticated and a keyed provider URL carries its API key
- a chain that finalizes every block keeps 0 confirmations regardless of the
  global LAUNCH_SYNC_CONFIRMATIONS, so no Arc-only variable is needed
- holder Transfer fetches bisect blocks once; the token list only splits at a
  single over-cap block, and one token still over the cap has that block skipped
- quoteUsdOf prices the ETH quote by key; a bare address(0) is no longer ETH
- contracts/deployments/launchpad-arc.json records Arc's Uniswap v4 addresses
  (factory and locker filled at deploy time) and a test pins the app's v4
  config to every chain's record
- the hero tolerates a poll from a machine that predates a chain (per-chain
  totals merge one level deeper, and a missing bucket counts as 0)
- the dev env example leaves the Arc fork override commented so Arc reads go
  through the proxy unless a third anvil is started
- the wallet menu shows the bridge hint on a registry chain the site cannot
  launch on yet; ?chain= for such a chain reads as all chains in the list,
  the live and list APIs and the home page
- LAUNCH_FACTORY_ABI names NativeQuoteUnsupported with a friendly message
- the bridge reads CHAINS.arc directly; the alias wrappers are gone
2026-09-16 21:26:04 +08:00
Kevin Codex e3cb18baec arc: review fixes across the form, list, trade panel and indexer
- GITLAWB filter chip is offered only on chains that have GITLAWB (filters now
  carry a chain list; Arc has none)
- /launch?chain=<unconfigured> mounts on a configured chain instead of a
  disabled tile (production, before the Arc contracts are set)
- one shortfall, one message: the native-gas checks skip a quote that already
  had the reserve applied (USDC on Arc)
- trade panel keeps a per-chain swap reserve for native and shared-gas buys
  (an Arc pool quoted in native USDC no longer reserves an ETH-sized dust)
- indexer: a single block over the node's log cap splits the token list
  instead of wedging the cursor; a query timeout is no longer bisected
- one chain-scoped quote-arm helper feeds the GITLAWB, USDG and USDC matches
- metadata validation error, env example, secrets script (Arc RPC optional
  until Arc is live) and stale two-chain copy updated
2026-09-16 21:26:04 +08:00
Kevin Codex 636f27b138 arc: third chain, quoted and gassed in USDC
Adds Arc (chain 5042, Circle's L1, public mainnet 2026-09-16) to the registry
behind the existing configured-chain gate: it reads "Coming soon" until the
factory env is set, so this ships before the contracts do.

- chain definition with USDC as the 18-decimal native asset and the public RPC
  as the fallback; ARC_RPC_URL (Alchemy) overrides it server-side, the browser
  keeps using the same-origin proxy
- Uniswap v4 addresses from Uniswap/contracts 5042.md, Universal Router v2.1.1
  with the v2 swap layout (proven on a fork, see the contracts commit)
- a "usdc" quote at a fixed dollar on the Permit2 path, with presets, the list
  filter, per-chain totals, and SQL pricing that no longer special-cases USDG
- gas reserve per chain, and the Arc quirk it exists for: the USDC quote IS the
  gas token, so a first buy or a trade for the whole balance is refused and the
  suggested buy leaves the reserve in the quote's own units
- stock source moved into client-safe config so the Stock quote button is not
  offered on a chain without a registry
- indexer: per-chain confirmations (Arc finalizes every block: 0) and log
  fetches that halve their range when a node caps results per call (Arc: 2000)
- env plumbing for the Arc factory, locker, deploy block and RPC across the
  Dockerfile, fly config, secrets script and env example; site copy names all
  three chains
2026-09-16 21:26:04 +08:00
Kevin Codex 0398e999fa home: show the GITLAWB burned across Base and Robinhood; dollar totals say ≈ while a price is missing
The network breakdown gains one line: GITLAWB sent to 0x…dEaD by GITLAWB-quoted launches, one combined
figure for both chains (one supply bridged over LayerZero), shown as an amount, never in USD. It sums
fees_quote_burned over GITLAWB-quoted launches; GITLAWB is only ever the quote side, so that is the whole burn.

Along the way:
- The live poller merges totals field by field, so a poll that lands on an old machine during a rolling
  deploy keeps the last value of a newly added total instead of dropping it.
- The receipt sync path no longer writes a Burned/Paid event for a position whose launch is not indexed
  yet: such a row was pinned with token NULL and never rolled into any total (the poller's later pass hit
  ON CONFLICT and the rebuild script joins on token). It now leaves the event for the poller, which
  indexes the launch first. The rebuild script attaches any historical orphaned rows by position id.
- The dollar totals (volume, fees to recipients, fees burned) fold every quote through its USD price and
  silently counted a quote at $0 while its price was unknown (cold start, a feed down >15 min). The totals
  now carry usd_partial and the hero shows ≈ with a tooltip while that is the case.
2026-09-13 09:31:21 +08:00
Kevin Codex 1f96280b84 home: rank by buyers, not launch order (live sort + ranking engine)
The home list defaulted to launch order, so the first screen was whatever was launched last:
duplicates from two wallets and zero-holder rows. Replace the Trending tab with Live and make it
the default. One rule, in lib/launchpad/ranking.ts and mirrored in SQL so paging stays correct:

  A token is live when an outside wallet traded it in the last 24h. Outside = not the launcher and
  not a buy inside the sniper window (launch block + 3, the holders panel's definition; one bot
  sniped 121 launches within 2s and would otherwise have made every token live). Live tokens rank
  by outside wallets this hour, then last trade, then holders. Then launches under 1h old, then
  quiet launches under a divider, one row per launcher (the rest are folded into it).

- queries: per-row `traders_1h_ex` / `traders_24h_ex` (outside wallets) in both windows, a `live`
  sort built as a CTE with the tier and a per-launcher row_number, `launcher_collapsed` on the kept
  row; `trending` stays accepted and orders like `live`. Nothing hidden: New, search and the API
  keep every launch. No schema change.
- ranking.ts absorbs trending.ts: tier + chip helpers for the list, strip score now weighs distinct
  outside wallets first (trades log-scaled), day window needs an outside wallet.
- list UI: Live tab first and default, rank numbers only for live rows, a chip per row saying why it
  sits there, a "Quiet launches" divider; the URL writes sort=new explicitly now.
- docs: llms.txt, agents page, rules FAQ ("Why is my token under quiet launches?"); smoke checks the
  live sort and its fields.

Verified against a local production copy: 51 live / 103 quiet rows at a pinned clock, one row per
launcher outside live, no overlap between pages, no self-traded token counted as live.
2026-09-10 14:49:48 +08:00
Kevin Codex c54e7f7871 Merge pull request #10 from beardthelion/fix/edit-rate-limit-nonce-ordering
fix: verify signature before consuming edit nonce; rate-limit on proven wallet
2026-09-09 21:19:44 +08:00
Kevin Codex 81e53e8157 GITLAWB badge: planet logo as a navy PNG tile
public/gitlawb-mark.png is now generated from the web repo's planet logo: the mark recoloured
white, the blue satellite kept, cropped and squared on a navy ground with rounded corners
baked into the alpha. Badge pill is the same navy so tile and pill read as one piece.
2026-09-08 16:39:18 +08:00
beardthelion a6e4407a94 Address PR #10 review: cross-platform tests, RPC outage 503, e2e gaps
Four points from the CHANGES_REQUESTED review, plus the test-key concern:

1. Cross-platform test command. `npm test` used inline `TEST_MOCK_DB=1
   NODE_OPTIONS=...` which fails on Windows (cmd does not expand inline
   env vars). The mock swap is actually done by tsconfig.test.json paths,
   so TEST_MOCK_DB was vestigial; the only need is the react-server
   condition for `server-only`. Pass `--conditions=react-server` straight
   to tsx, which is portable with no env var and no new dependency.
   Removed `test:unit`: plain `node --test` cannot run the server test
   (needs tsconfig path mocks + react-server), so it was broken, and
   `test` now runs the whole suite cross-platform.

2. RPC outage classification. viem 2.55.19's verifyMessage returns false
   (rather than throwing) when the RPC transport is unreachable, so an
   outage surfaced as 401 "signature does not match" instead of 503.
   Added a getChainId liveness probe on the false branch: if the probe
   throws, the false came from an outage (503); otherwise it is a genuine
   mismatch (401). The nonce is intact either way. The probe only runs on
   the failure path, so the happy path keeps its cost.

3. Transport-level outage test through viem itself. The chain mock now
   returns a real viem client with an unreachable transport when
   transportOutage is set, so the outage path runs through viem's actual
   verifyErc6492 path (the same one smart-wallet verification uses) rather
   than a hand-rolled stub. Added a unit test asserting 503 + nonce intact.

4. e2e IP-limit test. The IP check used a fresh IP per request, so it
   passed even with no limiter. Kept the cross-IP isolation check and
   added a single repeated IP past the 30/min limit, asserting it
   eventually gets 429.

Test key: replaced the hardcoded TEST_PRIVATE_KEY with a freshly
generated disposable key (never funded, public by design) and labelled
it. The old key is in git history; if it was ever used for real funds
those should be moved, since it is now public.
2026-09-08 01:08:27 -05:00
beardthelion 11b70df124 fix: remove wallet rate limit from nonce endpoint; strengthen test mock
CodeRabbit review caught two issues:

1. The wallet rate limit added to issueNonce was still abusable. The nonce
   endpoint is unauthenticated (no signature), so the launcher check only
   confirms the supplied wallet matches the public on-chain launcher
   address. An attacker who knows that address passes the launcher check
   and spends the wallet bucket. Removed the wallet rate limit from
   issueNonce entirely; the IP limit in the route is the only limit on
   this endpoint.

2. The db-mock only checked nonce + used_at, not the full predicate set
   (chain_id, token, wallet, expires_at). The begin() did not
   snapshot/restore on rollback, and the metadata branch could not fail,
   so nonce rollback on a failed INSERT was untested. Fixed: the mock now
   checks all predicates, snapshots state before the transaction callback
   and restores on rejection, and supports a configurable metadata-write
   failure.

Updated tests: the nonce DoS test now uses the creator's wallet (the real
attack vector), added predicate-match tests (wrong wallet, wrong chain_id,
expired), and a transaction rollback test.
2026-09-07 16:27:41 -05:00
beardthelion c01409e376 test: add e2e test for edit nonce/rate-limit fix
Hits the real Next.js dev server with a real Postgres DB to verify the
fix end-to-end. Covers: non-creator rejection, creator nonce issuance,
10 non-creator requests not freezing the creator's budget (finding 1),
invalid signature not burning the nonce (finding 2), valid signature
consuming the nonce and writing metadata, nonce reuse rejection, 10
invalid-sig edit requests not freezing the creator's edit budget
(finding 1), non-creator edit rejection, and IP-keyed rate limiting.

RED-GREEN verified: 3 tests fail on the original code for the right
reasons (nonce burned on invalid sig, wallet frozen by attacker, wallet
bucket spent before creator check), all 17 pass after the fix.
2026-09-07 15:47:04 -05:00
Kevin Codex 3d8ce76840 fix(review 2): metadata key ownership, independent deploy secrets, locked repair scripts
- Metadata ownership is per (chain, launcher, meta_key), the thing the public URI is keyed by:
  the first registration under a key fixes its content; later rows under the same key must match
  exactly (the salt search) or get 409. Serialized with a transaction-scoped advisory lock so two
  concurrent first writes cannot both win (tested: 10/10 races → one 200, one 409). Reads prefer the
  launched row, then the FIRST registration, never a newer one.
- fly-secrets.sh reads every key independently from the secrets file (no RPC derivation), fails on
  missing required keys, supports --dry-run; verified against mocked files.
- rebuild-launch-totals.sql / rebuild-holders.sql run in one transaction holding SHARE locks on the
  raw event tables and EXCLUSIVE on the target table, so ingestion waits instead of racing (tested:
  an ingestion started mid-rebuild blocked 3s, then applied on top; totals consistent).
2026-09-07 12:51:08 +08:00
Kevin Codex 9a65ad4edf fix: signed-or-insert-only metadata, salt-search-stable URI, atomic trade/fee accounting; CI, contributing, security policy
- Metadata registration is INSERT-only for unsigned callers: the creator registers before broadcasting
  (salt still private); replaying the public launch params returns 409 and cannot overwrite image /
  description / website. Later changes go through the wallet-signed edit flow. (review finding 1)
- The on-chain metadataURI is keyed by a client-chosen meta_key instead of the salt, so findSalt's
  result stays valid for USDG / stock quotes. Proven on the live Robinhood factory: 17/100 deterministic
  USDG launches reverted with the old scheme, 0/100 with the new one. (finding 2)
- Swap and fee ingestion commit the event row and the launch totals in one transaction; a rebuild script
  recomputes totals from raw events if ever needed (production checked: no drift). (finding 3)
- Fresh checkout: npm run migrate reads DATABASE_URL from .env.local/.env; fly-secrets.sh is self-contained
  (FLY_APP, SECRETS_ENV); README run + deploy steps corrected.
- CI workflow (lint, typecheck, unit tests, production build; forge build + unit tests), CONTRIBUTING.md,
  SECURITY.md with private reporting.
2026-09-07 11:48:37 +08:00
Kevin Codex 312489a7b4 Initial commit
openlaunch — launch a token. Free. Open source. On Base or Robinhood Chain.
Contracts (LaunchFactory / LaunchLocker / LaunchToken, Foundry), the site (Next.js),
brand assets. No platform fee, liquidity locked forever.
2026-09-07 11:28:24 +08:00