world_id_primitives/
rp.rs1#![allow(clippy::unreadable_literal)]
2
3use std::{fmt, str::FromStr};
4
5use ark_ff::{BigInteger as _, PrimeField as _};
6use serde::{Deserialize, Deserializer, Serialize, Serializer, de::Error as _};
7
8use crate::FieldElement;
9
10#[derive(Clone, Copy, Debug, PartialEq, Eq, PartialOrd, Ord, Hash)]
12pub struct RpId(u64);
13
14impl RpId {
15 #[must_use]
17 pub const fn into_inner(self) -> u64 {
18 self.0
19 }
20
21 #[must_use]
23 pub const fn new(value: u64) -> Self {
24 Self(value)
25 }
26}
27
28impl fmt::Display for RpId {
29 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
30 write!(f, "rp_{:016x}", self.0)
31 }
32}
33
34impl FromStr for RpId {
35 type Err = String;
36
37 fn from_str(s: &str) -> Result<Self, Self::Err> {
38 if let Some(id) = s.strip_prefix("rp_") {
39 Ok(Self(u64::from_str_radix(id, 16).map_err(|_| {
40 "Invalid RP ID format: expected hex string".to_string()
41 })?))
42 } else {
43 Err("A valid RP ID must start with 'rp_'".to_string())
44 }
45 }
46}
47
48impl From<u64> for RpId {
49 fn from(value: u64) -> Self {
50 Self(value)
51 }
52}
53
54impl From<RpId> for FieldElement {
55 fn from(value: RpId) -> Self {
56 Self::from(value.0)
57 }
58}
59
60impl Serialize for RpId {
61 fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
62 where
63 S: Serializer,
64 {
65 if serializer.is_human_readable() {
66 serializer.serialize_str(&self.to_string())
67 } else {
68 u64::serialize(&self.0, serializer)
69 }
70 }
71}
72
73impl<'de> Deserialize<'de> for RpId {
74 fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
75 where
76 D: Deserializer<'de>,
77 {
78 if deserializer.is_human_readable() {
79 let s = String::deserialize(deserializer)?;
80 Self::from_str(&s).map_err(D::Error::custom)
81 } else {
82 let value = u64::deserialize(deserializer)?;
83 Ok(Self(value))
84 }
85 }
86}
87
88#[must_use]
95pub fn compute_rp_signature_msg(
96 nonce: ark_babyjubjub::Fq,
97 created_at: u64,
98 expires_at: u64,
99) -> Vec<u8> {
100 let mut msg = Vec::with_capacity(48);
101 msg.extend(nonce.into_bigint().to_bytes_be());
102 msg.extend(created_at.to_be_bytes());
103 msg.extend(expires_at.to_be_bytes());
104 msg
105}
106
107#[cfg(test)]
108mod tests {
109 use super::*;
110
111 #[test]
112 fn test_rpid_display() {
113 let rp_id = RpId::new(0x123456789abcdef0);
114 assert_eq!(rp_id.to_string(), "rp_123456789abcdef0");
115
116 let rp_id = RpId::new(u64::MAX);
117 assert_eq!(rp_id.to_string(), "rp_ffffffffffffffff");
118
119 let rp_id = RpId::new(0);
120 assert_eq!(rp_id.to_string(), "rp_0000000000000000");
121 }
122
123 #[test]
124 fn test_rpid_from_str() {
125 let rp_id = "rp_123456789abcdef0".parse::<RpId>().unwrap();
126 assert_eq!(rp_id.0, 0x123456789abcdef0);
127
128 let rp_id = "rp_ffffffffffffffff".parse::<RpId>().unwrap();
129 assert_eq!(rp_id.0, u64::MAX);
130
131 let rp_id = "rp_0000000000000000".parse::<RpId>().unwrap();
132 assert_eq!(rp_id.0, 0);
133
134 let rp_id = "rp_123456789ABCDEF0".parse::<RpId>().unwrap();
135 assert_eq!(rp_id.0, 0x123456789abcdef0);
136 }
137
138 #[test]
139 fn test_rpid_from_str_errors() {
140 assert!("123456789abcdef0".parse::<RpId>().is_err());
141 assert!("rp_invalid".parse::<RpId>().is_err());
142 assert!("rp_".parse::<RpId>().is_err());
143 }
144
145 #[test]
146 fn test_rpid_roundtrip() {
147 let original = RpId::new(0x123456789abcdef0);
148 let s = original.to_string();
149 let parsed = s.parse::<RpId>().unwrap();
150 assert_eq!(original, parsed);
151 }
152
153 #[test]
154 fn test_rpid_json_serialization() {
155 let rp_id = RpId::new(0x123456789abcdef0);
156 let json = serde_json::to_string(&rp_id).unwrap();
157 assert_eq!(json, "\"rp_123456789abcdef0\"");
158
159 let deserialized: RpId = serde_json::from_str(&json).unwrap();
160 assert_eq!(rp_id, deserialized);
161 }
162
163 #[test]
164 fn test_rpid_binary_serialization() {
165 let rp_id = RpId::new(0x123456789abcdef0);
166
167 let mut buffer = Vec::new();
168 ciborium::into_writer(&rp_id, &mut buffer).unwrap();
169
170 let decoded: RpId = ciborium::from_reader(&buffer[..]).unwrap();
171
172 assert_eq!(rp_id, decoded);
173 }
174
175 #[test]
176 fn test_compute_rp_signature_msg_fixed_length() {
177 let nonce = ark_babyjubjub::Fq::from(1u64);
180 let created_at = 1000u64;
181 let expires_at = 2000u64;
182
183 let msg = compute_rp_signature_msg(nonce, created_at, expires_at);
184
185 assert_eq!(
187 msg.len(),
188 48,
189 "RP signature message must be exactly 48 bytes"
190 );
191 }
192}