Make the dApp deployable into a stack it did not start - #9
Open
acedward wants to merge 3 commits into
Open
Conversation
Everything here assumes the devnet is on this host's loopback and that a human
pastes a new contract address into frontend/.env. Neither holds for a compose
stack that brings up its OWN chain, deploys this contract once per bring-up and
serves the dApp from an image built long before. Four opt-in knobs cover it;
every one defaults to today's behaviour, so no existing deploy, build or CI run
changes.
* Runtime contract-address override. contractAddressFor() now resolves per call
from window.SHIELDED_NIGHT.<NETWORK>_ADDRESS, falling back to the build-time
import.meta.env value (blank counts as absent). A deployment injects it with a
/config.js served before the module bundle, so one image serves any stack. The
literal SHIELDED_NIGHT survives minification, so a packaging step can grep the
built bundle to prove the lane is present instead of trusting it. New
dependency-free module frontend/src/lib/runtime-config.ts, so the root unit
tier can test it without vite.
* Env-overridable endpoints. networkFor() applies MN_INDEXER_URL,
MN_INDEXER_WS_URL, MN_NODE_URL and MN_PROOF_SERVER_URL on undeployed, and
MN_PROOF_SERVER_URL alone on the hosted envs — a caller inside the same docker
network must dial service hostnames and cannot reach 127.0.0.1 at all, but
repointing preview's indexer from a stray exported variable would be a silent,
expensive bug. deploy / deploy-and-lock / lock / verify-deployment inherit it.
* External-stack mode for the integration suite. MN_EXTERNAL_STACK=1 skips
testcontainers and runs the suite against an already-running stack, so a
packaging of this dApp can be gated by THIS suite rather than a transcribed
driver. The stack is never torn down (we do not stop what we did not start),
and setup preflights the endpoints so a wrong URL fails immediately by name
instead of ten minutes later as a wallet-sync timeout. CI is unchanged and
still self-hosting.
* Deploy record. DEPLOY_OUT=<path> writes {address, networkId, name, symbol,
decimals, deployedAt, commit, locked} atomically, so an automated deployment
reads data instead of scraping stdout. Unset writes nothing.
23 unit tests in the existing tier cover the three resolution rules; no new
dependency, no lockfile change, and the contract and src/managed are untouched.
CI has been red since bun 1.4.0 shipped, on every job that installs the ROOT
dependencies, and on `main` as much as on this branch:
error: lockfile had changes, but lockfile is frozen
note: overrides in package.json changed since bun.lock was saved
`overrides` carried its explanation as a `"//"` key. Bun no longer counts that
key as an override, so the parsed override set no longer matches the one saved
in bun.lock and every frozen install fails. frontend/, whose overrides have no
such key, installs fine on 1.4.0 — which is what isolates the cause.
Move the note to a top-level `//overrides` key (ignored by npm and bun alike)
and drop the stale entry from the lockfile. The lockfile diff is exactly that
one line: no package resolution changes.
Measured with the official images against a pristine checkout: `bun install
--frozen-lockfile` fails on 1.4.0 before this change and passes after it on
1.2.23, 1.3.11 and 1.4.0, leaving both lockfiles byte-identical.
The runtime address override needs a script that runs before the module bundle, and index.html carried no such tag: a stack-hosted deployment would have had to edit built output to add one — the kind of patch this change set exists to avoid. Adding the tag alone would break the other deployments, where /config.js does not exist: a static host answers with a 404 or an HTML SPA fallback, and the browser refuses to execute it, so every page load logs an error. So ship the file too. public/config.js is a no-op — it only ensures window.SHIELDED_NIGHT exists — and vite copies it to dist/config.js, which index.html loads as a classic script (deferred modules run after it). Every deployment serves a real file; a stack-hosted one overwrites that single file at container start with the address it just deployed and touches nothing else. Verified: dist/ carries config.js, dist/index.html references it, and the minification marker is still in the bundle.
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
Four opt-in integration knobs so this repo can be consumed by a compose stack that brings up its own chain, deploys the contract once per bring-up, and serves the dApp from an image built long before. Every knob defaults to today's behaviour.
window.SHIELDED_NIGHT = { UNDEPLOYED_ADDRESS: "…" }dist/config.js, whichindex.htmlnow loads before the bundleMN_INDEXER_URL,MN_INDEXER_WS_URL,MN_NODE_URL,MN_PROOF_SERVER_URLnetworkFor()→ deploy / deploy-and-lock / lock / verify-deployment + the integration suite127.0.0.1(compose service hostnames, non-default ports)MN_EXTERNAL_STACK=1test/integration/global-setup.tsDEPLOY_OUT=<path>scripts/deploy.ts,scripts/deploy-and-lock.tsPlus one unrelated repair that CI needed either way (second commit, see below).
Why
The three gaps are exactly what stops a stack-hosted deployment today:
UNDEPLOYED_ADDRESSvia Vite'senvPrefix), but a stack only learns its address after its own deploy.contractAddressFor()now resolves per call fromwindow.SHIELDED_NIGHT.<NETWORK>_ADDRESS, falling back to the build-time value; blank counts as absent, so a bundle built with an emptyUNDEPLOYED_ADDRESSgains "Local (undeployed)" the moment one is injected. Only ADDRESSES are injectable — the wallet still supplies the indexer/node/proof URLs, so there is no URL lane to get wrong.index.htmlnow loads/config.jsas a classic script (it runs before the deferred module bundle) andpublic/config.jsships a no-op placeholder, so every deployment serves a real file. Without the placeholder a static host would answer/config.jswith a 404 or an HTML SPA fallback the browser refuses to execute, and a stack-hosted image would have had to edit built output to add the tag — the kind of patch this change set exists to avoid. A deployment now overwrites that one file and touches nothing else.undeployedURLs were hard-coded to127.0.0.1, which a container on the same docker network cannot reach at all. The four overrides fix that for every script through one resolution point. On the HOSTED envs onlyMN_PROOF_SERVER_URLapplies, deliberately: the indexer/node URLs identify the network itself, and silently repointingpreviewbecause a local-stack variable was left exported would be an expensive, invisible bug. (The hosted configs hard-coded a local proof server, so that one is worth overriding.)MN_EXTERNAL_STACK=1lets THIS suite be the gate. It never tears down what it did not start, and it preflights the endpoints so a wrong URL fails immediately, by name, instead of ten minutes later as a wallet-sync timeout.DEPLOY_OUTis the small fourth piece: scraping the address off stdout in a one-shot container is fragile, so the same run can also write{address, networkId, name, symbol, decimals, deployedAt, commit, locked}atomically (temp + rename).commitcomes fromSHIELDED_NIGHT_COMMIT(an image built from a pinned SHA has no.git), elsegit rev-parse HEAD, elsenull.The CI repair (commit 2)
CI has been red on
mainsince bun 1.4.0 shipped — every job that installs the ROOT dependencies dies atbun install --frozen-lockfilewith "overrides in package.json changed since bun.lock was saved".overridescarried its explanation as a"//"key, which bun no longer counts as an override, so the saved lockfile no longer matches.frontend/, whose overrides have no such key, installs fine on 1.4.0 — which isolates the cause. The note moves to a top-level//overrideskey and the stale line leaves the lockfile.Measured with the official images against a pristine
main:bun install --frozen-lockfilefails onoven/bun:1.4.0and passes on1.3.11; after the fix it passes on 1.2.23, 1.3.11 and 1.4.0, leaving both lockfiles byte-identical. The lockfile diff is that one line — no package resolution changes.How it was verified
bun run compact:fast+bun run typecheck+bun run test:unit→ 78 passed (23 new; was 55). The new unit files pin: build-time fallback, injected-wins, blank/trim handling, per-network isolation; all four overrides onundeployedand proof-server-only on hosted;DEPLOY_OUTunset ⇒ nothing written, set ⇒ one complete file and no temp left behind.frontend:bun install --frozen-lockfile(root AND frontend, as CI does),bun run typecheck,bun run build— green;dist/carriesconfig.jsandindex.htmlreferences it.dist/assets/index-*.js:…(t=typeof window>"u"?void 0:window)=>{const e=t?.SHIELDED_NIGHT;…}, so a downstream image's guard isgrep -q SHIELDED_NIGHT dist/assets/*.js.bun run compact(full ZK, compactc 0.31.1) reproducedsrc/managed/byte-for-byte locally, as CI's rebuild job also asserts.MN_EXTERNAL_STACK=1 MN_ENV=undeployed MN_INDEXER_URL=… MN_INDEXER_WS_URL=… MN_NODE_URL=… MN_PROOF_SERVER_URL=… bun run smokepassed 4/4 smoke tests (deploy + metadata, the full two-step round trip, two-user independence, and the atomic single-transaction convert pair) without starting a container, and left the stack running on teardown. On that same stack,DEPLOY_OUT=…/contract.json bun run scripts/deploy.tswrote the record (parent directory created,commitresolved from git), andverify-deployment.tsreached the overridden indexer and matched 11/11 verifier keys byte-for-byte on chain.Non-breaking
Every knob is opt-in and unset means today's behaviour.
frontend/.envis untouched, no dependency changed (the only lockfile edit is the stale comment line above), andsrc/shielded-night.compactandsrc/managed/are not touched at all, so the byte-exact-rebuild claim is unaffected.Docs
README.md("Deploying into a stack you already have"),frontend/README.md("Runtime address override"),TESTING.md(env-var table + "Running against a stack you already have").