import json
from base64 import b64encode
from collections import OrderedDict
from hashlib import blake2b
from typing import Any
from Cryptodome.Hash import keccak
from multiversx_sdk.core.address import Address
from multiversx_sdk.core.constants import (
BECH32_ADDRESS_LENGTH, DIGEST_SIZE,
MIN_TRANSACTION_VERSION_THAT_SUPPORTS_OPTIONS,
TRANSACTION_OPTIONS_TX_GUARDED, TRANSACTION_OPTIONS_TX_HASH_SIGN)
from multiversx_sdk.core.errors import BadUsageError, NotEnoughGasError
from multiversx_sdk.core.interfaces import INetworkConfig
from multiversx_sdk.core.proto.transaction_serializer import ProtoSerializer
from multiversx_sdk.core.transaction import Transaction
[docs]
class TransactionComputer:
def __init__(self) -> None:
pass
[docs]
def compute_transaction_fee(self, transaction: Transaction, network_config: INetworkConfig) -> int:
"""`TransactionsFactoryConfig` can be used here as the `network_config`."""
move_balance_gas = network_config.min_gas_limit + len(transaction.data) * network_config.gas_per_data_byte
if move_balance_gas > transaction.gas_limit:
raise NotEnoughGasError(transaction.gas_limit)
fee_for_move = move_balance_gas * transaction.gas_price
if move_balance_gas == transaction.gas_limit:
return int(fee_for_move)
diff = transaction.gas_limit - move_balance_gas
modified_gas_price = transaction.gas_price * network_config.gas_price_modifier
processing_fee = diff * modified_gas_price
return int(fee_for_move + processing_fee)
[docs]
def compute_bytes_for_signing(self, transaction: Transaction) -> bytes:
self._ensure_fields(transaction)
dictionary = self._to_dictionary(transaction)
serialized = self._dict_to_json(dictionary)
return serialized
[docs]
def compute_bytes_for_verifying(self, transaction: Transaction) -> bytes:
is_signed_by_hash = self.has_options_set_for_hash_signing(transaction)
if is_signed_by_hash:
return self.compute_hash_for_signing(transaction)
return self.compute_bytes_for_signing(transaction)
[docs]
def compute_hash_for_signing(self, transaction: Transaction) -> bytes:
return keccak.new(digest_bits=256).update(self.compute_bytes_for_signing(transaction)).digest()
[docs]
def compute_transaction_hash(self, transaction: Transaction) -> bytes:
proto = ProtoSerializer()
serialized_tx = proto.serialize_transaction(transaction)
tx_hash = blake2b(serialized_tx, digest_size=DIGEST_SIZE).hexdigest()
return bytes.fromhex(tx_hash)
[docs]
def has_options_set_for_guarded_transaction(self, transaction: Transaction) -> bool:
return (transaction.options & TRANSACTION_OPTIONS_TX_GUARDED) == TRANSACTION_OPTIONS_TX_GUARDED
[docs]
def has_options_set_for_hash_signing(self, transaction: Transaction) -> bool:
return (transaction.options & TRANSACTION_OPTIONS_TX_HASH_SIGN) == TRANSACTION_OPTIONS_TX_HASH_SIGN
[docs]
def apply_guardian(self, transaction: Transaction, guardian: Address) -> None:
if transaction.version < MIN_TRANSACTION_VERSION_THAT_SUPPORTS_OPTIONS:
transaction.version = MIN_TRANSACTION_VERSION_THAT_SUPPORTS_OPTIONS
transaction.options = transaction.options | TRANSACTION_OPTIONS_TX_GUARDED
transaction.guardian = guardian
[docs]
def apply_options_for_hash_signing(self, transaction: Transaction) -> None:
if transaction.version < MIN_TRANSACTION_VERSION_THAT_SUPPORTS_OPTIONS:
transaction.version = MIN_TRANSACTION_VERSION_THAT_SUPPORTS_OPTIONS
transaction.options = transaction.options | TRANSACTION_OPTIONS_TX_HASH_SIGN
[docs]
def is_relayed_v3_transaction(self, transaction: Transaction) -> bool:
if transaction.relayer and not transaction.relayer.is_empty():
return True
return False
def _ensure_fields(self, transaction: Transaction) -> None:
if len(transaction.sender.to_bech32()) != BECH32_ADDRESS_LENGTH:
raise BadUsageError("Invalid `sender` field. Should be the bech32 address of the sender.")
if len(transaction.receiver.to_bech32()) != BECH32_ADDRESS_LENGTH:
raise BadUsageError("Invalid `receiver` field. Should be the bech32 address of the receiver.")
if not len(transaction.chain_id):
raise BadUsageError("The `chainID` field is not set")
if transaction.version < MIN_TRANSACTION_VERSION_THAT_SUPPORTS_OPTIONS:
if self.has_options_set_for_guarded_transaction(transaction) or self.has_options_set_for_hash_signing(transaction):
raise BadUsageError(f"Non-empty transaction options requires transaction version >= {MIN_TRANSACTION_VERSION_THAT_SUPPORTS_OPTIONS}")
def _to_dictionary(self, transaction: Transaction, with_signature: bool = False) -> dict[str, Any]:
"""Only used when serializing transaction for signing. Internal use only."""
dictionary: dict[str, Any] = OrderedDict()
dictionary["nonce"] = transaction.nonce
dictionary["value"] = str(transaction.value)
dictionary["receiver"] = transaction.receiver.to_bech32()
dictionary["sender"] = transaction.sender.to_bech32()
if transaction.sender_username:
dictionary["senderUsername"] = b64encode(transaction.sender_username.encode()).decode()
if transaction.receiver_username:
dictionary["receiverUsername"] = b64encode(transaction.receiver_username.encode()).decode()
dictionary["gasPrice"] = transaction.gas_price
dictionary["gasLimit"] = transaction.gas_limit
if transaction.data:
dictionary["data"] = b64encode(transaction.data).decode()
if with_signature:
if transaction.signature:
dictionary["signature"] = transaction.signature.hex()
dictionary["chainID"] = transaction.chain_id
if transaction.version:
dictionary["version"] = transaction.version
if transaction.options:
dictionary["options"] = transaction.options
if transaction.guardian:
dictionary["guardian"] = transaction.guardian.to_bech32()
if transaction.relayer:
dictionary["relayer"] = transaction.relayer.to_bech32()
return dictionary
def _dict_to_json(self, dictionary: dict[str, Any]) -> bytes:
return json.dumps(dictionary, separators=(',', ':')).encode("utf-8")