openzeppelin_relayer/bootstrap/
config_processor.rs

1//! This module provides functionality for processing configuration files and populating
2//! repositories.
3use 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
25/// Process all plugins from the config file and store them in the repository.
26async 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
51/// Process a signer configuration from the config file and convert it into a `SignerRepoModel`.
52///
53/// This function handles different types of signers including:
54/// - Test signers with randomly generated keys
55/// - Local signers with keys loaded from keystore files
56/// - AWS KMS signers
57/// - Vault signers that retrieve private keys from HashiCorp Vault
58/// - Vault Cloud signers that retrieve private keys from HashiCorp Cloud
59/// - Vault Transit signers that use HashiCorp Vault's Transit engine for signing
60async 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
225/// Process all signers from the config file and store them in the repository.
226///
227/// For each signer in the config file:
228/// 1. Process it using `process_signer`
229/// 2. Store the resulting model in the repository
230///
231/// This function processes signers in parallel using futures.
232async 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
253/// Process all notification configurations from the config file and store them in the repository.
254///
255/// For each notification in the config file:
256/// 1. Convert it to a repository model
257/// 2. Store the resulting model in the repository
258///
259/// This function processes notifications in parallel using futures.
260async 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
282/// Process all network configurations from the config file and store them in the repository.
283///
284/// For each network in the config file:
285/// 1. Convert it to a repository model using TryFrom
286/// 2. Store the resulting model in the repository
287///
288/// This function processes networks in parallel using futures.
289async 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
310/// Process all relayer configurations from the config file and store them in the repository.
311///
312/// For each relayer in the config file:
313/// 1. Convert it to a repository model
314/// 2. Retrieve the associated signer
315/// 3. Create a signer service
316/// 4. Get the signer's address and add it to the relayer model
317/// 5. Store the resulting model in the repository
318///
319/// This function processes relayers in parallel using futures.
320async 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
355/// Process a complete configuration file by initializing all repositories.
356///
357/// This function processes the entire configuration file in the following order:
358/// 1. Process signers
359/// 2. Process notifications
360/// 3. Process networks
361/// 4. Process relayers
362pub 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        // Create a mock job producer
399        let mut mock_job_producer = MockJobProducerTrait::new();
400
401        // Set up expectations for the mock
402        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    // utility function to setup a mock AppRole login response
535    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        // Create test signers
645        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        // Create config
657        let config = Config {
658            signers,
659            relayers: vec![],
660            notifications: vec![],
661            networks: NetworksFileConfig::new(vec![]).unwrap(),
662            plugins: Some(vec![]),
663        };
664
665        // Create app state
666        let app_state = ThinData(create_test_app_state());
667
668        // Process signers
669        process_signers(&config, &app_state).await?;
670
671        // Verify signers were created
672        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        // Create test notifications
683        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        // Create config
699        let config = Config {
700            signers: vec![],
701            relayers: vec![],
702            notifications,
703            networks: NetworksFileConfig::new(vec![]).unwrap(),
704            plugins: Some(vec![]),
705        };
706
707        // Create app state
708        let app_state = ThinData(create_test_app_state());
709
710        // Process notifications
711        process_notifications(&config, &app_state).await?;
712
713        // Verify notifications were created
714        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        // Create test signers
953        let signers = vec![SignerFileConfig {
954            id: "test-signer-1".to_string(),
955            config: SignerFileConfigEnum::Test(TestSignerFileConfig {}),
956        }];
957
958        // Create test relayers
959        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        // Create config
972        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        // Create app state
981        let app_state = ThinData(create_test_app_state());
982
983        // First process signers (required for relayers)
984        process_signers(&config, &app_state).await?;
985
986        // Process relayers
987        process_relayers(&config, &app_state).await?;
988
989        // Verify relayers were created
990        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()); // Address should be populated
995
996        Ok(())
997    }
998
999    #[tokio::test]
1000    async fn test_process_plugins() -> Result<()> {
1001        // Create test plugins
1002        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        // Create config
1014        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        // Create app state
1023        let app_state = ThinData(create_test_app_state());
1024
1025        // Process plugins
1026        process_plugins(&config, &app_state).await?;
1027
1028        // Verify plugins were created
1029        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        // Create test signers, relayers, and notifications
1049        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        // Create config
1079        let config = Config {
1080            signers,
1081            relayers,
1082            notifications,
1083            networks: NetworksFileConfig::new(vec![]).unwrap(),
1084            plugins: Some(plugins),
1085        };
1086
1087        // Create shared repositories
1088        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        // Create a mock job producer
1099        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        // Create app state
1115        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 the entire config file
1127        process_config_file(config, app_state).await?;
1128
1129        // Verify all repositories were populated
1130        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}