├── .bumpversion.cfg ├── .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 ├── 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 /.bumpversion.cfg: -------------------------------------------------------------------------------- 1 | [bumpversion] 2 | current_version = 5.1.0 3 | commit = True 4 | tag = True 5 | parse = (?P\d+)\.(?P\d+)\.(?P\d+)(-(?P[^.]*)\.(?P\d+))? 6 | serialize = 7 | {major}.{minor}.{patch}-{stage}.{devnum} 8 | {major}.{minor}.{patch} 9 | 10 | [bumpversion:part:stage] 11 | optional_value = stable 12 | first_value = stable 13 | values = 14 | alpha 15 | beta 16 | stable 17 | 18 | [bumpversion:part:devnum] 19 | 20 | [bumpversion:file:setup.py] 21 | search = version="{current_version}", 22 | replace = version="{new_version}", 23 | -------------------------------------------------------------------------------- /.circleci/config.yml: -------------------------------------------------------------------------------- 1 | version: 2.1 2 | 3 | # heavily inspired by https://raw.githubusercontent.com/pinax/pinax-wiki/6bd2a99ab6f702e300d708532a6d1d9aa638b9f8/.circleci/config.yml 4 | 5 | parameters: 6 | go_version: 7 | default: "1.22.4" 8 | type: string 9 | 10 | common_go_steps: &common_go_steps 11 | working_directory: ~/repo 12 | steps: 13 | - checkout 14 | - run: 15 | name: checkout fixtures submodule 16 | command: git submodule update --init --recursive 17 | - run: 18 | name: merge pull request base 19 | command: ./.circleci/merge_pr.sh 20 | - run: 21 | name: merge pull request base (2nd try) 22 | command: ./.circleci/merge_pr.sh 23 | when: on_fail 24 | - run: 25 | name: merge pull request base (3rd try) 26 | command: ./.circleci/merge_pr.sh 27 | when: on_fail 28 | - restore_cache: 29 | keys: 30 | - cache-v1-{{ arch }}-{{ .Environment.CIRCLE_JOB }}-{{ checksum "setup.py" }}-{{ checksum "tox.ini" }} 31 | - run: 32 | name: install dependencies 33 | command: | 34 | python -m pip install --upgrade pip 35 | python -m pip install tox 36 | - run: 37 | name: install golang-<< pipeline.parameters.go_version >> 38 | command: ./.circleci/install_golang.sh << pipeline.parameters.go_version >> 39 | - run: 40 | name: run tox 41 | command: python -m tox run -r 42 | - save_cache: 43 | paths: 44 | - .hypothesis 45 | - .tox 46 | - ~/.cache/pip 47 | - ~/.local 48 | key: cache-v1-{{ arch }}-{{ .Environment.CIRCLE_JOB }}-{{ checksum "setup.py" }}-{{ checksum "tox.ini" }} 49 | 50 | orbs: 51 | win: circleci/windows@5.0.0 52 | 53 | windows-wheel-steps: 54 | windows-wheel-setup: &windows-wheel-setup 55 | executor: 56 | name: win/default 57 | shell: bash.exe 58 | working_directory: C:\Users\circleci\project\py-geth 59 | environment: 60 | TOXENV: windows-wheel 61 | restore-cache-step: &restore-cache-step 62 | restore_cache: 63 | keys: 64 | - cache-v1-{{ arch }}-{{ .Environment.CIRCLE_JOB }}-{{ checksum "setup.py" }}-{{ checksum "tox.ini" }} 65 | install-pyenv-step: &install-pyenv-step 66 | run: 67 | name: install pyenv 68 | command: | 69 | pip install pyenv-win --target $HOME/.pyenv 70 | echo 'export PYENV="$HOME/.pyenv/pyenv-win/"' >> $BASH_ENV 71 | echo 'export PYENV_ROOT="$HOME/.pyenv/pyenv-win/"' >> $BASH_ENV 72 | echo 'export PYENV_USERPROFILE="$HOME/.pyenv/pyenv-win/"' >> $BASH_ENV 73 | echo 'export PATH="$PATH:$HOME/.pyenv/pyenv-win/bin"' >> $BASH_ENV 74 | echo 'export PATH="$PATH:$HOME/.pyenv/pyenv-win/shims"' >> $BASH_ENV 75 | source $BASH_ENV 76 | pyenv update 77 | install-latest-python-step: &install-latest-python-step 78 | run: 79 | name: install latest python version and tox 80 | command: | 81 | LATEST_VERSION=$(pyenv install --list | grep -E "${MINOR_VERSION}\.[0-9]+$" | tail -1) 82 | echo "installing python version $LATEST_VERSION" 83 | pyenv install $LATEST_VERSION 84 | pyenv global $LATEST_VERSION 85 | python3 -m pip install --upgrade pip 86 | python3 -m pip install tox 87 | run-tox-step: &run-tox-step 88 | run: 89 | name: run tox 90 | command: | 91 | echo 'running tox with' $(python3 --version) 92 | python3 -m tox run -r 93 | save-cache-step: &save-cache-step 94 | save_cache: 95 | paths: 96 | - .tox 97 | key: cache-v1-{{ arch }}-{{ .Environment.CIRCLE_JOB }}-{{ checksum "setup.py" }}-{{ checksum "tox.ini" }} 98 | 99 | jobs: 100 | py38-install-geth-v1_14_0: 101 | <<: *common_go_steps 102 | docker: 103 | - image: cimg/python:3.8 104 | environment: 105 | GETH_VERSION: v1.14.0 106 | TOXENV: py38-install-geth-v1_14_0 107 | py39-install-geth-v1_14_0: 108 | <<: *common_go_steps 109 | docker: 110 | - image: cimg/python:3.9 111 | environment: 112 | GETH_VERSION: v1.14.0 113 | TOXENV: py39-install-geth-v1_14_0 114 | py310-install-geth-v1_14_0: 115 | <<: *common_go_steps 116 | docker: 117 | - image: cimg/python:3.10 118 | environment: 119 | GETH_VERSION: v1.14.0 120 | TOXENV: py310-install-geth-v1_14_0 121 | py311-install-geth-v1_14_0: 122 | <<: *common_go_steps 123 | docker: 124 | - image: cimg/python:3.11 125 | environment: 126 | GETH_VERSION: v1.14.0 127 | TOXENV: py311-install-geth-v1_14_0 128 | py312-install-geth-v1_14_0: 129 | <<: *common_go_steps 130 | docker: 131 | - image: cimg/python:3.12 132 | environment: 133 | GETH_VERSION: v1.14.0 134 | TOXENV: py312-install-geth-v1_14_0 135 | py38-install-geth-v1_14_2: 136 | <<: *common_go_steps 137 | docker: 138 | - image: cimg/python:3.8 139 | environment: 140 | GETH_VERSION: v1.14.2 141 | TOXENV: py38-install-geth-v1_14_2 142 | py39-install-geth-v1_14_2: 143 | <<: *common_go_steps 144 | docker: 145 | - image: cimg/python:3.9 146 | environment: 147 | GETH_VERSION: v1.14.2 148 | TOXENV: py39-install-geth-v1_14_2 149 | py310-install-geth-v1_14_2: 150 | <<: *common_go_steps 151 | docker: 152 | - image: cimg/python:3.10 153 | environment: 154 | GETH_VERSION: v1.14.2 155 | TOXENV: py310-install-geth-v1_14_2 156 | py311-install-geth-v1_14_2: 157 | <<: *common_go_steps 158 | docker: 159 | - image: cimg/python:3.11 160 | environment: 161 | GETH_VERSION: v1.14.2 162 | TOXENV: py311-install-geth-v1_14_2 163 | py312-install-geth-v1_14_2: 164 | <<: *common_go_steps 165 | docker: 166 | - image: cimg/python:3.12 167 | environment: 168 | GETH_VERSION: v1.14.2 169 | TOXENV: py312-install-geth-v1_14_2 170 | py38-install-geth-v1_14_3: 171 | <<: *common_go_steps 172 | docker: 173 | - image: cimg/python:3.8 174 | environment: 175 | GETH_VERSION: v1.14.3 176 | TOXENV: py38-install-geth-v1_14_3 177 | py39-install-geth-v1_14_3: 178 | <<: *common_go_steps 179 | docker: 180 | - image: cimg/python:3.9 181 | environment: 182 | GETH_VERSION: v1.14.3 183 | TOXENV: py39-install-geth-v1_14_3 184 | py310-install-geth-v1_14_3: 185 | <<: *common_go_steps 186 | docker: 187 | - image: cimg/python:3.10 188 | environment: 189 | GETH_VERSION: v1.14.3 190 | TOXENV: py310-install-geth-v1_14_3 191 | py311-install-geth-v1_14_3: 192 | <<: *common_go_steps 193 | docker: 194 | - image: cimg/python:3.11 195 | environment: 196 | GETH_VERSION: v1.14.3 197 | TOXENV: py311-install-geth-v1_14_3 198 | py312-install-geth-v1_14_3: 199 | <<: *common_go_steps 200 | docker: 201 | - image: cimg/python:3.12 202 | environment: 203 | GETH_VERSION: v1.14.3 204 | TOXENV: py312-install-geth-v1_14_3 205 | py38-install-geth-v1_14_4: 206 | <<: *common_go_steps 207 | docker: 208 | - image: cimg/python:3.8 209 | environment: 210 | GETH_VERSION: v1.14.4 211 | TOXENV: py38-install-geth-v1_14_4 212 | py39-install-geth-v1_14_4: 213 | <<: *common_go_steps 214 | docker: 215 | - image: cimg/python:3.9 216 | environment: 217 | GETH_VERSION: v1.14.4 218 | TOXENV: py39-install-geth-v1_14_4 219 | py310-install-geth-v1_14_4: 220 | <<: *common_go_steps 221 | docker: 222 | - image: cimg/python:3.10 223 | environment: 224 | GETH_VERSION: v1.14.4 225 | TOXENV: py310-install-geth-v1_14_4 226 | py311-install-geth-v1_14_4: 227 | <<: *common_go_steps 228 | docker: 229 | - image: cimg/python:3.11 230 | environment: 231 | GETH_VERSION: v1.14.4 232 | TOXENV: py311-install-geth-v1_14_4 233 | py312-install-geth-v1_14_4: 234 | <<: *common_go_steps 235 | docker: 236 | - image: cimg/python:3.12 237 | environment: 238 | GETH_VERSION: v1.14.4 239 | TOXENV: py312-install-geth-v1_14_4 240 | py38-install-geth-v1_14_5: 241 | <<: *common_go_steps 242 | docker: 243 | - image: cimg/python:3.8 244 | environment: 245 | GETH_VERSION: v1.14.5 246 | TOXENV: py38-install-geth-v1_14_5 247 | py39-install-geth-v1_14_5: 248 | <<: *common_go_steps 249 | docker: 250 | - image: cimg/python:3.9 251 | environment: 252 | GETH_VERSION: v1.14.5 253 | TOXENV: py39-install-geth-v1_14_5 254 | py310-install-geth-v1_14_5: 255 | <<: *common_go_steps 256 | docker: 257 | - image: cimg/python:3.10 258 | environment: 259 | GETH_VERSION: v1.14.5 260 | TOXENV: py310-install-geth-v1_14_5 261 | py311-install-geth-v1_14_5: 262 | <<: *common_go_steps 263 | docker: 264 | - image: cimg/python:3.11 265 | environment: 266 | GETH_VERSION: v1.14.5 267 | TOXENV: py311-install-geth-v1_14_5 268 | py312-install-geth-v1_14_5: 269 | <<: *common_go_steps 270 | docker: 271 | - image: cimg/python:3.12 272 | environment: 273 | GETH_VERSION: v1.14.5 274 | TOXENV: py312-install-geth-v1_14_5 275 | py38-install-geth-v1_14_6: 276 | <<: *common_go_steps 277 | docker: 278 | - image: cimg/python:3.8 279 | environment: 280 | GETH_VERSION: v1.14.6 281 | TOXENV: py38-install-geth-v1_14_6 282 | py39-install-geth-v1_14_6: 283 | <<: *common_go_steps 284 | docker: 285 | - image: cimg/python:3.9 286 | environment: 287 | GETH_VERSION: v1.14.6 288 | TOXENV: py39-install-geth-v1_14_6 289 | py310-install-geth-v1_14_6: 290 | <<: *common_go_steps 291 | docker: 292 | - image: cimg/python:3.10 293 | environment: 294 | GETH_VERSION: v1.14.6 295 | TOXENV: py310-install-geth-v1_14_6 296 | py311-install-geth-v1_14_6: 297 | <<: *common_go_steps 298 | docker: 299 | - image: cimg/python:3.11 300 | environment: 301 | GETH_VERSION: v1.14.6 302 | TOXENV: py311-install-geth-v1_14_6 303 | py312-install-geth-v1_14_6: 304 | <<: *common_go_steps 305 | docker: 306 | - image: cimg/python:3.12 307 | environment: 308 | GETH_VERSION: v1.14.6 309 | TOXENV: py312-install-geth-v1_14_6 310 | py38-install-geth-v1_14_7: 311 | <<: *common_go_steps 312 | docker: 313 | - image: cimg/python:3.8 314 | environment: 315 | GETH_VERSION: v1.14.7 316 | TOXENV: py38-install-geth-v1_14_7 317 | py39-install-geth-v1_14_7: 318 | <<: *common_go_steps 319 | docker: 320 | - image: cimg/python:3.9 321 | environment: 322 | GETH_VERSION: v1.14.7 323 | TOXENV: py39-install-geth-v1_14_7 324 | py310-install-geth-v1_14_7: 325 | <<: *common_go_steps 326 | docker: 327 | - image: cimg/python:3.10 328 | environment: 329 | GETH_VERSION: v1.14.7 330 | TOXENV: py310-install-geth-v1_14_7 331 | py311-install-geth-v1_14_7: 332 | <<: *common_go_steps 333 | docker: 334 | - image: cimg/python:3.11 335 | environment: 336 | GETH_VERSION: v1.14.7 337 | TOXENV: py311-install-geth-v1_14_7 338 | py312-install-geth-v1_14_7: 339 | <<: *common_go_steps 340 | docker: 341 | - image: cimg/python:3.12 342 | environment: 343 | GETH_VERSION: v1.14.7 344 | TOXENV: py312-install-geth-v1_14_7 345 | py38-install-geth-v1_14_8: 346 | <<: *common_go_steps 347 | docker: 348 | - image: cimg/python:3.8 349 | environment: 350 | GETH_VERSION: v1.14.8 351 | TOXENV: py38-install-geth-v1_14_8 352 | py39-install-geth-v1_14_8: 353 | <<: *common_go_steps 354 | docker: 355 | - image: cimg/python:3.9 356 | environment: 357 | GETH_VERSION: v1.14.8 358 | TOXENV: py39-install-geth-v1_14_8 359 | py310-install-geth-v1_14_8: 360 | <<: *common_go_steps 361 | docker: 362 | - image: cimg/python:3.10 363 | environment: 364 | GETH_VERSION: v1.14.8 365 | TOXENV: py310-install-geth-v1_14_8 366 | py311-install-geth-v1_14_8: 367 | <<: *common_go_steps 368 | docker: 369 | - image: cimg/python:3.11 370 | environment: 371 | GETH_VERSION: v1.14.8 372 | TOXENV: py311-install-geth-v1_14_8 373 | py312-install-geth-v1_14_8: 374 | <<: *common_go_steps 375 | docker: 376 | - image: cimg/python:3.12 377 | environment: 378 | GETH_VERSION: v1.14.8 379 | TOXENV: py312-install-geth-v1_14_8 380 | py38-install-geth-v1_14_9: 381 | <<: *common_go_steps 382 | docker: 383 | - image: cimg/python:3.8 384 | environment: 385 | GETH_VERSION: v1.14.9 386 | TOXENV: py38-install-geth-v1_14_9 387 | py39-install-geth-v1_14_9: 388 | <<: *common_go_steps 389 | docker: 390 | - image: cimg/python:3.9 391 | environment: 392 | GETH_VERSION: v1.14.9 393 | TOXENV: py39-install-geth-v1_14_9 394 | py310-install-geth-v1_14_9: 395 | <<: *common_go_steps 396 | docker: 397 | - image: cimg/python:3.10 398 | environment: 399 | GETH_VERSION: v1.14.9 400 | TOXENV: py310-install-geth-v1_14_9 401 | py311-install-geth-v1_14_9: 402 | <<: *common_go_steps 403 | docker: 404 | - image: cimg/python:3.11 405 | environment: 406 | GETH_VERSION: v1.14.9 407 | TOXENV: py311-install-geth-v1_14_9 408 | py312-install-geth-v1_14_9: 409 | <<: *common_go_steps 410 | docker: 411 | - image: cimg/python:3.12 412 | environment: 413 | GETH_VERSION: v1.14.9 414 | TOXENV: py312-install-geth-v1_14_9 415 | py38-install-geth-v1_14_10: 416 | <<: *common_go_steps 417 | docker: 418 | - image: cimg/python:3.8 419 | environment: 420 | GETH_VERSION: v1.14.10 421 | TOXENV: py38-install-geth-v1_14_10 422 | py39-install-geth-v1_14_10: 423 | <<: *common_go_steps 424 | docker: 425 | - image: cimg/python:3.9 426 | environment: 427 | GETH_VERSION: v1.14.10 428 | TOXENV: py39-install-geth-v1_14_10 429 | py310-install-geth-v1_14_10: 430 | <<: *common_go_steps 431 | docker: 432 | - image: cimg/python:3.10 433 | environment: 434 | GETH_VERSION: v1.14.10 435 | TOXENV: py310-install-geth-v1_14_10 436 | py311-install-geth-v1_14_10: 437 | <<: *common_go_steps 438 | docker: 439 | - image: cimg/python:3.11 440 | environment: 441 | GETH_VERSION: v1.14.10 442 | TOXENV: py311-install-geth-v1_14_10 443 | py312-install-geth-v1_14_10: 444 | <<: *common_go_steps 445 | docker: 446 | - image: cimg/python:3.12 447 | environment: 448 | GETH_VERSION: v1.14.10 449 | TOXENV: py312-install-geth-v1_14_10 450 | py38-install-geth-v1_14_11: 451 | <<: *common_go_steps 452 | docker: 453 | - image: cimg/python:3.8 454 | environment: 455 | GETH_VERSION: v1.14.11 456 | TOXENV: py38-install-geth-v1_14_11 457 | py39-install-geth-v1_14_11: 458 | <<: *common_go_steps 459 | docker: 460 | - image: cimg/python:3.9 461 | environment: 462 | GETH_VERSION: v1.14.11 463 | TOXENV: py39-install-geth-v1_14_11 464 | py310-install-geth-v1_14_11: 465 | <<: *common_go_steps 466 | docker: 467 | - image: cimg/python:3.10 468 | environment: 469 | GETH_VERSION: v1.14.11 470 | TOXENV: py310-install-geth-v1_14_11 471 | py311-install-geth-v1_14_11: 472 | <<: *common_go_steps 473 | docker: 474 | - image: cimg/python:3.11 475 | environment: 476 | GETH_VERSION: v1.14.11 477 | TOXENV: py311-install-geth-v1_14_11 478 | py312-install-geth-v1_14_11: 479 | <<: *common_go_steps 480 | docker: 481 | - image: cimg/python:3.12 482 | environment: 483 | GETH_VERSION: v1.14.11 484 | TOXENV: py312-install-geth-v1_14_11 485 | py38-install-geth-v1_14_12: 486 | <<: *common_go_steps 487 | docker: 488 | - image: cimg/python:3.8 489 | environment: 490 | GETH_VERSION: v1.14.12 491 | TOXENV: py38-install-geth-v1_14_12 492 | py39-install-geth-v1_14_12: 493 | <<: *common_go_steps 494 | docker: 495 | - image: cimg/python:3.9 496 | environment: 497 | GETH_VERSION: v1.14.12 498 | TOXENV: py39-install-geth-v1_14_12 499 | py310-install-geth-v1_14_12: 500 | <<: *common_go_steps 501 | docker: 502 | - image: cimg/python:3.10 503 | environment: 504 | GETH_VERSION: v1.14.12 505 | TOXENV: py310-install-geth-v1_14_12 506 | py311-install-geth-v1_14_12: 507 | <<: *common_go_steps 508 | docker: 509 | - image: cimg/python:3.11 510 | environment: 511 | GETH_VERSION: v1.14.12 512 | TOXENV: py311-install-geth-v1_14_12 513 | py312-install-geth-v1_14_12: 514 | <<: *common_go_steps 515 | docker: 516 | - image: cimg/python:3.12 517 | environment: 518 | GETH_VERSION: v1.14.12 519 | TOXENV: py312-install-geth-v1_14_12 520 | 521 | py38-lint: 522 | <<: *common_go_steps 523 | docker: 524 | - image: cimg/python:3.8 525 | environment: 526 | TOXENV: py38-lint 527 | py39-lint: 528 | <<: *common_go_steps 529 | docker: 530 | - image: cimg/python:3.9 531 | environment: 532 | TOXENV: py39-lint 533 | py310-lint: 534 | <<: *common_go_steps 535 | docker: 536 | - image: cimg/python:3.10 537 | environment: 538 | TOXENV: py310-lint 539 | py311-lint: 540 | <<: *common_go_steps 541 | docker: 542 | - image: cimg/python:3.11 543 | environment: 544 | TOXENV: py311-lint 545 | py312-lint: 546 | <<: *common_go_steps 547 | docker: 548 | - image: cimg/python:3.12 549 | environment: 550 | TOXENV: py312-lint 551 | 552 | py38-wheel: 553 | <<: *common_go_steps 554 | docker: 555 | - image: cimg/python:3.8 556 | environment: 557 | TOXENV: py38-wheel 558 | py39-wheel: 559 | <<: *common_go_steps 560 | docker: 561 | - image: cimg/python:3.9 562 | environment: 563 | TOXENV: py39-wheel 564 | py310-wheel: 565 | <<: *common_go_steps 566 | docker: 567 | - image: cimg/python:3.10 568 | environment: 569 | TOXENV: py310-wheel 570 | py311-wheel: 571 | <<: *common_go_steps 572 | docker: 573 | - image: cimg/python:3.11 574 | environment: 575 | TOXENV: py311-wheel 576 | py312-wheel: 577 | <<: *common_go_steps 578 | docker: 579 | - image: cimg/python:3.12 580 | environment: 581 | TOXENV: py312-wheel 582 | 583 | py311-windows-wheel: 584 | <<: *windows-wheel-setup 585 | steps: 586 | - checkout 587 | - <<: *restore-cache-step 588 | - <<: *install-pyenv-step 589 | - run: 590 | name: set minor version 591 | command: echo "export MINOR_VERSION='3.11'" >> $BASH_ENV 592 | - <<: *install-latest-python-step 593 | - <<: *run-tox-step 594 | - <<: *save-cache-step 595 | 596 | py312-windows-wheel: 597 | <<: *windows-wheel-setup 598 | steps: 599 | - checkout 600 | - <<: *restore-cache-step 601 | - <<: *install-pyenv-step 602 | - run: 603 | name: set minor version 604 | command: echo "export MINOR_VERSION='3.12'" >> $BASH_ENV 605 | - <<: *install-latest-python-step 606 | - <<: *run-tox-step 607 | - <<: *save-cache-step 608 | 609 | workflows: 610 | version: 2 611 | test: 612 | jobs: 613 | - py38-install-geth-v1_14_0 614 | - py39-install-geth-v1_14_0 615 | - py310-install-geth-v1_14_0 616 | - py311-install-geth-v1_14_0 617 | - py312-install-geth-v1_14_0 618 | 619 | - py38-install-geth-v1_14_2 620 | - py39-install-geth-v1_14_2 621 | - py310-install-geth-v1_14_2 622 | - py311-install-geth-v1_14_2 623 | - py312-install-geth-v1_14_2 624 | 625 | - py38-install-geth-v1_14_3 626 | - py39-install-geth-v1_14_3 627 | - py310-install-geth-v1_14_3 628 | - py311-install-geth-v1_14_3 629 | - py312-install-geth-v1_14_3 630 | 631 | - py38-install-geth-v1_14_4 632 | - py39-install-geth-v1_14_4 633 | - py310-install-geth-v1_14_4 634 | - py311-install-geth-v1_14_4 635 | - py312-install-geth-v1_14_4 636 | 637 | - py38-install-geth-v1_14_5 638 | - py39-install-geth-v1_14_5 639 | - py310-install-geth-v1_14_5 640 | - py311-install-geth-v1_14_5 641 | - py312-install-geth-v1_14_5 642 | 643 | - py38-install-geth-v1_14_6 644 | - py39-install-geth-v1_14_6 645 | - py310-install-geth-v1_14_6 646 | - py311-install-geth-v1_14_6 647 | - py312-install-geth-v1_14_6 648 | 649 | - py38-install-geth-v1_14_7 650 | - py39-install-geth-v1_14_7 651 | - py310-install-geth-v1_14_7 652 | - py311-install-geth-v1_14_7 653 | - py312-install-geth-v1_14_7 654 | 655 | - py38-install-geth-v1_14_8 656 | - py39-install-geth-v1_14_8 657 | - py310-install-geth-v1_14_8 658 | - py311-install-geth-v1_14_8 659 | - py312-install-geth-v1_14_8 660 | 661 | - py38-install-geth-v1_14_9 662 | - py39-install-geth-v1_14_9 663 | - py310-install-geth-v1_14_9 664 | - py311-install-geth-v1_14_9 665 | - py312-install-geth-v1_14_9 666 | 667 | - py38-install-geth-v1_14_10 668 | - py39-install-geth-v1_14_10 669 | - py310-install-geth-v1_14_10 670 | - py311-install-geth-v1_14_10 671 | - py312-install-geth-v1_14_10 672 | 673 | - py38-install-geth-v1_14_11 674 | - py39-install-geth-v1_14_11 675 | - py310-install-geth-v1_14_11 676 | - py311-install-geth-v1_14_11 677 | - py312-install-geth-v1_14_11 678 | 679 | - py38-install-geth-v1_14_12 680 | - py39-install-geth-v1_14_12 681 | - py310-install-geth-v1_14_12 682 | - py311-install-geth-v1_14_12 683 | - py312-install-geth-v1_14_12 684 | 685 | - py38-lint 686 | - py39-lint 687 | - py310-lint 688 | - py311-lint 689 | - py312-lint 690 | 691 | - py38-wheel 692 | - py39-wheel 693 | - py310-wheel 694 | - py311-wheel 695 | - py312-wheel 696 | - py311-windows-wheel 697 | - py312-windows-wheel 698 | -------------------------------------------------------------------------------- /.circleci/install_geth.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | python --version 4 | echo $GETH_VERSION 5 | export GETH_BASE_INSTALL_PATH=~/repo/install/ 6 | mkdir -p $HOME/.ethash 7 | if [ -n "$GETH_VERSION" ]; then python -m geth.install $GETH_VERSION; fi 8 | if [ -n "$GETH_VERSION" ]; then export GETH_BINARY="$GETH_BASE_INSTALL_PATH/geth-$GETH_VERSION/bin/geth"; fi 9 | if [ -n "$GETH_VERSION" ]; then $GETH_BINARY version; fi 10 | 11 | # Modifying the path is tough with tox, hence copying the executable 12 | # to a known directory which is included in $PATH 13 | cp $GETH_BINARY $HOME/.local/bin 14 | -------------------------------------------------------------------------------- /.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 | # Covers JetBrains IDEs: IntelliJ, RubyMine, PhpStorm, AppCode, PyCharm, CLion, Android Studio and Webstorm 88 | # For a more precise, explicit template, see: 89 | # https://intellij-support.jetbrains.com/hc/en-us/articles/206544839 90 | 91 | ## General 92 | .idea/* 93 | .idea_modules/* 94 | 95 | ## File-based project format: 96 | *.iws 97 | 98 | ## IntelliJ 99 | out/ 100 | 101 | ## Plugin-specific files: 102 | 103 | ### JIRA plugin 104 | atlassian-ide-plugin.xml 105 | 106 | ### Crashlytics plugin (for Android Studio and IntelliJ) 107 | com_crashlytics_export_strings.xml 108 | crashlytics.properties 109 | crashlytics-build.properties 110 | fabric.properties 111 | 112 | # END JetBrains section 113 | -------------------------------------------------------------------------------- /.pre-commit-config.yaml: -------------------------------------------------------------------------------- 1 | exclude: '.project-template|tests/core/accounts/projects/|.bumpversion.cfg' 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 | -------------------------------------------------------------------------------- /.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.1.0 (2024-11-20) 2 | --------------------------- 3 | 4 | Bugfixes 5 | ~~~~~~~~ 6 | 7 | - ``ipc_path`` property should always return a string (`#239 `__) 8 | 9 | 10 | Features 11 | ~~~~~~~~ 12 | 13 | - Add support for new geth ``v1.14.9``. (`#234 `__) 14 | - Add support for Geth 1.14.12. (`#241 `__) 15 | 16 | 17 | py-geth v5.0.0 (2024-08-14) 18 | --------------------------- 19 | 20 | Breaking Changes 21 | ~~~~~~~~~~~~~~~~ 22 | 23 | - Replace ``subprocess+wget`` with ``requests`` to retrieve geth binaries (`#228 `__) 24 | 25 | 26 | Features 27 | ~~~~~~~~ 28 | 29 | - Add support for geth ``v1.14.8`` (`#231 `__) 30 | 31 | 32 | py-geth v5.0.0-beta.3 (2024-07-11) 33 | ---------------------------------- 34 | 35 | Features 36 | ~~~~~~~~ 37 | 38 | - Add support for geth ``v1.14.6``. (`#224 `__) 39 | - Add ``tx_pool_lifetime`` flag option (`#225 `__) 40 | - Add support for geth ``v1.14.7`` (`#227 `__) 41 | 42 | 43 | py-geth v5.0.0-beta.2 (2024-06-28) 44 | ---------------------------------- 45 | 46 | Bugfixes 47 | ~~~~~~~~ 48 | 49 | - Add missing fields for genesis data. Change mixhash -> mixHash to more closely match Geth (`#221 `__) 50 | 51 | 52 | py-geth v5.0.0-beta.1 (2024-06-19) 53 | ---------------------------------- 54 | 55 | Breaking Changes 56 | ~~~~~~~~~~~~~~~~ 57 | 58 | - Return type of functions in ``accounts.py`` changed from ``bytes`` to ``str`` (`#199 `__) 59 | - Changed return type of ``get_geth_version_info_string`` from ``bytes`` to ``str`` (`#204 `__) 60 | - 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 `__) 61 | - 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 `__) 62 | 63 | 64 | Bugfixes 65 | ~~~~~~~~ 66 | 67 | - Remove duplicates from dev mode account parsing for ``get_accounts()``. (`#219 `__) 68 | 69 | 70 | Improved Documentation 71 | ~~~~~~~~~~~~~~~~~~~~~~ 72 | 73 | - Update documentation for ``DevGethProcess`` transition to using ``geth --dev``. (`#200 `__) 74 | 75 | 76 | Features 77 | ~~~~~~~~ 78 | 79 | - Add support for newly released geth version ``v1.13.15``. (`#193 `__) 80 | - Add support for geth ``v1.14.0`` - ``v1.14.3``, with the exception for the missing geth ``v1.14.1`` release. (`#195 `__) 81 | - Add support for geth versions ``v1.14.4`` and ``v1.14.5``. (`#206 `__) 82 | - Update all raised ``Exceptions`` to inherit from a ``PyGethException`` (`#212 `__) 83 | 84 | 85 | Internal Changes - for py-geth Contributors 86 | ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ 87 | 88 | - Adding basic type hints across the lib (`#196 `__) 89 | - Use a pydantic model to validate typing of ``geth_kwargs`` when passed as an argument (`#199 `__) 90 | - Change args for ``construct_popen_command`` from indivdual kwargs to geth_kwargs and validate with GethKwargs model (`#205 `__) 91 | - Use the latest golang version ``v1.22.4`` when running CircleCI jobs. (`#206 `__) 92 | - Refactor ``data_dir`` property of ``BaseGethProcess`` and derived classes to fix typing (`#208 `__) 93 | - Run ``mypy`` locally with all dev deps installed, instead of using the pre-commit ``mirrors-mypy`` hook (`#210 `__) 94 | - Add ``fill_default_genesis_data`` function to properly fill ``genesis_data`` defaults (`#215 `__) 95 | 96 | 97 | Removals 98 | ~~~~~~~~ 99 | 100 | - Remove support for geth < ``v1.13.0``. (`#195 `__) 101 | - Remove deprecated ``ipc_api`` and ``miner_threads`` geth cli flags (`#202 `__) 102 | - Removed deprecated ``LiveGethProcess``, use ``MainnetGethProcess`` instead (`#203 `__) 103 | - Remove handling of ``--ssh`` geth kwarg (`#205 `__) 104 | - 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 `__) 105 | 106 | 107 | py-geth v4.4.0 (2024-03-27) 108 | --------------------------- 109 | 110 | Features 111 | ~~~~~~~~ 112 | 113 | - Add support for geth ``v1.13.12 and v1.13.13`` (`#188 `__) 114 | - Add support for ``geth v1.13.14`` (`#189 `__) 115 | 116 | 117 | Internal Changes - for py-geth Contributors 118 | ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ 119 | 120 | - Merge template updates, noteably add python 3.12 support (`#186 `__) 121 | 122 | 123 | py-geth v4.3.0 (2024-02-12) 124 | --------------------------- 125 | 126 | Features 127 | ~~~~~~~~ 128 | 129 | - Add support for geth ``v1.13.11`` (`#182 `__) 130 | 131 | 132 | py-geth v4.2.0 (2024-01-23) 133 | --------------------------- 134 | 135 | Features 136 | ~~~~~~~~ 137 | 138 | - Add support for geth ``v1.13.10`` (`#179 `__) 139 | 140 | 141 | py-geth v4.1.0 (2024-01-10) 142 | --------------------------- 143 | 144 | Bugfixes 145 | ~~~~~~~~ 146 | 147 | - Fix issue where could not set custom extraData in chain genesis (`#167 `__) 148 | 149 | 150 | Features 151 | ~~~~~~~~ 152 | 153 | - Add support for geth ``1.13.5`` (`#165 `__) 154 | - Allow clique consensus parameters period and epoch in chain genesis (`#169 `__) 155 | - Add support for geth ``v1.13.6`` and ``v1.13.7`` (`#173 `__) 156 | - Add support for geth ``v1.13.8`` (`#175 `__) 157 | - Added support for ``geth v1.13.9`` (`#176 `__) 158 | 159 | 160 | Internal Changes - for py-geth Contributors 161 | ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ 162 | 163 | - Change the name of ``master`` branch to ``main`` (`#166 `__) 164 | 165 | 166 | py-geth v4.0.0 (2023-10-30) 167 | --------------------------- 168 | 169 | Breaking Changes 170 | ~~~~~~~~~~~~~~~~ 171 | 172 | - 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 `__) 173 | 174 | 175 | Features 176 | ~~~~~~~~ 177 | 178 | - Add support for geth ``1.12.0`` and ``1.12.1`` (`#151 `__) 179 | - Add support for geth versions v1.12.2 to v1.13.4 (`#160 `__) 180 | 181 | 182 | Internal Changes - for py-geth Contributors 183 | ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ 184 | 185 | - Use golang version ``1.21.3`` for CI builds to ensure compatibility with the latest version. (`#160 `__) 186 | - Merge template updates, including using pre-commit for linting and drop ``pkg_resources`` for version info (`#162 `__) 187 | 188 | 189 | Miscellaneous Changes 190 | ~~~~~~~~~~~~~~~~~~~~~ 191 | 192 | - `#152 `__ 193 | 194 | 195 | py-geth v3.13.0 (2023-06-07) 196 | ---------------------------- 197 | 198 | Features 199 | ~~~~~~~~ 200 | 201 | - Allow initializing `BaseGethProcess` with `stdin`, `stdout`, and `stderr` (`#139 `__) 202 | - Add support for geth `1.11.6` (`#141 `__) 203 | 204 | 205 | Internal Changes - for py-geth Contributors 206 | ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ 207 | 208 | - Update `tox` and the way it is installed for CircleCI runs (`#141 `__) 209 | - merge in python project template (`#142 `__) 210 | - Changed `.format` strings to f-strings, removed other python2 code (`#146 `__) 211 | 212 | 213 | Removals 214 | ~~~~~~~~ 215 | 216 | - Remove `miner.thread` default since no longer supported (`#144 `__) 217 | 218 | 219 | 3.12.0 220 | ------ 221 | 222 | - Add support for geth `1.11.3`, `1.11.4`, and `1.11.5` 223 | - Add `miner_etherbase` to supported geth kwargs 224 | 225 | 3.11.0 226 | ------ 227 | 228 | - Upgrade circleci golang version to `1.20.1` 229 | - Add support for python `3.11` 230 | - Add support for geth `1.10.26`, `1.11.0`, `1.11.1`, and `1.11.2` 231 | - Fix incorrect comment in `install_geth.sh` 232 | - Add `clique` to `ALL_APIS` 233 | - Add `gcmode` option to Geth process wrapper 234 | 235 | 3.10.0 236 | ------ 237 | 238 | - Add support for geth `1.10.24`-`1.10.25` 239 | - Patch CVE-2007-4559 - directory traversal vulnerability 240 | 241 | 3.9.1 242 | ----- 243 | 244 | - Add support for geth `1.10.18`-`1.10.23` 245 | - Remove support for geth versions `1.9.X` 246 | - Upgrade CI Go version to `1.18.1` 247 | - Some updates to `setup.py`, `tox.ini`, and circleci `config.yml` 248 | - Update supported python versions to reflect what is being tested 249 | - Add python 3.10 support 250 | - Remove dependency on `idna` 251 | - Remove deprecated `setuptools-markdown` 252 | - Updates to `pytest`, `tox`, `setuptools`, `flake8`, and `pluggy` dependencies 253 | - Spelling fix in `create_new_account` docstring 254 | 255 | 3.8.0 256 | ----- 257 | 258 | - Add support for geth 1.10.14-1.10.17 259 | 260 | 3.7.0 261 | ----- 262 | 263 | - Remove extraneous logging formatting from the LoggingMixin 264 | - Add support for geth 1.10.12-1.10.13 265 | 266 | 3.6.0 267 | ----- 268 | 269 | - Add support for geth 1.10.9-1.10.11 270 | - Add support for python 3.9 271 | - Update flake8 requirement to 3.9.2 272 | - Add script to update geth versions 273 | - Set upgrade block numbers in default config 274 | - Allow passing a port by both string and integer to overrides 275 | - Add --preload flag option 276 | - Add --cache flag option 277 | - Add --tx_pool_global_slots flag option 278 | - Add --tx_pool_price_limit flag option 279 | - Handle StopIteration in JoinableQueues when using LoggingMixin 280 | - General code cleanup 281 | 282 | 3.5.0 283 | ----- 284 | 285 | - Add support for geth 1.10.7-1.10.8 286 | 287 | 3.4.0 288 | ----- 289 | 290 | - Add support for geth 1.10.6 291 | 292 | 3.3.0 293 | ----- 294 | 295 | - Add support for geth 1.10.5 296 | 297 | 3.2.0 298 | ----- 299 | 300 | - Add support for geth 1.10.4 301 | 302 | 3.1.0 303 | ----- 304 | 305 | - Add support for geth 1.10.2-1.10.3 306 | 307 | 3.0.0 308 | ----- 309 | 310 | - Add support for geth 1.9.20-1.10.0 311 | - Remove support for geth <= 1.9.14 312 | 313 | 2.4.0 314 | ----- 315 | 316 | - Add support for geth 1.9.13-1.9.19 317 | 318 | 2.3.0 319 | ----- 320 | 321 | - Add support for geth 1.9.8-1.9.12 322 | 323 | 2.2.0 324 | ----- 325 | 326 | - Add support for geth 1.9.x 327 | - Readme bugfix for pypi badges 328 | 329 | 2.1.0 330 | ----- 331 | 332 | - remove support for python 2.x 333 | - Geth versions `<1.7` are no longer tested in CI 334 | - Support for geth versions up to `geth==1.8.22` 335 | - Support for python 3.6 and 3.7 336 | 337 | 1.10.2 338 | ------ 339 | 340 | - Support for testing and installation of `geth==1.7.2` 341 | 342 | 1.10.1 343 | ------ 344 | 345 | - Support for testing and installation of `geth==1.7.0` 346 | 347 | 1.10.0 348 | ------ 349 | 350 | - Support and testing against `geth==1.6.1` 351 | - Support and testing against `geth==1.6.2` 352 | - Support and testing against `geth==1.6.3` 353 | - Support and testing against `geth==1.6.4` 354 | - Support and testing against `geth==1.6.5` 355 | - Support and testing against `geth==1.6.6` 356 | - Support and testing against `geth==1.6.7` 357 | 358 | 1.9.0 359 | ----- 360 | 361 | - Rename `LiveGethProcess` to `MainnetGethProcess`. `LiveGethProcess` now raises deprecation warning when instantiated. 362 | - Implement `geth` installation scripts and API 363 | - Expand test suite to cover through `geth==1.6.6` 364 | 365 | 1.8.0 366 | ----- 367 | 368 | - Bugfix for `--ipcapi` flag removal in geth 1.6.x 369 | 370 | 1.7.1 371 | ----- 372 | 373 | - Bugfix for `ensure_path_exists` utility function. 374 | 375 | 1.7.0 376 | ----- 377 | 378 | - Change to use `compat` instead of `async` since async is a keyword 379 | - Change env variable for gevent threading to be `GETH_THREADING_BACKEND` 380 | 381 | 1.6.0 382 | ----- 383 | 384 | - Remove hard dependency on gevent. 385 | - Expand testing against 1.5.5 and 1.5.6 386 | 387 | 1.5.0 388 | ----- 389 | 390 | - Deprecate the `--testnet` based chain. 391 | - TestnetGethProcess now is an alias for whatever the current primary testnet is 392 | - RopstenGethProcess now represents the current ropsten test network 393 | - travis-ci geth version pinning. 394 | 395 | 1.4.1 396 | ----- 397 | 398 | - Add `rpc_cors_domain` to supported arguments for running geth instances. 399 | 400 | 1.4.0 401 | ----- 402 | 403 | - Add `shh` flag to wrapper to allow enabling of whisper in geth processes. 404 | 405 | 1.3.0 406 | ----- 407 | 408 | - Bugfix for python3 when no contracts are found. 409 | - Allow genesis configuration through constructor of GethProcess classes. 410 | 411 | 1.2.0 412 | ----- 413 | 414 | - Add gevent monkeypatch for socket when using requests and urllib. 415 | 416 | 1.1.0 417 | ----- 418 | 419 | - Fix websocket addition 420 | 421 | 1.0.0 422 | ----- 423 | 424 | - Add Websocket interface to default list of interfaces that are presented by 425 | geth. 426 | 427 | 0.9.0 428 | ----- 429 | 430 | - Fix broken LiveGethProcess and TestnetGethProcess classes. 431 | - Let DevGethProcesses use a local geth.ipc if the path is short enough. 432 | 433 | 0.8.0 434 | ----- 435 | 436 | - Add `homesteadBlock`, `daoForkBlock`, and `doaForkSupport` to the genesis 437 | config that is written for test chains. 438 | 439 | 0.7.0 440 | ----- 441 | 442 | - Rename python module from `pygeth` to `geth` 443 | 444 | 0.6.0 445 | ----- 446 | 447 | - Add `is_rpc_ready` and `wait_for_rpc` api. 448 | - Add `is_ipc_ready` and `wait_for_ipc` api. 449 | - Add `is_dag_generated` and `wait_for_dag` api. 450 | - Refactor `LoggingMixin` core logic into base `InterceptedStreamsMixin` 451 | 452 | 453 | 0.5.0 454 | ----- 455 | 456 | - Fix deprecated usage of `--genesis` 457 | 458 | 459 | 0.4.0 460 | ----- 461 | 462 | - Fix broken loggin mixin (again) 463 | 464 | 465 | 0.3.0 466 | ----- 467 | 468 | - Fix broken loggin mixin. 469 | 470 | 471 | 0.2.0 472 | ----- 473 | 474 | - Add logging mixins 475 | 476 | 477 | 0.1.0 478 | ----- 479 | 480 | - Initial Release 481 | -------------------------------------------------------------------------------- /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-2023 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 tests * 5 | 6 | global-include *.pyi 7 | 8 | recursive-exclude * __pycache__ 9 | recursive-exclude * *.py[co] 10 | prune .tox 11 | prune venv* 12 | 13 | include geth/default_blockchain_password 14 | include geth/genesis.json 15 | include geth/py.typed 16 | -------------------------------------------------------------------------------- /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 "lint - fix linting issues with pre-commit" 10 | @echo "test - run tests quickly with the default Python" 11 | @echo "docs - view draft of newsfragments to be added to CHANGELOG" 12 | @echo "notes - consume towncrier newsfragments/ and update CHANGELOG" 13 | @echo "release - package and upload a release (does not run notes target)" 14 | @echo "dist - package" 15 | 16 | clean: clean-build clean-pyc 17 | 18 | clean-build: 19 | rm -fr build/ 20 | rm -fr dist/ 21 | 22 | clean-pyc: 23 | find . -name '*.pyc' -exec rm -f {} + 24 | find . -name '*.pyo' -exec rm -f {} + 25 | find . -name '*~' -exec rm -f {} + 26 | find . -name '__pycache__' -exec rm -rf {} + 27 | 28 | lint: 29 | @pre-commit run --all-files --show-diff-on-failure || ( \ 30 | echo "\n\n\n * pre-commit should have fixed the errors above. Running again to make sure everything is good..." \ 31 | && pre-commit run --all-files --show-diff-on-failure \ 32 | ) 33 | 34 | test: 35 | pytest tests 36 | 37 | docs: 38 | python ./newsfragments/validate_files.py 39 | towncrier build --draft --version preview 40 | 41 | check-bump: 42 | ifndef bump 43 | $(error bump must be set, typically: major, minor, patch, or devnum) 44 | endif 45 | 46 | notes: check-bump 47 | # Let UPCOMING_VERSION be the version that is used for the current bump 48 | $(eval UPCOMING_VERSION=$(shell bumpversion $(bump) --dry-run --list | grep new_version= | sed 's/new_version=//g')) 49 | # Now generate the release notes to have them included in the release commit 50 | towncrier build --yes --version $(UPCOMING_VERSION) 51 | # Before we bump the version, make sure that the towncrier-generated docs will build 52 | make docs 53 | git commit -m "Compile release notes for v$(UPCOMING_VERSION)" 54 | 55 | release: check-bump clean 56 | # require that upstream is configured for ethereum/py-geth 57 | @git remote -v | grep -E "upstream\tgit@github.com:ethereum/py-geth.git \(push\)|upstream\thttps://(www.)?github.com/ethereum/py-geth \(push\)" 58 | # verify that docs build correctly 59 | ./newsfragments/validate_files.py is-empty 60 | make docs 61 | CURRENT_SIGN_SETTING=$(git config commit.gpgSign) 62 | git config commit.gpgSign true 63 | bumpversion $(bump) 64 | git push upstream && git push upstream --tags 65 | python -m build 66 | twine upload dist/* 67 | git config commit.gpgSign "$(CURRENT_SIGN_SETTING)" 68 | 69 | dist: clean 70 | python -m build 71 | ls -l dist 72 | -------------------------------------------------------------------------------- /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 | Installation 19 | 20 | ```bash 21 | python -m pip install py-geth 22 | ``` 23 | 24 | ## Quickstart 25 | 26 | To run geth connected to the mainnet 27 | 28 | ```python 29 | >>> from geth import MainnetGethProcess 30 | >>> geth = MainnetGethProcess() 31 | >>> geth.start() 32 | ``` 33 | 34 | Or in dev mode for testing. These require you to give them a name. 35 | 36 | ```python 37 | >>> from geth import DevGethProcess 38 | >>> geth = DevGethProcess('testing') 39 | >>> geth.start() 40 | ``` 41 | 42 | By default the `DevGethProcess` sets up test chains in the default `datadir` 43 | used by `geth`. If you would like to change the location for these test 44 | chains, you can specify an alternative `base_dir`. 45 | 46 | ```python 47 | >>> geth = DevGethProcess('testing', '/tmp/some-other-base-dir/') 48 | >>> geth.start() 49 | ``` 50 | 51 | Each instance has a few convenient properties. 52 | 53 | ```python 54 | >>> geth.data_dir 55 | "~/.ethereum" 56 | >>> geth.rpc_port 57 | 8545 58 | >>> geth.ipc_path 59 | "~/.ethereum/geth.ipc" 60 | >>> geth.accounts 61 | ['0xd3cda913deb6f67967b99d67acdfa1712c293601'] 62 | >>> geth.is_alive 63 | False 64 | >>> geth.is_running 65 | False 66 | >>> geth.is_stopped 67 | False 68 | >>> geth.start() 69 | >>> geth.is_alive 70 | True # indicates that the subprocess hasn't exited 71 | >>> geth.is_running 72 | True # indicates that `start()` has been called (but `stop()` hasn't) 73 | >>> geth.is_stopped 74 | False 75 | >>> geth.stop() 76 | >>> geth.is_alive 77 | False 78 | >>> geth.is_running 79 | False 80 | >>> geth.is_stopped 81 | True 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.14.12 122 | ``` 123 | 124 | Or from python using the `install_geth` function. 125 | 126 | ```python 127 | >>> from geth import install_geth 128 | >>> install_geth('v1.14.12') 129 | ``` 130 | 131 | The installed binary can be found in the `$HOME/.py-geth` directory, under your 132 | home directory. The `v1.14.12` binary would be located at 133 | `$HOME/.py-geth/geth-v1.14.12/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 json 2 | 3 | import pytest 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 | fill_default_genesis_data, 30 | validate_genesis_data, 31 | ) 32 | from .wrapper import ( 33 | get_geth_binary_path, 34 | ) 35 | 36 | 37 | def get_live_data_dir() -> str: 38 | """ 39 | `py-geth` needs a base directory to store it's chain data. By default this is 40 | the directory that `geth` uses as it's `datadir`. 41 | """ 42 | if sys.platform == "darwin": 43 | data_dir = os.path.expanduser( 44 | os.path.join( 45 | "~", 46 | "Library", 47 | "Ethereum", 48 | ) 49 | ) 50 | elif sys.platform in {"linux", "linux2", "linux3"}: 51 | data_dir = os.path.expanduser( 52 | os.path.join( 53 | "~", 54 | ".ethereum", 55 | ) 56 | ) 57 | elif sys.platform == "win32": 58 | data_dir = os.path.expanduser( 59 | os.path.join( 60 | "\\", 61 | "~", 62 | "AppData", 63 | "Roaming", 64 | "Ethereum", 65 | ) 66 | ) 67 | 68 | else: 69 | raise PyGethValueError( 70 | f"Unsupported platform: '{sys.platform}'. Only darwin/linux2/win32 are" 71 | " supported. You must specify the geth datadir manually" 72 | ) 73 | return data_dir 74 | 75 | 76 | def get_sepolia_data_dir() -> str: 77 | return os.path.abspath( 78 | os.path.expanduser( 79 | os.path.join( 80 | get_live_data_dir(), 81 | "sepolia", 82 | ) 83 | ) 84 | ) 85 | 86 | 87 | def get_default_base_dir() -> str: 88 | return get_live_data_dir() 89 | 90 | 91 | def get_chain_data_dir(base_dir: str, name: str) -> str: 92 | data_dir = os.path.abspath(os.path.join(base_dir, name)) 93 | ensure_path_exists(data_dir) 94 | return data_dir 95 | 96 | 97 | def get_genesis_file_path(data_dir: str) -> str: 98 | return os.path.join(data_dir, "genesis.json") 99 | 100 | 101 | def is_live_chain(data_dir: str) -> bool: 102 | return is_same_path(data_dir, get_live_data_dir()) 103 | 104 | 105 | def is_sepolia_chain(data_dir: str) -> bool: 106 | return is_same_path(data_dir, get_sepolia_data_dir()) 107 | 108 | 109 | def write_genesis_file( 110 | genesis_file_path: str, 111 | overwrite: bool = False, 112 | **genesis_data: Unpack[GenesisDataTypedDict], 113 | ) -> None: 114 | if os.path.exists(genesis_file_path) and not overwrite: 115 | raise PyGethValueError( 116 | "Genesis file already present. Call with " 117 | "`overwrite=True` to overwrite this file" 118 | ) 119 | 120 | validate_genesis_data(genesis_data) 121 | # use GenesisData model to fill defaults 122 | filled_genesis_data_model = fill_default_genesis_data(genesis_data) 123 | 124 | with open(genesis_file_path, "w") as genesis_file: 125 | genesis_file.write( 126 | json.dumps(force_obj_to_text(filled_genesis_data_model.model_dump())) 127 | ) 128 | 129 | 130 | def initialize_chain(genesis_data: GenesisDataTypedDict, data_dir: str) -> None: 131 | validate_genesis_data(genesis_data) 132 | # init with genesis.json 133 | genesis_file_path = get_genesis_file_path(data_dir) 134 | write_genesis_file(genesis_file_path, **genesis_data) 135 | init_proc = subprocess.Popen( 136 | ( 137 | get_geth_binary_path(), 138 | "--datadir", 139 | data_dir, 140 | "init", 141 | genesis_file_path, 142 | ), 143 | stdin=subprocess.PIPE, 144 | stdout=subprocess.PIPE, 145 | stderr=subprocess.PIPE, 146 | ) 147 | stdoutdata, stderrdata = init_proc.communicate() 148 | init_proc.wait() 149 | if init_proc.returncode: 150 | raise PyGethValueError( 151 | "Error initializing genesis.json: \n" 152 | f" stdout={stdoutdata.decode()}\n" 153 | f" stderr={stderrdata.decode()}" 154 | ) 155 | -------------------------------------------------------------------------------- /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 | }, 21 | "nonce": "0x0", 22 | "timestamp": "0x0", 23 | "parentHash": "0x0000000000000000000000000000000000000000000000000000000000000000", 24 | "extraData": "0x0000000000000000000000000000000000000000000000000000000000000000", 25 | "gasLimit": "0x47e7c4", 26 | "difficulty": "0x0", 27 | "mixHash": "0x0000000000000000000000000000000000000000000000000000000000000000", 28 | "alloc": {} 29 | } 30 | -------------------------------------------------------------------------------- /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 | 50 | 51 | LINUX = "linux" 52 | OSX = "darwin" 53 | WINDOWS = "win32" 54 | 55 | 56 | # 57 | # System utilities. 58 | # 59 | @contextlib.contextmanager 60 | def chdir(path: str) -> Generator[None, None, None]: 61 | original_path = os.getcwd() 62 | try: 63 | os.chdir(path) 64 | yield 65 | finally: 66 | os.chdir(original_path) 67 | 68 | 69 | def get_platform() -> str: 70 | if sys.platform.startswith("linux"): 71 | return LINUX 72 | elif sys.platform == OSX: 73 | return OSX 74 | elif sys.platform == WINDOWS: 75 | return WINDOWS 76 | else: 77 | raise PyGethKeyError(f"Unknown platform: {sys.platform}") 78 | 79 | 80 | def is_executable_available(program: str) -> bool: 81 | def is_exe(fpath: str) -> bool: 82 | return os.path.isfile(fpath) and os.access(fpath, os.X_OK) 83 | 84 | fpath = os.path.dirname(program) 85 | if fpath: 86 | if is_exe(program): 87 | return True 88 | else: 89 | for path in os.environ["PATH"].split(os.pathsep): 90 | path = path.strip('"') 91 | exe_file = os.path.join(path, program) 92 | if is_exe(exe_file): 93 | return True 94 | 95 | return False 96 | 97 | 98 | def ensure_path_exists(dir_path: str) -> bool: 99 | """ 100 | Make sure that a path exists 101 | """ 102 | if not os.path.exists(dir_path): 103 | os.makedirs(dir_path) 104 | return True 105 | return False 106 | 107 | 108 | def ensure_parent_dir_exists(path: str) -> None: 109 | ensure_path_exists(os.path.dirname(path)) 110 | 111 | 112 | def check_subprocess_call( 113 | command: list[str], 114 | message: str | None = None, 115 | stderr: IO_Any = subprocess.STDOUT, 116 | **proc_kwargs: Any, 117 | ) -> int: 118 | if message: 119 | print(message) 120 | print(f"Executing: {' '.join(command)}") 121 | 122 | return subprocess.check_call(command, stderr=stderr, **proc_kwargs) 123 | 124 | 125 | def check_subprocess_output( 126 | command: list[str], 127 | message: str | None = None, 128 | stderr: IO_Any = subprocess.STDOUT, 129 | **proc_kwargs: Any, 130 | ) -> Any: 131 | if message: 132 | print(message) 133 | print(f"Executing: {' '.join(command)}") 134 | 135 | return subprocess.check_output(command, stderr=stderr, **proc_kwargs) 136 | 137 | 138 | def chmod_plus_x(executable_path: str) -> None: 139 | current_st = os.stat(executable_path) 140 | os.chmod(executable_path, current_st.st_mode | stat.S_IEXEC) 141 | 142 | 143 | def get_go_executable_path() -> str: 144 | return os.environ.get("GO_BINARY", "go") 145 | 146 | 147 | def is_go_available() -> bool: 148 | return is_executable_available(get_go_executable_path()) 149 | 150 | 151 | # 152 | # Installation filesystem path utilities 153 | # 154 | def get_base_install_path(identifier: str) -> str: 155 | if "GETH_BASE_INSTALL_PATH" in os.environ: 156 | return os.path.join( 157 | os.environ["GETH_BASE_INSTALL_PATH"], 158 | f"geth-{identifier}", 159 | ) 160 | else: 161 | return os.path.expanduser( 162 | os.path.join( 163 | "~", 164 | ".py-geth", 165 | f"geth-{identifier}", 166 | ) 167 | ) 168 | 169 | 170 | def get_source_code_archive_path(identifier: str) -> str: 171 | return os.path.join( 172 | get_base_install_path(identifier), 173 | "release.tar.gz", 174 | ) 175 | 176 | 177 | def get_source_code_extract_path(identifier: str) -> str: 178 | return os.path.join( 179 | get_base_install_path(identifier), 180 | "source", 181 | ) 182 | 183 | 184 | def get_source_code_path(identifier: str) -> str: 185 | return os.path.join( 186 | get_base_install_path(identifier), 187 | "source", 188 | f"go-ethereum-{identifier.lstrip('v')}", 189 | ) 190 | 191 | 192 | def get_build_path(identifier: str) -> str: 193 | source_code_path = get_source_code_path(identifier) 194 | return os.path.join( 195 | source_code_path, 196 | "build", 197 | ) 198 | 199 | 200 | def get_built_executable_path(identifier: str) -> str: 201 | build_path = get_build_path(identifier) 202 | return os.path.join( 203 | build_path, 204 | "bin", 205 | "geth", 206 | ) 207 | 208 | 209 | def get_executable_path(identifier: str) -> str: 210 | base_install_path = get_base_install_path(identifier) 211 | return os.path.join( 212 | base_install_path, 213 | "bin", 214 | "geth", 215 | ) 216 | 217 | 218 | # 219 | # Installation primitives. 220 | # 221 | DOWNLOAD_SOURCE_CODE_URI_TEMPLATE = ( 222 | "https://github.com/ethereum/go-ethereum/archive/{0}.tar.gz" 223 | ) 224 | 225 | 226 | def download_source_code_release(identifier: str) -> None: 227 | download_uri = DOWNLOAD_SOURCE_CODE_URI_TEMPLATE.format(identifier) 228 | source_code_archive_path = get_source_code_archive_path(identifier) 229 | 230 | ensure_parent_dir_exists(source_code_archive_path) 231 | try: 232 | response = requests.get(download_uri) 233 | response.raise_for_status() 234 | with open(source_code_archive_path, "wb") as f: 235 | f.write(response.content) 236 | 237 | print(f"Downloading source code release from {download_uri}") 238 | 239 | except (HTTPError, Timeout, ConnectionError) as e: 240 | raise PyGethException( 241 | f"An error occurred while downloading from {download_uri}: {e}" 242 | ) 243 | 244 | 245 | def extract_source_code_release(identifier: str) -> None: 246 | source_code_archive_path = get_source_code_archive_path(identifier) 247 | source_code_extract_path = get_source_code_extract_path(identifier) 248 | ensure_path_exists(source_code_extract_path) 249 | 250 | print( 251 | f"Extracting archive: {source_code_archive_path} -> {source_code_extract_path}" 252 | ) 253 | 254 | with tarfile.open(source_code_archive_path, "r:gz") as archive_file: 255 | 256 | def is_within_directory(directory: str, target: str) -> bool: 257 | abs_directory = os.path.abspath(directory) 258 | abs_target = os.path.abspath(target) 259 | 260 | prefix = os.path.commonprefix([abs_directory, abs_target]) 261 | 262 | return prefix == abs_directory 263 | 264 | def safe_extract(tar: tarfile.TarFile, path: str = ".") -> None: 265 | for member in tar.getmembers(): 266 | member_path = os.path.join(path, member.name) 267 | if not is_within_directory(path, member_path): 268 | raise PyGethException("Attempted Path Traversal in Tar File") 269 | 270 | tar.extractall(path) 271 | 272 | safe_extract(archive_file, source_code_extract_path) 273 | 274 | 275 | def build_from_source_code(identifier: str) -> None: 276 | if not is_go_available(): 277 | raise PyGethOSError( 278 | "The `go` runtime was not found but is required to build geth. If " 279 | "the `go` executable is not in your $PATH you can specify the path " 280 | "using the environment variable GO_BINARY to specify the path." 281 | ) 282 | source_code_path = get_source_code_path(identifier) 283 | 284 | with chdir(source_code_path): 285 | make_command = ["make", "geth"] 286 | 287 | check_subprocess_call( 288 | make_command, 289 | message="Building `geth` binary", 290 | ) 291 | 292 | built_executable_path = get_built_executable_path(identifier) 293 | if not os.path.exists(built_executable_path): 294 | raise PyGethOSError( 295 | "Built executable not found in expected location: " 296 | f"{built_executable_path}" 297 | ) 298 | print(f"Making built binary executable: chmod +x {built_executable_path}") 299 | chmod_plus_x(built_executable_path) 300 | 301 | executable_path = get_executable_path(identifier) 302 | ensure_parent_dir_exists(executable_path) 303 | if os.path.exists(executable_path): 304 | if os.path.islink(executable_path): 305 | os.remove(executable_path) 306 | else: 307 | raise PyGethOSError( 308 | f"Non-symlink file already present at `{executable_path}`" 309 | ) 310 | os.symlink(built_executable_path, executable_path) 311 | chmod_plus_x(executable_path) 312 | 313 | 314 | def install_from_source_code_release(identifier: str) -> None: 315 | download_source_code_release(identifier) 316 | extract_source_code_release(identifier) 317 | build_from_source_code(identifier) 318 | 319 | executable_path = get_executable_path(identifier) 320 | assert os.path.exists(executable_path), f"Executable not found @ {executable_path}" 321 | 322 | check_version_command = [executable_path, "version"] 323 | 324 | version_output = check_subprocess_output( 325 | check_version_command, 326 | message=f"Checking installed executable version @ {executable_path}", 327 | ) 328 | 329 | print(f"geth successfully installed at: {executable_path}\n\n{version_output}\n\n") 330 | 331 | 332 | install_v1_14_0 = functools.partial(install_from_source_code_release, V1_14_0) 333 | install_v1_14_2 = functools.partial(install_from_source_code_release, V1_14_2) 334 | install_v1_14_3 = functools.partial(install_from_source_code_release, V1_14_3) 335 | install_v1_14_4 = functools.partial(install_from_source_code_release, V1_14_4) 336 | install_v1_14_5 = functools.partial(install_from_source_code_release, V1_14_5) 337 | install_v1_14_6 = functools.partial(install_from_source_code_release, V1_14_6) 338 | install_v1_14_7 = functools.partial(install_from_source_code_release, V1_14_7) 339 | install_v1_14_8 = functools.partial(install_from_source_code_release, V1_14_8) 340 | install_v1_14_9 = functools.partial(install_from_source_code_release, V1_14_9) 341 | install_v1_14_10 = functools.partial(install_from_source_code_release, V1_14_10) 342 | install_v1_14_11 = functools.partial(install_from_source_code_release, V1_14_11) 343 | install_v1_14_12 = functools.partial(install_from_source_code_release, V1_14_12) 344 | 345 | INSTALL_FUNCTIONS = { 346 | LINUX: { 347 | V1_14_0: install_v1_14_0, 348 | V1_14_2: install_v1_14_2, 349 | V1_14_3: install_v1_14_3, 350 | V1_14_4: install_v1_14_4, 351 | V1_14_5: install_v1_14_5, 352 | V1_14_6: install_v1_14_6, 353 | V1_14_7: install_v1_14_7, 354 | V1_14_8: install_v1_14_8, 355 | V1_14_9: install_v1_14_9, 356 | V1_14_10: install_v1_14_10, 357 | V1_14_11: install_v1_14_11, 358 | V1_14_12: install_v1_14_12, 359 | }, 360 | OSX: { 361 | V1_14_0: install_v1_14_0, 362 | V1_14_2: install_v1_14_2, 363 | V1_14_3: install_v1_14_3, 364 | V1_14_4: install_v1_14_4, 365 | V1_14_5: install_v1_14_5, 366 | V1_14_6: install_v1_14_6, 367 | V1_14_7: install_v1_14_7, 368 | V1_14_8: install_v1_14_8, 369 | V1_14_9: install_v1_14_9, 370 | V1_14_10: install_v1_14_10, 371 | V1_14_11: install_v1_14_11, 372 | V1_14_12: install_v1_14_12, 373 | }, 374 | } 375 | 376 | 377 | def install_geth(identifier: str, platform: str | None = None) -> None: 378 | if platform is None: 379 | platform = get_platform() 380 | 381 | if platform not in INSTALL_FUNCTIONS: 382 | raise PyGethValueError( 383 | "Installation of go-ethereum is not supported on your platform " 384 | f"({platform}). Supported platforms are: " 385 | f"{', '.join(sorted(INSTALL_FUNCTIONS.keys()))}" 386 | ) 387 | elif identifier not in INSTALL_FUNCTIONS[platform]: 388 | raise PyGethValueError( 389 | f"Installation of geth=={identifier} is not supported. Must be one of " 390 | f"{', '.join(sorted(INSTALL_FUNCTIONS[platform].keys()))}" 391 | ) 392 | 393 | install_fn = INSTALL_FUNCTIONS[platform][identifier] 394 | install_fn() 395 | 396 | 397 | if __name__ == "__main__": 398 | try: 399 | identifier = sys.argv[1] 400 | except IndexError: 401 | print( 402 | "Invocation error. Should be invoked as `python -m geth.install `" # noqa: E501 403 | ) 404 | sys.exit(1) 405 | 406 | install_geth(identifier) 407 | -------------------------------------------------------------------------------- /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 json 10 | import logging 11 | import os 12 | import subprocess 13 | import time 14 | from types import ( 15 | TracebackType, 16 | ) 17 | from typing import ( 18 | cast, 19 | ) 20 | from urllib.error import ( 21 | URLError, 22 | ) 23 | from urllib.request import ( 24 | urlopen, 25 | ) 26 | 27 | import semantic_version 28 | 29 | from geth import ( 30 | get_geth_version, 31 | ) 32 | from geth.accounts import ( 33 | ensure_account_exists, 34 | get_accounts, 35 | ) 36 | from geth.chain import ( 37 | get_chain_data_dir, 38 | get_default_base_dir, 39 | get_genesis_file_path, 40 | get_live_data_dir, 41 | get_sepolia_data_dir, 42 | initialize_chain, 43 | is_live_chain, 44 | is_sepolia_chain, 45 | ) 46 | from geth.exceptions import ( 47 | PyGethNotImplementedError, 48 | PyGethValueError, 49 | ) 50 | from geth.types import ( 51 | GethKwargsTypedDict, 52 | IO_Any, 53 | ) 54 | from geth.utils.networking import ( 55 | get_ipc_socket, 56 | ) 57 | from geth.utils.proc import ( 58 | kill_proc, 59 | ) 60 | from geth.utils.timeout import ( 61 | Timeout, 62 | ) 63 | from geth.utils.validation import ( 64 | GenesisDataTypedDict, 65 | validate_genesis_data, 66 | validate_geth_kwargs, 67 | ) 68 | from geth.wrapper import ( 69 | construct_popen_command, 70 | construct_test_chain_kwargs, 71 | ) 72 | 73 | logger = logging.getLogger(__name__) 74 | with open(os.path.join(os.path.dirname(__file__), "genesis.json")) as genesis_file: 75 | GENESIS_JSON = json.load(genesis_file) 76 | 77 | 78 | class BaseGethProcess(ABC): 79 | _proc = None 80 | 81 | def __init__( 82 | self, 83 | geth_kwargs: GethKwargsTypedDict, 84 | stdin: IO_Any = subprocess.PIPE, 85 | stdout: IO_Any = subprocess.PIPE, 86 | stderr: IO_Any = subprocess.PIPE, 87 | ): 88 | validate_geth_kwargs(geth_kwargs) 89 | self.geth_kwargs = geth_kwargs 90 | self.command = construct_popen_command(**geth_kwargs) 91 | self.stdin = stdin 92 | self.stdout = stdout 93 | self.stderr = stderr 94 | 95 | is_running = False 96 | 97 | def start(self) -> None: 98 | if self.is_running: 99 | raise PyGethValueError("Already running") 100 | self.is_running = True 101 | 102 | logger.info(f"Launching geth: {' '.join(self.command)}") 103 | self.proc = subprocess.Popen( 104 | self.command, 105 | stdin=self.stdin, 106 | stdout=self.stdout, 107 | stderr=self.stderr, 108 | ) 109 | 110 | def __enter__(self) -> BaseGethProcess: 111 | self.start() 112 | return self 113 | 114 | def stop(self) -> None: 115 | if not self.is_running: 116 | raise PyGethValueError("Not running") 117 | 118 | if self.proc.poll() is None: 119 | kill_proc(self.proc) 120 | 121 | self.is_running = False 122 | 123 | def __exit__( 124 | self, 125 | exc_type: type[BaseException] | None, 126 | exc_value: BaseException | None, 127 | tb: TracebackType | None, 128 | ) -> None: 129 | self.stop() 130 | 131 | @property 132 | @abstractmethod 133 | def data_dir(self) -> str: 134 | raise PyGethNotImplementedError("Must be implemented by subclasses.") 135 | 136 | @property 137 | def is_alive(self) -> bool: 138 | return self.is_running and self.proc.poll() is None 139 | 140 | @property 141 | def is_stopped(self) -> bool: 142 | return self.proc is not None and self.proc.poll() is not None 143 | 144 | @property 145 | def accounts(self) -> tuple[str, ...]: 146 | return get_accounts(**self.geth_kwargs) 147 | 148 | @property 149 | def rpc_enabled(self) -> bool: 150 | _rpc_enabled = self.geth_kwargs.get("rpc_enabled", False) 151 | return cast(bool, _rpc_enabled) 152 | 153 | @property 154 | def rpc_host(self) -> str: 155 | _rpc_host = self.geth_kwargs.get("rpc_host", "127.0.0.1") 156 | return cast(str, _rpc_host) 157 | 158 | @property 159 | def rpc_port(self) -> str: 160 | _rpc_port = self.geth_kwargs.get("rpc_port", "8545") 161 | return cast(str, _rpc_port) 162 | 163 | @property 164 | def is_rpc_ready(self) -> bool: 165 | try: 166 | urlopen(f"http://{self.rpc_host}:{self.rpc_port}") 167 | except URLError: 168 | return False 169 | else: 170 | return True 171 | 172 | def wait_for_rpc(self, timeout: int = 0) -> None: 173 | if not self.rpc_enabled: 174 | raise PyGethValueError("RPC interface is not enabled") 175 | 176 | with Timeout(timeout) as _timeout: 177 | while True: 178 | if self.is_rpc_ready: 179 | break 180 | time.sleep(0.1) 181 | _timeout.check() 182 | 183 | @property 184 | def ipc_enabled(self) -> bool: 185 | return not self.geth_kwargs.get("ipc_disable", None) 186 | 187 | @property 188 | def ipc_path(self) -> str: 189 | return self.geth_kwargs.get("ipc_path") or os.path.abspath( 190 | os.path.expanduser( 191 | os.path.join( 192 | self.data_dir, 193 | "geth.ipc", 194 | ) 195 | ) 196 | ) 197 | 198 | @property 199 | def is_ipc_ready(self) -> bool: 200 | try: 201 | with get_ipc_socket(self.ipc_path): 202 | pass 203 | except OSError: 204 | return False 205 | else: 206 | return True 207 | 208 | def wait_for_ipc(self, timeout: int = 0) -> None: 209 | if not self.ipc_enabled: 210 | raise PyGethValueError("IPC interface is not enabled") 211 | 212 | with Timeout(timeout) as _timeout: 213 | while True: 214 | if self.is_ipc_ready: 215 | break 216 | time.sleep(0.1) 217 | _timeout.check() 218 | 219 | 220 | class MainnetGethProcess(BaseGethProcess): 221 | def __init__(self, geth_kwargs: GethKwargsTypedDict | None = None): 222 | if geth_kwargs is None: 223 | geth_kwargs = {} 224 | 225 | if "data_dir" in geth_kwargs: 226 | raise PyGethValueError( 227 | "You cannot specify `data_dir` for a MainnetGethProcess" 228 | ) 229 | 230 | super().__init__(geth_kwargs) 231 | 232 | @property 233 | def data_dir(self) -> str: 234 | return get_live_data_dir() 235 | 236 | 237 | class SepoliaGethProcess(BaseGethProcess): 238 | def __init__(self, geth_kwargs: GethKwargsTypedDict | None = None): 239 | if geth_kwargs is None: 240 | geth_kwargs = {} 241 | 242 | if "data_dir" in geth_kwargs: 243 | raise PyGethValueError( 244 | f"You cannot specify `data_dir` for a {type(self).__name__}" 245 | ) 246 | if "network_id" in geth_kwargs: 247 | raise PyGethValueError( 248 | f"You cannot specify `network_id` for a {type(self).__name__}" 249 | ) 250 | 251 | geth_kwargs["network_id"] = "11155111" 252 | geth_kwargs["data_dir"] = get_sepolia_data_dir() 253 | 254 | super().__init__(geth_kwargs) 255 | 256 | @property 257 | def data_dir(self) -> str: 258 | return get_sepolia_data_dir() 259 | 260 | 261 | class TestnetGethProcess(SepoliaGethProcess): 262 | """ 263 | Alias for whatever the current primary testnet chain is. 264 | """ 265 | 266 | 267 | class DevGethProcess(BaseGethProcess): 268 | """ 269 | Geth developer mode process for testing purposes. 270 | """ 271 | 272 | _data_dir: str 273 | 274 | def __init__( 275 | self, 276 | chain_name: str, 277 | base_dir: str | None = None, 278 | overrides: GethKwargsTypedDict | None = None, 279 | genesis_data: GenesisDataTypedDict | None = None, 280 | ): 281 | if overrides is None: 282 | overrides = {} 283 | 284 | if genesis_data is None: 285 | genesis_data = GenesisDataTypedDict(**GENESIS_JSON) 286 | 287 | validate_genesis_data(genesis_data) 288 | 289 | if "data_dir" in overrides: 290 | raise PyGethValueError("You cannot specify `data_dir` for a DevGethProcess") 291 | 292 | if base_dir is None: 293 | base_dir = get_default_base_dir() 294 | 295 | self._data_dir = get_chain_data_dir(base_dir, chain_name) 296 | overrides["data_dir"] = self.data_dir 297 | geth_kwargs = construct_test_chain_kwargs(**overrides) 298 | validate_geth_kwargs(geth_kwargs) 299 | 300 | # ensure that an account is present 301 | coinbase = ensure_account_exists(**geth_kwargs) 302 | 303 | # ensure that the chain is initialized 304 | genesis_file_path = get_genesis_file_path(self.data_dir) 305 | needs_init = all( 306 | ( 307 | not os.path.exists(genesis_file_path), 308 | not is_live_chain(self.data_dir), 309 | not is_sepolia_chain(self.data_dir), 310 | ) 311 | ) 312 | if needs_init: 313 | genesis_data["coinbase"] = coinbase 314 | genesis_data.setdefault("alloc", {}).setdefault( 315 | coinbase, {"balance": "1000000000000000000000000000000"} 316 | ) 317 | 318 | modify_genesis_based_on_geth_version(genesis_data) 319 | initialize_chain(genesis_data, self.data_dir) 320 | 321 | super().__init__(geth_kwargs) 322 | 323 | @property 324 | def data_dir(self) -> str: 325 | return self._data_dir 326 | 327 | 328 | def modify_genesis_based_on_geth_version(genesis_data: GenesisDataTypedDict) -> None: 329 | geth_version = get_geth_version() 330 | if geth_version <= semantic_version.Version("1.14.0"): 331 | # geth <= v1.14.0 needs negative `terminalTotalDifficulty` to load EVM 332 | # instructions correctly: https://github.com/ethereum/go-ethereum/pull/29579 333 | if "config" not in genesis_data: 334 | genesis_data["config"] = {} 335 | genesis_data["config"]["terminalTotalDifficulty"] = -1 336 | -------------------------------------------------------------------------------- /geth/py.typed: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Ere11i/py-geth/152d6c98d39d705fd98b96d1f8f52187c67fb968/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 | gcmode: Literal["full", "archive"] | None 21 | geth_executable: str | None 22 | ipc_disable: bool | None 23 | ipc_path: str | None 24 | max_peers: str | None 25 | network_id: str | None 26 | nice: bool | None 27 | no_discover: bool | None 28 | password: bytes | str | None 29 | port: str | None 30 | preload: str | None 31 | rpc_addr: str | None 32 | rpc_api: str | None 33 | rpc_cors_domain: str | None 34 | rpc_enabled: bool | None 35 | rpc_port: str | None 36 | stdin: str | None 37 | suffix_args: list[str] | None 38 | suffix_kwargs: dict[str, str] | None 39 | tx_pool_global_slots: str | None 40 | tx_pool_lifetime: str | None 41 | tx_pool_price_limit: str | None 42 | verbosity: str | None 43 | ws_addr: str | None 44 | ws_api: str | None 45 | ws_enabled: bool | None 46 | ws_origins: str | None 47 | ws_port: str | None 48 | 49 | 50 | class GenesisDataTypedDict(TypedDict, total=False): 51 | alloc: dict[str, dict[str, Any]] 52 | baseFeePerGas: str 53 | blobGasUsed: str 54 | coinbase: str 55 | config: dict[str, Any] 56 | difficulty: str 57 | excessBlobGas: str 58 | extraData: str 59 | gasLimit: str 60 | gasUsed: str 61 | mixHash: str 62 | nonce: str 63 | number: str 64 | parentHash: str 65 | timestamp: str 66 | -------------------------------------------------------------------------------- /geth/utils/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Ere11i/py-geth/152d6c98d39d705fd98b96d1f8f52187c67fb968/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 | ) 15 | 16 | from geth.exceptions import ( 17 | PyGethValueError, 18 | ) 19 | from geth.types import ( 20 | GenesisDataTypedDict, 21 | GethKwargsTypedDict, 22 | ) 23 | 24 | 25 | class GethKwargs(BaseModel): 26 | cache: str | None = None 27 | data_dir: str | None = None 28 | dev_mode: bool | None = False 29 | gcmode: Literal["full", "archive"] | None = None 30 | geth_executable: str | None = None 31 | ipc_disable: bool | None = None 32 | ipc_path: str | None = None 33 | max_peers: str | None = None 34 | network_id: str | None = None 35 | nice: bool | None = True 36 | no_discover: bool | None = None 37 | password: bytes | str | None = None 38 | port: str | None = None 39 | preload: str | None = None 40 | rpc_addr: str | None = None 41 | rpc_api: str | None = None 42 | rpc_cors_domain: str | None = None 43 | rpc_enabled: bool | None = None 44 | rpc_port: str | None = None 45 | stdin: str | None = None 46 | suffix_args: list[str] | None = None 47 | suffix_kwargs: dict[str, str] | None = None 48 | tx_pool_global_slots: str | None = None 49 | tx_pool_lifetime: str | None = None 50 | tx_pool_price_limit: str | None = None 51 | verbosity: str | None = None 52 | ws_addr: str | None = None 53 | ws_api: str | None = None 54 | ws_enabled: bool | None = None 55 | ws_origins: str | None = None 56 | ws_port: str | None = None 57 | 58 | model_config = ConfigDict(extra="forbid") 59 | 60 | 61 | def validate_geth_kwargs(geth_kwargs: GethKwargsTypedDict) -> None: 62 | """ 63 | Converts geth_kwargs to GethKwargs and raises a ValueError if the conversion fails. 64 | """ 65 | try: 66 | GethKwargs(**geth_kwargs) 67 | except ValidationError as e: 68 | raise PyGethValueError(f"geth_kwargs validation failed: {e}") 69 | except TypeError as e: 70 | raise PyGethValueError(f"error while validating geth_kwargs: {e}") 71 | 72 | 73 | class GenesisDataConfig(BaseModel): 74 | chainId: int = 0 75 | ethash: dict[str, Any] = {} # so that geth treats config as PoW -> PoS transition 76 | homesteadBlock: int = 0 77 | daoForkBlock: int = 0 78 | daoForkSupport: bool = True 79 | eip150Block: int = 0 80 | eip155Block: int = 0 81 | eip158Block: int = 0 82 | byzantiumBlock: int = 0 83 | constantinopleBlock: int = 0 84 | petersburgBlock: int = 0 85 | istanbulBlock: int = 0 86 | berlinBlock: int = 0 87 | londonBlock: int = 0 88 | arrowGlacierBlock: int = 0 89 | grayGlacierBlock: int = 0 90 | # merge 91 | terminalTotalDifficulty: int = 0 92 | terminalTotalDifficultyPassed: bool = True 93 | # post-merge, timestamp is used for network transitions 94 | shanghaiTime: int = 0 95 | cancunTime: int = 0 96 | 97 | model_config = ConfigDict(extra="forbid") 98 | 99 | 100 | class GenesisData(BaseModel): 101 | alloc: dict[str, dict[str, Any]] = {} 102 | baseFeePerGas: str = "0x0" 103 | blobGasUsed: str = "0x0" 104 | coinbase: str = "0x3333333333333333333333333333333333333333" 105 | config: dict[str, Any] = GenesisDataConfig().model_dump() 106 | difficulty: str = "0x0" 107 | excessBlobGas: str = "0x0" 108 | extraData: str = ( 109 | "0x0000000000000000000000000000000000000000000000000000000000000000" 110 | ) 111 | gasLimit: str = "0x47e7c4" 112 | gasUsed: str = "0x0" 113 | mixHash: str = "0x0000000000000000000000000000000000000000000000000000000000000000" 114 | nonce: str = "0x0" 115 | number: str = "0x0" 116 | parentHash: str = ( 117 | "0x0000000000000000000000000000000000000000000000000000000000000000" 118 | ) 119 | timestamp: str = "0x0" 120 | 121 | model_config = ConfigDict(extra="forbid") 122 | 123 | 124 | def validate_genesis_data(genesis_data: GenesisDataTypedDict) -> None: 125 | """ 126 | Validates the genesis data 127 | """ 128 | try: 129 | GenesisData(**genesis_data) 130 | except ValidationError as e: 131 | raise PyGethValueError(f"genesis_data validation failed: {e}") 132 | except TypeError as e: 133 | raise PyGethValueError(f"error while validating genesis_data: {e}") 134 | 135 | """ 136 | Validates the genesis data config field 137 | """ 138 | genesis_data_config = genesis_data.get("config", None) 139 | if genesis_data_config: 140 | try: 141 | GenesisDataConfig(**genesis_data_config) 142 | except ValidationError as e: 143 | raise PyGethValueError(f"genesis_data config field validation failed: {e}") 144 | except TypeError as e: 145 | raise PyGethValueError( 146 | f"error while validating genesis_data config field: {e}" 147 | ) 148 | 149 | 150 | def fill_default_genesis_data( 151 | genesis_data: GenesisDataTypedDict, 152 | ) -> GenesisData: 153 | """ 154 | Fills in default values for the genesis data 155 | """ 156 | try: 157 | genesis_data_filled = GenesisData(**genesis_data) 158 | except ValidationError as e: 159 | raise PyGethValueError( 160 | f"genesis_data validation failed while filling defaults: {e}" 161 | ) 162 | except TypeError as e: 163 | raise PyGethValueError(f"error while filling default genesis_data: {e}") 164 | 165 | if genesis_data.get("config"): 166 | try: 167 | genesis_data_config_filled = GenesisDataConfig(**genesis_data["config"]) 168 | except ValidationError as e: 169 | raise PyGethValueError( 170 | f"genesis_data validation failed while filling config defaults: {e}" 171 | ) 172 | except TypeError as e: 173 | raise PyGethValueError( 174 | f"error while filling default genesis_data config: {e}" 175 | ) 176 | 177 | genesis_data_filled.config = genesis_data_config_filled.model_dump() 178 | 179 | return genesis_data_filled 180 | -------------------------------------------------------------------------------- /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.rpc_enabled: 157 | builder.append("--http") 158 | 159 | if gk.rpc_addr is not None: 160 | builder.extend(("--http.addr", gk.rpc_addr)) 161 | 162 | if gk.rpc_port is not None: 163 | builder.extend(("--http.port", gk.rpc_port)) 164 | 165 | if gk.rpc_api is not None: 166 | builder.extend(("--http.api", gk.rpc_api)) 167 | 168 | if gk.rpc_cors_domain is not None: 169 | builder.extend(("--http.corsdomain", gk.rpc_cors_domain)) 170 | 171 | if gk.ws_enabled: 172 | builder.append("--ws") 173 | 174 | if gk.ws_addr is not None: 175 | builder.extend(("--ws.addr", gk.ws_addr)) 176 | 177 | if gk.ws_origins is not None: 178 | builder.extend(("--ws.origins", gk.ws_port)) 179 | 180 | if gk.ws_port is not None: 181 | builder.extend(("--ws.port", gk.ws_port)) 182 | 183 | if gk.ws_api is not None: 184 | builder.extend(("--ws.api", gk.ws_api)) 185 | 186 | if gk.data_dir is not None: 187 | builder.extend(("--datadir", gk.data_dir)) 188 | 189 | if gk.max_peers is not None: 190 | builder.extend(("--maxpeers", gk.max_peers)) 191 | 192 | if gk.network_id is not None: 193 | builder.extend(("--networkid", gk.network_id)) 194 | 195 | if gk.port is not None: 196 | builder.extend(("--port", gk.port)) 197 | 198 | if gk.ipc_disable: 199 | builder.append("--ipcdisable") 200 | 201 | if gk.ipc_path is not None: 202 | builder.extend(("--ipcpath", gk.ipc_path)) 203 | 204 | if gk.verbosity is not None: 205 | builder.extend(("--verbosity", gk.verbosity)) 206 | 207 | if isinstance(gk.password, str) and gk.password is not None: 208 | # If password is a string, it's a file path 209 | # If password is bytes, it's the password itself and is passed directly to 210 | # the geth process elsewhere 211 | builder.extend(("--password", gk.password)) 212 | 213 | if gk.preload is not None: 214 | builder.extend(("--preload", gk.preload)) 215 | 216 | if gk.no_discover: 217 | builder.append("--nodiscover") 218 | 219 | if gk.tx_pool_global_slots is not None: 220 | builder.extend(("--txpool.globalslots", gk.tx_pool_global_slots)) 221 | 222 | if gk.tx_pool_lifetime is not None: 223 | builder.extend(("--txpool.lifetime", gk.tx_pool_lifetime)) 224 | 225 | if gk.tx_pool_price_limit is not None: 226 | builder.extend(("--txpool.pricelimit", gk.tx_pool_price_limit)) 227 | 228 | if gk.cache: 229 | builder.extend(("--cache", gk.cache)) 230 | 231 | if gk.gcmode: 232 | builder.extend(("--gcmode", gk.gcmode)) 233 | 234 | if gk.suffix_kwargs: 235 | builder.extend(gk.suffix_kwargs) 236 | 237 | if gk.suffix_args: 238 | builder.extend(gk.suffix_args) 239 | 240 | return builder.command 241 | 242 | 243 | def geth_wrapper( 244 | **geth_kwargs: Unpack[GethKwargsTypedDict], 245 | ) -> tuple[bytes, bytes, list[str], subprocess.Popen[bytes]]: 246 | validate_geth_kwargs(geth_kwargs) 247 | stdin = geth_kwargs.pop("stdin", None) 248 | command = construct_popen_command(**geth_kwargs) 249 | 250 | proc = subprocess.Popen( 251 | command, 252 | stdin=subprocess.PIPE, 253 | stdout=subprocess.PIPE, 254 | stderr=subprocess.PIPE, 255 | ) 256 | stdin_bytes: bytes | None = None 257 | if stdin is not None: 258 | stdin_bytes = force_bytes(stdin) 259 | 260 | stdoutdata, stderrdata = proc.communicate(stdin_bytes) 261 | 262 | if proc.returncode != 0: 263 | raise PyGethGethError( 264 | command=command, 265 | return_code=proc.returncode, 266 | stdin_data=stdin, 267 | stdout_data=stdoutdata, 268 | stderr_data=stderrdata, 269 | ) 270 | 271 | return stdoutdata, stderrdata, command, proc 272 | 273 | 274 | def spawn_geth( 275 | geth_kwargs: GethKwargsTypedDict, 276 | stdin: IO_Any = subprocess.PIPE, 277 | stdout: IO_Any = subprocess.PIPE, 278 | stderr: IO_Any = subprocess.PIPE, 279 | ) -> tuple[list[str], subprocess.Popen[bytes]]: 280 | validate_geth_kwargs(geth_kwargs) 281 | command = construct_popen_command(**geth_kwargs) 282 | 283 | proc = subprocess.Popen( 284 | command, 285 | stdin=stdin, 286 | stdout=stdout, 287 | stderr=stderr, 288 | ) 289 | 290 | return command, proc 291 | -------------------------------------------------------------------------------- /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 | remove_all_unused_imports = true 3 | exclude = "__init__.py" 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 | known_third_party = "hypothesis,pytest" 11 | known_first_party = "geth" 12 | multi_line_output = 3 13 | profile = "black" 14 | 15 | [tool.mypy] 16 | check_untyped_defs = true 17 | disallow_incomplete_defs = true 18 | disallow_untyped_defs = true 19 | disallow_any_generics = true 20 | disallow_untyped_calls = true 21 | disallow_untyped_decorators = true 22 | disallow_subclassing_any = true 23 | ignore_missing_imports = true 24 | strict_optional = true 25 | strict_equality = true 26 | warn_redundant_casts = true 27 | warn_return_any = true 28 | warn_unused_configs = true 29 | warn_unused_ignores = true 30 | 31 | 32 | [tool.pydocstyle] 33 | # All error codes found here: 34 | # http://www.pydocstyle.org/en/3.0.0/error_codes.html 35 | # 36 | # Ignored: 37 | # D1 - Missing docstring error codes 38 | # 39 | # Selected: 40 | # D2 - Whitespace error codes 41 | # D3 - Quote error codes 42 | # D4 - Content related error codes 43 | select = "D2,D3,D4" 44 | 45 | # Extra ignores: 46 | # D200 - One-line docstring should fit on one line with quotes 47 | # D203 - 1 blank line required before class docstring 48 | # D204 - 1 blank line required after class docstring 49 | # D205 - 1 blank line required between summary line and description 50 | # D212 - Multi-line docstring summary should start at the first line 51 | # D302 - Use u""" for Unicode docstrings 52 | # D400 - First line should end with a period 53 | # D401 - First line should be in imperative mood 54 | # D412 - No blank lines allowed between a section header and its content 55 | # D415 - First line should end with a period, question mark, or exclamation point 56 | add-ignore = "D200,D203,D204,D205,D212,D302,D400,D401,D412,D415" 57 | 58 | # Explanation: 59 | # D400 - Enabling this error code seems to make it a requirement that the first 60 | # sentence in a docstring is not split across two lines. It also makes it a 61 | # requirement that no docstring can have a multi-sentence description without a 62 | # summary line. Neither one of those requirements seem appropriate. 63 | 64 | [tool.pytest.ini_options] 65 | addopts = "-v --showlocals --durations 10" 66 | xfail_strict = true 67 | log_format = "%(levelname)8s %(asctime)s %(filename)20s %(message)s" 68 | log_date_format = "%m-%d %H:%M:%S" 69 | 70 | [tool.towncrier] 71 | # Read https://github.com/ethereum/py-geth/blob/main/newsfragments/README.md for instructions 72 | package = "geth" 73 | filename = "CHANGELOG.rst" 74 | directory = "newsfragments" 75 | underlines = ["-", "~", "^"] 76 | title_format = "py-geth v{version} ({project_date})" 77 | issue_format = "`#{issue} `__" 78 | 79 | [[tool.towncrier.type]] 80 | directory = "breaking" 81 | name = "Breaking Changes" 82 | showcontent = true 83 | 84 | [[tool.towncrier.type]] 85 | directory = "bugfix" 86 | name = "Bugfixes" 87 | showcontent = true 88 | 89 | [[tool.towncrier.type]] 90 | directory = "deprecation" 91 | name = "Deprecations" 92 | showcontent = true 93 | 94 | [[tool.towncrier.type]] 95 | directory = "docs" 96 | name = "Improved Documentation" 97 | showcontent = true 98 | 99 | [[tool.towncrier.type]] 100 | directory = "feature" 101 | name = "Features" 102 | showcontent = true 103 | 104 | [[tool.towncrier.type]] 105 | directory = "internal" 106 | name = "Internal Changes - for py-geth Contributors" 107 | showcontent = true 108 | 109 | [[tool.towncrier.type]] 110 | directory = "misc" 111 | name = "Miscellaneous Changes" 112 | showcontent = false 113 | 114 | [[tool.towncrier.type]] 115 | directory = "performance" 116 | name = "Performance Improvements" 117 | showcontent = true 118 | 119 | [[tool.towncrier.type]] 120 | directory = "removal" 121 | name = "Removals" 122 | showcontent = true 123 | -------------------------------------------------------------------------------- /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 | "bumpversion>=0.5.3", 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>=21,<22", 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 'bumpversion' utility. 40 | version="5.1.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=["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 | ], 76 | ) 77 | -------------------------------------------------------------------------------- /tests/core/accounts/conftest.py: -------------------------------------------------------------------------------- 1 | import os 2 | 3 | import pytest 4 | 5 | PROJECTS_DIR = os.path.join(os.path.abspath(os.path.dirname(__file__)), "projects") 6 | 7 | 8 | @pytest.fixture 9 | def one_account_data_dir(): 10 | data_dir = os.path.join(PROJECTS_DIR, "test-01") 11 | return data_dir 12 | 13 | 14 | @pytest.fixture 15 | def three_account_data_dir(): 16 | data_dir = os.path.join(PROJECTS_DIR, "test-02") 17 | return data_dir 18 | 19 | 20 | @pytest.fixture 21 | def no_account_data_dir(): 22 | data_dir = os.path.join(PROJECTS_DIR, "test-03") 23 | return data_dir 24 | -------------------------------------------------------------------------------- /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 | from geth import ( 2 | DevGethProcess, 3 | ) 4 | 5 | 6 | def test_with_no_overrides(base_dir): 7 | geth = DevGethProcess("testing", base_dir=base_dir) 8 | 9 | geth.start() 10 | 11 | assert geth.is_running 12 | assert geth.is_alive 13 | 14 | geth.stop() 15 | 16 | assert geth.is_stopped 17 | 18 | 19 | def test_dev_geth_process_generates_accounts(base_dir): 20 | geth = DevGethProcess("testing", base_dir=base_dir) 21 | assert len(set(geth.accounts)) == 1 22 | -------------------------------------------------------------------------------- /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 threading 2 | 3 | import pytest 4 | 5 | from geth import ( 6 | DevGethProcess, 7 | ) 8 | from geth.mixins import ( 9 | LoggingMixin, 10 | ) 11 | 12 | _errors = [] 13 | 14 | 15 | @pytest.fixture(autouse=True) 16 | def fail_from_errors_on_other_threads(): 17 | """ 18 | Causes errors when `LoggingMixin` is improperly implemented. 19 | Useful for preventing false-positives in logging-based tests. 20 | """ 21 | 22 | def pytest_excepthook(*args, **kwargs): 23 | _errors.extend(args) 24 | 25 | threading.excepthook = pytest_excepthook 26 | 27 | yield 28 | 29 | if _errors: 30 | caught_errors_str = ", ".join([str(err) for err in _errors]) 31 | pytest.fail(f"Caught exceptions from other threads:\n{caught_errors_str}") 32 | 33 | 34 | class WithLogging(LoggingMixin, DevGethProcess): 35 | pass 36 | 37 | 38 | def test_with_logging(base_dir, caplog): 39 | test_stdout_path = f"{base_dir}/testing/stdoutlogs.log" 40 | test_stderr_path = f"{base_dir}/testing/stderrlogs.log" 41 | 42 | geth = WithLogging( 43 | "testing", 44 | base_dir=base_dir, 45 | stdout_logfile_path=test_stdout_path, 46 | stderr_logfile_path=test_stderr_path, 47 | ) 48 | 49 | geth.start() 50 | 51 | assert geth.is_running 52 | assert geth.is_alive 53 | 54 | stdout_logger_info = geth.stdout_callbacks[0] 55 | stderr_logger_info = geth.stderr_callbacks[0] 56 | 57 | stdout_logger_info("test_out") 58 | stderr_logger_info("test_err") 59 | 60 | with open(test_stdout_path) as out_log_file: 61 | line = out_log_file.readline() 62 | assert line == "test_out\n" 63 | 64 | with open(test_stderr_path) as err_log_file: 65 | line = err_log_file.readline() 66 | assert line == "test_err\n" 67 | 68 | geth.stop() 69 | -------------------------------------------------------------------------------- /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 os 2 | 3 | import pytest 4 | 5 | from geth.chain import ( 6 | is_live_chain, 7 | ) 8 | 9 | 10 | @pytest.mark.parametrize( 11 | "platform,data_dir,should_be_live", 12 | ( 13 | ("darwin", "~", False), 14 | ("darwin", "~/Library/Ethereum", True), 15 | ("linux2", "~", False), 16 | ("linux2", "~/.ethereum", True), 17 | ), 18 | ) 19 | def test_is_live_chain(monkeypatch, platform, data_dir, should_be_live): 20 | monkeypatch.setattr("sys.platform", platform) 21 | if platform == "win32": 22 | monkeypatch.setattr("os.path.sep", "\\") 23 | 24 | expanded_data_dir = os.path.expanduser(data_dir) 25 | relative_data_dir = os.path.relpath(expanded_data_dir) 26 | 27 | assert is_live_chain(data_dir) is should_be_live 28 | assert is_live_chain(expanded_data_dir) is should_be_live 29 | assert is_live_chain(relative_data_dir) is should_be_live 30 | -------------------------------------------------------------------------------- /tests/core/utility/test_is_sepolia_chain.py: -------------------------------------------------------------------------------- 1 | import os 2 | 3 | import pytest 4 | 5 | from geth.chain import ( 6 | is_sepolia_chain, 7 | ) 8 | 9 | 10 | @pytest.mark.parametrize( 11 | "platform,data_dir,should_be_sepolia", 12 | ( 13 | ("darwin", "~", False), 14 | ("darwin", "~/Library/Ethereum/sepolia", True), 15 | ("linux2", "~", False), 16 | ("linux2", "~/.ethereum/sepolia", True), 17 | ), 18 | ) 19 | def test_is_sepolia_chain(monkeypatch, platform, data_dir, should_be_sepolia): 20 | monkeypatch.setattr("sys.platform", platform) 21 | if platform == "win32": 22 | monkeypatch.setattr("os.path.sep", "\\") 23 | 24 | expanded_data_dir = os.path.expanduser(data_dir) 25 | relative_data_dir = os.path.relpath(expanded_data_dir) 26 | 27 | assert is_sepolia_chain(data_dir) is should_be_sepolia 28 | assert is_sepolia_chain(expanded_data_dir) is should_be_sepolia 29 | assert is_sepolia_chain(relative_data_dir) is should_be_sepolia 30 | -------------------------------------------------------------------------------- /tests/core/utility/test_validation.py: -------------------------------------------------------------------------------- 1 | from __future__ import ( 2 | annotations, 3 | ) 4 | 5 | import sys 6 | from typing import ( 7 | get_type_hints, 8 | ) 9 | 10 | import pytest 11 | 12 | from geth.exceptions import ( 13 | PyGethValueError, 14 | ) 15 | from geth.types import ( 16 | GenesisDataTypedDict, 17 | ) 18 | from geth.utils.validation import ( 19 | GenesisData, 20 | fill_default_genesis_data, 21 | validate_genesis_data, 22 | validate_geth_kwargs, 23 | ) 24 | 25 | 26 | @pytest.mark.parametrize( 27 | "geth_kwargs", 28 | [ 29 | { 30 | "data_dir": "/tmp", 31 | "network_id": "123", 32 | "rpc_port": "1234", 33 | "dev_mode": True, 34 | }, 35 | ], 36 | ) 37 | def test_validate_geth_kwargs_good(geth_kwargs): 38 | assert validate_geth_kwargs(geth_kwargs) is None 39 | 40 | 41 | @pytest.mark.parametrize( 42 | "geth_kwargs", 43 | [ 44 | { 45 | "data_dir": "/tmp", 46 | "network_id": 123, 47 | "dev_mode": "abc", 48 | } 49 | ], 50 | ) 51 | def test_validate_geth_kwargs_bad(geth_kwargs): 52 | with pytest.raises(PyGethValueError): 53 | validate_geth_kwargs(geth_kwargs) 54 | 55 | 56 | @pytest.mark.parametrize( 57 | "genesis_data", 58 | [ 59 | { 60 | "difficulty": "0x00012131", 61 | "nonce": "abc", 62 | "timestamp": "1234", 63 | } 64 | ], 65 | ) 66 | def test_validate_genesis_data_good(genesis_data): 67 | assert validate_genesis_data(genesis_data) is None 68 | 69 | 70 | @pytest.mark.parametrize( 71 | "genesis_data", 72 | [ 73 | { 74 | "difficulty": "0x00012131", 75 | "nonce": "abc", 76 | "cats": "1234", 77 | }, 78 | { 79 | "difficulty": "0x00012131", 80 | "nonce": "abc", 81 | "config": "1234", 82 | }, 83 | { 84 | "difficulty": "0x00012131", 85 | "nonce": "abc", 86 | "config": None, 87 | }, 88 | "kangaroo", 89 | ], 90 | ) 91 | def test_validate_genesis_data_bad(genesis_data): 92 | with pytest.raises(PyGethValueError): 93 | validate_genesis_data(genesis_data) 94 | 95 | 96 | @pytest.mark.parametrize( 97 | "genesis_data,expected", 98 | [ 99 | ( 100 | { 101 | "difficulty": "0x00012131", 102 | "nonce": "abc", 103 | "timestamp": "1234", 104 | }, 105 | { 106 | "alloc": {}, 107 | "baseFeePerGas": "0x0", 108 | "blobGasUsed": "0x0", 109 | "coinbase": "0x3333333333333333333333333333333333333333", 110 | "config": { 111 | "chainId": 0, 112 | "ethash": {}, 113 | "homesteadBlock": 0, 114 | "daoForkBlock": 0, 115 | "daoForkSupport": True, 116 | "eip150Block": 0, 117 | "eip155Block": 0, 118 | "eip158Block": 0, 119 | "byzantiumBlock": 0, 120 | "constantinopleBlock": 0, 121 | "petersburgBlock": 0, 122 | "istanbulBlock": 0, 123 | "berlinBlock": 0, 124 | "londonBlock": 0, 125 | "arrowGlacierBlock": 0, 126 | "grayGlacierBlock": 0, 127 | "terminalTotalDifficulty": 0, 128 | "terminalTotalDifficultyPassed": True, 129 | "shanghaiTime": 0, 130 | "cancunTime": 0, 131 | }, 132 | "difficulty": "0x00012131", 133 | "excessBlobGas": "0x0", 134 | "extraData": "0x0000000000000000000000000000000000000000000000000000000000000000", # noqa: E501 135 | "gasLimit": "0x47e7c4", 136 | "gasUsed": "0x0", 137 | "mixHash": "0x0000000000000000000000000000000000000000000000000000000000000000", # noqa: E501 138 | "nonce": "abc", 139 | "number": "0x0", 140 | "parentHash": "0x0000000000000000000000000000000000000000000000000000000000000000", # noqa: E501 141 | "timestamp": "1234", 142 | }, 143 | ), 144 | ( 145 | { 146 | "difficulty": "0x00012131", 147 | "nonce": "abc", 148 | "config": { 149 | "homesteadBlock": 5, 150 | "daoForkBlock": 1, 151 | "daoForkSupport": False, 152 | "eip150Block": 27777777, 153 | "eip155Block": 99, 154 | "eip158Block": 32, 155 | }, 156 | }, 157 | { 158 | "alloc": {}, 159 | "baseFeePerGas": "0x0", 160 | "blobGasUsed": "0x0", 161 | "coinbase": "0x3333333333333333333333333333333333333333", 162 | "config": { 163 | "chainId": 0, 164 | "ethash": {}, 165 | "homesteadBlock": 5, 166 | "daoForkBlock": 1, 167 | "daoForkSupport": False, 168 | "eip150Block": 27777777, 169 | "eip155Block": 99, 170 | "eip158Block": 32, 171 | "byzantiumBlock": 0, 172 | "constantinopleBlock": 0, 173 | "petersburgBlock": 0, 174 | "istanbulBlock": 0, 175 | "berlinBlock": 0, 176 | "londonBlock": 0, 177 | "arrowGlacierBlock": 0, 178 | "grayGlacierBlock": 0, 179 | "terminalTotalDifficulty": 0, 180 | "terminalTotalDifficultyPassed": True, 181 | "shanghaiTime": 0, 182 | "cancunTime": 0, 183 | }, 184 | "difficulty": "0x00012131", 185 | "excessBlobGas": "0x0", 186 | "extraData": "0x0000000000000000000000000000000000000000000000000000000000000000", # noqa: E501 187 | "gasLimit": "0x47e7c4", 188 | "gasUsed": "0x0", 189 | "mixHash": "0x0000000000000000000000000000000000000000000000000000000000000000", # noqa: E501 190 | "nonce": "abc", 191 | "number": "0x0", 192 | "parentHash": "0x0000000000000000000000000000000000000000000000000000000000000000", # noqa: E501 193 | "timestamp": "0x0", 194 | }, 195 | ), 196 | ], 197 | ) 198 | def test_fill_default_genesis_data_good(genesis_data, expected): 199 | genesis_data_td = GenesisDataTypedDict(**genesis_data) 200 | filled_genesis_data = fill_default_genesis_data(genesis_data_td).model_dump() 201 | assert filled_genesis_data == expected 202 | 203 | 204 | @pytest.mark.parametrize( 205 | "genesis_data,expected_exception,expected_message", 206 | [ 207 | ( 208 | { 209 | "difficulty": "0x00012131", 210 | "nonce": "abc", 211 | "timestamp": 1234, 212 | }, 213 | PyGethValueError, 214 | "genesis_data validation failed while filling defaults: ", 215 | ), 216 | ( 217 | { 218 | "difficulty": "0x00012131", 219 | "nonce": "abc", 220 | "config": { 221 | "homesteadBlock": 5, 222 | "daoForkBlock": "beep", 223 | "daoForkSupport": False, 224 | "eip150Block": 27777777, 225 | "eip155Block": 99, 226 | "eip158Block": 32, 227 | }, 228 | }, 229 | PyGethValueError, 230 | "genesis_data validation failed while filling config defaults: ", 231 | ), 232 | ( 233 | "abc123", 234 | PyGethValueError, 235 | "error while filling default genesis_data: ", 236 | ), 237 | ( 238 | {"difficulty": "0x00012131", "nonce": "abc", "config": ["beep"]}, 239 | PyGethValueError, 240 | "genesis_data validation failed while filling defaults: ", 241 | ), 242 | ], 243 | ) 244 | def test_fill_default_genesis_data_bad( 245 | genesis_data, expected_exception, expected_message 246 | ): 247 | with pytest.raises(expected_exception) as excinfo: 248 | fill_default_genesis_data(genesis_data) 249 | assert str(excinfo.value).startswith(expected_message) 250 | 251 | 252 | @pytest.mark.skipif(sys.version_info < (3, 9), reason="get_type_hints requires >=py39") 253 | @pytest.mark.parametrize( 254 | "model, typed_dict", 255 | [ 256 | (GenesisData, GenesisDataTypedDict), 257 | ], 258 | ) 259 | def test_model_fields_match_typed_dict(model, typed_dict): 260 | # Get the fields and types from the Pydantic model 261 | model_fields = get_type_hints(model) 262 | assert len(model_fields) > 0, "Model has no fields" 263 | 264 | # Get the fields and types from the TypedDict 265 | typed_dict_fields = get_type_hints(typed_dict) 266 | assert len(typed_dict_fields) > 0, "TypedDict has no fields" 267 | assert len(typed_dict_fields) == len(model_fields), "Field counts do not match" 268 | 269 | # Verify that the fields match 270 | assert model_fields == typed_dict_fields, "Fields do not match" 271 | -------------------------------------------------------------------------------- /tests/core/waiting/conftest.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Ere11i/py-geth/152d6c98d39d705fd98b96d1f8f52187c67fb968/tests/core/waiting/conftest.py -------------------------------------------------------------------------------- /tests/core/waiting/test_waiting_for_ipc_socket.py: -------------------------------------------------------------------------------- 1 | from flaky import ( 2 | flaky, 3 | ) 4 | import pytest 5 | 6 | from geth import ( 7 | DevGethProcess, 8 | ) 9 | from geth.utils.timeout import ( 10 | Timeout, 11 | ) 12 | 13 | 14 | def test_waiting_for_ipc_socket(base_dir): 15 | with DevGethProcess("testing", base_dir=base_dir) as geth: 16 | assert geth.is_running 17 | geth.wait_for_ipc(timeout=20) 18 | 19 | 20 | @flaky(max_runs=3) 21 | def test_timeout_waiting_for_ipc_socket(base_dir): 22 | with DevGethProcess("testing", base_dir=base_dir) as geth: 23 | assert geth.is_running 24 | with pytest.raises(Timeout): 25 | geth.wait_for_ipc(timeout=0.01) 26 | -------------------------------------------------------------------------------- /tests/core/waiting/test_waiting_for_rpc_connection.py: -------------------------------------------------------------------------------- 1 | from flaky import ( 2 | flaky, 3 | ) 4 | import pytest 5 | 6 | from geth import ( 7 | DevGethProcess, 8 | ) 9 | from geth.utils.timeout import ( 10 | Timeout, 11 | ) 12 | 13 | 14 | def test_waiting_for_rpc_connection(base_dir): 15 | with DevGethProcess("testing", base_dir=base_dir) as geth: 16 | assert geth.is_running 17 | geth.wait_for_rpc(timeout=20) 18 | 19 | 20 | @flaky(max_runs=3) 21 | def test_timeout_waiting_for_rpc_connection(base_dir): 22 | with DevGethProcess("testing", base_dir=base_dir) as geth: 23 | with pytest.raises(Timeout): 24 | geth.wait_for_rpc(timeout=0.1) 25 | -------------------------------------------------------------------------------- /tests/installation/test_geth_installation.py: -------------------------------------------------------------------------------- 1 | import os 2 | 3 | import pytest 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}-lint 4 | py{38,39,310,311,312}-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 \ 7 | } 8 | py{38,39,310,311,312}-wheel 9 | windows-wheel 10 | 11 | [flake8] 12 | exclude=venv*,.tox,docs,build 13 | extend-ignore=E203 14 | max-line-length=88 15 | 16 | [testenv] 17 | usedevelop=True 18 | commands= 19 | install-geth: {[common_geth_installation_and_check]commands} 20 | passenv= 21 | GETH_VERSION 22 | GOROOT 23 | GOPATH 24 | HOME 25 | PATH 26 | setenv= 27 | installation: GETH_RUN_INSTALL_TESTS=enabled 28 | deps= 29 | .[test] 30 | install-geth: {[common_geth_installation_and_check]deps} 31 | basepython= 32 | windows-wheel: python 33 | py38: python3.8 34 | py39: python3.9 35 | py310: python3.10 36 | py311: python3.11 37 | py312: python3.12 38 | allowlist_externals=bash,make,pre-commit 39 | 40 | [common_geth_installation_and_check] 41 | deps=.[dev,test] 42 | commands= 43 | bash ./.circleci/install_geth.sh 44 | pytest {posargs:tests/core} 45 | pytest {posargs:-s tests/installation} 46 | 47 | [testenv:py{38,39,310,311,312}-lint] 48 | deps=pre-commit 49 | extras= 50 | dev 51 | commands= 52 | pre-commit install 53 | pre-commit run --all-files --show-diff-on-failure 54 | 55 | [testenv:py{38,39,310,311,312}-wheel] 56 | deps= 57 | wheel 58 | build[virtualenv] 59 | allowlist_externals= 60 | /bin/rm 61 | /bin/bash 62 | commands= 63 | python -m pip install --upgrade pip 64 | /bin/rm -rf build dist 65 | python -m build 66 | /bin/bash -c 'python -m pip install --upgrade "$(ls dist/py_geth-*-py3-none-any.whl)" --progress-bar off' 67 | python -c "import geth" 68 | skip_install=true 69 | 70 | [testenv:windows-wheel] 71 | deps= 72 | wheel 73 | build[virtualenv] 74 | allowlist_externals= 75 | bash.exe 76 | commands= 77 | python --version 78 | python -m pip install --upgrade pip 79 | bash.exe -c "rm -rf build dist" 80 | python -m build 81 | bash.exe -c 'python -m pip install --upgrade "$(ls dist/py_geth-*-py3-none-any.whl)" --progress-bar off' 82 | python -c "import geth" 83 | skip_install=true 84 | -------------------------------------------------------------------------------- /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 | 29 | currently_supported_geth_versions = [] 30 | with open("tox.ini") as tox_ini: 31 | for line_number, line in enumerate(tox_ini, start=1): 32 | if line_number == 15: 33 | # supported versions are near the beginning of the tox.ini file 34 | break 35 | if "install-geth" in line: 36 | line.replace(" ", "") 37 | circleci_python_versions = line[ 38 | line.find("py{") + 3 : line.find("}") 39 | ].split(",") 40 | if GETH_VERSION_REGEX.search(line): 41 | line = line.replace(" ", "") # clean space 42 | line = line.replace("\n", "") # remove trailing indent 43 | line = line.replace("\\", "") # remove the multiline backslash 44 | line = line if line[-1] != "," else line[:-1] 45 | for version in line.split(","): 46 | currently_supported_geth_versions.append(version.strip()) 47 | LATEST_SUPPORTED_GETH_VERSION = currently_supported_geth_versions[-1] 48 | LATEST_PYTHON_VERSION = circleci_python_versions[-1] 49 | 50 | # .circleci/config.yml pattern 51 | CIRCLE_CI_PATTERN = { 52 | "jobs": "", 53 | "workflow_test_jobs": "", 54 | } 55 | # geth/install.py pattern 56 | GETH_INSTALL_PATTERN = { 57 | "versions": "", 58 | "installs": "", 59 | "version<->install": "", 60 | } 61 | 62 | user_provided_versions = sys.argv[1:] 63 | normalized_user_versions = [] 64 | for index, user_provided_version in enumerate(user_provided_versions): 65 | if "v" not in user_provided_version: 66 | user_provided_version = f"v{user_provided_version}" 67 | normalized_user_versions.append(user_provided_version) 68 | 69 | if ( 70 | not GETH_VERSION_REGEX.match(user_provided_version) 71 | or len(user_provided_versions) == 0 72 | ): 73 | raise ValueError("missing or improper format for provided geth versions") 74 | 75 | if user_provided_version in currently_supported_geth_versions: 76 | raise ValueError( 77 | f"provided version is already supported: {user_provided_version}" 78 | ) 79 | latest_user_provided_version = normalized_user_versions[-1] 80 | 81 | # set up .circleci/config.yml pattern 82 | if index > 0: 83 | CIRCLE_CI_PATTERN["workflow_test_jobs"] += "\n" 84 | 85 | for py_version in circleci_python_versions: 86 | py_version_decimal = f"{py_version[0]}.{py_version[1:]}" 87 | CIRCLE_CI_PATTERN["jobs"] += ( 88 | f" py{py_version}-install-geth-{user_provided_version}:\n" 89 | f" <<: *common_go_steps\n" 90 | " docker:\n" 91 | f" - image: cimg/python:{py_version_decimal}\n" 92 | " environment:\n" 93 | f" GETH_VERSION: {user_provided_version.replace('_', '.')}\n" 94 | f" TOXENV: py{py_version}-install-geth-{user_provided_version}\n" 95 | ) 96 | 97 | CIRCLE_CI_PATTERN[ 98 | "workflow_test_jobs" 99 | ] += f"\n - py{py_version}-install-geth-{user_provided_version}" 100 | 101 | # set up geth/install.py pattern 102 | user_version_upper = user_provided_version.upper() 103 | user_version_period = user_provided_version.replace("_", ".") 104 | GETH_INSTALL_PATTERN[ 105 | "versions" 106 | ] += f'{user_version_upper} = "{user_version_period}"\n' 107 | 108 | user_version_install = f"install_v{user_version_upper[1:]}" 109 | GETH_INSTALL_PATTERN["installs"] += ( 110 | f"{user_version_install} = functools.partial(" 111 | f"install_from_source_code_release, {user_version_upper})\n" 112 | ) 113 | GETH_INSTALL_PATTERN[ 114 | "version<->install" 115 | ] += f" {user_version_upper}: {user_version_install},\n" 116 | 117 | # update .circleci/config.yml versions 118 | with fileinput.FileInput(".circleci/config.yml", inplace=True) as cci_config: 119 | for line in cci_config: 120 | if ( 121 | f"TOXENV: py{LATEST_PYTHON_VERSION}-install-geth-{LATEST_SUPPORTED_GETH_VERSION}" # noqa: E501 122 | ) in line: 123 | print( 124 | f" TOXENV: py{LATEST_PYTHON_VERSION}-install-geth-" 125 | f"{LATEST_SUPPORTED_GETH_VERSION}\n" + CIRCLE_CI_PATTERN["jobs"], 126 | end="", 127 | ) 128 | elif ( 129 | f"- py{LATEST_PYTHON_VERSION}-install-geth-{LATEST_SUPPORTED_GETH_VERSION}" 130 | ) in line: 131 | print( 132 | f" - py{LATEST_PYTHON_VERSION}-install-geth-{LATEST_SUPPORTED_GETH_VERSION}\n" # noqa: E501 133 | + CIRCLE_CI_PATTERN["workflow_test_jobs"] 134 | ) 135 | else: 136 | print(line, end="") 137 | 138 | # update geth/install.py versions 139 | with fileinput.FileInput("geth/install.py", inplace=True) as geth_install: 140 | latest_supported_upper = LATEST_SUPPORTED_GETH_VERSION.upper() 141 | latest_supported_period = LATEST_SUPPORTED_GETH_VERSION.replace("_", ".") 142 | latest_version_install = f"install_v{latest_supported_upper[1:]}" 143 | for line in geth_install: 144 | if f'{latest_supported_upper} = "{latest_supported_period}"' in line: 145 | print( 146 | f'{latest_supported_upper} = "{latest_supported_period}"\n' 147 | + GETH_INSTALL_PATTERN["versions"], 148 | end="", 149 | ) 150 | elif f"{latest_version_install} = functools.partial" in line: 151 | print( 152 | f"{latest_version_install} = functools.partial(" 153 | f"install_from_source_code_release, {latest_supported_upper})\n" 154 | + GETH_INSTALL_PATTERN["installs"], 155 | end="", 156 | ) 157 | elif (f"{latest_supported_upper}: {latest_version_install}") in line: 158 | print( 159 | f" {latest_supported_upper}: {latest_version_install},\n" 160 | + GETH_INSTALL_PATTERN["version<->install"], 161 | end="", 162 | ) 163 | else: 164 | print(line, end="") 165 | 166 | # update versions in readme to the latest supported version 167 | with fileinput.FileInput("README.md", inplace=True) as readme: 168 | latest_supported_period = LATEST_SUPPORTED_GETH_VERSION.replace("_", ".") 169 | latest_user_provided_period = latest_user_provided_version.replace("_", ".") 170 | for line in readme: 171 | print( 172 | line.replace(latest_supported_period, latest_user_provided_period), 173 | end="", 174 | ) 175 | 176 | # update tox.ini versions 177 | with fileinput.FileInput("tox.ini", inplace=True) as tox_ini: 178 | all_versions = currently_supported_geth_versions + normalized_user_versions 179 | write_versions = False 180 | for line in tox_ini: 181 | if write_versions: 182 | print(" ", end="") 183 | for num, v in enumerate(all_versions, start=1): 184 | if num == len(all_versions): 185 | print(f"{v} \\") 186 | elif not num % 7: 187 | print(f"{v}, \\\n ", end="") 188 | else: 189 | print(v, end=", ") 190 | write_versions = False 191 | else: 192 | if "install-geth-{" in line: 193 | write_versions = True 194 | if GETH_VERSION_REGEX.search(line): 195 | # clean up the older version lines 196 | print(end="") 197 | else: 198 | print(line, end="") 199 | --------------------------------------------------------------------------------