From 8cc86f8b325d363617bba14e86ff5922a8af9b69 Mon Sep 17 00:00:00 2001 From: acedata-bot Date: Sun, 10 May 2026 02:53:28 -0700 Subject: [PATCH 1/2] fix(spend): wrap signature in solders Signature before get_signature_statuses MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit `solana-py >= 0.34` / `solders` strictly types `get_signature_statuses(...)` — it now rejects a `List[str]` with `TypeError: argument signatures: str object cannot be converted to Signature`. The on-chain spend itself succeeds (send_raw_transaction returns a finalized tx), but the post-send confirm loop blew up trying to convert the signature, so the SpendRecord never got persisted in Postgres and the MCP tool returned an error to the caller despite the chain having moved USDC. Verified live against the production devnet endpoint (vault 9d36b255-..., tx 2gPpot9Gcx7wPQ6u7uiqyLEjCrEmtLd7HqPooE4qsarmseTuuxMm35AfYovi5cZ8qFGGjgBvtH1E2WWLo1Yyf6aE): "TypeError(\"argument signatures: str object cannot be converted to Signature\")" Wrap on entry to `_confirm` with `Signature.from_string(signature)`. Caller contract unchanged (still passes a `str`). --- api/core/spend_executor.py | 12 ++++++++++-- 1 file changed, 10 insertions(+), 2 deletions(-) diff --git a/api/core/spend_executor.py b/api/core/spend_executor.py index d60bfc8..d18fe8f 100644 --- a/api/core/spend_executor.py +++ b/api/core/spend_executor.py @@ -63,12 +63,20 @@ async def _next_nonce(db: AsyncSession, vault: Vault) -> int: async def _confirm(rpc, signature: str, timeout_s: float = 30.0) -> None: - """Block until the cluster confirms the tx (or raise on timeout).""" + """Block until the cluster confirms the tx (or raise on timeout). + + `rpc.get_signature_statuses` in modern solana-py / solders requires a list + of `Signature` objects (not strings). Wrap on entry so callers can keep + passing the base58 string we stash on `SpendResult.tx_signature`. + """ import asyncio + from solders.signature import Signature + + sig_obj = Signature.from_string(signature) deadline = asyncio.get_event_loop().time() + timeout_s while asyncio.get_event_loop().time() < deadline: - status = await rpc.get_signature_statuses([signature]) + status = await rpc.get_signature_statuses([sig_obj]) s = status.value[0] if s is not None: if s.err is not None: From 3439f0064c4e98380a300c2633e525a3928c8dab Mon Sep 17 00:00:00 2001 From: acedata-bot Date: Sun, 10 May 2026 02:57:18 -0700 Subject: [PATCH 2/2] fix(spend): coerce confirmation_status enum to str before set membership `solders.transaction_status.TransactionConfirmationStatus` is not hashable, so `s.confirmation_status in {"confirmed","finalized"}` raises TypeError. The on-chain spend already landed by the time we hit this; the bug only prevents the SpendRecord from being persisted and a successful response from being returned to the caller. Verified live with the second devnet spend, vault balance 3.99 -> 3.98: the chain side is fine, only the post-confirm path was busted. Coerce via `str(s.confirmation_status).lower().rsplit(".", 1)[-1]` so it works against both enum values like TransactionConfirmationStatus.Finalized and plain strings (older solders). --- api/core/spend_executor.py | 14 ++++++++++---- 1 file changed, 10 insertions(+), 4 deletions(-) diff --git a/api/core/spend_executor.py b/api/core/spend_executor.py index d18fe8f..e408672 100644 --- a/api/core/spend_executor.py +++ b/api/core/spend_executor.py @@ -65,9 +65,11 @@ async def _next_nonce(db: AsyncSession, vault: Vault) -> int: async def _confirm(rpc, signature: str, timeout_s: float = 30.0) -> None: """Block until the cluster confirms the tx (or raise on timeout). - `rpc.get_signature_statuses` in modern solana-py / solders requires a list - of `Signature` objects (not strings). Wrap on entry so callers can keep - passing the base58 string we stash on `SpendResult.tx_signature`. + `solana-py >= 0.34` / `solders` typing notes: + - `get_signature_statuses(...)` requires `Signature` objects, not strings + - `s.confirmation_status` is a `solders.transaction_status.TransactionConfirmationStatus` + enum value (not hashable into a `set` of strings). Compare via its + string repr instead. """ import asyncio @@ -81,7 +83,11 @@ async def _confirm(rpc, signature: str, timeout_s: float = 30.0) -> None: if s is not None: if s.err is not None: raise SpendError(reason="cluster_rejected", cluster_message=str(s.err)) - if s.confirmation_status in {"confirmed", "finalized"}: + # `confirmation_status` may be a solders enum or a plain string + # depending on the solana-py / solders version. Coerce to string + # before membership-checking against the {confirmed,finalized} set. + cs = str(s.confirmation_status).lower().rsplit(".", 1)[-1] + if cs in {"confirmed", "finalized"}: return await asyncio.sleep(0.5) raise SpendError(reason="confirm_timeout")