├── cardano_clusterlib ├── py.typed ├── exceptions.py ├── __init__.py ├── types.py ├── consts.py ├── helpers.py ├── clusterlib.py ├── gov_group.py ├── genesis_group.py ├── node_group.py ├── gov_committee_group.py ├── key_group.py ├── address_group.py ├── legacy_gov_group.py ├── gov_vote_group.py ├── gov_drep_group.py ├── clusterlib_helpers.py ├── stake_pool_group.py ├── structs.py ├── clusterlib_klass.py └── stake_address_group.py ├── docs ├── source │ ├── readme.rst │ ├── modules.rst │ ├── index.rst │ ├── conf.py │ └── cardano_clusterlib.rst ├── requirements.txt └── Makefile ├── requirements-dev.txt ├── .github ├── dependabot.yml └── workflows │ ├── repo_tests.yaml │ └── codeql.yml ├── .readthedocs.yaml ├── .markdownlint.yaml ├── Makefile ├── .gitignore ├── .pre-commit-config.yaml ├── pyproject.toml ├── CODE-OF-CONDUCT.md ├── README.md ├── upgrading └── refactor_to_0_4_0rc1.sed └── LICENSE /cardano_clusterlib/py.typed: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /docs/source/readme.rst: -------------------------------------------------------------------------------- 1 | .. mdinclude:: ../../README.md 2 | -------------------------------------------------------------------------------- /cardano_clusterlib/exceptions.py: -------------------------------------------------------------------------------- 1 | class CLIError(Exception): 2 | pass 3 | -------------------------------------------------------------------------------- /docs/requirements.txt: -------------------------------------------------------------------------------- 1 | Sphinx==8.1.3 2 | docutils==0.20.1 3 | m2r2==0.3.3.post2 4 | sphinx-rtd-theme==3.0.1 5 | -------------------------------------------------------------------------------- /docs/source/modules.rst: -------------------------------------------------------------------------------- 1 | Source Documentation 2 | ==================== 3 | 4 | .. toctree:: 5 | :maxdepth: 4 6 | 7 | cardano_clusterlib 8 | -------------------------------------------------------------------------------- /requirements-dev.txt: -------------------------------------------------------------------------------- 1 | -e . 2 | 3 | # linting 4 | mypy~=1.16.1 5 | pyrefly~=0.22.0 6 | pyright~=1.1.404 7 | pre-commit~=4.2.0 8 | 9 | build 10 | virtualenv 11 | -------------------------------------------------------------------------------- /cardano_clusterlib/__init__.py: -------------------------------------------------------------------------------- 1 | """Imports.""" 2 | 3 | from cardano_clusterlib.clusterlib_klass import ClusterLib 4 | from cardano_clusterlib.exceptions import CLIError 5 | 6 | __all__ = [ 7 | "CLIError", 8 | "ClusterLib", 9 | ] 10 | -------------------------------------------------------------------------------- /.github/dependabot.yml: -------------------------------------------------------------------------------- 1 | # Set update schedule for GitHub Actions 2 | 3 | version: 2 4 | updates: 5 | 6 | - package-ecosystem: "github-actions" 7 | directory: "/" 8 | schedule: 9 | # Check for updates to GitHub Actions every day 10 | interval: "daily" 11 | -------------------------------------------------------------------------------- /.readthedocs.yaml: -------------------------------------------------------------------------------- 1 | # Required 2 | version: 2 3 | 4 | 5 | # Set the version of Python and other tools you might need 6 | build: 7 | os: ubuntu-22.04 8 | tools: 9 | python: "3.10" 10 | 11 | # Build documentation in the docs/ directory with Sphinx 12 | sphinx: 13 | configuration: docs/source/conf.py 14 | 15 | # Optionally set the requirements required to build your docs 16 | python: 17 | install: 18 | - requirements: docs/requirements.txt 19 | - method: pip 20 | path: . 21 | -------------------------------------------------------------------------------- /cardano_clusterlib/types.py: -------------------------------------------------------------------------------- 1 | import pathlib as pl 2 | import typing as tp 3 | 4 | if tp.TYPE_CHECKING: 5 | from cardano_clusterlib.clusterlib_klass import ClusterLib # noqa: F401 6 | 7 | FileType = str | pl.Path 8 | FileTypeList = list[FileType] | list[str] | list[pl.Path] 9 | # List of `FileType`s, empty list, or empty tuple 10 | OptionalFiles = FileTypeList | tuple[()] 11 | # TODO: needed until https://github.com/python/typing/issues/256 is fixed 12 | UnpackableSequence = list | tuple | set | frozenset 13 | -------------------------------------------------------------------------------- /docs/source/index.rst: -------------------------------------------------------------------------------- 1 | .. cardano-clusterlib documentation master file, created by 2 | sphinx-quickstart on Thu Mar 11 11:45:19 2021. 3 | You can adapt this file completely to your liking, but it should at least 4 | contain the root `toctree` directive. 5 | 6 | Welcome to cardano-clusterlib's documentation! 7 | ============================================== 8 | 9 | .. toctree:: 10 | :maxdepth: 2 11 | :caption: Contents: 12 | 13 | readme 14 | modules 15 | 16 | 17 | Indices and tables 18 | ================== 19 | 20 | * :ref:`genindex` 21 | * :ref:`modindex` 22 | * :ref:`search` 23 | -------------------------------------------------------------------------------- /.markdownlint.yaml: -------------------------------------------------------------------------------- 1 | # Default state for all rules 2 | default: true 3 | 4 | # MD012/no-multiple-blanks - Multiple consecutive blank lines 5 | MD012: 6 | # Consecutive blank lines 7 | maximum: 2 8 | 9 | # MD013/line-length - Line length 10 | MD013: 11 | # Number of characters 12 | line_length: 1000 13 | # Number of characters for headings 14 | heading_line_length: 128 15 | # Include code blocks 16 | code_blocks: false 17 | # Include tables 18 | tables: true 19 | # Include headings 20 | headings: true 21 | # Include headings 22 | headers: true 23 | # Strict length checking 24 | strict: false 25 | # Stern length checking 26 | stern: false 27 | 28 | # MD024/no-duplicate-heading/no-duplicate-header - Multiple headings with the same content 29 | MD024: 30 | allow_different_nesting: true 31 | siblings_only: true 32 | -------------------------------------------------------------------------------- /docs/Makefile: -------------------------------------------------------------------------------- 1 | # Minimal makefile for Sphinx documentation 2 | 3 | SPHINXOPTS ?= 4 | SPHINXBUILD ?= sphinx-build 5 | SPHINXAPIDOC ?= sphinx-apidoc 6 | SOURCEDIR = source 7 | BUILDDIR = build 8 | MODDIR ?= ../cardano_clusterlib 9 | 10 | # Put it first so that "make" without argument is like "make help". 11 | help: 12 | @$(SPHINXBUILD) -M help "$(SOURCEDIR)" "$(BUILDDIR)" $(SPHINXOPTS) $(O) 13 | 14 | apidoc: 15 | @$(SPHINXAPIDOC) -f -H "Source Documentation" -o "$(SOURCEDIR)" "$(MODDIR)" 16 | 17 | clean: 18 | rm -rf "$(BUILDDIR)"/* 19 | 20 | .PHONY: help apidoc clean Makefile 21 | 22 | # Catch-all target: route all unknown targets to Sphinx using the new 23 | # "make mode" option. $(O) is meant as a shortcut for $(SPHINXOPTS). 24 | %: Makefile apidoc 25 | @$(SPHINXBUILD) -M $@ "$(SOURCEDIR)" "$(BUILDDIR)" $(SPHINXOPTS) $(O) 26 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | .PHONY: install 2 | install: 3 | python3 -m pip install --require-virtualenv --upgrade pip 4 | python3 -m pip install --require-virtualenv --upgrade -r requirements-dev.txt $(PIP_INSTALL_ARGS) 5 | 6 | .PHONY: .install_doc 7 | .install_doc: 8 | python3 -m pip install --require-virtualenv --upgrade -r docs/requirements.txt 9 | 10 | # run linters 11 | .PHONY: lint 12 | lint: 13 | pre-commit run -a --show-diff-on-failure --color=always 14 | 15 | # build package 16 | .PHONY: build 17 | build: 18 | python3 -m build 19 | 20 | # upload package to PyPI 21 | .PHONY: upload 22 | upload: 23 | if ! command -v twine >/dev/null 2>&1; then python3 -m pip install --require-virtualenv --upgrade twine; fi 24 | twine upload --skip-existing dist/* 25 | 26 | # release package to PyPI 27 | .PHONY: release 28 | release: build upload 29 | 30 | .PHONY: .docs_build_dir 31 | .docs_build_dir: 32 | mkdir -p docs/build 33 | 34 | # generate sphinx documentation 35 | .PHONY: doc 36 | doc: .docs_build_dir .install_doc 37 | $(MAKE) -C docs clean 38 | $(MAKE) -C docs html 39 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Local files 2 | /tmp*/ 3 | /.bin*/ 4 | /.patches/ 5 | 6 | # Byte-compiled / optimized / DLL files 7 | __pycache__/ 8 | *.py[cod] 9 | *$py.class 10 | 11 | # mypy 12 | .mypy_cache/ 13 | .dmypy.json 14 | 15 | # Exuberant Ctags tag file 16 | /tags 17 | 18 | # Env variables 19 | /.source* 20 | 21 | # Distribution / packaging 22 | .Python 23 | env/ 24 | build/ 25 | develop-eggs/ 26 | dist/ 27 | downloads/ 28 | eggs/ 29 | .eggs/ 30 | lib/ 31 | lib64/ 32 | parts/ 33 | sdist/ 34 | var/ 35 | wheels/ 36 | *.egg-info/ 37 | .installed.cfg 38 | *.egg 39 | 40 | # PyInstaller 41 | # Usually these files are written by a python script from a template 42 | # before PyInstaller builds the exe, so as to inject date/other infos into it. 43 | *.manifest 44 | *.spec 45 | 46 | # Installer logs 47 | pip-log.txt 48 | pip-delete-this-directory.txt 49 | 50 | # Unit test / coverage reports 51 | htmlcov/ 52 | .tox/ 53 | .coverage 54 | .coverage.* 55 | .cache 56 | 57 | .pytest_cache 58 | nosetests.xml 59 | coverage.xml 60 | *,cover 61 | .hypothesis/ 62 | 63 | # Translations 64 | *.mo 65 | *.pot 66 | 67 | # Sphinx documentation 68 | docs/_build/ 69 | 70 | # PyBuilder 71 | target/ 72 | 73 | # Jupyter Notebook 74 | .ipynb_checkpoints 75 | 76 | # pyenv 77 | .python-version 78 | 79 | # dotenv 80 | .env 81 | 82 | # virtualenv 83 | .venv 84 | venv/ 85 | ENV/ 86 | 87 | # Rope project settings 88 | .ropeproject 89 | 90 | # mkdocs documentation 91 | /site 92 | state-cluster 93 | -------------------------------------------------------------------------------- /.github/workflows/repo_tests.yaml: -------------------------------------------------------------------------------- 1 | name: repo_tests 2 | 3 | on: 4 | pull_request: 5 | branches: 6 | - master 7 | 8 | env: 9 | PY_COLORS: "1" 10 | 11 | jobs: 12 | lint: 13 | runs-on: ubuntu-latest 14 | steps: 15 | - name: Check out repository 16 | uses: actions/checkout@v6 17 | - name: Set up python 18 | id: setup-python 19 | uses: actions/setup-python@v6 20 | with: 21 | python-version: '3.12' 22 | - name: Install dependencies 23 | run: | 24 | python3 -m venv .venv 25 | source .venv/bin/activate 26 | make install 27 | - name: Load cached pre-commit env 28 | id: cached-pre-commit 29 | uses: actions/cache@v4 30 | with: 31 | path: ~/.cache/pre-commit 32 | key: pre-commit-${{ runner.os }}-${{ steps.setup-python.outputs.python-version }}-${{ hashFiles('.pre-commit-config.yaml') }} 33 | - name: Install pre-commit hooks 34 | run: | 35 | mkdir -p ~/.cache/pre-commit 36 | true > ~/.cache/pre-commit/pre-commit.log 37 | source .venv/bin/activate 38 | pre-commit install-hooks --color=always 39 | retval="$?" 40 | if [ "$retval" -ne 0 ]; then 41 | cat ~/.cache/pre-commit/pre-commit.log 42 | fi 43 | exit "$retval" 44 | - name: Run pre-commit linters 45 | run: | 46 | source .venv/bin/activate 47 | pre-commit run -a --show-diff-on-failure --color=always 48 | -------------------------------------------------------------------------------- /.pre-commit-config.yaml: -------------------------------------------------------------------------------- 1 | repos: 2 | - repo: https://github.com/pre-commit/pre-commit-hooks 3 | rev: v5.0.0 4 | hooks: 5 | - id: trailing-whitespace 6 | language_version: python3 7 | exclude_types: [html] 8 | - id: end-of-file-fixer 9 | language_version: python3 10 | exclude_types: [html] 11 | - id: check-yaml 12 | language_version: python3 13 | - id: debug-statements 14 | language_version: python3 15 | - repo: local 16 | hooks: 17 | - id: pyrefly 18 | name: pyrefly 19 | entry: pyrefly 20 | # pyrefly doesn't handle editable installs and adds 'cardano_clusterlib' to 'project-excludes', 21 | # so it must be added here 22 | args: [ check, --remove-unused-ignores, cardano_clusterlib ] 23 | pass_filenames: false 24 | language: system 25 | types: [python] 26 | - repo: https://github.com/charliermarsh/ruff-pre-commit 27 | rev: v0.12.1 28 | hooks: 29 | - id: ruff-check 30 | args: [ --fix ] 31 | - id: ruff-format 32 | - repo: https://github.com/shellcheck-py/shellcheck-py 33 | rev: v0.10.0.1 34 | hooks: 35 | - id: shellcheck 36 | - repo: https://github.com/igorshubovych/markdownlint-cli 37 | rev: v0.45.0 38 | hooks: 39 | - id: markdownlint 40 | - repo: local 41 | hooks: 42 | - id: mypy 43 | name: mypy 44 | entry: mypy 45 | language: system 46 | types: [python] 47 | - id: pyright 48 | name: pyright 49 | entry: pyright 50 | language: system 51 | types: [python] 52 | -------------------------------------------------------------------------------- /.github/workflows/codeql.yml: -------------------------------------------------------------------------------- 1 | name: "CodeQL" 2 | 3 | on: 4 | pull_request: 5 | branches: [ "master" ] 6 | paths: 7 | - '**.py' 8 | schedule: 9 | - cron: '17 0 * * 3' 10 | 11 | jobs: 12 | analyze: 13 | name: Analyze 14 | runs-on: ubuntu-latest 15 | permissions: 16 | actions: read 17 | contents: read 18 | security-events: write 19 | 20 | strategy: 21 | fail-fast: false 22 | matrix: 23 | language: [ 'python' ] 24 | # CodeQL supports [ 'cpp', 'csharp', 'go', 'java', 'javascript', 'python', 'ruby' ] 25 | # Use only 'java' to analyze code written in Java, Kotlin or both 26 | # Use only 'javascript' to analyze code written in JavaScript, TypeScript or both 27 | # Learn more about CodeQL language support at https://aka.ms/codeql-docs/language-support 28 | 29 | steps: 30 | - name: Checkout repository 31 | uses: actions/checkout@v6 32 | 33 | # Initializes the CodeQL tools for scanning. 34 | - name: Initialize CodeQL 35 | uses: github/codeql-action/init@v4 36 | with: 37 | languages: ${{ matrix.language }} 38 | # If you wish to specify custom queries, you can do so here or in a config file. 39 | # By default, queries listed here will override any specified in a config file. 40 | # Prefix the list here with "+" to use these queries and those in the config file. 41 | 42 | # For more details on CodeQL's query packs, refer to: https://docs.github.com/en/code-security/code-scanning/automatically-scanning-your-code-for-vulnerabilities-and-errors/configuring-code-scanning#using-queries-in-ql-packs 43 | # queries: security-extended,security-and-quality 44 | 45 | - name: Perform CodeQL Analysis 46 | uses: github/codeql-action/analyze@v4 47 | with: 48 | category: "/language:${{matrix.language}}" 49 | -------------------------------------------------------------------------------- /cardano_clusterlib/consts.py: -------------------------------------------------------------------------------- 1 | import enum 2 | import typing as tp 3 | 4 | DEFAULT_COIN: tp.Final[str] = "lovelace" 5 | MAINNET_MAGIC: tp.Final[int] = 764824073 6 | CONFIRM_BLOCKS_NUM: tp.Final[int] = 2 7 | 8 | # The SUBCOMMAND_MARK is used to mark the beginning of a subcommand. It is used to differentiate 9 | # between options and subcommands. That is needed for CLI coverage recording. 10 | # For example, the command `cardano-cli query tx-mempool --cardano-mode info` 11 | # has the following arguments: 12 | # ["query", "tx-mempool", "--cardano-mode", SUBCOMMAND_MARK, "info"] 13 | SUBCOMMAND_MARK: tp.Final[str] = "SUBCOMMAND" 14 | 15 | 16 | class CommandEras: 17 | CONWAY: tp.Final[str] = "conway" 18 | LATEST: tp.Final[str] = "latest" 19 | 20 | 21 | class Eras(enum.Enum): 22 | BYRON = 1 23 | SHELLEY = 2 24 | ALLEGRA = 3 25 | MARY = 4 26 | ALONZO = 6 27 | BABBAGE = 8 28 | CONWAY = 9 29 | DEFAULT = CONWAY 30 | LATEST = CONWAY # noqa: PIE796 31 | 32 | 33 | class MultiSigTypeArgs: 34 | ALL: tp.Final[str] = "all" 35 | ANY: tp.Final[str] = "any" 36 | AT_LEAST: tp.Final[str] = "atLeast" 37 | 38 | 39 | class MultiSlotTypeArgs: 40 | BEFORE: tp.Final[str] = "before" 41 | AFTER: tp.Final[str] = "after" 42 | 43 | 44 | class ScriptTypes: 45 | SIMPLE_V1: tp.Final[str] = "simple_v1" 46 | SIMPLE_V2: tp.Final[str] = "simple_v2" 47 | PLUTUS_V1: tp.Final[str] = "plutus_v1" 48 | PLUTUS_V2: tp.Final[str] = "plutus_v2" 49 | PLUTUS_V3: tp.Final[str] = "plutus_v3" 50 | 51 | 52 | class Votes(enum.Enum): 53 | YES = 1 54 | NO = 2 55 | ABSTAIN = 3 56 | 57 | 58 | class KeyType(enum.Enum): 59 | PAYMENT = "payment" 60 | STAKE = "stake" 61 | DREP = "drep" 62 | CC_COLD = "cc-cold" 63 | CC_HOT = "cc-hot" 64 | 65 | 66 | class OutputFormat(enum.Enum): 67 | TEXT_ENVELOPE = "text-envelope" 68 | BECH32 = "bech32" 69 | -------------------------------------------------------------------------------- /cardano_clusterlib/helpers.py: -------------------------------------------------------------------------------- 1 | import itertools 2 | import pathlib as pl 3 | import random 4 | import string 5 | 6 | from cardano_clusterlib import exceptions 7 | from cardano_clusterlib import types as itp 8 | 9 | 10 | def get_rand_str(length: int = 8) -> str: 11 | """Return random ASCII lowercase string.""" 12 | if length < 1: 13 | return "" 14 | return "".join(random.choice(string.ascii_lowercase) for i in range(length)) 15 | 16 | 17 | def read_from_file(file: itp.FileType) -> str: 18 | """Read file content.""" 19 | with open(pl.Path(file).expanduser(), encoding="utf-8") as in_file: 20 | return in_file.read() 21 | 22 | 23 | def read_address_from_file(addr_file: itp.FileType) -> str: 24 | """Read address stored in file.""" 25 | return read_from_file(file=addr_file).strip() 26 | 27 | 28 | def _prepend_flag(flag: str, contents: itp.UnpackableSequence) -> list[str]: 29 | """Prepend flag to every item of the sequence. 30 | 31 | Args: 32 | flag: A flag to prepend to every item of the `contents`. 33 | contents: A list (iterable) of content to be prepended. 34 | 35 | Returns: 36 | list[str]: A list of flag followed by content, see below. 37 | 38 | >>> ClusterLib._prepend_flag(None, "--foo", [1, 2, 3]) 39 | ['--foo', '1', '--foo', '2', '--foo', '3'] 40 | """ 41 | return list(itertools.chain.from_iterable([flag, str(x)] for x in contents)) 42 | 43 | 44 | def _check_outfiles(*out_files: itp.FileType) -> None: 45 | """Check that the expected output files were created. 46 | 47 | Args: 48 | *out_files: Variable length list of expected output files. 49 | """ 50 | for out_file in out_files: 51 | out_file_p = pl.Path(out_file).expanduser() 52 | if not out_file_p.exists(): 53 | msg = f"The expected file `{out_file}` doesn't exist." 54 | raise exceptions.CLIError(msg) 55 | 56 | 57 | def _maybe_path(file: itp.FileType | None) -> pl.Path | None: 58 | """Return `Path` if `file` is thruthy.""" 59 | return pl.Path(file) if file else None 60 | -------------------------------------------------------------------------------- /pyproject.toml: -------------------------------------------------------------------------------- 1 | [build-system] 2 | requires = ["setuptools>=77.0.3", "setuptools_scm[toml]"] 3 | build-backend = "setuptools.build_meta" 4 | 5 | [project] 6 | name = "cardano-clusterlib" 7 | authors = [ 8 | {name = "Martin Kourim", email = "martin.kourim@iohk.io"}, 9 | ] 10 | description = "Python wrapper for cardano-cli for working with cardano cluster" 11 | readme = "README.md" 12 | requires-python = ">=3.9" 13 | keywords = ["cardano", "cardano-node", "cardano-cli", "cardano-node-tests"] 14 | license = "Apache-2.0" 15 | classifiers = [ 16 | "Topic :: Software Development :: Libraries :: Python Modules", 17 | "Programming Language :: Python :: 3", 18 | "Programming Language :: Python :: 3.9", 19 | "Programming Language :: Python :: 3.10", 20 | "Programming Language :: Python :: 3.11", 21 | "Programming Language :: Python :: 3.12", 22 | "Programming Language :: Python :: 3.13", 23 | "Intended Audience :: Developers", 24 | ] 25 | dynamic = ["version"] 26 | dependencies = [ 27 | "packaging", 28 | ] 29 | 30 | [project.urls] 31 | homepage = "https://github.com/input-output-hk/cardano-clusterlib-py" 32 | documentation = "https://cardano-clusterlib-py.readthedocs.io/" 33 | repository = "https://github.com/input-output-hk/cardano-clusterlib-py" 34 | 35 | [tool.setuptools.packages.find] 36 | where = ["."] 37 | include = ["cardano_clusterlib*"] 38 | 39 | [tool.setuptools_scm] 40 | 41 | [tool.ruff] 42 | line-length = 100 43 | 44 | [tool.ruff.lint] 45 | select = ["ANN", "ARG", "B", "C4", "C90", "D", "DTZ", "E", "EM", "F", "FURB", "I001", "ISC", "N", "PERF", "PIE", "PL", "PLE", "PLR", "PLW", "PT", "PTH", "Q", "RET", "RSE", "RUF", "SIM", "TRY", "UP", "W", "YTT"] 46 | ignore = ["D10", "D203", "D212", "D213", "D214", "D215", "D404", "D405", "D406", "D407", "D408", "D409", "D410", "D411", "D413", "ISC001", "PLR0912", "PLR0913", "PLR0915", "PT001", "PT007", "PT012", "PT018", "PT023", "PTH123", "RET504", "TRY002", "TRY301", "UP006", "UP007", "UP035"] 47 | 48 | [tool.ruff.lint.per-file-ignores] 49 | "docs/**.py" = ["ANN"] 50 | 51 | [tool.ruff.lint.isort] 52 | force-single-line = true 53 | 54 | [tool.mypy] 55 | show_error_context = true 56 | verbosity = 0 57 | ignore_missing_imports = true 58 | follow_imports = "normal" 59 | no_implicit_optional = true 60 | allow_untyped_globals = false 61 | warn_unused_configs = true 62 | warn_return_any = true 63 | 64 | [tool.pyrefly] 65 | project_includes = ["cardano_clusterlib"] 66 | ignore_errors_in_generated_code = true 67 | use_untyped_imports = true 68 | ignore_missing_source = true 69 | 70 | [[tool.pyrefly.sub_config]] 71 | matches = "cardano_clusterlib/clusterlib_klass.py" 72 | 73 | # Ignore the bad-argument-type errors for Self@ClusterLib, that are reported only for LSP 74 | [tool.pyrefly.sub_config.errors] 75 | bad-argument-type = false 76 | -------------------------------------------------------------------------------- /CODE-OF-CONDUCT.md: -------------------------------------------------------------------------------- 1 | # Contributor Covenant Code of Conduct 2 | 3 | ## Our Pledge 4 | 5 | In the interest of fostering an open and welcoming environment, we as 6 | contributors and maintainers pledge to making participation in our project and 7 | our community a harassment-free experience for everyone, regardless of age, body 8 | size, disability, ethnicity, gender identity and expression, level of experience, 9 | education, socio-economic status, nationality, personal appearance, race, 10 | religion, or sexual identity and orientation. 11 | 12 | ## Our Standards 13 | 14 | Examples of behavior that contributes to creating a positive environment 15 | include: 16 | 17 | * Using welcoming and inclusive language 18 | * Being respectful of differing viewpoints and experiences 19 | * Gracefully accepting constructive criticism 20 | * Focusing on what is best for the community 21 | * Showing empathy towards other community members 22 | 23 | Examples of unacceptable behavior by participants include: 24 | 25 | * The use of sexualized language or imagery and unwelcome sexual attention or 26 | advances 27 | * Trolling, insulting/derogatory comments, and personal or political attacks 28 | * Public or private harassment 29 | * Publishing others' private information, such as a physical or electronic 30 | address, without explicit permission 31 | * Other conduct which could reasonably be considered inappropriate in a 32 | professional setting 33 | 34 | ## Our Responsibilities 35 | 36 | Project maintainers are responsible for clarifying the standards of acceptable 37 | behavior and are expected to take appropriate and fair corrective action in 38 | response to any instances of unacceptable behavior. 39 | 40 | Project maintainers have the right and responsibility to remove, edit, or 41 | reject comments, commits, code, wiki edits, issues, and other contributions 42 | that are not aligned to this Code of Conduct, or to ban temporarily or 43 | permanently any contributor for other behaviors that they deem inappropriate, 44 | threatening, offensive, or harmful. 45 | 46 | ## Scope 47 | 48 | This Code of Conduct applies both within project spaces and in public spaces 49 | when an individual is representing the project or its community. Examples of 50 | representing a project or community include using an official project e-mail 51 | address, posting via an official social media account, or acting as an appointed 52 | representative at an online or offline event. Representation of a project may be 53 | further defined and clarified by project maintainers. 54 | 55 | ## Enforcement 56 | 57 | Instances of abusive, harassing, or otherwise unacceptable behavior may be 58 | reported by contacting the project team at [code-of-conduct@iohk.io](mailto:code-of-conduct@iohk.io). All 59 | complaints will be reviewed and investigated and will result in a response that 60 | is deemed necessary and appropriate to the circumstances. The project team is 61 | obligated to maintain confidentiality with regard to the reporter of an incident. 62 | Further details of specific enforcement policies may be posted separately. 63 | 64 | Project maintainers who do not follow or enforce the Code of Conduct in good 65 | faith may face temporary or permanent repercussions as determined by other 66 | members of the project's leadership. 67 | 68 | ## Attribution 69 | 70 | This Code of Conduct is adapted from the [Contributor Covenant][homepage], version 1.4, 71 | available at 72 | 73 | [homepage]: https://www.contributor-covenant.org 74 | -------------------------------------------------------------------------------- /cardano_clusterlib/clusterlib.py: -------------------------------------------------------------------------------- 1 | """Legacy top-level module. 2 | 3 | Import everything that used to be available here for backwards compatibility. 4 | """ 5 | 6 | # flake8: noqa 7 | from cardano_clusterlib.clusterlib_klass import ClusterLib 8 | from cardano_clusterlib.consts import CommandEras 9 | from cardano_clusterlib.consts import DEFAULT_COIN 10 | from cardano_clusterlib.consts import Eras 11 | from cardano_clusterlib.consts import KeyType 12 | from cardano_clusterlib.consts import MAINNET_MAGIC 13 | from cardano_clusterlib.consts import MultiSigTypeArgs 14 | from cardano_clusterlib.consts import MultiSlotTypeArgs 15 | from cardano_clusterlib.consts import OutputFormat 16 | from cardano_clusterlib.consts import ScriptTypes 17 | from cardano_clusterlib.consts import Votes 18 | from cardano_clusterlib.exceptions import CLIError 19 | from cardano_clusterlib.helpers import get_rand_str 20 | from cardano_clusterlib.helpers import read_address_from_file 21 | from cardano_clusterlib.structs import ActionConstitution 22 | from cardano_clusterlib.structs import ActionHardfork 23 | from cardano_clusterlib.structs import ActionInfo 24 | from cardano_clusterlib.structs import ActionNoConfidence 25 | from cardano_clusterlib.structs import ActionPParamsUpdate 26 | from cardano_clusterlib.structs import ActionTreasuryWithdrawal 27 | from cardano_clusterlib.structs import ActionUpdateCommittee 28 | from cardano_clusterlib.structs import AddressInfo 29 | from cardano_clusterlib.structs import AddressRecord 30 | from cardano_clusterlib.structs import CCMember 31 | from cardano_clusterlib.structs import CLIOut 32 | from cardano_clusterlib.structs import ColdKeyPair 33 | from cardano_clusterlib.structs import ComplexCert 34 | from cardano_clusterlib.structs import ComplexProposal 35 | from cardano_clusterlib.structs import DataForBuild 36 | from cardano_clusterlib.structs import GenesisKeys 37 | from cardano_clusterlib.structs import KeyPair 38 | from cardano_clusterlib.structs import LeadershipSchedule 39 | from cardano_clusterlib.structs import Mint 40 | from cardano_clusterlib.structs import OptionalMint 41 | from cardano_clusterlib.structs import OptionalScriptCerts 42 | from cardano_clusterlib.structs import OptionalScriptProposals 43 | from cardano_clusterlib.structs import OptionalScriptTxIn 44 | from cardano_clusterlib.structs import OptionalScriptVotes 45 | from cardano_clusterlib.structs import OptionalScriptWithdrawals 46 | from cardano_clusterlib.structs import OptionalTxOuts 47 | from cardano_clusterlib.structs import OptionalUTXOData 48 | from cardano_clusterlib.structs import PoolCreationOutput 49 | from cardano_clusterlib.structs import PoolData 50 | from cardano_clusterlib.structs import PoolParamsTop 51 | from cardano_clusterlib.structs import PoolUser 52 | from cardano_clusterlib.structs import ScriptTxIn 53 | from cardano_clusterlib.structs import ScriptVote 54 | from cardano_clusterlib.structs import ScriptWithdrawal 55 | from cardano_clusterlib.structs import StakeAddrInfo 56 | from cardano_clusterlib.structs import TxFiles 57 | from cardano_clusterlib.structs import TxOut 58 | from cardano_clusterlib.structs import TxRawOutput 59 | from cardano_clusterlib.structs import UTXOData 60 | from cardano_clusterlib.structs import Value 61 | from cardano_clusterlib.structs import VoteCC 62 | from cardano_clusterlib.structs import VoteDrep 63 | from cardano_clusterlib.structs import VoteSPO 64 | from cardano_clusterlib.txtools import calculate_utxos_balance 65 | from cardano_clusterlib.txtools import collect_data_for_build 66 | from cardano_clusterlib.txtools import filter_utxo_with_highest_amount 67 | from cardano_clusterlib.txtools import filter_utxos 68 | from cardano_clusterlib.types import FileType 69 | -------------------------------------------------------------------------------- /cardano_clusterlib/gov_group.py: -------------------------------------------------------------------------------- 1 | """Group of subgroups for governance in Conway+ eras.""" 2 | 3 | import logging 4 | 5 | from cardano_clusterlib import gov_action_group 6 | from cardano_clusterlib import gov_committee_group 7 | from cardano_clusterlib import gov_drep_group 8 | from cardano_clusterlib import gov_vote_group 9 | from cardano_clusterlib import types as itp 10 | 11 | LOGGER = logging.getLogger(__name__) 12 | 13 | 14 | class GovernanceGroup: 15 | def __init__(self, clusterlib_obj: "itp.ClusterLib") -> None: 16 | self._clusterlib_obj = clusterlib_obj 17 | 18 | # Groups of commands 19 | self._action_group: gov_action_group.GovActionGroup | None = None 20 | self._committee_group: gov_committee_group.GovCommitteeGroup | None = None 21 | self._drep_group: gov_drep_group.GovDrepGroup | None = None 22 | self._vote_group: gov_vote_group.GovVoteGroup | None = None 23 | 24 | @property 25 | def action(self) -> gov_action_group.GovActionGroup: 26 | """Action group.""" 27 | if not self._action_group: 28 | self._action_group = gov_action_group.GovActionGroup( 29 | clusterlib_obj=self._clusterlib_obj 30 | ) 31 | return self._action_group 32 | 33 | @property 34 | def committee(self) -> gov_committee_group.GovCommitteeGroup: 35 | """Committee group.""" 36 | if not self._committee_group: 37 | self._committee_group = gov_committee_group.GovCommitteeGroup( 38 | clusterlib_obj=self._clusterlib_obj 39 | ) 40 | return self._committee_group 41 | 42 | @property 43 | def drep(self) -> gov_drep_group.GovDrepGroup: 44 | """Drep group.""" 45 | if not self._drep_group: 46 | self._drep_group = gov_drep_group.GovDrepGroup(clusterlib_obj=self._clusterlib_obj) 47 | return self._drep_group 48 | 49 | @property 50 | def vote(self) -> gov_vote_group.GovVoteGroup: 51 | """Vote group.""" 52 | if not self._vote_group: 53 | self._vote_group = gov_vote_group.GovVoteGroup(clusterlib_obj=self._clusterlib_obj) 54 | return self._vote_group 55 | 56 | def get_anchor_data_hash( 57 | self, 58 | text: str = "", 59 | file_binary: itp.FileType | None = None, 60 | file_text: itp.FileType | None = None, 61 | ) -> str: 62 | """Compute the hash of some anchor data. 63 | 64 | Args: 65 | text: A text to hash as UTF-8. 66 | file_binary: A path to the binary file to hash. 67 | file_text: A path to the text file to hash. 68 | 69 | Returns: 70 | str: A hash string. 71 | """ 72 | if text: 73 | content_args = ["--text", text] 74 | elif file_binary: 75 | content_args = ["--file-binary", str(file_binary)] 76 | elif file_text: 77 | content_args = ["--file-text", str(file_text)] 78 | else: 79 | msg = "Either `text`, `file_binary` or `file_text` is needed." 80 | raise ValueError(msg) 81 | 82 | out_hash = ( 83 | self._clusterlib_obj.cli( 84 | ["cardano-cli", "hash", "anchor-data", *content_args], add_default_args=False 85 | ) 86 | .stdout.rstrip() 87 | .decode("ascii") 88 | ) 89 | 90 | return out_hash 91 | 92 | def get_script_hash( 93 | self, 94 | script_file: itp.FileType | None = None, 95 | ) -> str: 96 | """Compute the hash of a script. 97 | 98 | Args: 99 | script_file: A path to the text file to hash. 100 | 101 | Returns: 102 | str: A hash string. 103 | """ 104 | # TODO: make it a top-level function to reflect `cardano-cli hash` 105 | out_hash = ( 106 | self._clusterlib_obj.cli( 107 | ["cardano-cli", "hash", "script", "--script-file", str(script_file)], 108 | add_default_args=False, 109 | ) 110 | .stdout.rstrip() 111 | .decode("ascii") 112 | ) 113 | 114 | return out_hash 115 | 116 | def __repr__(self) -> str: 117 | return f"<{self.__class__.__name__}: clusterlib_obj={id(self._clusterlib_obj)}>" 118 | -------------------------------------------------------------------------------- /docs/source/conf.py: -------------------------------------------------------------------------------- 1 | # pylint: skip-file 2 | # type: ignore 3 | # Configuration file for the Sphinx documentation builder. 4 | # 5 | # This file only contains a selection of the most common options. For a full 6 | # list see the documentation: 7 | # https://www.sphinx-doc.org/en/master/usage/configuration.html 8 | import inspect 9 | import os 10 | import subprocess 11 | import sys 12 | 13 | import cardano_clusterlib 14 | 15 | # -- Path setup -------------------------------------------------------------- 16 | 17 | # If extensions (or modules to document with autodoc) are in another directory, 18 | # add these directories to sys.path here. If the directory is relative to the 19 | # documentation root, use os.path.abspath to make it absolute, like shown here. 20 | sys.path.insert(0, os.path.abspath("..")) # noqa: PTH100 21 | 22 | # -- Project information ----------------------------------------------------- 23 | 24 | project = "cardano-clusterlib" 25 | author = "Cardano Test Engineering Team" 26 | copyright = author 27 | 28 | 29 | # -- General configuration --------------------------------------------------- 30 | 31 | # Add any Sphinx extension module names here, as strings. They can be 32 | # extensions coming with Sphinx (named 'sphinx.ext.*') or your custom 33 | # ones. 34 | extensions = [ 35 | "sphinx.ext.autodoc", 36 | "sphinx.ext.autosummary", 37 | # "sphinx.ext.doctest", 38 | # "sphinx.ext.coverage", 39 | # "sphinx.ext.githubpages", 40 | "sphinx.ext.linkcode", 41 | "sphinx.ext.napoleon", 42 | "m2r2", 43 | ] 44 | 45 | # Add any paths that contain templates here, relative to this directory. 46 | templates_path = ["_templates"] 47 | 48 | # List of patterns, relative to source directory, that match files and 49 | # directories to ignore when looking for source files. 50 | # This pattern also affects html_static_path and html_extra_path. 51 | exclude_patterns = [] 52 | 53 | # source_suffix = '.rst' 54 | source_suffix = [".rst", ".md"] 55 | 56 | 57 | # -- Options for HTML output ------------------------------------------------- 58 | 59 | # The theme to use for HTML and HTML Help pages. See the documentation for 60 | # a list of builtin themes. 61 | # html_theme = 'alabaster' 62 | html_theme = "sphinx_rtd_theme" 63 | 64 | # Add any paths that contain custom static files (such as style sheets) here, 65 | # relative to this directory. They are copied after the builtin static files, 66 | # so a file named "default.css" will overwrite the builtin "default.css". 67 | html_static_path = ["_static"] 68 | 69 | # Resolve function for the linkcode extension. 70 | 71 | # store current git revision 72 | if os.environ.get("CARDANO_CLUSTERLIB_GIT_REV"): 73 | cardano_clusterlib._git_rev = os.environ.get("CARDANO_CLUSTERLIB_GIT_REV") 74 | else: 75 | with subprocess.Popen( 76 | ["git", "rev-parse", "HEAD"], stdout=subprocess.PIPE, stderr=subprocess.PIPE 77 | ) as p: 78 | stdout, __ = p.communicate() 79 | cardano_clusterlib._git_rev = stdout.decode().strip() 80 | if not cardano_clusterlib._git_rev: 81 | cardano_clusterlib._git_rev = "master" 82 | 83 | 84 | def linkcode_resolve(domain, info): 85 | def find_source(): 86 | # try to find the file and line number, based on code from numpy: 87 | # https://github.com/numpy/numpy/blob/master/doc/source/conf.py#L286 88 | obj = sys.modules.get(info["module"]) 89 | if obj is None: 90 | return None 91 | 92 | for part in info["fullname"].split("."): 93 | try: 94 | obj = getattr(obj, part) 95 | except Exception: # noqa: PERF203 96 | return None 97 | 98 | # strip decorators, which would resolve to the source of the decorator 99 | # possibly an upstream bug in getsourcefile, bpo-1764286 100 | obj = inspect.unwrap(obj) 101 | 102 | fn = inspect.getsourcefile(obj) 103 | fn = os.path.relpath(fn, start=os.path.dirname(cardano_clusterlib.__file__)) # noqa: PTH120 104 | source, lineno = inspect.getsourcelines(obj) 105 | return fn, lineno, lineno + len(source) - 1 106 | 107 | if domain != "py" or not info["module"]: 108 | return None 109 | 110 | try: 111 | fn, l_start, l_end = find_source() 112 | filename = f"cardano_clusterlib/{fn}#L{l_start}-L{l_end}" 113 | # print(filename) 114 | except Exception: 115 | filename = info["module"].replace(".", "/") + ".py" 116 | # print(f"EXC: {filename}") 117 | 118 | return ( 119 | "https://github.com/input-output-hk/cardano-clusterlib-py/blob/" 120 | f"{cardano_clusterlib._git_rev}/{filename}" 121 | ) 122 | -------------------------------------------------------------------------------- /cardano_clusterlib/genesis_group.py: -------------------------------------------------------------------------------- 1 | """Group of methods related to genesis block.""" 2 | 3 | import logging 4 | import pathlib as pl 5 | 6 | from cardano_clusterlib import clusterlib_helpers 7 | from cardano_clusterlib import exceptions 8 | from cardano_clusterlib import helpers 9 | from cardano_clusterlib import structs 10 | from cardano_clusterlib import types as itp 11 | 12 | LOGGER = logging.getLogger(__name__) 13 | 14 | 15 | class GenesisGroup: 16 | def __init__(self, clusterlib_obj: "itp.ClusterLib") -> None: 17 | self._clusterlib_obj = clusterlib_obj 18 | 19 | self._genesis_keys: structs.GenesisKeys | None = None 20 | self._genesis_utxo_addr: str = "" 21 | self._cli_args = ("cardano-cli", "latest", "genesis") 22 | 23 | @property 24 | def genesis_keys(self) -> structs.GenesisKeys: 25 | """Return data container with genesis-related keys.""" 26 | if self._genesis_keys: 27 | return self._genesis_keys 28 | 29 | genesis_utxo_vkey = self._clusterlib_obj.state_dir / "shelley" / "genesis-utxo.vkey" 30 | genesis_utxo_skey = self._clusterlib_obj.state_dir / "shelley" / "genesis-utxo.skey" 31 | genesis_vkeys = list( 32 | self._clusterlib_obj.state_dir.glob("shelley/genesis-keys/genesis?.vkey") 33 | ) 34 | delegate_skeys = list( 35 | self._clusterlib_obj.state_dir.glob("shelley/delegate-keys/delegate?.skey") 36 | ) 37 | 38 | if not genesis_vkeys: 39 | msg = "The genesis verification keys don't exist." 40 | raise exceptions.CLIError(msg) 41 | if not delegate_skeys: 42 | msg = "The delegation signing keys don't exist." 43 | raise exceptions.CLIError(msg) 44 | 45 | for file_name in ( 46 | genesis_utxo_vkey, 47 | genesis_utxo_skey, 48 | ): 49 | if not file_name.exists(): 50 | msg = f"The file `{file_name}` doesn't exist." 51 | raise exceptions.CLIError(msg) 52 | 53 | genesis_keys = structs.GenesisKeys( 54 | genesis_utxo_vkey=genesis_utxo_skey, 55 | genesis_utxo_skey=genesis_utxo_skey, 56 | genesis_vkeys=genesis_vkeys, 57 | delegate_skeys=delegate_skeys, 58 | ) 59 | 60 | self._genesis_keys = genesis_keys 61 | 62 | return genesis_keys 63 | 64 | @property 65 | def genesis_utxo_addr(self) -> str: 66 | """Produce a genesis UTxO address.""" 67 | if self._genesis_utxo_addr: 68 | return self._genesis_utxo_addr 69 | 70 | self._genesis_utxo_addr = self.gen_genesis_addr( 71 | addr_name=f"genesis-{self._clusterlib_obj._rand_str}", 72 | vkey_file=self.genesis_keys.genesis_utxo_vkey, 73 | destination_dir=self._clusterlib_obj.state_dir, 74 | ) 75 | 76 | return self._genesis_utxo_addr 77 | 78 | def gen_genesis_addr( 79 | self, addr_name: str, vkey_file: itp.FileType, destination_dir: itp.FileType = "." 80 | ) -> str: 81 | """Generate the address for an initial UTxO based on the verification key. 82 | 83 | Args: 84 | addr_name: A name of genesis address. 85 | vkey_file: A path to corresponding vkey file. 86 | destination_dir: A path to directory for storing artifacts (optional). 87 | 88 | Returns: 89 | str: A generated genesis address. 90 | """ 91 | destination_dir = pl.Path(destination_dir).expanduser() 92 | out_file = destination_dir / f"{addr_name}_genesis.addr" 93 | clusterlib_helpers._check_files_exist(out_file, clusterlib_obj=self._clusterlib_obj) 94 | 95 | self._clusterlib_obj.cli( 96 | [ 97 | *self._cli_args, 98 | "initial-addr", 99 | *self._clusterlib_obj.magic_args, 100 | "--verification-key-file", 101 | str(vkey_file), 102 | "--out-file", 103 | str(out_file), 104 | ], 105 | add_default_args=False, 106 | ) 107 | 108 | helpers._check_outfiles(out_file) 109 | return helpers.read_address_from_file(out_file) 110 | 111 | def get_genesis_vkey_hash(self, vkey_file: itp.FileType) -> str: 112 | """Return the hash of a genesis public key. 113 | 114 | Args: 115 | vkey_file: A path to corresponding vkey file. 116 | 117 | Returns: 118 | str: A generated key-hash. 119 | """ 120 | cli_out = self._clusterlib_obj.cli( 121 | [ 122 | *self._cli_args, 123 | "key-hash", 124 | "--verification-key-file", 125 | str(vkey_file), 126 | ], 127 | add_default_args=False, 128 | ) 129 | 130 | return cli_out.stdout.rstrip().decode("ascii") 131 | 132 | def __repr__(self) -> str: 133 | return f"<{self.__class__.__name__}: clusterlib_obj={id(self._clusterlib_obj)}>" 134 | -------------------------------------------------------------------------------- /docs/source/cardano_clusterlib.rst: -------------------------------------------------------------------------------- 1 | cardano\_clusterlib package 2 | =========================== 3 | 4 | Submodules 5 | ---------- 6 | 7 | cardano\_clusterlib.address\_group module 8 | ----------------------------------------- 9 | 10 | .. automodule:: cardano_clusterlib.address_group 11 | :members: 12 | :undoc-members: 13 | :show-inheritance: 14 | 15 | cardano\_clusterlib.clusterlib module 16 | ------------------------------------- 17 | 18 | .. automodule:: cardano_clusterlib.clusterlib 19 | :members: 20 | :undoc-members: 21 | :show-inheritance: 22 | 23 | cardano\_clusterlib.clusterlib\_helpers module 24 | ---------------------------------------------- 25 | 26 | .. automodule:: cardano_clusterlib.clusterlib_helpers 27 | :members: 28 | :undoc-members: 29 | :show-inheritance: 30 | 31 | cardano\_clusterlib.clusterlib\_klass module 32 | -------------------------------------------- 33 | 34 | .. automodule:: cardano_clusterlib.clusterlib_klass 35 | :members: 36 | :undoc-members: 37 | :show-inheritance: 38 | 39 | cardano\_clusterlib.consts module 40 | --------------------------------- 41 | 42 | .. automodule:: cardano_clusterlib.consts 43 | :members: 44 | :undoc-members: 45 | :show-inheritance: 46 | 47 | cardano\_clusterlib.conway\_gov\_action\_group module 48 | ----------------------------------------------------- 49 | 50 | .. automodule:: cardano_clusterlib.conway_gov_action_group 51 | :members: 52 | :undoc-members: 53 | :show-inheritance: 54 | 55 | cardano\_clusterlib.conway\_gov\_committee\_group module 56 | -------------------------------------------------------- 57 | 58 | .. automodule:: cardano_clusterlib.conway_gov_committee_group 59 | :members: 60 | :undoc-members: 61 | :show-inheritance: 62 | 63 | cardano\_clusterlib.conway\_gov\_drep\_group module 64 | --------------------------------------------------- 65 | 66 | .. automodule:: cardano_clusterlib.conway_gov_drep_group 67 | :members: 68 | :undoc-members: 69 | :show-inheritance: 70 | 71 | cardano\_clusterlib.conway\_gov\_group module 72 | --------------------------------------------- 73 | 74 | .. automodule:: cardano_clusterlib.conway_gov_group 75 | :members: 76 | :undoc-members: 77 | :show-inheritance: 78 | 79 | cardano\_clusterlib.conway\_gov\_query\_group module 80 | ---------------------------------------------------- 81 | 82 | .. automodule:: cardano_clusterlib.conway_gov_query_group 83 | :members: 84 | :undoc-members: 85 | :show-inheritance: 86 | 87 | cardano\_clusterlib.conway\_gov\_vote\_group module 88 | --------------------------------------------------- 89 | 90 | .. automodule:: cardano_clusterlib.conway_gov_vote_group 91 | :members: 92 | :undoc-members: 93 | :show-inheritance: 94 | 95 | cardano\_clusterlib.coverage module 96 | ----------------------------------- 97 | 98 | .. automodule:: cardano_clusterlib.coverage 99 | :members: 100 | :undoc-members: 101 | :show-inheritance: 102 | 103 | cardano\_clusterlib.exceptions module 104 | ------------------------------------- 105 | 106 | .. automodule:: cardano_clusterlib.exceptions 107 | :members: 108 | :undoc-members: 109 | :show-inheritance: 110 | 111 | cardano\_clusterlib.genesis\_group module 112 | ----------------------------------------- 113 | 114 | .. automodule:: cardano_clusterlib.genesis_group 115 | :members: 116 | :undoc-members: 117 | :show-inheritance: 118 | 119 | cardano\_clusterlib.governance\_group module 120 | -------------------------------------------- 121 | 122 | .. automodule:: cardano_clusterlib.governance_group 123 | :members: 124 | :undoc-members: 125 | :show-inheritance: 126 | 127 | cardano\_clusterlib.helpers module 128 | ---------------------------------- 129 | 130 | .. automodule:: cardano_clusterlib.helpers 131 | :members: 132 | :undoc-members: 133 | :show-inheritance: 134 | 135 | cardano\_clusterlib.key\_group module 136 | ------------------------------------- 137 | 138 | .. automodule:: cardano_clusterlib.key_group 139 | :members: 140 | :undoc-members: 141 | :show-inheritance: 142 | 143 | cardano\_clusterlib.node\_group module 144 | -------------------------------------- 145 | 146 | .. automodule:: cardano_clusterlib.node_group 147 | :members: 148 | :undoc-members: 149 | :show-inheritance: 150 | 151 | cardano\_clusterlib.query\_group module 152 | --------------------------------------- 153 | 154 | .. automodule:: cardano_clusterlib.query_group 155 | :members: 156 | :undoc-members: 157 | :show-inheritance: 158 | 159 | cardano\_clusterlib.stake\_address\_group module 160 | ------------------------------------------------ 161 | 162 | .. automodule:: cardano_clusterlib.stake_address_group 163 | :members: 164 | :undoc-members: 165 | :show-inheritance: 166 | 167 | cardano\_clusterlib.stake\_pool\_group module 168 | --------------------------------------------- 169 | 170 | .. automodule:: cardano_clusterlib.stake_pool_group 171 | :members: 172 | :undoc-members: 173 | :show-inheritance: 174 | 175 | cardano\_clusterlib.structs module 176 | ---------------------------------- 177 | 178 | .. automodule:: cardano_clusterlib.structs 179 | :members: 180 | :undoc-members: 181 | :show-inheritance: 182 | 183 | cardano\_clusterlib.transaction\_group module 184 | --------------------------------------------- 185 | 186 | .. automodule:: cardano_clusterlib.transaction_group 187 | :members: 188 | :undoc-members: 189 | :show-inheritance: 190 | 191 | cardano\_clusterlib.txtools module 192 | ---------------------------------- 193 | 194 | .. automodule:: cardano_clusterlib.txtools 195 | :members: 196 | :undoc-members: 197 | :show-inheritance: 198 | 199 | cardano\_clusterlib.types module 200 | -------------------------------- 201 | 202 | .. automodule:: cardano_clusterlib.types 203 | :members: 204 | :undoc-members: 205 | :show-inheritance: 206 | 207 | Module contents 208 | --------------- 209 | 210 | .. automodule:: cardano_clusterlib 211 | :members: 212 | :undoc-members: 213 | :show-inheritance: 214 | -------------------------------------------------------------------------------- /cardano_clusterlib/node_group.py: -------------------------------------------------------------------------------- 1 | """Group of methods for node operation.""" 2 | 3 | import logging 4 | import pathlib as pl 5 | 6 | from cardano_clusterlib import clusterlib_helpers 7 | from cardano_clusterlib import helpers 8 | from cardano_clusterlib import structs 9 | from cardano_clusterlib import types as itp 10 | 11 | LOGGER = logging.getLogger(__name__) 12 | 13 | 14 | class NodeGroup: 15 | def __init__(self, clusterlib_obj: "itp.ClusterLib") -> None: 16 | self._clusterlib_obj = clusterlib_obj 17 | 18 | def gen_kes_key_pair( 19 | self, node_name: str, destination_dir: itp.FileType = "." 20 | ) -> structs.KeyPair: 21 | """Generate a key pair for a node KES operational key. 22 | 23 | Args: 24 | node_name: A name of the node the key pair is generated for. 25 | destination_dir: A path to directory for storing artifacts (optional). 26 | 27 | Returns: 28 | structs.KeyPair: A data container containing the key pair. 29 | """ 30 | destination_dir = pl.Path(destination_dir).expanduser() 31 | vkey = destination_dir / f"{node_name}_kes.vkey" 32 | skey = destination_dir / f"{node_name}_kes.skey" 33 | clusterlib_helpers._check_files_exist(vkey, skey, clusterlib_obj=self._clusterlib_obj) 34 | 35 | self._clusterlib_obj.cli( 36 | [ 37 | "node", 38 | "key-gen-KES", 39 | "--verification-key-file", 40 | str(vkey), 41 | "--signing-key-file", 42 | str(skey), 43 | ] 44 | ) 45 | 46 | helpers._check_outfiles(vkey, skey) 47 | return structs.KeyPair(vkey, skey) 48 | 49 | def gen_vrf_key_pair( 50 | self, node_name: str, destination_dir: itp.FileType = "." 51 | ) -> structs.KeyPair: 52 | """Generate a key pair for a node VRF operational key. 53 | 54 | Args: 55 | node_name: A name of the node the key pair is generated for. 56 | destination_dir: A path to directory for storing artifacts (optional). 57 | 58 | Returns: 59 | structs.KeyPair: A data container containing the key pair. 60 | """ 61 | destination_dir = pl.Path(destination_dir).expanduser() 62 | vkey = destination_dir / f"{node_name}_vrf.vkey" 63 | skey = destination_dir / f"{node_name}_vrf.skey" 64 | clusterlib_helpers._check_files_exist(vkey, skey, clusterlib_obj=self._clusterlib_obj) 65 | 66 | self._clusterlib_obj.cli( 67 | [ 68 | "node", 69 | "key-gen-VRF", 70 | "--verification-key-file", 71 | str(vkey), 72 | "--signing-key-file", 73 | str(skey), 74 | ] 75 | ) 76 | 77 | helpers._check_outfiles(vkey, skey) 78 | return structs.KeyPair(vkey, skey) 79 | 80 | def gen_cold_key_pair_and_counter( 81 | self, node_name: str, destination_dir: itp.FileType = "." 82 | ) -> structs.ColdKeyPair: 83 | """Generate a key pair for operator's offline key and a new certificate issue counter. 84 | 85 | Args: 86 | node_name: A name of the node the key pair and the counter is generated for. 87 | destination_dir: A path to directory for storing artifacts (optional). 88 | 89 | Returns: 90 | structs.ColdKeyPair: A data container containing the key pair and the counter. 91 | """ 92 | destination_dir = pl.Path(destination_dir).expanduser() 93 | vkey = destination_dir / f"{node_name}_cold.vkey" 94 | skey = destination_dir / f"{node_name}_cold.skey" 95 | counter = destination_dir / f"{node_name}_cold.counter" 96 | clusterlib_helpers._check_files_exist( 97 | vkey, skey, counter, clusterlib_obj=self._clusterlib_obj 98 | ) 99 | 100 | self._clusterlib_obj.cli( 101 | [ 102 | "node", 103 | "key-gen", 104 | "--cold-verification-key-file", 105 | str(vkey), 106 | "--cold-signing-key-file", 107 | str(skey), 108 | "--operational-certificate-issue-counter-file", 109 | str(counter), 110 | ] 111 | ) 112 | 113 | helpers._check_outfiles(vkey, skey, counter) 114 | return structs.ColdKeyPair(vkey, skey, counter) 115 | 116 | def gen_node_operational_cert( 117 | self, 118 | node_name: str, 119 | kes_vkey_file: itp.FileType, 120 | cold_skey_file: itp.FileType, 121 | cold_counter_file: itp.FileType, 122 | kes_period: int | None = None, 123 | destination_dir: itp.FileType = ".", 124 | ) -> pl.Path: 125 | """Generate a node operational certificate. 126 | 127 | This certificate is used when starting the node and not submitted through a transaction. 128 | 129 | Args: 130 | node_name: A name of the node the certificate is generated for. 131 | kes_vkey_file: A path to pool KES vkey file. 132 | cold_skey_file: A path to pool cold skey file. 133 | cold_counter_file: A path to pool cold counter file. 134 | kes_period: A start KES period. The current KES period is used when not specified. 135 | destination_dir: A path to directory for storing artifacts (optional). 136 | 137 | Returns: 138 | Path: A path to the generated certificate. 139 | """ 140 | destination_dir = pl.Path(destination_dir).expanduser() 141 | out_file = destination_dir / f"{node_name}.opcert" 142 | clusterlib_helpers._check_files_exist(out_file, clusterlib_obj=self._clusterlib_obj) 143 | 144 | kes_period = ( 145 | kes_period if kes_period is not None else self._clusterlib_obj.g_query.get_kes_period() 146 | ) 147 | 148 | self._clusterlib_obj.cli( 149 | [ 150 | "node", 151 | "issue-op-cert", 152 | "--kes-verification-key-file", 153 | str(kes_vkey_file), 154 | "--cold-signing-key-file", 155 | str(cold_skey_file), 156 | "--operational-certificate-issue-counter", 157 | str(cold_counter_file), 158 | "--kes-period", 159 | str(kes_period), 160 | "--out-file", 161 | str(out_file), 162 | ] 163 | ) 164 | 165 | helpers._check_outfiles(out_file) 166 | return out_file 167 | 168 | def __repr__(self) -> str: 169 | return f"<{self.__class__.__name__}: clusterlib_obj={id(self._clusterlib_obj)}>" 170 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # README for cardano-clusterlib 2 | 3 | [![CodeQL](https://github.com/input-output-hk/cardano-clusterlib-py/actions/workflows/codeql.yml/badge.svg)](https://github.com/input-output-hk/cardano-clusterlib-py/actions) 4 | [![Documentation Status](https://readthedocs.org/projects/cardano-clusterlib-py/badge/?version=latest)](https://cardano-clusterlib-py.readthedocs.io/en/latest/?badge=latest) 5 | [![PyPi Version](https://img.shields.io/pypi/v/cardano-clusterlib.svg)](https://pypi.org/project/cardano-clusterlib/) 6 | 7 | Python wrapper for cardano-cli for working with cardano cluster. It supports all cardano-cli commands (except parts of `genesis` and `governance`). 8 | 9 | The library is used for development of [cardano-node system tests](https://github.com/input-output-hk/cardano-node-tests). 10 | 11 | ## Installation 12 | 13 | ```sh 14 | # create and activate virtual env 15 | $ python3 -m venv .env 16 | $ . .env/bin/activate 17 | # install cardano-clusterlib from PyPI 18 | $ pip install cardano-clusterlib 19 | # - OR - install cardano-clusterlib in development mode together with dev requirements 20 | $ make install 21 | ``` 22 | 23 | ## Usage 24 | 25 | The library needs working `cardano-cli` (the command is available on `PATH`, `cardano-node` is running, `CARDANO_NODE_SOCKET_PATH` is set). In `state_dir` it expects "shelley/genesis.json". 26 | 27 | ```python 28 | # instantiate `ClusterLib` 29 | cluster = clusterlib.ClusterLib(state_dir="path/to/cluster/state_dir") 30 | ``` 31 | 32 | ### Transfer funds 33 | 34 | ```python 35 | from cardano_clusterlib import clusterlib 36 | 37 | # instantiate `ClusterLib` 38 | cluster = clusterlib.ClusterLib(state_dir="path/to/cluster/state_dir") 39 | 40 | src_address = "addr_test1vpst87uzwafqkxumyf446zr2jsyn44cfpu9fe8yqanyuh6glj2hkl" 41 | src_skey_file = "/path/to/skey" 42 | 43 | dst_addr = cluster.g_address.gen_payment_addr_and_keys(name="destination_address") 44 | amount_lovelace = 10_000_000 # 10 ADA 45 | 46 | # specify where to send funds and amounts to send 47 | txouts = [clusterlib.TxOut(address=dst_addr.address, amount=amount_lovelace)] 48 | 49 | # provide keys needed for signing the transaction 50 | tx_files = clusterlib.TxFiles(signing_key_files=[src_skey_file]) 51 | 52 | # build, sign and submit the transaction 53 | tx_raw_output = cluster.g_transaction.send_tx( 54 | src_address=src_address, 55 | tx_name="send_funds", 56 | txouts=txouts, 57 | tx_files=tx_files, 58 | ) 59 | 60 | # check that the funds were received 61 | cluster.g_query.get_utxo(dst_addr.address) 62 | ``` 63 | 64 | ### Lock and redeem funds with Plutus script 65 | 66 | ```python 67 | from cardano_clusterlib import clusterlib 68 | 69 | # instantiate `ClusterLib` 70 | cluster = clusterlib.ClusterLib(state_dir="path/to/cluster/state_dir") 71 | 72 | # source address - for funding 73 | src_address = "addr_test1vpst87uzwafqkxumyf446zr2jsyn44cfpu9fe8yqanyuh6glj2hkl" 74 | src_skey_file = "/path/to/skey" 75 | 76 | # destination address - for redeeming 77 | dst_addr = cluster.g_address.gen_payment_addr_and_keys(name="destination_address") 78 | 79 | amount_fund = 10_000_000 # 10 ADA 80 | amount_redeem = 5_000_000 # 5 ADA 81 | 82 | # get address of the Plutus script 83 | script_address = cluster.g_address.gen_payment_addr( 84 | addr_name="script_address", payment_script_file="path/to/script.plutus" 85 | ) 86 | 87 | # create a Tx output with a datum hash at the script address 88 | 89 | # provide keys needed for signing the transaction 90 | tx_files_fund = clusterlib.TxFiles(signing_key_files=[src_skey_file]) 91 | 92 | # get datum hash 93 | datum_hash = cluster.g_transaction.get_hash_script_data(script_data_file="path/to/file.datum") 94 | 95 | # specify Tx outputs for script address and collateral 96 | txouts_fund = [ 97 | clusterlib.TxOut(address=script_address, amount=amount_fund, datum_hash=datum_hash), 98 | # for collateral 99 | clusterlib.TxOut(address=dst_addr.address, amount=2_000_000), 100 | ] 101 | 102 | # build and submit the Tx 103 | tx_output_fund = cluster.g_transaction.build_tx( 104 | src_address=src_address, 105 | tx_name="fund_script_address", 106 | tx_files=tx_files_fund, 107 | txouts=txouts_fund, 108 | fee_buffer=2_000_000, 109 | ) 110 | tx_signed_fund = cluster.g_transaction.sign_tx( 111 | tx_body_file=tx_output_fund.out_file, 112 | signing_key_files=tx_files_fund.signing_key_files, 113 | tx_name="fund_script_address", 114 | ) 115 | cluster.g_transaction.submit_tx(tx_file=tx_signed_fund, txins=tx_output_fund.txins) 116 | 117 | # get newly created UTxOs 118 | fund_utxos = cluster.g_query.get_utxo(tx_raw_output=tx_output_fund) 119 | script_utxos = clusterlib.filter_utxos(utxos=fund_utxos, address=script_address) 120 | collateral_utxos = clusterlib.filter_utxos(utxos=fund_utxos, address=dst_addr.address) 121 | 122 | # redeem the locked UTxO 123 | 124 | plutus_txins = [ 125 | clusterlib.ScriptTxIn( 126 | txins=script_utxos, 127 | script_file="path/to/script.plutus", 128 | collaterals=collateral_utxos, 129 | datum_file="path/to/file.datum", 130 | redeemer_file="path/to/file.redeemer", 131 | ) 132 | ] 133 | 134 | tx_files_redeem = clusterlib.TxFiles(signing_key_files=[dst_addr.skey_file]) 135 | 136 | txouts_redeem = [ 137 | clusterlib.TxOut(address=dst_addr.address, amount=amount_redeem), 138 | ] 139 | 140 | # The entire locked UTxO will be spent and fees will be covered from the locked UTxO. 141 | # One UTxO with "amount_redeem" amount will be created on "destination address". 142 | # Second UTxO with change will be created on "destination address". 143 | tx_output_redeem = cluster.g_transaction.build_tx( 144 | src_address=src_address, # this will not be used, because txins (`script_txins`) are specified explicitly 145 | tx_name="redeem_funds", 146 | tx_files=tx_files_redeem, 147 | txouts=txouts_redeem, 148 | script_txins=plutus_txins, 149 | change_address=dst_addr.address, 150 | ) 151 | tx_signed_redeem = cluster.g_transaction.sign_tx( 152 | tx_body_file=tx_output_redeem.out_file, 153 | signing_key_files=tx_files_redeem.signing_key_files, 154 | tx_name="redeem_funds", 155 | ) 156 | cluster.g_transaction.submit_tx(tx_file=tx_signed_redeem, txins=tx_output_fund.txins) 157 | ``` 158 | 159 | ### More examples 160 | 161 | See [cardano-node-tests](https://github.com/input-output-hk/cardano-node-tests) for more examples, e.g. [minting new tokens](https://github.com/input-output-hk/cardano-node-tests/blob/4b50e8069f5294aaba14140ef0509e2857bec35d/cardano_node_tests/utils/clusterlib_utils.py#L491) or [minting new tokens with Plutus](https://github.com/input-output-hk/cardano-node-tests/blob/4b50e8069f5294aaba14140ef0509e2857bec35d/cardano_node_tests/tests/tests_plutus/test_mint_build.py#L151-L195) 162 | 163 | 164 | ## Source Documentation 165 | 166 | 167 | 168 | 169 | ## Contributing 170 | 171 | Install this package and its dependencies as described above. 172 | 173 | Run `pre-commit install` to set up the git hook scripts that will check you changes before every commit. Alternatively run `make lint` manually before pushing your changes. 174 | 175 | Follow the [Google Python Style Guide](https://google.github.io/styleguide/pyguide.html), with the exception that formatting is handled automatically by [Ruff](https://github.com/astral-sh/ruff) (through `pre-commit` command). 176 | -------------------------------------------------------------------------------- /cardano_clusterlib/gov_committee_group.py: -------------------------------------------------------------------------------- 1 | """Group of methods for Conway governance committee commands.""" 2 | 3 | import logging 4 | import pathlib as pl 5 | 6 | from cardano_clusterlib import clusterlib_helpers 7 | from cardano_clusterlib import helpers 8 | from cardano_clusterlib import structs 9 | from cardano_clusterlib import types as itp 10 | 11 | LOGGER = logging.getLogger(__name__) 12 | 13 | 14 | class GovCommitteeGroup: 15 | def __init__(self, clusterlib_obj: "itp.ClusterLib") -> None: 16 | self._clusterlib_obj = clusterlib_obj 17 | self._group_args = ("governance", "committee") 18 | 19 | def _get_cold_vkey_args( 20 | self, 21 | cold_vkey: str = "", 22 | cold_vkey_file: itp.FileType = "", 23 | cold_vkey_hash: str = "", 24 | ) -> list[str]: 25 | """Get arguments for cold verification key.""" 26 | if cold_vkey: 27 | key_args = ["--cold-verification-key", str(cold_vkey)] 28 | elif cold_vkey_file: 29 | key_args = ["--cold-verification-key-file", str(cold_vkey_file)] 30 | elif cold_vkey_hash: 31 | key_args = ["--cold-key-hash", str(cold_vkey_hash)] 32 | else: 33 | msg = "Either `cold_vkey`, `cold_vkey_file` or `cold_vkey_hash` is needed." 34 | raise ValueError(msg) 35 | 36 | return key_args 37 | 38 | def gen_cold_key_resignation_cert( 39 | self, 40 | key_name: str, 41 | cold_vkey: str = "", 42 | cold_vkey_file: itp.FileType = "", 43 | cold_vkey_hash: str = "", 44 | resignation_metadata_url: str = "", 45 | resignation_metadata_hash: str = "", 46 | destination_dir: itp.FileType = ".", 47 | ) -> pl.Path: 48 | """Create cold key resignation certificate for a Constitutional Committee Member.""" 49 | destination_dir = pl.Path(destination_dir).expanduser() 50 | cert_file = destination_dir / f"{key_name}_committee_cold_resignation.cert" 51 | clusterlib_helpers._check_files_exist(cert_file, clusterlib_obj=self._clusterlib_obj) 52 | 53 | key_args = self._get_cold_vkey_args( 54 | cold_vkey=cold_vkey, cold_vkey_file=cold_vkey_file, cold_vkey_hash=cold_vkey_hash 55 | ) 56 | 57 | self._clusterlib_obj.cli( 58 | [ 59 | *self._group_args, 60 | "create-cold-key-resignation-certificate", 61 | *key_args, 62 | "--resignation-metadata-url", 63 | resignation_metadata_url, 64 | "--resignation-metadata-hash", 65 | resignation_metadata_hash, 66 | "--out-file", 67 | str(cert_file), 68 | ] 69 | ) 70 | 71 | helpers._check_outfiles(cert_file) 72 | return cert_file 73 | 74 | def gen_hot_key_auth_cert( 75 | self, 76 | key_name: str, 77 | cold_vkey: str = "", 78 | cold_vkey_file: itp.FileType = "", 79 | cold_vkey_hash: str = "", 80 | hot_key: str = "", 81 | hot_key_file: itp.FileType = "", 82 | hot_key_hash: str = "", 83 | destination_dir: itp.FileType = ".", 84 | ) -> pl.Path: 85 | """Create hot key authorization certificate for a Constitutional Committee Member.""" 86 | destination_dir = pl.Path(destination_dir).expanduser() 87 | cert_file = destination_dir / f"{key_name}_committee_hot_auth.cert" 88 | clusterlib_helpers._check_files_exist(cert_file, clusterlib_obj=self._clusterlib_obj) 89 | 90 | cold_vkey_args = self._get_cold_vkey_args( 91 | cold_vkey=cold_vkey, cold_vkey_file=cold_vkey_file, cold_vkey_hash=cold_vkey_hash 92 | ) 93 | 94 | if hot_key: 95 | hot_key_args = ["--hot-verification-key", str(hot_key)] 96 | elif hot_key_file: 97 | hot_key_args = ["--hot-verification-key-file", str(hot_key_file)] 98 | elif hot_key_hash: 99 | hot_key_args = ["--hot-verification-key-hash", str(hot_key_hash)] 100 | else: 101 | msg = "Either `hot_key`, `hot_key_file` or `hot_key_hash` is needed." 102 | raise ValueError(msg) 103 | 104 | self._clusterlib_obj.cli( 105 | [ 106 | *self._group_args, 107 | "create-hot-key-authorization-certificate", 108 | *cold_vkey_args, 109 | *hot_key_args, 110 | "--out-file", 111 | str(cert_file), 112 | ] 113 | ) 114 | 115 | helpers._check_outfiles(cert_file) 116 | return cert_file 117 | 118 | def gen_cold_key_pair( 119 | self, key_name: str, destination_dir: itp.FileType = "." 120 | ) -> structs.KeyPair: 121 | """Create a cold key pair for a Constitutional Committee Member.""" 122 | destination_dir = pl.Path(destination_dir).expanduser() 123 | vkey = destination_dir / f"{key_name}_committee_cold.vkey" 124 | skey = destination_dir / f"{key_name}_committee_cold.skey" 125 | clusterlib_helpers._check_files_exist(vkey, skey, clusterlib_obj=self._clusterlib_obj) 126 | 127 | self._clusterlib_obj.cli( 128 | [ 129 | *self._group_args, 130 | "key-gen-cold", 131 | "--cold-verification-key-file", 132 | str(vkey), 133 | "--cold-signing-key-file", 134 | str(skey), 135 | ] 136 | ) 137 | 138 | helpers._check_outfiles(vkey, skey) 139 | return structs.KeyPair(vkey, skey) 140 | 141 | def gen_hot_key_pair( 142 | self, key_name: str, destination_dir: itp.FileType = "." 143 | ) -> structs.KeyPair: 144 | """Create a cold key pair for a Constitutional Committee Member.""" 145 | destination_dir = pl.Path(destination_dir).expanduser() 146 | vkey = destination_dir / f"{key_name}_committee_hot.vkey" 147 | skey = destination_dir / f"{key_name}_committee_hot.skey" 148 | clusterlib_helpers._check_files_exist(vkey, skey, clusterlib_obj=self._clusterlib_obj) 149 | 150 | self._clusterlib_obj.cli( 151 | [ 152 | *self._group_args, 153 | "key-gen-hot", 154 | "--verification-key-file", 155 | str(vkey), 156 | "--signing-key-file", 157 | str(skey), 158 | ] 159 | ) 160 | 161 | helpers._check_outfiles(vkey, skey) 162 | return structs.KeyPair(vkey, skey) 163 | 164 | def get_key_hash( 165 | self, 166 | vkey: str = "", 167 | vkey_file: itp.FileType = "", 168 | ) -> str: 169 | """Get the identifier (hash) of a public key.""" 170 | vkey_file = pl.Path(vkey_file).expanduser() 171 | clusterlib_helpers._check_files_exist(vkey_file, clusterlib_obj=self._clusterlib_obj) 172 | 173 | if vkey: 174 | key_args = ["--verification-key", str(vkey)] 175 | elif vkey_file: 176 | key_args = ["--verification-key-file", str(vkey_file)] 177 | else: 178 | msg = "Either `vkey` or `vkey_file` is needed." 179 | raise ValueError(msg) 180 | 181 | key_hash = ( 182 | self._clusterlib_obj.cli([*self._group_args, "key-hash", *key_args]) 183 | .stdout.rstrip() 184 | .decode("ascii") 185 | ) 186 | 187 | return key_hash 188 | -------------------------------------------------------------------------------- /upgrading/refactor_to_0_4_0rc1.sed: -------------------------------------------------------------------------------- 1 | # use as `sed -i -f refactor_to_0_4_0rc1.sed *.py` 2 | # match 'cluster.*' and not 'clusterlib_utils.*' 3 | s/cluste\(r[^l][^(.]*\|r\)\.gen_payment_addr_and_keys(/cluste\1.g_address.gen_payment_addr_and_keys(/g 4 | s/cluste\(r[^l][^(.]*\|r\)\.gen_payment_addr(/cluste\1.g_address.gen_payment_addr(/g 5 | s/cluste\(r[^l][^(.]*\|r\)\.gen_payment_key_pair(/cluste\1.g_address.gen_payment_key_pair(/g 6 | s/cluste\(r[^l][^(.]*\|r\)\.get_payment_vkey_hash(/cluste\1.g_address.get_payment_vkey_hash(/g 7 | s/cluste\(r[^l][^(.]*\|r\)\.get_address_info(/cluste\1.g_address.get_address_info(/g 8 | s/cluste\(r[^l][^(.]*\|r\)\.gen_script_addr(/cluste\1.g_address.gen_script_addr(/g 9 | 10 | s/cluste\(r[^l][^(.]*\|r\)\.genesis_keys/cluste\1.g_genesis.genesis_keys/g 11 | s/cluste\(r[^l][^(.]*\|r\)\.genesis_utxo_addr/cluste\1.g_genesis.genesis_utxo_addr/g 12 | s/cluste\(r[^l][^(.]*\|r\)\.gen_genesis_addr(/cluste\1.g_genesis.gen_genesis_addr(/g 13 | 14 | s/cluste\(r[^l][^(.]*\|r\)\.gen_update_proposal(/cluste\1.g_governance.gen_update_proposal(/g 15 | s/cluste\(r[^l][^(.]*\|r\)\.gen_mir_cert_to_treasury(/cluste\1.g_governance.gen_mir_cert_to_treasury(/g 16 | s/cluste\(r[^l][^(.]*\|r\)\.gen_mir_cert_to_rewards(/cluste\1.g_governance.gen_mir_cert_to_rewards(/g 17 | s/cluste\(r[^l][^(.]*\|r\)\.gen_mir_cert_stake_addr(/cluste\1.g_governance.gen_mir_cert_stake_addr(/g 18 | s/cluste\(r[^l][^(.]*\|r\)\.submit_update_proposal(/cluste\1.g_governance.submit_update_proposal(/g 19 | 20 | s/cluste\(r[^l][^(.]*\|r\)\.gen_verification_key(/cluste\1.g_key.gen_verification_key(/g 21 | s/cluste\(r[^l][^(.]*\|r\)\.gen_non_extended_verification_key(/cluste\1.g_key.gen_non_extended_verification_key(/g 22 | 23 | s/cluste\(r[^l][^(.]*\|r\)\.gen_kes_key_pair(/cluste\1.g_node.gen_kes_key_pair(/g 24 | s/cluste\(r[^l][^(.]*\|r\)\.gen_vrf_key_pair(/cluste\1.g_node.gen_vrf_key_pair(/g 25 | s/cluste\(r[^l][^(.]*\|r\)\.gen_cold_key_pair_and_counter(/cluste\1.g_node.gen_cold_key_pair_and_counter(/g 26 | s/cluste\(r[^l][^(.]*\|r\)\.gen_node_operational_cert(/cluste\1.g_node.gen_node_operational_cert(/g 27 | 28 | s/cluste\(r[^l][^(.]*\|r\)\.query_cli(/cluste\1.g_query.query_cli(/g 29 | s/cluste\(r[^l][^(.]*\|r\)\.get_utxo(/cluste\1.g_query.get_utxo(/g 30 | s/cluste\(r[^l][^(.]*\|r\)\.get_tip(/cluste\1.g_query.get_tip(/g 31 | s/cluste\(r[^l][^(.]*\|r\)\.get_ledger_state(/cluste\1.g_query.get_ledger_state(/g 32 | s/cluste\(r[^l][^(.]*\|r\)\.save_ledger_state(/cluste\1.g_query.save_ledger_state(/g 33 | s/cluste\(r[^l][^(.]*\|r\)\.get_protocol_state(/cluste\1.g_query.get_protocol_state(/g 34 | s/cluste\(r[^l][^(.]*\|r\)\.get_protocol_params(/cluste\1.g_query.get_protocol_params(/g 35 | s/cluste\(r[^l][^(.]*\|r\)\.get_registered_stake_pools_ledger_state(/cluste\1.g_query.get_registered_stake_pools_ledger_state(/g 36 | s/cluste\(r[^l][^(.]*\|r\)\.get_stake_snapshot(/cluste\1.g_query.get_stake_snapshot(/g 37 | s/cluste\(r[^l][^(.]*\|r\)\.get_pool_params(/cluste\1.g_query.get_pool_params(/g 38 | s/cluste\(r[^l][^(.]*\|r\)\.get_stake_addr_info(/cluste\1.g_query.get_stake_addr_info(/g 39 | s/cluste\(r[^l][^(.]*\|r\)\.get_address_deposit(/cluste\1.g_query.get_address_deposit(/g 40 | s/cluste\(r[^l][^(.]*\|r\)\.get_pool_deposit(/cluste\1.g_query.get_pool_deposit(/g 41 | s/cluste\(r[^l][^(.]*\|r\)\.get_stake_distribution(/cluste\1.g_query.get_stake_distribution(/g 42 | s/cluste\(r[^l][^(.]*\|r\)\.get_stake_pools(/cluste\1.g_query.get_stake_pools(/g 43 | s/cluste\(r[^l][^(.]*\|r\)\.get_leadership_schedule(/cluste\1.g_query.get_leadership_schedule(/g 44 | s/cluste\(r[^l][^(.]*\|r\)\.get_slot_no(/cluste\1.g_query.get_slot_no(/g 45 | s/cluste\(r[^l][^(.]*\|r\)\.get_block_no(/cluste\1.g_query.get_block_no(/g 46 | s/cluste\(r[^l][^(.]*\|r\)\.get_epoch(/cluste\1.g_query.get_epoch(/g 47 | s/cluste\(r[^l][^(.]*\|r\)\.get_era(/cluste\1.g_query.get_era(/g 48 | s/cluste\(r[^l][^(.]*\|r\)\.get_address_balance(/cluste\1.g_query.get_address_balance(/g 49 | s/cluste\(r[^l][^(.]*\|r\)\.get_utxo_with_highest_amount(/cluste\1.g_query.get_utxo_with_highest_amount(/g 50 | s/cluste\(r[^l][^(.]*\|r\)\.get_kes_period(/cluste\1.g_query.get_kes_period(/g 51 | s/cluste\(r[^l][^(.]*\|r\)\.get_kes_period_info(/cluste\1.g_query.get_kes_period_info(/g 52 | 53 | s/cluste\(r[^l][^(.]*\|r\)\.gen_stake_addr(/cluste\1.g_stake_address.gen_stake_addr(/g 54 | s/cluste\(r[^l][^(.]*\|r\)\.gen_stake_key_pair(/cluste\1.g_stake_address.gen_stake_key_pair(/g 55 | s/cluste\(r[^l][^(.]*\|r\)\.gen_stake_addr_registration_cert(/cluste\1.g_stake_address.gen_stake_addr_registration_cert(/g 56 | s/cluste\(r[^l][^(.]*\|r\)\.gen_stake_addr_deregistration_cert(/cluste\1.g_stake_address.gen_stake_addr_deregistration_cert(/g 57 | s/cluste\(r[^l][^(.]*\|r\)\.gen_stake_addr_delegation_cert(/cluste\1.g_stake_address.gen_stake_addr_delegation_cert(/g 58 | s/cluste\(r[^l][^(.]*\|r\)\.gen_stake_addr_and_keys(/cluste\1.g_stake_address.gen_stake_addr_and_keys(/g 59 | s/cluste\(r[^l][^(.]*\|r\)\.withdraw_reward(/cluste\1.g_stake_address.withdraw_reward(/g 60 | 61 | s/cluste\(r[^l][^(.]*\|r\)\.gen_pool_metadata_hash(/cluste\1.g_stake_pool.gen_pool_metadata_hash(/g 62 | s/cluste\(r[^l][^(.]*\|r\)\.gen_pool_registration_cert(/cluste\1.g_stake_pool.gen_pool_registration_cert(/g 63 | s/cluste\(r[^l][^(.]*\|r\)\.gen_pool_deregistration_cert(/cluste\1.g_stake_pool.gen_pool_deregistration_cert(/g 64 | s/cluste\(r[^l][^(.]*\|r\)\.get_stake_pool_id(/cluste\1.g_stake_pool.get_stake_pool_id(/g 65 | s/cluste\(r[^l][^(.]*\|r\)\.create_stake_pool(/cluste\1.g_stake_pool.create_stake_pool(/g 66 | s/cluste\(r[^l][^(.]*\|r\)\.register_stake_pool(/cluste\1.g_stake_pool.register_stake_pool(/g 67 | s/cluste\(r[^l][^(.]*\|r\)\.deregister_stake_pool(/cluste\1.g_stake_pool.deregister_stake_pool(/g 68 | 69 | s/cluste\(r[^l][^(.]*\|r\)\.tx_era_arg/cluste\1.g_transaction.tx_era_arg/g 70 | s/cluste\(r[^l][^(.]*\|r\)\.calculate_tx_ttl(/cluste\1.g_transaction.calculate_tx_ttl(/g 71 | s/cluste\(r[^l][^(.]*\|r\)\.get_txid(/cluste\1.g_transaction.get_txid(/g 72 | s/cluste\(r[^l][^(.]*\|r\)\.view_tx(/cluste\1.g_transaction.view_tx(/g 73 | s/cluste\(r[^l][^(.]*\|r\)\.get_hash_script_data(/cluste\1.g_transaction.get_hash_script_data(/g 74 | s/cluste\(r[^l][^(.]*\|r\)\.get_tx_deposit(/cluste\1.g_transaction.get_tx_deposit(/g 75 | s/cluste\(r[^l][^(.]*\|r\)\.build_raw_tx_bare(/cluste\1.g_transaction.build_raw_tx_bare(/g 76 | s/cluste\(r[^l][^(.]*\|r\)\.build_raw_tx(/cluste\1.g_transaction.build_raw_tx(/g 77 | s/cluste\(r[^l][^(.]*\|r\)\.estimate_fee(/cluste\1.g_transaction.estimate_fee(/g 78 | s/cluste\(r[^l][^(.]*\|r\)\.calculate_tx_fee(/cluste\1.g_transaction.calculate_tx_fee(/g 79 | s/cluste\(r[^l][^(.]*\|r\)\.calculate_min_value(/cluste\1.g_transaction.calculate_min_value(/g 80 | s/cluste\(r[^l][^(.]*\|r\)\.calculate_min_req_utxo(/cluste\1.g_transaction.calculate_min_req_utxo(/g 81 | s/cluste\(r[^l][^(.]*\|r\)\.build_tx(/cluste\1.g_transaction.build_tx(/g 82 | s/cluste\(r[^l][^(.]*\|r\)\.sign_tx(/cluste\1.g_transaction.sign_tx(/g 83 | s/cluste\(r[^l][^(.]*\|r\)\.witness_tx(/cluste\1.g_transaction.witness_tx(/g 84 | s/cluste\(r[^l][^(.]*\|r\)\.assemble_tx(/cluste\1.g_transaction.assemble_tx(/g 85 | s/cluste\(r[^l][^(.]*\|r\)\.submit_tx_bare(/cluste\1.g_transaction.submit_tx_bare(/g 86 | s/cluste\(r[^l][^(.]*\|r\)\.submit_tx(/cluste\1.g_transaction.submit_tx(/g 87 | s/cluste\(r[^l][^(.]*\|r\)\.send_tx(/cluste\1.g_transaction.send_tx(/g 88 | s/cluste\(r[^l][^(.]*\|r\)\.build_multisig_script(/cluste\1.g_transaction.build_multisig_script(/g 89 | s/cluste\(r[^l][^(.]*\|r\)\.get_policyid(/cluste\1.g_transaction.get_policyid(/g 90 | s/cluste\(r[^l][^(.]*\|r\)\.calculate_plutus_script_cost(/cluste\1.g_transaction.calculate_plutus_script_cost(/g 91 | s/cluste\(r[^l][^(.]*\|r\)\.send_funds(/cluste\1.g_transaction.send_funds(/g 92 | -------------------------------------------------------------------------------- /cardano_clusterlib/key_group.py: -------------------------------------------------------------------------------- 1 | """Group of methods for working with key commands.""" 2 | 3 | import logging 4 | import pathlib as pl 5 | import typing as tp 6 | 7 | from cardano_clusterlib import clusterlib_helpers 8 | from cardano_clusterlib import consts 9 | from cardano_clusterlib import helpers 10 | from cardano_clusterlib import types as itp 11 | 12 | LOGGER = logging.getLogger(__name__) 13 | 14 | 15 | class KeyGroup: 16 | def __init__(self, clusterlib_obj: "itp.ClusterLib") -> None: 17 | self._clusterlib_obj = clusterlib_obj 18 | 19 | def gen_verification_key( 20 | self, 21 | key_name: str, 22 | signing_key_file: itp.FileType, 23 | destination_dir: itp.FileType = ".", 24 | ) -> pl.Path: 25 | """Generate a verification file from a signing key. 26 | 27 | Args: 28 | key_name: A name of the key. 29 | signing_key_file: A path to signing key file. 30 | destination_dir: A path to directory for storing artifacts (optional). 31 | 32 | Returns: 33 | Path: A path to the generated verification key file. 34 | """ 35 | destination_dir = pl.Path(destination_dir).expanduser() 36 | out_file = destination_dir / f"{key_name}.vkey" 37 | clusterlib_helpers._check_files_exist(out_file, clusterlib_obj=self._clusterlib_obj) 38 | 39 | self._clusterlib_obj.cli( 40 | [ 41 | "key", 42 | "verification-key", 43 | "--signing-key-file", 44 | str(signing_key_file), 45 | "--verification-key-file", 46 | str(out_file), 47 | ] 48 | ) 49 | 50 | helpers._check_outfiles(out_file) 51 | return out_file 52 | 53 | def gen_non_extended_verification_key( 54 | self, 55 | key_name: str, 56 | extended_verification_key_file: itp.FileType, 57 | destination_dir: itp.FileType = ".", 58 | ) -> pl.Path: 59 | """Generate a non-extended key from a verification key. 60 | 61 | Args: 62 | key_name: A name of the key. 63 | extended_verification_key_file: A path to the extended verification key file. 64 | destination_dir: A path to directory for storing artifacts (optional). 65 | 66 | Returns: 67 | Path: A path to the generated non-extended verification key file. 68 | """ 69 | destination_dir = pl.Path(destination_dir).expanduser() 70 | out_file = destination_dir / f"{key_name}.vkey" 71 | clusterlib_helpers._check_files_exist(out_file, clusterlib_obj=self._clusterlib_obj) 72 | 73 | self._clusterlib_obj.cli( 74 | [ 75 | "key", 76 | "non-extended-key", 77 | "--extended-verification-key-file", 78 | str(extended_verification_key_file), 79 | "--verification-key-file", 80 | str(out_file), 81 | ] 82 | ) 83 | 84 | helpers._check_outfiles(out_file) 85 | return out_file 86 | 87 | def gen_mnemonic( 88 | self, 89 | size: tp.Literal[12, 15, 18, 21, 24], 90 | out_file: itp.FileType = "", 91 | ) -> list[str]: 92 | """Generate a mnemonic sentence that can be used for key derivation. 93 | 94 | Args: 95 | size: Number of words in the mnemonic (12, 15, 18, 21, or 24). 96 | out_file: A path to a file where the mnemonic will be stored (optional). 97 | 98 | Returns: 99 | list[str]: A list of words in the generated mnemonic. 100 | """ 101 | out_args = [] 102 | if out_file: 103 | clusterlib_helpers._check_files_exist(out_file, clusterlib_obj=self._clusterlib_obj) 104 | out_args = ["--out-file", str(out_file)] 105 | 106 | out = ( 107 | self._clusterlib_obj.cli( 108 | [ 109 | "key", 110 | "generate-mnemonic", 111 | *out_args, 112 | "--size", 113 | str(size), 114 | ] 115 | ) 116 | .stdout.strip() 117 | .decode("ascii") 118 | ) 119 | 120 | if out_file: 121 | helpers._check_outfiles(out_file) 122 | words = helpers.read_from_file(file=out_file).strip().split() 123 | else: 124 | words = out.split() 125 | 126 | return words 127 | 128 | def derive_from_mnemonic( 129 | self, 130 | key_name: str, 131 | key_type: consts.KeyType, 132 | mnemonic_file: itp.FileType, 133 | account_number: int = 0, 134 | key_number: int | None = None, 135 | out_format: consts.OutputFormat = consts.OutputFormat.TEXT_ENVELOPE, 136 | destination_dir: itp.FileType = ".", 137 | ) -> pl.Path: 138 | """Derive an extended signing key from a mnemonic sentence. 139 | 140 | Args: 141 | key_name: A name of the key. 142 | key_type: A type of the key. 143 | mnemonic_file: A path to a file containing the mnemonic sentence. 144 | account_number: An account number (default is 0). 145 | key_number: A key number (optional, required for payment and stake keys). 146 | out_format: An output format (default is text-envelope). 147 | destination_dir: A path to directory for storing artifacts (optional). 148 | 149 | Returns: 150 | Path: A path to the generated extended signing key file. 151 | """ 152 | destination_dir = pl.Path(destination_dir).expanduser() 153 | out_file = destination_dir / f"{key_name}.skey" 154 | clusterlib_helpers._check_files_exist(out_file, clusterlib_obj=self._clusterlib_obj) 155 | 156 | key_args = [] 157 | key_number_err = f"`key_number` must be specified when key_type is '{key_type.value}'" 158 | if key_type == consts.KeyType.DREP: 159 | key_args.append("--drep-key") 160 | elif key_type == consts.KeyType.CC_COLD: 161 | key_args.append("--cc-cold-key") 162 | elif key_type == consts.KeyType.CC_HOT: 163 | key_args.append("--cc-hot-key") 164 | elif key_type == consts.KeyType.PAYMENT: 165 | if key_number is None: 166 | raise ValueError(key_number_err) 167 | key_args.extend(["--payment-key-with-number", str(key_number)]) 168 | elif key_type == consts.KeyType.STAKE: 169 | if key_number is None: 170 | raise ValueError(key_number_err) 171 | key_args.extend(["--stake-key-with-number", str(key_number)]) 172 | else: 173 | err = f"Unsupported key_type: {key_type}" 174 | raise ValueError(err) 175 | 176 | self._clusterlib_obj.cli( 177 | [ 178 | "key", 179 | "derive-from-mnemonic", 180 | f"--key-output-{out_format.value}", 181 | *key_args, 182 | "--account-number", 183 | str(account_number), 184 | "--mnemonic-from-file", 185 | str(mnemonic_file), 186 | "--signing-key-file", 187 | str(out_file), 188 | ] 189 | ) 190 | 191 | helpers._check_outfiles(out_file) 192 | return out_file 193 | 194 | def __repr__(self) -> str: 195 | return f"<{self.__class__.__name__}: clusterlib_obj={id(self._clusterlib_obj)}>" 196 | -------------------------------------------------------------------------------- /cardano_clusterlib/address_group.py: -------------------------------------------------------------------------------- 1 | """Group of methods for working with payment addresses.""" 2 | 3 | import json 4 | import logging 5 | import pathlib as pl 6 | 7 | from cardano_clusterlib import clusterlib_helpers 8 | from cardano_clusterlib import helpers 9 | from cardano_clusterlib import structs 10 | from cardano_clusterlib import types as itp 11 | 12 | LOGGER = logging.getLogger(__name__) 13 | 14 | 15 | class AddressGroup: 16 | def __init__(self, clusterlib_obj: "itp.ClusterLib") -> None: 17 | self._clusterlib_obj = clusterlib_obj 18 | 19 | def gen_payment_addr( 20 | self, 21 | addr_name: str, 22 | payment_vkey: str | None = None, 23 | payment_vkey_file: itp.FileType | None = None, 24 | payment_script_file: itp.FileType | None = None, 25 | stake_vkey: str | None = None, 26 | stake_vkey_file: itp.FileType | None = None, 27 | stake_script_file: itp.FileType | None = None, 28 | stake_address: str | None = None, 29 | destination_dir: itp.FileType = ".", 30 | ) -> str: 31 | """Generate a payment address, with optional delegation to a stake address. 32 | 33 | Args: 34 | addr_name: A name of payment address. 35 | payment_vkey: A vkey file (Bech32, optional). 36 | payment_vkey_file: A path to corresponding vkey file (optional). 37 | payment_script_file: A path to corresponding payment script file (optional). 38 | stake_vkey: A stake vkey file (optional). 39 | stake_vkey_file: A path to corresponding stake vkey file (optional). 40 | stake_script_file: A path to corresponding stake script file (optional). 41 | stake_address: A stake address (Bech32, optional). 42 | destination_dir: A path to directory for storing artifacts (optional). 43 | 44 | Returns: 45 | str: A generated payment address. 46 | """ 47 | destination_dir = pl.Path(destination_dir).expanduser() 48 | out_file = destination_dir / f"{addr_name}.addr" 49 | clusterlib_helpers._check_files_exist(out_file, clusterlib_obj=self._clusterlib_obj) 50 | 51 | if payment_vkey_file: 52 | cli_args = ["--payment-verification-key-file", str(payment_vkey_file)] 53 | elif payment_script_file: 54 | cli_args = ["--payment-script-file", str(payment_script_file)] 55 | elif payment_vkey: 56 | cli_args = ["--payment-verification-key", str(payment_vkey)] 57 | else: 58 | msg = "Either `payment_vkey_file`, `payment_script_file` or `payment_vkey` is needed." 59 | raise ValueError(msg) 60 | 61 | if stake_vkey: 62 | cli_args.extend(["--stake-verification-key", str(stake_vkey)]) 63 | elif stake_vkey_file: 64 | cli_args.extend(["--stake-verification-key-file", str(stake_vkey_file)]) 65 | elif stake_script_file: 66 | cli_args.extend(["--stake-script-file", str(stake_script_file)]) 67 | elif stake_address: 68 | cli_args.extend(["--stake-address", str(stake_address)]) 69 | 70 | self._clusterlib_obj.cli( 71 | [ 72 | "address", 73 | "build", 74 | *self._clusterlib_obj.magic_args, 75 | *cli_args, 76 | "--out-file", 77 | str(out_file), 78 | ] 79 | ) 80 | 81 | helpers._check_outfiles(out_file) 82 | return helpers.read_address_from_file(out_file) 83 | 84 | def gen_payment_key_pair( 85 | self, key_name: str, extended: bool = False, destination_dir: itp.FileType = "." 86 | ) -> structs.KeyPair: 87 | """Generate an address key pair. 88 | 89 | Args: 90 | key_name: A name of the key pair. 91 | extended: A bool indicating whether to generate extended ed25519 Shelley-era key 92 | (False by default). 93 | destination_dir: A path to directory for storing artifacts (optional). 94 | 95 | Returns: 96 | structs.KeyPair: A data container containing the key pair. 97 | """ 98 | destination_dir = pl.Path(destination_dir).expanduser() 99 | vkey = destination_dir / f"{key_name}.vkey" 100 | skey = destination_dir / f"{key_name}.skey" 101 | clusterlib_helpers._check_files_exist(vkey, skey, clusterlib_obj=self._clusterlib_obj) 102 | 103 | extended_args = ["--extended-key"] if extended else [] 104 | 105 | self._clusterlib_obj.cli( 106 | [ 107 | "address", 108 | "key-gen", 109 | "--verification-key-file", 110 | str(vkey), 111 | *extended_args, 112 | "--signing-key-file", 113 | str(skey), 114 | ] 115 | ) 116 | 117 | helpers._check_outfiles(vkey, skey) 118 | return structs.KeyPair(vkey, skey) 119 | 120 | def get_payment_vkey_hash( 121 | self, 122 | payment_vkey_file: itp.FileType | None = None, 123 | payment_vkey: str | None = None, 124 | ) -> str: 125 | """Return the hash of an address key. 126 | 127 | Args: 128 | payment_vkey_file: A path to payment vkey file (optional). 129 | payment_vkey: A payment vkey, (Bech32, optional). 130 | 131 | Returns: 132 | str: A generated hash. 133 | """ 134 | if payment_vkey: 135 | cli_args = ["--payment-verification-key", payment_vkey] 136 | elif payment_vkey_file: 137 | cli_args = ["--payment-verification-key-file", str(payment_vkey_file)] 138 | else: 139 | msg = "Either `payment_vkey` or `payment_vkey_file` is needed." 140 | raise ValueError(msg) 141 | 142 | return ( 143 | self._clusterlib_obj.cli(["address", "key-hash", *cli_args]) 144 | .stdout.rstrip() 145 | .decode("ascii") 146 | ) 147 | 148 | def get_address_info( 149 | self, 150 | address: str, 151 | ) -> structs.AddressInfo: 152 | """Get information about an address. 153 | 154 | Args: 155 | address: A Cardano address. 156 | 157 | Returns: 158 | structs.AddressInfo: A data container containing address info. 159 | """ 160 | addr_dict: dict[str, str] = json.loads( 161 | self._clusterlib_obj.cli(["address", "info", "--address", str(address)]) 162 | .stdout.rstrip() 163 | .decode("utf-8") 164 | ) 165 | return structs.AddressInfo(**addr_dict) 166 | 167 | def gen_payment_addr_and_keys( 168 | self, 169 | name: str, 170 | stake_vkey_file: itp.FileType | None = None, 171 | stake_script_file: itp.FileType | None = None, 172 | destination_dir: itp.FileType = ".", 173 | ) -> structs.AddressRecord: 174 | """Generate payment address and key pair. 175 | 176 | Args: 177 | name: A name of the address and key pair. 178 | stake_vkey_file: A path to corresponding stake vkey file (optional). 179 | stake_script_file: A path to corresponding payment script file (optional). 180 | destination_dir: A path to directory for storing artifacts (optional). 181 | 182 | Returns: 183 | structs.AddressRecord: A data container containing the address and 184 | key pair / script file. 185 | """ 186 | key_pair = self.gen_payment_key_pair(key_name=name, destination_dir=destination_dir) 187 | addr = self.gen_payment_addr( 188 | addr_name=name, 189 | payment_vkey_file=key_pair.vkey_file, 190 | stake_vkey_file=stake_vkey_file, 191 | stake_script_file=stake_script_file, 192 | destination_dir=destination_dir, 193 | ) 194 | 195 | return structs.AddressRecord( 196 | address=addr, vkey_file=key_pair.vkey_file, skey_file=key_pair.skey_file 197 | ) 198 | 199 | def __repr__(self) -> str: 200 | return f"<{self.__class__.__name__}: clusterlib_obj={id(self._clusterlib_obj)}>" 201 | -------------------------------------------------------------------------------- /cardano_clusterlib/legacy_gov_group.py: -------------------------------------------------------------------------------- 1 | """Group of methods for governance.""" 2 | 3 | import logging 4 | import pathlib as pl 5 | 6 | from cardano_clusterlib import clusterlib_helpers 7 | from cardano_clusterlib import helpers 8 | from cardano_clusterlib import structs 9 | from cardano_clusterlib import types as itp 10 | 11 | LOGGER = logging.getLogger(__name__) 12 | 13 | 14 | class LegacyGovGroup: 15 | def __init__(self, clusterlib_obj: "itp.ClusterLib") -> None: 16 | self._clusterlib_obj = clusterlib_obj 17 | self._cli_args = ("cardano-cli", "legacy", "governance") 18 | 19 | def gen_update_proposal( 20 | self, 21 | cli_args: itp.UnpackableSequence, 22 | epoch: int, 23 | tx_name: str, 24 | destination_dir: itp.FileType = ".", 25 | ) -> pl.Path: 26 | """Create an update proposal. 27 | 28 | Args: 29 | cli_args: A list (iterable) of CLI arguments. 30 | epoch: An epoch where the update proposal will take effect. 31 | tx_name: A name of the transaction. 32 | destination_dir: A path to directory for storing artifacts (optional). 33 | 34 | Returns: 35 | Path: A path to the update proposal file. 36 | """ 37 | destination_dir = pl.Path(destination_dir).expanduser() 38 | out_file = destination_dir / f"{tx_name}_update.proposal" 39 | clusterlib_helpers._check_files_exist(out_file, clusterlib_obj=self._clusterlib_obj) 40 | 41 | self._clusterlib_obj.cli( 42 | [ 43 | *self._cli_args, 44 | "create-update-proposal", 45 | *cli_args, 46 | "--out-file", 47 | str(out_file), 48 | "--epoch", 49 | str(epoch), 50 | *helpers._prepend_flag( 51 | "--genesis-verification-key-file", 52 | self._clusterlib_obj.g_genesis.genesis_keys.genesis_vkeys, 53 | ), 54 | ], 55 | add_default_args=False, 56 | ) 57 | 58 | helpers._check_outfiles(out_file) 59 | return out_file 60 | 61 | def gen_mir_cert_to_treasury( 62 | self, 63 | transfer: int, 64 | tx_name: str, 65 | destination_dir: itp.FileType = ".", 66 | ) -> pl.Path: 67 | """Create an MIR certificate to transfer from the reserves pot to the treasury pot. 68 | 69 | Args: 70 | transfer: An amount of Lovelace to transfer. 71 | tx_name: A name of the transaction. 72 | destination_dir: A path to directory for storing artifacts (optional). 73 | 74 | Returns: 75 | Path: A path to the MIR certificate file. 76 | """ 77 | destination_dir = pl.Path(destination_dir).expanduser() 78 | out_file = destination_dir / f"{tx_name}_mir_to_treasury.cert" 79 | clusterlib_helpers._check_files_exist(out_file, clusterlib_obj=self._clusterlib_obj) 80 | 81 | self._clusterlib_obj.cli( 82 | [ 83 | *self._cli_args, 84 | "create-mir-certificate", 85 | "transfer-to-treasury", 86 | "--transfer", 87 | str(transfer), 88 | "--out-file", 89 | str(out_file), 90 | ], 91 | add_default_args=False, 92 | ) 93 | 94 | helpers._check_outfiles(out_file) 95 | return out_file 96 | 97 | def gen_mir_cert_to_rewards( 98 | self, 99 | transfer: int, 100 | tx_name: str, 101 | destination_dir: itp.FileType = ".", 102 | ) -> pl.Path: 103 | """Create an MIR certificate to transfer from the treasury pot to the reserves pot. 104 | 105 | Args: 106 | transfer: An amount of Lovelace to transfer. 107 | tx_name: A name of the transaction. 108 | destination_dir: A path to directory for storing artifacts (optional). 109 | 110 | Returns: 111 | Path: A path to the MIR certificate file. 112 | """ 113 | destination_dir = pl.Path(destination_dir).expanduser() 114 | out_file = destination_dir / f"{tx_name}_mir_to_rewards.cert" 115 | clusterlib_helpers._check_files_exist(out_file, clusterlib_obj=self._clusterlib_obj) 116 | 117 | self._clusterlib_obj.cli( 118 | [ 119 | *self._cli_args, 120 | "create-mir-certificate", 121 | "transfer-to-rewards", 122 | "--transfer", 123 | str(transfer), 124 | "--out-file", 125 | str(out_file), 126 | ], 127 | add_default_args=False, 128 | ) 129 | 130 | helpers._check_outfiles(out_file) 131 | return out_file 132 | 133 | def gen_mir_cert_stake_addr( 134 | self, 135 | stake_addr: str, 136 | reward: int, 137 | tx_name: str, 138 | use_treasury: bool = False, 139 | destination_dir: itp.FileType = ".", 140 | ) -> pl.Path: 141 | """Create an MIR certificate to pay stake addresses. 142 | 143 | Args: 144 | stake_addr: A stake address string. 145 | reward: An amount of Lovelace to transfer. 146 | tx_name: A name of the transaction. 147 | use_treasury: A bool indicating whether to use treasury or reserves (default). 148 | destination_dir: A path to directory for storing artifacts (optional). 149 | 150 | Returns: 151 | Path: A path to the MIR certificate file. 152 | """ 153 | destination_dir = pl.Path(destination_dir).expanduser() 154 | funds_src = "treasury" if use_treasury else "reserves" 155 | out_file = destination_dir / f"{tx_name}_{funds_src}_mir_stake.cert" 156 | clusterlib_helpers._check_files_exist(out_file, clusterlib_obj=self._clusterlib_obj) 157 | 158 | self._clusterlib_obj.cli( 159 | [ 160 | *self._cli_args, 161 | "create-mir-certificate", 162 | "stake-addresses", 163 | f"--{funds_src}", 164 | "--stake-address", 165 | str(stake_addr), 166 | "--reward", 167 | str(reward), 168 | "--out-file", 169 | str(out_file), 170 | ], 171 | add_default_args=False, 172 | ) 173 | 174 | helpers._check_outfiles(out_file) 175 | return out_file 176 | 177 | def submit_update_proposal( 178 | self, 179 | cli_args: itp.UnpackableSequence, 180 | src_address: str, 181 | src_skey_file: itp.FileType, 182 | tx_name: str, 183 | epoch: int | None = None, 184 | destination_dir: itp.FileType = ".", 185 | ) -> structs.TxRawOutput: 186 | """Submit an update proposal. 187 | 188 | Args: 189 | cli_args: A list (iterable) of CLI arguments. 190 | src_address: An address used for fee and inputs. 191 | src_skey_file: A path to skey file corresponding to the `src_address`. 192 | tx_name: A name of the transaction. 193 | epoch: An epoch where the update proposal will take effect (optional). 194 | destination_dir: A path to directory for storing artifacts (optional). 195 | 196 | Returns: 197 | structs.TxRawOutput: A data container with transaction output details. 198 | """ 199 | # TODO: assumption is update proposals submitted near beginning of epoch 200 | epoch = epoch if epoch is not None else self._clusterlib_obj.g_query.get_epoch() 201 | 202 | out_file = self.gen_update_proposal( 203 | cli_args=cli_args, 204 | epoch=epoch, 205 | tx_name=tx_name, 206 | destination_dir=destination_dir, 207 | ) 208 | 209 | return self._clusterlib_obj.g_transaction.send_tx( 210 | src_address=src_address, 211 | tx_name=f"{tx_name}_submit_proposal", 212 | tx_files=structs.TxFiles( 213 | proposal_files=[out_file], 214 | signing_key_files=[ 215 | *self._clusterlib_obj.g_genesis.genesis_keys.delegate_skeys, 216 | pl.Path(src_skey_file), 217 | ], 218 | ), 219 | destination_dir=destination_dir, 220 | ) 221 | 222 | def __repr__(self) -> str: 223 | return f"<{self.__class__.__name__}: clusterlib_obj={id(self._clusterlib_obj)}>" 224 | -------------------------------------------------------------------------------- /cardano_clusterlib/gov_vote_group.py: -------------------------------------------------------------------------------- 1 | """Group of methods for Conway governance vote commands.""" 2 | 3 | import json 4 | import logging 5 | import pathlib as pl 6 | import typing as tp 7 | 8 | from cardano_clusterlib import clusterlib_helpers 9 | from cardano_clusterlib import consts 10 | from cardano_clusterlib import helpers 11 | from cardano_clusterlib import structs 12 | from cardano_clusterlib import types as itp 13 | 14 | LOGGER = logging.getLogger(__name__) 15 | 16 | 17 | class GovVoteGroup: 18 | def __init__(self, clusterlib_obj: "itp.ClusterLib") -> None: 19 | self._clusterlib_obj = clusterlib_obj 20 | self._group_args = ("governance", "vote") 21 | 22 | def _get_vote_args( 23 | self, 24 | vote: consts.Votes, 25 | ) -> list[str]: 26 | if vote == consts.Votes.YES: 27 | vote_args = ["--yes"] 28 | elif vote == consts.Votes.NO: 29 | vote_args = ["--no"] 30 | elif vote == consts.Votes.ABSTAIN: 31 | vote_args = ["--abstain"] 32 | else: 33 | msg = "No vote was specified." 34 | raise ValueError(msg) 35 | 36 | return vote_args 37 | 38 | def _get_gov_action_args( 39 | self, 40 | action_txid: str, 41 | action_ix: int, 42 | ) -> list[str]: 43 | gov_action_args = [ 44 | "--governance-action-tx-id", 45 | str(action_txid), 46 | "--governance-action-index", 47 | str(action_ix), 48 | ] 49 | return gov_action_args 50 | 51 | def _get_anchor_args( 52 | self, 53 | anchor_url: str = "", 54 | anchor_data_hash: str = "", 55 | ) -> list[str]: 56 | anchor_args = [] 57 | if anchor_url: 58 | if not anchor_data_hash: 59 | msg = "Anchor data hash is required when anchor URL is specified." 60 | raise ValueError(msg) 61 | anchor_args = [ 62 | "--anchor-url", 63 | str(anchor_url), 64 | "--anchor-data-hash", 65 | str(anchor_data_hash), 66 | ] 67 | return anchor_args 68 | 69 | def create_committee( 70 | self, 71 | vote_name: str, 72 | action_txid: str, 73 | action_ix: int, 74 | vote: consts.Votes, 75 | cc_hot_vkey: str = "", 76 | cc_hot_vkey_file: itp.FileType | None = None, 77 | cc_hot_key_hash: str = "", 78 | cc_hot_script_hash: str = "", 79 | anchor_url: str = "", 80 | anchor_data_hash: str = "", 81 | destination_dir: itp.FileType = ".", 82 | ) -> structs.VoteCC: 83 | """Create a governance action vote for a commitee member.""" 84 | destination_dir = pl.Path(destination_dir).expanduser() 85 | out_file = destination_dir / f"{vote_name}_cc.vote" 86 | clusterlib_helpers._check_files_exist(out_file, clusterlib_obj=self._clusterlib_obj) 87 | 88 | vote_args = self._get_vote_args(vote=vote) 89 | gov_action_args = self._get_gov_action_args(action_txid=action_txid, action_ix=action_ix) 90 | anchor_args = self._get_anchor_args( 91 | anchor_url=anchor_url, anchor_data_hash=anchor_data_hash 92 | ) 93 | 94 | if cc_hot_vkey: 95 | cred_args = ["--cc-hot-verification-key", cc_hot_vkey] 96 | elif cc_hot_vkey_file: 97 | cred_args = ["--cc-hot-verification-key-file", str(cc_hot_vkey_file)] 98 | elif cc_hot_key_hash: 99 | cred_args = ["--cc-hot-key-hash", cc_hot_key_hash] 100 | elif cc_hot_script_hash: 101 | cred_args = ["--cc-hot-script-hash", cc_hot_script_hash] 102 | else: 103 | msg = "No CC key or script hash was specified." 104 | raise ValueError(msg) 105 | 106 | self._clusterlib_obj.cli( 107 | [ 108 | *self._group_args, 109 | "create", 110 | *vote_args, 111 | *gov_action_args, 112 | *cred_args, 113 | *anchor_args, 114 | "--out-file", 115 | str(out_file), 116 | ] 117 | ) 118 | helpers._check_outfiles(out_file) 119 | 120 | vote_cc = structs.VoteCC( 121 | action_txid=action_txid, 122 | action_ix=action_ix, 123 | vote=vote, 124 | vote_file=out_file, 125 | cc_hot_vkey=cc_hot_vkey, 126 | cc_hot_vkey_file=helpers._maybe_path(cc_hot_vkey_file), 127 | cc_hot_key_hash=cc_hot_key_hash, 128 | cc_hot_script_hash=cc_hot_script_hash, 129 | anchor_url=anchor_url, 130 | anchor_data_hash=anchor_data_hash, 131 | ) 132 | return vote_cc 133 | 134 | def create_drep( 135 | self, 136 | vote_name: str, 137 | action_txid: str, 138 | action_ix: int, 139 | vote: consts.Votes, 140 | drep_vkey: str = "", 141 | drep_vkey_file: itp.FileType | None = None, 142 | drep_key_hash: str = "", 143 | drep_script_hash: str = "", 144 | anchor_url: str = "", 145 | anchor_data_hash: str = "", 146 | destination_dir: itp.FileType = ".", 147 | ) -> structs.VoteDrep: 148 | """Create a governance action vote for a DRep.""" 149 | destination_dir = pl.Path(destination_dir).expanduser() 150 | out_file = destination_dir / f"{vote_name}_drep.vote" 151 | clusterlib_helpers._check_files_exist(out_file, clusterlib_obj=self._clusterlib_obj) 152 | 153 | vote_args = self._get_vote_args(vote=vote) 154 | gov_action_args = self._get_gov_action_args(action_txid=action_txid, action_ix=action_ix) 155 | anchor_args = self._get_anchor_args( 156 | anchor_url=anchor_url, anchor_data_hash=anchor_data_hash 157 | ) 158 | 159 | if drep_vkey: 160 | cred_args = ["--drep-verification-key", drep_vkey] 161 | elif drep_vkey_file: 162 | cred_args = ["--drep-verification-key-file", str(drep_vkey_file)] 163 | elif drep_key_hash: 164 | cred_args = ["--drep-key-hash", drep_key_hash] 165 | elif drep_script_hash: 166 | cred_args = ["--drep-script-hash", drep_script_hash] 167 | else: 168 | msg = "No DRep key or script hash was specified." 169 | raise ValueError(msg) 170 | 171 | self._clusterlib_obj.cli( 172 | [ 173 | *self._group_args, 174 | "create", 175 | *vote_args, 176 | *gov_action_args, 177 | *cred_args, 178 | *anchor_args, 179 | "--out-file", 180 | str(out_file), 181 | ] 182 | ) 183 | helpers._check_outfiles(out_file) 184 | 185 | vote_drep = structs.VoteDrep( 186 | action_txid=action_txid, 187 | action_ix=action_ix, 188 | vote=vote, 189 | vote_file=out_file, 190 | drep_vkey=drep_vkey, 191 | drep_vkey_file=helpers._maybe_path(drep_vkey_file), 192 | drep_key_hash=drep_key_hash, 193 | drep_script_hash=drep_script_hash, 194 | anchor_url=anchor_url, 195 | anchor_data_hash=anchor_data_hash, 196 | ) 197 | return vote_drep 198 | 199 | def create_spo( 200 | self, 201 | vote_name: str, 202 | action_txid: str, 203 | action_ix: int, 204 | vote: consts.Votes, 205 | stake_pool_vkey: str = "", 206 | cold_vkey_file: itp.FileType | None = None, 207 | stake_pool_id: str = "", 208 | anchor_url: str = "", 209 | anchor_data_hash: str = "", 210 | destination_dir: itp.FileType = ".", 211 | ) -> structs.VoteSPO: 212 | """Create a governance action vote for an SPO.""" 213 | destination_dir = pl.Path(destination_dir).expanduser() 214 | out_file = destination_dir / f"{vote_name}_spo.vote" 215 | clusterlib_helpers._check_files_exist(out_file, clusterlib_obj=self._clusterlib_obj) 216 | 217 | vote_args = self._get_vote_args(vote=vote) 218 | gov_action_args = self._get_gov_action_args(action_txid=action_txid, action_ix=action_ix) 219 | anchor_args = self._get_anchor_args( 220 | anchor_url=anchor_url, anchor_data_hash=anchor_data_hash 221 | ) 222 | 223 | if stake_pool_vkey: 224 | key_args = ["--stake-pool-verification-key", stake_pool_vkey] 225 | elif cold_vkey_file: 226 | key_args = ["--cold-verification-key-file", str(cold_vkey_file)] 227 | elif stake_pool_id: 228 | key_args = ["--stake-pool-id", stake_pool_id] 229 | else: 230 | msg = "No key was specified." 231 | raise ValueError(msg) 232 | 233 | self._clusterlib_obj.cli( 234 | [ 235 | *self._group_args, 236 | "create", 237 | *vote_args, 238 | *gov_action_args, 239 | *key_args, 240 | *anchor_args, 241 | "--out-file", 242 | str(out_file), 243 | ] 244 | ) 245 | helpers._check_outfiles(out_file) 246 | 247 | vote_drep = structs.VoteSPO( 248 | action_txid=action_txid, 249 | action_ix=action_ix, 250 | vote=vote, 251 | stake_pool_vkey=stake_pool_vkey, 252 | cold_vkey_file=helpers._maybe_path(cold_vkey_file), 253 | stake_pool_id=stake_pool_id, 254 | vote_file=out_file, 255 | anchor_url=anchor_url, 256 | anchor_data_hash=anchor_data_hash, 257 | ) 258 | return vote_drep 259 | 260 | def view(self, vote_file: itp.FileType) -> dict[str, tp.Any]: 261 | """View a governance action vote.""" 262 | vote_file = pl.Path(vote_file).expanduser() 263 | clusterlib_helpers._check_files_exist(vote_file, clusterlib_obj=self._clusterlib_obj) 264 | 265 | stdout = self._clusterlib_obj.cli( 266 | [ 267 | *self._group_args, 268 | "view", 269 | "--vote-file", 270 | str(vote_file), 271 | ] 272 | ).stdout.strip() 273 | stdout_dec = stdout.decode("utf-8") if stdout else "" 274 | 275 | out: dict[str, tp.Any] = json.loads(stdout_dec) 276 | return out 277 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | 2 | Apache License 3 | Version 2.0, January 2004 4 | http://www.apache.org/licenses/ 5 | 6 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 7 | 8 | 1. Definitions. 9 | 10 | "License" shall mean the terms and conditions for use, reproduction, 11 | and distribution as defined by Sections 1 through 9 of this document. 12 | 13 | "Licensor" shall mean the copyright owner or entity authorized by 14 | the copyright owner that is granting the License. 15 | 16 | "Legal Entity" shall mean the union of the acting entity and all 17 | other entities that control, are controlled by, or are under common 18 | control with that entity. For the purposes of this definition, 19 | "control" means (i) the power, direct or indirect, to cause the 20 | direction or management of such entity, whether by contract or 21 | otherwise, or (ii) ownership of fifty percent (50%) or more of the 22 | outstanding shares, or (iii) beneficial ownership of such entity. 23 | 24 | "You" (or "Your") shall mean an individual or Legal Entity 25 | exercising permissions granted by this License. 26 | 27 | "Source" form shall mean the preferred form for making modifications, 28 | including but not limited to software source code, documentation 29 | source, and configuration files. 30 | 31 | "Object" form shall mean any form resulting from mechanical 32 | transformation or translation of a Source form, including but 33 | not limited to compiled object code, generated documentation, 34 | and conversions to other media types. 35 | 36 | "Work" shall mean the work of authorship, whether in Source or 37 | Object form, made available under the License, as indicated by a 38 | copyright notice that is included in or attached to the work 39 | (an example is provided in the Appendix below). 40 | 41 | "Derivative Works" shall mean any work, whether in Source or Object 42 | form, that is based on (or derived from) the Work and for which the 43 | editorial revisions, annotations, elaborations, or other modifications 44 | represent, as a whole, an original work of authorship. For the purposes 45 | of this License, Derivative Works shall not include works that remain 46 | separable from, or merely link (or bind by name) to the interfaces of, 47 | the Work and Derivative Works thereof. 48 | 49 | "Contribution" shall mean any work of authorship, including 50 | the original version of the Work and any modifications or additions 51 | to that Work or Derivative Works thereof, that is intentionally 52 | submitted to Licensor for inclusion in the Work by the copyright owner 53 | or by an individual or Legal Entity authorized to submit on behalf of 54 | the copyright owner. For the purposes of this definition, "submitted" 55 | means any form of electronic, verbal, or written communication sent 56 | to the Licensor or its representatives, including but not limited to 57 | communication on electronic mailing lists, source code control systems, 58 | and issue tracking systems that are managed by, or on behalf of, the 59 | Licensor for the purpose of discussing and improving the Work, but 60 | excluding communication that is conspicuously marked or otherwise 61 | designated in writing by the copyright owner as "Not a Contribution." 62 | 63 | "Contributor" shall mean Licensor and any individual or Legal Entity 64 | on behalf of whom a Contribution has been received by Licensor and 65 | subsequently incorporated within the Work. 66 | 67 | 2. Grant of Copyright License. Subject to the terms and conditions of 68 | this License, each Contributor hereby grants to You a perpetual, 69 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 70 | copyright license to reproduce, prepare Derivative Works of, 71 | publicly display, publicly perform, sublicense, and distribute the 72 | Work and such Derivative Works in Source or Object form. 73 | 74 | 3. Grant of Patent License. Subject to the terms and conditions of 75 | this License, each Contributor hereby grants to You a perpetual, 76 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 77 | (except as stated in this section) patent license to make, have made, 78 | use, offer to sell, sell, import, and otherwise transfer the Work, 79 | where such license applies only to those patent claims licensable 80 | by such Contributor that are necessarily infringed by their 81 | Contribution(s) alone or by combination of their Contribution(s) 82 | with the Work to which such Contribution(s) was submitted. If You 83 | institute patent litigation against any entity (including a 84 | cross-claim or counterclaim in a lawsuit) alleging that the Work 85 | or a Contribution incorporated within the Work constitutes direct 86 | or contributory patent infringement, then any patent licenses 87 | granted to You under this License for that Work shall terminate 88 | as of the date such litigation is filed. 89 | 90 | 4. Redistribution. You may reproduce and distribute copies of the 91 | Work or Derivative Works thereof in any medium, with or without 92 | modifications, and in Source or Object form, provided that You 93 | meet the following conditions: 94 | 95 | (a) You must give any other recipients of the Work or 96 | Derivative Works a copy of this License; and 97 | 98 | (b) You must cause any modified files to carry prominent notices 99 | stating that You changed the files; and 100 | 101 | (c) You must retain, in the Source form of any Derivative Works 102 | that You distribute, all copyright, patent, trademark, and 103 | attribution notices from the Source form of the Work, 104 | excluding those notices that do not pertain to any part of 105 | the Derivative Works; and 106 | 107 | (d) If the Work includes a "NOTICE" text file as part of its 108 | distribution, then any Derivative Works that You distribute must 109 | include a readable copy of the attribution notices contained 110 | within such NOTICE file, excluding those notices that do not 111 | pertain to any part of the Derivative Works, in at least one 112 | of the following places: within a NOTICE text file distributed 113 | as part of the Derivative Works; within the Source form or 114 | documentation, if provided along with the Derivative Works; or, 115 | within a display generated by the Derivative Works, if and 116 | wherever such third-party notices normally appear. The contents 117 | of the NOTICE file are for informational purposes only and 118 | do not modify the License. You may add Your own attribution 119 | notices within Derivative Works that You distribute, alongside 120 | or as an addendum to the NOTICE text from the Work, provided 121 | that such additional attribution notices cannot be construed 122 | as modifying the License. 123 | 124 | You may add Your own copyright statement to Your modifications and 125 | may provide additional or different license terms and conditions 126 | for use, reproduction, or distribution of Your modifications, or 127 | for any such Derivative Works as a whole, provided Your use, 128 | reproduction, and distribution of the Work otherwise complies with 129 | the conditions stated in this License. 130 | 131 | 5. Submission of Contributions. Unless You explicitly state otherwise, 132 | any Contribution intentionally submitted for inclusion in the Work 133 | by You to the Licensor shall be under the terms and conditions of 134 | this License, without any additional terms or conditions. 135 | Notwithstanding the above, nothing herein shall supersede or modify 136 | the terms of any separate license agreement you may have executed 137 | with Licensor regarding such Contributions. 138 | 139 | 6. Trademarks. This License does not grant permission to use the trade 140 | names, trademarks, service marks, or product names of the Licensor, 141 | except as required for reasonable and customary use in describing the 142 | origin of the Work and reproducing the content of the NOTICE file. 143 | 144 | 7. Disclaimer of Warranty. Unless required by applicable law or 145 | agreed to in writing, Licensor provides the Work (and each 146 | Contributor provides its Contributions) on an "AS IS" BASIS, 147 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 148 | implied, including, without limitation, any warranties or conditions 149 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A 150 | PARTICULAR PURPOSE. You are solely responsible for determining the 151 | appropriateness of using or redistributing the Work and assume any 152 | risks associated with Your exercise of permissions under this License. 153 | 154 | 8. Limitation of Liability. In no event and under no legal theory, 155 | whether in tort (including negligence), contract, or otherwise, 156 | unless required by applicable law (such as deliberate and grossly 157 | negligent acts) or agreed to in writing, shall any Contributor be 158 | liable to You for damages, including any direct, indirect, special, 159 | incidental, or consequential damages of any character arising as a 160 | result of this License or out of the use or inability to use the 161 | Work (including but not limited to damages for loss of goodwill, 162 | work stoppage, computer failure or malfunction, or any and all 163 | other commercial damages or losses), even if such Contributor 164 | has been advised of the possibility of such damages. 165 | 166 | 9. Accepting Warranty or Additional Liability. While redistributing 167 | the Work or Derivative Works thereof, You may choose to offer, 168 | and charge a fee for, acceptance of support, warranty, indemnity, 169 | or other liability obligations and/or rights consistent with this 170 | License. However, in accepting such obligations, You may act only 171 | on Your own behalf and on Your sole responsibility, not on behalf 172 | of any other Contributor, and only if You agree to indemnify, 173 | defend, and hold each Contributor harmless for any liability 174 | incurred by, or claims asserted against, such Contributor by reason 175 | of your accepting any such warranty or additional liability. 176 | 177 | END OF TERMS AND CONDITIONS 178 | 179 | APPENDIX: How to apply the Apache License to your work. 180 | 181 | To apply the Apache License to your work, attach the following 182 | boilerplate notice, with the fields enclosed by brackets "[]" 183 | replaced with your own identifying information. (Don't include 184 | the brackets!) The text should be enclosed in the appropriate 185 | comment syntax for the file format. We also recommend that a 186 | file or class name and description of purpose be included on the 187 | same "printed page" as the copyright notice for easier 188 | identification within third-party archives. 189 | 190 | Copyright 2022 Input Output (Hong Kong) Ltd. 191 | 192 | Licensed under the Apache License, Version 2.0 (the "License"); 193 | you may not use this file except in compliance with the License. 194 | You may obtain a copy of the License at 195 | 196 | http://www.apache.org/licenses/LICENSE-2.0 197 | 198 | Unless required by applicable law or agreed to in writing, software 199 | distributed under the License is distributed on an "AS IS" BASIS, 200 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 201 | See the License for the specific language governing permissions and 202 | limitations under the License. 203 | -------------------------------------------------------------------------------- /cardano_clusterlib/gov_drep_group.py: -------------------------------------------------------------------------------- 1 | """Group of methods for Conway governance DRep commands.""" 2 | 3 | import logging 4 | import pathlib as pl 5 | 6 | from cardano_clusterlib import clusterlib_helpers 7 | from cardano_clusterlib import exceptions 8 | from cardano_clusterlib import helpers 9 | from cardano_clusterlib import structs 10 | from cardano_clusterlib import types as itp 11 | 12 | LOGGER = logging.getLogger(__name__) 13 | 14 | 15 | class GovDrepGroup: 16 | def __init__(self, clusterlib_obj: "itp.ClusterLib") -> None: 17 | self._clusterlib_obj = clusterlib_obj 18 | self._group_args = ("governance", "drep") 19 | self._has_output_hex_prop: bool | None = None 20 | 21 | @property 22 | def _has_output_hex(self) -> bool: 23 | """Check if `drep id` has a `--output-hex` option.""" 24 | if self._has_output_hex_prop is not None: 25 | return self._has_output_hex_prop 26 | 27 | err = "" 28 | try: 29 | self._clusterlib_obj.cli( 30 | ["cardano-cli", "conway", "governance", "drep", "id", "--output-hex"], 31 | add_default_args=False, 32 | ) 33 | except exceptions.CLIError as excp: 34 | err = str(excp) 35 | 36 | self._has_output_hex_prop = "Invalid option" not in err 37 | return self._has_output_hex_prop 38 | 39 | def _get_cred_args( 40 | self, 41 | drep_script_hash: str = "", 42 | drep_vkey: str = "", 43 | drep_vkey_file: itp.FileType | None = None, 44 | drep_key_hash: str = "", 45 | ) -> list[str]: 46 | """Get arguments for script or vkey credentials.""" 47 | if drep_script_hash: 48 | cred_args = ["--drep-script-hash", str(drep_script_hash)] 49 | elif drep_vkey: 50 | cred_args = ["--drep-verification-key", str(drep_vkey)] 51 | elif drep_vkey_file: 52 | cred_args = ["--drep-verification-key-file", str(drep_vkey_file)] 53 | elif drep_key_hash: 54 | cred_args = ["--drep-key-hash", str(drep_key_hash)] 55 | else: 56 | msg = ( 57 | "Either `script_hash`, `drep_vkey`, `drep_vkey_file` or `drep_key_hash` is needed." 58 | ) 59 | raise ValueError(msg) 60 | 61 | return cred_args 62 | 63 | def gen_key_pair(self, key_name: str, destination_dir: itp.FileType = ".") -> structs.KeyPair: 64 | """Generate DRep verification and signing keys. 65 | 66 | Args: 67 | key_name: A name of the key pair. 68 | destination_dir: A path to directory for storing artifacts (optional). 69 | 70 | Returns: 71 | structs.KeyPair: A data container containing the key pair. 72 | """ 73 | destination_dir = pl.Path(destination_dir).expanduser() 74 | vkey = destination_dir / f"{key_name}_drep.vkey" 75 | skey = destination_dir / f"{key_name}_drep.skey" 76 | clusterlib_helpers._check_files_exist(vkey, skey, clusterlib_obj=self._clusterlib_obj) 77 | 78 | self._clusterlib_obj.cli( 79 | [ 80 | *self._group_args, 81 | "key-gen", 82 | "--verification-key-file", 83 | str(vkey), 84 | "--signing-key-file", 85 | str(skey), 86 | ] 87 | ) 88 | 89 | helpers._check_outfiles(vkey, skey) 90 | return structs.KeyPair(vkey, skey) 91 | 92 | def get_id( 93 | self, 94 | drep_vkey: str = "", 95 | drep_vkey_file: itp.FileType | None = None, 96 | out_format: str = "", 97 | ) -> str: 98 | """Return a DRep id. 99 | 100 | Args: 101 | drep_vkey: DRep verification key (Bech32 or hex-encoded, optional). 102 | drep_vkey_file: A path to corresponding drep vkey file (optional). 103 | out_format: Output format (optional, bech32 by default). 104 | 105 | Returns: 106 | str: A generated DRep id. 107 | """ 108 | if drep_vkey: 109 | cli_args = ["--drep-verification-key", str(drep_vkey)] 110 | elif drep_vkey_file: 111 | cli_args = ["--drep-verification-key-file", str(drep_vkey_file)] 112 | else: 113 | msg = "Either `drep_vkey` or `drep_vkey_file` is needed." 114 | raise ValueError(msg) 115 | 116 | if out_format: 117 | if out_format not in ("hex", "bech32"): 118 | msg = f"Invalid output format: {out_format} (expected 'hex' or 'bech32')." 119 | raise ValueError(msg) 120 | if self._has_output_hex: 121 | cli_args.append(f"--output-{out_format}") 122 | else: 123 | cli_args.extend(["--output-format", str(out_format)]) 124 | 125 | drep_id = ( 126 | self._clusterlib_obj.cli( 127 | [ 128 | *self._group_args, 129 | "id", 130 | *cli_args, 131 | ] 132 | ) 133 | .stdout.strip() 134 | .decode("ascii") 135 | ) 136 | 137 | return drep_id 138 | 139 | def gen_registration_cert( 140 | self, 141 | cert_name: str, 142 | deposit_amt: int, 143 | drep_script_hash: str = "", 144 | drep_vkey: str = "", 145 | drep_vkey_file: itp.FileType | None = None, 146 | drep_key_hash: str = "", 147 | drep_metadata_url: str = "", 148 | drep_metadata_hash: str = "", 149 | destination_dir: itp.FileType = ".", 150 | ) -> pl.Path: 151 | """Generate a DRep registration certificate. 152 | 153 | Args: 154 | cert_name: A name of the cert. 155 | deposit_amt: A key registration deposit amount. 156 | drep_script_hash: DRep script hash (hex-encoded, optional). 157 | drep_vkey: DRep verification key (Bech32 or hex-encoded, optional). 158 | drep_vkey_file: Filepath of the DRep verification key (optional). 159 | drep_key_hash: DRep verification key hash 160 | (either Bech32-encoded or hex-encoded, optional). 161 | drep_metadata_url: URL to the metadata file (optional). 162 | drep_metadata_hash: Hash of the metadata file (optional). 163 | destination_dir: A path to directory for storing artifacts (optional). 164 | 165 | Returns: 166 | Path: A path to the generated certificate. 167 | """ 168 | destination_dir = pl.Path(destination_dir).expanduser() 169 | out_file = destination_dir / f"{cert_name}_drep_reg.cert" 170 | clusterlib_helpers._check_files_exist(out_file, clusterlib_obj=self._clusterlib_obj) 171 | 172 | cred_args = self._get_cred_args( 173 | drep_script_hash=drep_script_hash, 174 | drep_vkey=drep_vkey, 175 | drep_vkey_file=drep_vkey_file, 176 | drep_key_hash=drep_key_hash, 177 | ) 178 | 179 | metadata_args = [] 180 | if drep_metadata_url: 181 | metadata_args = [ 182 | "--drep-metadata-url", 183 | str(drep_metadata_url), 184 | "--drep-metadata-hash", 185 | str(drep_metadata_hash), 186 | ] 187 | 188 | self._clusterlib_obj.cli( 189 | [ 190 | *self._group_args, 191 | "registration-certificate", 192 | *cred_args, 193 | "--key-reg-deposit-amt", 194 | str(deposit_amt), 195 | *metadata_args, 196 | "--out-file", 197 | str(out_file), 198 | ] 199 | ) 200 | 201 | helpers._check_outfiles(out_file) 202 | return out_file 203 | 204 | def gen_update_cert( 205 | self, 206 | cert_name: str, 207 | deposit_amt: int, 208 | drep_vkey: str = "", 209 | drep_vkey_file: itp.FileType | None = None, 210 | drep_key_hash: str = "", 211 | drep_metadata_url: str = "", 212 | drep_metadata_hash: str = "", 213 | destination_dir: itp.FileType = ".", 214 | ) -> pl.Path: 215 | """Generate a DRep update certificate. 216 | 217 | Args: 218 | cert_name: A name of the cert. 219 | deposit_amt: A key registration deposit amount. 220 | drep_vkey: DRep verification key (Bech32 or hex-encoded, optional). 221 | drep_vkey_file: Filepath of the DRep verification key (optional). 222 | drep_key_hash: DRep verification key hash 223 | (either Bech32-encoded or hex-encoded, optional). 224 | drep_metadata_url: URL to the metadata file (optional). 225 | drep_metadata_hash: Hash of the metadata file (optional). 226 | destination_dir: A path to directory for storing artifacts (optional). 227 | 228 | Returns: 229 | Path: A path to the generated certificate. 230 | """ 231 | destination_dir = pl.Path(destination_dir).expanduser() 232 | out_file = destination_dir / f"{cert_name}_drep_update.cert" 233 | clusterlib_helpers._check_files_exist(out_file, clusterlib_obj=self._clusterlib_obj) 234 | 235 | cred_args = self._get_cred_args( 236 | drep_vkey=drep_vkey, 237 | drep_vkey_file=drep_vkey_file, 238 | drep_key_hash=drep_key_hash, 239 | ) 240 | 241 | metadata_args = [] 242 | if drep_metadata_url: 243 | metadata_args = [ 244 | "--drep-metadata-url", 245 | str(drep_metadata_url), 246 | "--drep-metadata-hash", 247 | str(drep_metadata_hash), 248 | ] 249 | 250 | self._clusterlib_obj.cli( 251 | [ 252 | *self._group_args, 253 | "update-certificate", 254 | *cred_args, 255 | "--key-reg-deposit-amt", 256 | str(deposit_amt), 257 | *metadata_args, 258 | "--out-file", 259 | str(out_file), 260 | ] 261 | ) 262 | 263 | helpers._check_outfiles(out_file) 264 | return out_file 265 | 266 | def gen_retirement_cert( 267 | self, 268 | cert_name: str, 269 | deposit_amt: int, 270 | drep_script_hash: str = "", 271 | drep_vkey: str = "", 272 | drep_vkey_file: itp.FileType | None = None, 273 | drep_key_hash: str = "", 274 | destination_dir: itp.FileType = ".", 275 | ) -> pl.Path: 276 | """Generate a DRep retirement certificate. 277 | 278 | Args: 279 | cert_name: A name of the cert. 280 | deposit_amt: A key registration deposit amount. 281 | drep_script_hash: DRep script hash (hex-encoded, optional). 282 | drep_vkey: DRep verification key (Bech32 or hex-encoded, optional). 283 | drep_vkey_file: Filepath of the DRep verification key (optional). 284 | drep_key_hash: DRep verification key hash 285 | (either Bech32-encoded or hex-encoded, optional). 286 | destination_dir: A path to directory for storing artifacts (optional). 287 | 288 | Returns: 289 | Path: A path to the generated certificate. 290 | """ 291 | destination_dir = pl.Path(destination_dir).expanduser() 292 | out_file = destination_dir / f"{cert_name}_drep_retirement.cert" 293 | clusterlib_helpers._check_files_exist(out_file, clusterlib_obj=self._clusterlib_obj) 294 | 295 | cred_args = self._get_cred_args( 296 | drep_script_hash=drep_script_hash, 297 | drep_vkey=drep_vkey, 298 | drep_vkey_file=drep_vkey_file, 299 | drep_key_hash=drep_key_hash, 300 | ) 301 | 302 | self._clusterlib_obj.cli( 303 | [ 304 | *self._group_args, 305 | "retirement-certificate", 306 | *cred_args, 307 | "--deposit-amt", 308 | str(deposit_amt), 309 | "--out-file", 310 | str(out_file), 311 | ] 312 | ) 313 | 314 | helpers._check_outfiles(out_file) 315 | return out_file 316 | 317 | def get_metadata_hash( 318 | self, 319 | drep_metadata_file: itp.FileType, 320 | ) -> str: 321 | """Get the hash of the metadata. 322 | 323 | Args: 324 | drep_metadata_file: A path to the metadata file. 325 | 326 | Returns: 327 | str: A hash of the metadata. 328 | """ 329 | drep_metadata_file = pl.Path(drep_metadata_file).expanduser() 330 | clusterlib_helpers._check_files_exist( 331 | drep_metadata_file, clusterlib_obj=self._clusterlib_obj 332 | ) 333 | 334 | metadata_hash = ( 335 | self._clusterlib_obj.cli( 336 | [ 337 | *self._group_args, 338 | "metadata-hash", 339 | "--drep-metadata-file", 340 | str(drep_metadata_file), 341 | ] 342 | ) 343 | .stdout.rstrip() 344 | .decode("ascii") 345 | ) 346 | 347 | return metadata_hash 348 | -------------------------------------------------------------------------------- /cardano_clusterlib/clusterlib_helpers.py: -------------------------------------------------------------------------------- 1 | """Helper functions for `ClusterLib`.""" 2 | 3 | import dataclasses 4 | import datetime 5 | import json 6 | import logging 7 | import pathlib as pl 8 | import re 9 | import time 10 | import typing as tp 11 | 12 | from cardano_clusterlib import exceptions 13 | from cardano_clusterlib import types as itp 14 | 15 | LOGGER = logging.getLogger(__name__) 16 | 17 | SPECIAL_ARG_CHARS_RE = re.compile("[^A-Za-z0-9/._-]") 18 | 19 | 20 | @dataclasses.dataclass(frozen=True, order=True) 21 | class EpochInfo: 22 | epoch: int 23 | first_slot: int 24 | last_slot: int 25 | 26 | 27 | def _find_genesis_json(clusterlib_obj: "itp.ClusterLib") -> pl.Path: 28 | """Find Shelley genesis JSON file in state dir.""" 29 | default = clusterlib_obj.state_dir / "shelley" / "genesis.json" 30 | if default.exists(): 31 | return default 32 | 33 | potential = [ 34 | *clusterlib_obj.state_dir.glob("*shelley*genesis.json"), 35 | *clusterlib_obj.state_dir.glob("*genesis*shelley.json"), 36 | ] 37 | if not potential: 38 | msg = f"Shelley genesis JSON file not found in `{clusterlib_obj.state_dir}`." 39 | raise exceptions.CLIError(msg) 40 | 41 | genesis_json = potential[0] 42 | LOGGER.debug(f"Using shelley genesis JSON file `{genesis_json}") 43 | return genesis_json 44 | 45 | 46 | def _find_conway_genesis_json(clusterlib_obj: "itp.ClusterLib") -> pl.Path: 47 | """Find Conway genesis JSON file in state dir.""" 48 | default = clusterlib_obj.state_dir / "shelley" / "genesis.conway.json" 49 | if default.exists(): 50 | return default 51 | 52 | potential = [ 53 | *clusterlib_obj.state_dir.glob("*conway*genesis.json"), 54 | *clusterlib_obj.state_dir.glob("*genesis*conway.json"), 55 | ] 56 | if not potential: 57 | msg = f"Conway genesis JSON file not found in `{clusterlib_obj.state_dir}`." 58 | raise exceptions.CLIError(msg) 59 | 60 | genesis_json = potential[0] 61 | LOGGER.debug(f"Using Conway genesis JSON file `{genesis_json}") 62 | return genesis_json 63 | 64 | 65 | def _check_files_exist(*out_files: itp.FileType, clusterlib_obj: "itp.ClusterLib") -> None: 66 | """Check that the output files don't already exist. 67 | 68 | Args: 69 | *out_files: Variable length list of expected output files. 70 | clusterlib_obj: An instance of `ClusterLib`. 71 | """ 72 | if clusterlib_obj.overwrite_outfiles: 73 | return 74 | 75 | for out_file in out_files: 76 | out_file_p = pl.Path(out_file).expanduser() 77 | if out_file_p.exists(): 78 | msg = f"The expected file `{out_file}` already exist." 79 | raise exceptions.CLIError(msg) 80 | 81 | 82 | def _format_cli_args(cli_args: list[str]) -> str: 83 | """Format CLI arguments for logging. 84 | 85 | Quote arguments with spaces and other "special" characters in them. 86 | 87 | Args: 88 | cli_args: List of CLI arguments. 89 | """ 90 | processed_args = [] 91 | for arg in cli_args: 92 | arg_p = f'"{arg}"' if SPECIAL_ARG_CHARS_RE.search(arg) else arg 93 | processed_args.append(arg_p) 94 | return " ".join(processed_args) 95 | 96 | 97 | def _write_cli_log(clusterlib_obj: "itp.ClusterLib", command: str) -> None: 98 | if not clusterlib_obj._cli_log: 99 | return 100 | 101 | with open(clusterlib_obj._cli_log, "a", encoding="utf-8") as logfile: 102 | logfile.write(f"{datetime.datetime.now(tz=datetime.timezone.utc)}: {command}\n") 103 | 104 | 105 | def _get_kes_period_info(kes_info: str) -> dict[str, tp.Any]: 106 | """Process the output of the `kes-period-info` command. 107 | 108 | Args: 109 | kes_info: The output of the `kes-period-info` command. 110 | """ 111 | messages_str = kes_info.split("{")[0] 112 | messages_list = [] 113 | 114 | valid_counters = False 115 | valid_kes_period = False 116 | 117 | if messages_str: 118 | message_entry: list = [] 119 | 120 | for line in messages_str.split("\n"): 121 | line_s = line.strip() 122 | if not line_s: 123 | continue 124 | if not message_entry or line_s[0].isalpha(): 125 | message_entry.append(line_s) 126 | else: 127 | messages_list.append(" ".join(message_entry)) 128 | message_entry = [line_s] 129 | 130 | messages_list.append(" ".join(message_entry)) 131 | 132 | for out_message in messages_list: 133 | if ( 134 | "counter agrees with" in out_message 135 | or "counter ahead of the node protocol state counter by 1" in out_message 136 | ): 137 | valid_counters = True 138 | elif "correct KES period interval" in out_message: 139 | valid_kes_period = True 140 | 141 | # Get output metrics 142 | metrics_str = kes_info.split("{")[-1] 143 | metrics_dict = {} 144 | 145 | if metrics_str and metrics_str.strip().endswith("}"): 146 | metrics_dict = json.loads(f"{{{metrics_str}") 147 | 148 | output_dict = { 149 | "messages": messages_list, 150 | "metrics": metrics_dict, 151 | "valid_counters": valid_counters, 152 | "valid_kes_period": valid_kes_period, 153 | } 154 | 155 | return output_dict 156 | 157 | 158 | def get_epoch_for_slot(cluster_obj: "itp.ClusterLib", slot_no: int) -> EpochInfo: 159 | """Given slot number, return corresponding epoch number and first and last slot of the epoch.""" 160 | genesis_byron = cluster_obj.state_dir / "byron" / "genesis.json" 161 | if not genesis_byron.exists(): 162 | msg = f"File '{genesis_byron}' does not exist." 163 | raise FileNotFoundError(msg) 164 | 165 | with open(genesis_byron, encoding="utf-8") as in_json: 166 | byron_dict = json.load(in_json) 167 | 168 | byron_k = int(byron_dict["protocolConsts"]["k"]) 169 | slots_in_byron_epoch = byron_k * 10 170 | slots_per_epoch_diff = cluster_obj.epoch_length - slots_in_byron_epoch 171 | num_byron_epochs = cluster_obj.slots_offset // slots_per_epoch_diff 172 | slots_in_byron = num_byron_epochs * slots_in_byron_epoch 173 | 174 | # Slot is in Byron era 175 | if slot_no < slots_in_byron: 176 | epoch_no = slot_no // slots_in_byron_epoch 177 | first_slot_in_epoch = epoch_no * slots_in_byron_epoch 178 | last_slot_in_epoch = first_slot_in_epoch + slots_in_byron_epoch - 1 179 | # Slot is in Shelley-based era 180 | else: 181 | slot_no_shelley = slot_no + cluster_obj.slots_offset 182 | epoch_no = slot_no_shelley // cluster_obj.epoch_length 183 | first_slot_in_epoch = epoch_no * cluster_obj.epoch_length - cluster_obj.slots_offset 184 | last_slot_in_epoch = first_slot_in_epoch + cluster_obj.epoch_length - 1 185 | 186 | return EpochInfo(epoch=epoch_no, first_slot=first_slot_in_epoch, last_slot=last_slot_in_epoch) 187 | 188 | 189 | def wait_for_block(clusterlib_obj: "itp.ClusterLib", tip: dict[str, tp.Any], block_no: int) -> int: 190 | """Wait for block number. 191 | 192 | Args: 193 | clusterlib_obj: An instance of `ClusterLib`. 194 | tip: Current tip - last block successfully applied to the ledger. 195 | block_no: A block number to wait for. 196 | 197 | Returns: 198 | int: A block number of last added block. 199 | """ 200 | initial_block = int(tip["block"]) 201 | initial_slot = int(tip["slot"]) 202 | 203 | if initial_block >= block_no: 204 | return initial_block 205 | 206 | next_block_timeout = 300 # in slots 207 | max_tip_throttle = 5 * clusterlib_obj.slot_length 208 | 209 | new_blocks = block_no - initial_block 210 | 211 | LOGGER.debug(f"Waiting for {new_blocks} new block(s) to be created.") 212 | LOGGER.debug(f"Initial block no: {initial_block}") 213 | 214 | this_slot = initial_slot 215 | this_block = initial_block 216 | timeout_slot = initial_slot + next_block_timeout 217 | blocks_to_go = new_blocks 218 | # Limit calls to `query tip` 219 | tip_throttle = 0 220 | 221 | while this_slot < timeout_slot: 222 | prev_block = this_block 223 | time.sleep((clusterlib_obj.slot_length * blocks_to_go) + tip_throttle) 224 | 225 | this_tip = clusterlib_obj.g_query.get_tip() 226 | this_slot = int(this_tip["slot"]) 227 | this_block = int(this_tip["block"]) 228 | 229 | if this_block >= block_no: 230 | break 231 | if this_block > prev_block: 232 | # New block was created, reset timeout slot 233 | timeout_slot = this_slot + next_block_timeout 234 | 235 | blocks_to_go = block_no - this_block 236 | tip_throttle = min(max_tip_throttle, tip_throttle + clusterlib_obj.slot_length) 237 | else: 238 | waited_sec = (this_slot - initial_slot) * clusterlib_obj.slot_length 239 | msg = f"Timeout waiting for {waited_sec} sec for {new_blocks} block(s)." 240 | raise exceptions.CLIError(msg) 241 | 242 | LOGGER.debug(f"New block(s) were created; block number: {this_block}") 243 | return this_block 244 | 245 | 246 | def poll_new_epoch( 247 | clusterlib_obj: "itp.ClusterLib", 248 | exp_epoch: int, 249 | padding_seconds: int = 0, 250 | ) -> None: 251 | """Wait for new epoch(s) by polling current epoch every 3 sec. 252 | 253 | Can be used only for waiting up to 3000 sec + padding seconds. 254 | 255 | Args: 256 | clusterlib_obj: An instance of `ClusterLib`. 257 | tip: Current tip - last block successfully applied to the ledger. 258 | exp_epoch: An epoch number to wait for. 259 | padding_seconds: A number of additional seconds to wait for (optional). 260 | """ 261 | for check_no in range(1000): 262 | wakeup_epoch = clusterlib_obj.g_query.get_epoch() 263 | if wakeup_epoch != exp_epoch: 264 | time.sleep(3) 265 | continue 266 | # We are in the expected epoch right from the beginning, we'll skip padding seconds 267 | if check_no == 0: 268 | break 269 | if padding_seconds: 270 | time.sleep(padding_seconds) 271 | break 272 | 273 | 274 | def wait_for_epoch( 275 | clusterlib_obj: "itp.ClusterLib", 276 | tip: dict[str, tp.Any], 277 | epoch_no: int, 278 | padding_seconds: int = 0, 279 | future_is_ok: bool = True, 280 | ) -> int: 281 | """Wait for epoch no. 282 | 283 | Args: 284 | clusterlib_obj: An instance of `ClusterLib`. 285 | tip: Current tip - last block successfully applied to the ledger. 286 | epoch_no: A number of epoch to wait for. 287 | padding_seconds: A number of additional seconds to wait for (optional). 288 | future_is_ok: A bool indicating whether current epoch > `epoch_no` is acceptable 289 | (default: True). 290 | 291 | Returns: 292 | int: The current epoch. 293 | """ 294 | start_epoch = int(tip["epoch"]) 295 | 296 | if epoch_no < start_epoch: 297 | if not future_is_ok: 298 | msg = f"Current epoch is {start_epoch}. The requested epoch {epoch_no} is in the past." 299 | raise exceptions.CLIError(msg) 300 | return start_epoch 301 | 302 | LOGGER.debug(f"Current epoch: {start_epoch}; Waiting for the beginning of epoch: {epoch_no}") 303 | 304 | new_epochs = epoch_no - start_epoch 305 | 306 | # Calculate and wait for the expected slot 307 | boundary_slot = int( 308 | (start_epoch + new_epochs) * clusterlib_obj.epoch_length - clusterlib_obj.slots_offset 309 | ) 310 | padding_slots = int(padding_seconds / clusterlib_obj.slot_length) if padding_seconds else 5 311 | exp_slot = boundary_slot + padding_slots 312 | clusterlib_obj.wait_for_slot(slot=exp_slot) 313 | 314 | this_epoch = clusterlib_obj.g_query.get_epoch() 315 | if this_epoch != epoch_no: 316 | LOGGER.error( 317 | f"Waited for epoch number {epoch_no} and current epoch is " 318 | f"number {this_epoch}, wrong `slots_offset` ({clusterlib_obj.slots_offset})?" 319 | ) 320 | # Attempt to get the epoch boundary as precisely as possible failed, now just 321 | # query epoch number and wait 322 | poll_new_epoch( 323 | clusterlib_obj=clusterlib_obj, exp_epoch=epoch_no, padding_seconds=padding_seconds 324 | ) 325 | 326 | # Still not in the correct epoch? Something is wrong. 327 | this_epoch = clusterlib_obj.g_query.get_epoch() 328 | if this_epoch != epoch_no: 329 | msg = f"Waited for epoch number {epoch_no} and current epoch is number {this_epoch}." 330 | raise exceptions.CLIError(msg) 331 | 332 | LOGGER.debug(f"Expected epoch started; epoch number: {this_epoch}") 333 | return this_epoch 334 | 335 | 336 | def get_slots_offset(clusterlib_obj: "itp.ClusterLib") -> int: 337 | """Get offset of slots from Byron era vs current configuration.""" 338 | tip = clusterlib_obj.g_query.get_tip() 339 | slot = int(tip["slot"]) 340 | slots_ep_end = int(tip["slotsToEpochEnd"]) 341 | epoch = int(tip["epoch"]) 342 | 343 | slots_total = slot + slots_ep_end 344 | slots_shelley = int(clusterlib_obj.epoch_length) * (epoch + 1) 345 | 346 | offset = slots_shelley - slots_total 347 | return offset 348 | -------------------------------------------------------------------------------- /cardano_clusterlib/stake_pool_group.py: -------------------------------------------------------------------------------- 1 | """Group of methods for working with stake pools.""" 2 | 3 | import logging 4 | import pathlib as pl 5 | 6 | from cardano_clusterlib import clusterlib_helpers 7 | from cardano_clusterlib import helpers 8 | from cardano_clusterlib import structs 9 | from cardano_clusterlib import types as itp 10 | 11 | LOGGER = logging.getLogger(__name__) 12 | 13 | 14 | class StakePoolGroup: 15 | def __init__(self, clusterlib_obj: "itp.ClusterLib") -> None: 16 | self._clusterlib_obj = clusterlib_obj 17 | 18 | def gen_pool_metadata_hash(self, pool_metadata_file: itp.FileType) -> str: 19 | """Generate the hash of pool metadata. 20 | 21 | Args: 22 | pool_metadata_file: A path to the pool metadata file. 23 | 24 | Returns: 25 | str: A metadata hash. 26 | """ 27 | return ( 28 | self._clusterlib_obj.cli( 29 | ["stake-pool", "metadata-hash", "--pool-metadata-file", str(pool_metadata_file)] 30 | ) 31 | .stdout.rstrip() 32 | .decode("ascii") 33 | ) 34 | 35 | def gen_pool_registration_cert( 36 | self, 37 | pool_data: structs.PoolData, 38 | vrf_vkey_file: itp.FileType, 39 | cold_vkey_file: itp.FileType, 40 | owner_stake_vkey_files: itp.FileTypeList, 41 | reward_account_vkey_file: itp.FileType | None = None, 42 | destination_dir: itp.FileType = ".", 43 | ) -> pl.Path: 44 | """Generate a stake pool registration certificate. 45 | 46 | Args: 47 | pool_data: A `structs.PoolData` data container containing info about the stake pool. 48 | vrf_vkey_file: A path to node VRF vkey file. 49 | cold_vkey_file: A path to pool cold vkey file. 50 | owner_stake_vkey_files: A list of paths to pool owner stake vkey files. 51 | reward_account_vkey_file: A path to pool reward account vkey file (optional). 52 | destination_dir: A path to directory for storing artifacts (optional). 53 | 54 | Returns: 55 | Path: A path to the generated certificate. 56 | """ 57 | destination_dir = pl.Path(destination_dir).expanduser() 58 | out_file = destination_dir / f"{pool_data.pool_name}_pool_reg.cert" 59 | clusterlib_helpers._check_files_exist(out_file, clusterlib_obj=self._clusterlib_obj) 60 | 61 | metadata_cmd = [] 62 | if pool_data.pool_metadata_url and pool_data.pool_metadata_hash: 63 | metadata_cmd = [ 64 | "--metadata-url", 65 | str(pool_data.pool_metadata_url), 66 | "--metadata-hash", 67 | str(pool_data.pool_metadata_hash), 68 | ] 69 | 70 | relay_cmd = [] 71 | if pool_data.pool_relay_dns: 72 | relay_cmd.extend(["--single-host-pool-relay", pool_data.pool_relay_dns]) 73 | if pool_data.pool_relay_ipv4: 74 | relay_cmd.extend(["--pool-relay-ipv4", pool_data.pool_relay_ipv4]) 75 | if pool_data.pool_relay_port: 76 | relay_cmd.extend(["--pool-relay-port", str(pool_data.pool_relay_port)]) 77 | 78 | self._clusterlib_obj.cli( 79 | [ 80 | "stake-pool", 81 | "registration-certificate", 82 | "--pool-pledge", 83 | str(pool_data.pool_pledge), 84 | "--pool-cost", 85 | str(pool_data.pool_cost), 86 | "--pool-margin", 87 | str(pool_data.pool_margin), 88 | "--vrf-verification-key-file", 89 | str(vrf_vkey_file), 90 | "--cold-verification-key-file", 91 | str(cold_vkey_file), 92 | "--pool-reward-account-verification-key-file", 93 | str(reward_account_vkey_file) 94 | if reward_account_vkey_file 95 | else str(next(iter(owner_stake_vkey_files))), 96 | *helpers._prepend_flag( 97 | "--pool-owner-stake-verification-key-file", owner_stake_vkey_files 98 | ), 99 | *self._clusterlib_obj.magic_args, 100 | "--out-file", 101 | str(out_file), 102 | *metadata_cmd, 103 | *relay_cmd, 104 | ] 105 | ) 106 | 107 | helpers._check_outfiles(out_file) 108 | return out_file 109 | 110 | def gen_pool_deregistration_cert( 111 | self, 112 | pool_name: str, 113 | cold_vkey_file: itp.FileType, 114 | epoch: int, 115 | destination_dir: itp.FileType = ".", 116 | ) -> pl.Path: 117 | """Generate a stake pool deregistration certificate. 118 | 119 | Args: 120 | pool_name: A name of the stake pool. 121 | cold_vkey_file: A path to pool cold vkey file. 122 | epoch: An epoch where the pool will be deregistered. 123 | destination_dir: A path to directory for storing artifacts (optional). 124 | 125 | Returns: 126 | Path: A path to the generated certificate. 127 | """ 128 | destination_dir = pl.Path(destination_dir).expanduser() 129 | out_file = destination_dir / f"{pool_name}_pool_dereg.cert" 130 | clusterlib_helpers._check_files_exist(out_file, clusterlib_obj=self._clusterlib_obj) 131 | 132 | self._clusterlib_obj.cli( 133 | [ 134 | "stake-pool", 135 | "deregistration-certificate", 136 | "--cold-verification-key-file", 137 | str(cold_vkey_file), 138 | "--epoch", 139 | str(epoch), 140 | "--out-file", 141 | str(out_file), 142 | ] 143 | ) 144 | 145 | helpers._check_outfiles(out_file) 146 | return out_file 147 | 148 | def get_stake_pool_id( 149 | self, 150 | cold_vkey_file: itp.FileType | None = None, 151 | stake_pool_vkey: str = "", 152 | ) -> str: 153 | """Return pool ID from the offline key. 154 | 155 | Args: 156 | cold_vkey_file: A path to pool cold vkey file. 157 | stake_pool_vkey: A stake pool verification key (Bech32 or hex-encoded). 158 | 159 | Returns: 160 | str: A pool ID. 161 | """ 162 | if stake_pool_vkey: 163 | key_args = ["--stake-pool-verification-key", str(stake_pool_vkey)] 164 | elif cold_vkey_file: 165 | key_args = ["--cold-verification-key-file", str(cold_vkey_file)] 166 | else: 167 | msg = "No key was specified." 168 | raise ValueError(msg) 169 | 170 | pool_id = ( 171 | self._clusterlib_obj.cli(["stake-pool", "id", *key_args]).stdout.strip().decode("ascii") 172 | ) 173 | return pool_id 174 | 175 | def create_stake_pool( 176 | self, 177 | pool_data: structs.PoolData, 178 | pool_owners: list[structs.PoolUser], 179 | tx_name: str, 180 | reward_account_key_pair: structs.KeyPair | structs.AddressRecord | None = None, 181 | destination_dir: itp.FileType = ".", 182 | ) -> structs.PoolCreationOutput: 183 | """Create and register a stake pool. 184 | 185 | Args: 186 | pool_data: A `structs.PoolData` data container containing info about the stake pool. 187 | pool_owners: A list of `structs.PoolUser` structures containing pool user addresses 188 | and keys. 189 | tx_name: A name of the transaction. 190 | reward_account_key_pair: A data container containing reward account key pair (optional). 191 | destination_dir: A path to directory for storing artifacts (optional). 192 | 193 | Returns: 194 | structs.PoolCreationOutput: A data container containing pool creation output. 195 | """ 196 | # Create the KES key pair 197 | node_kes = self._clusterlib_obj.g_node.gen_kes_key_pair( 198 | node_name=pool_data.pool_name, 199 | destination_dir=destination_dir, 200 | ) 201 | LOGGER.debug(f"KES keys created - {node_kes.vkey_file}; {node_kes.skey_file}") 202 | 203 | # Create the VRF key pair 204 | node_vrf = self._clusterlib_obj.g_node.gen_vrf_key_pair( 205 | node_name=pool_data.pool_name, 206 | destination_dir=destination_dir, 207 | ) 208 | LOGGER.debug(f"VRF keys created - {node_vrf.vkey_file}; {node_vrf.skey_file}") 209 | 210 | # Create the cold key pair and node operational certificate counter 211 | node_cold = self._clusterlib_obj.g_node.gen_cold_key_pair_and_counter( 212 | node_name=pool_data.pool_name, 213 | destination_dir=destination_dir, 214 | ) 215 | LOGGER.debug( 216 | "Cold keys created and counter created - " 217 | f"{node_cold.vkey_file}; {node_cold.skey_file}; {node_cold.counter_file}" 218 | ) 219 | 220 | pool_reg_cert_file, tx_raw_output = self.register_stake_pool( 221 | pool_data=pool_data, 222 | pool_owners=pool_owners, 223 | vrf_vkey_file=node_vrf.vkey_file, 224 | cold_key_pair=node_cold, 225 | tx_name=tx_name, 226 | reward_account_vkey_file=reward_account_key_pair.vkey_file 227 | if reward_account_key_pair 228 | else None, 229 | destination_dir=destination_dir, 230 | ) 231 | 232 | return structs.PoolCreationOutput( 233 | stake_pool_id=self.get_stake_pool_id(node_cold.vkey_file), 234 | vrf_key_pair=node_vrf, 235 | cold_key_pair=node_cold, 236 | pool_reg_cert_file=pool_reg_cert_file, 237 | pool_data=pool_data, 238 | pool_owners=pool_owners, 239 | reward_account_key_pair=reward_account_key_pair or pool_owners[0].stake, 240 | tx_raw_output=tx_raw_output, 241 | kes_key_pair=node_kes, 242 | ) 243 | 244 | def register_stake_pool( 245 | self, 246 | pool_data: structs.PoolData, 247 | pool_owners: list[structs.PoolUser], 248 | vrf_vkey_file: itp.FileType, 249 | cold_key_pair: structs.ColdKeyPair, 250 | tx_name: str, 251 | reward_account_vkey_file: itp.FileType | None = None, 252 | deposit: int | None = None, 253 | destination_dir: itp.FileType = ".", 254 | ) -> tuple[pl.Path, structs.TxRawOutput]: 255 | """Register a stake pool. 256 | 257 | Args: 258 | pool_data: A `structs.PoolData` data container containing info about the stake pool. 259 | pool_owners: A list of `structs.PoolUser` structures containing pool user addresses 260 | and keys. 261 | vrf_vkey_file: A path to node VRF vkey file. 262 | cold_key_pair: A `structs.ColdKeyPair` data container containing the key pair 263 | and the counter. 264 | tx_name: A name of the transaction. 265 | reward_account_vkey_file: A path to reward account vkey file (optional). 266 | deposit: A deposit amount needed by the transaction (optional). 267 | destination_dir: A path to directory for storing artifacts (optional). 268 | 269 | Returns: 270 | tuple[Path, structs.TxRawOutput]: A tuple with pool registration cert file and 271 | transaction output details. 272 | """ 273 | tx_name = f"{tx_name}_reg_pool" 274 | pool_reg_cert_file = self.gen_pool_registration_cert( 275 | pool_data=pool_data, 276 | vrf_vkey_file=vrf_vkey_file, 277 | cold_vkey_file=cold_key_pair.vkey_file, 278 | owner_stake_vkey_files=[p.stake.vkey_file for p in pool_owners], 279 | reward_account_vkey_file=reward_account_vkey_file, 280 | destination_dir=destination_dir, 281 | ) 282 | 283 | # Submit the pool registration certificate through a tx 284 | tx_files = structs.TxFiles( 285 | certificate_files=[pool_reg_cert_file], 286 | signing_key_files=[ 287 | *[p.payment.skey_file for p in pool_owners], 288 | *[p.stake.skey_file for p in pool_owners], 289 | cold_key_pair.skey_file, 290 | ], 291 | ) 292 | 293 | tx_raw_output = self._clusterlib_obj.g_transaction.send_tx( 294 | src_address=pool_owners[0].payment.address, 295 | tx_name=tx_name, 296 | tx_files=tx_files, 297 | deposit=deposit, 298 | destination_dir=destination_dir, 299 | ) 300 | 301 | return pool_reg_cert_file, tx_raw_output 302 | 303 | def deregister_stake_pool( 304 | self, 305 | pool_owners: list[structs.PoolUser], 306 | cold_key_pair: structs.ColdKeyPair, 307 | epoch: int, 308 | pool_name: str, 309 | tx_name: str, 310 | destination_dir: itp.FileType = ".", 311 | ) -> tuple[pl.Path, structs.TxRawOutput]: 312 | """Deregister a stake pool. 313 | 314 | Args: 315 | pool_owners: A list of `structs.PoolUser` structures containing pool user addresses 316 | and keys. 317 | cold_key_pair: A `structs.ColdKeyPair` data container containing the key pair 318 | and the counter. 319 | epoch: An epoch where the update proposal will take effect (optional). 320 | pool_name: A name of the stake pool. 321 | tx_name: A name of the transaction. 322 | destination_dir: A path to directory for storing artifacts (optional). 323 | 324 | Returns: 325 | tuple[Path, structs.TxRawOutput]: A data container with pool registration cert file and 326 | transaction output details. 327 | """ 328 | tx_name = f"{tx_name}_dereg_pool" 329 | LOGGER.debug( 330 | f"Deregistering stake pool starting with epoch: {epoch}; " 331 | f"Current epoch is: {self._clusterlib_obj.g_query.get_epoch()}" 332 | ) 333 | pool_dereg_cert_file = self.gen_pool_deregistration_cert( 334 | pool_name=pool_name, 335 | cold_vkey_file=cold_key_pair.vkey_file, 336 | epoch=epoch, 337 | destination_dir=destination_dir, 338 | ) 339 | 340 | # Submit the pool deregistration certificate through a tx 341 | tx_files = structs.TxFiles( 342 | certificate_files=[pool_dereg_cert_file], 343 | signing_key_files=[ 344 | *[p.payment.skey_file for p in pool_owners], 345 | *[p.stake.skey_file for p in pool_owners], 346 | cold_key_pair.skey_file, 347 | ], 348 | ) 349 | 350 | tx_raw_output = self._clusterlib_obj.g_transaction.send_tx( 351 | src_address=pool_owners[0].payment.address, 352 | tx_name=tx_name, 353 | tx_files=tx_files, 354 | destination_dir=destination_dir, 355 | ) 356 | 357 | return pool_dereg_cert_file, tx_raw_output 358 | 359 | def __repr__(self) -> str: 360 | return f"<{self.__class__.__name__}: clusterlib_obj={id(self._clusterlib_obj)}>" 361 | -------------------------------------------------------------------------------- /cardano_clusterlib/structs.py: -------------------------------------------------------------------------------- 1 | import dataclasses 2 | import datetime 3 | import pathlib as pl 4 | import typing as tp 5 | 6 | from cardano_clusterlib import consts 7 | from cardano_clusterlib import types as itp 8 | 9 | 10 | @dataclasses.dataclass(frozen=True) 11 | class CLIOut: 12 | stdout: bytes 13 | stderr: bytes 14 | 15 | 16 | @dataclasses.dataclass(frozen=True, order=True) 17 | class KeyPair: 18 | vkey_file: pl.Path 19 | skey_file: pl.Path 20 | 21 | 22 | @dataclasses.dataclass(frozen=True, order=True) 23 | class ColdKeyPair: 24 | vkey_file: pl.Path 25 | skey_file: pl.Path 26 | counter_file: pl.Path 27 | 28 | 29 | @dataclasses.dataclass(frozen=True, order=True) 30 | class AddressRecord: 31 | address: str 32 | vkey_file: pl.Path 33 | skey_file: pl.Path 34 | 35 | 36 | @dataclasses.dataclass(frozen=True, order=True) 37 | class StakeAddrInfo: 38 | address: str 39 | delegation: str 40 | reward_account_balance: int 41 | registration_deposit: int 42 | vote_delegation: str 43 | delegation_hex: str = "" 44 | vote_delegation_hex: str = "" 45 | 46 | def __bool__(self) -> bool: 47 | return bool(self.address) 48 | 49 | 50 | @dataclasses.dataclass(frozen=True, order=True) 51 | class UTXOData: 52 | utxo_hash: str 53 | utxo_ix: int 54 | amount: int 55 | address: str 56 | coin: str = consts.DEFAULT_COIN 57 | decoded_coin: str = "" 58 | datum_hash: str = "" 59 | inline_datum_hash: str = "" 60 | inline_datum: str | dict | None = None 61 | reference_script: dict | None = None 62 | 63 | 64 | @dataclasses.dataclass(frozen=True, order=True) 65 | class TxOut: 66 | address: str 67 | amount: int 68 | coin: str = consts.DEFAULT_COIN 69 | datum_hash: str = "" 70 | datum_hash_file: itp.FileType = "" 71 | datum_hash_cbor_file: itp.FileType = "" 72 | datum_hash_value: str = "" 73 | datum_embed_file: itp.FileType = "" 74 | datum_embed_cbor_file: itp.FileType = "" 75 | datum_embed_value: str = "" 76 | inline_datum_file: itp.FileType = "" 77 | inline_datum_cbor_file: itp.FileType = "" 78 | inline_datum_value: str = "" 79 | reference_script_file: itp.FileType = "" 80 | 81 | 82 | # List of `TxOut`s, empty list, or empty tuple 83 | OptionalTxOuts = list[TxOut] | tuple[()] 84 | # List of `UTXOData`s, empty list, or empty tuple 85 | OptionalUTXOData = list[UTXOData] | tuple[()] 86 | 87 | 88 | @dataclasses.dataclass(frozen=True, order=True) 89 | class ScriptTxIn: 90 | """Data structure for Tx inputs that are combined with scripts (simple or Plutus).""" 91 | 92 | txins: list[UTXOData] 93 | script_file: itp.FileType = "" 94 | reference_txin: UTXOData | None = None 95 | reference_type: str = "" 96 | # Values below needed only when working with Plutus 97 | collaterals: OptionalUTXOData = () 98 | execution_units: tp.Optional[tuple[int, int]] = None 99 | datum_file: itp.FileType = "" 100 | datum_cbor_file: itp.FileType = "" 101 | datum_value: str = "" 102 | inline_datum_present: bool = False 103 | redeemer_file: itp.FileType = "" 104 | redeemer_cbor_file: itp.FileType = "" 105 | redeemer_value: str = "" 106 | 107 | 108 | @dataclasses.dataclass(frozen=True, order=True) 109 | class ScriptWithdrawal: 110 | """Data structure for withdrawals that are combined with Plutus scripts.""" 111 | 112 | txout: TxOut 113 | script_file: itp.FileType = "" 114 | reference_txin: UTXOData | None = None 115 | reference_type: str = "" 116 | collaterals: OptionalUTXOData = () 117 | execution_units: tp.Optional[tuple[int, int]] = None 118 | redeemer_file: itp.FileType = "" 119 | redeemer_cbor_file: itp.FileType = "" 120 | redeemer_value: str = "" 121 | 122 | 123 | @dataclasses.dataclass(frozen=True, order=True) 124 | class ComplexCert: 125 | """Data structure for certificates with optional data for Plutus scripts. 126 | 127 | If used for one certificate, it needs to be used for all the other certificates in a given 128 | transaction (instead of `TxFiles.certificate_files`). Otherwise, order of certificates 129 | cannot be guaranteed. 130 | """ 131 | 132 | certificate_file: itp.FileType 133 | script_file: itp.FileType = "" 134 | reference_txin: UTXOData | None = None 135 | reference_type: str = "" 136 | collaterals: OptionalUTXOData = () 137 | execution_units: tp.Optional[tuple[int, int]] = None 138 | redeemer_file: itp.FileType = "" 139 | redeemer_cbor_file: itp.FileType = "" 140 | redeemer_value: str = "" 141 | 142 | 143 | @dataclasses.dataclass(frozen=True, order=True) 144 | class ComplexProposal: 145 | """Data structure for proposal with optional data for Plutus scripts. 146 | 147 | If used for one proposal, it needs to be used for all the other proposals in a given 148 | transaction (instead of `TxFiles.proposal_files`). Otherwise, order of proposals 149 | cannot be guaranteed. 150 | """ 151 | 152 | proposal_file: itp.FileType 153 | script_file: itp.FileType = "" 154 | collaterals: OptionalUTXOData = () 155 | execution_units: tp.Optional[tuple[int, int]] = None 156 | redeemer_file: itp.FileType = "" 157 | redeemer_cbor_file: itp.FileType = "" 158 | redeemer_value: str = "" 159 | 160 | 161 | @dataclasses.dataclass(frozen=True, order=True) 162 | class ScriptVote: 163 | """Data structure for voting that are combined with scripts.""" 164 | 165 | vote_file: itp.FileType = "" 166 | script_file: itp.FileType = "" 167 | # Values below needed only when working with Plutus 168 | collaterals: OptionalUTXOData = () 169 | execution_units: tp.Optional[tuple[int, int]] = None 170 | redeemer_file: itp.FileType = "" 171 | redeemer_cbor_file: itp.FileType = "" 172 | redeemer_value: str = "" 173 | 174 | 175 | @dataclasses.dataclass(frozen=True, order=True) 176 | class Mint: 177 | txouts: list[TxOut] 178 | script_file: itp.FileType = "" 179 | reference_txin: UTXOData | None = None 180 | reference_type: str = "" 181 | policyid: str = "" 182 | # Values below needed only when working with Plutus 183 | collaterals: OptionalUTXOData = () 184 | execution_units: tp.Optional[tuple[int, int]] = None 185 | redeemer_file: itp.FileType = "" 186 | redeemer_cbor_file: itp.FileType = "" 187 | redeemer_value: str = "" 188 | 189 | 190 | # List of `ScriptTxIn`s, empty list, or empty tuple 191 | OptionalScriptTxIn = list[ScriptTxIn] | tuple[()] 192 | # List of `ComplexCert`s, empty list, or empty tuple 193 | OptionalScriptCerts = list[ComplexCert] | tuple[()] 194 | # List of `ComplexProposal`s, empty list, or empty tuple 195 | OptionalScriptProposals = list[ComplexProposal] | tuple[()] 196 | # List of `ScriptWithdrawal`s, empty list, or empty tuple 197 | OptionalScriptWithdrawals = list[ScriptWithdrawal] | tuple[()] 198 | # List of `Mint`s, empty list, or empty tuple 199 | OptionalMint = list[Mint] | tuple[()] 200 | # List of `ScriptVote`s, empty list, or empty tuple 201 | OptionalScriptVotes = list[ScriptVote] | tuple[()] 202 | 203 | 204 | @dataclasses.dataclass(frozen=True, order=True) 205 | class TxFiles: 206 | certificate_files: itp.OptionalFiles = () 207 | proposal_files: itp.OptionalFiles = () 208 | metadata_json_files: itp.OptionalFiles = () 209 | metadata_cbor_files: itp.OptionalFiles = () 210 | signing_key_files: itp.OptionalFiles = () 211 | auxiliary_script_files: itp.OptionalFiles = () 212 | vote_files: itp.OptionalFiles = () 213 | metadata_json_detailed_schema: bool = False 214 | 215 | 216 | @dataclasses.dataclass(frozen=True, order=True) 217 | class PoolUser: 218 | payment: AddressRecord 219 | stake: AddressRecord 220 | 221 | 222 | @dataclasses.dataclass(frozen=True, order=True) 223 | class PoolData: 224 | pool_name: str 225 | pool_pledge: int 226 | pool_cost: int 227 | pool_margin: float 228 | pool_metadata_url: str = "" 229 | pool_metadata_hash: str = "" 230 | pool_relay_dns: str = "" 231 | pool_relay_ipv4: str = "" 232 | pool_relay_port: int = 0 233 | 234 | 235 | @dataclasses.dataclass(frozen=True, order=True) 236 | class TxRawOutput: 237 | txins: list[UTXOData] # UTXOs used as inputs 238 | txouts: list[TxOut] # Tx outputs 239 | txouts_count: int # Final number of tx outputs after adding change address and joining outputs 240 | tx_files: TxFiles # Files needed for transaction building (certificates, signing keys, etc.) 241 | out_file: pl.Path # Output file path for the transaction body 242 | fee: int # Tx fee 243 | build_args: list[str] # Arguments that were passed to `cardano-cli transaction build*` 244 | era: str = "" # Era used for the transaction 245 | script_txins: OptionalScriptTxIn = () # Tx inputs that are combined with scripts 246 | script_withdrawals: OptionalScriptWithdrawals = () # Withdrawals that are combined with scripts 247 | script_votes: OptionalScriptVotes = () # Votes that are combined with scripts 248 | complex_certs: OptionalScriptCerts = () # Certificates that are combined with scripts 249 | complex_proposals: OptionalScriptProposals = () # Proposals that are combined with scripts 250 | mint: OptionalMint = () # Minting data (Tx outputs, script, etc.) 251 | invalid_hereafter: int | None = None # Validity interval upper bound 252 | invalid_before: int | None = None # Validity interval lower bound 253 | current_treasury_value: int | None = None # Current treasury value 254 | treasury_donation: int | None = None # Amount of funds that will be donated to treasury 255 | withdrawals: OptionalTxOuts = () # All withdrawals (including those combined with scripts) 256 | change_address: str = "" # Address for change 257 | return_collateral_txouts: OptionalTxOuts = () # Tx outputs for returning collateral 258 | total_collateral_amount: int | None = None # Total collateral amount 259 | readonly_reference_txins: OptionalUTXOData = () # Tx inputs for plutus script context 260 | script_valid: bool = True # Whether the plutus script is valid 261 | required_signers: itp.OptionalFiles = () # Signing keys required for the transaction 262 | # Hashes of signing keys that are required for the transaction 263 | required_signer_hashes: list[str] | tuple[()] = () 264 | combined_reference_txins: OptionalUTXOData = () # All reference tx inputs 265 | 266 | 267 | @dataclasses.dataclass(frozen=True, order=True) 268 | class PoolCreationOutput: 269 | stake_pool_id: str 270 | vrf_key_pair: KeyPair 271 | cold_key_pair: ColdKeyPair 272 | pool_reg_cert_file: pl.Path 273 | pool_data: PoolData 274 | pool_owners: list[PoolUser] 275 | reward_account_key_pair: KeyPair | AddressRecord 276 | tx_raw_output: TxRawOutput 277 | kes_key_pair: KeyPair | None = None 278 | 279 | 280 | @dataclasses.dataclass(frozen=True, order=True) 281 | class GenesisKeys: 282 | genesis_utxo_vkey: pl.Path 283 | genesis_utxo_skey: pl.Path 284 | genesis_vkeys: list[pl.Path] 285 | delegate_skeys: list[pl.Path] 286 | 287 | 288 | @dataclasses.dataclass(frozen=True, order=True) 289 | class PoolParamsTop: 290 | pool_params: dict 291 | future_pool_params: dict 292 | retiring: int | None 293 | 294 | 295 | @dataclasses.dataclass(frozen=True, order=True) 296 | class AddressInfo: 297 | address: str 298 | era: str 299 | encoding: str 300 | type: str 301 | base16: str 302 | 303 | 304 | @dataclasses.dataclass(frozen=True, order=True) 305 | class Value: 306 | value: int 307 | coin: str 308 | 309 | 310 | @dataclasses.dataclass(frozen=True, order=True) 311 | class LeadershipSchedule: 312 | slot_no: int 313 | utc_time: datetime.datetime 314 | 315 | 316 | @dataclasses.dataclass(frozen=True, order=True) 317 | class DataForBuild: 318 | txins: list[UTXOData] 319 | txouts: list[TxOut] 320 | withdrawals: OptionalTxOuts 321 | script_withdrawals: OptionalScriptWithdrawals 322 | 323 | 324 | @dataclasses.dataclass(frozen=True, order=True) 325 | class CCMember: 326 | epoch: int 327 | cold_vkey: str = "" 328 | cold_vkey_file: itp.FileType = "" 329 | cold_vkey_hash: str = "" 330 | cold_skey: str = "" 331 | cold_skey_file: itp.FileType = "" 332 | cold_skey_hash: str = "" 333 | cold_script_hash: str = "" 334 | 335 | 336 | @dataclasses.dataclass(frozen=True, order=True) 337 | class VoteCC: 338 | action_txid: str 339 | action_ix: int 340 | vote: consts.Votes 341 | vote_file: pl.Path 342 | cc_hot_vkey: str = "" 343 | cc_hot_vkey_file: pl.Path | None = None 344 | cc_hot_key_hash: str = "" 345 | cc_hot_script_hash: str = "" 346 | anchor_url: str = "" 347 | anchor_data_hash: str = "" 348 | 349 | 350 | @dataclasses.dataclass(frozen=True, order=True) 351 | class VoteDrep: 352 | action_txid: str 353 | action_ix: int 354 | vote: consts.Votes 355 | vote_file: pl.Path 356 | drep_vkey: str = "" 357 | drep_vkey_file: pl.Path | None = None 358 | drep_key_hash: str = "" 359 | drep_script_hash: str = "" 360 | anchor_url: str = "" 361 | anchor_data_hash: str = "" 362 | 363 | 364 | @dataclasses.dataclass(frozen=True, order=True) 365 | class VoteSPO: 366 | action_txid: str 367 | action_ix: int 368 | vote: consts.Votes 369 | vote_file: pl.Path 370 | stake_pool_vkey: str = "" 371 | cold_vkey_file: pl.Path | None = None 372 | stake_pool_id: str = "" 373 | anchor_url: str = "" 374 | anchor_data_hash: str = "" 375 | 376 | 377 | @dataclasses.dataclass(frozen=True, order=True) 378 | class ActionConstitution: 379 | action_file: pl.Path 380 | deposit_amt: int 381 | anchor_url: str 382 | anchor_data_hash: str 383 | constitution_url: str 384 | constitution_hash: str 385 | deposit_return_stake_vkey: str = "" 386 | deposit_return_stake_vkey_file: pl.Path | None = None 387 | deposit_return_stake_key_hash: str = "" 388 | prev_action_txid: str = "" 389 | prev_action_ix: int = -1 390 | 391 | 392 | @dataclasses.dataclass(frozen=True, order=True) 393 | class ActionInfo: 394 | action_file: pl.Path 395 | deposit_amt: int 396 | anchor_url: str 397 | anchor_data_hash: str 398 | deposit_return_stake_vkey: str = "" 399 | deposit_return_stake_vkey_file: pl.Path | None = None 400 | deposit_return_stake_key_hash: str = "" 401 | 402 | 403 | @dataclasses.dataclass(frozen=True, order=True) 404 | class ActionNoConfidence: 405 | action_file: pl.Path 406 | deposit_amt: int 407 | anchor_url: str 408 | anchor_data_hash: str 409 | prev_action_txid: str 410 | prev_action_ix: int 411 | deposit_return_stake_vkey: str = "" 412 | deposit_return_stake_vkey_file: pl.Path | None = None 413 | deposit_return_stake_key_hash: str = "" 414 | 415 | 416 | @dataclasses.dataclass(frozen=True, order=True) 417 | class ActionUpdateCommittee: 418 | action_file: pl.Path 419 | deposit_amt: int 420 | anchor_url: str 421 | anchor_data_hash: str 422 | threshold: str 423 | add_cc_members: list[CCMember] = dataclasses.field(default_factory=list) 424 | rem_cc_members: list[CCMember] = dataclasses.field(default_factory=list) 425 | prev_action_txid: str = "" 426 | prev_action_ix: int = -1 427 | deposit_return_stake_vkey: str = "" 428 | deposit_return_stake_vkey_file: pl.Path | None = None 429 | deposit_return_stake_key_hash: str = "" 430 | 431 | 432 | @dataclasses.dataclass(frozen=True, order=True) 433 | class ActionPParamsUpdate: 434 | action_file: pl.Path 435 | deposit_amt: int 436 | anchor_url: str 437 | anchor_data_hash: str 438 | cli_args: itp.UnpackableSequence 439 | prev_action_txid: str = "" 440 | prev_action_ix: int = -1 441 | deposit_return_stake_vkey: str = "" 442 | deposit_return_stake_vkey_file: pl.Path | None = None 443 | deposit_return_stake_key_hash: str = "" 444 | 445 | 446 | @dataclasses.dataclass(frozen=True, order=True) 447 | class ActionTreasuryWithdrawal: 448 | action_file: pl.Path 449 | transfer_amt: int 450 | deposit_amt: int 451 | anchor_url: str 452 | anchor_data_hash: str 453 | funds_receiving_stake_vkey: str = "" 454 | funds_receiving_stake_vkey_file: pl.Path | None = None 455 | funds_receiving_stake_key_hash: str = "" 456 | deposit_return_stake_vkey: str = "" 457 | deposit_return_stake_vkey_file: pl.Path | None = None 458 | deposit_return_stake_key_hash: str = "" 459 | 460 | 461 | @dataclasses.dataclass(frozen=True, order=True) 462 | class ActionHardfork: 463 | action_file: pl.Path 464 | deposit_amt: int 465 | anchor_url: str 466 | anchor_data_hash: str 467 | protocol_major_version: int 468 | protocol_minor_version: int 469 | prev_action_txid: str = "" 470 | prev_action_ix: int = -1 471 | deposit_return_stake_vkey: str = "" 472 | deposit_return_stake_vkey_file: pl.Path | None = None 473 | deposit_return_stake_key_hash: str = "" 474 | 475 | 476 | @dataclasses.dataclass(frozen=True, order=True) 477 | class SPOStakeDistrib: 478 | spo_vkey_hex: str 479 | stake_distribution: int 480 | vote_delegation: str 481 | -------------------------------------------------------------------------------- /cardano_clusterlib/clusterlib_klass.py: -------------------------------------------------------------------------------- 1 | """Wrapper for cardano-cli for working with cardano cluster.""" 2 | 3 | import json 4 | import logging 5 | import pathlib as pl 6 | import subprocess 7 | import time 8 | 9 | from packaging import version 10 | 11 | from cardano_clusterlib import address_group 12 | from cardano_clusterlib import clusterlib_helpers 13 | from cardano_clusterlib import consts 14 | from cardano_clusterlib import exceptions 15 | from cardano_clusterlib import genesis_group 16 | from cardano_clusterlib import gov_group 17 | from cardano_clusterlib import helpers 18 | from cardano_clusterlib import key_group 19 | from cardano_clusterlib import legacy_gov_group 20 | from cardano_clusterlib import node_group 21 | from cardano_clusterlib import query_group 22 | from cardano_clusterlib import stake_address_group 23 | from cardano_clusterlib import stake_pool_group 24 | from cardano_clusterlib import structs 25 | from cardano_clusterlib import transaction_group 26 | from cardano_clusterlib import types as itp 27 | 28 | LOGGER = logging.getLogger(__name__) 29 | 30 | 31 | class ClusterLib: 32 | """Methods for working with cardano cluster using `cardano-cli`.. 33 | 34 | Attributes: 35 | state_dir: A directory with cluster state files (keys, config files, logs, ...). 36 | protocol: A cluster protocol - full cardano mode by default. 37 | slots_offset: Difference in slots between cluster's start era and Shelley era 38 | (Byron vs Shelley) 39 | socket_path: A path to socket file for communication with the node. This overrides the 40 | `CARDANO_NODE_SOCKET_PATH` environment variable. 41 | command_era: An era used for CLI commands, by default same as the latest network Era. 42 | """ 43 | 44 | def __init__( 45 | self, 46 | state_dir: itp.FileType, 47 | slots_offset: int | None = None, 48 | socket_path: itp.FileType = "", 49 | command_era: str = consts.CommandEras.LATEST, 50 | ) -> None: 51 | try: 52 | self.command_era = getattr(consts.CommandEras, command_era.upper()) 53 | except AttributeError as excp: 54 | msg = f"Unknown command era `{command_era}`." 55 | raise exceptions.CLIError(msg) from excp 56 | 57 | self.cluster_id = 0 # Can be used for identifying cluster instance 58 | # Number of new blocks before the Tx is considered confirmed 59 | self.confirm_blocks = consts.CONFIRM_BLOCKS_NUM 60 | self._rand_str = helpers.get_rand_str(4) 61 | self._cli_log = "" 62 | 63 | self.era_in_use = ( 64 | consts.Eras.__members__.get(command_era.upper()) or consts.Eras["DEFAULT"] 65 | ).name.lower() 66 | 67 | self.state_dir = pl.Path(state_dir).expanduser().resolve() 68 | if not self.state_dir.exists(): 69 | msg = f"The state dir `{self.state_dir}` doesn't exist." 70 | raise exceptions.CLIError(msg) 71 | 72 | self._init_socket_path = socket_path 73 | self.socket_path: pl.Path | None = None 74 | self.socket_args: list[str] = [] 75 | self.set_socket_path(socket_path=socket_path) 76 | 77 | self.pparams_file = self.state_dir / f"pparams-{self._rand_str}.json" 78 | 79 | self.genesis_json = clusterlib_helpers._find_genesis_json(clusterlib_obj=self) 80 | with open(self.genesis_json, encoding="utf-8") as in_json: 81 | self.genesis = json.load(in_json) 82 | 83 | self.slot_length = self.genesis["slotLength"] 84 | self.epoch_length = self.genesis["epochLength"] 85 | self.epoch_length_sec = self.epoch_length * self.slot_length 86 | self.slots_per_kes_period = self.genesis["slotsPerKESPeriod"] 87 | self.max_kes_evolutions = self.genesis["maxKESEvolutions"] 88 | 89 | self.network_magic = self.genesis["networkMagic"] 90 | if self.network_magic == consts.MAINNET_MAGIC: 91 | self.magic_args = ["--mainnet"] 92 | else: 93 | self.magic_args = ["--testnet-magic", str(self.network_magic)] 94 | 95 | self._slots_offset = slots_offset if slots_offset is not None else None 96 | 97 | self.ttl_length = 1000 98 | # TODO: proper calculation based on `utxoCostPerWord` needed 99 | self._min_change_value = 1800_000 100 | 101 | # Conway+ era 102 | self.conway_genesis_json: pl.Path | None = None 103 | self.conway_genesis: dict = {} 104 | 105 | if consts.Eras[self.era_in_use.upper()].value >= consts.Eras.CONWAY.value: 106 | # Conway genesis 107 | self.conway_genesis_json = clusterlib_helpers._find_conway_genesis_json( 108 | clusterlib_obj=self 109 | ) 110 | with open(self.conway_genesis_json, encoding="utf-8") as in_json: 111 | self.conway_genesis = json.load(in_json) 112 | 113 | self.overwrite_outfiles = True 114 | 115 | self._cli_version: version.Version | None = None 116 | 117 | # Groups of commands 118 | self._transaction_group: transaction_group.TransactionGroup | None = None 119 | self._query_group: query_group.QueryGroup | None = None 120 | self._address_group: address_group.AddressGroup | None = None 121 | self._stake_address_group: stake_address_group.StakeAddressGroup | None = None 122 | self._stake_pool_group: stake_pool_group.StakePoolGroup | None = None 123 | self._node_group: node_group.NodeGroup | None = None 124 | self._key_group: key_group.KeyGroup | None = None 125 | self._genesis_group: genesis_group.GenesisGroup | None = None 126 | self._legacy_gov_group: legacy_gov_group.LegacyGovGroup | None = None 127 | self._governance_group: gov_group.GovernanceGroup | None = None 128 | 129 | def set_socket_path(self, socket_path: itp.FileType | None) -> None: 130 | """Set a path to socket file for communication with the node.""" 131 | if not socket_path: 132 | self.socket_path = None 133 | self.socket_args = [] 134 | return 135 | 136 | socket_path = pl.Path(socket_path).expanduser().resolve() 137 | if not socket_path.exists(): 138 | msg = f"The socket `{socket_path}` doesn't exist." 139 | raise exceptions.CLIError(msg) 140 | 141 | self.socket_path = socket_path 142 | self.socket_args = ["--socket-path", str(self.socket_path)] 143 | 144 | @property 145 | def cli_version(self) -> version.Version: 146 | """Version of `cardano-cli`.""" 147 | if self._cli_version is None: 148 | version_out = self.cli( 149 | ["cardano-cli", "--version"], add_default_args=False 150 | ).stdout.decode() 151 | version_str = version_out.split(" ")[1] 152 | self._cli_version = version.parse(version_str) 153 | return self._cli_version 154 | 155 | @property 156 | def slots_offset(self) -> int: 157 | """Get offset of slots from Byron era vs current configuration.""" 158 | if self._slots_offset is None: 159 | self._slots_offset = clusterlib_helpers.get_slots_offset(clusterlib_obj=self) 160 | return self._slots_offset 161 | 162 | @property 163 | def g_transaction(self) -> transaction_group.TransactionGroup: 164 | """Transaction group.""" 165 | if not self._transaction_group: 166 | self._transaction_group = transaction_group.TransactionGroup(clusterlib_obj=self) 167 | return self._transaction_group 168 | 169 | @property 170 | def g_query(self) -> query_group.QueryGroup: 171 | """Query group.""" 172 | if not self._query_group: 173 | self._query_group = query_group.QueryGroup(clusterlib_obj=self) 174 | return self._query_group 175 | 176 | @property 177 | def g_address(self) -> address_group.AddressGroup: 178 | """Address group.""" 179 | if not self._address_group: 180 | self._address_group = address_group.AddressGroup(clusterlib_obj=self) 181 | return self._address_group 182 | 183 | @property 184 | def g_stake_address(self) -> stake_address_group.StakeAddressGroup: 185 | """Stake address group.""" 186 | if not self._stake_address_group: 187 | self._stake_address_group = stake_address_group.StakeAddressGroup(clusterlib_obj=self) 188 | return self._stake_address_group 189 | 190 | @property 191 | def g_stake_pool(self) -> stake_pool_group.StakePoolGroup: 192 | """Stake pool group.""" 193 | if not self._stake_pool_group: 194 | self._stake_pool_group = stake_pool_group.StakePoolGroup(clusterlib_obj=self) 195 | return self._stake_pool_group 196 | 197 | @property 198 | def g_node(self) -> node_group.NodeGroup: 199 | """Node group.""" 200 | if not self._node_group: 201 | self._node_group = node_group.NodeGroup(clusterlib_obj=self) 202 | return self._node_group 203 | 204 | @property 205 | def g_key(self) -> key_group.KeyGroup: 206 | """Key group.""" 207 | if not self._key_group: 208 | self._key_group = key_group.KeyGroup(clusterlib_obj=self) 209 | return self._key_group 210 | 211 | @property 212 | def g_genesis(self) -> genesis_group.GenesisGroup: 213 | """Genesis group.""" 214 | if not self._genesis_group: 215 | self._genesis_group = genesis_group.GenesisGroup(clusterlib_obj=self) 216 | return self._genesis_group 217 | 218 | @property 219 | def g_legacy_governance(self) -> legacy_gov_group.LegacyGovGroup: 220 | """Legacy governance group.""" 221 | if not self._legacy_gov_group: 222 | self._legacy_gov_group = legacy_gov_group.LegacyGovGroup(clusterlib_obj=self) 223 | return self._legacy_gov_group 224 | 225 | @property 226 | def g_governance(self) -> gov_group.GovernanceGroup: 227 | """Governance group.""" 228 | if self._governance_group: 229 | return self._governance_group 230 | 231 | if not self.conway_genesis: 232 | msg = "The governance group can be used only with Command era >= Conway." 233 | raise exceptions.CLIError(msg) 234 | 235 | self._governance_group = gov_group.GovernanceGroup(clusterlib_obj=self) 236 | return self._governance_group 237 | 238 | def cli( 239 | self, 240 | cli_args: list[str], 241 | timeout: float | None = None, 242 | add_default_args: bool = True, 243 | ) -> structs.CLIOut: 244 | """Run the `cardano-cli` command. 245 | 246 | Args: 247 | cli_args: A list of arguments for cardano-cli. 248 | timeout: A timeout for the command, in seconds (optional). 249 | add_default_args: Whether to add default arguments to the command (optional). 250 | 251 | Returns: 252 | structs.CLIOut: A data container containing command stdout and stderr. 253 | """ 254 | cli_args_strs_all = [str(arg) for arg in cli_args] 255 | 256 | if add_default_args: 257 | cli_args_strs_all.insert(0, "cardano-cli") 258 | cli_args_strs_all.insert(1, self.command_era) 259 | 260 | cli_args_strs = [arg for arg in cli_args_strs_all if arg != consts.SUBCOMMAND_MARK] 261 | 262 | cmd_str = clusterlib_helpers._format_cli_args(cli_args=cli_args_strs) 263 | clusterlib_helpers._write_cli_log(clusterlib_obj=self, command=cmd_str) 264 | LOGGER.debug("Running `%s`", cmd_str) 265 | 266 | # Re-run the command when running into 267 | # Network.Socket.connect: : resource exhausted (Resource temporarily unavailable) 268 | # or 269 | # MuxError (MuxIOException writev: resource vanished (Broken pipe)) "(sendAll errored)" 270 | err_msg = "" 271 | for __ in range(3): 272 | retcode = None 273 | with subprocess.Popen( 274 | cli_args_strs, stdout=subprocess.PIPE, stderr=subprocess.PIPE 275 | ) as p: 276 | stdout, stderr = p.communicate(timeout=timeout) 277 | retcode = p.returncode 278 | 279 | if retcode == 0: 280 | break 281 | 282 | # pyrefly: ignore # missing-attribute 283 | stderr_dec = stderr.decode() 284 | err_msg = ( 285 | f"An error occurred running a CLI command `{cmd_str}` on path " 286 | f"`{pl.Path.cwd()}`: {stderr_dec}" 287 | ) 288 | if "resource exhausted" in stderr_dec or "resource vanished" in stderr_dec: 289 | LOGGER.error(err_msg) 290 | time.sleep(0.4) 291 | continue 292 | raise exceptions.CLIError(err_msg) 293 | else: 294 | raise exceptions.CLIError(err_msg) 295 | 296 | # pyrefly: ignore # bad-argument-type 297 | return structs.CLIOut(stdout or b"", stderr or b"") 298 | 299 | def refresh_pparams_file(self) -> None: 300 | """Refresh protocol parameters file.""" 301 | self.g_query.query_cli(["protocol-parameters", "--out-file", str(self.pparams_file)]) 302 | 303 | def create_pparams_file(self) -> None: 304 | """Create protocol parameters file if it doesn't exist.""" 305 | if self.pparams_file.exists(): 306 | return 307 | self.refresh_pparams_file() 308 | 309 | def wait_for_new_block(self, new_blocks: int = 1) -> int: 310 | """Wait for new block(s) to be created. 311 | 312 | Args: 313 | new_blocks: A number of new blocks to wait for (optional). 314 | 315 | Returns: 316 | int: A block number of last added block. 317 | """ 318 | initial_tip = self.g_query.get_tip() 319 | initial_block = int(initial_tip["block"]) 320 | 321 | if new_blocks < 1: 322 | return initial_block 323 | 324 | return clusterlib_helpers.wait_for_block( 325 | clusterlib_obj=self, tip=initial_tip, block_no=initial_block + new_blocks 326 | ) 327 | 328 | def wait_for_block(self, block: int) -> int: 329 | """Wait for block number. 330 | 331 | Args: 332 | block: A block number to wait for. 333 | 334 | Returns: 335 | int: A block number of last added block. 336 | """ 337 | return clusterlib_helpers.wait_for_block( 338 | clusterlib_obj=self, tip=self.g_query.get_tip(), block_no=block 339 | ) 340 | 341 | def wait_for_slot(self, slot: int) -> int: 342 | """Wait for slot number. 343 | 344 | Args: 345 | slot: A slot number to wait for. 346 | 347 | Returns: 348 | int: A slot number of last block. 349 | """ 350 | min_sleep = 1.5 # in sec 351 | long_sleep = 15 # in sec 352 | no_block_time = 0 # in slots 353 | next_block_timeout = 300 # in slots 354 | last_slot = -1 355 | printed = False 356 | for __ in range(100): 357 | this_slot = self.g_query.get_slot_no() 358 | 359 | slots_diff = slot - this_slot 360 | if slots_diff <= 0: 361 | return this_slot 362 | 363 | if this_slot == last_slot: 364 | if no_block_time >= next_block_timeout: 365 | msg = f"Failed to wait for slot number {slot}, no new blocks are being created." 366 | raise exceptions.CLIError(msg) 367 | else: 368 | no_block_time = 0 369 | 370 | _sleep_time = slots_diff * self.slot_length 371 | sleep_time = max(min_sleep, _sleep_time) 372 | 373 | if not printed and sleep_time > long_sleep: 374 | LOGGER.info(f"Waiting for {sleep_time:.2f} sec for slot no {slot}.") 375 | printed = True 376 | 377 | last_slot = this_slot 378 | no_block_time += slots_diff 379 | time.sleep(sleep_time) 380 | 381 | msg = f"Failed to wait for slot number {slot}." 382 | raise exceptions.CLIError(msg) 383 | 384 | def wait_for_new_epoch(self, new_epochs: int = 1, padding_seconds: int = 0) -> int: 385 | """Wait for new epoch(s). 386 | 387 | Args: 388 | new_epochs: A number of new epochs to wait for (optional). 389 | padding_seconds: A number of additional seconds to wait for (optional). 390 | 391 | Returns: 392 | int: The current epoch. 393 | """ 394 | start_tip = self.g_query.get_tip() 395 | start_epoch = int(start_tip["epoch"]) 396 | 397 | if new_epochs < 1: 398 | return start_epoch 399 | 400 | epoch_no = start_epoch + new_epochs 401 | return clusterlib_helpers.wait_for_epoch( 402 | clusterlib_obj=self, tip=start_tip, epoch_no=epoch_no, padding_seconds=padding_seconds 403 | ) 404 | 405 | def wait_for_epoch( 406 | self, epoch_no: int, padding_seconds: int = 0, future_is_ok: bool = True 407 | ) -> int: 408 | """Wait for epoch no. 409 | 410 | Args: 411 | epoch_no: A number of epoch to wait for. 412 | padding_seconds: A number of additional seconds to wait for (optional). 413 | future_is_ok: A bool indicating whether current epoch > `epoch_no` is acceptable 414 | (default: True). 415 | 416 | Returns: 417 | int: The current epoch. 418 | """ 419 | return clusterlib_helpers.wait_for_epoch( 420 | clusterlib_obj=self, 421 | tip=self.g_query.get_tip(), 422 | epoch_no=epoch_no, 423 | padding_seconds=padding_seconds, 424 | future_is_ok=future_is_ok, 425 | ) 426 | 427 | def time_to_epoch_end(self, tip: dict | None = None) -> float: 428 | """How many seconds to go to start of a new epoch.""" 429 | tip = tip or self.g_query.get_tip() 430 | epoch = int(tip["epoch"]) 431 | slot = int(tip["slot"]) 432 | slots_to_go = (epoch + 1) * self.epoch_length - (slot + self.slots_offset - 1) 433 | return float(slots_to_go * self.slot_length) 434 | 435 | def time_from_epoch_start(self, tip: dict | None = None) -> float: 436 | """How many seconds passed from start of the current epoch.""" 437 | s_to_epoch_stop = self.time_to_epoch_end(tip=tip) 438 | return float(self.epoch_length_sec - s_to_epoch_stop) 439 | 440 | def __repr__(self) -> str: 441 | return f"<{self.__class__.__name__}: command_era={self.command_era}>" 442 | -------------------------------------------------------------------------------- /cardano_clusterlib/stake_address_group.py: -------------------------------------------------------------------------------- 1 | """Group of methods for working with stake addresses.""" 2 | 3 | import logging 4 | import pathlib as pl 5 | 6 | from cardano_clusterlib import clusterlib_helpers 7 | from cardano_clusterlib import exceptions 8 | from cardano_clusterlib import helpers 9 | from cardano_clusterlib import structs 10 | from cardano_clusterlib import types as itp 11 | 12 | LOGGER = logging.getLogger(__name__) 13 | 14 | 15 | class StakeAddressGroup: 16 | def __init__(self, clusterlib_obj: "itp.ClusterLib") -> None: 17 | self._clusterlib_obj = clusterlib_obj 18 | 19 | def _get_stake_vkey_args( 20 | self, 21 | stake_vkey: str = "", 22 | stake_vkey_file: itp.FileType | None = None, 23 | stake_script_file: itp.FileType | None = None, 24 | stake_address: str | None = None, 25 | ) -> list[str]: 26 | """Return CLI args for stake vkey.""" 27 | if stake_vkey: 28 | stake_args = ["--stake-verification-key", stake_vkey] 29 | elif stake_vkey_file: 30 | stake_args = ["--stake-verification-key-file", str(stake_vkey_file)] 31 | elif stake_script_file: 32 | stake_args = ["--stake-script-file", str(stake_script_file)] 33 | elif stake_address: 34 | stake_args = ["--stake-address", stake_address] 35 | else: 36 | msg = "Either `stake_vkey_file`, `stake_script_file` or `stake_address` is needed." 37 | raise ValueError(msg) 38 | 39 | return stake_args 40 | 41 | def _get_drep_args( 42 | self, 43 | drep_script_hash: str = "", 44 | drep_vkey: str = "", 45 | drep_vkey_file: itp.FileType | None = None, 46 | drep_key_hash: str = "", 47 | always_abstain: bool = False, 48 | always_no_confidence: bool = False, 49 | ) -> list[str]: 50 | """Return CLI args for DRep identification.""" 51 | if always_abstain: 52 | drep_args = ["--always-abstain"] 53 | elif always_no_confidence: 54 | drep_args = ["--always-no-confidence"] 55 | elif drep_script_hash: 56 | drep_args = ["--drep-script-hash", str(drep_script_hash)] 57 | elif drep_vkey: 58 | drep_args = ["--drep-verification-key", str(drep_vkey)] 59 | elif drep_vkey_file: 60 | drep_args = ["--drep-verification-key-file", str(drep_vkey_file)] 61 | elif drep_key_hash: 62 | drep_args = ["--drep-key-hash", str(drep_key_hash)] 63 | else: 64 | msg = "DRep identification, verification key or script hash is needed." 65 | raise ValueError(msg) 66 | 67 | return drep_args 68 | 69 | def _get_pool_key_args( 70 | self, 71 | stake_pool_vkey: str = "", 72 | cold_vkey_file: itp.FileType | None = None, 73 | stake_pool_id: str = "", 74 | ) -> list[str]: 75 | """Return CLI args for pool key.""" 76 | if stake_pool_vkey: 77 | pool_key_args = ["--stake-pool-verification-key", stake_pool_vkey] 78 | elif cold_vkey_file: 79 | pool_key_args = ["--cold-verification-key-file", str(cold_vkey_file)] 80 | elif stake_pool_id: 81 | pool_key_args = ["--stake-pool-id", stake_pool_id] 82 | else: 83 | msg = "No stake pool key was specified." 84 | raise ValueError(msg) 85 | 86 | return pool_key_args 87 | 88 | def gen_stake_addr( 89 | self, 90 | addr_name: str, 91 | stake_vkey_file: itp.FileType | None = None, 92 | stake_script_file: itp.FileType | None = None, 93 | destination_dir: itp.FileType = ".", 94 | ) -> str: 95 | """Generate a stake address. 96 | 97 | Args: 98 | addr_name: A name of payment address. 99 | stake_vkey_file: A path to corresponding stake vkey file (optional). 100 | stake_script_file: A path to corresponding payment script file (optional). 101 | destination_dir: A path to directory for storing artifacts (optional). 102 | 103 | Returns: 104 | str: A generated stake address. 105 | """ 106 | destination_dir = pl.Path(destination_dir).expanduser() 107 | out_file = destination_dir / f"{addr_name}_stake.addr" 108 | clusterlib_helpers._check_files_exist(out_file, clusterlib_obj=self._clusterlib_obj) 109 | 110 | if stake_vkey_file: 111 | cli_args = ["--stake-verification-key-file", str(stake_vkey_file)] 112 | elif stake_script_file: 113 | cli_args = ["--stake-script-file", str(stake_script_file)] 114 | else: 115 | msg = "Either `stake_vkey_file` or `stake_script_file` is needed." 116 | raise ValueError(msg) 117 | 118 | self._clusterlib_obj.cli( 119 | [ 120 | "stake-address", 121 | "build", 122 | *cli_args, 123 | *self._clusterlib_obj.magic_args, 124 | "--out-file", 125 | str(out_file), 126 | ] 127 | ) 128 | 129 | helpers._check_outfiles(out_file) 130 | return helpers.read_address_from_file(out_file) 131 | 132 | def gen_stake_key_pair( 133 | self, key_name: str, destination_dir: itp.FileType = "." 134 | ) -> structs.KeyPair: 135 | """Generate a stake address key pair. 136 | 137 | Args: 138 | key_name: A name of the key pair. 139 | destination_dir: A path to directory for storing artifacts (optional). 140 | 141 | Returns: 142 | structs.KeyPair: A data container containing the key pair. 143 | """ 144 | destination_dir = pl.Path(destination_dir).expanduser() 145 | vkey = destination_dir / f"{key_name}_stake.vkey" 146 | skey = destination_dir / f"{key_name}_stake.skey" 147 | clusterlib_helpers._check_files_exist(vkey, skey, clusterlib_obj=self._clusterlib_obj) 148 | 149 | self._clusterlib_obj.cli( 150 | [ 151 | "stake-address", 152 | "key-gen", 153 | "--verification-key-file", 154 | str(vkey), 155 | "--signing-key-file", 156 | str(skey), 157 | ] 158 | ) 159 | 160 | helpers._check_outfiles(vkey, skey) 161 | return structs.KeyPair(vkey, skey) 162 | 163 | def gen_stake_addr_registration_cert( 164 | self, 165 | addr_name: str, 166 | deposit_amt: int = -1, 167 | stake_vkey: str = "", 168 | stake_vkey_file: itp.FileType | None = None, 169 | stake_script_file: itp.FileType | None = None, 170 | stake_address: str | None = None, 171 | destination_dir: itp.FileType = ".", 172 | ) -> pl.Path: 173 | """Generate a stake address registration certificate. 174 | 175 | Args: 176 | addr_name: A name of stake address. 177 | deposit_amt: A stake address registration deposit amount (required in Conway+). 178 | stake_vkey: A stake vkey file (optional). 179 | stake_vkey_file: A path to corresponding stake vkey file (optional). 180 | stake_script_file: A path to corresponding stake script file (optional). 181 | stake_address: Stake address key, bech32 or hex-encoded (optional). 182 | destination_dir: A path to directory for storing artifacts (optional). 183 | 184 | Returns: 185 | Path: A path to the generated certificate. 186 | """ 187 | destination_dir = pl.Path(destination_dir).expanduser() 188 | out_file = destination_dir / f"{addr_name}_stake_reg.cert" 189 | clusterlib_helpers._check_files_exist(out_file, clusterlib_obj=self._clusterlib_obj) 190 | 191 | stake_args = self._get_stake_vkey_args( 192 | stake_vkey=stake_vkey, 193 | stake_vkey_file=stake_vkey_file, 194 | stake_script_file=stake_script_file, 195 | stake_address=stake_address, 196 | ) 197 | 198 | deposit_args = [] if deposit_amt == -1 else ["--key-reg-deposit-amt", str(deposit_amt)] 199 | 200 | self._clusterlib_obj.cli( 201 | [ 202 | "stake-address", 203 | "registration-certificate", 204 | *deposit_args, 205 | *stake_args, 206 | "--out-file", 207 | str(out_file), 208 | ] 209 | ) 210 | 211 | helpers._check_outfiles(out_file) 212 | return out_file 213 | 214 | def gen_stake_addr_deregistration_cert( 215 | self, 216 | addr_name: str, 217 | deposit_amt: int = -1, 218 | stake_vkey_file: itp.FileType | None = None, 219 | stake_script_file: itp.FileType | None = None, 220 | stake_address: str | None = None, 221 | destination_dir: itp.FileType = ".", 222 | ) -> pl.Path: 223 | """Generate a stake address deregistration certificate. 224 | 225 | Args: 226 | addr_name: A name of stake address. 227 | deposit_amt: A stake address registration deposit amount (required in Conway+). 228 | stake_vkey_file: A path to corresponding stake vkey file (optional). 229 | stake_script_file: A path to corresponding stake script file (optional). 230 | stake_address: Stake address key, bech32 or hex-encoded (optional). 231 | destination_dir: A path to directory for storing artifacts (optional). 232 | 233 | Returns: 234 | Path: A path to the generated certificate. 235 | """ 236 | destination_dir = pl.Path(destination_dir).expanduser() 237 | out_file = destination_dir / f"{addr_name}_stake_dereg.cert" 238 | clusterlib_helpers._check_files_exist(out_file, clusterlib_obj=self._clusterlib_obj) 239 | 240 | stake_args = self._get_stake_vkey_args( 241 | stake_vkey_file=stake_vkey_file, 242 | stake_script_file=stake_script_file, 243 | stake_address=stake_address, 244 | ) 245 | 246 | deposit_args = [] if deposit_amt == -1 else ["--key-reg-deposit-amt", str(deposit_amt)] 247 | 248 | self._clusterlib_obj.cli( 249 | [ 250 | "stake-address", 251 | "deregistration-certificate", 252 | *deposit_args, 253 | *stake_args, 254 | "--out-file", 255 | str(out_file), 256 | ] 257 | ) 258 | 259 | helpers._check_outfiles(out_file) 260 | return out_file 261 | 262 | def gen_stake_addr_delegation_cert( 263 | self, 264 | addr_name: str, 265 | stake_vkey: str = "", 266 | stake_vkey_file: itp.FileType | None = None, 267 | stake_script_file: itp.FileType | None = None, 268 | stake_address: str | None = None, 269 | stake_pool_vkey: str = "", 270 | cold_vkey_file: itp.FileType | None = None, 271 | stake_pool_id: str = "", 272 | destination_dir: itp.FileType = ".", 273 | ) -> pl.Path: 274 | """Generate a stake address delegation certificate. 275 | 276 | Args: 277 | addr_name: A name of stake address. 278 | stake_vkey: A stake vkey file (optional). 279 | stake_vkey_file: A path to corresponding stake vkey file (optional). 280 | stake_script_file: A path to corresponding stake script file (optional). 281 | stake_address: Stake address key, bech32 or hex-encoded (optional). 282 | stake_pool_vkey: A stake pool verification key (Bech32 or hex-encoded, optional). 283 | cold_vkey_file: A path to pool cold vkey file (optional). 284 | stake_pool_id: An ID of the stake pool (optional). 285 | destination_dir: A path to directory for storing artifacts (optional). 286 | 287 | Returns: 288 | Path: A path to the generated certificate. 289 | """ 290 | destination_dir = pl.Path(destination_dir).expanduser() 291 | out_file = destination_dir / f"{addr_name}_stake_deleg.cert" 292 | clusterlib_helpers._check_files_exist(out_file, clusterlib_obj=self._clusterlib_obj) 293 | 294 | stake_key_args = self._get_stake_vkey_args( 295 | stake_vkey=stake_vkey, 296 | stake_vkey_file=stake_vkey_file, 297 | stake_script_file=stake_script_file, 298 | stake_address=stake_address, 299 | ) 300 | pool_key_args = self._get_pool_key_args( 301 | stake_pool_vkey=stake_pool_vkey, 302 | cold_vkey_file=cold_vkey_file, 303 | stake_pool_id=stake_pool_id, 304 | ) 305 | 306 | self._clusterlib_obj.cli( 307 | [ 308 | "stake-address", 309 | "stake-delegation-certificate", 310 | *stake_key_args, 311 | *pool_key_args, 312 | "--out-file", 313 | str(out_file), 314 | ] 315 | ) 316 | 317 | helpers._check_outfiles(out_file) 318 | return out_file 319 | 320 | def gen_vote_delegation_cert( 321 | self, 322 | addr_name: str, 323 | stake_vkey: str = "", 324 | stake_vkey_file: itp.FileType | None = None, 325 | stake_script_file: itp.FileType | None = None, 326 | stake_address: str | None = None, 327 | drep_script_hash: str = "", 328 | drep_vkey: str = "", 329 | drep_vkey_file: itp.FileType | None = None, 330 | drep_key_hash: str = "", 331 | always_abstain: bool = False, 332 | always_no_confidence: bool = False, 333 | destination_dir: itp.FileType = ".", 334 | ) -> pl.Path: 335 | """Generate a stake address vote delegation certificate. 336 | 337 | Args: 338 | addr_name: A name of stake address. 339 | stake_vkey: A stake vkey file (optional). 340 | stake_vkey_file: A path to corresponding stake vkey file (optional). 341 | stake_script_file: A path to corresponding stake script file (optional). 342 | stake_address: Stake address key, bech32 or hex-encoded (optional). 343 | drep_script_hash: DRep script hash (hex-encoded, optional). 344 | drep_vkey: DRep verification key (Bech32 or hex-encoded, optional). 345 | drep_vkey_file: Filepath of the DRep verification key (optional). 346 | drep_key_hash: DRep verification key hash 347 | (either Bech32-encoded or hex-encoded, optional). 348 | always_abstain: A bool indicating whether to delegate to always-abstain DRep (optional). 349 | always_no_confidence: A bool indicating whether to delegate to 350 | always-vote-no-confidence DRep (optional). 351 | destination_dir: A path to directory for storing artifacts (optional). 352 | 353 | Returns: 354 | Path: A path to the generated certificate. 355 | """ 356 | if not self._clusterlib_obj.conway_genesis: 357 | msg = "Conway governance can be used only with Command era >= Conway." 358 | raise exceptions.CLIError(msg) 359 | 360 | destination_dir = pl.Path(destination_dir).expanduser() 361 | out_file = destination_dir / f"{addr_name}_vote_deleg.cert" 362 | clusterlib_helpers._check_files_exist(out_file, clusterlib_obj=self._clusterlib_obj) 363 | 364 | stake_args = self._get_stake_vkey_args( 365 | stake_vkey=stake_vkey, 366 | stake_vkey_file=stake_vkey_file, 367 | stake_script_file=stake_script_file, 368 | stake_address=stake_address, 369 | ) 370 | drep_args = self._get_drep_args( 371 | drep_script_hash=drep_script_hash, 372 | drep_vkey=drep_vkey, 373 | drep_vkey_file=drep_vkey_file, 374 | drep_key_hash=drep_key_hash, 375 | always_abstain=always_abstain, 376 | always_no_confidence=always_no_confidence, 377 | ) 378 | 379 | self._clusterlib_obj.cli( 380 | [ 381 | "stake-address", 382 | "vote-delegation-certificate", 383 | *stake_args, 384 | *drep_args, 385 | "--out-file", 386 | str(out_file), 387 | ] 388 | ) 389 | 390 | helpers._check_outfiles(out_file) 391 | return out_file 392 | 393 | def gen_stake_and_vote_delegation_cert( 394 | self, 395 | addr_name: str, 396 | stake_vkey: str = "", 397 | stake_vkey_file: itp.FileType | None = None, 398 | stake_script_file: itp.FileType | None = None, 399 | stake_address: str | None = None, 400 | stake_pool_vkey: str = "", 401 | cold_vkey_file: itp.FileType | None = None, 402 | stake_pool_id: str = "", 403 | drep_script_hash: str = "", 404 | drep_vkey: str = "", 405 | drep_vkey_file: itp.FileType | None = None, 406 | drep_key_hash: str = "", 407 | always_abstain: bool = False, 408 | always_no_confidence: bool = False, 409 | destination_dir: itp.FileType = ".", 410 | ) -> pl.Path: 411 | """Generate a stake address stake and vote delegation certificate. 412 | 413 | Args: 414 | addr_name: A name of stake address. 415 | stake_vkey: A stake vkey file (optional). 416 | stake_vkey_file: A path to corresponding stake vkey file (optional). 417 | stake_script_file: A path to corresponding stake script file (optional). 418 | stake_address: Stake address key, bech32 or hex-encoded (optional). 419 | stake_pool_vkey: A stake pool verification key (Bech32 or hex-encoded, optional). 420 | cold_vkey_file: A path to pool cold vkey file (optional). 421 | stake_pool_id: An ID of the stake pool (optional). 422 | (either Bech32-encoded or hex-encoded, optional). 423 | drep_script_hash: DRep script hash (hex-encoded, optional). 424 | drep_vkey: DRep verification key (Bech32 or hex-encoded, optional). 425 | drep_vkey_file: Filepath of the DRep verification key (optional). 426 | drep_key_hash: DRep verification key hash 427 | always_abstain: A bool indicating whether to delegate to always-abstain DRep (optional). 428 | always_no_confidence: A bool indicating whether to delegate to 429 | always-vote-no-confidence DRep (optional). 430 | destination_dir: A path to directory for storing artifacts (optional). 431 | 432 | Returns: 433 | Path: A path to the generated certificate. 434 | """ 435 | if not self._clusterlib_obj.conway_genesis: 436 | msg = "Conway governance can be used only with Command era >= Conway." 437 | raise exceptions.CLIError(msg) 438 | 439 | destination_dir = pl.Path(destination_dir).expanduser() 440 | out_file = destination_dir / f"{addr_name}_vote_deleg.cert" 441 | clusterlib_helpers._check_files_exist(out_file, clusterlib_obj=self._clusterlib_obj) 442 | 443 | stake_key_args = self._get_stake_vkey_args( 444 | stake_vkey=stake_vkey, 445 | stake_vkey_file=stake_vkey_file, 446 | stake_script_file=stake_script_file, 447 | stake_address=stake_address, 448 | ) 449 | pool_key_args = self._get_pool_key_args( 450 | stake_pool_vkey=stake_pool_vkey, 451 | cold_vkey_file=cold_vkey_file, 452 | stake_pool_id=stake_pool_id, 453 | ) 454 | drep_args = self._get_drep_args( 455 | drep_script_hash=drep_script_hash, 456 | drep_vkey=drep_vkey, 457 | drep_vkey_file=drep_vkey_file, 458 | drep_key_hash=drep_key_hash, 459 | always_abstain=always_abstain, 460 | always_no_confidence=always_no_confidence, 461 | ) 462 | 463 | self._clusterlib_obj.cli( 464 | [ 465 | "stake-address", 466 | "stake-and-vote-delegation-certificate", 467 | *stake_key_args, 468 | *pool_key_args, 469 | *drep_args, 470 | "--out-file", 471 | str(out_file), 472 | ] 473 | ) 474 | 475 | helpers._check_outfiles(out_file) 476 | return out_file 477 | 478 | def gen_stake_addr_and_keys( 479 | self, name: str, destination_dir: itp.FileType = "." 480 | ) -> structs.AddressRecord: 481 | """Generate stake address and key pair. 482 | 483 | Args: 484 | name: A name of the address and key pair. 485 | destination_dir: A path to directory for storing artifacts (optional). 486 | 487 | Returns: 488 | structs.AddressRecord: A data container containing the address 489 | and key pair / script file. 490 | """ 491 | key_pair = self.gen_stake_key_pair(key_name=name, destination_dir=destination_dir) 492 | addr = self.gen_stake_addr( 493 | addr_name=name, stake_vkey_file=key_pair.vkey_file, destination_dir=destination_dir 494 | ) 495 | 496 | return structs.AddressRecord( 497 | address=addr, vkey_file=key_pair.vkey_file, skey_file=key_pair.skey_file 498 | ) 499 | 500 | def get_stake_vkey_hash( 501 | self, 502 | stake_vkey_file: itp.FileType | None = None, 503 | stake_vkey: str | None = None, 504 | ) -> str: 505 | """Return the hash of a stake address key. 506 | 507 | Args: 508 | stake_vkey_file: A path to stake vkey file (optional). 509 | stake_vkey: A stake vkey (Bech32, optional). 510 | 511 | Returns: 512 | str: A generated hash. 513 | """ 514 | if stake_vkey: 515 | cli_args = ["--stake-verification-key", stake_vkey] 516 | elif stake_vkey_file: 517 | cli_args = ["--stake-verification-key-file", str(stake_vkey_file)] 518 | else: 519 | msg = "Either `stake_vkey` or `stake_vkey_file` is needed." 520 | raise ValueError(msg) 521 | 522 | return ( 523 | self._clusterlib_obj.cli(["stake-address", "key-hash", *cli_args]) 524 | .stdout.rstrip() 525 | .decode("ascii") 526 | ) 527 | 528 | def withdraw_reward( 529 | self, 530 | stake_addr_record: structs.AddressRecord, 531 | dst_addr_record: structs.AddressRecord, 532 | tx_name: str, 533 | verify: bool = True, 534 | destination_dir: itp.FileType = ".", 535 | ) -> structs.TxRawOutput: 536 | """Withdraw reward to payment address. 537 | 538 | Args: 539 | stake_addr_record: A `structs.AddressRecord` data container for the stake address 540 | with reward. 541 | dst_addr_record: A `structs.AddressRecord` data container for the destination 542 | payment address. 543 | tx_name: A name of the transaction. 544 | verify: A bool indicating whether to verify that the reward was transferred correctly. 545 | destination_dir: A path to directory for storing artifacts (optional). 546 | """ 547 | dst_address = dst_addr_record.address 548 | src_init_balance = self._clusterlib_obj.g_query.get_address_balance(dst_address) 549 | 550 | tx_files_withdrawal = structs.TxFiles( 551 | signing_key_files=[dst_addr_record.skey_file, stake_addr_record.skey_file], 552 | ) 553 | 554 | tx_raw_withdrawal_output = self._clusterlib_obj.g_transaction.send_tx( 555 | src_address=dst_address, 556 | tx_name=f"{tx_name}_reward_withdrawal", 557 | tx_files=tx_files_withdrawal, 558 | withdrawals=[structs.TxOut(address=stake_addr_record.address, amount=-1)], 559 | destination_dir=destination_dir, 560 | ) 561 | 562 | if not verify: 563 | return tx_raw_withdrawal_output 564 | 565 | # Check that reward is 0 566 | if ( 567 | self._clusterlib_obj.g_query.get_stake_addr_info( 568 | stake_addr_record.address 569 | ).reward_account_balance 570 | != 0 571 | ): 572 | msg = "Not all rewards were transferred." 573 | raise exceptions.CLIError(msg) 574 | 575 | # Check that rewards were transferred 576 | src_reward_balance = self._clusterlib_obj.g_query.get_address_balance(dst_address) 577 | if ( 578 | src_reward_balance 579 | != src_init_balance 580 | - tx_raw_withdrawal_output.fee 581 | + tx_raw_withdrawal_output.withdrawals[0].amount # type: ignore 582 | ): 583 | msg = f"Incorrect balance for destination address `{dst_address}`." 584 | raise exceptions.CLIError(msg) 585 | 586 | return tx_raw_withdrawal_output 587 | 588 | def __repr__(self) -> str: 589 | return f"<{self.__class__.__name__}: clusterlib_obj={id(self._clusterlib_obj)}>" 590 | --------------------------------------------------------------------------------