From cde9f05b86cc1650171f845cc3e3a7679ae5377c Mon Sep 17 00:00:00 2001 From: Hareem Adderley Date: Tue, 3 Jun 2025 08:19:00 -0500 Subject: [PATCH 1/7] fix: allow certificates to be a List or NonEmptyOrderedSet --- pycardano/transaction.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/pycardano/transaction.py b/pycardano/transaction.py index 0f926853..a6def31f 100644 --- a/pycardano/transaction.py +++ b/pycardano/transaction.py @@ -577,7 +577,9 @@ class TransactionBody(MapCBORSerializable): ttl: Optional[int] = field(default=None, metadata={"key": 3, "optional": True}) - certificates: Optional[List[Certificate]] = field( + certificates: Optional[ + Union[List[Certificate], NonEmptyOrderedSet[Certificate]] + ] = field( default=None, metadata={ "key": 4, From 67a215c5634b5ef2da074ed1b25cc70606177ac6 Mon Sep 17 00:00:00 2001 From: Hareem Adderley Date: Tue, 3 Jun 2025 08:25:32 -0500 Subject: [PATCH 2/7] fix: add version check for member import for Python 3.13 --- pycardano/backend/cardano_cli.py | 11 ++++++++++- pycardano/utils.py | 16 +++++++++++++++- 2 files changed, 25 insertions(+), 2 deletions(-) diff --git a/pycardano/backend/cardano_cli.py b/pycardano/backend/cardano_cli.py index 5c0072ea..9ade9f9b 100644 --- a/pycardano/backend/cardano_cli.py +++ b/pycardano/backend/cardano_cli.py @@ -44,6 +44,11 @@ Value, ) from pycardano.types import JsonDict +from pycardano.utils import greater_than_version + +if greater_than_version((3, 13)): + from enum import member # type: ignore[attr-defined] + __all__ = ["CardanoCliChainContext", "CardanoCliNetwork", "DockerConfig"] @@ -70,7 +75,11 @@ class CardanoCliNetwork(Enum): PREVIEW = ["--testnet-magic", str(2)] PREPROD = ["--testnet-magic", str(1)] GUILDNET = ["--testnet-magic", str(141)] - CUSTOM = partial(network_magic) + CUSTOM = ( + member(partial(network_magic)) + if greater_than_version((3, 13)) + else partial(network_magic) + ) class DockerConfig: diff --git a/pycardano/utils.py b/pycardano/utils.py index 7fa139f0..26a51746 100644 --- a/pycardano/utils.py +++ b/pycardano/utils.py @@ -3,7 +3,8 @@ from __future__ import annotations import math -from typing import Dict, List, Optional, Union +import sys +from typing import Dict, List, Optional, Union, Tuple import cbor2 from nacl.encoding import RawEncoder @@ -24,6 +25,7 @@ "min_lovelace_post_alonzo", "script_data_hash", "tiered_reference_script_fee", + "greater_than_version", ] @@ -266,3 +268,15 @@ def script_data_hash( encoder=RawEncoder, ) ) + + +def greater_than_version(version: Tuple[int, int]) -> bool: + """Check if the current Python version is greater than or equal to the specified version + + Args: + version (Tuple[int, int]): Tuple of major and minor version + + Returns: + True if the current Python version is greater than or equal to the specified version + """ + return sys.version_info >= version From 7e8c1f5776974798a6947ada4f7ab3a59b80cebc Mon Sep 17 00:00:00 2001 From: Hareem Adderley Date: Tue, 3 Jun 2025 08:29:54 -0500 Subject: [PATCH 3/7] fix: add support for latest command in transaction submission and ID retrieval --- pycardano/backend/cardano_cli.py | 37 +++++++++++++++++++++++--------- 1 file changed, 27 insertions(+), 10 deletions(-) diff --git a/pycardano/backend/cardano_cli.py b/pycardano/backend/cardano_cli.py index 9ade9f9b..4c4c0da7 100644 --- a/pycardano/backend/cardano_cli.py +++ b/pycardano/backend/cardano_cli.py @@ -520,22 +520,39 @@ def submit_tx_cbor(self, cbor: Union[bytes, str]) -> str: try: self._run_command( - ["transaction", "submit", "--tx-file", tmp_tx_file.name] + [ + "latest", + "transaction", + "submit", + "--tx-file", + tmp_tx_file.name, + ] + self._network_args ) - except CardanoCliError as err: - raise TransactionFailedException( - "Failed to submit transaction" - ) from err + except CardanoCliError: + try: + self._run_command( + ["transaction", "submit", "--tx-file", tmp_tx_file.name] + + self._network_args + ) + except CardanoCliError as err: + raise TransactionFailedException( + "Failed to submit transaction" + ) from err # Get the transaction ID try: txid = self._run_command( - ["transaction", "txid", "--tx-file", tmp_tx_file.name] + ["latest", "transaction", "txid", "--tx-file", tmp_tx_file.name] ) - except CardanoCliError as err: - raise PyCardanoException( - f"Unable to get transaction id for {tmp_tx_file.name}" - ) from err + except CardanoCliError: + try: + txid = self._run_command( + ["transaction", "txid", "--tx-file", tmp_tx_file.name] + ) + except CardanoCliError as err: + raise PyCardanoException( + f"Unable to get transaction id for {tmp_tx_file.name}" + ) from err return txid From 16510316abd7d5662c3958737cf196e2d09b1295 Mon Sep 17 00:00:00 2001 From: Hareem Adderley Date: Tue, 3 Jun 2025 08:30:40 -0500 Subject: [PATCH 4/7] lint: correct formatting of certificates field --- pycardano/transaction.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pycardano/transaction.py b/pycardano/transaction.py index a6def31f..e2099966 100644 --- a/pycardano/transaction.py +++ b/pycardano/transaction.py @@ -579,7 +579,7 @@ class TransactionBody(MapCBORSerializable): certificates: Optional[ Union[List[Certificate], NonEmptyOrderedSet[Certificate]] - ] = field( + ] = field( default=None, metadata={ "key": 4, From 62267f33a253d5780207c54eab7efc076c0fd46d Mon Sep 17 00:00:00 2001 From: Hareem Adderley Date: Tue, 3 Jun 2025 09:54:21 -0500 Subject: [PATCH 5/7] lint: sort imports --- pycardano/utils.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pycardano/utils.py b/pycardano/utils.py index 26a51746..97364fd4 100644 --- a/pycardano/utils.py +++ b/pycardano/utils.py @@ -4,7 +4,7 @@ import math import sys -from typing import Dict, List, Optional, Union, Tuple +from typing import Dict, List, Optional, Tuple, Union import cbor2 from nacl.encoding import RawEncoder From 86862090afd68c82b3971e68bf2a1c3332c52c64 Mon Sep 17 00:00:00 2001 From: Hareem Adderley Date: Tue, 3 Jun 2025 09:54:57 -0500 Subject: [PATCH 6/7] test: add cardano-cli tests for latest --- test/pycardano/backend/test_cardano_cli.py | 71 ++++++++++++++++++++-- 1 file changed, 67 insertions(+), 4 deletions(-) diff --git a/test/pycardano/backend/test_cardano_cli.py b/test/pycardano/backend/test_cardano_cli.py index b1004dd1..3633df56 100644 --- a/test/pycardano/backend/test_cardano_cli.py +++ b/test/pycardano/backend/test_cardano_cli.py @@ -16,6 +16,7 @@ ProtocolParameters, RawPlutusData, TransactionInput, + CardanoCliError, ) QUERY_TIP_RESULT = { @@ -483,15 +484,19 @@ } -def override_run_command(cmd: List[str]): +def override_run_command_older_version(cmd: List[str]): """ - Override the run_command method of CardanoCliChainContext to return a mock result + Override the run_command method of CardanoCliChainContext to return a mock result of older versions of cardano-cli. Args: cmd: The command to run Returns: The mock result """ + if "latest" in cmd: + raise CardanoCliError( + "Older versions of cardano-cli do not support the latest command" + ) if "tip" in cmd: return json.dumps(QUERY_TIP_RESULT) if "protocol-parameters" in cmd: @@ -506,6 +511,23 @@ def override_run_command(cmd: List[str]): return None +def override_run_command_latest(cmd: List[str]): + """ + Override the run_command method of CardanoCliChainContext to return a mock result of the latest versions of cardano-cli. + Args: + cmd: The command to run + + Returns: + The mock result + """ + if "tip" in cmd: + return json.dumps(QUERY_TIP_RESULT) + if "txid" in cmd: + return "270be16fa17cdb3ef683bf2c28259c978d4b7088792074f177c8efda247e23f7" + else: + return None + + @pytest.fixture def chain_context(genesis_file, config_file): """ @@ -519,7 +541,32 @@ def chain_context(genesis_file, config_file): """ with patch( "pycardano.backend.cardano_cli.CardanoCliChainContext._run_command", - side_effect=override_run_command, + side_effect=override_run_command_older_version, + ): + context = CardanoCliChainContext( + binary=Path("cardano-cli"), + socket=Path("node.socket"), + config_file=config_file, + network=CardanoCliNetwork.PREPROD, + ) + context._run_command = override_run_command_older_version + return context + + +@pytest.fixture +def chain_context_latest(genesis_file, config_file): + """ + Create a CardanoCliChainContext with a mock run_command method + Args: + genesis_file: The genesis file + config_file: The config file + + Returns: + The CardanoCliChainContext + """ + with patch( + "pycardano.backend.cardano_cli.CardanoCliChainContext._run_command", + side_effect=override_run_command_latest, ): context = CardanoCliChainContext( binary=Path("cardano-cli"), @@ -527,7 +574,7 @@ def chain_context(genesis_file, config_file): config_file=config_file, network=CardanoCliNetwork.PREPROD, ) - context._run_command = override_run_command + context._run_command = override_run_command_latest return context @@ -722,6 +769,14 @@ def test_utxo(self, chain_context): "55fe36f482e21ff6ae2caf2e33c3565572b568852dccd3f317ddecb91463d780" ) + def test_submit_tx_bytes(self, chain_context): + results = chain_context.submit_tx("testcborhexfromtransaction".encode("utf-8")) + + assert ( + results + == "270be16fa17cdb3ef683bf2c28259c978d4b7088792074f177c8efda247e23f7" + ) + def test_submit_tx(self, chain_context): results = chain_context.submit_tx("testcborhexfromtransaction") @@ -730,5 +785,13 @@ def test_submit_tx(self, chain_context): == "270be16fa17cdb3ef683bf2c28259c978d4b7088792074f177c8efda247e23f7" ) + def test_submit_tx_latest(self, chain_context_latest): + results = chain_context_latest.submit_tx("testcborhexfromtransaction") + + assert ( + results + == "270be16fa17cdb3ef683bf2c28259c978d4b7088792074f177c8efda247e23f7" + ) + def test_epoch(self, chain_context): assert chain_context.epoch == 98 From 1346a1a4648cf3e7db0a9d9a97c4a031ae356745 Mon Sep 17 00:00:00 2001 From: Hareem Adderley Date: Tue, 3 Jun 2025 14:21:47 -0500 Subject: [PATCH 7/7] test: add fixtures for transaction failure scenarios in cardano-cli tests --- test/pycardano/backend/test_cardano_cli.py | 150 ++++++++++++++++----- 1 file changed, 114 insertions(+), 36 deletions(-) diff --git a/test/pycardano/backend/test_cardano_cli.py b/test/pycardano/backend/test_cardano_cli.py index 3633df56..a81dc3b4 100644 --- a/test/pycardano/backend/test_cardano_cli.py +++ b/test/pycardano/backend/test_cardano_cli.py @@ -8,15 +8,17 @@ from pycardano import ( ALONZO_COINS_PER_UTXO_WORD, CardanoCliChainContext, + CardanoCliError, CardanoCliNetwork, DatumHash, GenesisParameters, MultiAsset, PlutusV2Script, ProtocolParameters, + PyCardanoException, RawPlutusData, + TransactionFailedException, TransactionInput, - CardanoCliError, ) QUERY_TIP_RESULT = { @@ -484,52 +486,102 @@ } -def override_run_command_older_version(cmd: List[str]): +@pytest.fixture +def chain_context(genesis_file, config_file): """ - Override the run_command method of CardanoCliChainContext to return a mock result of older versions of cardano-cli. + Create a CardanoCliChainContext with a mock run_command method Args: - cmd: The command to run + genesis_file: The genesis file + config_file: The config file Returns: - The mock result + The CardanoCliChainContext """ - if "latest" in cmd: - raise CardanoCliError( - "Older versions of cardano-cli do not support the latest command" + + def override_run_command_older_version(cmd: List[str]): + """ + Override the run_command method of CardanoCliChainContext to return a mock result of older versions of cardano-cli. + Args: + cmd: The command to run + + Returns: + The mock result + """ + if "latest" in cmd: + raise CardanoCliError( + "Older versions of cardano-cli do not support the latest command" + ) + if "tip" in cmd: + return json.dumps(QUERY_TIP_RESULT) + if "protocol-parameters" in cmd: + return json.dumps(QUERY_PROTOCOL_PARAMETERS_RESULT) + if "utxo" in cmd: + return json.dumps(QUERY_UTXO_RESULT) + if "txid" in cmd: + return "270be16fa17cdb3ef683bf2c28259c978d4b7088792074f177c8efda247e23f7" + if "version" in cmd: + return "cardano-cli 8.1.2 - linux-x86_64 - ghc-8.10\ngit rev d2d90b48c5577b4412d5c9c9968b55f8ab4b9767" + else: + return None + + with patch( + "pycardano.backend.cardano_cli.CardanoCliChainContext._run_command", + side_effect=override_run_command_older_version, + ): + context = CardanoCliChainContext( + binary=Path("cardano-cli"), + socket=Path("node.socket"), + config_file=config_file, + network=CardanoCliNetwork.PREPROD, ) - if "tip" in cmd: - return json.dumps(QUERY_TIP_RESULT) - if "protocol-parameters" in cmd: - return json.dumps(QUERY_PROTOCOL_PARAMETERS_RESULT) - if "utxo" in cmd: - return json.dumps(QUERY_UTXO_RESULT) - if "txid" in cmd: - return "270be16fa17cdb3ef683bf2c28259c978d4b7088792074f177c8efda247e23f7" - if "version" in cmd: - return "cardano-cli 8.1.2 - linux-x86_64 - ghc-8.10\ngit rev d2d90b48c5577b4412d5c9c9968b55f8ab4b9767" - else: - return None + context._run_command = override_run_command_older_version + return context -def override_run_command_latest(cmd: List[str]): +@pytest.fixture +def chain_context_latest(genesis_file, config_file): """ - Override the run_command method of CardanoCliChainContext to return a mock result of the latest versions of cardano-cli. + Create a CardanoCliChainContext with a mock run_command method Args: - cmd: The command to run + genesis_file: The genesis file + config_file: The config file Returns: - The mock result + The CardanoCliChainContext """ - if "tip" in cmd: - return json.dumps(QUERY_TIP_RESULT) - if "txid" in cmd: - return "270be16fa17cdb3ef683bf2c28259c978d4b7088792074f177c8efda247e23f7" - else: - return None + + def override_run_command_latest(cmd: List[str]): + """ + Override the run_command method of CardanoCliChainContext to return a mock result of the latest versions of cardano-cli. + Args: + cmd: The command to run + + Returns: + The mock result + """ + if "tip" in cmd: + return json.dumps(QUERY_TIP_RESULT) + if "txid" in cmd: + return "270be16fa17cdb3ef683bf2c28259c978d4b7088792074f177c8efda247e23f7" + else: + return None + + with patch( + "pycardano.backend.cardano_cli.CardanoCliChainContext._run_command", + side_effect=override_run_command_latest, + ): + context = CardanoCliChainContext( + binary=Path("cardano-cli"), + socket=Path("node.socket"), + config_file=config_file, + network=CardanoCliNetwork.PREPROD, + ) + context._run_command = override_run_command_latest + return context @pytest.fixture -def chain_context(genesis_file, config_file): +def chain_context_tx_fail(genesis_file, config_file): """ Create a CardanoCliChainContext with a mock run_command method Args: @@ -539,9 +591,17 @@ def chain_context(genesis_file, config_file): Returns: The CardanoCliChainContext """ + + def override_run_command_fail(cmd: List[str]): + if "transaction" in cmd: + raise CardanoCliError("Intentionally raised error for testing purposes") + if "tip" in cmd: + return json.dumps(QUERY_TIP_RESULT) + return None + with patch( "pycardano.backend.cardano_cli.CardanoCliChainContext._run_command", - side_effect=override_run_command_older_version, + side_effect=override_run_command_fail, ): context = CardanoCliChainContext( binary=Path("cardano-cli"), @@ -549,12 +609,12 @@ def chain_context(genesis_file, config_file): config_file=config_file, network=CardanoCliNetwork.PREPROD, ) - context._run_command = override_run_command_older_version + context._run_command = override_run_command_fail return context @pytest.fixture -def chain_context_latest(genesis_file, config_file): +def chain_context_tx_id_fail(genesis_file, config_file): """ Create a CardanoCliChainContext with a mock run_command method Args: @@ -564,9 +624,17 @@ def chain_context_latest(genesis_file, config_file): Returns: The CardanoCliChainContext """ + + def override_run_command_fail(cmd: List[str]): + if "txid" in cmd: + raise CardanoCliError("Intentionally raised error for testing purposes") + if "tip" in cmd: + return json.dumps(QUERY_TIP_RESULT) + return None + with patch( "pycardano.backend.cardano_cli.CardanoCliChainContext._run_command", - side_effect=override_run_command_latest, + side_effect=override_run_command_fail, ): context = CardanoCliChainContext( binary=Path("cardano-cli"), @@ -574,7 +642,7 @@ def chain_context_latest(genesis_file, config_file): config_file=config_file, network=CardanoCliNetwork.PREPROD, ) - context._run_command = override_run_command_latest + context._run_command = override_run_command_fail return context @@ -793,5 +861,15 @@ def test_submit_tx_latest(self, chain_context_latest): == "270be16fa17cdb3ef683bf2c28259c978d4b7088792074f177c8efda247e23f7" ) + def test_submit_tx_fail(self, chain_context_tx_fail): + with pytest.raises(TransactionFailedException) as exc_info: + chain_context_tx_fail.submit_tx("testcborhexfromtransaction") + assert str(exc_info.value) == "Failed to submit transaction" + + def test_submit_tx_id_fail(self, chain_context_tx_id_fail): + with pytest.raises(PyCardanoException) as exc_info: + chain_context_tx_id_fail.submit_tx("testcborhexfromtransaction") + assert str(exc_info.value).startswith("Unable to get transaction id for") + def test_epoch(self, chain_context): assert chain_context.epoch == 98