Skip to content

fix(prices): NIGHT has 6 decimals — seed and map it as 1 NIGHT = 10^6 Stars - #60

Merged
acedward merged 1 commit into
mainfrom
00007-night-decimals-6
Sep 3, 2026
Merged

fix(prices): NIGHT has 6 decimals — seed and map it as 1 NIGHT = 10^6 Stars#60
acedward merged 1 commit into
mainfrom
00007-night-decimals-6

Conversation

@acedward

@acedward acedward commented Sep 3, 2026

Copy link
Copy Markdown
Contributor

What / why

known_tokens.decimals means base units per PRICED coin (see the table's own comment in packages/database/migrations/000-init.sql and resolveTokenPrice()/tokenPriceFromAsset() in packages/database/price-map.ts): a token's price is served per base unit as asset_prices.price_usd / 10^decimals, and that value is never rescaled again downstream.

NIGHT's seed row (landed in #54, "seeded asset prices") carries decimals: 0. That prices one Star (NIGHT's base unit) at NIGHT's whole-coin price (~$0.019) instead of at ~$0.000000019 — every GET /v1/prices, GET /v1/quote and sponsorship threshold touching NIGHT is off by 10^6. Anything registered to mirror NIGHT's row (e.g. sNight on midnight-1-offers, see that project's Q14 plan question) inherits the same error.

Ledger source of truth: STARS_PER_NIGHT = 1_000_000 in midnight-ledger/ledger/src/structure.rs (MAX_SUPPLY = 24_000_000_000 * STARS_PER_NIGHT) — 1 NIGHT = 10^6 Stars. Owner decision (2026-09-03): "decimals for Night = sNight = 6".

Changes

  1. packages/database/migrations/000-init.sql — NIGHT decimals 0 → 6. Reviewed the USDC placeholder row (1111…1111) too: real USDC is 6 decimals on every chain it exists on (same reasoning already applied to the USDM placeholder row), so it moves 0 → 6 as well, with the reasoning recorded in the comment. USDM was already 6. Faucet-minted dev/test tokens (WBTC, WETH, TESTTOKEN*, …) are untouched — they keep the table's DEFAULT 0, which is genuinely correct for them (the faucet mints 1000 base units = 1000 coins).
  2. packages/database/price-map.tsPriceMapEntry.decimals can be set syntactically on DEFAULT_NAME_ASSET_MAP entries, but I found it would be dead there: resolveAssetId() only ever reads decimals off the overrides parameter (the PRICE_FEED_MAP-parsed map), never off the built-in name-fallback map. So the seed row is the one and only place NIGHT's decimals has to be correct — documented this in a comment rather than adding a no-op decimals: 6 to the built-in map entry. No behavioural change to this file.
  3. Tests — updated to the corrected numbers, computed with the same exact decimal-string arithmetic tokenPriceFromAsset() uses (no floats in the expectations):
    • packages/database/asset-prices.test.ts — NIGHT/USDC decimals now 6; added a NIGHT per-base-unit-price test mirroring the existing USDM one (0.01918181 / 10^6 = "0.00000001918181", exact).
    • packages/node/api.test.tsGET /v1/prices NIGHT row: decimals: 6, price_usd: "0.00000001918181".
    • packages/node/offers-sponsorship.test.ts and packages/batcher/sponsorship-gate.test.ts — the sponsorship gate's give_usd/want_usd. The proven fixture gives exactly 1,000,000 base units of NIGHT, which at 6 decimals is exactly 1 NIGHT, so give_usd is numerically the coin price itself (0.01918181) — and both display paths (round2() in packages/node/offer-sponsorship.ts, usd() = .toFixed(2) in packages/batcher/celestia.ts) round that to "0.02". The underlying sponsorship decision (percentage-based, full double precision) is unaffected by the scale change — only the previously-19181.81-now-0.02 displayed USD amounts needed updating.
  4. Docs (README.md, API.md, FRONTEND-API-HANDOFF.md, .env.mainnet.example, .env.preview.example, deploy/.env.example) — state that decimals is base units per priced coin, never a colour's own display decimals, and that NIGHT is seeded at 6 (1 NIGHT = 10^6 Stars), with the ledger reference.

Rollout note

This changes a SEED VALUE only — no schema shape change (one-schema-file rule respected: 000-init.sql edited in place, no new migration file). An existing database keeps its old NIGHT/USDC rows (decimals: 0) after this merges; a fresh redeploy picks up 6 automatically. For a live deployment (e.g. preprod) that must not be redeployed from zero, an operator with DB access runs:

UPDATE known_tokens SET decimals = 6 WHERE name IN ('NIGHT', 'USDC');

(or just WHERE name = 'NIGHT' if the USDC placeholder decimals is not wanted — see point 1 above for why I moved it too).

Breaking / behaviour change

Non-breaking for the API shapeGET /v1/prices and GET /v1/quote return the same fields. Behaviour-changing for the VALUES: every NIGHT-priced quote and sponsorship threshold moves by 10^6 — that is the fix, not a regression. Anyone consuming absolute USD amounts for NIGHT (rather than ratios) will see numbers 10^6 times smaller than before, which is now correct.

Testing

CI is the gate (no local docker stack was brought up, per repo convention). Ran the exact unit-tests CI job locally (bun test packages/database packages/validator packages/node packages/offer-guard packages/batcher packages/solver-core packages/solver packages/solver-frontend packages/tests/grand-e2e, with the same Compact-artifact stub CI creates):

  • 1293 pass, 2 skip, 1 fail across 100 files, both before and after re-checking against a pristine main (see below).
  • The one failing test, packages/tests/grand-e2e/lib/solver-offerfiles-real-image-secret-scan.test.ts ("handles a large physical inventory and internal-link fanout without process amplification"), is pre-existing and unrelated: it creates 15,000 hard links + 1,000 symlinks and asserts a Docker-based scan completes inside a 25s budget. I reproduced the identical failure (Expected: 0, Received: 70) on a clean main checkout run through the same full suite, on this same (shared, loaded) machine — it passes standalone (9/9) but is flaky under load as part of the full run, on main as much as on this branch. Not touched by this PR's diff.
  • Everything touched by this PR — packages/database/asset-prices.test.ts, price-map.test.ts, packages/node/offers-sponsorship.test.ts, packages/node/api.test.ts, packages/batcher/sponsorship-gate.test.ts, packages/batcher/price-lookup.test.ts — is 145/145 green, run in isolation and as part of the full suite, across multiple repeated runs.
  • bun run check:pgtypes — clean, no generated-query drift from the SQL comment/value changes.
  • bun run typecheck (backend + solver + price-feed) — 0 diagnostics in all three gates.
  • Reproduced the typecheck-playground CI job locally (bun build the five api-examples entrypoints + bunx tsc -b in docs/ with the same generated-contract stub) — clean.

No attribution lines.

… Stars

known_tokens.decimals means "base units per PRICED coin" (resolveTokenPrice()
divides an asset's per-coin price by 10^decimals; the value is never rescaled
again downstream). NIGHT's seed row (packages/database/migrations/000-init.sql,
landed in PR #54) carried decimals=0, which prices ONE STAR (NIGHT's base
unit) at NIGHT's whole-coin price of ~$0.019 instead of at ~$0.000000019 —
every GET /v1/prices, GET /v1/quote and sponsorship threshold touching NIGHT
was off by 10^6, and anything registered to mirror NIGHT's row (e.g. sNight on
midnight-1-offers, per its own Q14) inherited the same error.

Ledger source of truth: STARS_PER_NIGHT = 1_000_000 in
midnight-ledger/ledger/src/structure.rs (MAX_SUPPLY = 24_000_000_000 *
STARS_PER_NIGHT) — 1 NIGHT is 10^6 Stars.

- packages/database/migrations/000-init.sql: NIGHT decimals 0 -> 6. USDC's
  placeholder row also moves 0 -> 6 (real USDC is 6 decimals on every chain it
  exists on, same reasoning already applied to the USDM placeholder). Faucet-
  minted dev/test tokens are untouched: they keep the table's DEFAULT of 0,
  which is genuinely correct for them (1000 base units = 1000 coins).
- packages/database/price-map.ts: documented that DEFAULT_NAME_ASSET_MAP
  entries cannot effectively carry `decimals` — resolveAssetId() only reads
  that field off the PRICE_FEED_MAP-parsed override map, never off the
  built-in name map — so the seed row is the one place NIGHT's decimals must
  be correct. No behavioural change to this file.
- Tests updated with the corrected numbers, computed with the same exact
  decimal-string arithmetic tokenPriceFromAsset() uses (no floats):
  packages/database/asset-prices.test.ts (NIGHT/USDC decimals, a new NIGHT
  per-base-unit-price test mirroring the existing USDM one),
  packages/node/api.test.ts (GET /v1/prices), packages/node/offers-sponsorship.test.ts
  and packages/batcher/sponsorship-gate.test.ts (the sponsorship gate's
  give_usd/want_usd, which round to 2 decimals for display — 0.01918181 ->
  "0.02" — since this fixture's 1,000,000 base units is exactly 1 NIGHT).
- Docs (README.md, API.md, FRONTEND-API-HANDOFF.md, .env.mainnet.example,
  .env.preview.example, deploy/.env.example): state that `decimals` is base
  units per priced coin, never a colour's own display decimals, and that
  NIGHT is seeded at 6.

Non-breaking for the API shape; behaviour-changing for NIGHT-priced quotes and
sponsorship thresholds, which now move by 10^6 (that is the fix). A SEED VALUE
only, no schema shape change (one-schema-file rule respected): an EXISTING
database keeps its old NIGHT/USDC rows (decimals=0) until migrated by hand.
@acedward
acedward merged commit 61f055e into main Sep 3, 2026
5 checks passed
acedward added a commit that referenced this pull request 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