1919EMPTY_CONTRACT_WAT = (Path (__file__ ).parent / 'data' / 'wasm' / 'empty.wat' ).resolve (strict = True )
2020ARGS_CONTRACT_WAT = (Path (__file__ ).parent / 'data' / 'wasm' / 'args.wat' ).resolve (strict = True )
2121ADDER_CONTRACT_WAT = (Path (__file__ ).parent / 'data' / 'wasm' / 'adder.wat' ).resolve (strict = True )
22+ BYTES_CONTRACT_WAT = (Path (__file__ ).parent / 'data' / 'wasm' / 'bytes.wat' ).resolve (strict = True )
2223
2324
2425def wat_to_wasm (wat_path : Path ) -> bytes :
@@ -552,6 +553,9 @@ def test_get_transaction_success_returns_decodable_result_xdr(server: StellarRpc
552553
553554 get_result = _rpc (server .port (), 'getTransaction' , {'hash' : tx_hash })['result' ]
554555 assert get_result ['status' ] == 'SUCCESS'
556+ # The K-side receipt's internal returnValue field must never reach RPC clients: the
557+ # server rewrites it into resultXdr/resultMetaXdr before the receipt is ever served.
558+ assert 'returnValue' not in get_result
555559
556560 tx_result = _tx_result_of (get_result )
557561 assert tx_result .result .code == xdr .TransactionResultCode .txSUCCESS
@@ -580,6 +584,8 @@ def send(tb: TransactionBuilder) -> dict[str, Any]:
580584 assert res ['result' ]['status' ] == 'PENDING'
581585 get_res = _rpc (server .port (), 'getTransaction' , {'hash' : res ['result' ]['hash' ]})['result' ]
582586 assert get_res ['status' ] == 'SUCCESS' , f'Transaction failed: { get_res } '
587+ # The internal K-side returnValue field must never leak into getTransaction responses.
588+ assert 'returnValue' not in get_res
583589 # Every successful receipt, whatever the operation, has a decodable txSUCCESS result.
584590 assert _tx_result_of (get_res ).result .code == xdr .TransactionResultCode .txSUCCESS
585591 return get_res
@@ -623,6 +629,49 @@ def send(tb: TransactionBuilder) -> dict[str, Any]:
623629 assert return_value .u32 == xdr .Uint32 (5 )
624630
625631
632+ def test_get_transaction_reports_empty_bytes_return_value (server : StellarRpcServer ) -> None :
633+ """A contract returning empty Bytes yields SCV_BYTES(b'') in sorobanMeta.returnValue.
634+
635+ Regression test for the zero-length edge of the receipt's hex encoding: an empty
636+ byte string round-trips through the K-side JSON encoding as an empty hex string, not
637+ as ``"0"`` (an odd-length non-hex value that breaks the XDR rewrite and would leak
638+ the internal ``returnValue`` field to clients).
639+ """
640+ keypair = Keypair .random ()
641+ account = Account (keypair .public_key , sequence = 0 )
642+
643+ def builder () -> TransactionBuilder :
644+ return TransactionBuilder (account , Network .TESTNET_NETWORK_PASSPHRASE )
645+
646+ def send (tb : TransactionBuilder ) -> dict [str , Any ]:
647+ env = tb .set_timeout (30 ).build ()
648+ env .sign (keypair )
649+ res = _rpc (server .port (), 'sendTransaction' , {'transaction' : env .to_xdr ()})
650+ assert 'result' in res , f'sendTransaction failed: { res } '
651+ get_res = _rpc (server .port (), 'getTransaction' , {'hash' : res ['result' ]['hash' ]})['result' ]
652+ assert get_res ['status' ] == 'SUCCESS' , f'Transaction failed: { get_res } '
653+ return get_res
654+
655+ send (builder ().append_create_account_op (keypair .public_key , '1000' ))
656+
657+ wasm_bytecode = wat_to_wasm (BYTES_CONTRACT_WAT )
658+ send (builder ().append_upload_contract_wasm_op (wasm_bytecode ))
659+
660+ from stellar_sdk .utils import sha256
661+
662+ salt = b'\x00 ' * 32
663+ send (builder ().append_create_contract_op (sha256 (wasm_bytecode ), keypair .public_key , None , salt ))
664+
665+ contract_address = server .encoder .contract_address_from_deployer_address (keypair .public_key , salt )
666+ invoke_result = send (builder ().append_invoke_contract_function_op (contract_address , 'empty_bytes' , []))
667+ assert 'returnValue' not in invoke_result # internal receipt field, must never be served
668+
669+ return_value = _soroban_return_value (_tx_meta_of (invoke_result ))
670+ assert return_value .type == SCValType .SCV_BYTES
671+ assert return_value .bytes is not None
672+ assert return_value .bytes .sc_bytes == b''
673+
674+
626675def test_get_transaction_failed_reports_error_result_xdr (server : StellarRpcServer ) -> None :
627676 """A FAILED receipt carries a real error TransactionResult, not an empty stub.
628677
@@ -654,6 +703,7 @@ def builder() -> TransactionBuilder:
654703
655704 get_result = _rpc (server .port (), 'getTransaction' , {'hash' : bad_hash })['result' ]
656705 assert get_result ['status' ] == 'FAILED'
706+ assert 'returnValue' not in get_result # internal receipt field, must never be served
657707
658708 tx_result = _tx_result_of (get_result )
659709 assert tx_result .result .code == xdr .TransactionResultCode .txFAILED
@@ -675,5 +725,5 @@ def test_get_transaction_not_found_omits_transaction_fields(server: StellarRpcSe
675725 """A NOT_FOUND response carries no transaction details (omitted, not empty/null)."""
676726 get_result = _rpc (server .port (), 'getTransaction' , {'hash' : 'b' * 64 })['result' ]
677727 assert get_result ['status' ] == 'NOT_FOUND'
678- for field in ('ledger' , 'createdAt' , 'envelopeXdr' , 'resultXdr' , 'resultMetaXdr' ):
728+ for field in ('ledger' , 'createdAt' , 'envelopeXdr' , 'resultXdr' , 'resultMetaXdr' , 'returnValue' ):
679729 assert field not in get_result , f'NOT_FOUND response must omit { field } '
0 commit comments