@fairmint/canton-node-sdk 0.0.223 → 0.0.225

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,197 @@
1
+ #!/usr/bin/env bash
2
+ set -euo pipefail
3
+
4
+ resolve_script_dir() {
5
+ local source="$1"
6
+ local dir=""
7
+
8
+ while [[ -L "${source}" ]]; do
9
+ dir="$(cd -P "$(dirname "${source}")" && pwd)"
10
+ source="$(readlink "${source}")"
11
+ if [[ "${source}" != /* ]]; then
12
+ source="${dir}/${source}"
13
+ fi
14
+ done
15
+
16
+ cd -P "$(dirname "${source}")" && pwd
17
+ }
18
+
19
+ PACKAGE_ROOT="$(cd "$(resolve_script_dir "${BASH_SOURCE[0]}")/.." && pwd)"
20
+ LOCALNET_SCRIPT="${PACKAGE_ROOT}/scripts/localnet-cloud.sh"
21
+ DEFAULT_QUICKSTART_REPO="https://github.com/digital-asset/cn-quickstart.git"
22
+ DEFAULT_QUICKSTART_REF="46201cd27ee16c99f78ab97b7e6513c56f28e004"
23
+
24
+ log() {
25
+ printf '[canton-localnet] %s\n' "$*"
26
+ }
27
+
28
+ usage() {
29
+ cat <<'USAGE'
30
+ Usage: canton-localnet <command>
31
+
32
+ Commands:
33
+ setup Prepare LocalNet prerequisites
34
+ start Start lean LocalNet + Keycloak and wait for ready endpoints
35
+ stop Stop LocalNet services
36
+ logs Show LocalNet diagnostic logs
37
+ status Show docker + endpoint status
38
+ smoke Run endpoint smoke checks
39
+ test Run project integration tests when configured
40
+ verify Run setup + start + smoke + test
41
+
42
+ One-liners:
43
+ npx @fairmint/canton-node-sdk start
44
+ npx @fairmint/canton-node-sdk verify
45
+
46
+ Environment:
47
+ CANTON_LOCALNET_QUICKSTART_DIR Use an existing cn-quickstart/quickstart directory
48
+ CANTON_LOCALNET_CACHE_DIR Cache root for fetched cn-quickstart assets
49
+ CANTON_LOCALNET_QUICKSTART_REF cn-quickstart git ref to fetch
50
+ CANTON_LOCALNET_INFRA_ONLY Defaults to true for this package binary
51
+ USAGE
52
+ }
53
+
54
+ require_command() {
55
+ local cmd="$1"
56
+ if ! command -v "${cmd}" >/dev/null 2>&1; then
57
+ log "Missing required command: ${cmd}"
58
+ exit 1
59
+ fi
60
+ }
61
+
62
+ cache_root() {
63
+ if [[ -n "${CANTON_LOCALNET_CACHE_DIR:-}" ]]; then
64
+ printf '%s' "${CANTON_LOCALNET_CACHE_DIR}"
65
+ return
66
+ fi
67
+
68
+ if [[ -n "${XDG_CACHE_HOME:-}" ]]; then
69
+ printf '%s' "${XDG_CACHE_HOME}/fairmint/canton-localnet"
70
+ return
71
+ fi
72
+
73
+ printf '%s' "${HOME}/.cache/fairmint/canton-localnet"
74
+ }
75
+
76
+ quickstart_ref() {
77
+ printf '%s' "${CANTON_LOCALNET_QUICKSTART_REF:-${DEFAULT_QUICKSTART_REF}}"
78
+ }
79
+
80
+ quickstart_repo() {
81
+ printf '%s' "${CANTON_LOCALNET_QUICKSTART_REPO:-${DEFAULT_QUICKSTART_REPO}}"
82
+ }
83
+
84
+ default_quickstart_root() {
85
+ printf '%s/cn-quickstart-%s' "$(cache_root)" "$(quickstart_ref)"
86
+ }
87
+
88
+ repo_quickstart_dir() {
89
+ printf '%s/libs/cn-quickstart/quickstart' "${PACKAGE_ROOT}"
90
+ }
91
+
92
+ use_repo_quickstart_default() {
93
+ [[ -z "${CANTON_LOCALNET_QUICKSTART_DIR:-}" \
94
+ && -f "${PACKAGE_ROOT}/.gitmodules" \
95
+ && ( -d "${PACKAGE_ROOT}/.git" || -f "${PACKAGE_ROOT}/.git" ) ]]
96
+ }
97
+
98
+ quickstart_dir() {
99
+ if [[ -n "${CANTON_LOCALNET_QUICKSTART_DIR:-}" ]]; then
100
+ printf '%s' "${CANTON_LOCALNET_QUICKSTART_DIR}"
101
+ return
102
+ fi
103
+
104
+ if use_repo_quickstart_default; then
105
+ repo_quickstart_dir
106
+ return
107
+ fi
108
+
109
+ printf '%s/quickstart' "$(default_quickstart_root)"
110
+ }
111
+
112
+ ensure_quickstart_checkout() {
113
+ local dir=""
114
+ local root=""
115
+ local tmp=""
116
+
117
+ if use_repo_quickstart_default; then
118
+ return
119
+ fi
120
+
121
+ dir="$(quickstart_dir)"
122
+ if [[ -d "${dir}/docker/modules/localnet" ]]; then
123
+ return
124
+ fi
125
+
126
+ if [[ -n "${CANTON_LOCALNET_QUICKSTART_DIR:-}" ]]; then
127
+ log "CANTON_LOCALNET_QUICKSTART_DIR is not a cn-quickstart quickstart directory: ${dir}"
128
+ exit 1
129
+ fi
130
+
131
+ require_command git
132
+
133
+ root="$(default_quickstart_root)"
134
+ if [[ -e "${root}" ]]; then
135
+ log "Cached cn-quickstart directory exists but is incomplete: ${root}"
136
+ log "Remove it or set CANTON_LOCALNET_CACHE_DIR to use a fresh cache."
137
+ exit 1
138
+ fi
139
+
140
+ mkdir -p "$(cache_root)"
141
+ tmp="$(mktemp -d "$(cache_root)/cn-quickstart.XXXXXX")"
142
+ trap 'rm -rf "${tmp}"' RETURN
143
+
144
+ log "Fetching cn-quickstart $(quickstart_ref)..."
145
+ git -C "${tmp}" init -q
146
+ git -C "${tmp}" remote add origin "$(quickstart_repo)"
147
+ if git -C "${tmp}" fetch --depth 1 origin "$(quickstart_ref)"; then
148
+ git -C "${tmp}" checkout --detach FETCH_HEAD -q
149
+ else
150
+ log "Direct ref fetch failed; retrying with a blobless repository fetch."
151
+ git -C "${tmp}" fetch --filter=blob:none origin
152
+ git -C "${tmp}" checkout --detach "$(quickstart_ref)" -q
153
+ fi
154
+
155
+ if [[ ! -d "${tmp}/quickstart/docker/modules/localnet" ]]; then
156
+ log "Fetched cn-quickstart does not contain quickstart/docker/modules/localnet."
157
+ exit 1
158
+ fi
159
+
160
+ mv "${tmp}" "${root}"
161
+ trap - RETURN
162
+ log "Cached cn-quickstart at ${root}."
163
+ }
164
+
165
+ main() {
166
+ local command="${1:-}"
167
+ local dir=""
168
+
169
+ case "${command}" in
170
+ "" | -h | --help | help)
171
+ usage
172
+ if [[ -z "${command}" ]]; then
173
+ exit 1
174
+ fi
175
+ exit 0
176
+ ;;
177
+ setup | start | verify)
178
+ ensure_quickstart_checkout
179
+ ;;
180
+ logs | status | stop | smoke | test)
181
+ ;;
182
+ *)
183
+ usage
184
+ exit 1
185
+ ;;
186
+ esac
187
+
188
+ dir="$(quickstart_dir)"
189
+ if ! use_repo_quickstart_default; then
190
+ export CANTON_LOCALNET_QUICKSTART_DIR="${dir}"
191
+ fi
192
+ export CANTON_LOCALNET_INFRA_ONLY="${CANTON_LOCALNET_INFRA_ONLY:-true}"
193
+
194
+ exec bash "${LOCALNET_SCRIPT}" "$@"
195
+ }
196
+
197
+ main "$@"
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@fairmint/canton-node-sdk",
3
- "version": "0.0.223",
3
+ "version": "0.0.225",
4
4
  "description": "Canton Node SDK",
5
5
  "keywords": [
6
6
  "canton",
@@ -21,7 +21,12 @@
21
21
  "author": "Fairmint",
22
22
  "main": "build/src/index.js",
23
23
  "types": "build/src/index.d.ts",
24
+ "bin": {
25
+ "canton-localnet": "bin/canton-localnet"
26
+ },
24
27
  "files": [
28
+ "bin/canton-localnet",
29
+ "scripts/localnet-cloud.sh",
25
30
  "build/**"
26
31
  ],
27
32
  "scripts": {
@@ -42,13 +47,14 @@
42
47
  "lint:fix": "eslint . --fix",
43
48
  "lint:npm": "npmPkgJsonLint . --fix",
44
49
  "lint:npm:check": "npmPkgJsonLint .",
45
- "localnet:quickstart": "bash ./scripts/localnet/setup-quickstart-localnet.sh",
46
- "localnet:setup": "bash ./scripts/localnet/setup-localnet.sh",
47
- "localnet:smoke": "bash ./scripts/localnet/localnet-cloud.sh smoke",
48
- "localnet:start": "bash ./scripts/localnet/start-localnet.sh",
49
- "localnet:status": "bash ./scripts/localnet/localnet-status.sh",
50
- "localnet:stop": "bash ./scripts/localnet/stop-localnet.sh",
51
- "localnet:verify": "bash ./scripts/localnet/localnet-cloud.sh verify",
50
+ "localnet:quickstart": "bash ./bin/canton-localnet setup",
51
+ "localnet:logs": "bash ./bin/canton-localnet logs",
52
+ "localnet:setup": "bash ./bin/canton-localnet setup",
53
+ "localnet:smoke": "bash ./bin/canton-localnet smoke",
54
+ "localnet:start": "bash ./bin/canton-localnet start",
55
+ "localnet:status": "bash ./bin/canton-localnet status",
56
+ "localnet:stop": "bash ./bin/canton-localnet stop",
57
+ "localnet:verify": "bash ./bin/canton-localnet verify",
52
58
  "prepack": "npm run clean && npm run build:core",
53
59
  "prepare-release": "tsx scripts/prepare-release.ts",
54
60
  "prepublishOnly": "npm run prepack",
@@ -0,0 +1,816 @@
1
+ #!/usr/bin/env bash
2
+ set -euo pipefail
3
+
4
+ SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
5
+ REPO_ROOT="$(cd "${SCRIPT_DIR}/.." && pwd)"
6
+ PROJECT_ROOT="${CANTON_LOCALNET_PROJECT_ROOT:-$(pwd)}"
7
+ QUICKSTART_DIR="${CANTON_LOCALNET_QUICKSTART_DIR:-${REPO_ROOT}/libs/cn-quickstart/quickstart}"
8
+ DOCKERD_PID_FILE="/tmp/localnet-dockerd.pid"
9
+ DOCKERD_LOG_FILE="/tmp/localnet-dockerd.log"
10
+ HOSTS_ENTRY="127.0.0.1 scan.localhost sv.localhost wallet.localhost"
11
+ CURL_CONNECT_TIMEOUT=2
12
+ CURL_MAX_TIME=5
13
+
14
+ log() {
15
+ printf '[localnet] %s\n' "$*"
16
+ }
17
+
18
+ is_truthy() {
19
+ case "${1,,}" in
20
+ 1 | true | yes | on)
21
+ return 0
22
+ ;;
23
+ *)
24
+ return 1
25
+ ;;
26
+ esac
27
+ }
28
+
29
+ require_command() {
30
+ local cmd="$1"
31
+ if ! command -v "${cmd}" >/dev/null 2>&1; then
32
+ log "Missing required command: ${cmd}"
33
+ exit 1
34
+ fi
35
+ }
36
+
37
+ ensure_sudo() {
38
+ if ! sudo_noninteractive_available; then
39
+ log "Passwordless sudo is required in this cloud environment."
40
+ exit 1
41
+ fi
42
+ }
43
+
44
+ sudo_noninteractive_available() {
45
+ command -v sudo >/dev/null 2>&1 && sudo -n true >/dev/null 2>&1
46
+ }
47
+
48
+ ensure_docker_packages() {
49
+ if command -v docker >/dev/null 2>&1 \
50
+ && docker compose version >/dev/null 2>&1; then
51
+ return
52
+ fi
53
+
54
+ if ! sudo_noninteractive_available || ! command -v apt-get >/dev/null 2>&1; then
55
+ log "Docker with Compose is required. Install Docker and start it, then retry."
56
+ exit 1
57
+ fi
58
+
59
+ log "Installing Docker packages..."
60
+ sudo apt-get update
61
+ sudo apt-get install -y docker.io docker-compose docker-compose-v2
62
+ }
63
+
64
+ ensure_legacy_iptables() {
65
+ if ! command -v update-alternatives >/dev/null 2>&1 \
66
+ || [[ ! -x /usr/sbin/iptables-legacy || ! -x /usr/sbin/ip6tables-legacy ]]; then
67
+ log "iptables legacy binaries unavailable; skipping backend switch."
68
+ return
69
+ fi
70
+
71
+ if ! iptables --version 2>/dev/null | grep -q 'legacy'; then
72
+ log "Switching iptables to legacy backend..."
73
+ sudo update-alternatives --set iptables /usr/sbin/iptables-legacy
74
+ fi
75
+
76
+ if ! ip6tables --version 2>/dev/null | grep -q 'legacy'; then
77
+ log "Switching ip6tables to legacy backend..."
78
+ sudo update-alternatives --set ip6tables /usr/sbin/ip6tables-legacy
79
+ fi
80
+ }
81
+
82
+ docker_ready() {
83
+ docker info >/dev/null 2>&1 || (sudo_noninteractive_available && sudo docker info >/dev/null 2>&1)
84
+ }
85
+
86
+ configure_docker_socket_permissions() {
87
+ if [[ ! -S /var/run/docker.sock ]]; then
88
+ return
89
+ fi
90
+
91
+ if ! sudo_noninteractive_available; then
92
+ return
93
+ fi
94
+
95
+ sudo groupadd -f docker >/dev/null 2>&1 || true
96
+ sudo chown root:docker /var/run/docker.sock >/dev/null 2>&1 || true
97
+ sudo chmod 660 /var/run/docker.sock || true
98
+ }
99
+
100
+ run_docker() {
101
+ if docker info >/dev/null 2>&1; then
102
+ docker "$@"
103
+ return
104
+ fi
105
+ if ! sudo_noninteractive_available || ! sudo docker info >/dev/null 2>&1; then
106
+ log "Docker daemon is not available."
107
+ exit 1
108
+ fi
109
+ sudo docker "$@"
110
+ }
111
+
112
+ resolve_quickstart_image_tag() {
113
+ local env_file=""
114
+ local splice_version=""
115
+ local parsed_value=""
116
+
117
+ for env_file in "${QUICKSTART_DIR}/.env" "${QUICKSTART_DIR}/.env.local"; do
118
+ if [[ ! -f "${env_file}" ]]; then
119
+ continue
120
+ fi
121
+
122
+ parsed_value="$(awk -F= -v key="SPLICE_VERSION" '
123
+ $0 ~ /^[[:space:]]*#/ { next }
124
+ $1 == key {
125
+ value = substr($0, index($0, "=") + 1)
126
+ gsub(/^[[:space:]]+|[[:space:]]+$/, "", value)
127
+ gsub(/^"|"$/, "", value)
128
+ gsub(/^'\''|'\''$/, "", value)
129
+ parsed = value
130
+ }
131
+ END {
132
+ if (parsed != "") {
133
+ print parsed
134
+ }
135
+ }
136
+ ' "${env_file}")"
137
+
138
+ if [[ -n "${parsed_value}" ]]; then
139
+ splice_version="${parsed_value}"
140
+ fi
141
+ done
142
+
143
+ printf '%s' "${splice_version}"
144
+ }
145
+
146
+ run_quickstart_command() {
147
+ local command="$1"
148
+ local docker_shim_dir=""
149
+ local quickstart_image_tag=""
150
+ local status=0
151
+
152
+ quickstart_image_tag="$(resolve_quickstart_image_tag)"
153
+
154
+ if ! docker info >/dev/null 2>&1 && sudo docker info >/dev/null 2>&1; then
155
+ docker_shim_dir="$(mktemp -d "/tmp/canton-localnet-docker-shim.XXXXXX")"
156
+ cat >"${docker_shim_dir}/docker" <<'EOF'
157
+ #!/usr/bin/env bash
158
+ exec sudo -E docker "$@"
159
+ EOF
160
+ chmod +x "${docker_shim_dir}/docker"
161
+ log "Using sudo docker shim for quickstart command."
162
+ fi
163
+
164
+ set +e
165
+ if [[ -n "${docker_shim_dir}" ]]; then
166
+ (
167
+ cd "${QUICKSTART_DIR}"
168
+ MODULES_DIR="${QUICKSTART_DIR}/docker/modules" \
169
+ LOCALNET_DIR="${QUICKSTART_DIR}/docker/modules/localnet" \
170
+ IMAGE_TAG="${quickstart_image_tag}" \
171
+ PATH="${docker_shim_dir}:${HOME}/.daml/bin:${PATH}" \
172
+ bash -lc "${command}"
173
+ )
174
+ status=$?
175
+ else
176
+ (
177
+ cd "${QUICKSTART_DIR}"
178
+ MODULES_DIR="${QUICKSTART_DIR}/docker/modules" \
179
+ LOCALNET_DIR="${QUICKSTART_DIR}/docker/modules/localnet" \
180
+ IMAGE_TAG="${quickstart_image_tag}" \
181
+ PATH="${HOME}/.daml/bin:${PATH}" \
182
+ bash -lc "${command}"
183
+ )
184
+ status=$?
185
+ fi
186
+ set -e
187
+
188
+ if [[ -n "${docker_shim_dir}" ]]; then
189
+ rm -rf "${docker_shim_dir}"
190
+ fi
191
+
192
+ return "${status}"
193
+ }
194
+
195
+ run_quickstart_make() {
196
+ local target="$1"
197
+ run_quickstart_command "make ${target}"
198
+ }
199
+
200
+ run_infra_compose() {
201
+ local compose_args="$1"
202
+
203
+ run_quickstart_command "docker compose \
204
+ -f \"\${LOCALNET_DIR}/compose.yaml\" \
205
+ -f \"\${MODULES_DIR}/keycloak/compose.yaml\" \
206
+ --env-file .env \
207
+ --env-file .env.local \
208
+ --env-file \"\${LOCALNET_DIR}/compose.env\" \
209
+ --env-file \"\${LOCALNET_DIR}/env/common.env\" \
210
+ --env-file \"\${MODULES_DIR}/keycloak/compose.env\" \
211
+ --profile app-provider \
212
+ --profile app-user \
213
+ --profile sv \
214
+ --profile keycloak \
215
+ ${compose_args}"
216
+ }
217
+
218
+ start_docker_daemon() {
219
+ local dockerd_pid=""
220
+
221
+ if docker_ready; then
222
+ configure_docker_socket_permissions
223
+ return
224
+ fi
225
+
226
+ if ! sudo_noninteractive_available; then
227
+ log "Docker daemon is not running. Start Docker and retry."
228
+ exit 1
229
+ fi
230
+
231
+ ensure_legacy_iptables
232
+
233
+ log "Starting Docker daemon with vfs storage driver..."
234
+ sudo nohup dockerd --host=unix:///var/run/docker.sock --pidfile="${DOCKERD_PID_FILE}" --storage-driver=vfs >"${DOCKERD_LOG_FILE}" 2>&1 &
235
+
236
+ for _ in $(seq 1 60); do
237
+ if sudo docker info >/dev/null 2>&1; then
238
+ configure_docker_socket_permissions
239
+ return
240
+ fi
241
+ if [[ -f "${DOCKERD_PID_FILE}" ]]; then
242
+ dockerd_pid="$(cat "${DOCKERD_PID_FILE}" 2>/dev/null || true)"
243
+ if [[ -n "${dockerd_pid}" ]] && ! ps -p "${dockerd_pid}" >/dev/null 2>&1; then
244
+ log "Docker daemon exited before becoming ready."
245
+ log "Inspect ${DOCKERD_LOG_FILE}."
246
+ exit 1
247
+ fi
248
+ fi
249
+ sleep 1
250
+ done
251
+
252
+ log "Docker failed to start. Inspect ${DOCKERD_LOG_FILE}."
253
+ exit 1
254
+ }
255
+
256
+ ensure_submodules() {
257
+ if [[ -n "${CANTON_LOCALNET_QUICKSTART_DIR:-}" ]]; then
258
+ if [[ -d "${QUICKSTART_DIR}" ]]; then
259
+ return
260
+ fi
261
+ log "Configured CANTON_LOCALNET_QUICKSTART_DIR does not exist: ${QUICKSTART_DIR}"
262
+ exit 1
263
+ fi
264
+
265
+ if [[ -d "${REPO_ROOT}/libs/splice" && -d "${QUICKSTART_DIR}" ]]; then
266
+ return
267
+ fi
268
+
269
+ require_command git
270
+
271
+ if ! git -C "${REPO_ROOT}" rev-parse --is-inside-work-tree >/dev/null 2>&1; then
272
+ log "Missing localnet assets and not in a git checkout. Install from npm with bundled assets or clone SDK with submodules."
273
+ exit 1
274
+ fi
275
+
276
+ if [[ ! -d "${REPO_ROOT}/libs/splice" ]]; then
277
+ log "Initializing libs/splice submodule..."
278
+ git -C "${REPO_ROOT}" submodule update --init --depth 1 libs/splice
279
+ fi
280
+
281
+ if [[ ! -d "${REPO_ROOT}/libs/cn-quickstart" ]]; then
282
+ log "Initializing libs/cn-quickstart submodule..."
283
+ git -C "${REPO_ROOT}" submodule update --init --recursive libs/cn-quickstart
284
+ fi
285
+
286
+ if [[ ! -d "${QUICKSTART_DIR}" ]]; then
287
+ log "cn-quickstart directory not found after submodule init."
288
+ exit 1
289
+ fi
290
+ }
291
+
292
+ ensure_hosts_entries() {
293
+ if ! grep -Eq '(^|[[:space:]])scan\.localhost([[:space:]]|$)' /etc/hosts \
294
+ || ! grep -Eq '(^|[[:space:]])sv\.localhost([[:space:]]|$)' /etc/hosts \
295
+ || ! grep -Eq '(^|[[:space:]])wallet\.localhost([[:space:]]|$)' /etc/hosts; then
296
+ log "Adding localnet host aliases to /etc/hosts..."
297
+ if sudo_noninteractive_available; then
298
+ echo "${HOSTS_ENTRY}" | sudo tee -a /etc/hosts >/dev/null
299
+ return
300
+ fi
301
+ if [[ -t 0 ]] && command -v sudo >/dev/null 2>&1; then
302
+ echo "${HOSTS_ENTRY}" | sudo tee -a /etc/hosts >/dev/null
303
+ return
304
+ fi
305
+ log "Missing localnet host aliases. Add this line to /etc/hosts, then retry:"
306
+ log "${HOSTS_ENTRY}"
307
+ exit 1
308
+ fi
309
+ }
310
+
311
+ quickstart_setup() {
312
+ if [[ ! -f "${QUICKSTART_DIR}/.env.local" ]]; then
313
+ if quickstart_infra_only_enabled; then
314
+ write_quickstart_env_local
315
+ else
316
+ log "Running cn-quickstart setup (OAuth2 enabled)..."
317
+ (
318
+ cd "${QUICKSTART_DIR}"
319
+ # Match CI behavior first, then fall back to prompt-based answers for newer setup flows.
320
+ echo "2" | make setup || true
321
+ if ! grep -Eq '^AUTH_MODE=oauth2$' "${QUICKSTART_DIR}/.env.local" 2>/dev/null; then
322
+ printf 'y\ny\n\nn\n' | make setup
323
+ fi
324
+ )
325
+ fi
326
+ else
327
+ log "Reusing existing ${QUICKSTART_DIR}/.env.local."
328
+ fi
329
+
330
+ if quickstart_infra_only_enabled; then
331
+ log "Skipping Daml SDK install for infrastructure-only localnet."
332
+ return
333
+ fi
334
+
335
+ if [[ ! -x "${HOME}/.daml/bin/daml" ]]; then
336
+ log "Installing Daml SDK..."
337
+ (
338
+ cd "${QUICKSTART_DIR}"
339
+ make install-daml-sdk
340
+ )
341
+ else
342
+ log "Reusing existing Daml SDK at ${HOME}/.daml/bin/daml."
343
+ fi
344
+ }
345
+
346
+ default_party_hint() {
347
+ local clean_user=""
348
+ local raw_user="${USER:-runner}"
349
+
350
+ clean_user="$(printf '%s' "${raw_user}" | tr -cd '[:alpha:]')"
351
+ if [[ -z "${clean_user}" ]]; then
352
+ clean_user="runner"
353
+ fi
354
+
355
+ printf 'quickstart-%s-1' "${clean_user,,}"
356
+ }
357
+
358
+ write_quickstart_env_local() {
359
+ local auth_mode="${CANTON_LOCALNET_AUTH_MODE:-oauth2}"
360
+ local observability_enabled="${CANTON_LOCALNET_OBSERVABILITY_ENABLED:-false}"
361
+ local party_hint="${CANTON_LOCALNET_PARTY_HINT:-}"
362
+ local test_mode="${CANTON_LOCALNET_TEST_MODE:-off}"
363
+
364
+ if [[ -z "${party_hint}" ]]; then
365
+ party_hint="$(default_party_hint)"
366
+ fi
367
+
368
+ log "Writing non-interactive cn-quickstart localnet config."
369
+ cat >"${QUICKSTART_DIR}/.env.local" <<EOF
370
+ OBSERVABILITY_ENABLED=${observability_enabled}
371
+ AUTH_MODE=${auth_mode}
372
+ PARTY_HINT=${party_hint}
373
+ TEST_MODE=${test_mode}
374
+ EOF
375
+ }
376
+
377
+ quickstart_fast_start_enabled() {
378
+ is_truthy "${CANTON_LOCALNET_FAST_START:-true}"
379
+ }
380
+
381
+ quickstart_force_full_start() {
382
+ is_truthy "${CANTON_LOCALNET_FORCE_FULL_START:-false}"
383
+ }
384
+
385
+ quickstart_infra_only_enabled() {
386
+ is_truthy "${CANTON_LOCALNET_INFRA_ONLY:-false}"
387
+ }
388
+
389
+ quickstart_build_artifacts_ready() {
390
+ local missing_paths=()
391
+
392
+ if [[ ! -f "${QUICKSTART_DIR}/backend/build/distributions/backend.tar" ]]; then
393
+ missing_paths+=("backend/build/distributions/backend.tar")
394
+ fi
395
+
396
+ if [[ ! -d "${QUICKSTART_DIR}/frontend/dist" ]]; then
397
+ missing_paths+=("frontend/dist")
398
+ fi
399
+
400
+ if ! compgen -G "${QUICKSTART_DIR}/daml/licensing/.daml/dist/*.dar" >/dev/null; then
401
+ missing_paths+=("daml/licensing/.daml/dist/*.dar")
402
+ fi
403
+
404
+ if ! compgen -G "${QUICKSTART_DIR}/backend/build/otel-agent/opentelemetry-javaagent-*.jar" >/dev/null; then
405
+ missing_paths+=("backend/build/otel-agent/opentelemetry-javaagent-*.jar")
406
+ fi
407
+
408
+ if [[ ${#missing_paths[@]} -gt 0 ]]; then
409
+ log "Fast start unavailable; missing quickstart build artifacts: ${missing_paths[*]}"
410
+ return 1
411
+ fi
412
+
413
+ return 0
414
+ }
415
+
416
+ extract_quickstart_compose_up_command() {
417
+ (
418
+ cd "${QUICKSTART_DIR}"
419
+ make -n start 2>/dev/null | awk '/docker compose .* up -d --no-recreate/{line=$0} END{print line}'
420
+ )
421
+ }
422
+
423
+ try_fast_start_localnet() {
424
+ local compose_up_command=""
425
+
426
+ compose_up_command="$(extract_quickstart_compose_up_command)"
427
+ if [[ -z "${compose_up_command}" ]]; then
428
+ log "Unable to determine quickstart compose startup command."
429
+ return 1
430
+ fi
431
+
432
+ log "Starting cn-quickstart with fast path (skip quickstart rebuild)..."
433
+ run_quickstart_command "${compose_up_command}"
434
+ }
435
+
436
+ start_infra_only_localnet() {
437
+ log "Starting cn-quickstart LocalNet infrastructure only (skip quickstart app, PQS, and onboarding)."
438
+ run_infra_compose 'up -d --no-recreate'
439
+ }
440
+
441
+ stop_infra_only_localnet() {
442
+ if [[ ! -f "${QUICKSTART_DIR}/.env.local" ]]; then
443
+ return
444
+ fi
445
+
446
+ log "Stopping cn-quickstart LocalNet infrastructure-only stack..."
447
+ run_infra_compose down || true
448
+ }
449
+
450
+ wait_for_services() {
451
+ log "Waiting for Keycloak..."
452
+ for _ in $(seq 1 30); do
453
+ if curl --connect-timeout "${CURL_CONNECT_TIMEOUT}" --max-time "${CURL_MAX_TIME}" -fsS http://localhost:8082/realms/AppProvider >/dev/null 2>&1; then
454
+ break
455
+ fi
456
+ sleep 2
457
+ done
458
+ if ! curl --connect-timeout "${CURL_CONNECT_TIMEOUT}" --max-time "${CURL_MAX_TIME}" -fsS http://localhost:8082/realms/AppProvider >/dev/null 2>&1; then
459
+ log "Keycloak did not become ready."
460
+ exit 1
461
+ fi
462
+
463
+ log "Waiting for Validator API..."
464
+ for _ in $(seq 1 30); do
465
+ 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)"
466
+ if [[ "${code}" == "200" || "${code}" == "401" ]]; then
467
+ break
468
+ fi
469
+ sleep 2
470
+ done
471
+ 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)"
472
+ if [[ "${code}" != "200" && "${code}" != "401" ]]; then
473
+ log "Validator API did not become ready."
474
+ exit 1
475
+ fi
476
+
477
+ log "Waiting for Scan API..."
478
+ for _ in $(seq 1 30); do
479
+ 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
480
+ break
481
+ fi
482
+ sleep 2
483
+ done
484
+ 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
485
+ log "Scan API did not become ready."
486
+ exit 1
487
+ fi
488
+
489
+ log "All localnet services are ready."
490
+ }
491
+
492
+ start_localnet() {
493
+ if quickstart_force_full_start; then
494
+ log "Forcing full cn-quickstart start (CANTON_LOCALNET_FORCE_FULL_START=true)."
495
+ run_quickstart_make start
496
+ wait_for_services
497
+ return
498
+ fi
499
+
500
+ if quickstart_infra_only_enabled; then
501
+ start_infra_only_localnet
502
+ wait_for_services
503
+ return
504
+ fi
505
+
506
+ if quickstart_fast_start_enabled && quickstart_build_artifacts_ready; then
507
+ if try_fast_start_localnet; then
508
+ wait_for_services
509
+ return
510
+ fi
511
+ log "Fast start failed; falling back to full cn-quickstart start."
512
+ fi
513
+
514
+ log "Starting cn-quickstart with full build..."
515
+ run_quickstart_make start
516
+ wait_for_services
517
+ }
518
+
519
+ stop_localnet() {
520
+ if [[ ! -d "${QUICKSTART_DIR}" ]]; then
521
+ log "cn-quickstart directory not found; nothing to stop."
522
+ stop_managed_dockerd
523
+ return
524
+ fi
525
+
526
+ log "Stopping cn-quickstart..."
527
+ stop_infra_only_localnet
528
+ if [[ -f "${QUICKSTART_DIR}/Makefile" ]]; then
529
+ run_quickstart_make stop || true
530
+ else
531
+ log "Quickstart Makefile not found; skipping quickstart stop target."
532
+ fi
533
+ stop_managed_dockerd
534
+ }
535
+
536
+ stop_managed_dockerd() {
537
+ local pid=""
538
+ local cmd=""
539
+
540
+ if [[ ! -f "${DOCKERD_PID_FILE}" ]]; then
541
+ return
542
+ fi
543
+
544
+ pid="$(cat "${DOCKERD_PID_FILE}" 2>/dev/null || true)"
545
+ if [[ -z "${pid}" ]]; then
546
+ rm -f "${DOCKERD_PID_FILE}" "${DOCKERD_LOG_FILE}"
547
+ return
548
+ fi
549
+
550
+ if ! ps -p "${pid}" >/dev/null 2>&1; then
551
+ rm -f "${DOCKERD_PID_FILE}" "${DOCKERD_LOG_FILE}"
552
+ return
553
+ fi
554
+
555
+ cmd="$(ps -p "${pid}" -o comm= 2>/dev/null | tr -d '[:space:]')"
556
+ if [[ "${cmd}" != "dockerd" ]]; then
557
+ log "PID ${pid} is not dockerd; skipping daemon cleanup."
558
+ rm -f "${DOCKERD_PID_FILE}"
559
+ return
560
+ fi
561
+
562
+ if ! sudo -n true >/dev/null 2>&1; then
563
+ log "Cannot stop managed dockerd without passwordless sudo."
564
+ return
565
+ fi
566
+
567
+ log "Stopping managed dockerd (pid ${pid})..."
568
+ sudo kill -TERM "${pid}" >/dev/null 2>&1 || true
569
+ for _ in $(seq 1 10); do
570
+ if ! ps -p "${pid}" >/dev/null 2>&1; then
571
+ break
572
+ fi
573
+ sleep 1
574
+ done
575
+ if ps -p "${pid}" >/dev/null 2>&1; then
576
+ sudo kill -KILL "${pid}" >/dev/null 2>&1 || true
577
+ fi
578
+ rm -f "${DOCKERD_PID_FILE}" "${DOCKERD_LOG_FILE}"
579
+ }
580
+
581
+ status_localnet() {
582
+ if docker_ready; then
583
+ log "Docker daemon is running."
584
+ else
585
+ log "Docker daemon is not running."
586
+ exit 1
587
+ fi
588
+
589
+ run_docker ps --format 'table {{.Names}}\t{{.Status}}\t{{.Ports}}'
590
+ echo
591
+
592
+ keycloak_ok="no"
593
+ validator_ok="no"
594
+ scan_ok="no"
595
+ if curl --connect-timeout "${CURL_CONNECT_TIMEOUT}" --max-time "${CURL_MAX_TIME}" -fsS http://localhost:8082/realms/AppProvider >/dev/null 2>&1; then
596
+ keycloak_ok="yes"
597
+ fi
598
+ 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)"
599
+ if [[ "${validator_code}" == "200" || "${validator_code}" == "401" ]]; then
600
+ validator_ok="yes"
601
+ fi
602
+ 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
603
+ scan_ok="yes"
604
+ fi
605
+
606
+ printf 'Keycloak ready: %s\n' "${keycloak_ok}"
607
+ printf 'Validator ready: %s (HTTP %s)\n' "${validator_ok}" "${validator_code:-n/a}"
608
+ printf 'Scan ready: %s\n' "${scan_ok}"
609
+ }
610
+
611
+ show_localnet_logs() {
612
+ log "==================== Docker Containers ===================="
613
+ if docker_ready; then
614
+ run_docker ps -a || true
615
+ else
616
+ log "Docker daemon is not running."
617
+ fi
618
+
619
+ echo
620
+ log "==================== Docker Compose Logs ===================="
621
+ if [[ -f "${QUICKSTART_DIR}/.env.local" ]]; then
622
+ run_infra_compose 'logs --tail=100' || true
623
+ elif [[ -d "${QUICKSTART_DIR}" ]]; then
624
+ log "Quickstart local env not found; skipping compose logs: ${QUICKSTART_DIR}/.env.local"
625
+ else
626
+ log "cn-quickstart directory not found: ${QUICKSTART_DIR}"
627
+ fi
628
+
629
+ echo
630
+ log "==================== Canton Logs ===================="
631
+ if [[ -f "${QUICKSTART_DIR}/logs/canton.log" ]]; then
632
+ tail -100 "${QUICKSTART_DIR}/logs/canton.log" || true
633
+ else
634
+ log "Canton log not found: ${QUICKSTART_DIR}/logs/canton.log"
635
+ fi
636
+
637
+ if [[ -f "${DOCKERD_LOG_FILE}" ]]; then
638
+ echo
639
+ log "==================== Managed Docker Daemon Logs ===================="
640
+ tail -100 "${DOCKERD_LOG_FILE}" || true
641
+ fi
642
+ }
643
+
644
+ run_smoke() {
645
+ local validator_code=""
646
+
647
+ log "Running localnet smoke checks..."
648
+
649
+ if ! curl --connect-timeout "${CURL_CONNECT_TIMEOUT}" --max-time "${CURL_MAX_TIME}" -fsS http://localhost:8082/realms/AppProvider >/dev/null 2>&1; then
650
+ log "Keycloak is not reachable."
651
+ exit 1
652
+ fi
653
+
654
+ 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)"
655
+ if [[ "${validator_code}" != "200" && "${validator_code}" != "401" ]]; then
656
+ log "Validator API is not reachable (HTTP ${validator_code})."
657
+ exit 1
658
+ fi
659
+
660
+ 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
661
+ log "Scan API is not reachable."
662
+ exit 1
663
+ fi
664
+
665
+ log "Smoke checks passed."
666
+ }
667
+
668
+ read_npm_script() {
669
+ local target_dir="$1"
670
+ local script_name="$2"
671
+
672
+ if [[ ! -f "${target_dir}/package.json" ]]; then
673
+ return
674
+ fi
675
+
676
+ 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}"
677
+ }
678
+
679
+ script_is_recursive_localnet_test() {
680
+ local script_value="$1"
681
+ echo "${script_value}" | grep -Eq '(^|[[:space:]])(canton-localnet[[:space:]]+test|[^[:space:]]*localnet-cloud\.sh[[:space:]]+test)([[:space:]]|$)'
682
+ }
683
+
684
+ run_integration_tests() {
685
+ local script_value=""
686
+
687
+ if [[ -n "${CANTON_LOCALNET_TEST_CMD:-}" ]]; then
688
+ log "Running custom integration command from CANTON_LOCALNET_TEST_CMD..."
689
+ (
690
+ cd "${PROJECT_ROOT}"
691
+ bash -lc "${CANTON_LOCALNET_TEST_CMD}"
692
+ )
693
+ return
694
+ fi
695
+
696
+ script_value="$(read_npm_script "${PROJECT_ROOT}" "test:integration")"
697
+ if [[ -n "${script_value}" ]]; then
698
+ if script_is_recursive_localnet_test "${script_value}"; then
699
+ log "Skipping recursive test:integration script."
700
+ else
701
+ log "Running project integration tests: npm run test:integration"
702
+ (
703
+ cd "${PROJECT_ROOT}"
704
+ npm run test:integration
705
+ )
706
+ return
707
+ fi
708
+ fi
709
+
710
+ script_value="$(read_npm_script "${PROJECT_ROOT}" "test:localnet")"
711
+ if [[ -n "${script_value}" ]]; then
712
+ if script_is_recursive_localnet_test "${script_value}"; then
713
+ log "Skipping recursive test:localnet script."
714
+ else
715
+ log "Running project integration tests: npm run test:localnet"
716
+ (
717
+ cd "${PROJECT_ROOT}"
718
+ npm run test:localnet
719
+ )
720
+ return
721
+ fi
722
+ fi
723
+
724
+ if [[ "${PROJECT_ROOT}" == "${REPO_ROOT}" && -d "${REPO_ROOT}/test/integration/localnet" && -d "${REPO_ROOT}/node_modules" ]]; then
725
+ log "Running bundled SDK localnet integration tests..."
726
+ (
727
+ cd "${REPO_ROOT}"
728
+ npm test -- test/integration/localnet
729
+ )
730
+ return
731
+ fi
732
+
733
+ log "No integration test command configured; skipping test step."
734
+ }
735
+
736
+ usage() {
737
+ cat <<'USAGE'
738
+ Usage: scripts/localnet/localnet-cloud.sh <command>
739
+
740
+ Commands:
741
+ setup Install prerequisites, init submodules, configure quickstart
742
+ start Start localnet and wait for ready endpoints
743
+ stop Stop localnet services
744
+ logs Show localnet diagnostic logs
745
+ status Show docker + endpoint status
746
+ smoke Run endpoint smoke checks
747
+ test Run project integration tests (if configured)
748
+ verify Run setup + start + smoke + test
749
+
750
+ Environment:
751
+ CANTON_LOCALNET_FAST_START=true|false Enable fast startup path (default: true)
752
+ CANTON_LOCALNET_FORCE_FULL_START=true|false Force full startup with rebuild
753
+ CANTON_LOCALNET_INFRA_ONLY=true|false Start only LocalNet + Keycloak infrastructure
754
+ CANTON_LOCALNET_QUICKSTART_DIR=<path> Use an existing cn-quickstart/quickstart directory
755
+ USAGE
756
+ }
757
+
758
+ main() {
759
+ if [[ "${1:-}" == "" ]]; then
760
+ usage
761
+ exit 1
762
+ fi
763
+
764
+ case "$1" in
765
+ setup)
766
+ ensure_docker_packages
767
+ start_docker_daemon
768
+ ensure_submodules
769
+ ensure_hosts_entries
770
+ quickstart_setup
771
+ ;;
772
+ start)
773
+ require_command curl
774
+ ensure_docker_packages
775
+ start_docker_daemon
776
+ ensure_submodules
777
+ ensure_hosts_entries
778
+ quickstart_setup
779
+ start_localnet
780
+ ;;
781
+ stop)
782
+ stop_localnet
783
+ ;;
784
+ logs)
785
+ show_localnet_logs
786
+ ;;
787
+ status)
788
+ require_command curl
789
+ status_localnet
790
+ ;;
791
+ smoke)
792
+ require_command curl
793
+ run_smoke
794
+ ;;
795
+ test)
796
+ run_integration_tests
797
+ ;;
798
+ verify)
799
+ require_command curl
800
+ ensure_docker_packages
801
+ start_docker_daemon
802
+ ensure_submodules
803
+ ensure_hosts_entries
804
+ quickstart_setup
805
+ start_localnet
806
+ run_smoke
807
+ run_integration_tests
808
+ ;;
809
+ *)
810
+ usage
811
+ exit 1
812
+ ;;
813
+ esac
814
+ }
815
+
816
+ main "$@"