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,669 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
from __future__ import annotations
|
|
3
|
+
|
|
4
|
+
import argparse
|
|
5
|
+
import json
|
|
6
|
+
import os
|
|
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_v4_pool_manager_address,
|
|
16
|
+
get_v4_position_manager_address,
|
|
17
|
+
get_v4_state_view_address,
|
|
18
|
+
load_local_env,
|
|
19
|
+
normalize_chain,
|
|
20
|
+
parse_amount,
|
|
21
|
+
resolve_token,
|
|
22
|
+
resolve_wallet_address,
|
|
23
|
+
sort_token_addresses,
|
|
24
|
+
)
|
|
25
|
+
from uniswap_autopilot.execute._internal.rpc import encode_uint
|
|
26
|
+
from uniswap_autopilot.lp.v3.tick import tick_to_sqrt_price_x96
|
|
27
|
+
from uniswap_autopilot.lp.v4.pool import compute_pool_id, query_v4_slot0
|
|
28
|
+
|
|
29
|
+
# V4 Action constants
|
|
30
|
+
INCREASE_LIQUIDITY = 0x00
|
|
31
|
+
DECREASE_LIQUIDITY = 0x01
|
|
32
|
+
MINT_POSITION = 0x02
|
|
33
|
+
BURN_POSITION = 0x03
|
|
34
|
+
COLLECT = 0x06
|
|
35
|
+
TAKE = 0x09
|
|
36
|
+
SETTLE = 0x0A
|
|
37
|
+
SETTLE_PAIR = 0x0B
|
|
38
|
+
TAKE_PAIR = 0x0D
|
|
39
|
+
CLOSE_CURRENCY = 0x12
|
|
40
|
+
SWEEP = 0x14
|
|
41
|
+
|
|
42
|
+
ZERO_ADDRESS = "0x0000000000000000000000000000000000000000"
|
|
43
|
+
_UINT256_MOD = 2**256
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
def _encode_parameters(tick_spacing: int, hooks_registration: int = 0) -> int:
|
|
47
|
+
return (tick_spacing << 24) | hooks_registration
|
|
48
|
+
|
|
49
|
+
|
|
50
|
+
def _uint256(val: int) -> bytes:
|
|
51
|
+
return val.to_bytes(32, "big")
|
|
52
|
+
|
|
53
|
+
|
|
54
|
+
def _uint128(val: int) -> bytes:
|
|
55
|
+
return val.to_bytes(16, "big")
|
|
56
|
+
|
|
57
|
+
|
|
58
|
+
def _address(addr: str) -> bytes:
|
|
59
|
+
return int(addr, 16).to_bytes(32, "big")
|
|
60
|
+
|
|
61
|
+
|
|
62
|
+
def _encode_bytes(data: bytes) -> bytes:
|
|
63
|
+
length = len(data)
|
|
64
|
+
padded = data + b"\x00" * ((32 - length % 32) % 32)
|
|
65
|
+
return _uint256(length) + padded
|
|
66
|
+
|
|
67
|
+
|
|
68
|
+
def _encode_unlock_data(actions: list[int], params: list[bytes]) -> str:
|
|
69
|
+
"""Build unlockData = abi.encode(bytes actions, bytes[] params)."""
|
|
70
|
+
if len(actions) != len(params):
|
|
71
|
+
raise ValueError("actions and params must have same length")
|
|
72
|
+
|
|
73
|
+
actions_bytes = bytes(actions)
|
|
74
|
+
|
|
75
|
+
# Encode bytes[] params
|
|
76
|
+
params_encoded = b""
|
|
77
|
+
offsets = []
|
|
78
|
+
# 1 word for array length, then N words for offsets, then data
|
|
79
|
+
data_start = 32 + 32 * len(params)
|
|
80
|
+
current_offset = data_start
|
|
81
|
+
for p in params:
|
|
82
|
+
offsets.append(current_offset)
|
|
83
|
+
padded_len = len(p) + ((32 - len(p) % 32) % 32)
|
|
84
|
+
current_offset += 32 + padded_len
|
|
85
|
+
|
|
86
|
+
params_encoded += _uint256(len(params))
|
|
87
|
+
for off in offsets:
|
|
88
|
+
params_encoded += _uint256(off)
|
|
89
|
+
for p in params:
|
|
90
|
+
params_encoded += _encode_bytes(p)
|
|
91
|
+
|
|
92
|
+
# Encode (bytes, bytes[])
|
|
93
|
+
# offset_actions = 64 (after two uint256 offset words)
|
|
94
|
+
# offset_params = 64 + size_of_actions_encoding
|
|
95
|
+
actions_padded = actions_bytes + b"\x00" * ((32 - len(actions_bytes) % 32) % 32)
|
|
96
|
+
actions_encoded = _uint256(len(actions_bytes)) + actions_padded
|
|
97
|
+
offset_actions = 64
|
|
98
|
+
offset_params = offset_actions + len(actions_encoded)
|
|
99
|
+
|
|
100
|
+
result = _uint256(offset_actions) + _uint256(offset_params) + actions_encoded + params_encoded
|
|
101
|
+
return "0x" + result.hex()
|
|
102
|
+
|
|
103
|
+
|
|
104
|
+
def _encode_pool_key(
|
|
105
|
+
currency0: str,
|
|
106
|
+
currency1: str,
|
|
107
|
+
hooks: str,
|
|
108
|
+
pool_manager: str,
|
|
109
|
+
fee: int,
|
|
110
|
+
tick_spacing: int,
|
|
111
|
+
) -> bytes:
|
|
112
|
+
params = _encode_parameters(tick_spacing)
|
|
113
|
+
return (
|
|
114
|
+
_address(currency0)
|
|
115
|
+
+ _address(currency1)
|
|
116
|
+
+ _address(hooks)
|
|
117
|
+
+ _address(pool_manager)
|
|
118
|
+
+ _uint256(fee)
|
|
119
|
+
+ _uint256(params)
|
|
120
|
+
)
|
|
121
|
+
|
|
122
|
+
|
|
123
|
+
def _encode_mint_params(
|
|
124
|
+
pool_key_bytes: bytes,
|
|
125
|
+
tick_lower: int,
|
|
126
|
+
tick_upper: int,
|
|
127
|
+
liquidity: int,
|
|
128
|
+
amount0_max: int,
|
|
129
|
+
amount1_max: int,
|
|
130
|
+
owner: str,
|
|
131
|
+
hook_data: bytes = b"",
|
|
132
|
+
) -> bytes:
|
|
133
|
+
return (
|
|
134
|
+
pool_key_bytes
|
|
135
|
+
+ _uint256(tick_lower % _UINT256_MOD)
|
|
136
|
+
+ _uint256(tick_upper % _UINT256_MOD)
|
|
137
|
+
+ _uint256(liquidity)
|
|
138
|
+
+ _uint128(amount0_max).rjust(32, b"\x00")
|
|
139
|
+
+ _uint128(amount1_max).rjust(32, b"\x00")
|
|
140
|
+
+ _address(owner)
|
|
141
|
+
+ _encode_bytes(hook_data)
|
|
142
|
+
)
|
|
143
|
+
|
|
144
|
+
|
|
145
|
+
def _encode_decrease_params(
|
|
146
|
+
token_id: int,
|
|
147
|
+
liquidity: int,
|
|
148
|
+
amount0_min: int,
|
|
149
|
+
amount1_min: int,
|
|
150
|
+
hook_data: bytes = b"",
|
|
151
|
+
) -> bytes:
|
|
152
|
+
return (
|
|
153
|
+
_uint256(token_id)
|
|
154
|
+
+ _uint256(liquidity)
|
|
155
|
+
+ _uint128(amount0_min).rjust(32, b"\x00")
|
|
156
|
+
+ _uint128(amount1_min).rjust(32, b"\x00")
|
|
157
|
+
+ _encode_bytes(hook_data)
|
|
158
|
+
)
|
|
159
|
+
|
|
160
|
+
|
|
161
|
+
def _encode_settle_pair_params(currency0: str, currency1: str) -> bytes:
|
|
162
|
+
return _address(currency0) + _address(currency1)
|
|
163
|
+
|
|
164
|
+
|
|
165
|
+
def _encode_take_pair_params(currency0: str, currency1: str, recipient: str) -> bytes:
|
|
166
|
+
return _address(currency0) + _address(currency1) + _address(recipient)
|
|
167
|
+
|
|
168
|
+
|
|
169
|
+
def _encode_close_currency_params(currency: str, recipient: str) -> bytes:
|
|
170
|
+
return _address(currency) + _address(recipient)
|
|
171
|
+
|
|
172
|
+
|
|
173
|
+
def _apply_slippage(amount_str: str, slippage_pct: float) -> str:
|
|
174
|
+
from decimal import Decimal, ROUND_DOWN
|
|
175
|
+
amount = Decimal(amount_str)
|
|
176
|
+
factor = Decimal("1") - Decimal(str(slippage_pct)) / Decimal("100")
|
|
177
|
+
result = (amount * factor).to_integral_value(rounding=ROUND_DOWN)
|
|
178
|
+
return str(result)
|
|
179
|
+
|
|
180
|
+
|
|
181
|
+
def _get_liquidity_for_amount0(sqrt_a: int, sqrt_b: int, amount0: int) -> int:
|
|
182
|
+
if amount0 == 0:
|
|
183
|
+
return 0
|
|
184
|
+
return amount0 * sqrt_a * sqrt_b // ((sqrt_b - sqrt_a) * (2 ** 96))
|
|
185
|
+
|
|
186
|
+
|
|
187
|
+
def _get_liquidity_for_amount1(sqrt_a: int, sqrt_b: int, amount1: int) -> int:
|
|
188
|
+
if amount1 == 0:
|
|
189
|
+
return 0
|
|
190
|
+
return amount1 * (2 ** 96) // (sqrt_b - sqrt_a)
|
|
191
|
+
|
|
192
|
+
|
|
193
|
+
def _get_liquidity_for_amounts(
|
|
194
|
+
sqrt_price_x96: int,
|
|
195
|
+
sqrt_ratio_a_x96: int,
|
|
196
|
+
sqrt_ratio_b_x96: int,
|
|
197
|
+
amount0: int,
|
|
198
|
+
amount1: int,
|
|
199
|
+
) -> int:
|
|
200
|
+
if sqrt_ratio_a_x96 > sqrt_ratio_b_x96:
|
|
201
|
+
sqrt_ratio_a_x96, sqrt_ratio_b_x96 = sqrt_ratio_b_x96, sqrt_ratio_a_x96
|
|
202
|
+
if sqrt_price_x96 <= sqrt_ratio_a_x96:
|
|
203
|
+
return _get_liquidity_for_amount0(sqrt_ratio_a_x96, sqrt_ratio_b_x96, amount0)
|
|
204
|
+
elif sqrt_price_x96 < sqrt_ratio_b_x96:
|
|
205
|
+
liq0 = _get_liquidity_for_amount0(sqrt_price_x96, sqrt_ratio_b_x96, amount0)
|
|
206
|
+
liq1 = _get_liquidity_for_amount1(sqrt_ratio_a_x96, sqrt_price_x96, amount1)
|
|
207
|
+
return min(liq0, liq1)
|
|
208
|
+
else:
|
|
209
|
+
return _get_liquidity_for_amount1(sqrt_ratio_a_x96, sqrt_ratio_b_x96, amount1)
|
|
210
|
+
|
|
211
|
+
|
|
212
|
+
def _get_amount0_delta(sqrt_a: int, sqrt_b: int, liquidity: int) -> int:
|
|
213
|
+
if liquidity == 0 or sqrt_b <= sqrt_a:
|
|
214
|
+
return 0
|
|
215
|
+
return liquidity * (2 ** 96) * (sqrt_b - sqrt_a) // (sqrt_a * sqrt_b)
|
|
216
|
+
|
|
217
|
+
|
|
218
|
+
def _get_amount1_delta(sqrt_a: int, sqrt_b: int, liquidity: int) -> int:
|
|
219
|
+
if liquidity == 0 or sqrt_b <= sqrt_a:
|
|
220
|
+
return 0
|
|
221
|
+
return liquidity * (sqrt_b - sqrt_a) // (2 ** 96)
|
|
222
|
+
|
|
223
|
+
|
|
224
|
+
def _compute_expected_amounts(
|
|
225
|
+
current_tick: int, tick_lower: int, tick_upper: int, liquidity: int,
|
|
226
|
+
) -> tuple[int, int]:
|
|
227
|
+
"""Estimate token amounts for a given liquidity in a tick range."""
|
|
228
|
+
if liquidity == 0:
|
|
229
|
+
return 0, 0
|
|
230
|
+
sqrt_price = tick_to_sqrt_price_x96(current_tick)
|
|
231
|
+
sqrt_a = tick_to_sqrt_price_x96(tick_lower)
|
|
232
|
+
sqrt_b = tick_to_sqrt_price_x96(tick_upper)
|
|
233
|
+
if current_tick <= tick_lower:
|
|
234
|
+
return _get_amount0_delta(sqrt_a, sqrt_b, liquidity), 0
|
|
235
|
+
elif current_tick < tick_upper:
|
|
236
|
+
return (
|
|
237
|
+
_get_amount0_delta(sqrt_price, sqrt_b, liquidity),
|
|
238
|
+
_get_amount1_delta(sqrt_a, sqrt_price, liquidity),
|
|
239
|
+
)
|
|
240
|
+
else:
|
|
241
|
+
return 0, _get_amount1_delta(sqrt_a, sqrt_b, liquidity)
|
|
242
|
+
|
|
243
|
+
|
|
244
|
+
def _build_modify_liquidities_calldata(unlock_data_hex: str, deadline: int) -> str:
|
|
245
|
+
# modifyLiquidities(bytes,uint256) — manual ABI encoding for dynamic bytes
|
|
246
|
+
sel = "0x0b22dd98" # modifyLiquidities(bytes,uint256)
|
|
247
|
+
data_bytes = bytes.fromhex(unlock_data_hex.replace("0x", ""))
|
|
248
|
+
data_len = len(data_bytes)
|
|
249
|
+
padded_len = data_len + ((32 - data_len % 32) % 32)
|
|
250
|
+
# slot0: offset to bytes = 64 (two 32-byte words: offset + uint256)
|
|
251
|
+
# slot1: uint256 deadline
|
|
252
|
+
# slot2: bytes length
|
|
253
|
+
# slot3+: bytes data padded to 32-byte boundary
|
|
254
|
+
encoded = (
|
|
255
|
+
sel.replace("0x", "")
|
|
256
|
+
+ hex(64)[2:].rjust(64, "0") # offset to bytes
|
|
257
|
+
+ encode_uint(deadline) # uint256 deadline
|
|
258
|
+
+ hex(data_len)[2:].rjust(64, "0") # bytes length
|
|
259
|
+
+ data_bytes.hex().ljust(padded_len * 2, "0") # padded data
|
|
260
|
+
)
|
|
261
|
+
return "0x" + encoded
|
|
262
|
+
|
|
263
|
+
|
|
264
|
+
def build_v4_mint_transaction(
|
|
265
|
+
chain_name: str,
|
|
266
|
+
token_a: str,
|
|
267
|
+
token_b: str,
|
|
268
|
+
fee: int,
|
|
269
|
+
tick_spacing: int,
|
|
270
|
+
tick_lower: int,
|
|
271
|
+
tick_upper: int,
|
|
272
|
+
amount_a: str,
|
|
273
|
+
amount_b: str,
|
|
274
|
+
slippage_pct: float = 0.5,
|
|
275
|
+
hooks: str = ZERO_ADDRESS,
|
|
276
|
+
recipient: str | None = None,
|
|
277
|
+
deadline_seconds: int = 600,
|
|
278
|
+
rpc_url: str | None = None,
|
|
279
|
+
request_only: bool = False,
|
|
280
|
+
) -> dict[str, Any]:
|
|
281
|
+
chain = normalize_chain(chain_name)
|
|
282
|
+
pm = get_v4_position_manager_address(chain_name)
|
|
283
|
+
pool_manager = get_v4_pool_manager_address(chain_name)
|
|
284
|
+
|
|
285
|
+
tok_a = resolve_token(chain, token_a, rpc_url)
|
|
286
|
+
tok_b = resolve_token(chain, token_b, rpc_url)
|
|
287
|
+
c0, c1 = sort_token_addresses(tok_a["address"], tok_b["address"])
|
|
288
|
+
|
|
289
|
+
# Remap amounts: after sort, c0<=c1 but amounts must follow
|
|
290
|
+
if c0.lower() == tok_a["address"].lower():
|
|
291
|
+
decimals0, decimals1 = tok_a["decimals"], tok_b["decimals"]
|
|
292
|
+
amount0_human, amount1_human = amount_a, amount_b
|
|
293
|
+
else:
|
|
294
|
+
decimals0, decimals1 = tok_b["decimals"], tok_a["decimals"]
|
|
295
|
+
amount0_human, amount1_human = amount_b, amount_a
|
|
296
|
+
|
|
297
|
+
base0 = decimal_to_base_units(parse_amount(amount0_human), decimals0)
|
|
298
|
+
base1 = decimal_to_base_units(parse_amount(amount1_human), decimals1)
|
|
299
|
+
|
|
300
|
+
if tick_lower >= tick_upper:
|
|
301
|
+
raise ValueError(f"tick_lower ({tick_lower}) must be less than tick_upper ({tick_upper})")
|
|
302
|
+
|
|
303
|
+
wallet = recipient or os.environ.get("SECURE_WALLET_ADDRESS") or os.environ.get("HOT_WALLET_ADDRESS")
|
|
304
|
+
if not wallet:
|
|
305
|
+
raise ValueError("recipient is required")
|
|
306
|
+
|
|
307
|
+
# V4 requires client-side liquidity calculation
|
|
308
|
+
state_view = get_v4_state_view_address(chain_name)
|
|
309
|
+
pool_id = compute_pool_id(c0, c1, hooks, pool_manager, fee, tick_spacing)
|
|
310
|
+
slot0 = query_v4_slot0(state_view, pool_id, rpc_url) if rpc_url else None
|
|
311
|
+
sqrt_price_x96 = int(slot0["sqrtPriceX96"]) if slot0 else 0
|
|
312
|
+
|
|
313
|
+
sqrt_a = tick_to_sqrt_price_x96(tick_lower)
|
|
314
|
+
sqrt_b = tick_to_sqrt_price_x96(tick_upper)
|
|
315
|
+
|
|
316
|
+
if sqrt_price_x96 > 0:
|
|
317
|
+
liquidity = _get_liquidity_for_amounts(sqrt_price_x96, sqrt_a, sqrt_b, int(base0), int(base1))
|
|
318
|
+
else:
|
|
319
|
+
# Cannot compute without pool state; caller must ensure rpc_url is provided
|
|
320
|
+
raise ValueError("RPC URL is required for V4 mint (pool state needed for liquidity calculation)")
|
|
321
|
+
|
|
322
|
+
deadline = str(int(time.time()) + deadline_seconds)
|
|
323
|
+
|
|
324
|
+
pool_key_bytes = _encode_pool_key(c0, c1, hooks, pool_manager, fee, tick_spacing)
|
|
325
|
+
mint_params = _encode_mint_params(
|
|
326
|
+
pool_key_bytes, tick_lower, tick_upper, liquidity,
|
|
327
|
+
int(base0), int(base1), wallet,
|
|
328
|
+
)
|
|
329
|
+
settle_pair_params = _encode_settle_pair_params(c0, c1)
|
|
330
|
+
|
|
331
|
+
unlock_data = _encode_unlock_data(
|
|
332
|
+
[MINT_POSITION, SETTLE_PAIR],
|
|
333
|
+
[mint_params, settle_pair_params],
|
|
334
|
+
)
|
|
335
|
+
|
|
336
|
+
calldata = _build_modify_liquidities_calldata(unlock_data, int(deadline))
|
|
337
|
+
|
|
338
|
+
result: dict[str, Any] = {
|
|
339
|
+
"action": "v4_mint",
|
|
340
|
+
"chain": {"key": chain.key, "chainId": chain.chain_id},
|
|
341
|
+
"fee": fee,
|
|
342
|
+
"tickSpacing": tick_spacing,
|
|
343
|
+
"hooks": hooks,
|
|
344
|
+
"tickLower": tick_lower,
|
|
345
|
+
"tickUpper": tick_upper,
|
|
346
|
+
"currency0": c0,
|
|
347
|
+
"currency1": c1,
|
|
348
|
+
"liquidity": str(liquidity),
|
|
349
|
+
"amount0Max": base0,
|
|
350
|
+
"amount1Max": base1,
|
|
351
|
+
"recipient": wallet,
|
|
352
|
+
"deadline": deadline,
|
|
353
|
+
"transaction": {
|
|
354
|
+
"kind": "v4_mint",
|
|
355
|
+
"to": pm,
|
|
356
|
+
"data": calldata,
|
|
357
|
+
"value": "0",
|
|
358
|
+
"chainId": chain.chain_id,
|
|
359
|
+
"from": wallet,
|
|
360
|
+
},
|
|
361
|
+
}
|
|
362
|
+
return result
|
|
363
|
+
|
|
364
|
+
|
|
365
|
+
def build_v4_decrease_liquidity_transaction(
|
|
366
|
+
chain_name: str,
|
|
367
|
+
token_id: int,
|
|
368
|
+
liquidity_pct: float,
|
|
369
|
+
slippage_pct: float = 0.5,
|
|
370
|
+
deadline_seconds: int = 600,
|
|
371
|
+
rpc_url: str | None = None,
|
|
372
|
+
wallet: str | None = None,
|
|
373
|
+
) -> dict[str, Any]:
|
|
374
|
+
chain = normalize_chain(chain_name)
|
|
375
|
+
pm = get_v4_position_manager_address(chain_name)
|
|
376
|
+
|
|
377
|
+
owner = resolve_wallet_address(wallet) or os.environ.get("SECURE_WALLET_ADDRESS") or os.environ.get("HOT_WALLET_ADDRESS")
|
|
378
|
+
if not owner:
|
|
379
|
+
raise ValueError("wallet is required")
|
|
380
|
+
|
|
381
|
+
from uniswap_autopilot.lp.v4.position import query_v4_position
|
|
382
|
+
pos = query_v4_position(token_id, chain_name, rpc_url)
|
|
383
|
+
current_liq = int(pos["liquidity"])
|
|
384
|
+
if current_liq == 0:
|
|
385
|
+
raise ValueError(f"position {token_id} has no liquidity")
|
|
386
|
+
|
|
387
|
+
if liquidity_pct <= 0 or liquidity_pct > 100:
|
|
388
|
+
raise ValueError("liquidity_pct must be between 0 and 100")
|
|
389
|
+
remove_liq = int(current_liq * liquidity_pct / 100)
|
|
390
|
+
if remove_liq == 0:
|
|
391
|
+
raise ValueError("liquidity_pct results in 0 liquidity")
|
|
392
|
+
|
|
393
|
+
# Compute expected amounts from liquidity for proper slippage protection
|
|
394
|
+
current_tick = pos.get("currentTick")
|
|
395
|
+
tick_lower = pos["tickLower"]
|
|
396
|
+
tick_upper = pos["tickUpper"]
|
|
397
|
+
amount0_min = "0"
|
|
398
|
+
amount1_min = "0"
|
|
399
|
+
if current_tick is not None and slippage_pct > 0:
|
|
400
|
+
est0, est1 = _compute_expected_amounts(current_tick, tick_lower, tick_upper, remove_liq)
|
|
401
|
+
if est0 > 0:
|
|
402
|
+
amount0_min = _apply_slippage(str(est0), slippage_pct)
|
|
403
|
+
if est1 > 0:
|
|
404
|
+
amount1_min = _apply_slippage(str(est1), slippage_pct)
|
|
405
|
+
|
|
406
|
+
deadline = str(int(time.time()) + deadline_seconds)
|
|
407
|
+
c0 = pos["currency0"]["address"]
|
|
408
|
+
c1 = pos["currency1"]["address"]
|
|
409
|
+
|
|
410
|
+
decrease_params = _encode_decrease_params(token_id, remove_liq, int(amount0_min), int(amount1_min))
|
|
411
|
+
take_pair_params = _encode_take_pair_params(c0, c1, owner)
|
|
412
|
+
|
|
413
|
+
unlock_data = _encode_unlock_data(
|
|
414
|
+
[DECREASE_LIQUIDITY, TAKE_PAIR],
|
|
415
|
+
[decrease_params, take_pair_params],
|
|
416
|
+
)
|
|
417
|
+
calldata = _build_modify_liquidities_calldata(unlock_data, int(deadline))
|
|
418
|
+
|
|
419
|
+
return {
|
|
420
|
+
"action": "v4_decrease",
|
|
421
|
+
"chain": {"key": chain.key, "chainId": chain.chain_id},
|
|
422
|
+
"tokenId": token_id,
|
|
423
|
+
"owner": owner,
|
|
424
|
+
"currentLiquidity": str(current_liq),
|
|
425
|
+
"removeLiquidity": str(remove_liq),
|
|
426
|
+
"removePct": liquidity_pct,
|
|
427
|
+
"deadline": deadline,
|
|
428
|
+
"transaction": {
|
|
429
|
+
"kind": "v4_decrease",
|
|
430
|
+
"to": pm,
|
|
431
|
+
"data": calldata,
|
|
432
|
+
"value": "0",
|
|
433
|
+
"chainId": chain.chain_id,
|
|
434
|
+
"from": owner,
|
|
435
|
+
},
|
|
436
|
+
}
|
|
437
|
+
|
|
438
|
+
|
|
439
|
+
def build_v4_collect_transaction(
|
|
440
|
+
chain_name: str,
|
|
441
|
+
token_id: int,
|
|
442
|
+
recipient: str | None = None,
|
|
443
|
+
rpc_url: str | None = None,
|
|
444
|
+
) -> dict[str, Any]:
|
|
445
|
+
chain = normalize_chain(chain_name)
|
|
446
|
+
pm = get_v4_position_manager_address(chain_name)
|
|
447
|
+
|
|
448
|
+
wallet = recipient or os.environ.get("SECURE_WALLET_ADDRESS") or os.environ.get("HOT_WALLET_ADDRESS")
|
|
449
|
+
if not wallet:
|
|
450
|
+
raise ValueError("recipient is required")
|
|
451
|
+
|
|
452
|
+
from uniswap_autopilot.lp.v4.position import query_v4_position
|
|
453
|
+
pos = query_v4_position(token_id, chain_name, rpc_url)
|
|
454
|
+
c0 = pos["currency0"]["address"]
|
|
455
|
+
c1 = pos["currency1"]["address"]
|
|
456
|
+
|
|
457
|
+
deadline = str(int(time.time()) + 600)
|
|
458
|
+
decrease_params = _encode_decrease_params(token_id, 0, 0, 0)
|
|
459
|
+
close0_params = _encode_close_currency_params(c0, wallet)
|
|
460
|
+
close1_params = _encode_close_currency_params(c1, wallet)
|
|
461
|
+
sweep0_params = _address(c0) + _address(wallet)
|
|
462
|
+
sweep1_params = _address(c1) + _address(wallet)
|
|
463
|
+
|
|
464
|
+
unlock_data = _encode_unlock_data(
|
|
465
|
+
[DECREASE_LIQUIDITY, CLOSE_CURRENCY, CLOSE_CURRENCY, SWEEP, SWEEP],
|
|
466
|
+
[decrease_params, close0_params, close1_params, sweep0_params, sweep1_params],
|
|
467
|
+
)
|
|
468
|
+
calldata = _build_modify_liquidities_calldata(unlock_data, int(deadline))
|
|
469
|
+
|
|
470
|
+
return {
|
|
471
|
+
"action": "v4_collect",
|
|
472
|
+
"chain": {"key": chain.key, "chainId": chain.chain_id},
|
|
473
|
+
"tokenId": token_id,
|
|
474
|
+
"recipient": wallet,
|
|
475
|
+
"deadline": deadline,
|
|
476
|
+
"transaction": {
|
|
477
|
+
"kind": "v4_collect",
|
|
478
|
+
"to": pm,
|
|
479
|
+
"data": calldata,
|
|
480
|
+
"value": "0",
|
|
481
|
+
"chainId": chain.chain_id,
|
|
482
|
+
"from": wallet,
|
|
483
|
+
},
|
|
484
|
+
}
|
|
485
|
+
|
|
486
|
+
|
|
487
|
+
def build_v4_increase_liquidity_transaction(
|
|
488
|
+
chain_name: str,
|
|
489
|
+
token_id: int,
|
|
490
|
+
amount0: str,
|
|
491
|
+
amount1: str,
|
|
492
|
+
slippage_pct: float = 0.5,
|
|
493
|
+
deadline_seconds: int = 600,
|
|
494
|
+
rpc_url: str | None = None,
|
|
495
|
+
wallet: str | None = None,
|
|
496
|
+
) -> dict[str, Any]:
|
|
497
|
+
chain = normalize_chain(chain_name)
|
|
498
|
+
pm = get_v4_position_manager_address(chain_name)
|
|
499
|
+
|
|
500
|
+
owner = resolve_wallet_address(wallet) or os.environ.get("SECURE_WALLET_ADDRESS") or os.environ.get("HOT_WALLET_ADDRESS")
|
|
501
|
+
if not owner:
|
|
502
|
+
raise ValueError("wallet is required")
|
|
503
|
+
|
|
504
|
+
from uniswap_autopilot.lp.v4.position import query_v4_position
|
|
505
|
+
pos = query_v4_position(token_id, chain_name, rpc_url)
|
|
506
|
+
if pos["liquidity"] == "0":
|
|
507
|
+
raise ValueError(f"position {token_id} has no liquidity; use mint instead")
|
|
508
|
+
|
|
509
|
+
c0 = pos["currency0"]["address"]
|
|
510
|
+
c1 = pos["currency1"]["address"]
|
|
511
|
+
decimals0 = pos["currency0"]["decimals"]
|
|
512
|
+
decimals1 = pos["currency1"]["decimals"]
|
|
513
|
+
|
|
514
|
+
base0 = decimal_to_base_units(parse_amount(amount0), decimals0)
|
|
515
|
+
base1 = decimal_to_base_units(parse_amount(amount1), decimals1)
|
|
516
|
+
|
|
517
|
+
# V4 requires client-side liquidity calculation (same as mint)
|
|
518
|
+
current_tick = pos.get("currentTick")
|
|
519
|
+
if current_tick is None:
|
|
520
|
+
raise ValueError("RPC URL is required for V4 increase (pool state needed for liquidity calculation)")
|
|
521
|
+
|
|
522
|
+
tick_lower = pos["tickLower"]
|
|
523
|
+
tick_upper = pos["tickUpper"]
|
|
524
|
+
sqrt_price_x96 = tick_to_sqrt_price_x96(current_tick)
|
|
525
|
+
sqrt_a = tick_to_sqrt_price_x96(tick_lower)
|
|
526
|
+
sqrt_b = tick_to_sqrt_price_x96(tick_upper)
|
|
527
|
+
liquidity = _get_liquidity_for_amounts(sqrt_price_x96, sqrt_a, sqrt_b, int(base0), int(base1))
|
|
528
|
+
|
|
529
|
+
deadline = str(int(time.time()) + deadline_seconds)
|
|
530
|
+
|
|
531
|
+
increase_params = (
|
|
532
|
+
_uint256(token_id)
|
|
533
|
+
+ _uint256(liquidity)
|
|
534
|
+
+ _uint128(int(base0)).rjust(32, b"\x00")
|
|
535
|
+
+ _uint128(int(base1)).rjust(32, b"\x00")
|
|
536
|
+
+ _encode_bytes(b"")
|
|
537
|
+
)
|
|
538
|
+
settle_pair_params = _encode_settle_pair_params(c0, c1)
|
|
539
|
+
|
|
540
|
+
unlock_data = _encode_unlock_data(
|
|
541
|
+
[INCREASE_LIQUIDITY, SETTLE_PAIR],
|
|
542
|
+
[increase_params, settle_pair_params],
|
|
543
|
+
)
|
|
544
|
+
calldata = _build_modify_liquidities_calldata(unlock_data, int(deadline))
|
|
545
|
+
|
|
546
|
+
return {
|
|
547
|
+
"action": "v4_increase",
|
|
548
|
+
"chain": {"key": chain.key, "chainId": chain.chain_id},
|
|
549
|
+
"tokenId": token_id,
|
|
550
|
+
"position": pos,
|
|
551
|
+
"owner": owner,
|
|
552
|
+
"currency0": c0,
|
|
553
|
+
"currency1": c1,
|
|
554
|
+
"liquidity": str(liquidity),
|
|
555
|
+
"amount0Max": base0,
|
|
556
|
+
"amount1Max": base1,
|
|
557
|
+
"deadline": deadline,
|
|
558
|
+
"transaction": {
|
|
559
|
+
"kind": "v4_increase",
|
|
560
|
+
"to": pm,
|
|
561
|
+
"data": calldata,
|
|
562
|
+
"value": "0",
|
|
563
|
+
"chainId": chain.chain_id,
|
|
564
|
+
"from": owner,
|
|
565
|
+
},
|
|
566
|
+
}
|
|
567
|
+
|
|
568
|
+
|
|
569
|
+
def main() -> None:
|
|
570
|
+
parser = argparse.ArgumentParser(description="Build Uniswap V4 LP transactions")
|
|
571
|
+
sub = parser.add_subparsers(dest="command")
|
|
572
|
+
|
|
573
|
+
m = sub.add_parser("mint", help="Mint a new V4 position")
|
|
574
|
+
m.add_argument("--chain", required=True)
|
|
575
|
+
m.add_argument("--token-a", required=True)
|
|
576
|
+
m.add_argument("--token-b", required=True)
|
|
577
|
+
m.add_argument("--fee", type=int, required=True)
|
|
578
|
+
m.add_argument("--tick-spacing", type=int, required=True)
|
|
579
|
+
m.add_argument("--tick-lower", type=int, required=True)
|
|
580
|
+
m.add_argument("--tick-upper", type=int, required=True)
|
|
581
|
+
m.add_argument("--amount-a", required=True)
|
|
582
|
+
m.add_argument("--amount-b", required=True)
|
|
583
|
+
m.add_argument("--slippage", type=float, default=0.5)
|
|
584
|
+
m.add_argument("--hooks", default=ZERO_ADDRESS)
|
|
585
|
+
m.add_argument("--recipient")
|
|
586
|
+
m.add_argument("--deadline", type=int, default=600)
|
|
587
|
+
m.add_argument("--rpc-url")
|
|
588
|
+
m.add_argument("--request-only", action="store_true")
|
|
589
|
+
m.add_argument("--output")
|
|
590
|
+
|
|
591
|
+
d = sub.add_parser("decrease", help="Decrease liquidity")
|
|
592
|
+
d.add_argument("--chain", required=True)
|
|
593
|
+
d.add_argument("--token-id", type=int, required=True)
|
|
594
|
+
d.add_argument("--liquidity-pct", type=float, required=True)
|
|
595
|
+
d.add_argument("--slippage", type=float, default=0.5)
|
|
596
|
+
d.add_argument("--deadline", type=int, default=600)
|
|
597
|
+
d.add_argument("--rpc-url")
|
|
598
|
+
d.add_argument("--output")
|
|
599
|
+
|
|
600
|
+
inc = sub.add_parser("increase", help="Increase liquidity")
|
|
601
|
+
inc.add_argument("--chain", required=True)
|
|
602
|
+
inc.add_argument("--token-id", type=int, required=True)
|
|
603
|
+
inc.add_argument("--amount0", required=True)
|
|
604
|
+
inc.add_argument("--amount1", required=True)
|
|
605
|
+
inc.add_argument("--slippage", type=float, default=0.5)
|
|
606
|
+
inc.add_argument("--deadline", type=int, default=600)
|
|
607
|
+
inc.add_argument("--rpc-url")
|
|
608
|
+
inc.add_argument("--output")
|
|
609
|
+
|
|
610
|
+
c = sub.add_parser("collect", help="Collect fees")
|
|
611
|
+
c.add_argument("--chain", required=True)
|
|
612
|
+
c.add_argument("--token-id", type=int, required=True)
|
|
613
|
+
c.add_argument("--recipient")
|
|
614
|
+
c.add_argument("--rpc-url")
|
|
615
|
+
c.add_argument("--output")
|
|
616
|
+
|
|
617
|
+
args = parser.parse_args()
|
|
618
|
+
load_local_env()
|
|
619
|
+
|
|
620
|
+
if args.command == "mint":
|
|
621
|
+
result = build_v4_mint_transaction(
|
|
622
|
+
chain_name=args.chain, token_a=args.token_a, token_b=args.token_b,
|
|
623
|
+
fee=args.fee, tick_spacing=args.tick_spacing,
|
|
624
|
+
tick_lower=args.tick_lower, tick_upper=args.tick_upper,
|
|
625
|
+
amount_a=args.amount_a, amount_b=args.amount_b,
|
|
626
|
+
slippage_pct=args.slippage, hooks=args.hooks,
|
|
627
|
+
recipient=args.recipient, deadline_seconds=args.deadline,
|
|
628
|
+
rpc_url=args.rpc_url, request_only=args.request_only,
|
|
629
|
+
)
|
|
630
|
+
print(f"V4 Mint tx: {args.token_a}/{args.token_b} fee={args.fee} ts={args.tick_spacing}")
|
|
631
|
+
if args.output:
|
|
632
|
+
Path(args.output).write_text(json.dumps(result, ensure_ascii=False, indent=2) + "\n", encoding="utf-8")
|
|
633
|
+
dump_json(result)
|
|
634
|
+
elif args.command == "decrease":
|
|
635
|
+
result = build_v4_decrease_liquidity_transaction(
|
|
636
|
+
chain_name=args.chain, token_id=args.token_id,
|
|
637
|
+
liquidity_pct=args.liquidity_pct, slippage_pct=args.slippage,
|
|
638
|
+
deadline_seconds=args.deadline, rpc_url=args.rpc_url,
|
|
639
|
+
)
|
|
640
|
+
print(f"V4 Decrease tx: position #{args.token_id} remove {args.liquidity_pct}%")
|
|
641
|
+
if args.output:
|
|
642
|
+
Path(args.output).write_text(json.dumps(result, ensure_ascii=False, indent=2) + "\n", encoding="utf-8")
|
|
643
|
+
dump_json(result)
|
|
644
|
+
elif args.command == "increase":
|
|
645
|
+
result = build_v4_increase_liquidity_transaction(
|
|
646
|
+
chain_name=args.chain, token_id=args.token_id,
|
|
647
|
+
amount0=args.amount0, amount1=args.amount1,
|
|
648
|
+
slippage_pct=args.slippage, deadline_seconds=args.deadline,
|
|
649
|
+
rpc_url=args.rpc_url,
|
|
650
|
+
)
|
|
651
|
+
print(f"V4 Increase tx: position #{args.token_id}")
|
|
652
|
+
if args.output:
|
|
653
|
+
Path(args.output).write_text(json.dumps(result, ensure_ascii=False, indent=2) + "\n", encoding="utf-8")
|
|
654
|
+
dump_json(result)
|
|
655
|
+
elif args.command == "collect":
|
|
656
|
+
result = build_v4_collect_transaction(
|
|
657
|
+
chain_name=args.chain, token_id=args.token_id,
|
|
658
|
+
recipient=args.recipient, rpc_url=args.rpc_url,
|
|
659
|
+
)
|
|
660
|
+
print(f"V4 Collect tx: position #{args.token_id}")
|
|
661
|
+
if args.output:
|
|
662
|
+
Path(args.output).write_text(json.dumps(result, ensure_ascii=False, indent=2) + "\n", encoding="utf-8")
|
|
663
|
+
dump_json(result)
|
|
664
|
+
else:
|
|
665
|
+
parser.print_help()
|
|
666
|
+
|
|
667
|
+
|
|
668
|
+
if __name__ == "__main__":
|
|
669
|
+
main()
|