satmachineadmin/CLAUDE.md
Padreug 05c1105897
Some checks failed
ci.yml / feat(v2): collision guard — refuse machines whose npub matches an operator account (#32) (pull_request) Failing after 0s
feat(v2): collision guard — refuse machines whose npub matches an operator account (#32)
Adds `_assert_no_pubkey_collision` to `views_api`, wired into
`api_create_machine` between the wallet-ownership guard and the
`create_machine` CRUD call. Refuses with HTTP 400 + operator-actionable
error message if the supplied `machine_npub` matches any existing
LNbits operator account's `accounts.pubkey`.

## Why this matters

Reproducer 2026-05-30T21:33Z (coord-log archive `2026-05-31-pre-rotation.md`):
Greg's operator account `accounts.pubkey` had been seeded as the same
value as Sintra's `dca_machines.machine_npub` (`522a4538…`) during
manual setup. The collision masked the routing bug for days — lnbits'
nostr-transport `auth.py:resolve_nostr_auth` was routing inbound
kind-21000 RPCs from the ATM directly to Greg's wallet *by coincidence*
of the matching pubkey. When Greg's account migrated to
`RemoteBunkerSigner` and got a fresh pubkey, the coincidence broke +
`auto-account-from-npub` fired for the orphaned ATM npub. A real $20
test cash-out silently landed on a fresh auto-account wallet
(`a94b564f…`); satmachineadmin lost the settlement entirely — no
`dca_settlements` row, no DCA distribution, no commission split.

The proper architectural fix is path B / `aiolabs/satmachineadmin#20`
(S6, in-progress with lnbits — coord-log `2026-05-31T15:25Z`). This
guard is the complementary preventive layer: stops a future operator
from re-entering the broken state by registering a machine whose npub
collides with an existing account.

## What's in this commit

- **`views_api._assert_no_pubkey_collision`** — canonicalises the input
  npub (accepts hex or `npub1…` bech32) via `normalize_public_key`,
  queries `lnbits.core.crud.users.get_account_by_pubkey` (which itself
  lowercases internally), raises HTTPException(400) on hit. Error
  message names the canonical pubkey prefix, explains the
  pubkey-collision dependency that breaks on operator pubkey rotation,
  + points to the `lamassu-next provision-atm` remediation path +
  this issue for context.
- **Wired into `api_create_machine`** after `_assert_wallet_owned_by`
  + before `create_machine`. `api_update_machine` is unaffected
  because `UpdateMachineData` doesn't allow npub changes on existing
  rows.
- **`tests/test_collision_guard.py`** — 7 unit tests covering hex /
  bech32 / uppercase-hex inputs all canonicalise to the same lookup,
  the no-collision case returns silently, error message asserts
  (truncated pubkey + remediation hint). Uses pytest monkeypatch to
  isolate the assertion logic from a live `get_account_by_pubkey` DB
  call — matches the assertion-style pattern of
  `tests/test_nostr_attribution.py`.
- **`CLAUDE.md`** — new "No-collision invariant" subsection under
  Security Considerations: documents the rule + the SQL check
  operators can run on existing installs + the
  `ATM_PRIVATE_KEY`-unset remediation + cross-refs to `#20` and `#32`.

## Regtest SQL check result

Ran the diagnostic SQL against the regtest LNbits + satmachineadmin DBs:

- 1 active `dca_machines.machine_npub`: `522a4538…` (Greg's Sintra)
- 1 collision found: the auto-account orphan `a94b564f…` (username =
  None — auto-account signature) created during yesterday's silent-drop
  failure mode. NOT a legitimate operator account. Greg's actual
  operator account `ac35c9fc…` carries pubkey `197a4cf4…` post-bunker
  migration, no collision there.

The orphan is operational cleanup (sweep + delete), separate from this
code fix. No real-operator collisions remain on the regtest instance.

## Test status

162 passed, 1 pre-existing async-plugin failure unchanged.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-31 18:45:56 +02:00

12 KiB

CLAUDE.md

This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository.

Project Overview

This is the Satoshi Machine Admin Extension for LNBits - a Dollar Cost Averaging (DCA) administration system that integrates with Lamassu ATM machines to automatically distribute Bitcoin to registered clients based on their deposit balances.

Development Commands

Code Quality & Formatting

# Format all code
make format

# Check code quality
make check

# Individual commands
poetry run black .                    # Python formatting
poetry run ruff check . --fix         # Python linting with auto-fix
poetry run ./node_modules/.bin/prettier --write .  # JavaScript formatting
poetry run mypy .                     # Python type checking
poetry run ./node_modules/.bin/pyright # JavaScript type checking

Testing

# Run tests
make test
# or
PYTHONUNBUFFERED=1 DEBUG=true poetry run pytest

Pre-commit Hooks

# Install pre-commit hooks
make install-pre-commit-hook

# Run pre-commit on all files
make pre-commit

Architecture

Core Structure

The Satoshi Machine Admin extension follows LNBits architecture patterns:

  • Backend (Python/FastAPI):

    • __init__.py - Extension initialization and router setup
    • models.py - Pydantic data models for DCA system
    • crud.py - Database operations for all DCA entities
    • views.py - Admin dashboard page route
    • views_api.py - DCA API endpoints
    • migrations.py - Database schema (condensed single migration)
    • tasks.py - Background polling and invoice listening
    • transaction_processor.py - Core Lamassu integration logic
  • Frontend (Vue.js 3 Options API + Quasar):

    • static/js/index.js - Admin dashboard Vue app
    • templates/myextension/index.html - Admin interface template

Key Conventions

  1. Template Syntax Rules:

    • Use {{ }} and {% %} ONLY in index.html files (Jinja2)
    • In Vue components: Use v-text='value' instead of {{ value }}
    • Use :attribute='value' for binding, v-html='value' for HTML content
  2. API Patterns:

    • Admin extension uses session-based superuser authentication (no API keys)
    • Client extension uses wallet admin keys for user-specific operations
    • Use LNbits.api.request() for all API calls
    • Destructure responses: const {data} = await LNbits.api.request(...)
  3. Component Registration:

    • Templates must be included BEFORE scripts in HTML
    • Components must be registered BEFORE app.mount()

The Magical G Object

The global this.g object provides access to:

  • this.g.user - Complete user data including wallets array
  • this.g.user.wallets[0].inkey - Invoice key (client extension only)
  • this.g.user.wallets[0].adminkey - Admin key (client extension only)
  • this.g.wallet - Currently selected wallet

Note: Admin extension uses superuser session authentication, not wallet keys.

Built-in Utilities

  • Currency conversion: /api/v1/currencies, /api/v1/conversion
  • QR code generation: /api/v1/qrcode/{data} or Quasar VueQrcode component
  • WebSocket support: wss://host/api/v1/ws/{id} with POST to /api/v1/ws/{id}/{data}

Configuration Files

  • config.json - Extension configuration (name: "DCA Admin")
  • manifest.json - Extension manifest for installation
  • pyproject.toml - Python dependencies and tool configuration
  • package.json - JavaScript dependencies

DCA System Architecture

Core Components

1. Lamassu ATM Integration

  • Secure SSH connection to remote Lamassu PostgreSQL database
  • Polls cash_out_txs table for new confirmed transactions
  • Supports both SSH password and private key authentication
  • Read-only database access for security

2. Commission Calculation Engine

  • Formula: base_amount = total_amount / (1 + effective_commission)
  • Effective commission: commission_percentage * (100 - discount) / 100
  • Supports discount percentages for promotional rates
  • Separates commission earnings to configurable wallet

3. DCA Distribution System

  • Flow Mode: Proportional distribution based on client balance ratios
  • Fixed Mode: Daily limit-based allocation (future enhancement)
  • Automatic Bitcoin transfers using LNBits internal payment system
  • Real-time balance tracking and deduction

4. Audit and Compliance

  • Complete transaction audit trail in lamassu_transactions table
  • Detailed distribution records in dca_payments table
  • Clickable transaction history with distribution breakdowns
  • CSV export capabilities for accounting

Database Schema

Core Tables:

  • dca_clients - Client registration and DCA mode settings
  • dca_deposits - Fiat deposit tracking and confirmation workflow
  • dca_payments - Bitcoin payment records and status tracking
  • lamassu_config - Database connection and polling configuration
  • lamassu_transactions - Complete audit trail of processed ATM transactions

Key Features:

  • Single migration (m001_initial_dca_schema) creates complete schema
  • UTC timezone handling throughout
  • Comprehensive indexing for performance
  • Foreign key relationships maintained

API Endpoints

Client Management

  • GET /api/v1/dca/clients - List all DCA clients
  • GET /api/v1/dca/clients/{id}/balance - Get client balance summary
  • POST /api/v1/dca/clients - Create test client (development)

Deposit Administration

  • GET /api/v1/dca/deposits - List all deposits
  • POST /api/v1/dca/deposits - Create new deposit
  • PUT /api/v1/dca/deposits/{id}/status - Confirm deposits

Transaction Processing

  • GET /api/v1/dca/transactions - List processed Lamassu transactions
  • GET /api/v1/dca/transactions/{id}/distributions - View distribution details
  • POST /api/v1/dca/manual-poll - Trigger manual database poll
  • POST /api/v1/dca/test-transaction - Process test transaction

Configuration

  • GET /api/v1/dca/config - Get Lamassu database configuration
  • POST /api/v1/dca/config - Save database and wallet settings
  • POST /api/v1/dca/test-connection - Verify connectivity

Frontend Architecture

Vue.js Components

  • Dashboard Overview - System status and recent activity
  • Client Management - DCA client table with balance tracking
  • Deposit Workflow - Quick deposit forms and confirmation
  • Transaction History - Lamassu transaction audit with drill-down
  • Configuration Panel - Database and wallet setup with testing

Key UX Features

  • Real-time balance updates during polling
  • Loading states for all async operations
  • Error handling with user-friendly notifications
  • Responsive design for mobile administration
  • Export functionality for audit and compliance

Technical Implementation Details

SSH Connection Setup

# On Lamassu server:
sudo mkdir -p /var/lib/postgresql/.ssh
sudo echo "your-public-key" >> /var/lib/postgresql/.ssh/authorized_keys
sudo chown -R postgres:postgres /var/lib/postgresql/.ssh
sudo chmod 700 /var/lib/postgresql/.ssh
sudo chmod 600 /var/lib/postgresql/.ssh/authorized_keys

Commission Calculation Example

# Real transaction: 2000 GTQ → 266,800 sats (3% commission, 0% discount)
crypto_atoms = 266800  # Total sats from Lamassu
commission_percentage = 0.03  # 3%
discount = 0.0  # No discount

effective_commission = 0.03 * (100 - 0) / 100 = 0.03
base_amount = 266800 / (1 + 0.03) = 258,835 sats (for DCA)
commission_amount = 266800 - 258835 = 7,965 sats (to commission wallet)

Polling Strategy

  • Automatic: Hourly background task via LNBits task system
  • Manual: Admin-triggered polling for immediate processing
  • Smart Recovery: Tracks last successful poll to prevent missed transactions
  • Error Handling: Graceful failure with detailed logging

Security Considerations

  • Superuser Authentication: Admin extension requires LNBits superuser login
  • Wallet Admin Keys: Client extension uses wallet admin keys for user operations
  • Database Access: Only superusers can write to satoshimachine database
  • SSH tunnel encryption for database connectivity
  • Read-only database permissions for Lamassu access
  • Input sanitization and type validation
  • Audit logging for all administrative actions

No-collision invariant — operator account pubkey ≠ ATM npub

dca_machines.machine_npub and accounts.pubkey MUST NEVER hold the same value across the LNbits instance. Enforced by views_api._assert_no_pubkey_collision at machine-creation time (rejects with HTTP 400) and by the matching SQL check operators can run on existing installs:

SELECT a.id, a.username, a.pubkey, m.id, m.machine_npub
FROM accounts a
JOIN ext_satoshimachine.dca_machines m
  ON LOWER(a.pubkey) = LOWER(m.machine_npub);

Why this matters: when the two values match, lnbits' nostr-transport auth.py:resolve_nostr_auth routes inbound kind-21000 RPCs from the ATM directly to that operator's wallet by collision — it works by coincidence, breaks silently the moment the operator's pubkey rotates (then auto-account-from-npub fires for the orphaned ATM npub, and the invoice lands on a fresh auto-account wallet instead). Reproduced on 2026-05-30 against Greg's Sintra (silent cash-out drop). The proper architectural routing fix is aiolabs/satmachineadmin#20 (path B / S6); the collision guard prevents the broken state from being entered in the first place.

When provisioning a new ATM via lamassu-next deploy/nixos/provision-atm.sh, leave ATM_PRIVATE_KEY unset so the script generates a fresh ATM keypair (distinct from any operator's nsec). See aiolabs/satmachineadmin#32 for design rationale + the (eventual) reverse-direction guard on account creation in lnbits proper.

Development Workflow

Adding New Features

  1. Models: Update models.py with new Pydantic schemas
  2. Database: Add migration to migrations.py (append only)
  3. CRUD: Implement database operations in crud.py
  4. API: Add endpoints to views_api.py
  5. Frontend: Update Vue.js components in static/js/index.js
  6. Templates: Modify HTML template if needed

Testing

  • Use "Test Connection" for database connectivity verification
  • Use "Test Transaction" for DCA flow validation
  • Manual polling for real-world transaction testing
  • Monitor LNBits logs for detailed error information

Debugging

  • Enable debug logging: DEBUG=true
  • Check SSH tunnel connectivity independently
  • Verify Lamassu database query results
  • Monitor wallet balance changes
  • Review transaction audit trail

Key Files Reference

  • transaction_processor.py - Core Lamassu integration and DCA logic
  • models.py - Complete data models for DCA system
  • crud.py - Database operations with optimized queries
  • migrations.py - Single condensed schema migration
  • static/js/index.js - Admin dashboard Vue.js application
  • templates/myextension/index.html - Admin interface template
  • config.json - Extension metadata and configuration
  • tasks.py - Background polling and invoice listeners

Important Notes

  • Extension uses LNBits internal payment system for Bitcoin transfers
  • All timestamps stored and processed in UTC timezone
  • Commission calculations handle edge cases and rounding
  • SSH authentication prefers private keys over passwords
  • Database polling is stateful and resumable after downtime
  • UI displays human-readable usernames where available
  • Export functions generate CSV files for external analysis

Extension Status

Current Version: v0.0.1 (Initial Release)
Status: Production Ready
Dependencies: LNBits v1.0.0+, SSH access to Lamassu server
License: MIT