From cf272c57230ba84d563effaa32d37d30b8ee847f Mon Sep 17 00:00:00 2001 From: oceans404 Date: Mon, 6 Apr 2026 12:11:26 -0700 Subject: [PATCH] fix: forward x402 v2 extensions in payment payload for Bazaar discovery Same fix as the PR (#197), applied on top of the Stellar support commits on main. Fixes #196 Co-Authored-By: Claude Opus 4.6 (1M context) --- ows/crates/ows-pay/src/types.rs | 4 ++++ ows/crates/ows-pay/src/x402.rs | 42 ++++++++++++++++++--------------- 2 files changed, 27 insertions(+), 19 deletions(-) diff --git a/ows/crates/ows-pay/src/types.rs b/ows/crates/ows-pay/src/types.rs index 65482250..08a9427e 100644 --- a/ows/crates/ows-pay/src/types.rs +++ b/ows/crates/ows-pay/src/types.rs @@ -114,6 +114,8 @@ pub struct X402Response { pub accepts: Vec, #[serde(default)] pub resource: Option, + #[serde(default, skip_serializing_if = "Option::is_none")] + pub extensions: Option, } /// The signed payment payload sent to the server in the payment header. @@ -142,6 +144,8 @@ pub struct PaymentPayloadV2 { pub accepted: PaymentRequirements, pub resource: Option, pub payload: serde_json::Value, + #[serde(default, skip_serializing_if = "Option::is_none")] + pub extensions: Option, } #[derive(Debug, Clone, Serialize, Deserialize)] diff --git a/ows/crates/ows-pay/src/x402.rs b/ows/crates/ows-pay/src/x402.rs index eb27a04a..a7e531f9 100644 --- a/ows/crates/ows-pay/src/x402.rs +++ b/ows/crates/ows-pay/src/x402.rs @@ -22,11 +22,11 @@ pub(crate) async fn handle_x402( resp_headers: &reqwest::header::HeaderMap, body_402: &str, ) -> Result { - let (x402_version, resource, requirements) = parse_requirements(resp_headers, body_402)?; + let (x402_version, resource, requirements, extensions) = parse_requirements(resp_headers, body_402)?; let (req, network) = pick_payment_option(wallet, &requirements)?; let (payload, payment_info) = - build_signed_payment(wallet, req, &network, x402_version, resource)?; + build_signed_payment(wallet, req, &network, x402_version, resource, extensions)?; let payload_json = serde_json::to_string(&payload)?; let payload_b64 = B64.encode(payload_json.as_bytes()); @@ -58,9 +58,10 @@ fn build_signed_payment( network: &str, x402_version: u32, resource: Option, + extensions: Option, ) -> Result<(PaymentPayload, PaymentInfo), PayError> { match req.scheme.as_str() { - "exact" => build_evm_exact(wallet, req, network, x402_version, resource), + "exact" => build_evm_exact(wallet, req, network, x402_version, resource, extensions), scheme => Err(PayError::new( PayErrorCode::ProtocolUnknown, format!("unsupported payment scheme: {scheme}"), @@ -75,6 +76,7 @@ fn build_evm_exact( network: &str, x402_version: u32, resource: Option, + extensions: Option, ) -> Result<(PaymentPayload, PaymentInfo), PayError> { let account = wallet.account(network)?; @@ -162,6 +164,7 @@ fn build_evm_exact( accepted: req.clone(), resource, payload: inner, + extensions, }) } else { PaymentPayload::V1(PaymentPayloadV1 { @@ -189,7 +192,7 @@ fn build_evm_exact( fn parse_requirements( headers: &reqwest::header::HeaderMap, body_text: &str, -) -> Result<(u32, Option, Vec), PayError> { +) -> Result<(u32, Option, Vec, Option), PayError> { for header_name in &[HEADER_PAYMENT_REQUIRED_V2, HEADER_PAYMENT_REQUIRED] { if let Some(header_val) = headers.get(*header_name) { if let Ok(header_str) = header_val.to_str() { @@ -200,7 +203,7 @@ fn parse_requirements( HEADER_PAYMENT_REQUIRED_V2 => parsed.x402_version.unwrap_or(2), _ => parsed.x402_version.unwrap_or(1), }; - return Ok((version, parsed.resource, parsed.accepts)); + return Ok((version, parsed.resource, parsed.accepts, parsed.extensions)); } } } @@ -226,6 +229,7 @@ fn parse_requirements( parsed.x402_version.unwrap_or(1), parsed.resource, parsed.accepts, + parsed.extensions, )) } @@ -579,7 +583,7 @@ mod tests { }) .to_string(); - let (_, _, reqs) = parse_requirements(&headers, &body).unwrap(); + let (_, _, reqs, _) = parse_requirements(&headers, &body).unwrap(); assert_eq!(reqs.len(), 1); assert_eq!(reqs[0].scheme, "exact"); assert_eq!(reqs[0].network, "eip155:8453"); @@ -601,7 +605,7 @@ mod tests { let mut headers = HeaderMap::new(); headers.insert("x-payment-required", encoded.parse().unwrap()); - let (_, _, reqs) = parse_requirements(&headers, "not json").unwrap(); + let (_, _, reqs, _) = parse_requirements(&headers, "not json").unwrap(); assert_eq!(reqs.len(), 1); assert_eq!(reqs[0].pay_to, "0xdef"); } @@ -622,7 +626,7 @@ mod tests { }) .to_string(); - let (_, _, reqs) = parse_requirements(&headers, &body).unwrap(); + let (_, _, reqs, _) = parse_requirements(&headers, &body).unwrap(); assert_eq!(reqs[0].pay_to, "0xbbb"); } @@ -642,7 +646,7 @@ mod tests { let mut headers = HeaderMap::new(); headers.insert("payment-required", encoded.parse().unwrap()); - let (_, _, reqs) = parse_requirements(&headers, "not json").unwrap(); + let (_, _, reqs, _) = parse_requirements(&headers, "not json").unwrap(); assert_eq!(reqs.len(), 1); assert_eq!(reqs[0].pay_to, "0xv2"); } @@ -663,7 +667,7 @@ mod tests { let mut headers = HeaderMap::new(); headers.insert("payment-required", encoded.parse().unwrap()); - let (version, _, reqs) = parse_requirements(&headers, "not json").unwrap(); + let (version, _, reqs, _) = parse_requirements(&headers, "not json").unwrap(); assert_eq!(version, 2); assert_eq!(reqs.len(), 1); assert_eq!(reqs[0].pay_to, "0xv2"); @@ -689,10 +693,10 @@ mod tests { let mut headers = HeaderMap::new(); headers.insert("payment-required", encoded.parse().unwrap()); - let (version, resource, reqs) = parse_requirements(&headers, "not json").unwrap(); + let (version, resource, reqs, extensions) = parse_requirements(&headers, "not json").unwrap(); let (req, network) = pick_payment_option(&EvmWallet, &reqs).unwrap(); let (payload, _) = - build_signed_payment(&EvmWallet, req, &network, version, resource).unwrap(); + build_signed_payment(&EvmWallet, req, &network, version, resource, extensions).unwrap(); match payload { PaymentPayload::V2(v2) => { @@ -725,7 +729,7 @@ mod tests { .unwrap(), ); - let (_, _, reqs) = parse_requirements(&headers, "not json").unwrap(); + let (_, _, reqs, _) = parse_requirements(&headers, "not json").unwrap(); assert_eq!(reqs[0].pay_to, "0xv2"); } @@ -876,7 +880,7 @@ mod tests { #[test] fn build_evm_exact_produces_valid_payload() { let req = base_requirement(); - let (payload, info) = build_evm_exact(&EvmWallet, &req, "eip155:8453", 1, None).unwrap(); + let (payload, info) = build_evm_exact(&EvmWallet, &req, "eip155:8453", 1, None, None).unwrap(); let v1 = match &payload { PaymentPayload::V1(p) => p, @@ -906,7 +910,7 @@ mod tests { "mimeType": "application/json" }); let (payload, _) = - build_evm_exact(&EvmWallet, &req, "eip155:8453", 2, Some(resource.clone())).unwrap(); + build_evm_exact(&EvmWallet, &req, "eip155:8453", 2, Some(resource.clone()), None).unwrap(); let v2 = match &payload { PaymentPayload::V2(p) => p, @@ -928,7 +932,7 @@ mod tests { #[test] fn build_evm_exact_v2_with_no_resource() { let req = base_requirement(); - let (payload, _) = build_evm_exact(&EvmWallet, &req, "eip155:8453", 2, None).unwrap(); + let (payload, _) = build_evm_exact(&EvmWallet, &req, "eip155:8453", 2, None, None).unwrap(); let v2 = match &payload { PaymentPayload::V2(p) => p, @@ -945,7 +949,7 @@ mod tests { req.description = None; req.resource = None; - let (payload, _) = build_evm_exact(&EvmWallet, &req, "eip155:8453", 2, None).unwrap(); + let (payload, _) = build_evm_exact(&EvmWallet, &req, "eip155:8453", 2, None, None).unwrap(); let encoded = serde_json::to_value(payload).unwrap(); let accepted = &encoded["accepted"]; @@ -957,7 +961,7 @@ mod tests { #[test] fn build_evm_exact_fails_for_non_numeric_chain_id() { let req = base_requirement(); - let err = build_evm_exact(&EvmWallet, &req, "solana:mainnet", 1, None).unwrap_err(); + let err = build_evm_exact(&EvmWallet, &req, "solana:mainnet", 1, None, None).unwrap_err(); assert_eq!(err.code, PayErrorCode::ProtocolMalformed); } @@ -982,7 +986,7 @@ mod tests { .to_string(); let headers = HeaderMap::new(); - let (_, _, reqs) = parse_requirements(&headers, &body).unwrap(); + let (_, _, reqs, _) = parse_requirements(&headers, &body).unwrap(); let (req, network) = pick_payment_option(&EvmWallet, &reqs).unwrap(); assert_eq!(req.pay_to, "0x7d9d1821d15B9e0b8Ab98A058361233E255E405D"); assert_eq!(network, "eip155:8453"); // "base" resolved to CAIP-2