Skip to content

Commit a4219b6

Browse files
test: add integration tests for getLedgerEntries
Cover the official spec surface: ACCOUNT, CONTRACT_CODE, and CONTRACT_DATA (instance and persistent storage) lookups, base64 key/xdr round-trips, numeric latestLedger/lastModifiedLedgerSeq, silent drop of unknown keys, the 200-key cap, key and xdrFormat validation errors, and rejection of the unsupported json xdrFormat. Adds a storage.wat fixture whose store() function writes a persistent contract-data entry via put_contract_data.
1 parent 8f392c5 commit a4219b6

2 files changed

Lines changed: 265 additions & 1 deletion

File tree

Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,31 @@
1+
(module
2+
(type (;0;) (func))
3+
(type (;1;) (func (result i64)))
4+
(type (;2;) (func (param i64 i64 i64) (result i64)))
5+
6+
;; put_contract_data(key, val, storage_type) -> Void
7+
(import "l" "_" (func $put_contract_data (type 2)))
8+
9+
;; store: write the persistent storage entry U32(7) -> U32(42), return Void.
10+
;; u32 HostVals carry the payload in the high 32 bits and tag 4 in the low bits;
11+
;; the storage type is a raw integer (0: temporary, 1: persistent, 2: instance).
12+
(func $store (type 1) (result i64)
13+
i64.const 30064771076 ;; key U32(7): (7 << 32) | 4
14+
i64.const 180388626436 ;; val U32(42): (42 << 32) | 4
15+
i64.const 1 ;; storage type: persistent
16+
call $put_contract_data)
17+
18+
;; _ (Soroban ABI stub)
19+
(func $stub (type 0))
20+
21+
(memory (;0;) 16)
22+
(global (;0;) (mut i32) (i32.const 1048576))
23+
(global (;1;) i32 (i32.const 1048576))
24+
(global (;2;) i32 (i32.const 1048576))
25+
26+
(export "memory" (memory 0))
27+
(export "store" (func $store))
28+
(export "_" (func $stub))
29+
(export "__data_end" (global 1))
30+
(export "__heap_base" (global 2))
31+
)

src/tests/integration/test_server.py

Lines changed: 234 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -11,14 +11,15 @@
1111
from typing import Any
1212

1313
import pytest
14-
from stellar_sdk import Account, Keypair, Network, StrKey, TransactionBuilder, xdr
14+
from stellar_sdk import Account, Address, Keypair, Network, StrKey, TransactionBuilder, xdr
1515
from stellar_sdk.xdr.sc_val_type import SCValType
1616

1717
from komet_node.server import StellarRpcServer
1818

1919
EMPTY_CONTRACT_WAT = (Path(__file__).parent / 'data' / 'wasm' / 'empty.wat').resolve(strict=True)
2020
ARGS_CONTRACT_WAT = (Path(__file__).parent / 'data' / 'wasm' / 'args.wat').resolve(strict=True)
2121
ADDER_CONTRACT_WAT = (Path(__file__).parent / 'data' / 'wasm' / 'adder.wat').resolve(strict=True)
22+
STORAGE_CONTRACT_WAT = (Path(__file__).parent / 'data' / 'wasm' / 'storage.wat').resolve(strict=True)
2223

2324

2425
def wat_to_wasm(wat_path: Path) -> bytes:
@@ -498,3 +499,235 @@ def builder() -> TransactionBuilder:
498499

499500
# All four transactions, including the non-Void invocation, advanced the ledger.
500501
assert _rpc(server.port(), 'getLatestLedger', {})['result']['sequence'] == 4
502+
503+
504+
# ----------------------------------------------------------------------
505+
# getLedgerEntries
506+
# ----------------------------------------------------------------------
507+
#
508+
# Spec: stellar-docs OpenRPC getLedgerEntries.json + stellar-rpc's Go serialization
509+
# (GetLedgerEntriesResponse). Result is {entries, latestLedger}; latestLedger and
510+
# lastModifiedLedgerSeq are JSON numbers; liveUntilLedgerSeq is optional (omitted when the
511+
# entry has no TTL); only found entries are returned; `key`/`xdr` are base64 LedgerKey /
512+
# LedgerEntryData strings.
513+
514+
515+
def _account_ledger_key(public_key: str) -> str:
516+
return xdr.LedgerKey(
517+
type=xdr.LedgerEntryType.ACCOUNT,
518+
account=xdr.LedgerKeyAccount(account_id=Keypair.from_public_key(public_key).xdr_account_id()),
519+
).to_xdr()
520+
521+
522+
def _contract_code_ledger_key(wasm_hash: bytes) -> str:
523+
return xdr.LedgerKey(
524+
type=xdr.LedgerEntryType.CONTRACT_CODE,
525+
contract_code=xdr.LedgerKeyContractCode(hash=xdr.Hash(wasm_hash)),
526+
).to_xdr()
527+
528+
529+
def _contract_data_ledger_key(contract_address: str, key: xdr.SCVal, durability: xdr.ContractDataDurability) -> str:
530+
return xdr.LedgerKey(
531+
type=xdr.LedgerEntryType.CONTRACT_DATA,
532+
contract_data=xdr.LedgerKeyContractData(
533+
contract=Address(contract_address).to_xdr_sc_address(),
534+
key=key,
535+
durability=durability,
536+
),
537+
).to_xdr()
538+
539+
540+
def _assert_ledger_entry_shape(entry: dict[str, Any], expected_key: str) -> None:
541+
"""Assert one entry matches the Go LedgerEntryResult serialization (base64 format)."""
542+
assert {'key', 'xdr', 'lastModifiedLedgerSeq'} <= set(entry)
543+
assert set(entry) <= {'key', 'xdr', 'lastModifiedLedgerSeq', 'liveUntilLedgerSeq'}
544+
assert entry['key'] == expected_key
545+
assert type(entry['xdr']) is str and entry['xdr'] != ''
546+
assert type(entry['lastModifiedLedgerSeq']) is int # JSON number, not string
547+
if 'liveUntilLedgerSeq' in entry: # optional; only Soroban entries carry a TTL
548+
assert type(entry['liveUntilLedgerSeq']) is int
549+
550+
551+
def _send_tx(server: StellarRpcServer, keypair: Keypair, tb: TransactionBuilder) -> None:
552+
env = tb.set_timeout(30).build()
553+
env.sign(keypair)
554+
res = _rpc(server.port(), 'sendTransaction', {'transaction': env.to_xdr()})
555+
assert res['result']['status'] == 'PENDING'
556+
get_res = _rpc(server.port(), 'getTransaction', {'hash': res['result']['hash']})['result']
557+
assert get_res['status'] == 'SUCCESS', f'Transaction failed: {get_res}'
558+
559+
560+
def test_get_ledger_entries_account(server: StellarRpcServer) -> None:
561+
"""An ACCOUNT ledger key resolves to an AccountEntry; unknown keys are silently dropped."""
562+
keypair = Keypair.random()
563+
account = Account(keypair.public_key, sequence=0)
564+
_send_tx(
565+
server,
566+
keypair,
567+
TransactionBuilder(account, Network.TESTNET_NETWORK_PASSPHRASE).append_create_account_op(
568+
destination=keypair.public_key, starting_balance='1000'
569+
),
570+
)
571+
572+
account_key = _account_ledger_key(keypair.public_key)
573+
missing_key = _account_ledger_key(Keypair.random().public_key)
574+
result = _rpc(server.port(), 'getLedgerEntries', {'keys': [account_key, missing_key]})['result']
575+
576+
assert set(result) == {'entries', 'latestLedger'}
577+
assert result['latestLedger'] == 1
578+
assert type(result['latestLedger']) is int # JSON number, not string
579+
580+
# Only the found entry is returned; the unknown key is not an error, just absent.
581+
assert len(result['entries']) == 1
582+
entry = result['entries'][0]
583+
_assert_ledger_entry_shape(entry, account_key)
584+
assert 0 <= entry['lastModifiedLedgerSeq'] <= result['latestLedger']
585+
586+
data = xdr.LedgerEntryData.from_xdr(entry['xdr'])
587+
assert data.type == xdr.LedgerEntryType.ACCOUNT
588+
assert data.account is not None
589+
assert data.account.account_id == Keypair.from_public_key(keypair.public_key).xdr_account_id()
590+
assert data.account.balance.int64 == 10_000_000_000 # 1000 XLM in stroops
591+
592+
593+
def test_get_ledger_entries_contract_code_and_data(server: StellarRpcServer) -> None:
594+
"""CONTRACT_CODE, the CONTRACT_DATA instance entry, and persistent CONTRACT_DATA storage."""
595+
from stellar_sdk.utils import sha256
596+
597+
keypair = Keypair.random()
598+
account = Account(keypair.public_key, sequence=0)
599+
600+
def builder() -> TransactionBuilder:
601+
return TransactionBuilder(account, Network.TESTNET_NETWORK_PASSPHRASE)
602+
603+
# Set up: create account, upload storage.wat, deploy, invoke store() which writes the
604+
# persistent storage entry U32(7) -> U32(42).
605+
_send_tx(server, keypair, builder().append_create_account_op(keypair.public_key, '1000'))
606+
wasm_bytecode = wat_to_wasm(STORAGE_CONTRACT_WAT)
607+
_send_tx(server, keypair, builder().append_upload_contract_wasm_op(wasm_bytecode))
608+
wasm_hash = sha256(wasm_bytecode)
609+
salt = b'\x00' * 32
610+
_send_tx(server, keypair, builder().append_create_contract_op(wasm_hash, keypair.public_key, None, salt))
611+
contract_address = server.encoder.contract_address_from_deployer_address(keypair.public_key, salt)
612+
_send_tx(server, keypair, builder().append_invoke_contract_function_op(contract_address, 'store', []))
613+
614+
code_key = _contract_code_ledger_key(wasm_hash)
615+
instance_key = _contract_data_ledger_key(
616+
contract_address,
617+
xdr.SCVal(type=SCValType.SCV_LEDGER_KEY_CONTRACT_INSTANCE),
618+
xdr.ContractDataDurability.PERSISTENT,
619+
)
620+
storage_key_scval = xdr.SCVal(type=SCValType.SCV_U32, u32=xdr.Uint32(7))
621+
storage_key = _contract_data_ledger_key(contract_address, storage_key_scval, xdr.ContractDataDurability.PERSISTENT)
622+
623+
result = _rpc(server.port(), 'getLedgerEntries', {'keys': [code_key, instance_key, storage_key]})['result']
624+
assert set(result) == {'entries', 'latestLedger'}
625+
assert result['latestLedger'] == 4
626+
assert type(result['latestLedger']) is int
627+
628+
entries = {entry['key']: entry for entry in result['entries']}
629+
assert set(entries) == {code_key, instance_key, storage_key}
630+
for key, entry in entries.items():
631+
_assert_ledger_entry_shape(entry, key)
632+
633+
# CONTRACT_CODE: the uploaded wasm bytecode round-trips through the ledger entry.
634+
code_data = xdr.LedgerEntryData.from_xdr(entries[code_key]['xdr'])
635+
assert code_data.type == xdr.LedgerEntryType.CONTRACT_CODE
636+
assert code_data.contract_code is not None
637+
assert code_data.contract_code.hash.hash == wasm_hash
638+
assert code_data.contract_code.code == wasm_bytecode
639+
640+
# CONTRACT_DATA (instance): the deployed contract's instance entry points at the wasm.
641+
instance_data = xdr.LedgerEntryData.from_xdr(entries[instance_key]['xdr'])
642+
assert instance_data.type == xdr.LedgerEntryType.CONTRACT_DATA
643+
assert instance_data.contract_data is not None
644+
assert instance_data.contract_data.contract == Address(contract_address).to_xdr_sc_address()
645+
assert instance_data.contract_data.durability == xdr.ContractDataDurability.PERSISTENT
646+
assert instance_data.contract_data.key.type == SCValType.SCV_LEDGER_KEY_CONTRACT_INSTANCE
647+
assert instance_data.contract_data.val.type == SCValType.SCV_CONTRACT_INSTANCE
648+
instance = instance_data.contract_data.val.instance
649+
assert instance is not None
650+
assert instance.executable.type == xdr.ContractExecutableType.CONTRACT_EXECUTABLE_WASM
651+
assert instance.executable.wasm_hash is not None
652+
assert instance.executable.wasm_hash.hash == wasm_hash
653+
654+
# CONTRACT_DATA (persistent): the value written by store() is readable.
655+
storage_data = xdr.LedgerEntryData.from_xdr(entries[storage_key]['xdr'])
656+
assert storage_data.type == xdr.LedgerEntryType.CONTRACT_DATA
657+
assert storage_data.contract_data is not None
658+
assert storage_data.contract_data.contract == Address(contract_address).to_xdr_sc_address()
659+
assert storage_data.contract_data.durability == xdr.ContractDataDurability.PERSISTENT
660+
assert storage_data.contract_data.key == storage_key_scval
661+
assert storage_data.contract_data.val == xdr.SCVal(type=SCValType.SCV_U32, u32=xdr.Uint32(42))
662+
663+
664+
def test_get_ledger_entries_no_matches_returns_empty_entries(server: StellarRpcServer) -> None:
665+
"""Unknown keys are not an error: the result is an empty entries array."""
666+
result = _rpc(server.port(), 'getLedgerEntries', {'keys': [_account_ledger_key(Keypair.random().public_key)]})[
667+
'result'
668+
]
669+
assert result['entries'] == []
670+
assert result['latestLedger'] == 0
671+
assert type(result['latestLedger']) is int
672+
673+
674+
def test_get_ledger_entries_unsupported_entry_type_is_not_found(server: StellarRpcServer) -> None:
675+
"""A well-formed key of a type komet-node does not track (DATA) is simply not found."""
676+
data_key = xdr.LedgerKey(
677+
type=xdr.LedgerEntryType.DATA,
678+
data=xdr.LedgerKeyData(
679+
account_id=Keypair.random().xdr_account_id(),
680+
data_name=xdr.String64(b'config'),
681+
),
682+
).to_xdr()
683+
result = _rpc(server.port(), 'getLedgerEntries', {'keys': [data_key]})['result']
684+
assert result['entries'] == []
685+
686+
687+
def test_get_ledger_entries_xdr_format_base64_accepted(server: StellarRpcServer) -> None:
688+
"""xdrFormat 'base64' is the explicit spelling of the default and must be accepted."""
689+
keys = [_account_ledger_key(Keypair.random().public_key)]
690+
result = _rpc(server.port(), 'getLedgerEntries', {'keys': keys, 'xdrFormat': 'base64'})
691+
assert 'error' not in result
692+
assert result['result']['entries'] == []
693+
694+
695+
def test_get_ledger_entries_xdr_format_json_rejected(server: StellarRpcServer) -> None:
696+
"""komet-node does not support the JSON XDR format; asking for it is an invalid-params error."""
697+
keys = [_account_ledger_key(Keypair.random().public_key)]
698+
result = _rpc(server.port(), 'getLedgerEntries', {'keys': keys, 'xdrFormat': 'json'})
699+
assert result['error']['code'] == -32602
700+
assert type(result['error']['message']) is str and result['error']['message'] != ''
701+
702+
703+
def test_get_ledger_entries_invalid_xdr_format_rejected(server: StellarRpcServer) -> None:
704+
keys = [_account_ledger_key(Keypair.random().public_key)]
705+
result = _rpc(server.port(), 'getLedgerEntries', {'keys': keys, 'xdrFormat': 'xml'})
706+
assert result['error']['code'] == -32602
707+
708+
709+
def test_get_ledger_entries_missing_keys_returns_invalid_params(server: StellarRpcServer) -> None:
710+
result = _rpc(server.port(), 'getLedgerEntries', {})
711+
assert result['error']['code'] == -32602
712+
713+
714+
def test_get_ledger_entries_non_array_keys_returns_invalid_params(server: StellarRpcServer) -> None:
715+
result = _rpc(server.port(), 'getLedgerEntries', {'keys': 'AAAAAA=='})
716+
assert result['error']['code'] == -32602
717+
718+
719+
def test_get_ledger_entries_non_string_key_returns_invalid_params(server: StellarRpcServer) -> None:
720+
result = _rpc(server.port(), 'getLedgerEntries', {'keys': [42]})
721+
assert result['error']['code'] == -32602
722+
723+
724+
def test_get_ledger_entries_invalid_key_xdr_returns_invalid_params(server: StellarRpcServer) -> None:
725+
result = _rpc(server.port(), 'getLedgerEntries', {'keys': ['not-a-ledger-key']})
726+
assert result['error']['code'] == -32602
727+
728+
729+
def test_get_ledger_entries_too_many_keys_returns_invalid_params(server: StellarRpcServer) -> None:
730+
"""The spec caps a request at 200 ledger keys."""
731+
key = _account_ledger_key(Keypair.random().public_key)
732+
result = _rpc(server.port(), 'getLedgerEntries', {'keys': [key] * 201})
733+
assert result['error']['code'] == -32602

0 commit comments

Comments
 (0)