Spaces:
Paused
Paused
| #!/usr/bin/env python3 | |
| """ | |
| 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" | |
| # Configuration | |
| HOLDER_CONFIG = { | |
| "min_holding_amount": 100, # Minimum 100 tokens | |
| "min_holding_period_hours": 24, # Must hold for 24 hours | |
| "max_holders_per_distribution": 1000, # Cap per distribution | |
| "distribution_interval_hours": 6, # Distribute every 6 hours | |
| "blacklist": [], # Blacklisted addresses | |
| } | |
| 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() | |
| # Create holders table | |
| 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 | |
| ) | |
| """) | |
| # Create transfers table | |
| cursor.execute(""" | |
| CREATE TABLE IF NOT EXISTS transfers ( | |
| tx_signature TEXT PRIMARY KEY, | |
| from_address TEXT, | |
| to_address TEXT, | |
| amount INTEGER, | |
| timestamp TIMESTAMP | |
| ) | |
| """) | |
| # Create distributions table | |
| 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 | |
| # Fetch parsed transaction | |
| 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() | |
| # Check if holder exists | |
| cursor.execute("SELECT current_balance FROM holders WHERE address = ?", (address,)) | |
| result = cursor.fetchone() | |
| if result: | |
| # Update existing holder | |
| new_balance = result[0] + amount_change | |
| cursor.execute(""" | |
| UPDATE holders | |
| SET current_balance = ?, last_seen = ? | |
| WHERE address = ? | |
| """, (new_balance, current_time, address)) | |
| # Update totals | |
| 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: | |
| # Create new holder | |
| 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"]) | |
| # Get holders who meet criteria | |
| 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() | |
| # Update eligibility | |
| 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() | |
| # Total holders | |
| cursor.execute("SELECT COUNT(*) FROM holders") | |
| total_holders = cursor.fetchone()[0] | |
| # Eligible holders | |
| cursor.execute("SELECT COUNT(*) FROM holders WHERE eligible = TRUE") | |
| eligible_holders = cursor.fetchone()[0] | |
| # Total balance | |
| cursor.execute("SELECT SUM(current_balance) FROM holders") | |
| total_balance = cursor.fetchone()[0] or 0 | |
| # New holders today | |
| 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) | |
| # Sync from chain | |
| count = tracker.sync_holders_from_chain() | |
| print(f"Synced {count} holders from Solana RPC") | |
| # Check eligibility | |
| eligible = tracker.check_eligibility() | |
| print(f"Eligible holders: {len(eligible)}") | |
| # Print stats | |
| 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) | |