Remove remaining child-key cleanup residue

This commit is contained in:
9qeklajc
2026-09-03 22:15:27 +02:00
parent 6c0b45dd98
commit 7aff25ac68
12 changed files with 16 additions and 41 deletions
-16
View File
@@ -371,22 +371,6 @@ Content-Type: application/json
## Monitoring
### Usage Alerts
Set up usage notifications:
```bash
POST /v1/wallet/alerts
Authorization: Bearer sk-...
Content-Type: application/json
{
"low_balance_threshold": 1000,
"daily_spend_limit": 5000,
"webhook_url": "https://your-app.com/webhook"
}
```
### Audit Logging
All API key usage is logged:
+3 -3
View File
@@ -206,7 +206,7 @@ async def _validate_bearer_key_locked(
Validates the provided API key using SQLModel.
If it's a cashu key, it redeems it and stores its hash and balance.
Otherwise checks if the hash of the key exists.
Includes a balance check against min_cost for limited keys.
Checks the key's available balance against min_cost when required.
"""
logger.debug(
"Starting bearer key validation",
@@ -1347,8 +1347,8 @@ async def adjust_payment_for_tokens(
# actual cost exceeded discounted reservation (due to tolerance_percentage)
if cost_difference > 0:
# Lock the billing row so the parent and child record the same
# database-determined charge under concurrent finalizations.
# Lock the key row so concurrent finalizations use the same
# database-determined charge.
actual_charge_msats = 0
for attempt in range(5):
locked_billing_key = (
+1 -1
View File
@@ -111,7 +111,7 @@ async def get_temporary_balances_api(
)
total = count_result.one()
# Aggregate totals across the whole (search-filtered) set, not just the
# Aggregate totals across the whole search-filtered set, not just this page.
balance_totals_result = await session.exec(
select(
func.coalesce(func.sum(ApiKey.balance), 0),
-5
View File
@@ -63,7 +63,6 @@ async def lifespan(_: FastAPI) -> AsyncGenerator[None, None]:
models_refresh_task = None
model_maps_refresh_task = None
model_paths_refresh_task = None
key_reset_task = None
stale_reservation_task = None
dead_key_prune_task = None
auto_topup_task = None
@@ -187,8 +186,6 @@ async def lifespan(_: FastAPI) -> AsyncGenerator[None, None]:
model_maps_refresh_task.cancel()
if model_paths_refresh_task is not None:
model_paths_refresh_task.cancel()
if key_reset_task is not None:
key_reset_task.cancel()
if stale_reservation_task is not None:
stale_reservation_task.cancel()
if dead_key_prune_task is not None:
@@ -222,8 +219,6 @@ async def lifespan(_: FastAPI) -> AsyncGenerator[None, None]:
tasks_to_wait.append(model_maps_refresh_task)
if model_paths_refresh_task is not None:
tasks_to_wait.append(model_paths_refresh_task)
if key_reset_task is not None:
tasks_to_wait.append(key_reset_task)
if stale_reservation_task is not None:
tasks_to_wait.append(stale_reservation_task)
if dead_key_prune_task is not None:
+2 -2
View File
@@ -3,8 +3,8 @@
Every finalization branch of ``adjust_payment_for_tokens`` must respect the
same accounting rules: a completed request is charged exactly once, its
reported ``charged_msats`` matches the actual debit, it never spends more than
its own reservation leaves available, and child keys spend their parent's
balance without raiding sibling reservations.
its own reservation leaves available, and concurrent requests cannot raid each
other's reservations.
"""
import asyncio
@@ -45,7 +45,7 @@ def _dead_key(created_at: int | None) -> ApiKey:
@pytest.mark.asyncio
async def test_prunes_old_refunded_zero_key(patched_db_engine: None) -> None:
"""A funded-then-refunded key (0/0/0, NULL parent, old) is pruned."""
"""A funded-then-refunded zero-balance key with an old timestamp is pruned."""
key = _dead_key(LONG_AGO)
async with create_session() as session:
session.add(key)
@@ -129,7 +129,7 @@ async def test_temporary_balances_totals(
) -> None:
await _add_key(
integration_session,
"parent",
"standalone_key",
balance=5000,
total_spent=100,
total_requests=3,
+2 -5
View File
@@ -337,16 +337,13 @@ async def test_topup_during_active_proxy_request( # type: ignore[no-untyped-def
@pytest.mark.integration
@pytest.mark.asyncio
async def test_maximum_balance_limits( # type: ignore[no-untyped-def]
async def test_large_balance_topup_is_allowed( # type: ignore[no-untyped-def]
integration_client: AsyncClient,
authenticated_client: AsyncClient,
testmint_wallet: Any,
integration_session,
) -> None:
"""Test if there are any maximum balance limits"""
# Note: The current implementation doesn't enforce maximum balance limits
# This test verifies large balances are handled correctly
"""Test that a large top-up is accepted and reflected in the balance."""
# Get current balance
response = await authenticated_client.get("/v1/wallet/")
+3 -3
View File
@@ -134,11 +134,11 @@ async def test_unmeasured_ehbp_releases_reservation(
@pytest.mark.asyncio
async def test_finalize_actual_cost_payment_rolls_back_when_parent_update_matches_no_rows(
async def test_finalize_actual_cost_payment_rolls_back_when_billing_key_update_matches_no_rows(
session: AsyncSession,
monkeypatch: pytest.MonkeyPatch,
) -> None:
key = ApiKey(hashed_key="ehbp-missing-parent", balance=10_000)
key = ApiKey(hashed_key="ehbp-failed-billing-update", balance=10_000)
session.add(key)
await session.commit()
await pay_for_request(key, 3_000, session)
@@ -158,7 +158,7 @@ async def test_finalize_actual_cost_payment_rolls_back_when_parent_update_matche
assert charged == 0
rollback_spy.assert_awaited_once()
updated = await _api_key(session, "ehbp-missing-parent")
updated = await _api_key(session, "ehbp-failed-billing-update")
assert updated is not None
assert updated.balance == 10_000
assert updated.reserved_balance == 0
-2
View File
@@ -41,8 +41,6 @@ def _invoice(**overrides: object) -> SimpleNamespace:
"paid_at": None,
"api_key_hash": None,
"mint_url": "http://mint:3338",
"balance_limit": None,
"balance_limit_reset": None,
"validity_date": None,
"created_at": 1,
"expires_at": 2,
+1 -1
View File
@@ -1,7 +1,7 @@
"""Tests for stale reserved_balance handling (issue #551).
Covers:
- pay_for_request stamping reserved_at on billing and child keys
- pay_for_request stamping reserved_at on charged keys
- release_stale_reservations sweeper semantics
- reset_all_reserved_balances clearing reserved_at
- refund endpoint self-healing stale/legacy reservations
@@ -245,7 +245,8 @@ export function CashuPaymentWorkflow({
<span className='text-muted-foreground text-[0.7rem] leading-relaxed'>
Redeems instantly and returns <code>sk-</code> key.
<br />
Optional limits can be set above for enhanced security.
Set an optional validity date above to expire the key
automatically.
</span>
</div>
</div>