x402-cli 1.1.1

A comprehensive developer CLI for project scaffolding, wallet management, and payment testing.
Documentation
use anyhow::{Context, Result};
use base64::prelude::*;
use colored::Colorize;
use reqwest::Client;
use serde::{Deserialize, Serialize};
use serde_json::json;
use std::time::Instant;

use base64::engine::general_purpose::STANDARD as Engine;

const FACILITATOR_URL: &str = "http://localhost:3001";

#[derive(Serialize, Clone)]
pub struct PaymentPayload {
    pub x402Version: u32,
    pub accepted: PaymentRequirements,
    pub payload: Payload,
}

#[derive(Serialize, Clone, Deserialize)]
pub struct PaymentRequirements {
    pub scheme: String,
    pub network: String,
    pub amount: String,
    pub asset: String,
    pub payTo: String,
    #[serde(flatten)]
    pub extra: Option<Extra>,
}

#[derive(Serialize, Clone, Deserialize, Default)]
pub struct Extra {
    #[serde(default)]
    pub sponsored: Option<bool>,
}

#[derive(Serialize, Clone)]
pub struct Payload {
    pub transaction: String,
    pub senderAuthenticator: String,
}

#[derive(Deserialize)]
struct VerifyResponse {
    pub isValid: bool,
    pub invalidReason: Option<String>,
    pub payer: Option<String>,
}

#[derive(Deserialize)]
struct SettleResponse {
    pub success: bool,
    pub transaction: String,
    pub network: String,
    pub payer: String,
}

#[derive(Deserialize)]
struct ErrorResponse {
    pub error: Option<String>,
}

pub async fn test_payment_flow(api_url: &str, _amount: u64) -> Result<()> {
    let client = Client::new();
    let start_time = Instant::now();

    let step1_msg = "  Step 1: Sending initial request...".dimmed();
    println!("{}", step1_msg);

    let response = client
        .get(api_url)
        .send()
        .await
        .context("Failed to send initial request")?;

    let status = response.status();
    println!("{}", format!("  Status: {}", status));

    if status.as_u16() != 402 {
        let status_str = format!("{}", status);
        println!("  ℹ Expected 402, got {}", status_str);
        println!("  ℹ Note: For real x402 testing, API must return 402 Payment Required");
        return Ok(());
    }

    println!("  ✓ Received 402 Payment Required");

    let payment_required_header = response
        .headers()
        .get("PAYMENT-REQUIRED")
        .context("Missing PAYMENT-REQUIRED header")?;

    let header_str = payment_required_header.to_str()?;
    let decoded_bytes = Engine.decode(header_str)
        .map_err(|e| anyhow::anyhow!("Failed to decode PAYMENT-REQUIRED header: {}", e))?;

    let requirements_str = String::from_utf8(decoded_bytes)
        .map_err(|e| anyhow::anyhow!("Failed to convert decoded bytes to UTF-8: {}", e))?;
    let requirements: PaymentRequirements = serde_json::from_str(&requirements_str)
        .map_err(|e| anyhow::anyhow!("Failed to parse PaymentRequirements: {}", e))?;

    println!(
        "{}",
        format!("  Payment Requirements: {} {} to {}",
                 requirements.amount.dimmed().cyan(), requirements.asset, requirements.payTo.dimmed())
    );

    println!("{}", "  Step 2: Building payment payload...".dimmed());

    let random_bytes: [u8; 32] = rand::random();
    let transaction_hash = format!("0x{}", hex::encode(random_bytes));
    let transaction_bytes = vec![0u8; 64];
    let sender_authenticator_bytes = vec![0u8; 64];

    let payload = Payload {
        transaction: Engine.encode(transaction_bytes.as_slice()),
        senderAuthenticator: Engine.encode(sender_authenticator_bytes.as_slice()),
    };

    let payment_payload = PaymentPayload {
        x402Version: 2,
        accepted: requirements.clone(),
        payload,
    };

    println!("{}", format!("  Transaction Hash: {}", transaction_hash.cyan()));

    println!("{}", "  Step 3: Verifying payment with facilitator...".dimmed());

    let verify_request = json!({
        "paymentPayload": payment_payload,
        "paymentRequirements": requirements
    });

    let verify_response = client
        .post(&format!("{}/verify", FACILITATOR_URL))
        .header("Content-Type", "application/json")
        .json(&verify_request)
        .send()
        .await
        .context("Failed to verify payment")?;

    if !verify_response.status().is_success() {
        let error_text = verify_response.text().await.unwrap_or_default();
        println!("{}", format!("  ⚠ Verification failed: {}", error_text).dimmed().yellow());
        return Ok(());
    }

    let verify_result: VerifyResponse = verify_response.json().await
        .context("Failed to parse verify response")?;

    if !verify_result.isValid {
        let reason = verify_result.invalidReason.unwrap_or_else(|| "Unknown".to_string());
        println!("{}", format!("  ✗ Payment invalid: {}", reason.bold().red()));
        return Ok(());
    }

    println!("{}", "  ✓ Payment verified".dimmed().green());

    println!("{}", "  Step 4: Settling payment with facilitator...".dimmed());

    let settle_response = client
        .post(&format!("{}/settle", FACILITATOR_URL))
        .header("Content-Type", "application/json")
        .json(&verify_request)
        .send()
        .await
        .context("Failed to settle payment")?;

    if !settle_response.status().is_success() {
        let error_text = settle_response.text().await.unwrap_or_default();
        println!("{}", format!("  ⚠ Settlement failed: {}", error_text).dimmed().yellow());
        return Ok(());
    }

    let settle_result: SettleResponse = settle_response.json().await
        .context("Failed to parse settle response")?;

    if !settle_result.success {
        println!("{}", "  ✗ Settlement failed".bold().red());
        return Ok(());
    }

    println!("{}", "  ✓ Payment settled".dimmed().green());
    println!("{}", format!("  Transaction: {}", settle_result.transaction.cyan()));
    println!("{}", format!("  Payer: {}", settle_result.payer.cyan()));

    println!("{}", "  Step 5: Retrying original request with payment proof...".dimmed());

    let payload_bytes = serde_json::to_vec(&payment_payload)
        .map_err(|e| anyhow::anyhow!("Failed to serialize payment payload: {}", e))?;
    let payment_signature = Engine.encode(&payload_bytes);

    let final_response = client
        .get(api_url)
        .header("PAYMENT-SIGNATURE", payment_signature)
        .send()
        .await
        .context("Failed to send final request")?;

    if final_response.status().is_success() {
        println!("{}", "  ✓ Received response from API".bold().green());
    } else {
        let final_status = final_response.status();
        println!("{}", format!("  ℹ API returned: {}", final_status).dimmed().yellow());
    }

    let elapsed = start_time.elapsed();
    println!();
    println!("{}", "Payment Flow Complete".cyan().bold());
    println!("{}", format!("Transaction: {}", settle_result.transaction.cyan()));
    println!("{}", format!("Payer: {}", settle_result.payer.cyan()));
    println!("{}", format!("Time: {}ms", elapsed.as_millis()));

    Ok(())
}