From 4f37c767d2f6d2028e4749d2f7b60be5fd9c67e7 Mon Sep 17 00:00:00 2001 From: Shroominic Date: Sun, 29 Jun 2025 11:11:24 -0300 Subject: [PATCH] fix wallet instance state management --- router/account.py | 7 ++- router/cashu.py | 112 +++++++++++++++++++++------------------------- router/main.py | 4 +- 3 files changed, 55 insertions(+), 68 deletions(-) diff --git a/router/account.py b/router/account.py index aee3f558..e87099d8 100644 --- a/router/account.py +++ b/router/account.py @@ -4,11 +4,10 @@ from fastapi import APIRouter, Depends, Header, HTTPException from .auth import validate_bearer_key from .cashu import ( - WALLET, - WALLET_LOCK, credit_balance, delete_key_if_zero_balance, refund_balance, + wallet, ) from .db import ApiKey, AsyncSession, get_session @@ -77,8 +76,8 @@ async def refund_wallet_endpoint( status_code=400, detail="Balance too small to refund (less than 1 sat)" ) - async with WALLET_LOCK: - token = await WALLET.send(remaining_balance_sats) + token = await wallet().send(remaining_balance_sats) + result = {"msats": remaining_balance_msats, "recipient": None, "token": token} # Only after successful refund, zero out the balance diff --git a/router/cashu.py b/router/cashu.py index f7dd857d..c4328a97 100644 --- a/router/cashu.py +++ b/router/cashu.py @@ -16,9 +16,19 @@ DEV_LN_ADDRESS = "routstr@minibits.cash" DEVS_DONATION_RATE = float(os.environ.get("DEVS_DONATION_RATE", 0.021)) # 2.1% NSEC = os.environ["NSEC"] # Nostr private key for the wallet -WALLET = Wallet(nsec=NSEC, mint_urls=[MINT]) -PAY_OUT_LOCK = asyncio.Lock() -WALLET_LOCK = asyncio.Lock() +WALLET: Wallet | None = None + + +async def init_wallet() -> None: + global WALLET + WALLET = await Wallet.create(nsec=NSEC) + + +def wallet() -> Wallet: + global WALLET + if WALLET is None: + raise ValueError("Wallet not initialized") + return WALLET async def delete_key_if_zero_balance(key: ApiKey, session: AsyncSession) -> None: @@ -28,63 +38,45 @@ async def delete_key_if_zero_balance(key: ApiKey, session: AsyncSession) -> None await session.commit() -async def init_wallet() -> None: - global WALLET - async with WALLET_LOCK: - WALLET = await Wallet.create(nsec=NSEC, mint_urls=[MINT]) - - -async def close_wallet() -> None: - global WALLET - async with WALLET_LOCK: - await WALLET.aclose() - - async def pay_out() -> None: """ Calculates the pay-out amount based on the spent balance, profit, and donation rate. """ - async with PAY_OUT_LOCK: - try: - from .db import create_session + try: + from .db import create_session - async with create_session() as session: - result = await session.exec( - select(func.sum(col(ApiKey.balance))).where(ApiKey.balance > 0) + async with create_session() as session: + result = await session.exec( + select(func.sum(col(ApiKey.balance))).where(ApiKey.balance > 0) + ) + balance = result.one_or_none() + if not balance: + # No balance to pay out - this is OK, not an error + return + + user_balance_sats = balance // 1000 + wallet_balance_sats = await wallet().get_balance() + + # Handle edge cases more gracefully + if wallet_balance_sats < user_balance_sats: + print( + f"Warning: Wallet balance ({wallet_balance_sats} sats) is less than user balance ({user_balance_sats} sats). Skipping payout." ) - balance = result.one_or_none() - if not balance: - # No balance to pay out - this is OK, not an error - return + return - user_balance_sats = balance // 1000 - async with WALLET_LOCK: - state = await WALLET.fetch_wallet_state() - wallet_balance_sats = state.balance + if (revenue := wallet_balance_sats - user_balance_sats) <= MINIMUM_PAYOUT: + # Not enough revenue yet - this is OK + return - # Handle edge cases more gracefully - if wallet_balance_sats < user_balance_sats: - print( - f"Warning: Wallet balance ({wallet_balance_sats} sats) is less than user balance ({user_balance_sats} sats). Skipping payout." - ) - return + devs_donation = int(revenue * DEVS_DONATION_RATE) + owners_draw = revenue - devs_donation - if ( - revenue := wallet_balance_sats - user_balance_sats - ) <= MINIMUM_PAYOUT: - # Not enough revenue yet - this is OK - return + # Send payouts + await wallet().send_to_lnurl(RECEIVE_LN_ADDRESS, owners_draw) + await wallet().send_to_lnurl(DEV_LN_ADDRESS, devs_donation) - devs_donation = int(revenue * DEVS_DONATION_RATE) - owners_draw = revenue - devs_donation - - # Send payouts - async with WALLET_LOCK: - await WALLET.send_to_lnurl(RECEIVE_LN_ADDRESS, owners_draw) - await WALLET.send_to_lnurl(DEV_LN_ADDRESS, devs_donation) - - except Exception as e: - print(f"Error in pay_out: {e}") + except Exception as e: + print(f"Error in pay_out: {e}") # Periodic payout task @@ -103,8 +95,7 @@ async def periodic_payout() -> None: async def credit_balance(cashu_token: str, key: ApiKey, session: AsyncSession) -> int: """Redeem a Cashu token and credit the amount to the API key balance.""" try: - async with WALLET_LOCK: - amount_sats, _ = await WALLET.redeem(cashu_token) + amount_sats, _ = await wallet().redeem(cashu_token) except Exception as e: print(f"Error in credit_balance: {e}") # Ensure the balance cannot become negative if redeem fails @@ -196,23 +187,20 @@ async def refund_balance(amount_msats: int, key: ApiKey, session: AsyncSession) if key.refund_address is None: raise ValueError("Refund address not set.") - async with WALLET_LOCK: - return await WALLET.send_to_lnurl( - key.refund_address, - amount=amount_sats, - ) + assert WALLET is not None, "Wallet not initialized" + return await WALLET.send_to_lnurl(key.refund_address, amount=amount_sats) async def x_cashu_refund(key: ApiKey, session: AsyncSession) -> str: - async with WALLET_LOCK: - refund_token = await WALLET.send(key.balance) + assert WALLET is not None, "Wallet not initialized" + refund_token = await WALLET.send(key.balance) await session.delete(key) await session.commit() return refund_token async def redeem(cashu_token: str, lnurl: str) -> int: - async with WALLET_LOCK: - amount_sats, _ = await WALLET.redeem(cashu_token) - await WALLET.send_to_lnurl(lnurl, amount=amount_sats) + assert WALLET is not None, "Wallet not initialized" + amount_sats, _ = await WALLET.redeem(cashu_token) + await WALLET.send_to_lnurl(lnurl, amount=amount_sats) return amount_sats diff --git a/router/main.py b/router/main.py index 80a646d8..4a163d22 100644 --- a/router/main.py +++ b/router/main.py @@ -8,7 +8,7 @@ from fastapi.middleware.cors import CORSMiddleware from .account import wallet_router from .admin import admin_router -from .cashu import check_for_refunds, close_wallet, init_wallet, periodic_payout +from .cashu import check_for_refunds, init_wallet, periodic_payout from .db import init_db from .discovery import providers_router from .models import MODELS, update_sats_pricing @@ -21,6 +21,7 @@ __version__ = "0.0.1" async def lifespan(_: FastAPI) -> AsyncGenerator[None, None]: await init_db() await init_wallet() + pricing_task = asyncio.create_task(update_sats_pricing()) refund_task = asyncio.create_task(check_for_refunds()) payout_task = asyncio.create_task(periodic_payout()) @@ -34,7 +35,6 @@ async def lifespan(_: FastAPI) -> AsyncGenerator[None, None]: await asyncio.gather( pricing_task, refund_task, payout_task, return_exceptions=True ) - await close_wallet() app = FastAPI(