use crate::error::Error;
use once_cell::sync::Lazy;
use regex::Regex;
use serde::{Deserialize, Deserializer, Serialize, Serializer};
use std::str::FromStr;
static CHAIN_ID_REGEX: Lazy<Regex> = Lazy::new(|| {
Regex::new(r"^[-a-z0-9]{3,8}:[-a-zA-Z0-9]{1,64}$").expect("Failed to compile CHAIN_ID_REGEX")
});
#[derive(Debug, Clone, PartialEq, Eq, Hash)]
pub struct ChainId {
namespace: String,
reference: String,
}
impl ChainId {
pub fn new(namespace: &str, reference: &str) -> Result<Self, Error> {
Self::validate_namespace(namespace)?;
Self::validate_reference(reference)?;
let chain_id = format!("{}:{}", namespace, reference);
if !CHAIN_ID_REGEX.is_match(&chain_id) {
return Err(Error::InvalidChainId(chain_id));
}
Ok(Self {
namespace: namespace.to_string(),
reference: reference.to_string(),
})
}
pub fn namespace(&self) -> &str {
&self.namespace
}
pub fn reference(&self) -> &str {
&self.reference
}
fn validate_namespace(namespace: &str) -> Result<(), Error> {
if !Regex::new(r"^[-a-z0-9]{3,8}$")
.expect("Failed to compile namespace regex")
.is_match(namespace)
{
return Err(Error::InvalidNamespace(namespace.to_string()));
}
Ok(())
}
fn validate_reference(reference: &str) -> Result<(), Error> {
if !Regex::new(r"^[-a-zA-Z0-9]{1,64}$")
.expect("Failed to compile reference regex")
.is_match(reference)
{
return Err(Error::InvalidReference(reference.to_string()));
}
Ok(())
}
}
impl FromStr for ChainId {
type Err = Error;
fn from_str(s: &str) -> Result<Self, Self::Err> {
if !CHAIN_ID_REGEX.is_match(s) {
return Err(Error::InvalidChainId(s.to_string()));
}
let parts: Vec<&str> = s.split(':').collect();
if parts.len() != 2 {
return Err(Error::InvalidChainId(s.to_string()));
}
ChainId::new(parts[0], parts[1])
}
}
impl std::fmt::Display for ChainId {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(f, "{}:{}", self.namespace, self.reference)
}
}
impl Serialize for ChainId {
fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
where
S: Serializer,
{
serializer.serialize_str(&self.to_string())
}
}
impl<'de> Deserialize<'de> for ChainId {
fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
where
D: Deserializer<'de>,
{
let s = String::deserialize(deserializer)?;
ChainId::from_str(&s).map_err(serde::de::Error::custom)
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_valid_chain_ids() {
let eth_mainnet = ChainId::from_str("eip155:1").unwrap();
assert_eq!(eth_mainnet.namespace(), "eip155");
assert_eq!(eth_mainnet.reference(), "1");
assert_eq!(eth_mainnet.to_string(), "eip155:1");
let btc_mainnet = ChainId::from_str(
"bip122:000000000019d6689c085ae165831e934ff763ae46a2a6c172b3f1b60a8ce26f",
)
.unwrap();
assert_eq!(btc_mainnet.namespace(), "bip122");
assert_eq!(
btc_mainnet.reference(),
"000000000019d6689c085ae165831e934ff763ae46a2a6c172b3f1b60a8ce26f"
);
let poly_mainnet = ChainId::new("polkadot", "91b171bb158e2d3848fa23a9f1c25182").unwrap();
assert_eq!(poly_mainnet.namespace(), "polkadot");
assert_eq!(poly_mainnet.reference(), "91b171bb158e2d3848fa23a9f1c25182");
}
#[test]
fn test_invalid_chain_ids() {
assert!(ChainId::from_str("").is_err());
assert!(ChainId::from_str("eip1551").is_err());
assert!(ChainId::from_str(":1").is_err());
assert!(ChainId::from_str("eip155:").is_err());
assert!(ChainId::from_str("ei:1").is_err());
assert!(ChainId::from_str("eip155toolong:1").is_err());
assert!(ChainId::from_str("EIP155:1").is_err()); assert!(ChainId::from_str("eip_155:1").is_err());
let long_reference = "a".repeat(65);
assert!(ChainId::from_str(&format!("eip155:{}", long_reference)).is_err());
}
#[test]
fn test_serialization() {
let chain_id = ChainId::from_str("eip155:1").unwrap();
let serialized = serde_json::to_string(&chain_id).unwrap();
assert_eq!(serialized, r#""eip155:1""#);
let deserialized: ChainId = serde_json::from_str(&serialized).unwrap();
assert_eq!(deserialized, chain_id);
}
#[test]
fn test_display_formatting() {
let chain_id = ChainId::from_str("eip155:1").unwrap();
assert_eq!(format!("{}", chain_id), "eip155:1");
assert_eq!(chain_id.to_string(), "eip155:1");
}
}