From 20f554e204ac9920c42049bbe83c86270527621c Mon Sep 17 00:00:00 2001 From: Hareem Adderley Date: Sat, 7 Oct 2023 09:44:26 -0500 Subject: [PATCH 01/13] feat: add cardano-cli chain context --- pycardano/backend/__init__.py | 1 + pycardano/backend/cardano_cli.py | 406 +++++++++++++++++++++++++++++++ pycardano/exception.py | 4 + 3 files changed, 411 insertions(+) create mode 100644 pycardano/backend/cardano_cli.py diff --git a/pycardano/backend/__init__.py b/pycardano/backend/__init__.py index 3ca43b7c..515b7cde 100644 --- a/pycardano/backend/__init__.py +++ b/pycardano/backend/__init__.py @@ -2,4 +2,5 @@ from .base import * from .blockfrost import * +from .cardano_cli import * from .ogmios import * diff --git a/pycardano/backend/cardano_cli.py b/pycardano/backend/cardano_cli.py new file mode 100644 index 00000000..3a78a00b --- /dev/null +++ b/pycardano/backend/cardano_cli.py @@ -0,0 +1,406 @@ +""" +Cardano CLI Chain Context +""" +import json +import os +import subprocess +import tempfile +import time +from enum import Enum +from functools import partial +from pathlib import Path +from typing import Optional, List, Dict, Union + +from cachetools import Cache, LRUCache, TTLCache, func + +from pycardano.address import Address +from pycardano.backend.base import ( + ALONZO_COINS_PER_UTXO_WORD, + ChainContext, + GenesisParameters, + ProtocolParameters, +) +from pycardano.exception import TransactionFailedException, CardanoCliError, PyCardanoException +from pycardano.hash import DatumHash, ScriptHash +from pycardano.transaction import ( + Asset, + AssetName, + MultiAsset, + TransactionInput, + TransactionOutput, + UTxO, + Value, +) +from pycardano.types import JsonDict + +__all__ = ["CardanoCliChainContext", "CardanoCliNetwork"] + + +class Mode(str, Enum): + """ + Mode enumeration. + """ + + ONLINE = "online" + OFFLINE = "offline" + + +def network_magic(magic_number: int) -> List[str]: + """ + Returns the network magic number for the cardano-cli + Args: + magic_number: The network magic number + + Returns: + The network magic number arguments + """ + return ["--testnet-magic", str(magic_number)] + + +class CardanoCliNetwork(Enum): + """ + Enum class for Cardano Era + """ + + MAINNET = ["--mainnet"] + TESTNET = ["--testnet-magic", str(1097911063)] + PREVIEW = ["--testnet-magic", str(2)] + PREPROD = ["--testnet-magic", str(1)] + GUILDNET = ["--testnet-magic", str(141)] + CUSTOM = partial(network_magic) + + +class CardanoCliChainContext(ChainContext): + _binary: Optional[Path] + _socket: Optional[Path] + _config_file: Optional[Path] + _mode: Mode + _network: CardanoCliNetwork + _last_known_block_slot: int + _last_chain_tip_fetch: float + _genesis_param: Optional[GenesisParameters] + _protocol_param: Optional[ProtocolParameters] + _utxo_cache: Cache + _datum_cache: Cache + + def __init__( + self, + binary: Path, + socket: Path, + config_file: Path, + network: CardanoCliNetwork, + refetch_chain_tip_interval: Optional[float] = None, + utxo_cache_size: int = 10000, + datum_cache_size: int = 10000, + ): + if not binary.exists() or not binary.is_file(): + raise CardanoCliError(f"cardano-cli binary file not found: {binary}") + + # Check the socket path file and set the CARDANO_NODE_SOCKET_PATH environment variable + try: + if not socket.exists(): + raise CardanoCliError(f"cardano-cli binary file not found: {binary}") + elif not socket.is_socket(): + raise CardanoCliError(f"{socket} is not a socket file") + + self._socket = socket + os.environ["CARDANO_NODE_SOCKET_PATH"] = self._socket.as_posix() + self._mode = Mode.ONLINE + except CardanoCliError: + self._socket = None + self._mode = Mode.OFFLINE + + self._binary = binary + self._network = network + self._config_file = config_file + self._last_known_block_slot = 0 + self._refetch_chain_tip_interval = ( + refetch_chain_tip_interval + if refetch_chain_tip_interval is not None + else 1000 + ) + self._last_chain_tip_fetch = 0 + self._genesis_param = None + self._protocol_param = None + if refetch_chain_tip_interval is None: + self._refetch_chain_tip_interval = ( + self.genesis_param.slot_length + / self.genesis_param.active_slots_coefficient + ) + + self._utxo_cache = TTLCache( + ttl=self._refetch_chain_tip_interval, maxsize=utxo_cache_size + ) + self._datum_cache = LRUCache(maxsize=datum_cache_size) + + def _run_command(self, cmd: List[str]) -> str: + """ + Runs the command in the cardano-cli + + :param cmd: Command as a list of strings + :return: The stdout if the command runs successfully + """ + try: + result = subprocess.run( + [self._binary.as_posix()] + cmd, capture_output=True, check=True + ) + return result.stdout.decode().strip() + except subprocess.CalledProcessError as err: + raise CardanoCliError(err.stderr.decode()) from err + + def _query_chain_tip(self) -> JsonDict: + result = self._run_command(["query", "tip"] + self._network.value) + return json.loads(result) + + def _query_current_protocol_params(self) -> JsonDict: + result = self._run_command(["query", "protocol-parameters"] + self._network.value) + return json.loads(result) + + def _query_genesis_config(self) -> JsonDict: + if not self._config_file.exists() or not self._config_file.is_file(): + raise CardanoCliError(f"Cardano config file not found: {self._config_file}") + with open(self._config_file, encoding="utf-8") as config_file: + config_json = json.load(config_file) + shelly_genesis_file = self._config_file.parent / config_json["ShelleyGenesisFile"] + if not shelly_genesis_file.exists() or not shelly_genesis_file.is_file(): + raise CardanoCliError(f"Shelly Genesis file not found: {shelly_genesis_file}") + with open(shelly_genesis_file, encoding="utf-8") as genesis_file: + genesis_json = json.load(genesis_file) + return genesis_json + + def _get_min_utxo(self) -> int: + params = self._query_genesis_config() + if "minUTxOValue" in params: + return params["minUTxOValue"] + elif "lovelacePerUTxOWord" in params: + return params["lovelacePerUTxOWord"] + elif "utxoCostPerWord" in params: + return params["utxoCostPerWord"] + elif "utxoCostPerByte" in params: + return params["utxoCostPerByte"] + + def _parse_cost_models(self, cli_result: JsonDict) -> Dict[str, Dict[str, int]]: + cli_cost_models = cli_result.get("costModels", {}) + + cost_models = {} + if "PlutusScriptV1" in cli_cost_models: + cost_models["PlutusScriptV1"] = cli_cost_models["PlutusScriptV1"].copy() + elif "PlutusV1" in cli_cost_models: + cost_models["PlutusV1"] = cli_cost_models["PlutusV1"].copy() + + if "PlutusScriptV2" in cli_cost_models: + cost_models["PlutusScriptV2"] = cli_cost_models["PlutusScriptV2"].copy() + elif "PlutusV2" in cli_cost_models: + cost_models["PlutusV2"] = cli_cost_models["PlutusV2"].copy() + + return cost_models + + def _is_chain_tip_updated(self): + # fetch at most every twenty seconds! + if time.time() - self._last_chain_tip_fetch < self._refetch_chain_tip_interval: + return False + self._last_chain_tip_fetch = time.time() + result = self._query_chain_tip() + return float(result["syncProgress"]) != 100.0 + + def _fetch_protocol_param(self) -> ProtocolParameters: + result = self._query_current_protocol_params() + return ProtocolParameters( + min_fee_constant=result["minFeeConstant"] if "minFeeConstant" in result else result["txFeeFixed"], + min_fee_coefficient=result["minFeeCoefficient"] if "minFeeCoefficient" in result else result["txFeePerByte"], + max_block_size=result["maxBlockBodySize"], + max_tx_size=result["maxTxSize"], + max_block_header_size=result["maxBlockHeaderSize"], + key_deposit=result["stakeAddressDeposit"], + pool_deposit=result["stakePoolDeposit"], + pool_influence=result["poolPledgeInfluence"], + monetary_expansion=result["monetaryExpansion"], + treasury_expansion=result["treasuryCut"], + decentralization_param=result.get("decentralization", 0), + extra_entropy=result.get("extraPraosEntropy", ""), + protocol_major_version=result["protocolVersion"]["major"], + protocol_minor_version=result["protocolVersion"]["minor"], + min_utxo=self._get_min_utxo(), + min_pool_cost=result["minPoolCost"], + price_mem=result["executionUnitPrices"]["priceMemory"] if "executionUnitPrices" in result else result["executionPrices"]["priceMemory"], + price_step=result["executionUnitPrices"]["priceSteps"] if "executionUnitPrices" in result else result["executionPrices"]["priceSteps"], + max_tx_ex_mem=result["maxTxExecutionUnits"]["memory"], + max_tx_ex_steps=result["maxTxExecutionUnits"]["steps"], + max_block_ex_mem=result["maxBlockExecutionUnits"]["memory"], + max_block_ex_steps=result["maxBlockExecutionUnits"]["steps"], + max_val_size=result["maxValueSize"], + collateral_percent=result["collateralPercentage"], + max_collateral_inputs=result["maxCollateralInputs"], + coins_per_utxo_word=result.get( + "coinsPerUtxoWord", ALONZO_COINS_PER_UTXO_WORD + ), + coins_per_utxo_byte=result.get("coinsPerUtxoByte", 0), + cost_models=self._parse_cost_models(result), + ) + + @staticmethod + def _fraction_parser(fraction: str) -> float: + x, y = fraction.split("/") + return int(x) / int(y) + + @property + def protocol_param(self) -> ProtocolParameters: + """Get current protocol parameters""" + if not self._protocol_param or self._is_chain_tip_updated(): + self._protocol_param = self._fetch_protocol_param() + return self._protocol_param + + @property + def genesis_param(self) -> GenesisParameters: + """Get chain genesis parameters""" + genesis_params = self._query_genesis_config() + return GenesisParameters( + active_slots_coefficient=genesis_params["activeSlotsCoeff"], + update_quorum=genesis_params["updateQuorum"], + max_lovelace_supply=genesis_params["maxLovelaceSupply"], + network_magic=genesis_params["networkMagic"], + epoch_length=genesis_params["epochLength"], + system_start=genesis_params["systemStart"], + slots_per_kes_period=genesis_params["slotsPerKESPeriod"], + slot_length=genesis_params["slotLength"], + max_kes_evolutions=genesis_params["maxKESEvolutions"], + security_param=genesis_params["securityParam"], + ) + + @property + def network(self) -> CardanoCliNetwork: + """Cet current network""" + return self._network + + @property + def epoch(self) -> int: + """Current epoch number""" + result = self._query_chain_tip() + return result["epoch"] + + @property + def era(self) -> int: + """Current Cardano era""" + result = self._query_chain_tip() + return result["era"] + + @property + @func.ttl_cache(ttl=1) + def last_block_slot(self) -> int: + result = self._query_chain_tip() + return result["slot"] + + def version(self): + """ + Gets the cardano-cli version + """ + return self._run_command(["version"]) + + def _utxos(self, address: str) -> List[UTxO]: + """Get all UTxOs associated with an address. + + Args: + address (str): An address encoded with bech32. + + Returns: + List[UTxO]: A list of UTxOs. + """ + key = (self.last_block_slot, address) + if key in self._utxo_cache: + return self._utxo_cache[key] + + result = self._run_command(["query", "utxo", "--address", address] + self._network.value) + raw_utxos = result.split("\n")[2:] + + # Parse the UTXOs into a list of dict objects + utxos = [] + for utxo_line in raw_utxos: + if len(utxo_line) == 0: + continue + + vals = utxo_line.split() + utxo_dict = { + "tx_hash": vals[0], + "tx_ix": vals[1], + "lovelaces": int(vals[2]), + "type": vals[3], + } + + tx_in = TransactionInput.from_primitive([utxo_dict["tx_hash"], int(utxo_dict["tx_ix"])]) + lovelace_amount = utxo_dict["lovelaces"] + + tx_out = TransactionOutput( + Address.from_primitive(address), + amount=Value(coin=int(lovelace_amount)) + ) + + extra = [i for i, j in enumerate(vals) if j == "+"] + for i in extra: + if "TxOutDatumNone" in vals[i + 1]: + continue + elif "TxOutDatumHash" in vals[i + 1] and "Data" in vals[i + 2]: + datum_hash = DatumHash.from_primitive(vals[i + 3]) + tx_out.datum_hash = datum_hash + else: + multi_assets = MultiAsset() + + policy_id = vals[i + 2].split(".")[0] + asset_hex_name = vals[i + 2].split(".")[1] + quantity = vals[i + 1] + + policy = ScriptHash.from_primitive(policy_id) + asset_name = AssetName.from_primitive(asset_hex_name) + + multi_assets.setdefault(policy, Asset())[ + asset_name + ] = quantity + + tx_out.amount = Value(lovelace_amount, multi_assets) + + utxo = UTxO(input=tx_in, output=tx_out) + + utxos.append(utxo) + + self._utxo_cache[key] = utxos + + return utxos + + def submit_tx_cbor(self, cbor: Union[bytes, str]) -> str: + """Submit a transaction to the blockchain. + + Args: + cbor (Union[bytes, str]): The transaction to be submitted. + + Returns: + str: The transaction hash. + + Raises: + :class:`TransactionFailedException`: When fails to submit the transaction to blockchain. + :class:`PyCardanoException`: When fails to retrieve the transaction hash. + """ + if isinstance(cbor, bytes): + cbor = cbor.hex() + + with tempfile.NamedTemporaryFile(mode="w") as tmp_tx_file: + tx_json = { + "type": f"Witnessed Tx {self.era}Era", + "description": "Generated by PyCardano", + "cborHex": cbor + } + + tmp_tx_file.write(json.dumps(tx_json)) + + tmp_tx_file.flush() + + try: + self._run_command(["transaction", "submit", "--tx-file", tmp_tx_file.name] + self._network.value) + 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]) + except CardanoCliError as err: + raise PyCardanoException(f"Unable to get transaction id for {tmp_tx_file.name}") from err + + return txid diff --git a/pycardano/exception.py b/pycardano/exception.py index fdcdf498..d487b209 100644 --- a/pycardano/exception.py +++ b/pycardano/exception.py @@ -60,3 +60,7 @@ class MaxInputCountExceededException(UTxOSelectionException): class InputUTxODepletedException(UTxOSelectionException): pass + + +class CardanoCliError(PyCardanoException): + pass \ No newline at end of file From d1266d135d156afdba727cbac7a39e3ae089b3a9 Mon Sep 17 00:00:00 2001 From: Hareem Adderley Date: Sat, 7 Oct 2023 09:45:35 -0500 Subject: [PATCH 02/13] fix: allow instances of str to submit_tx_cbor --- pycardano/backend/base.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pycardano/backend/base.py b/pycardano/backend/base.py index 63aa09b7..1a2bfd4a 100644 --- a/pycardano/backend/base.py +++ b/pycardano/backend/base.py @@ -171,7 +171,7 @@ def submit_tx(self, tx: Union[Transaction, bytes, str]): """ if isinstance(tx, Transaction): return self.submit_tx_cbor(tx.to_cbor()) - elif isinstance(tx, bytes): + elif isinstance(tx, bytes) or isinstance(tx, str): return self.submit_tx_cbor(tx) else: raise InvalidArgumentException( From bcd683b5fcc505d599e58c581dd5ea34c20e887a Mon Sep 17 00:00:00 2001 From: Hareem Adderley Date: Sat, 7 Oct 2023 19:17:49 -0500 Subject: [PATCH 03/13] fix: cast to int for asset amount and check for None in get_min_utxo --- pycardano/backend/cardano_cli.py | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/pycardano/backend/cardano_cli.py b/pycardano/backend/cardano_cli.py index 3a78a00b..40035139 100644 --- a/pycardano/backend/cardano_cli.py +++ b/pycardano/backend/cardano_cli.py @@ -169,14 +169,14 @@ def _query_genesis_config(self) -> JsonDict: return genesis_json def _get_min_utxo(self) -> int: - params = self._query_genesis_config() - if "minUTxOValue" in params: + params = self._query_current_protocol_params() + if "minUTxOValue" in params and params["minUTxOValue"] is not None: return params["minUTxOValue"] - elif "lovelacePerUTxOWord" in params: + elif "lovelacePerUTxOWord" in params and params["lovelacePerUTxOWord"] is not None: return params["lovelacePerUTxOWord"] - elif "utxoCostPerWord" in params: + elif "utxoCostPerWord" in params and params["utxoCostPerWord"] is not None: return params["utxoCostPerWord"] - elif "utxoCostPerByte" in params: + elif "utxoCostPerByte" in params and params["utxoCostPerByte"] is not None: return params["utxoCostPerByte"] def _parse_cost_models(self, cli_result: JsonDict) -> Dict[str, Dict[str, int]]: @@ -346,7 +346,7 @@ def _utxos(self, address: str) -> List[UTxO]: policy_id = vals[i + 2].split(".")[0] asset_hex_name = vals[i + 2].split(".")[1] - quantity = vals[i + 1] + quantity = int(vals[i + 1]) policy = ScriptHash.from_primitive(policy_id) asset_name = AssetName.from_primitive(asset_hex_name) From e1fc6c632282d7a46908d8e8faf61565a6b70edb Mon Sep 17 00:00:00 2001 From: Hareem Adderley Date: Sat, 7 Oct 2023 19:18:31 -0500 Subject: [PATCH 04/13] test: add test for cardano-cli chain context --- test/pycardano/backend/conftest.py | 220 +++++++++ test/pycardano/backend/test_cardano_cli.py | 543 +++++++++++++++++++++ 2 files changed, 763 insertions(+) create mode 100644 test/pycardano/backend/conftest.py create mode 100644 test/pycardano/backend/test_cardano_cli.py diff --git a/test/pycardano/backend/conftest.py b/test/pycardano/backend/conftest.py new file mode 100644 index 00000000..6398958b --- /dev/null +++ b/test/pycardano/backend/conftest.py @@ -0,0 +1,220 @@ +import json +from pathlib import Path +from unittest.mock import patch + +import pytest + +@pytest.fixture(scope="session") +def genesis_json(): + return { + "activeSlotsCoeff": 0.05, + "epochLength": 432000, + "genDelegs": { + "637f2e950b0fd8f8e3e811c5fbeb19e411e7a2bf37272b84b29c1a0b": { + "delegate": "aae9293510344ddd636364c2673e34e03e79e3eefa8dbaa70e326f7d", + "vrf": "227116365af2ed943f1a8b5e6557bfaa34996f1578eec667a5e2b361c51e4ce7" + }, + "8a4b77c4f534f8b8cc6f269e5ebb7ba77fa63a476e50e05e66d7051c": { + "delegate": "d15422b2e8b60e500a82a8f4ceaa98b04e55a0171d1125f6c58f8758", + "vrf": "0ada6c25d62db5e1e35d3df727635afa943b9e8a123ab83785e2281605b09ce2" + }, + "b00470cd193d67aac47c373602fccd4195aad3002c169b5570de1126": { + "delegate": "b3b539e9e7ed1b32fbf778bf2ebf0a6b9f980eac90ac86623d11881a", + "vrf": "0ff0ce9b820376e51c03b27877cd08f8ba40318f1a9f85a3db0b60dd03f71a7a" + }, + "b260ffdb6eba541fcf18601923457307647dce807851b9d19da133ab": { + "delegate": "7c64eb868b4ef566391a321c85323f41d2b95480d7ce56ad2abcb022", + "vrf": "7fb22abd39d550c9a022ec8104648a26240a9ff9c88b8b89a6e20d393c03098e" + }, + "ced1599fd821a39593e00592e5292bdc1437ae0f7af388ef5257344a": { + "delegate": "de7ca985023cf892f4de7f5f1d0a7181668884752d9ebb9e96c95059", + "vrf": "c301b7fc4d1b57fb60841bcec5e3d2db89602e5285801e522fce3790987b1124" + }, + "dd2a7d71a05bed11db61555ba4c658cb1ce06c8024193d064f2a66ae": { + "delegate": "1e113c218899ee7807f4028071d0e108fc790dade9fd1a0d0b0701ee", + "vrf": "faf2702aa4893c877c622ab22dfeaf1d0c8aab98b837fe2bf667314f0d043822" + }, + "f3b9e74f7d0f24d2314ea5dfbca94b65b2059d1ff94d97436b82d5b4": { + "delegate": "fd637b08cc379ef7b99c83b416458fcda8a01a606041779331008fb9", + "vrf": "37f2ea7c843a688159ddc2c38a2f997ab465150164a9136dca69564714b73268" + } + }, + "initialFunds": {}, + "maxKESEvolutions": 62, + "maxLovelaceSupply": 45000000000000000, + "networkId": "Testnet", + "networkMagic": 1, + "protocolParams": { + "protocolVersion": { + "minor": 0, + "major": 2 + }, + "decentralisationParam": 1, + "eMax": 18, + "extraEntropy": { + "tag": "NeutralNonce" + }, + "maxTxSize": 16384, + "maxBlockBodySize": 65536, + "maxBlockHeaderSize": 1100, + "minFeeA": 44, + "minFeeB": 155381, + "minUTxOValue": 1000000, + "poolDeposit": 500000000, + "minPoolCost": 340000000, + "keyDeposit": 2000000, + "nOpt": 150, + "rho": 0.003, + "tau": 0.20, + "a0": 0.3 + }, + "securityParam": 2160, + "slotLength": 1, + "slotsPerKESPeriod": 129600, + "staking": { + "pools": {}, + "stake": {} + }, + "systemStart": "2022-06-01T00:00:00Z", + "updateQuorum": 5 + } + + +@pytest.fixture(autouse=True) +def mock_check_socket(): + with patch("pathlib.Path.exists", return_value=True), patch( + "pathlib.Path.is_socket", return_value=True + ), patch("pathlib.Path.is_file", return_value=True): + yield + + +@pytest.fixture(scope="session") +def genesis_file(genesis_json): + genesis_file_path = Path.cwd() / "shelley-genesis.json" + + with open(genesis_file_path, "w", encoding="utf-8") as file: + file.write(json.dumps(genesis_json, indent=4)) + + yield genesis_file_path + + genesis_file_path.unlink() + + +@pytest.fixture(scope="session") +def config_file(): + config_file_path = Path.cwd() / "config.json" + + config_json = { + "AlonzoGenesisFile": "alonzo-genesis.json", + "AlonzoGenesisHash": "7e94a15f55d1e82d10f09203fa1d40f8eede58fd8066542cf6566008068ed874", + "ApplicationName": "cardano-sl", + "ApplicationVersion": 0, + "ByronGenesisFile": "byron-genesis.json", + "ByronGenesisHash": "d4b8de7a11d929a323373cbab6c1a9bdc931beffff11db111cf9d57356ee1937", + "ConwayGenesisFile": "conway-genesis.json", + "ConwayGenesisHash": "f28f1c1280ea0d32f8cd3143e268650d6c1a8e221522ce4a7d20d62fc09783e1", + "EnableP2P": True, + "LastKnownBlockVersion-Alt": 0, + "LastKnownBlockVersion-Major": 2, + "LastKnownBlockVersion-Minor": 0, + "Protocol": "Cardano", + "RequiresNetworkMagic": "RequiresMagic", + "ShelleyGenesisFile": "shelley-genesis.json", + "ShelleyGenesisHash": "162d29c4e1cf6b8a84f2d692e67a3ac6bc7851bc3e6e4afe64d15778bed8bd86", + "TargetNumberOfActivePeers": 20, + "TargetNumberOfEstablishedPeers": 50, + "TargetNumberOfKnownPeers": 100, + "TargetNumberOfRootPeers": 100, + "TraceAcceptPolicy": True, + "TraceBlockFetchClient": False, + "TraceBlockFetchDecisions": False, + "TraceBlockFetchProtocol": False, + "TraceBlockFetchProtocolSerialised": False, + "TraceBlockFetchServer": False, + "TraceChainDb": True, + "TraceChainSyncBlockServer": False, + "TraceChainSyncClient": False, + "TraceChainSyncHeaderServer": False, + "TraceChainSyncProtocol": False, + "TraceConnectionManager": True, + "TraceDNSResolver": True, + "TraceDNSSubscription": True, + "TraceDiffusionInitialization": True, + "TraceErrorPolicy": True, + "TraceForge": True, + "TraceHandshake": False, + "TraceInboundGovernor": True, + "TraceIpSubscription": True, + "TraceLedgerPeers": True, + "TraceLocalChainSyncProtocol": False, + "TraceLocalErrorPolicy": True, + "TraceLocalHandshake": False, + "TraceLocalRootPeers": True, + "TraceLocalTxSubmissionProtocol": False, + "TraceLocalTxSubmissionServer": False, + "TraceMempool": True, + "TraceMux": False, + "TracePeerSelection": True, + "TracePeerSelectionActions": True, + "TracePublicRootPeers": True, + "TraceServer": True, + "TraceTxInbound": False, + "TraceTxOutbound": False, + "TraceTxSubmissionProtocol": False, + "TracingVerbosity": "NormalVerbosity", + "TurnOnLogMetrics": True, + "TurnOnLogging": True, + "defaultBackends": [ + "KatipBK" + ], + "defaultScribes": [ + [ + "StdoutSK", + "stdout" + ] + ], + "hasEKG": 12788, + "hasPrometheus": [ + "0.0.0.0", + 12798 + ], + "minSeverity": "Info", + "options": { + "mapBackends": { + "cardano.node.metrics": [ + "EKGViewBK" + ], + "cardano.node.resources": [ + "EKGViewBK" + ] + }, + "mapSubtrace": { + "cardano.node.metrics": { + "subtrace": "Neutral" + } + } + }, + "rotation": { + "rpKeepFilesNum": 10, + "rpLogLimitBytes": 5000000, + "rpMaxAgeHours": 24 + }, + "setupBackends": [ + "KatipBK" + ], + "setupScribes": [ + { + "scFormat": "ScText", + "scKind": "StdoutSK", + "scName": "stdout", + "scRotation": None + } + ] + } + + with open(config_file_path, "w", encoding="utf-8") as file: + file.write(json.dumps(config_json, indent=4)) + + yield config_file_path + + config_file_path.unlink() diff --git a/test/pycardano/backend/test_cardano_cli.py b/test/pycardano/backend/test_cardano_cli.py new file mode 100644 index 00000000..9b85e624 --- /dev/null +++ b/test/pycardano/backend/test_cardano_cli.py @@ -0,0 +1,543 @@ +import json +from pathlib import Path +from typing import List +from unittest.mock import patch + +import pytest + +from pycardano import CardanoCliChainContext, ProtocolParameters, ALONZO_COINS_PER_UTXO_WORD, CardanoCliNetwork, \ + GenesisParameters, TransactionInput, MultiAsset + +QUERY_TIP_RESULT = { + "block": 1460093, + "epoch": 98, + "era": "Babbage", + "hash": "c1bda7b2975dd3bf9969a57d92528ba7d60383b6e1c4a37b68379c4f4330e790", + "slot": 41008115, + "slotInEpoch": 313715, + "slotsToEpochEnd": 118285, + "syncProgress": "100.00" +} + +QUERY_PROTOCOL_PARAMETERS_RESULT = { + "collateralPercentage": 150, + "costModels": { + "PlutusV1": [ + 205665, + 812, + 1, + 1, + 1000, + 571, + 0, + 1, + 1000, + 24177, + 4, + 1, + 1000, + 32, + 117366, + 10475, + 4, + 23000, + 100, + 23000, + 100, + 23000, + 100, + 23000, + 100, + 23000, + 100, + 23000, + 100, + 100, + 100, + 23000, + 100, + 19537, + 32, + 175354, + 32, + 46417, + 4, + 221973, + 511, + 0, + 1, + 89141, + 32, + 497525, + 14068, + 4, + 2, + 196500, + 453240, + 220, + 0, + 1, + 1, + 1000, + 28662, + 4, + 2, + 245000, + 216773, + 62, + 1, + 1060367, + 12586, + 1, + 208512, + 421, + 1, + 187000, + 1000, + 52998, + 1, + 80436, + 32, + 43249, + 32, + 1000, + 32, + 80556, + 1, + 57667, + 4, + 1000, + 10, + 197145, + 156, + 1, + 197145, + 156, + 1, + 204924, + 473, + 1, + 208896, + 511, + 1, + 52467, + 32, + 64832, + 32, + 65493, + 32, + 22558, + 32, + 16563, + 32, + 76511, + 32, + 196500, + 453240, + 220, + 0, + 1, + 1, + 69522, + 11687, + 0, + 1, + 60091, + 32, + 196500, + 453240, + 220, + 0, + 1, + 1, + 196500, + 453240, + 220, + 0, + 1, + 1, + 806990, + 30482, + 4, + 1927926, + 82523, + 4, + 265318, + 0, + 4, + 0, + 85931, + 32, + 205665, + 812, + 1, + 1, + 41182, + 32, + 212342, + 32, + 31220, + 32, + 32696, + 32, + 43357, + 32, + 32247, + 32, + 38314, + 32, + 57996947, + 18975, + 10 + ], + "PlutusV2": [ + 205665, + 812, + 1, + 1, + 1000, + 571, + 0, + 1, + 1000, + 24177, + 4, + 1, + 1000, + 32, + 117366, + 10475, + 4, + 23000, + 100, + 23000, + 100, + 23000, + 100, + 23000, + 100, + 23000, + 100, + 23000, + 100, + 100, + 100, + 23000, + 100, + 19537, + 32, + 175354, + 32, + 46417, + 4, + 221973, + 511, + 0, + 1, + 89141, + 32, + 497525, + 14068, + 4, + 2, + 196500, + 453240, + 220, + 0, + 1, + 1, + 1000, + 28662, + 4, + 2, + 245000, + 216773, + 62, + 1, + 1060367, + 12586, + 1, + 208512, + 421, + 1, + 187000, + 1000, + 52998, + 1, + 80436, + 32, + 43249, + 32, + 1000, + 32, + 80556, + 1, + 57667, + 4, + 1000, + 10, + 197145, + 156, + 1, + 197145, + 156, + 1, + 204924, + 473, + 1, + 208896, + 511, + 1, + 52467, + 32, + 64832, + 32, + 65493, + 32, + 22558, + 32, + 16563, + 32, + 76511, + 32, + 196500, + 453240, + 220, + 0, + 1, + 1, + 69522, + 11687, + 0, + 1, + 60091, + 32, + 196500, + 453240, + 220, + 0, + 1, + 1, + 196500, + 453240, + 220, + 0, + 1, + 1, + 1159724, + 392670, + 0, + 2, + 806990, + 30482, + 4, + 1927926, + 82523, + 4, + 265318, + 0, + 4, + 0, + 85931, + 32, + 205665, + 812, + 1, + 1, + 41182, + 32, + 212342, + 32, + 31220, + 32, + 32696, + 32, + 43357, + 32, + 32247, + 32, + 38314, + 32, + 35892428, + 10, + 57996947, + 18975, + 10, + 38887044, + 32947, + 10 + ] + }, + "decentralization": None, + "executionUnitPrices": { + "priceMemory": 5.77e-2, + "priceSteps": 7.21e-5 + }, + "extraPraosEntropy": None, + "maxBlockBodySize": 90112, + "maxBlockExecutionUnits": { + "memory": 62000000, + "steps": 20000000000 + }, + "maxBlockHeaderSize": 1100, + "maxCollateralInputs": 3, + "maxTxExecutionUnits": { + "memory": 14000000, + "steps": 10000000000 + }, + "maxTxSize": 16384, + "maxValueSize": 5000, + "minPoolCost": 340000000, + "minUTxOValue": None, + "monetaryExpansion": 3.0e-3, + "poolPledgeInfluence": 0.3, + "poolRetireMaxEpoch": 18, + "protocolVersion": { + "major": 8, + "minor": 0 + }, + "stakeAddressDeposit": 2000000, + "stakePoolDeposit": 500000000, + "stakePoolTargetNum": 500, + "treasuryCut": 0.2, + "txFeeFixed": 155381, + "txFeePerByte": 44, + "utxoCostPerByte": 4310, + "utxoCostPerWord": None +} + +QUERY_UTXO_RESULT = """ TxHash TxIx Amount +-------------------------------------------------------------------------------------- +270be16fa17cdb3ef683bf2c28259c978d4b7088792074f177c8efda247e23f7 0 1000000 lovelace + TxOutDatumNone +270be16fa17cdb3ef683bf2c28259c978d4b7088792074f177c8efda247e23f7 1 9498624998 lovelace + 1000000000 328a60495759e0d8e244eca5b85b2467d142c8a755d6cd0592dff47b.6d656c636f696e +""" + + +def override_run_command(cmd: List[str]): + """ + Override the run_command method of CardanoCliChainContext to return a mock result + Args: + cmd: The command to run + + Returns: + The mock result + """ + 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 QUERY_UTXO_RESULT + if "txid" in cmd: + return "270be16fa17cdb3ef683bf2c28259c978d4b7088792074f177c8efda247e23f7" + else: + return None + + +@pytest.fixture +def chain_context(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, + ): + context = CardanoCliChainContext( + binary=Path("cardano-cli"), + socket=Path("node.socket"), + config_file=config_file, + network=CardanoCliNetwork.PREPROD) + context._run_command = override_run_command + return context + + +class TestCardanoCliChainContext: + def test_protocol_param(self, chain_context): + assert ( + ProtocolParameters( + min_fee_constant=QUERY_PROTOCOL_PARAMETERS_RESULT["txFeeFixed"], + min_fee_coefficient=QUERY_PROTOCOL_PARAMETERS_RESULT["txFeePerByte"], + max_block_size=QUERY_PROTOCOL_PARAMETERS_RESULT["maxBlockBodySize"], + max_tx_size=QUERY_PROTOCOL_PARAMETERS_RESULT["maxTxSize"], + max_block_header_size=QUERY_PROTOCOL_PARAMETERS_RESULT["maxBlockHeaderSize"], + key_deposit=QUERY_PROTOCOL_PARAMETERS_RESULT["stakeAddressDeposit"], + pool_deposit=QUERY_PROTOCOL_PARAMETERS_RESULT["stakePoolDeposit"], + pool_influence=QUERY_PROTOCOL_PARAMETERS_RESULT["poolPledgeInfluence"], + monetary_expansion=QUERY_PROTOCOL_PARAMETERS_RESULT["monetaryExpansion"], + treasury_expansion=QUERY_PROTOCOL_PARAMETERS_RESULT["treasuryCut"], + decentralization_param=QUERY_PROTOCOL_PARAMETERS_RESULT.get("decentralization", 0), + extra_entropy=QUERY_PROTOCOL_PARAMETERS_RESULT.get("extraPraosEntropy", ""), + protocol_major_version=int(QUERY_PROTOCOL_PARAMETERS_RESULT["protocolVersion"]["major"]), + protocol_minor_version=int(QUERY_PROTOCOL_PARAMETERS_RESULT["protocolVersion"]["minor"]), + min_utxo=QUERY_PROTOCOL_PARAMETERS_RESULT["utxoCostPerByte"], + min_pool_cost=QUERY_PROTOCOL_PARAMETERS_RESULT["minPoolCost"], + price_mem=float(QUERY_PROTOCOL_PARAMETERS_RESULT["executionUnitPrices"]["priceMemory"]), + price_step=float(QUERY_PROTOCOL_PARAMETERS_RESULT["executionUnitPrices"]["priceSteps"]), + max_tx_ex_mem=int(QUERY_PROTOCOL_PARAMETERS_RESULT["maxTxExecutionUnits"]["memory"]), + max_tx_ex_steps=int(QUERY_PROTOCOL_PARAMETERS_RESULT["maxTxExecutionUnits"]["steps"]), + max_block_ex_mem=int(QUERY_PROTOCOL_PARAMETERS_RESULT["maxBlockExecutionUnits"]["memory"]), + max_block_ex_steps=int(QUERY_PROTOCOL_PARAMETERS_RESULT["maxBlockExecutionUnits"]["steps"]), + max_val_size=QUERY_PROTOCOL_PARAMETERS_RESULT["maxValueSize"], + collateral_percent=QUERY_PROTOCOL_PARAMETERS_RESULT["collateralPercentage"], + max_collateral_inputs=QUERY_PROTOCOL_PARAMETERS_RESULT["maxCollateralInputs"], + coins_per_utxo_word=QUERY_PROTOCOL_PARAMETERS_RESULT.get( + "coinsPerUtxoWord", ALONZO_COINS_PER_UTXO_WORD + ), + coins_per_utxo_byte=QUERY_PROTOCOL_PARAMETERS_RESULT.get("coinsPerUtxoByte", 0), + cost_models=QUERY_PROTOCOL_PARAMETERS_RESULT["costModels"], + ) + == chain_context.protocol_param + ) + + def test_genesis(self, chain_context, genesis_json): + assert ( + GenesisParameters( + active_slots_coefficient=genesis_json["activeSlotsCoeff"], + update_quorum=genesis_json["updateQuorum"], + max_lovelace_supply=genesis_json["maxLovelaceSupply"], + network_magic=genesis_json["networkMagic"], + epoch_length=genesis_json["epochLength"], + system_start=genesis_json["systemStart"], + slots_per_kes_period=genesis_json["slotsPerKESPeriod"], + slot_length=genesis_json["slotLength"], + max_kes_evolutions=genesis_json["maxKESEvolutions"], + security_param=genesis_json["securityParam"], + ) + == chain_context.genesis_param + ) + + def test_utxo(self, chain_context): + results = chain_context.utxos( + "addr_test1qqmnh90jyfaajul4h2mawrxz4rfx04hpaadstm6y8wr90kyhf4dqfm247jlvna83g5wx9veaymzl6g9t833grknh3yhqxhzh4n" + ) + + assert results[0].input == TransactionInput.from_primitive( + ["270be16fa17cdb3ef683bf2c28259c978d4b7088792074f177c8efda247e23f7", 0] + ) + assert results[0].output.amount == 1000000 + + assert results[1].input == TransactionInput.from_primitive( + ["270be16fa17cdb3ef683bf2c28259c978d4b7088792074f177c8efda247e23f7", 1] + ) + assert results[1].output.amount.coin == 9498624998 + assert results[1].output.amount.multi_asset == MultiAsset.from_primitive( + { + "328a60495759e0d8e244eca5b85b2467d142c8a755d6cd0592dff47b": { + "6d656c636f696e": 1000000000 + } + } + ) + + def test_submit_tx(self, chain_context): + results = chain_context.submit_tx( + "testcborhexfromtransaction" + ) + + assert results == "270be16fa17cdb3ef683bf2c28259c978d4b7088792074f177c8efda247e23f7" From eaa41662b110a124b4e77e13566b799d49e20b61 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Niels=20M=C3=BCndler?= Date: Mon, 30 Oct 2023 14:02:48 +0100 Subject: [PATCH 05/13] Black formatting --- pycardano/backend/cardano_cli.py | 93 +++++++---- pycardano/exception.py | 2 +- test/pycardano/backend/conftest.py | 77 +++------ test/pycardano/backend/test_cardano_cli.py | 183 ++++++++++++--------- 4 files changed, 195 insertions(+), 160 deletions(-) diff --git a/pycardano/backend/cardano_cli.py b/pycardano/backend/cardano_cli.py index 40035139..8e79c3eb 100644 --- a/pycardano/backend/cardano_cli.py +++ b/pycardano/backend/cardano_cli.py @@ -20,7 +20,11 @@ GenesisParameters, ProtocolParameters, ) -from pycardano.exception import TransactionFailedException, CardanoCliError, PyCardanoException +from pycardano.exception import ( + TransactionFailedException, + CardanoCliError, + PyCardanoException, +) from pycardano.hash import DatumHash, ScriptHash from pycardano.transaction import ( Asset, @@ -84,14 +88,14 @@ class CardanoCliChainContext(ChainContext): _datum_cache: Cache def __init__( - self, - binary: Path, - socket: Path, - config_file: Path, - network: CardanoCliNetwork, - refetch_chain_tip_interval: Optional[float] = None, - utxo_cache_size: int = 10000, - datum_cache_size: int = 10000, + self, + binary: Path, + socket: Path, + config_file: Path, + network: CardanoCliNetwork, + refetch_chain_tip_interval: Optional[float] = None, + utxo_cache_size: int = 10000, + datum_cache_size: int = 10000, ): if not binary.exists() or not binary.is_file(): raise CardanoCliError(f"cardano-cli binary file not found: {binary}") @@ -124,8 +128,8 @@ def __init__( self._protocol_param = None if refetch_chain_tip_interval is None: self._refetch_chain_tip_interval = ( - self.genesis_param.slot_length - / self.genesis_param.active_slots_coefficient + self.genesis_param.slot_length + / self.genesis_param.active_slots_coefficient ) self._utxo_cache = TTLCache( @@ -153,7 +157,9 @@ def _query_chain_tip(self) -> JsonDict: return json.loads(result) def _query_current_protocol_params(self) -> JsonDict: - result = self._run_command(["query", "protocol-parameters"] + self._network.value) + result = self._run_command( + ["query", "protocol-parameters"] + self._network.value + ) return json.loads(result) def _query_genesis_config(self) -> JsonDict: @@ -161,9 +167,13 @@ def _query_genesis_config(self) -> JsonDict: raise CardanoCliError(f"Cardano config file not found: {self._config_file}") with open(self._config_file, encoding="utf-8") as config_file: config_json = json.load(config_file) - shelly_genesis_file = self._config_file.parent / config_json["ShelleyGenesisFile"] + shelly_genesis_file = ( + self._config_file.parent / config_json["ShelleyGenesisFile"] + ) if not shelly_genesis_file.exists() or not shelly_genesis_file.is_file(): - raise CardanoCliError(f"Shelly Genesis file not found: {shelly_genesis_file}") + raise CardanoCliError( + f"Shelly Genesis file not found: {shelly_genesis_file}" + ) with open(shelly_genesis_file, encoding="utf-8") as genesis_file: genesis_json = json.load(genesis_file) return genesis_json @@ -172,7 +182,10 @@ def _get_min_utxo(self) -> int: params = self._query_current_protocol_params() if "minUTxOValue" in params and params["minUTxOValue"] is not None: return params["minUTxOValue"] - elif "lovelacePerUTxOWord" in params and params["lovelacePerUTxOWord"] is not None: + elif ( + "lovelacePerUTxOWord" in params + and params["lovelacePerUTxOWord"] is not None + ): return params["lovelacePerUTxOWord"] elif "utxoCostPerWord" in params and params["utxoCostPerWord"] is not None: return params["utxoCostPerWord"] @@ -206,8 +219,12 @@ def _is_chain_tip_updated(self): def _fetch_protocol_param(self) -> ProtocolParameters: result = self._query_current_protocol_params() return ProtocolParameters( - min_fee_constant=result["minFeeConstant"] if "minFeeConstant" in result else result["txFeeFixed"], - min_fee_coefficient=result["minFeeCoefficient"] if "minFeeCoefficient" in result else result["txFeePerByte"], + min_fee_constant=result["minFeeConstant"] + if "minFeeConstant" in result + else result["txFeeFixed"], + min_fee_coefficient=result["minFeeCoefficient"] + if "minFeeCoefficient" in result + else result["txFeePerByte"], max_block_size=result["maxBlockBodySize"], max_tx_size=result["maxTxSize"], max_block_header_size=result["maxBlockHeaderSize"], @@ -222,8 +239,12 @@ def _fetch_protocol_param(self) -> ProtocolParameters: protocol_minor_version=result["protocolVersion"]["minor"], min_utxo=self._get_min_utxo(), min_pool_cost=result["minPoolCost"], - price_mem=result["executionUnitPrices"]["priceMemory"] if "executionUnitPrices" in result else result["executionPrices"]["priceMemory"], - price_step=result["executionUnitPrices"]["priceSteps"] if "executionUnitPrices" in result else result["executionPrices"]["priceSteps"], + price_mem=result["executionUnitPrices"]["priceMemory"] + if "executionUnitPrices" in result + else result["executionPrices"]["priceMemory"], + price_step=result["executionUnitPrices"]["priceSteps"] + if "executionUnitPrices" in result + else result["executionPrices"]["priceSteps"], max_tx_ex_mem=result["maxTxExecutionUnits"]["memory"], max_tx_ex_steps=result["maxTxExecutionUnits"]["steps"], max_block_ex_mem=result["maxBlockExecutionUnits"]["memory"], @@ -309,7 +330,9 @@ def _utxos(self, address: str) -> List[UTxO]: if key in self._utxo_cache: return self._utxo_cache[key] - result = self._run_command(["query", "utxo", "--address", address] + self._network.value) + result = self._run_command( + ["query", "utxo", "--address", address] + self._network.value + ) raw_utxos = result.split("\n")[2:] # Parse the UTXOs into a list of dict objects @@ -326,12 +349,13 @@ def _utxos(self, address: str) -> List[UTxO]: "type": vals[3], } - tx_in = TransactionInput.from_primitive([utxo_dict["tx_hash"], int(utxo_dict["tx_ix"])]) + tx_in = TransactionInput.from_primitive( + [utxo_dict["tx_hash"], int(utxo_dict["tx_ix"])] + ) lovelace_amount = utxo_dict["lovelaces"] tx_out = TransactionOutput( - Address.from_primitive(address), - amount=Value(coin=int(lovelace_amount)) + Address.from_primitive(address), amount=Value(coin=int(lovelace_amount)) ) extra = [i for i, j in enumerate(vals) if j == "+"] @@ -351,9 +375,7 @@ def _utxos(self, address: str) -> List[UTxO]: policy = ScriptHash.from_primitive(policy_id) asset_name = AssetName.from_primitive(asset_hex_name) - multi_assets.setdefault(policy, Asset())[ - asset_name - ] = quantity + multi_assets.setdefault(policy, Asset())[asset_name] = quantity tx_out.amount = Value(lovelace_amount, multi_assets) @@ -385,7 +407,7 @@ def submit_tx_cbor(self, cbor: Union[bytes, str]) -> str: tx_json = { "type": f"Witnessed Tx {self.era}Era", "description": "Generated by PyCardano", - "cborHex": cbor + "cborHex": cbor, } tmp_tx_file.write(json.dumps(tx_json)) @@ -393,14 +415,23 @@ def submit_tx_cbor(self, cbor: Union[bytes, str]) -> str: tmp_tx_file.flush() try: - self._run_command(["transaction", "submit", "--tx-file", tmp_tx_file.name] + self._network.value) + self._run_command( + ["transaction", "submit", "--tx-file", tmp_tx_file.name] + + self._network.value + ) except CardanoCliError as err: - raise TransactionFailedException("Failed to submit transaction") from 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]) + 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 + raise PyCardanoException( + f"Unable to get transaction id for {tmp_tx_file.name}" + ) from err return txid diff --git a/pycardano/exception.py b/pycardano/exception.py index d487b209..c8772142 100644 --- a/pycardano/exception.py +++ b/pycardano/exception.py @@ -63,4 +63,4 @@ class InputUTxODepletedException(UTxOSelectionException): class CardanoCliError(PyCardanoException): - pass \ No newline at end of file + pass diff --git a/test/pycardano/backend/conftest.py b/test/pycardano/backend/conftest.py index 6398958b..23571df5 100644 --- a/test/pycardano/backend/conftest.py +++ b/test/pycardano/backend/conftest.py @@ -4,6 +4,7 @@ import pytest + @pytest.fixture(scope="session") def genesis_json(): return { @@ -12,32 +13,32 @@ def genesis_json(): "genDelegs": { "637f2e950b0fd8f8e3e811c5fbeb19e411e7a2bf37272b84b29c1a0b": { "delegate": "aae9293510344ddd636364c2673e34e03e79e3eefa8dbaa70e326f7d", - "vrf": "227116365af2ed943f1a8b5e6557bfaa34996f1578eec667a5e2b361c51e4ce7" + "vrf": "227116365af2ed943f1a8b5e6557bfaa34996f1578eec667a5e2b361c51e4ce7", }, "8a4b77c4f534f8b8cc6f269e5ebb7ba77fa63a476e50e05e66d7051c": { "delegate": "d15422b2e8b60e500a82a8f4ceaa98b04e55a0171d1125f6c58f8758", - "vrf": "0ada6c25d62db5e1e35d3df727635afa943b9e8a123ab83785e2281605b09ce2" + "vrf": "0ada6c25d62db5e1e35d3df727635afa943b9e8a123ab83785e2281605b09ce2", }, "b00470cd193d67aac47c373602fccd4195aad3002c169b5570de1126": { "delegate": "b3b539e9e7ed1b32fbf778bf2ebf0a6b9f980eac90ac86623d11881a", - "vrf": "0ff0ce9b820376e51c03b27877cd08f8ba40318f1a9f85a3db0b60dd03f71a7a" + "vrf": "0ff0ce9b820376e51c03b27877cd08f8ba40318f1a9f85a3db0b60dd03f71a7a", }, "b260ffdb6eba541fcf18601923457307647dce807851b9d19da133ab": { "delegate": "7c64eb868b4ef566391a321c85323f41d2b95480d7ce56ad2abcb022", - "vrf": "7fb22abd39d550c9a022ec8104648a26240a9ff9c88b8b89a6e20d393c03098e" + "vrf": "7fb22abd39d550c9a022ec8104648a26240a9ff9c88b8b89a6e20d393c03098e", }, "ced1599fd821a39593e00592e5292bdc1437ae0f7af388ef5257344a": { "delegate": "de7ca985023cf892f4de7f5f1d0a7181668884752d9ebb9e96c95059", - "vrf": "c301b7fc4d1b57fb60841bcec5e3d2db89602e5285801e522fce3790987b1124" + "vrf": "c301b7fc4d1b57fb60841bcec5e3d2db89602e5285801e522fce3790987b1124", }, "dd2a7d71a05bed11db61555ba4c658cb1ce06c8024193d064f2a66ae": { "delegate": "1e113c218899ee7807f4028071d0e108fc790dade9fd1a0d0b0701ee", - "vrf": "faf2702aa4893c877c622ab22dfeaf1d0c8aab98b837fe2bf667314f0d043822" + "vrf": "faf2702aa4893c877c622ab22dfeaf1d0c8aab98b837fe2bf667314f0d043822", }, "f3b9e74f7d0f24d2314ea5dfbca94b65b2059d1ff94d97436b82d5b4": { "delegate": "fd637b08cc379ef7b99c83b416458fcda8a01a606041779331008fb9", - "vrf": "37f2ea7c843a688159ddc2c38a2f997ab465150164a9136dca69564714b73268" - } + "vrf": "37f2ea7c843a688159ddc2c38a2f997ab465150164a9136dca69564714b73268", + }, }, "initialFunds": {}, "maxKESEvolutions": 62, @@ -45,15 +46,10 @@ def genesis_json(): "networkId": "Testnet", "networkMagic": 1, "protocolParams": { - "protocolVersion": { - "minor": 0, - "major": 2 - }, + "protocolVersion": {"minor": 0, "major": 2}, "decentralisationParam": 1, "eMax": 18, - "extraEntropy": { - "tag": "NeutralNonce" - }, + "extraEntropy": {"tag": "NeutralNonce"}, "maxTxSize": 16384, "maxBlockBodySize": 65536, "maxBlockHeaderSize": 1100, @@ -66,24 +62,21 @@ def genesis_json(): "nOpt": 150, "rho": 0.003, "tau": 0.20, - "a0": 0.3 + "a0": 0.3, }, "securityParam": 2160, "slotLength": 1, "slotsPerKESPeriod": 129600, - "staking": { - "pools": {}, - "stake": {} - }, + "staking": {"pools": {}, "stake": {}}, "systemStart": "2022-06-01T00:00:00Z", - "updateQuorum": 5 + "updateQuorum": 5, } @pytest.fixture(autouse=True) def mock_check_socket(): with patch("pathlib.Path.exists", return_value=True), patch( - "pathlib.Path.is_socket", return_value=True + "pathlib.Path.is_socket", return_value=True ), patch("pathlib.Path.is_file", return_value=True): yield @@ -164,52 +157,32 @@ def config_file(): "TracingVerbosity": "NormalVerbosity", "TurnOnLogMetrics": True, "TurnOnLogging": True, - "defaultBackends": [ - "KatipBK" - ], - "defaultScribes": [ - [ - "StdoutSK", - "stdout" - ] - ], + "defaultBackends": ["KatipBK"], + "defaultScribes": [["StdoutSK", "stdout"]], "hasEKG": 12788, - "hasPrometheus": [ - "0.0.0.0", - 12798 - ], + "hasPrometheus": ["0.0.0.0", 12798], "minSeverity": "Info", "options": { "mapBackends": { - "cardano.node.metrics": [ - "EKGViewBK" - ], - "cardano.node.resources": [ - "EKGViewBK" - ] + "cardano.node.metrics": ["EKGViewBK"], + "cardano.node.resources": ["EKGViewBK"], }, - "mapSubtrace": { - "cardano.node.metrics": { - "subtrace": "Neutral" - } - } + "mapSubtrace": {"cardano.node.metrics": {"subtrace": "Neutral"}}, }, "rotation": { "rpKeepFilesNum": 10, "rpLogLimitBytes": 5000000, - "rpMaxAgeHours": 24 + "rpMaxAgeHours": 24, }, - "setupBackends": [ - "KatipBK" - ], + "setupBackends": ["KatipBK"], "setupScribes": [ { "scFormat": "ScText", "scKind": "StdoutSK", "scName": "stdout", - "scRotation": None + "scRotation": None, } - ] + ], } with open(config_file_path, "w", encoding="utf-8") as file: diff --git a/test/pycardano/backend/test_cardano_cli.py b/test/pycardano/backend/test_cardano_cli.py index 9b85e624..8f02c84c 100644 --- a/test/pycardano/backend/test_cardano_cli.py +++ b/test/pycardano/backend/test_cardano_cli.py @@ -5,8 +5,15 @@ import pytest -from pycardano import CardanoCliChainContext, ProtocolParameters, ALONZO_COINS_PER_UTXO_WORD, CardanoCliNetwork, \ - GenesisParameters, TransactionInput, MultiAsset +from pycardano import ( + CardanoCliChainContext, + ProtocolParameters, + ALONZO_COINS_PER_UTXO_WORD, + CardanoCliNetwork, + GenesisParameters, + TransactionInput, + MultiAsset, +) QUERY_TIP_RESULT = { "block": 1460093, @@ -16,7 +23,7 @@ "slot": 41008115, "slotInEpoch": 313715, "slotsToEpochEnd": 118285, - "syncProgress": "100.00" + "syncProgress": "100.00", } QUERY_PROTOCOL_PARAMETERS_RESULT = { @@ -188,7 +195,7 @@ 32, 57996947, 18975, - 10 + 10, ], "PlutusV2": [ 205665, @@ -365,26 +372,17 @@ 10, 38887044, 32947, - 10 - ] + 10, + ], }, "decentralization": None, - "executionUnitPrices": { - "priceMemory": 5.77e-2, - "priceSteps": 7.21e-5 - }, + "executionUnitPrices": {"priceMemory": 5.77e-2, "priceSteps": 7.21e-5}, "extraPraosEntropy": None, "maxBlockBodySize": 90112, - "maxBlockExecutionUnits": { - "memory": 62000000, - "steps": 20000000000 - }, + "maxBlockExecutionUnits": {"memory": 62000000, "steps": 20000000000}, "maxBlockHeaderSize": 1100, "maxCollateralInputs": 3, - "maxTxExecutionUnits": { - "memory": 14000000, - "steps": 10000000000 - }, + "maxTxExecutionUnits": {"memory": 14000000, "steps": 10000000000}, "maxTxSize": 16384, "maxValueSize": 5000, "minPoolCost": 340000000, @@ -392,10 +390,7 @@ "monetaryExpansion": 3.0e-3, "poolPledgeInfluence": 0.3, "poolRetireMaxEpoch": 18, - "protocolVersion": { - "major": 8, - "minor": 0 - }, + "protocolVersion": {"major": 8, "minor": 0}, "stakeAddressDeposit": 2000000, "stakePoolDeposit": 500000000, "stakePoolTargetNum": 500, @@ -403,7 +398,7 @@ "txFeeFixed": 155381, "txFeePerByte": 44, "utxoCostPerByte": 4310, - "utxoCostPerWord": None + "utxoCostPerWord": None, } QUERY_UTXO_RESULT = """ TxHash TxIx Amount @@ -446,14 +441,15 @@ def chain_context(genesis_file, config_file): The CardanoCliChainContext """ with patch( - "pycardano.backend.cardano_cli.CardanoCliChainContext._run_command", - side_effect=override_run_command, + "pycardano.backend.cardano_cli.CardanoCliChainContext._run_command", + side_effect=override_run_command, ): context = CardanoCliChainContext( binary=Path("cardano-cli"), socket=Path("node.socket"), config_file=config_file, - network=CardanoCliNetwork.PREPROD) + network=CardanoCliNetwork.PREPROD, + ) context._run_command = override_run_command return context @@ -461,56 +457,90 @@ def chain_context(genesis_file, config_file): class TestCardanoCliChainContext: def test_protocol_param(self, chain_context): assert ( - ProtocolParameters( - min_fee_constant=QUERY_PROTOCOL_PARAMETERS_RESULT["txFeeFixed"], - min_fee_coefficient=QUERY_PROTOCOL_PARAMETERS_RESULT["txFeePerByte"], - max_block_size=QUERY_PROTOCOL_PARAMETERS_RESULT["maxBlockBodySize"], - max_tx_size=QUERY_PROTOCOL_PARAMETERS_RESULT["maxTxSize"], - max_block_header_size=QUERY_PROTOCOL_PARAMETERS_RESULT["maxBlockHeaderSize"], - key_deposit=QUERY_PROTOCOL_PARAMETERS_RESULT["stakeAddressDeposit"], - pool_deposit=QUERY_PROTOCOL_PARAMETERS_RESULT["stakePoolDeposit"], - pool_influence=QUERY_PROTOCOL_PARAMETERS_RESULT["poolPledgeInfluence"], - monetary_expansion=QUERY_PROTOCOL_PARAMETERS_RESULT["monetaryExpansion"], - treasury_expansion=QUERY_PROTOCOL_PARAMETERS_RESULT["treasuryCut"], - decentralization_param=QUERY_PROTOCOL_PARAMETERS_RESULT.get("decentralization", 0), - extra_entropy=QUERY_PROTOCOL_PARAMETERS_RESULT.get("extraPraosEntropy", ""), - protocol_major_version=int(QUERY_PROTOCOL_PARAMETERS_RESULT["protocolVersion"]["major"]), - protocol_minor_version=int(QUERY_PROTOCOL_PARAMETERS_RESULT["protocolVersion"]["minor"]), - min_utxo=QUERY_PROTOCOL_PARAMETERS_RESULT["utxoCostPerByte"], - min_pool_cost=QUERY_PROTOCOL_PARAMETERS_RESULT["minPoolCost"], - price_mem=float(QUERY_PROTOCOL_PARAMETERS_RESULT["executionUnitPrices"]["priceMemory"]), - price_step=float(QUERY_PROTOCOL_PARAMETERS_RESULT["executionUnitPrices"]["priceSteps"]), - max_tx_ex_mem=int(QUERY_PROTOCOL_PARAMETERS_RESULT["maxTxExecutionUnits"]["memory"]), - max_tx_ex_steps=int(QUERY_PROTOCOL_PARAMETERS_RESULT["maxTxExecutionUnits"]["steps"]), - max_block_ex_mem=int(QUERY_PROTOCOL_PARAMETERS_RESULT["maxBlockExecutionUnits"]["memory"]), - max_block_ex_steps=int(QUERY_PROTOCOL_PARAMETERS_RESULT["maxBlockExecutionUnits"]["steps"]), - max_val_size=QUERY_PROTOCOL_PARAMETERS_RESULT["maxValueSize"], - collateral_percent=QUERY_PROTOCOL_PARAMETERS_RESULT["collateralPercentage"], - max_collateral_inputs=QUERY_PROTOCOL_PARAMETERS_RESULT["maxCollateralInputs"], - coins_per_utxo_word=QUERY_PROTOCOL_PARAMETERS_RESULT.get( - "coinsPerUtxoWord", ALONZO_COINS_PER_UTXO_WORD - ), - coins_per_utxo_byte=QUERY_PROTOCOL_PARAMETERS_RESULT.get("coinsPerUtxoByte", 0), - cost_models=QUERY_PROTOCOL_PARAMETERS_RESULT["costModels"], - ) - == chain_context.protocol_param + ProtocolParameters( + min_fee_constant=QUERY_PROTOCOL_PARAMETERS_RESULT["txFeeFixed"], + min_fee_coefficient=QUERY_PROTOCOL_PARAMETERS_RESULT["txFeePerByte"], + max_block_size=QUERY_PROTOCOL_PARAMETERS_RESULT["maxBlockBodySize"], + max_tx_size=QUERY_PROTOCOL_PARAMETERS_RESULT["maxTxSize"], + max_block_header_size=QUERY_PROTOCOL_PARAMETERS_RESULT[ + "maxBlockHeaderSize" + ], + key_deposit=QUERY_PROTOCOL_PARAMETERS_RESULT["stakeAddressDeposit"], + pool_deposit=QUERY_PROTOCOL_PARAMETERS_RESULT["stakePoolDeposit"], + pool_influence=QUERY_PROTOCOL_PARAMETERS_RESULT["poolPledgeInfluence"], + monetary_expansion=QUERY_PROTOCOL_PARAMETERS_RESULT[ + "monetaryExpansion" + ], + treasury_expansion=QUERY_PROTOCOL_PARAMETERS_RESULT["treasuryCut"], + decentralization_param=QUERY_PROTOCOL_PARAMETERS_RESULT.get( + "decentralization", 0 + ), + extra_entropy=QUERY_PROTOCOL_PARAMETERS_RESULT.get( + "extraPraosEntropy", "" + ), + protocol_major_version=int( + QUERY_PROTOCOL_PARAMETERS_RESULT["protocolVersion"]["major"] + ), + protocol_minor_version=int( + QUERY_PROTOCOL_PARAMETERS_RESULT["protocolVersion"]["minor"] + ), + min_utxo=QUERY_PROTOCOL_PARAMETERS_RESULT["utxoCostPerByte"], + min_pool_cost=QUERY_PROTOCOL_PARAMETERS_RESULT["minPoolCost"], + price_mem=float( + QUERY_PROTOCOL_PARAMETERS_RESULT["executionUnitPrices"][ + "priceMemory" + ] + ), + price_step=float( + QUERY_PROTOCOL_PARAMETERS_RESULT["executionUnitPrices"][ + "priceSteps" + ] + ), + max_tx_ex_mem=int( + QUERY_PROTOCOL_PARAMETERS_RESULT["maxTxExecutionUnits"]["memory"] + ), + max_tx_ex_steps=int( + QUERY_PROTOCOL_PARAMETERS_RESULT["maxTxExecutionUnits"]["steps"] + ), + max_block_ex_mem=int( + QUERY_PROTOCOL_PARAMETERS_RESULT["maxBlockExecutionUnits"]["memory"] + ), + max_block_ex_steps=int( + QUERY_PROTOCOL_PARAMETERS_RESULT["maxBlockExecutionUnits"]["steps"] + ), + max_val_size=QUERY_PROTOCOL_PARAMETERS_RESULT["maxValueSize"], + collateral_percent=QUERY_PROTOCOL_PARAMETERS_RESULT[ + "collateralPercentage" + ], + max_collateral_inputs=QUERY_PROTOCOL_PARAMETERS_RESULT[ + "maxCollateralInputs" + ], + coins_per_utxo_word=QUERY_PROTOCOL_PARAMETERS_RESULT.get( + "coinsPerUtxoWord", ALONZO_COINS_PER_UTXO_WORD + ), + coins_per_utxo_byte=QUERY_PROTOCOL_PARAMETERS_RESULT.get( + "coinsPerUtxoByte", 0 + ), + cost_models=QUERY_PROTOCOL_PARAMETERS_RESULT["costModels"], + ) + == chain_context.protocol_param ) def test_genesis(self, chain_context, genesis_json): assert ( - GenesisParameters( - active_slots_coefficient=genesis_json["activeSlotsCoeff"], - update_quorum=genesis_json["updateQuorum"], - max_lovelace_supply=genesis_json["maxLovelaceSupply"], - network_magic=genesis_json["networkMagic"], - epoch_length=genesis_json["epochLength"], - system_start=genesis_json["systemStart"], - slots_per_kes_period=genesis_json["slotsPerKESPeriod"], - slot_length=genesis_json["slotLength"], - max_kes_evolutions=genesis_json["maxKESEvolutions"], - security_param=genesis_json["securityParam"], - ) - == chain_context.genesis_param + GenesisParameters( + active_slots_coefficient=genesis_json["activeSlotsCoeff"], + update_quorum=genesis_json["updateQuorum"], + max_lovelace_supply=genesis_json["maxLovelaceSupply"], + network_magic=genesis_json["networkMagic"], + epoch_length=genesis_json["epochLength"], + system_start=genesis_json["systemStart"], + slots_per_kes_period=genesis_json["slotsPerKESPeriod"], + slot_length=genesis_json["slotLength"], + max_kes_evolutions=genesis_json["maxKESEvolutions"], + security_param=genesis_json["securityParam"], + ) + == chain_context.genesis_param ) def test_utxo(self, chain_context): @@ -536,8 +566,9 @@ def test_utxo(self, chain_context): ) def test_submit_tx(self, chain_context): - results = chain_context.submit_tx( - "testcborhexfromtransaction" - ) + results = chain_context.submit_tx("testcborhexfromtransaction") - assert results == "270be16fa17cdb3ef683bf2c28259c978d4b7088792074f177c8efda247e23f7" + assert ( + results + == "270be16fa17cdb3ef683bf2c28259c978d4b7088792074f177c8efda247e23f7" + ) From 45fa2660383937f2da7dafd47039b2d10d49b219 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Niels=20M=C3=BCndler?= Date: Mon, 30 Oct 2023 14:07:21 +0100 Subject: [PATCH 06/13] Fix some QA issues --- pycardano/backend/cardano_cli.py | 12 ++++++++---- 1 file changed, 8 insertions(+), 4 deletions(-) diff --git a/pycardano/backend/cardano_cli.py b/pycardano/backend/cardano_cli.py index 8e79c3eb..4d8c8909 100644 --- a/pycardano/backend/cardano_cli.py +++ b/pycardano/backend/cardano_cli.py @@ -13,6 +13,7 @@ from cachetools import Cache, LRUCache, TTLCache, func +from pycardano import Network from pycardano.address import Address from pycardano.backend.base import ( ALONZO_COINS_PER_UTXO_WORD, @@ -75,9 +76,9 @@ class CardanoCliNetwork(Enum): class CardanoCliChainContext(ChainContext): - _binary: Optional[Path] + _binary: Path _socket: Optional[Path] - _config_file: Optional[Path] + _config_file: Path _mode: Mode _network: CardanoCliNetwork _last_known_block_slot: int @@ -191,6 +192,7 @@ def _get_min_utxo(self) -> int: return params["utxoCostPerWord"] elif "utxoCostPerByte" in params and params["utxoCostPerByte"] is not None: return params["utxoCostPerByte"] + raise ValueError("Cannot determine minUTxOValue, invalid protocol params") def _parse_cost_models(self, cli_result: JsonDict) -> Dict[str, Dict[str, int]]: cli_cost_models = cli_result.get("costModels", {}) @@ -289,9 +291,11 @@ def genesis_param(self) -> GenesisParameters: ) @property - def network(self) -> CardanoCliNetwork: + def network(self) -> Network: """Cet current network""" - return self._network + if self._network == CardanoCliNetwork.MAINNET: + return Network.MAINNET + return Network.TESTNET @property def epoch(self) -> int: From 20b544f4f0f29e1471568b05fa343abe4f5220da Mon Sep 17 00:00:00 2001 From: Hareem Adderley Date: Thu, 2 Nov 2023 11:36:17 -0500 Subject: [PATCH 07/13] refactor: use `--out-file /dev/stdout` to get utxo data as json --- pycardano/backend/cardano_cli.py | 116 +++++++++++++-------- test/pycardano/backend/test_cardano_cli.py | 33 +++--- 2 files changed, 93 insertions(+), 56 deletions(-) diff --git a/pycardano/backend/cardano_cli.py b/pycardano/backend/cardano_cli.py index 4d8c8909..48b8d81c 100644 --- a/pycardano/backend/cardano_cli.py +++ b/pycardano/backend/cardano_cli.py @@ -13,7 +13,11 @@ from cachetools import Cache, LRUCache, TTLCache, func -from pycardano import Network +from pycardano.serialization import RawCBOR +from pycardano.nativescript import NativeScript +from pycardano.plutus import PlutusV2Script, PlutusV1Script + +from pycardano.network import Network from pycardano.address import Address from pycardano.backend.base import ( ALONZO_COINS_PER_UTXO_WORD, @@ -194,7 +198,8 @@ def _get_min_utxo(self) -> int: return params["utxoCostPerByte"] raise ValueError("Cannot determine minUTxOValue, invalid protocol params") - def _parse_cost_models(self, cli_result: JsonDict) -> Dict[str, Dict[str, int]]: + @staticmethod + def _parse_cost_models(cli_result: JsonDict) -> Dict[str, Dict[str, int]]: cli_cost_models = cli_result.get("costModels", {}) cost_models = {} @@ -211,7 +216,7 @@ def _parse_cost_models(self, cli_result: JsonDict) -> Dict[str, Dict[str, int]]: return cost_models def _is_chain_tip_updated(self): - # fetch at most every twenty seconds! + # fetch at almost every twenty seconds! if time.time() - self._last_chain_tip_fetch < self._refetch_chain_tip_interval: return False self._last_chain_tip_fetch = time.time() @@ -321,6 +326,29 @@ def version(self): """ return self._run_command(["version"]) + @staticmethod + def _get_script( + reference_script: dict, + ) -> Union[PlutusV1Script, PlutusV2Script, NativeScript]: + """ + Get a script object from a reference script dictionary. + Args: + reference_script: + + Returns: + + """ + script_type = reference_script["script"]["type"] + script_json: JsonDict = reference_script["script"] + if script_type == "PlutusScriptV1": + v1script = PlutusV1Script(bytes.fromhex(script_json["cborHex"])) + return v1script + elif script_type == "PlutusScriptV2": + v2script = PlutusV2Script(bytes.fromhex(script_json["cborHex"])) + return v2script + else: + return NativeScript.from_dict(script_json) + def _utxos(self, address: str) -> List[UTxO]: """Get all UTxOs associated with an address. @@ -335,57 +363,61 @@ def _utxos(self, address: str) -> List[UTxO]: return self._utxo_cache[key] result = self._run_command( - ["query", "utxo", "--address", address] + self._network.value + ["query", "utxo", "--address", address, "--out-file", "/dev/stdout"] + + self._network.value ) - raw_utxos = result.split("\n")[2:] - # Parse the UTXOs into a list of dict objects + raw_utxos = json.loads(result) + utxos = [] - for utxo_line in raw_utxos: - if len(utxo_line) == 0: - continue - - vals = utxo_line.split() - utxo_dict = { - "tx_hash": vals[0], - "tx_ix": vals[1], - "lovelaces": int(vals[2]), - "type": vals[3], - } + for tx_hash in raw_utxos.keys(): + tx_id, tx_idx = tx_hash.split("#") + utxo = raw_utxos[tx_hash] + tx_in = TransactionInput.from_primitive([tx_id, int(tx_idx)]) + + value = Value() + multi_asset = MultiAsset() + for asset in utxo["value"].keys(): + if asset == "lovelace": + value.coin = utxo["value"][asset] + else: + policy_id = asset + policy = ScriptHash.from_primitive(policy_id) - tx_in = TransactionInput.from_primitive( - [utxo_dict["tx_hash"], int(utxo_dict["tx_ix"])] - ) - lovelace_amount = utxo_dict["lovelaces"] + for asset_hex_name in utxo["value"][asset].keys(): + asset_name = AssetName.from_primitive(asset_hex_name) + amount = utxo["value"][asset][asset_hex_name] + multi_asset.setdefault(policy, Asset())[asset_name] = amount - tx_out = TransactionOutput( - Address.from_primitive(address), amount=Value(coin=int(lovelace_amount)) - ) + value.multi_asset = multi_asset - extra = [i for i, j in enumerate(vals) if j == "+"] - for i in extra: - if "TxOutDatumNone" in vals[i + 1]: - continue - elif "TxOutDatumHash" in vals[i + 1] and "Data" in vals[i + 2]: - datum_hash = DatumHash.from_primitive(vals[i + 3]) - tx_out.datum_hash = datum_hash - else: - multi_assets = MultiAsset() + datum_hash = ( + DatumHash.from_primitive(utxo["datumhash"]) + if utxo.get("datumhash") and utxo.get("inlineDatum") is None + else None + ) - policy_id = vals[i + 2].split(".")[0] - asset_hex_name = vals[i + 2].split(".")[1] - quantity = int(vals[i + 1]) + datum = None - policy = ScriptHash.from_primitive(policy_id) - asset_name = AssetName.from_primitive(asset_hex_name) + if utxo.get("datum"): + datum = RawCBOR(bytes.fromhex(utxo["datum"])) + elif utxo.get("inlineDatumhash"): + datum = utxo["inlineDatum"] - multi_assets.setdefault(policy, Asset())[asset_name] = quantity + script = None - tx_out.amount = Value(lovelace_amount, multi_assets) + if utxo.get("referenceScript"): + script = self._get_script(utxo["referenceScript"]) - utxo = UTxO(input=tx_in, output=tx_out) + tx_out = TransactionOutput( + Address.from_primitive(utxo["address"]), + amount=value, + datum_hash=datum_hash, + datum=datum, + script=script, + ) - utxos.append(utxo) + utxos.append(UTxO(tx_in, tx_out)) self._utxo_cache[key] = utxos diff --git a/test/pycardano/backend/test_cardano_cli.py b/test/pycardano/backend/test_cardano_cli.py index 8f02c84c..a4470519 100644 --- a/test/pycardano/backend/test_cardano_cli.py +++ b/test/pycardano/backend/test_cardano_cli.py @@ -401,11 +401,7 @@ "utxoCostPerWord": None, } -QUERY_UTXO_RESULT = """ TxHash TxIx Amount --------------------------------------------------------------------------------------- -270be16fa17cdb3ef683bf2c28259c978d4b7088792074f177c8efda247e23f7 0 1000000 lovelace + TxOutDatumNone -270be16fa17cdb3ef683bf2c28259c978d4b7088792074f177c8efda247e23f7 1 9498624998 lovelace + 1000000000 328a60495759e0d8e244eca5b85b2467d142c8a755d6cd0592dff47b.6d656c636f696e -""" +QUERY_UTXO_RESULT = '{"fbaa018740241abb935240051134914389c3f94647d8bd6c30cb32d3fdb799bf#0": {"address": "addr1x8nz307k3sr60gu0e47cmajssy4fmld7u493a4xztjrll0aj764lvrxdayh2ux30fl0ktuh27csgmpevdu89jlxppvrswgxsta", "datum": null, "inlineDatum": {"constructor": 0, "fields": [{"constructor": 0, "fields": [{"bytes": "2e11e7313e00ccd086cfc4f1c3ebed4962d31b481b6a153c23601c0f"}, {"bytes": "636861726c69335f6164615f6e6674"}]}, {"constructor": 0, "fields": [{"bytes": ""}, {"bytes": ""}]}, {"constructor": 0, "fields": [{"bytes": "8e51398904a5d3fc129fbf4f1589701de23c7824d5c90fdb9490e15a"}, {"bytes": "434841524c4933"}]}, {"constructor": 0, "fields": [{"bytes": "d8d46a3e430fab5dc8c5a0a7fc82abbf4339a89034a8c804bb7e6012"}, {"bytes": "636861726c69335f6164615f6c71"}]}, {"int": 997}, {"list": [{"bytes": "4dd98a2ef34bc7ac3858bbcfdf94aaa116bb28ca7e01756140ba4d19"}]}, {"int": 10000000000}]}, "inlineDatumhash": "c56003cba9cfcf2f73cf6a5f4d6354d03c281bcd2bbd7a873d7475faa10a7123", "referenceScript": null, "value": {"2e11e7313e00ccd086cfc4f1c3ebed4962d31b481b6a153c23601c0f": {"636861726c69335f6164615f6e6674": 1}, "8e51398904a5d3fc129fbf4f1589701de23c7824d5c90fdb9490e15a": {"434841524c4933": 1367726755}, "d8d46a3e430fab5dc8c5a0a7fc82abbf4339a89034a8c804bb7e6012": {"636861726c69335f6164615f6c71": 9223372035870126880}, "lovelace": 708864940}}}' def override_run_command(cmd: List[str]): @@ -549,19 +545,28 @@ def test_utxo(self, chain_context): ) assert results[0].input == TransactionInput.from_primitive( - ["270be16fa17cdb3ef683bf2c28259c978d4b7088792074f177c8efda247e23f7", 0] + ["fbaa018740241abb935240051134914389c3f94647d8bd6c30cb32d3fdb799bf", 0] ) - assert results[0].output.amount == 1000000 + assert results[0].output.amount.coin == 708864940 - assert results[1].input == TransactionInput.from_primitive( - ["270be16fa17cdb3ef683bf2c28259c978d4b7088792074f177c8efda247e23f7", 1] + assert ( + str(results[0].output.address) + == "addr1x8nz307k3sr60gu0e47cmajssy4fmld7u493a4xztjrll0aj764lvrxdayh2ux30fl0ktuh27csgmpevdu89jlxppvrswgxsta" ) - assert results[1].output.amount.coin == 9498624998 - assert results[1].output.amount.multi_asset == MultiAsset.from_primitive( + + assert isinstance(results[0].output.datum, dict) + + assert results[0].output.amount.multi_asset == MultiAsset.from_primitive( { - "328a60495759e0d8e244eca5b85b2467d142c8a755d6cd0592dff47b": { - "6d656c636f696e": 1000000000 - } + "2e11e7313e00ccd086cfc4f1c3ebed4962d31b481b6a153c23601c0f": { + "636861726c69335f6164615f6e6674": 1 + }, + "8e51398904a5d3fc129fbf4f1589701de23c7824d5c90fdb9490e15a": { + "434841524c4933": 1367726755 + }, + "d8d46a3e430fab5dc8c5a0a7fc82abbf4339a89034a8c804bb7e6012": { + "636861726c69335f6164615f6c71": 9223372035870126880 + }, } ) From efe16980a58670ac6fbffdc188ca34b0dd5cdc03 Mon Sep 17 00:00:00 2001 From: Hareem Adderley Date: Thu, 2 Nov 2023 11:43:46 -0500 Subject: [PATCH 08/13] fix: remove unused offline/online mode code --- pycardano/backend/cardano_cli.py | 12 ------------ 1 file changed, 12 deletions(-) diff --git a/pycardano/backend/cardano_cli.py b/pycardano/backend/cardano_cli.py index 48b8d81c..846aff7e 100644 --- a/pycardano/backend/cardano_cli.py +++ b/pycardano/backend/cardano_cli.py @@ -45,15 +45,6 @@ __all__ = ["CardanoCliChainContext", "CardanoCliNetwork"] -class Mode(str, Enum): - """ - Mode enumeration. - """ - - ONLINE = "online" - OFFLINE = "offline" - - def network_magic(magic_number: int) -> List[str]: """ Returns the network magic number for the cardano-cli @@ -83,7 +74,6 @@ class CardanoCliChainContext(ChainContext): _binary: Path _socket: Optional[Path] _config_file: Path - _mode: Mode _network: CardanoCliNetwork _last_known_block_slot: int _last_chain_tip_fetch: float @@ -114,10 +104,8 @@ def __init__( self._socket = socket os.environ["CARDANO_NODE_SOCKET_PATH"] = self._socket.as_posix() - self._mode = Mode.ONLINE except CardanoCliError: self._socket = None - self._mode = Mode.OFFLINE self._binary = binary self._network = network From 6ae4e642b12ab2195ba436573126232f17b1d9df Mon Sep 17 00:00:00 2001 From: Hareem Adderley Date: Thu, 2 Nov 2023 11:45:56 -0500 Subject: [PATCH 09/13] fix: remove unused fraction parser method --- pycardano/backend/cardano_cli.py | 5 ----- 1 file changed, 5 deletions(-) diff --git a/pycardano/backend/cardano_cli.py b/pycardano/backend/cardano_cli.py index 846aff7e..788aa95a 100644 --- a/pycardano/backend/cardano_cli.py +++ b/pycardano/backend/cardano_cli.py @@ -254,11 +254,6 @@ def _fetch_protocol_param(self) -> ProtocolParameters: cost_models=self._parse_cost_models(result), ) - @staticmethod - def _fraction_parser(fraction: str) -> float: - x, y = fraction.split("/") - return int(x) / int(y) - @property def protocol_param(self) -> ProtocolParameters: """Get current protocol parameters""" From 57d4f2e2bd825c2b7f413b714efcca95b7a11362 Mon Sep 17 00:00:00 2001 From: Hareem Adderley Date: Thu, 2 Nov 2023 20:42:50 -0500 Subject: [PATCH 10/13] fix: add docker configuration to use cardano-cli in a Docker container and network args method to use custom networks --- poetry.lock | 50 ++++++++++++++- pycardano/backend/cardano_cli.py | 101 ++++++++++++++++++++++++------- pyproject.toml | 1 + 3 files changed, 128 insertions(+), 24 deletions(-) diff --git a/poetry.lock b/poetry.lock index 53b35dcd..ab80fded 100644 --- a/poetry.lock +++ b/poetry.lock @@ -547,6 +547,28 @@ files = [ {file = "decorator-5.1.1.tar.gz", hash = "sha256:637996211036b6385ef91435e4fae22989472f9d571faba8927ba8253acbc330"}, ] +[[package]] +name = "docker" +version = "6.1.3" +description = "A Python library for the Docker Engine API." +category = "main" +optional = false +python-versions = ">=3.7" +files = [ + {file = "docker-6.1.3-py3-none-any.whl", hash = "sha256:aecd2277b8bf8e506e484f6ab7aec39abe0038e29fa4a6d3ba86c3fe01844ed9"}, + {file = "docker-6.1.3.tar.gz", hash = "sha256:aa6d17830045ba5ef0168d5eaa34d37beeb113948c413affe1d5991fc11f9a20"}, +] + +[package.dependencies] +packaging = ">=14.0" +pywin32 = {version = ">=304", markers = "sys_platform == \"win32\""} +requests = ">=2.26.0" +urllib3 = ">=1.26.0" +websocket-client = ">=0.32.0" + +[package.extras] +ssh = ["paramiko (>=2.4.3)"] + [[package]] name = "docutils" version = "0.17.1" @@ -1059,7 +1081,7 @@ asn1crypto = ">=1.5.1" name = "packaging" version = "23.1" description = "Core utilities for Python packages" -category = "dev" +category = "main" optional = false python-versions = ">=3.7" files = [ @@ -1295,6 +1317,30 @@ files = [ {file = "pytz-2023.3.tar.gz", hash = "sha256:1d8ce29db189191fb55338ee6d0387d82ab59f3d00eac103412d64e0ebd0c588"}, ] +[[package]] +name = "pywin32" +version = "306" +description = "Python for Window Extensions" +category = "main" +optional = false +python-versions = "*" +files = [ + {file = "pywin32-306-cp310-cp310-win32.whl", hash = "sha256:06d3420a5155ba65f0b72f2699b5bacf3109f36acbe8923765c22938a69dfc8d"}, + {file = "pywin32-306-cp310-cp310-win_amd64.whl", hash = "sha256:84f4471dbca1887ea3803d8848a1616429ac94a4a8d05f4bc9c5dcfd42ca99c8"}, + {file = "pywin32-306-cp311-cp311-win32.whl", hash = "sha256:e65028133d15b64d2ed8f06dd9fbc268352478d4f9289e69c190ecd6818b6407"}, + {file = "pywin32-306-cp311-cp311-win_amd64.whl", hash = "sha256:a7639f51c184c0272e93f244eb24dafca9b1855707d94c192d4a0b4c01e1100e"}, + {file = "pywin32-306-cp311-cp311-win_arm64.whl", hash = "sha256:70dba0c913d19f942a2db25217d9a1b726c278f483a919f1abfed79c9cf64d3a"}, + {file = "pywin32-306-cp312-cp312-win32.whl", hash = "sha256:383229d515657f4e3ed1343da8be101000562bf514591ff383ae940cad65458b"}, + {file = "pywin32-306-cp312-cp312-win_amd64.whl", hash = "sha256:37257794c1ad39ee9be652da0462dc2e394c8159dfd913a8a4e8eb6fd346da0e"}, + {file = "pywin32-306-cp312-cp312-win_arm64.whl", hash = "sha256:5821ec52f6d321aa59e2db7e0a35b997de60c201943557d108af9d4ae1ec7040"}, + {file = "pywin32-306-cp37-cp37m-win32.whl", hash = "sha256:1c73ea9a0d2283d889001998059f5eaaba3b6238f767c9cf2833b13e6a685f65"}, + {file = "pywin32-306-cp37-cp37m-win_amd64.whl", hash = "sha256:72c5f621542d7bdd4fdb716227be0dd3f8565c11b280be6315b06ace35487d36"}, + {file = "pywin32-306-cp38-cp38-win32.whl", hash = "sha256:e4c092e2589b5cf0d365849e73e02c391c1349958c5ac3e9d5ccb9a28e017b3a"}, + {file = "pywin32-306-cp38-cp38-win_amd64.whl", hash = "sha256:e8ac1ae3601bee6ca9f7cb4b5363bf1c0badb935ef243c4733ff9a393b1690c0"}, + {file = "pywin32-306-cp39-cp39-win32.whl", hash = "sha256:e25fd5b485b55ac9c057f67d94bc203f3f6595078d1fb3b458c9c28b7153a802"}, + {file = "pywin32-306-cp39-cp39-win_amd64.whl", hash = "sha256:39b61c15272833b5c329a2989999dcae836b1eed650252ab1b7bfbe1d59f30f4"}, +] + [[package]] name = "requests" version = "2.28.2" @@ -1704,4 +1750,4 @@ testing = ["big-O", "flake8 (<5)", "jaraco.functools", "jaraco.itertools", "more [metadata] lock-version = "2.0" python-versions = "^3.7" -content-hash = "6d227be4b5f57497cf7b45d18e4e666faf87b53b0936f340be9521861d238868" +content-hash = "10d50e3a827a03b07829571b5dfcdb04270bcf07ed9f652a8d02d53ecdbbddae" diff --git a/pycardano/backend/cardano_cli.py b/pycardano/backend/cardano_cli.py index 788aa95a..04ba7c90 100644 --- a/pycardano/backend/cardano_cli.py +++ b/pycardano/backend/cardano_cli.py @@ -11,7 +11,9 @@ from pathlib import Path from typing import Optional, List, Dict, Union +import docker from cachetools import Cache, LRUCache, TTLCache, func +from docker.errors import APIError from pycardano.serialization import RawCBOR from pycardano.nativescript import NativeScript @@ -42,7 +44,7 @@ ) from pycardano.types import JsonDict -__all__ = ["CardanoCliChainContext", "CardanoCliNetwork"] +__all__ = ["CardanoCliChainContext", "CardanoCliNetwork", "DockerConfig"] def network_magic(magic_number: int) -> List[str]: @@ -70,6 +72,22 @@ class CardanoCliNetwork(Enum): CUSTOM = partial(network_magic) +class DockerConfig: + """ + Docker configuration to use the cardano-cli in a Docker container + """ + + container_name: str + """ The name of the Docker container containing the cardano-cli""" + + host_socket: Optional[Path] + """ The path to the Docker host socket file""" + + def __init__(self, container_name: str, host_socket: Optional[Path] = None): + self.container_name = container_name + self.host_socket = host_socket + + class CardanoCliChainContext(ChainContext): _binary: Path _socket: Optional[Path] @@ -81,6 +99,8 @@ class CardanoCliChainContext(ChainContext): _protocol_param: Optional[ProtocolParameters] _utxo_cache: Cache _datum_cache: Cache + _docker_config: Optional[DockerConfig] + _network_magic_number: Optional[int] def __init__( self, @@ -91,21 +111,24 @@ def __init__( refetch_chain_tip_interval: Optional[float] = None, utxo_cache_size: int = 10000, datum_cache_size: int = 10000, + docker_config: Optional[DockerConfig] = None, + network_magic_number: Optional[int] = None, ): - if not binary.exists() or not binary.is_file(): - raise CardanoCliError(f"cardano-cli binary file not found: {binary}") - - # Check the socket path file and set the CARDANO_NODE_SOCKET_PATH environment variable - try: - if not socket.exists(): + if docker_config is None: + if not binary.exists() or not binary.is_file(): raise CardanoCliError(f"cardano-cli binary file not found: {binary}") - elif not socket.is_socket(): - raise CardanoCliError(f"{socket} is not a socket file") - self._socket = socket - os.environ["CARDANO_NODE_SOCKET_PATH"] = self._socket.as_posix() - except CardanoCliError: - self._socket = None + # Check the socket path file and set the CARDANO_NODE_SOCKET_PATH environment variable + try: + if not socket.exists(): + raise CardanoCliError(f"cardano-node socket not found: {socket}") + elif not socket.is_socket(): + raise CardanoCliError(f"{socket} is not a socket file") + + self._socket = socket + os.environ["CARDANO_NODE_SOCKET_PATH"] = self._socket.as_posix() + except CardanoCliError: + self._socket = None self._binary = binary self._network = network @@ -129,29 +152,63 @@ def __init__( ttl=self._refetch_chain_tip_interval, maxsize=utxo_cache_size ) self._datum_cache = LRUCache(maxsize=datum_cache_size) + self._docker_config = docker_config + self._network_magic_number = network_magic_number + + @property + def _network_args(self) -> List[str]: + if self._network is CardanoCliNetwork.CUSTOM: + return self._network.value(self._network_magic_number) + else: + return self._network.value def _run_command(self, cmd: List[str]) -> str: """ - Runs the command in the cardano-cli + Runs the command in the cardano-cli. If the docker configuration is set, it will run the command in the + docker container. :param cmd: Command as a list of strings :return: The stdout if the command runs successfully """ try: - result = subprocess.run( - [self._binary.as_posix()] + cmd, capture_output=True, check=True - ) - return result.stdout.decode().strip() + if self._docker_config: + docker_config = self._docker_config + if docker_config.host_socket is None: + client = docker.from_env() + else: + client = docker.DockerClient( + base_url=docker_config.host_socket.as_posix() + ) + + container = client.containers.get(docker_config.container_name) + + exec_result = container.exec_run( + [self._binary.as_posix()] + cmd, stdout=True, stderr=True + ) + + if exec_result.exit_code == 0: + output = exec_result.output.decode() + return output + else: + error = exec_result.output.decode() + raise CardanoCliError(error) + else: + result = subprocess.run( + [self._binary.as_posix()] + cmd, capture_output=True, check=True + ) + return result.stdout.decode().strip() except subprocess.CalledProcessError as err: raise CardanoCliError(err.stderr.decode()) from err + except APIError as err: + raise CardanoCliError(err) from err def _query_chain_tip(self) -> JsonDict: - result = self._run_command(["query", "tip"] + self._network.value) + result = self._run_command(["query", "tip"] + self._network_args) return json.loads(result) def _query_current_protocol_params(self) -> JsonDict: result = self._run_command( - ["query", "protocol-parameters"] + self._network.value + ["query", "protocol-parameters"] + self._network_args ) return json.loads(result) @@ -347,7 +404,7 @@ def _utxos(self, address: str) -> List[UTxO]: result = self._run_command( ["query", "utxo", "--address", address, "--out-file", "/dev/stdout"] - + self._network.value + + self._network_args ) raw_utxos = json.loads(result) @@ -436,7 +493,7 @@ def submit_tx_cbor(self, cbor: Union[bytes, str]) -> str: try: self._run_command( ["transaction", "submit", "--tx-file", tmp_tx_file.name] - + self._network.value + + self._network_args ) except CardanoCliError as err: raise TransactionFailedException( diff --git a/pyproject.toml b/pyproject.toml index e57f6393..60da629b 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -35,6 +35,7 @@ ECPy = "^1.2.5" frozendict = "^2.3.8" frozenlist = "^1.3.3" cachetools = "^5.3.0" +docker = "^6.1.3" [tool.poetry.dev-dependencies] Sphinx = "^4.3.2" From 9a549dda6b128da65082fa00415bb6a9701a9c7b Mon Sep 17 00:00:00 2001 From: Hareem Adderley Date: Thu, 2 Nov 2023 20:50:04 -0500 Subject: [PATCH 11/13] test: add integration tests for cardano-cli --- integration-test/docker-compose.yml | 1 + integration-test/test/test_cardano_cli.py | 68 +++++++++++++++++++++++ 2 files changed, 69 insertions(+) create mode 100644 integration-test/test/test_cardano_cli.py diff --git a/integration-test/docker-compose.yml b/integration-test/docker-compose.yml index fb6fce14..e79fe347 100644 --- a/integration-test/docker-compose.yml +++ b/integration-test/docker-compose.yml @@ -13,6 +13,7 @@ services: entrypoint: bash environment: NETWORK: "${NETWORK:-local-alonzo}" + CARDANO_NODE_SOCKET_PATH: "/ipc/node.socket" command: /code/run_node.sh networks: diff --git a/integration-test/test/test_cardano_cli.py b/integration-test/test/test_cardano_cli.py new file mode 100644 index 00000000..4192f502 --- /dev/null +++ b/integration-test/test/test_cardano_cli.py @@ -0,0 +1,68 @@ +import os +from pathlib import Path + + +from pycardano import ( + CardanoCliChainContext, + CardanoCliNetwork, + ProtocolParameters, + GenesisParameters, + Network, +) +from pycardano.backend.cardano_cli import DockerConfig + + +class TestCardanoCli: + network_env = os.getenv("NETWORK", "local-alonzo") + host_socket = os.getenv("DOCKER_HOST", None) + network_magic = os.getenv("NETWORK_MAGIC", 42) + + configs_dir = Path(__file__).parent.parent / "configs" + + chain_context = CardanoCliChainContext( + binary=Path("cardano-cli"), + socket=Path("/ipc/node.socket"), + config_file=configs_dir / network_env / "config.json", + network=CardanoCliNetwork.CUSTOM, + docker_config=DockerConfig( + container_name="cardano-node", + host_socket=Path(host_socket) if host_socket else None, + ), + network_magic_number=int(network_magic), + ) + + def test_protocol_param(self): + protocol_param = self.chain_context.protocol_param + + assert protocol_param is not None + assert isinstance(protocol_param, ProtocolParameters) + + cost_models = protocol_param.cost_models + for _, cost_model in cost_models.items(): + assert "addInteger-cpu-arguments-intercept" in cost_model + assert "addInteger-cpu-arguments-slope" in cost_model + + def test_genesis_param(self): + genesis_param = self.chain_context.genesis_param + + assert genesis_param is not None + assert isinstance(genesis_param, GenesisParameters) + + def test_network(self): + network = self.chain_context.network + + assert network is not None + assert isinstance(network, Network) + + def test_epoch(self): + epoch = self.chain_context.epoch + + assert epoch is not None + assert isinstance(epoch, int) + assert epoch > 0 + + def test_last_block_slot(self): + last_block_slot = self.chain_context.last_block_slot + + assert isinstance(last_block_slot, int) + assert last_block_slot > 0 From 3e72507d390bf86b9a6348eb1cde2d45e65987e7 Mon Sep 17 00:00:00 2001 From: Hareem Adderley Date: Mon, 6 Nov 2023 09:56:31 -0500 Subject: [PATCH 12/13] test: fix cardano-node container name --- integration-test/test/test_cardano_cli.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/integration-test/test/test_cardano_cli.py b/integration-test/test/test_cardano_cli.py index 4192f502..ff0a07f1 100644 --- a/integration-test/test/test_cardano_cli.py +++ b/integration-test/test/test_cardano_cli.py @@ -25,7 +25,7 @@ class TestCardanoCli: config_file=configs_dir / network_env / "config.json", network=CardanoCliNetwork.CUSTOM, docker_config=DockerConfig( - container_name="cardano-node", + container_name="integration-test_cardano-node_1", host_socket=Path(host_socket) if host_socket else None, ), network_magic_number=int(network_magic), From ac4a29bb77aaa44fb97dc63165ab94207548a8ab Mon Sep 17 00:00:00 2001 From: Jerry Date: Mon, 1 Jan 2024 09:47:04 -0800 Subject: [PATCH 13/13] Add more integration tests for cardano cli context --- integration-test/docker-compose.yml | 1 + integration-test/test/test_cardano_cli.py | 3 +-- integration-test/test/test_plutus.py | 22 ++++++++++++++++++++++ pycardano/backend/cardano_cli.py | 22 +++++++++++++--------- test/pycardano/backend/test_cardano_cli.py | 6 +++--- 5 files changed, 40 insertions(+), 14 deletions(-) diff --git a/integration-test/docker-compose.yml b/integration-test/docker-compose.yml index e79fe347..ccdd13ee 100644 --- a/integration-test/docker-compose.yml +++ b/integration-test/docker-compose.yml @@ -22,6 +22,7 @@ services: volumes: - .:/code + - /tmp:/tmp - node-db:/data/db - node-ipc:/ipc ports: diff --git a/integration-test/test/test_cardano_cli.py b/integration-test/test/test_cardano_cli.py index ff0a07f1..b3e82fbe 100644 --- a/integration-test/test/test_cardano_cli.py +++ b/integration-test/test/test_cardano_cli.py @@ -1,13 +1,12 @@ import os from pathlib import Path - from pycardano import ( CardanoCliChainContext, CardanoCliNetwork, - ProtocolParameters, GenesisParameters, Network, + ProtocolParameters, ) from pycardano.backend.cardano_cli import DockerConfig diff --git a/integration-test/test/test_plutus.py b/integration-test/test/test_plutus.py index 784790ff..e33ea198 100644 --- a/integration-test/test/test_plutus.py +++ b/integration-test/test/test_plutus.py @@ -1,4 +1,6 @@ +import collections import time +from typing import Dict, Union import cbor2 import pytest @@ -7,6 +9,7 @@ from pycardano import * from .base import TEST_RETRIES, TestBase +from .test_cardano_cli import TestCardanoCli class TestPlutus(TestBase): @@ -371,3 +374,22 @@ class TestPlutusOgmiosOnly(TestPlutus): @classmethod def setup_class(cls): cls.chain_context._kupo_url = None + + +def evaluate_tx(tx: Transaction) -> Dict[str, ExecutionUnits]: + redeemers = tx.transaction_witness_set.redeemer + execution_units = {} + + if redeemers: + for r in redeemers: + k = f"{r.tag.name.lower()}:{r.index}" + execution_units[k] = ExecutionUnits(1000000, 1000000000) + + return execution_units + + +class TestPlutusCardanoCLI(TestPlutus): + @classmethod + def setup_class(cls): + cls.chain_context = TestCardanoCli.chain_context + cls.chain_context.evaluate_tx = evaluate_tx diff --git a/pycardano/backend/cardano_cli.py b/pycardano/backend/cardano_cli.py index 04ba7c90..5f506f6b 100644 --- a/pycardano/backend/cardano_cli.py +++ b/pycardano/backend/cardano_cli.py @@ -9,17 +9,13 @@ from enum import Enum from functools import partial from pathlib import Path -from typing import Optional, List, Dict, Union +from typing import Dict, List, Optional, Union +import cbor2 import docker from cachetools import Cache, LRUCache, TTLCache, func from docker.errors import APIError -from pycardano.serialization import RawCBOR -from pycardano.nativescript import NativeScript -from pycardano.plutus import PlutusV2Script, PlutusV1Script - -from pycardano.network import Network from pycardano.address import Address from pycardano.backend.base import ( ALONZO_COINS_PER_UTXO_WORD, @@ -28,11 +24,15 @@ ProtocolParameters, ) from pycardano.exception import ( - TransactionFailedException, CardanoCliError, PyCardanoException, + TransactionFailedException, ) from pycardano.hash import DatumHash, ScriptHash +from pycardano.nativescript import NativeScript +from pycardano.network import Network +from pycardano.plutus import PlutusV1Script, PlutusV2Script +from pycardano.serialization import RawCBOR from pycardano.transaction import ( Asset, AssetName, @@ -381,10 +381,14 @@ def _get_script( script_type = reference_script["script"]["type"] script_json: JsonDict = reference_script["script"] if script_type == "PlutusScriptV1": - v1script = PlutusV1Script(bytes.fromhex(script_json["cborHex"])) + v1script = PlutusV1Script( + cbor2.loads(bytes.fromhex(script_json["cborHex"])) + ) return v1script elif script_type == "PlutusScriptV2": - v2script = PlutusV2Script(bytes.fromhex(script_json["cborHex"])) + v2script = PlutusV2Script( + cbor2.loads(bytes.fromhex(script_json["cborHex"])) + ) return v2script else: return NativeScript.from_dict(script_json) diff --git a/test/pycardano/backend/test_cardano_cli.py b/test/pycardano/backend/test_cardano_cli.py index a4470519..785181e6 100644 --- a/test/pycardano/backend/test_cardano_cli.py +++ b/test/pycardano/backend/test_cardano_cli.py @@ -6,13 +6,13 @@ import pytest from pycardano import ( - CardanoCliChainContext, - ProtocolParameters, ALONZO_COINS_PER_UTXO_WORD, + CardanoCliChainContext, CardanoCliNetwork, GenesisParameters, - TransactionInput, MultiAsset, + ProtocolParameters, + TransactionInput, ) QUERY_TIP_RESULT = {