From 4eba7d77dbd23ea66cf9d11059e6fc698178592f Mon Sep 17 00:00:00 2001 From: Cursor Agent Date: Mon, 10 Aug 2026 16:53:30 +0000 Subject: [PATCH 1/8] Soft-migrate LocalNet CLI toward canton-dev-tools (ENG-1635) Delegate bin/canton-localnet to @fairmint/canton-dev-tools when installed, keep deprecated SDK scripts as fallback, and document pin ownership move. Co-authored-by: HardlyDifficult --- .cursor/skills/localnet-testing/SKILL.md | 9 ++- AGENTS.md | 31 +++++++-- README.md | 20 +++++- bin/canton-localnet | 81 ++++++++++++++++++++++- package.json | 12 ++++ scripts/check-package-artifacts.ts | 4 ++ scripts/localnet-cloud.sh | 4 ++ test/unit/scripts/canton-localnet.test.ts | 60 +++++++++++++++++ 8 files changed, 209 insertions(+), 12 deletions(-) diff --git a/.cursor/skills/localnet-testing/SKILL.md b/.cursor/skills/localnet-testing/SKILL.md index ef9f5516..dd6c590f 100644 --- a/.cursor/skills/localnet-testing/SKILL.md +++ b/.cursor/skills/localnet-testing/SKILL.md @@ -1,5 +1,10 @@ # LocalNet testing Read the public [LocalNet guide](https://github.com/Fairmint/canton-node-sdk/wiki/LocalNet-testing) -first. This repository's current `package.json`, `bin/canton-localnet`, and integration tests are -the source of truth for `localnet:start`, `localnet:stop`, `test:integration`, and related commands. +first. + +**ENG-1635:** `@fairmint/canton-dev-tools` owns shared LocalNet pins going forward. This repository's +`bin/canton-localnet` soft-delegates to that package when installed; otherwise it falls back to the +deprecated `scripts/localnet-cloud.sh`. Prefer Dev Tools commands for pin-sensitive work. Current +`package.json` `localnet:*` scripts, integration tests, and the fallback scripts remain the source of +truth for commands that still run through this repo until the hard cutover. diff --git a/AGENTS.md b/AGENTS.md index 9d1795b5..53a900b6 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -1,21 +1,38 @@ # canton-node-sdk See [CLAUDE.md](CLAUDE.md), [README.md](README.md), and -`.cursor/skills/localnet-testing/SKILL.md`. `package.json` (`localnet:*` scripts) and -`bin/canton-localnet` are the source of truth for LocalNet commands. +`.cursor/skills/localnet-testing/SKILL.md`. + +## LocalNet ownership (ENG-1635) + +**`@fairmint/canton-dev-tools` owns LocalNet pins going forward** (cn-quickstart ref, Splice, +Scribe, protocol version). See that package's `COMPATIBILITY.md`. + +This repository still ships `bin/canton-localnet` and `scripts/localnet-cloud.sh` as a **soft +migration** fallback: + +- `bin/canton-localnet` soft-delegates to `@fairmint/canton-dev-tools` when that package is + installed (optionalDependency / optional peer). +- Set `CANTON_LOCALNET_FORCE_LEGACY=1` to force the deprecated SDK scripts. +- `package.json` `localnet:*` scripts still call `bin/canton-localnet` (which prefers Dev Tools). +- Prefer `npx @fairmint/canton-dev-tools ` or `npm run localnet:dev-tools -- ` + when the optional dependency is present. +- Do **not** treat SDK-local pins in `bin/canton-localnet` as the long-term source of truth. ## Cursor Cloud specific instructions Repo checks (`npm install`, `npm run fix`, `npm test`, `npm run build`) need no dashboard secrets. -`npm install` does not require `NPM_TOKEN` here (dependencies are public). +`npm install` does not require `NPM_TOKEN` here (dependencies are public). The optional +`@fairmint/canton-dev-tools` git dependency needs GitHub network access (public repo). ### Canton LocalNet on a cloud VM LocalNet runs Canton Network Quickstart in Docker. `npm run localnet:start` (= -`bin/canton-localnet start`, infra-only + OAuth2 by default) is self-provisioning on the cloud image: -it `apt`-installs Docker, starts a `dockerd` (vfs storage driver, iptables-legacy) via passwordless -`sudo`, adds `scan.localhost`/`sv.localhost`/`wallet.localhost` to `/etc/hosts`, runs cn-quickstart -`make setup`, brings up the compose stack, and waits for the Validator, Scan, and Ledger JSON APIs. +`bin/canton-localnet start`, which prefers `@fairmint/canton-dev-tools`, infra-only + OAuth2 by +default) is self-provisioning on the cloud image: it `apt`-installs Docker, starts a `dockerd` +(vfs storage driver, iptables-legacy) via passwordless `sudo`, adds +`scan.localhost`/`sv.localhost`/`wallet.localhost` to `/etc/hosts`, runs cn-quickstart `make setup`, +brings up the compose stack, and waits for the Validator, Scan, and Ledger JSON APIs. Prerequisites (on demand — heavy, not in the dashboard update script): diff --git a/README.md b/README.md index 0d61f856..2a30bc4b 100644 --- a/README.md +++ b/README.md @@ -44,4 +44,22 @@ npm test npm run build ``` -Run `npm run localnet:verify` for the full LocalNet smoke and integration path. +### LocalNet (ENG-1635 soft migration) + +**Pin owner:** [`@fairmint/canton-dev-tools`](https://github.com/Fairmint/canton-dev-tools) (see its +`COMPATIBILITY.md`). This SDK still ships `bin/canton-localnet` / `scripts/localnet-cloud.sh` as a +temporary fallback. The SDK binary soft-delegates to Dev Tools when that optional dependency is +installed. + +```bash +# Preferred once Dev Tools is available (optionalDependency / optional peer): +npx @fairmint/canton-dev-tools start +npm run localnet:dev-tools -- readiness + +# Existing SDK scripts still work (delegate when possible, else legacy scripts): +npm run localnet:verify +``` + +Until `@fairmint/canton-dev-tools` is published to npm, this repo pins the optional dependency to the +ENG-1635 git SHA. After publish, swap that pin to a semver range. Do not delete +`scripts/localnet-cloud.sh` until the hard-cutover follow-up. diff --git a/bin/canton-localnet b/bin/canton-localnet index eab8f834..6f57a62e 100755 --- a/bin/canton-localnet +++ b/bin/canton-localnet @@ -1,4 +1,6 @@ #!/usr/bin/env bash +# Soft migration (ENG-1635): prefer @fairmint/canton-dev-tools when installed. +# Fallback to the deprecated SDK-owned LocalNet scripts until the hard cutover. set -euo pipefail resolve_script_dir() { @@ -23,13 +25,20 @@ DEFAULT_QUICKSTART_REF="2f4edfc17621a7dfb6d44357050c22f4b3914c89" DEFAULT_SPLICE_VERSION="0.6.8" log() { - printf '[canton-localnet] %s\n' "$*" + printf '[canton-localnet] %s\n' "$*" >&2 } usage() { cat <<'USAGE' Usage: canton-localnet +DEPRECATED (ENG-1635): LocalNet pin ownership is moving to @fairmint/canton-dev-tools. +This binary soft-delegates to that package when available; otherwise it falls back to the +SDK-owned scripts/localnet-cloud.sh (kept temporarily). Prefer: + + npx @fairmint/canton-dev-tools start + npm install -D @fairmint/canton-dev-tools + Commands: setup Prepare LocalNet prerequisites start Start lean LocalNet + Keycloak and wait for ready endpoints @@ -50,6 +59,7 @@ Environment: CANTON_LOCALNET_QUICKSTART_REF cn-quickstart git ref to fetch CANTON_LOCALNET_SPLICE_VERSION Splice image tag (defaults to the SDK's pinned API version) CANTON_LOCALNET_INFRA_ONLY Defaults to true for this package binary + CANTON_LOCALNET_FORCE_LEGACY Set to 1 to skip soft-delegation to canton-dev-tools USAGE } @@ -61,6 +71,50 @@ require_command() { fi } +# Resolve @fairmint/canton-dev-tools CLI without following a PATH entry that points +# back at this same SDK binary (both packages historically expose canton-localnet). +find_dev_tools_bin() { + local dir="${PACKAGE_ROOT}" + local candidate="" + + while true; do + candidate="${dir}/node_modules/@fairmint/canton-dev-tools/bin/canton-dev-tools" + if [[ -x "${candidate}" ]]; then + printf '%s' "${candidate}" + return 0 + fi + if [[ "${dir}" == "/" ]]; then + break + fi + dir="$(dirname "${dir}")" + done + + if [[ -n "${npm_config_local_prefix:-}" ]]; then + candidate="${npm_config_local_prefix}/node_modules/@fairmint/canton-dev-tools/bin/canton-dev-tools" + if [[ -x "${candidate}" ]]; then + printf '%s' "${candidate}" + return 0 + fi + fi + + return 1 +} + +maybe_delegate_to_dev_tools() { + local dev_tools_bin="" + + if [[ "${CANTON_LOCALNET_FORCE_LEGACY:-}" == "1" ]]; then + return 1 + fi + + if ! dev_tools_bin="$(find_dev_tools_bin)"; then + return 1 + fi + + log "Delegating to @fairmint/canton-dev-tools (ENG-1635). Set CANTON_LOCALNET_FORCE_LEGACY=1 to use deprecated SDK LocalNet scripts." + exec "${dev_tools_bin}" "$@" +} + cache_root() { if [[ -n "${CANTON_LOCALNET_CACHE_DIR:-}" ]]; then printf '%s' "${CANTON_LOCALNET_CACHE_DIR}" @@ -177,10 +231,12 @@ ensure_quickstart_checkout() { log "Cached cn-quickstart at ${root}." } -main() { +run_legacy_localnet() { local command="${1:-}" local dir="" + log "Using deprecated SDK LocalNet scripts. Install @fairmint/canton-dev-tools for the shared pin owner (ENG-1635)." + case "${command}" in "" | -h | --help | help) usage @@ -210,4 +266,25 @@ main() { exec bash "${LOCALNET_SCRIPT}" "$@" } +main() { + local command="${1:-}" + + case "${command}" in + "" | -h | --help | help) + # Help should describe both paths; do not require soft-delegation. + usage + if [[ -z "${command}" ]]; then + exit 1 + fi + exit 0 + ;; + esac + + if maybe_delegate_to_dev_tools "$@"; then + return + fi + + run_legacy_localnet "$@" +} + main "$@" diff --git a/package.json b/package.json index a45385e1..d6afc58c 100644 --- a/package.json +++ b/package.json @@ -55,6 +55,7 @@ "localnet:status": "bash ./bin/canton-localnet status", "localnet:stop": "bash ./bin/canton-localnet stop", "localnet:verify": "bash ./bin/canton-localnet verify", + "localnet:dev-tools": "canton-dev-tools", "prepack": "npm run clean && npm run build:core", "prepare-release": "tsx scripts/prepare-release.ts", "prepublishOnly": "npm run prepack", @@ -80,6 +81,17 @@ "ws": "8.21.0", "zod": "4.4.3" }, + "optionalDependencies": { + "@fairmint/canton-dev-tools": "github:Fairmint/canton-dev-tools#c2efb4b" + }, + "peerDependencies": { + "@fairmint/canton-dev-tools": ">=0.1.0" + }, + "peerDependenciesMeta": { + "@fairmint/canton-dev-tools": { + "optional": true + } + }, "devDependencies": { "@types/jest": "30.0.0", "@types/node": "26.1.0", diff --git a/scripts/check-package-artifacts.ts b/scripts/check-package-artifacts.ts index b05ef153..39a2ea2e 100644 --- a/scripts/check-package-artifacts.ts +++ b/scripts/check-package-artifacts.ts @@ -111,6 +111,8 @@ function verifyPackagedLocalnetBinary(): void { env: { ...process.env, CANTON_LOCALNET_CACHE_DIR: join(tempDir, 'cache'), + // Soft-migration: packaged SDK fallback must still work without Dev Tools. + CANTON_LOCALNET_FORCE_LEGACY: '1', HOME: join(tempDir, 'home'), }, }); @@ -121,6 +123,8 @@ function verifyPackagedLocalnetBinary(): void { } function verifyPackagedLocalnetPins(): void { + // Fallback pin defaults remain required until the ENG-1635 hard cutover removes SDK LocalNet scripts. + // @fairmint/canton-dev-tools owns the shared pin set going forward. const localnetBin = readFileSync(join(process.cwd(), 'bin', 'canton-localnet'), 'utf8'); const spliceVersion = readFileSync(join(process.cwd(), 'libs', 'splice', 'VERSION'), 'utf8').trim(); const quickstartRef = spawnSync('git', ['rev-parse', 'HEAD:libs/cn-quickstart'], { encoding: 'utf8' }); diff --git a/scripts/localnet-cloud.sh b/scripts/localnet-cloud.sh index e58336d0..1d70e944 100755 --- a/scripts/localnet-cloud.sh +++ b/scripts/localnet-cloud.sh @@ -1,4 +1,8 @@ #!/usr/bin/env bash +# DEPRECATED (ENG-1635): LocalNet pin ownership is moving to @fairmint/canton-dev-tools. +# This script remains as a temporary fallback for bin/canton-localnet until a later hard cutover. +# Prefer: npx @fairmint/canton-dev-tools +# Do not delete this file in the soft-migration PR. set -euo pipefail SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" diff --git a/test/unit/scripts/canton-localnet.test.ts b/test/unit/scripts/canton-localnet.test.ts index 4ffe6d1f..442c1a5b 100644 --- a/test/unit/scripts/canton-localnet.test.ts +++ b/test/unit/scripts/canton-localnet.test.ts @@ -23,6 +23,8 @@ function runPackagedLocalnetWithVersion(version: string): string { env: { ...process.env, CANTON_LOCALNET_SPLICE_VERSION: '', + // Keep unit coverage on the deprecated SDK fallback path. + CANTON_LOCALNET_FORCE_LEGACY: '1', }, }); } finally { @@ -39,3 +41,61 @@ describe('canton-localnet Splice version selection', (): void => { expect(runPackagedLocalnetWithVersion(' \n\t')).toBe('0.6.8'); }); }); + +describe('canton-localnet soft delegation', (): void => { + it('delegates to @fairmint/canton-dev-tools when that package is installed nearby', (): void => { + const packageRoot = mkdtempSync(resolve(tmpdir(), 'canton-localnet-delegate-')); + const localnetBin = resolve(packageRoot, 'bin/canton-localnet'); + const devToolsBin = resolve(packageRoot, 'node_modules/@fairmint/canton-dev-tools/bin/canton-dev-tools'); + + mkdirSync(resolve(packageRoot, 'bin'), { recursive: true }); + mkdirSync(resolve(packageRoot, 'scripts'), { recursive: true }); + mkdirSync(resolve(devToolsBin, '..'), { recursive: true }); + copyFileSync(resolve(REPO_ROOT, 'bin/canton-localnet'), localnetBin); + chmodSync(localnetBin, 0o755); + writeFileSync(resolve(packageRoot, 'scripts/localnet-cloud.sh'), 'printf "legacy\\n"\n'); + writeFileSync(devToolsBin, '#!/usr/bin/env bash\nprintf "dev-tools:%s\\n" "$*"\n'); + chmodSync(devToolsBin, 0o755); + + try { + const output = execFileSync(localnetBin, ['status'], { + encoding: 'utf8', + env: { + ...process.env, + CANTON_LOCALNET_FORCE_LEGACY: '', + }, + }); + expect(output.trim()).toBe('dev-tools:status'); + } finally { + rmSync(packageRoot, { recursive: true, force: true }); + } + }); + + it('keeps the legacy path when CANTON_LOCALNET_FORCE_LEGACY=1', (): void => { + const packageRoot = mkdtempSync(resolve(tmpdir(), 'canton-localnet-force-legacy-')); + const localnetBin = resolve(packageRoot, 'bin/canton-localnet'); + const devToolsBin = resolve(packageRoot, 'node_modules/@fairmint/canton-dev-tools/bin/canton-dev-tools'); + + mkdirSync(resolve(packageRoot, 'bin'), { recursive: true }); + mkdirSync(resolve(packageRoot, 'scripts'), { recursive: true }); + mkdirSync(resolve(devToolsBin, '..'), { recursive: true }); + copyFileSync(resolve(REPO_ROOT, 'bin/canton-localnet'), localnetBin); + chmodSync(localnetBin, 0o755); + writeFileSync(resolve(packageRoot, 'scripts/localnet-cloud.sh'), 'printf "legacy\\n"\n'); + writeFileSync(devToolsBin, '#!/usr/bin/env bash\nprintf "dev-tools\\n"\n'); + chmodSync(devToolsBin, 0o755); + + try { + const output = execFileSync(localnetBin, ['status'], { + encoding: 'utf8', + env: { + ...process.env, + CANTON_LOCALNET_FORCE_LEGACY: '1', + }, + }); + expect(output.trim()).toBe('legacy'); + } finally { + rmSync(packageRoot, { recursive: true, force: true }); + } + }); +}); From 1c8857292dba8b6399220d7a54ec4b0d137c93a9 Mon Sep 17 00:00:00 2001 From: Cursor Agent Date: Mon, 10 Aug 2026 16:56:58 +0000 Subject: [PATCH 2/8] Format LocalNet migration docs and package.json Co-authored-by: HardlyDifficult --- .cursor/skills/localnet-testing/SKILL.md | 10 +++++----- AGENTS.md | 7 +++---- README.md | 16 ++++++++-------- package.json | 24 ++++++++++++------------ 4 files changed, 28 insertions(+), 29 deletions(-) diff --git a/.cursor/skills/localnet-testing/SKILL.md b/.cursor/skills/localnet-testing/SKILL.md index dd6c590f..9f501754 100644 --- a/.cursor/skills/localnet-testing/SKILL.md +++ b/.cursor/skills/localnet-testing/SKILL.md @@ -3,8 +3,8 @@ Read the public [LocalNet guide](https://github.com/Fairmint/canton-node-sdk/wiki/LocalNet-testing) first. -**ENG-1635:** `@fairmint/canton-dev-tools` owns shared LocalNet pins going forward. This repository's -`bin/canton-localnet` soft-delegates to that package when installed; otherwise it falls back to the -deprecated `scripts/localnet-cloud.sh`. Prefer Dev Tools commands for pin-sensitive work. Current -`package.json` `localnet:*` scripts, integration tests, and the fallback scripts remain the source of -truth for commands that still run through this repo until the hard cutover. +**ENG-1635:** `@fairmint/canton-dev-tools` owns shared LocalNet pins going forward. This +repository's `bin/canton-localnet` soft-delegates to that package when installed; otherwise it falls +back to the deprecated `scripts/localnet-cloud.sh`. Prefer Dev Tools commands for pin-sensitive +work. Current `package.json` `localnet:*` scripts, integration tests, and the fallback scripts +remain the source of truth for commands that still run through this repo until the hard cutover. diff --git a/AGENTS.md b/AGENTS.md index 53a900b6..6983f361 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -1,7 +1,6 @@ # canton-node-sdk -See [CLAUDE.md](CLAUDE.md), [README.md](README.md), and -`.cursor/skills/localnet-testing/SKILL.md`. +See [CLAUDE.md](CLAUDE.md), [README.md](README.md), and `.cursor/skills/localnet-testing/SKILL.md`. ## LocalNet ownership (ENG-1635) @@ -29,8 +28,8 @@ Repo checks (`npm install`, `npm run fix`, `npm test`, `npm run build`) need no LocalNet runs Canton Network Quickstart in Docker. `npm run localnet:start` (= `bin/canton-localnet start`, which prefers `@fairmint/canton-dev-tools`, infra-only + OAuth2 by -default) is self-provisioning on the cloud image: it `apt`-installs Docker, starts a `dockerd` -(vfs storage driver, iptables-legacy) via passwordless `sudo`, adds +default) is self-provisioning on the cloud image: it `apt`-installs Docker, starts a `dockerd` (vfs +storage driver, iptables-legacy) via passwordless `sudo`, adds `scan.localhost`/`sv.localhost`/`wallet.localhost` to `/etc/hosts`, runs cn-quickstart `make setup`, brings up the compose stack, and waits for the Validator, Scan, and Ledger JSON APIs. diff --git a/README.md b/README.md index 2a30bc4b..baac4ed2 100644 --- a/README.md +++ b/README.md @@ -4,10 +4,10 @@ Low-level TypeScript SDK for Canton Network nodes (Ledger JSON API, Validator AP ## Developer documentation -The public [GitHub wiki](https://github.com/Fairmint/canton-node-sdk/wiki) is the canonical guide for -configuration, API boundaries, external signing, LocalNet, examples, and contribution. The public -[`src/index.ts`](https://github.com/Fairmint/canton-node-sdk/blob/main/src/index.ts) defines the -supported package surface; use the installed declarations and public +The public [GitHub wiki](https://github.com/Fairmint/canton-node-sdk/wiki) is the canonical guide +for configuration, API boundaries, external signing, LocalNet, examples, and contribution. The +public [`src/index.ts`](https://github.com/Fairmint/canton-node-sdk/blob/main/src/index.ts) defines +the supported package surface; use the installed declarations and public [`examples/`](https://github.com/Fairmint/canton-node-sdk/tree/main/examples) and [`test/`](https://github.com/Fairmint/canton-node-sdk/tree/main/test) for exact methods, request shapes, and error behavior. @@ -19,10 +19,10 @@ npm install @fairmint/canton-node-sdk ``` ```ts -import { Canton } from "@fairmint/canton-node-sdk"; +import { Canton } from '@fairmint/canton-node-sdk'; async function main(): Promise { - const canton = new Canton({ network: "localnet" }); + const canton = new Canton({ network: 'localnet' }); const version = await canton.ledger.getVersion(); console.log(version); } @@ -60,6 +60,6 @@ npm run localnet:dev-tools -- readiness npm run localnet:verify ``` -Until `@fairmint/canton-dev-tools` is published to npm, this repo pins the optional dependency to the -ENG-1635 git SHA. After publish, swap that pin to a semver range. Do not delete +Until `@fairmint/canton-dev-tools` is published to npm, this repo pins the optional dependency to +the ENG-1635 git SHA. After publish, swap that pin to a semver range. Do not delete `scripts/localnet-cloud.sh` until the hard-cutover follow-up. diff --git a/package.json b/package.json index d6afc58c..013836b7 100644 --- a/package.json +++ b/package.json @@ -47,6 +47,7 @@ "lint:fix": "eslint . --fix", "lint:npm": "npmPkgJsonLint . --fix", "lint:npm:check": "npmPkgJsonLint .", + "localnet:dev-tools": "canton-dev-tools", "localnet:logs": "bash ./bin/canton-localnet logs", "localnet:quickstart": "bash ./bin/canton-localnet setup", "localnet:setup": "bash ./bin/canton-localnet setup", @@ -55,7 +56,6 @@ "localnet:status": "bash ./bin/canton-localnet status", "localnet:stop": "bash ./bin/canton-localnet stop", "localnet:verify": "bash ./bin/canton-localnet verify", - "localnet:dev-tools": "canton-dev-tools", "prepack": "npm run clean && npm run build:core", "prepare-release": "tsx scripts/prepare-release.ts", "prepublishOnly": "npm run prepack", @@ -81,17 +81,6 @@ "ws": "8.21.0", "zod": "4.4.3" }, - "optionalDependencies": { - "@fairmint/canton-dev-tools": "github:Fairmint/canton-dev-tools#c2efb4b" - }, - "peerDependencies": { - "@fairmint/canton-dev-tools": ">=0.1.0" - }, - "peerDependenciesMeta": { - "@fairmint/canton-dev-tools": { - "optional": true - } - }, "devDependencies": { "@types/jest": "30.0.0", "@types/node": "26.1.0", @@ -116,6 +105,17 @@ "typescript": "5.9.3", "typescript-7": "npm:typescript@7.0.2" }, + "peerDependencies": { + "@fairmint/canton-dev-tools": ">=0.1.0" + }, + "peerDependenciesMeta": { + "@fairmint/canton-dev-tools": { + "optional": true + } + }, + "optionalDependencies": { + "@fairmint/canton-dev-tools": "github:Fairmint/canton-dev-tools#c2efb4b" + }, "engines": { "node": ">=22.0.0" }, From 0e313897c329ef8651ba5156018c14bf36cd661e Mon Sep 17 00:00:00 2001 From: Cursor Agent Date: Mon, 10 Aug 2026 17:12:25 +0000 Subject: [PATCH 3/8] Harden package boundary checks for ENG-1635 soft migration Narrow files to build/src/**, forbid CI-only build/test leakage, document production vs LocalNet soft-migration surfaces, and pin Dev Tools to bf32f24. Co-authored-by: HardlyDifficult --- README.md | 3 ++ docs/package-boundary.md | 53 ++++++++++++++++++++++++++++++ package.json | 4 +-- scripts/check-package-artifacts.ts | 50 +++++++++++++++++++++++----- 4 files changed, 99 insertions(+), 11 deletions(-) create mode 100644 docs/package-boundary.md diff --git a/README.md b/README.md index baac4ed2..19742344 100644 --- a/README.md +++ b/README.md @@ -63,3 +63,6 @@ npm run localnet:verify Until `@fairmint/canton-dev-tools` is published to npm, this repo pins the optional dependency to the ENG-1635 git SHA. After publish, swap that pin to a semver range. Do not delete `scripts/localnet-cloud.sh` until the hard-cutover follow-up. + +See [docs/package-boundary.md](docs/package-boundary.md) for what the npm package publishes versus +CI-only surfaces (`npm run check:package-artifacts`). diff --git a/docs/package-boundary.md b/docs/package-boundary.md new file mode 100644 index 00000000..86f6df66 --- /dev/null +++ b/docs/package-boundary.md @@ -0,0 +1,53 @@ +# Package boundary (production vs CI-only) + +This document describes what `@fairmint/canton-node-sdk` publishes to npm versus what stays +repository / CI-only. Repeatable enforcement lives in `npm run check:package-artifacts` +(`.github/workflows/package-artifacts.yml`). + +## Production (published) + +| Surface | Path / field | Notes | +| --- | --- | --- | +| Runtime SDK | `build/src/**` | Ledger / Validator / Scan clients and helpers from `src/` | +| Package metadata | `package.json`, `LICENSE`, `README.md` | Always included by npm | + +### Soft-migration LocalNet CLI (temporary) + +| Surface | Path / field | Notes | +| --- | --- | --- | +| LocalNet CLI | `bin/canton-localnet` (`package.json#bin`) | Soft-delegates to `@fairmint/canton-dev-tools` when installed | +| Cloud LocalNet helper | `scripts/localnet-cloud.sh` | Used by the fallback CLI path | + +**TODO (ENG-1635 hard cutover):** remove `bin/canton-localnet` and `scripts/localnet-cloud.sh` +from `package.json` `files` / `bin` once consumers depend on `@fairmint/canton-dev-tools` for +LocalNet. Until then, package artifact checks *require* these paths (known published surface) and +document the exception. + +Prefer: + +```bash +npx @fairmint/canton-dev-tools start +npm install -D @fairmint/canton-dev-tools +``` + +## CI-only / must not publish + +| Surface | Why | +| --- | --- | +| `libs/**` (cn-quickstart, splice submodules) | Docker / LocalNet fixtures; huge; not runtime | +| `*.dar` | DAML archives are not Node runtime artifacts | +| `fixtures/**` | Test fixtures (none shipped today; guarded) | +| `test/**`, `build/test/**` | Unit / LocalNet integration tests | +| `scripts/**` except `localnet-cloud.sh` | Codegen, release, and lint tooling | +| `examples/**`, `build/examples/**` | Demo sources (see wiki / repo tree) | +| `build/scripts/**` | Compiled lint/codegen helpers | +| `node_modules/**`, crash dumps (`core*`) | Accidental local artifacts | + +`prepack` runs `clean` + `build:core` so a normal publish only emits `build/src/**`. The `files` +field is narrowed to `build/src/**` so a dirty workspace that still contains `build/test` cannot +leak compiled tests into the tarball. + +## Related packages + +- Canonical LocalNet owner: [`@fairmint/canton-dev-tools`](https://github.com/Fairmint/canton-dev-tools) +- Cross-SDK audit: `docs/sdk-package-boundary-audit.md` in that repo (ENG-1635) diff --git a/package.json b/package.json index 013836b7..33bf2bfa 100644 --- a/package.json +++ b/package.json @@ -27,7 +27,7 @@ "files": [ "bin/canton-localnet", "scripts/localnet-cloud.sh", - "build/**" + "build/src/**" ], "scripts": { "artifacts:manifest": "mkdir -p artifacts && npm pack --dry-run --json --ignore-scripts > /tmp/npm-pack.json 2>/dev/null && jq -r '.[0].files[].path' /tmp/npm-pack.json | sort | tsx scripts/collapse-manifest.ts > artifacts/npm-manifest.txt", @@ -114,7 +114,7 @@ } }, "optionalDependencies": { - "@fairmint/canton-dev-tools": "github:Fairmint/canton-dev-tools#c2efb4b" + "@fairmint/canton-dev-tools": "github:Fairmint/canton-dev-tools#bf32f24" }, "engines": { "node": ">=22.0.0" diff --git a/scripts/check-package-artifacts.ts b/scripts/check-package-artifacts.ts index 39a2ea2e..5395b302 100644 --- a/scripts/check-package-artifacts.ts +++ b/scripts/check-package-artifacts.ts @@ -1,5 +1,15 @@ #!/usr/bin/env tsx +/** + * Repeatable npm package boundary check for @fairmint/canton-node-sdk. + * + * Production surface: build/src/** (+ package metadata). + * + * Soft-migration exception (ENG-1635): bin/canton-localnet + scripts/localnet-cloud.sh are still + * published on purpose. TODO(ENG-1635 hard cutover): drop those from package.json files/bin and + * from REQUIRED_SOFT_MIGRATION_LOCALNET_PATHS below once Dev Tools owns LocalNet for all consumers. + */ + import { spawnSync, type SpawnSyncReturns } from 'child_process'; import { chmodSync, cpSync, mkdirSync, mkdtempSync, readFileSync, rmSync, symlinkSync } from 'fs'; import { tmpdir } from 'os'; @@ -25,6 +35,18 @@ const DEFAULT_MAX_UNPACKED_BYTES = 15 * 1024 * 1024; const configuredMaxUnpackedBytes = process.env['MAX_PACKAGE_UNPACKED_BYTES']; const maxUnpackedBytes = parseMaxUnpackedBytes(configuredMaxUnpackedBytes); +/** Temporary publish allowlist until ENG-1635 hard cutover removes SDK LocalNet scripts. */ +const REQUIRED_SOFT_MIGRATION_LOCALNET_PATHS = ['bin/canton-localnet', 'scripts/localnet-cloud.sh'] as const; + +const REQUIRED_RUNTIME_PATHS = [ + 'build/src/index.js', + 'build/src/index.d.ts', + 'build/src/clients/ledger-json-api/operations/v2/contracts/get-contract-by-id.d.ts', + 'build/src/clients/ledger-json-api/operations/v2/dars/upload-dar.d.ts', + 'build/src/clients/ledger-json-api/operations/v2/dars/validate-dar.d.ts', + ...REQUIRED_SOFT_MIGRATION_LOCALNET_PATHS, +] as const; + function formatBytes(bytes: number): string { return `${(bytes / 1024 / 1024).toFixed(2)} MB`; } @@ -44,6 +66,21 @@ function forbiddenPackagePathReason(packagePath: string): string | null { if (packagePath === 'libs' || packagePath.startsWith('libs/')) { return 'submodules under libs/ must not be published'; } + if (packagePath === 'fixtures' || packagePath.startsWith('fixtures/')) { + return 'test fixtures must not be published'; + } + if (packagePath === 'test' || packagePath.startsWith('test/')) { + return 'source tests must not be published'; + } + if (packagePath === 'build/test' || packagePath.startsWith('build/test/')) { + return 'compiled tests are CI-only and must not be published'; + } + if (packagePath === 'build/scripts' || packagePath.startsWith('build/scripts/')) { + return 'compiled repo scripts are CI-only and must not be published'; + } + if (packagePath === 'build/examples' || packagePath.startsWith('build/examples/')) { + return 'compiled examples are CI-only and must not be published'; + } if (packagePath === 'node_modules' || packagePath.startsWith('node_modules/')) { return 'node_modules must not be published'; } @@ -175,15 +212,7 @@ if (result.unpackedSize > maxUnpackedBytes) { } const packagePaths = new Set(result.files.map((file) => file.path)); -for (const requiredPath of [ - 'build/src/index.js', - 'build/src/index.d.ts', - 'build/src/clients/ledger-json-api/operations/v2/contracts/get-contract-by-id.d.ts', - 'build/src/clients/ledger-json-api/operations/v2/dars/upload-dar.d.ts', - 'build/src/clients/ledger-json-api/operations/v2/dars/validate-dar.d.ts', - 'bin/canton-localnet', - 'scripts/localnet-cloud.sh', -]) { +for (const requiredPath of REQUIRED_RUNTIME_PATHS) { if (!packagePaths.has(requiredPath)) { errors.push(`package is missing required runtime entry ${requiredPath}`); } @@ -209,3 +238,6 @@ verifyPackagedLocalnetBinary(); console.log( `✓ ${result.name}@${result.version} package artifact is ${formatBytes(result.unpackedSize)} unpacked across ${result.files.length} files` ); +console.log( + `⚠ ENG-1635 soft migration: still publishing ${REQUIRED_SOFT_MIGRATION_LOCALNET_PATHS.join(', ')} (TODO: remove after hard cutover to @fairmint/canton-dev-tools)` +); From 7b2ff5f0cd1ce2b60999b7d67f6649d1482c04b0 Mon Sep 17 00:00:00 2001 From: Cursor Agent Date: Mon, 10 Aug 2026 18:04:52 +0000 Subject: [PATCH 4/8] Fix LocalNet soft-migration review findings Bump package.json to npm's current 0.0.251 so the optional @fairmint/canton-dev-tools peer (>=0.0.232) is satisfiable, replace the removed Dev Tools audit doc link with ENG-1635, and apply the documented CANTON_LOCALNET_INFRA_ONLY=true default before soft-delegation. Co-authored-by: HardlyDifficult --- bin/canton-localnet | 10 +++- docs/package-boundary.md | 2 +- package.json | 2 +- test/unit/scripts/canton-localnet.test.ts | 59 +++++++++++++++++++++++ 4 files changed, 70 insertions(+), 3 deletions(-) diff --git a/bin/canton-localnet b/bin/canton-localnet index 6f57a62e..b1815245 100755 --- a/bin/canton-localnet +++ b/bin/canton-localnet @@ -100,6 +100,12 @@ find_dev_tools_bin() { return 1 } +# Defaults advertised by this package binary. Apply before soft-delegation so +# Dev Tools inherits the same infra-only default as the legacy fallback path. +apply_package_localnet_defaults() { + export CANTON_LOCALNET_INFRA_ONLY="${CANTON_LOCALNET_INFRA_ONLY:-true}" +} + maybe_delegate_to_dev_tools() { local dev_tools_bin="" @@ -111,6 +117,8 @@ maybe_delegate_to_dev_tools() { return 1 fi + apply_package_localnet_defaults + log "Delegating to @fairmint/canton-dev-tools (ENG-1635). Set CANTON_LOCALNET_FORCE_LEGACY=1 to use deprecated SDK LocalNet scripts." exec "${dev_tools_bin}" "$@" } @@ -261,7 +269,7 @@ run_legacy_localnet() { export CANTON_LOCALNET_QUICKSTART_DIR="${dir}" fi export CANTON_LOCALNET_SPLICE_VERSION="${CANTON_LOCALNET_SPLICE_VERSION:-$(default_splice_version)}" - export CANTON_LOCALNET_INFRA_ONLY="${CANTON_LOCALNET_INFRA_ONLY:-true}" + apply_package_localnet_defaults exec bash "${LOCALNET_SCRIPT}" "$@" } diff --git a/docs/package-boundary.md b/docs/package-boundary.md index 86f6df66..19c2311f 100644 --- a/docs/package-boundary.md +++ b/docs/package-boundary.md @@ -50,4 +50,4 @@ leak compiled tests into the tarball. ## Related packages - Canonical LocalNet owner: [`@fairmint/canton-dev-tools`](https://github.com/Fairmint/canton-dev-tools) -- Cross-SDK audit: `docs/sdk-package-boundary-audit.md` in that repo (ENG-1635) +- Soft migration / hard cutover tracking: [ENG-1635](https://linear.app/fairmint/issue/ENG-1635/establish-canton-dev-tools-and-migrate-shared-canton-test) diff --git a/package.json b/package.json index 33bf2bfa..d5a859c2 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "@fairmint/canton-node-sdk", - "version": "0.0.196", + "version": "0.0.251", "description": "Canton Node SDK", "keywords": [ "canton", diff --git a/test/unit/scripts/canton-localnet.test.ts b/test/unit/scripts/canton-localnet.test.ts index 442c1a5b..35294647 100644 --- a/test/unit/scripts/canton-localnet.test.ts +++ b/test/unit/scripts/canton-localnet.test.ts @@ -98,4 +98,63 @@ describe('canton-localnet soft delegation', (): void => { rmSync(packageRoot, { recursive: true, force: true }); } }); + + it('applies CANTON_LOCALNET_INFRA_ONLY=true before soft-delegation', (): void => { + const packageRoot = mkdtempSync(resolve(tmpdir(), 'canton-localnet-infra-default-')); + const localnetBin = resolve(packageRoot, 'bin/canton-localnet'); + const devToolsBin = resolve(packageRoot, 'node_modules/@fairmint/canton-dev-tools/bin/canton-dev-tools'); + + mkdirSync(resolve(packageRoot, 'bin'), { recursive: true }); + mkdirSync(resolve(packageRoot, 'scripts'), { recursive: true }); + mkdirSync(resolve(devToolsBin, '..'), { recursive: true }); + copyFileSync(resolve(REPO_ROOT, 'bin/canton-localnet'), localnetBin); + chmodSync(localnetBin, 0o755); + writeFileSync(resolve(packageRoot, 'scripts/localnet-cloud.sh'), 'printf "legacy\\n"\n'); + writeFileSync(devToolsBin, '#!/usr/bin/env bash\nprintf "infra:%s\\n" "${CANTON_LOCALNET_INFRA_ONLY}"\n'); + chmodSync(devToolsBin, 0o755); + + try { + const output = execFileSync(localnetBin, ['status'], { + encoding: 'utf8', + env: { + ...process.env, + CANTON_LOCALNET_FORCE_LEGACY: '', + // Unset so the package binary must supply the documented default. + CANTON_LOCALNET_INFRA_ONLY: '', + }, + }); + expect(output.trim()).toBe('infra:true'); + } finally { + rmSync(packageRoot, { recursive: true, force: true }); + } + }); + + it('preserves an explicit CANTON_LOCALNET_INFRA_ONLY override on soft-delegation', (): void => { + const packageRoot = mkdtempSync(resolve(tmpdir(), 'canton-localnet-infra-override-')); + const localnetBin = resolve(packageRoot, 'bin/canton-localnet'); + const devToolsBin = resolve(packageRoot, 'node_modules/@fairmint/canton-dev-tools/bin/canton-dev-tools'); + + mkdirSync(resolve(packageRoot, 'bin'), { recursive: true }); + mkdirSync(resolve(packageRoot, 'scripts'), { recursive: true }); + mkdirSync(resolve(devToolsBin, '..'), { recursive: true }); + copyFileSync(resolve(REPO_ROOT, 'bin/canton-localnet'), localnetBin); + chmodSync(localnetBin, 0o755); + writeFileSync(resolve(packageRoot, 'scripts/localnet-cloud.sh'), 'printf "legacy\\n"\n'); + writeFileSync(devToolsBin, '#!/usr/bin/env bash\nprintf "infra:%s\\n" "${CANTON_LOCALNET_INFRA_ONLY}"\n'); + chmodSync(devToolsBin, 0o755); + + try { + const output = execFileSync(localnetBin, ['status'], { + encoding: 'utf8', + env: { + ...process.env, + CANTON_LOCALNET_FORCE_LEGACY: '', + CANTON_LOCALNET_INFRA_ONLY: 'false', + }, + }); + expect(output.trim()).toBe('infra:false'); + } finally { + rmSync(packageRoot, { recursive: true, force: true }); + } + }); }); From d55bac59c6abb383295870a473402abbf3da7d96 Mon Sep 17 00:00:00 2001 From: Cursor Agent Date: Mon, 10 Aug 2026 21:03:15 +0000 Subject: [PATCH 5/8] Hard-cutover LocalNet to @fairmint/canton-dev-tools@0.1.1 Delete the SDK-owned LocalNet engine (scripts/localnet-cloud.sh, shims, published canton-localnet bin) and wire localnet:* to the published Dev Tools CLI. Integration tests import helpers from @fairmint/canton-dev-tools/testing; package artifacts no longer ship LocalNet scripts. Co-authored-by: HardlyDifficult --- .cursor/skills/localnet-testing/SKILL.md | 16 +- .github/workflows/test-cn-quickstart.yml | 2 +- AGENTS.md | 35 +- README.md | 32 +- bin/canton-localnet | 298 ----- docs/package-boundary.md | 27 +- package.json | 37 +- scripts/check-package-artifacts.ts | 91 +- scripts/localnet-cloud.sh | 1182 ----------------- scripts/localnet-status.sh | 5 - scripts/localnet/localnet-cloud.sh | 5 - scripts/localnet/localnet-status.sh | 5 - scripts/localnet/setup-localnet.sh | 5 - scripts/localnet/setup-quickstart-localnet.sh | 5 - scripts/localnet/start-localnet.sh | 5 - scripts/localnet/stop-localnet.sh | 5 - scripts/setup-localnet.sh | 5 - scripts/setup-quickstart-localnet.sh | 5 - scripts/start-localnet.sh | 5 - scripts/stop-localnet.sh | 5 - .../localnet/ledger-api/dars.test.ts | 8 +- .../ledger-api/interactive-submission.test.ts | 2 +- .../ledger-api/paid-traffic-cost.test.ts | 2 +- test/integration/localnet/ledger-api/setup.ts | 2 +- test/integration/localnet/scan-api/setup.ts | 2 +- .../scan-api/snapshot-timestamps.test.ts | 2 +- .../localnet/validator-api/scan-proxy.test.ts | 2 +- .../localnet/validator-api/setup.ts | 2 +- .../localnet/validator-api/wallet.test.ts | 2 +- test/unit/scripts/canton-localnet.test.ts | 160 --- test/unit/scripts/localnet-cloud.test.ts | 134 -- test/utils/index.ts | 3 - test/utils/localnetLedgerClients.ts | 115 -- test/utils/testConfig.ts | 79 -- 34 files changed, 98 insertions(+), 2192 deletions(-) delete mode 100755 bin/canton-localnet delete mode 100755 scripts/localnet-cloud.sh delete mode 100755 scripts/localnet-status.sh delete mode 100644 scripts/localnet/localnet-cloud.sh delete mode 100644 scripts/localnet/localnet-status.sh delete mode 100644 scripts/localnet/setup-localnet.sh delete mode 100644 scripts/localnet/setup-quickstart-localnet.sh delete mode 100644 scripts/localnet/start-localnet.sh delete mode 100644 scripts/localnet/stop-localnet.sh delete mode 100755 scripts/setup-localnet.sh delete mode 100755 scripts/setup-quickstart-localnet.sh delete mode 100755 scripts/start-localnet.sh delete mode 100755 scripts/stop-localnet.sh delete mode 100644 test/unit/scripts/canton-localnet.test.ts delete mode 100644 test/unit/scripts/localnet-cloud.test.ts delete mode 100644 test/utils/index.ts delete mode 100644 test/utils/localnetLedgerClients.ts delete mode 100644 test/utils/testConfig.ts diff --git a/.cursor/skills/localnet-testing/SKILL.md b/.cursor/skills/localnet-testing/SKILL.md index 9f501754..86739369 100644 --- a/.cursor/skills/localnet-testing/SKILL.md +++ b/.cursor/skills/localnet-testing/SKILL.md @@ -3,8 +3,14 @@ Read the public [LocalNet guide](https://github.com/Fairmint/canton-node-sdk/wiki/LocalNet-testing) first. -**ENG-1635:** `@fairmint/canton-dev-tools` owns shared LocalNet pins going forward. This -repository's `bin/canton-localnet` soft-delegates to that package when installed; otherwise it falls -back to the deprecated `scripts/localnet-cloud.sh`. Prefer Dev Tools commands for pin-sensitive -work. Current `package.json` `localnet:*` scripts, integration tests, and the fallback scripts -remain the source of truth for commands that still run through this repo until the hard cutover. +**ENG-1635:** `@fairmint/canton-dev-tools@0.1.1+` owns LocalNet lifecycle, pins, and shared test +helpers. This repository does not ship LocalNet scripts or a `canton-localnet` binary. + +- Commands: `npm run localnet:*` (wired to `canton-dev-tools`) or + `npx @fairmint/canton-dev-tools ` +- Helpers: `@fairmint/canton-dev-tools/testing` +- Pins: Dev Tools + [COMPATIBILITY.md](https://github.com/Fairmint/canton-dev-tools/blob/main/COMPATIBILITY.md) + +Domain integration tests under `test/integration/localnet/**` remain in this repo; only their +imports come from Dev Tools. diff --git a/.github/workflows/test-cn-quickstart.yml b/.github/workflows/test-cn-quickstart.yml index f061dcca..c2348c89 100644 --- a/.github/workflows/test-cn-quickstart.yml +++ b/.github/workflows/test-cn-quickstart.yml @@ -63,7 +63,7 @@ jobs: uses: actions/cache/restore@v6 with: path: ${{ github.workspace }}/libs/cn-quickstart/quickstart/.env.local - key: quickstart-env-${{ runner.os }}-${{ hashFiles('.github/workflows/test-cn-quickstart.yml', 'package.json', 'package-lock.json', 'scripts/localnet-cloud.sh') }}-${{ steps.quickstart-env-cache-key.outputs.submodules }} + key: quickstart-env-${{ runner.os }}-${{ hashFiles('.github/workflows/test-cn-quickstart.yml', 'package.json', 'package-lock.json') }}-${{ steps.quickstart-env-cache-key.outputs.submodules }} - name: Restore Node Dependencies Cache id: node-cache-restore diff --git a/AGENTS.md b/AGENTS.md index 6983f361..ee326a62 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -1,35 +1,32 @@ # canton-node-sdk -See [CLAUDE.md](CLAUDE.md), [README.md](README.md), and `.cursor/skills/localnet-testing/SKILL.md`. +See [CLAUDE.md](CLAUDE.md), [README.md](README.md), and +`.cursor/skills/localnet-testing/SKILL.md`. `package.json` (`localnet:*` scripts) is the source of +truth for LocalNet commands in this repo; lifecycle is owned by +`@fairmint/canton-dev-tools@0.1.1+`. ## LocalNet ownership (ENG-1635) -**`@fairmint/canton-dev-tools` owns LocalNet pins going forward** (cn-quickstart ref, Splice, -Scribe, protocol version). See that package's `COMPATIBILITY.md`. +**`@fairmint/canton-dev-tools` owns LocalNet** (CLI, pins, and shared test helpers). This SDK does +not ship a LocalNet engine or `canton-localnet` binary. -This repository still ships `bin/canton-localnet` and `scripts/localnet-cloud.sh` as a **soft -migration** fallback: - -- `bin/canton-localnet` soft-delegates to `@fairmint/canton-dev-tools` when that package is - installed (optionalDependency / optional peer). -- Set `CANTON_LOCALNET_FORCE_LEGACY=1` to force the deprecated SDK scripts. -- `package.json` `localnet:*` scripts still call `bin/canton-localnet` (which prefers Dev Tools). -- Prefer `npx @fairmint/canton-dev-tools ` or `npm run localnet:dev-tools -- ` - when the optional dependency is present. -- Do **not** treat SDK-local pins in `bin/canton-localnet` as the long-term source of truth. +- Install pin: `devDependency` `@fairmint/canton-dev-tools@0.1.1` (exact). +- Repo scripts: `npm run localnet:*` → `canton-dev-tools `. +- Integration helpers: import from `@fairmint/canton-dev-tools/testing`. +- Pins / auth defaults: see Dev Tools + [COMPATIBILITY.md](https://github.com/Fairmint/canton-dev-tools/blob/main/COMPATIBILITY.md). ## Cursor Cloud specific instructions Repo checks (`npm install`, `npm run fix`, `npm test`, `npm run build`) need no dashboard secrets. -`npm install` does not require `NPM_TOKEN` here (dependencies are public). The optional -`@fairmint/canton-dev-tools` git dependency needs GitHub network access (public repo). +`npm install` does not require `NPM_TOKEN` here (dependencies are public). ### Canton LocalNet on a cloud VM -LocalNet runs Canton Network Quickstart in Docker. `npm run localnet:start` (= -`bin/canton-localnet start`, which prefers `@fairmint/canton-dev-tools`, infra-only + OAuth2 by -default) is self-provisioning on the cloud image: it `apt`-installs Docker, starts a `dockerd` (vfs -storage driver, iptables-legacy) via passwordless `sudo`, adds +LocalNet runs Canton Network Quickstart in Docker via `@fairmint/canton-dev-tools`. +`npm run localnet:start` (= `canton-dev-tools start`, infra-only + OAuth2 by default) is +self-provisioning on the cloud image: it `apt`-installs Docker, starts a `dockerd` (vfs storage +driver, iptables-legacy) via passwordless `sudo`, adds `scan.localhost`/`sv.localhost`/`wallet.localhost` to `/etc/hosts`, runs cn-quickstart `make setup`, brings up the compose stack, and waits for the Validator, Scan, and Ledger JSON APIs. diff --git a/README.md b/README.md index 19742344..0fdfe7bf 100644 --- a/README.md +++ b/README.md @@ -44,25 +44,31 @@ npm test npm run build ``` -### LocalNet (ENG-1635 soft migration) +### LocalNet (owned by `@fairmint/canton-dev-tools`) -**Pin owner:** [`@fairmint/canton-dev-tools`](https://github.com/Fairmint/canton-dev-tools) (see its -`COMPATIBILITY.md`). This SDK still ships `bin/canton-localnet` / `scripts/localnet-cloud.sh` as a -temporary fallback. The SDK binary soft-delegates to Dev Tools when that optional dependency is -installed. +LocalNet lifecycle and shared test helpers live in +[`@fairmint/canton-dev-tools@0.1.1`](https://www.npmjs.com/package/@fairmint/canton-dev-tools) +(see its [COMPATIBILITY.md](https://github.com/Fairmint/canton-dev-tools/blob/main/COMPATIBILITY.md)). +This SDK does not publish a LocalNet CLI or `scripts/localnet-cloud.sh`. ```bash -# Preferred once Dev Tools is available (optionalDependency / optional peer): -npx @fairmint/canton-dev-tools start -npm run localnet:dev-tools -- readiness +npm install # installs @fairmint/canton-dev-tools as an exact-pinned devDependency +npm run localnet:start +npm run localnet:smoke +npm run localnet:stop -# Existing SDK scripts still work (delegate when possible, else legacy scripts): -npm run localnet:verify +# Or call the Dev Tools CLI directly: +npx @fairmint/canton-dev-tools start ``` -Until `@fairmint/canton-dev-tools` is published to npm, this repo pins the optional dependency to -the ENG-1635 git SHA. After publish, swap that pin to a semver range. Do not delete -`scripts/localnet-cloud.sh` until the hard-cutover follow-up. +Integration helpers: + +```ts +import { + buildIntegrationTestClientConfig, + getLocalnetParticipantAdminLedgerClient, +} from '@fairmint/canton-dev-tools/testing'; +``` See [docs/package-boundary.md](docs/package-boundary.md) for what the npm package publishes versus CI-only surfaces (`npm run check:package-artifacts`). diff --git a/bin/canton-localnet b/bin/canton-localnet deleted file mode 100755 index b1815245..00000000 --- a/bin/canton-localnet +++ /dev/null @@ -1,298 +0,0 @@ -#!/usr/bin/env bash -# Soft migration (ENG-1635): prefer @fairmint/canton-dev-tools when installed. -# Fallback to the deprecated SDK-owned LocalNet scripts until the hard cutover. -set -euo pipefail - -resolve_script_dir() { - local source="$1" - local dir="" - - while [[ -L "${source}" ]]; do - dir="$(cd -P "$(dirname "${source}")" && pwd)" - source="$(readlink "${source}")" - if [[ "${source}" != /* ]]; then - source="${dir}/${source}" - fi - done - - cd -P "$(dirname "${source}")" && pwd -} - -PACKAGE_ROOT="$(cd "$(resolve_script_dir "${BASH_SOURCE[0]}")/.." && pwd)" -LOCALNET_SCRIPT="${PACKAGE_ROOT}/scripts/localnet-cloud.sh" -DEFAULT_QUICKSTART_REPO="https://github.com/digital-asset/cn-quickstart.git" -DEFAULT_QUICKSTART_REF="2f4edfc17621a7dfb6d44357050c22f4b3914c89" -DEFAULT_SPLICE_VERSION="0.6.8" - -log() { - printf '[canton-localnet] %s\n' "$*" >&2 -} - -usage() { - cat <<'USAGE' -Usage: canton-localnet - -DEPRECATED (ENG-1635): LocalNet pin ownership is moving to @fairmint/canton-dev-tools. -This binary soft-delegates to that package when available; otherwise it falls back to the -SDK-owned scripts/localnet-cloud.sh (kept temporarily). Prefer: - - npx @fairmint/canton-dev-tools start - npm install -D @fairmint/canton-dev-tools - -Commands: - setup Prepare LocalNet prerequisites - start Start lean LocalNet + Keycloak and wait for ready endpoints - stop Stop LocalNet services - logs Show LocalNet diagnostic logs - status Show docker + endpoint status - smoke Run endpoint smoke checks - test Run project integration tests when configured - verify Run setup + start + smoke + test - -One-liners: - npx @fairmint/canton-node-sdk start - npx @fairmint/canton-node-sdk verify - -Environment: - CANTON_LOCALNET_QUICKSTART_DIR Use an existing cn-quickstart/quickstart directory - CANTON_LOCALNET_CACHE_DIR Cache root for fetched cn-quickstart assets - CANTON_LOCALNET_QUICKSTART_REF cn-quickstart git ref to fetch - CANTON_LOCALNET_SPLICE_VERSION Splice image tag (defaults to the SDK's pinned API version) - CANTON_LOCALNET_INFRA_ONLY Defaults to true for this package binary - CANTON_LOCALNET_FORCE_LEGACY Set to 1 to skip soft-delegation to canton-dev-tools -USAGE -} - -require_command() { - local cmd="$1" - if ! command -v "${cmd}" >/dev/null 2>&1; then - log "Missing required command: ${cmd}" - exit 1 - fi -} - -# Resolve @fairmint/canton-dev-tools CLI without following a PATH entry that points -# back at this same SDK binary (both packages historically expose canton-localnet). -find_dev_tools_bin() { - local dir="${PACKAGE_ROOT}" - local candidate="" - - while true; do - candidate="${dir}/node_modules/@fairmint/canton-dev-tools/bin/canton-dev-tools" - if [[ -x "${candidate}" ]]; then - printf '%s' "${candidate}" - return 0 - fi - if [[ "${dir}" == "/" ]]; then - break - fi - dir="$(dirname "${dir}")" - done - - if [[ -n "${npm_config_local_prefix:-}" ]]; then - candidate="${npm_config_local_prefix}/node_modules/@fairmint/canton-dev-tools/bin/canton-dev-tools" - if [[ -x "${candidate}" ]]; then - printf '%s' "${candidate}" - return 0 - fi - fi - - return 1 -} - -# Defaults advertised by this package binary. Apply before soft-delegation so -# Dev Tools inherits the same infra-only default as the legacy fallback path. -apply_package_localnet_defaults() { - export CANTON_LOCALNET_INFRA_ONLY="${CANTON_LOCALNET_INFRA_ONLY:-true}" -} - -maybe_delegate_to_dev_tools() { - local dev_tools_bin="" - - if [[ "${CANTON_LOCALNET_FORCE_LEGACY:-}" == "1" ]]; then - return 1 - fi - - if ! dev_tools_bin="$(find_dev_tools_bin)"; then - return 1 - fi - - apply_package_localnet_defaults - - log "Delegating to @fairmint/canton-dev-tools (ENG-1635). Set CANTON_LOCALNET_FORCE_LEGACY=1 to use deprecated SDK LocalNet scripts." - exec "${dev_tools_bin}" "$@" -} - -cache_root() { - if [[ -n "${CANTON_LOCALNET_CACHE_DIR:-}" ]]; then - printf '%s' "${CANTON_LOCALNET_CACHE_DIR}" - return - fi - - if [[ -n "${XDG_CACHE_HOME:-}" ]]; then - printf '%s' "${XDG_CACHE_HOME}/fairmint/canton-localnet" - return - fi - - printf '%s' "${HOME}/.cache/fairmint/canton-localnet" -} - -quickstart_ref() { - printf '%s' "${CANTON_LOCALNET_QUICKSTART_REF:-${DEFAULT_QUICKSTART_REF}}" -} - -quickstart_repo() { - printf '%s' "${CANTON_LOCALNET_QUICKSTART_REPO:-${DEFAULT_QUICKSTART_REPO}}" -} - -default_splice_version() { - if [[ -f "${PACKAGE_ROOT}/libs/splice/VERSION" ]]; then - local version="" - version="$(tr -d '[:space:]' < "${PACKAGE_ROOT}/libs/splice/VERSION")" - if [[ -n "${version}" ]]; then - printf '%s' "${version}" - return - fi - fi - - printf '%s' "${DEFAULT_SPLICE_VERSION}" -} - -default_quickstart_root() { - printf '%s/cn-quickstart-%s' "$(cache_root)" "$(quickstart_ref)" -} - -repo_quickstart_dir() { - printf '%s/libs/cn-quickstart/quickstart' "${PACKAGE_ROOT}" -} - -use_repo_quickstart_default() { - [[ -z "${CANTON_LOCALNET_QUICKSTART_DIR:-}" \ - && -f "${PACKAGE_ROOT}/.gitmodules" \ - && ( -d "${PACKAGE_ROOT}/.git" || -f "${PACKAGE_ROOT}/.git" ) ]] -} - -quickstart_dir() { - if [[ -n "${CANTON_LOCALNET_QUICKSTART_DIR:-}" ]]; then - printf '%s' "${CANTON_LOCALNET_QUICKSTART_DIR}" - return - fi - - if use_repo_quickstart_default; then - repo_quickstart_dir - return - fi - - printf '%s/quickstart' "$(default_quickstart_root)" -} - -ensure_quickstart_checkout() { - local dir="" - local root="" - local tmp="" - - if use_repo_quickstart_default; then - return - fi - - dir="$(quickstart_dir)" - if [[ -d "${dir}/docker/modules/localnet" ]]; then - return - fi - - if [[ -n "${CANTON_LOCALNET_QUICKSTART_DIR:-}" ]]; then - log "CANTON_LOCALNET_QUICKSTART_DIR is not a cn-quickstart quickstart directory: ${dir}" - exit 1 - fi - - require_command git - - root="$(default_quickstart_root)" - if [[ -e "${root}" ]]; then - log "Cached cn-quickstart directory exists but is incomplete: ${root}" - log "Remove it or set CANTON_LOCALNET_CACHE_DIR to use a fresh cache." - exit 1 - fi - - mkdir -p "$(cache_root)" - tmp="$(mktemp -d "$(cache_root)/cn-quickstart.XXXXXX")" - trap 'rm -rf "${tmp}"' RETURN - - log "Fetching cn-quickstart $(quickstart_ref)..." - git -C "${tmp}" init -q - git -C "${tmp}" remote add origin "$(quickstart_repo)" - if git -C "${tmp}" fetch --depth 1 origin "$(quickstart_ref)"; then - git -C "${tmp}" checkout --detach FETCH_HEAD -q - else - log "Direct ref fetch failed; retrying with a blobless repository fetch." - git -C "${tmp}" fetch --filter=blob:none origin - git -C "${tmp}" checkout --detach "$(quickstart_ref)" -q - fi - - if [[ ! -d "${tmp}/quickstart/docker/modules/localnet" ]]; then - log "Fetched cn-quickstart does not contain quickstart/docker/modules/localnet." - exit 1 - fi - - mv "${tmp}" "${root}" - trap - RETURN - log "Cached cn-quickstart at ${root}." -} - -run_legacy_localnet() { - local command="${1:-}" - local dir="" - - log "Using deprecated SDK LocalNet scripts. Install @fairmint/canton-dev-tools for the shared pin owner (ENG-1635)." - - case "${command}" in - "" | -h | --help | help) - usage - if [[ -z "${command}" ]]; then - exit 1 - fi - exit 0 - ;; - setup | start | verify) - ensure_quickstart_checkout - ;; - logs | status | stop | smoke | test) - ;; - *) - usage - exit 1 - ;; - esac - - dir="$(quickstart_dir)" - if ! use_repo_quickstart_default; then - export CANTON_LOCALNET_QUICKSTART_DIR="${dir}" - fi - export CANTON_LOCALNET_SPLICE_VERSION="${CANTON_LOCALNET_SPLICE_VERSION:-$(default_splice_version)}" - apply_package_localnet_defaults - - exec bash "${LOCALNET_SCRIPT}" "$@" -} - -main() { - local command="${1:-}" - - case "${command}" in - "" | -h | --help | help) - # Help should describe both paths; do not require soft-delegation. - usage - if [[ -z "${command}" ]]; then - exit 1 - fi - exit 0 - ;; - esac - - if maybe_delegate_to_dev_tools "$@"; then - return - fi - - run_legacy_localnet "$@" -} - -main "$@" diff --git a/docs/package-boundary.md b/docs/package-boundary.md index 19c2311f..127d27c3 100644 --- a/docs/package-boundary.md +++ b/docs/package-boundary.md @@ -11,24 +11,9 @@ repository / CI-only. Repeatable enforcement lives in `npm run check:package-art | Runtime SDK | `build/src/**` | Ledger / Validator / Scan clients and helpers from `src/` | | Package metadata | `package.json`, `LICENSE`, `README.md` | Always included by npm | -### Soft-migration LocalNet CLI (temporary) - -| Surface | Path / field | Notes | -| --- | --- | --- | -| LocalNet CLI | `bin/canton-localnet` (`package.json#bin`) | Soft-delegates to `@fairmint/canton-dev-tools` when installed | -| Cloud LocalNet helper | `scripts/localnet-cloud.sh` | Used by the fallback CLI path | - -**TODO (ENG-1635 hard cutover):** remove `bin/canton-localnet` and `scripts/localnet-cloud.sh` -from `package.json` `files` / `bin` once consumers depend on `@fairmint/canton-dev-tools` for -LocalNet. Until then, package artifact checks *require* these paths (known published surface) and -document the exception. - -Prefer: - -```bash -npx @fairmint/canton-dev-tools start -npm install -D @fairmint/canton-dev-tools -``` +LocalNet CLI and shared integration-test helpers are **not** published here. They live in +[`@fairmint/canton-dev-tools`](https://www.npmjs.com/package/@fairmint/canton-dev-tools) +(`0.1.1+`), including `scripts/localnet-cloud.sh` and `@fairmint/canton-dev-tools/testing`. ## CI-only / must not publish @@ -38,7 +23,8 @@ npm install -D @fairmint/canton-dev-tools | `*.dar` | DAML archives are not Node runtime artifacts | | `fixtures/**` | Test fixtures (none shipped today; guarded) | | `test/**`, `build/test/**` | Unit / LocalNet integration tests | -| `scripts/**` except `localnet-cloud.sh` | Codegen, release, and lint tooling | +| `scripts/**` | Codegen, release, lint tooling (LocalNet engine removed) | +| `bin/**` | No published CLI; use `@fairmint/canton-dev-tools` | | `examples/**`, `build/examples/**` | Demo sources (see wiki / repo tree) | | `build/scripts/**` | Compiled lint/codegen helpers | | `node_modules/**`, crash dumps (`core*`) | Accidental local artifacts | @@ -50,4 +36,5 @@ leak compiled tests into the tarball. ## Related packages - Canonical LocalNet owner: [`@fairmint/canton-dev-tools`](https://github.com/Fairmint/canton-dev-tools) -- Soft migration / hard cutover tracking: [ENG-1635](https://linear.app/fairmint/issue/ENG-1635/establish-canton-dev-tools-and-migrate-shared-canton-test) + ([COMPATIBILITY.md](https://github.com/Fairmint/canton-dev-tools/blob/main/COMPATIBILITY.md)) +- Tracking: [ENG-1635](https://linear.app/fairmint/issue/ENG-1635/establish-canton-dev-tools-and-migrate-shared-canton-test) diff --git a/package.json b/package.json index d5a859c2..ff44518f 100644 --- a/package.json +++ b/package.json @@ -21,12 +21,7 @@ "author": "Fairmint", "main": "build/src/index.js", "types": "build/src/index.d.ts", - "bin": { - "canton-localnet": "bin/canton-localnet" - }, "files": [ - "bin/canton-localnet", - "scripts/localnet-cloud.sh", "build/src/**" ], "scripts": { @@ -47,15 +42,17 @@ "lint:fix": "eslint . --fix", "lint:npm": "npmPkgJsonLint . --fix", "lint:npm:check": "npmPkgJsonLint .", - "localnet:dev-tools": "canton-dev-tools", - "localnet:logs": "bash ./bin/canton-localnet logs", - "localnet:quickstart": "bash ./bin/canton-localnet setup", - "localnet:setup": "bash ./bin/canton-localnet setup", - "localnet:smoke": "bash ./bin/canton-localnet smoke", - "localnet:start": "bash ./bin/canton-localnet start", - "localnet:status": "bash ./bin/canton-localnet status", - "localnet:stop": "bash ./bin/canton-localnet stop", - "localnet:verify": "bash ./bin/canton-localnet verify", + "localnet:diagnostics": "canton-dev-tools diagnostics", + "localnet:logs": "canton-dev-tools logs", + "localnet:quickstart": "canton-dev-tools setup", + "localnet:readiness": "canton-dev-tools readiness", + "localnet:setup": "canton-dev-tools setup", + "localnet:smoke": "canton-dev-tools smoke", + "localnet:start": "canton-dev-tools start", + "localnet:status": "canton-dev-tools status", + "localnet:stop": "canton-dev-tools stop", + "localnet:teardown": "canton-dev-tools teardown", + "localnet:verify": "canton-dev-tools verify", "prepack": "npm run clean && npm run build:core", "prepare-release": "tsx scripts/prepare-release.ts", "prepublishOnly": "npm run prepack", @@ -82,6 +79,7 @@ "zod": "4.4.3" }, "devDependencies": { + "@fairmint/canton-dev-tools": "0.1.1", "@types/jest": "30.0.0", "@types/node": "26.1.0", "@types/ws": "8.18.1", @@ -105,17 +103,6 @@ "typescript": "5.9.3", "typescript-7": "npm:typescript@7.0.2" }, - "peerDependencies": { - "@fairmint/canton-dev-tools": ">=0.1.0" - }, - "peerDependenciesMeta": { - "@fairmint/canton-dev-tools": { - "optional": true - } - }, - "optionalDependencies": { - "@fairmint/canton-dev-tools": "github:Fairmint/canton-dev-tools#bf32f24" - }, "engines": { "node": ">=22.0.0" }, diff --git a/scripts/check-package-artifacts.ts b/scripts/check-package-artifacts.ts index 5395b302..06a7ea9d 100644 --- a/scripts/check-package-artifacts.ts +++ b/scripts/check-package-artifacts.ts @@ -3,17 +3,11 @@ /** * Repeatable npm package boundary check for @fairmint/canton-node-sdk. * - * Production surface: build/src/** (+ package metadata). - * - * Soft-migration exception (ENG-1635): bin/canton-localnet + scripts/localnet-cloud.sh are still - * published on purpose. TODO(ENG-1635 hard cutover): drop those from package.json files/bin and - * from REQUIRED_SOFT_MIGRATION_LOCALNET_PATHS below once Dev Tools owns LocalNet for all consumers. + * Production surface: build/src/** (+ package metadata). LocalNet CLI/helpers live in + * @fairmint/canton-dev-tools and must not ship in this package. */ import { spawnSync, type SpawnSyncReturns } from 'child_process'; -import { chmodSync, cpSync, mkdirSync, mkdtempSync, readFileSync, rmSync, symlinkSync } from 'fs'; -import { tmpdir } from 'os'; -import { join } from 'path'; interface NpmPackFile { path: string; @@ -35,16 +29,17 @@ const DEFAULT_MAX_UNPACKED_BYTES = 15 * 1024 * 1024; const configuredMaxUnpackedBytes = process.env['MAX_PACKAGE_UNPACKED_BYTES']; const maxUnpackedBytes = parseMaxUnpackedBytes(configuredMaxUnpackedBytes); -/** Temporary publish allowlist until ENG-1635 hard cutover removes SDK LocalNet scripts. */ -const REQUIRED_SOFT_MIGRATION_LOCALNET_PATHS = ['bin/canton-localnet', 'scripts/localnet-cloud.sh'] as const; - const REQUIRED_RUNTIME_PATHS = [ 'build/src/index.js', 'build/src/index.d.ts', 'build/src/clients/ledger-json-api/operations/v2/contracts/get-contract-by-id.d.ts', 'build/src/clients/ledger-json-api/operations/v2/dars/upload-dar.d.ts', 'build/src/clients/ledger-json-api/operations/v2/dars/validate-dar.d.ts', - ...REQUIRED_SOFT_MIGRATION_LOCALNET_PATHS, +] as const; + +const FORBIDDEN_LOCALNET_ENGINE_PATHS = [ + 'bin/canton-localnet', + 'scripts/localnet-cloud.sh', ] as const; function formatBytes(bytes: number): string { @@ -81,6 +76,12 @@ function forbiddenPackagePathReason(packagePath: string): string | null { if (packagePath === 'build/examples' || packagePath.startsWith('build/examples/')) { return 'compiled examples are CI-only and must not be published'; } + if (packagePath === 'bin' || packagePath.startsWith('bin/')) { + return 'LocalNet CLI belongs in @fairmint/canton-dev-tools, not this SDK'; + } + if (packagePath === 'scripts' || packagePath.startsWith('scripts/')) { + return 'repo scripts (including LocalNet) must not be published'; + } if (packagePath === 'node_modules' || packagePath.startsWith('node_modules/')) { return 'node_modules must not be published'; } @@ -126,60 +127,6 @@ function throwIfSpawnFailed(command: string, result: SpawnSyncReturns): throw new Error(spawnFailureDetails(command, result)); } -function verifyPackagedLocalnetBinary(): void { - const tempDir = mkdtempSync(join(tmpdir(), 'canton-node-sdk-package-')); - - try { - const packageRoot = join(tempDir, 'node_modules', '@fairmint', 'canton-node-sdk'); - const binDir = join(tempDir, 'node_modules', '.bin'); - const localnetBin = join(packageRoot, 'bin', 'canton-localnet'); - const localnetSymlink = join(binDir, 'canton-localnet'); - - mkdirSync(packageRoot, { recursive: true }); - mkdirSync(binDir, { recursive: true }); - cpSync(join(process.cwd(), 'bin'), join(packageRoot, 'bin'), { recursive: true }); - cpSync(join(process.cwd(), 'scripts'), join(packageRoot, 'scripts'), { recursive: true }); - chmodSync(localnetBin, 0o755); - symlinkSync('../@fairmint/canton-node-sdk/bin/canton-localnet', localnetSymlink); - - const logs = spawnSync(localnetSymlink, ['logs'], { - cwd: tempDir, - encoding: 'utf8', - env: { - ...process.env, - CANTON_LOCALNET_CACHE_DIR: join(tempDir, 'cache'), - // Soft-migration: packaged SDK fallback must still work without Dev Tools. - CANTON_LOCALNET_FORCE_LEGACY: '1', - HOME: join(tempDir, 'home'), - }, - }); - throwIfSpawnFailed('packaged canton-localnet logs', logs); - } finally { - rmSync(tempDir, { recursive: true, force: true }); - } -} - -function verifyPackagedLocalnetPins(): void { - // Fallback pin defaults remain required until the ENG-1635 hard cutover removes SDK LocalNet scripts. - // @fairmint/canton-dev-tools owns the shared pin set going forward. - const localnetBin = readFileSync(join(process.cwd(), 'bin', 'canton-localnet'), 'utf8'); - const spliceVersion = readFileSync(join(process.cwd(), 'libs', 'splice', 'VERSION'), 'utf8').trim(); - const quickstartRef = spawnSync('git', ['rev-parse', 'HEAD:libs/cn-quickstart'], { encoding: 'utf8' }); - - throwIfSpawnFailed('resolve pinned cn-quickstart revision', quickstartRef); - - if (!localnetBin.includes(`DEFAULT_SPLICE_VERSION="${spliceVersion}"`)) { - throw new Error(`bin/canton-localnet must default to the pinned Splice version ${spliceVersion}`); - } - - const expectedQuickstartRef = quickstartRef.stdout.trim(); - if (!localnetBin.includes(`DEFAULT_QUICKSTART_REF="${expectedQuickstartRef}"`)) { - throw new Error(`bin/canton-localnet must default to the pinned cn-quickstart revision ${expectedQuickstartRef}`); - } -} - -verifyPackagedLocalnetPins(); - const prepack = spawnSync('npm', ['run', 'prepack'], { encoding: 'utf8', }); @@ -218,6 +165,12 @@ for (const requiredPath of REQUIRED_RUNTIME_PATHS) { } } +for (const forbiddenPath of FORBIDDEN_LOCALNET_ENGINE_PATHS) { + if (packagePaths.has(forbiddenPath)) { + errors.push(`${forbiddenPath}: LocalNet engine must not ship in the SDK (use @fairmint/canton-dev-tools)`); + } +} + for (const file of result.files) { const reason = forbiddenPackagePathReason(file.path); if (reason) { @@ -233,11 +186,7 @@ if (errors.length > 0) { process.exit(1); } -verifyPackagedLocalnetBinary(); - console.log( `✓ ${result.name}@${result.version} package artifact is ${formatBytes(result.unpackedSize)} unpacked across ${result.files.length} files` ); -console.log( - `⚠ ENG-1635 soft migration: still publishing ${REQUIRED_SOFT_MIGRATION_LOCALNET_PATHS.join(', ')} (TODO: remove after hard cutover to @fairmint/canton-dev-tools)` -); +console.log('✓ LocalNet engine excluded (owned by @fairmint/canton-dev-tools)'); diff --git a/scripts/localnet-cloud.sh b/scripts/localnet-cloud.sh deleted file mode 100755 index 1d70e944..00000000 --- a/scripts/localnet-cloud.sh +++ /dev/null @@ -1,1182 +0,0 @@ -#!/usr/bin/env bash -# DEPRECATED (ENG-1635): LocalNet pin ownership is moving to @fairmint/canton-dev-tools. -# This script remains as a temporary fallback for bin/canton-localnet until a later hard cutover. -# Prefer: npx @fairmint/canton-dev-tools -# Do not delete this file in the soft-migration PR. -set -euo pipefail - -SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" -REPO_ROOT="$(cd "${SCRIPT_DIR}/.." && pwd)" -PROJECT_ROOT="${CANTON_LOCALNET_PROJECT_ROOT:-$(pwd)}" -QUICKSTART_DIR="${CANTON_LOCALNET_QUICKSTART_DIR:-${REPO_ROOT}/libs/cn-quickstart/quickstart}" -DOCKERD_PID_FILE="/tmp/localnet-dockerd.pid" -DOCKERD_LOG_FILE="/tmp/localnet-dockerd.log" -HOSTS_ENTRY="127.0.0.1 scan.localhost sv.localhost wallet.localhost" -CURL_CONNECT_TIMEOUT=2 -CURL_MAX_TIME=5 -LOCALNET_SPLICE_VERSION="${CANTON_LOCALNET_SPLICE_VERSION:-}" -LOCALNET_SCRIBE_VERSION="${CANTON_LOCALNET_SCRIBE_VERSION:-}" -LOCALNET_PROTOCOL_VERSION="${CANTON_LOCALNET_PROTOCOL_VERSION:-}" -VALIDATOR_READY_ATTEMPTS="${CANTON_LOCALNET_VALIDATOR_READY_ATTEMPTS:-90}" -SCAN_READY_ATTEMPTS="${CANTON_LOCALNET_SCAN_READY_ATTEMPTS:-90}" -SPLICE_CONFIG_PENDING_KEY="CANTON_NODE_SDK_SPLICE_CONFIG_PENDING" -SPLICE_CONFIG_CHANGED="false" - -log() { - printf '[localnet] %s\n' "$*" -} - -is_truthy() { - case "${1,,}" in - 1 | true | yes | on) - return 0 - ;; - *) - return 1 - ;; - esac -} - -require_command() { - local cmd="$1" - if ! command -v "${cmd}" >/dev/null 2>&1; then - log "Missing required command: ${cmd}" - exit 1 - fi -} - -require_positive_integer() { - local name="$1" - local value="$2" - - if [[ ! "${value}" =~ ^[1-9][0-9]*$ ]]; then - log "${name} must be a positive integer; received '${value}'." - exit 1 - fi -} - -validate_configuration() { - require_positive_integer "CANTON_LOCALNET_VALIDATOR_READY_ATTEMPTS" "${VALIDATOR_READY_ATTEMPTS}" - require_positive_integer "CANTON_LOCALNET_SCAN_READY_ATTEMPTS" "${SCAN_READY_ATTEMPTS}" -} - -ensure_sudo() { - if ! sudo_noninteractive_available; then - log "Passwordless sudo is required in this cloud environment." - exit 1 - fi -} - -sudo_noninteractive_available() { - command -v sudo >/dev/null 2>&1 && sudo -n true >/dev/null 2>&1 -} - -ensure_docker_packages() { - if command -v docker >/dev/null 2>&1 \ - && docker compose version >/dev/null 2>&1; then - return - fi - - if ! sudo_noninteractive_available || ! command -v apt-get >/dev/null 2>&1; then - log "Docker with Compose is required. Install Docker and start it, then retry." - exit 1 - fi - - log "Installing Docker packages..." - sudo apt-get update - sudo apt-get install -y docker.io docker-compose docker-compose-v2 -} - -ensure_legacy_iptables() { - if ! command -v update-alternatives >/dev/null 2>&1 \ - || [[ ! -x /usr/sbin/iptables-legacy || ! -x /usr/sbin/ip6tables-legacy ]]; then - log "iptables legacy binaries unavailable; skipping backend switch." - return - fi - - if ! iptables --version 2>/dev/null | grep -q 'legacy'; then - log "Switching iptables to legacy backend..." - sudo update-alternatives --set iptables /usr/sbin/iptables-legacy - fi - - if ! ip6tables --version 2>/dev/null | grep -q 'legacy'; then - log "Switching ip6tables to legacy backend..." - sudo update-alternatives --set ip6tables /usr/sbin/ip6tables-legacy - fi -} - -docker_ready() { - docker info >/dev/null 2>&1 || (sudo_noninteractive_available && sudo docker info >/dev/null 2>&1) -} - -configure_docker_socket_permissions() { - if [[ ! -S /var/run/docker.sock ]]; then - return - fi - - if ! sudo_noninteractive_available; then - return - fi - - sudo groupadd -f docker >/dev/null 2>&1 || true - sudo chown root:docker /var/run/docker.sock >/dev/null 2>&1 || true - sudo chmod 660 /var/run/docker.sock || true -} - -run_docker() { - if docker info >/dev/null 2>&1; then - docker "$@" - return - fi - if ! sudo_noninteractive_available || ! sudo docker info >/dev/null 2>&1; then - log "Docker daemon is not available." - exit 1 - fi - sudo docker "$@" -} - -resolve_quickstart_image_tag() { - local env_file="" - local splice_version="" - local parsed_value="" - - for env_file in "${QUICKSTART_DIR}/.env" "${QUICKSTART_DIR}/.env.local"; do - if [[ ! -f "${env_file}" ]]; then - continue - fi - - parsed_value="$(awk -F= -v key="SPLICE_VERSION" ' - $0 ~ /^[[:space:]]*#/ { next } - $1 == key { - value = substr($0, index($0, "=") + 1) - gsub(/^[[:space:]]+|[[:space:]]+$/, "", value) - gsub(/^"|"$/, "", value) - gsub(/^'\''|'\''$/, "", value) - parsed = value - } - END { - if (parsed != "") { - print parsed - } - } - ' "${env_file}")" - - if [[ -n "${parsed_value}" ]]; then - splice_version="${parsed_value}" - fi - done - - printf '%s' "${splice_version}" -} - -run_quickstart_command() { - local command="$1" - local docker_shim_dir="" - local quickstart_image_tag="" - local quickstart_path="${HOME}/.dpm/bin:${HOME}/.daml/bin:${PATH}" - local status=0 - - quickstart_image_tag="$(resolve_quickstart_image_tag)" - - if ! docker info >/dev/null 2>&1 && sudo docker info >/dev/null 2>&1; then - docker_shim_dir="$(mktemp -d "/tmp/canton-localnet-docker-shim.XXXXXX")" - cat >"${docker_shim_dir}/docker" <<'EOF' -#!/usr/bin/env bash -exec sudo -E docker "$@" -EOF - chmod +x "${docker_shim_dir}/docker" - log "Using sudo docker shim for quickstart command." - fi - - if [[ -n "${docker_shim_dir}" ]]; then - quickstart_path="${docker_shim_dir}:${quickstart_path}" - fi - - set +e - ( - cd "${QUICKSTART_DIR}" - MODULES_DIR="${QUICKSTART_DIR}/docker/modules" \ - LOCALNET_DIR="${QUICKSTART_DIR}/docker/modules/localnet" \ - IMAGE_TAG="${quickstart_image_tag}" \ - PATH="${quickstart_path}" \ - bash -lc "${command}" - ) - status=$? - set -e - - if [[ -n "${docker_shim_dir}" ]]; then - rm -rf "${docker_shim_dir}" - fi - - return "${status}" -} - -run_quickstart_make() { - local target="$1" - run_quickstart_command "make ${target}" -} - -run_infra_compose() { - local compose_args="$1" - local keycloak_compose_file="" - local keycloak_env_file="" - local keycloak_profile="" - - if [[ "$(current_auth_mode)" == "oauth2" ]]; then - keycloak_compose_file='-f "${MODULES_DIR}/keycloak/compose.yaml"' - keycloak_env_file='--env-file "${MODULES_DIR}/keycloak/compose.env"' - keycloak_profile='--profile keycloak' - fi - - run_quickstart_command "docker compose \ - -f \"\${LOCALNET_DIR}/compose.yaml\" \ - ${keycloak_compose_file} \ - --env-file .env \ - --env-file .env.local \ - --env-file \"\${LOCALNET_DIR}/compose.env\" \ - --env-file \"\${LOCALNET_DIR}/env/common.env\" \ - ${keycloak_env_file} \ - --profile app-provider \ - --profile app-user \ - --profile sv \ - ${keycloak_profile} \ - ${compose_args}" -} - -start_docker_daemon() { - local dockerd_pid="" - - if docker_ready; then - configure_docker_socket_permissions - return - fi - - if ! sudo_noninteractive_available; then - log "Docker daemon is not running. Start Docker and retry." - exit 1 - fi - - ensure_legacy_iptables - - log "Starting Docker daemon with vfs storage driver..." - sudo nohup dockerd --host=unix:///var/run/docker.sock --pidfile="${DOCKERD_PID_FILE}" --storage-driver=vfs >"${DOCKERD_LOG_FILE}" 2>&1 & - - for _ in $(seq 1 60); do - if sudo docker info >/dev/null 2>&1; then - configure_docker_socket_permissions - return - fi - if [[ -f "${DOCKERD_PID_FILE}" ]]; then - dockerd_pid="$(cat "${DOCKERD_PID_FILE}" 2>/dev/null || true)" - if [[ -n "${dockerd_pid}" ]] && ! ps -p "${dockerd_pid}" >/dev/null 2>&1; then - log "Docker daemon exited before becoming ready." - log "Inspect ${DOCKERD_LOG_FILE}." - exit 1 - fi - fi - sleep 1 - done - - log "Docker failed to start. Inspect ${DOCKERD_LOG_FILE}." - exit 1 -} - -ensure_submodules() { - if [[ -n "${CANTON_LOCALNET_QUICKSTART_DIR:-}" ]]; then - if [[ -d "${QUICKSTART_DIR}" ]]; then - return - fi - log "Configured CANTON_LOCALNET_QUICKSTART_DIR does not exist: ${QUICKSTART_DIR}" - exit 1 - fi - - if [[ -d "${REPO_ROOT}/libs/splice" && -d "${QUICKSTART_DIR}" ]]; then - return - fi - - require_command git - - if ! git -C "${REPO_ROOT}" rev-parse --is-inside-work-tree >/dev/null 2>&1; then - log "Missing localnet assets and not in a git checkout. Install from npm with bundled assets or clone SDK with submodules." - exit 1 - fi - - if [[ ! -d "${REPO_ROOT}/libs/splice" ]]; then - log "Initializing libs/splice submodule..." - git -C "${REPO_ROOT}" submodule update --init --depth 1 libs/splice - fi - - if [[ ! -d "${REPO_ROOT}/libs/cn-quickstart" ]]; then - log "Initializing libs/cn-quickstart submodule..." - git -C "${REPO_ROOT}" submodule update --init --recursive libs/cn-quickstart - fi - - if [[ ! -d "${QUICKSTART_DIR}" ]]; then - log "cn-quickstart directory not found after submodule init." - exit 1 - fi -} - -ensure_hosts_entries() { - if ! grep -Eq '(^|[[:space:]])scan\.localhost([[:space:]]|$)' /etc/hosts \ - || ! grep -Eq '(^|[[:space:]])sv\.localhost([[:space:]]|$)' /etc/hosts \ - || ! grep -Eq '(^|[[:space:]])wallet\.localhost([[:space:]]|$)' /etc/hosts; then - log "Adding localnet host aliases to /etc/hosts..." - if sudo_noninteractive_available; then - echo "${HOSTS_ENTRY}" | sudo tee -a /etc/hosts >/dev/null - return - fi - if [[ -t 0 ]] && command -v sudo >/dev/null 2>&1; then - echo "${HOSTS_ENTRY}" | sudo tee -a /etc/hosts >/dev/null - return - fi - log "Missing localnet host aliases. Add this line to /etc/hosts, then retry:" - log "${HOSTS_ENTRY}" - exit 1 - fi -} - -set_quickstart_env_value() { - local file="$1" - local key="$2" - local value="$3" - - if [[ ! -f "${file}" ]]; then - return - fi - - if grep -Eq "^${key}=" "${file}"; then - KEY="${key}" VALUE="${value}" perl -0pi -e ' - my $key = $ENV{"KEY"}; - my $value = $ENV{"VALUE"}; - s/^\Q$key\E=.*/$key=$value/m; - ' "${file}" - else - printf '\n%s=%s\n' "${key}" "${value}" >>"${file}" - fi -} - -patch_quickstart_canton_healthcheck() { - local healthcheck="${QUICKSTART_DIR}/docker/modules/localnet/docker/canton/health-check.sh" - - if [[ ! -f "${healthcheck}" ]]; then - log "Canton quickstart healthcheck not found: ${healthcheck}" - exit 1 - fi - - cat >"${healthcheck}" <<'EOF' -#!/bin/bash -set -eou pipefail - -http_check() { - local port="$1" - local path="$2" - local status="" - - echo "Checking http://localhost:${port}${path}" - exec 3<>"/dev/tcp/127.0.0.1/${port}" - printf 'GET %s HTTP/1.1\r\nHost: localhost\r\nConnection: close\r\n\r\n' "${path}" >&3 - IFS=$'\r' read -r status <&3 || true - exec 3<&- - exec 3>&- - - case "${status}" in - HTTP/*\ 2*) ;; - *) - echo "Unexpected status from ${port}${path}: ${status}" - return 1 - ;; - esac -} - -if [ "${APP_USER_PROFILE:-off}" = "on" ]; then - http_check "2${CANTON_HTTP_HEALTHCHECK_PORT_SUFFIX}" "/health" -fi -if [ "${APP_PROVIDER_PROFILE:-off}" = "on" ]; then - http_check "3${CANTON_HTTP_HEALTHCHECK_PORT_SUFFIX}" "/health" -fi -if [ "${SV_PROFILE:-off}" = "on" ]; then - http_check "4${CANTON_HTTP_HEALTHCHECK_PORT_SUFFIX}" "/health" -fi -EOF - chmod +x "${healthcheck}" -} - -patch_quickstart_splice_healthcheck() { - local healthcheck="${QUICKSTART_DIR}/docker/modules/localnet/docker/splice/health-check.sh" - - if [[ ! -f "${healthcheck}" ]]; then - log "Splice quickstart healthcheck not found: ${healthcheck}" - exit 1 - fi - - cat >"${healthcheck}" <<'EOF' -#!/bin/bash -set -eou pipefail - -http_check() { - local port="$1" - local path="$2" - local status="" - - echo "Checking http://localhost:${port}${path}" - exec 3<>"/dev/tcp/127.0.0.1/${port}" - printf 'GET %s HTTP/1.1\r\nHost: localhost\r\nConnection: close\r\n\r\n' "${path}" >&3 - IFS=$'\r' read -r status <&3 || true - exec 3<&- - exec 3>&- - - case "${status}" in - HTTP/*\ 2*) ;; - *) - echo "Unexpected status from ${port}${path}: ${status}" - return 1 - ;; - esac -} - -if [ "${APP_USER_PROFILE:-off}" = "on" ]; then - http_check "2${VALIDATOR_ADMIN_API_PORT_SUFFIX}" "/api/validator/readyz" -fi -if [ "${APP_PROVIDER_PROFILE:-off}" = "on" ]; then - http_check "3${VALIDATOR_ADMIN_API_PORT_SUFFIX}" "/api/validator/readyz" -fi -if [ "${SV_PROFILE:-off}" = "on" ]; then - http_check "4${VALIDATOR_ADMIN_API_PORT_SUFFIX}" "/api/validator/readyz" - http_check "5012" "/api/scan/readyz" - http_check "5014" "/api/sv/readyz" -fi -EOF - chmod +x "${healthcheck}" -} - -configure_quickstart_localnet() { - local env_file="" - local canton_conf="${QUICKSTART_DIR}/docker/modules/localnet/conf/canton/app.conf" - local splice_sv_conf="${QUICKSTART_DIR}/docker/modules/localnet/conf/splice/sv/app.conf" - - SPLICE_CONFIG_CHANGED="false" - if [[ "$(quickstart_env_value "${SPLICE_CONFIG_PENDING_KEY}")" == "true" ]]; then - SPLICE_CONFIG_CHANGED="true" - fi - - for env_file in "${QUICKSTART_DIR}/.env" "${QUICKSTART_DIR}/.env.local"; do - if [[ -n "${LOCALNET_SPLICE_VERSION}" ]]; then - set_quickstart_env_value "${env_file}" "SPLICE_VERSION" "${LOCALNET_SPLICE_VERSION}" - fi - if [[ -n "${LOCALNET_SCRIBE_VERSION}" ]]; then - set_quickstart_env_value "${env_file}" "SCRIBE_VERSION" "${LOCALNET_SCRIBE_VERSION}" - fi - if [[ -n "${CANTON_LOCALNET_DAML_RUNTIME_VERSION:-}" ]]; then - set_quickstart_env_value "${env_file}" "DAML_RUNTIME_VERSION" "${CANTON_LOCALNET_DAML_RUNTIME_VERSION}" - fi - done - - if [[ ! -f "${canton_conf}" ]]; then - log "Canton quickstart config not found: ${canton_conf}" - exit 1 - fi - - if [[ ! -f "${splice_sv_conf}" ]]; then - log "Splice SV quickstart config not found: ${splice_sv_conf}" - exit 1 - fi - - if ! grep -Eq '^[[:space:]]*canton\.scan-apps\.scan-app\.enable-forced-acs-snapshots[[:space:]]*=[[:space:]]*(true|yes|on)[[:space:]]*$' "${splice_sv_conf}"; then - cat >>"${splice_sv_conf}" <<'EOF' - -# LocalNet integration tests use this endpoint to create deterministic snapshot-backed fixtures. -canton.scan-apps.scan-app.enable-forced-acs-snapshots = true -EOF - set_quickstart_env_value "${QUICKSTART_DIR}/.env.local" "${SPLICE_CONFIG_PENDING_KEY}" "true" - SPLICE_CONFIG_CHANGED="true" - fi - - if [[ -n "${LOCALNET_PROTOCOL_VERSION}" ]]; then - PROTOCOL_VERSION="${LOCALNET_PROTOCOL_VERSION}" perl -0pi -e ' - my $protocol_version = $ENV{"PROTOCOL_VERSION"}; - s/initial-protocol-version = \d+/initial-protocol-version = $protocol_version/g; - s/(non-standard-config = yes\n)(?![[:space:]]*alpha-version-support = yes)/$1 alpha-version-support = yes\n/; - s/(initial-protocol-version = \d+\n)(?![[:space:]]*alpha-version-support = yes)/$1 alpha-version-support = yes\n/; - ' "${canton_conf}" - fi - - patch_quickstart_canton_healthcheck - patch_quickstart_splice_healthcheck -} - -requested_auth_mode() { - printf '%s' "${CANTON_LOCALNET_AUTH_MODE:-oauth2}" -} - -quickstart_env_value() { - local key="$1" - local parsed_value="" - - if [[ -f "${QUICKSTART_DIR}/.env.local" ]]; then - parsed_value="$(awk -F= -v key="${key}" ' - $0 ~ /^[[:space:]]*#/ { next } - $1 == key { - value = substr($0, index($0, "=") + 1) - gsub(/^[[:space:]]+|[[:space:]]+$/, "", value) - gsub(/^"|"$/, "", value) - gsub(/^'\''|'\''$/, "", value) - parsed = value - } - END { - if (parsed != "") { - print parsed - } - } - ' "${QUICKSTART_DIR}/.env.local")" - fi - - printf '%s' "${parsed_value}" -} - -configured_auth_mode() { - quickstart_env_value "AUTH_MODE" -} - -quickstart_profile_config_complete() { - local key="" - - for key in OBSERVABILITY_ENABLED AUTH_MODE PARTY_HINT TEST_MODE; do - if [[ -z "$(quickstart_env_value "${key}")" ]]; then - return 1 - fi - done - - return 0 -} - -verify_configured_auth_mode() { - local expected_auth_mode="$1" - local actual_auth_mode="" - - actual_auth_mode="$(configured_auth_mode)" - if [[ "${actual_auth_mode}" != "${expected_auth_mode}" ]]; then - log "cn-quickstart setup did not configure AUTH_MODE=${expected_auth_mode}; found '${actual_auth_mode:-unset}'." - exit 1 - fi -} - -current_auth_mode() { - local configured="" - - configured="$(configured_auth_mode)" - if [[ -n "${configured}" ]]; then - printf '%s' "${configured}" - return - fi - - requested_auth_mode -} - -run_quickstart_setup() { - local auth_mode="" - - auth_mode="$(requested_auth_mode)" - case "${auth_mode}" in - shared-secret) - log "Running cn-quickstart setup (shared-secret mode)..." - ( - cd "${QUICKSTART_DIR}" - printf 'Y\nn\n\n' | make setup || true - ) - verify_configured_auth_mode "shared-secret" - ;; - oauth2) - log "Running cn-quickstart setup (OAuth2 enabled)..." - ( - cd "${QUICKSTART_DIR}" - # Match CI behavior first, then fall back to prompt-based answers for newer setup flows. - echo "2" | make setup || true - if [[ "$(configured_auth_mode)" != "oauth2" ]]; then - printf 'y\ny\n\nn\n' | make setup - fi - ) - verify_configured_auth_mode "oauth2" - ;; - *) - log "Unsupported CANTON_LOCALNET_AUTH_MODE: ${auth_mode}" - exit 1 - ;; - esac -} - -quickstart_setup() { - if [[ ! -f "${QUICKSTART_DIR}/.env.local" ]]; then - run_quickstart_setup - elif ! quickstart_profile_config_complete; then - log "Regenerating incomplete cn-quickstart config." - rm -f "${QUICKSTART_DIR}/.env.local" - run_quickstart_setup - elif [[ -n "${CANTON_LOCALNET_AUTH_MODE:-}" && "$(configured_auth_mode)" != "${CANTON_LOCALNET_AUTH_MODE}" ]]; then - log "Regenerating cn-quickstart config for CANTON_LOCALNET_AUTH_MODE=${CANTON_LOCALNET_AUTH_MODE}." - rm -f "${QUICKSTART_DIR}/.env.local" - run_quickstart_setup - else - log "Reusing existing ${QUICKSTART_DIR}/.env.local." - fi - - if [[ ! -f "${QUICKSTART_DIR}/.env.local" ]]; then - log "cn-quickstart setup failed: ${QUICKSTART_DIR}/.env.local was not created." - exit 1 - fi - - configure_quickstart_localnet - - if quickstart_infra_only_enabled; then - log "Skipping Daml SDK install for infrastructure-only localnet." - return - fi - - if [[ ! -x "${HOME}/.daml/bin/daml" ]]; then - log "Installing Daml SDK..." - ( - cd "${QUICKSTART_DIR}" - make install-daml-sdk - ) - else - log "Reusing existing Daml SDK at ${HOME}/.daml/bin/daml." - fi -} - -quickstart_fast_start_enabled() { - is_truthy "${CANTON_LOCALNET_FAST_START:-true}" -} - -quickstart_force_full_start() { - is_truthy "${CANTON_LOCALNET_FORCE_FULL_START:-false}" -} - -quickstart_infra_only_enabled() { - is_truthy "${CANTON_LOCALNET_INFRA_ONLY:-false}" -} - -quickstart_build_artifacts_ready() { - local missing_paths=() - - if [[ ! -f "${QUICKSTART_DIR}/backend/build/distributions/backend.tar" ]]; then - missing_paths+=("backend/build/distributions/backend.tar") - fi - - if [[ ! -d "${QUICKSTART_DIR}/frontend/dist" ]]; then - missing_paths+=("frontend/dist") - fi - - if ! compgen -G "${QUICKSTART_DIR}/daml/licensing/.daml/dist/*.dar" >/dev/null; then - missing_paths+=("daml/licensing/.daml/dist/*.dar") - fi - - if ! compgen -G "${QUICKSTART_DIR}/backend/build/otel-agent/opentelemetry-javaagent-*.jar" >/dev/null; then - missing_paths+=("backend/build/otel-agent/opentelemetry-javaagent-*.jar") - fi - - if [[ ${#missing_paths[@]} -gt 0 ]]; then - log "Fast start unavailable; missing quickstart build artifacts: ${missing_paths[*]}" - return 1 - fi - - return 0 -} - -extract_quickstart_compose_up_command() { - ( - cd "${QUICKSTART_DIR}" - make -n start 2>/dev/null | awk '/docker compose .* up -d --no-recreate/{line=$0} END{print line}' - ) -} - -try_fast_start_localnet() { - local compose_up_command="" - - compose_up_command="$(extract_quickstart_compose_up_command)" - if [[ -z "${compose_up_command}" ]]; then - log "Unable to determine quickstart compose startup command." - return 1 - fi - - log "Starting cn-quickstart with fast path (skip quickstart rebuild)..." - run_quickstart_command "${compose_up_command}" -} - -start_infra_only_localnet() { - log "Starting cn-quickstart LocalNet infrastructure only (skip quickstart app, PQS, and onboarding)." - run_infra_compose 'up -d --no-recreate' -} - -splice_container_running() { - local running="" - - running="$(run_docker inspect --format '{{.State.Running}}' splice 2>/dev/null || true)" - [[ "${running}" == "true" ]] -} - -recreate_splice_after_config_change() { - local should_recreate="$1" - - if [[ "${should_recreate}" != "true" ]]; then - return - fi - - log "Recreating the running Splice service to apply updated LocalNet configuration." - run_infra_compose 'up -d --no-deps --force-recreate splice' -} - -mark_splice_config_applied() { - if [[ "${SPLICE_CONFIG_CHANGED}" != "true" ]]; then - return - fi - - set_quickstart_env_value "${QUICKSTART_DIR}/.env.local" "${SPLICE_CONFIG_PENDING_KEY}" "false" - SPLICE_CONFIG_CHANGED="false" -} - -stop_infra_only_localnet() { - if [[ ! -f "${QUICKSTART_DIR}/.env.local" ]]; then - return - fi - - log "Stopping cn-quickstart LocalNet infrastructure-only stack..." - run_infra_compose down || true -} - -wait_for_services() { - local auth_mode="" - local code="" - local ledger_code="" - - auth_mode="$(current_auth_mode)" - - if [[ "${auth_mode}" == "oauth2" ]]; then - log "Waiting for Keycloak..." - for _ in $(seq 1 30); do - if curl --connect-timeout "${CURL_CONNECT_TIMEOUT}" --max-time "${CURL_MAX_TIME}" -fsS http://localhost:8082/realms/AppProvider >/dev/null 2>&1; then - break - fi - sleep 2 - done - if ! curl --connect-timeout "${CURL_CONNECT_TIMEOUT}" --max-time "${CURL_MAX_TIME}" -fsS http://localhost:8082/realms/AppProvider >/dev/null 2>&1; then - log "Keycloak did not become ready." - exit 1 - fi - else - log "Checking for Keycloak (optional in ${auth_mode} mode)..." - for _ in $(seq 1 5); do - if curl --connect-timeout "${CURL_CONNECT_TIMEOUT}" --max-time "${CURL_MAX_TIME}" -fsS http://localhost:8082/realms/AppProvider >/dev/null 2>&1; then - log "Keycloak is ready." - break - fi - sleep 2 - done - fi - - log "Waiting for Validator API..." - for _ in $(seq 1 "${VALIDATOR_READY_ATTEMPTS}"); do - code="$(curl --connect-timeout "${CURL_CONNECT_TIMEOUT}" --max-time "${CURL_MAX_TIME}" -sS -o /dev/null -w '%{http_code}' http://localhost:3903/api/validator/v0/wallet/user-status || true)" - if [[ "${code}" == "200" || "${code}" == "401" ]]; then - break - fi - sleep 2 - done - code="$(curl --connect-timeout "${CURL_CONNECT_TIMEOUT}" --max-time "${CURL_MAX_TIME}" -sS -o /dev/null -w '%{http_code}' http://localhost:3903/api/validator/v0/wallet/user-status || true)" - if [[ "${code}" != "200" && "${code}" != "401" ]]; then - log "Validator API did not become ready." - exit 1 - fi - - log "Waiting for Scan API..." - for _ in $(seq 1 "${SCAN_READY_ATTEMPTS}"); do - if curl --connect-timeout "${CURL_CONNECT_TIMEOUT}" --max-time "${CURL_MAX_TIME}" -fsS http://scan.localhost:4000/api/scan/v0/dso-party-id >/dev/null 2>&1; then - break - fi - sleep 2 - done - if ! curl --connect-timeout "${CURL_CONNECT_TIMEOUT}" --max-time "${CURL_MAX_TIME}" -fsS http://scan.localhost:4000/api/scan/v0/dso-party-id >/dev/null 2>&1; then - log "Scan API did not become ready." - exit 1 - fi - - log "Waiting for Ledger JSON API..." - for _ in $(seq 1 60); do - ledger_code="$(curl --connect-timeout "${CURL_CONNECT_TIMEOUT}" --max-time "${CURL_MAX_TIME}" -sS -o /dev/null -w '%{http_code}' http://localhost:3975/v2/version || true)" - if [[ "${ledger_code}" == "200" || "${ledger_code}" == "401" ]]; then - break - fi - sleep 2 - done - ledger_code="$(curl --connect-timeout "${CURL_CONNECT_TIMEOUT}" --max-time "${CURL_MAX_TIME}" -sS -o /dev/null -w '%{http_code}' http://localhost:3975/v2/version || true)" - if [[ "${ledger_code}" != "200" && "${ledger_code}" != "401" ]]; then - log "Ledger JSON API did not become ready (HTTP ${ledger_code})." - exit 1 - fi - - log "All localnet services are ready." -} - -start_localnet() { - local should_recreate_splice="false" - - if [[ "${SPLICE_CONFIG_CHANGED}" == "true" ]] && splice_container_running; then - should_recreate_splice="true" - fi - - if quickstart_force_full_start; then - log "Forcing full cn-quickstart start (CANTON_LOCALNET_FORCE_FULL_START=true)." - run_quickstart_make start - recreate_splice_after_config_change "${should_recreate_splice}" - wait_for_services - mark_splice_config_applied - return - fi - - if quickstart_infra_only_enabled; then - start_infra_only_localnet - recreate_splice_after_config_change "${should_recreate_splice}" - wait_for_services - mark_splice_config_applied - return - fi - - if quickstart_fast_start_enabled && quickstart_build_artifacts_ready; then - if try_fast_start_localnet; then - recreate_splice_after_config_change "${should_recreate_splice}" - wait_for_services - mark_splice_config_applied - return - fi - log "Fast start failed; falling back to full cn-quickstart start." - fi - - log "Starting cn-quickstart with full build..." - run_quickstart_make start - recreate_splice_after_config_change "${should_recreate_splice}" - wait_for_services - mark_splice_config_applied -} - -stop_localnet() { - if [[ ! -d "${QUICKSTART_DIR}" ]]; then - log "cn-quickstart directory not found; nothing to stop." - stop_managed_dockerd - return - fi - - log "Stopping cn-quickstart..." - stop_infra_only_localnet - if [[ -f "${QUICKSTART_DIR}/Makefile" ]]; then - run_quickstart_make stop || true - else - log "Quickstart Makefile not found; skipping quickstart stop target." - fi - stop_managed_dockerd -} - -stop_managed_dockerd() { - local pid="" - local cmd="" - - if [[ ! -f "${DOCKERD_PID_FILE}" ]]; then - return - fi - - pid="$(cat "${DOCKERD_PID_FILE}" 2>/dev/null || true)" - if [[ -z "${pid}" ]]; then - rm -f "${DOCKERD_PID_FILE}" "${DOCKERD_LOG_FILE}" - return - fi - - if ! ps -p "${pid}" >/dev/null 2>&1; then - rm -f "${DOCKERD_PID_FILE}" "${DOCKERD_LOG_FILE}" - return - fi - - cmd="$(ps -p "${pid}" -o comm= 2>/dev/null | tr -d '[:space:]')" - if [[ "${cmd}" != "dockerd" ]]; then - log "PID ${pid} is not dockerd; skipping daemon cleanup." - rm -f "${DOCKERD_PID_FILE}" - return - fi - - if ! sudo -n true >/dev/null 2>&1; then - log "Cannot stop managed dockerd without passwordless sudo." - return - fi - - log "Stopping managed dockerd (pid ${pid})..." - sudo kill -TERM "${pid}" >/dev/null 2>&1 || true - for _ in $(seq 1 10); do - if ! ps -p "${pid}" >/dev/null 2>&1; then - break - fi - sleep 1 - done - if ps -p "${pid}" >/dev/null 2>&1; then - sudo kill -KILL "${pid}" >/dev/null 2>&1 || true - fi - rm -f "${DOCKERD_PID_FILE}" "${DOCKERD_LOG_FILE}" -} - -status_localnet() { - if docker_ready; then - log "Docker daemon is running." - else - log "Docker daemon is not running." - exit 1 - fi - - run_docker ps --format 'table {{.Names}}\t{{.Status}}\t{{.Ports}}' - echo - - keycloak_ok="no" - validator_ok="no" - scan_ok="no" - ledger_ok="no" - if curl --connect-timeout "${CURL_CONNECT_TIMEOUT}" --max-time "${CURL_MAX_TIME}" -fsS http://localhost:8082/realms/AppProvider >/dev/null 2>&1; then - keycloak_ok="yes" - fi - validator_code="$(curl --connect-timeout "${CURL_CONNECT_TIMEOUT}" --max-time "${CURL_MAX_TIME}" -sS -o /dev/null -w '%{http_code}' http://localhost:3903/api/validator/v0/wallet/user-status || true)" - if [[ "${validator_code}" == "200" || "${validator_code}" == "401" ]]; then - validator_ok="yes" - fi - if curl --connect-timeout "${CURL_CONNECT_TIMEOUT}" --max-time "${CURL_MAX_TIME}" -fsS http://scan.localhost:4000/api/scan/v0/dso-party-id >/dev/null 2>&1; then - scan_ok="yes" - fi - ledger_code="$(curl --connect-timeout "${CURL_CONNECT_TIMEOUT}" --max-time "${CURL_MAX_TIME}" -sS -o /dev/null -w '%{http_code}' http://localhost:3975/v2/version || true)" - if [[ "${ledger_code}" == "200" || "${ledger_code}" == "401" ]]; then - ledger_ok="yes" - fi - - printf 'Keycloak ready: %s\n' "${keycloak_ok}" - printf 'Validator ready: %s (HTTP %s)\n' "${validator_ok}" "${validator_code:-n/a}" - printf 'Scan ready: %s\n' "${scan_ok}" - printf 'Ledger JSON API ready: %s (HTTP %s)\n' "${ledger_ok}" "${ledger_code:-n/a}" -} - -show_localnet_logs() { - log "==================== Docker Containers ====================" - if docker_ready; then - run_docker ps -a || true - else - log "Docker daemon is not running." - fi - - echo - log "==================== Docker Compose Logs ====================" - if [[ -f "${QUICKSTART_DIR}/.env.local" ]]; then - run_infra_compose 'logs --tail=100' || true - elif [[ -d "${QUICKSTART_DIR}" ]]; then - log "Quickstart local env not found; skipping compose logs: ${QUICKSTART_DIR}/.env.local" - else - log "cn-quickstart directory not found: ${QUICKSTART_DIR}" - fi - - echo - log "==================== Canton Logs ====================" - if [[ -f "${QUICKSTART_DIR}/logs/canton.log" ]]; then - tail -100 "${QUICKSTART_DIR}/logs/canton.log" || true - else - log "Canton log not found: ${QUICKSTART_DIR}/logs/canton.log" - fi - - if [[ -f "${DOCKERD_LOG_FILE}" ]]; then - echo - log "==================== Managed Docker Daemon Logs ====================" - tail -100 "${DOCKERD_LOG_FILE}" || true - fi -} - -run_smoke() { - local auth_mode="" - local validator_code="" - local ledger_code="" - - log "Running localnet smoke checks..." - auth_mode="$(current_auth_mode)" - - if curl --connect-timeout "${CURL_CONNECT_TIMEOUT}" --max-time "${CURL_MAX_TIME}" -fsS http://localhost:8082/realms/AppProvider >/dev/null 2>&1; then - log "Keycloak is reachable." - elif [[ "${auth_mode}" == "oauth2" ]]; then - log "Keycloak is not reachable." - exit 1 - else - log "Keycloak not detected (expected in ${auth_mode} mode)." - fi - - validator_code="$(curl --connect-timeout "${CURL_CONNECT_TIMEOUT}" --max-time "${CURL_MAX_TIME}" -sS -o /dev/null -w '%{http_code}' http://localhost:3903/api/validator/v0/wallet/user-status || true)" - if [[ "${validator_code}" != "200" && "${validator_code}" != "401" ]]; then - log "Validator API is not reachable (HTTP ${validator_code})." - exit 1 - fi - - if ! curl --connect-timeout "${CURL_CONNECT_TIMEOUT}" --max-time "${CURL_MAX_TIME}" -fsS http://scan.localhost:4000/api/scan/v0/dso-party-id >/dev/null 2>&1; then - log "Scan API is not reachable." - exit 1 - fi - - ledger_code="$(curl --connect-timeout "${CURL_CONNECT_TIMEOUT}" --max-time "${CURL_MAX_TIME}" -sS -o /dev/null -w '%{http_code}' http://localhost:3975/v2/version || true)" - if [[ "${ledger_code}" != "200" && "${ledger_code}" != "401" ]]; then - log "Ledger JSON API is not reachable (HTTP ${ledger_code})." - exit 1 - fi - - log "Smoke checks passed." -} - -read_npm_script() { - local target_dir="$1" - local script_name="$2" - - if [[ ! -f "${target_dir}/package.json" ]]; then - return - fi - - node -e 'const fs=require("fs"); const pkgPath=process.argv[1]; const scriptName=process.argv[2]; try { const pkg = JSON.parse(fs.readFileSync(pkgPath, "utf8")); const script = pkg?.scripts?.[scriptName]; if (typeof script === "string") process.stdout.write(script); } catch {}' "${target_dir}/package.json" "${script_name}" -} - -script_is_recursive_localnet_test() { - local script_value="$1" - echo "${script_value}" | grep -Eq '(^|[[:space:]])(canton-localnet[[:space:]]+test|[^[:space:]]*localnet-cloud\.sh[[:space:]]+test)([[:space:]]|$)' -} - -run_integration_tests() { - local script_value="" - - if [[ -n "${CANTON_LOCALNET_TEST_CMD:-}" ]]; then - log "Running custom integration command from CANTON_LOCALNET_TEST_CMD..." - ( - cd "${PROJECT_ROOT}" - bash -lc "${CANTON_LOCALNET_TEST_CMD}" - ) - return - fi - - script_value="$(read_npm_script "${PROJECT_ROOT}" "test:integration")" - if [[ -n "${script_value}" ]]; then - if script_is_recursive_localnet_test "${script_value}"; then - log "Skipping recursive test:integration script." - else - log "Running project integration tests: npm run test:integration" - ( - cd "${PROJECT_ROOT}" - npm run test:integration - ) - return - fi - fi - - script_value="$(read_npm_script "${PROJECT_ROOT}" "test:localnet")" - if [[ -n "${script_value}" ]]; then - if script_is_recursive_localnet_test "${script_value}"; then - log "Skipping recursive test:localnet script." - else - log "Running project integration tests: npm run test:localnet" - ( - cd "${PROJECT_ROOT}" - npm run test:localnet - ) - return - fi - fi - - if [[ "${PROJECT_ROOT}" == "${REPO_ROOT}" && -d "${REPO_ROOT}/test/integration/localnet" && -d "${REPO_ROOT}/node_modules" ]]; then - log "Running bundled SDK localnet integration tests..." - ( - cd "${REPO_ROOT}" - npm test -- test/integration/localnet - ) - return - fi - - log "No integration test command configured; skipping test step." -} - -usage() { - cat <<'USAGE' -Usage: scripts/localnet/localnet-cloud.sh - -Commands: - setup Install prerequisites, init submodules, configure quickstart - start Start localnet and wait for ready endpoints - stop Stop localnet services - logs Show localnet diagnostic logs - status Show docker + endpoint status - smoke Run endpoint smoke checks - test Run project integration tests (if configured) - verify Run setup + start + smoke + test - -Environment: - CANTON_LOCALNET_FAST_START=true|false Enable fast startup path (default: true) - CANTON_LOCALNET_FORCE_FULL_START=true|false Force full startup with rebuild - CANTON_LOCALNET_INFRA_ONLY=true|false Start only LocalNet + Keycloak infrastructure - CANTON_LOCALNET_AUTH_MODE=oauth2|shared-secret - CANTON_LOCALNET_QUICKSTART_DIR= Use an existing cn-quickstart/quickstart directory -USAGE -} - -main() { - if [[ "${1:-}" == "" ]]; then - usage - exit 1 - fi - - case "$1" in - setup) - validate_configuration - ensure_docker_packages - start_docker_daemon - ensure_submodules - ensure_hosts_entries - quickstart_setup - ;; - start) - validate_configuration - require_command curl - ensure_docker_packages - start_docker_daemon - ensure_submodules - ensure_hosts_entries - quickstart_setup - start_localnet - ;; - stop) - stop_localnet - ;; - logs) - show_localnet_logs - ;; - status) - require_command curl - status_localnet - ;; - smoke) - require_command curl - run_smoke - ;; - test) - run_integration_tests - ;; - verify) - validate_configuration - require_command curl - ensure_docker_packages - start_docker_daemon - ensure_submodules - ensure_hosts_entries - quickstart_setup - start_localnet - run_smoke - run_integration_tests - ;; - *) - usage - exit 1 - ;; - esac -} - -if [[ "${BASH_SOURCE[0]}" == "$0" ]]; then - main "$@" -fi diff --git a/scripts/localnet-status.sh b/scripts/localnet-status.sh deleted file mode 100755 index 6462942d..00000000 --- a/scripts/localnet-status.sh +++ /dev/null @@ -1,5 +0,0 @@ -#!/usr/bin/env bash -set -euo pipefail - -SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" -bash "${SCRIPT_DIR}/localnet-cloud.sh" status diff --git a/scripts/localnet/localnet-cloud.sh b/scripts/localnet/localnet-cloud.sh deleted file mode 100644 index 04812c53..00000000 --- a/scripts/localnet/localnet-cloud.sh +++ /dev/null @@ -1,5 +0,0 @@ -#!/usr/bin/env bash -set -euo pipefail - -SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" -bash "${SCRIPT_DIR}/../localnet-cloud.sh" "$@" diff --git a/scripts/localnet/localnet-status.sh b/scripts/localnet/localnet-status.sh deleted file mode 100644 index 4a7b5966..00000000 --- a/scripts/localnet/localnet-status.sh +++ /dev/null @@ -1,5 +0,0 @@ -#!/usr/bin/env bash -set -euo pipefail - -SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" -bash "${SCRIPT_DIR}/../localnet-status.sh" diff --git a/scripts/localnet/setup-localnet.sh b/scripts/localnet/setup-localnet.sh deleted file mode 100644 index 5c9d77ef..00000000 --- a/scripts/localnet/setup-localnet.sh +++ /dev/null @@ -1,5 +0,0 @@ -#!/usr/bin/env bash -set -euo pipefail - -SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" -bash "${SCRIPT_DIR}/../setup-localnet.sh" diff --git a/scripts/localnet/setup-quickstart-localnet.sh b/scripts/localnet/setup-quickstart-localnet.sh deleted file mode 100644 index 4fbcd805..00000000 --- a/scripts/localnet/setup-quickstart-localnet.sh +++ /dev/null @@ -1,5 +0,0 @@ -#!/usr/bin/env bash -set -euo pipefail - -SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" -bash "${SCRIPT_DIR}/../setup-quickstart-localnet.sh" diff --git a/scripts/localnet/start-localnet.sh b/scripts/localnet/start-localnet.sh deleted file mode 100644 index e0a44593..00000000 --- a/scripts/localnet/start-localnet.sh +++ /dev/null @@ -1,5 +0,0 @@ -#!/usr/bin/env bash -set -euo pipefail - -SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" -bash "${SCRIPT_DIR}/../start-localnet.sh" diff --git a/scripts/localnet/stop-localnet.sh b/scripts/localnet/stop-localnet.sh deleted file mode 100644 index 012e24e1..00000000 --- a/scripts/localnet/stop-localnet.sh +++ /dev/null @@ -1,5 +0,0 @@ -#!/usr/bin/env bash -set -euo pipefail - -SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" -bash "${SCRIPT_DIR}/../stop-localnet.sh" diff --git a/scripts/setup-localnet.sh b/scripts/setup-localnet.sh deleted file mode 100755 index 968ec649..00000000 --- a/scripts/setup-localnet.sh +++ /dev/null @@ -1,5 +0,0 @@ -#!/usr/bin/env bash -set -euo pipefail - -SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" -bash "${SCRIPT_DIR}/setup-quickstart-localnet.sh" diff --git a/scripts/setup-quickstart-localnet.sh b/scripts/setup-quickstart-localnet.sh deleted file mode 100755 index 1fa690da..00000000 --- a/scripts/setup-quickstart-localnet.sh +++ /dev/null @@ -1,5 +0,0 @@ -#!/usr/bin/env bash -set -euo pipefail - -SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" -bash "${SCRIPT_DIR}/localnet-cloud.sh" setup diff --git a/scripts/start-localnet.sh b/scripts/start-localnet.sh deleted file mode 100755 index 433b518b..00000000 --- a/scripts/start-localnet.sh +++ /dev/null @@ -1,5 +0,0 @@ -#!/usr/bin/env bash -set -euo pipefail - -SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" -bash "${SCRIPT_DIR}/localnet-cloud.sh" start diff --git a/scripts/stop-localnet.sh b/scripts/stop-localnet.sh deleted file mode 100755 index dac53bfc..00000000 --- a/scripts/stop-localnet.sh +++ /dev/null @@ -1,5 +0,0 @@ -#!/usr/bin/env bash -set -euo pipefail - -SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" -bash "${SCRIPT_DIR}/localnet-cloud.sh" stop diff --git a/test/integration/localnet/ledger-api/dars.test.ts b/test/integration/localnet/ledger-api/dars.test.ts index 431e80b4..d6625336 100644 --- a/test/integration/localnet/ledger-api/dars.test.ts +++ b/test/integration/localnet/ledger-api/dars.test.ts @@ -1,12 +1,12 @@ /** End-to-end validation for the canonical Ledger JSON API DAR endpoints and wire formats. */ -import { readFile, readdir } from 'node:fs/promises'; -import path from 'node:path'; -import { ApiError, SynchronizerId } from '../../../../src'; import { getLocalnetNonAdminLedgerClient, getLocalnetParticipantAdminLedgerClient, -} from '../../../utils/localnetLedgerClients'; +} from '@fairmint/canton-dev-tools/testing'; +import { readFile, readdir } from 'node:fs/promises'; +import path from 'node:path'; +import { ApiError, SynchronizerId } from '../../../../src'; const QUICKSTART_DARS_DIRECTORY = path.resolve(__dirname, '../../../..', 'libs/cn-quickstart/quickstart/daml/dars'); const ABSENT_VALID_DAR_PATH = path.resolve( diff --git a/test/integration/localnet/ledger-api/interactive-submission.test.ts b/test/integration/localnet/ledger-api/interactive-submission.test.ts index b02bf338..2ff53537 100644 --- a/test/integration/localnet/ledger-api/interactive-submission.test.ts +++ b/test/integration/localnet/ledger-api/interactive-submission.test.ts @@ -14,7 +14,7 @@ import { waitForCompletionWithMetadata, } from '../../../../src'; import type { InteractiveSubmissionExecuteRequest } from '../../../../src/clients/ledger-json-api/schemas/api/interactive-submission'; -import { buildIntegrationTestClientConfig, retry } from '../../../utils/testConfig'; +import { buildIntegrationTestClientConfig, retry } from '@fairmint/canton-dev-tools/testing'; import { getClient } from './setup'; const WALLET_APP_INSTALL_TEMPLATE = '#splice-wallet:Splice.Wallet.Install:WalletAppInstall'; diff --git a/test/integration/localnet/ledger-api/paid-traffic-cost.test.ts b/test/integration/localnet/ledger-api/paid-traffic-cost.test.ts index 58033e60..a011e7f6 100644 --- a/test/integration/localnet/ledger-api/paid-traffic-cost.test.ts +++ b/test/integration/localnet/ledger-api/paid-traffic-cost.test.ts @@ -10,7 +10,7 @@ import { CompletionStreamResponseSchema } from '../../../../src/clients/ledger-j import { EnvLoader } from '../../../../src/core/config/EnvLoader'; import { ConfigurationError } from '../../../../src/core/errors'; import { getPaidTrafficCostFromCompletion } from '../../../../src/utils/traffic/paid-traffic-cost'; -import { buildIntegrationTestClientConfig } from '../../../utils/testConfig'; +import { buildIntegrationTestClientConfig } from '@fairmint/canton-dev-tools/testing'; import { getClient } from './setup'; const WALLET_APP_INSTALL_TEMPLATE_SUFFIX = 'Splice.Wallet.Install:WalletAppInstall'; diff --git a/test/integration/localnet/ledger-api/setup.ts b/test/integration/localnet/ledger-api/setup.ts index 62c7eaaf..db211b0a 100644 --- a/test/integration/localnet/ledger-api/setup.ts +++ b/test/integration/localnet/ledger-api/setup.ts @@ -3,7 +3,7 @@ import { CantonRuntime, LedgerJsonApiClient } from '../../../../src'; import { EnvLoader } from '../../../../src/core/config/EnvLoader'; import { ConfigurationError } from '../../../../src/core/errors'; -import { buildIntegrationTestClientConfig } from '../../../utils/testConfig'; +import { buildIntegrationTestClientConfig } from '@fairmint/canton-dev-tools/testing'; let client: LedgerJsonApiClient | null = null; const WALLET_APP_INSTALL_TEMPLATE_SUFFIX = 'Splice.Wallet.Install:WalletAppInstall'; diff --git a/test/integration/localnet/scan-api/setup.ts b/test/integration/localnet/scan-api/setup.ts index cce8b36c..2518083e 100644 --- a/test/integration/localnet/scan-api/setup.ts +++ b/test/integration/localnet/scan-api/setup.ts @@ -1,7 +1,7 @@ /** Shared setup for ScanApiClient integration tests. */ import { CantonRuntime, ScanApiClient } from '../../../../src'; -import { buildIntegrationTestClientConfig } from '../../../utils/testConfig'; +import { buildIntegrationTestClientConfig } from '@fairmint/canton-dev-tools/testing'; let client: ScanApiClient | null = null; diff --git a/test/integration/localnet/scan-api/snapshot-timestamps.test.ts b/test/integration/localnet/scan-api/snapshot-timestamps.test.ts index 871245b8..9e0d21a8 100644 --- a/test/integration/localnet/scan-api/snapshot-timestamps.test.ts +++ b/test/integration/localnet/scan-api/snapshot-timestamps.test.ts @@ -2,7 +2,7 @@ import { randomUUID } from 'node:crypto'; -import { retry } from '../../../utils/testConfig'; +import { retry } from '@fairmint/canton-dev-tools/testing'; import { ensureValidatorUserOnboarded, getClient as getValidatorClient, diff --git a/test/integration/localnet/validator-api/scan-proxy.test.ts b/test/integration/localnet/validator-api/scan-proxy.test.ts index 4f75eeec..39d882ba 100644 --- a/test/integration/localnet/validator-api/scan-proxy.test.ts +++ b/test/integration/localnet/validator-api/scan-proxy.test.ts @@ -7,7 +7,7 @@ import { randomUUID } from 'node:crypto'; import { ApiError } from '../../../../src'; import type { components } from '../../../../src/generated/apps/validator/src/main/openapi/scan-proxy'; -import { retry } from '../../../utils/testConfig'; +import { retry } from '@fairmint/canton-dev-tools/testing'; import { getClient as getScanClient } from '../scan-api/setup'; import { ensureValidatorUserOnboarded, getClient, VALIDATOR_ONBOARDING_HOOK_TIMEOUT_MS } from './setup'; diff --git a/test/integration/localnet/validator-api/setup.ts b/test/integration/localnet/validator-api/setup.ts index 5afc5780..51278aff 100644 --- a/test/integration/localnet/validator-api/setup.ts +++ b/test/integration/localnet/validator-api/setup.ts @@ -5,7 +5,7 @@ import { tmpdir } from 'node:os'; import { join } from 'node:path'; import { ApiError, CantonRuntime, ValidatorApiClient } from '../../../../src'; -import { buildIntegrationTestClientConfig, retry } from '../../../utils/testConfig'; +import { buildIntegrationTestClientConfig, retry } from '@fairmint/canton-dev-tools/testing'; const DEFAULT_VALIDATOR_USER_NAME = 'app-provider-validator'; const VALIDATOR_ONBOARDING_TIMEOUT_MS = 240_000; diff --git a/test/integration/localnet/validator-api/wallet.test.ts b/test/integration/localnet/validator-api/wallet.test.ts index e252669a..71db7fb2 100644 --- a/test/integration/localnet/validator-api/wallet.test.ts +++ b/test/integration/localnet/validator-api/wallet.test.ts @@ -3,7 +3,7 @@ import { randomUUID } from 'node:crypto'; import { type ValidatorWalletTransaction } from '../../../../src'; -import { retry } from '../../../utils/testConfig'; +import { retry } from '@fairmint/canton-dev-tools/testing'; import { getClient as getLedgerClient } from '../ledger-api/setup'; import { ensureValidatorUserOnboarded, getClient, VALIDATOR_ONBOARDING_HOOK_TIMEOUT_MS } from './setup'; diff --git a/test/unit/scripts/canton-localnet.test.ts b/test/unit/scripts/canton-localnet.test.ts deleted file mode 100644 index 35294647..00000000 --- a/test/unit/scripts/canton-localnet.test.ts +++ /dev/null @@ -1,160 +0,0 @@ -import { execFileSync } from 'node:child_process'; -import { chmodSync, copyFileSync, mkdirSync, mkdtempSync, rmSync, writeFileSync } from 'node:fs'; -import { tmpdir } from 'node:os'; -import { resolve } from 'node:path'; - -const REPO_ROOT = resolve(__dirname, '../../..'); - -function runPackagedLocalnetWithVersion(version: string): string { - const packageRoot = mkdtempSync(resolve(tmpdir(), 'canton-localnet-bin-')); - const localnetBin = resolve(packageRoot, 'bin/canton-localnet'); - - mkdirSync(resolve(packageRoot, 'bin'), { recursive: true }); - mkdirSync(resolve(packageRoot, 'libs/splice'), { recursive: true }); - mkdirSync(resolve(packageRoot, 'scripts'), { recursive: true }); - copyFileSync(resolve(REPO_ROOT, 'bin/canton-localnet'), localnetBin); - chmodSync(localnetBin, 0o755); - writeFileSync(resolve(packageRoot, 'libs/splice/VERSION'), version); - writeFileSync(resolve(packageRoot, 'scripts/localnet-cloud.sh'), 'printf "%s" "${CANTON_LOCALNET_SPLICE_VERSION}"\n'); - - try { - return execFileSync(localnetBin, ['status'], { - encoding: 'utf8', - env: { - ...process.env, - CANTON_LOCALNET_SPLICE_VERSION: '', - // Keep unit coverage on the deprecated SDK fallback path. - CANTON_LOCALNET_FORCE_LEGACY: '1', - }, - }); - } finally { - rmSync(packageRoot, { recursive: true, force: true }); - } -} - -describe('canton-localnet Splice version selection', (): void => { - it('uses a non-empty trimmed packaged version', (): void => { - expect(runPackagedLocalnetWithVersion(' 1.2.3 \n')).toBe('1.2.3'); - }); - - it('falls back to the built-in version when the packaged version is blank', (): void => { - expect(runPackagedLocalnetWithVersion(' \n\t')).toBe('0.6.8'); - }); -}); - -describe('canton-localnet soft delegation', (): void => { - it('delegates to @fairmint/canton-dev-tools when that package is installed nearby', (): void => { - const packageRoot = mkdtempSync(resolve(tmpdir(), 'canton-localnet-delegate-')); - const localnetBin = resolve(packageRoot, 'bin/canton-localnet'); - const devToolsBin = resolve(packageRoot, 'node_modules/@fairmint/canton-dev-tools/bin/canton-dev-tools'); - - mkdirSync(resolve(packageRoot, 'bin'), { recursive: true }); - mkdirSync(resolve(packageRoot, 'scripts'), { recursive: true }); - mkdirSync(resolve(devToolsBin, '..'), { recursive: true }); - copyFileSync(resolve(REPO_ROOT, 'bin/canton-localnet'), localnetBin); - chmodSync(localnetBin, 0o755); - writeFileSync(resolve(packageRoot, 'scripts/localnet-cloud.sh'), 'printf "legacy\\n"\n'); - writeFileSync(devToolsBin, '#!/usr/bin/env bash\nprintf "dev-tools:%s\\n" "$*"\n'); - chmodSync(devToolsBin, 0o755); - - try { - const output = execFileSync(localnetBin, ['status'], { - encoding: 'utf8', - env: { - ...process.env, - CANTON_LOCALNET_FORCE_LEGACY: '', - }, - }); - expect(output.trim()).toBe('dev-tools:status'); - } finally { - rmSync(packageRoot, { recursive: true, force: true }); - } - }); - - it('keeps the legacy path when CANTON_LOCALNET_FORCE_LEGACY=1', (): void => { - const packageRoot = mkdtempSync(resolve(tmpdir(), 'canton-localnet-force-legacy-')); - const localnetBin = resolve(packageRoot, 'bin/canton-localnet'); - const devToolsBin = resolve(packageRoot, 'node_modules/@fairmint/canton-dev-tools/bin/canton-dev-tools'); - - mkdirSync(resolve(packageRoot, 'bin'), { recursive: true }); - mkdirSync(resolve(packageRoot, 'scripts'), { recursive: true }); - mkdirSync(resolve(devToolsBin, '..'), { recursive: true }); - copyFileSync(resolve(REPO_ROOT, 'bin/canton-localnet'), localnetBin); - chmodSync(localnetBin, 0o755); - writeFileSync(resolve(packageRoot, 'scripts/localnet-cloud.sh'), 'printf "legacy\\n"\n'); - writeFileSync(devToolsBin, '#!/usr/bin/env bash\nprintf "dev-tools\\n"\n'); - chmodSync(devToolsBin, 0o755); - - try { - const output = execFileSync(localnetBin, ['status'], { - encoding: 'utf8', - env: { - ...process.env, - CANTON_LOCALNET_FORCE_LEGACY: '1', - }, - }); - expect(output.trim()).toBe('legacy'); - } finally { - rmSync(packageRoot, { recursive: true, force: true }); - } - }); - - it('applies CANTON_LOCALNET_INFRA_ONLY=true before soft-delegation', (): void => { - const packageRoot = mkdtempSync(resolve(tmpdir(), 'canton-localnet-infra-default-')); - const localnetBin = resolve(packageRoot, 'bin/canton-localnet'); - const devToolsBin = resolve(packageRoot, 'node_modules/@fairmint/canton-dev-tools/bin/canton-dev-tools'); - - mkdirSync(resolve(packageRoot, 'bin'), { recursive: true }); - mkdirSync(resolve(packageRoot, 'scripts'), { recursive: true }); - mkdirSync(resolve(devToolsBin, '..'), { recursive: true }); - copyFileSync(resolve(REPO_ROOT, 'bin/canton-localnet'), localnetBin); - chmodSync(localnetBin, 0o755); - writeFileSync(resolve(packageRoot, 'scripts/localnet-cloud.sh'), 'printf "legacy\\n"\n'); - writeFileSync(devToolsBin, '#!/usr/bin/env bash\nprintf "infra:%s\\n" "${CANTON_LOCALNET_INFRA_ONLY}"\n'); - chmodSync(devToolsBin, 0o755); - - try { - const output = execFileSync(localnetBin, ['status'], { - encoding: 'utf8', - env: { - ...process.env, - CANTON_LOCALNET_FORCE_LEGACY: '', - // Unset so the package binary must supply the documented default. - CANTON_LOCALNET_INFRA_ONLY: '', - }, - }); - expect(output.trim()).toBe('infra:true'); - } finally { - rmSync(packageRoot, { recursive: true, force: true }); - } - }); - - it('preserves an explicit CANTON_LOCALNET_INFRA_ONLY override on soft-delegation', (): void => { - const packageRoot = mkdtempSync(resolve(tmpdir(), 'canton-localnet-infra-override-')); - const localnetBin = resolve(packageRoot, 'bin/canton-localnet'); - const devToolsBin = resolve(packageRoot, 'node_modules/@fairmint/canton-dev-tools/bin/canton-dev-tools'); - - mkdirSync(resolve(packageRoot, 'bin'), { recursive: true }); - mkdirSync(resolve(packageRoot, 'scripts'), { recursive: true }); - mkdirSync(resolve(devToolsBin, '..'), { recursive: true }); - copyFileSync(resolve(REPO_ROOT, 'bin/canton-localnet'), localnetBin); - chmodSync(localnetBin, 0o755); - writeFileSync(resolve(packageRoot, 'scripts/localnet-cloud.sh'), 'printf "legacy\\n"\n'); - writeFileSync(devToolsBin, '#!/usr/bin/env bash\nprintf "infra:%s\\n" "${CANTON_LOCALNET_INFRA_ONLY}"\n'); - chmodSync(devToolsBin, 0o755); - - try { - const output = execFileSync(localnetBin, ['status'], { - encoding: 'utf8', - env: { - ...process.env, - CANTON_LOCALNET_FORCE_LEGACY: '', - CANTON_LOCALNET_INFRA_ONLY: 'false', - }, - }); - expect(output.trim()).toBe('infra:false'); - } finally { - rmSync(packageRoot, { recursive: true, force: true }); - } - }); -}); diff --git a/test/unit/scripts/localnet-cloud.test.ts b/test/unit/scripts/localnet-cloud.test.ts deleted file mode 100644 index 2100dcdc..00000000 --- a/test/unit/scripts/localnet-cloud.test.ts +++ /dev/null @@ -1,134 +0,0 @@ -import { execFileSync } from 'node:child_process'; -import { mkdirSync, mkdtempSync, readFileSync, rmSync, writeFileSync } from 'node:fs'; -import { tmpdir } from 'node:os'; -import { resolve } from 'node:path'; - -const REPO_ROOT = resolve(__dirname, '../../..'); - -function runSourcedLocalnetScript(body: string, args: readonly string[] = []): string { - return execFileSync('bash', ['-c', `source scripts/localnet-cloud.sh\n${body}`, 'localnet-cloud-test', ...args], { - cwd: REPO_ROOT, - encoding: 'utf8', - }); -} - -describe('localnet-cloud configuration lifecycle', (): void => { - it('persists a pending forced-snapshot restart across setup and start processes', (): void => { - const quickstart = mkdtempSync(resolve(tmpdir(), 'canton-localnet-config-')); - const localnet = resolve(quickstart, 'docker/modules/localnet'); - const cantonConfig = resolve(localnet, 'conf/canton/app.conf'); - const spliceConfig = resolve(localnet, 'conf/splice/sv/app.conf'); - const cantonHealthcheck = resolve(localnet, 'docker/canton/health-check.sh'); - const spliceHealthcheck = resolve(localnet, 'docker/splice/health-check.sh'); - - mkdirSync(resolve(localnet, 'conf/canton'), { recursive: true }); - mkdirSync(resolve(localnet, 'conf/splice/sv'), { recursive: true }); - mkdirSync(resolve(localnet, 'docker/canton'), { recursive: true }); - mkdirSync(resolve(localnet, 'docker/splice'), { recursive: true }); - writeFileSync(cantonConfig, 'canton {}\n'); - writeFileSync(spliceConfig, 'canton {}\n'); - writeFileSync(cantonHealthcheck, '#!/bin/bash\n'); - writeFileSync(spliceHealthcheck, '#!/bin/bash\n'); - writeFileSync(resolve(quickstart, '.env.local'), 'AUTH_MODE=oauth2\n'); - - try { - const setupOutput = runSourcedLocalnetScript( - 'QUICKSTART_DIR="$1"\n' + 'configure_quickstart_localnet\n' + 'printf "%s\\n" "${SPLICE_CONFIG_CHANGED}"', - [quickstart] - ); - const startOutput = runSourcedLocalnetScript( - 'QUICKSTART_DIR="$1"\n' + - 'configure_quickstart_localnet\n' + - 'printf "pending:%s\\n" "${SPLICE_CONFIG_CHANGED}"\n' + - 'quickstart_force_full_start() { return 1; }\n' + - 'quickstart_infra_only_enabled() { return 0; }\n' + - 'splice_container_running() { return 0; }\n' + - "start_infra_only_localnet() { printf 'start-infra\\n'; }\n" + - 'run_infra_compose() { printf \'recreate:%s\\n\' "$1"; }\n' + - "wait_for_services() { printf 'wait\\n'; }\n" + - 'start_localnet\n' + - 'printf "applied:%s:%s\\n" "${SPLICE_CONFIG_CHANGED}" "$(quickstart_env_value "${SPLICE_CONFIG_PENDING_KEY}")"', - [quickstart] - ); - - expect(setupOutput.trim()).toBe('true'); - expect(startOutput.trim().split('\n')).toEqual([ - 'pending:true', - 'start-infra', - '[localnet] Recreating the running Splice service to apply updated LocalNet configuration.', - 'recreate:up -d --no-deps --force-recreate splice', - 'wait', - 'applied:false:false', - ]); - const configured = readFileSync(spliceConfig, 'utf8'); - expect(configured.match(/canton\.scan-apps\.scan-app\.enable-forced-acs-snapshots = true/g)).toHaveLength(1); - } finally { - rmSync(quickstart, { recursive: true, force: true }); - } - }); - - it('recreates a previously running Splice service after infra-only startup and before readiness', (): void => { - const output = runSourcedLocalnetScript(` -quickstart_force_full_start() { return 1; } -quickstart_infra_only_enabled() { return 0; } -splice_container_running() { return 0; } -start_infra_only_localnet() { printf 'start-infra\\n'; } -run_infra_compose() { printf 'recreate:%s\\n' "$1"; } -wait_for_services() { printf 'wait\\n'; } -set_quickstart_env_value() { :; } -SPLICE_CONFIG_CHANGED=true -start_localnet -`); - - expect(output.trim().split('\n')).toEqual([ - 'start-infra', - '[localnet] Recreating the running Splice service to apply updated LocalNet configuration.', - 'recreate:up -d --no-deps --force-recreate splice', - 'wait', - ]); - }); - - it('recreates a previously running Splice service after fast startup and before readiness', (): void => { - const output = runSourcedLocalnetScript(` -quickstart_force_full_start() { return 1; } -quickstart_infra_only_enabled() { return 1; } -quickstart_fast_start_enabled() { return 0; } -quickstart_build_artifacts_ready() { return 0; } -try_fast_start_localnet() { printf 'start-fast\\n'; } -splice_container_running() { return 0; } -run_infra_compose() { printf 'recreate:%s\\n' "$1"; } -wait_for_services() { printf 'wait\\n'; } -set_quickstart_env_value() { :; } -SPLICE_CONFIG_CHANGED=true -start_localnet -`); - - expect(output.trim().split('\n')).toEqual([ - 'start-fast', - '[localnet] Recreating the running Splice service to apply updated LocalNet configuration.', - 'recreate:up -d --no-deps --force-recreate splice', - 'wait', - ]); - }); - - it('does not recreate Splice for a fresh stack or unchanged configuration', (): void => { - const output = runSourcedLocalnetScript(` -quickstart_force_full_start() { return 1; } -quickstart_infra_only_enabled() { return 0; } -start_infra_only_localnet() { printf 'start-infra\\n'; } -run_infra_compose() { printf 'unexpected-recreate:%s\\n' "$1"; } -wait_for_services() { printf 'wait\\n'; } -set_quickstart_env_value() { :; } - -splice_container_running() { return 1; } -SPLICE_CONFIG_CHANGED=true -start_localnet - -splice_container_running() { return 0; } -SPLICE_CONFIG_CHANGED=false -start_localnet -`); - - expect(output.trim().split('\n')).toEqual(['start-infra', 'wait', 'start-infra', 'wait']); - }); -}); diff --git a/test/utils/index.ts b/test/utils/index.ts deleted file mode 100644 index 92648601..00000000 --- a/test/utils/index.ts +++ /dev/null @@ -1,3 +0,0 @@ -/** Test utilities for integration tests. */ - -export * from './testConfig'; diff --git a/test/utils/localnetLedgerClients.ts b/test/utils/localnetLedgerClients.ts deleted file mode 100644 index 4e210667..00000000 --- a/test/utils/localnetLedgerClients.ts +++ /dev/null @@ -1,115 +0,0 @@ -/** Role-checked Ledger JSON API clients for either cn-quickstart LocalNet authentication mode. */ - -import { createHmac } from 'node:crypto'; -import { CantonRuntime, LedgerJsonApiClient } from '../../src'; - -const LEDGER_API_URL = 'http://localhost:3975'; -const OAUTH_TOKEN_URL = 'http://localhost:8082/realms/AppProvider/protocol/openid-connect/token'; - -let participantAdminClientPromise: Promise | undefined; -let nonAdminClientPromise: Promise | undefined; - -/** Resolve a participant-admin client without assuming whether LocalNet uses shared-secret or OAuth2 authentication. */ -export async function getLocalnetParticipantAdminLedgerClient(): Promise { - participantAdminClientPromise ??= resolveRoleCheckedClient( - [createSharedSecretClient('ledger-api-user'), createOAuthAdminClient()], - true - ); - return participantAdminClientPromise; -} - -/** Resolve an authenticated client that is positively verified not to hold the ParticipantAdmin right. */ -export async function getLocalnetNonAdminLedgerClient(): Promise { - nonAdminClientPromise ??= resolveRoleCheckedClient( - [createSharedSecretClient('app-provider'), createOAuthNonAdminClient()], - false - ); - return nonAdminClientPromise; -} - -async function resolveRoleCheckedClient( - candidates: readonly LedgerJsonApiClient[], - requireParticipantAdmin: boolean -): Promise { - let lastAuthenticationError: unknown; - - for (const candidate of candidates) { - let authenticated; - try { - authenticated = await candidate.getAuthenticatedUser({}); - } catch (error) { - lastAuthenticationError = error; - continue; - } - - const rights = await candidate.listUserRights({ userId: authenticated.user.id }); - const hasParticipantAdmin = - rights.rights?.some((right) => right.kind !== undefined && 'ParticipantAdmin' in right.kind) ?? false; - if (hasParticipantAdmin !== requireParticipantAdmin) { - throw new Error( - `LocalNet fixture ${authenticated.user.id} ${ - requireParticipantAdmin ? 'does not have' : 'unexpectedly has' - } ParticipantAdmin` - ); - } - return candidate; - } - - if (lastAuthenticationError instanceof Error) { - throw lastAuthenticationError; - } - throw new Error('Could not authenticate a role-checked LocalNet Ledger client'); -} - -function createOAuthAdminClient(): LedgerJsonApiClient { - return new LedgerJsonApiClient(new CantonRuntime({ network: 'localnet', provider: 'app-provider' })); -} - -function createOAuthNonAdminClient(): LedgerJsonApiClient { - return new LedgerJsonApiClient( - new CantonRuntime({ - network: 'localnet', - provider: 'app-provider', - authUrl: OAUTH_TOKEN_URL, - apis: { - LEDGER_JSON_API: { - apiUrl: LEDGER_API_URL, - auth: { - grantType: 'password', - clientId: 'app-provider-unsafe', - username: 'app-provider', - password: 'abc123', - scope: 'openid', - }, - }, - }, - }) - ); -} - -/** Create a client for cn-quickstart's intentionally unsafe, test-only HS256 authentication mode. */ -function createSharedSecretClient(subject: string): LedgerJsonApiClient { - const encode = (value: object): string => Buffer.from(JSON.stringify(value)).toString('base64url'); - const header = encode({ alg: 'HS256', typ: 'JWT' }); - const payload = encode({ sub: subject, aud: 'https://canton.network.global' }); - const unsignedToken = `${header}.${payload}`; - const signature = createHmac('sha256', 'unsafe').update(unsignedToken).digest('base64url'); - - return new LedgerJsonApiClient( - new CantonRuntime({ - network: 'localnet', - provider: 'app-provider', - authUrl: '', - apis: { - LEDGER_JSON_API: { - apiUrl: LEDGER_API_URL, - auth: { - grantType: 'client_credentials', - clientId: `shared-secret-${subject}`, - bearerToken: `${unsignedToken}.${signature}`, - }, - }, - }, - }) - ); -} diff --git a/test/utils/testConfig.ts b/test/utils/testConfig.ts deleted file mode 100644 index e8abee83..00000000 --- a/test/utils/testConfig.ts +++ /dev/null @@ -1,79 +0,0 @@ -/** - * Shared test configuration utilities for integration tests. - * - * Tests use the SDK's built-in localnet defaults with OAuth2 authentication (cn-quickstart with OAuth2). - * - * @example - * LocalNet usage (default) - * ```bash - * npm run test - * ``` - */ - -import type { ClientConfig } from '../../src'; - -/** - * Build a ClientConfig for integration tests. - * - * Returns LocalNet configuration using SDK's built-in OAuth2 defaults for cn-quickstart. The SDK automatically - * configures: - * - * - OAuth2 auth URL (Keycloak at localhost:8082) - * - API endpoints (Validator: 3903, JSON API: 3975, Scan: 4000/api/scan) - * - Client credentials (app-provider-validator) - * - * @returns ClientConfig for use with SDK clients - */ -export function buildIntegrationTestClientConfig(): ClientConfig { - // Use SDK's built-in localnet defaults with OAuth2 - // This matches cn-quickstart setup with "make setup" option 2 (with OAuth2) - // Must specify provider to get the correct API endpoints and credentials - return { - network: 'localnet', - provider: 'app-provider', - }; -} - -/** - * Sleep for a specified number of milliseconds. - * - * @param ms - Milliseconds to sleep - */ -export async function sleep(ms: number): Promise { - return new Promise((resolve) => setTimeout(resolve, ms)); -} - -/** - * Retry a function until it succeeds or times out. - * - * @param fn - Async function to retry - * @param options - Retry options - * @returns The result of the function - * @throws The last error if all retries fail - */ -export async function retry( - fn: () => Promise, - options: { - timeoutMs?: number; - pollIntervalMs?: number; - description?: string; - } = {} -): Promise { - const timeoutMs = options.timeoutMs ?? 30_000; - const pollIntervalMs = options.pollIntervalMs ?? 1_000; - const description = options.description ?? 'operation'; - - const deadline = Date.now() + timeoutMs; - let lastError: unknown; - - while (Date.now() < deadline) { - try { - return await fn(); - } catch (err) { - lastError = err instanceof Error ? err : new Error(String(err)); - await sleep(pollIntervalMs); - } - } - - throw new Error(`Timed out waiting for ${description}${lastError ? `: ${(lastError as Error).message}` : ''}`); -} From 38de3710d469b8a267cb44c90f750d023f4a68e8 Mon Sep 17 00:00:00 2001 From: Cursor Agent Date: Mon, 10 Aug 2026 21:07:57 +0000 Subject: [PATCH 6/8] Map Dev Tools peer types to workspace SDK source Point tsconfig.lint paths and Jest moduleNameMapper at local src so @fairmint/canton-dev-tools/testing branded types match this checkout. Co-authored-by: HardlyDifficult --- AGENTS.md | 3 +++ jest.config.js | 6 ++++++ tsconfig.lint.json | 7 ++++++- 3 files changed, 15 insertions(+), 1 deletion(-) diff --git a/AGENTS.md b/AGENTS.md index ee326a62..a50377a2 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -15,6 +15,9 @@ not ship a LocalNet engine or `canton-localnet` binary. - Integration helpers: import from `@fairmint/canton-dev-tools/testing`. - Pins / auth defaults: see Dev Tools [COMPATIBILITY.md](https://github.com/Fairmint/canton-dev-tools/blob/main/COMPATIBILITY.md). +- Workspace TypeScript/Jest map `@fairmint/canton-node-sdk` to local `src/` so Dev Tools + peer types match this checkout (`tsconfig.lint.json` paths, `jest.config.js` + `moduleNameMapper`). ## Cursor Cloud specific instructions diff --git a/jest.config.js b/jest.config.js index 55f95ecc..c1a0aef9 100644 --- a/jest.config.js +++ b/jest.config.js @@ -7,6 +7,12 @@ module.exports = { transform: { '^.+\\.ts$': 'ts-jest', }, + // LocalNet helpers from @fairmint/canton-dev-tools peer on this package; map to + // workspace source so branded types and runtime clients match local changes. + moduleNameMapper: { + '^@fairmint/canton-node-sdk$': '/src/index.ts', + '^@fairmint/canton-node-sdk/(.*)$': '/src/$1', + }, collectCoverageFrom: ['src/**/*.ts', '!src/**/*.d.ts', '!src/**/*.test.ts', '!src/**/*.spec.ts'], coverageDirectory: 'coverage', coverageReporters: ['text', 'lcov', 'html'], diff --git a/tsconfig.lint.json b/tsconfig.lint.json index a592074c..8b7e2fe2 100644 --- a/tsconfig.lint.json +++ b/tsconfig.lint.json @@ -3,7 +3,12 @@ "compilerOptions": { "rootDir": ".", "outDir": "./build", - "types": ["node", "jest"] + "types": ["node", "jest"], + "paths": { + "*": ["./*"], + "@fairmint/canton-node-sdk": ["./src/index.ts"], + "@fairmint/canton-node-sdk/*": ["./src/*"] + } }, "include": ["src/**/*", "test/**/*", "scripts/**/*", "examples/**/*"], "exclude": ["**/*.example.ts"] From 7186cf0853a032e61aa107c0ff417dec5896ec0b Mon Sep 17 00:00:00 2001 From: Cursor Agent Date: Mon, 10 Aug 2026 21:16:22 +0000 Subject: [PATCH 7/8] Allow immediate installs of @fairmint/* under min-release-age Hard-cutover to @fairmint/canton-dev-tools@0.1.1 failed CI because .npmrc min-release-age=1 rejects packages published within 24h. Exclude the Fairmint scope and require npm >=11.17 for min-release-age-exclude. Co-authored-by: HardlyDifficult --- .github/workflows/package-artifacts.yml | 3 ++- .github/workflows/publish.yml | 5 +++-- .github/workflows/test-cn-quickstart.yml | 3 ++- .npmrc | 2 ++ 4 files changed, 9 insertions(+), 4 deletions(-) diff --git a/.github/workflows/package-artifacts.yml b/.github/workflows/package-artifacts.yml index 4186ade8..c1985634 100644 --- a/.github/workflows/package-artifacts.yml +++ b/.github/workflows/package-artifacts.yml @@ -25,7 +25,8 @@ jobs: - name: Upgrade npm # Keep PR package checks aligned with the trusted-publishing npm CLI. - run: npm install --global npm@^11.10.0 + # npm >=11.17 required for .npmrc min-release-age-exclude (@fairmint/*). + run: npm install --global npm@^11.17.0 - name: Install dependencies run: npm i diff --git a/.github/workflows/publish.yml b/.github/workflows/publish.yml index d0aedb99..5a20e07b 100644 --- a/.github/workflows/publish.yml +++ b/.github/workflows/publish.yml @@ -31,8 +31,9 @@ jobs: registry-url: 'https://registry.npmjs.org' - name: Upgrade npm - # Trusted publishing requires npm CLI >= 11.5.1 - run: npm install --global npm@^11.10.0 + # Trusted publishing requires npm CLI >= 11.5.1; + # npm >=11.17 required for .npmrc min-release-age-exclude (@fairmint/*). + run: npm install --global npm@^11.17.0 - name: Install dependencies run: npm i diff --git a/.github/workflows/test-cn-quickstart.yml b/.github/workflows/test-cn-quickstart.yml index c2348c89..aa6ff899 100644 --- a/.github/workflows/test-cn-quickstart.yml +++ b/.github/workflows/test-cn-quickstart.yml @@ -29,7 +29,8 @@ jobs: node-version: '22.14' - name: Upgrade npm - run: npm install --global npm@^11.10.0 + # npm >=11.17 required for .npmrc min-release-age-exclude (@fairmint/*). + run: npm install --global npm@^11.17.0 - name: Initialize Submodules run: | diff --git a/.npmrc b/.npmrc index 6c6f2f20..7079b9e0 100644 --- a/.npmrc +++ b/.npmrc @@ -1 +1,3 @@ min-release-age=1 +# Fairmint-owned packages are published by us; allow immediate consumption after cutovers. +min-release-age-exclude[]=@fairmint/* From bb8482eada826f29e6dd7495955ee16597e5e194 Mon Sep 17 00:00:00 2001 From: Cursor Agent Date: Mon, 10 Aug 2026 21:27:06 +0000 Subject: [PATCH 8/8] Resolve @fairmint/canton-dev-tools/testing under ts-jest TS 5 classic resolution ignores package exports, so map the testing subpath in tsconfig.lint.json and jest for LocalNet integration tests. Co-authored-by: HardlyDifficult --- jest.config.js | 15 ++++++++++++--- tsconfig.lint.json | 6 ++++-- 2 files changed, 16 insertions(+), 5 deletions(-) diff --git a/jest.config.js b/jest.config.js index c1a0aef9..a032b68e 100644 --- a/jest.config.js +++ b/jest.config.js @@ -5,13 +5,22 @@ module.exports = { testMatch: ['**/*.test.ts', '**/*.spec.ts'], testPathIgnorePatterns: ['/node_modules/'], transform: { - '^.+\\.ts$': 'ts-jest', + '^.+\\.ts$': [ + 'ts-jest', + { + // Use the lint/test project so package exports + path maps resolve under TS 5.x. + tsconfig: '/tsconfig.lint.json', + }, + ], }, - // LocalNet helpers from @fairmint/canton-dev-tools peer on this package; map to - // workspace source so branded types and runtime clients match local changes. + // LocalNet helpers from @fairmint/canton-dev-tools peer on this package; map SDK + // imports to workspace source so branded types match local changes. Map the + // Dev Tools testing subpath explicitly — classic TS resolution ignores exports. moduleNameMapper: { '^@fairmint/canton-node-sdk$': '/src/index.ts', '^@fairmint/canton-node-sdk/(.*)$': '/src/$1', + '^@fairmint/canton-dev-tools/testing$': + '/node_modules/@fairmint/canton-dev-tools/dist/testing/index.js', }, collectCoverageFrom: ['src/**/*.ts', '!src/**/*.d.ts', '!src/**/*.test.ts', '!src/**/*.spec.ts'], coverageDirectory: 'coverage', diff --git a/tsconfig.lint.json b/tsconfig.lint.json index 8b7e2fe2..525cc17f 100644 --- a/tsconfig.lint.json +++ b/tsconfig.lint.json @@ -5,9 +5,11 @@ "outDir": "./build", "types": ["node", "jest"], "paths": { - "*": ["./*"], "@fairmint/canton-node-sdk": ["./src/index.ts"], - "@fairmint/canton-node-sdk/*": ["./src/*"] + "@fairmint/canton-node-sdk/*": ["./src/*"], + "@fairmint/canton-dev-tools/testing": [ + "./node_modules/@fairmint/canton-dev-tools/dist/testing/index.d.ts" + ] } }, "include": ["src/**/*", "test/**/*", "scripts/**/*", "examples/**/*"],