brawny 0.1.13__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (141) hide show
  1. brawny/__init__.py +106 -0
  2. brawny/_context.py +232 -0
  3. brawny/_rpc/__init__.py +38 -0
  4. brawny/_rpc/broadcast.py +172 -0
  5. brawny/_rpc/clients.py +98 -0
  6. brawny/_rpc/context.py +49 -0
  7. brawny/_rpc/errors.py +252 -0
  8. brawny/_rpc/gas.py +158 -0
  9. brawny/_rpc/manager.py +982 -0
  10. brawny/_rpc/selector.py +156 -0
  11. brawny/accounts.py +534 -0
  12. brawny/alerts/__init__.py +132 -0
  13. brawny/alerts/abi_resolver.py +530 -0
  14. brawny/alerts/base.py +152 -0
  15. brawny/alerts/context.py +271 -0
  16. brawny/alerts/contracts.py +635 -0
  17. brawny/alerts/encoded_call.py +201 -0
  18. brawny/alerts/errors.py +267 -0
  19. brawny/alerts/events.py +680 -0
  20. brawny/alerts/function_caller.py +364 -0
  21. brawny/alerts/health.py +185 -0
  22. brawny/alerts/routing.py +118 -0
  23. brawny/alerts/send.py +364 -0
  24. brawny/api.py +660 -0
  25. brawny/chain.py +93 -0
  26. brawny/cli/__init__.py +16 -0
  27. brawny/cli/app.py +17 -0
  28. brawny/cli/bootstrap.py +37 -0
  29. brawny/cli/commands/__init__.py +41 -0
  30. brawny/cli/commands/abi.py +93 -0
  31. brawny/cli/commands/accounts.py +632 -0
  32. brawny/cli/commands/console.py +495 -0
  33. brawny/cli/commands/contract.py +139 -0
  34. brawny/cli/commands/health.py +112 -0
  35. brawny/cli/commands/init_project.py +86 -0
  36. brawny/cli/commands/intents.py +130 -0
  37. brawny/cli/commands/job_dev.py +254 -0
  38. brawny/cli/commands/jobs.py +308 -0
  39. brawny/cli/commands/logs.py +87 -0
  40. brawny/cli/commands/maintenance.py +182 -0
  41. brawny/cli/commands/migrate.py +51 -0
  42. brawny/cli/commands/networks.py +253 -0
  43. brawny/cli/commands/run.py +249 -0
  44. brawny/cli/commands/script.py +209 -0
  45. brawny/cli/commands/signer.py +248 -0
  46. brawny/cli/helpers.py +265 -0
  47. brawny/cli_templates.py +1445 -0
  48. brawny/config/__init__.py +74 -0
  49. brawny/config/models.py +404 -0
  50. brawny/config/parser.py +633 -0
  51. brawny/config/routing.py +55 -0
  52. brawny/config/validation.py +246 -0
  53. brawny/daemon/__init__.py +14 -0
  54. brawny/daemon/context.py +69 -0
  55. brawny/daemon/core.py +702 -0
  56. brawny/daemon/loops.py +327 -0
  57. brawny/db/__init__.py +78 -0
  58. brawny/db/base.py +986 -0
  59. brawny/db/base_new.py +165 -0
  60. brawny/db/circuit_breaker.py +97 -0
  61. brawny/db/global_cache.py +298 -0
  62. brawny/db/mappers.py +182 -0
  63. brawny/db/migrate.py +349 -0
  64. brawny/db/migrations/001_init.sql +186 -0
  65. brawny/db/migrations/002_add_included_block.sql +7 -0
  66. brawny/db/migrations/003_add_broadcast_at.sql +10 -0
  67. brawny/db/migrations/004_broadcast_binding.sql +20 -0
  68. brawny/db/migrations/005_add_retry_after.sql +9 -0
  69. brawny/db/migrations/006_add_retry_count_column.sql +11 -0
  70. brawny/db/migrations/007_add_gap_tracking.sql +18 -0
  71. brawny/db/migrations/008_add_transactions.sql +72 -0
  72. brawny/db/migrations/009_add_intent_metadata.sql +5 -0
  73. brawny/db/migrations/010_add_nonce_gap_index.sql +9 -0
  74. brawny/db/migrations/011_add_job_logs.sql +24 -0
  75. brawny/db/migrations/012_add_claimed_by.sql +5 -0
  76. brawny/db/ops/__init__.py +29 -0
  77. brawny/db/ops/attempts.py +108 -0
  78. brawny/db/ops/blocks.py +83 -0
  79. brawny/db/ops/cache.py +93 -0
  80. brawny/db/ops/intents.py +296 -0
  81. brawny/db/ops/jobs.py +110 -0
  82. brawny/db/ops/logs.py +97 -0
  83. brawny/db/ops/nonces.py +322 -0
  84. brawny/db/postgres.py +2535 -0
  85. brawny/db/postgres_new.py +196 -0
  86. brawny/db/queries.py +584 -0
  87. brawny/db/sqlite.py +2733 -0
  88. brawny/db/sqlite_new.py +191 -0
  89. brawny/history.py +126 -0
  90. brawny/interfaces.py +136 -0
  91. brawny/invariants.py +155 -0
  92. brawny/jobs/__init__.py +26 -0
  93. brawny/jobs/base.py +287 -0
  94. brawny/jobs/discovery.py +233 -0
  95. brawny/jobs/job_validation.py +111 -0
  96. brawny/jobs/kv.py +125 -0
  97. brawny/jobs/registry.py +283 -0
  98. brawny/keystore.py +484 -0
  99. brawny/lifecycle.py +551 -0
  100. brawny/logging.py +290 -0
  101. brawny/metrics.py +594 -0
  102. brawny/model/__init__.py +53 -0
  103. brawny/model/contexts.py +319 -0
  104. brawny/model/enums.py +70 -0
  105. brawny/model/errors.py +194 -0
  106. brawny/model/events.py +93 -0
  107. brawny/model/startup.py +20 -0
  108. brawny/model/types.py +483 -0
  109. brawny/networks/__init__.py +96 -0
  110. brawny/networks/config.py +269 -0
  111. brawny/networks/manager.py +423 -0
  112. brawny/obs/__init__.py +67 -0
  113. brawny/obs/emit.py +158 -0
  114. brawny/obs/health.py +175 -0
  115. brawny/obs/heartbeat.py +133 -0
  116. brawny/reconciliation.py +108 -0
  117. brawny/scheduler/__init__.py +19 -0
  118. brawny/scheduler/poller.py +472 -0
  119. brawny/scheduler/reorg.py +632 -0
  120. brawny/scheduler/runner.py +708 -0
  121. brawny/scheduler/shutdown.py +371 -0
  122. brawny/script_tx.py +297 -0
  123. brawny/scripting.py +251 -0
  124. brawny/startup.py +76 -0
  125. brawny/telegram.py +393 -0
  126. brawny/testing.py +108 -0
  127. brawny/tx/__init__.py +41 -0
  128. brawny/tx/executor.py +1071 -0
  129. brawny/tx/fees.py +50 -0
  130. brawny/tx/intent.py +423 -0
  131. brawny/tx/monitor.py +628 -0
  132. brawny/tx/nonce.py +498 -0
  133. brawny/tx/replacement.py +456 -0
  134. brawny/tx/utils.py +26 -0
  135. brawny/utils.py +205 -0
  136. brawny/validation.py +69 -0
  137. brawny-0.1.13.dist-info/METADATA +156 -0
  138. brawny-0.1.13.dist-info/RECORD +141 -0
  139. brawny-0.1.13.dist-info/WHEEL +5 -0
  140. brawny-0.1.13.dist-info/entry_points.txt +2 -0
  141. brawny-0.1.13.dist-info/top_level.txt +1 -0
brawny/alerts/base.py ADDED
@@ -0,0 +1,152 @@
1
+ """Alert formatting helpers.
2
+
3
+ Provides utilities for formatting alert messages with explorer links
4
+ and proper escaping for Telegram MarkdownV2.
5
+ """
6
+
7
+ from __future__ import annotations
8
+
9
+
10
+ # Explorer URLs for different chains
11
+ EXPLORER_URLS = {
12
+ 1: "https://etherscan.io",
13
+ 5: "https://goerli.etherscan.io",
14
+ 11155111: "https://sepolia.etherscan.io",
15
+ 17000: "https://holesky.etherscan.io",
16
+ 137: "https://polygonscan.com",
17
+ 80001: "https://mumbai.polygonscan.com",
18
+ 80002: "https://amoy.polygonscan.com",
19
+ 42161: "https://arbiscan.io",
20
+ 421613: "https://goerli.arbiscan.io",
21
+ 421614: "https://sepolia.arbiscan.io",
22
+ 10: "https://optimistic.etherscan.io",
23
+ 420: "https://goerli-optimism.etherscan.io",
24
+ 11155420: "https://sepolia-optimism.etherscan.io",
25
+ 8453: "https://basescan.org",
26
+ 84531: "https://goerli.basescan.org",
27
+ 84532: "https://sepolia.basescan.org",
28
+ 43114: "https://snowtrace.io",
29
+ 56: "https://bscscan.com",
30
+ 250: "https://ftmscan.com",
31
+ 100: "https://gnosisscan.io",
32
+ 324: "https://era.zksync.network",
33
+ 534352: "https://scrollscan.com",
34
+ 59144: "https://lineascan.build",
35
+ 81457: "https://blastscan.io",
36
+ }
37
+
38
+
39
+ def get_explorer_url(chain_id: int) -> str:
40
+ """Get block explorer URL for chain.
41
+
42
+ Args:
43
+ chain_id: Chain ID
44
+
45
+ Returns:
46
+ Explorer base URL
47
+ """
48
+ return EXPLORER_URLS.get(chain_id, "https://etherscan.io")
49
+
50
+
51
+ def format_tx_link(tx_hash: str, chain_id: int = 1) -> str:
52
+ """Format transaction link for explorer.
53
+
54
+ Args:
55
+ tx_hash: Transaction hash
56
+ chain_id: Chain ID
57
+
58
+ Returns:
59
+ Markdown formatted link
60
+ """
61
+ explorer = get_explorer_url(chain_id)
62
+ return f"[View Transaction]({explorer}/tx/{tx_hash})"
63
+
64
+
65
+ def format_address_link(address: str, chain_id: int = 1) -> str:
66
+ """Format address link for explorer.
67
+
68
+ Args:
69
+ address: Ethereum address
70
+ chain_id: Chain ID
71
+
72
+ Returns:
73
+ Markdown formatted link
74
+ """
75
+ explorer = get_explorer_url(chain_id)
76
+ return f"[{address[:10]}...]({explorer}/address/{address})"
77
+
78
+
79
+ # MarkdownV2 special characters that need escaping
80
+ _MARKDOWN_V2_SPECIAL = r"_*[]()~`>#+=|{}.!-"
81
+
82
+
83
+ def escape_markdown_v2(text: str) -> str:
84
+ """Escape special characters for Telegram MarkdownV2.
85
+
86
+ Args:
87
+ text: Text to escape
88
+
89
+ Returns:
90
+ Escaped text safe for MarkdownV2
91
+ """
92
+ result = []
93
+ for char in text:
94
+ if char in _MARKDOWN_V2_SPECIAL:
95
+ result.append("\\")
96
+ result.append(char)
97
+ return "".join(result)
98
+
99
+
100
+ def shorten(hex_string: str, prefix: int = 6, suffix: int = 4) -> str:
101
+ """Shorten a hex string (address or hash) for display.
102
+
103
+ Args:
104
+ hex_string: Full hex string (e.g., 0x1234...abcd)
105
+ prefix: Characters to keep at start (including 0x)
106
+ suffix: Characters to keep at end
107
+
108
+ Returns:
109
+ Shortened string like "0x1234...abcd"
110
+
111
+ Example:
112
+ >>> shorten("0x1234567890abcdef1234567890abcdef12345678")
113
+ "0x1234...5678"
114
+ """
115
+ if not hex_string or len(hex_string) <= prefix + suffix + 3:
116
+ return hex_string
117
+ return f"{hex_string[:prefix]}...{hex_string[-suffix:]}"
118
+
119
+
120
+ def explorer_link(
121
+ hash_or_address: str,
122
+ chain_id: int = 1,
123
+ label: str | None = None,
124
+ ) -> str:
125
+ """Create a MarkdownV2 explorer link with emoji.
126
+
127
+ Automatically detects if input is a tx hash or address.
128
+
129
+ Args:
130
+ hash_or_address: Transaction hash or address
131
+ chain_id: Chain ID for explorer URL
132
+ label: Custom label (default: "🔗 View on Explorer")
133
+
134
+ Returns:
135
+ MarkdownV2 formatted link like "[🔗 View on Explorer](url)"
136
+
137
+ Example:
138
+ >>> explorer_link("0xabc123...")
139
+ "[🔗 View on Explorer](https://etherscan.io/tx/0xabc123...)"
140
+ """
141
+ explorer = get_explorer_url(chain_id)
142
+
143
+ # Detect type: tx hash is 66 chars, address is 42 chars
144
+ if len(hash_or_address) == 66:
145
+ path = f"tx/{hash_or_address}"
146
+ else:
147
+ path = f"address/{hash_or_address}"
148
+
149
+ url = f"{explorer}/{path}"
150
+ display = label or "🔗 View on Explorer"
151
+
152
+ return f"[{display}]({url})"
@@ -0,0 +1,271 @@
1
+ """Alert context for the Alerts extension.
2
+
3
+ AlertContext is passed to all alert hooks and provides:
4
+ - Job metadata and trigger information
5
+ - Transaction and receipt data
6
+ - Contract handles with ABI resolution
7
+ - Brownie-compatible event access via ctx.events
8
+ """
9
+
10
+ from __future__ import annotations
11
+
12
+ from dataclasses import dataclass, field
13
+ from typing import TYPE_CHECKING, Any
14
+
15
+ from brawny.alerts.base import (
16
+ shorten as _shorten,
17
+ explorer_link as _explorer_link,
18
+ get_explorer_url,
19
+ )
20
+ from brawny.alerts.contracts import ContractHandle, ContractSystem
21
+ from brawny.alerts.events import EventDict, decode_logs
22
+
23
+ if TYPE_CHECKING:
24
+ from brawny.jobs.base import Job, TxReceipt, TxInfo, BlockInfo
25
+ from brawny.model.types import Trigger
26
+
27
+ from brawny.model.errors import ErrorInfo, FailureType, FailureStage, HookType
28
+
29
+
30
+ @dataclass
31
+ class JobMetadata:
32
+ """Job metadata for alert context."""
33
+
34
+ id: str
35
+ name: str
36
+
37
+
38
+ @dataclass
39
+ class AlertContext:
40
+ """Context passed to alert hooks.
41
+
42
+ Provides access to job metadata, trigger data, transaction info,
43
+ receipt data, and contract handles.
44
+
45
+ Attributes:
46
+ job: Job metadata (id, name)
47
+ trigger: The trigger that initiated this flow
48
+ chain_id: Chain ID
49
+ hook: HookType indicating which hook is being called
50
+ tx: Transaction info (hash, nonce, gas params) - available after submit
51
+ receipt: Transaction receipt - only available in alert_confirmed
52
+ block: Block info - available in alert_confirmed
53
+ error_info: Structured error info (JSON-safe) - available in alert_failed
54
+ failure_type: Type of failure - available in alert_failed
55
+ events: Brownie-compatible decoded events (only in alert_confirmed)
56
+
57
+ Contract access:
58
+ Use Contract("0x...") from brawny.
59
+
60
+ Event access (brownie-compatible):
61
+ ctx.events["Deposit"][0] # First Deposit event
62
+ ctx.events["Deposit"]["amount"] # Field from first Deposit
63
+ "Deposit" in ctx.events # Check if event exists
64
+ """
65
+
66
+ job: JobMetadata
67
+ trigger: Trigger
68
+ chain_id: int = 1
69
+ hook: HookType | None = None
70
+ tx: TxInfo | None = None
71
+ receipt: TxReceipt | None = None
72
+ block: BlockInfo | None = None
73
+ error_info: ErrorInfo | None = None
74
+ failure_type: FailureType | None = None
75
+ failure_stage: FailureStage | None = None # Kept for backward compat
76
+ _contract_system: ContractSystem | None = None
77
+ _events: EventDict | None = None
78
+
79
+ # Backward compat: error property
80
+ @property
81
+ def error(self) -> Exception | None:
82
+ """Deprecated: use error_info instead. Returns None."""
83
+ return None
84
+
85
+ @property
86
+ def is_permanent_failure(self) -> bool:
87
+ """True if retrying won't help (simulation revert, on-chain revert, deadline)."""
88
+ return self.failure_type is not None and self.failure_type in {
89
+ FailureType.SIMULATION_REVERTED,
90
+ FailureType.TX_REVERTED,
91
+ FailureType.DEADLINE_EXPIRED,
92
+ }
93
+
94
+ @property
95
+ def is_transient_failure(self) -> bool:
96
+ """True if failure might resolve on retry (network issues)."""
97
+ return self.failure_type is not None and self.failure_type in {
98
+ FailureType.SIMULATION_NETWORK_ERROR,
99
+ FailureType.BROADCAST_FAILED,
100
+ }
101
+
102
+ @property
103
+ def error_message(self) -> str:
104
+ """Convenience helper: error message or 'unknown'."""
105
+ return self.error_info.message if self.error_info else "unknown"
106
+
107
+ @property
108
+ def events(self) -> EventDict:
109
+ """Decoded events from receipt. Brownie-compatible access.
110
+
111
+ Usage:
112
+ ctx.events["Transfer"][0] # First Transfer event
113
+ ctx.events["Transfer"]["amount"] # Field from first Transfer
114
+ len(ctx.events) # Total event count
115
+ "Transfer" in ctx.events # Check if event type exists
116
+
117
+ Returns:
118
+ EventDict with all decoded events
119
+
120
+ Raises:
121
+ ReceiptRequiredError: If accessed without a receipt
122
+ RuntimeError: If contract system not configured
123
+ """
124
+ from brawny.alerts.errors import ReceiptRequiredError
125
+
126
+ if self.receipt is None:
127
+ raise ReceiptRequiredError(
128
+ "ctx.events requires receipt. Only available in alert_confirmed."
129
+ )
130
+
131
+ if self._contract_system is None:
132
+ raise RuntimeError(
133
+ "Contract system not configured. Initialize ContractSystem for alert contexts."
134
+ )
135
+
136
+ if self._events is None:
137
+ self._events = decode_logs(
138
+ logs=self.receipt.logs,
139
+ contract_system=self._contract_system,
140
+ )
141
+
142
+ return self._events
143
+
144
+ @classmethod
145
+ def from_job(
146
+ cls,
147
+ job: Job,
148
+ trigger: Trigger,
149
+ chain_id: int = 1,
150
+ hook: HookType | None = None,
151
+ tx: TxInfo | None = None,
152
+ receipt: TxReceipt | None = None,
153
+ block: BlockInfo | None = None,
154
+ error_info: ErrorInfo | None = None,
155
+ failure_type: FailureType | None = None,
156
+ failure_stage: FailureStage | None = None,
157
+ contract_system: ContractSystem | None = None,
158
+ ) -> AlertContext:
159
+ """Create AlertContext from a Job instance.
160
+
161
+ Args:
162
+ job: The job instance
163
+ trigger: The trigger that initiated this flow
164
+ chain_id: Chain ID
165
+ hook: HookType indicating which hook is being called
166
+ tx: Transaction info (optional)
167
+ receipt: Transaction receipt (optional, required for alert_confirmed)
168
+ block: Block info (optional)
169
+ error_info: Structured error info (optional, for alert_failed)
170
+ failure_type: Type of failure (optional, for alert_failed)
171
+ failure_stage: Stage when failure occurred (optional, for alert_failed)
172
+ contract_system: Contract system for event decoding
173
+
174
+ Returns:
175
+ AlertContext instance
176
+ """
177
+ return cls(
178
+ job=JobMetadata(
179
+ id=job.job_id,
180
+ name=job.name,
181
+ ),
182
+ trigger=trigger,
183
+ chain_id=chain_id,
184
+ hook=hook,
185
+ tx=tx,
186
+ receipt=receipt,
187
+ block=block,
188
+ error_info=error_info,
189
+ failure_type=failure_type,
190
+ failure_stage=failure_stage,
191
+ _contract_system=contract_system,
192
+ )
193
+
194
+ def has_receipt(self) -> bool:
195
+ """Check if receipt is available.
196
+
197
+ Use this to conditionally access receipt-only features.
198
+ """
199
+ return self.receipt is not None
200
+
201
+ def has_error(self) -> bool:
202
+ """Check if error_info is available.
203
+
204
+ Use this to conditionally handle error information.
205
+ """
206
+ return self.error_info is not None
207
+
208
+ def format_tx_link(self, explorer_url: str | None = None) -> str:
209
+ """Format a link to the transaction on a block explorer.
210
+
211
+ Args:
212
+ explorer_url: Base explorer URL (e.g., "https://etherscan.io")
213
+ If None, returns just the tx hash
214
+
215
+ Returns:
216
+ Formatted link or tx hash
217
+ """
218
+ if self.tx is None:
219
+ return "No transaction"
220
+
221
+ tx_hash = self.tx.hash
222
+ if explorer_url:
223
+ return f"{explorer_url}/tx/{tx_hash}"
224
+ return tx_hash
225
+
226
+ def shorten(self, hex_string: str, prefix: int = 6, suffix: int = 4) -> str:
227
+ """Shorten a hex string (address or hash) for display.
228
+
229
+ Args:
230
+ hex_string: Full hex string (e.g., 0x1234...abcd)
231
+ prefix: Characters to keep at start (including 0x)
232
+ suffix: Characters to keep at end
233
+
234
+ Returns:
235
+ Shortened string like "0x1234...abcd"
236
+
237
+ Example:
238
+ ctx.shorten(ctx.receipt.transactionHash.hex())
239
+ # Returns: "0x1234...5678"
240
+ """
241
+ return _shorten(hex_string, prefix, suffix)
242
+
243
+ def explorer_link(
244
+ self,
245
+ hash_or_address: str,
246
+ label: str | None = None,
247
+ ) -> str:
248
+ """Create a Markdown explorer link with emoji.
249
+
250
+ Automatically uses the chain_id from the trigger context.
251
+ Detects if input is a tx hash or address.
252
+
253
+ Args:
254
+ hash_or_address: Transaction hash or address
255
+ label: Custom label (default: "🔗 View on Explorer")
256
+
257
+ Returns:
258
+ Markdown formatted link like "[🔗 View on Explorer](url)"
259
+
260
+ Example:
261
+ ctx.explorer_link(ctx.receipt.transactionHash.hex())
262
+ # Returns: "[🔗 View on Explorer](https://etherscan.io/tx/0x...)"
263
+ """
264
+ return _explorer_link(hash_or_address, self.chain_id, label)
265
+
266
+
267
+ # Re-export types for convenience
268
+ __all__ = [
269
+ "AlertContext",
270
+ "JobMetadata",
271
+ ]