aboutsummaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
authorAdam Soltys <[email protected]>2026-03-16 23:38:47 -0700
committerAdam Soltys <[email protected]>2026-03-16 23:38:47 -0700
commitee88e5f213334382cbea04402cab9bc29405412a (patch)
treeaf340b5582cf69c19a56c22754caafae10cc203e
parent988b7c1b028f0528433acc36a54a4a3c514108e3 (diff)
downloadcoinos-server-ee88e5f213334382cbea04402cab9bc29405412a.tar.gz
coinos-server-ee88e5f213334382cbea04402cab9bc29405412a.zip
streamline dev setup and docker images
- Rewrite setup.sh: auto-installs Docker, downloads regtest snapshot, pulls images, starts all services. Idempotent, works on fresh Ubuntu/macOS. - Create coinos-base image (196MB) shared by all containers: bitcoin, elements, lightning, strfry, kvrocks, electrs, tigerbeetle, chopsticks. Total pull ~490MB, down from ~4.3GB. - Move all images to ghcr.io/coinos/* namespace. - Drop nutshell (mint/wallet), postgres, mon from default dev compose. - Comment out ark stack (optional), guard ark.ts against empty config. - Add missing sampledata: lightningc, strfry, kvrocks, valkey configs. - Strip strfry binary (145MB debug symbols removed). - Use single CLN version (v25.12.1) for all 3 lightning nodes. - Switch valkey to alpine variant (110MB → 41MB). - Update config.ts.sample with correct service URLs. - Regtest snapshot hosted on GitHub Releases (~2MB). Co-Authored-By: Claude Opus 4.6 (1M context) <[email protected]>
-rw-r--r--.gitignore1
-rw-r--r--README.md101
-rw-r--r--compose.yml.sample318
-rw-r--r--config.ts.sample34
-rw-r--r--lib/ark.ts6
-rw-r--r--sampledata/archive-kv/kvrocks.conf12
-rw-r--r--sampledata/db/valkey.conf5
-rw-r--r--sampledata/lightningc/config11
-rw-r--r--sampledata/strfry/strfry.conf144
-rwxr-xr-xsetup.sh674
-rw-r--r--wallet.Dockerfile2
11 files changed, 1026 insertions, 282 deletions
diff --git a/.gitignore b/.gitignore
index b6da50d9..6339432b 100644
--- a/.gitignore
+++ b/.gitignore
@@ -21,3 +21,4 @@ printer
failedlogins.txt
.claude
req*
+regtest-data.tar.gz
diff --git a/README.md b/README.md
index b5089c0f..82e90a37 100644
--- a/README.md
+++ b/README.md
@@ -4,72 +4,87 @@ Coinos is a web-based bitcoin and nostr client. You can use it as a front end to
This repository contains the code for the API server. The frontend code is at <a href="https://github.com/coinos/coinos-ui">https://github.com/coinos/coinos-ui</a>
-## Requirements
-
-- Make sure you have <a href="https://docs.docker.com/get-docker/">Docker</a> installed in your system..
-
-## Quick Install
-
-1. Start the Docker application.
-
-2. Run the start up script:
+## Quick Start (regtest)
```bash
-chmod +x setup.sh
+git clone https://github.com/coinos/coinos-server.git
+cd coinos-server
./setup.sh
```
-The above script will setup docker containers for the different components that Coinos needs to run.
+The setup script handles everything automatically:
-## Understanding Coinos:
+- Installs Docker if needed (Linux)
+- Downloads a pre-built regtest snapshot (~2MB) with wallets, blocks, and Lightning channels
+- Pulls container images (~500MB total, all sharing a common base)
+- Starts all services
-Coinos comprises of the folowing components:
+If the snapshot isn't available, it falls back to generating everything from scratch (wallets, blocks, channels). Use `./setup.sh --no-snapshot` to force this.
-- **The Coinos Server:** This is the main server implementation that setups and handles the API requests. Depends on the Bitcoin node, the Lightning node and the KeyDB database.
-- **[Bitcoin node](https://github.com/bitcoin/bitcoin):** This is the Bitcoin Core implementation that upholds the chains consensus rules.
-- **[Liquid Implementation](https://liquid.net/):** This is a Bitcoin layer-2 network that uses Bitcoin as its native asset and allows users to issue their own assets.
-- **[Core Lightning](https://docs.corelightning.org/docs/home):** This is also a Bitcoin layer-2 network that facilitates lightning fast Bitcoin payments via the Lightning Network protocol.
-- **[KeyDB](https://docs.keydb.dev/):** KeyDB is a high performance fork of Redis with a focus on multithreading, memory efficiency, and high throughput.
-- **[Nostr](https://nostr.com/):** Nostr is a simple, open protocol that enables global, decentralized, and censorship-resistant social media.
-- **[Cashu Nutshell](https://github.com/cashubtc/nutshell):** Nutshell is an Ecash wallet and mint for Bitcoin Lightning based on the Cashu protocol.
+After setup:
-A complete Coinos site utilizes all of the above components to support it's features. However,
-its important to note the most important ones: **Coinos Server**, **Bitcoin node**, **Core Lightning** and **KeyDB**. Without these,
-none of the Coinos features would work and the server would probably not start.
+| Service | URL |
+|---|---|
+| API server | http://localhost:3119 |
+| Esplora (block explorer) | http://localhost:3000 |
+| Nostr relay | ws://localhost:7777 |
-If you want to run thin, you can remove the other containers
-from the setup(You will get to see where to do this later).
+To start the frontend: `cd ~/coinos-ui && bun dev`
-### How Coinos Works
+## Requirements
+
+- Linux or macOS
+- [Docker](https://docs.docker.com/get-docker/) with the Compose plugin
+- ~2GB free disk space
-Running the startup script `./setup.sh` runs a few commands for you in the background. The commands are performed in this order:
+On Linux, the setup script installs Docker automatically. On macOS, install [Docker Desktop](https://docs.docker.com/desktop/install/mac-install/) first.
-1. `cp config.ts.sample config.ts`:
+## Architecture
-A `config.ts` file is created and it's contents copied from the sample file `config.ts.sample`. This file holds the configuration options
-the various Coinos components discussed above.
+**Core services** (always running):
+- **Coinos Server** — Hono/Bun API server
+- **[Bitcoin Core](https://github.com/bitcoin/bitcoin)** — Bitcoin node (regtest)
+- **[Core Lightning](https://docs.corelightning.org/)** — Lightning Network (3 nodes for testing)
+- **[Valkey](https://valkey.io/)** — Redis-compatible database
+- **[TigerBeetle](https://tigerbeetle.com/)** — Double-entry accounting ledger
+- **[Liquid](https://liquid.net/)** — Bitcoin sidechain
+- **[strfry](https://github.com/hoytech/strfry)** — Nostr relay
+- **[Esplora](https://github.com/nicolgit/nigiri)** — Block explorer API (Chopsticks + Electrs)
+- **[Kvrocks](https://kvrocks.apache.org/)** — Archive cache
-2. `cp compose.yml.sample compose.yml`
+**Optional** (commented out in `compose.yml`, uncomment to enable):
+- **[Ark](https://arkade.fun/)** — Off-chain UTXO protocol
+- **[Cashu Nutshell](https://github.com/cashubtc/nutshell)** — Ecash mint
-A `compose.yml` file is created and it's contents copied from the sample file `compose.yml.sample`. This file conatins instructions
-on the various Docker containers required to start Coinos. You will also notice duplicate component containers with different names such as cl,clb and clc. These are meant to easen the testing and development process locally as you can simulate transactions and connections.
+All `asoltys/*` images share a common `coinos-base` layer, so Docker only downloads the base once.
-3. `cp -r sampledata data`
+## Useful Commands
-A folder `data` is created and it's contents copied from the `sampledata` folder. This directory contains various folders for the different components integrated by Coinos. This is where you will find specific configurations and settings for the different components such as Bitcoin and Lightning.
+```bash
+# Mine a block
+docker exec bc bitcoin-cli -regtest generatetoaddress 1 \
+ $(docker exec bc bitcoin-cli -regtest getnewaddress)
-4. `docker compose up -d` and `docker run -it -v $(pwd):/home/bun/app --entrypoint bun asoltys/coinos-server i`
+# Check Lightning node
+docker exec cl lightning-cli --network=regtest getinfo
-Finally, the various Docker containers configured above are started and the Coinos server application is run. At this point, all containers should be running, otherwise it could indicate a problem with the previous tasks execution. You can check your container's status by running this command: `docker ps -a`.
+# View logs
+docker compose logs -f app
-5. `docker exec -it bc bitcoin-cli createwallet coinos` and `docker exec -it bc bitcoin-cli rescanblockchain`
+# Restart a service
+docker compose restart app
-Bitcoin needs a wallet and to be synced to the blockchain to work. The startup script creates a Bitcoin wallet and rescans the blockchain to update it's state. By default, the chain is set to `regtest` so we also generate a few blocks and an address to get going.
+# Stop everything
+docker compose down
-6. `docker exec -it lq elements-cli createwallet coinos`
+# Regenerate the regtest snapshot (maintainers)
+./setup.sh snapshot
+```
-Liquid requires a wallet also, thus we generate one for it.
+## Configuration
-Basically, the startup script automates tasks you would have done manually and easen the process. You can also add your own custom commands that you would like executed when starting up.
+- `config.ts` — App config (generated from `config.ts.sample`)
+- `compose.yml` — Docker Compose services (generated from `compose.yml.sample`)
+- `data/` — Runtime data for all services
-### Tips
+To reset: `rm -rf data config.ts compose.yml` and re-run `./setup.sh`.
diff --git a/compose.yml.sample b/compose.yml.sample
index e871ad02..d9621d4a 100644
--- a/compose.yml.sample
+++ b/compose.yml.sample
@@ -1,44 +1,6 @@
services:
- arkd:
- image: asoltys/arkd
- container_name: arkd
- restart: unless-stopped
- depends_on:
- - arkd-wallet
- ports:
- - "7070:7070"
- environment:
- - ARKD_LOG_LEVEL=6
- - ARKD_NO_MACAROONS=true
- - ARKD_VTXO_TREE_EXPIRY=100
- - ARKD_SCHEDULER_TYPE=block
- - ARKD_UNILATERAL_EXIT_DELAY=512
- - ARKD_BOARDING_EXIT_DELAY=1024
- - ARKD_DATADIR=./data/regtest
- - ARKD_WALLET_ADDR=arkd-wallet:6060
- - ARKD_ESPLORA_URL=http://cs:3000
- - ARKD_VTXO_MIN_AMOUNT=1
- - ARKD_LIVE_STORE_TYPE=inmemory
- - ARKD_EVENT_DB_TYPE=badger
- - ARKD_DB_TYPE=sqlite
- - ARKD_ROUND_INTERVAL=10
- - ARKD_ALLOW_CSV_BLOCK_TYPE=true
- - ARKD_UNLOCKER_TYPE=env
- - ARKD_UNLOCKER_PASSWORD=testpassword
- volumes:
- - ./data/ark:/app/data
- tb:
- image: ghcr.io/tigerbeetle/tigerbeetle:latest
- container_name: tb
- privileged: true
- command: ["start", "--addresses=0.0.0.0:3000", "/data/0_0.tigerbeetle"]
- volumes:
- - ./data/tigerbeetle:/data
- ports:
- - "3001:3000"
- restart: always
app:
- image: asoltys/coinos-server
+ image: ghcr.io/coinos/base
container_name: app
depends_on:
- db
@@ -54,21 +16,19 @@ services:
ports:
- '3119:3119'
volumes:
- - .aws/:/root/.aws
- ./:/home/bun/app
- ./logs:/logs
- ./data/bitcoin/bitcoin.conf:/bitcoin.conf
- ./data/sockets:/sockets
- - ./mklittlefs:/usr/bin/mklittlefs
restart: always
bc:
- image: asoltys/bitcoin:28.0
+ image: ghcr.io/coinos/bitcoin:30.2
container_name: bc
volumes:
- ./data/bitcoin:/home/bitcoin/.bitcoin
restart: always
lq:
- image: asoltys/elements:23.2.4
+ image: ghcr.io/coinos/elements:23.2.4
container_name: lq
volumes:
- ./data/liquid:/home/elements/.elements
@@ -76,7 +36,7 @@ services:
ports:
- 7043:7040
cl:
- image: elementsproject/lightningd:v25.12.1
+ image: ghcr.io/coinos/lightningd:v25.12.1
container_name: cl
environment:
LIGHTNINGD_NETWORK: regtest
@@ -87,7 +47,7 @@ services:
- '1984:1984'
restart: always
clb:
- image: elementsproject/lightningd:v25.05
+ image: ghcr.io/coinos/lightningd:v25.12.1
container_name: clb
environment:
LIGHTNINGD_NETWORK: regtest
@@ -95,7 +55,7 @@ services:
- ./data/lightningb:/root/.lightning
restart: always
clc:
- image: elementsproject/lightningd:v25.05
+ image: ghcr.io/coinos/lightningd:v25.12.1
container_name: clc
environment:
LIGHTNINGD_NETWORK: regtest
@@ -103,7 +63,7 @@ services:
- ./data/lightningc:/root/.lightning
restart: always
db:
- image: valkey/valkey:8.0
+ image: valkey/valkey:8.0-alpine
container_name: db
ports:
- '6379:6379'
@@ -113,7 +73,7 @@ services:
- ./data/db/valkey.conf:/etc/valkey/valkey.conf
restart: always
arc:
- image: apache/kvrocks
+ image: ghcr.io/coinos/kvrocks
container_name: arc
ports:
- '6380:6379'
@@ -121,30 +81,28 @@ services:
volumes:
- ./data/archive-kv:/data
restart: always
+ tb:
+ image: ghcr.io/coinos/tigerbeetle
+ container_name: tb
+ privileged: true
+ command: ["start", "--addresses=0.0.0.0:3000", "/data/0_0.tigerbeetle"]
+ volumes:
+ - ./data/tigerbeetle:/data
+ ports:
+ - "3001:3000"
+ restart: always
sf:
container_name: sf
- image: asoltys/strsrv
- cap_add:
- - SYS_PTRACE
- security_opt:
- - seccomp:unconfined
+ image: ghcr.io/coinos/strsrv
volumes:
- - ./strsrv:/app/strsrv
- ./data/strfry/strfry.conf:/etc/strfry.conf
- ./data/strfry/db:/app/strfry-db
- ./data/sockets:/app/sockets
ports:
- "7777:7777"
- mon:
- container_name: mon
- image: asoltys/mon
- volumes:
- - /var/run/docker.sock:/var/run/docker.sock
- environment:
- - RUNE=S9ybLdb07sJLlsAXMM7k4wa-VBw2nZ-_6XQtIwT8maw9MA==
cs:
- image: ghcr.io/vulpemventures/nigiri-chopsticks:latest
- container_name: cs
+ image: ghcr.io/coinos/chopsticks
+ container_name: cs
command:
- --wallet-name
- coinos
@@ -157,57 +115,13 @@ services:
- --addr
- 0.0.0.0:3000
depends_on:
- - bc
+ - bc
- eb
restart: unless-stopped
ports:
- "3000:3000"
- arkd-wallet:
- restart: unless-stopped
- build:
- context: .
- dockerfile: wallet.Dockerfile
- container_name: arkd-wallet
- depends_on:
- - nbxplorer
- ports:
- - "6060:6060"
- environment:
- - ARKD_WALLET_LOG_LEVEL=5
- - ARKD_WALLET_NBXPLORER_URL=http://nbxplorer:32838
- - ARKD_WALLET_DATADIR=./data/regtest
- - ARKD_WALLET_NETWORK=regtest
- - ARKD_WALLET_SIGNER_KEY=afcd3fa10f82a05fddc9574fdb13b3991b568e89cc39a72ba4401df8abef35f0
- volumes:
- - ./data/arkd-wallet:/app/data
- nbxplorer:
- restart: unless-stopped
- container_name: nbxplorer
- ports:
- - 32838:32838
- image: nicolasdorier/nbxplorer:2.5.30
- environment:
- - NBXPLORER_NETWORK=regtest
- - NBXPLORER_CHAINS=btc
- - NBXPLORER_BTCRPCURL=http://bc:18443/wallet/coinos
- - NBXPLORER_BTCNODEENDPOINT=bc:18444
- - NBXPLORER_BTCRPCUSER=admin1
- - NBXPLORER_BTCRPCPASSWORD=123
- - NBXPLORER_VERBOSE=1
- - NBXPLORER_BIND=0.0.0.0:32838
- - NBXPLORER_TRIMEVENTS=10000
- - NBXPLORER_SIGNALFILESDIR=/datadir
- - NBXPLORER_POSTGRES=User ID=postgres;Host=pgnbxplorer;Port=5432;Application Name=nbxplorer;MaxPoolSize=20;Database=nbxplorer
- - NBXPLORER_EXPOSERPC=1
- - NBXPLORER_NOAUTH=1
- volumes:
- - type: tmpfs
- target: /datadir
- depends_on:
- pgnbxplorer:
- condition: service_healthy
eb:
- image: ghcr.io/vulpemventures/electrs:latest
+ image: ghcr.io/coinos/electrs
container_name: eb
entrypoint:
- /build/electrs
@@ -233,114 +147,94 @@ services:
volumes:
- ./data/bitcoin:/config
restart: unless-stopped
- # el:
- # image: vulpemventures/electrs-liquid:latest
- # container_name: el
- # entrypoint:
- # - /build/electrs
- # command:
- # - --jsonrpc-import
- # - -vvvv
- # - --parent-network
- # - regtest
- # - --network
- # - liquidregtest
- # - --daemon-dir
- # - /config
- # - --daemon-rpc-addr
- # - lq:7040
- # - --cookie
- # - admin1:123
- # - --http-addr
- # - 0.0.0.0:3002
- # - --electrum-rpc-addr
- # - 0.0.0.0:60401
- # - --cors
- # - "*"
+ # ── Ark (optional) ──────────────────────────────────────────────
+ # Uncomment to enable Ark vault payments.
+ # Requires: arkd, arkd-wallet, nbxplorer, pgnbxplorer
+ #
+ # arkd:
+ # image: ghcr.io/coinos/arkd
+ # container_name: arkd
+ # restart: unless-stopped
# depends_on:
- # - lq
+ # - arkd-wallet
# ports:
- # - '3003:3002'
+ # - "7070:7070"
+ # environment:
+ # - ARKD_LOG_LEVEL=6
+ # - ARKD_NO_MACAROONS=true
+ # - ARKD_VTXO_TREE_EXPIRY=100
+ # - ARKD_SCHEDULER_TYPE=block
+ # - ARKD_UNILATERAL_EXIT_DELAY=512
+ # - ARKD_BOARDING_EXIT_DELAY=1024
+ # - ARKD_DATADIR=./data/regtest
+ # - ARKD_WALLET_ADDR=arkd-wallet:6060
+ # - ARKD_ESPLORA_URL=http://cs:3000
+ # - ARKD_VTXO_MIN_AMOUNT=1
+ # - ARKD_LIVE_STORE_TYPE=inmemory
+ # - ARKD_EVENT_DB_TYPE=badger
+ # - ARKD_DB_TYPE=sqlite
+ # - ARKD_ROUND_INTERVAL=10
+ # - ARKD_ALLOW_CSV_BLOCK_TYPE=true
+ # - ARKD_UNLOCKER_TYPE=env
+ # - ARKD_UNLOCKER_PASSWORD=testpassword
+ # volumes:
+ # - ./data/ark:/app/data
+ # arkd-wallet:
# restart: unless-stopped
- mint:
- image: asoltys/nutshell:main
- container_name: mint
- ports:
- - "3338:3338"
- environment:
- - MINT_RPC_SERVER_ENABLE=TRUE
- - MINT_RPC_SERVER_ADDR=localhost
- - MINT_RPC_SERVER_PORT=8086
- - MINT_RPC_SERVER_KEY=./server_private.pem
- - MINT_RPC_SERVER_CERT=./server_cert.pem
- - MINT_RPC_SERVER_CA=./ca_cert.pem
- - MINT_CLNREST_URL=http://cl:3010
- - MINT_CLNREST_RUNE=S9ybLdb07sJLlsAXMM7k4wa-VBw2nZ-_6XQtIwT8maw9MA==
- - MINT_BACKEND_BOLT11_SAT=CLNRestWallet
- - MINT_INFO_CONTACT=[["email","[email protected]"],["twitter","@coinoswallet"],["nostr","npub1h2qfjpnxau9k7ja9qkf50043xfpfy8j5v60xsqryef64y44puwnq28w8ch"]]
- - MINT_INFO_ICON_URL="https://coinos.io/images/icon.png"
- # - MINT_BACKEND_BOLT11_SAT=FakeWallet
- - MINT_LISTEN_HOST=0.0.0.0
- - MINT_LISTEN_PORT=3338
- - MINT_PRIVATE_KEY=TEST_PRIVATE_KEY
- - MINT_CLNREST_COINOS_URL=http://app:3119
- - MINT_CLNREST_COINOS_JWT=eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpZCI6IjI4MDE4NmMwLWQzODktNDViZS1iOGI2LWM4N2EwN2VlNjA5ZiIsImlhdCI6MTc0MTgwNDQyN30.oDDQeHN2CxjAOSOa6x75La8mQYnozkuSbVrBnTTto4A
- command: ["poetry", "run", "mint"]
- volumes:
- - "/home/adam/nutshell:/app"
- - "./data/mint:/app/data/mint"
- # mintb:
- # image: asoltys/nutshell
- # container_name: mintb
+ # image: asoltys/arkd-wallet
+ # build:
+ # context: .
+ # dockerfile: wallet.Dockerfile
+ # container_name: arkd-wallet
+ # depends_on:
+ # - nbxplorer
# ports:
- # - "3339:3338"
+ # - "6060:6060"
# environment:
- # - MINT_CLNREST_URL=http://clb:3010
- # - MINT_CLNREST_RUNE=kTyXJ6yJwESVezCl4yB4z0zGMjXi11qggnKRQ4nnc2Y9MA==
- # - MINT_BACKEND_BOLT11_SAT=CLNRestWallet
- # # - MINT_BACKEND_BOLT11_SAT=FakeWallet
- # - MINT_LISTEN_HOST=0.0.0.0
- # - MINT_LISTEN_PORT=3338
- # - MINT_PRIVATE_KEY=TEST_PRIVATE_KEYB
- # - MINT_CLNREST_COINOS_URL=http://app:3119
- # - MINT_CLNREST_COINOS_JWT=eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpZCI6IjVjYTM0YWU4LTdiMTItNDU4OS04ODMyLTUyMTNjOWVhMjc5NyIsImlhdCI6MTcyNDA0NjI3OH0.bdX1YLnpgqpjmjrkCdIdDbdmZUdViNSVrAJ0OX3ByPo
- # command: ["poetry", "run", "mint"]
+ # - ARKD_WALLET_LOG_LEVEL=5
+ # - ARKD_WALLET_NBXPLORER_URL=http://nbxplorer:32838
+ # - ARKD_WALLET_DATADIR=./data/regtest
+ # - ARKD_WALLET_NETWORK=regtest
+ # - ARKD_WALLET_SIGNER_KEY=afcd3fa10f82a05fddc9574fdb13b3991b568e89cc39a72ba4401df8abef35f0
# volumes:
- # - "/home/adam/nutshell:/app"
- # - "./data/mint:/app/data/mint"
- #
- wallet:
- image: asoltys/nutshell
- container_name: wallet
- ports:
- - "4448:4448"
- depends_on:
- - mint
- environment:
- - MINT_URL=http://mint:3338
- - API_HOST=0.0.0.0
- command: ["poetry", "run", "cashu", "-d"]
- pgnbxplorer:
- restart: unless-stopped
- image: postgres:16
- container_name: pgnbxplorer
- ports:
- - 5432:5432
- environment:
- - POSTGRES_HOST_AUTH_METHOD=trust
- volumes:
- - type: tmpfs
- target: /var/lib/postgresql/data
- healthcheck:
- test:
- [
- "CMD-SHELL",
- "pg_isready -U postgres -d nbxplorer -h 127.0.0.1 -p 5432",
- ]
- interval: 2s
- timeout: 2s
- retries: 30
-networks:
- default:
+ # - ./data/arkd-wallet:/app/data
+ # nbxplorer:
+ # restart: unless-stopped
+ # container_name: nbxplorer
+ # image: nicolasdorier/nbxplorer:2.5.30
+ # ports:
+ # - 32838:32838
+ # environment:
+ # - NBXPLORER_NETWORK=regtest
+ # - NBXPLORER_CHAINS=btc
+ # - NBXPLORER_BTCRPCURL=http://bc:18443/wallet/coinos
+ # - NBXPLORER_BTCNODEENDPOINT=bc:18444
+ # - NBXPLORER_BTCRPCUSER=admin1
+ # - NBXPLORER_BTCRPCPASSWORD=123
+ # - NBXPLORER_BIND=0.0.0.0:32838
+ # - NBXPLORER_POSTGRES=User ID=postgres;Host=pgnbxplorer;Port=5432;Database=nbxplorer
+ # - NBXPLORER_NOAUTH=1
+ # volumes:
+ # - type: tmpfs
+ # target: /datadir
+ # depends_on:
+ # pgnbxplorer:
+ # condition: service_healthy
+ # pgnbxplorer:
+ # restart: unless-stopped
+ # image: postgres:16-alpine
+ # container_name: pgnbxplorer
+ # environment:
+ # - POSTGRES_HOST_AUTH_METHOD=trust
+ # volumes:
+ # - type: tmpfs
+ # target: /var/lib/postgresql/data
+ # healthcheck:
+ # test: ["CMD-SHELL", "pg_isready -U postgres -d nbxplorer -h 127.0.0.1 -p 5432"]
+ # interval: 2s
+ # timeout: 2s
+ # retries: 30
+networks:
+ default:
name: net
external: true
diff --git a/config.ts.sample b/config.ts.sample
index b28c6b6b..fc461a63 100644
--- a/config.ts.sample
+++ b/config.ts.sample
@@ -10,16 +10,11 @@ export default {
},
archive: "redis://arc",
db: "redis://db",
- nostr: "ws://nostr:8080",
+ nostr: "ws://sf:7777",
+ publicRelay: "ws://localhost:7777",
+ cache: "wss://cache2.primal.net/v1",
relays: [
- "ws://nostr:8080",
- "wss://relay.primal.net",
- "wss://relay.damus.io",
- "wss://nostr-pub.wellorder.net",
- "wss://brb.io",
- "wss://nostr.v0l.io",
- "wss://relay.nostr.bg",
- "wss://nostr.orangepill.dev"
+ "ws://sf:7777",
],
jwt: "00d0ab4f7738a83feb37f661526512063c41e49278b7c32cba87314269a5788b",
bitcoin: {
@@ -42,22 +37,33 @@ export default {
password: "123",
network: "regtest",
port: 7040,
- btc: "5ac9f65c0efcc4775e0baec4ec03abdde22473cd3cf33c0419ca290e0751b225"
+ btc: "5ac9f65c0efcc4775e0baec4ec03abdde22473cd3cf33c0419ca290e0751b225",
},
lightning: "/home/bun/app/data/lightning/regtest/lightning-rpc",
lightningb: "/home/bun/app/data/lightningb/regtest/lightning-rpc",
- fee: 0.001,
- adminpass: "pwa",
- txWebhookSecret: process.env.TX_WEBHOOK_SECRET || "pwa",
+ fee: {
+ bitcoin: 0.004,
+ liquid: 0.001,
+ lightning: 0.001,
+ },
+ admin: "admin",
+ adminpass: "pw",
+ testSecret: process.env.E2E_TEST_SECRET || "local-e2e-secret",
+ txWebhookSecret: process.env.TX_WEBHOOK_SECRET || "pw",
support: "[email protected]",
nostrKey: "nsec1msk3dqy2pzqy2murmm5s5gejfgkjkm2zesnh266gud2wcvhcunzqlu0h90",
nostrKey2: "nsec17ugnsk0qrnakf7xaxzckya4xvx58q063k6q4vq2l577k4uyv9r0qmdcgm5",
mintUrl: "http://mint:3338",
+ vapid: {
+ pk: "",
+ sk: "",
+ },
square: {
environment: SquareEnvironment.Sandbox,
- appId: "sandbox-sq0adb-9QbyDZeHTPc_RhFSEr_SaQ",
+ appId: "",
url: "https://connect.squareupsandbox.com/",
scopes: ["MERCHANT_PROFILE_READ", "ORDERS_READ", "PAYMENTS_READ"],
},
tigerbeetle: { cluster_id: 0n, replica_addresses: ["tb:3000"] },
+ hostname: "localhost",
};
diff --git a/lib/ark.ts b/lib/ark.ts
index f4f6d70d..03f02412 100644
--- a/lib/ark.ts
+++ b/lib/ark.ts
@@ -185,8 +185,10 @@ export const refreshArkWallet = async (force = false) => {
};
// Initial check after 60s startup delay; retry every 60s to catch ark rounds
-setTimeout(refreshArkWallet, 60_000);
-setInterval(() => refreshArkWallet(), 60_000);
+if (config.ark?.arkPrivateKey) {
+ setTimeout(refreshArkWallet, 60_000);
+ setInterval(() => refreshArkWallet(), 60_000);
+}
export const sendArk = async (address: string, amount: number) => {
diff --git a/sampledata/archive-kv/kvrocks.conf b/sampledata/archive-kv/kvrocks.conf
new file mode 100644
index 00000000..0a75a51e
--- /dev/null
+++ b/sampledata/archive-kv/kvrocks.conf
@@ -0,0 +1,12 @@
+bind 0.0.0.0
+port 6379
+workers 4
+dir /data
+
+# Low memory footprint for archival workload
+rocksdb.block_cache_size 256
+rocksdb.compression zstd
+rocksdb.wal_size_limit_mb 256
+
+# Log to stdout for Docker
+log-dir stdout
diff --git a/sampledata/db/valkey.conf b/sampledata/db/valkey.conf
new file mode 100644
index 00000000..712c2e63
--- /dev/null
+++ b/sampledata/db/valkey.conf
@@ -0,0 +1,5 @@
+dir /data
+loglevel warning
+save ""
+appendonly yes
+appendfsync everysec
diff --git a/sampledata/lightningc/config b/sampledata/lightningc/config
new file mode 100644
index 00000000..3ec7d15f
--- /dev/null
+++ b/sampledata/lightningc/config
@@ -0,0 +1,11 @@
+alias=coinos
+rgb=006000
+bitcoin-rpcconnect=bc
+bitcoin-rpcport=18443
+bitcoin-rpcuser=admin1
+bitcoin-rpcpassword=123
+network=regtest
+addr=clc:9735
+clnrest-port=3010
+clnrest-protocol=http
+clnrest-host=0.0.0.0
diff --git a/sampledata/strfry/strfry.conf b/sampledata/strfry/strfry.conf
new file mode 100644
index 00000000..a14d8444
--- /dev/null
+++ b/sampledata/strfry/strfry.conf
@@ -0,0 +1,144 @@
+##
+## Default strfry config
+##
+
+# Directory that contains the strfry LMDB database (restart required)
+db = "./strfry-db/"
+
+dbParams {
+ # Maximum number of threads/processes that can simultaneously have LMDB transactions open (restart required)
+ maxreaders = 256
+
+ # Size of mmap() to use when loading LMDB (default is 10TB, does *not* correspond to disk-space used) (restart required)
+ mapsize = 10995116277760
+
+ # Disables read-ahead when accessing the LMDB mapping. Reduces IO activity when DB size is larger than RAM. (restart required)
+ noReadAhead = false
+}
+
+events {
+ # Maximum size of normalised JSON, in bytes
+ maxEventSize = 1000000
+
+ # Events newer than this will be rejected
+ rejectEventsNewerThanSeconds = 900
+
+ # Events older than this will be rejected
+ rejectEventsOlderThanSeconds = 94608000
+
+ # Ephemeral events older than this will be rejected
+ rejectEphemeralEventsOlderThanSeconds = 60
+
+ # Ephemeral events will be deleted from the DB when older than this
+ ephemeralEventsLifetimeSeconds = 300
+
+ # Maximum number of tags allowed
+ maxNumTags = 2000
+
+ # Maximum size for tag values, in bytes
+ maxTagValSize = 1024
+}
+
+relay {
+ # Interface to listen on. Use 0.0.0.0 to listen on all interfaces (restart required)
+ bind = "0.0.0.0"
+
+ # Port to open for the nostr websocket protocol (restart required)
+ port = 7777
+
+ # Set OS-limit on maximum number of open files/sockets (if 0, don't attempt to set) (restart required)
+ nofiles = 0
+
+ # HTTP header that contains the client's real IP, before reverse proxying (ie x-real-ip) (MUST be all lower-case)
+ realIpHeader = ""
+
+ info {
+ # NIP-11: Name of this server. Short/descriptive (< 30 characters)
+ name = "coinos relay"
+
+ # NIP-11: Detailed information about relay, free-form
+ description = "coinos dev relay"
+
+ # NIP-11: Administrative nostr pubkey, for contact purposes
+ pubkey = ""
+
+ # NIP-11: Alternative administrative contact (email, website, etc)
+ contact = ""
+
+ # NIP-11: URL pointing to an image to be used as an icon for the relay
+ icon = ""
+
+ # List of supported lists as JSON array, or empty string to use default. Example: "[1,2]"
+ nips = ""
+ }
+
+ # Maximum accepted incoming websocket frame size (should be larger than max event) (restart required)
+ maxWebsocketPayloadSize = 131072
+
+ # Websocket-level PING message frequency (should be less than any reverse proxy idle timeouts) (restart required)
+ autoPingSeconds = 55
+
+ # If TCP keep-alive should be enabled (detect dropped connections to upstream reverse proxy)
+ enableTcpKeepalive = false
+
+ # How much uninterrupted CPU time a REQ query should get during its DB scan
+ queryTimesliceBudgetMicroseconds = 10000
+
+ # Maximum records that can be returned per filter
+ maxFilterLimit = 500
+
+ # Maximum number of subscriptions (concurrent REQs) a connection can have open at any time
+ maxSubsPerConnection = 20
+
+ writePolicy {
+ # If non-empty, path to an executable script that implements the writePolicy plugin logic
+ plugin = ""
+ }
+
+ compression {
+ # Use permessage-deflate compression if supported by client. Reduces bandwidth, but slight increase in CPU (restart required)
+ enabled = true
+
+ # Maintain a sliding window buffer for each connection. Improves compression, but uses more memory (restart required)
+ slidingWindow = true
+ }
+
+ logging {
+ # Dump all incoming messages
+ dumpInAll = false
+
+ # Dump all incoming EVENT messages
+ dumpInEvents = false
+
+ # Dump all incoming REQ/CLOSE messages
+ dumpInReqs = false
+
+ # Log performance metrics for initial REQ database scans
+ dbScanPerf = false
+
+ # Log reason for invalid event rejection? Can be disabled to silence excessive logging
+ invalidEvents = true
+ }
+
+ numThreads {
+ # Ingester threads: route incoming requests, validate events/sigs (restart required)
+ ingester = 3
+
+ # reqWorker threads: Handle initial DB scan for events (restart required)
+ reqWorker = 3
+
+ # reqMonitor threads: Handle filtering of new events (restart required)
+ reqMonitor = 3
+
+ # negentropy threads: Handle negentropy protocol messages (restart required)
+ negentropy = 2
+ }
+
+ negentropy {
+ # Support negentropy protocol messages
+ enabled = true
+
+ # Maximum records that sync will process before returning an error
+ maxSyncEvents = 1000000
+ }
+}
diff --git a/setup.sh b/setup.sh
index 8c19b8eb..c72fa016 100755
--- a/setup.sh
+++ b/setup.sh
@@ -1,10 +1,664 @@
-cp config.ts.sample config.ts
-cp compose.yml.sample compose.yml
-cp -r sampledata data
-sudo chown 100:100 data/nostr/data
-docker compose up -d
-docker run -it -v $(pwd):/home/bun/app --entrypoint bun asoltys/coinos-server i
-docker exec -it bc bitcoin-cli createwallet coinos
-docker exec -it bc bitcoin-cli rescanblockchain
-docker exec -it bc bitcoin-cli generatetoaddress 500 $(docker exec -it bc bitcoin-cli getnewaddress "" "p2sh-segwit")
-docker exec -it lq elements-cli createwallet coinos
+#!/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}"; }
+
+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
+ cp compose.yml.sample compose.yml
+ ok "Created compose.yml"
+ else
+ ok "compose.yml exists"
+ 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"
+
+ if [ -d node_modules ] && [ -d node_modules/.cache ]; then
+ ok "node_modules exists (run 'bun i' manually to update)"
+ return
+ fi
+
+ info "Running bun install (via Docker)..."
+ docker run --rm \
+ -v "$DIR":/home/bun/app \
+ -w /home/bun/app \
+ ghcr.io/coinos/base bun i
+
+ ok "Dependencies installed"
+}
+
+# ── 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
diff --git a/wallet.Dockerfile b/wallet.Dockerfile
index 8c1d48fd..944e7fc2 100644
--- a/wallet.Dockerfile
+++ b/wallet.Dockerfile
@@ -1,5 +1,5 @@
# First stage: build the ark-wallet-daemon binary
-FROM golang:1.25.6 AS builder
+FROM golang:1.26 AS builder
ARG VERSION
ARG TARGETOS