Skip to content

Commit 287bcfa

Browse files
committed
fix: forward x402 v2 extensions in payment payload for Bazaar discovery
The x402 v2 protocol includes an `extensions` field in the 402 PAYMENT-REQUIRED response that carries discovery metadata (e.g. extensions.bazaar with input/output schemas). Facilitators read this from the payment payload during settle to catalog services in the Bazaar discovery layer. Previously, OWS parsed `accepts` and `resource` from the 402 response but dropped `extensions`. The payment payload sent back in the PAYMENT-SIGNATURE header never included extensions, so facilitators couldn't index the service. Changes: - Add `extensions: Option<serde_json::Value>` to `X402Response` - Add `extensions: Option<serde_json::Value>` to `PaymentPayloadV2` - Thread extensions through parse_requirements -> build_signed_payment -> build_evm_exact -> PaymentPayloadV2 Fixes #196
1 parent 2fbd309 commit 287bcfa

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

@@ -166,6 +168,7 @@ fn build_evm_exact(
166168
accepted: req.clone(),
167169
resource,
168170
payload: inner,
171+
extensions,
169172
})
170173
} else {
171174
PaymentPayload::V1(PaymentPayloadV1 {
@@ -193,7 +196,7 @@ fn build_evm_exact(
193196
fn parse_requirements(
194197
headers: &reqwest::header::HeaderMap,
195198
body_text: &str,
196-
) -> Result<(u32, Option<serde_json::Value>, Vec<PaymentRequirements>), PayError> {
199+
) -> Result<(u32, Option<serde_json::Value>, Vec<PaymentRequirements>, Option<serde_json::Value>), PayError> {
197200
for header_name in &[HEADER_PAYMENT_REQUIRED_V2, HEADER_PAYMENT_REQUIRED] {
198201
if let Some(header_val) = headers.get(*header_name) {
199202
if let Ok(header_str) = header_val.to_str() {
@@ -204,7 +207,7 @@ fn parse_requirements(
204207
HEADER_PAYMENT_REQUIRED_V2 => parsed.x402_version.unwrap_or(2),
205208
_ => parsed.x402_version.unwrap_or(1),
206209
};
207-
return Ok((version, parsed.resource, parsed.accepts));
210+
return Ok((version, parsed.resource, parsed.accepts, parsed.extensions));
208211
}
209212
}
210213
}
@@ -230,6 +233,7 @@ fn parse_requirements(
230233
parsed.x402_version.unwrap_or(1),
231234
parsed.resource,
232235
parsed.accepts,
236+
parsed.extensions,
233237
))
234238
}
235239

@@ -583,7 +587,7 @@ mod tests {
583587
})
584588
.to_string();
585589

586-
let (_, _, reqs) = parse_requirements(&headers, &body).unwrap();
590+
let (_, _, reqs, _) = parse_requirements(&headers, &body).unwrap();
587591
assert_eq!(reqs.len(), 1);
588592
assert_eq!(reqs[0].scheme, "exact");
589593
assert_eq!(reqs[0].network, "eip155:8453");
@@ -605,7 +609,7 @@ mod tests {
605609
let mut headers = HeaderMap::new();
606610
headers.insert("x-payment-required", encoded.parse().unwrap());
607611

608-
let (_, _, reqs) = parse_requirements(&headers, "not json").unwrap();
612+
let (_, _, reqs, _) = parse_requirements(&headers, "not json").unwrap();
609613
assert_eq!(reqs.len(), 1);
610614
assert_eq!(reqs[0].pay_to, "0xdef");
611615
}
@@ -626,7 +630,7 @@ mod tests {
626630
})
627631
.to_string();
628632

629-
let (_, _, reqs) = parse_requirements(&headers, &body).unwrap();
633+
let (_, _, reqs, _) = parse_requirements(&headers, &body).unwrap();
630634
assert_eq!(reqs[0].pay_to, "0xbbb");
631635
}
632636

@@ -646,7 +650,7 @@ mod tests {
646650
let mut headers = HeaderMap::new();
647651
headers.insert("payment-required", encoded.parse().unwrap());
648652

649-
let (_, _, reqs) = parse_requirements(&headers, "not json").unwrap();
653+
let (_, _, reqs, _) = parse_requirements(&headers, "not json").unwrap();
650654
assert_eq!(reqs.len(), 1);
651655
assert_eq!(reqs[0].pay_to, "0xv2");
652656
}
@@ -667,7 +671,7 @@ mod tests {
667671
let mut headers = HeaderMap::new();
668672
headers.insert("payment-required", encoded.parse().unwrap());
669673

670-
let (version, _, reqs) = parse_requirements(&headers, "not json").unwrap();
674+
let (version, _, reqs, _) = parse_requirements(&headers, "not json").unwrap();
671675
assert_eq!(version, 2);
672676
assert_eq!(reqs.len(), 1);
673677
assert_eq!(reqs[0].pay_to, "0xv2");
@@ -693,10 +697,10 @@ mod tests {
693697
let mut headers = HeaderMap::new();
694698
headers.insert("payment-required", encoded.parse().unwrap());
695699

696-
let (version, resource, reqs) = parse_requirements(&headers, "not json").unwrap();
700+
let (version, resource, reqs, extensions) = parse_requirements(&headers, "not json").unwrap();
697701
let (req, network) = pick_payment_option(&EvmWallet, &reqs).unwrap();
698702
let (payload, _) =
699-
build_signed_payment(&EvmWallet, req, &network, version, resource).unwrap();
703+
build_signed_payment(&EvmWallet, req, &network, version, resource, extensions).unwrap();
700704

701705
match payload {
702706
PaymentPayload::V2(v2) => {
@@ -729,7 +733,7 @@ mod tests {
729733
.unwrap(),
730734
);
731735

732-
let (_, _, reqs) = parse_requirements(&headers, "not json").unwrap();
736+
let (_, _, reqs, _) = parse_requirements(&headers, "not json").unwrap();
733737
assert_eq!(reqs[0].pay_to, "0xv2");
734738
}
735739

@@ -880,7 +884,7 @@ mod tests {
880884
#[test]
881885
fn build_evm_exact_produces_valid_payload() {
882886
let req = base_requirement();
883-
let (payload, info) = build_evm_exact(&EvmWallet, &req, "eip155:8453", 1, None).unwrap();
887+
let (payload, info) = build_evm_exact(&EvmWallet, &req, "eip155:8453", 1, None, None).unwrap();
884888

885889
let v1 = match &payload {
886890
PaymentPayload::V1(p) => p,
@@ -910,7 +914,7 @@ mod tests {
910914
"mimeType": "application/json"
911915
});
912916
let (payload, _) =
913-
build_evm_exact(&EvmWallet, &req, "eip155:8453", 2, Some(resource.clone())).unwrap();
917+
build_evm_exact(&EvmWallet, &req, "eip155:8453", 2, Some(resource.clone()), None).unwrap();
914918

915919
let v2 = match &payload {
916920
PaymentPayload::V2(p) => p,
@@ -932,7 +936,7 @@ mod tests {
932936
#[test]
933937
fn build_evm_exact_v2_with_no_resource() {
934938
let req = base_requirement();
935-
let (payload, _) = build_evm_exact(&EvmWallet, &req, "eip155:8453", 2, None).unwrap();
939+
let (payload, _) = build_evm_exact(&EvmWallet, &req, "eip155:8453", 2, None, None).unwrap();
936940

937941
let v2 = match &payload {
938942
PaymentPayload::V2(p) => p,
@@ -949,7 +953,7 @@ mod tests {
949953
req.description = None;
950954
req.resource = None;
951955

952-
let (payload, _) = build_evm_exact(&EvmWallet, &req, "eip155:8453", 2, None).unwrap();
956+
let (payload, _) = build_evm_exact(&EvmWallet, &req, "eip155:8453", 2, None, None).unwrap();
953957
let encoded = serde_json::to_value(payload).unwrap();
954958
let accepted = &encoded["accepted"];
955959

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

@@ -986,7 +990,7 @@ mod tests {
986990
.to_string();
987991

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

0 commit comments

Comments
 (0)