diff --git a/examples/v1.ipynb b/examples/v1.ipynb index 0189a369..12a791ed 100644 --- a/examples/v1.ipynb +++ b/examples/v1.ipynb @@ -2,7 +2,7 @@ "cells": [ { "cell_type": "code", - "execution_count": 1, + "execution_count": 2, "metadata": {}, "outputs": [], "source": [ @@ -1110,7 +1110,7 @@ " password=\"password\",\n", " address_index=0\n", ")\n", - "# the user is responsible for managing the nonce\n", + "# the developer is responsible for managing the nonce\n", "account.nonce = entrypoint.recall_account_nonce(account.address)\n", "\n", "transfers_controller = entrypoint.create_transfers_controller()\n", @@ -1156,7 +1156,7 @@ " password=\"password\",\n", " address_index=0\n", ")\n", - "# the user is responsible for managing the nonce\n", + "# the developer is responsible for managing the nonce\n", "alice.nonce = entrypoint.recall_account_nonce(alice.address)\n", "\n", "bob = Address.new_from_bech32(\"erd1spyavw0956vq68xj8y4tenjpq2wd5a9p2c6j8gsz7ztyrnpxrruqzu66jx\")\n", @@ -1205,7 +1205,7 @@ " password=\"password\",\n", " address_index=0\n", ")\n", - "# the user is responsible for managing the nonce\n", + "# the developer is responsible for managing the nonce\n", "alice.nonce = entrypoint.recall_account_nonce(alice.address)\n", "\n", "esdt = Token(identifier=\"TEST-123456\")\n", @@ -1259,7 +1259,7 @@ " password=\"password\",\n", " address_index=0\n", ")\n", - "# the user is responsible for managing the nonce\n", + "# the developer is responsible for managing the nonce\n", "alice.nonce = entrypoint.recall_account_nonce(alice.address)\n", "\n", "bob = Address.new_from_bech32(\"erd1spyavw0956vq68xj8y4tenjpq2wd5a9p2c6j8gsz7ztyrnpxrruqzu66jx\")\n", @@ -1317,7 +1317,7 @@ " password=\"password\",\n", " address_index=0\n", ")\n", - "# the user is responsible for managing the nonce\n", + "# the developer is responsible for managing the nonce\n", "account.nonce = entrypoint.recall_account_nonce(account.address)\n", "\n", "esdt = Token(identifier=\"TEST-123456\")\n", @@ -1454,7 +1454,7 @@ " password=\"password\",\n", " address_index=0\n", ")\n", - "# the user is responsible for managing the nonce\n", + "# the developer is responsible for managing the nonce\n", "account.nonce = entrypoint.recall_account_nonce(account.address)\n", "\n", "# load the abi file\n", @@ -1626,7 +1626,7 @@ " password=\"password\",\n", " address_index=0\n", ")\n", - "# the user is responsible for managing the nonce\n", + "# the developer is responsible for managing the nonce\n", "alice.nonce = entrypoint.recall_account_nonce(alice.address)\n", "\n", "# set the nonce\n", @@ -1678,7 +1678,7 @@ " password=\"password\",\n", " address_index=0\n", ")\n", - "# the user is responsible for managing the nonce\n", + "# the developer is responsible for managing the nonce\n", "account.nonce = entrypoint.recall_account_nonce(account.address)\n", "\n", "# load the abi file\n", @@ -1756,7 +1756,7 @@ " password=\"password\",\n", " address_index=0\n", ")\n", - "# the user is responsible for managing the nonce\n", + "# the developer is responsible for managing the nonce\n", "account.nonce = entrypoint.recall_account_nonce(account.address)\n", "\n", "# load the abi file\n", @@ -1822,7 +1822,7 @@ " password=\"password\",\n", " address_index=0\n", ")\n", - "# the user is responsible for managing the nonce\n", + "# the developer is responsible for managing the nonce\n", "account.nonce = entrypoint.recall_account_nonce(account.address)\n", "\n", "# load the abi file\n", @@ -1933,6 +1933,131 @@ "parsed_event = events_parser.parse_event(event)" ] }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "#### Encoding/Decoding custom types\n", + "\n", + "Whenever needed, the contract ABI can be used for manually encoding or decoding custom types.\n", + "\n", + "Let's encode a struct called `EsdtTokenPayment` (of [multisig](https://github.com/multiversx/mx-contracts-rs/tree/main/contracts/multisig) contract) into binary data." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "from pathlib import Path\n", + "from multiversx_sdk.abi import Abi\n", + "\n", + "abi = Abi.load(Path(\"contracts/multisig-full.abi.json\"))\n", + "encoded = abi.encode_custom_type(\"EsdtTokenPayment\", [\"TEST-8b028f\", 0, 10000])\n", + "print(encoded)" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "Now, let's decode a struct using the ABI." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "from multiversx_sdk.abi import Abi, AbiDefinition\n", + "\n", + "abi_definition = AbiDefinition.from_dict(\n", + " {\n", + " \"endpoints\": [],\n", + " \"events\": [],\n", + " \"types\": {\n", + " \"DepositEvent\": {\n", + " \"type\": \"struct\",\n", + " \"fields\": [\n", + " {\"name\": \"tx_nonce\", \"type\": \"u64\"},\n", + " {\"name\": \"opt_function\", \"type\": \"Option\"},\n", + " {\"name\": \"opt_arguments\", \"type\": \"Option>\"},\n", + " {\"name\": \"opt_gas_limit\", \"type\": \"Option\"},\n", + " ],\n", + " }\n", + " },\n", + " }\n", + ")\n", + "abi = Abi(abi_definition)\n", + "\n", + "decoded_type = abi.decode_custom_type(name=\"DepositEvent\", data=bytes.fromhex(\"00000000000003db000000\"))\n", + "print(decoded_type)" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "If you don't wish to use the ABI, there is another way to do it. First, let's encode a struct." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "from multiversx_sdk.abi import Serializer, U64Value, StructValue, Field, StringValue, BigUIntValue\n", + "\n", + "struct = StructValue([\n", + " Field(name=\"token_identifier\", value=StringValue(\"TEST-8b028f\")),\n", + " Field(name=\"token_nonce\", value=U64Value()),\n", + " Field(name=\"amount\", value=BigUIntValue(10000)),\n", + "])\n", + "\n", + "serializer = Serializer()\n", + "serialized_struct = serializer.serialize([struct])\n", + "print(serialized_struct)" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "Now, let's decode a struct without using the ABI." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "from multiversx_sdk.abi import Serializer, U64Value, OptionValue, BytesValue, ListValue, StructValue, Field\n", + "\n", + "tx_nonce = U64Value()\n", + "function = OptionValue(BytesValue())\n", + "arguments = OptionValue(ListValue([BytesValue()]))\n", + "gas_limit = OptionValue(U64Value())\n", + "\n", + "attributes = StructValue([\n", + " Field(\"tx_nonce\", tx_nonce),\n", + " Field(\"opt_function\", function),\n", + " Field(\"opt_arguments\", arguments),\n", + " Field(\"opt_gas_limit\", gas_limit)\n", + "])\n", + "\n", + "serializer = Serializer()\n", + "serializer.deserialize(\"00000000000003db000000\", [attributes])\n", + "\n", + "print(tx_nonce.get_payload())\n", + "print(function.get_payload())\n", + "print(arguments.get_payload())\n", + "print(gas_limit.get_payload())" + ] + }, { "cell_type": "markdown", "metadata": {}, @@ -2037,7 +2162,7 @@ " password=\"password\",\n", " address_index=0\n", ")\n", - "# the user is responsible for managing the nonce\n", + "# the developer is responsible for managing the nonce\n", "account.nonce = entrypoint.recall_account_nonce(account.address)\n", "\n", "# load the abi file\n", @@ -2136,7 +2261,7 @@ " password=\"password\",\n", " address_index=0\n", ")\n", - "# the user is responsible for managing the nonce\n", + "# the developer is responsible for managing the nonce\n", "alice.nonce = entrypoint.recall_account_nonce(alice.address)\n", "\n", "# set the nonce\n", diff --git a/multiversx_sdk/abi/abi.py b/multiversx_sdk/abi/abi.py index 02ebab54..ef05b410 100644 --- a/multiversx_sdk/abi/abi.py +++ b/multiversx_sdk/abi/abi.py @@ -22,7 +22,7 @@ from multiversx_sdk.abi.enum_value import EnumValue from multiversx_sdk.abi.explicit_enum_value import ExplicitEnumValue from multiversx_sdk.abi.fields import Field -from multiversx_sdk.abi.interface import IPayloadHolder +from multiversx_sdk.abi.interface import IPayloadHolder, ISingleValue from multiversx_sdk.abi.list_value import ListValue from multiversx_sdk.abi.managed_decimal_signed_value import ManagedDecimalSignedValue from multiversx_sdk.abi.managed_decimal_value import ManagedDecimalValue @@ -250,6 +250,24 @@ def decode_event(self, event_name: str, topics: list[bytes], additional_data: li return result + def encode_custom_type(self, name: str, values: list[Any]): + try: + custom_type: IPayloadHolder = self.custom_types_prototypes_by_name[name] + except KeyError: + raise Exception(f'Missing custom type! No custom type found for name: "{name}"') + + custom_type.set_payload(values) + return self._serializer.serialize([custom_type]) + + def decode_custom_type(self, name: str, data: bytes) -> Any: + try: + custom_type: ISingleValue = self.custom_types_prototypes_by_name[name] + except KeyError: + raise Exception(f'Missing custom type! No custom type found for name: "{name}"') + + custom_type.decode_top_level(data) + return custom_type.get_payload() + def _get_custom_type_prototype(self, type_name: str) -> Any: type_prototype = self.custom_types_prototypes_by_name.get(type_name) diff --git a/multiversx_sdk/abi/abi_test.py b/multiversx_sdk/abi/abi_test.py index 3501ae7e..35cb3c61 100644 --- a/multiversx_sdk/abi/abi_test.py +++ b/multiversx_sdk/abi/abi_test.py @@ -1,15 +1,18 @@ +import re from decimal import Decimal from pathlib import Path from types import SimpleNamespace from typing import Optional +import pytest + from multiversx_sdk.abi.abi import Abi from multiversx_sdk.abi.abi_definition import AbiDefinition, ParameterDefinition from multiversx_sdk.abi.address_value import AddressValue from multiversx_sdk.abi.biguint_value import BigUIntValue from multiversx_sdk.abi.bytes_value import BytesValue from multiversx_sdk.abi.counted_variadic_values import CountedVariadicValues -from multiversx_sdk.abi.enum_value import EnumValue +from multiversx_sdk.abi.enum_value import EnumValue, _EnumPayload from multiversx_sdk.abi.explicit_enum_value import ExplicitEnumValue from multiversx_sdk.abi.fields import Field from multiversx_sdk.abi.list_value import ListValue @@ -408,3 +411,73 @@ def test_encode_decode_managed_decimals(): values = abi.decode_endpoint_output_parameters("foobar", [bytes.fromhex("0000000202bc00000002")]) assert values[0] == Decimal("7") + + +def test_decode_custom_struct(): + abi_definition = AbiDefinition.from_dict( + { + "endpoints": [], + "events": [], + "types": { + "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"}, + ], + } + }, + } + ) + abi = Abi(abi_definition) + + with pytest.raises(Exception, match=re.escape('Missing custom type! No custom type found for name: "customType"')): + abi.decode_custom_type("customType", b"") + + decoded_type = abi.decode_custom_type(name="DepositEvent", data=bytes.fromhex("00000000000003db000000")) + assert decoded_type == SimpleNamespace( + tx_nonce=987, + opt_function=None, + opt_arguments=None, + opt_gas_limit=None, + ) + + +def test_decode_custom_enum(): + abi = Abi.load(testdata / "multisig-full.abi.json") + + decoded_type = abi.decode_custom_type( + name="Action", + data=bytes.fromhex( + "0500000000000000000500d006f73c4221216fa679bc559005584c4f1160e569e1000000012a0000000003616464000000010000000107" + ), + ) + + expected_output = _EnumPayload() + setattr( + expected_output, + "0", + SimpleNamespace( + **{ + "to": bytes.fromhex("00000000000000000500d006f73c4221216fa679bc559005584c4f1160e569e1"), + "egld_amount": 42, + "opt_gas_limit": None, + "endpoint_name": b"add", + "arguments": [bytes([0x07])], + }, + ), + ) + setattr(expected_output, "__discriminant__", 5) + assert decoded_type == expected_output + + +def test_encode_custom_struct(): + abi = Abi.load(testdata / "multisig-full.abi.json") + + with pytest.raises(Exception, match=re.escape('Missing custom type! No custom type found for name: "customType"')): + abi.encode_custom_type("customType", []) + + encoded = abi.encode_custom_type("EsdtTokenPayment", ["TEST-8b028f", 0, 10000]) + assert encoded == "0000000b544553542d3862303238660000000000000000000000022710"