1use crate::{
9 config::{EvmNetworkConfig, SolanaNetworkConfig, StellarNetworkConfig},
10 jobs::JobProducerTrait,
11 models::{
12 ApiError, ApiResponse, NetworkConfigData, NetworkRepoModel, NetworkResponse,
13 PaginationMeta, PaginationQuery, ThinDataAppState, UpdateNetworkRequest,
14 },
15 repositories::{
16 ApiKeyRepositoryTrait, NetworkRepository, PluginRepositoryTrait, RelayerRepository,
17 Repository, TransactionCounterTrait, TransactionRepository,
18 },
19};
20use actix_web::HttpResponse;
21use eyre::Result;
22
23pub async fn list_networks<J, RR, TR, NR, NFR, SR, TCR, PR, AKR>(
34 query: PaginationQuery,
35 state: ThinDataAppState<J, RR, TR, NR, NFR, SR, TCR, PR, AKR>,
36) -> Result<HttpResponse, ApiError>
37where
38 J: JobProducerTrait + Send + Sync + 'static,
39 RR: RelayerRepository
40 + Repository<crate::models::RelayerRepoModel, String>
41 + Send
42 + Sync
43 + 'static,
44 TR: TransactionRepository
45 + Repository<crate::models::TransactionRepoModel, String>
46 + Send
47 + Sync
48 + 'static,
49 NR: NetworkRepository + Repository<NetworkRepoModel, String> + Send + Sync + 'static,
50 NFR: Repository<crate::models::NotificationRepoModel, String> + Send + Sync + 'static,
51 SR: Repository<crate::models::SignerRepoModel, String> + Send + Sync + 'static,
52 TCR: TransactionCounterTrait + Send + Sync + 'static,
53 PR: PluginRepositoryTrait + Send + Sync + 'static,
54 AKR: ApiKeyRepositoryTrait + Send + Sync + 'static,
55{
56 let networks = state.network_repository.list_paginated(query).await?;
57
58 let mapped_networks: Vec<NetworkResponse> =
59 networks.items.into_iter().map(|n| n.into()).collect();
60
61 Ok(HttpResponse::Ok().json(ApiResponse::paginated(
62 mapped_networks,
63 PaginationMeta {
64 total_items: networks.total,
65 current_page: networks.page,
66 per_page: networks.per_page,
67 },
68 )))
69}
70
71pub async fn get_network<J, RR, TR, NR, NFR, SR, TCR, PR, AKR>(
82 network_id: String,
83 state: ThinDataAppState<J, RR, TR, NR, NFR, SR, TCR, PR, AKR>,
84) -> Result<HttpResponse, ApiError>
85where
86 J: JobProducerTrait + Send + Sync + 'static,
87 RR: RelayerRepository
88 + Repository<crate::models::RelayerRepoModel, String>
89 + Send
90 + Sync
91 + 'static,
92 TR: TransactionRepository
93 + Repository<crate::models::TransactionRepoModel, String>
94 + Send
95 + Sync
96 + 'static,
97 NR: NetworkRepository + Repository<NetworkRepoModel, String> + Send + Sync + 'static,
98 NFR: Repository<crate::models::NotificationRepoModel, String> + Send + Sync + 'static,
99 SR: Repository<crate::models::SignerRepoModel, String> + Send + Sync + 'static,
100 TCR: TransactionCounterTrait + Send + Sync + 'static,
101 PR: PluginRepositoryTrait + Send + Sync + 'static,
102 AKR: ApiKeyRepositoryTrait + Send + Sync + 'static,
103{
104 let network = state
105 .network_repository
106 .get_by_id(network_id.clone())
107 .await
108 .map_err(|e| match e {
109 crate::models::RepositoryError::NotFound(_) => {
110 ApiError::NotFound(format!("Network with ID '{network_id}' not found"))
111 }
112 _ => ApiError::InternalError(format!("Failed to retrieve network: {e}")),
113 })?;
114
115 let network_response: NetworkResponse = network.into();
116
117 Ok(HttpResponse::Ok().json(ApiResponse::success(network_response)))
118}
119
120pub async fn update_network<J, RR, TR, NR, NFR, SR, TCR, PR, AKR>(
133 network_id: String,
134 request: UpdateNetworkRequest,
135 state: ThinDataAppState<J, RR, TR, NR, NFR, SR, TCR, PR, AKR>,
136) -> Result<HttpResponse, ApiError>
137where
138 J: JobProducerTrait + Send + Sync + 'static,
139 RR: RelayerRepository
140 + Repository<crate::models::RelayerRepoModel, String>
141 + Send
142 + Sync
143 + 'static,
144 TR: TransactionRepository
145 + Repository<crate::models::TransactionRepoModel, String>
146 + Send
147 + Sync
148 + 'static,
149 NR: NetworkRepository + Repository<NetworkRepoModel, String> + Send + Sync + 'static,
150 NFR: Repository<crate::models::NotificationRepoModel, String> + Send + Sync + 'static,
151 SR: Repository<crate::models::SignerRepoModel, String> + Send + Sync + 'static,
152 TCR: TransactionCounterTrait + Send + Sync + 'static,
153 PR: PluginRepositoryTrait + Send + Sync + 'static,
154 AKR: ApiKeyRepositoryTrait + Send + Sync + 'static,
155{
156 request.validate()?;
158
159 if request.rpc_urls.is_none() {
161 return Err(ApiError::BadRequest(
162 "At least one field must be provided for update".to_string(),
163 ));
164 }
165
166 let mut network = state
168 .network_repository
169 .get_by_id(network_id.clone())
170 .await
171 .map_err(|e| match e {
172 crate::models::RepositoryError::NotFound(_) => {
173 ApiError::NotFound(format!("Network with ID '{network_id}' not found"))
174 }
175 _ => ApiError::InternalError(format!("Failed to retrieve network: {e}")),
176 })?;
177
178 let common = network.common();
180 let mut updated_common = common.clone();
181
182 if let Some(rpc_urls) = request.rpc_urls {
184 updated_common.rpc_urls = Some(rpc_urls);
185 }
186
187 let updated_config = match network.config {
189 NetworkConfigData::Evm(evm_config) => NetworkConfigData::Evm(EvmNetworkConfig {
190 common: updated_common,
191 chain_id: evm_config.chain_id,
192 required_confirmations: evm_config.required_confirmations,
193 features: evm_config.features,
194 symbol: evm_config.symbol,
195 gas_price_cache: evm_config.gas_price_cache,
196 }),
197 NetworkConfigData::Solana(_) => NetworkConfigData::Solana(SolanaNetworkConfig {
198 common: updated_common,
199 }),
200 NetworkConfigData::Stellar(stellar_config) => {
201 NetworkConfigData::Stellar(StellarNetworkConfig {
202 common: updated_common,
203 passphrase: stellar_config.passphrase,
204 horizon_url: stellar_config.horizon_url,
205 })
206 }
207 };
208
209 network.config = updated_config;
211
212 let saved_network = state
214 .network_repository
215 .update(network.id.clone(), network)
216 .await?;
217
218 let network_response: NetworkResponse = saved_network.into();
219
220 Ok(HttpResponse::Ok().json(ApiResponse::success(network_response)))
221}
222
223#[cfg(test)]
224mod tests {
225 use super::*;
226 use crate::{
227 config::{NetworkConfigCommon, StellarNetworkConfig},
228 models::{NetworkType, PaginationQuery, RpcConfig},
229 utils::mocks::mockutils::{
230 create_mock_app_state, create_mock_network, create_mock_solana_network,
231 },
232 };
233 use actix_web::web::ThinData;
234
235 fn create_mock_stellar_network() -> NetworkRepoModel {
237 NetworkRepoModel {
238 id: "stellar:testnet".to_string(),
239 name: "Stellar Testnet".to_string(),
240 network_type: NetworkType::Stellar,
241 config: NetworkConfigData::Stellar(StellarNetworkConfig {
242 common: NetworkConfigCommon {
243 network: "stellar-testnet".to_string(),
244 from: None,
245 rpc_urls: Some(vec![RpcConfig::new(
246 "https://soroban-testnet.stellar.org".to_string(),
247 )]),
248 explorer_urls: None,
249 average_blocktime_ms: Some(5000),
250 is_testnet: Some(true),
251 tags: None,
252 },
253 passphrase: Some("Test SDF Network ; September 2015".to_string()),
254 horizon_url: Some("https://horizon-testnet.stellar.org".to_string()),
255 }),
256 }
257 }
258
259 fn create_mock_evm_network_with_id(id: &str, name: &str) -> NetworkRepoModel {
261 let mut network = create_mock_network();
262 network.id = id.to_string();
263 network.name = name.to_string();
264 network
265 }
266
267 #[actix_web::test]
272 async fn test_list_networks_empty() {
273 let app_state = create_mock_app_state(None, None, None, None, None, None).await;
274 let query = PaginationQuery {
275 page: 1,
276 per_page: 10,
277 };
278
279 let result = list_networks(query, ThinData(app_state)).await;
280
281 assert!(result.is_ok());
282 let response = result.unwrap();
283 assert_eq!(response.status(), 200);
284 }
285
286 #[actix_web::test]
287 async fn test_list_networks_with_single_network() {
288 let network = create_mock_network();
289 let app_state =
290 create_mock_app_state(None, None, None, Some(vec![network]), None, None).await;
291 let query = PaginationQuery {
292 page: 1,
293 per_page: 10,
294 };
295
296 let result = list_networks(query, ThinData(app_state)).await;
297
298 assert!(result.is_ok());
299 let response = result.unwrap();
300 assert_eq!(response.status(), 200);
301 }
302
303 #[actix_web::test]
304 async fn test_list_networks_with_multiple_networks() {
305 let evm_network = create_mock_evm_network_with_id("evm:sepolia", "Sepolia");
306 let solana_network = create_mock_solana_network();
307 let stellar_network = create_mock_stellar_network();
308
309 let app_state = create_mock_app_state(
310 None,
311 None,
312 None,
313 Some(vec![evm_network, solana_network, stellar_network]),
314 None,
315 None,
316 )
317 .await;
318
319 let query = PaginationQuery {
320 page: 1,
321 per_page: 10,
322 };
323
324 let result = list_networks(query, ThinData(app_state)).await;
325
326 assert!(result.is_ok());
327 let response = result.unwrap();
328 assert_eq!(response.status(), 200);
329 }
330
331 #[actix_web::test]
332 async fn test_list_networks_pagination() {
333 let network1 = create_mock_evm_network_with_id("evm:network1", "Network 1");
334 let network2 = create_mock_evm_network_with_id("evm:network2", "Network 2");
335 let network3 = create_mock_evm_network_with_id("evm:network3", "Network 3");
336
337 let app_state = create_mock_app_state(
338 None,
339 None,
340 None,
341 Some(vec![network1, network2, network3]),
342 None,
343 None,
344 )
345 .await;
346
347 let query = PaginationQuery {
349 page: 1,
350 per_page: 2,
351 };
352
353 let result = list_networks(query, ThinData(app_state)).await;
354
355 assert!(result.is_ok());
356 let response = result.unwrap();
357 assert_eq!(response.status(), 200);
358 }
359
360 #[actix_web::test]
365 async fn test_get_network_success() {
366 let network = create_mock_network();
367 let network_id = network.id.clone();
368 let app_state =
369 create_mock_app_state(None, None, None, Some(vec![network]), None, None).await;
370
371 let result = get_network(network_id, ThinData(app_state)).await;
372
373 assert!(result.is_ok());
374 let response = result.unwrap();
375 assert_eq!(response.status(), 200);
376 }
377
378 #[actix_web::test]
379 async fn test_get_network_not_found() {
380 let app_state = create_mock_app_state(None, None, None, None, None, None).await;
381
382 let result = get_network("nonexistent-network".to_string(), ThinData(app_state)).await;
383
384 assert!(result.is_err());
385 let error = result.unwrap_err();
386 match error {
387 ApiError::NotFound(msg) => {
388 assert!(msg.contains("nonexistent-network"));
389 }
390 _ => panic!("Expected NotFound error, got {error:?}"),
391 }
392 }
393
394 #[actix_web::test]
395 async fn test_get_network_evm() {
396 let network = create_mock_network();
397 let network_id = network.id.clone();
398 let app_state =
399 create_mock_app_state(None, None, None, Some(vec![network]), None, None).await;
400
401 let result = get_network(network_id, ThinData(app_state)).await;
402
403 assert!(result.is_ok());
404 let response = result.unwrap();
405 assert_eq!(response.status(), 200);
406 }
407
408 #[actix_web::test]
409 async fn test_get_network_solana() {
410 let mut network = create_mock_solana_network();
411 network.id = "solana:devnet".to_string();
412 let network_id = network.id.clone();
413 let app_state =
414 create_mock_app_state(None, None, None, Some(vec![network]), None, None).await;
415
416 let result = get_network(network_id, ThinData(app_state)).await;
417
418 assert!(result.is_ok());
419 let response = result.unwrap();
420 assert_eq!(response.status(), 200);
421 }
422
423 #[actix_web::test]
424 async fn test_get_network_stellar() {
425 let network = create_mock_stellar_network();
426 let network_id = network.id.clone();
427 let app_state =
428 create_mock_app_state(None, None, None, Some(vec![network]), None, None).await;
429
430 let result = get_network(network_id, ThinData(app_state)).await;
431
432 assert!(result.is_ok());
433 let response = result.unwrap();
434 assert_eq!(response.status(), 200);
435 }
436
437 #[actix_web::test]
442 async fn test_update_network_evm_success() {
443 let network = create_mock_network();
444 let network_id = network.id.clone();
445 let app_state =
446 create_mock_app_state(None, None, None, Some(vec![network]), None, None).await;
447
448 let request = UpdateNetworkRequest {
449 rpc_urls: Some(vec![
450 RpcConfig::new("https://new-rpc1.example.com".to_string()),
451 RpcConfig::new("https://new-rpc2.example.com".to_string()),
452 ]),
453 };
454
455 let result = update_network(network_id, request, ThinData(app_state)).await;
456
457 assert!(result.is_ok());
458 let response = result.unwrap();
459 assert_eq!(response.status(), 200);
460 }
461
462 #[actix_web::test]
463 async fn test_update_network_solana_success() {
464 let mut network = create_mock_solana_network();
465 network.id = "solana:devnet".to_string();
466 let network_id = network.id.clone();
467 let app_state =
468 create_mock_app_state(None, None, None, Some(vec![network]), None, None).await;
469
470 let request = UpdateNetworkRequest {
471 rpc_urls: Some(vec![RpcConfig::new(
472 "https://api.devnet.solana.com".to_string(),
473 )]),
474 };
475
476 let result = update_network(network_id, request, ThinData(app_state)).await;
477
478 assert!(result.is_ok());
479 let response = result.unwrap();
480 assert_eq!(response.status(), 200);
481 }
482
483 #[actix_web::test]
484 async fn test_update_network_stellar_success() {
485 let network = create_mock_stellar_network();
486 let network_id = network.id.clone();
487 let app_state =
488 create_mock_app_state(None, None, None, Some(vec![network]), None, None).await;
489
490 let request = UpdateNetworkRequest {
491 rpc_urls: Some(vec![RpcConfig::new(
492 "https://new-soroban-testnet.stellar.org".to_string(),
493 )]),
494 };
495
496 let result = update_network(network_id, request, ThinData(app_state)).await;
497
498 assert!(result.is_ok());
499 let response = result.unwrap();
500 assert_eq!(response.status(), 200);
501 }
502
503 #[actix_web::test]
504 async fn test_update_network_not_found() {
505 let app_state = create_mock_app_state(None, None, None, None, None, None).await;
506
507 let request = UpdateNetworkRequest {
508 rpc_urls: Some(vec![RpcConfig::new("https://rpc.example.com".to_string())]),
509 };
510
511 let result = update_network(
512 "nonexistent-network".to_string(),
513 request,
514 ThinData(app_state),
515 )
516 .await;
517
518 assert!(result.is_err());
519 let error = result.unwrap_err();
520 match error {
521 ApiError::NotFound(msg) => {
522 assert!(msg.contains("nonexistent-network"));
523 }
524 _ => panic!("Expected NotFound error, got {error:?}"),
525 }
526 }
527
528 #[actix_web::test]
529 async fn test_update_network_no_fields_provided() {
530 let network = create_mock_network();
531 let network_id = network.id.clone();
532 let app_state =
533 create_mock_app_state(None, None, None, Some(vec![network]), None, None).await;
534
535 let request = UpdateNetworkRequest { rpc_urls: None };
536
537 let result = update_network(network_id, request, ThinData(app_state)).await;
538
539 assert!(result.is_err());
540 let error = result.unwrap_err();
541 match error {
542 ApiError::BadRequest(msg) => {
543 assert!(msg.contains("At least one field must be provided"));
544 }
545 _ => panic!("Expected BadRequest error, got {error:?}"),
546 }
547 }
548
549 #[actix_web::test]
550 async fn test_update_network_empty_rpc_urls() {
551 let network = create_mock_network();
552 let network_id = network.id.clone();
553 let app_state =
554 create_mock_app_state(None, None, None, Some(vec![network]), None, None).await;
555
556 let request = UpdateNetworkRequest {
557 rpc_urls: Some(vec![]),
558 };
559
560 let result = update_network(network_id, request, ThinData(app_state)).await;
561
562 assert!(result.is_err());
563 let error = result.unwrap_err();
564 match error {
565 ApiError::BadRequest(msg) => {
566 assert!(msg.contains("at least one RPC endpoint"));
567 }
568 _ => panic!("Expected BadRequest error, got {error:?}"),
569 }
570 }
571
572 #[actix_web::test]
573 async fn test_update_network_invalid_rpc_url() {
574 let network = create_mock_network();
575 let network_id = network.id.clone();
576 let app_state =
577 create_mock_app_state(None, None, None, Some(vec![network]), None, None).await;
578
579 let request = UpdateNetworkRequest {
580 rpc_urls: Some(vec![RpcConfig::new(
581 "ftp://invalid-protocol.com".to_string(),
582 )]),
583 };
584
585 let result = update_network(network_id, request, ThinData(app_state)).await;
586
587 assert!(result.is_err());
588 let error = result.unwrap_err();
589 match error {
590 ApiError::BadRequest(msg) => {
591 assert!(msg.contains("Invalid RPC URL"));
592 }
593 _ => panic!("Expected BadRequest error, got {error:?}"),
594 }
595 }
596
597 #[actix_web::test]
598 async fn test_update_network_with_weighted_rpc_urls() {
599 let network = create_mock_network();
600 let network_id = network.id.clone();
601 let app_state =
602 create_mock_app_state(None, None, None, Some(vec![network]), None, None).await;
603
604 let request = UpdateNetworkRequest {
605 rpc_urls: Some(vec![
606 RpcConfig {
607 url: "https://primary-rpc.example.com".to_string(),
608 weight: 80,
609 },
610 RpcConfig {
611 url: "https://backup-rpc.example.com".to_string(),
612 weight: 20,
613 },
614 ]),
615 };
616
617 let result = update_network(network_id, request, ThinData(app_state)).await;
618
619 assert!(result.is_ok());
620 let response = result.unwrap();
621 assert_eq!(response.status(), 200);
622 }
623
624 #[actix_web::test]
625 async fn test_update_network_preserves_other_evm_fields() {
626 let network = create_mock_network();
628 let network_id = network.id.clone();
629 let app_state =
630 create_mock_app_state(None, None, None, Some(vec![network]), None, None).await;
631
632 let request = UpdateNetworkRequest {
633 rpc_urls: Some(vec![RpcConfig::new(
634 "https://new-rpc.example.com".to_string(),
635 )]),
636 };
637
638 let result = update_network(network_id.clone(), request, ThinData(app_state.clone())).await;
639
640 assert!(result.is_ok());
641
642 let get_result = get_network(network_id, ThinData(app_state)).await;
644 assert!(get_result.is_ok());
645 }
646}