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/db/ops/nonces.py
ADDED
|
@@ -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
|