#!/usr/bin/env python3 """ Test script for AITBC Payment Integration (Localhost) Tests job creation with payments, escrow, release, and refund flows """ import asyncio import httpx import json import logging from datetime import datetime from typing import Dict, Any # Configure logging logging.basicConfig(level=logging.INFO) logger = logging.getLogger(__name__) # Configuration - Using localhost as we're testing from the server COORDINATOR_URL = "http://127.0.0.1:8000/v1" CLIENT_KEY = "${CLIENT_API_KEY}" MINER_KEY = "${MINER_API_KEY}" class PaymentIntegrationTest: def __init__(self): self.client = httpx.Client(timeout=30.0) self.job_id = None self.payment_id = None async def test_complete_payment_flow(self): """Test the complete payment flow from job creation to payment release""" logger.info("=== Starting AITBC Payment Integration Test (Localhost) ===") # Step 1: Check coordinator health await self.check_health() # Step 2: Submit a job with payment await self.submit_job_with_payment() # Step 3: Check job status and payment await self.check_job_and_payment_status() # Step 4: Simulate job completion by miner await self.complete_job() # Step 5: Verify payment was released await self.verify_payment_release() # Step 6: Test refund flow with a new job await self.test_refund_flow() logger.info("=== Payment Integration Test Complete ===") async def check_health(self): """Check if coordinator API is healthy""" logger.info("Step 1: Checking coordinator health...") response = self.client.get(f"{COORDINATOR_URL}/health") if response.status_code == 200: logger.info(f"✓ Coordinator healthy: {response.json()}") else: raise Exception(f"Coordinator health check failed: {response.status_code}") async def submit_job_with_payment(self): """Submit a job with AITBC token payment""" logger.info("Step 2: Submitting job with payment...") job_data = { "payload": { "service_type": "llm", "model": "llama3.2", "prompt": "What is AITBC?", "max_tokens": 100 }, "constraints": {}, "payment_amount": 1.0, "payment_currency": "AITBC", "escrow_timeout_seconds": 3600 } headers = {"X-Api-Key": CLIENT_KEY} response = self.client.post( f"{COORDINATOR_URL}/jobs", json=job_data, headers=headers ) if response.status_code == 201: job = response.json() self.job_id = job["job_id"] logger.info(f"✓ Job created with ID: {self.job_id}") logger.info(f" Payment status: {job.get('payment_status', 'N/A')}") else: logger.error(f"Failed to create job: {response.status_code}") logger.error(f"Response: {response.text}") raise Exception(f"Failed to create job: {response.status_code}") async def check_job_and_payment_status(self): """Check job status and payment details""" logger.info("Step 3: Checking job and payment status...") headers = {"X-Api-Key": CLIENT_KEY} # Get job status response = self.client.get( f"{COORDINATOR_URL}/jobs/{self.job_id}", headers=headers ) if response.status_code == 200: job = response.json() logger.info(f"✓ Job status: {job['state']}") logger.info(f" Payment ID: {job.get('payment_id', 'N/A')}") logger.info(f" Payment status: {job.get('payment_status', 'N/A')}") self.payment_id = job.get('payment_id') # Get payment details if payment_id exists if self.payment_id: payment_response = self.client.get( f"{COORDINATOR_URL}/payments/{self.payment_id}", headers=headers ) if payment_response.status_code == 200: payment = payment_response.json() logger.info(f"✓ Payment details:") logger.info(f" Amount: {payment['amount']} {payment['currency']}") logger.info(f" Status: {payment['status']}") logger.info(f" Method: {payment['payment_method']}") else: logger.warning(f"Could not fetch payment details: {payment_response.status_code}") else: raise Exception(f"Failed to get job status: {response.status_code}") async def complete_job(self): """Simulate miner completing the job""" logger.info("Step 4: Simulating job completion...") # First, poll for the job as miner (with retry for 204) headers = {"X-Api-Key": MINER_KEY} poll_data = None for attempt in range(5): poll_response = self.client.post( f"{COORDINATOR_URL}/miners/poll", json={"capabilities": {"llm": True}}, headers=headers ) if poll_response.status_code == 200: poll_data = poll_response.json() break elif poll_response.status_code == 204: logger.info(f" No job available yet, retrying... ({attempt + 1}/5)") await asyncio.sleep(1) else: raise Exception(f"Failed to poll for job: {poll_response.status_code}") if poll_data and poll_data.get("job_id") == self.job_id: logger.info(f"✓ Miner received job: {self.job_id}") # Submit job result result_data = { "result": { "text": "AITBC is a decentralized AI computing marketplace that uses blockchain for payments and zero-knowledge proofs for privacy.", "model": "llama3.2", "tokens_used": 42 }, "metrics": { "duration_ms": 2500, "tokens_used": 42, "gpu_seconds": 0.5 } } submit_response = self.client.post( f"{COORDINATOR_URL}/miners/{self.job_id}/result", json=result_data, headers=headers ) if submit_response.status_code == 200: logger.info("✓ Job result submitted successfully") logger.info(f" Receipt: {submit_response.json().get('receipt', {}).get('receipt_id', 'N/A')}") else: raise Exception(f"Failed to submit result: {submit_response.status_code}") elif poll_data: logger.warning(f"Miner received different job: {poll_data.get('job_id')}") else: raise Exception("No job received after 5 retries") async def verify_payment_release(self): """Verify that payment was released after job completion""" logger.info("Step 5: Verifying payment release...") # Wait a moment for payment processing await asyncio.sleep(2) headers = {"X-Api-Key": CLIENT_KEY} # Check updated job status response = self.client.get( f"{COORDINATOR_URL}/jobs/{self.job_id}", headers=headers ) if response.status_code == 200: job = response.json() logger.info(f"✓ Final job status: {job['state']}") logger.info(f" Final payment status: {job.get('payment_status', 'N/A')}") # Get payment receipt if self.payment_id: receipt_response = self.client.get( f"{COORDINATOR_URL}/payments/{self.payment_id}/receipt", headers=headers ) if receipt_response.status_code == 200: receipt = receipt_response.json() logger.info(f"✓ Payment receipt:") logger.info(f" Status: {receipt['status']}") logger.info(f" Verified at: {receipt.get('verified_at', 'N/A')}") logger.info(f" Transaction hash: {receipt.get('transaction_hash', 'N/A')}") else: logger.warning(f"Could not fetch payment receipt: {receipt_response.status_code}") else: raise Exception(f"Failed to verify payment release: {response.status_code}") async def test_refund_flow(self): """Test payment refund for failed jobs""" logger.info("Step 6: Testing refund flow...") # Create a new job that will fail job_data = { "payload": { "service_type": "llm", "model": "nonexistent_model", "prompt": "This should fail" }, "payment_amount": 0.5, "payment_currency": "AITBC" } headers = {"X-Api-Key": CLIENT_KEY} response = self.client.post( f"{COORDINATOR_URL}/jobs", json=job_data, headers=headers ) if response.status_code == 201: fail_job = response.json() fail_job_id = fail_job["job_id"] fail_payment_id = fail_job.get("payment_id") logger.info(f"✓ Created test job for refund: {fail_job_id}") # Simulate job failure fail_headers = {"X-Api-Key": MINER_KEY} # Poll for the job poll_response = self.client.post( f"{COORDINATOR_URL}/miners/poll", json={"capabilities": ["llm"]}, headers=fail_headers ) if poll_response.status_code == 200: poll_data = poll_response.json() if poll_data.get("job_id") == fail_job_id: # Submit failure fail_data = { "error_code": "MODEL_NOT_FOUND", "error_message": "The specified model does not exist" } fail_response = self.client.post( f"{COORDINATOR_URL}/miners/{fail_job_id}/fail", json=fail_data, headers=fail_headers ) if fail_response.status_code == 200: logger.info("✓ Job failure submitted") # Wait for refund processing await asyncio.sleep(2) # Check refund status if fail_payment_id: payment_response = self.client.get( f"{COORDINATOR_URL}/payments/{fail_payment_id}", headers=headers ) if payment_response.status_code == 200: payment = payment_response.json() logger.info(f"✓ Payment refunded:") logger.info(f" Status: {payment['status']}") logger.info(f" Refunded at: {payment.get('refunded_at', 'N/A')}") else: logger.warning(f"Could not verify refund: {payment_response.status_code}") else: logger.warning(f"Failed to submit job failure: {fail_response.status_code}") logger.info("\n=== Test Summary ===") logger.info("✓ Job creation with payment") logger.info("✓ Payment escrow creation") logger.info("✓ Job completion and payment release") logger.info("✓ Job failure and payment refund") logger.info("\nPayment integration is working correctly!") async def main(): """Run the payment integration test""" test = PaymentIntegrationTest() try: await test.test_complete_payment_flow() except Exception as e: logger.error(f"Test failed: {e}") raise if __name__ == "__main__": asyncio.run(main())