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
151 lines
5.6 KiB
Python
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}
|