Merge pull request #644 from Routstr/fix/api-key-refund-token-retrieval-main

fix: return persisted API-key refund token
This commit is contained in:
9qeklajc
2026-07-31 01:25:18 +02:00
committed by GitHub
2 changed files with 104 additions and 0 deletions
+32
View File
@@ -259,6 +259,36 @@ async def _lookup_key_no_create(
return None
async def _get_persisted_api_key_refund(
key: ApiKey, session: AsyncSession
) -> dict[str, str] | None:
result = await session.exec(
select(CashuTransaction)
.where(
CashuTransaction.api_key_hashed_key == key.hashed_key,
CashuTransaction.type == "out",
CashuTransaction.source == "apikey",
)
.order_by(col(CashuTransaction.created_at).desc())
)
refund = result.first()
if refund is None:
return None
if refund.swept:
raise HTTPException(status_code=410, detail="Refund has been swept")
refund.collected = True
session.add(refund)
await session.commit()
persisted = {"token": refund.token}
if refund.unit == "sat":
persisted["sats"] = str(refund.amount)
else:
persisted["msats"] = str(refund.amount)
return persisted
async def _restore_balance(
session: AsyncSession, hashed_key: str, balance: int, reserved_balance: int, mint_url: str
) -> None:
@@ -353,6 +383,8 @@ async def refund_wallet_endpoint(
if key.total_balance <= 0:
if cached := await _refund_cache_get(bearer_value):
return cached
if persisted := await _get_persisted_api_key_refund(key, session):
return persisted
if key.parent_key_hash:
raise HTTPException(
+72
View File
@@ -221,6 +221,78 @@ def _make_api_key(
return key
@pytest.mark.asyncio
async def test_apikey_refund_returns_persisted_token_after_cache_loss() -> None:
key = _make_api_key(balance=0, refund_currency="sat")
refund_token = "cashuApersisted_refund_token"
refund_tx = _make_cashu_tx(
token=refund_token,
amount=5,
unit="sat",
type="out",
request_id=None,
)
refund_tx.source = "apikey"
refund_tx.api_key_hashed_key = key.hashed_key
session = MagicMock()
session.get = AsyncMock(return_value=key)
session.exec = AsyncMock(return_value=_exec_result(refund_tx))
session.add = MagicMock()
session.commit = AsyncMock()
with (
patch("routstr.balance._refund_cache_get", AsyncMock(return_value=None)),
patch("routstr.balance.send_token", AsyncMock()) as mock_send_token,
):
result = await refund_wallet_endpoint(
authorization="Bearer sk-testhash",
x_cashu=None,
session=session,
)
assert result == {"token": refund_token, "sats": "5"}
assert refund_tx.collected is True
session.add.assert_called_once_with(refund_tx)
session.commit.assert_awaited_once()
mock_send_token.assert_not_awaited()
@pytest.mark.asyncio
async def test_apikey_refund_rejects_persisted_token_after_sweep() -> None:
from fastapi import HTTPException
key = _make_api_key(balance=0, refund_currency="sat")
refund_tx = _make_cashu_tx(
token="cashuAswept_apikey_refund",
amount=5,
unit="sat",
request_id=None,
swept=True,
)
refund_tx.source = "apikey"
refund_tx.api_key_hashed_key = key.hashed_key
session = MagicMock()
session.get = AsyncMock(return_value=key)
session.exec = AsyncMock(return_value=_exec_result(refund_tx))
session.add = MagicMock()
session.commit = AsyncMock()
with patch("routstr.balance._refund_cache_get", AsyncMock(return_value=None)):
with pytest.raises(HTTPException) as exc_info:
await refund_wallet_endpoint(
authorization="Bearer sk-testhash",
x_cashu=None,
session=session,
)
assert exc_info.value.status_code == 410
assert exc_info.value.detail == "Refund has been swept"
session.add.assert_not_called()
session.commit.assert_not_awaited()
@pytest.mark.asyncio
async def test_apikey_refund_stores_cashu_transaction_with_apikey_source() -> None:
key = _make_api_key(balance=5000, refund_currency="sat")