airmicrodrip / token_launcher.py
josephrw's picture
Upload folder using huggingface_hub
e9d4f6a verified
Raw
History Blame Contribute Delete
17.3 kB
#!/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))