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,100 @@
1
+ #!/usr/bin/env python3
2
+ from __future__ import annotations
3
+
4
+ import argparse
5
+ import json
6
+ from pathlib import Path
7
+ from typing import Any
8
+
9
+
10
+ from uniswap_autopilot.common.common import (
11
+ dump_json,
12
+ get_v2_router02_address,
13
+ load_local_env,
14
+ normalize_chain,
15
+ resolve_token,
16
+ sort_token_addresses,
17
+ )
18
+ from uniswap_autopilot.execute._internal.rpc import (
19
+ build_calldata, encode_address, encode_uint, query_erc20_allowance,
20
+ )
21
+
22
+
23
+ def check_v2_approvals(
24
+ token0_address: str,
25
+ token1_address: str,
26
+ owner: str,
27
+ chain_name: str,
28
+ rpc_url: str | None = None,
29
+ ) -> dict[str, Any]:
30
+ _chain = normalize_chain(chain_name)
31
+ router02 = get_v2_router02_address(chain_name)
32
+
33
+ result = {"action": "v2_approval_check", "owner": owner, "spender": router02}
34
+
35
+ for label, token_addr in [("token0", token0_address), ("token1", token1_address)]:
36
+ try:
37
+ allowance = query_erc20_allowance(token_addr, owner, router02, rpc_url)
38
+ except Exception as exc:
39
+ allowance = 0
40
+ result[label] = {"queryError": str(exc)}
41
+ result[label] = {
42
+ "address": token_addr,
43
+ "allowance": str(allowance),
44
+ "needsApproval": allowance == 0,
45
+ }
46
+
47
+ return result
48
+
49
+
50
+ def build_v2_approval_tx(
51
+ token_address: str,
52
+ owner: str,
53
+ chain_name: str,
54
+ amount: str | None = None,
55
+ ) -> dict[str, Any]:
56
+ chain = normalize_chain(chain_name)
57
+ router02 = get_v2_router02_address(chain_name)
58
+ approve_amount = amount or "115792089237316195423570985008687907853269984665640564039457584007913129639935"
59
+
60
+ calldata = build_calldata(
61
+ "approve(address,uint256)",
62
+ encode_address(router02),
63
+ encode_uint(int(approve_amount)),
64
+ )
65
+
66
+ return {
67
+ "kind": "approval",
68
+ "to": token_address,
69
+ "data": calldata,
70
+ "value": "0",
71
+ "chainId": chain.chain_id,
72
+ "from": owner,
73
+ }
74
+
75
+
76
+ def main() -> None:
77
+ parser = argparse.ArgumentParser(description="检查 V2 LP approval 状态")
78
+ parser.add_argument("--chain", required=True, help="链名")
79
+ parser.add_argument("--token-a", required=True, help="代币 A")
80
+ parser.add_argument("--token-b", required=True, help="代币 B")
81
+ parser.add_argument("--owner", required=True, help="钱包地址")
82
+ parser.add_argument("--rpc-url", help="RPC URL")
83
+ parser.add_argument("--output", help="输出 JSON 文件路径")
84
+ args = parser.parse_args()
85
+
86
+ load_local_env()
87
+ chain = normalize_chain(args.chain)
88
+
89
+ tok_a = resolve_token(chain, args.token_a)
90
+ tok_b = resolve_token(chain, args.token_b)
91
+ token0, token1 = sort_token_addresses(tok_a["address"], tok_b["address"])
92
+
93
+ result = check_v2_approvals(token0, token1, args.owner, args.chain, args.rpc_url)
94
+ dump_json(result)
95
+ if args.output:
96
+ Path(args.output).write_text(json.dumps(result, ensure_ascii=False, indent=2) + "\n", encoding="utf-8")
97
+
98
+
99
+ if __name__ == "__main__":
100
+ main()
@@ -0,0 +1,226 @@
1
+ #!/usr/bin/env python3
2
+ from __future__ import annotations
3
+
4
+ import argparse
5
+ import json
6
+ import sys
7
+ import time
8
+ from pathlib import Path
9
+ from typing import Any
10
+
11
+
12
+ from uniswap_autopilot.common.common import (
13
+ decimal_to_base_units,
14
+ dump_json,
15
+ get_v2_router02_address,
16
+ load_local_env,
17
+ normalize_chain,
18
+ parse_amount,
19
+ resolve_token,
20
+ resolve_wallet_address,
21
+ sort_token_addresses,
22
+ )
23
+ from uniswap_autopilot.execute._internal.rpc import build_calldata, encode_address, encode_uint
24
+ from uniswap_autopilot.lp.v2.pair import query_pair_full_info
25
+
26
+
27
+ def _apply_slippage(amount_base_units: str, slippage_pct: float) -> str:
28
+ from decimal import Decimal
29
+ amount = Decimal(amount_base_units)
30
+ factor = Decimal("1") - Decimal(str(slippage_pct)) / Decimal("100")
31
+ return str(int(amount * factor))
32
+
33
+
34
+ def build_add_liquidity_tx(
35
+ chain_name: str,
36
+ token_a: str,
37
+ token_b: str,
38
+ amount_a: str,
39
+ amount_b: str,
40
+ slippage_pct: float = 0.5,
41
+ recipient: str | None = None,
42
+ deadline_seconds: int = 600,
43
+ rpc_url: str | None = None,
44
+ ) -> dict[str, Any]:
45
+ chain = normalize_chain(chain_name)
46
+ router02 = get_v2_router02_address(chain_name)
47
+
48
+ tok_a = resolve_token(chain, token_a, rpc_url)
49
+ tok_b = resolve_token(chain, token_b, rpc_url)
50
+ if tok_a["address"] == "NATIVE" or tok_b["address"] == "NATIVE":
51
+ raise ValueError("V2 LP does not support NATIVE token; use wrapped token (e.g. WETH instead of ETH)")
52
+ token0, token1 = sort_token_addresses(tok_a["address"], tok_b["address"])
53
+
54
+ # Map amounts to sorted token order
55
+ if tok_a["address"].lower() == token0.lower():
56
+ amount0_human, amount1_human = amount_a, amount_b
57
+ dec0, dec1 = tok_a["decimals"], tok_b["decimals"]
58
+ else:
59
+ amount0_human, amount1_human = amount_b, amount_a
60
+ dec0, dec1 = tok_b["decimals"], tok_a["decimals"]
61
+
62
+ amount0_desired = decimal_to_base_units(parse_amount(amount0_human), dec0)
63
+ amount1_desired = decimal_to_base_units(parse_amount(amount1_human), dec1)
64
+ amount0_min = _apply_slippage(amount0_desired, slippage_pct)
65
+ amount1_min = _apply_slippage(amount1_desired, slippage_pct)
66
+
67
+ to = recipient or resolve_wallet_address(None) or ""
68
+ if not to:
69
+ raise ValueError("recipient wallet address is required (--recipient or configure wallet env)")
70
+
71
+ deadline = str(int(time.time()) + deadline_seconds)
72
+
73
+ calldata = build_calldata(
74
+ "addLiquidity(address,address,uint256,uint256,uint256,uint256,address,uint256)",
75
+ encode_address(token0),
76
+ encode_address(token1),
77
+ encode_uint(int(amount0_desired)),
78
+ encode_uint(int(amount1_desired)),
79
+ encode_uint(int(amount0_min)),
80
+ encode_uint(int(amount1_min)),
81
+ encode_address(to),
82
+ encode_uint(int(deadline)),
83
+ )
84
+
85
+ return {
86
+ "action": "v2_add_liquidity",
87
+ "chain": {"key": chain.key, "chainId": chain.chain_id},
88
+ "token0": {"address": token0, "decimals": dec0, "amountDesired": amount0_desired, "amountMin": amount0_min},
89
+ "token1": {"address": token1, "decimals": dec1, "amountDesired": amount1_desired, "amountMin": amount1_min},
90
+ "recipient": to,
91
+ "deadline": deadline,
92
+ "transaction": {
93
+ "kind": "v2_add_liquidity",
94
+ "to": router02,
95
+ "data": calldata,
96
+ "value": "0",
97
+ "chainId": chain.chain_id,
98
+ "from": to,
99
+ },
100
+ }
101
+
102
+
103
+ def build_remove_liquidity_tx(
104
+ chain_name: str,
105
+ token_a: str,
106
+ token_b: str,
107
+ liquidity: str,
108
+ slippage_pct: float = 0.5,
109
+ recipient: str | None = None,
110
+ deadline_seconds: int = 600,
111
+ rpc_url: str | None = None,
112
+ ) -> dict[str, Any]:
113
+ chain = normalize_chain(chain_name)
114
+ router02 = get_v2_router02_address(chain_name)
115
+
116
+ tok_a = resolve_token(chain, token_a, rpc_url)
117
+ tok_b = resolve_token(chain, token_b, rpc_url)
118
+ token0, token1 = sort_token_addresses(tok_a["address"], tok_b["address"])
119
+ if tok_a["address"].lower() == token0.lower():
120
+ dec0, dec1 = tok_a["decimals"], tok_b["decimals"]
121
+ else:
122
+ dec0, dec1 = tok_b["decimals"], tok_a["decimals"]
123
+
124
+ deadline = str(int(time.time()) + deadline_seconds)
125
+ to = recipient or resolve_wallet_address(None) or ""
126
+
127
+ # Query current reserves to estimate minimum amounts
128
+ pair_info = query_pair_full_info(chain_name, tok_a["address"], tok_b["address"], rpc_url)
129
+ if not pair_info.get("exists"):
130
+ raise ValueError(f"V2 pair {tok_a['symbol']}/{tok_b['symbol']} does not exist on {chain_name}")
131
+
132
+ reserves = pair_info.get("reserves", {})
133
+ total_supply = int(pair_info.get("totalSupply", "1"))
134
+ liquidity_int = int(liquidity)
135
+
136
+ # Pro-rata: amount_min = (liquidity * reserve) / totalSupply * (1 - slippage)
137
+ from decimal import Decimal
138
+ reserve0 = reserves.get("reserve0", 0)
139
+ reserve1 = reserves.get("reserve1", 0)
140
+
141
+ share0 = Decimal(liquidity_int) * Decimal(reserve0) / Decimal(total_supply)
142
+ share1 = Decimal(liquidity_int) * Decimal(reserve1) / Decimal(total_supply)
143
+ factor = Decimal("1") - Decimal(str(slippage_pct)) / Decimal("100")
144
+ amount0_min = str(int(share0 * factor))
145
+ amount1_min = str(int(share1 * factor))
146
+
147
+ calldata = build_calldata(
148
+ "removeLiquidity(address,address,uint256,uint256,uint256,address,uint256)",
149
+ encode_address(token0),
150
+ encode_address(token1),
151
+ encode_uint(int(liquidity_int)),
152
+ encode_uint(int(amount0_min)),
153
+ encode_uint(int(amount1_min)),
154
+ encode_address(to),
155
+ encode_uint(int(deadline)),
156
+ )
157
+
158
+ return {
159
+ "action": "v2_remove_liquidity",
160
+ "chain": {"key": chain.key, "chainId": chain.chain_id},
161
+ "token0": {"address": token0, "decimals": dec0, "amountMin": amount0_min},
162
+ "token1": {"address": token1, "decimals": dec1, "amountMin": amount1_min},
163
+ "liquidity": str(liquidity_int),
164
+ "recipient": to,
165
+ "deadline": deadline,
166
+ "transaction": {
167
+ "kind": "v2_remove_liquidity",
168
+ "to": router02,
169
+ "data": calldata,
170
+ "value": "0",
171
+ "chainId": chain.chain_id,
172
+ "from": to,
173
+ },
174
+ }
175
+
176
+
177
+ def main() -> None:
178
+ parser = argparse.ArgumentParser(description="构建 V2 LP 交易")
179
+ sub = parser.add_subparsers(dest="command")
180
+
181
+ add = sub.add_parser("add", help="构建 addLiquidity 交易")
182
+ add.add_argument("--chain", required=True, help="链名")
183
+ add.add_argument("--token-a", required=True, help="代币 A")
184
+ add.add_argument("--token-b", required=True, help="代币 B")
185
+ add.add_argument("--amount-a", required=True, help="代币 A 数量")
186
+ add.add_argument("--amount-b", required=True, help="代币 B 数量")
187
+ add.add_argument("--slippage", type=float, default=0.5, help="滑点百分比,默认 0.5")
188
+ add.add_argument("--recipient", help="接收地址")
189
+ add.add_argument("--rpc-url", help="RPC URL")
190
+ add.add_argument("--output", help="输出 JSON 文件路径")
191
+
192
+ remove = sub.add_parser("remove", help="构建 removeLiquidity 交易")
193
+ remove.add_argument("--chain", required=True, help="链名")
194
+ remove.add_argument("--token-a", required=True, help="代币 A")
195
+ remove.add_argument("--token-b", required=True, help="代币 B")
196
+ remove.add_argument("--liquidity", required=True, help="要移除的 LP token 数量(base units)")
197
+ remove.add_argument("--slippage", type=float, default=0.5, help="滑点百分比")
198
+ remove.add_argument("--recipient", help="接收地址")
199
+ remove.add_argument("--rpc-url", help="RPC URL")
200
+ remove.add_argument("--output", help="输出 JSON 文件路径")
201
+
202
+ args = parser.parse_args()
203
+ load_local_env()
204
+
205
+ if args.command == "add":
206
+ result = build_add_liquidity_tx(
207
+ args.chain, args.token_a, args.token_b,
208
+ args.amount_a, args.amount_b,
209
+ args.slippage, args.recipient, rpc_url=args.rpc_url,
210
+ )
211
+ elif args.command == "remove":
212
+ result = build_remove_liquidity_tx(
213
+ args.chain, args.token_a, args.token_b,
214
+ args.liquidity, args.slippage, args.recipient, rpc_url=args.rpc_url,
215
+ )
216
+ else:
217
+ parser.print_help()
218
+ sys.exit(1)
219
+
220
+ dump_json(result)
221
+ if args.output:
222
+ Path(args.output).write_text(json.dumps(result, ensure_ascii=False, indent=2) + "\n", encoding="utf-8")
223
+
224
+
225
+ if __name__ == "__main__":
226
+ main()
@@ -0,0 +1,204 @@
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
+ dump_json,
13
+ load_local_env,
14
+ normalize_chain,
15
+ resolve_token,
16
+ resolve_wallet_address,
17
+ sort_token_addresses,
18
+ )
19
+ from uniswap_autopilot.execute._internal.rpc import resolve_rpc_url
20
+ from uniswap_autopilot.lp.v2.approve import build_v2_approval_tx, check_v2_approvals
21
+ from uniswap_autopilot.lp.v2.build_tx import build_add_liquidity_tx, build_remove_liquidity_tx
22
+ from uniswap_autopilot.lp.v2.pair import query_lp_balance, query_pair_full_info
23
+
24
+
25
+ def run_v2_add_liquidity_flow(
26
+ chain_name: str,
27
+ token_a: str,
28
+ token_b: str,
29
+ amount_a: str,
30
+ amount_b: str,
31
+ slippage: float = 0.5,
32
+ wallet: str | None = None,
33
+ rpc_url: str | None = None,
34
+ output_dir: str | None = None,
35
+ broadcast_args: argparse.Namespace | None = None,
36
+ ) -> dict[str, Any]:
37
+ chain = normalize_chain(chain_name)
38
+ wallet_addr = resolve_wallet_address(wallet)
39
+ if not wallet_addr:
40
+ raise ValueError("wallet address required (--wallet or configure wallet env)")
41
+
42
+ rpc = rpc_url or resolve_rpc_url(None, chain.chain_id)[0]
43
+ if not rpc:
44
+ raise RuntimeError(f"RPC URL not configured for {chain_name}; set an RPC env var or pass --rpc-url")
45
+
46
+ # 1. Query pair info
47
+ pair_info = query_pair_full_info(chain_name, token_a, token_b, rpc)
48
+
49
+ # 2. Check approvals
50
+ if pair_info.get("exists"):
51
+ token0 = pair_info["token0"]
52
+ token1 = pair_info["token1"]
53
+ else:
54
+ tok_a = resolve_token(chain, token_a, rpc)
55
+ tok_b = resolve_token(chain, token_b, rpc)
56
+ token0, token1 = sort_token_addresses(tok_a["address"], tok_b["address"])
57
+
58
+ approval_status = check_v2_approvals(token0, token1, wallet_addr, chain_name, rpc)
59
+
60
+ # 3. Build add liquidity tx
61
+ add_result = build_add_liquidity_tx(
62
+ chain_name, token_a, token_b, amount_a, amount_b,
63
+ slippage=slippage, recipient=wallet_addr, rpc_url=rpc,
64
+ )
65
+
66
+ # 4. Build approval txs if needed
67
+ approval_txs = []
68
+ for label in ["token0", "token1"]:
69
+ tok_info = approval_status.get(label, {})
70
+ if tok_info.get("needsApproval"):
71
+ approval_txs.append(build_v2_approval_tx(tok_info["address"], wallet_addr, chain_name))
72
+
73
+ output = {
74
+ "action": "v2_add_liquidity_flow",
75
+ "pair": pair_info,
76
+ "approval": approval_status,
77
+ "addLiquidity": add_result,
78
+ "approvalTxs": approval_txs,
79
+ "nextActions": ([] if not approval_txs else ["broadcast-approvals"]) + ["broadcast-add-liquidity"],
80
+ }
81
+
82
+ dump_json(output)
83
+ if output_dir:
84
+ out_path = Path(output_dir)
85
+ out_path.mkdir(parents=True, exist_ok=True)
86
+ out_path.joinpath("v2_add_liquidity_flow.json").write_text(
87
+ json.dumps(output, ensure_ascii=False, indent=2) + "\n", encoding="utf-8"
88
+ )
89
+
90
+ return output
91
+
92
+
93
+ def run_v2_remove_liquidity_flow(
94
+ chain_name: str,
95
+ token_a: str,
96
+ token_b: str,
97
+ liquidity_pct: float = 100.0,
98
+ slippage: float = 0.5,
99
+ wallet: str | None = None,
100
+ rpc_url: str | None = None,
101
+ output_dir: str | None = None,
102
+ broadcast_args: argparse.Namespace | None = None,
103
+ ) -> dict[str, Any]:
104
+ chain = normalize_chain(chain_name)
105
+ wallet_addr = resolve_wallet_address(wallet)
106
+ if not wallet_addr:
107
+ raise ValueError("wallet address required (--wallet or configure wallet env)")
108
+
109
+ rpc = rpc_url or resolve_rpc_url(None, chain.chain_id)[0]
110
+ if not rpc:
111
+ raise RuntimeError(f"RPC URL not configured for {chain_name}; set an RPC env var or pass --rpc-url")
112
+
113
+ # 1. Query pair info
114
+ pair_info = query_pair_full_info(chain_name, token_a, token_b, rpc)
115
+ if not pair_info.get("exists"):
116
+ raise ValueError(f"V2 pair {token_a}/{token_b} does not exist on {chain_name}")
117
+
118
+ pair_address = pair_info["pairAddress"]
119
+
120
+ # 2. Query LP balance
121
+ lp_balance = query_lp_balance(pair_address, wallet_addr, rpc)
122
+ if lp_balance == 0:
123
+ raise ValueError(f"No LP tokens for {token_a}/{token_b} in wallet {wallet_addr}")
124
+
125
+ # Calculate liquidity to remove based on percentage
126
+ from decimal import Decimal
127
+ liquidity_to_remove = str(int(Decimal(lp_balance) * Decimal(str(liquidity_pct)) / Decimal("100")))
128
+ if int(liquidity_to_remove) == 0:
129
+ raise ValueError("Calculated liquidity to remove is 0")
130
+
131
+ # 3. Build remove liquidity tx
132
+ remove_result = build_remove_liquidity_tx(
133
+ chain_name, token_a, token_b, liquidity_to_remove,
134
+ slippage=slippage, recipient=wallet_addr, rpc_url=rpc,
135
+ )
136
+
137
+ output = {
138
+ "action": "v2_remove_liquidity_flow",
139
+ "pair": pair_info,
140
+ "lpBalance": str(lp_balance),
141
+ "liquidityToRemove": liquidity_to_remove,
142
+ "removePct": liquidity_pct,
143
+ "removeLiquidity": remove_result,
144
+ "nextActions": ["broadcast-remove-liquidity"],
145
+ }
146
+
147
+ dump_json(output)
148
+ if output_dir:
149
+ out_path = Path(output_dir)
150
+ out_path.mkdir(parents=True, exist_ok=True)
151
+ out_path.joinpath("v2_remove_liquidity_flow.json").write_text(
152
+ json.dumps(output, ensure_ascii=False, indent=2) + "\n", encoding="utf-8"
153
+ )
154
+
155
+ return output
156
+
157
+
158
+ def main() -> None:
159
+ parser = argparse.ArgumentParser(description="Uniswap V2 LP 全流程编排")
160
+ sub = parser.add_subparsers(dest="command")
161
+
162
+ add = sub.add_parser("add", help="添加 V2 流动性")
163
+ add.add_argument("--chain", required=True, help="链名")
164
+ add.add_argument("--token-a", required=True, help="代币 A")
165
+ add.add_argument("--token-b", required=True, help="代币 B")
166
+ add.add_argument("--amount-a", required=True, help="代币 A 数量")
167
+ add.add_argument("--amount-b", required=True, help="代币 B 数量")
168
+ add.add_argument("--slippage", type=float, default=0.5, help="滑点百分比")
169
+ add.add_argument("--wallet", help="钱包地址")
170
+ add.add_argument("--rpc-url", help="RPC URL")
171
+ add.add_argument("--output-dir", help="输出目录")
172
+
173
+ remove = sub.add_parser("remove", help="移除 V2 流动性")
174
+ remove.add_argument("--chain", required=True, help="链名")
175
+ remove.add_argument("--token-a", required=True, help="代币 A")
176
+ remove.add_argument("--token-b", required=True, help="代币 B")
177
+ remove.add_argument("--liquidity-pct", type=float, default=100.0, help="移除百分比,默认 100%%")
178
+ remove.add_argument("--slippage", type=float, default=0.5, help="滑点百分比")
179
+ remove.add_argument("--wallet", help="钱包地址")
180
+ remove.add_argument("--rpc-url", help="RPC URL")
181
+ remove.add_argument("--output-dir", help="输出目录")
182
+
183
+ args = parser.parse_args()
184
+ load_local_env()
185
+
186
+ if args.command == "add":
187
+ run_v2_add_liquidity_flow(
188
+ args.chain, args.token_a, args.token_b,
189
+ args.amount_a, args.amount_b,
190
+ args.slippage, args.wallet, args.rpc_url, args.output_dir,
191
+ )
192
+ elif args.command == "remove":
193
+ run_v2_remove_liquidity_flow(
194
+ args.chain, args.token_a, args.token_b,
195
+ args.liquidity_pct, args.slippage,
196
+ args.wallet, args.rpc_url, args.output_dir,
197
+ )
198
+ else:
199
+ parser.print_help()
200
+ sys.exit(1)
201
+
202
+
203
+ if __name__ == "__main__":
204
+ main()
@@ -0,0 +1,135 @@
1
+ #!/usr/bin/env python3
2
+ from __future__ import annotations
3
+
4
+ import argparse
5
+ import json
6
+ from pathlib import Path
7
+ from typing import Any
8
+
9
+
10
+ from uniswap_autopilot.common.common import (
11
+ dump_json,
12
+ get_v2_factory_address,
13
+ load_local_env,
14
+ normalize_chain,
15
+ resolve_token,
16
+ sort_token_addresses,
17
+ )
18
+ from uniswap_autopilot.execute._internal.rpc import (
19
+ decode_address, decode_uint, encode_address, encode_selector, eth_call, resolve_rpc_url,
20
+ )
21
+
22
+ ZERO_ADDR = "0x0000000000000000000000000000000000000000"
23
+
24
+
25
+ def query_pair_address(token0: str, token1: str, factory: str, rpc_url: str) -> str:
26
+ sel = encode_selector("getPair(address,address)")
27
+ data = sel + encode_address(token0).replace("0x", "") + encode_address(token1).replace("0x", "")
28
+ raw = eth_call(factory, data, rpc_url)
29
+ addr = decode_address(raw)
30
+ if not addr or addr == ZERO_ADDR:
31
+ return ""
32
+ return addr
33
+
34
+
35
+
36
+ def query_reserves(pair_address: str, rpc_url: str) -> dict[str, Any]:
37
+ sel = encode_selector("getReserves()")
38
+ raw = eth_call(pair_address, sel, rpc_url)
39
+ clean = raw.replace("0x", "")
40
+ if len(clean) < 128:
41
+ return {"reserve0": 0, "reserve1": 0}
42
+ reserve0 = decode_uint("0x" + clean[0:64])
43
+ reserve1 = decode_uint("0x" + clean[64:128])
44
+ return {"reserve0": reserve0, "reserve1": reserve1}
45
+
46
+
47
+ def query_total_supply(pair_address: str, rpc_url: str) -> int:
48
+ sel = encode_selector("totalSupply()")
49
+ raw = eth_call(pair_address, sel, rpc_url)
50
+ return decode_uint(raw)
51
+
52
+
53
+ def query_lp_balance(pair_address: str, owner: str, rpc_url: str) -> int:
54
+ sel = encode_selector("balanceOf(address)")
55
+ data = sel + encode_address(owner).replace("0x", "")
56
+ raw = eth_call(pair_address, data, rpc_url)
57
+ return decode_uint(raw)
58
+
59
+
60
+ def query_pair_tokens(pair_address: str, rpc_url: str) -> tuple[str, str]:
61
+ sel0 = encode_selector("token0()")
62
+ raw0 = eth_call(pair_address, sel0, rpc_url)
63
+ token0 = decode_address(raw0)
64
+ sel1 = encode_selector("token1()")
65
+ raw1 = eth_call(pair_address, sel1, rpc_url)
66
+ token1 = decode_address(raw1)
67
+ return token0, token1
68
+
69
+
70
+ def query_pair_full_info(
71
+ chain_name: str,
72
+ token_a: str,
73
+ token_b: str,
74
+ rpc_url: str | None = None,
75
+ ) -> dict[str, Any]:
76
+ chain = normalize_chain(chain_name)
77
+ rpc = rpc_url or resolve_rpc_url(None, chain.chain_id)[0]
78
+ if not rpc:
79
+ raise RuntimeError(f"RPC URL not configured for {chain_name}")
80
+
81
+ factory = get_v2_factory_address(chain_name)
82
+
83
+ tok_a = resolve_token(chain, token_a, rpc)
84
+ tok_b = resolve_token(chain, token_b, rpc)
85
+
86
+ token0, token1 = sort_token_addresses(tok_a["address"], tok_b["address"])
87
+
88
+ pair_address = query_pair_address(token0, token1, factory, rpc)
89
+ if not pair_address:
90
+ return {
91
+ "action": "v2_pair_info",
92
+ "chain": {"key": chain.key, "chainId": chain.chain_id},
93
+ "tokenA": tok_a,
94
+ "tokenB": tok_b,
95
+ "token0": token0,
96
+ "token1": token1,
97
+ "pairAddress": None,
98
+ "exists": False,
99
+ }
100
+
101
+ reserves = query_reserves(pair_address, rpc)
102
+ total_supply = query_total_supply(pair_address, rpc)
103
+
104
+ return {
105
+ "action": "v2_pair_info",
106
+ "chain": {"key": chain.key, "chainId": chain.chain_id},
107
+ "tokenA": tok_a,
108
+ "tokenB": tok_b,
109
+ "token0": token0,
110
+ "token1": token1,
111
+ "pairAddress": pair_address,
112
+ "exists": True,
113
+ "reserves": reserves,
114
+ "totalSupply": str(total_supply),
115
+ }
116
+
117
+
118
+ def main() -> None:
119
+ parser = argparse.ArgumentParser(description="查询 Uniswap V2 Pair 信息")
120
+ parser.add_argument("--chain", required=True, help="链名")
121
+ parser.add_argument("--token-a", required=True, help="代币 A symbol 或地址")
122
+ parser.add_argument("--token-b", required=True, help="代币 B symbol 或地址")
123
+ parser.add_argument("--rpc-url", help="RPC URL")
124
+ parser.add_argument("--output", help="输出 JSON 文件路径")
125
+ args = parser.parse_args()
126
+
127
+ load_local_env()
128
+ result = query_pair_full_info(args.chain, args.token_a, args.token_b, args.rpc_url)
129
+ dump_json(result)
130
+ if args.output:
131
+ Path(args.output).write_text(json.dumps(result, ensure_ascii=False, indent=2) + "\n", encoding="utf-8")
132
+
133
+
134
+ if __name__ == "__main__":
135
+ main()