py-geth 4.2.0__py3-none-any.whl → 5.2.0__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
geth/__init__.py CHANGED
@@ -14,10 +14,20 @@ from .mixins import (
14
14
  )
15
15
  from .process import (
16
16
  DevGethProcess,
17
- LiveGethProcess,
18
17
  MainnetGethProcess,
19
- RopstenGethProcess,
18
+ SepoliaGethProcess,
20
19
  TestnetGethProcess,
21
20
  )
22
21
 
23
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
+ )
geth/accounts.py CHANGED
@@ -1,6 +1,24 @@
1
+ from __future__ import (
2
+ annotations,
3
+ )
4
+
1
5
  import os
2
6
  import re
3
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
+
4
22
  from .utils.proc import (
5
23
  format_error_message,
6
24
  )
@@ -9,29 +27,36 @@ from .wrapper import (
9
27
  )
10
28
 
11
29
 
12
- def get_accounts(data_dir, **geth_kwargs):
30
+ def get_accounts(
31
+ **geth_kwargs: Unpack[GethKwargsTypedDict],
32
+ ) -> tuple[str, ...] | tuple[()]:
13
33
  """
14
34
  Returns all geth accounts as tuple of hex encoded strings
15
35
 
16
- >>> geth_accounts()
36
+ >>> get_accounts(data_dir='some/data/dir')
17
37
  ... ('0x...', '0x...')
18
38
  """
19
- command, proc = spawn_geth(
20
- dict(data_dir=data_dir, suffix_args=["account", "list"], **geth_kwargs)
21
- )
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)
22
47
  stdoutdata, stderrdata = proc.communicate()
23
48
 
24
49
  if proc.returncode:
25
- if "no keys in store" in stderrdata.decode("utf-8"):
50
+ if "no keys in store" in stderrdata.decode():
26
51
  return tuple()
27
52
  else:
28
- raise ValueError(
53
+ raise PyGethValueError(
29
54
  format_error_message(
30
55
  "Error trying to list accounts",
31
56
  command,
32
57
  proc.returncode,
33
- stdoutdata,
34
- stderrdata,
58
+ stdoutdata.decode(),
59
+ stderrdata.decode(),
35
60
  )
36
61
  )
37
62
  accounts = parse_geth_accounts(stdoutdata)
@@ -41,8 +66,8 @@ def get_accounts(data_dir, **geth_kwargs):
41
66
  account_regex = re.compile(b"([a-f0-9]{40})")
42
67
 
43
68
 
44
- def create_new_account(data_dir, password, **geth_kwargs):
45
- """
69
+ def create_new_account(**geth_kwargs: Unpack[GethKwargsTypedDict]) -> str:
70
+ r"""
46
71
  Creates a new Ethereum account on geth.
47
72
 
48
73
  This is useful for testing when you want to stress
@@ -57,11 +82,11 @@ def create_new_account(data_dir, password, **geth_kwargs):
57
82
  If geth process is already running you can create new
58
83
  accounts using
59
84
  `web3.personal.newAccount()
60
- <https://github.com/ethereum/go-ethereum/wiki/JavaScript-Console#personalnewaccount>_`
61
- RPC API.
85
+ <https://geth.ethereum.org/docs/interacting-with-geth/rpc/ns-personal>_`
62
86
 
87
+ RPC API.
63
88
 
64
- Example py.test fixture for tests:
89
+ Example pytest fixture for tests:
65
90
 
66
91
  .. code-block:: python
67
92
 
@@ -92,59 +117,85 @@ def create_new_account(data_dir, password, **geth_kwargs):
92
117
  account = create_new_account(data_dir, DEFAULT_PASSWORD_PATH)
93
118
  return account
94
119
 
95
- :param data_dir: Geth data fir path - where to keep "keystore" folder
96
- :param password: Path to a file containing the password
97
- for newly created account
98
- :param geth_kwargs: Extra command line arguments password to geth
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
+
99
130
  :return: Account as 0x prefixed hex string
131
+ :rtype: str
100
132
  """
101
- if os.path.exists(password):
102
- geth_kwargs["password"] = password
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")
103
138
 
104
- command, proc = spawn_geth(
105
- dict(data_dir=data_dir, suffix_args=["account", "new"], **geth_kwargs)
106
- )
139
+ password = geth_kwargs.get("password")
107
140
 
108
- if os.path.exists(password):
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):
109
155
  stdoutdata, stderrdata = proc.communicate()
110
156
  else:
111
157
  stdoutdata, stderrdata = proc.communicate(b"\n".join((password, password)))
112
158
 
113
159
  if proc.returncode:
114
- raise ValueError(
160
+ raise PyGethValueError(
115
161
  format_error_message(
116
162
  "Error trying to create a new account",
117
163
  command,
118
164
  proc.returncode,
119
- stdoutdata,
120
- stderrdata,
165
+ stdoutdata.decode(),
166
+ stderrdata.decode(),
121
167
  )
122
168
  )
123
169
 
124
170
  match = account_regex.search(stdoutdata)
125
171
  if not match:
126
- raise ValueError(
172
+ raise PyGethValueError(
127
173
  format_error_message(
128
174
  "Did not find an address in process output",
129
175
  command,
130
176
  proc.returncode,
131
- stdoutdata,
132
- stderrdata,
177
+ stdoutdata.decode(),
178
+ stderrdata.decode(),
133
179
  )
134
180
  )
135
181
 
136
- return b"0x" + match.groups()[0]
182
+ return "0x" + match.groups()[0].decode()
183
+
137
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")
138
188
 
139
- def ensure_account_exists(data_dir, **geth_kwargs):
140
- accounts = get_accounts(data_dir, **geth_kwargs)
189
+ validate_geth_kwargs(geth_kwargs)
190
+ accounts = get_accounts(**geth_kwargs)
141
191
  if not accounts:
142
- account = create_new_account(data_dir, **geth_kwargs)
192
+ account = create_new_account(**geth_kwargs)
143
193
  else:
144
194
  account = accounts[0]
145
195
  return account
146
196
 
147
197
 
148
- def parse_geth_accounts(raw_accounts_output):
198
+ def parse_geth_accounts(raw_accounts_output: bytes) -> tuple[str, ...]:
149
199
  accounts = account_regex.findall(raw_accounts_output)
150
- return tuple(b"0x" + account for account in accounts)
200
+ accounts_set = set(accounts) # remove duplicates
201
+ return tuple("0x" + account.decode() for account in accounts_set)
geth/chain.py CHANGED
@@ -1,7 +1,23 @@
1
+ from __future__ import (
2
+ annotations,
3
+ )
4
+
1
5
  import json
2
6
  import os
7
+ import subprocess
3
8
  import sys
4
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
+
5
21
  from .utils.encoding import (
6
22
  force_obj_to_text,
7
23
  )
@@ -9,12 +25,16 @@ from .utils.filesystem import (
9
25
  ensure_path_exists,
10
26
  is_same_path,
11
27
  )
28
+ from .utils.validation import (
29
+ fill_default_genesis_data,
30
+ validate_genesis_data,
31
+ )
12
32
  from .wrapper import (
13
- spawn_geth,
33
+ get_geth_binary_path,
14
34
  )
15
35
 
16
36
 
17
- def get_live_data_dir():
37
+ def get_live_data_dir() -> str:
18
38
  """
19
39
  `py-geth` needs a base directory to store it's chain data. By default this is
20
40
  the directory that `geth` uses as it's `datadir`.
@@ -46,124 +66,89 @@ def get_live_data_dir():
46
66
  )
47
67
 
48
68
  else:
49
- raise ValueError(
50
- (
51
- f"Unsupported platform: '{sys.platform}'. Only darwin/linux2/win32 are"
52
- " supported. You must specify the geth datadir manually"
53
- )
69
+ raise PyGethValueError(
70
+ f"Unsupported platform: '{sys.platform}'. Only darwin/linux2/win32 are"
71
+ " supported. You must specify the geth datadir manually"
54
72
  )
55
73
  return data_dir
56
74
 
57
75
 
58
- def get_ropsten_data_dir():
76
+ def get_sepolia_data_dir() -> str:
59
77
  return os.path.abspath(
60
78
  os.path.expanduser(
61
79
  os.path.join(
62
80
  get_live_data_dir(),
63
- "ropsten",
81
+ "sepolia",
64
82
  )
65
83
  )
66
84
  )
67
85
 
68
86
 
69
- def get_default_base_dir():
87
+ def get_default_base_dir() -> str:
70
88
  return get_live_data_dir()
71
89
 
72
90
 
73
- def get_chain_data_dir(base_dir, name):
91
+ def get_chain_data_dir(base_dir: str, name: str) -> str:
74
92
  data_dir = os.path.abspath(os.path.join(base_dir, name))
75
93
  ensure_path_exists(data_dir)
76
94
  return data_dir
77
95
 
78
96
 
79
- def get_genesis_file_path(data_dir):
97
+ def get_genesis_file_path(data_dir: str) -> str:
80
98
  return os.path.join(data_dir, "genesis.json")
81
99
 
82
100
 
83
- def is_live_chain(data_dir):
101
+ def is_live_chain(data_dir: str) -> bool:
84
102
  return is_same_path(data_dir, get_live_data_dir())
85
103
 
86
104
 
87
- def is_ropsten_chain(data_dir):
88
- return is_same_path(data_dir, get_ropsten_data_dir())
105
+ def is_sepolia_chain(data_dir: str) -> bool:
106
+ return is_same_path(data_dir, get_sepolia_data_dir())
89
107
 
90
108
 
91
109
  def write_genesis_file(
92
- genesis_file_path,
93
- overwrite=False,
94
- nonce="0xdeadbeefdeadbeef",
95
- timestamp="0x0",
96
- parentHash="0x0000000000000000000000000000000000000000000000000000000000000000",
97
- extraData=None,
98
- gasLimit="0x47d5cc",
99
- difficulty="0x01",
100
- mixhash="0x0000000000000000000000000000000000000000000000000000000000000000",
101
- coinbase="0x3333333333333333333333333333333333333333",
102
- alloc=None,
103
- config=None,
104
- clique_period: int = 5,
105
- clique_epoch: int = 30000,
106
- ):
110
+ genesis_file_path: str,
111
+ overwrite: bool = False,
112
+ **genesis_data: Unpack[GenesisDataTypedDict],
113
+ ) -> None:
107
114
  if os.path.exists(genesis_file_path) and not overwrite:
108
- raise ValueError(
109
- "Genesis file already present. call with `overwrite=True` to overwrite this file" # noqa: E501
115
+ raise PyGethValueError(
116
+ "Genesis file already present. Call with "
117
+ "`overwrite=True` to overwrite this file"
110
118
  )
111
119
 
112
- if alloc is None:
113
- alloc = {}
114
-
115
- if config is None:
116
- config = {
117
- "homesteadBlock": 0,
118
- "eip150Block": 0,
119
- "eip155Block": 0,
120
- "eip158Block": 0,
121
- "byzantiumBlock": 0,
122
- "constantinopleBlock": 0,
123
- "petersburgBlock": 0,
124
- "istanbulBlock": 0,
125
- "berlinBlock": 0,
126
- "londonBlock": 0,
127
- "shanghaiBlock": 0,
128
- "daoForkBlock": 0,
129
- "daoForSupport": True,
130
- # Using the Ethash consensus algorithm is deprecated
131
- # Instead, use the Clique consensus algorithm
132
- # https://geth.ethereum.org/docs/interface/private-network
133
- "clique": {"period": clique_period, "epoch": clique_epoch},
134
- }
135
-
136
- # Assign a signer (coinbase) to the genesis block for Clique
137
- extraData = (
138
- bytes("0x" + "0" * 64 + coinbase[2:] + "0" * 130, "ascii")
139
- if extraData is None
140
- else extraData
141
- )
142
-
143
- genesis_data = {
144
- "nonce": nonce,
145
- "timestamp": timestamp,
146
- "parentHash": parentHash,
147
- "extraData": extraData,
148
- "gasLimit": gasLimit,
149
- "difficulty": difficulty,
150
- "mixhash": mixhash,
151
- "coinbase": coinbase,
152
- "alloc": alloc,
153
- "config": config,
154
- }
120
+ validate_genesis_data(genesis_data)
121
+ # use GenesisData model to fill defaults
122
+ filled_genesis_data_model = fill_default_genesis_data(genesis_data)
155
123
 
156
124
  with open(genesis_file_path, "w") as genesis_file:
157
- genesis_file.write(json.dumps(force_obj_to_text(genesis_data)))
125
+ genesis_file.write(
126
+ json.dumps(force_obj_to_text(filled_genesis_data_model.model_dump()))
127
+ )
158
128
 
159
129
 
160
- def initialize_chain(genesis_data, data_dir, **geth_kwargs):
130
+ def initialize_chain(genesis_data: GenesisDataTypedDict, data_dir: str) -> None:
131
+ validate_genesis_data(genesis_data)
132
+ # init with genesis.json
161
133
  genesis_file_path = get_genesis_file_path(data_dir)
162
134
  write_genesis_file(genesis_file_path, **genesis_data)
163
- command, proc = spawn_geth(
164
- dict(data_dir=data_dir, suffix_args=["init", genesis_file_path], **geth_kwargs)
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,
165
146
  )
166
- stdoutdata, stderrdata = proc.communicate()
167
-
168
- if proc.returncode:
169
- raise ValueError(f"Error: {stdoutdata + stderrdata}")
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
+ )
geth/exceptions.py CHANGED
@@ -1,23 +1,61 @@
1
- import textwrap
1
+ from __future__ import (
2
+ annotations,
3
+ )
2
4
 
3
- from .utils.encoding import (
4
- force_text,
5
+ import codecs
6
+ import textwrap
7
+ from typing import (
8
+ Any,
5
9
  )
6
10
 
7
11
 
8
- def force_text_maybe(value):
9
- if value is not None:
10
- return force_text(value, "utf8")
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
+ """
11
34
 
35
+ user_message: str | None = None
12
36
 
13
- DEFAULT_MESSAGE = "An error occurred during execution"
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
14
46
 
15
47
 
16
48
  class GethError(Exception):
17
- message = DEFAULT_MESSAGE
49
+ message = "An error occurred during execution"
18
50
 
19
51
  def __init__(
20
- self, command, return_code, stdin_data, stdout_data, stderr_data, message=None
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,
21
59
  ):
22
60
  if message is not None:
23
61
  self.message = message
@@ -27,10 +65,9 @@ class GethError(Exception):
27
65
  self.stderr_data = force_text_maybe(stderr_data)
28
66
  self.stdout_data = force_text_maybe(stdout_data)
29
67
 
30
- def __str__(self):
68
+ def __str__(self) -> str:
31
69
  return textwrap.dedent(
32
- (
33
- f"""
70
+ f"""
34
71
  {self.message}
35
72
  > command: `{" ".join(self.command)}`
36
73
  > return code: `{self.return_code}`
@@ -39,5 +76,41 @@ class GethError(Exception):
39
76
  > stdout:
40
77
  {self.stderr_data}
41
78
  """
42
- )
43
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
geth/genesis.json CHANGED
@@ -1,12 +1,29 @@
1
1
  {
2
- "nonce": "0xdeadbeefdeadbeef",
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",
3
22
  "timestamp": "0x0",
4
23
  "parentHash": "0x0000000000000000000000000000000000000000000000000000000000000000",
5
- "extraData": "0x686f727365",
6
- "gasLimit": "0x2fefd8",
7
- "difficulty": "0x400",
8
- "mixhash": "0x0000000000000000000000000000000000000000000000000000000000000000",
9
- "coinbase": "0x3333333333333333333333333333333333333333",
10
- "alloc": {
11
- }
24
+ "extraData": "0x0000000000000000000000000000000000000000000000000000000000000000",
25
+ "gasLimit": "0x47e7c4",
26
+ "difficulty": "0x0",
27
+ "mixHash": "0x0000000000000000000000000000000000000000000000000000000000000000",
28
+ "alloc": {}
12
29
  }