Skip to content

Make the dApp deployable into a stack it did not start - #9

Open
acedward wants to merge 3 commits into
mainfrom
00007-stack-integration
Open

Make the dApp deployable into a stack it did not start#9
acedward wants to merge 3 commits into
mainfrom
00007-stack-integration

Conversation

@acedward

@acedward acedward commented Sep 2, 2026

Copy link
Copy Markdown
Contributor

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.

Knob Where What it does
window.SHIELDED_NIGHT = { UNDEPLOYED_ADDRESS: "…" } the SPA — overwrite the built dist/config.js, which index.html now loads before the bundle overrides the built-in contract address at RUNTIME, per network
MN_INDEXER_URL, MN_INDEXER_WS_URL, MN_NODE_URL, MN_PROOF_SERVER_URL networkFor() → deploy / deploy-and-lock / lock / verify-deployment + the integration suite dial a stack that is not on 127.0.0.1 (compose service hostnames, non-default ports)
MN_EXTERNAL_STACK=1 test/integration/global-setup.ts run the suite against an already-running stack instead of booting one with testcontainers
DEPLOY_OUT=<path> scripts/deploy.ts, scripts/deploy-and-lock.ts also write the deploy as JSON, published atomically

Plus 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:

  1. The contract address is build-time only (UNDEPLOYED_ADDRESS via Vite's envPrefix), but a stack only learns its address after its own deploy. contractAddressFor() now resolves per call from window.SHIELDED_NIGHT.<NETWORK>_ADDRESS, falling back to the build-time value; blank counts as absent, so a bundle built with an empty UNDEPLOYED_ADDRESS gains "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.html now loads /config.js as a classic script (it runs before the deferred module bundle) and public/config.js ships a no-op placeholder, so every deployment serves a real file. Without the placeholder a static host would answer /config.js with 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.
  2. The undeployed URLs were hard-coded to 127.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 only MN_PROOF_SERVER_URL applies, deliberately: the indexer/node URLs identify the network itself, and silently repointing preview because 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.)
  3. The integration suite always started its own stack, so a packaging of this dApp could only be gated by a transcribed driver. MN_EXTERNAL_STACK=1 lets 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_OUT is 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). commit comes from SHIELDED_NIGHT_COMMIT (an image built from a pinned SHA has no .git), else git rev-parse HEAD, else null.

The CI repair (commit 2)

CI has been red on main since bun 1.4.0 shipped — every job that installs the ROOT dependencies dies at bun install --frozen-lockfile with "overrides in package.json changed since bun.lock was saved". overrides carried 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 //overrides key and the stale line leaves the lockfile.

Measured with the official images against a pristine main: bun install --frozen-lockfile fails on oven/bun:1.4.0 and passes on 1.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:unit78 passed (23 new; was 55). The new unit files pin: build-time fallback, injected-wins, blank/trim handling, per-network isolation; all four overrides on undeployed and proof-server-only on hosted; DEPLOY_OUT unset ⇒ 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/ carries config.js and index.html references it.
  • Minification marker measured in dist/assets/index-*.js: …(t=typeof window>"u"?void 0:window)=>{const e=t?.SHIELDED_NIGHT;…}, so a downstream image's guard is grep -q SHIELDED_NIGHT dist/assets/*.js.
  • bun run compact (full ZK, compactc 0.31.1) reproduced src/managed/ byte-for-byte locally, as CI's rebuild job also asserts.
  • The whole point, exercised end to end: a devnet was brought up by hand on random ports (node 57085 / indexer 57165 / proof 57086 — none of the defaults) and MN_EXTERNAL_STACK=1 MN_ENV=undeployed MN_INDEXER_URL=… MN_INDEXER_WS_URL=… MN_NODE_URL=… MN_PROOF_SERVER_URL=… bun run smoke passed 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.ts wrote the record (parent directory created, commit resolved from git), and verify-deployment.ts reached the overridden indexer and matched 11/11 verifier keys byte-for-byte on chain.
  • Pointed at dead ports, the suite fails in global setup naming all three unreachable URLs, and starts nothing.

Non-breaking

Every knob is opt-in and unset means today's behaviour. frontend/.env is untouched, no dependency changed (the only lockfile edit is the stale comment line above), and src/shielded-night.compact and src/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").

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.
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