Rename Castle Accounting extension to Libra

Full identifier rename: module path lnbits.extensions.castle →
lnbits.extensions.libra, DB ext_castle → ext_libra, URL prefix
/castle/ → /libra/, manifest id castle → libra, fava ledger slug
default castle-ledger → libra-ledger, Beancount source metadata
castle-api → libra-api and link prefixes castle-{entry,tx}- →
libra-{entry,tx}-, column castle_wallet_id → libra_wallet_id, all
Python/JS/HTML identifiers (castle_ext, CastleSettings,
castle_reference, castleWalletConfigured, etc.).

Display name "Castle Accounting" → "Libra" (the scales/balance
metaphor — fits double-entry bookkeeping).

No backward compat: production hosts will be force-updated. Old
castle-prefixed Beancount metadata in existing Fava ledgers is
historical; new entries use libra-* prefixes going forward.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
Padreug 2026-05-05 10:24:46 +02:00
commit c174cda48d
44 changed files with 953 additions and 953 deletions

View file

@ -1,5 +1,5 @@
"""
Background tasks for Castle accounting extension.
Background tasks for Libra accounting extension.
These tasks handle automated reconciliation checks and maintenance.
"""
@ -62,11 +62,11 @@ async def check_all_balance_assertions() -> dict:
# Log results
if results["failed"] > 0:
print(f"[CASTLE] Daily reconciliation check: {results['failed']} FAILED assertions!")
print(f"[LIBRA] Daily reconciliation check: {results['failed']} FAILED assertions!")
for failed in results["failed_assertions"]:
print(f" - Account {failed['account_id']}: expected {failed['expected_sats']}, got {failed['actual_sats']}")
else:
print(f"[CASTLE] Daily reconciliation check: All {results['passed']} assertions passed ✓")
print(f"[LIBRA] Daily reconciliation check: All {results['passed']} assertions passed ✓")
return results
@ -78,7 +78,7 @@ async def scheduled_daily_reconciliation():
This function is meant to be called by a scheduler (cron, systemd timer, etc.)
or by LNbits background task system.
"""
print(f"[CASTLE] Running scheduled daily reconciliation check at {datetime.now()}")
print(f"[LIBRA] Running scheduled daily reconciliation check at {datetime.now()}")
try:
results = await check_all_balance_assertions()
@ -86,38 +86,38 @@ async def scheduled_daily_reconciliation():
# TODO: Send notifications if there are failures
# This could send email, webhook, or in-app notification
if results["failed"] > 0:
print(f"[CASTLE] WARNING: {results['failed']} balance assertions failed!")
print(f"[LIBRA] WARNING: {results['failed']} balance assertions failed!")
# Future: Send alert notification
return results
except Exception as e:
print(f"[CASTLE] Error in scheduled reconciliation: {e}")
print(f"[LIBRA] Error in scheduled reconciliation: {e}")
raise
async def scheduled_account_sync():
"""
Scheduled task that runs hourly to sync accounts from Beancount to Castle DB.
Scheduled task that runs hourly to sync accounts from Beancount to Libra DB.
This ensures Castle DB stays in sync with Beancount (source of truth) by
automatically adding any new accounts created in Beancount to Castle's
This ensures Libra DB stays in sync with Beancount (source of truth) by
automatically adding any new accounts created in Beancount to Libra's
metadata database for permission tracking.
"""
from .account_sync import sync_accounts_from_beancount
logger.info(f"[CASTLE] Running scheduled account sync at {datetime.now()}")
logger.info(f"[LIBRA] Running scheduled account sync at {datetime.now()}")
try:
stats = await sync_accounts_from_beancount(force_full_sync=False)
if stats["accounts_added"] > 0:
logger.info(
f"[CASTLE] Account sync: Added {stats['accounts_added']} new accounts"
f"[LIBRA] Account sync: Added {stats['accounts_added']} new accounts"
)
if stats["errors"]:
logger.warning(
f"[CASTLE] Account sync: {len(stats['errors'])} errors encountered"
f"[LIBRA] Account sync: {len(stats['errors'])} errors encountered"
)
for error in stats["errors"][:5]: # Log first 5 errors
logger.error(f" - {error}")
@ -125,24 +125,24 @@ async def scheduled_account_sync():
return stats
except Exception as e:
logger.error(f"[CASTLE] Error in scheduled account sync: {e}")
logger.error(f"[LIBRA] Error in scheduled account sync: {e}")
raise
async def wait_for_account_sync():
"""
Background task that periodically syncs accounts from Beancount to Castle DB.
Background task that periodically syncs accounts from Beancount to Libra DB.
Runs hourly to ensure Castle DB stays in sync with Beancount.
Runs hourly to ensure Libra DB stays in sync with Beancount.
"""
logger.info("[CASTLE] Account sync background task started")
logger.info("[LIBRA] Account sync background task started")
while True:
try:
# Run sync
await scheduled_account_sync()
except Exception as e:
logger.error(f"[CASTLE] Account sync error: {e}")
logger.error(f"[LIBRA] Account sync error: {e}")
# Wait 1 hour before next sync
await asyncio.sleep(3600) # 3600 seconds = 1 hour
@ -157,9 +157,9 @@ def start_daily_reconciliation_task():
For cron setup:
# Run daily at 2 AM
0 2 * * * curl -X POST http://localhost:5000/castle/api/v1/tasks/daily-reconciliation -H "X-Api-Key: YOUR_ADMIN_KEY"
0 2 * * * curl -X POST http://localhost:5000/libra/api/v1/tasks/daily-reconciliation -H "X-Api-Key: YOUR_ADMIN_KEY"
"""
print("[CASTLE] Daily reconciliation task registered")
print("[LIBRA] Daily reconciliation task registered")
# In a production system, you would register this with LNbits task scheduler
# For now, it can be triggered manually via API endpoint
@ -173,7 +173,7 @@ async def wait_for_paid_invoices():
before the payment is detected by client-side polling.
"""
invoice_queue = Queue()
register_invoice_listener(invoice_queue, "ext_castle")
register_invoice_listener(invoice_queue, "ext_libra")
while True:
payment = await invoice_queue.get()
@ -182,10 +182,10 @@ async def wait_for_paid_invoices():
async def on_invoice_paid(payment: Payment) -> None:
"""
Handle a paid Castle invoice by automatically submitting to Fava.
Handle a paid Libra invoice by automatically submitting to Fava.
This function is called automatically when any invoice on the Castle wallet
is paid. It checks if the invoice is a Castle payment and records it in
This function is called automatically when any invoice on the Libra wallet
is paid. It checks if the invoice is a Libra payment and records it in
Beancount via Fava.
Concurrency Protection:
@ -194,13 +194,13 @@ async def on_invoice_paid(payment: Payment) -> None:
- Uses idempotent entry creation to prevent duplicate entries even if
the same payment is processed multiple times
"""
# Only process Castle-specific payments
if not payment.extra or payment.extra.get("tag") != "castle":
# Only process Libra-specific payments
if not payment.extra or payment.extra.get("tag") != "libra":
return
user_id = payment.extra.get("user_id")
if not user_id:
logger.warning(f"Castle invoice {payment.payment_hash} missing user_id in metadata")
logger.warning(f"Libra invoice {payment.payment_hash} missing user_id in metadata")
return
from .fava_client import get_fava_client
@ -216,7 +216,7 @@ async def on_invoice_paid(payment: Payment) -> None:
user_lock = fava.get_user_lock(user_id)
async with user_lock:
logger.info(f"Recording Castle payment {payment.payment_hash} for user {user_id[:8]} to Fava")
logger.info(f"Recording Libra payment {payment.payment_hash} for user {user_id[:8]} to Fava")
try:
from decimal import Decimal
@ -246,14 +246,14 @@ async def on_invoice_paid(payment: Payment) -> None:
total_fiat_balance = fiat_balances.get(fiat_currency, Decimal(0))
# Determine receivables and payables based on balance
# Positive balance = user owes castle (receivable)
# Negative balance = castle owes user (payable)
# Positive balance = user owes libra (receivable)
# Negative balance = libra owes user (payable)
if total_fiat_balance > 0:
# User owes castle
# User owes libra
total_receivable = total_fiat_balance
total_payable = Decimal(0)
else:
# Castle owes user
# Libra owes user
total_receivable = Decimal(0)
total_payable = abs(total_fiat_balance)
@ -318,5 +318,5 @@ async def on_invoice_paid(payment: Payment) -> None:
)
except Exception as e:
logger.error(f"Error recording Castle payment {payment.payment_hash}: {e}")
logger.error(f"Error recording Libra payment {payment.payment_hash}: {e}")
raise