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.
Files changed (135) hide show
  1. brawny/__init__.py +2 -0
  2. brawny/_context.py +5 -5
  3. brawny/_rpc/__init__.py +36 -12
  4. brawny/_rpc/broadcast.py +14 -13
  5. brawny/_rpc/caller.py +243 -0
  6. brawny/_rpc/client.py +539 -0
  7. brawny/_rpc/clients.py +11 -11
  8. brawny/_rpc/context.py +23 -0
  9. brawny/_rpc/errors.py +465 -31
  10. brawny/_rpc/gas.py +7 -6
  11. brawny/_rpc/pool.py +18 -0
  12. brawny/_rpc/retry.py +266 -0
  13. brawny/_rpc/retry_policy.py +81 -0
  14. brawny/accounts.py +28 -9
  15. brawny/alerts/__init__.py +15 -18
  16. brawny/alerts/abi_resolver.py +212 -36
  17. brawny/alerts/base.py +2 -2
  18. brawny/alerts/contracts.py +77 -10
  19. brawny/alerts/errors.py +30 -3
  20. brawny/alerts/events.py +38 -5
  21. brawny/alerts/health.py +19 -13
  22. brawny/alerts/send.py +513 -55
  23. brawny/api.py +39 -11
  24. brawny/assets/AGENTS.md +325 -0
  25. brawny/async_runtime.py +48 -0
  26. brawny/chain.py +3 -3
  27. brawny/cli/commands/__init__.py +2 -0
  28. brawny/cli/commands/console.py +69 -19
  29. brawny/cli/commands/contract.py +2 -2
  30. brawny/cli/commands/controls.py +121 -0
  31. brawny/cli/commands/health.py +2 -2
  32. brawny/cli/commands/job_dev.py +6 -5
  33. brawny/cli/commands/jobs.py +99 -2
  34. brawny/cli/commands/maintenance.py +13 -29
  35. brawny/cli/commands/migrate.py +1 -0
  36. brawny/cli/commands/run.py +10 -3
  37. brawny/cli/commands/script.py +8 -3
  38. brawny/cli/commands/signer.py +143 -26
  39. brawny/cli/helpers.py +0 -3
  40. brawny/cli_templates.py +25 -349
  41. brawny/config/__init__.py +4 -1
  42. brawny/config/models.py +43 -57
  43. brawny/config/parser.py +268 -57
  44. brawny/config/validation.py +52 -15
  45. brawny/daemon/context.py +4 -2
  46. brawny/daemon/core.py +185 -63
  47. brawny/daemon/loops.py +166 -98
  48. brawny/daemon/supervisor.py +261 -0
  49. brawny/db/__init__.py +14 -26
  50. brawny/db/base.py +248 -151
  51. brawny/db/global_cache.py +11 -1
  52. brawny/db/migrate.py +175 -28
  53. brawny/db/migrations/001_init.sql +4 -3
  54. brawny/db/migrations/010_add_nonce_gap_index.sql +1 -1
  55. brawny/db/migrations/011_add_job_logs.sql +1 -2
  56. brawny/db/migrations/012_add_claimed_by.sql +2 -2
  57. brawny/db/migrations/013_attempt_unique.sql +10 -0
  58. brawny/db/migrations/014_add_lease_expires_at.sql +5 -0
  59. brawny/db/migrations/015_add_signer_alias.sql +14 -0
  60. brawny/db/migrations/016_runtime_controls_and_quarantine.sql +32 -0
  61. brawny/db/migrations/017_add_job_drain.sql +6 -0
  62. brawny/db/migrations/018_add_nonce_reset_audit.sql +20 -0
  63. brawny/db/migrations/019_add_job_cooldowns.sql +8 -0
  64. brawny/db/migrations/020_attempt_unique_initial.sql +7 -0
  65. brawny/db/ops/__init__.py +3 -25
  66. brawny/db/ops/logs.py +1 -2
  67. brawny/db/queries.py +47 -91
  68. brawny/db/serialized.py +65 -0
  69. brawny/db/sqlite/__init__.py +1001 -0
  70. brawny/db/sqlite/connection.py +231 -0
  71. brawny/db/sqlite/execute.py +116 -0
  72. brawny/db/sqlite/mappers.py +190 -0
  73. brawny/db/sqlite/repos/attempts.py +372 -0
  74. brawny/db/sqlite/repos/block_state.py +102 -0
  75. brawny/db/sqlite/repos/cache.py +104 -0
  76. brawny/db/sqlite/repos/intents.py +1021 -0
  77. brawny/db/sqlite/repos/jobs.py +200 -0
  78. brawny/db/sqlite/repos/maintenance.py +182 -0
  79. brawny/db/sqlite/repos/signers_nonces.py +566 -0
  80. brawny/db/sqlite/tx.py +119 -0
  81. brawny/http.py +194 -0
  82. brawny/invariants.py +11 -24
  83. brawny/jobs/base.py +8 -0
  84. brawny/jobs/job_validation.py +2 -1
  85. brawny/keystore.py +83 -7
  86. brawny/lifecycle.py +64 -12
  87. brawny/logging.py +0 -2
  88. brawny/metrics.py +84 -12
  89. brawny/model/contexts.py +111 -9
  90. brawny/model/enums.py +1 -0
  91. brawny/model/errors.py +18 -0
  92. brawny/model/types.py +47 -131
  93. brawny/network_guard.py +133 -0
  94. brawny/networks/__init__.py +5 -5
  95. brawny/networks/config.py +1 -7
  96. brawny/networks/manager.py +14 -11
  97. brawny/runtime_controls.py +74 -0
  98. brawny/scheduler/poller.py +11 -7
  99. brawny/scheduler/reorg.py +95 -39
  100. brawny/scheduler/runner.py +442 -168
  101. brawny/scheduler/shutdown.py +3 -3
  102. brawny/script_tx.py +3 -3
  103. brawny/telegram.py +53 -7
  104. brawny/testing.py +1 -0
  105. brawny/timeout.py +38 -0
  106. brawny/tx/executor.py +922 -308
  107. brawny/tx/intent.py +54 -16
  108. brawny/tx/monitor.py +31 -12
  109. brawny/tx/nonce.py +212 -90
  110. brawny/tx/replacement.py +69 -18
  111. brawny/tx/retry_policy.py +24 -0
  112. brawny/tx/stages/types.py +75 -0
  113. brawny/types.py +18 -0
  114. brawny/utils.py +41 -0
  115. {brawny-0.1.13.dist-info → brawny-0.1.22.dist-info}/METADATA +3 -3
  116. brawny-0.1.22.dist-info/RECORD +163 -0
  117. brawny/_rpc/manager.py +0 -982
  118. brawny/_rpc/selector.py +0 -156
  119. brawny/db/base_new.py +0 -165
  120. brawny/db/mappers.py +0 -182
  121. brawny/db/migrations/008_add_transactions.sql +0 -72
  122. brawny/db/ops/attempts.py +0 -108
  123. brawny/db/ops/blocks.py +0 -83
  124. brawny/db/ops/cache.py +0 -93
  125. brawny/db/ops/intents.py +0 -296
  126. brawny/db/ops/jobs.py +0 -110
  127. brawny/db/ops/nonces.py +0 -322
  128. brawny/db/postgres.py +0 -2535
  129. brawny/db/postgres_new.py +0 -196
  130. brawny/db/sqlite.py +0 -2733
  131. brawny/db/sqlite_new.py +0 -191
  132. brawny-0.1.13.dist-info/RECORD +0 -141
  133. {brawny-0.1.13.dist-info → brawny-0.1.22.dist-info}/WHEEL +0 -0
  134. {brawny-0.1.13.dist-info → brawny-0.1.22.dist-info}/entry_points.txt +0 -0
  135. {brawny-0.1.13.dist-info → brawny-0.1.22.dist-info}/top_level.txt +0 -0
@@ -52,7 +52,7 @@ def contract_call(
52
52
  from brawny.alerts.contracts import ContractSystem
53
53
  from brawny.logging import get_logger, setup_logging
54
54
  from brawny.model.enums import LogFormat
55
- from brawny._rpc import RPCManager
55
+ from brawny._rpc.clients import ReadClient
56
56
 
57
57
  if not config_path or not os.path.exists(config_path):
58
58
  click.echo(
@@ -74,7 +74,7 @@ def contract_call(
74
74
  config=config.redacted_dict(),
75
75
  )
76
76
 
77
- rpc = RPCManager.from_config(config)
77
+ rpc = ReadClient.from_config(config)
78
78
  # ContractSystem uses global ABI cache at ~/.brawny/abi_cache.db
79
79
  contract_system = ContractSystem(rpc, config)
80
80
 
@@ -0,0 +1,121 @@
1
+ """Runtime controls commands."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from datetime import datetime, timedelta
6
+
7
+ import click
8
+
9
+
10
+ @click.group()
11
+ def controls() -> None:
12
+ """Manage runtime controls."""
13
+ pass
14
+
15
+
16
+ @controls.command("list")
17
+ @click.option("--config", "config_path", default=None, help="Path to config.yaml")
18
+ def controls_list(config_path: str | None) -> None:
19
+ """List runtime controls."""
20
+ from brawny.cli.helpers import get_db
21
+
22
+ db = get_db(config_path)
23
+ try:
24
+ controls_list = db.list_runtime_controls()
25
+ if not controls_list:
26
+ click.echo("No runtime controls set.")
27
+ return
28
+ click.echo()
29
+ for rc in controls_list:
30
+ status = "active" if rc.active else "inactive"
31
+ expires_at = rc.expires_at.isoformat() if rc.expires_at else "none"
32
+ click.echo(
33
+ f" {rc.control}: {status} expires_at={expires_at} mode={rc.mode}"
34
+ )
35
+ if rc.reason:
36
+ click.echo(f" reason: {rc.reason}")
37
+ if rc.actor:
38
+ click.echo(f" actor: {rc.actor}")
39
+ click.echo()
40
+ finally:
41
+ db.close()
42
+
43
+
44
+ @controls.command("activate")
45
+ @click.argument("control")
46
+ @click.option(
47
+ "--ttl-seconds",
48
+ type=int,
49
+ default=900,
50
+ show_default=True,
51
+ help="TTL in seconds (ignored if --forever)",
52
+ )
53
+ @click.option("--forever", is_flag=True, help="Set without expiration")
54
+ @click.option("--reason", "reason", default=None, help="Reason for activation")
55
+ @click.option("--actor", "actor", default="cli", help="Actor label for audit")
56
+ @click.option("--config", "config_path", default=None, help="Path to config.yaml")
57
+ def controls_activate(
58
+ control: str,
59
+ ttl_seconds: int,
60
+ forever: bool,
61
+ reason: str | None,
62
+ actor: str,
63
+ config_path: str | None,
64
+ ) -> None:
65
+ """Activate a runtime control."""
66
+ from brawny.cli.helpers import get_db
67
+
68
+ expires_at = None
69
+ if not forever:
70
+ expires_at = datetime.utcnow() + timedelta(seconds=ttl_seconds)
71
+
72
+ db = get_db(config_path)
73
+ try:
74
+ db.set_runtime_control(
75
+ control=control,
76
+ active=True,
77
+ expires_at=expires_at,
78
+ reason=reason,
79
+ actor=actor,
80
+ mode="manual",
81
+ )
82
+ if expires_at:
83
+ click.echo(f"Control '{control}' activated until {expires_at.isoformat()}.")
84
+ else:
85
+ click.echo(f"Control '{control}' activated with no expiration.")
86
+ finally:
87
+ db.close()
88
+
89
+
90
+ @controls.command("deactivate")
91
+ @click.argument("control")
92
+ @click.option("--reason", "reason", default=None, help="Reason for deactivation")
93
+ @click.option("--actor", "actor", default="cli", help="Actor label for audit")
94
+ @click.option("--config", "config_path", default=None, help="Path to config.yaml")
95
+ def controls_deactivate(
96
+ control: str,
97
+ reason: str | None,
98
+ actor: str,
99
+ config_path: str | None,
100
+ ) -> None:
101
+ """Deactivate a runtime control."""
102
+ from brawny.cli.helpers import get_db
103
+
104
+ db = get_db(config_path)
105
+ try:
106
+ db.set_runtime_control(
107
+ control=control,
108
+ active=False,
109
+ expires_at=None,
110
+ reason=reason,
111
+ actor=actor,
112
+ mode="manual",
113
+ )
114
+ click.echo(f"Control '{control}' deactivated.")
115
+ finally:
116
+ db.close()
117
+
118
+
119
+ def register(main) -> None:
120
+ """Register runtime controls commands."""
121
+ main.add_command(controls)
@@ -17,7 +17,7 @@ from brawny.cli.helpers import get_db, print_json
17
17
  def health(fmt: str, config_path: str | None) -> None:
18
18
  """Health check endpoint."""
19
19
  from brawny.config import Config, get_config
20
- from brawny._rpc import RPCManager
20
+ from brawny._rpc.clients import ReadClient
21
21
 
22
22
  try:
23
23
  if config_path:
@@ -47,7 +47,7 @@ def health(fmt: str, config_path: str | None) -> None:
47
47
  }
48
48
 
49
49
  try:
50
- rpc = RPCManager.from_config(config)
50
+ rpc = ReadClient.from_config(config)
51
51
  rpc_health = rpc.get_health()
52
52
  result["components"]["rpc"] = {
53
53
  "status": "ok" if rpc_health["healthy_endpoints"] > 0 else "degraded",
@@ -48,7 +48,7 @@ def job_run(
48
48
  from brawny.logging import get_logger, setup_logging
49
49
  from brawny.model.enums import LogFormat
50
50
  from brawny.model.types import BlockInfo, JobContext, idempotency_key
51
- from brawny._rpc import RPCManager
51
+ from brawny._rpc.clients import ReadClient
52
52
  from brawny.scripting import set_job_context
53
53
 
54
54
  if not config_path or not os.path.exists(config_path):
@@ -73,16 +73,13 @@ def job_run(
73
73
 
74
74
  db = create_database(
75
75
  config.database_url,
76
- pool_size=config.database_pool_size,
77
- pool_max_overflow=config.database_pool_max_overflow,
78
- pool_timeout=config.database_pool_timeout_seconds,
79
76
  circuit_breaker_failures=config.db_circuit_breaker_failures,
80
77
  circuit_breaker_seconds=config.db_circuit_breaker_seconds,
81
78
  )
82
79
  db.connect()
83
80
 
84
81
  try:
85
- rpc = RPCManager.from_config(config)
82
+ rpc = ReadClient.from_config(config)
86
83
  contract_system = ContractSystem(rpc, config)
87
84
 
88
85
  modules = list(jobs_modules)
@@ -128,11 +125,15 @@ def job_run(
128
125
  err=True,
129
126
  )
130
127
 
128
+ base_fee = 0
129
+ if block_data:
130
+ base_fee = block_data.get("baseFeePerGas", 0)
131
131
  block = BlockInfo(
132
132
  chain_id=config.chain_id,
133
133
  block_number=block_number,
134
134
  block_hash=block_hash,
135
135
  timestamp=timestamp,
136
+ base_fee=base_fee,
136
137
  )
137
138
 
138
139
  job_config = db.get_job(job_id)
@@ -23,6 +23,8 @@ def jobs_list(config_path: str | None) -> None:
23
23
 
24
24
  suppress_logging()
25
25
 
26
+ from datetime import datetime
27
+
26
28
  from brawny.cli.helpers import discover_jobs_for_cli, get_config, get_db
27
29
  from brawny.jobs.registry import get_registry
28
30
 
@@ -52,6 +54,18 @@ def jobs_list(config_path: str | None) -> None:
52
54
  return
53
55
 
54
56
  click.echo()
57
+ def _parse_drain_until(value):
58
+ if value is None:
59
+ return None
60
+ if isinstance(value, datetime):
61
+ return value
62
+ try:
63
+ return datetime.fromisoformat(value)
64
+ except ValueError:
65
+ return None
66
+
67
+ now = datetime.utcnow()
68
+
55
69
  for job_id in sorted(code_jobs.keys()):
56
70
  job = code_jobs[job_id]
57
71
  db_job = db_jobs.get(job_id)
@@ -62,13 +76,21 @@ def jobs_list(config_path: str | None) -> None:
62
76
  # Get enabled status from DB, default to True for new jobs
63
77
  enabled = db_job.enabled if db_job else True
64
78
 
79
+ drain_until = _parse_drain_until(db_job.drain_until) if db_job else None
80
+ draining = drain_until is not None and drain_until > now
81
+
65
82
  # Status indicator
66
- if enabled:
83
+ if draining:
84
+ status = click.style("! draining", fg="yellow")
85
+ elif enabled:
67
86
  status = click.style("✓ enabled ", fg="green")
68
87
  else:
69
88
  status = click.style("✗ disabled", fg="red")
70
89
 
71
- click.echo(f" {status} {job_id} {click.style(f'every {interval} blocks', dim=True)}")
90
+ line = f" {status} {job_id} {click.style(f'every {interval} blocks', dim=True)}"
91
+ if draining:
92
+ line += click.style(f" (until {drain_until.isoformat()})", dim=True)
93
+ click.echo(line)
72
94
 
73
95
  click.echo()
74
96
 
@@ -206,6 +228,81 @@ def jobs_disable(job_id: str, config_path: str | None) -> None:
206
228
  db.close()
207
229
 
208
230
 
231
+ @jobs.command("drain")
232
+ @click.argument("job_id")
233
+ @click.option(
234
+ "--ttl-seconds",
235
+ type=int,
236
+ default=3600,
237
+ show_default=True,
238
+ help="Drain duration in seconds (ignored if --until is set)",
239
+ )
240
+ @click.option(
241
+ "--until",
242
+ "until_iso",
243
+ default=None,
244
+ help="Drain until ISO timestamp (e.g. 2025-01-01T00:00:00)",
245
+ )
246
+ @click.option("--reason", "-r", default=None, help="Reason for drain")
247
+ @click.option("--config", "config_path", default=None, help="Path to config.yaml")
248
+ def jobs_drain(
249
+ job_id: str,
250
+ ttl_seconds: int,
251
+ until_iso: str | None,
252
+ reason: str | None,
253
+ config_path: str | None,
254
+ ) -> None:
255
+ """Drain a job (pause new intents) until a timestamp."""
256
+ from datetime import datetime, timedelta
257
+
258
+ from brawny.cli.helpers import get_db
259
+
260
+ if until_iso:
261
+ try:
262
+ drain_until = datetime.fromisoformat(until_iso)
263
+ except ValueError:
264
+ click.echo("Invalid --until format, expected ISO timestamp.", err=True)
265
+ raise SystemExit(1)
266
+ else:
267
+ drain_until = datetime.utcnow() + timedelta(seconds=ttl_seconds)
268
+
269
+ db = get_db(config_path)
270
+ try:
271
+ updated = db.set_job_drain(
272
+ job_id,
273
+ drain_until=drain_until,
274
+ reason=reason,
275
+ actor="cli",
276
+ source="cli",
277
+ )
278
+ if updated:
279
+ click.echo(
280
+ f"Job '{job_id}' drained until {drain_until.isoformat()}."
281
+ )
282
+ else:
283
+ click.echo(f"Job '{job_id}' not found.", err=True)
284
+ finally:
285
+ db.close()
286
+
287
+
288
+ @jobs.command("undrain")
289
+ @click.argument("job_id")
290
+ @click.option("--config", "config_path", default=None, help="Path to config.yaml")
291
+ def jobs_undrain(job_id: str, config_path: str | None) -> None:
292
+ """Clear job drain."""
293
+ from brawny.cli.helpers import get_db
294
+
295
+ db = get_db(config_path)
296
+ try:
297
+ updated = db.clear_job_drain(job_id, actor="cli", source="cli")
298
+ if updated:
299
+ click.echo(f"Job '{job_id}' undrained.")
300
+ else:
301
+ click.echo(f"Job '{job_id}' not found.", err=True)
302
+ finally:
303
+ db.close()
304
+
305
+
209
306
  @jobs.command("remove")
210
307
  @click.argument("job_id")
211
308
  @click.option("--config", "config_path", default=None, help="Path to config.yaml")
@@ -12,7 +12,7 @@ def reconcile() -> None:
12
12
  """Run nonce reconciliation."""
13
13
  from brawny.config import get_config
14
14
  from brawny.db import create_database
15
- from brawny._rpc import RPCManager
15
+ from brawny._rpc.clients import ReadClient
16
16
  from brawny.tx.nonce import NonceManager
17
17
 
18
18
  click.echo("Running nonce reconciliation...")
@@ -20,16 +20,13 @@ def reconcile() -> None:
20
20
  config = get_config()
21
21
  db = create_database(
22
22
  config.database_url,
23
- pool_size=config.database_pool_size,
24
- pool_max_overflow=config.database_pool_max_overflow,
25
- pool_timeout=config.database_pool_timeout_seconds,
26
23
  circuit_breaker_failures=config.db_circuit_breaker_failures,
27
24
  circuit_breaker_seconds=config.db_circuit_breaker_seconds,
28
25
  )
29
26
  db.connect()
30
27
 
31
28
  try:
32
- rpc = RPCManager.from_config(config)
29
+ rpc = ReadClient.from_config(config)
33
30
  nonce_manager = NonceManager(db, rpc, config.chain_id)
34
31
  nonce_manager.reconcile()
35
32
  click.echo("Nonce reconciliation complete.")
@@ -124,30 +121,17 @@ def repair_claims(
124
121
  config = get_config(config_path)
125
122
  db = get_db(config_path)
126
123
  try:
127
- if db.dialect == "sqlite":
128
- query = """
129
- SELECT i.intent_id, i.job_id, i.claimed_at
130
- FROM tx_intents i
131
- WHERE i.chain_id = ?
132
- AND i.status = 'claimed'
133
- AND (i.claimed_at IS NULL OR datetime(i.claimed_at) < datetime('now', ? || ' minutes'))
134
- AND NOT EXISTS (SELECT 1 FROM tx_attempts a WHERE a.intent_id = i.intent_id)
135
- ORDER BY (i.claimed_at IS NOT NULL), i.claimed_at ASC
136
- LIMIT ?
137
- """
138
- stuck = db.execute_returning(query, (config.chain_id, -older_than, limit))
139
- else:
140
- query = """
141
- SELECT i.intent_id, i.job_id, i.claimed_at
142
- FROM tx_intents i
143
- WHERE i.chain_id = %s
144
- AND i.status = 'claimed'
145
- AND (i.claimed_at IS NULL OR i.claimed_at < NOW() - make_interval(mins => %s))
146
- AND NOT EXISTS (SELECT 1 FROM tx_attempts a WHERE a.intent_id = i.intent_id)
147
- ORDER BY i.claimed_at ASC NULLS FIRST
148
- LIMIT %s
149
- """
150
- stuck = db.execute_returning(query, (config.chain_id, older_than, limit))
124
+ query = """
125
+ SELECT i.intent_id, i.job_id, i.claimed_at
126
+ FROM tx_intents i
127
+ WHERE i.chain_id = ?
128
+ AND i.status = 'claimed'
129
+ AND (i.claimed_at IS NULL OR datetime(i.claimed_at) < datetime('now', ? || ' minutes'))
130
+ AND NOT EXISTS (SELECT 1 FROM tx_attempts a WHERE a.intent_id = i.intent_id)
131
+ ORDER BY (i.claimed_at IS NOT NULL), i.claimed_at ASC
132
+ LIMIT ?
133
+ """
134
+ stuck = db.execute_returning(query, (config.chain_id, -older_than, limit))
151
135
 
152
136
  if not stuck:
153
137
  click.echo("No stuck claims found matching criteria.")
@@ -42,6 +42,7 @@ def migrate(status: bool, config_path: str | None) -> None:
42
42
  applied = migrator.migrate()
43
43
  for m in applied:
44
44
  click.echo(f" Applied: {m.version} - {m.filename}")
45
+ verify_critical_schema(db)
45
46
  click.echo(f"\nSuccessfully applied {len(applied)} migration(s).")
46
47
  finally:
47
48
  db.close()
@@ -103,9 +103,16 @@ def start(
103
103
 
104
104
  # Show signers
105
105
  if daemon.keystore:
106
- signers = daemon.keystore.list_keys()
107
- if signers:
108
- click.echo(f" Signers: {len(signers)} ({', '.join(signers)})")
106
+ signers_with_aliases = daemon.keystore.list_keys_with_aliases()
107
+ if signers_with_aliases:
108
+ # Format: "alias (0x123...)" or just "0x123..." if no alias
109
+ formatted = []
110
+ for addr, alias in signers_with_aliases:
111
+ if alias:
112
+ formatted.append(f"{alias} ({addr[:10]}...)")
113
+ else:
114
+ formatted.append(addr[:10] + "...")
115
+ click.echo(f" Signers: {len(signers_with_aliases)} ({', '.join(formatted)})")
109
116
 
110
117
  # Show startup warnings/errors
111
118
  for msg in startup_messages:
@@ -66,7 +66,7 @@ def run(
66
66
  from brawny.alerts.contracts import ContractSystem
67
67
  from brawny.config import Config
68
68
  from brawny.keystore import create_keystore
69
- from brawny._rpc import RPCManager
69
+ from brawny._rpc.clients import BroadcastClient
70
70
  from brawny.accounts import _init_accounts
71
71
  from brawny.history import _init_history
72
72
  from brawny.chain import _init_chain
@@ -103,11 +103,15 @@ def run(
103
103
  rpc_endpoints = [local_url]
104
104
  click.echo(f"Started Anvil fork at {local_url}")
105
105
 
106
- # Create RPC manager
107
- rpc = RPCManager(
106
+ # Create broadcast client
107
+ from brawny._rpc.retry_policy import broadcast_policy
108
+
109
+ rpc = BroadcastClient(
108
110
  endpoints=rpc_endpoints,
109
111
  timeout_seconds=config.rpc_timeout_seconds,
110
112
  max_retries=config.rpc_max_retries,
113
+ retry_backoff_base=config.rpc_retry_backoff_base,
114
+ retry_policy=broadcast_policy(config),
111
115
  )
112
116
  _set_fallback_rpc(rpc)
113
117
 
@@ -134,6 +138,7 @@ def run(
134
138
  block_number=block_data["number"],
135
139
  block_hash=block_data["hash"],
136
140
  timestamp=block_data["timestamp"],
141
+ base_fee=block_data.get("baseFeePerGas", 0),
137
142
  )
138
143
  ctx = JobContext(
139
144
  block=block,