@@ -498,3 +498,182 @@ def builder() -> TransactionBuilder:
498498
499499 # All four transactions, including the non-Void invocation, advanced the ledger.
500500 assert _rpc (server .port (), 'getLatestLedger' , {})['result' ]['sequence' ] == 4
501+
502+
503+ # ---------------------------------------------------------------------------
504+ # getTransaction: resultXdr / resultMetaXdr contents.
505+ #
506+ # Per the official spec, `resultXdr` is "a base64 encoded string of the raw
507+ # TransactionResult XDR struct" and `resultMetaXdr` the raw TransactionMeta, present
508+ # when status is SUCCESS or FAILED. Fields are either omitted or valid XDR — an
509+ # empty-string stub is neither.
510+ # ---------------------------------------------------------------------------
511+
512+
513+ def _tx_result_of (get_result : dict [str , Any ]) -> xdr .TransactionResult :
514+ """Decode a receipt's resultXdr, asserting it is real base64 XDR (not an empty stub)."""
515+ result_xdr = get_result .get ('resultXdr' )
516+ assert isinstance (result_xdr , str ), f'resultXdr missing or not a string: { get_result } '
517+ assert result_xdr != '' , 'resultXdr must be real XDR or omitted, not an empty string'
518+ return xdr .TransactionResult .from_xdr (result_xdr )
519+
520+
521+ def _tx_meta_of (get_result : dict [str , Any ]) -> xdr .TransactionMeta :
522+ """Decode a receipt's resultMetaXdr, asserting it is real base64 XDR (not an empty stub)."""
523+ meta_xdr = get_result .get ('resultMetaXdr' )
524+ assert isinstance (meta_xdr , str ), f'resultMetaXdr missing or not a string: { get_result } '
525+ assert meta_xdr != '' , 'resultMetaXdr must be real XDR or omitted, not an empty string'
526+ return xdr .TransactionMeta .from_xdr (meta_xdr )
527+
528+
529+ def _soroban_return_value (meta : xdr .TransactionMeta ) -> xdr .SCVal :
530+ """Extract sorobanMeta.returnValue from a TransactionMeta (v3 at protocol version 22)."""
531+ assert meta .v == 3 , f'expected TransactionMeta v3 at protocol version 22, got v{ meta .v } '
532+ assert meta .v3 is not None and meta .v3 .soroban_meta is not None
533+ return meta .v3 .soroban_meta .return_value
534+
535+
536+ def test_get_transaction_success_returns_decodable_result_xdr (server : StellarRpcServer ) -> None :
537+ """A SUCCESS receipt carries a real TransactionResult and TransactionMeta.
538+
539+ The TransactionResult must decode from base64 and report code txSUCCESS; the
540+ TransactionMeta must decode as well.
541+ """
542+ keypair = Keypair .random ()
543+ account = Account (keypair .public_key , sequence = 0 )
544+ envelope = (
545+ TransactionBuilder (account , Network .TESTNET_NETWORK_PASSPHRASE )
546+ .append_create_account_op (destination = keypair .public_key , starting_balance = '1000' )
547+ .set_timeout (30 )
548+ .build ()
549+ )
550+ envelope .sign (keypair )
551+ tx_hash = _rpc (server .port (), 'sendTransaction' , {'transaction' : envelope .to_xdr ()})['result' ]['hash' ]
552+
553+ get_result = _rpc (server .port (), 'getTransaction' , {'hash' : tx_hash })['result' ]
554+ assert get_result ['status' ] == 'SUCCESS'
555+
556+ tx_result = _tx_result_of (get_result )
557+ assert tx_result .result .code == xdr .TransactionResultCode .txSUCCESS
558+
559+ _tx_meta_of (get_result ) # must decode as TransactionMeta
560+
561+
562+ def test_get_transaction_invocation_reports_return_value (server : StellarRpcServer ) -> None :
563+ """A successful contract invocation surfaces its return value in resultMetaXdr.
564+
565+ ``add(2, 3)`` returns U32(5). Real stellar-rpc reports the invocation's return value
566+ as ``sorobanMeta.returnValue`` inside the TransactionMeta, and the TransactionResult
567+ carries a successful InvokeHostFunction operation result. Every receipt along the
568+ lifecycle (create account, upload, deploy) must carry decodable result XDR too.
569+ """
570+ keypair = Keypair .random ()
571+ account = Account (keypair .public_key , sequence = 0 )
572+
573+ def builder () -> TransactionBuilder :
574+ return TransactionBuilder (account , Network .TESTNET_NETWORK_PASSPHRASE )
575+
576+ def send (tb : TransactionBuilder ) -> dict [str , Any ]:
577+ env = tb .set_timeout (30 ).build ()
578+ env .sign (keypair )
579+ res = _rpc (server .port (), 'sendTransaction' , {'transaction' : env .to_xdr ()})
580+ assert res ['result' ]['status' ] == 'PENDING'
581+ get_res = _rpc (server .port (), 'getTransaction' , {'hash' : res ['result' ]['hash' ]})['result' ]
582+ assert get_res ['status' ] == 'SUCCESS' , f'Transaction failed: { get_res } '
583+ # Every successful receipt, whatever the operation, has a decodable txSUCCESS result.
584+ assert _tx_result_of (get_res ).result .code == xdr .TransactionResultCode .txSUCCESS
585+ return get_res
586+
587+ # Set up: create account, upload adder.wat, deploy contract
588+ send (builder ().append_create_account_op (keypair .public_key , '1000' ))
589+
590+ wasm_bytecode = wat_to_wasm (ADDER_CONTRACT_WAT )
591+ send (builder ().append_upload_contract_wasm_op (wasm_bytecode ))
592+
593+ from stellar_sdk .utils import sha256
594+
595+ wasm_hash = sha256 (wasm_bytecode )
596+ salt = b'\x00 ' * 32
597+ send (builder ().append_create_contract_op (wasm_hash , keypair .public_key , None , salt ))
598+
599+ contract_address = server .encoder .contract_address_from_deployer_address (keypair .public_key , salt )
600+ invoke_result = send (
601+ builder ().append_invoke_contract_function_op (
602+ contract_address ,
603+ 'add' ,
604+ [
605+ xdr .SCVal (type = SCValType .SCV_U32 , u32 = xdr .Uint32 (2 )),
606+ xdr .SCVal (type = SCValType .SCV_U32 , u32 = xdr .Uint32 (3 )),
607+ ],
608+ )
609+ )
610+
611+ # The TransactionResult reports the successful InvokeHostFunction operation.
612+ tx_result = _tx_result_of (invoke_result )
613+ op_results = tx_result .result .results
614+ assert op_results , 'TransactionResult must carry the InvokeHostFunction operation result'
615+ assert op_results [0 ].tr is not None
616+ invoke_op_result = op_results [0 ].tr .invoke_host_function_result
617+ assert invoke_op_result is not None
618+ assert invoke_op_result .code == xdr .InvokeHostFunctionResultCode .INVOKE_HOST_FUNCTION_SUCCESS
619+
620+ # The TransactionMeta reports the call's return value: U32(5).
621+ return_value = _soroban_return_value (_tx_meta_of (invoke_result ))
622+ assert return_value .type == SCValType .SCV_U32
623+ assert return_value .u32 == xdr .Uint32 (5 )
624+
625+
626+ def test_get_transaction_failed_reports_error_result_xdr (server : StellarRpcServer ) -> None :
627+ """A FAILED receipt carries a real error TransactionResult, not an empty stub.
628+
629+ Invoking a never-deployed contract fails. The spec requires `resultXdr` (a base64
630+ TransactionResult, here with code txFAILED) when status is FAILED, and `ledger`/
631+ `createdAt` to be reported with the same shape as on SUCCESS receipts.
632+ """
633+ keypair = Keypair .random ()
634+ account = Account (keypair .public_key , sequence = 0 )
635+
636+ def send (tb : TransactionBuilder ) -> str :
637+ env = tb .set_timeout (30 ).build ()
638+ env .sign (keypair )
639+ res = _rpc (server .port (), 'sendTransaction' , {'transaction' : env .to_xdr ()})
640+ tx_hash = res ['result' ]['hash' ]
641+ assert isinstance (tx_hash , str )
642+ return tx_hash
643+
644+ def builder () -> TransactionBuilder :
645+ return TransactionBuilder (account , Network .TESTNET_NETWORK_PASSPHRASE )
646+
647+ # A successful reference transaction first, to compare receipt shapes across statuses.
648+ ok_hash = send (builder ().append_create_account_op (keypair .public_key , '1000' ))
649+ ok_result = _rpc (server .port (), 'getTransaction' , {'hash' : ok_hash })['result' ]
650+ assert ok_result ['status' ] == 'SUCCESS'
651+
652+ missing_contract = StrKey .encode_contract (b'\x22 ' * 32 ) # valid C-strkey, never deployed
653+ bad_hash = send (builder ().append_invoke_contract_function_op (missing_contract , 'foo' , []))
654+
655+ get_result = _rpc (server .port (), 'getTransaction' , {'hash' : bad_hash })['result' ]
656+ assert get_result ['status' ] == 'FAILED'
657+
658+ tx_result = _tx_result_of (get_result )
659+ assert tx_result .result .code == xdr .TransactionResultCode .txFAILED
660+
661+ # resultMetaXdr may be omitted on a failed transaction, but must never be an empty stub.
662+ if 'resultMetaXdr' in get_result :
663+ _tx_meta_of (get_result )
664+
665+ # `ledger`/`createdAt` are present on FAILED receipts, with the same JSON types as on
666+ # SUCCESS receipts (their exact number-vs-string encoding is covered elsewhere).
667+ for field in ('ledger' , 'createdAt' ):
668+ assert field in get_result , f'FAILED receipt must report { field } '
669+ assert type (get_result [field ]) is type (ok_result [field ]), f'{ field } type differs across statuses'
670+ # createdAt in getTransaction (singular) is an int64 rendered as a decimal string.
671+ assert isinstance (get_result ['createdAt' ], str ) and get_result ['createdAt' ].isdigit ()
672+
673+
674+ def test_get_transaction_not_found_omits_transaction_fields (server : StellarRpcServer ) -> None :
675+ """A NOT_FOUND response carries no transaction details (omitted, not empty/null)."""
676+ get_result = _rpc (server .port (), 'getTransaction' , {'hash' : 'b' * 64 })['result' ]
677+ assert get_result ['status' ] == 'NOT_FOUND'
678+ for field in ('ledger' , 'createdAt' , 'envelopeXdr' , 'resultXdr' , 'resultMetaXdr' ):
679+ assert field not in get_result , f'NOT_FOUND response must omit { field } '
0 commit comments