mirror of
https://github.com/Routstr/routstr-core.git
synced 2026-08-09 11:04:36 +00:00
fix wallet instance state management
This commit is contained in:
+3
-4
@@ -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
|
||||
|
||||
+50
-62
@@ -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
|
||||
|
||||
+2
-2
@@ -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(
|
||||
|
||||
Reference in New Issue
Block a user