Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Empty file.
Empty file.
Empty file.
65 changes: 65 additions & 0 deletions stable-channels-lsp/docker-compose.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,65 @@
# Stable Channels LSP — Umbrel app compose.
#
# ldk-server — LDK Lightning node + LSPS2 service (gRPC backend)
# sc-lsp — Stable Channels front daemon (REST, stability engine)
# gui — web dashboard (wasm) + nginx /api/ reverse proxy
#
# Configs are rendered by hooks/pre-start from the Bitcoin app's exports into
# ${APP_DATA_DIR}/data/config. Umbrel injects container names as
# <app-id>_<service>_1; those names are baked into the rendered configs
# (gRPC target + TLS SAN) and SC_LSP_UPSTREAM below.

x-log-rotation: &log-rotation
driver: json-file
options:
max-size: "${SC_DOCKER_LOG_MAX_SIZE:-10m}"
max-file: "${SC_DOCKER_LOG_MAX_FILE:-3}"

services:
app_proxy:
environment:
APP_HOST: stable-channels-lsp_gui_1
APP_PORT: 80

ldk-server:
image: ghcr.io/toneloc/sc-ldk-server:5f631bd@sha256:cd3e9cdea982fcd81e88f024bb4500da44001f542a5be22b962ec3459c980859
init: true
logging: *log-rotation
restart: on-failure
stop_grace_period: 2m
user: "1000:1000"
volumes:
- ${APP_DATA_DIR}/data/config:/etc/ldk-server:ro
- ${APP_DATA_DIR}/data/ldk-server:/data/ldk-server
ports:
# Lightning P2P must be host-published so remote peers (the wallet
# users' nodes) can connect inbound for JIT opens.
# Umbrel's LND app already reserves host port 9735.
- "19735:9735"

sc-lsp:
image: ghcr.io/toneloc/sc-lsp:5f631bd@sha256:94455ad53e4551a3fab018ea1950e06f650be15b46df56da21f9fd835da20c57
init: true
logging: *log-rotation
restart: on-failure
stop_grace_period: 1m
user: "1000:1000"
depends_on: [ldk-server]
volumes:
- ${APP_DATA_DIR}/data/config:/etc/sc-lsp:ro
- ${APP_DATA_DIR}/data/sc-lsp:/data/stable-channels-lsp
# Read ldk-server's auto-generated tls.crt + api_key for its gRPC.
- ${APP_DATA_DIR}/data/ldk-server:/data/ldk-server:ro

gui:
image: ghcr.io/toneloc/sc-lsp-gui:5f631bd@sha256:2883019ae65f7362cd1d19ef37a99f6848044fc5fb76ff808a801b8251aee7d4
init: true
logging: *log-rotation
restart: on-failure
stop_grace_period: 30s
depends_on: [sc-lsp]
environment:
SC_LSP_UPSTREAM: https://stable-channels-lsp_sc-lsp_1:3002
volumes:
# Read sc-lsp's generated api_key so /setup can display it.
- ${APP_DATA_DIR}/data/sc-lsp:/run/sc-lsp:ro
152 changes: 152 additions & 0 deletions stable-channels-lsp/hooks/pre-start
Original file line number Diff line number Diff line change
@@ -0,0 +1,152 @@
#!/usr/bin/env bash
# Bootstraps ldk-server.toml + sc-lsp.toml in ${APP_DATA_DIR}/data/config from
# the Bitcoin app's exports (sourced by Umbrel before this hook runs).
# Existing files are operator-owned and intentionally preserved on restart.
set -euo pipefail
umask 077

SCRIPT_DIR="$(CDPATH= cd -- "$(dirname -- "$0")" && pwd)"
APP_DIR="$(dirname "$SCRIPT_DIR")"
APP_DATA_DIR="${APP_DATA_DIR:-$APP_DIR}"
CONF_DIR="${APP_DATA_DIR}/data/config"

BITCOIN_IP="${APP_BITCOIN_NODE_IP:?APP_BITCOIN_NODE_IP not set (is the Bitcoin app installed?)}"
BITCOIN_RPC_PORT="${APP_BITCOIN_RPC_PORT:-8332}"
BITCOIN_RPC_USER="${APP_BITCOIN_RPC_USER:?APP_BITCOIN_RPC_USER not set}"
BITCOIN_RPC_PASS="${APP_BITCOIN_RPC_PASS:?APP_BITCOIN_RPC_PASS not set}"

# Umbrel reports "mainnet"; LDK/rust-bitcoin expect "bitcoin".
case "${APP_BITCOIN_NETWORK:-mainnet}" in
mainnet|bitcoin) NETWORK="bitcoin" ;;
testnet) NETWORK="testnet" ;;
testnet4) NETWORK="testnet4" ;;
signet) NETWORK="signet" ;;
regtest) NETWORK="regtest" ;;
*) echo "unsupported APP_BITCOIN_NETWORK: ${APP_BITCOIN_NETWORK}" >&2; exit 1 ;;
esac

# Umbrel-injected container names (<app-id>_<service>_1) — used as the gRPC
# hostname, so the TLS cert SAN must include the ldk-server one (same class of
# issue as the e2e stack's "[tls] hosts" gotcha).
LDK_SERVER_HOST="stable-channels-lsp_ldk-server_1"

mkdir -p "${CONF_DIR}" \
"${APP_DATA_DIR}/data/ldk-server" \
"${APP_DATA_DIR}/data/sc-lsp"

LDK_CONFIG_TMP="${CONF_DIR}/.ldk-server.toml.$$"
SC_CONFIG_TMP="${CONF_DIR}/.sc-lsp.toml.$$"
cleanup() {
rm -f -- "$LDK_CONFIG_TMP" "$SC_CONFIG_TMP"
}
trap cleanup EXIT HUP INT TERM

if [ ! -e "${CONF_DIR}/ldk-server.toml" ]; then
cat > "$LDK_CONFIG_TMP" <<EOF
# Created by hooks/pre-start on first launch. Operator edits are preserved.
[node]
network = "${NETWORK}"
alias = "Stable Channels"
listening_addresses = ["0.0.0.0:9735"]
grpc_service_address = "0.0.0.0:3536"

[storage.disk]
dir_path = "/data/ldk-server"

[bitcoind]
rpc_address = "${BITCOIN_IP}:${BITCOIN_RPC_PORT}"
rpc_user = "${BITCOIN_RPC_USER}"
rpc_password = "${BITCOIN_RPC_PASS}"

# LSPS2 service — values mirror the production bidaemon deployment.
[liquidity.lsps2_service]
advertise_service = true
channel_opening_fee_ppm = 0
channel_over_provisioning_ppm = 1000000
min_channel_opening_fee_msat = 0
min_channel_lifetime = 100
max_client_to_self_delay = 1024
min_payment_size_msat = 0
max_payment_size_msat = 100000000000
client_trusts_lsp = true
disable_client_reserve = false

[log]
level = "Info"
file = "/data/ldk-server/${NETWORK}/ldk-server.log"
log_to_file = true

[tls]
hosts = ["${LDK_SERVER_HOST}"]
EOF
chmod 600 "$LDK_CONFIG_TMP"
mv -f -- "$LDK_CONFIG_TMP" "${CONF_DIR}/ldk-server.toml"
fi

if [ ! -e "${CONF_DIR}/sc-lsp.toml" ]; then
cat > "$SC_CONFIG_TMP" <<EOF
# Created by hooks/pre-start on first launch. Operator edits are preserved.
[node]
rest_service_address = "0.0.0.0:3002"
network = "${NETWORK}"

[storage.disk]
dir_path = "/data/stable-channels-lsp"

[ldk_server]
config_path = "/etc/sc-lsp/ldk-server.toml"
grpc_address = "${LDK_SERVER_HOST}:3536"
cert_path = "/data/ldk-server/tls.crt"
api_key_path = "/data/ldk-server/${NETWORK}/api_key"

# Optional mobile push notifications. Copy the credential files into this
# app's data/config directory, then uncomment the relevant settings below.
[push]
# APNs (iOS)
# apns_key_path = "/etc/sc-lsp/AuthKey.p8"
# apns_key_id = "<Apple key ID>"
# apns_team_id = "<Apple team ID>"
# apns_topic = "com.stablechannels.app"
# apns_environment = "production"

# FCM (Android)
# fcm_service_account_path = "/etc/sc-lsp/firebase-service-account.json"
EOF
chmod 600 "$SC_CONFIG_TMP"
mv -f -- "$SC_CONFIG_TMP" "${CONF_DIR}/sc-lsp.toml"
fi

chmod 600 "${CONF_DIR}/ldk-server.toml" "${CONF_DIR}/sc-lsp.toml"

# The GUI setup helper uses this marker to locate sc-lsp's network-scoped API
# key. Derive it from the persistent config, not the current Bitcoin export,
# so an operator edit and the marker cannot silently diverge.
SC_EFFECTIVE_NETWORK="$({
awk -F= '
/^[[:space:]]*\[node\][[:space:]]*$/ { in_node = 1; next }
/^[[:space:]]*\[/ { in_node = 0 }
in_node && $1 ~ /^[[:space:]]*network[[:space:]]*$/ {
value = $2
sub(/^[[:space:]]*"/, "", value)
sub(/".*/, "", value)
print value
exit
}
' "${CONF_DIR}/sc-lsp.toml"
} || true)"
case "$SC_EFFECTIVE_NETWORK" in
bitcoin|testnet|testnet4|signet|regtest) ;;
*)
echo "unsupported or missing node.network in ${CONF_DIR}/sc-lsp.toml" >&2
exit 1
;;
esac
printf '%s\n' "$SC_EFFECTIVE_NETWORK" > "${APP_DATA_DIR}/data/sc-lsp/network"

trap - EXIT HUP INT TERM

# Umbrel runs hooks as root; local test containers may not, and Docker Desktop
# handles bind-mount ownership itself.
if [ "$(id -u)" = "0" ]; then
chown -R 1000:1000 "${APP_DATA_DIR}/data"
fi
36 changes: 36 additions & 0 deletions stable-channels-lsp/umbrel-app.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
manifestVersion: 1.1
id: stable-channels-lsp
category: bitcoin
name: Stable Channels LSP
version: "0.9.4"
tagline: Run a Lightning LSP with USD-stabilized channels
description: >-
Stable Channels LSP is a Lightning Service Provider daemon that offers
just-in-time (JIT) channel opens and USD-stabilized channel balances to
Stable Channels wallet users. It runs an LDK-based Lightning node against
your Umbrel's Bitcoin node, an LSPS2 liquidity service, and a web dashboard
for balances, channels, payments, and the audit log.


The app includes an operator dashboard and automatically configures its
private API connection on first launch.


Your node's Lightning identity and channel funds live in this app's data
directory. Keep Umbrel backups current and never restore the same node on
two machines at once.
releaseNotes: ""
developer: Stable Channels
website: https://stablechannels.com
dependencies: [bitcoin]
repo: https://github.com/toneloc/stable-channels
support: https://github.com/toneloc/stable-channels/issues
port: 21013
gallery: []
path: ""
defaultUsername: ""
defaultPassword: ""
backupIgnore:
- data/ldk-server/*/ldk-server.log*
submitter: toneloc
submission: https://github.com/getumbrel/umbrel-apps/pull/5948