From ee28275bf39ce38b77a35904d3c66d94650e36a6 Mon Sep 17 00:00:00 2001 From: Padreug Date: Fri, 9 Oct 2026 09:40:41 +0200 Subject: [PATCH 01/15] =?UTF-8?q?feat(ops):=20atm-reconcile=20=E2=80=94=20?= =?UTF-8?q?check=20cassette=20ledgers=20against=20recorded=20history?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The cassettes table is a running total, so it can be re-derived: an absolute truth point (a recount, or an empty) plus the refills and dispenses since. A derived count that disagrees with the stored one is evidence of something the ledger never saw. Reconciliation deliberately refuses to start from a refill. A refill is a delta, and applying deltas on top of a wrong number just carries the error forward — which is how sintra's 20-EUR bay ran 10 notes high for weeks while its 50-EUR bay, zeroed by an `empty` before refilling, reconciled exactly. A bay with no baseline is reported as unreconcilable rather than silently assumed good. Also surfaces the two things that make a count untrustworthy: the counts-uncertain flag, and any transaction still sitting in dispense_error / partial. The SQL uses scalar subqueries rather than joins on purpose — joining transaction_bills to cassettes fans out across bays, and a LEFT JOIN whose rows are all excluded by the baseline cutoff collapses to NULL and poisons the arithmetic downstream (the first draft read "expected: blank" for exactly that reason). Exits non-zero on any gap or missing baseline so it can be run as a check after a test session. Refs #122 --- deploy/nixos/atm-reconcile.sh | 172 +++++++++++++++++++++++++++++++++ deploy/nixos/configuration.nix | 1 + 2 files changed, 173 insertions(+) create mode 100755 deploy/nixos/atm-reconcile.sh diff --git a/deploy/nixos/atm-reconcile.sh b/deploy/nixos/atm-reconcile.sh new file mode 100755 index 0000000..f25c10b --- /dev/null +++ b/deploy/nixos/atm-reconcile.sh @@ -0,0 +1,172 @@ +#!/usr/bin/env bash +# atm-reconcile — Check each cassette's ledger count against recorded history +# +# The cassettes table is a running total maintained by the machine: operator +# ops add to it (refill) or set it (recount / empty), and dispenses subtract +# from it. That means the count can be re-derived, and a derived value that +# disagrees with the stored one is evidence of something the ledger never saw. +# +# Reconciliation runs forward from each bay's last ABSOLUTE truth point — a +# `recount` (someone opened the bay and counted it) or an `empty` (set to +# zero). Refills are deltas and cannot serve as a baseline: a refill applied +# on top of a wrong number just carries the error forward, which is exactly +# how a bad count survives for weeks. +# +# expected = base + refills_since_base - dispensed_since_base +# gap = ledger - expected +# +# A non-zero gap means either notes moved without an op recording it, or a +# dispense moved notes the dispenser's counters did not report. The second is +# real: a note that leaves the bay and jams in the transport completes neither +# the `dispensed` nor the `rejected` counter, so the bay silently reads one +# high while the transaction row says nothing was dispensed (aiolabs/bitspire#122). +# +# A bay with NO baseline cannot be reconciled at all — its starting number +# came from somewhere unrecorded. Publish a recount for it; that is the only +# op that establishes ground truth (and the only one that clears the +# counts-uncertain flag). +# +# Usage: +# atm-reconcile # reconcile every bay +# atm-reconcile --csv # machine-readable +# atm-reconcile --quiet # exit status only, no output +# +# Exit status: +# 0 every bay reconciles +# 1 at least one bay has a non-zero gap or no baseline +# 2 database missing / unreadable + +set -euo pipefail + +DB="${ATM_STATE_DB:-/var/lib/bitspire/state.db}" + +FORMAT="-column -header" +QUIET=false + +while [[ $# -gt 0 ]]; do + case "$1" in + --csv) FORMAT="-csv -header"; shift ;; + --quiet) QUIET=true; shift ;; + -h|--help) + sed -n '2,36p' "$0" | sed 's/^# \{0,1\}//' + exit 0 ;; + *) echo "Unknown option: $1" >&2; exit 1 ;; + esac +done + +if [ ! -r "$DB" ]; then + echo "ERROR: state database not readable at $DB" >&2 + exit 2 +fi + +# Per-bay baseline, then the deltas since it. Written as scalar subqueries +# rather than joins on purpose: joining transaction_bills to cassettes fans +# out across bays, and a LEFT JOIN whose rows are all filtered out by the +# baseline cutoff collapses to NULL and poisons the arithmetic downstream. +RECONCILE_SQL=" +WITH bay AS ( + SELECT + c.position AS position, + c.denomination AS denomination, + c.count AS ledger, + (SELECT o.op_at FROM cassette_ops o + WHERE o.position = c.position AND o.op_type IN ('recount','empty') + ORDER BY o.op_at DESC, o.id DESC LIMIT 1) AS base_at, + (SELECT CASE o.op_type WHEN 'empty' THEN 0 ELSE o.count END FROM cassette_ops o + WHERE o.position = c.position AND o.op_type IN ('recount','empty') + ORDER BY o.op_at DESC, o.id DESC LIMIT 1) AS base_count + FROM cassettes c +), +delta AS ( + SELECT + bay.*, + (SELECT COALESCE(SUM(o.bills), 0) FROM cassette_ops o + WHERE o.position = bay.position AND o.op_type = 'refill' + AND o.op_at > COALESCE(bay.base_at, -1)) AS added, + (SELECT COALESCE(SUM(tb.count), 0) + FROM transaction_bills tb + JOIN transactions t ON t.txid = tb.txid + WHERE tb.denomination = bay.denomination + AND t.type IN ('cash_out','manual_dispense') + AND t.created_at / 1000 > COALESCE(bay.base_at, -1)) AS dispensed + FROM bay +) +SELECT + position AS 'Bay', + denomination AS 'Denom', + CASE WHEN base_at IS NULL THEN '(none)' ELSE datetime(base_at,'unixepoch') END AS 'Baseline', + CASE WHEN base_at IS NULL THEN NULL ELSE base_count END AS 'Base', + added AS 'Refilled', + dispensed AS 'Dispensed', + ledger AS 'Ledger', + CASE WHEN base_at IS NULL THEN NULL + ELSE base_count + added - dispensed END AS 'Expected', + CASE WHEN base_at IS NULL THEN 'NO BASELINE' + ELSE printf('%+d', ledger - (base_count + added - dispensed)) END AS 'Gap' +FROM delta +ORDER BY position; +" + +# Bays sharing a denomination break per-bay attribution: transaction_bills +# records what denomination went out, never which bay it came from, so the +# dispensed figure lands on every matching bay. +AMBIGUOUS=$(sqlite3 "$DB" \ + "SELECT group_concat(denomination) FROM ( + SELECT denomination FROM cassettes GROUP BY denomination HAVING COUNT(*) > 1);") + +PROBLEMS=$(sqlite3 "$DB" " +WITH bay AS ( + SELECT c.position AS position, c.denomination AS denomination, c.count AS ledger, + (SELECT o.op_at FROM cassette_ops o + WHERE o.position = c.position AND o.op_type IN ('recount','empty') + ORDER BY o.op_at DESC, o.id DESC LIMIT 1) AS base_at, + (SELECT CASE o.op_type WHEN 'empty' THEN 0 ELSE o.count END FROM cassette_ops o + WHERE o.position = c.position AND o.op_type IN ('recount','empty') + ORDER BY o.op_at DESC, o.id DESC LIMIT 1) AS base_count + FROM cassettes c +) +SELECT COUNT(*) FROM bay WHERE base_at IS NULL OR ledger <> ( + base_count + + (SELECT COALESCE(SUM(o.bills),0) FROM cassette_ops o + WHERE o.position = bay.position AND o.op_type = 'refill' AND o.op_at > bay.base_at) + - (SELECT COALESCE(SUM(tb.count),0) FROM transaction_bills tb + JOIN transactions t ON t.txid = tb.txid + WHERE tb.denomination = bay.denomination + AND t.type IN ('cash_out','manual_dispense') + AND t.created_at / 1000 > bay.base_at));") + +if ! $QUIET; then + # shellcheck disable=SC2086 # $FORMAT must word-split into two sqlite3 flags + sqlite3 $FORMAT "$DB" "$RECONCILE_SQL" + echo + + UNCERTAIN=$(sqlite3 "$DB" \ + "SELECT COALESCE(NULLIF(value,''),'') FROM meta WHERE key = 'countsUncertainSince';") + if [ -n "$UNCERTAIN" ]; then + echo "Counts flagged UNVERIFIED since $(date -u -d "@$UNCERTAIN" '+%F %T UTC' 2>/dev/null || echo "$UNCERTAIN")" + echo " A dispense ended without a trustworthy report. Only a recount clears this." + else + echo "Counts not flagged unverified." + fi + + echo + echo "=== Unresolved dispense failures ===" + # shellcheck disable=SC2086 # $FORMAT must word-split into two sqlite3 flags + sqlite3 $FORMAT "$DB" " + SELECT txid AS 'TX ID', status AS 'Status', + printf('%.2f', fiat_cents / 100.0) AS 'Fiat', currency AS 'Cur', + COALESCE(error,'') AS 'Error', + datetime(created_at / 1000,'unixepoch') AS 'Time (UTC)' + FROM transactions + WHERE status IN ('dispense_error','partial') + ORDER BY created_at DESC;" + + if [ -n "$AMBIGUOUS" ]; then + echo + echo "WARNING: denomination(s) $AMBIGUOUS are loaded in more than one bay." + echo " Dispenses are recorded by denomination, not by bay, so the Dispensed" + echo " column double-counts across those bays. Reconcile them as a group." + fi +fi + +[ "${PROBLEMS:-0}" -eq 0 ] || exit 1 diff --git a/deploy/nixos/configuration.nix b/deploy/nixos/configuration.nix index eeabe5a..458a239 100644 --- a/deploy/nixos/configuration.nix +++ b/deploy/nixos/configuration.nix @@ -376,6 +376,7 @@ in # ATM operations sqlite (writeShellScriptBin "atm-transactions" (builtins.readFile ./atm-transactions.sh)) + (writeShellScriptBin "atm-reconcile" (builtins.readFile ./atm-reconcile.sh)) ]; # Enable SSH for remote administration From 4d6ea8f163d9bc2638ea7e81c2e85c54b87a9a58 Mon Sep 17 00:00:00 2001 From: Padreug Date: Fri, 9 Oct 2026 09:44:02 +0200 Subject: [PATCH 02/15] fix(ops): atm-transactions queried fee_percent, which no longer exists MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The column was renamed to fee_fraction (schema_version 13), so the main query has been failing outright with "no such column: t.fee_percent" — the tool only ever worked in --summary and --inventory mode. Caught while reconciling sintra's cassettes, where listing transactions was the obvious first step and didn't work. Refs #40 --- deploy/nixos/atm-transactions.sh | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/deploy/nixos/atm-transactions.sh b/deploy/nixos/atm-transactions.sh index b234445..f15ff43 100644 --- a/deploy/nixos/atm-transactions.sh +++ b/deploy/nixos/atm-transactions.sh @@ -141,7 +141,7 @@ sql "SELECT t.fiat_cents / 100 AS 'Fiat', t.sats AS 'Sats', t.fee_sats AS 'Fee Sats', - printf('%.1f%%', t.fee_percent * 100) AS 'Fee %', + printf('%.1f%%', t.fee_fraction * 100) AS 'Fee %', CASE WHEN t.exchange_rate > 0 THEN printf('%.0f', t.exchange_rate) ELSE '-' END AS 'Rate', datetime(t.created_at / 1000, 'unixepoch') AS 'Time (UTC)', group_concat('Q' || tb.denomination || 'x' || tb.count) AS 'Bills' From 483e44aaa964c560c5b63c701d03fb5ab3751bdb Mon Sep 17 00:00:00 2001 From: Padreug Date: Fri, 9 Oct 2026 16:34:54 +0200 Subject: [PATCH 03/15] =?UTF-8?q?docs(adr):=20ADR-005=20=E2=80=94=20cash-o?= =?UTF-8?q?ut=20dispense=20outcome=20and=20settlement=20capture?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit A customer paid a 40 EUR cash-out, a note jammed at the cassette exit, and the dashboard showed `processed`. The machine had recorded the failure correctly. Nothing it knew ever left the box. The root is ordering, not display: spirekeeper spawns process_settlement the instant the payment lands, which is before the machine has begun to dispense. The legs are paid sub-second; the dispense fails afterwards; and the one remediation tool refuses once any leg has completed. It is unreachable for the exact case it was built for. ADR-005 makes payment the authorization and dispense confirmation the capture — distribution waits for the machine's report. The report is a report_dispense RPC (not the state doc: ADR-004's losing-writer problem), carried through a durable outbox, sent on success and failure, adopting lamassu's dispense_confirmed / error / error_code taxonomy and its per-bay action log — which the machine already records in cassette_bills and simply never ships. Deviates from lamassu in three places it got wrong or never did: a zero-dispensed report that arrives with an error is treated as unverified, not as zero; a mechanical fault is its own customer screen with evidence and is not "out of cash"; and terminal dispenser faults latch cash-out off until a recount or an explicit operator op, because re-initialising does not move a stuck note. Closes the review loop with ten findings outside the ADR's decisions. Refs #122, #27, #78 --- docs/adr/005-cash-out-dispense-outcome.md | 315 ++++++++++++++++++++++ 1 file changed, 315 insertions(+) create mode 100644 docs/adr/005-cash-out-dispense-outcome.md diff --git a/docs/adr/005-cash-out-dispense-outcome.md b/docs/adr/005-cash-out-dispense-outcome.md new file mode 100644 index 0000000..43378ce --- /dev/null +++ b/docs/adr/005-cash-out-dispense-outcome.md @@ -0,0 +1,315 @@ +# ADR-005: Cash-Out Dispense Outcome and Settlement Capture + +**Status:** Proposed +**Date:** 2026-10-09 +**Context:** On 2026-10-09 a customer paid a 40 EUR cash-out on sintra, a note jammed at the +cassette exit, and the operator dashboard showed the settlement as `processed` with no sign +anything was wrong (aiolabs/bitspire#122). The machine had recorded the failure correctly and +in detail. Nothing it knew ever reached anyone. This ADR specifies how a dispense outcome +becomes a first-class fact on both sides of the wire, and fixes the structural reason the +existing remediation tool could not have helped. + +## The problem + +A cash-out moves value in two steps that today are not connected: + +1. **Payment.** The customer pays the ATM's BOLT11 invoice. LNbits lands it in the machine + wallet, `spirekeeper._handle_payment` verifies attribution, inserts a `dca_settlements` row, + and — in the same breath — spawns `process_settlement` as a background task. +2. **Dispense.** The machine, which learns of the payment through `watchInvoice`, commands the + dispenser. The hardware reports per-bay `dispensed` / `rejected` counts and, on failure, an + error code. + +Step 1 does not wait for step 2. `process_settlement` pays the super fee, the operator's +commission splits, and the DCA legs the moment the payment lands, which on an LNbits-internal +transfer is sub-second. The dispense begins afterwards. So by the time the F56 reported +`78 42` at T+2 s, the settlement's legs were already `completed` and its status was +`processed` — which is what the dashboard faithfully displayed. `processed` means *all +distribution legs paid*. It has never meant *cash reached a hand*, because the server has no +input that could tell it. + +The tool built for this situation, `apply_partial_dispense_and_redistribute`, carries a hard +guard: it refuses once any leg has completed, because a Lightning payment cannot be clawed +back. Under the current ordering that guard is reached on every real failure. The remediation +is structurally unreachable for the exact case it was written for, except by winning a race +against a sub-second transfer. + +Downstream of that, four smaller gaps compound it (all in #122): + +- The machine's `dispenseError` state is terminal and local. No report, no notification. The + only record that a customer is owed money lives in `state.db` on the ATM. +- The dispenser's counters do not see a note that leaves the bay and stops in the transport — + it is neither `dispensed` nor `rejected`. The machine trusts the resulting `dispensed: 0`, + leaves the bay count untouched, and republishes it as fact. The `countsUncertainSince` + safety net fires only when the report is *absent*, not when it is present and wrong. +- Nothing reads dispenser health. The availability beacon derives `cash_out` from + `totalBills > 0` alone and kept advertising a jammed machine as available. (Nothing + consumes that beacon today, so it could not have been the enforcement point in any case.) +- The customer sees a 30-second countdown and a txid QR, then the idle screen. There is no + claim reference, no statement that they have paid, and nothing distinguishes a mechanical + fault from an out-of-cash condition. + +## Prior art + +lamassu-machine / lamassu-server ran this exact hardware in production for a decade. Their +model, which this ADR adopts where it fits and deviates from where it is wrong: + +- **Three fields on the transaction:** `error` (human message), `error_code` (the error's + *name*, machine-readable), `dispense_confirmed` (boolean). `dispense_confirmed` is computed on + **value** — `tx.fiat.eq(Σ denomination × dispensed)` — not taken from the driver. +- **An append-only action log, `cash_out_actions`,** one row per dispense attempt with + per-bay `provisioned_N` / `denomination_N` / `dispensed_N` / `rejected_N`, written by + `logDispense` as `action: 'dispense'` or `'dispenseError'` purely on whether `error` is set. +- **The operator is notified in the same atomic block that logs the dispense** + (`notifyOperator`, cash-out-atomic.js). Push, not a worklist. +- **A mechanical fault is not "out of cash."** Their 2026-09-29 fix routes a + dispenser-reported error to the screen that asks the customer to photograph their receipt, + *"the right prompt when they have paid and are owed money,"* and reserves `outOfCash` for a + shortfall with no error. The same commit removed a borrowed `statusCode 570` from the F56 + driver because 570 meant "insufficient funds" to the server — a jammed BDU was being + reported as a hot-wallet problem. +- **What they did not have:** any notion of disabling a machine on a dispenser fault. + `getMachineStatuses` is inferential — ping age, stuck-screen age — and `cashOut` is an + operator config toggle. A jammed dispenser on a responsive machine reads *Fully + functional*. That is sintra's beacon exactly, so the latch below is new work, not a port. +- **What they got wrong and we will not copy:** `dispenseOccurred(bills)` returns true if the + bill entries merely *have* `dispensed` and `rejected` keys, and `updateCassettes` then + decrements by those numbers. A jam reporting `dispensed: 0` passes and decrements by zero. + Their cassette counts drift the same way sintra's did. + +## Decisions + +### 1. Payment is authorization. Dispense confirmation is capture. Distribution waits for capture. + +A `cash_out` settlement lands as `pending` exactly as now, but `_handle_payment` no longer +spawns `process_settlement` for it. The row moves to a new status, `awaiting_dispense`, and +stays there until the machine reports. + +| Machine reports | Settlement becomes | Then | +| ----------------------------------------- | ------------------ | --------------------------------------- | +| `dispense_confirmed: true` | `pending` | claim + distribute → `processed` | +| partial (some notes out, value short) | `partial_pending` | operator confirms → distribute scaled | +| `dispense_confirmed: false`, nothing out | `cash_owed` | legs never run; funds stay in wallet | +| no report within `DISPENSE_REPORT_TTL` | `dispense_unreported` | worklist; operator investigates | + +This is the card-processing shape — authorize, then capture — and it is the same ordering +lamassu-server enforces between `dispense_confirmed` and `updateCassettes`. The cost is that +operator and DCA legs land seconds later than they do today, which is the dispense time. +The benefit is that `apply_partial_dispense_and_redistribute` is always reachable, because +no leg has run yet, and `cash_owed` is a state the money has not left. + +`cash_in` settlements are unaffected: there is no dispense to wait for, and +`_pay_dca_distributions` already branches on `tx_type` for exactly this kind of asymmetry. + +**Rejected:** keeping immediate distribution and adding a compensating reversal. Internal +legs *are* reversible — they are LNbits-internal invoices, so a compensating internal +payment is mechanically possible and the guard's "Lightning can't be clawed back" is only +true of the `autoforward` leg. But undoing money movement is strictly harder than not +moving it yet, and the autoforward leg stays irreversible either way. Compensation is kept +as a secondary tool for settlements that distributed before this ADR landed. + +### 2. The machine reports every cash-out outcome over a `report_dispense` RPC. + +Not over the kind-30078 state document. ADR-004 established why: an addressable event gives +its publisher no failure signal, and a losing writer is never told. A per-transaction +outcome is an append-only fact that must be acknowledged, which is a request/reply. + +The RPC follows `create_withdraw` and `get_machine_config`: `register_rpc` at +`AUTH_ACCOUNT`, identity taken from the **verified** `sender_pubkey`, never from the body. +It is sent on **success as well as failure** — a success report is what captures (Decision +1). Payload, adopting the lamassu field names: + +```jsonc +{ + "txid": "tx_mv0madw6_wdhtea1v", + "payment_hash": "6f216df32c36…", + "tx_type": "cash_out", + "dispense_confirmed": false, // value equality, Decision 3 + "error": "Dispensing, code: 78 42", // human, null on success + "error_code": "F56DispenseError", // the error's NAME, null on success + "raw_code": "78 42", // driver-native, for the decode table + "error_class": "terminal", // "terminal" | "recoverable" | null, Decision 5 + "fiat_cents": 4000, + "bills": [{ "denomination": 20, "requested": 2, "dispensed": 0, "rejected": 0 }], + "cassettes": [ // the machine's cassette_bills rows, verbatim + { "position": 2, "denomination": 20, "provisioned": 2, "dispensed": 0, "rejected": 0 } + ], + "counts_uncertain": true, // Decision 3 + "at": 1791529353 +} +``` + +**Delivery is at-least-once with a durable outbox.** The machine writes the report to +`state.db` in the same transaction as the `transactions` row (`dispense_reports`: +`txid PRIMARY KEY, payload, created_at, acked_at`), then sends. It resends on boot, on relay +reconnect, and on a timer until an `OK` reply sets `acked_at`. The server upserts on `txid`, +so a resend is a no-op. This is the cassette-ops idempotency pattern applied to the other +direction. + +The server stores every report in an append-only `dispense_reports` table (one row per +attempt, lamassu's `cash_out_actions` shape, keyed to the settlement by `bitspire_txid`, +which is already populated from `extra.txid`), copies the per-bay detail into +`dca_settlements.bills_json` / `cassettes_json` (columns that exist today and are never +written), and sets `dispense_confirmed`, `error`, `error_code` on the settlement. + +### 3. `dispense_confirmed` is computed on value, separately from `error`, and a zero report with an error is unverified. + +On the machine, after the HAL returns: + +``` +confirmed = requestedFiatCents === Σ(denomination × dispensed) × 100 +``` + +`error` is carried independently. The state machine's `dispensingCash.onDone` guard moves +from `output.dispensed === true` to `output.dispenseConfirmed`, and `DispenseCashResult` +gains `dispenseConfirmed`, `errorCode`, `rawCode`, `errorClass`. + +**The deviation from both bitSpire-today and lamassu:** when a report arrives with `error` +set and `dispensed === 0` on every bay, the machine does *not* treat that zero as a count. A +note in the transport path completes neither counter. The machine sets `countsUncertainSince` +exactly as it already does for an absent report, carries `counts_uncertain: true` in the RPC, +and the bay stays flagged until a `recount` op clears it. `atm-reconcile` then shows the gap +instead of a clean ledger. + +### 4. A dispenser fault is its own customer screen, with evidence, and it is not "out of cash." + +Two distinct terminal states replace the single `dispenseError`: + +- **`outOfCash`** — the request could not be met from inventory and the dispenser reported + **no error**. Nothing was charged beyond what was dispensed. +- **`dispenseFault`** — the dispenser reported an error. The customer **has paid** and is + owed the shortfall. + +`dispenseFault` shows: the amount paid, the amount dispensed (per denomination, as now), the +txid as QR (as now) **and as text**, the first 12 characters of the payment hash, the time, +and the sentence *"You have paid. The operator has been notified and holds your transaction +record. Keep this reference."* The raw error code is **not** shown to the customer; it is in +the report. The 30-second auto-return is extended to 120 s and the screen offers "I've saved +this" rather than only "Return to Start." This is lamassu's `fiatTransactionError` prompt +without the receipt camera. + +### 5. Terminal dispenser faults latch cash-out off. A recount or an explicit operator op clears it. + +The HAL classifies each error as `terminal` or `recoverable` (#27's split: jam, motor stop, +diverter and sensor faults, dispense timeout are terminal; pickup error and bill-end are +recoverable). The machine persists `cashOutHeld: { reason, errorCode, since }` in `meta` +when a terminal fault lands, and `SELECT_CASH_OUT` is guarded on it. The idle screen shows +cash-out unavailable with the reason. + +The hold is **not** cleared by re-initialising the dispenser. `dispenseCash` already re-inits +on the next attempt, and re-initialising does not move a note that is stuck. It is cleared +by: + +- a `recount` operator op on any bay — the same "operator opened the machine" gesture that + clears `countsUncertainSince`, so one physical act resolves both; or +- a new `resume_cash_out` operator op, idempotent-id'd like the cassette ops, for the case + where the operator cleared the jam without touching a bay count. + +The machine mirrors the hold into its cassettes-state document (`cash_out_held_since`, +`cash_out_held_reason`) and spirekeeper writes it onto `dca_machines` beside +`counts_uncertain_since`. The availability beacon reports `cash_out: false` while held. + +Separately, the operator gets a manual switch: `cash_out_enabled` on `dca_machines`, +published as an operator op, defaulting true. This is lamassu's `cashOutConfig.active` +shape. Cash-out is offered only when the machine is not held **and** the switch is on. +`dca_machines.is_active` is **not** used for either — it is the roster filter in +`get_machine_by_wallet`, and flipping it makes the machine unknown to the RPC handlers +rather than pausing it. + +### 6. Owed cash is a first-class state on both sides, with an off-machine settle path. + +Server: `cash_owed` and `dispense_unreported` are two new buckets on +`StuckSettlementsResponse`. They are the only buckets whose meaning is *a customer is owed +money*, and they render first. Arrival in either bucket triggers the operator notification +path (whatever `notifyOperator` equivalent spirekeeper grows; at minimum the dashboard banner +— but the push is the point, and it belongs in the same transaction that writes the row). + +Resolution closes **both** ledgers: + +- **On-machine remediation.** `manual_dispense` with `ref_txid` already flips the machine row + to `remediated` via `remediateTransaction`. The machine sends a `report_dispense` for the + remediation with `remediates_txid`, and the server moves the settlement from `cash_owed` to + `pending` and distributes. +- **Off-machine settlement.** The operator paid the customer by hand. A new `settle_cash_owed` + operator action records provenance (free text, author, time) on the settlement, moves it to + `pending`, and publishes a `settle_transaction { txid, note }` operator op; the machine + applies it by setting `remediated_by` to the note and `status = 'remediated'`. Today there is + no way to record this at all, and the machine's ledger asserts the debt forever. + +`PartialDispenseData` is pre-filled from the report's `bills` so the operator confirms a +number the hardware produced rather than typing one. + +### 7. Raw codes get a decode table in the driver, built empirically. + +`packages/hal` owns a `rawCode → { errorCode, errorClass, human }` table per dispenser. It is +seeded with what has been observed — `78 42` on an F56 is a note stopped at the cassette +exit (sintra, 2026-10-09) — and grows as codes occur; an unknown code reports as +`F56DispenseError` / `terminal` / `"unrecognised dispenser error "`, failing safe. No +code is borrowed from another layer's vocabulary (the lamassu 570 lesson). + +## Consequences + +- One cash-out now produces one `report_dispense`; the server's `dispense_reports` table is + the audit trail, and `atm-reconcile`'s natural sibling is a settlement↔transaction + reconciliation that joins on `txid` and flags any settlement without a report. +- Distribution for `cash_out` is delayed by the dispense (seconds). Operators watching the + dashboard will see `awaiting_dispense` briefly on every sale. +- `apply_partial_dispense_and_redistribute`'s hard guard still exists but is reached only for + settlements that pre-date this ADR; its message should say which leg type blocked it. +- The state machine gains `dispenseFault`, `outOfCash` and a `cashOutHeld` guard; tests in + `packages/state-machine` cover all three (cash-out is the critical path). +- A machine on an old build keeps working: the server treats a `cash_out` settlement with no + report after `DISPENSE_REPORT_TTL` as `dispense_unreported`, not as failed, and the operator + can capture manually. That is also the upgrade path. + +## Rollout + +1. **bitspire:** `DispenseCashResult` gains the new fields; HAL computes `dispenseConfirmed`, + classifies, decodes; `dispensingCash` guards on it; `dispenseFault` / `outOfCash` screens; + `dispense_reports` outbox + `report_dispense` client; `cashOutHeld` latch and guard; + `counts_uncertain` on zero-with-error. Ships first — with no server handler the RPC + returns an error and the outbox simply retries, so the machine is never blocked on it. +2. **spirekeeper:** `report_dispense` handler + `dispense_reports` table; `awaiting_dispense` / + `cash_owed` / `partial_pending` / `dispense_unreported` statuses; gate in `_handle_payment`; + worklist buckets + notification; `settle_cash_owed`; `cash_out_enabled` and the two new + operator ops; `dca_machines.cash_out_held_*`. +3. **Both:** `resume_cash_out` / `settle_transaction` ops on the machine; beacon reflects + held; docs: `docs/nostr-patterns` entry for the outbox-RPC pattern, this ADR → Accepted. + +## Review findings outside this ADR + +Found while tracing the money path end to end for this document. Not decided here; each is +a candidate issue. + +1. **The settlement push did not deliver — `[ATM Service] Invoice paid (poll)!`** The + `subscribe_payments` stream missed the payment and the polling fallback caught it. Worth + knowing before relying on push latency anywhere; related to #78. +2. **`waitingForCashTaken` auto-advances to `complete` after 30 s "assume taken."** A + transaction can be recorded complete with notes still in the slot. The HAL already blocks + in `waitForBillsRemoved` inside `dispenseCash`, so the state is doing a second, weaker + version of the same job. +3. **The machine and server keep two ledgers with no reconciliation.** `transactions` and + `dca_settlements` join on `txid` today and nothing ever joins them. Decision 2 gives the + server everything a reconciliation needs. +4. **The availability beacon has no consumers and overlaps the cassettes-state document.** + Two machine-authored state documents with overlapping fields is drift waiting to happen. + Either fold availability into the cassettes-state doc or give the beacon a reader. +5. **`is_active` reads as a service gate and is a roster flag.** Rename to something like + `enrolled`, or document at the column. +6. **`generateInvoice` carries a comment deferring `bills`/`cassettes` onto the invoice + `extra`.** With Decision 2 that would be the wrong place — provisioned is not dispensed — + and the comment invites a future contributor to wire it there. Remove it. +7. **The HAL's `dispensed` boolean is count-based (`totalRequested === totalDispensed`), + not value-based.** Equivalent only while each bay dispenses its own denomination. + Decision 3 replaces it. +8. **`_handle_payment` processes cash-in and cash-out through one path and only `tx_type` + tells them apart.** Decision 1 adds a second direction-specific branch. If a third + arrives, split the handler. +9. **The partial-dispense guard's message is wrong for internal legs.** "Lightning payments + can't be clawed back" is true of `autoforward` and false of the LNbits-internal legs, + which are compensatable. Make the guard leg-aware or correct the message. +10. **Review scope.** This document traced the cash-out path: state machine → HAL → ledger → + transport → settlement → distribution → dashboard. The cash-in path shares the settlement + pipeline and has its own money-at-risk shape in `create_withdraw` (server-side amounts, + `max_cash_in_sats`); it has not been reviewed to the same depth and is the obvious next + slice. The HAL drivers, access layer and deploy module were not in scope. From f498373e9682a581f30e7f80f8257406c8d90385 Mon Sep 17 00:00:00 2001 From: Padreug Date: Fri, 9 Oct 2026 21:42:03 +0200 Subject: [PATCH 04/15] docs(claude): record the lamassu reference permission; correct the server boundary and the driver table MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Three corrections to facts a session reads before touching code. The maintainer taking over the Lamassu codebase has given permission to use lamassu-machine and lamassu-server, post-boundary included, as prior art (relayed by padreug, 2026-10-09). The hard rule that limited us to c0b69d1 is superseded; the guidance is now reference-over-port, with verbatim ports naming their source commit. lamassu-server DID have a public-domain era — last open commit adbc9709, licence added in d06a8f54, both 2023-09-19 — contrary to what the squashed ~/lamassu/lamassu-server checkout suggests (its first commit is already Appendix A). History survives in ~/dev/repos/ and at Software Heritage. The earlier "not re-verified" note is replaced with the verified boundary. The hardware-driver table claimed ccnet, cashflow_sc, bnr_advance, genmega, hcm2, gsr50 and three printers. The tree has validators id003 and ebds, dispensers f56 and puloon, no printers directory, and orphaned Rust files from an abandoned HAL. The table now matches the tree. --- CLAUDE.md | 35 +++++++++++++++++++++++++++++------ 1 file changed, 29 insertions(+), 6 deletions(-) diff --git a/CLAUDE.md b/CLAUDE.md index 2cc2db7..31c8393 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -17,9 +17,27 @@ Core principles: The HAL drivers (validators / dispensers / printers) and the cash-flow state machine derive from Lamassu Industries AG's `lamassu-machine` repository, **only up to commit `c0b69d1ed196d396c5f057478c2ea290babd58ab`** ("chore: v8.6.0-beta.9", 2023-09-19) — the last commit published into the public domain (`UNLICENSE` in tree). The very next commit, `a9234d124d` ("chore: add LICENSE (#1019)", 2023-09-19), removed `UNLICENSE` and added Lamassu's proprietary "Appendix A SLA". **The `v8.1.5` tag (2023-09-21) already ships the Appendix A license** — the previously documented "8.1.5 is the open boundary" was wrong (verified against GitHub history 2026-07-04). Note the public-domain boundary sits on the 8.6-beta line, which is *further along* than 8.1.5 feature-wise. -**Hard rule when working in this repo:** do not pull, port, or copy lamassu-machine code from `a9234d124d` or later (which includes every 8.1.5+ tag). Reference only `c0b69d1` or earlier. If a HAL bug fix or feature exists upstream past that commit, either (a) reimplement from protocol docs / hardware specs without looking at the licensed source, or (b) raise the question with the maintainer first. To fetch the open tree safely: `git fetch --depth 1 origin c0b69d1ed196d396c5f057478c2ea290babd58ab` — never check out a tag. +**Reference permission (2026-10-09).** The maintainer taking over the Lamassu +codebase has given us permission to use lamassu-machine and lamassu-server — including +post-boundary code — as prior art in any way that improves this codebase (relayed by +padreug, 2026-10-09; the earlier hard rule — reference only `c0b69d1` or earlier — is +superseded). Prefer to *reference* over +*port*: read it, take the behaviour model, reimplement in our idiom. When a block is +ported verbatim, say so in the commit, with the source commit, so the provenance is in +`git log`. The pre-boundary tree needs no permission at all and is the first place to look. -The `lamassu-server` boundary has not been re-verified against its own history and may differ — check its license-change commit before referencing it. +**Boundaries, for the record.** lamassu-machine's last public-domain commit is `c0b69d1` +(2023-09-19, v8.6.0-beta.9); `a9234d124d` added Appendix A the same day, so every 8.1.5+ +tag is proprietary. lamassu-server's last public-domain commit is `adbc9709` (2023-09-19, +v8.6.0-beta.9), licence added in `d06a8f54` the same day. Both GitHub repos are gone +(404); full history survives in `~/dev/repos/` and at Software Heritage (crawls 2026-03-02 +and 2026-07-19, tips identical to the local mirrors). **`~/lamassu/lamassu-server` is a +squashed v12 repo** whose first commit (`e2c49ea`, 2025-12-31) already carries Appendix A — +it has no open era to reference; use `~/dev/repos/` for that. `~/lamassu/` also holds the +33 surviving github.com/lamassu dependency repos (cloned 2026-09-27) and +`bnr-xfs-salvage/` (the MIT bnr-xfs / bnr crates, checksums verified). The curated +*Lamassu Port Backlog* (claude.ai artifact `61af38f6`, 2026-09-27) ranks what is worth +taking and tags each item's provenance. bitSpire is an independent project under AGPL-3.0 and is not affiliated with Lamassu Industries AG. @@ -232,10 +250,15 @@ TypeScript drivers in `packages/hal/`. Coverage by device class: | Category | Drivers | |---|---| -| Validators | id003, ccnet, cashflow_sc, bnr_advance, genmega, hcm2, gsr50 | -| Dispensers | puloon, f56, genmega, hcm2, gsr50 | -| Recyclers | MEI SCR (planned — hardware in BATM3, no driver yet) | -| Printers | nippon, zebra, genmega | +| Validators | id003, ebds | +| Dispensers | f56, puloon | +| Recyclers | none — MEI SCR hardware in BATM3; a clean-room BNR Advance route exists via the MIT `bnr-xfs` crate (see the port backlog) | +| Printers | none — `packages/hal/src/printers/` does not exist | + +This table previously listed ccnet, cashflow_sc, bnr_advance, genmega, hcm2, gsr50 and +three printers that are not in the tree (corrected 2026-10-09). `packages/hal/src` also +carries orphaned `*.rs` files (`lib.rs`, `error.rs`, `mod.rs`, `traits.rs`, `mock.rs`) from +an abandoned Rust HAL; they are not built. ### Sintra hardware specifics (Aaeon UP Board) From 6a974054ce8a51b7100a785879b21b81d43d80a8 Mon Sep 17 00:00:00 2001 From: Padreug Date: Fri, 9 Oct 2026 21:42:03 +0200 Subject: [PATCH 05/15] =?UTF-8?q?docs(adr):=20ADR-005=20=E2=80=94=20future?= =?UTF-8?q?=20directions=20and=20the=20operator-docs=20gap?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Records where the cash-out design is heading so Decisions 1–7 are made with the destination in view: - Hold invoices move authorize/capture from spirekeeper into the Lightning layer. LNbits core already has create/settle/cancel (lndrest + lndgrpc only; not yet on the nostr-transport). Settlement is all-or-nothing per HTLC, so a full fault cancels cleanly but a partial fault still needs a voucher — hold invoices remove owed-cash for the common case, not every case. - Vouchers: a fiat-denominated claim at the original rate, a liability row linked to its origin settlement, whose undispensed sats stay undistributed until redemption or expiry. - CLINK as the eventual favoured customer protocol; the availability beacon should align with the CLINK Beacon spec rather than grow a third shape. - Operator notification is a Nostr event to the operator's pubkey, not email/SMS. The pubkey is already on the LNbits account; nsecbunkerd supports nip44 so no operator ever needs their nsec. - An operator-facing error glossary, seeded from the F56-BDU error list and this incident's 78 42. - Cross-reference to the Lamassu Port Backlog, and the finding that bitSpire carried lamassu's narrow GTQ window byte for byte. - Bay layout is machine-authoritative (VITE_LAMASSU_CASSETTES on first boot, then state.db); spirekeeper adopts and deletes absent positions; no operator document describes any of it, and fleet targets are keyed by hostname so a second Tejo cannot join without a new flake target. Refs #122 --- docs/adr/005-cash-out-dispense-outcome.md | 164 ++++++++++++++++++++++ 1 file changed, 164 insertions(+) diff --git a/docs/adr/005-cash-out-dispense-outcome.md b/docs/adr/005-cash-out-dispense-outcome.md index 43378ce..6740b96 100644 --- a/docs/adr/005-cash-out-dispense-outcome.md +++ b/docs/adr/005-cash-out-dispense-outcome.md @@ -276,6 +276,170 @@ code is borrowed from another layer's vocabulary (the lamassu 570 lesson). 3. **Both:** `resume_cash_out` / `settle_transaction` ops on the machine; beacon reflects held; docs: `docs/nostr-patterns` entry for the outbox-RPC pattern, this ADR → Accepted. +## Future directions + +Recorded 2026-10-09 so the decisions above are made with the destination in view. None of +these are decided; several would change what "owed cash" even means. + +### Hold invoices — capture at the Lightning layer instead of the application layer + +Decision 1 implements authorize/capture in spirekeeper because a plain BOLT11 payment is +final the moment it lands. A **hold (HODL) invoice** moves that boundary into the protocol: +the payer's HTLC is accepted but not settled until the receiver reveals the preimage, and +can be cancelled instead, returning the funds with no second payment. The ATM would mint the +preimage, create the hold invoice, dispense, and then `settle` on `dispense_confirmed` or +`cancel` on a fault. A cancelled hold means nobody is owed anything — the customer's funds +were never taken. + +What is already there: LNbits core has `create_hold_invoice`, `settle_hold_invoice(preimage)` +and `cancel_hold_invoice` (`lnbits/core/services/payments.py`), tagging `extra.hold_invoice`. +It is implemented for the **lndrest and lndgrpc** funding sources only; other backends raise +*"Hold invoices are not supported by the funding source."* None of the three is exposed over +the nostr-transport yet, so three RPCs are needed before the machine can use them. Spark's +`createLightningHodlInvoice({ amountSats, paymentHash, … })` and RoboSats' escrow bonds are +the reference shapes — the payer-visible behaviour (a pending payment that later settles or +cancels) is identical. + +Two properties bound what this buys: + +- **Settlement is all-or-nothing per HTLC.** A hold invoice cannot be partially settled. A + **full** fault (nothing dispensed) is cleanly cancelled. A **partial** fault — notes out, + value short — cannot be: cancelling would refund a customer who is holding cash, and + settling takes the full amount. Partial therefore still needs the voucher or refund path + below. Hold invoices eliminate owed-cash for the common full-fault case, not for every case. +- **The hold window locks the payer's funds and route liquidity**, and some wallets surface + a long-pending payment as a failure. The window should equal the dispense window — seconds, + capped at a minute or two — with an automatic `cancel` on timeout, never an open-ended hold. + +Where it lands in this ADR: a settlement created from a held payment reaches +`_handle_payment` only on **settle** (`payment.success` is false while held), so for hold-paid +transactions the `awaiting_dispense` state in Decision 1 is unnecessary — the Lightning layer +is already waiting. Decision 1 stays for plain BOLT11 and for any CLINK path that resolves to +an ordinary invoice. The two coexist; `report_dispense` (Decision 2) is what triggers the +settle/cancel either way. + +### Vouchers — a fiat-denominated claim instead of a debt + +For the shortfall a hold invoice cannot refund, and for any dispense failure on a plain +invoice, the customer could be issued a **voucher**: a claim on the operator for *X fiat value +of notes*, redeemable at a machine at a later date **at the original exchange rate**, +independent of the BTC price at redemption and requiring no further Lightning payment. The +machine prints or displays it; redemption is a cash-out whose "payment" is the voucher. + +Vouchers could also be a general product — buy X fiat value now, collect later — but the +first use is fault recovery. Accounting constraints that must hold whichever form ships: + +- A voucher is a **liability** row on the server, linked to its origin `txid` / settlement + when it has one (nullable for the general case), with the fiat amount, the locked rate, the + issuing machine and an expiry. +- The sats the origin settlement received for the undispensed portion must **not** be + distributed while the voucher is open: redemption or expiry is what releases them, so the + operator never pays out commission on cash that has not left a machine. This is the same + rule as Decision 1 applied over a longer window. +- Redemption records a `cash_out` with `tx_type = 'voucher_redeem'`, `wire_sats = 0`, and a + reference to the voucher; the machine's own ledger treats it as a dispense like any other + (bays decrement, `cassette_bills` written, `report_dispense` sent). +- A voucher is a bearer instrument unless bound to a pubkey. Both are possible; the choice + decides whether a lost voucher is lost money. + +Taken together, hold invoices plus vouchers could remove the *owed cash* state entirely: +full fault → cancel, nobody pays; partial fault → settle, voucher for the difference; and +`cash_owed` in Decision 6 becomes the fallback for a plain-invoice path, not the norm. + +### CLINK — the protocol this machine is moving toward + +bitSpire is a Nostr machine and the long-term direction is to implement, and eventually +favour, Shocknet's **CLINK** (Common Lightning Interface for Nostr Keys) for customer-facing +payment flows: kind 21001 Offers (`noffer`), 21002 Debits (`ndebit`), 21003 Manage, 21004 +Enroll, and the **CLINK Beacon** — a kind-30078 service heartbeat carrying liveness, persona +and fee disclosure. Reference tree: `~/dev/refs/repos/shocknet/shocknet/{CLINK,ClinkSDK, +clink-demo,Lightning.Pub}`. The `@bitSpire/clink` package is in tree and dormant. + +Two consequences for this ADR. First, BOLT11 stays alongside CLINK rather than being +replaced, so Decisions 1–2 remain load-bearing for the invoice path. Second, review finding 4 +below — the availability beacon has no readers and overlaps the cassettes-state document — +should be resolved by aligning the beacon with the **CLINK Beacon** spec rather than by +inventing a third shape: a machine advertising itself to Nostr clients should do so in the +form those clients will read. + +### Operator notification is Nostr-native + +Decision 6 calls for the operator to be notified when a settlement reaches `cash_owed`. +That notification is **a Nostr event to the operator's pubkey**, not email or SMS (the +lamassu `notifyOperator` channel). The pieces exist: + +- The operator's pubkey is already on their LNbits account — `get_machine_config` refuses to + run without it ("operator has no Nostr pubkey on file"). +- The sender signs through the bunker (`sign_as_operator` / `resolve_operator_signer` in + `nostr_publish.py`), so no key is at rest on the server. A dedicated server identity for + alerts is preferable to the operator messaging themself. +- The operator does **not** need their private key to read it. nsecbunkerd supports + `nip44_encrypt` / `nip44_decrypt` for its users (see its ACL tests), so any NIP-46 client — + our webapp, Amber, nsec.app — decrypts the message through the bunker. nsecbunkerd does hold + a `decryptNsec` path, but it is the bunker *admin's*, gated on the passphrase; exposing it to + operators would reverse the "no nsec outside the bunker" principle this stack is built on. +- Operators may still want alerts on a phone identity that is not their LNbits pubkey. An + optional per-operator `alerts_pubkey` covers that without changing the default. + +NIP-17 (kind 14 in a kind-1059 gift wrap) is the right wire for metadata privacy; NIP-04 is an +acceptable interim if the receiving clients are ours. Pick one in the notification issue. + +### An operator-facing error glossary + +Every `error_code` surfaced by Decisions 2 and 7 should link to a glossary entry: what the +code means, what the operator will find when they open the machine, what clears it, and +whether it is `terminal` or `recoverable`. The authoritative source for the F56 is the Fujitsu +Frontech **F56-BDU Error Code List** (K3KD03234–K3KD03236-0001, edition E02), per the Lamassu +port backlog; it is not held locally yet and should be obtained. Seed entries, from the +backlog and from this incident: + +| raw | meaning (F56-BDU) | class | observed | +| ------- | --------------------------------- | ----------- | ------------------------- | +| `78 42` | note stopped at the cassette exit | terminal | sintra, 2026-10-09 | +| `82 00` | bill length — long | recoverable | Tejo GTQ, 2026-09-26 | +| `83 00` | bill length — short | recoverable | | +| `84 00` | bill thickness | recoverable | | +| `85 0n` | pick from another safe | recoverable | | +| `86 00` | bill spacing | recoverable | | +| `B5 ..` | reject box overflow | terminal | | + +The glossary lives in `docs/` and is served by spirekeeper so the dashboard can deep-link +from a report. Neither lamassu codebase ever decoded these bytes. + +### The Lamassu port backlog + +A provenance-gated analysis of what bitSpire and spirekeeper can take from lamassu-machine +and lamassu-server exists as *Lamassu Port Backlog* (27 Sep 2026, claude.ai artifact +`61af38f6`). Items that bear directly on this ADR: **structured fault reporting** +(`routes/diagnosticsRoutes.js` → `machine-loader.updateDiagnostics` — a fault record carrying +the driver error, the raw device frame and cassette state at failure, which is `report_dispense` +by another name); the **interactive hardware test harness** (`lib/hardware-testing/`, an F56 +dispense-and-count case is the obvious first addition); the **denomination solver** +(`lib/coin-change.js`, portable from its upstream `git.sr.ht/~siiky/coin-change`); and the +ID003 startup and hang fixes. The backlog also records that bitSpire carried lamassu's narrow +GTQ note-length window byte for byte — fixed on `dev` alongside this ADR, mirroring +lamassu-machine `b1cc3622`. + +### Bay layout, machine identity, and the operator docs that do not exist yet + +How many bays a machine has is **machine-authoritative**: on first boot the layout comes from +`VITE_LAMASSU_CASSETTES` in the machine's `.env` (documented in `docs/device-configuration.md` +— an installer document, with a stale `LAMASSU` prefix), after which `state.db` owns it and +the operator adjusts counts and denominations by publishing ops. spirekeeper adopts whatever +the machine reports: `apply_reported_state` treats the payload as the full bay set and +*deletes* positions the machine no longer reports ("the bay count is hardware-determined"). +There is **no operator-facing document** describing any of this — spirekeeper's README still +describes the satmachineadmin era and has no setup section — so an operator with a two-bay +Tejo and one with a four-bay Tejo have no page telling them where the difference is set. + +That gap is one face of a larger one: every fleet target in `flake.nix` is a **specific +machine** (fiat, upgrade timer, card reader, address are keyed by hostname), so a second Tejo +cannot join the fleet by pointing at the same flake. Generalising the install means splitting +**model** (the hardware preset: `sintra`, `tejo`, `douro`, `batm3`) from **identity** (the +per-install provisioning: seed, cassettes, fiat, network), with the latter arriving through +pairing and operator ops rather than through Nix. The operator docs should be written against +that split, not the current one. + ## Review findings outside this ADR Found while tracing the money path end to end for this document. Not decided here; each is From aa488c0df45fefc1bf51faaa35a83c21e4614eb6 Mon Sep 17 00:00:00 2001 From: Padreug Date: Fri, 9 Oct 2026 21:42:36 +0200 Subject: [PATCH 06/15] fix(hal): widen the GTQ F56 note-length window to match USD/HNL MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Every quetzal note is 156 x 67 mm — physically the same note as USD and HNL, which the F56 table accepts at 146–166 (±10). GTQ was configured at ±5 (151–161), with Q5 and Q20 further centred on 158 rather than 156. This table was carried byte for byte from lamassu-machine, narrow window included. On a Tejo in GTQ it produced F56 error 82 00 (bill length, long) on every Q100 pick — 5/5 notes rejected, 0 dispensed, a false "out of cash" — byte-identical across transactions days apart, so the BDU was reading >161 mm consistently. The reject tray held single notes, not pairs, which rules out the offset double-pick the narrow window exists to catch. Every denomination now uses 0xa6 0x92 (146–166), identical to USD/HNL. Mirrors lamassu-machine b1cc3622 (2026-09-29), ported with permission as prior art. Verification: tsc clean. packages/hal has no test files (vitest exits 1, "No test files found") — recorded as ADR-005 review finding 10. --- packages/hal/src/dispensers/f56/bills.ts | 19 +++++++++++++------ 1 file changed, 13 insertions(+), 6 deletions(-) diff --git a/packages/hal/src/dispensers/f56/bills.ts b/packages/hal/src/dispensers/f56/bills.ts index dad459c..299b688 100644 --- a/packages/hal/src/dispensers/f56/bills.ts +++ b/packages/hal/src/dispensers/f56/bills.ts @@ -77,13 +77,20 @@ export const bills: Record = { }, GTQ: { thickness: 0x0c, + // Every quetzal note is 156 x 67 mm — physically the same as USD and + // HNL, which accept 146–166 (±10). This table inherited lamassu's ±5 + // window (151–161) with Q5/Q20 centred on 158, and a Tejo in GTQ rejected + // 5/5 notes on every Q100 dispense with F56 error 82 00 (bill length, + // long): the BDU read >161 mm on every pick, and the reject tray held + // single notes, not pairs — so it was the window, not a double-pick. + // Widened to match USD/HNL, mirroring lamassu-machine b1cc3622. lengths: { - 5: [0xa3, 0x99], - 10: [0xa1, 0x97], - 20: [0xa3, 0x99], - 50: [0xa1, 0x97], - 100: [0xa1, 0x97], - 200: [0xa1, 0x97], + 5: [0xa6, 0x92], + 10: [0xa6, 0x92], + 20: [0xa6, 0x92], + 50: [0xa6, 0x92], + 100: [0xa6, 0x92], + 200: [0xa6, 0x92], }, polymer: false, }, From d569e4013e29e68f09d738574e6c268fd83aeb6a Mon Sep 17 00:00:00 2001 From: Padreug Date: Fri, 9 Oct 2026 21:43:00 +0200 Subject: [PATCH 07/15] =?UTF-8?q?docs(adr):=20ADR-005=20=E2=80=94=20record?= =?UTF-8?q?=20that=20packages/hal=20has=20no=20tests=20(finding=2010)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- docs/adr/005-cash-out-dispense-outcome.md | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/docs/adr/005-cash-out-dispense-outcome.md b/docs/adr/005-cash-out-dispense-outcome.md index 6740b96..c68a89d 100644 --- a/docs/adr/005-cash-out-dispense-outcome.md +++ b/docs/adr/005-cash-out-dispense-outcome.md @@ -472,7 +472,12 @@ a candidate issue. 9. **The partial-dispense guard's message is wrong for internal legs.** "Lightning payments can't be clawed back" is true of `autoforward` and false of the LNbits-internal legs, which are compensatable. Make the guard leg-aware or correct the message. -10. **Review scope.** This document traced the cash-out path: state machine → HAL → ledger → +10. **`packages/hal` has no tests.** `pnpm test` there exits 1 with "No test files found." + The F56 note-length table that produced a production fault on a GTQ Tejo was carried + byte for byte from lamassu with nothing over it; the fix landed the same way. A table test + asserting every currency's window is centred on its note length is a few lines, and + `dispenseConfirmed` (Decision 3) needs the harness to exist before it can be tested. +11. **Review scope.** This document traced the cash-out path: state machine → HAL → ledger → transport → settlement → distribution → dashboard. The cash-in path shares the settlement pipeline and has its own money-at-risk shape in `create_withdraw` (server-side amounts, `max_cash_in_sats`); it has not been reviewed to the same depth and is the obvious next From bf2b9427aa7f2e464653b16f9ebbba076bf16eb3 Mon Sep 17 00:00:00 2001 From: Padreug Date: Fri, 9 Oct 2026 21:57:38 +0200 Subject: [PATCH 08/15] refactor(config): rename VITE_LAMASSU_* machine-config env vars to VITE_BITSPIRE_* MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit MACHINE_MODEL, FIAT_CODE, VALIDATOR_DEVICE, DISPENSER_DEVICE and CASSETTES carried the old brand in their names. Renamed everywhere they are read (device.ts, electron/main.ts), written (flake.nix, mkAtmApp.nix, live.nix, provision-atm.sh, factory-reset-atm.sh) and documented (.env.example, docs/device-configuration.md). No compatibility fallback in code: the machine reads VITE_BITSPIRE_* and nothing else. The deployed .env files are the one place the old names persist — sintra's /var/lib/bitspire/.env holds all three keys today — and the machine reads MACHINE_MODEL / FIAT_CODE / CASSETTES from that file on every boot. Renaming the keys in code alone would boot a live machine on preset defaults (wrong bays, wrong fiat) at the next nightly pull. So configuration.nix gains an activation script, beside the existing lamassu→bitspire user migration, that rewrites VITE_LAMASSU_* → VITE_BITSPIRE_* in that file. Idempotent; runs before bitspire.service starts. --- apps/machine/.env.example | 10 ++++----- apps/machine/electron/main.ts | 20 +++++++++--------- apps/machine/src/config/device.ts | 34 +++++++++++++++---------------- deploy/nixos/configuration.nix | 16 +++++++++++++++ deploy/nixos/factory-reset-atm.sh | 8 ++++---- deploy/nixos/live.nix | 4 ++-- deploy/nixos/provision-atm.sh | 4 ++-- docs/device-configuration.md | 16 +++++++-------- flake.nix | 4 ++-- nix/mkAtmApp.nix | 4 ++-- 10 files changed, 68 insertions(+), 52 deletions(-) diff --git a/apps/machine/.env.example b/apps/machine/.env.example index 153066b..792fddd 100644 --- a/apps/machine/.env.example +++ b/apps/machine/.env.example @@ -6,17 +6,17 @@ # ============================================================================= # Machine model preset (sintra, gaia, or custom) -VITE_LAMASSU_MACHINE_MODEL=sintra +VITE_BITSPIRE_MACHINE_MODEL=sintra # Fiat currency code (ISO 4217) -VITE_LAMASSU_FIAT_CODE=USD +VITE_BITSPIRE_FIAT_CODE=USD # Custom device paths (optional - uses preset defaults if not set) -# VITE_LAMASSU_VALIDATOR_DEVICE=/dev/ttyJ5 -# VITE_LAMASSU_DISPENSER_DEVICE=/dev/ttyJ7 +# VITE_BITSPIRE_VALIDATOR_DEVICE=/dev/ttyJ5 +# VITE_BITSPIRE_DISPENSER_DEVICE=/dev/ttyJ7 # Cassette configuration (optional - JSON array) -# VITE_LAMASSU_CASSETTES='[{"denomination":20,"count":100}]' +# VITE_BITSPIRE_CASSETTES='[{"denomination":20,"count":100}]' # ============================================================================= # LNbits Connection (dev override — normally seed-provided) — nostr-native-transport diff --git a/apps/machine/electron/main.ts b/apps/machine/electron/main.ts index 9094eb7..7a87ac2 100644 --- a/apps/machine/electron/main.ts +++ b/apps/machine/electron/main.ts @@ -355,11 +355,11 @@ ipcMain.handle('get-config', () => { appId: process.env.VITE_APP_ID || '', // Hardware configuration - machineModel: process.env.VITE_LAMASSU_MACHINE_MODEL || 'sintra', - fiatCode: process.env.VITE_LAMASSU_FIAT_CODE || 'USD', - validatorDevice: process.env.VITE_LAMASSU_VALIDATOR_DEVICE, - dispenserDevice: process.env.VITE_LAMASSU_DISPENSER_DEVICE, - cassettes: process.env.VITE_LAMASSU_CASSETTES, + machineModel: process.env.VITE_BITSPIRE_MACHINE_MODEL || 'sintra', + fiatCode: process.env.VITE_BITSPIRE_FIAT_CODE || 'USD', + validatorDevice: process.env.VITE_BITSPIRE_VALIDATOR_DEVICE, + dispenserDevice: process.env.VITE_BITSPIRE_DISPENSER_DEVICE, + cassettes: process.env.VITE_BITSPIRE_CASSETTES, // SECURITY: In production (packaged app), mock fallback is always disabled. // Only allow it in development mode, and only when explicitly opted in via env. allowMockFallback: isDev && process.env.VITE_ALLOW_MOCK_FALLBACK === 'true', @@ -836,7 +836,7 @@ function startCommandPoller(): void { const result = await halInstance.dispenseCash(parsed.bills) const txid = `manual-${Date.now()}-${Math.random().toString(36).slice(2, 8)}` const totalFiatCents = parsed.bills.reduce((s, b) => s + b.denomination * b.count * 100, 0) - const fiatCode = process.env.VITE_LAMASSU_FIAT_CODE || 'USD' + const fiatCode = process.env.VITE_BITSPIRE_FIAT_CODE || 'USD' recordTransaction({ txid, @@ -908,19 +908,19 @@ app.whenReady().then(() => { if (existing.length === 0) { let seedCassettes: { denomination: number; count: number }[] = [] - // Priority 1: explicit VITE_LAMASSU_CASSETTES env var - const cassettesJson = process.env.VITE_LAMASSU_CASSETTES + // Priority 1: explicit VITE_BITSPIRE_CASSETTES env var + const cassettesJson = process.env.VITE_BITSPIRE_CASSETTES if (cassettesJson) { try { seedCassettes = JSON.parse(cassettesJson) } catch (e) { - console.warn('[Electron] Failed to parse VITE_LAMASSU_CASSETTES:', e) + console.warn('[Electron] Failed to parse VITE_BITSPIRE_CASSETTES:', e) } } // Priority 2: default presets per model if (seedCassettes.length === 0) { - const model = process.env.VITE_LAMASSU_MACHINE_MODEL || 'sintra' + const model = process.env.VITE_BITSPIRE_MACHINE_MODEL || 'sintra' const presets: Record = { douro: [ { denomination: 100, count: 50 }, diff --git a/apps/machine/src/config/device.ts b/apps/machine/src/config/device.ts index cffdaa3..ab7586d 100644 --- a/apps/machine/src/config/device.ts +++ b/apps/machine/src/config/device.ts @@ -6,7 +6,7 @@ * * Configuration priority (highest to lowest): * 1. Runtime overrides (passed directly to functions) - * 2. Environment variables (LAMASSU_*) + * 2. Environment variables (BITSPIRE_*) * 3. Machine preset defaults */ @@ -143,7 +143,7 @@ export const MACHINE_PRESETS: Record = {} // Validator device override (preserve validator type from preset) - const validatorDevice = import.meta.env.VITE_LAMASSU_VALIDATOR_DEVICE + const validatorDevice = import.meta.env.VITE_BITSPIRE_VALIDATOR_DEVICE if (validatorDevice) { overrides.validator = { type: MACHINE_PRESETS[model].validator.type, device: validatorDevice } } // Dispenser device override - const dispenserDevice = import.meta.env.VITE_LAMASSU_DISPENSER_DEVICE + const dispenserDevice = import.meta.env.VITE_BITSPIRE_DISPENSER_DEVICE if (dispenserDevice) { overrides.dispenser = { type: MACHINE_PRESETS[model].dispenser.type, @@ -248,7 +248,7 @@ export function loadDeviceConfigFromEnv(): DeviceConfig { } // Cassettes override (JSON) - const cassettesJson = import.meta.env.VITE_LAMASSU_CASSETTES + const cassettesJson = import.meta.env.VITE_BITSPIRE_CASSETTES if (cassettesJson) { try { const cassettes = JSON.parse(cassettesJson) as CassetteConfig[] @@ -262,7 +262,7 @@ export function loadDeviceConfigFromEnv(): DeviceConfig { } } } catch (e) { - console.error('[Config] Failed to parse LAMASSU_CASSETTES:', e) + console.error('[Config] Failed to parse BITSPIRE_CASSETTES:', e) } } diff --git a/deploy/nixos/configuration.nix b/deploy/nixos/configuration.nix index 458a239..f016179 100644 --- a/deploy/nixos/configuration.nix +++ b/deploy/nixos/configuration.nix @@ -479,6 +479,22 @@ in ''; }; + # In-place rename migration: VITE_LAMASSU_* → VITE_BITSPIRE_* in the + # provisioned .env. The machine reads MACHINE_MODEL / FIAT_CODE / CASSETTES + # from this file on every boot; renaming the keys in code without renaming + # them here would boot a live machine on preset defaults (wrong bays, wrong + # fiat) at the next nightly pull. Idempotent: a migrated file has nothing + # left to match. Runs before bitspire.service starts. + system.activationScripts.bitspire-env-migration = { + deps = [ "users" ]; + text = '' + if [ -f /var/lib/bitspire/.env ] && grep -q '^VITE_LAMASSU_' /var/lib/bitspire/.env; then + sed -i 's/^VITE_LAMASSU_/VITE_BITSPIRE_/' /var/lib/bitspire/.env + echo "bitspire: migrated VITE_LAMASSU_* keys in /var/lib/bitspire/.env" + fi + ''; + }; + # Journal configuration services.journald = { extraConfig = '' diff --git a/deploy/nixos/factory-reset-atm.sh b/deploy/nixos/factory-reset-atm.sh index b46e7a3..e223cc9 100755 --- a/deploy/nixos/factory-reset-atm.sh +++ b/deploy/nixos/factory-reset-atm.sh @@ -40,8 +40,8 @@ ENV=/var/lib/bitspire/.env DB=/var/lib/bitspire/state.db # Preserve model + fiat from the existing .env (fall back to sintra/EUR). -model=$(grep -E '^VITE_LAMASSU_MACHINE_MODEL=' "$ENV" 2>/dev/null | cut -d= -f2- || true) -fiat=$(grep -E '^VITE_LAMASSU_FIAT_CODE=' "$ENV" 2>/dev/null | cut -d= -f2- || true) +model=$(grep -E '^VITE_BITSPIRE_MACHINE_MODEL=' "$ENV" 2>/dev/null | cut -d= -f2- || true) +fiat=$(grep -E '^VITE_BITSPIRE_FIAT_CODE=' "$ENV" 2>/dev/null | cut -d= -f2- || true) model=${model:-sintra} fiat=${fiat:-EUR} @@ -52,8 +52,8 @@ rm -f "$DB" "$DB-wal" "$DB-shm" # Truncate .env to the minimal image-baked template. cat > "$ENV" < Date: Fri, 9 Oct 2026 21:58:55 +0200 Subject: [PATCH 09/15] refactor(dev): rename the lamassu-* dev-infra containers, databases and credentials to bitspire-* MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Container names (relay, bitcoind, lnd, lnd-alice, lightning-pub, miner, postgres), the regtest bitcoind rpcuser/rpcpassword, the postgres role and database (bitspire_dev), the dev admin token, BITSPIRE_HOST_IP, the devenv project name, and the banners/log prefixes in the scripts. Credentials are dev-only regtest values and are consistent across all 31 sites (rpcuser=, rpcpassword=, rpcpass=, --user) — the containers would not talk to each other otherwise. Anyone with the old stack running needs `docker compose down` once before `up`: the container names changed, so compose will otherwise see a conflict. Secret-scanner allowlist markers added on the credential lines, and on two pre-existing dev.sh comments ("private key") the hook flags as PRIVATE KEY — prose, no key material; this diff introduced neither. --- devenv.nix | 100 ++++++++++----------- docker/dev.sh | 56 ++++++------ docker/docker-compose.dev.yml | 48 +++++----- docker/docker-compose.regtest.yml | 24 ++--- docker/regtest-bootstrap.sh | 6 +- docker/regtest.sh | 12 +-- docker/start-with-regtest.sh | 28 +++--- docker/strfry.conf | 4 +- packages/nostr-client/dev/mock-machine.mjs | 2 +- packages/nostr-client/dev/test-debit.mjs | 2 +- packages/nostr-client/dev/test-pay.mjs | 4 +- 11 files changed, 143 insertions(+), 143 deletions(-) diff --git a/devenv.nix b/devenv.nix index 5315aca..b9e5faf 100644 --- a/devenv.nix +++ b/devenv.nix @@ -2,7 +2,7 @@ { # Project metadata - name = "lamassu-next"; + name = "bitspire-next"; # ============================================ # Languages @@ -65,7 +65,7 @@ services.postgres = { enable = true; - initialDatabases = [{ name = "lamassu_dev"; }]; + initialDatabases = [{ name = "bitspire_dev"; }]; listen_addresses = "127.0.0.1"; }; @@ -97,7 +97,7 @@ env = { RUST_BACKTRACE = "1"; - DATABASE_URL = "postgresql://localhost/lamassu_dev"; + DATABASE_URL = "postgresql://localhost/bitspire_dev"; # Nostr development relay NOSTR_RELAY_URL = "ws://localhost:7777"; @@ -130,7 +130,7 @@ # Wait for strfry echo -n "strfry relay: " - until docker exec lamassu-relay nc -z localhost 7777 2>/dev/null; do + until docker exec bitspire-relay nc -z localhost 7777 2>/dev/null; do echo -n "." sleep 2 done @@ -138,7 +138,7 @@ # Wait for bitcoind echo -n "bitcoind: " - until docker exec lamassu-bitcoind bitcoin-cli -regtest -rpcuser=lamassu -rpcpassword=lamassu getblockchaininfo >/dev/null 2>&1; do + until docker exec bitspire-bitcoind bitcoin-cli -regtest -rpcuser=bitspire -rpcpassword=bitspire getblockchaininfo >/dev/null 2>&1; do # pragma: allowlist secret echo -n "." sleep 2 done @@ -146,7 +146,7 @@ # Wait for LND echo -n "LND: " - until docker exec lamassu-lnd lncli --network=regtest getinfo >/dev/null 2>&1; do + until docker exec bitspire-lnd lncli --network=regtest getinfo >/dev/null 2>&1; do echo -n "." sleep 3 done @@ -154,7 +154,7 @@ # Wait for Alice's LND echo -n "LND (Alice): " - until docker exec lamassu-lnd-alice lncli --network=regtest getinfo >/dev/null 2>&1; do + until docker exec bitspire-lnd-alice lncli --network=regtest getinfo >/dev/null 2>&1; do echo -n "." sleep 3 done @@ -196,7 +196,7 @@ mine-blocks.exec = '' BLOCKS=''${1:-1} echo "Mining $BLOCKS regtest block(s)..." - docker exec lamassu-bitcoind bitcoin-cli -regtest -rpcuser=lamassu -rpcpassword=lamassu -generate "$BLOCKS" + docker exec bitspire-bitcoind bitcoin-cli -regtest -rpcuser=bitspire -rpcpassword=bitspire -generate "$BLOCKS" # pragma: allowlist secret ''; # Start auto-miner (mines 1 block every 30 seconds) @@ -211,19 +211,19 @@ # Stop auto-miner auto-mine-stop.exec = '' echo "Stopping auto-miner..." - docker stop lamassu-miner 2>/dev/null || true - docker rm lamassu-miner 2>/dev/null || true + docker stop bitspire-miner 2>/dev/null || true + docker rm bitspire-miner 2>/dev/null || true echo "Auto-miner stopped." ''; # Connect to LND CLI lncli.exec = '' - docker exec -it lamassu-lnd lncli --network=regtest "$@" + docker exec -it bitspire-lnd lncli --network=regtest "$@" ''; # Connect to Bitcoin CLI btccli.exec = '' - docker exec -it lamassu-bitcoind bitcoin-cli -regtest -rpcuser=lamassu -rpcpassword=lamassu "$@" + docker exec -it bitspire-bitcoind bitcoin-cli -regtest -rpcuser=bitspire -rpcpassword=bitspire "$@" # pragma: allowlist secret ''; # Test relay connection @@ -234,7 +234,7 @@ # Connect to Alice's LND CLI (second node for testing payments) lncli-alice.exec = '' - docker exec -it lamassu-lnd-alice lncli --network=regtest "$@" + docker exec -it bitspire-lnd-alice lncli --network=regtest "$@" ''; # Setup Lightning channel between Alice and the main LND node @@ -243,46 +243,46 @@ echo "" # Get LND's pubkey and address - LND_INFO=$(docker exec lamassu-lnd lncli --network=regtest getinfo 2>/dev/null) + LND_INFO=$(docker exec bitspire-lnd lncli --network=regtest getinfo 2>/dev/null) LND_PUBKEY=$(echo "$LND_INFO" | jq -r '.identity_pubkey') echo "LND pubkey: $LND_PUBKEY" # Connect Alice to LND echo "Connecting Alice to LND..." - docker exec lamassu-lnd-alice lncli --network=regtest connect "$LND_PUBKEY@lnd:9735" 2>/dev/null || true + docker exec bitspire-lnd-alice lncli --network=regtest connect "$LND_PUBKEY@lnd:9735" 2>/dev/null || true # Check if Alice has enough funds - ALICE_BALANCE=$(docker exec lamassu-lnd-alice lncli --network=regtest walletbalance 2>/dev/null | jq -r '.confirmed_balance') + ALICE_BALANCE=$(docker exec bitspire-lnd-alice lncli --network=regtest walletbalance 2>/dev/null | jq -r '.confirmed_balance') echo "Alice's on-chain balance: $ALICE_BALANCE sats" if [ "$ALICE_BALANCE" -lt 1000000 ]; then echo "" echo "Alice needs funds. Getting new address..." - ALICE_ADDR=$(docker exec lamassu-lnd-alice lncli --network=regtest newaddress p2wkh | jq -r '.address') + ALICE_ADDR=$(docker exec bitspire-lnd-alice lncli --network=regtest newaddress p2wkh | jq -r '.address') echo "Alice's address: $ALICE_ADDR" echo "" echo "Sending 5 BTC to Alice..." - docker exec lamassu-bitcoind bitcoin-cli -regtest -rpcuser=lamassu -rpcpassword=lamassu sendtoaddress "$ALICE_ADDR" 5 + docker exec bitspire-bitcoind bitcoin-cli -regtest -rpcuser=bitspire -rpcpassword=bitspire sendtoaddress "$ALICE_ADDR" 5 # pragma: allowlist secret echo "Mining 6 blocks for confirmation..." - docker exec lamassu-bitcoind bitcoin-cli -regtest -rpcuser=lamassu -rpcpassword=lamassu -generate 6 >/dev/null + docker exec bitspire-bitcoind bitcoin-cli -regtest -rpcuser=bitspire -rpcpassword=bitspire -generate 6 >/dev/null # pragma: allowlist secret sleep 2 - ALICE_BALANCE=$(docker exec lamassu-lnd-alice lncli --network=regtest walletbalance 2>/dev/null | jq -r '.confirmed_balance') + ALICE_BALANCE=$(docker exec bitspire-lnd-alice lncli --network=regtest walletbalance 2>/dev/null | jq -r '.confirmed_balance') echo "Alice's new balance: $ALICE_BALANCE sats" fi # Open channel from Alice to LND (1M sats) echo "" echo "Opening 1M sat channel from Alice to LND..." - docker exec lamassu-lnd-alice lncli --network=regtest openchannel --node_key="$LND_PUBKEY" --local_amt=1000000 + docker exec bitspire-lnd-alice lncli --network=regtest openchannel --node_key="$LND_PUBKEY" --local_amt=1000000 echo "" echo "Mining 6 blocks to confirm channel..." - docker exec lamassu-bitcoind bitcoin-cli -regtest -rpcuser=lamassu -rpcpassword=lamassu -generate 6 >/dev/null + docker exec bitspire-bitcoind bitcoin-cli -regtest -rpcuser=bitspire -rpcpassword=bitspire -generate 6 >/dev/null # pragma: allowlist secret sleep 3 echo "" echo "Channel status:" - docker exec lamassu-lnd-alice lncli --network=regtest listchannels | jq '.channels[] | {remote_pubkey, capacity, local_balance, remote_balance, active}' + docker exec bitspire-lnd-alice lncli --network=regtest listchannels | jq '.channels[] | {remote_pubkey, capacity, local_balance, remote_balance, active}' echo "" echo "Channel setup complete! Alice can now pay invoices to Lightning.Pub." ''; @@ -294,14 +294,14 @@ exit 1 fi echo "Paying invoice from Alice's node..." - docker exec lamassu-lnd-alice lncli --network=regtest payinvoice --force "$1" + docker exec bitspire-lnd-alice lncli --network=regtest payinvoice --force "$1" ''; # Create invoice on Alice's node (for testing ATM payouts) alice-invoice.exec = '' AMOUNT=''${1:-1000} MEMO=''${2:-"Test invoice"} - docker exec lamassu-lnd-alice lncli --network=regtest addinvoice --amt="$AMOUNT" --memo="$MEMO" | jq -r '.payment_request' + docker exec bitspire-lnd-alice lncli --network=regtest addinvoice --amt="$AMOUNT" --memo="$MEMO" | jq -r '.payment_request' ''; # Fund ATM account in Lightning.Pub @@ -324,7 +324,7 @@ echo "" echo "Invoice created. Paying from Alice..." - docker exec lamassu-lnd-alice lncli --network=regtest payinvoice --force "$INVOICE" + docker exec bitspire-lnd-alice lncli --network=regtest payinvoice --force "$INVOICE" echo "" echo "ATM account funded with $AMOUNT sats!" @@ -334,14 +334,14 @@ test-setup.exec = '' echo "" echo "═══════════════════════════════════════════════════════════" - echo " Lamassu Next - Test Setup Validation" + echo " bitSpire - Test Setup Validation" echo "═══════════════════════════════════════════════════════════" echo "" # Mine a block to wake up LND sync (regtest quirk: LND reports # "not synced" when no blocks mined recently) echo "Mining block to sync nodes..." - docker exec lamassu-bitcoind bitcoin-cli -regtest -rpcuser=lamassu -rpcpassword=lamassu -generate 1 >/dev/null 2>&1 + docker exec bitspire-bitcoind bitcoin-cli -regtest -rpcuser=bitspire -rpcpassword=bitspire -generate 1 >/dev/null 2>&1 # pragma: allowlist secret sleep 1 echo "" @@ -362,19 +362,19 @@ echo "───────────────────────────────────────────────────────────" # Check containers - docker ps --format '{{.Names}}' | grep -q lamassu-relay + docker ps --format '{{.Names}}' | grep -q bitspire-relay check $? "strfry relay running" - docker ps --format '{{.Names}}' | grep -q lamassu-bitcoind + docker ps --format '{{.Names}}' | grep -q bitspire-bitcoind check $? "bitcoind running" - docker ps --format '{{.Names}}' | grep -q lamassu-lnd + docker ps --format '{{.Names}}' | grep -q bitspire-lnd check $? "LND running" - docker ps --format '{{.Names}}' | grep -q lamassu-lnd-alice + docker ps --format '{{.Names}}' | grep -q bitspire-lnd-alice check $? "LND Alice running" - docker ps --format '{{.Names}}' | grep -q lamassu-lightning-pub + docker ps --format '{{.Names}}' | grep -q bitspire-lightning-pub check $? "Lightning.Pub running" echo "" @@ -386,32 +386,32 @@ check $? "Nostr relay port open" # Check bitcoind RPC - docker exec lamassu-bitcoind bitcoin-cli -regtest -rpcuser=lamassu -rpcpassword=lamassu getblockchaininfo >/dev/null 2>&1 + docker exec bitspire-bitcoind bitcoin-cli -regtest -rpcuser=bitspire -rpcpassword=bitspire getblockchaininfo >/dev/null 2>&1 # pragma: allowlist secret check $? "Bitcoin RPC responding" # Check LND - docker exec lamassu-lnd lncli --network=regtest getinfo >/dev/null 2>&1 + docker exec bitspire-lnd lncli --network=regtest getinfo >/dev/null 2>&1 check $? "LND RPC responding" # Check Alice - docker exec lamassu-lnd-alice lncli --network=regtest getinfo >/dev/null 2>&1 + docker exec bitspire-lnd-alice lncli --network=regtest getinfo >/dev/null 2>&1 check $? "LND Alice RPC responding" echo "" echo "3. Blockchain State" echo "───────────────────────────────────────────────────────────" - BLOCKS=$(docker exec lamassu-bitcoind bitcoin-cli -regtest -rpcuser=lamassu -rpcpassword=lamassu getblockcount 2>/dev/null) + BLOCKS=$(docker exec bitspire-bitcoind bitcoin-cli -regtest -rpcuser=bitspire -rpcpassword=bitspire getblockcount 2>/dev/null) # pragma: allowlist secret [ "$BLOCKS" -ge 100 ] check $? "Block height >= 100 (current: $BLOCKS)" # Check LND synced - LND_SYNCED=$(docker exec lamassu-lnd lncli --network=regtest getinfo 2>/dev/null | jq -r '.synced_to_chain') + LND_SYNCED=$(docker exec bitspire-lnd lncli --network=regtest getinfo 2>/dev/null | jq -r '.synced_to_chain') [ "$LND_SYNCED" = "true" ] check $? "LND synced to chain" # Check Alice synced - ALICE_SYNCED=$(docker exec lamassu-lnd-alice lncli --network=regtest getinfo 2>/dev/null | jq -r '.synced_to_chain') + ALICE_SYNCED=$(docker exec bitspire-lnd-alice lncli --network=regtest getinfo 2>/dev/null | jq -r '.synced_to_chain') [ "$ALICE_SYNCED" = "true" ] check $? "LND Alice synced to chain" @@ -420,17 +420,17 @@ echo "───────────────────────────────────────────────────────────" # Check Alice has active channels - ALICE_CHANNELS=$(docker exec lamassu-lnd-alice lncli --network=regtest listchannels 2>/dev/null | jq '.channels | length') + ALICE_CHANNELS=$(docker exec bitspire-lnd-alice lncli --network=regtest listchannels 2>/dev/null | jq '.channels | length') [ "$ALICE_CHANNELS" -ge 1 ] check $? "Alice has active channels (count: $ALICE_CHANNELS)" # Check channel is active - ACTIVE_CHANNEL=$(docker exec lamassu-lnd-alice lncli --network=regtest listchannels 2>/dev/null | jq '.channels[0].active') + ACTIVE_CHANNEL=$(docker exec bitspire-lnd-alice lncli --network=regtest listchannels 2>/dev/null | jq '.channels[0].active') [ "$ACTIVE_CHANNEL" = "true" ] check $? "Channel is active" # Check Alice has outbound capacity - ALICE_LOCAL=$(docker exec lamassu-lnd-alice lncli --network=regtest listchannels 2>/dev/null | jq -r '.channels[0].local_balance // 0') + ALICE_LOCAL=$(docker exec bitspire-lnd-alice lncli --network=regtest listchannels 2>/dev/null | jq -r '.channels[0].local_balance // 0') [ "$ALICE_LOCAL" -ge 10000 ] check $? "Alice has outbound capacity ($ALICE_LOCAL sats)" @@ -439,12 +439,12 @@ echo "───────────────────────────────────────────────────────────" # Create test invoice on LND and pay from Alice - TEST_INVOICE=$(docker exec lamassu-lnd lncli --network=regtest addinvoice --amt=100 --memo="test-setup validation" 2>/dev/null | jq -r '.payment_request') + TEST_INVOICE=$(docker exec bitspire-lnd lncli --network=regtest addinvoice --amt=100 --memo="test-setup validation" 2>/dev/null | jq -r '.payment_request') if [ -n "$TEST_INVOICE" ]; then check 0 "Created 100 sat test invoice" # Pay it - PAY_RESULT=$(docker exec lamassu-lnd-alice lncli --network=regtest payinvoice --force "$TEST_INVOICE" 2>&1) + PAY_RESULT=$(docker exec bitspire-lnd-alice lncli --network=regtest payinvoice --force "$TEST_INVOICE" 2>&1) if echo "$PAY_RESULT" | grep -q "SUCCEEDED"; then check 0 "Payment Alice → LND succeeded" else @@ -478,7 +478,7 @@ # Create invoice on Alice (simulating customer's wallet) echo "1. Creating invoice on Alice's wallet..." - INVOICE=$(docker exec lamassu-lnd-alice lncli --network=regtest addinvoice --amt="$AMOUNT" --memo="E2E test" | jq -r '.payment_request') + INVOICE=$(docker exec bitspire-lnd-alice lncli --network=regtest addinvoice --amt="$AMOUNT" --memo="E2E test" | jq -r '.payment_request') echo " Invoice: ''${INVOICE:0:40}..." # Use test-pay.mjs to pay via Lightning.Pub @@ -490,7 +490,7 @@ echo "" echo "3. Verifying payment on Alice..." sleep 2 - PAID=$(docker exec lamassu-lnd-alice lncli --network=regtest listinvoices 2>/dev/null | jq '.invoices[-1].settled') + PAID=$(docker exec bitspire-lnd-alice lncli --network=regtest listinvoices 2>/dev/null | jq '.invoices[-1].settled') if [ "$PAID" = "true" ]; then echo " ✓ Payment received!" else @@ -507,15 +507,15 @@ echo "" echo "LND (Lightning.Pub's node):" - docker exec lamassu-lnd lncli --network=regtest getinfo 2>/dev/null | jq '{pubkey: .identity_pubkey, alias: .alias, channels: .num_active_channels, peers: .num_peers}' + docker exec bitspire-lnd lncli --network=regtest getinfo 2>/dev/null | jq '{pubkey: .identity_pubkey, alias: .alias, channels: .num_active_channels, peers: .num_peers}' echo "" echo "LND Alice (Payment source):" - docker exec lamassu-lnd-alice lncli --network=regtest getinfo 2>/dev/null | jq '{pubkey: .identity_pubkey, alias: .alias, channels: .num_active_channels, peers: .num_peers}' + docker exec bitspire-lnd-alice lncli --network=regtest getinfo 2>/dev/null | jq '{pubkey: .identity_pubkey, alias: .alias, channels: .num_active_channels, peers: .num_peers}' echo "" echo "Channel Details:" - docker exec lamassu-lnd-alice lncli --network=regtest listchannels 2>/dev/null | jq '.channels[] | {peer: .remote_pubkey[0:16], capacity, local: .local_balance, remote: .remote_balance, active}' + docker exec bitspire-lnd-alice lncli --network=regtest listchannels 2>/dev/null | jq '.channels[] | {peer: .remote_pubkey[0:16], capacity, local: .local_balance, remote: .remote_balance, active}' echo "" echo "Lightning.Pub Nostr pubkey:" @@ -533,7 +533,7 @@ enterShell = '' echo "" - echo " ⚡ Lamassu Next - Nostr-Native Lightning ATM" + echo " ⚡ bitSpire - Nostr-Native Lightning ATM" echo " ─────────────────────────────────────────────" echo " Node.js: $(node --version)" echo " Rust: $(rustc --version | cut -d' ' -f2)" diff --git a/docker/dev.sh b/docker/dev.sh index 25926a3..f47d42f 100755 --- a/docker/dev.sh +++ b/docker/dev.sh @@ -1,6 +1,6 @@ #!/bin/bash # -# Lamassu Next Development Environment +# bitSpire Development Environment # # Single command to manage the complete development stack: # - Regtest Bitcoin/Lightning network (from ~/dev/local/docker/regtest) @@ -67,7 +67,7 @@ success() { echo -e "${GREEN}✓${NC} $1"; } # Ensure state directory exists mkdir -p "$STATE_DIR" -# Get ATM pubkey from its private key +# Get ATM pubkey from its private key # pragma: allowlist secret get_atm_pubkey() { local env_file="$PROJECT_DIR/apps/machine/.env" if [[ ! -f "$env_file" ]]; then @@ -96,8 +96,8 @@ get_atm_pubkey() { get_local_ip() { # Allow explicit override via env var - if [[ -n "${LAMASSU_HOST_IP:-}" ]]; then - echo "$LAMASSU_HOST_IP" + if [[ -n "${BITSPIRE_HOST_IP:-}" ]]; then + echo "$BITSPIRE_HOST_IP" return fi @@ -146,7 +146,7 @@ lnd4_has_active_channels() { } is_lightning_pub_ready() { - docker logs lamassu-lightning-pub 2>&1 | grep -q "LightningPub listening" + docker logs bitspire-lightning-pub 2>&1 | grep -q "LightningPub listening" } get_lnd4_balance() { @@ -422,8 +422,8 @@ wait_for_lightning_pub() { local attempts=0 while ! is_lightning_pub_ready && [[ $attempts -lt 45 ]]; do # Check for errors - if docker logs lamassu-lightning-pub 2>&1 | grep -q "Error:"; then - local err=$(docker logs lamassu-lightning-pub 2>&1 | grep "Error:" | tail -1) + if docker logs bitspire-lightning-pub 2>&1 | grep -q "Error:"; then + local err=$(docker logs bitspire-lightning-pub 2>&1 | grep "Error:" | tail -1) error "Lightning.Pub error: $err" return 1 fi @@ -437,14 +437,14 @@ wait_for_lightning_pub() { return 0 else error "Lightning.Pub failed to start (timeout)" - docker logs lamassu-lightning-pub 2>&1 | tail -10 + docker logs bitspire-lightning-pub 2>&1 | tail -10 return 1 fi } extract_lightning_pub_info() { - local pubkey=$(docker logs lamassu-lightning-pub 2>&1 | grep -oP 'pubkey:\s*\K[a-f0-9]+' | tail -1) - local nprofile=$(docker logs lamassu-lightning-pub 2>&1 | grep -oP 'nprofile:\s*\K\S+' | tail -1) + local pubkey=$(docker logs bitspire-lightning-pub 2>&1 | grep -oP 'pubkey:\s*\K[a-f0-9]+' | tail -1) + local nprofile=$(docker logs bitspire-lightning-pub 2>&1 | grep -oP 'nprofile:\s*\K\S+' | tail -1) echo "$pubkey" > "$PUBKEY_FILE" echo "$nprofile" > "$NPROFILE_FILE" @@ -467,7 +467,7 @@ update_atm_env() { VITE_RELAY_URL=ws://localhost:7777 VITE_LIGHTNING_PUB_PUBKEY=$pubkey VITE_LIGHTNING_PUB_API_URL=http://localhost:1776 -VITE_ADMIN_TOKEN=lamassu-dev-admin-token +VITE_ADMIN_TOKEN=bitspire-dev-admin-token VITE_ATM_PRIVATE_KEY=f391a2c3fc734f443b0f685688a0441b5fb9805853c0023f570c5a3c6412b136 # Extension API (for LNURL-withdraw) @@ -486,7 +486,7 @@ EOF setup_atm_app() { # Use a fixed app name so we reuse the same app across restarts - local app_name="lamassu-atm-dev" + local app_name="bitspire-atm-dev" # Check if we already have valid app state if [[ -f "$APP_ID_FILE" ]]; then @@ -501,7 +501,7 @@ setup_atm_app() { local response=$(curl -s -X POST http://localhost:1776/api/admin/app/add \ -H "Content-Type: application/json" \ - -H "Authorization: Bearer lamassu-dev-admin-token" \ + -H "Authorization: Bearer bitspire-dev-admin-token" \ -d "{\"name\":\"$app_name\",\"allow_user_creation\":true}" 2>/dev/null) if echo "$response" | grep -q '"status":"OK"'; then @@ -566,8 +566,8 @@ get_atm_balance() { } show_status() { - local pubkey=$(cat "$PUBKEY_FILE" 2>/dev/null || docker logs lamassu-lightning-pub 2>&1 | grep -oP 'pubkey:\s*\K[a-f0-9]+' | tail -1) - local nprofile=$(cat "$NPROFILE_FILE" 2>/dev/null || docker logs lamassu-lightning-pub 2>&1 | grep -oP 'nprofile:\s*\K\S+' | tail -1) + local pubkey=$(cat "$PUBKEY_FILE" 2>/dev/null || docker logs bitspire-lightning-pub 2>&1 | grep -oP 'pubkey:\s*\K[a-f0-9]+' | tail -1) + local nprofile=$(cat "$NPROFILE_FILE" 2>/dev/null || docker logs bitspire-lightning-pub 2>&1 | grep -oP 'nprofile:\s*\K\S+' | tail -1) local local_ip=$(get_local_ip) local lndconnect=$(generate_lndconnect 2>/dev/null || echo "") local lnd4_balance=$(get_lnd4_balance) @@ -575,7 +575,7 @@ show_status() { echo "" echo -e "${BOLD}╔═══════════════════════════════════════════════════════════════════╗${NC}" - echo -e "${BOLD}║ LAMASSU NEXT - DEVELOPMENT ENVIRONMENT ║${NC}" + echo -e "${BOLD}║ BITSPIRE - DEVELOPMENT ENVIRONMENT ║${NC}" echo -e "${BOLD}╚═══════════════════════════════════════════════════════════════════╝${NC}" echo "" @@ -663,7 +663,7 @@ cmd_up() { fi echo "" - log "Starting Lamassu development environment..." + log "Starting bitSpire development environment..." echo "" # 1. Start shared regtest if needed (not in standalone mode) @@ -696,7 +696,7 @@ cmd_up() { log "Using Lightning.Pub image: $image" - # 4. Start lamassu services + # 4. Start bitspire services export REGTEST_DATA_DIR="$REGTEST_DIR/data" export LIGHTNING_PUB_IMAGE="$image" export HOST_IP=$(get_local_ip) @@ -708,7 +708,7 @@ cmd_up() { echo "unified" > "$MODE_FILE" fi - log "Starting lamassu services..." + log "Starting bitspire services..." docker compose -f "$SCRIPT_DIR/$compose_file" up -d # 5. Wait for Lightning.Pub @@ -746,7 +746,7 @@ cmd_up() { cmd_down() { local compose_file=$(get_compose_file) - log "Stopping lamassu services..." + log "Stopping bitspire services..." docker compose -f "$SCRIPT_DIR/$compose_file" down 2>/dev/null || true if [[ "$1" == "--all" ]]; then @@ -757,7 +757,7 @@ cmd_down() { } cmd_reset() { - warn "This will delete all lamassu state (Lightning.Pub identity, ATM app, etc.)" + warn "This will delete all bitspire state (Lightning.Pub identity, ATM app, etc.)" read -p "Continue? [y/N] " -n 1 -r echo if [[ ! $REPLY =~ ^[Yy]$ ]]; then @@ -782,7 +782,7 @@ cmd_logs() { } cmd_status() { - if ! docker ps --format '{{.Names}}' | grep -q lamassu-lightning-pub; then + if ! docker ps --format '{{.Names}}' | grep -q bitspire-lightning-pub; then error "Services not running. Start with: ./dev.sh up" exit 1 fi @@ -806,7 +806,7 @@ cmd_fund() { local lp_pubkey=$(cat "$PUBKEY_FILE") local env_file="$PROJECT_DIR/apps/machine/.env" - # Get ATM private key from .env + # Get ATM private key from .env # pragma: allowlist secret if [[ ! -f "$env_file" ]]; then error "ATM not configured. Run './dev.sh up' first." exit 1 @@ -952,7 +952,7 @@ cmd_atm() { fi # Check if services are running - if ! docker ps --format '{{.Names}}' | grep -q lamassu-lightning-pub; then + if ! docker ps --format '{{.Names}}' | grep -q bitspire-lightning-pub; then warn "Services not running. Start with: ./dev.sh up" read -p "Start services first? [Y/n] " -n 1 -r echo @@ -1013,7 +1013,7 @@ case "${1:-help}" in shift cmd_logs "$@" ;; - # Internal command for external orchestration (called by regtest-start --with-lamassu) + # Internal command for external orchestration (called by regtest-start --with-bitspire) _setup_after_start) if ! is_lightning_pub_ready; then wait_for_lightning_pub || exit 1 @@ -1041,7 +1041,7 @@ case "${1:-help}" in cmd_zeus ;; *) - echo "Lamassu Next Development Environment" + echo "bitSpire Development Environment" echo "" echo "Usage: $0 [options]" echo "" @@ -1069,7 +1069,7 @@ case "${1:-help}" in echo " --machine Launch ATM app after startup" echo "" echo "Environment variables:" - echo " LAMASSU_HOST_IP Override auto-detected LAN IP (for VPN/multi-NIC)" + echo " BITSPIRE_HOST_IP Override auto-detected LAN IP (for VPN/multi-NIC)" echo "" echo "Examples:" echo " $0 up # Start (uses shared regtest)" @@ -1082,6 +1082,6 @@ case "${1:-help}" in echo " $0 fund 200000 # Add 200k more sats" echo " $0 logs lightning-pub" echo " $0 reset && $0 up --fund # Fresh start with funding" - echo " LAMASSU_HOST_IP=192.168.1.50 $0 up # Override LAN IP" + echo " BITSPIRE_HOST_IP=192.168.1.50 $0 up # Override LAN IP" ;; esac diff --git a/docker/docker-compose.dev.yml b/docker/docker-compose.dev.yml index 6a963a2..fa9860e 100644 --- a/docker/docker-compose.dev.yml +++ b/docker/docker-compose.dev.yml @@ -1,11 +1,11 @@ -# Lamassu Next Development Infrastructure +# bitSpire Development Infrastructure # Usage: docker compose -f docker-compose.dev.yml up -d services: # Private Nostr relay for ATM communication strfry: image: ghcr.io/hoytech/strfry:latest - container_name: lamassu-relay + container_name: bitspire-relay ports: - '7777:7777' volumes: @@ -25,7 +25,7 @@ services: # Bitcoin Core in regtest mode bitcoind: image: lncm/bitcoind:v27.0 - container_name: lamassu-bitcoind + container_name: bitspire-bitcoind volumes: - bitcoind-data:/data/.bitcoin environment: @@ -33,8 +33,8 @@ services: command: - -regtest - -server - - -rpcuser=lamassu - - -rpcpassword=lamassu + - -rpcuser=bitspire + - -rpcpassword=bitspire # pragma: allowlist secret - -rpcallowip=0.0.0.0/0 - -rpcbind=0.0.0.0 - -zmqpubrawblock=tcp://0.0.0.0:28332 @@ -51,8 +51,8 @@ services: 'CMD', 'bitcoin-cli', '-regtest', - '-rpcuser=lamassu', - '-rpcpassword=lamassu', + '-rpcuser=bitspire', + '-rpcpassword=bitspire', # pragma: allowlist secret 'getblockchaininfo', ] interval: 10s @@ -63,7 +63,7 @@ services: # LND Lightning node lnd: image: lightninglabs/lnd:v0.18.0-beta - container_name: lamassu-lnd + container_name: bitspire-lnd depends_on: bitcoind: condition: service_healthy @@ -76,8 +76,8 @@ services: - --bitcoin.regtest - --bitcoin.node=bitcoind - --bitcoind.rpchost=bitcoind:18443 - - --bitcoind.rpcuser=lamassu - - --bitcoind.rpcpass=lamassu + - --bitcoind.rpcuser=bitspire + - --bitcoind.rpcpass=bitspire # pragma: allowlist secret - --bitcoind.zmqpubrawblock=tcp://bitcoind:28332 - --bitcoind.zmqpubrawtx=tcp://bitcoind:28333 - --rpclisten=0.0.0.0:10009 @@ -104,7 +104,7 @@ services: # Using patched image with Kind 0 profile publishing support lightning-pub: image: lightning-pub-patched:latest - container_name: lamassu-lightning-pub + container_name: bitspire-lightning-pub depends_on: lnd: condition: service_healthy @@ -126,14 +126,14 @@ services: # Disable external liquidity provider for regtest - DISABLE_LIQUIDITY_PROVIDER=true # Admin token for HTTP API access (development only) - - ADMIN_TOKEN=lamassu-dev-admin-token + - ADMIN_TOKEN=bitspire-dev-admin-token restart: unless-stopped # Second LND node (Alice) for payment testing # This node can pay invoices to Lightning.Pub's LND lnd-alice: image: lightninglabs/lnd:v0.18.0-beta - container_name: lamassu-lnd-alice + container_name: bitspire-lnd-alice depends_on: bitcoind: condition: service_healthy @@ -146,8 +146,8 @@ services: - --bitcoin.regtest - --bitcoin.node=bitcoind - --bitcoind.rpchost=bitcoind:18443 - - --bitcoind.rpcuser=lamassu - - --bitcoind.rpcpass=lamassu + - --bitcoind.rpcuser=bitspire + - --bitcoind.rpcpass=bitspire # pragma: allowlist secret - --bitcoind.zmqpubrawblock=tcp://bitcoind:28332 - --bitcoind.zmqpubrawtx=tcp://bitcoind:28333 - --rpclisten=0.0.0.0:10009 @@ -174,7 +174,7 @@ services: # Mines 1 block every 30 seconds miner: image: alpine:latest - container_name: lamassu-miner + container_name: bitspire-miner depends_on: bitcoind: condition: service_healthy @@ -186,11 +186,11 @@ services: echo "Starting auto-miner (1 block every 30 seconds)..." while true; do # Create wallet if not exists - curl -s --user lamassu:lamassu --data-binary '{"jsonrpc":"1.0","method":"createwallet","params":["miner"]}' http://bitcoind:18443/ > /dev/null 2>&1 + curl -s --user bitspire:bitspire --data-binary '{"jsonrpc":"1.0","method":"createwallet","params":["miner"]}' http://bitcoind:18443/ > /dev/null 2>&1 # Mine a block - ADDR=$$(curl -s --user lamassu:lamassu --data-binary '{"jsonrpc":"1.0","method":"getnewaddress","params":[]}' http://bitcoind:18443/ | jq -r '.result // empty') + ADDR=$$(curl -s --user bitspire:bitspire --data-binary '{"jsonrpc":"1.0","method":"getnewaddress","params":[]}' http://bitcoind:18443/ | jq -r '.result // empty') if [ -n "$$ADDR" ]; then - curl -s --user lamassu:lamassu --data-binary "{\"jsonrpc\":\"1.0\",\"method\":\"generatetoaddress\",\"params\":[1,\"$$ADDR\"]}" http://bitcoind:18443/ > /dev/null + curl -s --user bitspire:bitspire --data-binary "{\"jsonrpc\":\"1.0\",\"method\":\"generatetoaddress\",\"params\":[1,\"$$ADDR\"]}" http://bitcoind:18443/ > /dev/null fi sleep 30 done @@ -201,17 +201,17 @@ services: # PostgreSQL for optional server-side state postgres: image: postgres:16-alpine - container_name: lamassu-postgres + container_name: bitspire-postgres ports: - '5432:5432' environment: - POSTGRES_DB: lamassu_dev - POSTGRES_USER: lamassu - POSTGRES_PASSWORD: lamassu_dev_password + POSTGRES_DB: bitspire_dev + POSTGRES_USER: bitspire + POSTGRES_PASSWORD: bitspire_dev_password # pragma: allowlist secret volumes: - postgres-data:/var/lib/postgresql/data healthcheck: - test: ['CMD-SHELL', 'pg_isready -U lamassu -d lamassu_dev'] + test: ['CMD-SHELL', 'pg_isready -U bitspire -d bitspire_dev'] interval: 10s timeout: 5s retries: 5 diff --git a/docker/docker-compose.regtest.yml b/docker/docker-compose.regtest.yml index 0ce54cd..43ca8a0 100644 --- a/docker/docker-compose.regtest.yml +++ b/docker/docker-compose.regtest.yml @@ -1,13 +1,13 @@ -# Lamassu Next - Regtest Integration +# bitSpire - Regtest Integration # -# This overlay connects lamassu-next services to the comprehensive regtest +# This overlay connects bitspire-next services to the comprehensive regtest # environment at ~/dev/local/docker/regtest # # Usage: # 1. Start regtest (minimal — only what LP needs): # cd ~/dev/local/docker/regtest && docker compose up -d bitcoind lnd-1 lnd-4 # -# 2. Start lamassu services: +# 2. Start bitspire services: # cd docker && docker compose -f docker-compose.regtest.yml up -d # # 3. Bootstrap (funds LND, opens channels, creates LP app): @@ -32,7 +32,7 @@ services: # Private Nostr relay for ATM communication strfry: image: ghcr.io/hoytech/strfry:latest - container_name: lamassu-relay + container_name: bitspire-relay ports: - '7777:7777' volumes: @@ -57,7 +57,7 @@ services: # Use LIGHTNING_PUB_IMAGE env var to specify image (default: lightning-pub-withdraw) lightning-pub: image: ${LIGHTNING_PUB_IMAGE:-lightning-pub-withdraw:latest} - container_name: lamassu-lightning-pub + container_name: bitspire-lightning-pub extra_hosts: - 'host.docker.internal:host-gateway' ports: @@ -80,7 +80,7 @@ services: # Disable external liquidity provider for regtest - DISABLE_LIQUIDITY_PROVIDER=true # Admin token for HTTP API access (development only) - - ADMIN_TOKEN=lamassu-dev-admin-token + - ADMIN_TOKEN=bitspire-dev-admin-token # Extension HTTP API URL (for LNURL callbacks from external wallets) # Use HOST_IP env var for your machine's LAN IP (required for phone wallets) - EXTENSION_SERVICE_URL=http://${HOST_IP:-localhost}:1777 @@ -101,7 +101,7 @@ services: # Keeps Lightning channels active during development miner: image: boltz/bitcoin-core:25.0 - container_name: lamassu-miner + container_name: bitspire-miner entrypoint: /bin/sh command: - -c @@ -132,17 +132,17 @@ services: # PostgreSQL for optional server-side state postgres: image: postgres:16-alpine - container_name: lamassu-postgres + container_name: bitspire-postgres ports: - '5432:5432' environment: - POSTGRES_DB: lamassu_dev - POSTGRES_USER: lamassu - POSTGRES_PASSWORD: lamassu_dev_password + POSTGRES_DB: bitspire_dev + POSTGRES_USER: bitspire + POSTGRES_PASSWORD: bitspire_dev_password # pragma: allowlist secret volumes: - postgres-data:/var/lib/postgresql/data healthcheck: - test: ['CMD-SHELL', 'pg_isready -U lamassu -d lamassu_dev'] + test: ['CMD-SHELL', 'pg_isready -U bitspire -d bitspire_dev'] interval: 10s timeout: 5s retries: 5 diff --git a/docker/regtest-bootstrap.sh b/docker/regtest-bootstrap.sh index f00ace1..ec1af77 100755 --- a/docker/regtest-bootstrap.sh +++ b/docker/regtest-bootstrap.sh @@ -4,7 +4,7 @@ # Prerequisites: # 1. Regtest running (minimal — only needs bitcoind + lnd-1 + lnd-4): # cd ~/dev/local/docker/regtest && docker compose up -d bitcoind lnd-1 lnd-4 -# 2. Lamassu services running: +# 2. bitSpire services running: # cd docker && docker compose -f docker-compose.regtest.yml up -d # # What this script does: @@ -59,7 +59,7 @@ lncli_4() { LP_URL="http://localhost:1776" LP_EXT_URL="http://localhost:1777" -ADMIN_TOKEN="lamassu-dev-admin-token" +ADMIN_TOKEN="bitspire-dev-admin-token" CHANNEL_SIZE=5000000 # 5M sats FUND_AMOUNT=10000 # 10k sats for LP app @@ -195,7 +195,7 @@ for i in $(seq 1 120); do fi if [ "$i" -eq 120 ]; then err "Lightning.Pub not ready after 120s" - err "Check: docker logs lamassu-lightning-pub" + err "Check: docker logs bitspire-lightning-pub" exit 1 fi sleep 1 diff --git a/docker/regtest.sh b/docker/regtest.sh index b8ab709..1c198a0 100755 --- a/docker/regtest.sh +++ b/docker/regtest.sh @@ -36,7 +36,7 @@ cmd_up() { echo -e "${CYAN}Starting regtest (bitcoind + lnd-1 + lnd-4)...${NC}" cd "$REGTEST_DIR" && docker compose up -d bitcoind lnd-1 lnd-4 - echo -e "${CYAN}Starting lamassu services...${NC}" + echo -e "${CYAN}Starting bitspire services...${NC}" docker compose -f "$COMPOSE_FILE" up -d echo -e "${CYAN}Running bootstrap...${NC}" @@ -44,7 +44,7 @@ cmd_up() { } cmd_down() { - echo -e "${CYAN}Stopping lamassu services...${NC}" + echo -e "${CYAN}Stopping bitspire services...${NC}" docker compose -f "$COMPOSE_FILE" down 2>/dev/null || true echo -e "${CYAN}Stopping regtest...${NC}" @@ -68,7 +68,7 @@ cmd_reset() { cmd_status() { echo -e "${CYAN}Containers:${NC}" - docker ps --format 'table {{.Names}}\t{{.Status}}\t{{.Ports}}' | grep -E 'regtest|lamassu|NAMES' | sort + docker ps --format 'table {{.Names}}\t{{.Status}}\t{{.Ports}}' | grep -E 'regtest|bitspire|NAMES' | sort PREFIX=$(detect_prefix) if [ -n "$PREFIX" ]; then @@ -92,9 +92,9 @@ cmd_status() { cmd_logs() { local svc="${1:-lp}" case "$svc" in - lp|lightning-pub) docker logs -f --tail 50 lamassu-lightning-pub ;; - relay|strfry) docker logs -f --tail 50 lamassu-relay ;; - miner) docker logs -f --tail 50 lamassu-miner ;; + lp|lightning-pub) docker logs -f --tail 50 bitspire-lightning-pub ;; + relay|strfry) docker logs -f --tail 50 bitspire-relay ;; + miner) docker logs -f --tail 50 bitspire-miner ;; lnd1|lnd-1) PREFIX=$(detect_prefix); docker logs -f --tail 50 "${PREFIX}-lnd-1-1" ;; lnd4|lnd-4) PREFIX=$(detect_prefix); docker logs -f --tail 50 "${PREFIX}-lnd-4-1" ;; bitcoind) PREFIX=$(detect_prefix); docker logs -f --tail 50 "${PREFIX}-bitcoind-1" ;; diff --git a/docker/start-with-regtest.sh b/docker/start-with-regtest.sh index b688e6e..93aff63 100755 --- a/docker/start-with-regtest.sh +++ b/docker/start-with-regtest.sh @@ -1,6 +1,6 @@ #!/bin/bash # -# Start lamassu-next development environment with comprehensive regtest +# Start bitspire-next development environment with comprehensive regtest # # This script: # 1. Connects to the regtest environment at ~/dev/local/docker/regtest @@ -37,10 +37,10 @@ CYAN='\033[0;36m' BOLD='\033[1m' NC='\033[0m' -log() { echo -e "${GREEN}[lamassu]${NC} $1"; } -warn() { echo -e "${YELLOW}[lamassu]${NC} $1"; } -error() { echo -e "${RED}[lamassu]${NC} $1"; } -info() { echo -e "${CYAN}[lamassu]${NC} $1"; } +log() { echo -e "${GREEN}[bitspire]${NC} $1"; } +warn() { echo -e "${YELLOW}[bitspire]${NC} $1"; } +error() { echo -e "${RED}[bitspire]${NC} $1"; } +info() { echo -e "${CYAN}[bitspire]${NC} $1"; } # Get local IP for external wallet access get_local_ip() { @@ -86,7 +86,7 @@ wait_for_lightning_pub() { local attempt=0 while [[ $attempt -lt $max_attempts ]]; do - if docker logs lamassu-lightning-pub 2>&1 | grep -q "LightningPub listening"; then + if docker logs bitspire-lightning-pub 2>&1 | grep -q "LightningPub listening"; then sleep 2 # Extra time for Nostr middleware return 0 fi @@ -95,18 +95,18 @@ wait_for_lightning_pub() { done error "Lightning.Pub failed to start within 60 seconds" - docker logs lamassu-lightning-pub 2>&1 | tail -20 + docker logs bitspire-lightning-pub 2>&1 | tail -20 return 1 } # Get Lightning.Pub nprofile from logs get_nprofile() { - docker logs lamassu-lightning-pub 2>&1 | grep -oP 'nprofile:\s*\K\S+' | tail -1 + docker logs bitspire-lightning-pub 2>&1 | grep -oP 'nprofile:\s*\K\S+' | tail -1 } # Get Lightning.Pub pubkey from logs get_pubkey() { - docker logs lamassu-lightning-pub 2>&1 | grep -oP 'pubkey:\s*\K[a-f0-9]+' | tail -1 + docker logs bitspire-lightning-pub 2>&1 | grep -oP 'pubkey:\s*\K[a-f0-9]+' | tail -1 } # Generate lndconnect URL for Zeus (using lnd-3 which has REST exposed) @@ -139,7 +139,7 @@ setup_atm_account() { # Create app local app_response=$(curl -s -X POST http://localhost:1776/api/admin/app/add \ -H "Content-Type: application/json" \ - -H "Authorization: Bearer lamassu-dev-admin-token" \ + -H "Authorization: Bearer bitspire-dev-admin-token" \ -d '{"name":"atm-app","allow_user_creation":true}' 2>/dev/null) if echo "$app_response" | grep -q '"status":"OK"'; then @@ -171,7 +171,7 @@ show_connection_info() { echo "" echo -e "${BOLD}═══════════════════════════════════════════════════════════════${NC}" - echo -e "${BOLD} LAMASSU REGTEST ENVIRONMENT ${NC}" + echo -e "${BOLD} BITSPIRE REGTEST ENVIRONMENT ${NC}" echo -e "${BOLD}═══════════════════════════════════════════════════════════════${NC}" echo "" @@ -262,7 +262,7 @@ start() { export LIGHTNING_PUB_IMAGE="$image" export HOST_IP=$(get_local_ip) - log "Starting lamassu services..." + log "Starting bitspire services..." docker compose -f "$SCRIPT_DIR/docker-compose.regtest.yml" up -d # Wait for Lightning.Pub and setup @@ -277,7 +277,7 @@ start() { # Stop services stop() { - log "Stopping lamassu services..." + log "Stopping bitspire services..." docker compose -f "$SCRIPT_DIR/docker-compose.regtest.yml" down rm -f "$SCRIPT_DIR/.atm-app-token" "$SCRIPT_DIR/.atm-app-id" log "Services stopped." @@ -290,7 +290,7 @@ logs() { # Show status/connection info status() { - if ! docker ps --format '{{.Names}}' | grep -q lamassu-lightning-pub; then + if ! docker ps --format '{{.Names}}' | grep -q bitspire-lightning-pub; then error "Services not running. Start with: $0 start" exit 1 fi diff --git a/docker/strfry.conf b/docker/strfry.conf index e775670..30e6dc0 100644 --- a/docker/strfry.conf +++ b/docker/strfry.conf @@ -1,5 +1,5 @@ ## -## strfry configuration for Lamassu ATM development +## strfry configuration for bitSpire ATM development ## relay { @@ -10,7 +10,7 @@ relay { nofiles = 0 info { - name = "Lamassu Dev Relay" + name = "bitSpire Dev Relay" description = "Private Nostr relay for ATM development and testing" pubkey = "" contact = "" diff --git a/packages/nostr-client/dev/mock-machine.mjs b/packages/nostr-client/dev/mock-machine.mjs index 5aca755..152e30b 100644 --- a/packages/nostr-client/dev/mock-machine.mjs +++ b/packages/nostr-client/dev/mock-machine.mjs @@ -24,7 +24,7 @@ const RELAY_URL = 'ws://localhost:7777' // Relay URL for browser access (different from Docker internal strfry:7777) const BROWSER_RELAY_URL = 'ws://localhost:7777' const LIGHTNING_PUB_HTTP = 'http://localhost:1776' -const ADMIN_TOKEN = 'lamassu-dev-admin-token' +const ADMIN_TOKEN = 'bitspire-dev-admin-token' // Lightning.Pub pubkey - fetched dynamically from the ATM user's ndebit let LIGHTNING_PUB_PUBKEY = null diff --git a/packages/nostr-client/dev/test-debit.mjs b/packages/nostr-client/dev/test-debit.mjs index a82f9bb..c1861ac 100644 --- a/packages/nostr-client/dev/test-debit.mjs +++ b/packages/nostr-client/dev/test-debit.mjs @@ -28,7 +28,7 @@ if (!NDEBIT || !BOLT11) { console.log('') console.log('Example:') console.log(' # First create an invoice on Alice:') - console.log(' docker exec lamassu-lnd-alice lncli --network=regtest addinvoice --amt 1000') + console.log(' docker exec bitspire-lnd-alice lncli --network=regtest addinvoice --amt 1000') console.log('') console.log(' # Then test the debit:') console.log(' node test-debit.mjs ndebit1... lnbcrt...') diff --git a/packages/nostr-client/dev/test-pay.mjs b/packages/nostr-client/dev/test-pay.mjs index 6b817d4..4385322 100644 --- a/packages/nostr-client/dev/test-pay.mjs +++ b/packages/nostr-client/dev/test-pay.mjs @@ -10,13 +10,13 @@ const LIGHTNING_PUB_PUBKEY = const RELAY_URL = process.env.NOSTR_RELAY_URL || 'ws://localhost:7777' // Get a fresh invoice from Alice first: -// docker exec lamassu-lnd-alice lncli --network=regtest addinvoice --amt 1000 +// docker exec bitspire-lnd-alice lncli --network=regtest addinvoice --amt 1000 const TEST_INVOICE = process.argv[2] if (!TEST_INVOICE) { console.log('Usage: node test-pay.mjs ') console.log( - 'Generate invoice: docker exec lamassu-lnd-alice lncli --network=regtest addinvoice --amt 1000' + 'Generate invoice: docker exec bitspire-lnd-alice lncli --network=regtest addinvoice --amt 1000' ) process.exit(1) } From 34c2a6c42d9387608a5a53ba02ab34881df2a643 Mon Sep 17 00:00:00 2001 From: Padreug Date: Fri, 9 Oct 2026 21:58:55 +0200 Subject: [PATCH 10/15] refactor: rename the remaining Lamassu-branded identifiers MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit LamassuEventKind → BitSpireEventKind (nostr-client; no consumers outside the package), the kiosk theme localStorage keys lamassu-theme / lamassu-color-mode → bitspire-* (a one-time theme reset on existing kiosks), the ui-shared UMD global LamassuUIShared → BitSpireUIShared, and the orphaned Rust HAL's Cargo name/description/repository plus its lib.rs header — now also labelled as the unbuilt leftover it is. nostr-client: 43/43 tests, tsc clean. Machine app: vue-tsc + electron tsc clean. --- apps/machine/src/composables/useTheme.ts | 4 ++-- packages/hal/Cargo.toml | 6 +++--- packages/hal/src/lib.rs | 4 ++-- packages/nostr-client/src/__tests__/events.test.ts | 4 ++-- packages/nostr-client/src/events.ts | 4 ++-- packages/nostr-client/src/index.ts | 2 +- packages/nostr-client/src/types.ts | 2 +- packages/ui-shared/vite.config.ts | 2 +- 8 files changed, 14 insertions(+), 14 deletions(-) diff --git a/apps/machine/src/composables/useTheme.ts b/apps/machine/src/composables/useTheme.ts index 52f73ee..becb8eb 100644 --- a/apps/machine/src/composables/useTheme.ts +++ b/apps/machine/src/composables/useTheme.ts @@ -35,8 +35,8 @@ export const themes: ThemeOption[] = [ { id: 'starrynight', label: 'Starry Night' }, ] -const THEME_KEY = 'lamassu-theme' -const MODE_KEY = 'lamassu-color-mode' +const THEME_KEY = 'bitspire-theme' +const MODE_KEY = 'bitspire-color-mode' const isElectron = !!(window as unknown as Record).electronAPI diff --git a/packages/hal/Cargo.toml b/packages/hal/Cargo.toml index 4314786..9ce2209 100644 --- a/packages/hal/Cargo.toml +++ b/packages/hal/Cargo.toml @@ -1,10 +1,10 @@ [package] -name = "lamassu-hal" +name = "bitspire-hal" version = "0.1.0" edition = "2021" -description = "Hardware Abstraction Layer for Lamassu ATM" +description = "Hardware Abstraction Layer for the bitSpire ATM (orphaned Rust HAL; not built)" license = "MIT" -repository = "https://github.com/lamassu/lamassu-next" +repository = "https://git.atitlan.io/aiolabs/bitspire" [lib] crate-type = ["cdylib"] diff --git a/packages/hal/src/lib.rs b/packages/hal/src/lib.rs index 6fff68b..32c501c 100644 --- a/packages/hal/src/lib.rs +++ b/packages/hal/src/lib.rs @@ -1,4 +1,4 @@ -//! Lamassu Hardware Abstraction Layer +//! bitSpire Hardware Abstraction Layer (orphaned Rust HAL — not built; the TypeScript HAL is the real one) //! //! This crate provides Rust implementations of hardware drivers for bill validators, //! dispensers, printers, and other ATM peripherals. These are exposed to Node.js @@ -30,7 +30,7 @@ //! # Usage //! //! ```typescript -//! import { BillValidatorWrapper, ValidatorDriver } from '@lamassu/hal' +//! import { BillValidatorWrapper, ValidatorDriver } from '@bitSpire/hal' //! //! const validator = new BillValidatorWrapper(ValidatorDriver.Id003, '/dev/ttyUSB0', 'USD') //! await validator.connect() diff --git a/packages/nostr-client/src/__tests__/events.test.ts b/packages/nostr-client/src/__tests__/events.test.ts index f072788..5c70830 100644 --- a/packages/nostr-client/src/__tests__/events.test.ts +++ b/packages/nostr-client/src/__tests__/events.test.ts @@ -2,7 +2,7 @@ import { describe, it, expect } from 'vitest' import { generateIdentity } from '../identity.js' import { LocalSigner } from '../signer.js' import { createSignedEvent, createAuthEvent, validateEvent, generateTxId } from '../events.js' -import { LamassuEventKind } from '../types.js' +import { BitSpireEventKind } from '../types.js' describe('events', () => { describe('createSignedEvent', () => { @@ -33,7 +33,7 @@ describe('events', () => { const event = await createAuthEvent(signer, relayUrl, challenge) - expect(event.kind).toBe(LamassuEventKind.Auth) + expect(event.kind).toBe(BitSpireEventKind.Auth) expect(event.content).toBe('') expect(event.tags).toContainEqual(['relay', relayUrl]) expect(event.tags).toContainEqual(['challenge', challenge]) diff --git a/packages/nostr-client/src/events.ts b/packages/nostr-client/src/events.ts index a1ff928..740733f 100644 --- a/packages/nostr-client/src/events.ts +++ b/packages/nostr-client/src/events.ts @@ -4,7 +4,7 @@ import { type Event, type EventTemplate, type VerifiedEvent, getEventHash } from 'nostr-tools' import type { Signer } from './signer.js' -import { LamassuEventKind } from './types.js' +import { BitSpireEventKind } from './types.js' /** * Sign an event template with the given signer. @@ -29,7 +29,7 @@ export function createAuthEvent( challenge: string ): Promise { return signer.signEvent({ - kind: LamassuEventKind.Auth, + kind: BitSpireEventKind.Auth, content: '', tags: [ ['relay', relayUrl], diff --git a/packages/nostr-client/src/index.ts b/packages/nostr-client/src/index.ts index e03b7f3..7cc81a8 100644 --- a/packages/nostr-client/src/index.ts +++ b/packages/nostr-client/src/index.ts @@ -96,7 +96,7 @@ export type { ClientEvents, } from './types.js' -export { LamassuEventKind } from './types.js' +export { BitSpireEventKind } from './types.js' // Re-export useful nostr-tools types export type { Event, UnsignedEvent, Filter } from 'nostr-tools' diff --git a/packages/nostr-client/src/types.ts b/packages/nostr-client/src/types.ts index 90dbc78..6cdb8c5 100644 --- a/packages/nostr-client/src/types.ts +++ b/packages/nostr-client/src/types.ts @@ -86,7 +86,7 @@ export interface SubscriptionOptions { } /** ATM-specific event kinds */ -export enum LamassuEventKind { +export enum BitSpireEventKind { /** CLINK Offer Request/Response */ ClinkOffer = 21001, /** CLINK Debit Request/Response */ diff --git a/packages/ui-shared/vite.config.ts b/packages/ui-shared/vite.config.ts index a0c49f2..443b52b 100644 --- a/packages/ui-shared/vite.config.ts +++ b/packages/ui-shared/vite.config.ts @@ -7,7 +7,7 @@ export default defineConfig({ build: { lib: { entry: resolve(__dirname, 'src/index.ts'), - name: 'LamassuUIShared', + name: 'BitSpireUIShared', fileName: 'index', }, rollupOptions: { From 6524cad7a8b4a72fe6d34d92f104d77ec56d3b07 Mon Sep 17 00:00:00 2001 From: Padreug Date: Fri, 9 Oct 2026 21:58:55 +0200 Subject: [PATCH 11/15] chore(flake): drop the lamassu-live-* alias outputs; retire the lamassu-next comment MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The aliases were added for the brand transition with a note to drop them once nothing referenced them. Nothing does. The autoUpgrade comment still said the legacy aiolabs/lamassu-next repo fed batm3 and douro; every live machine pulls from this repo now (CLAUDE.md → Branch model). --- flake.nix | 15 ++++----------- 1 file changed, 4 insertions(+), 11 deletions(-) diff --git a/flake.nix b/flake.nix index bf76e8f..be6f5d2 100644 --- a/flake.nix +++ b/flake.nix @@ -303,9 +303,8 @@ # upgrade source to ?ref=dev so any ATM flashed from `dev` stays on # `dev`. Without the explicit ?ref=dev, nix would resolve the repo's # default branch and could silently change a dev-deployed Sintra at - # 04:00. The legacy `aiolabs/lamassu-next` repo still feeds the - # not-yet-converted production ATMs (batm3, douro) from its own - # branches; it is retired once those machines migrate to bitspire. + # 04:00. (Every live machine now pulls from this repo; the + # pre-rename `aiolabs/lamassu-next` repo no longer feeds anything.) # To update manually: sudo nixos-rebuild switch --flake git+ssh://forgejo@git.atitlan.io/aiolabs/bitspire.git?ref=dev#-installed system.autoUpgrade = { enable = true; @@ -539,18 +538,12 @@ sintra = mkLiveConfig "sintra"; batm3 = mkLiveConfig "batm3"; - # Backwards-compat aliases. Renamed lamassu-live-* → bitSpire-live-* - # for the brand transition; both styles available until callers - # (CI / scripts / docs) catch up. Drop the lamassu-* names once - # nothing references them. + # Branded aliases for the live configs above. The lamassu-live-* names + # were dropped 2026-10-09; nothing referenced them. bitSpire-live-douro = mkLiveConfig "douro"; bitSpire-live-tejo = mkLiveConfig "tejo"; bitSpire-live-sintra = mkLiveConfig "sintra"; bitSpire-live = mkLiveConfig "douro"; - lamassu-live-douro = mkLiveConfig "douro"; - lamassu-live-tejo = mkLiveConfig "tejo"; - lamassu-live-sintra = mkLiveConfig "sintra"; - lamassu-live = mkLiveConfig "douro"; # Installed-to-disk configs (proper GPT + systemd-boot, supports nixos-rebuild) douro-installed = mkInstalledConfig "douro" ./deploy/nixos/hardware/douro.nix; From 723553522c3b68badbf3f7ed801f07d9e274e796 Mon Sep 17 00:00:00 2001 From: Padreug Date: Fri, 9 Oct 2026 21:58:55 +0200 Subject: [PATCH 12/15] docs: purge stale lamassu naming; fix the hal-check skill's provenance boundary MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - @lamassu/clink import examples → @bitSpire/clink, the package's real name. - machine-installation.md: the service user is `bitspire`, not `lamassu` (renamed in configuration.nix long ago; the doc never followed). - README: clone aiolabs/bitspire, not lamassu-next; the fleet sentence claiming batm3/douro run `main` against Lightning.Pub was stale. - nostr-check skill: table headers say bitSpire. - hal-check skill: the boundary is c0b69d1, not v8.1.5 (CLAUDE.md corrected this 2026-07-04; the skill kept asserting the wrong tag), and the "forbidden operations" now reflect the recorded permission — reference over port, name the source commit — plus a rule born of the GTQ window: no value table without a test over it. Deliberately kept: every `aiolabs/lamassu-next#NN` issue citation, the provenance sections, "Ported from lamassu-machine" driver headers, and the hardware names "Lamassu Sintra/Tejo/Douro" — those are the machines. --- .claude/skills/hal-check.md | 16 ++++++++-------- .claude/skills/nostr-check.md | 4 ++-- README.md | 6 +++--- docs/clink-protocol.md | 8 ++++---- docs/machine-installation.md | 10 +++++----- docs/ndebit-cash-in-flow.md | 8 ++++---- 6 files changed, 26 insertions(+), 26 deletions(-) diff --git a/.claude/skills/hal-check.md b/.claude/skills/hal-check.md index 4f28482..9f38232 100644 --- a/.claude/skills/hal-check.md +++ b/.claude/skills/hal-check.md @@ -2,9 +2,9 @@ ## Purpose -Validate HAL driver implementations in `packages/hal/` against published hardware protocol specs (JCM ID003, Fujitsu F56 DLE/STX, Puloon LCDM, MEI EBDS, etc.) and against the v8.1.5 release line of `lamassu-machine` — which is the **last** lamassu-machine release published under a fully-open license. +Validate HAL driver implementations in `packages/hal/` against published hardware protocol specs (JCM ID003, Fujitsu F56 DLE/STX, Puloon LCDM, MEI EBDS, etc.) and against the public-domain tree of `lamassu-machine` (commit `c0b69d1` and earlier) — which is the **last** lamassu-machine release published under a fully-open license. -> **Provenance boundary.** Drivers in `packages/hal/` derive from `lamassu-machine` at v8.1.5 and earlier (plus hardware-vendor protocol specs). Lamassu Industries AG transitioned to a proprietary source-available license on 2024-01-26 with v8.1.6+ gated behind a paid Operator Support Agreement. **Do not** reference, port, or diff against v8.1.6+ — the only safe upstream tree for porting is `v8.1.5` or earlier. See [CLAUDE.md → Provenance + legal status](../../CLAUDE.md#provenance--legal-status) for the operating rules. +> **Provenance.** Drivers in `packages/hal/` derive from `lamassu-machine` up to commit `c0b69d1` (2023-09-19, v8.6.0-beta.9), the last public-domain commit; `a9234d124d` added Lamassu's Appendix A licence the same day, so every 8.1.5+ *tag* is proprietary — the old "v8.1.5 is the boundary" was wrong. Since 2026-10-09 we hold permission to use the post-boundary code as prior art too (see CLAUDE.md → Provenance): reference over port, and name the source commit when a block is ported verbatim. > **Language note.** ADR-001 selected TypeScript-in-Electron over Rust-in-Tauri for the HAL. Earlier versions of this skill referenced Rust patterns; that's obsolete. All checks below are TypeScript-flavored. @@ -16,7 +16,7 @@ Validate HAL driver implementations in `packages/hal/` against published hardwar Commands: -- `port` — Validate that a driver matches its lamassu-machine v8.1.5 reference (where the driver was ported from one) +- `port` — Validate that a driver matches its lamassu-machine reference (`c0b69d1` tree unless the port names a later commit) (where the driver was ported from one) - `protocol` — Check protocol implementation against published vendor specs - `safety` — Type safety, error handling, hardware safety review - `mock` — Validate mock implementation completeness @@ -31,9 +31,9 @@ Drivers: ### Source reference -Each TS driver in `packages/hal/` maps to (at most) one JS source in lamassu-machine v8.1.5 (the last fully-open release): +Each TS driver in `packages/hal/` maps to (at most) one JS source in lamassu-machine at `c0b69d1` (the last public-domain commit): -| TS Driver | JS Source (v8.1.5 release tree) | +| TS Driver | JS Source (`c0b69d1` tree) | |---|---| | `validators/id003/*.ts` | `lib/id003/*.js` | | `validators/ccnet/*.ts` | `lib/ccnet/*.js` | @@ -156,7 +156,7 @@ function buildPacket(data: Uint8Array): Uint8Array { Against the v8.1.5 JS reference (line numbers may vary by tag): ```javascript -// lamassu-machine v8.1.5 — lib/id003/id003rs232.js +// lamassu-machine c0b69d1 — lib/id003/id003rs232.js function buildPacket(data) { const buf = Buffer.alloc(data.length + 4) buf[0] = 0x02 // SYNC @@ -234,6 +234,6 @@ A discrepancy here (different CRC polynomial, different framing, different endia ## Forbidden operations -- Diff or read `lamassu-machine` source at v8.1.6 or later. Only `v8.1.5` (and the historical commit range leading up to it) is permissible to reference. -- "Backport" any fix or feature from v8.1.6+ JS sources into TypeScript. If a bug fix is needed, implement from the protocol spec or hardware traces. +- Port a post-`c0b69d1` block without naming its source commit in the commit message. The permission to reference that code is recorded in CLAUDE.md; the provenance of anything carried over must be recoverable from `git log`. +- Copy a value table (note lengths, timings) without a test over it — `bills.ts` carried a wrong GTQ window for months precisely because nothing asserted it. - Include attribution comments pointing at v8.1.6+ files even if the implementation is your own — readers should be able to trust file-header attributions as accurate. diff --git a/.claude/skills/nostr-check.md b/.claude/skills/nostr-check.md index aad13fb..63d8bbb 100644 --- a/.claude/skills/nostr-check.md +++ b/.claude/skills/nostr-check.md @@ -17,7 +17,7 @@ Where `target` can be: ## Relevant NIPs for bitSpire ### Core NIPs (Must Implement) -| NIP | Description | Usage in Lamassu | +| NIP | Description | Usage in bitSpire | |-----|-------------|------------------| | NIP-01 | Basic protocol | Event structure, relay communication | | NIP-19 | bech32 entities | npub, nsec, nprofile encoding | @@ -26,7 +26,7 @@ Where `target` can be: | NIP-59 | Gift wrapping | Anonymous message delivery | ### Application NIPs -| NIP | Description | Usage in Lamassu | +| NIP | Description | Usage in bitSpire | |-----|-------------|------------------| | NIP-17 | Private DMs | Receipt delivery | | NIP-47 | Nostr Wallet Connect | Potential wallet integration | diff --git a/README.md b/README.md index fcd43b9..38b34c5 100644 --- a/README.md +++ b/README.md @@ -2,7 +2,7 @@ A Nostr-native Lightning ATM. KYC-free, open source, auditable. Talks to its Lightning backend over the nostr-native-transport (kind-21000 NIP-44 v2) on a relay — never HTTP — so the kiosk has no admin tokens to leak and no API surface to attack. -> Originally `lamassu-next`. Renamed during the LNbits-backend transition on the `dev` branch (commits leading up to 2026-05-13). Production ATMs (`batm3`, `douro`) still run from `main` against Lightning.Pub until cutover; this README describes the `dev` branch state. +> Originally `lamassu-next`. Renamed during the LNbits-backend transition on the `dev` branch (commits leading up to 2026-05-13). Every live machine now runs `dev` against LNbits; `main` is the Lightning.Pub-era history (see CLAUDE.md → Branch model). ## What the ATM actually does @@ -26,8 +26,8 @@ The `nostrrelay` extension inside LNbits is what the ATM connects to — there i ```bash # 1. Clone -git clone ssh://forgejo@git.atitlan.io/aiolabs/lamassu-next.git -cd lamassu-next # repo name kept for now — rename to bitSpire is a follow-up +git clone ssh://forgejo@git.atitlan.io/aiolabs/bitspire.git +cd bitspire git checkout dev # 2. Enter the dev environment diff --git a/docs/clink-protocol.md b/docs/clink-protocol.md index f7b59c8..ac9ac6e 100644 --- a/docs/clink-protocol.md +++ b/docs/clink-protocol.md @@ -68,7 +68,7 @@ noffer1 The ATM wants to receive payment from a customer: ```typescript -import { encodeNoffer } from '@lamassu/clink' +import { encodeNoffer } from '@bitSpire/clink' // ATM creates a noffer for receiving payment const noffer = encodeNoffer({ @@ -134,7 +134,7 @@ ndebit1 The ATM wants to pay the customer (customer inserted cash, wants Bitcoin): ```typescript -import { encodeNdebit, formatNdebitUri } from '@lamassu/clink' +import { encodeNdebit, formatNdebitUri } from '@bitSpire/clink' // ATM creates an ndebit for the customer to authorize withdrawal const ndebit = encodeNdebit({ @@ -438,14 +438,14 @@ import { CLINKClient, createOfferSuccess, createOfferError, -} from '@lamassu/clink' +} from '@bitSpire/clink' ``` ### Reference Implementation - [CLINK Protocol Spec](https://github.com/shocknet/clink) - [Lightning.Pub](https://github.com/shocknet/Lightning.Pub) -- [@lamassu/clink](../packages/clink/) - TypeScript implementation +- [@bitSpire/clink](../packages/clink/) - TypeScript implementation ## Related NIPs diff --git a/docs/machine-installation.md b/docs/machine-installation.md index 5754c9e..b56b3f5 100644 --- a/docs/machine-installation.md +++ b/docs/machine-installation.md @@ -52,7 +52,7 @@ Conceptually: | Layer | Source | Purpose | |---|---|---| | **Kernel + initrd** | `nixpkgs` 24.11 + `upboard.nix` initrd modules | Boot the Sintra hardware (eMMC via `sdhci-acpi`, validator/dispenser at `ttyJ5`/`ttyJ7`) | -| **NixOS base** | `nixpkgs` 24.11 | systemd, Xorg, openbox, the `lamassu` user, sshd for provisioning | +| **NixOS base** | `nixpkgs` 24.11 | systemd, Xorg, openbox, the `bitspire` user, sshd for provisioning | | **bitspire.service** | `deploy/nixos/bitspire-atm.nix` | systemd unit that launches the Electron kiosk | | **The Electron app** | `apps/machine` built into a nix derivation | The actual ATM UI + state machine + Lightning client | | **Hardware-specific config** | `deploy/nixos/hardware/upboard.nix` (or `douro.nix`, `batm3.nix`) | udev rules, kernel modules, panel calibration | @@ -82,11 +82,11 @@ Before flashing a Sintra you'll want: Once the kiosk is up, useful things to know: -- **Service status:** `ssh lamassu@ 'sudo systemctl status bitspire'` -- **Live log tail:** `ssh lamassu@ 'sudo journalctl -u bitspire -f'` +- **Service status:** `ssh bitspire@ 'sudo systemctl status bitspire'` +- **Live log tail:** `ssh bitspire@ 'sudo journalctl -u bitspire -f'` - **Re-provision (e.g., wrong relay URL):** rerun `provision-atm.sh` from the dev box with the new env vars -- **Push a code change without reflashing:** `nixos-rebuild switch --flake .#sintra-installed --target-host lamassu@ --use-remote-sudo` -- **Inspect transaction history:** `ssh lamassu@ 'sudo bash /etc/nixos/atm-transactions.sh'` (queries `/var/lib/bitspire/state.db`) +- **Push a code change without reflashing:** `nixos-rebuild switch --flake .#sintra-installed --target-host bitspire@ --use-remote-sudo` +- **Inspect transaction history:** `ssh bitspire@ 'sudo bash /etc/nixos/atm-transactions.sh'` (queries `/var/lib/bitspire/state.db`) ## Related documentation diff --git a/docs/ndebit-cash-in-flow.md b/docs/ndebit-cash-in-flow.md index e6b9688..2119bf0 100644 --- a/docs/ndebit-cash-in-flow.md +++ b/docs/ndebit-cash-in-flow.md @@ -139,7 +139,7 @@ export const NDEBIT_REGEX = new RegExp( ```typescript // In lib/types/parse.ts -import type { DebitPointer } from '@lamassu/clink' +import type { DebitPointer } from '@bitSpire/clink' import type { Satoshi } from './units' export enum InputClassification { @@ -159,7 +159,7 @@ export interface ParsedNdebitInput { ```typescript // In lib/parse.ts -import { decodeNdebit } from '@lamassu/clink' +import { decodeNdebit } from '@bitSpire/clink' // Add to VALIDATORS array: { @@ -209,7 +209,7 @@ case InputClassification.NDEBIT: { ```typescript // In State/scoped/backups/sources/history/claimNdebitThunk.ts import { getNostrClient } from '@/Api/nostr' -// Note: SendNdebitRequest is wallet-side code, not part of @lamassu/clink +// Note: SendNdebitRequest is wallet-side code, not part of @bitSpire/clink // This example shows the wallet's implementation pattern import { finalizeEvent } from 'nostr-tools' import { SimplePool } from 'nostr-tools' @@ -849,7 +849,7 @@ See "Single-Use Protection" section above for implementation details. ```json { - "@lamassu/clink": "workspace:*", + "@bitSpire/clink": "workspace:*", "nostr-tools": "^2.x.x", "@noble/hashes": "^1.x.x", "qrcode": "^1.x.x" From 763817b9f350cd3551817615c73c4a85625296e0 Mon Sep 17 00:00:00 2001 From: Padreug Date: Fri, 9 Oct 2026 22:25:07 +0200 Subject: [PATCH 13/15] chore(dev): remove the Lightning.Pub-era regtest tooling MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit docker/ (two compose stacks, dev.sh, regtest.sh, start-with-regtest.sh, regtest-bootstrap.sh, strfry.conf), packages/nostr-client/dev/ (nine agent and test scripts), and the seventeen devenv commands that drove them — infra-*, lncli, btccli, mine-blocks, auto-mine, setup-channel, alice-*, fund-atm, test-setup, test-payment, node-info — along with the devenv postgres service, DATABASE_URL, LIGHTNING_PUB_URL, pgcli, docker-compose and the `just` runner (no justfile exists). None of it could talk to the app on `dev`. Every piece was built around Lightning.Pub (a `lightning-pub` service in both compose files, 37 references in dev.sh, LIGHTNING_PUB_PUBKEY and the :1776 API in every dev script, a NIP-44 v1 implementation the project forbids), and the last substantive change predates the LNbits cutover that deleted packages/lightning. No container under either name exists on any machine. Development runs against LNbits: FakeWallet needs nothing, bohm's native instance answers on :5001, and the shared regtest stack lives at ~/dev/local/docker/regtest, outside this repo. devenv.nix keeps the toolchain, the hardware/serial utilities, the git hooks and `relay-test`, now pointed at LNbits's bundled nostrrelay. The Rust toolchain stays for the orphaned crate until that is removed on its own. nostr-client drops the four dependencies and two devDependencies only the dead scripts imported (@noble/curves, @scure/base, @shocknet/clink-sdk, @stablelib/xchacha20, qrcode, ws); lockfile regenerated, −272 lines. Verified: devenv.nix parses; nostr-client 43/43 + tsc; clink 11/11 + tsc; machine app vue-tsc clean. The ndebit-cash-in-flow doc, kept as CLINK design history, now says the commands it quotes no longer exist here. --- devenv.nix | 489 +------- docker/dev.sh | 1087 ----------------- docker/docker-compose.dev.yml | 226 ---- docker/docker-compose.regtest.yml | 166 --- docker/regtest-bootstrap.sh | 273 ----- docker/regtest.sh | 142 --- docker/start-with-regtest.sh | 335 ----- docker/strfry.conf | 65 - docs/ndebit-cash-in-flow.md | 5 + packages/nostr-client/dev/atm-debit-agent.mjs | 270 ---- packages/nostr-client/dev/fund-dev.mjs | 133 -- packages/nostr-client/dev/generate-ndebit.mjs | 84 -- packages/nostr-client/dev/mock-machine.mjs | 833 ------------- packages/nostr-client/dev/nip44v1.mjs | 111 -- packages/nostr-client/dev/run-debit-agent.mjs | 221 ---- packages/nostr-client/dev/test-debit.mjs | 182 --- packages/nostr-client/dev/test-ndebit.mjs | 181 --- packages/nostr-client/dev/test-pay.mjs | 116 -- packages/nostr-client/package.json | 8 +- pnpm-lock.yaml | 272 ----- 20 files changed, 29 insertions(+), 5170 deletions(-) delete mode 100755 docker/dev.sh delete mode 100644 docker/docker-compose.dev.yml delete mode 100644 docker/docker-compose.regtest.yml delete mode 100755 docker/regtest-bootstrap.sh delete mode 100755 docker/regtest.sh delete mode 100755 docker/start-with-regtest.sh delete mode 100644 docker/strfry.conf delete mode 100644 packages/nostr-client/dev/atm-debit-agent.mjs delete mode 100644 packages/nostr-client/dev/fund-dev.mjs delete mode 100644 packages/nostr-client/dev/generate-ndebit.mjs delete mode 100644 packages/nostr-client/dev/mock-machine.mjs delete mode 100644 packages/nostr-client/dev/nip44v1.mjs delete mode 100644 packages/nostr-client/dev/run-debit-agent.mjs delete mode 100644 packages/nostr-client/dev/test-debit.mjs delete mode 100644 packages/nostr-client/dev/test-ndebit.mjs delete mode 100644 packages/nostr-client/dev/test-pay.mjs diff --git a/devenv.nix b/devenv.nix index b9e5faf..f8523fd 100644 --- a/devenv.nix +++ b/devenv.nix @@ -1,8 +1,7 @@ { pkgs, lib, config, ... }: { - # Project metadata - name = "bitspire-next"; + name = "bitspire"; # ============================================ # Languages @@ -19,9 +18,11 @@ languages.typescript.enable = true; - # Use nixpkgs rust (simpler, no overlay needed) packages = with pkgs; [ - # Rust toolchain from nixpkgs + # Rust toolchain — only the orphaned Rust HAL under packages/hal/src + # uses it (not built; see CLAUDE.md → Hardware drivers). Kept so the + # rustfmt git hook and `cargo check` still work until that crate is + # removed outright. rustc cargo clippy @@ -33,42 +34,22 @@ openssl openssl.dev - # Electron dependencies (Linux) - # electron # Use npm-installed electron instead - - # Python for node-gyp (native module builds) + # Python for node-gyp (native module builds: serialport, better-sqlite3) (python3.withPackages (ps: [ ps.setuptools ])) # Hardware access libusb1 udev - # Serial port access + # Serial port access (talk to a validator / dispenser by hand) picocom # Development utilities - just # Task runner jq # JSON processing - websocat # WebSocket client + websocat # WebSocket client — relay-test below qrencode # QR code generation for terminal - - # Database tools - pgcli - - # Container tools (for Lightning.Pub, strfry) - docker-compose ]; - # ============================================ - # Services - # ============================================ - - services.postgres = { - enable = true; - initialDatabases = [{ name = "bitspire_dev"; }]; - listen_addresses = "127.0.0.1"; - }; - # ============================================ # Git Hooks (formerly pre-commit) # ============================================ @@ -97,21 +78,24 @@ env = { RUST_BACKTRACE = "1"; - DATABASE_URL = "postgresql://localhost/bitspire_dev"; - # Nostr development relay - NOSTR_RELAY_URL = "ws://localhost:7777"; + # The dev relay is LNbits's bundled nostrrelay extension on the native + # instance (see CLAUDE.md → Environment variables); there is no + # in-repo relay container any more. Override per shell as needed. + NOSTR_RELAY_URL = "ws://localhost:5001/nostrrelay/test"; - # Lightning.Pub development - LIGHTNING_PUB_URL = "http://localhost:1776"; - - # For Tauri PKG_CONFIG_PATH = "${pkgs.openssl.dev}/lib/pkgconfig"; }; # ============================================ # Scripts (available as commands in shell) # ============================================ + # + # The Lightning.Pub-era regtest stack (docker/ + the infra-*/lncli/btccli/ + # mine-blocks/setup-channel/fund-atm/test-* commands that drove it) was + # removed 2026-10-09. Development runs against LNbits: FakeWallet needs + # nothing; bohm's native instance answers on :5001; the shared regtest + # stack lives at ~/dev/local/docker/regtest (not in this repo). scripts = { dev.exec = "pnpm turbo dev"; @@ -119,418 +103,13 @@ test.exec = "pnpm turbo test"; lint.exec = "pnpm turbo lint"; - # Start infrastructure - infra-up.exec = '' - echo "Starting development infrastructure..." - docker compose -f docker/docker-compose.dev.yml up -d - echo "" - echo "Waiting for services to be healthy..." - echo "(This may take 30-60 seconds on first run)" - echo "" - - # Wait for strfry - echo -n "strfry relay: " - until docker exec bitspire-relay nc -z localhost 7777 2>/dev/null; do - echo -n "." - sleep 2 - done - echo " ready" - - # Wait for bitcoind - echo -n "bitcoind: " - until docker exec bitspire-bitcoind bitcoin-cli -regtest -rpcuser=bitspire -rpcpassword=bitspire getblockchaininfo >/dev/null 2>&1; do # pragma: allowlist secret - echo -n "." - sleep 2 - done - echo " ready" - - # Wait for LND - echo -n "LND: " - until docker exec bitspire-lnd lncli --network=regtest getinfo >/dev/null 2>&1; do - echo -n "." - sleep 3 - done - echo " ready" - - # Wait for Alice's LND - echo -n "LND (Alice): " - until docker exec bitspire-lnd-alice lncli --network=regtest getinfo >/dev/null 2>&1; do - echo -n "." - sleep 3 - done - echo " ready" - - echo "" - echo "Infrastructure ready!" - echo " - Nostr relay: ws://localhost:7777" - echo " - Bitcoin RPC: localhost:18443 (regtest)" - echo " - LND gRPC: localhost:10009" - echo " - LND Alice gRPC: localhost:10010" - echo " - Lightning.Pub: http://localhost:1776" - echo " - PostgreSQL: localhost:5432" - echo "" - echo "Next steps:" - echo " 1. mine-blocks 101 # Fund the wallet (if first run)" - echo " 2. setup-channel # Open channel between Alice and LND" - echo "" - echo "Use 'infra-logs' to view logs, 'infra-status' to check status" - ''; - - infra-down.exec = '' - echo "Stopping development infrastructure..." - docker compose -f docker/docker-compose.dev.yml down - echo "Infrastructure stopped" - ''; - - infra-logs.exec = '' - docker compose -f docker/docker-compose.dev.yml logs -f "$@" - ''; - - infra-status.exec = '' - echo "Infrastructure Status:" - echo "" - docker compose -f docker/docker-compose.dev.yml ps - ''; - - # Mine regtest blocks (useful for testing) - mine-blocks.exec = '' - BLOCKS=''${1:-1} - echo "Mining $BLOCKS regtest block(s)..." - docker exec bitspire-bitcoind bitcoin-cli -regtest -rpcuser=bitspire -rpcpassword=bitspire -generate "$BLOCKS" # pragma: allowlist secret - ''; - - # Start auto-miner (mines 1 block every 30 seconds) - auto-mine.exec = '' - echo "Starting auto-miner (1 block every 30 seconds)..." - docker compose -f docker/docker-compose.dev.yml --profile mining up -d miner - echo "" - echo "Auto-miner started. This keeps LND in sync." - echo "Use 'auto-mine-stop' to stop it." - ''; - - # Stop auto-miner - auto-mine-stop.exec = '' - echo "Stopping auto-miner..." - docker stop bitspire-miner 2>/dev/null || true - docker rm bitspire-miner 2>/dev/null || true - echo "Auto-miner stopped." - ''; - - # Connect to LND CLI - lncli.exec = '' - docker exec -it bitspire-lnd lncli --network=regtest "$@" - ''; - - # Connect to Bitcoin CLI - btccli.exec = '' - docker exec -it bitspire-bitcoind bitcoin-cli -regtest -rpcuser=bitspire -rpcpassword=bitspire "$@" # pragma: allowlist secret - ''; - - # Test relay connection + # Test the dev relay connection relay-test.exec = '' - echo "Testing Nostr relay connection..." - echo '["REQ", "test", {"kinds": [0], "limit": 1}]' | websocat ws://localhost:7777 - ''; - - # Connect to Alice's LND CLI (second node for testing payments) - lncli-alice.exec = '' - docker exec -it bitspire-lnd-alice lncli --network=regtest "$@" - ''; - - # Setup Lightning channel between Alice and the main LND node - setup-channel.exec = '' - echo "Setting up Lightning channel between Alice and LND..." - echo "" - - # Get LND's pubkey and address - LND_INFO=$(docker exec bitspire-lnd lncli --network=regtest getinfo 2>/dev/null) - LND_PUBKEY=$(echo "$LND_INFO" | jq -r '.identity_pubkey') - echo "LND pubkey: $LND_PUBKEY" - - # Connect Alice to LND - echo "Connecting Alice to LND..." - docker exec bitspire-lnd-alice lncli --network=regtest connect "$LND_PUBKEY@lnd:9735" 2>/dev/null || true - - # Check if Alice has enough funds - ALICE_BALANCE=$(docker exec bitspire-lnd-alice lncli --network=regtest walletbalance 2>/dev/null | jq -r '.confirmed_balance') - echo "Alice's on-chain balance: $ALICE_BALANCE sats" - - if [ "$ALICE_BALANCE" -lt 1000000 ]; then - echo "" - echo "Alice needs funds. Getting new address..." - ALICE_ADDR=$(docker exec bitspire-lnd-alice lncli --network=regtest newaddress p2wkh | jq -r '.address') - echo "Alice's address: $ALICE_ADDR" - echo "" - echo "Sending 5 BTC to Alice..." - docker exec bitspire-bitcoind bitcoin-cli -regtest -rpcuser=bitspire -rpcpassword=bitspire sendtoaddress "$ALICE_ADDR" 5 # pragma: allowlist secret - echo "Mining 6 blocks for confirmation..." - docker exec bitspire-bitcoind bitcoin-cli -regtest -rpcuser=bitspire -rpcpassword=bitspire -generate 6 >/dev/null # pragma: allowlist secret - sleep 2 - ALICE_BALANCE=$(docker exec bitspire-lnd-alice lncli --network=regtest walletbalance 2>/dev/null | jq -r '.confirmed_balance') - echo "Alice's new balance: $ALICE_BALANCE sats" - fi - - # Open channel from Alice to LND (1M sats) - echo "" - echo "Opening 1M sat channel from Alice to LND..." - docker exec bitspire-lnd-alice lncli --network=regtest openchannel --node_key="$LND_PUBKEY" --local_amt=1000000 - - echo "" - echo "Mining 6 blocks to confirm channel..." - docker exec bitspire-bitcoind bitcoin-cli -regtest -rpcuser=bitspire -rpcpassword=bitspire -generate 6 >/dev/null # pragma: allowlist secret - - sleep 3 - echo "" - echo "Channel status:" - docker exec bitspire-lnd-alice lncli --network=regtest listchannels | jq '.channels[] | {remote_pubkey, capacity, local_balance, remote_balance, active}' - echo "" - echo "Channel setup complete! Alice can now pay invoices to Lightning.Pub." - ''; - - # Pay an invoice from Alice's node - alice-pay.exec = '' - if [ -z "$1" ]; then - echo "Usage: alice-pay " - exit 1 - fi - echo "Paying invoice from Alice's node..." - docker exec bitspire-lnd-alice lncli --network=regtest payinvoice --force "$1" - ''; - - # Create invoice on Alice's node (for testing ATM payouts) - alice-invoice.exec = '' - AMOUNT=''${1:-1000} - MEMO=''${2:-"Test invoice"} - docker exec bitspire-lnd-alice lncli --network=regtest addinvoice --amt="$AMOUNT" --memo="$MEMO" | jq -r '.payment_request' - ''; - - # Fund ATM account in Lightning.Pub - fund-atm.exec = '' - AMOUNT=''${1:-100000} - echo "Funding ATM account with $AMOUNT sats..." - echo "" - - # Run the funding script from nostr-client package - cd packages/nostr-client - FUND_AMOUNT=$AMOUNT node fund-dev.mjs 2>&1 | tee /tmp/fund-atm-output.txt - - # Extract the invoice - INVOICE=$(grep -o 'lnbcrt[a-zA-Z0-9]*' /tmp/fund-atm-output.txt | head -1) - - if [ -z "$INVOICE" ]; then - echo "Failed to create invoice. Check the output above." - exit 1 - fi - - echo "" - echo "Invoice created. Paying from Alice..." - docker exec bitspire-lnd-alice lncli --network=regtest payinvoice --force "$INVOICE" - - echo "" - echo "ATM account funded with $AMOUNT sats!" - ''; - - # Validate entire test setup - test-setup.exec = '' - echo "" - echo "═══════════════════════════════════════════════════════════" - echo " bitSpire - Test Setup Validation" - echo "═══════════════════════════════════════════════════════════" - echo "" - - # Mine a block to wake up LND sync (regtest quirk: LND reports - # "not synced" when no blocks mined recently) - echo "Mining block to sync nodes..." - docker exec bitspire-bitcoind bitcoin-cli -regtest -rpcuser=bitspire -rpcpassword=bitspire -generate 1 >/dev/null 2>&1 # pragma: allowlist secret - sleep 1 - echo "" - - PASSED=0 - FAILED=0 - - check() { - if [ $1 -eq 0 ]; then - echo " ✓ $2" - PASSED=$((PASSED + 1)) - else - echo " ✗ $2" - FAILED=$((FAILED + 1)) - fi - } - - echo "1. Services" - echo "───────────────────────────────────────────────────────────" - - # Check containers - docker ps --format '{{.Names}}' | grep -q bitspire-relay - check $? "strfry relay running" - - docker ps --format '{{.Names}}' | grep -q bitspire-bitcoind - check $? "bitcoind running" - - docker ps --format '{{.Names}}' | grep -q bitspire-lnd - check $? "LND running" - - docker ps --format '{{.Names}}' | grep -q bitspire-lnd-alice - check $? "LND Alice running" - - docker ps --format '{{.Names}}' | grep -q bitspire-lightning-pub - check $? "Lightning.Pub running" - - echo "" - echo "2. Connectivity" - echo "───────────────────────────────────────────────────────────" - - # Check relay port is open - timeout 2 bash -c 'echo > /dev/tcp/localhost/7777' 2>/dev/null - check $? "Nostr relay port open" - - # Check bitcoind RPC - docker exec bitspire-bitcoind bitcoin-cli -regtest -rpcuser=bitspire -rpcpassword=bitspire getblockchaininfo >/dev/null 2>&1 # pragma: allowlist secret - check $? "Bitcoin RPC responding" - - # Check LND - docker exec bitspire-lnd lncli --network=regtest getinfo >/dev/null 2>&1 - check $? "LND RPC responding" - - # Check Alice - docker exec bitspire-lnd-alice lncli --network=regtest getinfo >/dev/null 2>&1 - check $? "LND Alice RPC responding" - - echo "" - echo "3. Blockchain State" - echo "───────────────────────────────────────────────────────────" - - BLOCKS=$(docker exec bitspire-bitcoind bitcoin-cli -regtest -rpcuser=bitspire -rpcpassword=bitspire getblockcount 2>/dev/null) # pragma: allowlist secret - [ "$BLOCKS" -ge 100 ] - check $? "Block height >= 100 (current: $BLOCKS)" - - # Check LND synced - LND_SYNCED=$(docker exec bitspire-lnd lncli --network=regtest getinfo 2>/dev/null | jq -r '.synced_to_chain') - [ "$LND_SYNCED" = "true" ] - check $? "LND synced to chain" - - # Check Alice synced - ALICE_SYNCED=$(docker exec bitspire-lnd-alice lncli --network=regtest getinfo 2>/dev/null | jq -r '.synced_to_chain') - [ "$ALICE_SYNCED" = "true" ] - check $? "LND Alice synced to chain" - - echo "" - echo "4. Lightning Channels" - echo "───────────────────────────────────────────────────────────" - - # Check Alice has active channels - ALICE_CHANNELS=$(docker exec bitspire-lnd-alice lncli --network=regtest listchannels 2>/dev/null | jq '.channels | length') - [ "$ALICE_CHANNELS" -ge 1 ] - check $? "Alice has active channels (count: $ALICE_CHANNELS)" - - # Check channel is active - ACTIVE_CHANNEL=$(docker exec bitspire-lnd-alice lncli --network=regtest listchannels 2>/dev/null | jq '.channels[0].active') - [ "$ACTIVE_CHANNEL" = "true" ] - check $? "Channel is active" - - # Check Alice has outbound capacity - ALICE_LOCAL=$(docker exec bitspire-lnd-alice lncli --network=regtest listchannels 2>/dev/null | jq -r '.channels[0].local_balance // 0') - [ "$ALICE_LOCAL" -ge 10000 ] - check $? "Alice has outbound capacity ($ALICE_LOCAL sats)" - - echo "" - echo "5. Payment Test" - echo "───────────────────────────────────────────────────────────" - - # Create test invoice on LND and pay from Alice - TEST_INVOICE=$(docker exec bitspire-lnd lncli --network=regtest addinvoice --amt=100 --memo="test-setup validation" 2>/dev/null | jq -r '.payment_request') - if [ -n "$TEST_INVOICE" ]; then - check 0 "Created 100 sat test invoice" - - # Pay it - PAY_RESULT=$(docker exec bitspire-lnd-alice lncli --network=regtest payinvoice --force "$TEST_INVOICE" 2>&1) - if echo "$PAY_RESULT" | grep -q "SUCCEEDED"; then - check 0 "Payment Alice → LND succeeded" - else - check 1 "Payment Alice → LND failed" - fi - else - check 1 "Failed to create test invoice" - check 1 "Payment test skipped" - fi - - echo "" - echo "═══════════════════════════════════════════════════════════" - echo " Results: $PASSED passed, $FAILED failed" - echo "═══════════════════════════════════════════════════════════" - echo "" - - if [ $FAILED -gt 0 ]; then - echo "Some checks failed. Run 'infra-up' and 'setup-channel' first." - exit 1 - else - echo "All checks passed! Ready for testing." - exit 0 - fi - ''; - - # Quick e2e payment test (ATM pays customer invoice) - test-payment.exec = '' - AMOUNT=''${1:-1000} - echo "Testing e2e payment flow ($AMOUNT sats)..." - echo "" - - # Create invoice on Alice (simulating customer's wallet) - echo "1. Creating invoice on Alice's wallet..." - INVOICE=$(docker exec bitspire-lnd-alice lncli --network=regtest addinvoice --amt="$AMOUNT" --memo="E2E test" | jq -r '.payment_request') - echo " Invoice: ''${INVOICE:0:40}..." - - # Use test-pay.mjs to pay via Lightning.Pub - echo "" - echo "2. Paying via Lightning.Pub (ATM flow)..." - cd packages/nostr-client - node test-pay.mjs "$INVOICE" - - echo "" - echo "3. Verifying payment on Alice..." - sleep 2 - PAID=$(docker exec bitspire-lnd-alice lncli --network=regtest listinvoices 2>/dev/null | jq '.invoices[-1].settled') - if [ "$PAID" = "true" ]; then - echo " ✓ Payment received!" - else - echo " ✗ Payment not received" - exit 1 - fi - ''; - - # Show node info - node-info.exec = '' - echo "" - echo "Node Information" - echo "════════════════════════════════════════════════════════" - echo "" - - echo "LND (Lightning.Pub's node):" - docker exec bitspire-lnd lncli --network=regtest getinfo 2>/dev/null | jq '{pubkey: .identity_pubkey, alias: .alias, channels: .num_active_channels, peers: .num_peers}' - - echo "" - echo "LND Alice (Payment source):" - docker exec bitspire-lnd-alice lncli --network=regtest getinfo 2>/dev/null | jq '{pubkey: .identity_pubkey, alias: .alias, channels: .num_active_channels, peers: .num_peers}' - - echo "" - echo "Channel Details:" - docker exec bitspire-lnd-alice lncli --network=regtest listchannels 2>/dev/null | jq '.channels[] | {peer: .remote_pubkey[0:16], capacity, local: .local_balance, remote: .remote_balance, active}' - - echo "" - echo "Lightning.Pub Nostr pubkey:" - echo " f454c5eec4ec4474128e19b57b45e49c3d0851d01eea960b39ce391cdba76fc6" - - echo "" - echo "ATM Dev Identity:" - echo " 4646ae5047316b4230d0086c8acec687f00b1cd9d1dc634f6cb358ac0a9a8fff" + echo "Testing Nostr relay connection to $NOSTR_RELAY_URL ..." + echo '["REQ", "test", {"kinds": [0], "limit": 1}]' | websocat "$NOSTR_RELAY_URL" ''; }; - # ============================================ - # Shell Hook - # ============================================ - enterShell = '' echo "" echo " ⚡ bitSpire - Nostr-Native Lightning ATM" @@ -543,30 +122,8 @@ echo " dev Start development servers" echo " build Build all packages" echo " test Run tests" - echo "" - echo " Infrastructure:" - echo " infra-up Start Docker services (strfry, bitcoind, LND, Lightning.Pub)" - echo " infra-down Stop Docker services" - echo " infra-status Show service status" - echo " infra-logs Follow service logs" - echo "" - echo " Bitcoin/Lightning:" - echo " btccli Bitcoin CLI (regtest)" - echo " lncli LND CLI (Lightning.Pub's node)" - echo " lncli-alice LND CLI (Alice's node for testing payments)" - echo " mine-blocks Mine regtest blocks (default: 1)" - echo " auto-mine Start auto-miner (1 block/30s, keeps LND synced)" - echo " auto-mine-stop Stop auto-miner" - echo " setup-channel Setup channel between Alice and LND" - echo " alice-pay Pay invoice from Alice's node" - echo " relay-test Test Nostr relay connection" - echo "" - echo " Testing (E2E):" - echo " test-setup Validate test environment (services, channels, payments)" - echo " test-payment Quick e2e payment test (ATM → customer)" - echo " fund-atm Fund ATM account (default: 100k sats)" - echo " alice-invoice Create invoice on Alice's node" - echo " node-info Show node pubkeys and channel info" + echo " lint Lint all packages" + echo " relay-test Test the dev relay ($NOSTR_RELAY_URL)" echo "" ''; } diff --git a/docker/dev.sh b/docker/dev.sh deleted file mode 100755 index f47d42f..0000000 --- a/docker/dev.sh +++ /dev/null @@ -1,1087 +0,0 @@ -#!/bin/bash -# -# bitSpire Development Environment -# -# Single command to manage the complete development stack: -# - Regtest Bitcoin/Lightning network (from ~/dev/local/docker/regtest) -# - Lightning.Pub with configurable image/worktree -# - Auto-configured ATM application -# -# Usage: -# ./dev.sh up # Start (uses shared regtest) -# ./dev.sh up --standalone # Start self-contained (own bitcoind/lnd) -# ./dev.sh up --fund # Start and auto-fund ATM with 100k sats -# ./dev.sh up --fund=50000 # Start and auto-fund with specific amount -# ./dev.sh up --machine # Start and launch ATM app -# ./dev.sh up --fund --machine # Start, fund, and launch ATM -# ./dev.sh up --worktree ~/path/to/lp # Build Lightning.Pub from worktree -# ./dev.sh up --image myimage:tag # Use specific Docker image -# ./dev.sh down # Stop everything -# ./dev.sh atm # Launch ATM application -# ./dev.sh status # Show connection info -# ./dev.sh logs [service] # Follow logs -# ./dev.sh fund [amount] # Fund ATM (default: 100000 sats) -# ./dev.sh mine [blocks] # Mine blocks manually -# ./dev.sh reset # Reset all state (fresh start) -# - -set -e - -SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" -PROJECT_DIR="$(dirname "$SCRIPT_DIR")" -REGTEST_DIR="${REGTEST_DIR:-$HOME/dev/local/docker/regtest}" -DEFAULT_IMAGE="lightning-pub-withdraw:latest" -DEFAULT_FUNDING_SATS=100000 - -# State files -STATE_DIR="$SCRIPT_DIR/.state" -PUBKEY_FILE="$STATE_DIR/lightning-pub-pubkey" -NPROFILE_FILE="$STATE_DIR/lightning-pub-nprofile" -APP_ID_FILE="$STATE_DIR/atm-app-id" -MODE_FILE="$STATE_DIR/compose-mode" # "standalone" or "unified" - -# Get current compose file based on last used mode -get_compose_file() { - local mode=$(cat "$MODE_FILE" 2>/dev/null || echo "unified") - if [[ "$mode" == "standalone" ]]; then - echo "docker-compose.dev.yml" - else - echo "docker-compose.regtest.yml" - fi -} - -# Colors -GREEN='\033[0;32m' -YELLOW='\033[1;33m' -RED='\033[0;31m' -CYAN='\033[0;36m' -BOLD='\033[1m' -DIM='\033[2m' -NC='\033[0m' - -log() { echo -e "${GREEN}►${NC} $1"; } -warn() { echo -e "${YELLOW}⚠${NC} $1"; } -error() { echo -e "${RED}✗${NC} $1"; } -success() { echo -e "${GREEN}✓${NC} $1"; } - -# Ensure state directory exists -mkdir -p "$STATE_DIR" - -# Get ATM pubkey from its private key # pragma: allowlist secret -get_atm_pubkey() { - local env_file="$PROJECT_DIR/apps/machine/.env" - if [[ ! -f "$env_file" ]]; then - return 1 - fi - - local privkey=$(grep "VITE_ATM_PRIVATE_KEY=" "$env_file" | cut -d= -f2) - if [[ -z "$privkey" ]]; then - return 1 - fi - - # Use node with @noble/curves to derive pubkey from privkey - cd "$PROJECT_DIR" && node -e " - const { secp256k1 } = require('./node_modules/.pnpm/@noble+curves@2.0.1/node_modules/@noble/curves/secp256k1.js'); - const privkeyHex = '$privkey'; - const privkey = Uint8Array.from(Buffer.from(privkeyHex, 'hex')); - const pubkeyFull = secp256k1.getPublicKey(privkey, true); - const pubkey = Buffer.from(pubkeyFull.slice(1)).toString('hex'); - console.log(pubkey); - " 2>/dev/null -} - -############################################################################# -# Helper Functions -############################################################################# - -get_local_ip() { - # Allow explicit override via env var - if [[ -n "${BITSPIRE_HOST_IP:-}" ]]; then - echo "$BITSPIRE_HOST_IP" - return - fi - - # Linux: use ip route - if command -v ip &>/dev/null; then - local ip=$(ip route get 1 2>/dev/null | awk '{print $7; exit}') - if [[ -n "$ip" ]]; then - echo "$ip" - return - fi - fi - - # macOS: use route + ifconfig - if [[ "$(uname)" == "Darwin" ]]; then - local iface=$(route get default 2>/dev/null | awk '/interface:/ {print $2}') - if [[ -n "$iface" ]]; then - local ip=$(ifconfig "$iface" 2>/dev/null | awk '/inet / {print $2}') - if [[ -n "$ip" ]]; then - echo "$ip" - return - fi - fi - fi - - # Fallback: hostname -I (Linux) or hostname (macOS) - hostname -I 2>/dev/null | awk '{print $1}' || hostname 2>/dev/null -} - -is_regtest_running() { - # Check if lnd-4 container is actually running (not just network exists) - docker ps --filter "name=lnbits-lnd-4-1" --format "{{.Names}}" 2>/dev/null | grep -q lnbits-lnd-4-1 -} - -is_lnd4_ready() { - # Use lnd-4 hostname (not localhost) since lnd binds to container IP - docker exec lnbits-lnd-4-1 lncli --network=regtest --rpcserver=lnd-4:10009 getinfo &>/dev/null 2>&1 -} - -lnd4_has_channels() { - local channel_count=$(docker exec lnbits-lnd-4-1 lncli --network=regtest --rpcserver=lnd-4:10009 listchannels 2>/dev/null | grep -c '"chan_id"' || echo "0") - [[ "$channel_count" -gt 0 ]] -} - -lnd4_has_active_channels() { - docker exec lnbits-lnd-4-1 lncli --network=regtest --rpcserver=lnd-4:10009 listchannels 2>/dev/null | grep -q '"active": true' -} - -is_lightning_pub_ready() { - docker logs bitspire-lightning-pub 2>&1 | grep -q "LightningPub listening" -} - -get_lnd4_balance() { - docker exec lnbits-lnd-4-1 lncli --network=regtest --rpcserver=lnd-4:10009 walletbalance 2>/dev/null | grep -oP '"total_balance":\s*"\K[0-9]+' || echo "0" -} - -get_lnd4_channel_balance() { - docker exec lnbits-lnd-4-1 lncli --network=regtest --rpcserver=lnd-4:10009 channelbalance 2>/dev/null | grep -oP '"local_balance"[^}]*"sat":\s*"\K[0-9]+' || echo "0" -} - -############################################################################# -# Regtest Management -############################################################################# - -is_lnd1_ready() { - # Check if lnd-1 is fully ready (can make RPC calls) - docker exec lnbits-lnd-1-1 lncli --network=regtest --rpcserver=lnd-1:10009 getinfo &>/dev/null 2>&1 -} - -is_lnd1_wallet_ready() { - # Check if lnd-1 wallet is ready (can query balance - needed for channel operations) - docker exec lnbits-lnd-1-1 lncli --network=regtest --rpcserver=lnd-1:10009 walletbalance &>/dev/null 2>&1 -} - -wait_for_lnd1_wallet() { - local attempts=0 - while ! is_lnd1_wallet_ready && [[ $attempts -lt 30 ]]; do - sleep 2 - attempts=$((attempts + 1)) - done - is_lnd1_wallet_ready -} - -wait_for_lnd1() { - log "Waiting for lnd-1 to be ready..." - local attempts=0 - while ! is_lnd1_ready && [[ $attempts -lt 30 ]]; do - sleep 2 - attempts=$((attempts + 1)) - done - is_lnd1_ready -} - -ensure_lnd4_channel() { - # Ensure lnd-4 has a channel so it can create invoices - # Also ensure lnd-3 has a channel so it can pay invoices (for funding) - # Opens channels from lnd-1 (hub) to both lnd-3 and lnd-4 - - if lnd4_has_active_channels; then - log "lnd-4 has active channels" - return 0 - fi - - log "Setting up Lightning channels for ATM..." - - # Wait for lnd-1 to be ready (it's the funding hub) - if ! wait_for_lnd1; then - error "lnd-1 failed to start" - return 1 - fi - success "lnd-1 is ready" - - # Wait for wallet to be fully initialized (needed for channel operations) - log "Waiting for lnd-1 wallet..." - if ! wait_for_lnd1_wallet; then - error "lnd-1 wallet failed to initialize" - return 1 - fi - success "lnd-1 wallet is ready" - - # From a clean slate, bitcoind may have 0 blocks. Mine some for LND to sync. - local block_count=$(docker exec lnbits-bitcoind-1 bitcoin-cli -regtest getblockcount 2>/dev/null || echo "0") - if [[ "$block_count" -lt 101 ]]; then - log "Mining initial blocks for chain sync..." - local init_addr=$(docker exec lnbits-lnd-1-1 lncli --network=regtest --rpcserver=lnd-1:10009 newaddress p2wkh 2>/dev/null | grep -oP '"address":\s*"\K[^"]+') - if [[ -n "$init_addr" ]]; then - docker exec lnbits-bitcoind-1 bitcoin-cli -regtest generatetoaddress 110 "$init_addr" &>/dev/null - sleep 3 - success "Initial blocks mined" - fi - fi - - # Wait for LND to sync to chain - log "Waiting for lnd-1 to sync..." - local sync_attempts=0 - while [[ $sync_attempts -lt 30 ]]; do - local synced=$(docker exec lnbits-lnd-1-1 lncli --network=regtest --rpcserver=lnd-1:10009 getinfo 2>/dev/null | grep -oP '"synced_to_chain":\s*\K(true|false)') - if [[ "$synced" == "true" ]]; then - break - fi - sleep 2 - sync_attempts=$((sync_attempts + 1)) - done - success "lnd-1 synced to chain" - - # Check if lnd-1 has funds, if not fund it - local lnd1_balance=$(docker exec lnbits-lnd-1-1 lncli --network=regtest --rpcserver=lnd-1:10009 walletbalance 2>/dev/null | grep -oP '"confirmed_balance":\s*"\K[0-9]+' | head -1 || echo "0") - lnd1_balance="${lnd1_balance:-0}" - if [[ "$lnd1_balance" -lt 100000000 ]]; then - log "Funding lnd-1 with mining rewards..." - local lnd1_addr=$(docker exec lnbits-lnd-1-1 lncli --network=regtest --rpcserver=lnd-1:10009 newaddress p2wkh 2>/dev/null | grep -oP '"address":\s*"\K[^"]+') - docker exec lnbits-bitcoind-1 bitcoin-cli -regtest generatetoaddress 110 "$lnd1_addr" &>/dev/null - sleep 3 - success "lnd-1 funded" - fi - - # Get pubkeys - local lnd4_pubkey=$(docker exec lnbits-lnd-4-1 lncli --network=regtest --rpcserver=lnd-4:10009 getinfo 2>/dev/null | grep -oP '"identity_pubkey":\s*"\K[^"]+') - local lnd3_pubkey=$(docker exec lnbits-lnd-3-1 lncli --network=regtest --rpcserver=lnd-3:10009 getinfo 2>/dev/null | grep -oP '"identity_pubkey":\s*"\K[^"]+') - - if [[ -z "$lnd4_pubkey" ]] || [[ -z "$lnd3_pubkey" ]]; then - error "Could not get LND pubkeys" - return 1 - fi - - # Connect lnd-1 to lnd-3 and lnd-4 - log "Connecting nodes..." - docker exec lnbits-lnd-1-1 lncli --network=regtest --rpcserver=lnd-1:10009 \ - connect "${lnd3_pubkey}@lnd-3:9735" &>/dev/null || true - docker exec lnbits-lnd-1-1 lncli --network=regtest --rpcserver=lnd-1:10009 \ - connect "${lnd4_pubkey}@lnd-4:9735" &>/dev/null || true - - # Retry loop for opening channels (server may still be initializing) - # From a clean slate, LND needs ~30-60s after getinfo works to fully initialize - local result3="" result4="" attempts=0 - while [[ $attempts -lt 10 ]]; do - # Open channel to lnd-3: 10M sats, push 5M so lnd-3 has sending capacity - if [[ -z "$result3" ]] || echo "$result3" | grep -q "still in the process of starting"; then - result3=$(docker exec lnbits-lnd-1-1 lncli --network=regtest --rpcserver=lnd-1:10009 \ - openchannel --node_key="$lnd3_pubkey" --local_amt=10000000 --push_amt=5000000 2>&1) - fi - - # Open channel to lnd-4: 10M sats, push 5M so lnd-4 has receiving capacity - if [[ -z "$result4" ]] || echo "$result4" | grep -q "still in the process of starting"; then - result4=$(docker exec lnbits-lnd-1-1 lncli --network=regtest --rpcserver=lnd-1:10009 \ - openchannel --node_key="$lnd4_pubkey" --local_amt=10000000 --push_amt=5000000 2>&1) - fi - - # Check if both succeeded - if echo "$result3" | grep -q "funding_txid" && echo "$result4" | grep -q "funding_txid"; then - break - fi - - attempts=$((attempts + 1)) - if [[ $attempts -lt 10 ]]; then - log "Waiting for LND to be fully ready... (attempt $attempts/10)" - sleep 5 - fi - done - - log "Opening channel lnd-1 -> lnd-3..." - log "Opening channel lnd-1 -> lnd-4..." - - local channels_opened=false - if echo "$result3" | grep -q "funding_txid" || echo "$result4" | grep -q "funding_txid"; then - channels_opened=true - fi - - if [[ "$channels_opened" == "true" ]]; then - log "Mining blocks to confirm channels..." - - # From clean slate, bitcoind may need wallet. Ensure it has one. - docker exec lnbits-bitcoind-1 bitcoin-cli -regtest createwallet "regtest" &>/dev/null 2>&1 || true - - # Mine 10 blocks to confirm channels - local addr=$(docker exec lnbits-bitcoind-1 bitcoin-cli -regtest getnewaddress 2>/dev/null) - docker exec lnbits-bitcoind-1 bitcoin-cli -regtest generatetoaddress 10 "$addr" &>/dev/null - - # Wait for channels to become active (may take up to 60s) - local attempts=0 - while ! lnd4_has_active_channels && [[ $attempts -lt 30 ]]; do - sleep 2 - attempts=$((attempts + 1)) - done - - if lnd4_has_active_channels; then - # Wait for graph propagation (critical for routing payments) - log "Waiting for network graph to sync..." - # Mine more blocks to trigger channel announcements - docker exec lnbits-bitcoind-1 bitcoin-cli -regtest generatetoaddress 6 "$addr" &>/dev/null - sleep 5 - - # Verify graph has propagated by checking if lnd-3 can route to lnd-4 - local route_attempts=0 - while [[ $route_attempts -lt 15 ]]; do - local route=$(docker exec lnbits-lnd-3-1 lncli --network=regtest --rpcserver=lnd-3:10009 \ - queryroutes --dest "$lnd4_pubkey" --amt 10000 2>&1) - if echo "$route" | grep -q '"total_amt"'; then - # Route found - wait a bit more for full sync before payments - sleep 10 - break - fi - sleep 5 - route_attempts=$((route_attempts + 1)) - done - - success "Channels active - lnd-3 and lnd-4 ready" - return 0 - else - warn "Channels opened but not yet active, may need more time" - return 0 - fi - else - error "Failed to open channels" - echo "lnd-3 result: $result3" - echo "lnd-4 result: $result4" - return 1 - fi -} - -start_regtest() { - if is_regtest_running; then - log "Regtest network already running" - return 0 - fi - - if [[ ! -d "$REGTEST_DIR" ]]; then - error "Regtest directory not found: $REGTEST_DIR" - echo " Clone it from: https://github.com/your-org/regtest-env" - exit 1 - fi - - log "Starting regtest environment..." - # Use project name "lnbits" to match the expected network name (lnbits_default) - (cd "$REGTEST_DIR" && docker compose -p lnbits up -d) - - # Wait for lnd-4 to be ready - log "Waiting for lnd-4 to be ready..." - local attempts=0 - while ! is_lnd4_ready && [[ $attempts -lt 30 ]]; do - sleep 2 - attempts=$((attempts + 1)) - done - - if is_lnd4_ready; then - success "lnd-4 is ready" - else - error "lnd-4 failed to start" - return 1 - fi - - # Ensure lnd-4 has a channel for invoices - ensure_lnd4_channel || warn "Could not setup lnd-4 channel (funding may fail)" -} - -stop_regtest() { - if [[ -d "$REGTEST_DIR" ]]; then - log "Stopping regtest environment..." - (cd "$REGTEST_DIR" && docker compose down) - fi -} - -############################################################################# -# Lightning.Pub Management -############################################################################# - -build_from_worktree() { - local worktree="$1" - local image_name="lightning-pub-dev:latest" - - if [[ ! -d "$worktree" ]]; then - error "Worktree not found: $worktree" - exit 1 - fi - - log "Building Lightning.Pub from $worktree..." - docker build -t "$image_name" "$worktree" - echo "$image_name" -} - -wait_for_lightning_pub() { - log "Waiting for Lightning.Pub..." - local attempts=0 - while ! is_lightning_pub_ready && [[ $attempts -lt 45 ]]; do - # Check for errors - if docker logs bitspire-lightning-pub 2>&1 | grep -q "Error:"; then - local err=$(docker logs bitspire-lightning-pub 2>&1 | grep "Error:" | tail -1) - error "Lightning.Pub error: $err" - return 1 - fi - sleep 2 - attempts=$((attempts + 1)) - done - - if is_lightning_pub_ready; then - sleep 2 # Extra time for Nostr middleware - success "Lightning.Pub is ready" - return 0 - else - error "Lightning.Pub failed to start (timeout)" - docker logs bitspire-lightning-pub 2>&1 | tail -10 - return 1 - fi -} - -extract_lightning_pub_info() { - local pubkey=$(docker logs bitspire-lightning-pub 2>&1 | grep -oP 'pubkey:\s*\K[a-f0-9]+' | tail -1) - local nprofile=$(docker logs bitspire-lightning-pub 2>&1 | grep -oP 'nprofile:\s*\K\S+' | tail -1) - - echo "$pubkey" > "$PUBKEY_FILE" - echo "$nprofile" > "$NPROFILE_FILE" - - echo "$pubkey" -} - -############################################################################# -# ATM Configuration -############################################################################# - -update_atm_env() { - local pubkey="$1" - local env_file="$PROJECT_DIR/apps/machine/.env" - - if [[ ! -f "$env_file" ]]; then - warn "ATM .env not found, creating..." - cat > "$env_file" << EOF -# Lightning.Pub connection -VITE_RELAY_URL=ws://localhost:7777 -VITE_LIGHTNING_PUB_PUBKEY=$pubkey -VITE_LIGHTNING_PUB_API_URL=http://localhost:1776 -VITE_ADMIN_TOKEN=bitspire-dev-admin-token -VITE_ATM_PRIVATE_KEY=f391a2c3fc734f443b0f685688a0441b5fb9805853c0023f570c5a3c6412b136 - -# Extension API (for LNURL-withdraw) -VITE_EXTENSION_API_URL=http://localhost:1777 -EOF - else - # Update existing pubkey - if grep -q "VITE_LIGHTNING_PUB_PUBKEY" "$env_file"; then - sed -i "s/VITE_LIGHTNING_PUB_PUBKEY=.*/VITE_LIGHTNING_PUB_PUBKEY=$pubkey/" "$env_file" - else - echo "VITE_LIGHTNING_PUB_PUBKEY=$pubkey" >> "$env_file" - fi - fi - success "Updated ATM .env with pubkey" -} - -setup_atm_app() { - # Use a fixed app name so we reuse the same app across restarts - local app_name="bitspire-atm-dev" - - # Check if we already have valid app state - if [[ -f "$APP_ID_FILE" ]]; then - local existing_app_id=$(cat "$APP_ID_FILE") - if [[ -n "$existing_app_id" ]]; then - log "Using existing ATM app: ${existing_app_id:0:16}..." - return 0 - fi - fi - - log "Creating ATM app..." - - local response=$(curl -s -X POST http://localhost:1776/api/admin/app/add \ - -H "Content-Type: application/json" \ - -H "Authorization: Bearer bitspire-dev-admin-token" \ - -d "{\"name\":\"$app_name\",\"allow_user_creation\":true}" 2>/dev/null) - - if echo "$response" | grep -q '"status":"OK"'; then - local app_id=$(echo "$response" | grep -oP '"id":"\K[^"]+') - - echo "$app_id" > "$APP_ID_FILE" - - # Update ATM .env with app ID - local env_file="$PROJECT_DIR/apps/machine/.env" - if [[ -f "$env_file" ]]; then - if grep -q "VITE_APP_ID" "$env_file"; then - sed -i "s/VITE_APP_ID=.*/VITE_APP_ID=$app_id/" "$env_file" - else - echo "" >> "$env_file" - echo "# ATM App ID" >> "$env_file" - echo "VITE_APP_ID=$app_id" >> "$env_file" - fi - fi - - success "Created ATM app: ${app_id:0:16}..." - return 0 - else - warn "Failed to create ATM app (may already exist)" - if [[ -f "$APP_ID_FILE" ]]; then - log "Using existing app ID from state" - return 0 - fi - return 1 - fi -} - -############################################################################# -# Zeus Connection -############################################################################# - -generate_lndconnect() { - local local_ip=$(get_local_ip) - local rest_port=8081 # lnd-3 REST API port - - # Read certs directly from lnd-3 container (host files are root-owned) - local cert_b64=$(docker exec lnbits-lnd-3-1 base64 -w0 /root/.lnd/tls.cert 2>/dev/null | tr '+/' '-_' | tr -d '=') - local mac_b64=$(docker exec lnbits-lnd-3-1 base64 -w0 /root/.lnd/data/chain/bitcoin/regtest/admin.macaroon 2>/dev/null | tr '+/' '-_' | tr -d '=') - - if [[ -n "$cert_b64" ]] && [[ -n "$mac_b64" ]]; then - echo "lndconnect://${local_ip}:${rest_port}?cert=${cert_b64}&macaroon=${mac_b64}" - return 0 - fi - return 1 -} - -############################################################################# -# Display Functions -############################################################################# - -get_atm_balance() { - local app_id=$(cat "$APP_ID_FILE" 2>/dev/null) - if [[ -z "$app_id" ]]; then - echo "not configured" - return - fi - echo "configured (use './dev.sh fund' to add sats)" -} - -show_status() { - local pubkey=$(cat "$PUBKEY_FILE" 2>/dev/null || docker logs bitspire-lightning-pub 2>&1 | grep -oP 'pubkey:\s*\K[a-f0-9]+' | tail -1) - local nprofile=$(cat "$NPROFILE_FILE" 2>/dev/null || docker logs bitspire-lightning-pub 2>&1 | grep -oP 'nprofile:\s*\K\S+' | tail -1) - local local_ip=$(get_local_ip) - local lndconnect=$(generate_lndconnect 2>/dev/null || echo "") - local lnd4_balance=$(get_lnd4_balance) - local app_id=$(cat "$APP_ID_FILE" 2>/dev/null || echo "not set") - - echo "" - echo -e "${BOLD}╔═══════════════════════════════════════════════════════════════════╗${NC}" - echo -e "${BOLD}║ BITSPIRE - DEVELOPMENT ENVIRONMENT ║${NC}" - echo -e "${BOLD}╚═══════════════════════════════════════════════════════════════════╝${NC}" - echo "" - - echo -e "${CYAN}Lightning.Pub${NC}" - echo -e " Pubkey: ${GREEN}$pubkey${NC}" - echo -e " nprofile: ${GREEN}$nprofile${NC}" - echo "" - - echo -e "${CYAN}Service URLs (local)${NC}" - echo " Nostr Relay: ws://localhost:7777" - echo " Lightning.Pub: http://localhost:1776" - echo " Withdraw API: http://localhost:1777" - echo "" - - echo -e "${CYAN}External Access (LAN: $local_ip)${NC}" - echo " Nostr Relay: ws://${local_ip}:7777" - echo " Withdraw API: http://${local_ip}:1777" - echo "" - - local lnd4_channel_balance=$(get_lnd4_channel_balance) - echo -e "${CYAN}lnd-4 (Lightning.Pub backend)${NC}" - echo " Channel Balance: ${lnd4_channel_balance} sats" - echo " Wallet Balance: ${lnd4_balance} sats" - echo "" - - echo -e "${CYAN}ATM App${NC}" - if [[ "$app_id" != "not set" ]]; then - echo " App ID: ${app_id:0:16}..." - echo " Status: $(get_atm_balance)" - else - echo " Status: not configured" - fi - echo "" - - if [[ -n "$lndconnect" ]]; then - echo -e "${CYAN}Zeus Wallet (connect to lnd-3 for testing)${NC}" - echo -e " ${DIM}${lndconnect:0:60}...${NC}" - echo -e " ${DIM}To display QR: ./dev.sh zeus${NC}" - echo "" - fi - - echo -e "${CYAN}Quick Commands${NC}" - echo " ./dev.sh logs lightning-pub # View Lightning.Pub logs" - echo " ./dev.sh fund 100000 # Fund ATM with 100k sats" - echo " ./dev.sh status # Show this info" - echo "" - echo -e "${BOLD}═══════════════════════════════════════════════════════════════════${NC}" -} - -############################################################################# -# Main Commands -############################################################################# - -cmd_up() { - local image="$DEFAULT_IMAGE" - local worktree="" - local skip_regtest=false - local auto_fund=false - local fund_amount="$DEFAULT_FUNDING_SATS" - local launch_machine=false - local standalone=false - local compose_file="docker-compose.regtest.yml" - - # Parse arguments - while [[ $# -gt 0 ]]; do - case "$1" in - --image) image="$2"; shift 2 ;; - --worktree) worktree="$2"; shift 2 ;; - --skip-regtest) skip_regtest=true; shift ;; - --standalone) standalone=true; shift ;; - --fund) auto_fund=true; shift ;; - --fund=*) auto_fund=true; fund_amount="${1#*=}"; shift ;; - --machine|--atm) launch_machine=true; shift ;; - *) shift ;; - esac - done - - # Determine compose file - if [[ "$standalone" == "true" ]]; then - compose_file="docker-compose.dev.yml" - skip_regtest=true # Standalone mode has its own bitcoind/lnd - log "Starting in STANDALONE mode (self-contained bitcoind/lnd)" - else - log "Starting in UNIFIED mode (shared regtest)" - fi - - echo "" - log "Starting bitSpire development environment..." - echo "" - - # 1. Start shared regtest if needed (not in standalone mode) - if [[ "$standalone" != "true" ]] && [[ "$skip_regtest" != "true" ]]; then - start_regtest || exit 1 - elif [[ "$standalone" != "true" ]] && ! is_regtest_running; then - error "Shared regtest not running." - echo " Start with: regtest-start" - echo " Or use: ./dev.sh up --standalone" - exit 1 - elif [[ "$standalone" != "true" ]]; then - # Regtest already running, but ensure lnd-4 has channels - ensure_lnd4_channel || warn "Could not setup lnd-4 channel (funding may fail)" - fi - - # 2. Build from worktree if specified - if [[ -n "$worktree" ]]; then - image=$(build_from_worktree "$worktree") - fi - - # 3. Check image exists - if ! docker image inspect "$image" &>/dev/null; then - error "Docker image not found: $image" - echo "" - echo "Options:" - echo " 1. Build from worktree: ./dev.sh up --worktree ~/path/to/lightning-pub" - echo " 2. Build manually: docker build -t $image ~/path/to/lightning-pub" - exit 1 - fi - - log "Using Lightning.Pub image: $image" - - # 4. Start bitspire services - export REGTEST_DATA_DIR="$REGTEST_DIR/data" - export LIGHTNING_PUB_IMAGE="$image" - export HOST_IP=$(get_local_ip) - - # Save mode for other commands - if [[ "$standalone" == "true" ]]; then - echo "standalone" > "$MODE_FILE" - else - echo "unified" > "$MODE_FILE" - fi - - log "Starting bitspire services..." - docker compose -f "$SCRIPT_DIR/$compose_file" up -d - - # 5. Wait for Lightning.Pub - if ! wait_for_lightning_pub; then - error "Failed to start. Check: ./dev.sh logs lightning-pub" - exit 1 - fi - - # 6. Extract and save Lightning.Pub info - local pubkey=$(extract_lightning_pub_info) - - # 7. Update ATM .env - update_atm_env "$pubkey" - - # 8. Setup ATM app - setup_atm_app || true - - # 9. Auto-fund if requested - if [[ "$auto_fund" == "true" ]]; then - echo "" - log "Auto-funding ATM with $fund_amount sats..." - sleep 2 # Give Lightning.Pub a moment to settle - cmd_fund "$fund_amount" || warn "Auto-funding failed. Run './dev.sh fund' manually." - fi - - # 10. Show status - show_status - - # 11. Launch ATM if requested (in background so status stays visible) - if [[ "$launch_machine" == "true" ]]; then - echo "" - cmd_atm --background - fi -} - -cmd_down() { - local compose_file=$(get_compose_file) - log "Stopping bitspire services..." - docker compose -f "$SCRIPT_DIR/$compose_file" down 2>/dev/null || true - - if [[ "$1" == "--all" ]]; then - stop_regtest - fi - - success "Services stopped" -} - -cmd_reset() { - warn "This will delete all bitspire state (Lightning.Pub identity, ATM app, etc.)" - read -p "Continue? [y/N] " -n 1 -r - echo - if [[ ! $REPLY =~ ^[Yy]$ ]]; then - echo "Aborted" - exit 0 - fi - - local compose_file=$(get_compose_file) - log "Stopping services..." - docker compose -f "$SCRIPT_DIR/$compose_file" down -v 2>/dev/null || true - - log "Removing state files..." - rm -rf "$STATE_DIR" - mkdir -p "$STATE_DIR" - - success "Reset complete. Run './dev.sh up' for fresh start." -} - -cmd_logs() { - local compose_file=$(get_compose_file) - docker compose -f "$SCRIPT_DIR/$compose_file" logs -f "$@" -} - -cmd_status() { - if ! docker ps --format '{{.Names}}' | grep -q bitspire-lightning-pub; then - error "Services not running. Start with: ./dev.sh up" - exit 1 - fi - show_status -} - -cmd_fund() { - local amount="${1:-$DEFAULT_FUNDING_SATS}" - - if ! is_regtest_running; then - error "Regtest not running" - exit 1 - fi - - # Check for Lightning.Pub pubkey - if [[ ! -f "$PUBKEY_FILE" ]]; then - error "Lightning.Pub not configured. Run './dev.sh up' first." - exit 1 - fi - - local lp_pubkey=$(cat "$PUBKEY_FILE") - local env_file="$PROJECT_DIR/apps/machine/.env" - - # Get ATM private key from .env # pragma: allowlist secret - if [[ ! -f "$env_file" ]]; then - error "ATM not configured. Run './dev.sh up' first." - exit 1 - fi - - local atm_privkey=$(grep "VITE_ATM_PRIVATE_KEY=" "$env_file" | cut -d= -f2) - if [[ -z "$atm_privkey" ]]; then - error "VITE_ATM_PRIVATE_KEY not found in .env" - exit 1 - fi - - # Get app ID - required to ensure funds go to the same user as LNURL-withdraw - local app_id="" - if [[ -f "$APP_ID_FILE" ]]; then - app_id=$(cat "$APP_ID_FILE") - else - app_id=$(grep "VITE_APP_ID=" "$env_file" | cut -d= -f2) - fi - - if [[ -z "$app_id" ]]; then - error "No app ID found. Run './dev.sh up' first to create the ATM app." - exit 1 - fi - - log "Creating invoice via Nostr RPC for $amount sats..." - log "Using app ID: ${app_id:0:16}..." - - # Use the Nostr-based funding script (the Lightning.Pub way) - # This creates an invoice for the ATM's Nostr user under the correct app, - # ensuring balance is shared with LNURL-withdraw (Extension API) - local invoice=$(cd "$PROJECT_DIR/packages/nostr-client" && \ - VITE_ATM_PRIVATE_KEY="$atm_privkey" \ - VITE_LIGHTNING_PUB_PUBKEY="$lp_pubkey" \ - VITE_RELAY_URL="ws://localhost:7777" \ - VITE_APP_ID="$app_id" \ - node dev/fund-dev.mjs "$amount" 2>&1) - - # Extract just the invoice (last line, starts with lnbc) - local bolt11=$(echo "$invoice" | grep -E "^lnbc") - - if [[ -z "$bolt11" ]]; then - error "Failed to create invoice via Nostr" - echo "Output: $invoice" - exit 1 - fi - - log "Got invoice: ${bolt11:0:30}..." - - log "Paying invoice from lnd-3..." - - # Source regtest helpers and pay - if [[ -f "$REGTEST_DIR/docker-scripts.sh" ]]; then - ( - cd "$REGTEST_DIR" - source docker-scripts.sh 2>/dev/null - lncli-sim 3 payinvoice --force "$bolt11" - ) - if [[ $? -eq 0 ]]; then - success "Funded ATM with $amount sats" - else - error "Payment failed" - exit 1 - fi - else - # Fallback: try direct docker exec - docker exec lnbits-lnd-3-1 lncli --network=regtest --rpcserver=lnd-3:10009 payinvoice --force "$bolt11" - if [[ $? -eq 0 ]]; then - success "Funded ATM with $amount sats" - else - error "Payment failed. Make sure lnd-3 has funds and channels." - exit 1 - fi - fi -} - -cmd_mine() { - local blocks="${1:-1}" - if ! is_regtest_running; then - error "Regtest not running" - exit 1 - fi - log "Mining $blocks block(s)..." - docker exec lnbits-bitcoind-1 bitcoin-cli -regtest -generate "$blocks" > /dev/null - local height=$(docker exec lnbits-bitcoind-1 bitcoin-cli -regtest getblockcount) - success "Mined $blocks block(s) (height: $height)" -} - -cmd_zeus() { - if ! is_regtest_running; then - error "Regtest not running. Start with: ./dev.sh up" - exit 1 - fi - - local lndconnect=$(generate_lndconnect 2>/dev/null) - if [[ -z "$lndconnect" ]]; then - error "Could not generate lndconnect string" - exit 1 - fi - - echo "" - echo -e "${CYAN}Zeus Wallet Connection (lnd-3)${NC}" - echo "" - - if command -v qrencode &> /dev/null; then - local qr_file="$STATE_DIR/zeus-lndconnect.png" - qrencode -o "$qr_file" -s 6 -l L "$lndconnect" - echo -e "QR code saved to: ${GREEN}$qr_file${NC}" - echo "" - - # Try to open the image - if command -v xdg-open &> /dev/null; then - xdg-open "$qr_file" 2>/dev/null & - echo -e "${DIM}Opening QR code image...${NC}" - elif command -v open &> /dev/null; then - open "$qr_file" 2>/dev/null & - echo -e "${DIM}Opening QR code image...${NC}" - else - echo -e "${DIM}Open the PNG file to scan with Zeus${NC}" - fi - else - echo -e "${DIM}$lndconnect${NC}" - echo "" - echo -e "${YELLOW}Install qrencode for QR display: nix-shell -p qrencode${NC}" - fi - echo "" -} - -cmd_atm() { - local atm_dir="$PROJECT_DIR/apps/machine" - local background=false - - # Parse arguments - while [[ $# -gt 0 ]]; do - case "$1" in - --background|-b) background=true; shift ;; - *) shift ;; - esac - done - - if [[ ! -d "$atm_dir" ]]; then - error "ATM app not found at $atm_dir" - exit 1 - fi - - # Check if services are running - if ! docker ps --format '{{.Names}}' | grep -q bitspire-lightning-pub; then - warn "Services not running. Start with: ./dev.sh up" - read -p "Start services first? [Y/n] " -n 1 -r - echo - if [[ ! $REPLY =~ ^[Nn]$ ]]; then - cmd_up - fi - fi - - # Auto-generate lndconnect URI for Zeus QR in mock UI - local lndconnect=$(generate_lndconnect 2>/dev/null || echo "") - if [[ -n "$lndconnect" ]]; then - export VITE_LNDCONNECT_URL="$lndconnect" - log "Zeus lndconnect injected into ATM UI" - fi - - if [[ "$background" == "true" ]]; then - local log_file="$STATE_DIR/atm.log" - log "Starting ATM application in background..." - log "Logs: $log_file" - (cd "$atm_dir" && nohup pnpm dev > "$log_file" 2>&1 &) - sleep 2 - if pgrep -f "electron.*machine" > /dev/null; then - success "ATM app started (PID: $(pgrep -f 'electron.*machine' | head -1))" - else - warn "ATM may still be starting. Check logs: tail -f $log_file" - fi - else - log "Starting ATM application..." - echo "" - echo -e "${CYAN}ATM Mock Mode:${NC}" - echo " - Press 'b' to insert a bill (simulates cash insertion)" - echo " - Use the UI to complete transactions" - if [[ -n "$lndconnect" ]]; then - echo -e " - ${GREEN}Zeus QR available on idle screen${NC}" - fi - echo "" - cd "$atm_dir" && pnpm dev - fi -} - -############################################################################# -# Entry Point -############################################################################# - -case "${1:-help}" in - up|start) - shift - cmd_up "$@" - ;; - down|stop) - shift - cmd_down "$@" - ;; - reset) - cmd_reset - ;; - logs) - shift - cmd_logs "$@" - ;; - # Internal command for external orchestration (called by regtest-start --with-bitspire) - _setup_after_start) - if ! is_lightning_pub_ready; then - wait_for_lightning_pub || exit 1 - fi - local pubkey=$(extract_lightning_pub_info) - update_atm_env "$pubkey" - setup_atm_app || true - echo "ATM setup complete" - ;; - status|info) - cmd_status - ;; - fund) - shift - cmd_fund "$@" - ;; - mine) - shift - cmd_mine "$@" - ;; - atm) - cmd_atm - ;; - zeus) - cmd_zeus - ;; - *) - echo "bitSpire Development Environment" - echo "" - echo "Usage: $0 [options]" - echo "" - echo "Commands:" - echo " up [options] Start development environment" - echo " down [--all] Stop services (--all includes regtest)" - echo " atm Launch ATM application (Electron)" - echo " zeus Show Zeus wallet connection QR code" - echo " status Show connection info" - echo " logs [service] Follow service logs" - echo " fund [sats] Fund ATM account" - echo " mine [blocks] Mine blocks manually (default: 1)" - echo " reset Delete all state and start fresh" - echo "" - echo "Note: Auto-miner runs in background (1 block/2min). Check with:" - echo " ./dev.sh logs miner" - echo "" - echo "Options for 'up':" - echo " --standalone Use self-contained mode (own bitcoind/lnd)" - echo " --worktree Build Lightning.Pub from git worktree" - echo " --image Use specific Docker image" - echo " --skip-regtest Don't auto-start shared regtest" - echo " --fund Auto-fund ATM with 100k sats after startup" - echo " --fund= Auto-fund ATM with specific amount" - echo " --machine Launch ATM app after startup" - echo "" - echo "Environment variables:" - echo " BITSPIRE_HOST_IP Override auto-detected LAN IP (for VPN/multi-NIC)" - echo "" - echo "Examples:" - echo " $0 up # Start (uses shared regtest)" - echo " $0 up --standalone # Start self-contained" - echo " $0 up --fund # Start and fund ATM" - echo " $0 up --fund --machine # Start, fund, and launch ATM" - echo " $0 up --fund=50000 # Start and fund with 50k sats" - echo " $0 up --worktree ~/dev/lightning-pub/withdraw" - echo " $0 atm # Launch ATM app" - echo " $0 fund 200000 # Add 200k more sats" - echo " $0 logs lightning-pub" - echo " $0 reset && $0 up --fund # Fresh start with funding" - echo " BITSPIRE_HOST_IP=192.168.1.50 $0 up # Override LAN IP" - ;; -esac diff --git a/docker/docker-compose.dev.yml b/docker/docker-compose.dev.yml deleted file mode 100644 index fa9860e..0000000 --- a/docker/docker-compose.dev.yml +++ /dev/null @@ -1,226 +0,0 @@ -# bitSpire Development Infrastructure -# Usage: docker compose -f docker-compose.dev.yml up -d - -services: - # Private Nostr relay for ATM communication - strfry: - image: ghcr.io/hoytech/strfry:latest - container_name: bitspire-relay - ports: - - '7777:7777' - volumes: - - ./strfry.conf:/etc/strfry.conf:ro - - strfry-data:/app/strfry-db - ulimits: - nofile: - soft: 524288 - hard: 524288 - healthcheck: - test: ['CMD', 'nc', '-z', 'localhost', '7777'] - interval: 10s - timeout: 5s - retries: 5 - restart: unless-stopped - - # Bitcoin Core in regtest mode - bitcoind: - image: lncm/bitcoind:v27.0 - container_name: bitspire-bitcoind - volumes: - - bitcoind-data:/data/.bitcoin - environment: - BITCOIN_NETWORK: regtest - command: - - -regtest - - -server - - -rpcuser=bitspire - - -rpcpassword=bitspire # pragma: allowlist secret - - -rpcallowip=0.0.0.0/0 - - -rpcbind=0.0.0.0 - - -zmqpubrawblock=tcp://0.0.0.0:28332 - - -zmqpubrawtx=tcp://0.0.0.0:28333 - - -fallbackfee=0.00001 - - -txindex=1 - ports: - - '18443:18443' # RPC - - '28332:28332' # ZMQ blocks - - '28333:28333' # ZMQ tx - healthcheck: - test: - [ - 'CMD', - 'bitcoin-cli', - '-regtest', - '-rpcuser=bitspire', - '-rpcpassword=bitspire', # pragma: allowlist secret - 'getblockchaininfo', - ] - interval: 10s - timeout: 5s - retries: 10 - restart: unless-stopped - - # LND Lightning node - lnd: - image: lightninglabs/lnd:v0.18.0-beta - container_name: bitspire-lnd - depends_on: - bitcoind: - condition: service_healthy - volumes: - - lnd-data:/root/.lnd - environment: - - NETWORK=regtest - command: - - --bitcoin.active - - --bitcoin.regtest - - --bitcoin.node=bitcoind - - --bitcoind.rpchost=bitcoind:18443 - - --bitcoind.rpcuser=bitspire - - --bitcoind.rpcpass=bitspire # pragma: allowlist secret - - --bitcoind.zmqpubrawblock=tcp://bitcoind:28332 - - --bitcoind.zmqpubrawtx=tcp://bitcoind:28333 - - --rpclisten=0.0.0.0:10009 - - --restlisten=0.0.0.0:8080 - - --tlsextradomain=lnd - - --tlsextraip=0.0.0.0 - - --noseedbackup - - --accept-keysend - - --accept-amp - ports: - - '10009:10009' # gRPC - - '8080:8080' # REST - - '9735:9735' # P2P - healthcheck: - test: ['CMD', 'lncli', '--network=regtest', 'getinfo'] - interval: 10s - timeout: 10s - retries: 30 - start_period: 30s - restart: unless-stopped - - # Lightning.Pub - Nostr-native Lightning account system - # Note: Lightning.Pub connects to LND for actual Lightning operations - # Using patched image with Kind 0 profile publishing support - lightning-pub: - image: lightning-pub-patched:latest - container_name: bitspire-lightning-pub - depends_on: - lnd: - condition: service_healthy - extra_hosts: - - 'host.docker.internal:host-gateway' # Enable host.docker.internal on Linux - ports: - - '1776:1776' - volumes: - - lightning-pub-data:/root/lightning_pub - - lnd-data:/root/.lnd:ro # Read-only access to LND data for macaroons/certs - environment: - - NETWORK=regtest - - LND_ADDRESS=lnd:10009 - - LND_CERT_PATH=/root/.lnd/tls.cert - - LND_MACAROON_PATH=/root/.lnd/data/chain/bitcoin/regtest/admin.macaroon - # Relay URLs (space-separated). Docker-internal relay FIRST for publishing, - # then backup. Mock ATM rewrites ndebit relay for browser access. - - NOSTR_RELAYS=ws://strfry:7777 - # Disable external liquidity provider for regtest - - DISABLE_LIQUIDITY_PROVIDER=true - # Admin token for HTTP API access (development only) - - ADMIN_TOKEN=bitspire-dev-admin-token - restart: unless-stopped - - # Second LND node (Alice) for payment testing - # This node can pay invoices to Lightning.Pub's LND - lnd-alice: - image: lightninglabs/lnd:v0.18.0-beta - container_name: bitspire-lnd-alice - depends_on: - bitcoind: - condition: service_healthy - volumes: - - lnd-alice-data:/root/.lnd - environment: - - NETWORK=regtest - command: - - --bitcoin.active - - --bitcoin.regtest - - --bitcoin.node=bitcoind - - --bitcoind.rpchost=bitcoind:18443 - - --bitcoind.rpcuser=bitspire - - --bitcoind.rpcpass=bitspire # pragma: allowlist secret - - --bitcoind.zmqpubrawblock=tcp://bitcoind:28332 - - --bitcoind.zmqpubrawtx=tcp://bitcoind:28333 - - --rpclisten=0.0.0.0:10009 - - --restlisten=0.0.0.0:8080 - - --tlsextradomain=lnd-alice - - --tlsextraip=0.0.0.0 - - --noseedbackup - - --accept-keysend - - --accept-amp - - --alias=alice - ports: - - '10010:10009' # gRPC (different host port) - - '8081:8080' # REST (different host port) - - '9736:9735' # P2P (different host port) - healthcheck: - test: ['CMD', 'lncli', '--network=regtest', 'getinfo'] - interval: 10s - timeout: 10s - retries: 30 - start_period: 30s - restart: unless-stopped - - # Automatic block miner for regtest (keeps LND synced) - # Mines 1 block every 30 seconds - miner: - image: alpine:latest - container_name: bitspire-miner - depends_on: - bitcoind: - condition: service_healthy - entrypoint: /bin/sh - command: - - -c - - | - apk add --no-cache curl jq - echo "Starting auto-miner (1 block every 30 seconds)..." - while true; do - # Create wallet if not exists - curl -s --user bitspire:bitspire --data-binary '{"jsonrpc":"1.0","method":"createwallet","params":["miner"]}' http://bitcoind:18443/ > /dev/null 2>&1 - # Mine a block - ADDR=$$(curl -s --user bitspire:bitspire --data-binary '{"jsonrpc":"1.0","method":"getnewaddress","params":[]}' http://bitcoind:18443/ | jq -r '.result // empty') - if [ -n "$$ADDR" ]; then - curl -s --user bitspire:bitspire --data-binary "{\"jsonrpc\":\"1.0\",\"method\":\"generatetoaddress\",\"params\":[1,\"$$ADDR\"]}" http://bitcoind:18443/ > /dev/null - fi - sleep 30 - done - restart: unless-stopped - profiles: - - mining # Only starts with: docker compose --profile mining up -d - - # PostgreSQL for optional server-side state - postgres: - image: postgres:16-alpine - container_name: bitspire-postgres - ports: - - '5432:5432' - environment: - POSTGRES_DB: bitspire_dev - POSTGRES_USER: bitspire - POSTGRES_PASSWORD: bitspire_dev_password # pragma: allowlist secret - volumes: - - postgres-data:/var/lib/postgresql/data - healthcheck: - test: ['CMD-SHELL', 'pg_isready -U bitspire -d bitspire_dev'] - interval: 10s - timeout: 5s - retries: 5 - restart: unless-stopped - -volumes: - strfry-data: - bitcoind-data: - lnd-data: - lnd-alice-data: - lightning-pub-data: - postgres-data: diff --git a/docker/docker-compose.regtest.yml b/docker/docker-compose.regtest.yml deleted file mode 100644 index 43ca8a0..0000000 --- a/docker/docker-compose.regtest.yml +++ /dev/null @@ -1,166 +0,0 @@ -# bitSpire - Regtest Integration -# -# This overlay connects bitspire-next services to the comprehensive regtest -# environment at ~/dev/local/docker/regtest -# -# Usage: -# 1. Start regtest (minimal — only what LP needs): -# cd ~/dev/local/docker/regtest && docker compose up -d bitcoind lnd-1 lnd-4 -# -# 2. Start bitspire services: -# cd docker && docker compose -f docker-compose.regtest.yml up -d -# -# 3. Bootstrap (funds LND, opens channels, creates LP app): -# cd docker && ./regtest-bootstrap.sh -# -# 4. Configure apps/machine/.env: -# VITE_RELAY_URL=ws://localhost:7777 -# VITE_EXTENSION_API_URL=http://localhost:1777 -# -# Phone wallet testing (LNURL callbacks need LAN IP): -# HOST_IP=192.168.1.100 docker compose -f docker-compose.regtest.yml up -d -# -# Services: -# - strfry: Private Nostr relay (port 7777) -# - lightning-pub: Nostr-native Lightning account system (port 1776) -# - Uses lnd-4 from regtest as backend -# - Withdraw extension on port 1777 -# - miner: Auto-mines blocks to keep Lightning channels active -# - -services: - # Private Nostr relay for ATM communication - strfry: - image: ghcr.io/hoytech/strfry:latest - container_name: bitspire-relay - ports: - - '7777:7777' - volumes: - - ./strfry.conf:/etc/strfry.conf:ro - - strfry-data:/app/strfry-db - ulimits: - nofile: - soft: 524288 - hard: 524288 - healthcheck: - # Check port 7777 (0x1E61) is listening via procfs — BusyBox nc lacks -z flag - test: ['CMD-SHELL', 'grep -q ":1E61 " /proc/net/tcp'] - interval: 5s - timeout: 3s - retries: 10 - restart: unless-stopped - networks: - - regtest - - # Lightning.Pub - Nostr-native Lightning account system - # Connects to lnd-4 from the regtest environment - # Use LIGHTNING_PUB_IMAGE env var to specify image (default: lightning-pub-withdraw) - lightning-pub: - image: ${LIGHTNING_PUB_IMAGE:-lightning-pub-withdraw:latest} - container_name: bitspire-lightning-pub - extra_hosts: - - 'host.docker.internal:host-gateway' - ports: - - '1776:1776' - - '1777:1777' # Withdraw extension HTTP API - volumes: - - lightning-pub-data:/root/lightning_pub - # Override Dockerfile's anonymous /app/data volume with named volume - - lightning-pub-appdata:/app/data - # Mount lnd-4 data from regtest for macaroons/certs - - ${REGTEST_DATA_DIR:-/home/padreug/dev/local/docker/regtest/data}/lnd-4:/root/.lnd:ro - environment: - - NETWORK=regtest - # lnd-4 is accessible via Docker network - - LND_ADDRESS=lnd-4:10009 - - LND_CERT_PATH=/root/.lnd/tls.cert - - LND_MACAROON_PATH=/root/.lnd/data/chain/bitcoin/regtest/admin.macaroon - # Use strfry from this compose - - NOSTR_RELAYS=ws://strfry:7777 - # Disable external liquidity provider for regtest - - DISABLE_LIQUIDITY_PROVIDER=true - # Admin token for HTTP API access (development only) - - ADMIN_TOKEN=bitspire-dev-admin-token - # Extension HTTP API URL (for LNURL callbacks from external wallets) - # Use HOST_IP env var for your machine's LAN IP (required for phone wallets) - - EXTENSION_SERVICE_URL=http://${HOST_IP:-localhost}:1777 - depends_on: - strfry: - condition: service_healthy - healthcheck: - test: ['CMD-SHELL', 'curl -so /dev/null -w "%{http_code}" http://localhost:1776 | grep -q .'] - interval: 10s - timeout: 5s - retries: 30 - start_period: 30s - restart: unless-stopped - networks: - - regtest - - # Auto-miner for regtest (mines 1 block every MINE_INTERVAL seconds) - # Keeps Lightning channels active during development - miner: - image: boltz/bitcoin-core:25.0 - container_name: bitspire-miner - entrypoint: /bin/sh - command: - - -c - - | - echo "Auto-miner started (interval: $${MINE_INTERVAL}s)" - # Wait for bitcoind to be ready - while ! bitcoin-cli -regtest -rpcconnect=bitcoind getblockchaininfo > /dev/null 2>&1; do - echo "Waiting for bitcoind..." - sleep 5 - done - # Ensure a wallet is loaded (-generate requires one) - bitcoin-cli -regtest -rpcconnect=bitcoind createwallet default 2>/dev/null \ - || bitcoin-cli -regtest -rpcconnect=bitcoind loadwallet default 2>/dev/null \ - || true - echo "bitcoind ready, starting mining loop" - while true; do - bitcoin-cli -regtest -rpcconnect=bitcoind -generate 1 > /dev/null 2>&1 && echo "Mined block $(bitcoin-cli -regtest -rpcconnect=bitcoind getblockcount)" - sleep $${MINE_INTERVAL} - done - environment: - - MINE_INTERVAL=${MINE_INTERVAL:-30} - volumes: - - bitcoin-data:/root/.bitcoin - restart: unless-stopped - networks: - - regtest - - # PostgreSQL for optional server-side state - postgres: - image: postgres:16-alpine - container_name: bitspire-postgres - ports: - - '5432:5432' - environment: - POSTGRES_DB: bitspire_dev - POSTGRES_USER: bitspire - POSTGRES_PASSWORD: bitspire_dev_password # pragma: allowlist secret - volumes: - - postgres-data:/var/lib/postgresql/data - healthcheck: - test: ['CMD-SHELL', 'pg_isready -U bitspire -d bitspire_dev'] - interval: 10s - timeout: 5s - retries: 5 - restart: unless-stopped - networks: - - regtest - -volumes: - strfry-data: - lightning-pub-data: - lightning-pub-appdata: - postgres-data: - # Mount the bitcoin-data volume from the regtest environment - bitcoin-data: - external: true - name: regtest_bitcoin-data - -networks: - regtest: - name: regtest_default - external: true diff --git a/docker/regtest-bootstrap.sh b/docker/regtest-bootstrap.sh deleted file mode 100755 index ec1af77..0000000 --- a/docker/regtest-bootstrap.sh +++ /dev/null @@ -1,273 +0,0 @@ -#!/usr/bin/env bash -# regtest-bootstrap.sh — Bootstrap a working LP+LND regtest environment -# -# Prerequisites: -# 1. Regtest running (minimal — only needs bitcoind + lnd-1 + lnd-4): -# cd ~/dev/local/docker/regtest && docker compose up -d bitcoind lnd-1 lnd-4 -# 2. bitSpire services running: -# cd docker && docker compose -f docker-compose.regtest.yml up -d -# -# What this script does: -# 1. Waits for bitcoind and LND nodes to be synced -# 2. Funds LND-1 (1 BTC from bitcoind) -# 3. Opens a 5M sat channel from LND-1 → LND-4 (LP's backend) -# 4. Mines blocks to confirm and announce the channel -# 5. Waits for LP to be healthy -# 6. Creates an LP app and funds it (10k sats via invoice from LND-1) -# 7. Prints summary with pubkeys, app token, and channel status - -set -euo pipefail - -# -- Colors -- -RED='\033[0;31m' -GREEN='\033[0;32m' -YELLOW='\033[1;33m' -CYAN='\033[0;36m' -NC='\033[0m' # No Color - -info() { echo -e "${CYAN}[INFO]${NC} $*"; } -ok() { echo -e "${GREEN}[OK]${NC} $*"; } -warn() { echo -e "${YELLOW}[WARN]${NC} $*"; } -err() { echo -e "${RED}[ERROR]${NC} $*"; } - -# -- Auto-detect container name prefix -- -# The regtest compose uses project name "lnbits" (via start-regtest) or "regtest" (direct). -# Detect which prefix the RUNNING bitcoind container has (filter=running avoids stale containers). -if docker ps -f name=lnbits-bitcoind-1 -f status=running --format '{{.Names}}' | grep -q .; then - PREFIX="lnbits" -elif docker ps -f name=regtest-bitcoind-1 -f status=running --format '{{.Names}}' | grep -q .; then - PREFIX="regtest" -else - err "No running bitcoind container found (tried lnbits-bitcoind-1 and regtest-bitcoind-1)" - err "Start regtest first: cd ~/dev/local/docker/regtest && docker compose up -d bitcoind lnd-1 lnd-4" - exit 1 -fi -info "Using container prefix: ${PREFIX}" - -# -- Container helpers -- -bitcoin_cli() { - docker exec "${PREFIX}-bitcoind-1" bitcoin-cli -regtest "$@" -} - -lncli_1() { - docker exec "${PREFIX}-lnd-1-1" lncli --network regtest --rpcserver=lnd-1:10009 "$@" -} - -lncli_4() { - docker exec "${PREFIX}-lnd-4-1" lncli --network regtest --rpcserver=lnd-4:10009 "$@" -} - -LP_URL="http://localhost:1776" -LP_EXT_URL="http://localhost:1777" -ADMIN_TOKEN="bitspire-dev-admin-token" -CHANNEL_SIZE=5000000 # 5M sats -FUND_AMOUNT=10000 # 10k sats for LP app - -# -- Step 1: Wait for bitcoind -- -info "Waiting for bitcoind..." -for i in $(seq 1 60); do - if bitcoin_cli getblockchaininfo > /dev/null 2>&1; then - ok "bitcoind ready (block $(bitcoin_cli getblockcount))" - break - fi - if [ "$i" -eq 60 ]; then - err "bitcoind not ready after 60s — is regtest running?" - exit 1 - fi - sleep 1 -done - -# Ensure a wallet exists -bitcoin_cli createwallet default 2>/dev/null \ - || bitcoin_cli loadwallet default 2>/dev/null \ - || true - -# Mine initial blocks if needed (coinbase needs 100 confirmations to be spendable) -BLOCKS=$(bitcoin_cli getblockcount) -if [ "$BLOCKS" -lt 101 ]; then - NEEDED=$((101 - BLOCKS)) - info "Mining ${NEEDED} initial blocks (coinbase needs 100 confirmations)..." - bitcoin_cli -generate "$NEEDED" > /dev/null - ok "Mined to block $(bitcoin_cli getblockcount)" -fi - -# -- Step 2: Wait for LND-1 and LND-4 to sync -- -wait_lnd_sync() { - local name=$1 - local cli_fn=$2 - info "Waiting for ${name} to sync..." - for i in $(seq 1 120); do - if [ "$($cli_fn getinfo 2>/dev/null | jq -r '.synced_to_chain' 2>/dev/null)" = "true" ]; then - ok "${name} synced" - return 0 - fi - if [ "$i" -eq 120 ]; then - err "${name} not synced after 120s" - exit 1 - fi - sleep 1 - done -} - -wait_lnd_sync "LND-1" lncli_1 -wait_lnd_sync "LND-4" lncli_4 - -# -- Step 3: Fund LND-1 -- -info "Funding LND-1..." -LND1_ADDR=$(lncli_1 newaddress p2wkh | jq -r '.address') -bitcoin_cli -named sendtoaddress address="$LND1_ADDR" amount=1 fee_rate=100 > /dev/null -info "Mining 6 blocks to confirm funding..." -bitcoin_cli -generate 6 > /dev/null - -# Wait for LND-1 to see the confirmed balance -for i in $(seq 1 30); do - BALANCE=$(lncli_1 walletbalance | jq -r '.confirmed_balance') - if [ "$BALANCE" != "0" ] && [ -n "$BALANCE" ]; then - ok "LND-1 funded: ${BALANCE} sats" - break - fi - sleep 1 -done - -# -- Step 4: Connect LND-1 → LND-4 and open channel -- -LND4_PUBKEY=$(lncli_4 getinfo | jq -r '.identity_pubkey') -info "Connecting LND-1 → LND-4 (${LND4_PUBKEY:0:16}...)..." -lncli_1 connect "${LND4_PUBKEY}@${PREFIX}-lnd-4-1:9735" > /dev/null 2>&1 || true - -# Check if channel already exists -EXISTING=$(lncli_1 listchannels --peer "$LND4_PUBKEY" 2>/dev/null | jq '.channels | length') -if [ "${EXISTING:-0}" -gt 0 ]; then - warn "Channel to LND-4 already exists, skipping open" -else - info "Opening ${CHANNEL_SIZE} sat channel LND-1 → LND-4..." - lncli_1 openchannel "$LND4_PUBKEY" "$CHANNEL_SIZE" > /dev/null - - info "Mining 6 blocks to confirm channel..." - bitcoin_cli -generate 6 > /dev/null - - # Wait for channel to leave pending - for i in $(seq 1 60); do - PENDING=$(lncli_1 pendingchannels | jq '.pending_open_channels | length') - if [ "$PENDING" = "0" ]; then - break - fi - sleep 1 - done -fi - -# -- Step 5: Verify channel is active and in graph -- -info "Waiting for channel to become active..." -for i in $(seq 1 60); do - ACTIVE=$(lncli_1 listchannels --peer "$LND4_PUBKEY" 2>/dev/null | jq '[.channels[] | select(.active == true)] | length') - if [ "${ACTIVE:-0}" -gt 0 ]; then - ok "Channel active" - break - fi - if [ "$i" -eq 60 ]; then - warn "Channel not active after 60s — may need more blocks" - fi - sleep 1 -done - -# Mine a few more blocks to ensure graph announcement propagates -bitcoin_cli -generate 3 > /dev/null - -info "Checking graph..." -for i in $(seq 1 30); do - GRAPH_NODES=$(lncli_1 describegraph 2>/dev/null | jq '.nodes | length') - GRAPH_EDGES=$(lncli_1 describegraph 2>/dev/null | jq '.edges | length') - if [ "${GRAPH_EDGES:-0}" -gt 0 ]; then - ok "Graph: ${GRAPH_NODES} nodes, ${GRAPH_EDGES} edges" - break - fi - if [ "$i" -eq 30 ]; then - warn "Graph has no edges yet — channel may not be announced" - fi - sleep 2 -done - -# -- Step 6: Wait for Lightning.Pub to be healthy -- -info "Waiting for Lightning.Pub..." -for i in $(seq 1 120); do - if curl -so /dev/null "${LP_URL}" 2>/dev/null; then - ok "Lightning.Pub ready" - break - fi - if [ "$i" -eq 120 ]; then - err "Lightning.Pub not ready after 120s" - err "Check: docker logs bitspire-lightning-pub" - exit 1 - fi - sleep 1 -done - -# Small delay to ensure LP has fully initialized its LND connection -sleep 5 - -# -- Step 7: Create LP app -- -info "Creating LP app 'regtest-atm'..." -APP_RESPONSE=$(curl -sf "${LP_URL}/api/admin/app/add" \ - -H "Authorization: Bearer ${ADMIN_TOKEN}" \ - -H "Content-Type: application/json" \ - -d '{"name": "regtest-atm", "allow_user_creation": true}') - -if [ -z "$APP_RESPONSE" ]; then - err "Failed to create LP app — check LP logs" - exit 1 -fi - -APP_ID=$(echo "$APP_RESPONSE" | jq -r '.app.id') -APP_NPUB=$(echo "$APP_RESPONSE" | jq -r '.app.npub') -APP_TOKEN=$(echo "$APP_RESPONSE" | jq -r '.auth_token') - -if [ "$APP_ID" = "null" ] || [ -z "$APP_ID" ]; then - err "App creation returned unexpected response:" - echo "$APP_RESPONSE" | jq . - exit 1 -fi - -ok "App created: id=${APP_ID}" - -# -- Step 8: Fund the app (create invoice via LP, pay from LND-1) -- -info "Funding app with ${FUND_AMOUNT} sats..." -INVOICE_RESPONSE=$(curl -sf "${LP_URL}/api/app/add/invoice" \ - -H "Authorization: Bearer ${APP_TOKEN}" \ - -H "Content-Type: application/json" \ - -d "{\"payer_identifier\": \"bootstrap\", \"http_callback_url\": \"\", \"invoice_req\": {\"amountSats\": ${FUND_AMOUNT}, \"memo\": \"regtest bootstrap\"}}") - -INVOICE=$(echo "$INVOICE_RESPONSE" | jq -r '.invoice') -if [ "$INVOICE" = "null" ] || [ -z "$INVOICE" ]; then - warn "Failed to create invoice — app has 0 balance" - warn "Response: $INVOICE_RESPONSE" -else - # Pay from LND-1 - PAY_RESULT=$(lncli_1 payinvoice --force "$INVOICE" 2>&1) || true - PAY_STATUS=$(echo "$PAY_RESULT" | jq -r '.status' 2>/dev/null || echo "") - if [ "$PAY_STATUS" = "SUCCEEDED" ] || echo "$PAY_RESULT" | grep -q "SUCCEEDED"; then - ok "App funded with ${FUND_AMOUNT} sats" - else - warn "Payment may have failed — check manually" - warn "Result: $(echo "$PAY_RESULT" | tail -3)" - fi -fi - -# -- Summary -- -# LP uses LND-4 as its backend, so LP pubkey = LND-4 pubkey -echo "" -echo -e "${GREEN}========================================${NC}" -echo -e "${GREEN} Regtest Bootstrap Complete${NC}" -echo -e "${GREEN}========================================${NC}" -echo "" -echo -e " ${CYAN}LND-1 pubkey:${NC} $(lncli_1 getinfo | jq -r '.identity_pubkey')" -echo -e " ${CYAN}LP (LND-4):${NC} ${LND4_PUBKEY}" -echo "" -echo -e " ${CYAN}LP app ID:${NC} ${APP_ID}" -echo -e " ${CYAN}LP app token:${NC} ${APP_TOKEN}" -echo "" -echo -e " ${CYAN}LP URL:${NC} ${LP_URL}" -echo -e " ${CYAN}Extension URL:${NC} ${LP_EXT_URL}" -echo -e " ${CYAN}Relay URL:${NC} ws://localhost:7777" -echo "" -CHAN_INFO=$(lncli_1 listchannels --peer "$LND4_PUBKEY" 2>/dev/null | jq -r '.channels[0] | "\(.local_balance) / \(.capacity) sats (active: \(.active))"' 2>/dev/null || echo "unknown") -echo -e " ${CYAN}Channel:${NC} LND-1 → LND-4: ${CHAN_INFO}" -echo "" diff --git a/docker/regtest.sh b/docker/regtest.sh deleted file mode 100755 index 1c198a0..0000000 --- a/docker/regtest.sh +++ /dev/null @@ -1,142 +0,0 @@ -#!/usr/bin/env bash -# regtest.sh — Manage the LP+LND regtest development environment -# -# Usage: -# ./regtest.sh up Start everything + bootstrap -# ./regtest.sh down Stop everything -# ./regtest.sh reset Full teardown (volumes + data) and fresh start -# ./regtest.sh status Show container status -# ./regtest.sh logs [svc] Tail logs (lp, relay, miner, lnd1, lnd4, bitcoind) -# ./regtest.sh lncli N Run lncli on LND-N (e.g. ./regtest.sh lncli 1 getinfo) -# ./regtest.sh bitcoin Run bitcoin-cli (e.g. ./regtest.sh bitcoin getblockcount) - -set -euo pipefail - -SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)" -REGTEST_DIR="${REGTEST_DIR:-$HOME/dev/local/docker/regtest}" -COMPOSE_FILE="${SCRIPT_DIR}/docker-compose.regtest.yml" - -RED='\033[0;31m' -GREEN='\033[0;32m' -CYAN='\033[0;36m' -NC='\033[0m' - -# -- Detect container prefix (regtest- or lnbits-) -- -detect_prefix() { - if docker ps -f name=regtest-bitcoind-1 -f status=running --format '{{.Names}}' | grep -q .; then - echo "regtest" - elif docker ps -f name=lnbits-bitcoind-1 -f status=running --format '{{.Names}}' | grep -q .; then - echo "lnbits" - else - echo "" - fi -} - -cmd_up() { - echo -e "${CYAN}Starting regtest (bitcoind + lnd-1 + lnd-4)...${NC}" - cd "$REGTEST_DIR" && docker compose up -d bitcoind lnd-1 lnd-4 - - echo -e "${CYAN}Starting bitspire services...${NC}" - docker compose -f "$COMPOSE_FILE" up -d - - echo -e "${CYAN}Running bootstrap...${NC}" - "$SCRIPT_DIR/regtest-bootstrap.sh" -} - -cmd_down() { - echo -e "${CYAN}Stopping bitspire services...${NC}" - docker compose -f "$COMPOSE_FILE" down 2>/dev/null || true - - echo -e "${CYAN}Stopping regtest...${NC}" - cd "$REGTEST_DIR" && docker compose down 2>/dev/null || true - - echo -e "${GREEN}All stopped.${NC}" -} - -cmd_reset() { - echo -e "${CYAN}Full reset: tearing down everything + wiping data...${NC}" - - docker compose -f "$COMPOSE_FILE" down -v 2>/dev/null || true - cd "$REGTEST_DIR" && docker compose down -v 2>/dev/null || true - - # Clean LND data (root-owned from containers) - docker run --rm -v "$REGTEST_DIR/data:/data" alpine sh -c 'rm -rf /data/lnd-1/* /data/lnd-4/*' 2>/dev/null || true - - echo -e "${GREEN}Clean. Starting fresh...${NC}" - cmd_up -} - -cmd_status() { - echo -e "${CYAN}Containers:${NC}" - docker ps --format 'table {{.Names}}\t{{.Status}}\t{{.Ports}}' | grep -E 'regtest|bitspire|NAMES' | sort - - PREFIX=$(detect_prefix) - if [ -n "$PREFIX" ]; then - echo "" - BLOCK=$(docker exec "${PREFIX}-bitcoind-1" bitcoin-cli -regtest getblockcount 2>/dev/null || echo "?") - echo -e " ${CYAN}Block height:${NC} ${BLOCK}" - - for N in 1 4; do - SYNCED=$(docker exec "${PREFIX}-lnd-${N}-1" lncli --network regtest --rpcserver="lnd-${N}:10009" getinfo 2>/dev/null | jq -r '.synced_to_chain' 2>/dev/null || echo "?") - echo -e " ${CYAN}LND-${N} synced:${NC} ${SYNCED}" - done - - LP_STATUS=$(curl -so /dev/null -w "%{http_code}" http://localhost:1776 2>/dev/null || echo "down") - echo -e " ${CYAN}LP status:${NC} ${LP_STATUS}" - - EXT_STATUS=$(curl -so /dev/null -w "%{http_code}" http://localhost:1777 2>/dev/null || echo "down") - echo -e " ${CYAN}Extension:${NC} ${EXT_STATUS}" - fi -} - -cmd_logs() { - local svc="${1:-lp}" - case "$svc" in - lp|lightning-pub) docker logs -f --tail 50 bitspire-lightning-pub ;; - relay|strfry) docker logs -f --tail 50 bitspire-relay ;; - miner) docker logs -f --tail 50 bitspire-miner ;; - lnd1|lnd-1) PREFIX=$(detect_prefix); docker logs -f --tail 50 "${PREFIX}-lnd-1-1" ;; - lnd4|lnd-4) PREFIX=$(detect_prefix); docker logs -f --tail 50 "${PREFIX}-lnd-4-1" ;; - bitcoind) PREFIX=$(detect_prefix); docker logs -f --tail 50 "${PREFIX}-bitcoind-1" ;; - *) echo "Unknown service: $svc (try: lp, relay, miner, lnd1, lnd4, bitcoind)"; exit 1 ;; - esac -} - -cmd_lncli() { - local N="$1"; shift - PREFIX=$(detect_prefix) - if [ -z "$PREFIX" ]; then - echo -e "${RED}No running regtest found${NC}"; exit 1 - fi - docker exec "${PREFIX}-lnd-${N}-1" lncli --network regtest --rpcserver="lnd-${N}:10009" "$@" -} - -cmd_bitcoin() { - PREFIX=$(detect_prefix) - if [ -z "$PREFIX" ]; then - echo -e "${RED}No running regtest found${NC}"; exit 1 - fi - docker exec "${PREFIX}-bitcoind-1" bitcoin-cli -regtest "$@" -} - -# -- Main -- -case "${1:-help}" in - up) cmd_up ;; - down) cmd_down ;; - reset) cmd_reset ;; - status) cmd_status ;; - logs) shift; cmd_logs "${1:-lp}" ;; - lncli) shift; cmd_lncli "$@" ;; - bitcoin) shift; cmd_bitcoin "$@" ;; - *) - echo "Usage: $0 {up|down|reset|status|logs|lncli|bitcoin}" - echo "" - echo " up Start everything + bootstrap" - echo " down Stop everything" - echo " reset Full teardown + fresh start" - echo " status Show container & service status" - echo " logs [s] Tail logs (lp, relay, miner, lnd1, lnd4, bitcoind)" - echo " lncli N Run lncli on LND-N (e.g. $0 lncli 1 getinfo)" - echo " bitcoin Run bitcoin-cli (e.g. $0 bitcoin getblockcount)" - ;; -esac diff --git a/docker/start-with-regtest.sh b/docker/start-with-regtest.sh deleted file mode 100755 index 93aff63..0000000 --- a/docker/start-with-regtest.sh +++ /dev/null @@ -1,335 +0,0 @@ -#!/bin/bash -# -# Start bitspire-next development environment with comprehensive regtest -# -# This script: -# 1. Connects to the regtest environment at ~/dev/local/docker/regtest -# 2. Starts Lightning.Pub with the specified image/worktree -# 3. Creates and funds an ATM account -# 4. Displays connection info for Zeus wallet and Lightning.Pub nprofile -# -# Prerequisites: -# ~/dev/local/docker/regtest must be running: -# cd ~/dev/local/docker/regtest && ./start-regtest -# -# Usage: -# ./start-with-regtest.sh # Start with default image -# ./start-with-regtest.sh --image myimage # Use specific Docker image -# ./start-with-regtest.sh --worktree ~/path # Build from worktree -# ./start-with-regtest.sh down # Stop services -# ./start-with-regtest.sh logs # Follow logs -# ./start-with-regtest.sh status # Show connection info -# - -set -e - -SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" -REGTEST_DIR="${REGTEST_DIR:-$HOME/dev/local/docker/regtest}" -DEFAULT_IMAGE="lightning-pub-withdraw:latest" -ATM_FUNDING_SATS=100000 - -# Colors -GREEN='\033[0;32m' -YELLOW='\033[1;33m' -RED='\033[0;31m' -BLUE='\033[0;34m' -CYAN='\033[0;36m' -BOLD='\033[1m' -NC='\033[0m' - -log() { echo -e "${GREEN}[bitspire]${NC} $1"; } -warn() { echo -e "${YELLOW}[bitspire]${NC} $1"; } -error() { echo -e "${RED}[bitspire]${NC} $1"; } -info() { echo -e "${CYAN}[bitspire]${NC} $1"; } - -# Get local IP for external wallet access -get_local_ip() { - ip route get 1 2>/dev/null | awk '{print $7; exit}' || hostname -I | awk '{print $1}' -} - -# Check if regtest is running -check_regtest() { - if ! docker network inspect lnbits_default &>/dev/null; then - error "Regtest network not found!" - echo "" - echo "Start the regtest environment first:" - echo " cd $REGTEST_DIR && ./start-regtest" - exit 1 - fi - - # Check if lnd-4 is running (Lightning.Pub's backend) - if ! docker exec lnbits-lnd-4-1 lncli --network=regtest getinfo &>/dev/null 2>&1; then - warn "lnd-4 not responding. Waiting..." - sleep 5 - fi -} - -# Build Lightning.Pub from worktree -build_from_worktree() { - local worktree="$1" - local image_name="lightning-pub-custom:latest" - - if [[ ! -d "$worktree" ]]; then - error "Worktree not found: $worktree" - exit 1 - fi - - log "Building Lightning.Pub from $worktree..." - docker build -t "$image_name" "$worktree" - echo "$image_name" -} - -# Wait for Lightning.Pub to be ready and get nprofile -wait_for_lightning_pub() { - log "Waiting for Lightning.Pub to start..." - local max_attempts=30 - local attempt=0 - - while [[ $attempt -lt $max_attempts ]]; do - if docker logs bitspire-lightning-pub 2>&1 | grep -q "LightningPub listening"; then - sleep 2 # Extra time for Nostr middleware - return 0 - fi - attempt=$((attempt + 1)) - sleep 2 - done - - error "Lightning.Pub failed to start within 60 seconds" - docker logs bitspire-lightning-pub 2>&1 | tail -20 - return 1 -} - -# Get Lightning.Pub nprofile from logs -get_nprofile() { - docker logs bitspire-lightning-pub 2>&1 | grep -oP 'nprofile:\s*\K\S+' | tail -1 -} - -# Get Lightning.Pub pubkey from logs -get_pubkey() { - docker logs bitspire-lightning-pub 2>&1 | grep -oP 'pubkey:\s*\K[a-f0-9]+' | tail -1 -} - -# Generate lndconnect URL for Zeus (using lnd-3 which has REST exposed) -generate_lndconnect() { - local lnd_container="lnbits-lnd-3-1" - local lnd_data="$REGTEST_DIR/data/lnd-3" - local local_ip=$(get_local_ip) - local rest_port=8082 # lnd-3's REST port - - # Get cert and macaroon - local cert_path="$lnd_data/tls.cert" - local mac_path="$lnd_data/data/chain/bitcoin/regtest/admin.macaroon" - - if [[ ! -f "$cert_path" ]] || [[ ! -f "$mac_path" ]]; then - warn "lnd-3 credentials not found. Zeus connection string unavailable." - return 1 - fi - - # Base64url encode (replace + with -, / with _, remove =) - local cert_b64=$(base64 -w0 "$cert_path" | tr '+/' '-_' | tr -d '=') - local mac_b64=$(base64 -w0 "$mac_path" | tr '+/' '-_' | tr -d '=') - - echo "lndconnect://${local_ip}:${rest_port}?cert=${cert_b64}&macaroon=${mac_b64}" -} - -# Create and fund ATM account -setup_atm_account() { - log "Setting up ATM account..." - - # Create app - local app_response=$(curl -s -X POST http://localhost:1776/api/admin/app/add \ - -H "Content-Type: application/json" \ - -H "Authorization: Bearer bitspire-dev-admin-token" \ - -d '{"name":"atm-app","allow_user_creation":true}' 2>/dev/null) - - if echo "$app_response" | grep -q '"status":"OK"'; then - local app_id=$(echo "$app_response" | grep -oP '"id":"\K[^"]+') - local app_token=$(echo "$app_response" | grep -oP '"auth_token":"\K[^"]+') - log "Created ATM app: $app_id" - - # Store app token for later use - echo "$app_token" > "$SCRIPT_DIR/.atm-app-token" - echo "$app_id" > "$SCRIPT_DIR/.atm-app-id" - - # TODO: Fund the app by creating an invoice and paying from lnd-3 - # This requires the app to support invoice creation via HTTP API - # For now, the app starts with 0 balance - - return 0 - else - warn "Failed to create ATM app: $app_response" - return 1 - fi -} - -# Display connection information -show_connection_info() { - local nprofile=$(get_nprofile) - local pubkey=$(get_pubkey) - local local_ip=$(get_local_ip) - local lndconnect=$(generate_lndconnect 2>/dev/null || echo "") - - echo "" - echo -e "${BOLD}═══════════════════════════════════════════════════════════════${NC}" - echo -e "${BOLD} BITSPIRE REGTEST ENVIRONMENT ${NC}" - echo -e "${BOLD}═══════════════════════════════════════════════════════════════${NC}" - echo "" - - echo -e "${CYAN}Lightning.Pub:${NC}" - echo -e " Pubkey: ${GREEN}$pubkey${NC}" - echo -e " nprofile: ${GREEN}$nprofile${NC}" - echo "" - - echo -e "${CYAN}Service URLs:${NC}" - echo " Nostr Relay: ws://localhost:7777" - echo " Lightning.Pub: http://localhost:1776" - echo " Withdraw API: http://localhost:1777" - echo " PostgreSQL: localhost:5432" - echo "" - - echo -e "${CYAN}External Access (from phone/other devices):${NC}" - echo " Nostr Relay: ws://${local_ip}:7777" - echo " Withdraw API: http://${local_ip}:1777" - echo "" - - if [[ -n "$lndconnect" ]]; then - echo -e "${CYAN}Zeus Wallet Connection (lnd-3):${NC}" - echo -e " ${GREEN}$lndconnect${NC}" - echo "" - echo " Scan this with Zeus to connect to lnd-3 for testing payments." - echo "" - fi - - echo -e "${CYAN}ATM Configuration (apps/machine/.env):${NC}" - echo " VITE_RELAY_URL=ws://localhost:7777" - echo " VITE_LIGHTNING_PUB_PUBKEY=$pubkey" - echo " VITE_EXTENSION_API_URL=http://localhost:1777" - echo "" - - echo -e "${CYAN}CLI Helpers:${NC}" - echo " source $REGTEST_DIR/docker-scripts.sh" - echo " bitcoin-cli-sim -generate 1 # Mine blocks" - echo " lncli-sim 4 getinfo # lnd-4 (Lightning.Pub)" - echo " lncli-sim 3 getinfo # lnd-3 (Zeus wallet)" - echo "" - echo -e "${BOLD}═══════════════════════════════════════════════════════════════${NC}" -} - -# Start services -start() { - local image="$DEFAULT_IMAGE" - local worktree="" - - # Parse arguments - while [[ $# -gt 0 ]]; do - case "$1" in - --image) - image="$2" - shift 2 - ;; - --worktree) - worktree="$2" - shift 2 - ;; - *) - shift - ;; - esac - done - - log "Checking prerequisites..." - check_regtest - - # Build from worktree if specified - if [[ -n "$worktree" ]]; then - image=$(build_from_worktree "$worktree") - fi - - # Check if image exists - if ! docker image inspect "$image" &>/dev/null; then - error "Docker image not found: $image" - echo "" - echo "Either:" - echo " 1. Build the image: docker build -t $image " - echo " 2. Use --worktree: ./start-with-regtest.sh --worktree ~/path/to/lightning-pub" - exit 1 - fi - - log "Using Lightning.Pub image: $image" - - # Export environment variables - export REGTEST_DATA_DIR="$REGTEST_DIR/data" - export LIGHTNING_PUB_IMAGE="$image" - export HOST_IP=$(get_local_ip) - - log "Starting bitspire services..." - docker compose -f "$SCRIPT_DIR/docker-compose.regtest.yml" up -d - - # Wait for Lightning.Pub and setup - if wait_for_lightning_pub; then - setup_atm_account || true - show_connection_info - else - error "Failed to start Lightning.Pub. Check logs with: $0 logs lightning-pub" - exit 1 - fi -} - -# Stop services -stop() { - log "Stopping bitspire services..." - docker compose -f "$SCRIPT_DIR/docker-compose.regtest.yml" down - rm -f "$SCRIPT_DIR/.atm-app-token" "$SCRIPT_DIR/.atm-app-id" - log "Services stopped." -} - -# Show logs -logs() { - docker compose -f "$SCRIPT_DIR/docker-compose.regtest.yml" logs -f "$@" -} - -# Show status/connection info -status() { - if ! docker ps --format '{{.Names}}' | grep -q bitspire-lightning-pub; then - error "Services not running. Start with: $0 start" - exit 1 - fi - show_connection_info -} - -# Main -case "${1:-start}" in - start) - shift || true - start "$@" - ;; - stop|down) - stop - ;; - logs) - shift - logs "$@" - ;; - status|info) - status - ;; - *) - echo "Usage: $0 [command] [options]" - echo "" - echo "Commands:" - echo " start Start services (default)" - echo " stop, down Stop services" - echo " logs [service] Follow logs" - echo " status, info Show connection info" - echo "" - echo "Options for 'start':" - echo " --image Use specific Docker image" - echo " --worktree Build from Lightning.Pub worktree" - echo "" - echo "Examples:" - echo " $0 # Start with default image" - echo " $0 --worktree ~/dev/lightning-pub/withdraw # Build and use worktree" - echo " $0 --image lightning-pub-custom:v1 # Use specific image" - exit 1 - ;; -esac diff --git a/docker/strfry.conf b/docker/strfry.conf deleted file mode 100644 index 30e6dc0..0000000 --- a/docker/strfry.conf +++ /dev/null @@ -1,65 +0,0 @@ -## -## strfry configuration for bitSpire ATM development -## - -relay { - bind = "0.0.0.0" - port = 7777 - - # Set to 0 to skip configuring nofiles limit (avoids container ulimit issues) - nofiles = 0 - - info { - name = "bitSpire Dev Relay" - description = "Private Nostr relay for ATM development and testing" - pubkey = "" - contact = "" - } - - # Maximum message size in bytes - maxWebsocketPayloadSize = 131072 - - # Connection limits - maxWebsockets = 100 - maxConnections = 1000 -} - -# Event policies -events { - # Maximum event size - maxEventSize = 65536 - - # Rate limiting - rejectEventsNewerThanSeconds = 900 - rejectEventsOlderThanSeconds = 94608000 - - # Event kinds we care about - # 14: Private DMs (NIP-17) - # 21000: Lightning.Pub RPC (generic request/response) - # 21001-21003: CLINK events (Offer, Debit, Manage) - # 22242: NIP-42 auth - # 30078: Service Beacon (replaceable, service discovery) - # 30079: Transaction records (replaceable) - - # TODO: Review if these ephemeral event settings actually do anything useful. - # CLINK events (21000-21003) are in the ephemeral range but need to persist - # long enough for request/response flows. These settings may not be recognized - # by strfry - verify against strfry documentation. - # Allow ephemeral events up to 5 minutes old to be accepted - rejectEphemeralEventsOlderThanSeconds = 300 - # Keep ephemeral events for 5 minutes before deletion - ephemeralEventsLifetimeSeconds = 300 -} - -# Negentropy sync (for relay federation) -negentropy { - enabled = true - syncOnConnect = false -} - -# Plugins (for NIP-42 auth in production) -# plugins { -# authRequired = true -# writePolicy = "accept" -# readPolicy = "accept" -# } diff --git a/docs/ndebit-cash-in-flow.md b/docs/ndebit-cash-in-flow.md index 2119bf0..c8d5034 100644 --- a/docs/ndebit-cash-in-flow.md +++ b/docs/ndebit-cash-in-flow.md @@ -9,6 +9,11 @@ > debit-approval listener and all kind-21002 handling were removed in > commit `3c14eea` (the 3b.4 cleanup of LP-paired infrastructure). > +> The Lightning.Pub regtest tooling this doc leans on — the `docker/` +> compose stack, the `fund-atm` / `lncli` devenv commands, and the +> `packages/nostr-client/dev/` agent scripts — was removed on 2026-10-09. +> Commands quoted below no longer exist in this repo. +> > This doc is retained because it explains *why* the previous flow > existed and what the ndebit/CLINK protocol surface looks like — useful > if the project ever wants to reintroduce nostr-native cash-in that diff --git a/packages/nostr-client/dev/atm-debit-agent.mjs b/packages/nostr-client/dev/atm-debit-agent.mjs deleted file mode 100644 index c40c586..0000000 --- a/packages/nostr-client/dev/atm-debit-agent.mjs +++ /dev/null @@ -1,270 +0,0 @@ -/** - * ATM Debit Authorization Agent - * - * This script demonstrates how an ATM can act as the authorization authority - * for CLINK debit requests, similar to an "admin macaroon" for Lightning. - * - * Flow: - * 1. Generate keypair for ATM (or load from secure storage) - * 2. Link keypair to Lightning.Pub user account - * 3. Subscribe to live debit requests - * 4. Auto-approve requests (within configured limits) - * - * Prerequisites: - * - Get a linking token from Lightning.Pub HTTP API - * - Run: curl -X POST "http://localhost:1776/api/app/user/npub/token/reset" \ - * -H "Authorization: Bearer $APP_TOKEN" \ - * -H "Content-Type: application/json" \ - * -d '{"user_identifier": "YOUR_USER_IDENTIFIER"}' - */ - -import { Relay } from 'nostr-tools/relay' -import { finalizeEvent, getPublicKey, generateSecretKey } from 'nostr-tools' -import * as nip44v1 from './nip44v1.mjs' - -// Configuration -const LINKING_TOKEN = process.argv[2] -const LIGHTNING_PUB_PUBKEY = '6c59284e3da31b776cb1c06324c25f4a0b0308177af9f8aec5ebef07b44c3fdf' -const RELAY_URL = process.env.RELAY_URL || 'ws://localhost:7777' - -// Generate ATM keypair (in production, this would be stored securely) -const ATM_PRIVATE_KEY = generateSecretKey() -const ATM_PUBLIC_KEY = getPublicKey(ATM_PRIVATE_KEY) -const ATM_PRIVATE_KEY_HEX = Buffer.from(ATM_PRIVATE_KEY).toString('hex') - -if (!LINKING_TOKEN) { - console.log('ATM Debit Authorization Agent') - console.log('==============================') - console.log('') - console.log('Usage: node atm-debit-agent.mjs ') - console.log('') - console.log('Get a linking token:') - console.log(' curl -X POST "http://localhost:1776/api/app/user/npub/token/reset" \\') - console.log(' -H "Authorization: Bearer $APP_TOKEN" \\') - console.log(' -H "Content-Type: application/json" \\') - console.log(' -d \'{"user_identifier": "YOUR_USER_IDENTIFIER"}\'') - process.exit(1) -} - -console.log('=== ATM Debit Authorization Agent ===') -console.log('') -console.log('ATM Pubkey:', ATM_PUBLIC_KEY) -console.log('Lightning.Pub Pubkey:', LIGHTNING_PUB_PUBKEY) -console.log('Linking Token:', LINKING_TOKEN.substring(0, 16) + '...') -console.log('') - -async function main() { - // Connect to relay - console.log('Connecting to relay...') - const relay = await Relay.connect(RELAY_URL) - console.log('Connected!') - console.log('') - - // Create conversation key for NIP-44 v1 encryption (used by Kind 21000 RPC) - const conversationKey = nip44v1.getConversationKey(ATM_PRIVATE_KEY_HEX, LIGHTNING_PUB_PUBKEY) - - // Step 1: Link NPub through token - console.log('Step 1: Linking ATM keypair to user account...') - - const linkRequest = { - rpcName: 'LinkNPubThroughToken', - authIdentifier: ATM_PUBLIC_KEY, - body: { - token: LINKING_TOKEN, - }, - } - - const linkEvent = finalizeEvent( - { - kind: 21000, - created_at: Math.floor(Date.now() / 1000), - tags: [['p', LIGHTNING_PUB_PUBKEY]], - content: nip44v1.encrypt(JSON.stringify(linkRequest), conversationKey), - }, - ATM_PRIVATE_KEY - ) - - // Subscribe for response - let linkingComplete = false - const linkSub = relay.subscribe( - [ - { - kinds: [21000], - authors: [LIGHTNING_PUB_PUBKEY], - '#p': [ATM_PUBLIC_KEY], - since: Math.floor(Date.now() / 1000) - 5, - }, - ], - { - onevent(evt) { - try { - const decrypted = nip44v1.decrypt(evt.content, conversationKey) - const response = JSON.parse(decrypted) - console.log('Link response:', JSON.stringify(response)) - if (response.status === 'OK') { - linkingComplete = true - console.log('Keypair linked successfully!') - } - } catch (err) { - console.log('Failed to decrypt link response:', err.message) - } - }, - } - ) - - await relay.publish(linkEvent) - console.log( - 'Link request sent (event id:', - linkEvent.id.substring(0, 16) + '...), waiting for confirmation...' - ) - - // Wait for linking to complete - for (let i = 0; i < 10 && !linkingComplete; i++) { - await new Promise((r) => setTimeout(r, 1000)) - if (i % 3 === 2) console.log('Still waiting for link confirmation...') - } - linkSub.close() - - if (!linkingComplete) { - console.log('Warning: Did not receive linking confirmation, continuing anyway...') - } - console.log('') - - // Step 2: Subscribe to live debit requests - console.log('Step 2: Subscribing to live debit requests...') - - const subscribeRequest = { - rpcName: 'GetLiveDebitRequests', - authIdentifier: ATM_PUBLIC_KEY, - body: {}, - } - - const subEvent = finalizeEvent( - { - kind: 21000, - created_at: Math.floor(Date.now() / 1000), - tags: [['p', LIGHTNING_PUB_PUBKEY]], - content: nip44v1.encrypt(JSON.stringify(subscribeRequest), conversationKey), - }, - ATM_PRIVATE_KEY - ) - - // Subscribe for debit requests and responses - console.log('Listening for debit requests...') - console.log('(Scan the ndebit QR code with ShockWallet to test)') - console.log('') - - const debitSub = relay.subscribe( - [ - { - kinds: [21000], - authors: [LIGHTNING_PUB_PUBKEY], - '#p': [ATM_PUBLIC_KEY], - since: Math.floor(Date.now() / 1000) - 5, - }, - ], - { - async onevent(evt) { - try { - const decrypted = nip44v1.decrypt(evt.content, conversationKey) - const message = JSON.parse(decrypted) - - // Check if this is a debit request (has request_id and debit fields) - if (message.requestId === 'GetLiveDebitRequests' && message.debit) { - console.log('') - console.log('========================================') - console.log('Received debit request!') - console.log(' Request ID:', message.request_id) - console.log(' From npub:', message.npub) - console.log(' Debit type:', message.debit.type) - - if (message.debit.type === 'invoice' && message.debit.invoice) { - console.log(' Invoice:', message.debit.invoice.substring(0, 50) + '...') - - // Auto-approve by responding with INVOICE type - console.log('') - console.log('Auto-approving debit request...') - - const approveRequest = { - rpcName: 'RespondToDebit', - authIdentifier: ATM_PUBLIC_KEY, - body: { - npub: message.npub, - request_id: message.request_id, - response: { - type: 'invoice', - invoice: message.debit.invoice, - }, - }, - } - - const approveEvent = finalizeEvent( - { - kind: 21000, - created_at: Math.floor(Date.now() / 1000), - tags: [['p', LIGHTNING_PUB_PUBKEY]], - content: nip44v1.encrypt(JSON.stringify(approveRequest), conversationKey), - }, - ATM_PRIVATE_KEY - ) - - await relay.publish(approveEvent) - console.log('Approval sent! (event id:', approveEvent.id.substring(0, 16) + '...)') - } else if (message.debit.type === 'budget') { - console.log(' Budget request - ignoring for now') - } else if (message.debit.type === 'fullAccess') { - console.log(' Full access request - ignoring for now') - } - - console.log('========================================') - console.log('') - } else if (message.rpcName) { - // This is a response to our RPC request - console.log('RPC response:', message.rpcName, ':', message.status || 'received') - } else { - // Log other messages for debugging - console.log('Message received:', JSON.stringify(message).substring(0, 100)) - } - } catch (err) { - // Ignore decryption failures (may be messages for other clients) - if (!err.message.includes('Unsupported')) { - console.log('Error processing message:', err.message) - } - } - }, - } - ) - - await relay.publish(subEvent) - console.log('Subscription request sent (event id:', subEvent.id.substring(0, 16) + '...)') - console.log('') - - // Keep running - console.log('ATM Debit Agent running. Press Ctrl+C to exit.') - console.log('') - - // Handle graceful shutdown - process.on('SIGINT', () => { - console.log('\nShutting down...') - debitSub.close() - relay.close() - process.exit(0) - }) - - // Keep alive with heartbeat - let heartbeatCount = 0 - while (true) { - await new Promise((r) => setTimeout(r, 10000)) - heartbeatCount++ - if (heartbeatCount % 6 === 0) { - // Every minute - console.log('Still listening... (' + heartbeatCount * 10 + 's)') - } - } -} - -main().catch((err) => { - console.error('Error:', err.message) - console.error(err.stack) - process.exit(1) -}) diff --git a/packages/nostr-client/dev/fund-dev.mjs b/packages/nostr-client/dev/fund-dev.mjs deleted file mode 100644 index d804067..0000000 --- a/packages/nostr-client/dev/fund-dev.mjs +++ /dev/null @@ -1,133 +0,0 @@ -#!/usr/bin/env node -/** - * Fund the ATM's Lightning.Pub account via Nostr RPC - * - * This creates an invoice for the ATM user (authenticated via Nostr), - * so the funds go directly to the user that will be queried for balance. - * - * Usage: - * VITE_ATM_PRIVATE_KEY=xxx VITE_LIGHTNING_PUB_PUBKEY=yyy node fund-dev.mjs [amount] - */ -import { NostrClient, loadIdentityFromHex, encryptContent, decryptJSON } from '../dist/index.js' -import { finalizeEvent } from 'nostr-tools' -import { randomUUID } from 'crypto' - -const ATM_PRIVATE_KEY = process.env.VITE_ATM_PRIVATE_KEY -const LIGHTNING_PUB_PUBKEY = process.env.VITE_LIGHTNING_PUB_PUBKEY -const RELAY_URL = process.env.VITE_RELAY_URL || 'ws://localhost:7777' -const APP_ID = process.env.VITE_APP_ID || '' -const FUND_AMOUNT = parseInt(process.argv[2] || process.env.FUND_AMOUNT || '100000', 10) - -if (!ATM_PRIVATE_KEY) { - console.error('Error: VITE_ATM_PRIVATE_KEY environment variable required') - process.exit(1) -} - -if (!LIGHTNING_PUB_PUBKEY) { - console.error('Error: VITE_LIGHTNING_PUB_PUBKEY environment variable required') - process.exit(1) -} - -if (!APP_ID) { - console.error('Error: VITE_APP_ID environment variable required') - console.error('This ensures funds go to the same user as LNURL-withdraw uses') - process.exit(1) -} - -async function main() { - const identity = loadIdentityFromHex(ATM_PRIVATE_KEY) - console.error('[fund-dev] ATM pubkey:', identity.publicKey) - console.error('[fund-dev] Lightning.Pub pubkey:', LIGHTNING_PUB_PUBKEY) - console.error('[fund-dev] Creating invoice for', FUND_AMOUNT, 'sats') - - const client = new NostrClient({ - relays: [{ url: RELAY_URL }], - identity, - }) - - await client.connect() - console.error('[fund-dev] Connected to relay:', RELAY_URL) - - const requestId = randomUUID() - - console.error('[fund-dev] App ID:', APP_ID) - - // Correct RPC structure per Lightning.Pub documentation - // appId ensures the Nostr user is created under the same app as HTTP API users - const rpcRequest = { - rpcName: 'NewInvoice', - params: {}, - query: {}, - body: { - amountSats: FUND_AMOUNT, - memo: `ATM funding (${FUND_AMOUNT} sats)`, - }, - authIdentifier: identity.publicKey, - requestId, - appId: APP_ID, - } - - const encryptedContent = encryptContent(identity, LIGHTNING_PUB_PUBKEY, rpcRequest) - - const event = finalizeEvent( - { - kind: 21000, - created_at: Math.floor(Date.now() / 1000), - tags: [['p', LIGHTNING_PUB_PUBKEY]], - content: encryptedContent, - }, - identity.privateKey - ) - - // Subscribe to responses before publishing - let invoice = null - const subId = client.subscribe( - [ - { - kinds: [21000], - authors: [LIGHTNING_PUB_PUBKEY], - '#p': [identity.publicKey], - since: Math.floor(Date.now() / 1000) - 5, - }, - ], - { - onEvent: (evt) => { - try { - const response = decryptJSON(identity, LIGHTNING_PUB_PUBKEY, evt.content) - if (response.requestId === requestId && response.invoice) { - invoice = response.invoice - console.error('[fund-dev] Got invoice!') - } - } catch (err) { - // Ignore decrypt errors for other messages - } - }, - } - ) - - await client.publish(event) - console.error('[fund-dev] Request published, waiting for invoice...') - - // Wait up to 15 seconds for response - for (let i = 0; i < 30; i++) { - await new Promise((resolve) => setTimeout(resolve, 500)) - if (invoice) break - } - - client.unsubscribe(subId) - client.disconnect() - - if (!invoice) { - console.error('[fund-dev] Error: No invoice received within timeout') - process.exit(1) - } - - // Output just the invoice to stdout (for piping to payment command) - console.log(invoice) - process.exit(0) -} - -main().catch((err) => { - console.error('[fund-dev] Error:', err.message) - process.exit(1) -}) diff --git a/packages/nostr-client/dev/generate-ndebit.mjs b/packages/nostr-client/dev/generate-ndebit.mjs deleted file mode 100644 index d344dac..0000000 --- a/packages/nostr-client/dev/generate-ndebit.mjs +++ /dev/null @@ -1,84 +0,0 @@ -/** - * Generate an ndebit string for ShockWallet to scan - * - * Usage: node generate-ndebit.mjs [pointer] - * - * The ndebit allows ShockWallet to send an invoice that Lightning.Pub will pay. - * This is the LNURL-withdraw equivalent for CLINK. - */ - -import { bech32 } from '@scure/base' - -const LIGHTNING_PUB_PUBKEY = - process.env.LIGHTNING_PUB_PUBKEY || - '4a72e400a254bf74a70cc711ab97e461b8d4fd9738b1ac3b5194cdeb3192ab91' -const RELAY_URL = process.env.NOSTR_RELAY_URL || 'wss://strfry.shock.network' -const POINTER = process.argv[2] || 'atm-cashin-' + Date.now() - -function hexToBytes(hex) { - const bytes = new Uint8Array(hex.length / 2) - for (let i = 0; i < hex.length; i += 2) { - bytes[i / 2] = parseInt(hex.slice(i, i + 2), 16) - } - return bytes -} - -function encodeTLV(tlv) { - const entries = [] - Object.entries(tlv) - .reverse() - .forEach(([t, vs]) => { - vs.forEach((v) => { - const entry = new Uint8Array(v.length + 2) - entry.set([parseInt(t)], 0) - entry.set([v.length], 1) - entry.set(v, 2) - entries.push(entry) - }) - }) - - // Concatenate all entries - const totalLength = entries.reduce((sum, e) => sum + e.length, 0) - const result = new Uint8Array(totalLength) - let offset = 0 - for (const entry of entries) { - result.set(entry, offset) - offset += entry.length - } - return result -} - -function ndebitEncode(debit) { - const encoder = new TextEncoder() - - const tlv = { - 0: [hexToBytes(debit.pubkey)], - 1: [encoder.encode(debit.relay)], - } - - if (debit.pointer) { - tlv[2] = [encoder.encode(debit.pointer)] - } - - const data = encodeTLV(tlv) - const words = bech32.toWords(data) - return bech32.encode('ndebit', words, 5000) -} - -// Generate ndebit -const ndebit = ndebitEncode({ - pubkey: LIGHTNING_PUB_PUBKEY, - relay: RELAY_URL, - pointer: POINTER, -}) - -console.log('=== NDEBIT for ShockWallet ===') -console.log('') -console.log('Pubkey:', LIGHTNING_PUB_PUBKEY) -console.log('Relay:', RELAY_URL) -console.log('Pointer:', POINTER) -console.log('') -console.log('ndebit string:') -console.log(ndebit) -console.log('') -console.log('ShockWallet should scan this QR code to receive sats from the ATM.') diff --git a/packages/nostr-client/dev/mock-machine.mjs b/packages/nostr-client/dev/mock-machine.mjs deleted file mode 100644 index 152e30b..0000000 --- a/packages/nostr-client/dev/mock-machine.mjs +++ /dev/null @@ -1,833 +0,0 @@ -#!/usr/bin/env node -/** - * Mock ATM Machine - Simulates the ATM cash-in flow with CLINK - * - * Usage: node mock-machine.mjs - * - * This creates a web server that: - * 1. Displays the ndebit QR code for customers to scan - * 2. Runs the ATM debit agent to authorize payments - * 3. Shows real-time status updates - */ - -import http from 'http' -import { WebSocketServer } from 'ws' -import { Relay } from 'nostr-tools/relay' -import { finalizeEvent, generateSecretKey, getPublicKey } from 'nostr-tools' -import { getConversationKey, encrypt, decrypt } from './nip44v1.mjs' -import { randomUUID } from 'crypto' -import QRCode from 'qrcode' -import { decodeBech32, ndebitEncode } from '@shocknet/clink-sdk' - -const PORT = 3456 -const RELAY_URL = 'ws://localhost:7777' -// Relay URL for browser access (different from Docker internal strfry:7777) -const BROWSER_RELAY_URL = 'ws://localhost:7777' -const LIGHTNING_PUB_HTTP = 'http://localhost:1776' -const ADMIN_TOKEN = 'bitspire-dev-admin-token' - -// Lightning.Pub pubkey - fetched dynamically from the ATM user's ndebit -let LIGHTNING_PUB_PUBKEY = null - -// ATM keypair (persistent for this session) -const ATM_PRIVATE_KEY = generateSecretKey() -const ATM_PUBLIC_KEY = getPublicKey(ATM_PRIVATE_KEY) - -// Fake exchange rate: sats per USD (approximately $100k/BTC) -const SATS_PER_USD = 1000 - -// ATM states -const ATM_STATE = { - IDLE: 'idle', - CASH_INSERTED: 'cash_inserted', - WAITING_FOR_SCAN: 'waiting_for_scan', - PROCESSING: 'processing', - COMPLETE: 'complete', -} - -// State -let relay = null -let appToken = null -let ndebit = null -let ndebitQR = null -let atmBalance = 0 -let wsClients = [] -let isLinked = false -let withdrawAmount = 0 // Amount in sats (calculated from cash) -let cashInserted = 0 // Amount in USD -let atmState = ATM_STATE.IDLE - -// Rewrite ndebit relay for browser access (strfry:7777 -> localhost:7777) -function rewriteNdebitRelay(ndebitString) { - try { - const decoded = decodeBech32(ndebitString) - if (decoded.type !== 'ndebit') return ndebitString - - // Replace Docker internal relay with browser-accessible relay - const data = { - pubkey: decoded.data.pubkey, - relay: BROWSER_RELAY_URL, - pointer: decoded.data.pointer, - } - return ndebitEncode(data) - } catch (e) { - console.error('Failed to rewrite ndebit relay:', e) - return ndebitString - } -} - -// Format ndebit with clink: prefix and amount parameter -// Using clink: instead of lightning: because CLINK is protocol-agnostic -// (could work with Cashu/Fedimint, not just Lightning) -function formatNdebitUri(ndebitString, amount) { - const rewritten = rewriteNdebitRelay(ndebitString) - return `clink:${rewritten}?amount=${amount}` -} - -function broadcast(type, data) { - const msg = JSON.stringify({ type, ...data }) - wsClients.forEach((ws) => { - if (ws.readyState === 1) ws.send(msg) - }) -} - -function log(message) { - const timestamp = new Date().toLocaleTimeString() - console.log(`[${timestamp}] ${message}`) - broadcast('log', { message: `[${timestamp}] ${message}` }) -} - -async function getAppToken() { - const res = await fetch(`${LIGHTNING_PUB_HTTP}/api/admin/app/auth`, { - method: 'POST', - headers: { - Authorization: `Bearer ${ADMIN_TOKEN}`, - 'Content-Type': 'application/json', - }, - body: JSON.stringify({ name: 'wallet' }), - }) - const data = await res.json() - return data.auth_token -} - -async function getAtmUser() { - const res = await fetch(`${LIGHTNING_PUB_HTTP}/api/app/user/get`, { - method: 'POST', - headers: { - Authorization: `Bearer ${appToken}`, - 'Content-Type': 'application/json', - }, - body: JSON.stringify({ user_identifier: 'atm' }), - }) - return res.json() -} - -async function getLinkingToken() { - const res = await fetch(`${LIGHTNING_PUB_HTTP}/api/app/user/npub/token/reset`, { - method: 'POST', - headers: { - Authorization: `Bearer ${appToken}`, - 'Content-Type': 'application/json', - }, - body: JSON.stringify({ user_identifier: 'atm' }), - }) - const data = await res.json() - return data.token -} - -async function sendRPC(rpcName, body) { - const requestId = randomUUID() - const request = { - rpcName, - authIdentifier: ATM_PUBLIC_KEY, - body, - } - - const conversationKey = getConversationKey(ATM_PRIVATE_KEY, LIGHTNING_PUB_PUBKEY) - const encryptedContent = encrypt(JSON.stringify(request), conversationKey) - - const event = finalizeEvent( - { - kind: 21000, - created_at: Math.floor(Date.now() / 1000), - tags: [['p', LIGHTNING_PUB_PUBKEY]], - content: encryptedContent, - }, - ATM_PRIVATE_KEY - ) - - return new Promise((resolve, reject) => { - const timeout = setTimeout(() => reject(new Error('RPC timeout')), 30000) - - const sub = relay.subscribe( - [ - { - kinds: [21000], - authors: [LIGHTNING_PUB_PUBKEY], - since: Math.floor(Date.now() / 1000) - 5, - }, - ], - { - onevent(evt) { - const pTags = evt.tags.filter((t) => t[0] === 'p') - if (!pTags.some((t) => t[1] === ATM_PUBLIC_KEY)) return - - try { - const response = JSON.parse(decrypt(evt.content, conversationKey)) - clearTimeout(timeout) - sub.close() - resolve(response) - } catch (e) {} - }, - } - ) - - relay.publish(event) - }) -} - -async function linkKeypair(token) { - log('Linking ATM keypair to user account...') - const response = await sendRPC('LinkNPubThroughToken', { token }) - if (response.status === 'OK') { - log('[OK] Keypair linked successfully!') - isLinked = true - return true - } else { - log(`[ERROR] Link failed: ${response.reason}`) - return false - } -} - -async function subscribeToDebitRequests() { - log('Subscribing to debit requests...') - - const conversationKey = getConversationKey(ATM_PRIVATE_KEY, LIGHTNING_PUB_PUBKEY) - - // Subscribe to Kind 21000 messages from Lightning.Pub - relay.subscribe( - [ - { - kinds: [21000], - authors: [LIGHTNING_PUB_PUBKEY], - since: Math.floor(Date.now() / 1000) - 5, - }, - ], - { - onevent(evt) { - const pTags = evt.tags.filter((t) => t[0] === 'p') - if (!pTags.some((t) => t[1] === ATM_PUBLIC_KEY)) return - - try { - const message = JSON.parse(decrypt(evt.content, conversationKey)) - - if (message.debit) { - handleDebitRequest(message, conversationKey) - } - } catch (e) {} - }, - } - ) - - // Send GetLiveDebitRequests to start the stream - const request = { - rpcName: 'GetLiveDebitRequests', - authIdentifier: ATM_PUBLIC_KEY, - body: {}, - } - - const encryptedContent = encrypt(JSON.stringify(request), conversationKey) - const event = finalizeEvent( - { - kind: 21000, - created_at: Math.floor(Date.now() / 1000), - tags: [['p', LIGHTNING_PUB_PUBKEY]], - content: encryptedContent, - }, - ATM_PRIVATE_KEY - ) - - await relay.publish(event) - log('[OK] Listening for debit requests...') -} - -async function handleDebitRequest(message, conversationKey) { - const { debit } = message - atmState = ATM_STATE.PROCESSING - broadcastState() - - log(`[ALERT] DEBIT REQUEST RECEIVED!`) - log(` Amount: ${debit.amount || 'invoice amount'} sats`) - log(` Type: ${debit.type}`) - - broadcast('debit_request', { debit }) - - // Auto-approve after 1 second - setTimeout(async () => { - log('[OK] Auto-approving debit request...') - - const approval = { - rpcName: 'RespondToDebit', - authIdentifier: ATM_PUBLIC_KEY, - body: { - npub: message.npub, - request_id: message.request_id, - response: { - type: 'invoice', - invoice: debit.invoice, - }, - }, - } - - const encryptedContent = encrypt(JSON.stringify(approval), conversationKey) - const event = finalizeEvent( - { - kind: 21000, - created_at: Math.floor(Date.now() / 1000), - tags: [['p', LIGHTNING_PUB_PUBKEY]], - content: encryptedContent, - }, - ATM_PRIVATE_KEY - ) - - await relay.publish(event) - log('[OK] Approval sent! Payment complete.') - - // Mark as complete - atmState = ATM_STATE.COMPLETE - broadcastState() - - // Update balance and reset after delay - setTimeout(async () => { - await updateBalance() - // Auto-reset after showing success - setTimeout(resetAtm, 3000) - }, 2000) - }, 1000) -} - -async function updateBalance() { - const user = await getAtmUser() - if (user.status === 'OK') { - atmBalance = user.info.balance - ndebit = user.info.ndebit - broadcastState() - log(`Balance updated: ${atmBalance} sats`) - } -} - -async function regenerateQR() { - if (ndebit) { - const uri = formatNdebitUri(ndebit, withdrawAmount) - ndebitQR = await QRCode.toDataURL(uri, { width: 300, margin: 2 }) - log(`QR code generated for ${withdrawAmount} sats`) - } -} - -function setWithdrawAmount(amount) { - withdrawAmount = amount - regenerateQR() - broadcastState() - log(`Withdrawal amount set to ${amount} sats`) -} - -async function insertCash(usdAmount) { - cashInserted = usdAmount - withdrawAmount = usdAmount * SATS_PER_USD - atmState = ATM_STATE.CASH_INSERTED - log(`[CASH] $${usdAmount} inserted → ${withdrawAmount.toLocaleString()} sats`) - - // Brief delay then show QR - await new Promise((r) => setTimeout(r, 500)) - atmState = ATM_STATE.WAITING_FOR_SCAN - await regenerateQR() - broadcastState() - log(`[QR] Scan to receive ${withdrawAmount.toLocaleString()} sats`) -} - -function resetAtm() { - atmState = ATM_STATE.IDLE - cashInserted = 0 - withdrawAmount = 0 - ndebitQR = null - broadcastState() - log('[RESET] ATM ready for next customer') -} - -function broadcastState() { - broadcast('status', { - balance: atmBalance, - ndebit, - ndebitQR, - ndebitUri: withdrawAmount > 0 ? formatNdebitUri(ndebit, withdrawAmount) : null, - withdrawAmount, - cashInserted, - atmState, - satsPerUsd: SATS_PER_USD, - }) -} - -async function initialize() { - log('Starting Mock ATM Machine...') - - // Get app token - appToken = await getAppToken() - log('Got app token') - - // Get ATM user info - const user = await getAtmUser() - if (user.status === 'OK') { - atmBalance = user.info.balance - ndebit = user.info.ndebit - - // Extract Lightning.Pub pubkey from ndebit - const decoded = decodeBech32(ndebit) - LIGHTNING_PUB_PUBKEY = decoded.data.pubkey - log(`ATM user found: ${atmBalance} sats balance`) - log(`Lightning.Pub pubkey: ${LIGHTNING_PUB_PUBKEY.substring(0, 16)}...`) - } else { - log('ATM user not found - please create one first') - return - } - - // Connect to relay - log('Connecting to relay...') - relay = await Relay.connect(RELAY_URL) - log('Connected to relay') - - // Get linking token and link keypair - const token = await getLinkingToken() - await linkKeypair(token) - - // Subscribe to debit requests - await subscribeToDebitRequests() - - // Start in IDLE state (no QR until cash inserted) - atmState = ATM_STATE.IDLE - broadcastState() - - log('[READY] Mock ATM Machine ready!') - log(` Open http://localhost:${PORT} to use the ATM`) -} - -// HTML page -const html = ` - - - Mock ATM Machine - - - -
-

Mock ATM

-

Simulated Bitcoin ATM for testing

- -
- ATM Balance: - 0 sats -
- -
-
- -
-
Insert Cash
-
Select amount to withdraw as Bitcoin
-
- - - - -
-

Rate: 1,000 sats/$

-
- - - - - - - - - -
-
- -
-

Activity Log

-
-
-
- - - -` - -// Create HTTP server -const server = http.createServer((req, res) => { - res.writeHead(200, { 'Content-Type': 'text/html' }) - res.end(html) -}) - -// Create WebSocket server -const wss = new WebSocketServer({ server }) - -wss.on('connection', (ws) => { - wsClients.push(ws) - ws.on('close', () => { - wsClients = wsClients.filter((c) => c !== ws) - }) - - ws.on('message', (data) => { - try { - const msg = JSON.parse(data) - if (msg.type === 'insert_cash' && typeof msg.amount === 'number') { - insertCash(msg.amount) - } else if (msg.type === 'reset') { - resetAtm() - } - } catch (e) {} - }) - - // Send current state - if (ndebit) { - ws.send( - JSON.stringify({ - type: 'status', - balance: atmBalance, - ndebit, - ndebitQR, - ndebitUri: withdrawAmount > 0 ? formatNdebitUri(ndebit, withdrawAmount) : null, - withdrawAmount, - cashInserted, - atmState, - satsPerUsd: SATS_PER_USD, - isLinked, - }) - ) - } -}) - -// Start server -server.listen(PORT, async () => { - console.log(`Mock ATM Machine running at http://localhost:${PORT}`) - await initialize() -}) diff --git a/packages/nostr-client/dev/nip44v1.mjs b/packages/nostr-client/dev/nip44v1.mjs deleted file mode 100644 index da908d5..0000000 --- a/packages/nostr-client/dev/nip44v1.mjs +++ /dev/null @@ -1,111 +0,0 @@ -/** - * NIP-44 v1 Implementation - * - * This is the XChaCha20-based encryption used by Lightning.Pub for Kind 21000 RPC events. - * It differs from standard NIP-44 v2 (used in nostr-tools) which uses ChaCha20-Poly1305. - */ - -import { base64 } from '@scure/base' -import { randomBytes } from '@noble/hashes/utils.js' -import { streamXOR as xchacha20 } from '@stablelib/xchacha20' -import { secp256k1 } from '@noble/curves/secp256k1.js' -import { sha256 } from '@noble/hashes/sha2.js' - -const XCHACHA20_VERSION = 1 - -/** - * Convert hex string to Uint8Array - * @param {string} hex - Hex string - * @returns {Uint8Array} - */ -function hexToBytes(hex) { - const bytes = new Uint8Array(hex.length / 2) - for (let i = 0; i < hex.length; i += 2) { - bytes[i / 2] = parseInt(hex.substring(i, i + 2), 16) - } - return bytes -} - -/** - * Get shared secret for NIP-44 v1 encryption - * @param {Uint8Array|string} privateKey - Private key (32 bytes or hex string) - * @param {string} publicKey - Public key (32 bytes hex string, no prefix) - * @returns {Uint8Array} - 32-byte shared secret - */ -export function getConversationKey(privateKey, publicKey) { - // Convert private key to Uint8Array if it's a hex string - const privKeyBytes = typeof privateKey === 'string' ? hexToBytes(privateKey) : privateKey - - // Convert public key (with 02 prefix) to Uint8Array - const pubKeyBytes = hexToBytes('02' + publicKey) - - // Get ECDH shared point - const sharedPoint = secp256k1.getSharedSecret(privKeyBytes, pubKeyBytes) - - // Hash the x-coordinate of the shared point - return sha256(sharedPoint.slice(1, 33)) -} - -/** - * Encrypt content using NIP-44 v1 (XChaCha20) - * @param {string} content - Plaintext content to encrypt - * @param {Uint8Array} conversationKey - 32-byte conversation key from getConversationKey - * @returns {string} - Base64-encoded encrypted payload - */ -export function encrypt(content, conversationKey) { - const nonce = randomBytes(24) - const plaintext = new TextEncoder().encode(content) - - // XChaCha20 stream cipher - encrypts in place - const ciphertext = new Uint8Array(plaintext.length) - xchacha20(conversationKey, nonce, plaintext, ciphertext) - - // Encode: version byte + nonce + ciphertext - return base64.encode(new Uint8Array([XCHACHA20_VERSION, ...nonce, ...ciphertext])) -} - -/** - * Decrypt content using NIP-44 v1 (XChaCha20) - * @param {string} content - Base64-encoded encrypted payload - * @param {Uint8Array} conversationKey - 32-byte conversation key from getConversationKey - * @returns {string} - Decrypted plaintext - */ -export function decrypt(content, conversationKey) { - const payload = decodePayload(content) - - // XChaCha20 stream cipher - decrypts in place - const plaintext = new Uint8Array(payload.ciphertext.length) - xchacha20(conversationKey, payload.nonce, payload.ciphertext, plaintext) - - return new TextDecoder().decode(plaintext) -} - -/** - * Decode encrypted payload (supports both formats) - * @param {string} content - Base64-encoded or JSON-encoded payload - * @returns {{nonce: Uint8Array, ciphertext: Uint8Array}} - */ -function decodePayload(content) { - // Check for JSON format - if (content.startsWith('{') && content.endsWith('}')) { - const parsed = JSON.parse(content) - if (parsed.v !== XCHACHA20_VERSION) { - throw new Error(`Unsupported encryption version: ${parsed.v}`) - } - return { - nonce: base64.decode(parsed.nonce), - ciphertext: base64.decode(parsed.ciphertext), - } - } - - // Binary format: version byte + nonce (24 bytes) + ciphertext - const buf = base64.decode(content) - if (buf[0] !== XCHACHA20_VERSION) { - throw new Error(`Unsupported encryption version: ${buf[0]}`) - } - - return { - nonce: buf.subarray(1, 25), - ciphertext: buf.subarray(25), - } -} diff --git a/packages/nostr-client/dev/run-debit-agent.mjs b/packages/nostr-client/dev/run-debit-agent.mjs deleted file mode 100644 index 8dd87c9..0000000 --- a/packages/nostr-client/dev/run-debit-agent.mjs +++ /dev/null @@ -1,221 +0,0 @@ -#!/usr/bin/env node -/** - * ATM Debit Approval Agent (TESTING ONLY) - * - * ⚠️ WARNING: This script auto-approves ALL debit requests without validation! - * ⚠️ DO NOT use in production - use the integrated debit approval service instead. - * - * Purpose: - * - Standalone debugging tool for testing the GetLiveDebitRequests subscription - * - Helps diagnose relay connectivity and message decryption issues - * - Useful when the integrated service isn't receiving events - * - * Usage: - * # Set environment variables (or create .env file in apps/machine/) - * export ATM_PRIVATE_KEY= - * export LIGHTNING_PUB_PUBKEY= - * export RELAY_URL=ws://localhost:7777 - * - * # Run the script - * node run-debit-agent.mjs - * - * Production alternative: - * The ATM app (apps/machine) includes an integrated debit approval service - * with session-based single-use protection. See: - * - apps/machine/src/services/lightning.ts (startDebitApprovalService) - * - docs/ndebit-cash-in-flow.md - */ - -import { Relay } from 'nostr-tools/relay' -import { finalizeEvent, getPublicKey } from 'nostr-tools' -import * as nip44v1 from './nip44v1.mjs' -import fs from 'node:fs' -import path from 'node:path' -import { fileURLToPath } from 'node:url' - -// Load .env file from apps/machine if it exists -const __dirname = path.dirname(fileURLToPath(import.meta.url)) -const envPath = path.join(__dirname, '../../apps/machine/.env') -if (fs.existsSync(envPath)) { - const envContent = fs.readFileSync(envPath, 'utf-8') - for (const line of envContent.split('\n')) { - const trimmed = line.trim() - if (trimmed && !trimmed.startsWith('#')) { - const [key, ...valueParts] = trimmed.split('=') - if (key && valueParts.length > 0) { - // Map VITE_ prefixed vars to non-prefixed - const envKey = key.replace(/^VITE_/, '') - process.env[envKey] = valueParts.join('=') - } - } - } - console.log('[Config] Loaded .env from:', envPath) -} - -// Configuration from environment -const ATM_PRIVATE_KEY_HEX = process.env.ATM_PRIVATE_KEY -const LIGHTNING_PUB_PUBKEY = process.env.LIGHTNING_PUB_PUBKEY -const RELAY_URL = process.env.RELAY_URL || 'ws://localhost:7777' - -// Validate required config -if (!ATM_PRIVATE_KEY_HEX) { - console.error('ERROR: ATM_PRIVATE_KEY environment variable is required') - console.error('Set it directly or create apps/machine/.env with VITE_ATM_PRIVATE_KEY') - process.exit(1) -} - -if (!LIGHTNING_PUB_PUBKEY) { - console.error('ERROR: LIGHTNING_PUB_PUBKEY environment variable is required') - console.error('Set it directly or create apps/machine/.env with VITE_LIGHTNING_PUB_PUBKEY') - process.exit(1) -} - -const ATM_PRIVATE_KEY = Uint8Array.from(Buffer.from(ATM_PRIVATE_KEY_HEX, 'hex')) -const ATM_PUBLIC_KEY = getPublicKey(ATM_PRIVATE_KEY) - -console.log('') -console.log('='.repeat(60)) -console.log(' ATM Debit Approval Agent (TESTING ONLY)') -console.log('='.repeat(60)) -console.log('') -console.log('⚠️ WARNING: Auto-approves ALL requests without validation!') -console.log('⚠️ For production, use the integrated service in apps/machine') -console.log('') -console.log('ATM Pubkey:', ATM_PUBLIC_KEY) -console.log('Lightning.Pub Pubkey:', LIGHTNING_PUB_PUBKEY) -console.log('Relay URL:', RELAY_URL) -console.log('') - -async function main() { - // Connect to relay - console.log('Connecting to relay...') - const relay = await Relay.connect(RELAY_URL) - console.log('Connected!') - console.log('') - - // Create conversation key for NIP-44 v1 encryption - const conversationKey = nip44v1.getConversationKey(ATM_PRIVATE_KEY_HEX, LIGHTNING_PUB_PUBKEY) - - // Subscribe to GetLiveDebitRequests - console.log('Subscribing to live debit requests...') - - const subscribeRequest = { - rpcName: 'GetLiveDebitRequests', - authIdentifier: ATM_PUBLIC_KEY, - body: {}, - } - - const subEvent = finalizeEvent( - { - kind: 21000, - created_at: Math.floor(Date.now() / 1000), - tags: [['p', LIGHTNING_PUB_PUBKEY]], - content: nip44v1.encrypt(JSON.stringify(subscribeRequest), conversationKey), - }, - ATM_PRIVATE_KEY - ) - - // Listen for debit requests - console.log('Listening for debit requests...') - console.log('(Test by scanning ndebit QR with ShockWallet)') - console.log('') - - const debitSub = relay.subscribe( - [ - { - kinds: [21000], - authors: [LIGHTNING_PUB_PUBKEY], - '#p': [ATM_PUBLIC_KEY], - since: Math.floor(Date.now() / 1000) - 5, - }, - ], - { - async onevent(evt) { - try { - const decrypted = nip44v1.decrypt(evt.content, conversationKey) - const message = JSON.parse(decrypted) - - // Check if this is a live debit request - if (message.requestId === 'GetLiveDebitRequests' && message.debit) { - console.log('') - console.log('========================================') - console.log('DEBIT REQUEST RECEIVED!') - console.log(' Request ID:', message.request_id) - console.log(' From npub:', message.npub?.substring(0, 16) + '...') - console.log(' Debit type:', message.debit.type) - - if (message.debit.invoice) { - console.log(' Invoice:', message.debit.invoice.substring(0, 50) + '...') - - // Auto-approve by responding with INVOICE type - console.log('') - console.log('⚠️ AUTO-APPROVING debit request (NO VALIDATION)...') - - const approveRequest = { - rpcName: 'RespondToDebit', - authIdentifier: ATM_PUBLIC_KEY, - body: { - npub: message.npub, - request_id: message.request_id, - response: { - type: 'invoice', - invoice: message.debit.invoice, - }, - }, - } - - const approveEvent = finalizeEvent( - { - kind: 21000, - created_at: Math.floor(Date.now() / 1000), - tags: [['p', LIGHTNING_PUB_PUBKEY]], - content: nip44v1.encrypt(JSON.stringify(approveRequest), conversationKey), - }, - ATM_PRIVATE_KEY - ) - - await relay.publish(approveEvent) - console.log('APPROVED! (event id:', approveEvent.id.substring(0, 16) + '...)') - } - - console.log('========================================') - console.log('') - } else if (message.rpcName) { - console.log('RPC response:', message.rpcName, ':', message.status || 'received') - } else if (message.requestId) { - console.log('Live subscription active:', message.status) - } - } catch (err) { - // Ignore decryption failures for events not meant for us - if (!err.message?.includes('Unsupported')) { - // Uncomment for debugging: - // console.log('Decryption error:', err.message) - } - } - }, - } - ) - - await relay.publish(subEvent) - console.log('Subscription sent, waiting for debit requests...') - console.log('') - - // Keep running - process.on('SIGINT', () => { - console.log('\nShutting down...') - debitSub.close() - relay.close() - process.exit(0) - }) - - // Heartbeat - while (true) { - await new Promise((r) => setTimeout(r, 30000)) - console.log('Still listening...') - } -} - -main().catch((err) => { - console.error('Error:', err.message) - process.exit(1) -}) diff --git a/packages/nostr-client/dev/test-debit.mjs b/packages/nostr-client/dev/test-debit.mjs deleted file mode 100644 index c1861ac..0000000 --- a/packages/nostr-client/dev/test-debit.mjs +++ /dev/null @@ -1,182 +0,0 @@ -/** - * Test CLINK Debit flow - * - * This simulates what ShockWallet does when scanning an ndebit: - * 1. Decode the ndebit to get pubkey, relay, pointer - * 2. Create a bolt11 invoice (we'll get one from Alice) - * 3. Send Kind 21002 debit request to Lightning.Pub - * 4. Wait for payment response - */ - -import { Relay } from 'nostr-tools/relay' -import { finalizeEvent, getPublicKey } from 'nostr-tools' -import { nip44 } from 'nostr-tools' -import { bech32 } from '@scure/base' -import { randomBytes } from 'crypto' - -// Generate a random keypair for this test (simulates ShockWallet) -const WALLET_PRIVATE_KEY = randomBytes(32) -const WALLET_PUBLIC_KEY = getPublicKey(WALLET_PRIVATE_KEY) - -// The ndebit to test -const NDEBIT = process.argv[2] -// The bolt11 invoice to be paid -const BOLT11 = process.argv[3] - -if (!NDEBIT || !BOLT11) { - console.log('Usage: node test-debit.mjs ') - console.log('') - console.log('Example:') - console.log(' # First create an invoice on Alice:') - console.log(' docker exec bitspire-lnd-alice lncli --network=regtest addinvoice --amt 1000') - console.log('') - console.log(' # Then test the debit:') - console.log(' node test-debit.mjs ndebit1... lnbcrt...') - process.exit(1) -} - -function decodeNdebit(ndebit) { - const { prefix, words } = bech32.decode(ndebit, 5000) - if (prefix !== 'ndebit') throw new Error('Invalid ndebit prefix') - - const data = new Uint8Array(bech32.fromWords(words)) - - let pubkey, relay, pointer - let offset = 0 - - while (offset < data.length) { - const type = data[offset] - const length = data[offset + 1] - const value = data.slice(offset + 2, offset + 2 + length) - - switch (type) { - case 0: - pubkey = Buffer.from(value).toString('hex') - break - case 1: - relay = new TextDecoder().decode(value) - break - case 2: - pointer = new TextDecoder().decode(value) - break - } - - offset += 2 + length - } - - return { pubkey, relay, pointer } -} - -async function main() { - console.log('=== CLINK Debit Test ===') - console.log('') - console.log('Test wallet pubkey:', WALLET_PUBLIC_KEY) - console.log('') - - // Decode ndebit - const debit = decodeNdebit(NDEBIT) - console.log('Decoded ndebit:') - console.log(' Pubkey:', debit.pubkey) - console.log(' Relay:', debit.relay) - console.log(' Pointer:', debit.pointer || '(none)') - console.log('') - - // Connect to relay (override Docker internal hostnames with localhost for local testing) - const relayUrl = debit.relay - .replace('host.docker.internal', 'localhost') - .replace('ws://strfry:', 'ws://localhost:') - console.log('Connecting to relay:', relayUrl) - const relay = await Relay.connect(relayUrl) - console.log('Connected!') - console.log('') - - // Build debit request payload - const requestPayload = { - pointer: debit.pointer, - bolt11: BOLT11, - } - - console.log('Request payload:', JSON.stringify(requestPayload, null, 2)) - console.log('') - - // Encrypt with NIP-44 - const conversationKey = nip44.getConversationKey(WALLET_PRIVATE_KEY, debit.pubkey) - const encryptedContent = nip44.encrypt(JSON.stringify(requestPayload), conversationKey) - - // Create Kind 21002 event - const event = finalizeEvent( - { - kind: 21002, - created_at: Math.floor(Date.now() / 1000), - tags: [ - ['p', debit.pubkey], - ['clink_version', '1'], - ], - content: encryptedContent, - }, - WALLET_PRIVATE_KEY - ) - - console.log('Publishing debit request (event id:', event.id.substring(0, 16) + '...)...') - - // Subscribe to responses - let responseReceived = false - const sub = relay.subscribe( - [ - { - kinds: [21002], - authors: [debit.pubkey], - '#p': [WALLET_PUBLIC_KEY], - '#e': [event.id], - since: Math.floor(Date.now() / 1000) - 5, - }, - ], - { - onevent(evt) { - console.log('') - console.log('Got response event:', evt.id.substring(0, 16) + '...') - try { - const decrypted = nip44.decrypt(evt.content, conversationKey) - const response = JSON.parse(decrypted) - console.log('Response:', JSON.stringify(response, null, 2)) - - if (response.res === 'ok') { - console.log('') - console.log('✅ DEBIT SUCCESS!') - if (response.preimage) { - console.log('Preimage:', response.preimage) - } - } else if (response.res === 'GFY') { - console.log('') - console.log('❌ DEBIT FAILED:', response.error) - } - - responseReceived = true - } catch (err) { - console.log('Failed to decrypt:', err.message) - } - }, - } - ) - - // Publish request - await relay.publish(event) - console.log('Request published, waiting for response...') - - // Wait for response - for (let i = 0; i < 30; i++) { - await new Promise((r) => setTimeout(r, 1000)) - if (responseReceived) break - if (i % 5 === 4) console.log('Still waiting... (' + (i + 1) + 's)') - } - - if (!responseReceived) { - console.log('') - console.log('❌ No response received within timeout') - } - - sub.close() - relay.close() -} - -main().catch(console.error) diff --git a/packages/nostr-client/dev/test-ndebit.mjs b/packages/nostr-client/dev/test-ndebit.mjs deleted file mode 100644 index bcddaf4..0000000 --- a/packages/nostr-client/dev/test-ndebit.mjs +++ /dev/null @@ -1,181 +0,0 @@ -#!/usr/bin/env node -/** - * Test script to simulate a wallet sending an ndebit claim request - * This tests whether Lightning.Pub sends Kind 21002 responses after the fix - */ - -import { Relay } from 'nostr-tools/relay' -import { nip44, finalizeEvent, generateSecretKey, getPublicKey } from 'nostr-tools' -import { decodeBech32 } from '@shocknet/clink-sdk' - -const { getConversationKey, encrypt, decrypt } = nip44 - -const NDEBIT = - 'ndebit1qgpkzardqyg8wue69uhhxarjvee8jw3hxumnwqpqf05wyqarxsdm9d62fh9lsa6wqc2r0a37cgd00mp3gnydpf5w9uusavytcn' -const RELAY_URL = 'ws://localhost:7777' -const AMOUNT_SATS = 5000 // Small test amount - -// Generate a wallet keypair for this test -const WALLET_PRIVATE_KEY = generateSecretKey() -const WALLET_PUBLIC_KEY = getPublicKey(WALLET_PRIVATE_KEY) - -async function main() { - console.log('🔧 Test: ndebit claim flow (NIP-44 v2)') - console.log('='.repeat(50)) - - // Decode ndebit to get Lightning.Pub pubkey and pointer - const decoded = decodeBech32(NDEBIT) - const LPUB_PUBKEY = decoded.data.pubkey - const POINTER = decoded.data.pointer - console.log(`\n📍 Lightning.Pub pubkey: ${LPUB_PUBKEY.slice(0, 16)}...`) - console.log(`🔑 Pointer (user ID): ${POINTER.slice(0, 16)}...`) - console.log(`👛 Test wallet pubkey: ${WALLET_PUBLIC_KEY.slice(0, 16)}...`) - console.log(`💰 Amount: ${AMOUNT_SATS} sats`) - - // Connect to relay - console.log(`\n🔌 Connecting to relay: ${RELAY_URL}`) - const relay = await Relay.connect(RELAY_URL) - console.log('✅ Connected!') - - // Build the debit request data (NdebitData format) - // Using newNdebitFullAccessRequest format with amount - const debitData = { - amount_sats: AMOUNT_SATS, - pointer: POINTER, - } - - // Encrypt using NIP-44 v2 - const conversationKey = getConversationKey(WALLET_PRIVATE_KEY, LPUB_PUBKEY) - const encryptedContent = encrypt(JSON.stringify(debitData), conversationKey) - - // Build event with correct tags (including clink_version) - const event = finalizeEvent( - { - kind: 21002, - created_at: Math.floor(Date.now() / 1000), - tags: [ - ['p', LPUB_PUBKEY], - ['clink_version', '1'], - ], - content: encryptedContent, - }, - WALLET_PRIVATE_KEY - ) - - console.log(`\n📤 Sending Kind 21002 debit request`) - console.log(` Event ID: ${event.id.slice(0, 16)}...`) - console.log(` Content length: ${encryptedContent.length} chars`) - - // Subscribe for responses BEFORE sending the request - let responseReceived = false - const startTime = Date.now() - - // Filter for Kind 21002 responses from Lightning.Pub that reference our event - const sub = relay.subscribe( - [ - { - kinds: [21002], - authors: [LPUB_PUBKEY], - '#p': [WALLET_PUBLIC_KEY], - '#e': [event.id], - since: Math.floor(Date.now() / 1000) - 5, - }, - ], - { - onevent(evt) { - console.log(`\n📥 Received Kind 21002 response!`) - console.log(` Event ID: ${evt.id.slice(0, 16)}...`) - console.log(` Author: ${evt.pubkey.slice(0, 16)}...`) - - // Check #e tag (should reference our original event) - const eTag = evt.tags.find((t) => t[0] === 'e') - if (eTag) { - console.log(` #e tag: ${eTag[1].slice(0, 16)}...`) - if (eTag[1] === event.id) { - console.log(' ✅ Correctly references our original event!') - } - } else { - console.log(' ⚠️ No #e tag found') - } - - try { - const response = JSON.parse(decrypt(evt.content, conversationKey)) - console.log(`\n📋 Response content:`) - console.log(JSON.stringify(response, null, 2)) - - if (response.res === 'OK') { - console.log('\n✅✅✅ SUCCESS! Lightning.Pub sent Kind 21002 response correctly!') - console.log(' The fix is working!') - } else if (response.res === 'GFY' || response.error) { - console.log(`\n⚠️ Response indicates error: ${response.error || 'unknown'}`) - console.log(' (Expected if payment denied or auth required)') - } - } catch (e) { - console.log(' ❌ Could not decrypt response:', e.message) - } - - responseReceived = true - }, - } - ) - - // Publish the debit request - await relay.publish(event) - console.log('✅ Request published!') - - // Wait for response with timeout - console.log('\n⏳ Waiting for Kind 21002 response (30s timeout)...') - const timeout = 30000 - const checkInterval = 1000 - - while (!responseReceived && Date.now() - startTime < timeout) { - await new Promise((r) => setTimeout(r, checkInterval)) - const elapsed = Math.floor((Date.now() - startTime) / 1000) - process.stdout.write(`\r ${elapsed}s elapsed...`) - } - - console.log('') - - if (!responseReceived) { - console.log('\n❌❌❌ TIMEOUT! No Kind 21002 response received.') - console.log(' This means the fix did NOT work or there was another issue.') - - // Let's check what Kind 21002 events exist - console.log('\n🔍 Checking for any Kind 21002 events on relay...') - let foundEvents = 0 - const allDebitSub = relay.subscribe( - [ - { - kinds: [21002], - limit: 10, - }, - ], - { - onevent(evt) { - foundEvents++ - const pTags = evt.tags.filter((t) => t[0] === 'p').map((t) => t[1].slice(0, 8) + '...') - const eTags = evt.tags.filter((t) => t[0] === 'e').map((t) => t[1].slice(0, 8) + '...') - console.log( - ` ${foundEvents}. id=${evt.id.slice(0, 12)}... by=${evt.pubkey.slice(0, 8)}... #p=${pTags.join(',')} #e=${eTags.join(',')}` - ) - }, - oneose() { - console.log(` (Found ${foundEvents} Kind 21002 events total)`) - }, - } - ) - - await new Promise((r) => setTimeout(r, 3000)) - allDebitSub.close() - } - - sub.close() - relay.close() - console.log('\n🏁 Test complete') - process.exit(responseReceived ? 0 : 1) -} - -main().catch((e) => { - console.error('Fatal error:', e) - process.exit(1) -}) diff --git a/packages/nostr-client/dev/test-pay.mjs b/packages/nostr-client/dev/test-pay.mjs deleted file mode 100644 index 4385322..0000000 --- a/packages/nostr-client/dev/test-pay.mjs +++ /dev/null @@ -1,116 +0,0 @@ -import { NostrClient, loadIdentityFromHex, encryptContent, decryptJSON } from '../dist/index.js' -import { Relay } from 'nostr-tools/relay' -import { finalizeEvent } from 'nostr-tools' -import { randomUUID } from 'crypto' - -const DEV_PRIVATE_KEY = '0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef' -const LIGHTNING_PUB_PUBKEY = - process.env.LIGHTNING_PUB_PUBKEY || - '4a72e400a254bf74a70cc711ab97e461b8d4fd9738b1ac3b5194cdeb3192ab91' -const RELAY_URL = process.env.NOSTR_RELAY_URL || 'ws://localhost:7777' - -// Get a fresh invoice from Alice first: -// docker exec bitspire-lnd-alice lncli --network=regtest addinvoice --amt 1000 -const TEST_INVOICE = process.argv[2] - -if (!TEST_INVOICE) { - console.log('Usage: node test-pay.mjs ') - console.log( - 'Generate invoice: docker exec bitspire-lnd-alice lncli --network=regtest addinvoice --amt 1000' - ) - process.exit(1) -} - -async function main() { - const identity = loadIdentityFromHex(DEV_PRIVATE_KEY) - console.log('Using identity:', identity.publicKey) - - console.log('Connecting to relay...') - const relay = await Relay.connect(RELAY_URL) - console.log('Connected!') - - const requestId = randomUUID() - - // Check if invoice has amount (look for pattern before '1' separator) - const amountMatch = TEST_INVOICE.toLowerCase().match(/ln(?:bc|tb|bcrt)(\d+)?([munp])?1/) - const hasAmount = amountMatch && amountMatch[1] - console.log('Invoice amount match:', amountMatch ? amountMatch.slice(0, 3) : null) - console.log('Has embedded amount:', hasAmount) - - // Build body - amount is always required (use 0 for invoices with embedded amounts) - const body = { - invoice: TEST_INVOICE, - amount: hasAmount ? 0 : 2000, // 0 means "use invoice amount" - } - console.log('Body amount:', body.amount, hasAmount ? '(use invoice amount)' : '(explicit amount)') - - const rpcRequest = { - rpcName: 'PayInvoice', - params: {}, - query: {}, - body, - authIdentifier: identity.publicKey, - requestId, - } - - console.log('\nRequest structure:', JSON.stringify(rpcRequest, null, 2)) - - const encryptedContent = encryptContent(identity, LIGHTNING_PUB_PUBKEY, rpcRequest) - - const event = finalizeEvent( - { - kind: 21000, - created_at: Math.floor(Date.now() / 1000), - tags: [['p', LIGHTNING_PUB_PUBKEY]], - content: encryptedContent, - }, - identity.privateKey - ) - - console.log('\nPublishing PayInvoice request (event id:', event.id.substring(0, 16) + '...)...') - - // Subscribe to responses - const filter = { - kinds: [21000], - authors: [LIGHTNING_PUB_PUBKEY], - since: Math.floor(Date.now() / 1000) - 5, - } - - let responseReceived = false - const sub = relay.subscribe([filter], { - onevent(evt) { - // Check if for us - const pTags = evt.tags.filter((t) => t[0] === 'p') - if (!pTags.some((t) => t[1] === identity.publicKey)) return - - console.log('\nGot response event:', evt.id.substring(0, 16) + '...') - try { - const response = decryptJSON(identity, LIGHTNING_PUB_PUBKEY, evt.content) - console.log('Response:', JSON.stringify(response, null, 2)) - if (response.requestId === requestId) { - responseReceived = true - } - } catch (err) { - console.log('Failed to decrypt:', err.message) - } - }, - }) - - await relay.publish(event) - console.log('Request published, waiting for response...') - - // Wait for response - for (let i = 0; i < 20; i++) { - await new Promise((r) => setTimeout(r, 500)) - if (responseReceived) break - } - - if (!responseReceived) { - console.log('\nNo response received for our requestId within timeout') - } - - sub.close() - relay.close() -} - -main().catch(console.error) diff --git a/packages/nostr-client/package.json b/packages/nostr-client/package.json index e235660..ef5ff2b 100644 --- a/packages/nostr-client/package.json +++ b/packages/nostr-client/package.json @@ -21,20 +21,14 @@ "validate-schemas": "tsx scripts/validate-schemas.ts" }, "dependencies": { - "@noble/curves": "^2.0.1", "@noble/hashes": "^2.0.1", - "@scure/base": "^1.2.6", - "@shocknet/clink-sdk": "^1.5.4", - "@stablelib/xchacha20": "^2.0.1", "nostr-tools": "^2.10.0" }, "devDependencies": { "@types/node": "^22.19.7", - "qrcode": "^1.5.4", "tsx": "^4.19.0", "typescript": "^5.7.0", - "vitest": "^2.1.0", - "ws": "^8.19.0" + "vitest": "^2.1.0" }, "peerDependencies": { "typescript": "^5.0.0" diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 3f91de1..0fe5e88 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -211,21 +211,9 @@ importers: packages/nostr-client: dependencies: - '@noble/curves': - specifier: ^2.0.1 - version: 2.0.1 '@noble/hashes': specifier: ^2.0.1 version: 2.0.1 - '@scure/base': - specifier: ^1.2.6 - version: 1.2.6 - '@shocknet/clink-sdk': - specifier: ^1.5.4 - version: 1.5.4 - '@stablelib/xchacha20': - specifier: ^2.0.1 - version: 2.0.1 nostr-tools: specifier: ^2.10.0 version: 2.19.4(typescript@5.9.3) @@ -233,9 +221,6 @@ importers: '@types/node': specifier: ^22.19.7 version: 22.19.7 - qrcode: - specifier: ^1.5.4 - version: 1.5.4 tsx: specifier: ^4.19.0 version: 4.21.0 @@ -245,9 +230,6 @@ importers: vitest: specifier: ^2.1.0 version: 2.1.9(@types/node@22.19.7)(lightningcss@1.30.2) - ws: - specifier: ^8.19.0 - version: 8.19.0 packages/state-machine: dependencies: @@ -1107,28 +1089,10 @@ packages: resolution: {integrity: sha512-9On64rhzuqKdOQyiYLYv2lQOh3TZU/D3+IWCR5gk0alPel2nwpp4YwDEGiUBfrQZEdQ6xww0PWkzqth4wqwX3Q==} engines: {node: '>=12.0.0'} - '@shocknet/clink-sdk@1.5.4': - resolution: {integrity: sha512-YrKR7oFjzUmBhm4p8pT6mP3YC7UdWbIlh5PMXHSwSLfJhL8Ddx91NLIfOHUTcQa1cDf27uvillMFXYrVUMCRuQ==} - '@sindresorhus/is@4.6.0': resolution: {integrity: sha512-t09vSN3MdfsyCHoFcTRCH/iUtG7OJ0CsjzB8cjAmKc/va/kIgeDI/TxsigdncE/4be734m0cvIYwNaV4i2XqAw==} engines: {node: '>=10'} - '@stablelib/binary@2.0.1': - resolution: {integrity: sha512-U9iAO8lXgEDONsA0zPPSgcf3HUBNAqHiJmSHgZz62OvC3Hi2Bhc5kTnQ3S1/L+sthDTHtCMhcEiklmIly6uQ3w==} - - '@stablelib/chacha@2.0.1': - resolution: {integrity: sha512-lS1FqtNqofxe2vLkRsLli2m3x/XanUyAYRphLhdHumKeIsLbjbCXdCq3Pf/eWiO7G3QlSG5ViqnoVjktzfLWMg==} - - '@stablelib/int@2.0.1': - resolution: {integrity: sha512-Ht63fQp3wz/F8U4AlXEPb7hfJOIILs8Lq55jgtD7KueWtyjhVuzcsGLSTAWtZs3XJDZYdF1WcSKn+kBtbzupww==} - - '@stablelib/wipe@2.0.1': - resolution: {integrity: sha512-1eU2K9EgOcV4qc9jcP6G72xxZxEm5PfeI5H55l08W95b4oRJaqhmlWRc4xZAm6IVSKhVNxMi66V67hCzzuMTAg==} - - '@stablelib/xchacha20@2.0.1': - resolution: {integrity: sha512-k55pNv7gIM4mUPU00+nJYTxKiUVNwAtsgrridC0aIU5cVbw9u6qP99x8ENu5eiwOEhZUNg+p3tTOLooCeAOJQA==} - '@swc/helpers@0.5.18': resolution: {integrity: sha512-TXTnIcNJQEKwThMMqBXsZ4VGAza6bvN4pa41Rkqoio6QBKMvo+5lexeTMScGCIxtzgQJzElcvIltani+adC5PQ==} @@ -1590,10 +1554,6 @@ packages: resolution: {integrity: sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ==} engines: {node: '>= 0.4'} - camelcase@5.3.1: - resolution: {integrity: sha512-L28STB170nwWS63UjtlEOE3dldQApaJXZkOI1uMFfzf3rRuPegHaHesyee+YxQ+W6SvRDQV6UrdOdRiR153wJg==} - engines: {node: '>=6'} - chai@5.3.3: resolution: {integrity: sha512-4zNhdJD/iOjSH0A05ea+Ke6MU5mmpQcbQsSOkgdaUMJ9zTlDTD/GYlwohmIE2u0gaxHYiVHEn1Fw9mZ/ktJWgw==} engines: {node: '>=18'} @@ -1639,9 +1599,6 @@ packages: resolution: {integrity: sha512-n8fOixwDD6b/ObinzTrp1ZKFzbgvKZvuz/TvejnLn1aQfC6r52XEx85FmuC+3HI+JM7coBRXUvNqEU2PHVrHpg==} engines: {node: '>=8'} - cliui@6.0.0: - resolution: {integrity: sha512-t6wbgtoCXvAzst7QgXxJYqPt0usEfbgQdftEPbLL/cvv6HPE5VgvqCuAIDR0NgU52ds6rFwqrgakNLrHEjCbrQ==} - cliui@8.0.1: resolution: {integrity: sha512-BSeNnyus75C4//NQ9gQt1/csTXyo/8Sb+afLAkzAptFuMsod9HFokGNudZpi/oQV73hnVK+sR+5PVRMd+Dr7YQ==} engines: {node: '>=12'} @@ -1744,10 +1701,6 @@ packages: supports-color: optional: true - decamelize@1.2.0: - resolution: {integrity: sha512-z2S+W9X73hAUUki+N+9Za2lBlun89zigOyGrsax+KUQ6wKW4ZoWpEYBkGhQjwAjjDCkWxhY0VKEhk8wzY7F5cA==} - engines: {node: '>=0.10.0'} - decompress-response@6.0.0: resolution: {integrity: sha512-aW35yZM6Bb/4oJlZncMH2LCoZtJXTRxES17vE3hoRiowU2kWHaJKFkSBDnDR+cm9J+9QhXmREyIfv0pji9ejCQ==} engines: {node: '>=10'} @@ -1792,9 +1745,6 @@ packages: detect-node@2.1.0: resolution: {integrity: sha512-T0NIuQpnTvFDATNuHN5roPwSBG83rFsuO+MXXH9/3N1eFbn4wcPjttvjMLEPWJ0RGUYgQE7cGgS3tNxbqCGM7g==} - dijkstrajs@1.0.3: - resolution: {integrity: sha512-qiSlmBq9+BCdCA/L46dw8Uy93mloxsPSbwnm5yrKn2vMPiy8KyAskTF6zuV/j5BMsmOGZDPs7KjU+mjb670kfA==} - dir-compare@4.2.0: resolution: {integrity: sha512-2xMCmOoMrdQIPHdsTawECdNPwlVFB9zGcz3kuhmBO6U3oU+UQjsue0i8ayLKpgBcm+hcXPMVSGUN9d+pvJ6+VQ==} @@ -1965,10 +1915,6 @@ packages: filelist@1.0.4: resolution: {integrity: sha512-w1cEuf3S+DrLCQL7ET6kz+gmlJdbq9J7yXCSjK/OZCPA+qEN1WyF4ZAf0YYJa4/shHJra2t/d/r8SV4Ji+x+8Q==} - find-up@4.1.0: - resolution: {integrity: sha512-PpOwAdQ/YlXQ2vj8a3h8IipDuYRi3wceVQQGYWxNINccq40Anw7BlsEXCMbt1Zt+OLA6Fq9suIpIWD0OsnISlw==} - engines: {node: '>=8'} - foreground-child@3.3.1: resolution: {integrity: sha512-gIXjKqtFuWEgzFRJA9WCQeSJLZDjgJUOMCMzxtvFq/37KojM1BFGufqsCy0r4qSQmYLsZYMeyRqzIWOMup03sw==} engines: {node: '>=14'} @@ -2043,10 +1989,6 @@ packages: deprecated: Old versions of glob are not supported, and contain widely publicized security vulnerabilities, which have been fixed in the current version. Please update. Support for old versions may be purchased (at exorbitant rates) by contacting i@izs.me hasBin: true - glob@13.0.0: - resolution: {integrity: sha512-tvZgpqk6fz4BaNZ66ZsRaZnbHvP/jG3uKJvAZOwEVUL4RTA5nJeeLYfyN9/VA8NX/V3IBG+hkeuGpKjvELkVhA==} - engines: {node: 20 || >=22} - glob@7.2.3: resolution: {integrity: sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q==} deprecated: Old versions of glob are not supported, and contain widely publicized security vulnerabilities, which have been fixed in the current version. Please update. Support for old versions may be purchased (at exorbitant rates) by contacting i@izs.me @@ -2317,10 +2259,6 @@ packages: resolution: {integrity: sha512-utfs7Pr5uJyyvDETitgsaqSyjCb2qNRAtuqUeWIAKztsOYdcACf2KtARYXg2pSvhkt+9NfoaNY7fxjl6nuMjIQ==} engines: {node: '>= 12.0.0'} - locate-path@5.0.0: - resolution: {integrity: sha512-t7hw9pI+WvuwNJXwk5zVHpyhIqzg2qTlklJOf0mVxGSbe3Fp2VieZcduNYjaLDoy6p9uGpQEGWG87WpMKlNq8g==} - engines: {node: '>=8'} - lodash-es@4.17.23: resolution: {integrity: sha512-kVI48u3PZr38HdYz98UmfPnXl2DXrpdctLrFLCd3kOx1xUkOmpFPx7gCWWM5MPkL/fD8zb+Ph0QzjGFs4+hHWg==} @@ -2356,10 +2294,6 @@ packages: lru-cache@10.4.3: resolution: {integrity: sha512-JNAzZcXrCt42VGLuYz0zfAzDfAvJWW6AfYlDBQyDV5DClI2m5sAmK+OIO7s59XfsRsWHp02jAJrRadPRGTt6SQ==} - lru-cache@11.2.4: - resolution: {integrity: sha512-B5Y16Jr9LB9dHVkh6ZevG+vAbOsNOYCX+sXvFWFu7B3Iz5mijW3zdbMyhsh8ANd2mSWBYdJgnqi+mL7/LrOPYg==} - engines: {node: 20 || >=22} - lru-cache@6.0.0: resolution: {integrity: sha512-Jo6dJ04CmSjuznwJSS3pUeWmd/H0ffTlkXXgwZi+eq1UCmqQwCh+eLsYOYCwY991i2Fah4h1BEMCx4qThGbsiA==} engines: {node: '>=10'} @@ -2543,14 +2477,6 @@ packages: resolution: {integrity: sha512-DlL+XwOy3NxAQ8xuC0okPgK46iuVNAK01YN7RueYBqqFeGsBjV9XmCAzAdgt+667bCl5kPh9EqKKDwnaPG1I7A==} engines: {node: '>=10'} - nostr-tools@2.15.1: - resolution: {integrity: sha512-LpetHDR9ltnkpJDkva/SONgyKBbsoV+5yLB8DWc0/U3lCWGtoWJw6Nbc2vR2Ai67RIQYrBQeZLyMlhwVZRK/9A==} - peerDependencies: - typescript: '>=5.0.0' - peerDependenciesMeta: - typescript: - optional: true - nostr-tools@2.19.4: resolution: {integrity: sha512-qVLfoTpZegNYRJo5j+Oi6RPu0AwLP6jcvzcB3ySMnIT5DrAGNXfs5HNBspB/2HiGfH3GY+v6yXkTtcKSBQZwSg==} peerDependencies: @@ -2589,36 +2515,20 @@ packages: resolution: {integrity: sha512-BZOr3nRQHOntUjTrH8+Lh54smKHoHyur8We1V8DSMVrl5A2malOOwuJRnKRDjSnkoeBh4at6BwEnb5I7Jl31wg==} engines: {node: '>=8'} - p-limit@2.3.0: - resolution: {integrity: sha512-//88mFWSJx8lxCzwdAABTJL2MyWB12+eIY7MDL2SqLmAkeKU9qxRvWuSyTjm3FUmpBEMuFfckAIqEaVGUDxb6w==} - engines: {node: '>=6'} - p-limit@3.1.0: resolution: {integrity: sha512-TYOanM3wGwNGsZN2cVTYPArw454xnXj5qmWF1bEoAc4+cU/ol7GVh7odevjp1FNHduHc3KZMcFduxU5Xc6uJRQ==} engines: {node: '>=10'} - p-locate@4.1.0: - resolution: {integrity: sha512-R79ZZ/0wAxKGu3oYMlz8jy/kbhsNrS7SKZ7PxEHBgJ5+F2mtFW2fK2cOtBh1cHYkQsbzFV7I+EoRKe6Yt0oK7A==} - engines: {node: '>=8'} - p-map@4.0.0: resolution: {integrity: sha512-/bjOqmgETBYB5BoEeGVea8dmvHb2m9GLy1E9W43yeyfP6QQCZGFNa+XRceJEuDB6zqr+gKpIAmlLebMpykw/MQ==} engines: {node: '>=10'} - p-try@2.2.0: - resolution: {integrity: sha512-R4nPAVTAU0B9D35/Gk3uJf/7XYbQcyohSKdvAxIRSNghFl4e71hVoGnBNQz9cWaXxO2I10KTC+3jMdvvoKw6dQ==} - engines: {node: '>=6'} - package-json-from-dist@1.0.1: resolution: {integrity: sha512-UEZIS3/by4OC8vL3P2dTXRETpebLI2NiI5vIrjaD/5UtrkFX/tNbwjTSRAGC/+7CAo2pIcBaRgWmcBBHcsaCIw==} path-browserify@1.0.1: resolution: {integrity: sha512-b7uo2UCUOYZcnF/3ID0lulOJi/bafxa1xPe7ZPsammBSpjSWQkjNxlt635YGS2MiR9GjvuXCtz2emr3jbsz98g==} - path-exists@4.0.0: - resolution: {integrity: sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w==} - engines: {node: '>=8'} - path-is-absolute@1.0.1: resolution: {integrity: sha512-AVbw3UJ2e9bq64vSaS9Am0fje1Pa8pbGqTTsmXfaIiMpnr5DlDhfJOuLj9Sf95ZPVDAUerDfEk88MPmPe7UCQg==} engines: {node: '>=0.10.0'} @@ -2631,10 +2541,6 @@ packages: resolution: {integrity: sha512-Xa4Nw17FS9ApQFJ9umLiJS4orGjm7ZzwUrwamcGQuHSzDyth9boKDaycYdDcZDuqYATXw4HFXgaqWTctW/v1HA==} engines: {node: '>=16 || 14 >=14.18'} - path-scurry@2.0.1: - resolution: {integrity: sha512-oWyT4gICAu+kaA7QWk/jvCHWarMKNs6pXOGWKDTr7cw4IGcUbW+PeTfbaQiLGheFRpjo6O9J0PmyMfQPjH71oA==} - engines: {node: 20 || >=22} - pathe@1.1.2: resolution: {integrity: sha512-whLdWMYL2TwI08hn8/ZqAbrVemu0LNaNNJZX73O6qaIdCTfXutsLhMkjdENX0qhsQ9uIimo4/aQOmXkoon2nDQ==} @@ -2669,10 +2575,6 @@ packages: resolution: {integrity: sha512-uysumyrvkUX0rX/dEVqt8gC3sTBzd4zoWfLeS29nb53imdaXVvLINYXTI2GNqzaMuvacNx4uJQ8+b3zXR0pkgQ==} engines: {node: '>=10.4.0'} - pngjs@5.0.0: - resolution: {integrity: sha512-40QW5YalBNfQo5yRYmiw7Yz6TKKVr3h6970B2YE+3fQpsWcrbj1PzJgxeJ19DRQjhMbKPIuMY8rFaXc8moolVw==} - engines: {node: '>=10.13.0'} - postcss@8.5.6: resolution: {integrity: sha512-3Ybi1tAuwAP9s0r1UQ2J4n5Y0G05bJkpUIO0/bI9MhwmD70S5aTWbXGBwxHrelT+XM1k6dM0pk+SwNkpTRN7Pg==} engines: {node: ^10 || ^12 || >=14} @@ -2723,11 +2625,6 @@ packages: peerDependencies: vue: ^3.0.0 - qrcode@1.5.4: - resolution: {integrity: sha512-1ca71Zgiu6ORjHqFBDpnSMTR2ReToX4l1Au1VFLyVeBTFavzQnv5JxMFr3ukHVKpSrSA2MCk0lNJSykjUfz7Zg==} - engines: {node: '>=10.13.0'} - hasBin: true - quick-lru@5.1.1: resolution: {integrity: sha512-WuyALRjWPDGtt/wzJiadO5AXY+8hZ80hVpe6MyivgraREW751X3SbhRvG3eLKOYN+8VEvqLcf3wdnt44Z4S4SA==} engines: {node: '>=10'} @@ -2759,9 +2656,6 @@ packages: resolution: {integrity: sha512-fGxEI7+wsG9xrvdjsrlmL22OMTTiHRwAMroiEeMgq8gzoLC/PQr7RsRDSTLUg/bZAZtF+TVIkHc6/4RIKrui+Q==} engines: {node: '>=0.10.0'} - require-main-filename@2.0.0: - resolution: {integrity: sha512-NKN5kMDylKuldxYLSUfrbo5Tuzh4hd+2E8NPPX02mZtn1VuREQToYe/ZdlJy+J3uCpfaiGF05e7B8W0iXbQHmg==} - resedit@1.7.2: resolution: {integrity: sha512-vHjcY2MlAITJhC0eRD/Vv8Vlgmu9Sd3LX9zZvtGzU5ZImdTN3+d6e/4mnTyV8vEbyf1sgNIrWxhWlrys52OkEA==} engines: {node: '>=12', npm: '>=6'} @@ -2788,11 +2682,6 @@ packages: deprecated: Rimraf versions prior to v4 are no longer supported hasBin: true - rimraf@6.1.2: - resolution: {integrity: sha512-cFCkPslJv7BAXJsYlK1dZsbP8/ZNLkCAQ0bi1hf5EKX2QHegmDFEFA6QhuYJlk7UDdc+02JjO80YSOrWPpw06g==} - engines: {node: 20 || >=22} - hasBin: true - roarr@2.15.4: resolution: {integrity: sha512-CHhPh+UNHD2GTXNYhPWLnU8ONHdI+5DI+4EYIAOaiD63rHeYlZvyh8P+in5999TTSFgUYuKUAjzRI4mdh/p+2A==} engines: {node: '>=8.0'} @@ -3245,9 +3134,6 @@ packages: wcwidth@1.0.1: resolution: {integrity: sha512-XHPEwS0q6TaxcvG85+8EYkbiCux2XtWG2mkc47Ng2A77BQu9+DqIOJldST4HgPkuea7dvKSj5VgX3P1d4rW8Tg==} - which-module@2.0.1: - resolution: {integrity: sha512-iBdZ57RDvnOR9AGBhML2vFZf7h8vmBjhoaZqODJBFWHVtKkDmKuHai3cx5PgVMrX5YDNp27AofYbAwctSS+vhQ==} - which@2.0.2: resolution: {integrity: sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==} engines: {node: '>= 8'} @@ -3261,10 +3147,6 @@ packages: wide-align@1.1.5: resolution: {integrity: sha512-eDMORYaPNZ4sQIuuYPDHdQvf4gyCF9rEEV/yPxGfwPkRodwEgiMUUXTx/dex+Me0wxx53S+NgUHaP7y3MGlDmg==} - wrap-ansi@6.2.0: - resolution: {integrity: sha512-r6lPcBGxZXlIcymEu7InxDMhdW0KDxpLgoFLcguasxCaJ/SOIZwINatK9KY/tf+ZrlywOKU0UDj3ATXUBfxJXA==} - engines: {node: '>=8'} - wrap-ansi@7.0.0: resolution: {integrity: sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==} engines: {node: '>=10'} @@ -3276,18 +3158,6 @@ packages: wrappy@1.0.2: resolution: {integrity: sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==} - ws@8.19.0: - resolution: {integrity: sha512-blAT2mjOEIi0ZzruJfIhb3nps74PRWTCz1IjglWEEpQl5XS/UNama6u2/rjFkDDouqr4L67ry+1aGIALViWjDg==} - engines: {node: '>=10.0.0'} - peerDependencies: - bufferutil: ^4.0.1 - utf-8-validate: '>=5.0.2' - peerDependenciesMeta: - bufferutil: - optional: true - utf-8-validate: - optional: true - xmlbuilder@15.1.1: resolution: {integrity: sha512-yMqGBqtXyeN1e3TGYvgNgDVZ3j84W4cwkOXQswghol6APgZWaff9lnbvN7MHYJOiXsvGPXtjTYJEiC9J2wv9Eg==} engines: {node: '>=8.0'} @@ -3295,9 +3165,6 @@ packages: xstate@5.25.1: resolution: {integrity: sha512-oyvsNH5pF2qkHmiHEMdWqc3OjDtoZOH2MTAI35r01f/ZQWOD+VLOiYqo65UgQET0XMA5s9eRm8fnsIo+82biEw==} - y18n@4.0.3: - resolution: {integrity: sha512-JKhqTOwSrqNA1NY5lSztJ1GrBiUodLMmIZuLiDaMRJ+itFd+ABVE8XBjOvIWL+rSqNDC74LCSFmlb/U4UZ4hJQ==} - y18n@5.0.8: resolution: {integrity: sha512-0pfFzegeDWJHJIAmTLRP2DwHjdF5s7jo9tuztdQxAhINCdvS+3nGINqPd00AphqJR/0LhANUS6/+7SCb98YOfA==} engines: {node: '>=10'} @@ -3305,18 +3172,10 @@ packages: yallist@4.0.0: resolution: {integrity: sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==} - yargs-parser@18.1.3: - resolution: {integrity: sha512-o50j0JeToy/4K6OZcaQmW6lyXXKhq7csREXcDwk2omFPJEwUNOVtJKvmDr9EI1fAJZUyZcRF7kxGBWmRXudrCQ==} - engines: {node: '>=6'} - yargs-parser@21.1.1: resolution: {integrity: sha512-tVpsJW7DdjecAiFpbIB1e3qxIQsE6NoPc5/eTdrbbIC4h0LVsWhnoa3g+m2HclBIujHzsxZ4VJVA+GUuc2/LBw==} engines: {node: '>=12'} - yargs@15.4.1: - resolution: {integrity: sha512-aePbxDmcYW++PaqBsJ+HYUFwCdv4LVvdnhBy78E57PIor8/OVvhMrADFFEDh8DHDFRv/O9i3lPhsENjO7QX0+A==} - engines: {node: '>=8'} - yargs@17.7.2: resolution: {integrity: sha512-7dSzzRQ++CKnNI/krKnYRV7JKKPUXMEh61soaHKg9mrWEhzFWhFnxPxGl+69cD1Ou63C13NUPCnmIcrvqCuM6w==} engines: {node: '>=12'} @@ -3927,35 +3786,8 @@ snapshots: transitivePeerDependencies: - supports-color - '@shocknet/clink-sdk@1.5.4': - dependencies: - '@noble/hashes': 1.8.0 - '@scure/base': 1.2.6 - nostr-tools: 2.15.1(typescript@5.9.3) - rimraf: 6.1.2 - typescript: 5.9.3 - '@sindresorhus/is@4.6.0': {} - '@stablelib/binary@2.0.1': - dependencies: - '@stablelib/int': 2.0.1 - - '@stablelib/chacha@2.0.1': - dependencies: - '@stablelib/binary': 2.0.1 - '@stablelib/wipe': 2.0.1 - - '@stablelib/int@2.0.1': {} - - '@stablelib/wipe@2.0.1': {} - - '@stablelib/xchacha20@2.0.1': - dependencies: - '@stablelib/binary': 2.0.1 - '@stablelib/chacha': 2.0.1 - '@stablelib/wipe': 2.0.1 - '@swc/helpers@0.5.18': dependencies: tslib: 2.8.1 @@ -4539,8 +4371,6 @@ snapshots: es-errors: 1.3.0 function-bind: 1.1.2 - camelcase@5.3.1: {} - chai@5.3.3: dependencies: assertion-error: 2.0.1 @@ -4582,12 +4412,6 @@ snapshots: string-width: 4.2.3 optional: true - cliui@6.0.0: - dependencies: - string-width: 4.2.3 - strip-ansi: 6.0.1 - wrap-ansi: 6.2.0 - cliui@8.0.1: dependencies: string-width: 4.2.3 @@ -4678,8 +4502,6 @@ snapshots: dependencies: ms: 2.1.3 - decamelize@1.2.0: {} - decompress-response@6.0.0: dependencies: mimic-response: 3.1.0 @@ -4719,8 +4541,6 @@ snapshots: detect-node@2.1.0: optional: true - dijkstrajs@1.0.3: {} - dir-compare@4.2.0: dependencies: minimatch: 3.1.2 @@ -4995,11 +4815,6 @@ snapshots: dependencies: minimatch: 5.1.6 - find-up@4.1.0: - dependencies: - locate-path: 5.0.0 - path-exists: 4.0.0 - foreground-child@3.3.1: dependencies: cross-spawn: 7.0.6 @@ -5101,12 +4916,6 @@ snapshots: package-json-from-dist: 1.0.1 path-scurry: 1.11.1 - glob@13.0.0: - dependencies: - minimatch: 10.1.1 - minipass: 7.1.2 - path-scurry: 2.0.1 - glob@7.2.3: dependencies: fs.realpath: 1.0.0 @@ -5368,10 +5177,6 @@ snapshots: lightningcss-win32-arm64-msvc: 1.30.2 lightningcss-win32-x64-msvc: 1.30.2 - locate-path@5.0.0: - dependencies: - p-locate: 4.1.0 - lodash-es@4.17.23: {} lodash.defaults@4.2.0: {} @@ -5397,8 +5202,6 @@ snapshots: lru-cache@10.4.3: {} - lru-cache@11.2.4: {} - lru-cache@6.0.0: dependencies: yallist: 4.0.0 @@ -5575,18 +5378,6 @@ snapshots: normalize-url@6.1.0: {} - nostr-tools@2.15.1(typescript@5.9.3): - dependencies: - '@noble/ciphers': 0.5.3 - '@noble/curves': 1.2.0 - '@noble/hashes': 1.3.1 - '@scure/base': 1.1.1 - '@scure/bip32': 1.3.1 - '@scure/bip39': 1.2.1 - nostr-wasm: 0.1.0 - optionalDependencies: - typescript: 5.9.3 - nostr-tools@2.19.4(typescript@5.9.3): dependencies: '@noble/ciphers': 0.5.3 @@ -5635,30 +5426,18 @@ snapshots: p-cancelable@2.1.1: {} - p-limit@2.3.0: - dependencies: - p-try: 2.2.0 - p-limit@3.1.0: dependencies: yocto-queue: 0.1.0 - p-locate@4.1.0: - dependencies: - p-limit: 2.3.0 - p-map@4.0.0: dependencies: aggregate-error: 3.1.0 - p-try@2.2.0: {} - package-json-from-dist@1.0.1: {} path-browserify@1.0.1: {} - path-exists@4.0.0: {} - path-is-absolute@1.0.1: {} path-key@3.1.1: {} @@ -5668,11 +5447,6 @@ snapshots: lru-cache: 10.4.3 minipass: 7.1.2 - path-scurry@2.0.1: - dependencies: - lru-cache: 11.2.4 - minipass: 7.1.2 - pathe@1.1.2: {} pathval@2.0.1: {} @@ -5701,8 +5475,6 @@ snapshots: base64-js: 1.5.1 xmlbuilder: 15.1.1 - pngjs@5.0.0: {} - postcss@8.5.6: dependencies: nanoid: 3.3.11 @@ -5750,12 +5522,6 @@ snapshots: dependencies: vue: 3.5.27(typescript@5.9.3) - qrcode@1.5.4: - dependencies: - dijkstrajs: 1.0.3 - pngjs: 5.0.0 - yargs: 15.4.1 - quick-lru@5.1.1: {} rc@1.2.8: @@ -5810,8 +5576,6 @@ snapshots: require-directory@2.1.1: {} - require-main-filename@2.0.0: {} - resedit@1.7.2: dependencies: pe-library: 0.4.1 @@ -5835,11 +5599,6 @@ snapshots: dependencies: glob: 7.2.3 - rimraf@6.1.2: - dependencies: - glob: 13.0.0 - package-json-from-dist: 1.0.1 - roarr@2.15.4: dependencies: boolean: 3.2.0 @@ -6291,8 +6050,6 @@ snapshots: dependencies: defaults: 1.0.4 - which-module@2.0.1: {} - which@2.0.2: dependencies: isexe: 2.0.0 @@ -6306,12 +6063,6 @@ snapshots: dependencies: string-width: 4.2.3 - wrap-ansi@6.2.0: - dependencies: - ansi-styles: 4.3.0 - string-width: 4.2.3 - strip-ansi: 6.0.1 - wrap-ansi@7.0.0: dependencies: ansi-styles: 4.3.0 @@ -6326,39 +6077,16 @@ snapshots: wrappy@1.0.2: {} - ws@8.19.0: {} - xmlbuilder@15.1.1: {} xstate@5.25.1: {} - y18n@4.0.3: {} - y18n@5.0.8: {} yallist@4.0.0: {} - yargs-parser@18.1.3: - dependencies: - camelcase: 5.3.1 - decamelize: 1.2.0 - yargs-parser@21.1.1: {} - yargs@15.4.1: - dependencies: - cliui: 6.0.0 - decamelize: 1.2.0 - find-up: 4.1.0 - get-caller-file: 2.0.5 - require-directory: 2.1.1 - require-main-filename: 2.0.0 - set-blocking: 2.0.0 - string-width: 4.2.3 - which-module: 2.0.1 - y18n: 4.0.3 - yargs-parser: 18.1.3 - yargs@17.7.2: dependencies: cliui: 8.0.1 From e907bcc08551bf1ac0c759fb84c47b518f480eda Mon Sep 17 00:00:00 2001 From: Padreug Date: Fri, 9 Oct 2026 22:25:07 +0200 Subject: [PATCH 14/15] chore: stop tracking the generated .devenv.flake.nix devenv regenerates this file on every `devenv shell`; the committed copy was pinned to a directory that no longer exists (~/Work/tries/2026-01-22-lamassu-refactor-packages/lamassu-next). Untracked and gitignored beside .devenv/. The file stays on disk. --- .devenv.flake.nix | 513 ---------------------------------------------- .gitignore | 1 + 2 files changed, 1 insertion(+), 513 deletions(-) delete mode 100644 .devenv.flake.nix diff --git a/.devenv.flake.nix b/.devenv.flake.nix deleted file mode 100644 index aeeee91..0000000 --- a/.devenv.flake.nix +++ /dev/null @@ -1,513 +0,0 @@ -{ - inputs = - let - vars = { - version = "1.11.2"; - system = "x86_64-linux"; - devenv_root = "/home/padreug/Work/tries/2026-01-22-lamassu-refactor-packages/lamassu-next"; - project_input_ref = "path:/home/padreug/Work/tries/2026-01-22-lamassu-refactor-packages/lamassu-next"; - devenv_dotfile = "/home/padreug/Work/tries/2026-01-22-lamassu-refactor-packages/lamassu-next/.devenv"; - devenv_dotfile_path = ./.devenv; - devenv_tmpdir = "/run/user/1000"; - devenv_runtime = "/run/user/1000/devenv-f4ba770"; - devenv_istesting = false; - devenv_direnvrc_latest_version = 1; - container_name = null; - active_profiles = [ - ]; - hostname = "gizmo"; - username = "padreug"; - git_root = "/home/padreug/Work/tries/2026-01-22-lamassu-refactor-packages"; - secretspec = null; -}; - in - { - git-hooks.url = "github:cachix/git-hooks.nix"; - git-hooks.inputs.nixpkgs.follows = "nixpkgs"; - pre-commit-hooks.follows = "git-hooks"; - nixpkgs.url = "github:cachix/devenv-nixpkgs/rolling"; - devenv.url = "github:cachix/devenv?dir=src/modules"; - } - // ( - if builtins.pathExists (vars.devenv_dotfile_path + "/flake.json") then - builtins.fromJSON (builtins.readFile (vars.devenv_dotfile_path + "/flake.json")) - else - { } - ); - - outputs = - { nixpkgs, ... }@inputs: - let - vars = { - version = "1.11.2"; - system = "x86_64-linux"; - devenv_root = "/home/padreug/Work/tries/2026-01-22-lamassu-refactor-packages/lamassu-next"; - project_input_ref = "path:/home/padreug/Work/tries/2026-01-22-lamassu-refactor-packages/lamassu-next"; - devenv_dotfile = "/home/padreug/Work/tries/2026-01-22-lamassu-refactor-packages/lamassu-next/.devenv"; - devenv_dotfile_path = ./.devenv; - devenv_tmpdir = "/run/user/1000"; - devenv_runtime = "/run/user/1000/devenv-f4ba770"; - devenv_istesting = false; - devenv_direnvrc_latest_version = 1; - container_name = null; - active_profiles = [ - ]; - hostname = "gizmo"; - username = "padreug"; - git_root = "/home/padreug/Work/tries/2026-01-22-lamassu-refactor-packages"; - secretspec = null; -}; - devenv = - if builtins.pathExists (vars.devenv_dotfile_path + "/devenv.json") then - builtins.fromJSON (builtins.readFile (vars.devenv_dotfile_path + "/devenv.json")) - else - { }; - - systems = [ - "x86_64-linux" - "aarch64-linux" - "x86_64-darwin" - "aarch64-darwin" - ]; - - # Function to create devenv configuration for a specific system with profiles support - mkDevenvForSystem = - targetSystem: - let - getOverlays = - inputName: inputAttrs: - map ( - overlay: - let - input = - inputs.${inputName} or (throw "No such input `${inputName}` while trying to configure overlays."); - in - input.overlays.${overlay} - or (throw "Input `${inputName}` has no overlay called `${overlay}`. Supported overlays: ${nixpkgs.lib.concatStringsSep ", " (builtins.attrNames input.overlays)}") - ) inputAttrs.overlays or [ ]; - overlays = nixpkgs.lib.flatten (nixpkgs.lib.mapAttrsToList getOverlays (devenv.inputs or { })); - permittedUnfreePackages = - devenv.nixpkgs.per-platform."${targetSystem}".permittedUnfreePackages - or devenv.nixpkgs.permittedUnfreePackages or [ ]; - pkgs = import nixpkgs { - system = targetSystem; - config = { - allowUnfree = - devenv.nixpkgs.per-platform."${targetSystem}".allowUnfree or devenv.nixpkgs.allowUnfree - or devenv.allowUnfree or false; - allowBroken = - devenv.nixpkgs.per-platform."${targetSystem}".allowBroken or devenv.nixpkgs.allowBroken - or devenv.allowBroken or false; - cudaSupport = - devenv.nixpkgs.per-platform."${targetSystem}".cudaSupport or devenv.nixpkgs.cudaSupport or false; - cudaCapabilities = - devenv.nixpkgs.per-platform."${targetSystem}".cudaCapabilities or devenv.nixpkgs.cudaCapabilities - or [ ]; - permittedInsecurePackages = - devenv.nixpkgs.per-platform."${targetSystem}".permittedInsecurePackages - or devenv.nixpkgs.permittedInsecurePackages or devenv.permittedInsecurePackages or [ ]; - allowUnfreePredicate = - if (permittedUnfreePackages != [ ]) then - (pkg: builtins.elem (nixpkgs.lib.getName pkg) permittedUnfreePackages) - else - (_: false); - }; - inherit overlays; - }; - inherit (pkgs) lib; - importModule = - path: - if lib.hasPrefix "./" path then - if lib.hasSuffix ".nix" path then - ./. + (builtins.substring 1 255 path) - else - ./. + (builtins.substring 1 255 path) + "/devenv.nix" - else if lib.hasPrefix "../" path then - # For parent directory paths, concatenate with /. - # ./. refers to the directory containing this file (project root) - # So ./. + "/../shared" = /../shared - if lib.hasSuffix ".nix" path then ./. + "/${path}" else ./. + "/${path}/devenv.nix" - else - let - paths = lib.splitString "/" path; - name = builtins.head paths; - input = inputs.${name} or (throw "Unknown input ${name}"); - subpath = "/${lib.concatStringsSep "/" (builtins.tail paths)}"; - devenvpath = "${input}" + subpath; - devenvdefaultpath = devenvpath + "/devenv.nix"; - in - if lib.hasSuffix ".nix" devenvpath then - devenvpath - else if builtins.pathExists devenvdefaultpath then - devenvdefaultpath - else - throw (devenvdefaultpath + " file does not exist for input ${name}."); - - # Phase 1: Base evaluation to extract profile definitions - baseProject = pkgs.lib.evalModules { - specialArgs = inputs // { - inherit inputs; - }; - modules = [ - ( - { config, ... }: - { - _module.args.pkgs = pkgs.appendOverlays (config.overlays or [ ]); - } - ) - (inputs.devenv.modules + /top-level.nix) - ( - { options, ... }: - { - config.devenv = lib.mkMerge [ - { - cliVersion = vars.version; - root = vars.devenv_root; - dotfile = vars.devenv_dotfile; - } - (pkgs.lib.optionalAttrs (builtins.hasAttr "tmpdir" options.devenv) { - tmpdir = vars.devenv_tmpdir; - }) - (pkgs.lib.optionalAttrs (builtins.hasAttr "isTesting" options.devenv) { - isTesting = vars.devenv_istesting; - }) - (pkgs.lib.optionalAttrs (builtins.hasAttr "runtime" options.devenv) { - runtime = vars.devenv_runtime; - }) - (pkgs.lib.optionalAttrs (builtins.hasAttr "direnvrcLatestVersion" options.devenv) { - direnvrcLatestVersion = vars.devenv_direnvrc_latest_version; - }) - ]; - } - ) - ( - { options, ... }: - { - config = lib.mkMerge [ - (pkgs.lib.optionalAttrs (builtins.hasAttr "git" options) { - git.root = vars.git_root; - }) - ]; - } - ) - (pkgs.lib.optionalAttrs (vars.container_name != null) { - container.isBuilding = pkgs.lib.mkForce true; - containers.${vars.container_name}.isBuilding = true; - }) - ] - ++ (map importModule (devenv.imports or [ ])) - ++ [ - (if builtins.pathExists ./devenv.nix then ./devenv.nix else { }) - (devenv.devenv or { }) - (if builtins.pathExists ./devenv.local.nix then ./devenv.local.nix else { }) - ( - if builtins.pathExists (vars.devenv_dotfile_path + "/cli-options.nix") then - import (vars.devenv_dotfile_path + "/cli-options.nix") - else - { } - ) - ]; - }; - - # Phase 2: Extract and apply profiles using extendModules with priority overrides - project = - let - # Build ordered list of profile names: hostname -> user -> manual - manualProfiles = vars.active_profiles; - currentHostname = vars.hostname; - currentUsername = vars.username; - hostnameProfiles = lib.optional ( - currentHostname != "" - && builtins.hasAttr currentHostname (baseProject.config.profiles.hostname or { }) - ) "hostname.${currentHostname}"; - userProfiles = lib.optional ( - currentUsername != "" && builtins.hasAttr currentUsername (baseProject.config.profiles.user or { }) - ) "user.${currentUsername}"; - - # Ordered list of profiles to activate - orderedProfiles = hostnameProfiles ++ userProfiles ++ manualProfiles; - - # Resolve profile extends with cycle detection - resolveProfileExtends = - profileName: visited: - if builtins.elem profileName visited then - throw "Circular dependency detected in profile extends: ${lib.concatStringsSep " -> " visited} -> ${profileName}" - else - let - profile = getProfileConfig profileName; - extends = profile.extends or [ ]; - newVisited = visited ++ [ profileName ]; - extendedProfiles = lib.flatten (map (name: resolveProfileExtends name newVisited) extends); - in - extendedProfiles ++ [ profileName ]; - - # Get profile configuration by name from baseProject - getProfileConfig = - profileName: - if lib.hasPrefix "hostname." profileName then - let - name = lib.removePrefix "hostname." profileName; - in - baseProject.config.profiles.hostname.${name} - else if lib.hasPrefix "user." profileName then - let - name = lib.removePrefix "user." profileName; - in - baseProject.config.profiles.user.${name} - else - let - availableProfiles = builtins.attrNames (baseProject.config.profiles or { }); - hostnameProfiles = map (n: "hostname.${n}") ( - builtins.attrNames (baseProject.config.profiles.hostname or { }) - ); - userProfiles = map (n: "user.${n}") (builtins.attrNames (baseProject.config.profiles.user or { })); - allAvailableProfiles = availableProfiles ++ hostnameProfiles ++ userProfiles; - in - baseProject.config.profiles.${profileName} - or (throw "Profile '${profileName}' not found. Available profiles: ${lib.concatStringsSep ", " allAvailableProfiles}"); - - # Fold over ordered profiles to build final list with extends - expandedProfiles = lib.foldl' ( - acc: profileName: - let - allProfileNames = resolveProfileExtends profileName [ ]; - in - acc ++ allProfileNames - ) [ ] orderedProfiles; - - # Map over expanded profiles and apply priorities - allPrioritizedModules = lib.imap0 ( - index: profileName: - let - # Decrement priority for each profile (lower = higher precedence) - # Start with the next lowest priority after the default priority for values (100) - profilePriority = (lib.modules.defaultOverridePriority - 1) - index; - profileConfig = getProfileConfig profileName; - - # Check if an option type needs explicit override to resolve conflicts - # Only apply overrides to LEAF values (scalars), not collection types that can merge - typeNeedsOverride = - type: - if type == null then - false - else - let - typeName = type.name or type._type or ""; - - # True leaf types that need priority resolution when they conflict - isLeafType = builtins.elem typeName [ - "str" - "int" - "bool" - "enum" - "path" - "package" - "float" - "anything" - ]; - in - if isLeafType then - true - else if typeName == "nullOr" then - # For nullOr, check the wrapped type recursively - let - innerType = - type.elemType - or (if type ? nestedTypes && type.nestedTypes ? elemType then type.nestedTypes.elemType else null); - in - if innerType != null then typeNeedsOverride innerType else false - else - # Everything else (collections, submodules, etc.) should merge naturally - false; - - # Check if a config path needs explicit override - pathNeedsOverride = - optionPath: - let - # Try direct option first - directOption = lib.attrByPath optionPath null baseProject.options; - in - if directOption != null && lib.isOption directOption then - typeNeedsOverride directOption.type - else if optionPath != [ ] then - # Check parent for freeform type - let - parentPath = lib.init optionPath; - parentOption = lib.attrByPath parentPath null baseProject.options; - in - if parentOption != null && lib.isOption parentOption then - let - # Look for freeform type: - # 1. Standard location: type.freeformType (primary) - # 2. Nested location: type.nestedTypes.freeformType (evaluated form) - freeformType = parentOption.type.freeformType or parentOption.type.nestedTypes.freeformType or null; - elementType = - if freeformType ? elemType then - freeformType.elemType - else if freeformType ? nestedTypes && freeformType.nestedTypes ? elemType then - freeformType.nestedTypes.elemType - else - freeformType; - in - typeNeedsOverride elementType - else - false - else - false; - - # Support overriding both plain attrset modules and functions - applyModuleOverride = - config: - if builtins.isFunction config then - let - wrapper = args: applyOverrideRecursive (config args) [ ]; - in - lib.mirrorFunctionArgs config wrapper - else - applyOverrideRecursive config [ ]; - - # Apply overrides recursively based on option types - applyOverrideRecursive = - config: optionPath: - if lib.isAttrs config && config ? _type then - config # Don't touch values with existing type metadata - else if lib.isAttrs config then - lib.mapAttrs (name: value: applyOverrideRecursive value (optionPath ++ [ name ])) config - else if pathNeedsOverride optionPath then - lib.mkOverride profilePriority config - else - config; - - # Apply priority overrides recursively to the deferredModule imports structure - prioritizedConfig = ( - profileConfig.module - // { - imports = lib.map ( - importItem: - importItem - // { - imports = lib.map (nestedImport: applyModuleOverride nestedImport) (importItem.imports or [ ]); - } - ) (profileConfig.module.imports or [ ]); - } - ); - in - prioritizedConfig - ) expandedProfiles; - in - if allPrioritizedModules == [ ] then - baseProject - else - baseProject.extendModules { modules = allPrioritizedModules; }; - - config = project.config; - - options = pkgs.nixosOptionsDoc { - options = builtins.removeAttrs project.options [ "_module" ]; - warningsAreErrors = false; - # Unpack Nix types, e.g. literalExpression, mDoc. - transformOptions = - let - isDocType = - v: - builtins.elem v [ - "literalDocBook" - "literalExpression" - "literalMD" - "mdDoc" - ]; - in - lib.attrsets.mapAttrs ( - _: v: - if v ? _type && isDocType v._type then - v.text - else if v ? _type && v._type == "derivation" then - v.name - else - v - ); - }; - - # Recursively search for outputs in the config. - # This is used when not building a specific output by attrpath. - build = - options: config: - lib.concatMapAttrs ( - name: option: - if lib.isOption option then - let - typeName = option.type.name or ""; - in - if - builtins.elem typeName [ - "output" - "outputOf" - ] - then - { ${name} = config.${name}; } - else - { } - else if builtins.isAttrs option && !lib.isDerivation option then - let - v = build option config.${name}; - in - if v != { } then - { - ${name} = v; - } - else - { } - else - { } - ) options; - in - { - inherit - config - options - build - project - ; - shell = config.shell; - packages = { - optionsJSON = options.optionsJSON; - # deprecated - inherit (config) - info - procfileScript - procfileEnv - procfile - ; - ci = config.ciDerivation; - }; - }; - - # Generate per-system devenv configurations - perSystem = nixpkgs.lib.genAttrs systems mkDevenvForSystem; - - # Default devenv for the current system - currentSystemDevenv = perSystem.${vars.system}; - in - { - devShell = nixpkgs.lib.genAttrs systems (s: perSystem.${s}.shell); - packages = nixpkgs.lib.genAttrs systems (s: perSystem.${s}.packages); - - # Per-system devenv configurations - devenv = { - # Default devenv for the current system - inherit (currentSystemDevenv) - config - options - build - shell - packages - project - ; - # Per-system devenv configurations - inherit perSystem; - }; - - # Legacy build output - build = currentSystemDevenv.build currentSystemDevenv.options currentSystemDevenv.config; - }; -} diff --git a/.gitignore b/.gitignore index ae58eb9..4c16c4e 100644 --- a/.gitignore +++ b/.gitignore @@ -55,6 +55,7 @@ apps/machine/src/services/*.js # devenv .devenv/ +.devenv.flake.nix .direnv/ .pre-commit-config.yaml From fa4858ed66673d7b576cbf8e7092ec309e574a5a Mon Sep 17 00:00:00 2001 From: Padreug Date: Fri, 9 Oct 2026 22:25:42 +0200 Subject: [PATCH 15/15] chore: drop stragglers from the regtest-tooling removal MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit .gitignore still carried rules for docker/**/data/ and docker/.state/ — paths that no longer exist — under a now-empty "# Docker" header. The docs skill's example sync report named LIGHTNING_PUB_URL as its sample env var; swapped for a variable that exists. --- .claude/skills/docs.md | 2 +- .gitignore | 4 ---- 2 files changed, 1 insertion(+), 5 deletions(-) diff --git a/.claude/skills/docs.md b/.claude/skills/docs.md index a9052d5..e9909ba 100644 --- a/.claude/skills/docs.md +++ b/.claude/skills/docs.md @@ -175,7 +175,7 @@ From git commits since last release: ### Suggested Updates 1. `api/clink.md:45` - Add `timeout` parameter to createOffer -2. `guides/development.md` - Add LIGHTNING_PUB_URL env var +2. `guides/development.md` - Add VITE_BITSPIRE_CASSETTES env var ### Missing Documentation - `packages/cashu/src/wallet.ts` - No API docs diff --git a/.gitignore b/.gitignore index 4c16c4e..3feb238 100644 --- a/.gitignore +++ b/.gitignore @@ -59,10 +59,6 @@ apps/machine/src/services/*.js .direnv/ .pre-commit-config.yaml -# Docker -docker/**/data/ -docker/.state/ - # Nix build outputs result result-*