brawny 0.1.13__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (141) hide show
  1. brawny/__init__.py +106 -0
  2. brawny/_context.py +232 -0
  3. brawny/_rpc/__init__.py +38 -0
  4. brawny/_rpc/broadcast.py +172 -0
  5. brawny/_rpc/clients.py +98 -0
  6. brawny/_rpc/context.py +49 -0
  7. brawny/_rpc/errors.py +252 -0
  8. brawny/_rpc/gas.py +158 -0
  9. brawny/_rpc/manager.py +982 -0
  10. brawny/_rpc/selector.py +156 -0
  11. brawny/accounts.py +534 -0
  12. brawny/alerts/__init__.py +132 -0
  13. brawny/alerts/abi_resolver.py +530 -0
  14. brawny/alerts/base.py +152 -0
  15. brawny/alerts/context.py +271 -0
  16. brawny/alerts/contracts.py +635 -0
  17. brawny/alerts/encoded_call.py +201 -0
  18. brawny/alerts/errors.py +267 -0
  19. brawny/alerts/events.py +680 -0
  20. brawny/alerts/function_caller.py +364 -0
  21. brawny/alerts/health.py +185 -0
  22. brawny/alerts/routing.py +118 -0
  23. brawny/alerts/send.py +364 -0
  24. brawny/api.py +660 -0
  25. brawny/chain.py +93 -0
  26. brawny/cli/__init__.py +16 -0
  27. brawny/cli/app.py +17 -0
  28. brawny/cli/bootstrap.py +37 -0
  29. brawny/cli/commands/__init__.py +41 -0
  30. brawny/cli/commands/abi.py +93 -0
  31. brawny/cli/commands/accounts.py +632 -0
  32. brawny/cli/commands/console.py +495 -0
  33. brawny/cli/commands/contract.py +139 -0
  34. brawny/cli/commands/health.py +112 -0
  35. brawny/cli/commands/init_project.py +86 -0
  36. brawny/cli/commands/intents.py +130 -0
  37. brawny/cli/commands/job_dev.py +254 -0
  38. brawny/cli/commands/jobs.py +308 -0
  39. brawny/cli/commands/logs.py +87 -0
  40. brawny/cli/commands/maintenance.py +182 -0
  41. brawny/cli/commands/migrate.py +51 -0
  42. brawny/cli/commands/networks.py +253 -0
  43. brawny/cli/commands/run.py +249 -0
  44. brawny/cli/commands/script.py +209 -0
  45. brawny/cli/commands/signer.py +248 -0
  46. brawny/cli/helpers.py +265 -0
  47. brawny/cli_templates.py +1445 -0
  48. brawny/config/__init__.py +74 -0
  49. brawny/config/models.py +404 -0
  50. brawny/config/parser.py +633 -0
  51. brawny/config/routing.py +55 -0
  52. brawny/config/validation.py +246 -0
  53. brawny/daemon/__init__.py +14 -0
  54. brawny/daemon/context.py +69 -0
  55. brawny/daemon/core.py +702 -0
  56. brawny/daemon/loops.py +327 -0
  57. brawny/db/__init__.py +78 -0
  58. brawny/db/base.py +986 -0
  59. brawny/db/base_new.py +165 -0
  60. brawny/db/circuit_breaker.py +97 -0
  61. brawny/db/global_cache.py +298 -0
  62. brawny/db/mappers.py +182 -0
  63. brawny/db/migrate.py +349 -0
  64. brawny/db/migrations/001_init.sql +186 -0
  65. brawny/db/migrations/002_add_included_block.sql +7 -0
  66. brawny/db/migrations/003_add_broadcast_at.sql +10 -0
  67. brawny/db/migrations/004_broadcast_binding.sql +20 -0
  68. brawny/db/migrations/005_add_retry_after.sql +9 -0
  69. brawny/db/migrations/006_add_retry_count_column.sql +11 -0
  70. brawny/db/migrations/007_add_gap_tracking.sql +18 -0
  71. brawny/db/migrations/008_add_transactions.sql +72 -0
  72. brawny/db/migrations/009_add_intent_metadata.sql +5 -0
  73. brawny/db/migrations/010_add_nonce_gap_index.sql +9 -0
  74. brawny/db/migrations/011_add_job_logs.sql +24 -0
  75. brawny/db/migrations/012_add_claimed_by.sql +5 -0
  76. brawny/db/ops/__init__.py +29 -0
  77. brawny/db/ops/attempts.py +108 -0
  78. brawny/db/ops/blocks.py +83 -0
  79. brawny/db/ops/cache.py +93 -0
  80. brawny/db/ops/intents.py +296 -0
  81. brawny/db/ops/jobs.py +110 -0
  82. brawny/db/ops/logs.py +97 -0
  83. brawny/db/ops/nonces.py +322 -0
  84. brawny/db/postgres.py +2535 -0
  85. brawny/db/postgres_new.py +196 -0
  86. brawny/db/queries.py +584 -0
  87. brawny/db/sqlite.py +2733 -0
  88. brawny/db/sqlite_new.py +191 -0
  89. brawny/history.py +126 -0
  90. brawny/interfaces.py +136 -0
  91. brawny/invariants.py +155 -0
  92. brawny/jobs/__init__.py +26 -0
  93. brawny/jobs/base.py +287 -0
  94. brawny/jobs/discovery.py +233 -0
  95. brawny/jobs/job_validation.py +111 -0
  96. brawny/jobs/kv.py +125 -0
  97. brawny/jobs/registry.py +283 -0
  98. brawny/keystore.py +484 -0
  99. brawny/lifecycle.py +551 -0
  100. brawny/logging.py +290 -0
  101. brawny/metrics.py +594 -0
  102. brawny/model/__init__.py +53 -0
  103. brawny/model/contexts.py +319 -0
  104. brawny/model/enums.py +70 -0
  105. brawny/model/errors.py +194 -0
  106. brawny/model/events.py +93 -0
  107. brawny/model/startup.py +20 -0
  108. brawny/model/types.py +483 -0
  109. brawny/networks/__init__.py +96 -0
  110. brawny/networks/config.py +269 -0
  111. brawny/networks/manager.py +423 -0
  112. brawny/obs/__init__.py +67 -0
  113. brawny/obs/emit.py +158 -0
  114. brawny/obs/health.py +175 -0
  115. brawny/obs/heartbeat.py +133 -0
  116. brawny/reconciliation.py +108 -0
  117. brawny/scheduler/__init__.py +19 -0
  118. brawny/scheduler/poller.py +472 -0
  119. brawny/scheduler/reorg.py +632 -0
  120. brawny/scheduler/runner.py +708 -0
  121. brawny/scheduler/shutdown.py +371 -0
  122. brawny/script_tx.py +297 -0
  123. brawny/scripting.py +251 -0
  124. brawny/startup.py +76 -0
  125. brawny/telegram.py +393 -0
  126. brawny/testing.py +108 -0
  127. brawny/tx/__init__.py +41 -0
  128. brawny/tx/executor.py +1071 -0
  129. brawny/tx/fees.py +50 -0
  130. brawny/tx/intent.py +423 -0
  131. brawny/tx/monitor.py +628 -0
  132. brawny/tx/nonce.py +498 -0
  133. brawny/tx/replacement.py +456 -0
  134. brawny/tx/utils.py +26 -0
  135. brawny/utils.py +205 -0
  136. brawny/validation.py +69 -0
  137. brawny-0.1.13.dist-info/METADATA +156 -0
  138. brawny-0.1.13.dist-info/RECORD +141 -0
  139. brawny-0.1.13.dist-info/WHEEL +5 -0
  140. brawny-0.1.13.dist-info/entry_points.txt +2 -0
  141. brawny-0.1.13.dist-info/top_level.txt +1 -0
brawny/db/queries.py ADDED
@@ -0,0 +1,584 @@
1
+ """Canonical SQL queries for brawny database operations.
2
+
3
+ All queries use :name placeholder style.
4
+ - SQLite: Supports :name natively with dict params
5
+ - Postgres: Rewritten to %(name)s in postgres.py
6
+
7
+ Dialect-specific queries use dict format: {"postgres": "...", "sqlite": "..."}
8
+ """
9
+
10
+ from __future__ import annotations
11
+
12
+ # =============================================================================
13
+ # Block State
14
+ # =============================================================================
15
+
16
+ GET_BLOCK_STATE = """
17
+ SELECT * FROM block_state WHERE chain_id = :chain_id
18
+ """
19
+
20
+ UPSERT_BLOCK_STATE = """
21
+ INSERT INTO block_state (chain_id, last_processed_block_number, last_processed_block_hash)
22
+ VALUES (:chain_id, :block_number, :block_hash)
23
+ ON CONFLICT(chain_id) DO UPDATE SET
24
+ last_processed_block_number = EXCLUDED.last_processed_block_number,
25
+ last_processed_block_hash = EXCLUDED.last_processed_block_hash,
26
+ updated_at = CURRENT_TIMESTAMP
27
+ """
28
+
29
+ GET_BLOCK_HASH_AT_HEIGHT = """
30
+ SELECT block_hash FROM block_hash_history
31
+ WHERE chain_id = :chain_id AND block_number = :block_number
32
+ """
33
+
34
+ INSERT_BLOCK_HASH = """
35
+ INSERT INTO block_hash_history (chain_id, block_number, block_hash)
36
+ VALUES (:chain_id, :block_number, :block_hash)
37
+ ON CONFLICT(chain_id, block_number) DO UPDATE SET
38
+ block_hash = EXCLUDED.block_hash,
39
+ inserted_at = CURRENT_TIMESTAMP
40
+ """
41
+
42
+ DELETE_BLOCK_HASHES_ABOVE = """
43
+ DELETE FROM block_hash_history
44
+ WHERE chain_id = :chain_id AND block_number > :block_number
45
+ """
46
+
47
+ DELETE_BLOCK_HASH_AT_HEIGHT = """
48
+ DELETE FROM block_hash_history
49
+ WHERE chain_id = :chain_id AND block_number = :block_number
50
+ """
51
+
52
+ GET_MAX_BLOCK_IN_HISTORY = """
53
+ SELECT MAX(block_number) as max_block FROM block_hash_history
54
+ WHERE chain_id = :chain_id
55
+ """
56
+
57
+ DELETE_BLOCK_HASHES_BELOW = """
58
+ DELETE FROM block_hash_history
59
+ WHERE chain_id = :chain_id AND block_number < :cutoff
60
+ """
61
+
62
+ GET_OLDEST_BLOCK_IN_HISTORY = """
63
+ SELECT MIN(block_number) as min_block FROM block_hash_history
64
+ WHERE chain_id = :chain_id
65
+ """
66
+
67
+ # =============================================================================
68
+ # Jobs
69
+ # =============================================================================
70
+
71
+ GET_JOB = "SELECT * FROM jobs WHERE job_id = :job_id"
72
+
73
+ GET_ENABLED_JOBS = "SELECT * FROM jobs WHERE enabled = 1 ORDER BY job_id"
74
+
75
+ LIST_ALL_JOBS = "SELECT * FROM jobs ORDER BY job_id"
76
+
77
+ UPSERT_JOB = """
78
+ INSERT INTO jobs (job_id, job_name, check_interval_blocks, enabled)
79
+ VALUES (:job_id, :job_name, :check_interval_blocks, :enabled)
80
+ ON CONFLICT(job_id) DO UPDATE SET
81
+ job_name = EXCLUDED.job_name,
82
+ check_interval_blocks = EXCLUDED.check_interval_blocks,
83
+ updated_at = CURRENT_TIMESTAMP
84
+ """
85
+
86
+ UPDATE_JOB_ENABLED = """
87
+ UPDATE jobs SET enabled = :enabled, updated_at = CURRENT_TIMESTAMP
88
+ WHERE job_id = :job_id
89
+ """
90
+
91
+ UPDATE_JOB_CHECKED = """
92
+ UPDATE jobs SET
93
+ last_checked_block_number = :block_number,
94
+ updated_at = CURRENT_TIMESTAMP
95
+ WHERE job_id = :job_id
96
+ """
97
+
98
+ UPDATE_JOB_TRIGGERED = """
99
+ UPDATE jobs SET
100
+ last_checked_block_number = :block_number,
101
+ last_triggered_block_number = :block_number,
102
+ updated_at = CURRENT_TIMESTAMP
103
+ WHERE job_id = :job_id
104
+ """
105
+
106
+ DELETE_JOB = "DELETE FROM jobs WHERE job_id = :job_id"
107
+
108
+ # =============================================================================
109
+ # Job KV Store
110
+ # =============================================================================
111
+
112
+ GET_JOB_KV = """
113
+ SELECT value_json FROM job_kv
114
+ WHERE job_id = :job_id AND key = :key
115
+ """
116
+
117
+ UPSERT_JOB_KV = """
118
+ INSERT INTO job_kv (job_id, key, value_json)
119
+ VALUES (:job_id, :key, :value_json)
120
+ ON CONFLICT(job_id, key) DO UPDATE SET
121
+ value_json = EXCLUDED.value_json,
122
+ updated_at = CURRENT_TIMESTAMP
123
+ """
124
+
125
+ DELETE_JOB_KV = """
126
+ DELETE FROM job_kv WHERE job_id = :job_id AND key = :key
127
+ """
128
+
129
+ DELETE_ALL_JOB_KV = """
130
+ DELETE FROM job_kv WHERE job_id = :job_id
131
+ """
132
+
133
+ # =============================================================================
134
+ # Signers / Nonce Management
135
+ # =============================================================================
136
+
137
+ GET_SIGNER = """
138
+ SELECT * FROM signers
139
+ WHERE chain_id = :chain_id AND signer_address = :address
140
+ """
141
+
142
+ LIST_SIGNERS = """
143
+ SELECT * FROM signers WHERE chain_id = :chain_id ORDER BY signer_address
144
+ """
145
+
146
+ UPSERT_SIGNER = """
147
+ INSERT INTO signers (chain_id, signer_address, next_nonce, last_synced_chain_nonce)
148
+ VALUES (:chain_id, :address, :next_nonce, :last_synced_chain_nonce)
149
+ ON CONFLICT(chain_id, signer_address) DO UPDATE SET
150
+ next_nonce = EXCLUDED.next_nonce,
151
+ last_synced_chain_nonce = EXCLUDED.last_synced_chain_nonce,
152
+ updated_at = CURRENT_TIMESTAMP
153
+ """
154
+
155
+ UPDATE_SIGNER_NEXT_NONCE = """
156
+ UPDATE signers SET next_nonce = :next_nonce, updated_at = CURRENT_TIMESTAMP
157
+ WHERE chain_id = :chain_id AND signer_address = :address
158
+ """
159
+
160
+ UPDATE_SIGNER_CHAIN_NONCE = """
161
+ UPDATE signers SET last_synced_chain_nonce = :chain_nonce, updated_at = CURRENT_TIMESTAMP
162
+ WHERE chain_id = :chain_id AND signer_address = :address
163
+ """
164
+
165
+ SET_GAP_STARTED_AT = """
166
+ UPDATE signers SET gap_started_at = :started_at, updated_at = CURRENT_TIMESTAMP
167
+ WHERE chain_id = :chain_id AND signer_address = :address
168
+ """
169
+
170
+ CLEAR_GAP_STARTED_AT = """
171
+ UPDATE signers SET gap_started_at = NULL, updated_at = CURRENT_TIMESTAMP
172
+ WHERE chain_id = :chain_id AND signer_address = :address
173
+ """
174
+
175
+ GET_SIGNER_BY_ALIAS = """
176
+ SELECT * FROM signers
177
+ WHERE chain_id = :chain_id AND alias = :alias
178
+ """
179
+
180
+ # =============================================================================
181
+ # Nonce Reservations
182
+ # =============================================================================
183
+
184
+ GET_NONCE_RESERVATION = """
185
+ SELECT * FROM nonce_reservations
186
+ WHERE chain_id = :chain_id AND signer_address = :address AND nonce = :nonce
187
+ """
188
+
189
+ GET_RESERVATIONS_FOR_SIGNER = """
190
+ SELECT * FROM nonce_reservations
191
+ WHERE chain_id = :chain_id AND signer_address = :address
192
+ ORDER BY nonce
193
+ """
194
+
195
+ GET_RESERVATIONS_FOR_SIGNER_WITH_STATUS = """
196
+ SELECT * FROM nonce_reservations
197
+ WHERE chain_id = :chain_id AND signer_address = :address AND status = :status
198
+ ORDER BY nonce
199
+ """
200
+
201
+ GET_RESERVATIONS_BELOW_NONCE = """
202
+ SELECT * FROM nonce_reservations
203
+ WHERE chain_id = :chain_id AND signer_address = :address AND nonce < :nonce
204
+ ORDER BY nonce
205
+ """
206
+
207
+ GET_NON_RELEASED_RESERVATIONS = """
208
+ SELECT * FROM nonce_reservations
209
+ WHERE chain_id = :chain_id AND signer_address = :address
210
+ AND status != :released_status
211
+ AND nonce >= :base_nonce
212
+ ORDER BY nonce
213
+ """
214
+
215
+ UPSERT_NONCE_RESERVATION = """
216
+ INSERT INTO nonce_reservations (chain_id, signer_address, nonce, status, intent_id)
217
+ VALUES (:chain_id, :address, :nonce, :status, :intent_id)
218
+ ON CONFLICT(chain_id, signer_address, nonce) DO UPDATE SET
219
+ status = EXCLUDED.status,
220
+ intent_id = EXCLUDED.intent_id,
221
+ updated_at = CURRENT_TIMESTAMP
222
+ """
223
+
224
+ UPDATE_NONCE_RESERVATION_STATUS = """
225
+ UPDATE nonce_reservations SET status = :status, updated_at = CURRENT_TIMESTAMP
226
+ WHERE chain_id = :chain_id AND signer_address = :address AND nonce = :nonce
227
+ """
228
+
229
+ UPDATE_NONCE_RESERVATION_STATUS_WITH_INTENT = """
230
+ UPDATE nonce_reservations SET status = :status, intent_id = :intent_id, updated_at = CURRENT_TIMESTAMP
231
+ WHERE chain_id = :chain_id AND signer_address = :address AND nonce = :nonce
232
+ """
233
+
234
+ # Dialect-specific: lock signer for nonce reservation
235
+ # Postgres uses FOR UPDATE, SQLite doesn't need it (uses BEGIN IMMEDIATE)
236
+ LOCK_SIGNER_FOR_UPDATE = {
237
+ "postgres": """
238
+ SELECT * FROM signers
239
+ WHERE chain_id = :chain_id AND signer_address = :address
240
+ FOR UPDATE
241
+ """,
242
+ "sqlite": """
243
+ SELECT * FROM signers
244
+ WHERE chain_id = :chain_id AND signer_address = :address
245
+ """,
246
+ }
247
+
248
+ ENSURE_SIGNER_EXISTS = """
249
+ INSERT INTO signers (chain_id, signer_address, next_nonce, last_synced_chain_nonce)
250
+ VALUES (:chain_id, :address, 0, NULL)
251
+ ON CONFLICT(chain_id, signer_address) DO NOTHING
252
+ """
253
+
254
+ # Dialect-specific: cleanup orphaned nonces
255
+ CLEANUP_ORPHANED_NONCES = {
256
+ "postgres": """
257
+ DELETE FROM nonce_reservations
258
+ WHERE chain_id = :chain_id
259
+ AND status = 'orphaned'
260
+ AND updated_at < NOW() - make_interval(hours => :hours)
261
+ """,
262
+ "sqlite": """
263
+ DELETE FROM nonce_reservations
264
+ WHERE chain_id = :chain_id
265
+ AND status = 'orphaned'
266
+ AND updated_at < datetime('now', :hours_offset)
267
+ """,
268
+ }
269
+
270
+ # =============================================================================
271
+ # Intents
272
+ # =============================================================================
273
+
274
+ CREATE_INTENT = """
275
+ INSERT INTO tx_intents (
276
+ intent_id, job_id, chain_id, signer_address, idempotency_key,
277
+ to_address, data, value_wei, gas_limit, max_fee_per_gas,
278
+ max_priority_fee_per_gas, min_confirmations, deadline_ts,
279
+ broadcast_group, broadcast_endpoints_json, retry_after, status,
280
+ metadata_json
281
+ ) VALUES (
282
+ :intent_id, :job_id, :chain_id, :signer_address, :idempotency_key,
283
+ :to_address, :data, :value_wei, :gas_limit, :max_fee_per_gas,
284
+ :max_priority_fee_per_gas, :min_confirmations, :deadline_ts,
285
+ :broadcast_group, :broadcast_endpoints_json, NULL, 'created',
286
+ :metadata_json
287
+ )
288
+ ON CONFLICT (chain_id, signer_address, idempotency_key) DO NOTHING
289
+ RETURNING *
290
+ """
291
+
292
+ GET_INTENT = "SELECT * FROM tx_intents WHERE intent_id = :intent_id"
293
+
294
+ GET_INTENT_BY_IDEMPOTENCY_KEY = """
295
+ SELECT * FROM tx_intents
296
+ WHERE chain_id = :chain_id
297
+ AND signer_address = :signer_address
298
+ AND idempotency_key = :idempotency_key
299
+ """
300
+
301
+ # Dialect-specific: claim next intent (uses FOR UPDATE SKIP LOCKED on Postgres)
302
+ CLAIM_NEXT_INTENT = {
303
+ "postgres": """
304
+ WITH claimed AS (
305
+ SELECT intent_id FROM tx_intents
306
+ WHERE status = 'created'
307
+ AND (deadline_ts IS NULL OR deadline_ts > CURRENT_TIMESTAMP)
308
+ AND (retry_after IS NULL OR retry_after <= CURRENT_TIMESTAMP)
309
+ ORDER BY created_at ASC
310
+ FOR UPDATE SKIP LOCKED
311
+ LIMIT 1
312
+ )
313
+ UPDATE tx_intents
314
+ SET status = 'claimed', claim_token = :claim_token,
315
+ claimed_at = CURRENT_TIMESTAMP, claimed_by = :claimed_by,
316
+ retry_after = NULL,
317
+ updated_at = CURRENT_TIMESTAMP
318
+ WHERE intent_id = (SELECT intent_id FROM claimed)
319
+ RETURNING *
320
+ """,
321
+ "sqlite": """
322
+ UPDATE tx_intents
323
+ SET status = 'claimed', claim_token = :claim_token,
324
+ claimed_at = CURRENT_TIMESTAMP, claimed_by = :claimed_by,
325
+ retry_after = NULL,
326
+ updated_at = CURRENT_TIMESTAMP
327
+ WHERE intent_id = (
328
+ SELECT intent_id FROM tx_intents
329
+ WHERE status = 'created'
330
+ AND (deadline_ts IS NULL OR deadline_ts > CURRENT_TIMESTAMP)
331
+ AND (retry_after IS NULL OR retry_after <= CURRENT_TIMESTAMP)
332
+ ORDER BY created_at ASC
333
+ LIMIT 1
334
+ )
335
+ RETURNING *
336
+ """,
337
+ }
338
+
339
+ UPDATE_INTENT_STATUS = """
340
+ UPDATE tx_intents
341
+ SET status = :status, updated_at = CURRENT_TIMESTAMP
342
+ WHERE intent_id = :intent_id
343
+ """
344
+
345
+ UPDATE_INTENT_TO_SENDING = """
346
+ UPDATE tx_intents
347
+ SET status = 'sending', updated_at = CURRENT_TIMESTAMP
348
+ WHERE intent_id = :intent_id AND status = 'claimed' AND claim_token = :claim_token
349
+ """
350
+
351
+ UPDATE_INTENT_TO_PENDING = """
352
+ UPDATE tx_intents
353
+ SET status = 'pending', updated_at = CURRENT_TIMESTAMP
354
+ WHERE intent_id = :intent_id
355
+ """
356
+
357
+ UPDATE_INTENT_TO_CONFIRMED = """
358
+ UPDATE tx_intents
359
+ SET status = 'confirmed', updated_at = CURRENT_TIMESTAMP
360
+ WHERE intent_id = :intent_id
361
+ """
362
+
363
+ UPDATE_INTENT_TO_FAILED = """
364
+ UPDATE tx_intents
365
+ SET status = 'failed', updated_at = CURRENT_TIMESTAMP
366
+ WHERE intent_id = :intent_id
367
+ """
368
+
369
+ UPDATE_INTENT_TO_REVERTED = """
370
+ UPDATE tx_intents
371
+ SET status = 'reverted', updated_at = CURRENT_TIMESTAMP
372
+ WHERE intent_id = :intent_id
373
+ """
374
+
375
+ UPDATE_INTENT_RETRY_AFTER = """
376
+ UPDATE tx_intents
377
+ SET retry_after = :retry_after, retry_count = retry_count + 1, updated_at = CURRENT_TIMESTAMP
378
+ WHERE intent_id = :intent_id
379
+ """
380
+
381
+ RELEASE_INTENT_CLAIM = """
382
+ UPDATE tx_intents
383
+ SET status = 'created', claim_token = NULL, claimed_at = NULL,
384
+ retry_after = :retry_after, updated_at = CURRENT_TIMESTAMP
385
+ WHERE intent_id = :intent_id AND claim_token = :claim_token
386
+ """
387
+
388
+ UPDATE_INTENT_BROADCAST_BINDING = """
389
+ UPDATE tx_intents
390
+ SET broadcast_group = :broadcast_group, broadcast_endpoints_json = :broadcast_endpoints_json,
391
+ updated_at = CURRENT_TIMESTAMP
392
+ WHERE intent_id = :intent_id
393
+ """
394
+
395
+ GET_INTENT_COUNT_BY_STATUS = """
396
+ SELECT COUNT(*) AS count FROM tx_intents
397
+ WHERE status IN ({placeholders}) AND job_id = :job_id
398
+ """
399
+
400
+ GET_PENDING_INTENT_COUNT = """
401
+ SELECT COUNT(*) AS count FROM tx_intents
402
+ WHERE status IN ({placeholders})
403
+ """
404
+
405
+ GET_BACKING_OFF_INTENT_COUNT = """
406
+ SELECT COUNT(*) AS count FROM tx_intents
407
+ WHERE retry_after > CURRENT_TIMESTAMP
408
+ """
409
+
410
+ # =============================================================================
411
+ # Attempts
412
+ # =============================================================================
413
+
414
+ CREATE_ATTEMPT = """
415
+ INSERT INTO tx_attempts (
416
+ attempt_id, intent_id, nonce, tx_hash, gas_params_json,
417
+ status, broadcast_block, broadcast_at, broadcast_group, endpoint_url
418
+ ) VALUES (
419
+ :attempt_id, :intent_id, :nonce, :tx_hash, :gas_params_json,
420
+ :status, :broadcast_block, :broadcast_at, :broadcast_group, :endpoint_url
421
+ )
422
+ RETURNING *
423
+ """
424
+
425
+ GET_ATTEMPT = "SELECT * FROM tx_attempts WHERE attempt_id = :attempt_id"
426
+
427
+ GET_ATTEMPT_BY_TX_HASH = "SELECT * FROM tx_attempts WHERE tx_hash = :tx_hash"
428
+
429
+ GET_ATTEMPTS_FOR_INTENT = """
430
+ SELECT * FROM tx_attempts WHERE intent_id = :intent_id ORDER BY created_at DESC
431
+ """
432
+
433
+ GET_LATEST_ATTEMPT_FOR_INTENT = """
434
+ SELECT * FROM tx_attempts
435
+ WHERE intent_id = :intent_id
436
+ ORDER BY created_at DESC
437
+ LIMIT 1
438
+ """
439
+
440
+ UPDATE_ATTEMPT_STATUS = """
441
+ UPDATE tx_attempts
442
+ SET status = :status, updated_at = CURRENT_TIMESTAMP
443
+ WHERE attempt_id = :attempt_id
444
+ """
445
+
446
+ UPDATE_ATTEMPT_INCLUDED = """
447
+ UPDATE tx_attempts
448
+ SET status = :status, included_block = :included_block, updated_at = CURRENT_TIMESTAMP
449
+ WHERE attempt_id = :attempt_id
450
+ """
451
+
452
+ UPDATE_ATTEMPT_ERROR = """
453
+ UPDATE tx_attempts
454
+ SET status = :status, error_code = :error_code, error_detail = :error_detail,
455
+ updated_at = CURRENT_TIMESTAMP
456
+ WHERE attempt_id = :attempt_id
457
+ """
458
+
459
+ GET_PENDING_ATTEMPTS = """
460
+ SELECT * FROM tx_attempts
461
+ WHERE status = 'pending' AND intent_id IN (
462
+ SELECT intent_id FROM tx_intents WHERE chain_id = :chain_id
463
+ )
464
+ ORDER BY created_at ASC
465
+ """
466
+
467
+ # =============================================================================
468
+ # ABI Cache
469
+ # =============================================================================
470
+
471
+ GET_ABI_CACHE = """
472
+ SELECT * FROM abi_cache
473
+ WHERE chain_id = :chain_id AND address = :address
474
+ """
475
+
476
+ UPSERT_ABI_CACHE = """
477
+ INSERT INTO abi_cache (chain_id, address, abi_json, source)
478
+ VALUES (:chain_id, :address, :abi_json, :source)
479
+ ON CONFLICT(chain_id, address) DO UPDATE SET
480
+ abi_json = EXCLUDED.abi_json,
481
+ source = EXCLUDED.source,
482
+ resolved_at = CURRENT_TIMESTAMP
483
+ """
484
+
485
+ DELETE_ABI_CACHE = """
486
+ DELETE FROM abi_cache WHERE chain_id = :chain_id AND address = :address
487
+ """
488
+
489
+ # =============================================================================
490
+ # Proxy Cache
491
+ # =============================================================================
492
+
493
+ GET_PROXY_CACHE = """
494
+ SELECT * FROM proxy_cache
495
+ WHERE chain_id = :chain_id AND proxy_address = :proxy_address
496
+ """
497
+
498
+ UPSERT_PROXY_CACHE = """
499
+ INSERT INTO proxy_cache (chain_id, proxy_address, implementation_address)
500
+ VALUES (:chain_id, :proxy_address, :implementation_address)
501
+ ON CONFLICT(chain_id, proxy_address) DO UPDATE SET
502
+ implementation_address = EXCLUDED.implementation_address,
503
+ resolved_at = CURRENT_TIMESTAMP
504
+ """
505
+
506
+ DELETE_PROXY_CACHE = """
507
+ DELETE FROM proxy_cache WHERE chain_id = :chain_id AND proxy_address = :proxy_address
508
+ """
509
+
510
+ # =============================================================================
511
+ # Stuck Transaction Detection
512
+ # =============================================================================
513
+
514
+ GET_STUCK_SENDING_INTENTS = """
515
+ SELECT * FROM tx_intents
516
+ WHERE status = 'sending'
517
+ AND updated_at < :cutoff_time
518
+ """
519
+
520
+ GET_STUCK_PENDING_INTENTS = """
521
+ SELECT * FROM tx_intents
522
+ WHERE status = 'pending'
523
+ AND updated_at < :cutoff_time
524
+ """
525
+
526
+ # =============================================================================
527
+ # Cleanup / Maintenance
528
+ # =============================================================================
529
+
530
+ DELETE_OLD_CONFIRMED_INTENTS = """
531
+ DELETE FROM tx_intents
532
+ WHERE status IN ('confirmed', 'failed', 'reverted')
533
+ AND updated_at < :cutoff_time
534
+ """
535
+
536
+ DELETE_ABANDONED_INTENTS = """
537
+ DELETE FROM tx_intents
538
+ WHERE status = 'created'
539
+ AND created_at < :cutoff_time
540
+ """
541
+
542
+ # =============================================================================
543
+ # Job Logs
544
+ # =============================================================================
545
+
546
+ INSERT_JOB_LOG = """
547
+ INSERT INTO job_logs (chain_id, job_id, block_number, level, fields_json)
548
+ VALUES (:chain_id, :job_id, :block_number, :level, :fields_json)
549
+ """
550
+
551
+ LIST_JOB_LOGS = """
552
+ SELECT * FROM job_logs
553
+ WHERE chain_id = :chain_id AND job_id = :job_id
554
+ ORDER BY ts DESC
555
+ LIMIT :limit
556
+ """
557
+
558
+ LIST_ALL_JOB_LOGS = """
559
+ SELECT * FROM job_logs
560
+ WHERE chain_id = :chain_id
561
+ ORDER BY ts DESC
562
+ LIMIT :limit
563
+ """
564
+
565
+ LIST_LATEST_JOB_LOGS = {
566
+ "postgres": """
567
+ SELECT DISTINCT ON (job_id) *
568
+ FROM job_logs
569
+ WHERE chain_id = :chain_id
570
+ ORDER BY job_id, ts DESC
571
+ """,
572
+ "sqlite": """
573
+ SELECT * FROM job_logs l1
574
+ WHERE chain_id = :chain_id
575
+ AND ts = (SELECT MAX(ts) FROM job_logs l2
576
+ WHERE l2.job_id = l1.job_id AND l2.chain_id = :chain_id)
577
+ ORDER BY job_id
578
+ """,
579
+ }
580
+
581
+ DELETE_OLD_JOB_LOGS = """
582
+ DELETE FROM job_logs
583
+ WHERE chain_id = :chain_id AND ts < :cutoff
584
+ """