feat(v2): nostr-transport roster-resolver hook — path B wallet routing (#20) #36
Loading…
Add table
Add a link
Reference in a new issue
No description provided.
Delete branch "feat/roster-resolver"
Deleting a branch is permanent. Although the deleted branch may continue to exist for a short time before it actually gets removed, it CANNOT be undone in most cases. Continue?
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):RouteHitfrozen 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— wrapscrud.get_machine_by_atm_pubkey_hexwith defensivenormalize_public_keycanonicalisation (bech32 → hex, uppercase → lowercase). Raises on malformed input per lnbits'15:15Zfail-closed contract item 2._build_route_hit()— lazy-importslnbits.core.services.nostr_transport.RouteHitand prefers it when available; falls back to the local class. Auto-upgrades to lnbits' canonical class once the lnbits image rebuilds againstdev@be148054.register_with_lnbits()— lazy-importsregister_roster_resolverand registersresolveunder"satmachineadmin". Soft-fails with a documented INFO log on lnbits versions without the hook.satmachineadmin/__init__.py— wiresregister_with_lnbits()intosatmachineadmin_start()after the cassette consumer task. Fire-and-forget; doesn't block startup.tests/test_roster_resolver.py(new, 158 LoC, 6 tests):RouteHitwith operator's(user_id, wallet_id)— assertion is field-shape-based (notisinstance), so it passes against both my localRouteHitand lnbits' canonical one.None15:15Z)register_with_lnbits()soft-fails cleanly when lnbits hook absentVerification
Dev-container boot smoke against post-#43 lnbits dev image (
be148054):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)
LNBITS_NOSTR_TRANSPORT_ROSTER_REQUIRED=truein regtest compose env9927f101…)9927f101…directly; no newaccountsrow for the ATM's npub;Payment.extracarriesnostr_sender_pubkey+nostr_event_idper the existing gap-G5 wiring(3)–(5) are operator-driven smoke; not autonomous-runnable.
Refs
be148054)~/dev/coordination/log.md§14:40Z→ §17:50Z15:25Z(sat-side close-out), §15:15Z(lnbits ack)15:20Z(user direction)Test plan
pytest /shared/extensions/satmachineadmin/tests/inside dev container → 161 passedExposes `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.5850fb1ef4cb1caf47d0cb1caf47d099efa52b69