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

safe-global / safe-eth-py / 10793540350

10 Sep 2024 01:31PM UTC coverage: 93.551% (-0.3%) from 93.892%
10793540350

push

github

falvaradorodriguez
Fix cowswap test

8777 of 9382 relevant lines covered (93.55%)

3.74 hits per line

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

85.99
/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,
116
        # https://github.com/ethereum/go-ethereum/blob/eaccdba4ab310e3fb98edbc4b340b5e7c4d767fd/core/tx_pool.go#L72
117
        "There is another transaction with same nonce in the queue": ReplacementTransactionUnderpriced,
118
        # https://github.com/openethereum/openethereum/blob/f1dc6821689c7f47d8fd07dfc0a2c5ad557b98ec/crates/rpc/src/v1/helpers/errors.rs#L374
119
        "There are too many transactions in the queue. Your transaction was dropped due to limit. Try increasing "
120
        "the fee": TransactionQueueLimitReached,
121
        # https://github.com/openethereum/openethereum/blob/f1dc6821689c7f47d8fd07dfc0a2c5ad557b98ec/crates/rpc/src/v1/helpers/errors.rs#L380
122
        "txpool is full": TransactionQueueLimitReached,
123
        # https://github.com/ethereum/go-ethereum/blob/eaccdba4ab310e3fb98edbc4b340b5e7c4d767fd/core/tx_pool.go#L68
124
        "transaction underpriced": TransactionGasPriceTooLow,
125
        # https://github.com/ethereum/go-ethereum/blob/eaccdba4ab310e3fb98edbc4b340b5e7c4d767fd/core/tx_pool.go#L64
126
        "Transaction gas price is too low": TransactionGasPriceTooLow,
127
        # https://github.com/openethereum/openethereum/blob/f1dc6821689c7f47d8fd07dfc0a2c5ad557b98ec/crates/rpc/src/v1/helpers/errors.rs#L386
128
        "from not found": FromAddressNotFound,
129
        "correct nonce": InvalidNonce,
130
        "nonce too low": NonceTooLow,
131
        # https://github.com/ethereum/go-ethereum/blob/bbfb1e4008a359a8b57ec654330c0e674623e52f/core/error.go#L46
132
        "nonce too high": NonceTooHigh,
133
        # https://github.com/ethereum/go-ethereum/blob/bbfb1e4008a359a8b57ec654330c0e674623e52f/core/error.go#L46
134
        "insufficient funds": InsufficientFunds,
135
        # https://github.com/openethereum/openethereum/blob/f1dc6821689c7f47d8fd07dfc0a2c5ad557b98ec/crates/rpc/src/v1/helpers/errors.rs#L389
136
        "doesn't have enough funds": InsufficientFunds,
137
        "sender account not recognized": SenderAccountNotFoundInNode,
138
        "unknown account": UnknownAccount,
139
        "exceeds block gas limit": GasLimitExceeded,  # Geth
140
        "exceeds current gas limit": GasLimitExceeded,
141
        # https://github.com/openethereum/openethereum/blob/f1dc6821689c7f47d8fd07dfc0a2c5ad557b98ec/crates/rpc/src/v1/helpers/errors.rs#L392
142
    }
143

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

155
    return with_exception_handling
4✔
156

157

158
class EthereumTxSent(NamedTuple):
4✔
159
    tx_hash: bytes
4✔
160
    tx: TxParams
4✔
161
    contract_address: Optional[ChecksumAddress]
4✔
162

163

164
class Erc20Info(NamedTuple):
4✔
165
    name: str
4✔
166
    symbol: str
4✔
167
    decimals: int
4✔
168

169

170
class Erc721Info(NamedTuple):
4✔
171
    name: str
4✔
172
    symbol: str
4✔
173

174

175
class TokenBalance(NamedTuple):
4✔
176
    token_address: str
4✔
177
    balance: int
4✔
178

179

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

189

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

200
    :return: A configured singleton of EthereumClient
201
    """
202
    try:
4✔
203
        from django.conf import settings
4✔
204

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

218

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

229

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

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

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

261
            query_params = {"to": payload["to"], "data": payload["data"]}  # Balance of
4✔
262
            if "from" in payload:
4✔
263
                query_params["from"] = payload["from"]
×
264

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

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

290
            results = response.json()
4✔
291

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

301
            all_results.extend(results)
4✔
302

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

330
        if errors and raise_exception:
4✔
331
            raise BatchCallFunctionFailed(f"Errors returned {errors}")
4✔
332
        else:
333
            return return_values
4✔
334

335
    def batch_call(
4✔
336
        self,
337
        contract_functions: Iterable[ContractFunction],
338
        from_address: Optional[ChecksumAddress] = None,
339
        raise_exception: bool = True,
340
        block_identifier: Optional[BlockIdentifier] = "latest",
341
    ) -> List[Optional[Any]]:
342
        """
343
        Do batch requests of multiple contract calls
344

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

363
            payload = {
4✔
364
                "to": contract_function.address,
365
                "data": contract_function.build_transaction(params)["data"],
366
                "output_type": [
367
                    output["type"] for output in contract_function.abi["outputs"]
368
                ],
369
                "fn_name": contract_function.fn_name,  # For debugging purposes
370
            }
371
            if from_address:
4✔
372
                payload["from"] = from_address
×
373
            payloads.append(payload)
4✔
374

375
        return self.batch_call_custom(
4✔
376
            payloads, raise_exception=raise_exception, block_identifier=block_identifier
377
        )
378

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

392
        :param contract_function:
393
        :param contract_addresses:
394
        :param from_address:
395
        :param raise_exception:
396
        :param block_identifier:
397
        :return:
398
        """
399

400
        assert contract_function, "Contract function is required"
4✔
401

402
        if not contract_addresses:
4✔
403
            return []
×
404

405
        contract_function.address = NULL_ADDRESS  # It's required by web3.py
4✔
406
        params: TxParams = {"gas": Wei(0), "gasPrice": Wei(0)}
4✔
407
        data = contract_function.build_transaction(params)["data"]
4✔
408
        output_type = [output["type"] for output in contract_function.abi["outputs"]]
4✔
409
        fn_name = contract_function.fn_name
4✔
410

411
        payloads = []
4✔
412
        for contract_address in contract_addresses:
4✔
413
            payload = {
4✔
414
                "to": contract_address,
415
                "data": data,
416
                "output_type": output_type,
417
                "fn_name": fn_name,  # For debugging purposes
418
            }
419
            if from_address:
4✔
420
                payload["from"] = from_address
×
421
            payloads.append(payload)
4✔
422

423
        return self.batch_call_custom(
4✔
424
            payloads, raise_exception=raise_exception, block_identifier=block_identifier
425
        )
426

427

428
class Erc20Manager(EthereumClientManager):
4✔
429
    """
430
    Manager for ERC20 operations
431
    """
432

433
    # keccak('Transfer(address,address,uint256)')
434
    # ddf252ad1be2c89b69c2b068fc378daa952ba7f163c4a11628f55a4df523b3ef
435
    TRANSFER_TOPIC = HexBytes(ERC20_721_TRANSFER_TOPIC)
4✔
436

437
    def decode_logs(self, logs: Sequence[LogReceipt]):
4✔
438
        decoded_logs = []
4✔
439
        for log in logs:
4✔
440
            decoded = self._decode_transfer_log(log["data"], log["topics"])
4✔
441
            if decoded:
4✔
442
                log_copy = dict(log)
4✔
443
                log_copy["args"] = decoded
4✔
444
                decoded_logs.append(log_copy)
4✔
445
        return decoded_logs
4✔
446

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

513
    def get_balance(
4✔
514
        self, address: ChecksumAddress, token_address: ChecksumAddress
515
    ) -> int:
516
        """
517
        Get balance of address for `erc20_address`
518

519
        :param address: owner address
520
        :param token_address: erc20 token address
521
        :return: balance
522
        """
523
        return (
4✔
524
            get_erc20_contract(self.w3, token_address)
525
            .functions.balanceOf(address)
526
            .call()
527
        )
528

529
    def get_balances(
4✔
530
        self,
531
        address: ChecksumAddress,
532
        token_addresses: Sequence[ChecksumAddress],
533
        include_native_balance: bool = True,
534
    ) -> List[BalanceDict]:
535
        """
536
        Get balances for Ether and tokens for an `address`
537

538
        :param address: Owner address checksummed
539
        :param token_addresses: token addresses to check
540
        :param include_native_balance: if `True` returns also the native token balance
541
        :return: ``List[BalanceDict]``
542
        """
543

544
        balances = self.ethereum_client.batch_call_same_function(
4✔
545
            get_erc20_contract(self.ethereum_client.w3).functions.balanceOf(address),
546
            token_addresses,
547
            raise_exception=False,
548
        )
549

550
        return_balances = [
4✔
551
            BalanceDict(
552
                balance=balance if isinstance(balance, int) else 0,
553
                token_address=token_address,
554
            )
555
            for token_address, balance in zip(token_addresses, balances)
556
        ]
557

558
        if not include_native_balance:
4✔
559
            return return_balances
4✔
560

561
        # Add ether balance response
562
        return [
4✔
563
            BalanceDict(
564
                balance=self.ethereum_client.get_balance(address), token_address=None
565
            )
566
        ] + return_balances
567

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

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

584
    def get_decimals(self, erc20_address: ChecksumAddress) -> int:
4✔
585
        erc20 = get_erc20_contract(self.w3, erc20_address)
4✔
586
        return erc20.functions.decimals().call()
4✔
587

588
    def get_info(self, erc20_address: ChecksumAddress) -> Erc20Info:
4✔
589
        """
590
        Get erc20 information (`name`, `symbol` and `decimals`). Use batching to get
591
        all info in the same request.
592

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

636
    def get_total_transfer_history(
4✔
637
        self,
638
        addresses: Optional[Sequence[ChecksumAddress]] = None,
639
        from_block: BlockIdentifier = BlockNumber(0),
640
        to_block: Optional[BlockIdentifier] = None,
641
        token_address: Optional[ChecksumAddress] = None,
642
    ) -> List[LogReceiptDecoded]:
643
        """
644
        Get events for erc20 and erc721 transfers from and to an `address`. We decode it manually.
645
        Example of an erc20 event:
646

647
        .. code-block:: python
648

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

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

732
        erc20_events = []
4✔
733
        # Do the request to `eth_getLogs`
734
        for topics in all_topics:
4✔
735
            parameters["topics"] = topics
4✔
736

737
            # Decode events. Just pick valid ERC20 Transfer events (ERC721 `Transfer` has the same signature)
738
            for event in self.slow_w3.eth.get_logs(parameters):
4✔
739
                event_args = self._decode_transfer_log(event["data"], event["topics"])
4✔
740
                if event_args:
4✔
741
                    erc20_events.append(LogReceiptDecoded(**event, args=event_args))
4✔
742

743
        erc20_events.sort(key=lambda x: x["blockNumber"])
4✔
744
        return erc20_events
4✔
745

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

759
        .. code-block:: python
760

761
            {
762
                "args": {
763
                    "from": "0x1Ce67Ea59377A163D47DFFc9BaAB99423BE6EcF1",
764
                    "to": "0xaE9E15896fd32E59C7d89ce7a95a9352D6ebD70E",
765
                    "value": 15000000000000000
766
                },
767
                "event": "Transfer",
768
                "logIndex": 42,
769
                "transactionIndex": 60,
770
                "transactionHash": "0x71d6d83fef3347bad848e83dfa0ab28296e2953de946ee152ea81c6dfb42d2b3",
771
                "address": "0xfecA834E7da9D437645b474450688DA9327112a5",
772
                "blockHash": "0x054de9a496fc7d10303068cbc7ee3e25181a3b26640497859a5e49f0342e7db2",
773
                "blockNumber": 7265022
774
            }
775

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

788
        erc20 = get_erc20_contract(self.slow_w3)
4✔
789

790
        argument_filters = {}
4✔
791
        if from_address:
4✔
792
            argument_filters["from"] = from_address
4✔
793
        if to_address:
4✔
794
            argument_filters["to"] = to_address
4✔
795

796
        return erc20.events.Transfer.create_filter(  # type: ignore[attr-defined]
4✔
797
            fromBlock=from_block,
798
            toBlock=to_block,
799
            address=token_address,
800
            argument_filters=argument_filters,
801
        ).get_all_entries()
802

803
    def send_tokens(
4✔
804
        self,
805
        to: str,
806
        amount: int,
807
        erc20_address: ChecksumAddress,
808
        private_key: str,
809
        nonce: Optional[int] = None,
810
        gas_price: Optional[int] = None,
811
        gas: Optional[int] = None,
812
    ) -> bytes:
813
        """
814
        Send tokens to address
815

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

835
        tx = erc20.functions.transfer(to, amount).build_transaction(tx_options)
4✔
836
        return self.ethereum_client.send_unsigned_transaction(
4✔
837
            tx, private_key=private_key
838
        )
839

840

841
class Erc721Manager(EthereumClientManager):
4✔
842
    # keccak('Transfer(address,address,uint256)')
843
    # ddf252ad1be2c89b69c2b068fc378daa952ba7f163c4a11628f55a4df523b3ef
844
    TRANSFER_TOPIC = Erc20Manager.TRANSFER_TOPIC
4✔
845

846
    def get_balance(
4✔
847
        self, address: ChecksumAddress, token_address: ChecksumAddress
848
    ) -> int:
849
        """
850
        Get balance of address for `erc20_address`
851

852
        :param address: owner address
853
        :param token_address: erc721 token address
854
        :return: balance
855
        """
856
        return (
×
857
            get_erc721_contract(self.w3, token_address)
858
            .functions.balanceOf(address)
859
            .call()
860
        )
861

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

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

886
    def get_info(self, token_address: ChecksumAddress) -> Erc721Info:
4✔
887
        """
888
        Get erc721 information (`name`, `symbol`). Use batching to get
889
        all info in the same request.
890

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

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

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

951

952
class TracingManager(EthereumClientManager):
4✔
953
    def filter_out_errored_traces(
4✔
954
        self, internal_txs: Sequence[Dict[str, Any]]
955
    ) -> Sequence[Dict[str, Any]]:
956
        """
957
        Filter out errored transactions (traces that are errored or that have an errored parent)
958

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

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

997
        trace_address = trace_address[:-number_traces]
4✔
998
        traces = reversed(self.trace_transaction(tx_hash))
4✔
999
        for trace in traces:
4✔
1000
            if trace_address == trace["traceAddress"]:
4✔
1001
                if (
4✔
1002
                    skip_delegate_calls
1003
                    and trace["action"].get("callType") == "delegatecall"
1004
                ):
1005
                    trace_address = trace_address[:-1]
4✔
1006
                else:
1007
                    return trace
4✔
1008
        return None
×
1009

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

1043
    def trace_block(self, block_identifier: BlockIdentifier) -> List[BlockTrace]:
4✔
1044
        return self.slow_w3.tracing.trace_block(block_identifier)  # type: ignore[attr-defined]
4✔
1045

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

1065
        results = self.ethereum_client.raw_batch_request(payload)
4✔
1066
        return [trace_list_result_formatter(block_traces) for block_traces in results]  # type: ignore[arg-type]
4✔
1067

1068
    def trace_transaction(self, tx_hash: EthereumHash) -> List[FilterTrace]:
4✔
1069
        """
1070
        :param tx_hash:
1071
        :return: List of internal txs for `tx_hash`
1072
        """
1073
        return self.slow_w3.tracing.trace_transaction(tx_hash)  # type: ignore[attr-defined]
4✔
1074

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

1096
    def trace_filter(
4✔
1097
        self,
1098
        from_block: int = 1,
1099
        to_block: Optional[int] = None,
1100
        from_address: Optional[Sequence[ChecksumAddress]] = None,
1101
        to_address: Optional[Sequence[ChecksumAddress]] = None,
1102
        after: Optional[int] = None,
1103
        count: Optional[int] = None,
1104
    ) -> List[FilterTrace]:
1105
        """
1106
        Get events using ``trace_filter`` method
1107

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

1116
        .. code-block:: python
1117

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

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

1195
        return self.slow_w3.tracing.trace_filter(parameters)  # type: ignore[attr-defined]
4✔
1196

1197

1198
class EthereumClient:
4✔
1199
    """
1200
    Manage ethereum operations. Uses web3 for the most part, but some other stuff is implemented from scratch.
1201
    Note: If you want to use `pending` state with `Parity`, it must be run with `--pruning=archive` or `--force-sealing`
1202
    """
1203

1204
    NULL_ADDRESS = NULL_ADDRESS
4✔
1205

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

1229
        self.w3_provider = HTTPProvider(
4✔
1230
            self.ethereum_node_url,
1231
            request_kwargs={"timeout": provider_timeout},
1232
            session=self.http_session,
1233
        )
1234
        self.w3_slow_provider = HTTPProvider(
4✔
1235
            self.ethereum_node_url,
1236
            request_kwargs={"timeout": slow_provider_timeout},
1237
            session=self.http_session,
1238
        )
1239
        self.w3: Web3 = Web3(self.w3_provider)
4✔
1240
        self.slow_w3: Web3 = Web3(self.w3_slow_provider)
4✔
1241

1242
        # Adjust Web3.py middleware
1243
        for w3 in self.w3, self.slow_w3:
4✔
1244
            # Don't spend resources con converting dictionaries to attribute dictionaries
1245
            w3.middleware_onion.remove("attrdict")
4✔
1246
            # Disable web3 automatic retry, it's handled on our HTTP Session
1247
            w3.provider.middlewares = ()
4✔
1248
            if self.use_caching_middleware:
4✔
1249
                w3.middleware_onion.add(simple_cache_middleware)
4✔
1250

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

1262
        self.erc20: Erc20Manager = Erc20Manager(self)
4✔
1263
        self.erc721: Erc721Manager = Erc721Manager(self)
4✔
1264
        self.tracing: TracingManager = TracingManager(self)
4✔
1265
        self.batch_call_manager: BatchCallManager = BatchCallManager(self)
4✔
1266
        self.batch_request_max_size = batch_request_max_size
4✔
1267

1268
    def __str__(self):
4✔
1269
        return f"EthereumClient for url={self.ethereum_node_url}"
4✔
1270

1271
    def raw_batch_request(
4✔
1272
        self, payload: Sequence[Dict[str, Any]], batch_size: Optional[int] = None
1273
    ) -> Iterable[Union[Optional[Dict[str, Any]], List[Dict[str, Any]]]]:
1274
        """
1275
        Perform a raw batch JSON RPC call
1276

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

1284
        batch_size = batch_size or self.batch_request_max_size
4✔
1285

1286
        all_results = []
4✔
1287
        for chunk in chunks(payload, batch_size):
4✔
1288
            response = self.http_session.post(
4✔
1289
                self.ethereum_node_url, json=chunk, timeout=self.slow_timeout
1290
            )
1291

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

1301
            results = response.json()
4✔
1302

1303
            # If there's an error some nodes return a json instead of a list
1304
            if isinstance(results, dict) and "error" in results:
4✔
1305
                logger.error(
4✔
1306
                    "Batch request problem with payload=%s, result=%s)", chunk, results
1307
                )
1308
                raise ValueError(f"Batch request error: {results}")
4✔
1309

1310
            all_results.extend(results)
4✔
1311

1312
        # Nodes like Erigon send back results out of order
1313
        for query, result in zip(payload, sorted(all_results, key=lambda x: x["id"])):
4✔
1314
            if "result" not in result:
4✔
1315
                message = f"Problem with payload=`{query}` result={result}"
4✔
1316
                logger.error(message)
4✔
1317
                raise ValueError(message)
4✔
1318

1319
            yield result["result"]
4✔
1320

1321
    @property
4✔
1322
    def current_block_number(self):
4✔
1323
        return self.w3.eth.block_number
4✔
1324

1325
    @cache
4✔
1326
    def get_chain_id(self) -> int:
4✔
1327
        """
1328
        :return: ChainId returned by the RPC `eth_chainId` method. It should never change, so it's cached.
1329
        """
1330
        return int(self.w3.eth.chain_id)
4✔
1331

1332
    @cache
4✔
1333
    def get_client_version(self) -> str:
4✔
1334
        """
1335
        :return: RPC version information
1336
        """
1337
        return self.w3.client_version
×
1338

1339
    def get_network(self) -> EthereumNetwork:
4✔
1340
        """
1341
        Get network name based on the chainId. This method is not cached as the method for getting the
1342
        `chainId` already is.
1343

1344
        :return: EthereumNetwork based on the chainId. If network is not
1345
            on our list, `EthereumNetwork.UNKNOWN` is returned
1346
        """
1347
        return EthereumNetwork(self.get_chain_id())
4✔
1348

1349
    @cache
4✔
1350
    def get_singleton_factory_address(self) -> Optional[ChecksumAddress]:
4✔
1351
        """
1352
        Get singleton factory address if available. Try the singleton managed by Safe by default unless
1353
        SAFE_SINGLETON_FACTORY_ADDRESS environment variable is defined.
1354

1355
        More info: https://github.com/safe-global/safe-singleton-factory
1356

1357
        :return: Get singleton factory address if available
1358
        """
1359
        address = os.environ.get(
4✔
1360
            "SAFE_SINGLETON_FACTORY_ADDRESS", SAFE_SINGLETON_FACTORY_ADDRESS
1361
        )
1362
        address_checksum = ChecksumAddress(HexAddress(HexStr(address)))
4✔
1363
        if self.is_contract(address_checksum):
4✔
1364
            return address_checksum
4✔
1365
        return None
4✔
1366

1367
    @cache
4✔
1368
    def is_eip1559_supported(self) -> bool:
4✔
1369
        """
1370
        :return: `True` if EIP1559 is supported by the node, `False` otherwise
1371
        """
1372
        try:
4✔
1373
            self.w3.eth.fee_history(1, "latest", reward_percentiles=[50])
4✔
1374
            return True
4✔
1375
        except (Web3Exception, ValueError):
×
1376
            return False
×
1377

1378
    @cached_property
4✔
1379
    def multicall(self) -> "Multicall":  # type: ignore # noqa F821
4✔
1380
        from .multicall import Multicall
4✔
1381

1382
        try:
4✔
1383
            return Multicall(self)
4✔
1384
        except EthereumNetworkNotSupported:
×
1385
            logger.warning("Multicall not supported for this network")
×
1386
            return None
×
1387

1388
    def batch_call(
4✔
1389
        self,
1390
        contract_functions: Iterable[ContractFunction],
1391
        from_address: Optional[ChecksumAddress] = None,
1392
        raise_exception: bool = True,
1393
        force_batch_call: bool = False,
1394
        block_identifier: Optional[BlockIdentifier] = "latest",
1395
    ) -> List[Optional[Union[bytes, Any]]]:
1396
        """
1397
        Call multiple functions. ``Multicall`` contract by MakerDAO will be used by default if available
1398

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

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

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

1469
    def deploy_and_initialize_contract(
4✔
1470
        self,
1471
        deployer_account: LocalAccount,
1472
        constructor_data: Union[bytes, HexStr],
1473
        initializer_data: Optional[Union[bytes, HexStr]] = None,
1474
        check_receipt: bool = True,
1475
        deterministic: bool = True,
1476
    ) -> EthereumTxSent:
1477
        """
1478

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

1520
                tx["gas"] = self.w3.eth.estimate_gas(tx)
4✔
1521
                tx_hash = self.send_unsigned_transaction(
4✔
1522
                    tx, private_key=deployer_account.key
1523
                )
1524
                if check_receipt:
4✔
1525
                    tx_receipt = self.get_transaction_receipt(
4✔
1526
                        Hash32(tx_hash), timeout=60
1527
                    )
1528
                    assert tx_receipt
4✔
1529
                    assert tx_receipt["status"]
4✔
1530

1531
        return EthereumTxSent(tx_hash, tx, contract_address)
4✔
1532

1533
    def get_nonce_for_account(
4✔
1534
        self,
1535
        address: ChecksumAddress,
1536
        block_identifier: Optional[BlockIdentifier] = "latest",
1537
    ):
1538
        """
1539
        Get nonce for account. `getTransactionCount` is the only method for what `pending` is currently working
1540
        (Geth and Parity)
1541

1542
        :param address:
1543
        :param block_identifier:
1544
        :return:
1545
        """
1546
        return self.w3.eth.get_transaction_count(
4✔
1547
            address, block_identifier=block_identifier
1548
        )
1549

1550
    def estimate_gas(
4✔
1551
        self,
1552
        to: str,
1553
        from_: Optional[str] = None,
1554
        value: Optional[int] = None,
1555
        data: Optional[EthereumData] = None,
1556
        gas: Optional[int] = None,
1557
        gas_price: Optional[int] = None,
1558
        block_identifier: Optional[BlockIdentifier] = None,
1559
    ) -> int:
1560
        """
1561
        Estimate gas calling `eth_estimateGas`
1562

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

1594
    @staticmethod
4✔
1595
    def estimate_data_gas(data: bytes):
4✔
1596
        """
1597
        Estimate gas costs only for "storage" of the ``data`` bytes provided
1598

1599
        :param data:
1600
        :return:
1601
        """
1602
        if isinstance(data, str):
4✔
1603
            data = HexBytes(data)
×
1604

1605
        gas = 0
4✔
1606
        for byte in data:
4✔
1607
            if not byte:
4✔
1608
                gas += GAS_CALL_DATA_ZERO_BYTE
4✔
1609
            else:
1610
                gas += GAS_CALL_DATA_BYTE
4✔
1611
        return gas
4✔
1612

1613
    def estimate_fee_eip1559(
4✔
1614
        self, tx_speed: TxSpeed = TxSpeed.NORMAL
1615
    ) -> Tuple[int, int]:
1616
        """
1617
        Check https://github.com/ethereum/execution-apis/blob/main/src/eth/fee_market.json#L15
1618

1619
        :return: Tuple[BaseFeePerGas, MaxPriorityFeePerGas]
1620
        :raises: ValueError if not supported on the network
1621
        """
1622
        if tx_speed == TxSpeed.SLOWEST:
4✔
1623
            percentile = 0
×
1624
        elif tx_speed == TxSpeed.VERY_SLOW:
4✔
1625
            percentile = 10
×
1626
        elif tx_speed == TxSpeed.SLOW:
4✔
1627
            percentile = 25
×
1628
        elif tx_speed == TxSpeed.NORMAL:
4✔
1629
            percentile = 50
4✔
1630
        elif tx_speed == TxSpeed.FAST:
×
1631
            percentile = 75
×
1632
        elif tx_speed == TxSpeed.VERY_FAST:
×
1633
            percentile = 90
×
1634
        elif tx_speed == TxSpeed.FASTEST:
×
1635
            percentile = 100
×
1636

1637
        result = self.w3.eth.fee_history(1, "latest", reward_percentiles=[percentile])
4✔
1638
        # Get next block `base_fee_per_gas`
1639
        base_fee_per_gas = result["baseFeePerGas"][-1]
4✔
1640
        max_priority_fee_per_gas = result["reward"][0][0]
4✔
1641
        return base_fee_per_gas, max_priority_fee_per_gas
4✔
1642

1643
    def set_eip1559_fees(
4✔
1644
        self, tx: TxParams, tx_speed: TxSpeed = TxSpeed.NORMAL
1645
    ) -> TxParams:
1646
        """
1647
        :return: TxParams in EIP1559 format
1648
        :raises: ValueError if EIP1559 not supported
1649
        """
1650
        base_fee_per_gas, max_priority_fee_per_gas = self.estimate_fee_eip1559(tx_speed)
4✔
1651
        tx = TxParams(**tx)  # Don't modify provided tx
4✔
1652
        if "gasPrice" in tx:
4✔
1653
            del tx["gasPrice"]
4✔
1654

1655
        if "chainId" not in tx:
4✔
1656
            tx["chainId"] = self.get_chain_id()
4✔
1657

1658
        tx["maxPriorityFeePerGas"] = Wei(max_priority_fee_per_gas)
4✔
1659
        tx["maxFeePerGas"] = Wei(base_fee_per_gas + max_priority_fee_per_gas)
4✔
1660
        return tx
4✔
1661

1662
    def get_balance(
4✔
1663
        self,
1664
        address: ChecksumAddress,
1665
        block_identifier: Optional[BlockIdentifier] = None,
1666
    ):
1667
        return self.w3.eth.get_balance(address, block_identifier)
4✔
1668

1669
    def get_transaction(self, tx_hash: EthereumHash) -> Optional[TxData]:
4✔
1670
        try:
4✔
1671
            return self.w3.eth.get_transaction(tx_hash)
4✔
1672
        except TransactionNotFound:
×
1673
            return None
×
1674

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

1695
    def get_transaction_receipt(
4✔
1696
        self, tx_hash: EthereumHash, timeout=None
1697
    ) -> Optional[TxReceipt]:
1698
        try:
4✔
1699
            if not timeout:
4✔
1700
                tx_receipt = self.w3.eth.get_transaction_receipt(tx_hash)
4✔
1701
            else:
1702
                try:
4✔
1703
                    tx_receipt = self.w3.eth.wait_for_transaction_receipt(
4✔
1704
                        tx_hash, timeout=timeout
1705
                    )
1706
                except TimeExhausted:
4✔
1707
                    return None
4✔
1708

1709
            # Parity returns tx_receipt even is tx is still pending, so we check `blockNumber` is not None
1710
            return (
4✔
1711
                tx_receipt
1712
                if tx_receipt and tx_receipt["blockNumber"] is not None
1713
                else None
1714
            )
1715
        except TransactionNotFound:
4✔
1716
            return None
4✔
1717

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

1746
    def get_block(
4✔
1747
        self, block_identifier: BlockIdentifier, full_transactions: bool = False
1748
    ) -> Optional[BlockData]:
1749
        try:
4✔
1750
            return self.w3.eth.get_block(
4✔
1751
                block_identifier, full_transactions=full_transactions
1752
            )
1753
        except BlockNotFound:
×
1754
            return None
×
1755

1756
    def _parse_block_identifier(self, block_identifier: BlockIdentifier) -> str:
4✔
1757
        if isinstance(block_identifier, int):
4✔
1758
            return hex(block_identifier)
4✔
1759
        elif isinstance(block_identifier, bytes):
4✔
1760
            return HexBytes(block_identifier).hex()
4✔
1761
        else:
1762
            return block_identifier
4✔
1763

1764
    def get_blocks(
4✔
1765
        self,
1766
        block_identifiers: Iterable[BlockIdentifier],
1767
        full_transactions: bool = False,
1768
    ) -> List[Optional[BlockData]]:
1769
        if not block_identifiers:
4✔
1770
            return []
4✔
1771
        payload = [
4✔
1772
            {
1773
                "id": i,
1774
                "jsonrpc": "2.0",
1775
                "method": "eth_getBlockByNumber"
1776
                if isinstance(block_identifier, int)
1777
                else "eth_getBlockByHash",
1778
                "params": [
1779
                    self._parse_block_identifier(block_identifier),
1780
                    full_transactions,
1781
                ],
1782
            }
1783
            for i, block_identifier in enumerate(block_identifiers)
1784
        ]
1785
        results = self.raw_batch_request(payload)
4✔
1786
        blocks = []
4✔
1787
        for raw_block in results:
4✔
1788
            if raw_block and isinstance(raw_block, dict):
4✔
1789
                if "extraData" in raw_block:
4✔
1790
                    del raw_block[
4✔
1791
                        "extraData"
1792
                    ]  # Remove extraData, raises some problems on parsing
1793
                blocks.append(block_formatter(raw_block))
4✔
1794
            else:
1795
                blocks.append(None)
×
1796
        return blocks
4✔
1797

1798
    def is_contract(self, contract_address: ChecksumAddress) -> bool:
4✔
1799
        return bool(self.w3.eth.get_code(contract_address))
4✔
1800

1801
    @staticmethod
4✔
1802
    def build_tx_params(
4✔
1803
        from_address: Optional[ChecksumAddress] = None,
1804
        to_address: Optional[ChecksumAddress] = None,
1805
        value: Optional[int] = None,
1806
        gas: Optional[int] = None,
1807
        gas_price: Optional[int] = None,
1808
        nonce: Optional[int] = None,
1809
        chain_id: Optional[int] = None,
1810
        tx_params: Optional[TxParams] = None,
1811
    ) -> TxParams:
1812
        """
1813
        Build tx params dictionary.
1814
        If an existing TxParams dictionary is provided the fields will be replaced by the provided ones
1815

1816
        :param from_address:
1817
        :param to_address:
1818
        :param value:
1819
        :param gas:
1820
        :param gas_price:
1821
        :param nonce:
1822
        :param chain_id:
1823
        :param tx_params: An existing TxParams dictionary will be replaced by the providen values
1824
        :return:
1825
        """
1826

1827
        new_tx_params: TxParams = tx_params if tx_params is not None else {}
4✔
1828

1829
        if from_address:
4✔
1830
            new_tx_params["from"] = from_address
4✔
1831

1832
        if to_address:
4✔
1833
            new_tx_params["to"] = to_address
×
1834

1835
        if value is not None:
4✔
1836
            new_tx_params["value"] = Wei(value)
×
1837

1838
        if gas_price is not None:
4✔
1839
            new_tx_params["gasPrice"] = Wei(gas_price)
4✔
1840

1841
        if gas is not None:
4✔
1842
            new_tx_params["gas"] = gas
4✔
1843

1844
        if nonce is not None:
4✔
1845
            new_tx_params["nonce"] = Nonce(nonce)
×
1846

1847
        if chain_id is not None:
4✔
1848
            new_tx_params["chainId"] = chain_id
×
1849

1850
        return new_tx_params
4✔
1851

1852
    @tx_with_exception_handling
4✔
1853
    def send_transaction(self, transaction_dict: TxParams) -> HexBytes:
4✔
1854
        return self.w3.eth.send_transaction(transaction_dict)
4✔
1855

1856
    @tx_with_exception_handling
4✔
1857
    def send_raw_transaction(self, raw_transaction: EthereumData) -> HexBytes:
4✔
1858
        if isinstance(raw_transaction, bytes):
4✔
1859
            value_bytes = raw_transaction
4✔
1860
        else:
1861
            value_bytes = bytes.fromhex(
×
1862
                raw_transaction.replace("0x", "")
1863
            )  # Remove '0x' and convert
1864
        return self.w3.eth.send_raw_transaction(value_bytes)
4✔
1865

1866
    def send_unsigned_transaction(
4✔
1867
        self,
1868
        tx: TxParams,
1869
        private_key: Optional[str] = None,
1870
        public_key: Optional[str] = None,
1871
        retry: bool = False,
1872
        block_identifier: Optional[BlockIdentifier] = "pending",
1873
    ) -> HexBytes:
1874
        """
1875
        Send a tx using an unlocked public key in the node or a private key. Both `public_key` and
1876
        `private_key` cannot be `None`
1877

1878
        :param tx:
1879
        :param private_key:
1880
        :param public_key:
1881
        :param retry: Retry if a problem with nonce is found
1882
        :param block_identifier: For nonce calculation, recommended is `pending`
1883
        :return: tx hash
1884
        """
1885

1886
        # TODO Refactor this method, it's not working well with new version of the nodes
1887
        if private_key:
4✔
1888
            address = Account.from_key(private_key).address
4✔
1889
        elif public_key:
4✔
1890
            address = public_key
4✔
1891
        else:
1892
            logger.error(
4✔
1893
                "No ethereum account provided. Need a public_key or private_key"
1894
            )
1895
            raise ValueError(
4✔
1896
                "Ethereum account was not configured or unlocked in the node"
1897
            )
1898

1899
        if tx.get("nonce") is None:
4✔
1900
            tx["nonce"] = self.get_nonce_for_account(
4✔
1901
                address, block_identifier=block_identifier
1902
            )
1903

1904
        number_errors = 5
4✔
1905
        while number_errors >= 0:
4✔
1906
            try:
4✔
1907
                if private_key:
4✔
1908
                    signed_tx = self.w3.eth.account.sign_transaction(
4✔
1909
                        tx, private_key=private_key
1910
                    )
1911
                    logger.debug(
4✔
1912
                        "Sending %d wei from %s to %s", tx["value"], address, tx["to"]
1913
                    )
1914
                    try:
4✔
1915
                        return self.send_raw_transaction(signed_tx.rawTransaction)
4✔
1916
                    except TransactionAlreadyImported as e:
4✔
1917
                        # Sometimes Parity 2.2.11 fails with Transaction already imported, even if it's not, but it's
1918
                        # processed
1919
                        tx_hash = signed_tx.hash
×
1920
                        logger.error(
×
1921
                            "Transaction with tx-hash=%s already imported: %s"
1922
                            % (tx_hash.hex(), str(e))
1923
                        )
1924
                        return tx_hash
×
1925
                elif public_key:
4✔
1926
                    tx["from"] = address
4✔
1927
                    return self.send_transaction(tx)
4✔
1928
            except ReplacementTransactionUnderpriced as e:
4✔
1929
                if not retry or not number_errors:
×
1930
                    raise e
×
1931
                current_nonce = tx["nonce"]
×
1932
                tx["nonce"] = max(
×
1933
                    current_nonce + 1,
1934
                    self.get_nonce_for_account(
1935
                        address, block_identifier=block_identifier
1936
                    ),
1937
                )
1938
                logger.error(
×
1939
                    "Tx with nonce=%d was already sent for address=%s, retrying with nonce=%s",
1940
                    current_nonce,
1941
                    address,
1942
                    tx["nonce"],
1943
                )
1944
            except InvalidNonce as e:
4✔
1945
                if not retry or not number_errors:
4✔
1946
                    raise e
4✔
1947
                logger.error(
4✔
1948
                    "address=%s Tx with invalid nonce=%d, retrying recovering nonce again",
1949
                    address,
1950
                    tx["nonce"],
1951
                )
1952
                tx["nonce"] = self.get_nonce_for_account(
4✔
1953
                    address, block_identifier=block_identifier
1954
                )
1955
                number_errors -= 1
4✔
1956
        return HexBytes("")
×
1957

1958
    def send_eth_to(
4✔
1959
        self,
1960
        private_key: str,
1961
        to: str,
1962
        gas_price: int,
1963
        value: Wei,
1964
        gas: Optional[int] = None,
1965
        nonce: Optional[int] = None,
1966
        retry: bool = False,
1967
        block_identifier: Optional[BlockIdentifier] = "pending",
1968
    ) -> bytes:
1969
        """
1970
        Send ether using configured account
1971

1972
        :param private_key: to
1973
        :param to: to
1974
        :param gas_price: gas_price
1975
        :param value: value(wei)
1976
        :param gas: gas, defaults to 22000
1977
        :param retry: Retry if a problem is found
1978
        :param nonce: Nonce of sender account
1979
        :param block_identifier: Block identifier for nonce calculation
1980
        :return: tx_hash
1981
        """
1982

1983
        assert fast_is_checksum_address(to)
4✔
1984

1985
        account = Account.from_key(private_key)
4✔
1986

1987
        tx: TxParams = {
4✔
1988
            "from": account.address,
1989
            "to": to,
1990
            "value": value,
1991
            "gas": gas or Wei(self.estimate_gas(to, account.address, value)),
1992
            "gasPrice": Wei(gas_price),
1993
            "chainId": self.get_chain_id(),
1994
        }
1995

1996
        if nonce is not None:
4✔
1997
            tx["nonce"] = Nonce(nonce)
×
1998

1999
        return self.send_unsigned_transaction(
4✔
2000
            tx, private_key=private_key, retry=retry, block_identifier=block_identifier
2001
        )
2002

2003
    def check_tx_with_confirmations(
4✔
2004
        self, tx_hash: EthereumHash, confirmations: int
2005
    ) -> bool:
2006
        """
2007
        Check tx hash and make sure it has the confirmations required
2008

2009
        :param tx_hash: Hash of the tx
2010
        :param confirmations: Minimum number of confirmations required
2011
        :return: True if tx was mined with the number of confirmations required, False otherwise
2012
        """
2013
        tx_receipt = self.get_transaction_receipt(tx_hash)
4✔
2014
        if not tx_receipt or tx_receipt["blockNumber"] is None:
4✔
2015
            # If `tx_receipt` exists but `blockNumber` is `None`, tx is still pending (just Parity)
2016
            return False
×
2017
        else:
2018
            return (
4✔
2019
                self.w3.eth.block_number - tx_receipt["blockNumber"]
2020
            ) >= confirmations
2021

2022
    @staticmethod
4✔
2023
    def private_key_to_address(private_key):
4✔
2024
        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