# Regtest dev fixture for RTL: bitcoind + 3 LND nodes + RTL.
#
# NOT suitable for production. All credentials are throwaway.
#
# Topology is alice -> bob -> carol, so bob forwards payments and RTL's
# routing/forwarding screens have data in them. See README.md.
#
# Node images come from Polar (https://lightningpolar.com), which publishes
# multi-arch (amd64 + arm64) builds. Nothing is built locally.

volumes:
  bitcoind_data:
  alice_data:
  bob_data:
  carol_data:
  cln_data:
  eclair_data:
  rtl_db:
  rtl_config:

x-lnd: &lnd
  image: polarlightning/lnd:0.20.0-beta
  restart: unless-stopped
  depends_on:
    - bitcoind

services:
  bitcoind:
    container_name: ${COMPOSE_PROJECT_NAME}_bitcoind
    image: polarlightning/bitcoind:30.0
    restart: unless-stopped
    command:
      - bitcoind
      - -server=1
      - -regtest=1
      # rpcauth hash for ${BITCOIN_RPC_USER}/${BITCOIN_RPC_PASSWORD}. '$$' escapes
      # compose interpolation and reaches bitcoind as a single '$'.
      - -rpcauth=rtldev:8a1f2c3d4e5b6a7c8d9e0f1a2b3c4d5e$$010df4b32c5e9a556cba1857eb5865990c983d8a56dadc0fdbf457cf90073c6c
      - -zmqpubrawblock=tcp://0.0.0.0:${BITCOIN_ZMQ_BLOCK_PORT}
      - -zmqpubrawtx=tcp://0.0.0.0:${BITCOIN_ZMQ_TX_PORT}
      # eclair's zmqblock consumes the hashblock topic, not rawblock (LND uses
      # rawblock/rawtx above); without this endpoint eclair never sees new
      # blocks and channels stay in WAIT_FOR_FUNDING_CONFIRMED forever.
      - -zmqpubhashblock=tcp://0.0.0.0:${BITCOIN_ZMQ_HASHBLOCK_PORT:-28336}
      - -txindex=1
      - -dnsseed=0
      - -rpcbind=0.0.0.0
      - -rpcallowip=0.0.0.0/0
      - -rpcport=${BITCOIN_RPC_PORT}
      - -listen=1
      - -listenonion=0
      - -fallbackfee=0.0002
    ports:
      - "${BITCOIN_RPC_PORT}:${BITCOIN_RPC_PORT}"
    volumes:
      - bitcoind_data:/home/bitcoin/.bitcoin

  # --alias / --externalip / --tlsextradomain are per-node on purpose: the alias
  # is what RTL displays, and the extradomain must match the hostname RTL dials
  # (https://alice:8080) or TLS validation fails.
  alice:
    <<: *lnd
    container_name: ${COMPOSE_PROJECT_NAME}_alice
    command:
      - lnd
      - --noseedbackup
      - --trickledelay=5000
      - --alias=alice
      - --externalip=alice
      - --tlsextradomain=alice
      - --tlsextradomain=${COMPOSE_PROJECT_NAME}_alice
      - --tlsextradomain=host.docker.internal
      - --listen=0.0.0.0:${LIGHTNING_P2P_PORT}
      - --rpclisten=0.0.0.0:${LIGHTNING_RPC_PORT}
      - --restlisten=0.0.0.0:${LIGHTNING_REST_PORT}
      - --bitcoin.active
      - --bitcoin.regtest
      - --bitcoin.node=bitcoind
      - --bitcoind.rpchost=${BITCOIN_HOST}:${BITCOIN_RPC_PORT}
      - --bitcoind.rpcuser=${BITCOIN_RPC_USER}
      - --bitcoind.rpcpass=${BITCOIN_RPC_PASSWORD}
      - --bitcoind.zmqpubrawblock=tcp://${BITCOIN_HOST}:${BITCOIN_ZMQ_BLOCK_PORT}
      - --bitcoind.zmqpubrawtx=tcp://${BITCOIN_HOST}:${BITCOIN_ZMQ_TX_PORT}
      - --accept-keysend
      - --accept-amp
    ports:
      - "${ALICE_REST_PORT}:${LIGHTNING_REST_PORT}"
    volumes:
      - alice_data:/home/lnd/.lnd

  bob:
    <<: *lnd
    container_name: ${COMPOSE_PROJECT_NAME}_bob
    command:
      - lnd
      - --noseedbackup
      - --trickledelay=5000
      - --alias=bob
      - --externalip=bob
      - --tlsextradomain=bob
      - --tlsextradomain=${COMPOSE_PROJECT_NAME}_bob
      - --tlsextradomain=host.docker.internal
      - --listen=0.0.0.0:${LIGHTNING_P2P_PORT}
      - --rpclisten=0.0.0.0:${LIGHTNING_RPC_PORT}
      - --restlisten=0.0.0.0:${LIGHTNING_REST_PORT}
      - --bitcoin.active
      - --bitcoin.regtest
      - --bitcoin.node=bitcoind
      - --bitcoind.rpchost=${BITCOIN_HOST}:${BITCOIN_RPC_PORT}
      - --bitcoind.rpcuser=${BITCOIN_RPC_USER}
      - --bitcoind.rpcpass=${BITCOIN_RPC_PASSWORD}
      - --bitcoind.zmqpubrawblock=tcp://${BITCOIN_HOST}:${BITCOIN_ZMQ_BLOCK_PORT}
      - --bitcoind.zmqpubrawtx=tcp://${BITCOIN_HOST}:${BITCOIN_ZMQ_TX_PORT}
      - --accept-keysend
      - --accept-amp
    ports:
      - "${BOB_REST_PORT}:${LIGHTNING_REST_PORT}"
    volumes:
      - bob_data:/home/lnd/.lnd

  carol:
    <<: *lnd
    container_name: ${COMPOSE_PROJECT_NAME}_carol
    command:
      - lnd
      - --noseedbackup
      - --trickledelay=5000
      - --alias=carol
      - --externalip=carol
      - --tlsextradomain=carol
      - --tlsextradomain=${COMPOSE_PROJECT_NAME}_carol
      - --tlsextradomain=host.docker.internal
      - --listen=0.0.0.0:${LIGHTNING_P2P_PORT}
      - --rpclisten=0.0.0.0:${LIGHTNING_RPC_PORT}
      - --restlisten=0.0.0.0:${LIGHTNING_REST_PORT}
      - --bitcoin.active
      - --bitcoin.regtest
      - --bitcoin.node=bitcoind
      - --bitcoind.rpchost=${BITCOIN_HOST}:${BITCOIN_RPC_PORT}
      - --bitcoind.rpcuser=${BITCOIN_RPC_USER}
      - --bitcoind.rpcpass=${BITCOIN_RPC_PASSWORD}
      - --bitcoind.zmqpubrawblock=tcp://${BITCOIN_HOST}:${BITCOIN_ZMQ_BLOCK_PORT}
      - --bitcoind.zmqpubrawtx=tcp://${BITCOIN_HOST}:${BITCOIN_ZMQ_TX_PORT}
      - --accept-keysend
      - --accept-amp
    ports:
      - "${CAROL_REST_PORT}:${LIGHTNING_REST_PORT}"
    volumes:
      - carol_data:/home/lnd/.lnd

  # Core Lightning node. Unlike the LND nodes it talks to RTL over clnrest (the
  # built-in REST plugin) using rune auth, so it needs --clnrest-* options and a
  # rune written where RTL can read it. create-rune.sh (run from the healthcheck)
  # creates the rune and writes it to /root/.lightning/rtl.rune (RTL mounts that
  # read-only); the healthcheck is unhealthy until it exists, so rtl waits for it.
  # --clnrest-host=0.0.0.0 is required so the rtl container can reach it; the default
  # 127.0.0.1 would only be reachable from inside this container. Protocol stays https
  # (clnrest default, self-signed) — RTL connects with rejectUnauthorized:false.
  cln:
    image: elementsproject/lightningd:v25.09
    container_name: ${COMPOSE_PROJECT_NAME}_cln
    restart: unless-stopped
    depends_on:
      - bitcoind
    environment:
      LIGHTNINGD_NETWORK: regtest
    command:
      - --alias=cln
      - --bitcoin-rpcconnect=${BITCOIN_HOST}
      - --bitcoin-rpcport=${BITCOIN_RPC_PORT}
      - --bitcoin-rpcuser=${BITCOIN_RPC_USER}
      - --bitcoin-rpcpassword=${BITCOIN_RPC_PASSWORD}
      - --bitcoin-retry-timeout=3600
      - --addr=0.0.0.0:9735
      - --announce-addr=cln:9735
      - --large-channels
      - --clnrest-host=0.0.0.0
      - --clnrest-port=3010
    ports:
      - "${CLN_REST_PORT:-3010}:3010"
    volumes:
      - cln_data:/root/.lightning
      - ./cln/create-rune.sh:/opt/create-rune.sh:ro
    healthcheck:
      # create-rune.sh ensures the rune exists (idempotent, one quick attempt) and
      # this reports healthy only once it does. Driving it from the healthcheck — which
      # retries on its interval — means a transient RPC-startup race self-heals instead
      # of a one-shot script permanently wedging the stack. rtl waits on this via
      # depends_on: condition: service_healthy before reading the rune at startup.
      test: ["CMD-SHELL", "sh /opt/create-rune.sh && test -f /root/.lightning/rtl.rune"]
      interval: 5s
      timeout: 10s
      retries: 40

  # Eclair has no on-chain wallet of its own -- it drives a bitcoind wallet over
  # RPC. Without a dedicated wallet it would grab "the default loaded wallet",
  # which here is the rtldev mining wallet, and eclair's on-chain balance would
  # show the miner's coins. This init container creates (or reloads) a wallet
  # named "eclair" before the eclair node starts; load_on_startup survives
  # bitcoind restarts.
  eclair-wallet-init:
    container_name: ${COMPOSE_PROJECT_NAME}_eclair_wallet_init
    image: polarlightning/bitcoind:30.0
    depends_on:
      - bitcoind
    entrypoint: ["/bin/sh", "-c"]
    command:
      - |
        bcli() { bitcoin-cli -regtest -rpcconnect=${BITCOIN_HOST} -rpcport=${BITCOIN_RPC_PORT} -rpcuser=${BITCOIN_RPC_USER} -rpcpassword=${BITCOIN_RPC_PASSWORD} "$$@"; }
        i=0
        until bcli getblockchaininfo >/dev/null 2>&1; do
          i=$$((i+1)); [ "$$i" -ge 60 ] && echo "bitcoind never came up" && exit 1
          sleep 1
        done
        bcli -named createwallet wallet_name=eclair load_on_startup=true >/dev/null 2>&1 \
          || bcli loadwallet eclair true >/dev/null 2>&1 \
          || true
        bcli -rpcwallet=eclair getwalletinfo >/dev/null
        echo "eclair wallet ready"

  # Eclair node. Talks to RTL over its HTTP API with basic auth (the
  # lnApiPassword in RTL's config). The polarlightning image is used because
  # acinq/eclair on Docker Hub is amd64-only and its newest versioned tag is
  # years stale; Polar builds the same ACINQ source multi-arch (amd64 + arm64).
  # The image entrypoint translates each --key=value into -Declair.key=value.
  # The entrypoint also overrides server.public-ips.0 with the container IP, but
  # the arg must still be present for that substitution to happen.
  eclair:
    image: polarlightning/eclair:0.13.1
    container_name: ${COMPOSE_PROJECT_NAME}_eclair
    restart: unless-stopped
    depends_on:
      bitcoind:
        condition: service_started
      eclair-wallet-init:
        condition: service_completed_successfully
    command:
      - polar-eclair
      - --node-alias=eclair
      - --server.public-ips.0=eclair
      - --server.port=9735
      - --api.enabled=true
      - --api.binding-ip=0.0.0.0
      - --api.port=8080
      - --api.password=${ECLAIR_API_PASSWORD:-rtldev}
      - --chain=regtest
      - --bitcoind.host=${BITCOIN_HOST}
      - --bitcoind.rpcport=${BITCOIN_RPC_PORT}
      - --bitcoind.rpcuser=${BITCOIN_RPC_USER}
      - --bitcoind.rpcpassword=${BITCOIN_RPC_PASSWORD}
      # zmqblock must be bitcoind's *hashblock* endpoint (eclair subscribes to
      # that topic); pointing it at the rawblock endpoint the LND nodes use
      # leaves eclair blind to new blocks.
      - --bitcoind.zmqblock=tcp://${BITCOIN_HOST}:${BITCOIN_ZMQ_HASHBLOCK_PORT:-28336}
      - --bitcoind.zmqtx=tcp://${BITCOIN_HOST}:${BITCOIN_ZMQ_TX_PORT}
      - --bitcoind.wallet=eclair
      - --datadir=/home/eclair/.eclair
      - --printToConsole=true
      # Regtest feerates are far from mainnet estimates; without a wide
      # tolerance eclair closes channels over feerate disagreements.
      - --on-chain-fees.feerate-tolerance.ratio-low=0.00001
      - --on-chain-fees.feerate-tolerance.ratio-high=10000.0
    ports:
      - "${ECLAIR_REST_PORT:-8281}:8080"
    volumes:
      - eclair_data:/home/eclair

  # RTL rewrites its config file on startup, so it cannot be given the tracked
  # template directly: a bind mount would either be read-only (RTL exits with
  # EROFS) or would let RTL scribble into a version-controlled file. Instead the
  # template is copied into a volume that 'down -v' discards, which keeps the
  # source pristine and every run starting from identical config.
  rtl-config-init:
    container_name: ${COMPOSE_PROJECT_NAME}_rtl_config_init
    image: busybox:1.36
    command: >
      sh -c "cp /template/RTL-Config.regtest.json /config/RTL-Config.json &&
             chmod 644 /config/RTL-Config.json &&
             echo 'config staged'"
    volumes:
      - ./rtl/RTL-Config.regtest.json:/template/RTL-Config.regtest.json:ro
      - rtl_config:/config

  rtl:
    container_name: ${COMPOSE_PROJECT_NAME}_rtl
    # Defaults to the published image; override with RTL_IMAGE (e.g. a locally built
    # branch image) to test unreleased changes: RTL_IMAGE=rtl:pr1625 docker compose up.
    image: ${RTL_IMAGE:-shahanafarooqui/rtl:v0.15.8}
    restart: unless-stopped
    depends_on:
      rtl-config-init:
        condition: service_completed_successfully
      alice:
        condition: service_started
      bob:
        condition: service_started
      carol:
        condition: service_started
      cln:
        condition: service_healthy
      eclair:
        condition: service_started
    ports:
      - "${RTL_PORT}:${RTL_PORT}"
    environment:
      RTL_CONFIG_PATH: /RTL/config
    volumes:
      - rtl_config:/RTL/config
      - alice_data:/lnd/alice:ro
      - bob_data:/lnd/bob:ro
      - carol_data:/lnd/carol:ro
      - cln_data:/cln:ro
      - rtl_db:/RTL/database
