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

Encode/Decode custom types using ABI #183

Merged
merged 4 commits into from
Jan 20, 2025
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
151 changes: 138 additions & 13 deletions examples/v1.ipynb
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@
"cells": [
{
"cell_type": "code",
"execution_count": 1,
"execution_count": 2,
"metadata": {},
"outputs": [],
"source": [
Expand Down Expand Up @@ -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",
Expand Down Expand Up @@ -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",
Expand Down Expand Up @@ -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",
Expand Down Expand Up @@ -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",
Expand Down Expand Up @@ -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",
Expand Down Expand Up @@ -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",
Expand Down Expand Up @@ -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",
Expand Down Expand Up @@ -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",
Expand Down Expand Up @@ -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",
Expand Down Expand Up @@ -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",
Expand Down Expand Up @@ -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<bytes>\"},\n",
" {\"name\": \"opt_arguments\", \"type\": \"Option<List<bytes>>\"},\n",
" {\"name\": \"opt_gas_limit\", \"type\": \"Option<u64>\"},\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": {},
Expand Down Expand Up @@ -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",
Expand Down Expand Up @@ -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",
Expand Down
20 changes: 19 additions & 1 deletion multiversx_sdk/abi/abi.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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]):
Copy link
Contributor

Choose a reason for hiding this comment

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

I wonder if using _get_custom_type_prototype() would have been better 💭

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

Not sure. We can think about it. I'll merge it as it is and we'll see.

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)

Expand Down
75 changes: 74 additions & 1 deletion multiversx_sdk/abi/abi_test.py
Original file line number Diff line number Diff line change
@@ -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
Expand Down Expand Up @@ -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<bytes>"},
{"name": "opt_arguments", "type": "Option<List<bytes>>"},
{"name": "opt_gas_limit", "type": "Option<u64>"},
],
}
},
}
)
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"
Loading