├── .circleci ├── config.yml ├── install_geth.sh ├── install_golang.sh └── merge_pr.sh ├── .github ├── ISSUE_TEMPLATE │ ├── bug_report.yml │ ├── config.yml │ └── feature_request.yml └── pull_request_template.md ├── .gitignore ├── .pre-commit-config.yaml ├── .project-template ├── fill_template_vars.py ├── refill_template_vars.py └── template_vars.txt ├── .readthedocs.yaml ├── CHANGELOG.rst ├── CONTRIBUTING.md ├── LICENSE ├── MANIFEST.in ├── Makefile ├── README.md ├── conftest.py ├── geth ├── __init__.py ├── accounts.py ├── chain.py ├── default_blockchain_password ├── exceptions.py ├── genesis.json ├── install.py ├── main.py ├── mixins.py ├── process.py ├── py.typed ├── reset.py ├── types.py ├── utils │ ├── __init__.py │ ├── encoding.py │ ├── filesystem.py │ ├── networking.py │ ├── proc.py │ ├── thread.py │ ├── timeout.py │ └── validation.py └── wrapper.py ├── newsfragments ├── README.md └── validate_files.py ├── pyproject.toml ├── scripts └── release │ └── test_package.py ├── setup.py ├── tests ├── core │ ├── accounts │ │ ├── conftest.py │ │ ├── projects │ │ │ ├── test-01 │ │ │ │ └── keystore │ │ │ │ │ └── UTC--2015-08-24T21-30-14.222885490Z--ae71658b3ab452f7e4f03bda6f777b860b2e2ff2 │ │ │ └── test-02 │ │ │ │ └── keystore │ │ │ │ ├── UTC--2015-08-24T21-30-14.222885490Z--ae71658b3ab452f7e4f03bda6f777b860b2e2ff2 │ │ │ │ ├── UTC--2015-08-24T21-32-00.716418819Z--e8e085862a8d951dd78ec5ea784b3e22ee1ca9c6 │ │ │ │ └── UTC--2015-08-24T21-32-04.748321142Z--0da70f43a568e88168436be52ed129f4a9bbdaf5 │ │ ├── test_account_list_parsing.py │ │ ├── test_create_geth_account.py │ │ └── test_geth_accounts.py │ ├── running │ │ ├── test_running_dev_chain.py │ │ ├── test_running_mainnet_chain.py │ │ ├── test_running_sepolia_chain.py │ │ ├── test_running_with_logging.py │ │ └── test_use_as_a_context_manager.py │ ├── test_import_and_version.py │ ├── test_library_files.py │ ├── utility │ │ ├── test_constructing_test_chain_kwargs.py │ │ ├── test_geth_version.py │ │ ├── test_is_live_chain.py │ │ ├── test_is_sepolia_chain.py │ │ └── test_validation.py │ └── waiting │ │ ├── conftest.py │ │ ├── test_waiting_for_ipc_socket.py │ │ └── test_waiting_for_rpc_connection.py └── installation │ └── test_geth_installation.py ├── tox.ini └── update_geth.py /.circleci/config.yml: -------------------------------------------------------------------------------- 1 | version: 2.1 2 | 3 | parameters: 4 | go_version: 5 | default: "1.22.4" 6 | type: string 7 | 8 | supported_python_minor_versions: &supported_python_minor_versions 9 | - "8" 10 | - "9" 11 | - "10" 12 | - "11" 13 | - "12" 14 | - "13" 15 | 16 | common_go_steps: &common_go_steps 17 | working_directory: ~/repo 18 | steps: 19 | - checkout 20 | - run: 21 | name: checkout fixtures submodule 22 | command: git submodule update --init --recursive 23 | - run: 24 | name: merge pull request base 25 | command: ./.circleci/merge_pr.sh 26 | - run: 27 | name: merge pull request base (2nd try) 28 | command: ./.circleci/merge_pr.sh 29 | when: on_fail 30 | - run: 31 | name: merge pull request base (3rd try) 32 | command: ./.circleci/merge_pr.sh 33 | when: on_fail 34 | - restore_cache: 35 | keys: 36 | - cache-v1-{{ arch }}-{{ .Environment.CIRCLE_JOB }}-{{ checksum "setup.py" }}-{{ checksum "tox.ini" }} 37 | - run: 38 | name: install dependencies 39 | command: | 40 | python -m pip install --upgrade pip 41 | python -m pip install tox 42 | - run: 43 | name: install golang-<< pipeline.parameters.go_version >> 44 | command: ./.circleci/install_golang.sh << pipeline.parameters.go_version >> 45 | - run: 46 | name: run tox 47 | command: python -m tox run -r 48 | - save_cache: 49 | paths: 50 | - .hypothesis 51 | - .tox 52 | - ~/.cache/pip 53 | - ~/.local 54 | key: cache-v1-{{ arch }}-{{ .Environment.CIRCLE_JOB }}-{{ checksum "setup.py" }}-{{ checksum "tox.ini" }} 55 | 56 | orbs: 57 | win: circleci/windows@5.0.0 58 | 59 | jobs: 60 | common: 61 | parameters: 62 | python_minor_version: 63 | type: string 64 | tox_env: 65 | type: string 66 | <<: *common_go_steps 67 | docker: 68 | - image: cimg/python:3.<< parameters.python_minor_version >> 69 | environment: 70 | TOXENV: py3<< parameters.python_minor_version >>-<< parameters.tox_env >> 71 | 72 | install-geth: 73 | parameters: 74 | python_minor_version: 75 | type: string 76 | geth_version: 77 | type: string 78 | <<: *common_go_steps 79 | docker: 80 | - image: cimg/python:3.<< parameters.python_minor_version >> 81 | environment: 82 | GETH_VERSION: v<< parameters.geth_version >> 83 | TOXENV: py3<< parameters.python_minor_version >>-install-geth-v<< parameters.geth_version >> 84 | 85 | windows-wheel: 86 | parameters: 87 | python_minor_version: 88 | type: string 89 | executor: 90 | name: win/default 91 | shell: bash.exe 92 | working_directory: C:\Users\circleci\project\py-geth 93 | environment: 94 | TOXENV: windows-wheel 95 | steps: 96 | - checkout 97 | - restore_cache: 98 | keys: 99 | - cache-v1-{{ arch }}-{{ .Environment.CIRCLE_JOB }}-{{ checksum "setup.py" }}-{{ checksum "tox.ini" }} 100 | - run: 101 | name: install pyenv 102 | command: | 103 | pip install pyenv-win --target $HOME/.pyenv 104 | echo 'export PYENV="$HOME/.pyenv/pyenv-win/"' >> $BASH_ENV 105 | echo 'export PYENV_ROOT="$HOME/.pyenv/pyenv-win/"' >> $BASH_ENV 106 | echo 'export PYENV_USERPROFILE="$HOME/.pyenv/pyenv-win/"' >> $BASH_ENV 107 | echo 'export PATH="$PATH:$HOME/.pyenv/pyenv-win/bin"' >> $BASH_ENV 108 | echo 'export PATH="$PATH:$HOME/.pyenv/pyenv-win/shims"' >> $BASH_ENV 109 | source $BASH_ENV 110 | pyenv update 111 | - run: 112 | name: install latest python version and tox 113 | command: | 114 | LATEST_VERSION=$(pyenv install --list | grep -E "^\s*3\.<< parameters.python_minor_version >>\.[0-9]+$" | tail -1 | tr -d ' ') 115 | echo "Installing python version $LATEST_VERSION" 116 | pyenv install $LATEST_VERSION 117 | pyenv global $LATEST_VERSION 118 | python3 -m pip install --upgrade pip 119 | python3 -m pip install tox 120 | - run: 121 | name: run tox 122 | command: | 123 | echo 'running tox with' $(python3 --version) 124 | python3 -m tox run -r 125 | - save_cache: 126 | paths: 127 | - .tox 128 | key: cache-v1-{{ arch }}-{{ .Environment.CIRCLE_JOB }}-{{ checksum "setup.py" }}-{{ checksum "tox.ini" }} 129 | 130 | workflows: 131 | version: 2 132 | test: 133 | jobs: &all_jobs 134 | - common: 135 | matrix: 136 | parameters: 137 | python_minor_version: *supported_python_minor_versions 138 | tox_env: ["lint", "wheel"] 139 | name: "py3<< matrix.python_minor_version >>-<< matrix.tox_env >>" 140 | - install-geth: 141 | matrix: 142 | parameters: 143 | python_minor_version: *supported_python_minor_versions 144 | geth_version: [ 145 | "1_14_7", "1_14_8", "1_14_9", "1_14_10", "1_14_11", "1_14_12", 146 | "1_14_13", "1_15_0", "1_15_1", "1_15_2", "1_15_3", "1_15_4", 147 | "1_15_5", "1_15_6", "1_15_7", "1_15_8", "1_15_9", "1_15_10", 148 | "1_15_11" 149 | ] 150 | name: "py3<< matrix.python_minor_version >>-install-geth-v<< matrix.geth_version >>" 151 | - windows-wheel: 152 | matrix: 153 | parameters: 154 | python_minor_version: [ "11", "12", "13" ] 155 | name: "py3<< matrix.python_minor_version >>-windows-wheel" 156 | 157 | nightly: 158 | triggers: 159 | - schedule: 160 | # Weekdays 12:00p UTC 161 | cron: "0 12 * * 1,2,3,4,5" 162 | filters: 163 | branches: 164 | only: 165 | - main 166 | jobs: *all_jobs 167 | -------------------------------------------------------------------------------- /.circleci/install_geth.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | python --version 4 | 5 | # convert underscored `GETH_VERSION` to dotted format 6 | GETH_VERSION=${GETH_VERSION//_/\.} 7 | export GETH_VERSION 8 | echo "Using Geth version: $GETH_VERSION" 9 | 10 | export GETH_BASE_INSTALL_PATH=~/repo/install/ 11 | 12 | if [ -n "$GETH_VERSION" ]; then python -m geth.install "$GETH_VERSION"; fi 13 | if [ -n "$GETH_VERSION" ]; then export GETH_BINARY="$GETH_BASE_INSTALL_PATH/geth-$GETH_VERSION/bin/geth"; fi 14 | if [ -n "$GETH_VERSION" ]; then $GETH_BINARY version; fi 15 | 16 | # Modifying the path is tough with tox, hence copying the executable 17 | # to a known directory which is included in $PATH 18 | cp "$GETH_BINARY" "$HOME"/.local/bin 19 | -------------------------------------------------------------------------------- /.circleci/install_golang.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | GO_VERSION=$1 3 | wget "https://dl.google.com/go/go$GO_VERSION.linux-amd64.tar.gz" 4 | sudo tar -zxvf go$GO_VERSION.linux-amd64.tar.gz -C /usr/local/ 5 | echo 'export GOROOT=/usr/local/go' >> $BASH_ENV 6 | echo 'export PATH=$PATH:/usr/local/go/bin' >> $BASH_ENV 7 | 8 | # Adding the below path to bashrc so that we could put our 9 | # future installed geth executables in below path 10 | echo 'export PATH=$PATH:$HOME/.local/bin' >> $BASH_ENV 11 | -------------------------------------------------------------------------------- /.circleci/merge_pr.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | if [[ -n "${CIRCLE_PR_NUMBER}" ]]; then 4 | PR_INFO_URL=https://api.github.com/repos/$CIRCLE_PROJECT_USERNAME/$CIRCLE_PROJECT_REPONAME/pulls/$CIRCLE_PR_NUMBER 5 | PR_BASE_BRANCH=$(curl -L "$PR_INFO_URL" | python -c 'import json, sys; obj = json.load(sys.stdin); sys.stdout.write(obj["base"]["ref"])') 6 | git fetch origin +"$PR_BASE_BRANCH":circleci/pr-base 7 | # We need these config values or git complains when creating the 8 | # merge commit 9 | git config --global user.name "Circle CI" 10 | git config --global user.email "circleci@example.com" 11 | git merge --no-edit circleci/pr-base 12 | fi 13 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/bug_report.yml: -------------------------------------------------------------------------------- 1 | name: Bug Report 2 | description: File a bug report 3 | labels: ["bug"] 4 | body: 5 | - type: markdown 6 | attributes: 7 | value: "## What was wrong" 8 | - type: textarea 9 | id: what-happened 10 | attributes: 11 | label: What happened? 12 | description: Also tell us what you expected to happen 13 | validations: 14 | required: true 15 | - type: textarea 16 | id: code-that-caused 17 | attributes: 18 | label: Code that produced the error 19 | description: Formats to Python, no backticks needed 20 | render: python 21 | validations: 22 | required: false 23 | - type: textarea 24 | id: error-output 25 | attributes: 26 | label: Full error output 27 | description: Formats to shell, no backticks needed 28 | render: shell 29 | validations: 30 | required: false 31 | - type: markdown 32 | attributes: 33 | value: "## Potential Solutions" 34 | - type: textarea 35 | id: how-to-fix 36 | attributes: 37 | label: Fill this section in if you know how this could or should be fixed 38 | description: Include any relevant examples or reference material 39 | validations: 40 | required: false 41 | - type: input 42 | id: lib-version 43 | attributes: 44 | label: py-geth Version 45 | description: Which version of py-geth are you using? 46 | placeholder: x.x.x 47 | validations: 48 | required: false 49 | - type: input 50 | id: py-version 51 | attributes: 52 | label: Python Version 53 | description: Which version of Python are you using? 54 | placeholder: x.x.x 55 | validations: 56 | required: false 57 | - type: input 58 | id: os 59 | attributes: 60 | label: Operating System 61 | description: Which operating system are you using? 62 | placeholder: osx/linux/win 63 | validations: 64 | required: false 65 | - type: textarea 66 | id: pip-freeze 67 | attributes: 68 | label: Output from `pip freeze` 69 | description: Run `python -m pip freeze` and paste the output below 70 | render: shell 71 | validations: 72 | required: false 73 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/config.yml: -------------------------------------------------------------------------------- 1 | blank_issues_enabled: true 2 | contact_links: 3 | - name: Questions about using py-geth? 4 | url: https://discord.gg/GHryRvPB84 5 | about: You can ask and answer usage questions on the Ethereum Python Community Discord 6 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/feature_request.yml: -------------------------------------------------------------------------------- 1 | name: Feature Request 2 | description: Request a new feature 3 | labels: ["feature_request"] 4 | body: 5 | - type: textarea 6 | id: feature-description 7 | attributes: 8 | label: What feature should we add? 9 | description: Include any relevant examples or reference material 10 | validations: 11 | required: true 12 | -------------------------------------------------------------------------------- /.github/pull_request_template.md: -------------------------------------------------------------------------------- 1 | ### What was wrong? 2 | 3 | Related to Issue # 4 | Closes # 5 | 6 | ### How was it fixed? 7 | 8 | ### Todo: 9 | 10 | - [ ] Clean up commit history 11 | - [ ] Add or update documentation related to these changes 12 | - [ ] Add entry to the [release notes](https://github.com/ethereum/py-geth/blob/main/newsfragments/README.md) 13 | 14 | #### Cute Animal Picture 15 | 16 | ![Put a link to a cute animal picture inside the parenthesis-->](<>) 17 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | *.py[cod] 2 | 3 | # C extensions 4 | *.so 5 | 6 | # Packages 7 | *.egg 8 | *.egg-info 9 | dist 10 | build 11 | .build 12 | eggs 13 | .eggs 14 | parts 15 | bin 16 | var 17 | sdist 18 | develop-eggs 19 | .installed.cfg 20 | lib 21 | lib64 22 | pip-wheel-metadata 23 | venv* 24 | .venv* 25 | 26 | # Installer logs 27 | pip-log.txt 28 | 29 | # Unit test / coverage reports 30 | .coverage 31 | .tox 32 | nosetests.xml 33 | 34 | # Translations 35 | *.mo 36 | 37 | # Mr Developer 38 | .mr.developer.cfg 39 | .project 40 | .pydevproject 41 | 42 | # Complexity 43 | output/*.html 44 | output/*/index.html 45 | 46 | # Sphinx 47 | docs/_build 48 | docs/modules.rst 49 | docs/*.internal.rst 50 | docs/*.utils.rst 51 | docs/*._utils.* 52 | 53 | # Blockchain 54 | chains 55 | 56 | # Hypothesis Property base testing 57 | .hypothesis 58 | 59 | # tox/pytest cache 60 | .cache 61 | .pytest_cache 62 | 63 | # pycache 64 | __pycache__/ 65 | 66 | # Test output logs 67 | logs 68 | 69 | # VIM temp files 70 | *.sw[op] 71 | 72 | # mypy 73 | .mypy_cache 74 | 75 | # macOS 76 | .DS_Store 77 | 78 | # pyenv 79 | .python-version 80 | 81 | # vs-code 82 | .vscode 83 | 84 | # py-geth-specific 85 | tests/core/accounts/projects/*/geth/* 86 | 87 | # jupyter notebook files 88 | *.ipynb 89 | 90 | # Covers JetBrains IDEs: IntelliJ, RubyMine, PhpStorm, AppCode, PyCharm, CLion, Android Studio and Webstorm 91 | # For a more precise, explicit template, see: 92 | # https://intellij-support.jetbrains.com/hc/en-us/articles/206544839 93 | 94 | ## General 95 | .idea/* 96 | .idea_modules/* 97 | 98 | ## File-based project format: 99 | *.iws 100 | 101 | ## IntelliJ 102 | out/ 103 | 104 | ## Plugin-specific files: 105 | 106 | ### JIRA plugin 107 | atlassian-ide-plugin.xml 108 | 109 | ### Crashlytics plugin (for Android Studio and IntelliJ) 110 | com_crashlytics_export_strings.xml 111 | crashlytics.properties 112 | crashlytics-build.properties 113 | fabric.properties 114 | 115 | # END JetBrains section 116 | -------------------------------------------------------------------------------- /.pre-commit-config.yaml: -------------------------------------------------------------------------------- 1 | exclude: '.project-template|tests/core/accounts/projects/' 2 | repos: 3 | - repo: https://github.com/pre-commit/pre-commit-hooks 4 | rev: v4.5.0 5 | hooks: 6 | - id: check-yaml 7 | - id: check-toml 8 | - id: end-of-file-fixer 9 | - id: trailing-whitespace 10 | - repo: https://github.com/asottile/pyupgrade 11 | rev: v3.15.0 12 | hooks: 13 | - id: pyupgrade 14 | args: [--py38-plus] 15 | - repo: https://github.com/psf/black 16 | rev: 23.9.1 17 | hooks: 18 | - id: black 19 | - repo: https://github.com/PyCQA/flake8 20 | rev: 6.1.0 21 | hooks: 22 | - id: flake8 23 | additional_dependencies: 24 | - flake8-bugbear==23.9.16 25 | exclude: setup.py 26 | - repo: https://github.com/PyCQA/autoflake 27 | rev: v2.2.1 28 | hooks: 29 | - id: autoflake 30 | - repo: https://github.com/pycqa/isort 31 | rev: 5.12.0 32 | hooks: 33 | - id: isort 34 | - repo: https://github.com/pycqa/pydocstyle 35 | rev: 6.3.0 36 | hooks: 37 | - id: pydocstyle 38 | additional_dependencies: 39 | - tomli # required until >= python311 40 | - repo: https://github.com/executablebooks/mdformat 41 | rev: 0.7.17 42 | hooks: 43 | - id: mdformat 44 | additional_dependencies: 45 | - mdformat-gfm 46 | - repo: local 47 | hooks: 48 | - id: mypy-local 49 | name: run mypy with all dev dependencies present 50 | entry: python -m mypy -p geth 51 | language: system 52 | always_run: true 53 | pass_filenames: false 54 | - repo: https://github.com/PrincetonUniversity/blocklint 55 | rev: v0.2.5 56 | hooks: 57 | - id: blocklint 58 | exclude: 'CHANGELOG.rst|tox.ini' 59 | -------------------------------------------------------------------------------- /.project-template/fill_template_vars.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | 3 | import os 4 | import sys 5 | import re 6 | from pathlib import Path 7 | 8 | 9 | def _find_files(project_root): 10 | path_exclude_pattern = r"\.git($|\/)|venv|_build" 11 | file_exclude_pattern = r"fill_template_vars\.py|\.swp$" 12 | filepaths = [] 13 | for dir_path, _dir_names, file_names in os.walk(project_root): 14 | if not re.search(path_exclude_pattern, dir_path): 15 | for file in file_names: 16 | if not re.search(file_exclude_pattern, file): 17 | filepaths.append(str(Path(dir_path, file))) 18 | 19 | return filepaths 20 | 21 | 22 | def _replace(pattern, replacement, project_root): 23 | print(f"Replacing values: {pattern}") 24 | for file in _find_files(project_root): 25 | try: 26 | with open(file) as f: 27 | content = f.read() 28 | content = re.sub(pattern, replacement, content) 29 | with open(file, "w") as f: 30 | f.write(content) 31 | except UnicodeDecodeError: 32 | pass 33 | 34 | 35 | def main(): 36 | project_root = Path(os.path.realpath(sys.argv[0])).parent.parent 37 | 38 | module_name = input("What is your python module name? ") 39 | 40 | pypi_input = input(f"What is your pypi package name? (default: {module_name}) ") 41 | pypi_name = pypi_input or module_name 42 | 43 | repo_input = input(f"What is your github project name? (default: {pypi_name}) ") 44 | repo_name = repo_input or pypi_name 45 | 46 | rtd_input = input( 47 | f"What is your readthedocs.org project name? (default: {pypi_name}) " 48 | ) 49 | rtd_name = rtd_input or pypi_name 50 | 51 | project_input = input( 52 | f"What is your project name (ex: at the top of the README)? (default: {repo_name}) " 53 | ) 54 | project_name = project_input or repo_name 55 | 56 | short_description = input("What is a one-liner describing the project? ") 57 | 58 | _replace("", module_name, project_root) 59 | _replace("", pypi_name, project_root) 60 | _replace("", repo_name, project_root) 61 | _replace("", rtd_name, project_root) 62 | _replace("", project_name, project_root) 63 | _replace("", short_description, project_root) 64 | 65 | os.makedirs(project_root / module_name, exist_ok=True) 66 | Path(project_root / module_name / "__init__.py").touch() 67 | Path(project_root / module_name / "py.typed").touch() 68 | 69 | 70 | if __name__ == "__main__": 71 | main() 72 | -------------------------------------------------------------------------------- /.project-template/refill_template_vars.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | 3 | import os 4 | import sys 5 | from pathlib import Path 6 | import subprocess 7 | 8 | 9 | def main(): 10 | template_dir = Path(os.path.dirname(sys.argv[0])) 11 | template_vars_file = template_dir / "template_vars.txt" 12 | fill_template_vars_script = template_dir / "fill_template_vars.py" 13 | 14 | with open(template_vars_file, "r") as input_file: 15 | content_lines = input_file.readlines() 16 | 17 | process = subprocess.Popen( 18 | [sys.executable, str(fill_template_vars_script)], 19 | stdin=subprocess.PIPE, 20 | stdout=subprocess.PIPE, 21 | stderr=subprocess.PIPE, 22 | text=True, 23 | ) 24 | 25 | for line in content_lines: 26 | process.stdin.write(line) 27 | process.stdin.flush() 28 | 29 | stdout, stderr = process.communicate() 30 | 31 | if process.returncode != 0: 32 | print(f"Error occurred: {stderr}") 33 | sys.exit(1) 34 | 35 | print(stdout) 36 | 37 | 38 | if __name__ == "__main__": 39 | main() 40 | -------------------------------------------------------------------------------- /.project-template/template_vars.txt: -------------------------------------------------------------------------------- 1 | geth 2 | py-geth 3 | py-geth 4 | 5 | PyGeth 6 | Python wrapper around running `geth` as a subprocess 7 | -------------------------------------------------------------------------------- /.readthedocs.yaml: -------------------------------------------------------------------------------- 1 | version: 2 2 | 3 | build: 4 | os: ubuntu-22.04 5 | tools: 6 | python: "3.10" 7 | 8 | sphinx: 9 | configuration: docs/conf.py 10 | fail_on_warning: true 11 | 12 | python: 13 | install: 14 | - method: pip 15 | path: . 16 | extra_requirements: 17 | - docs 18 | 19 | # Build all formats for RTD Downloads - htmlzip, pdf, epub 20 | formats: all 21 | -------------------------------------------------------------------------------- /CHANGELOG.rst: -------------------------------------------------------------------------------- 1 | py-geth v5.6.0 (2025-05-07) 2 | --------------------------- 3 | 4 | Features 5 | ~~~~~~~~ 6 | 7 | - Adds support for geth ``v1.15.11``. (`#271 `__) 8 | 9 | 10 | py-geth v5.5.0 (2025-05-02) 11 | --------------------------- 12 | 13 | Features 14 | ~~~~~~~~ 15 | 16 | - Adds support for geth ``v1.15.8`` and ``v1.15.9``. (`#268 `__) 17 | - Adds support for geth ``v1.15.10``. (`#269 `__) 18 | 19 | 20 | py-geth v5.4.0 (2025-04-02) 21 | --------------------------- 22 | 23 | Features 24 | ~~~~~~~~ 25 | 26 | - Support for geth ``v1.15.6``. (`#261 `__) 27 | - Support for geth ``v1.15.7``. (`#265 `__) 28 | 29 | 30 | py-geth v5.3.0 (2025-03-11) 31 | --------------------------- 32 | 33 | Bugfixes 34 | ~~~~~~~~ 35 | 36 | - Fix a bug where defaults were being filled for all config values if not provided in ``genesis.json``. (`#255 `__) 37 | 38 | 39 | Features 40 | ~~~~~~~~ 41 | 42 | - Adds support for geth ``v1.15.0`` through ``v1.15.5``, add new fields ``blobSchedule`` and ``pragueTime`` to genesis data config. (`#251 `__) 43 | - Add ``version`` property to ``BaseGethProcess``, returning the version of the running geth process. (`#255 `__) 44 | 45 | 46 | py-geth v5.2.1 (2025-02-03) 47 | --------------------------- 48 | 49 | Features 50 | ~~~~~~~~ 51 | 52 | - Add support for ``geth v1.14.3``. (`#245 `__) 53 | - Adds ability to set 'dev_period' ('--dev.period') (`#247 `__) 54 | 55 | 56 | py-geth v5.2.0 (2025-01-14) 57 | --------------------------- 58 | 59 | Features 60 | ~~~~~~~~ 61 | 62 | - Merge template, including adding ``python313`` support, replace ``bumpversion`` with ``bump-my-version`` (`#243 `__) 63 | 64 | 65 | Internal Changes - for py-geth Contributors 66 | ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ 67 | 68 | - Re-organize circleci config, making use of ``matrix``, to parametrize CI jobs and reduce code duplication. (`#244 `__) 69 | 70 | 71 | py-geth v5.1.0 (2024-11-20) 72 | --------------------------- 73 | 74 | Bugfixes 75 | ~~~~~~~~ 76 | 77 | - ``ipc_path`` property should always return a string (`#239 `__) 78 | 79 | 80 | Features 81 | ~~~~~~~~ 82 | 83 | - Add support for new geth ``v1.14.9``. (`#234 `__) 84 | - Add support for Geth 1.14.12. (`#241 `__) 85 | 86 | 87 | py-geth v5.0.0 (2024-08-14) 88 | --------------------------- 89 | 90 | Breaking Changes 91 | ~~~~~~~~~~~~~~~~ 92 | 93 | - Replace ``subprocess+wget`` with ``requests`` to retrieve geth binaries (`#228 `__) 94 | 95 | 96 | Features 97 | ~~~~~~~~ 98 | 99 | - Add support for geth ``v1.14.8`` (`#231 `__) 100 | 101 | 102 | py-geth v5.0.0-beta.3 (2024-07-11) 103 | ---------------------------------- 104 | 105 | Features 106 | ~~~~~~~~ 107 | 108 | - Add support for geth ``v1.14.6``. (`#224 `__) 109 | - Add ``tx_pool_lifetime`` flag option (`#225 `__) 110 | - Add support for geth ``v1.14.7`` (`#227 `__) 111 | 112 | 113 | py-geth v5.0.0-beta.2 (2024-06-28) 114 | ---------------------------------- 115 | 116 | Bugfixes 117 | ~~~~~~~~ 118 | 119 | - Add missing fields for genesis data. Change mixhash -> mixHash to more closely match Geth (`#221 `__) 120 | 121 | 122 | py-geth v5.0.0-beta.1 (2024-06-19) 123 | ---------------------------------- 124 | 125 | Breaking Changes 126 | ~~~~~~~~~~~~~~~~ 127 | 128 | - Return type of functions in ``accounts.py`` changed from ``bytes`` to ``str`` (`#199 `__) 129 | - Changed return type of ``get_geth_version_info_string`` from ``bytes`` to ``str`` (`#204 `__) 130 | - Use a ``pydantic`` model and a ``TypedDict`` to validate and fill default kwargs for ``genesis_data``. Alters the signature of ``write_genesis_file`` to require ``kwargs`` or a ``dict`` for ``genesis_data``. (`#210 `__) 131 | - Use ``GethKwargsTypedDict`` to typecheck the ``geth_kwargs`` dict when passed as an argument. Breaks signatures of functions ``get_accounts``, ``create_new_account``, and ``ensure_account_exists``, requiring all ``kwargs`` now. (`#213 `__) 132 | 133 | 134 | Bugfixes 135 | ~~~~~~~~ 136 | 137 | - Remove duplicates from dev mode account parsing for ``get_accounts()``. (`#219 `__) 138 | 139 | 140 | Improved Documentation 141 | ~~~~~~~~~~~~~~~~~~~~~~ 142 | 143 | - Update documentation for ``DevGethProcess`` transition to using ``geth --dev``. (`#200 `__) 144 | 145 | 146 | Features 147 | ~~~~~~~~ 148 | 149 | - Add support for newly released geth version ``v1.13.15``. (`#193 `__) 150 | - Add support for geth ``v1.14.0`` - ``v1.14.3``, with the exception for the missing geth ``v1.14.1`` release. (`#195 `__) 151 | - Add support for geth versions ``v1.14.4`` and ``v1.14.5``. (`#206 `__) 152 | - Update all raised ``Exceptions`` to inherit from a ``PyGethException`` (`#212 `__) 153 | 154 | 155 | Internal Changes - for py-geth Contributors 156 | ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ 157 | 158 | - Adding basic type hints across the lib (`#196 `__) 159 | - Use a pydantic model to validate typing of ``geth_kwargs`` when passed as an argument (`#199 `__) 160 | - Change args for ``construct_popen_command`` from indivdual kwargs to geth_kwargs and validate with GethKwargs model (`#205 `__) 161 | - Use the latest golang version ``v1.22.4`` when running CircleCI jobs. (`#206 `__) 162 | - Refactor ``data_dir`` property of ``BaseGethProcess`` and derived classes to fix typing (`#208 `__) 163 | - Run ``mypy`` locally with all dev deps installed, instead of using the pre-commit ``mirrors-mypy`` hook (`#210 `__) 164 | - Add ``fill_default_genesis_data`` function to properly fill ``genesis_data`` defaults (`#215 `__) 165 | 166 | 167 | Removals 168 | ~~~~~~~~ 169 | 170 | - Remove support for geth < ``v1.13.0``. (`#195 `__) 171 | - Remove deprecated ``ipc_api`` and ``miner_threads`` geth cli flags (`#202 `__) 172 | - Removed deprecated ``LiveGethProcess``, use ``MainnetGethProcess`` instead (`#203 `__) 173 | - Remove handling of ``--ssh`` geth kwarg (`#205 `__) 174 | - Drop support for geth ``v1.13.x``, keeping only ``v1.14.0`` and above. Also removes all APIs related to mining, DAG, and the ``personal`` namespace. (`#206 `__) 175 | 176 | 177 | py-geth v4.4.0 (2024-03-27) 178 | --------------------------- 179 | 180 | Features 181 | ~~~~~~~~ 182 | 183 | - Add support for geth ``v1.13.12 and v1.13.13`` (`#188 `__) 184 | - Add support for ``geth v1.13.14`` (`#189 `__) 185 | 186 | 187 | Internal Changes - for py-geth Contributors 188 | ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ 189 | 190 | - Merge template updates, noteably add python 3.12 support (`#186 `__) 191 | 192 | 193 | py-geth v4.3.0 (2024-02-12) 194 | --------------------------- 195 | 196 | Features 197 | ~~~~~~~~ 198 | 199 | - Add support for geth ``v1.13.11`` (`#182 `__) 200 | 201 | 202 | py-geth v4.2.0 (2024-01-23) 203 | --------------------------- 204 | 205 | Features 206 | ~~~~~~~~ 207 | 208 | - Add support for geth ``v1.13.10`` (`#179 `__) 209 | 210 | 211 | py-geth v4.1.0 (2024-01-10) 212 | --------------------------- 213 | 214 | Bugfixes 215 | ~~~~~~~~ 216 | 217 | - Fix issue where could not set custom extraData in chain genesis (`#167 `__) 218 | 219 | 220 | Features 221 | ~~~~~~~~ 222 | 223 | - Add support for geth ``1.13.5`` (`#165 `__) 224 | - Allow clique consensus parameters period and epoch in chain genesis (`#169 `__) 225 | - Add support for geth ``v1.13.6`` and ``v1.13.7`` (`#173 `__) 226 | - Add support for geth ``v1.13.8`` (`#175 `__) 227 | - Added support for ``geth v1.13.9`` (`#176 `__) 228 | 229 | 230 | Internal Changes - for py-geth Contributors 231 | ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ 232 | 233 | - Change the name of ``master`` branch to ``main`` (`#166 `__) 234 | 235 | 236 | py-geth v4.0.0 (2023-10-30) 237 | --------------------------- 238 | 239 | Breaking Changes 240 | ~~~~~~~~~~~~~~~~ 241 | 242 | - Drop support for geth ``v1.9`` and ``v1.10`` series. Shanghai was introduced in geth ``v1.11.0`` so this is a good place to draw the line. Drop official support for Python 3.7. (`#160 `__) 243 | 244 | 245 | Features 246 | ~~~~~~~~ 247 | 248 | - Add support for geth ``1.12.0`` and ``1.12.1`` (`#151 `__) 249 | - Add support for geth versions v1.12.2 to v1.13.4 (`#160 `__) 250 | 251 | 252 | Internal Changes - for py-geth Contributors 253 | ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ 254 | 255 | - Use golang version ``1.21.3`` for CI builds to ensure compatibility with the latest version. (`#160 `__) 256 | - Merge template updates, including using pre-commit for linting and drop ``pkg_resources`` for version info (`#162 `__) 257 | 258 | 259 | Miscellaneous Changes 260 | ~~~~~~~~~~~~~~~~~~~~~ 261 | 262 | - `#152 `__ 263 | 264 | 265 | py-geth v3.13.0 (2023-06-07) 266 | ---------------------------- 267 | 268 | Features 269 | ~~~~~~~~ 270 | 271 | - Allow initializing `BaseGethProcess` with `stdin`, `stdout`, and `stderr` (`#139 `__) 272 | - Add support for geth `1.11.6` (`#141 `__) 273 | 274 | 275 | Internal Changes - for py-geth Contributors 276 | ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ 277 | 278 | - Update `tox` and the way it is installed for CircleCI runs (`#141 `__) 279 | - merge in python project template (`#142 `__) 280 | - Changed `.format` strings to f-strings, removed other python2 code (`#146 `__) 281 | 282 | 283 | Removals 284 | ~~~~~~~~ 285 | 286 | - Remove `miner.thread` default since no longer supported (`#144 `__) 287 | 288 | 289 | 3.12.0 290 | ------ 291 | 292 | - Add support for geth `1.11.3`, `1.11.4`, and `1.11.5` 293 | - Add `miner_etherbase` to supported geth kwargs 294 | 295 | 3.11.0 296 | ------ 297 | 298 | - Upgrade circleci golang version to `1.20.1` 299 | - Add support for python `3.11` 300 | - Add support for geth `1.10.26`, `1.11.0`, `1.11.1`, and `1.11.2` 301 | - Fix incorrect comment in `install_geth.sh` 302 | - Add `clique` to `ALL_APIS` 303 | - Add `gcmode` option to Geth process wrapper 304 | 305 | 3.10.0 306 | ------ 307 | 308 | - Add support for geth `1.10.24`-`1.10.25` 309 | - Patch CVE-2007-4559 - directory traversal vulnerability 310 | 311 | 3.9.1 312 | ----- 313 | 314 | - Add support for geth `1.10.18`-`1.10.23` 315 | - Remove support for geth versions `1.9.X` 316 | - Upgrade CI Go version to `1.18.1` 317 | - Some updates to `setup.py`, `tox.ini`, and circleci `config.yml` 318 | - Update supported python versions to reflect what is being tested 319 | - Add python 3.10 support 320 | - Remove dependency on `idna` 321 | - Remove deprecated `setuptools-markdown` 322 | - Updates to `pytest`, `tox`, `setuptools`, `flake8`, and `pluggy` dependencies 323 | - Spelling fix in `create_new_account` docstring 324 | 325 | 3.8.0 326 | ----- 327 | 328 | - Add support for geth 1.10.14-1.10.17 329 | 330 | 3.7.0 331 | ----- 332 | 333 | - Remove extraneous logging formatting from the LoggingMixin 334 | - Add support for geth 1.10.12-1.10.13 335 | 336 | 3.6.0 337 | ----- 338 | 339 | - Add support for geth 1.10.9-1.10.11 340 | - Add support for python 3.9 341 | - Update flake8 requirement to 3.9.2 342 | - Add script to update geth versions 343 | - Set upgrade block numbers in default config 344 | - Allow passing a port by both string and integer to overrides 345 | - Add --preload flag option 346 | - Add --cache flag option 347 | - Add --tx_pool_global_slots flag option 348 | - Add --tx_pool_price_limit flag option 349 | - Handle StopIteration in JoinableQueues when using LoggingMixin 350 | - General code cleanup 351 | 352 | 3.5.0 353 | ----- 354 | 355 | - Add support for geth 1.10.7-1.10.8 356 | 357 | 3.4.0 358 | ----- 359 | 360 | - Add support for geth 1.10.6 361 | 362 | 3.3.0 363 | ----- 364 | 365 | - Add support for geth 1.10.5 366 | 367 | 3.2.0 368 | ----- 369 | 370 | - Add support for geth 1.10.4 371 | 372 | 3.1.0 373 | ----- 374 | 375 | - Add support for geth 1.10.2-1.10.3 376 | 377 | 3.0.0 378 | ----- 379 | 380 | - Add support for geth 1.9.20-1.10.0 381 | - Remove support for geth <= 1.9.14 382 | 383 | 2.4.0 384 | ----- 385 | 386 | - Add support for geth 1.9.13-1.9.19 387 | 388 | 2.3.0 389 | ----- 390 | 391 | - Add support for geth 1.9.8-1.9.12 392 | 393 | 2.2.0 394 | ----- 395 | 396 | - Add support for geth 1.9.x 397 | - Readme bugfix for pypi badges 398 | 399 | 2.1.0 400 | ----- 401 | 402 | - remove support for python 2.x 403 | - Geth versions `<1.7` are no longer tested in CI 404 | - Support for geth versions up to `geth==1.8.22` 405 | - Support for python 3.6 and 3.7 406 | 407 | 1.10.2 408 | ------ 409 | 410 | - Support for testing and installation of `geth==1.7.2` 411 | 412 | 1.10.1 413 | ------ 414 | 415 | - Support for testing and installation of `geth==1.7.0` 416 | 417 | 1.10.0 418 | ------ 419 | 420 | - Support and testing against `geth==1.6.1` 421 | - Support and testing against `geth==1.6.2` 422 | - Support and testing against `geth==1.6.3` 423 | - Support and testing against `geth==1.6.4` 424 | - Support and testing against `geth==1.6.5` 425 | - Support and testing against `geth==1.6.6` 426 | - Support and testing against `geth==1.6.7` 427 | 428 | 1.9.0 429 | ----- 430 | 431 | - Rename `LiveGethProcess` to `MainnetGethProcess`. `LiveGethProcess` now raises deprecation warning when instantiated. 432 | - Implement `geth` installation scripts and API 433 | - Expand test suite to cover through `geth==1.6.6` 434 | 435 | 1.8.0 436 | ----- 437 | 438 | - Bugfix for `--ipcapi` flag removal in geth 1.6.x 439 | 440 | 1.7.1 441 | ----- 442 | 443 | - Bugfix for `ensure_path_exists` utility function. 444 | 445 | 1.7.0 446 | ----- 447 | 448 | - Change to use `compat` instead of `async` since async is a keyword 449 | - Change env variable for gevent threading to be `GETH_THREADING_BACKEND` 450 | 451 | 1.6.0 452 | ----- 453 | 454 | - Remove hard dependency on gevent. 455 | - Expand testing against 1.5.5 and 1.5.6 456 | 457 | 1.5.0 458 | ----- 459 | 460 | - Deprecate the `--testnet` based chain. 461 | - TestnetGethProcess now is an alias for whatever the current primary testnet is 462 | - RopstenGethProcess now represents the current ropsten test network 463 | - travis-ci geth version pinning. 464 | 465 | 1.4.1 466 | ----- 467 | 468 | - Add `rpc_cors_domain` to supported arguments for running geth instances. 469 | 470 | 1.4.0 471 | ----- 472 | 473 | - Add `shh` flag to wrapper to allow enabling of whisper in geth processes. 474 | 475 | 1.3.0 476 | ----- 477 | 478 | - Bugfix for python3 when no contracts are found. 479 | - Allow genesis configuration through constructor of GethProcess classes. 480 | 481 | 1.2.0 482 | ----- 483 | 484 | - Add gevent monkeypatch for socket when using requests and urllib. 485 | 486 | 1.1.0 487 | ----- 488 | 489 | - Fix websocket addition 490 | 491 | 1.0.0 492 | ----- 493 | 494 | - Add Websocket interface to default list of interfaces that are presented by 495 | geth. 496 | 497 | 0.9.0 498 | ----- 499 | 500 | - Fix broken LiveGethProcess and TestnetGethProcess classes. 501 | - Let DevGethProcesses use a local geth.ipc if the path is short enough. 502 | 503 | 0.8.0 504 | ----- 505 | 506 | - Add `homesteadBlock`, `daoForkBlock`, and `doaForkSupport` to the genesis 507 | config that is written for test chains. 508 | 509 | 0.7.0 510 | ----- 511 | 512 | - Rename python module from `pygeth` to `geth` 513 | 514 | 0.6.0 515 | ----- 516 | 517 | - Add `is_rpc_ready` and `wait_for_rpc` api. 518 | - Add `is_ipc_ready` and `wait_for_ipc` api. 519 | - Add `is_dag_generated` and `wait_for_dag` api. 520 | - Refactor `LoggingMixin` core logic into base `InterceptedStreamsMixin` 521 | 522 | 523 | 0.5.0 524 | ----- 525 | 526 | - Fix deprecated usage of `--genesis` 527 | 528 | 529 | 0.4.0 530 | ----- 531 | 532 | - Fix broken loggin mixin (again) 533 | 534 | 535 | 0.3.0 536 | ----- 537 | 538 | - Fix broken loggin mixin. 539 | 540 | 541 | 0.2.0 542 | ----- 543 | 544 | - Add logging mixins 545 | 546 | 547 | 0.1.0 548 | ----- 549 | 550 | - Initial Release 551 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # Development 2 | 3 | To start development for `py-geth` you should begin by cloning the repo. 4 | 5 | ```bash 6 | $ git clone git@github.com:ethereum/py-geth.git 7 | ``` 8 | 9 | # Cute Animal Pictures 10 | 11 | All pull requests need to have a cute animal picture. This is a very important 12 | part of the development process. 13 | 14 | # Pull Requests 15 | 16 | In general, pull requests are welcome. Please try to adhere to the following. 17 | 18 | - code should conform to PEP8 and as well as the linting done by flake8 19 | - include tests. 20 | - include any relevant documentation updates. 21 | - update the CHANGELOG to include a brief description of what was done. 22 | 23 | It's a good idea to make pull requests early on. A pull request represents the 24 | start of a discussion, and doesn't necessarily need to be the final, finished 25 | submission. 26 | 27 | GitHub's documentation for working on pull requests is [available here][pull-requests]. 28 | 29 | Always run the tests before submitting pull requests, and ideally run `tox` in 30 | order to check that your modifications don't break anything. 31 | 32 | Once you've made a pull request take a look at the travis build status in the 33 | GitHub interface and make sure the tests are running as you'd expect. 34 | 35 | [pull-requests]: https://help.github.com/articles/about-pull-requests 36 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2016-2025 The Ethereum Foundation 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /MANIFEST.in: -------------------------------------------------------------------------------- 1 | include LICENSE 2 | include README.md 3 | 4 | recursive-include scripts * 5 | recursive-include tests * 6 | 7 | global-include *.pyi 8 | 9 | recursive-exclude * __pycache__ 10 | recursive-exclude * *.py[co] 11 | prune .tox 12 | prune venv* 13 | 14 | include geth/default_blockchain_password 15 | include geth/genesis.json 16 | include geth/py.typed 17 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | CURRENT_SIGN_SETTING := $(shell git config commit.gpgSign) 2 | 3 | .PHONY: clean-pyc clean-build docs 4 | 5 | 6 | help: 7 | @echo "clean-build - remove build artifacts" 8 | @echo "clean-pyc - remove Python file artifacts" 9 | @echo "clean - run clean-build and clean-pyc" 10 | @echo "dist - build package and cat contents of the dist directory" 11 | @echo "lint - fix linting issues with pre-commit" 12 | @echo "test - run tests quickly with the default Python" 13 | @echo "docs - view draft of newsfragments to be added to CHANGELOG" 14 | @echo "package-test - build package and install it in a venv for manual testing" 15 | @echo "notes - consume towncrier newsfragments/ and update CHANGELOG" 16 | @echo "release - package and upload a release (does not run notes target)" 17 | 18 | clean: clean-build clean-pyc 19 | 20 | clean-build: 21 | rm -fr build/ 22 | rm -fr dist/ 23 | rm -fr *.egg-info 24 | 25 | clean-pyc: 26 | find . -name '*.pyc' -exec rm -f {} + 27 | find . -name '*.pyo' -exec rm -f {} + 28 | find . -name '*~' -exec rm -f {} + 29 | find . -name '__pycache__' -exec rm -rf {} + 30 | 31 | clean: clean-build clean-pyc 32 | 33 | dist: clean 34 | python -m build 35 | ls -l dist 36 | 37 | lint: 38 | @pre-commit run --all-files --show-diff-on-failure || ( \ 39 | echo "\n\n\n * pre-commit should have fixed the errors above. Running again to make sure everything is good..." \ 40 | && pre-commit run --all-files --show-diff-on-failure \ 41 | ) 42 | 43 | test: 44 | python -m pytest tests 45 | 46 | # docs commands 47 | 48 | docs: 49 | python ./newsfragments/validate_files.py 50 | towncrier build --draft --version preview 51 | 52 | # release commands 53 | 54 | package-test: clean 55 | python -m build 56 | python scripts/release/test_package.py 57 | 58 | notes: check-bump 59 | # Let UPCOMING_VERSION be the version that is used for the current bump 60 | $(eval UPCOMING_VERSION=$(shell bump-my-version bump --dry-run $(bump) -v | awk -F"'" '/New version will be / {print $$2}')) 61 | # Now generate the release notes to have them included in the release commit 62 | towncrier build --yes --version $(UPCOMING_VERSION) 63 | # Before we bump the version, make sure that the towncrier-generated docs will build 64 | make docs 65 | git commit -m "Compile release notes for v$(UPCOMING_VERSION)" 66 | 67 | release: check-bump check-git clean 68 | # verify that notes command ran correctly 69 | ./newsfragments/validate_files.py is-empty 70 | CURRENT_SIGN_SETTING=$(git config commit.gpgSign) 71 | git config commit.gpgSign true 72 | bump-my-version bump $(bump) 73 | python -m build 74 | git config commit.gpgSign "$(CURRENT_SIGN_SETTING)" 75 | git push upstream && git push upstream --tags 76 | twine upload dist/* 77 | 78 | # release helpers 79 | 80 | check-bump: 81 | ifndef bump 82 | $(error bump must be set, typically: major, minor, patch, or devnum) 83 | endif 84 | 85 | check-git: 86 | # require that upstream is configured for ethereum/py-geth 87 | @if ! git remote -v | grep "upstream[[:space:]]git@github.com:ethereum/py-geth.git (push)\|upstream[[:space:]]https://github.com/ethereum/py-geth (push)"; then \ 88 | echo "Error: You must have a remote named 'upstream' that points to 'py-geth'"; \ 89 | exit 1; \ 90 | fi 91 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # py-geth 2 | 3 | [![Join the conversation on Discord](https://img.shields.io/discord/809793915578089484?color=blue&label=chat&logo=discord&logoColor=white)](https://discord.gg/GHryRvPB84) 4 | [![Build Status](https://circleci.com/gh/ethereum/py-geth.svg?style=shield)](https://circleci.com/gh/ethereum/py-geth) 5 | [![PyPI version](https://badge.fury.io/py/py-geth.svg)](https://badge.fury.io/py/py-geth) 6 | [![Python versions](https://img.shields.io/pypi/pyversions/py-geth.svg)](https://pypi.python.org/pypi/py-geth) 7 | 8 | Python wrapper around running `geth` as a subprocess 9 | 10 | ## System Dependency 11 | 12 | This library requires the `geth` executable to be present. 13 | 14 | > If managing your own bundled version of geth, set the path to the binary using the `GETH_BINARY` environment variable. 15 | 16 | ## Installation 17 | 18 | ```bash 19 | python -m pip install py-geth 20 | ``` 21 | 22 | ## Quickstart 23 | 24 | To run geth connected to the mainnet 25 | 26 | ```python 27 | >>> from geth import MainnetGethProcess 28 | >>> geth = MainnetGethProcess() 29 | >>> geth.start() 30 | ``` 31 | 32 | Or in dev mode for testing. These require you to give them a name. 33 | 34 | ```python 35 | >>> from geth import DevGethProcess 36 | >>> geth = DevGethProcess('testing') 37 | >>> geth.start() 38 | ``` 39 | 40 | By default the `DevGethProcess` sets up test chains in the default `datadir` 41 | used by `geth`. If you would like to change the location for these test 42 | chains, you can specify an alternative `base_dir`. 43 | 44 | ```python 45 | >>> geth = DevGethProcess('testing', '/tmp/some-other-base-dir/') 46 | >>> geth.start() 47 | ``` 48 | 49 | Each instance has a few convenient properties. 50 | 51 | ```python 52 | >>> geth.data_dir 53 | "~/.ethereum" 54 | >>> geth.rpc_port 55 | 8545 56 | >>> geth.ipc_path 57 | "~/.ethereum/geth.ipc" 58 | >>> geth.accounts 59 | ['0xd3cda913deb6f67967b99d67acdfa1712c293601'] 60 | >>> geth.is_alive 61 | False 62 | >>> geth.is_running 63 | False 64 | >>> geth.is_stopped 65 | False 66 | >>> geth.start() 67 | >>> geth.is_alive 68 | True # indicates that the subprocess hasn't exited 69 | >>> geth.is_running 70 | True # indicates that `start()` has been called (but `stop()` hasn't) 71 | >>> geth.is_stopped 72 | False 73 | >>> geth.stop() 74 | >>> geth.is_alive 75 | False 76 | >>> geth.is_running 77 | False 78 | >>> geth.is_stopped 79 | True 80 | >>> geth.version 81 | "1.15.11-stable" 82 | ``` 83 | 84 | When testing it can be nice to see the logging output produced by the `geth` 85 | process. `py-geth` provides a mixin class that can be used to log the stdout 86 | and stderr output to a logfile. 87 | 88 | ```python 89 | >>> from geth import LoggingMixin, DevGethProcess 90 | >>> class MyGeth(LoggingMixin, DevGethProcess): 91 | ... pass 92 | >>> geth = MyGeth() 93 | >>> geth.start() 94 | ``` 95 | 96 | All logs will be written to logfiles in `./logs/` in the current directory. 97 | 98 | The underlying `geth` process can take additional time to open the RPC or IPC 99 | connections. You can use the following interfaces to query whether these are ready. 100 | 101 | ```python 102 | >>> geth.wait_for_rpc(timeout=30) # wait up to 30 seconds for the RPC connection to open 103 | >>> geth.is_rpc_ready 104 | True 105 | >>> geth.wait_for_ipc(timeout=30) # wait up to 30 seconds for the IPC socket to open 106 | >>> geth.is_ipc_ready 107 | True 108 | ``` 109 | 110 | ## Installing specific versions of `geth` 111 | 112 | > This feature is experimental and subject to breaking changes. 113 | 114 | Versions of `geth` dating back to v1.14.0 can be installed using `py-geth`. 115 | See [install.py](https://github.com/ethereum/py-geth/blob/main/geth/install.py) for 116 | the current list of supported versions. 117 | 118 | Installation can be done via the command line: 119 | 120 | ```bash 121 | $ python -m geth.install v1.15.11 122 | ``` 123 | 124 | Or from python using the `install_geth` function. 125 | 126 | ```python 127 | >>> from geth import install_geth 128 | >>> install_geth('v1.15.11') 129 | ``` 130 | 131 | The installed binary can be found in the `$HOME/.py-geth` directory, under your 132 | home directory. The `v1.15.11` binary would be located at 133 | `$HOME/.py-geth/geth-v1.15.11/bin/geth`. 134 | 135 | ## About `DevGethProcess` 136 | 137 | The `DevGethProcess` will run geth in `--dev` mode and is designed to facilitate testing. 138 | In that regard, it is preconfigured as follows. 139 | 140 | - A single account is created, allocated 1 billion ether, and assigned as the coinbase. 141 | - All APIs are enabled on both `rpc` and `ipc` interfaces. 142 | - Networking is configured to not look for or connect to any peers. 143 | - The `networkid` of `1234` is used. 144 | - Verbosity is set to `5` (DEBUG) 145 | - The RPC interface *tries* to bind to 8545 but will find an open port if this 146 | port is not available. 147 | - The DevP2P interface *tries* to bind to 30303 but will find an open port if this 148 | port is not available. 149 | 150 | ## Development 151 | 152 | Clone the repository: 153 | 154 | ```shell 155 | $ git clone git@github.com:ethereum/py-geth.git 156 | ``` 157 | 158 | Next, run the following from the newly-created `py-geth` directory: 159 | 160 | ```sh 161 | $ python -m pip install -e ".[dev]" 162 | ``` 163 | 164 | ### Running the tests 165 | 166 | You can run the tests with: 167 | 168 | ```sh 169 | pytest tests 170 | ``` 171 | 172 | ## Developer Setup 173 | 174 | If you would like to hack on py-geth, please check out the [Snake Charmers 175 | Tactical Manual](https://github.com/ethereum/snake-charmers-tactical-manual) 176 | for information on how we do: 177 | 178 | - Testing 179 | - Pull Requests 180 | - Documentation 181 | 182 | We use [pre-commit](https://pre-commit.com/) to maintain consistent code style. Once 183 | installed, it will run automatically with every commit. You can also run it manually 184 | with `make lint`. If you need to make a commit that skips the `pre-commit` checks, you 185 | can do so with `git commit --no-verify`. 186 | 187 | ### Development Environment Setup 188 | 189 | You can set up your dev environment with: 190 | 191 | ```sh 192 | git clone git@github.com:ethereum/py-geth.git 193 | cd py-geth 194 | virtualenv -p python3 venv 195 | . venv/bin/activate 196 | python -m pip install -e ".[dev]" 197 | pre-commit install 198 | ``` 199 | 200 | ### Release setup 201 | 202 | To release a new version: 203 | 204 | ```sh 205 | make release bump=$$VERSION_PART_TO_BUMP$$ 206 | ``` 207 | 208 | #### How to bumpversion 209 | 210 | The version format for this repo is `{major}.{minor}.{patch}` for stable, and 211 | `{major}.{minor}.{patch}-{stage}.{devnum}` for unstable (`stage` can be alpha or beta). 212 | 213 | To issue the next version in line, specify which part to bump, 214 | like `make release bump=minor` or `make release bump=devnum`. This is typically done from the 215 | main branch, except when releasing a beta (in which case the beta is released from main, 216 | and the previous stable branch is released from said branch). 217 | 218 | If you are in a beta version, `make release bump=stage` will switch to a stable. 219 | 220 | To issue an unstable version when the current version is stable, specify the 221 | new version explicitly, like `make release bump="--new-version 4.0.0-alpha.1 devnum"` 222 | 223 | ## Adding Support For New Geth Versions 224 | 225 | There is an automation script to facilitate adding support for new geth versions: `update_geth.py` 226 | 227 | To add support for a geth version, run the following line from the py-geth directory, substituting 228 | the version for the one you wish to add support for. Note that the `v` in the versioning is 229 | optional. 230 | 231 | ```shell 232 | $ python update_geth.py v1_14_0 233 | ``` 234 | 235 | To introduce support for more than one version, pass in the versions in increasing order, 236 | ending with the latest version. 237 | 238 | ```shell 239 | $ python update_geth.py v1_14_0 v1_14_2 v1_14_3 240 | ``` 241 | 242 | Always review your changes before committing as something may cause this existing pattern to change at some point. 243 | It is best to compare the git difference with a previous commit that introduced support for a new geth version to make 244 | sure everything looks good. 245 | -------------------------------------------------------------------------------- /conftest.py: -------------------------------------------------------------------------------- 1 | import pytest 2 | import json 3 | 4 | import requests 5 | 6 | 7 | @pytest.fixture 8 | def open_port(): 9 | from geth.utils import ( 10 | get_open_port, 11 | ) 12 | 13 | return get_open_port() 14 | 15 | 16 | @pytest.fixture() 17 | def rpc_client(open_port): 18 | from testrpc.client.utils import ( 19 | force_obj_to_text, 20 | ) 21 | 22 | endpoint = f"http://127.0.0.1:{open_port}" 23 | 24 | def make_request(method, params=None): 25 | global nonce 26 | nonce += 1 27 | payload = { 28 | "id": nonce, 29 | "jsonrpc": "2.0", 30 | "method": method, 31 | "params": params or [], 32 | } 33 | payload_data = json.dumps(force_obj_to_text(payload, True)) 34 | 35 | response = requests.post( 36 | endpoint, 37 | data=payload_data, 38 | headers={ 39 | "Content-Type": "application/json", 40 | }, 41 | ) 42 | 43 | result = response.json() 44 | 45 | if "error" in result: 46 | raise AssertionError(result["error"]) 47 | 48 | return result["result"] 49 | 50 | return make_request 51 | 52 | 53 | @pytest.fixture() 54 | def data_dir(tmpdir): 55 | return str(tmpdir.mkdir("data-dir")) 56 | 57 | 58 | @pytest.fixture() 59 | def base_dir(tmpdir): 60 | return str(tmpdir.mkdir("base-dir")) 61 | -------------------------------------------------------------------------------- /geth/__init__.py: -------------------------------------------------------------------------------- 1 | from importlib.metadata import ( 2 | version as __version, 3 | ) 4 | 5 | from .install import ( 6 | install_geth, 7 | ) 8 | from .main import ( 9 | get_geth_version, 10 | ) 11 | from .mixins import ( 12 | InterceptedStreamsMixin, 13 | LoggingMixin, 14 | ) 15 | from .process import ( 16 | DevGethProcess, 17 | MainnetGethProcess, 18 | SepoliaGethProcess, 19 | TestnetGethProcess, 20 | ) 21 | 22 | __version__ = __version("py-geth") 23 | 24 | __all__ = ( 25 | "install_geth", 26 | "get_geth_version", 27 | "InterceptedStreamsMixin", 28 | "LoggingMixin", 29 | "MainnetGethProcess", 30 | "SepoliaGethProcess", 31 | "TestnetGethProcess", 32 | "DevGethProcess", 33 | ) 34 | -------------------------------------------------------------------------------- /geth/accounts.py: -------------------------------------------------------------------------------- 1 | from __future__ import ( 2 | annotations, 3 | ) 4 | 5 | import os 6 | import re 7 | 8 | from typing_extensions import ( 9 | Unpack, 10 | ) 11 | 12 | from geth.exceptions import ( 13 | PyGethValueError, 14 | ) 15 | from geth.types import ( 16 | GethKwargsTypedDict, 17 | ) 18 | from geth.utils.validation import ( 19 | validate_geth_kwargs, 20 | ) 21 | 22 | from .utils.proc import ( 23 | format_error_message, 24 | ) 25 | from .wrapper import ( 26 | spawn_geth, 27 | ) 28 | 29 | 30 | def get_accounts( 31 | **geth_kwargs: Unpack[GethKwargsTypedDict], 32 | ) -> tuple[str, ...] | tuple[()]: 33 | """ 34 | Returns all geth accounts as tuple of hex encoded strings 35 | 36 | >>> get_accounts(data_dir='some/data/dir') 37 | ... ('0x...', '0x...') 38 | """ 39 | validate_geth_kwargs(geth_kwargs) 40 | 41 | if not geth_kwargs.get("data_dir"): 42 | raise PyGethValueError("data_dir is required to get accounts") 43 | 44 | geth_kwargs["suffix_args"] = ["account", "list"] 45 | 46 | command, proc = spawn_geth(geth_kwargs) 47 | stdoutdata, stderrdata = proc.communicate() 48 | 49 | if proc.returncode: 50 | if "no keys in store" in stderrdata.decode(): 51 | return tuple() 52 | else: 53 | raise PyGethValueError( 54 | format_error_message( 55 | "Error trying to list accounts", 56 | command, 57 | proc.returncode, 58 | stdoutdata.decode(), 59 | stderrdata.decode(), 60 | ) 61 | ) 62 | accounts = parse_geth_accounts(stdoutdata) 63 | return accounts 64 | 65 | 66 | account_regex = re.compile(b"([a-f0-9]{40})") 67 | 68 | 69 | def create_new_account(**geth_kwargs: Unpack[GethKwargsTypedDict]) -> str: 70 | r""" 71 | Creates a new Ethereum account on geth. 72 | 73 | This is useful for testing when you want to stress 74 | interaction (transfers) between Ethereum accounts. 75 | 76 | This command communicates with ``geth`` command over 77 | terminal interaction. It creates keystore folder and new 78 | account there. 79 | 80 | This function only works against offline geth processes, 81 | because geth builds an account cache when starting up. 82 | If geth process is already running you can create new 83 | accounts using 84 | `web3.personal.newAccount() 85 | _` 86 | 87 | RPC API. 88 | 89 | Example pytest fixture for tests: 90 | 91 | .. code-block:: python 92 | 93 | import os 94 | 95 | from geth.wrapper import DEFAULT_PASSWORD_PATH 96 | from geth.accounts import create_new_account 97 | 98 | 99 | @pytest.fixture 100 | def target_account() -> str: 101 | '''Create a new Ethereum account on a running Geth node. 102 | 103 | The account can be used as a withdrawal target for tests. 104 | 105 | :return: 0x address of the account 106 | ''' 107 | 108 | # We store keystore files in the current working directory 109 | # of the test run 110 | data_dir = os.getcwd() 111 | 112 | # Use the default password "this-is-not-a-secure-password" 113 | # as supplied in geth/default_blockchain_password file. 114 | # The supplied password must be bytes, not string, 115 | # as we only want ASCII characters and do not want to 116 | # deal encoding problems with passwords 117 | account = create_new_account(data_dir, DEFAULT_PASSWORD_PATH) 118 | return account 119 | 120 | :param \**geth_kwargs: 121 | Command line arguments to pass to geth. See below: 122 | 123 | :Required Keyword Arguments: 124 | * *data_dir* (``str``) -- 125 | Geth datadir path - where to keep "keystore" folder 126 | * *password* (``str`` or ``bytes``) -- 127 | Password to use for the new account, either the password as bytes or a str 128 | path to a file containing the password. 129 | 130 | :return: Account as 0x prefixed hex string 131 | :rtype: str 132 | """ 133 | if not geth_kwargs.get("data_dir"): 134 | raise PyGethValueError("data_dir is required to create a new account") 135 | 136 | if not geth_kwargs.get("password"): 137 | raise PyGethValueError("password is required to create a new account") 138 | 139 | password = geth_kwargs.get("password") 140 | 141 | geth_kwargs.update({"suffix_args": ["account", "new"]}) 142 | validate_geth_kwargs(geth_kwargs) 143 | 144 | if isinstance(password, str): 145 | if not os.path.exists(password): 146 | raise PyGethValueError(f"Password file not found at path: {password}") 147 | elif not isinstance(password, bytes): 148 | raise PyGethValueError( 149 | "Password must be either a str (path to a file) or bytes" 150 | ) 151 | 152 | command, proc = spawn_geth(geth_kwargs) 153 | 154 | if isinstance(password, str): 155 | stdoutdata, stderrdata = proc.communicate() 156 | else: 157 | stdoutdata, stderrdata = proc.communicate(b"\n".join((password, password))) 158 | 159 | if proc.returncode: 160 | raise PyGethValueError( 161 | format_error_message( 162 | "Error trying to create a new account", 163 | command, 164 | proc.returncode, 165 | stdoutdata.decode(), 166 | stderrdata.decode(), 167 | ) 168 | ) 169 | 170 | match = account_regex.search(stdoutdata) 171 | if not match: 172 | raise PyGethValueError( 173 | format_error_message( 174 | "Did not find an address in process output", 175 | command, 176 | proc.returncode, 177 | stdoutdata.decode(), 178 | stderrdata.decode(), 179 | ) 180 | ) 181 | 182 | return "0x" + match.groups()[0].decode() 183 | 184 | 185 | def ensure_account_exists(**geth_kwargs: Unpack[GethKwargsTypedDict]) -> str: 186 | if not geth_kwargs.get("data_dir"): 187 | raise PyGethValueError("data_dir is required to get accounts") 188 | 189 | validate_geth_kwargs(geth_kwargs) 190 | accounts = get_accounts(**geth_kwargs) 191 | if not accounts: 192 | account = create_new_account(**geth_kwargs) 193 | else: 194 | account = accounts[0] 195 | return account 196 | 197 | 198 | def parse_geth_accounts(raw_accounts_output: bytes) -> tuple[str, ...]: 199 | accounts = account_regex.findall(raw_accounts_output) 200 | accounts_set = set(accounts) # remove duplicates 201 | return tuple("0x" + account.decode() for account in accounts_set) 202 | -------------------------------------------------------------------------------- /geth/chain.py: -------------------------------------------------------------------------------- 1 | from __future__ import ( 2 | annotations, 3 | ) 4 | 5 | import json 6 | import os 7 | import subprocess 8 | import sys 9 | 10 | from typing_extensions import ( 11 | Unpack, 12 | ) 13 | 14 | from geth.exceptions import ( 15 | PyGethValueError, 16 | ) 17 | from geth.types import ( 18 | GenesisDataTypedDict, 19 | ) 20 | 21 | from .utils.encoding import ( 22 | force_obj_to_text, 23 | ) 24 | from .utils.filesystem import ( 25 | ensure_path_exists, 26 | is_same_path, 27 | ) 28 | from .utils.validation import ( 29 | validate_genesis_data, 30 | ) 31 | from .wrapper import ( 32 | get_geth_binary_path, 33 | ) 34 | 35 | 36 | def get_live_data_dir() -> str: 37 | """ 38 | `py-geth` needs a base directory to store it's chain data. By default this is 39 | the directory that `geth` uses as it's `datadir`. 40 | """ 41 | if sys.platform == "darwin": 42 | data_dir = os.path.expanduser( 43 | os.path.join( 44 | "~", 45 | "Library", 46 | "Ethereum", 47 | ) 48 | ) 49 | elif sys.platform in {"linux", "linux2", "linux3"}: 50 | data_dir = os.path.expanduser( 51 | os.path.join( 52 | "~", 53 | ".ethereum", 54 | ) 55 | ) 56 | elif sys.platform == "win32": 57 | data_dir = os.path.expanduser( 58 | os.path.join( 59 | "\\", 60 | "~", 61 | "AppData", 62 | "Roaming", 63 | "Ethereum", 64 | ) 65 | ) 66 | 67 | else: 68 | raise PyGethValueError( 69 | f"Unsupported platform: '{sys.platform}'. Only darwin/linux2/win32 are" 70 | " supported. You must specify the geth datadir manually" 71 | ) 72 | return data_dir 73 | 74 | 75 | def get_sepolia_data_dir() -> str: 76 | return os.path.abspath( 77 | os.path.expanduser( 78 | os.path.join( 79 | get_live_data_dir(), 80 | "sepolia", 81 | ) 82 | ) 83 | ) 84 | 85 | 86 | def get_default_base_dir() -> str: 87 | return get_live_data_dir() 88 | 89 | 90 | def get_chain_data_dir(base_dir: str, name: str) -> str: 91 | data_dir = os.path.abspath(os.path.join(base_dir, name)) 92 | ensure_path_exists(data_dir) 93 | return data_dir 94 | 95 | 96 | def get_genesis_file_path(data_dir: str) -> str: 97 | return os.path.join(data_dir, "genesis.json") 98 | 99 | 100 | def is_live_chain(data_dir: str) -> bool: 101 | return is_same_path(data_dir, get_live_data_dir()) 102 | 103 | 104 | def is_sepolia_chain(data_dir: str) -> bool: 105 | return is_same_path(data_dir, get_sepolia_data_dir()) 106 | 107 | 108 | def write_genesis_file( 109 | genesis_file_path: str, 110 | overwrite: bool = False, 111 | **genesis_data: Unpack[GenesisDataTypedDict], 112 | ) -> None: 113 | if os.path.exists(genesis_file_path) and not overwrite: 114 | raise PyGethValueError( 115 | "Genesis file already present. Call with " 116 | "`overwrite=True` to overwrite this file" 117 | ) 118 | 119 | validate_genesis_data(genesis_data) 120 | 121 | with open(genesis_file_path, "w") as genesis_file: 122 | genesis_file.write(force_obj_to_text(json.dumps(genesis_data))) 123 | 124 | 125 | def initialize_chain(genesis_data: GenesisDataTypedDict, data_dir: str) -> None: 126 | validate_genesis_data(genesis_data) 127 | # init with genesis.json 128 | genesis_file_path = get_genesis_file_path(data_dir) 129 | write_genesis_file(genesis_file_path, **genesis_data) 130 | init_proc = subprocess.Popen( 131 | ( 132 | get_geth_binary_path(), 133 | "--datadir", 134 | data_dir, 135 | "init", 136 | genesis_file_path, 137 | ), 138 | stdin=subprocess.PIPE, 139 | stdout=subprocess.PIPE, 140 | stderr=subprocess.PIPE, 141 | ) 142 | stdoutdata, stderrdata = init_proc.communicate() 143 | init_proc.wait() 144 | if init_proc.returncode: 145 | raise PyGethValueError( 146 | "Error initializing genesis.json: \n" 147 | f" stdout={stdoutdata.decode()}\n" 148 | f" stderr={stderrdata.decode()}" 149 | ) 150 | -------------------------------------------------------------------------------- /geth/default_blockchain_password: -------------------------------------------------------------------------------- 1 | this-is-not-a-secure-password 2 | -------------------------------------------------------------------------------- /geth/exceptions.py: -------------------------------------------------------------------------------- 1 | from __future__ import ( 2 | annotations, 3 | ) 4 | 5 | import codecs 6 | import textwrap 7 | from typing import ( 8 | Any, 9 | ) 10 | 11 | 12 | def force_text_maybe(value: bytes | bytearray | str | None) -> str | None: 13 | if isinstance(value, (bytes, bytearray)): 14 | return codecs.decode(value, "utf8") 15 | elif isinstance(value, str) or value is None: 16 | return value 17 | else: 18 | raise PyGethTypeError(f"Unsupported type: {type(value)}") 19 | 20 | 21 | class PyGethException(Exception): 22 | """ 23 | Exception mixin inherited by all exceptions of py-geth 24 | 25 | This allows:: 26 | 27 | try: 28 | some_call() 29 | except PyGethException: 30 | # deal with py-geth exception 31 | except: 32 | # deal with other exceptions 33 | """ 34 | 35 | user_message: str | None = None 36 | 37 | def __init__( 38 | self, 39 | *args: Any, 40 | user_message: str | None = None, 41 | ): 42 | super().__init__(*args) 43 | 44 | # Assign properties of PyGethException 45 | self.user_message = user_message 46 | 47 | 48 | class GethError(Exception): 49 | message = "An error occurred during execution" 50 | 51 | def __init__( 52 | self, 53 | command: list[str], 54 | return_code: int, 55 | stdin_data: str | bytes | bytearray | None = None, 56 | stdout_data: str | bytes | bytearray | None = None, 57 | stderr_data: str | bytes | bytearray | None = None, 58 | message: str | None = None, 59 | ): 60 | if message is not None: 61 | self.message = message 62 | self.command = command 63 | self.return_code = return_code 64 | self.stdin_data = force_text_maybe(stdin_data) 65 | self.stderr_data = force_text_maybe(stderr_data) 66 | self.stdout_data = force_text_maybe(stdout_data) 67 | 68 | def __str__(self) -> str: 69 | return textwrap.dedent( 70 | f""" 71 | {self.message} 72 | > command: `{" ".join(self.command)}` 73 | > return code: `{self.return_code}` 74 | > stderr: 75 | {self.stdout_data} 76 | > stdout: 77 | {self.stderr_data} 78 | """ 79 | ).strip() 80 | 81 | 82 | class PyGethGethError(PyGethException, GethError): 83 | def __init__( 84 | self, 85 | *args: Any, 86 | **kwargs: Any, 87 | ): 88 | GethError.__init__(*args, **kwargs) 89 | 90 | 91 | class PyGethAttributeError(PyGethException, AttributeError): 92 | pass 93 | 94 | 95 | class PyGethKeyError(PyGethException, KeyError): 96 | pass 97 | 98 | 99 | class PyGethTypeError(PyGethException, TypeError): 100 | pass 101 | 102 | 103 | class PyGethValueError(PyGethException, ValueError): 104 | pass 105 | 106 | 107 | class PyGethOSError(PyGethException, OSError): 108 | pass 109 | 110 | 111 | class PyGethNotImplementedError(PyGethException, NotImplementedError): 112 | pass 113 | 114 | 115 | class PyGethFileNotFoundError(PyGethException, FileNotFoundError): 116 | pass 117 | -------------------------------------------------------------------------------- /geth/genesis.json: -------------------------------------------------------------------------------- 1 | { 2 | "config": { 3 | "ethash": {}, 4 | "homesteadBlock": 0, 5 | "eip150Block": 0, 6 | "eip155Block": 0, 7 | "eip158Block": 0, 8 | "byzantiumBlock": 0, 9 | "constantinopleBlock": 0, 10 | "petersburgBlock": 0, 11 | "istanbulBlock": 0, 12 | "berlinBlock": 0, 13 | "londonBlock": 0, 14 | "arrowGlacierBlock": 0, 15 | "grayGlacierBlock": 0, 16 | "terminalTotalDifficulty": 0, 17 | "terminalTotalDifficultyPassed": true, 18 | "shanghaiTime": 0, 19 | "cancunTime": 0, 20 | "blobSchedule": { 21 | "cancun": { 22 | "target": 3, 23 | "max": 6, 24 | "baseFeeUpdateFraction": 3338477 25 | }, 26 | "prague": { 27 | "target": 6, 28 | "max": 9, 29 | "baseFeeUpdateFraction": 5007716 30 | } 31 | } 32 | 33 | }, 34 | "nonce": "0x0", 35 | "timestamp": "0x0", 36 | "parentHash": "0x0000000000000000000000000000000000000000000000000000000000000000", 37 | "extraData": "0x0000000000000000000000000000000000000000000000000000000000000000", 38 | "gasLimit": "0x47e7c4", 39 | "difficulty": "0x0", 40 | "mixHash": "0x0000000000000000000000000000000000000000000000000000000000000000", 41 | "alloc": {} 42 | } 43 | -------------------------------------------------------------------------------- /geth/install.py: -------------------------------------------------------------------------------- 1 | """ 2 | Install geth 3 | """ 4 | from __future__ import ( 5 | annotations, 6 | ) 7 | 8 | import contextlib 9 | import functools 10 | import os 11 | import stat 12 | import subprocess 13 | import sys 14 | import tarfile 15 | from typing import ( 16 | Any, 17 | Generator, 18 | ) 19 | 20 | import requests 21 | from requests.exceptions import ( 22 | ConnectionError, 23 | HTTPError, 24 | Timeout, 25 | ) 26 | 27 | from geth.exceptions import ( 28 | PyGethException, 29 | PyGethKeyError, 30 | PyGethOSError, 31 | PyGethValueError, 32 | ) 33 | from geth.types import ( 34 | IO_Any, 35 | ) 36 | 37 | V1_14_0 = "v1.14.0" 38 | V1_14_2 = "v1.14.2" 39 | V1_14_3 = "v1.14.3" 40 | V1_14_4 = "v1.14.4" 41 | V1_14_5 = "v1.14.5" 42 | V1_14_6 = "v1.14.6" 43 | V1_14_7 = "v1.14.7" 44 | V1_14_8 = "v1.14.8" 45 | V1_14_9 = "v1.14.9" 46 | V1_14_10 = "v1.14.10" 47 | V1_14_11 = "v1.14.11" 48 | V1_14_12 = "v1.14.12" 49 | V1_14_13 = "v1.14.13" 50 | V1_15_0 = "v1.15.0" 51 | V1_15_1 = "v1.15.1" 52 | V1_15_2 = "v1.15.2" 53 | V1_15_3 = "v1.15.3" 54 | V1_15_4 = "v1.15.4" 55 | V1_15_5 = "v1.15.5" 56 | V1_15_6 = "v1.15.6" 57 | V1_15_7 = "v1.15.7" 58 | V1_15_8 = "v1.15.8" 59 | V1_15_9 = "v1.15.9" 60 | V1_15_10 = "v1.15.10" 61 | V1_15_11 = "v1.15.11" 62 | 63 | 64 | LINUX = "linux" 65 | OSX = "darwin" 66 | WINDOWS = "win32" 67 | 68 | 69 | # 70 | # System utilities. 71 | # 72 | @contextlib.contextmanager 73 | def chdir(path: str) -> Generator[None, None, None]: 74 | original_path = os.getcwd() 75 | try: 76 | os.chdir(path) 77 | yield 78 | finally: 79 | os.chdir(original_path) 80 | 81 | 82 | def get_platform() -> str: 83 | if sys.platform.startswith("linux"): 84 | return LINUX 85 | elif sys.platform == OSX: 86 | return OSX 87 | elif sys.platform == WINDOWS: 88 | return WINDOWS 89 | else: 90 | raise PyGethKeyError(f"Unknown platform: {sys.platform}") 91 | 92 | 93 | def is_executable_available(program: str) -> bool: 94 | def is_exe(fpath: str) -> bool: 95 | return os.path.isfile(fpath) and os.access(fpath, os.X_OK) 96 | 97 | fpath = os.path.dirname(program) 98 | if fpath: 99 | if is_exe(program): 100 | return True 101 | else: 102 | for path in os.environ["PATH"].split(os.pathsep): 103 | path = path.strip('"') 104 | exe_file = os.path.join(path, program) 105 | if is_exe(exe_file): 106 | return True 107 | 108 | return False 109 | 110 | 111 | def ensure_path_exists(dir_path: str) -> bool: 112 | """ 113 | Make sure that a path exists 114 | """ 115 | if not os.path.exists(dir_path): 116 | os.makedirs(dir_path) 117 | return True 118 | return False 119 | 120 | 121 | def ensure_parent_dir_exists(path: str) -> None: 122 | ensure_path_exists(os.path.dirname(path)) 123 | 124 | 125 | def check_subprocess_call( 126 | command: list[str], 127 | message: str | None = None, 128 | stderr: IO_Any = subprocess.STDOUT, 129 | **proc_kwargs: Any, 130 | ) -> int: 131 | if message: 132 | print(message) 133 | print(f"Executing: {' '.join(command)}") 134 | 135 | return subprocess.check_call(command, stderr=stderr, **proc_kwargs) 136 | 137 | 138 | def check_subprocess_output( 139 | command: list[str], 140 | message: str | None = None, 141 | stderr: IO_Any = subprocess.STDOUT, 142 | **proc_kwargs: Any, 143 | ) -> Any: 144 | if message: 145 | print(message) 146 | print(f"Executing: {' '.join(command)}") 147 | 148 | return subprocess.check_output(command, stderr=stderr, **proc_kwargs) 149 | 150 | 151 | def chmod_plus_x(executable_path: str) -> None: 152 | current_st = os.stat(executable_path) 153 | os.chmod(executable_path, current_st.st_mode | stat.S_IEXEC) 154 | 155 | 156 | def get_go_executable_path() -> str: 157 | return os.environ.get("GO_BINARY", "go") 158 | 159 | 160 | def is_go_available() -> bool: 161 | return is_executable_available(get_go_executable_path()) 162 | 163 | 164 | # 165 | # Installation filesystem path utilities 166 | # 167 | def get_base_install_path(identifier: str) -> str: 168 | if "GETH_BASE_INSTALL_PATH" in os.environ: 169 | return os.path.join( 170 | os.environ["GETH_BASE_INSTALL_PATH"], 171 | f"geth-{identifier}", 172 | ) 173 | else: 174 | return os.path.expanduser( 175 | os.path.join( 176 | "~", 177 | ".py-geth", 178 | f"geth-{identifier}", 179 | ) 180 | ) 181 | 182 | 183 | def get_source_code_archive_path(identifier: str) -> str: 184 | return os.path.join( 185 | get_base_install_path(identifier), 186 | "release.tar.gz", 187 | ) 188 | 189 | 190 | def get_source_code_extract_path(identifier: str) -> str: 191 | return os.path.join( 192 | get_base_install_path(identifier), 193 | "source", 194 | ) 195 | 196 | 197 | def get_source_code_path(identifier: str) -> str: 198 | return os.path.join( 199 | get_base_install_path(identifier), 200 | "source", 201 | f"go-ethereum-{identifier.lstrip('v')}", 202 | ) 203 | 204 | 205 | def get_build_path(identifier: str) -> str: 206 | source_code_path = get_source_code_path(identifier) 207 | return os.path.join( 208 | source_code_path, 209 | "build", 210 | ) 211 | 212 | 213 | def get_built_executable_path(identifier: str) -> str: 214 | build_path = get_build_path(identifier) 215 | return os.path.join( 216 | build_path, 217 | "bin", 218 | "geth", 219 | ) 220 | 221 | 222 | def get_executable_path(identifier: str) -> str: 223 | base_install_path = get_base_install_path(identifier) 224 | return os.path.join( 225 | base_install_path, 226 | "bin", 227 | "geth", 228 | ) 229 | 230 | 231 | # 232 | # Installation primitives. 233 | # 234 | DOWNLOAD_SOURCE_CODE_URI_TEMPLATE = ( 235 | "https://github.com/ethereum/go-ethereum/archive/{0}.tar.gz" 236 | ) 237 | 238 | 239 | def download_source_code_release(identifier: str) -> None: 240 | download_uri = DOWNLOAD_SOURCE_CODE_URI_TEMPLATE.format(identifier) 241 | source_code_archive_path = get_source_code_archive_path(identifier) 242 | 243 | ensure_parent_dir_exists(source_code_archive_path) 244 | try: 245 | response = requests.get(download_uri) 246 | response.raise_for_status() 247 | with open(source_code_archive_path, "wb") as f: 248 | f.write(response.content) 249 | 250 | print(f"Downloading source code release from {download_uri}") 251 | 252 | except (HTTPError, Timeout, ConnectionError) as e: 253 | raise PyGethException( 254 | f"An error occurred while downloading from {download_uri}: {e}" 255 | ) 256 | 257 | 258 | def extract_source_code_release(identifier: str) -> None: 259 | source_code_archive_path = get_source_code_archive_path(identifier) 260 | source_code_extract_path = get_source_code_extract_path(identifier) 261 | ensure_path_exists(source_code_extract_path) 262 | 263 | print( 264 | f"Extracting archive: {source_code_archive_path} -> {source_code_extract_path}" 265 | ) 266 | 267 | with tarfile.open(source_code_archive_path, "r:gz") as archive_file: 268 | 269 | def is_within_directory(directory: str, target: str) -> bool: 270 | abs_directory = os.path.abspath(directory) 271 | abs_target = os.path.abspath(target) 272 | 273 | prefix = os.path.commonprefix([abs_directory, abs_target]) 274 | 275 | return prefix == abs_directory 276 | 277 | def safe_extract(tar: tarfile.TarFile, path: str = ".") -> None: 278 | for member in tar.getmembers(): 279 | member_path = os.path.join(path, member.name) 280 | if not is_within_directory(path, member_path): 281 | raise PyGethException("Attempted Path Traversal in Tar File") 282 | 283 | tar.extractall(path) 284 | 285 | safe_extract(archive_file, source_code_extract_path) 286 | 287 | 288 | def build_from_source_code(identifier: str) -> None: 289 | if not is_go_available(): 290 | raise PyGethOSError( 291 | "The `go` runtime was not found but is required to build geth. If " 292 | "the `go` executable is not in your $PATH you can specify the path " 293 | "using the environment variable GO_BINARY to specify the path." 294 | ) 295 | source_code_path = get_source_code_path(identifier) 296 | 297 | with chdir(source_code_path): 298 | make_command = ["make", "geth"] 299 | 300 | check_subprocess_call( 301 | make_command, 302 | message="Building `geth` binary", 303 | ) 304 | 305 | built_executable_path = get_built_executable_path(identifier) 306 | if not os.path.exists(built_executable_path): 307 | raise PyGethOSError( 308 | "Built executable not found in expected location: " 309 | f"{built_executable_path}" 310 | ) 311 | print(f"Making built binary executable: chmod +x {built_executable_path}") 312 | chmod_plus_x(built_executable_path) 313 | 314 | executable_path = get_executable_path(identifier) 315 | ensure_parent_dir_exists(executable_path) 316 | if os.path.exists(executable_path): 317 | if os.path.islink(executable_path): 318 | os.remove(executable_path) 319 | else: 320 | raise PyGethOSError( 321 | f"Non-symlink file already present at `{executable_path}`" 322 | ) 323 | os.symlink(built_executable_path, executable_path) 324 | chmod_plus_x(executable_path) 325 | 326 | 327 | def install_from_source_code_release(identifier: str) -> None: 328 | download_source_code_release(identifier) 329 | extract_source_code_release(identifier) 330 | build_from_source_code(identifier) 331 | 332 | executable_path = get_executable_path(identifier) 333 | assert os.path.exists(executable_path), f"Executable not found @ {executable_path}" 334 | 335 | check_version_command = [executable_path, "version"] 336 | 337 | version_output = check_subprocess_output( 338 | check_version_command, 339 | message=f"Checking installed executable version @ {executable_path}", 340 | ) 341 | 342 | print(f"geth successfully installed at: {executable_path}\n\n{version_output}\n\n") 343 | 344 | 345 | install_v1_14_0 = functools.partial(install_from_source_code_release, V1_14_0) 346 | install_v1_14_2 = functools.partial(install_from_source_code_release, V1_14_2) 347 | install_v1_14_3 = functools.partial(install_from_source_code_release, V1_14_3) 348 | install_v1_14_4 = functools.partial(install_from_source_code_release, V1_14_4) 349 | install_v1_14_5 = functools.partial(install_from_source_code_release, V1_14_5) 350 | install_v1_14_6 = functools.partial(install_from_source_code_release, V1_14_6) 351 | install_v1_14_7 = functools.partial(install_from_source_code_release, V1_14_7) 352 | install_v1_14_8 = functools.partial(install_from_source_code_release, V1_14_8) 353 | install_v1_14_9 = functools.partial(install_from_source_code_release, V1_14_9) 354 | install_v1_14_10 = functools.partial(install_from_source_code_release, V1_14_10) 355 | install_v1_14_11 = functools.partial(install_from_source_code_release, V1_14_11) 356 | install_v1_14_12 = functools.partial(install_from_source_code_release, V1_14_12) 357 | install_v1_14_13 = functools.partial(install_from_source_code_release, V1_14_13) 358 | install_v1_15_0 = functools.partial(install_from_source_code_release, V1_15_0) 359 | install_v1_15_1 = functools.partial(install_from_source_code_release, V1_15_1) 360 | install_v1_15_2 = functools.partial(install_from_source_code_release, V1_15_2) 361 | install_v1_15_3 = functools.partial(install_from_source_code_release, V1_15_3) 362 | install_v1_15_4 = functools.partial(install_from_source_code_release, V1_15_4) 363 | install_v1_15_5 = functools.partial(install_from_source_code_release, V1_15_5) 364 | install_v1_15_6 = functools.partial(install_from_source_code_release, V1_15_6) 365 | install_v1_15_7 = functools.partial(install_from_source_code_release, V1_15_7) 366 | install_v1_15_8 = functools.partial(install_from_source_code_release, V1_15_8) 367 | install_v1_15_9 = functools.partial(install_from_source_code_release, V1_15_9) 368 | install_v1_15_10 = functools.partial(install_from_source_code_release, V1_15_10) 369 | install_v1_15_11 = functools.partial(install_from_source_code_release, V1_15_11) 370 | 371 | INSTALL_FUNCTIONS = { 372 | LINUX: { 373 | V1_14_0: install_v1_14_0, 374 | V1_14_2: install_v1_14_2, 375 | V1_14_3: install_v1_14_3, 376 | V1_14_4: install_v1_14_4, 377 | V1_14_5: install_v1_14_5, 378 | V1_14_6: install_v1_14_6, 379 | V1_14_7: install_v1_14_7, 380 | V1_14_8: install_v1_14_8, 381 | V1_14_9: install_v1_14_9, 382 | V1_14_10: install_v1_14_10, 383 | V1_14_11: install_v1_14_11, 384 | V1_14_12: install_v1_14_12, 385 | V1_14_13: install_v1_14_13, 386 | V1_15_0: install_v1_15_0, 387 | V1_15_1: install_v1_15_1, 388 | V1_15_2: install_v1_15_2, 389 | V1_15_3: install_v1_15_3, 390 | V1_15_4: install_v1_15_4, 391 | V1_15_5: install_v1_15_5, 392 | V1_15_6: install_v1_15_6, 393 | V1_15_7: install_v1_15_7, 394 | V1_15_8: install_v1_15_8, 395 | V1_15_9: install_v1_15_9, 396 | V1_15_10: install_v1_15_10, 397 | V1_15_11: install_v1_15_11, 398 | }, 399 | OSX: { 400 | V1_14_0: install_v1_14_0, 401 | V1_14_2: install_v1_14_2, 402 | V1_14_3: install_v1_14_3, 403 | V1_14_4: install_v1_14_4, 404 | V1_14_5: install_v1_14_5, 405 | V1_14_6: install_v1_14_6, 406 | V1_14_7: install_v1_14_7, 407 | V1_14_8: install_v1_14_8, 408 | V1_14_9: install_v1_14_9, 409 | V1_14_10: install_v1_14_10, 410 | V1_14_11: install_v1_14_11, 411 | V1_14_12: install_v1_14_12, 412 | V1_14_13: install_v1_14_13, 413 | V1_15_0: install_v1_15_0, 414 | V1_15_1: install_v1_15_1, 415 | V1_15_2: install_v1_15_2, 416 | V1_15_3: install_v1_15_3, 417 | V1_15_4: install_v1_15_4, 418 | V1_15_5: install_v1_15_5, 419 | V1_15_6: install_v1_15_6, 420 | V1_15_7: install_v1_15_7, 421 | V1_15_8: install_v1_15_8, 422 | V1_15_9: install_v1_15_9, 423 | V1_15_10: install_v1_15_10, 424 | V1_15_11: install_v1_15_11, 425 | }, 426 | } 427 | 428 | 429 | def install_geth(identifier: str, platform: str | None = None) -> None: 430 | if platform is None: 431 | platform = get_platform() 432 | 433 | if platform not in INSTALL_FUNCTIONS: 434 | raise PyGethValueError( 435 | "Installation of go-ethereum is not supported on your platform " 436 | f"({platform}). Supported platforms are: " 437 | f"{', '.join(sorted(INSTALL_FUNCTIONS.keys()))}" 438 | ) 439 | elif identifier not in INSTALL_FUNCTIONS[platform]: 440 | raise PyGethValueError( 441 | f"Installation of geth=={identifier} is not supported. Must be one of " 442 | f"{', '.join(sorted(INSTALL_FUNCTIONS[platform].keys()))}" 443 | ) 444 | 445 | install_fn = INSTALL_FUNCTIONS[platform][identifier] 446 | install_fn() 447 | 448 | 449 | if __name__ == "__main__": 450 | try: 451 | identifier = sys.argv[1] 452 | except IndexError: 453 | print( 454 | "Invocation error. Should be invoked as `python -m geth.install `" # noqa: E501 455 | ) 456 | sys.exit(1) 457 | 458 | install_geth(identifier) 459 | -------------------------------------------------------------------------------- /geth/main.py: -------------------------------------------------------------------------------- 1 | from __future__ import ( 2 | annotations, 3 | ) 4 | 5 | import re 6 | 7 | import semantic_version 8 | from typing_extensions import ( 9 | Unpack, 10 | ) 11 | 12 | from geth.exceptions import ( 13 | PyGethTypeError, 14 | PyGethValueError, 15 | ) 16 | from geth.types import ( 17 | GethKwargsTypedDict, 18 | ) 19 | from geth.utils.validation import ( 20 | validate_geth_kwargs, 21 | ) 22 | 23 | from .utils.encoding import ( 24 | force_text, 25 | ) 26 | from .wrapper import ( 27 | geth_wrapper, 28 | ) 29 | 30 | 31 | def get_geth_version_info_string(**geth_kwargs: Unpack[GethKwargsTypedDict]) -> str: 32 | if "suffix_args" in geth_kwargs: 33 | raise PyGethTypeError( 34 | "The `get_geth_version` function cannot be called with the " 35 | "`suffix_args` parameter" 36 | ) 37 | geth_kwargs["suffix_args"] = ["version"] 38 | validate_geth_kwargs(geth_kwargs) 39 | stdoutdata, stderrdata, command, proc = geth_wrapper(**geth_kwargs) 40 | return stdoutdata.decode("utf-8") 41 | 42 | 43 | VERSION_REGEX = r"Version: (.*)\n" 44 | 45 | 46 | def get_geth_version( 47 | **geth_kwargs: Unpack[GethKwargsTypedDict], 48 | ) -> semantic_version.Version: 49 | validate_geth_kwargs(geth_kwargs) 50 | version_info_string = get_geth_version_info_string(**geth_kwargs) 51 | version_match = re.search(VERSION_REGEX, force_text(version_info_string, "utf8")) 52 | if not version_match: 53 | raise PyGethValueError( 54 | f"Did not match version string in geth output:\n{version_info_string}" 55 | ) 56 | version_string = version_match.groups()[0] 57 | return semantic_version.Version(version_string) 58 | -------------------------------------------------------------------------------- /geth/mixins.py: -------------------------------------------------------------------------------- 1 | from __future__ import ( 2 | annotations, 3 | ) 4 | 5 | import datetime 6 | import logging 7 | import os 8 | import queue 9 | import time 10 | from typing import ( 11 | TYPE_CHECKING, 12 | Any, 13 | Callable, 14 | ) 15 | 16 | from geth.exceptions import ( 17 | PyGethAttributeError, 18 | ) 19 | from geth.utils.filesystem import ( 20 | ensure_path_exists, 21 | ) 22 | from geth.utils.thread import ( 23 | spawn, 24 | ) 25 | from geth.utils.timeout import ( 26 | Timeout, 27 | ) 28 | 29 | 30 | def construct_logger_file_path(prefix: str, suffix: str) -> str: 31 | ensure_path_exists("./logs") 32 | timestamp = datetime.datetime.now().strftime(f"{prefix}-%Y%m%d-%H%M%S-{suffix}.log") 33 | return os.path.join("logs", timestamp) 34 | 35 | 36 | def _get_file_logger(name: str, filename: str) -> logging.Logger: 37 | # create logger with 'spam_application' 38 | logger = logging.getLogger(name) 39 | logger.setLevel(logging.DEBUG) 40 | # create file handler which logs even debug messages 41 | fh = logging.FileHandler(filename) 42 | fh.setLevel(logging.DEBUG) 43 | # create console handler with a higher log level 44 | ch = logging.StreamHandler() 45 | ch.setLevel(logging.ERROR) 46 | # create formatter and add it to the handlers 47 | formatter = logging.Formatter("%(message)s") 48 | fh.setFormatter(formatter) 49 | ch.setFormatter(formatter) 50 | # add the handlers to the logger 51 | logger.addHandler(fh) 52 | logger.addHandler(ch) 53 | 54 | return logger 55 | 56 | 57 | # only needed until we drop support for python 3.8 58 | if TYPE_CHECKING: 59 | BaseQueue = queue.Queue[Any] 60 | else: 61 | BaseQueue = queue.Queue 62 | 63 | 64 | class JoinableQueue(BaseQueue): 65 | def __iter__(self) -> Any: 66 | while True: 67 | item = self.get() 68 | 69 | is_stop_iteration_type = isinstance(item, type) and issubclass( 70 | item, StopIteration 71 | ) 72 | if isinstance(item, StopIteration) or is_stop_iteration_type: 73 | return 74 | 75 | elif isinstance(item, Exception): 76 | raise item 77 | 78 | elif isinstance(item, type) and issubclass(item, Exception): 79 | raise item 80 | 81 | yield item 82 | 83 | def join(self, timeout: int | None = None) -> None: 84 | with Timeout(timeout) as _timeout: 85 | while not self.empty(): 86 | time.sleep(0) 87 | _timeout.check() 88 | 89 | 90 | class InterceptedStreamsMixin: 91 | """ 92 | Mixin class for GethProcess instances that feeds all of the stdout and 93 | stderr lines into some set of provided callback functions. 94 | """ 95 | 96 | stdout_callbacks: list[Callable[[str], None]] 97 | stderr_callbacks: list[Callable[[str], None]] 98 | 99 | def __init__(self, *args: Any, **kwargs: Any): 100 | super().__init__(*args, **kwargs) 101 | self.stdout_callbacks = [] 102 | self.stdout_queue = JoinableQueue() 103 | 104 | self.stderr_callbacks = [] 105 | self.stderr_queue = JoinableQueue() 106 | 107 | def register_stdout_callback(self, callback_fn: Callable[[str], None]) -> None: 108 | self.stdout_callbacks.append(callback_fn) 109 | 110 | def register_stderr_callback(self, callback_fn: Callable[[str], None]) -> None: 111 | self.stderr_callbacks.append(callback_fn) 112 | 113 | def produce_stdout_queue(self) -> None: 114 | if hasattr(self, "proc"): 115 | for line in iter(self.proc.stdout.readline, b""): 116 | self.stdout_queue.put(line) 117 | time.sleep(0) 118 | else: 119 | raise PyGethAttributeError("No `proc` attribute found") 120 | 121 | def produce_stderr_queue(self) -> None: 122 | if hasattr(self, "proc"): 123 | for line in iter(self.proc.stderr.readline, b""): 124 | self.stderr_queue.put(line) 125 | time.sleep(0) 126 | else: 127 | raise PyGethAttributeError("No `proc` attribute found") 128 | 129 | def consume_stdout_queue(self) -> None: 130 | for line in self.stdout_queue: 131 | for fn in self.stdout_callbacks: 132 | fn(line.strip()) 133 | self.stdout_queue.task_done() 134 | time.sleep(0) 135 | 136 | def consume_stderr_queue(self) -> None: 137 | for line in self.stderr_queue: 138 | for fn in self.stderr_callbacks: 139 | fn(line.strip()) 140 | self.stderr_queue.task_done() 141 | time.sleep(0) 142 | 143 | def start(self) -> None: 144 | # type ignored because this is a mixin but will always have a start method 145 | # because it will be mixed with BaseGethProcess 146 | super().start() # type: ignore[misc] 147 | 148 | spawn(self.produce_stdout_queue) 149 | spawn(self.produce_stderr_queue) 150 | 151 | spawn(self.consume_stdout_queue) 152 | spawn(self.consume_stderr_queue) 153 | 154 | def stop(self) -> None: 155 | # type ignored because this is a mixin but will always have a stop method 156 | # because it will be mixed with BaseGethProcess 157 | super().stop() # type: ignore[misc] 158 | 159 | try: 160 | self.stdout_queue.put(StopIteration) 161 | self.stdout_queue.join(5) 162 | except Timeout: 163 | pass 164 | 165 | try: 166 | self.stderr_queue.put(StopIteration) 167 | self.stderr_queue.join(5) 168 | except Timeout: 169 | pass 170 | 171 | 172 | class LoggingMixin(InterceptedStreamsMixin): 173 | def __init__(self, *args: Any, **kwargs: Any): 174 | stdout_logfile_path = kwargs.pop( 175 | "stdout_logfile_path", 176 | construct_logger_file_path("geth", "stdout"), 177 | ) 178 | stderr_logfile_path = kwargs.pop( 179 | "stderr_logfile_path", 180 | construct_logger_file_path("geth", "stderr"), 181 | ) 182 | 183 | super().__init__(*args, **kwargs) 184 | 185 | stdout_logger = _get_file_logger("geth-stdout", stdout_logfile_path) 186 | stderr_logger = _get_file_logger("geth-stderr", stderr_logfile_path) 187 | 188 | self.register_stdout_callback(stdout_logger.info) 189 | self.register_stderr_callback(stderr_logger.info) 190 | -------------------------------------------------------------------------------- /geth/process.py: -------------------------------------------------------------------------------- 1 | from __future__ import ( 2 | annotations, 3 | ) 4 | 5 | from abc import ( 6 | ABC, 7 | abstractmethod, 8 | ) 9 | import copy 10 | import json 11 | import logging 12 | import os 13 | import subprocess 14 | import time 15 | from types import ( 16 | TracebackType, 17 | ) 18 | from typing import ( 19 | cast, 20 | ) 21 | from urllib.error import ( 22 | URLError, 23 | ) 24 | from urllib.request import ( 25 | urlopen, 26 | ) 27 | 28 | import semantic_version 29 | 30 | from geth import ( 31 | get_geth_version, 32 | ) 33 | from geth.accounts import ( 34 | ensure_account_exists, 35 | get_accounts, 36 | ) 37 | from geth.chain import ( 38 | get_chain_data_dir, 39 | get_default_base_dir, 40 | get_genesis_file_path, 41 | get_live_data_dir, 42 | get_sepolia_data_dir, 43 | initialize_chain, 44 | is_live_chain, 45 | is_sepolia_chain, 46 | ) 47 | from geth.exceptions import ( 48 | PyGethNotImplementedError, 49 | PyGethValueError, 50 | ) 51 | from geth.types import ( 52 | GethKwargsTypedDict, 53 | IO_Any, 54 | ) 55 | from geth.utils.networking import ( 56 | get_ipc_socket, 57 | ) 58 | from geth.utils.proc import ( 59 | kill_proc, 60 | ) 61 | from geth.utils.timeout import ( 62 | Timeout, 63 | ) 64 | from geth.utils.validation import ( 65 | GenesisDataTypedDict, 66 | validate_genesis_data, 67 | validate_geth_kwargs, 68 | ) 69 | from geth.wrapper import ( 70 | construct_popen_command, 71 | construct_test_chain_kwargs, 72 | ) 73 | 74 | logger = logging.getLogger(__name__) 75 | with open(os.path.join(os.path.dirname(__file__), "genesis.json")) as genesis_file: 76 | GENESIS_JSON = json.load(genesis_file) 77 | 78 | 79 | class BaseGethProcess(ABC): 80 | _proc = None 81 | 82 | def __init__( 83 | self, 84 | geth_kwargs: GethKwargsTypedDict, 85 | stdin: IO_Any = subprocess.PIPE, 86 | stdout: IO_Any = subprocess.PIPE, 87 | stderr: IO_Any = subprocess.PIPE, 88 | ): 89 | validate_geth_kwargs(geth_kwargs) 90 | self.geth_kwargs = geth_kwargs 91 | self.command = construct_popen_command(**geth_kwargs) 92 | self.stdin = stdin 93 | self.stdout = stdout 94 | self.stderr = stderr 95 | 96 | is_running = False 97 | 98 | def start(self) -> None: 99 | if self.is_running: 100 | raise PyGethValueError("Already running") 101 | self.is_running = True 102 | 103 | logger.info(f"Launching geth: {' '.join(self.command)}") 104 | self.proc = subprocess.Popen( 105 | self.command, 106 | stdin=self.stdin, 107 | stdout=self.stdout, 108 | stderr=self.stderr, 109 | ) 110 | 111 | def __enter__(self) -> BaseGethProcess: 112 | self.start() 113 | return self 114 | 115 | def stop(self) -> None: 116 | if not self.is_running: 117 | raise PyGethValueError("Not running") 118 | 119 | if self.proc.poll() is None: 120 | kill_proc(self.proc) 121 | 122 | self.is_running = False 123 | 124 | def __exit__( 125 | self, 126 | exc_type: type[BaseException] | None, 127 | exc_value: BaseException | None, 128 | tb: TracebackType | None, 129 | ) -> None: 130 | self.stop() 131 | 132 | @property 133 | @abstractmethod 134 | def data_dir(self) -> str: 135 | raise PyGethNotImplementedError("Must be implemented by subclasses.") 136 | 137 | @property 138 | def is_alive(self) -> bool: 139 | return self.is_running and self.proc.poll() is None 140 | 141 | @property 142 | def is_stopped(self) -> bool: 143 | return self.proc is not None and self.proc.poll() is not None 144 | 145 | @property 146 | def accounts(self) -> tuple[str, ...]: 147 | return get_accounts(**self.geth_kwargs) 148 | 149 | @property 150 | def rpc_enabled(self) -> bool: 151 | _rpc_enabled = self.geth_kwargs.get("rpc_enabled", False) 152 | return cast(bool, _rpc_enabled) 153 | 154 | @property 155 | def rpc_host(self) -> str: 156 | _rpc_host = self.geth_kwargs.get("rpc_host", "127.0.0.1") 157 | return cast(str, _rpc_host) 158 | 159 | @property 160 | def rpc_port(self) -> str: 161 | _rpc_port = self.geth_kwargs.get("rpc_port", "8545") 162 | return cast(str, _rpc_port) 163 | 164 | @property 165 | def is_rpc_ready(self) -> bool: 166 | try: 167 | urlopen(f"http://{self.rpc_host}:{self.rpc_port}") 168 | except URLError: 169 | return False 170 | else: 171 | return True 172 | 173 | def wait_for_rpc(self, timeout: int = 0) -> None: 174 | if not self.rpc_enabled: 175 | raise PyGethValueError("RPC interface is not enabled") 176 | 177 | with Timeout(timeout) as _timeout: 178 | while True: 179 | if self.is_rpc_ready: 180 | break 181 | time.sleep(0.1) 182 | _timeout.check() 183 | 184 | @property 185 | def ipc_enabled(self) -> bool: 186 | return not self.geth_kwargs.get("ipc_disable", None) 187 | 188 | @property 189 | def ipc_path(self) -> str: 190 | return self.geth_kwargs.get("ipc_path") or os.path.abspath( 191 | os.path.expanduser( 192 | os.path.join( 193 | self.data_dir, 194 | "geth.ipc", 195 | ) 196 | ) 197 | ) 198 | 199 | @property 200 | def is_ipc_ready(self) -> bool: 201 | try: 202 | with get_ipc_socket(self.ipc_path): 203 | pass 204 | except OSError: 205 | return False 206 | else: 207 | return True 208 | 209 | def wait_for_ipc(self, timeout: int = 0) -> None: 210 | if not self.ipc_enabled: 211 | raise PyGethValueError("IPC interface is not enabled") 212 | 213 | with Timeout(timeout) as _timeout: 214 | while True: 215 | if self.is_ipc_ready: 216 | break 217 | time.sleep(0.1) 218 | _timeout.check() 219 | 220 | @property 221 | def version(self) -> str: 222 | return str(get_geth_version(**self.geth_kwargs)) 223 | 224 | 225 | class MainnetGethProcess(BaseGethProcess): 226 | def __init__(self, geth_kwargs: GethKwargsTypedDict | None = None): 227 | if geth_kwargs is None: 228 | geth_kwargs = {} 229 | 230 | if "data_dir" in geth_kwargs: 231 | raise PyGethValueError( 232 | "You cannot specify `data_dir` for a MainnetGethProcess" 233 | ) 234 | 235 | super().__init__(geth_kwargs) 236 | 237 | @property 238 | def data_dir(self) -> str: 239 | return get_live_data_dir() 240 | 241 | 242 | class SepoliaGethProcess(BaseGethProcess): 243 | def __init__(self, geth_kwargs: GethKwargsTypedDict | None = None): 244 | if geth_kwargs is None: 245 | geth_kwargs = {} 246 | 247 | if "data_dir" in geth_kwargs: 248 | raise PyGethValueError( 249 | f"You cannot specify `data_dir` for a {type(self).__name__}" 250 | ) 251 | if "network_id" in geth_kwargs: 252 | raise PyGethValueError( 253 | f"You cannot specify `network_id` for a {type(self).__name__}" 254 | ) 255 | 256 | geth_kwargs["network_id"] = "11155111" 257 | geth_kwargs["data_dir"] = get_sepolia_data_dir() 258 | 259 | super().__init__(geth_kwargs) 260 | 261 | @property 262 | def data_dir(self) -> str: 263 | return get_sepolia_data_dir() 264 | 265 | 266 | class TestnetGethProcess(SepoliaGethProcess): 267 | """ 268 | Alias for whatever the current primary testnet chain is. 269 | """ 270 | 271 | 272 | class DevGethProcess(BaseGethProcess): 273 | """ 274 | Geth developer mode process for testing purposes. 275 | """ 276 | 277 | _data_dir: str 278 | 279 | def __init__( 280 | self, 281 | chain_name: str, 282 | base_dir: str | None = None, 283 | overrides: GethKwargsTypedDict | None = None, 284 | genesis_data: GenesisDataTypedDict | None = None, 285 | ): 286 | if overrides is None: 287 | overrides = {} 288 | 289 | if genesis_data is None: 290 | # deepcopy since we may modify the data on init below 291 | genesis_data = GenesisDataTypedDict(**copy.deepcopy(GENESIS_JSON)) 292 | 293 | validate_genesis_data(genesis_data) 294 | 295 | if "data_dir" in overrides: 296 | raise PyGethValueError("You cannot specify `data_dir` for a DevGethProcess") 297 | 298 | if base_dir is None: 299 | base_dir = get_default_base_dir() 300 | 301 | self._data_dir = get_chain_data_dir(base_dir, chain_name) 302 | overrides["data_dir"] = self.data_dir 303 | geth_kwargs = construct_test_chain_kwargs(**overrides) 304 | validate_geth_kwargs(geth_kwargs) 305 | 306 | # ensure that an account is present 307 | coinbase = ensure_account_exists(**geth_kwargs) 308 | 309 | # ensure that the chain is initialized 310 | genesis_file_path = get_genesis_file_path(self.data_dir) 311 | needs_init = all( 312 | ( 313 | not os.path.exists(genesis_file_path), 314 | not is_live_chain(self.data_dir), 315 | not is_sepolia_chain(self.data_dir), 316 | ) 317 | ) 318 | if needs_init: 319 | genesis_data["coinbase"] = coinbase 320 | genesis_data.setdefault("alloc", {}).setdefault( 321 | coinbase, {"balance": "1000000000000000000000000000000"} 322 | ) 323 | 324 | modify_genesis_based_on_geth_version(genesis_data) 325 | initialize_chain(genesis_data, self.data_dir) 326 | 327 | super().__init__(geth_kwargs) 328 | 329 | @property 330 | def data_dir(self) -> str: 331 | return self._data_dir 332 | 333 | 334 | def modify_genesis_based_on_geth_version(genesis_data: GenesisDataTypedDict) -> None: 335 | geth_version = get_geth_version() 336 | if geth_version <= semantic_version.Version("1.14.0"): 337 | # geth <= v1.14.0 needs negative `terminalTotalDifficulty` to load EVM 338 | # instructions correctly: https://github.com/ethereum/go-ethereum/pull/29579 339 | if "config" not in genesis_data: 340 | genesis_data["config"] = {} 341 | genesis_data["config"]["terminalTotalDifficulty"] = -1 342 | -------------------------------------------------------------------------------- /geth/py.typed: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ethereum/py-geth/d2559a139c66a13f4bad6aba731ea8fd0c823512/geth/py.typed -------------------------------------------------------------------------------- /geth/reset.py: -------------------------------------------------------------------------------- 1 | from __future__ import ( 2 | annotations, 3 | ) 4 | 5 | import os 6 | 7 | from typing_extensions import ( 8 | Unpack, 9 | ) 10 | 11 | from geth.exceptions import ( 12 | PyGethValueError, 13 | ) 14 | from geth.types import ( 15 | GethKwargsTypedDict, 16 | ) 17 | from geth.utils.validation import ( 18 | validate_geth_kwargs, 19 | ) 20 | 21 | from .chains import ( 22 | is_live_chain, 23 | is_testnet_chain, 24 | ) 25 | from .utils.filesystem import ( 26 | remove_dir_if_exists, 27 | remove_file_if_exists, 28 | ) 29 | from .wrapper import ( 30 | spawn_geth, 31 | ) 32 | 33 | 34 | def soft_reset_chain( 35 | allow_live: bool = False, 36 | allow_testnet: bool = False, 37 | **geth_kwargs: Unpack[GethKwargsTypedDict], 38 | ) -> None: 39 | validate_geth_kwargs(geth_kwargs) 40 | data_dir = geth_kwargs.get("data_dir") 41 | 42 | if data_dir is None or (not allow_live and is_live_chain(data_dir)): 43 | raise PyGethValueError( 44 | "To reset the live chain you must call this function with `allow_live=True`" 45 | ) 46 | 47 | if not allow_testnet and is_testnet_chain(data_dir): 48 | raise PyGethValueError( 49 | "To reset the testnet chain you must call this function with `allow_testnet=True`" # noqa: E501 50 | ) 51 | 52 | suffix_args = geth_kwargs.pop("suffix_args") or [] 53 | suffix_args.extend(("removedb",)) 54 | geth_kwargs.update({"suffix_args": suffix_args}) 55 | 56 | _, proc = spawn_geth(geth_kwargs) 57 | 58 | stdoutdata, stderrdata = proc.communicate(b"y") 59 | 60 | if "Removing chaindata" not in stdoutdata.decode(): 61 | raise PyGethValueError( 62 | "An error occurred while removing the chain:\n\nError:\n" 63 | f"{stderrdata.decode()}\n\nOutput:\n{stdoutdata.decode()}" 64 | ) 65 | 66 | 67 | def hard_reset_chain( 68 | data_dir: str, allow_live: bool = False, allow_testnet: bool = False 69 | ) -> None: 70 | if not allow_live and is_live_chain(data_dir): 71 | raise PyGethValueError( 72 | "To reset the live chain you must call this function with `allow_live=True`" 73 | ) 74 | 75 | if not allow_testnet and is_testnet_chain(data_dir): 76 | raise PyGethValueError( 77 | "To reset the testnet chain you must call this function with `allow_testnet=True`" # noqa: E501 78 | ) 79 | 80 | blockchain_dir = os.path.join(data_dir, "chaindata") 81 | remove_dir_if_exists(blockchain_dir) 82 | 83 | dapp_dir = os.path.join(data_dir, "dapp") 84 | remove_dir_if_exists(dapp_dir) 85 | 86 | nodekey_path = os.path.join(data_dir, "nodekey") 87 | remove_file_if_exists(nodekey_path) 88 | 89 | nodes_path = os.path.join(data_dir, "nodes") 90 | remove_dir_if_exists(nodes_path) 91 | 92 | geth_ipc_path = os.path.join(data_dir, "geth.ipc") 93 | remove_file_if_exists(geth_ipc_path) 94 | 95 | history_path = os.path.join(data_dir, "history") 96 | remove_file_if_exists(history_path) 97 | -------------------------------------------------------------------------------- /geth/types.py: -------------------------------------------------------------------------------- 1 | from __future__ import ( 2 | annotations, 3 | ) 4 | 5 | from typing import ( 6 | IO, 7 | Any, 8 | Literal, 9 | TypedDict, 10 | Union, 11 | ) 12 | 13 | IO_Any = Union[IO[Any], int, None] 14 | 15 | 16 | class GethKwargsTypedDict(TypedDict, total=False): 17 | cache: str | None 18 | data_dir: str | None 19 | dev_mode: bool | None 20 | dev_period: str | None 21 | gcmode: Literal["full", "archive"] | None 22 | geth_executable: str | None 23 | ipc_disable: bool | None 24 | ipc_path: str | None 25 | max_peers: str | None 26 | network_id: str | None 27 | nice: bool | None 28 | no_discover: bool | None 29 | password: bytes | str | None 30 | port: str | None 31 | preload: str | None 32 | rpc_addr: str | None 33 | rpc_api: str | None 34 | rpc_cors_domain: str | None 35 | rpc_enabled: bool | None 36 | rpc_port: str | None 37 | stdin: str | None 38 | suffix_args: list[str] | None 39 | suffix_kwargs: dict[str, str] | None 40 | tx_pool_global_slots: str | None 41 | tx_pool_lifetime: str | None 42 | tx_pool_price_limit: str | None 43 | verbosity: str | None 44 | ws_addr: str | None 45 | ws_api: str | None 46 | ws_enabled: bool | None 47 | ws_origins: str | None 48 | ws_port: str | None 49 | 50 | 51 | class GenesisDataTypedDict(TypedDict, total=False): 52 | alloc: dict[str, dict[str, Any]] 53 | baseFeePerGas: str 54 | blobGasUsed: str 55 | coinbase: str 56 | config: dict[str, Any] 57 | difficulty: str 58 | excessBlobGas: str 59 | extraData: str 60 | gasLimit: str 61 | gasUsed: str 62 | mixHash: str 63 | nonce: str 64 | number: str 65 | parentHash: str 66 | timestamp: str 67 | -------------------------------------------------------------------------------- /geth/utils/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ethereum/py-geth/d2559a139c66a13f4bad6aba731ea8fd0c823512/geth/utils/__init__.py -------------------------------------------------------------------------------- /geth/utils/encoding.py: -------------------------------------------------------------------------------- 1 | from __future__ import ( 2 | annotations, 3 | ) 4 | 5 | import codecs 6 | from typing import ( 7 | Any, 8 | ) 9 | 10 | from geth.exceptions import ( 11 | PyGethTypeError, 12 | ) 13 | 14 | 15 | def is_string(value: Any) -> bool: 16 | return isinstance(value, (bytes, bytearray, str)) 17 | 18 | 19 | def force_bytes(value: bytes | bytearray | str, encoding: str = "iso-8859-1") -> bytes: 20 | if isinstance(value, bytes): 21 | return value 22 | elif isinstance(value, bytearray): 23 | return bytes(value) 24 | elif isinstance(value, str): 25 | encoded = codecs.encode(value, encoding) 26 | if isinstance(encoded, (bytes, bytearray)): 27 | return encoded 28 | else: 29 | raise PyGethTypeError( 30 | f"Encoding {encoding!r} produced non-binary result: {encoded!r}" 31 | ) 32 | else: 33 | raise PyGethTypeError( 34 | f"Unsupported type: {type(value)}, expected bytes, bytearray or str" 35 | ) 36 | 37 | 38 | def force_text(value: bytes | bytearray | str, encoding: str = "iso-8859-1") -> str: 39 | if isinstance(value, (bytes, bytearray)): 40 | return codecs.decode(value, encoding) 41 | elif isinstance(value, str): 42 | return value 43 | else: 44 | raise PyGethTypeError( 45 | f"Unsupported type: {type(value)}, " 46 | "expected value to be bytes, bytearray or str" 47 | ) 48 | 49 | 50 | def force_obj_to_text(obj: Any) -> Any: 51 | if is_string(obj): 52 | return force_text(obj) 53 | elif isinstance(obj, dict): 54 | return {force_obj_to_text(k): force_obj_to_text(v) for k, v in obj.items()} 55 | elif isinstance(obj, (list, tuple)): 56 | return type(obj)(force_obj_to_text(v) for v in obj) 57 | else: 58 | return obj 59 | -------------------------------------------------------------------------------- /geth/utils/filesystem.py: -------------------------------------------------------------------------------- 1 | import os 2 | import shutil 3 | 4 | 5 | def mkdir(path: str) -> None: 6 | os.makedirs(path, exist_ok=True) 7 | 8 | 9 | def ensure_path_exists(dir_path: str) -> bool: 10 | """ 11 | Make sure that a path exists 12 | """ 13 | if not os.path.exists(dir_path): 14 | mkdir(dir_path) 15 | return True 16 | return False 17 | 18 | 19 | def remove_file_if_exists(path: str) -> bool: 20 | if os.path.isfile(path): 21 | os.remove(path) 22 | return True 23 | return False 24 | 25 | 26 | def remove_dir_if_exists(path: str) -> bool: 27 | if os.path.isdir(path): 28 | shutil.rmtree(path) 29 | return True 30 | return False 31 | 32 | 33 | def is_executable_available(program: str) -> bool: 34 | def is_exe(fpath: str) -> bool: 35 | return os.path.isfile(fpath) and os.access(fpath, os.X_OK) 36 | 37 | fpath = os.path.dirname(program) 38 | if fpath: 39 | if is_exe(program): 40 | return True 41 | else: 42 | for path in os.environ["PATH"].split(os.pathsep): 43 | path = path.strip('"') 44 | exe_file = os.path.join(path, program) 45 | if is_exe(exe_file): 46 | return True 47 | 48 | return False 49 | 50 | 51 | def is_same_path(p1: str, p2: str) -> bool: 52 | n_p1 = os.path.abspath(os.path.expanduser(p1)) 53 | n_p2 = os.path.abspath(os.path.expanduser(p2)) 54 | 55 | try: 56 | return os.path.samefile(n_p1, n_p2) 57 | except FileNotFoundError: 58 | return n_p1 == n_p2 59 | -------------------------------------------------------------------------------- /geth/utils/networking.py: -------------------------------------------------------------------------------- 1 | import contextlib 2 | import socket 3 | import time 4 | from typing import ( 5 | Generator, 6 | ) 7 | 8 | from geth.exceptions import ( 9 | PyGethValueError, 10 | ) 11 | 12 | from .timeout import ( 13 | Timeout, 14 | ) 15 | 16 | 17 | def is_port_open(port: int) -> bool: 18 | sock = socket.socket() 19 | try: 20 | sock.bind(("127.0.0.1", port)) 21 | except OSError: 22 | return False 23 | else: 24 | return True 25 | finally: 26 | sock.close() 27 | 28 | 29 | def get_open_port() -> str: 30 | sock = socket.socket() 31 | sock.bind(("127.0.0.1", 0)) 32 | port = sock.getsockname()[1] 33 | sock.close() 34 | return str(port) 35 | 36 | 37 | @contextlib.contextmanager 38 | def get_ipc_socket( 39 | ipc_path: str, timeout: float = 0.1 40 | ) -> Generator[socket.socket, None, None]: 41 | sock = socket.socket(socket.AF_UNIX, socket.SOCK_STREAM) 42 | sock.connect(ipc_path) 43 | sock.settimeout(timeout) 44 | 45 | yield sock 46 | 47 | sock.close() 48 | 49 | 50 | def wait_for_http_connection(port: int, timeout: int = 5) -> None: 51 | with Timeout(timeout) as _timeout: 52 | while True: 53 | s = socket.socket(socket.AF_INET, socket.SOCK_STREAM) 54 | s.settimeout(1) 55 | try: 56 | s.connect(("127.0.0.1", port)) 57 | except (socket.timeout, ConnectionRefusedError): 58 | time.sleep(0.1) 59 | _timeout.check() 60 | continue 61 | else: 62 | break 63 | else: 64 | raise PyGethValueError( 65 | "Unable to establish HTTP connection, " 66 | f"timed out after {timeout} seconds" 67 | ) 68 | -------------------------------------------------------------------------------- /geth/utils/proc.py: -------------------------------------------------------------------------------- 1 | from __future__ import ( 2 | annotations, 3 | ) 4 | 5 | import signal 6 | import subprocess 7 | import time 8 | from typing import ( 9 | AnyStr, 10 | ) 11 | 12 | from .timeout import ( 13 | Timeout, 14 | ) 15 | 16 | 17 | def wait_for_popen(proc: subprocess.Popen[AnyStr], timeout: int = 30) -> None: 18 | try: 19 | with Timeout(timeout) as _timeout: 20 | while proc.poll() is None: 21 | time.sleep(0.1) 22 | _timeout.check() 23 | except Timeout: 24 | pass 25 | 26 | 27 | def kill_proc(proc: subprocess.Popen[AnyStr]) -> None: 28 | try: 29 | if proc.poll() is None: 30 | try: 31 | proc.send_signal(signal.SIGINT) 32 | wait_for_popen(proc, 30) 33 | except KeyboardInterrupt: 34 | print( 35 | "Trying to close geth process. Press Ctrl+C 2 more times " 36 | "to force quit" 37 | ) 38 | if proc.poll() is None: 39 | try: 40 | proc.terminate() 41 | wait_for_popen(proc, 10) 42 | except KeyboardInterrupt: 43 | print( 44 | "Trying to close geth process. Press Ctrl+C 1 more times " 45 | "to force quit" 46 | ) 47 | if proc.poll() is None: 48 | proc.kill() 49 | wait_for_popen(proc, 2) 50 | except KeyboardInterrupt: 51 | proc.kill() 52 | 53 | 54 | def format_error_message( 55 | prefix: str, command: list[str], return_code: int, stdoutdata: str, stderrdata: str 56 | ) -> str: 57 | lines = [prefix] 58 | 59 | lines.append(f"Command : {' '.join(command)}") 60 | lines.append(f"Return Code: {return_code}") 61 | 62 | if stdoutdata: 63 | lines.append(f"stdout:\n`{stdoutdata}`") 64 | else: 65 | lines.append("stdout: N/A") 66 | 67 | if stderrdata: 68 | lines.append(f"stderr:\n`{stderrdata}`") 69 | else: 70 | lines.append("stderr: N/A") 71 | 72 | return "\n".join(lines) 73 | -------------------------------------------------------------------------------- /geth/utils/thread.py: -------------------------------------------------------------------------------- 1 | import threading 2 | from typing import ( 3 | Any, 4 | Callable, 5 | ) 6 | 7 | 8 | def spawn(target: Callable[..., Any], *args: Any, **kwargs: Any) -> threading.Thread: 9 | thread = threading.Thread( 10 | target=target, 11 | args=args, 12 | kwargs=kwargs, 13 | ) 14 | thread.daemon = True 15 | thread.start() 16 | return thread 17 | -------------------------------------------------------------------------------- /geth/utils/timeout.py: -------------------------------------------------------------------------------- 1 | from __future__ import ( 2 | annotations, 3 | ) 4 | 5 | import time 6 | from types import ( 7 | TracebackType, 8 | ) 9 | from typing import ( 10 | Any, 11 | Literal, 12 | ) 13 | 14 | from geth.exceptions import ( 15 | PyGethValueError, 16 | ) 17 | 18 | 19 | class Timeout(Exception): 20 | """ 21 | A limited subset of the `gevent.Timeout` context manager. 22 | """ 23 | 24 | seconds = None 25 | exception = None 26 | begun_at = None 27 | is_running = None 28 | 29 | def __init__( 30 | self, 31 | seconds: int | None = None, 32 | exception: Any | None = None, 33 | *args: Any, 34 | **kwargs: Any, 35 | ): 36 | self.seconds = seconds 37 | self.exception = exception 38 | 39 | def __enter__(self) -> Timeout: 40 | self.start() 41 | return self 42 | 43 | def __exit__( 44 | self, 45 | exc_type: type[BaseException] | None, 46 | exc_value: BaseException | None, 47 | tb: TracebackType | None, 48 | ) -> Literal[False]: 49 | return False 50 | 51 | def __str__(self) -> str: 52 | if self.seconds is None: 53 | return "" 54 | return f"{self.seconds} seconds" 55 | 56 | @property 57 | def expire_at(self) -> float: 58 | if self.seconds is None: 59 | raise PyGethValueError( 60 | "Timeouts with `seconds == None` do not have an expiration time" 61 | ) 62 | elif self.begun_at is None: 63 | raise PyGethValueError("Timeout has not been started") 64 | return self.begun_at + self.seconds 65 | 66 | def start(self) -> None: 67 | if self.is_running is not None: 68 | raise PyGethValueError("Timeout has already been started") 69 | self.begun_at = time.time() 70 | self.is_running = True 71 | 72 | def check(self) -> None: 73 | if self.is_running is None: 74 | raise PyGethValueError("Timeout has not been started") 75 | elif self.is_running is False: 76 | raise PyGethValueError("Timeout has already been cancelled") 77 | elif self.seconds is None: 78 | return 79 | elif time.time() > self.expire_at: 80 | self.is_running = False 81 | if isinstance(self.exception, type): 82 | raise self.exception(str(self)) 83 | elif isinstance(self.exception, Exception): 84 | raise self.exception 85 | else: 86 | raise self 87 | 88 | def cancel(self) -> None: 89 | self.is_running = False 90 | -------------------------------------------------------------------------------- /geth/utils/validation.py: -------------------------------------------------------------------------------- 1 | from __future__ import ( 2 | annotations, 3 | ) 4 | 5 | from typing import ( 6 | Any, 7 | Literal, 8 | ) 9 | 10 | from pydantic import ( 11 | BaseModel, 12 | ConfigDict, 13 | ValidationError, 14 | model_validator, 15 | ) 16 | 17 | from geth.exceptions import ( 18 | PyGethValueError, 19 | ) 20 | from geth.types import ( 21 | GenesisDataTypedDict, 22 | GethKwargsTypedDict, 23 | ) 24 | 25 | 26 | class GethKwargs(BaseModel): 27 | cache: str | None = None 28 | data_dir: str | None = None 29 | dev_mode: bool | None = False 30 | dev_period: str | None = None 31 | gcmode: Literal["full", "archive"] | None = None 32 | geth_executable: str | None = None 33 | ipc_disable: bool | None = None 34 | ipc_path: str | None = None 35 | max_peers: str | None = None 36 | network_id: str | None = None 37 | nice: bool | None = True 38 | no_discover: bool | None = None 39 | password: bytes | str | None = None 40 | port: str | None = None 41 | preload: str | None = None 42 | rpc_addr: str | None = None 43 | rpc_api: str | None = None 44 | rpc_cors_domain: str | None = None 45 | rpc_enabled: bool | None = None 46 | rpc_port: str | None = None 47 | stdin: str | None = None 48 | suffix_args: list[str] | None = None 49 | suffix_kwargs: dict[str, str] | None = None 50 | tx_pool_global_slots: str | None = None 51 | tx_pool_lifetime: str | None = None 52 | tx_pool_price_limit: str | None = None 53 | verbosity: str | None = None 54 | ws_addr: str | None = None 55 | ws_api: str | None = None 56 | ws_enabled: bool | None = None 57 | ws_origins: str | None = None 58 | ws_port: str | None = None 59 | 60 | model_config = ConfigDict(extra="forbid") 61 | 62 | 63 | def validate_geth_kwargs(geth_kwargs: GethKwargsTypedDict) -> None: 64 | """ 65 | Converts geth_kwargs to GethKwargs and raises a ValueError if the conversion fails. 66 | """ 67 | try: 68 | GethKwargs(**geth_kwargs) 69 | except ValidationError as e: 70 | raise PyGethValueError(f"geth_kwargs validation failed: {e}") 71 | except TypeError as e: 72 | raise PyGethValueError(f"error while validating geth_kwargs: {e}") 73 | 74 | 75 | class GenesisDataConfig(BaseModel): 76 | """ 77 | Default values are pulled from the ``genesis.json`` file internal to the repository. 78 | """ 79 | 80 | chainId: int | None = None 81 | ethash: dict[str, Any] | None = None 82 | homesteadBlock: int | None = None 83 | daoForkBlock: int | None = None 84 | daoForkSupport: bool | None = None 85 | eip150Block: int | None = None 86 | eip155Block: int | None = None 87 | eip158Block: int | None = None 88 | byzantiumBlock: int | None = None 89 | constantinopleBlock: int | None = None 90 | petersburgBlock: int | None = None 91 | istanbulBlock: int | None = None 92 | berlinBlock: int | None = None 93 | londonBlock: int | None = None 94 | arrowGlacierBlock: int | None = None 95 | grayGlacierBlock: int | None = None 96 | # merge 97 | terminalTotalDifficulty: int | None = None 98 | terminalTotalDifficultyPassed: bool | None = None 99 | # post-merge, timestamp is used for network transitions 100 | shanghaiTime: int | None = None 101 | cancunTime: int | None = None 102 | pragueTime: int | None = None 103 | # blobs 104 | blobSchedule: dict[str, Any] = {} 105 | 106 | @model_validator(mode="after") 107 | def check_blob_schedule_required( 108 | self, 109 | ) -> GenesisDataConfig: 110 | if self.cancunTime and not self.blobSchedule.get("cancun"): 111 | raise PyGethValueError( 112 | "blobSchedule 'cancun' value is required when cancunTime is set" 113 | ) 114 | if self.pragueTime and not self.blobSchedule.get("prague"): 115 | raise PyGethValueError( 116 | "blobSchedule 'prague' value is required when pragueTime is set" 117 | ) 118 | return self 119 | 120 | 121 | class GenesisData(BaseModel): 122 | alloc: dict[str, dict[str, Any]] = {} 123 | baseFeePerGas: str | None = None 124 | blobGasUsed: str | None = None 125 | coinbase: str | None = None 126 | config: dict[str, Any] = GenesisDataConfig().model_dump() 127 | difficulty: str | None = None 128 | excessBlobGas: str | None = None 129 | extraData: str | None = None 130 | gasLimit: str | None = None 131 | gasUsed: str | None = None 132 | mixHash: str | None = None 133 | nonce: str | None = None 134 | number: str | None = None 135 | parentHash: str | None = None 136 | timestamp: str | None = None 137 | 138 | model_config = ConfigDict(extra="forbid") 139 | 140 | 141 | def validate_genesis_data(genesis_data: GenesisDataTypedDict) -> None: 142 | """ 143 | Validates the genesis data 144 | """ 145 | try: 146 | GenesisData(**genesis_data) 147 | except ValidationError as e: 148 | raise PyGethValueError(f"genesis_data validation failed: {e}") 149 | except TypeError as e: 150 | raise PyGethValueError(f"error while validating genesis_data: {e}") 151 | 152 | """ 153 | Validates the genesis data config field 154 | """ 155 | genesis_data_config = genesis_data.get("config", None) 156 | if genesis_data_config: 157 | try: 158 | GenesisDataConfig(**genesis_data_config) 159 | except ValidationError as e: 160 | raise PyGethValueError(f"genesis_data config field validation failed: {e}") 161 | except TypeError as e: 162 | raise PyGethValueError( 163 | f"error while validating genesis_data config field: {e}" 164 | ) 165 | -------------------------------------------------------------------------------- /geth/wrapper.py: -------------------------------------------------------------------------------- 1 | from __future__ import ( 2 | annotations, 3 | ) 4 | 5 | import functools 6 | import os 7 | import subprocess 8 | import sys 9 | import tempfile 10 | from typing import ( 11 | Any, 12 | Iterable, 13 | cast, 14 | ) 15 | 16 | from typing_extensions import ( 17 | Unpack, 18 | ) 19 | 20 | from geth.exceptions import ( 21 | PyGethGethError, 22 | PyGethValueError, 23 | ) 24 | from geth.types import ( 25 | GethKwargsTypedDict, 26 | IO_Any, 27 | ) 28 | from geth.utils.encoding import ( 29 | force_bytes, 30 | ) 31 | from geth.utils.filesystem import ( 32 | is_executable_available, 33 | ) 34 | from geth.utils.networking import ( 35 | get_open_port, 36 | is_port_open, 37 | ) 38 | from geth.utils.validation import ( 39 | GethKwargs, 40 | validate_geth_kwargs, 41 | ) 42 | 43 | is_nice_available = functools.partial(is_executable_available, "nice") 44 | 45 | 46 | PYGETH_DIR = os.path.abspath(os.path.dirname(__file__)) 47 | 48 | 49 | DEFAULT_PASSWORD_PATH = os.path.join(PYGETH_DIR, "default_blockchain_password") 50 | 51 | 52 | ALL_APIS = "admin,debug,eth,net,txpool,web3" 53 | 54 | 55 | def get_max_socket_path_length() -> int: 56 | if "UNIX_PATH_MAX" in os.environ: 57 | return int(os.environ["UNIX_PATH_MAX"]) 58 | if sys.platform.startswith("darwin"): 59 | return 104 60 | elif sys.platform.startswith("linux"): 61 | return 108 62 | elif sys.platform.startswith("win"): 63 | return 260 64 | 65 | 66 | def construct_test_chain_kwargs( 67 | **overrides: Unpack[GethKwargsTypedDict], 68 | ) -> GethKwargsTypedDict: 69 | validate_geth_kwargs(overrides) 70 | overrides.setdefault("dev_mode", True) 71 | overrides.setdefault("password", DEFAULT_PASSWORD_PATH) 72 | overrides.setdefault("no_discover", True) 73 | overrides.setdefault("max_peers", "0") 74 | overrides.setdefault("network_id", "1234") 75 | 76 | if is_port_open(30303): 77 | overrides.setdefault("port", "30303") 78 | else: 79 | overrides.setdefault("port", get_open_port()) 80 | 81 | overrides.setdefault("ws_enabled", True) 82 | overrides.setdefault("ws_api", ALL_APIS) 83 | 84 | if is_port_open(8546): 85 | overrides.setdefault("ws_port", "8546") 86 | else: 87 | overrides.setdefault("ws_port", get_open_port()) 88 | 89 | overrides.setdefault("rpc_enabled", True) 90 | overrides.setdefault("rpc_api", ALL_APIS) 91 | if is_port_open(8545): 92 | overrides.setdefault("rpc_port", "8545") 93 | else: 94 | overrides.setdefault("rpc_port", get_open_port()) 95 | 96 | if "ipc_path" not in overrides: 97 | # try to use a `geth.ipc` within the provided data_dir if the path is 98 | # short enough. 99 | if overrides.get("data_dir") is not None: 100 | data_dir = cast(str, overrides["data_dir"]) 101 | max_path_length = get_max_socket_path_length() 102 | geth_ipc_path = os.path.abspath(os.path.join(data_dir, "geth.ipc")) 103 | if len(geth_ipc_path) <= max_path_length: 104 | overrides.setdefault("ipc_path", geth_ipc_path) 105 | 106 | # Otherwise default to a tempfile based ipc path. 107 | overrides.setdefault( 108 | "ipc_path", 109 | os.path.join(tempfile.mkdtemp(), "geth.ipc"), 110 | ) 111 | 112 | overrides.setdefault("verbosity", "5") 113 | 114 | return overrides 115 | 116 | 117 | def get_geth_binary_path() -> str: 118 | return os.environ.get("GETH_BINARY", "geth") 119 | 120 | 121 | class CommandBuilder: 122 | def __init__(self) -> None: 123 | self.command: list[str] = [] 124 | 125 | def append(self, value: Any) -> None: 126 | self.command.append(str(value)) 127 | 128 | def extend(self, value_list: Iterable[Any]) -> None: 129 | self.command.extend([str(v) for v in value_list]) 130 | 131 | 132 | def construct_popen_command(**geth_kwargs: Unpack[GethKwargsTypedDict]) -> list[str]: 133 | # validate geth_kwargs and fill defaults that may not have been provided 134 | validate_geth_kwargs(geth_kwargs) 135 | gk = GethKwargs(**geth_kwargs) 136 | 137 | if gk.geth_executable is None: 138 | gk.geth_executable = get_geth_binary_path() 139 | 140 | if not is_executable_available(gk.geth_executable): 141 | raise PyGethValueError( 142 | "No geth executable found. Please ensure geth is installed and " 143 | "available on your PATH or use the GETH_BINARY environment variable" 144 | ) 145 | 146 | builder = CommandBuilder() 147 | 148 | if gk.nice and is_nice_available(): 149 | builder.extend(("nice", "-n", "20")) 150 | 151 | builder.append(gk.geth_executable) 152 | 153 | if gk.dev_mode: 154 | builder.append("--dev") 155 | 156 | if gk.dev_period is not None: 157 | builder.extend(("--dev.period", gk.dev_period)) 158 | 159 | if gk.rpc_enabled: 160 | builder.append("--http") 161 | 162 | if gk.rpc_addr is not None: 163 | builder.extend(("--http.addr", gk.rpc_addr)) 164 | 165 | if gk.rpc_port is not None: 166 | builder.extend(("--http.port", gk.rpc_port)) 167 | 168 | if gk.rpc_api is not None: 169 | builder.extend(("--http.api", gk.rpc_api)) 170 | 171 | if gk.rpc_cors_domain is not None: 172 | builder.extend(("--http.corsdomain", gk.rpc_cors_domain)) 173 | 174 | if gk.ws_enabled: 175 | builder.append("--ws") 176 | 177 | if gk.ws_addr is not None: 178 | builder.extend(("--ws.addr", gk.ws_addr)) 179 | 180 | if gk.ws_origins is not None: 181 | builder.extend(("--ws.origins", gk.ws_port)) 182 | 183 | if gk.ws_port is not None: 184 | builder.extend(("--ws.port", gk.ws_port)) 185 | 186 | if gk.ws_api is not None: 187 | builder.extend(("--ws.api", gk.ws_api)) 188 | 189 | if gk.data_dir is not None: 190 | builder.extend(("--datadir", gk.data_dir)) 191 | 192 | if gk.max_peers is not None: 193 | builder.extend(("--maxpeers", gk.max_peers)) 194 | 195 | if gk.network_id is not None: 196 | builder.extend(("--networkid", gk.network_id)) 197 | 198 | if gk.port is not None: 199 | builder.extend(("--port", gk.port)) 200 | 201 | if gk.ipc_disable: 202 | builder.append("--ipcdisable") 203 | 204 | if gk.ipc_path is not None: 205 | builder.extend(("--ipcpath", gk.ipc_path)) 206 | 207 | if gk.verbosity is not None: 208 | builder.extend(("--verbosity", gk.verbosity)) 209 | 210 | if isinstance(gk.password, str) and gk.password is not None: 211 | # If password is a string, it's a file path 212 | # If password is bytes, it's the password itself and is passed directly to 213 | # the geth process elsewhere 214 | builder.extend(("--password", gk.password)) 215 | 216 | if gk.preload is not None: 217 | builder.extend(("--preload", gk.preload)) 218 | 219 | if gk.no_discover: 220 | builder.append("--nodiscover") 221 | 222 | if gk.tx_pool_global_slots is not None: 223 | builder.extend(("--txpool.globalslots", gk.tx_pool_global_slots)) 224 | 225 | if gk.tx_pool_lifetime is not None: 226 | builder.extend(("--txpool.lifetime", gk.tx_pool_lifetime)) 227 | 228 | if gk.tx_pool_price_limit is not None: 229 | builder.extend(("--txpool.pricelimit", gk.tx_pool_price_limit)) 230 | 231 | if gk.cache: 232 | builder.extend(("--cache", gk.cache)) 233 | 234 | if gk.gcmode: 235 | builder.extend(("--gcmode", gk.gcmode)) 236 | 237 | if gk.suffix_kwargs: 238 | builder.extend(gk.suffix_kwargs) 239 | 240 | if gk.suffix_args: 241 | builder.extend(gk.suffix_args) 242 | 243 | return builder.command 244 | 245 | 246 | def geth_wrapper( 247 | **geth_kwargs: Unpack[GethKwargsTypedDict], 248 | ) -> tuple[bytes, bytes, list[str], subprocess.Popen[bytes]]: 249 | validate_geth_kwargs(geth_kwargs) 250 | stdin = geth_kwargs.pop("stdin", None) 251 | command = construct_popen_command(**geth_kwargs) 252 | 253 | proc = subprocess.Popen( 254 | command, 255 | stdin=subprocess.PIPE, 256 | stdout=subprocess.PIPE, 257 | stderr=subprocess.PIPE, 258 | ) 259 | stdin_bytes: bytes | None = None 260 | if stdin is not None: 261 | stdin_bytes = force_bytes(stdin) 262 | 263 | stdoutdata, stderrdata = proc.communicate(stdin_bytes) 264 | 265 | if proc.returncode != 0: 266 | raise PyGethGethError( 267 | command=command, 268 | return_code=proc.returncode, 269 | stdin_data=stdin, 270 | stdout_data=stdoutdata, 271 | stderr_data=stderrdata, 272 | ) 273 | 274 | return stdoutdata, stderrdata, command, proc 275 | 276 | 277 | def spawn_geth( 278 | geth_kwargs: GethKwargsTypedDict, 279 | stdin: IO_Any = subprocess.PIPE, 280 | stdout: IO_Any = subprocess.PIPE, 281 | stderr: IO_Any = subprocess.PIPE, 282 | ) -> tuple[list[str], subprocess.Popen[bytes]]: 283 | validate_geth_kwargs(geth_kwargs) 284 | command = construct_popen_command(**geth_kwargs) 285 | 286 | proc = subprocess.Popen( 287 | command, 288 | stdin=stdin, 289 | stdout=stdout, 290 | stderr=stderr, 291 | ) 292 | 293 | return command, proc 294 | -------------------------------------------------------------------------------- /newsfragments/README.md: -------------------------------------------------------------------------------- 1 | This directory collects "newsfragments": short files that each contain 2 | a snippet of ReST-formatted text that will be added to the next 3 | release notes. This should be a description of aspects of the change 4 | (if any) that are relevant to users. (This contrasts with the 5 | commit message and PR description, which are a description of the change as 6 | relevant to people working on the code itself.) 7 | 8 | Each file should be named like `..rst`, where 9 | `` is an issue number, and `` is one of: 10 | 11 | - `breaking` 12 | - `bugfix` 13 | - `deprecation` 14 | - `docs` 15 | - `feature` 16 | - `internal` 17 | - `misc` 18 | - `performance` 19 | - `removal` 20 | 21 | So for example: `123.feature.rst`, `456.bugfix.rst` 22 | 23 | If the PR fixes an issue, use that number here. If there is no issue, 24 | then open up the PR first and use the PR number for the newsfragment. 25 | 26 | Note that the `towncrier` tool will automatically 27 | reflow your text, so don't try to do any fancy formatting. Run 28 | `towncrier build --draft` to get a preview of what the release notes entry 29 | will look like in the final release notes. 30 | -------------------------------------------------------------------------------- /newsfragments/validate_files.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | 3 | # Towncrier silently ignores files that do not match the expected ending. 4 | # We use this script to ensure we catch these as errors in CI. 5 | 6 | import pathlib 7 | import sys 8 | 9 | ALLOWED_EXTENSIONS = { 10 | ".breaking.rst", 11 | ".bugfix.rst", 12 | ".deprecation.rst", 13 | ".docs.rst", 14 | ".feature.rst", 15 | ".internal.rst", 16 | ".misc.rst", 17 | ".performance.rst", 18 | ".removal.rst", 19 | } 20 | 21 | ALLOWED_FILES = { 22 | "validate_files.py", 23 | "README.md", 24 | } 25 | 26 | THIS_DIR = pathlib.Path(__file__).parent 27 | 28 | num_args = len(sys.argv) - 1 29 | assert num_args in {0, 1} 30 | if num_args == 1: 31 | assert sys.argv[1] in ("is-empty",) 32 | 33 | for fragment_file in THIS_DIR.iterdir(): 34 | if fragment_file.name in ALLOWED_FILES: 35 | continue 36 | elif num_args == 0: 37 | full_extension = "".join(fragment_file.suffixes) 38 | if full_extension not in ALLOWED_EXTENSIONS: 39 | raise Exception(f"Unexpected file: {fragment_file}") 40 | elif sys.argv[1] == "is-empty": 41 | raise Exception(f"Unexpected file: {fragment_file}") 42 | else: 43 | raise RuntimeError( 44 | f"Strange: arguments {sys.argv} were validated, but not found" 45 | ) 46 | -------------------------------------------------------------------------------- /pyproject.toml: -------------------------------------------------------------------------------- 1 | [tool.autoflake] 2 | exclude = "__init__.py" 3 | remove_all_unused_imports = true 4 | 5 | [tool.isort] 6 | combine_as_imports = true 7 | extra_standard_library = "pytest" 8 | force_grid_wrap = 1 9 | force_sort_within_sections = true 10 | force_to_top = "pytest" 11 | honor_noqa = true 12 | known_first_party = "geth" 13 | known_third_party = "hypothesis" 14 | multi_line_output = 3 15 | profile = "black" 16 | use_parentheses = true 17 | 18 | [tool.mypy] 19 | check_untyped_defs = true 20 | disallow_any_generics = true 21 | disallow_incomplete_defs = true 22 | disallow_subclassing_any = true 23 | disallow_untyped_calls = true 24 | disallow_untyped_decorators = true 25 | disallow_untyped_defs = true 26 | ignore_missing_imports = true 27 | strict_equality = true 28 | strict_optional = true 29 | warn_redundant_casts = true 30 | warn_return_any = true 31 | warn_unused_configs = true 32 | warn_unused_ignores = true 33 | 34 | 35 | [tool.pydocstyle] 36 | # All error codes found here: 37 | # http://www.pydocstyle.org/en/3.0.0/error_codes.html 38 | # 39 | # Ignored: 40 | # D1 - Missing docstring error codes 41 | # 42 | # Selected: 43 | # D2 - Whitespace error codes 44 | # D3 - Quote error codes 45 | # D4 - Content related error codes 46 | select = "D2,D3,D4" 47 | 48 | # Extra ignores: 49 | # D200 - One-line docstring should fit on one line with quotes 50 | # D203 - 1 blank line required before class docstring 51 | # D204 - 1 blank line required after class docstring 52 | # D205 - 1 blank line required between summary line and description 53 | # D212 - Multi-line docstring summary should start at the first line 54 | # D302 - Use u""" for Unicode docstrings 55 | # D400 - First line should end with a period 56 | # D401 - First line should be in imperative mood 57 | # D412 - No blank lines allowed between a section header and its content 58 | # D415 - First line should end with a period, question mark, or exclamation point 59 | add-ignore = "D200,D203,D204,D205,D212,D302,D400,D401,D412,D415" 60 | 61 | # Explanation: 62 | # D400 - Enabling this error code seems to make it a requirement that the first 63 | # sentence in a docstring is not split across two lines. It also makes it a 64 | # requirement that no docstring can have a multi-sentence description without a 65 | # summary line. Neither one of those requirements seem appropriate. 66 | 67 | [tool.pytest.ini_options] 68 | addopts = "-v --showlocals --durations 10" 69 | log_date_format = "%m-%d %H:%M:%S" 70 | log_format = "%(levelname)8s %(asctime)s %(filename)20s %(message)s" 71 | xfail_strict = true 72 | 73 | [tool.towncrier] 74 | # Read https://github.com/ethereum/py-geth/blob/main/newsfragments/README.md for instructions 75 | directory = "newsfragments" 76 | filename = "CHANGELOG.rst" 77 | issue_format = "`#{issue} `__" 78 | package = "geth" 79 | title_format = "py-geth v{version} ({project_date})" 80 | underlines = ["-", "~", "^"] 81 | 82 | [[tool.towncrier.type]] 83 | directory = "breaking" 84 | name = "Breaking Changes" 85 | showcontent = true 86 | 87 | [[tool.towncrier.type]] 88 | directory = "bugfix" 89 | name = "Bugfixes" 90 | showcontent = true 91 | 92 | [[tool.towncrier.type]] 93 | directory = "deprecation" 94 | name = "Deprecations" 95 | showcontent = true 96 | 97 | [[tool.towncrier.type]] 98 | directory = "docs" 99 | name = "Improved Documentation" 100 | showcontent = true 101 | 102 | [[tool.towncrier.type]] 103 | directory = "feature" 104 | name = "Features" 105 | showcontent = true 106 | 107 | [[tool.towncrier.type]] 108 | directory = "internal" 109 | name = "Internal Changes - for py-geth Contributors" 110 | showcontent = true 111 | 112 | [[tool.towncrier.type]] 113 | directory = "misc" 114 | name = "Miscellaneous Changes" 115 | showcontent = false 116 | 117 | [[tool.towncrier.type]] 118 | directory = "performance" 119 | name = "Performance Improvements" 120 | showcontent = true 121 | 122 | [[tool.towncrier.type]] 123 | directory = "removal" 124 | name = "Removals" 125 | showcontent = true 126 | 127 | [tool.bumpversion] 128 | current_version = "5.6.0" 129 | parse = """ 130 | (?P\\d+) 131 | \\.(?P\\d+) 132 | \\.(?P\\d+) 133 | (- 134 | (?P[^.]*) 135 | \\.(?P\\d+) 136 | )? 137 | """ 138 | serialize = [ 139 | "{major}.{minor}.{patch}-{stage}.{devnum}", 140 | "{major}.{minor}.{patch}", 141 | ] 142 | search = "{current_version}" 143 | replace = "{new_version}" 144 | regex = false 145 | ignore_missing_version = false 146 | tag = true 147 | sign_tags = true 148 | tag_name = "v{new_version}" 149 | tag_message = "Bump version: {current_version} → {new_version}" 150 | allow_dirty = false 151 | commit = true 152 | message = "Bump version: {current_version} → {new_version}" 153 | 154 | [tool.bumpversion.parts.stage] 155 | optional_value = "stable" 156 | first_value = "stable" 157 | values = [ 158 | "alpha", 159 | "beta", 160 | "stable", 161 | ] 162 | 163 | [tool.bumpversion.part.devnum] 164 | 165 | [[tool.bumpversion.files]] 166 | filename = "setup.py" 167 | search = "version=\"{current_version}\"" 168 | replace = "version=\"{new_version}\"" 169 | -------------------------------------------------------------------------------- /scripts/release/test_package.py: -------------------------------------------------------------------------------- 1 | from pathlib import ( 2 | Path, 3 | ) 4 | import subprocess 5 | from tempfile import ( 6 | TemporaryDirectory, 7 | ) 8 | import venv 9 | 10 | 11 | def create_venv(parent_path: Path) -> Path: 12 | venv_path = parent_path / "package-smoke-test" 13 | venv.create(venv_path, with_pip=True) 14 | subprocess.run( 15 | [venv_path / "bin" / "pip", "install", "-U", "pip", "setuptools"], check=True 16 | ) 17 | return venv_path 18 | 19 | 20 | def find_wheel(project_path: Path) -> Path: 21 | wheels = list(project_path.glob("dist/*.whl")) 22 | 23 | if len(wheels) != 1: 24 | raise Exception( 25 | f"Expected one wheel. Instead found: {wheels} " 26 | f"in project {project_path.absolute()}" 27 | ) 28 | 29 | return wheels[0] 30 | 31 | 32 | def install_wheel(venv_path: Path, wheel_path: Path) -> None: 33 | subprocess.run( 34 | [venv_path / "bin" / "pip", "install", f"{wheel_path}"], 35 | check=True, 36 | ) 37 | 38 | 39 | def test_install_local_wheel() -> None: 40 | with TemporaryDirectory() as tmpdir: 41 | venv_path = create_venv(Path(tmpdir)) 42 | wheel_path = find_wheel(Path(".")) 43 | install_wheel(venv_path, wheel_path) 44 | print("Installed", wheel_path.absolute(), "to", venv_path) 45 | print(f"Activate with `source {venv_path}/bin/activate`") 46 | input("Press enter when the test has completed. The directory will be deleted.") 47 | 48 | 49 | if __name__ == "__main__": 50 | test_install_local_wheel() 51 | -------------------------------------------------------------------------------- /setup.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | from setuptools import ( 3 | find_packages, 4 | setup, 5 | ) 6 | 7 | extras_require = { 8 | "dev": [ 9 | "build>=0.9.0", 10 | "bump_my_version>=0.19.0", 11 | "ipython", 12 | "mypy==1.10.0", 13 | "pre-commit>=3.4.0", 14 | "tox>=4.0.0", 15 | "twine", 16 | "wheel", 17 | ], 18 | "docs": [ 19 | "towncrier>=24,<25", 20 | ], 21 | "test": [ 22 | "flaky>=3.2.0", 23 | "pytest>=7.0.0", 24 | "pytest-xdist>=2.4.0", 25 | ], 26 | } 27 | 28 | extras_require["dev"] = ( 29 | extras_require["dev"] + extras_require["docs"] + extras_require["test"] 30 | ) 31 | 32 | 33 | with open("./README.md") as readme: 34 | long_description = readme.read() 35 | 36 | 37 | setup( 38 | name="py-geth", 39 | # *IMPORTANT*: Don't manually change the version here. Use the 'bump-my-version' utility. 40 | version="5.6.0", 41 | description="""py-geth: Run Go-Ethereum as a subprocess""", 42 | long_description_content_type="text/markdown", 43 | long_description=long_description, 44 | author="The Ethereum Foundation", 45 | author_email="snakecharmers@ethereum.org", 46 | url="https://github.com/ethereum/py-geth", 47 | include_package_data=True, 48 | py_modules=["geth"], 49 | install_requires=[ 50 | "eval_type_backport>=0.1.0; python_version < '3.10'", 51 | "pydantic>=2.6.0", 52 | "requests>=2.23", 53 | "semantic-version>=2.6.0", 54 | "types-requests>=2.0.0", 55 | "typing-extensions>=4.0.1", 56 | ], 57 | python_requires=">=3.8, <4", 58 | extras_require=extras_require, 59 | license="MIT", 60 | zip_safe=False, 61 | keywords="ethereum go-ethereum geth", 62 | packages=find_packages(exclude=["scripts", "scripts.*", "tests", "tests.*"]), 63 | package_data={"geth": ["py.typed"]}, 64 | classifiers=[ 65 | "Development Status :: 2 - Pre-Alpha", 66 | "Intended Audience :: Developers", 67 | "License :: OSI Approved :: MIT License", 68 | "Natural Language :: English", 69 | "Programming Language :: Python :: 3", 70 | "Programming Language :: Python :: 3.8", 71 | "Programming Language :: Python :: 3.9", 72 | "Programming Language :: Python :: 3.10", 73 | "Programming Language :: Python :: 3.11", 74 | "Programming Language :: Python :: 3.12", 75 | "Programming Language :: Python :: 3.13", 76 | ], 77 | ) 78 | -------------------------------------------------------------------------------- /tests/core/accounts/conftest.py: -------------------------------------------------------------------------------- 1 | import pytest 2 | import os 3 | 4 | PROJECTS_DIR = os.path.join(os.path.abspath(os.path.dirname(__file__)), "projects") 5 | 6 | 7 | @pytest.fixture 8 | def one_account_data_dir(): 9 | data_dir = os.path.join(PROJECTS_DIR, "test-01") 10 | return data_dir 11 | 12 | 13 | @pytest.fixture 14 | def three_account_data_dir(): 15 | data_dir = os.path.join(PROJECTS_DIR, "test-02") 16 | return data_dir 17 | 18 | 19 | @pytest.fixture 20 | def no_account_data_dir(): 21 | data_dir = os.path.join(PROJECTS_DIR, "test-03") 22 | return data_dir 23 | -------------------------------------------------------------------------------- /tests/core/accounts/projects/test-01/keystore/UTC--2015-08-24T21-30-14.222885490Z--ae71658b3ab452f7e4f03bda6f777b860b2e2ff2: -------------------------------------------------------------------------------- 1 | {"address":"ae71658b3ab452f7e4f03bda6f777b860b2e2ff2","Crypto":{"cipher":"aes-128-ctr","ciphertext":"dcc6f842d72fdbb8afffc15ded1af0d74e613ba4d987d3cc83f6199da5761d1d","cipherparams":{"iv":"8a242df8817f6a89de25fa2c6f0540fd"},"kdf":"scrypt","kdfparams":{"dklen":32,"n":262144,"p":1,"r":8,"salt":"d29b0b98e506d976e71c5487558b643461f5711d60e91686e5812be2cbdbdbcd"},"mac":"a0f724700dee043c55f36305005afbbcc9f3088ee1b7cdbed2dfcf28a0dba663"},"id":"29090707-ae9e-463b-8a16-870a5362f6d2","version":3} -------------------------------------------------------------------------------- /tests/core/accounts/projects/test-02/keystore/UTC--2015-08-24T21-30-14.222885490Z--ae71658b3ab452f7e4f03bda6f777b860b2e2ff2: -------------------------------------------------------------------------------- 1 | {"address":"ae71658b3ab452f7e4f03bda6f777b860b2e2ff2","Crypto":{"cipher":"aes-128-ctr","ciphertext":"dcc6f842d72fdbb8afffc15ded1af0d74e613ba4d987d3cc83f6199da5761d1d","cipherparams":{"iv":"8a242df8817f6a89de25fa2c6f0540fd"},"kdf":"scrypt","kdfparams":{"dklen":32,"n":262144,"p":1,"r":8,"salt":"d29b0b98e506d976e71c5487558b643461f5711d60e91686e5812be2cbdbdbcd"},"mac":"a0f724700dee043c55f36305005afbbcc9f3088ee1b7cdbed2dfcf28a0dba663"},"id":"29090707-ae9e-463b-8a16-870a5362f6d2","version":3} -------------------------------------------------------------------------------- /tests/core/accounts/projects/test-02/keystore/UTC--2015-08-24T21-32-00.716418819Z--e8e085862a8d951dd78ec5ea784b3e22ee1ca9c6: -------------------------------------------------------------------------------- 1 | {"address":"e8e085862a8d951dd78ec5ea784b3e22ee1ca9c6","Crypto":{"cipher":"aes-128-ctr","ciphertext":"92ebde5b7f92b0cdbfcba1340a88a550d07a304f7ce333a901142a5c08258822","cipherparams":{"iv":"b0710ec019b1f96cfc21f4b8133e0be1"},"kdf":"scrypt","kdfparams":{"dklen":32,"n":262144,"p":1,"r":8,"salt":"9ba6f3c6462054a25ef358f217a6c6aa2a1369c0dcef43112fa499fe72869028"},"mac":"7ed8591ce7f97783dd897c0c484f4c7e4905165d8695e60d6eac6ca65f26a475"},"id":"3985e8a9-9114-473f-aa1f-cbf0b768c510","version":3} -------------------------------------------------------------------------------- /tests/core/accounts/projects/test-02/keystore/UTC--2015-08-24T21-32-04.748321142Z--0da70f43a568e88168436be52ed129f4a9bbdaf5: -------------------------------------------------------------------------------- 1 | {"address":"0da70f43a568e88168436be52ed129f4a9bbdaf5","Crypto":{"cipher":"aes-128-ctr","ciphertext":"f8cb976d758fb6ba68068771394027d34da12adfdd8ed0ecd92b36ec7e30458a","cipherparams":{"iv":"6de38f095db7f3c73dd4b091aab5513b"},"kdf":"scrypt","kdfparams":{"dklen":32,"n":262144,"p":1,"r":8,"salt":"2593f0013f0abe1e7a4bf861a33f42a9e00ea34c2be03466565a6ef7e751de83"},"mac":"20ab16c4dcaea6194429a575d500edb05ff55ad57809e0a6a117f632bb614a01"},"id":"080519cb-696a-4a73-893d-d825dd8b9332","version":3} -------------------------------------------------------------------------------- /tests/core/accounts/test_account_list_parsing.py: -------------------------------------------------------------------------------- 1 | from geth.accounts import ( 2 | parse_geth_accounts, 3 | ) 4 | 5 | raw_accounts = b"""Account #0: {8c28b76a845f525a7f91149864574d3a4986e693} 6 | keystore:///private/tmp/pytest-of-pygeth/pytest-1/test_with_no_overrides0/base-dir 7 | /testing/keystore/UTC--2024-06-19T20-40-51.284430000Z 8 | --8c28b76a845f525a7f91149864574d3a4986e693\n 9 | Account #1: {6f137a71a6f197df2cbbf010dcbd3c444ef5c925} keystore:///private/tmp 10 | /pytest-of-pygeth/pytest-1/test_with_no_overrides0/base-dir 11 | /testing/keystore/UTC--2024-06-19T20-40-51.284430000Z 12 | --6f137a71a6f197df2cbbf010dcbd3c444ef5c925\n""" 13 | accounts = ( 14 | "0x8c28b76a845f525a7f91149864574d3a4986e693", 15 | "0x6f137a71a6f197df2cbbf010dcbd3c444ef5c925", 16 | ) 17 | 18 | 19 | def test_parsing_accounts_output(): 20 | assert sorted(list(parse_geth_accounts(raw_accounts))) == sorted(list(accounts)) 21 | -------------------------------------------------------------------------------- /tests/core/accounts/test_create_geth_account.py: -------------------------------------------------------------------------------- 1 | import os 2 | 3 | from geth.accounts import ( 4 | create_new_account, 5 | get_accounts, 6 | ) 7 | 8 | 9 | def test_create_new_account_with_text_password(tmpdir): 10 | data_dir = str(tmpdir.mkdir("data-dir")) 11 | 12 | assert not get_accounts(data_dir=data_dir) 13 | 14 | account_0 = create_new_account(data_dir=data_dir, password=b"some-text-password") 15 | account_1 = create_new_account(data_dir=data_dir, password=b"some-text-password") 16 | 17 | accounts = get_accounts(data_dir=data_dir) 18 | assert sorted((account_0, account_1)) == sorted(tuple(set(accounts))) 19 | 20 | 21 | def test_create_new_account_with_file_based_password(tmpdir): 22 | pw_file_path = str(tmpdir.mkdir("data-dir").join("geth_password_file")) 23 | 24 | with open(pw_file_path, "w") as pw_file: 25 | pw_file.write("some-text-password-in-a-file") 26 | 27 | data_dir = os.path.dirname(pw_file_path) 28 | 29 | assert not get_accounts(data_dir=data_dir) 30 | 31 | account_0 = create_new_account(data_dir=data_dir, password=pw_file_path) 32 | account_1 = create_new_account(data_dir=data_dir, password=pw_file_path) 33 | 34 | accounts = get_accounts(data_dir=data_dir) 35 | assert sorted((account_0, account_1)) == sorted(tuple(set(accounts))) 36 | -------------------------------------------------------------------------------- /tests/core/accounts/test_geth_accounts.py: -------------------------------------------------------------------------------- 1 | from geth.accounts import ( 2 | get_accounts, 3 | ) 4 | 5 | 6 | def test_single_account(one_account_data_dir): 7 | data_dir = one_account_data_dir 8 | accounts = get_accounts(data_dir=data_dir) 9 | assert tuple(set(accounts)) == ("0xae71658b3ab452f7e4f03bda6f777b860b2e2ff2",) 10 | 11 | 12 | def test_multiple_accounts(three_account_data_dir): 13 | data_dir = three_account_data_dir 14 | accounts = get_accounts(data_dir=data_dir) 15 | assert sorted(tuple(set(accounts))) == sorted( 16 | ( 17 | "0xae71658b3ab452f7e4f03bda6f777b860b2e2ff2", 18 | "0xe8e085862a8d951dd78ec5ea784b3e22ee1ca9c6", 19 | "0x0da70f43a568e88168436be52ed129f4a9bbdaf5", 20 | ) 21 | ) 22 | 23 | 24 | def test_no_accounts(no_account_data_dir): 25 | data_dir = no_account_data_dir 26 | accounts = get_accounts(data_dir=data_dir) 27 | assert accounts == tuple() 28 | -------------------------------------------------------------------------------- /tests/core/running/test_running_dev_chain.py: -------------------------------------------------------------------------------- 1 | import copy 2 | import json 3 | import os 4 | import re 5 | 6 | from geth import ( 7 | DevGethProcess, 8 | ) 9 | 10 | # open genesis.json file from geth main directory 11 | MAIN_DIR = os.path.abspath(os.path.join(os.path.dirname(__file__), "..", "..", "..")) 12 | 13 | with open(os.path.join(MAIN_DIR, "geth", "genesis.json")) as genesis_file: 14 | GENESIS_JSON = json.load(genesis_file) 15 | 16 | 17 | def test_version(): 18 | geth = DevGethProcess("testing") 19 | # x.y.z-stable 20 | regex = re.compile(r"\d+\.\d+\.\d+-stable") 21 | assert regex.match(geth.version) 22 | 23 | 24 | def test_with_no_overrides(base_dir): 25 | geth = DevGethProcess("testing", base_dir=base_dir) 26 | 27 | geth.start() 28 | assert geth.is_running 29 | assert geth.is_alive 30 | geth.stop() 31 | assert geth.is_stopped 32 | 33 | 34 | def test_dev_geth_process_generates_accounts(base_dir): 35 | geth = DevGethProcess("testing", base_dir=base_dir) 36 | assert len(set(geth.accounts)) == 1 37 | 38 | 39 | def test_dev_geth_process_generates_genesis_json_from_genesis_data(base_dir): 40 | shanghai_genesis = copy.deepcopy(GENESIS_JSON) 41 | config = shanghai_genesis.pop("config") 42 | 43 | # stop at Shanghai, drop any keys in config after `shanghaiTime` 44 | shanghai_config = {} 45 | for key, _value in config.items(): 46 | shanghai_config[key] = config[key] 47 | if key == "shanghaiTime": 48 | break 49 | 50 | assert "cancunTime" not in shanghai_config 51 | shanghai_genesis["config"] = shanghai_config 52 | 53 | geth = DevGethProcess("testing", base_dir=base_dir, genesis_data=shanghai_genesis) 54 | 55 | # assert genesis.json exists and has the correct data 56 | assert os.path.exists(os.path.join(geth.data_dir, "genesis.json")) 57 | with open(os.path.join(geth.data_dir, "genesis.json")) as genesis_file: 58 | genesis_data = json.load(genesis_file) 59 | 60 | assert genesis_data == shanghai_genesis 61 | 62 | geth.start() 63 | assert geth.is_running 64 | assert geth.is_alive 65 | geth.stop() 66 | assert geth.is_stopped 67 | 68 | 69 | def test_default_config(base_dir): 70 | geth = DevGethProcess("testing", base_dir=base_dir) 71 | 72 | assert os.path.exists(os.path.join(geth.data_dir, "genesis.json")) 73 | with open(os.path.join(geth.data_dir, "genesis.json")) as genesis_file: 74 | genesis_data = json.load(genesis_file) 75 | 76 | # assert genesis_data == GENESIS_JSON with the exception of an added coinbase and 77 | # alloc for that coinbase 78 | injected_coinbase = genesis_data.pop("coinbase") 79 | assert injected_coinbase in genesis_data["alloc"] 80 | assert injected_coinbase in geth.accounts 81 | 82 | injected_cb_alloc = genesis_data["alloc"].pop(injected_coinbase) 83 | assert injected_cb_alloc == {"balance": "1000000000000000000000000000000"} 84 | 85 | try: 86 | assert genesis_data == GENESIS_JSON 87 | except AssertionError: 88 | assert geth.version.startswith("1.14.0") 89 | assert genesis_data["config"]["terminalTotalDifficulty"] == -1 90 | 91 | geth.start() 92 | assert geth.is_running 93 | assert geth.is_alive 94 | geth.stop() 95 | assert geth.is_stopped 96 | -------------------------------------------------------------------------------- /tests/core/running/test_running_mainnet_chain.py: -------------------------------------------------------------------------------- 1 | import pytest 2 | 3 | from geth import ( 4 | MainnetGethProcess, 5 | ) 6 | from geth.mixins import ( 7 | LoggingMixin, 8 | ) 9 | from geth.utils.networking import ( 10 | get_open_port, 11 | ) 12 | 13 | 14 | class LoggedMainnetGethProcess(LoggingMixin, MainnetGethProcess): 15 | pass 16 | 17 | 18 | def test_live_chain_with_no_overrides(): 19 | geth = LoggedMainnetGethProcess(geth_kwargs={"port": get_open_port()}) 20 | 21 | geth.start() 22 | 23 | geth.wait_for_ipc(180) 24 | 25 | assert geth.is_running 26 | assert geth.is_alive 27 | 28 | geth.stop() 29 | 30 | assert geth.is_stopped 31 | 32 | 33 | @pytest.mark.parametrize( 34 | "ipc_path", 35 | [ 36 | "", 37 | None, 38 | ], 39 | ) 40 | def test_ipc_path_always_returns_a_string(ipc_path): 41 | geth = LoggedMainnetGethProcess(geth_kwargs={"ipc_path": ipc_path}) 42 | 43 | assert isinstance(geth.ipc_path, str) 44 | -------------------------------------------------------------------------------- /tests/core/running/test_running_sepolia_chain.py: -------------------------------------------------------------------------------- 1 | from geth import ( 2 | SepoliaGethProcess, 3 | ) 4 | from geth.mixins import ( 5 | LoggingMixin, 6 | ) 7 | from geth.utils.networking import ( 8 | get_open_port, 9 | ) 10 | 11 | 12 | class LoggedSepoliaGethProcess(LoggingMixin, SepoliaGethProcess): 13 | pass 14 | 15 | 16 | def test_testnet_chain_with_no_overrides(): 17 | geth = LoggedSepoliaGethProcess(geth_kwargs={"port": get_open_port()}) 18 | 19 | geth.start() 20 | 21 | geth.wait_for_ipc(180) 22 | 23 | assert geth.is_running 24 | assert geth.is_alive 25 | 26 | geth.stop() 27 | 28 | assert geth.is_stopped 29 | -------------------------------------------------------------------------------- /tests/core/running/test_running_with_logging.py: -------------------------------------------------------------------------------- 1 | import pytest 2 | import threading 3 | 4 | from geth import ( 5 | DevGethProcess, 6 | ) 7 | from geth.mixins import ( 8 | LoggingMixin, 9 | ) 10 | 11 | _errors = [] 12 | 13 | 14 | @pytest.fixture(autouse=True) 15 | def fail_from_errors_on_other_threads(): 16 | """ 17 | Causes errors when `LoggingMixin` is improperly implemented. 18 | Useful for preventing false-positives in logging-based tests. 19 | """ 20 | 21 | def pytest_excepthook(*args, **kwargs): 22 | _errors.extend(args) 23 | 24 | threading.excepthook = pytest_excepthook 25 | 26 | yield 27 | 28 | if _errors: 29 | caught_errors_str = ", ".join([str(err) for err in _errors]) 30 | pytest.fail(f"Caught exceptions from other threads:\n{caught_errors_str}") 31 | 32 | 33 | class WithLogging(LoggingMixin, DevGethProcess): 34 | pass 35 | 36 | 37 | def test_with_logging(base_dir, caplog): 38 | test_stdout_path = f"{base_dir}/testing/stdoutlogs.log" 39 | test_stderr_path = f"{base_dir}/testing/stderrlogs.log" 40 | 41 | geth = WithLogging( 42 | "testing", 43 | base_dir=base_dir, 44 | stdout_logfile_path=test_stdout_path, 45 | stderr_logfile_path=test_stderr_path, 46 | ) 47 | 48 | geth.start() 49 | 50 | assert geth.is_running 51 | assert geth.is_alive 52 | 53 | stdout_logger_info = geth.stdout_callbacks[0] 54 | stderr_logger_info = geth.stderr_callbacks[0] 55 | 56 | stdout_logger_info("test_out") 57 | stderr_logger_info("test_err") 58 | 59 | with open(test_stdout_path) as out_log_file: 60 | line = out_log_file.readline() 61 | assert line == "test_out\n" 62 | 63 | with open(test_stderr_path) as err_log_file: 64 | line = err_log_file.readline() 65 | assert line == "test_err\n" 66 | 67 | geth.stop() 68 | -------------------------------------------------------------------------------- /tests/core/running/test_use_as_a_context_manager.py: -------------------------------------------------------------------------------- 1 | from geth import ( 2 | DevGethProcess, 3 | ) 4 | 5 | 6 | def test_using_as_a_context_manager(base_dir): 7 | geth = DevGethProcess("testing", base_dir=base_dir) 8 | 9 | assert not geth.is_running 10 | assert not geth.is_alive 11 | 12 | with geth: 13 | assert geth.is_running 14 | assert geth.is_alive 15 | 16 | assert geth.is_stopped 17 | -------------------------------------------------------------------------------- /tests/core/test_import_and_version.py: -------------------------------------------------------------------------------- 1 | def test_import_and_version(): 2 | import geth 3 | 4 | assert isinstance(geth.__version__, str) 5 | -------------------------------------------------------------------------------- /tests/core/test_library_files.py: -------------------------------------------------------------------------------- 1 | import os 2 | 3 | from geth.exceptions import ( 4 | GethError, 5 | ) 6 | 7 | PY_GETH_PATH = os.path.join( 8 | os.path.dirname(os.path.abspath(__file__)), "..", "..", "geth" 9 | ) 10 | DEFAULT_EXCEPTIONS = ( 11 | AssertionError, 12 | AttributeError, 13 | FileNotFoundError, 14 | GethError, 15 | KeyError, 16 | NotImplementedError, 17 | OSError, 18 | TypeError, 19 | ValueError, 20 | ) 21 | 22 | 23 | def test_no_default_exceptions_are_raised_within_py_geth(): 24 | for root, _dirs, files in os.walk(PY_GETH_PATH): 25 | for file in files: 26 | if file.endswith(".py"): 27 | file_path = os.path.join(root, file) 28 | with open(file_path, encoding="utf-8") as f: 29 | for idx, line in enumerate(f): 30 | for exception in DEFAULT_EXCEPTIONS: 31 | exception_name = exception.__name__ 32 | if f"raise {exception_name}" in line: 33 | raise Exception( 34 | f"``{exception_name}`` raised in py-geth file " 35 | f"``{file}``, line {idx + 1}. " 36 | f"Replace with ``PyGeth{exception_name}``:\n" 37 | f" file_path:{file_path}\n" 38 | f" line:{idx + 1}" 39 | ) 40 | -------------------------------------------------------------------------------- /tests/core/utility/test_constructing_test_chain_kwargs.py: -------------------------------------------------------------------------------- 1 | import contextlib 2 | import os 3 | import shutil 4 | import tempfile 5 | 6 | from geth.wrapper import ( 7 | construct_test_chain_kwargs, 8 | get_max_socket_path_length, 9 | ) 10 | 11 | 12 | @contextlib.contextmanager 13 | def tempdir(): 14 | directory = tempfile.mkdtemp() 15 | 16 | try: 17 | yield directory 18 | finally: 19 | shutil.rmtree(directory) 20 | 21 | 22 | def test_short_data_directory_paths_use_local_geth_ipc_socket(): 23 | with tempdir() as data_dir: 24 | expected_path = os.path.abspath(os.path.join(data_dir, "geth.ipc")) 25 | assert len(expected_path) < get_max_socket_path_length() 26 | chain_kwargs = construct_test_chain_kwargs(data_dir=data_dir) 27 | 28 | assert chain_kwargs["ipc_path"] == expected_path 29 | 30 | 31 | def test_long_data_directory_paths_use_tempfile_geth_ipc_socket(): 32 | with tempdir() as temp_directory: 33 | data_dir = os.path.abspath( 34 | os.path.join( 35 | temp_directory, 36 | "this-path-is-longer-than-the-maximum-unix-socket-path-length", 37 | "and-thus-the-underlying-function-should-not-use-it-for-the", 38 | "geth-ipc-path", 39 | ) 40 | ) 41 | data_dir_ipc_path = os.path.abspath(os.path.join(data_dir, "geth.ipc")) 42 | assert len(data_dir_ipc_path) > get_max_socket_path_length() 43 | 44 | chain_kwargs = construct_test_chain_kwargs(data_dir=data_dir) 45 | 46 | assert chain_kwargs["ipc_path"] != data_dir_ipc_path 47 | -------------------------------------------------------------------------------- /tests/core/utility/test_geth_version.py: -------------------------------------------------------------------------------- 1 | import semantic_version 2 | 3 | from geth import ( 4 | get_geth_version, 5 | ) 6 | 7 | 8 | def test_get_geth_version(): 9 | version = get_geth_version() 10 | 11 | assert isinstance(version, semantic_version.Version) 12 | -------------------------------------------------------------------------------- /tests/core/utility/test_is_live_chain.py: -------------------------------------------------------------------------------- 1 | import pytest 2 | import os 3 | 4 | from geth.chain import ( 5 | is_live_chain, 6 | ) 7 | 8 | 9 | @pytest.mark.parametrize( 10 | "platform,data_dir,should_be_live", 11 | ( 12 | ("darwin", "~", False), 13 | ("darwin", "~/Library/Ethereum", True), 14 | ("linux2", "~", False), 15 | ("linux2", "~/.ethereum", True), 16 | ), 17 | ) 18 | def test_is_live_chain(monkeypatch, platform, data_dir, should_be_live): 19 | monkeypatch.setattr("sys.platform", platform) 20 | if platform == "win32": 21 | monkeypatch.setattr("os.path.sep", "\\") 22 | 23 | expanded_data_dir = os.path.expanduser(data_dir) 24 | relative_data_dir = os.path.relpath(expanded_data_dir) 25 | 26 | assert is_live_chain(data_dir) is should_be_live 27 | assert is_live_chain(expanded_data_dir) is should_be_live 28 | assert is_live_chain(relative_data_dir) is should_be_live 29 | -------------------------------------------------------------------------------- /tests/core/utility/test_is_sepolia_chain.py: -------------------------------------------------------------------------------- 1 | import pytest 2 | import os 3 | 4 | from geth.chain import ( 5 | is_sepolia_chain, 6 | ) 7 | 8 | 9 | @pytest.mark.parametrize( 10 | "platform,data_dir,should_be_sepolia", 11 | ( 12 | ("darwin", "~", False), 13 | ("darwin", "~/Library/Ethereum/sepolia", True), 14 | ("linux2", "~", False), 15 | ("linux2", "~/.ethereum/sepolia", True), 16 | ), 17 | ) 18 | def test_is_sepolia_chain(monkeypatch, platform, data_dir, should_be_sepolia): 19 | monkeypatch.setattr("sys.platform", platform) 20 | if platform == "win32": 21 | monkeypatch.setattr("os.path.sep", "\\") 22 | 23 | expanded_data_dir = os.path.expanduser(data_dir) 24 | relative_data_dir = os.path.relpath(expanded_data_dir) 25 | 26 | assert is_sepolia_chain(data_dir) is should_be_sepolia 27 | assert is_sepolia_chain(expanded_data_dir) is should_be_sepolia 28 | assert is_sepolia_chain(relative_data_dir) is should_be_sepolia 29 | -------------------------------------------------------------------------------- /tests/core/utility/test_validation.py: -------------------------------------------------------------------------------- 1 | from __future__ import ( 2 | annotations, 3 | ) 4 | 5 | import pytest 6 | 7 | from geth.exceptions import ( 8 | PyGethValueError, 9 | ) 10 | from geth.utils.validation import ( 11 | validate_genesis_data, 12 | validate_geth_kwargs, 13 | ) 14 | 15 | 16 | @pytest.mark.parametrize( 17 | "geth_kwargs", 18 | [ 19 | { 20 | "data_dir": "/tmp", 21 | "network_id": "123", 22 | "rpc_port": "1234", 23 | "dev_mode": True, 24 | }, 25 | ], 26 | ) 27 | def test_validate_geth_kwargs_good(geth_kwargs): 28 | assert validate_geth_kwargs(geth_kwargs) is None 29 | 30 | 31 | @pytest.mark.parametrize( 32 | "geth_kwargs", 33 | [ 34 | { 35 | "data_dir": "/tmp", 36 | "network_id": 123, 37 | "dev_mode": "abc", 38 | } 39 | ], 40 | ) 41 | def test_validate_geth_kwargs_bad(geth_kwargs): 42 | with pytest.raises(PyGethValueError): 43 | validate_geth_kwargs(geth_kwargs) 44 | 45 | 46 | @pytest.mark.parametrize( 47 | "genesis_data", 48 | [ 49 | { 50 | "difficulty": "0x00012131", 51 | "nonce": "abc", 52 | "timestamp": "1234", 53 | } 54 | ], 55 | ) 56 | def test_validate_genesis_data_good(genesis_data): 57 | assert validate_genesis_data(genesis_data) is None 58 | 59 | 60 | @pytest.mark.parametrize( 61 | "genesis_data", 62 | [ 63 | { 64 | "difficulty": "0x00012131", 65 | "nonce": "abc", 66 | "cats": "1234", 67 | }, 68 | { 69 | "difficulty": "0x00012131", 70 | "nonce": "abc", 71 | "config": "1234", 72 | }, 73 | { 74 | "difficulty": "0x00012131", 75 | "nonce": "abc", 76 | "config": None, 77 | }, 78 | "kangaroo", 79 | { 80 | "difficulty": "0x00012131", 81 | "nonce": "abc", 82 | "timestamp": "1234", 83 | "config": { 84 | "cancunTime": 5, 85 | "blobSchedule": {}, 86 | }, 87 | }, 88 | ], 89 | ) 90 | def test_validate_genesis_data_bad(genesis_data): 91 | with pytest.raises(PyGethValueError): 92 | validate_genesis_data(genesis_data) 93 | -------------------------------------------------------------------------------- /tests/core/waiting/conftest.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ethereum/py-geth/d2559a139c66a13f4bad6aba731ea8fd0c823512/tests/core/waiting/conftest.py -------------------------------------------------------------------------------- /tests/core/waiting/test_waiting_for_ipc_socket.py: -------------------------------------------------------------------------------- 1 | import pytest 2 | 3 | from flaky import ( 4 | flaky, 5 | ) 6 | 7 | from geth import ( 8 | DevGethProcess, 9 | ) 10 | from geth.utils.timeout import ( 11 | Timeout, 12 | ) 13 | 14 | 15 | def test_waiting_for_ipc_socket(base_dir): 16 | with DevGethProcess("testing", base_dir=base_dir) as geth: 17 | assert geth.is_running 18 | geth.wait_for_ipc(timeout=20) 19 | 20 | 21 | @flaky(max_runs=3) 22 | def test_timeout_waiting_for_ipc_socket(base_dir): 23 | with DevGethProcess("testing", base_dir=base_dir) as geth: 24 | assert geth.is_running 25 | with pytest.raises(Timeout): 26 | geth.wait_for_ipc(timeout=0.01) 27 | -------------------------------------------------------------------------------- /tests/core/waiting/test_waiting_for_rpc_connection.py: -------------------------------------------------------------------------------- 1 | import pytest 2 | 3 | from flaky import ( 4 | flaky, 5 | ) 6 | 7 | from geth import ( 8 | DevGethProcess, 9 | ) 10 | from geth.utils.timeout import ( 11 | Timeout, 12 | ) 13 | 14 | 15 | def test_waiting_for_rpc_connection(base_dir): 16 | with DevGethProcess("testing", base_dir=base_dir) as geth: 17 | assert geth.is_running 18 | geth.wait_for_rpc(timeout=20) 19 | 20 | 21 | @flaky(max_runs=3) 22 | def test_timeout_waiting_for_rpc_connection(base_dir): 23 | with DevGethProcess("testing", base_dir=base_dir) as geth: 24 | with pytest.raises(Timeout): 25 | geth.wait_for_rpc(timeout=0.1) 26 | -------------------------------------------------------------------------------- /tests/installation/test_geth_installation.py: -------------------------------------------------------------------------------- 1 | import pytest 2 | import os 3 | 4 | import semantic_version 5 | 6 | from geth import ( 7 | get_geth_version, 8 | ) 9 | from geth.install import ( 10 | INSTALL_FUNCTIONS, 11 | get_executable_path, 12 | get_platform, 13 | install_geth, 14 | ) 15 | 16 | INSTALLATION_TEST_PARAMS = tuple( 17 | (platform, version) 18 | for platform, platform_install_functions in INSTALL_FUNCTIONS.items() 19 | for version in platform_install_functions.keys() 20 | ) 21 | 22 | 23 | @pytest.mark.skipif( 24 | "GETH_RUN_INSTALL_TESTS" not in os.environ, 25 | reason=( 26 | "Installation tests will not run unless `GETH_RUN_INSTALL_TESTS` " 27 | "environment variable is set" 28 | ), 29 | ) 30 | @pytest.mark.parametrize( 31 | "platform,version", 32 | INSTALLATION_TEST_PARAMS, 33 | ) 34 | def test_geth_installation_as_function_call(monkeypatch, tmpdir, platform, version): 35 | if get_platform() != platform: 36 | pytest.skip("Wrong platform for install script") 37 | 38 | base_install_path = str(tmpdir.mkdir("temporary-dir")) 39 | monkeypatch.setenv("GETH_BASE_INSTALL_PATH", base_install_path) 40 | 41 | # sanity check that it's not already installed. 42 | executable_path = get_executable_path(version) 43 | assert not os.path.exists(executable_path) 44 | 45 | install_geth(identifier=version, platform=platform) 46 | 47 | assert os.path.exists(executable_path) 48 | monkeypatch.setenv("GETH_BINARY", executable_path) 49 | 50 | actual_version = get_geth_version() 51 | expected_version = semantic_version.Spec(version.lstrip("v")) 52 | 53 | assert actual_version in expected_version 54 | -------------------------------------------------------------------------------- /tox.ini: -------------------------------------------------------------------------------- 1 | [tox] 2 | envlist= 3 | py{38,39,310,311,312,313}-lint 4 | py{38,39,310,311,312,313}-install-geth-{\ 5 | v1_14_0, v1_14_2, v1_14_3, v1_14_4, v1_14_5, v1_14_6, v1_14_7, \ 6 | v1_14_8, v1_14_9, v1_14_10, v1_14_11, v1_14_12, v1_14_13, v1_15_0, \ 7 | v1_15_1, v1_15_2, v1_15_3, v1_15_4, v1_15_5, v1_15_6, v1_15_7, \ 8 | v1_15_8, v1_15_9, v1_15_10, v1_15_11 \ 9 | } 10 | py{38,39,310,311,312,313}-wheel 11 | windows-wheel 12 | 13 | [flake8] 14 | exclude=venv*,.tox,docs,build 15 | extend-ignore=E203 16 | max-line-length=88 17 | 18 | [blocklint] 19 | max_issue_threshold=1 20 | 21 | [testenv] 22 | usedevelop=True 23 | commands= 24 | install-geth: {[common_geth_installation_and_check]commands} 25 | passenv= 26 | GETH_VERSION 27 | GOROOT 28 | GOPATH 29 | HOME 30 | PATH 31 | setenv= 32 | installation: GETH_RUN_INSTALL_TESTS=enabled 33 | deps= 34 | .[test] 35 | install-geth: {[common_geth_installation_and_check]deps} 36 | basepython= 37 | windows-wheel: python 38 | py38: python3.8 39 | py39: python3.9 40 | py310: python3.10 41 | py311: python3.11 42 | py312: python3.12 43 | py313: python3.13 44 | allowlist_externals=bash,make,pre-commit 45 | 46 | [common_geth_installation_and_check] 47 | deps=.[dev,test] 48 | commands= 49 | bash ./.circleci/install_geth.sh 50 | pytest {posargs:tests/core} 51 | pytest {posargs:-s tests/installation} 52 | 53 | [testenv:py{38,39,310,311,312,313}-lint] 54 | deps=pre-commit 55 | extras= 56 | dev 57 | commands= 58 | pre-commit install 59 | pre-commit run --all-files --show-diff-on-failure 60 | 61 | [testenv:py{38,39,310,311,312,313}-wheel] 62 | deps= 63 | wheel 64 | build[virtualenv] 65 | allowlist_externals= 66 | /bin/rm 67 | /bin/bash 68 | commands= 69 | python -m pip install --upgrade pip 70 | /bin/rm -rf build dist 71 | python -m build 72 | /bin/bash -c 'python -m pip install --upgrade "$(ls dist/py_geth-*-py3-none-any.whl)" --progress-bar off' 73 | python -c "import geth" 74 | skip_install=true 75 | 76 | [testenv:windows-wheel] 77 | deps= 78 | wheel 79 | build[virtualenv] 80 | allowlist_externals= 81 | bash.exe 82 | commands= 83 | python --version 84 | python -m pip install --upgrade pip 85 | bash.exe -c "rm -rf build dist" 86 | python -m build 87 | bash.exe -c 'python -m pip install --upgrade "$(ls dist/py_geth-*-py3-none-any.whl)" --progress-bar off' 88 | python -c "import geth" 89 | skip_install=true 90 | -------------------------------------------------------------------------------- /update_geth.py: -------------------------------------------------------------------------------- 1 | """ 2 | A script to automate adding support for new geth versions. 3 | 4 | To add support for a geth version, run the following line from the py-geth directory, 5 | substituting the version for the one you wish to add support for. Note that the 'v' in 6 | the versioning is optional. 7 | 8 | .. code-block:: shell 9 | 10 | $ python update_geth.py v1_10_9 11 | 12 | To introduce support for more than one version, pass in the versions in increasing 13 | order, ending with the latest version. 14 | 15 | .. code-block:: shell 16 | 17 | $ python update_geth.py v1_10_7 v1_10_8 v1_10_9 18 | 19 | Note: Always review your changes before committing as something may cause this existing 20 | pattern to change at some point. 21 | """ 22 | 23 | import fileinput 24 | import re 25 | import sys 26 | 27 | GETH_VERSION_REGEX = re.compile(r"v\d*_\d+") # v0_0_0 pattern 28 | GETH_VERSION_REGEX_NO_V = re.compile(r"\d*_\d+") # 0_0_0 pattern 29 | 30 | currently_supported_geth_versions = [] 31 | with open("tox.ini") as tox_ini: 32 | for line_number, line in enumerate(tox_ini, start=1): 33 | if line_number == 15: 34 | # supported versions are near the beginning of the tox.ini file 35 | break 36 | if "install-geth" in line: 37 | line.replace(" ", "") 38 | circleci_python_versions = line[ 39 | line.find("py{") + 3 : line.find("}") 40 | ].split(",") 41 | if GETH_VERSION_REGEX.search(line): 42 | line = line.replace(" ", "") # clean space 43 | line = line.replace("\n", "") # remove trailing indent 44 | line = line.replace("\\", "") # remove the multiline backslash 45 | line = line if line[-1] != "," else line[:-1] 46 | for version in line.split(","): 47 | currently_supported_geth_versions.append(version.strip()) 48 | LATEST_SUPPORTED_GETH_VERSION = currently_supported_geth_versions[-1] 49 | LATEST_PYTHON_VERSION = circleci_python_versions[-1] 50 | 51 | # geth/install.py pattern 52 | GETH_INSTALL_PATTERN = { 53 | "versions": "", 54 | "installs": "", 55 | "version<->install": "", 56 | } 57 | 58 | user_provided_versions = sys.argv[1:] 59 | normalized_user_versions = [] 60 | for user_provided_version in user_provided_versions: 61 | if "v" not in user_provided_version: 62 | user_provided_version = f"v{user_provided_version}" 63 | normalized_user_versions.append(user_provided_version) 64 | 65 | if ( 66 | not GETH_VERSION_REGEX.match(user_provided_version) 67 | or len(user_provided_versions) == 0 68 | ): 69 | raise ValueError("missing or improper format for provided geth versions") 70 | 71 | if user_provided_version in currently_supported_geth_versions: 72 | raise ValueError( 73 | f"provided version is already supported: {user_provided_version}" 74 | ) 75 | latest_user_provided_version = normalized_user_versions[-1] 76 | 77 | # set up geth/install.py pattern 78 | user_version_upper = user_provided_version.upper() 79 | user_version_period = user_provided_version.replace("_", ".") 80 | GETH_INSTALL_PATTERN[ 81 | "versions" 82 | ] += f'{user_version_upper} = "{user_version_period}"\n' 83 | 84 | user_version_install = f"install_v{user_version_upper[1:]}" 85 | GETH_INSTALL_PATTERN["installs"] += ( 86 | f"{user_version_install} = functools.partial(" 87 | f"install_from_source_code_release, {user_version_upper})\n" 88 | ) 89 | GETH_INSTALL_PATTERN[ 90 | "version<->install" 91 | ] += f" {user_version_upper}: {user_version_install},\n" 92 | 93 | 94 | ALL_VERSIONS = currently_supported_geth_versions + normalized_user_versions 95 | 96 | # update .circleci/config.yml versions 97 | with fileinput.FileInput(".circleci/config.yml", inplace=True) as cci_config: 98 | all_versions_no_v = [version[1:] for version in ALL_VERSIONS] 99 | in_geth_versions = False 100 | for line in cci_config: 101 | if in_geth_versions: 102 | print(" ", end="") 103 | for num, v in enumerate(all_versions_no_v, start=1): 104 | if num == len(all_versions_no_v): 105 | print(f'"{v}"', end="\n") 106 | # at most 6 versions per line 107 | elif not num % 6: 108 | print(f'"{v}",\n ', end="") 109 | else: 110 | print(f'"{v}"', end=", ") 111 | in_geth_versions = False 112 | else: 113 | if "geth_version: [" in line: 114 | in_geth_versions = True 115 | if GETH_VERSION_REGEX_NO_V.search(line): 116 | # clean up the older version lines 117 | print(end="") 118 | else: 119 | print(line, end="") 120 | 121 | # update geth/install.py versions 122 | with fileinput.FileInput("geth/install.py", inplace=True) as geth_install: 123 | latest_supported_upper = LATEST_SUPPORTED_GETH_VERSION.upper() 124 | latest_supported_period = LATEST_SUPPORTED_GETH_VERSION.replace("_", ".") 125 | latest_version_install = f"install_v{latest_supported_upper[1:]}" 126 | for line in geth_install: 127 | if f'{latest_supported_upper} = "{latest_supported_period}"' in line: 128 | print( 129 | f'{latest_supported_upper} = "{latest_supported_period}"\n' 130 | + GETH_INSTALL_PATTERN["versions"], 131 | end="", 132 | ) 133 | elif f"{latest_version_install} = functools.partial" in line: 134 | print( 135 | f"{latest_version_install} = functools.partial(" 136 | f"install_from_source_code_release, {latest_supported_upper})\n" 137 | + GETH_INSTALL_PATTERN["installs"], 138 | end="", 139 | ) 140 | elif (f"{latest_supported_upper}: {latest_version_install}") in line: 141 | print( 142 | f" {latest_supported_upper}: {latest_version_install},\n" 143 | + GETH_INSTALL_PATTERN["version<->install"], 144 | end="", 145 | ) 146 | else: 147 | print(line, end="") 148 | 149 | # update versions in readme to the latest supported version 150 | with fileinput.FileInput("README.md", inplace=True) as readme: 151 | latest_supported_period = LATEST_SUPPORTED_GETH_VERSION.replace("_", ".").replace( 152 | "v", "" 153 | ) 154 | latest_user_provided_period = latest_user_provided_version.replace( 155 | "_", "." 156 | ).replace("v", "") 157 | for line in readme: 158 | print( 159 | line.replace(latest_supported_period, latest_user_provided_period), 160 | end="", 161 | ) 162 | 163 | # update tox.ini versions 164 | with fileinput.FileInput("tox.ini", inplace=True) as tox_ini: 165 | write_versions = False 166 | for line in tox_ini: 167 | if write_versions: 168 | print(" ", end="") 169 | for num, v in enumerate(ALL_VERSIONS, start=1): 170 | if num == len(ALL_VERSIONS): 171 | print(f"{v} \\") 172 | elif not num % 7: 173 | print(f"{v}, \\\n ", end="") 174 | else: 175 | print(v, end=", ") 176 | write_versions = False 177 | else: 178 | if "install-geth-{" in line: 179 | write_versions = True 180 | if GETH_VERSION_REGEX.search(line): 181 | # clean up the older version lines 182 | print(end="") 183 | else: 184 | print(line, end="") 185 | --------------------------------------------------------------------------------