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
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