1use base64::engine::general_purpose::URL_SAFE_NO_PAD;
8use base64::Engine as _;
9use serde::{Deserialize, Deserializer, Serialize, Serializer};
10use std::fmt;
11use std::str::FromStr;
12
13use crate::error::CoreError;
14
15#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
22#[serde(transparent)]
23pub struct NetworkId(String);
24
25impl NetworkId {
26 pub fn new(s: impl Into<String>) -> Result<Self, CoreError> {
27 let s: String = s.into();
28 validate_network_id(&s)?;
29 Ok(Self(s))
30 }
31
32 pub fn as_str(&self) -> &str {
33 &self.0
34 }
35}
36
37impl fmt::Display for NetworkId {
38 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
39 f.write_str(&self.0)
40 }
41}
42
43impl FromStr for NetworkId {
44 type Err = CoreError;
45 fn from_str(s: &str) -> Result<Self, Self::Err> {
46 Self::new(s)
47 }
48}
49
50fn validate_network_id(s: &str) -> Result<(), CoreError> {
51 let invalid = |reason: &str| CoreError::InvalidNetworkId(format!("{s:?}: {reason}"));
52 if s.is_empty() {
53 return Err(invalid("empty"));
54 }
55 if s.len() > 64 {
56 return Err(invalid("longer than 64 bytes"));
57 }
58 if s.starts_with('-') || s.ends_with('-') {
59 return Err(invalid("leading or trailing hyphen"));
60 }
61 if !s
62 .bytes()
63 .all(|b| b.is_ascii_lowercase() || b.is_ascii_digit() || b == b'-')
64 {
65 return Err(invalid("contains characters outside [a-z0-9-]"));
66 }
67 Ok(())
68}
69
70#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
76pub struct AgentPubkey([u8; 32]);
77
78impl AgentPubkey {
79 pub const SEALED_SENDER: AgentPubkey = AgentPubkey([0xFF; 32]);
84
85 pub fn from_bytes(bytes: [u8; 32]) -> Self {
86 Self(bytes)
87 }
88
89 pub fn as_bytes(&self) -> &[u8; 32] {
90 &self.0
91 }
92}
93
94impl fmt::Display for AgentPubkey {
95 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
96 f.write_str(&URL_SAFE_NO_PAD.encode(self.0))
97 }
98}
99
100impl FromStr for AgentPubkey {
101 type Err = CoreError;
102 fn from_str(s: &str) -> Result<Self, Self::Err> {
103 let bytes = URL_SAFE_NO_PAD.decode(s)?;
104 let arr: [u8; 32] = bytes.try_into().map_err(|v: Vec<u8>| {
105 CoreError::InvalidAgentPubkey(format!("expected 32 bytes, got {}", v.len()))
106 })?;
107 Ok(Self(arr))
108 }
109}
110
111impl Serialize for AgentPubkey {
112 fn serialize<S: Serializer>(&self, ser: S) -> Result<S::Ok, S::Error> {
113 ser.serialize_str(&self.to_string())
114 }
115}
116
117impl<'de> Deserialize<'de> for AgentPubkey {
118 fn deserialize<D: Deserializer<'de>>(de: D) -> Result<Self, D::Error> {
119 let s = String::deserialize(de)?;
120 s.parse().map_err(serde::de::Error::custom)
121 }
122}
123
124macro_rules! opaque_id {
129 ($name:ident, $err_variant:ident, $ctx:literal) => {
130 #[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
131 pub struct $name([u8; 16]);
132
133 impl $name {
134 pub fn from_bytes(bytes: [u8; 16]) -> Self {
135 Self(bytes)
136 }
137
138 pub fn as_bytes(&self) -> &[u8; 16] {
139 &self.0
140 }
141
142 pub fn generate() -> Self {
144 use rand::RngCore as _;
145 let mut bytes = [0u8; 16];
146 rand::thread_rng().fill_bytes(&mut bytes);
147 Self(bytes)
148 }
149 }
150
151 impl fmt::Display for $name {
152 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
153 f.write_str(&URL_SAFE_NO_PAD.encode(self.0))
154 }
155 }
156
157 impl FromStr for $name {
158 type Err = CoreError;
159 fn from_str(s: &str) -> Result<Self, Self::Err> {
160 let bytes = URL_SAFE_NO_PAD.decode(s)?;
161 let arr: [u8; 16] = bytes.try_into().map_err(|v: Vec<u8>| {
162 CoreError::$err_variant(format!(
163 concat!($ctx, ": expected 16 bytes, got {}"),
164 v.len()
165 ))
166 })?;
167 Ok(Self(arr))
168 }
169 }
170
171 impl Serialize for $name {
172 fn serialize<S: Serializer>(&self, ser: S) -> Result<S::Ok, S::Error> {
173 ser.serialize_str(&self.to_string())
174 }
175 }
176
177 impl<'de> Deserialize<'de> for $name {
178 fn deserialize<D: Deserializer<'de>>(de: D) -> Result<Self, D::Error> {
179 let s = String::deserialize(de)?;
180 s.parse().map_err(serde::de::Error::custom)
181 }
182 }
183 };
184}
185
186opaque_id!(ChannelId, InvalidChannelId, "channel_id");
187opaque_id!(MessageId, InvalidMessageId, "message_id");
188opaque_id!(BlobId, InvalidBlobId, "blob_id");
189
190impl ChannelId {
191 #[must_use]
195 pub fn feed_for(author: &AgentPubkey) -> Self {
196 use sha2::{Digest as _, Sha256};
197 let mut hasher = Sha256::new();
198 hasher.update(b"parley-feed-v1");
199 hasher.update(author.as_bytes());
200 let digest = hasher.finalize();
201 let mut id = [0u8; 16];
202 id.copy_from_slice(&digest[..16]);
203 Self(id)
204 }
205}
206
207#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash, Serialize, Deserialize)]
213#[serde(transparent)]
214pub struct Seq(pub u64);
215
216impl fmt::Display for Seq {
217 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
218 self.0.fmt(f)
219 }
220}
221
222#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
227pub struct Nonce([u8; 16]);
228
229impl Nonce {
230 pub fn from_bytes(bytes: [u8; 16]) -> Self {
231 Self(bytes)
232 }
233
234 pub fn as_bytes(&self) -> &[u8; 16] {
235 &self.0
236 }
237
238 pub fn generate() -> Self {
239 use rand::RngCore as _;
240 let mut bytes = [0u8; 16];
241 rand::thread_rng().fill_bytes(&mut bytes);
242 Self(bytes)
243 }
244}
245
246impl fmt::Display for Nonce {
247 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
248 f.write_str(&URL_SAFE_NO_PAD.encode(self.0))
249 }
250}
251
252impl FromStr for Nonce {
253 type Err = CoreError;
254 fn from_str(s: &str) -> Result<Self, Self::Err> {
255 let bytes = URL_SAFE_NO_PAD.decode(s)?;
256 let arr: [u8; 16] = bytes.try_into().map_err(|v: Vec<u8>| {
257 CoreError::InvalidNonce(format!("expected 16 bytes, got {}", v.len()))
258 })?;
259 Ok(Self(arr))
260 }
261}
262
263impl Serialize for Nonce {
264 fn serialize<S: Serializer>(&self, ser: S) -> Result<S::Ok, S::Error> {
265 ser.serialize_str(&self.to_string())
266 }
267}
268
269impl<'de> Deserialize<'de> for Nonce {
270 fn deserialize<D: Deserializer<'de>>(de: D) -> Result<Self, D::Error> {
271 let s = String::deserialize(de)?;
272 s.parse().map_err(serde::de::Error::custom)
273 }
274}
275
276#[cfg(test)]
277#[allow(clippy::unwrap_used)]
278mod tests {
279 use super::*;
280
281 #[test]
282 fn network_id_validates() {
283 assert!(NetworkId::new("parley-mainnet").is_ok());
284 assert!(NetworkId::new("a").is_ok());
285 assert!(NetworkId::new("").is_err());
286 assert!(NetworkId::new("-leading").is_err());
287 assert!(NetworkId::new("trailing-").is_err());
288 assert!(NetworkId::new("UPPER").is_err());
289 assert!(NetworkId::new("under_score").is_err());
290 assert!(NetworkId::new("x".repeat(65)).is_err());
291 }
292
293 #[test]
294 fn channel_id_roundtrip() {
295 let id = ChannelId::generate();
296 let s = id.to_string();
297 assert_eq!(s.len(), 22);
298 let parsed: ChannelId = s.parse().unwrap();
299 assert_eq!(id, parsed);
300 }
301
302 #[test]
303 fn agent_pubkey_roundtrip() {
304 let pk = AgentPubkey::from_bytes([7u8; 32]);
305 let s = pk.to_string();
306 assert_eq!(s.len(), 43);
307 let parsed: AgentPubkey = s.parse().unwrap();
308 assert_eq!(pk, parsed);
309 }
310}