uni-exec-engine 0.2.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- uni_exec_engine-0.2.0.dist-info/METADATA +576 -0
- uni_exec_engine-0.2.0.dist-info/RECORD +80 -0
- uni_exec_engine-0.2.0.dist-info/WHEEL +5 -0
- uni_exec_engine-0.2.0.dist-info/licenses/LICENSE +21 -0
- uni_exec_engine-0.2.0.dist-info/top_level.txt +1 -0
- uniswap_autopilot/__init__.py +3 -0
- uniswap_autopilot/analytics/__init__.py +0 -0
- uniswap_autopilot/analytics/il_calculator.py +525 -0
- uniswap_autopilot/analytics/portfolio.py +234 -0
- uniswap_autopilot/analytics/position.py +433 -0
- uniswap_autopilot/analytics/range_suggest.py +270 -0
- uniswap_autopilot/audit.py +194 -0
- uniswap_autopilot/common/__init__.py +0 -0
- uniswap_autopilot/common/approval_cleanup.py +255 -0
- uniswap_autopilot/common/check_balance.py +64 -0
- uniswap_autopilot/common/common.py +804 -0
- uniswap_autopilot/common/deep_link.py +132 -0
- uniswap_autopilot/common/gas.py +141 -0
- uniswap_autopilot/data/auto_trade_policy.example.json +29 -0
- uniswap_autopilot/data/chains.json +135 -0
- uniswap_autopilot/data/common-token-addresses.json +777 -0
- uniswap_autopilot/execute/__init__.py +0 -0
- uniswap_autopilot/execute/_internal/__init__.py +5 -0
- uniswap_autopilot/execute/_internal/constants.py +15 -0
- uniswap_autopilot/execute/_internal/preflight.py +150 -0
- uniswap_autopilot/execute/_internal/pure_signer.py +182 -0
- uniswap_autopilot/execute/_internal/rpc.py +462 -0
- uniswap_autopilot/execute/_internal/signer.py +298 -0
- uniswap_autopilot/execute/_internal/submit.py +73 -0
- uniswap_autopilot/execute/_internal/tx.py +370 -0
- uniswap_autopilot/execute/broadcast.py +380 -0
- uniswap_autopilot/execute/detect.py +52 -0
- uniswap_autopilot/execute/telegram_confirm.py +272 -0
- uniswap_autopilot/lp/compare_pools.py +338 -0
- uniswap_autopilot/lp/v2/__init__.py +0 -0
- uniswap_autopilot/lp/v2/approve.py +100 -0
- uniswap_autopilot/lp/v2/build_tx.py +226 -0
- uniswap_autopilot/lp/v2/flow.py +204 -0
- uniswap_autopilot/lp/v2/pair.py +135 -0
- uniswap_autopilot/lp/v2/positions.py +177 -0
- uniswap_autopilot/lp/v3/__init__.py +0 -0
- uniswap_autopilot/lp/v3/approve.py +109 -0
- uniswap_autopilot/lp/v3/auto_rebalance.py +282 -0
- uniswap_autopilot/lp/v3/build_tx.py +465 -0
- uniswap_autopilot/lp/v3/compound.py +258 -0
- uniswap_autopilot/lp/v3/flow.py +358 -0
- uniswap_autopilot/lp/v3/pool.py +175 -0
- uniswap_autopilot/lp/v3/position.py +112 -0
- uniswap_autopilot/lp/v3/tick.py +75 -0
- uniswap_autopilot/lp/v4/__init__.py +0 -0
- uniswap_autopilot/lp/v4/approve.py +105 -0
- uniswap_autopilot/lp/v4/build_tx.py +669 -0
- uniswap_autopilot/lp/v4/flow.py +368 -0
- uniswap_autopilot/lp/v4/pool.py +174 -0
- uniswap_autopilot/lp/v4/position.py +185 -0
- uniswap_autopilot/policy.py +371 -0
- uniswap_autopilot/price_feed.py +100 -0
- uniswap_autopilot/py.typed +0 -0
- uniswap_autopilot/search/__init__.py +0 -0
- uniswap_autopilot/search/risk.py +200 -0
- uniswap_autopilot/search/search.py +580 -0
- uniswap_autopilot/state_machine.py +315 -0
- uniswap_autopilot/swap/__init__.py +1 -0
- uniswap_autopilot/swap/deep_link.py +69 -0
- uniswap_autopilot/swap/extensions/__init__.py +2 -0
- uniswap_autopilot/swap/extensions/bridge.py +197 -0
- uniswap_autopilot/swap/extensions/limit_order.py +272 -0
- uniswap_autopilot/swap/extensions/slippage.py +123 -0
- uniswap_autopilot/swap/flow.py +693 -0
- uniswap_autopilot/swap/flow_core/__init__.py +2 -0
- uniswap_autopilot/swap/flow_core/artifacts.py +11 -0
- uniswap_autopilot/swap/flow_core/broadcast.py +50 -0
- uniswap_autopilot/swap/flow_core/diagnostics.py +216 -0
- uniswap_autopilot/swap/flow_core/paper.py +113 -0
- uniswap_autopilot/swap/flow_core/policy.py +142 -0
- uniswap_autopilot/swap/links/__init__.py +2 -0
- uniswap_autopilot/swap/links/deep_link.py +69 -0
- uniswap_autopilot/swap/trading_api/permit.py +41 -0
- uniswap_autopilot/swap/trading_api/quote.py +282 -0
- uniswap_autopilot/swap/trading_api/swap.py +248 -0
|
@@ -0,0 +1,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()
|