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,282 @@
1
+ #!/usr/bin/env python3
2
+ from __future__ import annotations
3
+
4
+ import argparse
5
+ import json
6
+ import sys
7
+ from pathlib import Path
8
+ from typing import Any
9
+
10
+
11
+ from uniswap_autopilot.common.common import (
12
+ add_common_arguments,
13
+ decimal_to_base_units,
14
+ dump_json,
15
+ load_local_env,
16
+ normalize_chain,
17
+ override_decimals,
18
+ parse_amount,
19
+ post_json,
20
+ require_api_key,
21
+ resolve_api_token,
22
+ resolve_token,
23
+ resolve_wallet_address,
24
+ )
25
+
26
+
27
+ def summarize_quote(raw_quote: dict[str, Any]) -> dict[str, Any]:
28
+ routing = raw_quote.get("routing")
29
+ quote = raw_quote.get("quote") or {}
30
+ summary: dict[str, Any] = {"routing": routing}
31
+ price_impact = quote.get("priceImpact")
32
+ if price_impact is not None:
33
+ summary["priceImpact"] = price_impact
34
+ try:
35
+ pi = float(price_impact)
36
+ if pi >= 10:
37
+ summary["priceImpactWarning"] = "EXTREME"
38
+ elif pi >= 5:
39
+ summary["priceImpactWarning"] = "HIGH"
40
+ elif pi >= 1:
41
+ summary["priceImpactWarning"] = "MODERATE"
42
+ except (ValueError, TypeError):
43
+ pass
44
+ if routing == "CLASSIC":
45
+ summary["outputAmount"] = ((quote.get("output") or {}).get("amount"))
46
+ summary["gasFee"] = quote.get("gasFee")
47
+ summary["gasFeeUSD"] = quote.get("gasFeeUSD")
48
+ summary["gasUseEstimate"] = quote.get("gasUseEstimate")
49
+ return summary
50
+
51
+ order_info = quote.get("orderInfo") or {}
52
+ outputs = order_info.get("outputs") or []
53
+ best_output = outputs[0].get("startAmount") if outputs else None
54
+ summary["bestOutputAmount"] = best_output
55
+ summary["deadline"] = order_info.get("deadline")
56
+ summary["chainId"] = order_info.get("chainId")
57
+ return summary
58
+
59
+
60
+ def build_approval_payload(
61
+ wallet: str,
62
+ chain_id: int,
63
+ api_token_in: dict[str, Any],
64
+ base_amount: str,
65
+ ) -> dict[str, Any]:
66
+ return {
67
+ "walletAddress": wallet,
68
+ "token": api_token_in["address"],
69
+ "amount": base_amount,
70
+ "chainId": chain_id,
71
+ }
72
+
73
+
74
+ def build_quote_payload(
75
+ wallet: str | None,
76
+ chain_id: int,
77
+ api_token_in: dict[str, Any],
78
+ api_token_out: dict[str, Any],
79
+ base_amount: str,
80
+ swap_type: str,
81
+ slippage: float,
82
+ routing_preference: str,
83
+ dst_chain_id: int | None = None,
84
+ ) -> dict[str, Any]:
85
+ return {
86
+ "swapper": wallet or "0x0000000000000000000000000000000000000001",
87
+ "tokenIn": api_token_in["address"],
88
+ "tokenOut": api_token_out["address"],
89
+ "tokenInChainId": chain_id,
90
+ "tokenOutChainId": dst_chain_id or chain_id,
91
+ "amount": base_amount,
92
+ "type": swap_type,
93
+ "slippageTolerance": slippage,
94
+ "routingPreference": routing_preference,
95
+ }
96
+
97
+
98
+ def prepare_quote_request_data(
99
+ chain_name: str,
100
+ token_in_name: str,
101
+ token_out_name: str,
102
+ amount_value: str,
103
+ wallet: str | None = None,
104
+ token_in_decimals: int | None = None,
105
+ token_out_decimals: int | None = None,
106
+ swap_type: str = "EXACT_INPUT",
107
+ slippage: float = 0.5,
108
+ routing_preference: str = "BEST_PRICE",
109
+ check_approval: bool = False,
110
+ auto_slippage: bool = False,
111
+ dst_chain_name: str | None = None,
112
+ ) -> tuple[dict[str, Any], dict[str, Any] | None, dict[str, Any]]:
113
+ chain = normalize_chain(chain_name)
114
+ amount = parse_amount(amount_value)
115
+ token_in = override_decimals(resolve_token(chain, token_in_name), token_in_decimals)
116
+
117
+ if dst_chain_name:
118
+ dst_chain = normalize_chain(dst_chain_name)
119
+ token_out = override_decimals(resolve_token(dst_chain, token_out_name), token_out_decimals)
120
+ else:
121
+ dst_chain = chain
122
+ token_out = override_decimals(resolve_token(chain, token_out_name), token_out_decimals)
123
+
124
+ api_token_in = resolve_api_token(chain, token_in)
125
+ api_token_out = resolve_api_token(dst_chain, token_out)
126
+ validated_wallet = resolve_wallet_address(wallet, "wallet")
127
+
128
+ if check_approval and not validated_wallet:
129
+ raise ValueError("--check-approval requires --wallet or a wallet address env var")
130
+ if slippage < 0 or slippage > 100:
131
+ raise ValueError("--slippage must be between 0 and 100")
132
+
133
+ effective_slippage = slippage
134
+ slippage_source = "manual"
135
+ if auto_slippage:
136
+ from uniswap_autopilot.swap.extensions.slippage import suggest_slippage
137
+ suggestion = suggest_slippage(chain_name, token_in_name, token_out_name, float(amount))
138
+ effective_slippage = suggestion["recommendedSlippage"]
139
+ slippage_source = f"auto({suggestion['category']})"
140
+
141
+ base_amount = decimal_to_base_units(amount, api_token_in["decimals"])
142
+ response: dict[str, Any] = {
143
+ "action": "trading_api_quote",
144
+ "chain": {"key": chain.key, "chainId": chain.chain_id},
145
+ "tokenIn": token_in,
146
+ "tokenOut": token_out,
147
+ "apiTokenIn": api_token_in,
148
+ "apiTokenOut": api_token_out,
149
+ "wallet": validated_wallet,
150
+ "humanAmount": format(amount, "f"),
151
+ "baseAmount": base_amount,
152
+ "slippage": effective_slippage,
153
+ "slippageSource": slippage_source,
154
+ }
155
+ if dst_chain_name:
156
+ response["dstChain"] = {"key": dst_chain.key, "chainId": dst_chain.chain_id}
157
+ response["crossChain"] = True
158
+
159
+ approval_payload: dict[str, Any] | None = None
160
+ if check_approval:
161
+ if token_in["address"] == "NATIVE":
162
+ response["approvalCheck"] = {
163
+ "skipped": True,
164
+ "reason": "token_in is native, approval is not required",
165
+ }
166
+ else:
167
+ approval_payload = build_approval_payload(
168
+ wallet=validated_wallet,
169
+ chain_id=chain.chain_id,
170
+ api_token_in=api_token_in,
171
+ base_amount=base_amount,
172
+ )
173
+
174
+ quote_payload = build_quote_payload(
175
+ wallet=validated_wallet,
176
+ chain_id=chain.chain_id,
177
+ api_token_in=api_token_in,
178
+ api_token_out=api_token_out,
179
+ base_amount=base_amount,
180
+ swap_type=swap_type,
181
+ slippage=effective_slippage,
182
+ routing_preference=routing_preference,
183
+ dst_chain_id=dst_chain.chain_id if dst_chain_name else None,
184
+ )
185
+ return response, approval_payload, quote_payload
186
+
187
+
188
+ def main() -> None:
189
+ parser = argparse.ArgumentParser(description="调用 Uniswap Trading API 做 quote / approval dry-run")
190
+ add_common_arguments(parser)
191
+ parser.add_argument(
192
+ "--wallet",
193
+ help="用户钱包地址。check-approval 必填;若未提供,则优先回退到 SECURE_WALLET_ADDRESS / HOT_WALLET_ADDRESS",
194
+ )
195
+ parser.add_argument(
196
+ "--swap-type",
197
+ choices=["EXACT_INPUT", "EXACT_OUTPUT"],
198
+ default="EXACT_INPUT",
199
+ help="Trading API quote type",
200
+ )
201
+ parser.add_argument(
202
+ "--slippage",
203
+ type=float,
204
+ default=0.5,
205
+ help="滑点百分比,默认 0.5",
206
+ )
207
+ parser.add_argument(
208
+ "--routing-preference",
209
+ default="BEST_PRICE",
210
+ help="如 BEST_PRICE / FASTEST / CLASSIC",
211
+ )
212
+ parser.add_argument(
213
+ "--auto-slippage",
214
+ action="store_true",
215
+ help="Automatically suggest slippage based on token pair and liquidity",
216
+ )
217
+ parser.add_argument(
218
+ "--dst-chain",
219
+ help="Destination chain for cross-chain swap (bridge)",
220
+ )
221
+ parser.add_argument(
222
+ "--check-approval",
223
+ action="store_true",
224
+ help="先调用 /check_approval",
225
+ )
226
+ parser.add_argument(
227
+ "--request-only",
228
+ action="store_true",
229
+ help="只打印将发送的请求 payload,不实际调用 Trading API",
230
+ )
231
+ parser.add_argument("--output", help="把完整 JSON 输出写入文件")
232
+ args = parser.parse_args()
233
+
234
+ try:
235
+ load_local_env()
236
+ response, approval_payload, quote_payload = prepare_quote_request_data(
237
+ chain_name=args.chain,
238
+ token_in_name=args.token_in,
239
+ token_out_name=args.token_out,
240
+ amount_value=args.amount,
241
+ wallet=args.wallet,
242
+ token_in_decimals=args.token_in_decimals,
243
+ token_out_decimals=args.token_out_decimals,
244
+ swap_type=args.swap_type,
245
+ slippage=args.slippage,
246
+ routing_preference=args.routing_preference,
247
+ check_approval=args.check_approval,
248
+ auto_slippage=args.auto_slippage,
249
+ dst_chain_name=args.dst_chain,
250
+ )
251
+ response["requestOnly"] = args.request_only
252
+ response["requestPayloads"] = {
253
+ "checkApproval": approval_payload,
254
+ "quote": quote_payload,
255
+ }
256
+
257
+ if not args.request_only:
258
+ api_key = require_api_key()
259
+ if approval_payload is not None:
260
+ response["approvalCheck"] = post_json("check_approval", approval_payload, api_key)
261
+ raw_quote = post_json("quote", quote_payload, api_key)
262
+ response["quoteSummary"] = summarize_quote(raw_quote)
263
+ pi_warning = (response["quoteSummary"] or {}).get("priceImpactWarning")
264
+ if pi_warning:
265
+ pi_val = response["quoteSummary"].get("priceImpact", "?")
266
+ print(f"WARNING: price impact {pi_val}% ({pi_warning})", file=sys.stderr)
267
+ response["rawQuote"] = raw_quote
268
+
269
+ if args.output:
270
+ Path(args.output).write_text(
271
+ json.dumps(response, ensure_ascii=False, indent=2) + "\n",
272
+ encoding="utf-8",
273
+ )
274
+
275
+ dump_json(response)
276
+ except Exception as exc: # noqa: BLE001 - CLI should return readable errors
277
+ print(json.dumps({"error": str(exc)}, ensure_ascii=False, indent=2), file=sys.stderr)
278
+ sys.exit(1)
279
+
280
+
281
+ if __name__ == "__main__":
282
+ main()
@@ -0,0 +1,248 @@
1
+ #!/usr/bin/env python3
2
+ from __future__ import annotations
3
+
4
+ import argparse
5
+ import json
6
+ import re
7
+ import sys
8
+ from pathlib import Path
9
+ from typing import Any
10
+
11
+
12
+ from uniswap_autopilot.common.common import dump_json, load_local_env, post_json, require_api_key
13
+
14
+ UNISWAPX_ROUTINGS = {"DUTCH_V2", "DUTCH_V3", "PRIORITY"}
15
+ HEX_RE = re.compile(r"^0x[0-9a-fA-F]+$")
16
+ DOMAIN_FIELD_TYPES = {
17
+ "name": "string",
18
+ "version": "string",
19
+ "chainId": "uint256",
20
+ "verifyingContract": "address",
21
+ "salt": "bytes32",
22
+ }
23
+
24
+
25
+ def load_quote_payload(path: str) -> dict[str, Any]:
26
+ data = json.loads(Path(path).read_text(encoding="utf-8"))
27
+ raw_quote = data.get("rawQuote")
28
+ if not raw_quote:
29
+ raise ValueError("quote file must contain rawQuote from trading_api_quote.py")
30
+ quote = raw_quote.get("quote")
31
+ if not quote:
32
+ raise ValueError("rawQuote.quote is missing")
33
+ return data
34
+
35
+
36
+ def is_uniswapx_route(raw_quote: dict[str, Any]) -> bool:
37
+ return str(raw_quote.get("routing") or "") in UNISWAPX_ROUTINGS
38
+
39
+
40
+ def infer_primary_type(types: dict[str, Any]) -> str:
41
+ if not types:
42
+ raise ValueError("permitData.types is missing")
43
+
44
+ referenced: set[str] = set()
45
+ for fields in types.values():
46
+ for field in fields or []:
47
+ field_type = field.get("type")
48
+ if isinstance(field_type, str) and field_type in types:
49
+ referenced.add(field_type)
50
+
51
+ candidates = [type_name for type_name in types if type_name not in referenced]
52
+ if len(candidates) == 1:
53
+ return candidates[0]
54
+ if "PermitSingle" in types:
55
+ return "PermitSingle"
56
+ raise ValueError("could not infer primaryType from permitData.types")
57
+
58
+
59
+ def build_eip712_domain_types(domain: dict[str, Any]) -> list[dict[str, str]]:
60
+ fields: list[dict[str, str]] = []
61
+ for name in domain:
62
+ field_type = DOMAIN_FIELD_TYPES.get(name)
63
+ if not field_type:
64
+ raise ValueError(f"unsupported EIP-712 domain field: {name}")
65
+ fields.append({"name": name, "type": field_type})
66
+ return fields
67
+
68
+
69
+ def normalize_permit_typed_data(permit_data: dict[str, Any]) -> dict[str, Any]:
70
+ domain = permit_data.get("domain")
71
+ types = permit_data.get("types")
72
+ values = permit_data.get("values")
73
+ if not isinstance(domain, dict) or not isinstance(types, dict) or not isinstance(values, dict):
74
+ raise ValueError("permitData must contain domain, types, and values")
75
+
76
+ normalized_types = {type_name: fields for type_name, fields in types.items()}
77
+ if "EIP712Domain" not in normalized_types:
78
+ normalized_types["EIP712Domain"] = build_eip712_domain_types(domain)
79
+
80
+ return {
81
+ "types": normalized_types,
82
+ "primaryType": infer_primary_type(types),
83
+ "domain": domain,
84
+ "message": values,
85
+ }
86
+
87
+
88
+ def validate_signature(signature: str) -> str:
89
+ cleaned = signature.strip()
90
+ if not HEX_RE.fullmatch(cleaned):
91
+ raise ValueError("signature must be a 0x-prefixed hex string")
92
+ return cleaned
93
+
94
+
95
+ def load_signature_value(signature: str | None = None, signature_file: str | None = None) -> str | None:
96
+ if signature and signature_file:
97
+ raise ValueError("use only one of --signature or --signature-file")
98
+ if signature:
99
+ return validate_signature(signature)
100
+ if not signature_file:
101
+ return None
102
+
103
+ raw = Path(signature_file).read_text(encoding="utf-8").strip()
104
+ if not raw:
105
+ raise ValueError("signature file is empty")
106
+ if raw.startswith("{"):
107
+ blob = json.loads(raw)
108
+ value = blob.get("signature")
109
+ if not isinstance(value, str):
110
+ raise ValueError("signature json file must contain a string 'signature' field")
111
+ return validate_signature(value)
112
+ return validate_signature(raw)
113
+
114
+
115
+ def build_permit_handoff(
116
+ raw_quote: dict[str, Any],
117
+ quote_file: str | None = None,
118
+ ) -> dict[str, Any]:
119
+ permit_data = raw_quote.get("permitData")
120
+ if not permit_data:
121
+ raise ValueError("quote does not contain permitData")
122
+
123
+ routing = str(raw_quote.get("routing") or "UNKNOWN")
124
+ uniswapx = is_uniswapx_route(raw_quote)
125
+ typed_data = normalize_permit_typed_data(permit_data)
126
+ handoff = {
127
+ "routing": routing,
128
+ "quoteFile": quote_file,
129
+ "signatureRule": {
130
+ "signPermitDataLocally": True,
131
+ "sendSignatureToSwap": True,
132
+ "sendPermitDataToSwap": not uniswapx,
133
+ },
134
+ "permitData": permit_data,
135
+ "typedData": typed_data,
136
+ "instructions": [
137
+ "Use typedData as the canonical EIP-712 JSON for wallet signing.",
138
+ "Save the resulting 0x signature into a text file or a JSON file with a 'signature' field.",
139
+ "Pass that file to swap_dry_run.py via --signature-file to continue /swap.",
140
+ ],
141
+ }
142
+ return handoff
143
+
144
+
145
+ def build_swap_payload(
146
+ raw_quote: dict[str, Any],
147
+ signature: str | None = None,
148
+ simulate_transaction: bool = False,
149
+ refresh_gas_price: bool = False,
150
+ ) -> dict[str, Any]:
151
+ quote = raw_quote.get("quote")
152
+ if not quote:
153
+ raise ValueError("rawQuote.quote is missing")
154
+
155
+ permit_data = raw_quote.get("permitData")
156
+ if permit_data and not signature:
157
+ raise ValueError("quote 返回了 permitData,调用 /swap 前必须提供 --signature")
158
+ if signature and not permit_data:
159
+ raise ValueError("quote 未返回 permitData,不应传入 --signature")
160
+
161
+ payload: dict[str, Any] = {
162
+ "quote": quote,
163
+ "refreshGasPrice": refresh_gas_price,
164
+ "simulateTransaction": simulate_transaction,
165
+ }
166
+ if permit_data:
167
+ payload["signature"] = signature
168
+ if not is_uniswapx_route(raw_quote):
169
+ payload["permitData"] = permit_data
170
+ return payload
171
+
172
+
173
+ def main() -> None:
174
+ parser = argparse.ArgumentParser(description="基于 quote 结果调用 /swap 做 dry-run")
175
+ parser.add_argument("--quote-file", required=True, help="trading_api_quote.py 输出的 JSON 文件路径")
176
+ parser.add_argument("--signature", help="当 quote 返回 permitData 时,传入签名")
177
+ parser.add_argument("--signature-file", help="从文件读取签名;支持纯文本 0x... 或 JSON {'signature': '0x...'}")
178
+ parser.add_argument("--permit-output", help="仅导出 permitData handoff JSON,不调用 /swap")
179
+ parser.add_argument("--typed-data-output", help="导出标准 EIP-712 typed data JSON,便于 cast/钱包签名")
180
+ parser.add_argument(
181
+ "--simulate-transaction",
182
+ action="store_true",
183
+ help="让 /swap 在服务端做 simulateTransaction",
184
+ )
185
+ parser.add_argument(
186
+ "--refresh-gas-price",
187
+ action="store_true",
188
+ help="让 /swap 重新抓 gas price",
189
+ )
190
+ parser.add_argument(
191
+ "--request-only",
192
+ action="store_true",
193
+ help="只打印将发送的 /swap body,不实际调用",
194
+ )
195
+ parser.add_argument("--output", help="把完整 JSON 输出写入文件")
196
+ args = parser.parse_args()
197
+
198
+ try:
199
+ load_local_env()
200
+ quote_blob = load_quote_payload(args.quote_file)
201
+ raw_quote = quote_blob["rawQuote"]
202
+ if args.permit_output or args.typed_data_output:
203
+ handoff = build_permit_handoff(raw_quote=raw_quote, quote_file=args.quote_file)
204
+ if args.permit_output:
205
+ Path(args.permit_output).write_text(
206
+ json.dumps(handoff, ensure_ascii=False, indent=2) + "\n",
207
+ encoding="utf-8",
208
+ )
209
+ if args.typed_data_output:
210
+ Path(args.typed_data_output).write_text(
211
+ json.dumps(handoff["typedData"], ensure_ascii=False, indent=2) + "\n",
212
+ encoding="utf-8",
213
+ )
214
+ dump_json(handoff if args.permit_output else handoff["typedData"])
215
+ return
216
+
217
+ signature = load_signature_value(args.signature, args.signature_file)
218
+ payload = build_swap_payload(
219
+ raw_quote=raw_quote,
220
+ signature=signature,
221
+ simulate_transaction=args.simulate_transaction,
222
+ refresh_gas_price=args.refresh_gas_price,
223
+ )
224
+
225
+ response: dict[str, Any] = {
226
+ "action": "swap_dry_run",
227
+ "requestOnly": args.request_only,
228
+ "requestPayload": payload,
229
+ }
230
+
231
+ if not args.request_only:
232
+ api_key = require_api_key()
233
+ response["swapResponse"] = post_json("swap", payload, api_key)
234
+
235
+ if args.output:
236
+ Path(args.output).write_text(
237
+ json.dumps(response, ensure_ascii=False, indent=2) + "\n",
238
+ encoding="utf-8",
239
+ )
240
+
241
+ dump_json(response)
242
+ except Exception as exc: # noqa: BLE001
243
+ print(json.dumps({"error": str(exc)}, ensure_ascii=False, indent=2), file=sys.stderr)
244
+ sys.exit(1)
245
+
246
+
247
+ if __name__ == "__main__":
248
+ main()