Skip to content

Commit 074fbe3

Browse files
test: cover getTransactions and getLedgers spec behavior
Integration tests for the two history methods against the official OpenRPC spec and the Go SDK protocol structs: response shape and JSON types (ledger sequences and top-level close times as numbers, per-tx createdAt as a number, per-ledger ledgerCloseTime as a string), headerXdr/metadataXdr decoding, cursor/limit pagination, and invalid parameter rejection. The methods are not implemented yet, so all six tests currently fail with method-not-found.
1 parent 8f392c5 commit 074fbe3

1 file changed

Lines changed: 208 additions & 0 deletions

File tree

src/tests/integration/test_server.py

Lines changed: 208 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -498,3 +498,211 @@ 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+
# getTransactions / getLedgers (transaction history)
505+
#
506+
# Ground truth: the official OpenRPC spec (stellar-docs, methods/getTransactions.json and
507+
# methods/getLedgers.json) and the Go serialization structs from stellar/go-stellar-sdk
508+
# protocols/rpc, which is what real stellar-rpc emits. Notable serialization traps asserted
509+
# below:
510+
# - ledger sequences and the top-level close-time fields are JSON numbers,
511+
# - per-transaction `createdAt` in getTransactions is a JSON *number* (upstream quirk;
512+
# the singular getTransaction returns it as a string),
513+
# - per-ledger `ledgerCloseTime` in getLedgers is a *string* (Go int64 `,string`),
514+
# - XDR fields are `omitempty`: real base64 XDR or absent, never empty strings.
515+
# ---------------------------------------------------------------------------
516+
517+
518+
def _rpc_result(port: int, method: str, params: dict[str, Any]) -> dict[str, Any]:
519+
"""Call an RPC method and return its result, failing the test on a JSON-RPC error."""
520+
response = _rpc(port, method, params)
521+
assert 'error' not in response, f'{method} returned an error: {response["error"]}'
522+
return response['result']
523+
524+
525+
def _is_hex64(value: Any) -> bool:
526+
return isinstance(value, str) and len(value) == 64 and all(c in '0123456789abcdef' for c in value)
527+
528+
529+
def _send_create_accounts(server: StellarRpcServer, count: int) -> list[tuple[str, str]]:
530+
"""Submit ``count`` successful create-account transactions; return (hash, envelopeXdr) pairs.
531+
532+
Each successful transaction closes its own ledger, so after this call the latest ledger
533+
is ``count`` and transaction ``i`` (1-based) sits alone in ledger ``i``.
534+
"""
535+
keypair = Keypair.random()
536+
account = Account(keypair.public_key, sequence=0)
537+
sent: list[tuple[str, str]] = []
538+
for _ in range(count):
539+
envelope = (
540+
TransactionBuilder(account, Network.TESTNET_NETWORK_PASSPHRASE)
541+
.append_create_account_op(destination=keypair.public_key, starting_balance='1000')
542+
.set_timeout(30)
543+
.build()
544+
)
545+
envelope.sign(keypair)
546+
xdr_str = envelope.to_xdr()
547+
send_res = _rpc(server.port(), 'sendTransaction', {'transaction': xdr_str})
548+
assert send_res['result']['status'] == 'PENDING'
549+
tx_hash = send_res['result']['hash']
550+
assert _rpc(server.port(), 'getTransaction', {'hash': tx_hash})['result']['status'] == 'SUCCESS'
551+
sent.append((tx_hash, xdr_str))
552+
return sent
553+
554+
555+
def test_get_transactions_spec_shape(server: StellarRpcServer) -> None:
556+
"""getTransactions returns the response shape of GetTransactionsResponse (Go SDK)."""
557+
before = int(time.time())
558+
sent = _send_create_accounts(server, 3)
559+
after = int(time.time())
560+
561+
result = _rpc_result(server.port(), 'getTransactions', {'startLedger': 1})
562+
563+
# All six top-level fields lack `omitempty` in the Go struct, so all must be present.
564+
required_keys = {
565+
'transactions',
566+
'latestLedger',
567+
'latestLedgerCloseTimestamp',
568+
'oldestLedger',
569+
'oldestLedgerCloseTimestamp',
570+
'cursor',
571+
}
572+
assert required_keys <= result.keys(), f'missing keys: {required_keys - result.keys()}'
573+
574+
assert type(result['latestLedger']) is int
575+
assert result['latestLedger'] == 3
576+
assert type(result['latestLedgerCloseTimestamp']) is int
577+
assert type(result['oldestLedger']) is int
578+
assert 0 <= result['oldestLedger'] <= 1
579+
assert type(result['oldestLedgerCloseTimestamp']) is int
580+
assert isinstance(result['cursor'], str)
581+
582+
txs = result['transactions']
583+
assert isinstance(txs, list)
584+
# All three transactions, in chain order (ascending ledger, then application order).
585+
assert [tx['txHash'] for tx in txs] == [tx_hash for tx_hash, _ in sent]
586+
for i, tx in enumerate(txs, start=1):
587+
assert tx['status'] == 'SUCCESS'
588+
assert _is_hex64(tx['txHash'])
589+
assert type(tx['applicationOrder']) is int
590+
assert tx['applicationOrder'] == 1 # one transaction per ledger on this node
591+
assert tx['feeBump'] is False
592+
assert type(tx['ledger']) is int
593+
assert tx['ledger'] == i
594+
# Upstream quirk: createdAt is a JSON number here (int64 without `,string` in Go),
595+
# unlike getTransaction (singular) where it is a string.
596+
assert type(tx['createdAt']) is int
597+
assert before <= tx['createdAt'] <= after
598+
assert tx['envelopeXdr'] == sent[i - 1][1]
599+
# omitempty: XDR fields carry real base64 XDR or are absent — never empty strings.
600+
for optional in ('resultXdr', 'resultMetaXdr'):
601+
if optional in tx:
602+
assert isinstance(tx[optional], str) and tx[optional] != ''
603+
604+
# xdrFormat: 'base64' is the default and must be accepted; unknown values are rejected.
605+
with_format = _rpc_result(server.port(), 'getTransactions', {'startLedger': 1, 'xdrFormat': 'base64'})
606+
assert [tx['txHash'] for tx in with_format['transactions']] == [tx_hash for tx_hash, _ in sent]
607+
assert _rpc(server.port(), 'getTransactions', {'startLedger': 1, 'xdrFormat': 'bogus'})['error']['code'] == -32602
608+
609+
# The limit for getTransactions ranges from 1 to 200.
610+
bad_limit = _rpc(server.port(), 'getTransactions', {'startLedger': 1, 'pagination': {'limit': 201}})
611+
assert bad_limit['error']['code'] == -32602
612+
613+
614+
def test_get_transactions_pagination(server: StellarRpcServer) -> None:
615+
"""A limited page returns a cursor from which the next page resumes without overlap."""
616+
sent = _send_create_accounts(server, 3)
617+
618+
page1 = _rpc_result(server.port(), 'getTransactions', {'startLedger': 1, 'pagination': {'limit': 2}})
619+
assert [tx['txHash'] for tx in page1['transactions']] == [sent[0][0], sent[1][0]]
620+
cursor = page1['cursor']
621+
assert isinstance(cursor, str) and cursor != ''
622+
623+
# Resume from the cursor; startLedger must be omitted on cursor requests.
624+
page2 = _rpc_result(server.port(), 'getTransactions', {'pagination': {'cursor': cursor, 'limit': 2}})
625+
assert [tx['txHash'] for tx in page2['transactions']] == [sent[2][0]]
626+
627+
628+
def test_get_transactions_invalid_params(server: StellarRpcServer) -> None:
629+
port = server.port()
630+
# startLedger beyond the latest ledger (0 on a fresh chain) is out of retention range.
631+
assert _rpc(port, 'getTransactions', {'startLedger': 999})['error']['code'] == -32602
632+
# startLedger and cursor are mutually exclusive.
633+
both = _rpc(port, 'getTransactions', {'startLedger': 1, 'pagination': {'cursor': '1'}})
634+
assert both['error']['code'] == -32602
635+
# startLedger must be a number.
636+
assert _rpc(port, 'getTransactions', {'startLedger': 'one'})['error']['code'] == -32602
637+
638+
639+
def test_get_ledgers_spec_shape(server: StellarRpcServer) -> None:
640+
"""getLedgers returns the response shape of GetLedgersResponse (Go SDK)."""
641+
_send_create_accounts(server, 2)
642+
643+
result = _rpc_result(server.port(), 'getLedgers', {'startLedger': 1})
644+
645+
required_keys = {
646+
'ledgers',
647+
'latestLedger',
648+
'latestLedgerCloseTime',
649+
'oldestLedger',
650+
'oldestLedgerCloseTime',
651+
'cursor',
652+
}
653+
assert required_keys <= result.keys(), f'missing keys: {required_keys - result.keys()}'
654+
655+
assert type(result['latestLedger']) is int
656+
assert result['latestLedger'] == 2
657+
assert type(result['latestLedgerCloseTime']) is int
658+
assert type(result['oldestLedger']) is int
659+
assert 0 <= result['oldestLedger'] <= 1
660+
assert type(result['oldestLedgerCloseTime']) is int
661+
assert isinstance(result['cursor'], str)
662+
663+
ledgers = result['ledgers']
664+
assert isinstance(ledgers, list)
665+
assert [ledger['sequence'] for ledger in ledgers] == [1, 2]
666+
hashes = set()
667+
for ledger in ledgers:
668+
assert type(ledger['sequence']) is int
669+
assert _is_hex64(ledger['hash'])
670+
hashes.add(ledger['hash'])
671+
# Per-ledger close time is a STRING containing a decimal number (Go int64 `,string`).
672+
assert isinstance(ledger['ledgerCloseTime'], str)
673+
assert ledger['ledgerCloseTime'].isdigit()
674+
# headerXdr is a base64 LedgerHeaderHistoryEntry for this ledger.
675+
header = xdr.LedgerHeaderHistoryEntry.from_xdr(ledger['headerXdr'])
676+
assert header.header.ledger_seq.uint32 == ledger['sequence']
677+
# metadataXdr is a base64 LedgerCloseMeta union for this ledger.
678+
meta = xdr.LedgerCloseMeta.from_xdr(ledger['metadataXdr'])
679+
meta_header = getattr(meta, f'v{meta.v}').ledger_header
680+
assert meta_header.header.ledger_seq.uint32 == ledger['sequence']
681+
# Ledger hashes identify ledgers and must be unique.
682+
assert len(hashes) == 2
683+
684+
# The limit for getLedgers ranges from 1 to 200.
685+
bad_limit = _rpc(server.port(), 'getLedgers', {'startLedger': 1, 'pagination': {'limit': 201}})
686+
assert bad_limit['error']['code'] == -32602
687+
688+
689+
def test_get_ledgers_pagination(server: StellarRpcServer) -> None:
690+
"""A limited page returns a cursor from which the next page resumes without overlap."""
691+
_send_create_accounts(server, 2)
692+
693+
page1 = _rpc_result(server.port(), 'getLedgers', {'startLedger': 1, 'pagination': {'limit': 1}})
694+
assert [ledger['sequence'] for ledger in page1['ledgers']] == [1]
695+
cursor = page1['cursor']
696+
assert isinstance(cursor, str) and cursor != ''
697+
698+
page2 = _rpc_result(server.port(), 'getLedgers', {'pagination': {'cursor': cursor, 'limit': 1}})
699+
assert [ledger['sequence'] for ledger in page2['ledgers']] == [2]
700+
701+
702+
def test_get_ledgers_invalid_params(server: StellarRpcServer) -> None:
703+
port = server.port()
704+
# startLedger beyond the latest ledger (0 on a fresh chain) is out of retention range.
705+
assert _rpc(port, 'getLedgers', {'startLedger': 999})['error']['code'] == -32602
706+
# startLedger and cursor are mutually exclusive.
707+
both = _rpc(port, 'getLedgers', {'startLedger': 1, 'pagination': {'cursor': '1'}})
708+
assert both['error']['code'] == -32602

0 commit comments

Comments
 (0)