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
brawny/scripting.py
ADDED
|
@@ -0,0 +1,251 @@
|
|
|
1
|
+
"""Scripting utilities for brawny.
|
|
2
|
+
|
|
3
|
+
Provides the @broadcast decorator for enabling transaction broadcasting
|
|
4
|
+
in standalone scripts. Job hooks cannot use @broadcast.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
from __future__ import annotations
|
|
8
|
+
|
|
9
|
+
import functools
|
|
10
|
+
import threading
|
|
11
|
+
from dataclasses import dataclass
|
|
12
|
+
from typing import TYPE_CHECKING, Any, Callable, TypeVar
|
|
13
|
+
|
|
14
|
+
if TYPE_CHECKING:
|
|
15
|
+
from brawny.alerts.contracts import ContractSystem
|
|
16
|
+
from brawny.keystore import Keystore
|
|
17
|
+
|
|
18
|
+
# Thread-local context for broadcast and job execution state
|
|
19
|
+
_context = threading.local()
|
|
20
|
+
|
|
21
|
+
F = TypeVar("F", bound=Callable[..., Any])
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
# =============================================================================
|
|
25
|
+
# Context Management
|
|
26
|
+
# =============================================================================
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
def broadcast_enabled() -> bool:
|
|
30
|
+
"""Check if broadcast context is active.
|
|
31
|
+
|
|
32
|
+
Returns:
|
|
33
|
+
True if inside a @broadcast decorated function
|
|
34
|
+
"""
|
|
35
|
+
return getattr(_context, "broadcast_active", False)
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
def in_job_context() -> bool:
|
|
39
|
+
"""Check if currently executing inside a job hook.
|
|
40
|
+
|
|
41
|
+
Returns:
|
|
42
|
+
True if inside a job hook (check, build_intent, alert hooks)
|
|
43
|
+
"""
|
|
44
|
+
return getattr(_context, "job_active", False)
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
def set_job_context(active: bool) -> None:
|
|
48
|
+
"""Set the job execution context flag.
|
|
49
|
+
|
|
50
|
+
Called by the job runner before/after executing job hooks.
|
|
51
|
+
|
|
52
|
+
Args:
|
|
53
|
+
active: True when entering job hook, False when exiting
|
|
54
|
+
"""
|
|
55
|
+
_context.job_active = active
|
|
56
|
+
|
|
57
|
+
|
|
58
|
+
def get_broadcast_context() -> BroadcastContext | None:
|
|
59
|
+
"""Get the current broadcast context if active.
|
|
60
|
+
|
|
61
|
+
Returns:
|
|
62
|
+
BroadcastContext or None if not in broadcast mode
|
|
63
|
+
"""
|
|
64
|
+
return getattr(_context, "broadcast_context", None)
|
|
65
|
+
|
|
66
|
+
|
|
67
|
+
# =============================================================================
|
|
68
|
+
# Errors
|
|
69
|
+
# =============================================================================
|
|
70
|
+
|
|
71
|
+
|
|
72
|
+
class BroadcastNotAllowedError(Exception):
|
|
73
|
+
"""Raised when .transact() is called outside @broadcast context."""
|
|
74
|
+
|
|
75
|
+
def __init__(self, function_name: str, reason: str | None = None) -> None:
|
|
76
|
+
self.function_name = function_name
|
|
77
|
+
self.reason = reason or "not inside @broadcast context"
|
|
78
|
+
super().__init__(
|
|
79
|
+
f"Cannot broadcast '{function_name}': {self.reason}. "
|
|
80
|
+
f"Use @broadcast decorator to enable transaction broadcasting."
|
|
81
|
+
)
|
|
82
|
+
|
|
83
|
+
|
|
84
|
+
class SignerNotFoundError(Exception):
|
|
85
|
+
"""Raised when the 'from' address cannot be resolved to a signer."""
|
|
86
|
+
|
|
87
|
+
def __init__(self, signer: str) -> None:
|
|
88
|
+
self.signer = signer
|
|
89
|
+
super().__init__(
|
|
90
|
+
f"Signer '{signer}' not found in keystore. "
|
|
91
|
+
f"Provide a valid wallet name or address."
|
|
92
|
+
)
|
|
93
|
+
|
|
94
|
+
|
|
95
|
+
class TransactionRevertedError(Exception):
|
|
96
|
+
"""Raised when a broadcasted transaction reverts on-chain."""
|
|
97
|
+
|
|
98
|
+
def __init__(self, tx_hash: str, reason: str | None = None) -> None:
|
|
99
|
+
self.tx_hash = tx_hash
|
|
100
|
+
self.reason = reason
|
|
101
|
+
msg = f"Transaction {tx_hash} reverted"
|
|
102
|
+
if reason:
|
|
103
|
+
msg += f": {reason}"
|
|
104
|
+
super().__init__(msg)
|
|
105
|
+
|
|
106
|
+
|
|
107
|
+
class TransactionTimeoutError(Exception):
|
|
108
|
+
"""Raised when waiting for a transaction receipt times out."""
|
|
109
|
+
|
|
110
|
+
def __init__(self, tx_hash: str, timeout_seconds: int) -> None:
|
|
111
|
+
self.tx_hash = tx_hash
|
|
112
|
+
self.timeout_seconds = timeout_seconds
|
|
113
|
+
super().__init__(
|
|
114
|
+
f"Timeout waiting for transaction {tx_hash} after {timeout_seconds}s"
|
|
115
|
+
)
|
|
116
|
+
|
|
117
|
+
|
|
118
|
+
# =============================================================================
|
|
119
|
+
# Broadcast Context
|
|
120
|
+
# =============================================================================
|
|
121
|
+
|
|
122
|
+
|
|
123
|
+
@dataclass
|
|
124
|
+
class BroadcastContext:
|
|
125
|
+
"""Context object available inside @broadcast decorated functions.
|
|
126
|
+
|
|
127
|
+
Provides access to contract system and broadcast configuration.
|
|
128
|
+
"""
|
|
129
|
+
|
|
130
|
+
system: ContractSystem
|
|
131
|
+
keystore: "Keystore | None" = None
|
|
132
|
+
timeout_seconds: int = 120
|
|
133
|
+
poll_interval_seconds: float = 2.0
|
|
134
|
+
|
|
135
|
+
def contract(self, address: str, abi: list[dict[str, Any]] | None = None):
|
|
136
|
+
"""Get a contract handle for the given address.
|
|
137
|
+
|
|
138
|
+
Prefer using Contract() from brawny instead:
|
|
139
|
+
from brawny import Contract
|
|
140
|
+
vault = Contract("0x...")
|
|
141
|
+
|
|
142
|
+
Args:
|
|
143
|
+
address: Contract address
|
|
144
|
+
abi: Optional ABI (resolved automatically if not provided)
|
|
145
|
+
|
|
146
|
+
Returns:
|
|
147
|
+
ContractHandle for interacting with the contract
|
|
148
|
+
"""
|
|
149
|
+
return self.system.handle(address=address, abi=abi)
|
|
150
|
+
|
|
151
|
+
|
|
152
|
+
# =============================================================================
|
|
153
|
+
# Broadcast Decorator
|
|
154
|
+
# =============================================================================
|
|
155
|
+
|
|
156
|
+
|
|
157
|
+
def broadcast(
|
|
158
|
+
system: ContractSystem | None = None,
|
|
159
|
+
keystore: "Keystore | None" = None,
|
|
160
|
+
timeout_seconds: int = 120,
|
|
161
|
+
poll_interval_seconds: float = 2.0,
|
|
162
|
+
) -> Callable[[F], F]:
|
|
163
|
+
"""Decorator to enable transaction broadcasting in a script.
|
|
164
|
+
|
|
165
|
+
The decorated function receives a BroadcastContext as its first argument,
|
|
166
|
+
which provides access to contract handles that can use .transact().
|
|
167
|
+
|
|
168
|
+
Usage:
|
|
169
|
+
from brawny import Contract
|
|
170
|
+
|
|
171
|
+
@broadcast(system=my_system)
|
|
172
|
+
def run(ctx):
|
|
173
|
+
vault = Contract("0x...")
|
|
174
|
+
receipt = vault.harvest.transact({"from": "yearn-worker"})
|
|
175
|
+
return receipt
|
|
176
|
+
|
|
177
|
+
Args:
|
|
178
|
+
system: ContractSystem instance for contract resolution and RPC access
|
|
179
|
+
keystore: Keystore instance for signing
|
|
180
|
+
timeout_seconds: Max time to wait for transaction receipts (default: 120)
|
|
181
|
+
poll_interval_seconds: Interval between receipt checks (default: 2.0)
|
|
182
|
+
|
|
183
|
+
Raises:
|
|
184
|
+
BroadcastNotAllowedError: If called from within a job hook
|
|
185
|
+
ValueError: If system is not provided
|
|
186
|
+
"""
|
|
187
|
+
|
|
188
|
+
def decorator(func: F) -> F:
|
|
189
|
+
@functools.wraps(func)
|
|
190
|
+
def wrapper(*args: Any, **kwargs: Any) -> Any:
|
|
191
|
+
# Check if we're inside a job hook
|
|
192
|
+
if in_job_context():
|
|
193
|
+
raise BroadcastNotAllowedError(
|
|
194
|
+
func.__name__,
|
|
195
|
+
reason="@broadcast cannot be used inside job hooks",
|
|
196
|
+
)
|
|
197
|
+
|
|
198
|
+
# Require system to be provided
|
|
199
|
+
nonlocal system
|
|
200
|
+
if system is None:
|
|
201
|
+
raise ValueError(
|
|
202
|
+
"@broadcast requires a ContractSystem. "
|
|
203
|
+
"Use @broadcast(system=my_system)"
|
|
204
|
+
)
|
|
205
|
+
if keystore is None:
|
|
206
|
+
raise ValueError(
|
|
207
|
+
"@broadcast requires a Keystore for signing. "
|
|
208
|
+
"Use @broadcast(system=my_system, keystore=my_keystore)"
|
|
209
|
+
)
|
|
210
|
+
|
|
211
|
+
# Create broadcast context
|
|
212
|
+
ctx = BroadcastContext(
|
|
213
|
+
system=system,
|
|
214
|
+
keystore=keystore,
|
|
215
|
+
timeout_seconds=timeout_seconds,
|
|
216
|
+
poll_interval_seconds=poll_interval_seconds,
|
|
217
|
+
)
|
|
218
|
+
|
|
219
|
+
# Set thread-local flags
|
|
220
|
+
previous_broadcast = getattr(_context, "broadcast_active", False)
|
|
221
|
+
previous_context = getattr(_context, "broadcast_context", None)
|
|
222
|
+
|
|
223
|
+
try:
|
|
224
|
+
_context.broadcast_active = True
|
|
225
|
+
_context.broadcast_context = ctx
|
|
226
|
+
|
|
227
|
+
# Call function with context as first argument
|
|
228
|
+
return func(ctx, *args, **kwargs)
|
|
229
|
+
|
|
230
|
+
finally:
|
|
231
|
+
# Restore previous state
|
|
232
|
+
_context.broadcast_active = previous_broadcast
|
|
233
|
+
_context.broadcast_context = previous_context
|
|
234
|
+
|
|
235
|
+
return wrapper # type: ignore
|
|
236
|
+
|
|
237
|
+
return decorator
|
|
238
|
+
|
|
239
|
+
|
|
240
|
+
__all__ = [
|
|
241
|
+
"broadcast",
|
|
242
|
+
"broadcast_enabled",
|
|
243
|
+
"in_job_context",
|
|
244
|
+
"set_job_context",
|
|
245
|
+
"get_broadcast_context",
|
|
246
|
+
"BroadcastContext",
|
|
247
|
+
"BroadcastNotAllowedError",
|
|
248
|
+
"SignerNotFoundError",
|
|
249
|
+
"TransactionRevertedError",
|
|
250
|
+
"TransactionTimeoutError",
|
|
251
|
+
]
|
brawny/startup.py
ADDED
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
"""Startup reconciliation helpers."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from typing import TYPE_CHECKING
|
|
6
|
+
|
|
7
|
+
from brawny.model.enums import IntentStatus
|
|
8
|
+
from brawny.reconciliation import reconcile_startup, ReconciliationStats
|
|
9
|
+
from brawny.tx.monitor import ConfirmationResult
|
|
10
|
+
|
|
11
|
+
if TYPE_CHECKING:
|
|
12
|
+
from brawny.db.base import Database
|
|
13
|
+
from brawny.tx.monitor import TxMonitor
|
|
14
|
+
import structlog
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
def reconcile_pending_intents(
|
|
18
|
+
db: Database,
|
|
19
|
+
monitor: TxMonitor,
|
|
20
|
+
chain_id: int,
|
|
21
|
+
logger: "structlog.stdlib.BoundLogger",
|
|
22
|
+
) -> int:
|
|
23
|
+
"""Reconcile pending intents at startup."""
|
|
24
|
+
pending_intents = db.get_intents_by_status(
|
|
25
|
+
IntentStatus.PENDING.value,
|
|
26
|
+
chain_id=chain_id,
|
|
27
|
+
)
|
|
28
|
+
reconciled = 0
|
|
29
|
+
for intent in pending_intents:
|
|
30
|
+
attempt = db.get_latest_attempt_for_intent(intent.intent_id)
|
|
31
|
+
if not attempt or not attempt.tx_hash:
|
|
32
|
+
continue
|
|
33
|
+
status = monitor.check_confirmation(intent, attempt)
|
|
34
|
+
if status.result == ConfirmationResult.CONFIRMED:
|
|
35
|
+
monitor.handle_confirmed(intent, attempt, status)
|
|
36
|
+
reconciled += 1
|
|
37
|
+
elif status.result == ConfirmationResult.REVERTED:
|
|
38
|
+
monitor.handle_reverted(intent, attempt, status)
|
|
39
|
+
reconciled += 1
|
|
40
|
+
elif status.result == ConfirmationResult.DROPPED:
|
|
41
|
+
monitor.handle_dropped(intent, attempt)
|
|
42
|
+
reconciled += 1
|
|
43
|
+
|
|
44
|
+
if reconciled > 0:
|
|
45
|
+
logger.info(
|
|
46
|
+
"startup.reconcile_pending",
|
|
47
|
+
reconciled=reconciled,
|
|
48
|
+
)
|
|
49
|
+
return reconciled
|
|
50
|
+
|
|
51
|
+
|
|
52
|
+
def run_startup_reconciliation(
|
|
53
|
+
db: Database,
|
|
54
|
+
chain_id: int,
|
|
55
|
+
logger: "structlog.stdlib.BoundLogger",
|
|
56
|
+
) -> ReconciliationStats:
|
|
57
|
+
"""Run general state reconciliation at startup.
|
|
58
|
+
|
|
59
|
+
This complements reconcile_pending_intents by handling:
|
|
60
|
+
- Orphaned claims (status != claimed but claim_token set)
|
|
61
|
+
- Orphaned nonces (reserved but intent is terminal)
|
|
62
|
+
- Detecting pending intents without attempts
|
|
63
|
+
- Detecting stale claims
|
|
64
|
+
|
|
65
|
+
Args:
|
|
66
|
+
db: Database connection
|
|
67
|
+
chain_id: Chain ID to reconcile
|
|
68
|
+
logger: Logger instance
|
|
69
|
+
|
|
70
|
+
Returns:
|
|
71
|
+
Statistics from the reconciliation run
|
|
72
|
+
"""
|
|
73
|
+
logger.info("startup.reconciliation_starting", chain_id=chain_id)
|
|
74
|
+
stats = reconcile_startup(db, chain_id)
|
|
75
|
+
logger.info("startup.reconciliation_complete", chain_id=chain_id)
|
|
76
|
+
return stats
|