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
|
@@ -0,0 +1,253 @@
|
|
|
1
|
+
"""Network management CLI commands (Brownie-compatible).
|
|
2
|
+
|
|
3
|
+
Usage:
|
|
4
|
+
brawny networks list
|
|
5
|
+
brawny networks add <type> <id> <settings...>
|
|
6
|
+
brawny networks delete <id>
|
|
7
|
+
|
|
8
|
+
These commands manage ~/.brawny/network-config.yaml (Brownie-compatible format).
|
|
9
|
+
This is separate from the project-level config.yaml networks section.
|
|
10
|
+
"""
|
|
11
|
+
|
|
12
|
+
from __future__ import annotations
|
|
13
|
+
|
|
14
|
+
from pathlib import Path
|
|
15
|
+
from typing import Any
|
|
16
|
+
|
|
17
|
+
import click
|
|
18
|
+
import yaml
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
def _get_config_path() -> Path:
|
|
22
|
+
"""Get user network config path."""
|
|
23
|
+
return Path.home() / ".brawny" / "network-config.yaml"
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
def _load_config() -> dict[str, Any]:
|
|
27
|
+
"""Load user network config."""
|
|
28
|
+
path = _get_config_path()
|
|
29
|
+
if path.exists():
|
|
30
|
+
with open(path) as f:
|
|
31
|
+
return yaml.safe_load(f) or {}
|
|
32
|
+
return {"live": [], "development": []}
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
def _save_config(config: dict[str, Any]) -> None:
|
|
36
|
+
"""Save user network config."""
|
|
37
|
+
path = _get_config_path()
|
|
38
|
+
path.parent.mkdir(parents=True, exist_ok=True)
|
|
39
|
+
with open(path, "w") as f:
|
|
40
|
+
yaml.dump(config, f, default_flow_style=False, sort_keys=False)
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
@click.group("networks")
|
|
44
|
+
def networks_cli() -> None:
|
|
45
|
+
"""Manage Brownie-compatible network configurations.
|
|
46
|
+
|
|
47
|
+
These networks are stored in ~/.brawny/network-config.yaml and used
|
|
48
|
+
by `network.connect()` in scripts and console.
|
|
49
|
+
"""
|
|
50
|
+
pass
|
|
51
|
+
|
|
52
|
+
|
|
53
|
+
def _redact_api_keys(url: str) -> str:
|
|
54
|
+
"""Redact API keys from URLs for safe display.
|
|
55
|
+
|
|
56
|
+
Matches common patterns:
|
|
57
|
+
- /v2/abc123... → /v2/abc1...
|
|
58
|
+
- /v3/abc123... → /v3/abc1...
|
|
59
|
+
- ?apikey=... → ?apikey=...
|
|
60
|
+
"""
|
|
61
|
+
import re
|
|
62
|
+
# Redact Alchemy/Infura-style path keys (show first 4 chars)
|
|
63
|
+
url = re.sub(r"(/v[23]/)([a-zA-Z0-9_-]{4})([a-zA-Z0-9_-]+)", r"\1\2...", url)
|
|
64
|
+
# Redact query param API keys
|
|
65
|
+
url = re.sub(r"([\?&]api[_-]?key=)([a-zA-Z0-9_-]{4})([a-zA-Z0-9_-]+)", r"\1\2...", url, flags=re.I)
|
|
66
|
+
return url
|
|
67
|
+
|
|
68
|
+
|
|
69
|
+
@networks_cli.command("list")
|
|
70
|
+
@click.option("--verbose", "-v", is_flag=True, help="Show detailed info (redacts API keys)")
|
|
71
|
+
def list_networks(verbose: bool) -> None:
|
|
72
|
+
"""List available networks."""
|
|
73
|
+
from brawny.networks.config import load_networks
|
|
74
|
+
|
|
75
|
+
networks = load_networks()
|
|
76
|
+
|
|
77
|
+
live = [(n.id, n) for n in networks.values() if not n.is_development]
|
|
78
|
+
dev = [(n.id, n) for n in networks.values() if n.is_development]
|
|
79
|
+
|
|
80
|
+
click.echo("\nLive Networks:")
|
|
81
|
+
if not live:
|
|
82
|
+
click.echo(" (none configured)")
|
|
83
|
+
for net_id, net in sorted(live):
|
|
84
|
+
if verbose:
|
|
85
|
+
# Redact API keys in displayed hosts for security
|
|
86
|
+
redacted_hosts = [_redact_api_keys(h) for h in net.hosts[:2]]
|
|
87
|
+
hosts_str = ", ".join(redacted_hosts)
|
|
88
|
+
if len(net.hosts) > 2:
|
|
89
|
+
hosts_str += f" (+{len(net.hosts) - 2} more)"
|
|
90
|
+
click.echo(f" {net_id}: chainid={net.chainid} hosts=[{hosts_str}]")
|
|
91
|
+
else:
|
|
92
|
+
click.echo(f" - {net_id} (chainid: {net.chainid})")
|
|
93
|
+
|
|
94
|
+
click.echo("\nDevelopment Networks:")
|
|
95
|
+
for net_id, net in sorted(dev):
|
|
96
|
+
fork_info = " (fork)" if net.is_fork else ""
|
|
97
|
+
if verbose:
|
|
98
|
+
click.echo(f" {net_id}: cmd={net.cmd} port={net.cmd_settings.get('port', 8545)}{fork_info}")
|
|
99
|
+
else:
|
|
100
|
+
click.echo(f" - {net_id}{fork_info}")
|
|
101
|
+
|
|
102
|
+
|
|
103
|
+
@networks_cli.command("add")
|
|
104
|
+
@click.argument("network_type", type=click.Choice(["live", "development"]))
|
|
105
|
+
@click.argument("network_id")
|
|
106
|
+
@click.argument("settings", nargs=-1)
|
|
107
|
+
def add_network(network_type: str, network_id: str, settings: tuple[str, ...]) -> None:
|
|
108
|
+
"""Add a new network to ~/.brawny/network-config.yaml.
|
|
109
|
+
|
|
110
|
+
Examples:
|
|
111
|
+
|
|
112
|
+
brawny networks add live mainnet-custom host=https://... chainid=1
|
|
113
|
+
|
|
114
|
+
# Multiple hosts for failover (RPC pool):
|
|
115
|
+
brawny networks add live mainnet host=https://eth.llamarpc.com host=https://eth-mainnet.g.alchemy.com/v2/KEY chainid=1
|
|
116
|
+
|
|
117
|
+
brawny networks add development my-fork cmd=anvil port=9545 fork=mainnet
|
|
118
|
+
"""
|
|
119
|
+
from brawny.networks.config import load_networks
|
|
120
|
+
|
|
121
|
+
# Check if network ID already exists
|
|
122
|
+
existing = load_networks()
|
|
123
|
+
if network_id in existing:
|
|
124
|
+
raise click.ClickException(
|
|
125
|
+
f"Network '{network_id}' already exists. "
|
|
126
|
+
f"Delete it first with: brawny networks delete {network_id}"
|
|
127
|
+
)
|
|
128
|
+
|
|
129
|
+
config = _load_config()
|
|
130
|
+
|
|
131
|
+
# Parse key=value settings - collect repeated keys as lists
|
|
132
|
+
params: dict[str, Any] = {}
|
|
133
|
+
multi_keys: dict[str, list[str]] = {} # For repeated keys like host=...
|
|
134
|
+
|
|
135
|
+
for s in settings:
|
|
136
|
+
if "=" not in s:
|
|
137
|
+
raise click.ClickException(f"Invalid: {s}. Use key=value format.")
|
|
138
|
+
key, value_str = s.split("=", 1)
|
|
139
|
+
|
|
140
|
+
# Special handling for 'host' - can be repeated for multi-host
|
|
141
|
+
if key == "host":
|
|
142
|
+
multi_keys.setdefault("host", []).append(value_str)
|
|
143
|
+
continue
|
|
144
|
+
|
|
145
|
+
# Parse integers and floats for other keys
|
|
146
|
+
value: Any = value_str
|
|
147
|
+
try:
|
|
148
|
+
value = int(value_str)
|
|
149
|
+
except ValueError:
|
|
150
|
+
try:
|
|
151
|
+
value = float(value_str)
|
|
152
|
+
except ValueError:
|
|
153
|
+
pass
|
|
154
|
+
params[key] = value
|
|
155
|
+
|
|
156
|
+
# Convert multi-host to single or list as needed
|
|
157
|
+
if "host" in multi_keys:
|
|
158
|
+
hosts = multi_keys["host"]
|
|
159
|
+
params["host"] = hosts if len(hosts) > 1 else hosts[0]
|
|
160
|
+
|
|
161
|
+
if network_type == "live":
|
|
162
|
+
if "host" not in params:
|
|
163
|
+
raise click.ClickException("Live networks require 'host'")
|
|
164
|
+
if "chainid" not in params:
|
|
165
|
+
raise click.ClickException("Live networks require 'chainid'")
|
|
166
|
+
|
|
167
|
+
# Ensure live section exists with at least one group
|
|
168
|
+
if "live" not in config or not config["live"]:
|
|
169
|
+
config["live"] = [{"name": "Custom", "networks": []}]
|
|
170
|
+
|
|
171
|
+
# Find Custom group or use first group
|
|
172
|
+
group = config["live"][0]
|
|
173
|
+
for g in config["live"]:
|
|
174
|
+
if g.get("name") == "Custom":
|
|
175
|
+
group = g
|
|
176
|
+
break
|
|
177
|
+
|
|
178
|
+
net_config = {"id": network_id, **params}
|
|
179
|
+
group.setdefault("networks", []).append(net_config)
|
|
180
|
+
|
|
181
|
+
else: # development
|
|
182
|
+
if "cmd" not in params:
|
|
183
|
+
raise click.ClickException("Development networks require 'cmd'")
|
|
184
|
+
|
|
185
|
+
# Separate cmd_settings from top-level params
|
|
186
|
+
cmd_keys = {"port", "fork", "fork_block", "accounts", "balance", "chain_id", "mnemonic", "block_time"}
|
|
187
|
+
cmd_settings = {k: params.pop(k) for k in list(params.keys()) if k in cmd_keys}
|
|
188
|
+
|
|
189
|
+
net_config: dict[str, Any] = {
|
|
190
|
+
"id": network_id,
|
|
191
|
+
"host": params.pop("host", "http://127.0.0.1"),
|
|
192
|
+
"cmd": params.pop("cmd"),
|
|
193
|
+
**params,
|
|
194
|
+
}
|
|
195
|
+
if cmd_settings:
|
|
196
|
+
net_config["cmd_settings"] = cmd_settings
|
|
197
|
+
|
|
198
|
+
config.setdefault("development", []).append(net_config)
|
|
199
|
+
|
|
200
|
+
_save_config(config)
|
|
201
|
+
click.echo(f"Added {network_type} network: {network_id}")
|
|
202
|
+
|
|
203
|
+
|
|
204
|
+
@networks_cli.command("delete")
|
|
205
|
+
@click.argument("network_id")
|
|
206
|
+
@click.option("--force", "-f", is_flag=True, help="Skip confirmation")
|
|
207
|
+
def delete_network(network_id: str, force: bool) -> None:
|
|
208
|
+
"""Delete a network from user config."""
|
|
209
|
+
# Don't allow deleting built-in defaults
|
|
210
|
+
builtin = {"development", "mainnet-fork"}
|
|
211
|
+
if network_id in builtin:
|
|
212
|
+
raise click.ClickException(
|
|
213
|
+
f"Cannot delete built-in network '{network_id}'. "
|
|
214
|
+
"You can override it by adding a network with the same ID."
|
|
215
|
+
)
|
|
216
|
+
|
|
217
|
+
config = _load_config()
|
|
218
|
+
found = False
|
|
219
|
+
|
|
220
|
+
# Check live networks
|
|
221
|
+
for group in config.get("live", []):
|
|
222
|
+
networks = group.get("networks", [])
|
|
223
|
+
for i, net in enumerate(networks):
|
|
224
|
+
if net["id"] == network_id:
|
|
225
|
+
if not force:
|
|
226
|
+
click.confirm(f"Delete '{network_id}'?", abort=True)
|
|
227
|
+
networks.pop(i)
|
|
228
|
+
found = True
|
|
229
|
+
break
|
|
230
|
+
if found:
|
|
231
|
+
break
|
|
232
|
+
|
|
233
|
+
# Check development networks
|
|
234
|
+
if not found:
|
|
235
|
+
dev = config.get("development", [])
|
|
236
|
+
for i, net in enumerate(dev):
|
|
237
|
+
if net["id"] == network_id:
|
|
238
|
+
if not force:
|
|
239
|
+
click.confirm(f"Delete '{network_id}'?", abort=True)
|
|
240
|
+
dev.pop(i)
|
|
241
|
+
found = True
|
|
242
|
+
break
|
|
243
|
+
|
|
244
|
+
if not found:
|
|
245
|
+
raise click.ClickException(f"Network '{network_id}' not found in user config")
|
|
246
|
+
|
|
247
|
+
_save_config(config)
|
|
248
|
+
click.echo(f"Deleted: {network_id}")
|
|
249
|
+
|
|
250
|
+
|
|
251
|
+
def register(main) -> None:
|
|
252
|
+
"""Register networks command with main CLI."""
|
|
253
|
+
main.add_command(networks_cli)
|
|
@@ -0,0 +1,249 @@
|
|
|
1
|
+
"""Runner command."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import os
|
|
6
|
+
import sys
|
|
7
|
+
|
|
8
|
+
import click
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
@click.command()
|
|
12
|
+
@click.option("--once", is_flag=True, help="Process once and exit")
|
|
13
|
+
@click.option("--workers", type=int, help="Worker count override")
|
|
14
|
+
@click.option("--dry-run", is_flag=True, help="Create intents but don't execute")
|
|
15
|
+
@click.option(
|
|
16
|
+
"--jobs-module",
|
|
17
|
+
"jobs_modules",
|
|
18
|
+
multiple=True,
|
|
19
|
+
help="Additional job module(s) to load",
|
|
20
|
+
)
|
|
21
|
+
@click.option("--config", "config_path", default="./config.yaml", help="Path to config.yaml")
|
|
22
|
+
@click.option(
|
|
23
|
+
"--no-strict",
|
|
24
|
+
is_flag=True,
|
|
25
|
+
help="Don't exit on job validation errors (warn only)",
|
|
26
|
+
)
|
|
27
|
+
def start(
|
|
28
|
+
once: bool,
|
|
29
|
+
workers: int | None,
|
|
30
|
+
dry_run: bool,
|
|
31
|
+
jobs_modules: tuple[str, ...],
|
|
32
|
+
config_path: str,
|
|
33
|
+
no_strict: bool,
|
|
34
|
+
) -> None:
|
|
35
|
+
"""Start the brawny daemon (job runner and transaction executor)."""
|
|
36
|
+
from brawny.config import Config
|
|
37
|
+
from brawny.daemon import BrawnyDaemon, RuntimeOverrides
|
|
38
|
+
from brawny.logging import setup_logging, set_runtime_logging, get_logger
|
|
39
|
+
from brawny.model.enums import LogFormat
|
|
40
|
+
from brawny.metrics import (
|
|
41
|
+
PrometheusMetricsProvider,
|
|
42
|
+
set_metrics_provider,
|
|
43
|
+
start_metrics_server,
|
|
44
|
+
)
|
|
45
|
+
from brawny.model.enums import IntentStatus
|
|
46
|
+
from brawny.scheduler.shutdown import ShutdownContext, ShutdownHandler
|
|
47
|
+
|
|
48
|
+
if not config_path or not os.path.exists(config_path):
|
|
49
|
+
click.echo(
|
|
50
|
+
f"Config file is required for run and was not found: {config_path}",
|
|
51
|
+
err=True,
|
|
52
|
+
)
|
|
53
|
+
sys.exit(1)
|
|
54
|
+
|
|
55
|
+
config = Config.from_yaml(config_path)
|
|
56
|
+
config, overrides_applied = config.apply_env_overrides()
|
|
57
|
+
|
|
58
|
+
log_level = os.environ.get("BRAWNY_LOG_LEVEL", "INFO")
|
|
59
|
+
# Start in startup mode (human-readable, warnings only)
|
|
60
|
+
setup_logging(log_level, LogFormat.JSON, config.chain_id, mode="startup")
|
|
61
|
+
|
|
62
|
+
click.echo(f"Config: {config_path}")
|
|
63
|
+
metrics_bind = f"127.0.0.1:{config.metrics_port}"
|
|
64
|
+
try:
|
|
65
|
+
provider = PrometheusMetricsProvider()
|
|
66
|
+
set_metrics_provider(provider)
|
|
67
|
+
start_metrics_server(metrics_bind, provider)
|
|
68
|
+
click.echo(f"Metrics: http://{metrics_bind}/metrics")
|
|
69
|
+
except Exception as e:
|
|
70
|
+
click.echo(click.style(f"Failed to start metrics server: {e}", fg="red"), err=True)
|
|
71
|
+
sys.exit(1)
|
|
72
|
+
|
|
73
|
+
# Build runtime overrides
|
|
74
|
+
runtime_overrides = RuntimeOverrides(
|
|
75
|
+
dry_run=dry_run,
|
|
76
|
+
once=once,
|
|
77
|
+
worker_count=workers,
|
|
78
|
+
strict_validation=not no_strict,
|
|
79
|
+
)
|
|
80
|
+
|
|
81
|
+
click.echo("Starting brawny runner...")
|
|
82
|
+
click.echo(f" Chain ID: {config.chain_id}")
|
|
83
|
+
if config.rpc_groups:
|
|
84
|
+
from brawny.config.routing import resolve_default_group
|
|
85
|
+
|
|
86
|
+
default_group = resolve_default_group(config)
|
|
87
|
+
click.echo(f" RPC Default Group: {default_group}")
|
|
88
|
+
click.echo(f" RPC Endpoints: {len(config.rpc_groups[default_group].endpoints)}")
|
|
89
|
+
click.echo(f" Workers: {workers or config.worker_count}")
|
|
90
|
+
if dry_run:
|
|
91
|
+
click.echo(" Mode: DRY RUN (no transactions)")
|
|
92
|
+
if once:
|
|
93
|
+
click.echo(" Mode: Single iteration")
|
|
94
|
+
|
|
95
|
+
# Initialize daemon
|
|
96
|
+
daemon = BrawnyDaemon(
|
|
97
|
+
config,
|
|
98
|
+
overrides=runtime_overrides,
|
|
99
|
+
extra_modules=list(jobs_modules),
|
|
100
|
+
)
|
|
101
|
+
|
|
102
|
+
validation_errors, routing_errors, startup_messages = daemon.initialize()
|
|
103
|
+
|
|
104
|
+
# Show signers
|
|
105
|
+
if daemon.keystore:
|
|
106
|
+
signers = daemon.keystore.list_keys()
|
|
107
|
+
if signers:
|
|
108
|
+
click.echo(f" Signers: {len(signers)} ({', '.join(signers)})")
|
|
109
|
+
|
|
110
|
+
# Show startup warnings/errors
|
|
111
|
+
for msg in startup_messages:
|
|
112
|
+
color = "yellow" if msg.level == "warning" else "red"
|
|
113
|
+
symbol = "\u26a0" if msg.level == "warning" else "\u2717"
|
|
114
|
+
text = f" {symbol} {msg.message}"
|
|
115
|
+
if msg.fix:
|
|
116
|
+
text += click.style(f" \u2192 {msg.fix}", dim=True)
|
|
117
|
+
click.echo(click.style(text, fg=color))
|
|
118
|
+
|
|
119
|
+
# Report validation errors
|
|
120
|
+
if validation_errors:
|
|
121
|
+
click.echo("")
|
|
122
|
+
click.echo(click.style("Job validation errors:", fg="red", bold=True))
|
|
123
|
+
for job_id, errors in validation_errors.items():
|
|
124
|
+
click.echo(f" {job_id}:")
|
|
125
|
+
for error in errors:
|
|
126
|
+
click.echo(f" - {error}")
|
|
127
|
+
|
|
128
|
+
click.echo("")
|
|
129
|
+
click.echo(click.style("Tip:", dim=True) + " Remove the @job decorator to hide incomplete jobs from discovery.")
|
|
130
|
+
|
|
131
|
+
if runtime_overrides.strict_validation:
|
|
132
|
+
click.echo("")
|
|
133
|
+
click.echo(
|
|
134
|
+
"Exiting due to validation errors. Use --no-strict to continue anyway.",
|
|
135
|
+
err=True,
|
|
136
|
+
)
|
|
137
|
+
sys.exit(1)
|
|
138
|
+
else:
|
|
139
|
+
click.echo("")
|
|
140
|
+
click.echo(
|
|
141
|
+
click.style("Continuing despite validation errors (--no-strict)", fg="yellow"),
|
|
142
|
+
)
|
|
143
|
+
|
|
144
|
+
# Report routing errors
|
|
145
|
+
if routing_errors:
|
|
146
|
+
click.echo("")
|
|
147
|
+
click.echo(click.style("Job routing configuration errors:", fg="red", bold=True))
|
|
148
|
+
for error in routing_errors:
|
|
149
|
+
click.echo(f" - {error}")
|
|
150
|
+
|
|
151
|
+
if runtime_overrides.strict_validation:
|
|
152
|
+
click.echo("")
|
|
153
|
+
click.echo(
|
|
154
|
+
"Exiting due to routing errors. Use --no-strict to continue anyway.",
|
|
155
|
+
err=True,
|
|
156
|
+
)
|
|
157
|
+
sys.exit(1)
|
|
158
|
+
else:
|
|
159
|
+
click.echo("")
|
|
160
|
+
click.echo(
|
|
161
|
+
click.style("Continuing despite routing errors (--no-strict)", fg="yellow"),
|
|
162
|
+
)
|
|
163
|
+
|
|
164
|
+
# Show discovered jobs
|
|
165
|
+
jobs = daemon.jobs
|
|
166
|
+
if not jobs:
|
|
167
|
+
click.echo(" Jobs discovered: 0")
|
|
168
|
+
else:
|
|
169
|
+
click.echo(f" Jobs discovered: {len(jobs)}")
|
|
170
|
+
job_items = list(jobs.items())[:20]
|
|
171
|
+
for job_id, job in job_items:
|
|
172
|
+
click.echo(f" - {job_id}: {job.name}")
|
|
173
|
+
if len(jobs) > 20:
|
|
174
|
+
click.echo(click.style(f" ... and {len(jobs) - 20} more", dim=True))
|
|
175
|
+
|
|
176
|
+
# Check for orphaned jobs
|
|
177
|
+
db = daemon.db
|
|
178
|
+
db_jobs = db.list_all_jobs()
|
|
179
|
+
discovered_job_ids = set(jobs.keys())
|
|
180
|
+
orphaned = [j for j in db_jobs if j.job_id not in discovered_job_ids and j.enabled]
|
|
181
|
+
if orphaned:
|
|
182
|
+
click.echo("")
|
|
183
|
+
click.echo(
|
|
184
|
+
click.style("Orphaned jobs", fg="yellow", bold=True)
|
|
185
|
+
+ click.style(" (in database but not discovered):", fg="yellow")
|
|
186
|
+
)
|
|
187
|
+
for j in orphaned:
|
|
188
|
+
click.echo(click.style(f" - {j.job_id}", fg="yellow"))
|
|
189
|
+
click.echo(click.style(" These won't run. Check: --jobs-module or ./jobs/", dim=True))
|
|
190
|
+
|
|
191
|
+
# Warn if no jobs registered
|
|
192
|
+
if not jobs:
|
|
193
|
+
pending_intents = db.get_intents_by_status(
|
|
194
|
+
IntentStatus.PENDING.value, chain_id=config.chain_id
|
|
195
|
+
)
|
|
196
|
+
pending_count = len(pending_intents)
|
|
197
|
+
|
|
198
|
+
click.echo("")
|
|
199
|
+
if pending_count > 0:
|
|
200
|
+
click.echo(
|
|
201
|
+
click.style(f"No jobs registered but {pending_count} pending intent(s) found.", fg="yellow"),
|
|
202
|
+
)
|
|
203
|
+
click.echo(
|
|
204
|
+
click.style(" Continuing to monitor pending transactions.", dim=True),
|
|
205
|
+
)
|
|
206
|
+
else:
|
|
207
|
+
modules = list(jobs_modules)
|
|
208
|
+
if modules:
|
|
209
|
+
click.echo(
|
|
210
|
+
click.style("No jobs registered.", fg="yellow")
|
|
211
|
+
+ " Ensure --jobs-module values are correct and jobs use @job.",
|
|
212
|
+
)
|
|
213
|
+
else:
|
|
214
|
+
click.echo(
|
|
215
|
+
click.style("No jobs registered.", fg="yellow")
|
|
216
|
+
+ " Add jobs under ./jobs or use --jobs-module.",
|
|
217
|
+
)
|
|
218
|
+
|
|
219
|
+
click.echo("\n--- Starting brawny ---")
|
|
220
|
+
|
|
221
|
+
# Switch to runtime logging (full structured JSON)
|
|
222
|
+
set_runtime_logging()
|
|
223
|
+
log = get_logger(__name__)
|
|
224
|
+
|
|
225
|
+
# Create shutdown handler
|
|
226
|
+
nonce_manager = daemon._executor.nonce_manager if daemon._executor else None
|
|
227
|
+
shutdown_handler = ShutdownHandler(config, db, daemon.rpc, nonce_manager=nonce_manager)
|
|
228
|
+
shutdown_handler.register_callback(lambda: daemon.stop(timeout=5.0))
|
|
229
|
+
|
|
230
|
+
with ShutdownContext(shutdown_handler):
|
|
231
|
+
if once:
|
|
232
|
+
try:
|
|
233
|
+
daemon.run(blocking=True)
|
|
234
|
+
click.echo("Single iteration complete.")
|
|
235
|
+
except Exception as e:
|
|
236
|
+
click.echo(f"Error: {e}", err=True)
|
|
237
|
+
sys.exit(1)
|
|
238
|
+
else:
|
|
239
|
+
click.echo("Polling for new blocks... (Ctrl+C to stop)")
|
|
240
|
+
try:
|
|
241
|
+
daemon.run(blocking=True)
|
|
242
|
+
except KeyboardInterrupt:
|
|
243
|
+
click.echo("\nShutdown requested...")
|
|
244
|
+
|
|
245
|
+
click.echo("Shutdown complete.")
|
|
246
|
+
|
|
247
|
+
|
|
248
|
+
def register(main) -> None:
|
|
249
|
+
main.add_command(start)
|