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
@@ -0,0 +1,1445 @@
1
+ """Templates for brawny init command."""
2
+
3
+ PYPROJECT_TEMPLATE = """\
4
+ [build-system]
5
+ requires = ["setuptools>=68.0", "wheel"]
6
+ build-backend = "setuptools.build_meta"
7
+
8
+ [project]
9
+ name = "{project_name}"
10
+ version = "0.1.0"
11
+ description = "brawny keeper project"
12
+ requires-python = ">=3.10"
13
+ dependencies = ["brawny>=0.1.0"]
14
+
15
+ [tool.setuptools.packages.find]
16
+ where = ["."]
17
+ include = ["{package_name}*"]
18
+ """
19
+
20
+ CONFIG_TEMPLATE = """\
21
+ # brawny configuration
22
+ # See: https://github.com/yearn/brawny#configuration
23
+
24
+ # Core settings
25
+ database_url: sqlite:///data/brawny.db
26
+ rpc_groups:
27
+ primary:
28
+ endpoints:
29
+ - ${{RPC_URL}}
30
+ rpc_default_group: primary
31
+ chain_id: 1
32
+
33
+ # Keystore configuration
34
+ # Options: "file" (preferred) or "env" (least preferred)
35
+ keystore_type: file
36
+ keystore_path: ~/.brawny/keys
37
+
38
+ # SQLite requires worker_count: 1. Use PostgreSQL for multi-worker setups.
39
+ worker_count: 1
40
+
41
+ # Prometheus metrics port (default: 9091)
42
+ # metrics_port: 9091
43
+
44
+ # Telegram alerts (optional)
45
+ # telegram:
46
+ # bot_token: ${{TELEGRAM_BOT_TOKEN}}
47
+ # chats:
48
+ # ops: "-1001234567890"
49
+ # default: ["ops"]
50
+ # parse_mode: "Markdown"
51
+
52
+ # Advanced settings (optional)
53
+ # advanced:
54
+ # poll_interval_seconds: 1.0
55
+ # reorg_depth: 32
56
+ # default_deadline_seconds: 3600
57
+ """
58
+
59
+ ENV_EXAMPLE_TEMPLATE = """\
60
+ # RPC endpoint (required)
61
+ RPC_URL=https://eth-mainnet.g.alchemy.com/v2/YOUR_KEY
62
+
63
+ # Keystore password (file keystore mode)
64
+ # BRAWNY_KEYSTORE_PASSWORD_WORKER=your-password
65
+ # Then import the key:
66
+ # brawny accounts import --name worker --private-key 0x...
67
+
68
+ # Telegram alerts (optional)
69
+ # TELEGRAM_BOT_TOKEN=123456:ABC-DEF...
70
+ """
71
+
72
+ GITIGNORE_TEMPLATE = """\
73
+ # Python
74
+ __pycache__/
75
+ *.py[cod]
76
+ *$py.class
77
+ *.so
78
+ .Python
79
+ build/
80
+ develop-eggs/
81
+ dist/
82
+ downloads/
83
+ eggs/
84
+ .eggs/
85
+ lib/
86
+ lib64/
87
+ parts/
88
+ sdist/
89
+ var/
90
+ wheels/
91
+ *.egg-info/
92
+ .installed.cfg
93
+ *.egg
94
+
95
+ # Virtual environments
96
+ .env
97
+ .venv
98
+ env/
99
+ venv/
100
+ ENV/
101
+
102
+ # brawny
103
+ data/
104
+ *.db
105
+ *.db-journal
106
+
107
+ # IDE
108
+ .idea/
109
+ .vscode/
110
+ *.swp
111
+ *.swo
112
+ *~
113
+
114
+ # OS
115
+ .DS_Store
116
+ Thumbs.db
117
+ """
118
+
119
+ INIT_JOBS_TEMPLATE = '"""Job definitions - auto-discovered from ./jobs."""\n'
120
+
121
+ AGENTS_TEMPLATE = """
122
+ # Agent Guide: Build a Compliant brawny Job
123
+
124
+ This file is meant for user agents that generate new job files. It is a fast, practical spec.
125
+
126
+ ## Golden Rules
127
+ - Avoid over-engineering.
128
+ - Aim for simplicity and elegance.
129
+
130
+ ## Job File Checklist (Minimal)
131
+ - Location: `jobs/<job_name>.py`
132
+ - Import `Job` and `job`.
133
+ - Add `@job` decorator (omit it to hide a WIP job from discovery/validation).
134
+ - Implement `check()` (sync or async).
135
+ - If it sends a transaction, implement `build_intent()` (sync).
136
+
137
+ ## Required vs Optional Hooks
138
+
139
+ ### Required
140
+ - `check(self) -> Trigger | None` OR `check(self, ctx) -> Trigger | None`
141
+ - Must return `trigger(...)` or `None`.
142
+ - **Implicit style** `def check(self):` - use API helpers (`block`, `kv`, `Contract`, `ctx()`)
143
+ - **Explicit style** `def check(self, ctx):` - ctx passed directly (param MUST be named 'ctx')
144
+ - Can be async: `async def check(self)` or `async def check(self, ctx)`
145
+
146
+ ### Required only for tx jobs
147
+ - `build_intent(self, trigger) -> TxIntentSpec`
148
+ - Build calldata and return `intent(...)`.
149
+ - Only called if `trigger.tx_required` is True.
150
+
151
+ ### Optional simulation hook
152
+ - `validate_simulation(self, output) -> bool`
153
+ - Return False to fail the intent after a successful simulation.
154
+
155
+ ### Optional alert hooks
156
+ - `alert_triggered(self, ctx)` - Called when job triggers
157
+ - `alert_confirmed(self, ctx)` - Called after TX confirms (ctx.receipt available)
158
+ - `alert_failed(self, ctx)` - Called on failure (ctx.tx can be None for pre-broadcast failures)
159
+ - Return `str`, `(str, parse_mode)`, or `None`.
160
+
161
+ ### Optional lifecycle hooks
162
+ - `on_success(self, ctx, receipt, intent, attempt)`
163
+ - `on_failure(self, ctx, error, intent, attempt)` # attempt can be None pre-broadcast
164
+
165
+ ## Job Class Attributes
166
+
167
+ ### Required (auto-derived if not set and @job is used)
168
+ - `job_id: str` - Stable identifier (must not change)
169
+ - `name: str` - Human-readable name for logs/alerts
170
+
171
+ ### Optional scheduling
172
+ - `check_interval_blocks: int = 1` - Min blocks between check() calls
173
+ - `check_timeout_seconds: int = 30` - Timeout for check()
174
+ - `build_timeout_seconds: int = 10` - Timeout for build_intent()
175
+ - `max_in_flight_intents: int | None = None` - Cap on active intents
176
+
177
+ ### Optional gas overrides (all values in wei)
178
+ - `max_fee: int | None = None` - Max fee cap for gating/txs (None = no gating)
179
+ - `priority_fee: int | None = None` - Tip override for this job
180
+
181
+ ### Optional simulation
182
+ - `disable_simulation: bool = False` - Skip pre-broadcast simulation
183
+ - `rpc: str | None = None` - Override RPC for simulation
184
+
185
+ ### Broadcast routing (via @job decorator)
186
+ Configure broadcast routing using the `@job` decorator:
187
+ ```python
188
+ @job(job_id="arb_exec", rpc_group="flashbots", signer="hot1")
189
+ class ArbitrageExecutor(Job):
190
+ ...
191
+ ```
192
+ - `job_id` - Optional override (defaults to snake_case of class name)
193
+ - `rpc_group` - Name of RPC group for reads and broadcasts
194
+ - `broadcast_group` - Name of RPC group for broadcasts (default: uses rpc_default_group)
195
+ - `read_group` - Name of RPC group for read operations (default: uses rpc_default_group)
196
+ - `signer` - Name of signer alias (required for tx jobs)
197
+
198
+ Define RPC groups in config:
199
+ ```yaml
200
+ rpc_groups:
201
+ primary:
202
+ endpoints:
203
+ - https://eth.llamarpc.com
204
+ private:
205
+ endpoints:
206
+ - https://rpc.flashbots.net
207
+ - https://relay.flashbots.net
208
+ rpc_default_group: primary
209
+ ```
210
+
211
+ ### Alert routing
212
+ - `@job(alert_to="ops")` - Route alerts to named chat defined in config
213
+ - `@job(alert_to=["ops", "dev"])` - Route to multiple chats
214
+ - Names must be defined in `telegram.chats` config section
215
+ - If not specified, uses `telegram.default` from config
216
+
217
+ ## Core API (What to Use)
218
+
219
+ ### Contract access (brownie-style)
220
+ ```python
221
+ from brawny import Contract
222
+ vault = Contract(self.vault_address) # By address
223
+ decimals = vault.decimals() # View call
224
+ data = vault.harvest.encode_input() # Get calldata
225
+ ```
226
+
227
+
228
+ ### JSON interfaces (brownie-style)
229
+ Place ABI JSON files in `./interfaces`, then:
230
+ ```python
231
+ from brawny import interface
232
+ token = interface.IERC20("0x1234...")
233
+ balance = token.balanceOf("0xabc...")
234
+ ```
235
+
236
+ ### Job hook helpers (implicit context)
237
+ ```python
238
+ from brawny import trigger, intent, block, gas_ok
239
+ return trigger(reason="...", data={...}, idempotency_parts=[block.number])
240
+ return intent(signer_address="worker", to_address=addr, data=calldata)
241
+ ```
242
+
243
+ ### Event access in alert hooks (brownie-compatible)
244
+ ```python
245
+ def alert_confirmed(self, ctx):
246
+ deposit = ctx.events["Deposit"][0] # First Deposit event
247
+ amount = deposit["amount"] # Field access
248
+ if "Deposit" in ctx.events: # Check if event exists
249
+ ...
250
+ ```
251
+
252
+ ### Other context access
253
+ - `ctx()` - Get full CheckContext/BuildContext when using implicit style
254
+ - `block.number`, `block.timestamp` - Current block info
255
+ - `rpc.*` - RPC manager proxy (e.g., `rpc.get_gas_price()`)
256
+ - `gas_ok()` - Check if current gas is below job's max_fee (async)
257
+ - `gas_quote()` - Get current base_fee (async)
258
+ - `kv.get(key, default=None)`, `kv.set(key, value)` - Persistent KV store (import from brawny)
259
+
260
+ ### Accounts
261
+ - Use `intent(signer_address=...)` with a signer alias or address.
262
+ - If you set `@job(signer="alias")`, use `self.signer` (alias) or `self.signer_address` (resolved address).
263
+ - The signer alias must exist in the accounts directory (`~/.brawny/accounts`).
264
+
265
+ ## Example: Transaction Job
266
+
267
+ ```python
268
+ from brawny import Job, job, Contract, trigger, intent, block
269
+
270
+ @job(signer="worker")
271
+ class MyKeeperJob(Job):
272
+ job_id = "my_keeper"
273
+ name = "My Keeper"
274
+ check_interval_blocks = 1
275
+ keeper_address = "0x..."
276
+
277
+ def check(self, ctx):
278
+ keeper = Contract(self.keeper_address)
279
+ if keeper.canWork():
280
+ return trigger(
281
+ reason="Keeper can work",
282
+ idempotency_parts=[block.number],
283
+ )
284
+ return None
285
+
286
+ def build_intent(self, trig):
287
+ keeper = Contract(self.keeper_address)
288
+ return intent(
289
+ signer_address=self.signer,
290
+ to_address=self.keeper_address,
291
+ data=keeper.work.encode_input(),
292
+ )
293
+ ```
294
+
295
+ ## Example: Job with Custom Broadcast and Alerts
296
+
297
+ ```python
298
+ from brawny import Job, Contract, trigger, intent, explorer_link
299
+ from brawny.jobs.registry import job
300
+
301
+ @job(rpc_group="flashbots", signer="treasury-signer", alert_to="private_ops")
302
+ class TreasuryJob(Job):
303
+ \"\"\"Critical treasury operations with dedicated RPC and private alerts.\"\"\"
304
+
305
+ name = "Treasury Operations"
306
+ check_interval_blocks = 1
307
+ treasury_address = "0x..."
308
+
309
+ def check(self, ctx):
310
+ treasury = Contract(self.treasury_address)
311
+ if treasury.needsRebalance():
312
+ return trigger(reason="Treasury needs rebalancing")
313
+ return None
314
+
315
+ def build_intent(self, trig):
316
+ treasury = Contract(self.treasury_address)
317
+ return intent(
318
+ signer_address=self.signer,
319
+ to_address=self.treasury_address,
320
+ data=treasury.rebalance.encode_input(),
321
+ )
322
+
323
+ def alert_confirmed(self, ctx):
324
+ if not ctx.tx:
325
+ return None
326
+ return f"Treasury rebalanced: {explorer_link(ctx.tx.hash)}"
327
+ ```
328
+
329
+ ## Example: Monitor-Only Job (Implicit Context Style)
330
+
331
+ ```python
332
+ from brawny import Job, job, Contract, trigger, kv
333
+
334
+ @job
335
+ class MonitorJob(Job):
336
+ job_id = "monitor"
337
+ name = "Monitor"
338
+
339
+ def check(self): # No ctx param - uses implicit context
340
+ value = Contract("0x...").value()
341
+ last = kv.get("last", 0)
342
+ if value > last:
343
+ kv.set("last", value)
344
+ return trigger(
345
+ reason="Value increased",
346
+ data={"value": value},
347
+ tx_required=False,
348
+ )
349
+ return None
350
+ ```
351
+
352
+ ## Natural-Language -> Job Translation Guide
353
+
354
+ When a user says:
355
+ - **"Check X every block"** -> `check_interval_blocks = 1`
356
+ - **"Only run if gas below Y"** -> set `max_fee` (wei) and use `await gas_ok()` in async check()
357
+ - **"Use signer Z"** -> `@job(signer="Z")` and use `self.signer` in `intent(...)`
358
+ - **"Alert on success/failure"** -> implement `alert_confirmed` / `alert_failed`
359
+ - **"Remember last value"** -> use `kv.get/set` (import from brawny)
360
+ - **"Use Flashbots"** -> `@job(rpc_group="flashbots")` with flashbots group in config
361
+ - **"Send alerts to private channel"** -> `@job(alert_to="private_ops")` with chat in config
362
+
363
+ ## Failure Modes
364
+
365
+ The `alert_failed` hook provides rich context about what failed and when.
366
+
367
+ ### Failure Classification
368
+
369
+ **FailureType** (what failed):
370
+ - `SIMULATION_REVERTED` - TX would revert on-chain (permanent)
371
+ - `SIMULATION_NETWORK_ERROR` - RPC error during simulation (transient)
372
+ - `DEADLINE_EXPIRED` - Intent took too long (permanent)
373
+ - `SIGNER_FAILED` - Keystore/signer issue
374
+ - `NONCE_FAILED` - Couldn't reserve nonce
375
+ - `SIGN_FAILED` - Signing error
376
+ - `BROADCAST_FAILED` - RPC rejected transaction (transient)
377
+ - `TX_REVERTED` - On-chain revert (permanent)
378
+ - `NONCE_CONSUMED` - Nonce used by another transaction
379
+ - `CHECK_EXCEPTION` - job.check() raised an exception
380
+ - `BUILD_TX_EXCEPTION` - job.build_tx() raised an exception
381
+ - `UNKNOWN` - Fallback for unexpected failures
382
+
383
+ **FailureStage** (when it failed):
384
+ - `PRE_BROADCAST` - Failed before reaching the chain
385
+ - `BROADCAST` - Failed during broadcast
386
+ - `POST_BROADCAST` - Failed after broadcast (on-chain)
387
+
388
+ ### AlertContext in alert_failed
389
+
390
+ ```python
391
+ # AlertContext fields (all hooks)
392
+ ctx.job # JobMetadata (id, name)
393
+ ctx.trigger # Trigger that initiated this flow
394
+ ctx.chain_id # Chain ID
395
+ ctx.hook # HookType enum (TRIGGERED, CONFIRMED, FAILED)
396
+ ctx.tx # TxInfo | None (hash, nonce, gas params)
397
+ ctx.receipt # TxReceipt | None (only in alert_confirmed)
398
+ ctx.block # BlockInfo | None
399
+ ctx.error_info # ErrorInfo | None (structured, JSON-safe)
400
+ ctx.failure_type # FailureType | None
401
+ ctx.failure_stage # FailureStage | None
402
+ ctx.events # EventDict (only in alert_confirmed)
403
+
404
+ # AlertContext properties
405
+ ctx.is_permanent_failure # True if retrying won't help
406
+ ctx.is_transient_failure # True if failure might resolve on retry
407
+ ctx.error_message # Convenience: error_info.message or "unknown"
408
+
409
+ # AlertContext methods
410
+ ctx.explorer_link(hash) # "[🔗 View](url)" markdown link
411
+ ctx.shorten(hex_str) # "0x1234...abcd"
412
+ ctx.has_receipt() # True if receipt available
413
+ ctx.has_error() # True if error_info available
414
+ ```
415
+
416
+ ### Example: Handling Failures
417
+
418
+ ```python
419
+ from brawny import Job, job
420
+ from brawny.model.errors import FailureType
421
+
422
+ @job
423
+ class RobustJob(Job):
424
+ job_id = "robust_job"
425
+ name = "Robust Job"
426
+
427
+ def alert_failed(self, ctx):
428
+ # Suppress alerts for transient failures
429
+ if ctx.is_transient_failure:
430
+ return None # No alert
431
+
432
+ # Detailed message for permanent failures
433
+ if ctx.failure_type == FailureType.SIMULATION_REVERTED:
434
+ return f"TX would revert: {ctx.error_message}"
435
+ elif ctx.failure_type == FailureType.TX_REVERTED:
436
+ if not ctx.tx:
437
+ return f"TX reverted on-chain: {ctx.error_message}"
438
+ return f"TX reverted on-chain: {ctx.explorer_link(ctx.tx.hash)}"
439
+ elif ctx.failure_type == FailureType.NONCE_CONSUMED:
440
+ return "Nonce conflict! Check signer activity."
441
+ elif ctx.failure_type == FailureType.CHECK_EXCEPTION:
442
+ return f"check() crashed: {ctx.error_message}"
443
+ elif ctx.failure_type == FailureType.BUILD_TX_EXCEPTION:
444
+ return f"build_intent() crashed: {ctx.error_message}"
445
+ else:
446
+ return f"Failed ({ctx.failure_type.value}): {ctx.error_message}"
447
+ ```
448
+
449
+ ## Required Output from Agent
450
+ When generating a new job file, the agent must provide:
451
+ - File path
452
+ - Job class name
453
+ - `job_id` and `name`
454
+ - `check()` implementation
455
+ - `build_intent()` if tx required
456
+ - Any alert hooks requested
457
+ """
458
+
459
+ EXAMPLES_TEMPLATE = '''\
460
+ """Example job patterns - NOT registered.
461
+
462
+ These are reference implementations. To use them:
463
+ 1. Copy the class to a new file (e.g., my_job.py)
464
+ 2. Add @job decorator
465
+ 3. Customize the implementation
466
+
467
+ Delete this file when you no longer need it.
468
+ """
469
+ from brawny import Job, Contract, trigger, kv
470
+
471
+ # Note: No @job decorator - these are templates only
472
+
473
+
474
+ class MonitorOnlyJob(Job):
475
+ """Monitor-only job - alerts without transactions.
476
+
477
+ Use cases:
478
+ - Price deviation alerts
479
+ - Health check monitoring
480
+ - Threshold breach notifications
481
+
482
+ Outcome:
483
+ - Creates: Trigger only (no intent, no transaction)
484
+ - Alerts: alert_triggered only
485
+ """
486
+
487
+ job_id = "monitor_example"
488
+ name = "Monitor Example"
489
+ check_interval_blocks = 10
490
+
491
+ def __init__(self, oracle_address: str, threshold_percent: float = 5.0):
492
+ self.oracle_address = oracle_address
493
+ self.threshold_percent = threshold_percent
494
+
495
+ def check(self, ctx):
496
+ """Check if condition is met.
497
+
498
+ Returns:
499
+ Trigger with tx_required=False, or None
500
+ """
501
+ oracle = Contract(self.oracle_address)
502
+ price = oracle.latestAnswer() / 1e8
503
+
504
+ last_price = kv.get("last_price")
505
+ if last_price is not None:
506
+ change_pct = abs(price - last_price) / last_price * 100
507
+ if change_pct >= self.threshold_percent:
508
+ kv.set("last_price", price)
509
+ return trigger(
510
+ reason=f"Price changed {change_pct:.2f}%",
511
+ data={
512
+ "old_price": last_price,
513
+ "new_price": price,
514
+ "change_percent": change_pct,
515
+ },
516
+ tx_required=False, # No transaction needed
517
+ )
518
+
519
+ kv.set("last_price", price)
520
+ return None
521
+
522
+ def alert_triggered(self, ctx):
523
+ """Format alert message.
524
+
525
+ Returns:
526
+ Tuple of (message, parse_mode) or string
527
+ """
528
+ data = {}
529
+ return (
530
+ f"Price alert: {data['old_price']:.2f} -> {data['new_price']:.2f}\\n"
531
+ f"Change: {data['change_percent']:.2f}%",
532
+ "Markdown",
533
+ )
534
+ '''
535
+
536
+ # Monitoring stack templates
537
+ DOCKER_COMPOSE_MONITORING_TEMPLATE = """\
538
+ # Production-friendly Prometheus + Grafana stack for Brawny
539
+ # Usage: docker-compose -f monitoring/docker-compose.yml up -d
540
+ #
541
+ # Access:
542
+ # Prometheus: http://localhost:9090
543
+ # Grafana: http://localhost:3000 (admin / admin)
544
+ #
545
+ # For production, set GF_ADMIN_PASSWORD in environment or .env
546
+
547
+ services:
548
+ prometheus:
549
+ image: prom/prometheus:v2.48.0
550
+ container_name: brawny-prometheus
551
+ ports:
552
+ - "9090:9090"
553
+ volumes:
554
+ - ./prometheus.yml:/etc/prometheus/prometheus.yml:ro
555
+ - prometheus_data:/prometheus
556
+ command:
557
+ - '--config.file=/etc/prometheus/prometheus.yml'
558
+ - '--storage.tsdb.path=/prometheus'
559
+ - '--storage.tsdb.retention.time=30d'
560
+ extra_hosts:
561
+ - "host.docker.internal:host-gateway"
562
+ healthcheck:
563
+ test: ["CMD", "wget", "-qO-", "http://localhost:9090/-/ready"]
564
+ interval: 10s
565
+ timeout: 3s
566
+ retries: 3
567
+ restart: unless-stopped
568
+
569
+ grafana:
570
+ image: grafana/grafana:10.2.3
571
+ container_name: brawny-grafana
572
+ ports:
573
+ - "3000:3000"
574
+ volumes:
575
+ - grafana_data:/var/lib/grafana
576
+ - ./grafana/provisioning:/etc/grafana/provisioning:ro
577
+ environment:
578
+ - GF_SECURITY_ADMIN_USER=admin
579
+ - GF_SECURITY_ADMIN_PASSWORD=${{GF_ADMIN_PASSWORD:-admin}}
580
+ - GF_USERS_ALLOW_SIGN_UP=false
581
+ healthcheck:
582
+ test: ["CMD", "wget", "-qO-", "http://localhost:3000/api/health"]
583
+ interval: 10s
584
+ timeout: 3s
585
+ retries: 3
586
+ restart: unless-stopped
587
+
588
+ volumes:
589
+ prometheus_data:
590
+ grafana_data:
591
+ """
592
+
593
+ PROMETHEUS_CONFIG_TEMPLATE = """\
594
+ # Prometheus configuration for Brawny
595
+ #
596
+ # Default: scrapes metrics from Brawny on host at localhost:9091
597
+ #
598
+ # Troubleshooting:
599
+ # macOS/Windows: host.docker.internal:9091 works out of the box
600
+ # Linux: if target is down, replace with your host IP (e.g., 172.17.0.1:9091)
601
+ # or run Brawny in the same Docker network
602
+
603
+ global:
604
+ scrape_interval: 15s
605
+ evaluation_interval: 15s
606
+
607
+ scrape_configs:
608
+ - job_name: 'brawny'
609
+ static_configs:
610
+ - targets: ['host.docker.internal:9091']
611
+ """
612
+
613
+ GRAFANA_DATASOURCE_TEMPLATE = """\
614
+ # Auto-provision Prometheus datasource
615
+ # UID is stable so dashboards can reference it reliably
616
+ apiVersion: 1
617
+
618
+ datasources:
619
+ - name: Prometheus
620
+ uid: prometheus
621
+ type: prometheus
622
+ access: proxy
623
+ url: http://prometheus:9090
624
+ isDefault: true
625
+ editable: false
626
+ """
627
+
628
+ GRAFANA_DASHBOARDS_PROVIDER_TEMPLATE = """\
629
+ # Auto-provision dashboards from this directory
630
+ apiVersion: 1
631
+
632
+ providers:
633
+ - name: 'brawny'
634
+ orgId: 1
635
+ folder: ''
636
+ type: file
637
+ disableDeletion: false
638
+ editable: true
639
+ options:
640
+ path: /etc/grafana/provisioning/dashboards
641
+ """
642
+
643
+ GRAFANA_DASHBOARD_TEMPLATE = """\
644
+ {
645
+ "annotations": {
646
+ "list": []
647
+ },
648
+ "editable": true,
649
+ "fiscalYearStartMonth": 0,
650
+ "graphTooltip": 0,
651
+ "id": null,
652
+ "links": [],
653
+ "panels": [
654
+ {
655
+ "gridPos": {
656
+ "h": 1,
657
+ "w": 24,
658
+ "x": 0,
659
+ "y": 0
660
+ },
661
+ "id": 1,
662
+ "title": "Status",
663
+ "type": "row"
664
+ },
665
+ {
666
+ "datasource": {
667
+ "type": "prometheus",
668
+ "uid": "prometheus"
669
+ },
670
+ "fieldConfig": {
671
+ "defaults": {
672
+ "color": {
673
+ "mode": "thresholds"
674
+ },
675
+ "mappings": [
676
+ {
677
+ "type": "value",
678
+ "options": {
679
+ "0": {
680
+ "text": "false"
681
+ },
682
+ "1": {
683
+ "text": "true"
684
+ }
685
+ }
686
+ }
687
+ ],
688
+ "thresholds": {
689
+ "mode": "absolute",
690
+ "steps": [
691
+ {
692
+ "color": "red",
693
+ "value": null
694
+ },
695
+ {
696
+ "color": "green",
697
+ "value": 1
698
+ }
699
+ ]
700
+ },
701
+ "unit": "short"
702
+ },
703
+ "overrides": []
704
+ },
705
+ "gridPos": {
706
+ "h": 4,
707
+ "w": 4,
708
+ "x": 0,
709
+ "y": 1
710
+ },
711
+ "id": 2,
712
+ "options": {
713
+ "colorMode": "background",
714
+ "graphMode": "none",
715
+ "justifyMode": "auto",
716
+ "orientation": "auto",
717
+ "reduceOptions": {
718
+ "calcs": [
719
+ "lastNotNull"
720
+ ],
721
+ "fields": "",
722
+ "values": false
723
+ },
724
+ "textMode": "auto"
725
+ },
726
+ "pluginVersion": "10.2.3",
727
+ "targets": [
728
+ {
729
+ "datasource": {
730
+ "type": "prometheus",
731
+ "uid": "prometheus"
732
+ },
733
+ "expr": "max(up{job=\"brawny\"})",
734
+ "refId": "A"
735
+ }
736
+ ],
737
+ "title": "Brawny Up",
738
+ "type": "stat"
739
+ },
740
+ {
741
+ "datasource": {
742
+ "type": "prometheus",
743
+ "uid": "prometheus"
744
+ },
745
+ "fieldConfig": {
746
+ "defaults": {
747
+ "color": {
748
+ "mode": "thresholds"
749
+ },
750
+ "mappings": [],
751
+ "thresholds": {
752
+ "mode": "absolute",
753
+ "steps": [
754
+ {
755
+ "color": "green",
756
+ "value": null
757
+ },
758
+ {
759
+ "color": "yellow",
760
+ "value": 120
761
+ },
762
+ {
763
+ "color": "red",
764
+ "value": 300
765
+ }
766
+ ]
767
+ },
768
+ "unit": "s"
769
+ },
770
+ "overrides": []
771
+ },
772
+ "gridPos": {
773
+ "h": 4,
774
+ "w": 4,
775
+ "x": 4,
776
+ "y": 1
777
+ },
778
+ "id": 3,
779
+ "options": {
780
+ "colorMode": "background",
781
+ "graphMode": "none",
782
+ "justifyMode": "auto",
783
+ "orientation": "auto",
784
+ "reduceOptions": {
785
+ "calcs": [
786
+ "lastNotNull"
787
+ ],
788
+ "fields": "",
789
+ "values": false
790
+ },
791
+ "textMode": "auto"
792
+ },
793
+ "pluginVersion": "10.2.3",
794
+ "targets": [
795
+ {
796
+ "datasource": {
797
+ "type": "prometheus",
798
+ "uid": "prometheus"
799
+ },
800
+ "expr": "max(brawny_oldest_pending_intent_age_seconds)",
801
+ "refId": "A"
802
+ }
803
+ ],
804
+ "title": "Oldest Pending Intent",
805
+ "type": "stat"
806
+ },
807
+ {
808
+ "datasource": {
809
+ "type": "prometheus",
810
+ "uid": "prometheus"
811
+ },
812
+ "fieldConfig": {
813
+ "defaults": {
814
+ "color": {
815
+ "mode": "thresholds"
816
+ },
817
+ "mappings": [],
818
+ "thresholds": {
819
+ "mode": "absolute",
820
+ "steps": [
821
+ {
822
+ "color": "green",
823
+ "value": null
824
+ },
825
+ {
826
+ "color": "yellow",
827
+ "value": 10
828
+ },
829
+ {
830
+ "color": "red",
831
+ "value": 50
832
+ }
833
+ ]
834
+ },
835
+ "unit": "short"
836
+ },
837
+ "overrides": []
838
+ },
839
+ "gridPos": {
840
+ "h": 4,
841
+ "w": 4,
842
+ "x": 8,
843
+ "y": 1
844
+ },
845
+ "id": 4,
846
+ "options": {
847
+ "colorMode": "background",
848
+ "graphMode": "none",
849
+ "justifyMode": "auto",
850
+ "orientation": "auto",
851
+ "reduceOptions": {
852
+ "calcs": [
853
+ "lastNotNull"
854
+ ],
855
+ "fields": "",
856
+ "values": false
857
+ },
858
+ "textMode": "auto"
859
+ },
860
+ "pluginVersion": "10.2.3",
861
+ "targets": [
862
+ {
863
+ "datasource": {
864
+ "type": "prometheus",
865
+ "uid": "prometheus"
866
+ },
867
+ "expr": "sum(brawny_pending_intents) or vector(0)",
868
+ "refId": "A"
869
+ }
870
+ ],
871
+ "title": "Pending Intents",
872
+ "type": "stat"
873
+ },
874
+ {
875
+ "datasource": {
876
+ "type": "prometheus",
877
+ "uid": "prometheus"
878
+ },
879
+ "fieldConfig": {
880
+ "defaults": {
881
+ "color": {
882
+ "mode": "thresholds"
883
+ },
884
+ "mappings": [],
885
+ "thresholds": {
886
+ "mode": "absolute",
887
+ "steps": [
888
+ {
889
+ "color": "green",
890
+ "value": null
891
+ }
892
+ ]
893
+ },
894
+ "unit": "short"
895
+ },
896
+ "overrides": []
897
+ },
898
+ "gridPos": {
899
+ "h": 4,
900
+ "w": 4,
901
+ "x": 16,
902
+ "y": 1
903
+ },
904
+ "id": 6,
905
+ "options": {
906
+ "colorMode": "value",
907
+ "graphMode": "area",
908
+ "justifyMode": "auto",
909
+ "orientation": "auto",
910
+ "reduceOptions": {
911
+ "calcs": [
912
+ "lastNotNull"
913
+ ],
914
+ "fields": "",
915
+ "values": false
916
+ },
917
+ "textMode": "auto"
918
+ },
919
+ "pluginVersion": "10.2.3",
920
+ "targets": [
921
+ {
922
+ "datasource": {
923
+ "type": "prometheus",
924
+ "uid": "prometheus"
925
+ },
926
+ "expr": "sum(brawny_blocks_processed_total) or vector(0)",
927
+ "refId": "A"
928
+ }
929
+ ],
930
+ "title": "Blocks Processed",
931
+ "type": "stat"
932
+ },
933
+ {
934
+ "datasource": {
935
+ "type": "prometheus",
936
+ "uid": "prometheus"
937
+ },
938
+ "fieldConfig": {
939
+ "defaults": {
940
+ "color": {
941
+ "mode": "thresholds"
942
+ },
943
+ "mappings": [],
944
+ "thresholds": {
945
+ "mode": "absolute",
946
+ "steps": [
947
+ {
948
+ "color": "green",
949
+ "value": null
950
+ }
951
+ ]
952
+ },
953
+ "unit": "short"
954
+ },
955
+ "overrides": []
956
+ },
957
+ "gridPos": {
958
+ "h": 4,
959
+ "w": 4,
960
+ "x": 20,
961
+ "y": 1
962
+ },
963
+ "id": 7,
964
+ "options": {
965
+ "colorMode": "value",
966
+ "graphMode": "area",
967
+ "justifyMode": "auto",
968
+ "orientation": "auto",
969
+ "reduceOptions": {
970
+ "calcs": [
971
+ "lastNotNull"
972
+ ],
973
+ "fields": "",
974
+ "values": false
975
+ },
976
+ "textMode": "auto"
977
+ },
978
+ "pluginVersion": "10.2.3",
979
+ "targets": [
980
+ {
981
+ "datasource": {
982
+ "type": "prometheus",
983
+ "uid": "prometheus"
984
+ },
985
+ "expr": "sum(brawny_tx_confirmed_total) or vector(0)",
986
+ "refId": "A"
987
+ }
988
+ ],
989
+ "title": "TX Confirmed",
990
+ "type": "stat"
991
+ },
992
+ {
993
+ "gridPos": {
994
+ "h": 1,
995
+ "w": 24,
996
+ "x": 0,
997
+ "y": 5
998
+ },
999
+ "id": 10,
1000
+ "title": "Activity",
1001
+ "type": "row"
1002
+ },
1003
+ {
1004
+ "datasource": {
1005
+ "type": "prometheus",
1006
+ "uid": "prometheus"
1007
+ },
1008
+ "fieldConfig": {
1009
+ "defaults": {
1010
+ "color": {
1011
+ "mode": "palette-classic"
1012
+ },
1013
+ "custom": {
1014
+ "axisBorderShow": false,
1015
+ "axisCenteredZero": false,
1016
+ "axisColorMode": "text",
1017
+ "axisLabel": "",
1018
+ "axisPlacement": "auto",
1019
+ "barAlignment": 0,
1020
+ "drawStyle": "line",
1021
+ "fillOpacity": 10,
1022
+ "gradientMode": "none",
1023
+ "hideFrom": {
1024
+ "legend": false,
1025
+ "tooltip": false,
1026
+ "viz": false
1027
+ },
1028
+ "insertNulls": false,
1029
+ "lineInterpolation": "linear",
1030
+ "lineWidth": 1,
1031
+ "pointSize": 5,
1032
+ "scaleDistribution": {
1033
+ "type": "linear"
1034
+ },
1035
+ "showPoints": "never",
1036
+ "spanNulls": false,
1037
+ "stacking": {
1038
+ "group": "A",
1039
+ "mode": "none"
1040
+ },
1041
+ "thresholdsStyle": {
1042
+ "mode": "line"
1043
+ }
1044
+ },
1045
+ "mappings": [],
1046
+ "thresholds": {
1047
+ "mode": "absolute",
1048
+ "steps": [
1049
+ {
1050
+ "color": "green",
1051
+ "value": null
1052
+ },
1053
+ {
1054
+ "color": "yellow",
1055
+ "value": 120
1056
+ },
1057
+ {
1058
+ "color": "red",
1059
+ "value": 300
1060
+ }
1061
+ ]
1062
+ },
1063
+ "unit": "s"
1064
+ },
1065
+ "overrides": []
1066
+ },
1067
+ "gridPos": {
1068
+ "h": 8,
1069
+ "w": 12,
1070
+ "x": 0,
1071
+ "y": 6
1072
+ },
1073
+ "id": 11,
1074
+ "options": {
1075
+ "legend": {
1076
+ "calcs": [],
1077
+ "displayMode": "list",
1078
+ "placement": "bottom",
1079
+ "showLegend": true
1080
+ },
1081
+ "tooltip": {
1082
+ "mode": "multi",
1083
+ "sort": "none"
1084
+ }
1085
+ },
1086
+ "pluginVersion": "10.2.3",
1087
+ "targets": [
1088
+ {
1089
+ "datasource": {
1090
+ "type": "prometheus",
1091
+ "uid": "prometheus"
1092
+ },
1093
+ "expr": "max(brawny_block_processing_lag_seconds)",
1094
+ "legendFormat": "seconds behind chain head",
1095
+ "refId": "A"
1096
+ }
1097
+ ],
1098
+ "title": "Block Processing Lag",
1099
+ "type": "timeseries"
1100
+ },
1101
+ {
1102
+ "datasource": {
1103
+ "type": "prometheus",
1104
+ "uid": "prometheus"
1105
+ },
1106
+ "fieldConfig": {
1107
+ "defaults": {
1108
+ "color": {
1109
+ "mode": "palette-classic"
1110
+ },
1111
+ "custom": {
1112
+ "axisBorderShow": false,
1113
+ "axisCenteredZero": false,
1114
+ "axisColorMode": "text",
1115
+ "axisLabel": "",
1116
+ "axisPlacement": "auto",
1117
+ "barAlignment": 0,
1118
+ "drawStyle": "line",
1119
+ "fillOpacity": 10,
1120
+ "gradientMode": "none",
1121
+ "hideFrom": {
1122
+ "legend": false,
1123
+ "tooltip": false,
1124
+ "viz": false
1125
+ },
1126
+ "insertNulls": false,
1127
+ "lineInterpolation": "linear",
1128
+ "lineWidth": 1,
1129
+ "pointSize": 5,
1130
+ "scaleDistribution": {
1131
+ "type": "linear"
1132
+ },
1133
+ "showPoints": "never",
1134
+ "spanNulls": false,
1135
+ "stacking": {
1136
+ "group": "A",
1137
+ "mode": "none"
1138
+ },
1139
+ "thresholdsStyle": {
1140
+ "mode": "line"
1141
+ }
1142
+ },
1143
+ "mappings": [],
1144
+ "thresholds": {
1145
+ "mode": "absolute",
1146
+ "steps": [
1147
+ {
1148
+ "color": "green",
1149
+ "value": null
1150
+ },
1151
+ {
1152
+ "color": "yellow",
1153
+ "value": 120
1154
+ },
1155
+ {
1156
+ "color": "red",
1157
+ "value": 300
1158
+ }
1159
+ ]
1160
+ },
1161
+ "unit": "s"
1162
+ },
1163
+ "overrides": []
1164
+ },
1165
+ "gridPos": {
1166
+ "h": 8,
1167
+ "w": 12,
1168
+ "x": 12,
1169
+ "y": 6
1170
+ },
1171
+ "id": 12,
1172
+ "options": {
1173
+ "legend": {
1174
+ "calcs": [],
1175
+ "displayMode": "list",
1176
+ "placement": "bottom",
1177
+ "showLegend": true
1178
+ },
1179
+ "tooltip": {
1180
+ "mode": "multi",
1181
+ "sort": "none"
1182
+ }
1183
+ },
1184
+ "pluginVersion": "10.2.3",
1185
+ "targets": [
1186
+ {
1187
+ "datasource": {
1188
+ "type": "prometheus",
1189
+ "uid": "prometheus"
1190
+ },
1191
+ "expr": "time() - max(brawny_last_tx_confirmed_timestamp)",
1192
+ "legendFormat": "seconds since last TX",
1193
+ "refId": "B"
1194
+ },
1195
+ {
1196
+ "datasource": {
1197
+ "type": "prometheus",
1198
+ "uid": "prometheus"
1199
+ },
1200
+ "expr": "time() - max(brawny_last_intent_created_timestamp)",
1201
+ "legendFormat": "seconds since last intent",
1202
+ "refId": "C"
1203
+ }
1204
+ ],
1205
+ "title": "Activity Staleness",
1206
+ "type": "timeseries"
1207
+ },
1208
+ {
1209
+ "gridPos": {
1210
+ "h": 1,
1211
+ "w": 24,
1212
+ "x": 0,
1213
+ "y": 14
1214
+ },
1215
+ "id": 20,
1216
+ "title": "Transactions",
1217
+ "type": "row"
1218
+ },
1219
+ {
1220
+ "datasource": {
1221
+ "type": "prometheus",
1222
+ "uid": "prometheus"
1223
+ },
1224
+ "fieldConfig": {
1225
+ "defaults": {
1226
+ "color": {
1227
+ "mode": "palette-classic"
1228
+ },
1229
+ "custom": {
1230
+ "axisBorderShow": false,
1231
+ "axisCenteredZero": false,
1232
+ "axisColorMode": "text",
1233
+ "axisLabel": "",
1234
+ "axisPlacement": "auto",
1235
+ "barAlignment": 0,
1236
+ "drawStyle": "line",
1237
+ "fillOpacity": 10,
1238
+ "gradientMode": "none",
1239
+ "hideFrom": {
1240
+ "legend": false,
1241
+ "tooltip": false,
1242
+ "viz": false
1243
+ },
1244
+ "insertNulls": false,
1245
+ "lineInterpolation": "linear",
1246
+ "lineWidth": 1,
1247
+ "pointSize": 5,
1248
+ "scaleDistribution": {
1249
+ "type": "linear"
1250
+ },
1251
+ "showPoints": "never",
1252
+ "spanNulls": false,
1253
+ "stacking": {
1254
+ "group": "A",
1255
+ "mode": "none"
1256
+ },
1257
+ "thresholdsStyle": {
1258
+ "mode": "off"
1259
+ }
1260
+ },
1261
+ "mappings": [],
1262
+ "thresholds": {
1263
+ "mode": "absolute",
1264
+ "steps": [
1265
+ {
1266
+ "color": "green",
1267
+ "value": null
1268
+ }
1269
+ ]
1270
+ },
1271
+ "unit": "short"
1272
+ },
1273
+ "overrides": []
1274
+ },
1275
+ "gridPos": {
1276
+ "h": 8,
1277
+ "w": 12,
1278
+ "x": 0,
1279
+ "y": 15
1280
+ },
1281
+ "id": 21,
1282
+ "options": {
1283
+ "legend": {
1284
+ "calcs": [],
1285
+ "displayMode": "list",
1286
+ "placement": "bottom",
1287
+ "showLegend": true
1288
+ },
1289
+ "tooltip": {
1290
+ "mode": "multi",
1291
+ "sort": "none"
1292
+ }
1293
+ },
1294
+ "pluginVersion": "10.2.3",
1295
+ "targets": [
1296
+ {
1297
+ "datasource": {
1298
+ "type": "prometheus",
1299
+ "uid": "prometheus"
1300
+ },
1301
+ "expr": "sum(rate(brawny_tx_broadcast_total[5m])) * 60 or vector(0)",
1302
+ "legendFormat": "broadcast/min",
1303
+ "refId": "A"
1304
+ },
1305
+ {
1306
+ "datasource": {
1307
+ "type": "prometheus",
1308
+ "uid": "prometheus"
1309
+ },
1310
+ "expr": "sum(rate(brawny_tx_confirmed_total[5m])) * 60 or vector(0)",
1311
+ "legendFormat": "confirmed/min",
1312
+ "refId": "B"
1313
+ },
1314
+ {
1315
+ "datasource": {
1316
+ "type": "prometheus",
1317
+ "uid": "prometheus"
1318
+ },
1319
+ "expr": "sum(rate(brawny_tx_failed_total[5m])) * 60 or vector(0)",
1320
+ "legendFormat": "failed/min",
1321
+ "refId": "C"
1322
+ }
1323
+ ],
1324
+ "title": "TX Throughput",
1325
+ "type": "timeseries"
1326
+ },
1327
+ {
1328
+ "datasource": {
1329
+ "type": "prometheus",
1330
+ "uid": "prometheus"
1331
+ },
1332
+ "fieldConfig": {
1333
+ "defaults": {
1334
+ "color": {
1335
+ "mode": "palette-classic"
1336
+ },
1337
+ "custom": {
1338
+ "axisBorderShow": false,
1339
+ "axisCenteredZero": false,
1340
+ "axisColorMode": "text",
1341
+ "axisLabel": "",
1342
+ "axisPlacement": "auto",
1343
+ "barAlignment": 0,
1344
+ "drawStyle": "line",
1345
+ "fillOpacity": 10,
1346
+ "gradientMode": "none",
1347
+ "hideFrom": {
1348
+ "legend": false,
1349
+ "tooltip": false,
1350
+ "viz": false
1351
+ },
1352
+ "insertNulls": false,
1353
+ "lineInterpolation": "linear",
1354
+ "lineWidth": 1,
1355
+ "pointSize": 5,
1356
+ "scaleDistribution": {
1357
+ "type": "linear"
1358
+ },
1359
+ "showPoints": "never",
1360
+ "spanNulls": false,
1361
+ "stacking": {
1362
+ "group": "A",
1363
+ "mode": "none"
1364
+ },
1365
+ "thresholdsStyle": {
1366
+ "mode": "line"
1367
+ }
1368
+ },
1369
+ "mappings": [],
1370
+ "thresholds": {
1371
+ "mode": "absolute",
1372
+ "steps": [
1373
+ {
1374
+ "color": "green",
1375
+ "value": null
1376
+ },
1377
+ {
1378
+ "color": "yellow",
1379
+ "value": 120
1380
+ },
1381
+ {
1382
+ "color": "red",
1383
+ "value": 300
1384
+ }
1385
+ ]
1386
+ },
1387
+ "unit": "s"
1388
+ },
1389
+ "overrides": []
1390
+ },
1391
+ "gridPos": {
1392
+ "h": 8,
1393
+ "w": 12,
1394
+ "x": 12,
1395
+ "y": 15
1396
+ },
1397
+ "id": 22,
1398
+ "options": {
1399
+ "legend": {
1400
+ "calcs": [],
1401
+ "displayMode": "list",
1402
+ "placement": "bottom",
1403
+ "showLegend": true
1404
+ },
1405
+ "tooltip": {
1406
+ "mode": "multi",
1407
+ "sort": "none"
1408
+ }
1409
+ },
1410
+ "pluginVersion": "10.2.3",
1411
+ "targets": [
1412
+ {
1413
+ "datasource": {
1414
+ "type": "prometheus",
1415
+ "uid": "prometheus"
1416
+ },
1417
+ "expr": "max(brawny_oldest_pending_intent_age_seconds)",
1418
+ "legendFormat": "oldest pending age",
1419
+ "refId": "A"
1420
+ }
1421
+ ],
1422
+ "title": "Oldest Pending Intent Age",
1423
+ "type": "timeseries"
1424
+ }
1425
+ ],
1426
+ "refresh": "10s",
1427
+ "schemaVersion": 39,
1428
+ "tags": [
1429
+ "brawny"
1430
+ ],
1431
+ "templating": {
1432
+ "list": []
1433
+ },
1434
+ "time": {
1435
+ "from": "now-1h",
1436
+ "to": "now"
1437
+ },
1438
+ "timepicker": {},
1439
+ "timezone": "browser",
1440
+ "title": "Brawny Overview",
1441
+ "uid": "brawny-overview",
1442
+ "version": 1,
1443
+ "weekStart": ""
1444
+ }
1445
+ """