diff --git a/routstr/balance.py b/routstr/balance.py index 03ddf36c..91b19ce5 100644 --- a/routstr/balance.py +++ b/routstr/balance.py @@ -109,13 +109,19 @@ async def account_info( # Note: validate_bearer_key already supports refund_address and key_expiry_time params -@router.get("/create") -async def create_balance( +class BalanceCreateRequest(BaseModel): + initial_balance_token: str + balance_limit: int | None = None + balance_limit_reset: str | None = None + validity_date: int | None = None + + +async def _create_balance( initial_balance_token: str, - balance_limit: int | None = None, - balance_limit_reset: str | None = None, - validity_date: int | None = None, - session: AsyncSession = Depends(get_session), + balance_limit: int | None, + balance_limit_reset: str | None, + validity_date: int | None, + session: AsyncSession, ) -> dict: key = await validate_bearer_key(initial_balance_token, session) @@ -135,6 +141,37 @@ async def create_balance( } +@router.post("/create") +async def create_balance_from_body( + payload: BalanceCreateRequest, + session: AsyncSession = Depends(get_session), +) -> dict: + return await _create_balance( + payload.initial_balance_token, + payload.balance_limit, + payload.balance_limit_reset, + payload.validity_date, + session, + ) + + +@router.get("/create") +async def create_balance( + initial_balance_token: str, + balance_limit: int | None = None, + balance_limit_reset: str | None = None, + validity_date: int | None = None, + session: AsyncSession = Depends(get_session), +) -> dict: + return await _create_balance( + initial_balance_token, + balance_limit, + balance_limit_reset, + validity_date, + session, + ) + + @router.get("/info") async def wallet_info( key: ApiKey = Depends(get_key_from_header), diff --git a/tests/unit/test_balance_create_api.py b/tests/unit/test_balance_create_api.py new file mode 100644 index 00000000..e1f70f5b --- /dev/null +++ b/tests/unit/test_balance_create_api.py @@ -0,0 +1,40 @@ +from types import SimpleNamespace +from unittest.mock import AsyncMock + +import pytest +from fastapi import FastAPI +from httpx import ASGITransport, AsyncClient + +from routstr import balance as balance_module +from routstr.core.db import get_session + + +@pytest.mark.asyncio +async def test_create_balance_accepts_large_cashu_token_in_post_body( + monkeypatch: pytest.MonkeyPatch, +) -> None: + token = "cashuA" + "x" * 20_000 + key = SimpleNamespace(hashed_key="hashed", balance=123_000) + validate_bearer_key = AsyncMock(return_value=key) + session = AsyncMock() + monkeypatch.setattr(balance_module, "validate_bearer_key", validate_bearer_key) + + async def override_get_session(): # type: ignore[no-untyped-def] + yield session + + app = FastAPI() + app.include_router(balance_module.balance_router) + app.dependency_overrides[get_session] = override_get_session + + async with AsyncClient( + transport=ASGITransport(app=app), # type: ignore[arg-type] + base_url="http://test", + ) as client: + response = await client.post( + "/v1/balance/create", + json={"initial_balance_token": token}, + ) + + assert response.status_code == 200 + assert response.json() == {"api_key": "sk-hashed", "balance": 123_000} + validate_bearer_key.assert_awaited_once_with(token, session) diff --git a/ui/components/landing/cashu-payment-workflow.tsx b/ui/components/landing/cashu-payment-workflow.tsx index e2244ea0..0835269c 100644 --- a/ui/components/landing/cashu-payment-workflow.tsx +++ b/ui/components/landing/cashu-payment-workflow.tsx @@ -91,25 +91,27 @@ export function CashuPaymentWorkflow({ setIsCreatingKey(true); try { - const params = new URLSearchParams({ + const requestPayload: { + initial_balance_token: string; + balance_limit?: number; + balance_limit_reset?: string; + validity_date?: number; + } = { initial_balance_token: initialToken.trim(), - }); - if (balanceLimit) params.append('balance_limit', balanceLimit); + }; + if (balanceLimit) requestPayload.balance_limit = Number(balanceLimit); if (balanceLimitReset) - params.append('balance_limit_reset', balanceLimitReset); + requestPayload.balance_limit_reset = balanceLimitReset; if (validityDate) { - const timestamp = Math.floor( + requestPayload.validity_date = Math.floor( new Date(validityDate + 'T23:59:59').getTime() / 1000 ); - params.append('validity_date', timestamp.toString()); } - const response = await fetch( - `${baseUrl}/v1/balance/create?${params.toString()}`, - { - method: 'GET', - headers: { 'Content-Type': 'application/json' }, - } - ); + const response = await fetch(`${baseUrl}/v1/balance/create`, { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify(requestPayload), + }); if (!response.ok) { const errorText = await response.text(); throw new Error(errorText || 'Failed to create API key'); diff --git a/ui/components/providers/RoutstrCreateKeySection.tsx b/ui/components/providers/RoutstrCreateKeySection.tsx index 2fecf5eb..2741a0a6 100644 --- a/ui/components/providers/RoutstrCreateKeySection.tsx +++ b/ui/components/providers/RoutstrCreateKeySection.tsx @@ -161,13 +161,11 @@ export function RoutstrCreateKeySection({ setIsCreatingCashu(true); try { - const params = new URLSearchParams({ - initial_balance_token: cashuToken.trim(), + const resp = await fetch(`${cleanUrl}/v1/balance/create`, { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ initial_balance_token: cashuToken.trim() }), }); - const resp = await fetch( - `${cleanUrl}/v1/balance/create?${params.toString()}`, - { method: 'GET', headers: { 'Content-Type': 'application/json' } } - ); if (!resp.ok) { const errorText = await resp.text();