x402_extensions/
sign_in_with_x.rs1use bon::Builder;
38use schemars::JsonSchema;
39use serde::{Deserialize, Serialize};
40use x402_core::types::{AnyJson, ExtensionInfo};
41
42#[derive(Builder, Debug, Clone, Serialize, Deserialize, JsonSchema)]
47#[serde(rename_all = "camelCase")]
48pub struct SignInWithXInfo {
49 #[builder(into)]
51 pub domain: String,
52
53 #[builder(into)]
55 pub uri: String,
56
57 #[builder(into)]
59 pub version: String,
60
61 #[builder(into)]
63 pub nonce: String,
64
65 #[builder(into)]
67 pub issued_at: String,
68
69 #[serde(skip_serializing_if = "Option::is_none")]
71 #[builder(into)]
72 pub expiration_time: Option<String>,
73
74 #[serde(skip_serializing_if = "Option::is_none")]
76 #[builder(into)]
77 pub statement: Option<String>,
78
79 #[serde(skip_serializing_if = "Option::is_none")]
81 pub resources: Option<Vec<String>>,
82}
83
84impl ExtensionInfo for SignInWithXInfo {
85 const ID: &'static str = "sign-in-with-x";
86
87 fn schema() -> AnyJson {
88 let schema = schemars::schema_for!(SignInWithXInfo);
89 serde_json::to_value(&schema).expect("SignInWithXInfo schema generation should not fail")
90 }
91}
92
93#[derive(Builder, Debug, Clone, Serialize, Deserialize, JsonSchema)]
97#[serde(rename_all = "camelCase")]
98pub struct SupportedChain {
99 #[builder(into)]
101 pub chain_id: String,
102
103 #[serde(rename = "type")]
105 #[builder(into)]
106 pub chain_type: String,
107}
108
109#[cfg(test)]
110mod tests {
111 use serde_json::json;
112 use x402_core::types::{Extension, ExtensionMapInsert, Record};
113
114 use super::*;
115
116 #[test]
117 fn sign_in_with_x_basic() {
118 let info = SignInWithXInfo::builder()
119 .domain("api.example.com")
120 .uri("https://api.example.com/premium-data")
121 .version("1")
122 .nonce("a1b2c3d4e5f67890a1b2c3d4e5f67890")
123 .issued_at("2024-01-15T10:30:00.000Z")
124 .build();
125
126 let ext = Extension::typed(info);
127 let (key, transport_ext) = ext.into_pair();
128
129 assert_eq!(key, "sign-in-with-x");
130 assert_eq!(transport_ext.info["domain"], "api.example.com");
131 assert_eq!(transport_ext.info["version"], "1");
132 }
133
134 #[test]
135 fn sign_in_with_x_with_optional_fields() {
136 let info = SignInWithXInfo::builder()
137 .domain("api.example.com")
138 .uri("https://api.example.com/premium-data")
139 .version("1")
140 .nonce("a1b2c3d4e5f67890a1b2c3d4e5f67890")
141 .issued_at("2024-01-15T10:30:00.000Z")
142 .expiration_time("2024-01-15T10:35:00.000Z")
143 .statement("Sign in to access premium data")
144 .resources(vec!["https://api.example.com/premium-data".to_string()])
145 .build();
146
147 let (_, ext) = Extension::typed(info).into_pair();
148
149 assert_eq!(ext.info["statement"], "Sign in to access premium data");
150 assert_eq!(ext.info["expirationTime"], "2024-01-15T10:35:00.000Z");
151 assert!(ext.info["resources"].is_array());
152 }
153
154 #[test]
155 fn sign_in_with_x_with_supported_chains() {
156 let info = SignInWithXInfo::builder()
157 .domain("api.example.com")
158 .uri("https://api.example.com/premium-data")
159 .version("1")
160 .nonce("a1b2c3d4e5f67890")
161 .issued_at("2024-01-15T10:30:00.000Z")
162 .build();
163
164 let chains = vec![
165 SupportedChain::builder()
166 .chain_id("eip155:8453")
167 .chain_type("eip191")
168 .build(),
169 ];
170
171 let ext = Extension::typed(info)
172 .with_extra("supportedChains", serde_json::to_value(&chains).unwrap());
173
174 let (key, transport_ext) = ext.into_pair();
175 assert_eq!(key, "sign-in-with-x");
176
177 let json = serde_json::to_value(&transport_ext).unwrap();
179 assert!(json.get("supportedChains").is_some());
180 assert_eq!(json["supportedChains"][0]["chainId"], "eip155:8453");
181 assert_eq!(json["supportedChains"][0]["type"], "eip191");
182 }
183
184 #[test]
185 fn sign_in_with_x_schema_is_generated() {
186 let schema = <SignInWithXInfo as ExtensionInfo>::schema();
187 assert!(schema.is_object());
188 }
189
190 #[test]
191 fn sign_in_with_x_insert_into_map() {
192 let mut extensions: Record<Extension> = Record::new();
193
194 let info = SignInWithXInfo::builder()
195 .domain("example.com")
196 .uri("https://example.com")
197 .version("1")
198 .nonce("test_nonce")
199 .issued_at("2024-01-01T00:00:00.000Z")
200 .build();
201
202 extensions.insert_typed(Extension::typed(info));
203
204 assert!(extensions.contains_key("sign-in-with-x"));
205 assert_eq!(extensions["sign-in-with-x"].info["domain"], "example.com");
206 }
207
208 #[test]
209 fn sign_in_with_x_roundtrip() {
210 let info = SignInWithXInfo::builder()
211 .domain("api.example.com")
212 .uri("https://api.example.com/resource")
213 .version("1")
214 .nonce("nonce123")
215 .issued_at("2024-01-15T10:30:00.000Z")
216 .expiration_time("2024-01-15T10:35:00.000Z")
217 .statement("Test statement")
218 .build();
219
220 let json = serde_json::to_value(&info).unwrap();
221 let deserialized: SignInWithXInfo = serde_json::from_value(json.clone()).unwrap();
222 let re_serialized = serde_json::to_value(&deserialized).unwrap();
223
224 assert_eq!(json, re_serialized);
225 }
226
227 #[test]
228 fn sign_in_with_x_transport_roundtrip_with_extra() {
229 let info = SignInWithXInfo::builder()
230 .domain("api.example.com")
231 .uri("https://api.example.com/resource")
232 .version("1")
233 .nonce("nonce123")
234 .issued_at("2024-01-15T10:30:00.000Z")
235 .build();
236
237 let ext = Extension::typed(info).with_extra(
238 "supportedChains",
239 json!([{"chainId": "eip155:8453", "type": "eip191"}]),
240 );
241
242 let (_, transport_ext) = ext.into_pair();
243
244 let json = serde_json::to_value(&transport_ext).unwrap();
246
247 let deserialized: Extension = serde_json::from_value(json.clone()).unwrap();
249
250 assert_eq!(transport_ext.info, deserialized.info);
252 assert_eq!(transport_ext.schema, deserialized.schema);
253 assert_eq!(
254 deserialized.extra.get("supportedChains").unwrap(),
255 &json!([{"chainId": "eip155:8453", "type": "eip191"}])
256 );
257 }
258
259 #[test]
260 fn sign_in_with_x_full_spec_example() {
261 let json = json!({
263 "info": {
264 "domain": "api.example.com",
265 "uri": "https://api.example.com/premium-data",
266 "version": "1",
267 "nonce": "a1b2c3d4e5f67890a1b2c3d4e5f67890",
268 "issuedAt": "2024-01-15T10:30:00.000Z",
269 "expirationTime": "2024-01-15T10:35:00.000Z",
270 "statement": "Sign in to access premium data",
271 "resources": ["https://api.example.com/premium-data"]
272 },
273 "supportedChains": [
274 {
275 "chainId": "eip155:8453",
276 "type": "eip191"
277 }
278 ],
279 "schema": {}
280 });
281
282 let ext: Extension = serde_json::from_value(json).unwrap();
283 assert_eq!(ext.info["domain"], "api.example.com");
284 assert!(ext.extra.contains_key("supportedChains"));
285
286 let typed_ext: Extension<SignInWithXInfo> = ext.into_typed().unwrap();
288 assert_eq!(typed_ext.info.domain, "api.example.com");
289 assert_eq!(typed_ext.info.version, "1");
290 assert_eq!(
291 typed_ext.info.statement.as_deref(),
292 Some("Sign in to access premium data")
293 );
294 }
295}