"""In-memory Lightning transaction ledger. Tracks invoices, settlements, and balances per the schema in ``docs/adr/018-lightning-ledger.md``. Uses a simple in-memory list so the dashboard can display real (ephemeral) data without requiring SQLite yet. """ from __future__ import annotations import logging import uuid from dataclasses import dataclass from datetime import UTC, datetime from enum import StrEnum logger = logging.getLogger(__name__) class TxType(StrEnum): incoming = "incoming" outgoing = "outgoing" class TxStatus(StrEnum): pending = "pending" settled = "settled" failed = "failed" expired = "expired" @dataclass class LedgerEntry: """Single ledger row matching the ADR-018 schema.""" id: str tx_type: TxType status: TxStatus payment_hash: str amount_sats: int memo: str source: str created_at: str invoice: str = "" preimage: str = "" task_id: str = "" agent_id: str = "" settled_at: str = "" fee_sats: int = 0 # ── In-memory store ────────────────────────────────────────────────── _entries: list[LedgerEntry] = [] def create_invoice_entry( payment_hash: str, amount_sats: int, memo: str = "", source: str = "tool_usage", task_id: str = "", agent_id: str = "", invoice: str = "", ) -> LedgerEntry: """Record a new incoming invoice in the ledger.""" entry = LedgerEntry( id=uuid.uuid4().hex[:16], tx_type=TxType.incoming, status=TxStatus.pending, payment_hash=payment_hash, amount_sats=amount_sats, memo=memo, source=source, task_id=task_id, agent_id=agent_id, invoice=invoice, created_at=datetime.now(UTC).isoformat(), ) _entries.append(entry) logger.debug("Ledger entry created: %s (%s sats)", entry.id, amount_sats) return entry def mark_settled(payment_hash: str, preimage: str = "") -> LedgerEntry | None: """Mark a pending entry as settled by payment hash.""" for entry in _entries: if entry.payment_hash == payment_hash and entry.status == TxStatus.pending: entry.status = TxStatus.settled entry.preimage = preimage entry.settled_at = datetime.now(UTC).isoformat() logger.debug("Ledger settled: %s", payment_hash[:12]) return entry return None def get_balance() -> dict: """Compute the current balance from settled and pending entries.""" incoming_total = sum( e.amount_sats for e in _entries if e.tx_type == TxType.incoming and e.status == TxStatus.settled ) outgoing_total = sum( e.amount_sats for e in _entries if e.tx_type == TxType.outgoing and e.status == TxStatus.settled ) fees = sum(e.fee_sats for e in _entries if e.status == TxStatus.settled) pending_in = sum( e.amount_sats for e in _entries if e.tx_type == TxType.incoming and e.status == TxStatus.pending ) pending_out = sum( e.amount_sats for e in _entries if e.tx_type == TxType.outgoing and e.status == TxStatus.pending ) net = incoming_total - outgoing_total - fees return { "incoming_total_sats": incoming_total, "outgoing_total_sats": outgoing_total, "fees_paid_sats": fees, "net_sats": net, "pending_incoming_sats": pending_in, "pending_outgoing_sats": pending_out, "available_sats": net - pending_out, } def get_transactions( tx_type: str | None = None, status: str | None = None, limit: int = 50, ) -> list[LedgerEntry]: """Return ledger entries, optionally filtered.""" result = _entries if tx_type: result = [e for e in result if e.tx_type.value == tx_type] if status: result = [e for e in result if e.status.value == status] return list(reversed(result))[:limit] def clear() -> None: """Reset the ledger (for testing).""" _entries.clear()