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,380 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
from __future__ import annotations
|
|
3
|
+
|
|
4
|
+
import argparse
|
|
5
|
+
import json
|
|
6
|
+
import os
|
|
7
|
+
import sys
|
|
8
|
+
import uuid
|
|
9
|
+
from pathlib import Path
|
|
10
|
+
from typing import Any
|
|
11
|
+
|
|
12
|
+
from uniswap_autopilot import policy as _policy
|
|
13
|
+
from uniswap_autopilot import state_machine
|
|
14
|
+
from uniswap_autopilot.audit import (
|
|
15
|
+
EVENT_BROADCAST,
|
|
16
|
+
EVENT_CONFIRM,
|
|
17
|
+
EVENT_ERROR,
|
|
18
|
+
EVENT_PREFLIGHT,
|
|
19
|
+
log_event,
|
|
20
|
+
)
|
|
21
|
+
from uniswap_autopilot.common.common import dump_json, load_local_env
|
|
22
|
+
from uniswap_autopilot.execute._internal.constants import (
|
|
23
|
+
APPROVE_SELECTOR,
|
|
24
|
+
CHAIN_BY_ID,
|
|
25
|
+
DEFAULT_PRIVATE_KEY_ENV,
|
|
26
|
+
GLOBAL_RPC_ENV_CANDIDATES,
|
|
27
|
+
HEX_DATA_RE,
|
|
28
|
+
HOT_WALLET_ACCOUNT_ENV_NAME,
|
|
29
|
+
HOT_WALLET_KEYSTORE_ENV_NAME,
|
|
30
|
+
HOT_WALLET_PASSWORD_FILE_ENV_NAME,
|
|
31
|
+
HOT_WALLET_PRIVATE_KEY_ENV_NAME,
|
|
32
|
+
)
|
|
33
|
+
from uniswap_autopilot.execute._internal.preflight import build_preflight_report
|
|
34
|
+
from uniswap_autopilot.execute._internal.rpc import (
|
|
35
|
+
estimate_transaction_gas,
|
|
36
|
+
execute_cast_receipt,
|
|
37
|
+
parse_cast_int_output,
|
|
38
|
+
query_erc20_allowance,
|
|
39
|
+
query_erc20_balance,
|
|
40
|
+
query_gas_price,
|
|
41
|
+
query_native_balance,
|
|
42
|
+
receipt_succeeded,
|
|
43
|
+
resolve_rpc_url,
|
|
44
|
+
rpc_env_candidates,
|
|
45
|
+
run_cast_text,
|
|
46
|
+
)
|
|
47
|
+
from uniswap_autopilot.execute._internal.signer import (
|
|
48
|
+
add_signer_arguments,
|
|
49
|
+
auto_select_signer_args,
|
|
50
|
+
build_signer_namespace,
|
|
51
|
+
detect_hot_wallet_backend,
|
|
52
|
+
ensure_signer_backend,
|
|
53
|
+
has_direct_signer,
|
|
54
|
+
sign_typed_data_with_backend,
|
|
55
|
+
)
|
|
56
|
+
from uniswap_autopilot.execute._internal.submit import (
|
|
57
|
+
broadcast_with_backend,
|
|
58
|
+
build_broadcast_package,
|
|
59
|
+
extract_transaction_hash,
|
|
60
|
+
)
|
|
61
|
+
from uniswap_autopilot.execute._internal.tx import (
|
|
62
|
+
base_units_to_human,
|
|
63
|
+
build_confirmation_phrase,
|
|
64
|
+
build_execute_preview,
|
|
65
|
+
decode_erc20_approve_call,
|
|
66
|
+
load_approval_transaction,
|
|
67
|
+
load_json,
|
|
68
|
+
load_swap_transaction,
|
|
69
|
+
lookup_token_decimals,
|
|
70
|
+
lookup_token_symbol,
|
|
71
|
+
normalize_transaction,
|
|
72
|
+
optional_address,
|
|
73
|
+
parse_intish,
|
|
74
|
+
summarize_transaction,
|
|
75
|
+
validate_hex_data,
|
|
76
|
+
)
|
|
77
|
+
|
|
78
|
+
__all__ = [
|
|
79
|
+
# audit
|
|
80
|
+
"EVENT_BROADCAST", "EVENT_CONFIRM", "EVENT_ERROR", "EVENT_PREFLIGHT", "log_event",
|
|
81
|
+
# common
|
|
82
|
+
"dump_json", "load_local_env",
|
|
83
|
+
# constants
|
|
84
|
+
"APPROVE_SELECTOR", "CHAIN_BY_ID", "DEFAULT_PRIVATE_KEY_ENV",
|
|
85
|
+
"GLOBAL_RPC_ENV_CANDIDATES", "HEX_DATA_RE",
|
|
86
|
+
"HOT_WALLET_ACCOUNT_ENV_NAME", "HOT_WALLET_KEYSTORE_ENV_NAME",
|
|
87
|
+
"HOT_WALLET_PASSWORD_FILE_ENV_NAME", "HOT_WALLET_PRIVATE_KEY_ENV_NAME",
|
|
88
|
+
# preflight
|
|
89
|
+
"build_preflight_report",
|
|
90
|
+
# rpc
|
|
91
|
+
"estimate_transaction_gas", "execute_cast_receipt", "parse_cast_int_output",
|
|
92
|
+
"query_erc20_allowance", "query_erc20_balance", "query_gas_price",
|
|
93
|
+
"query_native_balance", "receipt_succeeded", "resolve_rpc_url",
|
|
94
|
+
"rpc_env_candidates", "run_cast_text",
|
|
95
|
+
# signer
|
|
96
|
+
"add_signer_arguments", "auto_select_signer_args", "build_signer_namespace",
|
|
97
|
+
"detect_hot_wallet_backend", "ensure_signer_backend", "has_direct_signer",
|
|
98
|
+
"sign_typed_data_with_backend",
|
|
99
|
+
# submit
|
|
100
|
+
"broadcast_with_backend", "build_broadcast_package", "extract_transaction_hash",
|
|
101
|
+
# tx
|
|
102
|
+
"base_units_to_human", "build_confirmation_phrase", "build_execute_preview",
|
|
103
|
+
"decode_erc20_approve_call", "load_approval_transaction", "load_json",
|
|
104
|
+
"load_swap_transaction", "lookup_token_decimals", "lookup_token_symbol",
|
|
105
|
+
"normalize_transaction", "optional_address", "parse_intish",
|
|
106
|
+
"summarize_transaction", "validate_hex_data",
|
|
107
|
+
# entry point
|
|
108
|
+
"main",
|
|
109
|
+
]
|
|
110
|
+
|
|
111
|
+
|
|
112
|
+
def main() -> None:
|
|
113
|
+
parser = argparse.ArgumentParser(description="对 approval 或 swap 交易做安全广播")
|
|
114
|
+
source_group = parser.add_mutually_exclusive_group(required=True)
|
|
115
|
+
source_group.add_argument("--swap-file", help="swap_dry_run.py 的输出文件")
|
|
116
|
+
source_group.add_argument(
|
|
117
|
+
"--approval-file", help="trading_api_quote.py 的 quote 输出文件;会读取 approvalCheck.approval"
|
|
118
|
+
)
|
|
119
|
+
source_group.add_argument("--lp-file", help="LP 脚本(build_lp_tx.py / run_lp_flow.py)的输出文件")
|
|
120
|
+
parser.add_argument("--rpc-url", help="显式指定 RPC URL;否则尝试从环境变量解析")
|
|
121
|
+
parser.add_argument("--broadcast", action="store_true", help="真的调用 cast send 广播交易")
|
|
122
|
+
parser.add_argument("--confirm", help="必须与脚本给出的 confirmation 完全一致才允许广播")
|
|
123
|
+
parser.add_argument(
|
|
124
|
+
"--telegram-confirm", action="store_true", help="下单前推送 Telegram inline button 等待用户确认"
|
|
125
|
+
)
|
|
126
|
+
parser.add_argument("--receipt-confirmations", type=int, default=1, help="广播后查询 receipt 的确认数,默认 1")
|
|
127
|
+
add_signer_arguments(parser)
|
|
128
|
+
parser.add_argument("--output", help="把完整 JSON 输出写入文件")
|
|
129
|
+
args = parser.parse_args()
|
|
130
|
+
|
|
131
|
+
run_id: str | None = None
|
|
132
|
+
try:
|
|
133
|
+
load_local_env()
|
|
134
|
+
|
|
135
|
+
if args.swap_file:
|
|
136
|
+
tx = load_swap_transaction(args.swap_file)
|
|
137
|
+
elif args.approval_file:
|
|
138
|
+
tx = load_approval_transaction(args.approval_file)
|
|
139
|
+
elif args.lp_file:
|
|
140
|
+
blob = json.loads(Path(args.lp_file).read_text(encoding="utf-8"))
|
|
141
|
+
lp_tx = (
|
|
142
|
+
blob.get("transaction")
|
|
143
|
+
or blob.get("mint", {}).get("transaction")
|
|
144
|
+
or blob.get("increase", {}).get("transaction")
|
|
145
|
+
or blob.get("decrease", {}).get("transaction")
|
|
146
|
+
or blob.get("collect", {}).get("transaction")
|
|
147
|
+
)
|
|
148
|
+
if not lp_tx:
|
|
149
|
+
raise ValueError("LP file must contain 'transaction' or a sub-action with 'transaction'")
|
|
150
|
+
tx = normalize_transaction(lp_tx, tx_kind="lp", source_file=str(args.lp_file), source_kind="lp-file")
|
|
151
|
+
else:
|
|
152
|
+
raise ValueError("one of --swap-file, --approval-file, --lp-file is required")
|
|
153
|
+
|
|
154
|
+
preview = build_execute_preview(tx, explicit_rpc_url=args.rpc_url)
|
|
155
|
+
summary = preview["summary"]
|
|
156
|
+
rpc_url = summary["rpcUrlResolved"]
|
|
157
|
+
_rpc_candidates = summary["rpcEnvCandidates"]
|
|
158
|
+
|
|
159
|
+
response: dict[str, Any] = {
|
|
160
|
+
"action": "execute_transaction",
|
|
161
|
+
"summary": summary,
|
|
162
|
+
"broadcastRequested": args.broadcast,
|
|
163
|
+
"commandPreview": preview["commandPreview"],
|
|
164
|
+
}
|
|
165
|
+
response["preflight"] = build_preflight_report(tx, explicit_rpc_url=args.rpc_url)
|
|
166
|
+
|
|
167
|
+
# Telegram confirmation if requested or exceeds threshold
|
|
168
|
+
THRESHOLD_USD = float(os.environ.get("TRADE_CONFIRM_THRESHOLD_USD", "0"))
|
|
169
|
+
|
|
170
|
+
# Calculate trade value in USD
|
|
171
|
+
from uniswap_autopilot.execute._internal.tx import estimate_trade_value_usd
|
|
172
|
+
|
|
173
|
+
estimate_data = {
|
|
174
|
+
**summary,
|
|
175
|
+
"tokenIn": tx.get("inputTokenSymbol", ""),
|
|
176
|
+
"tokenOut": tx.get("outputTokenSymbol", ""),
|
|
177
|
+
"amountIn": tx.get("inputAmount", "0"),
|
|
178
|
+
"amountOut": tx.get("outputAmount", "0"),
|
|
179
|
+
"inputToken": tx.get("inputToken"),
|
|
180
|
+
"outputToken": tx.get("outputToken"),
|
|
181
|
+
"chain": (tx.get("chainKey") or "").lower(),
|
|
182
|
+
}
|
|
183
|
+
trade_value_usd = estimate_trade_value_usd(estimate_data)
|
|
184
|
+
|
|
185
|
+
if args.telegram_confirm or (THRESHOLD_USD > 0 and trade_value_usd >= THRESHOLD_USD):
|
|
186
|
+
from uniswap_autopilot.execute.telegram_confirm import request_trade_confirmation
|
|
187
|
+
|
|
188
|
+
trade_details = {
|
|
189
|
+
"chain": summary.get("chain"),
|
|
190
|
+
"tokenIn": summary.get("tokenIn"),
|
|
191
|
+
"tokenOut": summary.get("tokenOut"),
|
|
192
|
+
"amountIn": summary.get("amountIn"),
|
|
193
|
+
"amountOut": summary.get("amountOut"),
|
|
194
|
+
"slippage": summary.get("slippage"),
|
|
195
|
+
"valueUsd": f"${trade_value_usd:.2f}",
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
if not request_trade_confirmation(trade_details, timeout_seconds=300):
|
|
199
|
+
print("❌ Trade rejected or timeout")
|
|
200
|
+
sys.exit(0)
|
|
201
|
+
|
|
202
|
+
# Telegram 确认后自动补上确认短语,免去手动 --confirm
|
|
203
|
+
if not args.confirm:
|
|
204
|
+
from uniswap_autopilot.execute._internal.tx import build_confirmation_phrase
|
|
205
|
+
|
|
206
|
+
args.confirm = build_confirmation_phrase(tx)
|
|
207
|
+
|
|
208
|
+
response["telegramConfirmation"] = {
|
|
209
|
+
"status": "approved",
|
|
210
|
+
"valueUsd": f"${trade_value_usd:.2f}",
|
|
211
|
+
}
|
|
212
|
+
else:
|
|
213
|
+
response["telegramConfirmation"] = {
|
|
214
|
+
"status": "skipped",
|
|
215
|
+
"reason": "below threshold",
|
|
216
|
+
"valueUsd": f"${trade_value_usd:.2f}",
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
chain_key = (summary.get("chain") or tx.get("chainKey") or "").lower() or None
|
|
220
|
+
wallet_addr = (tx.get("from") if isinstance(tx, dict) else None) or summary.get("from") or summary.get("sender")
|
|
221
|
+
|
|
222
|
+
if args.broadcast:
|
|
223
|
+
response["preflight"] = build_preflight_report(
|
|
224
|
+
tx,
|
|
225
|
+
explicit_rpc_url=args.rpc_url,
|
|
226
|
+
strict=True,
|
|
227
|
+
)
|
|
228
|
+
# --- state machine: init run_id + preflight checkpoint ---
|
|
229
|
+
run_id = (
|
|
230
|
+
os.environ.get("AUDIT_RUN_ID")
|
|
231
|
+
or os.environ.get("STAGEFORGE_RUN_ID")
|
|
232
|
+
or f"uniswap-{uuid.uuid4().hex[:12]}"
|
|
233
|
+
)
|
|
234
|
+
action = state_machine.next_action(run_id)
|
|
235
|
+
if action is None:
|
|
236
|
+
raise RuntimeError(f"run {run_id} is in terminal state — cannot proceed")
|
|
237
|
+
if action == state_machine.STATE_PREFLIGHT:
|
|
238
|
+
state_machine.transition(run_id, state_machine.STATE_PREFLIGHT)
|
|
239
|
+
log_event(
|
|
240
|
+
event=EVENT_PREFLIGHT,
|
|
241
|
+
chain=chain_key,
|
|
242
|
+
wallet=wallet_addr,
|
|
243
|
+
error_code=None if response["preflight"].get("ok") else "preflight_failed",
|
|
244
|
+
details={"ok": response["preflight"].get("ok"), "issues": response["preflight"].get("issues")},
|
|
245
|
+
)
|
|
246
|
+
if response["preflight"].get("ok") is False:
|
|
247
|
+
raise RuntimeError(
|
|
248
|
+
"broadcast preflight failed: " + "; ".join(response["preflight"].get("issues") or [])
|
|
249
|
+
)
|
|
250
|
+
# --- policy gate (PREFLIGHT → SIGNED) ---
|
|
251
|
+
pol = _policy.load_policy()
|
|
252
|
+
policy_ctx = {
|
|
253
|
+
"chain": chain_key,
|
|
254
|
+
"sender": wallet_addr,
|
|
255
|
+
"receiver": tx.get("to"),
|
|
256
|
+
"amount": tx.get("value"),
|
|
257
|
+
}
|
|
258
|
+
policy_result = _policy.check_uniswap(pol, policy_ctx)
|
|
259
|
+
response["policyCheck"] = policy_result.to_dict()
|
|
260
|
+
if not policy_result.allowed:
|
|
261
|
+
state_machine.transition(run_id, state_machine.STATE_FAILED,
|
|
262
|
+
payload={"policy_violations": policy_result.to_dict()["violations"]})
|
|
263
|
+
log_event(
|
|
264
|
+
event=EVENT_ERROR,
|
|
265
|
+
chain=chain_key,
|
|
266
|
+
wallet=wallet_addr,
|
|
267
|
+
error_code="policy_rejected",
|
|
268
|
+
details={"violations": policy_result.to_dict()["violations"]},
|
|
269
|
+
)
|
|
270
|
+
raise RuntimeError(
|
|
271
|
+
f"Policy rejected: {'; '.join(v.message for v in policy_result.violations)}"
|
|
272
|
+
)
|
|
273
|
+
if policy_result.warnings:
|
|
274
|
+
log_event(
|
|
275
|
+
event=EVENT_PREFLIGHT,
|
|
276
|
+
chain=chain_key,
|
|
277
|
+
wallet=wallet_addr,
|
|
278
|
+
details={"stage": "policy", "warnings": policy_result.to_dict()["warnings"]},
|
|
279
|
+
)
|
|
280
|
+
# --- sign + broadcast (guard side effects before execution) ---
|
|
281
|
+
action = state_machine.next_action(run_id)
|
|
282
|
+
tx_hash_ = ""
|
|
283
|
+
receipt_rpc_url = rpc_url
|
|
284
|
+
if action == state_machine.STATE_SIGNED:
|
|
285
|
+
state_machine.transition(run_id, state_machine.STATE_SIGNED)
|
|
286
|
+
broadcast = broadcast_with_backend(
|
|
287
|
+
tx=tx,
|
|
288
|
+
explicit_rpc_url=args.rpc_url,
|
|
289
|
+
confirm=args.confirm,
|
|
290
|
+
signer_args_source=args,
|
|
291
|
+
)
|
|
292
|
+
response["commandPreview"] = broadcast["commandPreview"]
|
|
293
|
+
response["broadcastResult"] = broadcast["broadcastResult"]
|
|
294
|
+
response["signerBackend"] = broadcast["signerBackend"]
|
|
295
|
+
if broadcast.get("serviceDecision") is not None:
|
|
296
|
+
response["serviceDecision"] = broadcast["serviceDecision"]
|
|
297
|
+
tx_hash_ = extract_transaction_hash(response["broadcastResult"]) or ""
|
|
298
|
+
response["transactionHash"] = tx_hash_
|
|
299
|
+
receipt_rpc_url = broadcast["rpcUrl"]
|
|
300
|
+
log_event(
|
|
301
|
+
event=EVENT_BROADCAST,
|
|
302
|
+
chain=chain_key,
|
|
303
|
+
wallet=wallet_addr,
|
|
304
|
+
tx_hash=tx_hash_ or None,
|
|
305
|
+
details={
|
|
306
|
+
"signerBackend": broadcast["signerBackend"],
|
|
307
|
+
"tokenIn": tx.get("inputTokenSymbol"),
|
|
308
|
+
"tokenOut": tx.get("outputTokenSymbol"),
|
|
309
|
+
"amountIn": str(tx.get("inputAmount", "")),
|
|
310
|
+
"amountOut": str(tx.get("outputAmount", "")),
|
|
311
|
+
},
|
|
312
|
+
)
|
|
313
|
+
state_machine.transition(run_id, state_machine.STATE_BROADCAST, payload={"tx_hash": tx_hash_})
|
|
314
|
+
elif action == state_machine.STATE_BROADCAST:
|
|
315
|
+
# Already broadcast in a previous run: recover persisted tx hash.
|
|
316
|
+
saved = state_machine.load_state(run_id) or {}
|
|
317
|
+
tx_hash_ = str(saved.get("payload", {}).get("tx_hash", "") or "")
|
|
318
|
+
if not tx_hash_:
|
|
319
|
+
raise RuntimeError(f"run {run_id} is BROADCAST but missing tx_hash payload")
|
|
320
|
+
response["transactionHash"] = tx_hash_
|
|
321
|
+
response["broadcastResult"] = {"transactionHash": tx_hash_}
|
|
322
|
+
elif action == state_machine.STATE_CONFIRMED:
|
|
323
|
+
saved = state_machine.load_state(run_id) or {}
|
|
324
|
+
tx_hash_ = str(saved.get("payload", {}).get("tx_hash", "") or "")
|
|
325
|
+
response["transactionHash"] = tx_hash_
|
|
326
|
+
response["note"] = f"run {run_id} already confirmed; skipping broadcast/confirm"
|
|
327
|
+
elif action is None:
|
|
328
|
+
raise RuntimeError(f"run {run_id} is in terminal state — cannot sign/broadcast")
|
|
329
|
+
else:
|
|
330
|
+
raise RuntimeError(f"unexpected next_action={action!r} before broadcast")
|
|
331
|
+
|
|
332
|
+
if tx_hash_ and state_machine.next_action(run_id) == state_machine.STATE_CONFIRMED:
|
|
333
|
+
response["receipt"] = execute_cast_receipt(
|
|
334
|
+
tx_hash=tx_hash_,
|
|
335
|
+
rpc_url=receipt_rpc_url,
|
|
336
|
+
confirmations=args.receipt_confirmations,
|
|
337
|
+
)
|
|
338
|
+
if "receipt" in response:
|
|
339
|
+
success = receipt_succeeded(response["receipt"])
|
|
340
|
+
log_event(
|
|
341
|
+
event=EVENT_CONFIRM,
|
|
342
|
+
chain=chain_key,
|
|
343
|
+
wallet=wallet_addr,
|
|
344
|
+
tx_hash=tx_hash_ or response.get("transactionHash"),
|
|
345
|
+
error_code=None if success else "receipt_failed",
|
|
346
|
+
details={
|
|
347
|
+
"status": response["receipt"].get("status"),
|
|
348
|
+
"confirmations": args.receipt_confirmations,
|
|
349
|
+
},
|
|
350
|
+
)
|
|
351
|
+
# --- state machine: confirmed ---
|
|
352
|
+
action = state_machine.next_action(run_id)
|
|
353
|
+
if action == state_machine.STATE_CONFIRMED:
|
|
354
|
+
state_machine.transition(run_id, state_machine.STATE_CONFIRMED)
|
|
355
|
+
if not success:
|
|
356
|
+
raise RuntimeError(f"broadcast receipt status is not successful: {response['receipt'].get('status')}")
|
|
357
|
+
|
|
358
|
+
if args.output:
|
|
359
|
+
Path(args.output).write_text(
|
|
360
|
+
json.dumps(response, ensure_ascii=False, indent=2) + "\n",
|
|
361
|
+
encoding="utf-8",
|
|
362
|
+
)
|
|
363
|
+
dump_json(response)
|
|
364
|
+
except Exception as exc: # noqa: BLE001
|
|
365
|
+
# --- state machine: failed (transition error = bug, let it surface) ---
|
|
366
|
+
if run_id:
|
|
367
|
+
state_machine.transition(run_id, state_machine.STATE_FAILED, payload={"error_code": type(exc).__name__})
|
|
368
|
+
log_event(
|
|
369
|
+
event=EVENT_ERROR,
|
|
370
|
+
chain=None,
|
|
371
|
+
wallet=None,
|
|
372
|
+
error_code=type(exc).__name__,
|
|
373
|
+
details={"message": str(exc), "action": "execute_transaction"},
|
|
374
|
+
)
|
|
375
|
+
print(json.dumps({"error": str(exc)}, ensure_ascii=False, indent=2), file=sys.stderr)
|
|
376
|
+
sys.exit(1)
|
|
377
|
+
|
|
378
|
+
|
|
379
|
+
if __name__ == "__main__":
|
|
380
|
+
main()
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
"""Detect signing backend availability."""
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import argparse
|
|
6
|
+
|
|
7
|
+
from uniswap_autopilot.common.common import dump_json, load_local_env
|
|
8
|
+
from uniswap_autopilot.execute._internal.signer import detect_hot_wallet_backend
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
def sanitize_backend_status(status: dict[str, object]) -> dict[str, object]:
|
|
12
|
+
sanitized = dict(status)
|
|
13
|
+
signer_args = sanitized.pop("signerArgs", None)
|
|
14
|
+
if signer_args is None:
|
|
15
|
+
return sanitized
|
|
16
|
+
|
|
17
|
+
mode = None
|
|
18
|
+
wallet_source = None
|
|
19
|
+
if getattr(signer_args, "private_key_env", None):
|
|
20
|
+
mode = "private-key-env"
|
|
21
|
+
wallet_source = getattr(signer_args, "wallet_source", None)
|
|
22
|
+
|
|
23
|
+
sanitized["signerConfig"] = {
|
|
24
|
+
"mode": mode,
|
|
25
|
+
"walletSource": wallet_source,
|
|
26
|
+
}
|
|
27
|
+
return sanitized
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
def main() -> None:
|
|
31
|
+
parser = argparse.ArgumentParser(description="Detect signing backend availability")
|
|
32
|
+
_ = parser.parse_args()
|
|
33
|
+
|
|
34
|
+
load_local_env()
|
|
35
|
+
hot = sanitize_backend_status(detect_hot_wallet_backend())
|
|
36
|
+
|
|
37
|
+
selected_backend = None
|
|
38
|
+
selected_wallet = None
|
|
39
|
+
if hot.get("available"):
|
|
40
|
+
selected_backend = "pure-python"
|
|
41
|
+
selected_wallet = hot.get("wallet")
|
|
42
|
+
|
|
43
|
+
dump_json({
|
|
44
|
+
"action": "detect_execution_context",
|
|
45
|
+
"signer": hot,
|
|
46
|
+
"selectedBackend": selected_backend,
|
|
47
|
+
"selectedWallet": selected_wallet,
|
|
48
|
+
})
|
|
49
|
+
|
|
50
|
+
|
|
51
|
+
if __name__ == "__main__":
|
|
52
|
+
main()
|