A null receipt, a non-array logs field or a malformed log would have thrown
outside the guarded request and wedged the range; anything not shaped like
receipts now counts as "unavailable" and the skip-with-alert path runs.
- Dockerfile: declare the Arc build args in the runtime stage too (ARG does
not cross FROM; the image's own env for them was empty and production only
worked because the same values are Fly secrets)
- indexer: a single token's Transfer logs in a block over the node's result
cap are read from the block's receipts (eth_getBlockReceipts is uncapped;
Arc's node has it) before the block is ever skipped
- verify.sh: a failed verification fails the script after every target ran
- form copy: "same launch, same rules" (the Arc factory carries a guard)
- LAUNCHPAD.md: why the Arc factory's copied NatSpec stays as deployed
CodeQL's autofix replaced the per-chain lookup with a switch, which loses the
registry guarantee that a new chain fails the typecheck until it names its RPC
variable. RPC_ENV_NAME in chainKeys.ts keeps that guarantee, is indexed without
a dynamic method call, and is shared by the server module and the test mock.
LaunchFactoryArc 0x815542E8b392389A1389E22E588E4B62A67Ade72 and LaunchLocker
0xcd1680D26922fcd9CabFbb8a56bA40C333fD842a on Arc (5042), block 21165817, tx
0x9814d588…6a4b7, deployed 2026-09-16 from the same deployer at nonce 0: the
same addresses as Base and Robinhood Chain. Deployed bytecode checked against
the local build (identical, immutables aside) and the locker against Base's.
Sourcify: exact match for both; the Arc explorer's API was still gated.
- 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
- 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
main's bridge (PR 49) registered Arc on its own for bridging. After the rebase
the bridge reuses the registry's chain: one definition, the RPC proxy has no
Arc special case, wagmi registers every chain through the same proxied
transport, and the wallet switcher shows the official Arc mark next to the
Base and Robinhood ones as a per-chain record.
Tests follow: the chain mock reads the per-chain RPC env like the real module
(the Arc proxy test sets ARC_RPC_URL), the bridge tests expect Arc as a
registry chain, and the chain-branching guard skips the bridge directories,
which speak Relay's own chain ids and names by design.
LaunchFactoryArc is only ever deployed on Arc, so its native-quote refusal no
longer switches on the chain id: the diff to LaunchFactory.sol is now the
contract note, the name, the error and one revert line. The sweep demo in the
fork suite deploys the plain LaunchFactory on the Arc fork instead of cheating
the chain id.
App: the wallet switcher and the list/feed chain filters offer configured
chains only (all chains in development), the stock USD and decimals arms in the
list query are scoped by chain like every other arm, the indexer skips a
single token's holder update for a block whose Transfer logs exceed the node's
cap instead of wedging, blank confirmation env values read as unset, the
insufficient-funds error no longer says ETH, and the last two-chain strings
are gone. verify.sh scopes address overrides to Arc; the drift check ignores
trailing whitespace.
The Arc guard lives in src/LaunchFactoryArc.sol, a verbatim copy of
src/LaunchFactory.sol plus the native-quote refusal, so the file Base and
Robinhood Chain were deployed and verified from stays byte-identical.
script/check-arc-factory.sh (run in CI) fails if the two files drift apart in
anything but the guard; docs/LaunchFactoryArc.diff records that diff.
The deploy script deploys LaunchFactoryArc on chain 5042 and LaunchFactory
elsewhere; verify.sh submits the Arc factory under its own source; the Arc
fork suite and the guard unit test target LaunchFactoryArc.
On Arc the native asset is USDC and the same balance is also the ERC-20 at
0x3600…0000. LaunchLocker keeps one ledger per currency, so a native-quoted
position there would read a credited ERC-20 share (a payout on Circle's
blocklist; three sanctioned addresses are blocklisted on Arc's USDC today) as
native surplus and pay it to another launch's recipients, after which every
smaller USDC collect underflows. Found in the pre-deploy contract review.
LaunchFactory.launch now reverts NativeQuoteUnsupported for quote = address(0)
when block.chainid is 5042 (Arc-only; Base and Robinhood unchanged, and the
locker is byte-identical everywhere). The Arc fork suite reproduces the sweep
with the guard cheated off, pins the refusal, and ports the rug-proof
assertions to Arc's PositionManager; a unit test pins the chain scoping.
The deploy script refuses to run from a non-zero deployer nonce unless
ALLOW_NONZERO_NONCE=true, since nonce 0 is what keeps the addresses identical;
verify.sh takes FACTORY/LOCKER overrides.
- 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
The factory is permissionless and documents address(0) as the native quote, so
a pool quoted in it can exist on Arc whether or not the form offers one. Every
USD conversion priced address(0) at ETH on every chain, so a $10k native-quoted
Arc launch would have shown a multi-million-dollar market cap, topped the mcap
and volume sorts, the feed, share cards and the public API, and inflated the
hero totals. Found in the pre-merge security review.
NATIVE_QUOTES names what address(0) is per chain (ETH; USDC at 18 decimals and
a fixed dollar on Arc). quoteInfo resolves it, the SQL stable arm carries it,
the ETH arm is scoped to the chains whose native asset is ETH, Arc native volume
joins the USDC bucket at 6 decimals, and the trade panel decides native by
address so such a pool trades with value rather than through Permit2.
LaunchLocker._burn moves an ERC-20 fee to 0x…dEaD with a plain transfer, and
would make collect() revert forever if the token refused it. Circle's USDC has
a blocklist and Arc reverts transfers to some special addresses, so the Arc fork
suite now launches in burn mode, buys, collects, and asserts the whole USDC fee
landed at dEaD (checked against live Arc: dEaD is not blocklisted).
DeployLaunchFactory knows Arc's PoolManager and PositionManager; verify.sh
targets the Arc Blockscout. The new fork suite deploys the factory on a fork of
Arc mainnet and proves a USDC-quoted launch, buy and fee collection on the live
Uniswap v4, that native and ERC-20 USDC are one balance, and that the live
Universal Router decodes the v2 ExactInputSingleParams layout and rejects v1.
It only runs under Circle's arc-foundry with the arc profile: USDC's ERC-20
face moves native balance through an Arc-only opcode, so upstream forge reverts
every transfer with OpcodeNotFound. Under plain forge the suite skips.
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
Every per-chain value now comes from a Record<ChainKey, …> keyed off a pure
chain list (src/lib/chainKeys.ts), so adding a chain fails the typecheck until
it has its RPC env, explorer, deploy block, swap site, stock source, GITLAWB
address and form copy. Before, ~25 two-way ternaries sent any non-Base chain
down the Robinhood arm (RPC env, deploy block, stock registry, copy), and the
sitemap, health route and posts regex hardcoded both chain ids or names.
- chainKeys.ts: keys, ids, labels, explorers, isChainKey/chainKeyOf/chainKeyOr,
chainList for copy; seo.ts reuses it instead of a second ChainKey
- server env lookups (RPC, deploy block) and NEXT_PUBLIC dev RPCs as records
(literal process.env reads kept so Next still inlines them)
- stocksServer: explicit STOCK_SOURCE per chain; unknown chains get no stocks
- GITLAWB_ADDRESSES allows null; the gitlawb SQL match is built from it
- wagmi config, chain toggles, network glyphs, badges, locker proof links and
hero per-chain counts all map over CHAIN_KEYS
- schema: drop the chain_id DEFAULT 8453 so an insert without a chain fails
instead of being filed under Base (every insert already names chain_id)
- chainKeys.test.ts guards against new chain-name branches and hardcoded ids
No behaviour change for Base or Robinhood.
Explain missing, queued and underpriced approvals without treating
missing receipts as failed transactions. Block visible wallet queues
before a new approval or deposit signature.
Verify mined replacement hashes against the exact approval and fresh
allowance. Prevent delayed polling from undoing confirmed recovery.
No nonce overrides, automatic retries or contract changes.
The note was served publicly from /brand and referenced by nothing. The
provenance it recorded already lives in a Source comment inside every SVG,
which the UI tests assert.
Viem accepts JSON-RPC error envelopes even on HTTP failures, so a single upstream error object can still crash a batched gas preflight. Return non-RPC HTTP errors for retryable failures and reject mismatched response IDs or malformed envelopes. Preserve Kevin's Arc proxy, method denials, recovery flow, and actionable error copy.
Cover real viem batching and retry behavior, keep bridge alerts within the panel, and leave unrelated local changes out. Verified 627 unit tests, 10 read-only live routes, Base/Arc approval preflights, lint, typecheck, and production build.
otherBridgeChain had no callers. docs/bridge.md now records the
maintainer's Base USDC to Arc transfer on 2026-09-16 and what the wallet
test surfaced: Phantom cannot add Arc or Robinhood, the proxy's whole-batch
403 on eth_fillTransaction, public-node rate limits, and why publicnode is
unsuitable as a proxy upstream.
viem probes eth_fillTransaction when estimating Base fees. The proxy replied
HTTP 403 for the whole batch, so every read in it failed with "unknown RPC
error" and viem never took its fallback path. Disallowed methods now get a
JSON-RPC -32601 error in their own slot while the rest of the batch is
forwarded; the allowlist itself is unchanged.
Public Base and Arc nodes rate-limit a single quote's burst of reads. They
answer inside a 200 body, sometimes with one object where a batch array was
due, which viem cannot use ("unknown RPC error" or a TypeError in its
batch scheduler). The proxy now maps rate-limit and malformed replies to
HTTP 429/502 so viem retries with backoff, and Arc reads go through the
proxy (ARC_RPC_URL upstream, else the official node) instead of straight
from every browser. The Arc origin leaves the CSP; a dev override
NEXT_PUBLIC_RPC_URL_ARC is allowed like the other chains.
viem wraps unrecognised wallet/RPC error codes as "An unknown RPC error
occurred." and keeps the provider text in details. bridgeErrorMessage now
appends that text, maps the wallet's "unrecognized chain" reply to an
actionable hint, and treats any insufficient-funds cause uniformly.
The observation that shows the discard control only decides visibility.
Removal now fetches a fresh provider status and, when a hash exists, the
source-chain receipt under the per-wallet lock, and keeps the record if
either shows activity. The button is disabled while that check runs.
Neither file is read by code or tests. They are output templates from the
contributor's design tooling and duplicate theme tokens that already live in
the stylesheet. docs/bridge.md remains the bridge documentation.
Review fixes on the Relay bridge:
- A sped-up or cancelled-and-replaced deposit no longer blocks tracking
forever. When the wallet's hash is unmined and Relay reports a different
source hash, that hash is verified as this wallet's exact deposit for the
same order and adopted.
- A record with no deposit anywhere can be discarded after 15 minutes, only
while Relay still reports "waiting" with no hashes and the source chain
has no receipt, observed within the last minute. Unmined approvals get the
same bounded discard; a late-mined approval only grants the exact
allowance every deposit re-reads.
- A wallet chain change during a quote no longer leaves the form locked on
"Finding your route…".
- Quote validity travels as ttlMs and is anchored on the browser clock at
request time, so device clock skew cannot expire or extend a quote.
- Fetch timeouts use a hand-linked signal instead of AbortSignal.any, which
older in-app wallet browsers lack.
- Relay's chain catalogue is cached per fetcher for a minute instead of
re-downloaded on every quote; an unverifiable copy is dropped.
- The USDC approval is no longer gated on the 45 s quote TTL, since a fresh
quote is required after it anyway.
- Wallet and RPC errors go through friendlyError; the dialog states that
Openlaunch does not operate Relay or hold funds in transit.
Unit tests cover the hash adoption rule, the quoting-lock reset, expiry
anchoring, the linked timeout signal, both discard gates and the catalogue
cache. 621 tests, lint, typecheck, build and the read-only live route suite
pass.
Replace letter placeholders with the existing official Base and Robinhood assets. Switch the Robinhood mark through theme CSS to preserve contrast without adding hydration state or changing wallet actions.
CodeQL mistook the split-array includes assertion for substring URL validation. Use Set membership for every expected origin so the test's token boundary is explicit without changing production CSP behavior.
Let users fund the same wallet across Base, Robinhood and Arc without leaving Openlaunch or deploying new contracts. Validate Relay orders and deposits, require exact USDC approvals, and preserve uncertain transfers for recovery instead of resending.
Include route, approval, recovery and UI regression tests, official asset provenance, and rollout documentation. Keep Robinhood USDC disabled because current routes fail the deposit and impact safeguards.
Two notes on the paged read, both taken:
- Identity. bb_launch_swaps is keyed (chain_id, tx_hash, log_index) and
the indexer stores each PoolManager.Swap log on its own, yet the feed
deduped on chain:tx:token:amount, so two equal swaps in one tx would
have collapsed into one row. The swap feed item now carries log_index
and the dedupe key is chain:tx_hash:log_index. The tape and the toast
tracker key swaps the same way, so the same pair stays two rows there
too instead of colliding as React keys.
- Consistency. Offset pages over a live table could skip a swap indexed
between two reads. Pages are now keyset: the cursor is the last row
seen and each page reads rows strictly older on the complete unique
ordering (block_time, log_index, chain_id, tx_hash), with a leading
block_time <= bound so the block_time index does the work (index scan,
49 rows read for a 48-row page on the production copy). No
transaction on the request path, no OFFSET.
Tests: the pager's fake source is keyset too; a new case inserts a swap
at the head between page reads and shows nothing skipped or repeated;
source contracts pin the cursor clause, the full ordering, the log-index
identity in the feed item and in the tape/toast keys.
A fixed 2n over-fetch let 30 dust swaps ahead of 24 real ones shrink
the feed to 18 rows while older real trades sat unread. The swap arm
now pages: collectNonDust reads 2n newest swaps, then older pages only
while dust keeps the count short, stopping when the quota is met, the
source runs dry, or FEED_SWAP_PAGES (4) pages have been read, so a bot
flood is still a bounded read (index scan on block_time + incremental
sort, no table scan). Pages are offset-based over a live table, so a
row that shifts between two reads is dropped by the tape's own key.
Launches and swaps are read separately and merged newest-first.
Regression test: 30 dust swaps ahead of 24 real ones yield all 24, with
exactly one extra page read; plus tests for the dry source, the page
cap, and a row shifting across a page boundary.
isDustSwap priced a swap whenever usd was a finite number, so a quote
whose live price momentarily read 0 (a GITLAWB pool hiccup, a stock feed
returning nothing sensible) would have marked every trade in that quote
as dust and emptied it from the feed. Only a positive price decides;
otherwise the display floor applies as with no price at all.
- tx-toasts.test.ts: the harness accepts binding overrides; a new test
runs the real fmtQuote through the actual TxToasts handler and checks
that a 4,999-raw USDG buy toasts as "Buy <0.01 USDG of DUSTY", a
sub-5-gwei sell as "Sell <0.00000001 ETH of DUSTY", 5,000 raw as
"Buy 0.01 USDG", and that no card ever reads as a zero-amount trade.
- feed-dust-wiring.test.ts: source contracts that getLaunchFeed
imports dropDust, over-fetches 2n swaps while the launch arm stays at
n, keeps every candidate row in the union, returns dropDust(items, n)
last, and puts no raw-wei floor in SQL; that the tape, the toasts and
the trade table all format swap amounts through fmtQuote with the
quote's decimals; and that the trade table's symbol-less cell reads
"<0.01" rather than "0" for a dust row.
A Uniswap v4 swap can move 1 wei of quote, and the tape and the toasts
were announcing such trades as "Buy 0 USDG": fmtQuoteUnits rounded
anything under half a cent (or under 5 gwei for 18-dec quotes) to a
bare "0". A zero delta can never be recorded as a buy, so every one of
those rows was a real dust trade shown dishonestly.
- feed-dust.ts: swaps worth less than FEED_DUST_USD (one cent) are
dropped from getLaunchFeed after pricing; with no USD price the rule
falls back to the display floor, so whatever would have read "0" is
dust while a price feed is down. Launches always pass. The swap arm
over-fetches 2n rows so a burst of dust cannot empty the feed. The
token trade table, totals, candles and ranking are untouched.
- fmtQuoteUnits: a non-zero amount under the display floor prints
"<0.01" / "<0.00000001"; only an exact zero prints "0", so empty
volume stats are unchanged.
An empty feed records nothing, so treating it as the post-gap baseline let the
next populated poll surface the missed activity. The gap stays open until a
populated snapshot is seen. (CodeRabbit)
The missing-contracts explanation helps a developer, but a visitor seeing a
chain that is not live yet (Arc, next) should read it as upcoming. Production
builds keep "Coming soon."; development still says the contract settings are
missing.
Cards show one at a time for 6s, so 20 queued let "just launched" and buy
cards run up to two minutes behind. Ten keeps what is on screen within about a
minute; the newest activity is kept and own confirmations are not capped.
A timeout can fire a moment before Date.now() reaches the deadline. expireToast
then returned the same state, the effect never re-ran, and the card plus
everything queued behind it (own confirmations included) stayed on screen
until dismissed. The callback now waits out the remainder. (CodeRabbit)
Polling pauses while the tab is hidden, so the first snapshot back marked
everything missed as new and queued up to 20 old activity cards, shown one at
a time for minutes. Snapshots more than 30s apart by server time now refresh
what has been seen without queueing it; live activity resumes on the next
poll. Server times are compared with each other, so client clock skew does
not matter.
Live launches, buys and sells are the social proof of engagement, so a
visitor sees them unless they turn them off in notification settings. Only
an explicit saved off mutes them; a missing, unknown or cleared value falls
back to on, and the server snapshot matches that default.