Commit Graph
118 Commits
Author SHA1 Message Date
Vasanth T 6b18a730af fix: address CodeQL hardening findings
Limit build-only CI tokens to read-only contents permissions.
Clarify intentional control filtering without changing signed post text,
and replace the sitemap test hostname-prefix check with exact origins.

Cover the image-preview React attribute boundary without changing its
runtime behavior or dismissing the existing scanner warning.
2026-09-12 23:13:54 +05:30
Kevin Codex 532b5eb84a Merge branch 'fix/holders-null-trader' 2026-09-12 21:55:05 +08:00
Kevin Codex 7aec314840 holders: a swap with an unknown sender no longer crashes the token page; the sync loop heals it
The indexer stores a swap with a null trader when the sender lookup fails
(a rate-limited node during a busy launch block). The holder panel then
called toLowerCase on it, the token page's error boundary replaced the
whole page, and /api/launch/holders returned 500 for that token.

- sniperSummary / creatorActivity skip swaps without a trader
- the sender lookup retries once before giving up
- healSwapTraders re-reads a bounded batch of null-trader swaps every poll
2026-09-12 21:53:25 +08:00
Kevin Codex 400143bfbb Merge pull request #33 from Ayush7614/test/time-swap-coverage
test(app): cover swap direction/wiring and ago buckets; fix NaNd on bad dates
2026-09-12 09:04:15 +08:00
Kevin Codex f77994414f Merge pull request #28 from Ayush7614/fix/amount-for-usd-finite-guard
fix(app): reject non-finite usd in amountForUsd
2026-09-11 19:59:11 +08:00
Ayush7614 f595a7a8a2 test(swap): decode+assert swapParams tuple fields in v1/v2 (CodeRabbit, PR #33)
The suite decoded settle/take but never params[0], so a uint128
regression or a misplaced minHopPriceX36 could pass. Decode the v1 and
v2 ExactInputSingleParams tuples and assert poolKey, zeroForOne,
amountIn, amountOutMinimum — plus minHopPriceX36 === 0n for v2.
2026-09-11 15:02:55 +05:30
Ayush7614 6b3a75117e test(app): cover swap direction/wiring and ago buckets; fix NaNd on bad dates
swap.ts claims unit-tested but only size/shape was covered: add
round-trip tests proving buy settles currency0/takes currency1 with
amountIn/minOut wired, sell flips, zero and uint128-scale amounts
survive, and v1 is the default. ago() rendered NaNd for unparseable
input (Math.max(0, NaN) is NaN): return 0s instead, with bucket, future-
clamp and garbage tests.
2026-09-11 14:46:40 +05:30
Ayush7614 74f63680c8 fix(app): reject non-finite usd in amountForUsd so the suggestion never becomes "Infinity"
amountForUsd(Infinity, 1, 18) returned the literal string "Infinity",
which viem parseUnits rejects downstream in suggestFirstBuy. Guard the
usd input with Number.isFinite like quoteUsd already was, and cover
Infinity/NaN for both inputs.
2026-09-11 14:27:27 +05:30
Kevin Codex d067240170 Merge pull request #26 from Gitlawb/fix/first-buy-suggestion-visibility
Fix/first buy suggestion visibility
2026-09-11 14:14:02 +08:00
Kevin Codex 9302fdc1d4 first buy (CodeRabbit, PR #26): keep the session flag in memory when storage is blocked
With sessionStorage blocked or absent, the explicit "No first buy" was lost on a remount within
the tab because reads returned false. The store now keeps the latest value in memory and serves
it whenever storage cannot be read, so such a tab keeps the choice until reload. The server
snapshot stays false, so the hydration behaviour is unchanged. Tests cover true and false under
absent and blocked storage.
2026-09-11 13:48:31 +08:00
Kevin Codex 9ed0a2fbc7 first buy (CodeRabbit, PR #26): read the session flag through useSyncExternalStore, not a state initializer
Once the default became selected without a wallet, a tab holding the "No first buy" flag rendered
the suggestion on the server and the cleared state on the client: a hydration mismatch. The flag
is now an external store (lib/launchpad/first-buy-session.ts) read with useSyncExternalStore: the
server snapshot is always false, so both renders match, and the client re-renders with the real
flag right after. No state is set in an effect. The form never touches sessionStorage directly.
Tests: the store's server/client snapshots, notifications and blocked-storage behaviour; the form
source test pins the hook and forbids direct storage access.
2026-09-11 13:14:07 +08:00
Kevin Codex 912024a8ce Merge pull request #27 from Gitlawb/feat/usd-market-cap
market cap in dollars everywhere; the launch form takes the starting cap in dollars
2026-09-11 13:05:24 +08:00
Kevin Codex f1db129ef3 market cap (CodeRabbit, PR #27): the "no USD price" mark reaches every site; fractional USDG
- One-string sites (trending strip, dashboard token lists, the mobile buy bar) rendered only the
  main figure, so an unpriced quote showed "10 ETH" with nothing saying it is not dollars. The
  display helper now also returns `compact`: the dollar figure when priced, else the quote figure
  with the mark; those sites use it.
- The post-buy estimate in the launch form showed only the main figure; it now carries the detail
  like "Opens at". The home row showed the detail only from md up; the mobile block has it too.
- A 6-decimal quote's detail rounded to whole units, so a 0.5 USDG cap read "1 USDG"; below 100
  units it keeps two decimals now.
- Tests: helper cases for compact and fractional USDG; a source test pins every site to render the
  detail or the compact form, and the form to have dropped the old quote-first formatter.
2026-09-11 13:02:40 +08:00
Kevin Codex 78788a774f market cap in dollars everywhere; the launch form takes the starting cap in dollars
The list, strip, chart and dashboard already led with a dollar market cap, but the launch form
did not: an ETH launch picked "1 / 5 / 10 / 25 ETH" while every other quote picked "$5K / $10K /
$25K / $100K", the custom cap was in quote units, and "Opens at" led with the quote figure. One
rule now, in lib/launchpad/market-cap.ts: market cap is a dollar figure, the quote figure is the
detail, and a quote with no USD price shows its own figure marked "no USD price" instead of a dash.

Form: the same four dollar targets for every priced quote ($25K default, about the old 10 ETH), a
dollar custom input, dollars first on "Opens at", the buy preview and the preview card, with the
quote figure after. The tick is still computed from the quote-denominated cap, so the dollar
figure shown is the one the chosen tick produces (within one 2% tick step). Without a price the
form falls back to quote units as before. A preset pick no longer carries across quotes.

Rows, strip, dashboard and the mobile buy bar use the one display helper; the row's small line
shows the quote figure (or the "no USD price" mark). API unchanged. Tests for the entry mode,
presets, pick carry-over, the tick round-trip and every label.
2026-09-11 12:27:07 +08:00
Kevin Codex 5d88cc1bc4 launch form: the suggested first buy is selected from the start, wallet or not
The suggestion only appeared once a wallet was connected and its balance read, so the form opened
with "First buy · optional", empty, and "Connect a wallet to see the suggested amount". The default
should be visible immediately: 0.01 ETH selected, header "· suggested". A connected wallet can only
take it away: dropped when the balance cannot cover amount + gas (or the token balance the amount,
or the native balance the gas, for an ERC-20 quote) or when a balance read failed; kept, marked
provisional, while a read is still pending so it does not flicker. The rule still never blocks a
launch. The "no wallet" note is gone; the cleared note shows without a wallet too.
2026-09-11 12:12:36 +08:00
Kevin Codex 058fbf7304 launch form: a cleared first-buy suggestion is visible and reversible
Clearing the field, toggling a preset off, or pressing "No first buy" all marked the suggestion
declined for the whole tab session, and the form then showed "· optional" with no explanation and
no way back except typing an amount. A creator who touched the field once while exploring saw no
suggestion afterwards and could not tell why.

Now: clearing the field or toggling a chip off declines in memory only; the explicit button is the
one action that persists for the session. Whenever the suggestion is declined and a wallet is
connected on the chain, the form says "No first buy. The launch stays free." with a "Suggest 0.01
ETH again" link. While the balance is still being read, or when the read failed, it says so
instead of silently showing "optional". Form source tests pin all three.
2026-09-11 12:07:22 +08:00
Kevin Codex 87a21aa4a2 Merge pull request #25 from Gitlawb/fix/eth-price-chainlink-fallback
eth price: Chainlink ETH/USD on Base as the second source, and a 15-minute bound on the stale value
2026-09-11 12:04:55 +08:00
Kevin Codex a47f2c7fce eth price (CodeRabbit, PR #25): the memo cannot outlive the stale bound; one refresh in flight
- A failed refresh just inside the 15-minute bound stored the stale value with a fresh attempt
  timestamp, so the 60s memo could serve it for up to a minute past the bound. The memo now yields
  whenever the cached value is past the bound; once the value is null the memo applies again.
- Concurrent callers each ran their own refresh, so a slow earlier attempt (Coinbase timing out,
  then Chainlink) could land after a later one and overwrite a fresher Coinbase result with an
  older timestamp. Refreshes now share one in-flight promise, the same pattern gitlawbServer uses,
  which also stops the thundering herd when the TTL lapses under load. Tests for both.
2026-09-11 11:55:50 +08:00
Kevin Codex b762ef2bb4 eth price: Chainlink ETH/USD on Base as the second source, and a 15-minute bound on the stale value
PR 23 made a bad Coinbase response keep the last good price instead of blanking USD, which was
right, but with no upper bound a long outage left an old dollar figure on every page and every
dollar-based sort, silently. Order on each refresh is now: fresh Coinbase spot; else the Chainlink
ETH/USD feed on Base when its round is under 30 minutes old (it fails independently of Coinbase
and updates on a 0.15% move or every 20 minutes); else the last good value for at most 15 minutes
after it was confirmed; else null, which the UI already renders as ETH-only figures.

The feed address was verified on-chain before use: description() "ETH / USD", decimals() 8, and a
live round within a few dollars of Coinbase. The read reuses the stock feeds' aggregator ABI and
round-to-USD helper with a 30-minute max age instead of the equities' five days, under the same
5s timeout as the Coinbase call, and is injectable for tests like the fetch already was. One
warning is logged when the source switches and one when both are down past the bound.

Tests: the existing six pin Chainlink down so they still describe Coinbase alone; new cases cover
the feed conversion boundaries, Coinbase down with Chainlink up, an unusable Coinbase body with a
live or a stale feed round, the 14/15-minute bound with recovery from either source, and a cold
double outage. Display and sorting only: nothing on-chain reads these prices.
2026-09-11 11:39:48 +08:00
Kevin Codex d10fc31d38 Merge pull request #24 from Gitlawb/feat/first-buy-default
launch form: suggest a first buy of about $25 so a token opens with a holder and a price
2026-09-11 11:16:41 +08:00
Kevin Codex cfcf31c5ee first buy: pin the quote-bound typed amount in the form source test 2026-09-11 11:13:15 +08:00
Kevin Codex 96c26b972c first buy (CodeRabbit, PR #24): bind a typed amount to its quote; never suggest "0"
- A typed amount now remembers the chain and quote it was typed for and only applies while that
  quote is selected, so "25" typed for USDG is dropped on a switch to ETH instead of becoming a
  blocked 25 ETH buy, and "0.01" ETH does not become a 0.01 USDG dust buy. Done as derived state
  rather than an effect (the lint rule rejects setState in effects). Clearing the suggestion still
  survives a quote switch: that intent is about the launch, not the asset.
- amountForUsd could return "0" when a quote has fewer decimals than the amount needs, which the
  rule then presented as a suggestion while the form ran no buy. It returns null now. No quote on
  either chain has that few decimals today; tests cover it anyway.
2026-09-11 11:12:43 +08:00
Kevin Codex c16d9213f8 first buy (CodeRabbit, PR #24): reserve gas for the launch too, not only the buy
The launch transaction is sent first and pays its own gas before the buy runs, so a reserve that
covered only the buy could let both an ETH and an ERC-20 first buy pass validation and then fail
after launch. The reserve is now the sum of a launch part (0.001 ETH: deploy, pool init, position
mint) and a buy part (0.0005 ETH: approvals and swap), used by the suggestion rule and by both
typed-amount checks. Boundary tests for both quote types at exactly the reserve and one wei under.
2026-09-11 10:43:11 +08:00
Kevin Codex 5e7e6505ba first buy (CodeRabbit, PR #24): an ERC-20 quote's buy also needs ETH for gas
The suggestion for a USDG, GITLAWB or stock quote checked only the token balance, so a wallet
holding tokens but no ETH could be handed a pre-filled buy that fails after the launch (the
approval and the swap pay gas in ETH). The native balance is now read whenever a wallet is
connected, the rule takes it as a separate input, and an ERC-20 suggestion needs both the token
balance for the amount and the native balance for the gas reserve; a missing native read means no
suggestion, as with any unknown balance. A typed ERC-20 amount gets the same gas check as a typed
ETH amount. Tests cover the "tokens but no gas" and "native balance unknown" cases.
2026-09-11 10:04:08 +08:00
Kevin Codex 7207c6b82c Merge pull request #23 from Ayush7614/fix/eth-price-stale-guard
fix(app): ETH price keeps last good value on bad upstream reads
2026-09-11 08:37:29 +08:00
Kevin Codex 7886f07fe9 Merge pull request #22 from Ayush7614/fix/error-boundaries-reset
fix(app): error boundaries use Next.js reset, safe digest, alert semantics
2026-09-11 08:29:36 +08:00
Ayush7614 4cd6ccba7f fix(app): ETH price keeps last good value on bad upstream reads
ethUsd overwrote its 60s memo with null whenever Coinbase answered
non-200 or an unparsable body: one 429 blanked every USD figure on the
site (layout, home, token pages, list/live/search/me APIs, GITLAWB USD)
for a full TTL. Check res.ok, parse defensively, keep the last good
price on any failure, and bound the fetch with a 5s timeout. Failures
still advance the memo timestamp so a down upstream is retried at most
once per TTL.

Verified: npm run lint, tsc --noEmit, npm test (296 pass), npm run build.
2026-09-11 03:46:18 +05:30
Ayush7614 b1740a0912 fix(app): error boundaries use Next.js reset, safe digest, alert semantics
Route and global error boundaries declared a retry prop that Next.js
never provides, so Try again was wired to undefined and did nothing.
Use the real reset prop, render the status as an h1 inside a labelled
landmark, announce the failure with role=alert, and display error.digest
only when it is a short opaque id.

The 404 copy claimed every bad URL was an unlaunched token; make it
generic and offer both home and the launch flow.

Verified: npm run lint, tsc --noEmit, npm test (295 pass), npm run build.
2026-09-11 03:44:49 +05:30
Kevin Codex f248a1aead launch form: suggest a first buy of about $25 so a token opens with a holder and a price
Of 380 launches on the production copy, 18 used the first buy at launch and 320 never bought at
all; tokens whose creator bought went on to get an outside wallet twice as often (18/60 vs 48/320).
The feature existed and nobody found it.

The form now pre-fills the smallest preset (0.01 ETH, 25 USDG, 500k GITLAWB) or $25 worth of a
stock quote, and says why: a token with no holders and no price move reads as dead on screeners
and sits under quiet launches on the home page.

One rule, in lib/launchpad/first-buy.ts: a suggested buy never blocks a launch. It exists only
when a wallet is connected on the right chain, the balance is known and covers the amount plus the
gas reserve, and the creator has not cleared it; otherwise the launch is free, as before. A typed
amount keeps the strict checks. The balance is read as soon as a wallet connects (it used to wait
for a typed amount). "No first buy" clears it, and clearing is remembered for the session.

Copy: "First buy · suggested", "Launch + first buy", the why line, a note when the wallet holds
only gas. The honesty lines stay (others can buy before you, slippage, gas; no "first holder").
Docs: llms.txt, agents page and the rules FAQ recommend a buy right after launch.

Tests: the suggestion rule (presets, $25 stock math, every no-suggestion reason), form source
assertions updated. The connected-wallet states need a browser with a wallet; the server-rendered
form was checked on both chains.
2026-09-10 20:30:33 +08:00
Kevin Codex 092f9bb82b Merge pull request #21 from Gitlawb/feat/live-ranking
Feat/live ranking
2026-09-10 15:34:41 +08:00
Kevin Codex 00acfad52a live sort (CodeRabbit, PR #21): fetch the strip in the batch; say what the sniper window and the grace hour cover
- live route: when the list cannot supply the strip's candidates (non-default sort, chain or filter),
  the trending query now runs inside the same Promise.all instead of after it.
- Wording, no rule change: the sniper window excludes every swap in launch block + 3, not only buys.
  All supply starts in the pool, so a sell in that window can only be a sniper flipping what it just
  bought; counting it would re-admit exactly the bot the window exists for. And the grace hour goes
  to a wallet's newest launch, not every launch: one wallet made 103 of the 200 Base launches, so an
  uncapped grace hour is a way to fill the board. The rules page, llms.txt and the code comments now
  say both; the rules FAQ also says "trade from an outside wallet" since sells count.
2026-09-10 15:24:45 +08:00
Kevin Codex f7b600d759 live sort (review): always request the sort, rank by outside trades, ship the tier with the row
Fixes from the branch review, none of which change the rule:

- The list's own fetch reused the URL params, which omit the default sort, and the API's default is
  "new": clicking Live or clearing filters loaded the newest-first list and drew chips over it. The
  request now always carries the sort.
- "trending" was still accepted from the URL but the list only knew "live", so a bookmarked
  /?sort=trending rendered the collapsed list with no tab, chips or divider. One parseSort() now
  allow-lists and aliases trending -> live for the page and both routes; the LaunchSort union drops it.
- Ordering and the chip used bb_launches.last_trade_at, which the launcher's own swaps and sniper
  buys also move. The stats probe now returns last_outside_trade_at (the same outside-wallet
  predicate) and the live sort orders by outside wallets this hour, then today, then that time; the
  other tiers by age. Holders left the order.
- The client recomputed the tier from its own clock, so the divider and ranks could disagree with the
  database order (hydration at the 1h boundary, duplicate dividers while the order was held). The
  SQL tier now travels with the row as live_tier and the client reads it; one divider, at the first
  quiet row.
- Home render and the live poll ran the identical live query twice (list + strip). The default
  view's first page is the strip's candidate set, so trendingFrom(page.items) replaces getTrending
  there; getTrending stays for other views.
- One LATERAL probe per launch instead of two: the hour's figures are FILTERs over the day window.
  The unused traders_24h column is gone; GRACE_HOURS no longer sizes the 1h stats window.
- Strip tooltip describes the new weighting; docs mention the new fields and the "then today" step.
2026-09-10 15:08:11 +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 eb0f050ecc Merge pull request #17 from philpof102-svg/feat/ol-stock-ux-main
feat(launch): sticky Stock chip + issuer disclaimer + fail-closed CTA
2026-09-10 12:14:28 +08:00
Kevin Codex a9916aabc7 Merge pull request #14 from Ayush7614/feat/seo-discoverability
seo: robots, sitemap, manifest, error boundaries, token canonical + JSON-LD
2026-09-10 12:10:38 +08:00
Kevin Codex 570b020435 seo (CodeRabbit, PR #14): error boundaries retry instead of reset
Next 16.3 hands error boundaries both `reset` and `retry`; `retry` refreshes
the router inside a transition and then resets, so server-side data that
failed is fetched again, while `reset` only re-renders the same tree. Both
boundaries now bind "Try again" to `retry`, matching the current Next docs.
2026-09-10 12:06:33 +08:00
Kevin Codex 4ddd617ff5 launch form (CodeRabbit, PR #17): clicking the active chain is a no-op
The previous commit cleared the picked stock whenever a chain button was
clicked, including the chain already selected, so a user could lose their
stock by tapping the highlighted chain. The handler now returns early for the
active chain and only resets state on an actual switch. Verified in headless
Chrome: pick AAPLc on Base, click Base again, the chip stays; switch to
Robinhood Chain and it is gone.
2026-09-10 12:06:10 +08:00
Kevin Codex 3264b6cae6 launch form (CodeRabbit, PR #17): drop the picked stock when the chain changes
The chain buttons reset the quote key and market cap but kept `stock`, so a
Coinbase stock picked on Base survived a switch to Robinhood Chain. Selecting
Stock again then showed that Base registry address as the quote, labelled as a
Robinhood stock, and it would have been handed to the Robinhood factory.
Reproduced in headless Chrome before the fix; after it, the chip is gone and
the Robinhood registry has to be searched. The launch-form test pins the reset.
2026-09-10 11:51:55 +08:00
Kevin Codex 70c0af7ac9 stock chip (PR #17 review): one message, issuer on the chip, no test hooks
- "Pick a stock to price the token in, or switch the quote." is one constant,
  shown by the validation list and the status box above the submit button; the
  third copy (a warm chip beside the search field) is gone.
- The picked-stock chip reads "AAPLc · Coinbase stock · Apple" instead of
  "Quote = AAPLc (registry)": the issuer is what a buyer needs to know.
- aria-live came off the whole stock section, where it re-announced the
  thirteen search results on every keystroke; the status box (role=status) is
  the only live region.
- data-testid hooks removed: the repo tests source contracts, and the new
  launch-form test pins the single message, the issuer label, "Switch quote"
  resetting to the first configured quote, and the shared disclaimer helper.
2026-09-10 11:41:32 +08:00
Raksha d95f589ee7 feat(launch): sticky Stock chip + issuer disclaimer + fail-closed CTA
Dig #2 (Grok Super B-20 stock UX): sticky « Quote = SYMBOL (registry) »
chip beside the Stock picker, Reg S / issuer disclaimer adjacent to the
pick, and CTA fail-closed with an explicit chip when Stock is selected
with no registry pick. Registry-gated via existing /api/quotes only.

Deferred: dig #3 above-fold Stock for Instant (Instant Advanced path
ships separately on feat/ol-stock-ux).
2026-09-10 11:41:32 +08:00
Kevin Codex c6c382bc6b seo (PR #14 review 2): real icon sizes in the manifest, log the root error in an effect
- manifest.ts: "any" only describes SVG icons; list icon.svg as the scalable
  icon and give icon.png its real 512x512 size next to the 180x180 apple icon
- global-error.tsx: log from useEffect like error.tsx does, instead of during
  render
2026-09-10 11:35:13 +08:00
Kevin Codex 6756659e08 seo (PR #14 review): reset prop, dynamic sitemap, formatting
- error.tsx / global-error.tsx: Next passes the boundary `reset`, not
  `retry`; the "Try again" button was wired to undefined
- sitemap.ts: force-dynamic so the token list is not frozen at build time
  (CI builds without DATABASE_URL, which left only the static routes)
- split a jammed import line in the token page and restore the newline
  before tokenJsonLd
2026-09-10 11:29:57 +08:00
Ayush7614 f22f14a4de seo: address review — retry boundaries, global-error styles, robots, sitemap filter, escaped JSON-LD
- error.tsx + global-error.tsx: use the stable retry() callback
  (Next 16.3) instead of reset() so recovery re-fetches RSC data.
- global-error.tsx: load ./globals.css and the shared font variables
  (new app/fonts.ts, also used by the root layout) since the fallback
  replaces the root layout and inherits neither.
- robots.ts: disallow /admin and /api (slashless) so the exact paths
  are covered, not just their children.
- sitemap.ts: WHERE chain_id IN (8453, 4663) so unknown chains can
  never be mislabelled as Base.
- JSON-LD: new jsonLdHtml() helper escapes < to \u003c so a
  creator-supplied name/description cannot terminate the script
  element; token page uses it, covered by a round-trip unit test.
2026-09-10 11:29:57 +08:00
Ayush7614 4d8472e62a seo: robots, sitemap, manifest, error boundaries, token canonical + JSON-LD
Add crawler discoverability the site was missing: /robots.txt
(allow /, disallow /admin + /api, sitemap ref), /sitemap.xml
(static routes + newest 1000 launches, fail-soft when the DB is
unconfigured), and /manifest.webmanifest (existing app icons).

Add route error boundaries (error.tsx + global-error.tsx) mirroring
not-found.tsx so a render failure shows a reset action instead of
Next's default digest page.

Token pages get a canonical URL and facts-only JSON-LD (on-chain
fields + creator metadata, no scores or flags) for rich results.

Pure helpers live in src/lib/seo.ts with node --test coverage.
2026-09-10 11:29:57 +08:00
Kevin Codex 81bf8aaa0b Merge pull request #20 from Gitlawb/feat/wallet-picker
wallet: a real connect picker; Coinbase Smart Wallet was never reachable
2026-09-10 11:19:16 +08:00
Kevin CodexandClaude Fable 5.1 c954e3fdc2 wallet picker: notice a provider injected after the sheet opened (CodeRabbit, PR 20)
hasInjectedProvider was read through useSyncExternalStore with a no-op
subscription, so a wallet that set window.ethereum after the picker mounted
never produced the "Browser wallet" row until the sheet was reopened. Subscribe
to the EIP-1193 "ethereum#initialized" event and to EIP-6963 announcements and
re-read the flag on either. Verified in headless Chrome: with the sheet open and
no wallet, injecting window.ethereum adds the row in place; a late EIP-6963
announce then replaces it with the announced wallet. The source-contract test
pins the subscription and the two event names.

Co-Authored-By: Claude Fable 5.1 <[email protected]>
Claude-Session: https://claude.ai/code/session_0146kzimBSuHTErfKYhMCaic
2026-09-10 11:05:12 +08:00
Kevin CodexandClaude Fable 5.1 d09606ced2 wallet: a real connect picker; Coinbase Smart Wallet was never reachable
Every "Connect wallet" button looked for a connector with id "coinbaseWallet",
but wagmi's Coinbase connector registers as "coinbaseWalletSDK". The lookup
never matched, so the app always fell through to the generic injected
connector. Coinbase Smart Wallet could not be chosen, and in a browser with
no wallet extension the click failed with ProviderNotFound and no UI reacted:
a dead button. That also hid that @coinbase/wallet-sdk (an optional peer of
wagmi) was not installed; the production bundle carried a "Cannot find
module" stub in its place.

- lib/wallet-connectors.ts: pure module choosing which wallets to offer
  (browser-announced EIP-6963 wallets first, the generic browser wallet only
  when something is injected without announcing itself, Coinbase always) and
  turning connect failures into plain text without echoing raw errors.
- components/WalletPicker.tsx: the connect dialog on the existing Sheet, one
  in-flight request at a time, errors shown inline with role=alert, closes on
  success. components/ConnectWallet.tsx: a button in the caller's styling
  that opens it.
- Header, trade panel, launch form, posts composer and the /me dashboard all
  connect through the picker; no component calls useConnect any more except
  the picker, and a test pins that.
- @coinbase/wallet-sdk ^4.3.7 installed (registry tarball, integrity matches,
  no eval or script/iframe injection; its origins were already in the CSP).

Verified in headless Chrome against the dev server: no wallet shows Coinbase
plus guidance and the Coinbase row waits on its popup; a fake EIP-6963 wallet
is listed, connects, and the header shows the address chip; a bare
window.ethereum is offered as "Browser wallet". Lint, typecheck, 279 tests
and a production build pass.

Co-Authored-By: Claude Fable 5.1 <[email protected]>
Claude-Session: https://claude.ai/code/session_0146kzimBSuHTErfKYhMCaic
2026-09-10 10:46:50 +08:00
Kevin Codex d82d7e5e4f Merge pull request #19 from Gitlawb/feat/feed-skeleton
feed: rebuild the /feed loading skeleton to match the real page
2026-09-09 23:14:09 +08:00
Kevin CodexandClaude Fable 5.1 ace3bcd20b feed: rebuild the /feed loading skeleton to match the real page
The /feed loader still mirrored the old single-column feed (48rem wide,
ten 32px rows), so navigating to Posts showed a narrow, short placeholder
that jumped into the wide two-column layout when posts streamed in.

- feed/loading.tsx now borrows the SectionShell and CommunityFeed CSS
  modules: intro (eyebrow, title, description, pill action), then the
  panel (toolbar, filters, result line, posts, foot) beside the guide and
  note aside, so breakpoints stay in sync with the real markup
- new SkFeedPost primitive matches one community post: 40px wallet
  avatar + author heading, 26px-pitch body lines, 20px token tile line,
  "Open conversation" line, 24px padding (20/16 on phones)
- SkPost gains `i` and `avatar` ("round" post / "tile" launch-tape row),
  a 28px avatar and a clamped-body shape; the home sidebar skeleton uses
  56px widget headers and COMPACT_LIMIT rows
- feed-loading.test.ts pins the shell/region usage, region order, post
  anatomy and the home sidebar shapes so the loader cannot drift again

Measured against the live page in headless Chrome at 1440px and 500px:
every region lands within 1-2px of the real one.

Co-Authored-By: Claude Fable 5.1 <[email protected]>
Claude-Session: https://claude.ai/code/session_01EBa4QU3W9Ak65FwHSUs6hR
2026-09-09 22:16:57 +08:00
Kevin Codex ba3c1a7e7d Merge pull request #18 from Gitlawb/chore/next-16.3.4
deps: Next 16.3.4 (fixes two critical RCE advisories), eslint-config-next 16.3.4, js-yaml 4.3.2
2026-09-09 21:43:02 +08:00