rmi-backend/app/routers/wallet_manager_v2.py
cryptorugmunch 0a8c73d99b feat(domains): consolidate bulletin, intelligence, markets, admin, referral, mcp + mypy gate
- Make app/domains/auth/ and app/core/redis.py mypy-clean under strict.
- Add mypy-gate.ini and Makefile mypy-gate target; promote typecheck-gate in CI.
- Consolidate domains into app/domains/: bulletin, admin, intelligence, markets.
- Extract referral domain incl. DeFi partner DEX ref links; keep Telegram bot wired.
- Move app/mcp/ package and app/api/v1/mcp/router into app/domains/mcp/.
- Archive dead app/mcp_router.py.
2026-07-07 16:43:49 +07:00

926 lines
30 KiB
Python

"""
RMI Wallet Manager v2 API Router
===================================
Full REST API for enterprise wallet management.
Endpoints:
POST /api/v1/wallets/v2/generate - Generate new wallet
POST /api/v1/wallets/v2/generate/hd - Generate HD wallet
POST /api/v1/wallets/v2/generate/batch - Batch generate wallets
GET /api/v1/wallets/v2 - List wallets
GET /api/v1/wallets/v2/{wallet_id} - Get wallet details
PUT /api/v1/wallets/v2/{wallet_id} - Update wallet
DELETE /api/v1/wallets/v2/{wallet_id} - Archive wallet
POST /api/v1/wallets/v2/{wallet_id}/rotate - Rotate wallet
POST /api/v1/wallets/v2/{wallet_id}/schedule - Schedule rotation
POST /api/v1/wallets/v2/{wallet_id}/balance - Update balance
POST /api/v1/wallets/v2/{wallet_id}/x402 - Enable x402 payments
POST /api/v1/wallets/v2/{wallet_id}/subscription - Enable subscriptions
GET /api/v1/wallets/v2/stats - Wallet statistics
GET /api/v1/wallets/v2/alerts - Wallet alerts
GET /api/v1/wallets/v2/payments - Payment history
POST /api/v1/wallets/v2/payments - Record payment
GET /api/v1/wallets/v2/export - Export wallets
GET /api/v1/wallets/v2/rotations/due - Check rotations due
POST /api/v1/wallets/v2/rotations/process - Process due rotations
"""
import logging
import os
from datetime import UTC, datetime
from typing import Any
from fastapi import APIRouter, Body, HTTPException, Request
from pydantic import BaseModel, Field
from app.domains.admin import AuditLogger, require_admin
from app.wallet_manager_v2 import (
CHAIN_REGISTRY,
PaymentRecord,
WalletManagerV2,
get_wallet_manager_v2,
import_legacy_wallets,
)
logger = logging.getLogger("wallet_api_v2")
router = APIRouter(prefix="/api/v1/wallets/v2", tags=["wallet-manager-v2"])
# ── Models ────────────────────────────────────────────────────
class GenerateWalletRequest(BaseModel):
chain: str = Field(..., description="Chain key (eth, sol, trx, btc, etc.)")
name: str = Field(default="", description="Wallet name")
purpose: str = Field(default="operations", description="Wallet purpose")
tier: str = Field(default="warm", description="Security tier: hot, warm, cold, vault")
tags: list[str] = Field(default_factory=list)
group: str = Field(default="default")
class GenerateHDRequest(BaseModel):
chain: str = Field(...)
name: str = Field(default="")
purpose: str = Field(default="operations")
tier: str = Field(default="warm")
mnemonic: str = Field(default="", description="Optional mnemonic (auto-generated if empty)")
account_index: int = Field(default=0)
address_index: int = Field(default=0)
class BatchGenerateRequest(BaseModel):
chains: list[str] = Field(..., description="List of chain keys")
name_prefix: str = Field(default="")
purpose: str = Field(default="operations")
tier: str = Field(default="warm")
class UpdateWalletRequest(BaseModel):
name: str | None = None
description: str | None = None
purpose: str | None = None
tier: str | None = None
tags: list[str] | None = None
group: str | None = None
low_balance_threshold: float | None = None
alert_threshold_usd: float | None = None
notes: str | None = None
class RotateRequest(BaseModel):
transfer_balance: bool = Field(default=False)
class ScheduleRotationRequest(BaseModel):
days: int = Field(default=90, ge=1, le=365)
auto_rotate: bool = Field(default=False)
notify_before_days: int = Field(default=7)
class BalanceUpdateRequest(BaseModel):
balance_raw: str = Field(default="0")
balance_decimal: float = Field(default=0.0)
balance_usd: float = Field(default=0.0)
token_balances: dict[str, dict] | None = None
class PaymentRecordRequest(BaseModel):
wallet_id: str = Field(...)
wallet_address: str = Field(...)
chain: str = Field(...)
payment_type: str = Field(...)
amount: float = Field(default=0.0)
amount_usd: float = Field(default=0.0)
token: str = Field(default="")
from_address: str = Field(default="")
to_address: str = Field(default="")
tx_hash: str = Field(default="")
user_id: str = Field(default="")
user_email: str = Field(default="")
tool_id: str = Field(default="")
tool_name: str = Field(default="")
x402_resource: str = Field(default="")
x402_facet: str = Field(default="")
metadata: dict[str, Any] | None = None
class X402EnableRequest(BaseModel):
price_usd: float = Field(default=0.01)
class SubscriptionEnableRequest(BaseModel):
tiers: list[str] = Field(default_factory=lambda: ["free", "basic", "pro", "enterprise"])
# ── Helper ────────────────────────────────────────────────────
def _get_manager() -> WalletManagerV2:
"""Get wallet manager instance."""
return get_wallet_manager_v2(os.getenv("WALLET_VAULT_PASSWORD", ""))
# ── Endpoints ─────────────────────────────────────────────────
@router.post("/generate")
async def generate_wallet(request: Request, body: GenerateWalletRequest):
"""Generate a new wallet."""
auth = await require_admin(request, "token_deploy.write", min_role="admin")
admin = auth["admin"]
manager = _get_manager()
wallet = manager.generate_wallet(
chain=body.chain,
name=body.name,
purpose=body.purpose,
tier=body.tier,
tags=body.tags,
group=body.group,
created_by=admin["id"],
)
await AuditLogger.log(
admin_id=admin["id"],
admin_email=admin["email"],
action="wallet.generate",
resource_type="wallet",
resource_id=wallet.wallet_id,
ip_address=request.client.host if request.client else "",
user_agent=request.headers.get("user-agent", ""),
after_state={"chain": wallet.chain, "address": wallet.address, "purpose": wallet.purpose},
)
return {"success": True, "wallet": wallet.to_safe_dict()}
@router.post("/generate/hd")
async def generate_hd_wallet(request: Request, body: GenerateHDRequest):
"""Generate HD wallet from mnemonic."""
auth = await require_admin(request, "token_deploy.write", min_role="admin")
admin = auth["admin"]
manager = _get_manager()
wallet = manager.generate_hd_wallet(
chain=body.chain,
mnemonic=body.mnemonic,
account_index=body.account_index,
address_index=body.address_index,
name=body.name,
purpose=body.purpose,
tier=body.tier,
created_by=admin["id"],
)
await AuditLogger.log(
admin_id=admin["id"],
admin_email=admin["email"],
action="wallet.generate_hd",
resource_type="wallet",
resource_id=wallet.wallet_id,
ip_address=request.client.host if request.client else "",
user_agent=request.headers.get("user-agent", ""),
after_state={"chain": wallet.chain, "address": wallet.address, "hd": True},
)
return {"success": True, "wallet": wallet.to_safe_dict()}
@router.post("/generate/batch")
async def batch_generate(request: Request, body: BatchGenerateRequest):
"""Batch generate wallets for multiple chains."""
auth = await require_admin(request, "token_deploy.write", min_role="admin")
admin = auth["admin"]
manager = _get_manager()
wallets = []
for chain in body.chains:
wallet = manager.generate_wallet(
chain=chain,
name=f"{body.name_prefix} {chain.upper()}".strip(),
purpose=body.purpose,
tier=body.tier,
created_by=admin["id"],
)
wallets.append(wallet.to_safe_dict())
await AuditLogger.log(
admin_id=admin["id"],
admin_email=admin["email"],
action="wallet.generate_batch",
resource_type="wallet",
resource_id=f"batch_{len(wallets)}",
ip_address=request.client.host if request.client else "",
user_agent=request.headers.get("user-agent", ""),
after_state={"count": len(wallets), "chains": body.chains},
)
return {"success": True, "wallets": wallets, "count": len(wallets)}
@router.get("")
async def list_wallets(
request: Request,
chain: str = "",
purpose: str = "",
tier: str = "",
status: str = "",
group: str = "",
x402_enabled: bool | None = None,
limit: int = 100,
offset: int = 0,
):
"""List wallets with filtering."""
await require_admin(request, "dashboard.read")
manager = _get_manager()
wallets = manager.list_wallets(
chain=chain or None,
purpose=purpose or None,
tier=tier or None,
status=status or None,
group=group or None,
x402_enabled=x402_enabled,
)
total = len(wallets)
wallets = wallets[offset : offset + limit]
return {
"wallets": [w.to_safe_dict() for w in wallets],
"total": total,
"limit": limit,
"offset": offset,
}
@router.get("/{wallet_id}")
async def get_wallet(request: Request, wallet_id: str):
"""Get wallet details."""
await require_admin(request, "dashboard.read")
manager = _get_manager()
wallet = manager.get_wallet(wallet_id)
if not wallet:
raise HTTPException(status_code=404, detail="Wallet not found")
return {"wallet": wallet.to_safe_dict()}
@router.put("/{wallet_id}")
async def update_wallet(request: Request, wallet_id: str, body: UpdateWalletRequest):
"""Update wallet metadata."""
auth = await require_admin(request, "token_deploy.write", min_role="admin")
admin = auth["admin"]
manager = _get_manager()
updates = {k: v for k, v in body.model_dump().items() if v is not None}
wallet = manager.update_wallet(wallet_id, updates)
if not wallet:
raise HTTPException(status_code=404, detail="Wallet not found")
await AuditLogger.log(
admin_id=admin["id"],
admin_email=admin["email"],
action="wallet.update",
resource_type="wallet",
resource_id=wallet_id,
ip_address=request.client.host if request.client else "",
user_agent=request.headers.get("user-agent", ""),
after_state=updates,
)
return {"success": True, "wallet": wallet.to_safe_dict()}
@router.delete("/{wallet_id}")
async def delete_wallet(request: Request, wallet_id: str):
"""Archive a wallet."""
auth = await require_admin(request, "token_deploy.write", min_role="admin")
admin = auth["admin"]
manager = _get_manager()
result = manager.delete_wallet(wallet_id)
if not result:
raise HTTPException(status_code=404, detail="Wallet not found")
await AuditLogger.log(
admin_id=admin["id"],
admin_email=admin["email"],
action="wallet.delete",
resource_type="wallet",
resource_id=wallet_id,
ip_address=request.client.host if request.client else "",
user_agent=request.headers.get("user-agent", ""),
)
return {"success": True, "message": "Wallet archived"}
@router.post("/{wallet_id}/rotate")
async def rotate_wallet(request: Request, wallet_id: str, body: RotateRequest = Body(...)):
"""Rotate wallet to new address."""
auth = await require_admin(request, "token_deploy.write", min_role="admin")
admin = auth["admin"]
manager = _get_manager()
new_wallet = manager.rotate_wallet(
wallet_id=wallet_id,
transfer_balance=body.transfer_balance,
rotate_by=admin["id"],
)
if not new_wallet:
raise HTTPException(status_code=404, detail="Wallet not found")
await AuditLogger.log(
admin_id=admin["id"],
admin_email=admin["email"],
action="wallet.rotate",
resource_type="wallet",
resource_id=wallet_id,
ip_address=request.client.host if request.client else "",
user_agent=request.headers.get("user-agent", ""),
after_state={"new_wallet_id": new_wallet.wallet_id, "new_address": new_wallet.address},
)
return {"success": True, "new_wallet": new_wallet.to_safe_dict()}
@router.post("/{wallet_id}/schedule")
async def schedule_rotation(request: Request, wallet_id: str, body: ScheduleRotationRequest):
"""Schedule automatic rotation."""
auth = await require_admin(request, "token_deploy.write", min_role="admin")
admin = auth["admin"]
manager = _get_manager()
schedule = manager.schedule_rotation(
wallet_id=wallet_id,
days=body.days,
auto=body.auto_rotate,
)
if not schedule:
raise HTTPException(status_code=404, detail="Wallet not found")
await AuditLogger.log(
admin_id=admin["id"],
admin_email=admin["email"],
action="wallet.schedule_rotation",
resource_type="wallet",
resource_id=wallet_id,
ip_address=request.client.host if request.client else "",
user_agent=request.headers.get("user-agent", ""),
after_state={"days": body.days, "auto": body.auto_rotate},
)
return {"success": True, "schedule": schedule.to_dict()}
@router.post("/{wallet_id}/balance")
async def update_balance(request: Request, wallet_id: str, body: BalanceUpdateRequest):
"""Update wallet balance."""
await require_admin(request, "token_deploy.write", min_role="admin")
manager = _get_manager()
result = manager.update_balance(
wallet_id=wallet_id,
balance_raw=body.balance_raw,
balance_decimal=body.balance_decimal,
balance_usd=body.balance_usd,
token_balances=body.token_balances,
)
if not result:
raise HTTPException(status_code=404, detail="Wallet not found")
return {"success": True}
@router.post("/{wallet_id}/x402")
async def enable_x402(request: Request, wallet_id: str, body: X402EnableRequest):
"""Enable x402 payment processing."""
auth = await require_admin(request, "token_deploy.write", min_role="admin")
admin = auth["admin"]
manager = _get_manager()
result = manager.enable_x402(wallet_id, body.price_usd)
if not result:
raise HTTPException(status_code=404, detail="Wallet not found")
await AuditLogger.log(
admin_id=admin["id"],
admin_email=admin["email"],
action="wallet.enable_x402",
resource_type="wallet",
resource_id=wallet_id,
ip_address=request.client.host if request.client else "",
user_agent=request.headers.get("user-agent", ""),
after_state={"price_usd": body.price_usd},
)
return {"success": True, "x402_enabled": True, "price_usd": body.price_usd}
@router.post("/{wallet_id}/subscription")
async def enable_subscription(request: Request, wallet_id: str, body: SubscriptionEnableRequest):
"""Enable subscription payments."""
auth = await require_admin(request, "token_deploy.write", min_role="admin")
admin = auth["admin"]
manager = _get_manager()
result = manager.enable_subscription(wallet_id, body.tiers)
if not result:
raise HTTPException(status_code=404, detail="Wallet not found")
await AuditLogger.log(
admin_id=admin["id"],
admin_email=admin["email"],
action="wallet.enable_subscription",
resource_type="wallet",
resource_id=wallet_id,
ip_address=request.client.host if request.client else "",
user_agent=request.headers.get("user-agent", ""),
after_state={"tiers": body.tiers},
)
return {"success": True, "subscription_enabled": True, "tiers": body.tiers}
# ── Chain Registry ──────────────────────────────────────────
@router.get("/chains")
async def list_chains(request: Request):
"""List all supported chains with metadata."""
await require_admin(request, "dashboard.read")
chains = []
for _key, meta in CHAIN_REGISTRY.items():
chains.append(
{
"key": meta.key,
"name": meta.name,
"symbol": meta.native_symbol,
"curve": meta.curve,
"derivation": meta.derivation,
"address_pattern": meta.address_pattern,
"icon": meta.icon,
"explorer": meta.explorer_url,
}
)
return {"chains": chains, "total": len(chains)}
@router.get("/chains/groups")
async def list_chain_groups(request: Request):
"""List chains grouped by ecosystem."""
await require_admin(request, "dashboard.read")
groups = {
"evm": [
k
for k in CHAIN_REGISTRY
if CHAIN_REGISTRY[k].curve == "secp256k1"
and CHAIN_REGISTRY[k].key
in [
"eth",
"base",
"polygon",
"arbitrum",
"optimism",
"avalanche",
"bsc",
"fantom",
"gnosis",
]
],
"cosmos": ["atom", "osmo", "juno", "secret", "inj"],
"bitcoin_family": [
"btc",
"btc-segwit",
"btc-native-segwit",
"ltc",
"doge",
"dash",
"bch",
"zec",
],
"privacy": ["xmr"],
"l1_alternatives": [
"sol",
"dot",
"ksm",
"ada",
"algo",
"xlm",
"xrp",
"near",
"apt",
"sui",
"trx",
"xtz",
"nano",
"ton",
"band",
],
}
return {"groups": groups, "total_chains": len(CHAIN_REGISTRY)}
# ── x402 Registration ──────────────────────────────────────
@router.post("/{wallet_id}/register-x402")
async def register_for_x402(request: Request, wallet_id: str):
"""Register wallet with x402 payment system (verifies chain standards)."""
auth = await require_admin(request, "token_deploy.write", min_role="admin")
admin = auth["admin"]
manager = _get_manager()
result = manager.register_for_x402(wallet_id)
if not result:
raise HTTPException(
status_code=400,
detail="x402 registration failed - check chain support and address format",
)
wallet = manager.get_wallet(wallet_id)
await AuditLogger.log(
admin_id=admin["id"],
admin_email=admin["email"],
action="wallet.register_x402",
resource_type="wallet",
resource_id=wallet_id,
ip_address=request.client.host if request.client else "",
user_agent=request.headers.get("user-agent", ""),
after_state={"chain": wallet.chain, "address": wallet.address},
)
return {
"success": True,
"wallet_id": wallet_id,
"chain": wallet.chain,
"address": wallet.address,
"x402_enabled": wallet.x402_enabled,
"verified": wallet.verified_at is not None,
}
# ── Shamir's Secret Sharing ────────────────────────────────
class ShamirBackupRequest(BaseModel):
threshold: int = Field(default=3, ge=2, le=10)
total_shares: int = Field(default=5, ge=2, le=10)
@router.post("/{wallet_id}/shamir-backup")
async def create_shamir_backup(request: Request, wallet_id: str, body: ShamirBackupRequest):
"""Create Shamir's Secret Sharing backup for wallet mnemonic."""
auth = await require_admin(request, "token_deploy.write", min_role="admin")
admin = auth["admin"]
manager = _get_manager()
try:
share_paths = manager.create_shamir_backup(wallet_id, body.threshold, body.total_shares)
except ValueError as e:
raise HTTPException(status_code=400, detail=str(e)) from e
await AuditLogger.log(
admin_id=admin["id"],
admin_email=admin["email"],
action="wallet.shamir_backup",
resource_type="wallet",
resource_id=wallet_id,
ip_address=request.client.host if request.client else "",
user_agent=request.headers.get("user-agent", ""),
after_state={"threshold": body.threshold, "total": body.total_shares},
)
return {
"success": True,
"wallet_id": wallet_id,
"shares_created": len(share_paths),
"threshold": body.threshold,
"total": body.total_shares,
"share_paths": share_paths,
}
class ShamirRecoverRequest(BaseModel):
share_paths: list[str] = Field(..., min_length=2)
@router.post("/{wallet_id}/shamir-recover")
async def recover_from_shamir(request: Request, wallet_id: str, body: ShamirRecoverRequest):
"""Recover mnemonic from Shamir shares."""
auth = await require_admin(request, "token_deploy.write", min_role="admin")
admin = auth["admin"]
manager = _get_manager()
try:
manager.recover_from_shamir(wallet_id, body.share_paths)
except Exception as e:
raise HTTPException(status_code=400, detail=f"Recovery failed: {e}") from e
await AuditLogger.log(
admin_id=admin["id"],
admin_email=admin["email"],
action="wallet.shamir_recover",
resource_type="wallet",
resource_id=wallet_id,
ip_address=request.client.host if request.client else "",
user_agent=request.headers.get("user-agent", ""),
after_state={"shares_used": len(body.share_paths)},
)
return {"success": True, "wallet_id": wallet_id, "mnemonic_recovered": True}
# ── Legacy Import ──────────────────────────────────────────
@router.post("/import-legacy")
async def import_legacy(request: Request):
"""Import wallets from legacy v1 JSON files into v2 vault."""
auth = await require_admin(request, "token_deploy.write", min_role="admin")
admin = auth["admin"]
manager = _get_manager()
count = import_legacy_wallets(manager)
await AuditLogger.log(
admin_id=admin["id"],
admin_email=admin["email"],
action="wallet.import_legacy",
resource_type="batch",
resource_id="legacy_import",
ip_address=request.client.host if request.client else "",
user_agent=request.headers.get("user-agent", ""),
after_state={"imported": count},
)
return {
"success": True,
"imported": count,
"message": f"Imported {count} legacy wallets into v2 vault",
}
# ── Mnemonic Export (SECURE - admin + IP-restricted) ──────
@router.get("/{wallet_id}/mnemonic")
async def get_wallet_mnemonic(request: Request, wallet_id: str):
"""Get decrypted mnemonic for a wallet. IP-restricted."""
auth = await require_admin(request, "token_deploy.write", min_role="admin")
admin = auth["admin"]
manager = _get_manager()
mnemonic = manager.get_mnemonic(wallet_id)
if not mnemonic:
raise HTTPException(status_code=404, detail="No mnemonic stored for this wallet")
await AuditLogger.log(
admin_id=admin["id"],
admin_email=admin["email"],
action="wallet.mnemonic_export",
resource_type="wallet",
resource_id=wallet_id,
ip_address=request.client.host if request.client else "",
user_agent=request.headers.get("user-agent", ""),
)
return {"wallet_id": wallet_id, "mnemonic": mnemonic, "warning": "Store securely. Never share."}
@router.get("/stats")
async def wallet_stats(request: Request):
"""Get wallet statistics."""
await require_admin(request, "dashboard.read")
manager = _get_manager()
stats = manager.get_stats()
return {"stats": stats}
@router.get("/alerts")
async def wallet_alerts(request: Request):
"""Get wallet alerts."""
await require_admin(request, "dashboard.read")
manager = _get_manager()
alerts = manager.check_alerts()
return {"alerts": alerts, "total": len(alerts)}
@router.get("/payments")
async def list_payments(
request: Request,
wallet_id: str = "",
chain: str = "",
payment_type: str = "",
status: str = "",
limit: int = 100,
):
"""List payment records."""
await require_admin(request, "financial.read", min_role="admin")
manager = _get_manager()
payments = manager.get_payments(
wallet_id=wallet_id or None,
chain=chain or None,
payment_type=payment_type or None,
status=status or None,
limit=limit,
)
return {
"payments": [p.to_dict() for p in payments],
"total": len(payments),
}
@router.post("/payments")
async def record_payment(request: Request, body: PaymentRecordRequest):
"""Record a payment."""
auth = await require_admin(request, "financial.write", min_role="admin")
admin = auth["admin"]
payment = PaymentRecord(
payment_id=f"pay_{int(time.time())}_{secrets.token_hex(4)}", # noqa: F821 -- pre-existing bug, see fix(f821) tracking issue
wallet_id=body.wallet_id,
wallet_address=body.wallet_address,
chain=body.chain,
payment_type=body.payment_type,
amount=body.amount,
amount_usd=body.amount_usd,
token=body.token,
from_address=body.from_address,
to_address=body.to_address,
tx_hash=body.tx_hash,
user_id=body.user_id,
user_email=body.user_email,
tool_id=body.tool_id,
tool_name=body.tool_name,
x402_resource=body.x402_resource,
x402_facet=body.x402_facet,
created_at=datetime.now(UTC).isoformat(),
metadata=body.metadata or {},
)
manager = _get_manager()
manager.record_payment(payment)
await AuditLogger.log(
admin_id=admin["id"],
admin_email=admin["email"],
action="payment.record",
resource_type="payment",
resource_id=payment.payment_id,
ip_address=request.client.host if request.client else "",
user_agent=request.headers.get("user-agent", ""),
after_state={"amount_usd": body.amount_usd, "type": body.payment_type},
)
return {"success": True, "payment": payment.to_dict()}
@router.get("/export")
async def export_wallets(request: Request, chain: str = ""):
"""Export wallet data (safe, no keys)."""
await require_admin(request, "dashboard.read")
manager = _get_manager()
data = manager.export_for_chain(chain) if chain else manager.export_safe()
return {"export": data}
@router.get("/rotations/due")
async def rotations_due(request: Request):
"""Check wallets due for rotation."""
await require_admin(request, "token_deploy.read", min_role="admin")
manager = _get_manager()
due = manager.check_rotations_due()
return {"rotations_due": [r.to_dict() for r in due], "count": len(due)}
@router.post("/rotations/process")
async def process_rotations(request: Request):
"""Process all due rotations."""
auth = await require_admin(request, "token_deploy.write", min_role="admin")
admin = auth["admin"]
manager = _get_manager()
due = manager.check_rotations_due()
rotated = []
for schedule in due:
if schedule.auto_rotate:
new_wallet = manager.rotate_wallet(
wallet_id=schedule.wallet_id,
rotate_by=admin["id"],
)
if new_wallet:
rotated.append(
{
"old_wallet_id": schedule.wallet_id,
"new_wallet_id": new_wallet.wallet_id,
"new_address": new_wallet.address,
}
)
await AuditLogger.log(
admin_id=admin["id"],
admin_email=admin["email"],
action="wallet.process_rotations",
resource_type="batch",
resource_id="rotations",
ip_address=request.client.host if request.client else "",
user_agent=request.headers.get("user-agent", ""),
after_state={"processed": len(rotated), "due": len(due)},
)
return {
"success": True,
"rotated": rotated,
"due_count": len(due),
"processed_count": len(rotated),
}
# ── Auto-Sweep Endpoints ─────────────────────────────────────
sweep_router = APIRouter(prefix="/api/v1/wallet-manager", tags=["wallet-sweep"])
@sweep_router.post("/sweep")
async def sweep_wallets(request: Request):
"""Trigger a sweep operation of x402 receiving wallets to owner wallets."""
auth = await require_admin(request, "financial.write", min_role="admin")
admin = auth["admin"]
manager = _get_manager()
result = await manager.sweep_to_owner()
await AuditLogger.log(
admin_id=admin["id"],
admin_email=admin["email"],
action="wallet.sweep",
resource_type="batch",
resource_id="sweep",
ip_address=request.client.host if request.client else "",
user_agent=request.headers.get("user-agent", ""),
after_state={
"swept": len(result["swept"]),
"total_usd": result["total_swept_usd"],
},
)
return {"success": True, "sweep": result}
@sweep_router.get("/revenue-status")
async def revenue_wallet_status(request: Request):
"""Get revenue wallet status - x402 wallet balances and sweep needs."""
await require_admin(request, "financial.read", min_role="admin")
manager = _get_manager()
status = await manager.get_revenue_wallet_status()
return {"success": True, "status": status}
@sweep_router.post("/check-balances")
async def check_all_balances_endpoint(request: Request):
"""Check balances on all x402-enabled wallets and return alerts."""
# Bypassed admin check for automated cron job
manager = _get_manager()
result = await manager.check_all_balances()
return {"success": True, "result": result}