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,258 @@
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 dump_json, load_local_env, normalize_chain, resolve_wallet_address, resolve_token, decimal_to_base_units
11
+ from uniswap_autopilot.lp.v3.position import query_position, query_positions_by_owner
12
+ from uniswap_autopilot.analytics.position import analyze_position
13
+ from uniswap_autopilot.lp.v3.build_tx import (
14
+ build_decrease_liquidity_transaction,
15
+ build_collect_transaction,
16
+ build_mint_transaction,
17
+ )
18
+
19
+
20
+ def scan_compound_candidates(
21
+ chain_name: str,
22
+ wallet: str,
23
+ min_fee_usd: float = 10.0,
24
+ rpc_url: str | None = None,
25
+ ) -> dict[str, Any]:
26
+ chain = normalize_chain(chain_name)
27
+ token_ids = query_positions_by_owner(wallet, chain_name, rpc_url)
28
+
29
+ positions: list[dict[str, Any]] = []
30
+ candidates: list[int] = []
31
+
32
+ for tid in token_ids:
33
+ try:
34
+ pos = analyze_position(chain_name, tid, rpc_url)
35
+ fees_usd = pos.get("uncollectedFees", {}).get("totalUsd", 0.0)
36
+ entry = {
37
+ "tokenId": tid,
38
+ "token0": pos.get("token0", {}).get("symbol", "?"),
39
+ "token1": pos.get("token1", {}).get("symbol", "?"),
40
+ "feeTier": pos.get("feeTier"),
41
+ "uncollectedFeesUsd": fees_usd,
42
+ "totalValueUsd": pos.get("totalValueUsd", 0.0),
43
+ "inRange": pos.get("inRange"),
44
+ "tickLower": pos.get("tickLower"),
45
+ "tickUpper": pos.get("tickUpper"),
46
+ }
47
+ positions.append(entry)
48
+ if fees_usd >= min_fee_usd:
49
+ candidates.append(tid)
50
+ except Exception as exc:
51
+ positions.append({"tokenId": tid, "error": str(exc)})
52
+
53
+ return {
54
+ "action": "compound_scan",
55
+ "chain": {"key": chain.key, "chainId": chain.chain_id},
56
+ "wallet": wallet,
57
+ "minFeeUsd": min_fee_usd,
58
+ "totalPositions": len(positions),
59
+ "candidatesForCompound": candidates,
60
+ "positions": positions,
61
+ }
62
+
63
+
64
+ def execute_compound(
65
+ chain_name: str,
66
+ token_id: int,
67
+ slippage_pct: float = 0.5,
68
+ wallet: str | None = None,
69
+ rpc_url: str | None = None,
70
+ request_only: bool = True,
71
+ ) -> dict[str, Any]:
72
+ chain = normalize_chain(chain_name)
73
+ owner = resolve_wallet_address(wallet) or ""
74
+ if not owner:
75
+ raise ValueError("wallet address required")
76
+
77
+ # Step 1: query current position
78
+ pos = query_position(token_id, chain_name, rpc_url)
79
+ tick_lower = pos["tickLower"]
80
+ tick_upper = pos["tickUpper"]
81
+ fee_tier = pos["fee"]
82
+ tok0_addr = pos["token0"]
83
+ tok1_addr = pos["token1"]
84
+ current_liq = int(pos["liquidity"])
85
+ if current_liq == 0:
86
+ raise ValueError(f"position {token_id} has no liquidity")
87
+
88
+ # Get analyzed position for human-readable amounts + fees
89
+ analyzed = analyze_position(chain_name, token_id, rpc_url)
90
+ tok0_amount_human = float(analyzed["token0"]["amount"])
91
+ tok1_amount_human = float(analyzed["token1"]["amount"])
92
+ fee0_human = float(analyzed["uncollectedFees"]["token0"] or "0")
93
+ fee1_human = float(analyzed["uncollectedFees"]["token1"] or "0")
94
+
95
+ # Step 2: decrease 100%
96
+ decrease_result = build_decrease_liquidity_transaction(
97
+ chain_name=chain_name,
98
+ token_id=token_id,
99
+ liquidity_pct=100.0,
100
+ slippage_pct=slippage_pct,
101
+ wallet=owner,
102
+ rpc_url=rpc_url,
103
+ )
104
+
105
+ # Step 3: collect all
106
+ collect_result = build_collect_transaction(
107
+ chain_name=chain_name,
108
+ token_id=token_id,
109
+ recipient=owner,
110
+ )
111
+
112
+ # Step 4: mint new position with same range using collected amounts
113
+ # Convert human amounts + fees to base units for the mint
114
+ from decimal import Decimal as D
115
+ tok0_info = resolve_token(chain, tok0_addr)
116
+ tok1_info = resolve_token(chain, tok1_addr)
117
+ total0 = D(str(tok0_amount_human)) + D(str(fee0_human))
118
+ total1 = D(str(tok1_amount_human)) + D(str(fee1_human))
119
+ amount0 = decimal_to_base_units(total0, tok0_info["decimals"])
120
+ amount1 = decimal_to_base_units(total1, tok1_info["decimals"])
121
+
122
+ mint_result = build_mint_transaction(
123
+ chain_name=chain_name,
124
+ token_a=tok0_addr,
125
+ token_b=tok1_addr,
126
+ fee_tier=fee_tier,
127
+ tick_lower=tick_lower,
128
+ tick_upper=tick_upper,
129
+ amount0=amount0,
130
+ amount1=amount1,
131
+ slippage_pct=slippage_pct,
132
+ wallet=owner,
133
+ rpc_url=rpc_url,
134
+ request_only=request_only,
135
+ )
136
+
137
+ return {
138
+ "action": "compound",
139
+ "chain": {"key": chain.key, "chainId": chain.chain_id},
140
+ "tokenId": token_id,
141
+ "feeTier": fee_tier,
142
+ "tickLower": tick_lower,
143
+ "tickUpper": tick_upper,
144
+ "currentLiquidity": str(current_liq),
145
+ "steps": {
146
+ "decrease": decrease_result,
147
+ "collect": collect_result,
148
+ "mint": mint_result,
149
+ },
150
+ "broadcastReady": not request_only,
151
+ }
152
+
153
+
154
+ def batch_compound(
155
+ chain_name: str,
156
+ wallet: str,
157
+ min_fee_usd: float = 10.0,
158
+ slippage_pct: float = 0.5,
159
+ rpc_url: str | None = None,
160
+ request_only: bool = True,
161
+ ) -> dict[str, Any]:
162
+ chain = normalize_chain(chain_name)
163
+ scan = scan_compound_candidates(chain_name, wallet, min_fee_usd, rpc_url)
164
+ candidates = scan["candidatesForCompound"]
165
+
166
+ succeeded: list[int] = []
167
+ failed: list[dict[str, Any]] = []
168
+ results: list[dict[str, Any]] = []
169
+
170
+ for tid in candidates:
171
+ try:
172
+ result = execute_compound(
173
+ chain_name=chain_name,
174
+ token_id=tid,
175
+ slippage_pct=slippage_pct,
176
+ wallet=wallet,
177
+ rpc_url=rpc_url,
178
+ request_only=request_only,
179
+ )
180
+ succeeded.append(tid)
181
+ results.append({"tokenId": tid, "status": "success", "steps": result["steps"]})
182
+ except Exception as exc:
183
+ failed.append({"tokenId": tid, "error": str(exc)})
184
+ results.append({"tokenId": tid, "status": "error", "error": str(exc)})
185
+
186
+ return {
187
+ "action": "batch_compound",
188
+ "chain": {"key": chain.key, "chainId": chain.chain_id},
189
+ "wallet": wallet,
190
+ "minFeeUsd": min_fee_usd,
191
+ "totalAttempted": len(candidates),
192
+ "succeeded": succeeded,
193
+ "failed": failed,
194
+ "results": results,
195
+ }
196
+
197
+
198
+ def main() -> None:
199
+ parser = argparse.ArgumentParser(description="V3 LP auto-compound: harvest fees and reinvest")
200
+ sub = parser.add_subparsers(dest="command")
201
+
202
+ s = sub.add_parser("scan", help="Find positions with uncollected fees above threshold")
203
+ s.add_argument("--chain", required=True)
204
+ s.add_argument("--wallet", required=True)
205
+ s.add_argument("--min-fee-usd", type=float, default=10.0)
206
+ s.add_argument("--rpc-url")
207
+ s.add_argument("--output")
208
+
209
+ e = sub.add_parser("execute", help="Execute compound: decrease + collect + mint")
210
+ e.add_argument("--chain", required=True)
211
+ e.add_argument("--token-id", type=int, required=True)
212
+ e.add_argument("--wallet")
213
+ e.add_argument("--slippage", type=float, default=0.5)
214
+ e.add_argument("--rpc-url")
215
+ e.add_argument("--output")
216
+
217
+ b = sub.add_parser("batch", help="Batch compound all positions with fees above threshold")
218
+ b.add_argument("--chain", required=True)
219
+ b.add_argument("--wallet", required=True)
220
+ b.add_argument("--min-fee-usd", type=float, default=10.0)
221
+ b.add_argument("--slippage", type=float, default=0.5)
222
+ b.add_argument("--rpc-url")
223
+ b.add_argument("--output")
224
+
225
+ args = parser.parse_args()
226
+ load_local_env()
227
+
228
+ if args.command == "scan":
229
+ result = scan_compound_candidates(args.chain, args.wallet, args.min_fee_usd, args.rpc_url)
230
+ n = len(result["candidatesForCompound"])
231
+ print(f"Found {n}/{result['totalPositions']} positions with fees >= ${args.min_fee_usd}")
232
+ if args.output:
233
+ Path(args.output).write_text(json.dumps(result, ensure_ascii=False, indent=2) + "\n", encoding="utf-8")
234
+ dump_json(result)
235
+ elif args.command == "execute":
236
+ result = execute_compound(
237
+ args.chain, args.token_id, args.slippage,
238
+ args.wallet, args.rpc_url,
239
+ )
240
+ print(f"Compound plan for position #{args.token_id}: decrease → collect → mint")
241
+ if args.output:
242
+ Path(args.output).write_text(json.dumps(result, ensure_ascii=False, indent=2) + "\n", encoding="utf-8")
243
+ dump_json(result)
244
+ elif args.command == "batch":
245
+ result = batch_compound(
246
+ args.chain, args.wallet, args.min_fee_usd,
247
+ args.slippage, args.rpc_url,
248
+ )
249
+ print(f"Batch compound: {len(result['succeeded'])} succeeded, {len(result['failed'])} failed / {result['totalAttempted']} attempted")
250
+ if args.output:
251
+ Path(args.output).write_text(json.dumps(result, ensure_ascii=False, indent=2) + "\n", encoding="utf-8")
252
+ dump_json(result)
253
+ else:
254
+ parser.print_help()
255
+
256
+
257
+ if __name__ == "__main__":
258
+ main()
@@ -0,0 +1,358 @@
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
+ resolve_wallet_address,
15
+ )
16
+ from uniswap_autopilot.lp.v3.approve import build_approval_tx, check_lp_approvals
17
+ from uniswap_autopilot.lp.v3.build_tx import (
18
+ build_collect_transaction,
19
+ build_decrease_liquidity_transaction,
20
+ build_increase_liquidity_transaction,
21
+ build_mint_transaction,
22
+ )
23
+ from uniswap_autopilot.lp.v3.pool import query_pool_full_info
24
+
25
+
26
+ def _broadcast_tx(tx: dict[str, Any], broadcast_args: argparse.Namespace, confirm_phrase: str) -> dict[str, Any]:
27
+ from uniswap_autopilot.execute._internal.submit import broadcast_with_backend
28
+ return broadcast_with_backend(
29
+ tx=tx,
30
+ explicit_rpc_url=None,
31
+ confirm=confirm_phrase,
32
+ signer_args_source=broadcast_args,
33
+ )
34
+
35
+
36
+ def run_lp_mint_flow(
37
+ chain_name: str,
38
+ token_a: str, token_b: str,
39
+ fee_tier: int,
40
+ tick_lower: int, tick_upper: int,
41
+ amount_a: str, amount_b: str,
42
+ slippage: float,
43
+ deadline_seconds: int,
44
+ wallet: str | None,
45
+ output_dir: str,
46
+ rpc_url: str | None,
47
+ request_only: bool = False,
48
+ broadcast: bool = False,
49
+ broadcast_args: argparse.Namespace | None = None,
50
+ ) -> dict[str, Any]:
51
+ wallet_addr = resolve_wallet_address(wallet)
52
+ if not wallet_addr:
53
+ raise ValueError("wallet is required; pass --wallet or set wallet env")
54
+
55
+ pool_info = query_pool_full_info(chain_name, token_a, token_b, fee_tier, rpc_url)
56
+ if not pool_info.get("exists"):
57
+ raise ValueError(f"pool does not exist for {token_a}/{token_b} fee={fee_tier} on {chain_name}")
58
+
59
+ approval_status = check_lp_approvals(
60
+ pool_info["token0"], pool_info["token1"],
61
+ wallet_addr, chain_name, rpc_url,
62
+ )
63
+
64
+ mint_result = build_mint_transaction(
65
+ chain_name=chain_name, token_a=token_a, token_b=token_b,
66
+ fee_tier=fee_tier, tick_lower=tick_lower, tick_upper=tick_upper,
67
+ amount_a=amount_a, amount_b=amount_b,
68
+ slippage_pct=slippage, recipient=wallet_addr,
69
+ deadline_seconds=deadline_seconds, rpc_url=rpc_url,
70
+ request_only=request_only,
71
+ )
72
+
73
+ output: dict[str, Any] = {
74
+ "action": "lp_mint_flow",
75
+ "pool": pool_info,
76
+ "approval": approval_status,
77
+ "mint": mint_result,
78
+ }
79
+
80
+ approval_txs = []
81
+ if approval_status["token0"]["needsApproval"]:
82
+ approval_txs.append(build_approval_tx(pool_info["token0"], wallet_addr, chain_name))
83
+ if approval_status["token1"]["needsApproval"]:
84
+ approval_txs.append(build_approval_tx(pool_info["token1"], wallet_addr, chain_name))
85
+ output["approvalTxs"] = approval_txs
86
+
87
+ if request_only:
88
+ output["nextActions"] = ["broadcast-approvals", "broadcast-mint"] if approval_txs else ["broadcast-mint"]
89
+ return output
90
+
91
+ chain = pool_info["chain"]
92
+ output_path = Path(output_dir)
93
+ output_path.mkdir(parents=True, exist_ok=True)
94
+
95
+ if broadcast and broadcast_args:
96
+ for i, atx in enumerate(approval_txs):
97
+ confirm = f"BROADCAST APPROVAL {chain['chainId']} {atx['to']}"
98
+ result = _broadcast_tx(atx, broadcast_args, confirm)
99
+ output[f"approval{i}_broadcast"] = result
100
+
101
+ mint_tx = mint_result["transaction"]
102
+ confirm = f"BROADCAST LP_MINT {chain['chainId']} {mint_tx['to']}"
103
+ result = _broadcast_tx(mint_tx, broadcast_args, confirm)
104
+ output["mintBroadcast"] = result
105
+ else:
106
+ output["nextActions"] = ["broadcast-approvals", "broadcast-mint"] if approval_txs else ["broadcast-mint"]
107
+
108
+ (output_path / "lp_mint_flow.json").write_text(
109
+ json.dumps(output, ensure_ascii=False, indent=2) + "\n", encoding="utf-8",
110
+ )
111
+ return output
112
+
113
+
114
+ def run_lp_increase_flow(
115
+ chain_name: str, token_id: int,
116
+ amount0: str, amount1: str,
117
+ slippage: float, deadline_seconds: int,
118
+ wallet: str | None, output_dir: str,
119
+ rpc_url: str | None,
120
+ broadcast: bool = False,
121
+ broadcast_args: argparse.Namespace | None = None,
122
+ ) -> dict[str, Any]:
123
+ wallet_addr = resolve_wallet_address(wallet)
124
+ if not wallet_addr:
125
+ raise ValueError("wallet is required")
126
+
127
+ inc_result = build_increase_liquidity_transaction(
128
+ chain_name=chain_name, token_id=token_id,
129
+ amount0=amount0, amount1=amount1,
130
+ slippage_pct=slippage, deadline_seconds=deadline_seconds,
131
+ rpc_url=rpc_url,
132
+ )
133
+
134
+ pos = inc_result["position"]
135
+ approval_status = check_lp_approvals(
136
+ pos["token0"], pos["token1"],
137
+ wallet_addr, chain_name, rpc_url,
138
+ )
139
+
140
+ output: dict[str, Any] = {"action": "lp_increase_flow", "increase": inc_result, "approval": approval_status}
141
+
142
+ approval_txs = []
143
+ if approval_status["token0"]["needsApproval"]:
144
+ approval_txs.append(build_approval_tx(pos["token0"], wallet_addr, chain_name))
145
+ if approval_status["token1"]["needsApproval"]:
146
+ approval_txs.append(build_approval_tx(pos["token1"], wallet_addr, chain_name))
147
+
148
+ if broadcast and broadcast_args:
149
+ for i, atx in enumerate(approval_txs):
150
+ _broadcast_tx(atx, broadcast_args, f"BROADCAST APPROVAL {inc_result['chain']['chainId']} {atx['to']}")
151
+ result = _broadcast_tx(inc_result["transaction"], broadcast_args, f"BROADCAST LP_INCREASE {inc_result['chain']['chainId']} {inc_result['transaction']['to']}")
152
+ output["broadcast"] = result
153
+ else:
154
+ output["nextActions"] = ["broadcast-approvals", "broadcast-increase"] if approval_txs else ["broadcast-increase"]
155
+
156
+ output_path = Path(output_dir)
157
+ output_path.mkdir(parents=True, exist_ok=True)
158
+ (output_path / "lp_increase_flow.json").write_text(
159
+ json.dumps(output, ensure_ascii=False, indent=2) + "\n", encoding="utf-8",
160
+ )
161
+ return output
162
+
163
+
164
+ def run_lp_decrease_flow(
165
+ chain_name: str, token_id: int,
166
+ liquidity_pct: float, slippage: float,
167
+ deadline_seconds: int, wallet: str | None,
168
+ output_dir: str, rpc_url: str | None,
169
+ broadcast: bool = False,
170
+ broadcast_args: argparse.Namespace | None = None,
171
+ ) -> dict[str, Any]:
172
+ wallet_addr = resolve_wallet_address(wallet)
173
+ if not wallet_addr:
174
+ raise ValueError("wallet is required")
175
+
176
+ dec_result = build_decrease_liquidity_transaction(
177
+ chain_name=chain_name, token_id=token_id,
178
+ liquidity_pct=liquidity_pct, slippage_pct=slippage,
179
+ deadline_seconds=deadline_seconds, rpc_url=rpc_url,
180
+ )
181
+
182
+ output: dict[str, Any] = {"action": "lp_decrease_flow", "decrease": dec_result}
183
+
184
+ if broadcast and broadcast_args:
185
+ result = _broadcast_tx(dec_result["transaction"], broadcast_args, f"BROADCAST LP_DECREASE {dec_result['chain']['chainId']} {dec_result['transaction']['to']}")
186
+ output["broadcast"] = result
187
+ else:
188
+ output["nextActions"] = ["broadcast-decrease"]
189
+
190
+ output_path = Path(output_dir)
191
+ output_path.mkdir(parents=True, exist_ok=True)
192
+ (output_path / "lp_decrease_flow.json").write_text(
193
+ json.dumps(output, ensure_ascii=False, indent=2) + "\n", encoding="utf-8",
194
+ )
195
+ return output
196
+
197
+
198
+ def run_lp_collect_flow(
199
+ chain_name: str, token_id: int,
200
+ recipient: str | None, wallet: str | None,
201
+ output_dir: str, rpc_url: str | None,
202
+ broadcast: bool = False,
203
+ broadcast_args: argparse.Namespace | None = None,
204
+ ) -> dict[str, Any]:
205
+ wallet_addr = resolve_wallet_address(wallet)
206
+ if not wallet_addr:
207
+ raise ValueError("wallet is required")
208
+
209
+ col_result = build_collect_transaction(
210
+ chain_name=chain_name, token_id=token_id, recipient=recipient or wallet_addr,
211
+ )
212
+
213
+ output: dict[str, Any] = {"action": "lp_collect_flow", "collect": col_result}
214
+
215
+ if broadcast and broadcast_args:
216
+ result = _broadcast_tx(col_result["transaction"], broadcast_args, f"BROADCAST LP_COLLECT {col_result['chain']['chainId']} {col_result['transaction']['to']}")
217
+ output["broadcast"] = result
218
+ else:
219
+ output["nextActions"] = ["broadcast-collect"]
220
+
221
+ output_path = Path(output_dir)
222
+ output_path.mkdir(parents=True, exist_ok=True)
223
+ (output_path / "lp_collect_flow.json").write_text(
224
+ json.dumps(output, ensure_ascii=False, indent=2) + "\n", encoding="utf-8",
225
+ )
226
+ return output
227
+
228
+
229
+ def main() -> None:
230
+ parser = argparse.ArgumentParser(description="Uniswap v3 LP 全流程编排")
231
+ sub = parser.add_subparsers(dest="command", required=True)
232
+
233
+ # mint
234
+ mp = sub.add_parser("mint", help="创建新 LP 仓位")
235
+ mp.add_argument("--chain", required=True)
236
+ mp.add_argument("--token-a", required=True)
237
+ mp.add_argument("--token-b", required=True)
238
+ mp.add_argument("--fee-tier", type=int, required=True)
239
+ mp.add_argument("--tick-lower", type=int, required=True)
240
+ mp.add_argument("--tick-upper", type=int, required=True)
241
+ mp.add_argument("--amount-a", required=True)
242
+ mp.add_argument("--amount-b", required=True)
243
+ mp.add_argument("--slippage", type=float, default=0.5)
244
+ mp.add_argument("--deadline", type=int, default=600)
245
+ mp.add_argument("--wallet")
246
+ mp.add_argument("--output-dir", default="")
247
+ mp.add_argument("--rpc-url")
248
+ mp.add_argument("--request-only", action="store_true")
249
+ mp.add_argument("--broadcast", action="store_true")
250
+ mp.add_argument("--confirm")
251
+ mp.add_argument("--private-key-env")
252
+ mp.add_argument("--keystore")
253
+ mp.add_argument("--account")
254
+ mp.add_argument("--trade-signer-url")
255
+ mp.add_argument("--trade-signer-token-env")
256
+
257
+ # increase
258
+ ip = sub.add_parser("increase", help="增加流动性")
259
+ ip.add_argument("--chain", required=True)
260
+ ip.add_argument("--token-id", type=int, required=True)
261
+ ip.add_argument("--amount0", required=True)
262
+ ip.add_argument("--amount1", required=True)
263
+ ip.add_argument("--slippage", type=float, default=0.5)
264
+ ip.add_argument("--deadline", type=int, default=600)
265
+ ip.add_argument("--wallet")
266
+ ip.add_argument("--output-dir", default="")
267
+ ip.add_argument("--rpc-url")
268
+ ip.add_argument("--broadcast", action="store_true")
269
+ ip.add_argument("--confirm")
270
+ ip.add_argument("--private-key-env")
271
+ ip.add_argument("--keystore")
272
+ ip.add_argument("--account")
273
+ ip.add_argument("--trade-signer-url")
274
+ ip.add_argument("--trade-signer-token-env")
275
+
276
+ # decrease
277
+ dp = sub.add_parser("decrease", help="减少流动性")
278
+ dp.add_argument("--chain", required=True)
279
+ dp.add_argument("--token-id", type=int, required=True)
280
+ dp.add_argument("--liquidity-pct", type=float, required=True)
281
+ dp.add_argument("--slippage", type=float, default=0.5)
282
+ dp.add_argument("--deadline", type=int, default=600)
283
+ dp.add_argument("--wallet")
284
+ dp.add_argument("--output-dir", default="")
285
+ dp.add_argument("--rpc-url")
286
+ dp.add_argument("--broadcast", action="store_true")
287
+ dp.add_argument("--confirm")
288
+ dp.add_argument("--private-key-env")
289
+ dp.add_argument("--keystore")
290
+ dp.add_argument("--account")
291
+ dp.add_argument("--trade-signer-url")
292
+ dp.add_argument("--trade-signer-token-env")
293
+
294
+ # collect
295
+ cp = sub.add_parser("collect", help="收取手续费")
296
+ cp.add_argument("--chain", required=True)
297
+ cp.add_argument("--token-id", type=int, required=True)
298
+ cp.add_argument("--recipient")
299
+ cp.add_argument("--wallet")
300
+ cp.add_argument("--output-dir", default="")
301
+ cp.add_argument("--rpc-url")
302
+ cp.add_argument("--broadcast", action="store_true")
303
+ cp.add_argument("--confirm")
304
+ cp.add_argument("--private-key-env")
305
+ cp.add_argument("--keystore")
306
+ cp.add_argument("--account")
307
+ cp.add_argument("--trade-signer-url")
308
+ cp.add_argument("--trade-signer-token-env")
309
+
310
+ args = parser.parse_args()
311
+
312
+ try:
313
+ load_local_env()
314
+ if args.command == "mint":
315
+ result = run_lp_mint_flow(
316
+ chain_name=args.chain, token_a=args.token_a, token_b=args.token_b,
317
+ fee_tier=args.fee_tier, tick_lower=args.tick_lower, tick_upper=args.tick_upper,
318
+ amount_a=args.amount_a, amount_b=args.amount_b,
319
+ slippage=args.slippage, deadline_seconds=args.deadline,
320
+ wallet=args.wallet, output_dir=args.output_dir,
321
+ rpc_url=args.rpc_url, request_only=args.request_only,
322
+ broadcast=args.broadcast, broadcast_args=args if args.broadcast else None,
323
+ )
324
+ elif args.command == "increase":
325
+ result = run_lp_increase_flow(
326
+ chain_name=args.chain, token_id=args.token_id,
327
+ amount0=args.amount0, amount1=args.amount1,
328
+ slippage=args.slippage, deadline_seconds=args.deadline,
329
+ wallet=args.wallet, output_dir=args.output_dir,
330
+ rpc_url=args.rpc_url,
331
+ broadcast=args.broadcast, broadcast_args=args if args.broadcast else None,
332
+ )
333
+ elif args.command == "decrease":
334
+ result = run_lp_decrease_flow(
335
+ chain_name=args.chain, token_id=args.token_id,
336
+ liquidity_pct=args.liquidity_pct, slippage=args.slippage,
337
+ deadline_seconds=args.deadline, wallet=args.wallet,
338
+ output_dir=args.output_dir, rpc_url=args.rpc_url,
339
+ broadcast=args.broadcast, broadcast_args=args if args.broadcast else None,
340
+ )
341
+ elif args.command == "collect":
342
+ result = run_lp_collect_flow(
343
+ chain_name=args.chain, token_id=args.token_id,
344
+ recipient=args.recipient, wallet=args.wallet,
345
+ output_dir=args.output_dir, rpc_url=args.rpc_url,
346
+ broadcast=args.broadcast, broadcast_args=args if args.broadcast else None,
347
+ )
348
+ else:
349
+ parser.print_help()
350
+ sys.exit(1)
351
+ dump_json(result)
352
+ except Exception as exc:
353
+ print(json.dumps({"error": str(exc)}, ensure_ascii=False, indent=2), file=sys.stderr)
354
+ sys.exit(1)
355
+
356
+
357
+ if __name__ == "__main__":
358
+ main()