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,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()
|