Merge PR #621: fix: propagate Cashu storage errors

This commit is contained in:
thefux
2026-07-20 18:10:15 +00:00
7 changed files with 130 additions and 31 deletions
+19 -9
View File
@@ -434,15 +434,25 @@ async def withdraw(
token = await send_token(
withdraw_request.amount, withdraw_request.unit, effective_mint
)
await store_cashu_transaction(
token=token,
amount=withdraw_request.amount,
unit=withdraw_request.unit,
mint_url=effective_mint,
typ="out",
collected=False,
source="admin",
)
try:
await store_cashu_transaction(
token=token,
amount=withdraw_request.amount,
unit=withdraw_request.unit,
mint_url=effective_mint,
typ="out",
collected=False,
source="admin",
)
except Exception:
logger.critical(
"Admin withdrawal token issued without a persisted audit record",
extra={
"amount": withdraw_request.amount,
"unit": withdraw_request.unit,
"mint_url": effective_mint,
},
)
return {"token": token}
+7 -6
View File
@@ -304,13 +304,14 @@ async def store_cashu_transaction(
)
session.add(tx)
await session.commit()
return True
except Exception as e:
logger.warning(
f"Failed to store cashu transaction: {e} (type={typ})",
extra={"error": str(e), "type": typ},
except Exception:
logger.critical(
"Failed to store Cashu transaction",
extra={"type": typ, "request_id": request_id, "source": source},
exc_info=True,
)
return False
raise
return True
class UpstreamProviderRow(SQLModel, table=True): # type: ignore
+11 -10
View File
@@ -142,16 +142,17 @@ async def _check_and_topup(row: UpstreamProviderRow) -> None:
)
return
stored = await store_cashu_transaction(
token=token,
amount=amount,
unit="sat",
mint_url=mint_url,
typ="out",
collected=False,
source="auto_topup",
)
if not stored:
try:
await store_cashu_transaction(
token=token,
amount=amount,
unit="sat",
mint_url=mint_url,
typ="out",
collected=False,
source="auto_topup",
)
except Exception:
logger.critical(
"Aborting auto top-up because its cashu token could not be persisted",
extra={"provider_id": row.id, "mint_url": mint_url},
+5 -5
View File
@@ -745,11 +745,11 @@ async def credit_balance(
)
except Exception:
pass
logger.debug(
"Cashu token successfully redeemed and stored",
extra={"amount": amount, "unit": unit, "mint_url": mint_url},
)
else:
logger.debug(
"Cashu token successfully redeemed and stored",
extra={"amount": amount, "unit": unit, "mint_url": mint_url},
)
return amount
except Exception as e:
logger.error(
+31
View File
@@ -49,3 +49,34 @@ async def test_withdraw_uses_effective_mint_and_records_outgoing_transaction(
collected=False,
source="admin",
)
@pytest.mark.asyncio
async def test_withdraw_returns_issued_token_when_audit_storage_fails(
monkeypatch: pytest.MonkeyPatch,
) -> None:
mint = "https://primary.example"
proofs = [SimpleNamespace(amount=100)]
token = "cashuBrecoverable"
monkeypatch.setattr(admin, "get_wallet", AsyncMock(return_value=object()))
monkeypatch.setattr(
admin, "get_proofs_per_mint_and_unit", Mock(return_value=proofs)
)
monkeypatch.setattr(
admin, "slow_filter_spend_proofs", AsyncMock(return_value=proofs)
)
monkeypatch.setattr(admin, "send_token", AsyncMock(return_value=token))
monkeypatch.setattr(
admin,
"store_cashu_transaction",
AsyncMock(side_effect=RuntimeError("database unavailable")),
)
critical = Mock()
monkeypatch.setattr(admin.logger, "critical", critical)
monkeypatch.setattr(admin.settings, "primary_mint", mint)
result = await admin.withdraw(Mock(), admin.WithdrawRequest(amount=75))
assert result == {"token": token}
critical.assert_called_once()
+1 -1
View File
@@ -136,7 +136,7 @@ async def test_auto_topup_does_not_send_untracked_token() -> None:
),
patch(
"routstr.upstream.auto_topup.store_cashu_transaction",
AsyncMock(return_value=False),
AsyncMock(side_effect=RuntimeError("database unavailable")),
),
):
await _check_and_topup(_row())
@@ -0,0 +1,56 @@
from unittest.mock import AsyncMock, patch
import pytest
from routstr.core.db import store_cashu_transaction
@pytest.mark.asyncio
@pytest.mark.parametrize(
"error",
[
OSError("disk full"),
RuntimeError("connection lost"),
ConnectionRefusedError("database unavailable"),
],
)
async def test_store_cashu_transaction_propagates_commit_errors(
error: Exception,
) -> None:
session = AsyncMock()
session.commit.side_effect = error
session.__aenter__.return_value = session
session.__aexit__.return_value = None
with (
patch("routstr.core.db.create_session", return_value=session),
patch("routstr.core.db.logger.critical") as critical,
):
with pytest.raises(type(error), match=str(error)):
await store_cashu_transaction(
token="cashuAtest",
amount=1_000,
unit="sat",
mint_url="https://mint.example",
typ="out",
request_id="request-1",
)
critical.assert_called_once()
@pytest.mark.asyncio
async def test_store_cashu_transaction_returns_true_after_commit() -> None:
session = AsyncMock()
session.__aenter__.return_value = session
session.__aexit__.return_value = None
with patch("routstr.core.db.create_session", return_value=session):
stored = await store_cashu_transaction(
token="cashuAtest",
amount=1_000,
unit="sat",
)
assert stored is True
session.commit.assert_awaited_once()