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/telegram.py ADDED
@@ -0,0 +1,393 @@
1
+ """Telegram Bot API wrapper with strict parameter naming.
2
+
3
+ All method signatures use Telegram Bot API parameter names verbatim.
4
+ No aliases. No renaming.
5
+
6
+ See: https://core.telegram.org/bots/api
7
+
8
+ Usage in jobs:
9
+
10
+ from brawny.telegram import telegram
11
+
12
+ class MyJob(Job):
13
+ def check(self, ctx):
14
+ # Send a simple message
15
+ telegram.send_message("Something happened!", chat_id="-100...")
16
+
17
+ # Send with markdown
18
+ telegram.send_message("*Bold*", chat_id="-100...", parse_mode="Markdown")
19
+
20
+ # Disable link preview
21
+ telegram.send_message("Check https://example.com", chat_id="-100...",
22
+ disable_web_page_preview=False)
23
+
24
+ # Silent notification
25
+ telegram.send_message("Low priority", chat_id="-100...",
26
+ disable_notification=True)
27
+
28
+ Configuration:
29
+ Set in config.yaml:
30
+ telegram:
31
+ bot_token: "${TELEGRAM_BOT_TOKEN}"
32
+ chats:
33
+ ops: "-1001234567890"
34
+ dev: "-1009876543210"
35
+ default: ["ops"]
36
+ """
37
+
38
+ from __future__ import annotations
39
+
40
+ import os # Used by _LazyTelegram for TELEGRAM_BOT_TOKEN
41
+ from typing import Any
42
+
43
+ import requests
44
+
45
+ from brawny.logging import get_logger
46
+
47
+ logger = get_logger(__name__)
48
+
49
+ # Telegram API limits
50
+ MAX_MESSAGE_LENGTH = 4096
51
+ TRUNCATION_SUFFIX = "\n...[truncated]"
52
+
53
+
54
+ def _truncate_message(text: str, max_length: int = MAX_MESSAGE_LENGTH) -> str:
55
+ """Truncate message to fit Telegram's limit."""
56
+ if len(text) <= max_length:
57
+ return text
58
+ suffix_len = len(TRUNCATION_SUFFIX)
59
+ if max_length <= suffix_len:
60
+ return text[:max_length]
61
+ return text[: max_length - suffix_len] + TRUNCATION_SUFFIX
62
+
63
+
64
+ class TelegramBot:
65
+ """Telegram Bot API wrapper.
66
+
67
+ All methods use Telegram API parameter names verbatim.
68
+ No aliases or renaming.
69
+
70
+ See: https://core.telegram.org/bots/api
71
+ """
72
+
73
+ def __init__(
74
+ self,
75
+ token: str | None = None,
76
+ timeout: int = 30,
77
+ ) -> None:
78
+ """Initialize Telegram bot.
79
+
80
+ Args:
81
+ token: Bot token. Required for API calls.
82
+ timeout: Request timeout in seconds.
83
+ """
84
+ self._token = token
85
+ self._timeout = timeout
86
+
87
+ @property
88
+ def configured(self) -> bool:
89
+ """Check if bot is configured with token."""
90
+ return bool(self._token)
91
+
92
+ @property
93
+ def api_url(self) -> str:
94
+ """Base URL for Telegram API."""
95
+ return f"https://api.telegram.org/bot{self._token}"
96
+
97
+ def _request(self, method: str, **params: Any) -> dict | bool | None:
98
+ """Make a request to the Telegram API.
99
+
100
+ Args:
101
+ method: API method name (e.g., "sendMessage")
102
+ **params: Method parameters
103
+
104
+ Returns:
105
+ API response (dict or bool depending on endpoint), or None on failure
106
+ """
107
+ if not self._token:
108
+ # Silent no-op: startup already warned about missing bot_token
109
+ return None
110
+
111
+ # Extract chat_id for error logging before filtering
112
+ chat_id = params.get("chat_id")
113
+
114
+ # Filter out None values
115
+ params = {k: v for k, v in params.items() if v is not None}
116
+
117
+ try:
118
+ response = requests.post(
119
+ f"{self.api_url}/{method}",
120
+ json=params,
121
+ timeout=self._timeout,
122
+ )
123
+ response.raise_for_status()
124
+ result = response.json()
125
+
126
+ if not result.get("ok"):
127
+ logger.error(
128
+ "telegram.api_error",
129
+ method=method,
130
+ error=result.get("description"),
131
+ chat_id=chat_id,
132
+ )
133
+ return None
134
+
135
+ return result.get("result")
136
+
137
+ except requests.exceptions.RequestException as e:
138
+ logger.error(
139
+ "telegram.request_failed",
140
+ method=method,
141
+ error=str(e)[:200],
142
+ chat_id=chat_id,
143
+ )
144
+ return None
145
+
146
+ def send_message(
147
+ self,
148
+ text: str,
149
+ *,
150
+ chat_id: str | int,
151
+ parse_mode: str | None = None,
152
+ disable_web_page_preview: bool | None = None,
153
+ disable_notification: bool | None = None,
154
+ message_thread_id: int | None = None,
155
+ reply_to_message_id: int | None = None,
156
+ ) -> dict | None:
157
+ """Send a text message.
158
+
159
+ https://core.telegram.org/bots/api#sendmessage
160
+
161
+ Args:
162
+ text: Message text (up to 4096 characters, auto-truncated)
163
+ chat_id: Target chat ID (required)
164
+ parse_mode: "Markdown", "MarkdownV2", "HTML", or None
165
+ disable_web_page_preview: Disable link previews
166
+ disable_notification: Send without notification sound
167
+ message_thread_id: Thread ID for forum topics
168
+ reply_to_message_id: Message ID to reply to
169
+
170
+ Returns:
171
+ Message object from Telegram API, or None on failure
172
+ """
173
+ return self._request(
174
+ "sendMessage",
175
+ chat_id=chat_id,
176
+ text=_truncate_message(text),
177
+ parse_mode=parse_mode,
178
+ disable_web_page_preview=disable_web_page_preview,
179
+ disable_notification=disable_notification,
180
+ message_thread_id=message_thread_id,
181
+ reply_to_message_id=reply_to_message_id,
182
+ )
183
+
184
+ def send_photo(
185
+ self,
186
+ *,
187
+ chat_id: str | int,
188
+ photo: str,
189
+ caption: str | None = None,
190
+ parse_mode: str | None = None,
191
+ disable_notification: bool | None = None,
192
+ ) -> dict | None:
193
+ """Send a photo.
194
+
195
+ https://core.telegram.org/bots/api#sendphoto
196
+
197
+ Args:
198
+ chat_id: Target chat ID (required)
199
+ photo: Photo URL or file_id
200
+ caption: Optional caption
201
+ parse_mode: Caption parse mode
202
+ disable_notification: Send without notification sound
203
+
204
+ Returns:
205
+ Message object or None
206
+ """
207
+ return self._request(
208
+ "sendPhoto",
209
+ chat_id=chat_id,
210
+ photo=photo,
211
+ caption=caption,
212
+ parse_mode=parse_mode,
213
+ disable_notification=disable_notification,
214
+ )
215
+
216
+ def send_document(
217
+ self,
218
+ *,
219
+ chat_id: str | int,
220
+ document: str,
221
+ caption: str | None = None,
222
+ parse_mode: str | None = None,
223
+ disable_notification: bool | None = None,
224
+ ) -> dict | None:
225
+ """Send a document.
226
+
227
+ https://core.telegram.org/bots/api#senddocument
228
+
229
+ Args:
230
+ chat_id: Target chat ID (required)
231
+ document: Document URL or file_id
232
+ caption: Optional caption
233
+ parse_mode: Caption parse mode
234
+ disable_notification: Send without notification sound
235
+
236
+ Returns:
237
+ Message object or None
238
+ """
239
+ return self._request(
240
+ "sendDocument",
241
+ chat_id=chat_id,
242
+ document=document,
243
+ caption=caption,
244
+ parse_mode=parse_mode,
245
+ disable_notification=disable_notification,
246
+ )
247
+
248
+ def edit_message_text(
249
+ self,
250
+ text: str,
251
+ *,
252
+ chat_id: str | int,
253
+ message_id: int,
254
+ parse_mode: str | None = None,
255
+ disable_web_page_preview: bool | None = None,
256
+ ) -> dict | None:
257
+ """Edit a message's text.
258
+
259
+ https://core.telegram.org/bots/api#editmessagetext
260
+
261
+ Args:
262
+ text: New message text
263
+ chat_id: Chat containing the message
264
+ message_id: ID of message to edit
265
+ parse_mode: Text parse mode
266
+ disable_web_page_preview: Disable link previews
267
+
268
+ Returns:
269
+ Edited message object or None
270
+ """
271
+ return self._request(
272
+ "editMessageText",
273
+ chat_id=chat_id,
274
+ message_id=message_id,
275
+ text=_truncate_message(text),
276
+ parse_mode=parse_mode,
277
+ disable_web_page_preview=disable_web_page_preview,
278
+ )
279
+
280
+ def delete_message(
281
+ self,
282
+ *,
283
+ chat_id: str | int,
284
+ message_id: int,
285
+ ) -> bool:
286
+ """Delete a message.
287
+
288
+ https://core.telegram.org/bots/api#deletemessage
289
+
290
+ Args:
291
+ chat_id: Chat containing the message
292
+ message_id: ID of message to delete
293
+
294
+ Returns:
295
+ True if deleted successfully
296
+ """
297
+ result = self._request(
298
+ "deleteMessage",
299
+ chat_id=chat_id,
300
+ message_id=message_id,
301
+ )
302
+ return result is True
303
+
304
+ def pin_chat_message(
305
+ self,
306
+ *,
307
+ chat_id: str | int,
308
+ message_id: int,
309
+ disable_notification: bool | None = None,
310
+ ) -> bool:
311
+ """Pin a message in a chat.
312
+
313
+ https://core.telegram.org/bots/api#pinchatmessage
314
+
315
+ Args:
316
+ chat_id: Chat containing the message
317
+ message_id: ID of message to pin
318
+ disable_notification: Pin without notification
319
+
320
+ Returns:
321
+ True if pinned successfully
322
+ """
323
+ result = self._request(
324
+ "pinChatMessage",
325
+ chat_id=chat_id,
326
+ message_id=message_id,
327
+ disable_notification=disable_notification,
328
+ )
329
+ return result is True
330
+
331
+ def get_me(self) -> dict | None:
332
+ """Get bot information.
333
+
334
+ https://core.telegram.org/bots/api#getme
335
+
336
+ Returns:
337
+ Bot user object or None
338
+ """
339
+ return self._request("getMe")
340
+
341
+ def get_chat(self, *, chat_id: str | int) -> dict | None:
342
+ """Get chat information.
343
+
344
+ https://core.telegram.org/bots/api#getchat
345
+
346
+ Args:
347
+ chat_id: Chat to get info for
348
+
349
+ Returns:
350
+ Chat object or None
351
+ """
352
+ return self._request("getChat", chat_id=chat_id)
353
+
354
+
355
+
356
+ class _LazyTelegram:
357
+ """Lazy proxy for TelegramBot that initializes on first access.
358
+
359
+ This defers TelegramBot creation until first use, ensuring environment
360
+ variables from .env are loaded before reading TELEGRAM_BOT_TOKEN.
361
+ """
362
+
363
+ _instance: TelegramBot | None = None
364
+
365
+ def _get_instance(self) -> TelegramBot:
366
+ if self._instance is None:
367
+ self._instance = TelegramBot(token=os.environ.get("TELEGRAM_BOT_TOKEN"))
368
+ return self._instance
369
+
370
+ def __getattr__(self, name: str) -> Any:
371
+ return getattr(self._get_instance(), name)
372
+
373
+
374
+ # Global instance using environment variables
375
+ # Users can import and use directly: from brawny.telegram import telegram
376
+ telegram: TelegramBot = _LazyTelegram() # type: ignore[assignment]
377
+
378
+
379
+ def get_telegram(token: str | None = None) -> TelegramBot:
380
+ """Get a Telegram bot instance.
381
+
382
+ Use this to create a bot with custom configuration.
383
+ For the default instance, just import `telegram` directly.
384
+
385
+ Args:
386
+ token: Bot token (defaults to env var)
387
+
388
+ Returns:
389
+ TelegramBot instance
390
+ """
391
+ if token:
392
+ return TelegramBot(token=token)
393
+ return telegram._get_instance() # type: ignore[union-attr]
brawny/testing.py ADDED
@@ -0,0 +1,108 @@
1
+ """Testing utilities for jobs with implicit context.
2
+
3
+ Provides helpers to set up the implicit context for unit testing jobs
4
+ without running the full framework.
5
+
6
+ Usage:
7
+ from brawny.testing import job_context
8
+
9
+ def test_my_job():
10
+ job = MyJob()
11
+ with job_context(job, block_number=1000) as ctx:
12
+ result = job.check(ctx)
13
+ assert result is not None
14
+ """
15
+
16
+ from __future__ import annotations
17
+
18
+ from contextlib import contextmanager
19
+ from typing import TYPE_CHECKING, Any, Generator, Literal
20
+ from unittest.mock import MagicMock
21
+
22
+ from brawny._context import _job_ctx, _current_job, set_check_block, reset_check_block
23
+ from brawny.jobs.kv import InMemoryJobKVStore
24
+ from brawny.model.contexts import BlockContext, CheckContext
25
+
26
+ if TYPE_CHECKING:
27
+ from brawny.jobs.base import Job
28
+
29
+
30
+ @contextmanager
31
+ def job_context(
32
+ job: Job,
33
+ block_number: int = 1000,
34
+ chain_id: int = 1,
35
+ block_hash: str | None = None,
36
+ timestamp: int | None = None,
37
+ rpc: Any | None = None,
38
+ contract_system: Any | None = None,
39
+ kv: Any | None = None,
40
+ phase: Literal["check", "build", "alert"] = "check",
41
+ ) -> Generator[CheckContext, None, None]:
42
+ """Set up implicit context for testing job hooks.
43
+
44
+ Creates a CheckContext and sets the contextvars so that implicit
45
+ context functions (contract, trigger, tx, block) work correctly.
46
+
47
+ Args:
48
+ job: The job instance to test
49
+ block_number: Block number for the context (default 1000)
50
+ chain_id: Chain ID (default 1)
51
+ block_hash: Block hash (default generates one)
52
+ timestamp: Block timestamp (default 1700000000)
53
+ rpc: RPC manager (default MagicMock)
54
+ contract_system: Contract system for ContractFactory (default None)
55
+ kv: KV store (default InMemoryJobKVStore)
56
+ phase: Which phase to simulate. "check" pins reads to block_number,
57
+ "build"/"alert" use latest (default: "check")
58
+
59
+ Yields:
60
+ The CheckContext for additional assertions or setup
61
+
62
+ Example:
63
+ from brawny import Contract, trigger
64
+ from brawny.testing import job_context
65
+
66
+ def test_harvest_check():
67
+ job = HarvestJob()
68
+ with job_context(job, block_number=12345) as ctx:
69
+ # Implicit context is now available
70
+ result = job.check(ctx)
71
+ assert result is not None
72
+ """
73
+ from brawny.alerts.contracts import SimpleContractFactory
74
+
75
+ block = BlockContext(
76
+ number=block_number,
77
+ timestamp=timestamp or 1700000000,
78
+ hash=block_hash or ("0x" + "ab" * 32),
79
+ base_fee=0,
80
+ chain_id=chain_id,
81
+ )
82
+
83
+ # Build ContractFactory if contract_system provided
84
+ contracts = SimpleContractFactory(contract_system) if contract_system else None
85
+
86
+ ctx = CheckContext(
87
+ block=block,
88
+ kv=kv or InMemoryJobKVStore(),
89
+ job_id=job.job_id,
90
+ rpc=rpc or MagicMock(),
91
+ logger=MagicMock(),
92
+ contracts=contracts,
93
+ _db=None,
94
+ )
95
+
96
+ ctx_token = _job_ctx.set(ctx)
97
+ job_token = _current_job.set(job)
98
+
99
+ # Only pin block in check phase (matches real runner behavior)
100
+ check_block_token = set_check_block(block_number) if phase == "check" else None
101
+
102
+ try:
103
+ yield ctx
104
+ finally:
105
+ if check_block_token is not None:
106
+ reset_check_block(check_block_token)
107
+ _job_ctx.reset(ctx_token)
108
+ _current_job.reset(job_token)
brawny/tx/__init__.py ADDED
@@ -0,0 +1,41 @@
1
+ """Transaction intent, attempt, nonce management, and execution."""
2
+
3
+ from brawny.tx.executor import ExecutionOutcome, ExecutionResult, TxExecutor
4
+ from brawny.tx.intent import (
5
+ abandon_intent,
6
+ claim_intent,
7
+ create_intent,
8
+ get_or_create_intent,
9
+ get_pending_for_signer,
10
+ release_claim,
11
+ revert_to_pending,
12
+ update_status,
13
+ )
14
+ from brawny.tx.monitor import ConfirmationResult, ConfirmationStatus, TxMonitor
15
+ from brawny.tx.nonce import NonceManager
16
+ from brawny.tx.replacement import ReplacementResult, TxReplacer
17
+
18
+ __all__ = [
19
+ # Executor
20
+ "TxExecutor",
21
+ "ExecutionResult",
22
+ "ExecutionOutcome",
23
+ # Monitor
24
+ "TxMonitor",
25
+ "ConfirmationResult",
26
+ "ConfirmationStatus",
27
+ # Replacement
28
+ "TxReplacer",
29
+ "ReplacementResult",
30
+ # Nonce Manager
31
+ "NonceManager",
32
+ # Intent functions
33
+ "create_intent",
34
+ "get_or_create_intent",
35
+ "claim_intent",
36
+ "release_claim",
37
+ "update_status",
38
+ "abandon_intent",
39
+ "get_pending_for_signer",
40
+ "revert_to_pending",
41
+ ]