web3 7.5.0__py3-none-any.whl → 7.6.1__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.
web3/utils/abi.py CHANGED
@@ -1,6 +1,7 @@
1
1
  import functools
2
2
  from typing import (
3
3
  Any,
4
+ Callable,
4
5
  Dict,
5
6
  List,
6
7
  Optional,
@@ -31,6 +32,16 @@ from eth_typing import (
31
32
  HexStr,
32
33
  Primitives,
33
34
  )
35
+ from eth_utils.abi import (
36
+ abi_to_signature,
37
+ event_abi_to_log_topic,
38
+ filter_abi_by_name,
39
+ filter_abi_by_type,
40
+ function_abi_to_4byte_selector,
41
+ get_abi_input_types,
42
+ get_aligned_abi_inputs,
43
+ get_normalized_abi_inputs,
44
+ )
34
45
  from eth_utils.address import (
35
46
  is_binary_address,
36
47
  is_checksum_address,
@@ -47,7 +58,6 @@ from eth_utils.toolz import (
47
58
  )
48
59
  from eth_utils.types import (
49
60
  is_list_like,
50
- is_text,
51
61
  )
52
62
  from hexbytes import (
53
63
  HexBytes,
@@ -55,17 +65,20 @@ from hexbytes import (
55
65
 
56
66
  from web3._utils.abi import (
57
67
  filter_by_argument_name,
68
+ get_abi_element_signature,
69
+ get_name_from_abi_element_identifier,
70
+ )
71
+ from web3._utils.decorators import (
72
+ deprecated_for,
58
73
  )
59
- from web3._utils.abi_element_identifiers import (
60
- FallbackFn,
61
- ReceiveFn,
74
+ from web3._utils.validation import (
75
+ validate_abi,
62
76
  )
63
77
  from web3.exceptions import (
64
78
  ABIConstructorNotFound,
65
79
  ABIFallbackNotFound,
66
80
  ABIReceiveNotFound,
67
81
  MismatchedABI,
68
- Web3TypeError,
69
82
  Web3ValidationError,
70
83
  Web3ValueError,
71
84
  )
@@ -73,15 +86,9 @@ from web3.types import (
73
86
  ABIElementIdentifier,
74
87
  )
75
88
 
76
- from eth_utils.abi import ( # noqa
77
- abi_to_signature,
78
- event_abi_to_log_topic,
79
- filter_abi_by_name,
80
- filter_abi_by_type,
81
- function_abi_to_4byte_selector,
82
- get_aligned_abi_inputs,
83
- get_normalized_abi_inputs,
84
- )
89
+
90
+ def _filter_by_signature(signature: str, contract_abi: ABI) -> List[ABIElement]:
91
+ return [abi for abi in contract_abi if abi_to_signature(abi) == signature]
85
92
 
86
93
 
87
94
  def _filter_by_argument_count(
@@ -98,9 +105,9 @@ def _filter_by_argument_count(
98
105
 
99
106
  def _filter_by_encodability(
100
107
  abi_codec: codec.ABIEncoder,
108
+ args: Sequence[Any],
109
+ kwargs: Dict[str, Any],
101
110
  contract_abi: ABI,
102
- *args: Optional[Sequence[Any]],
103
- **kwargs: Optional[Dict[str, Any]],
104
111
  ) -> List[ABICallable]:
105
112
  return [
106
113
  cast(ABICallable, function_abi)
@@ -158,13 +165,121 @@ def _get_fallback_function_abi(contract_abi: ABI) -> ABIFallback:
158
165
  raise ABIFallbackNotFound("No fallback function was found in the contract ABI.")
159
166
 
160
167
 
168
+ def _get_any_abi_signature_with_name(
169
+ element_name: str, elements: Sequence[ABIElement]
170
+ ) -> str:
171
+ """
172
+ Find an ABI identifier signature by element name. A signature identifier is
173
+ returned, "name(arg1Type,arg2Type,...)".
174
+
175
+ If multiple ABIs match the name and every one contain arguments, the first
176
+ result is returned. Otherwise the signature without arguments is returned.
177
+ Returns None if no ABI exists with the provided name.
178
+ """
179
+ element_signatures_with_name = [
180
+ abi_to_signature(element)
181
+ for element in elements
182
+ if element.get("name", "") == get_name_from_abi_element_identifier(element_name)
183
+ ]
184
+
185
+ if len(element_signatures_with_name) == 1:
186
+ return element_signatures_with_name[0]
187
+ elif len(element_signatures_with_name) > 1:
188
+ # Check for function signature without args
189
+ signature_without_args = f"{element_name}()"
190
+ if signature_without_args not in element_signatures_with_name:
191
+ # Element without arguments not found, use the first available signature
192
+ return element_signatures_with_name[0]
193
+ else:
194
+ return signature_without_args
195
+ else:
196
+ return None
197
+
198
+
199
+ def _build_abi_input_error(
200
+ abi: ABI,
201
+ num_args: int,
202
+ *args: Any,
203
+ abi_codec: ABICodec,
204
+ **kwargs: Any,
205
+ ) -> str:
206
+ """
207
+ Build a string representation of the ABI input error.
208
+ """
209
+ errors: Dict[str, str] = dict(
210
+ {
211
+ "zero_args": "",
212
+ "invalid_args": "",
213
+ "encoding": "",
214
+ "unexpected_args": "",
215
+ }
216
+ )
217
+
218
+ for abi_element in abi:
219
+ abi_element_input_types = get_abi_input_types(abi_element)
220
+ abi_signature = abi_to_signature(abi_element)
221
+ abi_element_name = get_name_from_abi_element_identifier(abi_signature)
222
+ types: Tuple[str, ...] = tuple()
223
+ aligned_args: Tuple[Any, ...] = tuple()
224
+
225
+ if len(abi_element_input_types) == num_args:
226
+ if num_args == 0:
227
+ if not errors["zero_args"]:
228
+ errors["zero_args"] += (
229
+ "The provided identifier matches multiple elements.\n"
230
+ f"If you meant to call `{abi_element_name}()`, "
231
+ "please specify the full signature.\n"
232
+ )
233
+
234
+ errors["zero_args"] += (
235
+ f" - signature: {abi_to_signature(abi_element)}, "
236
+ f"type: {abi_element['type']}\n"
237
+ )
238
+ else:
239
+ try:
240
+ arguments = get_normalized_abi_inputs(abi_element, *args, **kwargs)
241
+ types, aligned_args = get_aligned_abi_inputs(abi_element, arguments)
242
+ except TypeError as e:
243
+ errors["invalid_args"] += (
244
+ f"Signature: {abi_signature}, type: {abi_element['type']}\n"
245
+ f"Arguments do not match types in `{abi_signature}`.\n"
246
+ f"Error: {e}\n"
247
+ )
248
+
249
+ argument_errors = ""
250
+ for position, (_type, arg) in enumerate(zip(types, aligned_args), start=1):
251
+ if abi_codec.is_encodable(_type, arg):
252
+ argument_errors += f"Argument {position} value `{arg}` is valid.\n"
253
+ else:
254
+ argument_errors += (
255
+ f"Argument {position} value `{arg}` is not compatible with "
256
+ f"type `{_type}`.\n"
257
+ )
258
+
259
+ if argument_errors != "":
260
+ errors["encoding"] += (
261
+ f"Signature: {abi_signature}, type: {abi_element['type']}\n"
262
+ + argument_errors
263
+ )
264
+
265
+ else:
266
+ errors["unexpected_args"] += (
267
+ f"Signature: {abi_signature}, type: {abi_element['type']}\n"
268
+ f"Expected {len(abi_element_input_types)} argument(s) but received "
269
+ f"{num_args} argument(s).\n"
270
+ )
271
+
272
+ return "".join(errors.values())
273
+
274
+
161
275
  def _mismatched_abi_error_diagnosis(
162
276
  abi_element_identifier: ABIElementIdentifier,
163
- matching_function_signatures: Sequence[str],
164
- arg_count_matches: int,
165
- encoding_matches: int,
166
- *args: Optional[Sequence[Any]],
167
- **kwargs: Optional[Dict[str, Any]],
277
+ abi: ABI,
278
+ num_matches: int = 0,
279
+ num_args: int = 0,
280
+ *args: Optional[Any],
281
+ abi_codec: Optional[Any] = None,
282
+ **kwargs: Optional[Any],
168
283
  ) -> str:
169
284
  """
170
285
  Raise a ``MismatchedABI`` when a function ABI lookup results in an error.
@@ -172,32 +287,59 @@ def _mismatched_abi_error_diagnosis(
172
287
  An error may result from multiple functions matching the provided signature and
173
288
  arguments or no functions are identified.
174
289
  """
175
- diagnosis = "\n"
176
- if arg_count_matches == 0:
177
- diagnosis += "Function invocation failed due to improper number of arguments."
178
- elif encoding_matches == 0:
179
- diagnosis += "Function invocation failed due to no matching argument types."
180
- elif encoding_matches > 1:
181
- diagnosis += (
182
- "Ambiguous argument encoding. "
183
- "Provided arguments can be encoded to multiple functions "
184
- "matching this call."
185
- )
290
+ name = get_name_from_abi_element_identifier(abi_element_identifier)
291
+ abis_matching_names = filter_abi_by_name(name, abi)
292
+ abis_matching_arg_count = [
293
+ abi_to_signature(abi)
294
+ for abi in _filter_by_argument_count(num_args, abis_matching_names)
295
+ ]
296
+ num_abis_matching_arg_count = len(abis_matching_arg_count)
186
297
 
187
- collapsed_args = _extract_argument_types(*args)
188
- collapsed_kwargs = dict(
189
- {(k, _extract_argument_types([v])) for k, v in kwargs.items()}
190
- )
298
+ if abi_codec is None:
299
+ abi_codec = ABICodec(default_registry)
191
300
 
192
- return (
193
- f"\nCould not identify the intended function with name "
194
- f"`{abi_element_identifier}`, positional arguments with type(s) "
195
- f"`({collapsed_args})` and keyword arguments with type(s) "
196
- f"`{collapsed_kwargs}`."
197
- f"\nFound {len(matching_function_signatures)} function(s) with the name "
198
- f"`{abi_element_identifier}`: {matching_function_signatures}{diagnosis}"
301
+ error = "ABI Not Found!\n"
302
+ if num_matches == 0 and num_abis_matching_arg_count == 0:
303
+ error += f"No element named `{name}` with {num_args} argument(s).\n"
304
+ elif num_matches > 1 or num_abis_matching_arg_count > 1:
305
+ error += (
306
+ f"Found multiple elements named `{name}` that accept {num_args} "
307
+ "argument(s).\n"
308
+ )
309
+ elif num_abis_matching_arg_count == 1:
310
+ error += (
311
+ f"Found {num_abis_matching_arg_count} element(s) named `{name}` that "
312
+ f"accept {num_args} argument(s).\n"
313
+ "The provided arguments are not valid.\n"
314
+ )
315
+ elif num_matches == 0:
316
+ error += (
317
+ f"Unable to find an element named `{name}` that matches the provided "
318
+ "identifier and argument types.\n"
319
+ )
320
+ arg_types = _extract_argument_types(*args)
321
+ kwarg_types = dict({(k, _extract_argument_types([v])) for k, v in kwargs.items()})
322
+ error += (
323
+ f"Provided argument types: ({arg_types})\n"
324
+ f"Provided keyword argument types: {kwarg_types}\n\n"
199
325
  )
200
326
 
327
+ if abis_matching_names:
328
+ error += (
329
+ f"Tried to find a matching ABI element named `{name}`, but encountered "
330
+ "the following problems:\n"
331
+ )
332
+
333
+ error += _build_abi_input_error(
334
+ abis_matching_names,
335
+ num_args,
336
+ *args,
337
+ abi_codec=abi_codec,
338
+ **kwargs,
339
+ )
340
+
341
+ return f"\n{error}"
342
+
201
343
 
202
344
  def _extract_argument_types(*args: Sequence[Any]) -> str:
203
345
  """
@@ -231,6 +373,84 @@ def _get_argument_readable_type(arg: Any) -> str:
231
373
  return arg.__class__.__name__
232
374
 
233
375
 
376
+ def _build_abi_filters(
377
+ abi_element_identifier: ABIElementIdentifier,
378
+ *args: Optional[Any],
379
+ abi_codec: Optional[Any] = None,
380
+ **kwargs: Optional[Any],
381
+ ) -> List[Callable[..., Sequence[ABIElement]]]:
382
+ """
383
+ Build a list of ABI filters to find an ABI element within a contract ABI. Each
384
+ filter is a partial function that takes a contract ABI and returns a filtered list.
385
+ Each parameter is checked before applying the relevant filter.
386
+
387
+ When the ``abi_element_identifier`` is a function name or signature and no arguments
388
+ are provided, the returned filters include the function name or signature.
389
+
390
+ A function ABI may take arguments and keyword arguments. When the ``args`` and
391
+ ``kwargs`` values are passed, several filters are combined together. Available
392
+ filters include the function name, argument count, argument name, argument type,
393
+ and argument encodability.
394
+
395
+ ``constructor``, ``fallback``, and ``receive`` ABI elements are handled only with a
396
+ filter by type.
397
+ """
398
+ if not isinstance(abi_element_identifier, str):
399
+ abi_element_identifier = get_abi_element_signature(abi_element_identifier)
400
+
401
+ if abi_element_identifier in ["constructor", "fallback", "receive"]:
402
+ return [functools.partial(filter_abi_by_type, abi_element_identifier)]
403
+
404
+ filters: List[Callable[..., Sequence[ABIElement]]] = []
405
+
406
+ arg_count = 0
407
+ if args or kwargs:
408
+ arg_count = len(args) + len(kwargs)
409
+
410
+ # Filter by arg count only if the identifier contains arguments
411
+ if "()" not in abi_element_identifier and arg_count:
412
+ filters.append(functools.partial(_filter_by_argument_count, arg_count))
413
+
414
+ if arg_count > 0:
415
+ filters.append(
416
+ functools.partial(
417
+ filter_abi_by_name,
418
+ get_name_from_abi_element_identifier(abi_element_identifier),
419
+ )
420
+ )
421
+
422
+ if args or kwargs:
423
+ if abi_codec is None:
424
+ abi_codec = ABICodec(default_registry)
425
+
426
+ filters.append(
427
+ functools.partial(
428
+ _filter_by_encodability,
429
+ abi_codec,
430
+ args,
431
+ kwargs,
432
+ )
433
+ )
434
+
435
+ if "(" in abi_element_identifier:
436
+ filters.append(
437
+ functools.partial(_filter_by_signature, abi_element_identifier)
438
+ )
439
+ else:
440
+ filters.append(
441
+ functools.partial(
442
+ filter_abi_by_name,
443
+ get_name_from_abi_element_identifier(abi_element_identifier),
444
+ )
445
+ )
446
+ if "(" in abi_element_identifier:
447
+ filters.append(
448
+ functools.partial(_filter_by_signature, abi_element_identifier)
449
+ )
450
+
451
+ return filters
452
+
453
+
234
454
  def get_abi_element_info(
235
455
  abi: ABI,
236
456
  abi_element_identifier: ABIElementIdentifier,
@@ -306,16 +526,21 @@ def get_abi_element_info(
306
526
  def get_abi_element(
307
527
  abi: ABI,
308
528
  abi_element_identifier: ABIElementIdentifier,
309
- *args: Optional[Sequence[Any]],
529
+ *args: Optional[Any],
310
530
  abi_codec: Optional[Any] = None,
311
- **kwargs: Optional[Dict[str, Any]],
531
+ **kwargs: Optional[Any],
312
532
  ) -> ABIElement:
313
533
  """
314
- Return the interface for an ``ABIElement`` which matches the provided identifier
315
- and arguments.
534
+ Return the interface for an ``ABIElement`` from the ``abi`` that matches the
535
+ provided identifier and arguments.
536
+
537
+ ``abi`` may be a list of all ABI elements in a contract or a subset of elements.
538
+ Passing only functions or events can be useful when names are not deterministic.
539
+ For example, if names overlap between functions and events.
316
540
 
317
- The ABI which matches the provided identifier, named arguments (``args``) and
318
- keyword args (``kwargs``) will be returned.
541
+ The ``ABIElementIdentifier`` value may be a function name, signature, or a
542
+ ``FallbackFn`` or ``ReceiveFn``. When named arguments (``args``) and/or keyword args
543
+ (``kwargs``) are provided, they are included in the search filters.
319
544
 
320
545
  The `abi_codec` may be overridden if custom encoding and decoding is required. The
321
546
  default is used if no codec is provided. More details about customizations are in
@@ -323,7 +548,9 @@ def get_abi_element(
323
548
 
324
549
  :param abi: Contract ABI.
325
550
  :type abi: `ABI`
326
- :param abi_element_identifier: Find an element ABI with matching identifier.
551
+ :param abi_element_identifier: Find an element ABI with matching identifier. The \
552
+ identifier may be a function name, signature, or ``FallbackFn`` or ``ReceiveFn``. \
553
+ A function signature is in the form ``name(arg1Type,arg2Type,...)``.
327
554
  :type abi_element_identifier: `ABIElementIdentifier`
328
555
  :param args: Find an element ABI with matching args.
329
556
  :type args: `Optional[Sequence[Any]]`
@@ -358,55 +585,38 @@ def get_abi_element(
358
585
  'type': 'uint256'}], 'payable': False, 'stateMutability': 'nonpayable', \
359
586
  'type': 'function'}
360
587
  """
588
+ validate_abi(abi)
589
+
361
590
  if abi_codec is None:
362
591
  abi_codec = ABICodec(default_registry)
363
592
 
364
- if abi_element_identifier is FallbackFn or abi_element_identifier == "fallback":
365
- return _get_fallback_function_abi(abi)
366
-
367
- if abi_element_identifier is ReceiveFn or abi_element_identifier == "receive":
368
- return _get_receive_function_abi(abi)
369
-
370
- if abi_element_identifier is None or not is_text(abi_element_identifier):
371
- raise Web3TypeError("Unsupported function identifier")
372
-
373
- filtered_abis_by_name: Sequence[ABIElement]
374
- if abi_element_identifier == "constructor":
375
- filtered_abis_by_name = [_get_constructor_function_abi(abi)]
376
- else:
377
- filtered_abis_by_name = filter_abi_by_name(
378
- cast(str, abi_element_identifier), abi
379
- )
380
-
381
- arg_count = len(args) + len(kwargs)
382
- filtered_abis_by_arg_count = _filter_by_argument_count(
383
- arg_count, filtered_abis_by_name
384
- )
385
-
386
- if not args and not kwargs and len(filtered_abis_by_arg_count) == 1:
387
- return filtered_abis_by_arg_count[0]
388
-
389
- elements_with_encodable_args = _filter_by_encodability(
390
- abi_codec, filtered_abis_by_arg_count, *args, **kwargs
593
+ abi_element_matches: Sequence[ABIElement] = pipe(
594
+ abi,
595
+ *_build_abi_filters(
596
+ abi_element_identifier,
597
+ *args,
598
+ abi_codec=abi_codec,
599
+ **kwargs,
600
+ ),
391
601
  )
392
602
 
393
- if len(elements_with_encodable_args) != 1:
394
- matching_function_signatures = [
395
- abi_to_signature(func) for func in filtered_abis_by_name
396
- ]
603
+ num_matches = len(abi_element_matches)
397
604
 
605
+ # Raise MismatchedABI unless one match is found
606
+ if num_matches != 1:
398
607
  error_diagnosis = _mismatched_abi_error_diagnosis(
399
608
  abi_element_identifier,
400
- matching_function_signatures,
401
- len(filtered_abis_by_arg_count),
402
- len(elements_with_encodable_args),
609
+ abi,
610
+ num_matches,
611
+ len(args) + len(kwargs),
403
612
  *args,
613
+ abi_codec=abi_codec,
404
614
  **kwargs,
405
615
  )
406
616
 
407
617
  raise MismatchedABI(error_diagnosis)
408
618
 
409
- return elements_with_encodable_args[0]
619
+ return abi_element_matches[0]
410
620
 
411
621
 
412
622
  def check_if_arguments_can_be_encoded(
@@ -472,12 +682,17 @@ def check_if_arguments_can_be_encoded(
472
682
  )
473
683
 
474
684
 
685
+ @deprecated_for("get_abi_element")
475
686
  def get_event_abi(
476
687
  abi: ABI,
477
688
  event_name: str,
478
689
  argument_names: Optional[Sequence[str]] = None,
479
690
  ) -> ABIEvent:
480
691
  """
692
+ .. warning::
693
+ This function is deprecated. It is unable to distinguish between
694
+ overloaded events. Use ``get_abi_element`` instead.
695
+
481
696
  Find the event interface with the given name and/or arguments.
482
697
 
483
698
  :param abi: Contract ABI.
web3/utils/address.py CHANGED
@@ -9,6 +9,9 @@ from eth_utils import (
9
9
  )
10
10
  import rlp
11
11
 
12
+ from web3.exceptions import (
13
+ Web3ValidationError,
14
+ )
12
15
  from web3.types import (
13
16
  HexStr,
14
17
  Nonce,
@@ -30,6 +33,11 @@ def get_create2_address(
30
33
  Determine the resulting `CREATE2` opcode contract address for a sender, salt and
31
34
  bytecode.
32
35
  """
36
+ if len(to_bytes(hexstr=salt)) != 32:
37
+ raise Web3ValidationError(
38
+ f"`salt` must be 32 bytes, {len(to_bytes(hexstr=salt))} != 32"
39
+ )
40
+
33
41
  contract_address = keccak(
34
42
  b"\xff"
35
43
  + to_bytes(hexstr=sender)
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.1
2
2
  Name: web3
3
- Version: 7.5.0
3
+ Version: 7.6.1
4
4
  Summary: web3: A Python library for interacting with Ethereum
5
5
  Home-page: https://github.com/ethereum/web3.py
6
6
  Author: The Ethereum Foundation
@@ -32,7 +32,7 @@ Requires-Dist: pydantic >=2.4.0
32
32
  Requires-Dist: requests >=2.23.0
33
33
  Requires-Dist: typing-extensions >=4.0.1
34
34
  Requires-Dist: types-requests >=2.0.0
35
- Requires-Dist: websockets >=10.0.0
35
+ Requires-Dist: websockets <14.0.0,>=10.0.0
36
36
  Requires-Dist: pyunormalize >=15.0.0
37
37
  Requires-Dist: pywin32 >=223 ; platform_system == "Windows"
38
38
  Provides-Extra: dev
@@ -56,8 +56,8 @@ Requires-Dist: hypothesis >=3.31.2 ; extra == 'dev'
56
56
  Requires-Dist: tox >=4.0.0 ; extra == 'dev'
57
57
  Requires-Dist: mypy ==1.10.0 ; extra == 'dev'
58
58
  Requires-Dist: pre-commit >=3.4.0 ; extra == 'dev'
59
- Requires-Dist: eth-tester[py-evm] <0.13.0b1,>=0.11.0b1 ; extra == 'dev'
60
- Requires-Dist: py-geth >=5.0.0 ; extra == 'dev'
59
+ Requires-Dist: eth-tester[py-evm] <0.13.0b1,>=0.12.0b1 ; extra == 'dev'
60
+ Requires-Dist: py-geth >=5.1.0 ; extra == 'dev'
61
61
  Provides-Extra: docs
62
62
  Requires-Dist: sphinx >=6.0.0 ; extra == 'docs'
63
63
  Requires-Dist: sphinx-autobuild >=2021.3.14 ; extra == 'docs'
@@ -73,11 +73,11 @@ Requires-Dist: hypothesis >=3.31.2 ; extra == 'test'
73
73
  Requires-Dist: tox >=4.0.0 ; extra == 'test'
74
74
  Requires-Dist: mypy ==1.10.0 ; extra == 'test'
75
75
  Requires-Dist: pre-commit >=3.4.0 ; extra == 'test'
76
- Requires-Dist: eth-tester[py-evm] <0.13.0b1,>=0.11.0b1 ; extra == 'test'
77
- Requires-Dist: py-geth >=5.0.0 ; extra == 'test'
76
+ Requires-Dist: eth-tester[py-evm] <0.13.0b1,>=0.12.0b1 ; extra == 'test'
77
+ Requires-Dist: py-geth >=5.1.0 ; extra == 'test'
78
78
  Provides-Extra: tester
79
- Requires-Dist: eth-tester[py-evm] <0.13.0b1,>=0.11.0b1 ; extra == 'tester'
80
- Requires-Dist: py-geth >=5.0.0 ; extra == 'tester'
79
+ Requires-Dist: eth-tester[py-evm] <0.13.0b1,>=0.12.0b1 ; extra == 'tester'
80
+ Requires-Dist: py-geth >=5.1.0 ; extra == 'tester'
81
81
 
82
82
  # web3.py
83
83