Skip to content

Seed sNight (shielded-night wrapper) as a default known token, with the preview colour - #61

Merged
acedward merged 6 commits into
mainfrom
00021-snight-default-token
Sep 3, 2026
Merged

Seed sNight (shielded-night wrapper) as a default known token, with the preview colour#61
acedward merged 6 commits into
mainfrom
00021-snight-default-token

Conversation

@acedward

@acedward acedward commented Sep 3, 2026

Copy link
Copy Markdown
Contributor

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.

('793c29c94f72972bfbd861e8e84e55480ccc8e57a7b74067f35a5672c816f99c', 'SNIGHT','shielded',   6, 'midnight-3'),

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:

Network shielded-night contract address sNight colour
preview (seeded) 80b89b9a4213c61da84f54b2ea02e2809f9c4dedbdafacd04b38d4667bee1396 793c29c94f72972bfbd861e8e84e55480ccc8e57a7b74067f35a5672c816f99c
preprod e354e6725893397e6a2dfa44522a017fabb5d9c92efed50288711f5f865c8950 8fac382b0d91ad68cf3e2479bf4d21a127f187b83151a11773a8b04bd4576819
mainnet not deployed (MAINNET_ADDRESS is empty in shielded-night's frontend/.env)

known_tokens.name is UNIQUE, so exactly one sNight row can exist per database. Preview is the seeded default, and the comment above the INSERT says 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's tokenType(pad(32, "shielded-night:wrapper"), kernel.self()). The addresses are the ones committed in the shielded-night repo's frontend/.env.

Rollout — preprod needs one manual step

⚠️ This PR does not reach the live preprod database. 000-init.sql runs against an empty database exactly once; the runtime applies migrationTable by BLOCK HEIGHT (applyUserMigrations(blockNumber, …) per block, keeping entries whose blockHeight ?? 1 equals the current height), so an entry with no blockHeight only 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:

  • preview, from an empty database — nothing to do; the row is there on first sync.
  • another network, from an empty database — patch the colour in 000-init.sql first (the comment tells you which one).
  • the preprod deployment running today (no SNIGHT row at all) — register 8fac382b0d91ad68cf3e2479bf4d21a127f187b83151a11773a8b04bd4576819 by hand, either with POST /v1/known-tokens (needs ENABLE_TOKEN_REGISTRY=true) or with one INSERT. On a preprod database rebuilt from this schema it is instead UPDATE known_tokens SET token_color = '8fac…6819' WHERE name = 'SNIGHT';.

Decisions

# Decision Why
Q1 Registry name is SNIGHT, not sNight POST /v1/known-tokens upper-cases every name it stores and all seeded names are upper-case; a mixed-case row would be the one a later POST of the same name could not match. Branding belongs in the frontend's token table.
Q2 Preview colour seeded in SQL, other networks patch the row The USDM precedent: one seeded default, the alternatives in a comment beside it. (An earlier revision of this branch used a per-network startup seed; the owner chose the simpler shape, accepting the manual step on preprod above.)
Q3 decimals = NIGHT's, i.e. 6 The sponsorship gate compares PER BASE UNIT USD values, so a NIGHT ↔ sNight offer of equal base units is at par only when both rows carry the same decimals — one sNight base unit is one Star. The test asserts SNIGHT's decimals against NIGHT's row, not against the literal, so the two can only ever move together. That already paid for itself: this branch was written when NIGHT was still seeded at 0, and #60's correction to 6 came 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, so SEEDED_ASSET_IDS is unchanged and the feed still makes one call). GET /v1/known-tokens gains one row on databases built from this schema. The grand-e2e dev stack check known-tokens lists NIGHT and does NOT auto-register offer colors is 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), with SNIGHT.decimals asserted 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 (and mainnet must stay undocumented), so the comment cannot rot away from the row or from the contract; and SNIGHT resolves to midnight-3 through the NAME map even with no asset_id.
  • packages/node/api.test.ts (+2 cases over the real router and real migrations): GET /v1/known-tokens lists exactly one SNIGHT with NIGHT's shape and not the other network's colour; GET /v1/prices returns 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 an origin/main @ 61f055e baseline of 1559 / 2 / 7 / 6 across 120 measured in the same clone with the same node_modules+5 passing, no new failure, no new test file. The 7 failures are environmental and identical on both sides: 6 are Cannot 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, whose beforeEach times out here for want of its scanner image. bun run typecheck is clean in all three gates, with the same root set and the same dependency-diagnostic counts as main.

Merged with main @ 61f055e

While 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 at decimals 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 SNIGHT row is 6, 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 .env examples — 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, so undeployed has an sNight colour of its own, is likewise a separate piece of work.

… 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.
@acedward acedward changed the title Register sNight (shielded-night wrapper) as a per-network default known token Seed sNight (shielded-night wrapper) as a default known token, with the preview colour Sep 3, 2026
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.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant