feat(v2): nostr-transport roster-resolver hook — path B wallet routing (#20) #36

Merged
padreug merged 1 commit from feat/roster-resolver into v2-bitspire 2026-05-31 20:33:18 +00:00
Owner

Closes #20.

Summary

Wires satmachineadmin into aiolabs/lnbits#42's roster-resolver registry so that inbound kind-21000 RPCs from a registered ATM npub route directly to the operator's wallet — delivering the path-B outcome "cash-out sats go to the operator's wallet, not an auto-created machine wallet" agreed in coord-log entries §14:40Z → §17:50Z.

What's in this PR

satmachineadmin/nostr_transport_roster.py (new, 143 LoC):

  • RouteHit frozen dataclass mirroring the agreed shape (operator_user_id, wallet_id, source_extension) — kept as a fallback so the module imports cleanly on lnbits versions pre-#43.
  • async def resolve(sender_pubkey_hex) -> RouteHit | None — wraps crud.get_machine_by_atm_pubkey_hex with defensive normalize_public_key canonicalisation (bech32 → hex, uppercase → lowercase). Raises on malformed input per lnbits' 15:15Z fail-closed contract item 2.
  • _build_route_hit() — lazy-imports lnbits.core.services.nostr_transport.RouteHit and prefers it when available; falls back to the local class. Auto-upgrades to lnbits' canonical class once the lnbits image rebuilds against dev@be148054.
  • register_with_lnbits() — lazy-imports register_roster_resolver and registers resolve under "satmachineadmin". Soft-fails with a documented INFO log on lnbits versions without the hook.

satmachineadmin/__init__.py — wires register_with_lnbits() into satmachineadmin_start() after the cassette consumer task. Fire-and-forget; doesn't block startup.

tests/test_roster_resolver.py (new, 158 LoC, 6 tests):

  1. Known ATM npub → RouteHit with operator's (user_id, wallet_id) — assertion is field-shape-based (not isinstance), so it passes against both my local RouteHit and lnbits' canonical one.
  2. Unknown sender → None
  3. Bech32 input → canonicalised to hex before crud lookup
  4. Uppercase hex → lowercased before lookup
  5. Malformed input → raises (fail-closed sub-case per 15:15Z)
  6. register_with_lnbits() soft-fails cleanly when lnbits hook absent

Verification

Dev-container boot smoke against post-#43 lnbits dev image (be148054):

satmachineadmin v2 loaded
satmachineadmin.nostr_transport_roster:register_with_lnbits:136 |
  satmachineadmin: registered 'satmachineadmin' roster resolver with
  lnbits nostr-transport — inbound kind-21000 from a registered ATM
  npub will route to the operator's wallet directly. (Behavior gated
  server-side by LNBITS_NOSTR_TRANSPORT_ROSTER_REQUIRED.)

Test suite: 161 passed, 0 failed (was 155/155 pre-PR; this PR adds 6 resolver tests).

Quality gates: black, ruff, mypy clean on new code.

Joint regtest smoke (still TODO; needs operator-driven Sintra interaction)

  1. Set LNBITS_NOSTR_TRANSPORT_ROSTER_REQUIRED=true in regtest compose env
  2. Restart lnbits container; verify registration logs "registered" (just done above)
  3. Pair Sintra-shape ATM via satmachineadmin UI against Greg's wallet (9927f101…)
  4. Trigger a cashout from that ATM
  5. Confirm: sats land in 9927f101… directly; no new accounts row for the ATM's npub; Payment.extra carries nostr_sender_pubkey + nostr_event_id per the existing gap-G5 wiring

(3)–(5) are operator-driven smoke; not autonomous-runnable.

Refs

  • Design archive + cross-link: aiolabs/satmachineadmin#20
  • lnbits-side implementation: aiolabs/lnbits!43 (merged be148054)
  • Coord-log: ~/dev/coordination/log.md §14:40Z → §17:50Z
  • Frozen shape contract: §15:25Z (sat-side close-out), §15:15Z (lnbits ack)
  • Failure-mode posture (aggressive / fail-closed): §15:20Z (user direction)

Test plan

  • pytest /shared/extensions/satmachineadmin/tests/ inside dev container → 161 passed
  • Extension boots cleanly + registration logs "registered" (not soft-fail-INFO)
  • Joint regtest smoke per the 5-step plan above (operator-driven)
  • Reviewer sanity-check on the soft-fail INFO message wording (logged once per boot — should read like an operational status, not an error)
Closes #20. ## Summary Wires satmachineadmin into `aiolabs/lnbits#42`'s roster-resolver registry so that inbound kind-21000 RPCs from a registered ATM npub route directly to the operator's wallet — delivering the path-B outcome "cash-out sats go to the operator's wallet, not an auto-created machine wallet" agreed in coord-log entries §`14:40Z` → §`17:50Z`. ## What's in this PR **`satmachineadmin/nostr_transport_roster.py`** (new, 143 LoC): - `RouteHit` frozen dataclass mirroring the agreed shape `(operator_user_id, wallet_id, source_extension)` — kept as a fallback so the module imports cleanly on lnbits versions pre-#43. - `async def resolve(sender_pubkey_hex) -> RouteHit | None` — wraps `crud.get_machine_by_atm_pubkey_hex` with defensive `normalize_public_key` canonicalisation (bech32 → hex, uppercase → lowercase). Raises on malformed input per lnbits' `15:15Z` fail-closed contract item 2. - `_build_route_hit()` — lazy-imports `lnbits.core.services.nostr_transport.RouteHit` and prefers it when available; falls back to the local class. Auto-upgrades to lnbits' canonical class once the lnbits image rebuilds against `dev@be148054`. - `register_with_lnbits()` — lazy-imports `register_roster_resolver` and registers `resolve` under `"satmachineadmin"`. Soft-fails with a documented INFO log on lnbits versions without the hook. **`satmachineadmin/__init__.py`** — wires `register_with_lnbits()` into `satmachineadmin_start()` after the cassette consumer task. Fire-and-forget; doesn't block startup. **`tests/test_roster_resolver.py`** (new, 158 LoC, 6 tests): 1. Known ATM npub → `RouteHit` with operator's `(user_id, wallet_id)` — assertion is field-shape-based (not `isinstance`), so it passes against both my local `RouteHit` and lnbits' canonical one. 2. Unknown sender → `None` 3. Bech32 input → canonicalised to hex before crud lookup 4. Uppercase hex → lowercased before lookup 5. Malformed input → raises (fail-closed sub-case per `15:15Z`) 6. `register_with_lnbits()` soft-fails cleanly when lnbits hook absent ## Verification Dev-container boot smoke against post-#43 lnbits dev image (`be148054`): ``` satmachineadmin v2 loaded satmachineadmin.nostr_transport_roster:register_with_lnbits:136 | satmachineadmin: registered 'satmachineadmin' roster resolver with lnbits nostr-transport — inbound kind-21000 from a registered ATM npub will route to the operator's wallet directly. (Behavior gated server-side by LNBITS_NOSTR_TRANSPORT_ROSTER_REQUIRED.) ``` Test suite: **161 passed, 0 failed** (was 155/155 pre-PR; this PR adds 6 resolver tests). Quality gates: black, ruff, mypy clean on new code. ## Joint regtest smoke (still TODO; needs operator-driven Sintra interaction) 1. ✅ Set `LNBITS_NOSTR_TRANSPORT_ROSTER_REQUIRED=true` in regtest compose env 2. ✅ Restart lnbits container; verify registration logs "registered" (just done above) 3. ⏳ Pair Sintra-shape ATM via satmachineadmin UI against Greg's wallet (`9927f101…`) 4. ⏳ Trigger a cashout from that ATM 5. ⏳ Confirm: sats land in `9927f101…` directly; no new `accounts` row for the ATM's npub; `Payment.extra` carries `nostr_sender_pubkey` + `nostr_event_id` per the existing gap-G5 wiring (3)–(5) are operator-driven smoke; not autonomous-runnable. ## Refs - Design archive + cross-link: aiolabs/satmachineadmin#20 - lnbits-side implementation: aiolabs/lnbits!43 (merged `be148054`) - Coord-log: `~/dev/coordination/log.md` §`14:40Z` → §`17:50Z` - Frozen shape contract: §`15:25Z` (sat-side close-out), §`15:15Z` (lnbits ack) - Failure-mode posture (aggressive / fail-closed): §`15:20Z` (user direction) ## Test plan - [x] `pytest /shared/extensions/satmachineadmin/tests/` inside dev container → 161 passed - [x] Extension boots cleanly + registration logs "registered" (not soft-fail-INFO) - [x] Joint regtest smoke per the 5-step plan above (operator-driven) - [ ] Reviewer sanity-check on the soft-fail INFO message wording (logged once per boot — should read like an operational status, not an error)
feat(v2): nostr-transport roster-resolver hook (#20 path-B)
Some checks failed
ci.yml / feat(v2): nostr-transport roster-resolver hook (#20 path-B) (pull_request) Failing after 0s
5850fb1ef4
Exposes `resolve(sender_pubkey_hex) -> RouteHit | None` and a
`register_with_lnbits()` helper that lazily-imports + soft-fails on
lnbits versions without `register_roster_resolver`. Wired into
`satmachineadmin_start()`.

The hook delivers the path-B outcome ("cash-out sats go to the
operator's wallet, not an auto-created machine wallet") once the
lnbits side ships its half. Shape contract `(operator_user_id,
wallet_id, source_extension)` frozen per coord-log 2026-05-31T15:25Z.
Branch held local until lnbits lands the registry — no behaviour
change on the current lnbits version, just the future-ready handoff
+ a benign INFO log on boot.

Boot-smoked in dev container: extension loads, registration logs the
documented soft-fail message, invoice listener + cassette consumer
unchanged. 6 new unit tests cover happy path, miss, bech32 +
uppercase canonicalisation, fail-closed on malformed input, and the
soft-fail register branch.
padreug force-pushed feat/roster-resolver from 5850fb1ef4
Some checks failed
ci.yml / feat(v2): nostr-transport roster-resolver hook (#20 path-B) (pull_request) Failing after 0s
to cb1caf47d0
Some checks failed
ci.yml / feat(v2): nostr-transport roster-resolver hook (#20 path-B) (pull_request) Failing after 0s
2026-05-31 18:12:13 +00:00
Compare
padreug force-pushed feat/roster-resolver from cb1caf47d0
Some checks failed
ci.yml / feat(v2): nostr-transport roster-resolver hook (#20 path-B) (pull_request) Failing after 0s
to 99efa52b69
Some checks failed
ci.yml / feat(v2): nostr-transport roster-resolver hook (#20 path-B) (pull_request) Failing after 0s
2026-05-31 19:46:28 +00:00
Compare
padreug merged commit e9f81d0cbb into v2-bitspire 2026-05-31 20:33:18 +00:00
padreug deleted branch feat/roster-resolver 2026-05-31 20:33:18 +00:00
Sign in to join this conversation.
No reviewers
No labels
No milestone
No project
No assignees
1 participant
Notifications
Due date
The due date is invalid or out of range. Please use the format "yyyy-mm-dd".

No due date set.

Dependencies

No dependencies set.

Reference
aiolabs/satmachineadmin!36
No description provided.