use base64::engine::general_purpose::URL_SAFE_NO_PAD;
use base64::Engine as _;
use serde::{Deserialize, Deserializer, Serialize, Serializer};
use std::fmt;
use std::str::FromStr;
use crate::error::CoreError;
#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
#[serde(transparent)]
pub struct NetworkId(String);
impl NetworkId {
pub fn new(s: impl Into<String>) -> Result<Self, CoreError> {
let s: String = s.into();
validate_network_id(&s)?;
Ok(Self(s))
}
pub fn as_str(&self) -> &str {
&self.0
}
}
impl fmt::Display for NetworkId {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.write_str(&self.0)
}
}
impl FromStr for NetworkId {
type Err = CoreError;
fn from_str(s: &str) -> Result<Self, Self::Err> {
Self::new(s)
}
}
fn validate_network_id(s: &str) -> Result<(), CoreError> {
let invalid = |reason: &str| CoreError::InvalidNetworkId(format!("{s:?}: {reason}"));
if s.is_empty() {
return Err(invalid("empty"));
}
if s.len() > 64 {
return Err(invalid("longer than 64 bytes"));
}
if s.starts_with('-') || s.ends_with('-') {
return Err(invalid("leading or trailing hyphen"));
}
if !s
.bytes()
.all(|b| b.is_ascii_lowercase() || b.is_ascii_digit() || b == b'-')
{
return Err(invalid("contains characters outside [a-z0-9-]"));
}
Ok(())
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
pub struct AgentPubkey([u8; 32]);
impl AgentPubkey {
pub fn from_bytes(bytes: [u8; 32]) -> Self {
Self(bytes)
}
pub fn as_bytes(&self) -> &[u8; 32] {
&self.0
}
}
impl fmt::Display for AgentPubkey {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.write_str(&URL_SAFE_NO_PAD.encode(self.0))
}
}
impl FromStr for AgentPubkey {
type Err = CoreError;
fn from_str(s: &str) -> Result<Self, Self::Err> {
let bytes = URL_SAFE_NO_PAD.decode(s)?;
let arr: [u8; 32] = bytes.try_into().map_err(|v: Vec<u8>| {
CoreError::InvalidAgentPubkey(format!("expected 32 bytes, got {}", v.len()))
})?;
Ok(Self(arr))
}
}
impl Serialize for AgentPubkey {
fn serialize<S: Serializer>(&self, ser: S) -> Result<S::Ok, S::Error> {
ser.serialize_str(&self.to_string())
}
}
impl<'de> Deserialize<'de> for AgentPubkey {
fn deserialize<D: Deserializer<'de>>(de: D) -> Result<Self, D::Error> {
let s = String::deserialize(de)?;
s.parse().map_err(serde::de::Error::custom)
}
}
macro_rules! opaque_id {
($name:ident, $err_variant:ident, $ctx:literal) => {
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
pub struct $name([u8; 16]);
impl $name {
pub fn from_bytes(bytes: [u8; 16]) -> Self {
Self(bytes)
}
pub fn as_bytes(&self) -> &[u8; 16] {
&self.0
}
pub fn generate() -> Self {
use rand::RngCore as _;
let mut bytes = [0u8; 16];
rand::thread_rng().fill_bytes(&mut bytes);
Self(bytes)
}
}
impl fmt::Display for $name {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.write_str(&URL_SAFE_NO_PAD.encode(self.0))
}
}
impl FromStr for $name {
type Err = CoreError;
fn from_str(s: &str) -> Result<Self, Self::Err> {
let bytes = URL_SAFE_NO_PAD.decode(s)?;
let arr: [u8; 16] = bytes.try_into().map_err(|v: Vec<u8>| {
CoreError::$err_variant(format!(
concat!($ctx, ": expected 16 bytes, got {}"),
v.len()
))
})?;
Ok(Self(arr))
}
}
impl Serialize for $name {
fn serialize<S: Serializer>(&self, ser: S) -> Result<S::Ok, S::Error> {
ser.serialize_str(&self.to_string())
}
}
impl<'de> Deserialize<'de> for $name {
fn deserialize<D: Deserializer<'de>>(de: D) -> Result<Self, D::Error> {
let s = String::deserialize(de)?;
s.parse().map_err(serde::de::Error::custom)
}
}
};
}
opaque_id!(ChannelId, InvalidChannelId, "channel_id");
opaque_id!(MessageId, InvalidMessageId, "message_id");
opaque_id!(BlobId, InvalidBlobId, "blob_id");
#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash, Serialize, Deserialize)]
#[serde(transparent)]
pub struct Seq(pub u64);
impl fmt::Display for Seq {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
self.0.fmt(f)
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
pub struct Nonce([u8; 16]);
impl Nonce {
pub fn from_bytes(bytes: [u8; 16]) -> Self {
Self(bytes)
}
pub fn as_bytes(&self) -> &[u8; 16] {
&self.0
}
pub fn generate() -> Self {
use rand::RngCore as _;
let mut bytes = [0u8; 16];
rand::thread_rng().fill_bytes(&mut bytes);
Self(bytes)
}
}
impl fmt::Display for Nonce {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.write_str(&URL_SAFE_NO_PAD.encode(self.0))
}
}
impl FromStr for Nonce {
type Err = CoreError;
fn from_str(s: &str) -> Result<Self, Self::Err> {
let bytes = URL_SAFE_NO_PAD.decode(s)?;
let arr: [u8; 16] = bytes.try_into().map_err(|v: Vec<u8>| {
CoreError::InvalidNonce(format!("expected 16 bytes, got {}", v.len()))
})?;
Ok(Self(arr))
}
}
impl Serialize for Nonce {
fn serialize<S: Serializer>(&self, ser: S) -> Result<S::Ok, S::Error> {
ser.serialize_str(&self.to_string())
}
}
impl<'de> Deserialize<'de> for Nonce {
fn deserialize<D: Deserializer<'de>>(de: D) -> Result<Self, D::Error> {
let s = String::deserialize(de)?;
s.parse().map_err(serde::de::Error::custom)
}
}
#[cfg(test)]
#[allow(clippy::unwrap_used)]
mod tests {
use super::*;
#[test]
fn network_id_validates() {
assert!(NetworkId::new("parley-mainnet").is_ok());
assert!(NetworkId::new("a").is_ok());
assert!(NetworkId::new("").is_err());
assert!(NetworkId::new("-leading").is_err());
assert!(NetworkId::new("trailing-").is_err());
assert!(NetworkId::new("UPPER").is_err());
assert!(NetworkId::new("under_score").is_err());
assert!(NetworkId::new("x".repeat(65)).is_err());
}
#[test]
fn channel_id_roundtrip() {
let id = ChannelId::generate();
let s = id.to_string();
assert_eq!(s.len(), 22);
let parsed: ChannelId = s.parse().unwrap();
assert_eq!(id, parsed);
}
#[test]
fn agent_pubkey_roundtrip() {
let pk = AgentPubkey::from_bytes([7u8; 32]);
let s = pk.to_string();
assert_eq!(s.len(), 43);
let parsed: AgentPubkey = s.parse().unwrap();
assert_eq!(pk, parsed);
}
}