├── .github ├── dependabot.yml └── workflows │ ├── codeql.yml │ └── repo_lint.yaml ├── .gitignore ├── .markdownlint.yaml ├── .pre-commit-config.yaml ├── .readthedocs.yaml ├── CODE-OF-CONDUCT.md ├── LICENSE ├── Makefile ├── README.md ├── cardano_clusterlib ├── __init__.py ├── address_group.py ├── clusterlib.py ├── clusterlib_helpers.py ├── clusterlib_klass.py ├── consts.py ├── exceptions.py ├── genesis_group.py ├── gov_action_group.py ├── gov_committee_group.py ├── gov_drep_group.py ├── gov_group.py ├── gov_vote_group.py ├── helpers.py ├── key_group.py ├── legacy_gov_group.py ├── node_group.py ├── py.typed ├── query_group.py ├── stake_address_group.py ├── stake_pool_group.py ├── structs.py ├── transaction_group.py ├── txtools.py └── types.py ├── docs ├── Makefile ├── requirements.txt └── source │ ├── cardano_clusterlib.rst │ ├── conf.py │ ├── index.rst │ ├── modules.rst │ └── readme.rst ├── pyproject.toml ├── requirements-dev.txt ├── setup.cfg ├── setup.py └── upgrading └── refactor_to_0_4_0rc1.sed /.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 | -------------------------------------------------------------------------------- /.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@v4 32 | 33 | # Initializes the CodeQL tools for scanning. 34 | - name: Initialize CodeQL 35 | uses: github/codeql-action/init@v3 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@v3 47 | with: 48 | category: "/language:${{matrix.language}}" 49 | -------------------------------------------------------------------------------- /.github/workflows/repo_lint.yaml: -------------------------------------------------------------------------------- 1 | name: repo_lint 2 | 3 | on: 4 | pull_request: 5 | branches: 6 | - master 7 | 8 | jobs: 9 | lint: 10 | 11 | runs-on: ubuntu-latest 12 | 13 | steps: 14 | - name: Check out repository 15 | uses: actions/checkout@v4 16 | - name: Set up python 17 | id: setup-python 18 | uses: actions/setup-python@v5 19 | with: 20 | python-version: '3.11' 21 | - name: Install dependencies 22 | run: make install 23 | - name: Load cached pre-commit env 24 | id: cached-pre-commit 25 | uses: actions/cache@v4 26 | with: 27 | path: ~/.cache/pre-commit 28 | key: pre-commit-${{ runner.os }}-${{ steps.setup-python.outputs.python-version }}-${{ hashFiles('.pre-commit-config.yaml') }} 29 | - name: Install pre-commit hooks 30 | run: | 31 | mkdir -p ~/.cache/pre-commit 32 | true > ~/.cache/pre-commit/pre-commit.log 33 | pre-commit install-hooks --color=always 34 | retval="$?" 35 | if [ "$retval" -ne 0 ]; then 36 | cat ~/.cache/pre-commit/pre-commit.log 37 | fi 38 | exit "$retval" 39 | - name: Run pre-commit linters 40 | run: pre-commit run -a --show-diff-on-failure --color=always 41 | -------------------------------------------------------------------------------- /.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 | -------------------------------------------------------------------------------- /.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 | -------------------------------------------------------------------------------- /.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: https://github.com/charliermarsh/ruff-pre-commit 16 | rev: v0.9.2 17 | hooks: 18 | - id: ruff 19 | args: [ --fix ] 20 | - id: ruff-format 21 | - repo: https://github.com/shellcheck-py/shellcheck-py 22 | rev: v0.10.0.1 23 | hooks: 24 | - id: shellcheck 25 | - repo: https://github.com/igorshubovych/markdownlint-cli 26 | rev: v0.43.0 27 | hooks: 28 | - id: markdownlint 29 | - repo: local 30 | hooks: 31 | - id: pyrefly 32 | name: pyrefly 33 | entry: pyrefly 34 | args: [ check, --remove-unused-ignores ] 35 | pass_filenames: false 36 | language: system 37 | types: [python] 38 | - id: mypy 39 | name: mypy 40 | entry: mypy 41 | language: system 42 | types: [python] 43 | -------------------------------------------------------------------------------- /.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 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | .PHONY: install 2 | install: 3 | python3 -m pip install --upgrade pip 4 | python3 -m pip install --upgrade wheel 5 | python3 -m pip install --upgrade --upgrade-strategy eager -r requirements-dev.txt $(PIP_INSTALL_ARGS) 6 | virtualenv --upgrade-embed-wheels 7 | 8 | .PHONY: .install_doc 9 | .install_doc: 10 | python3 -m pip install --upgrade --upgrade-strategy eager -r docs/requirements.txt 11 | 12 | # run linters 13 | .PHONY: lint 14 | lint: 15 | pre-commit run -a 16 | if command -v pytype >/dev/null 2>&1; then pytype -k -j auto cardano_clusterlib; fi 17 | 18 | # build package 19 | .PHONY: build 20 | build: 21 | python3 -m build 22 | 23 | # upload package to PyPI 24 | .PHONY: upload 25 | upload: 26 | if ! type twine >/dev/null 2>&1; then python3 -m pip install --upgrade twine; fi 27 | twine upload --skip-existing dist/* 28 | 29 | # release package to PyPI 30 | .PHONY: release 31 | release: build upload 32 | 33 | .PHONY: .docs_build_dir 34 | .docs_build_dir: 35 | mkdir -p docs/build 36 | 37 | # generate sphinx documentation 38 | .PHONY: doc 39 | doc: .docs_build_dir .install_doc 40 | $(MAKE) -C docs clean 41 | $(MAKE) -C docs html 42 | -------------------------------------------------------------------------------- /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/__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 | -------------------------------------------------------------------------------- /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 AssertionError(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 AssertionError(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/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 MAINNET_MAGIC 12 | from cardano_clusterlib.consts import MultiSigTypeArgs 13 | from cardano_clusterlib.consts import MultiSlotTypeArgs 14 | from cardano_clusterlib.consts import ScriptTypes 15 | from cardano_clusterlib.consts import Votes 16 | from cardano_clusterlib.exceptions import CLIError 17 | from cardano_clusterlib.helpers import get_rand_str 18 | from cardano_clusterlib.helpers import read_address_from_file 19 | from cardano_clusterlib.structs import ActionConstitution 20 | from cardano_clusterlib.structs import ActionHardfork 21 | from cardano_clusterlib.structs import ActionInfo 22 | from cardano_clusterlib.structs import ActionNoConfidence 23 | from cardano_clusterlib.structs import ActionPParamsUpdate 24 | from cardano_clusterlib.structs import ActionTreasuryWithdrawal 25 | from cardano_clusterlib.structs import ActionUpdateCommittee 26 | from cardano_clusterlib.structs import AddressInfo 27 | from cardano_clusterlib.structs import AddressRecord 28 | from cardano_clusterlib.structs import CCMember 29 | from cardano_clusterlib.structs import CLIOut 30 | from cardano_clusterlib.structs import ColdKeyPair 31 | from cardano_clusterlib.structs import ComplexCert 32 | from cardano_clusterlib.structs import ComplexProposal 33 | from cardano_clusterlib.structs import DataForBuild 34 | from cardano_clusterlib.structs import GenesisKeys 35 | from cardano_clusterlib.structs import KeyPair 36 | from cardano_clusterlib.structs import LeadershipSchedule 37 | from cardano_clusterlib.structs import Mint 38 | from cardano_clusterlib.structs import OptionalMint 39 | from cardano_clusterlib.structs import OptionalScriptCerts 40 | from cardano_clusterlib.structs import OptionalScriptProposals 41 | from cardano_clusterlib.structs import OptionalScriptTxIn 42 | from cardano_clusterlib.structs import OptionalScriptVotes 43 | from cardano_clusterlib.structs import OptionalScriptWithdrawals 44 | from cardano_clusterlib.structs import OptionalTxOuts 45 | from cardano_clusterlib.structs import OptionalUTXOData 46 | from cardano_clusterlib.structs import PoolCreationOutput 47 | from cardano_clusterlib.structs import PoolData 48 | from cardano_clusterlib.structs import PoolParamsTop 49 | from cardano_clusterlib.structs import PoolUser 50 | from cardano_clusterlib.structs import ScriptTxIn 51 | from cardano_clusterlib.structs import ScriptVote 52 | from cardano_clusterlib.structs import ScriptWithdrawal 53 | from cardano_clusterlib.structs import StakeAddrInfo 54 | from cardano_clusterlib.structs import TxFiles 55 | from cardano_clusterlib.structs import TxOut 56 | from cardano_clusterlib.structs import TxRawOutput 57 | from cardano_clusterlib.structs import UTXOData 58 | from cardano_clusterlib.structs import Value 59 | from cardano_clusterlib.structs import VoteCC 60 | from cardano_clusterlib.structs import VoteDrep 61 | from cardano_clusterlib.structs import VoteSPO 62 | from cardano_clusterlib.txtools import calculate_utxos_balance 63 | from cardano_clusterlib.txtools import collect_data_for_build 64 | from cardano_clusterlib.txtools import filter_utxo_with_highest_amount 65 | from cardano_clusterlib.txtools import filter_utxos 66 | from cardano_clusterlib.types import FileType 67 | -------------------------------------------------------------------------------- /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 AssertionError(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/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 | # pyrefly: ignore # missing-attribute 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 | # pyrefly: ignore # bad-specialization, bad-argument-type, not-a-type 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 | for __ in range(3): 271 | retcode = None 272 | with subprocess.Popen( 273 | cli_args_strs, stdout=subprocess.PIPE, stderr=subprocess.PIPE 274 | ) as p: 275 | stdout, stderr = p.communicate(timeout=timeout) 276 | retcode = p.returncode 277 | 278 | if retcode == 0: 279 | break 280 | 281 | # pyrefly: ignore # missing-attribute 282 | stderr_dec = stderr.decode() 283 | err_msg = ( 284 | f"An error occurred running a CLI command `{cmd_str}` on path " 285 | f"`{pl.Path.cwd()}`: {stderr_dec}" 286 | ) 287 | if "resource exhausted" in stderr_dec or "resource vanished" in stderr_dec: 288 | LOGGER.error(err_msg) 289 | time.sleep(0.4) 290 | continue 291 | raise exceptions.CLIError(err_msg) 292 | else: 293 | raise exceptions.CLIError(err_msg) 294 | 295 | # pyrefly: ignore # bad-argument-type 296 | return structs.CLIOut(stdout or b"", stderr or b"") 297 | 298 | def refresh_pparams_file(self) -> None: 299 | """Refresh protocol parameters file.""" 300 | self.g_query.query_cli(["protocol-parameters", "--out-file", str(self.pparams_file)]) 301 | 302 | def create_pparams_file(self) -> None: 303 | """Create protocol parameters file if it doesn't exist.""" 304 | if self.pparams_file.exists(): 305 | return 306 | self.refresh_pparams_file() 307 | 308 | def wait_for_new_block(self, new_blocks: int = 1) -> int: 309 | """Wait for new block(s) to be created. 310 | 311 | Args: 312 | new_blocks: A number of new blocks to wait for (optional). 313 | 314 | Returns: 315 | int: A block number of last added block. 316 | """ 317 | initial_tip = self.g_query.get_tip() 318 | initial_block = int(initial_tip["block"]) 319 | 320 | if new_blocks < 1: 321 | return initial_block 322 | 323 | return clusterlib_helpers.wait_for_block( 324 | clusterlib_obj=self, tip=initial_tip, block_no=initial_block + new_blocks 325 | ) 326 | 327 | def wait_for_block(self, block: int) -> int: 328 | """Wait for block number. 329 | 330 | Args: 331 | block: A block number to wait for. 332 | 333 | Returns: 334 | int: A block number of last added block. 335 | """ 336 | return clusterlib_helpers.wait_for_block( 337 | clusterlib_obj=self, tip=self.g_query.get_tip(), block_no=block 338 | ) 339 | 340 | def wait_for_slot(self, slot: int) -> int: 341 | """Wait for slot number. 342 | 343 | Args: 344 | slot: A slot number to wait for. 345 | 346 | Returns: 347 | int: A slot number of last block. 348 | """ 349 | min_sleep = 1.5 # in sec 350 | long_sleep = 15 # in sec 351 | no_block_time = 0 # in slots 352 | next_block_timeout = 300 # in slots 353 | last_slot = -1 354 | printed = False 355 | for __ in range(100): 356 | this_slot = self.g_query.get_slot_no() 357 | 358 | slots_diff = slot - this_slot 359 | if slots_diff <= 0: 360 | return this_slot 361 | 362 | if this_slot == last_slot: 363 | if no_block_time >= next_block_timeout: 364 | msg = f"Failed to wait for slot number {slot}, no new blocks are being created." 365 | raise exceptions.CLIError(msg) 366 | else: 367 | no_block_time = 0 368 | 369 | _sleep_time = slots_diff * self.slot_length 370 | sleep_time = max(min_sleep, _sleep_time) 371 | 372 | if not printed and sleep_time > long_sleep: 373 | LOGGER.info(f"Waiting for {sleep_time:.2f} sec for slot no {slot}.") 374 | printed = True 375 | 376 | last_slot = this_slot 377 | no_block_time += slots_diff 378 | time.sleep(sleep_time) 379 | 380 | msg = f"Failed to wait for slot number {slot}." 381 | raise exceptions.CLIError(msg) 382 | 383 | def wait_for_new_epoch(self, new_epochs: int = 1, padding_seconds: int = 0) -> int: 384 | """Wait for new epoch(s). 385 | 386 | Args: 387 | new_epochs: A number of new epochs to wait for (optional). 388 | padding_seconds: A number of additional seconds to wait for (optional). 389 | 390 | Returns: 391 | int: The current epoch. 392 | """ 393 | start_tip = self.g_query.get_tip() 394 | start_epoch = int(start_tip["epoch"]) 395 | 396 | if new_epochs < 1: 397 | return start_epoch 398 | 399 | epoch_no = start_epoch + new_epochs 400 | return clusterlib_helpers.wait_for_epoch( 401 | clusterlib_obj=self, tip=start_tip, epoch_no=epoch_no, padding_seconds=padding_seconds 402 | ) 403 | 404 | def wait_for_epoch( 405 | self, epoch_no: int, padding_seconds: int = 0, future_is_ok: bool = True 406 | ) -> int: 407 | """Wait for epoch no. 408 | 409 | Args: 410 | epoch_no: A number of epoch to wait for. 411 | padding_seconds: A number of additional seconds to wait for (optional). 412 | future_is_ok: A bool indicating whether current epoch > `epoch_no` is acceptable 413 | (default: True). 414 | 415 | Returns: 416 | int: The current epoch. 417 | """ 418 | return clusterlib_helpers.wait_for_epoch( 419 | clusterlib_obj=self, 420 | tip=self.g_query.get_tip(), 421 | epoch_no=epoch_no, 422 | padding_seconds=padding_seconds, 423 | future_is_ok=future_is_ok, 424 | ) 425 | 426 | def time_to_epoch_end(self, tip: dict | None = None) -> float: 427 | """How many seconds to go to start of a new epoch.""" 428 | tip = tip or self.g_query.get_tip() 429 | epoch = int(tip["epoch"]) 430 | slot = int(tip["slot"]) 431 | slots_to_go = (epoch + 1) * self.epoch_length - (slot + self.slots_offset - 1) 432 | return float(slots_to_go * self.slot_length) 433 | 434 | def time_from_epoch_start(self, tip: dict | None = None) -> float: 435 | """How many seconds passed from start of the current epoch.""" 436 | s_to_epoch_stop = self.time_to_epoch_end(tip=tip) 437 | return float(self.epoch_length_sec - s_to_epoch_stop) 438 | 439 | def __repr__(self) -> str: 440 | return f"<{self.__class__.__name__}: command_era={self.command_era}>" 441 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /cardano_clusterlib/exceptions.py: -------------------------------------------------------------------------------- 1 | class CLIError(Exception): 2 | pass 3 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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 AssertionError(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 AssertionError(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 AssertionError(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 | -------------------------------------------------------------------------------- /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 AssertionError(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 AssertionError(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 AssertionError(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/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 AssertionError(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 | -------------------------------------------------------------------------------- /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 AssertionError(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 AssertionError(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 AssertionError(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 AssertionError(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 AssertionError(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 | -------------------------------------------------------------------------------- /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_address_from_file(addr_file: itp.FileType) -> str: 18 | """Read address stored in file.""" 19 | with open(pl.Path(addr_file).expanduser(), encoding="utf-8") as in_file: 20 | return in_file.read().strip() 21 | 22 | 23 | def _prepend_flag(flag: str, contents: itp.UnpackableSequence) -> list[str]: 24 | """Prepend flag to every item of the sequence. 25 | 26 | Args: 27 | flag: A flag to prepend to every item of the `contents`. 28 | contents: A list (iterable) of content to be prepended. 29 | 30 | Returns: 31 | list[str]: A list of flag followed by content, see below. 32 | 33 | >>> ClusterLib._prepend_flag(None, "--foo", [1, 2, 3]) 34 | ['--foo', '1', '--foo', '2', '--foo', '3'] 35 | """ 36 | return list(itertools.chain.from_iterable([flag, str(x)] for x in contents)) 37 | 38 | 39 | def _check_outfiles(*out_files: itp.FileType) -> None: 40 | """Check that the expected output files were created. 41 | 42 | Args: 43 | *out_files: Variable length list of expected output files. 44 | """ 45 | for out_file in out_files: 46 | out_file_p = pl.Path(out_file).expanduser() 47 | if not out_file_p.exists(): 48 | msg = f"The expected file `{out_file}` doesn't exist." 49 | raise exceptions.CLIError(msg) 50 | 51 | 52 | def _maybe_path(file: itp.FileType | None) -> pl.Path | None: 53 | """Return `Path` if `file` is thruthy.""" 54 | return pl.Path(file) if file else None 55 | -------------------------------------------------------------------------------- /cardano_clusterlib/key_group.py: -------------------------------------------------------------------------------- 1 | """Group of methods for working with key 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 types as itp 9 | 10 | LOGGER = logging.getLogger(__name__) 11 | 12 | 13 | class KeyGroup: 14 | def __init__(self, clusterlib_obj: "itp.ClusterLib") -> None: 15 | self._clusterlib_obj = clusterlib_obj 16 | 17 | def gen_verification_key( 18 | self, 19 | key_name: str, 20 | signing_key_file: itp.FileType, 21 | destination_dir: itp.FileType = ".", 22 | ) -> pl.Path: 23 | """Generate a verification file from a signing key. 24 | 25 | Args: 26 | key_name: A name of the key. 27 | signing_key_file: A path to signing key file. 28 | destination_dir: A path to directory for storing artifacts (optional). 29 | 30 | Returns: 31 | Path: A path to the generated verification key file. 32 | """ 33 | destination_dir = pl.Path(destination_dir).expanduser() 34 | out_file = destination_dir / f"{key_name}.vkey" 35 | clusterlib_helpers._check_files_exist(out_file, clusterlib_obj=self._clusterlib_obj) 36 | 37 | self._clusterlib_obj.cli( 38 | [ 39 | "key", 40 | "verification-key", 41 | "--signing-key-file", 42 | str(signing_key_file), 43 | "--verification-key-file", 44 | str(out_file), 45 | ] 46 | ) 47 | 48 | helpers._check_outfiles(out_file) 49 | return out_file 50 | 51 | def gen_non_extended_verification_key( 52 | self, 53 | key_name: str, 54 | extended_verification_key_file: itp.FileType, 55 | destination_dir: itp.FileType = ".", 56 | ) -> pl.Path: 57 | """Generate a non-extended key from a verification key. 58 | 59 | Args: 60 | key_name: A name of the key. 61 | extended_verification_key_file: A path to the extended verification key file. 62 | destination_dir: A path to directory for storing artifacts (optional). 63 | 64 | Returns: 65 | Path: A path to the generated non-extended verification key file. 66 | """ 67 | destination_dir = pl.Path(destination_dir).expanduser() 68 | out_file = destination_dir / f"{key_name}.vkey" 69 | clusterlib_helpers._check_files_exist(out_file, clusterlib_obj=self._clusterlib_obj) 70 | 71 | self._clusterlib_obj.cli( 72 | [ 73 | "key", 74 | "non-extended-key", 75 | "--extended-verification-key-file", 76 | str(extended_verification_key_file), 77 | "--verification-key-file", 78 | str(out_file), 79 | ] 80 | ) 81 | 82 | helpers._check_outfiles(out_file) 83 | return out_file 84 | 85 | def __repr__(self) -> str: 86 | return f"<{self.__class__.__name__}: clusterlib_obj={id(self._clusterlib_obj)}>" 87 | -------------------------------------------------------------------------------- /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/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 | -------------------------------------------------------------------------------- /cardano_clusterlib/py.typed: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/input-output-hk/cardano-clusterlib-py/655a67d682625d1f8788ce104538c94e15cbb373/cardano_clusterlib/py.typed -------------------------------------------------------------------------------- /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 AssertionError(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 AssertionError(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 AssertionError(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 AssertionError(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 AssertionError(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 | -------------------------------------------------------------------------------- /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 AssertionError(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 | destination_dir: itp.FileType = ".", 181 | ) -> structs.PoolCreationOutput: 182 | """Create and register a stake pool. 183 | 184 | Args: 185 | pool_data: A `structs.PoolData` data container containing info about the stake pool. 186 | pool_owners: A list of `structs.PoolUser` structures containing pool user addresses 187 | and keys. 188 | tx_name: A name of the transaction. 189 | destination_dir: A path to directory for storing artifacts (optional). 190 | 191 | Returns: 192 | structs.PoolCreationOutput: A data container containing pool creation output. 193 | """ 194 | # Create the KES key pair 195 | node_kes = self._clusterlib_obj.g_node.gen_kes_key_pair( 196 | node_name=pool_data.pool_name, 197 | destination_dir=destination_dir, 198 | ) 199 | LOGGER.debug(f"KES keys created - {node_kes.vkey_file}; {node_kes.skey_file}") 200 | 201 | # Create the VRF key pair 202 | node_vrf = self._clusterlib_obj.g_node.gen_vrf_key_pair( 203 | node_name=pool_data.pool_name, 204 | destination_dir=destination_dir, 205 | ) 206 | LOGGER.debug(f"VRF keys created - {node_vrf.vkey_file}; {node_vrf.skey_file}") 207 | 208 | # Create the cold key pair and node operational certificate counter 209 | node_cold = self._clusterlib_obj.g_node.gen_cold_key_pair_and_counter( 210 | node_name=pool_data.pool_name, 211 | destination_dir=destination_dir, 212 | ) 213 | LOGGER.debug( 214 | "Cold keys created and counter created - " 215 | f"{node_cold.vkey_file}; {node_cold.skey_file}; {node_cold.counter_file}" 216 | ) 217 | 218 | pool_reg_cert_file, tx_raw_output = self.register_stake_pool( 219 | pool_data=pool_data, 220 | pool_owners=pool_owners, 221 | vrf_vkey_file=node_vrf.vkey_file, 222 | cold_key_pair=node_cold, 223 | tx_name=tx_name, 224 | destination_dir=destination_dir, 225 | ) 226 | 227 | return structs.PoolCreationOutput( 228 | stake_pool_id=self.get_stake_pool_id(node_cold.vkey_file), 229 | vrf_key_pair=node_vrf, 230 | cold_key_pair=node_cold, 231 | pool_reg_cert_file=pool_reg_cert_file, 232 | pool_data=pool_data, 233 | pool_owners=pool_owners, 234 | tx_raw_output=tx_raw_output, 235 | kes_key_pair=node_kes, 236 | ) 237 | 238 | def register_stake_pool( 239 | self, 240 | pool_data: structs.PoolData, 241 | pool_owners: list[structs.PoolUser], 242 | vrf_vkey_file: itp.FileType, 243 | cold_key_pair: structs.ColdKeyPair, 244 | tx_name: str, 245 | reward_account_vkey_file: itp.FileType | None = None, 246 | deposit: int | None = None, 247 | destination_dir: itp.FileType = ".", 248 | ) -> tuple[pl.Path, structs.TxRawOutput]: 249 | """Register a stake pool. 250 | 251 | Args: 252 | pool_data: A `structs.PoolData` data container containing info about the stake pool. 253 | pool_owners: A list of `structs.PoolUser` structures containing pool user addresses 254 | and keys. 255 | vrf_vkey_file: A path to node VRF vkey file. 256 | cold_key_pair: A `structs.ColdKeyPair` data container containing the key pair 257 | and the counter. 258 | tx_name: A name of the transaction. 259 | reward_account_vkey_file: A path to reward account vkey file (optional). 260 | deposit: A deposit amount needed by the transaction (optional). 261 | destination_dir: A path to directory for storing artifacts (optional). 262 | 263 | Returns: 264 | tuple[Path, structs.TxRawOutput]: A tuple with pool registration cert file and 265 | transaction output details. 266 | """ 267 | tx_name = f"{tx_name}_reg_pool" 268 | pool_reg_cert_file = self.gen_pool_registration_cert( 269 | pool_data=pool_data, 270 | vrf_vkey_file=vrf_vkey_file, 271 | cold_vkey_file=cold_key_pair.vkey_file, 272 | owner_stake_vkey_files=[p.stake.vkey_file for p in pool_owners], 273 | reward_account_vkey_file=reward_account_vkey_file, 274 | destination_dir=destination_dir, 275 | ) 276 | 277 | # Submit the pool registration certificate through a tx 278 | tx_files = structs.TxFiles( 279 | certificate_files=[pool_reg_cert_file], 280 | signing_key_files=[ 281 | *[p.payment.skey_file for p in pool_owners], 282 | *[p.stake.skey_file for p in pool_owners], 283 | cold_key_pair.skey_file, 284 | ], 285 | ) 286 | 287 | tx_raw_output = self._clusterlib_obj.g_transaction.send_tx( 288 | src_address=pool_owners[0].payment.address, 289 | tx_name=tx_name, 290 | tx_files=tx_files, 291 | deposit=deposit, 292 | destination_dir=destination_dir, 293 | ) 294 | 295 | return pool_reg_cert_file, tx_raw_output 296 | 297 | def deregister_stake_pool( 298 | self, 299 | pool_owners: list[structs.PoolUser], 300 | cold_key_pair: structs.ColdKeyPair, 301 | epoch: int, 302 | pool_name: str, 303 | tx_name: str, 304 | destination_dir: itp.FileType = ".", 305 | ) -> tuple[pl.Path, structs.TxRawOutput]: 306 | """Deregister a stake pool. 307 | 308 | Args: 309 | pool_owners: A list of `structs.PoolUser` structures containing pool user addresses 310 | and keys. 311 | cold_key_pair: A `structs.ColdKeyPair` data container containing the key pair 312 | and the counter. 313 | epoch: An epoch where the update proposal will take effect (optional). 314 | pool_name: A name of the stake pool. 315 | tx_name: A name of the transaction. 316 | destination_dir: A path to directory for storing artifacts (optional). 317 | 318 | Returns: 319 | tuple[Path, structs.TxRawOutput]: A data container with pool registration cert file and 320 | transaction output details. 321 | """ 322 | tx_name = f"{tx_name}_dereg_pool" 323 | LOGGER.debug( 324 | f"Deregistering stake pool starting with epoch: {epoch}; " 325 | f"Current epoch is: {self._clusterlib_obj.g_query.get_epoch()}" 326 | ) 327 | pool_dereg_cert_file = self.gen_pool_deregistration_cert( 328 | pool_name=pool_name, 329 | cold_vkey_file=cold_key_pair.vkey_file, 330 | epoch=epoch, 331 | destination_dir=destination_dir, 332 | ) 333 | 334 | # Submit the pool deregistration certificate through a tx 335 | tx_files = structs.TxFiles( 336 | certificate_files=[pool_dereg_cert_file], 337 | signing_key_files=[ 338 | *[p.payment.skey_file for p in pool_owners], 339 | *[p.stake.skey_file for p in pool_owners], 340 | cold_key_pair.skey_file, 341 | ], 342 | ) 343 | 344 | tx_raw_output = self._clusterlib_obj.g_transaction.send_tx( 345 | src_address=pool_owners[0].payment.address, 346 | tx_name=tx_name, 347 | tx_files=tx_files, 348 | destination_dir=destination_dir, 349 | ) 350 | 351 | return pool_dereg_cert_file, tx_raw_output 352 | 353 | def __repr__(self) -> str: 354 | return f"<{self.__class__.__name__}: clusterlib_obj={id(self._clusterlib_obj)}>" 355 | -------------------------------------------------------------------------------- /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 | 44 | def __bool__(self) -> bool: 45 | return bool(self.address) 46 | 47 | 48 | @dataclasses.dataclass(frozen=True, order=True) 49 | class UTXOData: 50 | utxo_hash: str 51 | utxo_ix: int 52 | amount: int 53 | address: str 54 | coin: str = consts.DEFAULT_COIN 55 | decoded_coin: str = "" 56 | datum_hash: str = "" 57 | inline_datum_hash: str = "" 58 | inline_datum: str | dict | None = None 59 | reference_script: dict | None = None 60 | 61 | 62 | @dataclasses.dataclass(frozen=True, order=True) 63 | class TxOut: 64 | address: str 65 | amount: int 66 | coin: str = consts.DEFAULT_COIN 67 | datum_hash: str = "" 68 | datum_hash_file: itp.FileType = "" 69 | datum_hash_cbor_file: itp.FileType = "" 70 | datum_hash_value: str = "" 71 | datum_embed_file: itp.FileType = "" 72 | datum_embed_cbor_file: itp.FileType = "" 73 | datum_embed_value: str = "" 74 | inline_datum_file: itp.FileType = "" 75 | inline_datum_cbor_file: itp.FileType = "" 76 | inline_datum_value: str = "" 77 | reference_script_file: itp.FileType = "" 78 | 79 | 80 | # List of `TxOut`s, empty list, or empty tuple 81 | OptionalTxOuts = list[TxOut] | tuple[()] 82 | # List of `UTXOData`s, empty list, or empty tuple 83 | OptionalUTXOData = list[UTXOData] | tuple[()] 84 | 85 | 86 | @dataclasses.dataclass(frozen=True, order=True) 87 | class ScriptTxIn: 88 | """Data structure for Tx inputs that are combined with scripts (simple or Plutus).""" 89 | 90 | txins: list[UTXOData] 91 | script_file: itp.FileType = "" 92 | reference_txin: UTXOData | None = None 93 | reference_type: str = "" 94 | # Values below needed only when working with Plutus 95 | collaterals: OptionalUTXOData = () 96 | execution_units: tp.Optional[tuple[int, int]] = None 97 | datum_file: itp.FileType = "" 98 | datum_cbor_file: itp.FileType = "" 99 | datum_value: str = "" 100 | inline_datum_present: bool = False 101 | redeemer_file: itp.FileType = "" 102 | redeemer_cbor_file: itp.FileType = "" 103 | redeemer_value: str = "" 104 | 105 | 106 | @dataclasses.dataclass(frozen=True, order=True) 107 | class ScriptWithdrawal: 108 | """Data structure for withdrawals that are combined with Plutus scripts.""" 109 | 110 | txout: TxOut 111 | script_file: itp.FileType = "" 112 | reference_txin: UTXOData | None = None 113 | reference_type: str = "" 114 | collaterals: OptionalUTXOData = () 115 | execution_units: tp.Optional[tuple[int, int]] = None 116 | redeemer_file: itp.FileType = "" 117 | redeemer_cbor_file: itp.FileType = "" 118 | redeemer_value: str = "" 119 | 120 | 121 | @dataclasses.dataclass(frozen=True, order=True) 122 | class ComplexCert: 123 | """Data structure for certificates with optional data for Plutus scripts. 124 | 125 | If used for one certificate, it needs to be used for all the other certificates in a given 126 | transaction (instead of `TxFiles.certificate_files`). Otherwise, order of certificates 127 | cannot be guaranteed. 128 | """ 129 | 130 | certificate_file: itp.FileType 131 | script_file: itp.FileType = "" 132 | reference_txin: UTXOData | None = None 133 | reference_type: str = "" 134 | collaterals: OptionalUTXOData = () 135 | execution_units: tp.Optional[tuple[int, int]] = None 136 | redeemer_file: itp.FileType = "" 137 | redeemer_cbor_file: itp.FileType = "" 138 | redeemer_value: str = "" 139 | 140 | 141 | @dataclasses.dataclass(frozen=True, order=True) 142 | class ComplexProposal: 143 | """Data structure for proposal with optional data for Plutus scripts. 144 | 145 | If used for one proposal, it needs to be used for all the other proposals in a given 146 | transaction (instead of `TxFiles.proposal_files`). Otherwise, order of proposals 147 | cannot be guaranteed. 148 | """ 149 | 150 | proposal_file: itp.FileType 151 | script_file: itp.FileType = "" 152 | collaterals: OptionalUTXOData = () 153 | execution_units: tp.Optional[tuple[int, int]] = None 154 | redeemer_file: itp.FileType = "" 155 | redeemer_cbor_file: itp.FileType = "" 156 | redeemer_value: str = "" 157 | 158 | 159 | @dataclasses.dataclass(frozen=True, order=True) 160 | class ScriptVote: 161 | """Data structure for voting that are combined with scripts.""" 162 | 163 | vote_file: itp.FileType = "" 164 | script_file: itp.FileType = "" 165 | # Values below needed only when working with Plutus 166 | collaterals: OptionalUTXOData = () 167 | execution_units: tp.Optional[tuple[int, int]] = None 168 | redeemer_file: itp.FileType = "" 169 | redeemer_cbor_file: itp.FileType = "" 170 | redeemer_value: str = "" 171 | 172 | 173 | @dataclasses.dataclass(frozen=True, order=True) 174 | class Mint: 175 | txouts: list[TxOut] 176 | script_file: itp.FileType = "" 177 | reference_txin: UTXOData | None = None 178 | reference_type: str = "" 179 | policyid: str = "" 180 | # Values below needed only when working with Plutus 181 | collaterals: OptionalUTXOData = () 182 | execution_units: tp.Optional[tuple[int, int]] = None 183 | redeemer_file: itp.FileType = "" 184 | redeemer_cbor_file: itp.FileType = "" 185 | redeemer_value: str = "" 186 | 187 | 188 | # List of `ScriptTxIn`s, empty list, or empty tuple 189 | OptionalScriptTxIn = list[ScriptTxIn] | tuple[()] 190 | # List of `ComplexCert`s, empty list, or empty tuple 191 | OptionalScriptCerts = list[ComplexCert] | tuple[()] 192 | # List of `ComplexProposal`s, empty list, or empty tuple 193 | OptionalScriptProposals = list[ComplexProposal] | tuple[()] 194 | # List of `ScriptWithdrawal`s, empty list, or empty tuple 195 | OptionalScriptWithdrawals = list[ScriptWithdrawal] | tuple[()] 196 | # List of `Mint`s, empty list, or empty tuple 197 | OptionalMint = list[Mint] | tuple[()] 198 | # List of `ScriptVote`s, empty list, or empty tuple 199 | OptionalScriptVotes = list[ScriptVote] | tuple[()] 200 | 201 | 202 | @dataclasses.dataclass(frozen=True, order=True) 203 | class TxFiles: 204 | certificate_files: itp.OptionalFiles = () 205 | proposal_files: itp.OptionalFiles = () 206 | metadata_json_files: itp.OptionalFiles = () 207 | metadata_cbor_files: itp.OptionalFiles = () 208 | signing_key_files: itp.OptionalFiles = () 209 | auxiliary_script_files: itp.OptionalFiles = () 210 | vote_files: itp.OptionalFiles = () 211 | metadata_json_detailed_schema: bool = False 212 | 213 | 214 | @dataclasses.dataclass(frozen=True, order=True) 215 | class PoolUser: 216 | payment: AddressRecord 217 | stake: AddressRecord 218 | 219 | 220 | @dataclasses.dataclass(frozen=True, order=True) 221 | class PoolData: 222 | pool_name: str 223 | pool_pledge: int 224 | pool_cost: int 225 | pool_margin: float 226 | pool_metadata_url: str = "" 227 | pool_metadata_hash: str = "" 228 | pool_relay_dns: str = "" 229 | pool_relay_ipv4: str = "" 230 | pool_relay_port: int = 0 231 | 232 | 233 | @dataclasses.dataclass(frozen=True, order=True) 234 | class TxRawOutput: 235 | txins: list[UTXOData] # UTXOs used as inputs 236 | txouts: list[TxOut] # Tx outputs 237 | txouts_count: int # Final number of tx outputs after adding change address and joining outputs 238 | tx_files: TxFiles # Files needed for transaction building (certificates, signing keys, etc.) 239 | out_file: pl.Path # Output file path for the transaction body 240 | fee: int # Tx fee 241 | build_args: list[str] # Arguments that were passed to `cardano-cli transaction build*` 242 | era: str = "" # Era used for the transaction 243 | script_txins: OptionalScriptTxIn = () # Tx inputs that are combined with scripts 244 | script_withdrawals: OptionalScriptWithdrawals = () # Withdrawals that are combined with scripts 245 | script_votes: OptionalScriptVotes = () # Votes that are combined with scripts 246 | complex_certs: OptionalScriptCerts = () # Certificates that are combined with scripts 247 | complex_proposals: OptionalScriptProposals = () # Proposals that are combined with scripts 248 | mint: OptionalMint = () # Minting data (Tx outputs, script, etc.) 249 | invalid_hereafter: int | None = None # Validity interval upper bound 250 | invalid_before: int | None = None # Validity interval lower bound 251 | current_treasury_value: int | None = None # Current treasury value 252 | treasury_donation: int | None = None # Amount of funds that will be donated to treasury 253 | withdrawals: OptionalTxOuts = () # All withdrawals (including those combined with scripts) 254 | change_address: str = "" # Address for change 255 | return_collateral_txouts: OptionalTxOuts = () # Tx outputs for returning collateral 256 | total_collateral_amount: int | None = None # Total collateral amount 257 | readonly_reference_txins: OptionalUTXOData = () # Tx inputs for plutus script context 258 | script_valid: bool = True # Whether the plutus script is valid 259 | required_signers: itp.OptionalFiles = () # Signing keys required for the transaction 260 | # Hashes of signing keys that are required for the transaction 261 | required_signer_hashes: list[str] | tuple[()] = () 262 | combined_reference_txins: OptionalUTXOData = () # All reference tx inputs 263 | 264 | 265 | @dataclasses.dataclass(frozen=True, order=True) 266 | class PoolCreationOutput: 267 | stake_pool_id: str 268 | vrf_key_pair: KeyPair 269 | cold_key_pair: ColdKeyPair 270 | pool_reg_cert_file: pl.Path 271 | pool_data: PoolData 272 | pool_owners: list[PoolUser] 273 | tx_raw_output: TxRawOutput 274 | kes_key_pair: KeyPair | None = None 275 | 276 | 277 | @dataclasses.dataclass(frozen=True, order=True) 278 | class GenesisKeys: 279 | genesis_utxo_vkey: pl.Path 280 | genesis_utxo_skey: pl.Path 281 | genesis_vkeys: list[pl.Path] 282 | delegate_skeys: list[pl.Path] 283 | 284 | 285 | @dataclasses.dataclass(frozen=True, order=True) 286 | class PoolParamsTop: 287 | pool_params: dict 288 | future_pool_params: dict 289 | retiring: int | None 290 | 291 | 292 | @dataclasses.dataclass(frozen=True, order=True) 293 | class AddressInfo: 294 | address: str 295 | era: str 296 | encoding: str 297 | type: str 298 | base16: str 299 | 300 | 301 | @dataclasses.dataclass(frozen=True, order=True) 302 | class Value: 303 | value: int 304 | coin: str 305 | 306 | 307 | @dataclasses.dataclass(frozen=True, order=True) 308 | class LeadershipSchedule: 309 | slot_no: int 310 | utc_time: datetime.datetime 311 | 312 | 313 | @dataclasses.dataclass(frozen=True, order=True) 314 | class DataForBuild: 315 | txins: list[UTXOData] 316 | txouts: list[TxOut] 317 | withdrawals: OptionalTxOuts 318 | script_withdrawals: OptionalScriptWithdrawals 319 | 320 | 321 | @dataclasses.dataclass(frozen=True, order=True) 322 | class CCMember: 323 | epoch: int 324 | cold_vkey: str = "" 325 | cold_vkey_file: itp.FileType = "" 326 | cold_vkey_hash: str = "" 327 | cold_skey: str = "" 328 | cold_skey_file: itp.FileType = "" 329 | cold_skey_hash: str = "" 330 | cold_script_hash: str = "" 331 | 332 | 333 | @dataclasses.dataclass(frozen=True, order=True) 334 | class VoteCC: 335 | action_txid: str 336 | action_ix: int 337 | vote: consts.Votes 338 | vote_file: pl.Path 339 | cc_hot_vkey: str = "" 340 | cc_hot_vkey_file: pl.Path | None = None 341 | cc_hot_key_hash: str = "" 342 | cc_hot_script_hash: str = "" 343 | anchor_url: str = "" 344 | anchor_data_hash: str = "" 345 | 346 | 347 | @dataclasses.dataclass(frozen=True, order=True) 348 | class VoteDrep: 349 | action_txid: str 350 | action_ix: int 351 | vote: consts.Votes 352 | vote_file: pl.Path 353 | drep_vkey: str = "" 354 | drep_vkey_file: pl.Path | None = None 355 | drep_key_hash: str = "" 356 | drep_script_hash: str = "" 357 | anchor_url: str = "" 358 | anchor_data_hash: str = "" 359 | 360 | 361 | @dataclasses.dataclass(frozen=True, order=True) 362 | class VoteSPO: 363 | action_txid: str 364 | action_ix: int 365 | vote: consts.Votes 366 | vote_file: pl.Path 367 | stake_pool_vkey: str = "" 368 | cold_vkey_file: pl.Path | None = None 369 | stake_pool_id: str = "" 370 | anchor_url: str = "" 371 | anchor_data_hash: str = "" 372 | 373 | 374 | @dataclasses.dataclass(frozen=True, order=True) 375 | class ActionConstitution: 376 | action_file: pl.Path 377 | deposit_amt: int 378 | anchor_url: str 379 | anchor_data_hash: str 380 | constitution_url: str 381 | constitution_hash: str 382 | deposit_return_stake_vkey: str = "" 383 | deposit_return_stake_vkey_file: pl.Path | None = None 384 | deposit_return_stake_key_hash: str = "" 385 | prev_action_txid: str = "" 386 | prev_action_ix: int = -1 387 | 388 | 389 | @dataclasses.dataclass(frozen=True, order=True) 390 | class ActionInfo: 391 | action_file: pl.Path 392 | deposit_amt: int 393 | anchor_url: str 394 | anchor_data_hash: str 395 | deposit_return_stake_vkey: str = "" 396 | deposit_return_stake_vkey_file: pl.Path | None = None 397 | deposit_return_stake_key_hash: str = "" 398 | 399 | 400 | @dataclasses.dataclass(frozen=True, order=True) 401 | class ActionNoConfidence: 402 | action_file: pl.Path 403 | deposit_amt: int 404 | anchor_url: str 405 | anchor_data_hash: str 406 | prev_action_txid: str 407 | prev_action_ix: int 408 | deposit_return_stake_vkey: str = "" 409 | deposit_return_stake_vkey_file: pl.Path | None = None 410 | deposit_return_stake_key_hash: str = "" 411 | 412 | 413 | @dataclasses.dataclass(frozen=True, order=True) 414 | class ActionUpdateCommittee: 415 | action_file: pl.Path 416 | deposit_amt: int 417 | anchor_url: str 418 | anchor_data_hash: str 419 | threshold: str 420 | add_cc_members: list[CCMember] = dataclasses.field(default_factory=list) 421 | rem_cc_members: list[CCMember] = dataclasses.field(default_factory=list) 422 | prev_action_txid: str = "" 423 | prev_action_ix: int = -1 424 | deposit_return_stake_vkey: str = "" 425 | deposit_return_stake_vkey_file: pl.Path | None = None 426 | deposit_return_stake_key_hash: str = "" 427 | 428 | 429 | @dataclasses.dataclass(frozen=True, order=True) 430 | class ActionPParamsUpdate: 431 | action_file: pl.Path 432 | deposit_amt: int 433 | anchor_url: str 434 | anchor_data_hash: str 435 | cli_args: itp.UnpackableSequence 436 | prev_action_txid: str = "" 437 | prev_action_ix: int = -1 438 | deposit_return_stake_vkey: str = "" 439 | deposit_return_stake_vkey_file: pl.Path | None = None 440 | deposit_return_stake_key_hash: str = "" 441 | 442 | 443 | @dataclasses.dataclass(frozen=True, order=True) 444 | class ActionTreasuryWithdrawal: 445 | action_file: pl.Path 446 | transfer_amt: int 447 | deposit_amt: int 448 | anchor_url: str 449 | anchor_data_hash: str 450 | funds_receiving_stake_vkey: str = "" 451 | funds_receiving_stake_vkey_file: pl.Path | None = None 452 | funds_receiving_stake_key_hash: str = "" 453 | deposit_return_stake_vkey: str = "" 454 | deposit_return_stake_vkey_file: pl.Path | None = None 455 | deposit_return_stake_key_hash: str = "" 456 | 457 | 458 | @dataclasses.dataclass(frozen=True, order=True) 459 | class ActionHardfork: 460 | action_file: pl.Path 461 | deposit_amt: int 462 | anchor_url: str 463 | anchor_data_hash: str 464 | protocol_major_version: int 465 | protocol_minor_version: int 466 | prev_action_txid: str = "" 467 | prev_action_ix: int = -1 468 | deposit_return_stake_vkey: str = "" 469 | deposit_return_stake_vkey_file: pl.Path | None = None 470 | deposit_return_stake_key_hash: str = "" 471 | -------------------------------------------------------------------------------- /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/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 | -------------------------------------------------------------------------------- /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/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 | -------------------------------------------------------------------------------- /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 | p = subprocess.Popen( 76 | ["git", "rev-parse", "HEAD"], stdout=subprocess.PIPE, stderr=subprocess.PIPE 77 | ) 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 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /docs/source/modules.rst: -------------------------------------------------------------------------------- 1 | Source Documentation 2 | ==================== 3 | 4 | .. toctree:: 5 | :maxdepth: 4 6 | 7 | cardano_clusterlib 8 | -------------------------------------------------------------------------------- /docs/source/readme.rst: -------------------------------------------------------------------------------- 1 | .. mdinclude:: ../../README.md 2 | -------------------------------------------------------------------------------- /pyproject.toml: -------------------------------------------------------------------------------- 1 | [build-system] 2 | requires = ["setuptools>=45", "setuptools_scm[toml]>=6.2"] 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 = {text = "Apache License 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 | "Intended Audience :: Developers", 23 | ] 24 | dynamic = ["version"] 25 | dependencies = [ 26 | "packaging", 27 | ] 28 | 29 | [tool.setuptools_scm] 30 | 31 | [project.urls] 32 | homepage = "https://github.com/input-output-hk/cardano-clusterlib-py" 33 | documentation = "https://cardano-clusterlib-py.readthedocs.io/" 34 | repository = "https://github.com/input-output-hk/cardano-clusterlib-py" 35 | 36 | [tool.ruff] 37 | line-length = 100 38 | 39 | [tool.ruff.lint] 40 | 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"] 41 | 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"] 42 | 43 | [tool.ruff.lint.per-file-ignores] 44 | "docs/**.py" = ["ANN"] 45 | 46 | [tool.ruff.lint.isort] 47 | force-single-line = true 48 | 49 | [tool.mypy] 50 | show_error_context = true 51 | verbosity = 0 52 | ignore_missing_imports = true 53 | follow_imports = "normal" 54 | no_implicit_optional = true 55 | allow_untyped_globals = false 56 | warn_unused_configs = true 57 | warn_return_any = true 58 | 59 | [tool.pyrefly] 60 | project_includes = ["cardano_clusterlib"] 61 | ignore_errors_in_generated_code = true 62 | use_untyped_imports = true 63 | ignore_missing_source = true 64 | 65 | [[tool.pyrefly.sub_config]] 66 | matches = "cardano_clusterlib/clusterlib_klass.py" 67 | 68 | # Ignore the bad-argument-type errors for Self@ClusterLib, that are reported only for LSP 69 | [tool.pyrefly.sub_config.errors] 70 | bad-argument-type = false 71 | -------------------------------------------------------------------------------- /requirements-dev.txt: -------------------------------------------------------------------------------- 1 | -e . --config-settings editable_mode=compat 2 | 3 | # linting 4 | mypy~=1.15.0 5 | pyrefly~=0.17.0 6 | pre-commit~=4.2.0 7 | 8 | build 9 | virtualenv 10 | -------------------------------------------------------------------------------- /setup.cfg: -------------------------------------------------------------------------------- 1 | [metadata] 2 | name = cardano-clusterlib 3 | url = https://github.com/input-output-hk/cardano-clusterlib-py 4 | author = Martin Kourim 5 | author_email = martin.kourim@iohk.io 6 | 7 | [options] 8 | zip_safe = False 9 | include_package_data = True 10 | packages = find: 11 | setup_requires = 12 | setuptools_scm 13 | install_requires = 14 | setuptools >= 45 15 | -------------------------------------------------------------------------------- /setup.py: -------------------------------------------------------------------------------- 1 | from setuptools import find_packages 2 | from setuptools import setup 3 | 4 | setup( 5 | name="cardano-clusterlib", 6 | packages=find_packages(), 7 | setup_requires=["setuptools_scm"], 8 | use_scm_version=True, 9 | ) 10 | -------------------------------------------------------------------------------- /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 | --------------------------------------------------------------------------------