""" Darkroom Airdrop Engine ======================= Advanced token distribution system with: • Snapshot-based airdrops (holdings of previous token) • Team/development allocation (configurable % of supply) • Anti-sniper protection (blacklist, tx limits, trading delays) • Vesting schedules for team tokens • Multi-chain support (EVM, Solana, TRON) • Batch distribution for gas efficiency All operations are admin-only and stay in /root/tools/ - never committed to git. Usage: POST /api/v1/admin/tokens/airdrop/snapshot - Create snapshot from existing token POST /api/v1/admin/tokens/airdrop/execute - Execute airdrop distribution POST /api/v1/admin/tokens/airdrop/team - Allocate team/dev tokens POST /api/v1/admin/tokens/airdrop/vesting - Set up vesting for team GET /api/v1/admin/tokens/airdrop/{id}/status - Check airdrop status POST /api/v1/admin/tokens/airdrop/antisniper - Enable anti-sniper protection """ from __future__ import annotations import json import logging import os import time from dataclasses import asdict, dataclass, field from datetime import datetime from typing import Any from app.token_deployer import TokenDeployerFactory, TokenDeployment, get_storage logger = logging.getLogger("darkroom_airdrop") # ── Data Models ─────────────────────────────────────────────── @dataclass class AirdropRecipient: """Single recipient in an airdrop.""" address: str amount: str reason: str = "" # e.g., "holder_of_CRM_v1", "team_allocation", "marketing" claimed: bool = False claim_tx: str = "" claimed_at: str | None = None @dataclass class AirdropSnapshot: """Snapshot of token holders at a specific block/time.""" snapshot_id: str source_token: str source_chain: str block_number: int | None = None timestamp: str = field(default_factory=lambda: datetime.utcnow().isoformat()) holders: list[AirdropRecipient] = field(default_factory=list) total_holders: int = 0 total_supply_snapshotted: str = "0" excluded_addresses: list[str] = field(default_factory=list) min_holdings: str = "0" metadata: dict[str, Any] = field(default_factory=dict) @dataclass class AirdropCampaign: """Full airdrop campaign with distribution rules.""" campaign_id: str deployment_id: str snapshot_id: str chain: str status: str = "pending" # pending, active, paused, completed, cancelled distribution_type: str = "snapshot" # snapshot, manual, team, marketing recipients: list[AirdropRecipient] = field(default_factory=list) total_amount: str = "0" distributed_amount: str = "0" remaining_amount: str = "0" team_allocation_percent: float = 0.0 team_vesting_months: int = 0 team_cliff_months: int = 0 anti_sniper_enabled: bool = True trading_delay_blocks: int = 0 max_wallet_percent: float = 0.0 max_tx_percent: float = 0.0 blacklist_preloaded: list[str] = field(default_factory=list) created_at: str = field(default_factory=lambda: datetime.utcnow().isoformat()) started_at: str | None = None completed_at: str | None = None tx_hashes: list[str] = field(default_factory=list) metadata: dict[str, Any] = field(default_factory=dict) def to_dict(self) -> dict: d = asdict(self) return d @dataclass class VestingSchedule: """Vesting schedule for team/dev tokens.""" schedule_id: str deployment_id: str beneficiary: str total_amount: str claimed_amount: str = "0" start_date: str = "" cliff_months: int = 0 vesting_months: int = 0 monthly_release: str = "0" status: str = "active" # active, completed, revoked tx_hashes: list[str] = field(default_factory=list) # ── Anti-Sniper Protection ──────────────────────────────────── class AntiSniperProtection: """ Protect token launches from snipers and bots. Features: 1. Pre-launch blacklist (known bot addresses) 2. Trading delay (blocks after launch before trading) 3. Max wallet / tx limits (prevent accumulation) 4. Dynamic blacklist (detect and ban sandwich bots) 5. Whitelist-only mode (initially) """ # Known bot/sniper patterns (addresses and creation patterns) KNOWN_BOT_PATTERNS = [ # noqa: RUF012 "0x0000000000000000000000000000000000000000", # Burn address ] @classmethod async def apply_protection( cls, deployer: Any, contract_address: str, deployment: TokenDeployment, blacklist_addresses: list[str] | None = None, trading_delay_blocks: int = 0, max_wallet_percent: float = 0.0, max_tx_percent: float = 0.0, ) -> dict[str, Any]: """Apply full anti-sniper protection suite to a token.""" results = { "blacklist_applied": 0, "trading_delayed": False, "max_wallet_set": False, "max_tx_set": False, "tx_hashes": [], } # 1. Blacklist known bots + provided addresses all_blacklist = set(cls.KNOWN_BOT_PATTERNS) if blacklist_addresses: all_blacklist.update(blacklist_addresses) for addr in all_blacklist: try: tx = await deployer.blacklist_add(contract_address, addr) results["tx_hashes"].append(tx) results["blacklist_applied"] += 1 logger.info(f"Anti-sniper: blacklisted {addr}") except Exception as e: logger.warning(f"Failed to blacklist {addr}: {e}") # 2. Disable trading initially (if supported) try: tx = await deployer.set_trading_enabled(contract_address, False) results["tx_hashes"].append(tx) results["trading_delayed"] = True logger.info(f"Anti-sniper: trading disabled, will enable after {trading_delay_blocks} blocks") except Exception as e: logger.warning(f"Trading disable not supported or failed: {e}") # 3. Set max wallet limit (if percent provided) if max_wallet_percent > 0: try: total_supply = int(deployment.total_supply) max_wallet = str(int(total_supply * max_wallet_percent / 100)) tx = await deployer.set_max_wallet(contract_address, max_wallet) results["tx_hashes"].append(tx) results["max_wallet_set"] = True logger.info(f"Anti-sniper: max wallet set to {max_wallet_percent}% ({max_wallet})") except Exception as e: logger.warning(f"Max wallet set failed: {e}") # 4. Set max tx limit (if percent provided) if max_tx_percent > 0: try: total_supply = int(deployment.total_supply) max_tx = str(int(total_supply * max_tx_percent / 100)) tx = await deployer.set_max_tx(contract_address, max_tx) results["tx_hashes"].append(tx) results["max_tx_set"] = True logger.info(f"Anti-sniper: max tx set to {max_tx_percent}% ({max_tx})") except Exception as e: logger.warning(f"Max tx set failed: {e}") return results @classmethod async def enable_trading_after_delay( cls, deployer: Any, contract_address: str, delay_blocks: int, current_block: int, ) -> str: """Enable trading after block delay.""" target_block = current_block + delay_blocks logger.info(f"Anti-sniper: trading will enable at block {target_block}") # This would be called by a cron job or background task # For now, return the target block return str(target_block) # ── Snapshot Engine ───────────────────────────────────────────── class SnapshotEngine: """ Create snapshots of token holders for airdrop eligibility. Supports EVM chains (via RPC) and Solana (via RPC). """ @staticmethod async def create_evm_snapshot( token_address: str, chain: str, rpc_url: str, min_holdings: str = "0", excluded_addresses: list[str] | None = None, block_number: int | None = None, ) -> AirdropSnapshot: """Create snapshot of EVM token holders.""" from web3 import Web3 w3 = Web3(Web3.HTTPProvider(rpc_url)) # Standard ERC-20 events/topics transfer_topic = "0xddf252ad1be2c89b69c2b068fc378daa952ba7f163c4a11628f55a4df523b3ef" # Get current block if not specified if block_number is None: block_number = w3.eth.block_number # Query Transfer events to find all holders # This is a simplified approach - for production, use a subgraph or indexer logs = w3.eth.get_logs( { "fromBlock": max(0, block_number - 100000), # Last ~100k blocks "toBlock": block_number, "address": token_address, "topics": [transfer_topic], } ) # Build holder balances from transfer logs balances: dict[str, int] = {} for log in logs: try: from_addr = "0x" + log.topics[1].hex()[-40:] to_addr = "0x" + log.topics[2].hex()[-40:] amount = int(log.data.hex(), 16) if log.data else 0 balances[from_addr] = balances.get(from_addr, 0) - amount balances[to_addr] = balances.get(to_addr, 0) + amount except Exception: continue # Filter for positive balances above minimum min_amount = int(min_holdings) excluded = set(excluded_addresses or []) holders = [] total_supply = 0 for addr, balance in balances.items(): if balance > 0 and balance >= min_amount and addr.lower() not in excluded: holders.append( AirdropRecipient( address=addr, amount=str(balance), reason="snapshot_holder", ) ) total_supply += balance snapshot = AirdropSnapshot( snapshot_id=f"snap_{chain}_{token_address}_{block_number}_{int(time.time())}", source_token=token_address, source_chain=chain, block_number=block_number, holders=holders, total_holders=len(holders), total_supply_snapshotted=str(total_supply), excluded_addresses=list(excluded), min_holdings=min_holdings, ) logger.info(f"Snapshot created: {snapshot.snapshot_id} - {len(holders)} holders, {total_supply} total") return snapshot @staticmethod async def create_solana_snapshot( token_address: str, rpc_url: str, min_holdings: str = "0", excluded_addresses: list[str] | None = None, ) -> AirdropSnapshot: """Create snapshot of SPL token holders.""" from solana.rpc.api import Client from solders.pubkey import Pubkey client = Client(rpc_url) mint = Pubkey.from_string(token_address) # Get all token accounts for this mint response = client.get_token_accounts_by_mint_json_parsed(mint, commitment="confirmed") holders = [] total_supply = 0 excluded = set(excluded_addresses or []) min_amount = int(min_holdings) for account in response["result"]["value"]: try: parsed = account["account"]["data"]["parsed"]["info"] owner = parsed["owner"] amount = int(parsed["tokenAmount"]["amount"]) if amount >= min_amount and owner not in excluded: holders.append( AirdropRecipient( address=owner, amount=str(amount), reason="snapshot_holder", ) ) total_supply += amount except Exception: continue snapshot = AirdropSnapshot( snapshot_id=f"snap_solana_{token_address}_{int(time.time())}", source_token=token_address, source_chain="solana", holders=holders, total_holders=len(holders), total_supply_snapshotted=str(total_supply), excluded_addresses=list(excluded), min_holdings=min_holdings, ) return snapshot # ── Airdrop Distributor ─────────────────────────────────────── class AirdropDistributor: """ Execute airdrop distributions across chains. Supports batch transfers for gas efficiency. """ @staticmethod async def execute_evm_airdrop( deployer: Any, contract_address: str, recipients: list[AirdropRecipient], batch_size: int = 50, ) -> dict[str, Any]: """Execute airdrop on EVM chain (batch or individual).""" results = { "total_recipients": len(recipients), "successful": 0, "failed": 0, "tx_hashes": [], "errors": [], } # For small airdrops, use individual transfers # For large ones, use a Merkle distributor or batch contract if len(recipients) <= batch_size: # Individual transfers for recipient in recipients: try: tx = await deployer.mint_tokens( contract_address, recipient.address, recipient.amount, ) recipient.claimed = True recipient.claim_tx = tx recipient.claimed_at = datetime.utcnow().isoformat() results["successful"] += 1 results["tx_hashes"].append(tx) logger.info(f"Airdropped {recipient.amount} to {recipient.address}") except Exception as e: results["failed"] += 1 results["errors"].append({"address": recipient.address, "error": str(e)}) logger.error(f"Airdrop failed for {recipient.address}: {e}") else: # Batch via multicall or distributor contract # For now, chunk into batches for i in range(0, len(recipients), batch_size): batch = recipients[i : i + batch_size] try: # Use a batch mint function if available tx = await AirdropDistributor._batch_mint_evm(deployer, contract_address, batch) for r in batch: r.claimed = True r.claim_tx = tx r.claimed_at = datetime.utcnow().isoformat() results["successful"] += len(batch) results["tx_hashes"].append(tx) except Exception as e: results["failed"] += len(batch) logger.error(f"Batch airdrop failed: {e}") return results @staticmethod async def _batch_mint_evm( deployer: Any, contract_address: str, recipients: list[AirdropRecipient], ) -> str: """Batch mint via multicall or custom contract.""" # This would use a deployed MerkleDistributor or Multicall contract # For now, return a placeholder logger.info(f"Batch mint of {len(recipients)} recipients") return "batch_tx_placeholder" @staticmethod async def execute_solana_airdrop( deployer: Any, contract_address: str, recipients: list[AirdropRecipient], ) -> dict[str, Any]: """Execute airdrop on Solana.""" results = { "total_recipients": len(recipients), "successful": 0, "failed": 0, "tx_hashes": [], } for recipient in recipients: try: tx = await deployer.mint_tokens( contract_address, recipient.address, recipient.amount, ) recipient.claimed = True recipient.claim_tx = tx results["successful"] += 1 results["tx_hashes"].append(tx) except Exception as e: results["failed"] += 1 logger.error(f"Solana airdrop failed for {recipient.address}: {e}") return results # ── Team Allocation Engine ──────────────────────────────────── class TeamAllocation: """ Allocate tokens to team, dev, marketing, treasury with vesting. Typical allocation: • Development: 15-20% • Marketing: 5-10% • Treasury/DAO: 10-15% • Advisors: 5-10% • Airdrop: 10-20% • Liquidity: 20-30% • Public sale: remaining """ @staticmethod async def allocate_team_tokens( deployer: Any, deployment: TokenDeployment, allocations: list[dict[str, Any]], ) -> dict[str, Any]: """ Allocate team/dev tokens with optional immediate + vested portions. allocations: [ {"address": "0x...", "role": "dev", "percent": 10, "immediate": 20, "vested": 80, "cliff_months": 6, "vesting_months": 24}, {"address": "0x...", "role": "marketing", "percent": 5, "immediate": 50, "vested": 50, "cliff_months": 3, "vesting_months": 12}, ] """ results = { "allocations": [], "total_allocated": 0, "tx_hashes": [], } total_supply = int(deployment.total_supply) for alloc in allocations: try: percent = alloc["percent"] amount = int(total_supply * percent / 100) immediate_percent = alloc.get("immediate", 0) immediate_amount = int(amount * immediate_percent / 100) vested_amount = amount - immediate_amount # Mint immediate portion if immediate_amount > 0: tx = await deployer.mint_tokens( deployment.contract_address, alloc["address"], str(immediate_amount), ) results["tx_hashes"].append(tx) # Set up vesting for remainder if vested_amount > 0: vesting = VestingSchedule( schedule_id=f"vest_{deployment.deployment_id}_{alloc['role']}_{int(time.time())}", deployment_id=deployment.deployment_id, beneficiary=alloc["address"], total_amount=str(vested_amount), start_date=datetime.utcnow().isoformat(), cliff_months=alloc.get("cliff_months", 0), vesting_months=alloc.get("vesting_months", 0), monthly_release=str(vested_amount // max(alloc.get("vesting_months", 1), 1)), ) # Store vesting schedule await AirdropStorage.save_vesting(vesting) results["allocations"].append( { "role": alloc["role"], "address": alloc["address"], "percent": percent, "total_amount": str(amount), "immediate": str(immediate_amount), "vested": str(vested_amount), "tx_hashes": results["tx_hashes"][-1:], } ) results["total_allocated"] += amount except Exception as e: logger.error(f"Team allocation failed for {alloc}: {e}") return results # ── Storage ───────────────────────────────────────────────────── class AirdropStorage: """Store airdrop snapshots, campaigns, and vesting schedules.""" @staticmethod async def save_snapshot(snapshot: AirdropSnapshot) -> bool: """Save snapshot to Redis/Supabase.""" try: from app.token_deployer import get_storage storage = await get_storage() if storage.redis: key = f"airdrop_snapshot:{snapshot.snapshot_id}" await storage.redis.set(key, json.dumps(snapshot.__dict__)) await storage.redis.sadd("airdrop_snapshots:all", snapshot.snapshot_id) if storage.supabase: storage.supabase.table("airdrop_snapshots").upsert( { "snapshot_id": snapshot.snapshot_id, "source_token": snapshot.source_token, "source_chain": snapshot.source_chain, "block_number": snapshot.block_number, "timestamp": snapshot.timestamp, "holders": [h.__dict__ for h in snapshot.holders], "total_holders": snapshot.total_holders, "total_supply": snapshot.total_supply_snapshotted, } ).execute() return True except Exception as e: logger.error(f"Save snapshot failed: {e}") return False @staticmethod async def save_campaign(campaign: AirdropCampaign) -> bool: """Save campaign to storage.""" try: from app.token_deployer import get_storage storage = await get_storage() if storage.redis: key = f"airdrop_campaign:{campaign.campaign_id}" await storage.redis.set(key, json.dumps(campaign.to_dict())) await storage.redis.sadd("airdrop_campaigns:all", campaign.campaign_id) return True except Exception as e: logger.error(f"Save campaign failed: {e}") return False @staticmethod async def save_vesting(vesting: VestingSchedule) -> bool: """Save vesting schedule.""" try: from app.token_deployer import get_storage storage = await get_storage() if storage.redis: key = f"vesting:{vesting.schedule_id}" await storage.redis.set(key, json.dumps(vesting.__dict__)) await storage.redis.sadd("vesting:all", vesting.schedule_id) return True except Exception as e: logger.error(f"Save vesting failed: {e}") return False @staticmethod async def get_campaign(campaign_id: str) -> AirdropCampaign | None: """Get campaign by ID.""" try: from app.token_deployer import get_storage storage = await get_storage() if storage.redis: data = await storage.redis.get(f"airdrop_campaign:{campaign_id}") if data: d = json.loads(data) return AirdropCampaign(**d) return None except Exception as e: logger.error(f"Get campaign failed: {e}") return None # ── Convenience Functions ───────────────────────────────────── async def create_full_token_with_protection( chain: str, name: str, symbol: str, decimals: int, initial_supply: str, team_allocations: list[dict[str, Any]] | None = None, airdrop_source_token: str | None = None, airdrop_source_chain: str | None = None, anti_sniper: bool = True, trading_delay_blocks: int = 0, max_wallet_percent: float = 2.0, max_tx_percent: float = 1.0, blacklist_addresses: list[str] | None = None, ) -> dict[str, Any]: """ Full token launch with team allocation, airdrop, and anti-sniper protection. This is the main function for launching CRM v2 or any new token. """ from app.token_deployer import DeployParams results = { "deployment": None, "anti_sniper": None, "team_allocation": None, "airdrop": None, "errors": [], } try: # 1. Deploy token deployer = TokenDeployerFactory.get_deployer(chain) params = DeployParams( chain=chain, name=name, symbol=symbol, decimals=decimals, initial_supply=initial_supply, mintable=True, burnable=True, blacklist_enabled=anti_sniper, trading_enabled=not anti_sniper, # Disabled initially if anti-sniper ) deployment = await deployer.deploy_token(params) results["deployment"] = deployment.to_dict() # Save deployment storage = await get_storage() await storage.save(deployment) # 2. Apply anti-sniper protection if anti_sniper: protection = await AntiSniperProtection.apply_protection( deployer, deployment.contract_address, deployment, blacklist_addresses=blacklist_addresses, trading_delay_blocks=trading_delay_blocks, max_wallet_percent=max_wallet_percent, max_tx_percent=max_tx_percent, ) results["anti_sniper"] = protection # 3. Team allocation if team_allocations: team_result = await TeamAllocation.allocate_team_tokens(deployer, deployment, team_allocations) results["team_allocation"] = team_result # 4. Airdrop from snapshot if airdrop_source_token and airdrop_source_chain: # Create snapshot if airdrop_source_chain in ["ethereum", "base", "bsc"]: rpc = os.getenv(f"{airdrop_source_chain.upper()}_RPC_URL", "") snapshot = await SnapshotEngine.create_evm_snapshot( airdrop_source_token, airdrop_source_chain, rpc, ) elif airdrop_source_chain == "solana": rpc = os.getenv("SOLANA_RPC_URL", "") snapshot = await SnapshotEngine.create_solana_snapshot( airdrop_source_token, rpc, ) else: raise ValueError(f"Unsupported snapshot chain: {airdrop_source_chain}") await AirdropStorage.save_snapshot(snapshot) # Execute airdrop if chain in ["ethereum", "base", "bsc"]: airdrop_result = await AirdropDistributor.execute_evm_airdrop( deployer, deployment.contract_address, snapshot.holders ) elif chain == "solana": airdrop_result = await AirdropDistributor.execute_solana_airdrop( deployer, deployment.contract_address, snapshot.holders ) else: airdrop_result = {"error": "Airdrop not supported for this chain yet"} results["airdrop"] = { "snapshot_id": snapshot.snapshot_id, "holders": len(snapshot.holders), "result": airdrop_result, } return results except Exception as e: logger.error(f"Full token launch failed: {e}") results["errors"].append(str(e)) raise