1use std::path::Path;
4
5use crate::{
6 config::{Config, SignerFileConfig, SignerFileConfigEnum},
7 jobs::JobProducerTrait,
8 models::{
9 AppState, AwsKmsSignerConfig, GoogleCloudKmsSignerConfig, GoogleCloudKmsSignerKeyConfig,
10 GoogleCloudKmsSignerServiceAccountConfig, LocalSignerConfig, NetworkRepoModel,
11 NotificationRepoModel, PluginModel, RelayerRepoModel, SignerConfig, SignerRepoModel,
12 TurnkeySignerConfig, VaultTransitSignerConfig,
13 },
14 repositories::{PluginRepositoryTrait, Repository},
15 services::{Signer, SignerFactory, VaultConfig, VaultService, VaultServiceTrait},
16 utils::unsafe_generate_random_private_key,
17};
18use actix_web::web::ThinData;
19use color_eyre::{eyre::WrapErr, Report, Result};
20use futures::future::try_join_all;
21use oz_keystore::{HashicorpCloudClient, LocalClient};
22use secrets::SecretVec;
23use zeroize::Zeroizing;
24
25async fn process_plugins<J: JobProducerTrait>(
27 config_file: &Config,
28 app_state: &ThinData<AppState<J>>,
29) -> Result<()> {
30 if let Some(plugins) = &config_file.plugins {
31 let plugin_futures = plugins.iter().map(|plugin| async {
32 let plugin_model = PluginModel::try_from(plugin.clone())
33 .wrap_err("Failed to convert plugin config")?;
34 app_state
35 .plugin_repository
36 .add(plugin_model)
37 .await
38 .wrap_err("Failed to create plugin repository entry")?;
39 Ok::<(), Report>(())
40 });
41
42 try_join_all(plugin_futures)
43 .await
44 .wrap_err("Failed to initialize plugin repository")?;
45 Ok(())
46 } else {
47 Ok(())
48 }
49}
50
51async fn process_signer(signer: &SignerFileConfig) -> Result<SignerRepoModel> {
61 let signer_repo_model = match &signer.config {
62 SignerFileConfigEnum::Test(_) => SignerRepoModel {
63 id: signer.id.clone(),
64 config: SignerConfig::Test(LocalSignerConfig {
65 raw_key: SecretVec::new(32, |b| {
66 b.copy_from_slice(&unsafe_generate_random_private_key())
67 }),
68 }),
69 },
70 SignerFileConfigEnum::Local(local_signer) => {
71 let passphrase = local_signer.passphrase.get_value()?;
72
73 let raw_key = SecretVec::new(32, |buffer| {
74 let loaded = LocalClient::load(
75 Path::new(&local_signer.path).to_path_buf(),
76 passphrase.to_str().as_str().to_string(),
77 );
78
79 buffer.copy_from_slice(&loaded);
80 });
81 SignerRepoModel {
82 id: signer.id.clone(),
83 config: SignerConfig::Local(LocalSignerConfig { raw_key }),
84 }
85 }
86 SignerFileConfigEnum::AwsKms(aws_kms_config) => SignerRepoModel {
87 id: signer.id.clone(),
88 config: SignerConfig::AwsKms(AwsKmsSignerConfig {
89 region: aws_kms_config.region.clone(),
90 key_id: aws_kms_config.key_id.clone(),
91 }),
92 },
93 SignerFileConfigEnum::Vault(vault_config) => {
94 let config = VaultConfig {
95 address: vault_config.address.clone(),
96 namespace: vault_config.namespace.clone(),
97 role_id: vault_config.role_id.get_value()?,
98 secret_id: vault_config.secret_id.get_value()?,
99 mount_path: vault_config
100 .mount_point
101 .clone()
102 .unwrap_or("secret".to_string()),
103 token_ttl: None,
104 };
105
106 let vault_service = VaultService::new(config);
107
108 let raw_key = {
109 let hex_secret = Zeroizing::new(
110 vault_service
111 .retrieve_secret(&vault_config.key_name)
112 .await?,
113 );
114 let decoded_bytes = hex::decode(hex_secret)
115 .map_err(|e| eyre::eyre!("Invalid hex in vault cloud secret: {}", e))?;
116
117 SecretVec::new(decoded_bytes.len(), |buffer| {
118 buffer.copy_from_slice(&decoded_bytes);
119 })
120 };
121
122 SignerRepoModel {
123 id: signer.id.clone(),
124 config: SignerConfig::Vault(LocalSignerConfig { raw_key }),
125 }
126 }
127 SignerFileConfigEnum::VaultCloud(vault_cloud_config) => {
128 let client = HashicorpCloudClient::new(
129 vault_cloud_config.client_id.clone(),
130 vault_cloud_config
131 .client_secret
132 .get_value()?
133 .to_str()
134 .to_string(),
135 vault_cloud_config.org_id.clone(),
136 vault_cloud_config.project_id.clone(),
137 vault_cloud_config.app_name.clone(),
138 );
139
140 let raw_key = {
141 let response = client.get_secret(&vault_cloud_config.key_name).await?;
142 let hex_secret = Zeroizing::new(response.secret.static_version.value.clone());
143
144 let decoded_bytes = hex::decode(hex_secret)
145 .map_err(|e| eyre::eyre!("Invalid hex in vault cloud secret: {}", e))?;
146
147 SecretVec::new(decoded_bytes.len(), |buffer| {
148 buffer.copy_from_slice(&decoded_bytes);
149 })
150 };
151
152 SignerRepoModel {
153 id: signer.id.clone(),
154 config: SignerConfig::Vault(LocalSignerConfig { raw_key }),
155 }
156 }
157 SignerFileConfigEnum::VaultTransit(vault_transit_config) => SignerRepoModel {
158 id: signer.id.clone(),
159 config: SignerConfig::VaultTransit(VaultTransitSignerConfig {
160 key_name: vault_transit_config.key_name.clone(),
161 address: vault_transit_config.address.clone(),
162 namespace: vault_transit_config.namespace.clone(),
163 role_id: vault_transit_config.role_id.get_value()?,
164 secret_id: vault_transit_config.secret_id.get_value()?,
165 pubkey: vault_transit_config.pubkey.clone(),
166 mount_point: vault_transit_config.mount_point.clone(),
167 }),
168 },
169 SignerFileConfigEnum::Turnkey(turnkey_config) => SignerRepoModel {
170 id: signer.id.clone(),
171 config: SignerConfig::Turnkey(TurnkeySignerConfig {
172 private_key_id: turnkey_config.private_key_id.clone(),
173 organization_id: turnkey_config.organization_id.clone(),
174 public_key: turnkey_config.public_key.clone(),
175 api_private_key: turnkey_config.api_private_key.get_value()?,
176 api_public_key: turnkey_config.api_public_key.clone(),
177 }),
178 },
179 SignerFileConfigEnum::GoogleCloudKms(google_cloud_kms_config) => SignerRepoModel {
180 id: signer.id.clone(),
181 config: SignerConfig::GoogleCloudKms(GoogleCloudKmsSignerConfig {
182 service_account: GoogleCloudKmsSignerServiceAccountConfig {
183 private_key: google_cloud_kms_config
184 .service_account
185 .private_key
186 .get_value()?,
187 client_email: google_cloud_kms_config
188 .service_account
189 .client_email
190 .get_value()?,
191 private_key_id: google_cloud_kms_config
192 .service_account
193 .private_key_id
194 .get_value()?,
195 client_id: google_cloud_kms_config.service_account.client_id.clone(),
196 project_id: google_cloud_kms_config.service_account.project_id.clone(),
197 auth_uri: google_cloud_kms_config.service_account.auth_uri.clone(),
198 token_uri: google_cloud_kms_config.service_account.token_uri.clone(),
199 client_x509_cert_url: google_cloud_kms_config
200 .service_account
201 .client_x509_cert_url
202 .clone(),
203 auth_provider_x509_cert_url: google_cloud_kms_config
204 .service_account
205 .auth_provider_x509_cert_url
206 .clone(),
207 universe_domain: google_cloud_kms_config
208 .service_account
209 .universe_domain
210 .clone(),
211 },
212 key: GoogleCloudKmsSignerKeyConfig {
213 location: google_cloud_kms_config.key.location.clone(),
214 key_id: google_cloud_kms_config.key.key_id.clone(),
215 key_ring_id: google_cloud_kms_config.key.key_ring_id.clone(),
216 key_version: google_cloud_kms_config.key.key_version,
217 },
218 }),
219 },
220 };
221
222 Ok(signer_repo_model)
223}
224
225async fn process_signers<J: JobProducerTrait>(
233 config_file: &Config,
234 app_state: &ThinData<AppState<J>>,
235) -> Result<()> {
236 let signer_futures = config_file.signers.iter().map(|signer| async {
237 let signer_repo_model = process_signer(signer).await?;
238
239 app_state
240 .signer_repository
241 .create(signer_repo_model)
242 .await
243 .wrap_err("Failed to create signer repository entry")?;
244 Ok::<(), Report>(())
245 });
246
247 try_join_all(signer_futures)
248 .await
249 .wrap_err("Failed to initialize signer repository")?;
250 Ok(())
251}
252
253async fn process_notifications<J: JobProducerTrait>(
261 config_file: &Config,
262 app_state: &ThinData<AppState<J>>,
263) -> Result<()> {
264 let notification_futures = config_file.notifications.iter().map(|notification| async {
265 let notification_repo_model = NotificationRepoModel::try_from(notification.clone())
266 .wrap_err("Failed to convert notification config")?;
267
268 app_state
269 .notification_repository
270 .create(notification_repo_model)
271 .await
272 .wrap_err("Failed to create notification repository entry")?;
273 Ok::<(), Report>(())
274 });
275
276 try_join_all(notification_futures)
277 .await
278 .wrap_err("Failed to initialize notification repository")?;
279 Ok(())
280}
281
282async fn process_networks<J: JobProducerTrait>(
290 config_file: &Config,
291 app_state: &ThinData<AppState<J>>,
292) -> Result<()> {
293 let network_futures = config_file.networks.iter().map(|network| async move {
294 let network_repo_model = NetworkRepoModel::try_from(network.clone())?;
295
296 app_state
297 .network_repository
298 .create(network_repo_model)
299 .await
300 .wrap_err("Failed to create network repository entry")?;
301 Ok::<(), Report>(())
302 });
303
304 try_join_all(network_futures)
305 .await
306 .wrap_err("Failed to initialize network repository")?;
307 Ok(())
308}
309
310async fn process_relayers<J: JobProducerTrait>(
321 config_file: &Config,
322 app_state: &ThinData<AppState<J>>,
323) -> Result<()> {
324 let signers = app_state.signer_repository.list_all().await?;
325
326 let relayer_futures = config_file.relayers.iter().map(|relayer| async {
327 let mut repo_model = RelayerRepoModel::try_from(relayer.clone())
328 .wrap_err("Failed to convert relayer config")?;
329 let signer_model = signers
330 .iter()
331 .find(|s| s.id == repo_model.signer_id)
332 .ok_or_else(|| eyre::eyre!("Signer not found"))?;
333 let network_type = repo_model.network_type;
334 let signer_service = SignerFactory::create_signer(&network_type, signer_model)
335 .await
336 .wrap_err("Failed to create signer service")?;
337
338 let address = signer_service.address().await?;
339 repo_model.address = address.to_string();
340
341 app_state
342 .relayer_repository
343 .create(repo_model)
344 .await
345 .wrap_err("Failed to create relayer repository entry")?;
346 Ok::<(), Report>(())
347 });
348
349 try_join_all(relayer_futures)
350 .await
351 .wrap_err("Failed to initialize relayer repository")?;
352 Ok(())
353}
354
355pub async fn process_config_file<J: JobProducerTrait>(
363 config_file: Config,
364 app_state: ThinData<AppState<J>>,
365) -> Result<()> {
366 process_plugins(&config_file, &app_state).await?;
367 process_signers(&config_file, &app_state).await?;
368 process_notifications(&config_file, &app_state).await?;
369 process_networks(&config_file, &app_state).await?;
370 process_relayers(&config_file, &app_state).await?;
371 Ok(())
372}
373
374#[cfg(test)]
375mod tests {
376 use super::*;
377 use crate::{
378 config::{
379 AwsKmsSignerFileConfig, ConfigFileNetworkType, GoogleCloudKmsSignerFileConfig,
380 KmsKeyConfig, NetworksFileConfig, NotificationFileConfig, PluginFileConfig,
381 RelayerFileConfig, ServiceAccountConfig, TestSignerFileConfig, VaultSignerFileConfig,
382 VaultTransitSignerFileConfig,
383 },
384 jobs::MockJobProducerTrait,
385 models::{NetworkType, PlainOrEnvValue, SecretString},
386 repositories::{
387 InMemoryNetworkRepository, InMemoryNotificationRepository, InMemoryPluginRepository,
388 InMemoryRelayerRepository, InMemorySignerRepository, InMemoryTransactionCounter,
389 InMemoryTransactionRepository, RelayerRepositoryStorage,
390 },
391 };
392 use serde_json::json;
393 use std::sync::Arc;
394 use wiremock::matchers::{body_json, header, method, path};
395 use wiremock::{Mock, MockServer, ResponseTemplate};
396
397 fn create_test_app_state() -> AppState<MockJobProducerTrait> {
398 let mut mock_job_producer = MockJobProducerTrait::new();
400
401 mock_job_producer
403 .expect_produce_transaction_request_job()
404 .returning(|_, _| Box::pin(async { Ok(()) }));
405
406 mock_job_producer
407 .expect_produce_submit_transaction_job()
408 .returning(|_, _| Box::pin(async { Ok(()) }));
409
410 mock_job_producer
411 .expect_produce_check_transaction_status_job()
412 .returning(|_, _| Box::pin(async { Ok(()) }));
413
414 mock_job_producer
415 .expect_produce_send_notification_job()
416 .returning(|_, _| Box::pin(async { Ok(()) }));
417
418 AppState {
419 relayer_repository: Arc::new(RelayerRepositoryStorage::in_memory(
420 InMemoryRelayerRepository::default(),
421 )),
422 transaction_repository: Arc::new(InMemoryTransactionRepository::default()),
423 signer_repository: Arc::new(InMemorySignerRepository::default()),
424 notification_repository: Arc::new(InMemoryNotificationRepository::default()),
425 network_repository: Arc::new(InMemoryNetworkRepository::default()),
426 transaction_counter_store: Arc::new(InMemoryTransactionCounter::default()),
427 job_producer: Arc::new(mock_job_producer),
428 plugin_repository: Arc::new(InMemoryPluginRepository::default()),
429 }
430 }
431
432 #[tokio::test]
433 async fn test_process_signer_test() {
434 let signer = SignerFileConfig {
435 id: "test-signer".to_string(),
436 config: SignerFileConfigEnum::Test(TestSignerFileConfig {}),
437 };
438
439 let result = process_signer(&signer).await;
440
441 assert!(
442 result.is_ok(),
443 "Failed to process test signer: {:?}",
444 result.err()
445 );
446 let model = result.unwrap();
447
448 assert_eq!(model.id, "test-signer");
449
450 match model.config {
451 SignerConfig::Test(config) => {
452 assert!(!config.raw_key.is_empty());
453 assert_eq!(config.raw_key.len(), 32);
454 }
455 _ => panic!("Expected Test config"),
456 }
457 }
458
459 #[tokio::test]
460 async fn test_process_signer_vault_transit() -> Result<()> {
461 let signer = SignerFileConfig {
462 id: "vault-transit-signer".to_string(),
463 config: SignerFileConfigEnum::VaultTransit(VaultTransitSignerFileConfig {
464 key_name: "test-transit-key".to_string(),
465 address: "https://vault.example.com".to_string(),
466 namespace: Some("test-namespace".to_string()),
467 role_id: PlainOrEnvValue::Plain {
468 value: SecretString::new("test-role"),
469 },
470 secret_id: PlainOrEnvValue::Plain {
471 value: SecretString::new("test-secret"),
472 },
473 pubkey: "test-pubkey".to_string(),
474 mount_point: Some("transit".to_string()),
475 }),
476 };
477
478 let result = process_signer(&signer).await;
479
480 assert!(
481 result.is_ok(),
482 "Failed to process vault transit signer: {:?}",
483 result.err()
484 );
485 let model = result.unwrap();
486
487 assert_eq!(model.id, "vault-transit-signer");
488
489 match model.config {
490 SignerConfig::VaultTransit(config) => {
491 assert_eq!(config.key_name, "test-transit-key");
492 assert_eq!(config.address, "https://vault.example.com");
493 assert_eq!(config.namespace, Some("test-namespace".to_string()));
494 assert_eq!(config.role_id.to_str().as_str(), "test-role");
495 assert_eq!(config.secret_id.to_str().as_str(), "test-secret");
496 assert_eq!(config.pubkey, "test-pubkey");
497 assert_eq!(config.mount_point, Some("transit".to_string()));
498 }
499 _ => panic!("Expected VaultTransit config"),
500 }
501
502 Ok(())
503 }
504
505 #[tokio::test]
506 async fn test_process_signer_aws_kms() -> Result<()> {
507 let signer = SignerFileConfig {
508 id: "aws-kms-signer".to_string(),
509 config: SignerFileConfigEnum::AwsKms(AwsKmsSignerFileConfig {
510 region: Some("us-east-1".to_string()),
511 key_id: "test-key-id".to_string(),
512 }),
513 };
514
515 let result = process_signer(&signer).await;
516
517 assert!(
518 result.is_ok(),
519 "Failed to process AWS KMS signer: {:?}",
520 result.err()
521 );
522 let model = result.unwrap();
523
524 assert_eq!(model.id, "aws-kms-signer");
525
526 match model.config {
527 SignerConfig::AwsKms(_) => {}
528 _ => panic!("Expected AwsKms config"),
529 }
530
531 Ok(())
532 }
533
534 async fn setup_mock_approle_login(
536 mock_server: &MockServer,
537 role_id: &str,
538 secret_id: &str,
539 token: &str,
540 ) {
541 Mock::given(method("POST"))
542 .and(path("/v1/auth/approle/login"))
543 .and(body_json(json!({
544 "role_id": role_id,
545 "secret_id": secret_id
546 })))
547 .respond_with(ResponseTemplate::new(200).set_body_json(json!({
548 "request_id": "test-request-id",
549 "lease_id": "",
550 "renewable": false,
551 "lease_duration": 0,
552 "data": null,
553 "wrap_info": null,
554 "warnings": null,
555 "auth": {
556 "client_token": token,
557 "accessor": "test-accessor",
558 "policies": ["default"],
559 "token_policies": ["default"],
560 "metadata": {
561 "role_name": "test-role"
562 },
563 "lease_duration": 3600,
564 "renewable": true,
565 "entity_id": "test-entity-id",
566 "token_type": "service",
567 "orphan": true
568 }
569 })))
570 .mount(mock_server)
571 .await;
572 }
573
574 #[tokio::test]
575 async fn test_process_signer_vault() -> Result<()> {
576 let mock_server = MockServer::start().await;
577
578 setup_mock_approle_login(&mock_server, "test-role-id", "test-secret-id", "test-token")
579 .await;
580
581 Mock::given(method("GET"))
582 .and(path("/v1/secret/data/test-key"))
583 .and(header("X-Vault-Token", "test-token"))
584 .respond_with(ResponseTemplate::new(200).set_body_json(json!({
585 "request_id": "test-request-id",
586 "lease_id": "",
587 "renewable": false,
588 "lease_duration": 0,
589 "data": {
590 "data": {
591 "value": "C5ACE14AB163556747F02C1110911537578FBE335FB74D18FBF82990AD70C3B9"
592 },
593 "metadata": {
594 "created_time": "2024-01-01T00:00:00Z",
595 "deletion_time": "",
596 "destroyed": false,
597 "version": 1
598 }
599 },
600 "wrap_info": null,
601 "warnings": null,
602 "auth": null
603 })))
604 .mount(&mock_server)
605 .await;
606
607 let signer = SignerFileConfig {
608 id: "vault-signer".to_string(),
609 config: SignerFileConfigEnum::Vault(VaultSignerFileConfig {
610 key_name: "test-key".to_string(),
611 address: mock_server.uri(),
612 namespace: Some("test-namespace".to_string()),
613 role_id: PlainOrEnvValue::Plain {
614 value: SecretString::new("test-role-id"),
615 },
616 secret_id: PlainOrEnvValue::Plain {
617 value: SecretString::new("test-secret-id"),
618 },
619 mount_point: Some("secret".to_string()),
620 }),
621 };
622
623 let result = process_signer(&signer).await;
624
625 assert!(
626 result.is_ok(),
627 "Failed to process Vault signer: {:?}",
628 result.err()
629 );
630 let model = result.unwrap();
631
632 assert_eq!(model.id, "vault-signer");
633
634 match model.config {
635 SignerConfig::Vault(_) => {}
636 _ => panic!("Expected Vault config"),
637 }
638
639 Ok(())
640 }
641
642 #[tokio::test]
643 async fn test_process_signers() -> Result<()> {
644 let signers = vec![
646 SignerFileConfig {
647 id: "test-signer-1".to_string(),
648 config: SignerFileConfigEnum::Test(TestSignerFileConfig {}),
649 },
650 SignerFileConfig {
651 id: "test-signer-2".to_string(),
652 config: SignerFileConfigEnum::Test(TestSignerFileConfig {}),
653 },
654 ];
655
656 let config = Config {
658 signers,
659 relayers: vec![],
660 notifications: vec![],
661 networks: NetworksFileConfig::new(vec![]).unwrap(),
662 plugins: Some(vec![]),
663 };
664
665 let app_state = ThinData(create_test_app_state());
667
668 process_signers(&config, &app_state).await?;
670
671 let stored_signers = app_state.signer_repository.list_all().await?;
673 assert_eq!(stored_signers.len(), 2);
674 assert!(stored_signers.iter().any(|s| s.id == "test-signer-1"));
675 assert!(stored_signers.iter().any(|s| s.id == "test-signer-2"));
676
677 Ok(())
678 }
679
680 #[tokio::test]
681 async fn test_process_notifications() -> Result<()> {
682 let notifications = vec![
684 NotificationFileConfig {
685 id: "test-notification-1".to_string(),
686 r#type: crate::config::NotificationFileConfigType::Webhook,
687 url: "https://hooks.slack.com/test1".to_string(),
688 signing_key: None,
689 },
690 NotificationFileConfig {
691 id: "test-notification-2".to_string(),
692 r#type: crate::config::NotificationFileConfigType::Webhook,
693 url: "https://hooks.slack.com/test2".to_string(),
694 signing_key: None,
695 },
696 ];
697
698 let config = Config {
700 signers: vec![],
701 relayers: vec![],
702 notifications,
703 networks: NetworksFileConfig::new(vec![]).unwrap(),
704 plugins: Some(vec![]),
705 };
706
707 let app_state = ThinData(create_test_app_state());
709
710 process_notifications(&config, &app_state).await?;
712
713 let stored_notifications = app_state.notification_repository.list_all().await?;
715 assert_eq!(stored_notifications.len(), 2);
716 assert!(stored_notifications
717 .iter()
718 .any(|n| n.id == "test-notification-1"));
719 assert!(stored_notifications
720 .iter()
721 .any(|n| n.id == "test-notification-2"));
722
723 Ok(())
724 }
725
726 #[tokio::test]
727 async fn test_process_networks_empty() -> Result<()> {
728 let config = Config {
729 signers: vec![],
730 relayers: vec![],
731 notifications: vec![],
732 networks: NetworksFileConfig::new(vec![]).unwrap(),
733 plugins: Some(vec![]),
734 };
735
736 let app_state = ThinData(create_test_app_state());
737
738 process_networks(&config, &app_state).await?;
739
740 let stored_networks = app_state.network_repository.list_all().await?;
741 assert_eq!(stored_networks.len(), 0);
742
743 Ok(())
744 }
745
746 #[tokio::test]
747 async fn test_process_networks_single_evm() -> Result<()> {
748 use crate::config::network::test_utils::*;
749
750 let networks = vec![create_evm_network_wrapped("mainnet")];
751
752 let config = Config {
753 signers: vec![],
754 relayers: vec![],
755 notifications: vec![],
756 networks: NetworksFileConfig::new(networks).unwrap(),
757 plugins: Some(vec![]),
758 };
759
760 let app_state = ThinData(create_test_app_state());
761
762 process_networks(&config, &app_state).await?;
763
764 let stored_networks = app_state.network_repository.list_all().await?;
765 assert_eq!(stored_networks.len(), 1);
766 assert_eq!(stored_networks[0].name, "mainnet");
767 assert_eq!(stored_networks[0].network_type, NetworkType::Evm);
768
769 Ok(())
770 }
771
772 #[tokio::test]
773 async fn test_process_networks_single_solana() -> Result<()> {
774 use crate::config::network::test_utils::*;
775
776 let networks = vec![create_solana_network_wrapped("devnet")];
777
778 let config = Config {
779 signers: vec![],
780 relayers: vec![],
781 notifications: vec![],
782 networks: NetworksFileConfig::new(networks).unwrap(),
783 plugins: Some(vec![]),
784 };
785
786 let app_state = ThinData(create_test_app_state());
787
788 process_networks(&config, &app_state).await?;
789
790 let stored_networks = app_state.network_repository.list_all().await?;
791 assert_eq!(stored_networks.len(), 1);
792 assert_eq!(stored_networks[0].name, "devnet");
793 assert_eq!(stored_networks[0].network_type, NetworkType::Solana);
794
795 Ok(())
796 }
797
798 #[tokio::test]
799 async fn test_process_networks_multiple_mixed() -> Result<()> {
800 use crate::config::network::test_utils::*;
801
802 let networks = vec![
803 create_evm_network_wrapped("mainnet"),
804 create_solana_network_wrapped("devnet"),
805 create_evm_network_wrapped("sepolia"),
806 create_solana_network_wrapped("testnet"),
807 ];
808
809 let config = Config {
810 signers: vec![],
811 relayers: vec![],
812 notifications: vec![],
813 networks: NetworksFileConfig::new(networks).unwrap(),
814 plugins: Some(vec![]),
815 };
816
817 let app_state = ThinData(create_test_app_state());
818
819 process_networks(&config, &app_state).await?;
820
821 let stored_networks = app_state.network_repository.list_all().await?;
822 assert_eq!(stored_networks.len(), 4);
823
824 let evm_networks: Vec<_> = stored_networks
825 .iter()
826 .filter(|n| n.network_type == NetworkType::Evm)
827 .collect();
828 assert_eq!(evm_networks.len(), 2);
829 assert!(evm_networks.iter().any(|n| n.name == "mainnet"));
830 assert!(evm_networks.iter().any(|n| n.name == "sepolia"));
831
832 let solana_networks: Vec<_> = stored_networks
833 .iter()
834 .filter(|n| n.network_type == NetworkType::Solana)
835 .collect();
836 assert_eq!(solana_networks.len(), 2);
837 assert!(solana_networks.iter().any(|n| n.name == "devnet"));
838 assert!(solana_networks.iter().any(|n| n.name == "testnet"));
839
840 Ok(())
841 }
842
843 #[tokio::test]
844 async fn test_process_networks_many_networks() -> Result<()> {
845 use crate::config::network::test_utils::*;
846
847 let networks = (0..10)
848 .map(|i| create_evm_network_wrapped(&format!("network-{}", i)))
849 .collect();
850
851 let config = Config {
852 signers: vec![],
853 relayers: vec![],
854 notifications: vec![],
855 networks: NetworksFileConfig::new(networks).unwrap(),
856 plugins: Some(vec![]),
857 };
858
859 let app_state = ThinData(create_test_app_state());
860
861 process_networks(&config, &app_state).await?;
862
863 let stored_networks = app_state.network_repository.list_all().await?;
864 assert_eq!(stored_networks.len(), 10);
865
866 for i in 0..10 {
867 let expected_name = format!("network-{}", i);
868 assert!(
869 stored_networks.iter().any(|n| n.name == expected_name),
870 "Network {} not found",
871 expected_name
872 );
873 }
874
875 Ok(())
876 }
877
878 #[tokio::test]
879 async fn test_process_networks_duplicate_names() -> Result<()> {
880 use crate::config::network::test_utils::*;
881
882 let networks = vec![
883 create_evm_network_wrapped("mainnet"),
884 create_solana_network_wrapped("mainnet"),
885 ];
886
887 let config = Config {
888 signers: vec![],
889 relayers: vec![],
890 notifications: vec![],
891 networks: NetworksFileConfig::new(networks).unwrap(),
892 plugins: Some(vec![]),
893 };
894
895 let app_state = ThinData(create_test_app_state());
896
897 process_networks(&config, &app_state).await?;
898
899 let stored_networks = app_state.network_repository.list_all().await?;
900 assert_eq!(stored_networks.len(), 2);
901
902 let mainnet_networks: Vec<_> = stored_networks
903 .iter()
904 .filter(|n| n.name == "mainnet")
905 .collect();
906 assert_eq!(mainnet_networks.len(), 2);
907 assert!(mainnet_networks
908 .iter()
909 .any(|n| n.network_type == NetworkType::Evm));
910 assert!(mainnet_networks
911 .iter()
912 .any(|n| n.network_type == NetworkType::Solana));
913
914 Ok(())
915 }
916
917 #[tokio::test]
918 async fn test_process_networks() -> Result<()> {
919 use crate::config::network::test_utils::*;
920
921 let networks = vec![
922 create_evm_network_wrapped("mainnet"),
923 create_solana_network_wrapped("devnet"),
924 ];
925
926 let config = Config {
927 signers: vec![],
928 relayers: vec![],
929 notifications: vec![],
930 networks: NetworksFileConfig::new(networks).unwrap(),
931 plugins: Some(vec![]),
932 };
933
934 let app_state = ThinData(create_test_app_state());
935
936 process_networks(&config, &app_state).await?;
937
938 let stored_networks = app_state.network_repository.list_all().await?;
939 assert_eq!(stored_networks.len(), 2);
940 assert!(stored_networks
941 .iter()
942 .any(|n| n.name == "mainnet" && n.network_type == NetworkType::Evm));
943 assert!(stored_networks
944 .iter()
945 .any(|n| n.name == "devnet" && n.network_type == NetworkType::Solana));
946
947 Ok(())
948 }
949
950 #[tokio::test]
951 async fn test_process_relayers() -> Result<()> {
952 let signers = vec![SignerFileConfig {
954 id: "test-signer-1".to_string(),
955 config: SignerFileConfigEnum::Test(TestSignerFileConfig {}),
956 }];
957
958 let relayers = vec![RelayerFileConfig {
960 id: "test-relayer-1".to_string(),
961 network_type: ConfigFileNetworkType::Evm,
962 signer_id: "test-signer-1".to_string(),
963 name: "test-relayer-1".to_string(),
964 network: "test-network".to_string(),
965 paused: false,
966 policies: None,
967 notification_id: None,
968 custom_rpc_urls: None,
969 }];
970
971 let config = Config {
973 signers: signers.clone(),
974 relayers,
975 notifications: vec![],
976 networks: NetworksFileConfig::new(vec![]).unwrap(),
977 plugins: Some(vec![]),
978 };
979
980 let app_state = ThinData(create_test_app_state());
982
983 process_signers(&config, &app_state).await?;
985
986 process_relayers(&config, &app_state).await?;
988
989 let stored_relayers = app_state.relayer_repository.list_all().await?;
991 assert_eq!(stored_relayers.len(), 1);
992 assert_eq!(stored_relayers[0].id, "test-relayer-1");
993 assert_eq!(stored_relayers[0].signer_id, "test-signer-1");
994 assert!(!stored_relayers[0].address.is_empty()); Ok(())
997 }
998
999 #[tokio::test]
1000 async fn test_process_plugins() -> Result<()> {
1001 let plugins = vec![
1003 PluginFileConfig {
1004 id: "test-plugin-1".to_string(),
1005 path: "/app/plugins/test.ts".to_string(),
1006 },
1007 PluginFileConfig {
1008 id: "test-plugin-2".to_string(),
1009 path: "/app/plugins/test2.ts".to_string(),
1010 },
1011 ];
1012
1013 let config = Config {
1015 signers: vec![],
1016 relayers: vec![],
1017 notifications: vec![],
1018 networks: NetworksFileConfig::new(vec![]).unwrap(),
1019 plugins: Some(plugins),
1020 };
1021
1022 let app_state = ThinData(create_test_app_state());
1024
1025 process_plugins(&config, &app_state).await?;
1027
1028 let plugin_1 = app_state
1030 .plugin_repository
1031 .get_by_id("test-plugin-1")
1032 .await?;
1033 let plugin_2 = app_state
1034 .plugin_repository
1035 .get_by_id("test-plugin-2")
1036 .await?;
1037
1038 assert!(plugin_1.is_some());
1039 assert!(plugin_2.is_some());
1040 assert_eq!(plugin_1.unwrap().path, "/app/plugins/test.ts");
1041 assert_eq!(plugin_2.unwrap().path, "/app/plugins/test2.ts");
1042
1043 Ok(())
1044 }
1045
1046 #[tokio::test]
1047 async fn test_process_config_file() -> Result<()> {
1048 let signers = vec![SignerFileConfig {
1050 id: "test-signer-1".to_string(),
1051 config: SignerFileConfigEnum::Test(TestSignerFileConfig {}),
1052 }];
1053
1054 let relayers = vec![RelayerFileConfig {
1055 id: "test-relayer-1".to_string(),
1056 network_type: ConfigFileNetworkType::Evm,
1057 signer_id: "test-signer-1".to_string(),
1058 name: "test-relayer-1".to_string(),
1059 network: "test-network".to_string(),
1060 paused: false,
1061 policies: None,
1062 notification_id: None,
1063 custom_rpc_urls: None,
1064 }];
1065
1066 let notifications = vec![NotificationFileConfig {
1067 id: "test-notification-1".to_string(),
1068 r#type: crate::config::NotificationFileConfigType::Webhook,
1069 url: "https://hooks.slack.com/test1".to_string(),
1070 signing_key: None,
1071 }];
1072
1073 let plugins = vec![PluginFileConfig {
1074 id: "test-plugin-1".to_string(),
1075 path: "/app/plugins/test.ts".to_string(),
1076 }];
1077
1078 let config = Config {
1080 signers,
1081 relayers,
1082 notifications,
1083 networks: NetworksFileConfig::new(vec![]).unwrap(),
1084 plugins: Some(plugins),
1085 };
1086
1087 let signer_repo = Arc::new(InMemorySignerRepository::default());
1089 let relayer_repo = Arc::new(RelayerRepositoryStorage::in_memory(
1090 InMemoryRelayerRepository::default(),
1091 ));
1092 let notification_repo = Arc::new(InMemoryNotificationRepository::default());
1093 let network_repo = Arc::new(InMemoryNetworkRepository::default());
1094 let transaction_repo = Arc::new(InMemoryTransactionRepository::default());
1095 let transaction_counter = Arc::new(InMemoryTransactionCounter::default());
1096 let plugin_repo = Arc::new(InMemoryPluginRepository::default());
1097
1098 let mut mock_job_producer = MockJobProducerTrait::new();
1100 mock_job_producer
1101 .expect_produce_transaction_request_job()
1102 .returning(|_, _| Box::pin(async { Ok(()) }));
1103 mock_job_producer
1104 .expect_produce_submit_transaction_job()
1105 .returning(|_, _| Box::pin(async { Ok(()) }));
1106 mock_job_producer
1107 .expect_produce_check_transaction_status_job()
1108 .returning(|_, _| Box::pin(async { Ok(()) }));
1109 mock_job_producer
1110 .expect_produce_send_notification_job()
1111 .returning(|_, _| Box::pin(async { Ok(()) }));
1112 let job_producer = Arc::new(mock_job_producer);
1113
1114 let app_state = ThinData(AppState {
1116 signer_repository: signer_repo.clone(),
1117 relayer_repository: relayer_repo.clone(),
1118 notification_repository: notification_repo.clone(),
1119 network_repository: network_repo.clone(),
1120 transaction_repository: transaction_repo.clone(),
1121 transaction_counter_store: transaction_counter.clone(),
1122 job_producer: job_producer.clone(),
1123 plugin_repository: plugin_repo.clone(),
1124 });
1125
1126 process_config_file(config, app_state).await?;
1128
1129 let stored_signers = signer_repo.list_all().await?;
1131 assert_eq!(stored_signers.len(), 1);
1132 assert_eq!(stored_signers[0].id, "test-signer-1");
1133
1134 let stored_relayers = relayer_repo.list_all().await?;
1135 assert_eq!(stored_relayers.len(), 1);
1136 assert_eq!(stored_relayers[0].id, "test-relayer-1");
1137 assert_eq!(stored_relayers[0].signer_id, "test-signer-1");
1138
1139 let stored_notifications = notification_repo.list_all().await?;
1140 assert_eq!(stored_notifications.len(), 1);
1141 assert_eq!(stored_notifications[0].id, "test-notification-1");
1142
1143 let stored_plugin = plugin_repo.get_by_id("test-plugin-1").await?;
1144 assert_eq!(stored_plugin.unwrap().path, "/app/plugins/test.ts");
1145
1146 Ok(())
1147 }
1148
1149 #[tokio::test]
1150 async fn test_process_signer_google_cloud_kms() {
1151 use crate::models::SecretString;
1152
1153 let signer = SignerFileConfig {
1154 id: "gcp-kms-signer".to_string(),
1155 config: SignerFileConfigEnum::GoogleCloudKms(GoogleCloudKmsSignerFileConfig {
1156 service_account: ServiceAccountConfig {
1157 private_key: PlainOrEnvValue::Plain {
1158 value: SecretString::new("-----BEGIN EXAMPLE PRIVATE KEY-----\nFAKEKEYDATA\n-----END EXAMPLE PRIVATE KEY-----\n"),
1159 },
1160 client_email: PlainOrEnvValue::Plain {
1161 value: SecretString::new("test-service-account@example.com"),
1162 },
1163 private_key_id: PlainOrEnvValue::Plain {
1164 value: SecretString::new("fake-private-key-id"),
1165 },
1166 client_id: "fake-client-id".to_string(),
1167 project_id: "fake-project-id".to_string(),
1168 auth_uri: "https://accounts.google.com/o/oauth2/auth".to_string(),
1169 token_uri: "https://oauth2.googleapis.com/token".to_string(),
1170 client_x509_cert_url: "https://www.googleapis.com/robot/v1/metadata/x509/test-service-account%40example.com".to_string(),
1171 auth_provider_x509_cert_url: "https://www.googleapis.com/oauth2/v1/certs".to_string(),
1172 universe_domain: "googleapis.com".to_string(),
1173 },
1174 key: KmsKeyConfig {
1175 location: "global".to_string(),
1176 key_id: "fake-key-id".to_string(),
1177 key_ring_id: "fake-key-ring-id".to_string(),
1178 key_version: 1,
1179 },
1180 }),
1181 };
1182
1183 let result = process_signer(&signer).await;
1184
1185 assert!(
1186 result.is_ok(),
1187 "Failed to process Google Cloud KMS signer: {:?}",
1188 result.err()
1189 );
1190 let model = result.unwrap();
1191
1192 assert_eq!(model.id, "gcp-kms-signer");
1193 }
1194}