Skip to content

Commit 7d57885

Browse files
test: assert getTransaction returns real result XDR
Per the official RPC spec, resultXdr/resultMetaXdr are base64-encoded TransactionResult/TransactionMeta XDR structs, present when the status is SUCCESS or FAILED; the current empty-string stubs are not valid XDR. The new tests decode the receipts with stellar_sdk and check the success result code, the invocation return value reported via sorobanMeta.returnValue (add(2, 3) -> U32(5)), the txFAILED error result for a transaction that invokes a missing contract (with ledger/createdAt shaped consistently with SUCCESS receipts), and that NOT_FOUND responses omit all transaction fields. The three XDR-content tests fail until the feature lands; the NOT_FOUND guard already passes.
1 parent 8f392c5 commit 7d57885

1 file changed

Lines changed: 179 additions & 0 deletions

File tree

src/tests/integration/test_server.py

Lines changed: 179 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -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

Comments
 (0)