Skip to main content

wavs_types/
chain_config.rs

1use std::collections::BTreeMap;
2
3use layer_climb_address::AddrKind;
4use layer_climb_config::ChainConfig;
5use serde::{Deserialize, Serialize};
6use utoipa::ToSchema;
7
8use crate::{ChainKey, ChainKeyError, ChainKeyId, ChainKeyNamespace};
9
10#[derive(Debug, thiserror::Error, PartialEq, Eq)]
11pub enum ChainConfigError {
12    #[error("Expected EVM chain")]
13    ExpectedEvmChain,
14    #[error("Expected Cosmos chain")]
15    ExpectedCosmosChain,
16    #[error("Invalid chain: {0}")]
17    InvalidChainKey(#[from] ChainKeyError),
18    #[error("Chain already exists: {0}")]
19    DuplicateChain(ChainKey),
20    #[error("Namespace for cosmos chain must be {cosmos} or {dev}, got {0}", cosmos=ChainKeyNamespace::COSMOS, dev=ChainKeyNamespace::DEV)]
21    InvalidNamespaceForCosmos(ChainKeyNamespace),
22    #[error("Namespace for cosmos chain must be {evm} or {dev}, got {0}", evm=ChainKeyNamespace::EVM, dev=ChainKeyNamespace::DEV)]
23    InvalidNamespaceForEvm(ChainKeyNamespace),
24    #[error("Namespace must be one of {cosmos}, {evm}, or {dev}, got {0}", cosmos=ChainKeyNamespace::COSMOS, evm=ChainKeyNamespace::EVM, dev=ChainKeyNamespace::DEV)]
25    InvalidNamespace(ChainKeyNamespace),
26    #[error("Chain ID mismatch: expected {expected}, got {actual}")]
27    IdMismatch {
28        expected: ChainKeyId,
29        actual: ChainKeyId,
30    },
31}
32
33#[derive(Debug, Deserialize, Serialize, Clone, ToSchema)]
34pub struct CosmosChainConfig {
35    pub chain_id: ChainKeyId,
36    pub bech32_prefix: String,
37    pub rpc_endpoint: Option<String>,
38    pub grpc_endpoint: Option<String>,
39    pub gas_price: f32,
40    pub gas_denom: String,
41    pub faucet_endpoint: Option<String>,
42}
43
44impl From<&CosmosChainConfig> for ChainKey {
45    fn from(config: &CosmosChainConfig) -> Self {
46        ChainKey {
47            id: config.chain_id.clone(),
48            namespace: ChainKeyNamespace::COSMOS.parse().unwrap(),
49        }
50    }
51}
52
53impl From<CosmosChainConfig> for ChainKey {
54    fn from(config: CosmosChainConfig) -> Self {
55        (&config).into()
56    }
57}
58
59#[derive(Debug, Deserialize, Serialize, Clone, ToSchema)]
60pub struct EvmChainConfig {
61    pub chain_id: ChainKeyId,
62    pub ws_endpoints: Vec<String>,
63    pub http_endpoint: Option<String>,
64    pub faucet_endpoint: Option<String>,
65    pub ws_priority_endpoint_index: Option<usize>,
66}
67
68impl From<&EvmChainConfig> for ChainKey {
69    fn from(config: &EvmChainConfig) -> Self {
70        ChainKey {
71            id: config.chain_id.clone(),
72            namespace: ChainKeyNamespace::EVM.parse().unwrap(),
73        }
74    }
75}
76
77impl From<EvmChainConfig> for ChainKey {
78    fn from(config: EvmChainConfig) -> Self {
79        (&config).into()
80    }
81}
82
83#[derive(Debug, Deserialize, Serialize, Clone, ToSchema)]
84#[serde(tag = "type", rename_all = "snake_case")]
85pub enum AnyChainConfig {
86    Cosmos(CosmosChainConfig),
87    Evm(EvmChainConfig),
88}
89
90impl From<&AnyChainConfig> for ChainKey {
91    fn from(config: &AnyChainConfig) -> Self {
92        match config {
93            AnyChainConfig::Cosmos(config) => config.into(),
94            AnyChainConfig::Evm(config) => config.into(),
95        }
96    }
97}
98
99impl From<AnyChainConfig> for ChainKey {
100    fn from(config: AnyChainConfig) -> Self {
101        (&config).into()
102    }
103}
104
105impl AnyChainConfig {
106    pub fn chain_id(&self) -> &ChainKeyId {
107        match self {
108            AnyChainConfig::Cosmos(config) => &config.chain_id,
109            AnyChainConfig::Evm(config) => &config.chain_id,
110        }
111    }
112}
113
114impl From<CosmosChainConfig> for AnyChainConfig {
115    fn from(config: CosmosChainConfig) -> Self {
116        AnyChainConfig::Cosmos(config)
117    }
118}
119
120impl From<EvmChainConfig> for AnyChainConfig {
121    fn from(config: EvmChainConfig) -> Self {
122        AnyChainConfig::Evm(config)
123    }
124}
125
126impl TryFrom<AnyChainConfig> for CosmosChainConfig {
127    type Error = ChainConfigError;
128
129    fn try_from(config: AnyChainConfig) -> Result<Self, Self::Error> {
130        match config {
131            AnyChainConfig::Cosmos(config) => Ok(config),
132            AnyChainConfig::Evm(_) => Err(ChainConfigError::ExpectedCosmosChain),
133        }
134    }
135}
136
137impl TryFrom<AnyChainConfig> for EvmChainConfig {
138    type Error = ChainConfigError;
139
140    fn try_from(config: AnyChainConfig) -> Result<Self, Self::Error> {
141        match config {
142            AnyChainConfig::Evm(config) => Ok(config),
143            AnyChainConfig::Cosmos(_) => Err(ChainConfigError::ExpectedEvmChain),
144        }
145    }
146}
147
148impl From<CosmosChainConfig> for ChainConfig {
149    fn from(config: CosmosChainConfig) -> Self {
150        Self {
151            chain_id: layer_climb_config::ChainId::new(config.chain_id),
152            rpc_endpoint: config.rpc_endpoint,
153            grpc_endpoint: config.grpc_endpoint,
154            grpc_web_endpoint: None,
155            gas_price: config.gas_price,
156            gas_denom: config.gas_denom,
157            address_kind: AddrKind::Cosmos {
158                prefix: config.bech32_prefix,
159            },
160        }
161    }
162}
163
164impl TryFrom<ChainConfig> for CosmosChainConfig {
165    type Error = ChainConfigError;
166
167    fn try_from(config: ChainConfig) -> Result<Self, Self::Error> {
168        Ok(Self {
169            chain_id: config.chain_id.as_str().parse()?,
170            bech32_prefix: match config.address_kind {
171                AddrKind::Cosmos { prefix } => prefix,
172                _ => return Err(ChainConfigError::ExpectedCosmosChain),
173            },
174            rpc_endpoint: config.rpc_endpoint,
175            grpc_endpoint: config.grpc_endpoint,
176            gas_price: config.gas_price,
177            gas_denom: config.gas_denom,
178            faucet_endpoint: None,
179        })
180    }
181}
182
183impl CosmosChainConfig {
184    pub fn to_chain_config(&self) -> ChainConfig {
185        self.clone().into()
186    }
187
188    pub fn from_chain_config(config: ChainConfig) -> Result<Self, ChainConfigError> {
189        config.try_into()
190    }
191}
192
193impl TryFrom<AnyChainConfig> for ChainConfig {
194    type Error = ChainConfigError;
195
196    fn try_from(config: AnyChainConfig) -> Result<Self, Self::Error> {
197        CosmosChainConfig::try_from(config).map(Into::into)
198    }
199}
200
201impl TryFrom<ChainConfig> for AnyChainConfig {
202    type Error = ChainConfigError;
203
204    fn try_from(config: ChainConfig) -> Result<Self, Self::Error> {
205        Ok(CosmosChainConfig::try_from(config)?.into())
206    }
207}
208
209impl AnyChainConfig {
210    pub fn to_cosmos_config(&self) -> Result<CosmosChainConfig, ChainConfigError> {
211        match self {
212            AnyChainConfig::Cosmos(config) => Ok(config.clone()),
213            AnyChainConfig::Evm(_) => Err(ChainConfigError::ExpectedCosmosChain),
214        }
215    }
216
217    pub fn to_evm_config(&self) -> Result<EvmChainConfig, ChainConfigError> {
218        match self {
219            AnyChainConfig::Evm(config) => Ok(config.clone()),
220            AnyChainConfig::Cosmos(_) => Err(ChainConfigError::ExpectedEvmChain),
221        }
222    }
223
224    pub fn to_layer_climb_config(&self) -> Result<ChainConfig, ChainConfigError> {
225        let cosmos_config = self.to_cosmos_config()?;
226        Ok(cosmos_config.to_chain_config())
227    }
228
229    pub fn from_layer_climb_config(config: ChainConfig) -> Result<Self, ChainConfigError> {
230        let cosmos_config = CosmosChainConfig::from_chain_config(config)?;
231        Ok(AnyChainConfig::Cosmos(cosmos_config))
232    }
233}
234
235/// Chains are identified by `ChainKey`, which is a combination of a namespace and id
236/// for now - we natively support 3 namespaces: cosmos, evm, and dev
237#[derive(Debug, Default, Deserialize, Serialize, Clone, ToSchema)]
238#[serde(rename_all = "snake_case")]
239pub struct ChainConfigs {
240    /// Cosmos-style chains (including Layer-SDK)
241    #[serde(default)]
242    pub cosmos: BTreeMap<ChainKeyId, CosmosChainConfigBuilder>,
243    /// EVM-style chains
244    #[serde(default)]
245    pub evm: BTreeMap<ChainKeyId, EvmChainConfigBuilder>,
246    /// DEV-only chains
247    /// The key here can be different than the chain_id inside the config
248    #[serde(default)]
249    pub dev: BTreeMap<ChainKeyId, AnyChainConfig>,
250}
251
252impl ChainConfigs {
253    pub fn get_chain(&self, key: &ChainKey) -> Option<AnyChainConfig> {
254        match key.namespace.as_str() {
255            ChainKeyNamespace::COSMOS => self
256                .cosmos
257                .get(&key.id)
258                .map(|c| AnyChainConfig::Cosmos(c.clone().build(key.id.clone()))),
259            ChainKeyNamespace::EVM => self
260                .evm
261                .get(&key.id)
262                .map(|c| AnyChainConfig::Evm(c.clone().build(key.id.clone()))),
263            ChainKeyNamespace::DEV => self.dev.get(&key.id).cloned(),
264            _ => None,
265        }
266    }
267
268    pub fn cosmos_iter(&self) -> impl Iterator<Item = CosmosChainConfig> + '_ {
269        self.cosmos
270            .iter()
271            .map(|(id, config)| config.clone().build(id.clone()))
272    }
273
274    pub fn evm_iter(&self) -> impl Iterator<Item = EvmChainConfig> + '_ {
275        self.evm
276            .iter()
277            .map(|(id, config)| config.clone().build(id.clone()))
278    }
279
280    pub fn dev_iter(&self) -> impl Iterator<Item = AnyChainConfig> + '_ {
281        self.dev.values().cloned()
282    }
283
284    pub fn all_chain_keys(&self) -> Result<Vec<ChainKey>, anyhow::Error> {
285        let mut keys = Vec::new();
286
287        for id in self.evm.keys() {
288            keys.push(ChainKey {
289                namespace: ChainKeyNamespace::EVM.parse()?,
290                id: id.clone(),
291            });
292        }
293        for id in self.cosmos.keys() {
294            keys.push(ChainKey {
295                namespace: ChainKeyNamespace::COSMOS.parse()?,
296                id: id.clone(),
297            });
298        }
299        for id in self.dev.keys() {
300            keys.push(ChainKey {
301                namespace: ChainKeyNamespace::DEV.parse()?,
302                id: id.clone(),
303            });
304        }
305
306        Ok(keys)
307    }
308
309    pub fn chain_keys(&self, namespace: ChainKeyNamespace) -> Vec<ChainKey> {
310        match namespace.as_str() {
311            ChainKeyNamespace::COSMOS => self
312                .cosmos
313                .keys()
314                .map(|id| ChainKey {
315                    namespace: namespace.clone(),
316                    id: id.clone(),
317                })
318                .collect(),
319            ChainKeyNamespace::EVM => self
320                .evm
321                .keys()
322                .map(|id| ChainKey {
323                    namespace: namespace.clone(),
324                    id: id.clone(),
325                })
326                .collect(),
327            ChainKeyNamespace::DEV => self
328                .dev
329                .keys()
330                .map(|id| ChainKey {
331                    namespace: namespace.clone(),
332                    id: id.clone(),
333                })
334                .collect(),
335            _ => Vec::new(),
336        }
337    }
338
339    pub fn add_chain(
340        &mut self,
341        key: ChainKey,
342        config: AnyChainConfig,
343    ) -> Result<(), ChainConfigError> {
344        if self.get_chain(&key).is_some() {
345            return Err(ChainConfigError::DuplicateChain(key));
346        }
347
348        match key.namespace.as_str() {
349            ChainKeyNamespace::DEV => {
350                self.dev.insert(key.id, config);
351            }
352            ChainKeyNamespace::EVM => match config {
353                AnyChainConfig::Evm(evm_config) => {
354                    if evm_config.chain_id != key.id {
355                        return Err(ChainConfigError::IdMismatch {
356                            expected: key.id,
357                            actual: evm_config.chain_id,
358                        });
359                    }
360                    let evm_config = EvmChainConfigBuilder {
361                        ws_endpoints: evm_config.ws_endpoints,
362                        http_endpoint: evm_config.http_endpoint,
363                        faucet_endpoint: evm_config.faucet_endpoint,
364                        ws_priority_endpoint_index: evm_config.ws_priority_endpoint_index,
365                    };
366                    self.evm.insert(key.id, evm_config);
367                }
368                _ => return Err(ChainConfigError::InvalidNamespaceForEvm(key.namespace)),
369            },
370            ChainKeyNamespace::COSMOS => match config {
371                AnyChainConfig::Cosmos(cosmos_config) => {
372                    if cosmos_config.chain_id != key.id {
373                        return Err(ChainConfigError::IdMismatch {
374                            expected: key.id,
375                            actual: cosmos_config.chain_id,
376                        });
377                    }
378                    let cosmos_config = CosmosChainConfigBuilder {
379                        bech32_prefix: cosmos_config.bech32_prefix,
380                        rpc_endpoint: cosmos_config.rpc_endpoint,
381                        grpc_endpoint: cosmos_config.grpc_endpoint,
382                        gas_price: cosmos_config.gas_price,
383                        gas_denom: cosmos_config.gas_denom,
384                        faucet_endpoint: cosmos_config.faucet_endpoint,
385                    };
386                    self.cosmos.insert(key.id, cosmos_config);
387                }
388                _ => return Err(ChainConfigError::InvalidNamespaceForCosmos(key.namespace)),
389            },
390            _ => return Err(ChainConfigError::InvalidNamespace(key.namespace)),
391        }
392
393        Ok(())
394    }
395}
396
397#[derive(Debug, Deserialize, Serialize, Clone, ToSchema)]
398pub struct CosmosChainConfigBuilder {
399    pub bech32_prefix: String,
400    pub rpc_endpoint: Option<String>,
401    pub grpc_endpoint: Option<String>,
402    pub gas_price: f32,
403    pub gas_denom: String,
404    pub faucet_endpoint: Option<String>,
405}
406
407impl CosmosChainConfigBuilder {
408    pub fn build(self, id: ChainKeyId) -> CosmosChainConfig {
409        CosmosChainConfig {
410            chain_id: id,
411            bech32_prefix: self.bech32_prefix,
412            rpc_endpoint: self.rpc_endpoint,
413            grpc_endpoint: self.grpc_endpoint,
414            gas_price: self.gas_price,
415            gas_denom: self.gas_denom,
416            faucet_endpoint: self.faucet_endpoint,
417        }
418    }
419}
420
421#[derive(Debug, Deserialize, Serialize, Clone, ToSchema)]
422pub struct EvmChainConfigBuilder {
423    pub ws_endpoints: Vec<String>,
424    pub http_endpoint: Option<String>,
425    pub faucet_endpoint: Option<String>,
426    pub ws_priority_endpoint_index: Option<usize>,
427}
428
429impl EvmChainConfigBuilder {
430    pub fn build(self, id: ChainKeyId) -> EvmChainConfig {
431        EvmChainConfig {
432            chain_id: id,
433            ws_endpoints: self.ws_endpoints,
434            http_endpoint: self.http_endpoint,
435            faucet_endpoint: self.faucet_endpoint,
436            ws_priority_endpoint_index: self.ws_priority_endpoint_index,
437        }
438    }
439}