diff --git a/crypto/identity/private_key.py b/crypto/identity/private_key.py index 5c0781dc..d15d9ae1 100644 --- a/crypto/identity/private_key.py +++ b/crypto/identity/private_key.py @@ -17,14 +17,9 @@ def sign(self, message: bytes) -> bytes: Returns: bytes: signature of the signed message """ - from crypto.transactions.signature import Signature - - signature = Signature.sign( - hexlify(message), - self - ) - - return signature.encode() + signature = self.private_key.sign(message) + + return hexlify(signature).decode() def to_hex(self): """Returns a private key in hex format diff --git a/crypto/transactions/builder/base.py b/crypto/transactions/builder/base.py index bca7c814..89330386 100644 --- a/crypto/transactions/builder/base.py +++ b/crypto/transactions/builder/base.py @@ -1,36 +1,59 @@ -from binascii import hexlify, unhexlify +from typing import Optional -from crypto.configuration.fee import get_fee -from crypto.constants import TRANSACTION_TYPE_GROUP +from crypto.configuration.network import get_network from crypto.identity.private_key import PrivateKey -from crypto.identity.public_key import PublicKey -from crypto.transactions.serializer import Serializer -from crypto.transactions.signature import Signature -from crypto.transactions.transaction import Transaction - -class BaseTransactionBuilder(object): - transaction: Transaction - - def __init__(self): - self.transaction = Transaction() - - if hasattr(self, 'transaction_type'): - self.transaction.type = getattr(self, 'transaction_type') - - if hasattr(self, 'transaction_type'): - self.transaction.fee = get_fee(getattr(self, 'transaction_type')) - - if hasattr(self, 'nonce'): - self.transaction.nonce = getattr(self, 'nonce') - - if hasattr(self, 'signatures'): - self.transaction.signatures = getattr(self, 'signatures') - - self.transaction.typeGroup = getattr(self, 'typeGroup', int(TRANSACTION_TYPE_GROUP.CORE)) - self.transaction.version = getattr(self, 'version', 1) - self.transaction.expiration = getattr(self, 'expiration', 0) - if self.transaction.type != 0: - self.transaction.amount = getattr(self, 'amount', 0) +from crypto.transactions.types.abstract_transaction import AbstractTransaction + + +class AbstractTransactionBuilder: + def __init__(self, data: Optional[dict] = None): + default_data = { + 'value': '0', + 'senderPublicKey': '', + 'gasPrice': '5', + 'nonce': '1', + 'network': get_network()['version'], + 'gasLimit': 1_000_000, + 'data': '', + } + self.transaction = self.get_transaction_instance(data or default_data) + + def __str__(self): + return self.to_json() + + @classmethod + def new(cls, data: Optional[dict] = None): + return cls(data) + + def gas_limit(self, gas_limit: int): + self.transaction.data['gasLimit'] = gas_limit + return self + + def recipient_address(self, recipient_address: str): + self.transaction.data['recipientAddress'] = recipient_address + return self + + def gas_price(self, gas_price: int): + self.transaction.data['gasPrice'] = gas_price + return self + + def nonce(self, nonce: str): + self.transaction.data['nonce'] = nonce + return self + + def network(self, network: int): + self.transaction.data['network'] = network + return self + + def sign(self, passphrase: str): + keys = PrivateKey.from_passphrase(passphrase) + self.transaction.data['senderPublicKey'] = keys.public_key + self.transaction = self.transaction.sign(keys) + self.transaction.data['id'] = self.transaction.get_id() + return self + + def verify(self): + return self.transaction.verify() def to_dict(self): return self.transaction.to_dict() @@ -38,83 +61,5 @@ def to_dict(self): def to_json(self): return self.transaction.to_json() - def sign(self, passphrase): - """Sign the transaction using the given passphrase - - Args: - passphrase (str): passphrase associated with the account sending this transaction - """ - self.transaction.senderPublicKey = PublicKey.from_passphrase(passphrase) - - msg = self.transaction.to_bytes(False, True, False) - secret = unhexlify(PrivateKey.from_passphrase(passphrase).to_hex()) - self.transaction.signature = Signature.sign(msg, secret) - self.transaction.id = self.transaction.get_id() - - def second_sign(self, passphrase): - """Sign the transaction using the given second passphrase - - Args: - passphrase (str): 2nd passphrase associated with the account sending this transaction - """ - msg = self.transaction.to_bytes(False, True, False) - secret = unhexlify(PrivateKey.from_passphrase(passphrase).to_hex()) - self.transaction.signSignature = Signature.sign(msg, secret) - self.transaction.id = self.transaction.get_id() - - def multi_sign(self, passphrase, index): - if not self.transaction.signatures: - self.transaction.signatures = [] - - if self.transaction.senderPublicKey is None: - raise Exception('Sender Public Key is required for multi signature') - - index = len(self.transaction.signatures) if index == -1 else index - - msg = self.transaction.to_bytes() - secret = unhexlify(PrivateKey.from_passphrase(passphrase).to_hex()) - signature = Signature.sign(msg, secret) - - index_formatted = hex(index).replace('x', '') - self.transaction.signatures.append(index_formatted + signature) - - def serialize(self, skip_signature=False, skip_second_signature=False, skip_multi_signature=False): - """Perform AIP11 compliant serialization. - - Args: - skip_signature (bool, optional): do you want to skip the signature - skip_second_signature (bool, optional): do you want to skip the 2nd signature - skip_multi_signature (bool, optional): do you want to skip multi signature - - Returns: - str: Serialized string - """ - return Serializer(self.to_dict()).serialize(skip_signature, skip_second_signature, skip_multi_signature) - - def schnorr_verify(self): - return self.transaction.verify_schnorr() - - def verify_secondsig_schnorr(self, secondPublicKey): - return self.transaction.verify_secondsig_schnorr(secondPublicKey) - - def verify_multisig_schnorr(self): - return self.transaction.verify_multisig_schnorr() - - def set_nonce(self, nonce): - self.transaction.nonce = nonce - - def set_amount(self, amount: int): - self.transaction.amount = amount - - def set_sender_public_key(self, public_key: str): - self.transaction.senderPublicKey = public_key - - def set_expiration(self, expiration: int): - self.transaction.expiration = expiration - - def set_type_group(self, type_group): - if type(type_group) == int: - self.transaction.typeGroup = type_group - else: - types = {TRANSACTION_TYPE_GROUP.TEST: 0, TRANSACTION_TYPE_GROUP.CORE: 1, TRANSACTION_TYPE_GROUP.RESERVED: 1000} - self.transaction.typeGroup = types[type_group] + def get_transaction_instance(self, data: dict) -> AbstractTransaction: + raise NotImplementedError("Subclasses must implement get_transaction_instance()") diff --git a/crypto/transactions/builder/transfer.py b/crypto/transactions/builder/transfer.py deleted file mode 100644 index 37d245aa..00000000 --- a/crypto/transactions/builder/transfer.py +++ /dev/null @@ -1,31 +0,0 @@ -from typing import Optional - -from crypto.constants import TRANSACTION_TRANSFER -from crypto.transactions.builder.base import BaseTransactionBuilder - -class Transfer(BaseTransactionBuilder): - - transaction_type = TRANSACTION_TRANSFER - - def __init__(self, recipientId: str, amount: int, vendorField: Optional[str] = None, fee: Optional[int] = None): - """Create a transfer transaction - - Args: - recipientId (str): address to which you want to send coins - amount (int): amount of coins you want to transfer - vendorField (str): value for the vendor field aka smartbridge - fee (int, optional): fee used for the transaction (default is already set) - """ - super().__init__() - - self.transaction.recipientId = recipientId - - if type(amount) == int and amount > 0: - self.transaction.amount = amount - else: - raise ValueError('Amount is not valid') - - self.transaction.vendorField = vendorField.encode() if vendorField else None - - if fee: - self.transaction.fee = fee diff --git a/crypto/transactions/builder/transfer_builder.py b/crypto/transactions/builder/transfer_builder.py new file mode 100644 index 00000000..9af863c8 --- /dev/null +++ b/crypto/transactions/builder/transfer_builder.py @@ -0,0 +1,12 @@ +from crypto.transactions.builder.base import AbstractTransactionBuilder +from crypto.transactions.types.transfer import Transfer + + +class TransferBuilder(AbstractTransactionBuilder): + def value(self, value: str): + self.transaction.data['value'] = value + self.transaction.refresh_payload_data() + return self + + def get_transaction_instance(self, data: dict): + return Transfer(data) diff --git a/crypto/transactions/deserializer.py b/crypto/transactions/deserializer.py index 01395f11..4fef67ab 100644 --- a/crypto/transactions/deserializer.py +++ b/crypto/transactions/deserializer.py @@ -1,76 +1,128 @@ -import inspect -from binascii import hexlify, unhexlify -from importlib import import_module -from typing import Union - -from binary.unsigned_integer.reader import read_bit8, read_bit16, read_bit32, read_bit64 - -from crypto.constants import TRANSACTION_TYPES -from crypto.transactions.deserializers.base import BaseDeserializer -from crypto.transactions.transaction import Transaction - -class Deserializer(object): - serialized: bytes - - def __init__(self, serialized: Union[bytes, str]): - self.serialized = unhexlify(serialized) - - def deserialize(self) -> Transaction: - """Deserialize transaction - - Returns: - Transaction: returns transaction object - """ - - transaction = Transaction() - transaction.version = read_bit8(self.serialized, offset=1) - transaction.network = read_bit8(self.serialized, offset=2) - transaction.typeGroup = read_bit32(self.serialized, offset=3) - transaction.type = read_bit16(self.serialized, offset=7) - transaction.nonce = read_bit64(self.serialized, offset=9) - transaction.senderPublicKey = hexlify(self.serialized)[34:66+34].decode() - transaction.fee = read_bit64(self.serialized, offset=50) - - vendor_field_length = read_bit8(self.serialized, offset=58) - if vendor_field_length > 0: - vendor_field_offset = (58 + 8) * 2 - vendorField_take = vendor_field_length * 2 - transaction.vendorFieldHex = hexlify( - self.serialized - )[vendor_field_offset:vendorField_take] - - asset_offset = (58 + 1) * 2 + vendor_field_length * 2 - - handled_transaction = self._handle_transaction_type(asset_offset, transaction) - transaction.amount = handled_transaction.amount - transaction.version = handled_transaction.version - transaction.id = transaction.get_id() +from crypto.transactions.types.abstract_transaction import AbstractTransaction +from crypto.transactions.types.transfer import Transfer +# from crypto.transactions.types.evm_call import EvmCall +# from crypto.transactions.types.vote import Vote +# from crypto.transactions.types.unvote import Unvote +# from crypto.transactions.types.validator_registration import ValidatorRegistration +# from crypto.transactions.types.validator_resignation import ValidatorResignation +from binascii import unhexlify, hexlify + +from binary.unsigned_integer.reader import ( + read_bit8, + read_bit32, + read_bit64, + # read_bit256, +) +# from crypto.enums.abi_function import AbiFunction # TODO: Implement or import AbiFunction +# from crypto.utils.abi_decoder import AbiDecoder # TODO: Implement or import AbiDecoder + + +class Deserializer: + SIGNATURE_SIZE = 64 + RECOVERY_SIZE = 1 + + def __init__(self, serialized: str): + self.serialized = unhexlify(serialized) if isinstance(serialized, str) else serialized + self.pointer = 0 + + @staticmethod + def new(serialized: str): + return Deserializer(serialized) + + def deserialize(self) -> AbstractTransaction: + data = {} + + self.deserialize_common(data) + self.deserialize_data(data) + transaction = self.guess_transaction_from_data(data) + self.deserialize_signatures(data) + + transaction.data = data + transaction.recover_sender() + + transaction.data['id'] = transaction.hash(skip_signature=False).hex() return transaction - def _handle_transaction_type(self, asset_offset: int, transaction): - """Handle deserialization for a given transaction type - - Args: - asset_offset (int): - transaction (Transaction): Transaction resource object - - Returns: - Transaction: Transaction object of currently deserialized data - """ - - deserializer_name = TRANSACTION_TYPES[transaction.type] - module = import_module('crypto.transactions.deserializers.{}'.format(deserializer_name)) - for attr in dir(module): - # If attr name is `BaseDeserializer`, skip it as it's a class and also has a - # subclass of BaseDeserializer - if attr == 'BaseDeserializer': - continue - - attribute = getattr(module, attr) - if inspect.isclass(attribute) and issubclass(attribute, BaseDeserializer): - # this attribute is actually a specific deserializer that we want to use - deserializer = attribute - break - - return deserializer(self.serialized, asset_offset, transaction).deserialize() + def read_bytes(self, length: int) -> bytes: + result = self.serialized[self.pointer:self.pointer + length] + self.pointer += length + return result + + def deserialize_common(self, data: dict): + data['network'], _ = read_bit8(self.serialized, self.pointer) + self.pointer += 1 + + nonce, _ = read_bit64(self.serialized, self.pointer) + data['nonce'] = str(nonce) + self.pointer += 8 + + gas_price, _ = read_bit32(self.serialized, self.pointer) + data['gasPrice'] = gas_price + self.pointer += 4 + + gas_limit, _ = read_bit32(self.serialized, self.pointer) + data['gasLimit'] = gas_limit + self.pointer += 4 + + data['value'] = '0' + + def deserialize_data(self, data: dict): + # @TODO: this should use read_bit256 + # value, _ = read_bit256(self.serialized, self.pointer) + value, _ = read_bit64(self.serialized, self.pointer) + + data['value'] = str(value) + self.pointer += 32 + + recipient_marker, _ = read_bit8(self.serialized, self.pointer) + self.pointer += 1 + + if recipient_marker == 1: + recipient_address_bytes = self.read_bytes(20) + recipient_address = '0x' + hexlify(recipient_address_bytes).decode() + data['recipientAddress'] = recipient_address + + payload_length, _ = read_bit32(self.serialized, self.pointer) + self.pointer += 4 + + payload_hex = '' + if payload_length > 0: + payload_bytes = self.read_bytes(payload_length) + payload_hex = hexlify(payload_bytes).decode() + + data['data'] = payload_hex + + def deserialize_signatures(self, data: dict): + signature_length = self.SIGNATURE_SIZE + self.RECOVERY_SIZE + signature_bytes = self.read_bytes(signature_length) + data['signature'] = hexlify(signature_bytes).decode() + + def guess_transaction_from_data(self, data: dict) -> AbstractTransaction: + if data['value'] != '0': + return Transfer(data) + + # payload_data = self.decode_payload(data) + payload_data = None # As AbiDecoder is not implemented + + if payload_data is None: + return Transfer(data) # Using Transfer for now + + # if function_name == AbiFunction.VOTE.value: + # return Vote(data) + # elif function_name == AbiFunction.UNVOTE.value: + # return Unvote(data) + # elif function_name == AbiFunction.VALIDATOR_REGISTRATION.value: + # return ValidatorRegistration(data) + # elif function_name == AbiFunction.VALIDATOR_RESIGNATION.value: + # return ValidatorResignation(data) + # else: + # return Transfer(data) + + # def decode_payload(self, data: dict) -> dict: + # payload = data.get('data', '') + # + # if payload == '': + # return None + # + # return AbiDecoder().decode_function_data(payload) diff --git a/crypto/transactions/serializer.py b/crypto/transactions/serializer.py index ff9c2800..974db7f1 100644 --- a/crypto/transactions/serializer.py +++ b/crypto/transactions/serializer.py @@ -1,114 +1,73 @@ -import inspect -from binascii import hexlify, unhexlify -from importlib import import_module - -from binary.hex.writer import write_high -from binary.unsigned_integer.writer import write_bit8, write_bit16, write_bit32, write_bit64 - +from binascii import unhexlify +from crypto.transactions.types.abstract_transaction import AbstractTransaction from crypto.configuration.network import get_network -from crypto.constants import TRANSACTION_TYPES -from crypto.exceptions import ArkSerializerException -from crypto.transactions.serializers.base import BaseSerializer +from binary.unsigned_integer.writer import ( + write_bit8, + write_bit32, + write_bit64, + # write_bit256, +) +# from crypto.utils.address import Address # TODO: Implement or import Address -class Serializer(object): - transaction: dict = {} - def __init__(self, transaction): +class Serializer: + def __init__(self, transaction: AbstractTransaction): if not transaction: - raise ArkSerializerException('No transaction data provided') + raise ValueError('No transaction data provided') self.transaction = transaction - def serialize_bytes(self, skip_signature: bool = True, skip_second_signature: bool = True, skip_multi_signature: bool = True) -> bytes: - """Perform AIP11 compliant serialization - - Returns: - bytes: Serialized bytes - """ - network_config = get_network() - bytes_data = bytes() - - bytes_data += write_bit8(0xff) - - bytes_data += write_bit8(self.transaction.get('version') or 0x01) - bytes_data += write_bit8(self.transaction.get('network') or network_config['version']) - bytes_data += write_bit32(self.transaction.get('typeGroup') or 0x01) - bytes_data += write_bit16(self.transaction.get('type')) - bytes_data += write_bit64(self.transaction.get('nonce') or 0x01) + @staticmethod + def new(transaction: AbstractTransaction): + return Serializer(transaction) - bytes_data += write_high(self.transaction.get('senderPublicKey')) - bytes_data += write_bit64(self.transaction.get('fee')) + @staticmethod + def get_bytes(transaction: AbstractTransaction, skip_signature: bool = False) -> bytes: + return transaction.serialize(skip_signature=skip_signature) - if self.transaction.get('vendorField'): - vendorFieldLength = len(self.transaction.get('vendorField') or '') - - bytes_data += write_bit8(vendorFieldLength) - bytes_data += self.transaction['vendorField'].encode() - elif self.transaction.get('vendorFieldHex'): - vendorField_hex_length = len(self.transaction['vendorFieldHex']) - - bytes_data += write_bit8(vendorField_hex_length / 2) - bytes_data += self.transaction['vendorFieldHex'] - else: - bytes_data += write_bit8(0x00) + def serialize(self, skip_signature: bool = False) -> bytes: + bytes_data = bytes() - bytes_data = self._handle_transaction_type(bytes_data) - bytes_data = self._handle_signature(bytes_data, skip_signature, skip_second_signature, skip_multi_signature) + bytes_data += self.serialize_common() + bytes_data += self.serialize_data() + if not skip_signature: + bytes_data += self.serialize_signatures() return bytes_data - def serialize(self, skip_signature: bool = True, skip_second_signature: bool = True, skip_multi_signature: bool = True) -> str: - """Perform AIP11 compliant serialization - - Returns: - str: Serialized string - """ - bytes_data = self.serialize_bytes(skip_signature, skip_second_signature, skip_multi_signature) - - return hexlify(bytes_data).decode() - - def _handle_transaction_type(self, bytes_data) -> bytes: - """Serialize transaction specific data (eg. validator registration) - - Args: - bytes_data (bytes): already serialized data about a transaction (eg. version, network) - - Returns: - bytes: bytes string - """ - serializer_name = TRANSACTION_TYPES[self.transaction['type']] - - module = import_module('crypto.transactions.serializers.{}'.format(serializer_name)) - for attr in dir(module): - # If attr name is `BaseSerializer`, skip it as it's a class and also has a - # subclass of BaseSerializer - if attr == 'BaseSerializer': - continue - - attribute = getattr(module, attr) - if inspect.isclass(attribute) and issubclass(attribute, BaseSerializer): - # this attribute is actually a specific serializer that we want to use - serializer = attribute - break - - return serializer(self.transaction, bytes_data).serialize() + def serialize_common(self) -> bytes: + bytes_data = bytes() + network_version = self.transaction.data.get('network', get_network()['version']) + bytes_data += write_bit8(int(network_version)) + bytes_data += write_bit64(int(self.transaction.data['nonce'])) + bytes_data += write_bit32(int(self.transaction.data['gasPrice'])) + bytes_data += write_bit32(int(self.transaction.data['gasLimit'])) + return bytes_data - def _handle_signature(self, bytes_data, skip_signature, skip_second_signature, skip_multi_signature) -> bytes: - """Serialize signature data of the transaction + def serialize_data(self) -> bytes: + bytes_data = bytes() + + # @TODO: this should use write_bit256 + # bytes_data += write_bit256(int(self.transaction.data['value'])) + bytes_data += write_bit64(int(self.transaction.data['value'])) + + if 'recipientAddress' in self.transaction.data: + bytes_data += write_bit8(1) + recipient_address = self.transaction.data['recipientAddress'] + bytes_data += unhexlify(recipient_address.replace('0x', '')) + else: + bytes_data += write_bit8(0) - Args: - bytes_data (bytes): already serialized data + payload_hex = self.transaction.data.get('data', '') + payload_length = len(payload_hex) // 2 + bytes_data += write_bit32(payload_length) - Returns: - bytes: bytes string - """ - if not skip_signature and self.transaction.get('signature'): - bytes_data += unhexlify(self.transaction['signature']) + if payload_length > 0: + bytes_data += unhexlify(payload_hex) - if not skip_second_signature and self.transaction.get('secondSignature'): - bytes_data += unhexlify(self.transaction['secondSignature']) - if not skip_second_signature and self.transaction.get('signSignature'): - bytes_data += unhexlify(self.transaction['signSignature']) - if not skip_multi_signature and self.transaction.get('signatures'): - bytes_data += unhexlify(''.join(self.transaction['signatures'])) + return bytes_data + def serialize_signatures(self) -> bytes: + bytes_data = bytes() + if 'signature' in self.transaction.data: + bytes_data += unhexlify(self.transaction.data['signature']) return bytes_data diff --git a/crypto/transactions/types/abstract_transaction.py b/crypto/transactions/types/abstract_transaction.py new file mode 100644 index 00000000..370add82 --- /dev/null +++ b/crypto/transactions/types/abstract_transaction.py @@ -0,0 +1,79 @@ +import json +from typing import Optional + +from crypto.configuration.network import get_network +from crypto.identity.address import address_from_public_key +from crypto.identity.private_key import PrivateKey + + +class AbstractTransaction: + def __init__(self, data: Optional[dict] = None): + self.data = data or {} + self.refresh_payload_data() + + def get_payload(self) -> str: + return '' + + def decode_payload(self, data: dict) -> Optional[dict]: + if 'data' not in data or data['data'] == '': + return None + # TODO: add AbiDecoder to decode the payload + return {} + + def refresh_payload_data(self): + self.data['data'] = self.get_payload().lstrip('0x') + + def get_id(self) -> str: + return self.hash(skip_signature=False).hex() + + def get_bytes(self, skip_signature: bool = False) -> bytes: + from crypto.transactions.serializer import Serializer + return Serializer.get_bytes(self, skip_signature) + + def sign(self, private_key: PrivateKey): + hash_ = self.hash(skip_signature=True) + # TODO: Implement signing logic + return self + + def get_public_key(self, compact_signature): + # TODO: Implement this method + pass + + def recover_sender(self): + compact_signature = self.get_signature() + public_key = self.get_public_key(compact_signature) + self.data['senderPublicKey'] = public_key.hex() + self.data['senderAddress'] = address_from_public_key(self.data['senderPublicKey']) + + def verify(self) -> bool: + # TODO: Implement this method + return True + + def serialize(self, skip_signature: bool = False) -> bytes: + from crypto.transactions.serializer import Serializer + return Serializer(self).serialize(skip_signature) + + def to_dict(self) -> dict: + return {k: v for k, v in self.data.items() if v is not None} + + def to_json(self) -> str: + return json.dumps(self.to_dict()) + + def hash(self, skip_signature: bool) -> bytes: + hash_data = { + 'gasPrice': self.data.get('gasPrice'), + 'network': self.data.get('network', get_network().get('version')), + 'nonce': self.data.get('nonce'), + 'value': self.data.get('value'), + 'gasLimit': self.data.get('gasLimit'), + 'data': self.data.get('data'), + 'recipientAddress': self.data.get('recipientAddress'), + 'signature': self.data.get('signature') if not skip_signature else None, + } + # TODO: Implement TransactionHasher + # return TransactionHasher.to_hash(hash_data, skip_signature) + return b'' + + def get_signature(self): + # TODO: Implement this method + pass diff --git a/crypto/transactions/types/transfer.py b/crypto/transactions/types/transfer.py new file mode 100644 index 00000000..abe87738 --- /dev/null +++ b/crypto/transactions/types/transfer.py @@ -0,0 +1,6 @@ +from crypto.transactions.types.abstract_transaction import AbstractTransaction + + +class Transfer(AbstractTransaction): + def get_payload(self) -> str: + return '' diff --git a/tests/fixtures/evm-sign.json b/tests/fixtures/evm-sign.json new file mode 100644 index 00000000..600ffa2d --- /dev/null +++ b/tests/fixtures/evm-sign.json @@ -0,0 +1,16 @@ +{ + "data": { + "network": 30, + "nonce": "13", + "gasPrice": 5, + "gasLimit": 1000000, + "value": "0", + "recipientAddress": "0xE536720791A7DaDBeBdBCD8c8546fb0791a11901", + "data": "a9059cbb00000000000000000000000027fa7caffaae77ddb9ab232fdbda56d5e5af2393000000000000000000000000000000000000000000000000016345785d8a0000", + "signature": "ba30f9042519079895c7408b0e92046c3f20680e0a9294e38ab3cfdd19b26cd4036fe2a80644abb922f1ad7cd682811a83c20120a8030df47b244a3bc44f4dbd00", + "senderPublicKey": "023efc1da7f315f3c533a4080e491f32cd4219731cef008976c3876539e1f192d3", + "senderAddress": "0x6F0182a0cc707b055322CcF6d4CB6a5Aff1aEb22", + "id": "3935ff0fe84ea6ac42fc889ed7cda4f97ddd11fd2d1c31e9201f14866acb6edc" + }, + "serialized": "1e0d000000000000000500000040420f00000000000000000000000000000000000000000000000000000000000000000001e536720791a7dadbebdbcd8c8546fb0791a1190144000000a9059cbb00000000000000000000000027fa7caffaae77ddb9ab232fdbda56d5e5af2393000000000000000000000000000000000000000000000000016345785d8a0000ba30f9042519079895c7408b0e92046c3f20680e0a9294e38ab3cfdd19b26cd4036fe2a80644abb922f1ad7cd682811a83c20120a8030df47b244a3bc44f4dbd00" +} diff --git a/tests/fixtures/transfer.json b/tests/fixtures/transfer.json new file mode 100644 index 00000000..4c577082 --- /dev/null +++ b/tests/fixtures/transfer.json @@ -0,0 +1,16 @@ +{ + "data": { + "network": 30, + "nonce": "12", + "gasPrice": 5, + "gasLimit": 21000, + "value": "10000000000000000000", + "recipientAddress": "0x07Ac3E438719be72a9e2591bB6015F10E8Af2468", + "data": "", + "signature": "b3bc84c8caf1b75c18a78dde87df9f555161003d341eafad659ab672501185e413a26284c3c95056809c7d440c4ffab26179c538864c4d14534ebd5a961852bf01", + "senderPublicKey": "023efc1da7f315f3c533a4080e491f32cd4219731cef008976c3876539e1f192d3", + "senderAddress": "0x6F0182a0cc707b055322CcF6d4CB6a5Aff1aEb22", + "id": "b5d7b17d30da123d9eebc8bb6012c1a4e950e1dad2b080404bb052c30b8a8b2e" + }, + "serialized": "1e0c0000000000000005000000085200000000000000000000000000000000000000000000000000008ac7230489e800000107ac3e438719be72a9e2591bb6015f10e8af246800000000b3bc84c8caf1b75c18a78dde87df9f555161003d341eafad659ab672501185e413a26284c3c95056809c7d440c4ffab26179c538864c4d14534ebd5a961852bf01" +} diff --git a/tests/fixtures/unvote.json b/tests/fixtures/unvote.json new file mode 100644 index 00000000..d5e3a7cf --- /dev/null +++ b/tests/fixtures/unvote.json @@ -0,0 +1,16 @@ +{ + "data": { + "network": 30, + "nonce": "13", + "gasPrice": 5, + "gasLimit": 200000, + "value": "0", + "recipientAddress": "0x522B3294E6d06aA25Ad0f1B8891242E335D3B459", + "data": "3174b689", + "signature": "d7534ec92c06a8547d0f2b3d3259dff5b0b17f8673d68dff9af023009c9c450e24205cb5f4fd6165d71c8b3ba3e9f741d1853110d44bd1e798e87f1a5d6a89c501", + "senderPublicKey": "023efc1da7f315f3c533a4080e491f32cd4219731cef008976c3876539e1f192d3", + "senderAddress": "0x6F0182a0cc707b055322CcF6d4CB6a5Aff1aEb22", + "id": "92ca281a6699a4eb08e8e5c4a644c216026f6c6d3560611c50cab54d1300b690" + }, + "serialized": "1e0d0000000000000005000000400d0300000000000000000000000000000000000000000000000000000000000000000001522b3294e6d06aa25ad0f1b8891242e335d3b459040000003174b689d7534ec92c06a8547d0f2b3d3259dff5b0b17f8673d68dff9af023009c9c450e24205cb5f4fd6165d71c8b3ba3e9f741d1853110d44bd1e798e87f1a5d6a89c501" +} diff --git a/tests/fixtures/validator-registration.json b/tests/fixtures/validator-registration.json new file mode 100644 index 00000000..170909cc --- /dev/null +++ b/tests/fixtures/validator-registration.json @@ -0,0 +1,16 @@ +{ + "data": { + "network": 30, + "nonce": "12", + "gasPrice": 5, + "gasLimit": 500000, + "value": "0", + "recipientAddress": "0x522B3294E6d06aA25Ad0f1B8891242E335D3B459", + "data": "602a9eee00000000000000000000000000000000000000000000000000000000000000200000000000000000000000000000000000000000000000000000000000000030a08058db53e2665c84a40f5152e76dd2b652125a6079130d4c315e728bcf4dd1dfb44ac26e82302331d61977d314111800000000000000000000000000000000", + "signature": "91b2ca61808b94392afa151ee893784a5221ab27b8fdf5871cc17c75e87acca8396530b2f320641326f00199478552e673d124406b44bcbe6075966016658d2201", + "senderPublicKey": "023efc1da7f315f3c533a4080e491f32cd4219731cef008976c3876539e1f192d3", + "senderAddress": "0x6F0182a0cc707b055322CcF6d4CB6a5Aff1aEb22", + "id": "3457dfd59d42a174feb30a1aac757e54caddd87d21e6483386a3440cc0fa6c5f" + }, + "serialized": "1e0c000000000000000500000020a10700000000000000000000000000000000000000000000000000000000000000000001522b3294e6d06aa25ad0f1b8891242e335d3b45984000000602a9eee00000000000000000000000000000000000000000000000000000000000000200000000000000000000000000000000000000000000000000000000000000030a08058db53e2665c84a40f5152e76dd2b652125a6079130d4c315e728bcf4dd1dfb44ac26e82302331d61977d31411180000000000000000000000000000000091b2ca61808b94392afa151ee893784a5221ab27b8fdf5871cc17c75e87acca8396530b2f320641326f00199478552e673d124406b44bcbe6075966016658d2201" +} diff --git a/tests/fixtures/validator-resignation.json b/tests/fixtures/validator-resignation.json new file mode 100644 index 00000000..77a37778 --- /dev/null +++ b/tests/fixtures/validator-resignation.json @@ -0,0 +1,16 @@ +{ + "data": { + "network": 30, + "nonce": "12", + "gasPrice": 5, + "gasLimit": 150000, + "value": "0", + "recipientAddress": "0x522B3294E6d06aA25Ad0f1B8891242E335D3B459", + "data": "b85f5da2", + "signature": "94fd248dc5984b56be6c9661c5a32fa062fb21af62b1474a33d985302f9bda8a044c30e4feb1f06da437c15d9e997816aa3233b3f142cd780e1ff69b80269d0d00", + "senderPublicKey": "023efc1da7f315f3c533a4080e491f32cd4219731cef008976c3876539e1f192d3", + "senderAddress": "0x6F0182a0cc707b055322CcF6d4CB6a5Aff1aEb22", + "id": "ab469546888715725add275778bcf0c1dd68afc163b48018e22a044db718e5b9" + }, + "serialized": "1e0c0000000000000005000000f0490200000000000000000000000000000000000000000000000000000000000000000001522b3294e6d06aa25ad0f1b8891242e335d3b45904000000b85f5da294fd248dc5984b56be6c9661c5a32fa062fb21af62b1474a33d985302f9bda8a044c30e4feb1f06da437c15d9e997816aa3233b3f142cd780e1ff69b80269d0d00" +} diff --git a/tests/fixtures/vote.json b/tests/fixtures/vote.json new file mode 100644 index 00000000..d7992974 --- /dev/null +++ b/tests/fixtures/vote.json @@ -0,0 +1,16 @@ +{ + "data": { + "network": 30, + "nonce": "12", + "gasPrice": 5, + "gasLimit": 200000, + "value": "0", + "recipientAddress": "0x522B3294E6d06aA25Ad0f1B8891242E335D3B459", + "data": "6dd7d8ea000000000000000000000000512f366d524157bcf734546eb29a6d687b762255", + "signature": "e1fd7b0ddc466072e2eac37b73283e8303d80ceb2dd2d64a8d6cdf5866662bc5261a08ca2d64942b6bb93b42ed820f1c8c1c92ce2312d380cc83fea022bfc2f301", + "senderPublicKey": "023efc1da7f315f3c533a4080e491f32cd4219731cef008976c3876539e1f192d3", + "senderAddress": "0x6F0182a0cc707b055322CcF6d4CB6a5Aff1aEb22", + "id": "749744e0d689c46e37ff2993a984599eac4989a9ef0028337b335c9d43abf936" + }, + "serialized": "1e0c0000000000000005000000400d0300000000000000000000000000000000000000000000000000000000000000000001522b3294e6d06aa25ad0f1b8891242e335d3b459240000006dd7d8ea000000000000000000000000512f366d524157bcf734546eb29a6d687b762255e1fd7b0ddc466072e2eac37b73283e8303d80ceb2dd2d64a8d6cdf5866662bc5261a08ca2d64942b6bb93b42ed820f1c8c1c92ce2312d380cc83fea022bfc2f301" +} diff --git a/tests/transactions/builder/test_transfer.py b/tests/transactions/builder/test_transfer.py deleted file mode 100644 index 0504dccd..00000000 --- a/tests/transactions/builder/test_transfer.py +++ /dev/null @@ -1,171 +0,0 @@ -import pytest - -from crypto.configuration.network import set_network -from crypto.constants import TRANSACTION_TRANSFER, TRANSACTION_TYPE_GROUP -from crypto.identity.public_key import PublicKey -from crypto.networks.devnet import Devnet -from crypto.transactions.builder.transfer import Transfer - - -set_network(Devnet) - - -def test_transfer_transaction(passphrase): - """Test if a transfer transaction gets built - """ - transaction = Transfer( - recipientId='0x6F0182a0cc707b055322CcF6d4CB6a5Aff1aEb22', - amount=1, - fee=10000000, - ) - transaction.set_type_group(TRANSACTION_TYPE_GROUP.CORE) - transaction.set_nonce(8) - transaction.sign(passphrase) - transaction_dict = transaction.to_dict() - - print(transaction_dict, transaction) - - assert transaction_dict['version'] == 1 - assert transaction_dict['nonce'] == 8 - assert transaction_dict['type'] is TRANSACTION_TRANSFER - assert transaction_dict['typeGroup'] == 1 - assert transaction_dict['typeGroup'] == TRANSACTION_TYPE_GROUP.CORE.value - assert transaction_dict['fee'] == 10000000 - assert transaction_dict['amount'] == 1 - assert transaction_dict['expiration'] == 0 - assert transaction_dict['senderPublicKey'] == '023efc1da7f315f3c533a4080e491f32cd4219731cef008976c3876539e1f192d3' - - transaction.schnorr_verify() # if no exception is raised, it means the transaction is valid - - -def test_transfer_transaction_update_amount(passphrase): - """Test if a transfer transaction can update an amount - """ - transaction = Transfer( - recipientId='0x6F0182a0cc707b055322CcF6d4CB6a5Aff1aEb22', - amount=200000000 - ) - transaction.set_amount(10) - transaction.set_type_group(TRANSACTION_TYPE_GROUP.CORE) - transaction.set_nonce(1) - transaction.sign(passphrase) - transaction_dict = transaction.to_dict() - - assert transaction_dict['nonce'] == 1 - assert transaction_dict['signature'] - assert transaction_dict['type'] is TRANSACTION_TRANSFER - assert transaction_dict['typeGroup'] == 1 - assert transaction_dict['typeGroup'] == TRANSACTION_TYPE_GROUP.CORE.value - assert transaction_dict['amount'] == 10 - assert transaction_dict['expiration'] == 0 - assert transaction_dict['senderPublicKey'] == '023efc1da7f315f3c533a4080e491f32cd4219731cef008976c3876539e1f192d3' - - transaction.schnorr_verify() # if no exception is raised, it means the transaction is valid - - -def test_transfer_transaction_custom_fee(passphrase): - """Test if a transfer transaction gets built with a custom fee - """ - transaction = Transfer( - recipientId='0x6F0182a0cc707b055322CcF6d4CB6a5Aff1aEb22', - amount=200000000, - fee=5 - ) - transaction.set_type_group(TRANSACTION_TYPE_GROUP.CORE) - transaction.set_nonce(1) - transaction.sign(passphrase) - transaction_dict = transaction.to_dict() - - assert transaction_dict['nonce'] == 1 - assert transaction_dict['signature'] - assert transaction_dict['type'] is TRANSACTION_TRANSFER - assert transaction_dict['typeGroup'] == TRANSACTION_TYPE_GROUP.CORE.value - assert transaction_dict['fee'] == 5 - assert transaction_dict['expiration'] == 0 - assert transaction_dict['senderPublicKey'] == '023efc1da7f315f3c533a4080e491f32cd4219731cef008976c3876539e1f192d3' - - transaction.schnorr_verify() # if no exception is raised, it means the transaction is valid - - -def test_transfer_secondsign_transaction(passphrase): - """Test if a transfer transaction with second signature gets built - """ - transaction = Transfer( - recipientId='0x6F0182a0cc707b055322CcF6d4CB6a5Aff1aEb22', - amount=200000000, - ) - transaction.set_type_group(TRANSACTION_TYPE_GROUP.CORE) - transaction.set_nonce(1) - transaction.sign(passphrase) - transaction.second_sign('second top secret passphrase') - transaction_dict = transaction.to_dict() - - assert transaction_dict['nonce'] == 1 - assert transaction_dict['signature'] - assert transaction_dict['signSignature'] - assert transaction_dict['type'] is TRANSACTION_TRANSFER - assert transaction_dict['typeGroup'] == TRANSACTION_TYPE_GROUP.CORE.value - assert transaction_dict['expiration'] == 0 - assert transaction_dict['senderPublicKey'] == '023efc1da7f315f3c533a4080e491f32cd4219731cef008976c3876539e1f192d3' - - transaction.schnorr_verify() # if no exception is raised, it means the transaction is valid - transaction.verify_secondsig_schnorr(PublicKey.from_passphrase('second top secret passphrase')) # if no exception is raised, it means the transaction is valid - - -def test_parse_signatures(transaction_type_0): - """Test if parse signature works when parsing serialized data - """ - transfer = Transfer( - recipientId=transaction_type_0['recipientId'], - amount=transaction_type_0['amount'] - ) - assert transfer.transaction.signature is None - transfer.transaction.parse_signatures(transaction_type_0['serialized'], 166) - assert transfer.transaction.signature - - -def test_transfer_transaction_amount_not_int(): - with pytest.raises(ValueError): - """Test error handling in constructor for non-integer amount - """ - Transfer( - recipientId='0x6F0182a0cc707b055322CcF6d4CB6a5Aff1aEb22', - amount='bad amount' - ) - - -def test_transfer_transaction_amount_zero(): - with pytest.raises(ValueError): - """Test error handling in constructor for non-integer amount - """ - Transfer( - recipientId='0x6F0182a0cc707b055322CcF6d4CB6a5Aff1aEb22', - amount=0 - ) - - -def test_transfer_serialize(passphrase): - transaction = Transfer( - recipientId='0x6F0182a0cc707b055322CcF6d4CB6a5Aff1aEb22', - amount=1, - fee=10000000, - ) - transaction.set_nonce(6) - transaction.sign(passphrase) - transaction.transaction.signature = '42faaaf6b5b5eff5bb78c7bb2b116ecbc0a83f53445b801818b72afb34b39226646608d5e7048c12d6aedcebfc3156f035b57ca70c6a5e899b7ac2a1be163bb0' - - assert transaction.serialize(False, False, False) == 'ff011e0100000000000600000000000000023efc1da7f315f3c533a4080e491f32cd4219731cef008976c3876539e1f192d38096980000000000000100000000000000000000006f0182a0cc707b055322ccf6d4cb6a5aff1aeb2242faaaf6b5b5eff5bb78c7bb2b116ecbc0a83f53445b801818b72afb34b39226646608d5e7048c12d6aedcebfc3156f035b57ca70c6a5e899b7ac2a1be163bb0' - - -def test_transfer_serialize_without_signatures(passphrase): - transaction = Transfer( - recipientId='0x6F0182a0cc707b055322CcF6d4CB6a5Aff1aEb22', - amount=1, - fee=10000000, - ) - transaction.set_nonce(6) - transaction.sign(passphrase) - transaction.second_sign(passphrase) - transaction.transaction.signature = '42faaaf6b5b5eff5bb78c7bb2b116ecbc0a83f53445b801818b72afb34b39226646608d5e7048c12d6aedcebfc3156f035b57ca70c6a5e899b7ac2a1be163bb0' - - assert transaction.serialize(True, True, True) == 'ff011e0100000000000600000000000000023efc1da7f315f3c533a4080e491f32cd4219731cef008976c3876539e1f192d38096980000000000000100000000000000000000006f0182a0cc707b055322ccf6d4cb6a5aff1aeb22' diff --git a/tests/transactions/builder/test_transfer_builder.py b/tests/transactions/builder/test_transfer_builder.py new file mode 100644 index 00000000..36b5abb6 --- /dev/null +++ b/tests/transactions/builder/test_transfer_builder.py @@ -0,0 +1,42 @@ +import pytest +import json +import os + +from crypto.transactions.builder.transfer_builder import TransferBuilder +from crypto.configuration.network import set_network +from crypto.networks.devnet import Devnet + +set_network(Devnet) + + +def get_transaction_fixture(fixture_name): + fixtures_path = os.path.join( + os.path.dirname(__file__), + '../../fixtures', + f'{fixture_name}.json' + ) + with open(fixtures_path, 'r') as f: + return json.load(f) + + +def test_transfer_transaction(passphrase): + fixture = get_transaction_fixture('transfer') + + builder = ( + TransferBuilder() + .gas_price(fixture['data']['gasPrice']) + .nonce(fixture['data']['nonce']) + .network(fixture['data']['network']) + .gas_limit(fixture['data']['gasLimit']) + .recipient_address(fixture['data']['recipientAddress']) + .value(fixture['data']['value']) + .sign(passphrase) + ) + + print(builder.transaction.serialize().hex()) + print(fixture['serialized']) + + + assert builder.transaction.serialize().hex() == fixture['serialized'] + assert builder.transaction.data['id'] == fixture['data']['id'] + assert builder.verify()