Skip to main content

walletkit_core/
field_element.rs

1//! `FieldElement` represents an element in a finite field used in the World ID Protocol's
2//! zero-knowledge proofs.
3use std::ops::Deref;
4use std::str::FromStr;
5
6use world_id_core::FieldElement as CoreFieldElement;
7
8use crate::error::WalletKitError;
9
10/// A wrapper around `FieldElement` to enable FFI interoperability.
11///
12/// `FieldElement` represents an element in a finite field used in the World ID Protocol's
13/// zero-knowledge proofs. This wrapper allows the type to be safely passed across FFI boundaries
14/// while maintaining proper serialization and deserialization semantics.
15///
16/// Field elements are typically 32 bytes when serialized.
17#[allow(clippy::module_name_repetitions)]
18#[derive(Debug, Clone, uniffi::Object)]
19pub struct FieldElement(pub CoreFieldElement);
20
21#[uniffi::export]
22impl FieldElement {
23    /// Creates a `FieldElement` from raw bytes (big-endian).
24    ///
25    /// # Errors
26    ///
27    /// Returns an error if the bytes cannot be deserialized into a valid field element.
28    #[uniffi::constructor]
29    pub fn from_bytes(bytes: Vec<u8>) -> Result<Self, WalletKitError> {
30        let len = bytes.len();
31        let val: [u8; 32] =
32            bytes.try_into().map_err(|_| WalletKitError::InvalidInput {
33                attribute: "field_element".to_string(),
34                reason: format!("Expected 32 bytes for field element, got {len}"),
35            })?;
36
37        let field_element = CoreFieldElement::from_be_bytes(&val)?;
38        Ok(Self(field_element))
39    }
40
41    /// Creates a `FieldElement` from a `u64` value.
42    ///
43    /// This is useful for testing or when working with small field element values.
44    #[must_use]
45    #[uniffi::constructor]
46    pub fn from_u64(value: u64) -> Self {
47        Self(CoreFieldElement::from(value))
48    }
49
50    /// Serializes the field element to bytes (big-endian).
51    ///
52    /// Returns a byte vector representing the field element.
53    #[must_use]
54    pub fn to_bytes(&self) -> Vec<u8> {
55        self.0.to_be_bytes().to_vec()
56    }
57
58    /// Creates a `FieldElement` from a hex string.
59    ///
60    /// The hex string can optionally start with "0x".
61    ///
62    /// # Errors
63    ///
64    /// Returns an error if the hex string is invalid or cannot be parsed.
65    #[uniffi::constructor]
66    pub fn try_from_hex_string(hex_string: &str) -> Result<Self, WalletKitError> {
67        let fe = CoreFieldElement::from_str(hex_string)?;
68        Ok(Self(fe))
69    }
70
71    /// Converts the field element to a hex-encoded, padded string.
72    #[must_use]
73    pub fn to_hex_string(&self) -> String {
74        self.0.to_string()
75    }
76}
77
78impl From<FieldElement> for CoreFieldElement {
79    fn from(val: FieldElement) -> Self {
80        val.0
81    }
82}
83
84impl From<CoreFieldElement> for FieldElement {
85    fn from(val: CoreFieldElement) -> Self {
86        Self(val)
87    }
88}
89
90impl From<u64> for FieldElement {
91    fn from(value: u64) -> Self {
92        Self(CoreFieldElement::from(value))
93    }
94}
95
96impl Deref for FieldElement {
97    type Target = CoreFieldElement;
98
99    fn deref(&self) -> &Self::Target {
100        &self.0
101    }
102}
103
104#[cfg(test)]
105mod tests {
106    use super::*;
107
108    #[test]
109    fn test_from_u64() {
110        let fe = FieldElement::from_u64(42);
111        let bytes = fe.to_bytes();
112        assert!(!bytes.is_empty());
113        assert_eq!(bytes[31], 0x2a);
114    }
115
116    #[test]
117    fn test_round_trip_bytes() {
118        let original = FieldElement::from_u64(12345);
119        let bytes = original.to_bytes();
120        let restored = FieldElement::from_bytes(bytes).unwrap();
121
122        // Compare the serialized forms since FieldElement doesn't implement PartialEq
123        let original_bytes = original.to_bytes();
124        let restored_bytes = restored.to_bytes();
125        assert_eq!(original_bytes, restored_bytes);
126    }
127
128    #[test]
129    fn test_hex_round_trip() {
130        let original = FieldElement::from_u64(999);
131        let hex = original.to_hex_string();
132        let restored = FieldElement::try_from_hex_string(&hex).unwrap();
133
134        let original_bytes = original.to_bytes();
135        let restored_bytes = restored.to_bytes();
136        assert_eq!(original_bytes, restored_bytes);
137    }
138
139    #[test]
140    fn test_hex_string_with_and_without_0x() {
141        let fe = FieldElement::from_u64(255);
142        let hex = fe.to_hex_string();
143
144        // Should work with 0x prefix
145        let with_prefix = FieldElement::try_from_hex_string(&hex).unwrap();
146
147        // Should also work without 0x prefix
148        let hex_no_prefix = hex.trim_start_matches("0x");
149        let without_prefix = FieldElement::try_from_hex_string(hex_no_prefix).unwrap();
150
151        let with_bytes = with_prefix.to_bytes();
152        let without_bytes = without_prefix.to_bytes();
153        assert_eq!(with_bytes, without_bytes);
154    }
155
156    #[test]
157    fn test_invalid_hex_string() {
158        assert!(FieldElement::try_from_hex_string("0xZZZZ").is_err());
159        assert!(FieldElement::try_from_hex_string("not hex").is_err());
160    }
161
162    /// Ensures encoding is consistent with different round trips
163    #[test]
164    fn test_encoding_round_trip() {
165        let sub_one = CoreFieldElement::from(42u64);
166        let sub_two = FieldElement::from(sub_one);
167
168        assert_eq!(sub_one, *sub_two);
169        assert_eq!(sub_one.to_string(), sub_two.to_hex_string());
170
171        let sub_three =
172            FieldElement::try_from_hex_string(&sub_two.to_hex_string()).unwrap();
173        assert_eq!(sub_one, *sub_three);
174    }
175}