aboutsummaryrefslogtreecommitdiffstats
path: root/setup.sh
blob: e556b637041590867028502c26067d9ee34ae493 (plain) (blame)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
#!/usr/bin/env bash
set -euo pipefail

########################################################################
# coinos regtest setup
#
# Sets up a complete local development environment:
#   Bitcoin Core, Liquid, Core Lightning (x3), TigerBeetle,
#   Nostr relay (strfry), Cashu mint, Ark, Esplora, and the coinos API
#
# Usage:
#   chmod +x setup.sh && ./setup.sh
#
# Idempotent — safe to re-run at any point.
########################################################################

# -- configuration --
BITCOIN_BLOCKS=500
CHANNEL_SATS=500000
LN_FUND_BTC=1
ARK_FUND_BTC=1
SNAPSHOT_URL="https://github.com/coinos/coinos-server/releases/download/regtest-data/regtest-data.tar.gz"
USE_SNAPSHOT=true  # set to false to generate everything from scratch

# -- colors --
RED='\033[0;31m'
GREEN='\033[0;32m'
YELLOW='\033[1;33m'
BLUE='\033[0;34m'
BOLD='\033[1m'
NC='\033[0m'

info()  { echo -e "  ${BLUE}•${NC} $*"; }
ok()    { echo -e "  ${GREEN}✓${NC} $*"; }
warn()  { echo -e "  ${YELLOW}!${NC} $*"; }
err()   { echo -e "  ${RED}✗${NC} $*"; }
step()  { echo -e "\n${BOLD}${GREEN}▸ $*${NC}"; }

# Bootstrap: if run via curl (not from inside the repo), clone first
if [ ! -f "compose.yml.sample" ]; then
  REPO_DIR="${COINOS_DIR:-$HOME/coinos-server}"
  if [ ! -d "$REPO_DIR" ]; then
    echo -e "  ${BLUE}•${NC} Cloning coinos-server..."
    git clone https://github.com/coinos/coinos-server.git "$REPO_DIR"
    cd "$REPO_DIR"
    git checkout staging 2>/dev/null || true
  else
    cd "$REPO_DIR"
  fi
  exec bash "$REPO_DIR/setup.sh" "$@"
fi

DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
cd "$DIR"

# ── helpers ──────────────────────────────────────────────────────────

wait_for() {
  local name="$1" cmd="$2" retries="${3:-30}" delay="${4:-2}"
  local i=0
  info "Waiting for $name..."
  while ! eval "$cmd" &>/dev/null 2>&1; do
    i=$((i + 1))
    if [ "$i" -ge "$retries" ]; then
      err "$name did not become ready after $((retries * delay))s"
      return 1
    fi
    sleep "$delay"
  done
  ok "$name"
}

bcli() { docker exec bc bitcoin-cli -regtest "$@"; }
lcli() { docker exec lq elements-cli -chain=liquidregtest "$@"; }
clcli()  { docker exec cl  lightning-cli --network=regtest "$@"; }
clbcli() { docker exec clb lightning-cli --network=regtest "$@"; }
clccli() { docker exec clc lightning-cli --network=regtest "$@"; }

mine() {
  local n="${1:-1}"
  local addr
  addr=$(bcli getnewaddress "" "bech32")
  bcli generatetoaddress "$n" "$addr" > /dev/null
}

# ── phase 1: prerequisites ──────────────────────────────────────────

check_prereqs() {
  step "Checking prerequisites"

  # Docker
  if ! command -v docker &>/dev/null; then
    info "Docker not found — installing via get.docker.com..."
    # Install script is noisy (dumps systemd status); suppress output
    set +e
    curl -fsSL https://get.docker.com | sudo sh >/dev/null 2>&1
    set -e
    if ! command -v docker &>/dev/null; then
      err "Docker installation failed"
      exit 1
    fi
    sudo usermod -aG docker "$USER" 2>/dev/null || true
    ok "Docker installed"
  fi

  if ! docker info &>/dev/null 2>&1; then
    # Docker group membership not active in current shell — re-exec via sg
    if getent group docker | grep -qw "$USER" 2>/dev/null; then
      info "Activating docker group membership..."
      sg docker -c "cd $DIR && bash $DIR/setup.sh"
      exit $?
    fi
    err "Cannot connect to Docker daemon."
    err "Try: sudo usermod -aG docker \$USER && newgrp docker && ./setup.sh"
    exit 1
  fi
  ok "Docker $(docker --version | awk '{print $3}' | tr -d ',')"

  # Docker Compose
  if ! docker compose version &>/dev/null 2>&1; then
    err "docker compose plugin not found"
    err "Install: https://docs.docker.com/compose/install/"
    exit 1
  fi
  ok "Docker Compose $(docker compose version --short)"

  # Disk space (want at least 2GB free)
  local avail_kb
  avail_kb=$(df -k "$DIR" | tail -1 | awk '{print $4}')
  local avail_gb=$((avail_kb / 1024 / 1024))
  if [ "$avail_gb" -lt 2 ]; then
    warn "Only ${avail_gb}GB free disk space (recommend ≥2GB)"
  else
    ok "${avail_gb}GB free disk space"
  fi
}

# ── phase 2: configuration files ────────────────────────────────────

setup_config() {
  step "Setting up configuration"

  if [ ! -f config.ts ]; then
    cp config.ts.sample config.ts
    ok "Created config.ts"
  else
    ok "config.ts exists"
  fi

  if [ -f compose.yml ]; then
    if ! diff -q compose.yml compose.yml.sample >/dev/null 2>&1; then
      warn "compose.yml differs from compose.yml.sample"
      read -rp "  Overwrite compose.yml with compose.yml.sample? [Y/n] " answer
      if [ "${answer,,}" != "n" ]; then
        cp compose.yml compose.yml.bak
        cp compose.yml.sample compose.yml
        ok "compose.yml updated (old saved as compose.yml.bak)"
      else
        ok "compose.yml kept as-is"
      fi
    else
      ok "compose.yml"
    fi
  else
    cp compose.yml.sample compose.yml
    ok "compose.yml"
  fi
}

# ── phase 3: data directories ───────────────────────────────────────

setup_data() {
  step "Setting up data directories"

  if [ -d data ]; then
    ok "data/ exists"
  elif [ "$USE_SNAPSHOT" = true ]; then
    # Try to download pre-built regtest snapshot
    info "Downloading regtest snapshot (4MB)..."
    if curl -fsSL "$SNAPSHOT_URL" -o /tmp/regtest-data.tar.gz 2>/dev/null; then
      tar xzf /tmp/regtest-data.tar.gz
      rm -f /tmp/regtest-data.tar.gz
      # Also extract config.ts if it was in the snapshot and we don't have one
      ok "Extracted regtest snapshot"
      SNAPSHOT_LOADED=true
    else
      warn "Snapshot download failed — will generate from scratch"
      cp -r sampledata data
      ok "Copied sampledata → data"
    fi
  else
    cp -r sampledata data
    ok "Copied sampledata → data"
  fi

  # Create directories not in sampledata
  local dirs=(
    data/lightningc
    data/strfry/db
    data/ark
    data/arkd-wallet
    data/tigerbeetle
    data/mint
    data/archive-kv
    data/sockets
    logs
    .aws
  )
  for d in "${dirs[@]}"; do
    mkdir -p "$d"
  done

  # Lightning C config (third node)
  if [ -f sampledata/lightningc/config ] && [ ! -f data/lightningc/config ]; then
    cp sampledata/lightningc/config data/lightningc/config
    ok "Created Lightning C config"
  fi

  # Strfry config
  if [ -f sampledata/strfry/strfry.conf ] && [ ! -f data/strfry/strfry.conf ]; then
    cp sampledata/strfry/strfry.conf data/strfry/strfry.conf
    ok "Created strfry.conf"
  fi

  # Kvrocks config
  if [ -f sampledata/archive-kv/kvrocks.conf ] && [ ! -f data/archive-kv/kvrocks.conf ]; then
    cp sampledata/archive-kv/kvrocks.conf data/archive-kv/kvrocks.conf
    ok "Created kvrocks.conf"
  fi

  # Valkey config (compose expects valkey.conf, not keydb.conf)
  if [ ! -f data/db/valkey.conf ]; then
    if [ -f sampledata/db/valkey.conf ]; then
      cp sampledata/db/valkey.conf data/db/valkey.conf
    else
      cat > data/db/valkey.conf <<'EOF'
dir /data
loglevel warning
save ""
appendonly yes
appendfsync everysec
EOF
    fi
    ok "Created valkey.conf"
  fi

  # Fix nostr data permissions (bun user = 100:100 inside container)
  if [ -d data/nostr ]; then
    sudo chown -R 100:100 data/nostr 2>/dev/null || true
  fi

  ok "All data directories ready"
}

# ── phase 4: TigerBeetle ────────────────────────────────────────────

setup_tigerbeetle() {
  step "Setting up TigerBeetle"

  if [ -f data/tigerbeetle/0_0.tigerbeetle ]; then
    ok "Data file already exists"
    return
  fi

  info "Formatting TigerBeetle data file..."
  docker run --rm --privileged \
    -v "$DIR/data/tigerbeetle:/data" \
    ghcr.io/coinos/tigerbeetle \
    format --cluster=0 --replica=0 --replica-count=1 /data/0_0.tigerbeetle

  ok "TigerBeetle formatted"
}

# ── phase 5: Docker network ─────────────────────────────────────────

setup_network() {
  step "Setting up Docker network"

  if docker network inspect net &>/dev/null 2>&1; then
    ok "Network 'net' exists"
  else
    docker network create net
    ok "Created network 'net'"
  fi
}

# ── phase 6: install dependencies ───────────────────────────────────

install_deps() {
  step "Installing application dependencies"

  # Server deps
  if [ -d node_modules ] && [ -d node_modules/.cache ]; then
    ok "Server node_modules exists"
  else
    info "Running bun install for server..."
    docker run --rm \
      -v "$DIR":/home/bun/app \
      -w /home/bun/app \
      ghcr.io/coinos/base bun i
    ok "Server dependencies installed"
  fi

  # UI deps
  local ui_dir="$DIR/../coinos-ui"
  if [ ! -d "$ui_dir" ]; then
    info "Cloning coinos-ui..."
    git clone -b staging https://github.com/coinos/coinos-ui.git "$ui_dir" 2>/dev/null || {
      warn "Could not clone coinos-ui — skipping frontend setup"
      return
    }
  fi

  if [ -d "$ui_dir/node_modules" ]; then
    ok "UI node_modules exists"
  else
    info "Running bun install for UI..."
    docker run --rm \
      -v "$ui_dir":/home/bun/app \
      -w /home/bun/app \
      ghcr.io/coinos/base bun i
    ok "UI dependencies installed"
  fi
}

# ── phase 7: pull images ────────────────────────────────────────────

pull_images() {
  step "Pulling container images"

  info "This may take a few minutes on first run..."
  docker compose pull --ignore-pull-failures 2>&1 | tail -3 || true

  ok "Images ready"
}

# ── phase 8: start services ─────────────────────────────────────────

start_services() {
  step "Starting services"

  docker compose up -d 2>&1
  ok "Containers started"
}

# ── phase 9: wait for services ──────────────────────────────────────

wait_for_services() {
  step "Waiting for services"

  wait_for "Valkey"          "docker exec db valkey-cli ping"    30 2
  wait_for "Bitcoin Core"    "bcli getblockchaininfo"            30 2
  wait_for "Liquid"          "lcli getblockchaininfo"            30 2
  wait_for "Lightning (cl)"  "clcli getinfo"                     60 3
  wait_for "Lightning (clb)" "clbcli getinfo"                    60 3
  wait_for "Lightning (clc)" "clccli getinfo"                    60 3
  wait_for "TigerBeetle"     "docker ps --filter name=^tb$ --filter status=running -q" 30 2
}

# ── phase 10: wallets ───────────────────────────────────────────────

setup_wallets() {
  step "Setting up wallets"

  # Bitcoin
  if bcli listwallets 2>/dev/null | grep -q coinos; then
    ok "Bitcoin 'coinos' wallet exists"
  else
    bcli createwallet coinos >/dev/null 2>&1 || true
    ok "Created Bitcoin 'coinos' wallet"
  fi

  # Liquid
  if lcli listwallets 2>/dev/null | grep -q coinos; then
    ok "Liquid 'coinos' wallet exists"
  else
    lcli createwallet coinos >/dev/null 2>&1 || true
    ok "Created Liquid 'coinos' wallet"
  fi

  bcli rescanblockchain >/dev/null 2>&1 &
}

# ── phase 11: generate blocks & fund ────────────────────────────────

fund_regtest() {
  step "Funding regtest environment"

  local height
  height=$(bcli getblockcount 2>/dev/null || echo 0)
  if [ "$height" -lt "$BITCOIN_BLOCKS" ]; then
    info "Generating blocks (current: $height, target: $BITCOIN_BLOCKS)..."
    local addr
    addr=$(bcli getnewaddress "" "bech32")
    bcli generatetoaddress "$BITCOIN_BLOCKS" "$addr" > /dev/null
    ok "Generated $BITCOIN_BLOCKS blocks"
  else
    ok "Already have $height blocks"
  fi

  # Fund Lightning nodes
  info "Funding Lightning nodes ($LN_FUND_BTC BTC each)..."

  local cl_addr clb_addr clc_addr
  cl_addr=$(clcli  newaddr 2>/dev/null | grep -m1 -o '"bech32": "[^"]*"' | cut -d'"' -f4)
  clb_addr=$(clbcli newaddr 2>/dev/null | grep -m1 -o '"bech32": "[^"]*"' | cut -d'"' -f4)
  clc_addr=$(clccli newaddr 2>/dev/null | grep -m1 -o '"bech32": "[^"]*"' | cut -d'"' -f4)

  bcli sendtoaddress "$cl_addr"  "$LN_FUND_BTC" > /dev/null
  bcli sendtoaddress "$clb_addr" "$LN_FUND_BTC" > /dev/null
  bcli sendtoaddress "$clc_addr" "$LN_FUND_BTC" > /dev/null
  ok "Sent $LN_FUND_BTC BTC to each Lightning node"

  mine 10
  ok "Confirmed funding transactions"

  # Wait for Lightning nodes to see confirmed funds
  info "Waiting for Lightning nodes to sync funds..."
  wait_for "cl funds"  "clcli  listfunds 2>/dev/null | grep -q outputs" 30 3 || true
  wait_for "clb funds" "clbcli listfunds 2>/dev/null | grep -q outputs" 30 3 || true
  wait_for "clc funds" "clccli listfunds 2>/dev/null | grep -q outputs" 30 3 || true
}

# ── phase 12: Lightning channels ────────────────────────────────────

setup_channels() {
  step "Opening Lightning channels"

  # Wait for confirmed funds before opening channels
  wait_for "cl confirmed funds" \
    "clcli listfunds 2>/dev/null | grep -q '\"status\": \"confirmed\"'" 30 3 || true

  # Use set +e for channel ops — failures are handled gracefully
  set +e

  # cl → clb
  local clb_id
  clb_id=$(clbcli getinfo 2>/dev/null | grep -m1 -o '"id": "[^"]*"' | cut -d'"' -f4)
  if [ -z "$clb_id" ]; then
    warn "Could not get clb node ID — skipping channels"
    set -e
    return
  fi

  clcli connect "$clb_id" clb 9735 > /dev/null 2>&1
  if clcli listpeerchannels 2>/dev/null | grep -q "$clb_id"; then
    ok "cl → clb channel exists"
  else
    local result
    result=$(clcli fundchannel "$clb_id" "$CHANNEL_SATS" 2>&1)
    if echo "$result" | grep -q "txid"; then
      ok "Opened cl → clb ($CHANNEL_SATS sats)"
    else
      warn "cl → clb channel: $result"
    fi
  fi

  # Mine blocks and wait for change UTXO to be confirmed before second channel
  mine 6
  info "Waiting for change UTXO to confirm..."
  sleep 8

  # cl → clc
  local clc_id
  clc_id=$(clccli getinfo 2>/dev/null | grep -m1 -o '"id": "[^"]*"' | cut -d'"' -f4)
  if [ -n "$clc_id" ]; then
    clcli connect "$clc_id" clc 9735 > /dev/null 2>&1
    if clcli listpeerchannels 2>/dev/null | grep -q "$clc_id"; then
      ok "cl → clc channel exists"
    else
      result=$(clcli fundchannel "$clc_id" "$CHANNEL_SATS" 2>&1)
      if echo "$result" | grep -q "txid"; then
        ok "Opened cl → clc ($CHANNEL_SATS sats)"
      else
        warn "cl → clc channel: $result"
      fi
    fi
  fi

  set -e

  # Confirm channel opens + wait for maturity
  mine 10
  info "Waiting for channels to become active..."
  sleep 5
  mine 6
  ok "Lightning channels ready"
}

# ── phase 13: Ark ───────────────────────────────────────────────────

setup_ark() {
  step "Setting up Ark"

  if ! docker ps --format '{{.Names}}' | grep -q '^arkd$'; then
    warn "arkd container not running — skipping"
    return
  fi

  # Arkd wallet needs initialization on first run
  # Wait for arkd admin port to be available
  wait_for "arkd admin" \
    "docker exec arkd wget -qO- http://localhost:7071/v1/admin/wallet/status 2>/dev/null | grep -q initialized" 30 3 || {
    warn "arkd admin not available — skipping"
    return
  }

  local wallet_status
  wallet_status=$(docker exec arkd wget -qO- http://localhost:7071/v1/admin/wallet/status 2>/dev/null) || true
  if echo "$wallet_status" | grep -q '"initialized":false'; then
    info "Initializing Ark wallet..."
    # Step 1: Get seed
    local seed
    seed=$(docker exec arkd wget -qO- http://localhost:7071/v1/admin/wallet/seed 2>/dev/null \
      | grep -m1 -o '"seed":"[^"]*"' | cut -d'"' -f4) || true
    if [ -z "$seed" ]; then
      warn "Could not get Ark wallet seed"
      return
    fi
    # Step 2: Create wallet with seed + password
    docker exec arkd sh -c "wget -qO- --header='Content-Type: application/json' \
      --post-data='{\"seed\":\"$seed\",\"password\":\"testpassword\"}' \
      http://localhost:7071/v1/admin/wallet/create" >/dev/null 2>&1 || true
    sleep 2
    # Step 3: Unlock
    docker exec arkd sh -c "wget -qO- --header='Content-Type: application/json' \
      --post-data='{\"password\":\"testpassword\"}' \
      http://localhost:7071/v1/admin/wallet/unlock" >/dev/null 2>&1 || true
    sleep 3
    ok "Ark wallet initialized"
  elif echo "$wallet_status" | grep -q '"unlocked":false'; then
    info "Unlocking Ark wallet..."
    docker exec arkd sh -c "wget -qO- --header='Content-Type: application/json' \
      --post-data='{\"password\":\"testpassword\"}' \
      http://localhost:7071/v1/admin/wallet/unlock" >/dev/null 2>&1 || true
    sleep 3
    ok "Ark wallet unlocked"
  else
    ok "Ark wallet ready"
  fi

  # Wait for arkd API to become available (may take a few seconds after unlock)
  wait_for "arkd API" "docker exec arkd wget -qO- http://localhost:7070/v1/info 2>/dev/null | grep -q signerPubkey" 30 3 || {
    warn "arkd API not available — skipping Ark funding"
    return
  }

  # Extract server pubkey
  local ark_info ark_pubkey
  ark_info=$(docker exec arkd wget -qO- http://localhost:7070/v1/info 2>/dev/null) || true

  ark_pubkey=$(echo "$ark_info" | grep -m1 -o '"signerPubkey":"[^"]*"' | cut -d'"' -f4) || true
  if [ -z "$ark_pubkey" ]; then
    ark_pubkey=$(echo "$ark_info" | grep -m1 -o '"signerPubkey": "[^"]*"' | cut -d'"' -f4) || true
  fi

  if [ -n "$ark_pubkey" ]; then
    ok "Ark pubkey: ${ark_pubkey:0:20}..."
    if grep -q 'arkServerPublicKey: ""' config.ts 2>/dev/null; then
      sed -i "s|arkServerPublicKey: \"\"|arkServerPublicKey: \"$ark_pubkey\"|" config.ts
      ok "Updated config.ts with Ark pubkey"
    fi
  fi

  # Fund Ark wallet
  local boarding_addr ark_wallet_resp
  ark_wallet_resp=$(docker exec arkd wget -qO- http://localhost:7071/v1/admin/wallet/address 2>/dev/null) || true
  boarding_addr=$(echo "$ark_wallet_resp" | grep -m1 -o '"address":"[^"]*"' | cut -d'"' -f4) || true
  if [ -z "$boarding_addr" ]; then
    boarding_addr=$(echo "$ark_wallet_resp" | grep -m1 -o '"address": "[^"]*"' | cut -d'"' -f4) || true
  fi

  if [ -n "$boarding_addr" ]; then
    bcli sendtoaddress "$boarding_addr" "$ARK_FUND_BTC" > /dev/null
    mine 6
    ok "Funded Ark wallet with $ARK_FUND_BTC BTC"
  else
    warn "Could not get Ark boarding address"
  fi
}

# ── phase 14: restart app ───────────────────────────────────────────

restart_app() {
  step "Restarting app"

  docker compose restart app >/dev/null 2>&1
  sleep 3
  ok "App restarted"
}

# ── phase 15: verify ────────────────────────────────────────────────

verify() {
  step "Verifying setup"

  echo ""
  local services=("app" "db" "bc" "lq" "cl" "clb" "clc" "tb" "sf" "cs" "eb" "arc")
  local optional=("arkd" "arkd-wallet" "nbxplorer" "pgnbxplorer" "mon" "mint" "wallet")

  for svc in "${services[@]}"; do
    if docker ps --format '{{.Names}}' | grep -q "^${svc}$"; then
      ok "$svc"
    else
      err "$svc not running"
    fi
  done

  for svc in "${optional[@]}"; do
    if docker ps --format '{{.Names}}' | grep -q "^${svc}$"; then
      ok "$svc (optional)"
    fi
  done

  # Summary
  echo ""
  local chan_count
  chan_count=$(clcli listpeerchannels 2>/dev/null | grep -c '"state":') || chan_count=0
  info "$chan_count Lightning channel(s)"

  local block_height
  block_height=$(bcli getblockcount 2>/dev/null || echo "?")
  info "Block height: $block_height"

  echo ""
  echo -e "${BOLD}────────────────────────────────────────${NC}"
  echo -e "${GREEN}${BOLD}  Setup complete!${NC}"
  echo -e "${BOLD}────────────────────────────────────────${NC}"
  echo ""
  echo "  API server:    http://localhost:3119"
  echo "  Esplora:       http://localhost:3000"
  echo "  Nostr relay:   ws://localhost:7777"
  echo "  Cashu mint:    http://localhost:3338"
  echo "  Ark server:    http://localhost:7070"
  echo ""
  echo "  Frontend:  cd ~/coinos-ui && bun dev"
  echo ""
  echo "  Mine a block:"
  echo "    docker exec bc bitcoin-cli -regtest generatetoaddress 1 \\"
  echo "      \$(docker exec bc bitcoin-cli -regtest getnewaddress)"
  echo ""
}

# ── main ─────────────────────────────────────────────────────────────

main() {
  SNAPSHOT_LOADED=false

  echo ""
  echo -e "${BOLD}╔════════════════════════════════════════╗${NC}"
  echo -e "${BOLD}║      coinos regtest setup              ║${NC}"
  echo -e "${BOLD}╚════════════════════════════════════════╝${NC}"

  check_prereqs
  setup_config
  setup_data
  setup_tigerbeetle
  setup_network
  install_deps
  pull_images
  start_services
  wait_for_services

  if [ "$SNAPSHOT_LOADED" = true ]; then
    # Snapshot has wallets, blocks, channels, ark — just need to
    # wait for services to load the pre-existing data
    step "Using pre-built regtest snapshot"
    wait_for "Bitcoin wallet" "bcli listwallets 2>/dev/null | grep -q coinos" 30 2 || true
    wait_for "Liquid wallet" "lcli listwallets 2>/dev/null | grep -q coinos" 30 2 || true
    ok "Snapshot data loaded"
  else
    setup_wallets
    fund_regtest
    setup_channels
    setup_ark
    restart_app
  fi

  verify
}

# ── snapshot command ──────────────────────────────────────────────────

create_snapshot() {
  step "Creating regtest snapshot"

  info "Stopping services..."
  docker compose down 2>/dev/null || true

  info "Packaging data/ and config.ts..."
  tar czf regtest-data.tar.gz data/ config.ts 2>/dev/null || \
  sudo tar czf regtest-data.tar.gz data/ config.ts

  local size
  size=$(ls -lh regtest-data.tar.gz | awk '{print $5}')
  ok "Created regtest-data.tar.gz ($size)"
  echo ""
  echo "  Upload to GitHub Releases:"
  echo "    gh release create regtest-data --title 'Regtest Data Snapshot' regtest-data.tar.gz"
  echo ""

  info "Restarting services..."
  docker compose up -d 2>/dev/null
}

case "${1:-}" in
  snapshot) create_snapshot ;;
  --no-snapshot) USE_SNAPSHOT=false; main ;;
  *) main ;;
esac