#!/usr/bin/env python3
"""Minimal x402 v2 payer for SableForge endpoints — 402 challenge, EIP-3009 signature, paid retry.

    pip install eth-account
    EVM_PRIVATE_KEY=0x... python x402_client.py /api/price "ids=bitcoin&vs_currencies=usd"

The wallet in EVM_PRIVATE_KEY must hold a little USDC on Base; it pays the payTo address.
Nothing here is SableForge-specific beyond the URL — the same code works against any x402 v2 resource.
"""
import base64
import json
import os
import sys
import time
import urllib.error
import urllib.parse
import urllib.request

from eth_account import Account
from eth_account.messages import encode_typed_data

BASE_URL = os.environ.get("X402_BASE_URL", "http://sableforge.servehttp.com")
CHAIN_ID = 8453

TWA_TYPES = {
    "TransferWithAuthorization": [
        ("from", "address"), ("to", "address"), ("value", "uint256"),
        ("validAfter", "uint256"), ("validBefore", "uint256"), ("nonce", "bytes32"),
    ]
}


def request_402(url, method="GET", body=None):
    """First call: no payment -> HTTP 402 with the base64 PAYMENT-REQUIRED header."""
    req = urllib.request.Request(url, method=method, data=body)
    req.add_header("User-Agent", "x402-client/1.0")
    if body:
        req.add_header("Content-Type", "application/json")
    try:
        with urllib.request.urlopen(req, timeout=30) as r:
            return True, json.loads(r.read().decode())
    except urllib.error.HTTPError as e:
        if e.code != 402:
            raise
        raw = e.headers.get("PAYMENT-REQUIRED")
        if not raw:
            raise RuntimeError("402 without a PAYMENT-REQUIRED header")
        challenge = json.loads(base64.b64decode(raw))
        return False, challenge


def sign(accept, key):
    """Sign the EIP-712 EIP-3009 TransferWithAuthorization the challenge asks for."""
    payer = Account.from_key(key).address
    auth = {
        "from": payer,
        "to": accept["payTo"],
        "value": int(accept["amount"]),
        "validAfter": 0,
        "validBefore": int(time.time()) + int(accept.get("maxTimeoutSeconds", 600)),
        "nonce": "0x" + os.urandom(32).hex(),
    }
    domain = {
        "name": accept["extra"].get("name", "USD Coin"),
        "version": accept["extra"].get("version", "2"),
        "chainId": CHAIN_ID,
        "verifyingContract": accept["asset"],
    }
    sig = Account.sign_message(encode_typed_data(full_message={
        "types": TWA_TYPES, "primaryType": "TransferWithAuthorization",
        "domain": domain, "message": auth,
    }), key).signature.hex()
    return {"x402Version": 2, "accepted": accept,
            "payload": {"signature": sig, "authorization": auth}}


def pay(url, key, method="GET", body=None):
    paid, challenge = request_402(url, method, body)
    if paid:
        return challenge, None
    accept = challenge["accepts"][0]
    payload = sign(accept, key)
    header = base64.b64encode(json.dumps(payload).encode()).decode()
    req = urllib.request.Request(url, method=method, data=body)
    req.add_header("PAYMENT-SIGNATURE", header)
    req.add_header("User-Agent", "x402-client/1.0")
    if body:
        req.add_header("Content-Type", "application/json")
    with urllib.request.urlopen(req, timeout=120) as r:
        receipt = r.headers.get("PAYMENT-RESPONSE")
        return json.loads(r.read().decode()), (json.loads(base64.b64decode(receipt)) if receipt else None)


def main():
    path = sys.argv[1] if len(sys.argv) > 1 else "/api/price"
    qs = sys.argv[2] if len(sys.argv) > 2 else ""
    url = BASE_URL + path + (("?" + qs) if qs else "")
    key = os.environ["EVM_PRIVATE_KEY"]
    data, receipt = pay(url, key)
    print(json.dumps(data, indent=2)[:4000])
    if receipt:
        print("\nsettlement:", json.dumps(receipt)[:600])


if __name__ == "__main__":
    main()
