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