merge: chore/cleanup-remove-bloat-and-secrets into main
This commit is contained in:
commit
bde2f3a97d
1173 changed files with 437609 additions and 0 deletions
234
app/domain/labels/sources/mbal_source.py
Normal file
234
app/domain/labels/sources/mbal_source.py
Normal file
|
|
@ -0,0 +1,234 @@
|
|||
"""
|
||||
MBAL (Multi-blockchain Anti-Laundering) Label Integration
|
||||
|
||||
Integrates with MBAL's 10M+ wallet labels via the MBAL API or direct ClickHouse access.
|
||||
MBAL provides comprehensive blockchain attribution, wallet clustering, and risk scoring
|
||||
across Bitcoin, Ethereum, and 15+ major chains.
|
||||
|
||||
API Documentation: https://mbal.io/api
|
||||
Rate Limits: Free tier - 100 queries/day, Pro - 1000/day, Enterprise - Unlimited
|
||||
"""
|
||||
|
||||
import asyncio
|
||||
import logging
|
||||
from datetime import datetime
|
||||
from typing import List, Optional
|
||||
from urllib.parse import urlencode
|
||||
|
||||
import httpx
|
||||
|
||||
from app.domain.labels.models import Label, LabelSource
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class MBALService:
|
||||
"""MBAL API Integration for multi-blockchain wallet labels"""
|
||||
|
||||
def __init__(self, api_key: Optional[str] = None):
|
||||
"""
|
||||
Initialize MBAL service.
|
||||
|
||||
Args:
|
||||
api_key: MBAL API key. If None, uses free tier with limitations.
|
||||
"""
|
||||
self.base_url = "https://api.mbal.io/v1"
|
||||
self.api_key = api_key
|
||||
self.client = httpx.AsyncClient(
|
||||
timeout=httpx.Timeout(30.0),
|
||||
limits=httpx.Limits(max_connections=10, max_keepalive_connections=5),
|
||||
)
|
||||
|
||||
# Cache for rate limiting
|
||||
self.last_request_time = 0
|
||||
self.request_count = 0
|
||||
self.reset_time = 0 # Unix timestamp when quota resets
|
||||
|
||||
async def close(self):
|
||||
"""Close the HTTP client."""
|
||||
await self.client.aclose()
|
||||
|
||||
async def get_labels(self, address: str, chain: str) -> List[Label]:
|
||||
"""
|
||||
Retrieve labels for a blockchain address from MBAL.
|
||||
|
||||
Args:
|
||||
address: The blockchain address to label
|
||||
chain: The blockchain (ethereum, bitcoin, tron, etc.)
|
||||
|
||||
Returns:
|
||||
List of labels with metadata
|
||||
"""
|
||||
try:
|
||||
# MBAL requires different endpoints per chain
|
||||
labels = []
|
||||
|
||||
# Prepare query payload
|
||||
params = {
|
||||
"address": address,
|
||||
"chain": chain.lower(),
|
||||
}
|
||||
|
||||
# Rate limiting - max 100 requests per day for free
|
||||
await self._rate_limit()
|
||||
|
||||
headers = {}
|
||||
if self.api_key:
|
||||
headers["Authorization"] = f"Bearer {self.api_key}"
|
||||
|
||||
# Query MBAL's address endpoint
|
||||
response = await self.client.get(
|
||||
f"{self.base_url}/address",
|
||||
params=params,
|
||||
headers=headers
|
||||
)
|
||||
response.raise_for_status()
|
||||
|
||||
data = response.json()
|
||||
|
||||
# Process the result according to MBAL's schema
|
||||
if "entities" in data:
|
||||
for entity in data["entities"]:
|
||||
label_value = entity.get("category", "unknown")
|
||||
entity_type = entity.get("type", "address")
|
||||
confidence = entity.get("confidence", 0.8) # MBAL typically confident
|
||||
|
||||
# Map MBAL categories to our standardized label types
|
||||
label_type = self._map_category_to_type(label_value)
|
||||
|
||||
labels.append(Label(
|
||||
address=address,
|
||||
chain=chain,
|
||||
label_type=label_type,
|
||||
label_value=label_value,
|
||||
source=LabelSource.MBAL, # MBAL source
|
||||
verified_at=datetime.now(),
|
||||
confidence=confidence,
|
||||
attributes={
|
||||
"mbal_entity_id": entity.get("entityId"),
|
||||
"mbal_tags": entity.get("tags", []),
|
||||
"mbal_risk_score": entity.get("riskScore", 0),
|
||||
"mbal_cluster_id": entity.get("clusterId"),
|
||||
}
|
||||
))
|
||||
|
||||
if "behaviors" in data and len(labels) == 0:
|
||||
# If no main entities, fall back to behavioral labels
|
||||
for behavior in data["behaviors"]:
|
||||
label_value = behavior["type"]
|
||||
label_type = self._map_behavior_to_type(label_value)
|
||||
confidence = behavior.get("confidence", 0.7)
|
||||
|
||||
labels.append(Label(
|
||||
address=address,
|
||||
chain=chain,
|
||||
label_type=label_type,
|
||||
label_value=label_value,
|
||||
source=LabelSource.MBAL, # MBAL API source
|
||||
verified_at=datetime.now(),
|
||||
confidence=confidence,
|
||||
attributes={
|
||||
"behavior_description": behavior.get("description"),
|
||||
"behavior_risk_level": behavior.get("riskLevel", "medium"),
|
||||
}
|
||||
))
|
||||
|
||||
return labels
|
||||
|
||||
except httpx.HTTPStatusError as e:
|
||||
if e.response.status_code == 429:
|
||||
logger.warning(f"MBAL rate limit exceeded: {e}")
|
||||
return []
|
||||
logger.error(f"MBAL API error: {e} for {chain}:{address}")
|
||||
return []
|
||||
except Exception as e:
|
||||
logger.error(f"Unexpected error querying MBAL: {e}")
|
||||
return []
|
||||
|
||||
def _map_category_to_type(self, category: str) -> str:
|
||||
"""Map MBAL category strings to our standard label type strings."""
|
||||
category_lower = category.lower()
|
||||
|
||||
# Exchange mappings
|
||||
if any(exchange in category_lower for exchange in ["binance", "coinbase", "kraken", "kucoin",
|
||||
"huobi", "gate.io", "okx", "bybit", "gemini"]):
|
||||
return "exchange"
|
||||
|
||||
# Service mappings
|
||||
if any(service in category_lower for service in ["miner", "validator", "staking", "liquidity",
|
||||
"amm", "dex", "bridge", "cex", "dexe"]):
|
||||
return "service"
|
||||
|
||||
# Actor mappings
|
||||
if any(actor in category_lower for actor in ["blacklist", "scammer", "hacker", "tornado",
|
||||
"mixer", "launderer", "crime", "malicious"]):
|
||||
return "actor"
|
||||
|
||||
# Default to entity for unknown but potentially identifiable categories
|
||||
return "actor" if "blacklist" in category_lower else "entity"
|
||||
|
||||
def _map_behavior_to_type(self, behavior: str) -> str:
|
||||
"""Map MBAL behaviors to our standard label type strings."""
|
||||
behavior_lower = behavior.lower()
|
||||
|
||||
if any(risky in behavior_lower for risky in ["malware", "ransom", "exploit", "theft",
|
||||
"scam", "hacking", "malicious"]):
|
||||
return "actor"
|
||||
|
||||
if any(behavior in behavior_lower for behavior in ["mining", "block generation", "transaction fee", "gas"]):
|
||||
return "service"
|
||||
|
||||
return "entity" # Default
|
||||
|
||||
async def _rate_limit(self):
|
||||
"""Implement simple rate limiting based on MBAL tier."""
|
||||
now = asyncio.get_event_loop().time()
|
||||
|
||||
# Check daily quota if using free tier
|
||||
if not self.api_key and self.request_count >= 100:
|
||||
# Free tier: 100 queries/day
|
||||
# Simple approach: wait until reset (24 hours from first query)
|
||||
if now < self.reset_time:
|
||||
sleep_time = self.reset_time - now
|
||||
logger.info(f"Waiting for MBAL daily quota reset: {sleep_time:.1f}s")
|
||||
await asyncio.sleep(sleep_time)
|
||||
|
||||
# Ensure minimum delay between requests
|
||||
time_since_last = now - self.last_request_time
|
||||
min_delay = 1.0 # At least 1s between requests
|
||||
if time_since_last < min_delay:
|
||||
await asyncio.sleep(min_delay - time_since_last)
|
||||
|
||||
# Update counters
|
||||
self.last_request_time = now
|
||||
if now >= self.reset_time:
|
||||
self.request_count = 0
|
||||
self.reset_time = now + 86400 # Reset in 24 hours
|
||||
self.request_count += 1
|
||||
|
||||
async def health_check(self) -> bool:
|
||||
"""
|
||||
Check if MBAL service is accessible.
|
||||
|
||||
Returns:
|
||||
True if MBAL API responds successfully
|
||||
"""
|
||||
try:
|
||||
response = await self.client.get(f"{self.base_url}/health")
|
||||
return response.status_code == 200
|
||||
except:
|
||||
return False
|
||||
|
||||
|
||||
# Global instance
|
||||
_mbalsy_instance: Optional[MBALService] = None
|
||||
|
||||
|
||||
def get_mbalsy_service() -> MBALService:
|
||||
"""Get the global MBAL service instance."""
|
||||
global _mbalsy_instance
|
||||
if _mbalsy_instance is None:
|
||||
import os
|
||||
api_key = os.getenv("MBAL_API_KEY", None)
|
||||
_mbalsy_instance = MBALService(api_key=api_key)
|
||||
return _mbalsy_instance
|
||||
Loading…
Add table
Add a link
Reference in a new issue