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,322 @@
1
+ """Signer state and nonce reservation operations."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from datetime import datetime
6
+ from uuid import UUID
7
+
8
+ from brawny.db.base_new import Database
9
+ from brawny.db import queries as Q
10
+ from brawny.db import mappers as M
11
+ from brawny.model.types import SignerState, NonceReservation
12
+
13
+
14
+ # =============================================================================
15
+ # Signer State
16
+ # =============================================================================
17
+
18
+
19
+ def get_signer(db: Database, chain_id: int, address: str) -> SignerState | None:
20
+ """Get signer state by chain and address."""
21
+ row = db.fetch_one(Q.GET_SIGNER, {"chain_id": chain_id, "address": address.lower()})
22
+ return M.row_to_signer_state(row) if row else None
23
+
24
+
25
+ def get_all_signers(db: Database, chain_id: int) -> list[SignerState]:
26
+ """Get all signers for a chain."""
27
+ rows = db.fetch_all(Q.LIST_SIGNERS, {"chain_id": chain_id})
28
+ return [M.row_to_signer_state(row) for row in rows]
29
+
30
+
31
+ def upsert_signer(
32
+ db: Database,
33
+ chain_id: int,
34
+ address: str,
35
+ next_nonce: int,
36
+ last_synced_chain_nonce: int,
37
+ ) -> None:
38
+ """Insert or update signer state."""
39
+ db.execute(Q.UPSERT_SIGNER, {
40
+ "chain_id": chain_id,
41
+ "address": address.lower(),
42
+ "next_nonce": next_nonce,
43
+ "last_synced_chain_nonce": last_synced_chain_nonce,
44
+ })
45
+
46
+
47
+ def update_signer_next_nonce(
48
+ db: Database, chain_id: int, address: str, next_nonce: int
49
+ ) -> bool:
50
+ """Update signer's next nonce value."""
51
+ count = db.execute_rowcount(Q.UPDATE_SIGNER_NEXT_NONCE, {
52
+ "chain_id": chain_id,
53
+ "address": address.lower(),
54
+ "next_nonce": next_nonce,
55
+ })
56
+ return count > 0
57
+
58
+
59
+ def update_signer_chain_nonce(
60
+ db: Database, chain_id: int, address: str, chain_nonce: int
61
+ ) -> bool:
62
+ """Update signer's last synced chain nonce."""
63
+ count = db.execute_rowcount(Q.UPDATE_SIGNER_CHAIN_NONCE, {
64
+ "chain_id": chain_id,
65
+ "address": address.lower(),
66
+ "chain_nonce": chain_nonce,
67
+ })
68
+ return count > 0
69
+
70
+
71
+ def set_gap_started_at(
72
+ db: Database, chain_id: int, address: str, started_at: datetime
73
+ ) -> bool:
74
+ """Record when gap blocking started for a signer."""
75
+ count = db.execute_rowcount(Q.SET_GAP_STARTED_AT, {
76
+ "chain_id": chain_id,
77
+ "address": address.lower(),
78
+ "started_at": started_at,
79
+ })
80
+ return count > 0
81
+
82
+
83
+ def clear_gap_started_at(db: Database, chain_id: int, address: str) -> bool:
84
+ """Clear gap blocking timestamp for a signer."""
85
+ count = db.execute_rowcount(Q.CLEAR_GAP_STARTED_AT, {
86
+ "chain_id": chain_id,
87
+ "address": address.lower(),
88
+ })
89
+ return count > 0
90
+
91
+
92
+ def get_signer_by_alias(db: Database, chain_id: int, alias: str) -> SignerState | None:
93
+ """Get signer by alias. Returns None if not found."""
94
+ row = db.fetch_one(Q.GET_SIGNER_BY_ALIAS, {"chain_id": chain_id, "alias": alias})
95
+ return M.row_to_signer_state(row) if row else None
96
+
97
+
98
+ # =============================================================================
99
+ # Nonce Reservations
100
+ # =============================================================================
101
+
102
+
103
+ def get_nonce_reservation(
104
+ db: Database, chain_id: int, address: str, nonce: int
105
+ ) -> NonceReservation | None:
106
+ """Get nonce reservation by chain, address, and nonce."""
107
+ row = db.fetch_one(Q.GET_NONCE_RESERVATION, {
108
+ "chain_id": chain_id,
109
+ "address": address.lower(),
110
+ "nonce": nonce,
111
+ })
112
+ return M.row_to_nonce_reservation(row) if row else None
113
+
114
+
115
+ def get_reservations_for_signer(
116
+ db: Database, chain_id: int, address: str, status: str | None = None
117
+ ) -> list[NonceReservation]:
118
+ """Get all reservations for a signer, optionally filtered by status."""
119
+ if status:
120
+ rows = db.fetch_all(Q.GET_RESERVATIONS_FOR_SIGNER_WITH_STATUS, {
121
+ "chain_id": chain_id,
122
+ "address": address.lower(),
123
+ "status": status,
124
+ })
125
+ else:
126
+ rows = db.fetch_all(Q.GET_RESERVATIONS_FOR_SIGNER, {
127
+ "chain_id": chain_id,
128
+ "address": address.lower(),
129
+ })
130
+ return [M.row_to_nonce_reservation(row) for row in rows]
131
+
132
+
133
+ def get_reservations_below_nonce(
134
+ db: Database, chain_id: int, address: str, nonce: int
135
+ ) -> list[NonceReservation]:
136
+ """Get reservations below a certain nonce."""
137
+ rows = db.fetch_all(Q.GET_RESERVATIONS_BELOW_NONCE, {
138
+ "chain_id": chain_id,
139
+ "address": address.lower(),
140
+ "nonce": nonce,
141
+ })
142
+ return [M.row_to_nonce_reservation(row) for row in rows]
143
+
144
+
145
+ def get_non_released_reservations(
146
+ db: Database,
147
+ chain_id: int,
148
+ address: str,
149
+ base_nonce: int,
150
+ released_status: str = "released",
151
+ ) -> list[NonceReservation]:
152
+ """Get non-released reservations at or above base_nonce."""
153
+ rows = db.fetch_all(Q.GET_NON_RELEASED_RESERVATIONS, {
154
+ "chain_id": chain_id,
155
+ "address": address.lower(),
156
+ "base_nonce": base_nonce,
157
+ "released_status": released_status,
158
+ })
159
+ return [M.row_to_nonce_reservation(row) for row in rows]
160
+
161
+
162
+ def upsert_nonce_reservation(
163
+ db: Database,
164
+ chain_id: int,
165
+ address: str,
166
+ nonce: int,
167
+ status: str,
168
+ intent_id: UUID | None = None,
169
+ ) -> None:
170
+ """Create or update nonce reservation."""
171
+ db.execute(Q.UPSERT_NONCE_RESERVATION, {
172
+ "chain_id": chain_id,
173
+ "address": address.lower(),
174
+ "nonce": nonce,
175
+ "status": status,
176
+ "intent_id": str(intent_id) if intent_id else None,
177
+ })
178
+
179
+
180
+ def update_nonce_reservation_status(
181
+ db: Database,
182
+ chain_id: int,
183
+ address: str,
184
+ nonce: int,
185
+ status: str,
186
+ intent_id: UUID | None = None,
187
+ ) -> bool:
188
+ """Update nonce reservation status, optionally setting intent_id."""
189
+ if intent_id is not None:
190
+ count = db.execute_rowcount(Q.UPDATE_NONCE_RESERVATION_STATUS_WITH_INTENT, {
191
+ "chain_id": chain_id,
192
+ "address": address.lower(),
193
+ "nonce": nonce,
194
+ "status": status,
195
+ "intent_id": str(intent_id),
196
+ })
197
+ else:
198
+ count = db.execute_rowcount(Q.UPDATE_NONCE_RESERVATION_STATUS, {
199
+ "chain_id": chain_id,
200
+ "address": address.lower(),
201
+ "nonce": nonce,
202
+ "status": status,
203
+ })
204
+ return count > 0
205
+
206
+
207
+ def release_nonce_reservation(
208
+ db: Database, chain_id: int, address: str, nonce: int
209
+ ) -> bool:
210
+ """Release a nonce reservation (set status to released)."""
211
+ return update_nonce_reservation_status(
212
+ db, chain_id, address, nonce, status="released"
213
+ )
214
+
215
+
216
+ def cleanup_orphaned_nonces(
217
+ db: Database, chain_id: int, hours: int
218
+ ) -> int:
219
+ """Delete orphaned nonce reservations older than specified hours.
220
+
221
+ Note: Uses dialect-specific query due to interval syntax differences.
222
+ """
223
+ query = Q.CLEANUP_ORPHANED_NONCES[db.dialect]
224
+ if db.dialect == "sqlite":
225
+ # SQLite uses datetime offset syntax
226
+ params = {"chain_id": chain_id, "hours_offset": f"-{hours} hours"}
227
+ else:
228
+ # Postgres uses INTERVAL syntax
229
+ params = {"chain_id": chain_id, "hours": hours}
230
+ return db.execute_rowcount(query, params)
231
+
232
+
233
+ # =============================================================================
234
+ # Atomic Nonce Reservation
235
+ # =============================================================================
236
+
237
+
238
+ def reserve_nonce_atomic(
239
+ db: Database,
240
+ chain_id: int,
241
+ address: str,
242
+ chain_nonce: int | None,
243
+ intent_id: UUID | None = None,
244
+ ) -> int:
245
+ """Reserve a nonce atomically using proper isolation.
246
+
247
+ Uses SERIALIZABLE isolation on Postgres, BEGIN IMMEDIATE on SQLite.
248
+ This is a dialect-specific operation that ensures atomic nonce reservation.
249
+
250
+ Steps:
251
+ 1. Ensure signer row exists
252
+ 2. Lock the signer row (FOR UPDATE on Postgres)
253
+ 3. Find the next available nonce (skipping existing reservations)
254
+ 4. Create the reservation
255
+ 5. Update the signer's next_nonce
256
+
257
+ Args:
258
+ db: Database instance
259
+ chain_id: The chain ID
260
+ address: Signer address
261
+ chain_nonce: Current on-chain nonce (from eth_getTransactionCount)
262
+ intent_id: Optional intent ID to associate with reservation
263
+
264
+ Returns:
265
+ The reserved nonce
266
+
267
+ Raises:
268
+ DatabaseError: If reservation fails or no nonce available within 100 slots
269
+ """
270
+ from brawny.model.errors import DatabaseError
271
+
272
+ address = address.lower()
273
+ isolation = "SERIALIZABLE" if db.dialect == "postgres" else None
274
+
275
+ with db.transaction(isolation_level=isolation):
276
+ # 1. Ensure signer row exists
277
+ db.execute(Q.ENSURE_SIGNER_EXISTS, {
278
+ "chain_id": chain_id,
279
+ "address": address,
280
+ })
281
+
282
+ # 2. Lock the signer row (FOR UPDATE on Postgres, no-op on SQLite)
283
+ lock_query = Q.LOCK_SIGNER_FOR_UPDATE[db.dialect]
284
+ row = db.fetch_one(lock_query, {
285
+ "chain_id": chain_id,
286
+ "address": address,
287
+ })
288
+
289
+ if row is None:
290
+ raise DatabaseError("Failed to lock signer row")
291
+
292
+ db_next_nonce = row["next_nonce"]
293
+ base_nonce = max(db_next_nonce, chain_nonce or db_next_nonce)
294
+
295
+ # 3. Get existing reservations to find gaps
296
+ reservations = get_non_released_reservations(
297
+ db, chain_id, address, base_nonce
298
+ )
299
+
300
+ # Find next available nonce (skip existing reservations)
301
+ candidate = base_nonce
302
+ for res in reservations:
303
+ if res.nonce == candidate:
304
+ candidate += 1
305
+ elif res.nonce > candidate:
306
+ break
307
+
308
+ if candidate - base_nonce > 100:
309
+ raise DatabaseError(
310
+ f"Could not find available nonce within 100 slots for signer {address}"
311
+ )
312
+
313
+ # 4. Create the reservation
314
+ upsert_nonce_reservation(
315
+ db, chain_id, address, candidate, "reserved", intent_id
316
+ )
317
+
318
+ # 5. Update signer's next_nonce
319
+ new_next_nonce = candidate + 1
320
+ update_signer_next_nonce(db, chain_id, address, new_next_nonce)
321
+
322
+ return candidate