mirror of
https://github.com/Routstr/routstr-core.git
synced 2026-07-31 15:56:14 +00:00
Compare commits
1
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
dc0e56e450 |
+37
-10
@@ -259,11 +259,26 @@ async def refund_wallet_endpoint(
|
||||
)
|
||||
)
|
||||
in_tx = in_tx_result.first()
|
||||
cashu_fingerprint = hashlib.sha256(x_cashu.encode()).hexdigest()[:16]
|
||||
if in_tx is None:
|
||||
logger.info(
|
||||
"refund_wallet_endpoint: x-cashu inbound transaction not found",
|
||||
extra={"cashu_fingerprint": cashu_fingerprint},
|
||||
)
|
||||
raise HTTPException(status_code=404, detail="Refund not found")
|
||||
|
||||
# Use the request_id to find the associated "out" (refund) transaction
|
||||
if in_tx.request_id is None:
|
||||
logger.info(
|
||||
"refund_wallet_endpoint: x-cashu inbound transaction has no request_id",
|
||||
extra={
|
||||
"cashu_fingerprint": cashu_fingerprint,
|
||||
"cashu_transaction_id": in_tx.id,
|
||||
"amount": in_tx.amount,
|
||||
"unit": in_tx.unit,
|
||||
"mint_url": in_tx.mint_url,
|
||||
},
|
||||
)
|
||||
raise HTTPException(status_code=404, detail="Refund not found")
|
||||
|
||||
out_tx_result = await session.exec(
|
||||
@@ -274,21 +289,33 @@ async def refund_wallet_endpoint(
|
||||
)
|
||||
out_tx = out_tx_result.first()
|
||||
if out_tx is None:
|
||||
# The "in" row exists with a request_id, but the "out" (refund)
|
||||
# row hasn't been written yet — the upstream request is still in
|
||||
# flight and the refund will be minted once it completes. Tell the
|
||||
# client to retry instead of 404ing permanently (race condition
|
||||
# where /v1/wallet/refund is polled before the refund exists).
|
||||
logger.debug(
|
||||
"refund_wallet_endpoint: refund pending (in row exists, out row not yet created)",
|
||||
extra={"request_id": in_tx.request_id},
|
||||
logger.info(
|
||||
"refund_wallet_endpoint: x-cashu refund transaction not ready",
|
||||
extra={
|
||||
"cashu_fingerprint": cashu_fingerprint,
|
||||
"cashu_transaction_id": in_tx.id,
|
||||
"refund_request_id": in_tx.request_id,
|
||||
"amount": in_tx.amount,
|
||||
"unit": in_tx.unit,
|
||||
"mint_url": in_tx.mint_url,
|
||||
"age_seconds": max(0, int(time.time()) - in_tx.created_at),
|
||||
},
|
||||
)
|
||||
raise HTTPException(
|
||||
status_code=425,
|
||||
detail="Refund is pending; retry shortly.",
|
||||
headers={"Retry-After": "2"},
|
||||
detail="Refund not ready. Retry later.",
|
||||
headers={"Retry-After": "5"},
|
||||
)
|
||||
if out_tx.swept:
|
||||
logger.info(
|
||||
"refund_wallet_endpoint: x-cashu refund transaction already swept",
|
||||
extra={
|
||||
"cashu_fingerprint": cashu_fingerprint,
|
||||
"cashu_transaction_id": in_tx.id,
|
||||
"refund_transaction_id": out_tx.id,
|
||||
"refund_request_id": in_tx.request_id,
|
||||
},
|
||||
)
|
||||
raise HTTPException(status_code=410, detail="Refund has been swept")
|
||||
|
||||
out_tx.collected = True
|
||||
|
||||
+10
-46
@@ -2,6 +2,7 @@ import json
|
||||
from unittest.mock import AsyncMock, MagicMock, patch
|
||||
|
||||
import pytest
|
||||
from fastapi import HTTPException
|
||||
from fastapi.responses import JSONResponse
|
||||
|
||||
from routstr.balance import refund_wallet_endpoint
|
||||
@@ -88,8 +89,6 @@ async def test_refund_x_cashu_sat_unit() -> None:
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_refund_x_cashu_not_found_raises_404() -> None:
|
||||
from fastapi import HTTPException
|
||||
|
||||
session = MagicMock()
|
||||
session.exec = AsyncMock(return_value=_exec_result(None))
|
||||
|
||||
@@ -104,67 +103,32 @@ async def test_refund_x_cashu_not_found_raises_404() -> None:
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_refund_x_cashu_pending_raises_425() -> None:
|
||||
"""in row exists with a request_id but out row not yet created → 425.
|
||||
|
||||
This is the race condition where /v1/wallet/refund is polled while the
|
||||
upstream request is still in flight. The endpoint must signal "retry"
|
||||
rather than a permanent 404.
|
||||
"""
|
||||
from fastapi import HTTPException
|
||||
|
||||
x_cashu_token = "cashuApending_token"
|
||||
async def test_refund_x_cashu_pending_out_tx_raises_425() -> None:
|
||||
in_tx = _make_cashu_tx(
|
||||
token=x_cashu_token, amount=0, unit="msat", type="in", request_id="req-pending"
|
||||
token="cashuApending_token",
|
||||
amount=0,
|
||||
unit="msat",
|
||||
type="in",
|
||||
request_id="req-pending",
|
||||
)
|
||||
|
||||
session = MagicMock()
|
||||
session.exec = AsyncMock(side_effect=[_exec_result(in_tx), _exec_result(None)])
|
||||
session.add = MagicMock()
|
||||
session.commit = AsyncMock()
|
||||
|
||||
with pytest.raises(HTTPException) as exc_info:
|
||||
await refund_wallet_endpoint(
|
||||
authorization="Bearer sk-somekey",
|
||||
x_cashu=x_cashu_token,
|
||||
x_cashu="cashuApending_token",
|
||||
session=session,
|
||||
)
|
||||
|
||||
assert exc_info.value.status_code == 425
|
||||
assert exc_info.value.headers == {"Retry-After": "2"}
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_refund_x_cashu_in_tx_without_request_id_raises_404() -> None:
|
||||
"""in row exists but has no request_id (cannot link to a refund) → 404.
|
||||
|
||||
This is a genuine "no refund will ever exist" case, distinct from the
|
||||
pending 425 path.
|
||||
"""
|
||||
from fastapi import HTTPException
|
||||
|
||||
x_cashu_token = "cashuAnoreqid_token"
|
||||
in_tx = _make_cashu_tx(
|
||||
token=x_cashu_token, amount=0, unit="msat", type="in", request_id=None
|
||||
)
|
||||
|
||||
session = MagicMock()
|
||||
session.exec = AsyncMock(side_effect=[_exec_result(in_tx)])
|
||||
|
||||
with pytest.raises(HTTPException) as exc_info:
|
||||
await refund_wallet_endpoint(
|
||||
authorization="Bearer sk-somekey",
|
||||
x_cashu=x_cashu_token,
|
||||
session=session,
|
||||
)
|
||||
|
||||
assert exc_info.value.status_code == 404
|
||||
assert exc_info.value.detail == "Refund not ready. Retry later."
|
||||
assert exc_info.value.headers == {"Retry-After": "5"}
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_refund_x_cashu_swept_raises_410() -> None:
|
||||
from fastapi import HTTPException
|
||||
|
||||
in_tx = _make_cashu_tx(token="cashuAswept_token", amount=0, unit="msat", type="in", request_id="req-swept")
|
||||
out_tx = _make_cashu_tx(token="cashuAswept", amount=100, unit="msat", type="out", request_id="req-swept", swept=True)
|
||||
|
||||
|
||||
Reference in New Issue
Block a user