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 Converter & refactoring and reorganizing #17

Merged
merged 3 commits into from
Apr 3, 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
44 changes: 22 additions & 22 deletions examples/Cookbook.ipynb
Original file line number Diff line number Diff line change
Expand Up @@ -286,7 +286,7 @@
"metadata": {},
"outputs": [],
"source": [
"from multiversx_sdk.core.transaction_factories import TransactionsFactoryConfig\n",
"from multiversx_sdk.core.transactions_factories import TransactionsFactoryConfig\n",
"\n",
"config = TransactionsFactoryConfig(chain_id=\"D\")"
]
Expand Down Expand Up @@ -314,7 +314,7 @@
],
"source": [
"from multiversx_sdk.core import TokenComputer\n",
"from multiversx_sdk.core.transaction_factories import \\\n",
"from multiversx_sdk.core.transactions_factories import \\\n",
Copy link
Contributor

Choose a reason for hiding this comment

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

Minor, easy to welcome breaking change.

" TransferTransactionsFactory\n",
"\n",
"transfer_factory = TransferTransactionsFactory(config, TokenComputer())\n",
Expand Down Expand Up @@ -530,9 +530,9 @@
"from pathlib import Path\n",
"\n",
"from multiversx_sdk.core import Address, Transaction, TransactionComputer\n",
"from multiversx_sdk.core.transaction_factories.relayed_transactions_factory import \\\n",
"from multiversx_sdk.core.transactions_factories.relayed_transactions_factory import \\\n",
" RelayedTransactionsFactory\n",
"from multiversx_sdk.core.transaction_factories.transactions_factory_config import \\\n",
"from multiversx_sdk.core.transactions_factories.transactions_factory_config import \\\n",
" TransactionsFactoryConfig\n",
"from multiversx_sdk.wallet.user_signer import UserSigner\n",
"\n",
Expand Down Expand Up @@ -593,9 +593,9 @@
"from pathlib import Path\n",
"\n",
"from multiversx_sdk.core import Address, Transaction, TransactionComputer\n",
"from multiversx_sdk.core.transaction_factories.relayed_transactions_factory import \\\n",
"from multiversx_sdk.core.transactions_factories.relayed_transactions_factory import \\\n",
" RelayedTransactionsFactory\n",
"from multiversx_sdk.core.transaction_factories.transactions_factory_config import \\\n",
"from multiversx_sdk.core.transactions_factories.transactions_factory_config import \\\n",
" TransactionsFactoryConfig\n",
"from multiversx_sdk.wallet.user_signer import UserSigner\n",
"\n",
Expand Down Expand Up @@ -658,7 +658,7 @@
"source": [
"from pathlib import Path\n",
"\n",
"from multiversx_sdk.core.transaction_factories import \\\n",
"from multiversx_sdk.core.transactions_factories import \\\n",
" SmartContractTransactionsFactory\n",
"\n",
"sc_factory = SmartContractTransactionsFactory(config, TokenComputer())\n",
Expand Down Expand Up @@ -873,7 +873,7 @@
"name": "stdout",
"output_type": "stream",
"text": [
"['civil', 'uncle', 'uphold', 'elevator', 'danger', 'describe', 'benefit', 'stumble', 'then', 'answer', 'seat', 'logic', 'bonus', 'rule', 'syrup', 'camp', 'sell', 'merge', 'tennis', 'just', 'convince', 'stool', 'blame', 'help']\n"
"['stay', 'coffee', 'you', 'crunch', 'midnight', 'brush', 'museum', 'immune', 'long', 'spawn', 'acquire', 'october', 'federal', 'rule', 'stick', 'track', 'tooth', 'tuition', 'act', 'oyster', 'brush', 'project', 'real', 'hazard']\n"
]
}
],
Expand Down Expand Up @@ -922,8 +922,8 @@
"name": "stdout",
"output_type": "stream",
"text": [
"Secret key: 909e3dcae6f0171f2dcbc5cec950b592b4e5479294eebf1f054b180ff8957c7a\n",
"Public key: 05a1dae02f3e9dd36d3c28401ffaf2a05febc4a0c5d5d9ce24241355c95cd5f0\n"
"Secret key: 17ecf7d2ea8d564076845d524a6d4cf2324e57964efa696ac8215ab9b410f2a0\n",
"Public key: d7dab8fe6933a6853239dabaa385aa83504cc303a10b82bf8f0885ca6fd0f7b0\n"
]
}
],
Expand Down Expand Up @@ -1279,7 +1279,7 @@
},
{
"cell_type": "code",
"execution_count": 95,
"execution_count": 39,
"metadata": {},
"outputs": [],
"source": [
Expand All @@ -1297,7 +1297,7 @@
},
{
"cell_type": "code",
"execution_count": 96,
"execution_count": 40,
"metadata": {},
"outputs": [],
"source": [
Expand All @@ -1322,7 +1322,7 @@
},
{
"cell_type": "code",
"execution_count": 97,
"execution_count": 41,
"metadata": {},
"outputs": [
{
Expand Down Expand Up @@ -1357,15 +1357,15 @@
},
{
"cell_type": "code",
"execution_count": 98,
"execution_count": 42,
"metadata": {},
"outputs": [
{
"name": "stdout",
"output_type": "stream",
"text": [
"Nonce: 848\n",
"Balance: 1544458693139999963\n"
"Nonce: 906\n",
"Balance: 5109981712879999921\n"
]
}
],
Expand All @@ -1385,7 +1385,7 @@
},
{
"cell_type": "code",
"execution_count": 99,
"execution_count": 43,
"metadata": {},
"outputs": [],
"source": [
Expand Down Expand Up @@ -1420,14 +1420,14 @@
},
{
"cell_type": "code",
"execution_count": 100,
"execution_count": 44,
"metadata": {},
"outputs": [
{
"name": "stdout",
"output_type": "stream",
"text": [
"Transaction hash: 4fcbadad2ecf8e505cfbf8e7ef4a05bc516c76efe01bdd963cefc052d6d66c90\n"
"Transaction hash: b9a6c3dde073d08ecc9c78934aae99baca9dc4fd834955ef9e58ea4348a37ee0\n"
]
}
],
Expand Down Expand Up @@ -1459,14 +1459,14 @@
},
{
"cell_type": "code",
"execution_count": 101,
"execution_count": 45,
"metadata": {},
"outputs": [
{
"name": "stdout",
"output_type": "stream",
"text": [
"Transactions hashes: (3, {'0': '4fcbadad2ecf8e505cfbf8e7ef4a05bc516c76efe01bdd963cefc052d6d66c90', '1': '1bbb818d7f6a1b5304af5a78216e8b59331faa84f0bfcc3561ff6e0ff8204625', '2': 'dfc4df48dbffebf8efde7e3b5d5be1d6c5e3914ceb988a5eeb164d676e588087'})\n"
"Transactions hashes: (3, {'0': 'b9a6c3dde073d08ecc9c78934aae99baca9dc4fd834955ef9e58ea4348a37ee0', '1': '275bce29a8cacb6ab32174d4ad74d16afb70a8a857a1d4d459579c43c576e9fa', '2': 'c5f3b48c32ceed4a5dc0938cd26148dd0f68a56383197eb1807192e88a9b3776'})\n"
]
}
],
Expand Down Expand Up @@ -1516,7 +1516,7 @@
},
{
"cell_type": "code",
"execution_count": 102,
"execution_count": 46,
"metadata": {},
"outputs": [
{
Expand Down
10 changes: 6 additions & 4 deletions multiversx_sdk/__init__.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
from multiversx_sdk.adapters.query_runner_adapter import QueryRunnerAdapter
from multiversx_sdk.converters.transactions_converter import \
TransactionsConverter
from multiversx_sdk.core.account import AccountNonceHolder
from multiversx_sdk.core.adapters.query_runner_adapter import \
QueryRunnerAdapter
from multiversx_sdk.core.address import (Address, AddressComputer,
AddressFactory)
from multiversx_sdk.core.code_metadata import CodeMetadata
Expand All @@ -10,7 +11,8 @@
from multiversx_sdk.core.token_payment import TokenPayment
from multiversx_sdk.core.tokens import (Token, TokenComputer,
TokenIdentifierParts, TokenTransfer)
from multiversx_sdk.core.transaction import Transaction, TransactionComputer
from multiversx_sdk.core.transaction import Transaction
from multiversx_sdk.core.transaction_computer import TransactionComputer
from multiversx_sdk.core.transaction_payload import TransactionPayload
from multiversx_sdk.core.transactions_factories.account_transactions_factory import \
AccountTransactionsFactory
Expand Down Expand Up @@ -63,5 +65,5 @@
"GenericError", "GenericResponse", "ApiNetworkProvider", "ProxyNetworkProvider",
"UserSigner", "Mnemonic", "UserSecretKey", "UserPublicKey", "ValidatorSecretKey",
"ValidatorPublicKey", "UserVerifier", "ValidatorSigner", "ValidatorVerifier", "ValidatorPEM",
"UserWallet", "UserPEM", "QueryRunnerAdapter"
"UserWallet", "UserPEM", "QueryRunnerAdapter", "TransactionsConverter"
]
3 changes: 3 additions & 0 deletions multiversx_sdk/adapters/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
from multiversx_sdk.adapters.query_runner_adapter import QueryRunnerAdapter

__all__ = ["QueryRunnerAdapter"]
4 changes: 4 additions & 0 deletions multiversx_sdk/converters/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
from multiversx_sdk.converters.transactions_converter import \
TransactionsConverter

__all__ = ["TransactionsConverter"]
3 changes: 3 additions & 0 deletions multiversx_sdk/converters/errors.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
class MissingFieldError(Exception):
def __init__(self, message: str) -> None:
super().__init__(message)
90 changes: 90 additions & 0 deletions multiversx_sdk/converters/transactions_converter.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,90 @@
import base64
from typing import Any, Dict

from multiversx_sdk.converters.errors import MissingFieldError
from multiversx_sdk.core.interfaces import ITransaction
from multiversx_sdk.core.transaction import Transaction


class TransactionsConverter:
Copy link
Contributor

Choose a reason for hiding this comment

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

in the specs there is also transaction_on_network_to_outcome(transaction: TransactionOnNetwork): TransactionOutcome; do we implement this one also? or not yet?

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

That has been added later. Will be implemented in a future PR.

def __init__(self) -> None:
pass

def transaction_to_dictionary(self, transaction: ITransaction) -> Dict[str, Any]:
return {
"nonce": transaction.nonce,
"value": str(transaction.value),
"receiver": transaction.receiver,
"sender": transaction.sender,
"senderUsername": self._value_to_b64_or_empty(transaction.sender_username),
"receiverUsername": self._value_to_b64_or_empty(transaction.receiver_username),
"gasPrice": transaction.gas_price,
"gasLimit": transaction.gas_limit,
"data": self._value_to_b64_or_empty(transaction.data),
"chainID": transaction.chain_id,
"version": transaction.version,
"options": transaction.options,
"guardian": transaction.guardian,
"signature": self._value_to_hex_or_empty(transaction.signature),
"guardianSignature": self._value_to_hex_or_empty(transaction.guardian_signature)
}

def dictionary_to_transaction(self, dictionary: Dict[str, Any]) -> Transaction:
self._ensure_mandatory_fields_for_transaction(dictionary)

return Transaction(
nonce=dictionary.get("nonce", None),
value=int(dictionary.get("value", None)),
receiver=dictionary["receiver"],
Copy link
Contributor

Choose a reason for hiding this comment

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

Indeed, sender, receiver, gas limit, chain ID are required - but if the input dictionary is missing them, the resulting exception will not be very intelligible (JS implementation would suffer from the same thing, but there we have the legacy IPlainTransaction interface).

Maybe validate the input dictionary around the required fields and throw exceptions with explicit messages?

Maybe can also stay as it is.

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

added a method to validate that the fields sender, receiver, chainID and gasLimit are not missing

receiver_username=self._bytes_from_b64(dictionary.get("receiverUsername", "")).decode(),
sender=dictionary["sender"],
sender_username=self._bytes_from_b64(dictionary.get("senderUsername", "")).decode(),
guardian=dictionary.get("guardian", None),
gas_price=dictionary.get("gasPrice", None),
gas_limit=dictionary["gasLimit"],
data=self._bytes_from_b64(dictionary.get("data", "")),
chain_id=dictionary["chainID"],
version=dictionary.get("version", None),
options=dictionary.get("options", None),
signature=self._bytes_from_hex(dictionary.get("signature", "")),
guardian_signature=self._bytes_from_hex(dictionary.get("guardianSignature", ""))
)

def _ensure_mandatory_fields_for_transaction(self, dictionary: Dict[str, Any]) -> None:
sender = dictionary.get("sender", None)
if sender is None:
raise MissingFieldError("The 'sender' key is missing from the dictionary")

receiver = dictionary.get("receiver", None)
if receiver is None:
raise MissingFieldError("The 'receiver' key is missing from the dictionary")

chain_id = dictionary.get("chainID", None)
if chain_id is None:
raise MissingFieldError("The 'chainID' key is missing from the dictionary")

gas_limit = dictionary.get("gasLimit", None)
if gas_limit is None:
raise MissingFieldError("The 'gasLimit' key is missing from the dictionary")

def _value_to_b64_or_empty(self, value: str | bytes) -> str:
value_as_bytes = value.encode() if isinstance(value, str) else value

if len(value):
return base64.b64encode(value_as_bytes).decode()
return ""

def _value_to_hex_or_empty(self, value: bytes) -> str:
if len(value):
return value.hex()
return ""

def _bytes_from_b64(self, value: str) -> bytes:
if len(value):
return base64.b64decode(value.encode())
return b""

def _bytes_from_hex(self, value: str) -> bytes:
if len(value):
return bytes.fromhex(value)
return b""
49 changes: 49 additions & 0 deletions multiversx_sdk/converters/transactions_converter_test.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
from multiversx_sdk.converters.transactions_converter import \
TransactionsConverter
from multiversx_sdk.core.transaction import Transaction


class TransactionMatcher:
def __init__(self, transaction: Transaction) -> None:
self.expected = transaction

def __eq__(self, actual: object) -> bool:
Copy link
Contributor

Choose a reason for hiding this comment

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

Interesting. Maybe we can also promote this as a core functionality (just a question)?

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

Well, I think this is more for testing purposes, but we can think about it.

if isinstance(actual, Transaction):
return self.expected.chain_id == actual.chain_id and \
self.expected.sender == actual.sender and \
self.expected.receiver == actual.receiver and \
self.expected.gas_limit == actual.gas_limit and \
self.expected.data == actual.data and \
self.expected.nonce == actual.nonce and \
self.expected.value == actual.value and \
self.expected.gas_price == actual.gas_price and \
self.expected.sender_username == actual.sender_username and \
self.expected.receiver_username == actual.receiver_username and \
self.expected.version == actual.version and \
self.expected.options == actual.options and \
self.expected.guardian == actual.guardian and \
self.expected.signature == actual.signature and \
self.expected.guardian_signature == actual.guardian_signature
return False


def test_transaction_converter():
converter = TransactionsConverter()

transaction = Transaction(
nonce=90,
value=123456789000000000000000000000,
sender="erd1qyu5wthldzr8wx5c9ucg8kjagg0jfs53s8nr3zpz3hypefsdd8ssycr6th",
receiver="erd1spyavw0956vq68xj8y4tenjpq2wd5a9p2c6j8gsz7ztyrnpxrruqzu66jx",
sender_username="alice",
receiver_username="bob",
gas_price=1000000000,
gas_limit=80000,
data=b"hello",
chain_id="localnet"
)

tx_as_dict = converter.transaction_to_dictionary(transaction)
restored_tx = converter.dictionary_to_transaction(tx_as_dict)

assert TransactionMatcher(transaction) == restored_tx
7 changes: 3 additions & 4 deletions multiversx_sdk/core/__init__.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,4 @@
from multiversx_sdk.core.account import AccountNonceHolder
from multiversx_sdk.core.adapters.query_runner_adapter import \
QueryRunnerAdapter
from multiversx_sdk.core.address import (Address, AddressComputer,
AddressFactory)
from multiversx_sdk.core.code_metadata import CodeMetadata
Expand All @@ -10,7 +8,8 @@
from multiversx_sdk.core.token_payment import TokenPayment
from multiversx_sdk.core.tokens import (Token, TokenComputer,
TokenIdentifierParts, TokenTransfer)
from multiversx_sdk.core.transaction import Transaction, TransactionComputer
from multiversx_sdk.core.transaction import Transaction
from multiversx_sdk.core.transaction_computer import TransactionComputer
from multiversx_sdk.core.transaction_payload import TransactionPayload
from multiversx_sdk.core.transactions_factories.account_transactions_factory import \
AccountTransactionsFactory
Expand Down Expand Up @@ -42,5 +41,5 @@
"DelegationTransactionsFactory", "TokenManagementTransactionsFactory",
"RegisterAndSetAllRolesTokenType", "TransactionsFactoryConfig",
"SmartContractTransactionsFactory", "TransferTransactionsFactory",
"RelayedTransactionsFactory", "AccountTransactionsFactory", "QueryRunnerAdapter"
"RelayedTransactionsFactory", "AccountTransactionsFactory"
]
4 changes: 0 additions & 4 deletions multiversx_sdk/core/adapters/__init__.py

This file was deleted.

3 changes: 2 additions & 1 deletion multiversx_sdk/core/proto/transaction_serializer_test.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
from multiversx_sdk.core.proto.transaction_serializer import ProtoSerializer
from multiversx_sdk.core.transaction import Transaction, TransactionComputer
from multiversx_sdk.core.transaction import Transaction
from multiversx_sdk.core.transaction_computer import TransactionComputer
from multiversx_sdk.testutils.wallets import load_wallets


Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,8 +2,7 @@

import pytest

from multiversx_sdk.core.adapters.query_runner_adapter import \
QueryRunnerAdapter
from multiversx_sdk.adapters.query_runner_adapter import QueryRunnerAdapter
from multiversx_sdk.core.smart_contract_queries_controller import \
SmartContractQueriesController
from multiversx_sdk.core.smart_contract_query import (
Expand Down
Loading
Loading