undeployed: protocol-selectable (midnight-1.x default | midnight-2.x) — UNDEPLOYED_PROTOCOL switch, v2 undeployed() profile, 2.x external-stack suite - #16
Merged
Conversation
MN_ENV now selects between stagenet (the default, unchanged) and a local ledger-v9 devnet. requestedEnv() is the single validator and names both accepted values; every env-dependent seam takes the env as an optional trailing argument so an unparameterized call keeps today's behaviour. - profile.ts: V2_ENV_NAMES / V2EnvName / requestedEnv / profileFor; undeployed() with the 1.x lane's loopback defaults and MN_INDEXER_URL, MN_INDEXER_WS_URL, MN_NODE_URL, MN_NODE_WS_URL, MN_PROOF_SERVER_URL overrides; WALLET_NETWORK_IDS (pinned to the SDK constants from deploy.ts at typecheck time so the unit tier need not load the wallet barrel); GENESIS_MINT_SEED; privateStateStoreName(env). - Maintenance key: the record's network is the env and is checked on read, so a stagenet key can never be used on undeployed or the reverse. On stagenet the durable absolute path stays mandatory; on undeployed the file is optional and defaults to the gitignored .local/private-state/v2-undeployed/maintenance-key.json, with a relative override accepted. Mode 0600 and the write/read-back check apply on both. - Records and output: v2-<env>-<address>.json, `[deploy] confirmed <env> contract`, UNDEPLOYED_ADDRESS= on undeployed, network.name = env. - deploy.ts: on undeployed only, a missing seed falls back to genesis-1 with a printed warning that it is shared; stagenet still refuses. - verify-deployment.ts: same env selection; the CV_ADDRESS message names the selected env. - Tests: new profile-env.unit.test.ts covers env selection, the profile defaults and overrides, key custody per env in both directions, record paths and the printed lines. The existing file now pins MN_ENV unset so it keeps asserting the stagenet defaults.
Preview, Preprod and Stagenet are pinned to the ledger generation their chain runs, but "Local (undeployed)" is whichever devnet is on the other end, and it was hard-wired to midnight-1.x. A local Midnight 2.x devnet therefore got the ledger-v8 adapter and could not be served at all. Make that row a setting read exactly like the contract addresses: build-time `UNDEPLOYED_PROTOCOL` (the vite envPrefix already exposes `UNDEPLOYED_`), overridable at runtime by `window.SHIELDED_NIGHT.UNDEPLOYED_PROTOCOL` from an injected /config.js, default `midnight-1.x` — so every existing build and deployment behaves exactly as before. - runtime-config.ts: `resolveUndeployedProtocol()` next to `resolveContractAddress()`; runtime wins, blank falls through, values are trimmed and case-sensitive, and anything else is an ERROR naming the variable and both accepted values rather than a silent fallback — a guess here picks the wrong ledger adapter against a real chain. The module stays dependency-free and free of `import.meta.env`. - networks.ts: `resolveNetwork()` returns the row with the setting applied (label `Local (undeployed · 2.x)` on 2.x); the exported NETWORKS const is never mutated, because other consumers import it. `contractConfigurationError()` reports a bad protocol value FIRST, so it reaches the page through the existing plumbing that disables Connect. `protocolMismatchHint()` explains the failure a wallet of the other generation produces — the wallet never announces its own. - useShieldedNight: resolve the row instead of looking it up statically, and log that hint when a connection fails at or after adapter creation. Unit tests cover default / build-time / runtime-wins / blank / invalid for both seams; the docs get the switch next to the address override.
The v2 tree gets the local-chain integration the 1.x tree already has, as the 2.x counterpart of MN_EXTERNAL_STACK=1. There is no self-booting mode: this tree carries no 2.x compose file, so the suite only ever joins a stack somebody else started and never tears anything down. - test/external/global-setup.ts refuses to run unless MN_EXTERNAL_STACK is truthy, naming the variable; MN_ENV defaults to undeployed here (stagenet is accepted, and then MN_SEED is mandatory); the indexer, node and proof server are probed with a 10 s fetch and a failure names the URL(s) that are wrong; the resolved env and profile are handed to the forked workers. - test/external/shielded-night.external.test.ts reuses the deploy path's building blocks (MidnightWalletProvider.build, syncWallet, the funding assertion and sync-timeout policy from profile.ts, initializeMidnightProviders, verifyAddress) rather than porting a second wallet builder. CV_ADDRESS joins an existing deployment via findDeployedContract; without it the suite deploys with a maintenance key sampled for the run. Cases: the 11-circuit set with matching verifier keys, release metadata and (when it deployed) the maintenance authority; the two-step round trip with exact balances; the atomic convertToShielded/convertToUnshielded pair with exact balances; and a wrong-secret withdrawal refused with "no balance for this secret". - Config split: vitest.config.ts is the unit tier and keeps test/external/** out through both the include pattern and the exclude list, so `bun run test:v2` and CI's unit-v2 job can never collect a test that needs a chain. vitest.external.config.ts runs the suite serially in forked workers with 10-minute test timeouts and retries off by default, because every case asserts exact balances. - Scripts: contracts/v2 `test:external`, root `test:v2:external`. - TESTING.md documents the suite, its env block, what it asserts, and that it needs a throwaway stack.
…-cwd= form README gains a "Which lane runs where" table (preview/preprod 1.x, stagenet 2.x, undeployed 1.x by default or 2.x opt-in) and a local 2.x recipe: MN_ENV=undeployed deploy:v2, verify:deployment:v2 and the MN_EXTERNAL_STACK=1 round-trip suite, with the printed <ENV>_ADDRESS, the per-env deployment record and the optional undeployed maintenance key. It also states that deploy:v2 always deploys a NEW contract, so resuming is the caller's job, as on the 1.x lane. UNDEPLOYED_PROTOCOL joins the runtime knob table (build-time env or window.SHIELDED_NIGHT, midnight-1.x default, invalid values reported on the page with Connect disabled), and the frontend README reconciles with it; the stale runtime-override link is repointed at its renamed heading. Every documented `bun --cwd frontend ...` becomes `bun --cwd=frontend ...`: bun 1.4 prints its usage and exits 0 for the space form, so those commands silently did nothing. Both READMEs and TESTING.md now say the lockfiles need bun >= 1.4. .env.example gains a commented block for the v2 undeployed lane.
…ght-undeployed-2x-lane
This was referenced Sep 9, 2026
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.
What
undeployedbecomes protocol-selectable instead of hard-wired to the Midnight 1.x (ledger-v8) lane, in the three places that decide it. Nothing changes by default.frontend/)networks.tsdeclaresundeployedasmidnight-1.x; the hook loads the v1 adapter for itUNDEPLOYED_PROTOCOL∈midnight-1.x(default) |midnight-2.x, read like the addresses: build-timeimport.meta.env.UNDEPLOYED_PROTOCOL(envPrefixUNDEPLOYED_already covered it), overridable at runtime bywindow.SHIELDED_NIGHT.UNDEPLOYED_PROTOCOLfrom/config.js(runtime wins). Label shows the generation (Local (undeployed · 2.x)). An invalid value is a visible error that disables Connect — never a silent fallback. A protocol-mismatch hint is logged when a connect fails after the adapter is reached.contracts/v2/scripts/)deploy.ts/verify-deployment.tsrefuse everyMN_ENVbutstagenet; onestagenet()profile; maintenance-key record typednetwork: 'stagenet'on durable storageMN_ENV∈stagenet(default) |undeployed;undeployed()profile (MN_NODE_URL/MN_NODE_WS_URL/MN_INDEXER_URL/MN_INDEXER_WS_URL/MN_PROOF_SERVER_URLwith the 1.x lane's loopback defaults, genesis-seed fallback with a warning); maintenance key optional and ephemeral onundeployed(default.local/private-state/v2-undeployed/maintenance-key.json, still 0600 + read-back, recordnetwork: 'undeployed', a key from another network is refused in both directions); per-env private-state store (shielded-night-v2-<env>) and record (v2-<env>-<addr>.json); prints<ENV>_ADDRESS=.contracts/v2/test/)test/external/: a 2.x counterpart of the 1.xMN_EXTERNAL_STACK=1round-trip suite — 11-circuit verifier-key check + metadata + authority + offline wrapper-colour derivation, two-step and atomic NIGHT ⇄ sNight with exact balance asserts, wrong-secret negative. Opt-in (bun run test:v2:external, refuses withoutMN_EXTERNAL_STACK, never starts or stops a stack).bun run test:v2now runs throughvitest.config.tsand collects zero external tests.Docs: README gains a "Which lane runs where" table, the local 2.x recipe and the switch; TESTING.md the external suite; every
bun --cwd frontend …is nowbun --cwd=frontend …(the space form is a silent no-op on bun ≥ 1.4, which the committed lockfiles require).Why
midnight-2-offers runs a local
undeployedledger-9 devnet. Since #13,mainis multi-network but its 2.x half is stagenet-only, so the demo had to stay pinned to theledger-v9branch (infra 00017, Q15). This PR is option A of that question: the missing work upstream, so one branch and one page build serve every network and the demo can re-pin tomain.Not breaking
No default changes:
undeployedis stillmidnight-1.xon the page,MN_ENVstill defaults tostagenetin the v2 scripts, the durable maintenance key is still mandatory onstagenetand its error text is unchanged. The Stagenet deployment052051d3…, its keys,frontend/.env, bothmanaged/trees and the public site are untouched (git diff 6c4b03d..HEAD -- src/managed contracts/v2/managed frontend/.envis empty).Evidence (live, 2026-09-09, midnight-2-offers core stack — node 2.0.0-rc.4 / indexer 4.4.0-rc.3 / proof-server 9.0.0-rc.5)
MN_ENV=undeployed … bun run deploy:v2→UNDEPLOYED_ADDRESS=0f334f634b4ddf8bbeb6abefc03f19f87f98b9c1add6f69cb2cb55dbb135faf6, verified 11/11,locked=false, key at the default path (0600,network: "undeployed");verify:deployment:v2→code and metadata match. Negatives fail before any network call:MN_ENV=stagenetwithout a key file → unchangedSet MN_MAINTENANCE_KEY_FILE…;MN_ENV=preview→Invalid MN_ENV "preview". The v2 commands support stagenet | undeployed.MN_EXTERNAL_STACK=1 MN_ENV=undeployed … bun run test:v2:external→ 4/4 passed joining that contract (598 s) and 4/4 deploying fresh inside the suite (548 s). The on-chaintokenColor()equalsrawTokenType(pad32("shielded-night:wrapper"), address).UNDEPLOYED_PROTOCOL=midnight-2.xbuild and a stock build served with a rewritten/config.js:midnight-2.x→Local (undeployed · 2.x);ledger9→UNDEPLOYED_PROTOCOL must be "midnight-1.x" or "midnight-2.x"; got "ledger9".with Connect disabled; no key → build-time value → 1.x default on a stock build; runtimemidnight-1.xbeats a 2.x build. The stock build + runtime key path is what the demo needs. The wallet-driven round trip in the browser needs a ledger-v9 wallet (Moth, feat: ledger v9 support via a dual ledger/SDK seam shieldedtech/moth-wallet#30) and was not performed here — the Node round trips above ran against the same contract.SHIELDED_NIGHTmarker present).Note for reviewers: a re-run of
deploy:v2deploys a second contract and reuses the key file — there is no resume path, same as the 1.x lane and the stagenet lane today; resumability stays with the caller (the demo image's JOIN oncontract.json).Downstream (midnight-2-offers, infra 00017 PR-D follow-up)
After merge: re-pin
SHIELDED_NIGHT_REFfrom30af63f(branchledger-v9) to this merge commit; setUNDEPLOYED_PROTOCOL: "midnight-2.x"in the injected/config.jsnext toUNDEPLOYED_ADDRESS; run the v2 one-shots withMN_ENV=undeployed(npm --prefix contracts/v2 run deploy/verify:deployment,DEPLOY_OUThonoured). Config changes only, no patch.