| |
| """ |
| AirMicroDrip Holder Tracker |
| Fetches real token holder data from Solana RPC |
| No mocks - real HTTP API calls only |
| """ |
|
|
| import os |
| import json |
| import sqlite3 |
| import requests |
| import logging |
| from typing import Dict, List, Optional |
| from datetime import datetime, timedelta |
|
|
| logger = logging.getLogger(__name__) |
|
|
| SOLANA_RPC_URL = os.environ.get("SOLANA_RPC_URL", "https://api.mainnet-beta.solana.com") |
| TOKEN_PROGRAM_ID = "TokenkegQfeZyiNwAJbNbGKPFXCWuBvf9Ss623VQ5DA" |
|
|
| |
| HOLDER_CONFIG = { |
| "min_holding_amount": 100, |
| "min_holding_period_hours": 24, |
| "max_holders_per_distribution": 1000, |
| "distribution_interval_hours": 6, |
| "blacklist": [], |
| } |
|
|
|
|
| class HolderTracker: |
| """Tracks token holders using real Solana RPC data""" |
| |
| def __init__( |
| self, |
| token_mint: str, |
| db_path: str = "holder_registry.db", |
| ): |
| self.token_mint = token_mint |
| self.db_path = db_path |
| self._init_database() |
| |
| def _init_database(self): |
| """Initialize SQLite database for holder registry""" |
| conn = sqlite3.connect(self.db_path) |
| cursor = conn.cursor() |
| |
| |
| cursor.execute(""" |
| CREATE TABLE IF NOT EXISTS holders ( |
| address TEXT PRIMARY KEY, |
| first_seen TIMESTAMP, |
| last_seen TIMESTAMP, |
| current_balance INTEGER, |
| total_received INTEGER, |
| total_sent INTEGER, |
| eligible BOOLEAN DEFAULT FALSE, |
| eligibility_timestamp TIMESTAMP, |
| drippage_received INTEGER DEFAULT 0 |
| ) |
| """) |
| |
| |
| cursor.execute(""" |
| CREATE TABLE IF NOT EXISTS transfers ( |
| tx_signature TEXT PRIMARY KEY, |
| from_address TEXT, |
| to_address TEXT, |
| amount INTEGER, |
| timestamp TIMESTAMP |
| ) |
| """) |
| |
| |
| cursor.execute(""" |
| CREATE TABLE IF NOT EXISTS distributions ( |
| distribution_id TEXT PRIMARY KEY, |
| timestamp TIMESTAMP, |
| total_amount INTEGER, |
| eligible_holders INTEGER, |
| avg_amount INTEGER |
| ) |
| """) |
| |
| conn.commit() |
| conn.close() |
| |
| def fetch_top_holders_from_rpc(self, limit: int = 20) -> List[Dict]: |
| """Fetch top token holders from Solana RPC""" |
| try: |
| payload = { |
| "jsonrpc": "2.0", |
| "id": 1, |
| "method": "getTokenLargestAccounts", |
| "params": [self.token_mint], |
| } |
| r = requests.post(SOLANA_RPC_URL, json=payload, timeout=10) |
| if r.status_code == 200: |
| result = r.json().get("result", {}).get("value", []) |
| holders = [] |
| for item in result[:limit]: |
| holders.append({ |
| "address": item.get("address"), |
| "balance": int(item.get("amount", 0)), |
| "ui_amount": item.get("uiAmount", 0), |
| }) |
| return holders |
| except Exception as e: |
| logger.warning("RPC error fetching holders: %s", e) |
| return [] |
| |
| def fetch_recent_transfers_from_rpc(self, limit: int = 10) -> List[Dict]: |
| """Fetch recent transfers for token mint via RPC""" |
| try: |
| payload = { |
| "jsonrpc": "2.0", |
| "id": 1, |
| "method": "getSignaturesForAddress", |
| "params": [self.token_mint, {"limit": limit}], |
| } |
| r = requests.post(SOLANA_RPC_URL, json=payload, timeout=10) |
| if r.status_code == 200: |
| sigs = r.json().get("result", []) |
| transfers = [] |
| for sig_info in sigs: |
| sig = sig_info.get("signature") |
| if not sig: |
| continue |
| |
| tx_payload = { |
| "jsonrpc": "2.0", |
| "id": 1, |
| "method": "getTransaction", |
| "params": [sig, {"encoding": "jsonParsed", "maxSupportedTransactionVersion": 0}], |
| } |
| tx_r = requests.post(SOLANA_RPC_URL, json=tx_payload, timeout=10) |
| if tx_r.status_code == 200: |
| tx = tx_r.json().get("result", {}) |
| meta = tx.get("meta", {}) |
| pre_balances = meta.get("preTokenBalances", []) |
| post_balances = meta.get("postTokenBalances", []) |
| if pre_balances and post_balances: |
| transfers.append({ |
| "signature": sig, |
| "slot": tx.get("slot"), |
| "pre_balances": pre_balances, |
| "post_balances": post_balances, |
| }) |
| return transfers |
| except Exception as e: |
| logger.warning("RPC error fetching transfers: %s", e) |
| return [] |
| |
| def sync_holders_from_chain(self): |
| """Sync holder data from real Solana RPC into SQLite""" |
| holders = self.fetch_top_holders_from_rpc() |
| current_time = datetime.utcnow().isoformat() |
| |
| conn = sqlite3.connect(self.db_path) |
| cursor = conn.cursor() |
| |
| for h in holders: |
| addr = h["address"] |
| balance = h["balance"] |
| |
| cursor.execute("SELECT address FROM holders WHERE address = ?", (addr,)) |
| if cursor.fetchone(): |
| cursor.execute( |
| "UPDATE holders SET current_balance = ?, last_seen = ? WHERE address = ?", |
| (balance, current_time, addr) |
| ) |
| else: |
| cursor.execute(""" |
| INSERT INTO holders (address, first_seen, last_seen, current_balance, total_received, total_sent) |
| VALUES (?, ?, ?, ?, ?, ?) |
| """, (addr, current_time, current_time, balance, balance, 0)) |
| print(f"New holder synced from chain: {addr}") |
| |
| conn.commit() |
| conn.close() |
| return len(holders) |
| |
| def _update_holder(self, address: str, amount_change: int): |
| """Update holder balance""" |
| conn = sqlite3.connect(self.db_path) |
| cursor = conn.cursor() |
| |
| current_time = datetime.utcnow().isoformat() |
| |
| |
| cursor.execute("SELECT current_balance FROM holders WHERE address = ?", (address,)) |
| result = cursor.fetchone() |
| |
| if result: |
| |
| new_balance = result[0] + amount_change |
| cursor.execute(""" |
| UPDATE holders |
| SET current_balance = ?, last_seen = ? |
| WHERE address = ? |
| """, (new_balance, current_time, address)) |
| |
| |
| if amount_change > 0: |
| cursor.execute(""" |
| UPDATE holders |
| SET total_received = total_received + ? |
| WHERE address = ? |
| """, (amount_change, address)) |
| else: |
| cursor.execute(""" |
| UPDATE holders |
| SET total_sent = total_sent + ? |
| WHERE address = ? |
| """, (-amount_change, address)) |
| else: |
| |
| cursor.execute(""" |
| INSERT INTO holders |
| (address, first_seen, last_seen, current_balance, total_received, total_sent) |
| VALUES (?, ?, ?, ?, ?, ?) |
| """, (address, current_time, current_time, amount_change, |
| max(0, amount_change), max(0, -amount_change))) |
| |
| conn.commit() |
| conn.close() |
| |
| def _is_new_holder(self, address: str) -> bool: |
| """Check if address is a new holder""" |
| conn = sqlite3.connect(self.db_path) |
| cursor = conn.cursor() |
| |
| cursor.execute("SELECT first_seen FROM holders WHERE address = ?", (address,)) |
| result = cursor.fetchone() |
| |
| conn.close() |
| |
| return result is None |
| |
| def _register_new_holder(self, address: str, amount: int): |
| """Register new holder""" |
| conn = sqlite3.connect(self.db_path) |
| cursor = conn.cursor() |
| |
| current_time = datetime.utcnow().isoformat() |
| |
| cursor.execute(""" |
| UPDATE holders |
| SET first_seen = ?, last_seen = ? |
| WHERE address = ? |
| """, (current_time, current_time, address)) |
| |
| conn.commit() |
| conn.close() |
| |
| def _log_transfer(self, signature: str, from_addr: str, to_addr: str, amount: int): |
| """Log transfer to database""" |
| conn = sqlite3.connect(self.db_path) |
| cursor = conn.cursor() |
| |
| current_time = datetime.utcnow().isoformat() |
| |
| cursor.execute(""" |
| INSERT OR IGNORE INTO transfers |
| (tx_signature, from_address, to_address, amount, timestamp) |
| VALUES (?, ?, ?, ?, ?) |
| """, (signature, from_addr, to_addr, amount, current_time)) |
| |
| conn.commit() |
| conn.close() |
| |
| def check_eligibility(self): |
| """Check which holders are eligible for drippage""" |
| conn = sqlite3.connect(self.db_path) |
| cursor = conn.cursor() |
| |
| current_time = datetime.utcnow() |
| min_time = current_time - timedelta(hours=HOLDER_CONFIG["min_holding_period_hours"]) |
| |
| |
| cursor.execute(""" |
| SELECT address, current_balance, first_seen |
| FROM holders |
| WHERE current_balance >= ? |
| AND first_seen <= ? |
| AND address NOT IN (SELECT address FROM blacklist) |
| ORDER BY current_balance DESC |
| LIMIT ? |
| """, ( |
| HOLDER_CONFIG["min_holding_amount"], |
| min_time.isoformat(), |
| HOLDER_CONFIG["max_holders_per_distribution"], |
| )) |
| |
| holders = cursor.fetchall() |
| |
| |
| for address, balance, first_seen in holders: |
| cursor.execute(""" |
| UPDATE holders |
| SET eligible = TRUE, eligibility_timestamp = ? |
| WHERE address = ? |
| """, (current_time.isoformat(), address)) |
| |
| conn.commit() |
| conn.close() |
| |
| return [ |
| { |
| "address": h[0], |
| "balance": h[1], |
| "first_seen": h[2], |
| } |
| for h in holders |
| ] |
| |
| def get_eligible_holders(self) -> List[Dict]: |
| """Get all currently eligible holders""" |
| conn = sqlite3.connect(self.db_path) |
| cursor = conn.cursor() |
| |
| cursor.execute(""" |
| SELECT address, current_balance, first_seen, eligibility_timestamp |
| FROM holders |
| WHERE eligible = TRUE |
| ORDER BY current_balance DESC |
| """) |
| |
| holders = cursor.fetchall() |
| conn.close() |
| |
| return [ |
| { |
| "address": h[0], |
| "balance": h[1], |
| "first_seen": h[2], |
| "holding_hours": (datetime.utcnow() - datetime.fromisoformat(h[2])).total_seconds() / 3600, |
| } |
| for h in holders |
| ] |
| |
| def get_holder_stats(self) -> Dict: |
| """Get holder statistics""" |
| conn = sqlite3.connect(self.db_path) |
| cursor = conn.cursor() |
| |
| |
| cursor.execute("SELECT COUNT(*) FROM holders") |
| total_holders = cursor.fetchone()[0] |
| |
| |
| cursor.execute("SELECT COUNT(*) FROM holders WHERE eligible = TRUE") |
| eligible_holders = cursor.fetchone()[0] |
| |
| |
| cursor.execute("SELECT SUM(current_balance) FROM holders") |
| total_balance = cursor.fetchone()[0] or 0 |
| |
| |
| today = datetime.utcnow().date() |
| cursor.execute(""" |
| SELECT COUNT(*) FROM holders |
| WHERE DATE(first_seen) = ? |
| """, (today.isoformat(),)) |
| new_holders_today = cursor.fetchone()[0] |
| |
| conn.close() |
| |
| return { |
| "total_holders": total_holders, |
| "eligible_holders": eligible_holders, |
| "total_balance": total_balance, |
| "new_holders_today": new_holders_today, |
| "eligibility_rate": eligible_holders / total_holders if total_holders > 0 else 0, |
| } |
| |
| def add_to_blacklist(self, address: str): |
| """Add address to blacklist""" |
| if address not in HOLDER_CONFIG["blacklist"]: |
| HOLDER_CONFIG["blacklist"].append(address) |
| print(f"Added {address} to blacklist") |
| |
| def remove_from_blacklist(self, address: str): |
| """Remove address from blacklist""" |
| if address in HOLDER_CONFIG["blacklist"]: |
| HOLDER_CONFIG["blacklist"].remove(address) |
| print(f"Removed {address} from blacklist") |
|
|
|
|
| def start_holder_sync(token_mint: str): |
| """Sync holders from chain and print stats""" |
| tracker = HolderTracker(token_mint) |
| |
| |
| count = tracker.sync_holders_from_chain() |
| print(f"Synced {count} holders from Solana RPC") |
| |
| |
| eligible = tracker.check_eligibility() |
| print(f"Eligible holders: {len(eligible)}") |
| |
| |
| stats = tracker.get_holder_stats() |
| print("\n" + "="*50) |
| print("Holder Statistics") |
| print("="*50) |
| print(f"Total Holders: {stats['total_holders']}") |
| print(f"Eligible Holders: {stats['eligible_holders']}") |
| print(f"Total Balance: {stats['total_balance']:,}") |
| print(f"New Holders Today: {stats['new_holders_today']}") |
| print(f"Eligibility Rate: {stats['eligibility_rate']:.2%}") |
| return stats |
|
|
|
|
| if __name__ == "__main__": |
| import sys |
| |
| if len(sys.argv) < 2: |
| print("Usage: python holder_tracker.py <token_mint>") |
| print("Example: python holder_tracker.py EPjFWdd5AufqSSqeM2qN1xzybapC8G4wEGGkZwyTDt1v") |
| sys.exit(1) |
| |
| token_mint = sys.argv[1] |
| start_holder_sync(token_mint) |
|
|