Skip to content

Commit

Permalink
feat: add support for Soroban PRC's getLedgers API interfaces. (#992)
Browse files Browse the repository at this point in the history
  • Loading branch information
overcat authored Nov 14, 2024
1 parent 121f459 commit 2f6800b
Show file tree
Hide file tree
Showing 6 changed files with 185 additions and 0 deletions.
3 changes: 3 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,9 @@ Release History

### Pending

#### Update
- feat: add support for Soroban PRC's `getLedgers` API interfaces. ([#992](https://github.com/StellarCN/py-stellar-base/pull/992))

### Version 12.0.0-beta1

Released on November 01, 2024
Expand Down
35 changes: 35 additions & 0 deletions stellar_sdk/soroban_rpc.py
Original file line number Diff line number Diff line change
Expand Up @@ -427,3 +427,38 @@ class GetVersionInfoResponse(BaseModel):
build_timestamp: str = Field(alias="buildTimestamp")
captive_core_version: str = Field(alias="captiveCoreVersion")
protocol_version: int = Field(alias="protocolVersion")


# get_ledgers
class GetLedgersRequest(BaseModel):
"""Request for JSON-RPC method getLedgers.
See `getLedgers documentation <https://developers.stellar.org/docs/data/rpc/api-reference/methods/getLedgers>`__ for
more information."""

start_ledger: int = Field(alias="startLedger")
pagination: Optional[PaginationOptions] = None


class LedgerInfo(BaseModel):
hash: str
sequence: int
ledger_close_time: int = Field(alias="ledgerCloseTime")
# LedgerHeaderHistoryEntry XDR in base64
ledger_header: str = Field(alias="headerXdr")
# LedgerCloseMeta XDR in base64
ledger_metadata: str = Field(alias="metadataXdr")


class GetLedgersResponse(BaseModel):
"""Response for JSON-RPC method getLedgers.
See `getLedgers documentation <https://developers.stellar.org/docs/data/rpc/api-reference/methods/getLedgers>`__ for
more information."""

ledgers: List[LedgerInfo]
latest_ledger: int = Field(alias="latestLedger")
latest_ledger_close_time: int = Field(alias="latestLedgerCloseTime")
oldest_ledger: int = Field(alias="oldestLedger")
oldest_ledger_close_time: int = Field(alias="oldestLedgerCloseTime")
cursor: str
24 changes: 24 additions & 0 deletions stellar_sdk/soroban_server.py
Original file line number Diff line number Diff line change
Expand Up @@ -268,6 +268,30 @@ def get_transactions(
)
return self._post(request, GetTransactionsResponse)

def get_ledgers(
self, start_ledger: int, cursor: str = None, limit: int = None
) -> GetLedgersResponse:
"""Fetch a detailed list of ledgers starting from the user specified starting point that you can paginate
as long as the pages fall within the history retention of their corresponding RPC provider.
See `Soroban RPC Documentation - getLedgers <https://developers.stellar.org/docs/data/rpc/api-reference/methods/getLedgers>`_
:param start_ledger: The first ledger to include in the results.
:param cursor: A cursor value for use in pagination.
:param limit: The maximum number of records to return.
:return: A :class:`GetLedgersResponse <stellar_sdk.soroban_rpc.GetLedgersResponse>` object.
:raises: :exc:`SorobanRpcErrorResponse <stellar_sdk.exceptions.SorobanRpcErrorResponse>` - If the Soroban-RPC instance returns an error response.
"""
pagination = PaginationOptions(cursor=cursor, limit=limit)
data = GetLedgersRequest(
startLedger=str(start_ledger),
pagination=pagination,
)
request: Request = Request[GetLedgersRequest](
id=_generate_unique_request_id(), method="getLedgers", params=data
)
return self._post(request, GetLedgersResponse)

def load_account(self, account_id: str) -> Account:
"""Load an account from the server, you can use the returned account
object as source account for transactions.
Expand Down
24 changes: 24 additions & 0 deletions stellar_sdk/soroban_server_async.py
Original file line number Diff line number Diff line change
Expand Up @@ -267,6 +267,30 @@ async def get_transactions(
)
return await self._post(request, GetTransactionsResponse)

async def get_ledgers(
self, start_ledger: int, cursor: str = None, limit: int = None
) -> GetLedgersResponse:
"""Fetch a detailed list of ledgers starting from the user specified starting point that you can paginate
as long as the pages fall within the history retention of their corresponding RPC provider.
See `Soroban RPC Documentation - getLedgers <https://developers.stellar.org/docs/data/rpc/api-reference/methods/getLedgers>`_
:param start_ledger: The first ledger to include in the results.
:param cursor: A cursor value for use in pagination.
:param limit: The maximum number of records to return.
:return: A :class:`GetLedgersResponse <stellar_sdk.soroban_rpc.GetLedgersResponse>` object.
:raises: :exc:`SorobanRpcErrorResponse <stellar_sdk.exceptions.SorobanRpcErrorResponse>` - If the Soroban-RPC instance returns an error response.
"""
pagination = PaginationOptions(cursor=cursor, limit=limit)
data = GetLedgersRequest(
startLedger=str(start_ledger),
pagination=pagination,
)
request: Request = Request[GetLedgersRequest](
id=_generate_unique_request_id(), method="getLedgers", params=data
)
return await self._post(request, GetLedgersResponse)

async def load_account(self, account_id: str) -> Account:
"""Load an account from the server, you can use the returned account
object as source account for transactions.
Expand Down
50 changes: 50 additions & 0 deletions tests/test_soroban_server_async.py
Original file line number Diff line number Diff line change
Expand Up @@ -593,6 +593,56 @@ async def test_get_transactions(self):
"pagination": {"cursor": None, "limit": 5},
}

async def test_get_ledgers(self):
result = {
"ledgers": [
{
"hash": "59ccafc5641a44826608a882da10b08f585b2b614be91976ade3927d4422413d",
"sequence": 10,
"ledgerCloseTime": "1731554414",
"headerXdr": "WcyvxWQaRIJmCKiC2hCwj1hbK2FL6Rl2reOSfUQiQT0AAAAVwLYrLkYHkh0rJ+GmAbGkeHIQpyDsekpf68ThNNsCXYI2jMO8189Z2jYaMg/mttiL9SVfJXThDGEIYe/klkKWMwAAAABnNWxuAAAAAAAAAAEAAAAApo8dlon1AzCqtWnWdzJ01L+/6QHyNiUhUM5rTILgk/cAAABAnJJp+kohsfTVKJYclwggQFe/Eie6zoL43O0xvKlYoYsd1I09szNQBqserhqzq+9WSTNS3wUZ8YC4U4e8O5+7AN8/YZgEqS/bQFcZLcQ910jqd4rcUrxJjOgFJMAUuBEZTxx260gYA3GwBr7nqsHUDnR+DGVQDc1IyOubd/pi5zcAAAAKDeC2s6dkAAAAAAAAAAPpLwAAAAAAAAAAAAAAAAAAAGQATEtAAAAAZAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA=",
"metadataXdr": "AAAAAQAAAABZzK/FZBpEgmYIqILaELCPWFsrYUvpGXat45J9RCJBPQAAABXAtisuRgeSHSsn4aYBsaR4chCnIOx6Sl/rxOE02wJdgjaMw7zXz1naNhoyD+a22Iv1JV8ldOEMYQhh7+SWQpYzAAAAAGc1bG4AAAAAAAAAAQAAAACmjx2WifUDMKq1adZ3MnTUv7/pAfI2JSFQzmtMguCT9wAAAECckmn6SiGx9NUolhyXCCBAV78SJ7rOgvjc7TG8qVihix3UjT2zM1AGqx6uGrOr71ZJM1LfBRnxgLhTh7w7n7sA3z9hmASpL9tAVxktxD3XSOp3itxSvEmM6AUkwBS4ERlPHHbrSBgDcbAGvueqwdQOdH4MZVANzUjI65t3+mLnNwAAAAoN4Lazp2QAAAAAAAAAA+kvAAAAAAAAAAAAAAAAAAAAZABMS0AAAABkAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAHAtisuRgeSHSsn4aYBsaR4chCnIOx6Sl/rxOE02wJdggAAAAIAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAGQAAAAAAAAAAA==",
},
{
"hash": "76b188897ab785d81a85508ad4434bb0c33a74c9c0874165120ccd3158f76ed3",
"sequence": 11,
"ledgerCloseTime": "1731554415",
"headerXdr": "drGIiXq3hdgahVCK1ENLsMM6dMnAh0FlEgzNMVj3btMAAAAVWcyvxWQaRIJmCKiC2hCwj1hbK2FL6Rl2reOSfUQiQT37/jXH2AmHKzOU33bzTVlQU8zUjm7dqF8UMsn+7c+c5QAAAABnNWxvAAAAAAAAAAEAAAAApo8dlon1AzCqtWnWdzJ01L+/6QHyNiUhUM5rTILgk/cAAABA/jgs2CQswku2XhYsIi8JODW/Fe9P6di07ZXI2pWjCPNjqZ4/PHVASh4R360tEJKNDerKq4N+8sULaM2TwxrABt8/YZgEqS/bQFcZLcQ910jqd4rcUrxJjOgFJMAUuBEZQZ8jWama0eWGWYdhBgDMBWlFpkK440lHonFcFQm0FpUAAAALDeC2s6dkAAAAAAAAAAPpLwAAAAAAAAAAAAAAAAAAAGQATEtAAAAAZAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA=",
"metadataXdr": "AAAAAQAAAAB2sYiJereF2BqFUIrUQ0uwwzp0ycCHQWUSDM0xWPdu0wAAABVZzK/FZBpEgmYIqILaELCPWFsrYUvpGXat45J9RCJBPfv+NcfYCYcrM5TfdvNNWVBTzNSObt2oXxQyyf7tz5zlAAAAAGc1bG8AAAAAAAAAAQAAAACmjx2WifUDMKq1adZ3MnTUv7/pAfI2JSFQzmtMguCT9wAAAED+OCzYJCzCS7ZeFiwiLwk4Nb8V70/p2LTtlcjalaMI82Opnj88dUBKHhHfrS0Qko0N6sqrg37yxQtozZPDGsAG3z9hmASpL9tAVxktxD3XSOp3itxSvEmM6AUkwBS4ERlBnyNZqZrR5YZZh2EGAMwFaUWmQrjjSUeicVwVCbQWlQAAAAsN4Lazp2QAAAAAAAAAA+kvAAAAAAAAAAAAAAAAAAAAZABMS0AAAABkAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAFZzK/FZBpEgmYIqILaELCPWFsrYUvpGXat45J9RCJBPQAAAAIAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAGQAAAAAAAAAAA==",
},
],
"latestLedger": 113,
"latestLedgerCloseTime": 1731554518,
"oldestLedger": 8,
"oldestLedgerCloseTime": 1731554412,
"cursor": "11",
}

data = {
"jsonrpc": "2.0",
"id": "198cb1a8-9104-4446-a269-88bf000c2721",
"result": result,
}

start_ledger = 10
GetLedgersResponse.model_validate(result)
limit = 2
with aioresponses() as m:
m.post(PRC_URL, payload=data)
async with SorobanServerAsync(PRC_URL) as client:
assert (
await client.get_ledgers(start_ledger, None, limit)
) == GetLedgersResponse.model_validate(result)

request_data = m.requests[("POST", URL(PRC_URL))][0].kwargs["json"]
assert len(request_data["id"]) == 32
assert request_data["jsonrpc"] == "2.0"
assert request_data["method"] == "getLedgers"
assert request_data["params"] == {
"startLedger": start_ledger,
"pagination": {"cursor": None, "limit": 2},
}

async def test_simulate_transaction(self):
result = {
"transactionData": "AAAAAAAAAAIAAAAGAAAAAcWLK/vE8FTnMk9r8gytPgJuQbutGm0gw9fUkY3tFlQRAAAAFAAAAAEAAAAAAAAAB300Hyg0HZG+Qie3zvsxLvugrNtFqd3AIntWy9bg2YvZAAAAAAAAAAEAAAAGAAAAAcWLK/vE8FTnMk9r8gytPgJuQbutGm0gw9fUkY3tFlQRAAAAEAAAAAEAAAACAAAADwAAAAdDb3VudGVyAAAAABIAAAAAAAAAAFi3xKLI8peqjz0kcSgf38zsr+SOVmMxPsGOEqc+ypihAAAAAQAAAAAAFcLDAAAF8AAAAQgAAAMcAAAAAAAAAJw=",
Expand Down
49 changes: 49 additions & 0 deletions tests/test_soroban_server_sync.py
Original file line number Diff line number Diff line change
Expand Up @@ -579,6 +579,55 @@ def test_get_transactions(self):
"pagination": {"cursor": None, "limit": 5},
}

def test_get_ledgers(self):
result = {
"ledgers": [
{
"hash": "59ccafc5641a44826608a882da10b08f585b2b614be91976ade3927d4422413d",
"sequence": 10,
"ledgerCloseTime": "1731554414",
"headerXdr": "WcyvxWQaRIJmCKiC2hCwj1hbK2FL6Rl2reOSfUQiQT0AAAAVwLYrLkYHkh0rJ+GmAbGkeHIQpyDsekpf68ThNNsCXYI2jMO8189Z2jYaMg/mttiL9SVfJXThDGEIYe/klkKWMwAAAABnNWxuAAAAAAAAAAEAAAAApo8dlon1AzCqtWnWdzJ01L+/6QHyNiUhUM5rTILgk/cAAABAnJJp+kohsfTVKJYclwggQFe/Eie6zoL43O0xvKlYoYsd1I09szNQBqserhqzq+9WSTNS3wUZ8YC4U4e8O5+7AN8/YZgEqS/bQFcZLcQ910jqd4rcUrxJjOgFJMAUuBEZTxx260gYA3GwBr7nqsHUDnR+DGVQDc1IyOubd/pi5zcAAAAKDeC2s6dkAAAAAAAAAAPpLwAAAAAAAAAAAAAAAAAAAGQATEtAAAAAZAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA=",
"metadataXdr": "AAAAAQAAAABZzK/FZBpEgmYIqILaELCPWFsrYUvpGXat45J9RCJBPQAAABXAtisuRgeSHSsn4aYBsaR4chCnIOx6Sl/rxOE02wJdgjaMw7zXz1naNhoyD+a22Iv1JV8ldOEMYQhh7+SWQpYzAAAAAGc1bG4AAAAAAAAAAQAAAACmjx2WifUDMKq1adZ3MnTUv7/pAfI2JSFQzmtMguCT9wAAAECckmn6SiGx9NUolhyXCCBAV78SJ7rOgvjc7TG8qVihix3UjT2zM1AGqx6uGrOr71ZJM1LfBRnxgLhTh7w7n7sA3z9hmASpL9tAVxktxD3XSOp3itxSvEmM6AUkwBS4ERlPHHbrSBgDcbAGvueqwdQOdH4MZVANzUjI65t3+mLnNwAAAAoN4Lazp2QAAAAAAAAAA+kvAAAAAAAAAAAAAAAAAAAAZABMS0AAAABkAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAHAtisuRgeSHSsn4aYBsaR4chCnIOx6Sl/rxOE02wJdggAAAAIAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAGQAAAAAAAAAAA==",
},
{
"hash": "76b188897ab785d81a85508ad4434bb0c33a74c9c0874165120ccd3158f76ed3",
"sequence": 11,
"ledgerCloseTime": "1731554415",
"headerXdr": "drGIiXq3hdgahVCK1ENLsMM6dMnAh0FlEgzNMVj3btMAAAAVWcyvxWQaRIJmCKiC2hCwj1hbK2FL6Rl2reOSfUQiQT37/jXH2AmHKzOU33bzTVlQU8zUjm7dqF8UMsn+7c+c5QAAAABnNWxvAAAAAAAAAAEAAAAApo8dlon1AzCqtWnWdzJ01L+/6QHyNiUhUM5rTILgk/cAAABA/jgs2CQswku2XhYsIi8JODW/Fe9P6di07ZXI2pWjCPNjqZ4/PHVASh4R360tEJKNDerKq4N+8sULaM2TwxrABt8/YZgEqS/bQFcZLcQ910jqd4rcUrxJjOgFJMAUuBEZQZ8jWama0eWGWYdhBgDMBWlFpkK440lHonFcFQm0FpUAAAALDeC2s6dkAAAAAAAAAAPpLwAAAAAAAAAAAAAAAAAAAGQATEtAAAAAZAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA=",
"metadataXdr": "AAAAAQAAAAB2sYiJereF2BqFUIrUQ0uwwzp0ycCHQWUSDM0xWPdu0wAAABVZzK/FZBpEgmYIqILaELCPWFsrYUvpGXat45J9RCJBPfv+NcfYCYcrM5TfdvNNWVBTzNSObt2oXxQyyf7tz5zlAAAAAGc1bG8AAAAAAAAAAQAAAACmjx2WifUDMKq1adZ3MnTUv7/pAfI2JSFQzmtMguCT9wAAAED+OCzYJCzCS7ZeFiwiLwk4Nb8V70/p2LTtlcjalaMI82Opnj88dUBKHhHfrS0Qko0N6sqrg37yxQtozZPDGsAG3z9hmASpL9tAVxktxD3XSOp3itxSvEmM6AUkwBS4ERlBnyNZqZrR5YZZh2EGAMwFaUWmQrjjSUeicVwVCbQWlQAAAAsN4Lazp2QAAAAAAAAAA+kvAAAAAAAAAAAAAAAAAAAAZABMS0AAAABkAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAFZzK/FZBpEgmYIqILaELCPWFsrYUvpGXat45J9RCJBPQAAAAIAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAGQAAAAAAAAAAA==",
},
],
"latestLedger": 113,
"latestLedgerCloseTime": 1731554518,
"oldestLedger": 8,
"oldestLedgerCloseTime": 1731554412,
"cursor": "11",
}

data = {
"jsonrpc": "2.0",
"id": "198cb1a8-9104-4446-a269-88bf000c2721",
"result": result,
}

start_ledger = 10
GetLedgersResponse.model_validate(result)
limit = 2
with requests_mock.Mocker() as m:
m.post(PRC_URL, json=data)
assert SorobanServer(PRC_URL).get_ledgers(
start_ledger, None, limit
) == GetLedgersResponse.model_validate(result)

request_data = m.last_request.json()
assert len(request_data["id"]) == 32
assert request_data["jsonrpc"] == "2.0"
assert request_data["method"] == "getLedgers"
assert request_data["params"] == {
"startLedger": start_ledger,
"pagination": {"cursor": None, "limit": 2},
}

def test_simulate_transaction(self):
result = {
"transactionData": "AAAAAAAAAAIAAAAGAAAAAcWLK/vE8FTnMk9r8gytPgJuQbutGm0gw9fUkY3tFlQRAAAAFAAAAAEAAAAAAAAAB300Hyg0HZG+Qie3zvsxLvugrNtFqd3AIntWy9bg2YvZAAAAAAAAAAEAAAAGAAAAAcWLK/vE8FTnMk9r8gytPgJuQbutGm0gw9fUkY3tFlQRAAAAEAAAAAEAAAACAAAADwAAAAdDb3VudGVyAAAAABIAAAAAAAAAAFi3xKLI8peqjz0kcSgf38zsr+SOVmMxPsGOEqc+ypihAAAAAQAAAAAAFcLDAAAF8AAAAQgAAAMcAAAAAAAAAJw=",
Expand Down

0 comments on commit 2f6800b

Please sign in to comment.