forked from Rockachopa/Timmy-time-dashboard
151 lines
4.1 KiB
Python
151 lines
4.1 KiB
Python
"""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):
|
|
"""Lightning transaction direction type."""
|
|
|
|
incoming = "incoming"
|
|
outgoing = "outgoing"
|
|
|
|
|
|
class TxStatus(StrEnum):
|
|
"""Lightning transaction settlement status."""
|
|
|
|
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()
|