From 5ab1abe1240ecd8965c7b82d72f0f492d9b6c581 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Andrei=20B=C4=83ncioiu?= Date: Mon, 10 Jun 2024 16:39:37 +0300 Subject: [PATCH] Integration into queries controller and contract transactions factory. --- .../core/smart_contract_queries_controller.py | 41 +++++- .../smart_contract_queries_controller_test.py | 82 ++++++++++-- multiversx_sdk/core/smart_contract_query.py | 12 ++ multiversx_sdk/core/transaction.py | 6 + ...smart_contract_transaction_factory_test.py | 123 +++++++++++++++--- .../smart_contract_transactions_factory.py | 61 ++++++++- 6 files changed, 285 insertions(+), 40 deletions(-) diff --git a/multiversx_sdk/core/smart_contract_queries_controller.py b/multiversx_sdk/core/smart_contract_queries_controller.py index 10690fe9..f1dd3459 100644 --- a/multiversx_sdk/core/smart_contract_queries_controller.py +++ b/multiversx_sdk/core/smart_contract_queries_controller.py @@ -1,5 +1,9 @@ from typing import Any, List, Optional, Protocol +from multiversx_sdk.abi import Serializer +from multiversx_sdk.abi.typesystem import (is_list_of_bytes, + is_list_of_typed_values) +from multiversx_sdk.core.constants import ARGS_SEPARATOR from multiversx_sdk.core.errors import SmartContractQueryError from multiversx_sdk.core.smart_contract_query import ( SmartContractQuery, SmartContractQueryResponse) @@ -10,9 +14,19 @@ def run_query(self, query: SmartContractQuery) -> SmartContractQueryResponse: ... +class IAbi(Protocol): + def encode_endpoint_input_parameters(self, endpoint_name: str, values: List[Any]) -> List[bytes]: + ... + + def decode_endpoint_output_parameters(self, endpoint_name: str, encoded_values: List[bytes]) -> List[Any]: + ... + + class SmartContractQueriesController: - def __init__(self, query_runner: IQueryRunner) -> None: + def __init__(self, query_runner: IQueryRunner, abi: Optional[IAbi] = None) -> None: self.query_runner = query_runner + self.abi = abi + self.serializer = Serializer(parts_separator=ARGS_SEPARATOR) def query( self, @@ -43,21 +57,40 @@ def create_query( self, contract: str, function: str, - arguments: List[bytes], + arguments: List[Any], caller: Optional[str] = None, value: Optional[int] = None ) -> SmartContractQuery: + prepared_arguments = self._encode_arguments(function, arguments) + return SmartContractQuery( contract=contract, function=function, - arguments=arguments, + arguments=prepared_arguments, caller=caller, value=value ) + def _encode_arguments(self, function_name: str, args: List[Any]) -> List[bytes]: + if self.abi: + return self.abi.encode_endpoint_input_parameters(function_name, args) + + if is_list_of_typed_values(args): + return self.serializer.serialize_to_parts(args) + + if is_list_of_bytes(args): + return args + + raise ValueError("cannot encode arguments: when ABI is not available, they must be either typed values or buffers") + def run_query(self, query: SmartContractQuery) -> SmartContractQueryResponse: query_response = self.query_runner.run_query(query) return query_response def parse_query_response(self, response: SmartContractQueryResponse) -> List[Any]: - return response.return_data_parts + encoded_values = response.return_data_parts + + if self.abi: + return self.abi.decode_endpoint_output_parameters(response.function, encoded_values) + + return encoded_values diff --git a/multiversx_sdk/core/smart_contract_queries_controller_test.py b/multiversx_sdk/core/smart_contract_queries_controller_test.py index e19a3bfb..cb15f344 100644 --- a/multiversx_sdk/core/smart_contract_queries_controller_test.py +++ b/multiversx_sdk/core/smart_contract_queries_controller_test.py @@ -1,7 +1,10 @@ import base64 +from pathlib import Path import pytest +from multiversx_sdk.abi.abi import Abi +from multiversx_sdk.abi.string_value import StringValue from multiversx_sdk.adapters.query_runner_adapter import QueryRunnerAdapter from multiversx_sdk.core.codec import encode_unsigned_number from multiversx_sdk.core.smart_contract_queries_controller import \ @@ -16,19 +19,20 @@ class TestSmartContractQueriesController: - provider = ProxyNetworkProvider("https://devnet-api.multiversx.com") - query_runner = QueryRunnerAdapter(network_provider=provider) - controller = SmartContractQueriesController(query_runner=query_runner) + testdata = Path(__file__).parent.parent / "testutils" / "testdata" def test_create_query_without_arguments(self): + query_runner = QueryRunnerAdapter(MockNetworkProvider()) + controller = SmartContractQueriesController(query_runner) contract = "erd1qqqqqqqqqqqqqpgqsnwuj85zv7t0wnxfetyqqyjvvg444lpk7uasxv8ktx" function = "getSum" - query = self.controller.create_query( + query = controller.create_query( contract=contract, function=function, arguments=[] ) + assert query.contract == contract assert query.function == function assert query.arguments == [] @@ -36,23 +40,53 @@ def test_create_query_without_arguments(self): assert query.value is None def test_create_query_with_arguments(self): + query_runner = QueryRunnerAdapter(MockNetworkProvider()) + controller = SmartContractQueriesController(query_runner) contract = "erd1qqqqqqqqqqqqqpgqsnwuj85zv7t0wnxfetyqqyjvvg444lpk7uasxv8ktx" function = "getSum" - query = self.controller.create_query( + query = controller.create_query( contract=contract, function=function, arguments=[encode_unsigned_number(7), "abba".encode()] ) + assert query.contract == contract assert query.function == function assert query.arguments == [encode_unsigned_number(7), "abba".encode()] assert query.caller is None assert query.value is None + def test_create_query_with_arguments_with_abi(self): + query_runner = QueryRunnerAdapter(MockNetworkProvider()) + abi = Abi.load(self.testdata / "lottery-esdt.abi.json") + controller = SmartContractQueriesController(query_runner, abi) + contract = "erd1qqqqqqqqqqqqqpgqsnwuj85zv7t0wnxfetyqqyjvvg444lpk7uasxv8ktx" + function = "getLotteryInfo" + + query = controller.create_query( + contract=contract, + function=function, + arguments=["myLottery"] + ) + + query_with_typed = controller.create_query( + contract=contract, + function=function, + arguments=[StringValue("myLottery")] + ) + + assert query.contract == contract + assert query.function == function + assert query.arguments == [b"myLottery"] + assert query.caller is None + assert query.value is None + + assert query_with_typed == query + def test_run_query_with_mock_provider(self): network_provider = MockNetworkProvider() - query_runner = QueryRunnerAdapter(network_provider=network_provider) + query_runner = QueryRunnerAdapter(network_provider) controller = SmartContractQueriesController(query_runner) contract_query_response = ContractQueryResponse() @@ -72,8 +106,7 @@ def test_run_query_with_mock_provider(self): assert response.return_data_parts == ["abba".encode()] def test_parse_query_response(self): - network_provider = MockNetworkProvider() - query_runner = QueryRunnerAdapter(network_provider=network_provider) + query_runner = QueryRunnerAdapter(MockNetworkProvider()) controller = SmartContractQueriesController(query_runner) response = SmartContractQueryResponse( @@ -86,28 +119,55 @@ def test_parse_query_response(self): parsed = controller.parse_query_response(response) assert parsed == ["abba".encode()] + def test_parse_query_response_with_abi(self): + query_runner = QueryRunnerAdapter(MockNetworkProvider()) + abi = Abi.load(self.testdata / "lottery-esdt.abi.json") + controller = SmartContractQueriesController(query_runner, abi) + + response = SmartContractQueryResponse( + function="getLotteryInfo", + return_code="ok", + return_message="ok", + return_data_parts=[bytes.fromhex("0000000b6c75636b792d746f6b656e000000010100000000000000005fc2b9dbffffffff00000001640000000a140ec80fa7ee88000000")] + ) + + [lottery_info] = controller.parse_query_response(response) + assert lottery_info.token_identifier == "lucky-token" + assert lottery_info.ticket_price == 1 + assert lottery_info.tickets_left == 0 + assert lottery_info.deadline == 0x000000005fc2b9db + assert lottery_info.max_entries_per_user == 0xffffffff + assert lottery_info.prize_distribution == bytes([0x64]) + assert lottery_info.prize_pool == 94720000000000000000000 + @pytest.mark.networkInteraction def test_run_query_on_network(self): + provider = ProxyNetworkProvider("https://devnet-api.multiversx.com") + query_runner = QueryRunnerAdapter(provider) + controller = SmartContractQueriesController(query_runner) contract = "erd1qqqqqqqqqqqqqpgqsnwuj85zv7t0wnxfetyqqyjvvg444lpk7uasxv8ktx" function = "getSum" - query = self.controller.create_query( + query = controller.create_query( contract=contract, function=function, arguments=[] ) - query_response = self.controller.run_query(query) + query_response = controller.run_query(query) assert query_response.return_code == "ok" assert query_response.return_message == "" assert query_response.return_data_parts == [b'\x05'] @pytest.mark.networkInteraction def test_query_on_network(self): + provider = ProxyNetworkProvider("https://devnet-api.multiversx.com") + query_runner = QueryRunnerAdapter(provider) + controller = SmartContractQueriesController(query_runner) contract = "erd1qqqqqqqqqqqqqpgqsnwuj85zv7t0wnxfetyqqyjvvg444lpk7uasxv8ktx" function = "getSum" - return_data_parts = self.controller.query( + return_data_parts = controller.query( contract=contract, function=function, arguments=[] diff --git a/multiversx_sdk/core/smart_contract_query.py b/multiversx_sdk/core/smart_contract_query.py index 2bfd5fc8..66bbde60 100644 --- a/multiversx_sdk/core/smart_contract_query.py +++ b/multiversx_sdk/core/smart_contract_query.py @@ -16,6 +16,12 @@ def __init__( self.caller = caller self.value = value + def __eq__(self, other: object) -> bool: + if not isinstance(other, SmartContractQuery): + return False + + return self.__dict__ == other.__dict__ + class SmartContractQueryResponse: def __init__( @@ -29,3 +35,9 @@ def __init__( self.return_code = return_code self.return_message = return_message self.return_data_parts = return_data_parts + + def __eq__(self, other: object) -> bool: + if not isinstance(other, SmartContractQueryResponse): + return False + + return self.__dict__ == other.__dict__ diff --git a/multiversx_sdk/core/transaction.py b/multiversx_sdk/core/transaction.py index e0fbddfc..0f87e289 100644 --- a/multiversx_sdk/core/transaction.py +++ b/multiversx_sdk/core/transaction.py @@ -41,3 +41,9 @@ def __init__(self, self.guardian = guardian or "" self.guardian_signature = guardian_signature or bytes() + + def __eq__(self, other: object) -> bool: + if not isinstance(other, Transaction): + return False + + return self.__dict__ == other.__dict__ diff --git a/multiversx_sdk/core/transactions_factories/smart_contract_transaction_factory_test.py b/multiversx_sdk/core/transactions_factories/smart_contract_transaction_factory_test.py index 005e7cff..62ca2b65 100644 --- a/multiversx_sdk/core/transactions_factories/smart_contract_transaction_factory_test.py +++ b/multiversx_sdk/core/transactions_factories/smart_contract_transaction_factory_test.py @@ -1,5 +1,8 @@ from pathlib import Path +from multiversx_sdk.abi.abi import Abi +from multiversx_sdk.abi.biguint_value import BigUIntValue +from multiversx_sdk.abi.small_int_values import U32Value from multiversx_sdk.core.address import Address from multiversx_sdk.core.constants import CONTRACT_DEPLOY_ADDRESS from multiversx_sdk.core.tokens import Token, TokenTransfer @@ -10,50 +13,106 @@ class TestSmartContractTransactionsFactory: + testdata = Path(__file__).parent.parent.parent / "testutils" / "testdata" + bytecode = (testdata / "adder.wasm").read_bytes() + abi = Abi.load(testdata / "adder.abi.json") + config = TransactionsFactoryConfig("D") factory = SmartContractTransactionsFactory(config) - testdata = Path(__file__).parent.parent.parent / "testutils" / "testdata" + abi_aware_factory = SmartContractTransactionsFactory(config, abi) def test_create_transaction_for_deploy(self): sender = Address.new_from_bech32("erd1qyu5wthldzr8wx5c9ucg8kjagg0jfs53s8nr3zpz3hypefsdd8ssycr6th") - contract = self.testdata / "adder.wasm" gas_limit = 6000000 - args = [0] + # Works due to legacy encoding fallbacks. transaction = self.factory.create_transaction_for_deploy( sender=sender, - bytecode=contract, + bytecode=self.bytecode, + gas_limit=gas_limit, + arguments=[1] + ) + + transaction_with_typed = self.factory.create_transaction_for_deploy( + sender=sender, + bytecode=self.bytecode, + gas_limit=gas_limit, + arguments=[BigUIntValue(1)] + ) + + transaction_abi_aware_with_untyped = self.abi_aware_factory.create_transaction_for_deploy( + sender=sender, + bytecode=self.bytecode, + gas_limit=gas_limit, + arguments=[1] + ) + + transaction_abi_aware_with_typed = self.abi_aware_factory.create_transaction_for_deploy( + sender=sender, + bytecode=self.bytecode, gas_limit=gas_limit, - arguments=args + arguments=[BigUIntValue(1)] ) assert transaction.sender == "erd1qyu5wthldzr8wx5c9ucg8kjagg0jfs53s8nr3zpz3hypefsdd8ssycr6th" assert transaction.receiver == CONTRACT_DEPLOY_ADDRESS - assert transaction.data + assert transaction.data == f"{self.bytecode.hex()}@0500@0504@01".encode() assert transaction.gas_limit == gas_limit assert transaction.value == 0 + assert transaction_with_typed == transaction + assert transaction_abi_aware_with_untyped == transaction + assert transaction_abi_aware_with_typed == transaction + def test_create_transaction_for_execute_no_transfer(self): sender = Address.new_from_bech32("erd1qyu5wthldzr8wx5c9ucg8kjagg0jfs53s8nr3zpz3hypefsdd8ssycr6th") contract = Address.new_from_bech32("erd1qqqqqqqqqqqqqpgqhy6nl6zq07rnzry8uyh6rtyq0uzgtk3e69fqgtz9l4") function = "add" gas_limit = 6000000 - args = [7] - intent = self.factory.create_transaction_for_execute( + # Works due to legacy encoding fallbacks. + transaction = self.factory.create_transaction_for_execute( + sender=sender, + contract=contract, + function=function, + gas_limit=gas_limit, + arguments=[7] + ) + + transaction_with_typed = self.factory.create_transaction_for_execute( + sender=sender, + contract=contract, + function=function, + gas_limit=gas_limit, + arguments=[U32Value(7)] + ) + + transaction_abi_aware_with_untyped = self.abi_aware_factory.create_transaction_for_execute( sender=sender, contract=contract, function=function, gas_limit=gas_limit, - arguments=args + arguments=[7] ) - assert intent.sender == "erd1qyu5wthldzr8wx5c9ucg8kjagg0jfs53s8nr3zpz3hypefsdd8ssycr6th" - assert intent.receiver == "erd1qqqqqqqqqqqqqpgqhy6nl6zq07rnzry8uyh6rtyq0uzgtk3e69fqgtz9l4" - assert intent.gas_limit == gas_limit - assert intent.data - assert intent.data.decode() == "add@07" - assert intent.value == 0 + transaction_abi_aware_with_typed = self.abi_aware_factory.create_transaction_for_execute( + sender=sender, + contract=contract, + function=function, + gas_limit=gas_limit, + arguments=[U32Value(7)] + ) + + assert transaction.sender == "erd1qyu5wthldzr8wx5c9ucg8kjagg0jfs53s8nr3zpz3hypefsdd8ssycr6th" + assert transaction.receiver == "erd1qqqqqqqqqqqqqpgqhy6nl6zq07rnzry8uyh6rtyq0uzgtk3e69fqgtz9l4" + assert transaction.gas_limit == gas_limit + assert transaction.data + assert transaction.data.decode() == "add@07" + assert transaction.value == 0 + + assert transaction_with_typed == transaction + assert transaction_abi_aware_with_untyped == transaction + assert transaction_abi_aware_with_typed == transaction def test_create_transaction_for_execute_and_tranfer_native_token(self): sender = Address.new_from_bech32("erd1qyu5wthldzr8wx5c9ucg8kjagg0jfs53s8nr3zpz3hypefsdd8ssycr6th") @@ -191,23 +250,51 @@ def test_create_transaction_for_upgrade(self): contract_address = Address.new_from_bech32("erd1qqqqqqqqqqqqqpgqhy6nl6zq07rnzry8uyh6rtyq0uzgtk3e69fqgtz9l4") contract = self.testdata / "adder.wasm" gas_limit = 6000000 - args = [0] + # Works due to legacy encoding fallbacks. transaction = self.factory.create_transaction_for_upgrade( sender=sender, contract=contract_address, bytecode=contract, gas_limit=gas_limit, - arguments=args + arguments=[7] + ) + + transaction_with_typed = self.factory.create_transaction_for_upgrade( + sender=sender, + contract=contract_address, + bytecode=contract, + gas_limit=gas_limit, + arguments=[BigUIntValue(7)] + ) + + transaction_abi_aware_with_untyped = self.abi_aware_factory.create_transaction_for_upgrade( + sender=sender, + contract=contract_address, + bytecode=contract, + gas_limit=gas_limit, + arguments=[7] + ) + + transaction_abi_aware_with_typed = self.abi_aware_factory.create_transaction_for_upgrade( + sender=sender, + contract=contract_address, + bytecode=contract, + gas_limit=gas_limit, + arguments=[BigUIntValue(7)] ) assert transaction.sender == "erd1qyu5wthldzr8wx5c9ucg8kjagg0jfs53s8nr3zpz3hypefsdd8ssycr6th" assert transaction.receiver == "erd1qqqqqqqqqqqqqpgqhy6nl6zq07rnzry8uyh6rtyq0uzgtk3e69fqgtz9l4" - assert transaction.data + assert transaction.data == f"upgradeContract@{self.bytecode.hex()}@0504@07".encode() assert transaction.data.decode().startswith("upgradeContract@") assert transaction.gas_limit == gas_limit assert transaction.value == 0 + assert transaction_with_typed == transaction + assert transaction_abi_aware_with_untyped == transaction + assert transaction_abi_aware_with_typed == transaction + def test_create_transaction_for_claiming_developer_rewards(self): sender = Address.new_from_bech32("erd1qyu5wthldzr8wx5c9ucg8kjagg0jfs53s8nr3zpz3hypefsdd8ssycr6th") contract_address = Address.new_from_bech32("erd1qqqqqqqqqqqqqpgqhy6nl6zq07rnzry8uyh6rtyq0uzgtk3e69fqgtz9l4") diff --git a/multiversx_sdk/core/transactions_factories/smart_contract_transactions_factory.py b/multiversx_sdk/core/transactions_factories/smart_contract_transactions_factory.py index fae3fce0..8ed1db6b 100644 --- a/multiversx_sdk/core/transactions_factories/smart_contract_transactions_factory.py +++ b/multiversx_sdk/core/transactions_factories/smart_contract_transactions_factory.py @@ -1,13 +1,16 @@ from pathlib import Path -from typing import Any, List, Protocol, Sequence, Union +from typing import Any, List, Optional, Protocol, Sequence, Union +from multiversx_sdk.abi.serializer import Serializer +from multiversx_sdk.abi.typesystem import is_list_of_typed_values from multiversx_sdk.core.address import Address from multiversx_sdk.core.code_metadata import CodeMetadata -from multiversx_sdk.core.constants import (CONTRACT_DEPLOY_ADDRESS, +from multiversx_sdk.core.constants import (ARGS_SEPARATOR, + CONTRACT_DEPLOY_ADDRESS, VM_TYPE_WASM_VM) from multiversx_sdk.core.errors import BadUsageError from multiversx_sdk.core.interfaces import IAddress, ITokenTransfer -from multiversx_sdk.core.serializer import arg_to_string, args_to_strings +from multiversx_sdk.core.serializer import arg_to_string, args_to_buffers from multiversx_sdk.core.tokens import TokenComputer from multiversx_sdk.core.transaction import Transaction from multiversx_sdk.core.transactions_factories.token_transfers_data_builder import \ @@ -24,9 +27,22 @@ class IConfig(Protocol): gas_limit_change_owner_address: int +class IAbi(Protocol): + def encode_endpoint_input_parameters(self, endpoint_name: str, values: List[Any]) -> List[bytes]: + ... + + def encode_constructor_input_parameters(self, values: List[Any]) -> List[bytes]: + ... + + def encode_upgrade_constructor_input_parameters(self, values: List[Any]) -> List[bytes]: + ... + + class SmartContractTransactionsFactory: - def __init__(self, config: IConfig) -> None: + def __init__(self, config: IConfig, abi: Optional[IAbi] = None) -> None: self.config = config + self.abi = abi + self.serializer = Serializer(parts_separator=ARGS_SEPARATOR) self.token_computer = TokenComputer() self._data_args_builder = TokenTransfersDataBuilder(self.token_computer) @@ -51,7 +67,8 @@ def create_transaction_for_deploy(self, str(metadata) ] - parts += args_to_strings(arguments) + prepared_arguments = self._encode_deploy_arguments(list(arguments)) + parts += [arg.hex() for arg in prepared_arguments] return TransactionBuilder( config=self.config, @@ -93,8 +110,10 @@ def create_transaction_for_execute(self, receiver=receiver, transfers=token_transfers) receiver = sender + prepared_arguments = self._encode_execute_arguments(function, list(arguments)) + data_parts.append(function) if not data_parts else data_parts.append(arg_to_string(function)) - data_parts.extend(args_to_strings(arguments)) + data_parts.extend([arg.hex() for arg in prepared_arguments]) return TransactionBuilder( config=self.config, @@ -128,7 +147,8 @@ def create_transaction_for_upgrade(self, str(metadata) ] - parts += args_to_strings(arguments) + prepared_arguments = self._encode_upgrade_arguments(list(arguments)) + parts += [arg.hex() for arg in prepared_arguments] return TransactionBuilder( config=self.config, @@ -168,3 +188,30 @@ def create_transaction_for_changing_owner_address(self, gas_limit=self.config.gas_limit_change_owner_address, add_data_movement_gas=False, ).build() + + def _encode_deploy_arguments(self, args: List[Any]) -> List[bytes]: + if self.abi: + return self.abi.encode_constructor_input_parameters(args) + + if is_list_of_typed_values(args): + return self.serializer.serialize_to_parts(args) + + return args_to_buffers(args) + + def _encode_execute_arguments(self, function_name: str, args: List[Any]) -> List[bytes]: + if self.abi: + return self.abi.encode_endpoint_input_parameters(function_name, args) + + if is_list_of_typed_values(args): + return self.serializer.serialize_to_parts(args) + + return args_to_buffers(args) + + def _encode_upgrade_arguments(self, args: List[Any]) -> List[bytes]: + if self.abi: + return self.abi.encode_upgrade_constructor_input_parameters(args) + + if is_list_of_typed_values(args): + return self.serializer.serialize_to_parts(args) + + return args_to_buffers(args)