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,272 @@
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
+ decimal_to_base_units,
13
+ dump_json,
14
+ load_local_env,
15
+ normalize_chain,
16
+ override_decimals,
17
+ parse_amount,
18
+ post_json,
19
+ require_api_key,
20
+ resolve_api_token,
21
+ resolve_token,
22
+ resolve_wallet_address,
23
+ )
24
+ from uniswap_autopilot.swap.trading_api.quote import build_quote_payload, summarize_quote
25
+ from uniswap_autopilot.swap.trading_api.swap import (
26
+ build_swap_payload,
27
+ is_uniswapx_route,
28
+ load_quote_payload,
29
+ load_signature_value,
30
+ )
31
+
32
+
33
+ def prepare_limit_order_quote(
34
+ chain_name: str,
35
+ token_in_name: str,
36
+ token_out_name: str,
37
+ amount: str,
38
+ target_price: float | None = None,
39
+ target_output: str | None = None,
40
+ wallet: str | None = None,
41
+ slippage: float = 0.5,
42
+ token_in_decimals: int | None = None,
43
+ token_out_decimals: int | None = None,
44
+ request_only: bool = False,
45
+ ) -> dict[str, Any]:
46
+ chain = normalize_chain(chain_name)
47
+ token_in = override_decimals(resolve_token(chain, token_in_name), token_in_decimals)
48
+ token_out = override_decimals(resolve_token(chain, token_out_name), token_out_decimals)
49
+ api_token_in = resolve_api_token(chain, token_in)
50
+ api_token_out = resolve_api_token(chain, token_out)
51
+ validated_wallet = resolve_wallet_address(wallet, "wallet")
52
+
53
+ input_amount = parse_amount(amount)
54
+
55
+ # Compute target output amount in token_out base units
56
+ if target_output is not None:
57
+ output_human = float(parse_amount(target_output))
58
+ elif target_price is not None:
59
+ output_human = float(input_amount) * target_price
60
+ else:
61
+ raise ValueError("either --target-price or --target-output is required")
62
+
63
+ output_base = decimal_to_base_units(parse_amount(str(output_human)), api_token_out["decimals"])
64
+
65
+ payload = build_quote_payload(
66
+ wallet=validated_wallet,
67
+ chain_id=chain.chain_id,
68
+ api_token_in=api_token_in,
69
+ api_token_out=api_token_out,
70
+ base_amount=output_base,
71
+ swap_type="EXACT_OUTPUT",
72
+ slippage=slippage,
73
+ routing_preference="BEST_PRICE",
74
+ )
75
+
76
+ response: dict[str, Any] = {
77
+ "action": "limit_order_quote",
78
+ "chain": {"key": chain.key, "chainId": chain.chain_id},
79
+ "tokenIn": token_in,
80
+ "tokenOut": token_out,
81
+ "apiTokenIn": api_token_in,
82
+ "apiTokenOut": api_token_out,
83
+ "wallet": validated_wallet,
84
+ "inputHumanAmount": format(input_amount, "f"),
85
+ "targetOutputHumanAmount": format(output_human, "f"),
86
+ "targetOutputBase": output_base,
87
+ "targetPrice": target_price,
88
+ "slippage": slippage,
89
+ "requestPayload": payload,
90
+ }
91
+
92
+ if not request_only:
93
+ api_key = require_api_key()
94
+ raw_quote = post_json("quote", payload, api_key)
95
+ response["quoteSummary"] = summarize_quote(raw_quote)
96
+ response["rawQuote"] = raw_quote
97
+ routing = raw_quote.get("routing")
98
+ if routing and not is_uniswapx_route(raw_quote):
99
+ response["warning"] = f"routing is {routing}, not a UniswapX Dutch order; limit order semantics may not apply"
100
+
101
+ return response
102
+
103
+
104
+ def submit_limit_order(
105
+ quote_file: str,
106
+ signature: str | None = None,
107
+ signature_file: str | None = None,
108
+ simulate: bool = True,
109
+ ) -> dict[str, Any]:
110
+ quote_data = load_quote_payload(quote_file)
111
+ raw_quote = quote_data.get("rawQuote") or quote_data
112
+
113
+ if not is_uniswapx_route(raw_quote):
114
+ routing = raw_quote.get("routing", "unknown")
115
+ return {
116
+ "action": "limit_order_submit",
117
+ "status": "rejected",
118
+ "reason": f"quote routing is '{routing}', not a UniswapX Dutch order; cannot submit as limit order",
119
+ }
120
+
121
+ effective_signature = load_signature_value(signature=signature, signature_file=signature_file)
122
+
123
+ api_key = require_api_key()
124
+ swap_payload = build_swap_payload(
125
+ raw_quote=raw_quote,
126
+ signature=effective_signature,
127
+ simulate_transaction=simulate,
128
+ refresh_gas_price=True,
129
+ )
130
+ swap_response = post_json("swap", swap_payload, api_key)
131
+
132
+ return {
133
+ "action": "limit_order_submit",
134
+ "status": "submitted",
135
+ "routing": raw_quote.get("routing"),
136
+ "simulated": simulate,
137
+ "swapPayload": swap_payload,
138
+ "swapResponse": swap_response,
139
+ }
140
+
141
+
142
+ def check_order_status(
143
+ chain_name: str,
144
+ tx_hash: str | None = None,
145
+ rpc_url: str | None = None,
146
+ ) -> dict[str, Any]:
147
+ if not tx_hash:
148
+ raise ValueError("--tx-hash is required")
149
+
150
+ chain = normalize_chain(chain_name)
151
+ from uniswap_autopilot.execute._internal.rpc import execute_cast_receipt, resolve_rpc_url
152
+
153
+ rpc, _ = resolve_rpc_url(rpc_url, chain.chain_id)
154
+ if not rpc:
155
+ raise RuntimeError(f"RPC URL not configured for {chain_name}")
156
+
157
+ receipt = execute_cast_receipt(tx_hash=tx_hash, rpc_url=rpc, confirmations=1)
158
+ status = receipt.get("status")
159
+ succeeded = status == "1" or status == 1
160
+
161
+ # Check for Dutch order events in logs
162
+ dutch_order_detected = False
163
+ logs = receipt.get("logs") or []
164
+ for log in logs:
165
+ topics = log.get("topics") or []
166
+ if topics and len(topics[0]) == 66:
167
+ # DutchOrderFilled event from UniswapX Reactor
168
+ if str(topics[0]).startswith("0x5ab3"):
169
+ dutch_order_detected = True
170
+ break
171
+
172
+ # Try to get block timestamp for expiry check
173
+ block_number_hex = receipt.get("blockNumber")
174
+ current_time = None
175
+ if block_number_hex:
176
+ try:
177
+ from uniswap_autopilot.execute._internal.rpc import _json_rpc
178
+ block = _json_rpc("eth_getBlockByNumber", [block_number_hex, False], rpc)
179
+ ts_hex = block.get("timestamp") if isinstance(block, dict) else None
180
+ if ts_hex:
181
+ current_time = int(ts_hex, 16)
182
+ except Exception:
183
+ pass
184
+
185
+ return {
186
+ "action": "limit_order_status",
187
+ "chain": {"key": chain.key, "chainId": chain.chain_id},
188
+ "txHash": tx_hash,
189
+ "status": "filled" if succeeded else "failed",
190
+ "dutchOrderDetected": dutch_order_detected,
191
+ "receipt": receipt,
192
+ "blockTimestamp": current_time,
193
+ }
194
+
195
+
196
+ def main() -> None:
197
+ parser = argparse.ArgumentParser(description="Uniswap limit orders via UniswapX Dutch auction")
198
+ sub = parser.add_subparsers(dest="command")
199
+
200
+ q = sub.add_parser("quote", help="Get a limit order quote (EXACT_OUTPUT via UniswapX)")
201
+ q.add_argument("--chain", required=True)
202
+ q.add_argument("--token-in", required=True)
203
+ q.add_argument("--token-out", required=True)
204
+ q.add_argument("--amount", required=True, help="Input amount")
205
+ q.add_argument("--target-price", type=float, help="Target price (token_out per token_in)")
206
+ q.add_argument("--target-output", help="Exact output amount (alternative to --target-price)")
207
+ q.add_argument("--wallet")
208
+ q.add_argument("--slippage", type=float, default=0.5)
209
+ q.add_argument("--token-in-decimals", type=int)
210
+ q.add_argument("--token-out-decimals", type=int)
211
+ q.add_argument("--request-only", action="store_true")
212
+ q.add_argument("--output")
213
+
214
+ s = sub.add_parser("submit", help="Submit a signed limit order")
215
+ s.add_argument("--quote-file", required=True)
216
+ s.add_argument("--signature")
217
+ s.add_argument("--signature-file")
218
+ s.add_argument("--no-simulate", action="store_true")
219
+ s.add_argument("--output")
220
+
221
+ c = sub.add_parser("status", help="Check order fill status by tx hash")
222
+ c.add_argument("--chain", required=True)
223
+ c.add_argument("--tx-hash", required=True)
224
+ c.add_argument("--rpc-url")
225
+ c.add_argument("--output")
226
+
227
+ args = parser.parse_args()
228
+ load_local_env()
229
+
230
+ if args.command == "quote":
231
+ result = prepare_limit_order_quote(
232
+ chain_name=args.chain,
233
+ token_in_name=args.token_in,
234
+ token_out_name=args.token_out,
235
+ amount=args.amount,
236
+ target_price=args.target_price,
237
+ target_output=args.target_output,
238
+ wallet=args.wallet,
239
+ slippage=args.slippage,
240
+ token_in_decimals=args.token_in_decimals,
241
+ token_out_decimals=args.token_out_decimals,
242
+ request_only=args.request_only,
243
+ )
244
+ if "warning" in result:
245
+ print(f"WARNING: {result['warning']}", file=sys.stderr)
246
+ print(f"Limit order quote: {args.amount} {args.token_in} -> {result['targetOutputHumanAmount']} {args.token_out}")
247
+ if args.output:
248
+ Path(args.output).write_text(json.dumps(result, ensure_ascii=False, indent=2) + "\n", encoding="utf-8")
249
+ dump_json(result)
250
+ elif args.command == "submit":
251
+ result = submit_limit_order(
252
+ quote_file=args.quote_file,
253
+ signature=args.signature,
254
+ signature_file=args.signature_file,
255
+ simulate=not args.no_simulate,
256
+ )
257
+ print(f"Limit order status: {result.get('status', 'unknown')}")
258
+ if args.output:
259
+ Path(args.output).write_text(json.dumps(result, ensure_ascii=False, indent=2) + "\n", encoding="utf-8")
260
+ dump_json(result)
261
+ elif args.command == "status":
262
+ result = check_order_status(args.chain, args.tx_hash, args.rpc_url)
263
+ print(f"Order status: {result['status']}")
264
+ if args.output:
265
+ Path(args.output).write_text(json.dumps(result, ensure_ascii=False, indent=2) + "\n", encoding="utf-8")
266
+ dump_json(result)
267
+ else:
268
+ parser.print_help()
269
+
270
+
271
+ if __name__ == "__main__":
272
+ main()
@@ -0,0 +1,123 @@
1
+ #!/usr/bin/env python3
2
+ from __future__ import annotations
3
+
4
+ import argparse
5
+ from typing import Any
6
+
7
+
8
+ from uniswap_autopilot.common.common import dump_json, load_local_env, normalize_chain, resolve_token
9
+ from uniswap_autopilot.search.search import _ds_lookup
10
+
11
+
12
+ MAJOR_CATEGORIES = {"wrapped-native", "wrapped-btc", "wrapped-eth"}
13
+ MAJOR_SYMBOLS = {"ETH", "WETH", "BTC", "WBTC", "CBETH", "CBBTC"}
14
+
15
+
16
+ def _token_is_stable(token: dict[str, Any]) -> bool:
17
+ return bool(token.get("isStable")) or token.get("category") == "stablecoin"
18
+
19
+
20
+ def _token_is_major(token: dict[str, Any]) -> bool:
21
+ return (
22
+ token.get("category") in MAJOR_CATEGORIES
23
+ or token.get("symbol", "").upper() in MAJOR_SYMBOLS
24
+ )
25
+
26
+
27
+ def _get_liquidity(chain_key: str, token_address: str) -> float:
28
+ if token_address == "NATIVE":
29
+ return float("inf")
30
+ ds = _ds_lookup(chain_key, token_address)
31
+ if ds:
32
+ return float(ds.get("liquidityUsd") or 0)
33
+ return 0.0
34
+
35
+
36
+ def suggest_slippage(
37
+ chain_name: str,
38
+ token_in_name: str,
39
+ token_out_name: str,
40
+ amount_usd: float | None = None,
41
+ ) -> dict[str, Any]:
42
+ chain = normalize_chain(chain_name)
43
+ tok_in = resolve_token(chain, token_in_name)
44
+ tok_out = resolve_token(chain, token_out_name)
45
+
46
+ in_stable = _token_is_stable(tok_in)
47
+ out_stable = _token_is_stable(tok_out)
48
+ in_major = _token_is_major(tok_in)
49
+ out_major = _token_is_major(tok_out)
50
+
51
+ liq_in = _get_liquidity(chain.key, tok_in["address"])
52
+ liq_out = _get_liquidity(chain.key, tok_out["address"])
53
+ pool_liq = min(liq_in, liq_out) if liq_in and liq_out else max(liq_in, liq_out)
54
+
55
+ if in_stable and out_stable:
56
+ category = "stable_stable"
57
+ base = 0.1
58
+ elif (in_stable or out_stable) and (in_major or out_major):
59
+ category = "stable_major"
60
+ base = 0.5
61
+ elif in_stable or out_stable:
62
+ if pool_liq >= 1_000_000:
63
+ category = "stable_midcap"
64
+ base = 1.0
65
+ elif pool_liq >= 100_000:
66
+ category = "stable_smallcap"
67
+ base = 2.0
68
+ else:
69
+ category = "stable_microcap"
70
+ base = 3.0
71
+ else:
72
+ if pool_liq >= 1_000_000:
73
+ category = "volatile_midcap"
74
+ base = 1.5
75
+ elif pool_liq >= 100_000:
76
+ category = "volatile_smallcap"
77
+ base = 3.0
78
+ else:
79
+ category = "volatile_microcap"
80
+ base = 5.0
81
+
82
+ dynamic_scale = 1.0
83
+ if amount_usd and pool_liq > 0:
84
+ trade_pct = amount_usd / pool_liq
85
+ if trade_pct > 0.02:
86
+ dynamic_scale = trade_pct / 0.02
87
+ elif amount_usd and pool_liq == 0:
88
+ dynamic_scale = 2.0
89
+ final = min(base * dynamic_scale, 10.0)
90
+ final = round(final, 2)
91
+
92
+ reasoning = f"{category}: base={base}%"
93
+ if dynamic_scale > 1.0:
94
+ reasoning += f", dynamic_scale={dynamic_scale:.2f}x (trade=${amount_usd:.0f} vs pool_liq=${pool_liq:,.0f})"
95
+ if pool_liq == 0:
96
+ reasoning += ", pool_liq=0 (no DexScreener data, using base)"
97
+
98
+ return {
99
+ "recommendedSlippage": final,
100
+ "category": category,
101
+ "baseSlippage": base,
102
+ "dynamicScale": round(dynamic_scale, 3),
103
+ "poolLiquidityUsd": pool_liq,
104
+ "reasoning": reasoning,
105
+ }
106
+
107
+
108
+ def main() -> None:
109
+ parser = argparse.ArgumentParser(description="Suggest slippage tolerance based on token pair")
110
+ parser.add_argument("--chain", required=True)
111
+ parser.add_argument("--token-in", required=True)
112
+ parser.add_argument("--token-out", required=True)
113
+ parser.add_argument("--amount-usd", type=float, help="Trade amount in USD for dynamic scaling")
114
+ args = parser.parse_args()
115
+
116
+ load_local_env()
117
+ result = suggest_slippage(args.chain, args.token_in, args.token_out, args.amount_usd)
118
+ print(f"Recommended: {result['recommendedSlippage']}% ({result['category']})")
119
+ dump_json(result)
120
+
121
+
122
+ if __name__ == "__main__":
123
+ main()