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

safe-global / safe-eth-py / 10629724120

30 Aug 2024 08:49AM UTC coverage: 93.903%. Remained the same
10629724120

Pull #1309

github

web-flow
Merge 175bdc70e into d0479e88e
Pull Request #1309: Add reference to main in safe-eth-py dependency in addresses actions

8671 of 9234 relevant lines covered (93.9%)

3.76 hits per line

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

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

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

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

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

96
logger = getLogger(__name__)
4✔
97

98

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

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

134
    @wraps(func)
4✔
135
    def with_exception_handling(*args, **kwargs):
4✔
136
        try:
4✔
137
            return func(*args, **kwargs)
4✔
138
        except (Web3Exception, ValueError) as exc:
4✔
139
            str_exc = str(exc).lower()
4✔
140
            for reason, custom_exception in error_with_exception.items():
4✔
141
                if reason.lower() in str_exc:
4✔
142
                    raise custom_exception(str(exc)) from exc
4✔
143
            raise exc
1✔
144

145
    return with_exception_handling
4✔
146

147

148
class EthereumTxSent(NamedTuple):
4✔
149
    tx_hash: bytes
4✔
150
    tx: TxParams
4✔
151
    contract_address: Optional[ChecksumAddress]
4✔
152

153

154
class Erc20Info(NamedTuple):
4✔
155
    name: str
4✔
156
    symbol: str
4✔
157
    decimals: int
4✔
158

159

160
class Erc721Info(NamedTuple):
4✔
161
    name: str
4✔
162
    symbol: str
4✔
163

164

165
class TokenBalance(NamedTuple):
4✔
166
    token_address: str
4✔
167
    balance: int
4✔
168

169

170
class TxSpeed(Enum):
4✔
171
    SLOWEST = 0
4✔
172
    VERY_SLOW = 1
4✔
173
    SLOW = 2
4✔
174
    NORMAL = 3
4✔
175
    FAST = 4
4✔
176
    VERY_FAST = 5
4✔
177
    FASTEST = 6
4✔
178

179

180
@cache
4✔
181
def get_auto_ethereum_client() -> "EthereumClient":
4✔
182
    """
183
    Use environment variables to configure `EthereumClient` and build a singleton:
184
        - `ETHEREUM_NODE_URL`: No default.
185
        - `ETHEREUM_RPC_TIMEOUT`: `10` by default.
186
        - `ETHEREUM_RPC_SLOW_TIMEOUT`: `60` by default.
187
        - `ETHEREUM_RPC_RETRY_COUNT`: `60` by default.
188
        - `ETHEREUM_RPC_BATCH_REQUEST_MAX_SIZE`: `500` by default.
189

190
    :return: A configured singleton of EthereumClient
191
    """
192
    try:
4✔
193
        from django.conf import settings
4✔
194

195
        ethereum_node_url = settings.ETHEREUM_NODE_URL
4✔
196
    except ModuleNotFoundError:
×
197
        ethereum_node_url = os.environ.get("ETHEREUM_NODE_URL")
×
198
    return EthereumClient(
4✔
199
        ethereum_node_url,
200
        provider_timeout=int(os.environ.get("ETHEREUM_RPC_TIMEOUT", 10)),
201
        slow_provider_timeout=int(os.environ.get("ETHEREUM_RPC_SLOW_TIMEOUT", 60)),
202
        retry_count=int(os.environ.get("ETHEREUM_RPC_RETRY_COUNT", 1)),
203
        batch_request_max_size=int(
204
            os.environ.get("ETHEREUM_RPC_BATCH_REQUEST_MAX_SIZE", 500)
205
        ),
206
    )
207

208

209
class EthereumClientManager:
4✔
210
    def __init__(self, ethereum_client: "EthereumClient"):
4✔
211
        self.ethereum_client = ethereum_client
4✔
212
        self.ethereum_node_url = ethereum_client.ethereum_node_url
4✔
213
        self.w3 = ethereum_client.w3
4✔
214
        self.slow_w3 = ethereum_client.slow_w3
4✔
215
        self.http_session = ethereum_client.http_session
4✔
216
        self.timeout = ethereum_client.timeout
4✔
217
        self.slow_timeout = ethereum_client.slow_timeout
4✔
218

219

220
class BatchCallManager(EthereumClientManager):
4✔
221
    def batch_call_custom(
4✔
222
        self,
223
        payloads: Iterable[Dict[str, Any]],
224
        raise_exception: bool = True,
225
        block_identifier: Optional[BlockIdentifier] = "latest",
226
        batch_size: Optional[int] = None,
227
    ) -> List[Optional[Any]]:
228
        """
229
        Do batch requests of multiple contract calls (`eth_call`)
230

231
        :param payloads: Iterable of Dictionaries with at least {'data': '<hex-string>',
232
            'output_type': <solidity-output-type>, 'to': '<checksummed-address>'}. `from` can also be provided and if
233
            `fn_name` is provided it will be used for debugging purposes
234
        :param raise_exception: If False, exception will not be raised if there's any problem and instead `None` will
235
            be returned as the value
236
        :param block_identifier: `latest` by default
237
        :param batch_size: If `payload` length is bigger than size, it will be split into smaller chunks before
238
            sending to the server
239
        :return: List with the ABI decoded return values
240
        :raises: ValueError if raise_exception=True
241
        """
242
        if not payloads:
4✔
243
            return []
×
244

245
        queries = []
4✔
246
        for i, payload in enumerate(payloads):
4✔
247
            assert "data" in payload, "`data` not present"
4✔
248
            assert "to" in payload, "`to` not present"
4✔
249
            assert "output_type" in payload, "`output-type` not present"
4✔
250

251
            query_params = {"to": payload["to"], "data": payload["data"]}  # Balance of
4✔
252
            if "from" in payload:
4✔
253
                query_params["from"] = payload["from"]
×
254

255
            queries.append(
4✔
256
                {
257
                    "jsonrpc": "2.0",
258
                    "method": "eth_call",
259
                    "params": [
260
                        query_params,
261
                        hex(block_identifier)
262
                        if isinstance(block_identifier, int)
263
                        else block_identifier,
264
                    ],
265
                    "id": i,
266
                }
267
            )
268

269
        batch_size = batch_size or self.ethereum_client.batch_request_max_size
4✔
270
        all_results = []
4✔
271
        for chunk in chunks(queries, batch_size):
4✔
272
            response = self.http_session.post(
4✔
273
                self.ethereum_node_url, json=chunk, timeout=self.slow_timeout
274
            )
275
            if not response.ok:
4✔
276
                raise ConnectionError(
×
277
                    f"Error connecting to {self.ethereum_node_url}: {response.text}"
278
                )
279

280
            results = response.json()
4✔
281

282
            # If there's an error some nodes return a json instead of a list
283
            if isinstance(results, dict) and "error" in results:
4✔
284
                logger.error(
×
285
                    "Batch call custom problem with payload=%s, result=%s)",
286
                    chunk,
287
                    results,
288
                )
289
                raise ValueError(f"Batch request error: {results}")
×
290

291
            all_results.extend(results)
4✔
292

293
        return_values: List[Optional[Any]] = []
4✔
294
        errors = []
4✔
295
        for payload, result in zip(
4✔
296
            payloads, sorted(all_results, key=lambda x: x["id"])
297
        ):
298
            if "error" in result:
4✔
299
                fn_name = payload.get("fn_name", HexBytes(payload["data"]).hex())
4✔
300
                errors.append(f'`{fn_name}`: {result["error"]}')
4✔
301
                return_values.append(None)
4✔
302
            else:
303
                output_type = payload["output_type"]
4✔
304
                try:
4✔
305
                    decoded_values = eth_abi.decode(
4✔
306
                        output_type, HexBytes(result["result"])
307
                    )
308
                    normalized_data = map_abi_data(
4✔
309
                        BASE_RETURN_NORMALIZERS, output_type, decoded_values
310
                    )
311
                    if len(normalized_data) == 1:
4✔
312
                        return_values.append(normalized_data[0])
4✔
313
                    else:
314
                        return_values.append(normalized_data)
×
315
                except (DecodingError, OverflowError):
4✔
316
                    fn_name = payload.get("fn_name", HexBytes(payload["data"]).hex())
4✔
317
                    errors.append(f"`{fn_name}`: DecodingError, cannot decode")
4✔
318
                    return_values.append(None)
4✔
319

320
        if errors and raise_exception:
4✔
321
            raise BatchCallFunctionFailed(f"Errors returned {errors}")
4✔
322
        else:
323
            return return_values
4✔
324

325
    def batch_call(
4✔
326
        self,
327
        contract_functions: Iterable[ContractFunction],
328
        from_address: Optional[ChecksumAddress] = None,
329
        raise_exception: bool = True,
330
        block_identifier: Optional[BlockIdentifier] = "latest",
331
    ) -> List[Optional[Any]]:
332
        """
333
        Do batch requests of multiple contract calls
334

335
        :param contract_functions: Iterable of contract functions using web3.py contracts. For instance, a valid
336
            argument would be [erc20_contract.functions.balanceOf(address), erc20_contract.functions.decimals()]
337
        :param from_address: Use this address as `from` in every call if provided
338
        :param block_identifier: `latest` by default
339
        :param raise_exception: If False, exception will not be raised if there's any problem and instead `None` will
340
            be returned as the value.
341
        :return: List with the ABI decoded return values
342
        """
343
        if not contract_functions:
4✔
344
            return []
×
345
        payloads = []
4✔
346
        params: TxParams = {"gas": Wei(0), "gasPrice": Wei(0)}
4✔
347
        for _, contract_function in enumerate(contract_functions):
4✔
348
            if not contract_function.address:
4✔
349
                raise ValueError(
×
350
                    f"Missing address for batch_call in `{contract_function.fn_name}`"
351
                )
352

353
            payload = {
4✔
354
                "to": contract_function.address,
355
                "data": contract_function.build_transaction(params)["data"],
356
                "output_type": [
357
                    output["type"] for output in contract_function.abi["outputs"]
358
                ],
359
                "fn_name": contract_function.fn_name,  # For debugging purposes
360
            }
361
            if from_address:
4✔
362
                payload["from"] = from_address
×
363
            payloads.append(payload)
4✔
364

365
        return self.batch_call_custom(
4✔
366
            payloads, raise_exception=raise_exception, block_identifier=block_identifier
367
        )
368

369
    def batch_call_same_function(
4✔
370
        self,
371
        contract_function: ContractFunction,
372
        contract_addresses: Sequence[ChecksumAddress],
373
        from_address: Optional[ChecksumAddress] = None,
374
        raise_exception: bool = True,
375
        block_identifier: Optional[BlockIdentifier] = "latest",
376
    ) -> List[Optional[Any]]:
377
        """
378
        Do batch requests using the same function to multiple address. ``batch_call`` could be used to achieve that,
379
        but generating the ContractFunction is slow, so this function allows to use the same contract_function for
380
        multiple addresses
381

382
        :param contract_function:
383
        :param contract_addresses:
384
        :param from_address:
385
        :param raise_exception:
386
        :param block_identifier:
387
        :return:
388
        """
389

390
        assert contract_function, "Contract function is required"
4✔
391

392
        if not contract_addresses:
4✔
393
            return []
×
394

395
        contract_function.address = NULL_ADDRESS  # It's required by web3.py
4✔
396
        params: TxParams = {"gas": Wei(0), "gasPrice": Wei(0)}
4✔
397
        data = contract_function.build_transaction(params)["data"]
4✔
398
        output_type = [output["type"] for output in contract_function.abi["outputs"]]
4✔
399
        fn_name = contract_function.fn_name
4✔
400

401
        payloads = []
4✔
402
        for contract_address in contract_addresses:
4✔
403
            payload = {
4✔
404
                "to": contract_address,
405
                "data": data,
406
                "output_type": output_type,
407
                "fn_name": fn_name,  # For debugging purposes
408
            }
409
            if from_address:
4✔
410
                payload["from"] = from_address
×
411
            payloads.append(payload)
4✔
412

413
        return self.batch_call_custom(
4✔
414
            payloads, raise_exception=raise_exception, block_identifier=block_identifier
415
        )
416

417

418
class Erc20Manager(EthereumClientManager):
4✔
419
    """
420
    Manager for ERC20 operations
421
    """
422

423
    # keccak('Transfer(address,address,uint256)')
424
    # ddf252ad1be2c89b69c2b068fc378daa952ba7f163c4a11628f55a4df523b3ef
425
    TRANSFER_TOPIC = HexBytes(ERC20_721_TRANSFER_TOPIC)
4✔
426

427
    def decode_logs(self, logs: Sequence[LogReceipt]):
4✔
428
        decoded_logs = []
4✔
429
        for log in logs:
4✔
430
            decoded = self._decode_transfer_log(log["data"], log["topics"])
4✔
431
            if decoded:
4✔
432
                log_copy = dict(log)
4✔
433
                log_copy["args"] = decoded
4✔
434
                decoded_logs.append(log_copy)
4✔
435
        return decoded_logs
4✔
436

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

503
    def get_balance(
4✔
504
        self, address: ChecksumAddress, token_address: ChecksumAddress
505
    ) -> int:
506
        """
507
        Get balance of address for `erc20_address`
508

509
        :param address: owner address
510
        :param token_address: erc20 token address
511
        :return: balance
512
        """
513
        return (
4✔
514
            get_erc20_contract(self.w3, token_address)
515
            .functions.balanceOf(address)
516
            .call()
517
        )
518

519
    def get_balances(
4✔
520
        self,
521
        address: ChecksumAddress,
522
        token_addresses: Sequence[ChecksumAddress],
523
        include_native_balance: bool = True,
524
    ) -> List[BalanceDict]:
525
        """
526
        Get balances for Ether and tokens for an `address`
527

528
        :param address: Owner address checksummed
529
        :param token_addresses: token addresses to check
530
        :param include_native_balance: if `True` returns also the native token balance
531
        :return: ``List[BalanceDict]``
532
        """
533

534
        balances = self.ethereum_client.batch_call_same_function(
4✔
535
            get_erc20_contract(self.ethereum_client.w3).functions.balanceOf(address),
536
            token_addresses,
537
            raise_exception=False,
538
        )
539

540
        return_balances = [
4✔
541
            {
542
                "token_address": token_address,
543
                "balance": balance
544
                if isinstance(balance, int)
545
                else 0,  # A `revert` with bytes can be returned
546
            }
547
            for token_address, balance in zip(token_addresses, balances)
548
        ]
549

550
        if not include_native_balance:
4✔
551
            return return_balances
4✔
552

553
        # Add ether balance response
554
        return [
4✔
555
            {
556
                "token_address": None,
557
                "balance": self.ethereum_client.get_balance(address),
558
            }
559
        ] + return_balances
560

561
    def get_name(self, erc20_address: ChecksumAddress) -> str:
4✔
562
        erc20 = get_erc20_contract(self.w3, erc20_address)
4✔
563
        data = erc20.functions.name().build_transaction(
4✔
564
            {"gas": Wei(0), "gasPrice": Wei(0)}
565
        )["data"]
566
        result = self.w3.eth.call({"to": erc20_address, "data": data})
4✔
567
        return decode_string_or_bytes32(result)
4✔
568

569
    def get_symbol(self, erc20_address: ChecksumAddress) -> str:
4✔
570
        erc20 = get_erc20_contract(self.w3, erc20_address)
4✔
571
        data = erc20.functions.symbol().build_transaction(
4✔
572
            {"gas": Wei(0), "gasPrice": Wei(0)}
573
        )["data"]
574
        result = self.w3.eth.call({"to": erc20_address, "data": data})
4✔
575
        return decode_string_or_bytes32(result)
4✔
576

577
    def get_decimals(self, erc20_address: ChecksumAddress) -> int:
4✔
578
        erc20 = get_erc20_contract(self.w3, erc20_address)
4✔
579
        return erc20.functions.decimals().call()
4✔
580

581
    def get_info(self, erc20_address: ChecksumAddress) -> Erc20Info:
4✔
582
        """
583
        Get erc20 information (`name`, `symbol` and `decimals`). Use batching to get
584
        all info in the same request.
585

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

629
    def get_total_transfer_history(
4✔
630
        self,
631
        addresses: Optional[Sequence[ChecksumAddress]] = None,
632
        from_block: BlockIdentifier = BlockNumber(0),
633
        to_block: Optional[BlockIdentifier] = None,
634
        token_address: Optional[ChecksumAddress] = None,
635
    ) -> List[LogReceiptDecoded]:
636
        """
637
        Get events for erc20 and erc721 transfers from and to an `address`. We decode it manually.
638
        Example of an erc20 event:
639

640
        .. code-block:: python
641

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

699
        :param addresses: Search events `from` and `to` these `addresses`. If not, every transfer event within the
700
            range will be retrieved
701
        :param from_block: Block to start querying from
702
        :param to_block: Block to stop querying from
703
        :param token_address: Address of the token
704
        :return: List of events sorted by blockNumber
705
        """
706
        topic_0 = self.TRANSFER_TOPIC.hex()
4✔
707
        if addresses:
4✔
708
            addresses_encoded = [
4✔
709
                HexBytes(eth_abi.encode(["address"], [address])).hex()
710
                for address in addresses
711
            ]
712
            # Topics for transfer `to` and `from` an address
713
            all_topics = [
4✔
714
                [topic_0, addresses_encoded],  # Topics from
715
                [topic_0, None, addresses_encoded],  # Topics to
716
            ]
717
        else:
718
            all_topics = [[topic_0]]  # All transfer events
4✔
719
        parameters: FilterParams = {"fromBlock": from_block}
4✔
720
        if to_block:
4✔
721
            parameters["toBlock"] = to_block
4✔
722
        if token_address:
4✔
723
            parameters["address"] = token_address
4✔
724

725
        erc20_events = []
4✔
726
        # Do the request to `eth_getLogs`
727
        for topics in all_topics:
4✔
728
            parameters["topics"] = topics
4✔
729

730
            # Decode events. Just pick valid ERC20 Transfer events (ERC721 `Transfer` has the same signature)
731
            for event in self.slow_w3.eth.get_logs(parameters):
4✔
732
                event["args"] = self._decode_transfer_log(
4✔
733
                    event["data"], event["topics"]
734
                )
735
                if event["args"]:
4✔
736
                    erc20_events.append(LogReceiptDecoded(event))
4✔
737

738
        erc20_events.sort(key=lambda x: x["blockNumber"])
4✔
739
        return erc20_events
4✔
740

741
    def get_transfer_history(
4✔
742
        self,
743
        from_block: int,
744
        to_block: Optional[int] = None,
745
        from_address: Optional[str] = None,
746
        to_address: Optional[str] = None,
747
        token_address: Optional[str] = None,
748
    ) -> List[Dict[str, Any]]:
749
        """
750
        DON'T USE, it will fail in some cases until they fix https://github.com/ethereum/web3.py/issues/1351
751
        Get events for erc20/erc721 transfers. At least one of `from_address`, `to_address` or `token_address` must be
752
        defined. Example of decoded event:
753

754
        .. code-block:: python
755

756
            {
757
                "args": {
758
                    "from": "0x1Ce67Ea59377A163D47DFFc9BaAB99423BE6EcF1",
759
                    "to": "0xaE9E15896fd32E59C7d89ce7a95a9352D6ebD70E",
760
                    "value": 15000000000000000
761
                },
762
                "event": "Transfer",
763
                "logIndex": 42,
764
                "transactionIndex": 60,
765
                "transactionHash": "0x71d6d83fef3347bad848e83dfa0ab28296e2953de946ee152ea81c6dfb42d2b3",
766
                "address": "0xfecA834E7da9D437645b474450688DA9327112a5",
767
                "blockHash": "0x054de9a496fc7d10303068cbc7ee3e25181a3b26640497859a5e49f0342e7db2",
768
                "blockNumber": 7265022
769
            }
770

771
        :param from_block: Block to start querying from
772
        :param to_block: Block to stop querying from
773
        :param from_address: Address sending the erc20 transfer
774
        :param to_address: Address receiving the erc20 transfer
775
        :param token_address: Address of the token
776
        :return: List of events (decoded)
777
        :throws: ReadTimeout
778
        """
779
        assert (
4✔
780
            from_address or to_address or token_address
781
        ), "At least one parameter must be provided"
782

783
        erc20 = get_erc20_contract(self.slow_w3)
4✔
784

785
        argument_filters = {}
4✔
786
        if from_address:
4✔
787
            argument_filters["from"] = from_address
4✔
788
        if to_address:
4✔
789
            argument_filters["to"] = to_address
4✔
790

791
        return erc20.events.Transfer.create_filter(
4✔
792
            fromBlock=from_block,
793
            toBlock=to_block,
794
            address=token_address,
795
            argument_filters=argument_filters,
796
        ).get_all_entries()
797

798
    def send_tokens(
4✔
799
        self,
800
        to: str,
801
        amount: int,
802
        erc20_address: ChecksumAddress,
803
        private_key: str,
804
        nonce: Optional[int] = None,
805
        gas_price: Optional[int] = None,
806
        gas: Optional[int] = None,
807
    ) -> bytes:
808
        """
809
        Send tokens to address
810

811
        :param to:
812
        :param amount:
813
        :param erc20_address:
814
        :param private_key:
815
        :param nonce:
816
        :param gas_price:
817
        :param gas:
818
        :return: tx_hash
819
        """
820
        erc20 = get_erc20_contract(self.w3, erc20_address)
4✔
821
        account = Account.from_key(private_key)
4✔
822
        tx_options: TxParams = {"from": account.address}
4✔
823
        if nonce:
4✔
824
            tx_options["nonce"] = Nonce(nonce)
×
825
        if gas_price:
4✔
826
            tx_options["gasPrice"] = Wei(gas_price)
×
827
        if gas:
4✔
828
            tx_options["gas"] = Wei(gas)
×
829

830
        tx = erc20.functions.transfer(to, amount).build_transaction(tx_options)
4✔
831
        return self.ethereum_client.send_unsigned_transaction(
4✔
832
            tx, private_key=private_key
833
        )
834

835

836
class Erc721Manager(EthereumClientManager):
4✔
837
    # keccak('Transfer(address,address,uint256)')
838
    # ddf252ad1be2c89b69c2b068fc378daa952ba7f163c4a11628f55a4df523b3ef
839
    TRANSFER_TOPIC = Erc20Manager.TRANSFER_TOPIC
4✔
840

841
    def get_balance(
4✔
842
        self, address: ChecksumAddress, token_address: ChecksumAddress
843
    ) -> int:
844
        """
845
        Get balance of address for `erc20_address`
846

847
        :param address: owner address
848
        :param token_address: erc721 token address
849
        :return: balance
850
        """
851
        return (
×
852
            get_erc721_contract(self.w3, token_address)
853
            .functions.balanceOf(address)
854
            .call()
855
        )
856

857
    def get_balances(
4✔
858
        self, address: ChecksumAddress, token_addresses: Sequence[ChecksumAddress]
859
    ) -> List[TokenBalance]:
860
        """
861
        Get balances for tokens for an `address`. If there's a problem with a token_address `0` will be
862
        returned for balance
863

864
        :param address: Owner address checksummed
865
        :param token_addresses: token addresses to check
866
        :return:
867
        """
868
        function = get_erc721_contract(self.ethereum_client.w3).functions.balanceOf(
×
869
            address
870
        )
871
        balances = self.ethereum_client.batch_call_same_function(
×
872
            function,
873
            token_addresses,
874
            raise_exception=False,
875
        )
876
        return [
×
877
            TokenBalance(token_address, balance if isinstance(balance, int) else 0)
878
            for (token_address, balance) in zip(token_addresses, balances)
879
        ]
880

881
    def get_info(self, token_address: ChecksumAddress) -> Erc721Info:
4✔
882
        """
883
        Get erc721 information (`name`, `symbol`). Use batching to get
884
        all info in the same request.
885

886
        :param token_address:
887
        :return: Erc721Info
888
        """
889
        erc721_contract = get_erc721_contract(self.w3, token_address)
×
890
        try:
×
891
            name, symbol = cast(
×
892
                List[str],
893
                self.ethereum_client.batch_call(
894
                    [
895
                        erc721_contract.functions.name(),
896
                        erc721_contract.functions.symbol(),
897
                    ]
898
                ),
899
            )
900
            return Erc721Info(name, symbol)
×
901
        except (DecodingError, ValueError):  # Not all the ERC721 have metadata
×
902
            raise InvalidERC721Info
×
903

904
    def get_owners(
4✔
905
        self, token_addresses_with_token_ids: Sequence[Tuple[ChecksumAddress, int]]
906
    ) -> List[Optional[ChecksumAddress]]:
907
        """
908
        :param token_addresses_with_token_ids: Tuple(token_address: str, token_id: int)
909
        :return: List of owner addresses, `None` if not found
910
        """
911
        return [
×
912
            ChecksumAddress(HexAddress(HexStr(owner)))
913
            if isinstance(owner, str)
914
            else None
915
            for owner in self.ethereum_client.batch_call(
916
                [
917
                    get_erc721_contract(
918
                        self.ethereum_client.w3, token_address
919
                    ).functions.ownerOf(token_id)
920
                    for token_address, token_id in token_addresses_with_token_ids
921
                ],
922
                raise_exception=False,
923
            )
924
        ]
925

926
    def get_token_uris(
4✔
927
        self, token_addresses_with_token_ids: Sequence[Tuple[ChecksumAddress, int]]
928
    ) -> List[Optional[str]]:
929
        """
930
        :param token_addresses_with_token_ids: Tuple(token_address: str, token_id: int)
931
        :return: List of token_uris, `None` if not found
932
        """
933
        return [
×
934
            token_uri if isinstance(token_uri, str) else None
935
            for token_uri in self.ethereum_client.batch_call(
936
                [
937
                    get_erc721_contract(
938
                        self.ethereum_client.w3, token_address
939
                    ).functions.tokenURI(token_id)
940
                    for token_address, token_id in token_addresses_with_token_ids
941
                ],
942
                raise_exception=False,
943
            )
944
        ]
945

946

947
class TracingManager(EthereumClientManager):
4✔
948
    def filter_out_errored_traces(
4✔
949
        self, internal_txs: Sequence[Dict[str, Any]]
950
    ) -> Sequence[Dict[str, Any]]:
951
        """
952
        Filter out errored transactions (traces that are errored or that have an errored parent)
953

954
        :param internal_txs: Traces for the SAME ethereum tx, sorted ascending by `trace_address`
955
            `sorted(t, key = lambda i: i['traceAddress'])`. It's the default output from methods returning `traces` like
956
            `trace_block` or `trace_transaction`
957
        :return: List of not errored traces
958
        """
959
        new_list = []
4✔
960
        errored_trace_address: Optional[List[int]] = None
4✔
961
        for internal_tx in internal_txs:
4✔
962
            if internal_tx.get("error") is not None:
4✔
963
                errored_trace_address = internal_tx["traceAddress"]
4✔
964
            elif (
4✔
965
                errored_trace_address is not None
966
                and internal_tx["traceAddress"][: len(errored_trace_address)]
967
                == errored_trace_address
968
            ):
969
                continue
4✔
970
            else:
971
                new_list.append(internal_tx)
4✔
972
        return new_list
4✔
973

974
    def get_previous_trace(
4✔
975
        self,
976
        tx_hash: EthereumHash,
977
        trace_address: Sequence[int],
978
        number_traces: int = 1,
979
        skip_delegate_calls: bool = False,
980
    ) -> Optional[Dict[str, Any]]:
981
        """
982
        :param tx_hash:
983
        :param trace_address:
984
        :param number_traces: Number of traces to skip, by default get the immediately previous one
985
        :param skip_delegate_calls: If True filter out delegate calls
986
        :return: Parent trace for a trace
987
        :raises: ``ValueError`` if tracing is not supported
988
        """
989
        if len(trace_address) < number_traces:
4✔
990
            return None
4✔
991

992
        trace_address = trace_address[:-number_traces]
4✔
993
        traces = reversed(self.trace_transaction(tx_hash))
4✔
994
        for trace in traces:
4✔
995
            if trace_address == trace["traceAddress"]:
4✔
996
                if (
4✔
997
                    skip_delegate_calls
998
                    and trace["action"].get("callType") == "delegatecall"
999
                ):
1000
                    trace_address = trace_address[:-1]
4✔
1001
                else:
1002
                    return trace
4✔
1003

1004
    def get_next_traces(
4✔
1005
        self,
1006
        tx_hash: EthereumHash,
1007
        trace_address: Sequence[int],
1008
        remove_delegate_calls: bool = False,
1009
        remove_calls: bool = False,
1010
    ) -> List[FilterTrace]:
1011
        """
1012
        :param tx_hash:
1013
        :param trace_address:
1014
        :param remove_delegate_calls: If True remove delegate calls from result
1015
        :param remove_calls: If True remove calls from result
1016
        :return: Children for a trace, E.g. if address is [0, 1] and number_traces = 1, it will return [0, 1, x]
1017
        :raises: ``ValueError`` if tracing is not supported
1018
        """
1019
        trace_address_len = len(trace_address)
4✔
1020
        traces: List[FilterTrace] = []
4✔
1021
        for trace in self.trace_transaction(tx_hash):
4✔
1022
            if (
4✔
1023
                trace_address_len + 1 == len(trace["traceAddress"])
1024
                and trace_address == trace["traceAddress"][:-1]
1025
            ):
1026
                if (
4✔
1027
                    remove_delegate_calls
1028
                    and trace["action"].get("callType") == "delegatecall"
1029
                ):
1030
                    pass
4✔
1031
                elif remove_calls and trace["action"].get("callType") == "call":
4✔
1032
                    pass
4✔
1033
                else:
1034
                    traces.append(trace)
4✔
1035
        return traces
4✔
1036

1037
    def trace_block(self, block_identifier: BlockIdentifier) -> List[BlockTrace]:
4✔
1038
        return self.slow_w3.tracing.trace_block(block_identifier)
4✔
1039

1040
    def trace_blocks(
4✔
1041
        self, block_identifiers: Sequence[BlockIdentifier]
1042
    ) -> List[List[BlockTrace]]:
1043
        if not block_identifiers:
4✔
1044
            return []
×
1045
        payload = [
4✔
1046
            {
1047
                "id": i,
1048
                "jsonrpc": "2.0",
1049
                "method": "trace_block",
1050
                "params": [
1051
                    hex(block_identifier)
1052
                    if isinstance(block_identifier, int)
1053
                    else block_identifier
1054
                ],
1055
            }
1056
            for i, block_identifier in enumerate(block_identifiers)
1057
        ]
1058

1059
        results = self.ethereum_client.raw_batch_request(payload)
4✔
1060
        return [trace_list_result_formatter(block_traces) for block_traces in results]
4✔
1061

1062
    def trace_transaction(self, tx_hash: EthereumHash) -> List[FilterTrace]:
4✔
1063
        """
1064
        :param tx_hash:
1065
        :return: List of internal txs for `tx_hash`
1066
        """
1067
        return self.slow_w3.tracing.trace_transaction(tx_hash)
4✔
1068

1069
    def trace_transactions(
4✔
1070
        self, tx_hashes: Sequence[EthereumHash]
1071
    ) -> List[List[FilterTrace]]:
1072
        """
1073
        :param tx_hashes:
1074
        :return: For every `tx_hash` a list of internal txs (in the same order as the `tx_hashes` were provided)
1075
        """
1076
        if not tx_hashes:
4✔
1077
            return []
×
1078
        payload = [
4✔
1079
            {
1080
                "id": i,
1081
                "jsonrpc": "2.0",
1082
                "method": "trace_transaction",
1083
                "params": [HexBytes(tx_hash).hex()],
1084
            }
1085
            for i, tx_hash in enumerate(tx_hashes)
1086
        ]
1087
        results = self.ethereum_client.raw_batch_request(payload)
4✔
1088
        return [trace_list_result_formatter(tx_traces) for tx_traces in results]
4✔
1089

1090
    def trace_filter(
4✔
1091
        self,
1092
        from_block: int = 1,
1093
        to_block: Optional[int] = None,
1094
        from_address: Optional[Sequence[ChecksumAddress]] = None,
1095
        to_address: Optional[Sequence[ChecksumAddress]] = None,
1096
        after: Optional[int] = None,
1097
        count: Optional[int] = None,
1098
    ) -> List[FilterTrace]:
1099
        """
1100
        Get events using ``trace_filter`` method
1101

1102
        :param from_block: Quantity or Tag - (optional) From this block. `0` is not working, it needs to be `>= 1`
1103
        :param to_block: Quantity or Tag - (optional) To this block.
1104
        :param from_address: Array - (optional) Sent from these addresses.
1105
        :param to_address: Address - (optional) Sent to these addresses.
1106
        :param after: Quantity - (optional) The offset trace number
1107
        :param count: Quantity - (optional) Integer number of traces to display in a batch.
1108
        :return:
1109

1110
        .. code-block:: python
1111

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

1171
        """
1172
        assert (
4✔
1173
            from_address or to_address
1174
        ), "You must provide at least `from_address` or `to_address`"
1175
        parameters: TraceFilterParams = {}
4✔
1176
        if after:
4✔
1177
            parameters["after"] = after
×
1178
        if count:
4✔
1179
            parameters["count"] = count
×
1180
        if from_block:
4✔
1181
            parameters["fromBlock"] = HexStr("0x%x" % from_block)
4✔
1182
        if to_block:
4✔
1183
            parameters["toBlock"] = HexStr("0x%x" % to_block)
4✔
1184
        if from_address:
4✔
1185
            parameters["fromAddress"] = from_address
×
1186
        if to_address:
4✔
1187
            parameters["toAddress"] = to_address
4✔
1188

1189
        return self.slow_w3.tracing.trace_filter(parameters)
4✔
1190

1191

1192
class EthereumClient:
4✔
1193
    """
1194
    Manage ethereum operations. Uses web3 for the most part, but some other stuff is implemented from scratch.
1195
    Note: If you want to use `pending` state with `Parity`, it must be run with `--pruning=archive` or `--force-sealing`
1196
    """
1197

1198
    NULL_ADDRESS = NULL_ADDRESS
4✔
1199

1200
    def __init__(
4✔
1201
        self,
1202
        ethereum_node_url: URI = URI("http://localhost:8545"),
1203
        provider_timeout: int = 15,
1204
        slow_provider_timeout: int = 60,
1205
        retry_count: int = 1,
1206
        use_caching_middleware: bool = True,
1207
        batch_request_max_size: int = 500,
1208
    ):
1209
        """
1210
        :param ethereum_node_url: Ethereum RPC uri
1211
        :param provider_timeout: Timeout for regular RPC queries
1212
        :param slow_provider_timeout: Timeout for slow (tracing, logs...) and custom RPC queries
1213
        :param retry_count: Retry count for failed requests
1214
        :param use_caching_middleware: Use web3 simple cache middleware: https://web3py.readthedocs.io/en/stable/middleware.html#web3.middleware.construct_simple_cache_middleware
1215
        :param batch_request_max_size: Max size for JSON RPC Batch requests. Some providers have a limitation on 500
1216
        """
1217
        self.http_session = prepare_http_session(1, 100, retry_count=retry_count)
4✔
1218
        self.ethereum_node_url: str = ethereum_node_url
4✔
1219
        self.timeout = provider_timeout
4✔
1220
        self.slow_timeout = slow_provider_timeout
4✔
1221
        self.use_caching_middleware = use_caching_middleware
4✔
1222

1223
        self.w3_provider = HTTPProvider(
4✔
1224
            self.ethereum_node_url,
1225
            request_kwargs={"timeout": provider_timeout},
1226
            session=self.http_session,
1227
        )
1228
        self.w3_slow_provider = HTTPProvider(
4✔
1229
            self.ethereum_node_url,
1230
            request_kwargs={"timeout": slow_provider_timeout},
1231
            session=self.http_session,
1232
        )
1233
        self.w3: Web3 = Web3(self.w3_provider)
4✔
1234
        self.slow_w3: Web3 = Web3(self.w3_slow_provider)
4✔
1235

1236
        # Adjust Web3.py middleware
1237
        for w3 in self.w3, self.slow_w3:
4✔
1238
            # Don't spend resources con converting dictionaries to attribute dictionaries
1239
            w3.middleware_onion.remove("attrdict")
4✔
1240
            # Disable web3 automatic retry, it's handled on our HTTP Session
1241
            w3.provider.middlewares = []
4✔
1242
            if self.use_caching_middleware:
4✔
1243
                w3.middleware_onion.add(simple_cache_middleware)
4✔
1244

1245
        # The geth_poa_middleware is required to connect to geth --dev or the Goerli public network.
1246
        # It may also be needed for other EVM compatible blockchains like Polygon or BNB Chain (Binance Smart Chain).
1247
        try:
4✔
1248
            if self.get_network() != EthereumNetwork.MAINNET:
4✔
1249
                for w3 in self.w3, self.slow_w3:
4✔
1250
                    w3.middleware_onion.inject(geth_poa_middleware, layer=0)
4✔
1251
        except (IOError, OSError):
×
1252
            # For tests using dummy connections (like IPC)
1253
            for w3 in self.w3, self.slow_w3:
×
1254
                w3.middleware_onion.inject(geth_poa_middleware, layer=0)
×
1255

1256
        self.erc20: Erc20Manager = Erc20Manager(self)
4✔
1257
        self.erc721: Erc721Manager = Erc721Manager(self)
4✔
1258
        self.tracing: TracingManager = TracingManager(self)
4✔
1259
        self.batch_call_manager: BatchCallManager = BatchCallManager(self)
4✔
1260
        self.batch_request_max_size = batch_request_max_size
4✔
1261

1262
    def __str__(self):
4✔
1263
        return f"EthereumClient for url={self.ethereum_node_url}"
4✔
1264

1265
    def raw_batch_request(
4✔
1266
        self, payload: Sequence[Dict[str, Any]], batch_size: Optional[int] = None
1267
    ) -> Iterable[Optional[Dict[str, Any]]]:
1268
        """
1269
        Perform a raw batch JSON RPC call
1270

1271
        :param payload: Batch request payload. Make sure all provided `ids` inside the payload are different
1272
        :param batch_size: If `payload` length is bigger than size, it will be split into smaller chunks before
1273
            sending to the server
1274
        :return:
1275
        :raises: ValueError
1276
        """
1277

1278
        batch_size = batch_size or self.batch_request_max_size
4✔
1279

1280
        all_results = []
4✔
1281
        for chunk in chunks(payload, batch_size):
4✔
1282
            response = self.http_session.post(
4✔
1283
                self.ethereum_node_url, json=chunk, timeout=self.slow_timeout
1284
            )
1285

1286
            if not response.ok:
4✔
1287
                logger.error(
×
1288
                    "Problem doing raw batch request with payload=%s status_code=%d result=%s",
1289
                    chunk,
1290
                    response.status_code,
1291
                    response.content,
1292
                )
1293
                raise ValueError(f"Batch request error: {response.content}")
×
1294

1295
            results = response.json()
4✔
1296

1297
            # If there's an error some nodes return a json instead of a list
1298
            if isinstance(results, dict) and "error" in results:
4✔
1299
                logger.error(
4✔
1300
                    "Batch request problem with payload=%s, result=%s)", chunk, results
1301
                )
1302
                raise ValueError(f"Batch request error: {results}")
4✔
1303

1304
            all_results.extend(results)
4✔
1305

1306
        # Nodes like Erigon send back results out of order
1307
        for query, result in zip(payload, sorted(all_results, key=lambda x: x["id"])):
4✔
1308
            if "result" not in result:
4✔
1309
                message = f"Problem with payload=`{query}` result={result}"
4✔
1310
                logger.error(message)
4✔
1311
                raise ValueError(message)
4✔
1312

1313
            yield result["result"]
4✔
1314

1315
    @property
4✔
1316
    def current_block_number(self):
4✔
1317
        return self.w3.eth.block_number
4✔
1318

1319
    @cache
4✔
1320
    def get_chain_id(self) -> int:
4✔
1321
        """
1322
        :return: ChainId returned by the RPC `eth_chainId` method. It should never change, so it's cached.
1323
        """
1324
        return int(self.w3.eth.chain_id)
4✔
1325

1326
    @cache
4✔
1327
    def get_client_version(self) -> str:
4✔
1328
        """
1329
        :return: RPC version information
1330
        """
1331
        return self.w3.clientVersion
×
1332

1333
    def get_network(self) -> EthereumNetwork:
4✔
1334
        """
1335
        Get network name based on the chainId. This method is not cached as the method for getting the
1336
        `chainId` already is.
1337

1338
        :return: EthereumNetwork based on the chainId. If network is not
1339
            on our list, `EthereumNetwork.UNKNOWN` is returned
1340
        """
1341
        return EthereumNetwork(self.get_chain_id())
4✔
1342

1343
    @cache
4✔
1344
    def get_singleton_factory_address(self) -> Optional[ChecksumAddress]:
4✔
1345
        """
1346
        Get singleton factory address if available. Try the singleton managed by Safe by default unless
1347
        SAFE_SINGLETON_FACTORY_ADDRESS environment variable is defined.
1348

1349
        More info: https://github.com/safe-global/safe-singleton-factory
1350

1351
        :return: Get singleton factory address if available
1352
        """
1353
        address = os.environ.get(
4✔
1354
            "SAFE_SINGLETON_FACTORY_ADDRESS", SAFE_SINGLETON_FACTORY_ADDRESS
1355
        )
1356
        if self.is_contract(address):
4✔
1357
            return address
4✔
1358
        return None
4✔
1359

1360
    @cache
4✔
1361
    def is_eip1559_supported(self) -> bool:
4✔
1362
        """
1363
        :return: `True` if EIP1559 is supported by the node, `False` otherwise
1364
        """
1365
        try:
4✔
1366
            self.w3.eth.fee_history(1, "latest", reward_percentiles=[50])
4✔
1367
            return True
4✔
1368
        except (Web3Exception, ValueError):
×
1369
            return False
×
1370

1371
    @cached_property
4✔
1372
    def multicall(self) -> "Multicall":  # noqa F821
4✔
1373
        from .multicall import Multicall
4✔
1374

1375
        try:
4✔
1376
            return Multicall(self)
4✔
1377
        except EthereumNetworkNotSupported:
×
1378
            logger.warning("Multicall not supported for this network")
×
1379
            return None
×
1380

1381
    def batch_call(
4✔
1382
        self,
1383
        contract_functions: Iterable[ContractFunction],
1384
        from_address: Optional[ChecksumAddress] = None,
1385
        raise_exception: bool = True,
1386
        force_batch_call: bool = False,
1387
        block_identifier: Optional[BlockIdentifier] = "latest",
1388
    ) -> List[Optional[Union[bytes, Any]]]:
1389
        """
1390
        Call multiple functions. ``Multicall`` contract by MakerDAO will be used by default if available
1391

1392
        :param contract_functions:
1393
        :param from_address: Only available when ``Multicall`` is not used
1394
        :param raise_exception: If ``True``, raise ``BatchCallException`` if one of the calls fails
1395
        :param force_batch_call: If ``True``, ignore multicall and always use batch calls to get the
1396
            result (less optimal). If ``False``, more optimal way will be tried.
1397
        :param block_identifier:
1398
        :return: List of elements decoded to their types, ``None`` if they cannot be decoded and
1399
            bytes if a revert error is returned and ``raise_exception=False``
1400
        :raises: BatchCallException
1401
        """
1402
        if self.multicall and not force_batch_call:  # Multicall is more optimal
4✔
1403
            return [
4✔
1404
                result.return_data_decoded
1405
                for result in self.multicall.try_aggregate(
1406
                    contract_functions,
1407
                    require_success=raise_exception,
1408
                    block_identifier=block_identifier,
1409
                )
1410
            ]
1411
        else:
1412
            return self.batch_call_manager.batch_call(
×
1413
                contract_functions,
1414
                from_address=from_address,
1415
                raise_exception=raise_exception,
1416
                block_identifier=block_identifier,
1417
            )
1418

1419
    def batch_call_same_function(
4✔
1420
        self,
1421
        contract_function: ContractFunction,
1422
        contract_addresses: Sequence[ChecksumAddress],
1423
        from_address: Optional[ChecksumAddress] = None,
1424
        raise_exception: bool = True,
1425
        force_batch_call: bool = False,
1426
        block_identifier: Optional[BlockIdentifier] = "latest",
1427
    ) -> List[Optional[Union[bytes, Any]]]:
1428
        """
1429
        Call the same function in multiple contracts. Way more optimal than using ``batch_call`` generating multiple
1430
        ``ContractFunction`` objects.
1431

1432
        :param contract_function:
1433
        :param contract_addresses:
1434
        :param from_address: Only available when ``Multicall`` is not used
1435
        :param raise_exception: If ``True``, raise ``BatchCallException`` if one of the calls fails
1436
        :param force_batch_call: If ``True``, ignore multicall and always use batch calls to get the
1437
            result (less optimal). If ``False``, more optimal way will be tried.
1438
        :param block_identifier:
1439
        :return: List of elements decoded to the same type, ``None`` if they cannot be decoded and
1440
            bytes if a revert error is returned and ``raise_exception=False``
1441
        :raises: BatchCallException
1442
        """
1443
        if self.multicall and not force_batch_call:  # Multicall is more optimal
4✔
1444
            return [
4✔
1445
                result.return_data_decoded
1446
                for result in self.multicall.try_aggregate_same_function(
1447
                    contract_function,
1448
                    contract_addresses,
1449
                    require_success=raise_exception,
1450
                    block_identifier=block_identifier,
1451
                )
1452
            ]
1453
        else:
1454
            return self.batch_call_manager.batch_call_same_function(
×
1455
                contract_function,
1456
                contract_addresses,
1457
                from_address=from_address,
1458
                raise_exception=raise_exception,
1459
                block_identifier=block_identifier,
1460
            )
1461

1462
    def deploy_and_initialize_contract(
4✔
1463
        self,
1464
        deployer_account: LocalAccount,
1465
        constructor_data: Union[bytes, HexStr],
1466
        initializer_data: Optional[Union[bytes, HexStr]] = None,
1467
        check_receipt: bool = True,
1468
        deterministic: bool = True,
1469
    ) -> EthereumTxSent:
1470
        """
1471

1472
        :param deployer_account:
1473
        :param constructor_data:
1474
        :param initializer_data:
1475
        :param check_receipt:
1476
        :param deterministic: Use Safe singleton factory for CREATE2 deterministic deployment
1477
        :return:
1478
        """
1479
        contract_address: Optional[ChecksumAddress] = None
4✔
1480
        for data in (constructor_data, initializer_data):
4✔
1481
            # Because initializer_data is not mandatory
1482
            if data:
4✔
1483
                data = HexBytes(data)
4✔
1484
                tx: TxParams = {
4✔
1485
                    "from": deployer_account.address,
1486
                    "data": data,
1487
                    "gasPrice": self.w3.eth.gas_price,
1488
                    "value": Wei(0),
1489
                    "to": contract_address if contract_address else "",
1490
                    "chainId": self.get_chain_id(),
1491
                    "nonce": self.get_nonce_for_account(deployer_account.address),
1492
                }
1493
                if not contract_address:
4✔
1494
                    if deterministic and (
4✔
1495
                        singleton_factory_address := self.get_singleton_factory_address()
1496
                    ):
1497
                        salt = HexBytes("0" * 64)
4✔
1498
                        tx["data"] = (
4✔
1499
                            salt + data
1500
                        )  # Add 32 bytes salt for singleton factory
1501
                        tx["to"] = singleton_factory_address
4✔
1502
                        contract_address = mk_contract_address_2(
4✔
1503
                            singleton_factory_address, salt, data
1504
                        )
1505
                        if self.is_contract(contract_address):
4✔
1506
                            raise ContractAlreadyDeployed(
×
1507
                                f"Contract {contract_address} already deployed",
1508
                                contract_address,
1509
                            )
1510
                    else:
1511
                        contract_address = mk_contract_address(tx["from"], tx["nonce"])
4✔
1512

1513
                tx["gas"] = self.w3.eth.estimate_gas(tx)
4✔
1514
                tx_hash = self.send_unsigned_transaction(
4✔
1515
                    tx, private_key=deployer_account.key
1516
                )
1517
                if check_receipt:
4✔
1518
                    tx_receipt = self.get_transaction_receipt(
4✔
1519
                        Hash32(tx_hash), timeout=60
1520
                    )
1521
                    assert tx_receipt
4✔
1522
                    assert tx_receipt["status"]
4✔
1523

1524
        return EthereumTxSent(tx_hash, tx, contract_address)
4✔
1525

1526
    def get_nonce_for_account(
4✔
1527
        self,
1528
        address: ChecksumAddress,
1529
        block_identifier: Optional[BlockIdentifier] = "latest",
1530
    ):
1531
        """
1532
        Get nonce for account. `getTransactionCount` is the only method for what `pending` is currently working
1533
        (Geth and Parity)
1534

1535
        :param address:
1536
        :param block_identifier:
1537
        :return:
1538
        """
1539
        return self.w3.eth.get_transaction_count(
4✔
1540
            address, block_identifier=block_identifier
1541
        )
1542

1543
    def estimate_gas(
4✔
1544
        self,
1545
        to: str,
1546
        from_: Optional[str] = None,
1547
        value: Optional[int] = None,
1548
        data: Optional[EthereumData] = None,
1549
        gas: Optional[int] = None,
1550
        gas_price: Optional[int] = None,
1551
        block_identifier: Optional[BlockIdentifier] = None,
1552
    ) -> int:
1553
        """
1554
        Estimate gas calling `eth_estimateGas`
1555

1556
        :param from_:
1557
        :param to:
1558
        :param value:
1559
        :param data:
1560
        :param gas:
1561
        :param gas_price:
1562
        :param block_identifier: Be careful, `Geth` does not support `pending` when estimating
1563
        :return: Amount of gas needed for transaction
1564
        :raises: ValueError
1565
        """
1566
        tx: TxParams = {"to": to}
4✔
1567
        if from_:
4✔
1568
            tx["from"] = from_
4✔
1569
        if value:
4✔
1570
            tx["value"] = value
4✔
1571
        if data:
4✔
1572
            tx["data"] = data
4✔
1573
        if gas:
4✔
1574
            tx["gas"] = gas
×
1575
        if gas_price:
4✔
1576
            tx["gasPrice"] = gas_price
×
1577
        try:
4✔
1578
            return self.w3.eth.estimate_gas(tx, block_identifier=block_identifier)
4✔
1579
        except (Web3Exception, ValueError):
4✔
1580
            if (
4✔
1581
                block_identifier is not None
1582
            ):  # Geth does not support setting `block_identifier`
1583
                return self.w3.eth.estimate_gas(tx, block_identifier=None)
×
1584
            else:
1585
                raise
4✔
1586

1587
    @staticmethod
4✔
1588
    def estimate_data_gas(data: bytes):
4✔
1589
        """
1590
        Estimate gas costs only for "storage" of the ``data`` bytes provided
1591

1592
        :param data:
1593
        :return:
1594
        """
1595
        if isinstance(data, str):
4✔
1596
            data = HexBytes(data)
×
1597

1598
        gas = 0
4✔
1599
        for byte in data:
4✔
1600
            if not byte:
4✔
1601
                gas += GAS_CALL_DATA_ZERO_BYTE
4✔
1602
            else:
1603
                gas += GAS_CALL_DATA_BYTE
4✔
1604
        return gas
4✔
1605

1606
    def estimate_fee_eip1559(
4✔
1607
        self, tx_speed: TxSpeed = TxSpeed.NORMAL
1608
    ) -> Tuple[int, int]:
1609
        """
1610
        Check https://github.com/ethereum/execution-apis/blob/main/src/eth/fee_market.json#L15
1611

1612
        :return: Tuple[BaseFeePerGas, MaxPriorityFeePerGas]
1613
        :raises: ValueError if not supported on the network
1614
        """
1615
        if tx_speed == TxSpeed.SLOWEST:
4✔
1616
            percentile = 0
×
1617
        elif tx_speed == TxSpeed.VERY_SLOW:
4✔
1618
            percentile = 10
×
1619
        elif tx_speed == TxSpeed.SLOW:
4✔
1620
            percentile = 25
×
1621
        elif tx_speed == TxSpeed.NORMAL:
4✔
1622
            percentile = 50
4✔
1623
        elif tx_speed == TxSpeed.FAST:
×
1624
            percentile = 75
×
1625
        elif tx_speed == TxSpeed.VERY_FAST:
×
1626
            percentile = 90
×
1627
        elif tx_speed == TxSpeed.FASTEST:
×
1628
            percentile = 100
×
1629

1630
        result = self.w3.eth.fee_history(1, "latest", reward_percentiles=[percentile])
4✔
1631
        # Get next block `base_fee_per_gas`
1632
        base_fee_per_gas = result["baseFeePerGas"][-1]
4✔
1633
        max_priority_fee_per_gas = result["reward"][0][0]
4✔
1634
        return base_fee_per_gas, max_priority_fee_per_gas
4✔
1635

1636
    def set_eip1559_fees(
4✔
1637
        self, tx: TxParams, tx_speed: TxSpeed = TxSpeed.NORMAL
1638
    ) -> TxParams:
1639
        """
1640
        :return: TxParams in EIP1559 format
1641
        :raises: ValueError if EIP1559 not supported
1642
        """
1643
        base_fee_per_gas, max_priority_fee_per_gas = self.estimate_fee_eip1559(tx_speed)
4✔
1644
        tx = TxParams(tx)  # Don't modify provided tx
4✔
1645
        if "gasPrice" in tx:
4✔
1646
            del tx["gasPrice"]
4✔
1647

1648
        if "chainId" not in tx:
4✔
1649
            tx["chainId"] = self.get_chain_id()
4✔
1650

1651
        tx["maxPriorityFeePerGas"] = max_priority_fee_per_gas
4✔
1652
        tx["maxFeePerGas"] = base_fee_per_gas + max_priority_fee_per_gas
4✔
1653
        return tx
4✔
1654

1655
    def get_balance(
4✔
1656
        self,
1657
        address: ChecksumAddress,
1658
        block_identifier: Optional[BlockIdentifier] = None,
1659
    ):
1660
        return self.w3.eth.get_balance(address, block_identifier)
4✔
1661

1662
    def get_transaction(self, tx_hash: EthereumHash) -> Optional[TxData]:
4✔
1663
        try:
4✔
1664
            return self.w3.eth.get_transaction(tx_hash)
4✔
1665
        except TransactionNotFound:
×
1666
            return None
×
1667

1668
    def get_transactions(
4✔
1669
        self, tx_hashes: Sequence[EthereumHash]
1670
    ) -> List[Optional[TxData]]:
1671
        if not tx_hashes:
4✔
1672
            return []
4✔
1673
        payload = [
4✔
1674
            {
1675
                "id": i,
1676
                "jsonrpc": "2.0",
1677
                "method": "eth_getTransactionByHash",
1678
                "params": [HexBytes(tx_hash).hex()],
1679
            }
1680
            for i, tx_hash in enumerate(tx_hashes)
1681
        ]
1682
        results = self.raw_batch_request(payload)
4✔
1683
        return [
4✔
1684
            transaction_result_formatter(raw_tx) if raw_tx else None
1685
            for raw_tx in results
1686
        ]
1687

1688
    def get_transaction_receipt(
4✔
1689
        self, tx_hash: EthereumHash, timeout=None
1690
    ) -> Optional[TxReceipt]:
1691
        try:
4✔
1692
            if not timeout:
4✔
1693
                tx_receipt = self.w3.eth.get_transaction_receipt(tx_hash)
4✔
1694
            else:
1695
                try:
4✔
1696
                    tx_receipt = self.w3.eth.wait_for_transaction_receipt(
4✔
1697
                        tx_hash, timeout=timeout
1698
                    )
1699
                except TimeExhausted:
4✔
1700
                    return None
4✔
1701

1702
            # Parity returns tx_receipt even is tx is still pending, so we check `blockNumber` is not None
1703
            return (
4✔
1704
                tx_receipt
1705
                if tx_receipt and tx_receipt["blockNumber"] is not None
1706
                else None
1707
            )
1708
        except TransactionNotFound:
4✔
1709
            return None
4✔
1710

1711
    def get_transaction_receipts(
4✔
1712
        self, tx_hashes: Sequence[EthereumData]
1713
    ) -> List[Optional[TxReceipt]]:
1714
        if not tx_hashes:
4✔
1715
            return []
4✔
1716
        payload = [
4✔
1717
            {
1718
                "id": i,
1719
                "jsonrpc": "2.0",
1720
                "method": "eth_getTransactionReceipt",
1721
                "params": [HexBytes(tx_hash).hex()],
1722
            }
1723
            for i, tx_hash in enumerate(tx_hashes)
1724
        ]
1725
        results = self.raw_batch_request(payload)
4✔
1726
        receipts = []
4✔
1727
        for tx_receipt in results:
4✔
1728
            # Parity returns tx_receipt even is tx is still pending, so we check `blockNumber` is not None
1729
            if tx_receipt and tx_receipt["blockNumber"] is not None:
4✔
1730
                receipts.append(receipt_formatter(tx_receipt))
4✔
1731
            else:
1732
                receipts.append(None)
×
1733
        return receipts
4✔
1734

1735
    def get_block(
4✔
1736
        self, block_identifier: BlockIdentifier, full_transactions: bool = False
1737
    ) -> Optional[BlockData]:
1738
        try:
4✔
1739
            return self.w3.eth.get_block(
4✔
1740
                block_identifier, full_transactions=full_transactions
1741
            )
1742
        except BlockNotFound:
×
1743
            return None
×
1744

1745
    def _parse_block_identifier(self, block_identifier: BlockIdentifier) -> str:
4✔
1746
        if isinstance(block_identifier, int):
4✔
1747
            return hex(block_identifier)
4✔
1748
        elif isinstance(block_identifier, bytes):
4✔
1749
            return HexBytes(block_identifier).hex()
4✔
1750
        else:
1751
            return block_identifier
4✔
1752

1753
    def get_blocks(
4✔
1754
        self,
1755
        block_identifiers: Iterable[BlockIdentifier],
1756
        full_transactions: bool = False,
1757
    ) -> List[Optional[BlockData]]:
1758
        if not block_identifiers:
4✔
1759
            return []
4✔
1760
        payload = [
4✔
1761
            {
1762
                "id": i,
1763
                "jsonrpc": "2.0",
1764
                "method": "eth_getBlockByNumber"
1765
                if isinstance(block_identifier, int)
1766
                else "eth_getBlockByHash",
1767
                "params": [
1768
                    self._parse_block_identifier(block_identifier),
1769
                    full_transactions,
1770
                ],
1771
            }
1772
            for i, block_identifier in enumerate(block_identifiers)
1773
        ]
1774
        results = self.raw_batch_request(payload)
4✔
1775
        blocks = []
4✔
1776
        for raw_block in results:
4✔
1777
            if raw_block:
4✔
1778
                if "extraData" in raw_block:
4✔
1779
                    del raw_block[
4✔
1780
                        "extraData"
1781
                    ]  # Remove extraData, raises some problems on parsing
1782
                blocks.append(block_formatter(raw_block))
4✔
1783
            else:
1784
                blocks.append(None)
×
1785
        return blocks
4✔
1786

1787
    def is_contract(self, contract_address: ChecksumAddress) -> bool:
4✔
1788
        return bool(self.w3.eth.get_code(contract_address))
4✔
1789

1790
    @staticmethod
4✔
1791
    def build_tx_params(
4✔
1792
        from_address: Optional[ChecksumAddress] = None,
1793
        to_address: Optional[ChecksumAddress] = None,
1794
        value: Optional[int] = None,
1795
        gas: Optional[int] = None,
1796
        gas_price: Optional[int] = None,
1797
        nonce: Optional[int] = None,
1798
        chain_id: Optional[int] = None,
1799
        tx_params: Optional[TxParams] = None,
1800
    ) -> TxParams:
1801
        """
1802
        Build tx params dictionary.
1803
        If an existing TxParams dictionary is provided the fields will be replaced by the provided ones
1804

1805
        :param from_address:
1806
        :param to_address:
1807
        :param value:
1808
        :param gas:
1809
        :param gas_price:
1810
        :param nonce:
1811
        :param chain_id:
1812
        :param tx_params: An existing TxParams dictionary will be replaced by the providen values
1813
        :return:
1814
        """
1815

1816
        tx_params: TxParams = tx_params or {}
4✔
1817

1818
        if from_address:
4✔
1819
            tx_params["from"] = from_address
4✔
1820

1821
        if to_address:
4✔
1822
            tx_params["to"] = to_address
×
1823

1824
        if value is not None:
4✔
1825
            tx_params["value"] = value
×
1826

1827
        if gas_price is not None:
4✔
1828
            tx_params["gasPrice"] = gas_price
4✔
1829

1830
        if gas is not None:
4✔
1831
            tx_params["gas"] = gas
4✔
1832

1833
        if nonce is not None:
4✔
1834
            tx_params["nonce"] = nonce
×
1835

1836
        if chain_id is not None:
4✔
1837
            tx_params["chainId"] = chain_id
×
1838

1839
        return tx_params
4✔
1840

1841
    @tx_with_exception_handling
4✔
1842
    def send_transaction(self, transaction_dict: TxParams) -> HexBytes:
4✔
1843
        return self.w3.eth.send_transaction(transaction_dict)
4✔
1844

1845
    @tx_with_exception_handling
4✔
1846
    def send_raw_transaction(self, raw_transaction: EthereumData) -> HexBytes:
4✔
1847
        return self.w3.eth.send_raw_transaction(bytes(raw_transaction))
4✔
1848

1849
    def send_unsigned_transaction(
4✔
1850
        self,
1851
        tx: TxParams,
1852
        private_key: Optional[str] = None,
1853
        public_key: Optional[str] = None,
1854
        retry: bool = False,
1855
        block_identifier: Optional[BlockIdentifier] = "pending",
1856
    ) -> HexBytes:
1857
        """
1858
        Send a tx using an unlocked public key in the node or a private key. Both `public_key` and
1859
        `private_key` cannot be `None`
1860

1861
        :param tx:
1862
        :param private_key:
1863
        :param public_key:
1864
        :param retry: Retry if a problem with nonce is found
1865
        :param block_identifier: For nonce calculation, recommended is `pending`
1866
        :return: tx hash
1867
        """
1868

1869
        # TODO Refactor this method, it's not working well with new version of the nodes
1870
        if private_key:
4✔
1871
            address = Account.from_key(private_key).address
4✔
1872
        elif public_key:
4✔
1873
            address = public_key
4✔
1874
        else:
1875
            logger.error(
4✔
1876
                "No ethereum account provided. Need a public_key or private_key"
1877
            )
1878
            raise ValueError(
4✔
1879
                "Ethereum account was not configured or unlocked in the node"
1880
            )
1881

1882
        if tx.get("nonce") is None:
4✔
1883
            tx["nonce"] = self.get_nonce_for_account(
4✔
1884
                address, block_identifier=block_identifier
1885
            )
1886

1887
        number_errors = 5
4✔
1888
        while number_errors >= 0:
4✔
1889
            try:
4✔
1890
                if private_key:
4✔
1891
                    signed_tx = self.w3.eth.account.sign_transaction(
4✔
1892
                        tx, private_key=private_key
1893
                    )
1894
                    logger.debug(
4✔
1895
                        "Sending %d wei from %s to %s", tx["value"], address, tx["to"]
1896
                    )
1897
                    try:
4✔
1898
                        return self.send_raw_transaction(signed_tx.rawTransaction)
4✔
1899
                    except TransactionAlreadyImported as e:
4✔
1900
                        # Sometimes Parity 2.2.11 fails with Transaction already imported, even if it's not, but it's
1901
                        # processed
1902
                        tx_hash = signed_tx.hash
×
1903
                        logger.error(
×
1904
                            "Transaction with tx-hash=%s already imported: %s"
1905
                            % (tx_hash.hex(), str(e))
1906
                        )
1907
                        return tx_hash
×
1908
                elif public_key:
4✔
1909
                    tx["from"] = address
4✔
1910
                    return self.send_transaction(tx)
4✔
1911
            except ReplacementTransactionUnderpriced as e:
4✔
1912
                if not retry or not number_errors:
×
1913
                    raise e
×
1914
                current_nonce = tx["nonce"]
×
1915
                tx["nonce"] = max(
×
1916
                    current_nonce + 1,
1917
                    self.get_nonce_for_account(
1918
                        address, block_identifier=block_identifier
1919
                    ),
1920
                )
1921
                logger.error(
×
1922
                    "Tx with nonce=%d was already sent for address=%s, retrying with nonce=%s",
1923
                    current_nonce,
1924
                    address,
1925
                    tx["nonce"],
1926
                )
1927
            except InvalidNonce as e:
4✔
1928
                if not retry or not number_errors:
4✔
1929
                    raise e
4✔
1930
                logger.error(
4✔
1931
                    "address=%s Tx with invalid nonce=%d, retrying recovering nonce again",
1932
                    address,
1933
                    tx["nonce"],
1934
                )
1935
                tx["nonce"] = self.get_nonce_for_account(
4✔
1936
                    address, block_identifier=block_identifier
1937
                )
1938
                number_errors -= 1
4✔
1939

1940
    def send_eth_to(
4✔
1941
        self,
1942
        private_key: str,
1943
        to: str,
1944
        gas_price: int,
1945
        value: Wei,
1946
        gas: Optional[int] = None,
1947
        nonce: Optional[int] = None,
1948
        retry: bool = False,
1949
        block_identifier: Optional[BlockIdentifier] = "pending",
1950
    ) -> bytes:
1951
        """
1952
        Send ether using configured account
1953

1954
        :param private_key: to
1955
        :param to: to
1956
        :param gas_price: gas_price
1957
        :param value: value(wei)
1958
        :param gas: gas, defaults to 22000
1959
        :param retry: Retry if a problem is found
1960
        :param nonce: Nonce of sender account
1961
        :param block_identifier: Block identifier for nonce calculation
1962
        :return: tx_hash
1963
        """
1964

1965
        assert fast_is_checksum_address(to)
4✔
1966

1967
        account = Account.from_key(private_key)
4✔
1968

1969
        tx: TxParams = {
4✔
1970
            "from": account.address,
1971
            "to": to,
1972
            "value": value,
1973
            "gas": gas or Wei(self.estimate_gas(to, account.address, value)),
1974
            "gasPrice": Wei(gas_price),
1975
            "chainId": self.get_chain_id(),
1976
        }
1977

1978
        if nonce is not None:
4✔
1979
            tx["nonce"] = Nonce(nonce)
×
1980

1981
        return self.send_unsigned_transaction(
4✔
1982
            tx, private_key=private_key, retry=retry, block_identifier=block_identifier
1983
        )
1984

1985
    def check_tx_with_confirmations(
4✔
1986
        self, tx_hash: EthereumHash, confirmations: int
1987
    ) -> bool:
1988
        """
1989
        Check tx hash and make sure it has the confirmations required
1990

1991
        :param tx_hash: Hash of the tx
1992
        :param confirmations: Minimum number of confirmations required
1993
        :return: True if tx was mined with the number of confirmations required, False otherwise
1994
        """
1995
        tx_receipt = self.get_transaction_receipt(tx_hash)
4✔
1996
        if not tx_receipt or tx_receipt["blockNumber"] is None:
4✔
1997
            # If `tx_receipt` exists but `blockNumber` is `None`, tx is still pending (just Parity)
1998
            return False
×
1999
        else:
2000
            return (
4✔
2001
                self.w3.eth.block_number - tx_receipt["blockNumber"]
2002
            ) >= confirmations
2003

2004
    @staticmethod
4✔
2005
    def private_key_to_address(private_key):
4✔
2006
        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