From efbc2d2308043a4ec9268893ef7266493ef44939 Mon Sep 17 00:00:00 2001 From: Hareem Adderley Date: Mon, 26 Feb 2024 00:39:49 -0500 Subject: [PATCH] Pool certificates (#321) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * feat: add cardano-cli chain context * fix: allow instances of str to submit_tx_cbor * fix: cast to int for asset amount and check for None in get_min_utxo * test: add test for cardano-cli chain context * Black formatting * Fix some QA issues * refactor: use `--out-file /dev/stdout` to get utxo data as json * fix: remove unused offline/online mode code * fix: remove unused fraction parser method * fix: add docker configuration to use cardano-cli in a Docker container and network args method to use custom networks * test: add integration tests for cardano-cli * test: fix cardano-node container name * feat: add initial functionality for pool certificates * test: add some tests for pool certificates * refactor: use built in fractions module * fix: output PoolRegistration as flat list * fix: clean up some code * test: add tests for pool params * Add more integration tests for cardano cli context * feat: add stake pool key pairs * fix: resolve mypy and black linting issues * feat: add witness count override for fee estimation add initial stake pool registration flag and deposit add pool vkey hashes if certificate exists * chore: add integration test temporary folders to ignore * test: add test for pool certificate related code * Simplify Certificate deserialization * Fix failing test cases for python<3.10 Syntax "Optional[type1 | type2]" is not supported in version <= 3.9 * Simplify relay parsing * Remove unused import --------- Co-authored-by: Hareem Adderley Co-authored-by: Niels Mündler Co-authored-by: Jerry --- .gitignore | 4 +- pycardano/backend/cardano_cli.py | 2 +- pycardano/certificate.py | 128 +++++++++- pycardano/cip/cip14.py | 1 + pycardano/hash.py | 29 ++- pycardano/key.py | 37 +++ pycardano/pool_params.py | 260 +++++++++++++++++++ pycardano/transaction.py | 6 +- pycardano/txbuilder.py | 38 ++- pycardano/witness.py | 65 ++++- test/conftest.py | 51 ++++ test/pycardano/backend/conftest.py | 10 +- test/pycardano/backend/test_cardano_cli.py | 6 +- test/pycardano/test_certificate.py | 82 +++++- test/pycardano/test_key.py | 81 +++++- test/pycardano/test_pool_params.py | 277 +++++++++++++++++++++ test/pycardano/test_serialization.py | 3 +- test/pycardano/test_txbuilder.py | 99 ++++++-- test/pycardano/util.py | 2 +- test/resources/keys/cold.skey | 5 + test/resources/keys/cold.vkey | 5 + 21 files changed, 1144 insertions(+), 47 deletions(-) create mode 100644 pycardano/pool_params.py create mode 100644 test/conftest.py create mode 100644 test/pycardano/test_pool_params.py create mode 100644 test/resources/keys/cold.skey create mode 100644 test/resources/keys/cold.vkey diff --git a/.gitignore b/.gitignore index 88a8b689..b5e58dea 100644 --- a/.gitignore +++ b/.gitignore @@ -6,4 +6,6 @@ dist # IDE .idea -.code \ No newline at end of file +.code +/integration-test/.env +/integration-test/tmp_configs/* diff --git a/pycardano/backend/cardano_cli.py b/pycardano/backend/cardano_cli.py index cba4f3b6..5215adcc 100644 --- a/pycardano/backend/cardano_cli.py +++ b/pycardano/backend/cardano_cli.py @@ -31,7 +31,7 @@ from pycardano.hash import DatumHash, ScriptHash from pycardano.nativescript import NativeScript from pycardano.network import Network -from pycardano.plutus import PlutusV1Script, PlutusV2Script, RawPlutusData, Datum +from pycardano.plutus import Datum, PlutusV1Script, PlutusV2Script, RawPlutusData from pycardano.serialization import RawCBOR from pycardano.transaction import ( Asset, diff --git a/pycardano/certificate.py b/pycardano/certificate.py index 11b8892a..b866055a 100644 --- a/pycardano/certificate.py +++ b/pycardano/certificate.py @@ -1,8 +1,11 @@ +from __future__ import annotations + from dataclasses import dataclass, field -from typing import Optional, Union +from typing import Optional, Tuple, Type, Union +from pycardano.exception import DeserializeException from pycardano.hash import PoolKeyHash, ScriptHash, VerificationKeyHash -from pycardano.serialization import ArrayCBORSerializable +from pycardano.serialization import ArrayCBORSerializable, limit_primitive_type __all__ = [ "Certificate", @@ -10,8 +13,14 @@ "StakeRegistration", "StakeDeregistration", "StakeDelegation", + "PoolRegistration", + "PoolRetirement", ] +from pycardano.pool_params import PoolParams + +unit_interval = Tuple[int, int] + @dataclass(repr=False) class StakeCredential(ArrayCBORSerializable): @@ -25,6 +34,18 @@ def __post_init__(self): else: self._CODE = 1 + @classmethod + @limit_primitive_type(list) + def from_primitive( + cls: Type[StakeCredential], values: Union[list, tuple] + ) -> StakeCredential: + if values[0] == 0: + return cls(VerificationKeyHash(values[1])) + elif values[0] == 1: + return cls(ScriptHash(values[1])) + else: + raise DeserializeException(f"Invalid StakeCredential type {values[0]}") + @dataclass(repr=False) class StakeRegistration(ArrayCBORSerializable): @@ -32,6 +53,16 @@ class StakeRegistration(ArrayCBORSerializable): stake_credential: StakeCredential + def __post_init__(self): + self._CODE = 0 + + @classmethod + @limit_primitive_type(list) + def from_primitive( + cls: Type[StakeRegistration], values: Union[list, tuple] + ) -> StakeRegistration: + return cls(stake_credential=StakeCredential.from_primitive(values[1])) + @dataclass(repr=False) class StakeDeregistration(ArrayCBORSerializable): @@ -39,6 +70,19 @@ class StakeDeregistration(ArrayCBORSerializable): stake_credential: StakeCredential + def __post_init__(self): + self._CODE = 1 + + @classmethod + @limit_primitive_type(list) + def from_primitive( + cls: Type[StakeDeregistration], values: Union[list, tuple] + ) -> StakeDeregistration: + if values[0] == 1: + return cls(StakeCredential.from_primitive(values[1])) + else: + raise DeserializeException(f"Invalid StakeDeregistration type {values[0]}") + @dataclass(repr=False) class StakeDelegation(ArrayCBORSerializable): @@ -48,5 +92,83 @@ class StakeDelegation(ArrayCBORSerializable): pool_keyhash: PoolKeyHash + def __post_init__(self): + self._CODE = 2 + + @classmethod + @limit_primitive_type(list) + def from_primitive( + cls: Type[StakeDelegation], values: Union[list, tuple] + ) -> StakeDelegation: + if values[0] == 2: + return cls( + stake_credential=StakeCredential.from_primitive(values[1]), + pool_keyhash=PoolKeyHash.from_primitive(values[2]), + ) + else: + raise DeserializeException(f"Invalid StakeDelegation type {values[0]}") + + +@dataclass(repr=False) +class PoolRegistration(ArrayCBORSerializable): + _CODE: int = field(init=False, default=3) + + pool_params: PoolParams + + def __post_init__(self): + self._CODE = 3 + + def to_primitive(self): + pool_params = self.pool_params.to_primitive() + if isinstance(pool_params, list): + return [self._CODE, *pool_params] + return super().to_primitive() + + @classmethod + @limit_primitive_type(list) + def from_primitive( + cls: Type[PoolRegistration], values: Union[list, tuple] + ) -> PoolRegistration: + if values[0] == 3: + if isinstance(values[1], list): + return cls( + pool_params=PoolParams.from_primitive(values[1]), + ) + else: + return cls( + pool_params=PoolParams.from_primitive(values[1:]), + ) + else: + raise DeserializeException(f"Invalid PoolRegistration type {values[0]}") + + +@dataclass(repr=False) +class PoolRetirement(ArrayCBORSerializable): + _CODE: int = field(init=False, default=4) + + pool_keyhash: PoolKeyHash + epoch: int + + def __post_init__(self): + self._CODE = 4 + + @classmethod + @limit_primitive_type(list) + def from_primitive( + cls: Type[PoolRetirement], values: Union[list, tuple] + ) -> PoolRetirement: + if values[0] == 4: + return cls( + pool_keyhash=PoolKeyHash.from_primitive(values[1]), epoch=values[2] + ) + else: + raise DeserializeException(f"Invalid PoolRetirement type {values[0]}") + -Certificate = Union[StakeRegistration, StakeDeregistration, StakeDelegation] +Certificate = Union[ + StakeRegistration, + StakeDeregistration, + StakeDelegation, + PoolRegistration, + PoolRetirement, +] diff --git a/pycardano/cip/cip14.py b/pycardano/cip/cip14.py index 6126f23a..88951452 100644 --- a/pycardano/cip/cip14.py +++ b/pycardano/cip/cip14.py @@ -2,6 +2,7 @@ from nacl.encoding import RawEncoder from nacl.hash import blake2b + from pycardano.crypto.bech32 import encode from pycardano.hash import ScriptHash from pycardano.transaction import AssetName diff --git a/pycardano/hash.py b/pycardano/hash.py index f8034fd5..e4023b8a 100644 --- a/pycardano/hash.py +++ b/pycardano/hash.py @@ -12,6 +12,9 @@ "AUXILIARY_DATA_HASH_SIZE", "POOL_KEY_HASH_SIZE", "SCRIPT_DATA_HASH_SIZE", + "VRF_KEY_HASH_SIZE", + "POOL_METADATA_HASH_SIZE", + "REWARD_ACCOUNT_HASH_SIZE", "ConstrainedBytes", "VerificationKeyHash", "ScriptHash", @@ -20,6 +23,9 @@ "DatumHash", "AuxiliaryDataHash", "PoolKeyHash", + "PoolMetadataHash", + "VrfKeyHash", + "RewardAccountHash", ] VERIFICATION_KEY_HASH_SIZE = 28 @@ -29,6 +35,9 @@ DATUM_HASH_SIZE = 32 AUXILIARY_DATA_HASH_SIZE = 32 POOL_KEY_HASH_SIZE = 28 +POOL_METADATA_HASH_SIZE = 32 +VRF_KEY_HASH_SIZE = 32 +REWARD_ACCOUNT_HASH_SIZE = 29 T = TypeVar("T", bound="ConstrainedBytes") @@ -124,7 +133,25 @@ class AuxiliaryDataHash(ConstrainedBytes): MAX_SIZE = MIN_SIZE = AUXILIARY_DATA_HASH_SIZE -class PoolKeyHash(ConstrainedBytes): +class PoolKeyHash(VerificationKeyHash): """Hash of a stake pool""" MAX_SIZE = MIN_SIZE = POOL_KEY_HASH_SIZE + + +class PoolMetadataHash(ConstrainedBytes): + """Hash of a stake pool metadata""" + + MAX_SIZE = MIN_SIZE = POOL_METADATA_HASH_SIZE + + +class VrfKeyHash(ConstrainedBytes): + """Hash of a Cardano VRF key.""" + + MAX_SIZE = MIN_SIZE = VRF_KEY_HASH_SIZE + + +class RewardAccountHash(ConstrainedBytes): + """Hash of a Cardano VRF key.""" + + MAX_SIZE = MIN_SIZE = REWARD_ACCOUNT_HASH_SIZE diff --git a/pycardano/key.py b/pycardano/key.py index 335a5649..35cce25a 100644 --- a/pycardano/key.py +++ b/pycardano/key.py @@ -32,6 +32,9 @@ "StakeSigningKey", "StakeVerificationKey", "StakeKeyPair", + "StakePoolSigningKey", + "StakePoolVerificationKey", + "StakePoolKeyPair", ] @@ -314,3 +317,37 @@ def from_signing_key( cls: Type[StakeKeyPair], signing_key: SigningKey ) -> StakeKeyPair: return cls(signing_key, StakeVerificationKey.from_signing_key(signing_key)) + + +class StakePoolSigningKey(SigningKey): + KEY_TYPE = "StakePoolSigningKey_ed25519" + DESCRIPTION = "Stake Pool Operator Signing Key" + + +class StakePoolVerificationKey(VerificationKey): + KEY_TYPE = "StakePoolVerificationKey_ed25519" + DESCRIPTION = "Stake Pool Operator Verification Key" + + +class StakePoolKeyPair: + def __init__(self, signing_key: SigningKey, verification_key: VerificationKey): + self.signing_key = signing_key + self.verification_key = verification_key + + @classmethod + def generate(cls: Type[StakePoolKeyPair]) -> StakePoolKeyPair: + signing_key = StakePoolSigningKey.generate() + return cls.from_signing_key(signing_key) + + @classmethod + def from_signing_key( + cls: Type[StakePoolKeyPair], signing_key: SigningKey + ) -> StakePoolKeyPair: + return cls(signing_key, StakePoolVerificationKey.from_signing_key(signing_key)) + + def __eq__(self, other): + if isinstance(other, StakePoolKeyPair): + return ( + other.signing_key == self.signing_key + and other.verification_key == self.verification_key + ) diff --git a/pycardano/pool_params.py b/pycardano/pool_params.py new file mode 100644 index 00000000..126a5d58 --- /dev/null +++ b/pycardano/pool_params.py @@ -0,0 +1,260 @@ +""" +Pool parameters for stake pool registration certificate. +""" +from __future__ import annotations + +import socket +from dataclasses import dataclass, field +from fractions import Fraction +from typing import List, Optional, Type, Union + +from pycardano.crypto.bech32 import bech32_decode +from pycardano.exception import DeserializeException +from pycardano.hash import ( + PoolKeyHash, + PoolMetadataHash, + RewardAccountHash, + VerificationKeyHash, + VrfKeyHash, +) +from pycardano.serialization import ( + ArrayCBORSerializable, + CBORSerializable, + limit_primitive_type, +) + +__all__ = [ + "PoolId", + "PoolMetadata", + "PoolParams", + "Relay", + "SingleHostAddr", + "SingleHostName", + "MultiHostName", + "is_bech32_cardano_pool_id", +] + + +def is_bech32_cardano_pool_id(pool_id: str) -> bool: + """Check if a string is a valid Cardano stake pool ID in bech32 format.""" + if pool_id is None or not pool_id.startswith("pool"): + return False + return bech32_decode(pool_id) != (None, None, None) + + +@dataclass(frozen=True) +class PoolId(CBORSerializable): + value: str + + def __post_init__(self): + if not is_bech32_cardano_pool_id(self.value): + raise ValueError( + "Invalid PoolId format. The PoolId should be a valid Cardano stake pool ID in bech32 format." + ) + + def __str__(self): + return self.value + + def __repr__(self): + return self.value + + def to_primitive(self) -> str: + return self.value + + @classmethod + @limit_primitive_type(str) + def from_primitive(cls: Type[PoolId], value: str) -> PoolId: + return cls(value) + + +@dataclass(repr=False) +class SingleHostAddr(ArrayCBORSerializable): + _CODE: int = field(init=False, default=0) + + port: Optional[int] + ipv4: Optional[Union[str, bytes]] + ipv6: Optional[Union[str, bytes]] + + def __init__( + self, + port: Optional[int] = None, + ipv4: Optional[Union[str, bytes]] = None, + ipv6: Optional[Union[str, bytes]] = None, + ): + super().__init__() + + self._CODE = 0 + self.port = port + + self.ipv4 = self.bytes_to_ipv4(ipv4) + self.ipv6 = self.bytes_to_ipv6(ipv6) + + @staticmethod + def ipv4_to_bytes(ip_address: Optional[str | bytes] = None) -> bytes | None: + """ + Convert IPv4 address to bytes format. + Args: + ip_address: The IPv4 address in human-readable format. + + Returns: + bytes: IPv4 address in bytes format. + """ + if isinstance(ip_address, str): + return socket.inet_aton(ip_address) + elif isinstance(ip_address, bytes): + return ip_address + else: + return None + + @staticmethod + def ipv6_to_bytes(ip_address: Optional[str | bytes] = None) -> bytes | None: + """ + Convert IPv6 address to bytes format. + Args: + ip_address: The IPv6 address in human-readable format. + + Returns: + The IPv6 address in bytes format. + """ + if isinstance(ip_address, str): + return socket.inet_pton(socket.AF_INET6, ip_address) + elif isinstance(ip_address, bytes): + return ip_address + else: + return None + + @staticmethod + def bytes_to_ipv4(bytes_ip_address: Optional[str | bytes] = None) -> str | None: + """ + Convert IPv4 address in bytes to human-readable format. + Args: + bytes_ip_address: The IPv4 address in bytes format. + Returns: + The IPv4 address in human-readable format. + """ + if isinstance(bytes_ip_address, str): + return bytes_ip_address + elif isinstance(bytes_ip_address, bytes): + return socket.inet_ntoa(bytes_ip_address) + else: + return None + + @staticmethod + def bytes_to_ipv6(bytes_ip_address: Optional[str | bytes] = None) -> str | None: + """ + Convert IPv6 address in bytes to human-readable format. + Args: + bytes_ip_address: The IPv6 address in bytes format. + Returns: + The IPv6 address in human-readable format. + """ + if isinstance(bytes_ip_address, str): + return bytes_ip_address + elif isinstance(bytes_ip_address, bytes): + return socket.inet_ntop(socket.AF_INET6, bytes_ip_address) + else: + return None + + def to_primitive(self) -> list: + return [ + self._CODE, + self.port, + self.ipv4_to_bytes(self.ipv4), + self.ipv6_to_bytes(self.ipv6), + ] + + @classmethod + @limit_primitive_type(list) + def from_primitive( + cls: Type[SingleHostAddr], values: Union[list, tuple] + ) -> SingleHostAddr: + if values[0] == 0: + return cls( + port=values[1], + ipv4=values[2], + ipv6=values[3], + ) + else: + raise DeserializeException(f"Invalid SingleHostAddr type {values[0]}") + + +@dataclass(repr=False) +class SingleHostName(ArrayCBORSerializable): + _CODE: int = field(init=False, default=1) + + port: Optional[int] + dns_name: Optional[str] + + def __post_init__(self): + self._CODE = 1 + + @classmethod + @limit_primitive_type(list) + def from_primitive( + cls: Type[SingleHostName], values: Union[list, tuple] + ) -> SingleHostName: + if values[0] == 1: + return cls( + port=values[1], + dns_name=values[2], + ) + else: + raise DeserializeException(f"Invalid SingleHostName type {values[0]}") + + +@dataclass(repr=False) +class MultiHostName(ArrayCBORSerializable): + _CODE: int = field(init=False, default=2) + + dns_name: Optional[str] + + def __post_init__(self): + self._CODE = 2 + + @classmethod + @limit_primitive_type(list) + def from_primitive( + cls: Type[MultiHostName], values: Union[list, tuple] + ) -> MultiHostName: + if values[0] == 2: + return cls( + dns_name=values[1], + ) + else: + raise DeserializeException(f"Invalid MultiHostName type {values[0]}") + + +Relay = Union[SingleHostAddr, SingleHostName, MultiHostName] + + +@dataclass(repr=False) +class PoolMetadata(ArrayCBORSerializable): + url: str + pool_metadata_hash: PoolMetadataHash + + +def fraction_parser(fraction: Union[Fraction, str, list]) -> Fraction: + if isinstance(fraction, Fraction): + return Fraction(int(fraction.numerator), int(fraction.denominator)) + elif isinstance(fraction, str): + numerator, denominator = fraction.split("/") + return Fraction(int(numerator), int(denominator)) + elif isinstance(fraction, list): + numerator, denominator = fraction[1] + return Fraction(int(numerator), int(denominator)) + else: + raise ValueError(f"Invalid fraction type {fraction}") + + +@dataclass(repr=False) +class PoolParams(ArrayCBORSerializable): + operator: PoolKeyHash + vrf_keyhash: VrfKeyHash + pledge: int + cost: int + margin: Fraction = field(metadata={"object_hook": fraction_parser}) + reward_account: RewardAccountHash + pool_owners: List[VerificationKeyHash] + relays: Optional[List[Relay]] = None + pool_metadata: Optional[PoolMetadata] = None + id: Optional[PoolId] = field(default=None, metadata={"optional": True}) diff --git a/pycardano/transaction.py b/pycardano/transaction.py index fa324059..3b961e93 100644 --- a/pycardano/transaction.py +++ b/pycardano/transaction.py @@ -504,7 +504,11 @@ class TransactionBody(MapCBORSerializable): ttl: Optional[int] = field(default=None, metadata={"key": 3, "optional": True}) certificates: Optional[List[Certificate]] = field( - default=None, metadata={"key": 4, "optional": True} + default=None, + metadata={ + "key": 4, + "optional": True, + }, ) withdraws: Optional[Withdrawals] = field( diff --git a/pycardano/txbuilder.py b/pycardano/txbuilder.py index ab27a780..52a0f989 100644 --- a/pycardano/txbuilder.py +++ b/pycardano/txbuilder.py @@ -8,6 +8,8 @@ from pycardano.backend.base import ChainContext from pycardano.certificate import ( Certificate, + PoolRegistration, + PoolRetirement, StakeCredential, StakeDelegation, StakeDeregistration, @@ -113,6 +115,10 @@ class TransactionBuilder: init=False, default_factory=lambda: set() ) + witness_override: Optional[int] = field(default=None) + + initial_stake_pool_registration: Optional[bool] = field(default=False) + _inputs: List[UTxO] = field(init=False, default_factory=lambda: []) _potential_inputs: List[UTxO] = field(init=False, default_factory=lambda: []) @@ -737,15 +743,35 @@ def _check_and_add_vkey(stake_credential: StakeCredential): cert, (StakeRegistration, StakeDeregistration, StakeDelegation) ): _check_and_add_vkey(cert.stake_credential) + elif isinstance(cert, PoolRegistration): + results.add(cert.pool_params.operator) + elif isinstance(cert, PoolRetirement): + results.add(cert.pool_keyhash) return results def _get_total_key_deposit(self): - results = set() + stake_registration_certs = set() + stake_pool_registration_certs = set() + + protocol_params = self.context.protocol_param + if self.certificates: for cert in self.certificates: if isinstance(cert, StakeRegistration): - results.add(cert.stake_credential.credential) - return self.context.protocol_param.key_deposit * len(results) + stake_registration_certs.add(cert.stake_credential.credential) + elif ( + isinstance(cert, PoolRegistration) + and self.initial_stake_pool_registration + ): + stake_pool_registration_certs.add(cert.pool_params.operator) + + stake_registration_deposit = protocol_params.key_deposit * len( + stake_registration_certs + ) + stake_pool_registration_deposit = protocol_params.pool_deposit * len( + stake_pool_registration_certs + ) + return stake_registration_deposit + stake_pool_registration_deposit def _withdrawal_vkey_hashes(self) -> Set[VerificationKeyHash]: results = set() @@ -845,8 +871,12 @@ def _build_fake_vkey_witnesses(self) -> List[VerificationKeyWitness]: vkey_hashes.update(self._native_scripts_vkey_hashes()) vkey_hashes.update(self._certificate_vkey_hashes()) vkey_hashes.update(self._withdrawal_vkey_hashes()) + + witness_count = self.witness_override or len(vkey_hashes) + return [ - VerificationKeyWitness(FAKE_VKEY, FAKE_TX_SIGNATURE) for _ in vkey_hashes + VerificationKeyWitness(FAKE_VKEY, FAKE_TX_SIGNATURE) + for _ in range(witness_count) ] def _build_fake_witness_set(self) -> TransactionWitnessSet: diff --git a/pycardano/witness.py b/pycardano/witness.py index 227cdefc..4fca9dae 100644 --- a/pycardano/witness.py +++ b/pycardano/witness.py @@ -1,7 +1,8 @@ """Transaction witness.""" +from __future__ import annotations from dataclasses import dataclass, field -from typing import Any, List, Optional, Union +from typing import Any, List, Optional, Type, Union from pycardano.key import ExtendedVerificationKey, VerificationKey from pycardano.nativescript import NativeScript @@ -9,6 +10,7 @@ from pycardano.serialization import ( ArrayCBORSerializable, MapCBORSerializable, + limit_primitive_type, list_hook, ) @@ -26,6 +28,16 @@ def __post_init__(self): if isinstance(self.vkey, ExtendedVerificationKey): self.vkey = self.vkey.to_non_extended() + @classmethod + @limit_primitive_type(list) + def from_primitive( + cls: Type[VerificationKeyWitness], values: Union[list, tuple] + ) -> VerificationKeyWitness: + return cls( + vkey=VerificationKey.from_primitive(values[0]), + signature=values[1], + ) + @dataclass(repr=False) class TransactionWitnessSet(MapCBORSerializable): @@ -65,3 +77,54 @@ class TransactionWitnessSet(MapCBORSerializable): default=None, metadata={"optional": True, "key": 5, "object_hook": list_hook(Redeemer)}, ) + + @classmethod + @limit_primitive_type(dict, list) + def from_primitive( + cls: Type[TransactionWitnessSet], values: Union[dict, list, tuple] + ) -> TransactionWitnessSet | None: + def _get_vkey_witnesses(data: Any): + return ( + [VerificationKeyWitness.from_primitive(witness) for witness in data] + if data + else None + ) + + def _get_native_scripts(data: Any): + return ( + [NativeScript.from_primitive(script) for script in data] + if data + else None + ) + + def _get_plutus_v1_scripts(data: Any): + return [PlutusV1Script(script) for script in data] if data else None + + def _get_plutus_v2_scripts(data: Any): + return [PlutusV2Script(script) for script in data] if data else None + + def _get_redeemers(data: Any): + return ( + [Redeemer.from_primitive(redeemer) for redeemer in data] + if data + else None + ) + + def _get_cls(data: Any): + return cls( + vkey_witnesses=_get_vkey_witnesses(data.get(0)), + native_scripts=_get_native_scripts(data.get(1)), + bootstrap_witness=data.get(2), + plutus_v1_script=_get_plutus_v1_scripts(data.get(3)), + plutus_data=data.get(4), + redeemer=_get_redeemers(data.get(5)), + plutus_v2_script=_get_plutus_v2_scripts(data.get(6)), + ) + + if isinstance(values, dict): + return _get_cls(values) + elif isinstance(values, list): + # TODO: May need to handle this differently + values = dict(values) + return _get_cls(values) + return None diff --git a/test/conftest.py b/test/conftest.py new file mode 100644 index 00000000..aff5d585 --- /dev/null +++ b/test/conftest.py @@ -0,0 +1,51 @@ +from fractions import Fraction +from test.pycardano.util import FixedChainContext + +import pytest + +from pycardano import ( + POOL_KEY_HASH_SIZE, + POOL_METADATA_HASH_SIZE, + REWARD_ACCOUNT_HASH_SIZE, + VERIFICATION_KEY_HASH_SIZE, + VRF_KEY_HASH_SIZE, + PoolKeyHash, + PoolMetadataHash, + RewardAccountHash, + VerificationKeyHash, + VrfKeyHash, +) +from pycardano.pool_params import ( + MultiHostName, + PoolMetadata, + PoolParams, + SingleHostAddr, + SingleHostName, +) + + +@pytest.fixture +def chain_context(): + return FixedChainContext() + + +@pytest.fixture +def pool_params(): + return PoolParams( + operator=PoolKeyHash(b"1" * POOL_KEY_HASH_SIZE), + vrf_keyhash=VrfKeyHash(b"1" * VRF_KEY_HASH_SIZE), + pledge=100_000_000, + cost=340_000_000, + margin=Fraction(1, 50), + reward_account=RewardAccountHash(b"1" * REWARD_ACCOUNT_HASH_SIZE), + pool_owners=[VerificationKeyHash(b"1" * VERIFICATION_KEY_HASH_SIZE)], + relays=[ + SingleHostAddr(port=3001, ipv4="192.168.0.1", ipv6="::1"), + SingleHostName(port=3001, dns_name="relay1.example.com"), + MultiHostName(dns_name="relay1.example.com"), + ], + pool_metadata=PoolMetadata( + url="https://meta1.example.com", + pool_metadata_hash=PoolMetadataHash(b"1" * POOL_METADATA_HASH_SIZE), + ), + ) diff --git a/test/pycardano/backend/conftest.py b/test/pycardano/backend/conftest.py index 23571df5..ae878e7e 100644 --- a/test/pycardano/backend/conftest.py +++ b/test/pycardano/backend/conftest.py @@ -90,7 +90,10 @@ def genesis_file(genesis_json): yield genesis_file_path - genesis_file_path.unlink() + try: + genesis_file_path.unlink() + except FileNotFoundError: + pass @pytest.fixture(scope="session") @@ -190,4 +193,7 @@ def config_file(): yield config_file_path - config_file_path.unlink() + try: + config_file_path.unlink() + except FileNotFoundError: + pass diff --git a/test/pycardano/backend/test_cardano_cli.py b/test/pycardano/backend/test_cardano_cli.py index 2d890731..5805ad58 100644 --- a/test/pycardano/backend/test_cardano_cli.py +++ b/test/pycardano/backend/test_cardano_cli.py @@ -9,13 +9,13 @@ ALONZO_COINS_PER_UTXO_WORD, CardanoCliChainContext, CardanoCliNetwork, + DatumHash, GenesisParameters, MultiAsset, - ProtocolParameters, - TransactionInput, PlutusV2Script, + ProtocolParameters, RawPlutusData, - DatumHash, + TransactionInput, ) QUERY_TIP_RESULT = { diff --git a/test/pycardano/test_certificate.py b/test/pycardano/test_certificate.py index 2c30717d..d534de22 100644 --- a/test/pycardano/test_certificate.py +++ b/test/pycardano/test_certificate.py @@ -1,46 +1,77 @@ from pycardano.address import Address from pycardano.certificate import ( - PoolKeyHash, + PoolRegistration, + PoolRetirement, StakeCredential, StakeDelegation, StakeDeregistration, StakeRegistration, ) -from pycardano.hash import POOL_KEY_HASH_SIZE +from pycardano.hash import POOL_KEY_HASH_SIZE, SCRIPT_HASH_SIZE, PoolKeyHash, ScriptHash TEST_ADDR = Address.from_primitive( "stake_test1upyz3gk6mw5he20apnwfn96cn9rscgvmmsxc9r86dh0k66gswf59n" ) -def test_stake_credential(): +def test_stake_credential_verification_key_hash(): stake_credential = StakeCredential(TEST_ADDR.staking_part) + stake_credential_cbor_hex = stake_credential.to_cbor_hex() + assert ( - stake_credential.to_cbor_hex() + stake_credential_cbor_hex == "8200581c4828a2dadba97ca9fd0cdc99975899470c219bdc0d828cfa6ddf6d69" ) + assert StakeCredential.from_cbor(stake_credential_cbor_hex) == stake_credential + + +def test_stake_credential_script_hash(): + stake_credential = StakeCredential(ScriptHash(b"1" * SCRIPT_HASH_SIZE)) + + stake_credential_cbor_hex = stake_credential.to_cbor_hex() + + assert ( + stake_credential_cbor_hex + == "8201581c31313131313131313131313131313131313131313131313131313131" + ) + + assert StakeCredential.from_cbor(stake_credential_cbor_hex) == stake_credential + def test_stake_registration(): stake_credential = StakeCredential(TEST_ADDR.staking_part) stake_registration = StakeRegistration(stake_credential) + stake_registration_cbor_hex = stake_registration.to_cbor_hex() + assert ( - stake_registration.to_cbor_hex() + stake_registration_cbor_hex == "82008200581c4828a2dadba97ca9fd0cdc99975899470c219bdc0d828cfa6ddf6d69" ) + assert ( + StakeRegistration.from_cbor(stake_registration_cbor_hex) == stake_registration + ) + def test_stake_deregistration(): stake_credential = StakeCredential(TEST_ADDR.staking_part) stake_deregistration = StakeDeregistration(stake_credential) + stake_deregistration_cbor_hex = stake_deregistration.to_cbor_hex() + assert ( - stake_deregistration.to_cbor_hex() + stake_deregistration_cbor_hex == "82018200581c4828a2dadba97ca9fd0cdc99975899470c219bdc0d828cfa6ddf6d69" ) + assert ( + StakeDeregistration.from_cbor(stake_deregistration_cbor_hex) + == stake_deregistration + ) + def test_stake_delegation(): stake_credential = StakeCredential(TEST_ADDR.staking_part) @@ -48,8 +79,45 @@ def test_stake_delegation(): stake_credential, PoolKeyHash(b"1" * POOL_KEY_HASH_SIZE) ) + stake_delegation_cbor_hex = stake_delegation.to_cbor_hex() + assert ( - stake_delegation.to_cbor_hex() + stake_delegation_cbor_hex == "83028200581c4828a2dadba97ca9fd0cdc99975899470c219bdc0d828cfa6ddf" "6d69581c31313131313131313131313131313131313131313131313131313131" ) + + assert StakeDelegation.from_cbor(stake_delegation_cbor_hex) == stake_delegation + + +def test_pool_registration(pool_params): + pool_registration = PoolRegistration(pool_params) + + pool_registration_cbor_hex = pool_registration.to_cbor_hex() + + assert ( + pool_registration_cbor_hex + == "8a03581c31313131313131313131313131313131313131313131313131313131582031313131313131313131313131313131313131" + "313131313131313131313131311a05f5e1001a1443fd00d81e82011832581d31313131313131313131313131313131313131313131" + "3131313131313181581c31313131313131313131313131313131313131313131313131313131838400190bb944c0a8000150000000" + "000000000000000000000000018301190bb97272656c6179312e6578616d706c652e636f6d82027272656c6179312e6578616d706c" + "652e636f6d82781968747470733a2f2f6d657461312e6578616d706c652e636f6d5820313131313131313131313131313131313131" + "3131313131313131313131313131" + ) + + assert PoolRegistration.from_cbor(pool_registration_cbor_hex) == pool_registration + + +def test_pool_retirement(): + pool_keyhash = PoolKeyHash(b"1" * POOL_KEY_HASH_SIZE) + epoch = 700 + pool_retirement = PoolRetirement(pool_keyhash, epoch) + + pool_retirement_cbor_hex = pool_retirement.to_cbor_hex() + + assert ( + pool_retirement_cbor_hex + == "8304581c313131313131313131313131313131313131313131313131313131311902bc" + ) + + assert PoolRetirement.from_cbor(pool_retirement_cbor_hex) == pool_retirement diff --git a/test/pycardano/test_key.py b/test/pycardano/test_key.py index 3801f246..ccd34589 100644 --- a/test/pycardano/test_key.py +++ b/test/pycardano/test_key.py @@ -7,6 +7,9 @@ PaymentKeyPair, PaymentSigningKey, PaymentVerificationKey, + StakePoolKeyPair, + StakePoolSigningKey, + StakePoolVerificationKey, ) SK = PaymentSigningKey.from_json( @@ -25,6 +28,22 @@ }""" ) +SPSK = StakePoolSigningKey.from_json( + """{ + "type": "StakePoolSigningKey_ed25519", + "description": "StakePoolSigningKey_ed25519", + "cborHex": "582044181bd0e6be21cea5b0751b8c6d4f88a5cb2d5dfec31a271add617f7ce559a9" + }""" +) + +SPVK = StakePoolVerificationKey.from_json( + """{ + "type": "StakePoolVerificationKey_ed25519", + "description": "StakePoolVerificationKey_ed25519", + "cborHex": "5820354ce32da92e7116f6c70e9be99a3a601d33137d0685ab5b7e2ff5b656989299" + }""" +) + EXTENDED_SK = ExtendedSigningKey.from_json( """{ "type": "PaymentExtendedSigningKeyShelley_ed25519_bip32", @@ -58,6 +77,24 @@ def test_payment_key(): assert PaymentKeyPair.from_signing_key(SK).verification_key.payload == VK.payload +def test_stake_pool_key(): + assert ( + SPSK.payload + == b"D\x18\x1b\xd0\xe6\xbe!\xce\xa5\xb0u\x1b\x8cmO\x88\xa5\xcb-]\xfe\xc3\x1a'\x1a\xdda\x7f|\xe5Y\xa9" + ) + assert ( + SPVK.payload + == b"5L\xe3-\xa9.q\x16\xf6\xc7\x0e\x9b\xe9\x9a:`\x1d3\x13}\x06\x85\xab[~/\xf5\xb6V\x98\x92\x99" + ) + assert ( + SPVK.hash().payload + == b'3/\x13v\xecJi\xe3\x93\xe1\x88`1\x80\xa6\r"\n\x10\xf0<1\xb6)|\xa4c\xb5' + ) + assert ( + StakePoolKeyPair.from_signing_key(SPSK).verification_key.payload == SPVK.payload + ) + + def test_extended_payment_key(): assert EXTENDED_VK == ExtendedVerificationKey.from_signing_key(EXTENDED_SK) @@ -86,12 +123,29 @@ def test_key_pair(): assert PaymentKeyPair(sk, vk) == PaymentKeyPair.from_signing_key(sk) +def test_stake_pool_key_pair(): + sk = StakePoolSigningKey.generate() + vk = StakePoolVerificationKey.from_signing_key(sk) + assert StakePoolKeyPair(sk, vk) == StakePoolKeyPair.from_signing_key(sk) + + def test_key_load(): - sk = PaymentSigningKey.load( + PaymentSigningKey.load( str(pathlib.Path(__file__).parent / "../resources/keys/payment.skey") ) +def test_stake_pool_key_load(): + sk = StakePoolSigningKey.load( + str(pathlib.Path(__file__).parent / "../resources/keys/cold.skey") + ) + vk = StakePoolVerificationKey.load( + str(pathlib.Path(__file__).parent / "../resources/keys/cold.vkey") + ) + assert sk == StakePoolSigningKey.from_json(sk.to_json()) + assert vk == StakePoolVerificationKey.from_json(vk.to_json()) + + def test_key_save(): with tempfile.NamedTemporaryFile() as f: SK.save(f.name) @@ -99,6 +153,16 @@ def test_key_save(): assert SK == sk +def test_stake_pool_key_save(): + with tempfile.NamedTemporaryFile() as skf, tempfile.NamedTemporaryFile() as vkf: + SPSK.save(skf.name) + sk = StakePoolSigningKey.load(skf.name) + SPVK.save(vkf.name) + vk = StakePoolSigningKey.load(vkf.name) + assert SPSK == sk + assert SPVK == vk + + def test_key_hash(): sk = PaymentSigningKey.generate() vk = PaymentVerificationKey.from_signing_key(sk) @@ -112,3 +176,18 @@ def test_key_hash(): assert len(sk_set) == 1 assert len(vk_set) == 1 + + +def test_stake_pool_key_hash(): + sk = StakePoolSigningKey.generate() + vk = StakePoolVerificationKey.from_signing_key(sk) + + sk_set = set() + vk_set = set() + + for _ in range(2): + sk_set.add(sk) + vk_set.add(vk) + + assert len(sk_set) == 1 + assert len(vk_set) == 1 diff --git a/test/pycardano/test_pool_params.py b/test/pycardano/test_pool_params.py new file mode 100644 index 00000000..0f038ccf --- /dev/null +++ b/test/pycardano/test_pool_params.py @@ -0,0 +1,277 @@ +from fractions import Fraction + +import pytest + +from pycardano import ( + POOL_KEY_HASH_SIZE, + POOL_METADATA_HASH_SIZE, + VERIFICATION_KEY_HASH_SIZE, + VRF_KEY_HASH_SIZE, + PoolMetadataHash, +) +from pycardano.hash import ( + REWARD_ACCOUNT_HASH_SIZE, + PoolKeyHash, + RewardAccountHash, + VerificationKeyHash, + VrfKeyHash, +) +from pycardano.pool_params import ( # Fraction, + MultiHostName, + PoolId, + PoolMetadata, + PoolParams, + SingleHostAddr, + SingleHostName, + fraction_parser, + is_bech32_cardano_pool_id, +) + +TEST_POOL_ID = "pool1mt8sdg37f2h3rypyuc77k7vxrjshtvjw04zdjlae9vdzyt9uu34" + + +# Parametrized test for happy path cases +@pytest.mark.parametrize( + "pool_id, expected", + [ + (TEST_POOL_ID, True), + ("pool1234567890abcdef", False), + ("pool1abcdefghijklmnopqrstuvwxyz", False), + ("pool1", False), + ("pool11234567890abcdef", False), + ("pool1abcdefghijklmnopqrstuvwxyz1234", False), + ("pool1!@#$%^&*()-_+={}[]|\\:;\"'<>,.?/", False), + ( + "pool1qqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqq", + False, + ), # One character short + ( + "pool1qqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqq", + False, + ), # One character too long + ("pool1!@#$%^&*()", False), # Invalid characters + ("pool1", False), # Too short + ("", False), # Empty string + (None, False), # None input + ("pool019", False), # Invalid character '1' + ( + "stake1uxtr5m6kygt77399zxqrykkluqr0grr4yrjtl5xplza6k8q5fghrp", + False, + ), # Incorrect HRP + ], +) +def test_is_bech32_cardano_pool_id(pool_id: str, expected: bool): + assert is_bech32_cardano_pool_id(pool_id) == expected + + +def test_pool_id(): + # Act + pool_id = PoolId(TEST_POOL_ID) + + # Assert + assert str(pool_id) == TEST_POOL_ID + assert pool_id.to_primitive() == TEST_POOL_ID + assert str(PoolId.from_primitive(TEST_POOL_ID)) == TEST_POOL_ID + + +# Parametrized test cases for error cases +@pytest.mark.parametrize( + "test_id, pool_id_str, expected_exception", + [ + ("ERR-1", "", ValueError), # Empty string + ("ERR-2", "1234567890", ValueError), # Not a bech32 format + ("ERR-3", "pool123", ValueError), # Too short to be valid + # Add more error cases as needed + ], +) +def test_pool_id_error_cases(test_id, pool_id_str, expected_exception): + # Act & Assert + with pytest.raises(expected_exception): + PoolId(pool_id_str) + + +@pytest.mark.parametrize( + "port, ipv4, ipv6", + [ + ( + 3001, + b"\xC0\xA8\x00\x01", + b" \x01\r\xb8\x85\xa3\x00\x00\x14-\x00\x00\x08\x01\r\xb8", + ), # IPv4 and IPv6 + (None, b"\xC0\xA8\x00\x01", None), # Only IPv4 + ( + None, + None, + b" \x01\r\xb8\x85\xa3\x00\x00\x14-\x00\x00\x08\x01\r\xb8", + ), # Only IPv6 + ], +) +def test_single_host_addr(port, ipv4, ipv6): + # Act + single_host_addr = SingleHostAddr.from_primitive([0, port, ipv4, ipv6]) + + # Assert + assert single_host_addr.port == port + assert single_host_addr.ipv4 == SingleHostAddr.bytes_to_ipv4(ipv4) + assert single_host_addr.ipv6 == SingleHostAddr.bytes_to_ipv6(ipv6) + assert single_host_addr.to_primitive() == [0, port, ipv4, ipv6] + + +@pytest.mark.parametrize( + "port, dns_name", + [ + (80, "example.com"), + (443, "secure.example.com"), + (None, "noport.example.com"), + ], +) +def test_single_host_name(port, dns_name): + # Arrange + primitive_values = [1, port, dns_name] + + # Act + single_host_name = SingleHostName.from_primitive(primitive_values) + + # Assert + assert single_host_name.port == port + assert single_host_name.dns_name == dns_name + assert single_host_name._CODE == 1 + assert single_host_name.to_primitive() == [1, port, dns_name] + + +@pytest.mark.parametrize( + "dns_name", + [ + "example.com", + "secure.example.com", + "noport.example.com", + ], +) +def test_multi_host_name(dns_name): + # Arrange + primitive_values = [2, dns_name] + + # Act + multi_host_name = MultiHostName.from_primitive(primitive_values) + + # Assert + assert multi_host_name.dns_name == dns_name + assert multi_host_name._CODE == 2 + assert multi_host_name.to_primitive() == [2, dns_name] + + +@pytest.mark.parametrize( + "url, pool_metadata_hash", + [ + ( + "https://example.com/metadata.json", + b"1" * POOL_METADATA_HASH_SIZE, + ), + ( + "https://pooldata.info/api/metadata", + b"2" * POOL_METADATA_HASH_SIZE, + ), + ( + "http://metadata.pool/endpoint", + b"3" * POOL_METADATA_HASH_SIZE, + ), + ], +) +def test_pool_metadata(url, pool_metadata_hash): + # Arrange + primitive_values = [url, pool_metadata_hash] + + # Act + pool_metadata = PoolMetadata.from_primitive(primitive_values) + + # Assert + assert pool_metadata.url == url + assert pool_metadata.pool_metadata_hash == PoolMetadataHash(pool_metadata_hash) + assert isinstance(pool_metadata, PoolMetadata) + assert pool_metadata.to_primitive() == primitive_values + + +@pytest.mark.parametrize( + "input_value", + [ + [30, [1, 2]], + "1/2", + "3/4", + "0/1", + "1/1", + Fraction(123456, 1), + Fraction(5, 6), + Fraction(7, 8), + Fraction(5, 6), + ], +) +def test_fraction_serializer(input_value): + # Act + result = fraction_parser(input_value) + + # Assert + assert isinstance(result, Fraction) + + +@pytest.mark.parametrize( + "operator, vrf_keyhash, pledge, cost, margin, reward_account, pool_owners, relays, pool_metadata", + [ + # Test case ID: HP-1 + ( + b"1" * POOL_KEY_HASH_SIZE, + b"1" * VRF_KEY_HASH_SIZE, + 10_000_000, + 340_000_000, + "1/10", + b"1" * REWARD_ACCOUNT_HASH_SIZE, + [b"1" * VERIFICATION_KEY_HASH_SIZE], + [ + [0, 3001, SingleHostAddr.ipv4_to_bytes("10.20.30.40"), None], + [1, 3001, "example.com"], + [2, "example.com"], + ], + [ + "https://example.com/metadata.json", + b"1" * POOL_METADATA_HASH_SIZE, + ], + ), + # Add more test cases with different realistic values + ], + ids=["test_pool_params-1"], +) # Add more IDs for each test case +def test_pool_params( + operator, + vrf_keyhash, + pledge, + cost, + margin, + reward_account, + pool_owners, + relays, + pool_metadata, +): + # Arrange + primitive_values = [ + operator, + vrf_keyhash, + pledge, + cost, + margin, + reward_account, + pool_owners, + relays, + pool_metadata, + ] + primitive_out = [ + operator, + vrf_keyhash, + pledge, + cost, + fraction_parser(margin), + reward_account, + pool_owners, + relays, + pool_metadata, + ] + + assert PoolParams.from_primitive(primitive_values).to_primitive() == primitive_out diff --git a/test/pycardano/test_serialization.py b/test/pycardano/test_serialization.py index 987921d2..890eb16a 100644 --- a/test/pycardano/test_serialization.py +++ b/test/pycardano/test_serialization.py @@ -1,12 +1,11 @@ from dataclasses import dataclass, field - -import pycardano from test.pycardano.util import check_two_way_cbor from typing import Any, Dict, List, Optional, Set, Tuple, Union import cbor2 import pytest +import pycardano from pycardano import Datum, RawPlutusData from pycardano.exception import DeserializeException from pycardano.plutus import PlutusV1Script, PlutusV2Script diff --git a/test/pycardano/test_txbuilder.py b/test/pycardano/test_txbuilder.py index 3060a908..7ba70819 100644 --- a/test/pycardano/test_txbuilder.py +++ b/test/pycardano/test_txbuilder.py @@ -1,13 +1,18 @@ import copy from dataclasses import replace +from fractions import Fraction from test.pycardano.test_key import SK -from test.pycardano.util import chain_context from unittest.mock import patch import pytest from pycardano.address import Address -from pycardano.certificate import StakeCredential, StakeDelegation, StakeRegistration +from pycardano.certificate import ( + PoolRegistration, + StakeCredential, + StakeDelegation, + StakeRegistration, +) from pycardano.coinselection import RandomImproveMultiAsset from pycardano.exception import ( InsufficientUTxOBalanceException, @@ -79,14 +84,14 @@ def test_tx_builder(chain_context): def test_tx_builder_no_change(chain_context): tx_builder = TransactionBuilder(chain_context, [RandomImproveMultiAsset([0, 0])]) sender = "addr_test1vrm9x2zsux7va6w892g38tvchnzahvcd9tykqf3ygnmwtaqyfg52x" - sender_address = Address.from_primitive(sender) + Address.from_primitive(sender) # Add sender address as input tx_builder.add_input_address(sender).add_output( TransactionOutput.from_primitive([sender, 500000]) ) - tx_body = tx_builder.build() + tx_builder.build() def test_tx_builder_with_certain_input(chain_context): @@ -206,7 +211,7 @@ def test_tx_builder_raises_utxo_selection(chain_context): ) with pytest.raises(UTxOSelectionException) as e: - tx_body = tx_builder.build( + tx_builder.build( change_address=sender_address, ) @@ -226,7 +231,7 @@ def test_tx_too_big_exception(chain_context): tx_builder.add_output(TransactionOutput.from_primitive([sender, 10])) with pytest.raises(InvalidTransactionException): - tx_body = tx_builder.build(change_address=sender_address) + tx_builder.build(change_address=sender_address) def test_tx_small_utxo_precise_fee(chain_context): @@ -278,7 +283,7 @@ def test_tx_small_utxo_balance_fail(chain_context): # Balance is smaller than minimum ada required in change # No more UTxO is available, throwing UTxO selection exception with pytest.raises(UTxOSelectionException): - tx_body = tx_builder.build(change_address=sender_address) + tx_builder.build(change_address=sender_address) def test_tx_small_utxo_balance_pass(chain_context): @@ -486,7 +491,7 @@ def test_tx_add_change_split_nfts_not_enough_add(chain_context): tx_builder.ttl = 123456789 with pytest.raises(InsufficientUTxOBalanceException): - tx_body = tx_builder.build(change_address=sender_address) + tx_builder.build(change_address=sender_address) def test_not_enough_input_amount(chain_context): @@ -502,7 +507,7 @@ def test_not_enough_input_amount(chain_context): with pytest.raises(UTxOSelectionException): # Tx builder must fail here because there is not enough amount of input ADA to pay tx fee - tx_body = tx_builder.build(change_address=sender_address) + tx_builder.build(change_address=sender_address) def test_add_script_input(chain_context): @@ -521,7 +526,7 @@ def test_add_script_input(chain_context): tx_in1, TransactionOutput(script_address, 10000000, datum_hash=datum.hash()) ) mint = MultiAsset.from_primitive({script_hash.payload: {b"TestToken": 1}}) - utxo2 = UTxO( + UTxO( tx_in2, TransactionOutput( script_address, Value(10000000, mint), datum_hash=datum.hash() @@ -536,7 +541,7 @@ def test_add_script_input(chain_context): "addr_test1vrm9x2zsux7va6w892g38tvchnzahvcd9tykqf3ygnmwtaqyfg52x" ) tx_builder.add_output(TransactionOutput(receiver, 5000000)) - tx_body = tx_builder.build(change_address=receiver) + tx_builder.build(change_address=receiver) witness = tx_builder.build_witness_set() assert [datum] == witness.plutus_data assert [redeemer1, redeemer2] == witness.redeemer @@ -564,7 +569,7 @@ def test_add_script_input_no_script(chain_context): "addr_test1vrm9x2zsux7va6w892g38tvchnzahvcd9tykqf3ygnmwtaqyfg52x" ) tx_builder.add_output(TransactionOutput(receiver, 5000000)) - tx_body = tx_builder.build(change_address=receiver) + tx_builder.build(change_address=receiver) witness = tx_builder.build_witness_set() assert [datum] == witness.plutus_data assert [redeemer] == witness.redeemer @@ -835,7 +840,7 @@ def test_wrong_redeemer_execution_units(chain_context): tx_in1, TransactionOutput(script_address, 10000000, datum_hash=datum.hash()) ) mint = MultiAsset.from_primitive({script_hash.payload: {b"TestToken": 1}}) - utxo2 = UTxO( + UTxO( tx_in2, TransactionOutput( script_address, Value(10000000, mint), datum_hash=datum.hash() @@ -856,7 +861,7 @@ def test_all_redeemer_should_provide_execution_units(chain_context): tx_in1 = TransactionInput.from_primitive( ["18cbe6cadecd3f89b60e08e68e5e6c7d72d730aaa1ad21431590f7e6643438ef", 0] ) - tx_in2 = TransactionInput.from_primitive( + TransactionInput.from_primitive( ["18cbe6cadecd3f89b60e08e68e5e6c7d72d730aaa1ad21431590f7e6643438ef", 1] ) plutus_script = PlutusV1Script(b"dummy test script") @@ -893,7 +898,7 @@ def test_add_minting_script(chain_context): "addr_test1vrm9x2zsux7va6w892g38tvchnzahvcd9tykqf3ygnmwtaqyfg52x" ) tx_builder.add_output(TransactionOutput(receiver, Value(5000000, mint))) - tx_body = tx_builder.build(change_address=receiver) + tx_builder.build(change_address=receiver) witness = tx_builder.build_witness_set() assert [plutus_script] == witness.plutus_v1_script @@ -915,7 +920,7 @@ def test_add_minting_script_only(chain_context): "addr_test1vrm9x2zsux7va6w892g38tvchnzahvcd9tykqf3ygnmwtaqyfg52x" ) tx_builder.add_output(TransactionOutput(receiver, Value(5000000, mint))) - tx_body = tx_builder.build(change_address=receiver) + tx_builder.build(change_address=receiver) witness = tx_builder.build_witness_set() assert [plutus_script] == witness.plutus_v1_script @@ -1019,7 +1024,7 @@ def test_estimate_execution_unit(chain_context): "addr_test1vrm9x2zsux7va6w892g38tvchnzahvcd9tykqf3ygnmwtaqyfg52x" ) tx_builder.add_output(TransactionOutput(receiver, 5000000)) - tx_body = tx_builder.build(change_address=receiver) + tx_builder.build(change_address=receiver) witness = tx_builder.build_witness_set() assert [datum] == witness.plutus_data assert [redeemer1] == witness.redeemer @@ -1112,6 +1117,62 @@ def test_tx_builder_certificates(chain_context): assert expected == tx_body.to_primitive() +def test_tx_builder_stake_pool_registration(chain_context, pool_params): + tx_builder = TransactionBuilder(chain_context, [RandomImproveMultiAsset([0, 0])]) + sender = "addr_test1vrm9x2zsux7va6w892g38tvchnzahvcd9tykqf3ygnmwtaqyfg52x" + sender_address = Address.from_primitive(sender) + + pool_registration = PoolRegistration(pool_params) + + tx_in3 = TransactionInput.from_primitive([b"2" * 32, 2]) + tx_out3 = TransactionOutput.from_primitive([sender, 505000000]) + utxo = UTxO(tx_in3, tx_out3) + + tx_builder.add_input(utxo) + + tx_builder.initial_stake_pool_registration = True + + tx_builder.certificates = [pool_registration] + + tx_body = tx_builder.build(change_address=sender_address) + + expected = { + 0: [[b"22222222222222222222222222222222", 2]], + 1: [ + [ + b"`\xf6S(P\xe1\xbc\xce\xe9\xc7*\x91\x13\xad\x98\xbc\xc5\xdb\xb3\r*\xc9`&$D\xf6\xe5\xf4", + 4819407, + ] + ], + 2: 180593, + 4: [ + [ + 3, + b"1111111111111111111111111111", + b"11111111111111111111111111111111", + 100000000, + 340000000, + Fraction(1, 50), + b"11111111111111111111111111111", + [b"1111111111111111111111111111"], + [ + [ + 0, + 3001, + b"\xc0\xa8\x00\x01", + b"\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x01", + ], + [1, 3001, "relay1.example.com"], + [2, "relay1.example.com"], + ], + ["https://meta1.example.com", b"11111111111111111111111111111111"], + ] + ], + } + + assert expected == tx_body.to_primitive() + + def test_tx_builder_withdrawal(chain_context): tx_builder = TransactionBuilder(chain_context, [RandomImproveMultiAsset([0, 0])]) sender = "addr_test1vrm9x2zsux7va6w892g38tvchnzahvcd9tykqf3ygnmwtaqyfg52x" @@ -1339,7 +1400,7 @@ def test_tx_builder_small_utxo_input(chain_context): ), ) ) - signed_tx = builder.build(change_address=address) + builder.build(change_address=address) def test_tx_builder_small_utxo_input_2(chain_context): @@ -1408,7 +1469,7 @@ def test_tx_builder_small_utxo_input_2(chain_context): ), ) ) - signed_tx = builder.build(change_address=address) + builder.build(change_address=address) def test_tx_builder_merge_change_to_output_3(chain_context): diff --git a/test/pycardano/util.py b/test/pycardano/util.py index 9334ff7f..892b16eb 100644 --- a/test/pycardano/util.py +++ b/test/pycardano/util.py @@ -6,7 +6,7 @@ from pycardano.backend.base import ChainContext, GenesisParameters, ProtocolParameters from pycardano.network import Network from pycardano.serialization import CBORSerializable -from pycardano.transaction import TransactionInput, TransactionOutput, UTxO, Value +from pycardano.transaction import TransactionInput, TransactionOutput, UTxO TEST_ADDR = "addr_test1vr2p8st5t5cxqglyjky7vk98k7jtfhdpvhl4e97cezuhn0cqcexl7" diff --git a/test/resources/keys/cold.skey b/test/resources/keys/cold.skey new file mode 100644 index 00000000..a4eb4881 --- /dev/null +++ b/test/resources/keys/cold.skey @@ -0,0 +1,5 @@ +{ + "type": "StakePoolSigningKey_ed25519", + "description": "StakePoolSigningKey_ed25519", + "cborHex": "582044181bd0e6be21cea5b0751b8c6d4f88a5cb2d5dfec31a271add617f7ce559a9" +} diff --git a/test/resources/keys/cold.vkey b/test/resources/keys/cold.vkey new file mode 100644 index 00000000..87a811b4 --- /dev/null +++ b/test/resources/keys/cold.vkey @@ -0,0 +1,5 @@ +{ + "type": "StakePoolVerificationKey_ed25519", + "description": "StakePoolVerificationKey_ed25519", + "cborHex": "5820354ce32da92e7116f6c70e9be99a3a601d33137d0685ab5b7e2ff5b656989299" +}