673 lines
24 KiB
Python
673 lines
24 KiB
Python
"""On-chain discovery storage and read models.
|
|
|
|
The on-chain layer is a research/discovery input. It stores normalized external
|
|
facts and can enqueue technical-check candidates, but it must not create or
|
|
mutate trading recommendations directly.
|
|
"""
|
|
|
|
import json
|
|
from datetime import datetime, timedelta
|
|
|
|
from app.db.altcoin_db import get_conn
|
|
from app.db.postgres_connection import ensure_migrations_once
|
|
|
|
|
|
MIN_MAPPING_CONFIDENCE = 70
|
|
|
|
|
|
SIGNAL_LABELS = {
|
|
"dex_volume_spike": "DEX 放量",
|
|
"liquidity_add": "流动性增加",
|
|
"liquidity_remove_risk": "流动性撤出风险",
|
|
"exchange_outflow": "交易所流出",
|
|
"exchange_inflow_risk": "交易所流入风险",
|
|
"whale_accumulation": "鲸鱼增持",
|
|
"holder_concentration_risk": "持仓集中风险",
|
|
"smart_money_buying": "聪明钱买入",
|
|
}
|
|
|
|
RAW_EVENT_TYPE_LABELS = {
|
|
"token_profile_latest": "Token 资料更新",
|
|
"token_boost_latest": "DEX 热度 Boost",
|
|
"token_boost_top": "DEX Boost 榜",
|
|
}
|
|
|
|
POSITIVE_SIGNALS = {"dex_volume_spike", "liquidity_add", "exchange_outflow", "whale_accumulation", "smart_money_buying"}
|
|
RISK_SIGNALS = {"liquidity_remove_risk", "exchange_inflow_risk", "holder_concentration_risk"}
|
|
|
|
|
|
def _now():
|
|
return datetime.now().isoformat()
|
|
|
|
|
|
def _dump(value):
|
|
return json.dumps(value or {}, ensure_ascii=False, sort_keys=True, default=str)
|
|
|
|
|
|
def _load(value, fallback=None):
|
|
try:
|
|
if isinstance(value, str) and value.strip():
|
|
return json.loads(value)
|
|
if value is not None:
|
|
return value
|
|
except Exception:
|
|
pass
|
|
return fallback
|
|
|
|
|
|
def _symbol_base(symbol):
|
|
return str(symbol or "").upper().replace("/USDT", "").replace("USDT", "").strip()
|
|
|
|
|
|
def normalize_symbol(symbol):
|
|
base = _symbol_base(symbol)
|
|
return f"{base}/USDT" if base else ""
|
|
|
|
|
|
def signal_label(code):
|
|
return SIGNAL_LABELS.get(str(code or ""), str(code or "链上信号"))
|
|
|
|
|
|
def signal_direction(code):
|
|
code = str(code or "")
|
|
if code in RISK_SIGNALS:
|
|
return "risk"
|
|
if code in POSITIVE_SIGNALS:
|
|
return "positive"
|
|
return "neutral"
|
|
|
|
|
|
def raw_event_type_label(event_type):
|
|
return RAW_EVENT_TYPE_LABELS.get(str(event_type or ""), str(event_type or "链上原始事件"))
|
|
|
|
|
|
def init_onchain_tables():
|
|
ensure_migrations_once()
|
|
|
|
|
|
def upsert_token_mapping(symbol, chain, contract_address, source="", confidence=0, raw=None, is_active=True):
|
|
init_onchain_tables()
|
|
now = _now()
|
|
symbol = normalize_symbol(symbol)
|
|
chain = str(chain or "").lower().strip()
|
|
contract_address = str(contract_address or "").strip()
|
|
if not symbol or not chain or not contract_address:
|
|
return 0
|
|
conn = get_conn()
|
|
cur = conn.execute(
|
|
"""
|
|
INSERT INTO onchain_token_map
|
|
(symbol, chain, contract_address, source, confidence, is_active, raw_json, created_at, updated_at)
|
|
VALUES (%s, %s, %s, %s, %s, %s, %s, %s, %s)
|
|
ON CONFLICT(symbol, chain, contract_address) DO UPDATE SET
|
|
source=excluded.source,
|
|
confidence=GREATEST(onchain_token_map.confidence, excluded.confidence),
|
|
is_active=excluded.is_active,
|
|
raw_json=excluded.raw_json,
|
|
updated_at=excluded.updated_at
|
|
""",
|
|
(symbol, chain, contract_address, source or "", int(confidence or 0), 1 if is_active else 0, _dump(raw), now, now),
|
|
)
|
|
conn.commit()
|
|
row = conn.execute(
|
|
"SELECT id FROM onchain_token_map WHERE symbol=%s AND chain=%s AND contract_address=%s",
|
|
(symbol, chain, contract_address),
|
|
).fetchone()
|
|
conn.close()
|
|
return int(row["id"] if row else 0)
|
|
|
|
|
|
def get_token_mappings(symbol="", min_confidence=MIN_MAPPING_CONFIDENCE, active_only=True):
|
|
init_onchain_tables()
|
|
clauses = ["confidence >= %s"]
|
|
params = [int(min_confidence or 0)]
|
|
if symbol:
|
|
clauses.append("symbol=%s")
|
|
params.append(normalize_symbol(symbol))
|
|
if active_only:
|
|
clauses.append("is_active=1")
|
|
conn = get_conn()
|
|
rows = conn.execute(
|
|
f"""
|
|
SELECT * FROM onchain_token_map
|
|
WHERE {' AND '.join(clauses)}
|
|
ORDER BY confidence DESC, updated_at DESC
|
|
""",
|
|
tuple(params),
|
|
).fetchall()
|
|
conn.close()
|
|
return [dict(row) for row in rows]
|
|
|
|
|
|
def _event_hash(event):
|
|
raw = "|".join(
|
|
[
|
|
str(event.get("source") or ""),
|
|
str(event.get("chain") or ""),
|
|
str(event.get("symbol") or ""),
|
|
str(event.get("signal_code") or event.get("event_type") or ""),
|
|
str(event.get("tx_hash") or event.get("contract_address") or ""),
|
|
str(event.get("detected_at") or ""),
|
|
str(round(float(event.get("value_usd") or 0), 2)),
|
|
]
|
|
).lower()
|
|
import hashlib
|
|
|
|
return hashlib.sha256(raw.encode()).hexdigest()[:24]
|
|
|
|
|
|
def _raw_event_hash(event):
|
|
raw = "|".join(
|
|
[
|
|
str(event.get("source") or ""),
|
|
str(event.get("chain") or ""),
|
|
str(event.get("event_type") or ""),
|
|
str(event.get("token_address") or ""),
|
|
str(event.get("url") or ""),
|
|
str(round(float(event.get("amount") or 0), 4)),
|
|
str(round(float(event.get("total_amount") or 0), 4)),
|
|
]
|
|
).lower()
|
|
import hashlib
|
|
|
|
return hashlib.sha256(raw.encode()).hexdigest()[:24]
|
|
|
|
|
|
def insert_onchain_event(event):
|
|
init_onchain_tables()
|
|
item = dict(event or {})
|
|
item["symbol"] = normalize_symbol(item.get("symbol"))
|
|
item["chain"] = str(item.get("chain") or "").lower().strip()
|
|
item["signal_code"] = str(item.get("signal_code") or "").strip()
|
|
item["event_type"] = str(item.get("event_type") or item["signal_code"] or "onchain_event")
|
|
if not item["symbol"] or not item["chain"] or not item["signal_code"]:
|
|
return 0
|
|
item["signal_label"] = item.get("signal_label") or signal_label(item["signal_code"])
|
|
item["direction"] = item.get("direction") or signal_direction(item["signal_code"])
|
|
item["detected_at"] = str(item.get("detected_at") or _now())
|
|
item["event_hash"] = item.get("event_hash") or _event_hash(item)
|
|
conn = get_conn()
|
|
try:
|
|
cur = conn.execute(
|
|
"""
|
|
INSERT INTO onchain_events (
|
|
event_hash, chain, symbol, contract_address, event_type, signal_code, signal_label,
|
|
direction, value_usd, amount, tx_hash, wallet_address, wallet_label,
|
|
counterparty_label, confidence, severity, status, detected_at, source, url, raw_json
|
|
) VALUES (%s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s)
|
|
ON CONFLICT(event_hash) DO NOTHING
|
|
RETURNING id
|
|
""",
|
|
(
|
|
item["event_hash"],
|
|
item["chain"],
|
|
item["symbol"],
|
|
item.get("contract_address") or "",
|
|
item["event_type"],
|
|
item["signal_code"],
|
|
item["signal_label"],
|
|
item["direction"],
|
|
float(item.get("value_usd") or 0),
|
|
float(item.get("amount") or 0),
|
|
item.get("tx_hash") or "",
|
|
item.get("wallet_address") or "",
|
|
item.get("wallet_label") or "",
|
|
item.get("counterparty_label") or "",
|
|
int(item.get("confidence") or 0),
|
|
item.get("severity") or "B",
|
|
item.get("status") or "new",
|
|
item["detected_at"],
|
|
item.get("source") or "",
|
|
item.get("url") or "",
|
|
_dump(item.get("raw") or item.get("raw_json") or {}),
|
|
),
|
|
)
|
|
row = cur.fetchone()
|
|
event_id = int(row["id"] if row else 0)
|
|
conn.commit()
|
|
finally:
|
|
conn.close()
|
|
return event_id
|
|
|
|
|
|
def find_mapping_by_contract(chain, contract_address):
|
|
init_onchain_tables()
|
|
chain = str(chain or "").lower().strip()
|
|
contract_address = str(contract_address or "").strip()
|
|
if not chain or not contract_address:
|
|
return None
|
|
conn = get_conn()
|
|
row = conn.execute(
|
|
"""
|
|
SELECT *
|
|
FROM onchain_token_map
|
|
WHERE chain=%s AND lower(contract_address)=lower(%s) AND is_active=1
|
|
ORDER BY confidence DESC, updated_at DESC
|
|
LIMIT 1
|
|
""",
|
|
(chain, contract_address),
|
|
).fetchone()
|
|
conn.close()
|
|
return dict(row) if row else None
|
|
|
|
|
|
def insert_onchain_raw_event(event):
|
|
init_onchain_tables()
|
|
item = dict(event or {})
|
|
item["source"] = str(item.get("source") or "").strip()
|
|
item["chain"] = str(item.get("chain") or "").lower().strip()
|
|
item["event_type"] = str(item.get("event_type") or "onchain_raw_event").strip()
|
|
item["token_address"] = str(item.get("token_address") or "").strip()
|
|
if not item["source"] or not item["chain"] or not item["event_type"] or not item["token_address"]:
|
|
return 0
|
|
item["detected_at"] = str(item.get("detected_at") or _now())
|
|
item["event_hash"] = item.get("event_hash") or _raw_event_hash(item)
|
|
item["mapped_symbol"] = normalize_symbol(item.get("mapped_symbol")) if item.get("mapped_symbol") else ""
|
|
item["mapping_status"] = str(item.get("mapping_status") or ("mapped" if item["mapped_symbol"] else "unmapped"))
|
|
conn = get_conn()
|
|
try:
|
|
cur = conn.execute(
|
|
"""
|
|
INSERT INTO onchain_raw_events (
|
|
event_hash, source, chain, event_type, token_address, symbol_guess, name,
|
|
title, description, url, icon, amount, total_amount, importance,
|
|
mapped_symbol, mapping_status, detected_at, raw_json
|
|
) VALUES (%s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s)
|
|
ON CONFLICT(event_hash) DO NOTHING
|
|
RETURNING id
|
|
""",
|
|
(
|
|
item["event_hash"],
|
|
item["source"],
|
|
item["chain"],
|
|
item["event_type"],
|
|
item["token_address"],
|
|
item.get("symbol_guess") or "",
|
|
item.get("name") or "",
|
|
item.get("title") or raw_event_type_label(item["event_type"]),
|
|
item.get("description") or "",
|
|
item.get("url") or "",
|
|
item.get("icon") or "",
|
|
float(item.get("amount") or 0),
|
|
float(item.get("total_amount") or 0),
|
|
float(item.get("importance") or 0),
|
|
item["mapped_symbol"],
|
|
item["mapping_status"],
|
|
item["detected_at"],
|
|
_dump(item.get("raw") or item.get("raw_json") or {}),
|
|
),
|
|
)
|
|
row = cur.fetchone()
|
|
event_id = int(row["id"] if row else 0)
|
|
conn.commit()
|
|
finally:
|
|
conn.close()
|
|
return event_id
|
|
|
|
|
|
def insert_token_metric(metric):
|
|
init_onchain_tables()
|
|
item = dict(metric or {})
|
|
item["symbol"] = normalize_symbol(item.get("symbol"))
|
|
item["chain"] = str(item.get("chain") or "").lower().strip()
|
|
item["window"] = str(item.get("window") or "1h").strip()
|
|
item["metric_time"] = str(item.get("metric_time") or _now())
|
|
if not item["symbol"] or not item["chain"] or not item["window"]:
|
|
return 0
|
|
conn = get_conn()
|
|
cur = conn.execute(
|
|
"""
|
|
INSERT INTO onchain_token_metrics (
|
|
symbol, chain, contract_address, "window", metric_time,
|
|
dex_volume_usd, dex_volume_change_pct, liquidity_usd, liquidity_change_pct,
|
|
exchange_netflow_usd, whale_accumulation_usd, holder_delta, smart_money_score,
|
|
onchain_score, risk_score, source, raw_json
|
|
) VALUES (%s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s)
|
|
ON CONFLICT(symbol, chain, contract_address, "window", metric_time) DO UPDATE SET
|
|
dex_volume_usd=excluded.dex_volume_usd,
|
|
dex_volume_change_pct=excluded.dex_volume_change_pct,
|
|
liquidity_usd=excluded.liquidity_usd,
|
|
liquidity_change_pct=excluded.liquidity_change_pct,
|
|
exchange_netflow_usd=excluded.exchange_netflow_usd,
|
|
whale_accumulation_usd=excluded.whale_accumulation_usd,
|
|
holder_delta=excluded.holder_delta,
|
|
smart_money_score=excluded.smart_money_score,
|
|
onchain_score=excluded.onchain_score,
|
|
risk_score=excluded.risk_score,
|
|
source=excluded.source,
|
|
raw_json=excluded.raw_json
|
|
RETURNING id
|
|
""",
|
|
(
|
|
item["symbol"],
|
|
item["chain"],
|
|
item.get("contract_address") or "",
|
|
item["window"],
|
|
item["metric_time"],
|
|
float(item.get("dex_volume_usd") or 0),
|
|
float(item.get("dex_volume_change_pct") or 0),
|
|
float(item.get("liquidity_usd") or 0),
|
|
float(item.get("liquidity_change_pct") or 0),
|
|
float(item.get("exchange_netflow_usd") or 0),
|
|
float(item.get("whale_accumulation_usd") or 0),
|
|
float(item.get("holder_delta") or 0),
|
|
float(item.get("smart_money_score") or 0),
|
|
float(item.get("onchain_score") or 0),
|
|
float(item.get("risk_score") or 0),
|
|
item.get("source") or "",
|
|
_dump(item.get("raw") or item.get("raw_json") or {}),
|
|
),
|
|
)
|
|
conn.commit()
|
|
metric_id = int(cur.fetchone()["id"] or 0)
|
|
conn.close()
|
|
return metric_id
|
|
|
|
|
|
def _latest_metrics_subquery(hours=24):
|
|
return """
|
|
SELECT m.*
|
|
FROM onchain_token_metrics m
|
|
JOIN (
|
|
SELECT symbol, chain, contract_address, MAX(metric_time) AS max_time
|
|
FROM onchain_token_metrics
|
|
WHERE metric_time >= %s
|
|
GROUP BY symbol, chain, contract_address
|
|
) latest ON latest.symbol=m.symbol
|
|
AND latest.chain=m.chain
|
|
AND latest.contract_address=m.contract_address
|
|
AND latest.max_time=m.metric_time
|
|
"""
|
|
|
|
|
|
def get_onchain_overview(hours=24):
|
|
init_onchain_tables()
|
|
cutoff = (datetime.now() - timedelta(hours=int(hours or 24))).isoformat()
|
|
conn = get_conn()
|
|
event_rows = conn.execute("SELECT * FROM onchain_events WHERE detected_at >= %s", (cutoff,)).fetchall()
|
|
raw_rows = conn.execute("SELECT * FROM onchain_raw_events WHERE detected_at >= %s", (cutoff,)).fetchall()
|
|
raw_latest = conn.execute(
|
|
"""
|
|
SELECT * FROM onchain_raw_events
|
|
WHERE detected_at >= %s
|
|
ORDER BY detected_at::timestamp DESC, importance DESC, id DESC
|
|
LIMIT 12
|
|
""",
|
|
(cutoff,),
|
|
).fetchall()
|
|
metric_rows = conn.execute(_latest_metrics_subquery(hours), (cutoff,)).fetchall()
|
|
rec_rows = conn.execute(
|
|
"SELECT symbol, id, execution_status, action_status, display_bucket FROM recommendation WHERE status='active'"
|
|
).fetchall()
|
|
conn.close()
|
|
active = {row["symbol"]: dict(row) for row in rec_rows}
|
|
events = [dict(row) for row in event_rows]
|
|
raw_events = [dict(row) for row in raw_rows]
|
|
metrics = [dict(row) for row in metric_rows]
|
|
hot = sorted(metrics, key=lambda x: float(x.get("onchain_score") or 0), reverse=True)[:8]
|
|
risks = sorted(metrics, key=lambda x: float(x.get("risk_score") or 0), reverse=True)[:8]
|
|
total_netflow = sum(float(x.get("exchange_netflow_usd") or 0) for x in metrics)
|
|
dex_volume = sum(float(x.get("dex_volume_usd") or 0) for x in metrics)
|
|
return {
|
|
"hours": int(hours or 24),
|
|
"updated_at": _now(),
|
|
"kpi": {
|
|
"event_count": len(events),
|
|
"raw_event_count": len(raw_events),
|
|
"raw_unmapped_count": sum(1 for e in raw_events if e.get("mapping_status") == "unmapped"),
|
|
"raw_mapped_count": sum(1 for e in raw_events if e.get("mapping_status") == "mapped"),
|
|
"token_count": len({(m["symbol"], m["chain"], m.get("contract_address") or "") for m in metrics}),
|
|
"positive_events": sum(1 for e in events if e.get("direction") == "positive"),
|
|
"risk_events": sum(1 for e in events if e.get("direction") == "risk"),
|
|
"exchange_netflow_usd": round(total_netflow, 2),
|
|
"dex_volume_usd": round(dex_volume, 2),
|
|
},
|
|
"hot_tokens": [_format_metric_item(row, active) for row in hot],
|
|
"risk_tokens": [_format_metric_item(row, active) for row in risks],
|
|
"raw_events": [_format_raw_event(row) for row in raw_latest],
|
|
"signals": _signal_counts(events),
|
|
}
|
|
|
|
|
|
def _signal_counts(events):
|
|
counts = {}
|
|
for e in events:
|
|
code = e.get("signal_code") or ""
|
|
if not code:
|
|
continue
|
|
counts.setdefault(code, {"signal_code": code, "signal_label": signal_label(code), "count": 0})
|
|
counts[code]["count"] += 1
|
|
return sorted(counts.values(), key=lambda x: x["count"], reverse=True)
|
|
|
|
|
|
def _format_metric_item(row, active=None):
|
|
active = active or {}
|
|
item = dict(row)
|
|
item["raw"] = _load(item.pop("raw_json", "{}"), {})
|
|
rec = active.get(item.get("symbol")) or {}
|
|
item["recommendation"] = {
|
|
"rec_id": rec.get("id") or 0,
|
|
"execution_status": rec.get("execution_status") or "",
|
|
"action_status": rec.get("action_status") or "",
|
|
"display_bucket": rec.get("display_bucket") or "",
|
|
"has_active": bool(rec),
|
|
}
|
|
return item
|
|
|
|
|
|
def list_onchain_tokens(limit=30, offset=0, chain="", signal="", hours=24):
|
|
init_onchain_tables()
|
|
limit = max(1, min(int(limit or 30), 100))
|
|
offset = max(0, int(offset or 0))
|
|
cutoff = (datetime.now() - timedelta(hours=int(hours or 24))).isoformat()
|
|
clauses = []
|
|
params = []
|
|
if chain:
|
|
clauses.append("m.chain=%s")
|
|
params.append(str(chain).lower())
|
|
if signal:
|
|
clauses.append(
|
|
"""
|
|
EXISTS (
|
|
SELECT 1 FROM onchain_events e
|
|
WHERE e.symbol=m.symbol AND e.chain=m.chain
|
|
AND e.detected_at >= %s AND e.signal_code=%s
|
|
)
|
|
"""
|
|
)
|
|
params.extend([cutoff, signal])
|
|
where = " AND ".join(clauses) if clauses else "1=1"
|
|
conn = get_conn()
|
|
total = conn.execute(
|
|
f"""
|
|
SELECT COUNT(*) FROM (
|
|
SELECT m.symbol, m.chain, m.contract_address
|
|
FROM onchain_token_metrics m
|
|
WHERE {where}
|
|
GROUP BY m.symbol, m.chain, m.contract_address
|
|
)
|
|
""",
|
|
tuple(params),
|
|
).fetchone()[0]
|
|
rows = conn.execute(
|
|
f"""
|
|
SELECT m.*,
|
|
(SELECT COUNT(*) FROM onchain_events e
|
|
WHERE e.symbol=m.symbol AND e.chain=m.chain AND e.detected_at >= %s) AS event_count,
|
|
(SELECT COUNT(*) FROM onchain_events e
|
|
WHERE e.symbol=m.symbol AND e.chain=m.chain AND e.direction='risk' AND e.detected_at >= %s) AS risk_event_count
|
|
FROM ({_latest_metrics_subquery(hours)}) m
|
|
WHERE {where}
|
|
ORDER BY m.onchain_score DESC, m.risk_score DESC, m.metric_time DESC
|
|
LIMIT %s OFFSET %s
|
|
""",
|
|
(cutoff, cutoff, cutoff, *params, limit, offset),
|
|
).fetchall()
|
|
rec_rows = conn.execute(
|
|
"SELECT symbol, id, execution_status, action_status, display_bucket FROM recommendation WHERE status='active'"
|
|
).fetchall()
|
|
conn.close()
|
|
active = {row["symbol"]: dict(row) for row in rec_rows}
|
|
return {
|
|
"items": [_format_metric_item(row, active) for row in rows],
|
|
"total": int(total or 0),
|
|
"limit": limit,
|
|
"offset": offset,
|
|
"has_more": offset + len(rows) < int(total or 0),
|
|
}
|
|
|
|
|
|
def get_onchain_token_detail(symbol, hours=72):
|
|
init_onchain_tables()
|
|
symbol = normalize_symbol(symbol)
|
|
cutoff = (datetime.now() - timedelta(hours=int(hours or 72))).isoformat()
|
|
conn = get_conn()
|
|
mappings = conn.execute(
|
|
"SELECT * FROM onchain_token_map WHERE symbol=%s ORDER BY confidence DESC, updated_at DESC",
|
|
(symbol,),
|
|
).fetchall()
|
|
events = conn.execute(
|
|
"""
|
|
SELECT * FROM onchain_events
|
|
WHERE symbol=%s AND detected_at >= %s
|
|
ORDER BY detected_at DESC, id DESC
|
|
LIMIT 100
|
|
""",
|
|
(symbol, cutoff),
|
|
).fetchall()
|
|
metrics = conn.execute(
|
|
"""
|
|
SELECT * FROM onchain_token_metrics
|
|
WHERE symbol=%s AND metric_time >= %s
|
|
ORDER BY metric_time DESC, id DESC
|
|
LIMIT 100
|
|
""",
|
|
(symbol, cutoff),
|
|
).fetchall()
|
|
rec = conn.execute(
|
|
"""
|
|
SELECT id, rec_time, action_status, execution_status, display_bucket, entry_price, current_price
|
|
FROM recommendation
|
|
WHERE symbol=%s AND status='active'
|
|
ORDER BY id DESC LIMIT 1
|
|
""",
|
|
(symbol,),
|
|
).fetchone()
|
|
conn.close()
|
|
return {
|
|
"symbol": symbol,
|
|
"hours": int(hours or 72),
|
|
"mappings": [_with_raw(row) for row in mappings],
|
|
"events": [_with_raw(row) for row in events],
|
|
"metrics": [_with_raw(row) for row in metrics],
|
|
"recommendation": dict(rec) if rec else None,
|
|
}
|
|
|
|
|
|
def list_onchain_events(limit=50, offset=0, chain="", signal="", status="", hours=24):
|
|
init_onchain_tables()
|
|
limit = max(1, min(int(limit or 50), 200))
|
|
offset = max(0, int(offset or 0))
|
|
cutoff = (datetime.now() - timedelta(hours=int(hours or 24))).isoformat()
|
|
clauses = ["detected_at >= %s"]
|
|
params = [cutoff]
|
|
if chain:
|
|
clauses.append("chain=%s")
|
|
params.append(str(chain).lower())
|
|
if signal:
|
|
clauses.append("signal_code=%s")
|
|
params.append(signal)
|
|
if status:
|
|
clauses.append("status=%s")
|
|
params.append(status)
|
|
where = " AND ".join(clauses)
|
|
conn = get_conn()
|
|
total = conn.execute(f"SELECT COUNT(*) FROM onchain_events WHERE {where}", tuple(params)).fetchone()[0]
|
|
rows = conn.execute(
|
|
f"""
|
|
SELECT * FROM onchain_events
|
|
WHERE {where}
|
|
ORDER BY detected_at::timestamp DESC, id DESC
|
|
LIMIT %s OFFSET %s
|
|
""",
|
|
(*params, limit, offset),
|
|
).fetchall()
|
|
conn.close()
|
|
return {"items": [_with_raw(row) for row in rows], "total": int(total or 0), "limit": limit, "offset": offset, "has_more": offset + len(rows) < int(total or 0)}
|
|
|
|
|
|
def list_onchain_raw_events(limit=50, offset=0, chain="", source="", event_type="", mapping_status="", hours=24):
|
|
init_onchain_tables()
|
|
limit = max(1, min(int(limit or 50), 200))
|
|
offset = max(0, int(offset or 0))
|
|
cutoff = (datetime.now() - timedelta(hours=int(hours or 24))).isoformat()
|
|
clauses = ["detected_at >= %s"]
|
|
params = [cutoff]
|
|
if chain:
|
|
clauses.append("chain=%s")
|
|
params.append(str(chain).lower())
|
|
if source:
|
|
clauses.append("source=%s")
|
|
params.append(source)
|
|
if event_type:
|
|
clauses.append("event_type=%s")
|
|
params.append(event_type)
|
|
if mapping_status:
|
|
clauses.append("mapping_status=%s")
|
|
params.append(mapping_status)
|
|
where = " AND ".join(clauses)
|
|
conn = get_conn()
|
|
total = conn.execute(f"SELECT COUNT(*) FROM onchain_raw_events WHERE {where}", tuple(params)).fetchone()[0]
|
|
rows = conn.execute(
|
|
f"""
|
|
SELECT * FROM onchain_raw_events
|
|
WHERE {where}
|
|
ORDER BY detected_at::timestamp DESC, importance DESC, id DESC
|
|
LIMIT %s OFFSET %s
|
|
""",
|
|
(*params, limit, offset),
|
|
).fetchall()
|
|
conn.close()
|
|
return {
|
|
"items": [_format_raw_event(row) for row in rows],
|
|
"total": int(total or 0),
|
|
"limit": limit,
|
|
"offset": offset,
|
|
"has_more": offset + len(rows) < int(total or 0),
|
|
}
|
|
|
|
|
|
def update_event_status(event_ids, status):
|
|
if not event_ids:
|
|
return 0
|
|
init_onchain_tables()
|
|
conn = get_conn()
|
|
cur = conn.execute(
|
|
"UPDATE onchain_events SET status=%s WHERE id IN (" + ",".join(["%s"] * len(event_ids)) + ")",
|
|
(status, *[int(x) for x in event_ids]),
|
|
)
|
|
conn.commit()
|
|
conn.close()
|
|
return int(cur.rowcount or 0)
|
|
|
|
|
|
def _with_raw(row):
|
|
item = dict(row)
|
|
if "raw_json" in item:
|
|
item["raw"] = _load(item.pop("raw_json"), {})
|
|
return item
|
|
|
|
|
|
def _format_raw_event(row):
|
|
item = _with_raw(row)
|
|
item["event_label"] = raw_event_type_label(item.get("event_type"))
|
|
item["token_short"] = _short_address(item.get("token_address"))
|
|
return item
|
|
|
|
|
|
def _short_address(value):
|
|
value = str(value or "")
|
|
if len(value) <= 14:
|
|
return value
|
|
return value[:6] + "..." + value[-4:]
|