Skip to content

Commit b545112

Browse files
authored
Fix transaction certificates in Conway and handle latest cardano-cli transaction commands (#446)
* fix: allow certificates to be a List or NonEmptyOrderedSet * fix: add version check for member import for Python 3.13 * fix: add support for latest command in transaction submission and ID retrieval * lint: correct formatting of certificates field * lint: sort imports * test: add cardano-cli tests for latest * test: add fixtures for transaction failure scenarios in cardano-cli tests
1 parent 503fb68 commit b545112

File tree

4 files changed

+214
-31
lines changed

4 files changed

+214
-31
lines changed

pycardano/backend/cardano_cli.py

Lines changed: 37 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -44,6 +44,11 @@
4444
Value,
4545
)
4646
from pycardano.types import JsonDict
47+
from pycardano.utils import greater_than_version
48+
49+
if greater_than_version((3, 13)):
50+
from enum import member # type: ignore[attr-defined]
51+
4752

4853
__all__ = ["CardanoCliChainContext", "CardanoCliNetwork", "DockerConfig"]
4954

@@ -70,7 +75,11 @@ class CardanoCliNetwork(Enum):
7075
PREVIEW = ["--testnet-magic", str(2)]
7176
PREPROD = ["--testnet-magic", str(1)]
7277
GUILDNET = ["--testnet-magic", str(141)]
73-
CUSTOM = partial(network_magic)
78+
CUSTOM = (
79+
member(partial(network_magic))
80+
if greater_than_version((3, 13))
81+
else partial(network_magic)
82+
)
7483

7584

7685
class DockerConfig:
@@ -511,22 +520,39 @@ def submit_tx_cbor(self, cbor: Union[bytes, str]) -> str:
511520

512521
try:
513522
self._run_command(
514-
["transaction", "submit", "--tx-file", tmp_tx_file.name]
523+
[
524+
"latest",
525+
"transaction",
526+
"submit",
527+
"--tx-file",
528+
tmp_tx_file.name,
529+
]
515530
+ self._network_args
516531
)
517-
except CardanoCliError as err:
518-
raise TransactionFailedException(
519-
"Failed to submit transaction"
520-
) from err
532+
except CardanoCliError:
533+
try:
534+
self._run_command(
535+
["transaction", "submit", "--tx-file", tmp_tx_file.name]
536+
+ self._network_args
537+
)
538+
except CardanoCliError as err:
539+
raise TransactionFailedException(
540+
"Failed to submit transaction"
541+
) from err
521542

522543
# Get the transaction ID
523544
try:
524545
txid = self._run_command(
525-
["transaction", "txid", "--tx-file", tmp_tx_file.name]
546+
["latest", "transaction", "txid", "--tx-file", tmp_tx_file.name]
526547
)
527-
except CardanoCliError as err:
528-
raise PyCardanoException(
529-
f"Unable to get transaction id for {tmp_tx_file.name}"
530-
) from err
548+
except CardanoCliError:
549+
try:
550+
txid = self._run_command(
551+
["transaction", "txid", "--tx-file", tmp_tx_file.name]
552+
)
553+
except CardanoCliError as err:
554+
raise PyCardanoException(
555+
f"Unable to get transaction id for {tmp_tx_file.name}"
556+
) from err
531557

532558
return txid

pycardano/transaction.py

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -577,7 +577,9 @@ class TransactionBody(MapCBORSerializable):
577577

578578
ttl: Optional[int] = field(default=None, metadata={"key": 3, "optional": True})
579579

580-
certificates: Optional[List[Certificate]] = field(
580+
certificates: Optional[
581+
Union[List[Certificate], NonEmptyOrderedSet[Certificate]]
582+
] = field(
581583
default=None,
582584
metadata={
583585
"key": 4,

pycardano/utils.py

Lines changed: 15 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,8 @@
33
from __future__ import annotations
44

55
import math
6-
from typing import Dict, List, Optional, Union
6+
import sys
7+
from typing import Dict, List, Optional, Tuple, Union
78

89
import cbor2
910
from nacl.encoding import RawEncoder
@@ -24,6 +25,7 @@
2425
"min_lovelace_post_alonzo",
2526
"script_data_hash",
2627
"tiered_reference_script_fee",
28+
"greater_than_version",
2729
]
2830

2931

@@ -266,3 +268,15 @@ def script_data_hash(
266268
encoder=RawEncoder,
267269
)
268270
)
271+
272+
273+
def greater_than_version(version: Tuple[int, int]) -> bool:
274+
"""Check if the current Python version is greater than or equal to the specified version
275+
276+
Args:
277+
version (Tuple[int, int]): Tuple of major and minor version
278+
279+
Returns:
280+
True if the current Python version is greater than or equal to the specified version
281+
"""
282+
return sys.version_info >= version

test/pycardano/backend/test_cardano_cli.py

Lines changed: 159 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -8,13 +8,16 @@
88
from pycardano import (
99
ALONZO_COINS_PER_UTXO_WORD,
1010
CardanoCliChainContext,
11+
CardanoCliError,
1112
CardanoCliNetwork,
1213
DatumHash,
1314
GenesisParameters,
1415
MultiAsset,
1516
PlutusV2Script,
1617
ProtocolParameters,
18+
PyCardanoException,
1719
RawPlutusData,
20+
TransactionFailedException,
1821
TransactionInput,
1922
)
2023

@@ -483,31 +486,135 @@
483486
}
484487

485488

486-
def override_run_command(cmd: List[str]):
489+
@pytest.fixture
490+
def chain_context(genesis_file, config_file):
491+
"""
492+
Create a CardanoCliChainContext with a mock run_command method
493+
Args:
494+
genesis_file: The genesis file
495+
config_file: The config file
496+
497+
Returns:
498+
The CardanoCliChainContext
499+
"""
500+
501+
def override_run_command_older_version(cmd: List[str]):
502+
"""
503+
Override the run_command method of CardanoCliChainContext to return a mock result of older versions of cardano-cli.
504+
Args:
505+
cmd: The command to run
506+
507+
Returns:
508+
The mock result
509+
"""
510+
if "latest" in cmd:
511+
raise CardanoCliError(
512+
"Older versions of cardano-cli do not support the latest command"
513+
)
514+
if "tip" in cmd:
515+
return json.dumps(QUERY_TIP_RESULT)
516+
if "protocol-parameters" in cmd:
517+
return json.dumps(QUERY_PROTOCOL_PARAMETERS_RESULT)
518+
if "utxo" in cmd:
519+
return json.dumps(QUERY_UTXO_RESULT)
520+
if "txid" in cmd:
521+
return "270be16fa17cdb3ef683bf2c28259c978d4b7088792074f177c8efda247e23f7"
522+
if "version" in cmd:
523+
return "cardano-cli 8.1.2 - linux-x86_64 - ghc-8.10\ngit rev d2d90b48c5577b4412d5c9c9968b55f8ab4b9767"
524+
else:
525+
return None
526+
527+
with patch(
528+
"pycardano.backend.cardano_cli.CardanoCliChainContext._run_command",
529+
side_effect=override_run_command_older_version,
530+
):
531+
context = CardanoCliChainContext(
532+
binary=Path("cardano-cli"),
533+
socket=Path("node.socket"),
534+
config_file=config_file,
535+
network=CardanoCliNetwork.PREPROD,
536+
)
537+
context._run_command = override_run_command_older_version
538+
return context
539+
540+
541+
@pytest.fixture
542+
def chain_context_latest(genesis_file, config_file):
487543
"""
488-
Override the run_command method of CardanoCliChainContext to return a mock result
544+
Create a CardanoCliChainContext with a mock run_command method
489545
Args:
490-
cmd: The command to run
546+
genesis_file: The genesis file
547+
config_file: The config file
491548
492549
Returns:
493-
The mock result
550+
The CardanoCliChainContext
494551
"""
495-
if "tip" in cmd:
496-
return json.dumps(QUERY_TIP_RESULT)
497-
if "protocol-parameters" in cmd:
498-
return json.dumps(QUERY_PROTOCOL_PARAMETERS_RESULT)
499-
if "utxo" in cmd:
500-
return json.dumps(QUERY_UTXO_RESULT)
501-
if "txid" in cmd:
502-
return "270be16fa17cdb3ef683bf2c28259c978d4b7088792074f177c8efda247e23f7"
503-
if "version" in cmd:
504-
return "cardano-cli 8.1.2 - linux-x86_64 - ghc-8.10\ngit rev d2d90b48c5577b4412d5c9c9968b55f8ab4b9767"
505-
else:
552+
553+
def override_run_command_latest(cmd: List[str]):
554+
"""
555+
Override the run_command method of CardanoCliChainContext to return a mock result of the latest versions of cardano-cli.
556+
Args:
557+
cmd: The command to run
558+
559+
Returns:
560+
The mock result
561+
"""
562+
if "tip" in cmd:
563+
return json.dumps(QUERY_TIP_RESULT)
564+
if "txid" in cmd:
565+
return "270be16fa17cdb3ef683bf2c28259c978d4b7088792074f177c8efda247e23f7"
566+
else:
567+
return None
568+
569+
with patch(
570+
"pycardano.backend.cardano_cli.CardanoCliChainContext._run_command",
571+
side_effect=override_run_command_latest,
572+
):
573+
context = CardanoCliChainContext(
574+
binary=Path("cardano-cli"),
575+
socket=Path("node.socket"),
576+
config_file=config_file,
577+
network=CardanoCliNetwork.PREPROD,
578+
)
579+
context._run_command = override_run_command_latest
580+
return context
581+
582+
583+
@pytest.fixture
584+
def chain_context_tx_fail(genesis_file, config_file):
585+
"""
586+
Create a CardanoCliChainContext with a mock run_command method
587+
Args:
588+
genesis_file: The genesis file
589+
config_file: The config file
590+
591+
Returns:
592+
The CardanoCliChainContext
593+
"""
594+
595+
def override_run_command_fail(cmd: List[str]):
596+
if "transaction" in cmd:
597+
raise CardanoCliError("Intentionally raised error for testing purposes")
598+
if "tip" in cmd:
599+
return json.dumps(QUERY_TIP_RESULT)
506600
return None
507601

602+
with patch(
603+
"pycardano.backend.cardano_cli.CardanoCliChainContext._run_command",
604+
side_effect=override_run_command_fail,
605+
):
606+
context = CardanoCliChainContext(
607+
binary=Path("cardano-cli"),
608+
socket=Path("node.socket"),
609+
config_file=config_file,
610+
network=CardanoCliNetwork.PREPROD,
611+
)
612+
context._run_command = override_run_command_fail
613+
return context
614+
508615

509616
@pytest.fixture
510-
def chain_context(genesis_file, config_file):
617+
def chain_context_tx_id_fail(genesis_file, config_file):
511618
"""
512619
Create a CardanoCliChainContext with a mock run_command method
513620
Args:
@@ -517,17 +624,25 @@ def chain_context(genesis_file, config_file):
517624
Returns:
518625
The CardanoCliChainContext
519626
"""
627+
628+
def override_run_command_fail(cmd: List[str]):
629+
if "txid" in cmd:
630+
raise CardanoCliError("Intentionally raised error for testing purposes")
631+
if "tip" in cmd:
632+
return json.dumps(QUERY_TIP_RESULT)
633+
return None
634+
520635
with patch(
521636
"pycardano.backend.cardano_cli.CardanoCliChainContext._run_command",
522-
side_effect=override_run_command,
637+
side_effect=override_run_command_fail,
523638
):
524639
context = CardanoCliChainContext(
525640
binary=Path("cardano-cli"),
526641
socket=Path("node.socket"),
527642
config_file=config_file,
528643
network=CardanoCliNetwork.PREPROD,
529644
)
530-
context._run_command = override_run_command
645+
context._run_command = override_run_command_fail
531646
return context
532647

533648

@@ -722,6 +837,14 @@ def test_utxo(self, chain_context):
722837
"55fe36f482e21ff6ae2caf2e33c3565572b568852dccd3f317ddecb91463d780"
723838
)
724839

840+
def test_submit_tx_bytes(self, chain_context):
841+
results = chain_context.submit_tx("testcborhexfromtransaction".encode("utf-8"))
842+
843+
assert (
844+
results
845+
== "270be16fa17cdb3ef683bf2c28259c978d4b7088792074f177c8efda247e23f7"
846+
)
847+
725848
def test_submit_tx(self, chain_context):
726849
results = chain_context.submit_tx("testcborhexfromtransaction")
727850

@@ -730,5 +853,23 @@ def test_submit_tx(self, chain_context):
730853
== "270be16fa17cdb3ef683bf2c28259c978d4b7088792074f177c8efda247e23f7"
731854
)
732855

856+
def test_submit_tx_latest(self, chain_context_latest):
857+
results = chain_context_latest.submit_tx("testcborhexfromtransaction")
858+
859+
assert (
860+
results
861+
== "270be16fa17cdb3ef683bf2c28259c978d4b7088792074f177c8efda247e23f7"
862+
)
863+
864+
def test_submit_tx_fail(self, chain_context_tx_fail):
865+
with pytest.raises(TransactionFailedException) as exc_info:
866+
chain_context_tx_fail.submit_tx("testcborhexfromtransaction")
867+
assert str(exc_info.value) == "Failed to submit transaction"
868+
869+
def test_submit_tx_id_fail(self, chain_context_tx_id_fail):
870+
with pytest.raises(PyCardanoException) as exc_info:
871+
chain_context_tx_id_fail.submit_tx("testcborhexfromtransaction")
872+
assert str(exc_info.value).startswith("Unable to get transaction id for")
873+
733874
def test_epoch(self, chain_context):
734875
assert chain_context.epoch == 98

0 commit comments

Comments
 (0)