fix nip60 wallet handling

This commit is contained in:
Shroominic
2025-06-05 15:01:25 +02:00
parent c31f312505
commit b4af5f0d17
4 changed files with 70 additions and 65 deletions
+3 -5
View File
@@ -3,11 +3,10 @@ from datetime import datetime, timezone
from fastapi import APIRouter, Request
from fastapi.responses import HTMLResponse
from sixty_nuts import Wallet
from sqlmodel import select
from .db import ApiKey, create_session
from .cashu import NSEC, MINT
from .cashu import WALLET
admin_router = APIRouter(prefix="/admin")
@@ -112,9 +111,8 @@ async def dashboard(request: Request) -> str:
# Calculate the total balance of all API keys
total_user_balance = int(sum(key.balance / 1000 for key in api_keys))
# Fetch balance from cashu
async with Wallet(nsec=NSEC, mint_urls=[MINT]) as wallet:
current_balance = (await wallet.fetch_wallet_state()).balance
owner_balance = current_balance - total_user_balance
current_balance = (await WALLET.fetch_wallet_state()).balance
owner_balance = current_balance - total_user_balance
return f"""<!DOCTYPE html>
<html>
+46 -38
View File
@@ -15,6 +15,15 @@ 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])
async def init_wallet():
global WALLET
WALLET = await Wallet.create(nsec=NSEC, mint_urls=[MINT])
async def close_wallet():
global WALLET
await WALLET.aclose()
async def pay_out() -> None:
"""
@@ -34,9 +43,8 @@ async def pay_out() -> None:
return
user_balance_sats = balance // 1000
async with Wallet(nsec=NSEC, mint_urls=[MINT]) as wallet:
state = await wallet.fetch_wallet_state()
wallet_balance_sats = state.balance
state = await WALLET.fetch_wallet_state()
wallet_balance_sats = state.balance
# Handle edge cases more gracefully
if wallet_balance_sats < user_balance_sats:
@@ -53,9 +61,8 @@ async def pay_out() -> None:
owners_draw = revenue - devs_donation
# Send payouts
async with Wallet(nsec=NSEC, mint_urls=[MINT]) as wallet:
await wallet.send_to_lnurl(RECEIVE_LN_ADDRESS, owners_draw)
await wallet.send_to_lnurl(DEV_LN_ADDRESS, devs_donation)
await WALLET.send_to_lnurl(RECEIVE_LN_ADDRESS, owners_draw)
await WALLET.send_to_lnurl(DEV_LN_ADDRESS, devs_donation)
except Exception as e:
# Log the error but don't crash - payouts can be retried later
@@ -63,15 +70,14 @@ async def pay_out() -> None:
async def credit_balance(cashu_token: str, key: ApiKey, session: AsyncSession) -> int:
async with Wallet(nsec=NSEC, mint_urls=[MINT]) as wallet:
state_before = await wallet.fetch_wallet_state()
await wallet.redeem(cashu_token)
state_after = await wallet.fetch_wallet_state()
amount = (state_after.balance - state_before.balance) * 1000
key.balance += amount
session.add(key)
await session.commit()
return amount
state_before = await WALLET.fetch_wallet_state()
await WALLET.redeem(cashu_token)
state_after = await WALLET.fetch_wallet_state()
amount = (state_after.balance - state_before.balance) * 1000
key.balance += amount
session.add(key)
await session.commit()
return amount
async def check_for_refunds() -> None:
@@ -81,7 +87,6 @@ async def check_for_refunds() -> None:
Raises:
Exception: If an error occurs during the refund check process.
"""
raise Exception("TODO migrate to sixty-nuts")
# Setting REFUND_PROCESSING_INTERVAL to 0 disables it
if REFUND_PROCESSING_INTERVAL == 0:
print("Automatic refund processing is disabled.")
@@ -112,31 +117,34 @@ async def check_for_refunds() -> None:
print(f"Error during refund check: {e}")
async def refund_balance(amount: int, key: ApiKey, session: AsyncSession) -> int:
async with Wallet(nsec=NSEC, mint_urls=[MINT]) as wallet:
if key.balance < amount:
raise ValueError("Insufficient balance.")
if amount <= 0:
amount = key.balance
async def refund_balance(amount_msats: int, key: ApiKey, session: AsyncSession) -> int:
if key.balance < amount_msats:
raise ValueError("Insufficient balance.")
if amount_msats <= 0:
amount_msats = key.balance
key.balance -= amount
session.add(key)
await session.commit()
# Convert msats to sats for cashu wallet
amount_sats = amount_msats // 1000
if amount_sats == 0:
raise ValueError("Amount too small to refund (less than 1 sat)")
if key.refund_address is None:
raise ValueError("Refund address not set.")
key.balance -= amount_msats
session.add(key)
await session.commit()
return await wallet.send_to_lnurl(
key.refund_address,
amount=amount,
)
if key.refund_address is None:
raise ValueError("Refund address not set.")
return await WALLET.send_to_lnurl(
key.refund_address,
amount=amount_sats,
)
async def redeem(cashu_token: str, lnurl: str) -> int:
async with Wallet(nsec=NSEC, mint_urls=[MINT]) as wallet:
state_before = await wallet.fetch_wallet_state()
await wallet.redeem(cashu_token)
state_after = await wallet.fetch_wallet_state()
amount = state_after.balance - state_before.balance
await wallet.send_to_lnurl(lnurl, amount=amount)
return amount
state_before = await WALLET.fetch_wallet_state()
await WALLET.redeem(cashu_token)
state_after = await WALLET.fetch_wallet_state()
amount = state_after.balance - state_before.balance
await WALLET.send_to_lnurl(lnurl, amount=amount)
return amount
+17 -11
View File
@@ -1,4 +1,5 @@
import asyncio
from contextlib import asynccontextmanager
import os
from fastapi import FastAPI
from fastapi.middleware.cors import CORSMiddleware
@@ -6,19 +7,31 @@ from fastapi.middleware.cors import CORSMiddleware
from .db import init_db
from .admin import admin_router
from .proxy import proxy_router
from .account import account_router
from .account import wallet_router
from .models import MODELS, update_sats_pricing
from .cashu import check_for_refunds
from .cashu import check_for_refunds, init_wallet, close_wallet
from .discovery import providers_router
__version__ = "0.0.1"
@asynccontextmanager
async def lifespan(_: FastAPI):
await init_db()
await init_wallet()
asyncio.create_task(update_sats_pricing())
asyncio.create_task(check_for_refunds())
yield
await close_wallet()
app = FastAPI(
version=__version__,
title=os.environ.get("NAME", "ARoutstrNode" + __version__),
description=os.environ.get("DESCRIPTION", "A Routstr Node"),
contact={"name": os.environ.get("NAME", ""), "npub": os.environ.get("NPUB", "")},
lifespan=lifespan,
)
# Configure CORS
@@ -46,13 +59,6 @@ async def info():
app.include_router(admin_router)
app.include_router(account_router)
app.include_router(wallet_router)
app.include_router(providers_router)
app.include_router(proxy_router)
@app.on_event("startup")
async def startup_event():
await init_db()
asyncio.create_task(update_sats_pricing())
asyncio.create_task(check_for_refunds())
+4 -11
View File
@@ -116,17 +116,10 @@ async def test_refund_balance_without_address(
test_session.add(key)
await test_session.commit()
# Mock the Wallet class at the router.account module level
with patch("router.account.Wallet") as mock_wallet_class:
# Create a mock wallet instance
mock_wallet = AsyncMock()
mock_wallet.__aenter__ = AsyncMock(return_value=mock_wallet)
mock_wallet.__aexit__ = AsyncMock(return_value=None)
# Mock the WALLET instance at the router.account module level
with patch("router.account.WALLET") as mock_wallet:
mock_wallet.send = AsyncMock(return_value="cashuBqQSEQ...")
# Make the Wallet class return our mock when instantiated
mock_wallet_class.return_value = mock_wallet
response = await async_client.post(
"/v1/wallet/refund", headers={"Authorization": f"Bearer sk-{api_key}"}
)
@@ -138,8 +131,8 @@ async def test_refund_balance_without_address(
assert data["msats"] == 500000
assert data["token"] == "cashuBqQSEQ..."
# Verify wallet.send was called with the correct amount
mock_wallet.send.assert_called_once_with(500000)
# Verify wallet.send was called with the correct amount (msats converted to sats)
mock_wallet.send.assert_called_once_with(500)
@pytest.mark.asyncio