|
11 | 11 | from pathlib import Path |
12 | 12 | from typing import TYPE_CHECKING, Any, Final |
13 | 13 |
|
14 | | -from stellar_sdk import Network |
| 14 | +from stellar_sdk import Network, TransactionEnvelope |
15 | 15 |
|
16 | 16 | from komet_node.interpreter import NodeInterpreter |
17 | | -from komet_node.transaction import TransactionEncoder |
| 17 | +from komet_node.transaction import TransactionEncoder, malformed_tx_result_xdr |
18 | 18 |
|
19 | 19 | if TYPE_CHECKING: |
20 | 20 | from http.server import HTTPServer as HTTPServerType |
@@ -192,14 +192,21 @@ def handle_rpc(self, method: str | None, params: dict[str, Any], request_id: Any |
192 | 192 | transaction = params.get('transaction') |
193 | 193 | if not isinstance(transaction, str): |
194 | 194 | return _error_str(request_id, -32602, "Invalid params: 'transaction' (XDR string) is required") |
| 195 | + # Undecodable XDR is a JSON-RPC client error (-32602, as in real stellar-rpc); |
| 196 | + # a transaction that decodes but cannot be processed (e.g. an unsupported |
| 197 | + # operation) is instead rejected at admission time with status ERROR below. |
| 198 | + try: |
| 199 | + parsed = TransactionEnvelope.from_xdr(transaction, self.encoder.network_passphrase) |
| 200 | + except Exception: |
| 201 | + traceback.print_exc() |
| 202 | + return _error_str(request_id, -32602, 'Invalid params: could not decode transaction XDR') |
195 | 203 | try: |
196 | 204 | envelope, program_steps = self.encoder.build_tx_request(method, request_id, transaction, now) |
197 | 205 | except Exception: |
198 | | - # build_tx_request both decodes XDR and validates it (e.g. rejecting |
199 | | - # sub-stroop amounts); either is a client error. Log the detail, but keep |
200 | | - # the client-facing message neutral rather than leaking internal exceptions. |
| 206 | + # Log the detail, but keep the client-facing response neutral rather than |
| 207 | + # leaking internal exceptions. |
201 | 208 | traceback.print_exc() |
202 | | - return _error_str(request_id, -32602, 'Invalid params: could not process transaction') |
| 209 | + return json.dumps(self._error_status_response(request_id, parsed.hash_hex(), now)) |
203 | 210 | # A hash that already has a receipt is a duplicate submission: the semantics |
204 | 211 | # answer DUPLICATE without running the steps (see node.md). Steps injected into |
205 | 212 | # the <program> cell (wasm uploads) would execute *before* dispatch, though, so |
@@ -256,6 +263,24 @@ def _archive_request(self, method: str | None, params: dict[str, Any], request_i |
256 | 263 | (self.requests_dir / f'request_{self._request_count}.json').write_text(json.dumps(archive)) |
257 | 264 | self._request_count += 1 |
258 | 265 |
|
| 266 | + def _error_status_response(self, rpc_id: Any, tx_hash: str, now: str) -> dict[str, Any]: |
| 267 | + """Synthesise the sendTransaction response for a transaction rejected at admission. |
| 268 | +
|
| 269 | + Mirrors real stellar-rpc: a transaction that decodes as XDR but cannot be processed |
| 270 | + gets status ``ERROR`` with a ``txMALFORMED`` ``errorResultXdr``. It never reaches |
| 271 | + the ledger, so no receipt is stored (``getTransaction`` stays ``NOT_FOUND``) and the |
| 272 | + ledger does not advance. |
| 273 | + """ |
| 274 | + metadata = json.loads((self.io_dir / 'metadata.json').read_text()) |
| 275 | + result = { |
| 276 | + 'hash': tx_hash, |
| 277 | + 'status': 'ERROR', |
| 278 | + 'errorResultXdr': malformed_tx_result_xdr(), |
| 279 | + 'latestLedger': metadata.get('latest_ledger', 0), |
| 280 | + 'latestLedgerCloseTime': now, |
| 281 | + } |
| 282 | + return {'jsonrpc': '2.0', 'id': rpc_id, 'result': result} |
| 283 | + |
259 | 284 | def _failure_response(self, rpc_id: Any, envelope: dict[str, Any], now: str) -> dict[str, Any]: |
260 | 285 | """Synthesise the sendTransaction response for a transaction that got stuck (failed). |
261 | 286 |
|
|
0 commit comments