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,298 @@
1
+ """Transaction signing via eth-account (pure Python).
2
+
3
+ Private key is read from an environment variable — never stored on disk.
4
+ Requires the ``[signer]`` extra::
5
+
6
+ pip install uniswap-autopilot[signer]
7
+ export EXECUTOR_PRIVATE_KEY=0x...
8
+ """
9
+
10
+ from __future__ import annotations
11
+
12
+ import argparse
13
+ import os
14
+ from typing import Any
15
+
16
+ from uniswap_autopilot.common.common import load_local_env, resolve_wallet_address
17
+ from uniswap_autopilot.execute._internal.constants import (
18
+ HOT_WALLET_PRIVATE_KEY_ENV_NAME,
19
+ )
20
+
21
+ # ── Pure Python signer (optional) ──────────────────────────────────────────
22
+
23
+ try:
24
+ from uniswap_autopilot.execute._internal.pure_signer import (
25
+ is_available as _pure_signer_available,
26
+ )
27
+ except ImportError:
28
+
29
+ def _pure_signer_available() -> bool:
30
+ return False
31
+
32
+
33
+ # ── Namespace / arg helpers ────────────────────────────────────────────────
34
+
35
+
36
+ def has_direct_signer(args: argparse.Namespace) -> bool:
37
+ return bool(getattr(args, "private_key_env", None))
38
+
39
+
40
+ def build_signer_namespace(
41
+ *,
42
+ private_key_env: str | None = None,
43
+ wallet_source: str | None = None,
44
+ ) -> argparse.Namespace:
45
+ return argparse.Namespace(
46
+ private_key_env=private_key_env,
47
+ wallet_source=wallet_source,
48
+ )
49
+
50
+
51
+ # ── Wallet resolution ─────────────────────────────────────────────────────
52
+
53
+
54
+ def resolve_wallet(args: argparse.Namespace | None) -> str | None:
55
+ """Resolve wallet address from args or environment."""
56
+ if args is not None and getattr(args, "private_key_env", None):
57
+ from uniswap_autopilot.execute._internal.pure_signer import get_address
58
+
59
+ addr = get_address(args.private_key_env)
60
+ if addr:
61
+ return addr
62
+ return resolve_wallet_address(args)
63
+
64
+
65
+ # ── Backend detection ──────────────────────────────────────────────────────
66
+
67
+
68
+ def detect_hot_wallet_backend() -> dict[str, Any]:
69
+ """Detect if pure-signer (eth-account) is available."""
70
+ load_local_env()
71
+ private_key_env = os.environ.get(HOT_WALLET_PRIVATE_KEY_ENV_NAME, "").strip() or None
72
+ wallet = resolve_wallet_address(None, preference="hot")
73
+
74
+ result: dict[str, Any] = {
75
+ "backend": "pure-python",
76
+ "configured": wallet is not None and private_key_env is not None,
77
+ "available": False,
78
+ "wallet": wallet,
79
+ "walletFound": wallet is not None,
80
+ "mode": None,
81
+ "reason": None,
82
+ "signerArgs": None,
83
+ }
84
+ if not wallet:
85
+ result["reason"] = "WALLET_ADDRESS is not configured"
86
+ return result
87
+ if not private_key_env:
88
+ result["reason"] = "Private key env var not configured"
89
+ return result
90
+ if not os.environ.get(private_key_env, "").strip():
91
+ result["reason"] = f"{private_key_env} is not set"
92
+ return result
93
+ if not _pure_signer_available():
94
+ result["reason"] = "eth-account not installed (pip install uniswap-autopilot[signer])"
95
+ return result
96
+
97
+ result["mode"] = "private-key-env"
98
+ result["available"] = True
99
+ result["signerArgs"] = build_signer_namespace(
100
+ private_key_env=private_key_env,
101
+ wallet_source="hot",
102
+ )
103
+ return result
104
+
105
+
106
+ def auto_select_signer_args(args: argparse.Namespace | None = None) -> argparse.Namespace | None:
107
+ """Auto-select signer args from environment."""
108
+ load_local_env()
109
+ if args is not None:
110
+ if has_direct_signer(args):
111
+ return args
112
+
113
+ # Try hot wallet backend
114
+ hot = detect_hot_wallet_backend()
115
+ if hot["available"]:
116
+ return hot["signerArgs"]
117
+
118
+ # Try secure wallet
119
+ secure_wallet = resolve_wallet_address(None, preference="secure")
120
+ if secure_wallet:
121
+ pk_env = os.environ.get(HOT_WALLET_PRIVATE_KEY_ENV_NAME, "").strip() or None
122
+ if pk_env and os.environ.get(pk_env, "").strip():
123
+ return build_signer_namespace(
124
+ private_key_env=pk_env,
125
+ wallet_source="secure",
126
+ )
127
+
128
+ return None
129
+
130
+
131
+ def ensure_signer_backend(args: argparse.Namespace | None, action_name: str = "broadcast") -> str:
132
+ """Ensure a signer is available. Returns 'pure-python'."""
133
+ if args is not None and has_direct_signer(args):
134
+ if _pure_signer_available():
135
+ return "pure-python"
136
+ raise RuntimeError(
137
+ f"Cannot {action_name}: eth-account is not installed. Install with: pip install uniswap-autopilot[signer]"
138
+ )
139
+
140
+ # Try auto-select
141
+ selected = auto_select_signer_args(args)
142
+ if selected is not None:
143
+ if _pure_signer_available():
144
+ return "pure-python"
145
+ raise RuntimeError(
146
+ f"Cannot {action_name}: eth-account is not installed. Install with: pip install uniswap-autopilot[signer]"
147
+ )
148
+
149
+ raise ValueError(
150
+ f"Cannot {action_name}: no signer configured. "
151
+ "Set EXECUTOR_PRIVATE_KEY env var and install: pip install uniswap-autopilot[signer]"
152
+ )
153
+
154
+
155
+ # ── Argument parsing ──────────────────────────────────────────────────────
156
+
157
+
158
+ def add_signer_arguments(parser: argparse.ArgumentParser) -> None:
159
+ """Add signer-related arguments to an argparse parser."""
160
+ parser.add_argument(
161
+ "--private-key-env",
162
+ default=HOT_WALLET_PRIVATE_KEY_ENV_NAME,
163
+ help="Environment variable holding the hex private key (default: %(default)s)",
164
+ )
165
+
166
+
167
+ # ── Signing API ────────────────────────────────────────────────────────────
168
+
169
+
170
+ def sign_typed_data_with_backend(
171
+ typed_data_file: str,
172
+ typed_data: dict[str, Any],
173
+ tx: dict[str, Any],
174
+ signer_args_source: argparse.Namespace,
175
+ ) -> dict[str, Any]:
176
+ """Sign EIP-712 typed data using pure Python signer."""
177
+ pk_env = getattr(signer_args_source, "private_key_env", None)
178
+ if not pk_env:
179
+ raise RuntimeError("private_key_env not set on signer args")
180
+
181
+ from uniswap_autopilot.execute._internal.pure_signer import sign_typed_data
182
+
183
+ signature = sign_typed_data(typed_data, private_key_env=pk_env)
184
+ return {
185
+ "signature": signature,
186
+ "signCommandPreview": f"pure_signer.sign_typed_data(env={pk_env})",
187
+ "signerBackend": "pure-python",
188
+ }
189
+
190
+
191
+ def sign_and_broadcast(
192
+ tx: dict[str, Any],
193
+ rpc_url: str,
194
+ signer_args_source: argparse.Namespace,
195
+ ) -> dict[str, Any]:
196
+ """Sign a transaction and broadcast via eth_sendRawTransaction.
197
+
198
+ Preflight gates (before signing):
199
+ 1. Wallet must have enough native balance for gas.
200
+ 2. Transaction must pass eth_estimateGas (dry-run simulation).
201
+ """
202
+ pk_env = getattr(signer_args_source, "private_key_env", None)
203
+ if not pk_env:
204
+ raise RuntimeError("private_key_env not set on signer args")
205
+
206
+ from uniswap_autopilot.audit import EVENT_ERROR, log_event
207
+ from uniswap_autopilot.execute._internal.pure_signer import (
208
+ get_address,
209
+ sign_transaction,
210
+ )
211
+ from uniswap_autopilot.execute._internal.rpc import (
212
+ _json_rpc,
213
+ estimate_transaction_gas,
214
+ query_gas_price,
215
+ query_native_balance,
216
+ )
217
+
218
+ # Resolve wallet address
219
+ wallet = tx.get("from") or get_address(private_key_env=pk_env)
220
+ if not wallet:
221
+ raise RuntimeError("Cannot determine wallet address for preflight checks")
222
+
223
+ run_id = os.environ.get("STAGEFORGE_RUN_ID") or os.environ.get("RUN_ID")
224
+ chain_id = tx.get("chainId")
225
+
226
+ # Gate 1: native balance must cover gas
227
+ balance = query_native_balance(wallet, rpc_url)
228
+ if balance == 0:
229
+ log_event(
230
+ event=EVENT_ERROR,
231
+ chain=str(chain_id) if chain_id else None,
232
+ wallet=wallet,
233
+ run_id=run_id,
234
+ error_code="no_gas",
235
+ details={"native_balance": 0},
236
+ )
237
+ raise RuntimeError(f"Wallet {wallet} has zero native balance — cannot pay for gas.")
238
+
239
+ # Gate 2: estimateGas simulation
240
+ try:
241
+ estimated = estimate_transaction_gas(tx, rpc_url)
242
+ except Exception as exc:
243
+ reason = str(getattr(exc, "args", [str(exc)])[0])
244
+ log_event(
245
+ event=EVENT_ERROR,
246
+ chain=str(chain_id) if chain_id else None,
247
+ wallet=wallet,
248
+ run_id=run_id,
249
+ error_code="simulation_failed",
250
+ details={"to": tx.get("to"), "value": str(tx.get("value", 0)), "revert": reason},
251
+ )
252
+ raise RuntimeError(f"Gas estimation failed (tx would revert): {reason}") from exc
253
+
254
+ # Optional: warn if balance < estimated * gas_price
255
+ try:
256
+ gas_price = query_gas_price(rpc_url)
257
+ if balance < estimated * gas_price:
258
+ log_event(
259
+ event=EVENT_ERROR,
260
+ chain=str(chain_id) if chain_id else None,
261
+ wallet=wallet,
262
+ run_id=run_id,
263
+ error_code="insufficient_gas",
264
+ details={
265
+ "native_balance": balance,
266
+ "estimated_gas": estimated,
267
+ "gas_price": gas_price,
268
+ "required": estimated * gas_price,
269
+ },
270
+ )
271
+ raise RuntimeError(
272
+ f"Wallet {wallet} balance ({balance}) insufficient for gas (estimate: {estimated} * price: {gas_price})"
273
+ )
274
+ except RuntimeError:
275
+ raise
276
+ except Exception:
277
+ pass # gas-price check is best-effort
278
+
279
+ signed_hex = sign_transaction(tx, private_key_env=pk_env, chain_id=chain_id)
280
+ tx_hash = _json_rpc("eth_sendRawTransaction", [signed_hex], rpc_url)
281
+
282
+ # Wait briefly and get receipt
283
+ import time
284
+
285
+ time.sleep(1)
286
+ receipt = _json_rpc("eth_getTransactionReceipt", [tx_hash], rpc_url)
287
+
288
+ if receipt is None:
289
+ broadcast_result = {"transactionHash": tx_hash, "status": "pending"}
290
+ else:
291
+ broadcast_result = receipt
292
+
293
+ return {
294
+ "commandPreview": f"pure_signer.sign_transaction + eth_sendRawTransaction (env={pk_env})",
295
+ "rpcUrl": rpc_url,
296
+ "broadcastResult": broadcast_result,
297
+ "signerBackend": "pure-python",
298
+ }
@@ -0,0 +1,73 @@
1
+ """Transaction broadcast — pure Python signing + eth_sendRawTransaction.
2
+
3
+ Requires ``pip install uniswap-autopilot[signer]`` for the ``eth-account`` library.
4
+ """
5
+
6
+ from __future__ import annotations
7
+
8
+ import argparse
9
+ from typing import Any
10
+
11
+ from uniswap_autopilot.execute._internal.rpc import resolve_rpc_url
12
+ from uniswap_autopilot.execute._internal.signer import (
13
+ ensure_signer_backend,
14
+ sign_and_broadcast,
15
+ )
16
+ from uniswap_autopilot.execute._internal.tx import build_confirmation_phrase
17
+
18
+
19
+ def build_broadcast_package(
20
+ tx: dict[str, Any],
21
+ explicit_rpc_url: str | None,
22
+ confirm: str | None,
23
+ signer_args_source: argparse.Namespace,
24
+ ) -> dict[str, Any]:
25
+ """Validate confirmation phrase and resolve RPC URL."""
26
+ expected_confirmation = build_confirmation_phrase(tx)
27
+ if confirm != expected_confirmation:
28
+ raise ValueError(f"--confirm must exactly equal: {expected_confirmation}")
29
+
30
+ rpc_url, rpc_candidates = resolve_rpc_url(explicit_rpc_url, tx["chainId"])
31
+ if not rpc_url:
32
+ raise RuntimeError(
33
+ f"RPC URL is not configured; set one of {', '.join(rpc_candidates)} or pass --rpc-url"
34
+ )
35
+ return {
36
+ "expectedConfirmation": expected_confirmation,
37
+ "rpcUrl": rpc_url,
38
+ "signerBackend": "pure-python",
39
+ }
40
+
41
+
42
+ def broadcast_with_backend(
43
+ tx: dict[str, Any],
44
+ explicit_rpc_url: str | None,
45
+ confirm: str | None,
46
+ signer_args_source: argparse.Namespace,
47
+ **kwargs: Any,
48
+ ) -> dict[str, Any]:
49
+ """Sign and broadcast a transaction.
50
+
51
+ Returns dict with broadcastResult containing the transaction receipt.
52
+ """
53
+ ensure_signer_backend(signer_args_source, action_name="broadcast")
54
+ package = build_broadcast_package(
55
+ tx=tx,
56
+ explicit_rpc_url=explicit_rpc_url,
57
+ confirm=confirm,
58
+ signer_args_source=signer_args_source,
59
+ )
60
+ return sign_and_broadcast(
61
+ tx=tx,
62
+ rpc_url=package["rpcUrl"],
63
+ signer_args_source=signer_args_source,
64
+ )
65
+
66
+
67
+ def extract_transaction_hash(broadcast_result: dict[str, Any]) -> str:
68
+ """Extract transaction hash from broadcast result."""
69
+ for field in ("transactionHash", "hash"):
70
+ value = broadcast_result.get(field)
71
+ if isinstance(value, str) and value.startswith("0x"):
72
+ return value
73
+ raise RuntimeError("broadcast result does not contain transactionHash")