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
|
File without changes
|
|
@@ -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
|