From 499fe06165e5981fac534af479caaf1fd303b42f Mon Sep 17 00:00:00 2001 From: criptoworld Date: Thu, 25 Jun 2026 21:49:36 +0200 Subject: [PATCH 01/19] docs(start9): design spec for StartOS adaptation Mirror the sparrow-frigate-startos TS SDK pattern under start9/, wrapping the same Rust binary. Maps StartOS config (bitcoind cookie, electrs.startos host, mainnet) to the binary's generic BROADCAST_POOL_* env vars. Isolated on the start9-adaptation branch; does not touch Umbrel packaging. Co-Authored-By: Claude Opus 4.8 --- .../2026-06-25-start9-adaptation-design.md | 107 ++++++++++++++++++ 1 file changed, 107 insertions(+) create mode 100644 docs/superpowers/specs/2026-06-25-start9-adaptation-design.md diff --git a/docs/superpowers/specs/2026-06-25-start9-adaptation-design.md b/docs/superpowers/specs/2026-06-25-start9-adaptation-design.md new file mode 100644 index 0000000..a627b34 --- /dev/null +++ b/docs/superpowers/specs/2026-06-25-start9-adaptation-design.md @@ -0,0 +1,107 @@ +# Diseño: Adaptación de Broadcast Pool a Start9 (StartOS) + +Fecha: 2026-06-25 · Rama: `start9-adaptation` + +## Contexto + +Broadcast Pool ya funciona empaquetado para Umbrel (`umbrel-app/sparrow-broadcast-pool/`). +El binario Rust `broadcast-pool` es un intermediario Electrum entre wallets (Sparrow/Liana) +y un indexador (electrs/Fulcrum), con mempool virtual para programar/retrasar broadcasts. + +Queremos ofrecerlo también en **Start9 (StartOS)** sin afectar al proyecto Umbrel. El binario +es **agnóstico de distro**: su superficie de integración es su contrato de variables de entorno +(`BROADCAST_POOL_*`). Se verificó que funciona en modo **genérico** (sin `BROADCAST_POOL_UMBREL=1`): +`BROADCAST_POOL_INDEXER_URL` se lee directo y la resolución de genesis usa solo `BROADCAST_POOL_RPC_*`. + +**Objetivo:** un paquete `.s9pk` que envuelve el mismo binario, declarando dependencias de +`bitcoind` + `electrs` y exponiendo el proxy Electrum (Sparrow/Liana) y el dashboard web. + +**Decisiones tomadas (brainstorming):** +- Mismo repo, **rama aislada** `start9-adaptation`, todo bajo `start9/` (no toca archivos Umbrel). +- Nodo de pruebas Start9 disponible con acceso SSH/web, indexador **electrs**, red **mainnet**. +- Verificación = **pruebas de humo seguras** (conexión, genesis, handshake, dashboard, que + Sparrow/Liana conecten). **NO** se difundirá una tx de valor real. + +## Arquitectura + +Patrón espejo de `criptoworld8484/sparrow-frigate-startos` (StartOS TypeScript SDK), que ya +depende de bitcoind+electrs y expone una interfaz Electrum — forma casi idéntica a la nuestra. + +``` +Sparrow / Liana ──TCP LAN:50050──▶ broadcast-pool (.s9pk) + │ (mismo binario Rust; entrypoint StartOS) + ├──▶ electrs.startos:50001 (upstream índice) + └──▶ bitcoind.startos:8332 (genesis/red/altura, cookie auth) +Navegador ──LAN/Tor HTTPS──▶ dashboard web :8080 +``` + +El binario Rust **no se modifica** (salvo un posible ajuste menor: que una `INDEXER_URL` +explícita no sea pisada por el LAN-scan en modo no-Umbrel; se valida en implementación). + +## Componentes (todo bajo `start9/`) + +Estructura TS SDK (como frigate): +- `start9/startos/manifest/index.ts` — id `broadcast-pool`, imágenes (dockerBuild), deps bitcoind+electrs. +- `start9/startos/dependencies.ts` — bitcoind (running, prune=0) + electrs (running). +- `start9/startos/interfaces.ts` — **dos** interfaces: + - `electrum` (TCP, sin SSL) en 50050 — Sparrow/Liana. + - `ui` (web) en 8080 — dashboard. +- `start9/startos/main.ts` — subcontenedor, monta volumen `main`→`/data` y bitcoind RO→`/mnt/bitcoind`, + daemon `entrypoint.sh`, health-check `checkPortListening(50050)`. +- `start9/startos/utils.ts` — constantes de puerto (`electrumPort=50050`, `webPort=8080`). +- `start9/startos/{index,versions/current,init,backups,actions,sdk,i18n}.ts` — plumbing estándar. +- `start9/Dockerfile` — build multi-stage del binario Rust (context = raíz del repo), arch x86_64+aarch64. +- `start9/entrypoint.sh` — traduce StartOS → env vars del binario (ver mapeo). +- `start9/{Makefile,s9pk.mk,package.json,tsconfig.json}` + `.github/workflows/build-s9pk.yml`. + +## Mapeo de configuración (entrypoint.sh StartOS → binario) + +| Binario (env) | Valor en StartOS | +|-----------------------------------|----------------------------------------------------| +| `BROADCAST_POOL_NETWORK` | `mainnet` (red del nodo Start9) | +| `BROADCAST_POOL_DATA_DIR` | `/data` | +| `BROADCAST_POOL_RPC_URL` | `http://bitcoind.startos:8332` | +| `BROADCAST_POOL_RPC_USER`/`PASS` | leídos del cookie `/mnt/bitcoind/.cookie` (`__cookie__:`) | +| `BROADCAST_POOL_INDEXER_URL` | `tcp://electrs.startos:50001` | +| `BROADCAST_POOL_ELECTRUM_HOST` | `0.0.0.0` | +| `BROADCAST_POOL_ELECTRUM_PORT` | `50050` | +| `BROADCAST_POOL_WEB_HOST`/`PORT` | `0.0.0.0` / `8080` | +| `BROADCAST_POOL_UMBREL` | **no se define** (modo genérico) | + +El cookie de bitcoind se resuelve igual que frigate (`/mnt/bitcoind/.cookie`, con fallback a +`find`), se parte por `:` en usuario/contraseña, y se espera a que exista antes de arrancar. + +## Manejo de errores +- Esperar el cookie de bitcoind (bucle con timeout) antes de lanzar el binario. +- Health-check de StartOS sobre el puerto 50050: el servicio aparece "starting" hasta que escucha. +- Si electrs aún sincroniza, el binario ya tolera respuestas lentas/erróneas del indexador + (timeouts + fallback de la mempool virtual); StartOS marca la dependencia electrs por su propio health. + +## Pruebas / Verificación (mainnet, sin mover fondos) +1. Build local del `.s9pk` (Makefile) y/o vía workflow `build-s9pk.yml`. +2. Sideload/instalar en el nodo Start9; comprobar que el servicio arranca y la interfaz Electrum + queda **verde** (health). +3. Smoke test del proxy en la dirección LAN:50050: + - `server.version` responde. + - `blockchain.block.header(0)` → hash = genesis mainnet + `000000000019d6689c085ae165831e934ff763ae46a2a6c172b3f1b60a8ce26f`. + - `server.features.genesis_hash` coincide con el del nodo. +4. Apuntar **Sparrow** (y opcionalmente **Liana en mainnet**) a la URL Electrum LAN → conecta y + carga el monedero (solo lectura). **No** difundir una tx real. +5. Dashboard web accesible por la interfaz `ui`. + +Tests automatizados del binario Rust: sin cambios (la suite existente cubre la lógica; el +empaquetado no añade lógica Rust nueva salvo el posible ajuste de INDEXER_URL, que llevaría su test). + +## Alcance / YAGNI +- v1 expone **un** puerto Electrum (50050) para Sparrow y Liana; el `liana_port` separado + (PoC de altura/anti-fee-sniping) **no** se expone en v1. +- Solo red **mainnet** (es la del nodo Start9); el binario soporta otras redes si se cambia el env. +- Sin acciones/actions extra más allá de las de plumbing en v1. + +## Riesgos +- Posible necesidad de un pequeño ajuste en `discovery.rs`/`main.rs` para que una `INDEXER_URL` + explícita no sea sobrescrita por el LAN-scan en modo no-Umbrel. Se valida al implementar; si + hace falta, es un cambio mínimo y testeado, compartido con Umbrel (no lo rompe). +- Versionado/SDK de StartOS: usar la misma versión del `@start9labs/start-sdk` que frigate para + evitar incompatibilidades. From e6c6d359899c93e0a698181afa22b9fe62d890d3 Mon Sep 17 00:00:00 2001 From: criptoworld Date: Fri, 26 Jun 2026 16:42:37 +0200 Subject: [PATCH 02/19] feat(start9): StartOS (.s9pk) package for Broadcast Pool Wraps the same Rust binary in a StartOS package under start9/, mirroring the proven sparrow-frigate-startos TS SDK structure. Depends on bitcoind + electrs; exposes an Electrum (TCP) interface on 50050 for Sparrow/Liana and a web UI on 8080. entrypoint.sh maps the StartOS env (bitcoind cookie, electrs.startos host) to the binary's generic BROADCAST_POOL_* contract. Network (mainnet/testnet4/signet) is auto-detected from Bitcoin Core. To make pure auto-detection correct, src/main.rs now runs RPC creation + network detection BEFORE deriving the network-specific data dir and opening the DB (previously the DB could open on the wrong network when BROADCAST_POOL_NETWORK was unset). No-op for Umbrel, where the network is provided. Verified locally: cargo tests (27) pass; the StartOS TypeScript type-checks against @start9labs/start-sdk 1.5.3 and bundles via ncc; entrypoint.sh passes sh -n. Full .s9pk build + live mainnet smoke test run via CI + the Start9 node. Co-Authored-By: Claude Opus 4.8 --- .github/workflows/build-startos-s9pk.yml | 105 +++++ .../2026-06-25-start9-adaptation-design.md | 7 +- src/main.rs | 20 +- start9/.gitignore | 3 + start9/Dockerfile | 34 ++ start9/Makefile | 2 + start9/entrypoint.sh | 80 ++++ start9/icon.png | Bin 0 -> 14687 bytes start9/package-lock.json | 426 ++++++++++++++++++ start9/package.json | 24 + start9/s9pk.mk | 138 ++++++ start9/startos/actions/index.ts | 3 + start9/startos/backups.ts | 9 + start9/startos/dependencies.ts | 34 ++ start9/startos/fileModels/store.json.ts | 16 + start9/startos/i18n/dictionaries/default.ts | 18 + .../startos/i18n/dictionaries/translations.ts | 18 + start9/startos/i18n/index.ts | 5 + start9/startos/index.ts | 11 + start9/startos/init/index.ts | 18 + start9/startos/init/seedFiles.ts | 6 + start9/startos/interfaces.ts | 52 +++ start9/startos/main.ts | 57 +++ start9/startos/manifest/i18n.ts | 36 ++ start9/startos/manifest/index.ts | 61 +++ start9/startos/sdk.ts | 7 + start9/startos/utils.ts | 4 + start9/startos/versions/current.ts | 13 + start9/startos/versions/index.ts | 7 + start9/tsconfig.json | 11 + 30 files changed, 1216 insertions(+), 9 deletions(-) create mode 100644 .github/workflows/build-startos-s9pk.yml create mode 100644 start9/.gitignore create mode 100644 start9/Dockerfile create mode 100644 start9/Makefile create mode 100644 start9/entrypoint.sh create mode 100644 start9/icon.png create mode 100644 start9/package-lock.json create mode 100644 start9/package.json create mode 100644 start9/s9pk.mk create mode 100644 start9/startos/actions/index.ts create mode 100644 start9/startos/backups.ts create mode 100644 start9/startos/dependencies.ts create mode 100644 start9/startos/fileModels/store.json.ts create mode 100644 start9/startos/i18n/dictionaries/default.ts create mode 100644 start9/startos/i18n/dictionaries/translations.ts create mode 100644 start9/startos/i18n/index.ts create mode 100644 start9/startos/index.ts create mode 100644 start9/startos/init/index.ts create mode 100644 start9/startos/init/seedFiles.ts create mode 100644 start9/startos/interfaces.ts create mode 100644 start9/startos/main.ts create mode 100644 start9/startos/manifest/i18n.ts create mode 100644 start9/startos/manifest/index.ts create mode 100644 start9/startos/sdk.ts create mode 100644 start9/startos/utils.ts create mode 100644 start9/startos/versions/current.ts create mode 100644 start9/startos/versions/index.ts create mode 100644 start9/tsconfig.json diff --git a/.github/workflows/build-startos-s9pk.yml b/.github/workflows/build-startos-s9pk.yml new file mode 100644 index 0000000..4bafde0 --- /dev/null +++ b/.github/workflows/build-startos-s9pk.yml @@ -0,0 +1,105 @@ +name: Build StartOS s9pk + +# Isolated from the Umbrel image workflow (which triggers on v* tags). This one +# builds the StartOS .s9pk from the start9/ package directory. +on: + workflow_dispatch: + push: + branches: [start9-adaptation] + tags: + - 'startos-v*' + +permissions: + contents: write + +defaults: + run: + working-directory: start9 + +jobs: + build: + runs-on: ubuntu-latest + strategy: + matrix: + arch: [x86_64, aarch64] + steps: + - uses: actions/checkout@v4 + + - uses: actions/setup-node@v4 + with: + node-version: '22' + + - name: Install system dependencies + run: | + sudo rm -f /etc/apt/sources.list.d/microsoft-prod.list + sudo apt-get update + sudo apt-get install -y --no-install-recommends \ + build-essential \ + squashfs-tools-ng \ + qemu-user-static \ + jq + + - uses: docker/setup-qemu-action@v3 + - uses: docker/setup-docker-action@v5 + - uses: docker/setup-buildx-action@v4 + + - name: Install start-cli + run: | + ARCH=$(uname -m) + OS=$(uname -s | tr '[:upper:]' '[:lower:]') + ASSET_NAME="start-cli_${ARCH}-${OS}" + DOWNLOAD_URL=$(curl -fsS \ + -H "Authorization: token ${{ github.token }}" \ + https://api.github.com/repos/Start9Labs/start-os/releases \ + | jq -r '[.[].assets[] | select(.name=="'"$ASSET_NAME"'")] | first | .browser_download_url') + if [ -z "$DOWNLOAD_URL" ] || [ "$DOWNLOAD_URL" = "null" ]; then + echo "Error: Could not find asset '$ASSET_NAME' in Start9Labs/start-os releases" + exit 1 + fi + curl -fsSL \ + -H "Authorization: token ${{ github.token }}" \ + -H "Accept: application/octet-stream" \ + "$DOWNLOAD_URL" -o /tmp/start-cli + sudo install -m 755 /tmp/start-cli /usr/local/bin/start-cli + start-cli --version + + - name: Build s9pk (${{ matrix.arch }}) + env: + DEV_KEY: ${{ secrets.DEV_KEY }} + run: | + test -f icon.png + test $(stat -c%s icon.png) -le 40960 + npm install + npm run check + if [ -n "$DEV_KEY" ]; then + mkdir -p "$HOME/.startos" + printf '%s' "$DEV_KEY" > "$HOME/.startos/developer.key.pem" + else + start-cli init-key + fi + make arch/${{ matrix.arch }} + sha256sum broadcast-pool_${{ matrix.arch }}.s9pk + + - name: Upload artifact + uses: actions/upload-artifact@v4 + with: + name: broadcast-pool_${{ matrix.arch }}.s9pk + path: start9/broadcast-pool_${{ matrix.arch }}.s9pk + compression-level: 0 + + release: + if: startsWith(github.ref, 'refs/tags/startos-v') + needs: build + runs-on: ubuntu-latest + steps: + - uses: actions/download-artifact@v4 + with: + pattern: broadcast-pool_* + merge-multiple: true + + - uses: softprops/action-gh-release@v2 + with: + files: | + broadcast-pool_x86_64.s9pk + broadcast-pool_aarch64.s9pk + generate_release_notes: true diff --git a/docs/superpowers/specs/2026-06-25-start9-adaptation-design.md b/docs/superpowers/specs/2026-06-25-start9-adaptation-design.md index a627b34..d4150b7 100644 --- a/docs/superpowers/specs/2026-06-25-start9-adaptation-design.md +++ b/docs/superpowers/specs/2026-06-25-start9-adaptation-design.md @@ -19,6 +19,10 @@ es **agnóstico de distro**: su superficie de integración es su contrato de var **Decisiones tomadas (brainstorming):** - Mismo repo, **rama aislada** `start9-adaptation`, todo bajo `start9/` (no toca archivos Umbrel). - Nodo de pruebas Start9 disponible con acceso SSH/web, indexador **electrs**, red **mainnet**. +- **Multi-red (testnet4, signet, mainnet) con la red autodetectada desde el nodo Bitcoin.** El + binario ya lo hace: `discovery::apply_network_from_rpc` (incondicional) mapea + `getblockchaininfo.chain` → red. El `entrypoint.sh` **no** fija `BROADCAST_POOL_NETWORK`. + (Se corrigió el orden en `src/main.rs`: detectar red ANTES de abrir la DB, que es por-red.) - Verificación = **pruebas de humo seguras** (conexión, genesis, handshake, dashboard, que Sparrow/Liana conecten). **NO** se difundirá una tx de valor real. @@ -66,6 +70,7 @@ Estructura TS SDK (como frigate): | `BROADCAST_POOL_ELECTRUM_HOST` | `0.0.0.0` | | `BROADCAST_POOL_ELECTRUM_PORT` | `50050` | | `BROADCAST_POOL_WEB_HOST`/`PORT` | `0.0.0.0` / `8080` | +| `BROADCAST_POOL_NETWORK` | **no se define** — autodetectada por RPC | | `BROADCAST_POOL_UMBREL` | **no se define** (modo genérico) | El cookie de bitcoind se resuelve igual que frigate (`/mnt/bitcoind/.cookie`, con fallback a @@ -96,7 +101,7 @@ empaquetado no añade lógica Rust nueva salvo el posible ajuste de INDEXER_URL, ## Alcance / YAGNI - v1 expone **un** puerto Electrum (50050) para Sparrow y Liana; el `liana_port` separado (PoC de altura/anti-fee-sniping) **no** se expone en v1. -- Solo red **mainnet** (es la del nodo Start9); el binario soporta otras redes si se cambia el env. +- **Multi-red por autodetección RPC** (testnet4/signet/mainnet); sin selector manual en la UI. - Sin acciones/actions extra más allá de las de plumbing en v1. ## Riesgos diff --git a/src/main.rs b/src/main.rs index 3452ac3..73286f4 100644 --- a/src/main.rs +++ b/src/main.rs @@ -338,13 +338,11 @@ async fn main() -> Result<()> { } } - let data_dir = get_data_dir(&config)?; - std::fs::create_dir_all(&data_dir)?; - - let db_path = config.db_path(&data_dir); - let db = Arc::new(Database::open(&db_path)?); - - // RPC is only created when needed (lazy init) + // RPC is only created when needed (lazy init). Built BEFORE deriving the data dir / DB + // so network auto-detection can correct config.network first: both the data dir and the + // DB filename are network-specific, so detecting the network AFTER opening the DB would + // bind the wrong network. This matters when BROADCAST_POOL_NETWORK is unset and we rely + // purely on auto-detection (e.g. StartOS), where the config default is testnet4. let rpc_needed = matches!( &cli.command, Commands::Start { .. } | Commands::TestRpc | Commands::BroadcastAll { .. } @@ -365,8 +363,14 @@ async fn main() -> Result<()> { None }; - // Auto-detect network (Bitcoin RPC), indexer (50001/50002), and LAN IP for wallet URL. + // Auto-detect network from Bitcoin Core BEFORE deriving the network-specific data dir and DB. discovery::apply_network_from_rpc(&mut config, rpc.as_deref()); + + let data_dir = get_data_dir(&config)?; + std::fs::create_dir_all(&data_dir)?; + + let db_path = config.db_path(&data_dir); + let db = Arc::new(Database::open(&db_path)?); let indexer_before = config.indexer.clone(); let indexer_found = if discovery::is_umbrel_mode() { discovery::heal_umbrel_indexer_config(&mut config); diff --git a/start9/.gitignore b/start9/.gitignore new file mode 100644 index 0000000..300d670 --- /dev/null +++ b/start9/.gitignore @@ -0,0 +1,3 @@ +node_modules/ +javascript/ +*.s9pk diff --git a/start9/Dockerfile b/start9/Dockerfile new file mode 100644 index 0000000..be05b52 --- /dev/null +++ b/start9/Dockerfile @@ -0,0 +1,34 @@ +# StartOS image for broadcast-pool. +# Built by `start-cli s9pk pack` with the manifest's dockerBuild workdir set to the +# REPOSITORY ROOT (start9/startos/manifest/index.ts -> source.dockerBuild.workdir = '..'), +# so all COPY paths below are root-relative — same convention as the Umbrel Dockerfile. +FROM rust:1.86-bookworm AS builder + +WORKDIR /build +COPY Cargo.toml Cargo.lock ./ +COPY src/ src/ + +RUN apt-get update && apt-get install -y pkg-config libssl-dev && rm -rf /var/lib/apt/lists/* +RUN cargo build --release && strip target/release/broadcast-pool + +FROM debian:bookworm-slim + +RUN apt-get update && apt-get install -y ca-certificates && rm -rf /var/lib/apt/lists/* + +COPY --from=builder /build/target/release/broadcast-pool /usr/local/bin/broadcast-pool +COPY src/api/dashboard.html /usr/share/broadcast-pool/dashboard.html +COPY start9/entrypoint.sh /entrypoint.sh +RUN chmod +x /entrypoint.sh + +# Static, non-network-specific defaults. The network is auto-detected from Bitcoin +# Core at runtime (see entrypoint.sh / discovery::apply_network_from_rpc), so we do +# NOT set BROADCAST_POOL_NETWORK here. +ENV BROADCAST_POOL_DASHBOARD=/usr/share/broadcast-pool/dashboard.html +ENV BROADCAST_POOL_DATA_DIR=/data +ENV BROADCAST_POOL_WEB_HOST=0.0.0.0 +ENV BROADCAST_POOL_WEB_PORT=8080 +ENV BROADCAST_POOL_ELECTRUM_HOST=0.0.0.0 +ENV BROADCAST_POOL_ELECTRUM_PORT=50050 + +EXPOSE 8080 50050 +ENTRYPOINT ["/entrypoint.sh"] diff --git a/start9/Makefile b/start9/Makefile new file mode 100644 index 0000000..9babe8f --- /dev/null +++ b/start9/Makefile @@ -0,0 +1,2 @@ +ARCHES := x86 arm +include s9pk.mk diff --git a/start9/entrypoint.sh b/start9/entrypoint.sh new file mode 100644 index 0000000..a0dd2a7 --- /dev/null +++ b/start9/entrypoint.sh @@ -0,0 +1,80 @@ +#!/bin/sh +set -eu + +# StartOS entrypoint for broadcast-pool. +# +# Translates the StartOS environment into the binary's generic BROADCAST_POOL_* contract. +# The network is NOT set here on purpose: the binary auto-detects it from Bitcoin Core +# (discovery::apply_network_from_rpc, via getblockchaininfo.chain), so the same image works +# on mainnet, testnet4 and signet without changes. +# +# Connection details default to the StartOS internal service hostnames but can be overridden +# by main.ts (which can read the real RPC/electrs addresses from the StartOS dependency config). + +DATA_DIR="${BROADCAST_POOL_DATA_DIR:-/data}" +BITCOIN_DATA_DIR="${BITCOIN_DATA_DIR:-/mnt/bitcoind}" +BITCOIN_RPC_URL="${BROADCAST_POOL_RPC_URL:-http://bitcoind.startos:8332}" +ELECTRS_URL="${BROADCAST_POOL_INDEXER_URL:-tcp://electrs.startos:50001}" + +BOOT_LOG="${DATA_DIR}/startos-boot.log" +mkdir -p "${DATA_DIR}" + +log() { echo "$(date -Iseconds) $*" >> "${BOOT_LOG}"; } + +log "=== broadcast-pool StartOS boot ===" +log "BITCOIN_RPC_URL=${BITCOIN_RPC_URL}" +log "ELECTRS_URL=${ELECTRS_URL}" +log "BITCOIN_DATA_DIR=${BITCOIN_DATA_DIR}" + +# --- Resolve the bitcoind RPC cookie (auth) --------------------------------- +# Bitcoin Core writes a .cookie file in its (network-specific) data dir as +# "__cookie__:". The cookie may live in a network subdirectory +# (e.g. signet/.cookie), so fall back to a recursive find like the frigate package. +resolve_cookie_file() { + if [ -f "${BITCOIN_DATA_DIR}/.cookie" ]; then + echo "${BITCOIN_DATA_DIR}/.cookie" + return 0 + fi + find "${BITCOIN_DATA_DIR}" -maxdepth 4 -name '.cookie' -type f 2>/dev/null | head -1 +} + +# Wait for the cookie to appear (bitcoind may still be starting). +WAIT_MAX="${BITCOIN_WAIT_MAX:-180}" +WAIT_INTERVAL="${BITCOIN_WAIT_INTERVAL:-5}" +elapsed=0 +COOKIE_FILE="" +while [ "${elapsed}" -lt "${WAIT_MAX}" ]; do + COOKIE_FILE="$(resolve_cookie_file || true)" + if [ -n "${COOKIE_FILE}" ] && [ -f "${COOKIE_FILE}" ]; then + break + fi + log "Waiting for Bitcoin Core cookie under ${BITCOIN_DATA_DIR} (${elapsed}s/${WAIT_MAX}s)" + sleep "${WAIT_INTERVAL}" + elapsed=$((elapsed + WAIT_INTERVAL)) +done + +if [ -z "${COOKIE_FILE}" ] || [ ! -f "${COOKIE_FILE}" ]; then + echo "ERROR: Bitcoin Core cookie not found under ${BITCOIN_DATA_DIR} after ${WAIT_MAX}s" >&2 + log "ERROR: cookie not found after ${WAIT_MAX}s" + exit 1 +fi + +COOKIE="$(cat "${COOKIE_FILE}")" +RPC_USER="${COOKIE%%:*}" +RPC_PASS="${COOKIE#*:}" +log "Cookie resolved from ${COOKIE_FILE} (user=${RPC_USER})" + +# --- Export the binary's generic contract ----------------------------------- +export BROADCAST_POOL_DATA_DIR="${DATA_DIR}" +export BROADCAST_POOL_RPC_URL="${BITCOIN_RPC_URL}" +export BROADCAST_POOL_RPC_USER="${RPC_USER}" +export BROADCAST_POOL_RPC_PASS="${RPC_PASS}" +export BROADCAST_POOL_INDEXER_URL="${ELECTRS_URL}" +export BROADCAST_POOL_ELECTRUM_HOST="${BROADCAST_POOL_ELECTRUM_HOST:-0.0.0.0}" +export BROADCAST_POOL_ELECTRUM_PORT="${BROADCAST_POOL_ELECTRUM_PORT:-50050}" +export BROADCAST_POOL_WEB_HOST="${BROADCAST_POOL_WEB_HOST:-0.0.0.0}" +export BROADCAST_POOL_WEB_PORT="${BROADCAST_POOL_WEB_PORT:-8080}" +# Deliberately NOT set: BROADCAST_POOL_NETWORK (auto-detected), BROADCAST_POOL_UMBREL. + +log "Starting broadcast-pool (network auto-detected from Bitcoin Core)" +exec broadcast-pool start --foreground diff --git a/start9/icon.png b/start9/icon.png new file mode 100644 index 0000000000000000000000000000000000000000..c2046c0a048efee6487aab45cc4b833e83a73566 GIT binary patch literal 14687 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 literal 0 HcmV?d00001 diff --git a/start9/package-lock.json b/start9/package-lock.json new file mode 100644 index 0000000..d8aa8d4 --- /dev/null +++ b/start9/package-lock.json @@ -0,0 +1,426 @@ +{ + "name": "broadcast-pool-startos", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "name": "broadcast-pool-startos", + "dependencies": { + "@start9labs/start-sdk": "1.5.3", + "bitcoin-core-startos": "github:Start9Labs/bitcoin-core-startos#next/28.x" + }, + "devDependencies": { + "@types/node": "^22.19.1", + "@vercel/ncc": "^0.38.4", + "prettier": "^3.6.2", + "typescript": "^5.9.3" + } + }, + "node_modules/@iarna/toml": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/@iarna/toml/-/toml-3.0.0.tgz", + "integrity": "sha512-td6ZUkz2oS3VeleBcN+m//Q6HlCFCPrnI0FZhrt/h4XqLEdOyYp2u21nd8MdsR+WJy5r9PTDaHTDDfhf4H4l6Q==", + "license": "ISC" + }, + "node_modules/@noble/curves": { + "version": "1.9.7", + "resolved": "https://registry.npmjs.org/@noble/curves/-/curves-1.9.7.tgz", + "integrity": "sha512-gbKGcRUYIjA3/zCCNaWDciTMFI0dCkvou3TL8Zmy5Nc7sJ47a0jtOeZoTaMxkuqRo9cRhjOdZJXegxYE5FN/xw==", + "license": "MIT", + "dependencies": { + "@noble/hashes": "1.8.0" + }, + "engines": { + "node": "^14.21.3 || >=16" + }, + "funding": { + "url": "https://paulmillr.com/funding/" + } + }, + "node_modules/@noble/hashes": { + "version": "1.8.0", + "resolved": "https://registry.npmjs.org/@noble/hashes/-/hashes-1.8.0.tgz", + "integrity": "sha512-jCs9ldd7NwzpgXDIf6P3+NrHh9/sD6CQdxHyjQI+h/6rDNo88ypBxxz45UDuZHz9r3tNz7N/VInSVoVdtXEI4A==", + "license": "MIT", + "engines": { + "node": "^14.21.3 || >=16" + }, + "funding": { + "url": "https://paulmillr.com/funding/" + } + }, + "node_modules/@nodable/entities": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/@nodable/entities/-/entities-2.2.0.tgz", + "integrity": "sha512-9uGyhaQavEUMC8AIddIjau4NsnsXhou+j5sBAGojCM1oxmQpVKTWR/9JxABD6UAv12vpIms55fPZKFQEhG6uBg==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/nodable" + } + ], + "license": "MIT" + }, + "node_modules/@start9labs/start-sdk": { + "version": "1.5.3", + "resolved": "https://registry.npmjs.org/@start9labs/start-sdk/-/start-sdk-1.5.3.tgz", + "integrity": "sha512-OyHe9J6hMvyA5ZavcLkxdVQvZcuTH9J9kagV6NDI83eAG/YpJFIq62gP/n/2PPNdHWwNSXVQmSwnsvsV8Gyg+A==", + "license": "MIT", + "dependencies": { + "@iarna/toml": "^3.0.0", + "@noble/curves": "^1.9.7", + "@noble/hashes": "^1.8.0", + "@types/ini": "^4.1.1", + "deep-equality-data-structures": "^2.0.0", + "fast-xml-parser": "~5.7.0", + "ini": "^5.0.0", + "isomorphic-fetch": "^3.0.0", + "mime": "^4.1.0", + "yaml": "^2.8.3", + "zod": "4.3.6", + "zod-deep-partial": "^1.2.0" + } + }, + "node_modules/@types/ini": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/@types/ini/-/ini-4.1.1.tgz", + "integrity": "sha512-MIyNUZipBTbyUNnhvuXJTY7B6qNI78meck9Jbv3wk0OgNwRyOOVEKDutAkOs1snB/tx0FafyR6/SN4Ps0hZPeg==", + "license": "MIT" + }, + "node_modules/@types/node": { + "version": "22.20.0", + "resolved": "https://registry.npmjs.org/@types/node/-/node-22.20.0.tgz", + "integrity": "sha512-QWlFW2wf3nTjC13/DqRnBpR4ZO36VJH/JVBkA/vcnmbTBNQIlnObqyqZE1tUR7+Ni23Lda8R1BxMfbXRpCUx5g==", + "dev": true, + "license": "MIT", + "dependencies": { + "undici-types": "~6.21.0" + } + }, + "node_modules/@vercel/ncc": { + "version": "0.38.4", + "resolved": "https://registry.npmjs.org/@vercel/ncc/-/ncc-0.38.4.tgz", + "integrity": "sha512-8LwjnlP39s08C08J5NstzriPvW1SP8Zfpp1BvC2sI35kPeZnHfxVkCwu4/+Wodgnd60UtT1n8K8zw+Mp7J9JmQ==", + "dev": true, + "license": "MIT", + "bin": { + "ncc": "dist/ncc/cli.js" + } + }, + "node_modules/anynum": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/anynum/-/anynum-1.0.1.tgz", + "integrity": "sha512-N6//FLET/tXYNM/F6ABca1oH6fWB+KlTt909Le28WMDBk8oaT4vY17DCrwg2MvmuqUKt3Ni4N5dGJ/EoBgcO6A==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/NaturalIntelligence" + } + ], + "license": "MIT" + }, + "node_modules/bitcoin-core-startos": { + "name": "bitcoin-core", + "resolved": "git+ssh://git@github.com/Start9Labs/bitcoin-core-startos.git#cfe246bbb5e9806fb4498b02ec9d542452c25b2d", + "dependencies": { + "@start9labs/start-sdk": "1.5.2", + "diskusage": "^1.2.0" + } + }, + "node_modules/bitcoin-core-startos/node_modules/@start9labs/start-sdk": { + "version": "1.5.2", + "resolved": "https://registry.npmjs.org/@start9labs/start-sdk/-/start-sdk-1.5.2.tgz", + "integrity": "sha512-3L4OKuH9kUtuH6G20BSPk85yN/jS3+0WNkP19ahwO9Nc7L6STel4hujvKii3osf0p0tU2q5Mk2Y8b9f2dVR1ng==", + "license": "MIT", + "dependencies": { + "@iarna/toml": "^3.0.0", + "@noble/curves": "^1.9.7", + "@noble/hashes": "^1.8.0", + "@types/ini": "^4.1.1", + "deep-equality-data-structures": "^2.0.0", + "fast-xml-parser": "~5.7.0", + "ini": "^5.0.0", + "isomorphic-fetch": "^3.0.0", + "mime": "^4.1.0", + "yaml": "^2.8.3", + "zod": "4.3.6", + "zod-deep-partial": "^1.2.0" + } + }, + "node_modules/deep-equality-data-structures": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/deep-equality-data-structures/-/deep-equality-data-structures-2.0.0.tgz", + "integrity": "sha512-qgrUr7MKXq7VRN+WUpQ48QlXVGL0KdibAoTX8KRg18lgOgqbEKMAW1WZsVCtakY4+XX42pbAJzTz/DlXEFM2Fg==", + "license": "MIT", + "dependencies": { + "object-hash": "^3.0.0" + } + }, + "node_modules/diskusage": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/diskusage/-/diskusage-1.2.0.tgz", + "integrity": "sha512-2u3OG3xuf5MFyzc4MctNRUKjjwK+UkovRYdD2ed/NZNZPrt0lqHnLKxGhlFVvAb4/oufIgQG3nWgwmeTbHOvXA==", + "hasInstallScript": true, + "license": "MIT", + "dependencies": { + "es6-promise": "^4.2.8", + "nan": "^2.18.0" + } + }, + "node_modules/es6-promise": { + "version": "4.2.8", + "resolved": "https://registry.npmjs.org/es6-promise/-/es6-promise-4.2.8.tgz", + "integrity": "sha512-HJDGx5daxeIvxdBxvG2cb9g4tEvwIk3i8+nhX0yGrYmZUzbkdg8QbDevheDB8gd0//uPj4c1EQua8Q+MViT0/w==", + "license": "MIT" + }, + "node_modules/fast-xml-builder": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/fast-xml-builder/-/fast-xml-builder-1.2.0.tgz", + "integrity": "sha512-00aAWieqff+ZJhsXA4g1g7M8k+7AYoMUUHF+/zFb5U6Uv/P0Vl4QZo84/IcufzYalLuEj9928bXN9PbbFzMF0Q==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/NaturalIntelligence" + } + ], + "license": "MIT", + "dependencies": { + "path-expression-matcher": "^1.5.0", + "xml-naming": "^0.1.0" + } + }, + "node_modules/fast-xml-parser": { + "version": "5.7.3", + "resolved": "https://registry.npmjs.org/fast-xml-parser/-/fast-xml-parser-5.7.3.tgz", + "integrity": "sha512-C0AaNuC+mscy6vrAQKAc/rMq+zAPHodfHGZu4sGVehvAQt/JLG1O5zEcYcXSY5zSqr4YVgxsB+pHXTq0i7eDlg==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/NaturalIntelligence" + } + ], + "license": "MIT", + "dependencies": { + "@nodable/entities": "^2.1.0", + "fast-xml-builder": "^1.1.7", + "path-expression-matcher": "^1.5.0", + "strnum": "^2.2.3" + }, + "bin": { + "fxparser": "src/cli/cli.js" + } + }, + "node_modules/ini": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/ini/-/ini-5.0.0.tgz", + "integrity": "sha512-+N0ngpO3e7cRUWOJAS7qw0IZIVc6XPrW4MlFBdD066F2L4k1L6ker3hLqSq7iXxU5tgS4WGkIUElWn5vogAEnw==", + "license": "ISC", + "engines": { + "node": "^18.17.0 || >=20.5.0" + } + }, + "node_modules/isomorphic-fetch": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/isomorphic-fetch/-/isomorphic-fetch-3.0.0.tgz", + "integrity": "sha512-qvUtwJ3j6qwsF3jLxkZ72qCgjMysPzDfeV240JHiGZsANBYd+EEuu35v7dfrJ9Up0Ak07D7GGSkGhCHTqg/5wA==", + "license": "MIT", + "dependencies": { + "node-fetch": "^2.6.1", + "whatwg-fetch": "^3.4.1" + } + }, + "node_modules/mime": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/mime/-/mime-4.1.0.tgz", + "integrity": "sha512-X5ju04+cAzsojXKes0B/S4tcYtFAJ6tTMuSPBEn9CPGlrWr8Fiw7qYeLT0XyH80HSoAoqWCaz+MWKh22P7G1cw==", + "funding": [ + "https://github.com/sponsors/broofa" + ], + "license": "MIT", + "bin": { + "mime": "bin/cli.js" + }, + "engines": { + "node": ">=16" + } + }, + "node_modules/nan": { + "version": "2.28.0", + "resolved": "https://registry.npmjs.org/nan/-/nan-2.28.0.tgz", + "integrity": "sha512-fTsDz99OTq2sVePhGdp4qQhggZFtKr64ZNVyVajRKtMOkJxYekplBh577PiJB12v/D3s2E5cGtOI45LWp6rnLQ==", + "license": "MIT" + }, + "node_modules/node-fetch": { + "version": "2.7.0", + "resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-2.7.0.tgz", + "integrity": "sha512-c4FRfUm/dbcWZ7U+1Wq0AwCyFL+3nt2bEw05wfxSz+DWpWsitgmSgYmy2dQdWyKC1694ELPqMs/YzUSNozLt8A==", + "license": "MIT", + "dependencies": { + "whatwg-url": "^5.0.0" + }, + "engines": { + "node": "4.x || >=6.0.0" + }, + "peerDependencies": { + "encoding": "^0.1.0" + }, + "peerDependenciesMeta": { + "encoding": { + "optional": true + } + } + }, + "node_modules/object-hash": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/object-hash/-/object-hash-3.0.0.tgz", + "integrity": "sha512-RSn9F68PjH9HqtltsSnqYC1XXoWe9Bju5+213R98cNGttag9q9yAOTzdbsqvIa7aNm5WffBZFpWYr2aWrklWAw==", + "license": "MIT", + "engines": { + "node": ">= 6" + } + }, + "node_modules/path-expression-matcher": { + "version": "1.6.1", + "resolved": "https://registry.npmjs.org/path-expression-matcher/-/path-expression-matcher-1.6.1.tgz", + "integrity": "sha512-h7bxdzhHk8Knyc4Tj+jMaa7fEEoUJy7p1qtbVgkYg1Uhpe5Np5VuGXCRZnkZvU+Q42M1vStt0ifa3ueykRJPmQ==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/NaturalIntelligence" + } + ], + "license": "MIT", + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/prettier": { + "version": "3.8.5", + "resolved": "https://registry.npmjs.org/prettier/-/prettier-3.8.5.tgz", + "integrity": "sha512-zxcTTCedNGJM4R8sj/Cq/F0W/c4iE0afWBcBwMTRtw4WHYP9TWkYjdiH3npPRUYsXQCPR0hTU9yjovOu+E6EQA==", + "dev": true, + "license": "MIT", + "bin": { + "prettier": "bin/prettier.cjs" + }, + "engines": { + "node": ">=14" + }, + "funding": { + "url": "https://github.com/prettier/prettier?sponsor=1" + } + }, + "node_modules/strnum": { + "version": "2.4.1", + "resolved": "https://registry.npmjs.org/strnum/-/strnum-2.4.1.tgz", + "integrity": "sha512-M9eUSMT2dCB2cTNPG7UYj6KuK7RJR2SN2+yCV/fTW3xzTCS6EaGZ5pSMgDIjB7r8zSfTGk+dvvn9rTjpVS9Mwg==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/NaturalIntelligence" + } + ], + "license": "MIT", + "dependencies": { + "anynum": "^1.0.1" + } + }, + "node_modules/tr46": { + "version": "0.0.3", + "resolved": "https://registry.npmjs.org/tr46/-/tr46-0.0.3.tgz", + "integrity": "sha512-N3WMsuqV66lT30CrXNbEjx4GEwlow3v6rr4mCcv6prnfwhS01rkgyFdjPNBYd9br7LpXV1+Emh01fHnq2Gdgrw==", + "license": "MIT" + }, + "node_modules/typescript": { + "version": "5.9.3", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.9.3.tgz", + "integrity": "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==", + "dev": true, + "license": "Apache-2.0", + "bin": { + "tsc": "bin/tsc", + "tsserver": "bin/tsserver" + }, + "engines": { + "node": ">=14.17" + } + }, + "node_modules/undici-types": { + "version": "6.21.0", + "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-6.21.0.tgz", + "integrity": "sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/webidl-conversions": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-3.0.1.tgz", + "integrity": "sha512-2JAn3z8AR6rjK8Sm8orRC0h/bcl/DqL7tRPdGZ4I1CjdF+EaMLmYxBHyXuKL849eucPFhvBoxMsflfOb8kxaeQ==", + "license": "BSD-2-Clause" + }, + "node_modules/whatwg-fetch": { + "version": "3.6.20", + "resolved": "https://registry.npmjs.org/whatwg-fetch/-/whatwg-fetch-3.6.20.tgz", + "integrity": "sha512-EqhiFU6daOA8kpjOWTL0olhVOF3i7OrFzSYiGsEMB8GcXS+RrzauAERX65xMeNWVqxA6HXH2m69Z9LaKKdisfg==", + "license": "MIT" + }, + "node_modules/whatwg-url": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/whatwg-url/-/whatwg-url-5.0.0.tgz", + "integrity": "sha512-saE57nupxk6v3HY35+jzBwYa0rKSy0XR8JSxZPwgLr7ys0IBzhGviA1/TUGJLmSVqs8pb9AnvICXEuOHLprYTw==", + "license": "MIT", + "dependencies": { + "tr46": "~0.0.3", + "webidl-conversions": "^3.0.0" + } + }, + "node_modules/xml-naming": { + "version": "0.1.0", + "resolved": "https://registry.npmjs.org/xml-naming/-/xml-naming-0.1.0.tgz", + "integrity": "sha512-k8KO9hrMyNk6tUWqUfkTEZbezRRpONVOzUTnc97VnCvyj6Tf9lyUR9EDAIeiVLv56jsMcoXEwjW8Kv5yPY52lw==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/NaturalIntelligence" + } + ], + "license": "MIT", + "engines": { + "node": ">=16.0.0" + } + }, + "node_modules/yaml": { + "version": "2.9.0", + "resolved": "https://registry.npmjs.org/yaml/-/yaml-2.9.0.tgz", + "integrity": "sha512-2AvhNX3mb8zd6Zy7INTtSpl1F15HW6Wnqj0srWlkKLcpYl/gMIMJiyuGq2KeI2YFxUPjdlB+3Lc10seMLtL4cA==", + "license": "ISC", + "bin": { + "yaml": "bin.mjs" + }, + "engines": { + "node": ">= 14.6" + }, + "funding": { + "url": "https://github.com/sponsors/eemeli" + } + }, + "node_modules/zod": { + "version": "4.3.6", + "resolved": "https://registry.npmjs.org/zod/-/zod-4.3.6.tgz", + "integrity": "sha512-rftlrkhHZOcjDwkGlnUtZZkvaPHCsDATp4pGpuOOMDaTdDDXF91wuVDJoWoPsKX/3YPQ5fHuF3STjcYyKr+Qhg==", + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/colinhacks" + } + }, + "node_modules/zod-deep-partial": { + "version": "1.4.4", + "resolved": "https://registry.npmjs.org/zod-deep-partial/-/zod-deep-partial-1.4.4.tgz", + "integrity": "sha512-aWkPl7hVStgE01WzbbSxCgX4O+sSpgt8JOjvFUtMTF75VgL6MhWQbiZi+AWGN85SfSTtI9gsOtL1vInoqfDVaA==", + "license": "MIT", + "peerDependencies": { + "zod": "^4.1.13" + } + } + } +} diff --git a/start9/package.json b/start9/package.json new file mode 100644 index 0000000..603d8fb --- /dev/null +++ b/start9/package.json @@ -0,0 +1,24 @@ +{ + "name": "broadcast-pool-startos", + "scripts": { + "build": "rm -rf ./javascript && ncc build startos/index.ts -o ./javascript", + "prettier": "prettier --write startos", + "check": "tsc --noEmit" + }, + "dependencies": { + "@start9labs/start-sdk": "1.5.3", + "bitcoin-core-startos": "github:Start9Labs/bitcoin-core-startos#next/28.x" + }, + "devDependencies": { + "@types/node": "^22.19.1", + "@vercel/ncc": "^0.38.4", + "prettier": "^3.6.2", + "typescript": "^5.9.3" + }, + "prettier": { + "trailingComma": "all", + "tabWidth": 2, + "semi": false, + "singleQuote": true + } +} diff --git a/start9/s9pk.mk b/start9/s9pk.mk new file mode 100644 index 0000000..4c47870 --- /dev/null +++ b/start9/s9pk.mk @@ -0,0 +1,138 @@ +# ** Plumbing. DO NOT EDIT **. +# This file is imported by ./Makefile. Make edits there + +PACKAGE_ID := $(shell awk -F"'" '/id:/ {print $$2}' startos/manifest/index.ts) +INGREDIENTS := $(shell start-cli s9pk list-ingredients 2>/dev/null) +# Resolve the actual git dir so this works inside git worktrees, where .git +# is a file pointing at

/.git/worktrees/ rather than a directory. +GIT_DIR := $(shell git rev-parse --git-dir 2>/dev/null) +GIT_DEPS := $(if $(GIT_DIR),$(GIT_DIR)/HEAD $(GIT_DIR)/index) +ARCHES ?= x86 arm riscv +# TARGETS is the list of leaf make-targets the build matrix fans out over. +# Defaults to the arches; variant packages override (e.g. immich, ollama, vllm +# set this to a list of variant or variant-arch leaf targets). +TARGETS ?= $(ARCHES) +ifdef VARIANT +BASE_NAME := $(PACKAGE_ID)_$(VARIANT) +else +BASE_NAME := $(PACKAGE_ID) +endif + +.PHONY: all arches aarch64 x86_64 riscv64 arm arm64 x86 riscv arch/* clean install check-deps check-init package ingredients +.DELETE_ON_ERROR: +.SECONDARY: + +define SUMMARY + @manifest=$$(start-cli s9pk inspect $(1) manifest); \ + size=$$(du -h $(1) | awk '{print $$1}'); \ + title=$$(printf '%s' "$$manifest" | jq -r .title); \ + version=$$(printf '%s' "$$manifest" | jq -r .version); \ + arches=$$(printf '%s' "$$manifest" | jq -r '[.images[].arch // []] | flatten | unique | join(", ")'); \ + sdkv=$$(printf '%s' "$$manifest" | jq -r .sdkVersion); \ + gitHash=$$(printf '%s' "$$manifest" | jq -r .gitHash | sed -E 's/(.*-modified)$$/\x1b[0;31m\1\x1b[0m/'); \ + printf "\n"; \ + printf "\033[1;32m✅ Build Complete!\033[0m\n"; \ + printf "\n"; \ + printf "\033[1;37m📦 $$title\033[0m \033[36mv$$version\033[0m\n"; \ + printf "───────────────────────────────\n"; \ + printf " \033[1;36mFilename:\033[0m %s\n" "$(1)"; \ + printf " \033[1;36mSize:\033[0m %s\n" "$$size"; \ + printf " \033[1;36mArch:\033[0m %s\n" "$$arches"; \ + printf " \033[1;36mSDK:\033[0m %s\n" "$$sdkv"; \ + printf " \033[1;36mGit:\033[0m %s\n" "$$gitHash"; \ + echo "" +endef + +all: $(TARGETS) + +arches: $(ARCHES) + +# Generic make-variable introspection. Used by the release workflow to +# read $(TARGETS) and fan out one matrix runner per target. `make -s +# print-TARGETS` echoes the list with no other output. +print-%: + @echo '$($*)' + +universal: $(BASE_NAME).s9pk + $(call SUMMARY,$<) + +arch/%: $(BASE_NAME)_%.s9pk + $(call SUMMARY,$<) + +x86 x86_64: arch/x86_64 +arm arm64 aarch64: arch/aarch64 +riscv riscv64: arch/riscv64 + +$(BASE_NAME).s9pk: $(INGREDIENTS) $(GIT_DEPS) + @$(MAKE) --no-print-directory ingredients + @echo " Packing '$@'..." + start-cli s9pk pack -o $@ + +$(BASE_NAME)_%.s9pk: $(INGREDIENTS) $(GIT_DEPS) + @$(MAKE) --no-print-directory ingredients + @echo " Packing '$@'..." + start-cli s9pk pack --arch=$* -o $@ + +ingredients: $(INGREDIENTS) + @echo " Re-evaluating ingredients..." + +install: | check-deps check-init + @HOST=$$(awk -F'/' '/^host:/ {print $$3}' ~/.startos/config.yaml); \ + if [ -z "$$HOST" ]; then \ + echo "Error: You must define \"host: http://server-name.local\" in ~/.startos/config.yaml"; \ + exit 1; \ + fi; \ + S9PK=$$(ls -t *.s9pk 2>/dev/null | head -1); \ + if [ -z "$$S9PK" ]; then \ + echo "Error: No .s9pk file found. Run 'make' first."; \ + exit 1; \ + fi; \ + printf "\n🚀 Installing %s to %s ...\n" "$$S9PK" "$$HOST"; \ + start-cli package install -s "$$S9PK" + +publish: | all + @REGISTRY=$$(awk -F'/' '/^registry:/ {print $$3}' ~/.startos/config.yaml); \ + if [ -z "$$REGISTRY" ]; then \ + echo "Error: You must define \"registry: https://my-registry.tld\" in ~/.startos/config.yaml"; \ + exit 1; \ + fi; \ + S3BASE=$$(awk -F'/' '/^s9pk-s3base:/ {print $$3}' ~/.startos/config.yaml); \ + if [ -z "$$S3BASE" ]; then \ + echo "Error: You must define \"s3base: https://s9pks.my-s3-bucket.tld\" in ~/.startos/config.yaml"; \ + exit 1; \ + fi; \ + command -v s3cmd >/dev/null || \ + (echo "Error: s3cmd not found. It must be installed to publish using s3." && exit 1); \ + printf "\n🚀 Publishing to %s; indexing on %s ...\n" "$$S3BASE" "$$REGISTRY"; \ + for s9pk in *.s9pk; do \ + age=$$(( $$(date +%s) - $$(stat -c %Y "$$s9pk") )); \ + if [ "$$age" -gt 3600 ]; then \ + printf "\033[1;33m⚠️ %s is %d minutes old. Publish anyway? [y/N] \033[0m" "$$s9pk" "$$((age / 60))"; \ + read -r ans; \ + case "$$ans" in [yY]*) ;; *) echo "Skipping $$s9pk"; continue ;; esac; \ + fi; \ + start-cli s9pk publish "$$s9pk"; \ + done + +check-deps: + @command -v start-cli >/dev/null || \ + (echo "Error: start-cli not found. Please see https://docs.start9.com/latest/developer-guide/sdk/installing-the-sdk" && exit 1) + @command -v npm >/dev/null || \ + (echo "Error: npm not found. Please install Node.js and npm." && exit 1) + +check-init: + @if [ ! -f ~/.startos/developer.key.pem ]; then \ + echo "Initializing StartOS developer environment..."; \ + start-cli init-key; \ + fi + +javascript/index.js: $(shell find startos -type f) tsconfig.json node_modules + npm run check + npm run build + +node_modules: package-lock.json package.json + npm ci + +clean: + @echo "Cleaning up build artifacts..." + @rm -rf $(PACKAGE_ID).s9pk $(PACKAGE_ID)_x86_64.s9pk $(PACKAGE_ID)_aarch64.s9pk $(PACKAGE_ID)_riscv64.s9pk javascript node_modules diff --git a/start9/startos/actions/index.ts b/start9/startos/actions/index.ts new file mode 100644 index 0000000..c82cb95 --- /dev/null +++ b/start9/startos/actions/index.ts @@ -0,0 +1,3 @@ +import { sdk } from '../sdk' + +export const actions = sdk.Actions.of() diff --git a/start9/startos/backups.ts b/start9/startos/backups.ts new file mode 100644 index 0000000..8561946 --- /dev/null +++ b/start9/startos/backups.ts @@ -0,0 +1,9 @@ +import { sdk } from './sdk' + +// Back up the whole main volume. The pending/scheduled transactions in the +// virtual mempool are the recovery-critical state, so nothing is excluded. +export const { createBackup, restoreInit } = sdk.setupBackups(async () => + sdk.Backups.ofVolumes('main').setOptions({ + exclude: [], + }), +) diff --git a/start9/startos/dependencies.ts b/start9/startos/dependencies.ts new file mode 100644 index 0000000..610c509 --- /dev/null +++ b/start9/startos/dependencies.ts @@ -0,0 +1,34 @@ +import { autoconfig } from 'bitcoin-core-startos/startos/actions/config/autoconfig' +import { i18n } from './i18n' +import { sdk } from './sdk' + +export const setDependencies = sdk.setupDependencies(async ({ effects }) => { + // Broadcast Pool only needs RPC (genesis/network/height) + a working Electrs, + // so the single hard requirement on bitcoind is that pruning is disabled + // (Electrs and prev-tx lookups need full block data). + await sdk.action.createTask(effects, 'bitcoind', autoconfig, 'critical', { + input: { + kind: 'partial', + value: { + prune: 0, + }, + }, + reason: i18n( + 'Pruning must be disabled for Electrs and transaction lookups to work.', + ), + when: { condition: 'input-not-matches', once: false }, + }) + + return { + bitcoind: { + kind: 'running', + versionRange: '>=28.0:0', + healthChecks: ['bitcoind', 'sync-progress'], + }, + electrs: { + kind: 'running', + versionRange: '>=0.10:0', + healthChecks: ['electrs', 'sync'], + }, + } +}) diff --git a/start9/startos/fileModels/store.json.ts b/start9/startos/fileModels/store.json.ts new file mode 100644 index 0000000..c469c6c --- /dev/null +++ b/start9/startos/fileModels/store.json.ts @@ -0,0 +1,16 @@ +import { FileHelper, z } from '@start9labs/start-sdk' +import { sdk } from '../sdk' + +export const shape = z + .object({ + initialized: z.boolean().catch(false), + }) + .strip() + +export const storeJson = FileHelper.json( + { + base: sdk.volumes.main, + subpath: '/store.json', + }, + shape, +) diff --git a/start9/startos/i18n/dictionaries/default.ts b/start9/startos/i18n/dictionaries/default.ts new file mode 100644 index 0000000..3069c4f --- /dev/null +++ b/start9/startos/i18n/dictionaries/default.ts @@ -0,0 +1,18 @@ +export const DEFAULT_LANG = 'en_US' + +const dict = { + 'Starting Broadcast Pool!': 0, + 'Electrum (TCP)': 1, + 'The Electrum interface is ready': 2, + 'The Electrum interface is not ready': 3, + 'Web Dashboard': 4, + 'The main Electrum interface for Sparrow and Liana (plain TCP, no SSL)': 5, + 'Web dashboard to monitor and schedule pending broadcasts': 6, + 'Provides blockchain data, RPC, and cookie authentication': 7, + 'Provides the Electrum index backend for address and history lookups': 8, + 'Pruning must be disabled for Electrs and transaction lookups to work.': 9, +} as const + +export type LangDict = Record + +export default dict diff --git a/start9/startos/i18n/dictionaries/translations.ts b/start9/startos/i18n/dictionaries/translations.ts new file mode 100644 index 0000000..af732a2 --- /dev/null +++ b/start9/startos/i18n/dictionaries/translations.ts @@ -0,0 +1,18 @@ +import { LangDict } from './default' + +const translations: Record = { + es_ES: { + 0: '¡Iniciando Broadcast Pool!', + 1: 'Electrum (TCP)', + 2: 'La interfaz Electrum está lista', + 3: 'La interfaz Electrum no está lista', + 4: 'Panel web', + 5: 'Interfaz Electrum principal para Sparrow y Liana (TCP plano, sin SSL)', + 6: 'Panel web para monitorizar y programar las difusiones pendientes', + 7: 'Proporciona datos de blockchain, RPC y autenticación por cookie', + 8: 'Proporciona el backend de índice Electrum para direcciones e historial', + 9: 'La poda debe estar deshabilitada para que Electrs y las búsquedas de transacciones funcionen.', + }, +} + +export default translations diff --git a/start9/startos/i18n/index.ts b/start9/startos/i18n/index.ts new file mode 100644 index 0000000..1849d6d --- /dev/null +++ b/start9/startos/i18n/index.ts @@ -0,0 +1,5 @@ +import { setupI18n } from '@start9labs/start-sdk' +import defaultDict, { DEFAULT_LANG } from './dictionaries/default' +import translations from './dictionaries/translations' + +export const i18n = setupI18n(defaultDict, translations, DEFAULT_LANG) diff --git a/start9/startos/index.ts b/start9/startos/index.ts new file mode 100644 index 0000000..7af589b --- /dev/null +++ b/start9/startos/index.ts @@ -0,0 +1,11 @@ +/** + * Plumbing. DO NOT EDIT. + */ +export { createBackup } from './backups' +export { main } from './main' +export { init, uninit } from './init' +export { actions } from './actions' +import { buildManifest } from '@start9labs/start-sdk' +import { manifest as sdkManifest } from './manifest' +import { versionGraph } from './versions' +export const manifest = buildManifest(versionGraph, sdkManifest) diff --git a/start9/startos/init/index.ts b/start9/startos/init/index.ts new file mode 100644 index 0000000..93439c5 --- /dev/null +++ b/start9/startos/init/index.ts @@ -0,0 +1,18 @@ +import { restoreInit } from '../backups' +import { setDependencies } from '../dependencies' +import { setInterfaces } from '../interfaces' +import { sdk } from '../sdk' +import { versionGraph } from '../versions' +import { actions } from '../actions' +import { seedFiles } from './seedFiles' + +export const init = sdk.setupInit( + restoreInit, + versionGraph, + seedFiles, + setInterfaces, + setDependencies, + actions, +) + +export const uninit = sdk.setupUninit(versionGraph) diff --git a/start9/startos/init/seedFiles.ts b/start9/startos/init/seedFiles.ts new file mode 100644 index 0000000..bf5e790 --- /dev/null +++ b/start9/startos/init/seedFiles.ts @@ -0,0 +1,6 @@ +import { storeJson } from '../fileModels/store.json' +import { sdk } from '../sdk' + +export const seedFiles = sdk.setupOnInit(async (effects) => { + await storeJson.merge(effects, {}) +}) diff --git a/start9/startos/interfaces.ts b/start9/startos/interfaces.ts new file mode 100644 index 0000000..b9c2237 --- /dev/null +++ b/start9/startos/interfaces.ts @@ -0,0 +1,52 @@ +import { i18n } from './i18n' +import { sdk } from './sdk' +import { electrumPort, webPort } from './utils' + +export const setInterfaces = sdk.setupInterfaces(async ({ effects }) => { + // --- Electrum proxy (plain TCP) for Sparrow / Liana --- + const electrumHost = sdk.MultiHost.of(effects, 'electrum') + const electrumOrigin = await electrumHost.bindPort(electrumPort, { + protocol: null, + preferredExternalPort: electrumPort, + addSsl: null, + secure: { ssl: false }, + }) + const electrum = sdk.createInterface(effects, { + id: 'electrum', + name: i18n('Electrum (TCP)'), + description: i18n( + 'The main Electrum interface for Sparrow and Liana (plain TCP, no SSL)', + ), + type: 'api', + masked: false, + schemeOverride: null, + username: null, + path: '', + query: {}, + }) + + // --- Web dashboard (HTTP UI) --- + const webHost = sdk.MultiHost.of(effects, 'web') + const webOrigin = await webHost.bindPort(webPort, { + protocol: 'http', + preferredExternalPort: webPort, + }) + const web = sdk.createInterface(effects, { + id: 'web', + name: i18n('Web Dashboard'), + description: i18n( + 'Web dashboard to monitor and schedule pending broadcasts', + ), + type: 'ui', + masked: false, + schemeOverride: null, + username: null, + path: '', + query: {}, + }) + + return [ + await electrumOrigin.export([electrum]), + await webOrigin.export([web]), + ] +}) diff --git a/start9/startos/main.ts b/start9/startos/main.ts new file mode 100644 index 0000000..f73fec2 --- /dev/null +++ b/start9/startos/main.ts @@ -0,0 +1,57 @@ +import { i18n } from './i18n' +import { sdk } from './sdk' +import { electrumPort } from './utils' + +export const main = sdk.setupMain(async ({ effects }) => { + console.info('Starting Broadcast Pool!') + + const container = await sdk.SubContainer.of( + effects, + { imageId: 'broadcast-pool' }, + sdk.Mounts.of() + .mountVolume({ + volumeId: 'main', + subpath: null, + mountpoint: '/data', + readonly: false, + }) + // Bitcoin Core data dir, read-only, for the RPC .cookie (auth) — the + // entrypoint reads it and derives RPC user/pass. Network is auto-detected + // from Bitcoin Core, so the same image works on mainnet/testnet4/signet. + .mountDependency({ + dependencyId: 'bitcoind', + volumeId: 'main', + subpath: null, + mountpoint: '/mnt/bitcoind', + readonly: true, + }), + 'broadcast-pool', + ) + + return sdk.Daemons.of(effects) + .addDaemon('broadcast-pool', { + subcontainer: container, + exec: { command: ['/entrypoint.sh'] }, + ready: { + display: i18n('Electrum (TCP)'), + fn: async () => { + const result = await sdk.healthCheck.checkPortListening( + effects, + electrumPort, + { + successMessage: i18n('The Electrum interface is ready'), + errorMessage: i18n('The Electrum interface is not ready'), + }, + ) + + if (result.result === 'success') return result + + return { + result: 'starting', + message: i18n('The Electrum interface is not ready'), + } + }, + }, + requires: [], + }) +}) diff --git a/start9/startos/manifest/i18n.ts b/start9/startos/manifest/i18n.ts new file mode 100644 index 0000000..f9a1d4c --- /dev/null +++ b/start9/startos/manifest/i18n.ts @@ -0,0 +1,36 @@ +export const short = { + en_US: 'Schedule and delay Bitcoin broadcasts from Sparrow/Liana', + es_ES: 'Programa y retrasa difusiones de Bitcoin desde Sparrow/Liana', +} + +export const long = { + en_US: + 'Broadcast Pool sits between your wallet (Sparrow or Liana) and your node\'s Electrum index. It intercepts transaction broadcasts and holds the signed transactions in a virtual mempool until a chosen criterion is met (immediate, scheduled date/time, fiat price, or block height). It connects automatically to Bitcoin Core and Electrs on StartOS and exposes a plain TCP Electrum interface plus a web dashboard. The network (mainnet, testnet4 or signet) is detected automatically from Bitcoin Core.', + es_ES: + 'Broadcast Pool se sitúa entre tu billetera (Sparrow o Liana) y el índice Electrum de tu nodo. Intercepta las difusiones de transacciones y retiene las transacciones firmadas en una mempool virtual hasta que se cumple el criterio elegido (inmediato, fecha/hora programada, precio fiat o altura de bloque). Se conecta automáticamente a Bitcoin Core y Electrs en StartOS y expone una interfaz Electrum TCP y un panel web. La red (mainnet, testnet4 o signet) se detecta automáticamente desde Bitcoin Core.', +} + +export const alertInstall = { + en_US: + 'Broadcast Pool requires Bitcoin Core (pruning disabled) and Electrs. A transaction shown as pending is retained in the virtual mempool and is NOT yet broadcast to the network until its trigger fires.', + es_ES: + 'Broadcast Pool requiere Bitcoin Core (sin poda) y Electrs. Una transacción marcada como pendiente se retiene en la mempool virtual y NO se difunde a la red hasta que se dispara su criterio.', +} + +export const alertStart = { + en_US: + 'Broadcast Pool will connect to Bitcoin Core and Electrs. Point Sparrow or Liana at the Electrum (TCP) interface shown below.', + es_ES: + 'Broadcast Pool se conectará a Bitcoin Core y Electrs. Apunta Sparrow o Liana a la interfaz Electrum (TCP) que se muestra abajo.', +} + +export const bitcoindDescription = { + en_US: 'Provides blockchain data, RPC, and cookie authentication', + es_ES: 'Proporciona datos de blockchain, RPC y autenticación por cookie', +} + +export const electrsDescription = { + en_US: 'Provides the Electrum index backend for address and history lookups', + es_ES: + 'Proporciona el backend de índice Electrum para direcciones e historial', +} diff --git a/start9/startos/manifest/index.ts b/start9/startos/manifest/index.ts new file mode 100644 index 0000000..276530b --- /dev/null +++ b/start9/startos/manifest/index.ts @@ -0,0 +1,61 @@ +import { setupManifest } from '@start9labs/start-sdk' +import { + alertInstall, + alertStart, + bitcoindDescription, + electrsDescription, + long, + short, +} from './i18n' + +export const manifest = setupManifest({ + id: 'broadcast-pool', + title: 'Broadcast Pool', + license: 'MIT', + packageRepo: 'https://github.com/criptoworld8484/broadcast-pool', + upstreamRepo: 'https://github.com/criptoworld8484/broadcast-pool', + marketingUrl: 'https://github.com/criptoworld8484/broadcast-pool', + donationUrl: null, + description: { short, long }, + volumes: ['main'], + images: { + 'broadcast-pool': { + source: { + dockerBuild: { + // The Rust binary is built from the repository root (Cargo.toml, src/), + // which lives one level above this StartOS package dir (start9/). So the + // build context is the repo root and the Dockerfile path is relative to it. + dockerfile: 'start9/Dockerfile', + workdir: '..', + }, + }, + arch: ['x86_64', 'aarch64'], + }, + }, + alerts: { + install: alertInstall, + update: null, + uninstall: null, + restore: null, + start: alertStart, + stop: null, + }, + dependencies: { + bitcoind: { + description: bitcoindDescription, + optional: false, + metadata: { + title: 'Bitcoin Core', + icon: 'https://raw.githubusercontent.com/Start9Labs/bitcoin-core-startos/refs/heads/30.x/dep-icon.svg', + }, + }, + electrs: { + description: electrsDescription, + optional: false, + metadata: { + title: 'Electrs', + icon: 'https://raw.githubusercontent.com/Start9Labs/electrs-startos/refs/heads/master/icon.svg', + }, + }, + }, +}) diff --git a/start9/startos/sdk.ts b/start9/startos/sdk.ts new file mode 100644 index 0000000..a2969a4 --- /dev/null +++ b/start9/startos/sdk.ts @@ -0,0 +1,7 @@ +import { StartSdk } from '@start9labs/start-sdk' +import { manifest } from './manifest' + +/** + * Plumbing. DO NOT EDIT. + */ +export const sdk = StartSdk.of().withManifest(manifest).build(true) diff --git a/start9/startos/utils.ts b/start9/startos/utils.ts new file mode 100644 index 0000000..6b0e588 --- /dev/null +++ b/start9/startos/utils.ts @@ -0,0 +1,4 @@ +// Electrum proxy port for Sparrow / Liana (plain TCP). +export const electrumPort = 50050 +// Web dashboard port. +export const webPort = 8080 diff --git a/start9/startos/versions/current.ts b/start9/startos/versions/current.ts new file mode 100644 index 0000000..f6cdebf --- /dev/null +++ b/start9/startos/versions/current.ts @@ -0,0 +1,13 @@ +import { IMPOSSIBLE, VersionInfo } from '@start9labs/start-sdk' + +export const current = VersionInfo.of({ + version: '0.3.11:1', + releaseNotes: { + en_US: 'Initial StartOS package: Electrum broadcast pool for Sparrow/Liana with virtual mempool and scheduled broadcasting. Network auto-detected from Bitcoin Core.', + es_ES: 'Paquete inicial para StartOS: pool de difusión Electrum para Sparrow/Liana con mempool virtual y difusión programada. Red autodetectada desde Bitcoin Core.', + }, + migrations: { + up: async () => {}, + down: IMPOSSIBLE, + }, +}) diff --git a/start9/startos/versions/index.ts b/start9/startos/versions/index.ts new file mode 100644 index 0000000..e596b0c --- /dev/null +++ b/start9/startos/versions/index.ts @@ -0,0 +1,7 @@ +import { VersionGraph } from '@start9labs/start-sdk' +import { current } from './current' + +export const versionGraph = VersionGraph.of({ + current, + other: [], +}) diff --git a/start9/tsconfig.json b/start9/tsconfig.json new file mode 100644 index 0000000..a2945a5 --- /dev/null +++ b/start9/tsconfig.json @@ -0,0 +1,11 @@ +{ + "include": ["startos/**/*.ts", "node_modules/**/startos"], + "compilerOptions": { + "target": "ES2018", + "module": "CommonJS", + "moduleResolution": "node", + "esModuleInterop": true, + "strict": true, + "skipLibCheck": true + } +} From 4c84801cee5ffa633db5f4cf72dacac72b82ec53 Mon Sep 17 00:00:00 2001 From: criptoworld Date: Fri, 26 Jun 2026 16:44:47 +0200 Subject: [PATCH 03/19] ci(start9): build ncc bundle before start-cli pack Co-Authored-By: Claude Opus 4.8 --- .github/workflows/build-startos-s9pk.yml | 3 +++ 1 file changed, 3 insertions(+) diff --git a/.github/workflows/build-startos-s9pk.yml b/.github/workflows/build-startos-s9pk.yml index 4bafde0..3108230 100644 --- a/.github/workflows/build-startos-s9pk.yml +++ b/.github/workflows/build-startos-s9pk.yml @@ -71,6 +71,9 @@ jobs: test $(stat -c%s icon.png) -le 40960 npm install npm run check + # Build the ncc bundle (javascript/index.js) BEFORE packing: start-cli + # s9pk pack/list-ingredients needs it to exist. + npm run build if [ -n "$DEV_KEY" ]; then mkdir -p "$HOME/.startos" printf '%s' "$DEV_KEY" > "$HOME/.startos/developer.key.pem" From 6cf80d055b33f1e3b2a8148703241c72418a9769 Mon Sep 17 00:00:00 2001 From: criptoworld Date: Fri, 26 Jun 2026 16:46:56 +0200 Subject: [PATCH 04/19] fix(start9): add LICENSE and instructions.md (required s9pk ingredients) Co-Authored-By: Claude Opus 4.8 --- start9/LICENSE | 21 +++++++++++++++++++++ start9/instructions.md | 29 +++++++++++++++++++++++++++++ 2 files changed, 50 insertions(+) create mode 100644 start9/LICENSE create mode 100644 start9/instructions.md diff --git a/start9/LICENSE b/start9/LICENSE new file mode 100644 index 0000000..6551de0 --- /dev/null +++ b/start9/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2026 criptoworld8484 + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/start9/instructions.md b/start9/instructions.md new file mode 100644 index 0000000..709cac5 --- /dev/null +++ b/start9/instructions.md @@ -0,0 +1,29 @@ +# Broadcast Pool + +Broadcast Pool sits between your wallet (Sparrow or Liana) and your node's +Electrum index. It intercepts transaction broadcasts and holds the signed +transactions in a **virtual mempool** until a chosen criterion is met +(immediate, scheduled date/time, fiat price, or block height). + +## Setup + +1. Make sure **Bitcoin Core** (pruning disabled) and **Electrs** are installed, + running, and synced. +2. Start Broadcast Pool. The network (mainnet, testnet4 or signet) is detected + automatically from Bitcoin Core. +3. Open the **Interfaces** section and copy the **Electrum (TCP)** LAN address. + +## Connecting a wallet + +- In **Sparrow** or **Liana**, set the Electrum server to the **Electrum (TCP)** + address shown in the Interfaces section (plain TCP, no SSL). Make sure the + wallet's network matches your node's network. +- Use the **Web Dashboard** interface to monitor pending transactions and to + schedule or trigger broadcasts. + +## Important + +A transaction shown as *pending* is **retained in the virtual mempool and has +NOT been broadcast to the network** until its trigger fires. Keep this service +and its data volume running so scheduled broadcasts are not lost. The signed +transaction hex is retained and can be re-broadcast manually if needed. From 35af3d19e33f7cec67b8026dcaef401450d310e0 Mon Sep 17 00:00:00 2001 From: criptoworld Date: Fri, 26 Jun 2026 16:49:23 +0200 Subject: [PATCH 05/19] fix(start9): add assets/ABOUT.md ingredient Co-Authored-By: Claude Opus 4.8 --- start9/assets/ABOUT.md | 9 +++++++++ 1 file changed, 9 insertions(+) create mode 100644 start9/assets/ABOUT.md diff --git a/start9/assets/ABOUT.md b/start9/assets/ABOUT.md new file mode 100644 index 0000000..253bb95 --- /dev/null +++ b/start9/assets/ABOUT.md @@ -0,0 +1,9 @@ +# Broadcast Pool + +Privacy-preserving Bitcoin broadcast scheduler for StartOS. Sits between your +wallet (Sparrow or Liana) and your node's Electrum index, holding signed +transactions in a virtual mempool until a chosen trigger fires (immediate, +scheduled time, fiat price, or block height). Network auto-detected from +Bitcoin Core (mainnet, testnet4, signet). + +Upstream: https://github.com/criptoworld8484/broadcast-pool From e4cdae026eaed4972e2e3d07bda964c1f49bd14c Mon Sep 17 00:00:00 2001 From: criptoworld Date: Fri, 26 Jun 2026 16:53:17 +0200 Subject: [PATCH 06/19] fix(start9): base image off published binary image, x86_64 v1 Resolves the s9pk ingredient path mismatch: the docker build context must be the package dir (start9/), which lacks the Rust source. Idiomatic StartOS packaging builds from a prebuilt artifact, so base the image off the already-published broadcast-pool-umbrel image and overlay the StartOS entrypoint/env. That image is linux/amd64 only, so v1 ships x86_64; aarch64 is a follow-up. Co-Authored-By: Claude Opus 4.8 --- .github/workflows/build-startos-s9pk.yml | 3 +- start9/Dockerfile | 45 ++++++++++-------------- start9/startos/manifest/index.ts | 12 +++---- 3 files changed, 25 insertions(+), 35 deletions(-) diff --git a/.github/workflows/build-startos-s9pk.yml b/.github/workflows/build-startos-s9pk.yml index 3108230..363b8d0 100644 --- a/.github/workflows/build-startos-s9pk.yml +++ b/.github/workflows/build-startos-s9pk.yml @@ -21,7 +21,7 @@ jobs: runs-on: ubuntu-latest strategy: matrix: - arch: [x86_64, aarch64] + arch: [x86_64] steps: - uses: actions/checkout@v4 @@ -104,5 +104,4 @@ jobs: with: files: | broadcast-pool_x86_64.s9pk - broadcast-pool_aarch64.s9pk generate_release_notes: true diff --git a/start9/Dockerfile b/start9/Dockerfile index be05b52..d37843a 100644 --- a/start9/Dockerfile +++ b/start9/Dockerfile @@ -1,34 +1,25 @@ # StartOS image for broadcast-pool. -# Built by `start-cli s9pk pack` with the manifest's dockerBuild workdir set to the -# REPOSITORY ROOT (start9/startos/manifest/index.ts -> source.dockerBuild.workdir = '..'), -# so all COPY paths below are root-relative — same convention as the Umbrel Dockerfile. -FROM rust:1.86-bookworm AS builder +# +# Idiomatic StartOS packaging builds the image from a prebuilt artifact rather than +# from repo source (the s9pk build context is this package dir, start9/, which does +# not contain the Rust crate). We reuse the already-published binary image as the base +# layer and only overlay the StartOS entrypoint + env. The base image carries the +# broadcast-pool binary (/usr/local/bin/broadcast-pool) and the dashboard. +# +# NOTE: the base image is currently linux/amd64 only, so this package is x86_64 only. +# Multi-arch (aarch64) is a follow-up: publish a multi-arch base image or per-arch +# release binaries and download the matching one here. +FROM ghcr.io/criptoworld8484/broadcast-pool-umbrel:0.3.11 -WORKDIR /build -COPY Cargo.toml Cargo.lock ./ -COPY src/ src/ +USER root +COPY --chmod=0755 entrypoint.sh /entrypoint.sh -RUN apt-get update && apt-get install -y pkg-config libssl-dev && rm -rf /var/lib/apt/lists/* -RUN cargo build --release && strip target/release/broadcast-pool - -FROM debian:bookworm-slim - -RUN apt-get update && apt-get install -y ca-certificates && rm -rf /var/lib/apt/lists/* - -COPY --from=builder /build/target/release/broadcast-pool /usr/local/bin/broadcast-pool -COPY src/api/dashboard.html /usr/share/broadcast-pool/dashboard.html -COPY start9/entrypoint.sh /entrypoint.sh -RUN chmod +x /entrypoint.sh - -# Static, non-network-specific defaults. The network is auto-detected from Bitcoin -# Core at runtime (see entrypoint.sh / discovery::apply_network_from_rpc), so we do -# NOT set BROADCAST_POOL_NETWORK here. -ENV BROADCAST_POOL_DASHBOARD=/usr/share/broadcast-pool/dashboard.html +# StartOS data volume + auto-detected network (do NOT pin BROADCAST_POOL_NETWORK). ENV BROADCAST_POOL_DATA_DIR=/data -ENV BROADCAST_POOL_WEB_HOST=0.0.0.0 -ENV BROADCAST_POOL_WEB_PORT=8080 -ENV BROADCAST_POOL_ELECTRUM_HOST=0.0.0.0 ENV BROADCAST_POOL_ELECTRUM_PORT=50050 +ENV BROADCAST_POOL_WEB_PORT=8080 + +# Run as the unprivileged uid baked into the base image (matches the StartOS convention). +USER app -EXPOSE 8080 50050 ENTRYPOINT ["/entrypoint.sh"] diff --git a/start9/startos/manifest/index.ts b/start9/startos/manifest/index.ts index 276530b..ab94b38 100644 --- a/start9/startos/manifest/index.ts +++ b/start9/startos/manifest/index.ts @@ -22,14 +22,14 @@ export const manifest = setupManifest({ 'broadcast-pool': { source: { dockerBuild: { - // The Rust binary is built from the repository root (Cargo.toml, src/), - // which lives one level above this StartOS package dir (start9/). So the - // build context is the repo root and the Dockerfile path is relative to it. - dockerfile: 'start9/Dockerfile', - workdir: '..', + // Build context is this package dir (start9/). The Dockerfile bases off the + // already-published binary image, so no Rust source is needed in the context. + dockerfile: 'Dockerfile', + workdir: '.', }, }, - arch: ['x86_64', 'aarch64'], + // x86_64 only for now: the base binary image is linux/amd64. aarch64 is a follow-up. + arch: ['x86_64'], }, }, alerts: { From 206bf2374a185c31a650a37638358cd2efea9c38 Mon Sep 17 00:00:00 2001 From: criptoworld Date: Fri, 26 Jun 2026 16:55:32 +0200 Subject: [PATCH 07/19] docs(start9): record base-image build approach + x86_64 v1 Co-Authored-By: Claude Opus 4.8 --- .../specs/2026-06-25-start9-adaptation-design.md | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/docs/superpowers/specs/2026-06-25-start9-adaptation-design.md b/docs/superpowers/specs/2026-06-25-start9-adaptation-design.md index d4150b7..00ca9e4 100644 --- a/docs/superpowers/specs/2026-06-25-start9-adaptation-design.md +++ b/docs/superpowers/specs/2026-06-25-start9-adaptation-design.md @@ -54,7 +54,11 @@ Estructura TS SDK (como frigate): daemon `entrypoint.sh`, health-check `checkPortListening(50050)`. - `start9/startos/utils.ts` — constantes de puerto (`electrumPort=50050`, `webPort=8080`). - `start9/startos/{index,versions/current,init,backups,actions,sdk,i18n}.ts` — plumbing estándar. -- `start9/Dockerfile` — build multi-stage del binario Rust (context = raíz del repo), arch x86_64+aarch64. +- `start9/Dockerfile` — **base de la imagen ya publicada** `broadcast-pool-umbrel:0.3.11` (que ya + contiene el binario + dashboard) + overlay del entrypoint/env de StartOS. Decisión: el contexto de + build del s9pk es el dir del paquete (`start9/`), que no contiene el crate Rust; empaquetar desde un + artefacto preconstruido es lo idiomático en StartOS (frigate descarga un binario). **v1 es x86_64** + (la imagen base es linux/amd64); aarch64 queda como follow-up (imagen multi-arch o binarios de release). - `start9/entrypoint.sh` — traduce StartOS → env vars del binario (ver mapeo). - `start9/{Makefile,s9pk.mk,package.json,tsconfig.json}` + `.github/workflows/build-s9pk.yml`. From 3ce5ea6e73fba887368eabdcf35726f5dfd6be81 Mon Sep 17 00:00:00 2001 From: criptoworld Date: Fri, 26 Jun 2026 17:20:10 +0200 Subject: [PATCH 08/19] fix(start9): depend on Fulcrum (the node's indexer), not electrs Validated on the live Start9 node (StartOS 0.4.0-beta.9, testnet4): the installed indexer is Fulcrum (Electrum TCP on fulcrum.startos:50001), not electrs, and Bitcoin Core RPC is normalized to bitcoind.startos:8332 on every network (so the testnet4 default-port concern is moot). Cookie is __cookie__:... at testnet4/.cookie under the bitcoind mount (handled by the entrypoint's recursive find). - manifest/dependencies: electrs -> fulcrum (health checks primary, sync-progress). - entrypoint: indexer default -> tcp://fulcrum.startos:50001. - drop the bitcoind autoconfig task (Fulcrum already requires an archival node). Co-Authored-By: Claude Opus 4.8 --- start9/entrypoint.sh | 5 ++++- start9/startos/dependencies.ts | 31 +++++++++---------------------- start9/startos/manifest/i18n.ts | 15 ++++++++------- start9/startos/manifest/index.ts | 10 +++++----- 4 files changed, 26 insertions(+), 35 deletions(-) diff --git a/start9/entrypoint.sh b/start9/entrypoint.sh index a0dd2a7..f3804eb 100644 --- a/start9/entrypoint.sh +++ b/start9/entrypoint.sh @@ -13,8 +13,11 @@ set -eu DATA_DIR="${BROADCAST_POOL_DATA_DIR:-/data}" BITCOIN_DATA_DIR="${BITCOIN_DATA_DIR:-/mnt/bitcoind}" +# StartOS normalizes the Bitcoin Core RPC port to 8332 on every network (verified: +# bitcoin.conf has "[testnet4] rpcbind=0.0.0.0:8332"), so this default is correct for +# mainnet/testnet4/signet. The indexer is Fulcrum (Electrum TCP on 50001). BITCOIN_RPC_URL="${BROADCAST_POOL_RPC_URL:-http://bitcoind.startos:8332}" -ELECTRS_URL="${BROADCAST_POOL_INDEXER_URL:-tcp://electrs.startos:50001}" +ELECTRS_URL="${BROADCAST_POOL_INDEXER_URL:-tcp://fulcrum.startos:50001}" BOOT_LOG="${DATA_DIR}/startos-boot.log" mkdir -p "${DATA_DIR}" diff --git a/start9/startos/dependencies.ts b/start9/startos/dependencies.ts index 610c509..2e881b2 100644 --- a/start9/startos/dependencies.ts +++ b/start9/startos/dependencies.ts @@ -1,34 +1,21 @@ -import { autoconfig } from 'bitcoin-core-startos/startos/actions/config/autoconfig' -import { i18n } from './i18n' import { sdk } from './sdk' -export const setDependencies = sdk.setupDependencies(async ({ effects }) => { - // Broadcast Pool only needs RPC (genesis/network/height) + a working Electrs, - // so the single hard requirement on bitcoind is that pruning is disabled - // (Electrs and prev-tx lookups need full block data). - await sdk.action.createTask(effects, 'bitcoind', autoconfig, 'critical', { - input: { - kind: 'partial', - value: { - prune: 0, - }, - }, - reason: i18n( - 'Pruning must be disabled for Electrs and transaction lookups to work.', - ), - when: { condition: 'input-not-matches', once: false }, - }) - +// Broadcast Pool needs Bitcoin Core RPC (genesis / network detection / block height, +// via the .cookie) and a working Electrum indexer. This node ships Fulcrum, which is +// Electrum-protocol compatible (the binary works with electrs or Fulcrum). Health-check +// ids come from the installed packages: bitcoind -> [bitcoind, sync-progress]; +// fulcrum -> [primary, sync-progress]. +export const setDependencies = sdk.setupDependencies(async () => { return { bitcoind: { kind: 'running', versionRange: '>=28.0:0', healthChecks: ['bitcoind', 'sync-progress'], }, - electrs: { + fulcrum: { kind: 'running', - versionRange: '>=0.10:0', - healthChecks: ['electrs', 'sync'], + versionRange: '>=1.9:0', + healthChecks: ['primary', 'sync-progress'], }, } }) diff --git a/start9/startos/manifest/i18n.ts b/start9/startos/manifest/i18n.ts index f9a1d4c..046aedd 100644 --- a/start9/startos/manifest/i18n.ts +++ b/start9/startos/manifest/i18n.ts @@ -5,16 +5,16 @@ export const short = { export const long = { en_US: - 'Broadcast Pool sits between your wallet (Sparrow or Liana) and your node\'s Electrum index. It intercepts transaction broadcasts and holds the signed transactions in a virtual mempool until a chosen criterion is met (immediate, scheduled date/time, fiat price, or block height). It connects automatically to Bitcoin Core and Electrs on StartOS and exposes a plain TCP Electrum interface plus a web dashboard. The network (mainnet, testnet4 or signet) is detected automatically from Bitcoin Core.', + 'Broadcast Pool sits between your wallet (Sparrow or Liana) and your node\'s Electrum index. It intercepts transaction broadcasts and holds the signed transactions in a virtual mempool until a chosen criterion is met (immediate, scheduled date/time, fiat price, or block height). It connects automatically to Bitcoin Core and Fulcrum on StartOS and exposes a plain TCP Electrum interface plus a web dashboard. The network (mainnet, testnet4 or signet) is detected automatically from Bitcoin Core.', es_ES: - 'Broadcast Pool se sitúa entre tu billetera (Sparrow o Liana) y el índice Electrum de tu nodo. Intercepta las difusiones de transacciones y retiene las transacciones firmadas en una mempool virtual hasta que se cumple el criterio elegido (inmediato, fecha/hora programada, precio fiat o altura de bloque). Se conecta automáticamente a Bitcoin Core y Electrs en StartOS y expone una interfaz Electrum TCP y un panel web. La red (mainnet, testnet4 o signet) se detecta automáticamente desde Bitcoin Core.', + 'Broadcast Pool se sitúa entre tu billetera (Sparrow o Liana) y el índice Electrum de tu nodo. Intercepta las difusiones de transacciones y retiene las transacciones firmadas en una mempool virtual hasta que se cumple el criterio elegido (inmediato, fecha/hora programada, precio fiat o altura de bloque). Se conecta automáticamente a Bitcoin Core y Fulcrum en StartOS y expone una interfaz Electrum TCP y un panel web. La red (mainnet, testnet4 o signet) se detecta automáticamente desde Bitcoin Core.', } export const alertInstall = { en_US: - 'Broadcast Pool requires Bitcoin Core (pruning disabled) and Electrs. A transaction shown as pending is retained in the virtual mempool and is NOT yet broadcast to the network until its trigger fires.', + 'Broadcast Pool requires Bitcoin Core (pruning disabled) and Fulcrum. A transaction shown as pending is retained in the virtual mempool and is NOT yet broadcast to the network until its trigger fires.', es_ES: - 'Broadcast Pool requiere Bitcoin Core (sin poda) y Electrs. Una transacción marcada como pendiente se retiene en la mempool virtual y NO se difunde a la red hasta que se dispara su criterio.', + 'Broadcast Pool requiere Bitcoin Core (sin poda) y Fulcrum. Una transacción marcada como pendiente se retiene en la mempool virtual y NO se difunde a la red hasta que se dispara su criterio.', } export const alertStart = { @@ -29,8 +29,9 @@ export const bitcoindDescription = { es_ES: 'Proporciona datos de blockchain, RPC y autenticación por cookie', } -export const electrsDescription = { - en_US: 'Provides the Electrum index backend for address and history lookups', +export const fulcrumDescription = { + en_US: + 'Provides the Electrum index backend (Fulcrum) for address and history lookups', es_ES: - 'Proporciona el backend de índice Electrum para direcciones e historial', + 'Proporciona el backend de índice Electrum (Fulcrum) para direcciones e historial', } diff --git a/start9/startos/manifest/index.ts b/start9/startos/manifest/index.ts index ab94b38..832986e 100644 --- a/start9/startos/manifest/index.ts +++ b/start9/startos/manifest/index.ts @@ -3,7 +3,7 @@ import { alertInstall, alertStart, bitcoindDescription, - electrsDescription, + fulcrumDescription, long, short, } from './i18n' @@ -49,12 +49,12 @@ export const manifest = setupManifest({ icon: 'https://raw.githubusercontent.com/Start9Labs/bitcoin-core-startos/refs/heads/30.x/dep-icon.svg', }, }, - electrs: { - description: electrsDescription, + fulcrum: { + description: fulcrumDescription, optional: false, metadata: { - title: 'Electrs', - icon: 'https://raw.githubusercontent.com/Start9Labs/electrs-startos/refs/heads/master/icon.svg', + title: 'Fulcrum', + icon: 'https://raw.githubusercontent.com/Start9Labs/fulcrum-startos/refs/heads/master/icon.png', }, }, }, From ad7d57a45c7e5387851c2142cd8224a5442bb9c1 Mon Sep 17 00:00:00 2001 From: criptoworld Date: Fri, 26 Jun 2026 17:26:22 +0200 Subject: [PATCH 09/19] fix(start9): run as root (StartOS mounts /data root-owned) The live node showed 'cannot create /data/startos-boot.log: Permission denied': StartOS 0.4 runs service subcontainers as root with a root-owned data volume, but the base image trailing USER app (uid 1000) couldn't write /data. Run as root. Co-Authored-By: Claude Opus 4.8 --- start9/Dockerfile | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/start9/Dockerfile b/start9/Dockerfile index d37843a..a4e3a9a 100644 --- a/start9/Dockerfile +++ b/start9/Dockerfile @@ -19,7 +19,8 @@ ENV BROADCAST_POOL_DATA_DIR=/data ENV BROADCAST_POOL_ELECTRUM_PORT=50050 ENV BROADCAST_POOL_WEB_PORT=8080 -# Run as the unprivileged uid baked into the base image (matches the StartOS convention). -USER app +# StartOS runs service subcontainers as root and mounts /data owned by root, so run +# as root (the base image's trailing `USER app` would hit EPERM writing to /data). +USER root ENTRYPOINT ["/entrypoint.sh"] From 3eab062cad07f96ae9f9647a939b0beca9ea5282 Mon Sep 17 00:00:00 2001 From: criptoworld Date: Fri, 26 Jun 2026 17:36:32 +0200 Subject: [PATCH 10/19] fix(indexer): resolve hostname indexer addresses via DNS (v0.3.12) connect_stream/fetch_prev_tx/forward_to_indexer_sync used str::parse::, which only accepts literal IPs. Umbrel passes an IP (10.21.21.10) so it worked, but StartOS passes a hostname (fulcrum.startos:50001) and parse() failed with 'Invalid indexer address' -> the binary reported 'tried TCP and SSL' and never connected to the indexer, even though it was reachable. Add resolve_socket_addr() using to_socket_addrs() (handles hostnames AND IPs) and use it in all three connect sites. Found on the live Start9 node (testnet4, Fulcrum at fulcrum.startos:50001). Co-Authored-By: Claude Opus 4.8 --- Cargo.lock | 2 +- Cargo.toml | 2 +- src/electrum_server/mod.rs | 2 +- src/pool/virtual_mempool.rs | 2 +- src/rpc/indexer_transport.rs | 42 +++++++++++++++++++++++++++++++++++- 5 files changed, 45 insertions(+), 5 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 5c6dd0a..1df6e55 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -294,7 +294,7 @@ checksum = "c4512299f36f043ab09a583e57bceb5a5aab7a73db1805848e8fef3c9e8c78b3" [[package]] name = "broadcast-pool" -version = "0.3.11" +version = "0.3.12" dependencies = [ "anyhow", "axum", diff --git a/Cargo.toml b/Cargo.toml index ab07dc0..a8a0ddc 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "broadcast-pool" -version = "0.3.11" +version = "0.3.12" edition = "2021" description = "Bitcoin Broadcast Pool - Privacy-preserving transaction scheduling and migration tool" license = "MIT" diff --git a/src/electrum_server/mod.rs b/src/electrum_server/mod.rs index 44257b3..0788c9d 100644 --- a/src/electrum_server/mod.rs +++ b/src/electrum_server/mod.rs @@ -105,7 +105,7 @@ fn forward_to_indexer_sync(request_str: &str, indexer_addr: &str) -> Option s, diff --git a/src/pool/virtual_mempool.rs b/src/pool/virtual_mempool.rs index 0f9e71f..7b9802a 100644 --- a/src/pool/virtual_mempool.rs +++ b/src/pool/virtual_mempool.rs @@ -217,7 +217,7 @@ fn fetch_prev_tx(txid_str: &str, indexer_addr: &str) -> Result { let addr = strip_indexer_host(indexer_addr); let mut stream = std::net::TcpStream::connect_timeout( - &addr.parse().context("Invalid indexer address")?, + &crate::rpc::indexer_transport::resolve_socket_addr(&addr)?, std::time::Duration::from_secs(5), ) .context("Failed to connect to indexer")?; diff --git a/src/rpc/indexer_transport.rs b/src/rpc/indexer_transport.rs index 4699403..0aa2649 100644 --- a/src/rpc/indexer_transport.rs +++ b/src/rpc/indexer_transport.rs @@ -127,11 +127,24 @@ fn connect_timeout_secs() -> u64 { } } +/// Resolve a `host:port` string (hostname OR literal IP) to a SocketAddr. +/// +/// `str::parse::()` only accepts numeric IPs, so it works for Umbrel +/// (IP indexer like 10.21.21.10) but fails for hostname-based indexers such as +/// StartOS `fulcrum.startos:50001`. Use DNS resolution so both work. +pub fn resolve_socket_addr(addr: &str) -> Result { + use std::net::ToSocketAddrs; + addr.to_socket_addrs() + .with_context(|| format!("Could not resolve indexer address ({})", addr))? + .next() + .with_context(|| format!("No address resolved for indexer ({})", addr)) +} + fn connect_stream(url: &str) -> Result { let use_ssl = url.starts_with("ssl://"); let addr = strip_scheme(url); let tcp = TcpStream::connect_timeout( - &addr.parse().context("Invalid indexer address")?, + &resolve_socket_addr(addr)?, Duration::from_secs(connect_timeout_secs()), ) .with_context(|| format!("TCP connect failed ({})", addr))?; @@ -220,3 +233,30 @@ pub fn probe_working_url(candidates: &[String]) -> Option { } None } + +#[cfg(test)] +mod tests { + use super::resolve_socket_addr; + + // resolve_socket_addr must accept both literal IPs (Umbrel) and hostnames + // (StartOS fulcrum.startos). The previous str::parse:: only handled IPs. + #[test] + fn resolves_literal_ip() { + let a = resolve_socket_addr("127.0.0.1:50001").expect("literal ip"); + assert_eq!(a.port(), 50001); + assert!(a.ip().is_loopback()); + } + + #[test] + fn resolves_localhost_hostname() { + // `localhost` is the portable hostname guaranteed to resolve in CI. + let a = resolve_socket_addr("localhost:50001").expect("hostname resolves"); + assert_eq!(a.port(), 50001); + } + + #[test] + fn literal_socketaddr_parse_rejects_hostname() { + // Documents the original bug: SocketAddr::parse cannot handle hostnames. + assert!("localhost:50001".parse::().is_err()); + } +} From c7e1ce1c213784b32401fecfdef48a0998ba0500 Mon Sep 17 00:00:00 2001 From: criptoworld Date: Fri, 26 Jun 2026 17:40:06 +0200 Subject: [PATCH 11/19] chore(start9): base off broadcast-pool-umbrel:0.3.12 (hostname indexer fix) Co-Authored-By: Claude Opus 4.8 --- start9/Dockerfile | 2 +- start9/startos/versions/current.ts | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/start9/Dockerfile b/start9/Dockerfile index a4e3a9a..79f3756 100644 --- a/start9/Dockerfile +++ b/start9/Dockerfile @@ -9,7 +9,7 @@ # NOTE: the base image is currently linux/amd64 only, so this package is x86_64 only. # Multi-arch (aarch64) is a follow-up: publish a multi-arch base image or per-arch # release binaries and download the matching one here. -FROM ghcr.io/criptoworld8484/broadcast-pool-umbrel:0.3.11 +FROM ghcr.io/criptoworld8484/broadcast-pool-umbrel:0.3.12 USER root COPY --chmod=0755 entrypoint.sh /entrypoint.sh diff --git a/start9/startos/versions/current.ts b/start9/startos/versions/current.ts index f6cdebf..e6b7902 100644 --- a/start9/startos/versions/current.ts +++ b/start9/startos/versions/current.ts @@ -1,7 +1,7 @@ import { IMPOSSIBLE, VersionInfo } from '@start9labs/start-sdk' export const current = VersionInfo.of({ - version: '0.3.11:1', + version: '0.3.12:1', releaseNotes: { en_US: 'Initial StartOS package: Electrum broadcast pool for Sparrow/Liana with virtual mempool and scheduled broadcasting. Network auto-detected from Bitcoin Core.', es_ES: 'Paquete inicial para StartOS: pool de difusión Electrum para Sparrow/Liana con mempool virtual y difusión programada. Red autodetectada desde Bitcoin Core.', From ade07fdaf25ed6f0fd86db27e79d81013fb787c8 Mon Sep 17 00:00:00 2001 From: criptoworld Date: Sat, 27 Jun 2026 09:20:53 +0200 Subject: [PATCH 12/19] fix(dashboard): correct wallet URL on StartOS / non-Umbrel (v0.3.13) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The dashboard showed the StartOS overlay IP (10.0.3.x) as the wallet URL — useless for Sparrow/Liana — and the warning text was hardcoded to 'Umbrel'. - discovery: exclude the StartOS overlay (10.0.3.x) from detect_lan_ip (it already excluded Umbrel/docker ranges) so a bogus container IP is never shown; also apply the exclusion to the 'ip route get' path. Add is_startos_mode() (reads BROADCAST_POOL_PLATFORM=startos, set by the StartOS entrypoint). - api: expose startos_mode in the config response. - dashboard: generic wallet warning (node LAN IP, electrs/Fulcrum) instead of Umbrel; on StartOS show guidance to use the Electrum (TCP) address from the service's Interfaces page rather than a (non-routable) container IP. - start9 entrypoint: export BROADCAST_POOL_PLATFORM=startos. Co-Authored-By: Claude Opus 4.8 --- Cargo.lock | 2 +- Cargo.toml | 2 +- src/api/dashboard.html | 18 +++++++++++++----- src/api/mod.rs | 2 ++ src/discovery.rs | 32 +++++++++++++++++++++++++++++--- start9/entrypoint.sh | 3 +++ 6 files changed, 49 insertions(+), 10 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 1df6e55..dd6b54f 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -294,7 +294,7 @@ checksum = "c4512299f36f043ab09a583e57bceb5a5aab7a73db1805848e8fef3c9e8c78b3" [[package]] name = "broadcast-pool" -version = "0.3.12" +version = "0.3.13" dependencies = [ "anyhow", "axum", diff --git a/Cargo.toml b/Cargo.toml index a8a0ddc..415b0a5 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "broadcast-pool" -version = "0.3.12" +version = "0.3.13" edition = "2021" description = "Bitcoin Broadcast Pool - Privacy-preserving transaction scheduling and migration tool" license = "MIT" diff --git a/src/api/dashboard.html b/src/api/dashboard.html index 59286c0..f9186dc 100644 --- a/src/api/dashboard.html +++ b/src/api/dashboard.html @@ -892,7 +892,7 @@

Configuration

Configure Sparrow or Liana to connect here (Electrum server)
-
-
Use your Umbrel LAN IP (same as in the browser). Do not use the electrs indexer.
+
Use your node’s LAN IP (the same host you use in the browser). Do not use the electrs/Fulcrum indexer.
Use this URL in your wallet server settings
@@ -1216,7 +1216,7 @@

Settings

cfg_min_delay: 'Min Delay (hours)', cfg_min_delay_hint: 'Minimum random delay before broadcast', cfg_max_delay: 'Max Delay (hours)', cfg_max_delay_hint: 'Maximum random delay before broadcast', cfg_reset: 'Reset', cfg_save: 'Save Settings', - cfg_wallet_title: 'Wallet Connection', cfg_wallet_hint: 'Configure your wallet to connect to Broadcast Pool here', cfg_wallet_url: 'Use this URL in your wallet server settings', cfg_wallet_warning: 'Use your Umbrel LAN IP (same as in the browser). Do not use the electrs indexer. In Sparrow: Settings → Network → disable Tor proxy or broadcasts bypass this pool.', + cfg_wallet_title: 'Wallet Connection', cfg_wallet_hint: 'Configure your wallet to connect to Broadcast Pool here', cfg_wallet_url: 'Use this URL in your wallet server settings', cfg_wallet_warning: 'Use your node’s LAN IP (the same host you use in the browser). Do not use the electrs/Fulcrum indexer. In Sparrow: Settings → Network → disable Tor proxy or broadcasts bypass this pool.', cfg_wallet_startos: 'On StartOS, connect your wallet using the Electrum (TCP) address shown in this service’s Interfaces page — not an internal IP.', detail_title: 'Transaction Details', detail_pool_id: 'Pool ID', detail_status: 'Status', detail_network: 'Network', detail_scheduled: 'Scheduled', detail_fee_rate: 'Fee Rate', detail_utxos: 'UTXOs', detail_broadcast_at: 'Broadcast At', detail_confirmed_at: 'Confirmed At', @@ -1326,7 +1326,7 @@

Settings

cfg_min_delay: 'Retraso M\u00ednimo (horas)', cfg_min_delay_hint: 'Retraso aleatorio m\u00ednimo antes de transmisi\u00f3n', cfg_max_delay: 'Retraso M\u00e1ximo (horas)', cfg_max_delay_hint: 'Retraso aleatorio m\u00e1ximo antes de transmisi\u00f3n', cfg_reset: 'Restablecer', cfg_save: 'Guardar Configuraci\u00f3n', - cfg_wallet_title: 'Conexi\u00f3n Wallet', cfg_wallet_hint: 'Configura tu wallet para conectarse aqu\u00ed', cfg_wallet_url: 'Usar esta URL en la configuraci\u00f3n del servidor de tu wallet', cfg_wallet_warning: 'Usa la IP LAN de Umbrel (la misma del navegador). No uses el indexer electrs. En Sparrow: Ajustes \u2192 Red \u2192 desactiva el proxy Tor o los env\u00edos no llegan al pool.', + cfg_wallet_title: 'Conexi\u00f3n Wallet', cfg_wallet_hint: 'Configura tu wallet para conectarse aqu\u00ed', cfg_wallet_url: 'Usar esta URL en la configuraci\u00f3n del servidor de tu wallet', cfg_wallet_warning: 'Usa la IP LAN de tu nodo (el mismo host que usas en el navegador). No uses el indexer electrs/Fulcrum. En Sparrow: Ajustes \u2192 Red \u2192 desactiva el proxy Tor o los env\u00edos no llegan al pool.', cfg_wallet_startos: 'En StartOS, conecta tu wallet con la direcci\u00f3n Electrum (TCP) que se muestra en la p\u00e1gina de Interfaces de este servicio \u2014 no una IP interna.', detail_title: 'Detalles de Transacci\u00f3n', detail_pool_id: 'ID del Pool', detail_status: 'Estado', detail_network: 'Red', detail_scheduled: 'Programada', detail_fee_rate: 'Tarifa', detail_utxos: 'UTXOs', detail_broadcast_at: 'Transmitida el', detail_confirmed_at: 'Confirmada el', @@ -2230,9 +2230,17 @@

Settings

} function updateWalletUrls(cfg) { - const walletUrl = cfg.wallet_connect_url || cfg.sparrow_connect_url; const walletEl = document.getElementById('wallet-url'); - if (walletEl && walletUrl) walletEl.textContent = walletUrl; + if (walletEl) { + if (cfg.startos_mode) { + // On StartOS the wallet address comes from the service's Interfaces page, + // not an auto-detected container IP. Show guidance instead of a bogus URL. + walletEl.textContent = t('cfg_wallet_startos'); + } else { + const walletUrl = cfg.wallet_connect_url || cfg.sparrow_connect_url; + if (walletUrl) walletEl.textContent = walletUrl; + } + } appUmbrelMode = !!cfg.umbrel_mode; const externalGroup = document.getElementById('cfg-external-indexer-group'); diff --git a/src/api/mod.rs b/src/api/mod.rs index 6c57823..7c3120a 100644 --- a/src/api/mod.rs +++ b/src/api/mod.rs @@ -121,6 +121,7 @@ fn config_to_response(config: &Config, network_changed: bool) -> ConfigResponse indexer_is_manual: is_manual, network_editable: !umbrel, umbrel_mode: umbrel, + startos_mode: crate::discovery::is_startos_mode(), network: config.network.network_type.data_dir_name().to_string(), broadcast_mode: config.schedule.broadcast_mode.to_string(), default_delay_hours: config.schedule.default_delay_hours, @@ -409,6 +410,7 @@ struct ConfigResponse { indexer_is_manual: bool, network_editable: bool, umbrel_mode: bool, + startos_mode: bool, network: String, broadcast_mode: String, default_delay_hours: u64, diff --git a/src/discovery.rs b/src/discovery.rs index 95502c5..dae36af 100644 --- a/src/discovery.rs +++ b/src/discovery.rs @@ -64,7 +64,7 @@ pub fn detect_lan_ip() -> Option { { if out.status.success() { let ip = String::from_utf8_lossy(&out.stdout).trim().to_string(); - if is_plausible_lan_ip(&ip) { + if is_plausible_lan_ip(&ip) && !is_likely_docker_bridge(&ip) { return Some(ip); } } @@ -88,9 +88,24 @@ fn is_plausible_lan_ip(ip: &str) -> bool { !ip.starts_with("127.") && !ip.starts_with("0.") && ip.contains('.') } -/// Umbrel / Docker internal subnets — not useful for Sparrow on the LAN. +/// Container/overlay subnets that are NOT reachable by wallets on the physical LAN: +/// Umbrel Docker (10.21.x), default Docker bridges (172.17/18.x), and the StartOS +/// service overlay (10.0.3.x). Detecting one of these as the "LAN IP" would show a +/// useless wallet URL, so they are excluded. fn is_likely_docker_bridge(ip: &str) -> bool { - ip.starts_with("10.21.") || ip.starts_with("172.17.") || ip.starts_with("172.18.") + ip.starts_with("10.21.") + || ip.starts_with("172.17.") + || ip.starts_with("172.18.") + || ip.starts_with("10.0.3.") +} + +/// True when running as a StartOS service. The StartOS entrypoint sets +/// `BROADCAST_POOL_PLATFORM=startos`. On StartOS the wallet connects via the address +/// shown in the service's Interfaces page, not an auto-detected container IP. +pub fn is_startos_mode() -> bool { + std::env::var("BROADCAST_POOL_PLATFORM") + .map(|v| v.eq_ignore_ascii_case("startos")) + .unwrap_or(false) } pub fn resolve_lan_host(config: &Config) -> Option { @@ -1129,6 +1144,17 @@ pub fn save_config_to_disk(config: &Config) -> anyhow::Result<()> { mod tests { use super::*; + // Container/overlay IPs must NOT be offered as a wallet LAN IP. The StartOS overlay + // (10.0.3.x) was being shown in the dashboard as the (useless) wallet URL. + #[test] + fn overlay_ips_are_not_lan() { + assert!(is_likely_docker_bridge("10.0.3.72"), "StartOS overlay"); + assert!(is_likely_docker_bridge("10.21.21.10"), "Umbrel docker"); + assert!(is_likely_docker_bridge("172.17.0.2"), "docker bridge"); + assert!(!is_likely_docker_bridge("192.168.50.134"), "real LAN"); + assert!(!is_likely_docker_bridge("10.0.0.5"), "non-overlay private"); + } + #[test] fn maps_bitcoin_chains() { assert_eq!(network_from_bitcoin_chain("main"), NetworkType::Mainnet); diff --git a/start9/entrypoint.sh b/start9/entrypoint.sh index f3804eb..64719d4 100644 --- a/start9/entrypoint.sh +++ b/start9/entrypoint.sh @@ -77,6 +77,9 @@ export BROADCAST_POOL_ELECTRUM_HOST="${BROADCAST_POOL_ELECTRUM_HOST:-0.0.0.0}" export BROADCAST_POOL_ELECTRUM_PORT="${BROADCAST_POOL_ELECTRUM_PORT:-50050}" export BROADCAST_POOL_WEB_HOST="${BROADCAST_POOL_WEB_HOST:-0.0.0.0}" export BROADCAST_POOL_WEB_PORT="${BROADCAST_POOL_WEB_PORT:-8080}" +# Platform hint: the dashboard can't auto-detect a LAN IP here (the container only sees +# the StartOS overlay), so it directs the user to the service's Interfaces page instead. +export BROADCAST_POOL_PLATFORM="startos" # Deliberately NOT set: BROADCAST_POOL_NETWORK (auto-detected), BROADCAST_POOL_UMBREL. log "Starting broadcast-pool (network auto-detected from Bitcoin Core)" From d0fb12dce34962203c7a5d53a7caec38292eff98 Mon Sep 17 00:00:00 2001 From: criptoworld Date: Sat, 27 Jun 2026 09:24:39 +0200 Subject: [PATCH 13/19] chore(start9): base off broadcast-pool-umbrel:0.3.13 (dashboard wallet URL fix) Co-Authored-By: Claude Opus 4.8 --- start9/Dockerfile | 2 +- start9/startos/versions/current.ts | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/start9/Dockerfile b/start9/Dockerfile index 79f3756..9fa0928 100644 --- a/start9/Dockerfile +++ b/start9/Dockerfile @@ -9,7 +9,7 @@ # NOTE: the base image is currently linux/amd64 only, so this package is x86_64 only. # Multi-arch (aarch64) is a follow-up: publish a multi-arch base image or per-arch # release binaries and download the matching one here. -FROM ghcr.io/criptoworld8484/broadcast-pool-umbrel:0.3.12 +FROM ghcr.io/criptoworld8484/broadcast-pool-umbrel:0.3.13 USER root COPY --chmod=0755 entrypoint.sh /entrypoint.sh diff --git a/start9/startos/versions/current.ts b/start9/startos/versions/current.ts index e6b7902..5027b39 100644 --- a/start9/startos/versions/current.ts +++ b/start9/startos/versions/current.ts @@ -1,7 +1,7 @@ import { IMPOSSIBLE, VersionInfo } from '@start9labs/start-sdk' export const current = VersionInfo.of({ - version: '0.3.12:1', + version: '0.3.13:1', releaseNotes: { en_US: 'Initial StartOS package: Electrum broadcast pool for Sparrow/Liana with virtual mempool and scheduled broadcasting. Network auto-detected from Bitcoin Core.', es_ES: 'Paquete inicial para StartOS: pool de difusión Electrum para Sparrow/Liana con mempool virtual y difusión programada. Red autodetectada desde Bitcoin Core.', From c907363d50072d567058656869fa3977e17137f7 Mon Sep 17 00:00:00 2001 From: criptoworld Date: Sat, 27 Jun 2026 09:28:45 +0200 Subject: [PATCH 14/19] fix(dashboard): ignore stale overlay IP persisted in lan_connect_host (v0.3.14) An earlier version persisted the StartOS overlay IP (10.0.3.x) into config.toml's lan_connect_host, so resolve_lan_host kept returning it even after detect_lan_ip was fixed. Reject overlay/bridge IPs when reading the persisted host too. Co-Authored-By: Claude Opus 4.8 --- Cargo.lock | 2 +- Cargo.toml | 2 +- src/discovery.rs | 4 +++- 3 files changed, 5 insertions(+), 3 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index dd6b54f..9e3d203 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -294,7 +294,7 @@ checksum = "c4512299f36f043ab09a583e57bceb5a5aab7a73db1805848e8fef3c9e8c78b3" [[package]] name = "broadcast-pool" -version = "0.3.13" +version = "0.3.14" dependencies = [ "anyhow", "axum", diff --git a/Cargo.toml b/Cargo.toml index 415b0a5..42d9b65 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "broadcast-pool" -version = "0.3.13" +version = "0.3.14" edition = "2021" description = "Bitcoin Broadcast Pool - Privacy-preserving transaction scheduling and migration tool" license = "MIT" diff --git a/src/discovery.rs b/src/discovery.rs index dae36af..2ce28a2 100644 --- a/src/discovery.rs +++ b/src/discovery.rs @@ -111,7 +111,9 @@ pub fn is_startos_mode() -> bool { pub fn resolve_lan_host(config: &Config) -> Option { if let Some(ref h) = config.electrum_server.lan_connect_host { let h = h.trim(); - if !h.is_empty() { + // Ignore a stale container/overlay IP persisted by an earlier version (e.g. the + // StartOS overlay 10.0.3.x) — it is not reachable by wallets on the LAN. + if !h.is_empty() && !is_likely_docker_bridge(h) { return Some(h.to_string()); } } From 5c9e68d7f638327d05aadafc8f399acc13921585 Mon Sep 17 00:00:00 2001 From: criptoworld Date: Sat, 27 Jun 2026 09:32:01 +0200 Subject: [PATCH 15/19] chore(start9): base off broadcast-pool-umbrel:0.3.14 Co-Authored-By: Claude Opus 4.8 --- start9/Dockerfile | 2 +- start9/startos/versions/current.ts | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/start9/Dockerfile b/start9/Dockerfile index 9fa0928..b7d28a1 100644 --- a/start9/Dockerfile +++ b/start9/Dockerfile @@ -9,7 +9,7 @@ # NOTE: the base image is currently linux/amd64 only, so this package is x86_64 only. # Multi-arch (aarch64) is a follow-up: publish a multi-arch base image or per-arch # release binaries and download the matching one here. -FROM ghcr.io/criptoworld8484/broadcast-pool-umbrel:0.3.13 +FROM ghcr.io/criptoworld8484/broadcast-pool-umbrel:0.3.14 USER root COPY --chmod=0755 entrypoint.sh /entrypoint.sh diff --git a/start9/startos/versions/current.ts b/start9/startos/versions/current.ts index 5027b39..669bc40 100644 --- a/start9/startos/versions/current.ts +++ b/start9/startos/versions/current.ts @@ -1,7 +1,7 @@ import { IMPOSSIBLE, VersionInfo } from '@start9labs/start-sdk' export const current = VersionInfo.of({ - version: '0.3.13:1', + version: '0.3.14:1', releaseNotes: { en_US: 'Initial StartOS package: Electrum broadcast pool for Sparrow/Liana with virtual mempool and scheduled broadcasting. Network auto-detected from Bitcoin Core.', es_ES: 'Paquete inicial para StartOS: pool de difusión Electrum para Sparrow/Liana con mempool virtual y difusión programada. Red autodetectada desde Bitcoin Core.', From 8345b6db8b44b7ab61af9fcd4f4aa33e37cbdca7 Mon Sep 17 00:00:00 2001 From: criptoworld Date: Sat, 27 Jun 2026 10:41:25 +0200 Subject: [PATCH 16/19] feat(start9): dedicated Liana Electrum interface (port 50051) Liana always sets a block-height nLockTime (anti-fee-sniping; not disableable like Sparrow's MTP), so a tx sent to the shared Sparrow port (50050) was categorized 'by_block'. The binary already has a dedicated 'liana' listener (source=liana -> manual/pending); expose it as a separate StartOS Electrum interface on 50051 and set BROADCAST_POOL_LIANA_ELECTRUM_PORT in the entrypoint. Liana points at this port so its txs arrive pending and can be scheduled by date/time from the dashboard. Package-only change (binary unchanged, still :0.3.14); s9pk revision 0.3.14:2. Co-Authored-By: Claude Opus 4.8 --- start9/Dockerfile | 3 ++ start9/entrypoint.sh | 5 ++++ start9/startos/i18n/dictionaries/default.ts | 4 ++- .../startos/i18n/dictionaries/translations.ts | 4 ++- start9/startos/interfaces.ts | 29 +++++++++++++++++-- start9/startos/utils.ts | 6 +++- start9/startos/versions/current.ts | 6 ++-- 7 files changed, 48 insertions(+), 9 deletions(-) diff --git a/start9/Dockerfile b/start9/Dockerfile index b7d28a1..3f1fe9d 100644 --- a/start9/Dockerfile +++ b/start9/Dockerfile @@ -17,8 +17,11 @@ COPY --chmod=0755 entrypoint.sh /entrypoint.sh # StartOS data volume + auto-detected network (do NOT pin BROADCAST_POOL_NETWORK). ENV BROADCAST_POOL_DATA_DIR=/data ENV BROADCAST_POOL_ELECTRUM_PORT=50050 +ENV BROADCAST_POOL_LIANA_ELECTRUM_PORT=50051 ENV BROADCAST_POOL_WEB_PORT=8080 +EXPOSE 50051 + # StartOS runs service subcontainers as root and mounts /data owned by root, so run # as root (the base image's trailing `USER app` would hit EPERM writing to /data). USER root diff --git a/start9/entrypoint.sh b/start9/entrypoint.sh index 64719d4..b672852 100644 --- a/start9/entrypoint.sh +++ b/start9/entrypoint.sh @@ -75,6 +75,11 @@ export BROADCAST_POOL_RPC_PASS="${RPC_PASS}" export BROADCAST_POOL_INDEXER_URL="${ELECTRS_URL}" export BROADCAST_POOL_ELECTRUM_HOST="${BROADCAST_POOL_ELECTRUM_HOST:-0.0.0.0}" export BROADCAST_POOL_ELECTRUM_PORT="${BROADCAST_POOL_ELECTRUM_PORT:-50050}" +# Dedicated Liana listener. Liana always sets a block-height nLockTime (anti-fee-sniping, +# not disableable like Sparrow's MTP), so a tx arriving on the Sparrow port would be +# categorized "by_block". Liana connects to THIS port so it is tagged source=liana and +# ingested as manual/pending, ready to schedule a date/time from the dashboard. +export BROADCAST_POOL_LIANA_ELECTRUM_PORT="${BROADCAST_POOL_LIANA_ELECTRUM_PORT:-50051}" export BROADCAST_POOL_WEB_HOST="${BROADCAST_POOL_WEB_HOST:-0.0.0.0}" export BROADCAST_POOL_WEB_PORT="${BROADCAST_POOL_WEB_PORT:-8080}" # Platform hint: the dashboard can't auto-detect a LAN IP here (the container only sees diff --git a/start9/startos/i18n/dictionaries/default.ts b/start9/startos/i18n/dictionaries/default.ts index 3069c4f..9b4d740 100644 --- a/start9/startos/i18n/dictionaries/default.ts +++ b/start9/startos/i18n/dictionaries/default.ts @@ -6,11 +6,13 @@ const dict = { 'The Electrum interface is ready': 2, 'The Electrum interface is not ready': 3, 'Web Dashboard': 4, - 'The main Electrum interface for Sparrow and Liana (plain TCP, no SSL)': 5, + 'The main Electrum interface for Sparrow (plain TCP, no SSL)': 5, 'Web dashboard to monitor and schedule pending broadcasts': 6, 'Provides blockchain data, RPC, and cookie authentication': 7, 'Provides the Electrum index backend for address and history lookups': 8, 'Pruning must be disabled for Electrs and transaction lookups to work.': 9, + 'Electrum — Liana (TCP)': 10, + 'Dedicated Electrum interface for Liana — transactions arrive as pending so you can schedule a broadcast time': 11, } as const export type LangDict = Record diff --git a/start9/startos/i18n/dictionaries/translations.ts b/start9/startos/i18n/dictionaries/translations.ts index af732a2..d0b4caf 100644 --- a/start9/startos/i18n/dictionaries/translations.ts +++ b/start9/startos/i18n/dictionaries/translations.ts @@ -7,11 +7,13 @@ const translations: Record = { 2: 'La interfaz Electrum está lista', 3: 'La interfaz Electrum no está lista', 4: 'Panel web', - 5: 'Interfaz Electrum principal para Sparrow y Liana (TCP plano, sin SSL)', + 5: 'Interfaz Electrum principal para Sparrow (TCP plano, sin SSL)', 6: 'Panel web para monitorizar y programar las difusiones pendientes', 7: 'Proporciona datos de blockchain, RPC y autenticación por cookie', 8: 'Proporciona el backend de índice Electrum para direcciones e historial', 9: 'La poda debe estar deshabilitada para que Electrs y las búsquedas de transacciones funcionen.', + 10: 'Electrum — Liana (TCP)', + 11: 'Interfaz Electrum dedicada para Liana — las transacciones entran como pendientes para que programes la fecha/hora de difusión', }, } diff --git a/start9/startos/interfaces.ts b/start9/startos/interfaces.ts index b9c2237..79ac987 100644 --- a/start9/startos/interfaces.ts +++ b/start9/startos/interfaces.ts @@ -1,9 +1,9 @@ import { i18n } from './i18n' import { sdk } from './sdk' -import { electrumPort, webPort } from './utils' +import { electrumPort, lianaPort, webPort } from './utils' export const setInterfaces = sdk.setupInterfaces(async ({ effects }) => { - // --- Electrum proxy (plain TCP) for Sparrow / Liana --- + // --- Electrum proxy (plain TCP) for Sparrow --- const electrumHost = sdk.MultiHost.of(effects, 'electrum') const electrumOrigin = await electrumHost.bindPort(electrumPort, { protocol: null, @@ -14,8 +14,30 @@ export const setInterfaces = sdk.setupInterfaces(async ({ effects }) => { const electrum = sdk.createInterface(effects, { id: 'electrum', name: i18n('Electrum (TCP)'), + description: i18n('The main Electrum interface for Sparrow (plain TCP, no SSL)'), + type: 'api', + masked: false, + schemeOverride: null, + username: null, + path: '', + query: {}, + }) + + // --- Dedicated Electrum proxy for Liana (plain TCP) --- + // Liana's anti-fee-sniping block-height nLockTime would be categorized "by_block" + // on the Sparrow port; connecting here tags the source as Liana → manual/pending. + const lianaHost = sdk.MultiHost.of(effects, 'liana') + const lianaOrigin = await lianaHost.bindPort(lianaPort, { + protocol: null, + preferredExternalPort: lianaPort, + addSsl: null, + secure: { ssl: false }, + }) + const liana = sdk.createInterface(effects, { + id: 'liana', + name: i18n('Electrum — Liana (TCP)'), description: i18n( - 'The main Electrum interface for Sparrow and Liana (plain TCP, no SSL)', + 'Dedicated Electrum interface for Liana — transactions arrive as pending so you can schedule a broadcast time', ), type: 'api', masked: false, @@ -47,6 +69,7 @@ export const setInterfaces = sdk.setupInterfaces(async ({ effects }) => { return [ await electrumOrigin.export([electrum]), + await lianaOrigin.export([liana]), await webOrigin.export([web]), ] }) diff --git a/start9/startos/utils.ts b/start9/startos/utils.ts index 6b0e588..b99ef3b 100644 --- a/start9/startos/utils.ts +++ b/start9/startos/utils.ts @@ -1,4 +1,8 @@ -// Electrum proxy port for Sparrow / Liana (plain TCP). +// Electrum proxy port for Sparrow (plain TCP). export const electrumPort = 50050 +// Dedicated Electrum port for Liana — txs here are ingested as manual/pending so the +// user can schedule a date/time (Liana's block-height nLockTime would otherwise be +// categorized as "by_block"). +export const lianaPort = 50051 // Web dashboard port. export const webPort = 8080 diff --git a/start9/startos/versions/current.ts b/start9/startos/versions/current.ts index 669bc40..cc278a5 100644 --- a/start9/startos/versions/current.ts +++ b/start9/startos/versions/current.ts @@ -1,10 +1,10 @@ import { IMPOSSIBLE, VersionInfo } from '@start9labs/start-sdk' export const current = VersionInfo.of({ - version: '0.3.14:1', + version: '0.3.14:2', releaseNotes: { - en_US: 'Initial StartOS package: Electrum broadcast pool for Sparrow/Liana with virtual mempool and scheduled broadcasting. Network auto-detected from Bitcoin Core.', - es_ES: 'Paquete inicial para StartOS: pool de difusión Electrum para Sparrow/Liana con mempool virtual y difusión programada. Red autodetectada desde Bitcoin Core.', + en_US: 'Add a dedicated Electrum interface for Liana (port 50051) so its transactions are held as pending for date/time scheduling instead of being categorized by block height.', + es_ES: 'Añade una interfaz Electrum dedicada para Liana (puerto 50051) para que sus transacciones entren como pendientes y se puedan programar por fecha/hora, en vez de categorizarse por altura de bloque.', }, migrations: { up: async () => {}, From 3bb03777900457e488817762de3a50a5781f489f Mon Sep 17 00:00:00 2001 From: criptoworld Date: Sat, 27 Jun 2026 11:00:50 +0200 Subject: [PATCH 17/19] Revert "feat(start9): dedicated Liana Electrum interface (port 50051)" This reverts commit 8345b6db8b44b7ab61af9fcd4f4aa33e37cbdca7. --- start9/Dockerfile | 3 -- start9/entrypoint.sh | 5 ---- start9/startos/i18n/dictionaries/default.ts | 4 +-- .../startos/i18n/dictionaries/translations.ts | 4 +-- start9/startos/interfaces.ts | 29 ++----------------- start9/startos/utils.ts | 6 +--- start9/startos/versions/current.ts | 6 ++-- 7 files changed, 9 insertions(+), 48 deletions(-) diff --git a/start9/Dockerfile b/start9/Dockerfile index 3f1fe9d..b7d28a1 100644 --- a/start9/Dockerfile +++ b/start9/Dockerfile @@ -17,11 +17,8 @@ COPY --chmod=0755 entrypoint.sh /entrypoint.sh # StartOS data volume + auto-detected network (do NOT pin BROADCAST_POOL_NETWORK). ENV BROADCAST_POOL_DATA_DIR=/data ENV BROADCAST_POOL_ELECTRUM_PORT=50050 -ENV BROADCAST_POOL_LIANA_ELECTRUM_PORT=50051 ENV BROADCAST_POOL_WEB_PORT=8080 -EXPOSE 50051 - # StartOS runs service subcontainers as root and mounts /data owned by root, so run # as root (the base image's trailing `USER app` would hit EPERM writing to /data). USER root diff --git a/start9/entrypoint.sh b/start9/entrypoint.sh index b672852..64719d4 100644 --- a/start9/entrypoint.sh +++ b/start9/entrypoint.sh @@ -75,11 +75,6 @@ export BROADCAST_POOL_RPC_PASS="${RPC_PASS}" export BROADCAST_POOL_INDEXER_URL="${ELECTRS_URL}" export BROADCAST_POOL_ELECTRUM_HOST="${BROADCAST_POOL_ELECTRUM_HOST:-0.0.0.0}" export BROADCAST_POOL_ELECTRUM_PORT="${BROADCAST_POOL_ELECTRUM_PORT:-50050}" -# Dedicated Liana listener. Liana always sets a block-height nLockTime (anti-fee-sniping, -# not disableable like Sparrow's MTP), so a tx arriving on the Sparrow port would be -# categorized "by_block". Liana connects to THIS port so it is tagged source=liana and -# ingested as manual/pending, ready to schedule a date/time from the dashboard. -export BROADCAST_POOL_LIANA_ELECTRUM_PORT="${BROADCAST_POOL_LIANA_ELECTRUM_PORT:-50051}" export BROADCAST_POOL_WEB_HOST="${BROADCAST_POOL_WEB_HOST:-0.0.0.0}" export BROADCAST_POOL_WEB_PORT="${BROADCAST_POOL_WEB_PORT:-8080}" # Platform hint: the dashboard can't auto-detect a LAN IP here (the container only sees diff --git a/start9/startos/i18n/dictionaries/default.ts b/start9/startos/i18n/dictionaries/default.ts index 9b4d740..3069c4f 100644 --- a/start9/startos/i18n/dictionaries/default.ts +++ b/start9/startos/i18n/dictionaries/default.ts @@ -6,13 +6,11 @@ const dict = { 'The Electrum interface is ready': 2, 'The Electrum interface is not ready': 3, 'Web Dashboard': 4, - 'The main Electrum interface for Sparrow (plain TCP, no SSL)': 5, + 'The main Electrum interface for Sparrow and Liana (plain TCP, no SSL)': 5, 'Web dashboard to monitor and schedule pending broadcasts': 6, 'Provides blockchain data, RPC, and cookie authentication': 7, 'Provides the Electrum index backend for address and history lookups': 8, 'Pruning must be disabled for Electrs and transaction lookups to work.': 9, - 'Electrum — Liana (TCP)': 10, - 'Dedicated Electrum interface for Liana — transactions arrive as pending so you can schedule a broadcast time': 11, } as const export type LangDict = Record diff --git a/start9/startos/i18n/dictionaries/translations.ts b/start9/startos/i18n/dictionaries/translations.ts index d0b4caf..af732a2 100644 --- a/start9/startos/i18n/dictionaries/translations.ts +++ b/start9/startos/i18n/dictionaries/translations.ts @@ -7,13 +7,11 @@ const translations: Record = { 2: 'La interfaz Electrum está lista', 3: 'La interfaz Electrum no está lista', 4: 'Panel web', - 5: 'Interfaz Electrum principal para Sparrow (TCP plano, sin SSL)', + 5: 'Interfaz Electrum principal para Sparrow y Liana (TCP plano, sin SSL)', 6: 'Panel web para monitorizar y programar las difusiones pendientes', 7: 'Proporciona datos de blockchain, RPC y autenticación por cookie', 8: 'Proporciona el backend de índice Electrum para direcciones e historial', 9: 'La poda debe estar deshabilitada para que Electrs y las búsquedas de transacciones funcionen.', - 10: 'Electrum — Liana (TCP)', - 11: 'Interfaz Electrum dedicada para Liana — las transacciones entran como pendientes para que programes la fecha/hora de difusión', }, } diff --git a/start9/startos/interfaces.ts b/start9/startos/interfaces.ts index 79ac987..b9c2237 100644 --- a/start9/startos/interfaces.ts +++ b/start9/startos/interfaces.ts @@ -1,9 +1,9 @@ import { i18n } from './i18n' import { sdk } from './sdk' -import { electrumPort, lianaPort, webPort } from './utils' +import { electrumPort, webPort } from './utils' export const setInterfaces = sdk.setupInterfaces(async ({ effects }) => { - // --- Electrum proxy (plain TCP) for Sparrow --- + // --- Electrum proxy (plain TCP) for Sparrow / Liana --- const electrumHost = sdk.MultiHost.of(effects, 'electrum') const electrumOrigin = await electrumHost.bindPort(electrumPort, { protocol: null, @@ -14,30 +14,8 @@ export const setInterfaces = sdk.setupInterfaces(async ({ effects }) => { const electrum = sdk.createInterface(effects, { id: 'electrum', name: i18n('Electrum (TCP)'), - description: i18n('The main Electrum interface for Sparrow (plain TCP, no SSL)'), - type: 'api', - masked: false, - schemeOverride: null, - username: null, - path: '', - query: {}, - }) - - // --- Dedicated Electrum proxy for Liana (plain TCP) --- - // Liana's anti-fee-sniping block-height nLockTime would be categorized "by_block" - // on the Sparrow port; connecting here tags the source as Liana → manual/pending. - const lianaHost = sdk.MultiHost.of(effects, 'liana') - const lianaOrigin = await lianaHost.bindPort(lianaPort, { - protocol: null, - preferredExternalPort: lianaPort, - addSsl: null, - secure: { ssl: false }, - }) - const liana = sdk.createInterface(effects, { - id: 'liana', - name: i18n('Electrum — Liana (TCP)'), description: i18n( - 'Dedicated Electrum interface for Liana — transactions arrive as pending so you can schedule a broadcast time', + 'The main Electrum interface for Sparrow and Liana (plain TCP, no SSL)', ), type: 'api', masked: false, @@ -69,7 +47,6 @@ export const setInterfaces = sdk.setupInterfaces(async ({ effects }) => { return [ await electrumOrigin.export([electrum]), - await lianaOrigin.export([liana]), await webOrigin.export([web]), ] }) diff --git a/start9/startos/utils.ts b/start9/startos/utils.ts index b99ef3b..6b0e588 100644 --- a/start9/startos/utils.ts +++ b/start9/startos/utils.ts @@ -1,8 +1,4 @@ -// Electrum proxy port for Sparrow (plain TCP). +// Electrum proxy port for Sparrow / Liana (plain TCP). export const electrumPort = 50050 -// Dedicated Electrum port for Liana — txs here are ingested as manual/pending so the -// user can schedule a date/time (Liana's block-height nLockTime would otherwise be -// categorized as "by_block"). -export const lianaPort = 50051 // Web dashboard port. export const webPort = 8080 diff --git a/start9/startos/versions/current.ts b/start9/startos/versions/current.ts index cc278a5..669bc40 100644 --- a/start9/startos/versions/current.ts +++ b/start9/startos/versions/current.ts @@ -1,10 +1,10 @@ import { IMPOSSIBLE, VersionInfo } from '@start9labs/start-sdk' export const current = VersionInfo.of({ - version: '0.3.14:2', + version: '0.3.14:1', releaseNotes: { - en_US: 'Add a dedicated Electrum interface for Liana (port 50051) so its transactions are held as pending for date/time scheduling instead of being categorized by block height.', - es_ES: 'Añade una interfaz Electrum dedicada para Liana (puerto 50051) para que sus transacciones entren como pendientes y se puedan programar por fecha/hora, en vez de categorizarse por altura de bloque.', + en_US: 'Initial StartOS package: Electrum broadcast pool for Sparrow/Liana with virtual mempool and scheduled broadcasting. Network auto-detected from Bitcoin Core.', + es_ES: 'Paquete inicial para StartOS: pool de difusión Electrum para Sparrow/Liana con mempool virtual y difusión programada. Red autodetectada desde Bitcoin Core.', }, migrations: { up: async () => {}, From d776edea05eac5aebf8166e5ade947f9450a8071 Mon Sep 17 00:00:00 2001 From: criptoworld Date: Sat, 27 Jun 2026 11:03:31 +0200 Subject: [PATCH 18/19] fix(electrum): detect Liana on the shared Sparrow port (v0.3.15) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Premise: Sparrow and Liana use the SAME Electrum port (50050). Liana always sets a block-height nLockTime (anti-fee-sniping, not disableable like Sparrow's MTP), so its tx was categorized 'by_block' instead of manual/pending — the user couldn't schedule a date/time for it. Distinguish the wallets on the shared port: Sparrow always sends server.version during its handshake; Liana (electrum-client/Rust) skips it. Track saw_server_version + the client name per session; at broadcast time, a Sparrow-port session that never sent server.version (or whose client name is Liana) is attributed to 'liana' → ingested as manual/pending (date/time scheduling; price scheduling already excluded for non-zero nLockTime). Sparrow's own by_block mode is unaffected (it always sends server.version). Reverts the separate-port StartOS interface (50051) — same port for both, per the premise. Co-Authored-By: Claude Opus 4.8 --- Cargo.lock | 2 +- Cargo.toml | 2 +- src/electrum_server/mod.rs | 107 ++++++++++++++++++++++++++++++++++++- 3 files changed, 108 insertions(+), 3 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 9e3d203..4a14a06 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -294,7 +294,7 @@ checksum = "c4512299f36f043ab09a583e57bceb5a5aab7a73db1805848e8fef3c9e8c78b3" [[package]] name = "broadcast-pool" -version = "0.3.14" +version = "0.3.15" dependencies = [ "anyhow", "axum", diff --git a/Cargo.toml b/Cargo.toml index 42d9b65..fb34e53 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "broadcast-pool" -version = "0.3.14" +version = "0.3.15" edition = "2021" description = "Bitcoin Broadcast Pool - Privacy-preserving transaction scheduling and migration tool" license = "MIT" diff --git a/src/electrum_server/mod.rs b/src/electrum_server/mod.rs index 0788c9d..5116eb8 100644 --- a/src/electrum_server/mod.rs +++ b/src/electrum_server/mod.rs @@ -241,6 +241,35 @@ fn peek_line_methods(line: &str) -> Option> { .map(|m| vec![m.to_string()]) } +/// Record `server.version` (and its client-name param) on the session. Sparrow always +/// sends this during its handshake; Liana's electrum-client skips it. The presence/absence +/// plus the client name let the broadcast path tell the wallets apart on the shared port. +fn capture_wallet_fingerprint(line: &str, session: &mut SessionState) { + let Ok(v) = serde_json::from_str::(line.trim()) else { + return; + }; + let items: Vec<&serde_json::Value> = match v.as_array() { + Some(arr) => arr.iter().collect(), + None => vec![&v], + }; + for item in items { + if item.get("method").and_then(|m| m.as_str()) != Some("server.version") { + continue; + } + session.saw_server_version = true; + if let Some(name) = item + .get("params") + .and_then(|p| p.as_array()) + .and_then(|a| a.first()) + .and_then(|n| n.as_str()) + { + if !name.is_empty() { + session.wallet_label = Some(name.to_string()); + } + } + } +} + /// Parse broadcast RPC even when strict struct decode would fail. /// Supports `[hex]`, `"hex"`, object params, and broadcast_package batches. fn params_first_string(params: &serde_json::Value) -> Option { @@ -352,6 +381,13 @@ struct SessionState { /// Last tx acked this session — Sparrow polls input scripthashes before enrichment finishes. recent_broadcast_txid: Option, rpc_lines_handled: u32, + /// Whether this session sent `server.version`. Sparrow always does during its handshake; + /// Liana (electrum-client / Rust) skips it. Used to tell the two wallets apart on the + /// SHARED Electrum port so Liana's block-height nLockTime txs are ingested as manual + /// (schedulable by date/time) instead of by_block. + saw_server_version: bool, + /// Client name from `server.version` params (e.g. "Sparrow 2.x"), when sent. + wallet_label: Option, } impl SessionState { @@ -364,7 +400,25 @@ impl SessionState { broadcast_intercepted: false, recent_broadcast_txid: None, rpc_lines_handled: 0, + saw_server_version: false, + wallet_label: None, + } + } + + /// Effective ingest source for a broadcast on this session. On the shared Sparrow + /// port, a wallet that never sent `server.version` (or whose client name is Liana) + /// is treated as Liana so its tx is ingested as manual/pending. + fn effective_source<'a>(&self, listener_label: &'a str) -> &'a str { + if listener_label == "sparrow" { + let looks_liana = self + .wallet_label + .as_deref() + .is_some_and(|l| l.to_ascii_lowercase().contains("liana")); + if looks_liana || !self.saw_server_version { + return "liana"; + } } + listener_label } fn log_disconnect_summary(&self, peer_addr: std::net::SocketAddr, source_label: &str) { @@ -1344,9 +1398,22 @@ async fn intercept_and_handle_broadcast( tracing::info!("Broadcast ack sent to wallet (outputs pre-stored), txid={}", preview.txid); // ── Phase 2: DB insert + input scripthash enrichment in background ── + // Attribute the tx to the actual wallet: on the shared Sparrow port, a session that + // never sent server.version (or whose client name is Liana) is Liana, so its tx is + // ingested as manual/pending (date/time scheduling) instead of by_block. + let effective_source = session.effective_source(source_label); + if effective_source != source_label { + tracing::info!( + "Broadcast on '{}' port attributed to '{}' (saw_server_version={}, wallet={:?})", + source_label, + effective_source, + session.saw_server_version, + session.wallet_label + ); + } let pm = pool_manager.clone(); let cfg = config.clone(); - let src = source_label.to_string(); + let src = effective_source.to_string(); let hex_owned = hex_param; let preview_txid = preview.txid.clone(); @@ -1672,6 +1739,10 @@ async fn process_client_line( session.rpc_lines_handled += 1; + // Wallet fingerprint: record server.version (and its client name) so a broadcast + // later in the session can be attributed to Sparrow vs Liana on the shared port. + capture_wallet_fingerprint(line_str, session); + // Handshake RPCs: always answer locally (never block on upstream electrs). if let Ok(handshake) = parse_subrequests(line_str) { if !handshake.is_empty() @@ -2291,6 +2362,40 @@ mod tests { const SAMPLE_TX: &str = "0100000002f327e86da3e66bd20e1129b1fb36d07056f0b9a117199e759396526b8f3a20780000000000fffffffff0ede03d75050f20801d50358829ae02c058e8677d2cc74df51f738285013c260000000000ffffffff02f028d6dc010000001976a914ffb035781c3c69e076d48b60c3d38592e7ce06a788ac00ca9a3b000000001976a914fa5139067622fd7e1e722a05c17c2bb7d5fd6df088ac00000000"; + // On the shared Sparrow port, Liana is identified by the absence of server.version + // (or a Liana client name) so its block-height nLockTime tx becomes manual, not by_block. + #[test] + fn effective_source_detects_liana_on_shared_port() { + // Sparrow: sends server.version → stays sparrow. + let mut sparrow = SessionState::new(); + capture_wallet_fingerprint( + r#"{"jsonrpc":"2.0","method":"server.version","params":["Sparrow 2.1.0","1.4"],"id":1}"#, + &mut sparrow, + ); + assert!(sparrow.saw_server_version); + assert_eq!(sparrow.effective_source("sparrow"), "sparrow"); + + // Liana: never sends server.version → treated as liana on the sparrow port. + let mut liana = SessionState::new(); + capture_wallet_fingerprint( + r#"{"jsonrpc":"2.0","method":"blockchain.block.header","params":[0],"id":1}"#, + &mut liana, + ); + assert!(!liana.saw_server_version); + assert_eq!(liana.effective_source("sparrow"), "liana"); + + // Liana that DOES send a Liana-named server.version → still liana. + let mut liana_named = SessionState::new(); + capture_wallet_fingerprint( + r#"{"jsonrpc":"2.0","method":"server.version","params":["Liana","1.4"],"id":1}"#, + &mut liana_named, + ); + assert_eq!(liana_named.effective_source("sparrow"), "liana"); + + // The dedicated liana listener is always liana regardless of fingerprint. + assert_eq!(sparrow.effective_source("liana"), "liana"); + } + #[test] fn extract_broadcast_hex_array_params() { let line = format!( From 1d35b47b491966011437b68d8fb526c5cd52bbea Mon Sep 17 00:00:00 2001 From: criptoworld Date: Sat, 27 Jun 2026 11:06:52 +0200 Subject: [PATCH 19/19] chore(start9): base off broadcast-pool-umbrel:0.3.15 (Liana shared-port detection) Co-Authored-By: Claude Opus 4.8 --- start9/Dockerfile | 2 +- start9/startos/versions/current.ts | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/start9/Dockerfile b/start9/Dockerfile index b7d28a1..46fc07a 100644 --- a/start9/Dockerfile +++ b/start9/Dockerfile @@ -9,7 +9,7 @@ # NOTE: the base image is currently linux/amd64 only, so this package is x86_64 only. # Multi-arch (aarch64) is a follow-up: publish a multi-arch base image or per-arch # release binaries and download the matching one here. -FROM ghcr.io/criptoworld8484/broadcast-pool-umbrel:0.3.14 +FROM ghcr.io/criptoworld8484/broadcast-pool-umbrel:0.3.15 USER root COPY --chmod=0755 entrypoint.sh /entrypoint.sh diff --git a/start9/startos/versions/current.ts b/start9/startos/versions/current.ts index 669bc40..452c222 100644 --- a/start9/startos/versions/current.ts +++ b/start9/startos/versions/current.ts @@ -1,7 +1,7 @@ import { IMPOSSIBLE, VersionInfo } from '@start9labs/start-sdk' export const current = VersionInfo.of({ - version: '0.3.14:1', + version: '0.3.15:1', releaseNotes: { en_US: 'Initial StartOS package: Electrum broadcast pool for Sparrow/Liana with virtual mempool and scheduled broadcasting. Network auto-detected from Bitcoin Core.', es_ES: 'Paquete inicial para StartOS: pool de difusión Electrum para Sparrow/Liana con mempool virtual y difusión programada. Red autodetectada desde Bitcoin Core.',