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,370 @@
1
+ from __future__ import annotations
2
+
3
+ import json
4
+ from decimal import Decimal
5
+ from pathlib import Path
6
+ from typing import Any
7
+
8
+ from uniswap_autopilot.common.common import validate_address
9
+ from uniswap_autopilot.execute._internal.constants import APPROVE_SELECTOR, CHAIN_BY_ID, HEX_DATA_RE
10
+ from uniswap_autopilot.execute._internal.rpc import resolve_rpc_url
11
+
12
+ def load_json(path: str) -> dict[str, Any]:
13
+ return json.loads(Path(path).read_text(encoding="utf-8"))
14
+
15
+ def parse_intish(value: Any, field_name: str) -> int | None:
16
+ if value in (None, "", "null"):
17
+ return None
18
+ if isinstance(value, bool):
19
+ raise ValueError(f"{field_name} must not be boolean")
20
+ if isinstance(value, int):
21
+ if value < 0:
22
+ raise ValueError(f"{field_name} must be non-negative")
23
+ return value
24
+ if isinstance(value, str):
25
+ cleaned = value.strip()
26
+ if not cleaned:
27
+ return None
28
+ try:
29
+ parsed = int(cleaned, 0)
30
+ except ValueError as exc:
31
+ raise ValueError(f"{field_name} must be an int or hex string") from exc
32
+ if parsed < 0:
33
+ raise ValueError(f"{field_name} must be non-negative")
34
+ return parsed
35
+ raise ValueError(f"{field_name} must be an int or string")
36
+
37
+ def validate_hex_data(value: Any, field_name: str = "data") -> str:
38
+ if not isinstance(value, str) or not HEX_DATA_RE.fullmatch(value):
39
+ raise ValueError(f"{field_name} must be a 0x-prefixed hex string")
40
+ if value == "0x":
41
+ raise ValueError(f"{field_name} must not be empty")
42
+ return value
43
+
44
+ def optional_address(value: Any) -> str | None:
45
+ if value in (None, "", "null"):
46
+ return None
47
+ try:
48
+ return validate_address(value, "address")
49
+ except ValueError:
50
+ return None
51
+
52
+ def lookup_token_symbol(chain_id: int, address: str | None) -> str | None:
53
+ if not address:
54
+ return None
55
+ chain = CHAIN_BY_ID.get(chain_id)
56
+ if not chain:
57
+ return None
58
+ normalized = address.lower()
59
+ for symbol, token in chain.tokens.items():
60
+ if token.address.lower() == normalized:
61
+ return symbol
62
+ return None
63
+
64
+ def lookup_token_decimals(
65
+ chain_id: int,
66
+ token_symbol: str | None = None,
67
+ token_address: str | None = None,
68
+ ) -> int:
69
+ chain = CHAIN_BY_ID.get(chain_id)
70
+ if not chain:
71
+ return 18
72
+ if token_symbol == "NATIVE":
73
+ return 18
74
+ if token_symbol:
75
+ token = chain.tokens.get(str(token_symbol).upper())
76
+ if token:
77
+ return token.decimals
78
+ if token_address:
79
+ normalized = token_address.lower()
80
+ for token in chain.tokens.values():
81
+ if token.address.lower() == normalized:
82
+ return token.decimals
83
+ return 18
84
+
85
+ def base_units_to_human(amount: Any, decimals: int) -> str:
86
+ raw = parse_intish(amount, "amount")
87
+ if raw is None:
88
+ return "0"
89
+ scaled = Decimal(raw) / (Decimal(10) ** decimals)
90
+ text = format(scaled, "f")
91
+ if "." in text:
92
+ text = text.rstrip("0").rstrip(".")
93
+ return text or "0"
94
+
95
+ def decode_erc20_approve_call(data: str) -> dict[str, str] | None:
96
+ validated = validate_hex_data(data, "data")
97
+ if not validated.startswith(APPROVE_SELECTOR):
98
+ return None
99
+ payload = validated[10:]
100
+ if len(payload) < 128:
101
+ return None
102
+ spender_word = payload[:64]
103
+ amount_word = payload[64:128]
104
+ spender = optional_address(f"0x{spender_word[-40:]}")
105
+ if not spender:
106
+ return None
107
+ return {
108
+ "spender": spender,
109
+ "amount": str(int(amount_word, 16)),
110
+ }
111
+
112
+ def normalize_transaction(
113
+ tx: dict[str, Any],
114
+ tx_kind: str,
115
+ source_file: str,
116
+ source_kind: str,
117
+ native_input: bool = False,
118
+ ) -> dict[str, Any]:
119
+ if not isinstance(tx, dict):
120
+ raise ValueError("transaction payload must be an object")
121
+
122
+ chain_id = parse_intish(tx.get("chainId"), "chainId")
123
+ if chain_id is None:
124
+ raise ValueError("chainId is required")
125
+
126
+ gas_limit = parse_intish(tx.get("gasLimit"), "gasLimit")
127
+ gas_price = parse_intish(tx.get("gasPrice"), "gasPrice")
128
+ value_int = parse_intish(tx.get("value"), "value")
129
+ normalized = {
130
+ "kind": tx_kind,
131
+ "sourceKind": source_kind,
132
+ "sourceFile": source_file,
133
+ "chainId": chain_id,
134
+ "chainKey": (CHAIN_BY_ID.get(chain_id).key if chain_id in CHAIN_BY_ID else None),
135
+ "to": validate_address(tx.get("to"), "to"),
136
+ "from": validate_address(tx.get("from"), "from") if tx.get("from") else None,
137
+ "data": validate_hex_data(tx.get("data"), "data"),
138
+ "value": str(value_int or 0),
139
+ "gasLimit": (str(gas_limit) if gas_limit is not None else None),
140
+ "gasPrice": (str(gas_price) if gas_price is not None else None),
141
+ "nativeInput": native_input,
142
+ }
143
+ if native_input and value_int == 0:
144
+ raise ValueError("native-input swap requires non-zero tx.value")
145
+ return normalized
146
+
147
+ def load_swap_transaction(path: str) -> dict[str, Any]:
148
+ blob = load_json(path)
149
+ swap_response = blob.get("swapResponse") or {}
150
+ swap = swap_response.get("swap")
151
+ if not swap:
152
+ raise ValueError("swap file must contain swapResponse.swap")
153
+ request_payload = blob.get("requestPayload") or {}
154
+ quote = request_payload.get("quote") or {}
155
+ input_token = (quote.get("input") or {}).get("token")
156
+ native_input = str(input_token or "").lower() == "0x0000000000000000000000000000000000000000"
157
+ normalized = normalize_transaction(
158
+ tx=swap,
159
+ tx_kind="swap",
160
+ source_file=path,
161
+ source_kind="swap-file",
162
+ native_input=native_input,
163
+ )
164
+ normalized["requestId"] = swap_response.get("requestId")
165
+ normalized["gasFee"] = swap_response.get("gasFee")
166
+ normalized["apiSignature"] = swap_response.get("signature")
167
+ quote_input = quote.get("input") or {}
168
+ quote_output = quote.get("output") or {}
169
+ normalized["inputToken"] = optional_address(quote_input.get("token"))
170
+ normalized["inputTokenSymbol"] = (
171
+ "NATIVE"
172
+ if native_input
173
+ else lookup_token_symbol(chain_id=normalized["chainId"], address=normalized["inputToken"])
174
+ )
175
+ normalized["inputAmount"] = (
176
+ str(parse_intish(quote_input.get("amount"), "quote.input.amount"))
177
+ if quote_input.get("amount") not in (None, "", "null")
178
+ else None
179
+ )
180
+ normalized["outputToken"] = optional_address(quote_output.get("token"))
181
+ normalized["outputTokenSymbol"] = lookup_token_symbol(
182
+ chain_id=normalized["chainId"],
183
+ address=normalized["outputToken"],
184
+ )
185
+ normalized["outputAmount"] = (
186
+ str(parse_intish(quote_output.get("amount"), "quote.output.amount"))
187
+ if quote_output.get("amount") not in (None, "", "null")
188
+ else None
189
+ )
190
+ permit_data = request_payload.get("permitData") or {}
191
+ permit_domain = permit_data.get("domain") or {}
192
+ permit_values = permit_data.get("values") or {}
193
+ normalized["permitVerifier"] = optional_address(permit_domain.get("verifyingContract"))
194
+ normalized["permitSpender"] = optional_address(permit_values.get("spender"))
195
+ return normalized
196
+
197
+ def load_approval_transaction(path: str) -> dict[str, Any]:
198
+ blob = load_json(path)
199
+ approval_check = blob.get("approvalCheck") or {}
200
+ approval = approval_check.get("approval")
201
+ if not approval:
202
+ raise ValueError("quote file must contain approvalCheck.approval")
203
+ normalized = normalize_transaction(
204
+ tx=approval,
205
+ tx_kind="approval",
206
+ source_file=path,
207
+ source_kind="quote-file",
208
+ )
209
+ normalized["requestId"] = approval_check.get("requestId")
210
+ request_payload = ((blob.get("requestPayloads") or {}).get("checkApproval")) or {}
211
+ decoded_approval = decode_erc20_approve_call(normalized["data"])
212
+ normalized["approvalToken"] = normalized["to"]
213
+ normalized["approvalSpender"] = (
214
+ decoded_approval.get("spender") if decoded_approval else None
215
+ )
216
+ normalized["approvalAmount"] = (
217
+ decoded_approval.get("amount") if decoded_approval else None
218
+ )
219
+ normalized["requiredAllowance"] = (
220
+ str(parse_intish(request_payload.get("amount"), "checkApproval.amount"))
221
+ if request_payload.get("amount") not in (None, "", "null")
222
+ else normalized["approvalAmount"]
223
+ )
224
+ raw_quote = blob.get("rawQuote") or {}
225
+ quote = raw_quote.get("quote") or {}
226
+ quote_input = quote.get("input") or {}
227
+ quote_output = quote.get("output") or {}
228
+ quote_input_meta = blob.get("tokenIn") or {}
229
+ quote_output_meta = blob.get("tokenOut") or {}
230
+ normalized["inputToken"] = optional_address(quote_input.get("token")) or normalized["approvalToken"]
231
+ normalized["inputTokenSymbol"] = (
232
+ quote_input_meta.get("symbol")
233
+ or lookup_token_symbol(chain_id=normalized["chainId"], address=normalized["inputToken"])
234
+ )
235
+ normalized["inputAmount"] = (
236
+ str(parse_intish(quote_input.get("amount"), "quote.input.amount"))
237
+ if quote_input.get("amount") not in (None, "", "null")
238
+ else normalized["requiredAllowance"]
239
+ )
240
+ normalized["outputToken"] = optional_address(quote_output.get("token"))
241
+ normalized["outputTokenSymbol"] = (
242
+ quote_output_meta.get("symbol")
243
+ or lookup_token_symbol(chain_id=normalized["chainId"], address=normalized["outputToken"])
244
+ )
245
+ normalized["outputAmount"] = (
246
+ str(parse_intish(quote_output.get("amount"), "quote.output.amount"))
247
+ if quote_output.get("amount") not in (None, "", "null")
248
+ else None
249
+ )
250
+ return normalized
251
+
252
+ def build_confirmation_phrase(tx: dict[str, Any]) -> str:
253
+ return f"BROADCAST {tx['kind'].upper()} {tx['chainId']} {tx['to'].lower()}"
254
+
255
+ def summarize_transaction(tx: dict[str, Any], rpc_url: str | None, rpc_candidates: list[str]) -> dict[str, Any]:
256
+ summary = {
257
+ "kind": tx["kind"],
258
+ "chain": {"chainId": tx["chainId"], "key": tx["chainKey"]},
259
+ "from": tx["from"],
260
+ "to": tx["to"],
261
+ "value": tx["value"],
262
+ "gasLimit": tx["gasLimit"],
263
+ "gasPrice": tx["gasPrice"],
264
+ "dataBytes": (len(tx["data"]) - 2) // 2,
265
+ "sourceKind": tx["sourceKind"],
266
+ "sourceFile": tx["sourceFile"],
267
+ "rpcUrlResolved": rpc_url,
268
+ "rpcEnvCandidates": rpc_candidates,
269
+ "requestId": tx.get("requestId"),
270
+ "confirmation": build_confirmation_phrase(tx),
271
+ }
272
+ if tx["kind"] == "swap":
273
+ summary["nativeInput"] = tx["nativeInput"]
274
+ summary["gasFee"] = tx.get("gasFee")
275
+ return summary
276
+
277
+ def build_execute_preview(tx: dict[str, Any], explicit_rpc_url: str | None = None) -> dict[str, Any]:
278
+ rpc_url, rpc_candidates = resolve_rpc_url(explicit_rpc_url, tx["chainId"])
279
+ summary = summarize_transaction(tx, rpc_url, rpc_candidates)
280
+ preview_rpc_url = rpc_url or "<unresolved-rpc-url>"
281
+ return {
282
+ "summary": summary,
283
+ "commandPreview": f"pure_signer.sign_transaction + eth_sendRawTransaction (rpc={preview_rpc_url})",
284
+ }
285
+
286
+
287
+ # Price feed integration
288
+
289
+
290
+ def _get_price_feed():
291
+ """Lazy import to avoid circular deps."""
292
+ from price_feed import get_price, get_prices_batch, get_eth_price
293
+ return get_price, get_prices_batch, get_eth_price
294
+
295
+
296
+ def estimate_trade_value_usd(summary: dict) -> float:
297
+ """Estimate trade value in USD from transaction summary.
298
+
299
+ Strategy (priority order):
300
+ 1. Stablecoin side → quote 金额即 USD
301
+ 2. ETH pair → quote ETH 数量 × 实时 ETH 价格(price-feed 多源)
302
+ 3. Non-ETH pair → price-feed 批量获取双边价格,拿到任一即返回
303
+ 4. 全失败 → 强制确认
304
+ """
305
+ get_price, get_prices_batch, get_eth_price = _get_price_feed()
306
+
307
+ token_in = (summary.get("tokenIn") or "").upper()
308
+ token_out = (summary.get("tokenOut") or "").upper()
309
+ stablecoins = {"USDC", "USDT", "DAI", "USDC.E", "USDT.E"}
310
+ eth_like = {"ETH", "WETH", "NATIVE"}
311
+
312
+ chain = (summary.get("chain") or "base").lower()
313
+
314
+ # ── 1. Stablecoin side → quote 金额即 USD ──
315
+ if token_in in stablecoins:
316
+ raw = summary.get("amountIn", "0")
317
+ try:
318
+ return float(str(raw).split()[0])
319
+ except (ValueError, IndexError):
320
+ return 0.0
321
+ if token_out in stablecoins:
322
+ raw = summary.get("amountOut", "0")
323
+ try:
324
+ return float(str(raw).split()[0])
325
+ except (ValueError, IndexError):
326
+ return 0.0
327
+
328
+ # ── 2. ETH pair → quote ETH 数量 × ETH 价格 ──
329
+ eth_amount = None
330
+ if token_in in eth_like:
331
+ raw = summary.get("amountIn", "0")
332
+ try:
333
+ eth_amount = float(str(raw).split()[0])
334
+ except (ValueError, IndexError):
335
+ pass
336
+ elif token_out in eth_like:
337
+ raw = summary.get("amountOut", "0")
338
+ try:
339
+ eth_amount = float(str(raw).split()[0])
340
+ except (ValueError, IndexError):
341
+ pass
342
+
343
+ if eth_amount is not None:
344
+ eth_price = get_eth_price(chain, tier="fast")
345
+ if eth_price is not None:
346
+ return eth_amount * eth_price
347
+
348
+ # ── 3. Non-ETH, non-stablecoin pair → price-feed 批量获取 ──
349
+ addr_in = summary.get("inputToken", "")
350
+ addr_out = summary.get("outputToken", "")
351
+
352
+ requests = []
353
+ for addr, raw_amt in [(addr_in, summary.get("amountIn", "0")), (addr_out, summary.get("amountOut", "0"))]:
354
+ if addr and addr.startswith("0x") and len(addr) == 42:
355
+ requests.append((chain, addr, raw_amt))
356
+
357
+ if requests:
358
+ fetch_list = [(c, a) for c, a, _ in requests]
359
+ results = get_prices_batch(fetch_list, tier="fast")
360
+ for chain_r, addr, raw_amt in requests:
361
+ key = f"{chain_r}:{addr.lower()}"
362
+ if key in results:
363
+ try:
364
+ amount = float(str(raw_amt).split()[0])
365
+ return amount * results[key]["price"]
366
+ except (ValueError, IndexError):
367
+ pass
368
+
369
+ # ── 4. 全失败 → 无法确认价值 ──
370
+ return 0.0