| """ |
| 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") |
|
|
|
|
| |
|
|
|
|
| @dataclass |
| class AirdropRecipient: |
| """Single recipient in an airdrop.""" |
|
|
| address: str |
| amount: str |
| reason: str = "" |
| 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" |
| distribution_type: str = "snapshot" |
| 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" |
| tx_hashes: list[str] = field(default_factory=list) |
|
|
|
|
| |
|
|
|
|
| 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_PATTERNS = [ |
| "0x0000000000000000000000000000000000000000", |
| ] |
|
|
| @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": [], |
| } |
|
|
| |
| 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}") |
|
|
| |
| 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}") |
|
|
| |
| 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}") |
|
|
| |
| 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}") |
|
|
| |
| |
| return str(target_block) |
|
|
|
|
| |
|
|
|
|
| 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)) |
|
|
| |
| transfer_topic = "0xddf252ad1be2c89b69c2b068fc378daa952ba7f163c4a11628f55a4df523b3ef" |
|
|
| |
| if block_number is None: |
| block_number = w3.eth.block_number |
|
|
| |
| |
| logs = w3.eth.get_logs( |
| { |
| "fromBlock": max(0, block_number - 100000), |
| "toBlock": block_number, |
| "address": token_address, |
| "topics": [transfer_topic], |
| } |
| ) |
|
|
| |
| 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 |
|
|
| |
| 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) |
|
|
| |
| 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 |
|
|
|
|
| |
|
|
|
|
| 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": [], |
| } |
|
|
| |
| |
| if len(recipients) <= batch_size: |
| |
| 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: |
| |
| |
| for i in range(0, len(recipients), batch_size): |
| batch = recipients[i : i + batch_size] |
| try: |
| |
| 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.""" |
| |
| |
| 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 |
|
|
|
|
| |
|
|
|
|
| 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 |
|
|
| |
| if immediate_amount > 0: |
| tx = await deployer.mint_tokens( |
| deployment.contract_address, |
| alloc["address"], |
| str(immediate_amount), |
| ) |
| results["tx_hashes"].append(tx) |
|
|
| |
| 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)), |
| ) |
| |
| 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 |
|
|
|
|
| |
|
|
|
|
| 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 |
|
|
|
|
| |
|
|
|
|
| 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: |
| |
| 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, |
| ) |
|
|
| deployment = await deployer.deploy_token(params) |
| results["deployment"] = deployment.to_dict() |
|
|
| |
| storage = await get_storage() |
| await storage.save(deployment) |
|
|
| |
| 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 |
|
|
| |
| if team_allocations: |
| team_result = await TeamAllocation.allocate_team_tokens(deployer, deployment, team_allocations) |
| results["team_allocation"] = team_result |
|
|
| |
| if airdrop_source_token and airdrop_source_chain: |
| |
| 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) |
|
|
| |
| 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 |
|
|