#!/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 autonomously_create_token( token_name: str = "AirMicroDrip", token_symbol: str = "DRIP", decimals: int = 9, ) -> Dict: """ Full autonomous flow: 1. Generate wallet 2. Request airdrop 3. Create token mint 4. Store in DB 5. Return mint address + backup info """ _ensure_db() # 1. Generate wallet wallet = generate_wallet() if wallet["status"] != "ok": return wallet kp = wallet["keypair"] pubkey = wallet["pubkey"] secret = wallet["secret"] # 2. Request airdrop 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.", "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 via the Solana devnet faucet.", } # 3. 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 created and funded. SPL token mint creation requires spl-token CLI in this environment.", "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"] # 4. 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))