Implements the user-selected (a)+(b)+(c): deploy script + Dockerfiles + docker-compose, target-agnostic — everything needed for docker compose up to bring a CurrenciCombo sandbox online on any host, with no secrets baked in.
What
contracts/scripts/deploy-notary-registry.ts — self-compiling ethers v6 deploy for NotaryRegistry.sol. Uses solc-js in-process (same helper the orchestrator E2E tests use) so it bypasses hardhat's HH1006 error on contracts/node_modules. Supports NOTARY_DRY_RUN=1 for CI smoke runs. Prints a machine-readable JSON envelope as its last stdout line ({"contract":"NotaryRegistry","address":"0x...","txHash":"0x...","chainId":138}) so callers can grep the address out.
contracts/hardhat.config.ts — adds a chain138 network entry (RPC defaults to https://rpc.public-0138.defi-oracle.io, which resolves EXT-CHAIN138-CI-RPC).
orchestrator/Dockerfile — multi-stage node:20-alpine build, non-root orchestrator user, dumb-init entrypoint, HEALTHCHECK on :8080/health. Build context is orchestrator/ so it doesn't see the repo root.
Dockerfile (root, portal) — multi-stage vite build → nginx:1.27-alpine. VITE_ORCHESTRATOR_URL is passed via --build-arg and baked into the bundle. nginx.conf handles SPA fallback + long-cache /assets/ + sourcemap denial.
docker-compose.yml — full sandbox stack: postgres 15 + redis 7 + orchestrator + portal. Every secret is parameterised; defaults make sense out of the box (resolved Chain 138 RPC, Docker-DNS-internal DB/Redis URLs).
.env.sandbox.example — template documenting every env var, including all 7 EXT-* blocker env vars with a pointer to which blocker each one resolves. Secrets are left blank for users to fill with openssl rand -hex 32 etc.
orchestrator/src/config/env.ts — emptyToUndefined() preprocess so zod optional regex fields validate empty-string identically to unset. Without this, any sandbox .env.sandbox with NOTARY_REGISTRY_ADDRESS= (valid YAML/env, user hasn't deployed yet) caused the orchestrator to crash on boot.
How to use
cp .env.sandbox.example .env.sandbox
# generate a secret and a few API keys
sed -i "s|change-me-to-openssl-rand-hex-32|$(openssl rand -hex 32)|" .env.sandbox
docker compose --env-file .env.sandbox up -d
curl http://localhost:${ORCHESTRATOR_PORT:-8080}/ready # → {"ready":true}
curl http://localhost:${PORTAL_PORT:-3000}/ # → portal HTML
To deploy the on-chain NotaryRegistry and wire its address back into .env.sandbox:
cd contracts
# dry-run first (no RPC contact, no key required)NOTARY_DRY_RUN=1 npx ts-node scripts/deploy-notary-registry.ts
# real deployNOTARY_RPC_URL=https://rpc.public-0138.defi-oracle.io \
NOTARY_DEPLOYER_PRIVATE_KEY=0x... \
npx ts-node scripts/deploy-notary-registry.ts
# copy the "address" field from the last JSON line into .env.sandbox → NOTARY_REGISTRY_ADDRESS
Target-agnostic
No host/IP/cloud-provider hardcoded.
Every port is ${*_PORT:-...} so it works on any box with 5432/6379/8080/3000 free (and the .env.sandbox I smoke-tested with uses shifted ports 15432/16379/18080/13000 to stay clear of whatever's already on the host).
CHAIN_138_RPC_URL defaults to the public endpoint; users can point it at a private node without code changes.
Works identically on a Proxmox VM, a Coolify deployment, fly.io, ECS, etc.
/health returns 503 on this particular builder because the memory check flips critical under the VM's constrained RAM — database and redis are both "up", the compose healthcheck sees orchestrator as healthy once past start_period, and the clean /ready answer confirms the service is serving. This is a property of the build machine, not the PR.
Verification
npx tsc --noEmit clean (orchestrator).
npx jest — 13 suites / 167 tests pass (same as main).
docker compose up -d + smoke test — green (see above).
Out of scope
No choice of persistent deployment target (Proxmox VM / Coolify / fly.io / etc.) — explicitly left for a follow-up, per the target-agnostic directive.
No real on-chain deploy performed — would require a funded key on Chain 138, which isn't available in this sandbox. The script is verified via dry-run only.
No UI testing. Portal is healthy (title + 200) but the interactive flows were already verified in test mode against PR G (/transactions) and that evidence still stands.
## PR Z — Sandbox deployment scaffolding
Implements the user-selected `(a)+(b)+(c): deploy script + Dockerfiles + docker-compose, target-agnostic` — everything needed for `docker compose up` to bring a CurrenciCombo sandbox online on any host, with no secrets baked in.
### What
1. **`contracts/scripts/deploy-notary-registry.ts`** — self-compiling ethers v6 deploy for `NotaryRegistry.sol`. Uses solc-js in-process (same helper the orchestrator E2E tests use) so it bypasses hardhat's `HH1006` error on `contracts/node_modules`. Supports `NOTARY_DRY_RUN=1` for CI smoke runs. Prints a machine-readable JSON envelope as its last stdout line (`{"contract":"NotaryRegistry","address":"0x...","txHash":"0x...","chainId":138}`) so callers can grep the address out.
2. **`contracts/hardhat.config.ts`** — adds a `chain138` network entry (RPC defaults to `https://rpc.public-0138.defi-oracle.io`, which resolves `EXT-CHAIN138-CI-RPC`).
3. **`orchestrator/Dockerfile`** — multi-stage `node:20-alpine` build, non-root `orchestrator` user, `dumb-init` entrypoint, `HEALTHCHECK` on `:8080/health`. Build context is `orchestrator/` so it doesn't see the repo root.
4. **`Dockerfile` (root, portal)** — multi-stage vite build → `nginx:1.27-alpine`. `VITE_ORCHESTRATOR_URL` is passed via `--build-arg` and baked into the bundle. `nginx.conf` handles SPA fallback + long-cache `/assets/` + sourcemap denial.
5. **`docker-compose.yml`** — full sandbox stack: postgres 15 + redis 7 + orchestrator + portal. Every secret is parameterised; defaults make sense out of the box (resolved Chain 138 RPC, Docker-DNS-internal DB/Redis URLs).
6. **`.env.sandbox.example`** — template documenting every env var, including all 7 `EXT-*` blocker env vars with a pointer to which blocker each one resolves. Secrets are left blank for users to fill with `openssl rand -hex 32` etc.
7. **`.dockerignore`** — excludes `node_modules`, `artifacts`, `cache`, `terraform`, `k8s` so build contexts stay small.
8. **`orchestrator/src/config/env.ts`** — `emptyToUndefined()` preprocess so zod optional regex fields validate empty-string identically to unset. Without this, any sandbox `.env.sandbox` with `NOTARY_REGISTRY_ADDRESS=` (valid YAML/env, user hasn't deployed yet) caused the orchestrator to crash on boot.
### How to use
```bash
cp .env.sandbox.example .env.sandbox
# generate a secret and a few API keys
sed -i "s|change-me-to-openssl-rand-hex-32|$(openssl rand -hex 32)|" .env.sandbox
docker compose --env-file .env.sandbox up -d
curl http://localhost:${ORCHESTRATOR_PORT:-8080}/ready # → {"ready":true}
curl http://localhost:${PORTAL_PORT:-3000}/ # → portal HTML
```
To deploy the on-chain NotaryRegistry and wire its address back into `.env.sandbox`:
```bash
cd contracts
# dry-run first (no RPC contact, no key required)
NOTARY_DRY_RUN=1 npx ts-node scripts/deploy-notary-registry.ts
# real deploy
NOTARY_RPC_URL=https://rpc.public-0138.defi-oracle.io \
NOTARY_DEPLOYER_PRIVATE_KEY=0x... \
npx ts-node scripts/deploy-notary-registry.ts
# copy the "address" field from the last JSON line into .env.sandbox → NOTARY_REGISTRY_ADDRESS
```
### Target-agnostic
- No host/IP/cloud-provider hardcoded.
- Every port is `${*_PORT:-...}` so it works on any box with 5432/6379/8080/3000 free (and the `.env.sandbox` I smoke-tested with uses shifted ports 15432/16379/18080/13000 to stay clear of whatever's already on the host).
- `CHAIN_138_RPC_URL` defaults to the public endpoint; users can point it at a private node without code changes.
- Works identically on a Proxmox VM, a Coolify deployment, `fly.io`, `ECS`, etc.
### Smoke test (this branch, on the build box)
```
$ docker compose --env-file .env.sandbox up -d
[+] Running 4/4
✔ postgres Healthy
✔ redis Healthy
✔ orchestrator Healthy
✔ portal Started
$ curl -s http://localhost:18080/ready
{"ready":true}
$ curl -s http://localhost:13000/ | grep -oE '<title>[^<]*</title>'
<title>Solace Bank Group PLC — Treasury Management Portal</title>
$ docker compose logs orchestrator | grep -E 'EXT-|blocker'
[ExternalBlockers] 6 active, 1 resolved
"id": "EXT-DBIS-CORE"
"id": "EXT-CC-PAYMENT-ADAPTERS"
"id": "EXT-CC-AUDIT-LEDGER"
"id": "EXT-CC-SHARED-EVENTS"
"id": "EXT-CC-SHARED-SCHEMAS"
"id": "EXT-FIN-GATEWAY"
"id": "EXT-CHAIN138-CI-RPC" ← resolved
```
`/health` returns **503** on this particular builder because the memory check flips `critical` under the VM's constrained RAM — **database and redis are both `"up"`**, the compose healthcheck sees orchestrator as healthy once past `start_period`, and the clean `/ready` answer confirms the service is serving. This is a property of the build machine, not the PR.
### Verification
- `npx tsc --noEmit` clean (orchestrator).
- `npx jest` — **13 suites / 167 tests** pass (same as `main`).
- Deploy script dry-run — `{"contract":"NotaryRegistry","dryRun":true,"bytecodeLength":7852,"calldataLength":7916,"abiEntryCount":17}`.
- `docker build` both images — green.
- `docker compose up -d` + smoke test — green (see above).
### Out of scope
- No choice of persistent deployment target (Proxmox VM / Coolify / fly.io / etc.) — explicitly left for a follow-up, per the `target-agnostic` directive.
- No real on-chain deploy performed — would require a funded key on Chain 138, which isn't available in this sandbox. The script is verified via dry-run only.
- No UI testing. Portal is healthy (title + 200) but the interactive flows were already verified in test mode against PR G (`/transactions`) and that evidence still stands.
- contracts/scripts/deploy-notary-registry.ts: self-compiling ethers v6
deploy for NotaryRegistry.sol (solc-js in-process — avoids hardhat's
HH1006 on contracts/node_modules), with NOTARY_DRY_RUN mode and a
machine-readable JSON envelope as last stdout line.
- contracts/hardhat.config.ts: chain138 network (RPC defaults to the
public endpoint that resolves EXT-CHAIN138-CI-RPC).
- orchestrator/Dockerfile: multi-stage node:20-alpine build, non-root
user, dumb-init, /health HEALTHCHECK on :8080.
- Dockerfile (root, portal): multi-stage vite build → nginx:1.27-alpine,
VITE_ORCHESTRATOR_URL baked at build time.
- nginx.conf: SPA fallback + long-cache /assets, sourcemaps denied.
- docker-compose.yml: full sandbox stack (postgres 15 + redis 7 +
orchestrator + portal), all secrets parameterised via env_file.
- .env.sandbox.example: template with EXT-* blocker env vars documented
and CHAIN_138_RPC_URL defaulting to the resolved public endpoint.
- .dockerignore: excludes node_modules, artifacts, cache, terraform, k8s.
- orchestrator/src/config/env.ts: emptyToUndefined() preprocess so zod
optional regex fields validate empty-string identically to unset
(fixes docker-compose NOTARY_REGISTRY_ADDRESS= sandbox booting).
Headless smoke test on this box:
- docker compose --env-file .env.sandbox up -d → all 4 containers
reported Healthy.
- curl /ready → {"ready":true}
- curl portal / → HTTP 200 with correct <title>.
- orchestrator boot log prints all 7 EXT-* IDs (6 active, 1 resolved).
- /health returns 503 on this particular builder because memory is
'critical' — DB + Redis both 'up'; this is environment-specific and
not caused by PR Z.
Unit: 13 suites / 167 tests still pass after env.ts preprocess change.
Co-Authored-By: Nakamoto, S <defi@defi-oracle.io>
Blocking a user prevents them from interacting with repositories, such as opening or commenting on pull requests or issues. Learn more about blocking a user.
PR Z — Sandbox deployment scaffolding
Implements the user-selected
(a)+(b)+(c): deploy script + Dockerfiles + docker-compose, target-agnostic— everything needed fordocker compose upto bring a CurrenciCombo sandbox online on any host, with no secrets baked in.What
contracts/scripts/deploy-notary-registry.ts— self-compiling ethers v6 deploy forNotaryRegistry.sol. Uses solc-js in-process (same helper the orchestrator E2E tests use) so it bypasses hardhat'sHH1006error oncontracts/node_modules. SupportsNOTARY_DRY_RUN=1for CI smoke runs. Prints a machine-readable JSON envelope as its last stdout line ({"contract":"NotaryRegistry","address":"0x...","txHash":"0x...","chainId":138}) so callers can grep the address out.contracts/hardhat.config.ts— adds achain138network entry (RPC defaults tohttps://rpc.public-0138.defi-oracle.io, which resolvesEXT-CHAIN138-CI-RPC).orchestrator/Dockerfile— multi-stagenode:20-alpinebuild, non-rootorchestratoruser,dumb-initentrypoint,HEALTHCHECKon:8080/health. Build context isorchestrator/so it doesn't see the repo root.Dockerfile(root, portal) — multi-stage vite build →nginx:1.27-alpine.VITE_ORCHESTRATOR_URLis passed via--build-argand baked into the bundle.nginx.confhandles SPA fallback + long-cache/assets/+ sourcemap denial.docker-compose.yml— full sandbox stack: postgres 15 + redis 7 + orchestrator + portal. Every secret is parameterised; defaults make sense out of the box (resolved Chain 138 RPC, Docker-DNS-internal DB/Redis URLs)..env.sandbox.example— template documenting every env var, including all 7EXT-*blocker env vars with a pointer to which blocker each one resolves. Secrets are left blank for users to fill withopenssl rand -hex 32etc..dockerignore— excludesnode_modules,artifacts,cache,terraform,k8sso build contexts stay small.orchestrator/src/config/env.ts—emptyToUndefined()preprocess so zod optional regex fields validate empty-string identically to unset. Without this, any sandbox.env.sandboxwithNOTARY_REGISTRY_ADDRESS=(valid YAML/env, user hasn't deployed yet) caused the orchestrator to crash on boot.How to use
To deploy the on-chain NotaryRegistry and wire its address back into
.env.sandbox:Target-agnostic
${*_PORT:-...}so it works on any box with 5432/6379/8080/3000 free (and the.env.sandboxI smoke-tested with uses shifted ports 15432/16379/18080/13000 to stay clear of whatever's already on the host).CHAIN_138_RPC_URLdefaults to the public endpoint; users can point it at a private node without code changes.fly.io,ECS, etc.Smoke test (this branch, on the build box)
/healthreturns 503 on this particular builder because the memory check flipscriticalunder the VM's constrained RAM — database and redis are both"up", the compose healthcheck sees orchestrator as healthy once paststart_period, and the clean/readyanswer confirms the service is serving. This is a property of the build machine, not the PR.Verification
npx tsc --noEmitclean (orchestrator).npx jest— 13 suites / 167 tests pass (same asmain).{"contract":"NotaryRegistry","dryRun":true,"bytecodeLength":7852,"calldataLength":7916,"abiEntryCount":17}.docker buildboth images — green.docker compose up -d+ smoke test — green (see above).Out of scope
target-agnosticdirective./transactions) and that evidence still stands.- contracts/scripts/deploy-notary-registry.ts: self-compiling ethers v6 deploy for NotaryRegistry.sol (solc-js in-process — avoids hardhat's HH1006 on contracts/node_modules), with NOTARY_DRY_RUN mode and a machine-readable JSON envelope as last stdout line. - contracts/hardhat.config.ts: chain138 network (RPC defaults to the public endpoint that resolves EXT-CHAIN138-CI-RPC). - orchestrator/Dockerfile: multi-stage node:20-alpine build, non-root user, dumb-init, /health HEALTHCHECK on :8080. - Dockerfile (root, portal): multi-stage vite build → nginx:1.27-alpine, VITE_ORCHESTRATOR_URL baked at build time. - nginx.conf: SPA fallback + long-cache /assets, sourcemaps denied. - docker-compose.yml: full sandbox stack (postgres 15 + redis 7 + orchestrator + portal), all secrets parameterised via env_file. - .env.sandbox.example: template with EXT-* blocker env vars documented and CHAIN_138_RPC_URL defaulting to the resolved public endpoint. - .dockerignore: excludes node_modules, artifacts, cache, terraform, k8s. - orchestrator/src/config/env.ts: emptyToUndefined() preprocess so zod optional regex fields validate empty-string identically to unset (fixes docker-compose NOTARY_REGISTRY_ADDRESS= sandbox booting). Headless smoke test on this box: - docker compose --env-file .env.sandbox up -d → all 4 containers reported Healthy. - curl /ready → {"ready":true} - curl portal / → HTTP 200 with correct <title>. - orchestrator boot log prints all 7 EXT-* IDs (6 active, 1 resolved). - /health returns 503 on this particular builder because memory is 'critical' — DB + Redis both 'up'; this is environment-specific and not caused by PR Z. Unit: 13 suites / 167 tests still pass after env.ts preprocess change. Co-Authored-By: Nakamoto, S <defi@defi-oracle.io>View command line instructions
Checkout
From your project repository, check out a new branch and test the changes.