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.
Files changed (141) hide show
  1. brawny/__init__.py +106 -0
  2. brawny/_context.py +232 -0
  3. brawny/_rpc/__init__.py +38 -0
  4. brawny/_rpc/broadcast.py +172 -0
  5. brawny/_rpc/clients.py +98 -0
  6. brawny/_rpc/context.py +49 -0
  7. brawny/_rpc/errors.py +252 -0
  8. brawny/_rpc/gas.py +158 -0
  9. brawny/_rpc/manager.py +982 -0
  10. brawny/_rpc/selector.py +156 -0
  11. brawny/accounts.py +534 -0
  12. brawny/alerts/__init__.py +132 -0
  13. brawny/alerts/abi_resolver.py +530 -0
  14. brawny/alerts/base.py +152 -0
  15. brawny/alerts/context.py +271 -0
  16. brawny/alerts/contracts.py +635 -0
  17. brawny/alerts/encoded_call.py +201 -0
  18. brawny/alerts/errors.py +267 -0
  19. brawny/alerts/events.py +680 -0
  20. brawny/alerts/function_caller.py +364 -0
  21. brawny/alerts/health.py +185 -0
  22. brawny/alerts/routing.py +118 -0
  23. brawny/alerts/send.py +364 -0
  24. brawny/api.py +660 -0
  25. brawny/chain.py +93 -0
  26. brawny/cli/__init__.py +16 -0
  27. brawny/cli/app.py +17 -0
  28. brawny/cli/bootstrap.py +37 -0
  29. brawny/cli/commands/__init__.py +41 -0
  30. brawny/cli/commands/abi.py +93 -0
  31. brawny/cli/commands/accounts.py +632 -0
  32. brawny/cli/commands/console.py +495 -0
  33. brawny/cli/commands/contract.py +139 -0
  34. brawny/cli/commands/health.py +112 -0
  35. brawny/cli/commands/init_project.py +86 -0
  36. brawny/cli/commands/intents.py +130 -0
  37. brawny/cli/commands/job_dev.py +254 -0
  38. brawny/cli/commands/jobs.py +308 -0
  39. brawny/cli/commands/logs.py +87 -0
  40. brawny/cli/commands/maintenance.py +182 -0
  41. brawny/cli/commands/migrate.py +51 -0
  42. brawny/cli/commands/networks.py +253 -0
  43. brawny/cli/commands/run.py +249 -0
  44. brawny/cli/commands/script.py +209 -0
  45. brawny/cli/commands/signer.py +248 -0
  46. brawny/cli/helpers.py +265 -0
  47. brawny/cli_templates.py +1445 -0
  48. brawny/config/__init__.py +74 -0
  49. brawny/config/models.py +404 -0
  50. brawny/config/parser.py +633 -0
  51. brawny/config/routing.py +55 -0
  52. brawny/config/validation.py +246 -0
  53. brawny/daemon/__init__.py +14 -0
  54. brawny/daemon/context.py +69 -0
  55. brawny/daemon/core.py +702 -0
  56. brawny/daemon/loops.py +327 -0
  57. brawny/db/__init__.py +78 -0
  58. brawny/db/base.py +986 -0
  59. brawny/db/base_new.py +165 -0
  60. brawny/db/circuit_breaker.py +97 -0
  61. brawny/db/global_cache.py +298 -0
  62. brawny/db/mappers.py +182 -0
  63. brawny/db/migrate.py +349 -0
  64. brawny/db/migrations/001_init.sql +186 -0
  65. brawny/db/migrations/002_add_included_block.sql +7 -0
  66. brawny/db/migrations/003_add_broadcast_at.sql +10 -0
  67. brawny/db/migrations/004_broadcast_binding.sql +20 -0
  68. brawny/db/migrations/005_add_retry_after.sql +9 -0
  69. brawny/db/migrations/006_add_retry_count_column.sql +11 -0
  70. brawny/db/migrations/007_add_gap_tracking.sql +18 -0
  71. brawny/db/migrations/008_add_transactions.sql +72 -0
  72. brawny/db/migrations/009_add_intent_metadata.sql +5 -0
  73. brawny/db/migrations/010_add_nonce_gap_index.sql +9 -0
  74. brawny/db/migrations/011_add_job_logs.sql +24 -0
  75. brawny/db/migrations/012_add_claimed_by.sql +5 -0
  76. brawny/db/ops/__init__.py +29 -0
  77. brawny/db/ops/attempts.py +108 -0
  78. brawny/db/ops/blocks.py +83 -0
  79. brawny/db/ops/cache.py +93 -0
  80. brawny/db/ops/intents.py +296 -0
  81. brawny/db/ops/jobs.py +110 -0
  82. brawny/db/ops/logs.py +97 -0
  83. brawny/db/ops/nonces.py +322 -0
  84. brawny/db/postgres.py +2535 -0
  85. brawny/db/postgres_new.py +196 -0
  86. brawny/db/queries.py +584 -0
  87. brawny/db/sqlite.py +2733 -0
  88. brawny/db/sqlite_new.py +191 -0
  89. brawny/history.py +126 -0
  90. brawny/interfaces.py +136 -0
  91. brawny/invariants.py +155 -0
  92. brawny/jobs/__init__.py +26 -0
  93. brawny/jobs/base.py +287 -0
  94. brawny/jobs/discovery.py +233 -0
  95. brawny/jobs/job_validation.py +111 -0
  96. brawny/jobs/kv.py +125 -0
  97. brawny/jobs/registry.py +283 -0
  98. brawny/keystore.py +484 -0
  99. brawny/lifecycle.py +551 -0
  100. brawny/logging.py +290 -0
  101. brawny/metrics.py +594 -0
  102. brawny/model/__init__.py +53 -0
  103. brawny/model/contexts.py +319 -0
  104. brawny/model/enums.py +70 -0
  105. brawny/model/errors.py +194 -0
  106. brawny/model/events.py +93 -0
  107. brawny/model/startup.py +20 -0
  108. brawny/model/types.py +483 -0
  109. brawny/networks/__init__.py +96 -0
  110. brawny/networks/config.py +269 -0
  111. brawny/networks/manager.py +423 -0
  112. brawny/obs/__init__.py +67 -0
  113. brawny/obs/emit.py +158 -0
  114. brawny/obs/health.py +175 -0
  115. brawny/obs/heartbeat.py +133 -0
  116. brawny/reconciliation.py +108 -0
  117. brawny/scheduler/__init__.py +19 -0
  118. brawny/scheduler/poller.py +472 -0
  119. brawny/scheduler/reorg.py +632 -0
  120. brawny/scheduler/runner.py +708 -0
  121. brawny/scheduler/shutdown.py +371 -0
  122. brawny/script_tx.py +297 -0
  123. brawny/scripting.py +251 -0
  124. brawny/startup.py +76 -0
  125. brawny/telegram.py +393 -0
  126. brawny/testing.py +108 -0
  127. brawny/tx/__init__.py +41 -0
  128. brawny/tx/executor.py +1071 -0
  129. brawny/tx/fees.py +50 -0
  130. brawny/tx/intent.py +423 -0
  131. brawny/tx/monitor.py +628 -0
  132. brawny/tx/nonce.py +498 -0
  133. brawny/tx/replacement.py +456 -0
  134. brawny/tx/utils.py +26 -0
  135. brawny/utils.py +205 -0
  136. brawny/validation.py +69 -0
  137. brawny-0.1.13.dist-info/METADATA +156 -0
  138. brawny-0.1.13.dist-info/RECORD +141 -0
  139. brawny-0.1.13.dist-info/WHEEL +5 -0
  140. brawny-0.1.13.dist-info/entry_points.txt +2 -0
  141. 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)
@@ -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}")