uni-exec-engine 0.2.0__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.
- uni_exec_engine-0.2.0.dist-info/METADATA +576 -0
- uni_exec_engine-0.2.0.dist-info/RECORD +80 -0
- uni_exec_engine-0.2.0.dist-info/WHEEL +5 -0
- uni_exec_engine-0.2.0.dist-info/licenses/LICENSE +21 -0
- uni_exec_engine-0.2.0.dist-info/top_level.txt +1 -0
- uniswap_autopilot/__init__.py +3 -0
- uniswap_autopilot/analytics/__init__.py +0 -0
- uniswap_autopilot/analytics/il_calculator.py +525 -0
- uniswap_autopilot/analytics/portfolio.py +234 -0
- uniswap_autopilot/analytics/position.py +433 -0
- uniswap_autopilot/analytics/range_suggest.py +270 -0
- uniswap_autopilot/audit.py +194 -0
- uniswap_autopilot/common/__init__.py +0 -0
- uniswap_autopilot/common/approval_cleanup.py +255 -0
- uniswap_autopilot/common/check_balance.py +64 -0
- uniswap_autopilot/common/common.py +804 -0
- uniswap_autopilot/common/deep_link.py +132 -0
- uniswap_autopilot/common/gas.py +141 -0
- uniswap_autopilot/data/auto_trade_policy.example.json +29 -0
- uniswap_autopilot/data/chains.json +135 -0
- uniswap_autopilot/data/common-token-addresses.json +777 -0
- uniswap_autopilot/execute/__init__.py +0 -0
- uniswap_autopilot/execute/_internal/__init__.py +5 -0
- uniswap_autopilot/execute/_internal/constants.py +15 -0
- uniswap_autopilot/execute/_internal/preflight.py +150 -0
- uniswap_autopilot/execute/_internal/pure_signer.py +182 -0
- uniswap_autopilot/execute/_internal/rpc.py +462 -0
- uniswap_autopilot/execute/_internal/signer.py +298 -0
- uniswap_autopilot/execute/_internal/submit.py +73 -0
- uniswap_autopilot/execute/_internal/tx.py +370 -0
- uniswap_autopilot/execute/broadcast.py +380 -0
- uniswap_autopilot/execute/detect.py +52 -0
- uniswap_autopilot/execute/telegram_confirm.py +272 -0
- uniswap_autopilot/lp/compare_pools.py +338 -0
- uniswap_autopilot/lp/v2/__init__.py +0 -0
- uniswap_autopilot/lp/v2/approve.py +100 -0
- uniswap_autopilot/lp/v2/build_tx.py +226 -0
- uniswap_autopilot/lp/v2/flow.py +204 -0
- uniswap_autopilot/lp/v2/pair.py +135 -0
- uniswap_autopilot/lp/v2/positions.py +177 -0
- uniswap_autopilot/lp/v3/__init__.py +0 -0
- uniswap_autopilot/lp/v3/approve.py +109 -0
- uniswap_autopilot/lp/v3/auto_rebalance.py +282 -0
- uniswap_autopilot/lp/v3/build_tx.py +465 -0
- uniswap_autopilot/lp/v3/compound.py +258 -0
- uniswap_autopilot/lp/v3/flow.py +358 -0
- uniswap_autopilot/lp/v3/pool.py +175 -0
- uniswap_autopilot/lp/v3/position.py +112 -0
- uniswap_autopilot/lp/v3/tick.py +75 -0
- uniswap_autopilot/lp/v4/__init__.py +0 -0
- uniswap_autopilot/lp/v4/approve.py +105 -0
- uniswap_autopilot/lp/v4/build_tx.py +669 -0
- uniswap_autopilot/lp/v4/flow.py +368 -0
- uniswap_autopilot/lp/v4/pool.py +174 -0
- uniswap_autopilot/lp/v4/position.py +185 -0
- uniswap_autopilot/policy.py +371 -0
- uniswap_autopilot/price_feed.py +100 -0
- uniswap_autopilot/py.typed +0 -0
- uniswap_autopilot/search/__init__.py +0 -0
- uniswap_autopilot/search/risk.py +200 -0
- uniswap_autopilot/search/search.py +580 -0
- uniswap_autopilot/state_machine.py +315 -0
- uniswap_autopilot/swap/__init__.py +1 -0
- uniswap_autopilot/swap/deep_link.py +69 -0
- uniswap_autopilot/swap/extensions/__init__.py +2 -0
- uniswap_autopilot/swap/extensions/bridge.py +197 -0
- uniswap_autopilot/swap/extensions/limit_order.py +272 -0
- uniswap_autopilot/swap/extensions/slippage.py +123 -0
- uniswap_autopilot/swap/flow.py +693 -0
- uniswap_autopilot/swap/flow_core/__init__.py +2 -0
- uniswap_autopilot/swap/flow_core/artifacts.py +11 -0
- uniswap_autopilot/swap/flow_core/broadcast.py +50 -0
- uniswap_autopilot/swap/flow_core/diagnostics.py +216 -0
- uniswap_autopilot/swap/flow_core/paper.py +113 -0
- uniswap_autopilot/swap/flow_core/policy.py +142 -0
- uniswap_autopilot/swap/links/__init__.py +2 -0
- uniswap_autopilot/swap/links/deep_link.py +69 -0
- uniswap_autopilot/swap/trading_api/permit.py +41 -0
- uniswap_autopilot/swap/trading_api/quote.py +282 -0
- uniswap_autopilot/swap/trading_api/swap.py +248 -0
|
@@ -0,0 +1,298 @@
|
|
|
1
|
+
"""Transaction signing via eth-account (pure Python).
|
|
2
|
+
|
|
3
|
+
Private key is read from an environment variable — never stored on disk.
|
|
4
|
+
Requires the ``[signer]`` extra::
|
|
5
|
+
|
|
6
|
+
pip install uniswap-autopilot[signer]
|
|
7
|
+
export EXECUTOR_PRIVATE_KEY=0x...
|
|
8
|
+
"""
|
|
9
|
+
|
|
10
|
+
from __future__ import annotations
|
|
11
|
+
|
|
12
|
+
import argparse
|
|
13
|
+
import os
|
|
14
|
+
from typing import Any
|
|
15
|
+
|
|
16
|
+
from uniswap_autopilot.common.common import load_local_env, resolve_wallet_address
|
|
17
|
+
from uniswap_autopilot.execute._internal.constants import (
|
|
18
|
+
HOT_WALLET_PRIVATE_KEY_ENV_NAME,
|
|
19
|
+
)
|
|
20
|
+
|
|
21
|
+
# ── Pure Python signer (optional) ──────────────────────────────────────────
|
|
22
|
+
|
|
23
|
+
try:
|
|
24
|
+
from uniswap_autopilot.execute._internal.pure_signer import (
|
|
25
|
+
is_available as _pure_signer_available,
|
|
26
|
+
)
|
|
27
|
+
except ImportError:
|
|
28
|
+
|
|
29
|
+
def _pure_signer_available() -> bool:
|
|
30
|
+
return False
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
# ── Namespace / arg helpers ────────────────────────────────────────────────
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
def has_direct_signer(args: argparse.Namespace) -> bool:
|
|
37
|
+
return bool(getattr(args, "private_key_env", None))
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
def build_signer_namespace(
|
|
41
|
+
*,
|
|
42
|
+
private_key_env: str | None = None,
|
|
43
|
+
wallet_source: str | None = None,
|
|
44
|
+
) -> argparse.Namespace:
|
|
45
|
+
return argparse.Namespace(
|
|
46
|
+
private_key_env=private_key_env,
|
|
47
|
+
wallet_source=wallet_source,
|
|
48
|
+
)
|
|
49
|
+
|
|
50
|
+
|
|
51
|
+
# ── Wallet resolution ─────────────────────────────────────────────────────
|
|
52
|
+
|
|
53
|
+
|
|
54
|
+
def resolve_wallet(args: argparse.Namespace | None) -> str | None:
|
|
55
|
+
"""Resolve wallet address from args or environment."""
|
|
56
|
+
if args is not None and getattr(args, "private_key_env", None):
|
|
57
|
+
from uniswap_autopilot.execute._internal.pure_signer import get_address
|
|
58
|
+
|
|
59
|
+
addr = get_address(args.private_key_env)
|
|
60
|
+
if addr:
|
|
61
|
+
return addr
|
|
62
|
+
return resolve_wallet_address(args)
|
|
63
|
+
|
|
64
|
+
|
|
65
|
+
# ── Backend detection ──────────────────────────────────────────────────────
|
|
66
|
+
|
|
67
|
+
|
|
68
|
+
def detect_hot_wallet_backend() -> dict[str, Any]:
|
|
69
|
+
"""Detect if pure-signer (eth-account) is available."""
|
|
70
|
+
load_local_env()
|
|
71
|
+
private_key_env = os.environ.get(HOT_WALLET_PRIVATE_KEY_ENV_NAME, "").strip() or None
|
|
72
|
+
wallet = resolve_wallet_address(None, preference="hot")
|
|
73
|
+
|
|
74
|
+
result: dict[str, Any] = {
|
|
75
|
+
"backend": "pure-python",
|
|
76
|
+
"configured": wallet is not None and private_key_env is not None,
|
|
77
|
+
"available": False,
|
|
78
|
+
"wallet": wallet,
|
|
79
|
+
"walletFound": wallet is not None,
|
|
80
|
+
"mode": None,
|
|
81
|
+
"reason": None,
|
|
82
|
+
"signerArgs": None,
|
|
83
|
+
}
|
|
84
|
+
if not wallet:
|
|
85
|
+
result["reason"] = "WALLET_ADDRESS is not configured"
|
|
86
|
+
return result
|
|
87
|
+
if not private_key_env:
|
|
88
|
+
result["reason"] = "Private key env var not configured"
|
|
89
|
+
return result
|
|
90
|
+
if not os.environ.get(private_key_env, "").strip():
|
|
91
|
+
result["reason"] = f"{private_key_env} is not set"
|
|
92
|
+
return result
|
|
93
|
+
if not _pure_signer_available():
|
|
94
|
+
result["reason"] = "eth-account not installed (pip install uniswap-autopilot[signer])"
|
|
95
|
+
return result
|
|
96
|
+
|
|
97
|
+
result["mode"] = "private-key-env"
|
|
98
|
+
result["available"] = True
|
|
99
|
+
result["signerArgs"] = build_signer_namespace(
|
|
100
|
+
private_key_env=private_key_env,
|
|
101
|
+
wallet_source="hot",
|
|
102
|
+
)
|
|
103
|
+
return result
|
|
104
|
+
|
|
105
|
+
|
|
106
|
+
def auto_select_signer_args(args: argparse.Namespace | None = None) -> argparse.Namespace | None:
|
|
107
|
+
"""Auto-select signer args from environment."""
|
|
108
|
+
load_local_env()
|
|
109
|
+
if args is not None:
|
|
110
|
+
if has_direct_signer(args):
|
|
111
|
+
return args
|
|
112
|
+
|
|
113
|
+
# Try hot wallet backend
|
|
114
|
+
hot = detect_hot_wallet_backend()
|
|
115
|
+
if hot["available"]:
|
|
116
|
+
return hot["signerArgs"]
|
|
117
|
+
|
|
118
|
+
# Try secure wallet
|
|
119
|
+
secure_wallet = resolve_wallet_address(None, preference="secure")
|
|
120
|
+
if secure_wallet:
|
|
121
|
+
pk_env = os.environ.get(HOT_WALLET_PRIVATE_KEY_ENV_NAME, "").strip() or None
|
|
122
|
+
if pk_env and os.environ.get(pk_env, "").strip():
|
|
123
|
+
return build_signer_namespace(
|
|
124
|
+
private_key_env=pk_env,
|
|
125
|
+
wallet_source="secure",
|
|
126
|
+
)
|
|
127
|
+
|
|
128
|
+
return None
|
|
129
|
+
|
|
130
|
+
|
|
131
|
+
def ensure_signer_backend(args: argparse.Namespace | None, action_name: str = "broadcast") -> str:
|
|
132
|
+
"""Ensure a signer is available. Returns 'pure-python'."""
|
|
133
|
+
if args is not None and has_direct_signer(args):
|
|
134
|
+
if _pure_signer_available():
|
|
135
|
+
return "pure-python"
|
|
136
|
+
raise RuntimeError(
|
|
137
|
+
f"Cannot {action_name}: eth-account is not installed. Install with: pip install uniswap-autopilot[signer]"
|
|
138
|
+
)
|
|
139
|
+
|
|
140
|
+
# Try auto-select
|
|
141
|
+
selected = auto_select_signer_args(args)
|
|
142
|
+
if selected is not None:
|
|
143
|
+
if _pure_signer_available():
|
|
144
|
+
return "pure-python"
|
|
145
|
+
raise RuntimeError(
|
|
146
|
+
f"Cannot {action_name}: eth-account is not installed. Install with: pip install uniswap-autopilot[signer]"
|
|
147
|
+
)
|
|
148
|
+
|
|
149
|
+
raise ValueError(
|
|
150
|
+
f"Cannot {action_name}: no signer configured. "
|
|
151
|
+
"Set EXECUTOR_PRIVATE_KEY env var and install: pip install uniswap-autopilot[signer]"
|
|
152
|
+
)
|
|
153
|
+
|
|
154
|
+
|
|
155
|
+
# ── Argument parsing ──────────────────────────────────────────────────────
|
|
156
|
+
|
|
157
|
+
|
|
158
|
+
def add_signer_arguments(parser: argparse.ArgumentParser) -> None:
|
|
159
|
+
"""Add signer-related arguments to an argparse parser."""
|
|
160
|
+
parser.add_argument(
|
|
161
|
+
"--private-key-env",
|
|
162
|
+
default=HOT_WALLET_PRIVATE_KEY_ENV_NAME,
|
|
163
|
+
help="Environment variable holding the hex private key (default: %(default)s)",
|
|
164
|
+
)
|
|
165
|
+
|
|
166
|
+
|
|
167
|
+
# ── Signing API ────────────────────────────────────────────────────────────
|
|
168
|
+
|
|
169
|
+
|
|
170
|
+
def sign_typed_data_with_backend(
|
|
171
|
+
typed_data_file: str,
|
|
172
|
+
typed_data: dict[str, Any],
|
|
173
|
+
tx: dict[str, Any],
|
|
174
|
+
signer_args_source: argparse.Namespace,
|
|
175
|
+
) -> dict[str, Any]:
|
|
176
|
+
"""Sign EIP-712 typed data using pure Python signer."""
|
|
177
|
+
pk_env = getattr(signer_args_source, "private_key_env", None)
|
|
178
|
+
if not pk_env:
|
|
179
|
+
raise RuntimeError("private_key_env not set on signer args")
|
|
180
|
+
|
|
181
|
+
from uniswap_autopilot.execute._internal.pure_signer import sign_typed_data
|
|
182
|
+
|
|
183
|
+
signature = sign_typed_data(typed_data, private_key_env=pk_env)
|
|
184
|
+
return {
|
|
185
|
+
"signature": signature,
|
|
186
|
+
"signCommandPreview": f"pure_signer.sign_typed_data(env={pk_env})",
|
|
187
|
+
"signerBackend": "pure-python",
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
|
|
191
|
+
def sign_and_broadcast(
|
|
192
|
+
tx: dict[str, Any],
|
|
193
|
+
rpc_url: str,
|
|
194
|
+
signer_args_source: argparse.Namespace,
|
|
195
|
+
) -> dict[str, Any]:
|
|
196
|
+
"""Sign a transaction and broadcast via eth_sendRawTransaction.
|
|
197
|
+
|
|
198
|
+
Preflight gates (before signing):
|
|
199
|
+
1. Wallet must have enough native balance for gas.
|
|
200
|
+
2. Transaction must pass eth_estimateGas (dry-run simulation).
|
|
201
|
+
"""
|
|
202
|
+
pk_env = getattr(signer_args_source, "private_key_env", None)
|
|
203
|
+
if not pk_env:
|
|
204
|
+
raise RuntimeError("private_key_env not set on signer args")
|
|
205
|
+
|
|
206
|
+
from uniswap_autopilot.audit import EVENT_ERROR, log_event
|
|
207
|
+
from uniswap_autopilot.execute._internal.pure_signer import (
|
|
208
|
+
get_address,
|
|
209
|
+
sign_transaction,
|
|
210
|
+
)
|
|
211
|
+
from uniswap_autopilot.execute._internal.rpc import (
|
|
212
|
+
_json_rpc,
|
|
213
|
+
estimate_transaction_gas,
|
|
214
|
+
query_gas_price,
|
|
215
|
+
query_native_balance,
|
|
216
|
+
)
|
|
217
|
+
|
|
218
|
+
# Resolve wallet address
|
|
219
|
+
wallet = tx.get("from") or get_address(private_key_env=pk_env)
|
|
220
|
+
if not wallet:
|
|
221
|
+
raise RuntimeError("Cannot determine wallet address for preflight checks")
|
|
222
|
+
|
|
223
|
+
run_id = os.environ.get("STAGEFORGE_RUN_ID") or os.environ.get("RUN_ID")
|
|
224
|
+
chain_id = tx.get("chainId")
|
|
225
|
+
|
|
226
|
+
# Gate 1: native balance must cover gas
|
|
227
|
+
balance = query_native_balance(wallet, rpc_url)
|
|
228
|
+
if balance == 0:
|
|
229
|
+
log_event(
|
|
230
|
+
event=EVENT_ERROR,
|
|
231
|
+
chain=str(chain_id) if chain_id else None,
|
|
232
|
+
wallet=wallet,
|
|
233
|
+
run_id=run_id,
|
|
234
|
+
error_code="no_gas",
|
|
235
|
+
details={"native_balance": 0},
|
|
236
|
+
)
|
|
237
|
+
raise RuntimeError(f"Wallet {wallet} has zero native balance — cannot pay for gas.")
|
|
238
|
+
|
|
239
|
+
# Gate 2: estimateGas simulation
|
|
240
|
+
try:
|
|
241
|
+
estimated = estimate_transaction_gas(tx, rpc_url)
|
|
242
|
+
except Exception as exc:
|
|
243
|
+
reason = str(getattr(exc, "args", [str(exc)])[0])
|
|
244
|
+
log_event(
|
|
245
|
+
event=EVENT_ERROR,
|
|
246
|
+
chain=str(chain_id) if chain_id else None,
|
|
247
|
+
wallet=wallet,
|
|
248
|
+
run_id=run_id,
|
|
249
|
+
error_code="simulation_failed",
|
|
250
|
+
details={"to": tx.get("to"), "value": str(tx.get("value", 0)), "revert": reason},
|
|
251
|
+
)
|
|
252
|
+
raise RuntimeError(f"Gas estimation failed (tx would revert): {reason}") from exc
|
|
253
|
+
|
|
254
|
+
# Optional: warn if balance < estimated * gas_price
|
|
255
|
+
try:
|
|
256
|
+
gas_price = query_gas_price(rpc_url)
|
|
257
|
+
if balance < estimated * gas_price:
|
|
258
|
+
log_event(
|
|
259
|
+
event=EVENT_ERROR,
|
|
260
|
+
chain=str(chain_id) if chain_id else None,
|
|
261
|
+
wallet=wallet,
|
|
262
|
+
run_id=run_id,
|
|
263
|
+
error_code="insufficient_gas",
|
|
264
|
+
details={
|
|
265
|
+
"native_balance": balance,
|
|
266
|
+
"estimated_gas": estimated,
|
|
267
|
+
"gas_price": gas_price,
|
|
268
|
+
"required": estimated * gas_price,
|
|
269
|
+
},
|
|
270
|
+
)
|
|
271
|
+
raise RuntimeError(
|
|
272
|
+
f"Wallet {wallet} balance ({balance}) insufficient for gas (estimate: {estimated} * price: {gas_price})"
|
|
273
|
+
)
|
|
274
|
+
except RuntimeError:
|
|
275
|
+
raise
|
|
276
|
+
except Exception:
|
|
277
|
+
pass # gas-price check is best-effort
|
|
278
|
+
|
|
279
|
+
signed_hex = sign_transaction(tx, private_key_env=pk_env, chain_id=chain_id)
|
|
280
|
+
tx_hash = _json_rpc("eth_sendRawTransaction", [signed_hex], rpc_url)
|
|
281
|
+
|
|
282
|
+
# Wait briefly and get receipt
|
|
283
|
+
import time
|
|
284
|
+
|
|
285
|
+
time.sleep(1)
|
|
286
|
+
receipt = _json_rpc("eth_getTransactionReceipt", [tx_hash], rpc_url)
|
|
287
|
+
|
|
288
|
+
if receipt is None:
|
|
289
|
+
broadcast_result = {"transactionHash": tx_hash, "status": "pending"}
|
|
290
|
+
else:
|
|
291
|
+
broadcast_result = receipt
|
|
292
|
+
|
|
293
|
+
return {
|
|
294
|
+
"commandPreview": f"pure_signer.sign_transaction + eth_sendRawTransaction (env={pk_env})",
|
|
295
|
+
"rpcUrl": rpc_url,
|
|
296
|
+
"broadcastResult": broadcast_result,
|
|
297
|
+
"signerBackend": "pure-python",
|
|
298
|
+
}
|
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
"""Transaction broadcast — pure Python signing + eth_sendRawTransaction.
|
|
2
|
+
|
|
3
|
+
Requires ``pip install uniswap-autopilot[signer]`` for the ``eth-account`` library.
|
|
4
|
+
"""
|
|
5
|
+
|
|
6
|
+
from __future__ import annotations
|
|
7
|
+
|
|
8
|
+
import argparse
|
|
9
|
+
from typing import Any
|
|
10
|
+
|
|
11
|
+
from uniswap_autopilot.execute._internal.rpc import resolve_rpc_url
|
|
12
|
+
from uniswap_autopilot.execute._internal.signer import (
|
|
13
|
+
ensure_signer_backend,
|
|
14
|
+
sign_and_broadcast,
|
|
15
|
+
)
|
|
16
|
+
from uniswap_autopilot.execute._internal.tx import build_confirmation_phrase
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
def build_broadcast_package(
|
|
20
|
+
tx: dict[str, Any],
|
|
21
|
+
explicit_rpc_url: str | None,
|
|
22
|
+
confirm: str | None,
|
|
23
|
+
signer_args_source: argparse.Namespace,
|
|
24
|
+
) -> dict[str, Any]:
|
|
25
|
+
"""Validate confirmation phrase and resolve RPC URL."""
|
|
26
|
+
expected_confirmation = build_confirmation_phrase(tx)
|
|
27
|
+
if confirm != expected_confirmation:
|
|
28
|
+
raise ValueError(f"--confirm must exactly equal: {expected_confirmation}")
|
|
29
|
+
|
|
30
|
+
rpc_url, rpc_candidates = resolve_rpc_url(explicit_rpc_url, tx["chainId"])
|
|
31
|
+
if not rpc_url:
|
|
32
|
+
raise RuntimeError(
|
|
33
|
+
f"RPC URL is not configured; set one of {', '.join(rpc_candidates)} or pass --rpc-url"
|
|
34
|
+
)
|
|
35
|
+
return {
|
|
36
|
+
"expectedConfirmation": expected_confirmation,
|
|
37
|
+
"rpcUrl": rpc_url,
|
|
38
|
+
"signerBackend": "pure-python",
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
def broadcast_with_backend(
|
|
43
|
+
tx: dict[str, Any],
|
|
44
|
+
explicit_rpc_url: str | None,
|
|
45
|
+
confirm: str | None,
|
|
46
|
+
signer_args_source: argparse.Namespace,
|
|
47
|
+
**kwargs: Any,
|
|
48
|
+
) -> dict[str, Any]:
|
|
49
|
+
"""Sign and broadcast a transaction.
|
|
50
|
+
|
|
51
|
+
Returns dict with broadcastResult containing the transaction receipt.
|
|
52
|
+
"""
|
|
53
|
+
ensure_signer_backend(signer_args_source, action_name="broadcast")
|
|
54
|
+
package = build_broadcast_package(
|
|
55
|
+
tx=tx,
|
|
56
|
+
explicit_rpc_url=explicit_rpc_url,
|
|
57
|
+
confirm=confirm,
|
|
58
|
+
signer_args_source=signer_args_source,
|
|
59
|
+
)
|
|
60
|
+
return sign_and_broadcast(
|
|
61
|
+
tx=tx,
|
|
62
|
+
rpc_url=package["rpcUrl"],
|
|
63
|
+
signer_args_source=signer_args_source,
|
|
64
|
+
)
|
|
65
|
+
|
|
66
|
+
|
|
67
|
+
def extract_transaction_hash(broadcast_result: dict[str, Any]) -> str:
|
|
68
|
+
"""Extract transaction hash from broadcast result."""
|
|
69
|
+
for field in ("transactionHash", "hash"):
|
|
70
|
+
value = broadcast_result.get(field)
|
|
71
|
+
if isinstance(value, str) and value.startswith("0x"):
|
|
72
|
+
return value
|
|
73
|
+
raise RuntimeError("broadcast result does not contain transactionHash")
|