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
|
@@ -0,0 +1,370 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import json
|
|
4
|
+
from decimal import Decimal
|
|
5
|
+
from pathlib import Path
|
|
6
|
+
from typing import Any
|
|
7
|
+
|
|
8
|
+
from uniswap_autopilot.common.common import validate_address
|
|
9
|
+
from uniswap_autopilot.execute._internal.constants import APPROVE_SELECTOR, CHAIN_BY_ID, HEX_DATA_RE
|
|
10
|
+
from uniswap_autopilot.execute._internal.rpc import resolve_rpc_url
|
|
11
|
+
|
|
12
|
+
def load_json(path: str) -> dict[str, Any]:
|
|
13
|
+
return json.loads(Path(path).read_text(encoding="utf-8"))
|
|
14
|
+
|
|
15
|
+
def parse_intish(value: Any, field_name: str) -> int | None:
|
|
16
|
+
if value in (None, "", "null"):
|
|
17
|
+
return None
|
|
18
|
+
if isinstance(value, bool):
|
|
19
|
+
raise ValueError(f"{field_name} must not be boolean")
|
|
20
|
+
if isinstance(value, int):
|
|
21
|
+
if value < 0:
|
|
22
|
+
raise ValueError(f"{field_name} must be non-negative")
|
|
23
|
+
return value
|
|
24
|
+
if isinstance(value, str):
|
|
25
|
+
cleaned = value.strip()
|
|
26
|
+
if not cleaned:
|
|
27
|
+
return None
|
|
28
|
+
try:
|
|
29
|
+
parsed = int(cleaned, 0)
|
|
30
|
+
except ValueError as exc:
|
|
31
|
+
raise ValueError(f"{field_name} must be an int or hex string") from exc
|
|
32
|
+
if parsed < 0:
|
|
33
|
+
raise ValueError(f"{field_name} must be non-negative")
|
|
34
|
+
return parsed
|
|
35
|
+
raise ValueError(f"{field_name} must be an int or string")
|
|
36
|
+
|
|
37
|
+
def validate_hex_data(value: Any, field_name: str = "data") -> str:
|
|
38
|
+
if not isinstance(value, str) or not HEX_DATA_RE.fullmatch(value):
|
|
39
|
+
raise ValueError(f"{field_name} must be a 0x-prefixed hex string")
|
|
40
|
+
if value == "0x":
|
|
41
|
+
raise ValueError(f"{field_name} must not be empty")
|
|
42
|
+
return value
|
|
43
|
+
|
|
44
|
+
def optional_address(value: Any) -> str | None:
|
|
45
|
+
if value in (None, "", "null"):
|
|
46
|
+
return None
|
|
47
|
+
try:
|
|
48
|
+
return validate_address(value, "address")
|
|
49
|
+
except ValueError:
|
|
50
|
+
return None
|
|
51
|
+
|
|
52
|
+
def lookup_token_symbol(chain_id: int, address: str | None) -> str | None:
|
|
53
|
+
if not address:
|
|
54
|
+
return None
|
|
55
|
+
chain = CHAIN_BY_ID.get(chain_id)
|
|
56
|
+
if not chain:
|
|
57
|
+
return None
|
|
58
|
+
normalized = address.lower()
|
|
59
|
+
for symbol, token in chain.tokens.items():
|
|
60
|
+
if token.address.lower() == normalized:
|
|
61
|
+
return symbol
|
|
62
|
+
return None
|
|
63
|
+
|
|
64
|
+
def lookup_token_decimals(
|
|
65
|
+
chain_id: int,
|
|
66
|
+
token_symbol: str | None = None,
|
|
67
|
+
token_address: str | None = None,
|
|
68
|
+
) -> int:
|
|
69
|
+
chain = CHAIN_BY_ID.get(chain_id)
|
|
70
|
+
if not chain:
|
|
71
|
+
return 18
|
|
72
|
+
if token_symbol == "NATIVE":
|
|
73
|
+
return 18
|
|
74
|
+
if token_symbol:
|
|
75
|
+
token = chain.tokens.get(str(token_symbol).upper())
|
|
76
|
+
if token:
|
|
77
|
+
return token.decimals
|
|
78
|
+
if token_address:
|
|
79
|
+
normalized = token_address.lower()
|
|
80
|
+
for token in chain.tokens.values():
|
|
81
|
+
if token.address.lower() == normalized:
|
|
82
|
+
return token.decimals
|
|
83
|
+
return 18
|
|
84
|
+
|
|
85
|
+
def base_units_to_human(amount: Any, decimals: int) -> str:
|
|
86
|
+
raw = parse_intish(amount, "amount")
|
|
87
|
+
if raw is None:
|
|
88
|
+
return "0"
|
|
89
|
+
scaled = Decimal(raw) / (Decimal(10) ** decimals)
|
|
90
|
+
text = format(scaled, "f")
|
|
91
|
+
if "." in text:
|
|
92
|
+
text = text.rstrip("0").rstrip(".")
|
|
93
|
+
return text or "0"
|
|
94
|
+
|
|
95
|
+
def decode_erc20_approve_call(data: str) -> dict[str, str] | None:
|
|
96
|
+
validated = validate_hex_data(data, "data")
|
|
97
|
+
if not validated.startswith(APPROVE_SELECTOR):
|
|
98
|
+
return None
|
|
99
|
+
payload = validated[10:]
|
|
100
|
+
if len(payload) < 128:
|
|
101
|
+
return None
|
|
102
|
+
spender_word = payload[:64]
|
|
103
|
+
amount_word = payload[64:128]
|
|
104
|
+
spender = optional_address(f"0x{spender_word[-40:]}")
|
|
105
|
+
if not spender:
|
|
106
|
+
return None
|
|
107
|
+
return {
|
|
108
|
+
"spender": spender,
|
|
109
|
+
"amount": str(int(amount_word, 16)),
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
def normalize_transaction(
|
|
113
|
+
tx: dict[str, Any],
|
|
114
|
+
tx_kind: str,
|
|
115
|
+
source_file: str,
|
|
116
|
+
source_kind: str,
|
|
117
|
+
native_input: bool = False,
|
|
118
|
+
) -> dict[str, Any]:
|
|
119
|
+
if not isinstance(tx, dict):
|
|
120
|
+
raise ValueError("transaction payload must be an object")
|
|
121
|
+
|
|
122
|
+
chain_id = parse_intish(tx.get("chainId"), "chainId")
|
|
123
|
+
if chain_id is None:
|
|
124
|
+
raise ValueError("chainId is required")
|
|
125
|
+
|
|
126
|
+
gas_limit = parse_intish(tx.get("gasLimit"), "gasLimit")
|
|
127
|
+
gas_price = parse_intish(tx.get("gasPrice"), "gasPrice")
|
|
128
|
+
value_int = parse_intish(tx.get("value"), "value")
|
|
129
|
+
normalized = {
|
|
130
|
+
"kind": tx_kind,
|
|
131
|
+
"sourceKind": source_kind,
|
|
132
|
+
"sourceFile": source_file,
|
|
133
|
+
"chainId": chain_id,
|
|
134
|
+
"chainKey": (CHAIN_BY_ID.get(chain_id).key if chain_id in CHAIN_BY_ID else None),
|
|
135
|
+
"to": validate_address(tx.get("to"), "to"),
|
|
136
|
+
"from": validate_address(tx.get("from"), "from") if tx.get("from") else None,
|
|
137
|
+
"data": validate_hex_data(tx.get("data"), "data"),
|
|
138
|
+
"value": str(value_int or 0),
|
|
139
|
+
"gasLimit": (str(gas_limit) if gas_limit is not None else None),
|
|
140
|
+
"gasPrice": (str(gas_price) if gas_price is not None else None),
|
|
141
|
+
"nativeInput": native_input,
|
|
142
|
+
}
|
|
143
|
+
if native_input and value_int == 0:
|
|
144
|
+
raise ValueError("native-input swap requires non-zero tx.value")
|
|
145
|
+
return normalized
|
|
146
|
+
|
|
147
|
+
def load_swap_transaction(path: str) -> dict[str, Any]:
|
|
148
|
+
blob = load_json(path)
|
|
149
|
+
swap_response = blob.get("swapResponse") or {}
|
|
150
|
+
swap = swap_response.get("swap")
|
|
151
|
+
if not swap:
|
|
152
|
+
raise ValueError("swap file must contain swapResponse.swap")
|
|
153
|
+
request_payload = blob.get("requestPayload") or {}
|
|
154
|
+
quote = request_payload.get("quote") or {}
|
|
155
|
+
input_token = (quote.get("input") or {}).get("token")
|
|
156
|
+
native_input = str(input_token or "").lower() == "0x0000000000000000000000000000000000000000"
|
|
157
|
+
normalized = normalize_transaction(
|
|
158
|
+
tx=swap,
|
|
159
|
+
tx_kind="swap",
|
|
160
|
+
source_file=path,
|
|
161
|
+
source_kind="swap-file",
|
|
162
|
+
native_input=native_input,
|
|
163
|
+
)
|
|
164
|
+
normalized["requestId"] = swap_response.get("requestId")
|
|
165
|
+
normalized["gasFee"] = swap_response.get("gasFee")
|
|
166
|
+
normalized["apiSignature"] = swap_response.get("signature")
|
|
167
|
+
quote_input = quote.get("input") or {}
|
|
168
|
+
quote_output = quote.get("output") or {}
|
|
169
|
+
normalized["inputToken"] = optional_address(quote_input.get("token"))
|
|
170
|
+
normalized["inputTokenSymbol"] = (
|
|
171
|
+
"NATIVE"
|
|
172
|
+
if native_input
|
|
173
|
+
else lookup_token_symbol(chain_id=normalized["chainId"], address=normalized["inputToken"])
|
|
174
|
+
)
|
|
175
|
+
normalized["inputAmount"] = (
|
|
176
|
+
str(parse_intish(quote_input.get("amount"), "quote.input.amount"))
|
|
177
|
+
if quote_input.get("amount") not in (None, "", "null")
|
|
178
|
+
else None
|
|
179
|
+
)
|
|
180
|
+
normalized["outputToken"] = optional_address(quote_output.get("token"))
|
|
181
|
+
normalized["outputTokenSymbol"] = lookup_token_symbol(
|
|
182
|
+
chain_id=normalized["chainId"],
|
|
183
|
+
address=normalized["outputToken"],
|
|
184
|
+
)
|
|
185
|
+
normalized["outputAmount"] = (
|
|
186
|
+
str(parse_intish(quote_output.get("amount"), "quote.output.amount"))
|
|
187
|
+
if quote_output.get("amount") not in (None, "", "null")
|
|
188
|
+
else None
|
|
189
|
+
)
|
|
190
|
+
permit_data = request_payload.get("permitData") or {}
|
|
191
|
+
permit_domain = permit_data.get("domain") or {}
|
|
192
|
+
permit_values = permit_data.get("values") or {}
|
|
193
|
+
normalized["permitVerifier"] = optional_address(permit_domain.get("verifyingContract"))
|
|
194
|
+
normalized["permitSpender"] = optional_address(permit_values.get("spender"))
|
|
195
|
+
return normalized
|
|
196
|
+
|
|
197
|
+
def load_approval_transaction(path: str) -> dict[str, Any]:
|
|
198
|
+
blob = load_json(path)
|
|
199
|
+
approval_check = blob.get("approvalCheck") or {}
|
|
200
|
+
approval = approval_check.get("approval")
|
|
201
|
+
if not approval:
|
|
202
|
+
raise ValueError("quote file must contain approvalCheck.approval")
|
|
203
|
+
normalized = normalize_transaction(
|
|
204
|
+
tx=approval,
|
|
205
|
+
tx_kind="approval",
|
|
206
|
+
source_file=path,
|
|
207
|
+
source_kind="quote-file",
|
|
208
|
+
)
|
|
209
|
+
normalized["requestId"] = approval_check.get("requestId")
|
|
210
|
+
request_payload = ((blob.get("requestPayloads") or {}).get("checkApproval")) or {}
|
|
211
|
+
decoded_approval = decode_erc20_approve_call(normalized["data"])
|
|
212
|
+
normalized["approvalToken"] = normalized["to"]
|
|
213
|
+
normalized["approvalSpender"] = (
|
|
214
|
+
decoded_approval.get("spender") if decoded_approval else None
|
|
215
|
+
)
|
|
216
|
+
normalized["approvalAmount"] = (
|
|
217
|
+
decoded_approval.get("amount") if decoded_approval else None
|
|
218
|
+
)
|
|
219
|
+
normalized["requiredAllowance"] = (
|
|
220
|
+
str(parse_intish(request_payload.get("amount"), "checkApproval.amount"))
|
|
221
|
+
if request_payload.get("amount") not in (None, "", "null")
|
|
222
|
+
else normalized["approvalAmount"]
|
|
223
|
+
)
|
|
224
|
+
raw_quote = blob.get("rawQuote") or {}
|
|
225
|
+
quote = raw_quote.get("quote") or {}
|
|
226
|
+
quote_input = quote.get("input") or {}
|
|
227
|
+
quote_output = quote.get("output") or {}
|
|
228
|
+
quote_input_meta = blob.get("tokenIn") or {}
|
|
229
|
+
quote_output_meta = blob.get("tokenOut") or {}
|
|
230
|
+
normalized["inputToken"] = optional_address(quote_input.get("token")) or normalized["approvalToken"]
|
|
231
|
+
normalized["inputTokenSymbol"] = (
|
|
232
|
+
quote_input_meta.get("symbol")
|
|
233
|
+
or lookup_token_symbol(chain_id=normalized["chainId"], address=normalized["inputToken"])
|
|
234
|
+
)
|
|
235
|
+
normalized["inputAmount"] = (
|
|
236
|
+
str(parse_intish(quote_input.get("amount"), "quote.input.amount"))
|
|
237
|
+
if quote_input.get("amount") not in (None, "", "null")
|
|
238
|
+
else normalized["requiredAllowance"]
|
|
239
|
+
)
|
|
240
|
+
normalized["outputToken"] = optional_address(quote_output.get("token"))
|
|
241
|
+
normalized["outputTokenSymbol"] = (
|
|
242
|
+
quote_output_meta.get("symbol")
|
|
243
|
+
or lookup_token_symbol(chain_id=normalized["chainId"], address=normalized["outputToken"])
|
|
244
|
+
)
|
|
245
|
+
normalized["outputAmount"] = (
|
|
246
|
+
str(parse_intish(quote_output.get("amount"), "quote.output.amount"))
|
|
247
|
+
if quote_output.get("amount") not in (None, "", "null")
|
|
248
|
+
else None
|
|
249
|
+
)
|
|
250
|
+
return normalized
|
|
251
|
+
|
|
252
|
+
def build_confirmation_phrase(tx: dict[str, Any]) -> str:
|
|
253
|
+
return f"BROADCAST {tx['kind'].upper()} {tx['chainId']} {tx['to'].lower()}"
|
|
254
|
+
|
|
255
|
+
def summarize_transaction(tx: dict[str, Any], rpc_url: str | None, rpc_candidates: list[str]) -> dict[str, Any]:
|
|
256
|
+
summary = {
|
|
257
|
+
"kind": tx["kind"],
|
|
258
|
+
"chain": {"chainId": tx["chainId"], "key": tx["chainKey"]},
|
|
259
|
+
"from": tx["from"],
|
|
260
|
+
"to": tx["to"],
|
|
261
|
+
"value": tx["value"],
|
|
262
|
+
"gasLimit": tx["gasLimit"],
|
|
263
|
+
"gasPrice": tx["gasPrice"],
|
|
264
|
+
"dataBytes": (len(tx["data"]) - 2) // 2,
|
|
265
|
+
"sourceKind": tx["sourceKind"],
|
|
266
|
+
"sourceFile": tx["sourceFile"],
|
|
267
|
+
"rpcUrlResolved": rpc_url,
|
|
268
|
+
"rpcEnvCandidates": rpc_candidates,
|
|
269
|
+
"requestId": tx.get("requestId"),
|
|
270
|
+
"confirmation": build_confirmation_phrase(tx),
|
|
271
|
+
}
|
|
272
|
+
if tx["kind"] == "swap":
|
|
273
|
+
summary["nativeInput"] = tx["nativeInput"]
|
|
274
|
+
summary["gasFee"] = tx.get("gasFee")
|
|
275
|
+
return summary
|
|
276
|
+
|
|
277
|
+
def build_execute_preview(tx: dict[str, Any], explicit_rpc_url: str | None = None) -> dict[str, Any]:
|
|
278
|
+
rpc_url, rpc_candidates = resolve_rpc_url(explicit_rpc_url, tx["chainId"])
|
|
279
|
+
summary = summarize_transaction(tx, rpc_url, rpc_candidates)
|
|
280
|
+
preview_rpc_url = rpc_url or "<unresolved-rpc-url>"
|
|
281
|
+
return {
|
|
282
|
+
"summary": summary,
|
|
283
|
+
"commandPreview": f"pure_signer.sign_transaction + eth_sendRawTransaction (rpc={preview_rpc_url})",
|
|
284
|
+
}
|
|
285
|
+
|
|
286
|
+
|
|
287
|
+
# Price feed integration
|
|
288
|
+
|
|
289
|
+
|
|
290
|
+
def _get_price_feed():
|
|
291
|
+
"""Lazy import to avoid circular deps."""
|
|
292
|
+
from price_feed import get_price, get_prices_batch, get_eth_price
|
|
293
|
+
return get_price, get_prices_batch, get_eth_price
|
|
294
|
+
|
|
295
|
+
|
|
296
|
+
def estimate_trade_value_usd(summary: dict) -> float:
|
|
297
|
+
"""Estimate trade value in USD from transaction summary.
|
|
298
|
+
|
|
299
|
+
Strategy (priority order):
|
|
300
|
+
1. Stablecoin side → quote 金额即 USD
|
|
301
|
+
2. ETH pair → quote ETH 数量 × 实时 ETH 价格(price-feed 多源)
|
|
302
|
+
3. Non-ETH pair → price-feed 批量获取双边价格,拿到任一即返回
|
|
303
|
+
4. 全失败 → 强制确认
|
|
304
|
+
"""
|
|
305
|
+
get_price, get_prices_batch, get_eth_price = _get_price_feed()
|
|
306
|
+
|
|
307
|
+
token_in = (summary.get("tokenIn") or "").upper()
|
|
308
|
+
token_out = (summary.get("tokenOut") or "").upper()
|
|
309
|
+
stablecoins = {"USDC", "USDT", "DAI", "USDC.E", "USDT.E"}
|
|
310
|
+
eth_like = {"ETH", "WETH", "NATIVE"}
|
|
311
|
+
|
|
312
|
+
chain = (summary.get("chain") or "base").lower()
|
|
313
|
+
|
|
314
|
+
# ── 1. Stablecoin side → quote 金额即 USD ──
|
|
315
|
+
if token_in in stablecoins:
|
|
316
|
+
raw = summary.get("amountIn", "0")
|
|
317
|
+
try:
|
|
318
|
+
return float(str(raw).split()[0])
|
|
319
|
+
except (ValueError, IndexError):
|
|
320
|
+
return 0.0
|
|
321
|
+
if token_out in stablecoins:
|
|
322
|
+
raw = summary.get("amountOut", "0")
|
|
323
|
+
try:
|
|
324
|
+
return float(str(raw).split()[0])
|
|
325
|
+
except (ValueError, IndexError):
|
|
326
|
+
return 0.0
|
|
327
|
+
|
|
328
|
+
# ── 2. ETH pair → quote ETH 数量 × ETH 价格 ──
|
|
329
|
+
eth_amount = None
|
|
330
|
+
if token_in in eth_like:
|
|
331
|
+
raw = summary.get("amountIn", "0")
|
|
332
|
+
try:
|
|
333
|
+
eth_amount = float(str(raw).split()[0])
|
|
334
|
+
except (ValueError, IndexError):
|
|
335
|
+
pass
|
|
336
|
+
elif token_out in eth_like:
|
|
337
|
+
raw = summary.get("amountOut", "0")
|
|
338
|
+
try:
|
|
339
|
+
eth_amount = float(str(raw).split()[0])
|
|
340
|
+
except (ValueError, IndexError):
|
|
341
|
+
pass
|
|
342
|
+
|
|
343
|
+
if eth_amount is not None:
|
|
344
|
+
eth_price = get_eth_price(chain, tier="fast")
|
|
345
|
+
if eth_price is not None:
|
|
346
|
+
return eth_amount * eth_price
|
|
347
|
+
|
|
348
|
+
# ── 3. Non-ETH, non-stablecoin pair → price-feed 批量获取 ──
|
|
349
|
+
addr_in = summary.get("inputToken", "")
|
|
350
|
+
addr_out = summary.get("outputToken", "")
|
|
351
|
+
|
|
352
|
+
requests = []
|
|
353
|
+
for addr, raw_amt in [(addr_in, summary.get("amountIn", "0")), (addr_out, summary.get("amountOut", "0"))]:
|
|
354
|
+
if addr and addr.startswith("0x") and len(addr) == 42:
|
|
355
|
+
requests.append((chain, addr, raw_amt))
|
|
356
|
+
|
|
357
|
+
if requests:
|
|
358
|
+
fetch_list = [(c, a) for c, a, _ in requests]
|
|
359
|
+
results = get_prices_batch(fetch_list, tier="fast")
|
|
360
|
+
for chain_r, addr, raw_amt in requests:
|
|
361
|
+
key = f"{chain_r}:{addr.lower()}"
|
|
362
|
+
if key in results:
|
|
363
|
+
try:
|
|
364
|
+
amount = float(str(raw_amt).split()[0])
|
|
365
|
+
return amount * results[key]["price"]
|
|
366
|
+
except (ValueError, IndexError):
|
|
367
|
+
pass
|
|
368
|
+
|
|
369
|
+
# ── 4. 全失败 → 无法确认价值 ──
|
|
370
|
+
return 0.0
|