From 27483448eb8f80dd9c2ac58a310e771d89221146 Mon Sep 17 00:00:00 2001 From: Alexandru Popenta Date: Thu, 20 Jun 2024 11:11:17 +0300 Subject: [PATCH 1/8] started implementation --- multiversx_sdk/abi/abi.py | 56 +- multiversx_sdk/abi/abi_definition.py | 11 +- multiversx_sdk/abi/abi_test.py | 10 + .../transaction_events_parser.py | 48 ++ .../transaction_events_parser_test.py | 26 + .../testutils/testdata/artificial.abi.json | 14 +- .../testutils/testdata/esdt-safe.abi.json | 725 ++++++++++++++++++ 7 files changed, 885 insertions(+), 5 deletions(-) create mode 100644 multiversx_sdk/core/transactions_outcome_parsers/transaction_events_parser.py create mode 100644 multiversx_sdk/core/transactions_outcome_parsers/transaction_events_parser_test.py create mode 100644 multiversx_sdk/testutils/testdata/esdt-safe.abi.json diff --git a/multiversx_sdk/abi/abi.py b/multiversx_sdk/abi/abi.py index 23c795e7..72f6fda7 100644 --- a/multiversx_sdk/abi/abi.py +++ b/multiversx_sdk/abi/abi.py @@ -4,7 +4,8 @@ from multiversx_sdk.abi.abi_definition import (AbiDefinition, EndpointDefinition, - EnumDefinition, + EnumDefinition, EventDefinition, + EventTopicDefinition, ParameterDefinition, StructDefinition) from multiversx_sdk.abi.address_value import AddressValue @@ -38,6 +39,7 @@ def __init__(self, definition: AbiDefinition) -> None: self.definition = definition self.custom_types_prototypes_by_name: Dict[str, Any] = {} self.endpoints_prototypes_by_name: Dict[str, EndpointPrototype] = {} + self.events_prototypes_by_name: Dict[str, EndpointPrototype] = {} for name in definition.types.enums: self.custom_types_prototypes_by_name[name] = self._create_custom_type_prototype(name) @@ -66,6 +68,17 @@ def __init__(self, definition: AbiDefinition) -> None: self.endpoints_prototypes_by_name[endpoint.name] = endpoint_prototype + for event in definition.events: + input_prototype = self._create_event_input_prototypes(event) + + # events do not have 'outputs' + event_prototype = EndpointPrototype( + input_parameters=input_prototype, + output_parameters=[] + ) + + self.events_prototypes_by_name[event.identifier] = event_prototype + def _create_custom_type_prototype(self, name: str) -> Any: if name in self.definition.types.enums: definition = self.definition.types.enums[name] @@ -125,10 +138,23 @@ def _create_endpoint_output_prototypes(self, endpoint: EndpointDefinition) -> Li return prototypes + def _create_event_input_prototypes(self, event: EventDefinition) -> List[Any]: + prototypes: List[Any] = [] + + for topic in event.inputs: + parameter_prototype = self._create_topic_prototype(topic) + prototypes.append(parameter_prototype) + + return prototypes + def _create_parameter_prototype(self, parameter: ParameterDefinition) -> Any: type_formula = self._type_formula_parser.parse_expression(parameter.type) return self._create_prototype(type_formula) + def _create_topic_prototype(self, parameter: EventTopicDefinition) -> Any: + type_formula = self._type_formula_parser.parse_expression(parameter.type) + return self._create_prototype(type_formula) + def encode_constructor_input_parameters(self, values: List[Any]) -> List[bytes]: return self._do_encode_endpoint_input_parameters("constructor", self.constructor_prototype, values) @@ -162,6 +188,26 @@ def decode_endpoint_output_parameters(self, endpoint_name: str, encoded_values: output_native_values = [value.get_payload() for value in output_values_as_native_object_holders] return output_native_values + def decode_event(self, event_name: str, encoded_values: List[bytes]) -> List[Any]: + event_prototype = self._get_event_prototype(event_name) + output_values = deepcopy(event_prototype.input_parameters) + self._serializer.deserialize_parts(encoded_values, output_values) + + output_values_as_native_object_holders = cast(List[IPayloadHolder], output_values) + output_native_values = [value.get_payload() for value in output_values_as_native_object_holders] + return output_native_values + + def get_event(self, name: str) -> EventDefinition: + event = [event for event in self.definition.events if event.identifier == name] + + if not len(event): + raise Exception(f"event [{name}] not found") + + if len(event) > 1: + raise Exception(f"more than one event found: [{event}]") + + return event[0] + def _get_custom_type_prototype(self, type_name: str) -> Any: type_prototype = self.custom_types_prototypes_by_name.get(type_name) @@ -178,6 +224,14 @@ def _get_endpoint_prototype(self, endpoint_name: str) -> 'EndpointPrototype': return endpoint_prototype + def _get_event_prototype(self, event_name: str) -> 'EndpointPrototype': + event_prototype = self.endpoints_prototypes_by_name.get(event_name) + + if not event_prototype: + raise ValueError(f"endpoint '{event_name}' not found") + + return event_prototype + def _create_prototype(self, type_formula: TypeFormula) -> Any: name = type_formula.name diff --git a/multiversx_sdk/abi/abi_definition.py b/multiversx_sdk/abi/abi_definition.py index a18c2a31..d8cee37a 100644 --- a/multiversx_sdk/abi/abi_definition.py +++ b/multiversx_sdk/abi/abi_definition.py @@ -8,11 +8,13 @@ def __init__(self, constructor: "EndpointDefinition", upgrade_constructor: "EndpointDefinition", endpoints: List["EndpointDefinition"], - types: "TypesDefinitions") -> None: + types: "TypesDefinitions", + events: List["EventDefinition"]) -> None: self.constructor = constructor self.upgrade_constructor = upgrade_constructor self.endpoints = endpoints self.types = types + self.events = events @classmethod def from_dict(cls, data: Dict[str, Any]) -> "AbiDefinition": @@ -25,11 +27,14 @@ def from_dict(cls, data: Dict[str, Any]) -> "AbiDefinition": endpoints = [EndpointDefinition.from_dict(item) for item in data["endpoints"]] if "endpoints" in data else [] types = TypesDefinitions.from_dict(data.get("types", {})) + events = [EventDefinition.from_dict(item) for item in data["events"]] if "events" in data else [] + return cls( constructor=constructor, upgrade_constructor=upgrade_constructor, endpoints=endpoints, - types=types + types=types, + events=events ) @classmethod @@ -282,7 +287,7 @@ def from_dict(cls, data: Dict[str, Any]) -> "EventTopicDefinition": return cls( name=data["name"], type=data["type"], - indexed=data["indexed"] + indexed=data.get("indexed", False) ) def __repr__(self): diff --git a/multiversx_sdk/abi/abi_test.py b/multiversx_sdk/abi/abi_test.py index 415168fe..2c9aeddd 100644 --- a/multiversx_sdk/abi/abi_test.py +++ b/multiversx_sdk/abi/abi_test.py @@ -31,6 +31,8 @@ def test_abi(): assert abi.definition.upgrade_constructor.inputs == [ParameterDefinition("initial_value", "BigUint")] assert abi.definition.upgrade_constructor.outputs == [] + assert abi.definition.events == [] + assert abi.definition.endpoints[0].name == "getSum" assert abi.definition.endpoints[0].inputs == [] assert abi.definition.endpoints[0].outputs == [ParameterDefinition("", "BigUint")] @@ -54,6 +56,14 @@ def test_abi(): assert abi.endpoints_prototypes_by_name["add"].output_parameters == [] +def test_abi_events(): + abi = Abi.load(testdata / "artificial.abi.json") + + assert len(abi.definition.events) == 1 + assert abi.events_prototypes_by_name["firstEvent"].input_parameters == [BigUIntValue()] + assert abi.events_prototypes_by_name["firstEvent"].output_parameters == [] + + def test_encode_endpoint_input_parameters_artificial_contract(): abi = Abi.load(testdata / "artificial.abi.json") diff --git a/multiversx_sdk/core/transactions_outcome_parsers/transaction_events_parser.py b/multiversx_sdk/core/transactions_outcome_parsers/transaction_events_parser.py new file mode 100644 index 00000000..761d7293 --- /dev/null +++ b/multiversx_sdk/core/transactions_outcome_parsers/transaction_events_parser.py @@ -0,0 +1,48 @@ +from typing import Any, List, Protocol + +from multiversx_sdk.abi.abi_definition import EventDefinition +from multiversx_sdk.core.transactions_outcome_parsers.resources import \ + TransactionEvent + + +class IAbi(Protocol): + def get_event(self, name: str) -> EventDefinition: + ... + + def decode_event(self, event_name: str, encoded_values: List[bytes]) -> List[Any]: + ... + + +class TransactionEventsParser: + def __init__(self, abi: IAbi, first_topic_as_identifier: bool = True) -> None: + self.abi = abi + + # By default, we consider that the first topic is the event identifier. + # This is true for log entries emitted by smart contracts: + # https://github.com/multiversx/mx-chain-vm-go/blob/v1.5.27/vmhost/contexts/output.go#L270 + # https://github.com/multiversx/mx-chain-vm-go/blob/v1.5.27/vmhost/contexts/output.go#L283 + self.first_topic_as_identifier = first_topic_as_identifier + + def parse_events(self, events: List[TransactionEvent]) -> List[Any]: + return [self.parse_event(event) for event in events] + + def parse_event(self, event: TransactionEvent) -> Any: + first_topic = event.topics[0].decode() if len(event.topics) else "" + abi_identifier = first_topic if self.first_topic_as_identifier else event.identifier + + if self.first_topic_as_identifier: + event.topics = event.topics[1:] + + event_definition = self.abi.get_event(abi_identifier) + return self.do_parse_event( + topics=event.topics, + data_items=event.data_items, + event_definition=event_definition + ) + + def do_parse_event(self, topics: List[bytes], data_items: List[bytes], event_definition: EventDefinition) -> Any: + result: Any = {} + + # "Indexed" ABI "event.inputs" correspond to "event.topics[1:]": + indexed_inputs = [input for input in event_definition.inputs if input.indexed] + decoded_topics = [] diff --git a/multiversx_sdk/core/transactions_outcome_parsers/transaction_events_parser_test.py b/multiversx_sdk/core/transactions_outcome_parsers/transaction_events_parser_test.py new file mode 100644 index 00000000..1aeb47c3 --- /dev/null +++ b/multiversx_sdk/core/transactions_outcome_parsers/transaction_events_parser_test.py @@ -0,0 +1,26 @@ +from pathlib import Path + +from multiversx_sdk.abi.abi import Abi +from multiversx_sdk.core.transactions_outcome_parsers.resources import \ + TransactionEvent +from multiversx_sdk.core.transactions_outcome_parsers.transaction_events_parser import \ + TransactionEventsParser + +testdata = Path(__file__).parent.parent.parent / "testutils" / "testdata" + + +def test_parse_events_minimalistic(): + abi = Abi.load(testdata / "esdt-safe.abi.json") + parser = TransactionEventsParser(abi=abi) + + values = parser.parse_events( + events=[ + TransactionEvent( + identifier="transferOverMaxAmount", + topics=["transferOverMaxAmount".encode(), bytes([0x2a]), bytes([0x2b])] + ) + ] + ) + + assert len(values) == 1 + assert values[0] == {"batch_id": 42, "tx_id": 43} diff --git a/multiversx_sdk/testutils/testdata/artificial.abi.json b/multiversx_sdk/testutils/testdata/artificial.abi.json index fae5d546..d8b0dbfe 100644 --- a/multiversx_sdk/testutils/testdata/artificial.abi.json +++ b/multiversx_sdk/testutils/testdata/artificial.abi.json @@ -41,5 +41,17 @@ "outputs": [] } ], - "types": {} + "types": {}, + "events": [ + { + "identifier": "firstEvent", + "inputs": [ + { + "name": "result", + "type": "BigUint", + "indexed": true + } + ] + } + ] } diff --git a/multiversx_sdk/testutils/testdata/esdt-safe.abi.json b/multiversx_sdk/testutils/testdata/esdt-safe.abi.json new file mode 100644 index 00000000..ee17c3f3 --- /dev/null +++ b/multiversx_sdk/testutils/testdata/esdt-safe.abi.json @@ -0,0 +1,725 @@ +{ + "buildInfo": { + "rustc": { + "version": "1.75.0-nightly", + "commitHash": "42b1224e9eb37177f608d3f6a6f2be2ee13902e4", + "commitDate": "2023-10-15", + "channel": "Nightly", + "short": "rustc 1.75.0-nightly (42b1224e9 2023-10-15)" + }, + "contractCrate": { + "name": "esdt-safe", + "version": "0.0.0" + }, + "framework": { + "name": "multiversx-sc", + "version": "0.43.5" + } + }, + "name": "EsdtSafe", + "constructor": { + "inputs": [ + { + "name": "min_valid_signers", + "type": "u32" + }, + { + "name": "signers", + "type": "variadic
", + "multi_arg": true + } + ], + "outputs": [] + }, + "endpoints": [ + { + "name": "upgrade", + "mutability": "mutable", + "inputs": [], + "outputs": [] + }, + { + "docs": [ + "Create an Elrond -> Sovereign transaction." + ], + "name": "deposit", + "mutability": "mutable", + "payableInTokens": [ + "*" + ], + "inputs": [ + { + "name": "to", + "type": "Address" + }, + { + "name": "opt_transfer_data", + "type": "optional", + "multi_arg": true + } + ], + "outputs": [] + }, + { + "docs": [ + "Claim funds for failed Elrond -> Sovereign transactions.", + "These are not sent automatically to prevent the contract getting stuck.", + "For example, if the receiver is a SC, a frozen account, etc." + ], + "name": "claimRefund", + "mutability": "mutable", + "inputs": [ + { + "name": "token_id", + "type": "TokenIdentifier" + } + ], + "outputs": [ + { + "type": "List" + } + ] + }, + { + "docs": [ + "Sets the statuses for the transactions, after they were executed on the Sovereign side.", + "", + "Only TransactionStatus::Executed (3) and TransactionStatus::Rejected (4) values are allowed.", + "Number of provided statuses must be equal to number of transactions in the batch." + ], + "name": "setTransactionBatchStatus", + "mutability": "mutable", + "inputs": [ + { + "name": "batch_id", + "type": "u64" + }, + { + "name": "signature", + "type": "array48" + }, + { + "name": "tx_statuses", + "type": "variadic", + "multi_arg": true + } + ], + "outputs": [] + }, + { + "name": "setMinValidSigners", + "onlyOwner": true, + "mutability": "mutable", + "inputs": [ + { + "name": "new_value", + "type": "u32" + } + ], + "outputs": [] + }, + { + "name": "addSigners", + "onlyOwner": true, + "mutability": "mutable", + "inputs": [ + { + "name": "signers", + "type": "variadic
", + "multi_arg": true + } + ], + "outputs": [] + }, + { + "name": "removeSigners", + "onlyOwner": true, + "mutability": "mutable", + "inputs": [ + { + "name": "signers", + "type": "variadic
", + "multi_arg": true + } + ], + "outputs": [] + }, + { + "name": "registerToken", + "mutability": "mutable", + "payableInTokens": [ + "EGLD" + ], + "inputs": [ + { + "name": "sov_token_id", + "type": "TokenIdentifier" + }, + { + "name": "token_type", + "type": "EsdtTokenType" + }, + { + "name": "token_display_name", + "type": "bytes" + }, + { + "name": "token_ticker", + "type": "bytes" + }, + { + "name": "num_decimals", + "type": "u32" + }, + { + "name": "bls_multisig", + "type": "array48" + } + ], + "outputs": [] + }, + { + "name": "clearRegisteredToken", + "onlyOwner": true, + "mutability": "mutable", + "inputs": [ + { + "name": "sov_token_id", + "type": "TokenIdentifier" + } + ], + "outputs": [] + }, + { + "name": "batchTransferEsdtToken", + "mutability": "mutable", + "inputs": [ + { + "name": "batch_id", + "type": "u64" + }, + { + "name": "signature", + "type": "array48" + }, + { + "name": "transfers", + "type": "variadic", + "multi_arg": true + } + ], + "outputs": [] + }, + { + "name": "setMaxTxBatchSize", + "onlyOwner": true, + "mutability": "mutable", + "inputs": [ + { + "name": "new_max_tx_batch_size", + "type": "u32" + } + ], + "outputs": [] + }, + { + "name": "setMaxTxBatchBlockDuration", + "onlyOwner": true, + "mutability": "mutable", + "inputs": [ + { + "name": "new_max_tx_batch_block_duration", + "type": "u64" + } + ], + "outputs": [] + }, + { + "name": "getCurrentTxBatch", + "mutability": "readonly", + "inputs": [], + "outputs": [ + { + "type": "optional,List,Option>>>>", + "multi_result": true + } + ] + }, + { + "name": "getFirstBatchAnyStatus", + "mutability": "readonly", + "inputs": [], + "outputs": [ + { + "type": "optional,List,Option>>>>", + "multi_result": true + } + ] + }, + { + "name": "getBatch", + "mutability": "readonly", + "inputs": [ + { + "name": "batch_id", + "type": "u64" + } + ], + "outputs": [ + { + "type": "optional,List,Option>>>>", + "multi_result": true + } + ] + }, + { + "name": "getBatchStatus", + "mutability": "readonly", + "inputs": [ + { + "name": "batch_id", + "type": "u64" + } + ], + "outputs": [ + { + "type": "BatchStatus" + } + ] + }, + { + "name": "getFirstBatchId", + "mutability": "readonly", + "inputs": [], + "outputs": [ + { + "type": "u64" + } + ] + }, + { + "name": "getLastBatchId", + "mutability": "readonly", + "inputs": [], + "outputs": [ + { + "type": "u64" + } + ] + }, + { + "name": "setMaxBridgedAmount", + "onlyOwner": true, + "mutability": "mutable", + "inputs": [ + { + "name": "token_id", + "type": "TokenIdentifier" + }, + { + "name": "max_amount", + "type": "BigUint" + } + ], + "outputs": [] + }, + { + "name": "getMaxBridgedAmount", + "mutability": "readonly", + "inputs": [ + { + "name": "token_id", + "type": "TokenIdentifier" + } + ], + "outputs": [ + { + "type": "BigUint" + } + ] + }, + { + "name": "pause", + "onlyOwner": true, + "mutability": "mutable", + "inputs": [], + "outputs": [] + }, + { + "name": "unpause", + "onlyOwner": true, + "mutability": "mutable", + "inputs": [], + "outputs": [] + }, + { + "name": "isPaused", + "mutability": "readonly", + "inputs": [], + "outputs": [ + { + "type": "bool" + } + ] + } + ], + "promisesCallbackNames": [ + "transfer_callback" + ], + "events": [ + { + "identifier": "deposit", + "inputs": [ + { + "name": "dest_address", + "type": "Address", + "indexed": true + }, + { + "name": "tokens", + "type": "List", + "indexed": true + }, + { + "name": "event_data", + "type": "DepositEvent" + } + ] + }, + { + "identifier": "setStatusEvent", + "inputs": [ + { + "name": "batch_id", + "type": "u64", + "indexed": true + }, + { + "name": "tx_id", + "type": "u64", + "indexed": true + }, + { + "name": "tx_status", + "type": "TransactionStatus", + "indexed": true + } + ] + }, + { + "identifier": "addRefundTransactionEvent", + "inputs": [ + { + "name": "tx_id", + "type": "u64", + "indexed": true + }, + { + "name": "original_tx_id", + "type": "u64", + "indexed": true + } + ] + }, + { + "identifier": "transferPerformedEvent", + "inputs": [ + { + "name": "batch_id", + "type": "u64", + "indexed": true + }, + { + "name": "tx_id", + "type": "u64", + "indexed": true + }, + { + "name": "tx", + "type": "Transaction" + } + ] + }, + { + "identifier": "transferFailedInvalidToken", + "inputs": [ + { + "name": "batch_id", + "type": "u64", + "indexed": true + }, + { + "name": "tx_id", + "type": "u64", + "indexed": true + } + ] + }, + { + "identifier": "transferFailedFrozenDestinationAccount", + "inputs": [ + { + "name": "batch_id", + "type": "u64", + "indexed": true + }, + { + "name": "tx_id", + "type": "u64", + "indexed": true + } + ] + }, + { + "identifier": "transferOverMaxAmount", + "inputs": [ + { + "name": "batch_id", + "type": "u64", + "indexed": true + }, + { + "name": "tx_id", + "type": "u64", + "indexed": true + } + ] + }, + { + "identifier": "transferFailedExecutionFailed", + "inputs": [ + { + "name": "batch_id", + "type": "u64", + "indexed": true + }, + { + "name": "tx_id", + "type": "u64", + "indexed": true + } + ] + } + ], + "hasCallback": true, + "types": { + "BatchStatus": { + "type": "enum", + "variants": [ + { + "name": "AlreadyProcessed", + "discriminant": 0 + }, + { + "name": "Empty", + "discriminant": 1 + }, + { + "name": "PartiallyFull", + "discriminant": 2, + "fields": [ + { + "name": "end_block_nonce", + "type": "u64" + }, + { + "name": "tx_ids", + "type": "List" + } + ] + }, + { + "name": "Full", + "discriminant": 3 + }, + { + "name": "WaitingForSignatures", + "discriminant": 4 + } + ] + }, + "DepositEvent": { + "type": "struct", + "fields": [ + { + "name": "tx_nonce", + "type": "u64" + }, + { + "name": "opt_function", + "type": "Option" + }, + { + "name": "opt_arguments", + "type": "Option>" + }, + { + "name": "opt_gas_limit", + "type": "Option" + } + ] + }, + "EsdtTokenPayment": { + "type": "struct", + "fields": [ + { + "name": "token_identifier", + "type": "TokenIdentifier" + }, + { + "name": "token_nonce", + "type": "u64" + }, + { + "name": "amount", + "type": "BigUint" + } + ] + }, + "EsdtTokenType": { + "type": "enum", + "variants": [ + { + "name": "Fungible", + "discriminant": 0 + }, + { + "name": "NonFungible", + "discriminant": 1 + }, + { + "name": "SemiFungible", + "discriminant": 2 + }, + { + "name": "Meta", + "discriminant": 3 + }, + { + "name": "Invalid", + "discriminant": 4 + } + ] + }, + "StolenFromFrameworkEsdtTokenData": { + "type": "struct", + "fields": [ + { + "name": "token_type", + "type": "EsdtTokenType" + }, + { + "name": "amount", + "type": "BigUint" + }, + { + "name": "frozen", + "type": "bool" + }, + { + "name": "hash", + "type": "bytes" + }, + { + "name": "name", + "type": "bytes" + }, + { + "name": "attributes", + "type": "bytes" + }, + { + "name": "creator", + "type": "Address" + }, + { + "name": "royalties", + "type": "BigUint" + }, + { + "name": "uris", + "type": "List" + } + ] + }, + "Transaction": { + "type": "struct", + "fields": [ + { + "name": "block_nonce", + "type": "u64" + }, + { + "name": "nonce", + "type": "u64" + }, + { + "name": "from", + "type": "Address" + }, + { + "name": "to", + "type": "Address" + }, + { + "name": "tokens", + "type": "List" + }, + { + "name": "token_data", + "type": "List" + }, + { + "name": "opt_transfer_data", + "type": "Option" + }, + { + "name": "is_refund_tx", + "type": "bool" + } + ] + }, + "TransactionStatus": { + "type": "enum", + "variants": [ + { + "name": "None", + "discriminant": 0 + }, + { + "name": "Pending", + "discriminant": 1 + }, + { + "name": "InProgress", + "discriminant": 2 + }, + { + "name": "Executed", + "discriminant": 3 + }, + { + "name": "Rejected", + "discriminant": 4 + } + ] + }, + "TransferData": { + "type": "struct", + "fields": [ + { + "name": "gas_limit", + "type": "u64" + }, + { + "name": "function", + "type": "bytes" + }, + { + "name": "args", + "type": "List" + } + ] + } + } +} From 87662b24055a1abbc011b7d91c91455750fce54a Mon Sep 17 00:00:00 2001 From: Alexandru Popenta Date: Fri, 21 Jun 2024 16:04:29 +0300 Subject: [PATCH 2/8] finish implementation and add some unit tests --- multiversx_sdk/abi/abi.py | 61 +++++++++--- multiversx_sdk/abi/abi_test.py | 1 - .../transactions_outcome_parsers/__init__.py | 4 +- .../transactions_outcome_parsers/resources.py | 4 + .../transaction_events_parser.py | 13 +-- .../transaction_events_parser_test.py | 92 ++++++++++++++++++- .../network_providers/contract_results.py | 1 + 7 files changed, 147 insertions(+), 29 deletions(-) diff --git a/multiversx_sdk/abi/abi.py b/multiversx_sdk/abi/abi.py index 5d88cdee..0f10d0aa 100644 --- a/multiversx_sdk/abi/abi.py +++ b/multiversx_sdk/abi/abi.py @@ -40,7 +40,7 @@ def __init__(self, definition: AbiDefinition) -> None: self.definition = definition self.custom_types_prototypes_by_name: Dict[str, Any] = {} self.endpoints_prototypes_by_name: Dict[str, EndpointPrototype] = {} - self.events_prototypes_by_name: Dict[str, EndpointPrototype] = {} + self.events_prototypes_by_name: Dict[str, EventPrototype] = {} for name in definition.types.enums: self.custom_types_prototypes_by_name[name] = self._create_custom_type_prototype(name) @@ -72,10 +72,8 @@ def __init__(self, definition: AbiDefinition) -> None: for event in definition.events: input_prototype = self._create_event_input_prototypes(event) - # events do not have 'outputs' - event_prototype = EndpointPrototype( - input_parameters=input_prototype, - output_parameters=[] + event_prototype = EventPrototype( + input_parameters=input_prototype ) self.events_prototypes_by_name[event.identifier] = event_prototype @@ -143,7 +141,7 @@ def _create_event_input_prototypes(self, event: EventDefinition) -> List[Any]: prototypes: List[Any] = [] for topic in event.inputs: - parameter_prototype = self._create_topic_prototype(topic) + parameter_prototype = EventField(name=topic.name, value=self._create_topic_prototype(topic)) prototypes.append(parameter_prototype) return prototypes @@ -189,14 +187,37 @@ def decode_endpoint_output_parameters(self, endpoint_name: str, encoded_values: output_native_values = [value.get_payload() for value in output_values_as_native_object_holders] return output_native_values - def decode_event(self, event_name: str, encoded_values: List[bytes]) -> List[Any]: - event_prototype = self._get_event_prototype(event_name) - output_values = deepcopy(event_prototype.input_parameters) - self._serializer.deserialize_parts(encoded_values, output_values) + def decode_event(self, event_definition: EventDefinition, topics: List[bytes], data_items: List[bytes]) -> List[Any]: + result: Any = {} + event_prototype = self._get_event_prototype(event_definition.identifier) + + indexed_inputs = [input for input in event_definition.inputs if input.indexed] + indexed_inputs_names = [item.name for item in indexed_inputs] + + parameters = deepcopy(event_prototype.input_parameters) + + output_values = [param.value for param in parameters if param.name in indexed_inputs_names] + self._serializer.deserialize_parts(topics, output_values) output_values_as_native_object_holders = cast(List[IPayloadHolder], output_values) output_native_values = [value.get_payload() for value in output_values_as_native_object_holders] - return output_native_values + + for i in range(len(indexed_inputs)): + result[indexed_inputs[i].name] = output_native_values[i] + + non_indexed_inputs = [input for input in event_definition.inputs if not input.indexed] + non_indexed_inputs_names = [item.name for item in non_indexed_inputs] + + output_values = [param.value for param in parameters if param.name in non_indexed_inputs_names] + self._serializer.deserialize_parts(data_items, output_values) + + output_values_as_native_object_holders = cast(List[IPayloadHolder], output_values) + output_native_values = [value.get_payload() for value in output_values_as_native_object_holders] + + for i in range(len(non_indexed_inputs)): + result[non_indexed_inputs[i].name] = output_native_values[i] + + return result def get_event(self, name: str) -> EventDefinition: event = [event for event in self.definition.events if event.identifier == name] @@ -225,11 +246,12 @@ def _get_endpoint_prototype(self, endpoint_name: str) -> 'EndpointPrototype': return endpoint_prototype - def _get_event_prototype(self, event_name: str) -> 'EndpointPrototype': - event_prototype = self.endpoints_prototypes_by_name.get(event_name) + def _get_event_prototype(self, event_name: str) -> 'EventPrototype': + event_prototype = self.events_prototypes_by_name.get(event_name) if not event_prototype: - raise ValueError(f"endpoint '{event_name}' not found") + raise ValueError(f"event '{event_name}' not found") + # return self._create_event_input_prototypes() return event_prototype @@ -302,3 +324,14 @@ class EndpointPrototype: def __init__(self, input_parameters: List[Any], output_parameters: List[Any]) -> None: self.input_parameters = input_parameters self.output_parameters = output_parameters + + +class EventField: + def __init__(self, name: str, value: Any) -> None: + self.name = name + self.value = value + + +class EventPrototype: + def __init__(self, input_parameters: List[EventField]) -> None: + self.input_parameters = input_parameters diff --git a/multiversx_sdk/abi/abi_test.py b/multiversx_sdk/abi/abi_test.py index 2c9aeddd..fedbdd5d 100644 --- a/multiversx_sdk/abi/abi_test.py +++ b/multiversx_sdk/abi/abi_test.py @@ -61,7 +61,6 @@ def test_abi_events(): assert len(abi.definition.events) == 1 assert abi.events_prototypes_by_name["firstEvent"].input_parameters == [BigUIntValue()] - assert abi.events_prototypes_by_name["firstEvent"].output_parameters == [] def test_encode_endpoint_input_parameters_artificial_contract(): diff --git a/multiversx_sdk/core/transactions_outcome_parsers/__init__.py b/multiversx_sdk/core/transactions_outcome_parsers/__init__.py index 815ab2ea..94d8ef62 100644 --- a/multiversx_sdk/core/transactions_outcome_parsers/__init__.py +++ b/multiversx_sdk/core/transactions_outcome_parsers/__init__.py @@ -2,7 +2,7 @@ DelegationTransactionsOutcomeParser from multiversx_sdk.core.transactions_outcome_parsers.resources import ( SmartContractResult, TransactionEvent, TransactionLogs, TransactionOutcome, - find_events_by_identifier) + find_events_by_first_topic, find_events_by_identifier) from multiversx_sdk.core.transactions_outcome_parsers.smart_contract_transactions_outcome_parser import \ SmartContractTransactionsOutcomeParser from multiversx_sdk.core.transactions_outcome_parsers.token_management_transactions_outcome_parser import \ @@ -11,5 +11,5 @@ __all__ = [ "TokenManagementTransactionsOutcomeParser", "SmartContractResult", "TransactionEvent", "TransactionLogs", "TransactionOutcome", "find_events_by_identifier", "DelegationTransactionsOutcomeParser", - "SmartContractTransactionsOutcomeParser" + "SmartContractTransactionsOutcomeParser", "find_events_by_first_topic" ] diff --git a/multiversx_sdk/core/transactions_outcome_parsers/resources.py b/multiversx_sdk/core/transactions_outcome_parsers/resources.py index b491fba7..4b73439c 100644 --- a/multiversx_sdk/core/transactions_outcome_parsers/resources.py +++ b/multiversx_sdk/core/transactions_outcome_parsers/resources.py @@ -59,6 +59,10 @@ def find_events_by_identifier(transaction_outcome: TransactionOutcome, identifie return find_events_by_predicate(transaction_outcome, lambda event: event.identifier == identifier) +def find_events_by_first_topic(transaction_outcome: TransactionOutcome, topic: str) -> List[TransactionEvent]: + return find_events_by_predicate(transaction_outcome, lambda event: event.topics[0].decode() == topic if len(event.topics) else False) + + def find_events_by_predicate( transaction_outcome: TransactionOutcome, predicate: Callable[[TransactionEvent], bool] diff --git a/multiversx_sdk/core/transactions_outcome_parsers/transaction_events_parser.py b/multiversx_sdk/core/transactions_outcome_parsers/transaction_events_parser.py index 761d7293..1fa222de 100644 --- a/multiversx_sdk/core/transactions_outcome_parsers/transaction_events_parser.py +++ b/multiversx_sdk/core/transactions_outcome_parsers/transaction_events_parser.py @@ -9,7 +9,7 @@ class IAbi(Protocol): def get_event(self, name: str) -> EventDefinition: ... - def decode_event(self, event_name: str, encoded_values: List[bytes]) -> List[Any]: + def decode_event(self, event_definition: EventDefinition, topics: List[bytes], data_items: List[bytes]) -> List[Any]: ... @@ -34,15 +34,8 @@ def parse_event(self, event: TransactionEvent) -> Any: event.topics = event.topics[1:] event_definition = self.abi.get_event(abi_identifier) - return self.do_parse_event( + return self.abi.decode_event( + event_definition=event_definition, topics=event.topics, data_items=event.data_items, - event_definition=event_definition ) - - def do_parse_event(self, topics: List[bytes], data_items: List[bytes], event_definition: EventDefinition) -> Any: - result: Any = {} - - # "Indexed" ABI "event.inputs" correspond to "event.topics[1:]": - indexed_inputs = [input for input in event_definition.inputs if input.indexed] - decoded_topics = [] diff --git a/multiversx_sdk/core/transactions_outcome_parsers/transaction_events_parser_test.py b/multiversx_sdk/core/transactions_outcome_parsers/transaction_events_parser_test.py index 1aeb47c3..e8b3b78c 100644 --- a/multiversx_sdk/core/transactions_outcome_parsers/transaction_events_parser_test.py +++ b/multiversx_sdk/core/transactions_outcome_parsers/transaction_events_parser_test.py @@ -1,8 +1,11 @@ from pathlib import Path +from types import SimpleNamespace from multiversx_sdk.abi.abi import Abi -from multiversx_sdk.core.transactions_outcome_parsers.resources import \ - TransactionEvent +from multiversx_sdk.core.address import Address +from multiversx_sdk.core.transactions_outcome_parsers.resources import ( + SmartContractCallOutcome, SmartContractResult, TransactionEvent, + TransactionLogs, TransactionOutcome, find_events_by_first_topic) from multiversx_sdk.core.transactions_outcome_parsers.transaction_events_parser import \ TransactionEventsParser @@ -24,3 +27,88 @@ def test_parse_events_minimalistic(): assert len(values) == 1 assert values[0] == {"batch_id": 42, "tx_id": 43} + + +def test_parse_esdt_safe_deposit_event(): + abi = Abi.load(testdata / "esdt-safe.abi.json") + parser = TransactionEventsParser(abi=abi) + + transaction_outcome = TransactionOutcome() + + logs = TransactionLogs( + events=[ + TransactionEvent( + topics=[ + bytes.fromhex("6465706f736974"), + bytes.fromhex("726cc2d4b46dd6bd74a4c84d02715bf85cae76318cab81bc09e7c261d4149a67"), + bytes.fromhex("0000000c5745474c442d30316534396400000000000000000000000164") + ], + data_items=[bytes.fromhex("00000000000003db000000")] + ) + ] + ) + + transaction_outcome.direct_smart_contract_call = SmartContractCallOutcome(return_code="ok", return_message="ok") + transaction_outcome.transaction_results = [ + SmartContractResult(data=bytes.fromhex("4036663662"), logs=logs) + ] + + events = find_events_by_first_topic(transaction_outcome, "deposit") + parsed = parser.parse_events(events) + + assert len(parsed) == 1 + assert parsed[0] == { + "dest_address": Address.new_from_bech32("erd1wfkv9495dhtt6a9yepxsyu2mlpw2ua333j4cr0qfulpxr4q5nfnshgyqun").get_public_key(), + "tokens": [SimpleNamespace( + token_identifier="WEGLD-01e49d", + token_nonce=0, + amount=100 + )], + "event_data": SimpleNamespace( + tx_nonce=987, + opt_function=None, + opt_arguments=None, + opt_gas_limit=None, + ) + } + + +def test_parse_multisig_start_perform_action(): + abi = Abi.load(testdata / "multisig-full.abi.json") + parser = TransactionEventsParser(abi=abi) + + transaction_outcome = TransactionOutcome( + direct_smart_contract_call_outcome=SmartContractCallOutcome(return_code="ok", return_message="ok"), + transaction_results=[SmartContractResult(data=bytes.fromhex("4036663662"))], + transaction_logs=TransactionLogs(events=[TransactionEvent( + identifier="performAction", + topics=[bytes.fromhex("7374617274506572666f726d416374696f6e")], + data_items=[bytes.fromhex("00000001000000000500000000000000000500d006f73c4221216fa679bc559005584c4f1160e569e1000000000000000003616464000000010000000107000000010139472eff6886771a982f3083da5d421f24c29181e63888228dc81ca60d69e1")] + )]) + ) + + events = find_events_by_first_topic(transaction_outcome, "startPerformAction") + parsed = parser.parse_events(events) + data = parsed[0]["data"] + + assert data == SimpleNamespace( + action_id=1, + group_id=0, + action_data=SimpleNamespace( + **{ + "0": SimpleNamespace( + to=Address.new_from_bech32("erd1qqqqqqqqqqqqqpgq6qr0w0zzyysklfneh32eqp2cf383zc89d8sstnkl60").get_public_key(), + egld_amount=0, + opt_gas_limit=None, + endpoint_name=b'add', + arguments=[bytes.fromhex("07")] + ), + '__discriminant__': 5 + } + ), + signers=[Address.new_from_bech32("erd1qyu5wthldzr8wx5c9ucg8kjagg0jfs53s8nr3zpz3hypefsdd8ssycr6th").get_public_key()] + ) + + +def test_parse_event_with_multi_values(): + pass diff --git a/multiversx_sdk/network_providers/contract_results.py b/multiversx_sdk/network_providers/contract_results.py index e00eefc4..82e41af7 100644 --- a/multiversx_sdk/network_providers/contract_results.py +++ b/multiversx_sdk/network_providers/contract_results.py @@ -55,6 +55,7 @@ def to_dictionary(self) -> Dict[str, Any]: "callType": self.call_type, "returnMessage": self.return_message, "isRefund": self.is_refund, + "logs": self.logs.to_dictionary() } @staticmethod From bea1305c13bcabc2e1898988b9c2b797e05a5370 Mon Sep 17 00:00:00 2001 From: Alexandru Popenta Date: Tue, 25 Jun 2024 09:37:34 +0300 Subject: [PATCH 3/8] add unit test & small fixes --- multiversx_sdk/abi/abi_definition.py | 3 +- multiversx_sdk/abi/abi_test.py | 2 +- .../transaction_events_parser_test.py | 54 ++++++++++++++++++- 3 files changed, 56 insertions(+), 3 deletions(-) diff --git a/multiversx_sdk/abi/abi_definition.py b/multiversx_sdk/abi/abi_definition.py index d8cee37a..cc12a65c 100644 --- a/multiversx_sdk/abi/abi_definition.py +++ b/multiversx_sdk/abi/abi_definition.py @@ -50,7 +50,8 @@ def _get_definition_for_upgrade(cls, data: Dict[str, Any]) -> "EndpointDefinitio return EndpointDefinition.from_dict(data["upgradeConstructor"]) # Fallback for contracts written using a not-old, but not-new Rust framework: - if "upgrade" in data["endpoints"]: + endpoints = data.get("endpoints", []) + if "upgrade" in endpoints: return EndpointDefinition.from_dict(data["endpoints"]["upgrade"]) # Fallback for contracts written using an old Rust framework: diff --git a/multiversx_sdk/abi/abi_test.py b/multiversx_sdk/abi/abi_test.py index fedbdd5d..d375bb5b 100644 --- a/multiversx_sdk/abi/abi_test.py +++ b/multiversx_sdk/abi/abi_test.py @@ -60,7 +60,7 @@ def test_abi_events(): abi = Abi.load(testdata / "artificial.abi.json") assert len(abi.definition.events) == 1 - assert abi.events_prototypes_by_name["firstEvent"].input_parameters == [BigUIntValue()] + assert abi.events_prototypes_by_name["firstEvent"].input_parameters[0].value == BigUIntValue() def test_encode_endpoint_input_parameters_artificial_contract(): diff --git a/multiversx_sdk/core/transactions_outcome_parsers/transaction_events_parser_test.py b/multiversx_sdk/core/transactions_outcome_parsers/transaction_events_parser_test.py index e8b3b78c..6c55e2ee 100644 --- a/multiversx_sdk/core/transactions_outcome_parsers/transaction_events_parser_test.py +++ b/multiversx_sdk/core/transactions_outcome_parsers/transaction_events_parser_test.py @@ -2,6 +2,7 @@ from types import SimpleNamespace from multiversx_sdk.abi.abi import Abi +from multiversx_sdk.abi.abi_definition import AbiDefinition from multiversx_sdk.core.address import Address from multiversx_sdk.core.transactions_outcome_parsers.resources import ( SmartContractCallOutcome, SmartContractResult, TransactionEvent, @@ -111,4 +112,55 @@ def test_parse_multisig_start_perform_action(): def test_parse_event_with_multi_values(): - pass + abi_definition = AbiDefinition.from_dict( + { + "events": [ + { + "identifier": "doFoobar", + "inputs": [ + { + "name": "a", + "type": "multi", + "indexed": True, + }, + { + "name": "b", + "type": "multi", + "indexed": True, + }, + { + "name": "c", + "type": "u8", + "indexed": False, + }, + ], + }, + ] + } + ) + + abi = Abi(abi_definition) + parser = TransactionEventsParser(abi=abi) + value = 42 + + parsed = parser.parse_event( + TransactionEvent( + identifier="foobar", + topics=[ + "doFoobar".encode(), + value.to_bytes(), + "test".encode(), + (value + 1).to_bytes(), + "test".encode(), + "test".encode(), + (value + 2).to_bytes() + ], + data_items=[value.to_bytes()] + ) + ) + + assert parsed == { + "a": [42, "test", 43, "test"], + "b": ["test", 44], + "c": 42 + } From 4509a7815e2dbfeb490eabc262734502b815c56c Mon Sep 17 00:00:00 2001 From: Alexandru Popenta Date: Tue, 25 Jun 2024 10:06:34 +0300 Subject: [PATCH 4/8] remove commented line --- multiversx_sdk/abi/abi.py | 1 - 1 file changed, 1 deletion(-) diff --git a/multiversx_sdk/abi/abi.py b/multiversx_sdk/abi/abi.py index 0f10d0aa..ffa163cd 100644 --- a/multiversx_sdk/abi/abi.py +++ b/multiversx_sdk/abi/abi.py @@ -251,7 +251,6 @@ def _get_event_prototype(self, event_name: str) -> 'EventPrototype': if not event_prototype: raise ValueError(f"event '{event_name}' not found") - # return self._create_event_input_prototypes() return event_prototype From 3f3ef9b84e9088cd46899ae0df851a27ea59ccdb Mon Sep 17 00:00:00 2001 From: Alexandru Popenta Date: Tue, 25 Jun 2024 17:01:51 +0300 Subject: [PATCH 5/8] fixes after review --- multiversx_sdk/abi/abi.py | 43 ++++++++----------- multiversx_sdk/abi/abi_definition.py | 11 +++++ multiversx_sdk/abi/abi_test.py | 2 +- .../transaction_events_parser.py | 23 +++++----- .../transaction_events_parser_test.py | 12 +++--- 5 files changed, 46 insertions(+), 45 deletions(-) diff --git a/multiversx_sdk/abi/abi.py b/multiversx_sdk/abi/abi.py index ffa163cd..a020b5c9 100644 --- a/multiversx_sdk/abi/abi.py +++ b/multiversx_sdk/abi/abi.py @@ -1,5 +1,6 @@ from copy import deepcopy from pathlib import Path +from types import SimpleNamespace from typing import Any, Dict, List, cast from multiversx_sdk.abi.abi_definition import (AbiDefinition, @@ -70,10 +71,10 @@ def __init__(self, definition: AbiDefinition) -> None: self.endpoints_prototypes_by_name[endpoint.name] = endpoint_prototype for event in definition.events: - input_prototype = self._create_event_input_prototypes(event) + prototype = self._create_event_input_prototypes(event) event_prototype = EventPrototype( - input_parameters=input_prototype + fields=prototype ) self.events_prototypes_by_name[event.identifier] = event_prototype @@ -141,8 +142,8 @@ def _create_event_input_prototypes(self, event: EventDefinition) -> List[Any]: prototypes: List[Any] = [] for topic in event.inputs: - parameter_prototype = EventField(name=topic.name, value=self._create_topic_prototype(topic)) - prototypes.append(parameter_prototype) + event_field_prototype = EventField(name=topic.name, value=self._create_event_field_prototype(topic)) + prototypes.append(event_field_prototype) return prototypes @@ -150,7 +151,7 @@ def _create_parameter_prototype(self, parameter: ParameterDefinition) -> Any: type_formula = self._type_formula_parser.parse_expression(parameter.type) return self._create_prototype(type_formula) - def _create_topic_prototype(self, parameter: EventTopicDefinition) -> Any: + def _create_event_field_prototype(self, parameter: EventTopicDefinition) -> Any: type_formula = self._type_formula_parser.parse_expression(parameter.type) return self._create_prototype(type_formula) @@ -187,49 +188,39 @@ def decode_endpoint_output_parameters(self, endpoint_name: str, encoded_values: output_native_values = [value.get_payload() for value in output_values_as_native_object_holders] return output_native_values - def decode_event(self, event_definition: EventDefinition, topics: List[bytes], data_items: List[bytes]) -> List[Any]: - result: Any = {} - event_prototype = self._get_event_prototype(event_definition.identifier) + def decode_event(self, event_name: str, topics: List[bytes], data_items: List[bytes]) -> SimpleNamespace: + result = SimpleNamespace() + event_definition = self.definition.get_event_definition(event_name) + event_prototype = self._get_event_prototype(event_name) indexed_inputs = [input for input in event_definition.inputs if input.indexed] indexed_inputs_names = [item.name for item in indexed_inputs] - parameters = deepcopy(event_prototype.input_parameters) + fields = deepcopy(event_prototype.fields) - output_values = [param.value for param in parameters if param.name in indexed_inputs_names] + output_values = [field.value for field in fields if field.name in indexed_inputs_names] self._serializer.deserialize_parts(topics, output_values) output_values_as_native_object_holders = cast(List[IPayloadHolder], output_values) output_native_values = [value.get_payload() for value in output_values_as_native_object_holders] for i in range(len(indexed_inputs)): - result[indexed_inputs[i].name] = output_native_values[i] + setattr(result, indexed_inputs[i].name, output_native_values[i]) non_indexed_inputs = [input for input in event_definition.inputs if not input.indexed] non_indexed_inputs_names = [item.name for item in non_indexed_inputs] - output_values = [param.value for param in parameters if param.name in non_indexed_inputs_names] + output_values = [field.value for field in fields if field.name in non_indexed_inputs_names] self._serializer.deserialize_parts(data_items, output_values) output_values_as_native_object_holders = cast(List[IPayloadHolder], output_values) output_native_values = [value.get_payload() for value in output_values_as_native_object_holders] for i in range(len(non_indexed_inputs)): - result[non_indexed_inputs[i].name] = output_native_values[i] + setattr(result, non_indexed_inputs[i].name, output_native_values[i]) return result - def get_event(self, name: str) -> EventDefinition: - event = [event for event in self.definition.events if event.identifier == name] - - if not len(event): - raise Exception(f"event [{name}] not found") - - if len(event) > 1: - raise Exception(f"more than one event found: [{event}]") - - return event[0] - def _get_custom_type_prototype(self, type_name: str) -> Any: type_prototype = self.custom_types_prototypes_by_name.get(type_name) @@ -332,5 +323,5 @@ def __init__(self, name: str, value: Any) -> None: class EventPrototype: - def __init__(self, input_parameters: List[EventField]) -> None: - self.input_parameters = input_parameters + def __init__(self, fields: List[EventField]) -> None: + self.fields = fields diff --git a/multiversx_sdk/abi/abi_definition.py b/multiversx_sdk/abi/abi_definition.py index cc12a65c..83d05950 100644 --- a/multiversx_sdk/abi/abi_definition.py +++ b/multiversx_sdk/abi/abi_definition.py @@ -66,6 +66,17 @@ def load(cls, path: Path) -> "AbiDefinition": data = json.loads(content) return cls.from_dict(data) + def get_event_definition(self, name: str) -> "EventDefinition": + event = [event for event in self.events if event.identifier == name] + + if not len(event): + raise Exception(f"event [{name}] not found") + + if len(event) > 1: + raise Exception(f"more than one event found: [{event}]") + + return event[0] + class EndpointDefinition: def __init__(self, diff --git a/multiversx_sdk/abi/abi_test.py b/multiversx_sdk/abi/abi_test.py index d375bb5b..f51774d1 100644 --- a/multiversx_sdk/abi/abi_test.py +++ b/multiversx_sdk/abi/abi_test.py @@ -60,7 +60,7 @@ def test_abi_events(): abi = Abi.load(testdata / "artificial.abi.json") assert len(abi.definition.events) == 1 - assert abi.events_prototypes_by_name["firstEvent"].input_parameters[0].value == BigUIntValue() + assert abi.events_prototypes_by_name["firstEvent"].fields[0].value == BigUIntValue() def test_encode_endpoint_input_parameters_artificial_contract(): diff --git a/multiversx_sdk/core/transactions_outcome_parsers/transaction_events_parser.py b/multiversx_sdk/core/transactions_outcome_parsers/transaction_events_parser.py index 1fa222de..7c3ea3b2 100644 --- a/multiversx_sdk/core/transactions_outcome_parsers/transaction_events_parser.py +++ b/multiversx_sdk/core/transactions_outcome_parsers/transaction_events_parser.py @@ -1,15 +1,13 @@ -from typing import Any, List, Protocol +from copy import deepcopy +from types import SimpleNamespace +from typing import List, Protocol -from multiversx_sdk.abi.abi_definition import EventDefinition from multiversx_sdk.core.transactions_outcome_parsers.resources import \ TransactionEvent class IAbi(Protocol): - def get_event(self, name: str) -> EventDefinition: - ... - - def decode_event(self, event_definition: EventDefinition, topics: List[bytes], data_items: List[bytes]) -> List[Any]: + def decode_event(self, event_name: str, topics: List[bytes], data_items: List[bytes]) -> SimpleNamespace: ... @@ -23,19 +21,20 @@ def __init__(self, abi: IAbi, first_topic_as_identifier: bool = True) -> None: # https://github.com/multiversx/mx-chain-vm-go/blob/v1.5.27/vmhost/contexts/output.go#L283 self.first_topic_as_identifier = first_topic_as_identifier - def parse_events(self, events: List[TransactionEvent]) -> List[Any]: + def parse_events(self, events: List[TransactionEvent]) -> List[SimpleNamespace]: return [self.parse_event(event) for event in events] - def parse_event(self, event: TransactionEvent) -> Any: + def parse_event(self, event: TransactionEvent) -> SimpleNamespace: first_topic = event.topics[0].decode() if len(event.topics) else "" abi_identifier = first_topic if self.first_topic_as_identifier else event.identifier + topics = deepcopy(event.topics) + if self.first_topic_as_identifier: - event.topics = event.topics[1:] + topics = topics[1:] - event_definition = self.abi.get_event(abi_identifier) return self.abi.decode_event( - event_definition=event_definition, - topics=event.topics, + event_name=abi_identifier, + topics=topics, data_items=event.data_items, ) diff --git a/multiversx_sdk/core/transactions_outcome_parsers/transaction_events_parser_test.py b/multiversx_sdk/core/transactions_outcome_parsers/transaction_events_parser_test.py index 6c55e2ee..c8c8f9e1 100644 --- a/multiversx_sdk/core/transactions_outcome_parsers/transaction_events_parser_test.py +++ b/multiversx_sdk/core/transactions_outcome_parsers/transaction_events_parser_test.py @@ -27,7 +27,7 @@ def test_parse_events_minimalistic(): ) assert len(values) == 1 - assert values[0] == {"batch_id": 42, "tx_id": 43} + assert values[0] == SimpleNamespace(**{"batch_id": 42, "tx_id": 43}) def test_parse_esdt_safe_deposit_event(): @@ -58,7 +58,7 @@ def test_parse_esdt_safe_deposit_event(): parsed = parser.parse_events(events) assert len(parsed) == 1 - assert parsed[0] == { + assert parsed[0] == SimpleNamespace(**{ "dest_address": Address.new_from_bech32("erd1wfkv9495dhtt6a9yepxsyu2mlpw2ua333j4cr0qfulpxr4q5nfnshgyqun").get_public_key(), "tokens": [SimpleNamespace( token_identifier="WEGLD-01e49d", @@ -71,7 +71,7 @@ def test_parse_esdt_safe_deposit_event(): opt_arguments=None, opt_gas_limit=None, ) - } + }) def test_parse_multisig_start_perform_action(): @@ -90,7 +90,7 @@ def test_parse_multisig_start_perform_action(): events = find_events_by_first_topic(transaction_outcome, "startPerformAction") parsed = parser.parse_events(events) - data = parsed[0]["data"] + data = parsed[0].data assert data == SimpleNamespace( action_id=1, @@ -159,8 +159,8 @@ def test_parse_event_with_multi_values(): ) ) - assert parsed == { + assert parsed == SimpleNamespace(**{ "a": [42, "test", 43, "test"], "b": ["test", 44], "c": 42 - } + }) From adfbe6ee5cf05923ffb6a33150ccd1b625d537b9 Mon Sep 17 00:00:00 2001 From: Alexandru Popenta Date: Tue, 25 Jun 2024 17:59:18 +0300 Subject: [PATCH 6/8] fix after review --- .../transaction_events_parser.py | 3 +-- .../transaction_events_parser_test.py | 25 +++++++++++-------- 2 files changed, 15 insertions(+), 13 deletions(-) diff --git a/multiversx_sdk/core/transactions_outcome_parsers/transaction_events_parser.py b/multiversx_sdk/core/transactions_outcome_parsers/transaction_events_parser.py index 7c3ea3b2..1a5349fb 100644 --- a/multiversx_sdk/core/transactions_outcome_parsers/transaction_events_parser.py +++ b/multiversx_sdk/core/transactions_outcome_parsers/transaction_events_parser.py @@ -1,4 +1,3 @@ -from copy import deepcopy from types import SimpleNamespace from typing import List, Protocol @@ -28,7 +27,7 @@ def parse_event(self, event: TransactionEvent) -> SimpleNamespace: first_topic = event.topics[0].decode() if len(event.topics) else "" abi_identifier = first_topic if self.first_topic_as_identifier else event.identifier - topics = deepcopy(event.topics) + topics = event.topics if self.first_topic_as_identifier: topics = topics[1:] diff --git a/multiversx_sdk/core/transactions_outcome_parsers/transaction_events_parser_test.py b/multiversx_sdk/core/transactions_outcome_parsers/transaction_events_parser_test.py index c8c8f9e1..7308edd8 100644 --- a/multiversx_sdk/core/transactions_outcome_parsers/transaction_events_parser_test.py +++ b/multiversx_sdk/core/transactions_outcome_parsers/transaction_events_parser_test.py @@ -27,7 +27,10 @@ def test_parse_events_minimalistic(): ) assert len(values) == 1 - assert values[0] == SimpleNamespace(**{"batch_id": 42, "tx_id": 43}) + assert values[0] == SimpleNamespace( + batch_id=42, + tx_id=43 + ) def test_parse_esdt_safe_deposit_event(): @@ -58,20 +61,20 @@ def test_parse_esdt_safe_deposit_event(): parsed = parser.parse_events(events) assert len(parsed) == 1 - assert parsed[0] == SimpleNamespace(**{ - "dest_address": Address.new_from_bech32("erd1wfkv9495dhtt6a9yepxsyu2mlpw2ua333j4cr0qfulpxr4q5nfnshgyqun").get_public_key(), - "tokens": [SimpleNamespace( + assert parsed[0] == SimpleNamespace( + dest_address=Address.new_from_bech32("erd1wfkv9495dhtt6a9yepxsyu2mlpw2ua333j4cr0qfulpxr4q5nfnshgyqun").get_public_key(), + tokens=[SimpleNamespace( token_identifier="WEGLD-01e49d", token_nonce=0, amount=100 )], - "event_data": SimpleNamespace( + event_data=SimpleNamespace( tx_nonce=987, opt_function=None, opt_arguments=None, opt_gas_limit=None, ) - }) + ) def test_parse_multisig_start_perform_action(): @@ -159,8 +162,8 @@ def test_parse_event_with_multi_values(): ) ) - assert parsed == SimpleNamespace(**{ - "a": [42, "test", 43, "test"], - "b": ["test", 44], - "c": 42 - }) + assert parsed == SimpleNamespace( + a=[42, "test", 43, "test"], + b=["test", 44], + c=42 + ) From bed50070a2fa9958b956e4138dd3428e6e8b8310 Mon Sep 17 00:00:00 2001 From: Alexandru Popenta Date: Wed, 26 Jun 2024 15:59:31 +0300 Subject: [PATCH 7/8] add extra check for empty event topic --- .../transactions_outcome_parsers/transaction_events_parser.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/multiversx_sdk/core/transactions_outcome_parsers/transaction_events_parser.py b/multiversx_sdk/core/transactions_outcome_parsers/transaction_events_parser.py index 1a5349fb..62307e75 100644 --- a/multiversx_sdk/core/transactions_outcome_parsers/transaction_events_parser.py +++ b/multiversx_sdk/core/transactions_outcome_parsers/transaction_events_parser.py @@ -25,7 +25,7 @@ def parse_events(self, events: List[TransactionEvent]) -> List[SimpleNamespace]: def parse_event(self, event: TransactionEvent) -> SimpleNamespace: first_topic = event.topics[0].decode() if len(event.topics) else "" - abi_identifier = first_topic if self.first_topic_as_identifier else event.identifier + abi_identifier = first_topic if first_topic and self.first_topic_as_identifier else event.identifier topics = event.topics From 0f533b4bc44dc40a5f1d06a76cccba35f14bec83 Mon Sep 17 00:00:00 2001 From: Alexandru Popenta Date: Wed, 26 Jun 2024 16:24:30 +0300 Subject: [PATCH 8/8] add unit test when first topic is not identifier --- .../transaction_events_parser_test.py | 48 ++++++++++++++++++- 1 file changed, 47 insertions(+), 1 deletion(-) diff --git a/multiversx_sdk/core/transactions_outcome_parsers/transaction_events_parser_test.py b/multiversx_sdk/core/transactions_outcome_parsers/transaction_events_parser_test.py index 7308edd8..0e18474d 100644 --- a/multiversx_sdk/core/transactions_outcome_parsers/transaction_events_parser_test.py +++ b/multiversx_sdk/core/transactions_outcome_parsers/transaction_events_parser_test.py @@ -6,7 +6,8 @@ from multiversx_sdk.core.address import Address from multiversx_sdk.core.transactions_outcome_parsers.resources import ( SmartContractCallOutcome, SmartContractResult, TransactionEvent, - TransactionLogs, TransactionOutcome, find_events_by_first_topic) + TransactionLogs, TransactionOutcome, find_events_by_first_topic, + find_events_by_identifier) from multiversx_sdk.core.transactions_outcome_parsers.transaction_events_parser import \ TransactionEventsParser @@ -167,3 +168,48 @@ def test_parse_event_with_multi_values(): b=["test", 44], c=42 ) + + +def test_parse_esdt_safe_deposit_event_without_first_topic(): + abi = Abi.load(testdata / "esdt-safe.abi.json") + parser = TransactionEventsParser(abi=abi) + + transaction_outcome = TransactionOutcome() + + logs = TransactionLogs( + events=[ + TransactionEvent( + identifier="deposit", + topics=[ + bytes.fromhex(""), + bytes.fromhex("726cc2d4b46dd6bd74a4c84d02715bf85cae76318cab81bc09e7c261d4149a67"), + bytes.fromhex("0000000c5745474c442d30316534396400000000000000000000000164") + ], + data_items=[bytes.fromhex("00000000000003db000000")] + ) + ] + ) + + transaction_outcome.direct_smart_contract_call = SmartContractCallOutcome(return_code="ok", return_message="ok") + transaction_outcome.transaction_results = [ + SmartContractResult(data=bytes.fromhex("4036663662"), logs=logs) + ] + + events = find_events_by_identifier(transaction_outcome, "deposit") + parsed = parser.parse_events(events) + + assert len(parsed) == 1 + assert parsed[0] == SimpleNamespace( + dest_address=Address.new_from_bech32("erd1wfkv9495dhtt6a9yepxsyu2mlpw2ua333j4cr0qfulpxr4q5nfnshgyqun").get_public_key(), + tokens=[SimpleNamespace( + token_identifier="WEGLD-01e49d", + token_nonce=0, + amount=100 + )], + event_data=SimpleNamespace( + tx_nonce=987, + opt_function=None, + opt_arguments=None, + opt_gas_limit=None, + ) + )