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 fn from_bytes(bytes: [u8; 32]) -> Self {
80 Self(bytes)
81 }
82
83 pub fn as_bytes(&self) -> &[u8; 32] {
84 &self.0
85 }
86}
87
88impl fmt::Display for AgentPubkey {
89 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
90 f.write_str(&URL_SAFE_NO_PAD.encode(self.0))
91 }
92}
93
94impl FromStr for AgentPubkey {
95 type Err = CoreError;
96 fn from_str(s: &str) -> Result<Self, Self::Err> {
97 let bytes = URL_SAFE_NO_PAD.decode(s)?;
98 let arr: [u8; 32] = bytes.try_into().map_err(|v: Vec<u8>| {
99 CoreError::InvalidAgentPubkey(format!("expected 32 bytes, got {}", v.len()))
100 })?;
101 Ok(Self(arr))
102 }
103}
104
105impl Serialize for AgentPubkey {
106 fn serialize<S: Serializer>(&self, ser: S) -> Result<S::Ok, S::Error> {
107 ser.serialize_str(&self.to_string())
108 }
109}
110
111impl<'de> Deserialize<'de> for AgentPubkey {
112 fn deserialize<D: Deserializer<'de>>(de: D) -> Result<Self, D::Error> {
113 let s = String::deserialize(de)?;
114 s.parse().map_err(serde::de::Error::custom)
115 }
116}
117
118macro_rules! opaque_id {
123 ($name:ident, $err_variant:ident, $ctx:literal) => {
124 #[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
125 pub struct $name([u8; 16]);
126
127 impl $name {
128 pub fn from_bytes(bytes: [u8; 16]) -> Self {
129 Self(bytes)
130 }
131
132 pub fn as_bytes(&self) -> &[u8; 16] {
133 &self.0
134 }
135
136 pub fn generate() -> Self {
138 use rand::RngCore as _;
139 let mut bytes = [0u8; 16];
140 rand::thread_rng().fill_bytes(&mut bytes);
141 Self(bytes)
142 }
143 }
144
145 impl fmt::Display for $name {
146 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
147 f.write_str(&URL_SAFE_NO_PAD.encode(self.0))
148 }
149 }
150
151 impl FromStr for $name {
152 type Err = CoreError;
153 fn from_str(s: &str) -> Result<Self, Self::Err> {
154 let bytes = URL_SAFE_NO_PAD.decode(s)?;
155 let arr: [u8; 16] = bytes.try_into().map_err(|v: Vec<u8>| {
156 CoreError::$err_variant(format!(
157 concat!($ctx, ": expected 16 bytes, got {}"),
158 v.len()
159 ))
160 })?;
161 Ok(Self(arr))
162 }
163 }
164
165 impl Serialize for $name {
166 fn serialize<S: Serializer>(&self, ser: S) -> Result<S::Ok, S::Error> {
167 ser.serialize_str(&self.to_string())
168 }
169 }
170
171 impl<'de> Deserialize<'de> for $name {
172 fn deserialize<D: Deserializer<'de>>(de: D) -> Result<Self, D::Error> {
173 let s = String::deserialize(de)?;
174 s.parse().map_err(serde::de::Error::custom)
175 }
176 }
177 };
178}
179
180opaque_id!(ChannelId, InvalidChannelId, "channel_id");
181opaque_id!(MessageId, InvalidMessageId, "message_id");
182opaque_id!(BlobId, InvalidBlobId, "blob_id");
183
184#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash, Serialize, Deserialize)]
190#[serde(transparent)]
191pub struct Seq(pub u64);
192
193impl fmt::Display for Seq {
194 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
195 self.0.fmt(f)
196 }
197}
198
199#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
204pub struct Nonce([u8; 16]);
205
206impl Nonce {
207 pub fn from_bytes(bytes: [u8; 16]) -> Self {
208 Self(bytes)
209 }
210
211 pub fn as_bytes(&self) -> &[u8; 16] {
212 &self.0
213 }
214
215 pub fn generate() -> Self {
216 use rand::RngCore as _;
217 let mut bytes = [0u8; 16];
218 rand::thread_rng().fill_bytes(&mut bytes);
219 Self(bytes)
220 }
221}
222
223impl fmt::Display for Nonce {
224 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
225 f.write_str(&URL_SAFE_NO_PAD.encode(self.0))
226 }
227}
228
229impl FromStr for Nonce {
230 type Err = CoreError;
231 fn from_str(s: &str) -> Result<Self, Self::Err> {
232 let bytes = URL_SAFE_NO_PAD.decode(s)?;
233 let arr: [u8; 16] = bytes.try_into().map_err(|v: Vec<u8>| {
234 CoreError::InvalidNonce(format!("expected 16 bytes, got {}", v.len()))
235 })?;
236 Ok(Self(arr))
237 }
238}
239
240impl Serialize for Nonce {
241 fn serialize<S: Serializer>(&self, ser: S) -> Result<S::Ok, S::Error> {
242 ser.serialize_str(&self.to_string())
243 }
244}
245
246impl<'de> Deserialize<'de> for Nonce {
247 fn deserialize<D: Deserializer<'de>>(de: D) -> Result<Self, D::Error> {
248 let s = String::deserialize(de)?;
249 s.parse().map_err(serde::de::Error::custom)
250 }
251}
252
253#[cfg(test)]
254#[allow(clippy::unwrap_used)]
255mod tests {
256 use super::*;
257
258 #[test]
259 fn network_id_validates() {
260 assert!(NetworkId::new("parley-mainnet").is_ok());
261 assert!(NetworkId::new("a").is_ok());
262 assert!(NetworkId::new("").is_err());
263 assert!(NetworkId::new("-leading").is_err());
264 assert!(NetworkId::new("trailing-").is_err());
265 assert!(NetworkId::new("UPPER").is_err());
266 assert!(NetworkId::new("under_score").is_err());
267 assert!(NetworkId::new("x".repeat(65)).is_err());
268 }
269
270 #[test]
271 fn channel_id_roundtrip() {
272 let id = ChannelId::generate();
273 let s = id.to_string();
274 assert_eq!(s.len(), 22);
275 let parsed: ChannelId = s.parse().unwrap();
276 assert_eq!(id, parsed);
277 }
278
279 #[test]
280 fn agent_pubkey_roundtrip() {
281 let pk = AgentPubkey::from_bytes([7u8; 32]);
282 let s = pk.to_string();
283 assert_eq!(s.len(), 43);
284 let parsed: AgentPubkey = s.parse().unwrap();
285 assert_eq!(pk, parsed);
286 }
287}