diff --git a/.gitignore b/.gitignore index 799b90cf..593e12d2 100644 --- a/.gitignore +++ b/.gitignore @@ -105,3 +105,4 @@ venv.bak/ # IDE .idea/ +.vscode/ diff --git a/CHANGELOG.md b/CHANGELOG.md index 999073d9..aa504ba1 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,6 +5,7 @@ Release History #### Update - fix: fix the issue where sending assets using `Transaction.append_payment_to_contract_op` fails when the sender's account is the same as the asset issuer's account. ([#1029](https://github.com/StellarCN/py-stellar-base/pull/1029)) +- fix: allow `SorobanServer.get_events()`, `.get_transactions()`, and `.get_ledgers()` to be paginated by making the `start_ledger` argument optional. ([#1032](https://github.com/StellarCN/py-stellar-base/pull/1032)) ### Version 12.2.0 diff --git a/poetry.lock b/poetry.lock index 6b7ee27a..41934f92 100644 --- a/poetry.lock +++ b/poetry.lock @@ -1,4 +1,4 @@ -# This file is automatically @generated by Poetry 2.1.1 and should not be changed by hand. +# This file is automatically @generated by Poetry 2.1.2 and should not be changed by hand. [[package]] name = "aiohappyeyeballs" @@ -1389,16 +1389,15 @@ files = [ [[package]] name = "typing-extensions" -version = "4.12.2" +version = "4.13.2" description = "Backported and Experimental Type Hints for Python 3.8+" optional = false python-versions = ">=3.8" groups = ["main", "dev"] files = [ - {file = "typing_extensions-4.12.2-py3-none-any.whl", hash = "sha256:04e5ca0351e0f3f85c6853954072df659d0d13fac324d0072316b67d7794700d"}, - {file = "typing_extensions-4.12.2.tar.gz", hash = "sha256:1a7ead55c7e559dd4dee8856e3a88b41225abfe1ce8df57b7c13915fe121ffb8"}, + {file = "typing_extensions-4.13.2-py3-none-any.whl", hash = "sha256:a439e7c04b49fec3e5d3e2beaa21755cadbbdc391694e28ccdd36ca4a1408f8c"}, + {file = "typing_extensions-4.13.2.tar.gz", hash = "sha256:e6c81219bd689f51865d9e372991c540bda33a0379d5573cddb9a3a23f7caaef"}, ] -markers = {dev = "python_version < \"3.11\""} [[package]] name = "urllib3" @@ -1569,4 +1568,4 @@ shamir = ["shamir-mnemonic"] [metadata] lock-version = "2.1" python-versions = ">=3.8,<4.0" -content-hash = "a42ba5d091634b0d9e5236e50fa4d4ab15c59c51416f8ba1c2baeab8bf4cac87" +content-hash = "23e576ab1cb88aac04bbb41e91c59071ef3855edcd82bb12ad62db0d0cecb8fa" diff --git a/pyproject.toml b/pyproject.toml index 46f42b12..bbf0df58 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -58,6 +58,7 @@ toml = "^0.10.2" pydantic = "^2.5.2" xdrlib3 = "^0.1.1" requests-sse = ">=0.3,<0.6" +typing-extensions = "^4.13.2" [tool.poetry.extras] aiohttp = ["aiohttp", "aiohttp-sse-client"] diff --git a/stellar_sdk/soroban_rpc.py b/stellar_sdk/soroban_rpc.py index 676d0054..e345cecf 100644 --- a/stellar_sdk/soroban_rpc.py +++ b/stellar_sdk/soroban_rpc.py @@ -2,7 +2,8 @@ from enum import Enum from typing import Any, Dict, Generic, List, Optional, Sequence, TypeVar, Union -from pydantic import BaseModel, ConfigDict, Field +from pydantic import BaseModel, ConfigDict, Field, model_validator +from typing_extensions import Self T = TypeVar("T") @@ -75,16 +76,27 @@ class PaginationOptions(BaseModel): limit: Optional[int] = None -class GetEventsRequest(BaseModel): +class PaginationMixin: + pagination: Optional[PaginationOptions] = None + + @model_validator(mode="after") + def verify_ledger_or_cursor(self) -> Self: + if self.pagination and ( + getattr(self, "start_ledger") and self.pagination.cursor + ): + raise ValueError("start_ledger and cursor cannot both be set") + return self + + +class GetEventsRequest(PaginationMixin, BaseModel): """Response for JSON-RPC method getEvents. See `getEvents documentation `__ for more information. """ - start_ledger: int = Field(alias="startLedger") + start_ledger: Optional[int] = Field(alias="startLedger", default=None) filters: Optional[Sequence[EventFilter]] = None - pagination: Optional[PaginationOptions] = None class GetEventsResponse(BaseModel): @@ -376,14 +388,13 @@ class GetFeeStatsResponse(BaseModel): # get_transactions -class GetTransactionsRequest(BaseModel): +class GetTransactionsRequest(PaginationMixin, BaseModel): """Request for JSON-RPC method getTransactions. See `getTransactions documentation `__ for more information.""" - start_ledger: int = Field(alias="startLedger") - pagination: Optional[PaginationOptions] = None + start_ledger: Optional[int] = Field(alias="startLedger", default=None) class Transaction(BaseModel): @@ -430,14 +441,13 @@ class GetVersionInfoResponse(BaseModel): # get_ledgers -class GetLedgersRequest(BaseModel): +class GetLedgersRequest(PaginationMixin, BaseModel): """Request for JSON-RPC method getLedgers. See `getLedgers documentation `__ for more information.""" - start_ledger: int = Field(alias="startLedger") - pagination: Optional[PaginationOptions] = None + start_ledger: Optional[int] = Field(alias="startLedger", default=None) class LedgerInfo(BaseModel): diff --git a/stellar_sdk/soroban_server.py b/stellar_sdk/soroban_server.py index 7ff38d9d..53edb682 100644 --- a/stellar_sdk/soroban_server.py +++ b/stellar_sdk/soroban_server.py @@ -67,7 +67,7 @@ def get_health(self) -> GetHealthResponse: def get_events( self, - start_ledger: int, + start_ledger: int = None, filters: Sequence[EventFilter] = None, cursor: str = None, limit: int = None, @@ -85,7 +85,7 @@ def get_events( """ pagination = PaginationOptions(cursor=cursor, limit=limit) data = GetEventsRequest( - startLedger=str(start_ledger), + startLedger=start_ledger, filters=filters, pagination=pagination, ) @@ -243,7 +243,7 @@ def get_fee_stats(self) -> GetFeeStatsResponse: def get_transactions( self, - start_ledger: int, + start_ledger: int = None, cursor: str = None, limit: int = None, ) -> GetTransactionsResponse: @@ -260,7 +260,7 @@ def get_transactions( """ pagination = PaginationOptions(cursor=cursor, limit=limit) data = GetTransactionsRequest( - startLedger=str(start_ledger), + startLedger=start_ledger, pagination=pagination, ) request: Request = Request[GetTransactionsRequest]( @@ -269,7 +269,10 @@ def get_transactions( return self._post(request, GetTransactionsResponse) def get_ledgers( - self, start_ledger: int, cursor: str = None, limit: int = None + self, + start_ledger: int = None, + 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. @@ -284,7 +287,7 @@ def get_ledgers( """ pagination = PaginationOptions(cursor=cursor, limit=limit) data = GetLedgersRequest( - startLedger=str(start_ledger), + startLedger=start_ledger, pagination=pagination, ) request: Request = Request[GetLedgersRequest]( diff --git a/stellar_sdk/soroban_server_async.py b/stellar_sdk/soroban_server_async.py index d4f05326..5a009110 100644 --- a/stellar_sdk/soroban_server_async.py +++ b/stellar_sdk/soroban_server_async.py @@ -67,7 +67,7 @@ async def get_health(self) -> GetHealthResponse: async def get_events( self, - start_ledger: int, + start_ledger: int = None, filters: Sequence[EventFilter] = None, cursor: str = None, limit: int = None, @@ -85,7 +85,7 @@ async def get_events( """ pagination = PaginationOptions(cursor=cursor, limit=limit) data = GetEventsRequest( - startLedger=str(start_ledger), + startLedger=start_ledger, filters=filters, pagination=pagination, ) @@ -242,7 +242,7 @@ async def get_fee_stats(self) -> GetFeeStatsResponse: async def get_transactions( self, - start_ledger: int, + start_ledger: int = None, cursor: str = None, limit: int = None, ) -> GetTransactionsResponse: @@ -259,7 +259,7 @@ async def get_transactions( """ pagination = PaginationOptions(cursor=cursor, limit=limit) data = GetTransactionsRequest( - startLedger=str(start_ledger), + startLedger=start_ledger, pagination=pagination, ) request: Request = Request[GetTransactionsRequest]( @@ -268,7 +268,10 @@ async def get_transactions( return await self._post(request, GetTransactionsResponse) async def get_ledgers( - self, start_ledger: int, cursor: str = None, limit: int = None + self, + start_ledger: int = None, + 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. @@ -283,7 +286,7 @@ async def get_ledgers( """ pagination = PaginationOptions(cursor=cursor, limit=limit) data = GetLedgersRequest( - startLedger=str(start_ledger), + startLedger=start_ledger, pagination=pagination, ) request: Request = Request[GetLedgersRequest]( diff --git a/tests/test_soroban_server_async.py b/tests/test_soroban_server_async.py index ed41ea53..49fc0280 100644 --- a/tests/test_soroban_server_async.py +++ b/tests/test_soroban_server_async.py @@ -2,6 +2,7 @@ import pytest from aioresponses import aioresponses +from pydantic import ValidationError from yarl import URL from stellar_sdk import Account, Keypair, Network, TransactionBuilder, scval @@ -16,7 +17,7 @@ from stellar_sdk.soroban_rpc import * from stellar_sdk.soroban_server_async import SorobanServerAsync -PRC_URL = "https://example.com/soroban_rpc" +RPC_URL = "https://example.com/soroban_rpc" @pytest.mark.asyncio @@ -41,12 +42,12 @@ async def test_load_account(self): account_id = "GDAT5HWTGIU4TSSZ4752OUC4SABDLTLZFRPZUJ3D6LKBNEPA7V2CIG54" with aioresponses() as m: - m.post(PRC_URL, payload=data) - async with SorobanServerAsync(PRC_URL) as client: + m.post(RPC_URL, payload=data) + async with SorobanServerAsync(RPC_URL) as client: assert (await client.load_account(account_id)) == Account( account_id, 3418793967628 ) - request_data = m.requests[("POST", URL(PRC_URL))][0].kwargs["json"] + request_data = m.requests[("POST", URL(RPC_URL))][0].kwargs["json"] assert len(request_data["id"]) == 32 assert request_data["jsonrpc"] == "2.0" assert request_data["method"] == "getLedgerEntries" @@ -63,15 +64,15 @@ async def test_load_account_not_found_raise(self): } account_id = "GDAT5HWTGIU4TSSZ4752OUC4SABDLTLZFRPZUJ3D6LKBNEPA7V2CIG54" with aioresponses() as m: - m.post(PRC_URL, payload=data) + m.post(RPC_URL, payload=data) with pytest.raises( AccountNotFoundException, match=f"Account not found, account_id: {account_id}", ): - async with SorobanServerAsync(PRC_URL) as client: + async with SorobanServerAsync(RPC_URL) as client: await client.load_account(account_id) - request_data = m.requests[("POST", URL(PRC_URL))][0].kwargs["json"] + request_data = m.requests[("POST", URL(RPC_URL))][0].kwargs["json"] assert len(request_data["id"]) == 32 assert request_data["jsonrpc"] == "2.0" assert request_data["method"] == "getLedgerEntries" @@ -92,12 +93,12 @@ async def test_get_health(self): "result": result, } with aioresponses() as m: - m.post(PRC_URL, payload=data) - async with SorobanServerAsync(PRC_URL) as client: + m.post(RPC_URL, payload=data) + async with SorobanServerAsync(RPC_URL) as client: assert await client.get_health() == GetHealthResponse.model_validate( result ) - request_data = m.requests[("POST", URL(PRC_URL))][0].kwargs["json"] + request_data = m.requests[("POST", URL(RPC_URL))][0].kwargs["json"] assert len(request_data["id"]) == 32 assert request_data["jsonrpc"] == "2.0" assert request_data["method"] == "getHealth" @@ -118,13 +119,13 @@ async def test_get_network(self): GetNetworkResponse.model_validate(result) with aioresponses() as m: - m.post(PRC_URL, payload=data) - async with SorobanServerAsync(PRC_URL) as client: + m.post(RPC_URL, payload=data) + async with SorobanServerAsync(RPC_URL) as client: assert await client.get_network() == GetNetworkResponse.model_validate( result ) - request_data = m.requests[("POST", URL(PRC_URL))][0].kwargs["json"] + request_data = m.requests[("POST", URL(RPC_URL))][0].kwargs["json"] assert len(request_data["id"]) == 32 assert request_data["jsonrpc"] == "2.0" assert request_data["method"] == "getNetwork" @@ -147,14 +148,14 @@ async def test_version_info(self): GetVersionInfoResponse.model_validate(result) with aioresponses() as m: - m.post(PRC_URL, payload=data) - async with SorobanServerAsync(PRC_URL) as client: + m.post(RPC_URL, payload=data) + async with SorobanServerAsync(RPC_URL) as client: assert ( await client.get_version_info() == GetVersionInfoResponse.model_validate(result) ) - request_data = m.requests[("POST", URL(PRC_URL))][0].kwargs["json"] + request_data = m.requests[("POST", URL(RPC_URL))][0].kwargs["json"] assert len(request_data["id"]) == 32 assert request_data["jsonrpc"] == "2.0" assert request_data["method"] == "getVersionInfo" @@ -181,13 +182,13 @@ async def test_get_contract_data(self): contract_id = "CBNYUGHFAIWK3HOINA2OIGOOBMQU4D3MPQWFYBTUYY5WY4FVDO2GWXUY" key = stellar_xdr.SCVal(stellar_xdr.SCValType.SCV_LEDGER_KEY_CONTRACT_INSTANCE) with aioresponses() as m: - m.post(PRC_URL, payload=data) - async with SorobanServerAsync(PRC_URL) as client: + m.post(RPC_URL, payload=data) + async with SorobanServerAsync(RPC_URL) as client: assert ( await client.get_contract_data(contract_id, key) ) == GetLedgerEntriesResponse.model_validate(result).entries[0] - request_data = m.requests[("POST", URL(PRC_URL))][0].kwargs["json"] + request_data = m.requests[("POST", URL(RPC_URL))][0].kwargs["json"] assert len(request_data["id"]) == 32 assert request_data["jsonrpc"] == "2.0" assert request_data["method"] == "getLedgerEntries" @@ -208,11 +209,11 @@ async def test_get_contract_data_not_found(self): contract_id = "CBNYUGHFAIWK3HOINA2OIGOOBMQU4D3MPQWFYBTUYY5WY4FVDO2GWXUY" key = stellar_xdr.SCVal(stellar_xdr.SCValType.SCV_LEDGER_KEY_CONTRACT_INSTANCE) with aioresponses() as m: - m.post(PRC_URL, payload=data) - async with SorobanServerAsync(PRC_URL) as client: + m.post(RPC_URL, payload=data) + async with SorobanServerAsync(RPC_URL) as client: assert (await client.get_contract_data(contract_id, key)) is None - request_data = m.requests[("POST", URL(PRC_URL))][0].kwargs["json"] + request_data = m.requests[("POST", URL(RPC_URL))][0].kwargs["json"] assert len(request_data["id"]) == 32 assert request_data["jsonrpc"] == "2.0" assert request_data["method"] == "getLedgerEntries" @@ -262,13 +263,13 @@ async def test_get_ledger_entries(self): ), ) with aioresponses() as m: - m.post(PRC_URL, payload=data) - async with SorobanServerAsync(PRC_URL) as client: + m.post(RPC_URL, payload=data) + async with SorobanServerAsync(RPC_URL) as client: assert ( await client.get_ledger_entries([key0, key1]) ) == GetLedgerEntriesResponse.model_validate(result) - request_data = m.requests[("POST", URL(PRC_URL))][0].kwargs["json"] + request_data = m.requests[("POST", URL(RPC_URL))][0].kwargs["json"] assert len(request_data["id"]) == 32 assert request_data["jsonrpc"] == "2.0" assert request_data["method"] == "getLedgerEntries" @@ -301,54 +302,55 @@ async def test_get_transaction(self): } tx_hash = "06dd9ee70bf93bbfe219e2b31363ab5a0361cc6285328592e4d3d1fed4c9025c" with aioresponses() as m: - m.post(PRC_URL, payload=data) - async with SorobanServerAsync(PRC_URL) as client: + m.post(RPC_URL, payload=data) + async with SorobanServerAsync(RPC_URL) as client: assert ( await client.get_transaction(tx_hash) ) == GetTransactionResponse.model_validate(result) - request_data = m.requests[("POST", URL(PRC_URL))][0].kwargs["json"] + request_data = m.requests[("POST", URL(RPC_URL))][0].kwargs["json"] assert len(request_data["id"]) == 32 assert request_data["jsonrpc"] == "2.0" assert request_data["method"] == "getTransaction" assert request_data["params"] == {"hash": tx_hash} async def test_get_events(self): + events = [ + { + "type": "contract", + "ledger": "12739", + "ledgerClosedAt": "2023-09-16T06:23:57Z", + "contractId": "CBNYUGHFAIWK3HOINA2OIGOOBMQU4D3MPQWFYBTUYY5WY4FVDO2GWXUY", + "id": "0000054713588387840-0000000000", + "pagingToken": "0000054713588387840-0000000000", + "topic": [ + "AAAADwAAAAdDT1VOVEVSAA==", + "AAAADwAAAAlpbmNyZW1lbnQAAAA=", + ], + "value": "AAAAAwAAAAE=", + "inSuccessfulContractCall": True, + "txHash": "db86e94aa98b7d38213c041ebbb727fbaabf0b7c435de594f36c2d51fc61926d", + }, + { + "type": "contract", + "ledger": "12747", + "ledgerClosedAt": "2023-09-16T06:24:05Z", + "contractId": "CBNYUGHFAIWK3HOINA2OIGOOBMQU4D3MPQWFYBTUYY5WY4FVDO2GWXUY", + "id": "0000054747948126208-0000000000", + "pagingToken": "0000054747948126208-0000000000", + "topic": [ + "AAAADwAAAAdDT1VOVEVSAA==", + "AAAADwAAAAlpbmNyZW1lbnQAAAA=", + ], + "value": "AAAAAwAAAAI=", + "inSuccessfulContractCall": True, + "txHash": "db86e94aa98b7d38213c041ebbb727fbaabf0b7c435de594f36c2d51fc61926d", + }, + ] result = { - "events": [ - { - "type": "contract", - "ledger": "12739", - "ledgerClosedAt": "2023-09-16T06:23:57Z", - "contractId": "CBNYUGHFAIWK3HOINA2OIGOOBMQU4D3MPQWFYBTUYY5WY4FVDO2GWXUY", - "id": "0000054713588387840-0000000000", - "pagingToken": "0000054713588387840-0000000000", - "topic": [ - "AAAADwAAAAdDT1VOVEVSAA==", - "AAAADwAAAAlpbmNyZW1lbnQAAAA=", - ], - "value": "AAAAAwAAAAE=", - "inSuccessfulContractCall": True, - "txHash": "db86e94aa98b7d38213c041ebbb727fbaabf0b7c435de594f36c2d51fc61926d", - }, - { - "type": "contract", - "ledger": "12747", - "ledgerClosedAt": "2023-09-16T06:24:05Z", - "contractId": "CBNYUGHFAIWK3HOINA2OIGOOBMQU4D3MPQWFYBTUYY5WY4FVDO2GWXUY", - "id": "0000054747948126208-0000000000", - "pagingToken": "0000054747948126208-0000000000", - "topic": [ - "AAAADwAAAAdDT1VOVEVSAA==", - "AAAADwAAAAlpbmNyZW1lbnQAAAA=", - ], - "value": "AAAAAwAAAAI=", - "inSuccessfulContractCall": True, - "txHash": "db86e94aa98b7d38213c041ebbb727fbaabf0b7c435de594f36c2d51fc61926d", - }, - ], + "events": events, "latestLedger": "187", - "cursor": "0000054713588387840-0000000000", + "cursor": "0000054747948126208-0000000000", } data = { "jsonrpc": "2.0", @@ -368,17 +370,17 @@ async def test_get_events(self): ], ) ] - GetEventsResponse.model_validate(result) + events_response = GetEventsResponse.model_validate(result) cursor = "0000054713588387839-0000000000" limit = 10 with aioresponses() as m: - m.post(PRC_URL, payload=data) - async with SorobanServerAsync(PRC_URL) as client: + m.post(RPC_URL, payload=data) + async with SorobanServerAsync(RPC_URL) as client: assert ( - await client.get_events(start_ledger, filters, cursor, limit) - ) == GetEventsResponse.model_validate(result) + await client.get_events(start_ledger, filters, limit=limit) + ) == events_response - request_data = m.requests[("POST", URL(PRC_URL))][0].kwargs["json"] + request_data = m.requests[("POST", URL(RPC_URL))][0].kwargs["json"] assert len(request_data["id"]) == 32 assert request_data["jsonrpc"] == "2.0" assert request_data["method"] == "getEvents" @@ -394,10 +396,50 @@ async def test_get_events(self): "type": "contract", } ], - "pagination": {"cursor": "0000054713588387839-0000000000", "limit": 10}, + "pagination": {"cursor": None, "limit": 10}, "startLedger": 100, } + # simulate the advance of one ledger + cursor = events_response.cursor + result = { + "events": [], + "latestLedger": "188", + "cursor": "0000054747948126210-0000000000", + } + data = { + "jsonrpc": "2.0", + "id": "198cb1a8-9104-4446-a269-88bf000c3986", + "result": result, + } + events_response = GetEventsResponse.model_validate(result) + with aioresponses() as m: + m.post(RPC_URL, payload=data) + async with SorobanServerAsync(RPC_URL) as client: + assert ( + await client.get_events(filters=filters, cursor=cursor, limit=limit) + ) == events_response + + request_data = m.requests[("POST", URL(RPC_URL))][0].kwargs["json"] + assert len(request_data["id"]) == 32 + assert request_data["jsonrpc"] == "2.0" + assert request_data["method"] == "getEvents" + assert request_data["params"] == { + "filters": [ + { + "contractIds": [ + "CBNYUGHFAIWK3HOINA2OIGOOBMQU4D3MPQWFYBTUYY5WY4FVDO2GWXUY" + ], + "topics": [ + ["AAAADwAAAAdDT1VOVEVSAA==", "AAAADwAAAAlpbmNyZW1lbnQAAAA="] + ], + "type": "contract", + } + ], + "pagination": {"cursor": "0000054747948126208-0000000000", "limit": 10}, + "startLedger": None, + } + async def test_get_latest_ledger(self): result = { "id": "e73d7654b72daa637f396669182c6072549736a9e3b6fcb8e685adb61f8c910a", @@ -410,13 +452,13 @@ async def test_get_latest_ledger(self): "result": result, } with aioresponses() as m: - m.post(PRC_URL, payload=data) - async with SorobanServerAsync(PRC_URL) as client: + m.post(RPC_URL, payload=data) + async with SorobanServerAsync(RPC_URL) as client: assert ( await client.get_latest_ledger() ) == GetLatestLedgerResponse.model_validate(result) - request_data = m.requests[("POST", URL(PRC_URL))][0].kwargs["json"] + request_data = m.requests[("POST", URL(RPC_URL))][0].kwargs["json"] assert len(request_data["id"]) == 32 assert request_data["jsonrpc"] == "2.0" assert request_data["method"] == "getLatestLedger" @@ -469,13 +511,13 @@ async def test_get_fee_stats(self): "result": result, } with aioresponses() as m: - m.post(PRC_URL, payload=data) - async with SorobanServerAsync(PRC_URL) as client: + m.post(RPC_URL, payload=data) + async with SorobanServerAsync(RPC_URL) as client: assert ( await client.get_fee_stats() ) == GetFeeStatsResponse.model_validate(result) - request_data = m.requests[("POST", URL(PRC_URL))][0].kwargs["json"] + request_data = m.requests[("POST", URL(RPC_URL))][0].kwargs["json"] assert len(request_data["id"]) == 32 assert request_data["jsonrpc"] == "2.0" assert request_data["method"] == "getFeeStats" @@ -578,13 +620,13 @@ async def test_get_transactions(self): GetTransactionsResponse.model_validate(result) limit = 5 with aioresponses() as m: - m.post(PRC_URL, payload=data) - async with SorobanServerAsync(PRC_URL) as client: + m.post(RPC_URL, payload=data) + async with SorobanServerAsync(RPC_URL) as client: assert ( await client.get_transactions(start_ledger, None, limit) ) == GetTransactionsResponse.model_validate(result) - request_data = m.requests[("POST", URL(PRC_URL))][0].kwargs["json"] + request_data = m.requests[("POST", URL(RPC_URL))][0].kwargs["json"] assert len(request_data["id"]) == 32 assert request_data["jsonrpc"] == "2.0" assert request_data["method"] == "getTransactions" @@ -628,13 +670,13 @@ async def test_get_ledgers(self): GetLedgersResponse.model_validate(result) limit = 2 with aioresponses() as m: - m.post(PRC_URL, payload=data) - async with SorobanServerAsync(PRC_URL) as client: + m.post(RPC_URL, payload=data) + async with SorobanServerAsync(RPC_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"] + request_data = m.requests[("POST", URL(RPC_URL))][0].kwargs["json"] assert len(request_data["id"]) == 32 assert request_data["jsonrpc"] == "2.0" assert request_data["method"] == "getLedgers" @@ -676,13 +718,13 @@ async def test_simulate_transaction(self): } transaction = _build_soroban_transaction(None, []) with aioresponses() as m: - m.post(PRC_URL, payload=data) - async with SorobanServerAsync(PRC_URL) as client: + m.post(RPC_URL, payload=data) + async with SorobanServerAsync(RPC_URL) as client: assert ( await client.simulate_transaction(transaction) ) == SimulateTransactionResponse.model_validate(result) - request_data = m.requests[("POST", URL(PRC_URL))][0].kwargs["json"] + request_data = m.requests[("POST", URL(RPC_URL))][0].kwargs["json"] assert len(request_data["id"]) == 32 assert request_data["jsonrpc"] == "2.0" assert request_data["method"] == "simulateTransaction" @@ -716,15 +758,15 @@ async def test_simulate_transaction_with_addl_resources(self): } transaction = _build_soroban_transaction(None, []) with aioresponses() as m: - m.post(PRC_URL, payload=data) - async with SorobanServerAsync(PRC_URL) as client: + m.post(RPC_URL, payload=data) + async with SorobanServerAsync(RPC_URL) as client: assert ( await client.simulate_transaction( transaction, ResourceLeeway(1000000) ) ) == SimulateTransactionResponse.model_validate(result) - request_data = m.requests[("POST", URL(PRC_URL))][0].kwargs["json"] + request_data = m.requests[("POST", URL(RPC_URL))][0].kwargs["json"] assert len(request_data["id"]) == 32 assert request_data["jsonrpc"] == "2.0" assert request_data["method"] == "simulateTransaction" @@ -758,8 +800,8 @@ async def test_prepare_transaction_without_auth_and_soroban_data(self): transaction = _build_soroban_transaction(None, []) with aioresponses() as m: - m.post(PRC_URL, payload=data) - async with SorobanServerAsync(PRC_URL) as client: + m.post(RPC_URL, payload=data) + async with SorobanServerAsync(RPC_URL) as client: new_transaction = await client.prepare_transaction(transaction) expected_transaction = copy.deepcopy(transaction) expected_transaction.transaction.fee += int(data["result"]["minResourceFee"]) @@ -813,8 +855,8 @@ async def test_prepare_transaction_with_soroban_data(self): ) # soroban_data will be overwritten by the response transaction = _build_soroban_transaction(soroban_data, []) with aioresponses() as m: - m.post(PRC_URL, payload=data) - async with SorobanServerAsync(PRC_URL) as client: + m.post(RPC_URL, payload=data) + async with SorobanServerAsync(RPC_URL) as client: new_transaction = await client.prepare_transaction(transaction) expected_transaction = copy.deepcopy(transaction) expected_transaction.transaction.fee = 50000 + int( @@ -876,8 +918,8 @@ async def test_prepare_transaction_with_auth(self): transaction = _build_soroban_transaction(None, [auth]) with aioresponses() as m: - m.post(PRC_URL, payload=data) - async with SorobanServerAsync(PRC_URL) as client: + m.post(RPC_URL, payload=data) + async with SorobanServerAsync(RPC_URL) as client: new_transaction = await client.prepare_transaction(transaction) expected_transaction = copy.deepcopy(transaction) expected_transaction.transaction.fee += int(data["result"]["minResourceFee"]) @@ -906,12 +948,12 @@ async def test_prepare_transaction_error_resp_prepare_transaction_exception_rais } transaction = _build_soroban_transaction(None, []) with aioresponses() as m: - m.post(PRC_URL, payload=data) + m.post(RPC_URL, payload=data) with pytest.raises( PrepareTransactionException, match="Simulation transaction failed, the response contains error information.", ) as e: - async with SorobanServerAsync(PRC_URL) as client: + async with SorobanServerAsync(RPC_URL) as client: await client.prepare_transaction(transaction) assert ( e.value.simulate_transaction_response @@ -938,12 +980,12 @@ async def test_prepare_transaction_invalid_results_value_raise( } transaction = _build_soroban_transaction(None, []) with aioresponses() as m: - m.post(PRC_URL, payload=data) + m.post(RPC_URL, payload=data) with pytest.raises( ValueError, match="Simulation results invalid", ) as e: - async with SorobanServerAsync(PRC_URL) as client: + async with SorobanServerAsync(RPC_URL) as client: await client.prepare_transaction(transaction) async def test_send_transaction(self): @@ -961,13 +1003,13 @@ async def test_send_transaction(self): transaction = _build_soroban_transaction(None, []) with aioresponses() as m: - m.post(PRC_URL, payload=data) - async with SorobanServerAsync(PRC_URL) as client: + m.post(RPC_URL, payload=data) + async with SorobanServerAsync(RPC_URL) as client: assert ( await client.send_transaction(transaction) ) == SendTransactionResponse.model_validate(result) - request_data = m.requests[("POST", URL(PRC_URL))][0].kwargs["json"] + request_data = m.requests[("POST", URL(RPC_URL))][0].kwargs["json"] assert len(request_data["id"]) == 32 assert request_data["jsonrpc"] == "2.0" assert request_data["method"] == "sendTransaction" @@ -993,13 +1035,13 @@ async def test_send_transaction_error(self): transaction = _build_soroban_transaction(None, []) with aioresponses() as m: - m.post(PRC_URL, payload=data) - async with SorobanServerAsync(PRC_URL) as client: + m.post(RPC_URL, payload=data) + async with SorobanServerAsync(RPC_URL) as client: assert ( await client.send_transaction(transaction) ) == SendTransactionResponse.model_validate(result) - request_data = m.requests[("POST", URL(PRC_URL))][0].kwargs["json"] + request_data = m.requests[("POST", URL(RPC_URL))][0].kwargs["json"] assert len(request_data["id"]) == 32 assert request_data["jsonrpc"] == "2.0" assert request_data["method"] == "sendTransaction" @@ -1017,14 +1059,25 @@ async def test_soroban_rpc_error_response_raise(self): }, } with aioresponses() as m: - m.post(PRC_URL, payload=data) + m.post(RPC_URL, payload=data) with pytest.raises(SorobanRpcErrorResponse) as e: - async with SorobanServerAsync(PRC_URL) as client: + async with SorobanServerAsync(RPC_URL) as client: await client.get_health() assert e.value.code == -32601 assert e.value.message == "method not found" assert e.value.data == "mockTest" + async def test_pagination_start_ledger_and_cursor_raise(self): + with pytest.raises(ValidationError) as e: + async with SorobanServerAsync(RPC_URL) as client: + await client.get_transactions( + start_ledger=67, cursor="8111217537191937", limit=1 + ) + assert e.value.error_count() == 1 + val_error = e.value.errors()[0] + assert val_error["type"] == "value_error" + assert val_error["msg"].endswith("start_ledger and cursor cannot both be set") + def _build_soroban_transaction( soroban_data: Optional[stellar_xdr.SorobanTransactionData], diff --git a/tests/test_soroban_server_sync.py b/tests/test_soroban_server_sync.py index fdc98db7..9c39404e 100644 --- a/tests/test_soroban_server_sync.py +++ b/tests/test_soroban_server_sync.py @@ -2,6 +2,7 @@ import pytest import requests_mock +from pydantic import ValidationError from stellar_sdk import Account, Keypair, Network, TransactionBuilder, scval from stellar_sdk import xdr as stellar_xdr @@ -15,7 +16,7 @@ from stellar_sdk.soroban_rpc import * from stellar_sdk.soroban_server import SorobanServer -PRC_URL = "https://example.com/soroban_rpc" +RPC_URL = "https://example.com/soroban_rpc" class TestSorobanServer: @@ -38,8 +39,8 @@ def test_load_account(self): } account_id = "GDAT5HWTGIU4TSSZ4752OUC4SABDLTLZFRPZUJ3D6LKBNEPA7V2CIG54" with requests_mock.Mocker() as m: - m.post(PRC_URL, json=data) - assert SorobanServer(PRC_URL).load_account(account_id) == Account( + m.post(RPC_URL, json=data) + assert SorobanServer(RPC_URL).load_account(account_id) == Account( account_id, 3418793967628 ) @@ -60,12 +61,12 @@ def test_load_account_not_found_raise(self): } account_id = "GDAT5HWTGIU4TSSZ4752OUC4SABDLTLZFRPZUJ3D6LKBNEPA7V2CIG54" with requests_mock.Mocker() as m: - m.post(PRC_URL, json=data) + m.post(RPC_URL, json=data) with pytest.raises( AccountNotFoundException, match=f"Account not found, account_id: {account_id}", ): - SorobanServer(PRC_URL).load_account(account_id) + SorobanServer(RPC_URL).load_account(account_id) request_data = m.last_request.json() assert len(request_data["id"]) == 32 @@ -88,9 +89,9 @@ def test_get_health(self): "result": result, } with requests_mock.Mocker() as m: - m.post(PRC_URL, json=data) + m.post(RPC_URL, json=data) assert SorobanServer( - PRC_URL + RPC_URL ).get_health() == GetHealthResponse.model_validate(result) request_data = m.last_request.json() @@ -114,9 +115,9 @@ def test_get_network(self): GetNetworkResponse.model_validate(result) with requests_mock.Mocker() as m: - m.post(PRC_URL, json=data) + m.post(RPC_URL, json=data) assert SorobanServer( - PRC_URL + RPC_URL ).get_network() == GetNetworkResponse.model_validate(result) request_data = m.last_request.json() @@ -142,9 +143,9 @@ def test_get_version_info(self): GetVersionInfoResponse.model_validate(result) with requests_mock.Mocker() as m: - m.post(PRC_URL, json=data) + m.post(RPC_URL, json=data) assert SorobanServer( - PRC_URL + RPC_URL ).get_version_info() == GetVersionInfoResponse.model_validate(result) request_data = m.last_request.json() @@ -174,9 +175,9 @@ def test_get_contract_data(self): contract_id = "CBNYUGHFAIWK3HOINA2OIGOOBMQU4D3MPQWFYBTUYY5WY4FVDO2GWXUY" key = stellar_xdr.SCVal(stellar_xdr.SCValType.SCV_LEDGER_KEY_CONTRACT_INSTANCE) with requests_mock.Mocker() as m: - m.post(PRC_URL, json=data) + m.post(RPC_URL, json=data) assert ( - SorobanServer(PRC_URL).get_contract_data(contract_id, key) + SorobanServer(RPC_URL).get_contract_data(contract_id, key) == GetLedgerEntriesResponse.model_validate(result).entries[0] ) @@ -201,8 +202,8 @@ def test_get_contract_data_not_found(self): contract_id = "CBNYUGHFAIWK3HOINA2OIGOOBMQU4D3MPQWFYBTUYY5WY4FVDO2GWXUY" key = stellar_xdr.SCVal(stellar_xdr.SCValType.SCV_LEDGER_KEY_CONTRACT_INSTANCE) with requests_mock.Mocker() as m: - m.post(PRC_URL, json=data) - assert SorobanServer(PRC_URL).get_contract_data(contract_id, key) is None + m.post(RPC_URL, json=data) + assert SorobanServer(RPC_URL).get_contract_data(contract_id, key) is None request_data = m.last_request.json() assert len(request_data["id"]) == 32 @@ -254,8 +255,8 @@ def test_get_ledger_entries(self): ), ) with requests_mock.Mocker() as m: - m.post(PRC_URL, json=data) - assert SorobanServer(PRC_URL).get_ledger_entries( + m.post(RPC_URL, json=data) + assert SorobanServer(RPC_URL).get_ledger_entries( [key0, key1] ) == GetLedgerEntriesResponse.model_validate(result) @@ -292,8 +293,8 @@ def test_get_transaction(self): } tx_hash = "06dd9ee70bf93bbfe219e2b31363ab5a0361cc6285328592e4d3d1fed4c9025c" with requests_mock.Mocker() as m: - m.post(PRC_URL, json=data) - assert SorobanServer(PRC_URL).get_transaction( + m.post(RPC_URL, json=data) + assert SorobanServer(RPC_URL).get_transaction( tx_hash ) == GetTransactionResponse.model_validate(result) @@ -304,41 +305,42 @@ def test_get_transaction(self): assert request_data["params"] == {"hash": tx_hash} def test_get_events(self): + events = [ + { + "type": "contract", + "ledger": "12739", + "ledgerClosedAt": "2023-09-16T06:23:57Z", + "contractId": "CBNYUGHFAIWK3HOINA2OIGOOBMQU4D3MPQWFYBTUYY5WY4FVDO2GWXUY", + "id": "0000054713588387840-0000000000", + "pagingToken": "0000054713588387840-0000000000", + "topic": [ + "AAAADwAAAAdDT1VOVEVSAA==", + "AAAADwAAAAlpbmNyZW1lbnQAAAA=", + ], + "value": "AAAAAwAAAAE=", + "inSuccessfulContractCall": True, + "txHash": "db86e94aa98b7d38213c041ebbb727fbaabf0b7c435de594f36c2d51fc61926d", + }, + { + "type": "contract", + "ledger": "12747", + "ledgerClosedAt": "2023-09-16T06:24:05Z", + "contractId": "CBNYUGHFAIWK3HOINA2OIGOOBMQU4D3MPQWFYBTUYY5WY4FVDO2GWXUY", + "id": "0000054747948126208-0000000000", + "pagingToken": "0000054747948126208-0000000000", + "topic": [ + "AAAADwAAAAdDT1VOVEVSAA==", + "AAAADwAAAAlpbmNyZW1lbnQAAAA=", + ], + "value": "AAAAAwAAAAI=", + "inSuccessfulContractCall": True, + "txHash": "db86e94aa98b7d38213c041ebbb727fbaabf0b7c435de594f36c2d51fc61926d", + }, + ] result = { - "events": [ - { - "type": "contract", - "ledger": "12739", - "ledgerClosedAt": "2023-09-16T06:23:57Z", - "contractId": "CBNYUGHFAIWK3HOINA2OIGOOBMQU4D3MPQWFYBTUYY5WY4FVDO2GWXUY", - "id": "0000054713588387840-0000000000", - "pagingToken": "0000054713588387840-0000000000", - "topic": [ - "AAAADwAAAAdDT1VOVEVSAA==", - "AAAADwAAAAlpbmNyZW1lbnQAAAA=", - ], - "value": "AAAAAwAAAAE=", - "inSuccessfulContractCall": True, - "txHash": "db86e94aa98b7d38213c041ebbb727fbaabf0b7c435de594f36c2d51fc61926d", - }, - { - "type": "contract", - "ledger": "12747", - "ledgerClosedAt": "2023-09-16T06:24:05Z", - "contractId": "CBNYUGHFAIWK3HOINA2OIGOOBMQU4D3MPQWFYBTUYY5WY4FVDO2GWXUY", - "id": "0000054747948126208-0000000000", - "pagingToken": "0000054747948126208-0000000000", - "topic": [ - "AAAADwAAAAdDT1VOVEVSAA==", - "AAAADwAAAAlpbmNyZW1lbnQAAAA=", - ], - "value": "AAAAAwAAAAI=", - "inSuccessfulContractCall": True, - "txHash": "db86e94aa98b7d38213c041ebbb727fbaabf0b7c435de594f36c2d51fc61926d", - }, - ], + "events": events, "latestLedger": "187", - "cursor": "0000054713588387840-0000000000", + "cursor": "0000054747948126208-0000000000", } data = { "jsonrpc": "2.0", @@ -358,14 +360,14 @@ def test_get_events(self): ], ) ] - GetEventsResponse.model_validate(result) - cursor = "0000054713588387839-0000000000" + events_response = GetEventsResponse.model_validate(result) limit = 10 with requests_mock.Mocker() as m: - m.post(PRC_URL, json=data) - assert SorobanServer(PRC_URL).get_events( - start_ledger, filters, cursor, limit - ) == GetEventsResponse.model_validate(result) + m.post(RPC_URL, json=data) + assert ( + SorobanServer(RPC_URL).get_events(start_ledger, filters, limit=limit) + == events_response + ) request_data = m.last_request.json() assert len(request_data["id"]) == 32 @@ -383,10 +385,52 @@ def test_get_events(self): "type": "contract", } ], - "pagination": {"cursor": "0000054713588387839-0000000000", "limit": 10}, + "pagination": {"cursor": None, "limit": 10}, "startLedger": 100, } + # simulate the advance of one ledger + cursor = events_response.cursor + result = { + "events": [], + "latestLedger": "188", + "cursor": "0000054747948126210-0000000000", + } + data = { + "jsonrpc": "2.0", + "id": "198cb1a8-9104-4446-a269-88bf000c3986", + "result": result, + } + events_response = GetEventsResponse.model_validate(result) + with requests_mock.Mocker() as m: + m.post(RPC_URL, json=data) + assert ( + SorobanServer(RPC_URL).get_events( + filters=filters, cursor=cursor, limit=limit + ) + == events_response + ) + + request_data = m.last_request.json() + assert len(request_data["id"]) == 32 + assert request_data["jsonrpc"] == "2.0" + assert request_data["method"] == "getEvents" + assert request_data["params"] == { + "filters": [ + { + "contractIds": [ + "CBNYUGHFAIWK3HOINA2OIGOOBMQU4D3MPQWFYBTUYY5WY4FVDO2GWXUY" + ], + "topics": [ + ["AAAADwAAAAdDT1VOVEVSAA==", "AAAADwAAAAlpbmNyZW1lbnQAAAA="] + ], + "type": "contract", + } + ], + "pagination": {"cursor": "0000054747948126208-0000000000", "limit": 10}, + "startLedger": None, + } + def test_get_latest_ledger(self): result = { "id": "e73d7654b72daa637f396669182c6072549736a9e3b6fcb8e685adb61f8c910a", @@ -399,9 +443,9 @@ def test_get_latest_ledger(self): "result": result, } with requests_mock.Mocker() as m: - m.post(PRC_URL, json=data) + m.post(RPC_URL, json=data) assert SorobanServer( - PRC_URL + RPC_URL ).get_latest_ledger() == GetLatestLedgerResponse.model_validate(result) request_data = m.last_request.json() @@ -457,9 +501,9 @@ def test_get_fee_stats(self): "result": result, } with requests_mock.Mocker() as m: - m.post(PRC_URL, json=data) + m.post(RPC_URL, json=data) assert SorobanServer( - PRC_URL + RPC_URL ).get_fee_stats() == GetFeeStatsResponse.model_validate(result) request_data = m.last_request.json() @@ -562,13 +606,14 @@ def test_get_transactions(self): } start_ledger = 1888539 - GetTransactionsResponse.model_validate(result) + txs_response = GetTransactionsResponse.model_validate(result) limit = 5 with requests_mock.Mocker() as m: - m.post(PRC_URL, json=data) - assert SorobanServer(PRC_URL).get_transactions( - start_ledger, None, limit - ) == GetTransactionsResponse.model_validate(result) + m.post(RPC_URL, json=data) + assert ( + SorobanServer(RPC_URL).get_transactions(start_ledger, None, limit) + == txs_response + ) request_data = m.last_request.json() assert len(request_data["id"]) == 32 @@ -579,6 +624,26 @@ def test_get_transactions(self): "pagination": {"cursor": None, "limit": 5}, } + def test_get_transactions_without_args(self): + data = { + "jsonrpc": "2.0", + "id": "198cb1a8-9104-4446-a269-88bf000c2721", + "result": { + "transactions": [], + "latestLedger": 1888542, + "latestLedgerCloseTimestamp": 1717166057, + "oldestLedger": 1871263, + "oldestLedgerCloseTimestamp": 1717075350, + "cursor": "8111217537191937", + }, + } + # test that all arguments are optional + with requests_mock.Mocker() as m: + m.post(RPC_URL, json=data) + assert isinstance( + SorobanServer(RPC_URL).get_transactions(), GetTransactionsResponse + ) + def test_get_ledgers(self): result = { "ledgers": [ @@ -611,13 +676,14 @@ def test_get_ledgers(self): } start_ledger = 10 - GetLedgersResponse.model_validate(result) + ledgers_response = 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) + m.post(RPC_URL, json=data) + assert ( + SorobanServer(RPC_URL).get_ledgers(start_ledger, None, limit) + == ledgers_response + ) request_data = m.last_request.json() assert len(request_data["id"]) == 32 @@ -628,6 +694,24 @@ def test_get_ledgers(self): "pagination": {"cursor": None, "limit": 2}, } + def test_get_ledgers_without_args(self): + data = { + "jsonrpc": "2.0", + "id": "198cb1a8-9104-4446-a269-88bf000c2721", + "result": { + "ledgers": [], + "latestLedger": 113, + "latestLedgerCloseTime": 1731554518, + "oldestLedger": 8, + "oldestLedgerCloseTime": 1731554412, + "cursor": "11", + }, + } + # test that all arguments are optional + with requests_mock.Mocker() as m: + m.post(RPC_URL, json=data) + assert isinstance(SorobanServer(RPC_URL).get_ledgers(), GetLedgersResponse) + def test_simulate_transaction(self): result = { "transactionData": "AAAAAAAAAAIAAAAGAAAAAcWLK/vE8FTnMk9r8gytPgJuQbutGm0gw9fUkY3tFlQRAAAAFAAAAAEAAAAAAAAAB300Hyg0HZG+Qie3zvsxLvugrNtFqd3AIntWy9bg2YvZAAAAAAAAAAEAAAAGAAAAAcWLK/vE8FTnMk9r8gytPgJuQbutGm0gw9fUkY3tFlQRAAAAEAAAAAEAAAACAAAADwAAAAdDb3VudGVyAAAAABIAAAAAAAAAAFi3xKLI8peqjz0kcSgf38zsr+SOVmMxPsGOEqc+ypihAAAAAQAAAAAAFcLDAAAF8AAAAQgAAAMcAAAAAAAAAJw=", @@ -661,8 +745,8 @@ def test_simulate_transaction(self): } transaction = _build_soroban_transaction(None, []) with requests_mock.Mocker() as m: - m.post(PRC_URL, json=data) - assert SorobanServer(PRC_URL).simulate_transaction( + m.post(RPC_URL, json=data) + assert SorobanServer(RPC_URL).simulate_transaction( transaction ) == SimulateTransactionResponse.model_validate(result) @@ -700,8 +784,8 @@ def test_simulate_transaction_with_addl_resources(self): } transaction = _build_soroban_transaction(None, []) with requests_mock.Mocker() as m: - m.post(PRC_URL, json=data) - assert SorobanServer(PRC_URL).simulate_transaction( + m.post(RPC_URL, json=data) + assert SorobanServer(RPC_URL).simulate_transaction( transaction, ResourceLeeway(1000000) ) == SimulateTransactionResponse.model_validate(result) @@ -739,8 +823,8 @@ def test_prepare_transaction_without_auth_and_soroban_data(self): transaction = _build_soroban_transaction(None, []) with requests_mock.Mocker() as m: - m.post(PRC_URL, json=data) - new_transaction = SorobanServer(PRC_URL).prepare_transaction(transaction) + m.post(RPC_URL, json=data) + new_transaction = SorobanServer(RPC_URL).prepare_transaction(transaction) expected_transaction = copy.deepcopy(transaction) expected_transaction.transaction.fee += int(data["result"]["minResourceFee"]) expected_transaction.transaction.soroban_data = ( @@ -793,8 +877,8 @@ def test_prepare_transaction_with_soroban_data(self): ) # soroban_data will be overwritten by the response transaction = _build_soroban_transaction(soroban_data, []) with requests_mock.Mocker() as m: - m.post(PRC_URL, json=data) - new_transaction = SorobanServer(PRC_URL).prepare_transaction(transaction) + m.post(RPC_URL, json=data) + new_transaction = SorobanServer(RPC_URL).prepare_transaction(transaction) expected_transaction = copy.deepcopy(transaction) expected_transaction.transaction.fee = 50000 + int( data["result"]["minResourceFee"] @@ -855,8 +939,8 @@ def test_prepare_transaction_with_auth(self): transaction = _build_soroban_transaction(None, [auth]) with requests_mock.Mocker() as m: - m.post(PRC_URL, json=data) - new_transaction = SorobanServer(PRC_URL).prepare_transaction(transaction) + m.post(RPC_URL, json=data) + new_transaction = SorobanServer(RPC_URL).prepare_transaction(transaction) expected_transaction = copy.deepcopy(transaction) expected_transaction.transaction.fee += int(data["result"]["minResourceFee"]) expected_transaction.transaction.soroban_data = ( @@ -882,12 +966,12 @@ def test_prepare_transaction_error_resp_prepare_transaction_exception_raise(self } transaction = _build_soroban_transaction(None, []) with requests_mock.Mocker() as m: - m.post(PRC_URL, json=data) + m.post(RPC_URL, json=data) with pytest.raises( PrepareTransactionException, match="Simulation transaction failed, the response contains error information.", ) as e: - SorobanServer(PRC_URL).prepare_transaction(transaction) + SorobanServer(RPC_URL).prepare_transaction(transaction) assert ( e.value.simulate_transaction_response == SimulateTransactionResponse.model_validate(data["result"]) @@ -913,12 +997,12 @@ def test_prepare_transaction_invalid_results_value_raise( } transaction = _build_soroban_transaction(None, []) with requests_mock.Mocker() as m: - m.post(PRC_URL, json=data) + m.post(RPC_URL, json=data) with pytest.raises( ValueError, match="Simulation results invalid", ) as e: - SorobanServer(PRC_URL).prepare_transaction(transaction) + SorobanServer(RPC_URL).prepare_transaction(transaction) def test_send_transaction(self): result = { @@ -935,8 +1019,8 @@ def test_send_transaction(self): transaction = _build_soroban_transaction(None, []) with requests_mock.Mocker() as m: - m.post(PRC_URL, json=data) - assert SorobanServer(PRC_URL).send_transaction( + m.post(RPC_URL, json=data) + assert SorobanServer(RPC_URL).send_transaction( transaction ) == SendTransactionResponse.model_validate(result) @@ -966,8 +1050,8 @@ def test_send_transaction_error(self): transaction = _build_soroban_transaction(None, []) with requests_mock.Mocker() as m: - m.post(PRC_URL, json=data) - assert SorobanServer(PRC_URL).send_transaction( + m.post(RPC_URL, json=data) + assert SorobanServer(RPC_URL).send_transaction( transaction ) == SendTransactionResponse.model_validate(result) @@ -989,13 +1073,23 @@ def test_soroban_rpc_error_response_raise(self): }, } with requests_mock.Mocker() as m: - m.post(PRC_URL, json=data) + m.post(RPC_URL, json=data) with pytest.raises(SorobanRpcErrorResponse) as e: - SorobanServer(PRC_URL).get_health() + SorobanServer(RPC_URL).get_health() assert e.value.code == -32601 assert e.value.message == "method not found" assert e.value.data == "mockTest" + def test_pagination_start_ledger_and_cursor_raise(self): + with pytest.raises(ValidationError) as e: + SorobanServer(RPC_URL).get_transactions( + start_ledger=67, cursor="8111217537191937", limit=1 + ) + assert e.value.error_count() == 1 + val_error = e.value.errors()[0] + assert val_error["type"] == "value_error" + assert val_error["msg"].endswith("start_ledger and cursor cannot both be set") + def _build_soroban_transaction( soroban_data: Optional[stellar_xdr.SorobanTransactionData],