@@ -48,14 +48,18 @@ def _rpc(port: int, method: str, params: dict[str, Any]) -> dict[str, Any]:
4848 return _post (port , body )
4949
5050
51- def _post (port : int , body : bytes ) -> dict [str , Any ]:
51+ def _post (port : int , body : bytes ) -> Any :
52+ return json .loads (_post_raw (port , body ))
53+
54+
55+ def _post_raw (port : int , body : bytes ) -> bytes :
5256 req = urllib .request .Request (
5357 f'http://localhost:{ port } ' ,
5458 data = body ,
5559 headers = {'Content-Type' : 'application/json' },
5660 )
5761 with urllib .request .urlopen (req ) as resp :
58- return json . loads ( resp .read () )
62+ return resp .read ()
5963
6064
6165@pytest .fixture
@@ -138,7 +142,7 @@ def test_malformed_body_returns_parse_error(server: StellarRpcServer) -> None:
138142
139143
140144def test_non_object_frame_returns_invalid_request (server : StellarRpcServer ) -> None :
141- result = _post (server .port (), b'[1, 2, 3] ' )
145+ result = _post (server .port (), b'"just a string" ' )
142146 assert result ['error' ]['code' ] == - 32600
143147
144148
@@ -498,3 +502,162 @@ def builder() -> TransactionBuilder:
498502
499503 # All four transactions, including the non-Void invocation, advanced the ledger.
500504 assert _rpc (server .port (), 'getLatestLedger' , {})['result' ]['sequence' ] == 4
505+
506+
507+ # ----------------------------------------------------------------------
508+ # xdrFormat parameter (getTransaction / sendTransaction)
509+ #
510+ # Real stellar-rpc accepts an optional `xdrFormat` param on both methods
511+ # (protocols/rpc: GetTransactionRequest.Format, SendTransactionRequest.Format)
512+ # and rejects invalid values with InvalidParams (-32602). komet-node supports
513+ # only 'base64' (the default); 'json' is rejected with a clear -32602 error.
514+ # ----------------------------------------------------------------------
515+
516+
517+ def _create_account_xdr () -> str :
518+ """A freshly signed CreateAccount transaction envelope, base64 XDR."""
519+ keypair = Keypair .random ()
520+ account = Account (keypair .public_key , sequence = 0 )
521+ envelope = (
522+ TransactionBuilder (account , Network .TESTNET_NETWORK_PASSPHRASE )
523+ .append_create_account_op (destination = keypair .public_key , starting_balance = '1000' )
524+ .set_timeout (30 )
525+ .build ()
526+ )
527+ envelope .sign (keypair )
528+ return envelope .to_xdr ()
529+
530+
531+ def test_xdr_format_base64_behaves_as_default (server : StellarRpcServer ) -> None :
532+ """xdrFormat 'base64' is the explicit spelling of the default on both methods."""
533+ send_result = _rpc (server .port (), 'sendTransaction' , {'transaction' : _create_account_xdr (), 'xdrFormat' : 'base64' })
534+ assert send_result ['result' ]['status' ] == 'PENDING'
535+ tx_hash = send_result ['result' ]['hash' ]
536+
537+ get_result = _rpc (server .port (), 'getTransaction' , {'hash' : tx_hash , 'xdrFormat' : 'base64' })
538+ assert get_result ['result' ]['status' ] == 'SUCCESS'
539+
540+
541+ def test_get_transaction_xdr_format_json_returns_invalid_params (server : StellarRpcServer ) -> None :
542+ """komet-node does not support the JSON XDR format: reject with a clear -32602 error."""
543+ result = _rpc (server .port (), 'getTransaction' , {'hash' : '0' * 64 , 'xdrFormat' : 'json' })
544+ assert result ['error' ]['code' ] == - 32602
545+ assert 'json' in result ['error' ]['message' ].lower ()
546+
547+
548+ def test_get_transaction_xdr_format_invalid_value_returns_invalid_params (server : StellarRpcServer ) -> None :
549+ result = _rpc (server .port (), 'getTransaction' , {'hash' : '0' * 64 , 'xdrFormat' : 'yaml' })
550+ assert result ['error' ]['code' ] == - 32602
551+
552+
553+ def test_get_transaction_xdr_format_non_string_returns_invalid_params (server : StellarRpcServer ) -> None :
554+ result = _rpc (server .port (), 'getTransaction' , {'hash' : '0' * 64 , 'xdrFormat' : 42 })
555+ assert result ['error' ]['code' ] == - 32602
556+
557+
558+ def test_send_transaction_xdr_format_json_returns_invalid_params_without_executing (server : StellarRpcServer ) -> None :
559+ """An unsupported xdrFormat is rejected before the transaction runs: no state change."""
560+ result = _rpc (server .port (), 'sendTransaction' , {'transaction' : _create_account_xdr (), 'xdrFormat' : 'json' })
561+ assert result ['error' ]['code' ] == - 32602
562+ assert 'json' in result ['error' ]['message' ].lower ()
563+
564+ # The transaction must not have executed: no receipt written, ledger not advanced.
565+ assert list ((server .io_dir / 'receipts' ).iterdir ()) == []
566+ assert _rpc (server .port (), 'getLatestLedger' , {})['result' ]['sequence' ] == 0
567+
568+
569+ def test_send_transaction_xdr_format_invalid_value_returns_invalid_params (server : StellarRpcServer ) -> None :
570+ result = _rpc (server .port (), 'sendTransaction' , {'transaction' : _create_account_xdr (), 'xdrFormat' : 'yaml' })
571+ assert result ['error' ]['code' ] == - 32602
572+ assert list ((server .io_dir / 'receipts' ).iterdir ()) == []
573+
574+
575+ # ----------------------------------------------------------------------
576+ # JSON-RPC 2.0 batch requests
577+ #
578+ # Per JSON-RPC 2.0 section 6: an array of request objects yields an array of
579+ # response objects (matched by id, order not significant); an empty array is a
580+ # single Invalid Request error; invalid batch elements each yield an Invalid
581+ # Request error with id null; notifications (no id) get no response, and a
582+ # batch of only notifications yields no response body at all.
583+ # ----------------------------------------------------------------------
584+
585+
586+ def _batch (port : int , requests : list [dict [str , Any ]]) -> Any :
587+ return _post (port , json .dumps (requests ).encode ())
588+
589+
590+ def test_batch_request_returns_array_of_responses (server : StellarRpcServer ) -> None :
591+ responses = _batch (
592+ server .port (),
593+ [
594+ {'jsonrpc' : '2.0' , 'id' : 1 , 'method' : 'getHealth' , 'params' : {}},
595+ {'jsonrpc' : '2.0' , 'id' : 2 , 'method' : 'getLatestLedger' , 'params' : {}},
596+ ],
597+ )
598+ assert isinstance (responses , list )
599+ assert len (responses ) == 2
600+ by_id = {response ['id' ]: response for response in responses }
601+ assert set (by_id ) == {1 , 2 }
602+ for response in responses :
603+ assert response ['jsonrpc' ] == '2.0'
604+ assert by_id [1 ]['result' ]['status' ] == 'healthy'
605+ assert by_id [2 ]['result' ]['sequence' ] == 0
606+
607+
608+ def test_empty_batch_returns_single_invalid_request (server : StellarRpcServer ) -> None :
609+ """An empty array is not a valid batch: one Invalid Request error object, not an array."""
610+ result = _post (server .port (), b'[]' )
611+ assert isinstance (result , dict )
612+ assert result ['error' ]['code' ] == - 32600
613+ assert result ['id' ] is None
614+
615+
616+ def test_batch_of_invalid_elements_returns_error_per_element (server : StellarRpcServer ) -> None :
617+ """rpc call with an invalid batch: one Invalid Request response per element, id null."""
618+ responses = _post (server .port (), b'[1, 2, 3]' )
619+ assert isinstance (responses , list )
620+ assert len (responses ) == 3
621+ for response in responses :
622+ assert response ['jsonrpc' ] == '2.0'
623+ assert response ['error' ]['code' ] == - 32600
624+ assert response ['id' ] is None
625+
626+
627+ def test_batch_mixed_valid_and_invalid_elements (server : StellarRpcServer ) -> None :
628+ responses = _batch (
629+ server .port (),
630+ [
631+ {'jsonrpc' : '2.0' , 'id' : 1 , 'method' : 'getHealth' , 'params' : {}},
632+ {'foo' : 'boo' },
633+ {'jsonrpc' : '2.0' , 'id' : 2 , 'method' : 'noSuchMethod' , 'params' : {}},
634+ ],
635+ )
636+ assert isinstance (responses , list )
637+ assert len (responses ) == 3
638+ by_id = {response ['id' ]: response for response in responses }
639+ assert by_id [1 ]['result' ]['status' ] == 'healthy'
640+ assert by_id [None ]['error' ]['code' ] == - 32600
641+ assert by_id [2 ]['error' ]['code' ] == - 32601
642+
643+
644+ def test_batch_notification_gets_no_response (server : StellarRpcServer ) -> None :
645+ """A request without an id is a notification: it is executed but not answered."""
646+ responses = _batch (
647+ server .port (),
648+ [
649+ {'jsonrpc' : '2.0' , 'id' : 1 , 'method' : 'getHealth' , 'params' : {}},
650+ {'jsonrpc' : '2.0' , 'method' : 'getHealth' , 'params' : {}}, # notification
651+ ],
652+ )
653+ assert isinstance (responses , list )
654+ assert len (responses ) == 1
655+ assert responses [0 ]['id' ] == 1
656+ assert responses [0 ]['result' ]['status' ] == 'healthy'
657+
658+
659+ def test_batch_of_only_notifications_returns_nothing (server : StellarRpcServer ) -> None :
660+ """If every batch element is a notification, the server must not return an empty array."""
661+ body = json .dumps ([{'jsonrpc' : '2.0' , 'method' : 'getHealth' , 'params' : {}}]).encode ()
662+ raw = _post_raw (server .port (), body )
663+ assert raw .strip () == b''
0 commit comments