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,693 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
from __future__ import annotations
|
|
3
|
+
|
|
4
|
+
import argparse
|
|
5
|
+
import json
|
|
6
|
+
import sys
|
|
7
|
+
import time
|
|
8
|
+
from pathlib import Path
|
|
9
|
+
from typing import Any
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
from uniswap_autopilot.common.common import add_common_arguments, decimal_to_base_units, dump_json, load_local_env, normalize_chain, parse_amount, resolve_token, resolve_wallet_address
|
|
13
|
+
from uniswap_autopilot.execute import broadcast as execute_transaction
|
|
14
|
+
from uniswap_autopilot.swap.flow_core.artifacts import write_json
|
|
15
|
+
from uniswap_autopilot.swap.flow_core.broadcast import maybe_broadcast
|
|
16
|
+
from uniswap_autopilot.swap.flow_core.diagnostics import (
|
|
17
|
+
build_swap_failure_message,
|
|
18
|
+
classify_swap_failure,
|
|
19
|
+
extract_permit_sig_deadline,
|
|
20
|
+
parse_intish,
|
|
21
|
+
)
|
|
22
|
+
from uniswap_autopilot.swap.flow_core.paper import (
|
|
23
|
+
append_jsonl,
|
|
24
|
+
build_paper_trade_entry_id,
|
|
25
|
+
build_paper_trade_info,
|
|
26
|
+
build_paper_trade_journal_entry,
|
|
27
|
+
build_paper_trade_paths,
|
|
28
|
+
build_quote_only_paper_swap,
|
|
29
|
+
)
|
|
30
|
+
from uniswap_autopilot.swap.flow_core.policy import (
|
|
31
|
+
evaluate_auto_trade_policy,
|
|
32
|
+
load_auto_trade_policy,
|
|
33
|
+
policy_rule_allows,
|
|
34
|
+
)
|
|
35
|
+
from uniswap_autopilot.swap.trading_api import quote as trading_api_quote
|
|
36
|
+
from uniswap_autopilot.swap.trading_api import swap as swap_dry_run
|
|
37
|
+
|
|
38
|
+
SIMULATION_FALLBACK_MARKERS = ("TRANSFER_FROM_FAILED",)
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
def run_trade_flow(
|
|
42
|
+
chain: str,
|
|
43
|
+
token_in: str,
|
|
44
|
+
token_out: str,
|
|
45
|
+
amount: str,
|
|
46
|
+
wallet: str | None,
|
|
47
|
+
output_dir: str,
|
|
48
|
+
token_in_decimals: int | None = None,
|
|
49
|
+
token_out_decimals: int | None = None,
|
|
50
|
+
swap_type: str = "EXACT_INPUT",
|
|
51
|
+
slippage: float = 0.5,
|
|
52
|
+
auto_slippage: bool = False,
|
|
53
|
+
auto_unwrap: bool = False,
|
|
54
|
+
dst_chain: str | None = None,
|
|
55
|
+
routing_preference: str = "BEST_PRICE",
|
|
56
|
+
signature: str | None = None,
|
|
57
|
+
signature_file: str | None = None,
|
|
58
|
+
quote_file: str | None = None,
|
|
59
|
+
rpc_url: str | None = None,
|
|
60
|
+
check_approval: bool = True,
|
|
61
|
+
broadcast_approval: bool = False,
|
|
62
|
+
approval_confirm: str | None = None,
|
|
63
|
+
broadcast_swap: bool = False,
|
|
64
|
+
swap_confirm: str | None = None,
|
|
65
|
+
assume_approval_ready: bool = False,
|
|
66
|
+
signer_args_source: argparse.Namespace | None = None,
|
|
67
|
+
receipt_confirmations: int = 1,
|
|
68
|
+
policy_file: str | None = None,
|
|
69
|
+
auto_sign_permit: bool = False,
|
|
70
|
+
auto_execute: bool = False,
|
|
71
|
+
paper_trade: bool = False,
|
|
72
|
+
journal_file: str | None = None,
|
|
73
|
+
use_flashbots: bool = False,
|
|
74
|
+
) -> dict[str, Any]:
|
|
75
|
+
load_local_env()
|
|
76
|
+
effective_slippage = slippage
|
|
77
|
+
slippage_source = "manual"
|
|
78
|
+
if auto_slippage:
|
|
79
|
+
from uniswap_autopilot.swap.extensions.slippage import suggest_slippage
|
|
80
|
+
suggestion = suggest_slippage(chain, token_in, token_out, float(parse_amount(amount)))
|
|
81
|
+
effective_slippage = suggestion["recommendedSlippage"]
|
|
82
|
+
slippage_source = f"auto({suggestion['category']})"
|
|
83
|
+
if paper_trade and broadcast_approval:
|
|
84
|
+
raise ValueError("--paper-trade cannot be combined with --broadcast-approval")
|
|
85
|
+
if paper_trade and broadcast_swap:
|
|
86
|
+
raise ValueError("--paper-trade cannot be combined with --broadcast-swap")
|
|
87
|
+
if paper_trade and auto_sign_permit:
|
|
88
|
+
raise ValueError("--paper-trade cannot be combined with --auto-sign-permit")
|
|
89
|
+
if paper_trade and auto_execute:
|
|
90
|
+
raise ValueError("--paper-trade cannot be combined with --auto-execute")
|
|
91
|
+
effective_flashbots = use_flashbots
|
|
92
|
+
if use_flashbots and normalize_chain(chain).key != "ethereum":
|
|
93
|
+
effective_flashbots = False
|
|
94
|
+
print(f"warning: --flashbots only works on Ethereum mainnet; ignoring for {chain}", file=sys.stderr)
|
|
95
|
+
effective_signer_args = execute_transaction.auto_select_signer_args(signer_args_source)
|
|
96
|
+
wallet_preference = "any"
|
|
97
|
+
if effective_signer_args is not None:
|
|
98
|
+
if execute_transaction.has_direct_signer(effective_signer_args):
|
|
99
|
+
wallet_preference = "secure"
|
|
100
|
+
resolved_wallet = resolve_wallet_address(wallet, "wallet", preference=wallet_preference)
|
|
101
|
+
if not resolved_wallet:
|
|
102
|
+
raise ValueError("--wallet is required unless a wallet address env var is configured")
|
|
103
|
+
|
|
104
|
+
paper_trade_entry_id: str | None = None
|
|
105
|
+
journal_path: Path | None = None
|
|
106
|
+
if paper_trade:
|
|
107
|
+
paper_trade_entry_id, output_root, journal_path = build_paper_trade_paths(output_dir, journal_file)
|
|
108
|
+
else:
|
|
109
|
+
output_root = Path(output_dir)
|
|
110
|
+
|
|
111
|
+
request_context = {
|
|
112
|
+
"chain": chain,
|
|
113
|
+
"tokenIn": token_in,
|
|
114
|
+
"tokenOut": token_out,
|
|
115
|
+
"amount": amount,
|
|
116
|
+
"wallet": resolved_wallet,
|
|
117
|
+
"swapType": swap_type,
|
|
118
|
+
"slippage": effective_slippage,
|
|
119
|
+
"slippageSource": slippage_source,
|
|
120
|
+
"routingPreference": routing_preference,
|
|
121
|
+
"outputDir": str(output_root),
|
|
122
|
+
}
|
|
123
|
+
if dst_chain:
|
|
124
|
+
request_context["dstChain"] = dst_chain
|
|
125
|
+
|
|
126
|
+
response: dict[str, Any] | None = None
|
|
127
|
+
try:
|
|
128
|
+
effective_signature = swap_dry_run.load_signature_value(signature=signature, signature_file=signature_file)
|
|
129
|
+
if quote_file:
|
|
130
|
+
quote_path = Path(quote_file)
|
|
131
|
+
else:
|
|
132
|
+
quote_path = output_root / "quote.json"
|
|
133
|
+
permit_path = output_root / "permit.json"
|
|
134
|
+
typed_data_path = output_root / "typed-data.json"
|
|
135
|
+
signature_path = output_root / "signature.txt"
|
|
136
|
+
swap_path = output_root / "swap.json"
|
|
137
|
+
policy_check: dict[str, Any] | None = None
|
|
138
|
+
if policy_file:
|
|
139
|
+
policy = load_auto_trade_policy(policy_file)
|
|
140
|
+
policy_check = evaluate_auto_trade_policy(
|
|
141
|
+
policy=policy,
|
|
142
|
+
chain_name=chain,
|
|
143
|
+
token_in_name=token_in,
|
|
144
|
+
token_out_name=token_out,
|
|
145
|
+
amount_value=amount,
|
|
146
|
+
slippage=slippage,
|
|
147
|
+
)
|
|
148
|
+
if not policy_check["allowed"]:
|
|
149
|
+
raise ValueError("trade does not satisfy auto-trade policy: " + "; ".join(policy_check["issues"]))
|
|
150
|
+
if auto_execute:
|
|
151
|
+
if policy_check is None:
|
|
152
|
+
raise ValueError("--auto-execute requires --policy-file")
|
|
153
|
+
if effective_signer_args is None or not execute_transaction.has_direct_signer(effective_signer_args):
|
|
154
|
+
raise ValueError("--auto-execute requires signer args")
|
|
155
|
+
effective_auto_sign_permit = auto_sign_permit or auto_execute
|
|
156
|
+
|
|
157
|
+
reused_saved_quote = effective_signature is not None or quote_file is not None
|
|
158
|
+
if reused_saved_quote:
|
|
159
|
+
if not quote_path.exists():
|
|
160
|
+
raise ValueError(f"quote reuse requires an existing quote file: {quote_path}")
|
|
161
|
+
quote_response = swap_dry_run.load_quote_payload(str(quote_path))
|
|
162
|
+
raw_quote = quote_response["rawQuote"]
|
|
163
|
+
# 验证复用的 quote 金额与传入的 amount 参数是否匹配
|
|
164
|
+
quote_input = raw_quote.get("quote", {}).get("input", {})
|
|
165
|
+
quote_input_amount = quote_input.get("amount")
|
|
166
|
+
if quote_input_amount:
|
|
167
|
+
# 将传入的 amount 转换成 base units 进行比较
|
|
168
|
+
current_chain = normalize_chain(chain)
|
|
169
|
+
current_token_in = resolve_token(current_chain, token_in)
|
|
170
|
+
current_base_amount = decimal_to_base_units(parse_amount(amount), current_token_in["decimals"])
|
|
171
|
+
if str(current_base_amount) != str(quote_input_amount):
|
|
172
|
+
raise ValueError(
|
|
173
|
+
f"quote file amount mismatch: quote at {quote_path} has {quote_input_amount} (wei), but requested amount is {amount} ({current_base_amount} wei)"
|
|
174
|
+
)
|
|
175
|
+
else:
|
|
176
|
+
api_key = trading_api_quote.require_api_key()
|
|
177
|
+
quote_response, approval_payload, quote_payload = trading_api_quote.prepare_quote_request_data(
|
|
178
|
+
chain_name=chain,
|
|
179
|
+
token_in_name=token_in,
|
|
180
|
+
token_out_name=token_out,
|
|
181
|
+
amount_value=amount,
|
|
182
|
+
wallet=resolved_wallet,
|
|
183
|
+
token_in_decimals=token_in_decimals,
|
|
184
|
+
token_out_decimals=token_out_decimals,
|
|
185
|
+
swap_type=swap_type,
|
|
186
|
+
slippage=effective_slippage,
|
|
187
|
+
routing_preference=routing_preference,
|
|
188
|
+
check_approval=check_approval,
|
|
189
|
+
auto_slippage=auto_slippage,
|
|
190
|
+
dst_chain_name=dst_chain,
|
|
191
|
+
)
|
|
192
|
+
quote_response["requestOnly"] = False
|
|
193
|
+
quote_response["requestPayloads"] = {
|
|
194
|
+
"checkApproval": approval_payload,
|
|
195
|
+
"quote": quote_payload,
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
if approval_payload is not None:
|
|
199
|
+
quote_response["approvalCheck"] = trading_api_quote.post_json("check_approval", approval_payload, api_key)
|
|
200
|
+
|
|
201
|
+
raw_quote = trading_api_quote.post_json("quote", quote_payload, api_key)
|
|
202
|
+
quote_response["quoteSummary"] = trading_api_quote.summarize_quote(raw_quote)
|
|
203
|
+
quote_response["rawQuote"] = raw_quote
|
|
204
|
+
write_json(quote_path, quote_response)
|
|
205
|
+
|
|
206
|
+
approval = (quote_response.get("approvalCheck") or {}).get("approval")
|
|
207
|
+
permit_data = raw_quote.get("permitData")
|
|
208
|
+
effective_assume_approval_ready = assume_approval_ready
|
|
209
|
+
if auto_execute:
|
|
210
|
+
if permit_data and effective_signature is None and not policy_rule_allows(policy_check, "allowAutoSignPermit"):
|
|
211
|
+
raise ValueError("auto-execute requires allowAutoSignPermit for permit-required trades")
|
|
212
|
+
if approval and not effective_assume_approval_ready and not policy_rule_allows(policy_check, "allowAutoApproval"):
|
|
213
|
+
raise ValueError("auto-execute requires allowAutoApproval for approval-required trades")
|
|
214
|
+
if not policy_rule_allows(policy_check, "allowAutoBroadcastSwap"):
|
|
215
|
+
raise ValueError("auto-execute requires allowAutoBroadcastSwap for swap broadcast")
|
|
216
|
+
|
|
217
|
+
response = {
|
|
218
|
+
"action": "trade_flow",
|
|
219
|
+
"inputs": {
|
|
220
|
+
"chain": chain,
|
|
221
|
+
"tokenIn": token_in,
|
|
222
|
+
"tokenOut": token_out,
|
|
223
|
+
"amount": amount,
|
|
224
|
+
"wallet": resolved_wallet,
|
|
225
|
+
"swapType": swap_type,
|
|
226
|
+
"slippage": effective_slippage,
|
|
227
|
+
"slippageSource": slippage_source,
|
|
228
|
+
"routingPreference": routing_preference,
|
|
229
|
+
},
|
|
230
|
+
"files": {
|
|
231
|
+
"quote": str(quote_path),
|
|
232
|
+
},
|
|
233
|
+
"quote": {
|
|
234
|
+
"summary": quote_response.get("quoteSummary") or trading_api_quote.summarize_quote(raw_quote),
|
|
235
|
+
"reusedQuote": reused_saved_quote,
|
|
236
|
+
},
|
|
237
|
+
"automation": {
|
|
238
|
+
"policyEnabled": policy_check is not None,
|
|
239
|
+
"autoExecuteRequested": auto_execute,
|
|
240
|
+
"autoSignPermitRequested": auto_sign_permit,
|
|
241
|
+
"autoSignPermitActive": effective_auto_sign_permit,
|
|
242
|
+
"walletPreference": wallet_preference,
|
|
243
|
+
"signerBackendDetected": (
|
|
244
|
+
"trade-signer"
|
|
245
|
+
if effective_signer_args is not None and execute_transaction.has_direct_signer(effective_signer_args)
|
|
246
|
+
else ("direct" if effective_signer_args is not None and execute_transaction.has_direct_signer(effective_signer_args) else None)
|
|
247
|
+
),
|
|
248
|
+
},
|
|
249
|
+
"nextActions": [],
|
|
250
|
+
}
|
|
251
|
+
if policy_check is not None:
|
|
252
|
+
response["policyCheck"] = policy_check
|
|
253
|
+
|
|
254
|
+
approval_broadcasted = False
|
|
255
|
+
auto_broadcasted_approval = False
|
|
256
|
+
approval_assumed_from_preflight = False
|
|
257
|
+
if approval:
|
|
258
|
+
approval_tx = execute_transaction.load_approval_transaction(str(quote_path))
|
|
259
|
+
effective_broadcast_approval = broadcast_approval
|
|
260
|
+
if auto_execute and not effective_broadcast_approval and not effective_assume_approval_ready:
|
|
261
|
+
if policy_rule_allows(policy_check, "allowAutoApproval"):
|
|
262
|
+
effective_broadcast_approval = True
|
|
263
|
+
auto_broadcasted_approval = True
|
|
264
|
+
else:
|
|
265
|
+
raise ValueError("auto-execute requires allowAutoApproval for approval-required trades")
|
|
266
|
+
approval_preflight = execute_transaction.build_preflight_report(
|
|
267
|
+
approval_tx,
|
|
268
|
+
explicit_rpc_url=rpc_url,
|
|
269
|
+
)
|
|
270
|
+
response["approval"] = {
|
|
271
|
+
"required": True,
|
|
272
|
+
"broadcastRequested": effective_broadcast_approval,
|
|
273
|
+
"autoBroadcast": auto_broadcasted_approval,
|
|
274
|
+
"assumeApprovalReady": effective_assume_approval_ready,
|
|
275
|
+
"assumeApprovalReadyInferred": False,
|
|
276
|
+
"preflight": approval_preflight,
|
|
277
|
+
**execute_transaction.build_execute_preview(approval_tx, explicit_rpc_url=rpc_url),
|
|
278
|
+
}
|
|
279
|
+
if not effective_broadcast_approval and not effective_assume_approval_ready:
|
|
280
|
+
allowance = (approval_preflight.get("allowance") or {})
|
|
281
|
+
if allowance.get("alreadySufficient"):
|
|
282
|
+
effective_assume_approval_ready = True
|
|
283
|
+
approval_assumed_from_preflight = True
|
|
284
|
+
response["approval"]["assumeApprovalReady"] = True
|
|
285
|
+
response["approval"]["assumeApprovalReadyInferred"] = True
|
|
286
|
+
if effective_broadcast_approval:
|
|
287
|
+
if policy_check is not None and not policy_rule_allows(policy_check, "allowAutoApproval"):
|
|
288
|
+
raise ValueError("auto-trade policy does not allow approval broadcast")
|
|
289
|
+
if effective_signer_args is None:
|
|
290
|
+
raise ValueError("broadcast_approval requires signer args")
|
|
291
|
+
effective_approval_confirm = approval_confirm
|
|
292
|
+
if effective_approval_confirm is None and auto_broadcasted_approval:
|
|
293
|
+
effective_approval_confirm = execute_transaction.build_confirmation_phrase(approval_tx)
|
|
294
|
+
approval_broadcast = maybe_broadcast(
|
|
295
|
+
tx=approval_tx,
|
|
296
|
+
explicit_rpc_url=rpc_url,
|
|
297
|
+
confirm=effective_approval_confirm,
|
|
298
|
+
signer_args_source=effective_signer_args,
|
|
299
|
+
receipt_confirmations=receipt_confirmations,
|
|
300
|
+
use_flashbots=effective_flashbots,
|
|
301
|
+
)
|
|
302
|
+
response["approval"].update(approval_broadcast)
|
|
303
|
+
approval_broadcasted = True
|
|
304
|
+
elif not effective_assume_approval_ready:
|
|
305
|
+
response["nextActions"].append("broadcast-approval")
|
|
306
|
+
else:
|
|
307
|
+
response["approval"] = {
|
|
308
|
+
"required": False,
|
|
309
|
+
"broadcastRequested": False,
|
|
310
|
+
"autoBroadcast": False,
|
|
311
|
+
}
|
|
312
|
+
|
|
313
|
+
permit_auto_signed = False
|
|
314
|
+
if permit_data and effective_signature is None:
|
|
315
|
+
handoff = swap_dry_run.build_permit_handoff(raw_quote=raw_quote, quote_file=str(quote_path))
|
|
316
|
+
write_json(permit_path, handoff)
|
|
317
|
+
write_json(typed_data_path, handoff["typedData"])
|
|
318
|
+
response["files"]["permit"] = str(permit_path)
|
|
319
|
+
response["files"]["typedData"] = str(typed_data_path)
|
|
320
|
+
response["permit"] = {
|
|
321
|
+
"required": True,
|
|
322
|
+
"routing": handoff["routing"],
|
|
323
|
+
"quoteFile": handoff["quoteFile"],
|
|
324
|
+
"signatureRule": handoff["signatureRule"],
|
|
325
|
+
"instructions": handoff["instructions"],
|
|
326
|
+
}
|
|
327
|
+
if effective_auto_sign_permit:
|
|
328
|
+
if policy_check is not None and not policy_rule_allows(policy_check, "allowAutoSignPermit"):
|
|
329
|
+
raise ValueError("auto-trade policy does not allow permit auto-sign")
|
|
330
|
+
if effective_signer_args is None or not execute_transaction.has_direct_signer(effective_signer_args):
|
|
331
|
+
raise ValueError("auto_sign_permit requires signer args")
|
|
332
|
+
quote_blob = raw_quote.get("quote") or {}
|
|
333
|
+
quote_input = quote_blob.get("input") or {}
|
|
334
|
+
quote_output = quote_blob.get("output") or {}
|
|
335
|
+
permit_domain = (raw_quote.get("permitData") or {}).get("domain") or {}
|
|
336
|
+
permit_sign_tx = {
|
|
337
|
+
"kind": "swap",
|
|
338
|
+
"chainId": normalize_chain(chain).chain_id,
|
|
339
|
+
"to": permit_domain.get("verifyingContract") or resolved_wallet,
|
|
340
|
+
"from": resolved_wallet,
|
|
341
|
+
"data": "0x01",
|
|
342
|
+
"value": "0",
|
|
343
|
+
"inputToken": quote_input.get("token") or "",
|
|
344
|
+
"inputAmount": quote_input.get("amount") or "0",
|
|
345
|
+
"outputToken": quote_output.get("token") or "",
|
|
346
|
+
"outputAmount": quote_output.get("amount") or None,
|
|
347
|
+
"nativeInput": str(quote_input.get("token") or "").lower() == "0x0000000000000000000000000000000000000000",
|
|
348
|
+
"sourceKind": "quote-file",
|
|
349
|
+
"sourceFile": str(quote_path),
|
|
350
|
+
}
|
|
351
|
+
sign_result = execute_transaction.sign_typed_data_with_backend(
|
|
352
|
+
typed_data_file=str(typed_data_path),
|
|
353
|
+
typed_data=handoff["typedData"],
|
|
354
|
+
tx=permit_sign_tx,
|
|
355
|
+
signer_args_source=effective_signer_args,
|
|
356
|
+
)
|
|
357
|
+
signed = sign_result["signature"]
|
|
358
|
+
signature_path.write_text(signed + "\n", encoding="utf-8")
|
|
359
|
+
effective_signature = signed
|
|
360
|
+
permit_auto_signed = True
|
|
361
|
+
response["files"]["signature"] = str(signature_path)
|
|
362
|
+
response["permit"].update(
|
|
363
|
+
{
|
|
364
|
+
"autoSigned": True,
|
|
365
|
+
"signatureProvided": True,
|
|
366
|
+
"signatureFile": str(signature_path),
|
|
367
|
+
"signCommandPreview": sign_result["signCommandPreview"],
|
|
368
|
+
"signerBackend": sign_result["signerBackend"],
|
|
369
|
+
}
|
|
370
|
+
)
|
|
371
|
+
if sign_result.get("serviceDecision") is not None:
|
|
372
|
+
response["permit"]["serviceDecision"] = sign_result["serviceDecision"]
|
|
373
|
+
else:
|
|
374
|
+
if broadcast_swap or auto_execute:
|
|
375
|
+
raise ValueError("swap broadcast requires permit signature; rerun with --signature or --signature-file")
|
|
376
|
+
response["nextActions"].append("sign-permit")
|
|
377
|
+
if paper_trade:
|
|
378
|
+
response["permit"]["paperSignatureBypassed"] = True
|
|
379
|
+
else:
|
|
380
|
+
return response
|
|
381
|
+
|
|
382
|
+
response["permit"] = {
|
|
383
|
+
**(response.get("permit") or {}),
|
|
384
|
+
"required": bool(permit_data),
|
|
385
|
+
"signatureProvided": effective_signature is not None,
|
|
386
|
+
"autoSigned": permit_auto_signed,
|
|
387
|
+
}
|
|
388
|
+
permit_sig_deadline = extract_permit_sig_deadline(raw_quote)
|
|
389
|
+
if permit_sig_deadline is not None:
|
|
390
|
+
response["permit"]["sigDeadline"] = str(permit_sig_deadline)
|
|
391
|
+
# Ignore tiny placeholder values often used in tests/mocks.
|
|
392
|
+
if permit_sig_deadline >= 1_000_000_000:
|
|
393
|
+
response["permit"]["sigDeadlineExpired"] = permit_sig_deadline <= int(time.time())
|
|
394
|
+
if response["permit"]["sigDeadlineExpired"]:
|
|
395
|
+
raise ValueError(
|
|
396
|
+
"permit signature has expired; refresh quote (do not reuse old quote/signature) and sign again"
|
|
397
|
+
)
|
|
398
|
+
else:
|
|
399
|
+
response["permit"]["sigDeadlineExpired"] = False
|
|
400
|
+
|
|
401
|
+
if permit_data and effective_signature is None and paper_trade:
|
|
402
|
+
response["swap"] = build_quote_only_paper_swap(
|
|
403
|
+
raw_quote=raw_quote,
|
|
404
|
+
reason="permit signature is unavailable, paper-trade records quote-only preview",
|
|
405
|
+
)
|
|
406
|
+
else:
|
|
407
|
+
api_key = trading_api_quote.require_api_key()
|
|
408
|
+
simulation_requested = True
|
|
409
|
+
simulation_used = True
|
|
410
|
+
simulation_fallback_reason: str | None = None
|
|
411
|
+
swap_payload = swap_dry_run.build_swap_payload(
|
|
412
|
+
raw_quote=raw_quote,
|
|
413
|
+
signature=effective_signature,
|
|
414
|
+
simulate_transaction=True,
|
|
415
|
+
refresh_gas_price=True,
|
|
416
|
+
)
|
|
417
|
+
try:
|
|
418
|
+
swap_response = swap_dry_run.post_json("swap", swap_payload, api_key)
|
|
419
|
+
except RuntimeError as exc:
|
|
420
|
+
if not any(marker in str(exc) for marker in SIMULATION_FALLBACK_MARKERS):
|
|
421
|
+
diagnosis = classify_swap_failure(
|
|
422
|
+
error_text=str(exc),
|
|
423
|
+
raw_quote=raw_quote,
|
|
424
|
+
wallet=resolved_wallet,
|
|
425
|
+
chain_name=chain,
|
|
426
|
+
explicit_rpc_url=rpc_url,
|
|
427
|
+
quote_path=quote_path,
|
|
428
|
+
reused_saved_quote=reused_saved_quote,
|
|
429
|
+
has_signature=effective_signature is not None,
|
|
430
|
+
approval_required=bool(approval),
|
|
431
|
+
assume_approval_ready=effective_assume_approval_ready,
|
|
432
|
+
)
|
|
433
|
+
response["swapFailureDiagnosis"] = diagnosis
|
|
434
|
+
raise RuntimeError(build_swap_failure_message(str(exc), diagnosis)) from exc
|
|
435
|
+
simulation_used = False
|
|
436
|
+
simulation_fallback_reason = str(exc)
|
|
437
|
+
swap_payload = swap_dry_run.build_swap_payload(
|
|
438
|
+
raw_quote=raw_quote,
|
|
439
|
+
signature=effective_signature,
|
|
440
|
+
simulate_transaction=False,
|
|
441
|
+
refresh_gas_price=False,
|
|
442
|
+
)
|
|
443
|
+
swap_response = swap_dry_run.post_json("swap", swap_payload, api_key)
|
|
444
|
+
|
|
445
|
+
wrapped_swap_response = {
|
|
446
|
+
"action": "swap_dry_run",
|
|
447
|
+
"requestOnly": False,
|
|
448
|
+
"requestPayload": swap_payload,
|
|
449
|
+
"swapResponse": swap_response,
|
|
450
|
+
}
|
|
451
|
+
write_json(swap_path, wrapped_swap_response)
|
|
452
|
+
response["files"]["swap"] = str(swap_path)
|
|
453
|
+
|
|
454
|
+
swap_tx = execute_transaction.load_swap_transaction(str(swap_path))
|
|
455
|
+
swap_preflight = execute_transaction.build_preflight_report(
|
|
456
|
+
swap_tx,
|
|
457
|
+
explicit_rpc_url=rpc_url,
|
|
458
|
+
)
|
|
459
|
+
effective_broadcast_swap = broadcast_swap
|
|
460
|
+
auto_broadcasted_swap = False
|
|
461
|
+
if auto_execute and not effective_broadcast_swap:
|
|
462
|
+
if policy_rule_allows(policy_check, "allowAutoBroadcastSwap"):
|
|
463
|
+
effective_broadcast_swap = True
|
|
464
|
+
auto_broadcasted_swap = True
|
|
465
|
+
else:
|
|
466
|
+
raise ValueError("auto-execute requires allowAutoBroadcastSwap for swap broadcast")
|
|
467
|
+
response["swap"] = {
|
|
468
|
+
"simulationRequested": simulation_requested,
|
|
469
|
+
"simulationUsed": simulation_used,
|
|
470
|
+
"simulationFallbackReason": simulation_fallback_reason,
|
|
471
|
+
"broadcastRequested": effective_broadcast_swap,
|
|
472
|
+
"autoBroadcast": auto_broadcasted_swap,
|
|
473
|
+
"preflight": swap_preflight,
|
|
474
|
+
**execute_transaction.build_execute_preview(swap_tx, explicit_rpc_url=rpc_url),
|
|
475
|
+
}
|
|
476
|
+
if effective_broadcast_swap:
|
|
477
|
+
if policy_check is not None and not policy_rule_allows(policy_check, "allowAutoBroadcastSwap"):
|
|
478
|
+
raise ValueError("auto-trade policy does not allow swap broadcast")
|
|
479
|
+
if approval and not (approval_broadcasted or effective_assume_approval_ready):
|
|
480
|
+
raise ValueError(
|
|
481
|
+
"swap broadcast requires approval to be broadcast in this run or --assume-approval-ready"
|
|
482
|
+
)
|
|
483
|
+
if effective_signer_args is None:
|
|
484
|
+
raise ValueError("broadcast_swap requires signer args")
|
|
485
|
+
effective_swap_confirm = swap_confirm
|
|
486
|
+
if effective_swap_confirm is None and auto_broadcasted_swap:
|
|
487
|
+
effective_swap_confirm = execute_transaction.build_confirmation_phrase(swap_tx)
|
|
488
|
+
swap_broadcast = maybe_broadcast(
|
|
489
|
+
tx=swap_tx,
|
|
490
|
+
explicit_rpc_url=rpc_url,
|
|
491
|
+
confirm=effective_swap_confirm,
|
|
492
|
+
signer_args_source=effective_signer_args,
|
|
493
|
+
receipt_confirmations=receipt_confirmations,
|
|
494
|
+
use_flashbots=effective_flashbots,
|
|
495
|
+
)
|
|
496
|
+
response["swap"].update(swap_broadcast)
|
|
497
|
+
|
|
498
|
+
# Gas tracking from receipt
|
|
499
|
+
receipt = swap_broadcast.get("receipt") or {}
|
|
500
|
+
gas_used = parse_intish(receipt.get("gasUsed"))
|
|
501
|
+
effective_gas_price = parse_intish(receipt.get("effectiveGasPrice"))
|
|
502
|
+
if gas_used is not None:
|
|
503
|
+
gas_cost_wei = gas_used * (effective_gas_price or 0)
|
|
504
|
+
response["swap"]["gasTracking"] = {
|
|
505
|
+
"gasUsed": gas_used,
|
|
506
|
+
"effectiveGasPrice": effective_gas_price,
|
|
507
|
+
"gasCostWei": gas_cost_wei,
|
|
508
|
+
}
|
|
509
|
+
|
|
510
|
+
# Auto WETH unwrap on L2
|
|
511
|
+
if auto_unwrap:
|
|
512
|
+
from uniswap_autopilot.common.gas import check_weth_unwrap_needed, build_weth_unwrap_tx
|
|
513
|
+
quote_out = (raw_quote.get("quote") or {}).get("output") or {}
|
|
514
|
+
if check_weth_unwrap_needed(chain, quote_out, token_out):
|
|
515
|
+
out_amount = str(quote_out.get("amount") or "0")
|
|
516
|
+
unwrap_tx = build_weth_unwrap_tx(chain, out_amount, resolved_wallet)
|
|
517
|
+
response["swap"]["wethUnwrap"] = {
|
|
518
|
+
"needed": True,
|
|
519
|
+
"transaction": unwrap_tx,
|
|
520
|
+
}
|
|
521
|
+
try:
|
|
522
|
+
unwrap_broadcast = maybe_broadcast(
|
|
523
|
+
tx=unwrap_tx.get("transaction") or unwrap_tx,
|
|
524
|
+
explicit_rpc_url=rpc_url,
|
|
525
|
+
confirm=None,
|
|
526
|
+
signer_args_source=effective_signer_args,
|
|
527
|
+
receipt_confirmations=receipt_confirmations,
|
|
528
|
+
use_flashbots=effective_flashbots,
|
|
529
|
+
)
|
|
530
|
+
response["swap"]["wethUnwrap"]["broadcast"] = unwrap_broadcast
|
|
531
|
+
except Exception as unwrap_exc:
|
|
532
|
+
response["swap"]["wethUnwrap"]["error"] = str(unwrap_exc)
|
|
533
|
+
response["nextActions"].append("broadcast-weth-unwrap")
|
|
534
|
+
elif dst_chain and response["swap"].get("broadcastRequested"):
|
|
535
|
+
response["bridgeVerification"] = {
|
|
536
|
+
"dstChain": dst_chain,
|
|
537
|
+
"status": "pending",
|
|
538
|
+
"instructions": "Use swap.extensions.bridge.verify_bridge_arrival() to poll for arrival",
|
|
539
|
+
}
|
|
540
|
+
else:
|
|
541
|
+
response["nextActions"].append("broadcast-swap")
|
|
542
|
+
if approval and not approval_broadcasted and not effective_assume_approval_ready:
|
|
543
|
+
response["nextActions"].append("ensure-approval-mined")
|
|
544
|
+
if approval_assumed_from_preflight:
|
|
545
|
+
response["nextActions"].append("approval-already-sufficient")
|
|
546
|
+
|
|
547
|
+
if paper_trade:
|
|
548
|
+
paper_info = build_paper_trade_info(
|
|
549
|
+
entry_id=paper_trade_entry_id or build_paper_trade_entry_id(),
|
|
550
|
+
journal_path=journal_path or (output_root / "paper-trade-journal.jsonl"),
|
|
551
|
+
run_output_dir=output_root,
|
|
552
|
+
status="recorded",
|
|
553
|
+
response=response,
|
|
554
|
+
)
|
|
555
|
+
append_jsonl(
|
|
556
|
+
Path(paper_info["journalFile"]),
|
|
557
|
+
build_paper_trade_journal_entry(
|
|
558
|
+
entry_id=paper_info["entryId"],
|
|
559
|
+
journal_status="recorded",
|
|
560
|
+
run_output_dir=output_root,
|
|
561
|
+
request_context=request_context,
|
|
562
|
+
response=response,
|
|
563
|
+
),
|
|
564
|
+
)
|
|
565
|
+
response["paperTrade"] = paper_info
|
|
566
|
+
return response
|
|
567
|
+
except Exception as exc:
|
|
568
|
+
if paper_trade:
|
|
569
|
+
effective_entry_id = paper_trade_entry_id or build_paper_trade_entry_id()
|
|
570
|
+
effective_journal_path = journal_path or (Path(output_dir) / "paper-trade-journal.jsonl")
|
|
571
|
+
effective_response = response or {
|
|
572
|
+
"action": "trade_flow",
|
|
573
|
+
"inputs": request_context,
|
|
574
|
+
"nextActions": [],
|
|
575
|
+
}
|
|
576
|
+
append_jsonl(
|
|
577
|
+
effective_journal_path,
|
|
578
|
+
build_paper_trade_journal_entry(
|
|
579
|
+
entry_id=effective_entry_id,
|
|
580
|
+
journal_status="error",
|
|
581
|
+
run_output_dir=output_root,
|
|
582
|
+
request_context=request_context,
|
|
583
|
+
response=effective_response,
|
|
584
|
+
error=str(exc),
|
|
585
|
+
),
|
|
586
|
+
)
|
|
587
|
+
raise
|
|
588
|
+
|
|
589
|
+
|
|
590
|
+
def main() -> None:
|
|
591
|
+
parser = argparse.ArgumentParser(description="串行编排 Uniswap quote -> permit -> swap -> execute preview")
|
|
592
|
+
add_common_arguments(parser)
|
|
593
|
+
parser.add_argument(
|
|
594
|
+
"--wallet",
|
|
595
|
+
help="用户钱包地址;若未提供,则优先回退到 SECURE_WALLET_ADDRESS / HOT_WALLET_ADDRESS",
|
|
596
|
+
)
|
|
597
|
+
parser.add_argument("--output-dir", default="", help="输出目录")
|
|
598
|
+
parser.add_argument(
|
|
599
|
+
"--swap-type",
|
|
600
|
+
choices=["EXACT_INPUT", "EXACT_OUTPUT"],
|
|
601
|
+
default="EXACT_INPUT",
|
|
602
|
+
help="Trading API quote type",
|
|
603
|
+
)
|
|
604
|
+
parser.add_argument("--slippage", type=float, default=0.5, help="滑点百分比,默认 0.5")
|
|
605
|
+
parser.add_argument("--auto-slippage", action="store_true", help="Automatically suggest slippage based on token pair")
|
|
606
|
+
parser.add_argument("--auto-unwrap", action="store_true", help="Auto-unwrap WETH to native on L2 after swap")
|
|
607
|
+
parser.add_argument("--flashbots", action="store_true", help="Route transaction through Flashbots private mempool (Ethereum only)")
|
|
608
|
+
parser.add_argument("--dst-chain", help="Destination chain for cross-chain swap (bridge)")
|
|
609
|
+
parser.add_argument("--routing-preference", default="BEST_PRICE", help="如 BEST_PRICE / FASTEST / CLASSIC")
|
|
610
|
+
parser.add_argument("--signature", help="直接传入 permitData 签名")
|
|
611
|
+
parser.add_argument("--signature-file", help="permitData 签名文件")
|
|
612
|
+
parser.add_argument("--quote-file", help="复用既有 quote.json;传签名时推荐使用,避免签名失效")
|
|
613
|
+
parser.add_argument("--rpc-url", help="显式指定 execute preview 用的 RPC URL")
|
|
614
|
+
parser.add_argument("--broadcast-approval", action="store_true", help="在 runner 内直接广播 approval")
|
|
615
|
+
parser.add_argument("--approval-confirm", help="approval 广播确认短语,必须精确匹配")
|
|
616
|
+
parser.add_argument("--broadcast-swap", action="store_true", help="在 runner 内直接广播 swap")
|
|
617
|
+
parser.add_argument("--swap-confirm", help="swap 广播确认短语,必须精确匹配")
|
|
618
|
+
parser.add_argument(
|
|
619
|
+
"--assume-approval-ready",
|
|
620
|
+
action="store_true",
|
|
621
|
+
help="当 quote 文件里存在 approval,但你已在链上完成授权时,允许直接广播 swap",
|
|
622
|
+
)
|
|
623
|
+
parser.add_argument("--receipt-confirmations", type=int, default=1, help="广播后查询 receipt 的确认数,默认 1")
|
|
624
|
+
parser.add_argument("--policy-file", help="自动交易策略 JSON;提供后会对链/币对/金额/滑点做强制校验")
|
|
625
|
+
parser.add_argument("--auto-sign-permit", action="store_true", help="当 permitData 存在时自动签名 typed data")
|
|
626
|
+
parser.add_argument(
|
|
627
|
+
"--auto-execute",
|
|
628
|
+
action="store_true",
|
|
629
|
+
help="命中策略时自动签 permit,并自动广播 approval / swap",
|
|
630
|
+
)
|
|
631
|
+
parser.add_argument(
|
|
632
|
+
"--paper-trade",
|
|
633
|
+
action="store_true",
|
|
634
|
+
help="不真实签名/广播;output-dir 会作为根目录并把每次假盘写到 runs/<entryId>/ 下,同时追加 journal",
|
|
635
|
+
)
|
|
636
|
+
parser.add_argument(
|
|
637
|
+
"--journal-file",
|
|
638
|
+
help="paper-trade 模式的 JSONL journal 路径;默认 <output-dir>/paper-trade-journal.jsonl",
|
|
639
|
+
)
|
|
640
|
+
execute_transaction.add_signer_arguments(parser)
|
|
641
|
+
parser.add_argument(
|
|
642
|
+
"--skip-approval-check",
|
|
643
|
+
action="store_true",
|
|
644
|
+
help="跳过 /check_approval。默认当 token_in 为 ERC20 时会检查",
|
|
645
|
+
)
|
|
646
|
+
parser.add_argument("--output", help="把完整 JSON 输出写入文件")
|
|
647
|
+
args = parser.parse_args()
|
|
648
|
+
|
|
649
|
+
try:
|
|
650
|
+
response = run_trade_flow(
|
|
651
|
+
chain=args.chain,
|
|
652
|
+
token_in=args.token_in,
|
|
653
|
+
token_out=args.token_out,
|
|
654
|
+
amount=args.amount,
|
|
655
|
+
wallet=args.wallet,
|
|
656
|
+
output_dir=args.output_dir,
|
|
657
|
+
token_in_decimals=args.token_in_decimals,
|
|
658
|
+
token_out_decimals=args.token_out_decimals,
|
|
659
|
+
swap_type=args.swap_type,
|
|
660
|
+
slippage=args.slippage,
|
|
661
|
+
auto_slippage=args.auto_slippage,
|
|
662
|
+
auto_unwrap=args.auto_unwrap,
|
|
663
|
+
dst_chain=args.dst_chain,
|
|
664
|
+
routing_preference=args.routing_preference,
|
|
665
|
+
signature=args.signature,
|
|
666
|
+
signature_file=args.signature_file,
|
|
667
|
+
quote_file=args.quote_file,
|
|
668
|
+
rpc_url=args.rpc_url,
|
|
669
|
+
check_approval=not args.skip_approval_check,
|
|
670
|
+
broadcast_approval=args.broadcast_approval,
|
|
671
|
+
approval_confirm=args.approval_confirm,
|
|
672
|
+
broadcast_swap=args.broadcast_swap,
|
|
673
|
+
swap_confirm=args.swap_confirm,
|
|
674
|
+
assume_approval_ready=args.assume_approval_ready,
|
|
675
|
+
signer_args_source=args,
|
|
676
|
+
receipt_confirmations=args.receipt_confirmations,
|
|
677
|
+
policy_file=args.policy_file,
|
|
678
|
+
auto_sign_permit=args.auto_sign_permit,
|
|
679
|
+
auto_execute=args.auto_execute,
|
|
680
|
+
paper_trade=args.paper_trade,
|
|
681
|
+
journal_file=args.journal_file,
|
|
682
|
+
use_flashbots=args.flashbots,
|
|
683
|
+
)
|
|
684
|
+
if args.output:
|
|
685
|
+
write_json(Path(args.output), response)
|
|
686
|
+
dump_json(response)
|
|
687
|
+
except Exception as exc: # noqa: BLE001
|
|
688
|
+
print(json.dumps({"error": str(exc)}, ensure_ascii=False, indent=2), file=sys.stderr)
|
|
689
|
+
sys.exit(1)
|
|
690
|
+
|
|
691
|
+
|
|
692
|
+
if __name__ == "__main__":
|
|
693
|
+
main()
|