use std::collections::{HashMap, HashSet};
use std::fmt;
use std::str::FromStr;
use std::sync::Arc;
use serde::{Deserialize, Deserializer, Serialize, Serializer, de};
#[derive(Debug, Clone, PartialEq, Eq, Hash)]
pub struct ChainId {
namespace: String,
reference: String,
}
impl ChainId {
pub fn new<N: Into<String>, R: Into<String>>(namespace: N, reference: R) -> Self {
Self {
namespace: namespace.into(),
reference: reference.into(),
}
}
#[must_use]
pub fn namespace(&self) -> &str {
&self.namespace
}
#[must_use]
pub fn reference(&self) -> &str {
&self.reference
}
#[must_use]
pub fn into_parts(self) -> (String, String) {
(self.namespace, self.reference)
}
}
impl fmt::Display for ChainId {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(f, "{}:{}", self.namespace, self.reference)
}
}
impl From<ChainId> for String {
fn from(value: ChainId) -> Self {
value.to_string()
}
}
#[derive(Debug, thiserror::Error)]
#[error("Invalid chain id format {0}")]
pub struct ChainIdFormatError(String);
impl FromStr for ChainId {
type Err = ChainIdFormatError;
fn from_str(s: &str) -> Result<Self, Self::Err> {
let (namespace, reference) = s
.split_once(':')
.ok_or_else(|| ChainIdFormatError(s.into()))?;
Ok(Self {
namespace: namespace.into(),
reference: reference.into(),
})
}
}
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)?;
Self::from_str(&s).map_err(de::Error::custom)
}
}
#[derive(Debug, Clone)]
#[non_exhaustive]
pub enum ChainIdPattern {
Wildcard {
namespace: String,
},
Exact {
namespace: String,
reference: String,
},
Set {
namespace: String,
references: HashSet<String>,
},
}
impl ChainIdPattern {
pub fn wildcard<S: Into<String>>(namespace: S) -> Self {
Self::Wildcard {
namespace: namespace.into(),
}
}
pub fn exact<N: Into<String>, R: Into<String>>(namespace: N, reference: R) -> Self {
Self::Exact {
namespace: namespace.into(),
reference: reference.into(),
}
}
pub fn set<N: Into<String>>(namespace: N, references: HashSet<String>) -> Self {
Self::Set {
namespace: namespace.into(),
references,
}
}
#[must_use]
pub fn matches(&self, chain_id: &ChainId) -> bool {
match self {
Self::Wildcard { namespace } => chain_id.namespace == *namespace,
Self::Exact {
namespace,
reference,
} => chain_id.namespace == *namespace && chain_id.reference == *reference,
Self::Set {
namespace,
references,
} => chain_id.namespace == *namespace && references.contains(&chain_id.reference),
}
}
#[must_use]
pub fn namespace(&self) -> &str {
match self {
Self::Wildcard { namespace }
| Self::Exact { namespace, .. }
| Self::Set { namespace, .. } => namespace,
}
}
}
impl fmt::Display for ChainIdPattern {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
Self::Wildcard { namespace } => write!(f, "{namespace}:*"),
Self::Exact {
namespace,
reference,
} => write!(f, "{namespace}:{reference}"),
Self::Set {
namespace,
references,
} => {
let refs: Vec<&str> = references.iter().map(AsRef::as_ref).collect();
write!(f, "{}:{{{}}}", namespace, refs.join(","))
}
}
}
}
impl FromStr for ChainIdPattern {
type Err = ChainIdFormatError;
fn from_str(s: &str) -> Result<Self, Self::Err> {
let (namespace, rest) = s
.split_once(':')
.ok_or_else(|| ChainIdFormatError(s.into()))?;
if namespace.is_empty() {
return Err(ChainIdFormatError(s.into()));
}
if rest == "*" {
return Ok(Self::wildcard(namespace));
}
if let Some(inner) = rest.strip_prefix('{').and_then(|r| r.strip_suffix('}')) {
let items: Vec<&str> = inner.split(',').map(str::trim).collect();
if items.is_empty() || items.iter().any(|item| item.is_empty()) {
return Err(ChainIdFormatError(s.into()));
}
let references = items.into_iter().map(Into::into).collect();
return Ok(Self::set(namespace, references));
}
if rest.is_empty() {
return Err(ChainIdFormatError(s.into()));
}
Ok(Self::exact(namespace, rest))
}
}
impl Serialize for ChainIdPattern {
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 ChainIdPattern {
fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
where
D: Deserializer<'de>,
{
let s = String::deserialize(deserializer)?;
Self::from_str(&s).map_err(de::Error::custom)
}
}
impl From<ChainId> for ChainIdPattern {
fn from(chain_id: ChainId) -> Self {
let (namespace, reference) = chain_id.into_parts();
Self::exact(namespace, reference)
}
}
pub trait ChainProvider {
fn signer_addresses(&self) -> Vec<String>;
fn chain_id(&self) -> ChainId;
}
impl<T: ChainProvider> ChainProvider for Arc<T> {
fn signer_addresses(&self) -> Vec<String> {
(**self).signer_addresses()
}
fn chain_id(&self) -> ChainId {
(**self).chain_id()
}
}
#[derive(Debug)]
pub struct ChainRegistry<P>(HashMap<ChainId, P>);
impl<P> ChainRegistry<P> {
#[must_use]
pub const fn new(providers: HashMap<ChainId, P>) -> Self {
Self(providers)
}
}
impl<P> ChainRegistry<P> {
#[must_use]
pub fn by_chain_id(&self, chain_id: &ChainId) -> Option<&P> {
self.0.get(chain_id)
}
#[must_use]
pub fn by_chain_id_pattern(&self, pattern: &ChainIdPattern) -> Vec<&P> {
self.0
.iter()
.filter_map(|(chain_id, provider)| pattern.matches(chain_id).then_some(provider))
.collect()
}
}
#[derive(Debug, Clone)]
pub struct DeployedTokenAmount<TAmount, TToken> {
pub amount: TAmount,
pub token: TToken,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct NetworkInfo {
pub name: &'static str,
pub namespace: &'static str,
pub reference: &'static str,
}
impl NetworkInfo {
#[must_use]
pub fn chain_id(&self) -> ChainId {
ChainId::new(self.namespace, self.reference)
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_chain_id_serialize_eip155() {
let chain_id = ChainId::new("eip155", "1");
let serialized = serde_json::to_string(&chain_id).unwrap();
assert_eq!(serialized, "\"eip155:1\"");
}
#[test]
fn test_chain_id_serialize_solana() {
let chain_id = ChainId::new("solana", "5eykt4UsFv8P8NJdTREpY1vzqKqZKvdp");
let serialized = serde_json::to_string(&chain_id).unwrap();
assert_eq!(serialized, "\"solana:5eykt4UsFv8P8NJdTREpY1vzqKqZKvdp\"");
}
#[test]
fn test_chain_id_deserialize_eip155() {
let chain_id: ChainId = serde_json::from_str("\"eip155:1\"").unwrap();
assert_eq!(chain_id.namespace(), "eip155");
assert_eq!(chain_id.reference(), "1");
}
#[test]
fn test_chain_id_deserialize_solana() {
let chain_id: ChainId =
serde_json::from_str("\"solana:5eykt4UsFv8P8NJdTREpY1vzqKqZKvdp\"").unwrap();
assert_eq!(chain_id.namespace(), "solana");
assert_eq!(chain_id.reference(), "5eykt4UsFv8P8NJdTREpY1vzqKqZKvdp");
}
#[test]
fn test_chain_id_roundtrip_eip155() {
let original = ChainId::new("eip155", "8453");
let serialized = serde_json::to_string(&original).unwrap();
let deserialized: ChainId = serde_json::from_str(&serialized).unwrap();
assert_eq!(original, deserialized);
}
#[test]
fn test_chain_id_roundtrip_solana() {
let original = ChainId::new("solana", "devnet");
let serialized = serde_json::to_string(&original).unwrap();
let deserialized: ChainId = serde_json::from_str(&serialized).unwrap();
assert_eq!(original, deserialized);
}
#[test]
fn test_chain_id_deserialize_invalid_format() {
let result: Result<ChainId, _> = serde_json::from_str("\"invalid\"");
assert!(result.is_err());
}
#[test]
fn test_chain_id_deserialize_unknown_namespace() {
let result: Result<ChainId, _> = serde_json::from_str("\"unknown:1\"");
assert!(result.is_ok());
}
#[test]
fn test_pattern_wildcard_matches() {
let pattern = ChainIdPattern::wildcard("eip155");
assert!(pattern.matches(&ChainId::new("eip155", "1")));
assert!(pattern.matches(&ChainId::new("eip155", "8453")));
assert!(pattern.matches(&ChainId::new("eip155", "137")));
assert!(!pattern.matches(&ChainId::new("solana", "mainnet")));
}
#[test]
fn test_pattern_exact_matches() {
let pattern = ChainIdPattern::exact("eip155", "1");
assert!(pattern.matches(&ChainId::new("eip155", "1")));
assert!(!pattern.matches(&ChainId::new("eip155", "8453")));
assert!(!pattern.matches(&ChainId::new("solana", "1")));
}
#[test]
fn test_pattern_set_matches() {
let references: HashSet<String> = vec!["1", "8453", "137"]
.into_iter()
.map(String::from)
.collect();
let pattern = ChainIdPattern::set("eip155", references);
assert!(pattern.matches(&ChainId::new("eip155", "1")));
assert!(pattern.matches(&ChainId::new("eip155", "8453")));
assert!(pattern.matches(&ChainId::new("eip155", "137")));
assert!(!pattern.matches(&ChainId::new("eip155", "42")));
assert!(!pattern.matches(&ChainId::new("solana", "1")));
}
#[test]
fn test_pattern_namespace() {
let wildcard = ChainIdPattern::wildcard("eip155");
assert_eq!(wildcard.namespace(), "eip155");
let exact = ChainIdPattern::exact("solana", "mainnet");
assert_eq!(exact.namespace(), "solana");
let references: HashSet<String> = vec!["1"].into_iter().map(String::from).collect();
let set = ChainIdPattern::set("eip155", references);
assert_eq!(set.namespace(), "eip155");
}
}