1use baseid_core::credential::CredentialSummary;
4use baseid_core::types::CredentialId;
5
6use crate::holder::WalletStore;
7use crate::matcher::{self, PresentationRequest};
8use crate::presenter::CredentialMatch;
9use crate::receive;
10use crate::{AnyCredential, CredentialFilter, CredentialRecord};
11
12pub struct Wallet<S: WalletStore> {
17 store: S,
18 holder_did: String,
19}
20
21impl<S: WalletStore> Wallet<S> {
22 pub fn new(store: S, holder_did: String) -> Self {
24 Self { store, holder_did }
25 }
26
27 pub async fn receive_credential(
29 &self,
30 response: &baseid_oid4vci::credential::CredentialResponse,
31 format_hint: &str,
32 ) -> baseid_core::Result<Vec<CredentialId>> {
33 receive::receive_oid4vci_credential(&self.store, response, format_hint).await
34 }
35
36 pub async fn find_matching_credentials(
38 &self,
39 request: &serde_json::Value,
40 ) -> baseid_core::Result<Vec<CredentialMatch>> {
41 let pr = PresentationRequest::from_value(request)?;
42 let summaries = self
44 .store
45 .list_credentials(CredentialFilter::default())
46 .await?;
47 let mut records = Vec::new();
48 for summary in &summaries {
49 let cred = self.store.get_credential(&summary.metadata.id).await?;
50 records.push(CredentialRecord {
51 id: summary.metadata.id.clone(),
52 credential: cred,
53 raw: None,
54 });
55 }
56 Ok(match pr {
57 PresentationRequest::PresentationExchange(pd) => {
58 matcher::match_credentials_pe(&records, &pd)
59 }
60 PresentationRequest::Dcql(dcql) => matcher::match_credentials_dcql(&records, &dcql),
61 })
62 }
63
64 pub async fn list_credentials(
66 &self,
67 filter: CredentialFilter,
68 ) -> baseid_core::Result<Vec<CredentialSummary>> {
69 self.store.list_credentials(filter).await
70 }
71
72 pub async fn get_credential(&self, id: &CredentialId) -> baseid_core::Result<AnyCredential> {
74 self.store.get_credential(id).await
75 }
76
77 pub async fn delete_credential(&self, id: &CredentialId) -> baseid_core::Result<()> {
79 self.store.delete_credential(id).await
80 }
81
82 pub fn holder_did(&self) -> &str {
84 &self.holder_did
85 }
86}
87
88#[cfg(test)]
89mod tests {
90 use super::*;
91 use crate::store::InMemoryStore;
92 use baseid_core::types::{CredentialFormat, KeyType};
93 use baseid_crypto::KeyPair;
94 use baseid_did::DidKeyResolver;
95 use baseid_oid4vci::credential::{CredentialEntry, CredentialResponse};
96 use baseid_vc::credential::Issuer;
97 use baseid_vc::VerifiableCredential;
98
99 fn make_signed_vc(type_name: &str, subject: serde_json::Value) -> (String, KeyPair, String) {
101 let kp = KeyPair::generate(KeyType::Ed25519).unwrap();
102 let doc = DidKeyResolver::create(&kp.public).unwrap();
103 let kid = doc.verification_method[0].id.clone();
104
105 let vc = VerifiableCredential {
106 context: vec!["https://www.w3.org/ns/credentials/v2".to_string()],
107 id: None,
108 r#type: vec!["VerifiableCredential".to_string(), type_name.to_string()],
109 issuer: Issuer::Uri(doc.id.clone()),
110 valid_from: Some("2024-01-01T00:00:00Z".to_string()),
111 valid_until: None,
112 credential_subject: subject,
113 credential_status: None,
114 proof: None,
115 };
116 let jwt = baseid_vc::sign_credential_jwt(&vc, &kp, &kid).unwrap();
117 (jwt, kp, doc.id)
118 }
119
120 fn mock_credential_response(jwt: &str) -> CredentialResponse {
122 CredentialResponse {
123 credentials: Some(vec![CredentialEntry {
124 credential: serde_json::Value::String(jwt.to_string()),
125 }]),
126 transaction_id: None,
127 notification_id: None,
128 interval: None,
129 }
130 }
131
132 fn mock_deferred_response() -> CredentialResponse {
134 CredentialResponse {
135 credentials: None,
136 transaction_id: Some("txn-deferred-1".to_string()),
137 notification_id: None,
138 interval: Some(5),
139 }
140 }
141
142 async fn wallet_with_vc(
144 type_name: &str,
145 subject: serde_json::Value,
146 ) -> (Wallet<InMemoryStore>, Vec<CredentialId>) {
147 let (jwt, _kp, holder_did) = make_signed_vc(type_name, subject);
148 let wallet = Wallet::new(InMemoryStore::new(), holder_did);
149 let response = mock_credential_response(&jwt);
150 let ids = wallet
151 .receive_credential(&response, "jwt_vc_json")
152 .await
153 .unwrap();
154 (wallet, ids)
155 }
156
157 #[tokio::test]
162 async fn wallet_new() {
163 let wallet = Wallet::new(InMemoryStore::new(), "did:key:holder".to_string());
164 assert_eq!(wallet.holder_did(), "did:key:holder");
165 }
166
167 #[tokio::test]
168 async fn wallet_list_empty() {
169 let wallet = Wallet::new(InMemoryStore::new(), "did:key:holder".to_string());
170 let list = wallet
171 .list_credentials(CredentialFilter::default())
172 .await
173 .unwrap();
174 assert!(list.is_empty());
175 }
176
177 #[tokio::test]
178 async fn wallet_receive_and_list() {
179 let (wallet, ids) = wallet_with_vc(
180 "UniversityDegreeCredential",
181 serde_json::json!({"id": "did:key:holder", "degree": {"type": "BachelorDegree"}}),
182 )
183 .await;
184
185 assert_eq!(ids.len(), 1);
186 let list = wallet
187 .list_credentials(CredentialFilter::default())
188 .await
189 .unwrap();
190 assert_eq!(list.len(), 1);
191 }
192
193 #[tokio::test]
194 async fn wallet_receive_and_get() {
195 let (wallet, ids) = wallet_with_vc(
196 "UniversityDegreeCredential",
197 serde_json::json!({"id": "did:key:holder", "degree": {"type": "BachelorDegree"}}),
198 )
199 .await;
200
201 let cred = wallet.get_credential(&ids[0]).await.unwrap();
202 assert_eq!(cred.credential_format(), CredentialFormat::W3cVc);
203 match &cred {
204 AnyCredential::W3cVc(vc) => {
205 assert!(vc
206 .r#type
207 .contains(&"UniversityDegreeCredential".to_string()));
208 }
209 _ => panic!("Expected W3cVc variant"),
210 }
211 }
212
213 #[tokio::test]
214 async fn wallet_delete() {
215 let (wallet, ids) = wallet_with_vc(
216 "UniversityDegreeCredential",
217 serde_json::json!({"id": "did:key:holder"}),
218 )
219 .await;
220
221 wallet.delete_credential(&ids[0]).await.unwrap();
222 let list = wallet
223 .list_credentials(CredentialFilter::default())
224 .await
225 .unwrap();
226 assert!(list.is_empty());
227 }
228
229 #[tokio::test]
230 async fn wallet_find_matching_pe() {
231 let (wallet, _ids) = wallet_with_vc(
232 "UniversityDegreeCredential",
233 serde_json::json!({"id": "did:key:holder", "degree": {"type": "BachelorDegree"}}),
234 )
235 .await;
236
237 let pe_request = serde_json::json!({
238 "id": "pd-1",
239 "input_descriptors": [{
240 "id": "degree",
241 "constraints": {
242 "fields": [{
243 "path": ["$.credentialSubject.degree"]
244 }]
245 },
246 "format": {"jwt_vc_json": {}}
247 }]
248 });
249
250 let matches = wallet.find_matching_credentials(&pe_request).await.unwrap();
251 assert_eq!(matches.len(), 1);
252 assert_eq!(matches[0].format, CredentialFormat::W3cVc);
253 assert!(matches[0]
254 .matching_fields
255 .contains(&"$.credentialSubject.degree".to_string()));
256 }
257
258 #[tokio::test]
259 async fn wallet_find_matching_dcql() {
260 let (wallet, _ids) = wallet_with_vc(
261 "UniversityDegreeCredential",
262 serde_json::json!({"id": "did:key:holder", "degree": {"type": "BachelorDegree"}}),
263 )
264 .await;
265
266 let dcql_request = serde_json::json!({
267 "credentials": [{
268 "id": "req-1",
269 "format": "jwt_vc_json",
270 "meta": {
271 "type_values": [["UniversityDegreeCredential"]]
272 },
273 "claims": [
274 {"path": ["degree"]}
275 ]
276 }]
277 });
278
279 let matches = wallet
280 .find_matching_credentials(&dcql_request)
281 .await
282 .unwrap();
283 assert_eq!(matches.len(), 1);
284 assert_eq!(matches[0].format, CredentialFormat::W3cVc);
285 assert_eq!(matches[0].matching_fields, vec!["degree"]);
286 }
287
288 #[tokio::test]
289 async fn wallet_find_no_match() {
290 let (wallet, _ids) = wallet_with_vc(
291 "UniversityDegreeCredential",
292 serde_json::json!({"id": "did:key:holder"}),
293 )
294 .await;
295
296 let dcql_request = serde_json::json!({
298 "credentials": [{
299 "id": "req-1",
300 "format": "mso_mdoc"
301 }]
302 });
303
304 let matches = wallet
305 .find_matching_credentials(&dcql_request)
306 .await
307 .unwrap();
308 assert!(matches.is_empty());
309 }
310
311 #[tokio::test]
312 async fn e2e_issue_store_match() {
313 let kp = KeyPair::generate(KeyType::P256).unwrap();
315 let doc = DidKeyResolver::create(&kp.public).unwrap();
316 let kid = doc.verification_method[0].id.clone();
317
318 let vc = VerifiableCredential {
319 context: vec!["https://www.w3.org/ns/credentials/v2".to_string()],
320 id: None,
321 r#type: vec![
322 "VerifiableCredential".to_string(),
323 "UniversityDegreeCredential".to_string(),
324 ],
325 issuer: Issuer::Uri(doc.id.clone()),
326 valid_from: Some("2024-01-01T00:00:00Z".to_string()),
327 valid_until: None,
328 credential_subject: serde_json::json!({
329 "id": "did:key:holder",
330 "degree": {"type": "BachelorDegree"}
331 }),
332 credential_status: None,
333 proof: None,
334 };
335 let jwt = baseid_vc::sign_credential_jwt(&vc, &kp, &kid).unwrap();
336
337 let wallet = Wallet::new(InMemoryStore::new(), "did:key:holder".to_string());
338 let response = mock_credential_response(&jwt);
339 let ids = wallet
340 .receive_credential(&response, "jwt_vc_json")
341 .await
342 .unwrap();
343 assert_eq!(ids.len(), 1);
344
345 let pe = serde_json::json!({
347 "id": "pd-e2e",
348 "input_descriptors": [{
349 "id": "degree",
350 "constraints": {
351 "fields": [{
352 "path": ["$.credentialSubject.degree"]
353 }]
354 },
355 "format": {"jwt_vc_json": {}}
356 }]
357 });
358
359 let matches = wallet.find_matching_credentials(&pe).await.unwrap();
360 assert_eq!(matches.len(), 1);
361 assert_eq!(matches[0].credential_id, ids[0]);
362 assert_eq!(matches[0].format, CredentialFormat::W3cVc);
363 assert!(matches[0]
364 .matching_fields
365 .contains(&"$.credentialSubject.degree".to_string()));
366
367 let cred = wallet.get_credential(&ids[0]).await.unwrap();
369 match &cred {
370 AnyCredential::W3cVc(stored_vc) => {
371 assert!(stored_vc
372 .r#type
373 .contains(&"UniversityDegreeCredential".to_string()));
374 assert_eq!(
375 stored_vc.credential_subject["degree"]["type"],
376 "BachelorDegree"
377 );
378 }
379 _ => panic!("Expected W3cVc variant"),
380 }
381 }
382
383 #[tokio::test]
384 async fn wallet_receive_multiple_formats() {
385 let (jwt_vc, _kp, holder_did) = make_signed_vc(
386 "UniversityDegreeCredential",
387 serde_json::json!({"id": "did:key:holder"}),
388 );
389
390 let wallet = Wallet::new(InMemoryStore::new(), holder_did);
391
392 let vc_response = mock_credential_response(&jwt_vc);
394 let vc_ids = wallet
395 .receive_credential(&vc_response, "jwt_vc_json")
396 .await
397 .unwrap();
398 assert_eq!(vc_ids.len(), 1);
399
400 let sd_jwt = baseid_sd_jwt::SdJwt {
402 jwt: "eyJ0eXAiOiJ2YytzZC1qd3QiLCJhbGciOiJFUzI1NiJ9.eyJpc3MiOiJ0ZXN0In0.c2ln"
403 .to_string(),
404 disclosures: vec![],
405 key_binding_jwt: None,
406 };
407 let compact = sd_jwt.serialize();
408 let sd_response = CredentialResponse {
409 credentials: Some(vec![CredentialEntry {
410 credential: serde_json::Value::String(compact),
411 }]),
412 transaction_id: None,
413 notification_id: None,
414 interval: None,
415 };
416 let sd_ids = wallet
417 .receive_credential(&sd_response, "dc+sd-jwt")
418 .await
419 .unwrap();
420 assert_eq!(sd_ids.len(), 1);
421
422 let all = wallet
424 .list_credentials(CredentialFilter::default())
425 .await
426 .unwrap();
427 assert_eq!(all.len(), 2);
428
429 let vc_only = wallet
431 .list_credentials(CredentialFilter {
432 format: Some(CredentialFormat::W3cVc),
433 issuer: None,
434 })
435 .await
436 .unwrap();
437 assert_eq!(vc_only.len(), 1);
438 assert_eq!(vc_only[0].metadata.format, CredentialFormat::W3cVc);
439
440 let sd_only = wallet
442 .list_credentials(CredentialFilter {
443 format: Some(CredentialFormat::SdJwtVc),
444 issuer: None,
445 })
446 .await
447 .unwrap();
448 assert_eq!(sd_only.len(), 1);
449 assert_eq!(sd_only[0].metadata.format, CredentialFormat::SdJwtVc);
450 }
451
452 #[tokio::test]
457 async fn wallet_get_nonexistent() {
458 let wallet = Wallet::new(InMemoryStore::new(), "did:key:holder".to_string());
459 let result = wallet
460 .get_credential(&CredentialId("nonexistent".to_string()))
461 .await;
462 assert!(result.is_err());
463 }
464
465 #[tokio::test]
466 async fn wallet_delete_nonexistent() {
467 let wallet = Wallet::new(InMemoryStore::new(), "did:key:holder".to_string());
468 let result = wallet
469 .delete_credential(&CredentialId("nonexistent".to_string()))
470 .await;
471 assert!(result.is_err());
472 }
473
474 #[tokio::test]
475 async fn wallet_find_matching_empty_store() {
476 let wallet = Wallet::new(InMemoryStore::new(), "did:key:holder".to_string());
477 let pe_request = serde_json::json!({
478 "id": "pd-1",
479 "input_descriptors": [{
480 "id": "desc-1",
481 "constraints": { "fields": [] },
482 "format": {"jwt_vc_json": {}}
483 }]
484 });
485
486 let matches = wallet.find_matching_credentials(&pe_request).await.unwrap();
487 assert!(matches.is_empty());
488 }
489
490 #[tokio::test]
491 async fn wallet_receive_deferred() {
492 let wallet = Wallet::new(InMemoryStore::new(), "did:key:holder".to_string());
493 let response = mock_deferred_response();
494 let ids = wallet
495 .receive_credential(&response, "jwt_vc_json")
496 .await
497 .unwrap();
498 assert!(ids.is_empty());
499 let list = wallet
500 .list_credentials(CredentialFilter::default())
501 .await
502 .unwrap();
503 assert!(list.is_empty());
504 }
505}