Spaces:
Paused
Paused
| #!/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 <backup>", | |
| } | |
| # 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 <backup>", | |
| } | |
| 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)) | |