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/tx/nonce.py ADDED
@@ -0,0 +1,498 @@
1
+ """Centralized nonce manager for transaction execution.
2
+
3
+ Implements the nonce management strategy from SPEC 8:
4
+ - Reserve nonce with SERIALIZABLE isolation
5
+ - Nonce status transitions (reserved → in_flight → released/orphaned)
6
+ - Reconciliation loop for startup and periodic sync
7
+ - SQLite-specific locking for development
8
+
9
+ Jobs NEVER allocate or set nonces - the nonce manager owns all nonce operations.
10
+ """
11
+
12
+ from __future__ import annotations
13
+
14
+ from contextlib import contextmanager
15
+ from typing import TYPE_CHECKING, Generator
16
+ from uuid import UUID
17
+
18
+ from web3 import Web3
19
+
20
+ from brawny.logging import LogEvents, get_logger
21
+ from brawny.model.enums import NonceStatus
22
+
23
+ if TYPE_CHECKING:
24
+ from brawny.db.base import Database
25
+ from brawny.model.types import NonceReservation
26
+ from brawny._rpc.manager import RPCManager
27
+
28
+ logger = get_logger(__name__)
29
+
30
+
31
+ class NonceManager:
32
+ """Centralized nonce manager for transaction execution.
33
+
34
+ Provides atomic nonce reservation with database-backed persistence.
35
+ Handles multiple in-flight nonces per signer to prevent global blocking.
36
+
37
+ Thread-safe: Uses database transactions for concurrency control.
38
+ """
39
+
40
+ def __init__(
41
+ self,
42
+ db: Database,
43
+ rpc: RPCManager,
44
+ chain_id: int,
45
+ ) -> None:
46
+ """Initialize nonce manager.
47
+
48
+ Args:
49
+ db: Database connection
50
+ rpc: RPC manager for chain state queries
51
+ chain_id: Chain ID for nonce tracking
52
+ """
53
+ self._db = db
54
+ self._rpc = rpc
55
+ self._chain_id = chain_id
56
+
57
+ def reserve_nonce(
58
+ self,
59
+ signer_address: str,
60
+ intent_id: UUID | None = None,
61
+ ) -> int:
62
+ """Reserve the next available nonce for a signer.
63
+
64
+ Algorithm:
65
+ 1. Lock signer row (or create if not exists)
66
+ 2. Fetch chain pending nonce
67
+ 3. Calculate base nonce as max(chain_nonce, db_next_nonce)
68
+ 4. Find next available nonce (skip existing reservations)
69
+ 5. Create reservation and update signer's next_nonce
70
+
71
+ Args:
72
+ signer_address: Ethereum address of the signer
73
+ intent_id: Optional intent ID to associate with reservation
74
+
75
+ Returns:
76
+ The reserved nonce value
77
+
78
+ Raises:
79
+ Exception: If reservation fails
80
+ """
81
+ signer_address = signer_address.lower()
82
+
83
+ try:
84
+ chain_nonce = self._rpc.get_transaction_count(
85
+ Web3.to_checksum_address(signer_address), block_identifier="pending"
86
+ )
87
+ except Exception as e:
88
+ logger.warning(
89
+ "nonce.chain_fetch_failed",
90
+ signer=signer_address,
91
+ error=str(e),
92
+ )
93
+ chain_nonce = None
94
+
95
+ nonce = self._db.reserve_nonce_atomic(
96
+ chain_id=self._chain_id,
97
+ address=signer_address,
98
+ chain_nonce=chain_nonce,
99
+ intent_id=intent_id,
100
+ )
101
+
102
+ logger.debug(
103
+ LogEvents.NONCE_RESERVE,
104
+ signer=signer_address,
105
+ nonce=nonce,
106
+ chain_nonce=chain_nonce,
107
+ intent_id=str(intent_id) if intent_id else None,
108
+ )
109
+
110
+ return nonce
111
+
112
+ def mark_in_flight(
113
+ self,
114
+ signer_address: str,
115
+ nonce: int,
116
+ intent_id: UUID,
117
+ ) -> bool:
118
+ """Mark a nonce reservation as in-flight (after broadcast).
119
+
120
+ Args:
121
+ signer_address: Ethereum address of the signer
122
+ nonce: The nonce value
123
+ intent_id: Intent ID to associate
124
+
125
+ Returns:
126
+ True if updated successfully
127
+ """
128
+ signer_address = signer_address.lower()
129
+ return self._db.update_nonce_reservation_status(
130
+ chain_id=self._chain_id,
131
+ address=signer_address,
132
+ nonce=nonce,
133
+ status=NonceStatus.IN_FLIGHT.value,
134
+ intent_id=intent_id,
135
+ )
136
+
137
+ def release(
138
+ self,
139
+ signer_address: str,
140
+ nonce: int,
141
+ ) -> bool:
142
+ """Release a nonce reservation (after confirm/fail/abandon).
143
+
144
+ Args:
145
+ signer_address: Ethereum address of the signer
146
+ nonce: The nonce value
147
+
148
+ Returns:
149
+ True if released successfully
150
+ """
151
+ signer_address = signer_address.lower()
152
+ return self._db.release_nonce_reservation(
153
+ self._chain_id, signer_address, nonce
154
+ )
155
+
156
+ @contextmanager
157
+ def reserved(
158
+ self,
159
+ signer_address: str,
160
+ intent_id: UUID | None = None,
161
+ ) -> Generator[int, None, None]:
162
+ """Context manager for nonce reservation with automatic release on failure.
163
+
164
+ Automatically releases the nonce if an exception occurs within the context.
165
+ On success path, caller is responsible for calling mark_in_flight() to
166
+ transition the nonce to in-flight status.
167
+
168
+ Usage:
169
+ with nonce_manager.reserved(signer) as nonce:
170
+ # Build and sign transaction with nonce
171
+ # If exception raised, nonce is automatically released
172
+
173
+ # After context, caller should call mark_in_flight() on success
174
+
175
+ Args:
176
+ signer_address: Ethereum address of the signer
177
+ intent_id: Optional intent ID to associate with reservation
178
+
179
+ Yields:
180
+ Reserved nonce value
181
+
182
+ Raises:
183
+ Exception: Re-raises any exception after releasing the nonce
184
+ """
185
+ signer_address = signer_address.lower()
186
+ nonce = self.reserve_nonce(signer_address, intent_id)
187
+
188
+ try:
189
+ yield nonce
190
+ except Exception:
191
+ # Release nonce on any exception
192
+ self.release(signer_address, nonce)
193
+ logger.debug(
194
+ "nonce.released_on_error",
195
+ signer=signer_address,
196
+ nonce=nonce,
197
+ )
198
+ raise
199
+
200
+ def mark_orphaned(
201
+ self,
202
+ signer_address: str,
203
+ nonce: int,
204
+ ) -> bool:
205
+ """Mark a nonce as orphaned (nonce used but no tx found).
206
+
207
+ Args:
208
+ signer_address: Ethereum address of the signer
209
+ nonce: The nonce value
210
+
211
+ Returns:
212
+ True if updated successfully
213
+ """
214
+ signer_address = signer_address.lower()
215
+ updated = self._db.update_nonce_reservation_status(
216
+ chain_id=self._chain_id,
217
+ address=signer_address,
218
+ nonce=nonce,
219
+ status=NonceStatus.ORPHANED.value,
220
+ )
221
+ if updated:
222
+ logger.warning(
223
+ LogEvents.NONCE_ORPHANED,
224
+ signer=signer_address,
225
+ nonce=nonce,
226
+ )
227
+ return updated
228
+
229
+ def get_reservation(
230
+ self,
231
+ signer_address: str,
232
+ nonce: int,
233
+ ) -> NonceReservation | None:
234
+ """Get a specific nonce reservation.
235
+
236
+ Args:
237
+ signer_address: Ethereum address of the signer
238
+ nonce: The nonce value
239
+
240
+ Returns:
241
+ Reservation if found, None otherwise
242
+ """
243
+ return self._db.get_nonce_reservation(
244
+ self._chain_id, signer_address.lower(), nonce
245
+ )
246
+
247
+ def get_active_reservations(
248
+ self,
249
+ signer_address: str,
250
+ ) -> list[NonceReservation]:
251
+ """Get all active (non-released) reservations for a signer.
252
+
253
+ Args:
254
+ signer_address: Ethereum address of the signer
255
+
256
+ Returns:
257
+ List of active reservations
258
+ """
259
+ all_reservations = self._db.get_reservations_for_signer(
260
+ self._chain_id, signer_address.lower()
261
+ )
262
+ return [
263
+ r for r in all_reservations
264
+ if r.status not in (NonceStatus.RELEASED,)
265
+ ]
266
+
267
+ def reconcile(self, signer_address: str | None = None) -> dict[str, int]:
268
+ """Reconcile nonce reservations with chain state.
269
+
270
+ Run at startup and periodically to:
271
+ - Reset next_nonce when gap detected (CRITICAL for recovery)
272
+ - Update signer's synced chain nonce
273
+ - Mark stale reservations as released or orphaned
274
+ - Clean up confirmed/used nonces
275
+ - Release gap reservations (nonces >= chain_nonce with no tx in mempool)
276
+
277
+ Args:
278
+ signer_address: Optional specific signer to reconcile.
279
+ If None, reconciles all signers.
280
+
281
+ Returns:
282
+ Dictionary with reconciliation stats
283
+ """
284
+ stats = {
285
+ "signers_checked": 0,
286
+ "nonces_released": 0,
287
+ "nonces_orphaned": 0,
288
+ "orphans_cleaned": 0,
289
+ "next_nonce_reset": 0,
290
+ "gap_reservations_released": 0,
291
+ }
292
+
293
+ if signer_address:
294
+ signers = [self._db.get_signer_state(self._chain_id, signer_address.lower())]
295
+ signers = [s for s in signers if s is not None]
296
+ else:
297
+ signers = self._db.get_all_signers(self._chain_id)
298
+
299
+ for signer in signers:
300
+ stats["signers_checked"] += 1
301
+
302
+ try:
303
+ # Get current chain nonce
304
+ chain_nonce = self._rpc.get_transaction_count(
305
+ Web3.to_checksum_address(signer.signer_address), block_identifier="pending"
306
+ )
307
+
308
+ # Update signer's synced chain nonce
309
+ self._db.update_signer_chain_nonce(
310
+ self._chain_id, signer.signer_address, chain_nonce
311
+ )
312
+
313
+ # CRITICAL FIX: Reset next_nonce when gap detected
314
+ # Without this, reserve_nonce_atomic() keeps returning stale nonces
315
+ if chain_nonce < signer.next_nonce:
316
+ gap_size = signer.next_nonce - chain_nonce
317
+ logger.warning(
318
+ "nonce.gap_reset",
319
+ signer=signer.signer_address,
320
+ old_next_nonce=signer.next_nonce,
321
+ chain_nonce=chain_nonce,
322
+ gap_size=gap_size,
323
+ )
324
+ self._db.update_signer_next_nonce(
325
+ self._chain_id, signer.signer_address, chain_nonce
326
+ )
327
+ stats["next_nonce_reset"] += 1
328
+
329
+ # Release all non-released reservations >= chain_nonce
330
+ # These are "gap" reservations whose txs are no longer in mempool
331
+ gap_reservations = self._db.get_reservations_for_signer(
332
+ self._chain_id, signer.signer_address
333
+ )
334
+ for reservation in gap_reservations:
335
+ if reservation.status == NonceStatus.RELEASED:
336
+ continue
337
+ if reservation.nonce >= chain_nonce:
338
+ # This reservation is in the gap - tx doesn't exist
339
+ self.release(signer.signer_address, reservation.nonce)
340
+ stats["gap_reservations_released"] += 1
341
+ logger.debug(
342
+ "nonce.gap_reservation_released",
343
+ signer=signer.signer_address,
344
+ nonce=reservation.nonce,
345
+ intent_id=str(reservation.intent_id) if reservation.intent_id else None,
346
+ )
347
+
348
+ # Get stale reservations (nonce < chain_nonce)
349
+ stale_reservations = self._db.get_reservations_below_nonce(
350
+ self._chain_id, signer.signer_address, chain_nonce
351
+ )
352
+
353
+ for reservation in stale_reservations:
354
+ if reservation.status == NonceStatus.RELEASED:
355
+ # Already released, skip
356
+ continue
357
+
358
+ if reservation.intent_id:
359
+ # Has associated intent - check if confirmed
360
+ attempt = self._db.get_latest_attempt_for_intent(
361
+ reservation.intent_id
362
+ )
363
+ if attempt and attempt.status.value == "confirmed":
364
+ # Confirmed - release the reservation
365
+ self.release(signer.signer_address, reservation.nonce)
366
+ stats["nonces_released"] += 1
367
+ else:
368
+ # Not confirmed but nonce is used - orphaned
369
+ self.mark_orphaned(signer.signer_address, reservation.nonce)
370
+ stats["nonces_orphaned"] += 1
371
+ else:
372
+ # No intent - just release
373
+ self.release(signer.signer_address, reservation.nonce)
374
+ stats["nonces_released"] += 1
375
+
376
+ logger.info(
377
+ LogEvents.NONCE_RECONCILE,
378
+ signer=signer.signer_address,
379
+ chain_nonce=chain_nonce,
380
+ stale_count=len(stale_reservations),
381
+ next_nonce_was_reset=chain_nonce < signer.next_nonce,
382
+ )
383
+
384
+ except Exception as e:
385
+ logger.error(
386
+ "nonce.reconcile.error",
387
+ signer=signer.signer_address,
388
+ error=str(e),
389
+ )
390
+
391
+ # Cleanup old orphaned reservations (24+ hours old)
392
+ stats["orphans_cleaned"] = self.cleanup_orphaned()
393
+
394
+ return stats
395
+
396
+ def cleanup_orphaned(self, older_than_hours: int = 24) -> int:
397
+ """Delete orphaned nonce reservations older than specified hours.
398
+
399
+ Orphaned reservations occur when a nonce was used but no transaction
400
+ was found on-chain. These are safe to delete after some time.
401
+
402
+ Args:
403
+ older_than_hours: Delete orphans older than this (default: 24h)
404
+
405
+ Returns:
406
+ Number of deleted reservations
407
+ """
408
+ deleted = self._db.cleanup_orphaned_nonces(self._chain_id, older_than_hours)
409
+ if deleted > 0:
410
+ logger.info(
411
+ "nonce.orphans_cleaned",
412
+ chain_id=self._chain_id,
413
+ deleted=deleted,
414
+ older_than_hours=older_than_hours,
415
+ )
416
+ return deleted
417
+
418
+ def sync_from_chain(self, signer_address: str) -> int:
419
+ """Sync signer state from chain and return current pending nonce.
420
+
421
+ Use this during startup or after external transactions.
422
+
423
+ Args:
424
+ signer_address: Ethereum address of the signer
425
+
426
+ Returns:
427
+ Current pending nonce from chain
428
+ """
429
+ signer_address = signer_address.lower()
430
+ chain_nonce = self._rpc.get_transaction_count(
431
+ Web3.to_checksum_address(signer_address), block_identifier="pending"
432
+ )
433
+
434
+ # Upsert signer with chain nonce
435
+ self._db.upsert_signer(
436
+ chain_id=self._chain_id,
437
+ address=signer_address,
438
+ next_nonce=chain_nonce,
439
+ last_synced_chain_nonce=chain_nonce,
440
+ )
441
+
442
+ logger.info(
443
+ "nonce.synced_from_chain",
444
+ signer=signer_address,
445
+ chain_nonce=chain_nonce,
446
+ )
447
+
448
+ return chain_nonce
449
+
450
+ def force_reset(self, signer_address: str) -> int:
451
+ """Force reset nonce state to match chain. Returns new next_nonce.
452
+
453
+ USE WITH CAUTION: May cause issues if dropped txs later mine.
454
+
455
+ This will:
456
+ - Query current chain pending nonce
457
+ - Reset local next_nonce to match chain
458
+ - Release all reservations with nonce >= chain_pending_nonce
459
+ - Clear gap tracking
460
+
461
+ Args:
462
+ signer_address: Ethereum address of the signer
463
+
464
+ Returns:
465
+ The new next_nonce (equal to chain pending nonce)
466
+ """
467
+ signer_address = signer_address.lower()
468
+ chain_nonce = self._rpc.get_transaction_count(
469
+ Web3.to_checksum_address(signer_address), block_identifier="pending"
470
+ )
471
+
472
+ # Release all reservations at or above chain nonce
473
+ reservations = self._db.get_reservations_for_signer(
474
+ self._chain_id, signer_address
475
+ )
476
+ released_count = 0
477
+ for r in reservations:
478
+ if r.nonce >= chain_nonce and r.status in (
479
+ NonceStatus.RESERVED,
480
+ NonceStatus.IN_FLIGHT,
481
+ ):
482
+ self.release(signer_address, r.nonce)
483
+ released_count += 1
484
+
485
+ # Reset next_nonce
486
+ self._db.update_signer_next_nonce(self._chain_id, signer_address, chain_nonce)
487
+
488
+ # Clear gap tracking
489
+ self._db.clear_gap_started_at(self._chain_id, signer_address)
490
+
491
+ logger.warning(
492
+ "nonce.force_reset",
493
+ signer=signer_address,
494
+ new_next_nonce=chain_nonce,
495
+ released_reservations=released_count,
496
+ )
497
+
498
+ return chain_nonce