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/base.py
ADDED
|
@@ -0,0 +1,986 @@
|
|
|
1
|
+
"""Database abstraction layer for brawny.
|
|
2
|
+
|
|
3
|
+
Provides a unified interface for both PostgreSQL and SQLite backends.
|
|
4
|
+
"""
|
|
5
|
+
|
|
6
|
+
from __future__ import annotations
|
|
7
|
+
|
|
8
|
+
from abc import ABC, abstractmethod
|
|
9
|
+
from contextlib import contextmanager
|
|
10
|
+
from dataclasses import dataclass
|
|
11
|
+
from datetime import datetime
|
|
12
|
+
from typing import TYPE_CHECKING, Any, Iterator, Literal
|
|
13
|
+
from uuid import UUID
|
|
14
|
+
|
|
15
|
+
if TYPE_CHECKING:
|
|
16
|
+
from brawny.model.errors import ErrorInfo, FailureType
|
|
17
|
+
from brawny.model.types import (
|
|
18
|
+
BroadcastInfo,
|
|
19
|
+
GasParams,
|
|
20
|
+
JobConfig,
|
|
21
|
+
NonceReservation,
|
|
22
|
+
SignerState,
|
|
23
|
+
Transaction,
|
|
24
|
+
TxAttempt,
|
|
25
|
+
TxIntent,
|
|
26
|
+
)
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
IsolationLevel = Literal["SERIALIZABLE", "REPEATABLE READ", "READ COMMITTED", "READ UNCOMMITTED"]
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
@dataclass
|
|
33
|
+
class BlockState:
|
|
34
|
+
"""Block processing state."""
|
|
35
|
+
|
|
36
|
+
chain_id: int
|
|
37
|
+
last_processed_block_number: int
|
|
38
|
+
last_processed_block_hash: str
|
|
39
|
+
created_at: datetime
|
|
40
|
+
updated_at: datetime
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
@dataclass
|
|
44
|
+
class BlockHashEntry:
|
|
45
|
+
"""Block hash history entry for reorg detection."""
|
|
46
|
+
|
|
47
|
+
id: int
|
|
48
|
+
chain_id: int
|
|
49
|
+
block_number: int
|
|
50
|
+
block_hash: str
|
|
51
|
+
inserted_at: datetime
|
|
52
|
+
|
|
53
|
+
|
|
54
|
+
@dataclass
|
|
55
|
+
class ABICacheEntry:
|
|
56
|
+
"""Cached ABI entry."""
|
|
57
|
+
|
|
58
|
+
chain_id: int
|
|
59
|
+
address: str
|
|
60
|
+
abi_json: str
|
|
61
|
+
source: str
|
|
62
|
+
resolved_at: datetime
|
|
63
|
+
|
|
64
|
+
|
|
65
|
+
@dataclass
|
|
66
|
+
class ProxyCacheEntry:
|
|
67
|
+
"""Cached proxy resolution entry."""
|
|
68
|
+
|
|
69
|
+
chain_id: int
|
|
70
|
+
proxy_address: str
|
|
71
|
+
implementation_address: str
|
|
72
|
+
resolved_at: datetime
|
|
73
|
+
|
|
74
|
+
|
|
75
|
+
class Database(ABC):
|
|
76
|
+
"""Abstract database interface.
|
|
77
|
+
|
|
78
|
+
Implementations must provide thread-safe connection management
|
|
79
|
+
and proper transaction isolation.
|
|
80
|
+
"""
|
|
81
|
+
|
|
82
|
+
@abstractmethod
|
|
83
|
+
def connect(self) -> None:
|
|
84
|
+
"""Establish database connection."""
|
|
85
|
+
...
|
|
86
|
+
|
|
87
|
+
@abstractmethod
|
|
88
|
+
def close(self) -> None:
|
|
89
|
+
"""Close database connection and cleanup resources."""
|
|
90
|
+
...
|
|
91
|
+
|
|
92
|
+
@abstractmethod
|
|
93
|
+
def is_connected(self) -> bool:
|
|
94
|
+
"""Check if database is connected."""
|
|
95
|
+
...
|
|
96
|
+
|
|
97
|
+
@abstractmethod
|
|
98
|
+
@contextmanager
|
|
99
|
+
def transaction(
|
|
100
|
+
self, isolation_level: IsolationLevel | None = None
|
|
101
|
+
) -> Iterator[None]:
|
|
102
|
+
"""Context manager for database transactions.
|
|
103
|
+
|
|
104
|
+
Args:
|
|
105
|
+
isolation_level: Optional isolation level override
|
|
106
|
+
"""
|
|
107
|
+
...
|
|
108
|
+
|
|
109
|
+
@abstractmethod
|
|
110
|
+
def execute(
|
|
111
|
+
self,
|
|
112
|
+
query: str,
|
|
113
|
+
params: tuple[Any, ...] | dict[str, Any] | None = None,
|
|
114
|
+
) -> None:
|
|
115
|
+
"""Execute a query without returning results."""
|
|
116
|
+
...
|
|
117
|
+
|
|
118
|
+
@abstractmethod
|
|
119
|
+
def execute_returning(
|
|
120
|
+
self,
|
|
121
|
+
query: str,
|
|
122
|
+
params: tuple[Any, ...] | dict[str, Any] | None = None,
|
|
123
|
+
) -> list[dict[str, Any]]:
|
|
124
|
+
"""Execute a query and return all results as dicts."""
|
|
125
|
+
...
|
|
126
|
+
|
|
127
|
+
@abstractmethod
|
|
128
|
+
def execute_one(
|
|
129
|
+
self,
|
|
130
|
+
query: str,
|
|
131
|
+
params: tuple[Any, ...] | dict[str, Any] | None = None,
|
|
132
|
+
) -> dict[str, Any] | None:
|
|
133
|
+
"""Execute a query and return a single result or None."""
|
|
134
|
+
...
|
|
135
|
+
|
|
136
|
+
@abstractmethod
|
|
137
|
+
def execute_returning_rowcount(
|
|
138
|
+
self,
|
|
139
|
+
query: str,
|
|
140
|
+
params: tuple[Any, ...] | dict[str, Any] | None = None,
|
|
141
|
+
) -> int:
|
|
142
|
+
"""Execute a query and return affected rowcount."""
|
|
143
|
+
...
|
|
144
|
+
|
|
145
|
+
# =========================================================================
|
|
146
|
+
# Block State Operations
|
|
147
|
+
# =========================================================================
|
|
148
|
+
|
|
149
|
+
@abstractmethod
|
|
150
|
+
def get_block_state(self, chain_id: int) -> BlockState | None:
|
|
151
|
+
"""Get the current block processing state."""
|
|
152
|
+
...
|
|
153
|
+
|
|
154
|
+
@abstractmethod
|
|
155
|
+
def upsert_block_state(
|
|
156
|
+
self,
|
|
157
|
+
chain_id: int,
|
|
158
|
+
block_number: int,
|
|
159
|
+
block_hash: str,
|
|
160
|
+
) -> None:
|
|
161
|
+
"""Update or insert block processing state."""
|
|
162
|
+
...
|
|
163
|
+
|
|
164
|
+
@abstractmethod
|
|
165
|
+
def get_block_hash_at_height(
|
|
166
|
+
self, chain_id: int, block_number: int
|
|
167
|
+
) -> str | None:
|
|
168
|
+
"""Get stored block hash at a specific height."""
|
|
169
|
+
...
|
|
170
|
+
|
|
171
|
+
@abstractmethod
|
|
172
|
+
def insert_block_hash(
|
|
173
|
+
self, chain_id: int, block_number: int, block_hash: str
|
|
174
|
+
) -> None:
|
|
175
|
+
"""Insert a block hash into history."""
|
|
176
|
+
...
|
|
177
|
+
|
|
178
|
+
@abstractmethod
|
|
179
|
+
def delete_block_hashes_above(self, chain_id: int, block_number: int) -> int:
|
|
180
|
+
"""Delete block hashes above a certain height (for reorg rewind)."""
|
|
181
|
+
...
|
|
182
|
+
|
|
183
|
+
@abstractmethod
|
|
184
|
+
def delete_block_hash_at_height(self, chain_id: int, block_number: int) -> bool:
|
|
185
|
+
"""Delete a specific block hash (for stale hash cleanup)."""
|
|
186
|
+
...
|
|
187
|
+
|
|
188
|
+
@abstractmethod
|
|
189
|
+
def cleanup_old_block_hashes(self, chain_id: int, keep_count: int) -> int:
|
|
190
|
+
"""Delete old block hashes beyond the history window."""
|
|
191
|
+
...
|
|
192
|
+
|
|
193
|
+
@abstractmethod
|
|
194
|
+
def get_oldest_block_in_history(self, chain_id: int) -> int | None:
|
|
195
|
+
"""Get the oldest block number in hash history."""
|
|
196
|
+
...
|
|
197
|
+
|
|
198
|
+
@abstractmethod
|
|
199
|
+
def get_latest_block_in_history(self, chain_id: int) -> int | None:
|
|
200
|
+
"""Get the newest block number in hash history."""
|
|
201
|
+
...
|
|
202
|
+
|
|
203
|
+
@abstractmethod
|
|
204
|
+
def get_inflight_intent_count(
|
|
205
|
+
self, chain_id: int, job_id: str, signer_address: str
|
|
206
|
+
) -> int:
|
|
207
|
+
"""Count inflight intents (created/claimed/sending/pending) for job+signer."""
|
|
208
|
+
...
|
|
209
|
+
|
|
210
|
+
@abstractmethod
|
|
211
|
+
def get_inflight_intents_for_scope(
|
|
212
|
+
self,
|
|
213
|
+
chain_id: int,
|
|
214
|
+
job_id: str,
|
|
215
|
+
signer_address: str,
|
|
216
|
+
to_address: str,
|
|
217
|
+
) -> list[dict[str, Any]]:
|
|
218
|
+
"""List inflight intents (created/claimed/sending/pending) for job+signer+to."""
|
|
219
|
+
...
|
|
220
|
+
|
|
221
|
+
# =========================================================================
|
|
222
|
+
# Job Operations
|
|
223
|
+
# =========================================================================
|
|
224
|
+
|
|
225
|
+
@abstractmethod
|
|
226
|
+
def get_job(self, job_id: str) -> JobConfig | None:
|
|
227
|
+
"""Get job configuration by ID."""
|
|
228
|
+
...
|
|
229
|
+
|
|
230
|
+
@abstractmethod
|
|
231
|
+
def get_enabled_jobs(self) -> list[JobConfig]:
|
|
232
|
+
"""Get all enabled jobs ordered by job_id."""
|
|
233
|
+
...
|
|
234
|
+
|
|
235
|
+
@abstractmethod
|
|
236
|
+
def list_all_jobs(self) -> list[JobConfig]:
|
|
237
|
+
"""Get all jobs (enabled and disabled) ordered by job_id."""
|
|
238
|
+
...
|
|
239
|
+
|
|
240
|
+
@abstractmethod
|
|
241
|
+
def upsert_job(
|
|
242
|
+
self,
|
|
243
|
+
job_id: str,
|
|
244
|
+
job_name: str,
|
|
245
|
+
check_interval_blocks: int,
|
|
246
|
+
enabled: bool = True,
|
|
247
|
+
) -> None:
|
|
248
|
+
"""Insert or update job configuration."""
|
|
249
|
+
...
|
|
250
|
+
|
|
251
|
+
@abstractmethod
|
|
252
|
+
def update_job_checked(
|
|
253
|
+
self, job_id: str, block_number: int, triggered: bool = False
|
|
254
|
+
) -> None:
|
|
255
|
+
"""Update job's last checked/triggered block numbers."""
|
|
256
|
+
...
|
|
257
|
+
|
|
258
|
+
@abstractmethod
|
|
259
|
+
def set_job_enabled(self, job_id: str, enabled: bool) -> bool:
|
|
260
|
+
"""Enable or disable a job. Returns True if job exists."""
|
|
261
|
+
...
|
|
262
|
+
|
|
263
|
+
@abstractmethod
|
|
264
|
+
def delete_job(self, job_id: str) -> bool:
|
|
265
|
+
"""Delete a job from the database. Returns True if job existed."""
|
|
266
|
+
...
|
|
267
|
+
|
|
268
|
+
@abstractmethod
|
|
269
|
+
def get_job_kv(self, job_id: str, key: str) -> Any | None:
|
|
270
|
+
"""Get a value from job's key-value store."""
|
|
271
|
+
...
|
|
272
|
+
|
|
273
|
+
@abstractmethod
|
|
274
|
+
def set_job_kv(self, job_id: str, key: str, value: Any) -> None:
|
|
275
|
+
"""Set a value in job's key-value store."""
|
|
276
|
+
...
|
|
277
|
+
|
|
278
|
+
@abstractmethod
|
|
279
|
+
def delete_job_kv(self, job_id: str, key: str) -> bool:
|
|
280
|
+
"""Delete a key from job's key-value store."""
|
|
281
|
+
...
|
|
282
|
+
|
|
283
|
+
# =========================================================================
|
|
284
|
+
# Signer & Nonce Operations
|
|
285
|
+
# =========================================================================
|
|
286
|
+
|
|
287
|
+
@abstractmethod
|
|
288
|
+
def get_signer_state(self, chain_id: int, address: str) -> SignerState | None:
|
|
289
|
+
"""Get signer state including next nonce."""
|
|
290
|
+
...
|
|
291
|
+
|
|
292
|
+
@abstractmethod
|
|
293
|
+
def get_all_signers(self, chain_id: int) -> list[SignerState]:
|
|
294
|
+
"""Get all signers for a chain."""
|
|
295
|
+
...
|
|
296
|
+
|
|
297
|
+
@abstractmethod
|
|
298
|
+
def upsert_signer(
|
|
299
|
+
self,
|
|
300
|
+
chain_id: int,
|
|
301
|
+
address: str,
|
|
302
|
+
next_nonce: int,
|
|
303
|
+
last_synced_chain_nonce: int | None = None,
|
|
304
|
+
) -> None:
|
|
305
|
+
"""Insert or update signer state."""
|
|
306
|
+
...
|
|
307
|
+
|
|
308
|
+
@abstractmethod
|
|
309
|
+
def update_signer_next_nonce(
|
|
310
|
+
self, chain_id: int, address: str, next_nonce: int
|
|
311
|
+
) -> None:
|
|
312
|
+
"""Update signer's next nonce value."""
|
|
313
|
+
...
|
|
314
|
+
|
|
315
|
+
@abstractmethod
|
|
316
|
+
def update_signer_chain_nonce(
|
|
317
|
+
self, chain_id: int, address: str, chain_nonce: int
|
|
318
|
+
) -> None:
|
|
319
|
+
"""Update signer's last synced chain nonce."""
|
|
320
|
+
...
|
|
321
|
+
|
|
322
|
+
@abstractmethod
|
|
323
|
+
def set_gap_started_at(
|
|
324
|
+
self, chain_id: int, address: str, started_at: datetime
|
|
325
|
+
) -> None:
|
|
326
|
+
"""Record when gap blocking started for a signer."""
|
|
327
|
+
...
|
|
328
|
+
|
|
329
|
+
@abstractmethod
|
|
330
|
+
def clear_gap_started_at(self, chain_id: int, address: str) -> None:
|
|
331
|
+
"""Clear gap tracking (gap resolved or force reset)."""
|
|
332
|
+
...
|
|
333
|
+
|
|
334
|
+
@abstractmethod
|
|
335
|
+
def get_signer_by_alias(self, chain_id: int, alias: str) -> SignerState | None:
|
|
336
|
+
"""Get signer by alias. Returns None if not found."""
|
|
337
|
+
...
|
|
338
|
+
|
|
339
|
+
@abstractmethod
|
|
340
|
+
def reserve_nonce_atomic(
|
|
341
|
+
self,
|
|
342
|
+
chain_id: int,
|
|
343
|
+
address: str,
|
|
344
|
+
chain_nonce: int | None,
|
|
345
|
+
intent_id: UUID | None = None,
|
|
346
|
+
) -> int:
|
|
347
|
+
"""Atomically reserve the next available nonce for a signer."""
|
|
348
|
+
...
|
|
349
|
+
|
|
350
|
+
@abstractmethod
|
|
351
|
+
def get_nonce_reservation(
|
|
352
|
+
self, chain_id: int, address: str, nonce: int
|
|
353
|
+
) -> NonceReservation | None:
|
|
354
|
+
"""Get a specific nonce reservation."""
|
|
355
|
+
...
|
|
356
|
+
|
|
357
|
+
@abstractmethod
|
|
358
|
+
def get_reservations_for_signer(
|
|
359
|
+
self, chain_id: int, address: str, status: str | None = None
|
|
360
|
+
) -> list[NonceReservation]:
|
|
361
|
+
"""Get all reservations for a signer, optionally filtered by status."""
|
|
362
|
+
...
|
|
363
|
+
|
|
364
|
+
@abstractmethod
|
|
365
|
+
def get_reservations_below_nonce(
|
|
366
|
+
self, chain_id: int, address: str, nonce: int
|
|
367
|
+
) -> list[NonceReservation]:
|
|
368
|
+
"""Get reservations with nonce less than given value."""
|
|
369
|
+
...
|
|
370
|
+
|
|
371
|
+
@abstractmethod
|
|
372
|
+
def create_nonce_reservation(
|
|
373
|
+
self,
|
|
374
|
+
chain_id: int,
|
|
375
|
+
address: str,
|
|
376
|
+
nonce: int,
|
|
377
|
+
status: str = "reserved",
|
|
378
|
+
intent_id: UUID | None = None,
|
|
379
|
+
) -> NonceReservation:
|
|
380
|
+
"""Create a new nonce reservation."""
|
|
381
|
+
...
|
|
382
|
+
|
|
383
|
+
@abstractmethod
|
|
384
|
+
def update_nonce_reservation_status(
|
|
385
|
+
self,
|
|
386
|
+
chain_id: int,
|
|
387
|
+
address: str,
|
|
388
|
+
nonce: int,
|
|
389
|
+
status: str,
|
|
390
|
+
intent_id: UUID | None = None,
|
|
391
|
+
) -> bool:
|
|
392
|
+
"""Update nonce reservation status. Returns True if updated."""
|
|
393
|
+
...
|
|
394
|
+
|
|
395
|
+
@abstractmethod
|
|
396
|
+
def release_nonce_reservation(
|
|
397
|
+
self, chain_id: int, address: str, nonce: int
|
|
398
|
+
) -> bool:
|
|
399
|
+
"""Release (mark as released) a nonce reservation."""
|
|
400
|
+
...
|
|
401
|
+
|
|
402
|
+
@abstractmethod
|
|
403
|
+
def cleanup_orphaned_nonces(
|
|
404
|
+
self, chain_id: int, older_than_hours: int = 24
|
|
405
|
+
) -> int:
|
|
406
|
+
"""Delete orphaned nonce reservations older than specified hours.
|
|
407
|
+
|
|
408
|
+
Args:
|
|
409
|
+
chain_id: Chain ID to cleanup
|
|
410
|
+
older_than_hours: Delete orphaned reservations older than this (default: 24h)
|
|
411
|
+
|
|
412
|
+
Returns:
|
|
413
|
+
Number of deleted reservations
|
|
414
|
+
"""
|
|
415
|
+
...
|
|
416
|
+
|
|
417
|
+
# =========================================================================
|
|
418
|
+
# Intent Operations
|
|
419
|
+
# =========================================================================
|
|
420
|
+
|
|
421
|
+
@abstractmethod
|
|
422
|
+
def create_intent(
|
|
423
|
+
self,
|
|
424
|
+
intent_id: UUID,
|
|
425
|
+
job_id: str,
|
|
426
|
+
chain_id: int,
|
|
427
|
+
signer_address: str,
|
|
428
|
+
idempotency_key: str,
|
|
429
|
+
to_address: str,
|
|
430
|
+
data: str | None,
|
|
431
|
+
value_wei: str,
|
|
432
|
+
gas_limit: int | None,
|
|
433
|
+
max_fee_per_gas: str | None,
|
|
434
|
+
max_priority_fee_per_gas: str | None,
|
|
435
|
+
min_confirmations: int,
|
|
436
|
+
deadline_ts: datetime | None,
|
|
437
|
+
broadcast_group: str | None = None,
|
|
438
|
+
broadcast_endpoints: list[str] | None = None,
|
|
439
|
+
) -> TxIntent | None:
|
|
440
|
+
"""Create a new intent. Returns None if idempotency_key exists."""
|
|
441
|
+
...
|
|
442
|
+
|
|
443
|
+
@abstractmethod
|
|
444
|
+
def get_intent(self, intent_id: UUID) -> TxIntent | None:
|
|
445
|
+
"""Get an intent by ID."""
|
|
446
|
+
...
|
|
447
|
+
|
|
448
|
+
@abstractmethod
|
|
449
|
+
def get_intent_by_idempotency_key(
|
|
450
|
+
self,
|
|
451
|
+
chain_id: int,
|
|
452
|
+
signer_address: str,
|
|
453
|
+
idempotency_key: str,
|
|
454
|
+
) -> TxIntent | None:
|
|
455
|
+
"""Get an intent by idempotency key (scoped to chain and signer)."""
|
|
456
|
+
...
|
|
457
|
+
|
|
458
|
+
@abstractmethod
|
|
459
|
+
def get_intents_by_status(
|
|
460
|
+
self,
|
|
461
|
+
status: str | list[str],
|
|
462
|
+
chain_id: int | None = None,
|
|
463
|
+
job_id: str | None = None,
|
|
464
|
+
limit: int = 100,
|
|
465
|
+
) -> list[TxIntent]:
|
|
466
|
+
"""Get intents by status."""
|
|
467
|
+
...
|
|
468
|
+
|
|
469
|
+
@abstractmethod
|
|
470
|
+
def list_intents_filtered(
|
|
471
|
+
self,
|
|
472
|
+
status: str | None = None,
|
|
473
|
+
job_id: str | None = None,
|
|
474
|
+
limit: int = 50,
|
|
475
|
+
) -> list[dict[str, Any]]:
|
|
476
|
+
"""List intents with optional filters, returning raw dict data for CLI display."""
|
|
477
|
+
...
|
|
478
|
+
|
|
479
|
+
@abstractmethod
|
|
480
|
+
def get_active_intent_count(self, job_id: str, chain_id: int | None = None) -> int:
|
|
481
|
+
"""Count active intents for a job (created/claimed/sending/pending)."""
|
|
482
|
+
...
|
|
483
|
+
|
|
484
|
+
@abstractmethod
|
|
485
|
+
def get_pending_intent_count(self, chain_id: int | None = None) -> int:
|
|
486
|
+
"""Count active intents across all jobs (created/claimed/sending/pending)."""
|
|
487
|
+
...
|
|
488
|
+
|
|
489
|
+
@abstractmethod
|
|
490
|
+
def get_backing_off_intent_count(self, chain_id: int | None = None) -> int:
|
|
491
|
+
"""Count intents with retry_after in the future."""
|
|
492
|
+
...
|
|
493
|
+
|
|
494
|
+
@abstractmethod
|
|
495
|
+
def get_oldest_pending_intent_age(self, chain_id: int) -> float | None:
|
|
496
|
+
"""Get age in seconds of the oldest pending intent.
|
|
497
|
+
|
|
498
|
+
Considers intents in: CREATED, PENDING, CLAIMED, SENDING status.
|
|
499
|
+
|
|
500
|
+
Returns:
|
|
501
|
+
Age in seconds, or None if no pending intents.
|
|
502
|
+
"""
|
|
503
|
+
...
|
|
504
|
+
|
|
505
|
+
@abstractmethod
|
|
506
|
+
def list_intent_inconsistencies(
|
|
507
|
+
self,
|
|
508
|
+
max_age_seconds: int,
|
|
509
|
+
limit: int = 100,
|
|
510
|
+
chain_id: int | None = None,
|
|
511
|
+
) -> list[dict[str, Any]]:
|
|
512
|
+
"""List intents with inconsistent state/metadata."""
|
|
513
|
+
...
|
|
514
|
+
|
|
515
|
+
@abstractmethod
|
|
516
|
+
def list_sending_intents_older_than(
|
|
517
|
+
self,
|
|
518
|
+
max_age_seconds: int,
|
|
519
|
+
limit: int = 100,
|
|
520
|
+
chain_id: int | None = None,
|
|
521
|
+
) -> list[TxIntent]:
|
|
522
|
+
"""List sending intents older than a threshold."""
|
|
523
|
+
...
|
|
524
|
+
|
|
525
|
+
@abstractmethod
|
|
526
|
+
def claim_next_intent(
|
|
527
|
+
self,
|
|
528
|
+
claim_token: str,
|
|
529
|
+
claimed_by: str | None = None,
|
|
530
|
+
) -> TxIntent | None:
|
|
531
|
+
"""Claim the next available intent for processing."""
|
|
532
|
+
...
|
|
533
|
+
|
|
534
|
+
@abstractmethod
|
|
535
|
+
def update_intent_status(
|
|
536
|
+
self,
|
|
537
|
+
intent_id: UUID,
|
|
538
|
+
status: str,
|
|
539
|
+
claim_token: str | None = None,
|
|
540
|
+
) -> bool:
|
|
541
|
+
"""Update intent status. Returns True if updated."""
|
|
542
|
+
...
|
|
543
|
+
|
|
544
|
+
@abstractmethod
|
|
545
|
+
def update_intent_status_if(
|
|
546
|
+
self,
|
|
547
|
+
intent_id: UUID,
|
|
548
|
+
status: str,
|
|
549
|
+
expected_status: str | list[str],
|
|
550
|
+
) -> bool:
|
|
551
|
+
"""Update intent status only if current status matches expected."""
|
|
552
|
+
...
|
|
553
|
+
|
|
554
|
+
@abstractmethod
|
|
555
|
+
def transition_intent_status(
|
|
556
|
+
self,
|
|
557
|
+
intent_id: UUID,
|
|
558
|
+
from_statuses: list[str],
|
|
559
|
+
to_status: str,
|
|
560
|
+
) -> tuple[bool, str | None]:
|
|
561
|
+
"""Atomically transition intent status, clearing claim if leaving CLAIMED.
|
|
562
|
+
|
|
563
|
+
The claim fields (claim_token, claimed_at, claimed_by) are cleared
|
|
564
|
+
automatically when:
|
|
565
|
+
- The actual previous status is 'claimed', AND
|
|
566
|
+
- The new status is NOT 'claimed'
|
|
567
|
+
|
|
568
|
+
This prevents clearing claim on claimed->claimed transitions.
|
|
569
|
+
|
|
570
|
+
Returns:
|
|
571
|
+
(success, old_status) - old_status is the actual previous status,
|
|
572
|
+
or None if no row matched the WHERE clause.
|
|
573
|
+
"""
|
|
574
|
+
...
|
|
575
|
+
|
|
576
|
+
@abstractmethod
|
|
577
|
+
def update_intent_signer(self, intent_id: UUID, signer_address: str) -> bool:
|
|
578
|
+
"""Update intent signer address (for alias resolution)."""
|
|
579
|
+
...
|
|
580
|
+
|
|
581
|
+
@abstractmethod
|
|
582
|
+
def release_intent_claim(self, intent_id: UUID) -> bool:
|
|
583
|
+
"""Release an intent claim (revert to created status)."""
|
|
584
|
+
...
|
|
585
|
+
|
|
586
|
+
@abstractmethod
|
|
587
|
+
def release_intent_claim_if_token(self, intent_id: UUID, claim_token: str) -> bool:
|
|
588
|
+
"""Release claim only if claim_token matches. Returns True if released."""
|
|
589
|
+
...
|
|
590
|
+
|
|
591
|
+
@abstractmethod
|
|
592
|
+
def clear_intent_claim(self, intent_id: UUID) -> bool:
|
|
593
|
+
"""Clear claim token and claimed_at without changing status."""
|
|
594
|
+
...
|
|
595
|
+
|
|
596
|
+
@abstractmethod
|
|
597
|
+
def set_intent_retry_after(self, intent_id: UUID, retry_after: datetime | None) -> bool:
|
|
598
|
+
"""Set intent retry-after timestamp (null clears backoff)."""
|
|
599
|
+
...
|
|
600
|
+
|
|
601
|
+
@abstractmethod
|
|
602
|
+
def increment_intent_retry_count(self, intent_id: UUID) -> int:
|
|
603
|
+
"""Atomically increment retry count and return new value."""
|
|
604
|
+
...
|
|
605
|
+
|
|
606
|
+
@abstractmethod
|
|
607
|
+
def release_stale_intent_claims(self, max_age_seconds: int) -> int:
|
|
608
|
+
"""Release stale intent claims with no attempts. Returns count released."""
|
|
609
|
+
...
|
|
610
|
+
|
|
611
|
+
@abstractmethod
|
|
612
|
+
def abandon_intent(self, intent_id: UUID) -> bool:
|
|
613
|
+
"""Mark an intent as abandoned."""
|
|
614
|
+
...
|
|
615
|
+
|
|
616
|
+
@abstractmethod
|
|
617
|
+
def get_pending_intents_for_signer(
|
|
618
|
+
self, chain_id: int, address: str
|
|
619
|
+
) -> list[TxIntent]:
|
|
620
|
+
"""Get pending intents for a signer (for reconciliation)."""
|
|
621
|
+
...
|
|
622
|
+
|
|
623
|
+
# =========================================================================
|
|
624
|
+
# Attempt Operations
|
|
625
|
+
# =========================================================================
|
|
626
|
+
|
|
627
|
+
@abstractmethod
|
|
628
|
+
def create_attempt(
|
|
629
|
+
self,
|
|
630
|
+
attempt_id: UUID,
|
|
631
|
+
intent_id: UUID,
|
|
632
|
+
nonce: int,
|
|
633
|
+
gas_params_json: str,
|
|
634
|
+
status: str = "signed",
|
|
635
|
+
tx_hash: str | None = None,
|
|
636
|
+
replaces_attempt_id: UUID | None = None,
|
|
637
|
+
broadcast_group: str | None = None,
|
|
638
|
+
endpoint_url: str | None = None,
|
|
639
|
+
binding: tuple[str, list[str]] | None = None,
|
|
640
|
+
) -> TxAttempt:
|
|
641
|
+
"""Create a new transaction attempt.
|
|
642
|
+
|
|
643
|
+
Args:
|
|
644
|
+
attempt_id: Unique attempt ID
|
|
645
|
+
intent_id: Parent intent ID
|
|
646
|
+
nonce: Transaction nonce
|
|
647
|
+
gas_params_json: Gas parameters as JSON
|
|
648
|
+
status: Initial status (default: "signed")
|
|
649
|
+
tx_hash: Transaction hash if known
|
|
650
|
+
replaces_attempt_id: ID of attempt being replaced
|
|
651
|
+
broadcast_group: RPC group used for broadcast
|
|
652
|
+
endpoint_url: Endpoint URL that accepted the transaction
|
|
653
|
+
binding: If provided (first broadcast), persist binding atomically.
|
|
654
|
+
Tuple of (group_name, endpoint_list).
|
|
655
|
+
"""
|
|
656
|
+
...
|
|
657
|
+
|
|
658
|
+
@abstractmethod
|
|
659
|
+
def get_attempt(self, attempt_id: UUID) -> TxAttempt | None:
|
|
660
|
+
"""Get an attempt by ID."""
|
|
661
|
+
...
|
|
662
|
+
|
|
663
|
+
@abstractmethod
|
|
664
|
+
def get_attempts_for_intent(self, intent_id: UUID) -> list[TxAttempt]:
|
|
665
|
+
"""Get all attempts for an intent."""
|
|
666
|
+
...
|
|
667
|
+
|
|
668
|
+
@abstractmethod
|
|
669
|
+
def get_latest_attempt_for_intent(self, intent_id: UUID) -> TxAttempt | None:
|
|
670
|
+
"""Get the most recent attempt for an intent."""
|
|
671
|
+
...
|
|
672
|
+
|
|
673
|
+
@abstractmethod
|
|
674
|
+
def get_attempt_by_tx_hash(self, tx_hash: str) -> TxAttempt | None:
|
|
675
|
+
"""Get an attempt by transaction hash."""
|
|
676
|
+
...
|
|
677
|
+
|
|
678
|
+
@abstractmethod
|
|
679
|
+
def update_attempt_status(
|
|
680
|
+
self,
|
|
681
|
+
attempt_id: UUID,
|
|
682
|
+
status: str,
|
|
683
|
+
tx_hash: str | None = None,
|
|
684
|
+
broadcast_block: int | None = None,
|
|
685
|
+
broadcast_at: datetime | None = None,
|
|
686
|
+
included_block: int | None = None,
|
|
687
|
+
error_code: str | None = None,
|
|
688
|
+
error_detail: str | None = None,
|
|
689
|
+
) -> bool:
|
|
690
|
+
"""Update attempt status and related fields."""
|
|
691
|
+
...
|
|
692
|
+
|
|
693
|
+
# =========================================================================
|
|
694
|
+
# Transaction Operations (NEW - replaces Intent/Attempt in Phase 2+)
|
|
695
|
+
#
|
|
696
|
+
# IMPORTANT: Transaction is the only durable execution model.
|
|
697
|
+
# Do not add attempt-related methods here.
|
|
698
|
+
# =========================================================================
|
|
699
|
+
|
|
700
|
+
@abstractmethod
|
|
701
|
+
def create_tx(
|
|
702
|
+
self,
|
|
703
|
+
tx_id: UUID,
|
|
704
|
+
job_id: str,
|
|
705
|
+
chain_id: int,
|
|
706
|
+
idempotency_key: str,
|
|
707
|
+
signer_address: str,
|
|
708
|
+
to_address: str,
|
|
709
|
+
data: str | None,
|
|
710
|
+
value_wei: str,
|
|
711
|
+
min_confirmations: int,
|
|
712
|
+
deadline_ts: datetime | None,
|
|
713
|
+
gas_params: GasParams | None = None,
|
|
714
|
+
) -> Transaction | None:
|
|
715
|
+
"""Create a new transaction.
|
|
716
|
+
|
|
717
|
+
Returns None if idempotency_key already exists (idempotency).
|
|
718
|
+
Initial status is always 'created'.
|
|
719
|
+
"""
|
|
720
|
+
...
|
|
721
|
+
|
|
722
|
+
@abstractmethod
|
|
723
|
+
def get_tx(self, tx_id: UUID) -> Transaction | None:
|
|
724
|
+
"""Get a transaction by ID."""
|
|
725
|
+
...
|
|
726
|
+
|
|
727
|
+
@abstractmethod
|
|
728
|
+
def get_tx_by_idempotency_key(
|
|
729
|
+
self,
|
|
730
|
+
chain_id: int,
|
|
731
|
+
signer_address: str,
|
|
732
|
+
idempotency_key: str,
|
|
733
|
+
) -> Transaction | None:
|
|
734
|
+
"""Get a transaction by idempotency key (scoped to chain and signer)."""
|
|
735
|
+
...
|
|
736
|
+
|
|
737
|
+
@abstractmethod
|
|
738
|
+
def get_tx_by_hash(self, tx_hash: str) -> Transaction | None:
|
|
739
|
+
"""Get a transaction by current tx hash.
|
|
740
|
+
|
|
741
|
+
NOTE: Does NOT search tx_hash_history. Only matches current_tx_hash.
|
|
742
|
+
"""
|
|
743
|
+
...
|
|
744
|
+
|
|
745
|
+
@abstractmethod
|
|
746
|
+
def list_pending_txs(
|
|
747
|
+
self,
|
|
748
|
+
chain_id: int | None = None,
|
|
749
|
+
job_id: str | None = None,
|
|
750
|
+
) -> list[Transaction]:
|
|
751
|
+
"""List transactions in CREATED or BROADCAST status."""
|
|
752
|
+
...
|
|
753
|
+
|
|
754
|
+
@abstractmethod
|
|
755
|
+
def claim_tx(self, claim_token: str) -> Transaction | None:
|
|
756
|
+
"""Claim the next CREATED transaction for processing.
|
|
757
|
+
|
|
758
|
+
This is a lease, not ownership. The claim gates execution only.
|
|
759
|
+
Status remains CREATED while claimed - no "claimed" status.
|
|
760
|
+
"""
|
|
761
|
+
...
|
|
762
|
+
|
|
763
|
+
@abstractmethod
|
|
764
|
+
def set_tx_broadcast(
|
|
765
|
+
self,
|
|
766
|
+
tx_id: UUID,
|
|
767
|
+
tx_hash: str,
|
|
768
|
+
nonce: int,
|
|
769
|
+
gas_params: GasParams,
|
|
770
|
+
broadcast_block: int,
|
|
771
|
+
broadcast_info: BroadcastInfo | None = None,
|
|
772
|
+
) -> bool:
|
|
773
|
+
"""Record initial broadcast.
|
|
774
|
+
|
|
775
|
+
Sets status=BROADCAST, creates first tx_hash_history record.
|
|
776
|
+
Returns True if successful, False if tx not found or wrong status.
|
|
777
|
+
"""
|
|
778
|
+
...
|
|
779
|
+
|
|
780
|
+
@abstractmethod
|
|
781
|
+
def set_tx_replaced(
|
|
782
|
+
self,
|
|
783
|
+
tx_id: UUID,
|
|
784
|
+
new_tx_hash: str,
|
|
785
|
+
gas_params: GasParams,
|
|
786
|
+
broadcast_block: int,
|
|
787
|
+
reason: str = "fee_bump",
|
|
788
|
+
) -> bool:
|
|
789
|
+
"""Record replacement broadcast.
|
|
790
|
+
|
|
791
|
+
Appends to tx_hash_history, updates current_tx_hash, increments
|
|
792
|
+
replacement_count. Status remains BROADCAST.
|
|
793
|
+
|
|
794
|
+
Returns True if successful, False if tx not found or wrong status.
|
|
795
|
+
"""
|
|
796
|
+
...
|
|
797
|
+
|
|
798
|
+
@abstractmethod
|
|
799
|
+
def set_tx_confirmed(
|
|
800
|
+
self,
|
|
801
|
+
tx_id: UUID,
|
|
802
|
+
included_block: int,
|
|
803
|
+
) -> bool:
|
|
804
|
+
"""Mark transaction confirmed.
|
|
805
|
+
|
|
806
|
+
Sets status=CONFIRMED, included_block, confirmed_at.
|
|
807
|
+
Returns True if successful, False if tx not found or wrong status.
|
|
808
|
+
"""
|
|
809
|
+
...
|
|
810
|
+
|
|
811
|
+
@abstractmethod
|
|
812
|
+
def set_tx_failed(
|
|
813
|
+
self,
|
|
814
|
+
tx_id: UUID,
|
|
815
|
+
failure_type: FailureType,
|
|
816
|
+
error_info: ErrorInfo | None = None,
|
|
817
|
+
) -> bool:
|
|
818
|
+
"""Mark transaction failed.
|
|
819
|
+
|
|
820
|
+
Sets status=FAILED, failure_type, error_info_json.
|
|
821
|
+
Returns True if successful, False if tx not found or already terminal.
|
|
822
|
+
"""
|
|
823
|
+
...
|
|
824
|
+
|
|
825
|
+
@abstractmethod
|
|
826
|
+
def release_stale_tx_claims(self, max_age_seconds: int) -> int:
|
|
827
|
+
"""Release claims older than threshold.
|
|
828
|
+
|
|
829
|
+
Returns count of claims released.
|
|
830
|
+
"""
|
|
831
|
+
...
|
|
832
|
+
|
|
833
|
+
# =========================================================================
|
|
834
|
+
# ABI Cache Operations
|
|
835
|
+
# =========================================================================
|
|
836
|
+
|
|
837
|
+
@abstractmethod
|
|
838
|
+
def get_cached_abi(self, chain_id: int, address: str) -> ABICacheEntry | None:
|
|
839
|
+
"""Get cached ABI for a contract."""
|
|
840
|
+
...
|
|
841
|
+
|
|
842
|
+
@abstractmethod
|
|
843
|
+
def set_cached_abi(
|
|
844
|
+
self,
|
|
845
|
+
chain_id: int,
|
|
846
|
+
address: str,
|
|
847
|
+
abi_json: str,
|
|
848
|
+
source: str,
|
|
849
|
+
) -> None:
|
|
850
|
+
"""Cache an ABI for a contract."""
|
|
851
|
+
...
|
|
852
|
+
|
|
853
|
+
@abstractmethod
|
|
854
|
+
def clear_cached_abi(self, chain_id: int, address: str) -> bool:
|
|
855
|
+
"""Clear cached ABI for a contract."""
|
|
856
|
+
...
|
|
857
|
+
|
|
858
|
+
@abstractmethod
|
|
859
|
+
def cleanup_expired_abis(self, max_age_seconds: int) -> int:
|
|
860
|
+
"""Delete ABIs older than max_age_seconds. Returns count deleted."""
|
|
861
|
+
...
|
|
862
|
+
|
|
863
|
+
# =========================================================================
|
|
864
|
+
# Proxy Cache Operations
|
|
865
|
+
# =========================================================================
|
|
866
|
+
|
|
867
|
+
@abstractmethod
|
|
868
|
+
def get_cached_proxy(
|
|
869
|
+
self, chain_id: int, proxy_address: str
|
|
870
|
+
) -> ProxyCacheEntry | None:
|
|
871
|
+
"""Get cached proxy implementation address."""
|
|
872
|
+
...
|
|
873
|
+
|
|
874
|
+
@abstractmethod
|
|
875
|
+
def set_cached_proxy(
|
|
876
|
+
self,
|
|
877
|
+
chain_id: int,
|
|
878
|
+
proxy_address: str,
|
|
879
|
+
implementation_address: str,
|
|
880
|
+
) -> None:
|
|
881
|
+
"""Cache a proxy implementation address."""
|
|
882
|
+
...
|
|
883
|
+
|
|
884
|
+
@abstractmethod
|
|
885
|
+
def clear_cached_proxy(self, chain_id: int, proxy_address: str) -> bool:
|
|
886
|
+
"""Clear cached proxy resolution."""
|
|
887
|
+
...
|
|
888
|
+
|
|
889
|
+
# =========================================================================
|
|
890
|
+
# Cleanup & Maintenance
|
|
891
|
+
# =========================================================================
|
|
892
|
+
|
|
893
|
+
@abstractmethod
|
|
894
|
+
def cleanup_old_intents(
|
|
895
|
+
self,
|
|
896
|
+
older_than_days: int,
|
|
897
|
+
statuses: list[str] | None = None,
|
|
898
|
+
) -> int:
|
|
899
|
+
"""Delete old intents. Returns count deleted."""
|
|
900
|
+
...
|
|
901
|
+
|
|
902
|
+
@abstractmethod
|
|
903
|
+
def get_database_stats(self) -> dict[str, Any]:
|
|
904
|
+
"""Get database statistics for health checks."""
|
|
905
|
+
...
|
|
906
|
+
|
|
907
|
+
# =========================================================================
|
|
908
|
+
# Reconciliation Operations
|
|
909
|
+
# =========================================================================
|
|
910
|
+
|
|
911
|
+
@abstractmethod
|
|
912
|
+
def clear_orphaned_claims(self, chain_id: int, older_than_minutes: int = 2) -> int:
|
|
913
|
+
"""Clear claim fields where status != 'claimed' and claim is stale.
|
|
914
|
+
|
|
915
|
+
Only clears if claimed_at is older than threshold to avoid racing
|
|
916
|
+
with in-progress transitions.
|
|
917
|
+
|
|
918
|
+
Returns number of rows updated.
|
|
919
|
+
"""
|
|
920
|
+
...
|
|
921
|
+
|
|
922
|
+
@abstractmethod
|
|
923
|
+
def release_orphaned_nonces(self, chain_id: int, older_than_minutes: int = 5) -> int:
|
|
924
|
+
"""Release nonces for terminal intents that are stale.
|
|
925
|
+
|
|
926
|
+
Only releases 'reserved' nonces (not 'in_flight') where:
|
|
927
|
+
- Intent is in terminal state (failed/abandoned/reverted)
|
|
928
|
+
- Intent hasn't been updated recently (avoids race with recovery)
|
|
929
|
+
|
|
930
|
+
Returns number of rows updated.
|
|
931
|
+
"""
|
|
932
|
+
...
|
|
933
|
+
|
|
934
|
+
@abstractmethod
|
|
935
|
+
def count_pending_without_attempts(self, chain_id: int) -> int:
|
|
936
|
+
"""Count pending intents with no attempt records (integrity issue)."""
|
|
937
|
+
...
|
|
938
|
+
|
|
939
|
+
@abstractmethod
|
|
940
|
+
def count_stale_claims(self, chain_id: int, older_than_minutes: int = 10) -> int:
|
|
941
|
+
"""Count intents stuck in CLAIMED for too long."""
|
|
942
|
+
...
|
|
943
|
+
|
|
944
|
+
# =========================================================================
|
|
945
|
+
# Invariant Queries (Phase 2)
|
|
946
|
+
# =========================================================================
|
|
947
|
+
|
|
948
|
+
@abstractmethod
|
|
949
|
+
def count_stuck_claimed(self, chain_id: int, older_than_minutes: int = 10) -> int:
|
|
950
|
+
"""Count intents stuck in CLAIMED status for too long.
|
|
951
|
+
|
|
952
|
+
Normal claim duration is seconds to a few minutes. If an intent
|
|
953
|
+
has been claimed for >10 minutes, the worker likely crashed.
|
|
954
|
+
"""
|
|
955
|
+
...
|
|
956
|
+
|
|
957
|
+
@abstractmethod
|
|
958
|
+
def count_orphaned_claims(self, chain_id: int) -> int:
|
|
959
|
+
"""Count intents with claim_token set but status != claimed.
|
|
960
|
+
|
|
961
|
+
Violates invariant: claim_token should only exist when claimed.
|
|
962
|
+
Note: Phase 1's clear_orphaned_claims repairs these; this just counts.
|
|
963
|
+
"""
|
|
964
|
+
...
|
|
965
|
+
|
|
966
|
+
@abstractmethod
|
|
967
|
+
def count_orphaned_nonces(self, chain_id: int) -> int:
|
|
968
|
+
"""Count reserved/in_flight nonces for failed/abandoned intents.
|
|
969
|
+
|
|
970
|
+
These nonces are wasted and should be released.
|
|
971
|
+
Note: Phase 1's release_orphaned_nonces repairs these; this just counts.
|
|
972
|
+
"""
|
|
973
|
+
...
|
|
974
|
+
|
|
975
|
+
@abstractmethod
|
|
976
|
+
def get_oldest_nonce_gap_age_seconds(self, chain_id: int) -> float:
|
|
977
|
+
"""Get age in seconds of the oldest nonce gap.
|
|
978
|
+
|
|
979
|
+
A "gap" is a reserved nonce below the current chain nonce that
|
|
980
|
+
hasn't been released. This indicates a transaction that was never
|
|
981
|
+
broadcast or was dropped without proper cleanup.
|
|
982
|
+
|
|
983
|
+
Returns 0 if no gaps exist OR if last_synced_chain_nonce is NULL
|
|
984
|
+
(stale sync state should not trigger false-positive alerts).
|
|
985
|
+
"""
|
|
986
|
+
...
|