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,209 @@
|
|
|
1
|
+
"""Script execution command (brownie-compatible).
|
|
2
|
+
|
|
3
|
+
Usage:
|
|
4
|
+
brawny run scripts/harvest.py
|
|
5
|
+
brawny run scripts/harvest.py main --arg1 value1
|
|
6
|
+
brawny run scripts/deploy.py --fork
|
|
7
|
+
"""
|
|
8
|
+
|
|
9
|
+
from __future__ import annotations
|
|
10
|
+
|
|
11
|
+
import importlib.util
|
|
12
|
+
import os
|
|
13
|
+
import sys
|
|
14
|
+
from pathlib import Path
|
|
15
|
+
|
|
16
|
+
import click
|
|
17
|
+
|
|
18
|
+
from brawny.logging import get_logger
|
|
19
|
+
|
|
20
|
+
log = get_logger(__name__)
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
@click.command("run")
|
|
24
|
+
@click.argument("script_path", type=click.Path(exists=True))
|
|
25
|
+
@click.argument("function", default="main")
|
|
26
|
+
@click.argument("args", nargs=-1)
|
|
27
|
+
@click.option("--config", "config_path", default="./config.yaml", help="Path to config.yaml")
|
|
28
|
+
@click.option("--fork", is_flag=True, help="Fork network with Anvil")
|
|
29
|
+
@click.option("--fork-block", type=int, help="Fork at specific block")
|
|
30
|
+
@click.option("--port", default=8545, type=int, help="Port for Anvil fork")
|
|
31
|
+
@click.option("--interactive", "-i", is_flag=True, help="Drop into console after script")
|
|
32
|
+
def run(
|
|
33
|
+
script_path: str,
|
|
34
|
+
function: str,
|
|
35
|
+
args: tuple[str, ...],
|
|
36
|
+
config_path: str,
|
|
37
|
+
fork: bool,
|
|
38
|
+
fork_block: int | None,
|
|
39
|
+
port: int,
|
|
40
|
+
interactive: bool,
|
|
41
|
+
):
|
|
42
|
+
"""Run a Python script with brawny context (brownie-compatible).
|
|
43
|
+
|
|
44
|
+
Scripts have access to:
|
|
45
|
+
|
|
46
|
+
accounts - Signing accounts from keystore
|
|
47
|
+
|
|
48
|
+
Contract() - Contract interaction
|
|
49
|
+
|
|
50
|
+
chain - Block information
|
|
51
|
+
|
|
52
|
+
history - Transaction history
|
|
53
|
+
|
|
54
|
+
Wei() - Unit conversion
|
|
55
|
+
|
|
56
|
+
web3, rpc - Direct RPC access
|
|
57
|
+
|
|
58
|
+
Example:
|
|
59
|
+
|
|
60
|
+
brawny run scripts/harvest.py
|
|
61
|
+
|
|
62
|
+
brawny run scripts/deploy.py deploy --fork
|
|
63
|
+
|
|
64
|
+
brawny run scripts/debug.py --interactive
|
|
65
|
+
"""
|
|
66
|
+
from brawny.alerts.contracts import ContractSystem
|
|
67
|
+
from brawny.config import Config
|
|
68
|
+
from brawny.keystore import create_keystore
|
|
69
|
+
from brawny._rpc import RPCManager
|
|
70
|
+
from brawny.accounts import _init_accounts
|
|
71
|
+
from brawny.history import _init_history
|
|
72
|
+
from brawny.chain import _init_chain
|
|
73
|
+
from brawny.script_tx import _init_broadcaster
|
|
74
|
+
from brawny.api import _set_fallback_rpc
|
|
75
|
+
from brawny._context import _job_ctx
|
|
76
|
+
from brawny.model.types import JobContext, BlockInfo
|
|
77
|
+
from brawny.logging import setup_logging
|
|
78
|
+
|
|
79
|
+
# Setup logging
|
|
80
|
+
setup_logging(log_level="INFO")
|
|
81
|
+
|
|
82
|
+
if not os.path.exists(config_path):
|
|
83
|
+
click.echo(f"Config file not found: {config_path}", err=True)
|
|
84
|
+
sys.exit(1)
|
|
85
|
+
|
|
86
|
+
config = Config.from_yaml(config_path)
|
|
87
|
+
config, _ = config.apply_env_overrides()
|
|
88
|
+
|
|
89
|
+
from brawny.config.routing import resolve_default_group
|
|
90
|
+
|
|
91
|
+
default_group = resolve_default_group(config)
|
|
92
|
+
rpc_endpoints = config.rpc_groups[default_group].endpoints
|
|
93
|
+
chain_id = config.chain_id
|
|
94
|
+
|
|
95
|
+
if not rpc_endpoints:
|
|
96
|
+
click.echo("No RPC endpoints configured", err=True)
|
|
97
|
+
sys.exit(1)
|
|
98
|
+
|
|
99
|
+
if fork:
|
|
100
|
+
# Start Anvil fork
|
|
101
|
+
from brawny.cli.commands.console import _start_anvil_fork
|
|
102
|
+
local_url = _start_anvil_fork(rpc_endpoints[0], chain_id, port, block=fork_block)
|
|
103
|
+
rpc_endpoints = [local_url]
|
|
104
|
+
click.echo(f"Started Anvil fork at {local_url}")
|
|
105
|
+
|
|
106
|
+
# Create RPC manager
|
|
107
|
+
rpc = RPCManager(
|
|
108
|
+
endpoints=rpc_endpoints,
|
|
109
|
+
timeout_seconds=config.rpc_timeout_seconds,
|
|
110
|
+
max_retries=config.rpc_max_retries,
|
|
111
|
+
)
|
|
112
|
+
_set_fallback_rpc(rpc)
|
|
113
|
+
|
|
114
|
+
# Create keystore for signing
|
|
115
|
+
keystore = create_keystore(
|
|
116
|
+
config.keystore_type,
|
|
117
|
+
keystore_path=config.keystore_path,
|
|
118
|
+
allowed_signers=[],
|
|
119
|
+
)
|
|
120
|
+
|
|
121
|
+
# Create contract system
|
|
122
|
+
contract_system = ContractSystem(rpc, config)
|
|
123
|
+
|
|
124
|
+
# Initialize script singletons
|
|
125
|
+
_init_accounts()
|
|
126
|
+
_init_history()
|
|
127
|
+
_init_chain(rpc, chain_id)
|
|
128
|
+
_init_broadcaster(rpc, keystore, chain_id)
|
|
129
|
+
|
|
130
|
+
# Set up job context for Contract() to work
|
|
131
|
+
block_data = rpc.get_block("latest")
|
|
132
|
+
block = BlockInfo(
|
|
133
|
+
chain_id=chain_id,
|
|
134
|
+
block_number=block_data["number"],
|
|
135
|
+
block_hash=block_data["hash"],
|
|
136
|
+
timestamp=block_data["timestamp"],
|
|
137
|
+
)
|
|
138
|
+
ctx = JobContext(
|
|
139
|
+
block=block,
|
|
140
|
+
rpc=rpc,
|
|
141
|
+
logger=log,
|
|
142
|
+
contract_system=contract_system,
|
|
143
|
+
hook_name="script",
|
|
144
|
+
)
|
|
145
|
+
_job_ctx.set(ctx)
|
|
146
|
+
|
|
147
|
+
# Load and run script
|
|
148
|
+
script_file = Path(script_path).resolve()
|
|
149
|
+
sys.path.insert(0, str(script_file.parent))
|
|
150
|
+
|
|
151
|
+
try:
|
|
152
|
+
# Load module
|
|
153
|
+
spec = importlib.util.spec_from_file_location("__script__", script_file)
|
|
154
|
+
if spec is None or spec.loader is None:
|
|
155
|
+
raise click.ClickException(f"Cannot load script: {script_path}")
|
|
156
|
+
|
|
157
|
+
module = importlib.util.module_from_spec(spec)
|
|
158
|
+
sys.modules["__script__"] = module
|
|
159
|
+
spec.loader.exec_module(module)
|
|
160
|
+
|
|
161
|
+
# Find and call function
|
|
162
|
+
if not hasattr(module, function):
|
|
163
|
+
available = [n for n in dir(module) if not n.startswith("_") and callable(getattr(module, n))]
|
|
164
|
+
raise click.ClickException(
|
|
165
|
+
f"Function '{function}' not found in {script_path}. "
|
|
166
|
+
f"Available: {', '.join(available)}"
|
|
167
|
+
)
|
|
168
|
+
|
|
169
|
+
func = getattr(module, function)
|
|
170
|
+
|
|
171
|
+
# Parse args (simple key=value style)
|
|
172
|
+
kwargs = {}
|
|
173
|
+
positional = []
|
|
174
|
+
for arg in args:
|
|
175
|
+
if "=" in arg:
|
|
176
|
+
key, value = arg.split("=", 1)
|
|
177
|
+
kwargs[key] = value
|
|
178
|
+
else:
|
|
179
|
+
positional.append(arg)
|
|
180
|
+
|
|
181
|
+
# Execute
|
|
182
|
+
log.info("script.run", script=script_path, function=function)
|
|
183
|
+
result = func(*positional, **kwargs)
|
|
184
|
+
|
|
185
|
+
if result is not None:
|
|
186
|
+
click.echo(f"Result: {result}")
|
|
187
|
+
|
|
188
|
+
log.info("script.complete")
|
|
189
|
+
|
|
190
|
+
# Interactive mode: drop into console with script namespace
|
|
191
|
+
if interactive:
|
|
192
|
+
click.echo("\nDropping into interactive console (script namespace available)...")
|
|
193
|
+
try:
|
|
194
|
+
from brawny.cli.commands.console import _start_repl
|
|
195
|
+
# Make script module's namespace available
|
|
196
|
+
namespace = {name: getattr(module, name) for name in dir(module) if not name.startswith("_")}
|
|
197
|
+
_start_repl(namespace)
|
|
198
|
+
except ImportError:
|
|
199
|
+
click.echo("Interactive mode requires prompt_toolkit. Install with: pip install prompt_toolkit", err=True)
|
|
200
|
+
|
|
201
|
+
finally:
|
|
202
|
+
sys.path.remove(str(script_file.parent))
|
|
203
|
+
if "__script__" in sys.modules:
|
|
204
|
+
del sys.modules["__script__"]
|
|
205
|
+
|
|
206
|
+
|
|
207
|
+
def register(main) -> None:
|
|
208
|
+
"""Register run command with main CLI (brownie-compatible script runner)."""
|
|
209
|
+
main.add_command(run)
|
|
@@ -0,0 +1,248 @@
|
|
|
1
|
+
"""Signer management commands."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import click
|
|
6
|
+
|
|
7
|
+
from brawny.cli.helpers import get_db
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
def resolve_signer_address(db, chain_id: int, address_or_alias: str) -> str:
|
|
11
|
+
"""Resolve address or alias to address.
|
|
12
|
+
|
|
13
|
+
Args:
|
|
14
|
+
db: Database connection
|
|
15
|
+
chain_id: Chain ID for lookup
|
|
16
|
+
address_or_alias: Either a 0x address or an alias string
|
|
17
|
+
|
|
18
|
+
Returns:
|
|
19
|
+
Lowercase address
|
|
20
|
+
|
|
21
|
+
Raises:
|
|
22
|
+
click.ClickException: If alias not found
|
|
23
|
+
"""
|
|
24
|
+
# If it looks like an address, use directly
|
|
25
|
+
if address_or_alias.startswith("0x") and len(address_or_alias) == 42:
|
|
26
|
+
return address_or_alias.lower()
|
|
27
|
+
|
|
28
|
+
# Otherwise, look up alias in signers table
|
|
29
|
+
signer = db.get_signer_by_alias(chain_id, address_or_alias)
|
|
30
|
+
if signer is None:
|
|
31
|
+
raise click.ClickException(f"Unknown signer alias: {address_or_alias}")
|
|
32
|
+
return signer.signer_address
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
@click.group()
|
|
36
|
+
def signer() -> None:
|
|
37
|
+
"""Signer management commands."""
|
|
38
|
+
pass
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
@signer.command("force-reset")
|
|
42
|
+
@click.argument("address_or_alias")
|
|
43
|
+
@click.option("--yes", "-y", is_flag=True, help="Skip confirmation prompt")
|
|
44
|
+
@click.option("--config", "config_path", default=None, help="Path to config.yaml")
|
|
45
|
+
def force_reset(address_or_alias: str, yes: bool, config_path: str | None) -> None:
|
|
46
|
+
"""Force reset nonce state for a signer. USE WITH CAUTION.
|
|
47
|
+
|
|
48
|
+
ADDRESS_OR_ALIAS can be:
|
|
49
|
+
- Full address: 0x1234567890abcdef1234567890abcdef12345678
|
|
50
|
+
- Signer alias: hot-wallet-1
|
|
51
|
+
|
|
52
|
+
This will:
|
|
53
|
+
- Query current chain pending nonce
|
|
54
|
+
- Reset local next_nonce to match chain
|
|
55
|
+
- Release all reservations with nonce >= chain_pending_nonce
|
|
56
|
+
- Clear gap tracking
|
|
57
|
+
|
|
58
|
+
WARNING: If any prior transactions later mine, you may have duplicate
|
|
59
|
+
transactions or nonce conflicts.
|
|
60
|
+
"""
|
|
61
|
+
from web3 import Web3
|
|
62
|
+
|
|
63
|
+
from brawny.config import Config, get_config
|
|
64
|
+
from brawny.db import create_database
|
|
65
|
+
from brawny._rpc import RPCManager
|
|
66
|
+
from brawny.tx.nonce import NonceManager
|
|
67
|
+
from brawny.model.enums import NonceStatus
|
|
68
|
+
|
|
69
|
+
# Load config
|
|
70
|
+
if config_path:
|
|
71
|
+
config = Config.from_yaml(config_path)
|
|
72
|
+
config, _ = config.apply_env_overrides()
|
|
73
|
+
else:
|
|
74
|
+
config = get_config()
|
|
75
|
+
|
|
76
|
+
# Connect to database
|
|
77
|
+
db = create_database(
|
|
78
|
+
config.database_url,
|
|
79
|
+
pool_size=config.database_pool_size,
|
|
80
|
+
pool_max_overflow=config.database_pool_max_overflow,
|
|
81
|
+
pool_timeout=config.database_pool_timeout_seconds,
|
|
82
|
+
circuit_breaker_failures=config.db_circuit_breaker_failures,
|
|
83
|
+
circuit_breaker_seconds=config.db_circuit_breaker_seconds,
|
|
84
|
+
)
|
|
85
|
+
db.connect()
|
|
86
|
+
|
|
87
|
+
try:
|
|
88
|
+
# Resolve address or alias
|
|
89
|
+
address = resolve_signer_address(db, config.chain_id, address_or_alias)
|
|
90
|
+
|
|
91
|
+
# Setup RPC and nonce manager
|
|
92
|
+
rpc = RPCManager.from_config(config)
|
|
93
|
+
nonce_manager = NonceManager(db, rpc, config.chain_id)
|
|
94
|
+
|
|
95
|
+
# Get current state
|
|
96
|
+
chain_pending = rpc.get_transaction_count(
|
|
97
|
+
Web3.to_checksum_address(address), block_identifier="pending"
|
|
98
|
+
)
|
|
99
|
+
signer_state = db.get_signer_state(config.chain_id, address)
|
|
100
|
+
reservations = db.get_reservations_for_signer(config.chain_id, address)
|
|
101
|
+
|
|
102
|
+
# Calculate affected reservations
|
|
103
|
+
affected = [
|
|
104
|
+
r for r in reservations
|
|
105
|
+
if r.nonce >= chain_pending and r.status in (NonceStatus.RESERVED, NonceStatus.IN_FLIGHT)
|
|
106
|
+
]
|
|
107
|
+
|
|
108
|
+
# Display current state
|
|
109
|
+
click.echo(f"\nSigner: {address}")
|
|
110
|
+
if address_or_alias.lower() != address:
|
|
111
|
+
click.echo(f"Alias: {address_or_alias}")
|
|
112
|
+
click.echo(f"Current chain pending nonce: {chain_pending}")
|
|
113
|
+
click.echo(f"Current local next_nonce: {signer_state.next_nonce if signer_state else 'N/A'}")
|
|
114
|
+
click.echo(f"Reservations to release: {len(affected)}")
|
|
115
|
+
|
|
116
|
+
if affected:
|
|
117
|
+
click.echo("\nReservations that will be released:")
|
|
118
|
+
for r in affected:
|
|
119
|
+
click.echo(f" - nonce {r.nonce}: {r.status.value} (intent: {r.intent_id})")
|
|
120
|
+
|
|
121
|
+
click.echo("\n" + click.style(
|
|
122
|
+
"WARNING: This may cause duplicate transactions if prior txs later mine!",
|
|
123
|
+
fg="yellow", bold=True
|
|
124
|
+
))
|
|
125
|
+
|
|
126
|
+
# Confirm if not --yes
|
|
127
|
+
if not yes:
|
|
128
|
+
if not click.confirm("\nProceed with force reset?"):
|
|
129
|
+
click.echo("Aborted.")
|
|
130
|
+
return
|
|
131
|
+
|
|
132
|
+
# Execute force reset
|
|
133
|
+
new_nonce = nonce_manager.force_reset(address)
|
|
134
|
+
click.echo(click.style(
|
|
135
|
+
f"\n✓ Reset complete. next_nonce now {new_nonce}",
|
|
136
|
+
fg="green", bold=True
|
|
137
|
+
))
|
|
138
|
+
|
|
139
|
+
finally:
|
|
140
|
+
db.close()
|
|
141
|
+
|
|
142
|
+
|
|
143
|
+
@signer.command("status")
|
|
144
|
+
@click.argument("address_or_alias")
|
|
145
|
+
@click.option("--config", "config_path", default=None, help="Path to config.yaml")
|
|
146
|
+
def status(address_or_alias: str, config_path: str | None) -> None:
|
|
147
|
+
"""Show nonce status for a signer.
|
|
148
|
+
|
|
149
|
+
ADDRESS_OR_ALIAS can be:
|
|
150
|
+
- Full address: 0x1234567890abcdef1234567890abcdef12345678
|
|
151
|
+
- Signer alias: hot-wallet-1
|
|
152
|
+
"""
|
|
153
|
+
from web3 import Web3
|
|
154
|
+
|
|
155
|
+
from brawny.config import Config, get_config
|
|
156
|
+
from brawny.db import create_database
|
|
157
|
+
from brawny._rpc import RPCManager
|
|
158
|
+
from brawny.model.enums import NonceStatus
|
|
159
|
+
|
|
160
|
+
# Load config
|
|
161
|
+
if config_path:
|
|
162
|
+
config = Config.from_yaml(config_path)
|
|
163
|
+
config, _ = config.apply_env_overrides()
|
|
164
|
+
else:
|
|
165
|
+
config = get_config()
|
|
166
|
+
|
|
167
|
+
# Connect to database
|
|
168
|
+
db = create_database(
|
|
169
|
+
config.database_url,
|
|
170
|
+
pool_size=config.database_pool_size,
|
|
171
|
+
pool_max_overflow=config.database_pool_max_overflow,
|
|
172
|
+
pool_timeout=config.database_pool_timeout_seconds,
|
|
173
|
+
circuit_breaker_failures=config.db_circuit_breaker_failures,
|
|
174
|
+
circuit_breaker_seconds=config.db_circuit_breaker_seconds,
|
|
175
|
+
)
|
|
176
|
+
db.connect()
|
|
177
|
+
|
|
178
|
+
try:
|
|
179
|
+
# Resolve address or alias
|
|
180
|
+
address = resolve_signer_address(db, config.chain_id, address_or_alias)
|
|
181
|
+
|
|
182
|
+
# Setup RPC
|
|
183
|
+
rpc = RPCManager.from_config(config)
|
|
184
|
+
|
|
185
|
+
# Get current state
|
|
186
|
+
chain_pending = rpc.get_transaction_count(
|
|
187
|
+
Web3.to_checksum_address(address), block_identifier="pending"
|
|
188
|
+
)
|
|
189
|
+
chain_confirmed = rpc.get_transaction_count(
|
|
190
|
+
Web3.to_checksum_address(address), block_identifier="latest"
|
|
191
|
+
)
|
|
192
|
+
signer_state = db.get_signer_state(config.chain_id, address)
|
|
193
|
+
reservations = db.get_reservations_for_signer(config.chain_id, address)
|
|
194
|
+
|
|
195
|
+
# Display status
|
|
196
|
+
click.echo(f"\nSigner: {address}")
|
|
197
|
+
if address_or_alias.lower() != address:
|
|
198
|
+
click.echo(f"Alias: {address_or_alias}")
|
|
199
|
+
|
|
200
|
+
click.echo(f"\nChain State:")
|
|
201
|
+
click.echo(f" Confirmed nonce (latest): {chain_confirmed}")
|
|
202
|
+
click.echo(f" Pending nonce: {chain_pending}")
|
|
203
|
+
|
|
204
|
+
if signer_state:
|
|
205
|
+
click.echo(f"\nLocal State:")
|
|
206
|
+
click.echo(f" next_nonce: {signer_state.next_nonce}")
|
|
207
|
+
click.echo(f" last_synced_chain_nonce: {signer_state.last_synced_chain_nonce}")
|
|
208
|
+
if signer_state.gap_started_at:
|
|
209
|
+
click.echo(click.style(
|
|
210
|
+
f" gap_started_at: {signer_state.gap_started_at} (BLOCKED)",
|
|
211
|
+
fg="red"
|
|
212
|
+
))
|
|
213
|
+
else:
|
|
214
|
+
click.echo(f"\nLocal State: Not initialized")
|
|
215
|
+
|
|
216
|
+
# Check for gap
|
|
217
|
+
active = [r for r in reservations if r.status not in (NonceStatus.RELEASED,)]
|
|
218
|
+
if active:
|
|
219
|
+
expected_next = min(r.nonce for r in active)
|
|
220
|
+
if chain_pending < expected_next:
|
|
221
|
+
gap = expected_next - chain_pending
|
|
222
|
+
click.echo(click.style(
|
|
223
|
+
f"\n⚠️ NONCE GAP DETECTED: chain_pending ({chain_pending}) < expected ({expected_next}), gap={gap}",
|
|
224
|
+
fg="yellow", bold=True
|
|
225
|
+
))
|
|
226
|
+
|
|
227
|
+
# Show reservations
|
|
228
|
+
if reservations:
|
|
229
|
+
click.echo(f"\nReservations ({len(reservations)} total):")
|
|
230
|
+
for r in sorted(reservations, key=lambda x: x.nonce):
|
|
231
|
+
status_color = {
|
|
232
|
+
NonceStatus.RESERVED: "yellow",
|
|
233
|
+
NonceStatus.IN_FLIGHT: "cyan",
|
|
234
|
+
NonceStatus.RELEASED: "green",
|
|
235
|
+
NonceStatus.ORPHANED: "red",
|
|
236
|
+
}.get(r.status, "white")
|
|
237
|
+
click.echo(f" nonce {r.nonce}: " + click.style(r.status.value, fg=status_color) +
|
|
238
|
+
f" (intent: {str(r.intent_id)[:8] if r.intent_id else 'none'})")
|
|
239
|
+
else:
|
|
240
|
+
click.echo(f"\nNo reservations")
|
|
241
|
+
|
|
242
|
+
finally:
|
|
243
|
+
db.close()
|
|
244
|
+
|
|
245
|
+
|
|
246
|
+
def register(main) -> None:
|
|
247
|
+
"""Register signer commands with the main CLI."""
|
|
248
|
+
main.add_command(signer)
|