• Home
  • Features
  • Pricing
  • Docs
  • Announcements
  • Sign In

safe-global / safe-eth-py / 13388974656

18 Feb 2025 10:56AM UTC coverage: 93.946% (-0.004%) from 93.95%
13388974656

Pull #1518

github

web-flow
Merge 4ce6371ba into 8b28bc770
Pull Request #1518: Add custom rate limit for aiohtpp

58 of 62 new or added lines in 3 files covered. (93.55%)

8 existing lines in 3 files now uncovered.

9543 of 10158 relevant lines covered (93.95%)

2.82 hits per line

Source File
Press 'n' to go to next uncovered line, 'b' for previous

85.36
/safe_eth/eth/ethereum_client.py
1
import os
3✔
2
from enum import Enum
3✔
3
from functools import cache, cached_property, wraps
3✔
4
from logging import getLogger
3✔
5
from typing import (
3✔
6
    Any,
7
    Dict,
8
    Iterable,
9
    List,
10
    NamedTuple,
11
    Optional,
12
    Sequence,
13
    Tuple,
14
    Type,
15
    Union,
16
    cast,
17
)
18

19
import eth_abi
3✔
20
from eth_abi.exceptions import DecodingError
3✔
21
from eth_account import Account
3✔
22
from eth_account.signers.local import LocalAccount
3✔
23
from eth_typing import URI, BlockNumber, ChecksumAddress, Hash32, HexAddress, HexStr
3✔
24
from hexbytes import HexBytes
3✔
25
from web3 import HTTPProvider, Web3
3✔
26
from web3._utils.abi import map_abi_data
3✔
27
from web3._utils.method_formatters import (
3✔
28
    block_formatter,
29
    receipt_formatter,
30
    trace_list_result_formatter,
31
    transaction_result_formatter,
32
)
33
from web3._utils.normalizers import BASE_RETURN_NORMALIZERS
3✔
34
from web3.contract.contract import ContractFunction
3✔
35
from web3.exceptions import (
3✔
36
    BlockNotFound,
37
    TimeExhausted,
38
    TransactionNotFound,
39
    Web3Exception,
40
)
41
from web3.middleware import ExtraDataToPOAMiddleware
3✔
42
from web3.types import (
3✔
43
    BlockData,
44
    BlockIdentifier,
45
    BlockTrace,
46
    FilterParams,
47
    FilterTrace,
48
    LogReceipt,
49
    Nonce,
50
    TraceFilterParams,
51
    TxData,
52
    TxParams,
53
    TxReceipt,
54
    Wei,
55
)
56

57
from safe_eth.eth.utils import (
3✔
58
    fast_is_checksum_address,
59
    fast_to_checksum_address,
60
    mk_contract_address,
61
    mk_contract_address_2,
62
)
63
from safe_eth.util import chunks
3✔
64

65
from ..util.http import prepare_http_session
3✔
66
from ..util.util import to_0x_hex_str
3✔
67
from .constants import (
3✔
68
    ERC20_721_TRANSFER_TOPIC,
69
    GAS_CALL_DATA_BYTE,
70
    GAS_CALL_DATA_ZERO_BYTE,
71
    NULL_ADDRESS,
72
    SAFE_SINGLETON_FACTORY_ADDRESS,
73
)
74
from .contracts import get_erc20_contract, get_erc721_contract
3✔
75
from .ethereum_network import EthereumNetwork, EthereumNetworkNotSupported
3✔
76
from .exceptions import (
3✔
77
    BatchCallFunctionFailed,
78
    ChainIdIsRequired,
79
    ContractAlreadyDeployed,
80
    FromAddressNotFound,
81
    GasLimitExceeded,
82
    InsufficientFunds,
83
    InvalidERC20Info,
84
    InvalidERC721Info,
85
    InvalidNonce,
86
    NonceTooHigh,
87
    NonceTooLow,
88
    ReplacementTransactionUnderpriced,
89
    SenderAccountNotFoundInNode,
90
    TransactionAlreadyImported,
91
    TransactionGasPriceTooLow,
92
    TransactionQueueLimitReached,
93
    UnknownAccount,
94
)
95
from .typing import BalanceDict, EthereumData, EthereumHash, LogReceiptDecoded
3✔
96
from .utils import decode_string_or_bytes32
3✔
97

98
logger = getLogger(__name__)
3✔
99

100

101
def tx_with_exception_handling(func):
3✔
102
    """
103
    Parity / OpenEthereum
104
        - https://github.com/openethereum/openethereum/blob/main/rpc/src/v1/helpers/errors.rs
105
    Geth
106
        - https://github.com/ethereum/go-ethereum/blob/master/core/error.go
107
        - https://github.com/ethereum/go-ethereum/blob/master/core/tx_pool.go
108
    Comparison
109
        - https://gist.github.com/kunal365roy/3c37ac9d1c3aaf31140f7c5faa083932
110

111
    :param func:
112
    :return:
113
    """
114
    error_with_exception: Dict[str, Type[Exception]] = {
3✔
115
        "EIP-155": ChainIdIsRequired,
116
        "Transaction with the same hash was already imported": TransactionAlreadyImported,
117
        "replacement transaction underpriced": ReplacementTransactionUnderpriced,
118
        # https://github.com/ethereum/go-ethereum/blob/eaccdba4ab310e3fb98edbc4b340b5e7c4d767fd/core/tx_pool.go#L72
119
        "There is another transaction with same nonce in the queue": ReplacementTransactionUnderpriced,
120
        # https://github.com/openethereum/openethereum/blob/f1dc6821689c7f47d8fd07dfc0a2c5ad557b98ec/crates/rpc/src/v1/helpers/errors.rs#L374
121
        "There are too many transactions in the queue. Your transaction was dropped due to limit. Try increasing "
122
        "the fee": TransactionQueueLimitReached,
123
        # https://github.com/openethereum/openethereum/blob/f1dc6821689c7f47d8fd07dfc0a2c5ad557b98ec/crates/rpc/src/v1/helpers/errors.rs#L380
124
        "txpool is full": TransactionQueueLimitReached,
125
        # https://github.com/ethereum/go-ethereum/blob/eaccdba4ab310e3fb98edbc4b340b5e7c4d767fd/core/tx_pool.go#L68
126
        "transaction underpriced": TransactionGasPriceTooLow,
127
        # https://github.com/ethereum/go-ethereum/blob/eaccdba4ab310e3fb98edbc4b340b5e7c4d767fd/core/tx_pool.go#L64
128
        "Transaction gas price is too low": TransactionGasPriceTooLow,
129
        # https://github.com/openethereum/openethereum/blob/f1dc6821689c7f47d8fd07dfc0a2c5ad557b98ec/crates/rpc/src/v1/helpers/errors.rs#L386
130
        "from not found": FromAddressNotFound,
131
        "correct nonce": InvalidNonce,
132
        "nonce too low": NonceTooLow,
133
        # https://github.com/ethereum/go-ethereum/blob/bbfb1e4008a359a8b57ec654330c0e674623e52f/core/error.go#L46
134
        "nonce too high": NonceTooHigh,
135
        # https://github.com/ethereum/go-ethereum/blob/bbfb1e4008a359a8b57ec654330c0e674623e52f/core/error.go#L46
136
        "insufficient funds": InsufficientFunds,
137
        # https://github.com/openethereum/openethereum/blob/f1dc6821689c7f47d8fd07dfc0a2c5ad557b98ec/crates/rpc/src/v1/helpers/errors.rs#L389
138
        "doesn't have enough funds": InsufficientFunds,
139
        "sender account not recognized": SenderAccountNotFoundInNode,
140
        "unknown account": UnknownAccount,
141
        "exceeds block gas limit": GasLimitExceeded,  # Geth
142
        "exceeds current gas limit": GasLimitExceeded,
143
        # https://github.com/openethereum/openethereum/blob/f1dc6821689c7f47d8fd07dfc0a2c5ad557b98ec/crates/rpc/src/v1/helpers/errors.rs#L392
144
    }
145

146
    @wraps(func)
3✔
147
    def with_exception_handling(*args, **kwargs):
3✔
148
        try:
3✔
149
            return func(*args, **kwargs)
3✔
150
        except (Web3Exception, ValueError) as exc:
3✔
151
            str_exc = str(exc).lower()
3✔
152
            for reason, custom_exception in error_with_exception.items():
3✔
153
                if reason.lower() in str_exc:
3✔
154
                    raise custom_exception(str(exc)) from exc
3✔
UNCOV
155
            raise exc
2✔
156

157
    return with_exception_handling
3✔
158

159

160
class EthereumTxSent(NamedTuple):
3✔
161
    tx_hash: bytes
3✔
162
    tx: TxParams
3✔
163
    contract_address: Optional[ChecksumAddress]
3✔
164

165

166
class Erc20Info(NamedTuple):
3✔
167
    name: str
3✔
168
    symbol: str
3✔
169
    decimals: int
3✔
170

171

172
class Erc721Info(NamedTuple):
3✔
173
    name: str
3✔
174
    symbol: str
3✔
175

176

177
class TokenBalance(NamedTuple):
3✔
178
    token_address: str
3✔
179
    balance: int
3✔
180

181

182
class TxSpeed(Enum):
3✔
183
    SLOWEST = 0
3✔
184
    VERY_SLOW = 1
3✔
185
    SLOW = 2
3✔
186
    NORMAL = 3
3✔
187
    FAST = 4
3✔
188
    VERY_FAST = 5
3✔
189
    FASTEST = 6
3✔
190

191

192
@cache
3✔
193
def get_auto_ethereum_client() -> "EthereumClient":
3✔
194
    """
195
    Use environment variables to configure `EthereumClient` and build a singleton:
196
        - `ETHEREUM_NODE_URL`: No default.
197
        - `ETHEREUM_RPC_TIMEOUT`: `10` by default.
198
        - `ETHEREUM_RPC_SLOW_TIMEOUT`: `60` by default.
199
        - `ETHEREUM_RPC_RETRY_COUNT`: `60` by default.
200
        - `ETHEREUM_RPC_BATCH_REQUEST_MAX_SIZE`: `500` by default.
201

202
    :return: A configured singleton of EthereumClient
203
    """
204
    try:
3✔
205
        from django.conf import settings
3✔
206

207
        ethereum_node_url = settings.ETHEREUM_NODE_URL
3✔
208
    except ModuleNotFoundError:
×
209
        ethereum_node_url = os.environ.get("ETHEREUM_NODE_URL")
×
210
    return EthereumClient(
3✔
211
        ethereum_node_url,
212
        provider_timeout=int(os.environ.get("ETHEREUM_RPC_TIMEOUT", 10)),
213
        slow_provider_timeout=int(os.environ.get("ETHEREUM_RPC_SLOW_TIMEOUT", 60)),
214
        retry_count=int(os.environ.get("ETHEREUM_RPC_RETRY_COUNT", 1)),
215
        batch_request_max_size=int(
216
            os.environ.get("ETHEREUM_RPC_BATCH_REQUEST_MAX_SIZE", 500)
217
        ),
218
    )
219

220

221
class EthereumClientManager:
3✔
222
    def __init__(self, ethereum_client: "EthereumClient"):
3✔
223
        self.ethereum_client = ethereum_client
3✔
224
        self.ethereum_node_url = ethereum_client.ethereum_node_url
3✔
225
        self.w3 = ethereum_client.w3
3✔
226
        self.slow_w3 = ethereum_client.slow_w3
3✔
227
        self.http_session = ethereum_client.http_session
3✔
228
        self.timeout = ethereum_client.timeout
3✔
229
        self.slow_timeout = ethereum_client.slow_timeout
3✔
230

231

232
class BatchCallManager(EthereumClientManager):
3✔
233
    def batch_call_custom(
3✔
234
        self,
235
        payloads: Iterable[Dict[str, Any]],
236
        raise_exception: bool = True,
237
        block_identifier: Optional[BlockIdentifier] = "latest",
238
        batch_size: Optional[int] = None,
239
    ) -> List[Optional[Any]]:
240
        """
241
        Do batch requests of multiple contract calls (`eth_call`)
242

243
        :param payloads: Iterable of Dictionaries with at least {'data': '<hex-string>',
244
            'output_type': <solidity-output-type>, 'to': '<checksummed-address>'}. `from` can also be provided and if
245
            `fn_name` is provided it will be used for debugging purposes
246
        :param raise_exception: If False, exception will not be raised if there's any problem and instead `None` will
247
            be returned as the value
248
        :param block_identifier: `latest` by default
249
        :param batch_size: If `payload` length is bigger than size, it will be split into smaller chunks before
250
            sending to the server
251
        :return: List with the ABI decoded return values
252
        :raises: ValueError if raise_exception=True
253
        """
254
        if not payloads:
3✔
255
            return []
×
256

257
        queries = []
3✔
258
        for i, payload in enumerate(payloads):
3✔
259
            assert "data" in payload, "`data` not present"
3✔
260
            assert "to" in payload, "`to` not present"
3✔
261
            assert "output_type" in payload, "`output-type` not present"
3✔
262

263
            query_params = {"to": payload["to"], "data": payload["data"]}  # Balance of
3✔
264
            if "from" in payload:
3✔
265
                query_params["from"] = payload["from"]
×
266

267
            queries.append(
3✔
268
                {
269
                    "jsonrpc": "2.0",
270
                    "method": "eth_call",
271
                    "params": [
272
                        query_params,
273
                        hex(block_identifier)
274
                        if isinstance(block_identifier, int)
275
                        else block_identifier,
276
                    ],
277
                    "id": i,
278
                }
279
            )
280

281
        batch_size = batch_size or self.ethereum_client.batch_request_max_size
3✔
282
        all_results = []
3✔
283
        for chunk in chunks(queries, batch_size):
3✔
284
            response = self.http_session.post(
3✔
285
                self.ethereum_node_url, json=chunk, timeout=self.slow_timeout
286
            )
287
            if not response.ok:
3✔
288
                raise ConnectionError(
×
289
                    f"Error connecting to {self.ethereum_node_url}: {response.text}"
290
                )
291

292
            results = response.json()
3✔
293

294
            # If there's an error some nodes return a json instead of a list
295
            if isinstance(results, dict) and "error" in results:
3✔
296
                logger.error(
×
297
                    "Batch call custom problem with payload=%s, result=%s)",
298
                    chunk,
299
                    results,
300
                )
301
                raise ValueError(f"Batch request error: {results}")
×
302

303
            all_results.extend(results)
3✔
304

305
        return_values: List[Optional[Any]] = []
3✔
306
        errors = []
3✔
307
        for payload, result in zip(
3✔
308
            payloads, sorted(all_results, key=lambda x: x["id"])
309
        ):
310
            if "error" in result:
3✔
311
                fn_name = payload.get(
3✔
312
                    "fn_name", to_0x_hex_str(HexBytes(payload["data"]))
313
                )
314
                errors.append(f'`{fn_name}`: {result["error"]}')
3✔
315
                return_values.append(None)
3✔
316
            else:
317
                output_type = payload["output_type"]
3✔
318
                try:
3✔
319
                    decoded_values = eth_abi.decode(
3✔
320
                        output_type, HexBytes(result["result"])
321
                    )
322
                    normalized_data = map_abi_data(
3✔
323
                        BASE_RETURN_NORMALIZERS, output_type, decoded_values
324
                    )
325
                    if len(normalized_data) == 1:
3✔
326
                        return_values.append(normalized_data[0])
3✔
327
                    else:
328
                        return_values.append(normalized_data)
×
329
                except (DecodingError, OverflowError):
3✔
330
                    fn_name = payload.get(
3✔
331
                        "fn_name", to_0x_hex_str(HexBytes(payload["data"]))
332
                    )
333
                    errors.append(f"`{fn_name}`: DecodingError, cannot decode")
3✔
334
                    return_values.append(None)
3✔
335

336
        if errors and raise_exception:
3✔
337
            raise BatchCallFunctionFailed(f"Errors returned {errors}")
3✔
338
        else:
339
            return return_values
3✔
340

341
    def batch_call(
3✔
342
        self,
343
        contract_functions: Iterable[ContractFunction],
344
        from_address: Optional[ChecksumAddress] = None,
345
        raise_exception: bool = True,
346
        block_identifier: Optional[BlockIdentifier] = "latest",
347
    ) -> List[Optional[Any]]:
348
        """
349
        Do batch requests of multiple contract calls
350

351
        :param contract_functions: Iterable of contract functions using web3.py contracts. For instance, a valid
352
            argument would be [erc20_contract.functions.balanceOf(address), erc20_contract.functions.decimals()]
353
        :param from_address: Use this address as `from` in every call if provided
354
        :param block_identifier: `latest` by default
355
        :param raise_exception: If False, exception will not be raised if there's any problem and instead `None` will
356
            be returned as the value.
357
        :return: List with the ABI decoded return values
358
        """
359
        if not contract_functions:
3✔
360
            return []
×
361
        payloads = []
3✔
362
        params: TxParams = {"gas": Wei(0), "gasPrice": Wei(0)}
3✔
363
        for _, contract_function in enumerate(contract_functions):
3✔
364
            if not contract_function.address:
3✔
365
                raise ValueError(
×
366
                    f"Missing address for batch_call in `{contract_function.fn_name}`"
367
                )
368

369
            payload = {
3✔
370
                "to": contract_function.address,
371
                "data": contract_function.build_transaction(params)["data"],
372
                "output_type": [
373
                    output["type"] for output in contract_function.abi["outputs"]
374
                ],
375
                "fn_name": contract_function.fn_name,  # For debugging purposes
376
            }
377
            if from_address:
3✔
378
                payload["from"] = from_address
×
379
            payloads.append(payload)
3✔
380

381
        return self.batch_call_custom(
3✔
382
            payloads, raise_exception=raise_exception, block_identifier=block_identifier
383
        )
384

385
    def batch_call_same_function(
3✔
386
        self,
387
        contract_function: ContractFunction,
388
        contract_addresses: Sequence[ChecksumAddress],
389
        from_address: Optional[ChecksumAddress] = None,
390
        raise_exception: bool = True,
391
        block_identifier: Optional[BlockIdentifier] = "latest",
392
    ) -> List[Optional[Any]]:
393
        """
394
        Do batch requests using the same function to multiple address. ``batch_call`` could be used to achieve that,
395
        but generating the ContractFunction is slow, so this function allows to use the same contract_function for
396
        multiple addresses
397

398
        :param contract_function:
399
        :param contract_addresses:
400
        :param from_address:
401
        :param raise_exception:
402
        :param block_identifier:
403
        :return:
404
        """
405

406
        assert contract_function, "Contract function is required"
3✔
407

408
        if not contract_addresses:
3✔
409
            return []
×
410

411
        contract_function.address = NULL_ADDRESS  # It's required by web3.py
3✔
412
        params: TxParams = {"gas": Wei(0), "gasPrice": Wei(0)}
3✔
413
        data = contract_function.build_transaction(params)["data"]
3✔
414
        output_type = [output["type"] for output in contract_function.abi["outputs"]]
3✔
415
        fn_name = contract_function.fn_name
3✔
416

417
        payloads = []
3✔
418
        for contract_address in contract_addresses:
3✔
419
            payload = {
3✔
420
                "to": contract_address,
421
                "data": data,
422
                "output_type": output_type,
423
                "fn_name": fn_name,  # For debugging purposes
424
            }
425
            if from_address:
3✔
426
                payload["from"] = from_address
×
427
            payloads.append(payload)
3✔
428

429
        return self.batch_call_custom(
3✔
430
            payloads, raise_exception=raise_exception, block_identifier=block_identifier
431
        )
432

433

434
class Erc20Manager(EthereumClientManager):
3✔
435
    """
436
    Manager for ERC20 operations
437
    """
438

439
    # keccak('Transfer(address,address,uint256)')
440
    # ddf252ad1be2c89b69c2b068fc378daa952ba7f163c4a11628f55a4df523b3ef
441
    TRANSFER_TOPIC = HexBytes(ERC20_721_TRANSFER_TOPIC)
3✔
442

443
    def decode_logs(self, logs: Sequence[LogReceipt]):
3✔
444
        decoded_logs = []
3✔
445
        for log in logs:
3✔
446
            decoded = self._decode_transfer_log(log["data"], log["topics"])
3✔
447
            if decoded:
3✔
448
                log_copy = dict(log)
3✔
449
                log_copy["args"] = decoded
3✔
450
                decoded_logs.append(log_copy)
3✔
451
        return decoded_logs
3✔
452

453
    def _decode_transfer_log(
3✔
454
        self, data: EthereumData, topics: Sequence[bytes]
455
    ) -> Optional[Dict[str, Any]]:
456
        topics_len = len(topics)
3✔
457
        if topics_len and topics[0] == self.TRANSFER_TOPIC:
3✔
458
            if topics_len == 1:
3✔
459
                # Not standard Transfer(address from, address to, uint256 unknown)
460
                # 1 topic (transfer topic)
461
                try:
3✔
462
                    _from, to, unknown = eth_abi.decode(
3✔
463
                        ["address", "address", "uint256"], HexBytes(data)
464
                    )
465
                    return {"from": _from, "to": to, "unknown": unknown}
3✔
466
                except DecodingError:
×
467
                    logger.warning(
×
468
                        "Cannot decode Transfer event `address from, address to, uint256 unknown` from data=%s",
469
                        data.hex() if isinstance(data, bytes) else data,
470
                    )
471
                    return None
×
472
            elif topics_len == 3:
3✔
473
                # ERC20 Transfer(address indexed from, address indexed to, uint256 value)
474
                # 3 topics (transfer topic + from + to)
475
                value_data = HexBytes(data)
3✔
476
                try:
3✔
477
                    value = eth_abi.decode(["uint256"], value_data)[0]
3✔
478
                except DecodingError:
×
479
                    logger.warning(
×
480
                        "Cannot decode Transfer event `uint256 value` from data=%s",
481
                        to_0x_hex_str(value_data),
482
                    )
483
                    return None
×
484
                from_to_data = b"".join(topics[1:])
3✔
485
                try:
3✔
486
                    _from, to = (
3✔
487
                        fast_to_checksum_address(address)
488
                        for address in eth_abi.decode(
489
                            ["address", "address"], from_to_data
490
                        )
491
                    )
492
                    return {"from": _from, "to": to, "value": value}
3✔
493
                except DecodingError:
3✔
494
                    logger.warning(
3✔
495
                        "Cannot decode Transfer event `address from, address to` from topics=%s",
496
                        to_0x_hex_str(from_to_data),
497
                    )
498
                    return None
3✔
499
            elif topics_len == 4:
3✔
500
                # ERC712 Transfer(address indexed from, address indexed to, uint256 indexed tokenId)
501
                # 4 topics (transfer topic + from + to + tokenId)
502
                from_to_token_id_data = b"".join(topics[1:])
3✔
503
                try:
3✔
504
                    _from, to, token_id = eth_abi.decode(
3✔
505
                        ["address", "address", "uint256"], from_to_token_id_data
506
                    )
507
                    _from, to = [
3✔
508
                        fast_to_checksum_address(address) for address in (_from, to)
509
                    ]
510
                    return {"from": _from, "to": to, "tokenId": token_id}
3✔
511
                except DecodingError:
×
512
                    logger.warning(
×
513
                        "Cannot decode Transfer event `address from, address to` from topics=%s",
514
                        to_0x_hex_str(from_to_token_id_data),
515
                    )
516
                    return None
×
517
        return None
3✔
518

519
    def get_balance(
3✔
520
        self, address: ChecksumAddress, token_address: ChecksumAddress
521
    ) -> int:
522
        """
523
        Get balance of address for `erc20_address`
524

525
        :param address: owner address
526
        :param token_address: erc20 token address
527
        :return: balance
528
        """
529
        return (
3✔
530
            get_erc20_contract(self.w3, token_address)
531
            .functions.balanceOf(address)
532
            .call()
533
        )
534

535
    def get_balances(
3✔
536
        self,
537
        address: ChecksumAddress,
538
        token_addresses: Sequence[ChecksumAddress],
539
        include_native_balance: bool = True,
540
    ) -> List[BalanceDict]:
541
        """
542
        Get balances for Ether and tokens for an `address`
543

544
        :param address: Owner address checksummed
545
        :param token_addresses: token addresses to check
546
        :param include_native_balance: if `True` returns also the native token balance
547
        :return: ``List[BalanceDict]``
548
        """
549

550
        balances = self.ethereum_client.batch_call_same_function(
3✔
551
            get_erc20_contract(self.ethereum_client.w3).functions.balanceOf(address),
552
            token_addresses,
553
            raise_exception=False,
554
        )
555

556
        return_balances = [
3✔
557
            BalanceDict(
558
                balance=balance if isinstance(balance, int) else 0,
559
                token_address=token_address,
560
            )
561
            for token_address, balance in zip(token_addresses, balances)
562
        ]
563

564
        if not include_native_balance:
3✔
565
            return return_balances
3✔
566

567
        # Add ether balance response
568
        return [
3✔
569
            BalanceDict(
570
                balance=self.ethereum_client.get_balance(address), token_address=None
571
            )
572
        ] + return_balances
573

574
    def get_name(self, erc20_address: ChecksumAddress) -> str:
3✔
575
        erc20 = get_erc20_contract(self.w3, erc20_address)
3✔
576
        data = erc20.functions.name().build_transaction(
3✔
577
            {"gas": Wei(0), "gasPrice": Wei(0)}
578
        )["data"]
579
        result = self.w3.eth.call({"to": erc20_address, "data": data})
3✔
580
        return decode_string_or_bytes32(result)
3✔
581

582
    def get_symbol(self, erc20_address: ChecksumAddress) -> str:
3✔
583
        erc20 = get_erc20_contract(self.w3, erc20_address)
3✔
584
        data = erc20.functions.symbol().build_transaction(
3✔
585
            {"gas": Wei(0), "gasPrice": Wei(0)}
586
        )["data"]
587
        result = self.w3.eth.call({"to": erc20_address, "data": data})
3✔
588
        return decode_string_or_bytes32(result)
3✔
589

590
    def get_decimals(self, erc20_address: ChecksumAddress) -> int:
3✔
591
        erc20 = get_erc20_contract(self.w3, erc20_address)
3✔
592
        return erc20.functions.decimals().call()
3✔
593

594
    def get_info(self, erc20_address: ChecksumAddress) -> Erc20Info:
3✔
595
        """
596
        Get erc20 information (`name`, `symbol` and `decimals`). Use batching to get
597
        all info in the same request.
598

599
        :param erc20_address:
600
        :return: Erc20Info
601
        :raises: InvalidERC20Info
602
        """
603
        erc20 = get_erc20_contract(self.w3, erc20_address)
3✔
604
        params: TxParams = {
3✔
605
            "gas": Wei(0),
606
            "gasPrice": Wei(0),
607
        }  # Prevent executing tx, we are just interested on `data`
608
        datas = [
3✔
609
            erc20.functions.name().build_transaction(params)["data"],
610
            erc20.functions.symbol().build_transaction(params)["data"],
611
            erc20.functions.decimals().build_transaction(params)["data"],
612
        ]
613
        payload = [
3✔
614
            {
615
                "id": i,
616
                "jsonrpc": "2.0",
617
                "method": "eth_call",
618
                "params": [{"to": erc20_address, "data": data}, "latest"],
619
            }
620
            for i, data in enumerate(datas)
621
        ]
622
        response = self.http_session.post(
3✔
623
            self.ethereum_client.ethereum_node_url,
624
            json=payload,
625
            timeout=self.slow_timeout,
626
        )
627
        if not response.ok:
3✔
628
            raise InvalidERC20Info(response.content)
×
629
        try:
3✔
630
            response_json = sorted(response.json(), key=lambda x: x["id"])
3✔
631
            errors = [r["error"] for r in response_json if "error" in r]
3✔
632
            if errors:
3✔
633
                raise InvalidERC20Info(f"{erc20_address} - {errors}")
×
634
            results = [HexBytes(r["result"]) for r in response_json]
3✔
635
            name = decode_string_or_bytes32(results[0])
3✔
636
            symbol = decode_string_or_bytes32(results[1])
3✔
637
            decimals = eth_abi.decode(["uint8"], results[2])[0]
3✔
638
            return Erc20Info(name, symbol, decimals)
3✔
639
        except (Web3Exception, DecodingError, ValueError) as e:
3✔
640
            raise InvalidERC20Info from e
3✔
641

642
    def get_total_transfer_history(
3✔
643
        self,
644
        addresses: Optional[Sequence[ChecksumAddress]] = None,
645
        from_block: BlockIdentifier = BlockNumber(0),
646
        to_block: Optional[BlockIdentifier] = None,
647
        token_address: Optional[ChecksumAddress] = None,
648
    ) -> List[LogReceiptDecoded]:
649
        """
650
        Get events for erc20 and erc721 transfers from and to an `address`. We decode it manually.
651
        Example of an erc20 event:
652

653
        .. code-block:: python
654

655
            {'logIndex': 0,
656
             'transactionIndex': 0,
657
             'transactionHash': HexBytes('0x4d0f25313603e554e3b040667f7f391982babbd195c7ae57a8c84048189f7794'),
658
             'blockHash': HexBytes('0x90fa67d848a0eaf3be625235dae28815389f5292d4465c48d1139f0c207f8d42'),
659
             'blockNumber': 791,
660
             'address': '0xf7d0Bd47BF3214494E7F5B40E392A25cb4788620',
661
             'data': '0x000000000000000000000000000000000000000000000000002001f716742000',
662
             'topics': [HexBytes('0xddf252ad1be2c89b69c2b068fc378daa952ba7f163c4a11628f55a4df523b3ef'),
663
              HexBytes('0x000000000000000000000000f5984365fca2e3bc7d2e020abb2c701df9070eb7'),
664
              HexBytes('0x0000000000000000000000001df62f291b2e969fb0849d99d9ce41e2f137006e')],
665
             'type': 'mined'
666
             'args': {'from': '0xf5984365FcA2e3bc7D2E020AbB2c701DF9070eB7',
667
                      'to': '0x1dF62f291b2E969fB0849d99D9Ce41e2F137006e',
668
                      'value': 9009360000000000
669
                     }
670
            }
671
            An example of an erc721 event
672
            {'address': '0x6631FcbB50677DfC6c02CCDcc03a8f68Db427a64',
673
             'blockHash': HexBytes('0x95c71c6c9373e9a8ca2c767dda1cd5083eb6addcce36fc216c9e1f458d6970f9'),
674
             'blockNumber': 5341681,
675
             'data': '0x',
676
             'logIndex': 0,
677
             'removed': False,
678
             'topics': [HexBytes('0xddf252ad1be2c89b69c2b068fc378daa952ba7f163c4a11628f55a4df523b3ef'),
679
              HexBytes('0x0000000000000000000000000000000000000000000000000000000000000000'),
680
              HexBytes('0x000000000000000000000000b5239c032ab9fb5abfc3903e770a4b6a9095542c'),
681
              HexBytes('0x0000000000000000000000000000000000000000000000000000000000000063')],
682
             'transactionHash': HexBytes('0xce8c8af0503e6f8a421345c10cdf92834c95186916a3f5b1437d2bba63d2db9e'),
683
             'transactionIndex': 0,
684
             'transactionLogIndex': '0x0',
685
             'type': 'mined',
686
             'args': {'from': '0x0000000000000000000000000000000000000000',
687
                      'to': '0xb5239C032AB9fB5aBFc3903e770A4B6a9095542C',
688
                      'tokenId': 99
689
                     }
690
             }
691
            An example of unknown transfer event (no indexed parts), could be a ERC20 or ERC721 transfer:
692
            {'address': '0x6631FcbB50677DfC6c02CCDcc03a8f68Db427a64',
693
             'blockHash': HexBytes('0x95c71c6c9373e9a8ca2c767dda1cd5083eb6addcce36fc216c9e1f458d6970f9'),
694
             'blockNumber': 5341681,
695
             'data': '0x',
696
             'logIndex': 0,
697
             'removed': False,
698
             'topics': [HexBytes('0xddf252ad1be2c89b69c2b068fc378daa952ba7f163c4a11628f55a4df523b3ef'),
699
              HexBytes('0x0000000000000000000000000000000000000000000000000000000000000000'),
700
              HexBytes('0x000000000000000000000000b5239c032ab9fb5abfc3903e770a4b6a9095542c'),
701
              HexBytes('0x0000000000000000000000000000000000000000000000000000000000000063')],
702
             'transactionHash': HexBytes('0xce8c8af0503e6f8a421345c10cdf92834c95186916a3f5b1437d2bba63d2db9e'),
703
             'transactionIndex': 0,
704
             'transactionLogIndex': '0x0',
705
             'type': 'mined',
706
             'args': {'from': '0x0000000000000000000000000000000000000000',
707
                      'to': '0xb5239C032AB9fB5aBFc3903e770A4B6a9095542C',
708
                      'unknown': 99
709
                     }
710
             }
711

712
        :param addresses: Search events `from` and `to` these `addresses`. If not, every transfer event within the
713
            range will be retrieved
714
        :param from_block: Block to start querying from
715
        :param to_block: Block to stop querying from
716
        :param token_address: Address of the token
717
        :return: List of events sorted by blockNumber
718
        """
719
        topic_0 = to_0x_hex_str(self.TRANSFER_TOPIC)
3✔
720
        if addresses:
3✔
721
            addresses_encoded = [
3✔
722
                to_0x_hex_str(eth_abi.encode(["address"], [address]))
723
                for address in addresses
724
            ]
725
            # Topics for transfer `to` and `from` an address
726
            all_topics: List[Sequence[Any]] = [
3✔
727
                [topic_0, addresses_encoded],  # Topics from
728
                [topic_0, None, addresses_encoded],  # Topics to
729
            ]
730
        else:
731
            all_topics = [[topic_0]]  # All transfer events
3✔
732
        parameters: FilterParams = {"fromBlock": from_block}
3✔
733
        if to_block:
3✔
734
            parameters["toBlock"] = to_block
3✔
735
        if token_address:
3✔
736
            parameters["address"] = token_address
3✔
737

738
        erc20_events = []
3✔
739
        # Do the request to `eth_getLogs`
740
        for topics in all_topics:
3✔
741
            parameters["topics"] = topics
3✔
742

743
            # Decode events. Just pick valid ERC20 Transfer events (ERC721 `Transfer` has the same signature)
744
            for event in self.slow_w3.eth.get_logs(parameters):
3✔
745
                event_args = self._decode_transfer_log(event["data"], event["topics"])
3✔
746
                if event_args:
3✔
747
                    erc20_events.append(LogReceiptDecoded(**event, args=event_args))
3✔
748

749
        erc20_events.sort(key=lambda x: x["blockNumber"])
3✔
750
        return erc20_events
3✔
751

752
    def get_transfer_history(
3✔
753
        self,
754
        from_block: int,
755
        to_block: Optional[int] = None,
756
        from_address: Optional[str] = None,
757
        to_address: Optional[str] = None,
758
        token_address: Optional[str] = None,
759
    ) -> List[Dict[str, Any]]:
760
        """
761
        DON'T USE, it will fail in some cases until they fix https://github.com/ethereum/web3.py/issues/1351
762
        Get events for erc20/erc721 transfers. At least one of `from_address`, `to_address` or `token_address` must be
763
        defined. Example of decoded event:
764

765
        .. code-block:: python
766

767
            {
768
                "args": {
769
                    "from": "0x1Ce67Ea59377A163D47DFFc9BaAB99423BE6EcF1",
770
                    "to": "0xaE9E15896fd32E59C7d89ce7a95a9352D6ebD70E",
771
                    "value": 15000000000000000
772
                },
773
                "event": "Transfer",
774
                "logIndex": 42,
775
                "transactionIndex": 60,
776
                "transactionHash": "0x71d6d83fef3347bad848e83dfa0ab28296e2953de946ee152ea81c6dfb42d2b3",
777
                "address": "0xfecA834E7da9D437645b474450688DA9327112a5",
778
                "blockHash": "0x054de9a496fc7d10303068cbc7ee3e25181a3b26640497859a5e49f0342e7db2",
779
                "blockNumber": 7265022
780
            }
781

782
        :param from_block: Block to start querying from
783
        :param to_block: Block to stop querying from
784
        :param from_address: Address sending the erc20 transfer
785
        :param to_address: Address receiving the erc20 transfer
786
        :param token_address: Address of the token
787
        :return: List of events (decoded)
788
        :throws: ReadTimeout
789
        """
790
        assert (
3✔
791
            from_address or to_address or token_address
792
        ), "At least one parameter must be provided"
793

794
        erc20 = get_erc20_contract(self.slow_w3)
3✔
795

796
        argument_filters = {}
3✔
797
        if from_address:
3✔
798
            argument_filters["from"] = from_address
3✔
799
        if to_address:
3✔
800
            argument_filters["to"] = to_address
3✔
801

802
        return erc20.events.Transfer.create_filter(
3✔
803
            from_block=from_block,
804
            to_block=to_block,
805
            address=token_address,
806
            argument_filters=argument_filters,
807
        ).get_all_entries()
808

809
    def send_tokens(
3✔
810
        self,
811
        to: str,
812
        amount: int,
813
        erc20_address: ChecksumAddress,
814
        private_key: str,
815
        nonce: Optional[int] = None,
816
        gas_price: Optional[int] = None,
817
        gas: Optional[int] = None,
818
    ) -> bytes:
819
        """
820
        Send tokens to address
821

822
        :param to:
823
        :param amount:
824
        :param erc20_address:
825
        :param private_key:
826
        :param nonce:
827
        :param gas_price:
828
        :param gas:
829
        :return: tx_hash
830
        """
831
        erc20 = get_erc20_contract(self.w3, erc20_address)
3✔
832
        account = Account.from_key(private_key)
3✔
833
        tx_options: TxParams = {"from": account.address}
3✔
834
        if nonce:
3✔
835
            tx_options["nonce"] = Nonce(nonce)
×
836
        if gas_price:
3✔
837
            tx_options["gasPrice"] = Wei(gas_price)
×
838
        if gas:
3✔
839
            tx_options["gas"] = Wei(gas)
×
840

841
        tx = erc20.functions.transfer(to, amount).build_transaction(tx_options)
3✔
842
        return self.ethereum_client.send_unsigned_transaction(
3✔
843
            tx, private_key=private_key
844
        )
845

846

847
class Erc721Manager(EthereumClientManager):
3✔
848
    # keccak('Transfer(address,address,uint256)')
849
    # ddf252ad1be2c89b69c2b068fc378daa952ba7f163c4a11628f55a4df523b3ef
850
    TRANSFER_TOPIC = Erc20Manager.TRANSFER_TOPIC
3✔
851

852
    def get_balance(
3✔
853
        self, address: ChecksumAddress, token_address: ChecksumAddress
854
    ) -> int:
855
        """
856
        Get balance of address for `erc20_address`
857

858
        :param address: owner address
859
        :param token_address: erc721 token address
860
        :return: balance
861
        """
862
        return (
×
863
            get_erc721_contract(self.w3, token_address)
864
            .functions.balanceOf(address)
865
            .call()
866
        )
867

868
    def get_balances(
3✔
869
        self, address: ChecksumAddress, token_addresses: Sequence[ChecksumAddress]
870
    ) -> List[TokenBalance]:
871
        """
872
        Get balances for tokens for an `address`. If there's a problem with a token_address `0` will be
873
        returned for balance
874

875
        :param address: Owner address checksummed
876
        :param token_addresses: token addresses to check
877
        :return:
878
        """
879
        function = get_erc721_contract(self.ethereum_client.w3).functions.balanceOf(
×
880
            address
881
        )
882
        balances = self.ethereum_client.batch_call_same_function(
×
883
            function,
884
            token_addresses,
885
            raise_exception=False,
886
        )
887
        return [
×
888
            TokenBalance(token_address, balance if isinstance(balance, int) else 0)
889
            for (token_address, balance) in zip(token_addresses, balances)
890
        ]
891

892
    def get_info(self, token_address: ChecksumAddress) -> Erc721Info:
3✔
893
        """
894
        Get erc721 information (`name`, `symbol`). Use batching to get
895
        all info in the same request.
896

897
        :param token_address:
898
        :return: Erc721Info
899
        """
900
        erc721_contract = get_erc721_contract(self.w3, token_address)
×
901
        try:
×
902
            name, symbol = cast(
×
903
                List[str],
904
                self.ethereum_client.batch_call(
905
                    [
906
                        erc721_contract.functions.name(),
907
                        erc721_contract.functions.symbol(),
908
                    ]
909
                ),
910
            )
911
            return Erc721Info(name, symbol)
×
912
        except (DecodingError, ValueError):  # Not all the ERC721 have metadata
×
913
            raise InvalidERC721Info
×
914

915
    def get_owners(
3✔
916
        self, token_addresses_with_token_ids: Sequence[Tuple[ChecksumAddress, int]]
917
    ) -> List[Optional[ChecksumAddress]]:
918
        """
919
        :param token_addresses_with_token_ids: Tuple(token_address: str, token_id: int)
920
        :return: List of owner addresses, `None` if not found
921
        """
922
        return [
×
923
            ChecksumAddress(HexAddress(HexStr(owner)))
924
            if isinstance(owner, str)
925
            else None
926
            for owner in self.ethereum_client.batch_call(
927
                [
928
                    get_erc721_contract(
929
                        self.ethereum_client.w3, token_address
930
                    ).functions.ownerOf(token_id)
931
                    for token_address, token_id in token_addresses_with_token_ids
932
                ],
933
                raise_exception=False,
934
            )
935
        ]
936

937
    def get_token_uris(
3✔
938
        self, token_addresses_with_token_ids: Sequence[Tuple[ChecksumAddress, int]]
939
    ) -> List[Optional[str]]:
940
        """
941
        :param token_addresses_with_token_ids: Tuple(token_address: str, token_id: int)
942
        :return: List of token_uris, `None` if not found
943
        """
944
        return [
×
945
            token_uri if isinstance(token_uri, str) else None
946
            for token_uri in self.ethereum_client.batch_call(
947
                [
948
                    get_erc721_contract(
949
                        self.ethereum_client.w3, token_address
950
                    ).functions.tokenURI(token_id)
951
                    for token_address, token_id in token_addresses_with_token_ids
952
                ],
953
                raise_exception=False,
954
            )
955
        ]
956

957

958
class TracingManager(EthereumClientManager):
3✔
959
    def filter_out_errored_traces(
3✔
960
        self, internal_txs: Sequence[Dict[str, Any]]
961
    ) -> Sequence[Dict[str, Any]]:
962
        """
963
        Filter out errored transactions (traces that are errored or that have an errored parent)
964

965
        :param internal_txs: Traces for the SAME ethereum tx, sorted ascending by `trace_address`
966
            `sorted(t, key = lambda i: i['traceAddress'])`. It's the default output from methods returning `traces` like
967
            `trace_block` or `trace_transaction`
968
        :return: List of not errored traces
969
        """
970
        new_list = []
3✔
971
        errored_trace_address: Optional[List[int]] = None
3✔
972
        for internal_tx in internal_txs:
3✔
973
            if internal_tx.get("error") is not None:
3✔
974
                errored_trace_address = internal_tx["traceAddress"]
3✔
975
            elif (
3✔
976
                errored_trace_address is not None
977
                and internal_tx["traceAddress"][: len(errored_trace_address)]
978
                == errored_trace_address
979
            ):
980
                continue
3✔
981
            else:
982
                new_list.append(internal_tx)
3✔
983
        return new_list
3✔
984

985
    def get_previous_trace(
3✔
986
        self,
987
        tx_hash: EthereumHash,
988
        trace_address: Sequence[int],
989
        number_traces: int = 1,
990
        skip_delegate_calls: bool = False,
991
    ) -> Optional[Dict[str, Any]]:
992
        """
993
        :param tx_hash:
994
        :param trace_address:
995
        :param number_traces: Number of traces to skip, by default get the immediately previous one
996
        :param skip_delegate_calls: If True filter out delegate calls
997
        :return: Parent trace for a trace
998
        :raises: ``ValueError`` if tracing is not supported
999
        """
1000
        if len(trace_address) < number_traces:
3✔
1001
            return None
3✔
1002

1003
        trace_address = trace_address[:-number_traces]
3✔
1004
        traces = reversed(self.trace_transaction(tx_hash))
3✔
1005
        for trace in traces:
3✔
1006
            if trace_address == trace["traceAddress"]:
3✔
1007
                if (
3✔
1008
                    skip_delegate_calls
1009
                    and trace["action"].get("callType") == "delegatecall"
1010
                ):
1011
                    trace_address = trace_address[:-1]
3✔
1012
                else:
1013
                    return trace
3✔
1014
        return None
×
1015

1016
    def get_next_traces(
3✔
1017
        self,
1018
        tx_hash: EthereumHash,
1019
        trace_address: Sequence[int],
1020
        remove_delegate_calls: bool = False,
1021
        remove_calls: bool = False,
1022
    ) -> List[FilterTrace]:
1023
        """
1024
        :param tx_hash:
1025
        :param trace_address:
1026
        :param remove_delegate_calls: If True remove delegate calls from result
1027
        :param remove_calls: If True remove calls from result
1028
        :return: Children for a trace, E.g. if address is [0, 1] and number_traces = 1, it will return [0, 1, x]
1029
        :raises: ``ValueError`` if tracing is not supported
1030
        """
1031
        trace_address_len = len(trace_address)
3✔
1032
        traces: List[FilterTrace] = []
3✔
1033
        for trace in self.trace_transaction(tx_hash):
3✔
1034
            if (
3✔
1035
                trace_address_len + 1 == len(trace["traceAddress"])
1036
                and trace_address == trace["traceAddress"][:-1]
1037
            ):
1038
                if (
3✔
1039
                    remove_delegate_calls
1040
                    and trace["action"].get("callType") == "delegatecall"
1041
                ):
1042
                    pass
3✔
1043
                elif remove_calls and trace["action"].get("callType") == "call":
3✔
1044
                    pass
3✔
1045
                else:
1046
                    traces.append(trace)
3✔
1047
        return traces
3✔
1048

1049
    def trace_block(self, block_identifier: BlockIdentifier) -> List[BlockTrace]:
3✔
1050
        return self.slow_w3.tracing.trace_block(block_identifier)  # type: ignore[attr-defined]
3✔
1051

1052
    def trace_blocks(
3✔
1053
        self, block_identifiers: Sequence[BlockIdentifier]
1054
    ) -> List[List[BlockTrace]]:
1055
        if not block_identifiers:
3✔
1056
            return []
×
1057
        payload = [
3✔
1058
            {
1059
                "id": i,
1060
                "jsonrpc": "2.0",
1061
                "method": "trace_block",
1062
                "params": [
1063
                    hex(block_identifier)
1064
                    if isinstance(block_identifier, int)
1065
                    else block_identifier
1066
                ],
1067
            }
1068
            for i, block_identifier in enumerate(block_identifiers)
1069
        ]
1070

1071
        results = self.ethereum_client.raw_batch_request(payload)
3✔
1072
        return [trace_list_result_formatter(block_traces) for block_traces in results]  # type: ignore[arg-type]
3✔
1073

1074
    def trace_transaction(self, tx_hash: EthereumHash) -> List[FilterTrace]:
3✔
1075
        """
1076
        :param tx_hash:
1077
        :return: List of internal txs for `tx_hash`
1078
        """
1079
        return self.slow_w3.tracing.trace_transaction(tx_hash)  # type: ignore[attr-defined]
3✔
1080

1081
    def trace_transactions(
3✔
1082
        self, tx_hashes: Sequence[EthereumHash]
1083
    ) -> List[List[FilterTrace]]:
1084
        """
1085
        :param tx_hashes:
1086
        :return: For every `tx_hash` a list of internal txs (in the same order as the `tx_hashes` were provided)
1087
        """
1088
        if not tx_hashes:
3✔
1089
            return []
×
1090
        payload = [
3✔
1091
            {
1092
                "id": i,
1093
                "jsonrpc": "2.0",
1094
                "method": "trace_transaction",
1095
                "params": [to_0x_hex_str(HexBytes(tx_hash))],
1096
            }
1097
            for i, tx_hash in enumerate(tx_hashes)
1098
        ]
1099
        results = self.ethereum_client.raw_batch_request(payload)
3✔
1100
        return [trace_list_result_formatter(tx_traces) for tx_traces in results]  # type: ignore[arg-type]
3✔
1101

1102
    def trace_filter(
3✔
1103
        self,
1104
        from_block: int = 1,
1105
        to_block: Optional[int] = None,
1106
        from_address: Optional[Sequence[ChecksumAddress]] = None,
1107
        to_address: Optional[Sequence[ChecksumAddress]] = None,
1108
        after: Optional[int] = None,
1109
        count: Optional[int] = None,
1110
    ) -> List[FilterTrace]:
1111
        """
1112
        Get events using ``trace_filter`` method
1113

1114
        :param from_block: Quantity or Tag - (optional) From this block. `0` is not working, it needs to be `>= 1`
1115
        :param to_block: Quantity or Tag - (optional) To this block.
1116
        :param from_address: Array - (optional) Sent from these addresses.
1117
        :param to_address: Address - (optional) Sent to these addresses.
1118
        :param after: Quantity - (optional) The offset trace number
1119
        :param count: Quantity - (optional) Integer number of traces to display in a batch.
1120
        :return:
1121

1122
        .. code-block:: python
1123

1124
            [
1125
                {
1126
                    "action": {
1127
                        "callType": "call",
1128
                        "from": "0x32be343b94f860124dc4fee278fdcbd38c102d88",
1129
                        "gas": "0x4c40d",
1130
                        "input": "0x",
1131
                        "to": "0x8bbb73bcb5d553b5a556358d27625323fd781d37",
1132
                        "value": "0x3f0650ec47fd240000"
1133
                    },
1134
                    "blockHash": "0x86df301bcdd8248d982dbf039f09faf792684e1aeee99d5b58b77d620008b80f",
1135
                    "blockNumber": 3068183,
1136
                    "result": {
1137
                        "gasUsed": "0x0",
1138
                        "output": "0x"
1139
                    },
1140
                    "subtraces": 0,
1141
                    "traceAddress": [],
1142
                    "transactionHash": "0x3321a7708b1083130bd78da0d62ead9f6683033231617c9d268e2c7e3fa6c104",
1143
                    "transactionPosition": 3,
1144
                    "type": "call"
1145
                },
1146
                {
1147
                    "action": {
1148
                        "from": "0x3b169a0fb55ea0b6bafe54c272b1fe4983742bf7",
1149
                        "gas": "0x49b0b",
1150
                        "init": "0x608060405234801561001057600080fd5b5060405161060a38038061060a833981018060405281019080805190602001909291908051820192919060200180519060200190929190805190602001909291908051906020019092919050505084848160008173ffffffffffffffffffffffffffffffffffffffff1614151515610116576040517f08c379a00000000000000000000000000000000000000000000000000000000081526004018080602001828103825260248152602001807f496e76616c6964206d617374657220636f707920616464726573732070726f7681526020017f696465640000000000000000000000000000000000000000000000000000000081525060400191505060405180910390fd5b806000806101000a81548173ffffffffffffffffffffffffffffffffffffffff021916908373ffffffffffffffffffffffffffffffffffffffff160217905550506000815111156101a35773ffffffffffffffffffffffffffffffffffffffff60005416600080835160208501846127105a03f46040513d6000823e600082141561019f573d81fd5b5050505b5050600081111561036d57600073ffffffffffffffffffffffffffffffffffffffff168273ffffffffffffffffffffffffffffffffffffffff1614156102b7578273ffffffffffffffffffffffffffffffffffffffff166108fc829081150290604051600060405180830381858888f1935050505015156102b2576040517f08c379a00000000000000000000000000000000000000000000000000000000081526004018080602001828103825260268152602001807f436f756c64206e6f74207061792073616665206372656174696f6e207769746881526020017f206574686572000000000000000000000000000000000000000000000000000081525060400191505060405180910390fd5b61036c565b6102d1828483610377640100000000026401000000009004565b151561036b576040517f08c379a00000000000000000000000000000000000000000000000000000000081526004018080602001828103825260268152602001807f436f756c64206e6f74207061792073616665206372656174696f6e207769746881526020017f20746f6b656e000000000000000000000000000000000000000000000000000081525060400191505060405180910390fd5b5b5b5050505050610490565b600060608383604051602401808373ffffffffffffffffffffffffffffffffffffffff1673ffffffffffffffffffffffffffffffffffffffff168152602001828152602001925050506040516020818303038152906040527fa9059cbb000000000000000000000000000000000000000000000000000000007bffffffffffffffffffffffffffffffffffffffffffffffffffffffff19166020820180517bffffffffffffffffffffffffffffffffffffffffffffffffffffffff838183161783525050505090506000808251602084016000896127105a03f16040513d6000823e3d60008114610473576020811461047b5760009450610485565b829450610485565b8151158315171594505b505050509392505050565b61016b8061049f6000396000f30060806040526004361061004c576000357c0100000000000000000000000000000000000000000000000000000000900463ffffffff1680634555d5c91461008b5780635c60da1b146100b6575b73ffffffffffffffffffffffffffffffffffffffff600054163660008037600080366000845af43d6000803e6000811415610086573d6000fd5b3d6000f35b34801561009757600080fd5b506100a061010d565b6040518082815260200191505060405180910390f35b3480156100c257600080fd5b506100cb610116565b604051808273ffffffffffffffffffffffffffffffffffffffff1673ffffffffffffffffffffffffffffffffffffffff16815260200191505060405180910390f35b60006002905090565b60008060009054906101000a900473ffffffffffffffffffffffffffffffffffffffff169050905600a165627a7a7230582007fffd557dfc8c4d2fdf56ba6381a6ce5b65b6260e1492d87f26c6d4f1d0410800290000000000000000000000008942595a2dc5181df0465af0d7be08c8f23c93af00000000000000000000000000000000000000000000000000000000000000a0000000000000000000000000d9e09beaeb338d81a7c5688358df0071d498811500000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000001b15f91a8c35300000000000000000000000000000000000000000000000000000000000001640ec78d9e00000000000000000000000000000000000000000000000000000000000000800000000000000000000000000000000000000000000000000000000000000002000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000001200000000000000000000000000000000000000000000000000000000000000004000000000000000000000000f763ea5fbb191d47dc4b083dcdc3cdfb586468f8000000000000000000000000ad25c9717d04c0a12086a1d352c1ccf4bf5fcbf80000000000000000000000000da7155692446c80a4e7ad72018e586f20fa3bfe000000000000000000000000bce0cc48ce44e0ac9ee38df4d586afbacef191fa0000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000",
1151
                        "value": "0x0"
1152
                    },
1153
                    "blockHash": "0x03f9f64dfeb7807b5df608e6957dd4d521fd71685aac5533451d27f0abe03660",
1154
                    "blockNumber": 3793534,
1155
                    "result": {
1156
                        "address": "0x61a7cc907c47c133d5ff5b685407201951fcbd08",
1157
                        "code": "0x60806040526004361061004c576000357c0100000000000000000000000000000000000000000000000000000000900463ffffffff1680634555d5c91461008b5780635c60da1b146100b6575b73ffffffffffffffffffffffffffffffffffffffff600054163660008037600080366000845af43d6000803e6000811415610086573d6000fd5b3d6000f35b34801561009757600080fd5b506100a061010d565b6040518082815260200191505060405180910390f35b3480156100c257600080fd5b506100cb610116565b604051808273ffffffffffffffffffffffffffffffffffffffff1673ffffffffffffffffffffffffffffffffffffffff16815260200191505060405180910390f35b60006002905090565b60008060009054906101000a900473ffffffffffffffffffffffffffffffffffffffff169050905600a165627a7a7230582007fffd557dfc8c4d2fdf56ba6381a6ce5b65b6260e1492d87f26c6d4f1d041080029",
1158
                        "gasUsed": "0x4683f"
1159
                    },
1160
                    "subtraces": 2,
1161
                    "traceAddress": [],
1162
                    "transactionHash": "0x6c7e8f8778d33d81b29c4bd7526ee50a4cea340d69eed6c89ada4e6fab731789",
1163
                    "transactionPosition": 1,
1164
                    "type": "create"
1165
                },
1166
                {
1167
                    'action': {
1168
                        'address': '0x4440adafbc6c4e45c299451c0eedc7c8b98c14ac',
1169
                        'balance': '0x0',
1170
                        'refundAddress': '0x0000000000000000000000000000000000000000'
1171
                    },
1172
                    'blockHash': '0x8512d367492371edf44ebcbbbd935bc434946dddc2b126cb558df5906012186c',
1173
                    'blockNumber': 7829689,
1174
                    'result': None,
1175
                    'subtraces': 0,
1176
                    'traceAddress': [0, 0, 0, 0, 0, 0],
1177
                    'transactionHash': '0x5f7af6aa390f9f8dd79ee692c37cbde76bb7869768b1bac438b6d176c94f637d',
1178
                    'transactionPosition': 35,
1179
                    'type': 'suicide'
1180
                }
1181
            ]
1182

1183
        """
1184
        assert (
3✔
1185
            from_address or to_address
1186
        ), "You must provide at least `from_address` or `to_address`"
1187
        parameters: TraceFilterParams = {}
3✔
1188
        if after:
3✔
1189
            parameters["after"] = after
×
1190
        if count:
3✔
1191
            parameters["count"] = count
×
1192
        if from_block:
3✔
1193
            parameters["fromBlock"] = HexStr("0x%x" % from_block)
3✔
1194
        if to_block:
3✔
1195
            parameters["toBlock"] = HexStr("0x%x" % to_block)
3✔
1196
        if from_address:
3✔
1197
            parameters["fromAddress"] = from_address
×
1198
        if to_address:
3✔
1199
            parameters["toAddress"] = to_address
3✔
1200

1201
        return self.slow_w3.tracing.trace_filter(parameters)  # type: ignore[attr-defined]
3✔
1202

1203

1204
class EthereumClient:
3✔
1205
    """
1206
    Manage ethereum operations. Uses web3 for the most part, but some other stuff is implemented from scratch.
1207
    Note: If you want to use `pending` state with `Parity`, it must be run with `--pruning=archive` or `--force-sealing`
1208
    """
1209

1210
    NULL_ADDRESS = NULL_ADDRESS
3✔
1211

1212
    def __init__(
3✔
1213
        self,
1214
        ethereum_node_url: URI = URI("http://localhost:8545"),
1215
        provider_timeout: int = 15,
1216
        slow_provider_timeout: int = 60,
1217
        retry_count: int = 1,
1218
        use_request_caching: bool = True,
1219
        batch_request_max_size: int = 500,
1220
    ):
1221
        """
1222
        :param ethereum_node_url: Ethereum RPC uri
1223
        :param provider_timeout: Timeout for regular RPC queries
1224
        :param slow_provider_timeout: Timeout for slow (tracing, logs...) and custom RPC queries
1225
        :param retry_count: Retry count for failed requests
1226
        :param use_request_caching: Use web3 request caching https://web3py.readthedocs.io/en/latest/internals.html#request-caching
1227
        :param batch_request_max_size: Max size for JSON RPC Batch requests. Some providers have a limitation on 500
1228
        """
1229
        self.http_session = prepare_http_session(1, 100, retry_count=retry_count)
3✔
1230
        self.ethereum_node_url: str = ethereum_node_url
3✔
1231
        self.timeout = provider_timeout
3✔
1232
        self.slow_timeout = slow_provider_timeout
3✔
1233
        self.use_request_caching = use_request_caching
3✔
1234

1235
        self.w3_provider = HTTPProvider(
3✔
1236
            self.ethereum_node_url,
1237
            cache_allowed_requests=use_request_caching,
1238
            request_kwargs={"timeout": provider_timeout},
1239
            session=self.http_session,
1240
        )
1241
        self.w3_slow_provider = HTTPProvider(
3✔
1242
            self.ethereum_node_url,
1243
            cache_allowed_requests=use_request_caching,
1244
            request_kwargs={"timeout": slow_provider_timeout},
1245
            session=self.http_session,
1246
        )
1247
        self.w3: Web3 = Web3(self.w3_provider)
3✔
1248
        self.slow_w3: Web3 = Web3(self.w3_slow_provider)
3✔
1249

1250
        # Adjust Web3.py middleware
1251
        for w3 in self.w3, self.slow_w3:
3✔
1252
            # Don't spend resources con converting dictionaries to attribute dictionaries
1253
            w3.middleware_onion.remove("attrdict")
3✔
1254

1255
        # The geth_poa_middleware is required to connect to geth --dev or the Goerli public network.
1256
        # It may also be needed for other EVM compatible blockchains like Polygon or BNB Chain (Binance Smart Chain).
1257
        try:
3✔
1258
            if self.get_network() != EthereumNetwork.MAINNET:
3✔
1259
                for w3 in self.w3, self.slow_w3:
3✔
1260
                    w3.middleware_onion.inject(ExtraDataToPOAMiddleware, layer=0)
3✔
1261
        except (IOError, OSError):
×
1262
            # For tests using dummy connections (like IPC)
1263
            for w3 in self.w3, self.slow_w3:
×
1264
                w3.middleware_onion.inject(ExtraDataToPOAMiddleware, layer=0)
×
1265

1266
        self.erc20: Erc20Manager = Erc20Manager(self)
3✔
1267
        self.erc721: Erc721Manager = Erc721Manager(self)
3✔
1268
        self.tracing: TracingManager = TracingManager(self)
3✔
1269
        self.batch_call_manager: BatchCallManager = BatchCallManager(self)
3✔
1270
        self.batch_request_max_size = batch_request_max_size
3✔
1271

1272
    def __str__(self):
3✔
1273
        return f"EthereumClient for url={self.ethereum_node_url}"
3✔
1274

1275
    def raw_batch_request(
3✔
1276
        self, payload: Sequence[Dict[str, Any]], batch_size: Optional[int] = None
1277
    ) -> Iterable[Union[Optional[Dict[str, Any]], List[Dict[str, Any]]]]:
1278
        """
1279
        Perform a raw batch JSON RPC call
1280

1281
        :param payload: Batch request payload. Make sure all provided `ids` inside the payload are different
1282
        :param batch_size: If `payload` length is bigger than size, it will be split into smaller chunks before
1283
            sending to the server
1284
        :return:
1285
        :raises: ValueError
1286
        """
1287

1288
        batch_size = batch_size or self.batch_request_max_size
3✔
1289

1290
        for payload_chunk in chunks(payload, batch_size):
3✔
1291
            response = self.http_session.post(
3✔
1292
                self.ethereum_node_url, json=payload_chunk, timeout=self.slow_timeout
1293
            )
1294

1295
            if not response.ok:
3✔
1296
                logger.error(
×
1297
                    "Problem doing raw batch request with payload=%s status_code=%d result=%s",
1298
                    payload_chunk,
1299
                    response.status_code,
1300
                    response.content,
1301
                )
1302
                raise ValueError(f"Batch request error: {response.content!r}")
×
1303

1304
            results = response.json()
3✔
1305

1306
            # If there's an error some nodes return a json instead of a list, and other return a list of one element
1307
            if (isinstance(results, dict) and "error" in results) or (
3✔
1308
                isinstance(results, list)
1309
                and len(results) == 1
1310
                and "error" in results[0]
1311
            ):
1312
                logger.error(
3✔
1313
                    "Batch request problem with payload=%s, result=%s)",
1314
                    payload_chunk,
1315
                    results,
1316
                )
1317
                raise ValueError(f"Batch request error: {results}")
3✔
1318

1319
            if len(results) != len(payload_chunk):
3✔
1320
                logger.error(
3✔
1321
                    "Different number of results than payload requests were returned doing raw batch request "
1322
                    "with payload=%s result=%s",
1323
                    payload_chunk,
1324
                    response.content,
1325
                )
1326
                raise ValueError(
3✔
1327
                    "Batch request error: Different number of results than payload requests were returned"
1328
                )
1329

1330
            # Sorted due to Nodes like Erigon send back results out of order
1331
            for query, result in zip(payload, sorted(results, key=lambda x: x["id"])):
3✔
1332
                if "result" not in result:
3✔
1333
                    message = (
×
1334
                        f"Batch request problem with payload=`{query}` result={result}"
1335
                    )
1336
                    logger.error(message)
×
1337
                    raise ValueError(f"Batch request error: {message}")
×
1338

1339
                yield result["result"]
3✔
1340

1341
    @property
3✔
1342
    def current_block_number(self):
3✔
1343
        return self.w3.eth.block_number
3✔
1344

1345
    @cache
3✔
1346
    def get_chain_id(self) -> int:
3✔
1347
        """
1348
        :return: ChainId returned by the RPC `eth_chainId` method. It should never change, so it's cached.
1349
        """
1350
        return int(self.w3.eth.chain_id)
3✔
1351

1352
    @cache
3✔
1353
    def get_client_version(self) -> str:
3✔
1354
        """
1355
        :return: RPC version information
1356
        """
1357
        return self.w3.client_version
×
1358

1359
    def get_network(self) -> EthereumNetwork:
3✔
1360
        """
1361
        Get network name based on the chainId. This method is not cached as the method for getting the
1362
        `chainId` already is.
1363

1364
        :return: EthereumNetwork based on the chainId. If network is not
1365
            on our list, `EthereumNetwork.UNKNOWN` is returned
1366
        """
1367
        return EthereumNetwork(self.get_chain_id())
3✔
1368

1369
    @cache
3✔
1370
    def get_singleton_factory_address(self) -> Optional[ChecksumAddress]:
3✔
1371
        """
1372
        Get singleton factory address if available. Try the singleton managed by Safe by default unless
1373
        SAFE_SINGLETON_FACTORY_ADDRESS environment variable is defined.
1374

1375
        More info: https://github.com/safe-global/safe-singleton-factory
1376

1377
        :return: Get singleton factory address if available
1378
        """
1379
        address = os.environ.get(
3✔
1380
            "SAFE_SINGLETON_FACTORY_ADDRESS", SAFE_SINGLETON_FACTORY_ADDRESS
1381
        )
1382
        address_checksum = ChecksumAddress(HexAddress(HexStr(address)))
3✔
1383
        if self.is_contract(address_checksum):
3✔
1384
            return address_checksum
3✔
1385
        return None
3✔
1386

1387
    @cache
3✔
1388
    def is_eip1559_supported(self) -> bool:
3✔
1389
        """
1390
        :return: `True` if EIP1559 is supported by the node, `False` otherwise
1391
        """
1392
        try:
3✔
1393
            self.w3.eth.fee_history(1, "latest", reward_percentiles=[50])
3✔
1394
            return True
3✔
1395
        except (Web3Exception, ValueError):
×
1396
            return False
×
1397

1398
    @cached_property
3✔
1399
    def multicall(self) -> "Multicall":  # type: ignore # noqa F821
3✔
1400
        from .multicall import Multicall
3✔
1401

1402
        try:
3✔
1403
            return Multicall(self)
3✔
1404
        except EthereumNetworkNotSupported:
×
1405
            logger.warning("Multicall not supported for this network")
×
1406
            return None
×
1407

1408
    def batch_call(
3✔
1409
        self,
1410
        contract_functions: Iterable[ContractFunction],
1411
        from_address: Optional[ChecksumAddress] = None,
1412
        raise_exception: bool = True,
1413
        force_batch_call: bool = False,
1414
        block_identifier: Optional[BlockIdentifier] = "latest",
1415
    ) -> List[Optional[Union[bytes, Any]]]:
1416
        """
1417
        Call multiple functions. ``Multicall`` contract by MakerDAO will be used by default if available
1418

1419
        :param contract_functions:
1420
        :param from_address: Only available when ``Multicall`` is not used
1421
        :param raise_exception: If ``True``, raise ``BatchCallException`` if one of the calls fails
1422
        :param force_batch_call: If ``True``, ignore multicall and always use batch calls to get the
1423
            result (less optimal). If ``False``, more optimal way will be tried.
1424
        :param block_identifier:
1425
        :return: List of elements decoded to their types, ``None`` if they cannot be decoded and
1426
            bytes if a revert error is returned and ``raise_exception=False``
1427
        :raises: BatchCallException
1428
        """
1429
        if self.multicall and not force_batch_call:  # Multicall is more optimal
3✔
1430
            return [
3✔
1431
                result.return_data_decoded
1432
                for result in self.multicall.try_aggregate(
1433
                    contract_functions,
1434
                    require_success=raise_exception,
1435
                    block_identifier=block_identifier,
1436
                )
1437
            ]
1438
        else:
1439
            return self.batch_call_manager.batch_call(
×
1440
                contract_functions,
1441
                from_address=from_address,
1442
                raise_exception=raise_exception,
1443
                block_identifier=block_identifier,
1444
            )
1445

1446
    def batch_call_same_function(
3✔
1447
        self,
1448
        contract_function: ContractFunction,
1449
        contract_addresses: Sequence[ChecksumAddress],
1450
        from_address: Optional[ChecksumAddress] = None,
1451
        raise_exception: bool = True,
1452
        force_batch_call: bool = False,
1453
        block_identifier: Optional[BlockIdentifier] = "latest",
1454
    ) -> List[Optional[Union[bytes, Any]]]:
1455
        """
1456
        Call the same function in multiple contracts. Way more optimal than using ``batch_call`` generating multiple
1457
        ``ContractFunction`` objects.
1458

1459
        :param contract_function:
1460
        :param contract_addresses:
1461
        :param from_address: Only available when ``Multicall`` is not used
1462
        :param raise_exception: If ``True``, raise ``BatchCallException`` if one of the calls fails
1463
        :param force_batch_call: If ``True``, ignore multicall and always use batch calls to get the
1464
            result (less optimal). If ``False``, more optimal way will be tried.
1465
        :param block_identifier:
1466
        :return: List of elements decoded to the same type, ``None`` if they cannot be decoded and
1467
            bytes if a revert error is returned and ``raise_exception=False``
1468
        :raises: BatchCallException
1469
        """
1470
        if self.multicall and not force_batch_call:  # Multicall is more optimal
3✔
1471
            return [
3✔
1472
                result.return_data_decoded
1473
                for result in self.multicall.try_aggregate_same_function(
1474
                    contract_function,
1475
                    contract_addresses,
1476
                    require_success=raise_exception,
1477
                    block_identifier=block_identifier,
1478
                )
1479
            ]
1480
        else:
1481
            return self.batch_call_manager.batch_call_same_function(
×
1482
                contract_function,
1483
                contract_addresses,
1484
                from_address=from_address,
1485
                raise_exception=raise_exception,
1486
                block_identifier=block_identifier,
1487
            )
1488

1489
    def deploy_and_initialize_contract(
3✔
1490
        self,
1491
        deployer_account: LocalAccount,
1492
        constructor_data: Union[bytes, HexStr],
1493
        initializer_data: Optional[Union[bytes, HexStr]] = None,
1494
        check_receipt: bool = True,
1495
        deterministic: bool = True,
1496
    ) -> EthereumTxSent:
1497
        """
1498

1499
        :param deployer_account:
1500
        :param constructor_data:
1501
        :param initializer_data:
1502
        :param check_receipt:
1503
        :param deterministic: Use Safe singleton factory for CREATE2 deterministic deployment
1504
        :return:
1505
        :raises ValueError: No contract was deployed/initialized
1506
        """
1507
        contract_address: Optional[ChecksumAddress] = None
3✔
1508
        assert (
3✔
1509
            constructor_data or initializer_data
1510
        ), "At least constructor_data or initializer_data must be provided"
1511
        tx_hash: Optional[HexBytes] = None
3✔
1512
        tx: Optional[TxParams] = None
3✔
1513
        for data in (constructor_data, initializer_data):
3✔
1514
            # Because initializer_data is not mandatory
1515
            if data:
3✔
1516
                data = HexBytes(data)
3✔
1517
                tx = {
3✔
1518
                    "from": deployer_account.address,
1519
                    "data": data,
1520
                    "gasPrice": self.w3.eth.gas_price,
1521
                    "value": Wei(0),
1522
                    "to": contract_address if contract_address else "",
1523
                    "chainId": self.get_chain_id(),
1524
                    "nonce": self.get_nonce_for_account(deployer_account.address),
1525
                }
1526
                if not contract_address:
3✔
1527
                    if deterministic and (
3✔
1528
                        singleton_factory_address := self.get_singleton_factory_address()
1529
                    ):
1530
                        salt = HexBytes("0" * 64)
3✔
1531
                        tx["data"] = (
3✔
1532
                            salt + data
1533
                        )  # Add 32 bytes salt for singleton factory
1534
                        tx["to"] = singleton_factory_address
3✔
1535
                        contract_address = mk_contract_address_2(
3✔
1536
                            singleton_factory_address, salt, data
1537
                        )
1538
                        if self.is_contract(contract_address):
3✔
1539
                            raise ContractAlreadyDeployed(
×
1540
                                f"Contract {contract_address} already deployed",
1541
                                contract_address,
1542
                            )
1543
                    else:
1544
                        contract_address = mk_contract_address(tx["from"], tx["nonce"])
3✔
1545

1546
                tx["gas"] = self.w3.eth.estimate_gas(tx)
3✔
1547
                tx_hash = self.send_unsigned_transaction(
3✔
1548
                    tx, private_key=to_0x_hex_str(deployer_account.key)
1549
                )
1550
                if check_receipt:
3✔
1551
                    tx_receipt = self.get_transaction_receipt(
3✔
1552
                        Hash32(tx_hash), timeout=60
1553
                    )
1554
                    assert tx_receipt
3✔
1555
                    assert tx_receipt["status"]
3✔
1556

1557
        if tx_hash is not None and tx is not None:
3✔
1558
            return EthereumTxSent(tx_hash, tx, contract_address)
3✔
1559
        raise ValueError("No contract was deployed/initialized")
×
1560

1561
    def get_nonce_for_account(
3✔
1562
        self,
1563
        address: ChecksumAddress,
1564
        block_identifier: Optional[BlockIdentifier] = "latest",
1565
    ):
1566
        """
1567
        Get nonce for account. `getTransactionCount` is the only method for what `pending` is currently working
1568
        (Geth and Parity)
1569

1570
        :param address:
1571
        :param block_identifier:
1572
        :return:
1573
        """
1574
        return self.w3.eth.get_transaction_count(
3✔
1575
            address, block_identifier=block_identifier
1576
        )
1577

1578
    def estimate_gas(
3✔
1579
        self,
1580
        to: str,
1581
        from_: Optional[str] = None,
1582
        value: Optional[int] = None,
1583
        data: Optional[EthereumData] = None,
1584
        gas: Optional[int] = None,
1585
        gas_price: Optional[int] = None,
1586
        block_identifier: Optional[BlockIdentifier] = None,
1587
    ) -> int:
1588
        """
1589
        Estimate gas calling `eth_estimateGas`
1590

1591
        :param from_:
1592
        :param to:
1593
        :param value:
1594
        :param data:
1595
        :param gas:
1596
        :param gas_price:
1597
        :param block_identifier: Be careful, `Geth` does not support `pending` when estimating
1598
        :return: Amount of gas needed for transaction
1599
        :raises: ValueError
1600
        """
1601
        tx: TxParams = {"to": to}
3✔
1602
        if from_:
3✔
1603
            tx["from"] = from_
3✔
1604
        if value:
3✔
1605
            tx["value"] = Wei(value)
3✔
1606
        if data:
3✔
1607
            tx["data"] = data
3✔
1608
        if gas:
3✔
1609
            tx["gas"] = gas
×
1610
        if gas_price:
3✔
1611
            tx["gasPrice"] = Wei(gas_price)
×
1612
        try:
3✔
1613
            return self.w3.eth.estimate_gas(tx, block_identifier=block_identifier)
3✔
1614
        except (Web3Exception, ValueError):
3✔
1615
            if (
3✔
1616
                block_identifier is not None
1617
            ):  # Geth does not support setting `block_identifier`
1618
                return self.w3.eth.estimate_gas(tx, block_identifier=None)
×
1619
            else:
1620
                raise
3✔
1621

1622
    @staticmethod
3✔
1623
    def estimate_data_gas(data: bytes):
3✔
1624
        """
1625
        Estimate gas costs only for "storage" of the ``data`` bytes provided
1626

1627
        :param data:
1628
        :return:
1629
        """
1630
        if isinstance(data, str):
3✔
1631
            data = HexBytes(data)
×
1632

1633
        gas = 0
3✔
1634
        for byte in data:
3✔
1635
            if not byte:
3✔
1636
                gas += GAS_CALL_DATA_ZERO_BYTE
3✔
1637
            else:
1638
                gas += GAS_CALL_DATA_BYTE
3✔
1639
        return gas
3✔
1640

1641
    def estimate_fee_eip1559(
3✔
1642
        self, tx_speed: TxSpeed = TxSpeed.NORMAL
1643
    ) -> Tuple[int, int]:
1644
        """
1645
        Check https://github.com/ethereum/execution-apis/blob/main/src/eth/fee_market.json#L15
1646

1647
        :return: Tuple[BaseFeePerGas, MaxPriorityFeePerGas]
1648
        :raises: ValueError if not supported on the network
1649
        """
1650
        if tx_speed == TxSpeed.SLOWEST:
3✔
1651
            percentile = 0
×
1652
        elif tx_speed == TxSpeed.VERY_SLOW:
3✔
1653
            percentile = 10
×
1654
        elif tx_speed == TxSpeed.SLOW:
3✔
1655
            percentile = 25
×
1656
        elif tx_speed == TxSpeed.NORMAL:
3✔
1657
            percentile = 50
3✔
1658
        elif tx_speed == TxSpeed.FAST:
×
1659
            percentile = 75
×
1660
        elif tx_speed == TxSpeed.VERY_FAST:
×
1661
            percentile = 90
×
1662
        elif tx_speed == TxSpeed.FASTEST:
×
1663
            percentile = 100
×
1664
        else:
1665
            percentile = 50
×
1666

1667
        result = self.w3.eth.fee_history(1, "latest", reward_percentiles=[percentile])
3✔
1668
        # Get next block `base_fee_per_gas`
1669
        base_fee_per_gas = result["baseFeePerGas"][-1]
3✔
1670
        max_priority_fee_per_gas = result["reward"][0][0]
3✔
1671
        return base_fee_per_gas, max_priority_fee_per_gas
3✔
1672

1673
    def set_eip1559_fees(
3✔
1674
        self, tx: TxParams, tx_speed: TxSpeed = TxSpeed.NORMAL
1675
    ) -> TxParams:
1676
        """
1677
        :return: TxParams in EIP1559 format
1678
        :raises: ValueError if EIP1559 not supported
1679
        """
1680
        base_fee_per_gas, max_priority_fee_per_gas = self.estimate_fee_eip1559(tx_speed)
3✔
1681
        tx = TxParams(**tx)  # Don't modify provided tx
3✔
1682
        if "gasPrice" in tx:
3✔
1683
            del tx["gasPrice"]
3✔
1684

1685
        if "chainId" not in tx:
3✔
1686
            tx["chainId"] = self.get_chain_id()
3✔
1687

1688
        tx["maxPriorityFeePerGas"] = Wei(max_priority_fee_per_gas)
3✔
1689
        tx["maxFeePerGas"] = Wei(base_fee_per_gas + max_priority_fee_per_gas)
3✔
1690
        return tx
3✔
1691

1692
    def get_balance(
3✔
1693
        self,
1694
        address: ChecksumAddress,
1695
        block_identifier: Optional[BlockIdentifier] = None,
1696
    ):
1697
        return self.w3.eth.get_balance(address, block_identifier)
3✔
1698

1699
    def get_transaction(self, tx_hash: EthereumHash) -> Optional[TxData]:
3✔
1700
        try:
3✔
1701
            return self.w3.eth.get_transaction(tx_hash)
3✔
1702
        except TransactionNotFound:
×
1703
            return None
×
1704

1705
    def get_transactions(
3✔
1706
        self, tx_hashes: Sequence[EthereumHash]
1707
    ) -> List[Optional[TxData]]:
1708
        if not tx_hashes:
3✔
1709
            return []
3✔
1710
        payload = [
3✔
1711
            {
1712
                "id": i,
1713
                "jsonrpc": "2.0",
1714
                "method": "eth_getTransactionByHash",
1715
                "params": [to_0x_hex_str(HexBytes(tx_hash))],
1716
            }
1717
            for i, tx_hash in enumerate(tx_hashes)
1718
        ]
1719
        results = self.raw_batch_request(payload)
3✔
1720
        return [
3✔
1721
            transaction_result_formatter(raw_tx) if raw_tx else None
1722
            for raw_tx in results
1723
        ]
1724

1725
    def get_transaction_receipt(
3✔
1726
        self, tx_hash: EthereumHash, timeout=None
1727
    ) -> Optional[TxReceipt]:
1728
        try:
3✔
1729
            if not timeout:
3✔
1730
                tx_receipt = self.w3.eth.get_transaction_receipt(tx_hash)
3✔
1731
            else:
1732
                try:
3✔
1733
                    tx_receipt = self.w3.eth.wait_for_transaction_receipt(
3✔
1734
                        tx_hash, timeout=timeout
1735
                    )
1736
                except TimeExhausted:
3✔
1737
                    return None
3✔
1738

1739
            # Parity returns tx_receipt even is tx is still pending, so we check `blockNumber` is not None
1740
            return (
3✔
1741
                tx_receipt
1742
                if tx_receipt and tx_receipt["blockNumber"] is not None
1743
                else None
1744
            )
1745
        except TransactionNotFound:
3✔
1746
            return None
3✔
1747

1748
    def get_transaction_receipts(
3✔
1749
        self, tx_hashes: Sequence[EthereumData]
1750
    ) -> List[Optional[TxReceipt]]:
1751
        if not tx_hashes:
3✔
1752
            return []
3✔
1753
        payload = [
3✔
1754
            {
1755
                "id": i,
1756
                "jsonrpc": "2.0",
1757
                "method": "eth_getTransactionReceipt",
1758
                "params": [to_0x_hex_str(HexBytes(tx_hash))],
1759
            }
1760
            for i, tx_hash in enumerate(tx_hashes)
1761
        ]
1762
        results = self.raw_batch_request(payload)
3✔
1763
        receipts = []
3✔
1764
        for tx_receipt in results:
3✔
1765
            # Parity returns tx_receipt even is tx is still pending, so we check `blockNumber` is not None
1766
            if (
3✔
1767
                tx_receipt
1768
                and isinstance(tx_receipt, dict)
1769
                and tx_receipt["blockNumber"] is not None
1770
            ):
1771
                receipts.append(receipt_formatter(tx_receipt))
3✔
1772
            else:
1773
                receipts.append(None)
×
1774
        return receipts
3✔
1775

1776
    def get_block(
3✔
1777
        self, block_identifier: BlockIdentifier, full_transactions: bool = False
1778
    ) -> Optional[BlockData]:
1779
        try:
3✔
1780
            return self.w3.eth.get_block(
3✔
1781
                block_identifier, full_transactions=full_transactions
1782
            )
1783
        except BlockNotFound:
×
1784
            return None
×
1785

1786
    def _parse_block_identifier(self, block_identifier: BlockIdentifier) -> str:
3✔
1787
        if isinstance(block_identifier, int):
3✔
1788
            return HexStr(hex(block_identifier))
3✔
1789
        elif isinstance(block_identifier, bytes):
3✔
1790
            return HexStr(to_0x_hex_str(HexBytes(block_identifier)))
3✔
1791
        return str(block_identifier)
3✔
1792

1793
    def get_blocks(
3✔
1794
        self,
1795
        block_identifiers: Iterable[BlockIdentifier],
1796
        full_transactions: bool = False,
1797
    ) -> List[Optional[BlockData]]:
1798
        if not block_identifiers:
3✔
1799
            return []
3✔
1800
        payload = [
3✔
1801
            {
1802
                "id": i,
1803
                "jsonrpc": "2.0",
1804
                "method": "eth_getBlockByNumber"
1805
                if isinstance(block_identifier, int)
1806
                else "eth_getBlockByHash",
1807
                "params": [
1808
                    self._parse_block_identifier(block_identifier),
1809
                    full_transactions,
1810
                ],
1811
            }
1812
            for i, block_identifier in enumerate(block_identifiers)
1813
        ]
1814
        results = self.raw_batch_request(payload)
3✔
1815
        blocks = []
3✔
1816
        for raw_block in results:
3✔
1817
            if raw_block and isinstance(raw_block, dict):
3✔
1818
                if "extraData" in raw_block:
3✔
1819
                    del raw_block[
3✔
1820
                        "extraData"
1821
                    ]  # Remove extraData, raises some problems on parsing
1822
                blocks.append(block_formatter(raw_block))
3✔
1823
            else:
1824
                blocks.append(None)
×
1825
        return blocks
3✔
1826

1827
    def is_contract(self, contract_address: ChecksumAddress) -> bool:
3✔
1828
        return bool(self.w3.eth.get_code(contract_address))
3✔
1829

1830
    @staticmethod
3✔
1831
    def build_tx_params(
3✔
1832
        from_address: Optional[ChecksumAddress] = None,
1833
        to_address: Optional[ChecksumAddress] = None,
1834
        value: Optional[int] = None,
1835
        gas: Optional[int] = None,
1836
        gas_price: Optional[int] = None,
1837
        nonce: Optional[int] = None,
1838
        chain_id: Optional[int] = None,
1839
        tx_params: Optional[TxParams] = None,
1840
    ) -> TxParams:
1841
        """
1842
        Build tx params dictionary.
1843
        If an existing TxParams dictionary is provided the fields will be replaced by the provided ones
1844

1845
        :param from_address:
1846
        :param to_address:
1847
        :param value:
1848
        :param gas:
1849
        :param gas_price:
1850
        :param nonce:
1851
        :param chain_id:
1852
        :param tx_params: An existing TxParams dictionary will be replaced by the provided values
1853
        :return:
1854
        """
1855

1856
        new_tx_params: TxParams = tx_params if tx_params is not None else {}
3✔
1857

1858
        if from_address:
3✔
1859
            new_tx_params["from"] = from_address
3✔
1860

1861
        if to_address:
3✔
1862
            new_tx_params["to"] = to_address
×
1863

1864
        if value is not None:
3✔
1865
            new_tx_params["value"] = Wei(value)
×
1866

1867
        if gas_price is not None:
3✔
1868
            new_tx_params["gasPrice"] = Wei(gas_price)
3✔
1869

1870
        if gas is not None:
3✔
1871
            new_tx_params["gas"] = gas
3✔
1872

1873
        if nonce is not None:
3✔
1874
            new_tx_params["nonce"] = Nonce(nonce)
×
1875

1876
        if chain_id is not None:
3✔
1877
            new_tx_params["chainId"] = chain_id
×
1878

1879
        return new_tx_params
3✔
1880

1881
    @tx_with_exception_handling
3✔
1882
    def send_transaction(self, transaction_dict: TxParams) -> HexBytes:
3✔
1883
        return self.w3.eth.send_transaction(transaction_dict)
3✔
1884

1885
    @tx_with_exception_handling
3✔
1886
    def send_raw_transaction(self, raw_transaction: EthereumData) -> HexBytes:
3✔
1887
        if isinstance(raw_transaction, bytes):
3✔
1888
            value_bytes = raw_transaction
3✔
1889
        else:
1890
            value_bytes = bytes.fromhex(
×
1891
                raw_transaction.replace("0x", "")
1892
            )  # Remove '0x' and convert
1893
        return self.w3.eth.send_raw_transaction(value_bytes)
3✔
1894

1895
    def send_unsigned_transaction(
3✔
1896
        self,
1897
        tx: TxParams,
1898
        private_key: Optional[str] = None,
1899
        public_key: Optional[str] = None,
1900
        retry: bool = False,
1901
        block_identifier: Optional[BlockIdentifier] = "pending",
1902
    ) -> HexBytes:
1903
        """
1904
        Send a tx using an unlocked public key in the node or a private key. Both `public_key` and
1905
        `private_key` cannot be `None`
1906

1907
        :param tx:
1908
        :param private_key:
1909
        :param public_key:
1910
        :param retry: Retry if a problem with nonce is found
1911
        :param block_identifier: For nonce calculation, recommended is `pending`
1912
        :return: tx hash
1913
        """
1914

1915
        # TODO Refactor this method, it's not working well with new version of the nodes
1916
        if private_key:
3✔
1917
            address = Account.from_key(private_key).address
3✔
1918
        elif public_key:
3✔
1919
            address = public_key
3✔
1920
        else:
1921
            logger.error(
3✔
1922
                "No ethereum account provided. Need a public_key or private_key"
1923
            )
1924
            raise ValueError(
3✔
1925
                "Ethereum account was not configured or unlocked in the node"
1926
            )
1927

1928
        if tx.get("nonce") is None:
3✔
1929
            tx["nonce"] = self.get_nonce_for_account(
3✔
1930
                address, block_identifier=block_identifier
1931
            )
1932

1933
        number_errors = 5
3✔
1934
        while number_errors >= 0:
3✔
1935
            try:
3✔
1936
                if private_key:
3✔
1937
                    signed_tx = self.w3.eth.account.sign_transaction(
3✔
1938
                        tx, private_key=private_key
1939
                    )
1940
                    logger.debug(
3✔
1941
                        "Sending %d wei from %s to %s", tx["value"], address, tx["to"]
1942
                    )
1943
                    try:
3✔
1944
                        return self.send_raw_transaction(signed_tx.raw_transaction)
3✔
1945
                    except TransactionAlreadyImported as e:
3✔
1946
                        # Sometimes Parity 2.2.11 fails with Transaction already imported, even if it's not, but it's
1947
                        # processed
1948
                        tx_hash = signed_tx.hash
×
1949
                        logger.error(
×
1950
                            "Transaction with tx-hash=%s already imported: %s"
1951
                            % (tx_hash.hex(), str(e))
1952
                        )
1953
                        return tx_hash
×
1954
                elif public_key:
3✔
1955
                    tx["from"] = address
3✔
1956
                    return self.send_transaction(tx)
3✔
1957
            except ReplacementTransactionUnderpriced as e:
3✔
1958
                if not retry or not number_errors:
×
1959
                    raise e
×
1960
                current_nonce = tx["nonce"]
×
1961
                tx["nonce"] = max(
×
1962
                    current_nonce + 1,
1963
                    self.get_nonce_for_account(
1964
                        address, block_identifier=block_identifier
1965
                    ),
1966
                )
1967
                logger.error(
×
1968
                    "Tx with nonce=%d was already sent for address=%s, retrying with nonce=%s",
1969
                    current_nonce,
1970
                    address,
1971
                    tx["nonce"],
1972
                )
1973
            except InvalidNonce as e:
3✔
1974
                if not retry or not number_errors:
3✔
1975
                    raise e
3✔
1976
                logger.error(
3✔
1977
                    "address=%s Tx with invalid nonce=%d, retrying recovering nonce again",
1978
                    address,
1979
                    tx["nonce"],
1980
                )
1981
                tx["nonce"] = self.get_nonce_for_account(
3✔
1982
                    address, block_identifier=block_identifier
1983
                )
1984
                number_errors -= 1
3✔
1985
        return HexBytes("")
×
1986

1987
    def send_eth_to(
3✔
1988
        self,
1989
        private_key: str,
1990
        to: str,
1991
        gas_price: int,
1992
        value: Wei,
1993
        gas: Optional[int] = None,
1994
        nonce: Optional[int] = None,
1995
        retry: bool = False,
1996
        block_identifier: Optional[BlockIdentifier] = "pending",
1997
    ) -> bytes:
1998
        """
1999
        Send ether using configured account
2000

2001
        :param private_key: to
2002
        :param to: to
2003
        :param gas_price: gas_price
2004
        :param value: value(wei)
2005
        :param gas: gas, defaults to 22000
2006
        :param retry: Retry if a problem is found
2007
        :param nonce: Nonce of sender account
2008
        :param block_identifier: Block identifier for nonce calculation
2009
        :return: tx_hash
2010
        """
2011

2012
        assert fast_is_checksum_address(to)
3✔
2013

2014
        account = Account.from_key(private_key)
3✔
2015

2016
        tx: TxParams = {
3✔
2017
            "from": account.address,
2018
            "to": to,
2019
            "value": value,
2020
            "gas": gas or Wei(self.estimate_gas(to, account.address, value)),
2021
            "gasPrice": Wei(gas_price),
2022
            "chainId": self.get_chain_id(),
2023
        }
2024

2025
        if nonce is not None:
3✔
2026
            tx["nonce"] = Nonce(nonce)
×
2027

2028
        return self.send_unsigned_transaction(
3✔
2029
            tx, private_key=private_key, retry=retry, block_identifier=block_identifier
2030
        )
2031

2032
    def check_tx_with_confirmations(
3✔
2033
        self, tx_hash: EthereumHash, confirmations: int
2034
    ) -> bool:
2035
        """
2036
        Check tx hash and make sure it has the confirmations required
2037

2038
        :param tx_hash: Hash of the tx
2039
        :param confirmations: Minimum number of confirmations required
2040
        :return: True if tx was mined with the number of confirmations required, False otherwise
2041
        """
2042
        tx_receipt = self.get_transaction_receipt(tx_hash)
3✔
2043
        if not tx_receipt or tx_receipt["blockNumber"] is None:
3✔
2044
            # If `tx_receipt` exists but `blockNumber` is `None`, tx is still pending (just Parity)
2045
            return False
×
2046
        else:
2047
            return (
3✔
2048
                self.w3.eth.block_number - tx_receipt["blockNumber"]
2049
            ) >= confirmations
2050

2051
    @staticmethod
3✔
2052
    def private_key_to_address(private_key):
3✔
2053
        return Account.from_key(private_key).address
×
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2025 Coveralls, Inc