diff --git a/constants/production.rs b/constants/production.rs index a260382..6ec0cf1 100644 --- a/constants/production.rs +++ b/constants/production.rs @@ -8,4 +8,11 @@ pub const HYPERGRID_ADDRESS: &str = "0xd65cb2ae7212e9b767c6953bb11cad1876d81cc8" pub const HYPERGRID_NAMESPACE_MINTER_ADDRESS: &str = "0x44a8Bd4f9370b248c91d54773Ac4a457B3454b50"; pub const HYPR_HASH: &str = "0x29575a1a0473dcc0e00d7137198ed715215de7bffd92911627d5e008410a5826"; pub const USDC_BASE_ADDRESS: &str = "0x833589fcd6edb6e08f4c7c32d4f71b54bda02913"; -pub const CIRCLE_PAYMASTER: &str = "0x0578cFB241215b77442a541325d6A4E6dFE700Ec"; \ No newline at end of file +pub const USDC_SEPOLIA_ADDRESS: &str = "0x036CbD53842c5426634e7929541eC2318f3dCF7e"; +pub const CIRCLE_PAYMASTER: &str = "0x0578cFB241215b77442a541325d6A4E6dFE700Ec"; + +// X402 Payment Configuration +pub const X402_PAYMENT_NETWORK: &str = "base"; +pub const USDC_EIP712_NAME: &str = "USD Coin"; +pub const USDC_EIP712_VERSION: &str = "2"; +pub const X402_FACILITATOR_BASE_URL: &str = "https://facilitator.x402.rs"; \ No newline at end of file diff --git a/constants/staging.rs b/constants/staging.rs index 25f17a8..16f4382 100644 --- a/constants/staging.rs +++ b/constants/staging.rs @@ -8,4 +8,11 @@ pub const HYPERGRID_ADDRESS: &str = "0x2138da52cbf52adf2e73139a898370e03bbebf0a" pub const HYPERGRID_NAMESPACE_MINTER_ADDRESS: &str = "0x44a8Bd4f9370b248c91d54773Ac4a457B3454b50"; pub const HYPR_HASH: &str = "0x29575a1a0473dcc0e00d7137198ed715215de7bffd92911627d5e008410a5826"; // TODO: Update with staging hash pub const USDC_BASE_ADDRESS: &str = "0x833589fcd6edb6e08f4c7c32d4f71b54bda02913"; -pub const CIRCLE_PAYMASTER: &str = "0x0578cFB241215b77442a541325d6A4E6dFE700Ec"; \ No newline at end of file +pub const USDC_SEPOLIA_ADDRESS: &str = "0x036CbD53842c5426634e7929541eC2318f3dCF7e"; +pub const CIRCLE_PAYMASTER: &str = "0x0578cFB241215b77442a541325d6A4E6dFE700Ec"; + +// X402 Payment Configuration +pub const X402_PAYMENT_NETWORK: &str = "base-sepolia"; +pub const USDC_EIP712_NAME: &str = "USDC"; +pub const USDC_EIP712_VERSION: &str = "2"; +pub const X402_FACILITATOR_BASE_URL: &str = "https://facilitator.x402.rs"; \ No newline at end of file diff --git a/metadata.json b/metadata.json index 4e08c90..53052cc 100644 --- a/metadata.json +++ b/metadata.json @@ -4,13 +4,8 @@ "image": "https://raw.githubusercontent.com/hyperware-ai/hpn/ccd0e9c0d08b2344b06ce4a5b8584f819b92e43e/hypergrid-logo.webp", "properties": { "package_name": "hypergrid", -<<<<<<< Updated upstream "current_version": "1.2.1", - "publisher": "ware.hypr", -======= - "current_version": "1.2.2", "publisher": "test.hypr", ->>>>>>> Stashed changes "mirrors": ["ware.hypr","sam.hypr", "backup-distro-node.os"], "code_hashes": { "1.0.0": "001a49117374abc3bdb38179d8ce05d76205b008bb55683e116be36f3e1635ce", diff --git a/provider/provider/Cargo.toml b/provider/provider/Cargo.toml index 158508a..d2d70d6 100644 --- a/provider/provider/Cargo.toml +++ b/provider/provider/Cargo.toml @@ -14,7 +14,7 @@ optional = true path = "../target/caller-utils" [dependencies.hyperprocess_macro] -branch = "develop" +branch = "set-response-body" git = "https://github.com/hyperware-ai/hyperprocess-macro" [dependencies.hyperware_process_lib] @@ -22,7 +22,7 @@ features = [ "hyperapp", "logging", ] -branch = "develop" +branch = "set-response-body" git = "https://github.com/hyperware-ai/process_lib" [dependencies.serde] diff --git a/provider/provider/src/lib.rs b/provider/provider/src/lib.rs index a039043..5ffe6c6 100644 --- a/provider/provider/src/lib.rs +++ b/provider/provider/src/lib.rs @@ -4,14 +4,23 @@ use hyperware_process_lib::logging::RemoteLogSettings; use hyperware_process_lib::{ eth::{Provider, Address as EthAddress}, get_state, + http::{ + StatusCode, + Method as HyperwareHttpMethod, + }, hypermap, logging::{debug, error, info, warn, init_logging, Level}, our, vfs::{create_drive, create_file, open_file}, Address, - hyperapp::{source, SaveOptions, sleep, get_server}, + hyperapp::{source, SaveOptions, sleep, get_server, set_response_status, set_response_body, add_response_header, get_request_header, get_request_url, get_parsed_query_params}, }; -use crate::constants::HYPR_SUFFIX; +use crate::constants::{ + HYPR_SUFFIX, + X402_FACILITATOR_BASE_URL, +}; +use crate::util::{parse_x_payment_header, build_payment_requirements}; +use base64ct::{Base64, Encoding}; use rmp_serde; use serde::{Deserialize, Serialize}; use serde_json; @@ -52,6 +61,155 @@ pub struct ValidateAndRegisterRequest { pub validation_arguments: Vec<(String, String)>, } +// x402 payment protocol structures +// These use camelCase field names per x402 spec (not Rust's snake_case convention) +#[derive(Clone, Debug, Serialize, Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct PaymentRequirements { + #[serde(rename = "x402Version")] + pub protocol_version: u8, + + #[serde(skip_serializing_if = "Option::is_none")] + pub accepts: Option>, + + #[serde(skip_serializing_if = "Option::is_none")] + pub error: Option, + + #[serde(skip_serializing_if = "Option::is_none")] + pub payer: Option, +} + +#[derive(Clone, Debug, Serialize, Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct AcceptedPayment { + pub scheme: String, + pub network: String, + pub max_amount_required: String, // USDC in atomic units (6 decimals) + pub resource: String, + pub description: String, + pub mime_type: String, + pub pay_to: String, // Ethereum address + pub max_timeout_seconds: u64, + pub asset: String, // USDC contract address + + #[serde(skip_serializing_if = "Option::is_none")] + pub output_schema: Option, + + #[serde(skip_serializing_if = "Option::is_none")] + pub extra: Option, +} + +// x402scan registry schema types +// FieldDef describes individual field requirements (type, required, enum, nested properties) +#[derive(Clone, Debug, Serialize, Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct FieldDef { + #[serde(skip_serializing_if = "Option::is_none")] + pub r#type: Option, + + #[serde(skip_serializing_if = "Option::is_none")] + pub required: Option, // Can be bool or string[] + + #[serde(skip_serializing_if = "Option::is_none")] + pub description: Option, + + #[serde(skip_serializing_if = "Option::is_none", rename = "enum")] + pub r#enum: Option>, + + #[serde(skip_serializing_if = "Option::is_none")] + pub properties: Option>, // Recursive for nested objects +} + +// InputSchema describes HTTP request requirements (method, params, body structure) +#[derive(Clone, Debug, Serialize, Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct InputSchema { + pub r#type: String, // Always "http" for our use case + + pub method: String, // "GET", "POST", etc + + #[serde(skip_serializing_if = "Option::is_none")] + pub body_type: Option, // "json", "form-data", "multipart-form-data", "text", "binary" + + #[serde(skip_serializing_if = "Option::is_none")] + pub query_params: Option>, + + #[serde(skip_serializing_if = "Option::is_none")] + pub body_fields: Option>, + + #[serde(skip_serializing_if = "Option::is_none")] + pub header_fields: Option>, +} + +// OutputSchema is the top-level schema wrapper for x402scan registry +#[derive(Clone, Debug, Serialize, Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct OutputSchema { + pub input: InputSchema, + + #[serde(skip_serializing_if = "Option::is_none")] + pub output: Option, // Flexible JSON for response format +} + +// X-PAYMENT header payload structures (from x402 client) +#[derive(Clone, Debug, Serialize, Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct PaymentPayload { + #[serde(rename = "x402Version")] + pub protocol_version: u8, + pub scheme: String, + pub network: String, + pub payload: ExactPayload, +} + +#[derive(Clone, Debug, Serialize, Deserialize)] +pub struct ExactPayload { + pub signature: String, + pub authorization: Authorization, +} + +#[derive(Clone, Debug, Serialize, Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct Authorization { + pub from: String, + pub to: String, + pub value: String, + pub valid_after: String, + pub valid_before: String, + pub nonce: String, +} + +// Facilitator API request/response structures +#[derive(Clone, Debug, Serialize, Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct FacilitatorVerifyRequest { + #[serde(rename = "x402Version")] + pub protocol_version: u8, + pub payment_payload: PaymentPayload, // Decoded payment object + pub payment_requirements: AcceptedPayment, // Single payment method, not the full PaymentRequirements wrapper +} + +#[derive(Clone, Debug, Serialize, Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct VerifyResponse { + pub is_valid: bool, + pub payer: String, + #[serde(skip_serializing_if = "Option::is_none")] + pub invalid_reason: Option, +} + +#[derive(Clone, Debug, Serialize, Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct SettleResponse { + pub success: bool, + pub payer: String, + #[serde(skip_serializing_if = "Option::is_none")] + pub transaction: Option, + pub network: String, + #[serde(skip_serializing_if = "Option::is_none")] + pub error_reason: Option, +} + // Type system for API endpoints #[derive(PartialEq, Clone, Debug, Serialize, Deserialize)] pub enum HttpMethod { @@ -357,6 +515,10 @@ impl Default for HypergridProviderState { path: "/api", config: HttpBindingConfig::new(false, false, false, None), }, + Binding::Http { + path: "/xfour", + config: HttpBindingConfig::new(false, false, false, None), + }, Binding::Ws { path: "/ws", config: WsBindingConfig::new(false, false, false), @@ -843,6 +1005,313 @@ impl HypergridProviderState { self.export_providers_json() } + /// HTTP 402 Payment Required endpoint for x402 micropayment protocol + /// + /// This endpoint implements the x402 payment flow: + /// 1. Initial request: Client sends query params (providername + provider args), gets 402 response with PaymentRequirements + /// 2. Payment retry: Client retries with X-PAYMENT header containing signed payment authorization + /// 3. Final response: After payment validation, return actual provider response with X-PAYMENT-RESPONSE header + #[http(path = "/xfour")] + async fn handle_xfour(&mut self) -> Result { + info!("x402 endpoint called"); + + // ===== CHECK FOR X-PAYMENT HEADER ===== + let x_payment_header = get_request_header("x-payment"); + + // ===== SHARED: QUERY PARAMETER VALIDATION ===== + let params = get_parsed_query_params(); + + let params = match params { + Some(p) if !p.is_empty() => p, + _ => { + let error_json = serde_json::json!({"error": "Missing query parameters. Expected ?providername=...&..."}); + let error_bytes = serde_json::to_vec(&error_json).unwrap(); + set_response_body(error_bytes); + set_response_status(StatusCode::BAD_REQUEST); + add_response_header("Content-Type".to_string(), "application/json".to_string()); + return Ok("".to_string()); + } + }; + + // ===== SHARED: PROVIDER NAME EXTRACTION ===== + let provider_name = match params.get("providername") { + Some(name) => name, + None => { + let error_json = serde_json::json!({"error": "Missing required parameter: providername"}); + let error_bytes = serde_json::to_vec(&error_json).unwrap(); + set_response_body(error_bytes); + set_response_status(StatusCode::BAD_REQUEST); + add_response_header("Content-Type".to_string(), "application/json".to_string()); + return Ok("".to_string()); + } + }; + + // ===== SHARED: PROVIDER LOOKUP ===== + let provider = match self.registered_providers.iter().find(|p| &p.provider_name == provider_name).cloned() { + Some(p) => p, + None => { + let error_json = serde_json::json!({"error": format!("Provider not found: {}", provider_name)}); + let error_bytes = serde_json::to_vec(&error_json).unwrap(); + set_response_body(error_bytes); + set_response_status(StatusCode::NOT_FOUND); + add_response_header("Content-Type".to_string(), "application/json".to_string()); + return Ok("".to_string()); + } + }; + + // ===== SHARED: GET RESOURCE URL ===== + // NOTE: Fallback URL uses test.hypr - this should never actually be used in production + // as get_request_url() should always succeed in HTTP context. If this fallback triggers, investigate. + let resource_url = get_request_url() + .unwrap_or_else(|| format!("http://unknown/provider:hypergrid:test.hypr/xfour?providername={}", provider_name)); + + // ===== BRANCH: PAYMENT VERIFICATION FLOW ===== + if let Some(x_payment_str) = x_payment_header { + info!("X-PAYMENT header detected, processing payment"); + info!("X-PAYMENT header received, length: {} chars", x_payment_str.len()); + + let payment_payload = match parse_x_payment_header(&x_payment_str) { + Ok(payload) => payload, + Err(e) => { + let error_json = serde_json::json!({"error": format!("Invalid X-PAYMENT header: {}", e)}); + let error_bytes = serde_json::to_vec(&error_json).unwrap(); + set_response_body(error_bytes); + set_response_status(StatusCode::BAD_REQUEST); + add_response_header("Content-Type".to_string(), "application/json".to_string()); + return Ok("".to_string()); + } + }; + + // Validate protocol version + if payment_payload.protocol_version != 1 { + error!("Unsupported x402 protocol version: {}", payment_payload.protocol_version); + let error_json = serde_json::json!({ + "error": format!("Unsupported x402 protocol version: {}. Expected version 1.", payment_payload.protocol_version) + }); + let error_bytes = serde_json::to_vec(&error_json).unwrap(); + set_response_body(error_bytes); + set_response_status(StatusCode::BAD_REQUEST); + add_response_header("Content-Type".to_string(), "application/json".to_string()); + return Ok("".to_string()); + } + + info!("Payment parsed - protocol v{}, scheme: {}, network: {}", + payment_payload.protocol_version, payment_payload.scheme, payment_payload.network); + + // Rebuild PaymentRequirements for verification + let payment_requirements = build_payment_requirements(&provider, &resource_url); + + // Find the matching payment method based on scheme and network + let payment_method = payment_requirements.accepts + .as_ref() + .and_then(|accepts| { + accepts.iter() + .find(|method| { + method.scheme == payment_payload.scheme && + method.network == payment_payload.network + }) + .cloned() + }); + + let payment_method = match payment_method { + Some(method) => { + info!("Found matching payment method for scheme: {}, network: {}", + payment_payload.scheme, payment_payload.network); + method + }, + None => { + error!("No matching payment method found for scheme: {}, network: {}", + payment_payload.scheme, payment_payload.network); + let error_json = serde_json::json!({ + "error": format!("No matching payment method for scheme: {}, network: {}", + payment_payload.scheme, payment_payload.network) + }); + let error_bytes = serde_json::to_vec(&error_json).unwrap(); + set_response_body(error_bytes); + set_response_status(StatusCode::BAD_REQUEST); + add_response_header("Content-Type".to_string(), "application/json".to_string()); + return Ok("".to_string()); + } + }; + + // Build facilitator verify request with the matched payment method + let verify_request = FacilitatorVerifyRequest { + protocol_version: 1, + payment_payload: payment_payload.clone(), + payment_requirements: payment_method, + }; + + let verify_body = serde_json::to_vec(&verify_request) + .map_err(|e| format!("Failed to serialize verify request: {}", e))?; + + // Call facilitator /verify + let verify_url = url::Url::parse(&format!("{}/verify", X402_FACILITATOR_BASE_URL)) + .map_err(|e| format!("Invalid facilitator URL: {}", e))?; + + let mut verify_headers = HashMap::new(); + verify_headers.insert("Content-Type".to_string(), "application/json".to_string()); + + let verify_response = match send_async_http_request( + HyperwareHttpMethod::POST, + verify_url, + Some(verify_headers), + 30, + verify_body, + ).await { + Ok(resp) => resp, + Err(e) => { + error!("Facilitator /verify request failed: {:?}", e); + let error_json = serde_json::json!({"error": "Payment verification service unavailable"}); + let error_bytes = serde_json::to_vec(&error_json).unwrap(); + set_response_body(error_bytes); + set_response_status(StatusCode::SERVICE_UNAVAILABLE); + add_response_header("Content-Type".to_string(), "application/json".to_string()); + return Ok("".to_string()); + } + }; + + info!("Facilitator /verify response: {:?}", verify_response.status()); + + // Parse verify response + let verify_result: VerifyResponse = serde_json::from_slice(verify_response.body()) + .map_err(|e| format!("Failed to parse verify response: {}", e))?; + + if !verify_result.is_valid { + warn!("Payment verification failed: {:?}", verify_result.invalid_reason); + let mut error_payment_reqs = payment_requirements.clone(); + error_payment_reqs.error = Some(verify_result.invalid_reason.unwrap_or_else(|| "Payment verification failed".to_string())); + let error_bytes = serde_json::to_vec(&error_payment_reqs).unwrap(); + set_response_body(error_bytes); + set_response_status(StatusCode::PAYMENT_REQUIRED); + add_response_header("Content-Type".to_string(), "application/json".to_string()); + return Ok("".to_string()); + } + + info!("Payment verified for payer: {}", verify_result.payer); + + // Call upstream provider API + let args_vec: Vec<(String, String)> = params.iter() + .filter(|(k, _)| k != &"providername") + .map(|(k, v)| (k.clone(), v.clone())) + .collect(); + + let upstream_response = match call_provider( + provider.provider_name.clone(), + provider.endpoint.clone(), + &args_vec, + our().node.to_string(), + ).await { + Ok(resp) => resp, + Err(e) => { + error!("Upstream API call failed: {}", e); + let error_json = serde_json::json!({"error": format!("Provider API call failed: {}", e)}); + let error_bytes = serde_json::to_vec(&error_json).unwrap(); + set_response_body(error_bytes); + set_response_status(StatusCode::BAD_GATEWAY); + add_response_header("Content-Type".to_string(), "application/json".to_string()); + return Ok("".to_string()); + } + }; + + info!("Upstream API call successful, settling payment"); + + // Call facilitator /settle + let settle_body = serde_json::to_vec(&verify_request) + .map_err(|e| format!("Failed to serialize settle request: {}", e))?; + + let settle_url = url::Url::parse(&format!("{}/settle", X402_FACILITATOR_BASE_URL)) + .map_err(|e| format!("Invalid facilitator URL: {}", e))?; + + let mut settle_headers = HashMap::new(); + settle_headers.insert("Content-Type".to_string(), "application/json".to_string()); + + // Call facilitator /settle and parse response + let settle_result: SettleResponse = match send_async_http_request( + HyperwareHttpMethod::POST, + settle_url, + Some(settle_headers), + 30, + settle_body, + ).await { + Ok(http_response) => { + info!("Facilitator /settle response: {:?}", http_response.status()); + + // Parse the HTTP response body into SettleResponse + serde_json::from_slice(http_response.body()) + .unwrap_or_else(|e| { + error!("Failed to parse settlement response: {}", e); + SettleResponse { + success: false, + payer: verify_result.payer.clone(), + transaction: None, + network: payment_payload.network.clone(), + error_reason: Some(format!("Failed to parse settlement response: {}", e)), + } + }) + } + Err(e) => { + // HTTP request failed - settlement service unavailable + error!("Facilitator /settle request failed but continuing: {:?}", e); + SettleResponse { + success: false, + payer: verify_result.payer.clone(), + transaction: None, + network: payment_payload.network.clone(), + error_reason: Some(format!("Settlement service error: {:?}", e)), + } + } + }; + + // Reject request if settlement fails - provider does not get paid + if !settle_result.success { + error!("Settlement failed, rejecting request: {:?}", settle_result.error_reason); + let error_json = serde_json::json!({ + "error": "Payment settlement failed. Please try again.", + "reason": settle_result.error_reason.unwrap_or_else(|| "Unknown settlement error".to_string()) + }); + let error_bytes = serde_json::to_vec(&error_json).unwrap(); + set_response_body(error_bytes); + set_response_status(StatusCode::PAYMENT_REQUIRED); + add_response_header("Content-Type".to_string(), "application/json".to_string()); + return Ok("".to_string()); + } + + // Encode settle response for X-PAYMENT-RESPONSE header + let settle_json = serde_json::to_vec(&settle_result) + .map_err(|e| format!("Failed to serialize settle response: {}", e))?; + + // Base64 encode for header + let encoded_len = Base64::encoded_len(&settle_json); + let mut buf = vec![0u8; encoded_len]; + let settle_b64 = Base64::encode(&settle_json, &mut buf) + .map_err(|e| format!("Failed to base64 encode settlement response: {}", e))? + .to_string(); + + // Return upstream response with X-PAYMENT-RESPONSE header + set_response_body(upstream_response.into_bytes()); + set_response_status(StatusCode::OK); + add_response_header("Content-Type".to_string(), "application/json".to_string()); + add_response_header("X-PAYMENT-RESPONSE".to_string(), settle_b64); + + info!("Payment flow completed successfully for provider '{}'", provider_name); + return Ok("".to_string()); + } + + // ===== BRANCH: 402 PAYMENT REQUIRED FLOW ===== + info!("No X-PAYMENT header, returning 402 Payment Required"); + + let payment_reqs = build_payment_requirements(&provider, &resource_url); + let payment_json = serde_json::to_vec(&payment_reqs) + .map_err(|e| format!("Failed to serialize payment requirements: {}", e))?; + + set_response_body(payment_json); + set_response_status(StatusCode::PAYMENT_REQUIRED); + add_response_header("Content-Type".to_string(), "application/json".to_string()); + + info!("Returning 402 Payment Required for provider '{}'", provider_name); + Ok("".to_string()) + } + #[http] async fn get_provider_namehash(&self, provider_name: String) -> Result { debug!("Getting namehash for provider: {}", provider_name); diff --git a/provider/provider/src/util.rs b/provider/provider/src/util.rs index c63c8ae..19bd8ff 100644 --- a/provider/provider/src/util.rs +++ b/provider/provider/src/util.rs @@ -1,5 +1,12 @@ -use crate::{EndpointDefinition, ProviderRequest}; -use crate::constants::{USDC_BASE_ADDRESS, WALLET_PREFIX}; +use crate::{ + EndpointDefinition, ProviderRequest, PaymentPayload, FieldDef, InputSchema, + OutputSchema, AcceptedPayment, PaymentRequirements, ParameterDefinition, + RegisteredProvider +}; +use crate::constants::{ + USDC_BASE_ADDRESS, WALLET_PREFIX, USDC_SEPOLIA_ADDRESS, USDC_EIP712_NAME, + USDC_EIP712_VERSION, X402_PAYMENT_NETWORK +}; use hyperware_process_lib::{ eth::{Address as EthAddress, EthError, TransactionReceipt, TxHash, U256}, get_blob, @@ -17,6 +24,7 @@ use serde_json; use std::collections::HashMap; use std::str::FromStr; use url::Url; +use base64ct::{Base64, Encoding}; /// Make an HTTP request using http-client and await its response. /// @@ -1065,3 +1073,122 @@ pub fn validate_response_status(response: &str) -> Result<(), String> { } } } + + + +/// Parse X-PAYMENT header value: base64 decode and deserialize to PaymentPayload +pub fn parse_x_payment_header(header_value: &str) -> Result { + // Allocate buffer for decoded data (base64 decoding produces smaller output than input) + let max_decoded_len = (header_value.len() * 3) / 4 + 3; + let mut decoded_bytes = vec![0u8; max_decoded_len]; + + let decoded_slice = Base64::decode(header_value.as_bytes(), &mut decoded_bytes) + .map_err(|e| format!("Failed to base64 decode X-PAYMENT header: {}", e))?; + + serde_json::from_slice(decoded_slice) + .map_err(|e| format!("Failed to parse X-PAYMENT JSON: {}", e)) +} + +/// Convert a ParameterDefinition to x402scan's FieldDef format +pub fn parameter_to_field_def(param: &ParameterDefinition) -> FieldDef { + FieldDef { + r#type: Some(param.value_type.clone()), + required: Some(serde_json::Value::Bool(true)), // All provider params are required + description: Some(format!("Parameter: {}", param.parameter_name)), + r#enum: None, + properties: None, + } +} + +/// Build InputSchema from provider's endpoint definition +pub fn build_input_schema(endpoint: &EndpointDefinition) -> InputSchema { + let mut query_params = HashMap::new(); + let mut body_fields = HashMap::new(); + let mut header_fields = HashMap::new(); + + // Add the fixed providername parameter + query_params.insert( + "providername".to_string(), + FieldDef { + r#type: Some("string".to_string()), + required: Some(serde_json::Value::Bool(true)), + description: Some("Name of the registered provider to call".to_string()), + r#enum: None, + properties: None, + } + ); + + // Convert provider's parameters by location + for param in &endpoint.parameters { + let field_def = parameter_to_field_def(param); + match param.location.as_str() { + "query" => { query_params.insert(param.parameter_name.clone(), field_def); }, + "body" => { body_fields.insert(param.parameter_name.clone(), field_def); }, + "header" => { header_fields.insert(param.parameter_name.clone(), field_def); }, + "path" => { + // Path params are part of the URL, not separate fields + // Could document them in description if needed + }, + _ => {}, + } + } + + InputSchema { + r#type: "http".to_string(), + method: endpoint.method.clone(), + body_type: if !body_fields.is_empty() { + Some("json".to_string()) + } else { + None + }, + query_params: if !query_params.is_empty() { Some(query_params) } else { None }, + body_fields: if !body_fields.is_empty() { Some(body_fields) } else { None }, + header_fields: if !header_fields.is_empty() { Some(header_fields) } else { None }, + } +} + +/// Build PaymentRequirements structure from provider and resource URL +pub fn build_payment_requirements(provider: &RegisteredProvider, resource_url: &str) -> PaymentRequirements { + // Convert USDC price to atomic units (6 decimals) + let max_amount_atomic = ((provider.price * 1_000_000.0).round() as u64).to_string(); + + // Build input schema from provider's endpoint definition + let input_schema = build_input_schema(&provider.endpoint); + + // Create output schema for x402scan registry compliance + let output_schema = OutputSchema { + input: input_schema, + output: Some(serde_json::json!({ + "type": "object", + "description": "Response from the provider's API endpoint" + })), + }; + + let accepted_payment = AcceptedPayment { + scheme: "exact".to_string(), + network: X402_PAYMENT_NETWORK.to_string(), + max_amount_required: max_amount_atomic, + resource: resource_url.to_string(), + description: provider.description.clone(), + mime_type: "application/json".to_string(), + pay_to: provider.registered_provider_wallet.clone(), + max_timeout_seconds: 60, + asset: if X402_PAYMENT_NETWORK == "base-sepolia" { + USDC_SEPOLIA_ADDRESS.to_string() + } else { + USDC_BASE_ADDRESS.to_string() + }, + output_schema: Some(output_schema), + extra: Some(serde_json::json!({ + "name": USDC_EIP712_NAME, + "version": USDC_EIP712_VERSION + })), + }; + + PaymentRequirements { + protocol_version: 1, + accepts: Some(vec![accepted_payment]), + error: Some("".to_string()), // Empty string for no error (x402 clients expect this field) + payer: None, + } +}