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#[derive(Debug, Default, Deserialize, Serialize, Clone, ToSchema)]
238#[serde(rename_all = "snake_case")]
239pub struct ChainConfigs {
240 #[serde(default)]
242 pub cosmos: BTreeMap<ChainKeyId, CosmosChainConfigBuilder>,
243 #[serde(default)]
245 pub evm: BTreeMap<ChainKeyId, EvmChainConfigBuilder>,
246 #[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}