Skip to main content

walletkit_core/
credential.rs

1//! FFI-friendly wrapper around [`CoreCredential`].
2
3use std::ops::Deref;
4
5use world_id_core::Credential as CoreCredential;
6
7use crate::error::WalletKitError;
8use crate::FieldElement;
9
10/// A wrapper around [`CoreCredential`] to enable FFI interoperability.
11///
12/// Encapsulates the credential and exposes accessors for fields that FFI
13/// callers need.
14#[derive(Debug, Clone, uniffi::Object)]
15pub struct Credential(CoreCredential);
16
17#[uniffi::export]
18impl Credential {
19    /// Deserializes a `Credential` from a JSON byte blob.
20    ///
21    /// # Errors
22    ///
23    /// Returns an error if the bytes cannot be deserialized.
24    #[uniffi::constructor]
25    #[allow(clippy::needless_pass_by_value)]
26    pub fn from_bytes(bytes: Vec<u8>) -> Result<Self, WalletKitError> {
27        let credential: CoreCredential =
28            serde_json::from_slice(&bytes).map_err(|e| {
29                WalletKitError::InvalidInput {
30                    attribute: "credential_bytes".to_string(),
31                    reason: format!("Failed to deserialize credential: {e}"),
32                }
33            })?;
34        Ok(Self(credential))
35    }
36
37    /// Returns the credential's `sub` field element.
38    #[must_use]
39    pub fn sub(&self) -> FieldElement {
40        self.0.sub.into()
41    }
42
43    /// Returns the credential's issuer schema ID.
44    #[must_use]
45    pub const fn issuer_schema_id(&self) -> u64 {
46        self.0.issuer_schema_id
47    }
48
49    /// Returns the credential's expiration timestamp (unix seconds).
50    #[must_use]
51    pub const fn expires_at(&self) -> u64 {
52        self.0.expires_at
53    }
54
55    /// Returns the credential's `associated_data_commitment` field element.
56    ///
57    /// The commitment scheme is issuer-defined.
58    #[must_use]
59    pub fn associated_data_commitment(&self) -> FieldElement {
60        self.0.associated_data_commitment.into()
61    }
62
63    /// Returns the credential's raw claims, in schema order.
64    ///
65    /// Each claim is a field element; interpretation is defined by the issuer
66    /// schema ([`Self::issuer_schema_id`]). Unset slots hold the zero field
67    /// element. This exposes nothing [`Self::to_bytes`] doesn't already
68    /// serialize — it is an accessor, not a disclosure mechanism; whether and
69    /// which claims leave the device is entirely the host app's policy.
70    #[must_use]
71    pub fn claims(&self) -> Vec<std::sync::Arc<FieldElement>> {
72        self.0
73            .claims
74            .iter()
75            .map(|claim| std::sync::Arc::new((*claim).into()))
76            .collect()
77    }
78
79    /// Returns the credential's raw claims as hex-encoded, padded strings, in
80    /// schema order.
81    ///
82    /// Convenience over [`Self::claims`] using the same encoding claims carry
83    /// in credential JSON.
84    #[must_use]
85    pub fn claims_hex(&self) -> Vec<String> {
86        self.0.claims.iter().map(ToString::to_string).collect()
87    }
88}
89
90impl Credential {
91    /// Serializes the credential to a JSON byte blob for storage.
92    ///
93    /// # Errors
94    ///
95    /// Returns an error if serialization fails.
96    pub fn to_bytes(&self) -> Result<Vec<u8>, WalletKitError> {
97        serde_json::to_vec(&self.0).map_err(|e| WalletKitError::SerializationError {
98            error: format!("Failed to serialize credential: {e}"),
99        })
100    }
101
102    /// Returns the credential's `genesis_issued_at` timestamp.
103    #[must_use]
104    pub const fn genesis_issued_at(&self) -> u64 {
105        self.0.genesis_issued_at
106    }
107}
108
109impl From<CoreCredential> for Credential {
110    fn from(val: CoreCredential) -> Self {
111        Self(val)
112    }
113}
114
115impl From<Credential> for CoreCredential {
116    fn from(val: Credential) -> Self {
117        val.0
118    }
119}
120
121impl Deref for Credential {
122    type Target = CoreCredential;
123
124    fn deref(&self) -> &Self::Target {
125        &self.0
126    }
127}
128
129#[cfg(test)]
130mod tests {
131    use ruint::aliases::U256;
132    use world_id_core::Credential as CoreCredential;
133
134    use super::Credential;
135
136    fn credential_with_claims() -> Credential {
137        let core = CoreCredential::new()
138            .claim_hash(0, U256::from(1u64))
139            .expect("claim 0 in bounds")
140            .claim_hash(1, U256::from(2u64))
141            .expect("claim 1 in bounds");
142        Credential(core)
143    }
144
145    #[test]
146    fn claims_expose_field_elements_in_schema_order() {
147        let credential = credential_with_claims();
148
149        let claims = credential.claims();
150        assert_eq!(claims.len(), credential.claims_hex().len());
151        assert_eq!(
152            claims[0].to_hex_string(),
153            "0x0000000000000000000000000000000000000000000000000000000000000001"
154        );
155        assert_eq!(
156            claims[1].to_hex_string(),
157            "0x0000000000000000000000000000000000000000000000000000000000000002"
158        );
159    }
160
161    #[test]
162    fn claims_hex_matches_field_element_encoding() {
163        let credential = credential_with_claims();
164
165        let hex = credential.claims_hex();
166        let from_elements: Vec<String> = credential
167            .claims()
168            .iter()
169            .map(|claim| claim.to_hex_string())
170            .collect();
171        assert_eq!(hex, from_elements);
172
173        // Unset slots are the zero field element.
174        assert!(hex[2..].iter().all(|claim| claim
175            == "0x0000000000000000000000000000000000000000000000000000000000000000"));
176    }
177}