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,368 @@
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.v4.approve import build_approval_tx, check_v4_approvals
17
+ from uniswap_autopilot.lp.v4.build_tx import (
18
+ build_v4_collect_transaction,
19
+ build_v4_decrease_liquidity_transaction,
20
+ build_v4_increase_liquidity_transaction,
21
+ build_v4_mint_transaction,
22
+ )
23
+ from uniswap_autopilot.lp.v4.pool import query_v4_pool_full_info
24
+
25
+ ZERO_ADDRESS = "0x0000000000000000000000000000000000000000"
26
+
27
+
28
+ def _broadcast_tx(tx: dict[str, Any], broadcast_args: argparse.Namespace, confirm_phrase: str) -> dict[str, Any]:
29
+ from uniswap_autopilot.execute._internal.submit import broadcast_with_backend
30
+ return broadcast_with_backend(
31
+ tx=tx,
32
+ explicit_rpc_url=None,
33
+ confirm=confirm_phrase,
34
+ signer_args_source=broadcast_args,
35
+ )
36
+
37
+
38
+ def run_v4_mint_flow(
39
+ chain_name: str,
40
+ token_a: str, token_b: str,
41
+ fee: int, tick_spacing: int,
42
+ tick_lower: int, tick_upper: int,
43
+ amount_a: str, amount_b: str,
44
+ slippage: float,
45
+ deadline_seconds: int,
46
+ hooks: str,
47
+ wallet: str | None,
48
+ output_dir: str,
49
+ rpc_url: str | None,
50
+ request_only: bool = False,
51
+ broadcast: bool = False,
52
+ broadcast_args: argparse.Namespace | None = None,
53
+ ) -> dict[str, Any]:
54
+ wallet_addr = resolve_wallet_address(wallet)
55
+ if not wallet_addr:
56
+ raise ValueError("wallet is required; pass --wallet or set wallet env")
57
+
58
+ pool_info = query_v4_pool_full_info(
59
+ chain_name, token_a, token_b, fee, tick_spacing, hooks, rpc_url,
60
+ )
61
+ if not pool_info.get("exists"):
62
+ raise ValueError(f"V4 pool does not exist for {token_a}/{token_b} fee={fee} tickSpacing={tick_spacing} on {chain_name}")
63
+
64
+ c0 = pool_info["currency0"]
65
+ c1 = pool_info["currency1"]
66
+ approval_status = check_v4_approvals(c0, c1, wallet_addr, chain_name, rpc_url)
67
+
68
+ mint_result = build_v4_mint_transaction(
69
+ chain_name=chain_name, token_a=token_a, token_b=token_b,
70
+ fee=fee, tick_spacing=tick_spacing,
71
+ tick_lower=tick_lower, tick_upper=tick_upper,
72
+ amount_a=amount_a, amount_b=amount_b,
73
+ slippage_pct=slippage, hooks=hooks,
74
+ recipient=wallet_addr,
75
+ deadline_seconds=deadline_seconds, rpc_url=rpc_url,
76
+ request_only=request_only,
77
+ )
78
+
79
+ output: dict[str, Any] = {
80
+ "action": "v4_mint_flow",
81
+ "pool": pool_info,
82
+ "approval": approval_status,
83
+ "mint": mint_result,
84
+ }
85
+
86
+ approval_txs = []
87
+ if approval_status["token0"]["needsApproval"]:
88
+ approval_txs.append(build_approval_tx(c0, wallet_addr, chain_name))
89
+ if approval_status["token1"]["needsApproval"]:
90
+ approval_txs.append(build_approval_tx(c1, wallet_addr, chain_name))
91
+ output["approvalTxs"] = approval_txs
92
+
93
+ if request_only:
94
+ output["nextActions"] = ["broadcast-approvals", "broadcast-mint"] if approval_txs else ["broadcast-mint"]
95
+ return output
96
+
97
+ chain = pool_info["chain"]
98
+ output_path = Path(output_dir)
99
+ output_path.mkdir(parents=True, exist_ok=True)
100
+
101
+ if broadcast and broadcast_args:
102
+ for i, atx in enumerate(approval_txs):
103
+ confirm = f"BROADCAST APPROVAL {chain['chainId']} {atx['to']}"
104
+ result = _broadcast_tx(atx, broadcast_args, confirm)
105
+ output[f"approval{i}_broadcast"] = result
106
+
107
+ mint_tx = mint_result["transaction"]
108
+ confirm = f"BROADCAST V4_MINT {chain['chainId']} {mint_tx['to']}"
109
+ result = _broadcast_tx(mint_tx, broadcast_args, confirm)
110
+ output["mintBroadcast"] = result
111
+ else:
112
+ output["nextActions"] = ["broadcast-approvals", "broadcast-mint"] if approval_txs else ["broadcast-mint"]
113
+
114
+ (output_path / "v4_mint_flow.json").write_text(
115
+ json.dumps(output, ensure_ascii=False, indent=2) + "\n", encoding="utf-8",
116
+ )
117
+ return output
118
+
119
+
120
+ def run_v4_increase_flow(
121
+ chain_name: str, token_id: int,
122
+ amount0: str, amount1: str,
123
+ slippage: float, deadline_seconds: int,
124
+ wallet: str | None, output_dir: str,
125
+ rpc_url: str | None,
126
+ broadcast: bool = False,
127
+ broadcast_args: argparse.Namespace | None = None,
128
+ ) -> dict[str, Any]:
129
+ wallet_addr = resolve_wallet_address(wallet)
130
+ if not wallet_addr:
131
+ raise ValueError("wallet is required")
132
+
133
+ inc_result = build_v4_increase_liquidity_transaction(
134
+ chain_name=chain_name, token_id=token_id,
135
+ amount0=amount0, amount1=amount1,
136
+ slippage_pct=slippage, deadline_seconds=deadline_seconds,
137
+ rpc_url=rpc_url,
138
+ )
139
+
140
+ pos = inc_result["position"]
141
+ c0 = pos["currency0"]["address"]
142
+ c1 = pos["currency1"]["address"]
143
+ approval_status = check_v4_approvals(c0, c1, wallet_addr, chain_name, rpc_url)
144
+
145
+ output: dict[str, Any] = {"action": "v4_increase_flow", "increase": inc_result, "approval": approval_status}
146
+
147
+ approval_txs = []
148
+ if approval_status["token0"]["needsApproval"]:
149
+ approval_txs.append(build_approval_tx(c0, wallet_addr, chain_name))
150
+ if approval_status["token1"]["needsApproval"]:
151
+ approval_txs.append(build_approval_tx(c1, wallet_addr, chain_name))
152
+
153
+ if broadcast and broadcast_args:
154
+ for i, atx in enumerate(approval_txs):
155
+ _broadcast_tx(atx, broadcast_args, f"BROADCAST APPROVAL {inc_result['chain']['chainId']} {atx['to']}")
156
+ result = _broadcast_tx(inc_result["transaction"], broadcast_args, f"BROADCAST V4_INCREASE {inc_result['chain']['chainId']} {inc_result['transaction']['to']}")
157
+ output["broadcast"] = result
158
+ else:
159
+ output["nextActions"] = ["broadcast-approvals", "broadcast-increase"] if approval_txs else ["broadcast-increase"]
160
+
161
+ output_path = Path(output_dir)
162
+ output_path.mkdir(parents=True, exist_ok=True)
163
+ (output_path / "v4_increase_flow.json").write_text(
164
+ json.dumps(output, ensure_ascii=False, indent=2) + "\n", encoding="utf-8",
165
+ )
166
+ return output
167
+
168
+
169
+ def run_v4_decrease_flow(
170
+ chain_name: str, token_id: int,
171
+ liquidity_pct: float, slippage: float,
172
+ deadline_seconds: int, wallet: str | None,
173
+ output_dir: str, rpc_url: str | None,
174
+ broadcast: bool = False,
175
+ broadcast_args: argparse.Namespace | None = None,
176
+ ) -> dict[str, Any]:
177
+ wallet_addr = resolve_wallet_address(wallet)
178
+ if not wallet_addr:
179
+ raise ValueError("wallet is required")
180
+
181
+ dec_result = build_v4_decrease_liquidity_transaction(
182
+ chain_name=chain_name, token_id=token_id,
183
+ liquidity_pct=liquidity_pct, slippage_pct=slippage,
184
+ deadline_seconds=deadline_seconds, rpc_url=rpc_url,
185
+ )
186
+
187
+ output: dict[str, Any] = {"action": "v4_decrease_flow", "decrease": dec_result}
188
+
189
+ if broadcast and broadcast_args:
190
+ result = _broadcast_tx(dec_result["transaction"], broadcast_args, f"BROADCAST V4_DECREASE {dec_result['chain']['chainId']} {dec_result['transaction']['to']}")
191
+ output["broadcast"] = result
192
+ else:
193
+ output["nextActions"] = ["broadcast-decrease"]
194
+
195
+ output_path = Path(output_dir)
196
+ output_path.mkdir(parents=True, exist_ok=True)
197
+ (output_path / "v4_decrease_flow.json").write_text(
198
+ json.dumps(output, ensure_ascii=False, indent=2) + "\n", encoding="utf-8",
199
+ )
200
+ return output
201
+
202
+
203
+ def run_v4_collect_flow(
204
+ chain_name: str, token_id: int,
205
+ recipient: str | None, wallet: str | None,
206
+ output_dir: str, rpc_url: str | None,
207
+ broadcast: bool = False,
208
+ broadcast_args: argparse.Namespace | None = None,
209
+ ) -> dict[str, Any]:
210
+ wallet_addr = resolve_wallet_address(wallet)
211
+ if not wallet_addr:
212
+ raise ValueError("wallet is required")
213
+
214
+ col_result = build_v4_collect_transaction(
215
+ chain_name=chain_name, token_id=token_id,
216
+ recipient=recipient or wallet_addr, rpc_url=rpc_url,
217
+ )
218
+
219
+ output: dict[str, Any] = {"action": "v4_collect_flow", "collect": col_result}
220
+
221
+ if broadcast and broadcast_args:
222
+ result = _broadcast_tx(col_result["transaction"], broadcast_args, f"BROADCAST V4_COLLECT {col_result['chain']['chainId']} {col_result['transaction']['to']}")
223
+ output["broadcast"] = result
224
+ else:
225
+ output["nextActions"] = ["broadcast-collect"]
226
+
227
+ output_path = Path(output_dir)
228
+ output_path.mkdir(parents=True, exist_ok=True)
229
+ (output_path / "v4_collect_flow.json").write_text(
230
+ json.dumps(output, ensure_ascii=False, indent=2) + "\n", encoding="utf-8",
231
+ )
232
+ return output
233
+
234
+
235
+ def main() -> None:
236
+ parser = argparse.ArgumentParser(description="Uniswap V4 LP full flow orchestration")
237
+ sub = parser.add_subparsers(dest="command", required=True)
238
+
239
+ # mint
240
+ mp = sub.add_parser("mint", help="Mint a new V4 LP position")
241
+ mp.add_argument("--chain", required=True)
242
+ mp.add_argument("--token-a", required=True)
243
+ mp.add_argument("--token-b", required=True)
244
+ mp.add_argument("--fee", type=int, required=True, help="Pool fee (e.g. 3000)")
245
+ mp.add_argument("--tick-spacing", type=int, required=True, help="Tick spacing (e.g. 60)")
246
+ mp.add_argument("--tick-lower", type=int, required=True)
247
+ mp.add_argument("--tick-upper", type=int, required=True)
248
+ mp.add_argument("--amount-a", required=True)
249
+ mp.add_argument("--amount-b", required=True)
250
+ mp.add_argument("--slippage", type=float, default=0.5)
251
+ mp.add_argument("--hooks", default=ZERO_ADDRESS)
252
+ mp.add_argument("--deadline", type=int, default=600)
253
+ mp.add_argument("--wallet")
254
+ mp.add_argument("--output-dir", default="")
255
+ mp.add_argument("--rpc-url")
256
+ mp.add_argument("--request-only", action="store_true")
257
+ mp.add_argument("--broadcast", action="store_true")
258
+ mp.add_argument("--confirm")
259
+ mp.add_argument("--private-key-env")
260
+ mp.add_argument("--keystore")
261
+ mp.add_argument("--account")
262
+ mp.add_argument("--trade-signer-url")
263
+ mp.add_argument("--trade-signer-token-env")
264
+
265
+ # increase
266
+ ip = sub.add_parser("increase", help="Increase V4 liquidity")
267
+ ip.add_argument("--chain", required=True)
268
+ ip.add_argument("--token-id", type=int, required=True)
269
+ ip.add_argument("--amount0", required=True)
270
+ ip.add_argument("--amount1", required=True)
271
+ ip.add_argument("--slippage", type=float, default=0.5)
272
+ ip.add_argument("--deadline", type=int, default=600)
273
+ ip.add_argument("--wallet")
274
+ ip.add_argument("--output-dir", default="")
275
+ ip.add_argument("--rpc-url")
276
+ ip.add_argument("--broadcast", action="store_true")
277
+ ip.add_argument("--confirm")
278
+ ip.add_argument("--private-key-env")
279
+ ip.add_argument("--keystore")
280
+ ip.add_argument("--account")
281
+ ip.add_argument("--trade-signer-url")
282
+ ip.add_argument("--trade-signer-token-env")
283
+
284
+ # decrease
285
+ dp = sub.add_parser("decrease", help="Decrease V4 liquidity")
286
+ dp.add_argument("--chain", required=True)
287
+ dp.add_argument("--token-id", type=int, required=True)
288
+ dp.add_argument("--liquidity-pct", type=float, required=True, help="Percentage to remove (1-100)")
289
+ dp.add_argument("--slippage", type=float, default=0.5)
290
+ dp.add_argument("--deadline", type=int, default=600)
291
+ dp.add_argument("--wallet")
292
+ dp.add_argument("--output-dir", default="")
293
+ dp.add_argument("--rpc-url")
294
+ dp.add_argument("--broadcast", action="store_true")
295
+ dp.add_argument("--confirm")
296
+ dp.add_argument("--private-key-env")
297
+ dp.add_argument("--keystore")
298
+ dp.add_argument("--account")
299
+ dp.add_argument("--trade-signer-url")
300
+ dp.add_argument("--trade-signer-token-env")
301
+
302
+ # collect
303
+ cp = sub.add_parser("collect", help="Collect V4 fees")
304
+ cp.add_argument("--chain", required=True)
305
+ cp.add_argument("--token-id", type=int, required=True)
306
+ cp.add_argument("--recipient")
307
+ cp.add_argument("--wallet")
308
+ cp.add_argument("--output-dir", default="")
309
+ cp.add_argument("--rpc-url")
310
+ cp.add_argument("--broadcast", action="store_true")
311
+ cp.add_argument("--confirm")
312
+ cp.add_argument("--private-key-env")
313
+ cp.add_argument("--keystore")
314
+ cp.add_argument("--account")
315
+ cp.add_argument("--trade-signer-url")
316
+ cp.add_argument("--trade-signer-token-env")
317
+
318
+ args = parser.parse_args()
319
+
320
+ try:
321
+ load_local_env()
322
+ if args.command == "mint":
323
+ result = run_v4_mint_flow(
324
+ chain_name=args.chain, token_a=args.token_a, token_b=args.token_b,
325
+ fee=args.fee, tick_spacing=args.tick_spacing,
326
+ tick_lower=args.tick_lower, tick_upper=args.tick_upper,
327
+ amount_a=args.amount_a, amount_b=args.amount_b,
328
+ slippage=args.slippage, deadline_seconds=args.deadline,
329
+ hooks=args.hooks,
330
+ wallet=args.wallet, output_dir=args.output_dir,
331
+ rpc_url=args.rpc_url, request_only=args.request_only,
332
+ broadcast=args.broadcast, broadcast_args=args if args.broadcast else None,
333
+ )
334
+ elif args.command == "increase":
335
+ result = run_v4_increase_flow(
336
+ chain_name=args.chain, token_id=args.token_id,
337
+ amount0=args.amount0, amount1=args.amount1,
338
+ slippage=args.slippage, deadline_seconds=args.deadline,
339
+ wallet=args.wallet, output_dir=args.output_dir,
340
+ rpc_url=args.rpc_url,
341
+ broadcast=args.broadcast, broadcast_args=args if args.broadcast else None,
342
+ )
343
+ elif args.command == "decrease":
344
+ result = run_v4_decrease_flow(
345
+ chain_name=args.chain, token_id=args.token_id,
346
+ liquidity_pct=args.liquidity_pct, slippage=args.slippage,
347
+ deadline_seconds=args.deadline, wallet=args.wallet,
348
+ output_dir=args.output_dir, rpc_url=args.rpc_url,
349
+ broadcast=args.broadcast, broadcast_args=args if args.broadcast else None,
350
+ )
351
+ elif args.command == "collect":
352
+ result = run_v4_collect_flow(
353
+ chain_name=args.chain, token_id=args.token_id,
354
+ recipient=args.recipient, wallet=args.wallet,
355
+ output_dir=args.output_dir, rpc_url=args.rpc_url,
356
+ broadcast=args.broadcast, broadcast_args=args if args.broadcast else None,
357
+ )
358
+ else:
359
+ parser.print_help()
360
+ sys.exit(1)
361
+ dump_json(result)
362
+ except Exception as exc:
363
+ print(json.dumps({"error": str(exc)}, ensure_ascii=False, indent=2), file=sys.stderr)
364
+ sys.exit(1)
365
+
366
+
367
+ if __name__ == "__main__":
368
+ main()
@@ -0,0 +1,174 @@
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_v4_pool_manager_address,
13
+ get_v4_state_view_address,
14
+ load_local_env,
15
+ normalize_chain,
16
+ resolve_token,
17
+ sort_token_addresses,
18
+ )
19
+ from uniswap_autopilot.execute._internal.rpc import (
20
+ decode_int256, decode_uint, encode_address, encode_bytes32, encode_selector,
21
+ encode_uint, eth_call, resolve_rpc_url,
22
+ )
23
+ from uniswap_autopilot.lp.v3.tick import tick_to_price
24
+
25
+ import hashlib
26
+
27
+ ZERO_ADDRESS = "0x0000000000000000000000000000000000000000"
28
+
29
+
30
+ def _encode_parameters(tick_spacing: int, hooks_registration: int = 0) -> int:
31
+ return (tick_spacing << 24) | hooks_registration
32
+
33
+
34
+ def compute_pool_id(
35
+ currency0: str,
36
+ currency1: str,
37
+ hooks: str,
38
+ pool_manager: str,
39
+ fee: int,
40
+ tick_spacing: int,
41
+ ) -> str:
42
+ c0 = currency0.lower()
43
+ c1 = currency1.lower()
44
+ if c0 > c1:
45
+ c0, c1 = c1, c0
46
+ params = _encode_parameters(tick_spacing)
47
+ # abi.encode(address,address,address,address,uint24,uint256)
48
+ encoded = (
49
+ encode_address(c0)
50
+ + encode_address(c1)
51
+ + encode_address(hooks)
52
+ + encode_address(pool_manager)
53
+ + encode_uint(fee)
54
+ + encode_uint(params)
55
+ )
56
+ raw_bytes = bytes.fromhex(encoded)
57
+ return "0x" + hashlib.sha3_256(raw_bytes).hexdigest()
58
+
59
+
60
+ def query_v4_slot0(
61
+ state_view: str,
62
+ pool_id: str,
63
+ rpc_url: str,
64
+ ) -> dict[str, Any]:
65
+ sel = encode_selector("getSlot0(bytes32)")
66
+ data = sel + encode_bytes32(pool_id).replace("0x", "")
67
+ raw = eth_call(state_view, data, rpc_url)
68
+ clean = raw.replace("0x", "")
69
+ # returns (uint160, int24, uint24, uint24)
70
+ sqrt_price_x96 = decode_uint("0x" + clean[0:64])
71
+ tick = decode_int256("0x" + clean[64:128])
72
+ protocol_fee = decode_uint("0x" + clean[128:192])
73
+ lp_fee = decode_uint("0x" + clean[192:256])
74
+ return {
75
+ "sqrtPriceX96": str(sqrt_price_x96),
76
+ "tick": tick,
77
+ "protocolFee": protocol_fee,
78
+ "lpFee": lp_fee,
79
+ }
80
+
81
+
82
+ def query_v4_liquidity(
83
+ state_view: str,
84
+ pool_id: str,
85
+ rpc_url: str,
86
+ ) -> int:
87
+ sel = encode_selector("getLiquidity(bytes32)")
88
+ data = sel + encode_bytes32(pool_id).replace("0x", "")
89
+ raw = eth_call(state_view, data, rpc_url)
90
+ return decode_uint(raw)
91
+
92
+
93
+ def query_v4_pool_full_info(
94
+ chain_name: str,
95
+ token_a: str,
96
+ token_b: str,
97
+ fee: int,
98
+ tick_spacing: int,
99
+ hooks: str = ZERO_ADDRESS,
100
+ rpc_url: str | None = None,
101
+ ) -> dict[str, Any]:
102
+ chain = normalize_chain(chain_name)
103
+ rpc, _ = resolve_rpc_url(rpc_url, chain.chain_id)
104
+ if not rpc:
105
+ raise RuntimeError(f"RPC URL not configured for {chain_name}")
106
+
107
+ pool_manager = get_v4_pool_manager_address(chain_name)
108
+ state_view = get_v4_state_view_address(chain_name)
109
+
110
+ tok_a = resolve_token(chain, token_a, rpc)
111
+ tok_b = resolve_token(chain, token_b, rpc)
112
+ c0, c1 = sort_token_addresses(tok_a["address"], tok_b["address"])
113
+ decimals0 = tok_a["decimals"] if c0.lower() == tok_a["address"].lower() else tok_b["decimals"]
114
+ decimals1 = tok_b["decimals"] if c1.lower() == tok_b["address"].lower() else tok_a["decimals"]
115
+
116
+ pool_id = compute_pool_id(c0, c1, hooks, pool_manager, fee, tick_spacing)
117
+
118
+ slot0 = query_v4_slot0(state_view, pool_id, rpc)
119
+ liquidity = query_v4_liquidity(state_view, pool_id, rpc)
120
+
121
+ current_tick = slot0["tick"]
122
+ current_price = tick_to_price(current_tick, decimals0, decimals1)
123
+
124
+ return {
125
+ "action": "v4_pool_info",
126
+ "chain": {"key": chain.key, "chainId": chain.chain_id},
127
+ "currency0": c0,
128
+ "currency1": c1,
129
+ "fee": fee,
130
+ "tickSpacing": tick_spacing,
131
+ "hooks": hooks,
132
+ "poolId": pool_id,
133
+ "poolManager": pool_manager,
134
+ "slot0": slot0,
135
+ "currentTick": current_tick,
136
+ "currentPrice": current_price,
137
+ "liquidity": str(liquidity),
138
+ "exists": slot0["sqrtPriceX96"] != "0",
139
+ }
140
+
141
+
142
+ def main() -> None:
143
+ parser = argparse.ArgumentParser(description="Uniswap V4 pool state queries via StateView")
144
+ parser.add_argument("--chain", required=True)
145
+ parser.add_argument("--token-a", required=True)
146
+ parser.add_argument("--token-b", required=True)
147
+ parser.add_argument("--fee", type=int, required=True, help="Pool fee (e.g. 3000)")
148
+ parser.add_argument("--tick-spacing", type=int, required=True, help="Tick spacing (e.g. 60)")
149
+ parser.add_argument("--hooks", default=ZERO_ADDRESS, help="Hooks contract address (default: zero address)")
150
+ parser.add_argument("--rpc-url")
151
+ parser.add_argument("--output")
152
+ args = parser.parse_args()
153
+
154
+ load_local_env()
155
+
156
+ result = query_v4_pool_full_info(
157
+ args.chain, args.token_a, args.token_b,
158
+ args.fee, args.tick_spacing, args.hooks, args.rpc_url,
159
+ )
160
+ if result["exists"]:
161
+ print(f"V4 Pool: {args.token_a}/{args.token_b} fee={args.fee} tickSpacing={args.tick_spacing}")
162
+ print(f" PoolId: {result['poolId']}")
163
+ print(f" Tick: {result['currentTick']} Price: {result['currentPrice']:.6f}")
164
+ print(f" Liquidity: {result['liquidity']}")
165
+ else:
166
+ print(f"V4 Pool not found: {args.token_a}/{args.token_b} fee={args.fee} tickSpacing={args.tick_spacing}")
167
+
168
+ if args.output:
169
+ Path(args.output).write_text(json.dumps(result, ensure_ascii=False, indent=2) + "\n", encoding="utf-8")
170
+ dump_json(result)
171
+
172
+
173
+ if __name__ == "__main__":
174
+ main()