Replaces the no-op tasks.py stub with a real invoice listener that lands
bitSpire settlements idempotently into dca_settlements.
Architecture: satmachineadmin runs *inside* the LNbits process, so it
plugs into LNbits' canonical extension hook (register_invoice_listener
from lnbits.tasks) instead of going through the Nostr transport layer.
External clients like bitSpire use Nostr; internal extensions consume
the resulting Payment objects directly. One invoice_listener queue per
extension, dispatched by invoice_callback_dispatcher.
Flow:
bitSpire ATM (Nostr kind-21000)
→ LNbits nostr_transport handler
→ core Payment system (create_invoice + status=SUCCESS on settle)
→ invoice_callback_dispatcher
→ satmachineadmin's invoice_queue
→ _handle_payment filters by wallet_id → active machine
→ bitspire.parse_settlement reads Payment.extra (or back-derives)
→ create_settlement_idempotent (keyed on payment_hash UNIQUE)
The parser (new bitspire.py module) is bitSpire-specific:
- Happy path (post-aiolabs/lamassu-next#44): Payment.extra carries
{source:"bitspire", net_sats, fee_sats, fee_pct, exchange_rate,
currency, txid, machine_npub, bills, cassettes}. Read directly,
zero back-derivation.
- Fallback path (pre-#44): extra is absent. Back-derive the split
using machine.fallback_commission_pct with the Lamassu-style
formula (calculations.calculate_commission), mark
used_fallback_split=true, log a WARNING that namechecks the
upstream issue so it's findable in logs.
Two-stage commission split (super first, operator remainder) is
computed at land time so the audit row is complete:
platform_fee_sats = round(commission_sats * super_fee_pct)
operator_fee_sats = commission_sats - platform_fee_sats
The actual payout (LP DCA legs + super-fee leg + operator-split legs)
happens in a separate settlement-processor task in P2. P1 only LANDS
the settlement with status='pending'.
Smoke-tested both paths against real LNbits 1.4 (nostr-transport venv):
happy: 266800 gross → 258835 net + 7965 commission
(2390 super @ 30%, 5575 operator)
fallback: 266800 gross → 254095 net + 12705 commission @ 5% default
Also adds crud.get_active_machine_by_wallet_id, the lookup that gates
inbound payments to known machine wallets.
Refs: aiolabs/satmachineadmin#9, aiolabs/lamassu-next#44
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
The initial m005 made bitspire_event_id the UNIQUE idempotency key on
dca_settlements, but settlements arriving through LNbits' invoice
listener (the canonical path per nostr-transport-branch architecture)
don't carry a Nostr event id at the Payment level — that's the
underlying transport's concern, not exposed to extensions.
The natural unique key is payment_hash:
- every LN invoice has a globally unique payment_hash
- subscription replays / dispatcher double-fires dedup via UNIQUE
- it's always present on the Payment object the invoice_listener delivers
Reshape the dca_settlements column constraints:
- payment_hash: TEXT NOT NULL UNIQUE (was: NOT NULL + separate index)
- bitspire_event_id: TEXT (was: NOT NULL UNIQUE) — kept nullable for
a future path where we subscribe to raw kind-21000 Nostr events
directly, bypassing the Payment system
Also rename the CRUD helper: get_settlement_by_event_id →
get_settlement_by_payment_hash, and update create_settlement_idempotent
to dedup on payment_hash. CreateDcaSettlementData / DcaSettlement
adjust accordingly.
The schema is unshipped (v2-bitspire branch is local only) — fixing
m005 in-place is appropriate. The separate dca_telemetry path for
kind-30078/30079 events already uses (machine_id, beacon_received_at)
semantics, so the UNIQUE-by-Nostr-event-id pattern isn't needed there
either.
Caught during P1a design before subscribing to register_invoice_listener.
Refs: aiolabs/satmachineadmin#9, aiolabs/lamassu-next#44
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Replaces v1's super-only single-config CRUD with the v2 operator-scoped data
layer that matches the m005 schema:
- Machines: create/get/get_by_npub/list_for_operator/update/delete
- Clients: scoped per (machine, user). Adds list_for_operator (across an
operator's fleet) and list_for_user (LP cross-operator view), plus
get_flow_mode_clients_for_machine for the distribution algorithm.
- Deposits: now carry machine_id and creator_user_id; per-operator listing.
- Settlements: create_settlement_idempotent treats bitspire_event_id as the
uniqueness key, returning the existing row on replay so subscription
re-delivery is safe by construction. mark_settlement_status drives the
pending → processed/partial/refunded/errored lifecycle.
- Commission splits: replace_commission_splits is an atomic per-scope
replace; the SetCommissionSplitsData model already validates legs sum
to 1.0 at the boundary. get_effective_commission_splits handles the
per-machine-override-or-operator-default precedence.
- Payments: leg-typed (dca / super_fee / operator_split / settlement /
autoforward / refund) with helpers for settlement/client/operator scopes.
- Balance summary: sums confirmed deposits minus completed dca legs.
- Telemetry: upsert_beacon_snapshot uses COALESCE so today's sparse
kind-30078 payload doesn't clobber post-#43 fields when they start
arriving. upsert_fleet_snapshot stores raw JSON until lamassu-next#42
fixes the kind-30079 schema.
- Super config: singleton get/update.
Also stubs three legacy entry points so __init__.py imports cleanly while
the rest of P0/P1 is in flight:
- tasks.py: no-op stubs for wait_for_paid_invoices + hourly_transaction_polling.
Real Nostr subscription manager lands in P1.
- views_api.py: a single /api/v1/dca/{...} catch-all returns 503 with a
precise message. v2 endpoints land in P1+.
- views.py: drops the super-only check on the index page (v2 is
operator-installable); platform-fee config moves to a super-only API in P1.
transaction_processor.py is left untouched but is now orphaned (no one
imports it) — gets a full rewrite in P1.
Refs: plan at ~/.claude/plans/snug-gliding-shamir.md
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Replaces the Lamassu-era data models (LamassuConfig, StoredLamassuTransaction,
single-config CRUD carriers) with the v2 Pydantic surface matching m005:
- Machine / CreateMachineData / UpdateMachineData: per-operator multi-machine
registry keyed by Nostr npub. Replaces single-row LamassuConfig + SSH fields.
- DcaClient now scoped per (machine_id, user_id). Includes autoforward fields
for satmachineadmin#8 (best-effort LN-address forwarding for LPs).
- DcaDeposit gains machine_id + creator_user_id (audit trail finding from v1).
- DcaSettlement: idempotency carrier for bitSpire kind-21000 events. Carries
platform_fee_sats + operator_fee_sats as absolute ints (v1 hook for the v2
customer-discount engine — see plan).
- CommissionSplitLeg / CommissionSplit / SetCommissionSplitsData: operator's
remainder-distribution rules. SetCommissionSplitsData validates legs sum
to 1.0 at the boundary so crud.py only sees valid sets.
- DcaPayment dropped transaction_type in favor of leg_type discriminator
(dca | super_fee | operator_split | settlement | autoforward | refund).
Gains settlement_id + machine_id + operator_user_id + destination_*.
- TelemetrySnapshot: sparse beacon + fleet snapshot fields, all nullable so
we degrade gracefully against today's minimal kind-30078 payload.
- SuperConfig / UpdateSuperConfigData: super-only platform-fee carrier.
- PartialDispenseData (satmachineadmin#3), SettleBalanceData (satmachineadmin#4):
operator UX action carriers.
Stays on pydantic v1 @validator pattern + Optional[X] hints to match the
rest of the codebase. The UP045 / N805 lint noise is pre-existing tech debt
across the repo, not introduced here.
Refs: plan at ~/.claude/plans/snug-gliding-shamir.md
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Breaking redesign. Drops the v1 Lamassu-era tables (lamassu_config,
lamassu_transactions, plus the singular-config dca_clients/deposits/payments)
and creates the v2 schema:
- dca_machines: per-operator multi-machine registry, keyed by Nostr npub.
Replaces the single-row lamassu_config pattern.
- dca_settlements: bitSpire kind-21000 idempotency. platform_fee_sats and
operator_fee_sats stored as absolute BIGINT — v1 hook so the v2 customer-
discount engine can record who-forgave-what without a migration.
- dca_commission_splits: operator-defined remainder rules (per-machine or
default; sum-to-1.0 invariant enforced at write).
- dca_payments: leg-typed (dca | super_fee | operator_split | settlement |
autoforward | refund). Drops the old transaction_type field.
- dca_clients: now scoped per (machine_id, user_id) so an LP can hold
positions across machines/operators on the same instance.
- dca_deposits: gains machine_id + creator_user_id for audit.
- dca_telemetry: sparse kind-30078 / kind-30079 snapshots; post-#43 fields
nullable until lamassu-next enriches the beacon.
- super_config: singleton row for super_fee_pct + super_fee_wallet_id.
No backwards compatibility — operators on the previous schema must wipe and
re-onboard. Old migrations m001-m004 remain so fresh installs still walk the
versioned path; m005 drops their tables before creating the v2 schema.
Incidental: stripped trailing whitespace in m004 (W291/W293 hygiene).
Refs: plan at ~/.claude/plans/snug-gliding-shamir.md
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Add PUT /api/v1/dca/deposits/{id} endpoint to update amount, currency,
and notes on pending deposits. Add DELETE endpoint to remove deposits
not yet inserted into the machine. Both endpoints reject confirmed
deposits. Frontend now shows edit/delete buttons only for pending rows.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
When there's a sync mismatch (more cash in ATM than tracked client
balances), cap each client's allocation to their remaining fiat
balance equivalent in sats. Orphan sats stay in the source wallet.
This prevents over-allocation when deposits haven't been recorded
yet or when there's a timing mismatch between ATM transactions
and balance tracking.
- Detect sync mismatch: total_confirmed_deposits < fiat_amount
- In sync mismatch mode: allocate based on client balance, not tx amount
- Track orphan_sats that couldn't be distributed
- Normal mode unchanged: proportional distribution using calculate_distribution()
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
LNbits create_invoice expects amount as float, not int. Added
explicit float() cast to both DCA distribution and commission
payment invoice creation calls.
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
- Extract pure calculation functions to calculations.py (no lnbits deps)
- transaction_processor.py now imports from calculations.py (DRY)
- Add 22 tests covering commission, distribution, and fiat round-trip
- Include real Lamassu transaction data (8.75%, 5.5% commission rates)
- Test edge cases: discounts (90%, 100%), zero commission, small amounts
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
The finally block was resetting runningTestTransaction instead of
processingSpecificTransaction, causing the button to stay in loading
state after processing.
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
LNbits 1.4 changed check_super_user to return Account (no wallets)
instead of User (with wallets). This broke the template rendering
because LNbits.map.user() requires the wallets property.
Switch to check_user_exists (returns User with wallets) and manually
check user.super_user for access control. This follows the same
pattern used by LNbits core admin pages.
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
LNbits 1.4 changed g.user initialization (PR #3615), moving it from
windowMixin to base.html. This means g.user can be null during initial
Vue template evaluation.
- Use optional chaining g.user?.wallets || [] in template
- Add null guard before accessing this.g.user.wallets in JS
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
Removes the test transaction button from the admin UI.
The test transaction endpoint is still available in the API for development and debugging purposes.
Implements functionality to manually process specific Lamassu transactions by ID, bypassing dispense checks.
This allows administrators to handle transactions that may have failed due to dispense issues or were settled manually outside of the automated process.
The feature includes a new UI dialog for entering the transaction ID and an API endpoint to fetch and process the transaction, crediting wallets and distributing funds according to the DCA configuration.
Refactor GTQ storage migration: Moved the conversion logic for centavo amounts to GTQ into a new migration function, m004_convert_to_gtq_storage, ensuring proper data type changes and updates across relevant tables. This enhances clarity and maintains the integrity of the migration process.
Client registration will now be handled by the DCA client extension.
The admin extension focuses solely on:
- Reading existing clients
- Managing deposits (pending → confirmed workflow)
- Monitoring DCA activity
Test client creation code preserved in 'feature/test-client-creation' branch.
🤖 Generated with Claude Code
Co-Authored-By: Claude <noreply@anthropic.com>
Update DCA API calls to use admin key: Changed references from `inkey` to `adminkey` in multiple DCA-related API requests to ensure proper access control and security compliance.
Refactor DCA Admin page endpoints: Update description, remove unused CRUD operations and API endpoints related to MyExtension, and streamline the code for improved clarity and functionality.
Remove QR Code dialog from MyExtension index.html: streamline the UI by eliminating unused dialog components, enhancing code clarity and maintainability.