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,2 @@
1
+ """Internal helpers for the swap trade-flow runner."""
2
+
@@ -0,0 +1,11 @@
1
+ from __future__ import annotations
2
+
3
+ import json
4
+ from pathlib import Path
5
+ from typing import Any
6
+
7
+
8
+ def write_json(path: Path, payload: dict[str, Any]) -> None:
9
+ path.parent.mkdir(parents=True, exist_ok=True)
10
+ path.write_text(json.dumps(payload, ensure_ascii=False, indent=2) + "\n", encoding="utf-8")
11
+
@@ -0,0 +1,50 @@
1
+ from __future__ import annotations
2
+
3
+ import argparse
4
+ from typing import Any
5
+
6
+ from uniswap_autopilot.execute import broadcast as execute_transaction
7
+
8
+
9
+ def maybe_broadcast(
10
+ tx: dict[str, Any],
11
+ explicit_rpc_url: str | None,
12
+ confirm: str | None,
13
+ signer_args_source: argparse.Namespace,
14
+ receipt_confirmations: int,
15
+ use_flashbots: bool = False,
16
+ ) -> dict[str, Any]:
17
+ broadcast = execute_transaction.broadcast_with_backend(
18
+ tx=tx,
19
+ explicit_rpc_url=explicit_rpc_url,
20
+ confirm=confirm,
21
+ signer_args_source=signer_args_source,
22
+ use_flashbots=use_flashbots,
23
+ )
24
+ preflight = execute_transaction.build_preflight_report(
25
+ tx=tx,
26
+ explicit_rpc_url=broadcast["rpcUrl"],
27
+ strict=True,
28
+ )
29
+ if not preflight.get("ok"):
30
+ raise RuntimeError("broadcast preflight failed: " + "; ".join(preflight.get("issues") or []))
31
+ broadcast_result = broadcast["broadcastResult"]
32
+ transaction_hash = execute_transaction.extract_transaction_hash(broadcast_result)
33
+ receipt = execute_transaction.execute_cast_receipt(
34
+ tx_hash=transaction_hash,
35
+ rpc_url=broadcast["rpcUrl"],
36
+ confirmations=receipt_confirmations,
37
+ )
38
+ if not execute_transaction.receipt_succeeded(receipt):
39
+ raise RuntimeError(f"broadcast receipt status is not successful: {receipt.get('status')}")
40
+ response = {
41
+ "commandPreview": broadcast["commandPreview"],
42
+ "preflight": preflight,
43
+ "broadcastResult": broadcast_result,
44
+ "transactionHash": transaction_hash,
45
+ "receipt": receipt,
46
+ "signerBackend": broadcast["signerBackend"],
47
+ }
48
+ if broadcast.get("serviceDecision") is not None:
49
+ response["serviceDecision"] = broadcast["serviceDecision"]
50
+ return response
@@ -0,0 +1,216 @@
1
+ from __future__ import annotations
2
+
3
+ from pathlib import Path
4
+ import time
5
+ from typing import Any
6
+
7
+ from uniswap_autopilot.common.common import normalize_chain
8
+ from uniswap_autopilot.execute import broadcast as execute_transaction
9
+
10
+ NATIVE_CURRENCY_ADDRESS = "0x0000000000000000000000000000000000000000"
11
+ STALE_QUOTE_SECONDS = 600
12
+
13
+
14
+ def append_unique(items: list[str], value: str) -> None:
15
+ if value and value not in items:
16
+ items.append(value)
17
+
18
+
19
+ def parse_intish(value: Any) -> int | None:
20
+ if value is None:
21
+ return None
22
+ if isinstance(value, int):
23
+ return value
24
+ if isinstance(value, str):
25
+ cleaned = value.strip()
26
+ if not cleaned:
27
+ return None
28
+ base = 16 if cleaned.lower().startswith("0x") else 10
29
+ return int(cleaned, base)
30
+ return int(value)
31
+
32
+
33
+ def extract_permit_sig_deadline(raw_quote: dict[str, Any]) -> int | None:
34
+ permit_values = (raw_quote.get("permitData") or {}).get("values") or {}
35
+ return parse_intish(permit_values.get("sigDeadline"))
36
+
37
+
38
+ def quote_age_seconds(path: Path) -> int | None:
39
+ if not path.exists():
40
+ return None
41
+ return max(0, int(time.time() - path.stat().st_mtime))
42
+
43
+
44
+ def classify_swap_failure(
45
+ *,
46
+ error_text: str,
47
+ raw_quote: dict[str, Any],
48
+ wallet: str,
49
+ chain_name: str,
50
+ explicit_rpc_url: str | None,
51
+ quote_path: Path,
52
+ reused_saved_quote: bool,
53
+ has_signature: bool,
54
+ approval_required: bool,
55
+ assume_approval_ready: bool,
56
+ ) -> dict[str, Any]:
57
+ error_lower = error_text.lower()
58
+ category = "swap_request_failed"
59
+ if "transfer_from_failed" in error_lower:
60
+ category = "transfer_from_failed"
61
+ elif "failed_to_estimate_gas" in error_lower or "execution reverted" in error_lower:
62
+ category = "simulation_reverted"
63
+
64
+ issues: list[str] = []
65
+ next_actions: list[str] = []
66
+ checks: dict[str, Any] = {}
67
+
68
+ now = int(time.time())
69
+ sig_deadline = extract_permit_sig_deadline(raw_quote)
70
+ if sig_deadline is not None:
71
+ checks["permitSigDeadline"] = sig_deadline
72
+ checks["permitSigDeadlineExpired"] = sig_deadline <= now
73
+ if sig_deadline <= now:
74
+ category = "permit_signature_expired"
75
+ append_unique(issues, "permitData.sigDeadline has passed")
76
+ append_unique(next_actions, "refresh-quote")
77
+ append_unique(next_actions, "re-sign-permit")
78
+
79
+ if reused_saved_quote:
80
+ age = quote_age_seconds(quote_path)
81
+ checks["quoteAgeSeconds"] = age
82
+ if age is not None and age > STALE_QUOTE_SECONDS:
83
+ append_unique(issues, f"reused quote is stale ({age}s old)")
84
+ append_unique(next_actions, "refresh-quote")
85
+ if has_signature:
86
+ append_unique(next_actions, "re-sign-permit")
87
+
88
+ chain = normalize_chain(chain_name)
89
+ rpc_url, rpc_candidates = execute_transaction.resolve_rpc_url(explicit_rpc_url, chain.chain_id)
90
+ checks["rpcUrlResolved"] = rpc_url
91
+ checks["rpcEnvCandidates"] = rpc_candidates
92
+ if not rpc_url:
93
+ append_unique(issues, "RPC URL is not configured, cannot verify balance/allowance diagnostics")
94
+ return {
95
+ "category": category,
96
+ "issues": issues,
97
+ "nextActions": next_actions,
98
+ "checks": checks,
99
+ }
100
+
101
+ quote_blob = raw_quote.get("quote") or {}
102
+ quote_input = quote_blob.get("input") or {}
103
+ input_token = str(quote_input.get("token") or "")
104
+ input_amount = parse_intish(quote_input.get("amount"))
105
+ gas_fee = parse_intish(quote_blob.get("gasFee")) or 0
106
+ checks["inputToken"] = input_token
107
+ checks["inputAmount"] = str(input_amount) if input_amount is not None else None
108
+ checks["gasFeeFromQuote"] = str(gas_fee)
109
+
110
+ try:
111
+ native_balance = execute_transaction.query_native_balance(wallet, rpc_url)
112
+ checks["nativeBalance"] = str(native_balance)
113
+ if native_balance < gas_fee:
114
+ if category not in {"insufficient_input_balance"}:
115
+ category = "insufficient_native_gas"
116
+ append_unique(issues, f"native balance {native_balance} is below quoted gas fee {gas_fee}")
117
+ append_unique(next_actions, "top-up-native-gas")
118
+ except Exception as exc: # noqa: BLE001
119
+ append_unique(issues, f"failed to query native balance: {exc}")
120
+ return {
121
+ "category": category,
122
+ "issues": issues,
123
+ "nextActions": next_actions,
124
+ "checks": checks,
125
+ }
126
+
127
+ if input_amount is None:
128
+ append_unique(issues, "quote.input.amount is missing")
129
+ return {
130
+ "category": category,
131
+ "issues": issues,
132
+ "nextActions": next_actions,
133
+ "checks": checks,
134
+ }
135
+
136
+ if input_token.lower() == NATIVE_CURRENCY_ADDRESS:
137
+ required_native = input_amount + gas_fee
138
+ checks["requiredNativeForInputAndGas"] = str(required_native)
139
+ if native_balance < required_native:
140
+ category = "insufficient_input_balance"
141
+ append_unique(
142
+ issues,
143
+ f"native balance {native_balance} is below required {required_native} (input + gas)",
144
+ )
145
+ append_unique(next_actions, "top-up-input-token")
146
+ append_unique(next_actions, "reduce-amount")
147
+ return {
148
+ "category": category,
149
+ "issues": issues,
150
+ "nextActions": next_actions,
151
+ "checks": checks,
152
+ }
153
+
154
+ try:
155
+ token_balance = execute_transaction.query_erc20_balance(wallet, input_token, rpc_url)
156
+ checks["inputTokenBalance"] = str(token_balance)
157
+ if token_balance < input_amount:
158
+ category = "insufficient_input_balance"
159
+ append_unique(issues, f"token balance {token_balance} is below required input {input_amount}")
160
+ append_unique(next_actions, "top-up-input-token")
161
+ append_unique(next_actions, "reduce-amount")
162
+ except Exception as exc: # noqa: BLE001
163
+ append_unique(issues, f"failed to query token balance: {exc}")
164
+ return {
165
+ "category": category,
166
+ "issues": issues,
167
+ "nextActions": next_actions,
168
+ "checks": checks,
169
+ }
170
+
171
+ permit_verifier = ((raw_quote.get("permitData") or {}).get("domain") or {}).get("verifyingContract")
172
+ if permit_verifier:
173
+ try:
174
+ allowance = execute_transaction.query_erc20_allowance(input_token, wallet, permit_verifier, rpc_url)
175
+ checks["permitAllowance"] = {
176
+ "spender": permit_verifier.lower(),
177
+ "current": str(allowance),
178
+ "required": str(input_amount),
179
+ "sufficient": allowance >= input_amount,
180
+ }
181
+ if allowance < input_amount:
182
+ category = "insufficient_allowance"
183
+ append_unique(
184
+ issues,
185
+ f"allowance {allowance} is below required input {input_amount} for {permit_verifier.lower()}",
186
+ )
187
+ if approval_required and not assume_approval_ready:
188
+ append_unique(next_actions, "broadcast-approval")
189
+ else:
190
+ append_unique(next_actions, "refresh-quote")
191
+ if has_signature:
192
+ append_unique(next_actions, "re-sign-permit")
193
+ except Exception as exc: # noqa: BLE001
194
+ append_unique(issues, f"failed to query permit allowance: {exc}")
195
+
196
+ return {
197
+ "category": category,
198
+ "issues": issues,
199
+ "nextActions": next_actions,
200
+ "checks": checks,
201
+ }
202
+
203
+
204
+ def build_swap_failure_message(error_text: str, diagnosis: dict[str, Any]) -> str:
205
+ parts = [f"swap dry-run failed ({diagnosis.get('category')})"]
206
+ issues = diagnosis.get("issues") or []
207
+ if issues:
208
+ parts.append("issues: " + "; ".join(issues))
209
+ actions = diagnosis.get("nextActions") or []
210
+ if actions:
211
+ parts.append("next: " + ", ".join(actions))
212
+ headline = error_text.splitlines()[0].strip()
213
+ if len(headline) > 220:
214
+ headline = headline[:220] + "..."
215
+ parts.append(f"upstream: {headline}")
216
+ return " | ".join(parts)
@@ -0,0 +1,113 @@
1
+ from __future__ import annotations
2
+
3
+ from datetime import datetime, timezone
4
+ import json
5
+ from pathlib import Path
6
+ from typing import Any
7
+ from uuid import uuid4
8
+
9
+ from uniswap_autopilot.swap.trading_api import quote as trading_api_quote
10
+
11
+
12
+ def append_jsonl(path: Path, payload: dict[str, Any]) -> None:
13
+ path.parent.mkdir(parents=True, exist_ok=True)
14
+ with path.open("a", encoding="utf-8") as handle:
15
+ handle.write(json.dumps(payload, ensure_ascii=False) + "\n")
16
+
17
+
18
+ def build_paper_trade_entry_id() -> str:
19
+ timestamp = datetime.now(timezone.utc).strftime("%Y%m%dT%H%M%SZ")
20
+ return f"{timestamp}-{uuid4().hex[:8]}"
21
+
22
+
23
+ def build_paper_trade_paths(output_dir: str, journal_file: str | None) -> tuple[str, Path, Path]:
24
+ entry_id = build_paper_trade_entry_id()
25
+ output_root = Path(output_dir)
26
+ run_output_dir = output_root / "runs" / entry_id
27
+ journal_path = Path(journal_file) if journal_file else output_root / "paper-trade-journal.jsonl"
28
+ return entry_id, run_output_dir, journal_path
29
+
30
+
31
+ def build_paper_trade_info(
32
+ *,
33
+ entry_id: str,
34
+ journal_path: Path,
35
+ run_output_dir: Path,
36
+ status: str,
37
+ response: dict[str, Any] | None,
38
+ error: str | None = None,
39
+ ) -> dict[str, Any]:
40
+ swap = (response or {}).get("swap") or {}
41
+ info: dict[str, Any] = {
42
+ "enabled": True,
43
+ "status": status,
44
+ "entryId": entry_id,
45
+ "journalFile": str(journal_path),
46
+ "runOutputDir": str(run_output_dir),
47
+ "recordedAt": datetime.now(timezone.utc).isoformat(),
48
+ "swapPreviewSource": "quote-only" if swap.get("quoteOnly") else "swap-response",
49
+ }
50
+ if error is not None:
51
+ info["error"] = error
52
+ return info
53
+
54
+
55
+ def build_paper_trade_journal_entry(
56
+ *,
57
+ entry_id: str,
58
+ journal_status: str,
59
+ run_output_dir: Path,
60
+ request_context: dict[str, Any],
61
+ response: dict[str, Any] | None,
62
+ error: str | None = None,
63
+ ) -> dict[str, Any]:
64
+ entry: dict[str, Any] = {
65
+ "entryId": entry_id,
66
+ "recordedAt": datetime.now(timezone.utc).isoformat(),
67
+ "mode": "paper-trade",
68
+ "status": journal_status,
69
+ "runOutputDir": str(run_output_dir),
70
+ "requestedTrade": request_context,
71
+ }
72
+ payload = response or {}
73
+ for field in (
74
+ "action",
75
+ "inputs",
76
+ "files",
77
+ "quote",
78
+ "automation",
79
+ "policyCheck",
80
+ "approval",
81
+ "permit",
82
+ "swap",
83
+ "swapFailureDiagnosis",
84
+ "nextActions",
85
+ ):
86
+ if field in payload:
87
+ entry[field] = payload[field]
88
+ if error is not None:
89
+ entry["error"] = error
90
+ return entry
91
+
92
+
93
+ def build_quote_only_paper_swap(raw_quote: dict[str, Any], reason: str) -> dict[str, Any]:
94
+ quote = raw_quote.get("quote") or {}
95
+ return {
96
+ "paperOnly": True,
97
+ "quoteOnly": True,
98
+ "paperOnlyReason": reason,
99
+ "simulationRequested": False,
100
+ "simulationUsed": False,
101
+ "simulationFallbackReason": None,
102
+ "broadcastRequested": False,
103
+ "autoBroadcast": False,
104
+ "quoteSummary": trading_api_quote.summarize_quote(raw_quote),
105
+ "quoteGasFee": quote.get("gasFee"),
106
+ "quoteGasFeeUSD": quote.get("gasFeeUSD"),
107
+ "quoteGasUseEstimate": quote.get("gasUseEstimate"),
108
+ "preflight": {
109
+ "checked": False,
110
+ "ok": None,
111
+ "reason": reason,
112
+ },
113
+ }
@@ -0,0 +1,142 @@
1
+ from __future__ import annotations
2
+
3
+ import json
4
+ from pathlib import Path
5
+ from typing import Any
6
+
7
+ from uniswap_autopilot.common.common import normalize_chain, parse_amount, resolve_token
8
+
9
+
10
+ def normalize_policy_token(chain_name: str, token_name: str) -> str:
11
+ chain = normalize_chain(chain_name)
12
+ token = resolve_token(chain, token_name)
13
+ if token["address"] == "NATIVE":
14
+ return "NATIVE"
15
+ return str(token["address"]).lower()
16
+
17
+
18
+ def load_auto_trade_policy(path: str) -> dict[str, Any]:
19
+ blob = json.loads(Path(path).read_text(encoding="utf-8"))
20
+ if not isinstance(blob, dict):
21
+ raise ValueError("auto-trade policy must be a JSON object")
22
+
23
+ enabled = blob.get("enabled")
24
+ if not isinstance(enabled, bool):
25
+ raise ValueError("auto-trade policy.enabled must be a boolean")
26
+
27
+ allowed_chains_raw = blob.get("allowedChains") or []
28
+ if not isinstance(allowed_chains_raw, list) or not all(isinstance(item, str) for item in allowed_chains_raw):
29
+ raise ValueError("auto-trade policy.allowedChains must be a string array")
30
+ allowed_chains = [normalize_chain(item).key for item in allowed_chains_raw]
31
+
32
+ allowed_pairs_raw = blob.get("allowedPairs")
33
+ if not isinstance(allowed_pairs_raw, list) or not allowed_pairs_raw:
34
+ raise ValueError("auto-trade policy.allowedPairs must be a non-empty array")
35
+
36
+ allowed_pairs: list[dict[str, Any]] = []
37
+ for index, raw_rule in enumerate(allowed_pairs_raw):
38
+ if not isinstance(raw_rule, dict):
39
+ raise ValueError(f"auto-trade policy.allowedPairs[{index}] must be an object")
40
+ chain_name = raw_rule.get("chain")
41
+ token_in = raw_rule.get("tokenIn")
42
+ token_out = raw_rule.get("tokenOut")
43
+ if not all(isinstance(value, str) and value.strip() for value in (chain_name, token_in, token_out)):
44
+ raise ValueError(
45
+ f"auto-trade policy.allowedPairs[{index}] must include non-empty chain/tokenIn/tokenOut"
46
+ )
47
+ normalized_chain = normalize_chain(chain_name).key
48
+ normalized_rule = {
49
+ "chain": normalized_chain,
50
+ "tokenIn": normalize_policy_token(normalized_chain, token_in),
51
+ "tokenOut": normalize_policy_token(normalized_chain, token_out),
52
+ "tokenInLabel": token_in,
53
+ "tokenOutLabel": token_out,
54
+ "allowAutoSignPermit": bool(raw_rule.get("allowAutoSignPermit", False)),
55
+ "allowAutoApproval": bool(raw_rule.get("allowAutoApproval", False)),
56
+ "allowAutoBroadcastSwap": bool(raw_rule.get("allowAutoBroadcastSwap", False)),
57
+ }
58
+ if "maxAmount" in raw_rule:
59
+ if not isinstance(raw_rule["maxAmount"], str):
60
+ raise ValueError(f"auto-trade policy.allowedPairs[{index}].maxAmount must be a string")
61
+ normalized_rule["maxAmount"] = str(parse_amount(raw_rule["maxAmount"]))
62
+ if "maxSlippage" in raw_rule:
63
+ max_slippage = raw_rule["maxSlippage"]
64
+ if not isinstance(max_slippage, (int, float)):
65
+ raise ValueError(f"auto-trade policy.allowedPairs[{index}].maxSlippage must be numeric")
66
+ if max_slippage < 0 or max_slippage > 100:
67
+ raise ValueError(f"auto-trade policy.allowedPairs[{index}].maxSlippage must be between 0 and 100")
68
+ normalized_rule["maxSlippage"] = float(max_slippage)
69
+ allowed_pairs.append(normalized_rule)
70
+
71
+ require_preflight_ok = blob.get("requirePreflightOk", True)
72
+ if not isinstance(require_preflight_ok, bool):
73
+ raise ValueError("auto-trade policy.requirePreflightOk must be a boolean")
74
+
75
+ return {
76
+ "enabled": enabled,
77
+ "allowedChains": allowed_chains,
78
+ "allowedPairs": allowed_pairs,
79
+ "requirePreflightOk": require_preflight_ok,
80
+ "sourceFile": path,
81
+ }
82
+
83
+
84
+ def evaluate_auto_trade_policy(
85
+ policy: dict[str, Any],
86
+ chain_name: str,
87
+ token_in_name: str,
88
+ token_out_name: str,
89
+ amount_value: str,
90
+ slippage: float,
91
+ ) -> dict[str, Any]:
92
+ chain_key = normalize_chain(chain_name).key
93
+ requested_token_in = normalize_policy_token(chain_key, token_in_name)
94
+ requested_token_out = normalize_policy_token(chain_key, token_out_name)
95
+ requested_amount = parse_amount(amount_value)
96
+
97
+ issues: list[str] = []
98
+ matched_rule: dict[str, Any] | None = None
99
+
100
+ if not policy["enabled"]:
101
+ issues.append("policy is disabled")
102
+
103
+ if policy["allowedChains"] and chain_key not in policy["allowedChains"]:
104
+ issues.append(f"chain '{chain_key}' is not allowed")
105
+
106
+ for rule in policy["allowedPairs"]:
107
+ if (
108
+ rule["chain"] == chain_key
109
+ and rule["tokenIn"] == requested_token_in
110
+ and rule["tokenOut"] == requested_token_out
111
+ ):
112
+ matched_rule = rule
113
+ break
114
+ if matched_rule is None:
115
+ issues.append("token pair is not allowed")
116
+ else:
117
+ if "maxAmount" in matched_rule and requested_amount > parse_amount(matched_rule["maxAmount"]):
118
+ issues.append(
119
+ f"amount {format(requested_amount, 'f')} exceeds maxAmount {matched_rule['maxAmount']}"
120
+ )
121
+ if "maxSlippage" in matched_rule and slippage > float(matched_rule["maxSlippage"]):
122
+ issues.append(
123
+ f"slippage {slippage} exceeds maxSlippage {matched_rule['maxSlippage']}"
124
+ )
125
+
126
+ return {
127
+ "policyFile": policy["sourceFile"],
128
+ "allowed": len(issues) == 0,
129
+ "chain": chain_key,
130
+ "tokenIn": requested_token_in,
131
+ "tokenOut": requested_token_out,
132
+ "amount": format(requested_amount, "f"),
133
+ "slippage": slippage,
134
+ "requirePreflightOk": policy["requirePreflightOk"],
135
+ "matchedRule": matched_rule,
136
+ "issues": issues,
137
+ }
138
+
139
+
140
+ def policy_rule_allows(policy_check: dict[str, Any] | None, field_name: str) -> bool:
141
+ return bool((policy_check or {}).get("matchedRule", {}).get(field_name))
142
+
@@ -0,0 +1,2 @@
1
+ """Deep-link helpers for swap entry points."""
2
+
@@ -0,0 +1,69 @@
1
+ #!/usr/bin/env python3
2
+ from __future__ import annotations
3
+
4
+ import argparse
5
+
6
+
7
+ from uniswap_autopilot.common.common import (
8
+ add_common_arguments,
9
+ build_swap_link,
10
+ dump_json,
11
+ normalize_chain,
12
+ override_decimals,
13
+ parse_amount,
14
+ resolve_token,
15
+ )
16
+
17
+
18
+ def build_swap_link_response(
19
+ chain_name: str,
20
+ token_in_name: str,
21
+ token_out_name: str,
22
+ amount_value: str,
23
+ field: str = "INPUT",
24
+ token_in_decimals: int | None = None,
25
+ token_out_decimals: int | None = None,
26
+ ) -> dict[str, object]:
27
+ chain = normalize_chain(chain_name)
28
+ amount = parse_amount(amount_value)
29
+ token_in = override_decimals(resolve_token(chain, token_in_name), token_in_decimals)
30
+ token_out = override_decimals(resolve_token(chain, token_out_name), token_out_decimals)
31
+ human_amount = format(amount, "f")
32
+ link = build_swap_link(chain, token_in, token_out, human_amount, field)
33
+ return {
34
+ "action": "build_swap_link",
35
+ "chain": {"key": chain.key, "chainId": chain.chain_id},
36
+ "tokenIn": token_in,
37
+ "tokenOut": token_out,
38
+ "amount": human_amount,
39
+ "field": field,
40
+ "deepLink": link,
41
+ }
42
+
43
+
44
+ def main() -> None:
45
+ parser = argparse.ArgumentParser(description="生成 Uniswap swap deep link")
46
+ add_common_arguments(parser)
47
+ parser.add_argument(
48
+ "--field",
49
+ choices=["INPUT", "OUTPUT"],
50
+ default="INPUT",
51
+ help="value 对应输入还是输出字段",
52
+ )
53
+ args = parser.parse_args()
54
+
55
+ dump_json(
56
+ build_swap_link_response(
57
+ chain_name=args.chain,
58
+ token_in_name=args.token_in,
59
+ token_out_name=args.token_out,
60
+ amount_value=args.amount,
61
+ field=args.field,
62
+ token_in_decimals=args.token_in_decimals,
63
+ token_out_decimals=args.token_out_decimals,
64
+ )
65
+ )
66
+
67
+
68
+ if __name__ == "__main__":
69
+ main()
@@ -0,0 +1,41 @@
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
+
9
+
10
+ from uniswap_autopilot.common.common import dump_json, load_local_env
11
+ from uniswap_autopilot.swap.trading_api.swap import build_permit_handoff, load_quote_payload
12
+
13
+
14
+ def main() -> None:
15
+ parser = argparse.ArgumentParser(description="从 quote 结果导出 permitData 签名包")
16
+ parser.add_argument("--quote-file", required=True, help="trading_api_quote.py 输出的 JSON 文件路径")
17
+ parser.add_argument("--output", required=True, help="导出的 permitData handoff JSON 路径")
18
+ parser.add_argument("--typed-data-output", help="额外导出标准 EIP-712 typed data JSON 路径")
19
+ args = parser.parse_args()
20
+
21
+ try:
22
+ load_local_env()
23
+ quote_blob = load_quote_payload(args.quote_file)
24
+ handoff = build_permit_handoff(raw_quote=quote_blob["rawQuote"], quote_file=args.quote_file)
25
+ Path(args.output).write_text(
26
+ json.dumps(handoff, ensure_ascii=False, indent=2) + "\n",
27
+ encoding="utf-8",
28
+ )
29
+ if args.typed_data_output:
30
+ Path(args.typed_data_output).write_text(
31
+ json.dumps(handoff["typedData"], ensure_ascii=False, indent=2) + "\n",
32
+ encoding="utf-8",
33
+ )
34
+ dump_json(handoff)
35
+ except Exception as exc: # noqa: BLE001
36
+ print(json.dumps({"error": str(exc)}, ensure_ascii=False, indent=2), file=sys.stderr)
37
+ sys.exit(1)
38
+
39
+
40
+ if __name__ == "__main__":
41
+ main()