brawny 0.1.13__py3-none-any.whl → 0.1.22__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 +2 -0
- brawny/_context.py +5 -5
- brawny/_rpc/__init__.py +36 -12
- brawny/_rpc/broadcast.py +14 -13
- brawny/_rpc/caller.py +243 -0
- brawny/_rpc/client.py +539 -0
- brawny/_rpc/clients.py +11 -11
- brawny/_rpc/context.py +23 -0
- brawny/_rpc/errors.py +465 -31
- brawny/_rpc/gas.py +7 -6
- brawny/_rpc/pool.py +18 -0
- brawny/_rpc/retry.py +266 -0
- brawny/_rpc/retry_policy.py +81 -0
- brawny/accounts.py +28 -9
- brawny/alerts/__init__.py +15 -18
- brawny/alerts/abi_resolver.py +212 -36
- brawny/alerts/base.py +2 -2
- brawny/alerts/contracts.py +77 -10
- brawny/alerts/errors.py +30 -3
- brawny/alerts/events.py +38 -5
- brawny/alerts/health.py +19 -13
- brawny/alerts/send.py +513 -55
- brawny/api.py +39 -11
- brawny/assets/AGENTS.md +325 -0
- brawny/async_runtime.py +48 -0
- brawny/chain.py +3 -3
- brawny/cli/commands/__init__.py +2 -0
- brawny/cli/commands/console.py +69 -19
- brawny/cli/commands/contract.py +2 -2
- brawny/cli/commands/controls.py +121 -0
- brawny/cli/commands/health.py +2 -2
- brawny/cli/commands/job_dev.py +6 -5
- brawny/cli/commands/jobs.py +99 -2
- brawny/cli/commands/maintenance.py +13 -29
- brawny/cli/commands/migrate.py +1 -0
- brawny/cli/commands/run.py +10 -3
- brawny/cli/commands/script.py +8 -3
- brawny/cli/commands/signer.py +143 -26
- brawny/cli/helpers.py +0 -3
- brawny/cli_templates.py +25 -349
- brawny/config/__init__.py +4 -1
- brawny/config/models.py +43 -57
- brawny/config/parser.py +268 -57
- brawny/config/validation.py +52 -15
- brawny/daemon/context.py +4 -2
- brawny/daemon/core.py +185 -63
- brawny/daemon/loops.py +166 -98
- brawny/daemon/supervisor.py +261 -0
- brawny/db/__init__.py +14 -26
- brawny/db/base.py +248 -151
- brawny/db/global_cache.py +11 -1
- brawny/db/migrate.py +175 -28
- brawny/db/migrations/001_init.sql +4 -3
- brawny/db/migrations/010_add_nonce_gap_index.sql +1 -1
- brawny/db/migrations/011_add_job_logs.sql +1 -2
- brawny/db/migrations/012_add_claimed_by.sql +2 -2
- brawny/db/migrations/013_attempt_unique.sql +10 -0
- brawny/db/migrations/014_add_lease_expires_at.sql +5 -0
- brawny/db/migrations/015_add_signer_alias.sql +14 -0
- brawny/db/migrations/016_runtime_controls_and_quarantine.sql +32 -0
- brawny/db/migrations/017_add_job_drain.sql +6 -0
- brawny/db/migrations/018_add_nonce_reset_audit.sql +20 -0
- brawny/db/migrations/019_add_job_cooldowns.sql +8 -0
- brawny/db/migrations/020_attempt_unique_initial.sql +7 -0
- brawny/db/ops/__init__.py +3 -25
- brawny/db/ops/logs.py +1 -2
- brawny/db/queries.py +47 -91
- brawny/db/serialized.py +65 -0
- brawny/db/sqlite/__init__.py +1001 -0
- brawny/db/sqlite/connection.py +231 -0
- brawny/db/sqlite/execute.py +116 -0
- brawny/db/sqlite/mappers.py +190 -0
- brawny/db/sqlite/repos/attempts.py +372 -0
- brawny/db/sqlite/repos/block_state.py +102 -0
- brawny/db/sqlite/repos/cache.py +104 -0
- brawny/db/sqlite/repos/intents.py +1021 -0
- brawny/db/sqlite/repos/jobs.py +200 -0
- brawny/db/sqlite/repos/maintenance.py +182 -0
- brawny/db/sqlite/repos/signers_nonces.py +566 -0
- brawny/db/sqlite/tx.py +119 -0
- brawny/http.py +194 -0
- brawny/invariants.py +11 -24
- brawny/jobs/base.py +8 -0
- brawny/jobs/job_validation.py +2 -1
- brawny/keystore.py +83 -7
- brawny/lifecycle.py +64 -12
- brawny/logging.py +0 -2
- brawny/metrics.py +84 -12
- brawny/model/contexts.py +111 -9
- brawny/model/enums.py +1 -0
- brawny/model/errors.py +18 -0
- brawny/model/types.py +47 -131
- brawny/network_guard.py +133 -0
- brawny/networks/__init__.py +5 -5
- brawny/networks/config.py +1 -7
- brawny/networks/manager.py +14 -11
- brawny/runtime_controls.py +74 -0
- brawny/scheduler/poller.py +11 -7
- brawny/scheduler/reorg.py +95 -39
- brawny/scheduler/runner.py +442 -168
- brawny/scheduler/shutdown.py +3 -3
- brawny/script_tx.py +3 -3
- brawny/telegram.py +53 -7
- brawny/testing.py +1 -0
- brawny/timeout.py +38 -0
- brawny/tx/executor.py +922 -308
- brawny/tx/intent.py +54 -16
- brawny/tx/monitor.py +31 -12
- brawny/tx/nonce.py +212 -90
- brawny/tx/replacement.py +69 -18
- brawny/tx/retry_policy.py +24 -0
- brawny/tx/stages/types.py +75 -0
- brawny/types.py +18 -0
- brawny/utils.py +41 -0
- {brawny-0.1.13.dist-info → brawny-0.1.22.dist-info}/METADATA +3 -3
- brawny-0.1.22.dist-info/RECORD +163 -0
- brawny/_rpc/manager.py +0 -982
- brawny/_rpc/selector.py +0 -156
- brawny/db/base_new.py +0 -165
- brawny/db/mappers.py +0 -182
- brawny/db/migrations/008_add_transactions.sql +0 -72
- brawny/db/ops/attempts.py +0 -108
- brawny/db/ops/blocks.py +0 -83
- brawny/db/ops/cache.py +0 -93
- brawny/db/ops/intents.py +0 -296
- brawny/db/ops/jobs.py +0 -110
- brawny/db/ops/nonces.py +0 -322
- brawny/db/postgres.py +0 -2535
- brawny/db/postgres_new.py +0 -196
- brawny/db/sqlite.py +0 -2733
- brawny/db/sqlite_new.py +0 -191
- brawny-0.1.13.dist-info/RECORD +0 -141
- {brawny-0.1.13.dist-info → brawny-0.1.22.dist-info}/WHEEL +0 -0
- {brawny-0.1.13.dist-info → brawny-0.1.22.dist-info}/entry_points.txt +0 -0
- {brawny-0.1.13.dist-info → brawny-0.1.22.dist-info}/top_level.txt +0 -0
|
@@ -0,0 +1,200 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import json
|
|
4
|
+
from datetime import datetime
|
|
5
|
+
from typing import Any
|
|
6
|
+
|
|
7
|
+
from brawny.db.sqlite import mappers, tx
|
|
8
|
+
from brawny.model.types import JobConfig
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
def get_job(db: Any, job_id: str) -> JobConfig | None:
|
|
12
|
+
row = db.execute_one("SELECT * FROM jobs WHERE job_id = ?", (job_id,))
|
|
13
|
+
if not row:
|
|
14
|
+
return None
|
|
15
|
+
return mappers._row_to_job_config(row)
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
def get_enabled_jobs(db: Any) -> list[JobConfig]:
|
|
19
|
+
rows = db.execute_returning(
|
|
20
|
+
"""
|
|
21
|
+
SELECT * FROM jobs
|
|
22
|
+
WHERE enabled = 1
|
|
23
|
+
AND (drain_until IS NULL OR drain_until <= CURRENT_TIMESTAMP)
|
|
24
|
+
ORDER BY job_id
|
|
25
|
+
"""
|
|
26
|
+
)
|
|
27
|
+
return [mappers._row_to_job_config(row) for row in rows]
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
def list_all_jobs(db: Any) -> list[JobConfig]:
|
|
31
|
+
rows = db.execute_returning("SELECT * FROM jobs ORDER BY job_id")
|
|
32
|
+
return [mappers._row_to_job_config(row) for row in rows]
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
def upsert_job(
|
|
36
|
+
db: Any,
|
|
37
|
+
job_id: str,
|
|
38
|
+
job_name: str,
|
|
39
|
+
check_interval_blocks: int,
|
|
40
|
+
enabled: bool = True,
|
|
41
|
+
) -> None:
|
|
42
|
+
db.execute(
|
|
43
|
+
"""
|
|
44
|
+
INSERT INTO jobs (job_id, job_name, check_interval_blocks, enabled)
|
|
45
|
+
VALUES (?, ?, ?, ?)
|
|
46
|
+
ON CONFLICT(job_id) DO UPDATE SET
|
|
47
|
+
job_name = excluded.job_name,
|
|
48
|
+
check_interval_blocks = excluded.check_interval_blocks,
|
|
49
|
+
updated_at = CURRENT_TIMESTAMP
|
|
50
|
+
""",
|
|
51
|
+
(job_id, job_name, check_interval_blocks, enabled),
|
|
52
|
+
)
|
|
53
|
+
|
|
54
|
+
|
|
55
|
+
def update_job_checked(db: Any, job_id: str, block_number: int, triggered: bool = False) -> None:
|
|
56
|
+
if triggered:
|
|
57
|
+
db.execute(
|
|
58
|
+
"""
|
|
59
|
+
UPDATE jobs SET
|
|
60
|
+
last_checked_block_number = ?,
|
|
61
|
+
last_triggered_block_number = ?,
|
|
62
|
+
updated_at = CURRENT_TIMESTAMP
|
|
63
|
+
WHERE job_id = ?
|
|
64
|
+
""",
|
|
65
|
+
(block_number, block_number, job_id),
|
|
66
|
+
)
|
|
67
|
+
return
|
|
68
|
+
|
|
69
|
+
db.execute(
|
|
70
|
+
"""
|
|
71
|
+
UPDATE jobs SET
|
|
72
|
+
last_checked_block_number = ?,
|
|
73
|
+
updated_at = CURRENT_TIMESTAMP
|
|
74
|
+
WHERE job_id = ?
|
|
75
|
+
""",
|
|
76
|
+
(block_number, job_id),
|
|
77
|
+
)
|
|
78
|
+
|
|
79
|
+
|
|
80
|
+
def set_job_enabled(db: Any, job_id: str, enabled: bool) -> bool:
|
|
81
|
+
rowcount = db.execute_returning_rowcount(
|
|
82
|
+
"UPDATE jobs SET enabled = ?, updated_at = CURRENT_TIMESTAMP WHERE job_id = ?",
|
|
83
|
+
(enabled, job_id),
|
|
84
|
+
)
|
|
85
|
+
return rowcount > 0
|
|
86
|
+
|
|
87
|
+
|
|
88
|
+
def set_job_drain(
|
|
89
|
+
db: Any,
|
|
90
|
+
job_id: str,
|
|
91
|
+
drain_until: datetime,
|
|
92
|
+
reason: str | None = None,
|
|
93
|
+
actor: str | None = None,
|
|
94
|
+
source: str | None = None,
|
|
95
|
+
) -> bool:
|
|
96
|
+
with tx.transaction_conn(db) as conn:
|
|
97
|
+
cursor = conn.cursor()
|
|
98
|
+
try:
|
|
99
|
+
cursor.execute(
|
|
100
|
+
"""
|
|
101
|
+
UPDATE jobs
|
|
102
|
+
SET drain_until = ?,
|
|
103
|
+
drain_reason = ?,
|
|
104
|
+
updated_at = CURRENT_TIMESTAMP
|
|
105
|
+
WHERE job_id = ?
|
|
106
|
+
""",
|
|
107
|
+
(drain_until.isoformat(), reason, job_id),
|
|
108
|
+
)
|
|
109
|
+
updated = cursor.rowcount > 0
|
|
110
|
+
finally:
|
|
111
|
+
cursor.close()
|
|
112
|
+
if updated:
|
|
113
|
+
db.record_mutation_audit(
|
|
114
|
+
entity_type="job",
|
|
115
|
+
entity_id=job_id,
|
|
116
|
+
action="drain",
|
|
117
|
+
actor=actor,
|
|
118
|
+
reason=reason,
|
|
119
|
+
source=source,
|
|
120
|
+
metadata={"drain_until": drain_until.isoformat()},
|
|
121
|
+
)
|
|
122
|
+
return updated
|
|
123
|
+
|
|
124
|
+
|
|
125
|
+
def clear_job_drain(
|
|
126
|
+
db: Any,
|
|
127
|
+
job_id: str,
|
|
128
|
+
actor: str | None = None,
|
|
129
|
+
source: str | None = None,
|
|
130
|
+
) -> bool:
|
|
131
|
+
with tx.transaction_conn(db) as conn:
|
|
132
|
+
cursor = conn.cursor()
|
|
133
|
+
try:
|
|
134
|
+
cursor.execute(
|
|
135
|
+
"""
|
|
136
|
+
UPDATE jobs
|
|
137
|
+
SET drain_until = NULL,
|
|
138
|
+
drain_reason = NULL,
|
|
139
|
+
updated_at = CURRENT_TIMESTAMP
|
|
140
|
+
WHERE job_id = ?
|
|
141
|
+
""",
|
|
142
|
+
(job_id,),
|
|
143
|
+
)
|
|
144
|
+
updated = cursor.rowcount > 0
|
|
145
|
+
finally:
|
|
146
|
+
cursor.close()
|
|
147
|
+
if updated:
|
|
148
|
+
db.record_mutation_audit(
|
|
149
|
+
entity_type="job",
|
|
150
|
+
entity_id=job_id,
|
|
151
|
+
action="undrain",
|
|
152
|
+
actor=actor,
|
|
153
|
+
reason=None,
|
|
154
|
+
source=source,
|
|
155
|
+
)
|
|
156
|
+
return updated
|
|
157
|
+
|
|
158
|
+
|
|
159
|
+
def delete_job(db: Any, job_id: str) -> bool:
|
|
160
|
+
with tx.transaction_conn(db) as conn:
|
|
161
|
+
cursor = conn.cursor()
|
|
162
|
+
try:
|
|
163
|
+
cursor.execute("DELETE FROM job_kv WHERE job_id = ?", (job_id,))
|
|
164
|
+
cursor.execute("DELETE FROM jobs WHERE job_id = ?", (job_id,))
|
|
165
|
+
deleted = cursor.rowcount > 0
|
|
166
|
+
finally:
|
|
167
|
+
cursor.close()
|
|
168
|
+
return deleted
|
|
169
|
+
|
|
170
|
+
|
|
171
|
+
def get_job_kv(db: Any, job_id: str, key: str) -> Any | None:
|
|
172
|
+
row = db.execute_one(
|
|
173
|
+
"SELECT value_json FROM job_kv WHERE job_id = ? AND key = ?",
|
|
174
|
+
(job_id, key),
|
|
175
|
+
)
|
|
176
|
+
if not row:
|
|
177
|
+
return None
|
|
178
|
+
return mappers.parse_json(row["value_json"])
|
|
179
|
+
|
|
180
|
+
|
|
181
|
+
def set_job_kv(db: Any, job_id: str, key: str, value: Any) -> None:
|
|
182
|
+
value_json = json.dumps(value)
|
|
183
|
+
db.execute(
|
|
184
|
+
"""
|
|
185
|
+
INSERT INTO job_kv (job_id, key, value_json)
|
|
186
|
+
VALUES (?, ?, ?)
|
|
187
|
+
ON CONFLICT(job_id, key) DO UPDATE SET
|
|
188
|
+
value_json = excluded.value_json,
|
|
189
|
+
updated_at = CURRENT_TIMESTAMP
|
|
190
|
+
""",
|
|
191
|
+
(job_id, key, value_json),
|
|
192
|
+
)
|
|
193
|
+
|
|
194
|
+
|
|
195
|
+
def delete_job_kv(db: Any, job_id: str, key: str) -> bool:
|
|
196
|
+
rowcount = db.execute_returning_rowcount(
|
|
197
|
+
"DELETE FROM job_kv WHERE job_id = ? AND key = ?",
|
|
198
|
+
(job_id, key),
|
|
199
|
+
)
|
|
200
|
+
return rowcount > 0
|
|
@@ -0,0 +1,182 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from typing import Any
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
def cleanup_old_intents(
|
|
7
|
+
db: Any,
|
|
8
|
+
older_than_days: int,
|
|
9
|
+
statuses: list[str] | None = None,
|
|
10
|
+
) -> int:
|
|
11
|
+
if statuses is None:
|
|
12
|
+
statuses = ["confirmed", "failed", "abandoned"]
|
|
13
|
+
|
|
14
|
+
placeholders = ",".join("?" * len(statuses))
|
|
15
|
+
return db.execute_returning_rowcount(
|
|
16
|
+
f"""
|
|
17
|
+
DELETE FROM tx_intents
|
|
18
|
+
WHERE status IN ({placeholders})
|
|
19
|
+
AND created_at < datetime('now', ? || ' days')
|
|
20
|
+
""",
|
|
21
|
+
(*statuses, f"-{older_than_days}"),
|
|
22
|
+
)
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
def get_database_stats(db: Any) -> dict[str, Any]:
|
|
26
|
+
stats: dict[str, Any] = {"type": "sqlite", "path": db._database_path}
|
|
27
|
+
|
|
28
|
+
rows = db.execute_returning(
|
|
29
|
+
"SELECT status, COUNT(*) as count FROM tx_intents GROUP BY status"
|
|
30
|
+
)
|
|
31
|
+
stats["intents_by_status"] = {row["status"]: row["count"] for row in rows}
|
|
32
|
+
|
|
33
|
+
row = db.execute_one("SELECT COUNT(*) as count FROM jobs")
|
|
34
|
+
stats["total_jobs"] = row["count"] if row else 0
|
|
35
|
+
|
|
36
|
+
row = db.execute_one("SELECT COUNT(*) as count FROM jobs WHERE enabled = 1")
|
|
37
|
+
stats["enabled_jobs"] = row["count"] if row else 0
|
|
38
|
+
|
|
39
|
+
rows = db.execute_returning("SELECT * FROM block_state")
|
|
40
|
+
stats["block_states"] = [
|
|
41
|
+
{
|
|
42
|
+
"chain_id": row["chain_id"],
|
|
43
|
+
"last_block": row["last_processed_block_number"],
|
|
44
|
+
}
|
|
45
|
+
for row in rows
|
|
46
|
+
]
|
|
47
|
+
|
|
48
|
+
return stats
|
|
49
|
+
|
|
50
|
+
|
|
51
|
+
def clear_orphaned_claims(db: Any, chain_id: int, older_than_minutes: int = 2) -> int:
|
|
52
|
+
return db.execute_returning_rowcount(
|
|
53
|
+
"""
|
|
54
|
+
UPDATE tx_intents
|
|
55
|
+
SET claim_token = NULL,
|
|
56
|
+
claimed_at = NULL,
|
|
57
|
+
claimed_by = NULL,
|
|
58
|
+
lease_expires_at = NULL,
|
|
59
|
+
updated_at = CURRENT_TIMESTAMP
|
|
60
|
+
WHERE chain_id = ?
|
|
61
|
+
AND status != 'claimed'
|
|
62
|
+
AND claim_token IS NOT NULL
|
|
63
|
+
AND claimed_at IS NOT NULL
|
|
64
|
+
AND claimed_at < datetime('now', ? || ' minutes')
|
|
65
|
+
""",
|
|
66
|
+
(chain_id, f"-{older_than_minutes}"),
|
|
67
|
+
)
|
|
68
|
+
|
|
69
|
+
|
|
70
|
+
def release_orphaned_nonces(db: Any, chain_id: int, older_than_minutes: int = 5) -> int:
|
|
71
|
+
return db.execute_returning_rowcount(
|
|
72
|
+
"""
|
|
73
|
+
UPDATE nonce_reservations
|
|
74
|
+
SET status = 'released',
|
|
75
|
+
updated_at = CURRENT_TIMESTAMP
|
|
76
|
+
WHERE chain_id = ?
|
|
77
|
+
AND status = 'reserved'
|
|
78
|
+
AND updated_at < datetime('now', ? || ' minutes')
|
|
79
|
+
AND intent_id IN (
|
|
80
|
+
SELECT intent_id FROM tx_intents
|
|
81
|
+
WHERE status IN ('failed', 'abandoned', 'reverted')
|
|
82
|
+
AND updated_at < datetime('now', ? || ' minutes')
|
|
83
|
+
)
|
|
84
|
+
""",
|
|
85
|
+
(chain_id, f"-{older_than_minutes}", f"-{older_than_minutes}"),
|
|
86
|
+
)
|
|
87
|
+
|
|
88
|
+
|
|
89
|
+
def count_pending_without_attempts(db: Any, chain_id: int) -> int:
|
|
90
|
+
result = db.execute_one(
|
|
91
|
+
"""
|
|
92
|
+
SELECT COUNT(*) as count
|
|
93
|
+
FROM tx_intents ti
|
|
94
|
+
LEFT JOIN tx_attempts ta ON ti.intent_id = ta.intent_id
|
|
95
|
+
WHERE ti.chain_id = ?
|
|
96
|
+
AND ti.status = 'pending'
|
|
97
|
+
AND ta.attempt_id IS NULL
|
|
98
|
+
""",
|
|
99
|
+
(chain_id,),
|
|
100
|
+
)
|
|
101
|
+
return result["count"] if result else 0
|
|
102
|
+
|
|
103
|
+
|
|
104
|
+
def count_stale_claims(db: Any, chain_id: int, older_than_minutes: int = 10) -> int:
|
|
105
|
+
result = db.execute_one(
|
|
106
|
+
"""
|
|
107
|
+
SELECT COUNT(*) as count
|
|
108
|
+
FROM tx_intents
|
|
109
|
+
WHERE chain_id = ?
|
|
110
|
+
AND status = 'claimed'
|
|
111
|
+
AND claimed_at IS NOT NULL
|
|
112
|
+
AND COALESCE(lease_expires_at, datetime(claimed_at, ? || ' minutes')) < CURRENT_TIMESTAMP
|
|
113
|
+
""",
|
|
114
|
+
(chain_id, f"+{older_than_minutes}"),
|
|
115
|
+
)
|
|
116
|
+
return result["count"] if result else 0
|
|
117
|
+
|
|
118
|
+
|
|
119
|
+
def count_stuck_claimed(db: Any, chain_id: int, older_than_minutes: int = 10) -> int:
|
|
120
|
+
result = db.execute_one(
|
|
121
|
+
"""
|
|
122
|
+
SELECT COUNT(*) as count
|
|
123
|
+
FROM tx_intents
|
|
124
|
+
WHERE chain_id = ?
|
|
125
|
+
AND status = 'claimed'
|
|
126
|
+
AND COALESCE(lease_expires_at, datetime(claimed_at, ? || ' minutes')) < CURRENT_TIMESTAMP
|
|
127
|
+
""",
|
|
128
|
+
(chain_id, f"+{older_than_minutes}"),
|
|
129
|
+
)
|
|
130
|
+
return result["count"] if result else 0
|
|
131
|
+
|
|
132
|
+
|
|
133
|
+
def count_orphaned_claims(db: Any, chain_id: int) -> int:
|
|
134
|
+
result = db.execute_one(
|
|
135
|
+
"""
|
|
136
|
+
SELECT COUNT(*) as count
|
|
137
|
+
FROM tx_intents
|
|
138
|
+
WHERE chain_id = ?
|
|
139
|
+
AND status != 'claimed'
|
|
140
|
+
AND claim_token IS NOT NULL
|
|
141
|
+
""",
|
|
142
|
+
(chain_id,),
|
|
143
|
+
)
|
|
144
|
+
return result["count"] if result else 0
|
|
145
|
+
|
|
146
|
+
|
|
147
|
+
def count_orphaned_nonces(db: Any, chain_id: int) -> int:
|
|
148
|
+
result = db.execute_one(
|
|
149
|
+
"""
|
|
150
|
+
SELECT COUNT(*) as count
|
|
151
|
+
FROM nonce_reservations nr
|
|
152
|
+
JOIN tx_intents ti ON nr.intent_id = ti.intent_id
|
|
153
|
+
WHERE nr.chain_id = ?
|
|
154
|
+
AND nr.status IN ('reserved', 'in_flight')
|
|
155
|
+
AND ti.status IN ('failed', 'abandoned', 'reverted')
|
|
156
|
+
""",
|
|
157
|
+
(chain_id,),
|
|
158
|
+
)
|
|
159
|
+
return result["count"] if result else 0
|
|
160
|
+
|
|
161
|
+
|
|
162
|
+
def get_oldest_nonce_gap_age_seconds(db: Any, chain_id: int) -> float:
|
|
163
|
+
result = db.execute_one(
|
|
164
|
+
"""
|
|
165
|
+
SELECT COALESCE(
|
|
166
|
+
(julianday('now') - julianday(datetime(MIN(nr.created_at)))) * 86400,
|
|
167
|
+
0
|
|
168
|
+
) AS oldest_gap_seconds
|
|
169
|
+
FROM signers s
|
|
170
|
+
JOIN nonce_reservations nr
|
|
171
|
+
ON nr.chain_id = s.chain_id
|
|
172
|
+
AND nr.signer_address = s.signer_address
|
|
173
|
+
WHERE s.chain_id = ?
|
|
174
|
+
AND s.last_synced_chain_nonce IS NOT NULL
|
|
175
|
+
AND nr.status IN ('reserved', 'in_flight')
|
|
176
|
+
AND nr.nonce < s.last_synced_chain_nonce
|
|
177
|
+
""",
|
|
178
|
+
(chain_id,),
|
|
179
|
+
)
|
|
180
|
+
if not result:
|
|
181
|
+
return 0.0
|
|
182
|
+
return float(result["oldest_gap_seconds"])
|