diff --git a/x402/.gitignore b/x402/.gitignore new file mode 100644 index 0000000..66576d7 --- /dev/null +++ b/x402/.gitignore @@ -0,0 +1,11 @@ +__pycache__/ +*.pyc +*.pyo +*.egg-info/ +dist/ +build/ +.eggs/ +*.egg +.env +.venv +venv/ diff --git a/x402/README.md b/x402/README.md new file mode 100644 index 0000000..10efed3 --- /dev/null +++ b/x402/README.md @@ -0,0 +1,189 @@ +# RTC x402 Payment Protocol + +Implementation of the HTTP 402 Payment Required protocol for RustChain micropayments. + +## Overview + +The x402 protocol enables machine-to-machine micropayments over HTTP: + +1. Client requests a resource +2. Server returns `402 Payment Required` with payment details +3. Client automatically pays via RTC +4. Client retries with payment proof +5. Server verifies payment and serves the resource + +This enables AI agents, IoT devices, and automated services to transact without human intervention. + +## Installation + +```bash +pip install flask requests pynacl +# Optional for BIP39 support: +pip install mnemonic +``` + +## Quick Start + +### Server Side + +```python +from flask import Flask +from rtc_payment_middleware import require_rtc_payment + +app = Flask(__name__) + +@app.route('/api/data') +@require_rtc_payment(amount=0.001, recipient='your_wallet_id') +def get_data(): + return {'data': 'premium content'} +``` + +### Client Side + +```python +from rtc_payment_client import RTCClient + +client = RTCClient( + wallet_seed='your-24-word-seed-phrase', + max_payment=1.0 # Safety limit +) + +# Automatic 402 handling +response = client.get('https://api.example.com/api/data') +# Client detects 402 → signs RTC payment → retries → returns 200 +``` + +## Protocol Specification + +### 402 Response Headers + +| Header | Description | Example | +|--------|-------------|---------| +| `X-Payment-Amount` | Payment amount required | `0.001` | +| `X-Payment-Currency` | Currency code | `RTC` | +| `X-Payment-Address` | Recipient wallet | `gurgguda` | +| `X-Payment-Network` | Network identifier | `rustchain` | +| `X-Payment-Nonce` | Unique payment nonce | `a1b2c3d4...` | +| `X-Payment-Endpoint` | Payment submission URL | `https://50.28.86.131/wallet/transfer/signed` | + +### Payment Proof Headers + +| Header | Description | +|--------|-------------| +| `X-Payment-TX` | Transaction hash | +| `X-Payment-Signature` | Ed25519 signature of `nonce:tx_hash` | +| `X-Payment-Sender` | Sender's public key (hex) | +| `X-Payment-Nonce` | Original nonce from 402 response | + +## Components + +### `rtc_payment_middleware.py` + +Flask middleware for payment-gated endpoints. + +```python +from rtc_payment_middleware import require_rtc_payment + +@app.route('/premium') +@require_rtc_payment( + amount=0.001, # RTC amount + recipient='wallet', # Recipient wallet ID + rate_limit=100 # Max requests per minute +) +def premium_endpoint(): + # g.rtc_sender contains payer's address + return {'data': 'paid content'} +``` + +### `rtc_payment_client.py` + +HTTP client with automatic payment handling. + +```python +from rtc_payment_client import RTCClient + +client = RTCClient( + wallet_seed='...', # BIP39 seed phrase + max_payment=1.0, # Max auto-pay amount + auto_pay=True # Enable automatic 402 handling +) + +# All HTTP methods supported +response = client.get(url) +response = client.post(url, json=data) + +# Check spending +print(client.total_spent) # Total RTC spent +print(client.payment_history) # List of receipts +``` + +## Security Considerations + +1. **Max Payment Limit**: Always set `max_payment` to prevent runaway spending +2. **SSL Verification**: Enable in production with trusted certificates +3. **Nonce Replay**: Server caches nonces to prevent replay attacks +4. **Rate Limiting**: Built-in rate limiting per sender + +## Examples + +### Run the Demo Server + +```bash +export RTC_PAYMENT_ADDRESS=gurgguda +python example_app.py +``` + +### Test with curl + +```bash +# Get 402 response +curl http://localhost:5000/api/data + +# Response: +# HTTP/1.1 402 Payment Required +# X-Payment-Amount: 0.001 +# X-Payment-Currency: RTC +# X-Payment-Address: gurgguda +# ... +``` + +### Run the Demo Client + +```bash +python example_client.py +``` + +## API Reference + +### `require_rtc_payment(amount, recipient, rate_limit)` + +Decorator to require RTC payment for Flask endpoints. + +**Parameters:** +- `amount` (float): Payment amount in RTC +- `recipient` (str): Wallet address to receive payment +- `rate_limit` (int): Max requests per minute per sender (default: 100) + +### `RTCClient(wallet_seed, max_payment, auto_pay)` + +HTTP client with automatic payment handling. + +**Parameters:** +- `wallet_seed` (str): BIP39 24-word seed phrase +- `max_payment` (float): Maximum auto-pay amount (default: 1.0) +- `auto_pay` (bool): Enable automatic 402 handling (default: True) + +**Properties:** +- `wallet_address`: Client's wallet address +- `total_spent`: Total RTC spent +- `payment_history`: List of PaymentReceipt objects + +## License + +MIT License - Part of the RustChain ecosystem. + +## Links + +- [RustChain Repository](https://github.com/Scottcjn/Rustchain) +- [x402 Protocol Spec](https://github.com/x402/spec) +- [HTTP 402 RFC](https://tools.ietf.org/html/rfc7231#section-6.5.2) diff --git a/x402/example_app.py b/x402/example_app.py new file mode 100644 index 0000000..9eaf099 --- /dev/null +++ b/x402/example_app.py @@ -0,0 +1,97 @@ +""" +Example Flask Application with RTC Payment-Gated Endpoints + +This demonstrates the x402 Payment Required protocol with RustChain. + +Run: + RTC_PAYMENT_ADDRESS=gurgguda python example_app.py + +Test: + # First request returns 402 + curl http://localhost:5000/api/data + + # With payment proof (after paying) + curl http://localhost:5000/api/data \ + -H "X-Payment-TX: " \ + -H "X-Payment-Signature: " \ + -H "X-Payment-Sender: " \ + -H "X-Payment-Nonce: " +""" + +import os +from flask import Flask, jsonify, g +from rtc_payment_middleware import require_rtc_payment + +app = Flask(__name__) + +# Configuration +PAYMENT_ADDRESS = os.environ.get('RTC_PAYMENT_ADDRESS', 'gurgguda') + + +@app.route('/') +def index(): + """Public endpoint - no payment required.""" + return jsonify({ + 'message': 'Welcome to the RTC-gated API', + 'endpoints': { + '/': 'This help message (free)', + '/api/data': 'Premium data endpoint (0.001 RTC)', + '/api/analysis': 'Analysis endpoint (0.005 RTC)', + '/api/bulk': 'Bulk data endpoint (0.01 RTC)' + } + }) + + +@app.route('/api/data') +@require_rtc_payment(amount=0.001, recipient=PAYMENT_ADDRESS) +def get_data(): + """Premium data endpoint - requires 0.001 RTC payment.""" + return jsonify({ + 'status': 'success', + 'data': { + 'message': 'This is premium data', + 'timestamp': __import__('time').time(), + 'paid_by': getattr(g, 'rtc_sender', 'unknown'), + 'amount_paid': getattr(g, 'rtc_payment_amount', 0) + } + }) + + +@app.route('/api/analysis') +@require_rtc_payment(amount=0.005, recipient=PAYMENT_ADDRESS) +def get_analysis(): + """Analysis endpoint - requires 0.005 RTC payment.""" + return jsonify({ + 'status': 'success', + 'analysis': { + 'trend': 'positive', + 'confidence': 0.87, + 'recommendation': 'hold', + 'generated_for': getattr(g, 'rtc_sender', 'unknown') + } + }) + + +@app.route('/api/bulk') +@require_rtc_payment(amount=0.01, recipient=PAYMENT_ADDRESS) +def get_bulk_data(): + """Bulk data endpoint - requires 0.01 RTC payment.""" + return jsonify({ + 'status': 'success', + 'bulk_data': [ + {'id': i, 'value': f'item_{i}'} for i in range(100) + ], + 'count': 100, + 'paid_by': getattr(g, 'rtc_sender', 'unknown') + }) + + +if __name__ == '__main__': + print(f"Starting RTC payment-gated API server...") + print(f"Payment address: {PAYMENT_ADDRESS}") + print(f"Endpoints:") + print(f" GET / - Free (info)") + print(f" GET /api/data - 0.001 RTC") + print(f" GET /api/analysis - 0.005 RTC") + print(f" GET /api/bulk - 0.01 RTC") + app.run(debug=True, port=5000) diff --git a/x402/example_client.py b/x402/example_client.py new file mode 100644 index 0000000..33fc4bf --- /dev/null +++ b/x402/example_client.py @@ -0,0 +1,60 @@ +""" +Example Client Using RTC Auto-Pay + +This demonstrates automatic 402 handling with the RTCClient. + +Usage: + python example_client.py +""" + +from rtc_payment_client import RTCClient + +# Initialize client with wallet +# In production, load seed from secure storage +DEMO_SEED = "abandon " * 24 # DO NOT use this - just for demo + +def main(): + print("RTC Payment Client Demo") + print("=" * 50) + + # Create client + client = RTCClient( + wallet_seed=DEMO_SEED, + max_payment=0.1, # Safety limit + auto_pay=True + ) + + print(f"Wallet address: {client.wallet_address}") + print() + + # Make request to payment-gated endpoint + print("Requesting /api/data (costs 0.001 RTC)...") + + try: + response = client.get("http://localhost:5000/api/data") + + if response.status_code == 200: + print(f"Success! Response: {response.json()}") + print(f"Total spent: {client.total_spent} RTC") + elif response.status_code == 402: + print("Payment required but auto_pay failed") + print(f"Response: {response.json()}") + else: + print(f"Unexpected status: {response.status_code}") + print(f"Response: {response.text}") + + except Exception as e: + print(f"Error: {e}") + + # Show payment history + print() + print("Payment History:") + for receipt in client.payment_history: + print(f" TX: {receipt.tx_hash[:16]}...") + print(f" Amount: {receipt.amount} RTC") + print(f" To: {receipt.recipient}") + print() + + +if __name__ == '__main__': + main() diff --git a/x402/requirements.txt b/x402/requirements.txt new file mode 100644 index 0000000..b6d701d --- /dev/null +++ b/x402/requirements.txt @@ -0,0 +1,5 @@ +flask>=2.0.0 +requests>=2.25.0 +pynacl>=1.5.0 +mnemonic>=0.20 +cryptography>=3.4.0 diff --git a/x402/rtc_payment_client.py b/x402/rtc_payment_client.py new file mode 100644 index 0000000..0e89180 --- /dev/null +++ b/x402/rtc_payment_client.py @@ -0,0 +1,420 @@ +""" +RTC Payment Client +Automatically handles HTTP 402 Payment Required responses with RTC micropayments. + +Usage: + from rtc_payment_client import RTCClient + + client = RTCClient( + wallet_seed='your-24-word-seed-phrase', + node_url='https://50.28.86.131' + ) + + # Automatic 402 handling + response = client.get('https://api.example.com/premium/data') + # Client detects 402 → signs RTC payment → retries → returns 200 +""" + +import hashlib +import json +import secrets +import time +from typing import Optional, Dict, Any, Union +from dataclasses import dataclass +import requests +from requests.adapters import HTTPAdapter +from urllib3.util.retry import Retry + +# Ed25519 signing +import nacl.signing +import nacl.encoding +from nacl.signing import SigningKey + +# PBKDF2 for proper seed derivation +from cryptography.hazmat.primitives.kdf.pbkdf2 import PBKDF2HMAC +from cryptography.hazmat.primitives import hashes +from cryptography.hazmat.backends import default_backend + +# BIP39 for seed phrase handling +try: + from mnemonic import Mnemonic + HAS_MNEMONIC = True +except ImportError: + HAS_MNEMONIC = False + + +@dataclass +class PaymentReceipt: + """Receipt for a completed RTC payment.""" + tx_hash: str + amount: float + recipient: str + sender: str + nonce: str + timestamp: float + + +class RTCWallet: + """ + RTC wallet for signing payments. + Supports BIP39 seed phrases or raw private keys. + """ + + def __init__( + self, + seed_phrase: Optional[str] = None, + private_key: Optional[bytes] = None + ): + """ + Initialize wallet from seed phrase or private key. + + Args: + seed_phrase: BIP39 24-word mnemonic + private_key: Raw 32-byte Ed25519 private key + """ + if seed_phrase: + self._init_from_seed(seed_phrase) + elif private_key: + self._init_from_key(private_key) + else: + raise ValueError("Must provide seed_phrase or private_key") + + def _init_from_seed(self, seed_phrase: str): + """Initialize from BIP39 seed phrase using PBKDF2HMAC derivation.""" + if HAS_MNEMONIC: + mnemo = Mnemonic("english") + entropy = mnemo.to_entropy(seed_phrase) + else: + # Fallback: use seed phrase directly + entropy = seed_phrase.encode() + + # Derive Ed25519 key using PBKDF2HMAC with rustchain-specific salt + kdf = PBKDF2HMAC( + algorithm=hashes.SHA256(), + length=32, + salt=b"rustchain-ed25519", + iterations=100000, + backend=default_backend() + ) + seed = kdf.derive(entropy if isinstance(entropy, bytes) else bytes(entropy)) + + self._signing_key = SigningKey(seed) + self._verify_key = self._signing_key.verify_key + + def _init_from_key(self, private_key: bytes): + """Initialize from raw private key.""" + self._signing_key = SigningKey(private_key) + self._verify_key = self._signing_key.verify_key + + @property + def address(self) -> str: + """Get wallet address in RTC format (RTC + truncated hash of pubkey).""" + pubkey_bytes = self._verify_key.encode() + return f"RTC{hashlib.sha256(pubkey_bytes).hexdigest()[:40]}" + + @property + def public_key(self) -> bytes: + """Get raw public key bytes.""" + return self._verify_key.encode() + + def sign(self, message: Union[str, bytes]) -> bytes: + """ + Sign a message with Ed25519. + + Args: + message: Message to sign (str or bytes) + + Returns: + 64-byte signature + """ + if isinstance(message, str): + message = message.encode() + + signed = self._signing_key.sign(message) + return signed.signature + + +class RTCPaymentHandler: + """ + Handles x402 payment flow. + Detects 402 responses, makes payments, and retries requests. + """ + + def __init__( + self, + wallet: RTCWallet, + node_url: str = "https://50.28.86.131", + max_payment: float = 1.0, # Max auto-pay amount + verify_ssl: bool = False + ): + """ + Initialize payment handler. + + Args: + wallet: RTCWallet instance for signing + node_url: RustChain node endpoint + max_payment: Maximum amount to auto-pay (safety limit) + verify_ssl: Whether to verify SSL certificates + """ + self.wallet = wallet + self.node_url = node_url + self.max_payment = max_payment + self.verify_ssl = verify_ssl + self._payment_history = [] + + def parse_402_response(self, response: requests.Response) -> Optional[Dict]: + """ + Parse payment requirements from 402 response. + + Args: + response: HTTP response with 402 status + + Returns: + Dict with payment details or None + """ + if response.status_code != 402: + return None + + # Try headers first (x402 standard) + amount = response.headers.get('X-Payment-Amount') + address = response.headers.get('X-Payment-Address') + nonce = response.headers.get('X-Payment-Nonce') + + if amount and address: + return { + 'amount': float(amount), + 'recipient': address, + 'currency': response.headers.get('X-Payment-Currency', 'RTC'), + 'network': response.headers.get('X-Payment-Network', 'rustchain'), + 'nonce': nonce or '', + 'endpoint': response.headers.get('X-Payment-Endpoint', f"{self.node_url}/wallet/transfer/signed") + } + + # Try JSON body + try: + data = response.json() + if 'payment' in data: + return data['payment'] + except (ValueError, json.JSONDecodeError): + pass + + return None + + def make_payment(self, payment_req: Dict) -> PaymentReceipt: + """ + Execute an RTC payment. + + Args: + payment_req: Payment requirements dict + + Returns: + PaymentReceipt with transaction details + + Raises: + ValueError: If payment exceeds max_payment limit + requests.RequestException: If payment fails + """ + amount = payment_req['amount'] + recipient = payment_req['recipient'] + nonce = payment_req.get('nonce', '') + + # Safety check + if amount > self.max_payment: + raise ValueError(f"Payment {amount} exceeds max_payment limit {self.max_payment}") + + # Create signed transfer + timestamp = int(time.time()) + tx_nonce = secrets.token_hex(16) # Cryptographically secure nonce + + # Build transfer message with correct field names for RustChain API + transfer_data = { + 'from_address': self.wallet.address, + 'to_address': recipient, + 'amount_rtc': amount, + 'timestamp': timestamp, + 'nonce': tx_nonce, + 'memo': f"x402:{nonce}" + } + + # Sign the transfer + message = json.dumps(transfer_data, sort_keys=True) + signature = self.wallet.sign(message) + + # Submit to chain with signature and public key + response = requests.post( + f"{self.node_url}/wallet/transfer/signed", + json={ + **transfer_data, + 'signature': signature.hex(), + 'public_key': self.wallet.public_key.hex() + }, + timeout=30, + verify=self.verify_ssl + ) + + if response.status_code not in [200, 201]: + raise requests.RequestException(f"Payment failed: {response.text}") + + result = response.json() + tx_hash = result.get('tx_hash', result.get('hash', hashlib.sha256(message.encode()).hexdigest())) + + receipt = PaymentReceipt( + tx_hash=tx_hash, + amount=amount, + recipient=recipient, + sender=self.wallet.address, + nonce=nonce, + timestamp=time.time() + ) + + self._payment_history.append(receipt) + return receipt + + def create_payment_headers(self, receipt: PaymentReceipt) -> Dict[str, str]: + """ + Create headers for authenticated retry request. + + Args: + receipt: Payment receipt from make_payment + + Returns: + Dict of headers to include in retry + """ + # Sign nonce:tx_hash for proof + proof_message = f"{receipt.nonce}:{receipt.tx_hash}" + signature = self.wallet.sign(proof_message) + + # Send raw public key hex (not wallet address) for signature verification + # Server expects hex bytes for Ed25519 verification + return { + 'X-Payment-TX': receipt.tx_hash, + 'X-Payment-Signature': signature.hex(), + 'X-Payment-Sender': self.wallet.public_key.hex(), + 'X-Payment-Nonce': receipt.nonce + } + + @property + def payment_history(self): + """Get list of completed payments.""" + return self._payment_history.copy() + + @property + def total_spent(self) -> float: + """Get total RTC spent.""" + return sum(p.amount for p in self._payment_history) + + +class RTCClient: + """ + HTTP client with automatic x402 payment handling. + Drop-in replacement for requests with RTC micropayment support. + """ + + def __init__( + self, + wallet_seed: Optional[str] = None, + private_key: Optional[bytes] = None, + node_url: str = "https://50.28.86.131", + max_payment: float = 1.0, + auto_pay: bool = True, + verify_ssl: bool = False + ): + """ + Initialize RTC-enabled HTTP client. + + Args: + wallet_seed: BIP39 seed phrase + private_key: Raw Ed25519 private key + node_url: RustChain node URL + max_payment: Maximum auto-pay amount + auto_pay: Whether to automatically handle 402s + verify_ssl: Whether to verify SSL certificates + """ + wallet = RTCWallet(seed_phrase=wallet_seed, private_key=private_key) + self.payment_handler = RTCPaymentHandler( + wallet=wallet, + node_url=node_url, + max_payment=max_payment, + verify_ssl=verify_ssl + ) + self.auto_pay = auto_pay + self.verify_ssl = verify_ssl + + # Create session with retry logic + self.session = requests.Session() + retry = Retry(total=3, backoff_factor=0.5) + adapter = HTTPAdapter(max_retries=retry) + self.session.mount('http://', adapter) + self.session.mount('https://', adapter) + + def _request( + self, + method: str, + url: str, + **kwargs + ) -> requests.Response: + """ + Make HTTP request with automatic 402 handling. + """ + kwargs.setdefault('verify', self.verify_ssl) + + # First attempt + response = self.session.request(method, url, **kwargs) + + # Handle 402 Payment Required + if response.status_code == 402 and self.auto_pay: + payment_req = self.payment_handler.parse_402_response(response) + + if payment_req: + # Make payment + receipt = self.payment_handler.make_payment(payment_req) + + # Retry with payment proof + payment_headers = self.payment_handler.create_payment_headers(receipt) + headers = kwargs.get('headers', {}) + headers.update(payment_headers) + kwargs['headers'] = headers + + response = self.session.request(method, url, **kwargs) + + return response + + def get(self, url: str, **kwargs) -> requests.Response: + """HTTP GET with automatic payment.""" + return self._request('GET', url, **kwargs) + + def post(self, url: str, **kwargs) -> requests.Response: + """HTTP POST with automatic payment.""" + return self._request('POST', url, **kwargs) + + def put(self, url: str, **kwargs) -> requests.Response: + """HTTP PUT with automatic payment.""" + return self._request('PUT', url, **kwargs) + + def delete(self, url: str, **kwargs) -> requests.Response: + """HTTP DELETE with automatic payment.""" + return self._request('DELETE', url, **kwargs) + + @property + def wallet_address(self) -> str: + """Get client's wallet address.""" + return self.payment_handler.wallet.address + + @property + def payment_history(self): + """Get payment history.""" + return self.payment_handler.payment_history + + @property + def total_spent(self) -> float: + """Get total RTC spent.""" + return self.payment_handler.total_spent + + +# Convenience exports +__all__ = [ + 'RTCClient', + 'RTCWallet', + 'RTCPaymentHandler', + 'PaymentReceipt' +] diff --git a/x402/rtc_payment_middleware.py b/x402/rtc_payment_middleware.py new file mode 100644 index 0000000..a997ac6 --- /dev/null +++ b/x402/rtc_payment_middleware.py @@ -0,0 +1,352 @@ +""" +RTC Payment Middleware for Flask +Implements x402 Payment Required protocol for RustChain micropayments. + +Usage: + from rtc_payment_middleware import require_rtc_payment + + @app.route('/api/data') + @require_rtc_payment(amount=0.001) + def get_data(): + return {'data': 'premium content'} +""" + +import functools +import hashlib +import json +import secrets +import time +from typing import Optional, Callable +from flask import request, Response, g +import requests +import nacl.signing +import nacl.encoding + +# RustChain node endpoint +RTC_NODE = "https://50.28.86.131" + +# Payment verification cache (in production, use Redis) +_payment_cache = {} +_rate_limits = {} # Global rate limit state for cleanup +CACHE_TTL = 300 # 5 minutes +RATE_LIMIT_TTL = 120 # 2 minutes for rate limit cleanup + + +def _cleanup_cache(): + """Clean up expired entries from caches to prevent memory leaks.""" + now = time.time() + + # Clean payment cache + expired_payments = [ + key for key, val in _payment_cache.items() + if now - val.get('timestamp', 0) > CACHE_TTL + ] + for key in expired_payments: + del _payment_cache[key] + + # Clean rate limits - remove entries from old minutes + current_minute = int(now // 60) + expired_rates = [ + key for key in _rate_limits.keys() + if int(key.split(':')[-1]) < current_minute - 2 + ] + for key in expired_rates: + del _rate_limits[key] + + +class RTCPaymentError(Exception): + """Base exception for RTC payment errors.""" + pass + + +class PaymentVerificationError(RTCPaymentError): + """Payment verification failed.""" + pass + + +class InsufficientPaymentError(RTCPaymentError): + """Payment amount insufficient.""" + pass + + +def verify_rtc_signature(message: bytes, signature: bytes, public_key: bytes) -> bool: + """ + Verify an Ed25519 signature. + + Args: + message: The original message that was signed + signature: The 64-byte Ed25519 signature + public_key: The 32-byte public key + + Returns: + True if signature is valid, False otherwise + """ + try: + verify_key = nacl.signing.VerifyKey(public_key) + verify_key.verify(message, signature) + return True + except nacl.exceptions.BadSignature: + return False + except Exception: + return False + + +def verify_payment_on_chain(tx_hash: str, expected_amount: float, recipient: str) -> bool: + """ + Verify a payment transaction on the RustChain ledger. + Queries the ledger for the specific transaction by hash. + + Args: + tx_hash: Transaction hash to verify + expected_amount: Expected payment amount in RTC + recipient: Expected recipient wallet address + + Returns: + True if payment is valid and confirmed + """ + try: + # Query ledger for the specific transaction + response = requests.get( + f"{RTC_NODE}/ledger", + params={"tx_hash": tx_hash}, + timeout=5, + verify=False # Self-signed cert + ) + if not response.ok: + return False + + ledger_data = response.json() + transactions = ledger_data.get("transactions", []) + + # Find the transaction by hash + for tx in transactions: + if tx.get("tx_hash") == tx_hash or tx.get("hash") == tx_hash: + # Verify recipient matches + tx_recipient = tx.get("to_address") or tx.get("recipient") + if tx_recipient != recipient: + return False + + # Verify amount is sufficient + tx_amount = tx.get("amount_rtc") or tx.get("amount", 0) + if float(tx_amount) < expected_amount: + return False + + return True + + # Transaction not found in ledger + return False + except (requests.RequestException, ValueError, json.JSONDecodeError): + return False + + +def generate_payment_nonce() -> str: + """Generate a unique cryptographically secure payment nonce.""" + return secrets.token_hex(16) + + +def create_402_response( + amount: float, + recipient: str, + currency: str = "RTC", + network: str = "rustchain", + nonce: Optional[str] = None +) -> Response: + """ + Create an HTTP 402 Payment Required response with x402 headers. + + Args: + amount: Payment amount required + recipient: Wallet address to receive payment + currency: Currency code (default: RTC) + network: Network identifier (default: rustchain) + nonce: Optional payment nonce for replay protection + + Returns: + Flask Response with 402 status and payment headers + """ + nonce = nonce or generate_payment_nonce() + + response = Response( + json.dumps({ + "error": "Payment Required", + "message": f"This endpoint requires a payment of {amount} {currency}", + "payment": { + "amount": amount, + "currency": currency, + "recipient": recipient, + "network": network, + "nonce": nonce, + "endpoint": f"{RTC_NODE}/wallet/transfer/signed" + } + }), + status=402, + mimetype='application/json' + ) + + # Set x402 payment headers + response.headers['X-Payment-Amount'] = str(amount) + response.headers['X-Payment-Currency'] = currency + response.headers['X-Payment-Address'] = recipient + response.headers['X-Payment-Network'] = network + response.headers['X-Payment-Nonce'] = nonce + response.headers['X-Payment-Endpoint'] = f"{RTC_NODE}/wallet/transfer/signed" + + return response + + +def extract_payment_proof(request) -> Optional[dict]: + """ + Extract payment proof from request headers. + + Expected headers: + X-Payment-TX: Transaction hash + X-Payment-Signature: Ed25519 signature of (nonce + tx_hash) + X-Payment-Sender: Sender's wallet address (public key hex) + X-Payment-Nonce: Original nonce from 402 response + + Returns: + Dict with payment proof or None if missing + """ + tx_hash = request.headers.get('X-Payment-TX') + signature = request.headers.get('X-Payment-Signature') + sender = request.headers.get('X-Payment-Sender') + nonce = request.headers.get('X-Payment-Nonce') + + if not all([tx_hash, signature, sender, nonce]): + return None + + return { + 'tx_hash': tx_hash, + 'signature': signature, + 'sender': sender, + 'nonce': nonce + } + + +def verify_payment_proof( + proof: dict, + expected_amount: float, + recipient: str +) -> bool: + """ + Verify payment proof from client. + + Args: + proof: Payment proof dict from extract_payment_proof + expected_amount: Expected payment amount + recipient: Expected recipient address + + Returns: + True if payment is verified + """ + # Check cache first + cache_key = f"{proof['tx_hash']}:{proof['nonce']}" + if cache_key in _payment_cache: + cached = _payment_cache[cache_key] + if time.time() - cached['timestamp'] < CACHE_TTL: + return cached['valid'] + + try: + # Verify signature + message = f"{proof['nonce']}:{proof['tx_hash']}".encode() + signature = bytes.fromhex(proof['signature']) + public_key = bytes.fromhex(proof['sender']) + + if not verify_rtc_signature(message, signature, public_key): + _payment_cache[cache_key] = {'valid': False, 'timestamp': time.time()} + return False + + # Verify on-chain + if not verify_payment_on_chain(proof['tx_hash'], expected_amount, recipient): + _payment_cache[cache_key] = {'valid': False, 'timestamp': time.time()} + return False + + _payment_cache[cache_key] = {'valid': True, 'timestamp': time.time()} + return True + + except Exception as e: + _payment_cache[cache_key] = {'valid': False, 'timestamp': time.time()} + return False + + +def require_rtc_payment( + amount: float, + recipient: Optional[str] = None, + rate_limit: int = 100 # Max requests per minute per sender +): + """ + Decorator to require RTC payment for an endpoint. + + Args: + amount: Payment amount in RTC + recipient: Wallet address to receive payment (defaults to env var) + rate_limit: Maximum requests per minute per sender + + Usage: + @app.route('/api/premium') + @require_rtc_payment(amount=0.001, recipient='gurgguda') + def premium_endpoint(): + return {'data': 'premium'} + """ + import os + recipient = recipient or os.environ.get('RTC_PAYMENT_ADDRESS', 'gurgguda') + + def decorator(f: Callable) -> Callable: + @functools.wraps(f) + def wrapper(*args, **kwargs): + global _rate_limits + + # Periodic cache cleanup to prevent memory leaks + _cleanup_cache() + + # Check for payment proof + proof = extract_payment_proof(request) + + if proof is None: + # No payment proof - return 402 + return create_402_response(amount, recipient) + + # Rate limiting (using global dict for cleanup) + sender = proof['sender'] + now = time.time() + minute_key = f"{sender}:{int(now // 60)}" + + if minute_key in _rate_limits: + if _rate_limits[minute_key] >= rate_limit: + return Response( + json.dumps({"error": "Rate limit exceeded"}), + status=429, + mimetype='application/json' + ) + _rate_limits[minute_key] += 1 + else: + _rate_limits[minute_key] = 1 + + # Verify payment + if not verify_payment_proof(proof, amount, recipient): + return Response( + json.dumps({"error": "Invalid payment proof"}), + status=402, + mimetype='application/json' + ) + + # Payment verified - store sender info and proceed + g.rtc_sender = sender + g.rtc_payment_amount = amount + + return f(*args, **kwargs) + + return wrapper + return decorator + + +# Convenience exports +__all__ = [ + 'require_rtc_payment', + 'create_402_response', + 'verify_payment_proof', + 'extract_payment_proof', + 'RTCPaymentError', + 'PaymentVerificationError', + 'InsufficientPaymentError' +]