Skip to main content

dtg_credentials/
create.rs

1/*!
2*   Builder methods for creating new entities.
3*/
4
5#[allow(deprecated)]
6use crate::{
7    CredentialSubject, CredentialSubjectBasic, CredentialSubjectEndorsement,
8    CredentialSubjectMembership, CredentialSubjectRCard, CredentialSubjectWitness, DTGCommon,
9    DTGCredential, DTGCredentialError, DTGCredentialType, WitnessContext,
10};
11use chrono::{DateTime, Utc};
12use serde_json::Value;
13
14impl DTGCredential {
15    /// Creates a new community-issued Verifiable Membership Credential (VMC) — the
16    /// membership **grant**, the community → member half of a membership edge.
17    ///
18    /// A membership edge is a *pair* of VMCs, and this is only one of them. The member
19    /// answers with [DTGCredential::new_member_vmc], and the edge is not complete until
20    /// they have: a community can always issue a credential naming somebody as a member,
21    /// but it cannot produce the acknowledgement without that party's signature. The pair
22    /// is what makes an unconsented membership claim unprovable.
23    ///
24    /// The grant MUST NOT carry a `digest` — that property is what marks the other
25    /// direction — and this constructor does not set one.
26    ///
27    /// issuer: The C-DID of the VTC or VTN granting membership
28    /// subject: The M-DID of the member, or the member VTC's C-DID for VTN membership
29    /// valid_from: The datetime from which this credential is valid
30    /// valid_until: Optional: The datetime this credential is valid until
31    /// personhood: Whether this VMC can be used as a form of Personhood Credential
32    ///             - Adds PersonhoodCredential to the type array if true
33    ///
34    /// # Give it an `id`
35    ///
36    /// Chain [DTGCredential::with_id] on: the member stores the grant under its `id`, and
37    /// re-issuing is only recognisable as a renewal rather than a duplicate if there is one.
38    pub fn new_vmc(
39        issuer: String,
40        subject: String,
41        valid_from: DateTime<Utc>,
42        valid_until: Option<DateTime<Utc>>,
43        personhood: bool,
44    ) -> Self {
45        let mut vmc = DTGCommon {
46            issuer,
47            valid_from,
48            valid_until,
49            credential_subject: CredentialSubject::Membership(CredentialSubjectMembership {
50                id: subject,
51                digest: None,
52            }),
53            ..Default::default()
54        };
55
56        vmc.type_.push(DTGCredentialType::Membership.to_string());
57
58        if personhood {
59            vmc.type_.push("PersonhoodCredential".to_string());
60        }
61
62        DTGCredential {
63            credential: vmc,
64            type_: DTGCredentialType::Membership,
65            version: crate::W3CVCVersion::V2_0,
66        }
67    }
68
69    /// Creates a new member-issued Verifiable Membership Credential (VMC) — the membership
70    /// **acknowledgement**, the member → community half of a membership edge.
71    ///
72    /// The roles of [DTGCredential::new_vmc] are reversed (the member issues, the community
73    /// is the subject) and the subject carries a `digest` of the grant being acknowledged.
74    /// That digest is what binds the two halves into one edge: an acknowledgement whose
75    /// digest matches no valid grant does not complete anything, and the binding forces an
76    /// order — the grant must exist before this can reference it.
77    ///
78    /// This is the member's consent artifact. Because the member is its issuer, withdrawing
79    /// consent needs no cooperation from the community.
80    ///
81    /// grant: The community-issued VMC being acknowledged. Its subject is taken as the
82    ///        member and its issuer as the community, so the two halves cannot disagree
83    ///        about who they are between.
84    /// valid_from: The datetime from which this credential is valid
85    /// valid_until: Optional: The datetime this credential is valid until
86    ///
87    /// # Errors
88    ///
89    /// [DTGCredentialError::WrongCredentialType] if `grant` is not a `MembershipCredential`,
90    /// and [DTGCredentialError::NotAMembershipGrant] if it already carries a `digest` — that
91    /// is an acknowledgement, and acknowledging one does not form an edge.
92    ///
93    /// # Digest the grant in the form you hold it
94    ///
95    /// The digest covers the grant's claims and not its `proof`, so this may be called
96    /// before or after the grant is signed and gives the same answer either way. What it
97    /// cannot survive is a grant whose *claims* differ — a re-issued grant carries a
98    /// different digest, which is what forces re-acknowledgement on renewal.
99    ///
100    /// # Give it an `id`
101    ///
102    /// As with the grant, chain [DTGCredential::with_id] on before signing. A community
103    /// keys a member's VMC by `id` to tell a re-send from a renewal.
104    pub fn new_member_vmc(
105        grant: &DTGCredential,
106        valid_from: DateTime<Utc>,
107        valid_until: Option<DateTime<Utc>>,
108    ) -> Result<Self, DTGCredentialError> {
109        if !matches!(grant.type_(), DTGCredentialType::Membership) {
110            return Err(DTGCredentialError::WrongCredentialType {
111                expected: DTGCredentialType::Membership.to_string(),
112                got: grant.type_().to_string(),
113            });
114        }
115
116        if grant.subject_digest().is_some() {
117            return Err(DTGCredentialError::NotAMembershipGrant(
118                "the credential carries a `digest`, so it is itself a member-issued \
119                 acknowledgement rather than a community-issued grant"
120                    .to_string(),
121            ));
122        }
123
124        // The member is the grant's subject and the community its issuer: reading both off
125        // the grant is what keeps the two halves naming the same pair. Taking them as
126        // parameters would let a caller acknowledge one grant while naming the parties of
127        // another, which verifies as a digest match and means nothing.
128        let mut vmc = DTGCommon {
129            issuer: grant.subject().to_string(),
130            valid_from,
131            valid_until,
132            credential_subject: CredentialSubject::Membership(CredentialSubjectMembership {
133                id: grant.issuer().to_string(),
134                digest: Some(grant.digest()?),
135            }),
136            ..Default::default()
137        };
138
139        vmc.type_.push(DTGCredentialType::Membership.to_string());
140
141        Ok(DTGCredential {
142            credential: vmc,
143            type_: DTGCredentialType::Membership,
144            version: crate::W3CVCVersion::V2_0,
145        })
146    }
147
148    /// Creates a new Verified Relationship Credential (VRC)
149    /// issuer: The issuer DID of the credential
150    /// subject: The DID of the subject of this credential
151    /// valid_from: The datetime from which this credential is valid
152    /// valid_until: Optional: The datetime this credential is valid until
153    pub fn new_vrc(
154        issuer: String,
155        subject: String,
156        valid_from: DateTime<Utc>,
157        valid_until: Option<DateTime<Utc>>,
158    ) -> Self {
159        let mut vrc = DTGCommon {
160            issuer,
161            valid_from,
162            valid_until,
163            credential_subject: CredentialSubject::Basic(CredentialSubjectBasic { id: subject }),
164            ..Default::default()
165        };
166
167        vrc.type_.push(DTGCredentialType::Relationship.to_string());
168
169        DTGCredential {
170            credential: vrc,
171            type_: DTGCredentialType::Relationship,
172            version: crate::W3CVCVersion::V2_0,
173        }
174    }
175
176    /// Creates a new Verified Invitation Credential (VIC)
177    /// issuer: The issuer DID of the credential
178    /// subject: The DID of the subject of this credential
179    /// valid_from: The datetime from which this credential is valid
180    /// valid_until: Optional: The datetime this credential is valid until
181    pub fn new_vic(
182        issuer: String,
183        subject: String,
184        valid_from: DateTime<Utc>,
185        valid_until: Option<DateTime<Utc>>,
186    ) -> Self {
187        let mut vic = DTGCommon {
188            issuer,
189            valid_from,
190            valid_until,
191            credential_subject: CredentialSubject::Basic(CredentialSubjectBasic { id: subject }),
192            ..Default::default()
193        };
194
195        vic.type_.push(DTGCredentialType::Invitation.to_string());
196
197        DTGCredential {
198            credential: vic,
199            type_: DTGCredentialType::Invitation,
200            version: crate::W3CVCVersion::V2_0,
201        }
202    }
203
204    /// Creates a new Verified Persona Credential (VPC)
205    /// issuer: The issuer DID of the credential
206    /// subject: The DID of the subject of this credential
207    /// valid_from: The datetime from which this credential is valid
208    /// valid_until: Optional: The datetime this credential is valid until
209    pub fn new_vpc(
210        issuer: String,
211        subject: String,
212        valid_from: DateTime<Utc>,
213        valid_until: Option<DateTime<Utc>>,
214    ) -> Self {
215        let mut vpc = DTGCommon {
216            issuer,
217            valid_from,
218            valid_until,
219            credential_subject: CredentialSubject::Basic(CredentialSubjectBasic { id: subject }),
220            ..Default::default()
221        };
222
223        vpc.type_.push(DTGCredentialType::Persona.to_string());
224
225        DTGCredential {
226            credential: vpc,
227            type_: DTGCredentialType::Persona,
228            version: crate::W3CVCVersion::V2_0,
229        }
230    }
231
232    /// Creates a new Verified Endorsement Credential (VEC)
233    /// issuer: The issuer DID of the credential
234    /// subject: The DID of the subject of this credential
235    /// valid_from: The datetime from which this credential is valid
236    /// valid_until: Optional: The datetime this credential is valid until
237    /// endorsement: The endorsement details for this credential
238    pub fn new_vec(
239        issuer: String,
240        subject: String,
241        valid_from: DateTime<Utc>,
242        valid_until: Option<DateTime<Utc>>,
243        endorsement: Value,
244    ) -> Self {
245        let mut vec = DTGCommon {
246            issuer,
247            valid_from,
248            valid_until,
249            credential_subject: CredentialSubject::Endorsement(CredentialSubjectEndorsement {
250                id: subject,
251                endorsement,
252            }),
253            ..Default::default()
254        };
255
256        vec.type_.push(DTGCredentialType::Endorsement.to_string());
257
258        DTGCredential {
259            credential: vec,
260            type_: DTGCredentialType::Endorsement,
261            version: crate::W3CVCVersion::V2_0,
262        }
263    }
264
265    /// Creates a new Verified Witness Credential (VWC)
266    /// issuer: The issuer DID of the credential - an M-DID, or the DID of a VTA acting
267    ///         according to VTC policy
268    /// subject: The DID of the observed party. For a witnessed bi-directional exchange this
269    ///          MUST be the issuer of the VRC that this VWC attests (the VRC referenced by
270    ///          `digest`), so that the two VWCs of an exchange are unambiguously bound to
271    ///          their respective directions. The witness should issue one VWC per direction.
272    /// valid_from: The datetime from which this credential is valid
273    /// valid_until: Optional: The datetime this credential is valid until
274    /// task_context: Required `threadId` of the trust task exchange the witnessing occurred in
275    /// digest: Cryptographic hash of the witnessed edge credential, binding this VWC to the
276    ///         specific edge. Produce it with [DTGCredential::digest] on that credential.
277    ///         REQUIRED by the specification; `Option` here because a VWC that predates the
278    ///         requirement still has to deserialize. A VWC without one identifies the
279    ///         observed party and the exchange, but not which edge was witnessed.
280    /// witness_context: Optional Semantic context for the witness
281    pub fn new_vwc(
282        issuer: String,
283        subject: String,
284        valid_from: DateTime<Utc>,
285        valid_until: Option<DateTime<Utc>>,
286        task_context: String,
287        digest: Option<String>,
288        witness_context: Option<WitnessContext>,
289    ) -> Self {
290        let mut vwc = DTGCommon {
291            issuer,
292            valid_from,
293            valid_until,
294            task_context: Some(task_context),
295            credential_subject: CredentialSubject::Witness(CredentialSubjectWitness {
296                id: subject,
297                digest,
298                witness_context,
299            }),
300            ..Default::default()
301        };
302
303        vwc.type_.push(DTGCredentialType::Witness.to_string());
304
305        DTGCredential {
306            credential: vwc,
307            type_: DTGCredentialType::Witness,
308            version: crate::W3CVCVersion::V2_0,
309        }
310    }
311
312    /// Creates a new Verified RCard Credential (VWC)
313    /// issuer: The issuer DID of the credential
314    /// subject: The DID of the subject of this credential
315    /// valid_from: The datetime from which this credential is valid
316    /// valid_until: Optional: The datetime this credential is valid until
317    /// card: JSON Value representing a Jcard (RFC 7095) format
318    #[deprecated(
319        since = "0.2.0",
320        note = "The r-card is a verifiable data structure (VDS), not a DTGCredential subtype. \
321                It was removed from the DTG Core Credentials specification in Working Draft 01 \
322                and will be defined by the planned DTG Verifiable Data Structures specification. \
323                This constructor will be removed in a future release."
324    )]
325    #[allow(deprecated)]
326    pub fn new_rcard(
327        issuer: String,
328        subject: String,
329        valid_from: DateTime<Utc>,
330        valid_until: Option<DateTime<Utc>>,
331        card: Value,
332    ) -> Self {
333        let mut rcard = DTGCommon {
334            issuer,
335            valid_from,
336            valid_until,
337            credential_subject: CredentialSubject::RCard(CredentialSubjectRCard {
338                id: subject,
339                card,
340            }),
341            ..Default::default()
342        };
343
344        rcard.type_.push(DTGCredentialType::RCard.to_string());
345
346        DTGCredential {
347            credential: rcard,
348            type_: DTGCredentialType::RCard,
349            version: crate::W3CVCVersion::V2_0,
350        }
351    }
352
353    /// Sets this credential's own identifier, consuming and returning it so it chains onto
354    /// any of the `new_*` constructors above.
355    ///
356    /// `id` MUST be a single URL per the W3C VC Data Model; `urn:uuid:<uuid>` is the usual
357    /// choice for a credential with no dereferenceable home. This crate does not validate it.
358    ///
359    /// ```
360    /// # use chrono::Utc;
361    /// # use dtg_credentials::DTGCredential;
362    /// let vmc = DTGCredential::new_vmc(
363    ///     "did:example:member".to_string(),
364    ///     "did:example:community".to_string(),
365    ///     Utc::now(),
366    ///     None,
367    ///     false,
368    /// )
369    /// .with_id("urn:uuid:2a4e1d90-6e0c-4d3f-9a4a-6d0a8f7c1b52");
370    /// assert_eq!(vmc.id(), Some("urn:uuid:2a4e1d90-6e0c-4d3f-9a4a-6d0a8f7c1b52"));
371    /// ```
372    ///
373    /// # Set it before signing
374    ///
375    /// A Data Integrity proof covers the credential minus its `proof`, so `id` is part of what
376    /// is signed. Chain this onto the constructor, before [DTGCredential::sign] — adding an id
377    /// to an already-signed credential leaves a document whose proof no longer verifies.
378    pub fn with_id(mut self, id: impl Into<String>) -> Self {
379        self.credential.id = Some(id.into());
380        self
381    }
382
383    /// Sets this credential's own identifier in place.
384    ///
385    /// The non-consuming form of [DTGCredential::with_id]; the same "before signing" caveat
386    /// applies.
387    pub fn set_id(&mut self, id: impl Into<String>) {
388        self.credential.id = Some(id.into());
389    }
390}
391
392#[cfg(test)]
393#[allow(deprecated)]
394mod tests {
395    use crate::{DTGCredential, WitnessContext};
396    use chrono::{DateTime, Utc};
397    use serde_json::json;
398
399    #[test]
400    fn test_vmc_serialization() {
401        let vmc = DTGCredential::new_vmc(
402            "did:example:issuer".to_string(),
403            "did:example:subject".to_string(),
404            DateTime::parse_from_rfc3339("2025-12-11T00:00:00Z")
405                .unwrap()
406                .with_timezone(&Utc),
407            None,
408            false,
409        );
410
411        let txt = serde_json::to_string_pretty(&vmc).unwrap();
412        let sample = r#"{
413  "@context": [
414    "https://www.w3.org/ns/credentials/v2",
415    "https://firstperson.network/credentials/dtg/v1"
416  ],
417  "type": [
418    "VerifiableCredential",
419    "DTGCredential",
420    "MembershipCredential"
421  ],
422  "issuer": "did:example:issuer",
423  "validFrom": "2025-12-11T00:00:00Z",
424  "credentialSubject": {
425    "id": "did:example:subject"
426  }
427}"#;
428
429        assert_eq!(txt, sample);
430    }
431
432    #[test]
433    fn test_vmc_phc_serialization() {
434        let vmc = DTGCredential::new_vmc(
435            "did:example:issuer".to_string(),
436            "did:example:subject".to_string(),
437            DateTime::parse_from_rfc3339("2025-12-11T00:00:00Z")
438                .unwrap()
439                .with_timezone(&Utc),
440            None,
441            true,
442        );
443
444        let txt = serde_json::to_string_pretty(&vmc).unwrap();
445        let sample = r#"{
446  "@context": [
447    "https://www.w3.org/ns/credentials/v2",
448    "https://firstperson.network/credentials/dtg/v1"
449  ],
450  "type": [
451    "VerifiableCredential",
452    "DTGCredential",
453    "MembershipCredential",
454    "PersonhoodCredential"
455  ],
456  "issuer": "did:example:issuer",
457  "validFrom": "2025-12-11T00:00:00Z",
458  "credentialSubject": {
459    "id": "did:example:subject"
460  }
461}"#;
462
463        assert_eq!(txt, sample);
464    }
465    /// `id` is OPTIONAL, and a credential that was never given one must keep serializing the
466    /// shape it always did — no `"id": null`, no empty string.
467    #[test]
468    fn test_vmc_without_id_omits_the_property() {
469        let vmc = DTGCredential::new_vmc(
470            "did:example:issuer".to_string(),
471            "did:example:subject".to_string(),
472            DateTime::parse_from_rfc3339("2025-12-11T00:00:00Z")
473                .unwrap()
474                .with_timezone(&Utc),
475            None,
476            false,
477        );
478
479        assert_eq!(vmc.id(), None);
480        let value: serde_json::Value = serde_json::to_value(&vmc).unwrap();
481        assert!(
482            value.get("id").is_none(),
483            "an unset id must not appear on the wire at all: {value}"
484        );
485    }
486
487    /// `with_id` puts the identifier at the top level of the credential — a sibling of
488    /// `issuer`, not something nested under `credentialSubject` (which carries the *subject's*
489    /// id, a different thing entirely).
490    #[test]
491    fn test_vmc_with_id_serialization() {
492        let vmc = DTGCredential::new_vmc(
493            "did:example:issuer".to_string(),
494            "did:example:subject".to_string(),
495            DateTime::parse_from_rfc3339("2025-12-11T00:00:00Z")
496                .unwrap()
497                .with_timezone(&Utc),
498            None,
499            false,
500        )
501        .with_id("urn:uuid:1e2d3c4b-5a69-4788-9099-aabbccddeeff");
502
503        let txt = serde_json::to_string_pretty(&vmc).unwrap();
504        let sample = r#"{
505  "@context": [
506    "https://www.w3.org/ns/credentials/v2",
507    "https://firstperson.network/credentials/dtg/v1"
508  ],
509  "type": [
510    "VerifiableCredential",
511    "DTGCredential",
512    "MembershipCredential"
513  ],
514  "id": "urn:uuid:1e2d3c4b-5a69-4788-9099-aabbccddeeff",
515  "issuer": "did:example:issuer",
516  "validFrom": "2025-12-11T00:00:00Z",
517  "credentialSubject": {
518    "id": "did:example:subject"
519  }
520}"#;
521
522        assert_eq!(txt, sample);
523    }
524
525    /// The identifier has to survive a round trip. It arrives on the wire and is read back
526    /// through `TryFrom<DTGCommon>`, which is where `taskContext` was previously being dropped
527    /// — a field that deserializes into nothing breaks signing and verification silently.
528    #[test]
529    fn test_id_round_trips_through_deserialization() {
530        let vmc = DTGCredential::new_vmc(
531            "did:example:issuer".to_string(),
532            "did:example:subject".to_string(),
533            Utc::now(),
534            None,
535            false,
536        )
537        .with_id("urn:uuid:1e2d3c4b-5a69-4788-9099-aabbccddeeff");
538
539        let txt = serde_json::to_string(&vmc).unwrap();
540        let parsed: DTGCredential = serde_json::from_str(&txt).unwrap();
541        assert_eq!(
542            parsed.id(),
543            Some("urn:uuid:1e2d3c4b-5a69-4788-9099-aabbccddeeff")
544        );
545    }
546
547    /// A credential with no `id` still deserializes — the property is OPTIONAL, and every
548    /// credential issued before this field existed has none.
549    #[test]
550    fn test_missing_id_deserializes_as_none() {
551        let parsed: DTGCredential = serde_json::from_str(
552            r#"{
553              "@context": ["https://www.w3.org/ns/credentials/v2"],
554              "type": ["VerifiableCredential", "DTGCredential", "MembershipCredential"],
555              "issuer": "did:example:issuer",
556              "validFrom": "2025-12-11T00:00:00Z",
557              "credentialSubject": { "id": "did:example:subject" }
558            }"#,
559        )
560        .unwrap();
561        assert_eq!(parsed.id(), None);
562    }
563
564    /// `set_id` is the in-place form of `with_id`; both write the same property.
565    #[test]
566    fn test_set_id_matches_with_id() {
567        let build = || {
568            DTGCredential::new_vrc(
569                "did:example:issuer".to_string(),
570                "did:example:subject".to_string(),
571                DateTime::parse_from_rfc3339("2025-12-11T00:00:00Z")
572                    .unwrap()
573                    .with_timezone(&Utc),
574                None,
575            )
576        };
577        let mut in_place = build();
578        in_place.set_id("urn:uuid:abc");
579        assert_eq!(
580            serde_json::to_value(&in_place).unwrap(),
581            serde_json::to_value(build().with_id("urn:uuid:abc")).unwrap()
582        );
583    }
584
585    #[test]
586    fn test_vrc_serialization() {
587        let vrc = DTGCredential::new_vrc(
588            "did:example:issuer".to_string(),
589            "did:example:subject".to_string(),
590            DateTime::parse_from_rfc3339("2025-12-11T00:00:00Z")
591                .unwrap()
592                .with_timezone(&Utc),
593            None,
594        );
595
596        let txt = serde_json::to_string_pretty(&vrc).unwrap();
597        let sample = r#"{
598  "@context": [
599    "https://www.w3.org/ns/credentials/v2",
600    "https://firstperson.network/credentials/dtg/v1"
601  ],
602  "type": [
603    "VerifiableCredential",
604    "DTGCredential",
605    "RelationshipCredential"
606  ],
607  "issuer": "did:example:issuer",
608  "validFrom": "2025-12-11T00:00:00Z",
609  "credentialSubject": {
610    "id": "did:example:subject"
611  }
612}"#;
613
614        assert_eq!(txt, sample);
615    }
616
617    #[test]
618    fn test_vic_serialization() {
619        let vic = DTGCredential::new_vic(
620            "did:example:issuer".to_string(),
621            "did:example:subject".to_string(),
622            DateTime::parse_from_rfc3339("2025-12-11T00:00:00Z")
623                .unwrap()
624                .with_timezone(&Utc),
625            None,
626        );
627
628        let txt = serde_json::to_string_pretty(&vic).unwrap();
629        let sample = r#"{
630  "@context": [
631    "https://www.w3.org/ns/credentials/v2",
632    "https://firstperson.network/credentials/dtg/v1"
633  ],
634  "type": [
635    "VerifiableCredential",
636    "DTGCredential",
637    "InvitationCredential"
638  ],
639  "issuer": "did:example:issuer",
640  "validFrom": "2025-12-11T00:00:00Z",
641  "credentialSubject": {
642    "id": "did:example:subject"
643  }
644}"#;
645
646        assert_eq!(txt, sample);
647    }
648
649    #[test]
650    fn test_vpc_serialization() {
651        let vpc = DTGCredential::new_vpc(
652            "did:example:issuer".to_string(),
653            "did:example:subject".to_string(),
654            DateTime::parse_from_rfc3339("2025-12-11T00:00:00Z")
655                .unwrap()
656                .with_timezone(&Utc),
657            None,
658        );
659
660        let txt = serde_json::to_string_pretty(&vpc).unwrap();
661        let sample = r#"{
662  "@context": [
663    "https://www.w3.org/ns/credentials/v2",
664    "https://firstperson.network/credentials/dtg/v1"
665  ],
666  "type": [
667    "VerifiableCredential",
668    "DTGCredential",
669    "PersonaCredential"
670  ],
671  "issuer": "did:example:issuer",
672  "validFrom": "2025-12-11T00:00:00Z",
673  "credentialSubject": {
674    "id": "did:example:subject"
675  }
676}"#;
677
678        assert_eq!(txt, sample);
679    }
680
681    #[test]
682    fn test_vec_serialization() {
683        let vec = DTGCredential::new_vec(
684            "did:example:issuer".to_string(),
685            "did:example:subject".to_string(),
686            DateTime::parse_from_rfc3339("2025-12-11T00:00:00Z")
687                .unwrap()
688                .with_timezone(&Utc),
689            None,
690            json!({
691              "type": "SkillEndorsement",
692              "name": "Software Development",
693              "competencyLevel": "expert"
694            }),
695        );
696
697        let txt = serde_json::to_string_pretty(&vec).unwrap();
698        let sample = r#"{
699  "@context": [
700    "https://www.w3.org/ns/credentials/v2",
701    "https://firstperson.network/credentials/dtg/v1"
702  ],
703  "type": [
704    "VerifiableCredential",
705    "DTGCredential",
706    "EndorsementCredential"
707  ],
708  "issuer": "did:example:issuer",
709  "validFrom": "2025-12-11T00:00:00Z",
710  "credentialSubject": {
711    "id": "did:example:subject",
712    "endorsement": {
713      "competencyLevel": "expert",
714      "name": "Software Development",
715      "type": "SkillEndorsement"
716    }
717  }
718}"#;
719
720        assert_eq!(txt, sample);
721    }
722
723    #[test]
724    fn test_vwc_serialization() {
725        let vwc = DTGCredential::new_vwc(
726            "did:example:issuer".to_string(),
727            "did:example:subject".to_string(),
728            DateTime::parse_from_rfc3339("2025-12-11T00:00:00Z")
729                .unwrap()
730                .with_timezone(&Utc),
731            None,
732            "thread-abc-123".to_string(),
733            Some("zQmbGXRT3v1RmfWkQ7Y3Z5Uj9pKq2NcXhLd8sVtA4eB6nMw".to_string()),
734            Some(WitnessContext {
735                event: Some("EthDenver 2024".to_string()),
736                session_id: Some("session-8822-nonce".to_string()),
737                method: Some("in-person-proximity".to_string()),
738            }),
739        );
740
741        let txt = serde_json::to_string_pretty(&vwc).unwrap();
742
743        let sample = r#"{
744  "@context": [
745    "https://www.w3.org/ns/credentials/v2",
746    "https://firstperson.network/credentials/dtg/v1"
747  ],
748  "type": [
749    "VerifiableCredential",
750    "DTGCredential",
751    "WitnessCredential"
752  ],
753  "issuer": "did:example:issuer",
754  "validFrom": "2025-12-11T00:00:00Z",
755  "taskContext": "thread-abc-123",
756  "credentialSubject": {
757    "id": "did:example:subject",
758    "digest": "zQmbGXRT3v1RmfWkQ7Y3Z5Uj9pKq2NcXhLd8sVtA4eB6nMw",
759    "witnessContext": {
760      "event": "EthDenver 2024",
761      "sessionId": "session-8822-nonce",
762      "method": "in-person-proximity"
763    }
764  }
765}"#;
766
767        assert_eq!(txt, sample);
768    }
769
770    #[test]
771    fn test_rcard_serialization() {
772        let rcard = DTGCredential::new_rcard(
773            "did:example:issuer".to_string(),
774            "did:example:subject".to_string(),
775            DateTime::parse_from_rfc3339("2025-12-11T00:00:00Z")
776                .unwrap()
777                .with_timezone(&Utc),
778            None,
779            json!([
780                "vcard",
781                [
782                    ["fn", {}, "text", "Alice Smith"],
783                    ["email", {}, "text", "alice@example.com"]
784                ]
785            ]),
786        );
787
788        let txt = serde_json::to_string_pretty(&rcard).unwrap();
789
790        let sample = r#"{
791  "@context": [
792    "https://www.w3.org/ns/credentials/v2",
793    "https://firstperson.network/credentials/dtg/v1"
794  ],
795  "type": [
796    "VerifiableCredential",
797    "DTGCredential",
798    "RCardCredential"
799  ],
800  "issuer": "did:example:issuer",
801  "validFrom": "2025-12-11T00:00:00Z",
802  "credentialSubject": {
803    "id": "did:example:subject",
804    "card": [
805      "vcard",
806      [
807        [
808          "fn",
809          {},
810          "text",
811          "Alice Smith"
812        ],
813        [
814          "email",
815          {},
816          "text",
817          "alice@example.com"
818        ]
819      ]
820    ]
821  }
822}"#;
823
824        assert_eq!(txt, sample);
825    }
826}