brawny 0.1.13__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 +106 -0
- brawny/_context.py +232 -0
- brawny/_rpc/__init__.py +38 -0
- brawny/_rpc/broadcast.py +172 -0
- brawny/_rpc/clients.py +98 -0
- brawny/_rpc/context.py +49 -0
- brawny/_rpc/errors.py +252 -0
- brawny/_rpc/gas.py +158 -0
- brawny/_rpc/manager.py +982 -0
- brawny/_rpc/selector.py +156 -0
- brawny/accounts.py +534 -0
- brawny/alerts/__init__.py +132 -0
- brawny/alerts/abi_resolver.py +530 -0
- brawny/alerts/base.py +152 -0
- brawny/alerts/context.py +271 -0
- brawny/alerts/contracts.py +635 -0
- brawny/alerts/encoded_call.py +201 -0
- brawny/alerts/errors.py +267 -0
- brawny/alerts/events.py +680 -0
- brawny/alerts/function_caller.py +364 -0
- brawny/alerts/health.py +185 -0
- brawny/alerts/routing.py +118 -0
- brawny/alerts/send.py +364 -0
- brawny/api.py +660 -0
- brawny/chain.py +93 -0
- brawny/cli/__init__.py +16 -0
- brawny/cli/app.py +17 -0
- brawny/cli/bootstrap.py +37 -0
- brawny/cli/commands/__init__.py +41 -0
- brawny/cli/commands/abi.py +93 -0
- brawny/cli/commands/accounts.py +632 -0
- brawny/cli/commands/console.py +495 -0
- brawny/cli/commands/contract.py +139 -0
- brawny/cli/commands/health.py +112 -0
- brawny/cli/commands/init_project.py +86 -0
- brawny/cli/commands/intents.py +130 -0
- brawny/cli/commands/job_dev.py +254 -0
- brawny/cli/commands/jobs.py +308 -0
- brawny/cli/commands/logs.py +87 -0
- brawny/cli/commands/maintenance.py +182 -0
- brawny/cli/commands/migrate.py +51 -0
- brawny/cli/commands/networks.py +253 -0
- brawny/cli/commands/run.py +249 -0
- brawny/cli/commands/script.py +209 -0
- brawny/cli/commands/signer.py +248 -0
- brawny/cli/helpers.py +265 -0
- brawny/cli_templates.py +1445 -0
- brawny/config/__init__.py +74 -0
- brawny/config/models.py +404 -0
- brawny/config/parser.py +633 -0
- brawny/config/routing.py +55 -0
- brawny/config/validation.py +246 -0
- brawny/daemon/__init__.py +14 -0
- brawny/daemon/context.py +69 -0
- brawny/daemon/core.py +702 -0
- brawny/daemon/loops.py +327 -0
- brawny/db/__init__.py +78 -0
- brawny/db/base.py +986 -0
- brawny/db/base_new.py +165 -0
- brawny/db/circuit_breaker.py +97 -0
- brawny/db/global_cache.py +298 -0
- brawny/db/mappers.py +182 -0
- brawny/db/migrate.py +349 -0
- brawny/db/migrations/001_init.sql +186 -0
- brawny/db/migrations/002_add_included_block.sql +7 -0
- brawny/db/migrations/003_add_broadcast_at.sql +10 -0
- brawny/db/migrations/004_broadcast_binding.sql +20 -0
- brawny/db/migrations/005_add_retry_after.sql +9 -0
- brawny/db/migrations/006_add_retry_count_column.sql +11 -0
- brawny/db/migrations/007_add_gap_tracking.sql +18 -0
- brawny/db/migrations/008_add_transactions.sql +72 -0
- brawny/db/migrations/009_add_intent_metadata.sql +5 -0
- brawny/db/migrations/010_add_nonce_gap_index.sql +9 -0
- brawny/db/migrations/011_add_job_logs.sql +24 -0
- brawny/db/migrations/012_add_claimed_by.sql +5 -0
- brawny/db/ops/__init__.py +29 -0
- brawny/db/ops/attempts.py +108 -0
- brawny/db/ops/blocks.py +83 -0
- brawny/db/ops/cache.py +93 -0
- brawny/db/ops/intents.py +296 -0
- brawny/db/ops/jobs.py +110 -0
- brawny/db/ops/logs.py +97 -0
- brawny/db/ops/nonces.py +322 -0
- brawny/db/postgres.py +2535 -0
- brawny/db/postgres_new.py +196 -0
- brawny/db/queries.py +584 -0
- brawny/db/sqlite.py +2733 -0
- brawny/db/sqlite_new.py +191 -0
- brawny/history.py +126 -0
- brawny/interfaces.py +136 -0
- brawny/invariants.py +155 -0
- brawny/jobs/__init__.py +26 -0
- brawny/jobs/base.py +287 -0
- brawny/jobs/discovery.py +233 -0
- brawny/jobs/job_validation.py +111 -0
- brawny/jobs/kv.py +125 -0
- brawny/jobs/registry.py +283 -0
- brawny/keystore.py +484 -0
- brawny/lifecycle.py +551 -0
- brawny/logging.py +290 -0
- brawny/metrics.py +594 -0
- brawny/model/__init__.py +53 -0
- brawny/model/contexts.py +319 -0
- brawny/model/enums.py +70 -0
- brawny/model/errors.py +194 -0
- brawny/model/events.py +93 -0
- brawny/model/startup.py +20 -0
- brawny/model/types.py +483 -0
- brawny/networks/__init__.py +96 -0
- brawny/networks/config.py +269 -0
- brawny/networks/manager.py +423 -0
- brawny/obs/__init__.py +67 -0
- brawny/obs/emit.py +158 -0
- brawny/obs/health.py +175 -0
- brawny/obs/heartbeat.py +133 -0
- brawny/reconciliation.py +108 -0
- brawny/scheduler/__init__.py +19 -0
- brawny/scheduler/poller.py +472 -0
- brawny/scheduler/reorg.py +632 -0
- brawny/scheduler/runner.py +708 -0
- brawny/scheduler/shutdown.py +371 -0
- brawny/script_tx.py +297 -0
- brawny/scripting.py +251 -0
- brawny/startup.py +76 -0
- brawny/telegram.py +393 -0
- brawny/testing.py +108 -0
- brawny/tx/__init__.py +41 -0
- brawny/tx/executor.py +1071 -0
- brawny/tx/fees.py +50 -0
- brawny/tx/intent.py +423 -0
- brawny/tx/monitor.py +628 -0
- brawny/tx/nonce.py +498 -0
- brawny/tx/replacement.py +456 -0
- brawny/tx/utils.py +26 -0
- brawny/utils.py +205 -0
- brawny/validation.py +69 -0
- brawny-0.1.13.dist-info/METADATA +156 -0
- brawny-0.1.13.dist-info/RECORD +141 -0
- brawny-0.1.13.dist-info/WHEEL +5 -0
- brawny-0.1.13.dist-info/entry_points.txt +2 -0
- brawny-0.1.13.dist-info/top_level.txt +1 -0
|
@@ -0,0 +1,201 @@
|
|
|
1
|
+
"""Encoded call types for contract interactions.
|
|
2
|
+
|
|
3
|
+
Provides EncodedCall, ReturnValue, and FunctionABI classes.
|
|
4
|
+
"""
|
|
5
|
+
|
|
6
|
+
from __future__ import annotations
|
|
7
|
+
|
|
8
|
+
from dataclasses import dataclass
|
|
9
|
+
from typing import TYPE_CHECKING, Any
|
|
10
|
+
|
|
11
|
+
if TYPE_CHECKING:
|
|
12
|
+
from brawny.alerts.contracts import ContractHandle
|
|
13
|
+
from brawny.jobs.base import TxReceipt
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
@dataclass
|
|
17
|
+
class FunctionABI:
|
|
18
|
+
"""Parsed function ABI entry."""
|
|
19
|
+
|
|
20
|
+
name: str
|
|
21
|
+
inputs: list[dict[str, Any]]
|
|
22
|
+
outputs: list[dict[str, Any]]
|
|
23
|
+
state_mutability: str
|
|
24
|
+
signature: str
|
|
25
|
+
selector: bytes
|
|
26
|
+
|
|
27
|
+
@property
|
|
28
|
+
def is_view(self) -> bool:
|
|
29
|
+
"""Check if function is view or pure (read-only)."""
|
|
30
|
+
return self.state_mutability in ("view", "pure")
|
|
31
|
+
|
|
32
|
+
@property
|
|
33
|
+
def is_state_changing(self) -> bool:
|
|
34
|
+
"""Check if function modifies state."""
|
|
35
|
+
return self.state_mutability in ("nonpayable", "payable")
|
|
36
|
+
|
|
37
|
+
@property
|
|
38
|
+
def is_payable(self) -> bool:
|
|
39
|
+
"""Check if function accepts ETH value."""
|
|
40
|
+
return self.state_mutability == "payable"
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
class ReturnValue(tuple):
|
|
44
|
+
"""Tuple with named field access for contract return values.
|
|
45
|
+
|
|
46
|
+
Brownie-compatible: prints as tuple, supports named access.
|
|
47
|
+
|
|
48
|
+
Supports multiple access patterns:
|
|
49
|
+
result[0] # Index access
|
|
50
|
+
result["fieldName"] # Dict-style access
|
|
51
|
+
result.fieldName # Attribute access
|
|
52
|
+
result.keys() # Get field names
|
|
53
|
+
result.items() # Get (name, value) pairs
|
|
54
|
+
dict(result.items()) # Convert to dict
|
|
55
|
+
"""
|
|
56
|
+
|
|
57
|
+
_names: tuple[str, ...]
|
|
58
|
+
_dict: dict[str, Any]
|
|
59
|
+
|
|
60
|
+
def __new__(
|
|
61
|
+
cls, values: tuple | list, abi: list[dict[str, Any]] | None = None
|
|
62
|
+
) -> "ReturnValue":
|
|
63
|
+
values = list(values)
|
|
64
|
+
|
|
65
|
+
# Recursively wrap nested tuples based on ABI components
|
|
66
|
+
if abi is not None:
|
|
67
|
+
for i in range(len(values)):
|
|
68
|
+
if isinstance(values[i], (tuple, list)) and not isinstance(
|
|
69
|
+
values[i], ReturnValue
|
|
70
|
+
):
|
|
71
|
+
if "components" in abi[i]:
|
|
72
|
+
components = abi[i]["components"]
|
|
73
|
+
if abi[i]["type"] == "tuple":
|
|
74
|
+
# Single struct
|
|
75
|
+
values[i] = ReturnValue(values[i], components)
|
|
76
|
+
else:
|
|
77
|
+
# Array of structs - wrap each element, keep as list
|
|
78
|
+
values[i] = [ReturnValue(v, components) for v in values[i]]
|
|
79
|
+
|
|
80
|
+
instance = super().__new__(cls, values)
|
|
81
|
+
|
|
82
|
+
# Build names from ABI or use fallback (arg0, arg1 - no brackets for attr access)
|
|
83
|
+
if abi is not None:
|
|
84
|
+
names = tuple(out.get("name") or f"arg{i}" for i, out in enumerate(abi))
|
|
85
|
+
else:
|
|
86
|
+
names = tuple(f"arg{i}" for i in range(len(values)))
|
|
87
|
+
|
|
88
|
+
object.__setattr__(instance, "_names", names)
|
|
89
|
+
object.__setattr__(instance, "_dict", dict(zip(names, values)))
|
|
90
|
+
return instance
|
|
91
|
+
|
|
92
|
+
def __getitem__(self, key):
|
|
93
|
+
if isinstance(key, str):
|
|
94
|
+
try:
|
|
95
|
+
return self._dict[key]
|
|
96
|
+
except KeyError:
|
|
97
|
+
raise KeyError(f"No field '{key}'. Available: {list(self._names)}")
|
|
98
|
+
return super().__getitem__(key)
|
|
99
|
+
|
|
100
|
+
def __getattr__(self, name: str):
|
|
101
|
+
if name.startswith("_"):
|
|
102
|
+
raise AttributeError(name)
|
|
103
|
+
try:
|
|
104
|
+
return self._dict[name]
|
|
105
|
+
except KeyError:
|
|
106
|
+
raise AttributeError(f"No field '{name}'. Available: {list(self._names)}")
|
|
107
|
+
|
|
108
|
+
def keys(self) -> tuple[str, ...]:
|
|
109
|
+
"""Return output names."""
|
|
110
|
+
return self._names
|
|
111
|
+
|
|
112
|
+
def items(self):
|
|
113
|
+
"""Return (name, value) pairs."""
|
|
114
|
+
return self._dict.items()
|
|
115
|
+
|
|
116
|
+
def dict(self) -> dict[str, object]:
|
|
117
|
+
"""Convert to dict, recursively unwrapping nested ReturnValues."""
|
|
118
|
+
|
|
119
|
+
def _norm(x: object) -> object:
|
|
120
|
+
if isinstance(x, ReturnValue):
|
|
121
|
+
return x.dict()
|
|
122
|
+
if isinstance(x, list):
|
|
123
|
+
return [_norm(v) for v in x]
|
|
124
|
+
return x
|
|
125
|
+
|
|
126
|
+
return {k: _norm(v) for k, v in self._dict.items()}
|
|
127
|
+
|
|
128
|
+
# Use tuple's __repr__ for Brownie parity: prints as (val1, val2)
|
|
129
|
+
__repr__ = tuple.__repr__
|
|
130
|
+
|
|
131
|
+
|
|
132
|
+
class EncodedCall(str):
|
|
133
|
+
"""Encoded calldata with .call() and .transact() methods.
|
|
134
|
+
|
|
135
|
+
This is a str subclass so it can be used directly as calldata,
|
|
136
|
+
while also providing Brownie-style modifiers for execution.
|
|
137
|
+
|
|
138
|
+
Usage:
|
|
139
|
+
# Get calldata (str)
|
|
140
|
+
calldata = vault.harvest()
|
|
141
|
+
|
|
142
|
+
# Force eth_call simulation
|
|
143
|
+
result = vault.harvest().call()
|
|
144
|
+
|
|
145
|
+
# Broadcast transaction (only in @broadcast context)
|
|
146
|
+
receipt = vault.harvest().transact({"from": "yearn-worker"})
|
|
147
|
+
"""
|
|
148
|
+
|
|
149
|
+
_contract: "ContractHandle"
|
|
150
|
+
_abi: FunctionABI
|
|
151
|
+
|
|
152
|
+
def __new__(
|
|
153
|
+
cls,
|
|
154
|
+
calldata: str,
|
|
155
|
+
contract: "ContractHandle",
|
|
156
|
+
abi: FunctionABI,
|
|
157
|
+
) -> "EncodedCall":
|
|
158
|
+
instance = super().__new__(cls, calldata)
|
|
159
|
+
instance._contract = contract
|
|
160
|
+
instance._abi = abi
|
|
161
|
+
return instance
|
|
162
|
+
|
|
163
|
+
def call(self) -> Any:
|
|
164
|
+
"""Execute eth_call and return decoded result.
|
|
165
|
+
|
|
166
|
+
Performs a static call (simulation) without broadcasting.
|
|
167
|
+
Works regardless of function state mutability.
|
|
168
|
+
|
|
169
|
+
Returns:
|
|
170
|
+
Decoded return value from the function
|
|
171
|
+
"""
|
|
172
|
+
return self._contract._call_with_calldata(str(self), self._abi)
|
|
173
|
+
|
|
174
|
+
def transact(self, tx_params: dict[str, Any] | None = None) -> "TxReceipt":
|
|
175
|
+
"""Broadcast the transaction and wait for receipt.
|
|
176
|
+
|
|
177
|
+
Only works inside a @broadcast decorated function.
|
|
178
|
+
Raises BroadcastNotAllowedError if not in broadcast context.
|
|
179
|
+
|
|
180
|
+
Args:
|
|
181
|
+
tx_params: Transaction parameters (Brownie-style)
|
|
182
|
+
- from: Signer name or address (required)
|
|
183
|
+
- value: ETH value to send (optional, for payable functions)
|
|
184
|
+
- gas: Gas limit (optional, auto-estimated if not provided)
|
|
185
|
+
- gasPrice: Gas price (optional)
|
|
186
|
+
- maxFeePerGas: EIP-1559 max fee (optional)
|
|
187
|
+
- maxPriorityFeePerGas: EIP-1559 priority fee (optional)
|
|
188
|
+
- nonce: Transaction nonce (optional, auto-fetched if not provided)
|
|
189
|
+
|
|
190
|
+
Returns:
|
|
191
|
+
Transaction receipt after confirmation
|
|
192
|
+
|
|
193
|
+
Raises:
|
|
194
|
+
BroadcastNotAllowedError: If not in @broadcast context
|
|
195
|
+
SignerNotFoundError: If 'from' address not in keystore
|
|
196
|
+
TransactionRevertedError: If transaction reverts
|
|
197
|
+
TransactionTimeoutError: If receipt wait times out
|
|
198
|
+
"""
|
|
199
|
+
if tx_params is None:
|
|
200
|
+
tx_params = {}
|
|
201
|
+
return self._contract._transact_with_calldata(str(self), tx_params, self._abi)
|
brawny/alerts/errors.py
ADDED
|
@@ -0,0 +1,267 @@
|
|
|
1
|
+
"""alerts-specific error classes for the Alerts extension.
|
|
2
|
+
|
|
3
|
+
These errors provide clear, actionable feedback when developers
|
|
4
|
+
misuse the contract interaction APIs in alert hooks.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
from __future__ import annotations
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
class DXError(Exception):
|
|
11
|
+
"""Base class for all alerts-related errors."""
|
|
12
|
+
|
|
13
|
+
pass
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
class ABINotFoundError(DXError):
|
|
17
|
+
"""Raised when ABI cannot be resolved for a contract address.
|
|
18
|
+
|
|
19
|
+
Includes details about which sources were checked.
|
|
20
|
+
"""
|
|
21
|
+
|
|
22
|
+
def __init__(self, address: str, checked_sources: list[str]) -> None:
|
|
23
|
+
self.address = address
|
|
24
|
+
self.checked_sources = checked_sources
|
|
25
|
+
sources_str = ", ".join(checked_sources) if checked_sources else "none"
|
|
26
|
+
super().__init__(
|
|
27
|
+
f"ABI not found for {address}. Checked: {sources_str}. "
|
|
28
|
+
f"Consider setting ABI manually with 'brawny abi set {address} --file abi.json'"
|
|
29
|
+
)
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
class ProxyResolutionError(DXError):
|
|
33
|
+
"""Raised when proxy resolution fails.
|
|
34
|
+
|
|
35
|
+
This can happen when:
|
|
36
|
+
- EIP-1967 slots don't contain valid addresses
|
|
37
|
+
- Beacon implementation call fails
|
|
38
|
+
- Max recursion depth exceeded
|
|
39
|
+
"""
|
|
40
|
+
|
|
41
|
+
def __init__(self, address: str, reason: str) -> None:
|
|
42
|
+
self.address = address
|
|
43
|
+
self.reason = reason
|
|
44
|
+
super().__init__(f"Failed to resolve proxy {address}: {reason}")
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
class StateChangingCallError(DXError):
|
|
48
|
+
"""Raised when attempting to call a state-changing function.
|
|
49
|
+
|
|
50
|
+
State-changing functions (nonpayable/payable) cannot be called
|
|
51
|
+
via eth_call in alert hooks. Use .encode_input() or .transact() in @broadcast.
|
|
52
|
+
"""
|
|
53
|
+
|
|
54
|
+
def __init__(
|
|
55
|
+
self,
|
|
56
|
+
function_name: str,
|
|
57
|
+
signature: str,
|
|
58
|
+
address: str | None = None,
|
|
59
|
+
job_id: str | None = None,
|
|
60
|
+
hook: str | None = None,
|
|
61
|
+
) -> None:
|
|
62
|
+
self.function_name = function_name
|
|
63
|
+
self.signature = signature
|
|
64
|
+
self.address = address
|
|
65
|
+
self.job_id = job_id
|
|
66
|
+
self.hook = hook
|
|
67
|
+
context = _format_context(job_id, hook, address, signature)
|
|
68
|
+
super().__init__(
|
|
69
|
+
f"{function_name}() is a state-changing function{context}. "
|
|
70
|
+
f"Use .encode_input() for calldata or .transact(...) inside @broadcast."
|
|
71
|
+
)
|
|
72
|
+
|
|
73
|
+
|
|
74
|
+
class ReceiptRequiredError(DXError):
|
|
75
|
+
"""Raised when accessing events without a receipt context.
|
|
76
|
+
|
|
77
|
+
Events are only available in alert_confirmed hook where
|
|
78
|
+
ctx.receipt is populated.
|
|
79
|
+
"""
|
|
80
|
+
|
|
81
|
+
def __init__(self, operation: str = "Events") -> None:
|
|
82
|
+
self.operation = operation
|
|
83
|
+
super().__init__(
|
|
84
|
+
f"{operation} are only available in alert_confirmed context where receipt is present. "
|
|
85
|
+
f"For other hooks, use ctx.block for current block information."
|
|
86
|
+
)
|
|
87
|
+
|
|
88
|
+
|
|
89
|
+
class EventNotFoundError(DXError):
|
|
90
|
+
"""Raised when expected event is not found in receipt logs.
|
|
91
|
+
|
|
92
|
+
Provides helpful information about what events are available.
|
|
93
|
+
"""
|
|
94
|
+
|
|
95
|
+
def __init__(
|
|
96
|
+
self,
|
|
97
|
+
event_name: str,
|
|
98
|
+
address: str,
|
|
99
|
+
available_events: list[str],
|
|
100
|
+
) -> None:
|
|
101
|
+
self.event_name = event_name
|
|
102
|
+
self.address = address
|
|
103
|
+
self.available_events = available_events
|
|
104
|
+
available_str = ", ".join(available_events) if available_events else "none"
|
|
105
|
+
super().__init__(
|
|
106
|
+
f"No '{event_name}' events found in receipt for {address}. "
|
|
107
|
+
f"Available decoded events: [{available_str}]"
|
|
108
|
+
)
|
|
109
|
+
|
|
110
|
+
|
|
111
|
+
class AmbiguousOverloadError(DXError):
|
|
112
|
+
"""Raised when function call matches multiple ABI overloads.
|
|
113
|
+
|
|
114
|
+
Provides guidance on using explicit signatures.
|
|
115
|
+
"""
|
|
116
|
+
|
|
117
|
+
def __init__(
|
|
118
|
+
self,
|
|
119
|
+
function_name: str,
|
|
120
|
+
arg_count: int,
|
|
121
|
+
candidates: list[str],
|
|
122
|
+
) -> None:
|
|
123
|
+
self.function_name = function_name
|
|
124
|
+
self.arg_count = arg_count
|
|
125
|
+
self.candidates = candidates
|
|
126
|
+
candidates_str = ", ".join(candidates)
|
|
127
|
+
super().__init__(
|
|
128
|
+
f"Multiple matches for '{function_name}' with {arg_count} argument(s). "
|
|
129
|
+
f"Candidates: {candidates_str}. "
|
|
130
|
+
f"Use explicit signature: contract.fn(\"{candidates[0]}\").call(...) "
|
|
131
|
+
f"or contract.fn(\"{candidates[0]}\").transact(...)."
|
|
132
|
+
)
|
|
133
|
+
|
|
134
|
+
|
|
135
|
+
class OverloadMatchError(DXError):
|
|
136
|
+
"""Raised when no overload matches the provided arguments."""
|
|
137
|
+
|
|
138
|
+
def __init__(
|
|
139
|
+
self,
|
|
140
|
+
function_name: str,
|
|
141
|
+
arg_count: int,
|
|
142
|
+
candidates: list[str],
|
|
143
|
+
) -> None:
|
|
144
|
+
self.function_name = function_name
|
|
145
|
+
self.arg_count = arg_count
|
|
146
|
+
self.candidates = candidates
|
|
147
|
+
candidates_str = ", ".join(candidates)
|
|
148
|
+
super().__init__(
|
|
149
|
+
f"No overload of '{function_name}' matches {arg_count} argument(s). "
|
|
150
|
+
f"Available: {candidates_str}. "
|
|
151
|
+
f"Use explicit signature: contract.fn(\"{candidates[0]}\").call(...)."
|
|
152
|
+
)
|
|
153
|
+
|
|
154
|
+
|
|
155
|
+
class FunctionNotFoundError(DXError):
|
|
156
|
+
"""Raised when function is not found in contract ABI."""
|
|
157
|
+
|
|
158
|
+
def __init__(
|
|
159
|
+
self,
|
|
160
|
+
function_name: str,
|
|
161
|
+
address: str,
|
|
162
|
+
available_functions: list[str] | None = None,
|
|
163
|
+
) -> None:
|
|
164
|
+
self.function_name = function_name
|
|
165
|
+
self.address = address
|
|
166
|
+
self.available_functions = available_functions
|
|
167
|
+
if available_functions:
|
|
168
|
+
available_str = ", ".join(available_functions[:10])
|
|
169
|
+
if len(available_functions) > 10:
|
|
170
|
+
available_str += f" ... ({len(available_functions) - 10} more)"
|
|
171
|
+
msg = (
|
|
172
|
+
f"Function '{function_name}' not found in ABI for {address}. "
|
|
173
|
+
f"Available functions: [{available_str}]"
|
|
174
|
+
)
|
|
175
|
+
else:
|
|
176
|
+
msg = f"Function '{function_name}' not found in ABI for {address}."
|
|
177
|
+
super().__init__(msg)
|
|
178
|
+
|
|
179
|
+
|
|
180
|
+
class InvalidAddressError(DXError):
|
|
181
|
+
"""Raised when an invalid Ethereum address is provided."""
|
|
182
|
+
|
|
183
|
+
def __init__(self, address: str) -> None:
|
|
184
|
+
self.address = address
|
|
185
|
+
super().__init__(
|
|
186
|
+
f"Invalid Ethereum address: {address}. "
|
|
187
|
+
f"Address must be a 40-character hex string with 0x prefix."
|
|
188
|
+
)
|
|
189
|
+
|
|
190
|
+
|
|
191
|
+
class EventDecodeError(DXError):
|
|
192
|
+
"""Raised when event log cannot be decoded.
|
|
193
|
+
|
|
194
|
+
This typically happens when the log signature doesn't match
|
|
195
|
+
any known event in the ABI.
|
|
196
|
+
"""
|
|
197
|
+
|
|
198
|
+
def __init__(self, log_index: int, topic0: str | None = None) -> None:
|
|
199
|
+
self.log_index = log_index
|
|
200
|
+
self.topic0 = topic0
|
|
201
|
+
if topic0:
|
|
202
|
+
super().__init__(
|
|
203
|
+
f"Failed to decode event at log index {log_index} "
|
|
204
|
+
f"with topic0 {topic0}. Event may not be in ABI."
|
|
205
|
+
)
|
|
206
|
+
else:
|
|
207
|
+
super().__init__(
|
|
208
|
+
f"Failed to decode event at log index {log_index}. "
|
|
209
|
+
f"Log has no topic0 (anonymous event)."
|
|
210
|
+
)
|
|
211
|
+
|
|
212
|
+
|
|
213
|
+
class ContractCallError(DXError):
|
|
214
|
+
"""Raised when a contract call fails.
|
|
215
|
+
|
|
216
|
+
Wraps underlying RPC errors with context about the call.
|
|
217
|
+
"""
|
|
218
|
+
|
|
219
|
+
def __init__(
|
|
220
|
+
self,
|
|
221
|
+
function_name: str,
|
|
222
|
+
address: str,
|
|
223
|
+
reason: str,
|
|
224
|
+
block_identifier: int | str | None = None,
|
|
225
|
+
signature: str | None = None,
|
|
226
|
+
job_id: str | None = None,
|
|
227
|
+
hook: str | None = None,
|
|
228
|
+
) -> None:
|
|
229
|
+
self.function_name = function_name
|
|
230
|
+
self.address = address
|
|
231
|
+
self.reason = reason
|
|
232
|
+
self.block_identifier = block_identifier
|
|
233
|
+
self.signature = signature
|
|
234
|
+
self.job_id = job_id
|
|
235
|
+
self.hook = hook
|
|
236
|
+
block_str = f" at block {block_identifier}" if block_identifier else ""
|
|
237
|
+
context = _format_context(job_id, hook, address, signature)
|
|
238
|
+
super().__init__(
|
|
239
|
+
f"Call to {function_name}() on {address}{block_str}{context} failed: {reason}"
|
|
240
|
+
)
|
|
241
|
+
|
|
242
|
+
|
|
243
|
+
def _format_context(
|
|
244
|
+
job_id: str | None,
|
|
245
|
+
hook: str | None,
|
|
246
|
+
address: str | None,
|
|
247
|
+
signature: str | None,
|
|
248
|
+
) -> str:
|
|
249
|
+
parts = []
|
|
250
|
+
if job_id:
|
|
251
|
+
parts.append(f"job={job_id}")
|
|
252
|
+
if hook:
|
|
253
|
+
parts.append(f"hook={hook}")
|
|
254
|
+
if signature:
|
|
255
|
+
parts.append(f"sig={signature}")
|
|
256
|
+
if not parts:
|
|
257
|
+
return ""
|
|
258
|
+
return f" ({', '.join(parts)})"
|
|
259
|
+
|
|
260
|
+
|
|
261
|
+
class ABICacheError(DXError):
|
|
262
|
+
"""Raised when ABI cache operations fail."""
|
|
263
|
+
|
|
264
|
+
def __init__(self, operation: str, reason: str) -> None:
|
|
265
|
+
self.operation = operation
|
|
266
|
+
self.reason = reason
|
|
267
|
+
super().__init__(f"ABI cache {operation} failed: {reason}")
|