Skip to content

Commit cf272c5

Browse files
oceans404claude
andcommitted
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) <noreply@anthropic.com>
1 parent 8697778 commit cf272c5

2 files changed

Lines changed: 27 additions & 19 deletions

File tree

ows/crates/ows-pay/src/types.rs

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -114,6 +114,8 @@ pub struct X402Response {
114114
pub accepts: Vec<PaymentRequirements>,
115115
#[serde(default)]
116116
pub resource: Option<serde_json::Value>,
117+
#[serde(default, skip_serializing_if = "Option::is_none")]
118+
pub extensions: Option<serde_json::Value>,
117119
}
118120

119121
/// The signed payment payload sent to the server in the payment header.
@@ -142,6 +144,8 @@ pub struct PaymentPayloadV2 {
142144
pub accepted: PaymentRequirements,
143145
pub resource: Option<serde_json::Value>,
144146
pub payload: serde_json::Value,
147+
#[serde(default, skip_serializing_if = "Option::is_none")]
148+
pub extensions: Option<serde_json::Value>,
145149
}
146150

147151
#[derive(Debug, Clone, Serialize, Deserialize)]

ows/crates/ows-pay/src/x402.rs

Lines changed: 23 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -22,11 +22,11 @@ pub(crate) async fn handle_x402(
2222
resp_headers: &reqwest::header::HeaderMap,
2323
body_402: &str,
2424
) -> Result<PayResult, PayError> {
25-
let (x402_version, resource, requirements) = parse_requirements(resp_headers, body_402)?;
25+
let (x402_version, resource, requirements, extensions) = parse_requirements(resp_headers, body_402)?;
2626
let (req, network) = pick_payment_option(wallet, &requirements)?;
2727

2828
let (payload, payment_info) =
29-
build_signed_payment(wallet, req, &network, x402_version, resource)?;
29+
build_signed_payment(wallet, req, &network, x402_version, resource, extensions)?;
3030

3131
let payload_json = serde_json::to_string(&payload)?;
3232
let payload_b64 = B64.encode(payload_json.as_bytes());
@@ -58,9 +58,10 @@ fn build_signed_payment(
5858
network: &str,
5959
x402_version: u32,
6060
resource: Option<serde_json::Value>,
61+
extensions: Option<serde_json::Value>,
6162
) -> Result<(PaymentPayload, PaymentInfo), PayError> {
6263
match req.scheme.as_str() {
63-
"exact" => build_evm_exact(wallet, req, network, x402_version, resource),
64+
"exact" => build_evm_exact(wallet, req, network, x402_version, resource, extensions),
6465
scheme => Err(PayError::new(
6566
PayErrorCode::ProtocolUnknown,
6667
format!("unsupported payment scheme: {scheme}"),
@@ -75,6 +76,7 @@ fn build_evm_exact(
7576
network: &str,
7677
x402_version: u32,
7778
resource: Option<serde_json::Value>,
79+
extensions: Option<serde_json::Value>,
7880
) -> Result<(PaymentPayload, PaymentInfo), PayError> {
7981
let account = wallet.account(network)?;
8082

@@ -162,6 +164,7 @@ fn build_evm_exact(
162164
accepted: req.clone(),
163165
resource,
164166
payload: inner,
167+
extensions,
165168
})
166169
} else {
167170
PaymentPayload::V1(PaymentPayloadV1 {
@@ -189,7 +192,7 @@ fn build_evm_exact(
189192
fn parse_requirements(
190193
headers: &reqwest::header::HeaderMap,
191194
body_text: &str,
192-
) -> Result<(u32, Option<serde_json::Value>, Vec<PaymentRequirements>), PayError> {
195+
) -> Result<(u32, Option<serde_json::Value>, Vec<PaymentRequirements>, Option<serde_json::Value>), PayError> {
193196
for header_name in &[HEADER_PAYMENT_REQUIRED_V2, HEADER_PAYMENT_REQUIRED] {
194197
if let Some(header_val) = headers.get(*header_name) {
195198
if let Ok(header_str) = header_val.to_str() {
@@ -200,7 +203,7 @@ fn parse_requirements(
200203
HEADER_PAYMENT_REQUIRED_V2 => parsed.x402_version.unwrap_or(2),
201204
_ => parsed.x402_version.unwrap_or(1),
202205
};
203-
return Ok((version, parsed.resource, parsed.accepts));
206+
return Ok((version, parsed.resource, parsed.accepts, parsed.extensions));
204207
}
205208
}
206209
}
@@ -226,6 +229,7 @@ fn parse_requirements(
226229
parsed.x402_version.unwrap_or(1),
227230
parsed.resource,
228231
parsed.accepts,
232+
parsed.extensions,
229233
))
230234
}
231235

@@ -579,7 +583,7 @@ mod tests {
579583
})
580584
.to_string();
581585

582-
let (_, _, reqs) = parse_requirements(&headers, &body).unwrap();
586+
let (_, _, reqs, _) = parse_requirements(&headers, &body).unwrap();
583587
assert_eq!(reqs.len(), 1);
584588
assert_eq!(reqs[0].scheme, "exact");
585589
assert_eq!(reqs[0].network, "eip155:8453");
@@ -601,7 +605,7 @@ mod tests {
601605
let mut headers = HeaderMap::new();
602606
headers.insert("x-payment-required", encoded.parse().unwrap());
603607

604-
let (_, _, reqs) = parse_requirements(&headers, "not json").unwrap();
608+
let (_, _, reqs, _) = parse_requirements(&headers, "not json").unwrap();
605609
assert_eq!(reqs.len(), 1);
606610
assert_eq!(reqs[0].pay_to, "0xdef");
607611
}
@@ -622,7 +626,7 @@ mod tests {
622626
})
623627
.to_string();
624628

625-
let (_, _, reqs) = parse_requirements(&headers, &body).unwrap();
629+
let (_, _, reqs, _) = parse_requirements(&headers, &body).unwrap();
626630
assert_eq!(reqs[0].pay_to, "0xbbb");
627631
}
628632

@@ -642,7 +646,7 @@ mod tests {
642646
let mut headers = HeaderMap::new();
643647
headers.insert("payment-required", encoded.parse().unwrap());
644648

645-
let (_, _, reqs) = parse_requirements(&headers, "not json").unwrap();
649+
let (_, _, reqs, _) = parse_requirements(&headers, "not json").unwrap();
646650
assert_eq!(reqs.len(), 1);
647651
assert_eq!(reqs[0].pay_to, "0xv2");
648652
}
@@ -663,7 +667,7 @@ mod tests {
663667
let mut headers = HeaderMap::new();
664668
headers.insert("payment-required", encoded.parse().unwrap());
665669

666-
let (version, _, reqs) = parse_requirements(&headers, "not json").unwrap();
670+
let (version, _, reqs, _) = parse_requirements(&headers, "not json").unwrap();
667671
assert_eq!(version, 2);
668672
assert_eq!(reqs.len(), 1);
669673
assert_eq!(reqs[0].pay_to, "0xv2");
@@ -689,10 +693,10 @@ mod tests {
689693
let mut headers = HeaderMap::new();
690694
headers.insert("payment-required", encoded.parse().unwrap());
691695

692-
let (version, resource, reqs) = parse_requirements(&headers, "not json").unwrap();
696+
let (version, resource, reqs, extensions) = parse_requirements(&headers, "not json").unwrap();
693697
let (req, network) = pick_payment_option(&EvmWallet, &reqs).unwrap();
694698
let (payload, _) =
695-
build_signed_payment(&EvmWallet, req, &network, version, resource).unwrap();
699+
build_signed_payment(&EvmWallet, req, &network, version, resource, extensions).unwrap();
696700

697701
match payload {
698702
PaymentPayload::V2(v2) => {
@@ -725,7 +729,7 @@ mod tests {
725729
.unwrap(),
726730
);
727731

728-
let (_, _, reqs) = parse_requirements(&headers, "not json").unwrap();
732+
let (_, _, reqs, _) = parse_requirements(&headers, "not json").unwrap();
729733
assert_eq!(reqs[0].pay_to, "0xv2");
730734
}
731735

@@ -876,7 +880,7 @@ mod tests {
876880
#[test]
877881
fn build_evm_exact_produces_valid_payload() {
878882
let req = base_requirement();
879-
let (payload, info) = build_evm_exact(&EvmWallet, &req, "eip155:8453", 1, None).unwrap();
883+
let (payload, info) = build_evm_exact(&EvmWallet, &req, "eip155:8453", 1, None, None).unwrap();
880884

881885
let v1 = match &payload {
882886
PaymentPayload::V1(p) => p,
@@ -906,7 +910,7 @@ mod tests {
906910
"mimeType": "application/json"
907911
});
908912
let (payload, _) =
909-
build_evm_exact(&EvmWallet, &req, "eip155:8453", 2, Some(resource.clone())).unwrap();
913+
build_evm_exact(&EvmWallet, &req, "eip155:8453", 2, Some(resource.clone()), None).unwrap();
910914

911915
let v2 = match &payload {
912916
PaymentPayload::V2(p) => p,
@@ -928,7 +932,7 @@ mod tests {
928932
#[test]
929933
fn build_evm_exact_v2_with_no_resource() {
930934
let req = base_requirement();
931-
let (payload, _) = build_evm_exact(&EvmWallet, &req, "eip155:8453", 2, None).unwrap();
935+
let (payload, _) = build_evm_exact(&EvmWallet, &req, "eip155:8453", 2, None, None).unwrap();
932936

933937
let v2 = match &payload {
934938
PaymentPayload::V2(p) => p,
@@ -945,7 +949,7 @@ mod tests {
945949
req.description = None;
946950
req.resource = None;
947951

948-
let (payload, _) = build_evm_exact(&EvmWallet, &req, "eip155:8453", 2, None).unwrap();
952+
let (payload, _) = build_evm_exact(&EvmWallet, &req, "eip155:8453", 2, None, None).unwrap();
949953
let encoded = serde_json::to_value(payload).unwrap();
950954
let accepted = &encoded["accepted"];
951955

@@ -957,7 +961,7 @@ mod tests {
957961
#[test]
958962
fn build_evm_exact_fails_for_non_numeric_chain_id() {
959963
let req = base_requirement();
960-
let err = build_evm_exact(&EvmWallet, &req, "solana:mainnet", 1, None).unwrap_err();
964+
let err = build_evm_exact(&EvmWallet, &req, "solana:mainnet", 1, None, None).unwrap_err();
961965
assert_eq!(err.code, PayErrorCode::ProtocolMalformed);
962966
}
963967

@@ -982,7 +986,7 @@ mod tests {
982986
.to_string();
983987

984988
let headers = HeaderMap::new();
985-
let (_, _, reqs) = parse_requirements(&headers, &body).unwrap();
989+
let (_, _, reqs, _) = parse_requirements(&headers, &body).unwrap();
986990
let (req, network) = pick_payment_option(&EvmWallet, &reqs).unwrap();
987991
assert_eq!(req.pay_to, "0x7d9d1821d15B9e0b8Ab98A058361233E255E405D");
988992
assert_eq!(network, "eip155:8453"); // "base" resolved to CAIP-2

0 commit comments

Comments
 (0)