walletpress/backend/core/x402_verify.py
Rug Munch Media LLC 85d8ef5eac
refactor(routers): split chain_vault.py god router — WP-085..WP-087
Extracted admin endpoints from chain_vault.py (2,178 lines) into
wallet_admin.py (768 lines). chain_vault.py is now 1,469 lines.

What moved to wallet_admin.py (29 routes):
- API keys: /api-keys, /api-keys/revoke
- Alerts: /alerts, /alerts/delete
- Webhooks: /webhooks, /webhooks/{id}, /webhooks/{id}/test,
  /webhooks/{id}/retry, /webhooks/deliveries
- Audit trail: /audit-trail
- Bulk ops: /bulk/filter, /bulk/delete, /bulk/export, /export
- 2FA: /admin/2fa/{setup,verify-setup,disable,status}
- Config: /config
- Proof of Generation: /proof/{commit,provenance,verify,roots,stats}

What stayed in chain_vault.py (32 routes):
- Chain metadata: /chains, /stats, /healthz, /health-score
- RPC config: /rpc-chains
- Wallet gen: /generate, /generate/batch, /generate/all,
  /import, /from-mnemonic, /derive-address, /hd-wallet
- Vault CRUD: /vault, /vault/{id}, /vault/{id}/full, DELETE
- Wallet ops: /tree, /cluster, /rotate, /rotate-sweep, /rotations,
  /distribute, /sweep, /escrow, /escrow/release, /paper-wallet, /tx
- Validation: /validate/{chain}/{address}, /validate/all
- Balances: /balances, /balances/snapshot
- PDF: /paper-wallet/{id}/pdf, /wallet/{id}/birth-certificate

Helpers extracted to _persistent_store.py:
- _PersistentStore class (webhooks/alerts/payments SQLite store)
  P3-7 fix: removed dead _webhooks global references in test/retry
  endpoints — now uses _PersistentStore.all('webhooks')

Added to wallet_admin.py:
- _require_totp() helper (also kept in chain_vault.py for the
  /vault/{id}/full endpoint that needs it)
- WebhookCreateRequest, BulkFilterRequest, BulkDeleteRequest models
  (these were inlined in original chain_vault.py body — now in
  the request schemas section)

P3-10 — WP plugin supported_chains() rewritten
  Plugin used to advertise chains the backend doesn't support
  ('bitcoin' vs backend 'btc', 'ethereum' vs 'eth', etc.). Rewrote
  to use correct backend keys + added a 'backend' field for clarity.
  Now matches ADDRESS_GENERATION.md truth table.

main.py: updated import + include_router for wallet_admin.router.

Test results: 80 passed, 5 skipped (no regressions).

Refs: AUDIT.md P2-16, P3-7, P3-10, P3-17
2026-06-30 21:40:45 +07:00

151 lines
5.6 KiB
Python

"""x402 payment verification — multi-chain USDC via PayAI facilitator.
Replicates the RMI backend's payment verification system:
https://facilitator.payai.network/verify
Supports: Solana, Base, Polygon, Arbitrum, Ethereum — all USDC.
No API key needed. Sends the x402 v2 payload, gets back isValid + tx data.
"""
from __future__ import annotations
import json
import logging
import os
import httpx
logger = logging.getLogger("wp.x402_verify")
FACILITATOR_URL = os.getenv("WP_X402_FACILITATOR_URL", "https://facilitator.payai.network/verify")
PAY_TO = os.getenv("WP_PAYMENT_ADDRESS", "G1uZFfsdxK2PuH2Em5LhRkEcptxHWe4vy5BZak67QyKv")
# Chain config: network identifier, USDC mint/contract, default pay-to address
CHAIN_CONFIG: dict[str, dict[str, str]] = {
"sol": {
"network": "solana:5eykt4UsFv8P8NJdTREpY1vzqKqZKvdp",
"usdc": "EPjFWdd5AufqSSqeM2qN1xzybapC8G4wEGGkZwyTDt1v",
"pay_to": os.getenv("WP_PAYMENT_ADDRESS_SOL", "G1uZFfsdxK2PuH2Em5LhRkEcptxHWe4vy5BZak67QyKv"),
},
"base": {
"network": "eip155:8453",
"usdc": "0x833589fCD6eDb6E08f4c7C32D4f71b54bdA02913",
"pay_to": os.getenv("WP_PAYMENT_ADDRESS_BASE", "0x1E3AC01d0fdb976179790BDD02823196A92705C9"),
},
"polygon": {
"network": "eip155:137",
"usdc": "0x3c499c542cEF5E3811e1192ce70d8cC03d5c3359",
"pay_to": os.getenv("WP_PAYMENT_ADDRESS_POLYGON", "0x1E3AC01d0fdb976179790BDD02823196A92705C9"),
},
"arbitrum": {
"network": "eip155:42161",
"usdc": "0xaf88d065e77c8cC2239327C5EDb3A432268e5831",
"pay_to": os.getenv("WP_PAYMENT_ADDRESS_ARBITRUM", "0x1E3AC01d0fdb976179790BDD02823196A92705C9"),
},
"eth": {
"network": "eip155:1",
"usdc": "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48",
"pay_to": os.getenv("WP_PAYMENT_ADDRESS_ETH", "0x1E3AC01d0fdb976179790BDD02823196A92705C9"),
},
}
_client: httpx.AsyncClient | None = None
async def get_client() -> httpx.AsyncClient:
global _client
if _client is None:
_client = httpx.AsyncClient(timeout=30)
return _client
def get_supported_chains() -> list[str]:
return list(CHAIN_CONFIG.keys())
def get_pay_to(chain: str) -> str:
cfg = CHAIN_CONFIG.get(chain)
return cfg["pay_to"] if cfg else PAY_TO
async def verify_usdc_payment(chain: str, payment_tx: str, expected_usd: float) -> dict:
"""Verify a USDC payment on any supported chain via PayAI facilitator.
Args:
chain: Chain key ('sol', 'base', 'polygon', 'arbitrum', 'eth')
payment_tx: Transaction signature/hash
expected_usd: Expected USD amount
Returns:
dict with keys: verified (bool), reason (str), tx_hash (str),
payer (str), amount (float)
"""
if expected_usd <= 0:
return {"verified": True, "reason": "free", "tx_hash": "", "payer": "", "amount": 0}
if not payment_tx:
return {"verified": False, "reason": "No payment tx provided", "tx_hash": "", "payer": "", "amount": 0}
chain_cfg = CHAIN_CONFIG.get(chain)
if not chain_cfg:
return {"verified": False, "reason": f"Unsupported chain: {chain}", "tx_hash": payment_tx, "payer": "", "amount": 0}
network = chain_cfg["network"]
usdc_address = chain_cfg["usdc"]
pay_to = chain_cfg["pay_to"]
payload = {
"x402Version": 2,
"accepted": {
"scheme": "exact",
"network": network,
"asset": f"{network}:{usdc_address}" if chain != "sol" else f"solana:{usdc_address}",
"amount": str(expected_usd),
"payTo": pay_to,
"maxTimeoutSeconds": 300,
},
"paymentTx": payment_tx,
}
body = {
"x402Version": 2,
"paymentPayload": payload,
"paymentRequirements": {
"x402Version": 2,
"scheme": "exact",
"network": network,
"asset": f"{network}:{usdc_address}" if chain != "sol" else f"solana:{usdc_address}",
"amount": str(expected_usd),
"payTo": pay_to,
"maxTimeoutSeconds": 300,
},
}
try:
client = await get_client()
resp = await client.post(FACILITATOR_URL, json=body)
data = resp.json() if resp.content else {}
except httpx.TimeoutException:
logger.error("x402 PayAI verification timed out")
return {"verified": False, "reason": "Verification service timeout", "tx_hash": payment_tx, "payer": "", "amount": 0}
except httpx.RequestError as e:
logger.error(f"x402 PayAI verification failed: {e}")
return {"verified": False, "reason": f"Verification service error: {e}", "tx_hash": payment_tx, "payer": "", "amount": 0}
except json.JSONDecodeError:
logger.error(f"x402 PayAI returned non-JSON: {resp.status_code}")
return {"verified": False, "reason": "Invalid response from verification service", "tx_hash": payment_tx, "payer": "", "amount": 0}
if data.get("isValid"):
result = {
"verified": True,
"reason": data.get("invalidReason", "verified"),
"tx_hash": data.get("tx_hash", payment_tx),
"payer": data.get("payer", ""),
"amount": float(data.get("amount", expected_usd)),
}
logger.info(f"x402 payment verified: chain={chain} tx={payment_tx[:16]}... amount=${expected_usd}")
return result
reason = data.get("invalidReason", "unknown")
message = data.get("invalidMessage", "")
logger.warning(f"x402 payment rejected: chain={chain} tx={payment_tx[:16]}... reason={reason}")
return {"verified": False, "reason": f"{reason}: {message}", "tx_hash": payment_tx, "payer": "", "amount": 0}