#!/usr/bin/env python3 """ AirMicroDrip Autonomous Token Launcher Creates a Solana wallet, requests devnet SOL, and mints a new SPL token automatically. Stores the keypair for user backup. """ import os import json import sqlite3 import requests import base64 import time from typing import Dict, Optional, List from datetime import datetime from pathlib import Path # Try to import solders for real transaction signing try: from solders.keypair import Keypair from solders.pubkey import Pubkey from solders.system_program import ID as SYSTEM_PROGRAM_ID SOLDERS_AVAILABLE = True except ImportError: SOLDERS_AVAILABLE = False print("[token_launcher] solders not available - using RPC-only mode") SOLANA_RPC_URL = os.environ.get("SOLANA_RPC_URL", "https://api.devnet.solana.com") TOKEN_PROGRAM_ID = "TokenkegQfeZyiNwAJbNbGKPFXCWuBvf9Ss623VQ5DA" MINT_LEN = 82 # bytes DB_PATH = os.environ.get("TOKEN_LAUNCH_DB", "token_launch/launch_registry.db") def _ensure_db(): Path(DB_PATH).parent.mkdir(parents=True, exist_ok=True) conn = sqlite3.connect(DB_PATH) cursor = conn.cursor() cursor.execute(""" CREATE TABLE IF NOT EXISTS launches ( id INTEGER PRIMARY KEY AUTOINCREMENT, wallet_pubkey TEXT UNIQUE, wallet_secret TEXT, mint_address TEXT UNIQUE, token_name TEXT, token_symbol TEXT, decimals INTEGER, total_supply INTEGER, network TEXT, created_at TEXT, status TEXT, backup_downloaded INTEGER DEFAULT 0, tx_signature TEXT ) """) conn.commit() conn.close() def _rpc_call(method: str, params: list) -> Optional[dict]: try: resp = requests.post( SOLANA_RPC_URL, json={"jsonrpc": "2.0", "id": 1, "method": method, "params": params}, headers={"Content-Type": "application/json"}, timeout=30, ) data = resp.json() return data.get("result") except Exception as e: print(f"[token_launcher] RPC error: {e}") return None def generate_wallet() -> Dict: """Generate a new Solana keypair.""" if not SOLDERS_AVAILABLE: return {"status": "error", "message": "solders library not installed"} kp = Keypair() pubkey = str(kp.pubkey()) # solders keypair bytes: first 32 = secret, last 32 = pubkey secret_bytes = bytes(kp)[:32] # Encode as base64 for storage (safer than base58 for raw bytes) secret_b64 = base64.b64encode(secret_bytes).decode("utf-8") return { "status": "ok", "pubkey": pubkey, "secret": secret_b64, "keypair": kp, } def request_airdrop(pubkey: str, lamports: int = 1_000_000_000) -> Optional[str]: """Request devnet SOL airdrop (1 SOL = 1B lamports).""" sig = _rpc_call("requestAirdrop", [pubkey, lamports]) if sig: print(f"[token_launcher] Airdrop requested: {sig}") # Wait for confirmation for _ in range(15): status = _rpc_call("getSignatureStatuses", [[sig]]) if status and status.get("value") and status["value"][0]: if status["value"][0].get("confirmationStatus") in ("confirmed", "finalized"): print(f"[token_launcher] Airdrop confirmed") return sig time.sleep(2) return None def get_minimum_balance_for_rent_exemption(data_len: int) -> int: """Get rent exemption in lamports.""" result = _rpc_call("getMinimumBalanceForRentExemption", [data_len]) return result or 1461600 # fallback for mint def _send_raw_transaction(tx_base64: str) -> Optional[str]: """Send a base64-encoded transaction.""" return _rpc_call("sendTransaction", [tx_base64, {"encoding": "base64", "skipPreflight": False}]) def create_token_mint( wallet_keypair, decimals: int = 9, token_name: str = "AirMicroDrip", token_symbol: str = "DRIP", ) -> Dict: """Create a new SPL token mint on Solana using raw RPC calls.""" if not SOLDERS_AVAILABLE: return {"status": "error", "message": "solders library not installed"} payer = wallet_keypair payer_pubkey = payer.pubkey() # Generate new mint keypair mint_kp = Keypair() mint_pubkey = mint_kp.pubkey() # Get rent exemption rent_lamports = get_minimum_balance_for_rent_exemption(MINT_LEN) # Get recent blockhash blockhash_result = _rpc_call("getLatestBlockhash", []) if not blockhash_result: return {"status": "error", "message": "Failed to get recent blockhash"} blockhash = blockhash_result["value"]["blockhash"] try: # Build transaction manually using solders Message + Transaction # Import here to handle version differences gracefully from solders.system_program import CreateAccountParams, create_account from solders.instruction import Instruction, AccountMeta from solders.message import Message from solders.transaction import Transaction # Create account instruction create_acc_ix = create_account( CreateAccountParams( from_pubkey=payer_pubkey, to_pubkey=mint_pubkey, lamports=rent_lamports, space=MINT_LEN, owner=Pubkey.from_string(TOKEN_PROGRAM_ID), ) ) # Initialize mint instruction (manually encoded) # Instruction 0 = InitializeMint init_data = bytes([0, decimals, 1]) + bytes(payer_pubkey) + bytes([0]) init_mint_ix = Instruction( program_id=Pubkey.from_string(TOKEN_PROGRAM_ID), accounts=[ AccountMeta(mint_pubkey, is_signer=False, is_writable=True), AccountMeta(payer_pubkey, is_signer=False, is_writable=False), ], data=init_data, ) # Build legacy transaction (not versioned - more compatible) msg = Message.new_with_blockhash( [create_acc_ix, init_mint_ix], payer_pubkey, blockhash, ) tx = Transaction([payer, mint_kp], msg, blockhash) tx_base64 = base64.b64encode(tx.serialize()).decode("utf-8") except Exception as e: # Fallback: try simpler approach if solders API differs print(f"[token_launcher] Transaction build warning: {e}") return { "status": "wallet_ready", "message": "Wallet created and funded, but automated mint creation requires spl-token CLI. Use 'spl-token create-token' with the backed-up keypair.", "wallet_pubkey": str(payer_pubkey), "next_step": "Install spl-token CLI and run: spl-token create-token --fee-payer ", } # Send transaction sig = _send_raw_transaction(tx_base64) if not sig: return {"status": "error", "message": "Failed to send create-mint transaction"} print(f"[token_launcher] Mint tx sent: {sig}") # Wait for confirmation for _ in range(20): status = _rpc_call("getSignatureStatuses", [[sig]]) if status and status.get("value") and status["value"][0]: if status["value"][0].get("confirmationStatus") in ("confirmed", "finalized"): if not status["value"][0].get("err"): print(f"[token_launcher] Mint confirmed: {mint_pubkey}") return { "status": "ok", "mint_address": str(mint_pubkey), "tx_signature": sig, "decimals": decimals, } else: return {"status": "error", "message": f"Transaction failed: {status['value'][0]['err']}"} time.sleep(2) return {"status": "error", "message": "Transaction confirmation timeout"} def _get_wallet_balance(pubkey: str) -> int: """Check a wallet's SOL balance via RPC.""" result = _rpc_call("getBalance", [pubkey]) if result and isinstance(result, dict): return result.get("value", 0) return 0 def _get_all_wallets_from_db() -> list: """Get all wallet records from DB that don't have a mint yet.""" _ensure_db() conn = sqlite3.connect(DB_PATH) cursor = conn.cursor() cursor.execute(""" SELECT wallet_pubkey, wallet_secret, status, created_at FROM launches WHERE wallet_secret IS NOT NULL ORDER BY created_at DESC """) rows = cursor.fetchall() conn.close() return [{"pubkey": r[0], "secret": r[1], "status": r[2], "created_at": r[3]} for r in rows] def autonomously_create_token( token_name: str = "AirMicroDrip", token_symbol: str = "DRIP", decimals: int = 9, existing_secret_b64: Optional[str] = None, ) -> Dict: """ Full autonomous flow: 1. Use existing wallet if funded, or generate new one 2. Request airdrop only if needed 3. Create token mint 4. Store in DB 5. Return mint address + backup info """ _ensure_db() kp = None pubkey = None secret = None # ── Try existing_secret_b64 first (user-provided funded wallet) ── if existing_secret_b64 and SOLDERS_AVAILABLE: try: secret_bytes = base64.b64decode(existing_secret_b64) kp = Keypair.from_seed(secret_bytes) pubkey = str(kp.pubkey()) secret = existing_secret_b64 balance = _get_wallet_balance(pubkey) if balance < 500_000: # Need at least 0.0005 SOL for fees return { "status": "error", "message": f"Provided wallet {pubkey} has insufficient balance ({balance} lamports). Fund with at least 0.005 SOL.", "wallet_pubkey": pubkey, "fund_url": f"https://faucet.solana.com/?address={pubkey}", } print(f"[token_launcher] Using provided wallet {pubkey} with {balance} lamports") except Exception as e: return {"status": "error", "message": f"Invalid existing_secret_b64: {e}"} # ── If no provided wallet, check DB for any previously created wallets with balance ── if not kp: wallets = _get_all_wallets_from_db() for w in wallets: bal = _get_wallet_balance(w["pubkey"]) if bal >= 500_000: try: secret_bytes = base64.b64decode(w["secret"]) kp = Keypair.from_seed(secret_bytes) pubkey = w["pubkey"] secret = w["secret"] print(f"[token_launcher] Reusing funded wallet {pubkey} with {bal} lamports") break except Exception: continue # ── No funded wallet found — generate new one ── if not kp: wallet = generate_wallet() if wallet["status"] != "ok": return wallet kp = wallet["keypair"] pubkey = wallet["pubkey"] secret = wallet["secret"] # Request airdrop for new wallet airdrop_sig = request_airdrop(pubkey, 2_000_000_000) # 2 SOL if not airdrop_sig: # Airdrop failed (devnet faucet rate-limited) — store wallet for manual funding conn = sqlite3.connect(DB_PATH) cursor = conn.cursor() cursor.execute(""" INSERT OR REPLACE INTO launches (wallet_pubkey, wallet_secret, mint_address, token_name, token_symbol, decimals, total_supply, network, created_at, status, tx_signature) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?) """, ( pubkey, secret, None, token_name, token_symbol, decimals, 1_000_000_000, "solana-devnet", datetime.utcnow().isoformat(), "wallet_created_needs_funding", None )) conn.commit() conn.close() return { "status": "wallet_created_needs_funding", "message": "Wallet created but devnet airdrop failed (faucet rate-limited). Fund the wallet manually, then retry with existing_secret_b64.", "wallet_pubkey": pubkey, "mint_address": None, "decimals": decimals, "total_supply": 1_000_000_000, "network": "solana-devnet", "backup_url": "/api/token/backup", "fund_url": f"https://faucet.solana.com/?address={pubkey}", "explorer_url": f"https://explorer.solana.com/address/{pubkey}?cluster=devnet", "warning": "Download your keypair backup immediately. Then fund this wallet and retry with existing_secret_b64 param.", } # ── Create mint ── result = create_token_mint(kp, decimals, token_name, token_symbol) if result["status"] not in ("ok", "wallet_ready"): return result if result["status"] == "wallet_ready": # Mint creation requires manual step — store wallet conn = sqlite3.connect(DB_PATH) cursor = conn.cursor() cursor.execute(""" INSERT OR REPLACE INTO launches (wallet_pubkey, wallet_secret, mint_address, token_name, token_symbol, decimals, total_supply, network, created_at, status, tx_signature) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?) """, ( pubkey, secret, None, token_name, token_symbol, decimals, 1_000_000_000, "solana-devnet", datetime.utcnow().isoformat(), "wallet_ready", None )) conn.commit() conn.close() return { "status": "wallet_ready", "message": "Wallet ready but automated mint creation hit a compatibility issue. Use spl-token CLI or retry.", "wallet_pubkey": pubkey, "mint_address": None, "backup_url": "/api/token/backup", "next_step": "Install spl-token CLI and run: spl-token create-token --fee-payer ", } mint_address = result["mint_address"] tx_sig = result["tx_signature"] # Store in DB conn = sqlite3.connect(DB_PATH) cursor = conn.cursor() cursor.execute(""" INSERT OR REPLACE INTO launches (wallet_pubkey, wallet_secret, mint_address, token_name, token_symbol, decimals, total_supply, network, created_at, status, tx_signature) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?) """, ( pubkey, secret, mint_address, token_name, token_symbol, decimals, 1_000_000_000, "solana-devnet", datetime.utcnow().isoformat(), "minted", tx_sig )) conn.commit() conn.close() return { "status": "ok", "message": f"Token '{token_name}' ({token_symbol}) created successfully on devnet.", "wallet_pubkey": pubkey, "mint_address": mint_address, "decimals": decimals, "total_supply": 1_000_000_000, "network": "solana-devnet", "tx_signature": tx_sig, "backup_url": "/api/token/backup", "warning": "Download and store your keypair backup immediately. It is the only way to recover this wallet.", } def get_launch_status() -> Optional[Dict]: """Get the most recent token launch status.""" _ensure_db() conn = sqlite3.connect(DB_PATH) cursor = conn.cursor() cursor.execute(""" SELECT wallet_pubkey, mint_address, token_name, token_symbol, decimals, total_supply, network, created_at, status, backup_downloaded, tx_signature FROM launches ORDER BY created_at DESC LIMIT 1 """) row = cursor.fetchone() conn.close() if not row: return None return { "wallet_pubkey": row[0], "mint_address": row[1], "token_name": row[2], "token_symbol": row[3], "decimals": row[4], "total_supply": row[5], "network": row[6], "created_at": row[7], "status": row[8], "backup_downloaded": bool(row[9]), "tx_signature": row[10], "explorer_url": f"https://explorer.solana.com/address/{row[1]}?cluster=devnet" if row[1] else None, } def get_keypair_backup() -> Optional[Dict]: """Get the wallet keypair for backup/download.""" _ensure_db() conn = sqlite3.connect(DB_PATH) cursor = conn.cursor() cursor.execute(""" SELECT wallet_pubkey, wallet_secret, mint_address, token_symbol FROM launches WHERE status IN ('minted', 'wallet_created_needs_funding', 'wallet_ready') ORDER BY created_at DESC LIMIT 1 """) row = cursor.fetchone() if row: cursor.execute("UPDATE launches SET backup_downloaded = 1 WHERE wallet_pubkey = ?", (row[0],)) conn.commit() conn.close() if not row: return None return { "pubkey": row[0], "secret": row[1], "mint_address": row[2], "token_symbol": row[3], } def get_existing_mint() -> Optional[str]: """Return the existing mint address if one exists.""" status = get_launch_status() return status["mint_address"] if status else None if __name__ == "__main__": result = autonomously_create_token() print(json.dumps(result, indent=2))