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
169 lines
6.7 KiB
Python
169 lines
6.7 KiB
Python
#!/usr/bin/env python3
|
|
"""WalletPress Receipt Verifier — CLI tool to verify x402 order receipts.
|
|
|
|
Usage:
|
|
# Verify an order receipt
|
|
python3 verify_receipt.py --order-id x402_abc123 \\
|
|
--signature "base64signature..." \\
|
|
--pubkey "ed25519pubkeyhex"
|
|
|
|
# Verify a key deletion attestation
|
|
python3 verify_receipt.py --order-id x402_abc123 \\
|
|
--deletion-sig "base64signature..." \\
|
|
--pubkey "ed25519pubkeyhex" \\
|
|
--addresses addr1,addr2,addr3
|
|
|
|
# Fetch and verify from a running WalletPress instance
|
|
python3 verify_receipt.py --order-id x402_abc123 --server http://localhost:8010
|
|
|
|
Trust: This tool is open source. It does NOT phone home. It does NOT
|
|
send your keys anywhere. It only verifies cryptographic signatures.
|
|
"""
|
|
|
|
import argparse
|
|
import hashlib
|
|
import sys
|
|
from typing import Any
|
|
|
|
try:
|
|
import httpx
|
|
except ImportError:
|
|
httpx = None # type: ignore
|
|
|
|
try:
|
|
from nacl.bindings import crypto_sign_open
|
|
except ImportError:
|
|
crypto_sign_open = None # type: ignore
|
|
|
|
|
|
def verify_receipt(order_id: str, chain: str, count: int, total_usd: float,
|
|
timestamp: float, signature: str, pubkey_hex: str) -> bool:
|
|
"""Verify an Ed25519-signed order receipt."""
|
|
if crypto_sign_open is None:
|
|
print("ERROR: pynacl is required. Install: pip install pynacl")
|
|
return False
|
|
message = f"walletpress:receipt:{order_id}:{chain}:{count}:{total_usd}:{timestamp}".encode()
|
|
import base64
|
|
sig_bytes = base64.b64decode(signature)
|
|
try:
|
|
crypto_sign_open(sig_bytes + message, bytes.fromhex(pubkey_hex))
|
|
return True
|
|
except Exception:
|
|
return False
|
|
|
|
|
|
def verify_key_deletion(order_id: str, chain: str, count: int,
|
|
addresses: list[str], signature: str, pubkey_hex: str) -> bool:
|
|
"""Verify a key deletion attestation — proves keys were wiped."""
|
|
if crypto_sign_open is None:
|
|
print("ERROR: pynacl is required. Install: pip install pynacl")
|
|
return False
|
|
addr_hash = hashlib.sha256("".join(sorted(addresses)).encode()).hexdigest()[:16]
|
|
message = f"walletpress:key-deletion:{order_id}:{chain}:{count}:{addr_hash}:{int(timestamp)}".encode()
|
|
import base64
|
|
sig_bytes = base64.b64decode(signature)
|
|
try:
|
|
crypto_sign_open(sig_bytes + message, bytes.fromhex(pubkey_hex))
|
|
return True
|
|
except Exception:
|
|
return False
|
|
|
|
|
|
def fetch_and_verify(server_url: str, order_id: str) -> dict[str, Any]:
|
|
"""Fetch order details from a WalletPress server and verify the receipt."""
|
|
if httpx is None:
|
|
return {"error": "httpx is required. Install: pip install httpx"}
|
|
try:
|
|
resp = httpx.get(f"{server_url}/api/v1/marketplace/verify-receipt", params={
|
|
"order_id": order_id,
|
|
}, timeout=10)
|
|
if resp.status_code == 404:
|
|
return {"error": f"Order {order_id} not found on {server_url}"}
|
|
data = resp.json()
|
|
return data
|
|
except httpx.RequestError as e:
|
|
return {"error": f"Failed to connect to {server_url}: {e}"}
|
|
|
|
|
|
def main():
|
|
parser = argparse.ArgumentParser(
|
|
description="WalletPress Receipt Verifier — prove your order was fulfilled",
|
|
formatter_class=argparse.RawDescriptionHelpFormatter,
|
|
epilog="""
|
|
Examples:
|
|
# Verify a receipt
|
|
python3 verify_receipt.py --order-id x402_abc123 --signature "sig" --pubkey "key"
|
|
|
|
# Verify key deletion
|
|
python3 verify_receipt.py --order-id x402_abc123 --deletion-sig "sig" --pubkey "key" --addresses addr1,addr2
|
|
|
|
# Fetch and verify from server
|
|
python3 verify_receipt.py --order-id x402_abc123 --server http://localhost:8010
|
|
""",
|
|
)
|
|
parser.add_argument("--order-id", required=True, help="Order ID from the generate response")
|
|
parser.add_argument("--signature", help="Receipt signature (base64)")
|
|
parser.add_argument("--deletion-sig", help="Key deletion attestation signature (base64)")
|
|
parser.add_argument("--pubkey", help="Server's Ed25519 public key (hex)")
|
|
parser.add_argument("--addresses", help="Comma-separated wallet addresses (for deletion verification)")
|
|
parser.add_argument("--server", help="WalletPress server URL to fetch order details from")
|
|
parser.add_argument("--chain", default="sol", help="Chain the wallets were generated on")
|
|
parser.add_argument("--count", type=int, default=1, help="Number of wallets generated")
|
|
parser.add_argument("--total-usd", type=float, default=0, help="Total amount paid in USD")
|
|
parser.add_argument("--timestamp", type=float, default=0, help="Order timestamp (Unix)")
|
|
|
|
args = parser.parse_args()
|
|
|
|
if args.server:
|
|
result = fetch_and_verify(args.server, args.order_id)
|
|
if "error" in result:
|
|
print(f"FAIL: {result['error']}")
|
|
sys.exit(1)
|
|
print(f"Order: {result.get('order_id', '?')}")
|
|
print(f"Chain: {result.get('chain', '?')}")
|
|
print(f"Count: {result.get('count', '?')}")
|
|
print(f"Amount: ${result.get('amount_usd', '?')}")
|
|
print(f"Valid: {result.get('valid', '?')}")
|
|
print(f"Status: {'VERIFIED' if result.get('valid') else 'INVALID'}")
|
|
sys.exit(0 if result.get('valid') else 1)
|
|
|
|
if args.signature and args.pubkey:
|
|
if not args.timestamp:
|
|
print("ERROR: --timestamp is required for receipt verification")
|
|
sys.exit(1)
|
|
valid = verify_receipt(
|
|
args.order_id, args.chain, args.count, args.total_usd,
|
|
args.timestamp, args.signature, args.pubkey,
|
|
)
|
|
print(f"Receipt: {'VALID' if valid else 'INVALID'}")
|
|
print(f" Order: {args.order_id}")
|
|
print(f" Chain: {args.chain}")
|
|
print(f" Count: {args.count}")
|
|
print(f" Amount: ${args.total_usd}")
|
|
print(" Algorithm: Ed25519")
|
|
if not valid:
|
|
sys.exit(1)
|
|
|
|
if args.deletion_sig and args.pubkey and args.addresses:
|
|
addresses = [a.strip() for a in args.addresses.split(",")]
|
|
valid = verify_key_deletion(
|
|
args.order_id, args.chain, args.count, addresses,
|
|
args.deletion_sig, args.pubkey,
|
|
)
|
|
print(f"Key Deletion: {'VERIFIED' if valid else 'FAILED'}")
|
|
print(f" Order: {args.order_id}")
|
|
print(f" Addresses: {len(addresses)} wallets")
|
|
print(" Algorithm: Ed25519")
|
|
if not valid:
|
|
print(" WARNING: Key deletion attestation is INVALID. The server may have kept your keys.")
|
|
sys.exit(1)
|
|
print(" Keys were generated in memory and wiped. We did not store them.")
|
|
|
|
if not args.signature and not args.deletion_sig and not args.server:
|
|
print("ERROR: Provide --signature, --deletion-sig, or --server")
|
|
parser.print_help()
|
|
sys.exit(1)
|
|
|
|
|
|
if __name__ == "__main__":
|
|
main()
|