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.
Files changed (135) hide show
  1. brawny/__init__.py +2 -0
  2. brawny/_context.py +5 -5
  3. brawny/_rpc/__init__.py +36 -12
  4. brawny/_rpc/broadcast.py +14 -13
  5. brawny/_rpc/caller.py +243 -0
  6. brawny/_rpc/client.py +539 -0
  7. brawny/_rpc/clients.py +11 -11
  8. brawny/_rpc/context.py +23 -0
  9. brawny/_rpc/errors.py +465 -31
  10. brawny/_rpc/gas.py +7 -6
  11. brawny/_rpc/pool.py +18 -0
  12. brawny/_rpc/retry.py +266 -0
  13. brawny/_rpc/retry_policy.py +81 -0
  14. brawny/accounts.py +28 -9
  15. brawny/alerts/__init__.py +15 -18
  16. brawny/alerts/abi_resolver.py +212 -36
  17. brawny/alerts/base.py +2 -2
  18. brawny/alerts/contracts.py +77 -10
  19. brawny/alerts/errors.py +30 -3
  20. brawny/alerts/events.py +38 -5
  21. brawny/alerts/health.py +19 -13
  22. brawny/alerts/send.py +513 -55
  23. brawny/api.py +39 -11
  24. brawny/assets/AGENTS.md +325 -0
  25. brawny/async_runtime.py +48 -0
  26. brawny/chain.py +3 -3
  27. brawny/cli/commands/__init__.py +2 -0
  28. brawny/cli/commands/console.py +69 -19
  29. brawny/cli/commands/contract.py +2 -2
  30. brawny/cli/commands/controls.py +121 -0
  31. brawny/cli/commands/health.py +2 -2
  32. brawny/cli/commands/job_dev.py +6 -5
  33. brawny/cli/commands/jobs.py +99 -2
  34. brawny/cli/commands/maintenance.py +13 -29
  35. brawny/cli/commands/migrate.py +1 -0
  36. brawny/cli/commands/run.py +10 -3
  37. brawny/cli/commands/script.py +8 -3
  38. brawny/cli/commands/signer.py +143 -26
  39. brawny/cli/helpers.py +0 -3
  40. brawny/cli_templates.py +25 -349
  41. brawny/config/__init__.py +4 -1
  42. brawny/config/models.py +43 -57
  43. brawny/config/parser.py +268 -57
  44. brawny/config/validation.py +52 -15
  45. brawny/daemon/context.py +4 -2
  46. brawny/daemon/core.py +185 -63
  47. brawny/daemon/loops.py +166 -98
  48. brawny/daemon/supervisor.py +261 -0
  49. brawny/db/__init__.py +14 -26
  50. brawny/db/base.py +248 -151
  51. brawny/db/global_cache.py +11 -1
  52. brawny/db/migrate.py +175 -28
  53. brawny/db/migrations/001_init.sql +4 -3
  54. brawny/db/migrations/010_add_nonce_gap_index.sql +1 -1
  55. brawny/db/migrations/011_add_job_logs.sql +1 -2
  56. brawny/db/migrations/012_add_claimed_by.sql +2 -2
  57. brawny/db/migrations/013_attempt_unique.sql +10 -0
  58. brawny/db/migrations/014_add_lease_expires_at.sql +5 -0
  59. brawny/db/migrations/015_add_signer_alias.sql +14 -0
  60. brawny/db/migrations/016_runtime_controls_and_quarantine.sql +32 -0
  61. brawny/db/migrations/017_add_job_drain.sql +6 -0
  62. brawny/db/migrations/018_add_nonce_reset_audit.sql +20 -0
  63. brawny/db/migrations/019_add_job_cooldowns.sql +8 -0
  64. brawny/db/migrations/020_attempt_unique_initial.sql +7 -0
  65. brawny/db/ops/__init__.py +3 -25
  66. brawny/db/ops/logs.py +1 -2
  67. brawny/db/queries.py +47 -91
  68. brawny/db/serialized.py +65 -0
  69. brawny/db/sqlite/__init__.py +1001 -0
  70. brawny/db/sqlite/connection.py +231 -0
  71. brawny/db/sqlite/execute.py +116 -0
  72. brawny/db/sqlite/mappers.py +190 -0
  73. brawny/db/sqlite/repos/attempts.py +372 -0
  74. brawny/db/sqlite/repos/block_state.py +102 -0
  75. brawny/db/sqlite/repos/cache.py +104 -0
  76. brawny/db/sqlite/repos/intents.py +1021 -0
  77. brawny/db/sqlite/repos/jobs.py +200 -0
  78. brawny/db/sqlite/repos/maintenance.py +182 -0
  79. brawny/db/sqlite/repos/signers_nonces.py +566 -0
  80. brawny/db/sqlite/tx.py +119 -0
  81. brawny/http.py +194 -0
  82. brawny/invariants.py +11 -24
  83. brawny/jobs/base.py +8 -0
  84. brawny/jobs/job_validation.py +2 -1
  85. brawny/keystore.py +83 -7
  86. brawny/lifecycle.py +64 -12
  87. brawny/logging.py +0 -2
  88. brawny/metrics.py +84 -12
  89. brawny/model/contexts.py +111 -9
  90. brawny/model/enums.py +1 -0
  91. brawny/model/errors.py +18 -0
  92. brawny/model/types.py +47 -131
  93. brawny/network_guard.py +133 -0
  94. brawny/networks/__init__.py +5 -5
  95. brawny/networks/config.py +1 -7
  96. brawny/networks/manager.py +14 -11
  97. brawny/runtime_controls.py +74 -0
  98. brawny/scheduler/poller.py +11 -7
  99. brawny/scheduler/reorg.py +95 -39
  100. brawny/scheduler/runner.py +442 -168
  101. brawny/scheduler/shutdown.py +3 -3
  102. brawny/script_tx.py +3 -3
  103. brawny/telegram.py +53 -7
  104. brawny/testing.py +1 -0
  105. brawny/timeout.py +38 -0
  106. brawny/tx/executor.py +922 -308
  107. brawny/tx/intent.py +54 -16
  108. brawny/tx/monitor.py +31 -12
  109. brawny/tx/nonce.py +212 -90
  110. brawny/tx/replacement.py +69 -18
  111. brawny/tx/retry_policy.py +24 -0
  112. brawny/tx/stages/types.py +75 -0
  113. brawny/types.py +18 -0
  114. brawny/utils.py +41 -0
  115. {brawny-0.1.13.dist-info → brawny-0.1.22.dist-info}/METADATA +3 -3
  116. brawny-0.1.22.dist-info/RECORD +163 -0
  117. brawny/_rpc/manager.py +0 -982
  118. brawny/_rpc/selector.py +0 -156
  119. brawny/db/base_new.py +0 -165
  120. brawny/db/mappers.py +0 -182
  121. brawny/db/migrations/008_add_transactions.sql +0 -72
  122. brawny/db/ops/attempts.py +0 -108
  123. brawny/db/ops/blocks.py +0 -83
  124. brawny/db/ops/cache.py +0 -93
  125. brawny/db/ops/intents.py +0 -296
  126. brawny/db/ops/jobs.py +0 -110
  127. brawny/db/ops/nonces.py +0 -322
  128. brawny/db/postgres.py +0 -2535
  129. brawny/db/postgres_new.py +0 -196
  130. brawny/db/sqlite.py +0 -2733
  131. brawny/db/sqlite_new.py +0 -191
  132. brawny-0.1.13.dist-info/RECORD +0 -141
  133. {brawny-0.1.13.dist-info → brawny-0.1.22.dist-info}/WHEEL +0 -0
  134. {brawny-0.1.13.dist-info → brawny-0.1.22.dist-info}/entry_points.txt +0 -0
  135. {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 alert hooks):
15
+ Usage (in lifecycle hooks):
16
16
  from brawny import Contract, shorten, explorer_link
17
17
 
18
- def alert_confirmed(self, ctx):
18
+ def on_success(self, ctx):
19
19
  vault = Contract("vault")
20
- return f"Done!\\n{explorer_link(ctx.receipt.transactionHash.hex())}"
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 RPCManager
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: RPCManager
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 management with multi-endpoint failover and health tracking.
1
+ """RPC core built from small pieces.
2
2
 
3
- OE6 Simplification:
4
- - Uses EndpointSelector for health-aware endpoint ordering
5
- - Explicit failover gate (only on RPCRetryableError)
6
- - Removed circuit breaker and rate limiter (simpler error handling)
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.manager import RPCManager
18
- from brawny._rpc.selector import EndpointSelector, EndpointHealth
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
- "RPCManager",
27
- "EndpointSelector",
28
- "EndpointHealth",
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
- RPCManager does the endpoint iteration; this module adds group context.
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.manager import RPCManager
27
+ from brawny._rpc.clients import BroadcastClient
27
28
 
28
29
 
29
- def create_broadcast_manager(endpoints: list[str], config: "Config") -> "RPCManager":
30
- """Create an RPCManager for broadcasting to specific endpoints.
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 RPCManager instance for broadcasting.
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
- RPCManager configured for the provided endpoints
42
+ BroadcastClient configured for the provided endpoints
42
43
  """
43
- from brawny._rpc.manager import RPCManager
44
+ from brawny._rpc.retry_policy import broadcast_policy
45
+ from brawny._rpc.clients import BroadcastClient
44
46
 
45
- return RPCManager(
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
- circuit_breaker_seconds=config.rpc_circuit_breaker_seconds,
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 RPCManager for the broadcast,
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))