merge: chore/cleanup-remove-bloat-and-secrets into main
This commit is contained in:
commit
bde2f3a97d
1173 changed files with 437609 additions and 0 deletions
302
app/routers/wallet_factory_router.py
Normal file
302
app/routers/wallet_factory_router.py
Normal file
|
|
@ -0,0 +1,302 @@
|
|||
"""
|
||||
RMI Wallet Factory API — Multi-Chain Wallet Generation & Rotation
|
||||
==================================================================
|
||||
REST API for generating, rotating, and managing wallets across 25+ blockchains.
|
||||
Secure key storage with AES-256-GCM encryption.
|
||||
|
||||
Endpoints:
|
||||
GET /api/v1/wallets/chains — List supported chains
|
||||
POST /api/v1/wallets/generate — Generate wallet(s)
|
||||
POST /api/v1/wallets/generate/batch — Generate for multiple chains
|
||||
POST /api/v1/wallets/rotate — Rotate to new wallet
|
||||
GET /api/v1/wallets/vault — List vault wallets (no keys)
|
||||
GET /api/v1/wallets/vault/{id} — Get wallet with key (auth required)
|
||||
GET /api/v1/wallets/stats — Wallet factory statistics
|
||||
POST /api/v1/wallets/export — Export for Apify/CLI usage
|
||||
|
||||
Free version: 3 chains, no rotation, no encryption
|
||||
Full version: All 25+ chains, rotation, encryption — available on Apify
|
||||
"""
|
||||
|
||||
import json
|
||||
import logging
|
||||
import os
|
||||
|
||||
from fastapi import APIRouter, HTTPException, Request
|
||||
from pydantic import BaseModel, Field
|
||||
|
||||
logger = logging.getLogger("wallet_api")
|
||||
|
||||
router = APIRouter(prefix="/api/v1/chain-vault", tags=["Chain Vault"])
|
||||
|
||||
# ── Import wallet factory ───────────────────────────────────────
|
||||
from app.wallet_factory import SUPPORTED_CHAINS, get_wallet_factory
|
||||
|
||||
# ── Auth ────────────────────────────────────────────────────────
|
||||
ADMIN_KEY = os.getenv("ADMIN_API_KEY", "")
|
||||
|
||||
|
||||
def _check_auth(request: Request) -> bool:
|
||||
"""Verify admin API key for sensitive operations."""
|
||||
auth = request.headers.get("X-API-Key", "") or request.headers.get("Authorization", "").replace("Bearer ", "")
|
||||
return auth == ADMIN_KEY if ADMIN_KEY else True # Allow if no admin key set
|
||||
|
||||
|
||||
# ── Models ──────────────────────────────────────────────────────
|
||||
|
||||
|
||||
class GenerateRequest(BaseModel):
|
||||
chain: str = Field(..., description="Chain key (btc, eth, sol, trx, etc.)")
|
||||
label: str = Field(default="", description="Optional wallet label")
|
||||
tags: list[str] = Field(default_factory=list, description="Optional tags")
|
||||
count: int = Field(default=1, ge=1, le=10, description="Number of wallets to generate")
|
||||
|
||||
|
||||
class BatchGenerateRequest(BaseModel):
|
||||
chains: list[str] = Field(..., description="List of chain keys")
|
||||
label_prefix: str = Field(default="", description="Label prefix for all wallets")
|
||||
|
||||
|
||||
class RotateRequest(BaseModel):
|
||||
chain: str = Field(..., description="Chain key to rotate")
|
||||
|
||||
|
||||
class ExportRequest(BaseModel):
|
||||
chains: list[str] = Field(default=[], description="Chains to export (empty = all)")
|
||||
format: str = Field(default="json", description="Export format: json, csv, env")
|
||||
|
||||
|
||||
# ── Public Endpoints ────────────────────────────────────────────
|
||||
|
||||
|
||||
@router.get("/chains")
|
||||
async def list_chains():
|
||||
"""List all supported blockchain networks with metadata.
|
||||
|
||||
Free tier: 3 chains shown. Full version: 25+ chains.
|
||||
"""
|
||||
chains = {}
|
||||
for key, cfg in SUPPORTED_CHAINS.items():
|
||||
chains[key] = {
|
||||
"name": cfg.name,
|
||||
"symbol": cfg.symbol,
|
||||
"family": cfg.family.value,
|
||||
"description": cfg.description,
|
||||
"hd_path": cfg.hd_path,
|
||||
"slip44": cfg.slip44,
|
||||
}
|
||||
return {
|
||||
"total_chains": len(chains),
|
||||
"chain_families": len({c["family"] for c in chains.values()}),
|
||||
"chains": chains,
|
||||
"pricing": {
|
||||
"free": "3 chains (btc, eth, sol) — basic generation only",
|
||||
"full": "25+ chains, rotation, encrypted vault, batch generation — available on Apify",
|
||||
},
|
||||
}
|
||||
|
||||
|
||||
@router.get("/stats")
|
||||
async def wallet_stats():
|
||||
"""Get wallet factory statistics and health."""
|
||||
factory = get_wallet_factory()
|
||||
return factory.stats
|
||||
|
||||
|
||||
@router.get("/vault")
|
||||
async def list_vault(request: Request):
|
||||
"""List all wallets in the vault (no private keys exposed)."""
|
||||
if not _check_auth(request):
|
||||
raise HTTPException(status_code=401, detail="Admin API key required")
|
||||
|
||||
vault_path = "/root/.rmi/wallets/vault.json"
|
||||
if not os.path.exists(vault_path):
|
||||
return {"wallets": [], "total": 0, "message": "Vault is empty"}
|
||||
|
||||
with open(vault_path) as f:
|
||||
vault = json.load(f)
|
||||
|
||||
wallets = []
|
||||
for key, data in vault.items():
|
||||
if key == "_meta":
|
||||
continue
|
||||
wallets.append(
|
||||
{
|
||||
"id": key,
|
||||
"chain": data.get("chain"),
|
||||
"chain_name": data.get("chain_name"),
|
||||
"address": data.get("address"),
|
||||
"label": data.get("label"),
|
||||
"tags": data.get("tags", []),
|
||||
"created_at": data.get("created_at"),
|
||||
"has_private_key": bool(data.get("private_key_hex")),
|
||||
"encrypted": data.get("encrypted", False),
|
||||
}
|
||||
)
|
||||
|
||||
return {
|
||||
"wallets": wallets,
|
||||
"total": len(wallets),
|
||||
"meta": vault.get("_meta", {}),
|
||||
}
|
||||
|
||||
|
||||
@router.get("/vault/{wallet_id}")
|
||||
async def get_wallet(wallet_id: str, request: Request):
|
||||
"""Get full wallet details INCLUDING private key (admin auth required)."""
|
||||
if not _check_auth(request):
|
||||
raise HTTPException(status_code=401, detail="Admin API key required")
|
||||
|
||||
vault_path = "/root/.rmi/wallets/vault.json"
|
||||
if not os.path.exists(vault_path):
|
||||
raise HTTPException(status_code=404, detail="Vault not found")
|
||||
|
||||
with open(vault_path) as f:
|
||||
vault = json.load(f)
|
||||
|
||||
if wallet_id not in vault:
|
||||
raise HTTPException(status_code=404, detail=f"Wallet {wallet_id} not found")
|
||||
|
||||
wallet = vault[wallet_id]
|
||||
|
||||
# Decrypt if needed
|
||||
if wallet.get("encrypted"):
|
||||
factory = get_wallet_factory()
|
||||
try:
|
||||
wallet["private_key_hex"] = factory.decrypt_key(wallet["private_key_hex"])
|
||||
wallet["decrypted"] = True
|
||||
except Exception as e:
|
||||
wallet["decrypted"] = False
|
||||
wallet["decrypt_error"] = str(e)
|
||||
|
||||
return wallet
|
||||
|
||||
|
||||
# ── Generation Endpoints ────────────────────────────────────────
|
||||
|
||||
|
||||
@router.post("/generate")
|
||||
async def generate_wallet(req: GenerateRequest, request: Request):
|
||||
"""Generate a new wallet for any supported chain.
|
||||
|
||||
Free tier: btc, eth, sol only. Full version: all 25+ chains.
|
||||
Returns address + public key only. Private key stored in vault.
|
||||
"""
|
||||
chain = req.chain.lower()
|
||||
if chain not in SUPPORTED_CHAINS:
|
||||
raise HTTPException(
|
||||
status_code=400,
|
||||
detail=f"Unsupported chain: {chain}. Use GET /chains to see supported chains.",
|
||||
)
|
||||
|
||||
factory = get_wallet_factory()
|
||||
wallets = []
|
||||
|
||||
for i in range(req.count):
|
||||
label = req.label or f"{chain}_wallet_{i + 1}"
|
||||
wallet = factory.generate(chain, label=label, tags=[*req.tags, f"generated_{i + 1}"])
|
||||
factory.save_to_vault(wallet)
|
||||
wallets.append(wallet.to_safe_dict())
|
||||
|
||||
return {
|
||||
"generated": len(wallets),
|
||||
"chain": chain,
|
||||
"chain_name": SUPPORTED_CHAINS[chain].name,
|
||||
"wallets": wallets,
|
||||
"vault_location": "/root/.rmi/wallets/vault.json",
|
||||
"note": "Private keys stored securely. Use GET /vault/{id} with admin key to retrieve.",
|
||||
}
|
||||
|
||||
|
||||
@router.post("/generate/batch")
|
||||
async def generate_batch(req: BatchGenerateRequest, request: Request):
|
||||
"""Generate wallets for multiple chains in one request."""
|
||||
invalid = [c for c in req.chains if c.lower() not in SUPPORTED_CHAINS]
|
||||
if invalid:
|
||||
raise HTTPException(status_code=400, detail=f"Unsupported chains: {invalid}")
|
||||
|
||||
factory = get_wallet_factory()
|
||||
results = {}
|
||||
|
||||
for chain in req.chains:
|
||||
wallet = factory.generate(chain, label=f"{req.label_prefix}_{chain}" if req.label_prefix else chain)
|
||||
factory.save_to_vault(wallet)
|
||||
results[chain] = wallet.to_safe_dict()
|
||||
|
||||
return {
|
||||
"chains_generated": len(results),
|
||||
"wallets": results,
|
||||
"vault_location": "/root/.rmi/wallets/vault.json",
|
||||
}
|
||||
|
||||
|
||||
# ── Rotation ────────────────────────────────────────────────────
|
||||
|
||||
|
||||
@router.post("/rotate")
|
||||
async def rotate_wallet(req: RotateRequest, request: Request):
|
||||
"""Rotate to a new wallet for a chain. Old wallet remains in vault.
|
||||
|
||||
Generates a fresh wallet and marks it as the active rotation target.
|
||||
Perfect for security-conscious payment collection.
|
||||
"""
|
||||
if not _check_auth(request):
|
||||
raise HTTPException(status_code=401, detail="Admin API key required")
|
||||
|
||||
chain = req.chain.lower()
|
||||
if chain not in SUPPORTED_CHAINS:
|
||||
raise HTTPException(status_code=400, detail=f"Unsupported chain: {chain}")
|
||||
|
||||
factory = get_wallet_factory()
|
||||
wallet = factory.rotate(chain)
|
||||
|
||||
return {
|
||||
"rotated": True,
|
||||
"chain": chain,
|
||||
"new_address": wallet.address,
|
||||
"rotation_index": factory._rotation_index.get(chain, 1),
|
||||
"message": f"New {chain.upper()} wallet active. Old wallet preserved in vault.",
|
||||
}
|
||||
|
||||
|
||||
# ── Export (Apify-ready) ────────────────────────────────────────
|
||||
|
||||
|
||||
@router.post("/export")
|
||||
async def export_wallets(req: ExportRequest, request: Request):
|
||||
"""Export wallet data for Apify integration or CLI usage.
|
||||
|
||||
Formats: json (full), csv (addresses only), env (KEY=VALUE pairs).
|
||||
Free version: 3 chains only.
|
||||
"""
|
||||
if not _check_auth(request):
|
||||
raise HTTPException(status_code=401, detail="Admin API key required")
|
||||
|
||||
chains = req.chains or list(SUPPORTED_CHAINS.keys())
|
||||
chains = [c for c in chains if c in SUPPORTED_CHAINS]
|
||||
|
||||
factory = get_wallet_factory()
|
||||
wallets = {}
|
||||
for c in chains:
|
||||
wallet = factory.generate(c)
|
||||
wallets[c] = wallet
|
||||
|
||||
if req.format == "csv":
|
||||
lines = ["chain,address,label,symbol"]
|
||||
for c, w in wallets.items():
|
||||
lines.append(f"{c},{w.address},{w.label},{w.symbol}")
|
||||
return {"format": "csv", "data": "\n".join(lines)}
|
||||
|
||||
elif req.format == "env":
|
||||
lines = [f"# RMI Wallet Export — {len(wallets)} chains"]
|
||||
for c, w in wallets.items():
|
||||
lines.append(f"WALLET_{c.upper()}_ADDRESS={w.address}")
|
||||
return {"format": "env", "data": "\n".join(lines)}
|
||||
|
||||
else:
|
||||
return {
|
||||
"format": "json",
|
||||
"total_wallets": len(wallets),
|
||||
"wallets": {c: w.to_safe_dict() for c, w in wallets.items()},
|
||||
"apify_link": "https://apify.com/rugmunch/wallet-factory",
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue