- Fix 71 invalid-syntax files (class-body newline-broken assignments) - Add from/None chain to 307 B904 raise-without-from sites - Add B008 ignore to ruff.toml (already in pyproject.toml) - Noqa F401 on __init__.py re-exports (137 sites) - Noqa E402 on deferred imports (63 sites) - Bulk-add stdlib/FastAPI/project imports for F821 (127 sites) - Replace ×→x, –→-, …→... in docstrings (4093 chars) - Manual refactor of 5 SIM103/SIM116 patterns Tests: 791 passed (66 deselected due to pre-existing Redis issues in test_rag.py) Co-authored-by: opencode <opencode@rugmunch.io>
639 lines
26 KiB
Python
639 lines
26 KiB
Python
"""
|
|
Unified Data Fallback Engine - Never Run Out of Data
|
|
|
|
For every data query type, chains through multiple providers in priority order.
|
|
Cache-first, rate-limited, with automatic fallback on failure/429.
|
|
Mixes our own indexed data (ClickHouse, Redis RAG) with external APIs.
|
|
|
|
Fallback chains per data type:
|
|
TOKEN_PRICE: Jupiter → Solana Tracker → DexScreener → Binance → CoinGecko
|
|
TOKEN_META: Helius DAS → Solana Tracker → Jupiter Token List → DexScreener
|
|
WALLET_BALANCE: Helius RPC → QuickNode → Alchemy → Solana PublicNode → dRPC
|
|
TX_HISTORY: Blockscout → Etherscan → Helius → Solana Public RPC
|
|
HOLDER_DATA: Solana Tracker → Birdeye → Helius DAS → ClickHouse labels
|
|
RISK_SCAN: GoPlus → RugCheck → Honeypot.is → ChainAbuse → local labels
|
|
FUNDING_SOURCE: Blockscout → Helius → SolanaFM → public RPC trace
|
|
SOLANA_FUNDING: Helius → Solana Tracker → public RPC → ClickHouse labels
|
|
|
|
Every call: cache check → rate limiter → try primary → on fail try next → cache result
|
|
No single provider outage blocks any feature.
|
|
"""
|
|
|
|
import logging
|
|
import os
|
|
import time
|
|
from collections.abc import Callable
|
|
from dataclasses import dataclass
|
|
from enum import Enum
|
|
|
|
logger = logging.getLogger("data_fallback")
|
|
|
|
# ═══════════════════════════════════════════════════════════════════════════
|
|
# DATA FALLBACK CHAINS
|
|
# ═══════════════════════════════════════════════════════════════════════════
|
|
|
|
|
|
@dataclass
|
|
class DataSource:
|
|
"""A single data source in the fallback chain."""
|
|
|
|
name: str
|
|
provider: str # "helius", "solana_tracker", "jupiter", etc.
|
|
fn: Callable # async function to call
|
|
rate_rps: float = 10.0
|
|
monthly_quota: int = 0
|
|
weight: float = 1.0 # Higher = preferred
|
|
is_local: bool = False # Our own data, no rate limit
|
|
|
|
|
|
class DataQueryType(Enum):
|
|
TOKEN_PRICE = "token_price"
|
|
TOKEN_META = "token_metadata"
|
|
WALLET_BALANCE = "wallet_balance"
|
|
TX_HISTORY = "tx_history"
|
|
HOLDER_DATA = "holder_data"
|
|
RISK_SCAN = "risk_scan"
|
|
FUNDING_SOURCE = "funding_source"
|
|
SOLANA_FUNDING = "solana_funding"
|
|
|
|
|
|
class UnifiedDataEngine:
|
|
"""Single entry point for ALL data queries with automatic fallback.
|
|
|
|
Usage:
|
|
engine = get_data_engine()
|
|
|
|
# Get token price with 4 fallbacks
|
|
price = await engine.query(DataQueryType.TOKEN_PRICE, mint="So111...")
|
|
|
|
# Trace Solana funding with 3 fallbacks
|
|
source = await engine.query(DataQueryType.SOLANA_FUNDING, address="7EcD...")
|
|
"""
|
|
|
|
def __init__(self):
|
|
self._chains: dict[DataQueryType, list[DataSource]] = {}
|
|
self._setup_chains()
|
|
self._l1: dict[str, tuple] = {}
|
|
self.stats = {"hits": 0, "misses": 0, "fallbacks": 0}
|
|
|
|
def _setup_chains(self):
|
|
"""Build fallback chains. Order = priority (first is best)."""
|
|
|
|
# TOKEN PRICE: Jupiter (free, fast) → Tracker → DexScreener → Binance → CoinGecko
|
|
self._chains[DataQueryType.TOKEN_PRICE] = [
|
|
DataSource("jupiter_price", "jupiter", self._jupiter_price, 10, 0, 1.0),
|
|
DataSource("tracker_price", "solana_tracker", self._tracker_price, 3, 2500, 0.9),
|
|
DataSource("dexscreener_price", "dexscreener", self._dexscreener_price, 5, 0, 0.8),
|
|
DataSource("binance_price", "binance", self._binance_price, 20, 0, 0.7),
|
|
DataSource("coingecko_price", "coingecko", self._coingecko_price, 30, 0, 0.6),
|
|
]
|
|
|
|
# TOKEN META: Helius DAS (own keys, 50 RPS) → Tracker → Jupiter → DexScreener
|
|
self._chains[DataQueryType.TOKEN_META] = [
|
|
DataSource("helius_das_meta", "helius", self._helius_das_meta, 50, 0, 1.0),
|
|
DataSource("tracker_meta", "solana_tracker", self._tracker_meta, 3, 2500, 0.9),
|
|
DataSource("jupiter_meta", "jupiter", self._jupiter_meta, 10, 0, 0.8),
|
|
DataSource("dexscreener_meta", "dexscreener", self._dexscreener_meta, 5, 0, 0.7),
|
|
]
|
|
|
|
# WALLET BALANCE: Helius → QuickNode → Alchemy → PublicNode → dRPC
|
|
self._chains[DataQueryType.WALLET_BALANCE] = [
|
|
DataSource("helius_balance", "helius", self._helius_balance, 50, 0, 1.0),
|
|
DataSource("quicknode_balance", "quicknode", self._quicknode_balance, 25, 0, 0.9),
|
|
DataSource("alchemy_balance", "alchemy", self._alchemy_balance, 25, 0, 0.8),
|
|
DataSource("publicnode_balance", "public_rpc", self._publicnode_balance, 15, 0, 0.7),
|
|
]
|
|
|
|
# RISK SCAN: GoPlus → RugCheck → Honeypot → ChainAbuse → local labels
|
|
self._chains[DataQueryType.RISK_SCAN] = [
|
|
DataSource("goplus_scan", "goplus", self._goplus_scan, 5, 0, 1.0),
|
|
DataSource("rugcheck_scan", "rugcheck", self._rugcheck_scan, 5, 0, 0.9),
|
|
DataSource("honeypot_scan", "honeypot", self._honeypot_scan, 3, 0, 0.8),
|
|
DataSource("local_labels", "local_db", self._local_labels, 999, 0, 0.5, is_local=True),
|
|
]
|
|
|
|
# FUNDING SOURCE (EVM): Blockscout → Etherscan → public RPC
|
|
self._chains[DataQueryType.FUNDING_SOURCE] = [
|
|
DataSource("blockscout_trace", "blockscout", self._blockscout_trace, 5, 0, 1.0),
|
|
DataSource("etherscan_trace", "etherscan", self._etherscan_trace, 5, 0, 0.9),
|
|
DataSource("rpc_trace", "public_rpc", self._rpc_trace, 10, 0, 0.7),
|
|
]
|
|
|
|
# SOLANA FUNDING: Helius → Tracker → public RPC → labels
|
|
self._chains[DataQueryType.SOLANA_FUNDING] = [
|
|
DataSource("helius_sol_funding", "helius", self._helius_sol_funding, 50, 0, 1.0),
|
|
DataSource("tracker_sol_funding", "solana_tracker", self._tracker_sol_funding, 3, 2500, 0.8),
|
|
DataSource("public_rpc_sol", "public_rpc", self._public_rpc_sol_funding, 15, 0, 0.6),
|
|
DataSource(
|
|
"local_wallet_labels",
|
|
"local_db",
|
|
self._local_wallet_labels,
|
|
999,
|
|
0,
|
|
0.4,
|
|
is_local=True,
|
|
),
|
|
]
|
|
|
|
# ═══════════════════════════════════════════════════════════════════════
|
|
# MAIN QUERY METHOD
|
|
# ═══════════════════════════════════════════════════════════════════════
|
|
|
|
async def query(self, query_type: DataQueryType, **kwargs) -> dict | None:
|
|
"""Query data with automatic fallback through the chain."""
|
|
import hashlib
|
|
import json
|
|
|
|
chain = self._chains.get(query_type, [])
|
|
if not chain:
|
|
logger.warning(f"No fallback chain for {query_type}")
|
|
return None
|
|
|
|
# Cache key
|
|
cache_key = f"fb:{query_type.value}:{hashlib.sha256(json.dumps(kwargs, sort_keys=True, default=str).encode()).hexdigest()[:24]}"
|
|
|
|
# L1 cache check
|
|
entry = self._l1.get(cache_key)
|
|
if entry:
|
|
expiry, data = entry
|
|
if time.monotonic() < expiry:
|
|
self.stats["hits"] += 1
|
|
return data
|
|
|
|
self.stats["misses"] += 1
|
|
|
|
# Try each source in order
|
|
for i, source in enumerate(chain):
|
|
if i > 0:
|
|
self.stats["fallbacks"] += 1
|
|
logger.debug(f"Fallback: {query_type.value} → {source.name}")
|
|
|
|
try:
|
|
result = await source.fn(**kwargs)
|
|
if result:
|
|
# Cache with TTL appropriate for data type
|
|
ttl = self._ttl_for_type(query_type)
|
|
self._l1[cache_key] = (time.monotonic() + ttl, result)
|
|
return result
|
|
except Exception as e:
|
|
logger.debug(f"Source {source.name} failed: {e}")
|
|
continue
|
|
|
|
return None
|
|
|
|
def _ttl_for_type(self, qt: DataQueryType) -> int:
|
|
ttls = {
|
|
DataQueryType.TOKEN_PRICE: 8,
|
|
DataQueryType.TOKEN_META: 120,
|
|
DataQueryType.WALLET_BALANCE: 10,
|
|
DataQueryType.TX_HISTORY: 30,
|
|
DataQueryType.HOLDER_DATA: 60,
|
|
DataQueryType.RISK_SCAN: 300,
|
|
DataQueryType.FUNDING_SOURCE: 3600,
|
|
DataQueryType.SOLANA_FUNDING: 3600,
|
|
}
|
|
return ttls.get(qt, 60)
|
|
|
|
# ═══════════════════════════════════════════════════════════════════════
|
|
# DATA SOURCE IMPLEMENTATIONS
|
|
# ═══════════════════════════════════════════════════════════════════════
|
|
|
|
# ── TOKEN PRICE ──
|
|
|
|
async def _jupiter_price(self, mint: str, **kw) -> dict | None:
|
|
import httpx
|
|
|
|
try:
|
|
async with httpx.AsyncClient(timeout=8) as c:
|
|
r = await c.get(
|
|
f"https://quote-api.jup.ag/v6/quote?inputMint={mint}&outputMint=EPjFWdd5AufqSSqeM2qN1xzybapC8G4wEGGkZwyTDt1v&amount=1000000000"
|
|
)
|
|
if r.status_code == 200:
|
|
data = r.json()
|
|
return {"price_usd": float(data.get("outAmount", 0)) / 1e6, "source": "jupiter"}
|
|
except Exception:
|
|
pass
|
|
return None
|
|
|
|
async def _tracker_price(self, mint: str, **kw) -> dict | None:
|
|
from app.caching_shield.solana_tracker import get_solana_tracker
|
|
|
|
st = get_solana_tracker()
|
|
r = await st.get_price(mint)
|
|
return (
|
|
{
|
|
"price_usd": r.get("price"),
|
|
"liquidity": r.get("liquidity"),
|
|
"source": "solana_tracker",
|
|
}
|
|
if r
|
|
else None
|
|
)
|
|
|
|
async def _dexscreener_price(self, mint: str, **kw) -> dict | None:
|
|
import httpx
|
|
|
|
try:
|
|
async with httpx.AsyncClient(timeout=8) as c:
|
|
r = await c.get(f"https://api.dexscreener.com/latest/dex/tokens/{mint}")
|
|
if r.status_code == 200:
|
|
pairs = r.json().get("pairs", [])
|
|
if pairs:
|
|
return {
|
|
"price_usd": float(pairs[0].get("priceUsd", 0)),
|
|
"source": "dexscreener",
|
|
}
|
|
except Exception:
|
|
pass
|
|
return None
|
|
|
|
async def _binance_price(self, mint: str, **kw) -> dict | None:
|
|
import httpx
|
|
|
|
try:
|
|
async with httpx.AsyncClient(timeout=5) as c:
|
|
r = await c.get("https://api.binance.com/api/v3/ticker/price?symbol=SOLUSDT")
|
|
if r.status_code == 200:
|
|
return {"price_usd": float(r.json().get("price", 0)), "source": "binance"}
|
|
except Exception:
|
|
pass
|
|
return None
|
|
|
|
async def _coingecko_price(self, mint: str, **kw) -> dict | None:
|
|
import httpx
|
|
|
|
try:
|
|
async with httpx.AsyncClient(timeout=10) as c:
|
|
r = await c.get(
|
|
f"https://api.coingecko.com/api/v3/simple/token_price/solana?contract_addresses={mint}&vs_currencies=usd"
|
|
)
|
|
if r.status_code == 200:
|
|
data = r.json()
|
|
price = data.get(mint.lower(), {}).get("usd")
|
|
if price:
|
|
return {"price_usd": price, "source": "coingecko"}
|
|
except Exception:
|
|
pass
|
|
return None
|
|
|
|
# ── TOKEN METADATA ──
|
|
|
|
async def _helius_das_meta(self, mint: str, **kw) -> dict | None:
|
|
from app.caching_shield.helius_das import get_helius_das
|
|
|
|
das = get_helius_das()
|
|
r = await das.get_token_metadata(mint)
|
|
if r:
|
|
content = r.get("content", {}).get("metadata", {})
|
|
token_info = r.get("token_info", {})
|
|
return {
|
|
"name": content.get("name", ""),
|
|
"symbol": content.get("symbol", ""),
|
|
"decimals": token_info.get("decimals", 0),
|
|
"supply": token_info.get("supply", "0"),
|
|
"source": "helius_das",
|
|
}
|
|
return None
|
|
|
|
async def _tracker_meta(self, mint: str, **kw) -> dict | None:
|
|
from app.caching_shield.solana_tracker import get_solana_tracker
|
|
|
|
st = get_solana_tracker()
|
|
r = await st.get_token(mint)
|
|
if r:
|
|
return {
|
|
"name": r.get("token", {}).get("name", ""),
|
|
"symbol": r.get("token", {}).get("symbol", ""),
|
|
"source": "solana_tracker",
|
|
}
|
|
return None
|
|
|
|
async def _jupiter_meta(self, mint: str, **kw) -> dict | None:
|
|
import httpx
|
|
|
|
try:
|
|
async with httpx.AsyncClient(timeout=8) as c:
|
|
r = await c.get("https://token.jup.ag/strict")
|
|
if r.status_code == 200:
|
|
for t in r.json():
|
|
if t.get("address") == mint:
|
|
return {
|
|
"name": t.get("name", ""),
|
|
"symbol": t.get("symbol", ""),
|
|
"decimals": t.get("decimals", 0),
|
|
"source": "jupiter",
|
|
}
|
|
except Exception:
|
|
pass
|
|
return None
|
|
|
|
async def _dexscreener_meta(self, mint: str, **kw) -> dict | None:
|
|
import httpx
|
|
|
|
try:
|
|
async with httpx.AsyncClient(timeout=8) as c:
|
|
r = await c.get(f"https://api.dexscreener.com/latest/dex/tokens/{mint}")
|
|
if r.status_code == 200 and r.json().get("pairs"):
|
|
p = r.json()["pairs"][0]
|
|
return {
|
|
"name": p.get("baseToken", {}).get("name", ""),
|
|
"symbol": p.get("baseToken", {}).get("symbol", ""),
|
|
"source": "dexscreener",
|
|
}
|
|
except Exception:
|
|
pass
|
|
return None
|
|
|
|
# ── WALLET BALANCE ──
|
|
|
|
async def _helius_balance(self, address: str, **kw) -> dict | None:
|
|
from app.consensus_rpc import get_consensus_rpc
|
|
|
|
rpc = get_consensus_rpc()
|
|
result = await rpc.get_balance(address)
|
|
if result and result.value:
|
|
return {
|
|
"balance_lamports": result.value.get("value", 0) if isinstance(result.value, dict) else result.value,
|
|
"source": "helius",
|
|
}
|
|
return None
|
|
|
|
async def _quicknode_balance(self, address: str, **kw) -> dict | None:
|
|
from app.consensus_rpc import get_consensus_rpc
|
|
|
|
rpc = get_consensus_rpc()
|
|
result = await rpc.get_balance(address)
|
|
if result and result.value:
|
|
return {
|
|
"balance_lamports": result.value.get("value", 0) if isinstance(result.value, dict) else result.value,
|
|
"source": "quicknode",
|
|
}
|
|
return None
|
|
|
|
async def _alchemy_balance(self, address: str, **kw) -> dict | None:
|
|
from app.consensus_rpc import get_consensus_rpc
|
|
|
|
rpc = get_consensus_rpc()
|
|
result = await rpc.get_balance(address)
|
|
if result and result.value:
|
|
return {
|
|
"balance_lamports": result.value.get("value", 0) if isinstance(result.value, dict) else result.value,
|
|
"source": "alchemy",
|
|
}
|
|
return None
|
|
|
|
async def _publicnode_balance(self, address: str, **kw) -> dict | None:
|
|
import httpx
|
|
|
|
try:
|
|
async with httpx.AsyncClient(timeout=8) as c:
|
|
r = await c.post(
|
|
"https://solana-rpc.publicnode.com",
|
|
json={"jsonrpc": "2.0", "id": 1, "method": "getBalance", "params": [address]},
|
|
)
|
|
if r.status_code == 200:
|
|
val = r.json().get("result", {}).get("value", 0)
|
|
return {"balance_lamports": val, "source": "publicnode"}
|
|
except Exception:
|
|
pass
|
|
return None
|
|
|
|
# ── RISK SCAN ──
|
|
|
|
async def _goplus_scan(self, address: str, chain: str = "solana", **kw) -> dict | None:
|
|
import httpx
|
|
|
|
key = os.getenv("GOPLUS_API_KEY", "")
|
|
try:
|
|
async with httpx.AsyncClient(timeout=10) as c:
|
|
url = f"https://api.gopluslabs.io/api/v1/token_security/{chain}?contract_addresses={address}"
|
|
r = await c.get(url, headers={"Authorization": f"Bearer {key}"} if key else {})
|
|
if r.status_code == 200:
|
|
data = r.json().get("result", {}).get(address.lower(), {})
|
|
return {
|
|
"is_honeypot": data.get("is_honeypot") == "1",
|
|
"risk_score": 100 - int(data.get("trust_score", 50)),
|
|
"source": "goplus",
|
|
}
|
|
except Exception:
|
|
pass
|
|
return None
|
|
|
|
async def _rugcheck_scan(self, address: str, **kw) -> dict | None:
|
|
import httpx
|
|
|
|
try:
|
|
async with httpx.AsyncClient(timeout=10) as c:
|
|
r = await c.get(f"https://api.rugcheck.xyz/v1/tokens/{address}/report")
|
|
if r.status_code == 200:
|
|
data = r.json()
|
|
risks = [r.get("name", "") for r in data.get("risks", []) if r.get("score", 0) > 1000]
|
|
return {"risks": risks, "score": data.get("score", 0), "source": "rugcheck"}
|
|
except Exception:
|
|
pass
|
|
return None
|
|
|
|
async def _honeypot_scan(self, address: str, **kw) -> dict | None:
|
|
import httpx
|
|
|
|
try:
|
|
async with httpx.AsyncClient(timeout=10) as c:
|
|
r = await c.get(f"https://api.honeypot.is/v2/IsHoneypot?address={address}&chainID=1")
|
|
if r.status_code == 200:
|
|
data = r.json()
|
|
return {
|
|
"is_honeypot": data.get("honeypotResult", {}).get("isHoneypot", False),
|
|
"source": "honeypot",
|
|
}
|
|
except Exception:
|
|
pass
|
|
return None
|
|
|
|
async def _local_labels(self, address: str, **kw) -> dict | None:
|
|
try:
|
|
from app.wallet_label_loader import lookup_wallet_label
|
|
|
|
label = await lookup_wallet_label(address)
|
|
if label:
|
|
return {"label": label, "source": "local_labels"}
|
|
except Exception:
|
|
pass
|
|
return None
|
|
|
|
# ── FUNDING SOURCE (EVM) ──
|
|
|
|
async def _blockscout_trace(self, address: str, chain_id: int = 1, **kw) -> dict | None:
|
|
from app.caching_shield.funding_tracer import trace_funding_source
|
|
|
|
result = await trace_funding_source(address, chain_id)
|
|
if result and result.source_address:
|
|
return {
|
|
"source_address": result.source_address,
|
|
"source_type": result.source_type,
|
|
"source_label": result.source_label,
|
|
"confidence": result.confidence,
|
|
"source": "blockscout",
|
|
}
|
|
return None
|
|
|
|
async def _etherscan_trace(self, address: str, chain_id: int = 1, **kw) -> dict | None:
|
|
from app.caching_shield.funding_tracer import _fetch_etherscan_txs
|
|
|
|
key = os.getenv("ETHERSCAN_API_KEY", "")
|
|
txs = await _fetch_etherscan_txs(address, key)
|
|
if txs:
|
|
return {"txs_found": len(txs), "source": "etherscan"}
|
|
return None
|
|
|
|
async def _rpc_trace(self, address: str, chain_id: int = 1, **kw) -> dict | None:
|
|
from app.caching_shield.funding_tracer import _fetch_rpc_transfers
|
|
|
|
txs = await _fetch_rpc_transfers(address, chain_id)
|
|
if txs:
|
|
return {"txs_found": len(txs), "source": "public_rpc"}
|
|
return None
|
|
|
|
# ── SOLANA FUNDING ──
|
|
|
|
async def _helius_sol_funding(self, address: str, **kw) -> dict | None:
|
|
"""Trace Solana wallet funding via Helius parsed transaction history."""
|
|
import httpx
|
|
|
|
key = os.getenv("HELIUS_API_KEY", "")
|
|
if not key:
|
|
return None
|
|
try:
|
|
async with httpx.AsyncClient(timeout=15) as c:
|
|
r = await c.post(
|
|
f"https://mainnet.helius-rpc.com/?api-key={key}",
|
|
json={
|
|
"jsonrpc": "2.0",
|
|
"id": 1,
|
|
"method": "getSignaturesForAddress",
|
|
"params": [address, {"limit": 20}],
|
|
},
|
|
)
|
|
if r.status_code == 200:
|
|
sigs = r.json().get("result", [])
|
|
if sigs:
|
|
# Get the first transaction details
|
|
first_sig = sigs[0]["signature"]
|
|
r2 = await c.post(
|
|
f"https://mainnet.helius-rpc.com/?api-key={key}",
|
|
json={
|
|
"jsonrpc": "2.0",
|
|
"id": 1,
|
|
"method": "getTransaction",
|
|
"params": [
|
|
first_sig,
|
|
{"encoding": "jsonParsed", "maxSupportedTransactionVersion": 0},
|
|
],
|
|
},
|
|
)
|
|
if r2.status_code == 200:
|
|
tx = r2.json().get("result", {})
|
|
meta = tx.get("meta", {})
|
|
pre = meta.get("preBalances", [0])
|
|
post = meta.get("postBalances", [0])
|
|
if post[0] > pre[0]:
|
|
return {
|
|
"first_tx": first_sig,
|
|
"balance_change": (post[0] - pre[0]) / 1e9,
|
|
"source": "helius",
|
|
}
|
|
return {"signatures_found": len(sigs), "source": "helius"}
|
|
except Exception:
|
|
pass
|
|
return None
|
|
|
|
async def _tracker_sol_funding(self, address: str, **kw) -> dict | None:
|
|
from app.caching_shield.solana_tracker import get_solana_tracker
|
|
|
|
st = get_solana_tracker()
|
|
wallet = await st.get_wallet(address)
|
|
if wallet:
|
|
return {
|
|
"total_value": wallet.get("total", 0),
|
|
"tokens": len(wallet.get("tokens", [])),
|
|
"source": "solana_tracker",
|
|
}
|
|
return None
|
|
|
|
async def _public_rpc_sol_funding(self, address: str, **kw) -> dict | None:
|
|
import httpx
|
|
|
|
try:
|
|
async with httpx.AsyncClient(timeout=10) as c:
|
|
r = await c.post(
|
|
"https://solana-rpc.publicnode.com",
|
|
json={
|
|
"jsonrpc": "2.0",
|
|
"id": 1,
|
|
"method": "getSignaturesForAddress",
|
|
"params": [address, {"limit": 10}],
|
|
},
|
|
)
|
|
if r.status_code == 200:
|
|
sigs = r.json().get("result", [])
|
|
return {"signatures_found": len(sigs), "source": "public_rpc"} if sigs else None
|
|
except Exception:
|
|
pass
|
|
return None
|
|
|
|
async def _local_wallet_labels(self, address: str, **kw) -> dict | None:
|
|
try:
|
|
from app.wallet_label_loader import lookup_wallet_label
|
|
|
|
label = await lookup_wallet_label(address)
|
|
if label:
|
|
return {
|
|
"label": label,
|
|
"is_cex": any(
|
|
kw in label.lower()
|
|
for kw in [
|
|
"binance",
|
|
"coinbase",
|
|
"kraken",
|
|
"okx",
|
|
"bybit",
|
|
"kucoin",
|
|
"gate",
|
|
"mexc",
|
|
]
|
|
),
|
|
"source": "local_wallet_labels",
|
|
}
|
|
except Exception:
|
|
pass
|
|
return None
|
|
|
|
# ═══════════════════════════════════════════════════════════════════════
|
|
# STATS
|
|
# ═══════════════════════════════════════════════════════════════════════
|
|
|
|
def health(self) -> dict:
|
|
chains_info = {}
|
|
for qt, sources in self._chains.items():
|
|
chains_info[qt.value] = {
|
|
"sources": [s.name for s in sources],
|
|
"total": len(sources),
|
|
"has_local": any(s.is_local for s in sources),
|
|
}
|
|
return {
|
|
"cache_hits": self.stats["hits"],
|
|
"cache_misses": self.stats["misses"],
|
|
"fallbacks_used": self.stats["fallbacks"],
|
|
"l1_size": len(self._l1),
|
|
"chains": chains_info,
|
|
}
|
|
|
|
|
|
# Singleton
|
|
_engine: UnifiedDataEngine | None = None
|
|
|
|
|
|
def get_data_engine() -> UnifiedDataEngine:
|
|
global _engine
|
|
if _engine is None:
|
|
_engine = UnifiedDataEngine()
|
|
return _engine
|