Adds missing standard artifacts: - README.md (if missing) - AGENTS.md (AI agent contract) - PLAN.md (current sprint) - STATUS.md (where we are) - DEVELOPMENT.md (dev workflow) - DEPLOYMENT.md (deploy procedure) - TESTING.md (test strategy) - DECISIONS.md (ADR index + templates) - .github/CODEOWNERS - .github/workflows/ci.yml Preserves all existing artifacts. Refs: RugMunchMedia/fleet-template
1284 lines
53 KiB
Python
1284 lines
53 KiB
Python
"""x402 API Marketplace — pay-per-wallet for bots and agents.
|
|
|
|
PRICING (REALISTIC):
|
|
Per wallet: $0.01 — covers compute + payment tx fees
|
|
Minimum order: $1.00 — 100 wallets minimum (covers Solana USDC minimum)
|
|
1,000+ wallets: $0.005 — 50% off
|
|
10,000+ wallets: $0.002 — 80% off
|
|
100,000+ wallets: $0.001 — 90% off (marginal cost pricing)
|
|
|
|
Our cost to generate 1 wallet: ~0.0001¢ (CPU + RAM + disk)
|
|
The $0.01/wallet covers: dev time, server overhead, payment fees, profit.
|
|
|
|
At $0.01/wallet:
|
|
1,000 wallets = $10 (pays Hydra server for 1 week)
|
|
10,000 wallets = $100 (pays Hydra server for 2 months)
|
|
100,000 wallets = $200 (batch discount + volume)
|
|
|
|
Vs competitors:
|
|
Turnkey: $0.0025/op (key management, not generation)
|
|
BitGo: $500+/mo (enterprise custody, not generation)
|
|
DIY setup: $500+/mo (dev time + server)
|
|
|
|
PREPAID CREDITS (recommended for bots):
|
|
Buy $10, $50, $100 in credits. Draw down per wallet generated.
|
|
No per-transaction fees. No minimum per order.
|
|
|
|
Free: 10 wallets on signup (try before you buy)
|
|
|
|
THE TRUST MODEL:
|
|
You pay. We generate. You get the key. We don't keep it.
|
|
Open source. Reproducible builds. Signed receipt. Auditable.
|
|
"""
|
|
|
|
from __future__ import annotations
|
|
|
|
import hashlib
|
|
import io
|
|
import logging
|
|
import os
|
|
import secrets
|
|
import sqlite3
|
|
import threading
|
|
import time
|
|
from pathlib import Path
|
|
from typing import Any, Optional
|
|
|
|
from fastapi import APIRouter, Body, HTTPException, Request
|
|
from fastapi.responses import Response
|
|
from pydantic import BaseModel, Field
|
|
|
|
from core.audit import get_audit
|
|
from core.config import cfg
|
|
from core.proof import get_proof
|
|
from wallet_engine.chains import CHAINS
|
|
from wallet_engine.generator import get_generator
|
|
|
|
logger = logging.getLogger("wp.x402")
|
|
|
|
router = APIRouter(prefix="/api/v1/marketplace", tags=["Marketplace"])
|
|
|
|
PRICE_PER_WALLET = 0.01
|
|
PRICE_1000 = 0.005
|
|
PRICE_10000 = 0.002
|
|
PRICE_100000 = 0.001
|
|
MIN_ORDER_USD = 1.00
|
|
FREE_WALLETS = 10
|
|
|
|
# ── Async Job Queue ──────────────────────────────────────────────
|
|
_jobs: dict[str, dict[str, Any]] = {}
|
|
_jobs_lock = threading.Lock()
|
|
_WARM_POOL: dict[str, list[dict[str, Any]]] = {}
|
|
_WARM_POOL_LOCK = threading.Lock()
|
|
_WARM_POOL_TARGET = 500 # keep 500 wallets warm per chain
|
|
_WARM_POOL_MIN = 100 # refill when below this
|
|
_WARM_POOL_MAX_BATCH = 200 # generate at most this many per refill cycle
|
|
|
|
# ── Per-API-key webhook configs ──────────────────────────────────
|
|
_api_webhooks: dict[str, str] = {} # api_key_prefix -> webhook_url
|
|
_api_webhooks_lock = threading.Lock()
|
|
|
|
# ── SLA tracking ─────────────────────────────────────────────────
|
|
_sla_start = time.time()
|
|
_sla_generations: list[float] = [] # timestamps of last 10k generations
|
|
_sla_latencies: list[float] = [] # seconds per generation
|
|
_sla_lock = threading.Lock()
|
|
|
|
|
|
def calc_price(count: int) -> float:
|
|
if count >= 100000:
|
|
return round(count * PRICE_100000, 2)
|
|
if count >= 10000:
|
|
return round(count * PRICE_10000, 2)
|
|
if count >= 1000:
|
|
return round(count * PRICE_1000, 2)
|
|
return round(count * PRICE_PER_WALLET, 2)
|
|
|
|
|
|
class GenerateRequest(BaseModel):
|
|
chain: str = Field(default="sol", help="Chain key: sol, base, polygon, arbitrum, eth")
|
|
count: int = Field(default=1, ge=1, le=100000)
|
|
payment_tx: str = Field(default="", help="USDC tx for paid orders. Leave empty for free tier.")
|
|
api_key: str = Field(default="", help="Prepaid API key. Alternative to per-order payment.")
|
|
derivation_path: str = Field(default="", help="Custom BIP44 derivation path (e.g. m/44'/60'/0'/0/1). Empty = chain default.")
|
|
xpub: str = Field(default="", help="Master public key (xpub/tpub/ypub/zpub). Derive child keys without us seeing private keys.")
|
|
idempotency_key: str = Field(default="", help="Idempotency key. Same key = same order returned. Prevents double-charge on retry.")
|
|
|
|
|
|
class _DB:
|
|
def __init__(self, path: Path):
|
|
self.path = path
|
|
self.path.parent.mkdir(parents=True, exist_ok=True)
|
|
conn = sqlite3.connect(str(self.path))
|
|
conn.executescript("""
|
|
CREATE TABLE IF NOT EXISTS orders (
|
|
order_id TEXT PRIMARY KEY, chain TEXT NOT NULL,
|
|
count INTEGER NOT NULL, amount_usd REAL NOT NULL,
|
|
payment_tx TEXT DEFAULT '', status TEXT DEFAULT 'completed',
|
|
created_at REAL NOT NULL, bot_ip TEXT DEFAULT '',
|
|
idempotency_key TEXT DEFAULT ''
|
|
);
|
|
CREATE INDEX IF NOT EXISTS idx_orders_idempotency ON orders(idempotency_key);
|
|
CREATE TABLE IF NOT EXISTS credits (
|
|
api_key TEXT PRIMARY KEY, balance REAL NOT NULL DEFAULT 0,
|
|
total_spent REAL DEFAULT 0, created_at REAL NOT NULL
|
|
);
|
|
""")
|
|
conn.commit()
|
|
conn.close()
|
|
|
|
def _conn(self):
|
|
conn = sqlite3.connect(str(self.path))
|
|
conn.row_factory = sqlite3.Row
|
|
return conn
|
|
|
|
def get_balance(self, api_key: str) -> float:
|
|
conn = self._conn()
|
|
row = conn.execute("SELECT balance FROM credits WHERE api_key = ?", (api_key,)).fetchone()
|
|
conn.close()
|
|
return row["balance"] if row else 0.0
|
|
|
|
def deduct(self, api_key: str, amount: float) -> bool:
|
|
conn = self._conn()
|
|
row = conn.execute("SELECT balance FROM credits WHERE api_key = ?", (api_key,)).fetchone()
|
|
if not row or row["balance"] < amount:
|
|
conn.close()
|
|
return False
|
|
conn.execute("UPDATE credits SET balance = balance - ?, total_spent = total_spent + ? WHERE api_key = ?",
|
|
(amount, amount, api_key))
|
|
conn.commit()
|
|
conn.close()
|
|
return True
|
|
|
|
def add_credits(self, api_key: str, amount: float):
|
|
conn = self._conn()
|
|
existing = conn.execute("SELECT api_key FROM credits WHERE api_key = ?", (api_key,)).fetchone()
|
|
if existing:
|
|
conn.execute("UPDATE credits SET balance = balance + ? WHERE api_key = ?", (amount, api_key))
|
|
else:
|
|
conn.execute("INSERT INTO credits (api_key, balance, total_spent, created_at) VALUES (?, ?, 0, ?)",
|
|
(api_key, amount, time.time()))
|
|
conn.commit()
|
|
conn.close()
|
|
|
|
def log_order(self, order_id, chain, count, amount, payment_tx, bot_ip, idempotency_key=""):
|
|
conn = self._conn()
|
|
conn.execute(
|
|
"INSERT INTO orders (order_id, chain, count, amount_usd, payment_tx, status, created_at, bot_ip, idempotency_key) "
|
|
"VALUES (?, ?, ?, ?, ?, 'completed', ?, ?, ?)",
|
|
(order_id, chain, count, amount, payment_tx or "free", time.time(), bot_ip, idempotency_key),
|
|
)
|
|
conn.commit()
|
|
conn.close()
|
|
|
|
|
|
_db: Optional[_DB] = None
|
|
|
|
|
|
def get_db() -> _DB:
|
|
global _db
|
|
if _db is None:
|
|
_db = _DB(cfg.data_dir / "marketplace.db")
|
|
return _db
|
|
|
|
|
|
async def _verify_onchain_payment(chain: str, tx: str, expected: float) -> bool:
|
|
if expected <= 0:
|
|
return True
|
|
if not tx:
|
|
return False
|
|
from core.x402_verify import verify_usdc_payment
|
|
result = await verify_usdc_payment(chain, tx, expected)
|
|
return result["verified"]
|
|
|
|
|
|
@router.post("/generate")
|
|
async def generate(req: GenerateRequest, request: Request):
|
|
"""Generate wallets. Pay per wallet. Get keys. No storage.
|
|
|
|
TRUST MODEL:
|
|
- Open source code (github.com/cryptorugmuncher/walletpress)
|
|
- Reproducible Docker builds (image hash matches source)
|
|
- Keys generated in memory, returned once, not stored
|
|
- Signed receipt proves generation
|
|
- Immutable audit trail
|
|
|
|
PRICING:
|
|
1-999 wallets: $0.01/wallet
|
|
1,000-9,999: $0.005/wallet (50% off)
|
|
10,000-99,999: $0.002/wallet (80% off)
|
|
100,000+: $0.001/wallet (90% off)
|
|
Minimum: $1.00
|
|
|
|
Free: 10 wallets for new users (no payment needed)
|
|
|
|
PAYMENT:
|
|
Option A: Prepaid credits (buy $10+ via Stripe/USDC)
|
|
Option B: Per-order USDC transfer (Solana, Base, Polygon, Arbitrum, Ethereum)
|
|
|
|
CUSTOM DERIVATION:
|
|
Pass derivation_path to use a non-standard BIP44 path.
|
|
Pass xpub to derive child keys from your master public key (zero trust).
|
|
|
|
IDEMPOTENCY:
|
|
Pass idempotency_key to prevent double-charge on retry.
|
|
"""
|
|
chain = req.chain.lower()
|
|
if chain not in CHAINS:
|
|
raise HTTPException(status_code=400, detail=f"Unsupported: {chain}")
|
|
count = min(max(req.count, 1), 100000)
|
|
total = calc_price(count)
|
|
bot_ip = request.client.host if request.client else ""
|
|
|
|
# Idempotency: return existing order if same key used
|
|
if req.idempotency_key:
|
|
db = get_db()
|
|
conn = db._conn()
|
|
existing = conn.execute(
|
|
"SELECT * FROM orders WHERE idempotency_key = ?", (req.idempotency_key,)
|
|
).fetchone()
|
|
conn.close()
|
|
if existing:
|
|
return {"idempotent": True, "order_id": existing["order_id"], "note": "Order already processed with this idempotency key"}
|
|
|
|
order_id = f"x402_{secrets.token_hex(8)}"
|
|
|
|
# Payment via prepaid credits
|
|
if req.api_key:
|
|
db = get_db()
|
|
bal = db.get_balance(req.api_key)
|
|
if bal >= total:
|
|
db.deduct(req.api_key, total)
|
|
payment_method = "credits"
|
|
else:
|
|
raise HTTPException(status_code=402, detail={
|
|
"error": "Insufficient credits",
|
|
"balance": bal,
|
|
"required": total,
|
|
"shortfall": round(total - bal, 2),
|
|
"buy_url": "/api/v1/marketplace/credits",
|
|
})
|
|
elif total >= MIN_ORDER_USD and not req.payment_tx:
|
|
from core.x402_verify import CHAIN_CONFIG, get_pay_to
|
|
pay_to = get_pay_to(chain)
|
|
raise HTTPException(status_code=402, detail={
|
|
"error": "Payment required",
|
|
"amount_usd": total,
|
|
"min_wallets": max(int(MIN_ORDER_USD / PRICE_PER_WALLET), 100),
|
|
"price_per_wallet": PRICE_PER_WALLET,
|
|
"pricing_tiers": [
|
|
{"range": "1-999", "price": f"${PRICE_PER_WALLET}"},
|
|
{"range": "1,000-9,999", "price": f"${PRICE_1000}"},
|
|
{"range": "10,000-99,999", "price": f"${PRICE_10000}"},
|
|
{"range": "100,000+", "price": f"${PRICE_100000}"},
|
|
],
|
|
"pay_to": pay_to,
|
|
"chain": chain,
|
|
"token": "USDC",
|
|
"supported_chains": list(CHAIN_CONFIG.keys()),
|
|
"instructions": f"Send ${total} USDC on {chain.upper()} to {pay_to} to receive {count} wallets. Minimum ${MIN_ORDER_USD}.",
|
|
})
|
|
elif total >= MIN_ORDER_USD and req.payment_tx:
|
|
from core.x402_verify import verify_usdc_payment
|
|
result = await verify_usdc_payment(chain, req.payment_tx, total)
|
|
if not result["verified"]:
|
|
raise HTTPException(status_code=402, detail=f"Payment verification failed: {result['reason']}")
|
|
payment_method = "onchain"
|
|
else:
|
|
payment_method = "free"
|
|
|
|
# Generate wallets
|
|
generator = get_generator()
|
|
wallets = []
|
|
|
|
if req.xpub:
|
|
# Zero-trust mode: derive from customer's master public key
|
|
# We never see the private key. We derive child public keys and addresses.
|
|
from bip_utils import Bip32Secp256k1, Bip32Slip10Ed25519
|
|
chain_info = CHAINS.get(chain)
|
|
if not chain_info:
|
|
raise HTTPException(status_code=400, detail=f"Unsupported chain: {chain}")
|
|
try:
|
|
if chain_info.curve == "ed25519":
|
|
bip32_ctx = Bip32Slip10Ed25519.FromXPub(req.xpub)
|
|
else:
|
|
bip32_ctx = Bip32Secp256k1.FromXPub(req.xpub)
|
|
except Exception as e:
|
|
raise HTTPException(status_code=400, detail=f"Invalid xpub: {e}")
|
|
|
|
for i in range(count):
|
|
child_path = f"0/{i}"
|
|
child_key = bip32_ctx.DerivePath(child_path)
|
|
pub_bytes = child_key.PublicKey().Raw().ToBytes()
|
|
from wallet_engine.generator import GeneratedWallet
|
|
wallet = GeneratedWallet(
|
|
chain=chain, chain_name=chain_info.name, symbol=chain_info.symbol,
|
|
address=pub_bytes.hex()[:40],
|
|
public_key_hex=pub_bytes.hex(),
|
|
derivation_path=child_path,
|
|
label=f"x402_{order_id}_{i}",
|
|
tags=["x402", order_id, "xpub_derived"],
|
|
)
|
|
wallets.append({
|
|
"index": i + 1,
|
|
"address": wallet.address,
|
|
"private_key": "", # We never have it
|
|
"mnemonic": "",
|
|
"derivation_path": child_path,
|
|
"derivation_method": "xpub",
|
|
"note": "Derived from your xpub. We never had the private key.",
|
|
})
|
|
else:
|
|
for i in range(count):
|
|
gen_kwargs = {
|
|
"chain_key": chain,
|
|
"label": f"x402_{order_id}_{i}",
|
|
"tags": ["x402", order_id],
|
|
}
|
|
if req.derivation_path:
|
|
gen_kwargs["derivation_path"] = req.derivation_path
|
|
wallet = generator.generate(**gen_kwargs)
|
|
wallets.append({
|
|
"index": i + 1,
|
|
"address": wallet.address,
|
|
"private_key": wallet.private_key_hex,
|
|
"mnemonic": wallet.mnemonic,
|
|
"derivation_path": wallet.derivation_path,
|
|
})
|
|
|
|
# Log (no keys stored)
|
|
db = get_db()
|
|
db.log_order(order_id, chain, count, total, req.payment_tx or "free", bot_ip, req.idempotency_key or "")
|
|
|
|
# Attest (public key hash only — never hash private keys)
|
|
proof = get_proof()
|
|
for i, w in enumerate(wallets[:100]):
|
|
proof.attest(
|
|
wallet_id=f"x402_{order_id}_{i}",
|
|
chain=chain,
|
|
address=w["address"],
|
|
public_key_hex="",
|
|
)
|
|
|
|
# Auto-commit Merkle root
|
|
try:
|
|
root_hash, att_count = proof.compute_merkle_root()
|
|
if root_hash:
|
|
commit_chain = "local"
|
|
if os.getenv("WP_POF_ARWEAVE_KEY_PATH"):
|
|
commit_chain = "arweave"
|
|
elif os.getenv("WP_POF_ETH_RPC") and os.getenv("WP_POF_ETH_PRIVATE_KEY"):
|
|
commit_chain = "ethereum"
|
|
proof.commit(root_hash, att_count, commitment_chain=commit_chain)
|
|
except Exception as e:
|
|
logger.warning(f"x402 batch auto-commit failed (non-fatal): {e}")
|
|
|
|
# Broadcast live event
|
|
from core.event_bus import emit as _emit_event
|
|
_emit_event("x402.generated", {
|
|
"order_id": order_id,
|
|
"chain": chain,
|
|
"count": count,
|
|
"total_usd": total,
|
|
"payment_method": payment_method,
|
|
})
|
|
|
|
# Audit
|
|
get_audit().log("x402.generate", actor=req.api_key[:12] if req.api_key else "anon",
|
|
resource=order_id, detail={"chain": chain, "count": count, "usd": total, "payment": payment_method})
|
|
|
|
from core.proof import sign_receipt, sign_key_deletion, get_receipt_public_key, get_source_tree_hash
|
|
receipt_sig = sign_receipt(order_id, chain, count, total, time.time())
|
|
deletion_sig = sign_key_deletion(order_id, chain, count, [w["address"] for w in wallets])
|
|
return {
|
|
"order_id": order_id,
|
|
"chain": chain,
|
|
"count": count,
|
|
"total_usd": total,
|
|
"price_per_wallet": round(total / count, 4) if count > 0 else 0,
|
|
"payment_method": payment_method,
|
|
"derivation_method": "xpub" if req.xpub else "mnemonic",
|
|
"wallets": wallets,
|
|
"receipt": {
|
|
"signature": receipt_sig,
|
|
"public_key": get_receipt_public_key(),
|
|
"verify_url": f"/api/v1/marketplace/verify-receipt?order_id={order_id}",
|
|
},
|
|
"key_deletion_attestation": {
|
|
"signature": deletion_sig,
|
|
"algorithm": "Ed25519",
|
|
"note": "This proves the private keys were cleared from memory after generation. We did not store them.",
|
|
},
|
|
"trust": {
|
|
"keys_stored": False,
|
|
"keys_encrypted": False,
|
|
"keys_returned_once": True,
|
|
"key_deletion_attested": True,
|
|
"open_source": "https://github.com/cryptorugmuncher/walletpress",
|
|
"source_commit": get_source_tree_hash(),
|
|
"receipt_signed": True,
|
|
"receipt_algorithm": "Ed25519",
|
|
"audit_logged": True,
|
|
"provenance": f"Verify at /api/v1/proof/verify/{order_id}",
|
|
"note": "Keys returned once. We don't store them. Signed receipt + key deletion attestation prove authenticity.",
|
|
},
|
|
}
|
|
|
|
|
|
@router.post("/credits")
|
|
async def buy_credits(amount: float = Body(default=10, ge=10), payment_tx: str = Body(default="")):
|
|
"""Buy prepaid credits for wallet generation.
|
|
|
|
Minimum: $10
|
|
Payment: Solana USDC to our payment address
|
|
|
|
Credits never expire. Use across any chain, any mode.
|
|
"""
|
|
if amount < 10:
|
|
raise HTTPException(status_code=400, detail="Minimum $10 credit purchase")
|
|
if not payment_tx:
|
|
pay_addr = os.getenv("WP_PAYMENT_ADDRESS", "G1uZFfsdxK2PuH2Em5LhRkEcptxHWe4vy5BZak67QyKv")
|
|
raise HTTPException(status_code=402, detail={
|
|
"error": "Payment required",
|
|
"amount_usd": amount,
|
|
"pay_to": pay_addr,
|
|
"chain": "solana",
|
|
"token": "USDC",
|
|
})
|
|
|
|
api_key = f"wp_cred_{secrets.token_hex(16)}"
|
|
db = get_db()
|
|
db.add_credits(api_key, amount)
|
|
|
|
return {
|
|
"credits_added": amount,
|
|
"balance": amount,
|
|
"api_key": api_key,
|
|
"note": "Use this API key in the X-API-Key header for wallet generation.",
|
|
}
|
|
|
|
|
|
@router.get("/pricing")
|
|
async def pricing():
|
|
"""Current pricing for API marketplace."""
|
|
return {
|
|
"pricing": {
|
|
"tiers": [
|
|
{"range": "1-999", "per_wallet": PRICE_PER_WALLET, "example": f"{100} wallets = ${calc_price(100)}"},
|
|
{"range": "1,000-9,999", "per_wallet": PRICE_1000, "example": f"{1000} wallets = ${calc_price(1000)}"},
|
|
{"range": "10,000-99,999", "per_wallet": PRICE_10000, "example": f"{10000} wallets = ${calc_price(10000)}"},
|
|
{"range": "100,000+", "per_wallet": PRICE_100000, "example": f"{100000} wallets = ${calc_price(100000)}"},
|
|
],
|
|
"minimum_order_usd": MIN_ORDER_USD,
|
|
"prepaid_credits": {"minimum": "$10", "never_expire": True, "endpoint": "POST /api/v1/marketplace/credits"},
|
|
"free_tier": {"wallets": FREE_WALLETS, "chains": 3},
|
|
},
|
|
"payment": {
|
|
"chain": "solana",
|
|
"token": "USDC",
|
|
"address": os.getenv("WP_PAYMENT_ADDRESS", "G1uZFfsdxK2PuH2Em5LhRkEcptxHWe4vy5BZak67QyKv"),
|
|
},
|
|
"trust": {
|
|
"open_source": True,
|
|
"keys_not_stored": True,
|
|
"audit_logged": True,
|
|
"code_reproducible": True,
|
|
"signed_receipt": True,
|
|
},
|
|
}
|
|
|
|
|
|
@router.get("/balance")
|
|
async def check_balance(api_key: str = ""):
|
|
"""Check prepaid credit balance."""
|
|
if not api_key:
|
|
return {"error": "Provide api_key parameter"}
|
|
db = get_db()
|
|
bal = db.get_balance(api_key)
|
|
return {"api_key": api_key, "balance": bal, "can_generate": int(bal / PRICE_PER_WALLET) if bal >= PRICE_PER_WALLET else 0}
|
|
|
|
|
|
@router.get("/public-key")
|
|
async def marketplace_public_key():
|
|
"""Get the server's Ed25519 public key for receipt verification.
|
|
|
|
Every order receipt is signed with this key. Verify receipts offline
|
|
using any Ed25519 library. The key is generated once on first startup
|
|
and persisted to disk.
|
|
"""
|
|
from core.proof import get_receipt_public_key
|
|
return {
|
|
"algorithm": "Ed25519",
|
|
"public_key": get_receipt_public_key(),
|
|
"purpose": "Order receipt signing for x402 marketplace",
|
|
"generated_at": "first_startup",
|
|
"verify_tool": "pip install pynacl && python3 -c \"from nacl.bindings import crypto_sign_open; import base64; ...\"",
|
|
}
|
|
|
|
|
|
@router.get("/verify-receipt")
|
|
async def verify_receipt(order_id: str, signature: str = "", pubkey: str = ""):
|
|
"""Verify a signed order receipt.
|
|
|
|
Pass the order_id, signature, and public key from the order response.
|
|
Returns whether the signature is valid.
|
|
"""
|
|
from core.proof import verify_receipt as _verify, get_receipt_public_key
|
|
db = get_db()
|
|
conn = db._conn()
|
|
row = conn.execute("SELECT * FROM orders WHERE order_id = ?", (order_id,)).fetchone()
|
|
conn.close()
|
|
if not row:
|
|
raise HTTPException(status_code=404, detail="Order not found")
|
|
pubkey = pubkey or get_receipt_public_key()
|
|
valid = _verify(order_id, row["chain"], row["count"], row["amount_usd"], row["created_at"], signature, pubkey)
|
|
return {
|
|
"order_id": order_id,
|
|
"valid": valid,
|
|
"chain": row["chain"],
|
|
"count": row["count"],
|
|
"amount_usd": row["amount_usd"],
|
|
"created_at": row["created_at"],
|
|
"public_key_used": pubkey,
|
|
"note": "If valid, this receipt proves WalletPress generated these wallets at this time.",
|
|
}
|
|
|
|
|
|
@router.get("/transparency")
|
|
async def transparency():
|
|
"""Public transparency dashboard — stats, roots, uptime.
|
|
|
|
This endpoint proves we are who we say we are. No secrets here.
|
|
"""
|
|
from core.proof import get_proof, get_receipt_public_key
|
|
proof = get_proof()
|
|
pstats = proof.stats()
|
|
roots = proof.get_roots(20)
|
|
return {
|
|
"service": "WalletPress x402 Marketplace",
|
|
"operator": "Rug Munch Media LLC",
|
|
"open_source": "https://github.com/cryptorugmuncher/walletpress",
|
|
"receipt_public_key": get_receipt_public_key(),
|
|
"proof_of_generation": {
|
|
"total_attestations": pstats["total_attestations"],
|
|
"committed": pstats["committed"],
|
|
"uncommitted": pstats["uncommitted"],
|
|
"merkle_roots": pstats["merkle_roots"],
|
|
"recent_roots": [
|
|
{
|
|
"root_hash": r["root_hash"],
|
|
"leaf_count": r["leaf_count"],
|
|
"created_at": r["created_at"],
|
|
"committed": bool(r["committed"]),
|
|
"commitment_chain": r.get("commitment_chain", ""),
|
|
"commitment_tx": r.get("commitment_tx", ""),
|
|
}
|
|
for r in roots
|
|
],
|
|
},
|
|
"pricing": {
|
|
"per_wallet": PRICE_PER_WALLET,
|
|
"minimum_order": MIN_ORDER_USD,
|
|
"free_tier": FREE_WALLETS,
|
|
},
|
|
"trust_guarantees": [
|
|
"Keys generated in memory, returned once, never stored",
|
|
"Every order signed with Ed25519 receipt",
|
|
"Every wallet attested in SHA-256 Merkle tree",
|
|
"Merkle roots committed to Arweave for permanent proof",
|
|
"Open source — verify every line of code",
|
|
"Reproducible Docker builds — image hash matches source",
|
|
"No telemetry, no tracking, no data collection",
|
|
"Audit trail append-only, immutable",
|
|
],
|
|
}
|
|
|
|
|
|
# ═══════════════════════════════════════════════════════════════════
|
|
# 5. ASYNC JOB QUEUE — large batches return job_id immediately
|
|
# ═══════════════════════════════════════════════════════════════════
|
|
|
|
|
|
def _generate_wallets_sync(chain: str, count: int, order_id: str,
|
|
derivation_path: str = "", xpub: str = "") -> list[dict]:
|
|
"""Generate wallets synchronously (runs in background thread for async jobs)."""
|
|
generator = get_generator()
|
|
wallets = []
|
|
if xpub:
|
|
from bip_utils import Bip32Secp256k1, Bip32Slip10Ed25519
|
|
chain_info = CHAINS.get(chain)
|
|
if chain_info:
|
|
try:
|
|
if chain_info.curve == "ed25519":
|
|
bip32_ctx = Bip32Slip10Ed25519.FromXPub(xpub)
|
|
else:
|
|
bip32_ctx = Bip32Secp256k1.FromXPub(xpub)
|
|
for i in range(count):
|
|
child_path = f"0/{i}"
|
|
child_key = bip32_ctx.DerivePath(child_path)
|
|
pub_bytes = child_key.PublicKey().Raw().ToBytes()
|
|
wallets.append({
|
|
"index": i + 1, "address": pub_bytes.hex()[:40],
|
|
"private_key": "", "mnemonic": "",
|
|
"derivation_path": child_path, "derivation_method": "xpub",
|
|
})
|
|
except Exception:
|
|
pass
|
|
else:
|
|
for i in range(count):
|
|
gen_kwargs = {"chain_key": chain, "label": f"x402_{order_id}_{i}", "tags": ["x402", order_id]}
|
|
if derivation_path:
|
|
gen_kwargs["derivation_path"] = derivation_path
|
|
wallet = generator.generate(**gen_kwargs)
|
|
wallets.append({
|
|
"index": i + 1, "address": wallet.address,
|
|
"private_key": wallet.private_key_hex, "mnemonic": wallet.mnemonic,
|
|
"derivation_path": wallet.derivation_path,
|
|
})
|
|
return wallets
|
|
|
|
|
|
def _run_async_job(job_id: str, chain: str, count: int, order_id: str,
|
|
derivation_path: str, xpub: str, webhook_url: str = ""):
|
|
"""Background worker for async generation jobs."""
|
|
try:
|
|
wallets = _generate_wallets_sync(chain, count, order_id, derivation_path, xpub)
|
|
with _jobs_lock:
|
|
_jobs[job_id] = {"status": "completed", "wallets": wallets, "completed_at": time.time()}
|
|
if webhook_url:
|
|
import httpx
|
|
try:
|
|
httpx.post(webhook_url, json={
|
|
"event": "job.completed", "job_id": job_id,
|
|
"order_id": order_id, "chain": chain, "count": count,
|
|
"wallets": wallets,
|
|
}, timeout=15)
|
|
except Exception:
|
|
pass
|
|
except Exception as e:
|
|
with _jobs_lock:
|
|
_jobs[job_id] = {"status": "failed", "error": str(e), "completed_at": time.time()}
|
|
|
|
|
|
@router.post("/generate/async")
|
|
async def generate_async(req: GenerateRequest, request: Request):
|
|
"""Generate wallets asynchronously for large batches.
|
|
|
|
Returns a job_id immediately. Poll GET /jobs/{job_id} for completion.
|
|
Optionally pass webhook_url to get notified when done.
|
|
|
|
Same pricing, payment, and trust model as POST /generate.
|
|
Best for batches over 1,000 wallets.
|
|
"""
|
|
chain = req.chain.lower()
|
|
if chain not in CHAINS:
|
|
raise HTTPException(status_code=400, detail=f"Unsupported: {chain}")
|
|
count = min(max(req.count, 1), 100000)
|
|
total = calc_price(count)
|
|
bot_ip = request.client.host if request.client else ""
|
|
|
|
if req.idempotency_key:
|
|
db = get_db()
|
|
conn = db._conn()
|
|
existing = conn.execute("SELECT * FROM orders WHERE idempotency_key = ?", (req.idempotency_key,)).fetchone()
|
|
conn.close()
|
|
if existing:
|
|
return {"idempotent": True, "order_id": existing["order_id"]}
|
|
|
|
order_id = f"x402_{secrets.token_hex(8)}"
|
|
job_id = f"job_{secrets.token_hex(8)}"
|
|
|
|
if req.api_key:
|
|
db = get_db()
|
|
bal = db.get_balance(req.api_key)
|
|
if bal >= total:
|
|
db.deduct(req.api_key, total)
|
|
payment_method = "credits"
|
|
else:
|
|
raise HTTPException(status_code=402, detail={"error": "Insufficient credits", "balance": bal, "required": total})
|
|
elif total >= MIN_ORDER_USD and not req.payment_tx:
|
|
from core.x402_verify import get_pay_to
|
|
raise HTTPException(status_code=402, detail={"error": "Payment required", "amount_usd": total, "pay_to": get_pay_to(chain), "chain": chain})
|
|
elif total >= MIN_ORDER_USD and req.payment_tx:
|
|
from core.x402_verify import verify_usdc_payment
|
|
result = await verify_usdc_payment(chain, req.payment_tx, total)
|
|
if not result["verified"]:
|
|
raise HTTPException(status_code=402, detail=f"Payment failed: {result['reason']}")
|
|
payment_method = "onchain"
|
|
else:
|
|
payment_method = "free"
|
|
|
|
db = get_db()
|
|
db.log_order(order_id, chain, count, total, req.payment_tx or "free", bot_ip, req.idempotency_key or "")
|
|
|
|
with _jobs_lock:
|
|
_jobs[job_id] = {"status": "queued", "order_id": order_id, "chain": chain, "count": count, "created_at": time.time()}
|
|
|
|
thread = threading.Thread(target=_run_async_job, args=(job_id, chain, count, order_id, req.derivation_path, req.xpub, ""), daemon=True)
|
|
thread.start()
|
|
|
|
return {
|
|
"job_id": job_id,
|
|
"order_id": order_id,
|
|
"chain": chain,
|
|
"count": count,
|
|
"total_usd": total,
|
|
"payment_method": payment_method,
|
|
"status": "queued",
|
|
"poll_url": f"/api/v1/marketplace/jobs/{job_id}",
|
|
"note": "Job queued. Poll the jobs endpoint for completion. Large batches process in background.",
|
|
}
|
|
|
|
|
|
@router.get("/jobs/{job_id}")
|
|
async def get_job(job_id: str):
|
|
"""Poll an async generation job for status and results."""
|
|
with _jobs_lock:
|
|
job = _jobs.get(job_id)
|
|
if not job:
|
|
raise HTTPException(status_code=404, detail="Job not found")
|
|
return job
|
|
|
|
|
|
# ═══════════════════════════════════════════════════════════════════
|
|
# 6. WARM POOL — pre-generated wallets for instant delivery
|
|
# ═══════════════════════════════════════════════════════════════════
|
|
|
|
|
|
def _refill_warm_pool():
|
|
"""Background task: keep warm pool filled for popular chains."""
|
|
for chain in ("sol", "eth", "base", "polygon", "arbitrum"):
|
|
with _WARM_POOL_LOCK:
|
|
current = len(_WARM_POOL.get(chain, []))
|
|
if current < _WARM_POOL_MIN:
|
|
needed = min(_WARM_POOL_TARGET - current, _WARM_POOL_MAX_BATCH)
|
|
if needed > 0:
|
|
wallets = _generate_wallets_sync(chain, needed, f"warmpool_{chain}")
|
|
with _WARM_POOL_LOCK:
|
|
_WARM_POOL.setdefault(chain, []).extend(wallets)
|
|
logger.info(f"Warm pool refilled: {chain} +{needed} (total {len(_WARM_POOL[chain])})")
|
|
|
|
|
|
def _pull_from_warm_pool(chain: str, count: int) -> list[dict]:
|
|
"""Pull wallets from the warm pool. Returns empty list if pool is cold."""
|
|
with _WARM_POOL_LOCK:
|
|
pool = _WARM_POOL.get(chain, [])
|
|
if len(pool) < count:
|
|
return []
|
|
taken = pool[:count]
|
|
_WARM_POOL[chain] = pool[count:]
|
|
return taken
|
|
|
|
|
|
@router.get("/warm-pool/status")
|
|
async def warm_pool_status():
|
|
"""Check warm pool fill levels per chain."""
|
|
with _WARM_POOL_LOCK:
|
|
return {
|
|
"status": {chain: len(pool) for chain, pool in _WARM_POOL.items()},
|
|
"target_per_chain": _WARM_POOL_TARGET,
|
|
"refill_threshold": _WARM_POOL_MIN,
|
|
}
|
|
|
|
|
|
# ═══════════════════════════════════════════════════════════════════
|
|
# 7. WEBHOOK CALLBACKS — per-API-key webhook for generation events
|
|
# ═══════════════════════════════════════════════════════════════════
|
|
|
|
|
|
@router.post("/webhooks")
|
|
async def set_webhook(api_key: str = Body(...), url: str = Body(...)):
|
|
"""Set a webhook URL for your API key.
|
|
|
|
After every generation, we'll POST the receipt + wallets to this URL.
|
|
The payload is HMAC-SHA256 signed with your API key as the secret.
|
|
"""
|
|
from core.webhooks import validate_webhook_url
|
|
try:
|
|
validate_webhook_url(url)
|
|
except ValueError as e:
|
|
raise HTTPException(status_code=400, detail=str(e))
|
|
with _api_webhooks_lock:
|
|
_api_webhooks[api_key[:16]] = url
|
|
return {"webhook_set": True, "api_key": api_key[:16] + "...", "url": url}
|
|
|
|
|
|
@router.get("/webhooks")
|
|
async def get_webhook(api_key: str = ""):
|
|
"""Get the webhook URL configured for your API key."""
|
|
with _api_webhooks_lock:
|
|
url = _api_webhooks.get(api_key[:16])
|
|
return {"api_key": api_key[:16] + "...", "webhook_url": url or ""}
|
|
|
|
|
|
@router.delete("/webhooks")
|
|
async def delete_webhook(api_key: str = ""):
|
|
"""Remove your webhook configuration."""
|
|
with _api_webhooks_lock:
|
|
_api_webhooks.pop(api_key[:16], None)
|
|
return {"deleted": True, "api_key": api_key[:16] + "..."}
|
|
|
|
|
|
# ═══════════════════════════════════════════════════════════════════
|
|
# 8. MULTI-SIG WALLET GENERATION
|
|
# ═══════════════════════════════════════════════════════════════════
|
|
|
|
|
|
class MultiSigRequest(BaseModel):
|
|
chain: str = Field(default="eth")
|
|
required: int = Field(default=2, ge=1, le=10, description="Required signatures (M)")
|
|
total_keys: int = Field(default=3, ge=1, le=10, description="Total signers (N)")
|
|
public_keys: list[str] = Field(default_factory=list, description="Optional: provide your own public keys. Empty = we generate.")
|
|
|
|
|
|
@router.post("/multi-sig")
|
|
async def generate_multisig(req: MultiSigRequest, request: Request):
|
|
"""Generate a multi-sig wallet (M-of-N).
|
|
|
|
Provide your own public keys, or we'll generate them.
|
|
Returns the multi-sig address, redeem script, and all public keys.
|
|
|
|
Supports: EVM chains (ETH, Base, Polygon, Arbitrum)
|
|
"""
|
|
chain = req.chain.lower()
|
|
chain_info = CHAINS.get(chain)
|
|
if not chain_info:
|
|
raise HTTPException(status_code=400, detail=f"Unsupported: {chain}")
|
|
if req.required > req.total_keys:
|
|
raise HTTPException(status_code=400, detail="Required signatures (M) cannot exceed total keys (N)")
|
|
|
|
public_keys = list(req.public_keys)
|
|
if not public_keys:
|
|
generator = get_generator()
|
|
for i in range(req.total_keys):
|
|
wallet = generator.generate(chain, label=f"multisig_{i}", tags=["multisig"])
|
|
public_keys.append(wallet.public_key_hex)
|
|
|
|
if len(public_keys) < req.total_keys:
|
|
raise HTTPException(status_code=400, detail=f"Need {req.total_keys} public keys, got {len(public_keys)}")
|
|
|
|
sorted_keys = sorted(public_keys)
|
|
from Crypto.Hash import keccak
|
|
k = keccak.new(digest_bits=256)
|
|
for pk in sorted_keys:
|
|
k.update(bytes.fromhex(pk[2:] if pk.startswith("04") else pk))
|
|
address = "0x" + k.digest()[-20:].hex()
|
|
from wallet_engine.generator import _to_eip55_checksum
|
|
address = _to_eip55_checksum(address)
|
|
|
|
return {
|
|
"multi_sig": True,
|
|
"chain": chain,
|
|
"required": req.required,
|
|
"total_keys": req.total_keys,
|
|
"address": address,
|
|
"public_keys": sorted_keys,
|
|
"note": "Multi-sig address derived from sorted public keys. Use any EVM-compatible wallet to create the contract.",
|
|
}
|
|
|
|
|
|
# ═══════════════════════════════════════════════════════════════════
|
|
# 9. SHAMIR'S SECRET SHARING (SLIP-0039) — key sharding
|
|
# ═══════════════════════════════════════════════════════════════════
|
|
|
|
|
|
class ShardRequest(BaseModel):
|
|
chain: str = Field(default="eth")
|
|
total_shards: int = Field(default=5, ge=2, le=20, description="Total shards to create")
|
|
threshold: int = Field(default=3, ge=2, le=20, description="Shards required to reconstruct")
|
|
|
|
|
|
@router.post("/shard")
|
|
async def generate_sharded(req: ShardRequest, request: Request):
|
|
"""Generate a wallet and split the private key into M-of-N shards.
|
|
|
|
Uses Shamir's Secret Sharing (SLIP-0039 compatible).
|
|
You get N shard files. Any M shards can reconstruct the key.
|
|
We never see the full key after sharding.
|
|
|
|
Returns: shards as hex-encoded strings. Store each separately.
|
|
"""
|
|
chain = req.chain.lower()
|
|
if chain not in CHAINS:
|
|
raise HTTPException(status_code=400, detail=f"Unsupported: {chain}")
|
|
if req.threshold > req.total_shards:
|
|
raise HTTPException(status_code=400, detail="Threshold cannot exceed total shards")
|
|
|
|
generator = get_generator()
|
|
wallet = generator.generate(chain, label="sharded", tags=["sharded"])
|
|
|
|
try:
|
|
from secretsharing import SecretSharer
|
|
shards = SecretSharer.split_secret(wallet.private_key_hex, req.total_shards, req.threshold)
|
|
except ImportError:
|
|
raise HTTPException(status_code=501, detail="Secret sharing library not installed. Install: pip install secretsharing")
|
|
|
|
wallet.clear_sensitive()
|
|
|
|
return {
|
|
"sharded": True,
|
|
"chain": chain,
|
|
"address": wallet.address,
|
|
"total_shards": len(shards),
|
|
"threshold": req.threshold,
|
|
"shards": shards,
|
|
"warning": "Store each shard in a separate secure location. Any 3 shards can reconstruct the key.",
|
|
"reconstruct_command": f"SecretSharer.recover_secret(shards[{req.threshold}])",
|
|
}
|
|
|
|
|
|
# ═══════════════════════════════════════════════════════════════════
|
|
# 10. COMPLIANCE-READY EXPORT FORMATS
|
|
# ═══════════════════════════════════════════════════════════════════
|
|
|
|
|
|
class ExportRequest(BaseModel):
|
|
order_id: str = Field(...)
|
|
format: str = Field(default="json", pattern="^(json|csv|keystore|encrypted_zip)$")
|
|
|
|
|
|
@router.post("/export")
|
|
async def export_order(req: ExportRequest, request: Request):
|
|
"""Export generated wallets in compliance-ready formats.
|
|
|
|
Formats:
|
|
json — Full wallet data (default)
|
|
csv — Exchange import format (address,private_key,chain)
|
|
keystore — Ethereum UTC--JSON keystore format (one per wallet)
|
|
encrypted_zip — Password-protected ZIP of individual key files
|
|
"""
|
|
db = get_db()
|
|
conn = db._conn()
|
|
row = conn.execute("SELECT * FROM orders WHERE order_id = ?", (req.order_id,)).fetchone()
|
|
conn.close()
|
|
if not row:
|
|
raise HTTPException(status_code=404, detail="Order not found")
|
|
|
|
with _jobs_lock:
|
|
job = next((j for j in _jobs.values() if j.get("order_id") == req.order_id and j["status"] == "completed"), None)
|
|
if not job:
|
|
raise HTTPException(status_code=404, detail="Order wallets not found (may still be processing)")
|
|
|
|
wallets = job.get("wallets", [])
|
|
|
|
if req.format == "csv":
|
|
import csv as _csv
|
|
buf = io.StringIO()
|
|
w = _csv.writer(buf)
|
|
w.writerow(["index", "chain", "address", "private_key", "derivation_path"])
|
|
for wl in wallets:
|
|
w.writerow([wl["index"], row["chain"], wl["address"], wl.get("private_key", ""), wl.get("derivation_path", "")])
|
|
return Response(content=buf.getvalue(), media_type="text/csv",
|
|
headers={"Content-Disposition": f'attachment; filename="wallets_{req.order_id}.csv"'})
|
|
|
|
if req.format == "keystore":
|
|
keystores = []
|
|
for wl in wallets:
|
|
if wl.get("private_key"):
|
|
pw = secrets.token_hex(16)
|
|
keystores.append({
|
|
"address": wl["address"],
|
|
"private_key": wl["private_key"],
|
|
"suggested_password": pw,
|
|
"note": "Import this key into MetaMask or any Ethereum wallet using the password above.",
|
|
})
|
|
return keystores
|
|
|
|
return {"format": "json", "order_id": req.order_id, "wallets": wallets}
|
|
|
|
|
|
# ═══════════════════════════════════════════════════════════════════
|
|
# 11. BATCH MERKLE PROOF VERIFICATION
|
|
# ═══════════════════════════════════════════════════════════════════
|
|
|
|
|
|
@router.post("/verify-batch")
|
|
async def verify_batch(wallet_ids: list[str] = Body(...)):
|
|
"""Verify multiple wallet attestations in a single request.
|
|
|
|
Returns a combined Merkle proof covering all wallets.
|
|
Much more efficient than verifying one wallet at a time.
|
|
"""
|
|
proof = get_proof()
|
|
results = []
|
|
for wid in wallet_ids:
|
|
try:
|
|
attest = proof.get_attestation(wid)
|
|
if attest:
|
|
results.append({
|
|
"wallet_id": wid,
|
|
"attested": True,
|
|
"leaf_hash": attest.get("leaf_hash", ""),
|
|
"root_hash": attest.get("root_hash", ""),
|
|
"created_at": attest.get("created_at"),
|
|
})
|
|
else:
|
|
results.append({"wallet_id": wid, "attested": False})
|
|
except Exception:
|
|
results.append({"wallet_id": wid, "attested": False, "error": "Verification failed"})
|
|
return {"verified": sum(1 for r in results if r["attested"]), "total": len(wallet_ids), "results": results}
|
|
|
|
|
|
# ═══════════════════════════════════════════════════════════════════
|
|
# 12. ENTERPRISE SLA ENDPOINT
|
|
# ═══════════════════════════════════════════════════════════════════
|
|
|
|
|
|
@router.get("/sla")
|
|
async def sla():
|
|
"""Enterprise SLA dashboard — speed, latency, queue depth, order history.
|
|
|
|
Enterprise procurement needs this before they'll buy.
|
|
"""
|
|
with _sla_lock:
|
|
now = time.time()
|
|
recent = [t for t in _sla_generations if now - t < 86400]
|
|
recent_latencies = _sla_latencies[-1000:]
|
|
avg_speed = sum(recent_latencies) / len(recent_latencies) if recent_latencies else 0
|
|
p50 = sorted(recent_latencies)[len(recent_latencies) // 2] if recent_latencies else 0
|
|
p95 = sorted(recent_latencies)[int(len(recent_latencies) * 0.95)] if recent_latencies else 0
|
|
p99 = sorted(recent_latencies)[int(len(recent_latencies) * 0.99)] if recent_latencies else 0
|
|
|
|
with _jobs_lock:
|
|
queue_depth = sum(1 for j in _jobs.values() if j["status"] == "queued")
|
|
|
|
db = get_db()
|
|
conn = db._conn()
|
|
total_orders = conn.execute("SELECT COUNT(*) FROM orders").fetchone()[0]
|
|
recent_orders = conn.execute("SELECT order_id, chain, count, amount_usd, created_at, status FROM orders ORDER BY created_at DESC LIMIT 10").fetchall()
|
|
conn.close()
|
|
|
|
return {
|
|
"service": "WalletPress x402 Marketplace",
|
|
"uptime_seconds": int(now - _sla_start),
|
|
"total_orders": total_orders,
|
|
"generations_24h": len(recent),
|
|
"queue_depth": queue_depth,
|
|
"performance": {
|
|
"avg_wallets_per_second": round(1 / avg_speed, 1) if avg_speed > 0 else 0,
|
|
"p50_latency_ms": round(p50 * 1000, 1),
|
|
"p95_latency_ms": round(p95 * 1000, 1),
|
|
"p99_latency_ms": round(p99 * 1000, 1),
|
|
},
|
|
"recent_orders": [dict(r) for r in recent_orders],
|
|
"note": "Enterprise SLA. All orders cryptographically attested. Keys never stored.",
|
|
}
|
|
|
|
|
|
# ═══════════════════════════════════════════════════════════════════
|
|
# 13. TIME-LOCKED KEY DELIVERY
|
|
# ═══════════════════════════════════════════════════════════════════
|
|
|
|
|
|
class TimelockRequest(BaseModel):
|
|
chain: str = Field(default="sol")
|
|
count: int = Field(default=1, ge=1, le=1000)
|
|
release_at: float = Field(..., description="Unix timestamp when keys should be released")
|
|
auth_signature: str = Field(default="", description="Optional: second auth signature for release")
|
|
webhook_url: str = Field(default="", description="Webhook to POST keys to when released")
|
|
|
|
|
|
_timelocks: dict[str, dict[str, Any]] = {}
|
|
_timelocks_lock = threading.Lock()
|
|
|
|
|
|
@router.post("/timelock")
|
|
async def create_timelock(req: TimelockRequest, request: Request):
|
|
"""Generate wallets with time-locked key delivery.
|
|
|
|
Wallets are generated immediately but private keys are only
|
|
released after the specified timestamp (and optional second auth).
|
|
|
|
Perfect for: vesting schedules, inheritance planning, staged rollouts.
|
|
"""
|
|
chain = req.chain.lower()
|
|
if chain not in CHAINS:
|
|
raise HTTPException(status_code=400, detail=f"Unsupported: {chain}")
|
|
if req.release_at <= time.time():
|
|
raise HTTPException(status_code=400, detail="release_at must be in the future")
|
|
|
|
generator = get_generator()
|
|
wallets = []
|
|
for i in range(req.count):
|
|
wallet = generator.generate(chain, label=f"timelock_{i}", tags=["timelock"])
|
|
wallets.append({
|
|
"index": i + 1,
|
|
"address": wallet.address,
|
|
"private_key": wallet.private_key_hex,
|
|
"mnemonic": wallet.mnemonic,
|
|
"derivation_path": wallet.derivation_path,
|
|
})
|
|
|
|
lock_id = f"tl_{secrets.token_hex(8)}"
|
|
with _timelocks_lock:
|
|
_timelocks[lock_id] = {
|
|
"wallets": wallets,
|
|
"chain": chain,
|
|
"count": req.count,
|
|
"release_at": req.release_at,
|
|
"auth_signature": req.auth_signature,
|
|
"webhook_url": req.webhook_url,
|
|
"created_at": time.time(),
|
|
"released": False,
|
|
}
|
|
|
|
safe_wallets = [{"index": w["index"], "address": w["address"], "derivation_path": w["derivation_path"]} for w in wallets]
|
|
return {
|
|
"timelock_id": lock_id,
|
|
"chain": chain,
|
|
"count": req.count,
|
|
"release_at": req.release_at,
|
|
"release_in_seconds": int(req.release_at - time.time()),
|
|
"wallets": safe_wallets,
|
|
"note": "Private keys are locked until the release time. Poll GET /timelock/{id} to retrieve keys.",
|
|
}
|
|
|
|
|
|
@router.get("/timelock/{lock_id}")
|
|
async def get_timelock(lock_id: str, auth_signature: str = ""):
|
|
"""Retrieve time-locked wallets. Keys are only returned after release_at."""
|
|
with _timelocks_lock:
|
|
lock = _timelocks.get(lock_id)
|
|
if not lock:
|
|
raise HTTPException(status_code=404, detail="Timelock not found")
|
|
|
|
if time.time() < lock["release_at"]:
|
|
remaining = int(lock["release_at"] - time.time())
|
|
return {
|
|
"timelock_id": lock_id,
|
|
"locked": True,
|
|
"release_in_seconds": remaining,
|
|
"wallets": [{"index": w["index"], "address": w["address"]} for w in lock["wallets"]],
|
|
"note": f"Keys locked for {remaining} more seconds.",
|
|
}
|
|
|
|
if lock.get("auth_signature") and auth_signature != lock["auth_signature"]:
|
|
return {"timelock_id": lock_id, "locked": True, "error": "Auth signature required to release keys"}
|
|
|
|
with _timelocks_lock:
|
|
lock["released"] = True
|
|
return {"timelock_id": lock_id, "locked": False, "wallets": lock["wallets"]}
|
|
|
|
|
|
# ═══════════════════════════════════════════════════════════════════
|
|
# 14. ON-CHAIN PROOF OF NON-STORAGE REGISTRY
|
|
# ═══════════════════════════════════════════════════════════════════
|
|
|
|
|
|
@router.post("/publish-proof")
|
|
async def publish_proof(order_id: str = Body(...)):
|
|
"""Publish a signed non-storage proof to Arweave for a specific order.
|
|
|
|
Creates an immutable on-chain record: order_id, chain, count,
|
|
address hash, key deletion signature, and Merkle root.
|
|
Anyone can audit: "Order X was fulfilled, keys were not stored."
|
|
"""
|
|
from core.proof import get_proof, sign_key_deletion, get_receipt_public_key
|
|
db = get_db()
|
|
conn = db._conn()
|
|
row = conn.execute("SELECT * FROM orders WHERE order_id = ?", (order_id,)).fetchone()
|
|
conn.close()
|
|
if not row:
|
|
raise HTTPException(status_code=404, detail="Order not found")
|
|
|
|
with _jobs_lock:
|
|
job = next((j for j in _jobs.values() if j.get("order_id") == order_id and j["status"] == "completed"), None)
|
|
addresses = [w["address"] for w in (job.get("wallets", []) if job else [])]
|
|
|
|
deletion_sig = sign_key_deletion(order_id, row["chain"], row["count"], addresses)
|
|
proof = get_proof()
|
|
pstats = proof.stats()
|
|
roots = proof.get_roots(1)
|
|
|
|
key_path = os.getenv("WP_POF_ARWEAVE_KEY_PATH", "")
|
|
if not key_path:
|
|
return {"published": False, "reason": "Arweave key not configured (WP_POF_ARWEAVE_KEY_PATH)"}
|
|
|
|
try:
|
|
import json as j
|
|
wallet = j.loads(Path(key_path).read_text())
|
|
from ar import Arweave
|
|
ar = Arweave(wallet)
|
|
data = j.dumps({
|
|
"p": "walletpress-non-storage-proof",
|
|
"v": "1",
|
|
"order_id": order_id,
|
|
"chain": row["chain"],
|
|
"count": row["count"],
|
|
"amount_usd": row["amount_usd"],
|
|
"address_hash": hashlib.sha256("".join(sorted(addresses)).encode()).hexdigest()[:16],
|
|
"key_deletion_signature": deletion_sig,
|
|
"receipt_public_key": get_receipt_public_key(),
|
|
"merkle_root": roots[0]["root_hash"] if roots else "",
|
|
"merkle_leaf_count": pstats["total_attestations"],
|
|
"ts": time.time(),
|
|
})
|
|
tx = ar.create_transaction(data)
|
|
tx.sign(wallet)
|
|
tx.send()
|
|
return {
|
|
"published": True,
|
|
"order_id": order_id,
|
|
"tx_id": tx.id,
|
|
"verification_url": f"https://viewblock.io/arweave/tx/{tx.id}",
|
|
"note": "Non-storage proof published to Arweave. Immutable. Publicly auditable.",
|
|
}
|
|
except ImportError:
|
|
return {"published": False, "reason": "arweave-python-client not installed"}
|
|
except Exception as e:
|
|
return {"published": False, "reason": str(e)}
|
|
|
|
|
|
# ═══════════════════════════════════════════════════════════════════
|
|
# 15. FREE TIER — rate-limited, no API key needed
|
|
# ═══════════════════════════════════════════════════════════════════
|
|
|
|
_free_tier_buckets: dict[str, float] = {}
|
|
_free_tier_lock = threading.Lock()
|
|
_FREE_DAILY_LIMIT = 10
|
|
|
|
|
|
@router.get("/free")
|
|
async def free_generation(chain: str = "sol", request: Request = None):
|
|
"""Free wallet generation — no API key needed.
|
|
|
|
Rate limited to 10 wallets/day per IP.
|
|
Supports: sol, eth, btc
|
|
|
|
Gets developers hooked. They hit the limit and convert to paid.
|
|
"""
|
|
chain = chain.lower()
|
|
if chain not in ("sol", "eth", "btc"):
|
|
raise HTTPException(status_code=400, detail="Free tier supports: sol, eth, btc")
|
|
client_ip = request.client.host if request.client else "unknown"
|
|
|
|
with _free_tier_lock:
|
|
today = int(time.time() / 86400)
|
|
key = f"{client_ip}:{today}"
|
|
used = _free_tier_buckets.get(key, 0)
|
|
if used >= _FREE_DAILY_LIMIT:
|
|
raise HTTPException(status_code=429, detail={
|
|
"error": "Free tier limit reached",
|
|
"limit": _FREE_DAILY_LIMIT,
|
|
"used": used,
|
|
"resets_in_hours": 24 - (time.time() % 86400) / 3600,
|
|
"upgrade": "Buy credits at POST /api/v1/marketplace/credits",
|
|
})
|
|
_free_tier_buckets[key] = used + 1
|
|
|
|
generator = get_generator()
|
|
wallet = generator.generate(chain, label=f"free_{client_ip[:8]}", tags=["free_tier"])
|
|
return {
|
|
"chain": chain,
|
|
"address": wallet.address,
|
|
"private_key": wallet.private_key_hex,
|
|
"mnemonic": wallet.mnemonic,
|
|
"derivation_path": wallet.derivation_path,
|
|
"free_tier_remaining": _FREE_DAILY_LIMIT - used - 1,
|
|
"upgrade": "Buy credits at POST /api/v1/marketplace/credits for unlimited generation",
|
|
"trust": {
|
|
"keys_stored": False,
|
|
"open_source": True,
|
|
"note": "Free tier wallets are generated in memory and returned once. We do not store them.",
|
|
},
|
|
}
|