From a44fb38481f9928c122a7125a40bebc62f2a189e Mon Sep 17 00:00:00 2001 From: Jun Luo <4catcode@gmail.com> Date: Sat, 16 Dec 2023 11:40:17 +0800 Subject: [PATCH] feat: support resource leeway parameter when simulating Soroban transactions. (#846) --- stellar_sdk/base_soroban_server.py | 8 +++++ stellar_sdk/soroban_rpc.py | 11 +++++++ stellar_sdk/soroban_server.py | 16 ++++++++-- stellar_sdk/soroban_server_async.py | 15 +++++++-- tests/test_soroban_server_async.py | 49 ++++++++++++++++++++++++++++- tests/test_soroban_server_sync.py | 46 ++++++++++++++++++++++++++- 6 files changed, 139 insertions(+), 6 deletions(-) diff --git a/stellar_sdk/base_soroban_server.py b/stellar_sdk/base_soroban_server.py index 4595cef3..452ea0ee 100644 --- a/stellar_sdk/base_soroban_server.py +++ b/stellar_sdk/base_soroban_server.py @@ -1,6 +1,7 @@ from __future__ import annotations import copy +import dataclasses import uuid from typing import TYPE_CHECKING @@ -12,6 +13,13 @@ from .transaction_envelope import TransactionEnvelope +@dataclasses.dataclass +class ResourceLeeway: + """Describes additional resource leeways for transaction simulation.""" + + cpu_instructions: int + + class Durability(Enum): TEMPORARY = "temporary" PERSISTENT = "persistent" diff --git a/stellar_sdk/soroban_rpc.py b/stellar_sdk/soroban_rpc.py index 1d911980..e2aef79d 100644 --- a/stellar_sdk/soroban_rpc.py +++ b/stellar_sdk/soroban_rpc.py @@ -145,6 +145,13 @@ class GetHealthResponse(BaseModel): # simulate_transaction +class ResourceConfig(BaseModel): + """ResourceConfig represents the additional resource leeways for transaction simulation.""" + + instruction_lee_way: int = Field(alias="instructionLeeway") + model_config = ConfigDict(populate_by_name=True) + + class SimulateTransactionRequest(BaseModel): """Response for JSON-RPC method simulateTransaction. @@ -159,6 +166,10 @@ class SimulateTransactionRequest(BaseModel): """ transaction: str + resource_config: Optional[ResourceConfig] = Field( + alias="resourceConfig", default=None + ) + model_config = ConfigDict(populate_by_name=True) class SimulateTransactionCost(BaseModel): diff --git a/stellar_sdk/soroban_server.py b/stellar_sdk/soroban_server.py index 2cb8a3d8..cbe432f4 100644 --- a/stellar_sdk/soroban_server.py +++ b/stellar_sdk/soroban_server.py @@ -9,6 +9,7 @@ from .address import Address from .base_soroban_server import ( Durability, + ResourceLeeway, _assemble_transaction, _generate_unique_request_id, ) @@ -157,7 +158,9 @@ def get_transaction(self, transaction_hash: str) -> GetTransactionResponse: return self._post(request, GetTransactionResponse) def simulate_transaction( - self, transaction_envelope: TransactionEnvelope + self, + transaction_envelope: TransactionEnvelope, + addl_resources: Optional[ResourceLeeway] = None, ) -> SimulateTransactionResponse: """Submit a trial contract invocation to get back return values, expected ledger footprint, and expected costs. @@ -168,6 +171,7 @@ def simulate_transaction( :class:`InvokeHostFunction ` or :class:`ExtendFootprintTTL ` operation. Any provided footprint will be ignored. + :param addl_resources: Additional resource include in the simulation. :return: A :class:`SimulateTransactionResponse ` object contains the cost, footprint, result/auth requirements (if applicable), and error of the transaction. """ @@ -176,10 +180,18 @@ def simulate_transaction( if isinstance(transaction_envelope, str) else transaction_envelope.to_xdr() ) + resource_config = None + if addl_resources: + resource_config = ResourceConfig( + instruction_lee_way=addl_resources.cpu_instructions, + ) + request = Request[SimulateTransactionRequest]( id=_generate_unique_request_id(), method="simulateTransaction", - params=SimulateTransactionRequest(transaction=xdr), + params=SimulateTransactionRequest( + transaction=xdr, resource_config=resource_config + ), ) return self._post(request, SimulateTransactionResponse) diff --git a/stellar_sdk/soroban_server_async.py b/stellar_sdk/soroban_server_async.py index eae9ed81..d4be1413 100644 --- a/stellar_sdk/soroban_server_async.py +++ b/stellar_sdk/soroban_server_async.py @@ -9,6 +9,7 @@ from .address import Address from .base_soroban_server import ( Durability, + ResourceLeeway, _assemble_transaction, _generate_unique_request_id, ) @@ -157,7 +158,9 @@ async def get_transaction(self, transaction_hash: str) -> GetTransactionResponse return await self._post(request, GetTransactionResponse) async def simulate_transaction( - self, transaction_envelope: TransactionEnvelope + self, + transaction_envelope: TransactionEnvelope, + addl_resources: Optional[ResourceLeeway] = None, ) -> SimulateTransactionResponse: """Submit a trial contract invocation to get back return values, expected ledger footprint, and expected costs. @@ -168,6 +171,7 @@ async def simulate_transaction( :class:`InvokeHostFunction ` or :class:`ExtendFootprintTTL ` operation. Any provided footprint will be ignored. + :param addl_resources: Additional resource include in the simulation. :return: A :class:`SimulateTransactionResponse ` object contains the cost, footprint, result/auth requirements (if applicable), and error of the transaction. """ @@ -176,10 +180,17 @@ async def simulate_transaction( if isinstance(transaction_envelope, str) else transaction_envelope.to_xdr() ) + resource_config = None + if addl_resources: + resource_config = ResourceConfig( + instruction_lee_way=addl_resources.cpu_instructions, + ) request = Request[SimulateTransactionRequest]( id=_generate_unique_request_id(), method="simulateTransaction", - params=SimulateTransactionRequest(transaction=xdr), + params=SimulateTransactionRequest( + transaction=xdr, resource_config=resource_config + ), ) return await self._post(request, SimulateTransactionResponse) diff --git a/tests/test_soroban_server_async.py b/tests/test_soroban_server_async.py index bc506645..6e42fd79 100644 --- a/tests/test_soroban_server_async.py +++ b/tests/test_soroban_server_async.py @@ -7,6 +7,7 @@ from stellar_sdk import Account, Keypair, Network, TransactionBuilder, scval from stellar_sdk import xdr as stellar_xdr from stellar_sdk.address import Address +from stellar_sdk.base_soroban_server import ResourceLeeway from stellar_sdk.exceptions import ( AccountNotFoundException, PrepareTransactionException, @@ -418,7 +419,53 @@ async def test_simulate_transaction(self): assert len(request_data["id"]) == 32 assert request_data["jsonrpc"] == "2.0" assert request_data["method"] == "simulateTransaction" - assert request_data["params"] == {"transaction": transaction.to_xdr()} + assert request_data["params"] == { + "transaction": transaction.to_xdr(), + "resourceConfig": None, + } + + async def test_simulate_transaction_with_addl_resources(self): + result = { + "transactionData": "AAAAAAAAAAIAAAAGAAAAAcWLK/vE8FTnMk9r8gytPgJuQbutGm0gw9fUkY3tFlQRAAAAFAAAAAEAAAAAAAAAB300Hyg0HZG+Qie3zvsxLvugrNtFqd3AIntWy9bg2YvZAAAAAAAAAAEAAAAGAAAAAcWLK/vE8FTnMk9r8gytPgJuQbutGm0gw9fUkY3tFlQRAAAAEAAAAAEAAAACAAAADwAAAAdDb3VudGVyAAAAABIAAAAAAAAAAFi3xKLI8peqjz0kcSgf38zsr+SOVmMxPsGOEqc+ypihAAAAAQAAAAAAFcLDAAAF8AAAAQgAAAMcAAAAAAAAAJw=", + "events": [ + "AAAAAQAAAAAAAAAAAAAAAgAAAAAAAAADAAAADwAAAAdmbl9jYWxsAAAAAA0AAAAgxYsr+8TwVOcyT2vyDK0+Am5Bu60abSDD19SRje0WVBEAAAAPAAAACWluY3JlbWVudAAAAAAAABAAAAABAAAAAgAAABIAAAAAAAAAAFi3xKLI8peqjz0kcSgf38zsr+SOVmMxPsGOEqc+ypihAAAAAwAAAAo=", + "AAAAAQAAAAAAAAABxYsr+8TwVOcyT2vyDK0+Am5Bu60abSDD19SRje0WVBEAAAACAAAAAAAAAAIAAAAPAAAACWZuX3JldHVybgAAAAAAAA8AAAAJaW5jcmVtZW50AAAAAAAAAwAAABQ=", + ], + "minResourceFee": "58595", + "results": [ + { + "auth": [ + "AAAAAAAAAAAAAAABxYsr+8TwVOcyT2vyDK0+Am5Bu60abSDD19SRje0WVBEAAAAJaW5jcmVtZW50AAAAAAAAAgAAABIAAAAAAAAAAFi3xKLI8peqjz0kcSgf38zsr+SOVmMxPsGOEqc+ypihAAAAAwAAAAoAAAAA" + ], + "xdr": "AAAAAwAAABQ=", + } + ], + "cost": {"cpuInsns": "1240100", "memBytes": "161637"}, + "latestLedger": "1479", + } + data = { + "jsonrpc": "2.0", + "id": "e1fabdcdf0244a2a9adfab94d7748b6c", + "result": result, + } + transaction = _build_soroban_transaction(None, []) + with aioresponses() as m: + m.post(PRC_URL, payload=data) + async with SorobanServerAsync(PRC_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"] + assert len(request_data["id"]) == 32 + assert request_data["jsonrpc"] == "2.0" + assert request_data["method"] == "simulateTransaction" + assert request_data["params"] == { + "transaction": transaction.to_xdr(), + "resourceConfig": {"instructionLeeway": 1000000}, + } async def test_prepare_transaction_without_auth_and_soroban_data(self): data = { diff --git a/tests/test_soroban_server_sync.py b/tests/test_soroban_server_sync.py index 59d9fb0c..55f1928d 100644 --- a/tests/test_soroban_server_sync.py +++ b/tests/test_soroban_server_sync.py @@ -6,6 +6,7 @@ from stellar_sdk import Account, Keypair, Network, TransactionBuilder, scval from stellar_sdk import xdr as stellar_xdr from stellar_sdk.address import Address +from stellar_sdk.base_soroban_server import ResourceLeeway from stellar_sdk.exceptions import ( AccountNotFoundException, PrepareTransactionException, @@ -407,7 +408,50 @@ def test_simulate_transaction(self): assert len(request_data["id"]) == 32 assert request_data["jsonrpc"] == "2.0" assert request_data["method"] == "simulateTransaction" - assert request_data["params"] == {"transaction": transaction.to_xdr()} + assert request_data["params"] == { + "transaction": transaction.to_xdr(), + "resourceConfig": None, + } + + def test_simulate_transaction_with_addl_resources(self): + result = { + "transactionData": "AAAAAAAAAAIAAAAGAAAAAcWLK/vE8FTnMk9r8gytPgJuQbutGm0gw9fUkY3tFlQRAAAAFAAAAAEAAAAAAAAAB300Hyg0HZG+Qie3zvsxLvugrNtFqd3AIntWy9bg2YvZAAAAAAAAAAEAAAAGAAAAAcWLK/vE8FTnMk9r8gytPgJuQbutGm0gw9fUkY3tFlQRAAAAEAAAAAEAAAACAAAADwAAAAdDb3VudGVyAAAAABIAAAAAAAAAAFi3xKLI8peqjz0kcSgf38zsr+SOVmMxPsGOEqc+ypihAAAAAQAAAAAAFcLDAAAF8AAAAQgAAAMcAAAAAAAAAJw=", + "events": [ + "AAAAAQAAAAAAAAAAAAAAAgAAAAAAAAADAAAADwAAAAdmbl9jYWxsAAAAAA0AAAAgxYsr+8TwVOcyT2vyDK0+Am5Bu60abSDD19SRje0WVBEAAAAPAAAACWluY3JlbWVudAAAAAAAABAAAAABAAAAAgAAABIAAAAAAAAAAFi3xKLI8peqjz0kcSgf38zsr+SOVmMxPsGOEqc+ypihAAAAAwAAAAo=", + "AAAAAQAAAAAAAAABxYsr+8TwVOcyT2vyDK0+Am5Bu60abSDD19SRje0WVBEAAAACAAAAAAAAAAIAAAAPAAAACWZuX3JldHVybgAAAAAAAA8AAAAJaW5jcmVtZW50AAAAAAAAAwAAABQ=", + ], + "minResourceFee": "58595", + "results": [ + { + "auth": [ + "AAAAAAAAAAAAAAABxYsr+8TwVOcyT2vyDK0+Am5Bu60abSDD19SRje0WVBEAAAAJaW5jcmVtZW50AAAAAAAAAgAAABIAAAAAAAAAAFi3xKLI8peqjz0kcSgf38zsr+SOVmMxPsGOEqc+ypihAAAAAwAAAAoAAAAA" + ], + "xdr": "AAAAAwAAABQ=", + } + ], + "cost": {"cpuInsns": "1240100", "memBytes": "161637"}, + "latestLedger": "1479", + } + data = { + "jsonrpc": "2.0", + "id": "e1fabdcdf0244a2a9adfab94d7748b6c", + "result": result, + } + transaction = _build_soroban_transaction(None, []) + with requests_mock.Mocker() as m: + m.post(PRC_URL, json=data) + assert SorobanServer(PRC_URL).simulate_transaction( + transaction, ResourceLeeway(1000000) + ) == SimulateTransactionResponse.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"] == "simulateTransaction" + assert request_data["params"] == { + "transaction": transaction.to_xdr(), + "resourceConfig": {"instructionLeeway": 1000000}, + } def test_prepare_transaction_without_auth_and_soroban_data(self): data = {