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
File without changes
@@ -0,0 +1,5 @@
1
+ """Internal execution helpers.
2
+
3
+ Public callers should use execute.broadcast or the CLI entrypoints unless they
4
+ explicitly need an internal building block.
5
+ """
@@ -0,0 +1,15 @@
1
+ from __future__ import annotations
2
+
3
+ import re
4
+
5
+ from uniswap_autopilot.common.common import CHAINS
6
+
7
+ HEX_DATA_RE = re.compile(r"^0x[0-9a-fA-F]*$")
8
+ CHAIN_BY_ID = {chain.chain_id: chain for chain in CHAINS.values()}
9
+ GLOBAL_RPC_ENV_CANDIDATES = ("ETH_RPC_URL", "RPC_URL")
10
+ APPROVE_SELECTOR = "0x095ea7b3"
11
+ DEFAULT_PRIVATE_KEY_ENV = "EXECUTOR_PRIVATE_KEY"
12
+ HOT_WALLET_PRIVATE_KEY_ENV_NAME = "HOT_WALLET_PRIVATE_KEY_ENV"
13
+ HOT_WALLET_KEYSTORE_ENV_NAME = "HOT_WALLET_KEYSTORE"
14
+ HOT_WALLET_ACCOUNT_ENV_NAME = "HOT_WALLET_ACCOUNT"
15
+ HOT_WALLET_PASSWORD_FILE_ENV_NAME = "HOT_WALLET_PASSWORD_FILE"
@@ -0,0 +1,150 @@
1
+ from __future__ import annotations
2
+
3
+ from typing import Any
4
+
5
+ from uniswap_autopilot.execute._internal.rpc import (
6
+ estimate_transaction_gas,
7
+ query_erc20_allowance,
8
+ query_erc20_balance,
9
+ query_gas_price,
10
+ query_native_balance,
11
+ resolve_rpc_url,
12
+ )
13
+ from uniswap_autopilot.execute._internal.tx import parse_intish
14
+
15
+ def build_preflight_report(
16
+ tx: dict[str, Any],
17
+ explicit_rpc_url: str | None = None,
18
+ strict: bool = False,
19
+ ) -> dict[str, Any]:
20
+ rpc_url, rpc_candidates = resolve_rpc_url(explicit_rpc_url, tx["chainId"])
21
+ if not rpc_url:
22
+ if strict:
23
+ raise RuntimeError(
24
+ f"RPC URL is not configured; set one of {', '.join(rpc_candidates)} or pass --rpc-url"
25
+ )
26
+ return {
27
+ "checked": False,
28
+ "ok": None,
29
+ "reason": "RPC URL is not configured",
30
+ "rpcUrlResolved": None,
31
+ "rpcEnvCandidates": rpc_candidates,
32
+ }
33
+
34
+ owner = tx.get("from")
35
+ if not owner:
36
+ if strict:
37
+ raise RuntimeError("preflight requires tx.from")
38
+ return {
39
+ "checked": False,
40
+ "ok": None,
41
+ "reason": "transaction.from is missing",
42
+ "rpcUrlResolved": rpc_url,
43
+ "rpcEnvCandidates": rpc_candidates,
44
+ }
45
+ try:
46
+ native_balance = query_native_balance(owner, rpc_url)
47
+ gas_limit = parse_intish(tx.get("gasLimit"), "gasLimit")
48
+ if gas_limit is None:
49
+ gas_limit = estimate_transaction_gas(tx, rpc_url)
50
+ gas_price = parse_intish(tx.get("gasPrice"), "gasPrice")
51
+ if gas_price is None:
52
+ gas_price = query_gas_price(rpc_url)
53
+
54
+ native_value = int(tx["value"])
55
+ gas_cost = gas_limit * gas_price
56
+ total_native_required = native_value + gas_cost
57
+ issues: list[str] = []
58
+ report: dict[str, Any] = {
59
+ "checked": True,
60
+ "ok": True,
61
+ "owner": owner,
62
+ "rpcUrlResolved": rpc_url,
63
+ "rpcEnvCandidates": rpc_candidates,
64
+ "nativeBalance": str(native_balance),
65
+ "gasLimitUsed": str(gas_limit),
66
+ "gasPriceUsed": str(gas_price),
67
+ "gasCost": str(gas_cost),
68
+ "nativeValue": str(native_value),
69
+ "totalNativeRequired": str(total_native_required),
70
+ "nativeSufficient": native_balance >= total_native_required,
71
+ "issues": issues,
72
+ }
73
+ if not report["nativeSufficient"]:
74
+ issues.append(
75
+ f"native balance {native_balance} is below required {total_native_required}"
76
+ )
77
+
78
+ if tx["kind"] == "approval":
79
+ token = tx.get("approvalToken")
80
+ spender = tx.get("approvalSpender")
81
+ required_allowance = tx.get("requiredAllowance")
82
+ if token and spender and required_allowance:
83
+ allowance = query_erc20_allowance(token, owner, spender, rpc_url)
84
+ required_int = int(required_allowance)
85
+ report["allowance"] = {
86
+ "token": token,
87
+ "spender": spender,
88
+ "current": str(allowance),
89
+ "required": str(required_int),
90
+ "sufficient": allowance >= required_int,
91
+ }
92
+ if allowance >= required_int:
93
+ report["allowance"]["alreadySufficient"] = True
94
+
95
+ if tx["kind"] == "swap":
96
+ input_token = tx.get("inputToken")
97
+ input_amount = tx.get("inputAmount")
98
+ if tx.get("nativeInput"):
99
+ report["inputBalance"] = {
100
+ "asset": "native",
101
+ "token": input_token,
102
+ "current": str(native_balance),
103
+ "required": str(total_native_required),
104
+ "sufficient": native_balance >= total_native_required,
105
+ }
106
+ elif input_token and input_amount:
107
+ token_balance = query_erc20_balance(owner, input_token, rpc_url)
108
+ required_int = int(input_amount)
109
+ sufficient = token_balance >= required_int
110
+ report["inputBalance"] = {
111
+ "asset": "erc20",
112
+ "token": input_token,
113
+ "current": str(token_balance),
114
+ "required": str(required_int),
115
+ "sufficient": sufficient,
116
+ }
117
+ if not sufficient:
118
+ issues.append(
119
+ f"token balance {token_balance} is below required input {required_int}"
120
+ )
121
+
122
+ permit_verifier = tx.get("permitVerifier")
123
+ if permit_verifier:
124
+ allowance = query_erc20_allowance(input_token, owner, permit_verifier, rpc_url)
125
+ allowance_sufficient = allowance >= required_int
126
+ report["allowance"] = {
127
+ "token": input_token,
128
+ "spender": permit_verifier,
129
+ "current": str(allowance),
130
+ "required": str(required_int),
131
+ "sufficient": allowance_sufficient,
132
+ "source": "permitVerifier",
133
+ }
134
+ if not allowance_sufficient:
135
+ issues.append(
136
+ f"allowance {allowance} is below required input {required_int} for {permit_verifier.lower()}"
137
+ )
138
+
139
+ report["ok"] = len(issues) == 0
140
+ return report
141
+ except Exception as exc: # noqa: BLE001
142
+ if strict:
143
+ raise
144
+ return {
145
+ "checked": False,
146
+ "ok": None,
147
+ "reason": str(exc),
148
+ "rpcUrlResolved": rpc_url,
149
+ "rpcEnvCandidates": rpc_candidates,
150
+ }
@@ -0,0 +1,182 @@
1
+ """Pure Python transaction signing and broadcasting via eth-account.
2
+
3
+ This module provides the same interface as cast-based signing but uses
4
+ the ``eth-account`` library instead of the Foundry CLI. Activated when
5
+ the ``[signer]`` extra is installed::
6
+
7
+ pip install uniswap-autopilot[signer]
8
+
9
+ Private key is read from the environment variable specified by
10
+ ``--private-key-env`` (default: ``EXECUTOR_PRIVATE_KEY``).
11
+ """
12
+
13
+ from __future__ import annotations
14
+
15
+ import os
16
+ from typing import Any
17
+
18
+ # eth-account is an optional dependency — import failures are handled gracefully.
19
+ try:
20
+ from eth_account import Account
21
+ from eth_account.messages import encode_typed_data
22
+
23
+ _HAS_ETH_ACCOUNT = True
24
+ except ImportError:
25
+ _HAS_ETH_ACCOUNT = False
26
+
27
+
28
+ class SignerUnavailableError(RuntimeError):
29
+ """Raised when eth-account is not installed."""
30
+
31
+
32
+ def is_available() -> bool:
33
+ """Check if the pure-Python signer is available."""
34
+ return _HAS_ETH_ACCOUNT
35
+
36
+
37
+ def _require_available():
38
+ if not _HAS_ETH_ACCOUNT:
39
+ raise SignerUnavailableError(
40
+ "eth-account is not installed. "
41
+ "Install it with: pip install uniswap-autopilot[signer]"
42
+ )
43
+
44
+
45
+ def _get_private_key(env_name: str) -> str:
46
+ """Read a hex private key from an environment variable."""
47
+ key = os.environ.get(env_name, "").strip()
48
+ if not key:
49
+ raise RuntimeError(f"Private key not found in env var {env_name}")
50
+ if not key.startswith("0x"):
51
+ key = "0x" + key
52
+ return key
53
+
54
+
55
+ def sign_typed_data(
56
+ typed_data: dict[str, Any],
57
+ private_key_env: str = "EXECUTOR_PRIVATE_KEY",
58
+ ) -> str:
59
+ """Sign EIP-712 typed data and return the signature hex string.
60
+
61
+ Parameters
62
+ ----------
63
+ typed_data : dict
64
+ The EIP-712 typed data object (domain, types, message).
65
+ private_key_env : str
66
+ Environment variable name holding the hex private key.
67
+
68
+ Returns
69
+ -------
70
+ str
71
+ Signature as a 0x-prefixed hex string.
72
+ """
73
+ _require_available()
74
+ pk = _get_private_key(private_key_env)
75
+
76
+ # Build the signable message from typed data components
77
+ domain = typed_data.get("domain", {})
78
+ types = typed_data.get("types", {})
79
+ primary_type = typed_data.get("primaryType", "")
80
+ message = typed_data.get("message", {})
81
+
82
+ # Remove EIP712Domain from types if present (it's implicit)
83
+ types_clean = {k: v for k, v in types.items() if k != "EIP712Domain"}
84
+
85
+ signable = encode_typed_data(
86
+ full_message={
87
+ "types": types_clean,
88
+ "domain": domain,
89
+ "primaryType": primary_type,
90
+ "message": message,
91
+ }
92
+ )
93
+ signed = Account.sign_message(signable, pk)
94
+ return signed.signature.hex()
95
+
96
+
97
+ def sign_transaction(
98
+ tx: dict[str, Any],
99
+ private_key_env: str = "EXECUTOR_PRIVATE_KEY",
100
+ chain_id: int | None = None,
101
+ ) -> str:
102
+ """Sign a legacy EVM transaction and return the signed raw tx hex.
103
+
104
+ Parameters
105
+ ----------
106
+ tx : dict
107
+ Transaction dict with keys: to, data, value, gas (or gasLimit), etc.
108
+ private_key_env : str
109
+ Environment variable name holding the hex private key.
110
+ chain_id : int, optional
111
+ Chain ID for EIP-155 replay protection.
112
+
113
+ Returns
114
+ -------
115
+ str
116
+ Signed raw transaction as 0x-prefixed hex string.
117
+ """
118
+ _require_available()
119
+ pk = _get_private_key(private_key_env)
120
+
121
+ # ``nonce`` MUST be supplied by the caller — silently defaulting to 0
122
+ # historically allowed the wrong nonce to be signed when the upstream
123
+ # pipeline forgot to attach one, leading to replacement-tx failures or,
124
+ # worse, replaying an old nonce against the network. Fail fast instead.
125
+ if "nonce" not in tx or tx["nonce"] is None:
126
+ raise ValueError(
127
+ "sign_transaction: 'nonce' is required. "
128
+ "Fetch it from the RPC (e.g. eth_getTransactionCount(addr, 'pending')) "
129
+ "and pass it explicitly — defaulting to 0 is unsafe."
130
+ )
131
+ nonce_raw = tx["nonce"]
132
+ if isinstance(nonce_raw, int):
133
+ nonce_int = nonce_raw
134
+ else:
135
+ nonce_int = int(str(nonce_raw), 0)
136
+ if nonce_int < 0:
137
+ raise ValueError(f"sign_transaction: nonce must be >= 0, got {nonce_int}")
138
+
139
+ gas_limit = tx.get("gasLimit") or tx.get("gas") or tx.get("gas_limit", 210000)
140
+ gas_price = tx.get("gasPrice") or tx.get("gas_price")
141
+ max_fee = tx.get("maxFeePerGas")
142
+ priority_fee = tx.get("maxPriorityFeePerGas")
143
+
144
+ typed_tx: dict[str, Any] = {
145
+ "nonce": nonce_int,
146
+ "to": tx["to"],
147
+ "data": tx["data"],
148
+ "value": int(tx["value"], 0) if isinstance(tx["value"], str) else int(tx["value"]),
149
+ "gas": int(gas_limit) if not isinstance(gas_limit, int) else gas_limit,
150
+ }
151
+
152
+ if chain_id is not None:
153
+ typed_tx["chainId"] = chain_id
154
+
155
+ # EIP-1559 if max_fee/priority_fee provided, else legacy
156
+ if max_fee and priority_fee:
157
+ typed_tx["maxFeePerGas"] = int(max_fee, 0) if isinstance(max_fee, str) else int(max_fee)
158
+ typed_tx["maxPriorityFeePerGas"] = int(priority_fee, 0) if isinstance(priority_fee, str) else int(priority_fee)
159
+ elif gas_price:
160
+ typed_tx["gasPrice"] = int(gas_price, 0) if isinstance(gas_price, str) else int(gas_price)
161
+ else:
162
+ # Default to legacy with reasonable gas price
163
+ typed_tx["gasPrice"] = 0
164
+
165
+ signed = Account.sign_transaction(typed_tx, pk)
166
+ return signed.raw_transaction.hex()
167
+
168
+
169
+ def get_address(private_key_env: str = "EXECUTOR_PRIVATE_KEY") -> str | None:
170
+ """Derive the wallet address from the private key env var.
171
+
172
+ Returns None if the env var is not set.
173
+ """
174
+ pk = os.environ.get(private_key_env, "").strip()
175
+ if not pk:
176
+ return None
177
+ if not pk.startswith("0x"):
178
+ pk = "0x" + pk
179
+ try:
180
+ return Account.from_key(pk).address
181
+ except Exception:
182
+ return None