walletpress/backend/agent/mcp_server.py
Rug Munch Media LLC 8b8e85a24d
fix(security): WP-002..WP-006 — remaining P0 fixes
Five P0 security/correctness bugs fixed in this commit:

WP-002 (P0-1) — Free x402 credits
  x402_service.buy_credits() previously accepted any non-empty payment_tx
  and minted credits for free. Now calls verify_payment() (the same
  on-chain USDC verifier used by /generate) before crediting.
  Direct theft vector closed.

WP-003 (P0-2) — Unsalted SHA-256 hosted passwords
  hosting.py used hashlib.sha256(password.encode()).hexdigest() with
  no salt — rainbow-table crackable. Replaced with argon2-cffi
  PasswordHasher (OWASP 2024 params: m=64MB, t=3, p=4).
  Legacy SHA-256 hashes are verified (for backwards compat) and
  transparently migrated to Argon2id on next successful login.
  login() now also strips password_hash from the response (P2-11).

WP-004 (P0-3) — In-memory _team_keys + no role enforcement
  main.py's _team_keys dict was module-level, lost on restart, and
  the middleware didn't enforce roles. Replaced with persistent
  KeyStore (survives restarts), added role field (admin/operator/viewer)
  to APIKey dataclass, ROLE_HIERARCHY constant, and require_role()
  helper. Middleware now:
    - attaches verified APIKey to request.state.api_key_obj
    - rejects mutation requests with viewer role (was the bypass)
    - verifies + attaches for /api/v1/team/* GET endpoints too
  KeyStore._save() now uses atomic temp-file rename (no more partial
  writes) and uses a threading.Lock for safety. P1-1 (verify saves
  on every read) also fixed: last_used_at is now in-memory only,
  flushed via flush_last_used().

WP-005 (P0-5) — Env-only vault KEK
  config.py now resolves the vault KEK in priority order:
    1. WP_VAULT_PASSWORD env var (legacy, dev only)
    2. ~/.walletpress/vault.key file (mode 0600)
    3. {data_dir}/vault.key file (mode 0600)
    4. Auto-generate on first run, persisted to vault.key
  Sources 2-4 are preferred — env vars leak into logs and process
  listings. Added WP_REQUIRE_KEY_FILE=1 to refuse startup without a
  KEK in production. _check_key_file_perms() warns on loose perms
  but doesn't fail (operators should chmod 600).

WP-006 (P0-6) — wallet_sweep doesn't sweep
  The MCP tool claimed to sweep funds but only returned an intent.
  Now actually builds, signs, and broadcasts EVM transactions:
    - Decrypts private key from vault via get_generator
    - Connects to chain RPC via Web3
    - Builds tx: balance - gas_cost to destination
    - Signs with eth_account
    - Broadcasts via existing _evm_broadcast helper
    - Records spending + audit log entry
  Non-EVM chains (Solana, TRON, BTC family) still return intent +
  clear 'not implemented' notice — to be added as separate PRs.

  Same fix applied to DCA scheduler (_exec_dca): when both
  from_wallet_id and to_address are set, actually broadcasts
  via _evm_sweep instead of just logging.

Test results:
  pytest tests/ tests/test_address_vectors.py
  # 80 + 22 = 102 passed, 5 skipped, no regressions

Refs: AUDIT.md, SECURITY.md, BUILDER.md
Closes: WP-002, WP-003, WP-004, WP-005, WP-006
2026-06-30 20:20:39 +07:00

1051 lines
37 KiB
Python

"""WalletPress AI Agent MCP Server — All wallet operations as AI-callable tools.
Every tool is a @tool decorator. The MCP server can be:
- Run standalone via `uvicorn agent.mcp_server:mcp_app` or `python -m agent.mcp_server`
- Mounted as a FastAPI sub-app in main.py
- Connected to any MCP client (Claude Code, opencode, Cursor, etc.)
"""
from __future__ import annotations
import logging
import time
from typing import Any
from mcp.server.fastmcp import FastMCP
from core.config import cfg
from core.vault import WalletEntry, get_vault
from core.agent_safety import (
check_action_allowed, is_write_action, is_killed,
request_confirmation, confirm_action, reject_action, list_pending,
check_address, check_spending_limit, record_spending,
audit_log, simulate_transaction,
kill_agent as safety_kill, resurrect_agent as safety_resurrect,
allowlist_add, blocklist_add, address_book_list, address_book_remove,
list_limits, audit_trail, verify_audit_chain,
)
logger = logging.getLogger("wp.agent.mcp")
mcp = FastMCP("WalletPress AI Agent", log_level="WARNING")
def _safety_check(action: str, params: dict) -> dict | None:
"""Run safety checks for a WRITE action. Returns error dict or None if OK."""
# Kill switch
if is_killed():
return {"error": "Agent is emergency stopped. Use agent_resurrect() to re-enable."}
# Permission tier
allowed, reason = check_action_allowed(action)
if not allowed:
return {"error": reason}
# All checks passed
return None
import threading
_HITL_SKIP_LOCAL = threading.local()
def _is_hitl_skip() -> bool:
return getattr(_HITL_SKIP_LOCAL, "skip", False)
def _set_hitl_skip(val: bool) -> None:
_HITL_SKIP_LOCAL.skip = val
def _write_with_hitl(action: str, params: dict, reason: str = "") -> dict | None:
"""Run safety checks for a WRITE action.
Returns:
None if checks pass and the action should proceed directly.
dict with error or confirmation request if action is blocked or needs HITL.
"""
if _is_hitl_skip():
return None # Called from agent_confirm — skip safety, execute directly
# Kill switch
if is_killed():
return {"error": "Agent is emergency stopped. Use agent_resurrect() to re-enable."}
# Permission tier
allowed, reason_tier = check_action_allowed(action)
if not allowed:
return {"error": reason_tier}
# Request HITL confirmation (always for WRITE actions)
result = request_confirmation(action, params, reason)
audit_log(action, params, {"status": "pending_confirmation"}, confirmed_by="")
return {
"status": "pending_confirmation",
"confirmation_id": result["confirmation_id"],
"action": action,
"reason": reason,
"expires_at": result["expires_at"],
"confirm_command": f"Use agent_confirm('{result['confirmation_id']}') to approve",
"reject_command": f"Use agent_reject('{result['confirmation_id']}') to reject",
}
def _confirmed_call(action: str, params: dict, fn, *args, **kwargs):
"""Execute a tool function inside a confirmed HITL flow.
Sets the per-thread skip flag so the tool runs without requesting
confirmation again. Thread-safe under concurrent requests.
"""
_set_hitl_skip(True)
try:
return fn(*args, **kwargs)
finally:
_set_hitl_skip(False)
# ── SAFETY & HITL TOOLS ──────────────────────────────────
@mcp.tool()
def agent_confirm(confirmation_id: str) -> dict:
"""Confirm a pending action and execute it.
After reviewing a pending confirmation, call this to approve execution.
Returns the result of the underlying action.
Args:
confirmation_id: The ID from a previous agent_execute or write request
"""
result = confirm_action(confirmation_id)
if "error" in result:
return result
action = result["action"]
params = result["params"]
# Execute the approved action inside HITL-skip mode
from agent.mcp_server import wallet_generate, wallet_from_mnemonic, vault_delete
from agent.mcp_server import wallet_rotate, wallet_sweep
from agent.mcp_server import schedule_dca, schedule_remove, anomaly_monitor
TOOL_MAP = {
"wallet_generate": lambda: _confirmed_call(action, params, wallet_generate, **params),
"wallet_from_mnemonic": lambda: _confirmed_call(action, params, wallet_from_mnemonic, **params),
"vault_delete": lambda: _confirmed_call(action, params, vault_delete, **params),
"wallet_rotate": lambda: _confirmed_call(action, params, wallet_rotate, **params),
"wallet_sweep": lambda: _confirmed_call(action, params, wallet_sweep, **params),
"schedule_dca": lambda: _confirmed_call(action, params, schedule_dca, **params),
"schedule_remove": lambda: _confirmed_call(action, params, schedule_remove, **params),
"anomaly_monitor": lambda: _confirmed_call(action, params, anomaly_monitor, **params),
}
fn = TOOL_MAP.get(action)
if not fn:
return {"error": f"No confirmation handler for action: {action}"}
audit_log(action, params, {"status": "executing"}, confirmed_by="human")
try:
exec_result = fn()
audit_log(action, params, exec_result, confirmed_by="human")
return {"confirmed": True, "action": action, "result": exec_result}
except Exception as e:
err_msg = f"{type(e).__name__}: {e}"
audit_log(action, params, {"error": err_msg}, confirmed_by="human")
return {"confirmed": True, "action": action, "error": err_msg}
@mcp.tool()
def agent_reject(confirmation_id: str, reason: str = "") -> dict:
"""Reject a pending action without executing it.
Args:
confirmation_id: The ID from a previous agent_execute or write request
reason: Optional reason for rejection
"""
result = reject_action(confirmation_id, reason)
if "error" in result:
return result
audit_log("agent_reject", {"confirmation_id": confirmation_id}, result)
return result
@mcp.tool()
def agent_pending() -> list[dict]:
"""List all pending confirmation requests waiting for your approval."""
return list_pending()
@mcp.tool()
def agent_kill(reason: str = "") -> dict:
"""Emergency stop the agent immediately.
All pending plans are canceled. Agent goes into read-only mode.
Returns a resurrection key needed to re-enable the agent.
Args:
reason: Why are you killing the agent?
"""
result = safety_kill(reason)
audit_log("agent_kill", {"reason": reason}, result)
return result
@mcp.tool()
def agent_resurrect(key: str) -> dict:
"""Re-enable a killed agent using the resurrection key.
Args:
key: The resurrection key from agent_kill()
"""
result = safety_resurrect(key)
if "error" not in result:
audit_log("agent_resurrect", {}, result)
return result
@mcp.tool()
def agent_limits() -> dict:
"""Show current spending limits and usage across all chains."""
return list_limits()
@mcp.tool()
def agent_simulate(chain: str, from_address: str, to_address: str, amount: float) -> dict:
"""Simulate a transaction to see gas costs and expected outcome before executing.
Args:
chain: Chain key (eth, sol, etc.)
from_address: Source wallet address
to_address: Destination address
amount: Amount in native tokens
"""
return simulate_transaction(chain, from_address, to_address, amount)
@mcp.tool()
def agent_referral_status() -> dict:
"""Show the agent's revenue/referral configuration.
In FREEMIUM mode, the agent uses Rug Munch Media's referral codes
on every swap/trade — this costs you nothing and helps fund development.
In SELF-REFERRAL mode, you keep 100% of referral revenue.
Shows which platforms are configured, which need wallet setup,
and what revenue share each platform provides.
"""
import os
from plugins.defi import REVENUE_MODE, REF_JUPITER_WALLET, REF_HYPERLIQUID_WALLET
from plugins.defi import REF_GMGN, REF_ODINBOT, REF_BANANA, REF_AXIOM, REF_PADRE
platforms = [
{"platform": "Jupiter", "type": "fee_account", "share": "50bps", "configured": bool(REF_JUPITER_WALLET), "wallet": REF_JUPITER_WALLET or "NOT SET", "docs": "https://jup.ag/referral"},
{"platform": "Hyperliquid", "type": "builder_code", "share": "up to 0.1%/trade", "configured": bool(REF_HYPERLIQUID_WALLET), "wallet": REF_HYPERLIQUID_WALLET or "NOT SET", "docs": "https://hyperliquid.gitbook.io/hyperliquid-docs/trading/builder-codes.md"},
{"platform": "GMGN", "type": "code", "share": "40%", "configured": True, "code": REF_GMGN, "setup": "Use code at gmgn.ai"},
{"platform": "OdinBot", "type": "code", "share": "40%", "configured": True, "code": REF_ODINBOT, "setup": "Use code at t.me/OdinBot"},
{"platform": "Banana Gun", "type": "code", "share": "40%", "configured": True, "code": REF_BANANA, "setup": "Use code at t.me/BananaGunBot"},
{"platform": "Axiom", "type": "code", "share": "30%", "configured": True, "code": REF_AXIOM, "setup": "Use code at t.me/AxiomTrade"},
{"platform": "Padre", "type": "code", "share": "35%", "configured": True, "code": REF_PADRE, "setup": "Use code at t.me/PadreBot"},
]
freemium_note = ""
if REVENUE_MODE == "freemium":
configured = sum(1 for p in platforms if p.get("configured"))
unconfigured = sum(1 for p in platforms if not p.get("configured"))
freemium_note = f"{configured} platforms sending revenue, {unconfigured} need wallet setup (Jupiter + Hyperliquid). Set WP_REF_REVENUE_MODE=self to keep your own referral revenue."
return {
"revenue_mode": REVENUE_MODE.upper(),
"your_cost": "free (we cover referral fees)" if REVENUE_MODE == "freemium" else "you control your own codes",
"platforms": platforms,
"note": freemium_note,
"how_to_configure": "Jupiter: set WP_REF_JUPITER_WALLET (fee account). Hyperliquid: set WP_REF_HYPERLIQUID_WALLET (builder wallet, needs 100 USDC in perps).",
}
@mcp.tool()
def agent_audit(limit: int = 50) -> list[dict]:
"""View the agent audit trail — every action logged with SHA-256 chain.
Args:
limit: Number of recent entries to show (default 50)
"""
return audit_trail(limit)
@mcp.tool()
def agent_audit_verify() -> dict:
"""Verify the integrity of the entire audit trail.
Recomputes all SHA-256 hashes and checks the chain is unbroken.
"""
return verify_audit_chain()
@mcp.tool()
def address_allowlist(address: str, chain: str, label: str = "") -> dict:
"""Add an address to the agent's allowlist. The agent can only send to known addresses.
Args:
address: Wallet address to allow
chain: Chain key (eth, sol, btc, etc.)
label: Optional human-readable label (e.g. 'Exchange wallet')
"""
return allowlist_add(address, chain, label)
@mcp.tool()
def address_blocklist(address: str, chain: str, label: str = "") -> dict:
"""Add an address to the blocklist. The agent will never send funds here.
Args:
address: Wallet address to block
chain: Chain key
label: Reason for blocking
"""
return blocklist_add(address, chain, label)
@mcp.tool()
def address_list(list_type: str = "") -> list[dict]:
"""List addresses in the agent's address book.
Args:
list_type: 'allowlist', 'blocklist', or '' for all
"""
return address_book_list(list_type)
@mcp.tool()
def address_check(address: str, chain: str) -> dict:
"""Check if an address is safe to send funds to.
Checks against: user's blocklist, known scam database, and
optionally the allowlist (if WP_AGENT_REQUIRE_ALLOWLIST=1).
Args:
address: Wallet address to check
chain: Chain key
"""
return check_address(address, chain)
# ── WALLET GENERATION ──────────────────────────────────────
@mcp.tool()
def wallet_generate(chain: str, label: str = "", count: int = 1) -> dict:
"""Generate one or more wallets for any supported chain.
Args:
chain: Chain key (eth, sol, btc, trx, bsc, polygon, base, ...)
label: Optional human-readable label
count: Number of wallets to generate (default 1, max 100)
"""
hitl = _write_with_hitl("wallet_generate", {"chain": chain, "label": label, "count": count},
f"Generate {count} wallet(s) on {chain}")
if "confirmation_id" in hitl:
return hitl
from wallet_engine.generator import get_generator
vault = get_vault()
generator = get_generator(cfg.vault_password)
wallets = []
for i in range(count):
lbl = label or f"{chain}_{i + 1}_{int(time.time())}"
try:
wallet = generator.generate(chain, label=lbl)
except ValueError as e:
return {"error": str(e)}
entry = WalletEntry(
id=f"wp_agent_{chain}_{int(time.time() * 1000)}_{i}",
chain=wallet.chain, address=wallet.address, label=wallet.label,
tags=wallet.tags, created_at=wallet.created_at,
encrypted_key=wallet.private_key_hex if wallet.encrypted else "",
public_key=wallet.public_key_hex,
derivation_path=wallet.derivation_path,
encrypted=wallet.encrypted,
)
vault.put(entry)
wallets.append({"id": entry.id, "chain": entry.chain, "address": entry.address, "label": entry.label})
return {"generated": len(wallets), "wallets": wallets}
@mcp.tool()
def wallet_from_mnemonic(mnemonic: str, chains: list[str] | None = None) -> dict:
"""Derive wallets from a BIP39 mnemonic phrase.
Args:
mnemonic: BIP39 seed phrase (12 or 24 words)
chains: List of chains to derive (empty = all 55 chains)
"""
hitl = _write_with_hitl("wallet_from_mnemonic", {"chains": chains},
"Derive wallets from a mnemonic phrase")
if hitl:
return hitl
from wallet_engine.chains import CHAINS
from wallet_engine.generator import get_generator
vault = get_vault()
generator = get_generator(cfg.vault_password)
chain_list = chains or list(CHAINS.keys())
results = {}
for ck in chain_list:
try:
wallet = generator.generate(ck, mnemonic=mnemonic, label=f"mnemonic_{ck}")
except ValueError:
results[ck] = {"error": "chain not supported"}
continue
entry = WalletEntry(
id=f"wp_mcp_mnemonic_{ck}_{int(time.time())}",
chain=wallet.chain, address=wallet.address, label=wallet.label,
tags=wallet.tags + ["from_mnemonic"], created_at=wallet.created_at,
encrypted_key=wallet.private_key_hex if wallet.encrypted else "",
public_key=wallet.public_key_hex, derivation_path=wallet.derivation_path,
encrypted=wallet.encrypted,
)
vault.put(entry)
results[ck] = {"address": entry.address, "wallet_id": entry.id}
return {"derived_for": len(results), "chains": results}
# ── VAULT MANAGEMENT ───────────────────────────────────────
@mcp.tool()
def vault_list(chain: str = "", limit: int = 50) -> list[dict]:
"""List wallets in the vault (no private keys returned).
Args:
chain: Optional filter by chain
limit: Max results (default 50)
"""
vault = get_vault()
wallets = vault.list(chain=chain, limit=limit) if chain else vault.list(limit=limit)
return [{
"id": w.id, "chain": w.chain, "address": w.address,
"label": w.label, "tags": w.tags, "created_at": w.created_at,
"encrypted": w.encrypted, "balance_usd": w.balance_usd,
} for w in wallets]
@mcp.tool()
def vault_get(wallet_id: str) -> dict:
"""Get full wallet details including private key (requires vault password configured).
Args:
wallet_id: Wallet ID from vault_list
"""
vault = get_vault()
wallet = vault.get(wallet_id)
if not wallet:
return {"error": f"Wallet {wallet_id} not found"}
result = {
"id": wallet.id, "chain": wallet.chain, "address": wallet.address,
"label": wallet.label, "tags": wallet.tags, "created_at": wallet.created_at,
"public_key": wallet.public_key, "derivation_path": wallet.derivation_path,
"encrypted": wallet.encrypted, "balance_usd": wallet.balance_usd,
}
if wallet.encrypted_key and wallet.encrypted and cfg.vault_password:
from wallet_engine.generator import get_generator
generator = get_generator(cfg.vault_password)
result["private_key"] = generator.decrypt_key(wallet.encrypted_key)
elif wallet.encrypted_key:
result["private_key"] = wallet.encrypted_key
return result
@mcp.tool()
def vault_delete(wallet_id: str) -> dict:
"""Delete a wallet permanently.
Args:
wallet_id: Wallet ID to delete
"""
hitl = _write_with_hitl("vault_delete", {"wallet_id": wallet_id},
f"Permanently delete wallet {wallet_id}")
if hitl:
return hitl
vault = get_vault()
if vault.delete(wallet_id):
return {"deleted": True, "wallet_id": wallet_id}
return {"error": f"Wallet {wallet_id} not found"}
@mcp.tool()
def vault_stats() -> dict:
"""Get vault statistics — total wallets, encrypted count, chains used, rotations."""
vault = get_vault()
return vault.stats()
# ── BALANCE & ANALYSIS ─────────────────────────────────────
@mcp.tool()
async def balance_check(chain: str, address: str) -> dict:
"""Check wallet balance on any supported chain.
Args:
chain: Chain key (eth, sol, btc, trx, ...)
address: Wallet address
"""
from routers.balance_fetcher import fetch_balance
return await fetch_balance(chain, address)
@mcp.tool()
def vault_balances(chain: str = "") -> dict:
"""Get balances for all wallets in the vault.
Args:
chain: Optional chain filter
"""
vault = get_vault()
wallets = vault.list(chain=chain, limit=500) if chain else vault.list(limit=500)
return {
"wallets": [{"id": w.id, "chain": w.chain, "address": w.address, "balance_usd": w.balance_usd} for w in wallets],
"total_usd": sum(w.balance_usd for w in wallets),
"wallet_count": len(wallets),
}
# ── WALLET MANAGEMENT ──────────────────────────────────────
@mcp.tool()
def wallet_rotate(wallet_id: str, reason: str = "agent_rotation") -> dict:
"""Rotate a wallet to a new address. Old wallet preserved with 'rotated' tag.
Args:
wallet_id: ID of existing wallet
reason: Rotation reason
"""
hitl = _write_with_hitl("wallet_rotate", {"wallet_id": wallet_id, "reason": reason},
f"Rotate keys for wallet {wallet_id}: {reason}")
if hitl:
return hitl
from wallet_engine.generator import get_generator
vault = get_vault()
old = vault.get(wallet_id)
if not old:
return {"error": f"Wallet {wallet_id} not found"}
generator = get_generator(cfg.vault_password)
new = generator.generate(old.chain, label=f"rotation_{old.chain}_{int(time.time())}", tags=["rotated", "active"])
new_entry = WalletEntry(
id=f"wp_rot_{old.chain}_{int(time.time())}",
chain=new.chain, address=new.address, label=new.label,
tags=new.tags, created_at=new.created_at,
encrypted_key=new.private_key_hex if new.encrypted else "",
public_key=new.public_key_hex, derivation_path=new.derivation_path,
encrypted=new.encrypted,
)
vault.put(new_entry)
vault.record_rotation(old.id, new.address, reason=reason)
return {
"rotated": True,
"old_wallet_id": old.id,
"old_address": old.address,
"new_wallet_id": new_entry.id,
"new_address": new_entry.address,
"chain": old.chain,
}
@mcp.tool()
def wallet_sweep(from_wallet_id: str, to_address: str) -> dict:
"""Sweep ALL funds from a vault wallet to an external address.
Implements actual on-chain transfer for EVM chains (Ethereum, Base,
Polygon, Arbitrum, Optimism, BNB Chain, Avalanche, Fantom, Gnosis, etc.).
For non-EVM chains (Solana, TRON, BTC family), returns a clear
"not implemented" error with the transaction params the caller can sign.
Security checks performed BEFORE signing:
1. Address allow/block list (block known scam addresses)
2. Spending limits (per-chain, per-period)
3. HITL confirmation (human must approve via agent_confirm)
Args:
from_wallet_id: Source wallet ID from vault
to_address: Destination address (must be valid for the chain)
"""
from core.balance_fetcher import EVM_RPC
# Address safety check
vault = get_vault()
wallet = vault.get(from_wallet_id)
chain = wallet.chain if wallet else "unknown"
addr_check = check_address(to_address, chain)
if not addr_check["allowed"]:
return {"error": f"Address blocked: {addr_check['reason']}"}
# Spending limit check
limit_check = check_spending_limit(chain, 0)
if not limit_check["allowed"]:
return {"error": f"Spending limit exceeded: {limit_check['exceeded']}"}
hitl = _write_with_hitl("wallet_sweep", {"from_wallet_id": from_wallet_id, "to_address": to_address},
f"Sweep funds from {from_wallet_id} to {to_address}")
if hitl:
return hitl
wallet = vault.get(from_wallet_id)
if not wallet:
return {"error": f"Wallet {from_wallet_id} not found"}
# EVM chain: build, sign, and broadcast via tx_broadcaster
if chain in EVM_RPC:
return _evm_sweep(wallet, to_address, chain)
# Non-EVM: return intent + clear "not implemented" notice
return {
"status": "intent_only",
"chain": chain,
"wallet_id": from_wallet_id,
"from_address": wallet.address,
"to_address": to_address,
"note": (
f"Auto-broadcast not yet implemented for {chain}. "
f"Sign the transaction externally with the wallet's private key "
f"and broadcast via tx_broadcaster.broadcast()."
),
}
def _evm_sweep(wallet, to_address: str, chain: str) -> dict:
"""Build, sign, and broadcast an EVM sweep transaction.
Returns the tx_hash on success or an error dict on failure.
"""
import asyncio
from decimal import Decimal
from eth_account import Account
from web3 import Web3
from web3.exceptions import Web3Exception
from core.balance_fetcher import EVM_RPC
from routers.tx_broadcaster import _evm_broadcast
rpc_url = EVM_RPC.get(chain)
if not rpc_url:
return {"error": f"No RPC configured for chain {chain}"}
# Decrypt the private key from the vault
from wallet_engine.generator import get_generator
gen = get_generator(cfg.vault_password)
if not wallet.encrypted_key or not wallet.encrypted:
return {"error": f"Wallet {wallet.id} has no encrypted key (client-side only?)"}
try:
privkey_hex = gen.decrypt_key(wallet.encrypted_key)
except Exception as e:
return {"error": f"Failed to decrypt private key: {e}"}
try:
w3 = Web3(Web3.HTTPProvider(rpc_url, request_kwargs={"timeout": 15}))
if not w3.is_connected():
return {"error": f"Cannot connect to RPC for {chain}"}
account = Account.from_key(privkey_hex)
nonce = w3.eth.get_transaction_count(account.address)
gas_price = w3.eth.gas_price
chain_id = w3.eth.chain_id
# Get balance — sweep = balance - gas
balance_wei = w3.eth.get_balance(account.address)
# Estimate gas for a simple transfer (21k) + buffer
gas_limit = 21000
gas_cost_wei = gas_limit * gas_price
if balance_wei <= gas_cost_wei:
return {
"error": f"Insufficient balance to cover gas",
"balance_wei": balance_wei,
"gas_cost_wei": gas_cost_wei,
}
value_wei = balance_wei - gas_cost_wei
# Build the transaction
tx = {
"nonce": nonce,
"to": to_address,
"value": value_wei,
"gas": gas_limit,
"gasPrice": gas_price,
"chainId": chain_id,
}
# Sign
signed = account.sign_transaction(tx)
# Broadcast via existing tx_broadcaster helper (handles errors uniformly)
loop = asyncio.new_event_loop()
try:
result = loop.run_until_complete(_evm_broadcast(chain, signed.raw_transaction.hex()))
finally:
loop.close()
if result.get("broadcast"):
# Record the spending
try:
amount_eth = float(w3.from_wei(value_wei, "ether"))
record_spending(chain, amount_eth)
except Exception:
pass
audit_log(
"wallet_sweep",
{"from_wallet_id": wallet.id, "to_address": to_address, "chain": chain},
{"tx_hash": result["tx_hash"], "amount_wei": value_wei},
confirmed_by="human",
)
return {
"status": "broadcast",
"chain": chain,
"from_wallet_id": wallet.id,
"from_address": wallet.address,
"to_address": to_address,
"tx_hash": result["tx_hash"],
"amount_wei": value_wei,
"amount_native": float(w3.from_wei(value_wei, "ether")),
"gas_cost_wei": gas_cost_wei,
"explorer_url": f"https://etherscan.io/tx/{result['tx_hash']}" if chain == "eth" else None,
}
else:
return {
"error": f"Broadcast failed: {result.get('error', 'unknown')}",
"chain": chain,
}
except Web3Exception as e:
return {"error": f"Web3 error: {e}", "chain": chain}
except Exception as e:
return {"error": f"{type(e).__name__}: {e}", "chain": chain}
# ── CROSS-CHAIN ────────────────────────────────────────────
@mcp.tool()
def validate_address(chain: str, address: str) -> dict:
"""Validate if an address is structurally valid for a given chain.
Args:
chain: Chain key
address: Address to validate
"""
from wallet_engine.chains import validate_address as va, CHAINS
chain_info = CHAINS.get(chain.lower())
if not chain_info:
return {"error": f"Unsupported chain: {chain}"}
valid = va(chain, address)
return {
"chain": chain,
"address": address,
"valid": valid,
"expected_pattern": chain_info.address_pattern,
}
@mcp.tool()
def chains_list() -> dict:
"""List all 55 supported blockchains with metadata."""
from wallet_engine.chains import CHAINS, ChainFamily
families = set()
chain_list = []
for k, c in sorted(CHAINS.items()):
families.add(c.family.value)
chain_list.append({
"key": k, "name": c.name, "symbol": c.symbol,
"family": c.family.value, "curve": c.curve,
})
return {
"total_chains": len(chain_list),
"total_families": len(families),
"chains": chain_list,
}
# ── AGENT INTELLIGENCE ─────────────────────────────────────
@mcp.tool()
def agent_plan(prompt: str, context: str = "") -> dict:
"""Plan a multi-step wallet operation from natural language.
The LLM analyzes the prompt and returns a structured plan of tool calls
to execute. Use this for complex operations like:
- "sweep all ETH from my vault to 0x..."
- "generate 5 SOL wallets for my airdrop campaign"
- "rotate my oldest wallet and check the new balance"
- "tell me my total portfolio value across all chains"
Args:
prompt: Natural language instruction
context: Optional context (e.g. current vault state summary)
"""
from agent.orchestrator import plan_operation
return plan_operation(prompt, context)
@mcp.tool()
def agent_execute(plan: list[dict] | str, confirm: bool = False) -> dict:
"""Execute a pre-approved multi-step plan.
Args:
plan: The plan from agent_plan, or a JSON string of steps
confirm: If True, requires human confirmation before executing
"""
from agent.orchestrator import execute_plan
return execute_plan(plan, confirm=confirm)
# ── SCHEDULER ──────────────────────────────────────────────
@mcp.tool()
def schedule_dca(chain: str, amount: float, frequency: str = "weekly",
to_address: str = "", label: str = "") -> dict:
"""Set up a DCA (dollar-cost averaging) schedule.
Generates a wallet and periodically sends funds to it.
Args:
chain: Chain key
amount: Amount in native token per interval
frequency: 'daily', 'weekly', 'biweekly', 'monthly'
to_address: Optional destination (empty = generate new wallet each time)
label: Optional label for generated wallets
"""
hitl = _write_with_hitl("schedule_dca", {"chain": chain, "amount": amount, "frequency": frequency},
f"Set up {frequency} DCA of {amount} {chain}")
if hitl:
return hitl
from agent.scheduler import add_dca_task
return add_dca_task(chain, amount, frequency, to_address, label)
@mcp.tool()
def schedule_list() -> list[dict]:
"""List all active scheduled tasks (DCA, sweeps, monitors)."""
from agent.scheduler import scheduler
return scheduler.list_tasks()
@mcp.tool()
def schedule_remove(task_id: str) -> dict:
"""Remove a scheduled task.
Args:
task_id: ID from schedule_list
"""
hitl = _write_with_hitl("schedule_remove", {"task_id": task_id},
f"Remove scheduled task {task_id}")
if hitl:
return hitl
from agent.scheduler import scheduler
if scheduler.remove_task(task_id):
return {"removed": True, "task_id": task_id}
return {"error": f"Task {task_id} not found"}
# ── ANOMALY DETECTION ──────────────────────────────────────
@mcp.tool()
async def anomaly_scan(address: str, chain: str = "eth") -> dict:
"""Scan a wallet for anomalous activity.
Checks: unusually large transactions, new address interactions,
rapid-fire transactions, unusual timing.
Args:
address: Wallet address to scan
chain: Chain key
"""
from agent.detector import scan_wallet
return await scan_wallet(address, chain)
@mcp.tool()
def anomaly_monitor(wallet_id: str, webhook_url: str = "") -> dict:
"""Set up continuous monitoring for a wallet.
Triggers alerts on: large transfers, first-time interactions,
unusual volume spikes, rapid consecutive transactions.
Args:
wallet_id: Wallet ID from vault
webhook_url: Optional webhook for alerts
"""
from agent.scheduler import add_monitor_task
return add_monitor_task(wallet_id, webhook_url)
# ── PROOF OF GENERATION ────────────────────────────────────
@mcp.tool()
def proof_attestations() -> dict:
"""Get Proof of Generation statistics — total attestations, committed, uncommitted roots."""
from core.proof import get_proof
return get_proof().stats()
@mcp.tool()
def proof_commit_pending(chain: str = "local") -> dict:
"""Commit all pending attestations to a Merkle root.
Args:
chain: 'local' (SQLite, free), 'arweave' (permanent, ~$0.000001),
'ethereum' (on-chain, ~$5-50 gas)
"""
from core.proof import get_proof
proof = get_proof()
root_hash, count = proof.compute_merkle_root()
if not root_hash:
return {"committed": False, "reason": "No pending attestations"}
result = proof.commit(root_hash, count, commitment_chain=chain)
return {
"committed": True,
"root_hash": root_hash,
"attestation_count": count,
"chain": chain,
"commitment_tx": result.get("commitment_tx", ""),
"verification_url": result.get("verification_url", ""),
}
@mcp.tool()
def proof_verify_wallet(wallet_id: str) -> dict:
"""Verify a wallet's Proof of Generation attestation.
Args:
wallet_id: Wallet ID from vault_list
"""
from core.proof import get_proof
from core.vault import get_vault
vault = get_vault()
wallet = vault.get(wallet_id)
if not wallet:
return {"error": f"Wallet {wallet_id} not found"}
result = get_proof().verify(wallet_id, wallet.address, wallet.public_key)
return result
@mcp.tool()
def proof_provenance(wallet_id: str) -> dict:
"""Get the complete cryptographic provenance for a wallet.
Includes the Merkle proof path so it can be independently verified.
This is the wallet's birth certificate.
Args:
wallet_id: Wallet ID from vault_list
"""
from core.proof import get_proof
from core.vault import get_vault
vault = get_vault()
wallet = vault.get(wallet_id)
if not wallet:
return {"error": f"Wallet {wallet_id} not found"}
result = get_proof().get_proof_by_wallet(wallet_id)
return result
@mcp.tool()
def proof_roots(limit: int = 10) -> dict:
"""List recent Merkle roots committed for wallet attestations.
Args:
limit: Max roots to return (default 10)
"""
from core.proof import get_proof
proof = get_proof()
return {
"stats": proof.stats(),
"roots": proof.get_roots(limit),
}
@mcp.tool()
def verify_order(order_id: str) -> dict:
"""Verify an x402 marketplace order — proves we generated the wallets and didn't keep keys.
Returns the order details, receipt signature, and attestation proof.
Anyone can independently verify this order was fulfilled by WalletPress.
Args:
order_id: Order ID from the generate response (e.g. 'x402_abc123...')
"""
from core.db_pool import DbPool
from core.config import cfg
from core.proof import verify_receipt as _verify_receipt, get_receipt_public_key
pool = DbPool(cfg.data_dir / "marketplace.db")
row = pool.execute("SELECT * FROM orders WHERE order_id = ?", (order_id,)).fetchone()
if not row:
return {"error": f"Order {order_id} not found"}
pubkey = get_receipt_public_key()
return {
"order_id": order_id,
"chain": row["chain"],
"count": row["count"],
"amount_usd": row["amount_usd"],
"created_at": row["created_at"],
"status": row["status"],
"payment_method": "onchain" if row.get("payment_tx") and row["payment_tx"] != "free" else "free",
"receipt_public_key": pubkey,
"verification": {
"keys_stored": False,
"keys_returned_once": True,
"open_source": "https://github.com/cryptorugmuncher/walletpress",
"note": "This order was fulfilled by WalletPress. Keys were generated in memory and returned once.",
},
}
@mcp.tool()
def marketplace_transparency() -> dict:
"""Get the public transparency dashboard — stats, roots, guarantees.
Proves WalletPress is operating transparently. 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
],
},
"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",
],
}
# ── STANDALONE ─────────────────────────────────────────────
if __name__ == "__main__":
mcp.run()