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,175 @@
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
+ get_v3_factory_address,
14
+ load_local_env,
15
+ normalize_chain,
16
+ resolve_token,
17
+ sort_token_addresses,
18
+ validate_fee_tier,
19
+ )
20
+ from uniswap_autopilot.execute._internal.rpc import (
21
+ decode_address, decode_int256, decode_uint, encode_address, encode_selector,
22
+ encode_uint, eth_call, resolve_rpc_url,
23
+ )
24
+ from uniswap_autopilot.lp.v3.tick import fee_tier_to_tick_spacing, tick_to_price
25
+
26
+
27
+ def query_pool_address(token0: str, token1: str, fee: int, factory: str, rpc_url: str) -> str:
28
+ sel = encode_selector("getPool(address,address,uint24)")
29
+ data = sel + encode_address(token0).replace("0x", "") + encode_address(token1).replace("0x", "") + encode_uint(fee).replace("0x", "")
30
+ raw = eth_call(factory, data, rpc_url)
31
+ addr = decode_address(raw)
32
+ if addr == "0x" + "0" * 40:
33
+ return "0x0000000000000000000000000000000000000000"
34
+ return addr
35
+
36
+
37
+ def query_slot0(pool_address: str, rpc_url: str) -> dict[str, Any]:
38
+ sel = encode_selector("slot0()")
39
+ raw = eth_call(pool_address, sel, rpc_url)
40
+ clean = raw.replace("0x", "")
41
+ # slot0 returns 7 values: uint160, int24, uint16, uint16, uint16, uint8, bool
42
+ if len(clean) < 7 * 64:
43
+ raise RuntimeError(f"slot0 returned unexpected output: {raw}")
44
+ sqrt_price_x96 = decode_uint("0x" + clean[0:64])
45
+ tick = decode_int256("0x" + clean[64:128])
46
+ observation_index = decode_uint("0x" + clean[128:192])
47
+ observation_cardinality = decode_uint("0x" + clean[192:256])
48
+ observation_cardinality_next = decode_uint("0x" + clean[256:320])
49
+ fee_protocol = decode_uint("0x" + clean[320:384])
50
+ unlocked = decode_uint("0x" + clean[384:448]) != 0
51
+ return {
52
+ "sqrtPriceX96": sqrt_price_x96,
53
+ "tick": tick,
54
+ "observationIndex": observation_index,
55
+ "observationCardinality": observation_cardinality,
56
+ "observationCardinalityNext": observation_cardinality_next,
57
+ "feeProtocol": fee_protocol,
58
+ "unlocked": unlocked,
59
+ }
60
+
61
+
62
+ def query_pool_liquidity(pool_address: str, rpc_url: str) -> int:
63
+ sel = encode_selector("liquidity()")
64
+ raw = eth_call(pool_address, sel, rpc_url)
65
+ return decode_uint(raw)
66
+
67
+
68
+ def query_pool_tokens(pool_address: str, rpc_url: str) -> tuple[str, str]:
69
+ sel0 = encode_selector("token0()")
70
+ raw0 = eth_call(pool_address, sel0, rpc_url)
71
+ t0 = decode_address(raw0)
72
+ sel1 = encode_selector("token1()")
73
+ raw1 = eth_call(pool_address, sel1, rpc_url)
74
+ t1 = decode_address(raw1)
75
+ return t0, t1
76
+
77
+
78
+ def query_pool_fee(pool_address: str, rpc_url: str) -> int:
79
+ sel = encode_selector("fee()")
80
+ raw = eth_call(pool_address, sel, rpc_url)
81
+ return decode_uint(raw)
82
+
83
+
84
+ def query_pool_full_info(
85
+ chain_name: str,
86
+ token_a: str,
87
+ token_b: str,
88
+ fee_tier: int,
89
+ rpc_url: str | None = None,
90
+ ) -> dict[str, Any]:
91
+ chain = normalize_chain(chain_name)
92
+ rpc, _ = resolve_rpc_url(rpc_url, chain.chain_id)
93
+ token_a_info = resolve_token(chain, token_a, rpc)
94
+ token_b_info = resolve_token(chain, token_b, rpc)
95
+ fee = validate_fee_tier(fee_tier)
96
+
97
+ def _resolve_addr(info: dict[str, Any]) -> str:
98
+ if info["address"] != "NATIVE":
99
+ return info["address"]
100
+ wrapped = chain.tokens.get(chain.wrapped_native_symbol.upper())
101
+ if not wrapped:
102
+ raise ValueError(f"wrapped native token not configured for chain '{chain.key}'")
103
+ return wrapped.address
104
+
105
+ addr_a = _resolve_addr(token_a_info)
106
+ addr_b = _resolve_addr(token_b_info)
107
+ token0_addr, token1_addr = sort_token_addresses(addr_a, addr_b)
108
+
109
+ factory = get_v3_factory_address(chain_name)
110
+ if not rpc:
111
+ raise RuntimeError(f"RPC URL not configured for {chain_name}; set {chain_name.upper()}_RPC_URL")
112
+
113
+ pool_address = query_pool_address(token0_addr, token1_addr, fee, factory, rpc)
114
+ if pool_address == "0x0000000000000000000000000000000000000000":
115
+ return {
116
+ "action": "pool_info",
117
+ "chain": {"key": chain.key, "chainId": chain.chain_id},
118
+ "tokenA": token_a_info,
119
+ "tokenB": token_b_info,
120
+ "token0": token0_addr,
121
+ "token1": token1_addr,
122
+ "feeTier": fee,
123
+ "poolAddress": None,
124
+ "exists": False,
125
+ }
126
+
127
+ slot0 = query_slot0(pool_address, rpc)
128
+ liquidity = query_pool_liquidity(pool_address, rpc)
129
+ tick_spacing = fee_tier_to_tick_spacing(fee)
130
+
131
+ decimals0 = token_a_info["decimals"] if token0_addr == addr_a else token_b_info["decimals"]
132
+ decimals1 = token_b_info["decimals"] if token1_addr == addr_b else token_a_info["decimals"]
133
+ current_price = tick_to_price(slot0["tick"], decimals0, decimals1)
134
+
135
+ return {
136
+ "action": "pool_info",
137
+ "chain": {"key": chain.key, "chainId": chain.chain_id},
138
+ "tokenA": token_a_info,
139
+ "tokenB": token_b_info,
140
+ "token0": token0_addr,
141
+ "token1": token1_addr,
142
+ "feeTier": fee,
143
+ "tickSpacing": tick_spacing,
144
+ "poolAddress": pool_address,
145
+ "exists": True,
146
+ "slot0": slot0,
147
+ "currentTick": slot0["tick"],
148
+ "currentPrice": current_price,
149
+ "liquidity": str(liquidity),
150
+ }
151
+
152
+
153
+ def main() -> None:
154
+ parser = argparse.ArgumentParser(description="查询 Uniswap v3 池子状态")
155
+ parser.add_argument("--chain", required=True, help="链名,例如 base / ethereum")
156
+ parser.add_argument("--token-a", required=True, help="代币 symbol 或地址")
157
+ parser.add_argument("--token-b", required=True, help="代币 symbol 或地址")
158
+ parser.add_argument("--fee-tier", type=int, required=True, help="手续费等级: 100 / 500 / 3000 / 10000")
159
+ parser.add_argument("--rpc-url", help="RPC URL,不提供则从环境变量读取")
160
+ parser.add_argument("--output", help="输出 JSON 文件路径")
161
+ args = parser.parse_args()
162
+
163
+ try:
164
+ load_local_env()
165
+ result = query_pool_full_info(args.chain, args.token_a, args.token_b, args.fee_tier, args.rpc_url)
166
+ if args.output:
167
+ Path(args.output).write_text(json.dumps(result, ensure_ascii=False, indent=2) + "\n", encoding="utf-8")
168
+ dump_json(result)
169
+ except Exception as exc:
170
+ print(json.dumps({"error": str(exc)}, ensure_ascii=False, indent=2), file=sys.stderr)
171
+ sys.exit(1)
172
+
173
+
174
+ if __name__ == "__main__":
175
+ main()
@@ -0,0 +1,112 @@
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
+ get_position_manager_address,
14
+ load_local_env,
15
+ normalize_chain,
16
+ )
17
+ from uniswap_autopilot.execute._internal.rpc import (
18
+ decode_address, decode_int256, decode_uint, encode_address, encode_selector,
19
+ encode_uint, eth_call, resolve_rpc_url,
20
+ )
21
+
22
+
23
+ def query_position(token_id: int, chain_name: str, rpc_url: str | None = None) -> dict[str, Any]:
24
+ chain = normalize_chain(chain_name)
25
+ pm = get_position_manager_address(chain_name)
26
+ rpc, _ = resolve_rpc_url(rpc_url, chain.chain_id)
27
+ if not rpc:
28
+ raise RuntimeError(f"RPC URL not configured for {chain_name}")
29
+
30
+ sel = encode_selector("positions(uint256)")
31
+ data = sel + encode_uint(token_id).replace("0x", "")
32
+ raw = eth_call(pm, data, rpc)
33
+ clean = raw.replace("0x", "")
34
+ # positions returns 12 uint256/int256/address values
35
+ if len(clean) < 12 * 64:
36
+ raise RuntimeError(f"positions() returned unexpected output: {raw}")
37
+
38
+ def _u(offset: int) -> int:
39
+ return decode_uint("0x" + clean[offset:offset + 64])
40
+
41
+ def _i(offset: int) -> int:
42
+ return decode_int256("0x" + clean[offset:offset + 64])
43
+
44
+ def _a(offset: int) -> str:
45
+ return decode_address("0x" + clean[offset:offset + 64])
46
+
47
+ return {
48
+ "tokenId": token_id,
49
+ "nonce": _u(0),
50
+ "operator": _a(64),
51
+ "token0": _a(128),
52
+ "token1": _a(192),
53
+ "fee": _u(256),
54
+ "tickLower": _i(320),
55
+ "tickUpper": _i(384),
56
+ "liquidity": str(_u(448)),
57
+ "feeGrowthInside0LastX128": str(_u(512)),
58
+ "feeGrowthInside1LastX128": str(_u(576)),
59
+ "tokensOwed0": str(_u(640)),
60
+ "tokensOwed1": str(_u(704)),
61
+ }
62
+
63
+
64
+ def query_positions_by_owner(owner: str, chain_name: str, rpc_url: str | None = None) -> list[int]:
65
+ chain = normalize_chain(chain_name)
66
+ pm = get_position_manager_address(chain_name)
67
+ rpc, _ = resolve_rpc_url(rpc_url, chain.chain_id)
68
+ if not rpc:
69
+ raise RuntimeError(f"RPC URL not configured for {chain_name}")
70
+
71
+ bal_sel = encode_selector("balanceOf(address)")
72
+ bal_data = bal_sel + encode_address(owner).replace("0x", "")
73
+ raw_bal = eth_call(pm, bal_data, rpc)
74
+ balance = decode_uint(raw_bal)
75
+
76
+ token_ids: list[int] = []
77
+ tobi_sel = encode_selector("tokenOfOwnerByIndex(address,uint256)")
78
+ for i in range(balance):
79
+ data = tobi_sel + encode_address(owner).replace("0x", "") + encode_uint(i).replace("0x", "")
80
+ raw_tid = eth_call(pm, data, rpc)
81
+ token_ids.append(decode_uint(raw_tid))
82
+ return token_ids
83
+
84
+
85
+ def main() -> None:
86
+ parser = argparse.ArgumentParser(description="查询 Uniswap v3 LP 仓位信息")
87
+ parser.add_argument("--chain", required=True, help="链名")
88
+ source = parser.add_mutually_exclusive_group(required=True)
89
+ source.add_argument("--token-id", type=int, help="LP NFT token ID")
90
+ source.add_argument("--owner", help="钱包地址,列出所有仓位")
91
+ parser.add_argument("--rpc-url", help="RPC URL")
92
+ parser.add_argument("--output", help="输出 JSON 文件路径")
93
+ args = parser.parse_args()
94
+
95
+ try:
96
+ load_local_env()
97
+ if args.token_id is not None:
98
+ result = {"action": "position_info", "position": query_position(args.token_id, args.chain, args.rpc_url)}
99
+ else:
100
+ token_ids = query_positions_by_owner(args.owner, args.chain, args.rpc_url)
101
+ positions = [query_position(tid, args.chain, args.rpc_url) for tid in token_ids]
102
+ result = {"action": "positions_by_owner", "owner": args.owner, "count": len(positions), "positions": positions}
103
+ if args.output:
104
+ Path(args.output).write_text(json.dumps(result, ensure_ascii=False, indent=2) + "\n", encoding="utf-8")
105
+ dump_json(result)
106
+ except Exception as exc:
107
+ print(json.dumps({"error": str(exc)}, ensure_ascii=False, indent=2), file=sys.stderr)
108
+ sys.exit(1)
109
+
110
+
111
+ if __name__ == "__main__":
112
+ main()
@@ -0,0 +1,75 @@
1
+ #!/usr/bin/env python3
2
+ from __future__ import annotations
3
+
4
+ from decimal import Decimal
5
+
6
+ MIN_TICK = -887272
7
+ MAX_TICK = 887272
8
+
9
+ _TICK_SPACING_BY_FEE: dict[int, int] = {
10
+ 100: 1,
11
+ 500: 10,
12
+ 3000: 60,
13
+ 10000: 200,
14
+ }
15
+
16
+
17
+ def fee_tier_to_tick_spacing(fee: int) -> int:
18
+ if fee not in _TICK_SPACING_BY_FEE:
19
+ raise ValueError(f"unknown fee tier {fee}; valid: {sorted(_TICK_SPACING_BY_FEE)}")
20
+ return _TICK_SPACING_BY_FEE[fee]
21
+
22
+
23
+ def nearest_usable_tick(tick: int, tick_spacing: int) -> int:
24
+ if tick_spacing <= 0:
25
+ raise ValueError(f"tick_spacing must be positive, got {tick_spacing}")
26
+ rounded = round(tick / tick_spacing) * tick_spacing
27
+ return max(MIN_TICK, min(MAX_TICK, rounded))
28
+
29
+
30
+ def price_to_tick(price: float, decimals0: int, decimals1: int) -> int:
31
+ if price <= 0:
32
+ raise ValueError("price must be positive")
33
+ adjusted = Decimal(str(price)) * (Decimal(10) ** (decimals0 - decimals1))
34
+ tick = int(adjusted.ln() / Decimal("1.0001").ln())
35
+ return max(MIN_TICK, min(MAX_TICK, tick))
36
+
37
+
38
+ def tick_to_price(tick: int, decimals0: int, decimals1: int) -> float:
39
+ adjusted = Decimal("1.0001") ** tick
40
+ price = adjusted * (Decimal(10) ** (decimals1 - decimals0))
41
+ return float(price)
42
+
43
+
44
+ def tick_to_sqrt_price_x96(tick: int) -> int:
45
+ adjusted = Decimal("1.0001") ** tick
46
+ sqrt_price = adjusted.sqrt()
47
+ return int(sqrt_price * (Decimal(2) ** 96))
48
+
49
+
50
+ def suggest_ticks_for_range(
51
+ current_tick: int,
52
+ tick_spacing: int,
53
+ price_lower: float | None = None,
54
+ price_upper: float | None = None,
55
+ decimals0: int = 18,
56
+ decimals1: int = 18,
57
+ ) -> tuple[int, int]:
58
+ if price_lower is not None and price_upper is not None:
59
+ lower = price_to_tick(price_lower, decimals0, decimals1)
60
+ upper = price_to_tick(price_upper, decimals0, decimals1)
61
+ elif price_lower is not None:
62
+ lower = price_to_tick(price_lower, decimals0, decimals1)
63
+ upper = MAX_TICK
64
+ elif price_upper is not None:
65
+ lower = MIN_TICK
66
+ upper = price_to_tick(price_upper, decimals0, decimals1)
67
+ else:
68
+ return (
69
+ nearest_usable_tick(MIN_TICK, tick_spacing),
70
+ nearest_usable_tick(MAX_TICK, tick_spacing),
71
+ )
72
+ return (
73
+ nearest_usable_tick(lower, tick_spacing),
74
+ nearest_usable_tick(upper, tick_spacing),
75
+ )
File without changes
@@ -0,0 +1,105 @@
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_position_manager_address,
13
+ load_local_env,
14
+ normalize_chain,
15
+ resolve_token,
16
+ )
17
+ from uniswap_autopilot.execute._internal.rpc import (
18
+ build_calldata, encode_address, encode_uint, query_erc20_allowance, resolve_rpc_url,
19
+ )
20
+
21
+
22
+ def check_v4_approvals(
23
+ token0_address: str,
24
+ token1_address: str,
25
+ owner: str,
26
+ chain_name: str,
27
+ rpc_url: str | None = None,
28
+ ) -> dict[str, Any]:
29
+ chain = normalize_chain(chain_name)
30
+ pm = get_v4_position_manager_address(chain_name)
31
+ rpc, _ = resolve_rpc_url(rpc_url, chain.chain_id)
32
+ if not rpc:
33
+ raise RuntimeError(f"RPC URL not configured for {chain_name}")
34
+
35
+ allowance0 = query_erc20_allowance(token0_address, owner, pm, rpc)
36
+ allowance1 = query_erc20_allowance(token1_address, owner, pm, rpc)
37
+
38
+ return {
39
+ "action": "v4_approval_check",
40
+ "owner": owner,
41
+ "spender": pm,
42
+ "token0": {"address": token0_address, "allowance": str(allowance0), "needsApproval": allowance0 == 0},
43
+ "token1": {"address": token1_address, "allowance": str(allowance1), "needsApproval": allowance1 == 0},
44
+ }
45
+
46
+
47
+ def build_approval_calldata(spender: str, amount: str | None = None) -> str:
48
+ approve_amount = amount or str(2**256 - 1)
49
+ return build_calldata(
50
+ "approve(address,uint256)",
51
+ encode_address(spender),
52
+ encode_uint(int(approve_amount)),
53
+ )
54
+
55
+
56
+ def build_approval_tx(
57
+ token: str,
58
+ owner: str,
59
+ chain_name: str,
60
+ amount: str | None = None,
61
+ ) -> dict[str, Any]:
62
+ chain = normalize_chain(chain_name)
63
+ pm = get_v4_position_manager_address(chain_name)
64
+ calldata = build_approval_calldata(pm, amount)
65
+ return {
66
+ "kind": "approval",
67
+ "to": token,
68
+ "data": calldata,
69
+ "value": "0",
70
+ "chainId": chain.chain_id,
71
+ "from": owner,
72
+ }
73
+
74
+
75
+ def main() -> None:
76
+ parser = argparse.ArgumentParser(description="Check ERC-20 approvals for V4 PositionManager")
77
+ parser.add_argument("--chain", required=True)
78
+ parser.add_argument("--token-a", required=True)
79
+ parser.add_argument("--token-b", required=True)
80
+ parser.add_argument("--owner", required=True)
81
+ parser.add_argument("--rpc-url")
82
+ parser.add_argument("--output")
83
+ args = parser.parse_args()
84
+
85
+ load_local_env()
86
+ chain = normalize_chain(args.chain)
87
+ token_a = resolve_token(chain, args.token_a)
88
+ token_b = resolve_token(chain, args.token_b)
89
+ from uniswap_autopilot.common.common import sort_token_addresses
90
+ t0, t1 = sort_token_addresses(token_a["address"], token_b["address"])
91
+
92
+ result = check_v4_approvals(t0, t1, args.owner, args.chain, args.rpc_url)
93
+ for key in ("token0", "token1"):
94
+ if result[key]["needsApproval"]:
95
+ result[key]["approvalTx"] = build_approval_tx(
96
+ result[key]["address"], args.owner, args.chain,
97
+ )
98
+
99
+ if args.output:
100
+ Path(args.output).write_text(json.dumps(result, ensure_ascii=False, indent=2) + "\n", encoding="utf-8")
101
+ dump_json(result)
102
+
103
+
104
+ if __name__ == "__main__":
105
+ main()