Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
19 commits
Select commit Hold shift + click to select a range
313a626
test: add cardano-cli tests for latest
KINGH242 Jun 3, 2025
bc363a2
test: add fixtures for transaction failure scenarios in cardano-cli t…
KINGH242 Jun 3, 2025
0cf1cd7
feat: add TextEnvelope class for JSON serialization and deserialization
KINGH242 Jun 6, 2025
564aed7
test: add tests for network magic and DRep serialization and TextEnve…
KINGH242 Jun 6, 2025
82dd140
test: add tests for TextEnvelope and witness
KINGH242 Jun 7, 2025
a39bea1
test: add unit tests for BlockFrostChainContext methods to improve co…
KINGH242 Jun 7, 2025
72c3b80
lint: clean up imports
KINGH242 Jun 7, 2025
2cb8699
fix: remove private properties from repr
KINGH242 Jun 7, 2025
b4e6c3a
fix: update vkey handling and improve repr
KINGH242 Jun 7, 2025
f30331f
test: add repr call in Transaction test for verification
KINGH242 Jun 7, 2025
04f1d48
style: correct typos in function docstrings
KINGH242 Jun 7, 2025
675f7fe
refactor: add JSON save and load methods to CBORSerializable
KINGH242 Jun 11, 2025
5340723
refactor: override add save and load methods for Address object
KINGH242 Jun 11, 2025
cce6dbe
refactor: remove duplicate save and load methods from Key class
KINGH242 Jun 11, 2025
971db5f
refactor: revert to previous version and add properties for to_json
KINGH242 Jun 11, 2025
e48aee5
test: add save and load tests for Address and CBORSerializable classes
KINGH242 Jun 11, 2025
18b3d5e
fix: add indentation to JSON serialization output
KINGH242 Jul 21, 2025
8960798
fix: add to_shallow_primitive method to display witness properly
KINGH242 Jul 21, 2025
ae1d02d
fix: update serialization to use class docstring or default string
KINGH242 Jul 21, 2025
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
45 changes: 44 additions & 1 deletion pycardano/address.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,8 +8,11 @@

from __future__ import annotations

import os
from enum import Enum
from typing import Type, Union
from typing import Optional, Type, Union

from typing_extensions import override

from pycardano.crypto.bech32 import decode, encode
from pycardano.exception import (
Expand Down Expand Up @@ -406,3 +409,43 @@ def __eq__(self, other):

def __repr__(self):
return f"{self.encode()}"

@override
def save(
self,
path: str,
key_type: Optional[str] = None,
description: Optional[str] = None,
):
"""
Save the Address object to a file.

This method writes the object's JSON representation to the specified file path.
It raises an error if the file already exists and is not empty.

Args:
path (str): The file path to save the object to.
key_type (str, optional): Not used in this context, but can be included for consistency.
description (str, optional): Not used in this context, but can be included for consistency.

Raises:
IOError: If the file already exists and is not empty.
"""
if os.path.isfile(path) and os.stat(path).st_size > 0:
raise IOError(f"File {path} already exists!")
with open(path, "w") as f:
f.write(self.encode())

@classmethod
def load(cls, path: str) -> Address:
"""
Load an Address object from a file.

Args:
path (str): The file path to load the object from.

Returns:
Address: The loaded Address object.
"""
with open(path) as f:
return cls.decode(f.read())
15 changes: 1 addition & 14 deletions pycardano/key.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,6 @@
from __future__ import annotations

import json
import os
from typing import Optional, Type

from nacl.encoding import RawEncoder
Expand Down Expand Up @@ -74,7 +73,7 @@ def to_primitive(self) -> bytes:
def from_primitive(cls: Type["Key"], value: bytes) -> Key:
return cls(value)

def to_json(self) -> str:
def to_json(self, **kwargs) -> str:
"""Serialize the key to JSON.

The json output has three fields: "type", "description", and "cborHex".
Expand Down Expand Up @@ -123,18 +122,6 @@ def from_json(cls: Type[Key], data: str, validate_type=False) -> Key:
description=obj["description"],
)

def save(self, path: str):
if os.path.isfile(path):
if os.stat(path).st_size > 0:
raise IOError(f"File {path} already exists!")
with open(path, "w") as f:
f.write(self.to_json())

@classmethod
def load(cls, path: str):
with open(path) as f:
return cls.from_json(f.read())

def __bytes__(self):
return self.payload

Expand Down
115 changes: 115 additions & 0 deletions pycardano/serialization.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,8 @@

from __future__ import annotations

import json
import os
import re
import typing
from collections import OrderedDict, UserList, defaultdict
Expand Down Expand Up @@ -535,6 +537,119 @@ def from_cbor(cls: Type[CBORBase], payload: Union[str, bytes]) -> CBORBase:
def __repr__(self):
return pformat(vars(self), indent=2)

@property
def json_type(self) -> str:
"""
Return the class name of the CBORSerializable object.

This property provides a default string representing the type of the object for use in JSON serialization.

Returns:
str: The class name of the object.
"""
return self.__class__.__name__

@property
def json_description(self) -> str:
"""
Return the docstring of the CBORSerializable object's class.

This property provides a default string description of the object for use in JSON serialization.

Returns:
str: The docstring of the object's class.
"""
return self.__class__.__doc__ or "Generated with PyCardano"

def to_json(self, **kwargs) -> str:
"""
Convert the CBORSerializable object to a JSON string containing type, description, and CBOR hex.

This method returns a JSON representation of the object, including its type, description, and CBOR hex encoding.

Args:
**kwargs: Additional keyword arguments that can include:
- key_type (str): The type to use in the JSON output. Defaults to the class name.
- description (str): The description to use in the JSON output. Defaults to the class docstring.

Returns:
str: The JSON string representation of the object.
"""
key_type = kwargs.pop("key_type", self.json_type)
description = kwargs.pop("description", self.json_description)
return json.dumps(
{
"type": key_type,
"description": description,
"cborHex": self.to_cbor_hex(),
},
indent=2,
)

@classmethod
def from_json(cls: Type[CBORSerializable], data: str) -> CBORSerializable:
"""
Load a CBORSerializable object from a JSON string containing its CBOR hex representation.

Args:
data (str): The JSON string to load the object from.

Returns:
CBORSerializable: The loaded CBORSerializable object.

Raises:
DeserializeException: If the loaded object is not of the expected type.
"""
obj = json.loads(data)

k = cls.from_cbor(obj["cborHex"])

if not isinstance(k, cls):
raise DeserializeException(
f"Expected type {cls.__name__} but got {type(k).__name__}."
)

return k

def save(
self,
path: str,
key_type: Optional[str] = None,
description: Optional[str] = None,
):
"""
Save the CBORSerializable object to a file in JSON format.

This method writes the object's JSON representation to the specified file path.
It raises an error if the file already exists and is not empty.

Args:
path (str): The file path to save the object to.
key_type (str, optional): The type to use in the JSON output.
description (str, optional): The description to use in the JSON output.

Raises:
IOError: If the file already exists and is not empty.
"""
if os.path.isfile(path) and os.stat(path).st_size > 0:
raise IOError(f"File {path} already exists!")
with open(path, "w") as f:
f.write(self.to_json(key_type=key_type, description=description))

@classmethod
def load(cls, path: str):
"""
Load a CBORSerializable object from a file containing its JSON representation.

Args:
path (str): The file path to load the object from.

Returns:
CBORSerializable: The loaded CBORSerializable object.
"""
with open(path) as f:
return cls.from_json(f.read())


def _restore_dataclass_field(
f: Field, v: Primitive
Expand Down
14 changes: 13 additions & 1 deletion pycardano/transaction.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,13 +4,13 @@

from copy import deepcopy
from dataclasses import dataclass, field
from pprint import pformat
from typing import Any, Callable, List, Optional, Type, Union

import cbor2
from cbor2 import CBORTag
from nacl.encoding import RawEncoder
from nacl.hash import blake2b
from pprintpp import pformat

from pycardano.address import Address
from pycardano.certificate import Certificate
Expand Down Expand Up @@ -694,6 +694,18 @@ class Transaction(ArrayCBORSerializable):

auxiliary_data: Optional[AuxiliaryData] = None

@property
def json_type(self) -> str:
return (
"Unwitnessed Tx ConwayEra"
if self.transaction_witness_set.is_empty()
else "Signed Tx ConwayEra"
)

@property
def json_description(self) -> str:
return "Ledger Cddl Format"

@property
def id(self) -> TransactionId:
return self.transaction_body.id
4 changes: 2 additions & 2 deletions pycardano/utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -80,7 +80,7 @@ def fee(
"""Calculate fee based on the length of a transaction's CBOR bytes and script execution.

Args:
context (ChainConext): A chain context.
context (ChainContext): A chain context.
length (int): The length of CBOR bytes, which could usually be derived
by `len(tx.to_cbor())`.
exec_steps (int): Number of execution steps run by plutus scripts in the transaction.
Expand Down Expand Up @@ -201,7 +201,7 @@ def min_lovelace_pre_alonzo(
def min_lovelace_post_alonzo(output: TransactionOutput, context: ChainContext) -> int:
"""Calculate minimum lovelace a transaction output needs to hold post alonzo.

This implementation is copied from the origianl Haskell implementation:
This implementation is copied from the original Haskell implementation:
https://github.com/input-output-hk/cardano-ledger/blob/eb053066c1d3bb51fb05978eeeab88afc0b049b2/eras/babbage/impl/src/Cardano/Ledger/Babbage/Rules/Utxo.hs#L242-L265

Args:
Expand Down
43 changes: 43 additions & 0 deletions pycardano/witness.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,8 @@
from dataclasses import dataclass, field
from typing import Any, List, Optional, Type, Union

from pprintpp import pformat

from pycardano.key import ExtendedVerificationKey, VerificationKey
from pycardano.nativescript import NativeScript
from pycardano.plutus import (
Expand All @@ -30,6 +32,14 @@ class VerificationKeyWitness(ArrayCBORSerializable):
vkey: Union[VerificationKey, ExtendedVerificationKey]
signature: bytes

@property
def json_type(self) -> str:
return "TxWitness ConwayEra"

@property
def json_description(self) -> str:
return "Key Witness ShelleyEra"

def __post_init__(self):
# When vkey is in extended format, we need to convert it to non-extended, so it can match the
# key hash of the input address we are trying to spend.
Expand All @@ -46,6 +56,26 @@ def from_primitive(
signature=values[1],
)

def to_shallow_primitive(self) -> Union[list, tuple]:
"""Convert to a shallow primitive representation."""
return [self.vkey.to_primitive(), self.signature]

def __eq__(self, other):
if not isinstance(other, VerificationKeyWitness):
return False
else:
return (
self.vkey.payload == other.vkey.payload
and self.signature == other.signature
)

def __repr__(self):
fields = {
"vkey": self.vkey.payload.hex(),
"signature": self.signature.hex(),
}
return pformat(fields, indent=2)


@dataclass(repr=False)
class TransactionWitnessSet(MapCBORSerializable):
Expand Down Expand Up @@ -126,3 +156,16 @@ def __post_init__(self):
self.plutus_v2_script = NonEmptyOrderedSet(self.plutus_v2_script)
if isinstance(self.plutus_v3_script, list):
self.plutus_v3_script = NonEmptyOrderedSet(self.plutus_v3_script)

def is_empty(self) -> bool:
"""Check if the witness set is empty."""
return (
not self.vkey_witnesses
and not self.native_scripts
and not self.bootstrap_witness
and not self.plutus_v1_script
and not self.plutus_data
and not self.redeemer
and not self.plutus_v2_script
and not self.plutus_v3_script
)
Loading
Loading