use std::{str::FromStr, sync::LazyLock};
use regex::Regex;
use serde::{Deserialize, Deserializer, Serialize};
use thiserror::Error;
use utoipa::ToSchema;
#[derive(
Clone, Debug, PartialEq, Eq, PartialOrd, Ord, Hash, ToSchema, bincode::Decode, bincode::Encode,
)]
pub struct ChainKey {
pub namespace: ChainKeyNamespace,
pub id: ChainKeyId,
}
type ChainKeyResult<T> = Result<T, ChainKeyError>;
static CHAIN_KEY_REGEX: LazyLock<Regex> =
LazyLock::new(|| Regex::new(r"^([-a-z0-9]{1,32}):([-_a-zA-Z0-9]{1,32})$").unwrap());
static NAMESPACE_REGEX: LazyLock<Regex> =
LazyLock::new(|| Regex::new(r"^[-a-z0-9]{1,32}$").unwrap());
static ID_REGEX: LazyLock<Regex> = LazyLock::new(|| Regex::new(r"^[-_a-zA-Z0-9]{1,32}$").unwrap());
impl ChainKey {
pub fn validate(s: impl AsRef<str>) -> ChainKeyResult<()> {
let s = s.as_ref();
if !CHAIN_KEY_REGEX.is_match(s) {
Err(ChainKeyError::FormatError)
} else {
Ok(())
}
}
pub fn new(s: impl Into<String>) -> ChainKeyResult<Self> {
let s = s.into();
let captures = CHAIN_KEY_REGEX
.captures(&s)
.ok_or(ChainKeyError::FormatError)?;
let namespace = ChainKeyNamespace::new(&captures[1])?;
let id = ChainKeyId::new(&captures[2])?;
Ok(Self { namespace, id })
}
}
impl<'de> Deserialize<'de> for ChainKey {
fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
where
D: Deserializer<'de>,
{
let s = String::deserialize(deserializer)?;
Self::new(s).map_err(serde::de::Error::custom)
}
}
impl Serialize for ChainKey {
fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
where
S: serde::Serializer,
{
serializer.serialize_str(&self.to_string())
}
}
impl std::fmt::Display for ChainKey {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(f, "{}:{}", self.namespace, self.id)
}
}
impl TryFrom<&str> for ChainKey {
type Error = ChainKeyError;
fn try_from(s: &str) -> Result<Self, Self::Error> {
s.parse()
}
}
impl FromStr for ChainKey {
type Err = ChainKeyError;
fn from_str(s: &str) -> Result<Self, Self::Err> {
Self::new(s)
}
}
impl From<ChainKey> for layer_climb::prelude::ChainId {
fn from(key: ChainKey) -> Self {
layer_climb::prelude::ChainId::new(key.id.into_inner())
}
}
#[derive(
Clone, Debug, PartialEq, Eq, PartialOrd, Ord, Hash, ToSchema, bincode::Decode, bincode::Encode,
)]
pub struct ChainKeyNamespace(String);
impl ChainKeyNamespace {
pub const EVM: &str = "evm";
pub const COSMOS: &str = "cosmos";
pub const DEV: &str = "dev";
pub fn new(s: impl Into<String>) -> ChainKeyResult<Self> {
let s = s.into();
if NAMESPACE_REGEX.is_match(&s) {
Ok(Self(s))
} else {
Err(ChainKeyError::InvalidNamespace)
}
}
pub fn as_str(&self) -> &str {
&self.0
}
pub fn into_inner(self) -> String {
self.0
}
}
impl FromStr for ChainKeyNamespace {
type Err = ChainKeyError;
fn from_str(s: &str) -> Result<Self, Self::Err> {
Self::new(s.to_string())
}
}
impl TryFrom<&str> for ChainKeyNamespace {
type Error = ChainKeyError;
fn try_from(s: &str) -> Result<Self, Self::Error> {
s.parse()
}
}
impl std::fmt::Display for ChainKeyNamespace {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.write_str(&self.0)
}
}
impl<'de> Deserialize<'de> for ChainKeyNamespace {
fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
where
D: Deserializer<'de>,
{
let s = String::deserialize(deserializer)?;
Self::new(s).map_err(serde::de::Error::custom)
}
}
impl Serialize for ChainKeyNamespace {
fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
where
S: serde::Serializer,
{
serializer.serialize_str(&self.0)
}
}
#[derive(
Clone, Debug, PartialEq, Eq, PartialOrd, Ord, Hash, ToSchema, bincode::Decode, bincode::Encode,
)]
pub struct ChainKeyId(String);
impl ChainKeyId {
pub fn new(s: impl Into<String>) -> ChainKeyResult<Self> {
let s = s.into();
if ID_REGEX.is_match(&s) {
Ok(Self(s))
} else {
Err(ChainKeyError::InvalidId)
}
}
pub fn as_str(&self) -> &str {
&self.0
}
pub fn into_inner(self) -> String {
self.0
}
}
impl FromStr for ChainKeyId {
type Err = ChainKeyError;
fn from_str(s: &str) -> Result<Self, Self::Err> {
Self::new(s.to_string())
}
}
impl TryFrom<&str> for ChainKeyId {
type Error = ChainKeyError;
fn try_from(s: &str) -> Result<Self, Self::Error> {
s.parse()
}
}
impl std::fmt::Display for ChainKeyId {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.write_str(&self.0)
}
}
impl<'de> Deserialize<'de> for ChainKeyId {
fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
where
D: Deserializer<'de>,
{
let s = String::deserialize(deserializer)?;
Self::new(s).map_err(serde::de::Error::custom)
}
}
impl Serialize for ChainKeyId {
fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
where
S: serde::Serializer,
{
serializer.serialize_str(&self.0)
}
}
impl From<ChainKeyId> for layer_climb::prelude::ChainId {
fn from(id: ChainKeyId) -> Self {
layer_climb::prelude::ChainId::new(id.into_inner())
}
}
#[derive(Error, Debug, PartialEq, Eq, Clone)]
pub enum ChainKeyError {
#[error("ChainKey must follow the CAIP-2-like format of 'namespace:id'")]
FormatError,
#[error("Invalid namespace component")]
InvalidNamespace,
#[error("Invalid id component")]
InvalidId,
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_valid_chain_ids() {
assert!(ChainKey::new("eip155:1").is_ok());
assert!(ChainKey::new("cosmos:cosmoshub-4").is_ok());
assert!(ChainKey::new("polkadot:b0a8d493285c2df73290dfb7e61f870f").is_ok());
assert!(ChainKey::new("abc:X_Y-Z123").is_ok());
assert!(ChainKey::new("abc-123:ABC_xyz-123").is_ok());
}
#[test]
fn test_invalid_chain_ids() {
assert!(ChainKey::new("no-colon").is_err());
assert!(ChainKey::new("too:many:colons").is_err());
assert!(ChainKey::new(":empty-namespace").is_err());
assert!(ChainKey::new("empty-id:").is_err());
assert!(ChainKey::new("thisiswaytoolongtobeavalidnamespace:ref").is_err()); assert!(ChainKey::new("ABC:ref").is_err()); assert!(ChainKey::new("ab_c:ref").is_err()); assert!(ChainKey::new("ab.c:ref").is_err());
assert!(ChainKey::new("abc:").is_err()); assert!(ChainKey::new("abc:this-is-way-too-long-to-be-a-valid-idt").is_err()); assert!(ChainKey::new("abc:id.with.dots").is_err()); assert!(ChainKey::new("abc:id@123").is_err()); assert!(ChainKey::new("abc:id space").is_err()); }
#[test]
fn test_accessors() {
let chain_key = ChainKey::new("eip155:1").unwrap();
assert_eq!(chain_key.to_string(), "eip155:1");
assert_eq!(chain_key.namespace.as_str(), "eip155");
assert_eq!(chain_key.id.as_str(), "1");
}
#[test]
fn test_edge_cases() {
assert!(ChainKey::new("abc:x").is_ok());
assert!(ChainKey::new("abcd1234:ABCD1234_abcd1234-ABCD1234").is_ok());
let chain_key = ChainKey::new("eip155:MyChain_123").unwrap();
assert_eq!(chain_key.id.as_str(), "MyChain_123");
}
}