mirror of
https://github.com/Routstr/routstr-core.git
synced 2026-10-05 20:28:23 +00:00
Remove remaining child-key cleanup residue
This commit is contained in:
@@ -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
@@ -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 = (
|
||||
|
||||
@@ -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),
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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/")
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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,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>
|
||||
|
||||
Reference in New Issue
Block a user