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

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 1 addition & 3 deletions .github/workflows/build.yml
Original file line number Diff line number Diff line change
Expand Up @@ -13,8 +13,6 @@ concurrency:
jobs:
build:
if: github.event.pull_request.draft == false
uses: start9labs/shared-workflows/.github/workflows/build.yml@master
# with:
# FREE_DISK_SPACE: true
uses: Start9Labs/start-technologies/.github/workflows/build.yml@master
secrets:
DEV_KEY: ${{ secrets.DEV_KEY }}
3 changes: 1 addition & 2 deletions .github/workflows/release.yml
Original file line number Diff line number Diff line change
Expand Up @@ -7,9 +7,8 @@ on:

jobs:
release:
uses: start9labs/shared-workflows/.github/workflows/release.yml@master
uses: Start9Labs/start-technologies/.github/workflows/release.yml@master
with:
# FREE_DISK_SPACE: true
RELEASE_REGISTRY: ${{ vars.RELEASE_REGISTRY }}
S3_S9PKS_BASE_URL: ${{ vars.S3_S9PKS_BASE_URL }}
secrets:
Expand Down
3 changes: 1 addition & 2 deletions .github/workflows/tagAndRelease.yml
Original file line number Diff line number Diff line change
Expand Up @@ -11,10 +11,9 @@ concurrency:

jobs:
tag:
uses: start9labs/shared-workflows/.github/workflows/tagAndRelease.yml@master
uses: Start9Labs/start-technologies/.github/workflows/tagAndRelease.yml@master
with:
REFERENCE_REGISTRY: ${{ vars.REFERENCE_REGISTRY }}
# FREE_DISK_SPACE: true
RELEASE_REGISTRY: ${{ vars.RELEASE_REGISTRY }}
S3_S9PKS_BASE_URL: ${{ vars.S3_S9PKS_BASE_URL }}
secrets:
Expand Down
18 changes: 16 additions & 2 deletions AGENTS.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,19 @@
# AGENTS.md

You are working in a StartOS service-package repository — a repo that builds a `.s9pk` for installation on StartOS.
This is a StartOS service-package repository — it builds a `.s9pk` for StartOS.

**Before doing anything in this repo, read [CONTRIBUTING.md](./CONTRIBUTING.md) and every document it links to.** That covers what this package is, how it's built, how it ships, and the conventions to follow. Do not begin work until you have read them all.
Develop it inside a StartOS packaging workspace created by `start-cli s9pk init-workspace`,
which provides the packaging guide and agent context one level up. If you're reading this in a
bare clone with no workspace, the full guide is at <https://docs.start9.com/packaging>.

Work this package's `TODO.md` from top to bottom. Keep `README.md` (architecture, for developers and LLMs) and `instructions.md` (end-user docs) in sync with your changes.

## This repo

- **Package id is `bolt12-pay`.** Dependent of `lnd`: it reaches LND's REST and gRPC over the LXC bridge (importing the host ids `controlHostId` / `gRPCHostId` and the internal ports `restPort` / `gRPCPort` from `lnd-startos/startos/interfaces`, resolved in `startos/utils.ts` through the reactive `bridgeAddress` helper — a `.const()` per host that heals main onto the real address when LND binds; while unresolved the env vars are omitted) and injects those addresses into `assets/start.sh` as `LND_REST_URL` / `LND_GRPC_ADDRESS` — the app and the embedded `lndk` read them from there (the `lnd.startos` DNS name is gone in StartOS 0.4.0). Reads LND's TLS cert + admin macaroon off a read-only dependency mount at `/mnt/lnd`.
- **Supports LND 0.20 and 0.21** (`>=0.20.1-beta:12`). BOLT12 needs onion messages: LND advertises them natively from 0.21, while 0.20 needs the `protocol.custom-*` entries in `lnd.conf`. Setting those on 0.21 crash-loops LND (`feature bit: 39 already set`), so `startos/dependencies.ts` reads LND's installed version via `sdk.getServiceManifest(...).const()` and posts the Auto-Configure task **only** to pre-0.21 nodes, clearing it when the user upgrades. LND 0.21's config spec drops the `onion-messages` toggle entirely, so the typed action we import can't describe the field — the task goes through the raw `effects.action.createTask` effect rather than `sdk.action.createTask`. Don't "simplify" that back to the typed helper, and don't re-pin the dependency to `>=0.21`.
- **Upstream is a git submodule at `upstream/`.** The image is built from its `app/` subdir by the `Dockerfile`; the container runs `assets/start.sh`, which starts the uvicorn web app on `0.0.0.0:8081` and a background `lndk` process serving gRPC on `127.0.0.1:7000`.

## Inspecting a running install

To run a command inside the service's container (read its generated config, grep app logs), use `start-cli package attach bolt12-pay -n bolt12-pay-sub -- <cmd>`. Select the subcontainer by **name** with `-n` (the name passed to `SubContainer.of` in `main.ts` — here `bolt12-pay-sub`) or by image with `-i`. Note: `-s/--subcontainer` matches the internal **Guid**, not the name, so passing a name to `-s` fails with "no matching subcontainers".
42 changes: 0 additions & 42 deletions CONTRIBUTING.md

This file was deleted.

2 changes: 1 addition & 1 deletion Makefile
Original file line number Diff line number Diff line change
@@ -1,3 +1,3 @@
ARCHES := x86 arm
# overrides to s9pk.mk must precede the include statement
include s9pk.mk
include node_modules/@start9labs/start-sdk/s9pk.mk
76 changes: 42 additions & 34 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -31,12 +31,12 @@

## Image and Container Runtime

| Property | Value |
|----------|-------|
| Image | `main` — built from [`Dockerfile`](./Dockerfile) (upstream `app/` via the `upstream/` submodule + LNDK runtime) |
| Base | `python:3.11-slim` + LNDK runtime from `alex71btc/lndk` |
| Architectures | x86_64, aarch64 |
| Entrypoint | `/usr/local/bin/docker_entrypoint.sh` → `start.sh` |
| Property | Value |
| ------------- | --------------------------------------------------------------------------------------------------------------- |
| Image | `main` — built from [`Dockerfile`](./Dockerfile) (upstream `app/` via the `upstream/` submodule + LNDK runtime) |
| Base | `python:3.11-slim` + LNDK runtime from `alex71btc/lndk` |
| Architectures | x86_64, aarch64 |
| Entrypoint | `/usr/local/bin/docker_entrypoint.sh` → `start.sh` |

The container runs two processes from [`assets/start.sh`](./assets/start.sh):

Expand All @@ -47,9 +47,9 @@ The container runs two processes from [`assets/start.sh`](./assets/start.sh):

## Volume and Data Layout

| Volume | Mount Point | Purpose |
|--------|-------------|---------|
| `main` | `/data` | App config, secrets, and LNDK data dir (`/data/lndk`) |
| Volume | Mount Point | Purpose |
| ------ | ----------- | ----------------------------------------------------- |
| `main` | `/data` | App config, secrets, and LNDK data dir (`/data/lndk`) |

**Dependency mounts:**

Expand All @@ -59,9 +59,9 @@ The container runs two processes from [`assets/start.sh`](./assets/start.sh):

## Network Access and Interfaces

| Interface | Port | Protocol | Purpose |
|-----------|------|----------|---------|
| Web UI | 8081 | HTTP | BOLT12 Pay web interface |
| Interface | Port | Protocol | Purpose |
| --------- | ---- | -------- | ------------------------ |
| Web UI | 8081 | HTTP | BOLT12 Pay web interface |

**Access methods (StartOS 0.4.0):**

Expand All @@ -78,12 +78,12 @@ For Lightning Address / LNURL / `.well-known` endpoints to resolve publicly, exp

### Set Primary URL

| Property | Value |
|----------|-------|
| ID | `set-primary-url` |
| Visibility | Enabled |
| Availability | Any status |
| Purpose | Choose which non-local URL to advertise as the LNURL / Lightning Address base |
| Property | Value |
| ------------ | ----------------------------------------------------------------------------- |
| ID | `set-primary-url` |
| Visibility | Enabled |
| Availability | Any status |
| Purpose | Choose which non-local URL to advertise as the LNURL / Lightning Address base |

Pick one of the service's non-local URLs (use a **clearnet or custom-domain** URL — Tor and `.local` won't resolve for external senders). The selection is stored on the `startos` volume and injected on next start as the app's native `LNURL_BASE_URL`, `LNURL_BASE_DOMAIN`, `PUBLIC_LNURL_ADDRESS`, and `PUBLIC_BIP353_ADDRESS` env vars. These are **defaults** — the in-app admin settings still override them. If a previously-selected URL is later removed, StartOS posts a task to pick a new one.

Expand All @@ -93,15 +93,24 @@ Pick one of the service's non-local URLs (use a **clearnet or custom-domain** UR

### LND (`lnd`)

| Property | Value |
|----------|-------|
| **Required** | Yes |
| **Health checks** | `lnd` must pass |
| **Mounted volumes** | `lnd:main` at `/mnt/lnd` (read-only) — TLS cert and admin macaroon |
| **Reached at** | `lnd.startos` (REST `:8080`, gRPC `:10009`) |
| **Purpose** | Create and pay BOLT12 offers via LNDK |
| Property | Value |
| ------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| **Required** | Yes |
| **Health checks** | `lnd` must pass |
| **Mounted volumes** | `lnd:main` at `/mnt/lnd` (read-only) — TLS cert and admin macaroon |
| **Reached at** | LND's REST (`:8080`) and gRPC (`:10009`) over the StartOS LXC bridge — resolved from LND's `control`/`grpc` hosts and injected into `start.sh` as `LND_REST_URL` / `LND_GRPC_ADDRESS` (the `lnd.startos` DNS name is gone in StartOS 0.4.0) |
| **Purpose** | Create and pay BOLT12 offers via LNDK |

**LND must have onion-message support enabled.** BOLT12 offers require `protocol.custom-message=513`, `protocol.custom-nodeann=39`, and `protocol.custom-init=39` in `lnd.conf`. BOLT12 Pay configures this automatically: on startup it posts a task against LND's hidden **Auto-Configure** action, which the user approves with one click — no manual `lnd.conf` editing. The task uses `input-not-matches`, so it clears once the settings are present and reappears if they are ever removed.
**Supports LND 0.20 and 0.21** (`>=0.20.1-beta:12`). BOLT12 offers need onion-message support, and how it is obtained depends on the LND version — so `startos/dependencies.ts` reads LND's installed version and posts the Auto-Configure task only where it is needed:

| Installed LND | Onion messages | Auto-Configure task |
| -------------- | ------------------------------------- | ------------------------------------------------------- |
| `>=0.21.0-beta:0` | Advertised natively (feature bit 39) | Not posted; cleared if one already exists |
| `<0.21.0-beta:0` | Require the `protocol.custom-*` entries in `lnd.conf` | Posted as `critical` against LND's hidden `autoconfig` action |

Setting the `protocol.custom-*` entries on LND 0.21 is not merely redundant — LND aborts server creation (`feature bit: 39 already set`) and crash-loops. The version is read with `sdk.getServiceManifest(...).const()`, so upgrading LND from 0.20 to 0.21 re-runs init and clears the task instead of stranding the user with a critical task they cannot satisfy.

LND 0.21's config spec removes the `onion-messages` toggle altogether, so the typed `autoconfig` action we import can no longer describe the field. The task is therefore posted through the raw `effects.action.createTask` effect (same `replayId` the SDK helper would derive, `lnd:autoconfig`), whose input is the OS's untyped `TaskInput`.

See [instructions.md](./instructions.md) for the user-facing steps.

Expand All @@ -119,23 +128,23 @@ LND credentials are not backed up here; they live on the LND package and are re-

## Health Checks

| Check | Display Name | Method | Messages |
|-------|--------------|--------|----------|
| Web UI | "Web UI" | Port 8081 listening | "BOLT12 Pay is ready" / "BOLT12 Pay web interface is not ready" |
| Check | Display Name | Method | Messages |
| ------ | ------------ | ------------------- | --------------------------------------------------------------- |
| Web UI | "Web UI" | Port 8081 listening | "BOLT12 Pay is ready" / "BOLT12 Pay web interface is not ready" |

---

## Limitations and Differences

1. **LND onion messages** — BOLT12 offers require LND's `protocol.custom-*` settings; BOLT12 Pay enables them via a one-click **Auto-Configure** task on LND (no manual `lnd.conf` editing). Requires an LND package recent enough to expose the Auto-Configure action.
1. **LND onion messages** — BOLT12 offers require onion-message support. LND provides it natively from 0.21; on LND 0.20 it is enabled through a one-click Auto-Configure task. BOLT12 Pay accepts LND `>=0.20.1-beta:12` and posts the task only to pre-0.21 nodes.
2. **LNURL base URL** — seeded from the **Set Primary URL** action; the in-app admin settings can still override it. All other app configuration is done inside the web UI.
3. **Mainnet only** — the LND macaroon path is pinned to `data/chain/bitcoin/mainnet`.

---

## Contributing

See [CONTRIBUTING.md](CONTRIBUTING.md) for build instructions and development workflow.
See [AGENTS.md](AGENTS.md) for repo conventions and the [StartOS packaging guide](https://docs.start9.com/packaging) for build instructions and development workflow.

---

Expand All @@ -152,12 +161,11 @@ volumes:
startos: (StartOS metadata; not mounted into the container)
dependency_mounts:
lnd: /mnt/lnd (read-only)
lnd_reached_at: LXC bridge (REST :8080, gRPC :10009), injected into start.sh as LND_REST_URL / LND_GRPC_ADDRESS
ports:
ui: 8081
dependencies:
- lnd (required; onion messages enabled via a one-click task against lnd's hidden `autoconfig` action — writes protocol.custom-message=513/nodeann=39/init=39)
- lnd (required, >=0.20.1-beta:12; onion messages native on 0.21, Auto-Configure task posted on 0.20)
actions:
- set-primary-url
tasks_posted:
- lnd/autoconfig (critical, input-not-matches: { onion-messages: true })
```
16 changes: 8 additions & 8 deletions assets/start.sh
Original file line number Diff line number Diff line change
Expand Up @@ -16,13 +16,13 @@ export HOST=0.0.0.0
export PORT=8081
export PYTHONPATH=/app

# LND is a required StartOS dependency, reachable at lnd.startos. Its data
# directory (TLS cert + macaroons) is mounted read-only at /mnt/lnd.
# LND is a required StartOS dependency, reached over the StartOS LXC bridge.
# LND_REST_URL (REST) and LND_GRPC_ADDRESS (gRPC) are injected by the StartOS
# service — its `lnd.startos` DNS name is gone. Its data directory (TLS cert +
# macaroons) is mounted read-only at /mnt/lnd.
export LND_DIR=/mnt/lnd
export LND_HOST=lnd.startos
export LND_TLS_CERT_PATH="$LND_DIR/tls.cert"
export LND_MACAROON_PATH="$LND_DIR/data/chain/bitcoin/mainnet/admin.macaroon"
export LND_REST_URL="https://${LND_HOST}:8080"
export LND_REST_INSECURE=true

export LNDK_CLI=/usr/local/bin/lndk-cli
Expand Down Expand Up @@ -55,18 +55,18 @@ while [ ! -f "$LND_TLS_CERT_PATH" ] || [ ! -f "$LND_MACAROON_PATH" ]; do
sleep 5
done

echo "Starting LNDK background loop against ${LND_HOST}..."
echo "Starting LNDK background loop against ${LND_GRPC_ADDRESS}..."
(
while true; do
if ! curl -ksS --connect-timeout 3 "${LND_REST_URL}/v1/getinfo" >/dev/null 2>&1; then
echo "LND REST not ready yet on ${LND_HOST}, retrying..."
echo "LND REST not ready yet at ${LND_REST_URL}, retrying..."
sleep 5
continue
fi

echo "Starting LNDK against ${LND_HOST}..."
echo "Starting LNDK against ${LND_GRPC_ADDRESS}..."
lndk \
--address="https://${LND_HOST}:10009" \
--address="${LND_GRPC_ADDRESS}" \
--cert-path="$LND_TLS_CERT_PATH" \
--macaroon-path="$LND_MACAROON_PATH" \
--data-dir=/data/lndk \
Expand Down
Loading