brawny 0.1.13__py3-none-any.whl → 0.1.22__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- brawny/__init__.py +2 -0
- brawny/_context.py +5 -5
- brawny/_rpc/__init__.py +36 -12
- brawny/_rpc/broadcast.py +14 -13
- brawny/_rpc/caller.py +243 -0
- brawny/_rpc/client.py +539 -0
- brawny/_rpc/clients.py +11 -11
- brawny/_rpc/context.py +23 -0
- brawny/_rpc/errors.py +465 -31
- brawny/_rpc/gas.py +7 -6
- brawny/_rpc/pool.py +18 -0
- brawny/_rpc/retry.py +266 -0
- brawny/_rpc/retry_policy.py +81 -0
- brawny/accounts.py +28 -9
- brawny/alerts/__init__.py +15 -18
- brawny/alerts/abi_resolver.py +212 -36
- brawny/alerts/base.py +2 -2
- brawny/alerts/contracts.py +77 -10
- brawny/alerts/errors.py +30 -3
- brawny/alerts/events.py +38 -5
- brawny/alerts/health.py +19 -13
- brawny/alerts/send.py +513 -55
- brawny/api.py +39 -11
- brawny/assets/AGENTS.md +325 -0
- brawny/async_runtime.py +48 -0
- brawny/chain.py +3 -3
- brawny/cli/commands/__init__.py +2 -0
- brawny/cli/commands/console.py +69 -19
- brawny/cli/commands/contract.py +2 -2
- brawny/cli/commands/controls.py +121 -0
- brawny/cli/commands/health.py +2 -2
- brawny/cli/commands/job_dev.py +6 -5
- brawny/cli/commands/jobs.py +99 -2
- brawny/cli/commands/maintenance.py +13 -29
- brawny/cli/commands/migrate.py +1 -0
- brawny/cli/commands/run.py +10 -3
- brawny/cli/commands/script.py +8 -3
- brawny/cli/commands/signer.py +143 -26
- brawny/cli/helpers.py +0 -3
- brawny/cli_templates.py +25 -349
- brawny/config/__init__.py +4 -1
- brawny/config/models.py +43 -57
- brawny/config/parser.py +268 -57
- brawny/config/validation.py +52 -15
- brawny/daemon/context.py +4 -2
- brawny/daemon/core.py +185 -63
- brawny/daemon/loops.py +166 -98
- brawny/daemon/supervisor.py +261 -0
- brawny/db/__init__.py +14 -26
- brawny/db/base.py +248 -151
- brawny/db/global_cache.py +11 -1
- brawny/db/migrate.py +175 -28
- brawny/db/migrations/001_init.sql +4 -3
- brawny/db/migrations/010_add_nonce_gap_index.sql +1 -1
- brawny/db/migrations/011_add_job_logs.sql +1 -2
- brawny/db/migrations/012_add_claimed_by.sql +2 -2
- brawny/db/migrations/013_attempt_unique.sql +10 -0
- brawny/db/migrations/014_add_lease_expires_at.sql +5 -0
- brawny/db/migrations/015_add_signer_alias.sql +14 -0
- brawny/db/migrations/016_runtime_controls_and_quarantine.sql +32 -0
- brawny/db/migrations/017_add_job_drain.sql +6 -0
- brawny/db/migrations/018_add_nonce_reset_audit.sql +20 -0
- brawny/db/migrations/019_add_job_cooldowns.sql +8 -0
- brawny/db/migrations/020_attempt_unique_initial.sql +7 -0
- brawny/db/ops/__init__.py +3 -25
- brawny/db/ops/logs.py +1 -2
- brawny/db/queries.py +47 -91
- brawny/db/serialized.py +65 -0
- brawny/db/sqlite/__init__.py +1001 -0
- brawny/db/sqlite/connection.py +231 -0
- brawny/db/sqlite/execute.py +116 -0
- brawny/db/sqlite/mappers.py +190 -0
- brawny/db/sqlite/repos/attempts.py +372 -0
- brawny/db/sqlite/repos/block_state.py +102 -0
- brawny/db/sqlite/repos/cache.py +104 -0
- brawny/db/sqlite/repos/intents.py +1021 -0
- brawny/db/sqlite/repos/jobs.py +200 -0
- brawny/db/sqlite/repos/maintenance.py +182 -0
- brawny/db/sqlite/repos/signers_nonces.py +566 -0
- brawny/db/sqlite/tx.py +119 -0
- brawny/http.py +194 -0
- brawny/invariants.py +11 -24
- brawny/jobs/base.py +8 -0
- brawny/jobs/job_validation.py +2 -1
- brawny/keystore.py +83 -7
- brawny/lifecycle.py +64 -12
- brawny/logging.py +0 -2
- brawny/metrics.py +84 -12
- brawny/model/contexts.py +111 -9
- brawny/model/enums.py +1 -0
- brawny/model/errors.py +18 -0
- brawny/model/types.py +47 -131
- brawny/network_guard.py +133 -0
- brawny/networks/__init__.py +5 -5
- brawny/networks/config.py +1 -7
- brawny/networks/manager.py +14 -11
- brawny/runtime_controls.py +74 -0
- brawny/scheduler/poller.py +11 -7
- brawny/scheduler/reorg.py +95 -39
- brawny/scheduler/runner.py +442 -168
- brawny/scheduler/shutdown.py +3 -3
- brawny/script_tx.py +3 -3
- brawny/telegram.py +53 -7
- brawny/testing.py +1 -0
- brawny/timeout.py +38 -0
- brawny/tx/executor.py +922 -308
- brawny/tx/intent.py +54 -16
- brawny/tx/monitor.py +31 -12
- brawny/tx/nonce.py +212 -90
- brawny/tx/replacement.py +69 -18
- brawny/tx/retry_policy.py +24 -0
- brawny/tx/stages/types.py +75 -0
- brawny/types.py +18 -0
- brawny/utils.py +41 -0
- {brawny-0.1.13.dist-info → brawny-0.1.22.dist-info}/METADATA +3 -3
- brawny-0.1.22.dist-info/RECORD +163 -0
- brawny/_rpc/manager.py +0 -982
- brawny/_rpc/selector.py +0 -156
- brawny/db/base_new.py +0 -165
- brawny/db/mappers.py +0 -182
- brawny/db/migrations/008_add_transactions.sql +0 -72
- brawny/db/ops/attempts.py +0 -108
- brawny/db/ops/blocks.py +0 -83
- brawny/db/ops/cache.py +0 -93
- brawny/db/ops/intents.py +0 -296
- brawny/db/ops/jobs.py +0 -110
- brawny/db/ops/nonces.py +0 -322
- brawny/db/postgres.py +0 -2535
- brawny/db/postgres_new.py +0 -196
- brawny/db/sqlite.py +0 -2733
- brawny/db/sqlite_new.py +0 -191
- brawny-0.1.13.dist-info/RECORD +0 -141
- {brawny-0.1.13.dist-info → brawny-0.1.22.dist-info}/WHEEL +0 -0
- {brawny-0.1.13.dist-info → brawny-0.1.22.dist-info}/entry_points.txt +0 -0
- {brawny-0.1.13.dist-info → brawny-0.1.22.dist-info}/top_level.txt +0 -0
brawny/__init__.py
CHANGED
|
@@ -39,6 +39,7 @@ from brawny.api import (
|
|
|
39
39
|
kv, # Persistent KV store
|
|
40
40
|
alert, # Send alerts from job hooks
|
|
41
41
|
rpc, # RPC proxy (internal package renamed to _rpc to avoid collision)
|
|
42
|
+
http, # Approved HTTP client proxy
|
|
42
43
|
get_address_from_alias,
|
|
43
44
|
Contract, # Brownie-style
|
|
44
45
|
Wei, # Brownie-style
|
|
@@ -85,6 +86,7 @@ __all__ = [
|
|
|
85
86
|
"kv",
|
|
86
87
|
"alert",
|
|
87
88
|
"rpc",
|
|
89
|
+
"http",
|
|
88
90
|
"get_address_from_alias",
|
|
89
91
|
# Brownie-style helpers
|
|
90
92
|
"Contract",
|
brawny/_context.py
CHANGED
|
@@ -12,12 +12,12 @@ Usage (in job methods):
|
|
|
12
12
|
vault = Contract("vault") # Works because context is set
|
|
13
13
|
return trigger(reason="...", tx_required=True)
|
|
14
14
|
|
|
15
|
-
Usage (in
|
|
15
|
+
Usage (in lifecycle hooks):
|
|
16
16
|
from brawny import Contract, shorten, explorer_link
|
|
17
17
|
|
|
18
|
-
def
|
|
18
|
+
def on_success(self, ctx):
|
|
19
19
|
vault = Contract("vault")
|
|
20
|
-
|
|
20
|
+
ctx.alert(f"Done!\\n{explorer_link(ctx.receipt.transaction_hash)}")
|
|
21
21
|
|
|
22
22
|
Usage (in console):
|
|
23
23
|
>>> claimer = interface.IClaimer("0x...") # Works via console context
|
|
@@ -33,7 +33,7 @@ from typing import TYPE_CHECKING, Any, Union
|
|
|
33
33
|
if TYPE_CHECKING:
|
|
34
34
|
from brawny.model.contexts import CheckContext, BuildContext, AlertContext
|
|
35
35
|
from brawny.jobs.base import Job
|
|
36
|
-
from brawny._rpc import
|
|
36
|
+
from brawny._rpc.clients import BroadcastClient
|
|
37
37
|
from brawny.alerts.contracts import ContractSystem
|
|
38
38
|
|
|
39
39
|
# Type alias for any phase context
|
|
@@ -48,7 +48,7 @@ class ActiveContext:
|
|
|
48
48
|
necessary dependencies without requiring full job/alert context.
|
|
49
49
|
"""
|
|
50
50
|
|
|
51
|
-
rpc:
|
|
51
|
+
rpc: BroadcastClient
|
|
52
52
|
contract_system: ContractSystem
|
|
53
53
|
chain_id: int
|
|
54
54
|
network_name: str | None = None
|
brawny/_rpc/__init__.py
CHANGED
|
@@ -1,9 +1,11 @@
|
|
|
1
|
-
"""RPC
|
|
1
|
+
"""RPC core built from small pieces.
|
|
2
2
|
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
3
|
+
EndpointPool orders endpoints deterministically.
|
|
4
|
+
Caller executes a single call against one endpoint.
|
|
5
|
+
call_with_retries composes pool + caller + RetryPolicy.
|
|
6
|
+
ReadClient and BroadcastClient call the retry helper directly.
|
|
7
|
+
Logs emit one rpc.attempt event per attempt with fixed fields.
|
|
8
|
+
request_id is injected for tests; production can use a UUID.
|
|
7
9
|
"""
|
|
8
10
|
|
|
9
11
|
from brawny._rpc.errors import (
|
|
@@ -11,28 +13,50 @@ from brawny._rpc.errors import (
|
|
|
11
13
|
RPCFatalError,
|
|
12
14
|
RPCRecoverableError,
|
|
13
15
|
RPCRetryableError,
|
|
16
|
+
RPCTransient,
|
|
17
|
+
RPCRateLimited,
|
|
18
|
+
RPCPermanent,
|
|
19
|
+
RPCDecode,
|
|
20
|
+
RpcErrorKind,
|
|
21
|
+
RpcErrorInfo,
|
|
22
|
+
classify_rpc_error,
|
|
14
23
|
classify_error,
|
|
15
|
-
normalize_error_code,
|
|
16
24
|
)
|
|
17
|
-
from brawny._rpc.
|
|
18
|
-
from brawny._rpc.
|
|
25
|
+
from brawny._rpc.clients import ReadClient, BroadcastClient
|
|
26
|
+
from brawny._rpc.pool import EndpointPool
|
|
27
|
+
from brawny._rpc.caller import Caller
|
|
28
|
+
from brawny._rpc.retry import call_with_retries
|
|
19
29
|
from brawny._rpc.context import (
|
|
20
30
|
get_job_context,
|
|
31
|
+
get_intent_budget_context,
|
|
21
32
|
reset_job_context,
|
|
33
|
+
reset_intent_budget_context,
|
|
22
34
|
set_job_context,
|
|
35
|
+
set_intent_budget_context,
|
|
23
36
|
)
|
|
24
37
|
|
|
25
38
|
__all__ = [
|
|
26
|
-
"
|
|
27
|
-
"
|
|
28
|
-
"
|
|
39
|
+
"ReadClient",
|
|
40
|
+
"BroadcastClient",
|
|
41
|
+
"EndpointPool",
|
|
42
|
+
"Caller",
|
|
43
|
+
"call_with_retries",
|
|
29
44
|
"RPCError",
|
|
30
45
|
"RPCFatalError",
|
|
31
46
|
"RPCRecoverableError",
|
|
32
47
|
"RPCRetryableError",
|
|
48
|
+
"RPCTransient",
|
|
49
|
+
"RPCRateLimited",
|
|
50
|
+
"RPCPermanent",
|
|
51
|
+
"RPCDecode",
|
|
52
|
+
"RpcErrorKind",
|
|
53
|
+
"RpcErrorInfo",
|
|
54
|
+
"classify_rpc_error",
|
|
33
55
|
"classify_error",
|
|
34
|
-
"normalize_error_code",
|
|
35
56
|
"get_job_context",
|
|
57
|
+
"get_intent_budget_context",
|
|
36
58
|
"reset_job_context",
|
|
59
|
+
"reset_intent_budget_context",
|
|
37
60
|
"set_job_context",
|
|
61
|
+
"set_intent_budget_context",
|
|
38
62
|
]
|
brawny/_rpc/broadcast.py
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
"""Broadcast helpers with isolation guarantees.
|
|
2
2
|
|
|
3
3
|
This is the ONLY place that wraps RPCPoolExhaustedError → RPCGroupUnavailableError.
|
|
4
|
-
|
|
4
|
+
BroadcastClient does the endpoint iteration; this module adds group context.
|
|
5
5
|
"""
|
|
6
6
|
|
|
7
7
|
from __future__ import annotations
|
|
@@ -20,16 +20,17 @@ from brawny._rpc.errors import (
|
|
|
20
20
|
RPCGroupUnavailableError,
|
|
21
21
|
RPCRecoverableError,
|
|
22
22
|
)
|
|
23
|
+
from brawny.timeout import Deadline
|
|
23
24
|
|
|
24
25
|
if TYPE_CHECKING:
|
|
25
26
|
from brawny.config import Config
|
|
26
|
-
from brawny._rpc.
|
|
27
|
+
from brawny._rpc.clients import BroadcastClient
|
|
27
28
|
|
|
28
29
|
|
|
29
|
-
def create_broadcast_manager(endpoints: list[str], config: "Config") -> "
|
|
30
|
-
"""Create
|
|
30
|
+
def create_broadcast_manager(endpoints: list[str], config: "Config") -> "BroadcastClient":
|
|
31
|
+
"""Create a BroadcastClient for broadcasting to specific endpoints.
|
|
31
32
|
|
|
32
|
-
This creates a dedicated
|
|
33
|
+
This creates a dedicated BroadcastClient instance for broadcasting.
|
|
33
34
|
Each call uses the provided endpoints (from binding snapshot for retries,
|
|
34
35
|
or from current config for first broadcast).
|
|
35
36
|
|
|
@@ -38,18 +39,17 @@ def create_broadcast_manager(endpoints: list[str], config: "Config") -> "RPCMana
|
|
|
38
39
|
config: Config for RPC settings
|
|
39
40
|
|
|
40
41
|
Returns:
|
|
41
|
-
|
|
42
|
+
BroadcastClient configured for the provided endpoints
|
|
42
43
|
"""
|
|
43
|
-
from brawny._rpc.
|
|
44
|
+
from brawny._rpc.retry_policy import broadcast_policy
|
|
45
|
+
from brawny._rpc.clients import BroadcastClient
|
|
44
46
|
|
|
45
|
-
return
|
|
47
|
+
return BroadcastClient(
|
|
46
48
|
endpoints=endpoints,
|
|
47
49
|
timeout_seconds=config.rpc_timeout_seconds,
|
|
48
50
|
max_retries=config.rpc_max_retries,
|
|
49
51
|
retry_backoff_base=config.rpc_retry_backoff_base,
|
|
50
|
-
|
|
51
|
-
rate_limit_per_second=config.rpc_rate_limit_per_second,
|
|
52
|
-
rate_limit_burst=config.rpc_rate_limit_burst,
|
|
52
|
+
retry_policy=broadcast_policy(config),
|
|
53
53
|
chain_id=config.chain_id,
|
|
54
54
|
log_init=False, # Don't log ephemeral broadcast managers
|
|
55
55
|
)
|
|
@@ -61,10 +61,11 @@ def broadcast_transaction(
|
|
|
61
61
|
group_name: str | None,
|
|
62
62
|
config: "Config",
|
|
63
63
|
job_id: str | None = None,
|
|
64
|
+
deadline: Deadline | None = None,
|
|
64
65
|
) -> tuple[str, str]:
|
|
65
66
|
"""Broadcast transaction with isolation guarantee.
|
|
66
67
|
|
|
67
|
-
This function creates a dedicated
|
|
68
|
+
This function creates a dedicated BroadcastClient for the broadcast,
|
|
68
69
|
ensuring the transaction is only sent to the specified endpoints.
|
|
69
70
|
|
|
70
71
|
Args:
|
|
@@ -89,7 +90,7 @@ def broadcast_transaction(
|
|
|
89
90
|
group_label = group_name or "ungrouped"
|
|
90
91
|
|
|
91
92
|
try:
|
|
92
|
-
tx_hash, endpoint_url = manager.send_raw_transaction(raw_tx)
|
|
93
|
+
tx_hash, endpoint_url = manager.send_raw_transaction(raw_tx, deadline=deadline)
|
|
93
94
|
|
|
94
95
|
# Record success metrics
|
|
95
96
|
latency = time.perf_counter() - start_time
|
brawny/_rpc/caller.py
ADDED
|
@@ -0,0 +1,243 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from typing import Any, Callable
|
|
4
|
+
from urllib.parse import urlsplit, urlunsplit
|
|
5
|
+
|
|
6
|
+
from web3 import Web3
|
|
7
|
+
from web3.exceptions import TransactionNotFound
|
|
8
|
+
from eth_utils import keccak
|
|
9
|
+
|
|
10
|
+
from brawny._rpc.errors import (
|
|
11
|
+
RPCError,
|
|
12
|
+
RPCFatalError,
|
|
13
|
+
RPCRecoverableError,
|
|
14
|
+
RPCDecode,
|
|
15
|
+
RPCPermanent,
|
|
16
|
+
RPCTransient,
|
|
17
|
+
RPCRateLimited,
|
|
18
|
+
RpcErrorKind,
|
|
19
|
+
classify_rpc_error,
|
|
20
|
+
)
|
|
21
|
+
from brawny.metrics import (
|
|
22
|
+
RPC_ERROR_CLASSIFIED,
|
|
23
|
+
RPC_ERROR_UNKNOWN,
|
|
24
|
+
get_metrics,
|
|
25
|
+
)
|
|
26
|
+
from brawny.network_guard import allow_network_calls
|
|
27
|
+
from brawny.timeout import Deadline
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
def _extract_url_auth(url: str) -> tuple[str, tuple[str, str] | None]:
|
|
31
|
+
split = urlsplit(url)
|
|
32
|
+
if split.username or split.password:
|
|
33
|
+
auth = (split.username or "", split.password or "")
|
|
34
|
+
clean_netloc = split.hostname or ""
|
|
35
|
+
if split.port:
|
|
36
|
+
clean_netloc = f"{clean_netloc}:{split.port}"
|
|
37
|
+
clean_url = urlunsplit((split.scheme, clean_netloc, split.path, split.query, split.fragment))
|
|
38
|
+
return clean_url, auth
|
|
39
|
+
return url, None
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
_FATAL_KINDS = frozenset({
|
|
43
|
+
RpcErrorKind.NONCE_TOO_LOW,
|
|
44
|
+
RpcErrorKind.NONCE_TOO_HIGH,
|
|
45
|
+
RpcErrorKind.INSUFFICIENT_FUNDS,
|
|
46
|
+
RpcErrorKind.INTRINSIC_GAS_TOO_LOW,
|
|
47
|
+
RpcErrorKind.GAS_LIMIT_EXCEEDED,
|
|
48
|
+
RpcErrorKind.TX_TYPE_NOT_SUPPORTED,
|
|
49
|
+
RpcErrorKind.EXECUTION_REVERTED,
|
|
50
|
+
RpcErrorKind.OUT_OF_GAS,
|
|
51
|
+
RpcErrorKind.INVALID_PARAMS,
|
|
52
|
+
RpcErrorKind.PARSE_ERROR,
|
|
53
|
+
RpcErrorKind.BAD_REQUEST,
|
|
54
|
+
})
|
|
55
|
+
|
|
56
|
+
_RECOVERABLE_KINDS = frozenset({
|
|
57
|
+
RpcErrorKind.REPLACEMENT_UNDERPRICED,
|
|
58
|
+
RpcErrorKind.TX_UNDERPRICED,
|
|
59
|
+
RpcErrorKind.MAX_FEE_TOO_LOW,
|
|
60
|
+
})
|
|
61
|
+
|
|
62
|
+
|
|
63
|
+
def _map_kind_to_class(kind: RpcErrorKind) -> type[RPCError]:
|
|
64
|
+
if kind in _RECOVERABLE_KINDS:
|
|
65
|
+
return RPCRecoverableError
|
|
66
|
+
if kind == RpcErrorKind.RATE_LIMIT:
|
|
67
|
+
return RPCRateLimited
|
|
68
|
+
if kind in (RpcErrorKind.PARSE_ERROR, RpcErrorKind.BAD_REQUEST):
|
|
69
|
+
return RPCDecode
|
|
70
|
+
if kind in (RpcErrorKind.INVALID_PARAMS,):
|
|
71
|
+
return RPCPermanent
|
|
72
|
+
if kind == RpcErrorKind.DEADLINE_EXHAUSTED:
|
|
73
|
+
return RPCPermanent
|
|
74
|
+
if kind in _FATAL_KINDS:
|
|
75
|
+
return RPCFatalError
|
|
76
|
+
return RPCTransient
|
|
77
|
+
|
|
78
|
+
|
|
79
|
+
def _handle_already_known(raw_tx: bytes) -> str:
|
|
80
|
+
if isinstance(raw_tx, str):
|
|
81
|
+
raw_tx = raw_tx[2:] if raw_tx.startswith("0x") else raw_tx
|
|
82
|
+
raw_tx = bytes.fromhex(raw_tx)
|
|
83
|
+
return "0x" + keccak(raw_tx).hex()
|
|
84
|
+
|
|
85
|
+
|
|
86
|
+
class Caller:
|
|
87
|
+
"""Execute a single RPC call against a specific endpoint."""
|
|
88
|
+
|
|
89
|
+
def __init__(self, endpoints: list[str], timeout_seconds: float, chain_id: int | None) -> None:
|
|
90
|
+
self._timeout = timeout_seconds
|
|
91
|
+
self._chain_id = chain_id
|
|
92
|
+
self._web3_instances: dict[str, Web3] = {}
|
|
93
|
+
self._web3_timeout_cache: dict[tuple[str, float], Web3] = {}
|
|
94
|
+
for endpoint in endpoints:
|
|
95
|
+
clean_url, auth = _extract_url_auth(endpoint)
|
|
96
|
+
request_kwargs: dict[str, Any] = {"timeout": timeout_seconds}
|
|
97
|
+
if auth:
|
|
98
|
+
request_kwargs["auth"] = auth
|
|
99
|
+
self._web3_instances[endpoint] = Web3(
|
|
100
|
+
Web3.HTTPProvider(clean_url, request_kwargs=request_kwargs)
|
|
101
|
+
)
|
|
102
|
+
|
|
103
|
+
def get_web3(self, endpoint_url: str, timeout: float) -> Web3:
|
|
104
|
+
if timeout == self._timeout:
|
|
105
|
+
return self._web3_instances[endpoint_url]
|
|
106
|
+
key = (endpoint_url, timeout)
|
|
107
|
+
cached = self._web3_timeout_cache.get(key)
|
|
108
|
+
if cached is not None:
|
|
109
|
+
return cached
|
|
110
|
+
clean_url, auth = _extract_url_auth(endpoint_url)
|
|
111
|
+
request_kwargs: dict[str, Any] = {"timeout": timeout}
|
|
112
|
+
if auth:
|
|
113
|
+
request_kwargs["auth"] = auth
|
|
114
|
+
w3 = Web3(Web3.HTTPProvider(clean_url, request_kwargs=request_kwargs))
|
|
115
|
+
self._web3_timeout_cache[key] = w3
|
|
116
|
+
return w3
|
|
117
|
+
|
|
118
|
+
def call(
|
|
119
|
+
self,
|
|
120
|
+
endpoint_url: str,
|
|
121
|
+
method: str,
|
|
122
|
+
args: tuple[Any, ...],
|
|
123
|
+
*,
|
|
124
|
+
timeout: float,
|
|
125
|
+
deadline: Deadline | None,
|
|
126
|
+
block_identifier: int | str,
|
|
127
|
+
) -> Any:
|
|
128
|
+
w3 = self.get_web3(endpoint_url, timeout)
|
|
129
|
+
try:
|
|
130
|
+
with allow_network_calls(reason="rpc"):
|
|
131
|
+
return self._execute_method(w3, method, args, block_identifier)
|
|
132
|
+
except Exception as exc: # noqa: BLE001 - preserve original exception
|
|
133
|
+
if isinstance(exc, RPCError):
|
|
134
|
+
if exc.method is None or exc.endpoint is None:
|
|
135
|
+
raise type(exc)(str(exc), code=exc.code, method=method, endpoint=endpoint_url) from exc
|
|
136
|
+
raise
|
|
137
|
+
|
|
138
|
+
extracted = classify_rpc_error(exc, endpoint=endpoint_url, method=method, deadline=deadline)
|
|
139
|
+
if extracted.kind == RpcErrorKind.ALREADY_KNOWN and method == "eth_sendRawTransaction":
|
|
140
|
+
raw_tx = args[0] if args else b""
|
|
141
|
+
return _handle_already_known(raw_tx)
|
|
142
|
+
self._raise_classified(exc, extracted, endpoint_url, method)
|
|
143
|
+
|
|
144
|
+
def call_with_web3(
|
|
145
|
+
self,
|
|
146
|
+
endpoint_url: str,
|
|
147
|
+
*,
|
|
148
|
+
timeout: float,
|
|
149
|
+
deadline: Deadline | None,
|
|
150
|
+
method: str,
|
|
151
|
+
fn: Callable[[Web3], Any],
|
|
152
|
+
) -> Any:
|
|
153
|
+
w3 = self.get_web3(endpoint_url, timeout)
|
|
154
|
+
try:
|
|
155
|
+
with allow_network_calls(reason="rpc"):
|
|
156
|
+
return fn(w3)
|
|
157
|
+
except Exception as exc: # noqa: BLE001
|
|
158
|
+
if isinstance(exc, RPCError):
|
|
159
|
+
if exc.method is None or exc.endpoint is None:
|
|
160
|
+
raise type(exc)(str(exc), code=exc.code, method=method, endpoint=endpoint_url) from exc
|
|
161
|
+
raise
|
|
162
|
+
extracted = classify_rpc_error(exc, endpoint=endpoint_url, method=method, deadline=deadline)
|
|
163
|
+
self._raise_classified(exc, extracted, endpoint_url, method)
|
|
164
|
+
|
|
165
|
+
@staticmethod
|
|
166
|
+
def _raise_classified(
|
|
167
|
+
exc: Exception,
|
|
168
|
+
extracted,
|
|
169
|
+
endpoint_url: str,
|
|
170
|
+
method: str,
|
|
171
|
+
) -> None:
|
|
172
|
+
metrics = get_metrics()
|
|
173
|
+
if extracted.kind == RpcErrorKind.UNKNOWN:
|
|
174
|
+
metrics.counter(RPC_ERROR_UNKNOWN).inc(
|
|
175
|
+
method=method or "unknown",
|
|
176
|
+
exception_type=type(exc).__name__,
|
|
177
|
+
provider=extracted.provider or "unknown",
|
|
178
|
+
http_status=str(extracted.http_status or "none"),
|
|
179
|
+
jsonrpc_code=str(extracted.code or "none"),
|
|
180
|
+
)
|
|
181
|
+
raise exc
|
|
182
|
+
metrics.counter(RPC_ERROR_CLASSIFIED).inc(
|
|
183
|
+
kind=extracted.kind.value,
|
|
184
|
+
method=method or "unknown",
|
|
185
|
+
source=extracted.classification_source,
|
|
186
|
+
)
|
|
187
|
+
|
|
188
|
+
error_class = _map_kind_to_class(extracted.kind)
|
|
189
|
+
err = error_class(
|
|
190
|
+
str(exc),
|
|
191
|
+
code=extracted.kind.value,
|
|
192
|
+
endpoint=endpoint_url,
|
|
193
|
+
method=method,
|
|
194
|
+
)
|
|
195
|
+
setattr(err, "failover_ok", extracted.failover_ok)
|
|
196
|
+
setattr(err, "classification_kind", extracted.kind)
|
|
197
|
+
raise err from exc
|
|
198
|
+
|
|
199
|
+
@staticmethod
|
|
200
|
+
def _execute_method(
|
|
201
|
+
w3: Web3,
|
|
202
|
+
method: str,
|
|
203
|
+
args: tuple[Any, ...],
|
|
204
|
+
block_identifier: int | str,
|
|
205
|
+
) -> Any:
|
|
206
|
+
if method == "eth_blockNumber":
|
|
207
|
+
return w3.eth.block_number
|
|
208
|
+
if method == "eth_getBlockByNumber":
|
|
209
|
+
block_num = args[0] if args else "latest"
|
|
210
|
+
full_tx = args[1] if len(args) > 1 else False
|
|
211
|
+
return w3.eth.get_block(block_num, full_transactions=full_tx)
|
|
212
|
+
if method == "eth_getTransactionCount":
|
|
213
|
+
address = args[0]
|
|
214
|
+
block = args[1] if len(args) > 1 else "pending"
|
|
215
|
+
return w3.eth.get_transaction_count(address, block)
|
|
216
|
+
if method == "eth_getTransactionReceipt":
|
|
217
|
+
tx_hash = args[0]
|
|
218
|
+
try:
|
|
219
|
+
return w3.eth.get_transaction_receipt(tx_hash)
|
|
220
|
+
except TransactionNotFound:
|
|
221
|
+
return None
|
|
222
|
+
if method == "eth_sendRawTransaction":
|
|
223
|
+
return w3.eth.send_raw_transaction(args[0])
|
|
224
|
+
if method == "eth_estimateGas":
|
|
225
|
+
return w3.eth.estimate_gas(args[0], block_identifier=block_identifier)
|
|
226
|
+
if method == "eth_call":
|
|
227
|
+
tx = args[0]
|
|
228
|
+
block = args[1] if len(args) > 1 else block_identifier
|
|
229
|
+
return w3.eth.call(tx, block_identifier=block)
|
|
230
|
+
if method == "eth_getStorageAt":
|
|
231
|
+
address = args[0]
|
|
232
|
+
slot = args[1]
|
|
233
|
+
block = args[2] if len(args) > 2 else block_identifier
|
|
234
|
+
return w3.eth.get_storage_at(address, slot, block_identifier=block)
|
|
235
|
+
if method == "eth_chainId":
|
|
236
|
+
return w3.eth.chain_id
|
|
237
|
+
if method == "eth_gasPrice":
|
|
238
|
+
return w3.eth.gas_price
|
|
239
|
+
if method == "eth_getBalance":
|
|
240
|
+
address = args[0]
|
|
241
|
+
block = args[1] if len(args) > 1 else block_identifier
|
|
242
|
+
return w3.eth.get_balance(address, block_identifier=block)
|
|
243
|
+
return w3.provider.make_request(method, list(args))
|