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
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
|
#!/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 -w / bc bitcoin-cli -regtest "$@"; }
lcli() { docker exec -w / lq elements-cli -chain=liquidregtest "$@"; }
clcli() { docker exec -w / cl lightning-cli --network=regtest "$@"; }
clbcli() { docker exec -w / clb lightning-cli --network=regtest "$@"; }
clccli() { docker exec -w / 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 "≥2GB free disk space (${avail_gb}GB available)"
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 2b: VAPID keys ─────────────────────────────────────────────
# Run after setup_data since the snapshot may overwrite config.ts
setup_vapid() {
if grep -q 'pk: ""' config.ts 2>/dev/null; then
step "Generating VAPID keys"
local vapid_json
vapid_json=$(docker run --rm -w /home/bun/app -v "$DIR":/home/bun/app \
ghcr.io/coinos/base bun -e \
"const wp = require('web-push'); const k = wp.generateVAPIDKeys(); console.log(JSON.stringify(k))" 2>/dev/null) || true
if [ -n "$vapid_json" ]; then
local vapid_pk vapid_sk
vapid_pk=$(echo "$vapid_json" | grep -o '"publicKey":"[^"]*"' | cut -d'"' -f4)
vapid_sk=$(echo "$vapid_json" | grep -o '"privateKey":"[^"]*"' | cut -d'"' -f4)
if [ -n "$vapid_pk" ] && [ -n "$vapid_sk" ]; then
sed -i "s|pk: \"\"|pk: \"$vapid_pk\"|" config.ts
sed -i "s|sk: \"\"|sk: \"$vapid_sk\"|" config.ts
ok "Generated VAPID keys"
fi
else
warn "Could not generate VAPID keys — web push notifications won't work"
fi
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 [ ! -f "$ui_dir/.env" ] && [ -f "$ui_dir/.env.sample" ]; then
sed 's|PUBLIC_COINOS_URL=http://localhost:3119|PUBLIC_COINOS_URL=http://app:3119|' \
"$ui_dir/.env.sample" > "$ui_dir/.env"
ok "Created UI .env (with Docker service URLs)"
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 -w / 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 -w / 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 -w / 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 -w / 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 -w / 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 -w / 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 -w / 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 -w / 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 -w / 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 -w / 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_vapid
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
|