Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Transaction events parser #67

Merged
merged 9 commits into from
Jun 27, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
79 changes: 78 additions & 1 deletion multiversx_sdk/abi/abi.py
Original file line number Diff line number Diff line change
@@ -1,10 +1,12 @@
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,
EndpointDefinition,
EnumDefinition,
EnumDefinition, EventDefinition,
EventTopicDefinition,
ParameterDefinition,
StructDefinition)
from multiversx_sdk.abi.address_value import AddressValue
Expand Down Expand Up @@ -39,6 +41,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, EventPrototype] = {}

for name in definition.types.enums:
self.custom_types_prototypes_by_name[name] = self._create_custom_type_prototype(name)
Expand Down Expand Up @@ -67,6 +70,15 @@ def __init__(self, definition: AbiDefinition) -> None:

self.endpoints_prototypes_by_name[endpoint.name] = endpoint_prototype

for event in definition.events:
prototype = self._create_event_input_prototypes(event)

event_prototype = EventPrototype(
fields=prototype
)

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]
Expand Down Expand Up @@ -126,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:
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

for field in event.fields (and those who are indexed = true will be matched to topics in the end, yes).

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

we can't use event.fields, this is EventDefiniton not EventPrototype. Or perhaps did I understand something wrong?

event_field_prototype = EventField(name=topic.name, value=self._create_event_field_prototype(topic))
prototypes.append(event_field_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_event_field_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)

Expand Down Expand Up @@ -163,6 +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_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]

fields = deepcopy(event_prototype.fields)

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)):
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 = [field.value for field in fields if field.name in non_indexed_inputs_names]
self._serializer.deserialize_parts(data_items, output_values)
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Indeed. AFAIK, non indexed fields are matched with the event data items (not topics).


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)):
setattr(result, non_indexed_inputs[i].name, output_native_values[i])

return result

def _get_custom_type_prototype(self, type_name: str) -> Any:
type_prototype = self.custom_types_prototypes_by_name.get(type_name)

Expand All @@ -179,6 +237,14 @@ def _get_endpoint_prototype(self, endpoint_name: str) -> 'EndpointPrototype':

return endpoint_prototype

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"event '{event_name}' not found")

return event_prototype

def _create_prototype(self, type_formula: TypeFormula) -> Any:
name = type_formula.name

Expand Down Expand Up @@ -248,3 +314,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, fields: List[EventField]) -> None:
self.fields = fields
25 changes: 21 additions & 4 deletions multiversx_sdk/abi/abi_definition.py
Original file line number Diff line number Diff line change
Expand Up @@ -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":
Expand All @@ -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
Expand All @@ -45,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:
Expand All @@ -60,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,
Expand Down Expand Up @@ -282,7 +299,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):
Expand Down
9 changes: 9 additions & 0 deletions multiversx_sdk/abi/abi_test.py
Original file line number Diff line number Diff line change
Expand Up @@ -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")]
Expand All @@ -54,6 +56,13 @@ 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"].fields[0].value == BigUIntValue()


def test_encode_endpoint_input_parameters_artificial_contract():
abi = Abi.load(testdata / "artificial.abi.json")

Expand Down
4 changes: 2 additions & 2 deletions multiversx_sdk/core/transactions_outcome_parsers/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -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 \
Expand All @@ -11,5 +11,5 @@
__all__ = [
"TokenManagementTransactionsOutcomeParser", "SmartContractResult", "TransactionEvent",
"TransactionLogs", "TransactionOutcome", "find_events_by_identifier", "DelegationTransactionsOutcomeParser",
"SmartContractTransactionsOutcomeParser"
"SmartContractTransactionsOutcomeParser", "find_events_by_first_topic"
]
4 changes: 4 additions & 0 deletions multiversx_sdk/core/transactions_outcome_parsers/resources.py
Original file line number Diff line number Diff line change
Expand Up @@ -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]
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
from types import SimpleNamespace
from typing import List, Protocol

from multiversx_sdk.core.transactions_outcome_parsers.resources import \
TransactionEvent


class IAbi(Protocol):
def decode_event(self, event_name: str, topics: List[bytes], data_items: List[bytes]) -> SimpleNamespace:
...


class TransactionEventsParser:
def __init__(self, abi: IAbi, first_topic_as_identifier: bool = True) -> None:
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

👍

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[SimpleNamespace]:
return [self.parse_event(event) for event in events]

def parse_event(self, event: TransactionEvent) -> SimpleNamespace:
first_topic = event.topics[0].decode() if len(event.topics) else ""
Copy link

@axenteoctavian axenteoctavian Jun 26, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm not sure if it's possible but...
If the event has 0 topics and you consider first topic as identifier, this function returns something bad. Maybe you can consider event.identifier as event Name if first_topic == "" && self.first_topic_as_identifier == True

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

i am not sure this is possible, but I've added the extra check.

abi_identifier = first_topic if first_topic and self.first_topic_as_identifier else event.identifier

topics = event.topics

if self.first_topic_as_identifier:
topics = topics[1:]

return self.abi.decode_event(
event_name=abi_identifier,
topics=topics,
data_items=event.data_items,
)
Loading
Loading