1
This commit is contained in:
parent
2f682cfd0d
commit
a22fb1e775
@ -55,6 +55,8 @@ ALPHAX_NODEREAL_MAX_LOGS_PER_TOKEN=25
|
||||
ALPHAX_NODEREAL_RAW_TRANSFER_ENABLED=1
|
||||
ALPHAX_NODEREAL_RAW_BLOCK_LOOKBACK=1
|
||||
ALPHAX_NODEREAL_RAW_MAX_LOGS_PER_CHAIN=30
|
||||
ALPHAX_NODEREAL_AUTO_MAPPING_ENABLED=1
|
||||
ALPHAX_NODEREAL_AUTO_MAPPING_CONFIDENCE=82
|
||||
ALPHAX_ONCHAIN_CANDIDATE_ENABLED=1
|
||||
ALPHAX_ONCHAIN_CANDIDATE_MIN_SCORE=70
|
||||
ALPHAX_ONCHAIN_CANDIDATE_MIN_CONFIDENCE=70
|
||||
|
||||
@ -84,6 +84,8 @@ def default_onchain_config(default_chains=("ethereum", "bsc")):
|
||||
"nodereal_raw_transfer_enabled": _env_bool("ALPHAX_NODEREAL_RAW_TRANSFER_ENABLED", True),
|
||||
"nodereal_raw_block_lookback": _env_int("ALPHAX_NODEREAL_RAW_BLOCK_LOOKBACK", 1),
|
||||
"nodereal_raw_max_logs_per_chain": _env_int("ALPHAX_NODEREAL_RAW_MAX_LOGS_PER_CHAIN", 30),
|
||||
"nodereal_auto_mapping_enabled": _env_bool("ALPHAX_NODEREAL_AUTO_MAPPING_ENABLED", True),
|
||||
"nodereal_auto_mapping_confidence": _env_int("ALPHAX_NODEREAL_AUTO_MAPPING_CONFIDENCE", 82),
|
||||
"candidate_enabled": _env_bool("ALPHAX_ONCHAIN_CANDIDATE_ENABLED", True),
|
||||
"candidate_min_score": _env_float("ALPHAX_ONCHAIN_CANDIDATE_MIN_SCORE", 70),
|
||||
"candidate_min_confidence": _env_int("ALPHAX_ONCHAIN_CANDIDATE_MIN_CONFIDENCE", 70),
|
||||
|
||||
@ -64,6 +64,10 @@ class NodeRealClient:
|
||||
result = self.call(chain, "eth_getLogs", [log_filter])
|
||||
return result if isinstance(result, list) else []
|
||||
|
||||
def eth_call(self, chain: str, to_address: str, data: str, block: str = "latest") -> str:
|
||||
result = self.call(chain, "eth_call", [{"to": to_address, "data": data}, block])
|
||||
return str(result or "")
|
||||
|
||||
def token_holder_count(self, chain: str, contract_address: str) -> int:
|
||||
return _hex_to_int(self.call(chain, "nr_getTokenHolderCount", [contract_address]))
|
||||
|
||||
@ -87,4 +91,3 @@ def _hex_to_int(value: Any) -> int:
|
||||
return int(text, 16) if text.startswith("0x") else int(text)
|
||||
except Exception:
|
||||
return 0
|
||||
|
||||
|
||||
@ -73,6 +73,9 @@ DEXSCREENER_RAW_ENDPOINTS = (
|
||||
("token_boost_top", "https://api.dexscreener.com/token-boosts/top/v1"),
|
||||
)
|
||||
TRANSFER_TOPIC = "0xddf252ad1be2c89b69c2b068fc378daa952ba7f163c4a11628f55a4df523b3ef"
|
||||
ERC20_SYMBOL_SELECTOR = "0x95d89b41"
|
||||
ERC20_NAME_SELECTOR = "0x06fdde03"
|
||||
ERC20_DECIMALS_SELECTOR = "0x313ce567"
|
||||
|
||||
|
||||
def _env_bool(name, default=False):
|
||||
@ -129,6 +132,8 @@ def get_onchain_params():
|
||||
"nodereal_raw_transfer_enabled": bool(cfg.get("nodereal_raw_transfer_enabled", True)),
|
||||
"nodereal_raw_block_lookback": int(cfg.get("nodereal_raw_block_lookback") or 1),
|
||||
"nodereal_raw_max_logs_per_chain": int(cfg.get("nodereal_raw_max_logs_per_chain") or 30),
|
||||
"nodereal_auto_mapping_enabled": bool(cfg.get("nodereal_auto_mapping_enabled", True)),
|
||||
"nodereal_auto_mapping_confidence": int(cfg.get("nodereal_auto_mapping_confidence") or 82),
|
||||
"candidate_enabled": bool(cfg.get("candidate_enabled", True)),
|
||||
"candidate_min_score": float(cfg.get("candidate_min_score") or 70),
|
||||
"candidate_min_confidence": int(cfg.get("candidate_min_confidence") or 70),
|
||||
@ -795,6 +800,107 @@ def _topic_to_address(topic):
|
||||
return ""
|
||||
|
||||
|
||||
def _decode_abi_string(value):
|
||||
text = str(value or "").strip()
|
||||
if not text or text == "0x":
|
||||
return ""
|
||||
payload = text[2:] if text.startswith("0x") else text
|
||||
try:
|
||||
raw = bytes.fromhex(payload)
|
||||
except Exception:
|
||||
return ""
|
||||
if not raw:
|
||||
return ""
|
||||
try:
|
||||
if len(raw) >= 96:
|
||||
offset = int.from_bytes(raw[:32], "big")
|
||||
if 0 <= offset + 32 <= len(raw):
|
||||
length = int.from_bytes(raw[offset:offset + 32], "big")
|
||||
body = raw[offset + 32:offset + 32 + length]
|
||||
return body.decode("utf-8", errors="ignore").strip("\x00 ").strip()
|
||||
return raw.rstrip(b"\x00").decode("utf-8", errors="ignore").strip()
|
||||
except Exception:
|
||||
return ""
|
||||
|
||||
|
||||
def _clean_erc20_symbol(value):
|
||||
symbol = str(value or "").upper().strip().replace("$", "")
|
||||
symbol = "".join(ch for ch in symbol if ch.isalnum())
|
||||
if symbol.endswith("USDT") and len(symbol) > 4:
|
||||
symbol = symbol[:-4]
|
||||
return symbol[:20]
|
||||
|
||||
|
||||
def _is_auto_mapping_symbol_allowed(base, token_name=""):
|
||||
base = _clean_erc20_symbol(base)
|
||||
if not base:
|
||||
return False
|
||||
symbol = f"{base}/USDT"
|
||||
if not _tradable_symbol(symbol):
|
||||
return False
|
||||
if base in NON_TARGET_NATIVE_BASES:
|
||||
return False
|
||||
text = f"{base} {token_name or ''}".lower()
|
||||
if any(marker in text for marker in BRIDGED_TOKEN_MARKERS):
|
||||
return False
|
||||
return True
|
||||
|
||||
|
||||
def _read_erc20_metadata(client, chain, contract):
|
||||
metadata = {"symbol": "", "name": "", "decimals": 18}
|
||||
try:
|
||||
metadata["symbol"] = _decode_abi_string(client.eth_call(chain, contract, ERC20_SYMBOL_SELECTOR))
|
||||
except Exception:
|
||||
metadata["symbol"] = ""
|
||||
try:
|
||||
metadata["name"] = _decode_abi_string(client.eth_call(chain, contract, ERC20_NAME_SELECTOR))
|
||||
except Exception:
|
||||
metadata["name"] = ""
|
||||
try:
|
||||
decimals = _hex_to_int(client.eth_call(chain, contract, ERC20_DECIMALS_SELECTOR))
|
||||
if 0 <= decimals <= 36:
|
||||
metadata["decimals"] = decimals
|
||||
except Exception:
|
||||
pass
|
||||
metadata["symbol"] = _clean_erc20_symbol(metadata.get("symbol"))
|
||||
return metadata
|
||||
|
||||
|
||||
def _auto_map_nodereal_contract(client, chain, contract, cfg=None):
|
||||
cfg = cfg or get_onchain_params()
|
||||
if not cfg.get("nodereal_auto_mapping_enabled", True):
|
||||
return None
|
||||
existing = find_mapping_by_contract(chain, contract)
|
||||
if existing:
|
||||
return existing
|
||||
metadata = _read_erc20_metadata(client, chain, contract)
|
||||
base = metadata.get("symbol") or ""
|
||||
if not _is_auto_mapping_symbol_allowed(base, metadata.get("name")):
|
||||
return None
|
||||
symbol = normalize_symbol(base)
|
||||
confidence = max(1, min(95, int(cfg.get("nodereal_auto_mapping_confidence") or 82)))
|
||||
mapping_id = onchain_db.upsert_token_mapping(
|
||||
symbol=symbol,
|
||||
chain=chain,
|
||||
contract_address=contract,
|
||||
source="nodereal_erc20_metadata",
|
||||
confidence=confidence,
|
||||
raw=metadata,
|
||||
is_active=True,
|
||||
)
|
||||
if not mapping_id:
|
||||
return None
|
||||
return {
|
||||
"id": mapping_id,
|
||||
"symbol": symbol,
|
||||
"chain": str(chain or "").lower(),
|
||||
"contract_address": contract,
|
||||
"source": "nodereal_erc20_metadata",
|
||||
"confidence": confidence,
|
||||
"raw_json": json.dumps(metadata, ensure_ascii=False),
|
||||
}
|
||||
|
||||
|
||||
def _nodereal_client(cfg=None):
|
||||
cfg = cfg or get_onchain_params()
|
||||
return NodeRealClient(
|
||||
@ -880,6 +986,30 @@ def _raw_event_from_nodereal_transfer(log, chain):
|
||||
}
|
||||
|
||||
|
||||
def _apply_raw_event_mapping(raw_event, client=None, cfg=None):
|
||||
item = dict(raw_event or {})
|
||||
chain = str(item.get("chain") or "").lower()
|
||||
contract = str(item.get("token_address") or "").strip()
|
||||
if not chain or not contract:
|
||||
return item
|
||||
mapping = find_mapping_by_contract(chain, contract)
|
||||
if not mapping and client:
|
||||
mapping = _auto_map_nodereal_contract(client, chain, contract, cfg=cfg)
|
||||
if mapping:
|
||||
item["mapped_symbol"] = normalize_symbol(mapping.get("symbol"))
|
||||
item["mapping_status"] = "mapped"
|
||||
item["symbol_guess"] = item.get("symbol_guess") or item["mapped_symbol"].split("/")[0]
|
||||
item["raw"] = {
|
||||
**(item.get("raw") or {}),
|
||||
"mapping": {
|
||||
"symbol": item["mapped_symbol"],
|
||||
"source": mapping.get("source") or "",
|
||||
"confidence": mapping.get("confidence") or 0,
|
||||
},
|
||||
}
|
||||
return item
|
||||
|
||||
|
||||
def _metric_from_nodereal_holder_count(holder_count, mapping):
|
||||
symbol = normalize_symbol(mapping.get("symbol"))
|
||||
chain = str(mapping.get("chain") or "").lower()
|
||||
@ -1039,7 +1169,7 @@ def fetch_nodereal_raw_events(client=None, cfg=None, limit=60):
|
||||
continue
|
||||
item = _raw_event_from_nodereal_transfer(log, chain)
|
||||
if item:
|
||||
raw_items.append(item)
|
||||
raw_items.append(_apply_raw_event_mapping(item, client=client, cfg=cfg))
|
||||
raw_items.sort(key=lambda item: item.get("amount") or 0, reverse=True)
|
||||
for item in raw_items[:per_chain]:
|
||||
if insert_onchain_raw_event(item):
|
||||
|
||||
@ -388,8 +388,12 @@ def test_nodereal_records_raw_events_without_strategy_mappings(monkeypatch, tmp_
|
||||
def block_number(self, chain):
|
||||
return 1000
|
||||
|
||||
def token_holder_count(self, chain, contract):
|
||||
return 0
|
||||
|
||||
def get_logs(self, chain, log_filter):
|
||||
assert "address" not in log_filter
|
||||
if "address" in log_filter:
|
||||
return []
|
||||
return [
|
||||
{
|
||||
"address": "0xabc",
|
||||
@ -418,6 +422,65 @@ def test_nodereal_records_raw_events_without_strategy_mappings(monkeypatch, tmp_
|
||||
assert raw["items"][0]["event_type"] == "evm_transfer"
|
||||
|
||||
|
||||
def test_nodereal_auto_maps_raw_event_from_erc20_metadata(monkeypatch, tmp_path):
|
||||
_temp_db(monkeypatch, tmp_path)
|
||||
monkeypatch.setenv("ALPHAX_NODEREAL_API_KEY", "test-key")
|
||||
monkeypatch.setenv("ALPHAX_NODEREAL_CHAINS", "ethereum")
|
||||
|
||||
def abi_string(value):
|
||||
body = value.encode()
|
||||
padded = body + (b"\x00" * ((32 - len(body) % 32) % 32))
|
||||
return "0x" + (32).to_bytes(32, "big").hex() + len(body).to_bytes(32, "big").hex() + padded.hex()
|
||||
|
||||
class MetadataNodeRealClient:
|
||||
def supports_chain(self, chain):
|
||||
return chain == "ethereum"
|
||||
|
||||
def block_number(self, chain):
|
||||
return 1000
|
||||
|
||||
def token_holder_count(self, chain, contract):
|
||||
return 0
|
||||
|
||||
def get_logs(self, chain, log_filter):
|
||||
if "address" in log_filter:
|
||||
return []
|
||||
return [
|
||||
{
|
||||
"address": "0xstorj",
|
||||
"transactionHash": "0xrawtx",
|
||||
"data": hex(1000 * 10**8),
|
||||
"topics": [
|
||||
onchain_monitor.TRANSFER_TOPIC,
|
||||
"0x0000000000000000000000001111111111111111111111111111111111111111",
|
||||
"0x0000000000000000000000002222222222222222222222222222222222222222",
|
||||
],
|
||||
}
|
||||
]
|
||||
|
||||
def eth_call(self, chain, to_address, data, block="latest"):
|
||||
if data == onchain_monitor.ERC20_SYMBOL_SELECTOR:
|
||||
return abi_string("STORJ")
|
||||
if data == onchain_monitor.ERC20_NAME_SELECTOR:
|
||||
return abi_string("Storj")
|
||||
if data == onchain_monitor.ERC20_DECIMALS_SELECTOR:
|
||||
return hex(8)
|
||||
return "0x"
|
||||
|
||||
monkeypatch.setattr(onchain_monitor, "_nodereal_client", lambda cfg=None: MetadataNodeRealClient())
|
||||
|
||||
result = onchain_monitor.fetch_nodereal_events(limit=10)
|
||||
|
||||
assert result["errors"] == []
|
||||
assert len(result["raw_events"]) == 1
|
||||
raw = onchain_db.list_onchain_raw_events(hours=50000)
|
||||
assert raw["items"][0]["mapping_status"] == "mapped"
|
||||
assert raw["items"][0]["mapped_symbol"] == "STORJ/USDT"
|
||||
mappings = onchain_db.get_token_mappings("STORJ/USDT")
|
||||
assert len(mappings) == 1
|
||||
assert mappings[0]["source"] == "nodereal_erc20_metadata"
|
||||
|
||||
|
||||
def test_nodereal_seeds_configured_token_mappings_from_env(monkeypatch, tmp_path):
|
||||
_temp_db(monkeypatch, tmp_path)
|
||||
monkeypatch.setenv("ALPHAX_NODEREAL_API_KEY", "test-key")
|
||||
|
||||
Loading…
Reference in New Issue
Block a user