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.
Files changed (80) hide show
  1. uni_exec_engine-0.2.0.dist-info/METADATA +576 -0
  2. uni_exec_engine-0.2.0.dist-info/RECORD +80 -0
  3. uni_exec_engine-0.2.0.dist-info/WHEEL +5 -0
  4. uni_exec_engine-0.2.0.dist-info/licenses/LICENSE +21 -0
  5. uni_exec_engine-0.2.0.dist-info/top_level.txt +1 -0
  6. uniswap_autopilot/__init__.py +3 -0
  7. uniswap_autopilot/analytics/__init__.py +0 -0
  8. uniswap_autopilot/analytics/il_calculator.py +525 -0
  9. uniswap_autopilot/analytics/portfolio.py +234 -0
  10. uniswap_autopilot/analytics/position.py +433 -0
  11. uniswap_autopilot/analytics/range_suggest.py +270 -0
  12. uniswap_autopilot/audit.py +194 -0
  13. uniswap_autopilot/common/__init__.py +0 -0
  14. uniswap_autopilot/common/approval_cleanup.py +255 -0
  15. uniswap_autopilot/common/check_balance.py +64 -0
  16. uniswap_autopilot/common/common.py +804 -0
  17. uniswap_autopilot/common/deep_link.py +132 -0
  18. uniswap_autopilot/common/gas.py +141 -0
  19. uniswap_autopilot/data/auto_trade_policy.example.json +29 -0
  20. uniswap_autopilot/data/chains.json +135 -0
  21. uniswap_autopilot/data/common-token-addresses.json +777 -0
  22. uniswap_autopilot/execute/__init__.py +0 -0
  23. uniswap_autopilot/execute/_internal/__init__.py +5 -0
  24. uniswap_autopilot/execute/_internal/constants.py +15 -0
  25. uniswap_autopilot/execute/_internal/preflight.py +150 -0
  26. uniswap_autopilot/execute/_internal/pure_signer.py +182 -0
  27. uniswap_autopilot/execute/_internal/rpc.py +462 -0
  28. uniswap_autopilot/execute/_internal/signer.py +298 -0
  29. uniswap_autopilot/execute/_internal/submit.py +73 -0
  30. uniswap_autopilot/execute/_internal/tx.py +370 -0
  31. uniswap_autopilot/execute/broadcast.py +380 -0
  32. uniswap_autopilot/execute/detect.py +52 -0
  33. uniswap_autopilot/execute/telegram_confirm.py +272 -0
  34. uniswap_autopilot/lp/compare_pools.py +338 -0
  35. uniswap_autopilot/lp/v2/__init__.py +0 -0
  36. uniswap_autopilot/lp/v2/approve.py +100 -0
  37. uniswap_autopilot/lp/v2/build_tx.py +226 -0
  38. uniswap_autopilot/lp/v2/flow.py +204 -0
  39. uniswap_autopilot/lp/v2/pair.py +135 -0
  40. uniswap_autopilot/lp/v2/positions.py +177 -0
  41. uniswap_autopilot/lp/v3/__init__.py +0 -0
  42. uniswap_autopilot/lp/v3/approve.py +109 -0
  43. uniswap_autopilot/lp/v3/auto_rebalance.py +282 -0
  44. uniswap_autopilot/lp/v3/build_tx.py +465 -0
  45. uniswap_autopilot/lp/v3/compound.py +258 -0
  46. uniswap_autopilot/lp/v3/flow.py +358 -0
  47. uniswap_autopilot/lp/v3/pool.py +175 -0
  48. uniswap_autopilot/lp/v3/position.py +112 -0
  49. uniswap_autopilot/lp/v3/tick.py +75 -0
  50. uniswap_autopilot/lp/v4/__init__.py +0 -0
  51. uniswap_autopilot/lp/v4/approve.py +105 -0
  52. uniswap_autopilot/lp/v4/build_tx.py +669 -0
  53. uniswap_autopilot/lp/v4/flow.py +368 -0
  54. uniswap_autopilot/lp/v4/pool.py +174 -0
  55. uniswap_autopilot/lp/v4/position.py +185 -0
  56. uniswap_autopilot/policy.py +371 -0
  57. uniswap_autopilot/price_feed.py +100 -0
  58. uniswap_autopilot/py.typed +0 -0
  59. uniswap_autopilot/search/__init__.py +0 -0
  60. uniswap_autopilot/search/risk.py +200 -0
  61. uniswap_autopilot/search/search.py +580 -0
  62. uniswap_autopilot/state_machine.py +315 -0
  63. uniswap_autopilot/swap/__init__.py +1 -0
  64. uniswap_autopilot/swap/deep_link.py +69 -0
  65. uniswap_autopilot/swap/extensions/__init__.py +2 -0
  66. uniswap_autopilot/swap/extensions/bridge.py +197 -0
  67. uniswap_autopilot/swap/extensions/limit_order.py +272 -0
  68. uniswap_autopilot/swap/extensions/slippage.py +123 -0
  69. uniswap_autopilot/swap/flow.py +693 -0
  70. uniswap_autopilot/swap/flow_core/__init__.py +2 -0
  71. uniswap_autopilot/swap/flow_core/artifacts.py +11 -0
  72. uniswap_autopilot/swap/flow_core/broadcast.py +50 -0
  73. uniswap_autopilot/swap/flow_core/diagnostics.py +216 -0
  74. uniswap_autopilot/swap/flow_core/paper.py +113 -0
  75. uniswap_autopilot/swap/flow_core/policy.py +142 -0
  76. uniswap_autopilot/swap/links/__init__.py +2 -0
  77. uniswap_autopilot/swap/links/deep_link.py +69 -0
  78. uniswap_autopilot/swap/trading_api/permit.py +41 -0
  79. uniswap_autopilot/swap/trading_api/quote.py +282 -0
  80. 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()