Skip to content

Commit 52a53e2

Browse files
committed
fix(sign): show detailed send errors after webapp signing
1 parent 2dd69ab commit 52a53e2

6 files changed

Lines changed: 221 additions & 6 deletions

File tree

bot/infrastructure/workers/signing_worker.py

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -144,7 +144,6 @@ async def handle_tx_signed(msg: TxSignedMessage) -> None:
144144

145145
await clear_state(state)
146146

147-
# Удаляем TX из Redis
148147
await redis_client.delete(tx_key)
149148
logger.info(f"TX {tx_id}: deleted from Redis")
150149

bot/routers/sign.py

Lines changed: 21 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,22 @@
3434
from keyboards.webapp import webapp_sign_keyboard
3535

3636

37+
def format_horizon_send_error(ex: BaseHorizonError) -> str:
38+
"""Build a user-facing send error from Horizon exception details."""
39+
parts = [f"{ex.title}, error {ex.status}"]
40+
result_codes = ex.extras.get("result_codes") if ex.extras else None
41+
if result_codes:
42+
try:
43+
from other.stellar_error_codes import get_stellar_error_message
44+
45+
parts.append(get_stellar_error_message(result_codes))
46+
except Exception:
47+
parts.append(str(result_codes))
48+
elif ex.detail:
49+
parts.append(str(ex.detail))
50+
return "\n".join(part for part in parts if part)
51+
52+
3753
async def submit_signed_xdr(
3854
session: AsyncSession,
3955
user_id: int,
@@ -80,9 +96,11 @@ async def submit_signed_xdr(
8096
)
8197

8298
except BadRequestError as ex:
83-
extras = ex.extras.get("result_codes", "no extras") if ex.extras else ex.detail
84-
result["error"] = f"{ex.title}: {extras}"
85-
msg = my_gettext(user_id, "send_error", app_context=app_context)
99+
result["error"] = format_horizon_send_error(ex)
100+
msg = (
101+
f"{my_gettext(user_id, 'send_error', app_context=app_context)}\n"
102+
f"{result['error']}"
103+
)
86104
await send_message(
87105
session,
88106
user_id,

bot/tests/test_signing_flow.py

Lines changed: 66 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -382,6 +382,9 @@ def test_webapp_sign_keyboard_uses_user_lang(self):
382382
tx_id = "123_abc12345"
383383
app_context = MagicMock()
384384
app_context.localization_service.get_user_language.return_value = "ru"
385+
app_context.localization_service.get_text.side_effect = (
386+
lambda user_id, key, params=(): key
387+
)
385388

386389
keyboard = webapp_sign_keyboard(tx_id, user_id=42, app_context=app_context)
387390
sign_btn = keyboard.inline_keyboard[0][0]
@@ -396,8 +399,13 @@ def test_webapp_sign_keyboard_falls_back_to_en_for_unknown_lang(self):
396399

397400
app_context = MagicMock()
398401
app_context.localization_service.get_user_language.return_value = "fr"
402+
app_context.localization_service.get_text.side_effect = (
403+
lambda user_id, key, params=(): key
404+
)
399405

400-
keyboard = webapp_sign_keyboard("123_abc12345", user_id=42, app_context=app_context)
406+
keyboard = webapp_sign_keyboard(
407+
"123_abc12345", user_id=42, app_context=app_context
408+
)
401409

402410
assert "lang=en" in keyboard.inline_keyboard[0][0].web_app.url
403411

@@ -430,8 +438,13 @@ def test_webapp_import_key_keyboard_uses_user_lang(self):
430438

431439
app_context = MagicMock()
432440
app_context.localization_service.get_user_language.return_value = "ru"
441+
app_context.localization_service.get_text.side_effect = (
442+
lambda user_id, key, params=(): key
443+
)
433444

434-
keyboard = webapp_import_key_keyboard("GXXX...", user_id=7, app_context=app_context)
445+
keyboard = webapp_import_key_keyboard(
446+
"GXXX...", user_id=7, app_context=app_context
447+
)
435448

436449
assert "lang=ru" in keyboard.inline_keyboard[0][0].web_app.url
437450
app_context.localization_service.get_user_language.assert_called_once_with(7)
@@ -538,3 +551,54 @@ async def test_handle_tx_signed_accesses_current_app_context(self, fake_redis):
538551
finally:
539552
faststream_tools.APP_CONTEXT = original_context
540553
await fake_redis.aclose()
554+
555+
556+
class TestSubmitSignedXdr:
557+
@pytest.mark.asyncio
558+
async def test_submit_signed_xdr_includes_horizon_detail_in_chat(
559+
self, mock_app_context
560+
):
561+
"""Should include decoded Horizon error details in the bot chat."""
562+
from unittest.mock import AsyncMock, patch
563+
from routers.sign import submit_signed_xdr
564+
from stellar_sdk.exceptions import BadRequestError
565+
566+
class FakeResponse:
567+
text = "bad request"
568+
status_code = 400
569+
570+
@staticmethod
571+
def json():
572+
return {
573+
"title": "Transaction Failed",
574+
"detail": "The transaction failed when submitted to the network.",
575+
"extras": {
576+
"result_codes": {
577+
"transaction": "tx_failed",
578+
"operations": ["op_underfunded"],
579+
}
580+
},
581+
}
582+
583+
session = AsyncMock()
584+
user_id = 123
585+
586+
with patch(
587+
"other.stellar_tools.async_stellar_send",
588+
side_effect=BadRequestError(FakeResponse()),
589+
):
590+
with patch(
591+
"routers.sign.send_message", new_callable=AsyncMock
592+
) as mock_send:
593+
result = await submit_signed_xdr(
594+
session,
595+
user_id,
596+
"AAAA...",
597+
app_context=mock_app_context,
598+
)
599+
600+
assert result["successful"] is False
601+
assert result["error"] is not None
602+
sent_text = mock_send.await_args.args[2]
603+
assert "send_error" in sent_text
604+
assert "Insufficient funds for the operation" in sent_text
Lines changed: 49 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,49 @@
1+
# webapp-sign-error-display: Show Stellar send errors in webapp signing flow
2+
3+
## Context
4+
5+
Web signing currently shows success immediately after the signed XDR is queued.
6+
The actual Stellar submission happens later in `bot/infrastructure/workers/signing_worker.py`,
7+
so Horizon failures like insufficient funds are not surfaced in the WebApp UI.
8+
The goal of this task is to make the web signing flow wait for the final submission
9+
result and display the same concrete send error class that the regular wallet flow
10+
already exposes.
11+
12+
## Files/Directories To Change
13+
14+
- `webapp/app.py`
15+
- `webapp/templates/sign.html`
16+
- `bot/infrastructure/workers/signing_worker.py`
17+
- `bot/routers/sign.py`
18+
- `shared/src/shared/constants.py`
19+
- `shared/src/shared/schemas.py` (only if a shared status payload is needed)
20+
- `bot/tests/test_signing_flow.py`
21+
22+
## Edit Permission
23+
24+
- [x] Allowed paths confirmed by user.
25+
- [x] No edits outside listed paths.
26+
27+
Permission evidence (copy user wording or exact confirmation):
28+
29+
> ++
30+
31+
## Change Plan
32+
33+
1. [x] Add a failing regression test in `bot/tests/test_signing_flow.py` for a WebApp-signed transaction that later fails during Stellar submission and must persist an error/result for the UI.
34+
2. [x] Extend shared Redis status/error fields in `shared/src/shared/constants.py` and, only if needed, `shared/src/shared/schemas.py` to represent final send outcomes without guessing field names ad hoc.
35+
3. [x] Update `bot/infrastructure/workers/signing_worker.py` and `bot/routers/sign.py` so the worker writes final success or detailed send failure back to Redis instead of deleting the transaction immediately.
36+
4. [x] Update `webapp/app.py` and `webapp/templates/sign.html` so `/api/tx/{tx_id}` returns final status/error and the WebApp waits for completion before showing success or a concrete send error.
37+
5. [x] Run focused tests for the signing flow, then broader lint/tests if the focused checks pass.
38+
39+
## Risks / Open Questions
40+
41+
- The current async queue design intentionally decouples signing from sending; waiting in the WebApp must not break existing bot-side callbacks or cleanup.
42+
- Redis transaction cleanup timing changes in this task; stale keys must not accumulate on failures.
43+
- Human-readable Horizon errors already exist in bot flow, but `submit_signed_xdr` currently sends generic chat text in some paths; the WebApp path should reuse decoded error details without widening the change unnecessarily.
44+
45+
## Verification
46+
47+
- `uv run pytest bot/tests/test_signing_flow.py -q` -> `25 passed`
48+
- `just lint` -> `ruff check` and `mypy core` passed
49+
- Confirmed by regression test: failed WebApp send now persists `status=error` and `error=<decoded message>` in Redis for UI polling.
Lines changed: 47 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,47 @@
1+
# webapp-chat-send-error: Restore webapp close behavior and show send errors in bot chat
2+
3+
## Context
4+
5+
Previous implementation incorrectly changed the WebApp UI flow to wait for
6+
network submission results. The intended behavior is unchanged WebApp UX:
7+
sign and close immediately. The actual fix should be in the bot flow after
8+
WebApp signing, so the user sees the same detailed send error in chat that a
9+
regular wallet send already shows.
10+
11+
## Files/Directories To Change
12+
13+
- `bot/routers/sign.py`
14+
- `bot/infrastructure/workers/signing_worker.py`
15+
- `webapp/app.py`
16+
- `webapp/templates/sign.html`
17+
- `webapp/static/js/i18n.js`
18+
- `shared/src/shared/constants.py`
19+
- `bot/tests/test_signing_flow.py`
20+
21+
## Edit Permission
22+
23+
- [x] Allowed paths confirmed by user.
24+
- [x] No edits outside listed paths.
25+
26+
Permission evidence (copy user wording or exact confirmation):
27+
28+
> ++
29+
30+
## Change Plan
31+
32+
1. [x] Replace the WebApp-oriented regression in `bot/tests/test_signing_flow.py` with a bot chat regression that requires detailed Horizon send errors to be included in the Telegram message after WebApp signing.
33+
2. [x] Update `bot/routers/sign.py` so `submit_signed_xdr()` sends the same detailed send error text to chat for Horizon failures instead of only the generic localized header.
34+
3. [x] Revert the unintended WebApp waiting/polling changes in `webapp/app.py`, `webapp/templates/sign.html`, `webapp/static/js/i18n.js`, and any no-longer-needed shared/worker status plumbing.
35+
4. [x] Keep `bot/infrastructure/workers/signing_worker.py` aligned with the original WebApp contract: queue, process, notify in chat, then clean up.
36+
5. [x] Run focused tests and lint for the touched backend paths.
37+
38+
## Risks / Open Questions
39+
40+
- The bot has multiple send flows with slightly different error formatting; this fix should improve the WebApp-post-sign path without broad refactoring.
41+
- Reverting the WebApp polling changes must not break the existing successful close-on-sign behavior.
42+
43+
## Verification
44+
45+
- `uv run pytest bot/tests/test_signing_flow.py -q` -> `25 passed`
46+
- `just lint` -> `ruff check` and `mypy core` passed
47+
- Confirmed by regression test that chat-side send error now contains both the localized `send_error` header and the decoded Horizon reason.
Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,38 @@
1+
# webapp-keyboard-validation: Fix webapp keyboard button validation in tests
2+
3+
## Context
4+
5+
`just check` fails in `tests/test_signing_flow.py` with
6+
`InlineKeyboardButton` validation errors in WebApp keyboard tests. The
7+
production keyboard code resolves `lang` correctly, but the tests create a
8+
partial `MagicMock` app_context that does not provide a real localized string
9+
for the shared return button, causing aiogram validation to reject the button
10+
text.
11+
12+
## Files/Directories To Change
13+
14+
- `bot/tests/test_signing_flow.py`
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+
> ++
24+
25+
## Change Plan
26+
27+
1. [x] Adjust the failing WebApp keyboard tests in `bot/tests/test_signing_flow.py` to use a valid localization mock for the shared return button.
28+
2. [x] Re-run the narrow test selection for the three failing cases.
29+
3. [x] Run `just check` to verify the repository gate is green again.
30+
31+
## Risks / Open Questions
32+
33+
- Keep the fix test-only unless inspection proves the production keyboard builder is wrong.
34+
35+
## Verification
36+
37+
- `uv run pytest bot/tests/test_signing_flow.py -q` -> `25 passed`
38+
- `just check` -> passed (`594 passed, 5 deselected`)

0 commit comments

Comments
 (0)