| |
| """ |
| 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: |
| 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 |
|
|
| 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()) |
| |
| secret_bytes = bytes(kp)[:32] |
| |
| 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}") |
| |
| 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 |
|
|
|
|
| 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() |
|
|
| |
| mint_kp = Keypair() |
| mint_pubkey = mint_kp.pubkey() |
|
|
| |
| rent_lamports = get_minimum_balance_for_rent_exemption(MINT_LEN) |
|
|
| |
| blockhash_result = _rpc_call("getLatestBlockhash", []) |
| if not blockhash_result: |
| return {"status": "error", "message": "Failed to get recent blockhash"} |
| blockhash = blockhash_result["value"]["blockhash"] |
|
|
| try: |
| |
| |
| 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_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), |
| ) |
| ) |
|
|
| |
| |
| 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, |
| ) |
|
|
| |
| 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: |
| |
| 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>", |
| } |
|
|
| |
| 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}") |
|
|
| |
| 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() |
|
|
| |
| wallet = generate_wallet() |
| if wallet["status"] != "ok": |
| return wallet |
| kp = wallet["keypair"] |
| pubkey = wallet["pubkey"] |
| secret = wallet["secret"] |
|
|
| |
| airdrop_sig = request_airdrop(pubkey, 2_000_000_000) |
|
|
| if not airdrop_sig: |
| |
| 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.", |
| } |
|
|
| |
| 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": |
| |
| 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 <backup>", |
| } |
|
|
| mint_address = result["mint_address"] |
| tx_sig = result["tx_signature"] |
|
|
| |
| 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)) |
|
|