Skip to content

Commit 9d9e01a

Browse files
committed
fix(assets): tolerate partial SEP request failures
1 parent 1ce81a5 commit 9d9e01a

3 files changed

Lines changed: 114 additions & 3 deletions

File tree

bot/infrastructure/services/anchor_transaction_service.py

Lines changed: 21 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33
from urllib.parse import urlencode
44
import asyncio
55

6+
from loguru import logger
67
from stellar_sdk import Keypair, TransactionEnvelope
78

89
from core.models.anchor_asset import AnchorAssetSupport, SepProtocolSupport
@@ -55,8 +56,14 @@ async def fetch_transactions(
5556
support.asset.code,
5657
headers,
5758
),
59+
return_exceptions=True,
5860
)
59-
transactions = [tx for protocol_txs in results for tx in protocol_txs]
61+
transactions = []
62+
for result in results:
63+
if isinstance(result, Exception):
64+
logger.info(f"SEP transactions request failed: {result}")
65+
continue
66+
transactions.extend(result)
6067
return transactions
6168

6269
async def _authenticate(
@@ -140,5 +147,17 @@ async def _default_fetch_json(
140147
timeout=self._request_timeout,
141148
)
142149
if response.status >= 400 or not isinstance(response.data, dict):
143-
raise ValueError(f"Unexpected JSON response from {url}: {response.status}")
150+
raise AnchorTransactionRequestError(
151+
url=url,
152+
status=response.status,
153+
body=response.data,
154+
)
144155
return response.data
156+
157+
158+
class AnchorTransactionRequestError(Exception):
159+
def __init__(self, *, url: str, status: int, body: object) -> None:
160+
self.url = url
161+
self.status = status
162+
self.body = body
163+
super().__init__(f"Unexpected JSON response from {url}: {status}; body={body}")

bot/tests/infrastructure/test_anchor_transaction_service.py

Lines changed: 39 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,11 @@
55

66
from core.domain.value_objects import Asset
77
from core.models.anchor_asset import AnchorAssetSupport, SepProtocol, SepProtocolSupport
8-
from infrastructure.services.anchor_transaction_service import AnchorTransactionService
8+
from core.models.anchor_transaction import AnchorTransactionProtocol
9+
from infrastructure.services.anchor_transaction_service import (
10+
AnchorTransactionRequestError,
11+
AnchorTransactionService,
12+
)
913

1014

1115
@pytest.mark.asyncio
@@ -65,3 +69,37 @@ async def fetch_json(method, url, params, headers, data):
6569
assert transactions[1].protocol.value == "SEP-24"
6670
assert calls[("GET", "https://anchor.test/sep6/transactions")] == 1
6771
assert calls[("GET", "https://anchor.test/sep24/transactions")] == 1
72+
73+
74+
@pytest.mark.asyncio
75+
async def test_fetch_transactions_keeps_successful_protocol_when_other_fails():
76+
async def fetch_json(method, url, params, headers, data):
77+
if url == "https://anchor.test/sep6/transactions":
78+
raise AnchorTransactionRequestError(
79+
url=url,
80+
status=403,
81+
body={"type": "authentication_required"},
82+
)
83+
if url == "https://anchor.test/sep24/transactions":
84+
return {"transactions": [{"id": "sep24-1", "status": "completed"}]}
85+
raise AssertionError(f"unexpected URL: {url}")
86+
87+
service = AnchorTransactionService(fetch_json=fetch_json)
88+
support = AnchorAssetSupport(
89+
asset=Asset("yXLM", "GISSUER"),
90+
anchor_domain="anchor.test",
91+
web_auth_endpoint=None,
92+
sep6=SepProtocolSupport(
93+
protocol=SepProtocol.SEP6,
94+
transfer_server="https://anchor.test/sep6",
95+
),
96+
sep24=SepProtocolSupport(
97+
protocol=SepProtocol.SEP24,
98+
transfer_server="https://anchor.test/sep24",
99+
),
100+
)
101+
102+
transactions = await service.fetch_transactions(support, Keypair.random())
103+
104+
assert [tx.id for tx in transactions] == ["sep24-1"]
105+
assert transactions[0].protocol is AnchorTransactionProtocol.SEP24
Lines changed: 54 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,54 @@
1+
# sep-requests-partial-failures: Handle partial SEP request failures
2+
3+
## Context
4+
5+
Ultra Capital returns `403 {"type":"authentication_required"}` from
6+
`/sep6/transactions?asset_code=yXLM` in the current flow. The Requests button
7+
should not fail the whole screen when one protocol endpoint rejects the request,
8+
and logs should include enough response details to diagnose anchor behavior.
9+
10+
## Files/Directories To Change
11+
12+
- `bot/infrastructure/services/anchor_transaction_service.py`
13+
- `bot/tests/infrastructure/test_anchor_transaction_service.py`
14+
- `docs/exec-plans/active/2026-05-23-sep-requests-partial-failures.md`
15+
16+
## Edit Permission
17+
18+
- [x] Allowed paths confirmed by user.
19+
- [x] No edits outside listed paths.
20+
21+
Permission evidence (copy user wording or exact confirmation):
22+
23+
> ладно давай кнопку добьем не работает Could not load requests.
24+
> Unexpected JSON response from https://ultracapital.xyz/sep6/transactions?asset_code=yXLM: 403
25+
> в логе пусто
26+
27+
## Change Plan
28+
29+
1. [x] Add explicit SEP request error type with status/body context.
30+
2. [x] Catch per-protocol transaction failures, log them, and continue with
31+
successful protocol results.
32+
3. [x] Add regression coverage for one protocol failing while another returns
33+
transactions.
34+
4. [x] Run focused tests and `just check-fast`.
35+
36+
## Risks / Open Questions
37+
38+
- If both protocol endpoints reject the request, the user will still see no
39+
requests; logs will now contain the status/body details.
40+
41+
## Verification
42+
43+
- `uv run pytest bot/tests/infrastructure/test_anchor_transaction_service.py -q`
44+
- `2 passed in 0.45s`
45+
- `uv run ruff check bot/infrastructure/services/anchor_transaction_service.py bot/tests/infrastructure/test_anchor_transaction_service.py`
46+
- `All checks passed!`
47+
- `just check-fast`
48+
- `ruff check .`: `All checks passed!`
49+
- `mypy core`: `Success: no issues found in 28 source files`
50+
- `pytest tests/core tests/infrastructure tests/other -m "not integration"`:
51+
`415 passed in 5.19s`
52+
- `check_import_boundaries.py`: `Import boundary checks passed.`
53+
- `check_docs_contract.py`: `Docs contract checks passed.`
54+
- `check_exec_plan_scope_lock.py`: `Execution plan scope-lock checks passed.`

0 commit comments

Comments
 (0)