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
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})"
|
brawny/alerts/context.py
ADDED
|
@@ -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
|
+
]
|