airmicrodrip / holder_tracker.py
josephrw's picture
Upload folder using huggingface_hub
e9d4f6a verified
Raw
History Blame Contribute Delete
14.9 kB
#!/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)