diff --git a/lightapi/python/symbollightapi/connector/BasicConnector.py b/lightapi/python/symbollightapi/connector/BasicConnector.py index 9f48c055ea..bdf736164f 100644 --- a/lightapi/python/symbollightapi/connector/BasicConnector.py +++ b/lightapi/python/symbollightapi/connector/BasicConnector.py @@ -51,9 +51,7 @@ async def post(self, url_path, request_payload, property_name=None, not_found_as Raises NodeException on connection or content failure. """ - return await self._dispatch('post', url_path, property_name, not_found_as_error, data=json.dumps(request_payload), headers={ - 'Content-Type': 'application/json' - }) + return await self._dispatch('post', url_path, property_name, not_found_as_error, json=request_payload) async def put(self, url_path, request_payload, property_name=None, not_found_as_error=True): """ @@ -61,6 +59,4 @@ async def put(self, url_path, request_payload, property_name=None, not_found_as_ Raises NodeException on connection or content failure. """ - return await self._dispatch('put', url_path, property_name, not_found_as_error, data=json.dumps(request_payload), headers={ - 'Content-Type': 'application/json' - }) + return await self._dispatch('put', url_path, property_name, not_found_as_error, json=request_payload) diff --git a/lightapi/python/symbollightapi/connector/NemBlockCalculator.py b/lightapi/python/symbollightapi/connector/NemBlockCalculator.py new file mode 100644 index 0000000000..801b115669 --- /dev/null +++ b/lightapi/python/symbollightapi/connector/NemBlockCalculator.py @@ -0,0 +1,273 @@ +from binascii import unhexlify + +from symbolchain.facade.NemFacade import NemFacade +from symbolchain.nc import MultisigAccountModificationType, TransactionType +from symbolchain.nem.Network import Network +from symbolchain.nem.TransactionFactory import TransactionFactory +from symbolchain.Network import NetworkLocator + +from ..model.Exceptions import NodeException + +BLOCK_TYPE_SIZE = 4 +VERSION_SIZE = 1 +RESERVED_PADDING_SIZE = 2 +NETWORK_SIZE = 1 +TIMESTAMP_SIZE = 4 +SIGNER_KEY_SIZE_FIELD = 4 +SIGNER_PUBLIC_KEY_SIZE = 32 +SIGNATURE_SIZE_FIELD = 4 +SIGNATURE_SIZE = 64 +PREVIOUS_BLOCK_HASH_OUTER_SIZE = 4 +PREVIOUS_BLOCK_HASH_SIZE = 4 +PREVIOUS_BLOCK_HASH_VALUE = 32 +HEIGHT_SIZE = 8 +TRANSACTION_COUNT_SIZE = 4 + + +class NemBlockCalculator: + """ + Handles NEM block size calculations and transaction building. + + Only populate variable-length fields for each descriptor, the SDK automatically fills in + and validates the fixed-width header fields. + """ + + def calculate_block_size(self, block_json): # pylint: disable=too-many-locals + """Calculates the serialized size of a NEM block.""" + + # Block structure components: + block_size = sum([ + BLOCK_TYPE_SIZE, + VERSION_SIZE, + RESERVED_PADDING_SIZE, + NETWORK_SIZE, + TIMESTAMP_SIZE, + SIGNER_KEY_SIZE_FIELD, + SIGNER_PUBLIC_KEY_SIZE, + SIGNATURE_SIZE_FIELD, + SIGNATURE_SIZE, + PREVIOUS_BLOCK_HASH_OUTER_SIZE, + PREVIOUS_BLOCK_HASH_SIZE, + PREVIOUS_BLOCK_HASH_VALUE, + HEIGHT_SIZE, + TRANSACTION_COUNT_SIZE + ]) + + transactions_size = sum( + self.calculate_transaction_size(tx_entry['tx']) + for tx_entry in block_json.get('txes', []) + ) + + return block_size + transactions_size + + def calculate_transaction_size(self, tx_json): + """Calculates the serialized size of a transaction.""" + + try: + transaction = self.build_transaction(tx_json) + return transaction.size + except Exception as exc: + raise NodeException(f'Failed to calculate transaction size for type {tx_json.get("type", "unknown")}: {exc}') from exc + + def build_transaction(self, tx_json): + """Builds a transaction object from JSON data.""" + + network_id = (tx_json['version'] >> 24) & 0xFF + network = NetworkLocator.find_by_identifier(Network.NETWORKS, network_id) + + facade = NemFacade(network) + + transaction_descriptor = self._build_transaction_descriptor(tx_json) + + return facade.transaction_factory.create(transaction_descriptor) + + def _build_transaction_descriptor(self, tx_json): + """Builds a transaction descriptor suitable for the facade factory.""" + + transaction_builders = { + TransactionType.TRANSFER.value: self._build_transfer_transaction, + TransactionType.ACCOUNT_KEY_LINK.value: self._build_account_key_link_transaction, + TransactionType.MULTISIG_ACCOUNT_MODIFICATION.value: self._build_multisig_account_modification_transaction, + TransactionType.NAMESPACE_REGISTRATION.value: self._build_namespace_registration_transaction, + TransactionType.MOSAIC_DEFINITION.value: self._build_mosaic_definition_transaction, + TransactionType.MOSAIC_SUPPLY_CHANGE.value: self._build_mosaic_supply_change_transaction, + TransactionType.MULTISIG.value: self._build_multisig_transaction, + TransactionType.MULTISIG_COSIGNATURE.value: self._build_cosignature_transaction + } + + builder_method = transaction_builders.get(tx_json['type']) + + if not builder_method: + raise NodeException(f'Unsupported transaction type {tx_json.get("type", "unknown")}') + + return builder_method(tx_json) + + def _lookup_transaction_name(self, tx_type_value, schema_version): # pylint: disable=no-self-use + """Resolves transaction names expected by the SDK factory.""" + + tx_type = TransactionType(tx_type_value) + transaction_name = TransactionFactory.lookup_transaction_name(tx_type, schema_version) + + return transaction_name + + def _build_transfer_transaction(self, tx_json): + """Builds a transfer transaction.""" + + schema_version = tx_json['version'] & 0xFF + + transaction = { + 'type': self._lookup_transaction_name(tx_json['type'], schema_version), + } + + message_json = tx_json.get('message') + if message_json and message_json.get('payload'): + transaction['message'] = { + 'message': unhexlify(message_json['payload']) + } + + mosaics = tx_json.get('mosaics') + if mosaics: + transaction['mosaics'] = [ + { + 'mosaic': { + 'mosaic_id': { + 'namespace_id': {'name': mosaic_json['mosaicId']['namespaceId']}, + 'name': mosaic_json['mosaicId']['name'] + } + } + } for mosaic_json in mosaics + ] + + return transaction + + def _build_account_key_link_transaction(self, tx_json): + """Builds an account key link transaction.""" + + schema_version = tx_json['version'] & 0xFF + + transaction = { + 'type': self._lookup_transaction_name(tx_json['type'], schema_version), + } + + return transaction + + def _build_multisig_account_modification_transaction(self, tx_json): + """Builds a multisig account modification transaction.""" + + schema_version = tx_json['version'] & 0xFF + + transaction = { + 'type': self._lookup_transaction_name(tx_json['type'], schema_version), + 'modifications': [ + { + 'modification': { + 'modification_type': MultisigAccountModificationType(modification['modificationType']).name.lower(), + 'cosignatory_public_key': modification['cosignatoryAccount'] + } + } for modification in tx_json.get('modifications', []) + ] + } + + if schema_version == 2: + transaction['min_approval_delta'] = tx_json['minCosignatories']['relativeChange'] + + return transaction + + def _build_namespace_registration_transaction(self, tx_json): + """Builds a namespace registration transaction.""" + + schema_version = tx_json['version'] & 0xFF + + transaction = { + 'type': self._lookup_transaction_name(tx_json['type'], schema_version), + 'name': tx_json['newPart'] + } + + if 'parent' in tx_json: + transaction['parent_name'] = tx_json['parent'] + + return transaction + + def _build_mosaic_definition_transaction(self, tx_json): + """Builds a mosaic definition transaction.""" + + schema_version = tx_json['version'] & 0xFF + mosaic_definition = tx_json['mosaicDefinition'] + + transaction = { + 'type': self._lookup_transaction_name(tx_json['type'], schema_version), + 'mosaic_definition': { + 'id': { + 'namespace_id': {'name': mosaic_definition['id']['namespaceId'].encode('utf8')}, + 'name': mosaic_definition['id']['name'].encode('utf8') + }, + 'description': mosaic_definition['description'].encode('utf8'), + 'properties': [ + { + 'property_': { + 'name': prop['name'].encode('utf8'), + 'value': prop['value'].encode('utf8') + }, + } for prop in mosaic_definition['properties'] + ] + } + } + + levy = mosaic_definition.get('levy') + if levy: + transaction['mosaic_definition']['levy'] = { + 'mosaic_id': { + 'namespace_id': {'name': levy['mosaicId']['namespaceId'].encode('utf8')}, + 'name': levy['mosaicId']['name'].encode('utf8') + }, + } + + return transaction + + def _build_mosaic_supply_change_transaction(self, tx_json): + """Builds a mosaic supply change transaction.""" + + schema_version = tx_json['version'] & 0xFF + + mosaic_id = tx_json['mosaicId'] + + transaction = { + 'type': self._lookup_transaction_name(tx_json['type'], schema_version), + 'mosaic_id': { + 'namespace_id': {'name': mosaic_id['namespaceId'].encode('utf8')}, + 'name': mosaic_id['name'].encode('utf8') + }, + } + + return transaction + + def _build_cosignature_transaction(self, tx_json): # pylint: disable=no-self-use + """Builds a cosignature transaction.""" + + schema_version = tx_json['version'] & 0xFF + + transaction = { + 'type': f'cosignature_v{schema_version}', + } + + return transaction + + def _build_multisig_transaction(self, tx_json): + """Builds a multisig transaction.""" + + schema_version = tx_json['version'] & 0xFF + + transaction = { + 'type': self._lookup_transaction_name(tx_json['type'], schema_version), + 'inner_transaction': TransactionFactory.to_non_verifiable_transaction( + self.build_transaction(tx_json['otherTrans']) + ), + 'cosignatures': [] + } + + for sig_json in tx_json.get('signatures', []): + cosignature_descriptor = self._build_transaction_descriptor(sig_json) + cosignature_descriptor.pop('type', None) + transaction['cosignatures'].append({'cosignature': cosignature_descriptor}) + + return transaction diff --git a/lightapi/python/symbollightapi/connector/NemConnector.py b/lightapi/python/symbollightapi/connector/NemConnector.py index 46834c7195..b19489f500 100644 --- a/lightapi/python/symbollightapi/connector/NemConnector.py +++ b/lightapi/python/symbollightapi/connector/NemConnector.py @@ -5,13 +5,17 @@ from aiolimiter import AsyncLimiter from symbolchain.CryptoTypes import Hash256, PublicKey, Signature from symbolchain.facade.NemFacade import NemFacade +from symbolchain.nc import TransactionType from symbolchain.nem.Network import Address, NetworkTimestamp +from ..model.Block import Block from ..model.Constants import DEFAULT_ASYNC_LIMITER_ARGUMENTS, TransactionStatus from ..model.Endpoint import Endpoint from ..model.Exceptions import NodeException from ..model.NodeInfo import NodeInfo +from ..model.Transaction import TransactionFactory, TransactionHandler from .BasicConnector import BasicConnector +from .NemBlockCalculator import NemBlockCalculator MosaicFeeInformation = namedtuple('MosaicFeeInformation', ['supply', 'divisibility']) @@ -295,3 +299,67 @@ async def try_wait_for_announced_transaction(self, transaction_hash, desired_sta return False # endregion + + # region POST (get_blocks_after, get_block) + + async def get_blocks_after(self, height): + """"Gets Blocks data""" + + blocks = await self.post('local/chain/blocks-after', {'height': height}) + + return [self._map_to_block(block) for block in blocks['data']] + + async def get_block(self, height): + """"Gets Block data""" + + block = await self.post('local/block/at', {'height': height}) + + return self._map_to_block(block) + + def _map_to_block(self, block_json): + block = block_json['block'] + nem_calculator = NemBlockCalculator() + size = nem_calculator.calculate_block_size(block_json) + + return Block( + block['height'], + block['timeStamp'], + [ + self._map_to_transaction(transaction, block['height']) + for transaction in block_json['txes'] + ], + block_json['difficulty'], + block_json['hash'], + block['signer'], + block['signature'], + size + ) + + @staticmethod + def _map_to_transaction(transaction, block_height): + """Maps a transaction to a object.""" + + tx_json = transaction['tx'] + tx_type = tx_json['type'] + + # Define common arguments for all transactions + common_args = { + 'transaction_hash': transaction['hash'], + 'height': block_height, + 'sender': tx_json['signer'], + 'fee': tx_json['fee'], + 'timestamp': tx_json['timeStamp'], + 'deadline': tx_json['deadline'], + 'signature': tx_json['signature'], + } + + specific_args = {} + + if TransactionType.MULTISIG.value == tx_type: + specific_args = TransactionHandler().map[tx_type](tx_json, transaction['innerHash']) + else: + specific_args = TransactionHandler().map[tx_type](tx_json) + + return TransactionFactory.create_transaction(tx_type, common_args, specific_args) + + # endregion diff --git a/lightapi/python/symbollightapi/model/Block.py b/lightapi/python/symbollightapi/model/Block.py new file mode 100644 index 0000000000..0c47fa49d8 --- /dev/null +++ b/lightapi/python/symbollightapi/model/Block.py @@ -0,0 +1,28 @@ +class Block: + """Block model.""" + + def __init__(self, height, timestamp, transactions, difficulty, block_hash, signer, signature, size): + """Create a Block model.""" + + # pylint: disable=too-many-arguments,too-many-positional-arguments + + self.height = height + self.timestamp = timestamp + self.transactions = transactions + self.difficulty = difficulty + self.block_hash = block_hash + self.signer = signer + self.signature = signature + self.size = size + + def __eq__(self, other): + return isinstance(other, Block) and all([ + self.height == other.height, + self.timestamp == other.timestamp, + self.transactions == other.transactions, + self.difficulty == other.difficulty, + self.block_hash == other.block_hash, + self.signer == other.signer, + self.signature == other.signature, + self.size == other.size + ]) diff --git a/lightapi/python/symbollightapi/model/Exceptions.py b/lightapi/python/symbollightapi/model/Exceptions.py index eb63bed05a..e89fa7033b 100644 --- a/lightapi/python/symbollightapi/model/Exceptions.py +++ b/lightapi/python/symbollightapi/model/Exceptions.py @@ -4,3 +4,7 @@ class NodeException(Exception): class CorruptDataException(NodeException): """Exception raised when corrupt data is received from a node.""" + + +class UnknownTransactionType(Exception): + """Exception raised when Unknown transaction type.""" diff --git a/lightapi/python/symbollightapi/model/Transaction.py b/lightapi/python/symbollightapi/model/Transaction.py new file mode 100644 index 0000000000..b392362057 --- /dev/null +++ b/lightapi/python/symbollightapi/model/Transaction.py @@ -0,0 +1,562 @@ +from collections import namedtuple + +from symbolchain.nc import TransactionType + +from ..model.Exceptions import UnknownTransactionType + +Message = namedtuple('Message', ['payload', 'is_plain']) +Mosaic = namedtuple('Mosaic', ['namespace_name', 'quantity']) +Modification = namedtuple('Modification', ['modification_type', 'cosignatory_account']) +MosaicLevy = namedtuple('MosaicLevy', ['fee', 'recipient', 'type', 'namespace_name']) +MosaicProperties = namedtuple('MosaicProperties', ['divisibility', 'initial_supply', 'supply_mutable', 'transferable']) + + +class Transaction: + def __init__( + self, + transaction_hash, + height, + sender, + fee, + timestamp, + deadline, + signature, + transaction_type + ): + """Create Transaction model.""" + + # pylint: disable=too-many-arguments,too-many-positional-arguments + + self.transaction_hash = transaction_hash + self.height = height + self.sender = sender + self.fee = fee + self.timestamp = timestamp + self.deadline = deadline + self.signature = signature + self.transaction_type = transaction_type + + def __eq__(self, other): + return isinstance(other, Transaction) and all([ + self.transaction_hash == other.transaction_hash, + self.height == other.height, + self.sender == other.sender, + self.fee == other.fee, + self.timestamp == other.timestamp, + self.deadline == other.deadline, + self.signature == other.signature, + self.transaction_type == other.transaction_type + ]) + + +class TransferTransaction(Transaction): + def __init__( + self, + transaction_hash, + height, + sender, + fee, + timestamp, + deadline, + signature, + amount, + recipient, + message, + mosaics + ): + """Create TransferTransaction model.""" + + # pylint: disable=too-many-arguments,too-many-positional-arguments + + super().__init__( + transaction_hash, + height, + sender, + fee, + timestamp, + deadline, + signature, + TransactionType.TRANSFER.value + ) + + self.amount = amount + self.recipient = recipient + self.message = message + self.mosaics = mosaics + + def __eq__(self, other): + return isinstance(other, TransferTransaction) and all([ + super().__eq__(other), + self.amount == other.amount, + self.recipient == other.recipient, + self.message == other.message, + self.mosaics == other.mosaics + ]) + + +class AccountKeyLinkTransaction(Transaction): + def __init__( + self, + transaction_hash, + height, + sender, + fee, + timestamp, + deadline, + signature, + mode, + remote_account + ): + """Create AccountKeyLinkTransaction model.""" + + # pylint: disable=too-many-arguments,too-many-positional-arguments + + super().__init__( + transaction_hash, + height, + sender, + fee, + timestamp, + deadline, + signature, + TransactionType.ACCOUNT_KEY_LINK.value + ) + + self.mode = mode + self.remote_account = remote_account + + def __eq__(self, other): + return isinstance(other, AccountKeyLinkTransaction) and all([ + super().__eq__(other), + self.mode == other.mode, + self.remote_account == other.remote_account + ]) + + +class MultisigAccountModificationTransaction(Transaction): + def __init__( + self, + transaction_hash, + height, + sender, + fee, + timestamp, + deadline, + signature, + min_cosignatories, + modifications + ): + """Create MultisigAccountModificationTransaction model.""" + + # pylint: disable=too-many-arguments,too-many-positional-arguments + + super().__init__( + transaction_hash, + height, + sender, + fee, + timestamp, + deadline, + signature, + TransactionType.MULTISIG_ACCOUNT_MODIFICATION.value + ) + + self.min_cosignatories = min_cosignatories + self.modifications = modifications + + def __eq__(self, other): + return isinstance(other, MultisigAccountModificationTransaction) and all([ + super().__eq__(other), + self.min_cosignatories == other.min_cosignatories, + self.modifications == other.modifications, + ]) + + +class MultisigTransaction(Transaction): + def __init__( + self, + transaction_hash, + height, + sender, + fee, + timestamp, + deadline, + signature, + signatures, + other_transaction, + inner_hash + ): + """Create MultisigTransaction model.""" + + # pylint: disable=too-many-arguments,too-many-positional-arguments + + super().__init__( + transaction_hash, + height, + sender, + fee, + timestamp, + deadline, + signature, + TransactionType.MULTISIG.value + ) + + self.signatures = signatures + self.other_transaction = other_transaction + self.inner_hash = inner_hash + + def __eq__(self, other): + return isinstance(other, MultisigTransaction) and all([ + super().__eq__(other), + self.signatures == other.signatures, + self.other_transaction == other.other_transaction, + self.inner_hash == other.inner_hash + ]) + + +class CosignSignatureTransaction(): + def __init__( + self, + timestamp, + other_hash, + other_account, + sender, + fee, + deadline, + signature + ): + """Create CosignSignatureTransaction model.""" + + # pylint: disable=too-many-arguments,too-many-positional-arguments + + self.transaction_type = TransactionType.MULTISIG_COSIGNATURE.value + self.timestamp = timestamp + self.other_hash = other_hash + self.other_account = other_account + self.sender = sender + self.fee = fee + self.deadline = deadline + self.signature = signature + + def __eq__(self, other): + return isinstance(other, CosignSignatureTransaction) and all([ + self.timestamp == other.timestamp, + self.other_hash == other.other_hash, + self.other_account == other.other_account, + self.sender == other.sender, + self.fee == other.fee, + self.deadline == other.deadline, + self.signature == other.signature + ]) + + +class NamespaceRegistrationTransaction(Transaction): + def __init__( + self, + transaction_hash, + height, + sender, + fee, + timestamp, + deadline, + signature, + rental_fee_sink, + rental_fee, + parent, + namespace + ): + """Create NamespaceRegistration model.""" + + # pylint: disable=too-many-arguments,too-many-positional-arguments + + super().__init__( + transaction_hash, + height, + sender, + fee, + timestamp, + deadline, + signature, + TransactionType.NAMESPACE_REGISTRATION.value + ) + + self.rental_fee_sink = rental_fee_sink + self.rental_fee = rental_fee + self.parent = parent + self.namespace = namespace + + def __eq__(self, other): + return isinstance(other, NamespaceRegistrationTransaction) and all([ + super().__eq__(other), + self.rental_fee_sink == other.rental_fee_sink, + self.rental_fee == other.rental_fee, + self.parent == other.parent, + self.namespace == other.namespace, + ]) + + +class MosaicDefinitionTransaction(Transaction): + def __init__( + self, + transaction_hash, + height, + sender, + fee, + timestamp, + deadline, + signature, + creation_fee, + creation_fee_sink, + creator, + description, + properties, + levy, + namespace_name + ): + """Create MosaicDefinitionTransaction model.""" + + # pylint: disable=too-many-arguments,too-many-positional-arguments + + super().__init__( + transaction_hash, + height, + sender, + fee, + timestamp, + deadline, + signature, + TransactionType.MOSAIC_DEFINITION.value + ) + + self.creation_fee = creation_fee + self.creation_fee_sink = creation_fee_sink + self.creator = creator + self.description = description + self.namespace_name = namespace_name + self.properties = properties + self.levy = levy + + def __eq__(self, other): + return isinstance(other, MosaicDefinitionTransaction) and all([ + super().__eq__(other), + self.creation_fee == other.creation_fee, + self.creation_fee_sink == other.creation_fee_sink, + self.creator == other.creator, + self.description == other.description, + self.namespace_name == other.namespace_name, + self.properties == other.properties, + self.levy == other.levy, + ]) + + +class MosaicSupplyChangeTransaction(Transaction): + def __init__( + self, + transaction_hash, + height, + sender, + fee, + timestamp, + deadline, + signature, + supply_type, + delta, + namespace_name + ): + """Create MosaicSupplyChangeTransaction model.""" + + # pylint: disable=too-many-arguments,too-many-positional-arguments + + super().__init__( + transaction_hash, + height, + sender, + fee, + timestamp, + deadline, + signature, + TransactionType.MOSAIC_SUPPLY_CHANGE.value + ) + + self.supply_type = supply_type + self.delta = delta + self.namespace_name = namespace_name + + def __eq__(self, other): + return isinstance(other, MosaicSupplyChangeTransaction) and all([ + super().__eq__(other), + self.supply_type == other.supply_type, + self.delta == other.delta, + self.namespace_name == other.namespace_name, + ]) + + +class TransactionHandler: + """Transaction handle mapper.""" + def __init__(self): + self.map = { + TransactionType.TRANSFER.value: self._map_transfer_args, + TransactionType.ACCOUNT_KEY_LINK.value: self._map_account_key_link_args, + TransactionType.MULTISIG_ACCOUNT_MODIFICATION.value: self._map_multisig_account_modification_args, + TransactionType.MULTISIG.value: self._map_multisig_transaction_args, + TransactionType.NAMESPACE_REGISTRATION.value: self._map_namespace_registration_args, + TransactionType.MOSAIC_DEFINITION.value: self._map_mosaic_definition_args, + TransactionType.MOSAIC_SUPPLY_CHANGE.value: self._map_mosaic_supply_change_args, + } + + @staticmethod + def _map_transfer_args(tx_json): + message = tx_json['message'] + + if message: + message = Message( + message['payload'], + message['type'] + ) + else: + message = None + + mosaics = None + if 'mosaics' in tx_json: + mosaics = [ + Mosaic( + f'{mosaic["mosaicId"]["namespaceId"]}.{mosaic["mosaicId"]["name"]}', + mosaic['quantity'] + ) + for mosaic in tx_json['mosaics'] + ] + + return { + 'amount': tx_json['amount'], + 'recipient': tx_json['recipient'], + 'message': message, + 'mosaics': mosaics, + } + + @staticmethod + def _map_account_key_link_args(tx_json): + return { + 'mode': tx_json['mode'], + 'remote_account': tx_json['remoteAccount'], + } + + @staticmethod + def _map_multisig_account_modification_args(tx_json): + return { + 'min_cosignatories': tx_json.get('minCosignatories', {}).get('relativeChange', 0), + 'modifications': [ + Modification( + modification['modificationType'], + modification['cosignatoryAccount']) + for modification in tx_json['modifications'] + ] + } + + def _map_multisig_transaction_args(self, tx_json, inner_hash): + + other_transaction = tx_json['otherTrans'] + + specific_args = self.map[other_transaction['type']](other_transaction) + + common_args = { + 'transaction_hash': None, + 'height': None, + 'sender': other_transaction['signer'], + 'fee': other_transaction['fee'], + 'timestamp': other_transaction['timeStamp'], + 'deadline': other_transaction['deadline'], + 'signature': None, + } + + return { + 'signatures': [ + CosignSignatureTransaction( + signature['timeStamp'], + signature['otherHash']['data'], + signature['otherAccount'], + signature['signer'], + signature['fee'], + signature['deadline'], + signature['signature'] + ) + for signature in tx_json['signatures'] + ], + 'other_transaction': TransactionFactory.create_transaction(other_transaction['type'], common_args, specific_args), + 'inner_hash': inner_hash, + } + + @staticmethod + def _map_namespace_registration_args(tx_json): + return { + 'rental_fee_sink': tx_json['rentalFeeSink'], + 'rental_fee': tx_json['rentalFee'], + 'parent': tx_json['parent'], + 'namespace': tx_json['newPart'], + } + + @staticmethod + def _map_mosaic_definition_args(tx_json): + mosaic_definition = tx_json['mosaicDefinition'] + mosaic_id = mosaic_definition['id'] + mosaic_levy = mosaic_definition['levy'] + mosaic_properties_json = { + item['name']: item['value'] + for item in mosaic_definition['properties'] + } + + mosaic_properties = MosaicProperties( + int(mosaic_properties_json['divisibility']), + int(mosaic_properties_json['initialSupply']), + mosaic_properties_json['supplyMutable'] != 'false', + mosaic_properties_json['transferable'] != 'false' + ) + + if mosaic_levy: + mosaic_levy = MosaicLevy( + mosaic_levy['fee'], + mosaic_levy['recipient'], + mosaic_levy['type'], + f'{mosaic_levy["mosaicId"]["namespaceId"]}.{mosaic_levy["mosaicId"]["name"] }' + ) + + return { + 'creation_fee': tx_json['creationFee'], + 'creation_fee_sink': tx_json['creationFeeSink'], + 'creator': mosaic_definition['creator'], + 'description': mosaic_definition['description'], + 'namespace_name': f'{mosaic_id["namespaceId"]}.{mosaic_id["name"] }', + 'properties': mosaic_properties, + 'levy': mosaic_levy, + } + + @staticmethod + def _map_mosaic_supply_change_args(tx_json): + mosaic_id = tx_json['mosaicId'] + return { + 'supply_type': tx_json['supplyType'], + 'delta': tx_json['delta'], + 'namespace_name': f'{mosaic_id["namespaceId"]}.{mosaic_id["name"] }', + } + + +class TransactionFactory: + """Create transaction models.""" + + @staticmethod + def create_transaction(tx_type, common_args, specific_args): + transaction_mapping = { + TransactionType.TRANSFER.value: TransferTransaction, + TransactionType.ACCOUNT_KEY_LINK.value: AccountKeyLinkTransaction, + TransactionType.MULTISIG_ACCOUNT_MODIFICATION.value: MultisigAccountModificationTransaction, + TransactionType.MULTISIG.value: MultisigTransaction, + TransactionType.NAMESPACE_REGISTRATION.value: NamespaceRegistrationTransaction, + TransactionType.MOSAIC_DEFINITION.value: MosaicDefinitionTransaction, + TransactionType.MOSAIC_SUPPLY_CHANGE.value: MosaicSupplyChangeTransaction, + } + + transaction_class = transaction_mapping.get(tx_type) + + if not transaction_class: + raise UnknownTransactionType(f'Unknown transaction type {tx_type}') + + return transaction_class(**common_args, **specific_args) diff --git a/lightapi/python/tests/connector/test_NemBlockCalculator.py b/lightapi/python/tests/connector/test_NemBlockCalculator.py new file mode 100644 index 0000000000..09c79585f3 --- /dev/null +++ b/lightapi/python/tests/connector/test_NemBlockCalculator.py @@ -0,0 +1,271 @@ +from symbollightapi.connector.NemBlockCalculator import NemBlockCalculator + +transaction_base = { + "timeStamp": 0, + "signature": + "b45ccd08d758e6c968b5c7fbe0127f2885f5ab53719f61bb0ad512ecd8db82a1ac50020ad47b58e55af7feb97f7dd084531af1ac0fa6019825d0568060a63d0d", + "fee": 0, + "deadline": 0, + "signer": "d8e06b38d4ce227fe735eb64bec55d6b9708cf91bcbcbe7e09f36ffd8b97763d" +} + +cosignature_transaction = { + **transaction_base, + "otherHash": { + "data": "5e29c5e77ec482afe2fc770a1d0d6a04205b75b854b8bbb9733e980f167f5eae" + }, + "otherAccount": "TBLGATVTVML66WWPSUN5RWKWEBKCPNYLQVWLTLFS", + "type": 4098, + "version": -1744830463 +} + +# pylint: disable=invalid-name + + +def _assert_transaction_size(tx_json, expected_size): + # Act: + nem_calculator = NemBlockCalculator() + transaction_size = nem_calculator.calculate_transaction_size(tx_json) + + # Assert: + assert expected_size == transaction_size + + +def test_can_calculate_transfer_transaction_v1(): + # Arrange: + tx_json = { + **transaction_base, + "amount": 100000, + "recipient": "TD3FGWIQR7GIOJSFG52JMCJYCVP2PC7ZNDZYDN4H", + "type": 257, + "message": {}, + "version": -1744830463, + } + + _assert_transaction_size(tx_json, 184) + + +def test_can_calculate_transfer_transaction_v2(): + # Arrange: + tx_json = { + **transaction_base, + "amount": 100000, + "recipient": "TDDRMIUIHSQIFPMIX4Z4FDBARSLVO5TASFV2UQSJ", + "mosaics": [ + { + "quantity": 199798819, + "mosaicId": { + "namespaceId": "testnam", + "name": "token" + } + } + ], + "type": 257, + "message": { + "payload": "313233", + "type": 1 + }, + "version": -1744830462, + } + + _assert_transaction_size(tx_json, 235) + + +def test_can_calculate_account_key_link_transaction_v1(): + # Arrange: + tx_json = { + **transaction_base, + "mode": 1, + "remoteAccount": "bb0e019d28df2d5241790c47a3ff99f39a1fc56017a1d291fb74fe6762d66aea", + "type": 2049, + "version": -1744830463, + } + + _assert_transaction_size(tx_json, 168) + + +def test_can_calculate_multisig_account_modification_transaction_v1(): + # Arrange: + tx_json = { + **transaction_base, + "modifications": [ + { + "modificationType": 1, + "cosignatoryAccount": "00112233445566778899AABBCCDDEEFF00112233445566778899AABBCCDDEEFF" + } + ], + "type": 4097, + "version": -1744830463, + } + + _assert_transaction_size(tx_json, 176) + + +def test_can_calculate_multisig_account_modification_transaction_v2(): + # Arrange: + tx_json = { + **transaction_base, + "minCosignatories": { + "relativeChange": 2 + }, + "modifications": [ + { + "modificationType": 1, + "cosignatoryAccount": "00112233445566778899AABBCCDDEEFF00112233445566778899AABBCCDDEEFF" + } + ], + "type": 4097, + "version": -1744830462, + } + + _assert_transaction_size(tx_json, 184) + + +def test_can_calculate_namespace_registration_with_root_transaction_v1(): + # Arrange: + tx_json = { + **transaction_base, + "parent": None, + "rentalFeeSink": "TD3FGWIQR7GIOJSFG52JMCJYCVP2PC7ZNDZYDN4H", + "rentalFee": 100000000, + "newPart": "root-namespace", + "type": 8193, + "version": -1744830463, + } + + _assert_transaction_size(tx_json, 202) + + +def test_can_calculate_namespace_registration_with_sub_namespace_transaction_v1(): + # Arrange: + tx_json = { + **transaction_base, + "parent": "root-namespace", + "rentalFeeSink": "TD3FGWIQR7GIOJSFG52JMCJYCVP2PC7ZNDZYDN4H", + "rentalFee": 100000000, + "newPart": "sub-namespace", + "type": 8193, + "version": -1744830463, + } + + _assert_transaction_size(tx_json, 215) + + +def test_can_calculate_mosaic_definition_without_properties_transaction_v1(): + # Arrange: + tx_json = { + **transaction_base, + "creationFeeSink": "TBMOSAICOD4F54EE5CDMR23CCBGOAM2XSJBR5OLC", + "creationFee": 100000000, + "mosaicDefinition": { + "creator": "55c9ad5388652e38a72a0f7792b6ee9091404680f864c908401734e98755c9b4", + "description": "test mosaic", + "id": { + "namespaceId": "test_namespace_1", + "name": "test_mosaic_2" + }, + "properties": [], + "levy": {} + }, + "type": 16385, + "version": -1744830463, + } + + _assert_transaction_size(tx_json, 284) + + +def test_can_calculate_mosaic_definition_transaction_v1(): + # Arrange: + tx_json = { + **transaction_base, + "creationFeeSink": "TBMOSAICOD4F54EE5CDMR23CCBGOAM2XSJBR5OLC", + "creationFee": 100000000, + "mosaicDefinition": { + "creator": "55c9ad5388652e38a72a0f7792b6ee9091404680f864c908401734e98755c9b4", + "description": "test mosaic", + "id": { + "namespaceId": "test_namespace_1", + "name": "test_mosaic_1" + }, + "properties": [ + { + "name": "divisibility", + "value": "6" + }, + { + "name": "initialSupply", + "value": "1000" + }, + { + "name": "supplyMutable", + "value": "true" + }, + { + "name": "transferable", + "value": "true" + } + ], + "levy": { + "type": 1, + "recipient": "TD3RXTHBLK6J3UD2BH2PXSOFLPWZOTR34WCG4HXH", + "mosaicId": { + "namespaceId": "nem", + "name": "xem" + }, + "fee": 1000 + } + }, + "type": 16385, + "version": -1744830463, + } + + _assert_transaction_size(tx_json, 469) + + +def test_can_calculate_mosaic_supply_change_transaction_v1(): + # Arrange: + tx_json = { + **transaction_base, + "mosaicId": { + "namespaceId": "test_namespace_1", + "name": "test_mosaic_1" + }, + "supplyType": 1, + "delta": 500, + "type": 16386, + "version": -1744830463, + } + + _assert_transaction_size(tx_json, 181) + + +def test_can_calculate_cosignature_transaction_v1(): + _assert_transaction_size(cosignature_transaction, 212) + + +def test_can_calculate_multisig_transaction_v1(): + # Arrange: + inner_tx_json = { + "timeStamp": 314820082, + "amount": 70000, + "fee": 50000, + "recipient": "TDPGL3LIAS5OPBMEJN63YRWS35HRQC6OC2523L5G", + "mosaics": [], + "type": 257, + "deadline": 314821342, + "message": {}, + "version": -1744830462, + "signer": "0f83292a6f9b7882915df4f64f11bd20161eab4a56fb051678884e2400005df8" + } + + tx_json = { + **transaction_base, + "otherTrans": inner_tx_json, + "signatures": [ + cosignature_transaction + ], + "type": 4100, + "version": -1744830463, + } + + _assert_transaction_size(tx_json, 472) diff --git a/lightapi/python/tests/connector/test_NemConnector.py b/lightapi/python/tests/connector/test_NemConnector.py index d618078f02..407e4f494f 100644 --- a/lightapi/python/tests/connector/test_NemConnector.py +++ b/lightapi/python/tests/connector/test_NemConnector.py @@ -1,3 +1,4 @@ +# pylint: disable=too-many-lines import json from binascii import unhexlify @@ -9,10 +10,21 @@ from symbolchain.nem.Network import Address, Network from symbollightapi.connector.NemConnector import NemConnector +from symbollightapi.model.Block import Block from symbollightapi.model.Constants import TimeoutSettings, TransactionStatus from symbollightapi.model.Endpoint import Endpoint from symbollightapi.model.Exceptions import NodeException from symbollightapi.model.NodeInfo import NodeInfo +from symbollightapi.model.Transaction import ( + AccountKeyLinkTransaction, + CosignSignatureTransaction, + MosaicDefinitionTransaction, + MosaicSupplyChangeTransaction, + MultisigAccountModificationTransaction, + MultisigTransaction, + NamespaceRegistrationTransaction, + TransferTransaction +) from ..test.LightApiTestUtils import HASHES, NEM_ADDRESSES, PUBLIC_KEYS @@ -173,6 +185,253 @@ } +CHAIN_BLOCK_1 = { # Included all type of transaction in the block + 'difficulty': 100000000000000, + 'txes': [ + { + 'tx': { + 'timeStamp': 73397, + 'mode': 1, + 'signature': ( + '1b81379847241e45da86b27911e5c9a9192ec04f644d98019657d32838b49c14' + '3eaa4815a3028b80f9affdbf0b94cd620f7a925e02783dda67b8627b69ddf70e' + ), + 'fee': 8000000, + 'remoteAccount': '7195f4d7a40ad7e31958ae96c4afed002962229675a4cae8dc8a18e290618981', + 'type': 2049, + 'deadline': 83397, + 'version': 1744830465, + 'signer': '22df5f43ee3739a10c346b3ec2d3878668c5514696be425f9067d3a11c777f1d' + }, + 'hash': '306f20260a1b7af692834809d3e7d53edd41616d5076ac0fac6cfa75982185df' + }, + { + 'tx': { + 'timeStamp': 73397, + 'amount': 180000040000000, + 'signature': ( + 'e0cc7f71e353ca0aaf2f009d74aeac5f97d4796b0f08c009058fb33d93c2e8ca' + '68c0b63e46ff125f43314014d324ac032d2c82996a6e47068b251f1d71fdd001' + ), + 'fee': 9000000, + 'recipient': 'NCOPERAWEWCD4A34NP5UQCCKEX44MW4SL3QYJYS5', + 'type': 257, + 'deadline': 83397, + 'message': { + 'payload': '476f6f64206c75636b21', + 'type': 1 + }, + 'version': 1744830465, + 'signer': '8d07f90fb4bbe7715fa327c926770166a11be2e494a970605f2e12557f66c9b9' + }, + 'hash': 'd6c9902cfa23dbbdd212d720f86391dd91d215bf77d806f03a6c2dd2e730628a' + }, + { + 'tx': { + 'timeStamp': 73397, + 'signature': ( + '81ff2235f9ad6f3f8adbc16051bf8691a45ee5ddcace4d6260ce9a2ae63dba59' + '4f2b486f25451a1f90da7f0e312d9e8570e4bc03798e58d19dec86feb4152307' + ), + 'fee': 40000000, + 'minCosignatories': { + 'relativeChange': 2 + }, + 'type': 4097, + 'deadline': 83397, + 'version': 1744830465, + 'signer': 'f41b99320549741c5cce42d9e4bb836d98c50ed5415d0c3c2912d1bb50e6a0e5', + 'modifications': [ + { + 'modificationType': 1, + 'cosignatoryAccount': '1fbdbdde28daf828245e4533765726f0b7790e0b7146e2ce205df3e86366980b' + }, + { + 'modificationType': 1, + 'cosignatoryAccount': 'f94e8702eb1943b23570b1b83be1b81536df35538978820e98bfce8f999e2d37' + } + ] + }, + 'hash': 'cc64ca69bfa95db2ff7ac1e21fe6d27ece189c603200ebc9778d8bb80ca25c3c' + }, + { + 'tx': { + 'timeStamp': 73397, + 'parent': None, + 'signature': ( + '9fc70720d0333d7d8f9eb14ef45ce45a846d37e79cf7a4244b4db36dcb0d3dfe' + '0170daefbf4d30f92f343110a6f03a14aedcf7913e465a4a1cc199639169410a' + ), + 'fee': 150000, + 'rentalFeeSink': 'NAMESPACEWH4MKFMBCVFERDPOOP4FK7MTBXDPZZA', + 'rentalFee': 100000000, + 'newPart': 'namespace', + 'type': 8193, + 'deadline': 83397, + 'version': 1744830465, + 'signer': 'a700809530e5428066807ec0d34859c52e260fc60634aaac13e3972dcfc08736' + }, + 'hash': '7e547e45cfc9c34809ce184db6ae7b028360c0f1492cc37b7b4d31c22af07dc3' + }, + { + 'tx': { + 'timeStamp': 73397, + 'creationFee': 10000000, + 'mosaicDefinition': { + 'creator': 'a700809530e5428066807ec0d34859c52e260fc60634aaac13e3972dcfc08736', + 'description': 'NEM namespace test', + 'id': { + 'namespaceId': 'namespace', + 'name': 'test' + }, + 'properties': [ + { + 'name': 'divisibility', + 'value': '4' + }, + { + 'name': 'initialSupply', + 'value': '3100000' + }, + { + 'name': 'supplyMutable', + 'value': 'false' + }, + { + 'name': 'transferable', + 'value': 'true' + } + ], + 'levy': { + 'fee': 500, + 'recipient': 'NBRYCNWZINEVNITUESKUMFIENWKYCRUGNFZV25AV', + 'type': 1, + 'mosaicId': { + 'namespaceId': 'nem', + 'name': 'xem' + } + } + }, + 'signature': ( + 'a80ccd44955ded7d35ee3aa011bfafd3f30cc746f63cb59a9d02171f908a0f4a' + '0294fcbba0b2838acd184daf1d9ae3c0f645308b442547156364192cd3d2d605' + ), + 'fee': 150000, + 'creationFeeSink': 'NBMOSAICOD4F54EE5CDMR23CCBGOAM2XSIUX6TRS', + 'type': 16385, + 'deadline': 83397, + 'version': 1744830465, + 'signer': 'a700809530e5428066807ec0d34859c52e260fc60634aaac13e3972dcfc08736' + }, + 'hash': '4725e523e5d5a562121f38953d6da3ae695060533fc0c5634b31de29c3b766e1' + }, + { + 'tx': { + 'timeStamp': 73397, + 'signature': ( + '7fef5a89a1c6c98347b8d488a8dd28902e8422680f917c28f3ef0100d394b91c' + 'd85f7cdfd7bdcd6f0cb8089ae9d4e6ef24a8caca35d1cfec7e33c9ccab5e1503' + ), + 'fee': 150000, + 'supplyType': 2, + 'delta': 500000, + 'type': 16386, + 'deadline': 83397, + 'mosaicId': { + 'namespaceId': 'namespace', + 'name': 'test' + }, + 'version': 1744830465, + 'signer': 'da04b4a1d64add6c70958d383f9d247af1aaa957cb89f15b2d059b278e0594d5' + }, + 'hash': 'cb805b4499479135934e70452d12ad9ecc26c46a111fe0cdda8e09741d257708' + }, + { + 'tx': { + 'timeStamp': 73397, + 'signature': ( + '0e7112b029e030d2d1c7dff79c88a29812f7254422d80e37a7aac5228fff5706' + '133500b0119a1327cab8787416b5873cc873e3181066c46cb2b108c5da10d90f' + ), + 'fee': 500000, + 'type': 4100, + 'deadline': 83397, + 'version': 1744830465, + 'signatures': [ + { + 'timeStamp': 261593985, + 'otherHash': { + 'data': 'edcc8d1c48165f5b771087fbe3c4b4d41f5f8f6c4ce715e050b86fb4e7fdeb64' + }, + 'otherAccount': 'NAGJG3QFWYZ37LMI7IQPSGQNYADGSJZGJRD2DIYA', + 'signature': ( + '249bc2dbad96e827eabc991b59dff7f12cc27f3e0da8ab3db6a3201169431786' + '72f712ba14ed7a3b890e161357a163e7408aa22e1d6d1382ebada57973862706' + ), + 'fee': 500000, + 'type': 4098, + 'deadline': 261680385, + 'version': 1744830465, + 'signer': 'ae6754c70b7e3ba0c51617c8f9efd462d0bf680d45e09c3444e817643d277826' + } + ], + 'signer': 'aa455d831430872feb0c6ae14265209182546c985a321c501be7fdc96ed04757', + 'otherTrans': { + 'timeStamp': 73397, + 'amount': 150000000000, + 'fee': 750000, + 'recipient': 'NBUH72UCGBIB64VYTAAJ7QITJ62BLISFFQOHVP65', + 'type': 257, + 'deadline': 83397, + 'message': {}, + 'version': 1744830465, + 'signer': 'fbae41931de6a0cc25153781321f3de0806c7ba9a191474bb9a838118c8de4d3' + } + }, + 'innerHash': 'edcc8d1c48165f5b771087fbe3c4b4d41f5f8f6c4ce715e050b86fb4e7fdeb64', + 'hash': '3375969dbc2aaae1cad0d89854d4f41b4fef553dbe9c7d39bdf72e3c538f98fe' + } + ], + 'block': { + 'timeStamp': 73976, + 'signature': ( + 'fdf6a9830e9320af79123f467fcb03d6beab735575ff50eab363d812c5581436' + '2ad7be0503db2ee70e60ac3408d83cdbcbd941067a6df703e0c21c7bf389f105' + ), + 'prevBlockHash': { + 'data': '438cf6375dab5a0d32f9b7bf151d4539e00a590f7c022d5572c7d41815a24be4' + }, + 'type': 1, + 'transactions': [], + 'version': 1744830465, + 'signer': 'f9bd190dd0c364261f5c8a74870cc7f7374e631352293c62ecc437657e5de2cd', + 'height': 2 + }, + 'hash': '1dd9d4d7b6af603d29c082f9aa4e123f07d18154ddbcd7ddc6702491b854c5e4' +} + +CHAIN_BLOCK_2 = { + 'difficulty': 90250000000000, + 'txes': [], + 'block': { + 'timeStamp': 78976, + 'signature': ( + '919ae66a34119b49812b335827b357f86884ab08b628029fd6e8db3572faeb4f' + '323a7bf9488c76ef8faa5b513036bbcce2d949ba3e41086d95a54c0007403c0b' + ), + 'prevBlockHash': { + 'data': '1dd9d4d7b6af603d29c082f9aa4e123f07d18154ddbcd7ddc6702491b854c5e4' + }, + 'type': 1, + 'transactions': [], + 'version': 1744830465, + 'signer': '45c1553fb1be7f25b6f79278b9ede1129bb9163f3b85883ea90f1c66f497e68b', + 'height': 3 + }, + 'hash': '9708256e8a8dfb76eed41dcfa2e47f4af520b7b3286afb7f60dca02851f8a53e' +} + + # endregion @@ -316,6 +575,12 @@ async def _process(self, request, response_body, status_code=200): self.urls.append(str(request.url)) return web.Response(body=json.dumps(response_body), headers={'Content-Type': 'application/json'}, status=status_code) + async def local_chain_blocks_after(self, request): + return await self._process(request, {'data': [CHAIN_BLOCK_1, CHAIN_BLOCK_2]}) + + async def local_block_at(self, request): + return await self._process(request, CHAIN_BLOCK_1) + # create a mock server mock_server = MockNemServer() @@ -334,6 +599,8 @@ async def _process(self, request, response_body, status_code=200): app.router.add_get('/account/transfers/incoming', mock_server.transfers) app.router.add_post('/block/at/public', mock_server.block_at) app.router.add_post('/transaction/announce', mock_server.announce_transaction) + app.router.add_post('/local/chain/blocks-after', mock_server.local_chain_blocks_after) + app.router.add_post('/local/block/at', mock_server.local_block_at) server = await aiohttp_client(app) # pylint: disable=redefined-outer-name server.mock = mock_server @@ -839,3 +1106,188 @@ async def test_can_try_wait_for_announced_transaction_confirmed_timeout(server): assert not result # endregion + + +# region POST (get_blocks_after) + +EXPECTED_BLOCK_2 = Block( + 2, + 73976, + [ + AccountKeyLinkTransaction( + '306f20260a1b7af692834809d3e7d53edd41616d5076ac0fac6cfa75982185df', + 2, + '22df5f43ee3739a10c346b3ec2d3878668c5514696be425f9067d3a11c777f1d', + 8000000, + 73397, + 83397, + '1b81379847241e45da86b27911e5c9a9192ec04f644d98019657d32838b49c14' + '3eaa4815a3028b80f9affdbf0b94cd620f7a925e02783dda67b8627b69ddf70e', + 1, + '7195f4d7a40ad7e31958ae96c4afed002962229675a4cae8dc8a18e290618981' + ), + TransferTransaction( + 'd6c9902cfa23dbbdd212d720f86391dd91d215bf77d806f03a6c2dd2e730628a', + 2, + '8d07f90fb4bbe7715fa327c926770166a11be2e494a970605f2e12557f66c9b9', + 9000000, + 73397, + 83397, + 'e0cc7f71e353ca0aaf2f009d74aeac5f97d4796b0f08c009058fb33d93c2e8ca' + '68c0b63e46ff125f43314014d324ac032d2c82996a6e47068b251f1d71fdd001', + 180000040000000, + 'NCOPERAWEWCD4A34NP5UQCCKEX44MW4SL3QYJYS5', + ('476f6f64206c75636b21', 1), + None + ), + MultisigAccountModificationTransaction( + 'cc64ca69bfa95db2ff7ac1e21fe6d27ece189c603200ebc9778d8bb80ca25c3c', + 2, + 'f41b99320549741c5cce42d9e4bb836d98c50ed5415d0c3c2912d1bb50e6a0e5', + 40000000, + 73397, + 83397, + '81ff2235f9ad6f3f8adbc16051bf8691a45ee5ddcace4d6260ce9a2ae63dba59' + '4f2b486f25451a1f90da7f0e312d9e8570e4bc03798e58d19dec86feb4152307', + 2, + [ + (1, '1fbdbdde28daf828245e4533765726f0b7790e0b7146e2ce205df3e86366980b'), + (1, 'f94e8702eb1943b23570b1b83be1b81536df35538978820e98bfce8f999e2d37') + ] + ), + NamespaceRegistrationTransaction( + '7e547e45cfc9c34809ce184db6ae7b028360c0f1492cc37b7b4d31c22af07dc3', + 2, + 'a700809530e5428066807ec0d34859c52e260fc60634aaac13e3972dcfc08736', + 150000, + 73397, + 83397, + '9fc70720d0333d7d8f9eb14ef45ce45a846d37e79cf7a4244b4db36dcb0d3dfe' + '0170daefbf4d30f92f343110a6f03a14aedcf7913e465a4a1cc199639169410a', + 'NAMESPACEWH4MKFMBCVFERDPOOP4FK7MTBXDPZZA', + 100000000, + None, + 'namespace' + ), + MosaicDefinitionTransaction( + '4725e523e5d5a562121f38953d6da3ae695060533fc0c5634b31de29c3b766e1', + 2, + 'a700809530e5428066807ec0d34859c52e260fc60634aaac13e3972dcfc08736', + 150000, + 73397, + 83397, + 'a80ccd44955ded7d35ee3aa011bfafd3f30cc746f63cb59a9d02171f908a0f4a' + '0294fcbba0b2838acd184daf1d9ae3c0f645308b442547156364192cd3d2d605', + 10000000, + 'NBMOSAICOD4F54EE5CDMR23CCBGOAM2XSIUX6TRS', + 'a700809530e5428066807ec0d34859c52e260fc60634aaac13e3972dcfc08736', + 'NEM namespace test', + (4, 3100000, False, True), + (500, 'NBRYCNWZINEVNITUESKUMFIENWKYCRUGNFZV25AV', 1, 'nem.xem'), + 'namespace.test' + ), + MosaicSupplyChangeTransaction( + 'cb805b4499479135934e70452d12ad9ecc26c46a111fe0cdda8e09741d257708', + 2, + 'da04b4a1d64add6c70958d383f9d247af1aaa957cb89f15b2d059b278e0594d5', + 150000, + 73397, + 83397, + '7fef5a89a1c6c98347b8d488a8dd28902e8422680f917c28f3ef0100d394b91c' + 'd85f7cdfd7bdcd6f0cb8089ae9d4e6ef24a8caca35d1cfec7e33c9ccab5e1503', + 2, + 500000, + 'namespace.test' + ), + MultisigTransaction( + '3375969dbc2aaae1cad0d89854d4f41b4fef553dbe9c7d39bdf72e3c538f98fe', + 2, + 'aa455d831430872feb0c6ae14265209182546c985a321c501be7fdc96ed04757', + 500000, + 73397, + 83397, + '0e7112b029e030d2d1c7dff79c88a29812f7254422d80e37a7aac5228fff5706' + '133500b0119a1327cab8787416b5873cc873e3181066c46cb2b108c5da10d90f', + [ + CosignSignatureTransaction( + 261593985, + 'edcc8d1c48165f5b771087fbe3c4b4d41f5f8f6c4ce715e050b86fb4e7fdeb64', + 'NAGJG3QFWYZ37LMI7IQPSGQNYADGSJZGJRD2DIYA', + 'ae6754c70b7e3ba0c51617c8f9efd462d0bf680d45e09c3444e817643d277826', + 500000, + 261680385, + '249bc2dbad96e827eabc991b59dff7f12cc27f3e0da8ab3db6a3201169431786' + '72f712ba14ed7a3b890e161357a163e7408aa22e1d6d1382ebada57973862706' + ) + ], + TransferTransaction( + None, + None, + 'fbae41931de6a0cc25153781321f3de0806c7ba9a191474bb9a838118c8de4d3', + 750000, + 73397, + 83397, + None, + 150000000000, + 'NBUH72UCGBIB64VYTAAJ7QITJ62BLISFFQOHVP65', + None, + None + ), + 'edcc8d1c48165f5b771087fbe3c4b4d41f5f8f6c4ce715e050b86fb4e7fdeb64' + ) + ], + 100000000000000, + '1dd9d4d7b6af603d29c082f9aa4e123f07d18154ddbcd7ddc6702491b854c5e4', + 'f9bd190dd0c364261f5c8a74870cc7f7374e631352293c62ecc437657e5de2cd', + ( + 'fdf6a9830e9320af79123f467fcb03d6beab735575ff50eab363d812c5581436' + '2ad7be0503db2ee70e60ac3408d83cdbcbd941067a6df703e0c21c7bf389f105' + ), + 2052 +) + + +async def test_can_query_blocks_after(server): # pylint: disable=redefined-outer-name + # Arrange: + connector = NemConnector(server.make_url('')) + + # Act: + blocks = await connector.get_blocks_after(1) + + # Assert: + assert [ + f'{server.make_url("")}/local/chain/blocks-after', + ] == server.mock.urls + assert 2 == len(blocks) + assert EXPECTED_BLOCK_2 == blocks[0] + assert Block( + 3, + 78976, + [], + 90250000000000, + '9708256e8a8dfb76eed41dcfa2e47f4af520b7b3286afb7f60dca02851f8a53e', + '45c1553fb1be7f25b6f79278b9ede1129bb9163f3b85883ea90f1c66f497e68b', + ( + '919ae66a34119b49812b335827b357f86884ab08b628029fd6e8db3572faeb4f' + '323a7bf9488c76ef8faa5b513036bbcce2d949ba3e41086d95a54c0007403c0b' + ), + 168 + ) == blocks[1] + +# endregion + + +# region POST (get_block) + +async def test_can_query_block_at(server): # pylint: disable=redefined-outer-name + # Arrange: + connector = NemConnector(server.make_url('')) + + # Act: + block = await connector.get_block(2) + + # Assert: + assert [f'{server.make_url("")}/local/block/at'] == server.mock.urls + assert EXPECTED_BLOCK_2 == block + +# endregion diff --git a/lightapi/python/tests/model/test_Block.py b/lightapi/python/tests/model/test_Block.py new file mode 100644 index 0000000000..9d78570c7e --- /dev/null +++ b/lightapi/python/tests/model/test_Block.py @@ -0,0 +1,61 @@ +import unittest + +from symbollightapi.model.Block import Block + + +class BlockTest(unittest.TestCase): + @staticmethod + def _create_default_block(override=None): + block = Block( + 10, + 21447079270401, + [], + 90000000000000, + 'a785cac7259bdd4cf423fd1079cbe0e24e119958a8075f302f9e17a1c407abe0', + '7e6d6a11c4a79f6eb1f0e3489fd683a9381c8e1bef6bcaedbbc9f03c70b65a57', + ( + 'a4bbf324a3480f58c2d15bdb15d0232da94db9519d5b727a3ea12c11cc11d368' + 'e0037c08e1994bc07adc4f790bcb09c1d727066b0308463e406e175572c4150a' + ), + 888 + ) + + if override: + setattr(block, override[0], override[1]) + + return block + + def test_can_create_block(self): + # Act: + block = self._create_default_block() + + # Assert: + self.assertEqual(10, block.height) + self.assertEqual(21447079270401, block.timestamp) + self.assertEqual([], block.transactions) + self.assertEqual(90000000000000, block.difficulty) + self.assertEqual('a785cac7259bdd4cf423fd1079cbe0e24e119958a8075f302f9e17a1c407abe0', block.block_hash) + self.assertEqual('7e6d6a11c4a79f6eb1f0e3489fd683a9381c8e1bef6bcaedbbc9f03c70b65a57', block.signer) + self.assertEqual( + ( + 'a4bbf324a3480f58c2d15bdb15d0232da94db9519d5b727a3ea12c11cc11d368' + 'e0037c08e1994bc07adc4f790bcb09c1d727066b0308463e406e175572c4150a' + ), block.signature) + self.assertEqual(888, block.size) + + def test_eq_is_supported(self): + # Arrange: + block = self._create_default_block() + + # Act + Assert: + self.assertEqual(block, self._create_default_block()) + self.assertNotEqual(block, None) + self.assertNotEqual(block, 17) + self.assertNotEqual(block, self._create_default_block(('height', 3))) + self.assertNotEqual(block, self._create_default_block(('timestamp', 73977))) + self.assertNotEqual(block, self._create_default_block(('transactions', [1, 2, 3]))) + self.assertNotEqual(block, self._create_default_block(('difficulty', 10000))) + self.assertNotEqual(block, self._create_default_block(('block_hash', 'invalid hash'))) + self.assertNotEqual(block, self._create_default_block(('signer', 'invalid signer'))) + self.assertNotEqual(block, self._create_default_block(('signature', 'invalid signature'))) + self.assertNotEqual(block, self._create_default_block(('size', 123))) diff --git a/lightapi/python/tests/model/test_Transaction.py b/lightapi/python/tests/model/test_Transaction.py new file mode 100644 index 0000000000..caebf2fe06 --- /dev/null +++ b/lightapi/python/tests/model/test_Transaction.py @@ -0,0 +1,401 @@ +import unittest + +from symbolchain.nc import TransactionType + +from symbollightapi.model.Exceptions import UnknownTransactionType +from symbollightapi.model.Transaction import ( + AccountKeyLinkTransaction, + CosignSignatureTransaction, + Message, + Modification, + Mosaic, + MosaicDefinitionTransaction, + MosaicLevy, + MosaicProperties, + MosaicSupplyChangeTransaction, + MultisigAccountModificationTransaction, + MultisigTransaction, + NamespaceRegistrationTransaction, + TransactionFactory, + TransferTransaction +) + +COMMON_ARGS = { + 'transaction_hash': '306f20260a1b7af692834809d3e7d53edd41616d5076ac0fac6cfa75982185df', + 'height': 10, + 'sender': '22df5f43ee3739a10c346b3ec2d3878668c5514696be425f9067d3a11c777f1d', + 'fee': 8000000, + 'timestamp': 73397, + 'deadline': 83397, + 'signature': + '1b81379847241e45da86b27911e5c9a9192ec04f644d98019657d32838b49c14' + '3eaa4815a3028b80f9affdbf0b94cd620f7a925e02783dda67b8627b69ddf70e', +} + +TRANSFER_TRANSACTION_ARGS = { + 'recipient': 'TCWZ6H3Y6K6G4FLH2YF2JBK2ZJU2Z4K4JZ3W5KQ', + 'amount': 1000000, + 'message': Message('test', 1), + 'mosaics': [Mosaic('nem.xem', 1000000)] +} + +ACCOUNT_KEY_LINK_TRANSACTION_ARGS = { + 'mode': 1, + 'remote_account': '22df5f43ee3739a10c346b3ec2d3878668c5514696be425f9067d3a11c777f1d', +} + +MULTISIG_ACCOUNT_MODIFICATION_TRANSACTION_ARGS = { + 'min_cosignatories': 1, + 'modifications': [ + Modification(1, '22df5f43ee3739a10c346b3ec2d3878668c5514696be425f9067d3a11c777f1d') + ] +} + +MULTISIG_TRANSACTION_ARGS = { + 'signatures': [ + CosignSignatureTransaction( + 261593985, + 'edcc8d1c48165f5b771087fbe3c4b4d41f5f8f6c4ce715e050b86fb4e7fdeb64', + 'TALIC367CZIV55GIQT35HDZAZ53CN3VPB3G55BMU', + '22df5f43ee3739a10c346b3ec2d3878668c5514696be425f9067d3a11c777f1d', + 500000, + 261593985, + '249bc2dbad96e827eabc991b59dff7f12cc27f3e0da8ab3db6a3201169431786' + '72f712ba14ed7a3b890e161357a163e7408aa22e1d6d1382ebada57973862706' + ) + ], + 'other_transaction': TransferTransaction( + **TRANSFER_TRANSACTION_ARGS, + timestamp=73397, + deadline=83397, + fee=8000000, + signature=None, + transaction_hash=None, + height=None, + sender='22df5f43ee3739a10c346b3ec2d3878668c5514696be425f9067d3a11c777f1d' + ), + 'inner_hash': 'edcc8d1c48165f5b771087fbe3c4b4d41f5f8f6c4ce715e050b86fb4e7fdeb64' +} + +NAMESPACE_REGISTRATION_TRANSACTION_ARGS = { + 'rental_fee_sink': 'TALIC367CZIV55GIQT35HDZAZ53CN3VPB3G55BMU', + 'rental_fee': 1000000, + 'parent': None, + 'namespace': 'test-namespace' +} + +MOSAIC_DEFINITION_TRANSACTION_ARGS = { + 'creation_fee': 1000000, + 'creation_fee_sink': 'TALIC367CZIV55GIQT35HDZAZ53CN3VPB3G55BMU', + 'creator': '595613ba7254a20f1c4c7d215103509f9f9c809de03897cff2b3527b181274e2', + 'description': 'mosaic description info', + 'namespace_name': 'test-namespace.name', + 'properties': MosaicProperties(0, 1000000, True, True), + 'levy': MosaicLevy(1, 'TALIC367CZIV55GIQT35HDZAZ53CN3VPB3G55BMU', 1, 'nem.xem') +} + +MOSAIC_SUPPLY_CHANGE_TRANSACTION_ARGS = { + 'supply_type': 1, + 'delta': 1000000, + 'namespace_name': 'test-namespace.name', +} + + +class BaseTransactionTest(unittest.TestCase): + TRANSACTION_CLASS = None + TRANSACTION_ARGS = None + INVALID_OBJECT = [ + ('transaction_hash', 'hash'), + ('height', 3), + ('sender', 'sender'), + ('fee', 10000), + ('timestamp', 83977), + ('deadline', 73977), + ('signature', 'signature'), + ('transaction_type', 123), + ] + + def _create_default_transaction(self, override=None): + transaction = self.TRANSACTION_CLASS(**COMMON_ARGS, **self.TRANSACTION_ARGS) # pylint: disable=not-callable + + if override: + setattr(transaction, override[0], override[1]) + + return transaction + + def _test_eq_is_supported(self): + # Arrange: + transaction = self._create_default_transaction() + + # Act + Assert: + self.assertEqual(transaction, self._create_default_transaction()) + self.assertNotEqual(transaction, None) + self.assertNotEqual(transaction, 1234567) + + for attr, value in self.INVALID_OBJECT: + self.assertNotEqual(transaction, self._create_default_transaction((attr, value))) + + +class TransferTransactionTest(BaseTransactionTest): + TRANSACTION_CLASS = TransferTransaction + TRANSACTION_ARGS = TRANSFER_TRANSACTION_ARGS + INVALID_OBJECT = BaseTransactionTest.INVALID_OBJECT + [ + ('recipient', 'recipient'), + ('amount', 1), + ('message', Message('test', 2)), + ('mosaics', [Mosaic('nem.xem', 2000000)]) + ] + + def test_eq_is_supported(self): + self._test_eq_is_supported() + + +class AccountKeyLinkTransactionTest(BaseTransactionTest): + TRANSACTION_CLASS = AccountKeyLinkTransaction + TRANSACTION_ARGS = ACCOUNT_KEY_LINK_TRANSACTION_ARGS + INVALID_OBJECT = [ + ('transaction_hash', 'hash'), + ('height', 3), + ('sender', 'sender'), + ('fee', 10000), + ('timestamp', 83977), + ('deadline', 73977), + ('signature', 'signature'), + ('transaction_type', 123), + ('mode', 2), + ('remote_account', 'address') + ] + + def test_eq_is_supported(self): + self._test_eq_is_supported() + + +class MultisigAccountModificationTest(BaseTransactionTest): + TRANSACTION_CLASS = MultisigAccountModificationTransaction + TRANSACTION_ARGS = MULTISIG_ACCOUNT_MODIFICATION_TRANSACTION_ARGS + INVALID_OBJECT = [ + ('transaction_hash', 'hash'), + ('height', 3), + ('sender', 'sender'), + ('fee', 10000), + ('timestamp', 83977), + ('deadline', 73977), + ('signature', 'signature'), + ('transaction_type', 123), + ('min_cosignatories', 0), + ('modifications', []) + ] + + def test_eq_is_supported(self): + self._test_eq_is_supported() + + +class MultisigTransactionTest(BaseTransactionTest): + TRANSACTION_CLASS = MultisigTransaction + TRANSACTION_ARGS = MULTISIG_TRANSACTION_ARGS + INVALID_OBJECT = BaseTransactionTest.INVALID_OBJECT + [ + ('signatures', []), + ('other_transaction', 1), + ('inner_hash', 'hash') + ] + + def test_eq_is_supported(self): + self._test_eq_is_supported() + + +class NamespaceRegistrationTest(BaseTransactionTest): + TRANSACTION_CLASS = NamespaceRegistrationTransaction + TRANSACTION_ARGS = NAMESPACE_REGISTRATION_TRANSACTION_ARGS + INVALID_OBJECT = BaseTransactionTest.INVALID_OBJECT + [ + ('rental_fee_sink', 'abc'), + ('rental_fee', 2), + ('parent', 'some-parent'), + ('namespace', 'some-namespace') + ] + + def test_eq_is_supported(self): + self._test_eq_is_supported() + + +class MosaicDefinitionTest(BaseTransactionTest): + TRANSACTION_CLASS = MosaicDefinitionTransaction + TRANSACTION_ARGS = MOSAIC_DEFINITION_TRANSACTION_ARGS + INVALID_OBJECT = BaseTransactionTest.INVALID_OBJECT + [ + ('creation_fee', 10), + ('creation_fee_sink', 'abc'), + ('creator', 'helo'), + ('description', 'random-text'), + ('namespace_name', 'random-text'), + ('properties', None), + ('levy', None), + ] + + def test_eq_is_supported(self): + self._test_eq_is_supported() + + +class MosaicSupplyChangeTest(BaseTransactionTest): + TRANSACTION_CLASS = MosaicSupplyChangeTransaction + TRANSACTION_ARGS = MOSAIC_SUPPLY_CHANGE_TRANSACTION_ARGS + INVALID_OBJECT = BaseTransactionTest.INVALID_OBJECT + [ + ('supply_type', 3), + ('delta', 100), + ('namespace_name', 'abc') + ] + + def test_eq_is_supported(self): + self._test_eq_is_supported() + + +class CosignSignatureTransactionTest(unittest.TestCase): + @staticmethod + def _create_default_cosign_signature_transaction(override=None): + cosign_signature_transaction = CosignSignatureTransaction( + timestamp=261593985, + other_hash='edcc8d1c48165f5b771087fbe3c4b4d41f5f8f6c4ce715e050b86fb4e7fdeb64', + other_account='TALIC367CZIV55GIQT35HDZAZ53CN3VPB3G55BMU', + sender='22df5f43ee3739a10c346b3ec2d3878668c5514696be425f9067d3a11c777f1d', + fee=500000, + deadline=261593985, + signature=( + '249bc2dbad96e827eabc991b59dff7f12cc27f3e0da8ab3db6a3201169431786' + '72f712ba14ed7a3b890e161357a163e7408aa22e1d6d1382ebada57973862706' + ) + ) + + if override: + setattr(cosign_signature_transaction, override[0], override[1]) + + return cosign_signature_transaction + + def test_can_create_cosign_signature_transaction(self): + # Act: + cosign_signature_transaction = self._create_default_cosign_signature_transaction() + + # Assert: + self.assertEqual(261593985, cosign_signature_transaction.timestamp) + self.assertEqual('edcc8d1c48165f5b771087fbe3c4b4d41f5f8f6c4ce715e050b86fb4e7fdeb64', cosign_signature_transaction.other_hash) + self.assertEqual('TALIC367CZIV55GIQT35HDZAZ53CN3VPB3G55BMU', cosign_signature_transaction.other_account) + self.assertEqual('22df5f43ee3739a10c346b3ec2d3878668c5514696be425f9067d3a11c777f1d', cosign_signature_transaction.sender) + self.assertEqual(500000, cosign_signature_transaction.fee) + self.assertEqual(261593985, cosign_signature_transaction.deadline) + self.assertEqual( + '249bc2dbad96e827eabc991b59dff7f12cc27f3e0da8ab3db6a3201169431786' + '72f712ba14ed7a3b890e161357a163e7408aa22e1d6d1382ebada57973862706', cosign_signature_transaction.signature) + self.assertEqual(TransactionType.MULTISIG_COSIGNATURE.value, cosign_signature_transaction.transaction_type) + + +class TransactionFactoryTest(unittest.TestCase): + def _run_common_args_test(self, transaction): + self.assertEqual(COMMON_ARGS['transaction_hash'], transaction.transaction_hash) + self.assertEqual(COMMON_ARGS['height'], transaction.height) + self.assertEqual(COMMON_ARGS['sender'], transaction.sender) + self.assertEqual(COMMON_ARGS['fee'], transaction.fee) + self.assertEqual(COMMON_ARGS['timestamp'], transaction.timestamp) + self.assertEqual(COMMON_ARGS['deadline'], transaction.deadline) + self.assertEqual(COMMON_ARGS['signature'], transaction.signature) + + def test_create_transfer_transaction(self): + # Arrange + Act: + transaction = TransactionFactory.create_transaction(TransactionType.TRANSFER.value, COMMON_ARGS, TRANSFER_TRANSACTION_ARGS) + + # Assert: + self._run_common_args_test(transaction) + self.assertEqual(TRANSFER_TRANSACTION_ARGS['recipient'], transaction.recipient) + self.assertEqual(TRANSFER_TRANSACTION_ARGS['amount'], transaction.amount) + self.assertEqual(TRANSFER_TRANSACTION_ARGS['message'], transaction.message) + self.assertEqual(TRANSFER_TRANSACTION_ARGS['mosaics'], transaction.mosaics) + self.assertEqual(TransactionType.TRANSFER.value, transaction.transaction_type) + + def test_create_account_key_link_transaction(self): + # Arrange + Act: + transaction = TransactionFactory.create_transaction( + TransactionType.ACCOUNT_KEY_LINK.value, + COMMON_ARGS, + ACCOUNT_KEY_LINK_TRANSACTION_ARGS + ) + + # Assert: + self._run_common_args_test(transaction) + self.assertEqual(ACCOUNT_KEY_LINK_TRANSACTION_ARGS['mode'], transaction.mode) + self.assertEqual(ACCOUNT_KEY_LINK_TRANSACTION_ARGS['remote_account'], transaction.remote_account) + self.assertEqual(TransactionType.ACCOUNT_KEY_LINK.value, transaction.transaction_type) + + def test_create_multisig_account_modification_transaction(self): + # Arrange + Act: + transaction = TransactionFactory.create_transaction( + TransactionType.MULTISIG_ACCOUNT_MODIFICATION.value, + COMMON_ARGS, + MULTISIG_ACCOUNT_MODIFICATION_TRANSACTION_ARGS + ) + + # Assert: + self._run_common_args_test(transaction) + self.assertEqual(MULTISIG_ACCOUNT_MODIFICATION_TRANSACTION_ARGS['min_cosignatories'], transaction.min_cosignatories) + self.assertEqual(MULTISIG_ACCOUNT_MODIFICATION_TRANSACTION_ARGS['modifications'], transaction.modifications) + self.assertEqual(TransactionType.MULTISIG_ACCOUNT_MODIFICATION.value, transaction.transaction_type) + + def test_create_multisig_transaction(self): + # Arrange + Act: + transaction = TransactionFactory.create_transaction(TransactionType.MULTISIG.value, COMMON_ARGS, MULTISIG_TRANSACTION_ARGS) + + # Assert: + self._run_common_args_test(transaction) + self.assertEqual(MULTISIG_TRANSACTION_ARGS['signatures'], transaction.signatures) + self.assertEqual(MULTISIG_TRANSACTION_ARGS['other_transaction'], transaction.other_transaction) + self.assertEqual(TransactionType.MULTISIG.value, transaction.transaction_type) + + def test_create_namespace_registration_transaction(self): + # Arrange + Act: + transaction = TransactionFactory.create_transaction( + TransactionType.NAMESPACE_REGISTRATION.value, + COMMON_ARGS, + NAMESPACE_REGISTRATION_TRANSACTION_ARGS + ) + + # Assert: + self._run_common_args_test(transaction) + self.assertEqual(NAMESPACE_REGISTRATION_TRANSACTION_ARGS['rental_fee_sink'], transaction.rental_fee_sink) + self.assertEqual(NAMESPACE_REGISTRATION_TRANSACTION_ARGS['rental_fee'], transaction.rental_fee) + self.assertEqual(NAMESPACE_REGISTRATION_TRANSACTION_ARGS['parent'], transaction.parent) + self.assertEqual(NAMESPACE_REGISTRATION_TRANSACTION_ARGS['namespace'], transaction.namespace) + self.assertEqual(TransactionType.NAMESPACE_REGISTRATION.value, transaction.transaction_type) + + def test_create_mosaic_definition_transaction(self): + # Arrange + Act: + transaction = TransactionFactory.create_transaction( + TransactionType.MOSAIC_DEFINITION.value, + COMMON_ARGS, + MOSAIC_DEFINITION_TRANSACTION_ARGS + ) + + # Assert: + self._run_common_args_test(transaction) + self.assertEqual(MOSAIC_DEFINITION_TRANSACTION_ARGS['creation_fee'], transaction.creation_fee) + self.assertEqual(MOSAIC_DEFINITION_TRANSACTION_ARGS['creation_fee_sink'], transaction.creation_fee_sink) + self.assertEqual(MOSAIC_DEFINITION_TRANSACTION_ARGS['creator'], transaction.creator) + self.assertEqual(MOSAIC_DEFINITION_TRANSACTION_ARGS['description'], transaction.description) + self.assertEqual(MOSAIC_DEFINITION_TRANSACTION_ARGS['namespace_name'], transaction.namespace_name) + self.assertEqual(MOSAIC_DEFINITION_TRANSACTION_ARGS['properties'], transaction.properties) + self.assertEqual(MOSAIC_DEFINITION_TRANSACTION_ARGS['levy'], transaction.levy) + self.assertEqual(TransactionType.MOSAIC_DEFINITION.value, transaction.transaction_type) + + def test_create_mosaic_supply_change_transaction(self): + # Arrange + Act: + transaction = TransactionFactory.create_transaction( + TransactionType.MOSAIC_SUPPLY_CHANGE.value, + COMMON_ARGS, + MOSAIC_SUPPLY_CHANGE_TRANSACTION_ARGS + ) + + # Assert: + self._run_common_args_test(transaction) + self.assertEqual(MOSAIC_SUPPLY_CHANGE_TRANSACTION_ARGS['supply_type'], transaction.supply_type) + self.assertEqual(MOSAIC_SUPPLY_CHANGE_TRANSACTION_ARGS['delta'], transaction.delta) + self.assertEqual(MOSAIC_SUPPLY_CHANGE_TRANSACTION_ARGS['namespace_name'], transaction.namespace_name) + self.assertEqual(TransactionType.MOSAIC_SUPPLY_CHANGE.value, transaction.transaction_type) + + def test_return_unknown_transaction_type(self): + # Arrange + Act: + with self.assertRaises(UnknownTransactionType): + TransactionFactory.create_transaction(123, COMMON_ARGS, {}) diff --git a/tools/shoestring/shoestring/internal/CertificateFactory.py b/tools/shoestring/shoestring/internal/CertificateFactory.py index adc0f7b0f5..54ca3cfd82 100644 --- a/tools/shoestring/shoestring/internal/CertificateFactory.py +++ b/tools/shoestring/shoestring/internal/CertificateFactory.py @@ -96,6 +96,11 @@ def _prepare_ca_certificate(self, ca_cn): 'basicConstraints = critical,CA:TRUE', 'subjectKeyIdentifier = hash', 'authorityKeyIdentifier = keyid:always,issuer' + '', + '[x509_v3_node]', + 'basicConstraints = CA:FALSE', + 'subjectKeyIdentifier = hash', + 'authorityKeyIdentifier = keyid,issuer' ])) # create new certs directory @@ -143,15 +148,9 @@ def generate_node_certificate(self, node_cn, days=375, start_date=None): '[req]', 'prompt = no', 'distinguished_name = dn', - 'x509_extensions = x509_v3', '', '[dn]', f'CN = {node_cn}', - '', - '[x509_v3]', - 'basicConstraints = CA:FALSE', - 'subjectKeyIdentifier = hash', - 'authorityKeyIdentifier = keyid,issuer' ])) # prepare node certificate signing request @@ -177,7 +176,8 @@ def generate_node_certificate(self, node_cn, days=375, start_date=None): '-notext', '-batch', '-in', 'node.csr.pem', - '-out', 'node.crt.pem' + '-out', 'node.crt.pem', + '-extensions', 'x509_v3_node' ] + ([] if not start_date else ['-startdate', start_date.strftime('%y%m%d%H%M%SZ')]))) @staticmethod diff --git a/tools/shoestring/tests/internal/test_CertificateFactory.py b/tools/shoestring/tests/internal/test_CertificateFactory.py index f18b1bc79c..f7cda3c12f 100644 --- a/tools/shoestring/tests/internal/test_CertificateFactory.py +++ b/tools/shoestring/tests/internal/test_CertificateFactory.py @@ -142,6 +142,9 @@ def _assert_certificate_duration(self, x509_output, test_start_time, expected_da self.assertEqual(expected_days, (cert_end_time - cert_start_time).days) + def _assert_certificate_is_x509v3(self, x509_output): + self.assertIn('X509v3 extensions', x509_output) + def _assert_can_generate_ca_certificate(self, additional_args, expected_duration_days): # Arrange: certificate has second resolution, so clear microseconds for assert below to work test_start_time = datetime.datetime.utcnow().replace(microsecond=0) @@ -174,6 +177,9 @@ def _assert_can_generate_ca_certificate(self, additional_args, expected_duration # - verify certificate is properly self signed self._create_executor().dispatch(['verify', '-CAfile', ca_certificate_path, ca_certificate_path]) + # - check certificate is x509v3 + self._assert_certificate_is_x509v3(x509_output) + def test_can_generate_ca_certificate(self): self._assert_can_generate_ca_certificate({}, 20 * 365) @@ -235,6 +241,9 @@ def _assert_can_generate_node_certificate(self, should_generate_certificate_chai if not future_start_delay_days: self._create_executor().dispatch(['verify', '-CAfile', ca_certificate_path, node_certificate_path]) + # - check certificate is x509v3 + self._assert_certificate_is_x509v3(x509_output) + def test_can_generate_node_certificate(self): self._assert_can_generate_node_certificate(False, {}, {})