Seed sNight (shielded-night wrapper) as a default known token, with the preview colour - #61
Merged
Merged
Conversation
… seed sNight is the shielded-night wrapper: NIGHT held as a shielded (Zswap) token, locked 1:1. Its colour is tokenType(pad(32,"shielded-night:wrapper"), self()), so it derives from the contract address and differs per network — which is why it cannot live in 000-init.sql (known_tokens.name is UNIQUE, so preview's and preprod's colours cannot share the name SNIGHT there) and why editing that file would not help anyway (the runtime applies migrationTable by BLOCK HEIGHT, so an entry only ever runs while a database syncs through block 1, never on one that is already live). network-tokens.ts therefore holds the colours keyed by network, beside the address each was derived from, and seedNetworkKnownTokens() inserts the running network's row idempotently: preview 80b89b9a…1396 -> 793c29c94f72972bfbd861e8e84e55480ccc8e57a7b74067f35a5672c816f99c preprod e354e672…8950 -> 8fac382b0d91ad68cf3e2479bf4d21a127f187b83151a11773a8b04bd4576819 mainnet and undeployed are absent: the contract is not deployed there. decimals are copied from the seeded NIGHT row rather than taken from the contract's 6, because the sponsorship gate compares per-base-unit USD values — a NIGHT <-> sNight offer of equal base units is only at par when both rows carry the same decimals, and copying keeps that true after any later correction of NIGHT's own value. The INSERT is guarded with NOT EXISTS on both unique columns, so neither a restart nor a concurrent writer can turn it into a UNIQUE violation; a name already held by another colour is reported with a code and a reason instead of throwing. DEFAULT_NAME_ASSET_MAP gains SNIGHT -> midnight-3, so SEEDED_ASSET_IDS is unchanged (no new asset to fetch). network-tokens.test.ts re-derives both colours from the committed addresses with ledger-v8, so a contract redeploy is a one-line, testable change. 000-init.sql is untouched.
apiRouter is the one place the node is handed a DB connection and all four main.*.ts entry points pass the same router, so the seed is wired there — once — and cancelled on server close. It retries, deliberately. On a FRESH database the schema does not exist yet when the router is built: the runtime spawns the HTTP server before the first block, and 000-init.sql is applied inside that block's transaction, so the first attempt legitimately fails with 42P01. The helper waits (1 s, up to ~10 min, unref'd) instead of failing, and then gives up rather than looping forever. On an existing database — the preprod rollout case — the first attempt succeeds and no timer is ever armed. Nothing here can crash the node: a node that cannot label one token must still serve. One log line per start: "seeded SNIGHT <colour> for <network>" the first time, "already registered" after that, a WARN for a real collision, and nothing at all on a network with no deployed contract. Tests: network-token-seed.test.ts drives the helper against a pglite database that starts EMPTY and only gets the schema part-way through — the fresh-database acceptance scenario — plus the give-up, stop() and "undeployed never queries" paths. api.test.ts adds three cases over the real router: the seed writes the preprod colour once, GET /v1/known-tokens lists it with NIGHT's shape, and GET /v1/prices prices it at NIGHT's per-base-unit price from the same single asset.
Every place that lists the default tokens or the NAME -> asset map now mentions SNIGHT, and says the thing an operator needs to know: it is not in the SQL seed, the kernel registers it at start for the network in MIDNIGHT_NETWORK_ID, and preview and preprod carry different colours because the colour derives from the contract address. The 000-init.sql change is a COMMENT ONLY — the INSERT is untouched, so the "three redeploy-stable tokens are seeded, and only those" assertion still holds — and it records both reasons sNight cannot go there: the UNIQUE name and the fact that the file never reaches a live database. API.md's GET /v1/known-tokens example gains the preprod row and names the preview colour; the demo-endpoint warnings are unchanged.
…g it at start Replaces the per-network startup seed from the earlier commits on this branch with a plain row in 000-init.sql, next to NIGHT/USDC/USDM — the USDM precedent. sNight's colour is tokenType(pad(32,"shielded-night:wrapper"), self()), so it derives from the shielded-night contract address and differs per network. The schema now seeds PREVIEW (793c29c9...f99c) as the default and the comment above the INSERT carries the derivation, both contract addresses, the preprod colour, the mainnet gap, and — in capitals — the instruction to patch the row when deploying elsewhere, plus the UPDATE/POST a database that already exists needs instead. The row carries NIGHT's decimals and asset (midnight-3), so a NIGHT <-> sNight offer of equal base units stays at par under the sponsorship gate. Removed again: packages/database/network-tokens.ts, packages/node/ network-token-seed.ts and their tests. packages/database/mod.ts and packages/node/api.ts are byte-identical to main once more. Tests, in packages/database/asset-prices.test.ts and packages/node/api.test.ts: the four seeded tokens are NIGHT/SNIGHT/USDC/USDM and SNIGHT.decimals is asserted against NIGHT's row rather than the literal 0; the seeded colour is re-derived from the preview address with ledger-v8; and the SQL comment itself is parsed so each documented address/colour pair must still derive from one another and mainnet must stay undocumented — the comment cannot rot away from the row.
…the row README, API.md, .env.preview.example and deploy/.env.example described the startup seed that no longer exists. They now say sNight is a schema seed like NIGHT/USDC/USDM, that the seeded colour is preview, and how another network gets its own: patch the row in 000-init.sql before the database is created, or UPDATE / POST /v1/known-tokens on a database that already exists. The API.md example response shows the preview colour (id 2) instead of the preprod one.
main corrected NIGHT to 6 decimals (1 NIGHT = 10^6 Stars, PR #60), which is the follow-up this branch had filed. sNight is seeded with NIGHT's decimals by design, so the SNIGHT row moves 0 -> 6 with it and the pair stays at par; the test still asserts SNIGHT.decimals against NIGHT's ROW, not a literal, so the two can only ever move together. Conflicts, all in the seeded-token block and its documentation: * 000-init.sql — main's NIGHT/USDC 6-decimals notes kept, the SNIGHT row and its "patch this row for another network" note kept, seeded at 6. * asset-prices.test.ts — main's USDC comment and the SNIGHT expectation kept side by side; the NAME-map backstop case now uses 6. * .env.preview.example, deploy/.env.example — both paragraphs kept.
This was referenced Sep 3, 2026
Merged
Draft
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Seeds sNight — the shielded-night wrapper, NIGHT held as a shielded (Zswap) token, locked 1:1 — as a default known token in
packages/database/migrations/000-init.sql, alongside NIGHT / USDC / USDM.The colours, and why one of them is the default
sNight's colour is
tokenType(pad(32, "shielded-night:wrapper"), self()), so unlike the other three seeds it derives from the contract address and differs per network:80b89b9a4213c61da84f54b2ea02e2809f9c4dedbdafacd04b38d4667bee1396793c29c94f72972bfbd861e8e84e55480ccc8e57a7b74067f35a5672c816f99ce354e6725893397e6a2dfa44522a017fabb5d9c92efed50288711f5f865c89508fac382b0d91ad68cf3e2479bf4d21a127f187b83151a11773a8b04bd4576819MAINNET_ADDRESSis empty in shielded-night'sfrontend/.env)known_tokens.nameisUNIQUE, so exactly one sNight row can exist per database. Preview is the seeded default, and the comment above theINSERTsays in capitals that the row must be patched when deploying to another network — with preprod's address and colour, and mainnet's gap, written out right there.Derivation. Each colour is
rawTokenType(pad32("shielded-night:wrapper"), <address>)from@midnight-ntwrk/ledger-v8(8.1.0, the version this repo pins), mirroring the contract'stokenType(pad(32, "shielded-night:wrapper"), kernel.self()). The addresses are the ones committed in the shielded-night repo'sfrontend/.env.Rollout — preprod needs one manual step
000-init.sqlruns against an empty database exactly once; the runtime appliesmigrationTableby BLOCK HEIGHT (applyUserMigrations(blockNumber, …)per block, keeping entries whoseblockHeight ?? 1equals the current height), so an entry with noblockHeightonly ever runs while a database syncs through block 1 — never on one that is already live. That is also why this is a seed and not a new migration entry.So:
000-init.sqlfirst (the comment tells you which one).SNIGHTrow at all) — register8fac382b0d91ad68cf3e2479bf4d21a127f187b83151a11773a8b04bd4576819by hand, either withPOST /v1/known-tokens(needsENABLE_TOKEN_REGISTRY=true) or with oneINSERT. On a preprod database rebuilt from this schema it is insteadUPDATE known_tokens SET token_color = '8fac…6819' WHERE name = 'SNIGHT';.Decisions
SNIGHT, notsNightPOST /v1/known-tokensupper-cases every name it stores and all seeded names are upper-case; a mixed-case row would be the one a laterPOSTof the same name could not match. Branding belongs in the frontend's token table.decimals= NIGHT's, i.e.60, and #60's correction to6came in through the merge below with no change to the assertion.Non-breaking
No schema change, no API shape change, no new price asset (
SNIGHT → midnight-3, soSEEDED_ASSET_IDSis unchanged and the feed still makes one call).GET /v1/known-tokensgains one row on databases built from this schema. The grand-e2e dev stack checkknown-tokens lists NIGHT and does NOT auto-register offer colorsis a membership test, so it is unaffected.Tests
packages/database/asset-prices.test.ts— "the three redeploy-stable tokens are seeded, and only those" becomes the four (NIGHT, SNIGHT, USDC, USDM), withSNIGHT.decimalsasserted against NIGHT's row. Plus 3 new cases: the seeded colour is re-derived live from the preview address with ledger-v8; the SQL comment itself is parsed and each documented<network> address … colour …pair must still derive from one another (andmainnetmust stay undocumented), so the comment cannot rot away from the row or from the contract; andSNIGHTresolves tomidnight-3through the NAME map even with noasset_id.packages/node/api.test.ts(+2 cases over the real router and real migrations):GET /v1/known-tokenslists exactly oneSNIGHTwith NIGHT's shape and not the other network's colour;GET /v1/pricesreturns NIGHT's per-base-unit price,source: seed, from one asset for both legs.Full suite in a space-free clone, after merging
main: 1564 pass / 2 skip / 7 fail / 6 errors across 120 files, against anorigin/main@61f055ebaseline of 1559 / 2 / 7 / 6 across 120 measured in the same clone with the samenode_modules— +5 passing, no new failure, no new test file. The 7 failures are environmental and identical on both sides: 6 areCannot find module './managed/contract/index.js'(the Compact artifacts are not built in this clone; each counts as one fail and one error) and 1 is the known Docker secret-scanner test, whosebeforeEachtimes out here for want of its scanner image.bun run typecheckis clean in all three gates, with the same root set and the same dependency-diagnostic counts asmain.Merged with
main@61f055eWhile this was being reworked, #60 (
00007-night-decimals-6) landed the NIGHT-decimals correction — 1 NIGHT = 10^6 Stars, so NIGHT and USDC are now seeded atdecimals 6. That is exactly the follow-up this PR used to carry, so it is no longer an open item here.sNight follows automatically by the Q3 rule above: the seeded
SNIGHTrow is6, and the pair stays at par. The merge conflicted only inside the seeded-token block and its documentation —000-init.sql,asset-prices.test.ts, and the two.envexamples — and both sides' text was kept in each case.Out of scope
A frontend icon/name entry for
SNIGHT(so the chip reads "Shielded Night" rather than falling back to the colour-derived coin) is deliberately not here — it belongs in the frontend repo. Deploying shielded-night into the compose dev stack, soundeployedhas an sNight colour of its own, is likewise a separate piece of work.