openzeppelin_relayer/models/signer/
config.rs

1//! Configuration file representation and parsing for signers.
2//!
3//! This module handles the configuration file format for signers, providing:
4//!
5//! - **Config Models**: Structures that match the configuration file schema
6//! - **Conversions**: Bidirectional mapping between config and domain models
7//! - **Collections**: Container types for managing multiple signer configurations
8//!
9//! Used primarily during application startup to parse signer settings from config files.
10//! Validation is handled by the domain model in signer.rs to ensure reusability.
11
12use crate::{
13    config::ConfigFileError,
14    models::signer::{
15        AwsKmsSignerConfig, AzureKeyVaultSignerConfig, CdpSignerConfig, GoogleCloudKmsSignerConfig,
16        GoogleCloudKmsSignerKeyConfig, GoogleCloudKmsSignerServiceAccountConfig, LocalSignerConfig,
17        Signer, SignerConfig, TurnkeySignerConfig, VaultSignerConfig, VaultTransitSignerConfig,
18    },
19    models::PlainOrEnvValue,
20};
21use secrets::SecretVec;
22use serde::{Deserialize, Serialize};
23use std::{collections::HashSet, path::Path};
24
25#[derive(Debug, Serialize, Deserialize, Clone, PartialEq)]
26#[serde(deny_unknown_fields)]
27pub struct LocalSignerFileConfig {
28    pub path: String,
29    pub passphrase: PlainOrEnvValue,
30}
31
32#[derive(Debug, Serialize, Deserialize, Clone, PartialEq)]
33#[serde(deny_unknown_fields)]
34pub struct AwsKmsSignerFileConfig {
35    pub region: String,
36    pub key_id: String,
37}
38
39#[derive(Debug, Serialize, Deserialize, Clone, PartialEq)]
40#[serde(deny_unknown_fields)]
41pub struct AzureKeyVaultSignerFileConfig {
42    pub auth_type: Option<crate::models::AzureKeyVaultAuthType>,
43    pub tenant_id: Option<PlainOrEnvValue>,
44    pub client_id: Option<PlainOrEnvValue>,
45    pub client_secret: Option<PlainOrEnvValue>,
46    pub federated_token_file: Option<PlainOrEnvValue>,
47    pub vault_url: String,
48    pub key_name: String,
49    pub key_version: Option<String>,
50}
51
52#[derive(Debug, Serialize, Deserialize, Clone, PartialEq)]
53#[serde(deny_unknown_fields)]
54pub struct TurnkeySignerFileConfig {
55    pub api_public_key: String,
56    pub api_private_key: PlainOrEnvValue,
57    pub organization_id: String,
58    pub private_key_id: String,
59    pub public_key: String,
60}
61
62#[derive(Debug, Serialize, Deserialize, Clone, PartialEq)]
63#[serde(deny_unknown_fields)]
64pub struct CdpSignerFileConfig {
65    pub api_key_id: String,
66    pub api_key_secret: PlainOrEnvValue,
67    pub wallet_secret: PlainOrEnvValue,
68    pub account_address: String,
69}
70
71#[derive(Debug, Serialize, Deserialize, Clone, PartialEq)]
72#[serde(deny_unknown_fields)]
73pub struct VaultSignerFileConfig {
74    pub address: String,
75    pub namespace: Option<String>,
76    pub role_id: PlainOrEnvValue,
77    pub secret_id: PlainOrEnvValue,
78    pub key_name: String,
79    pub mount_point: Option<String>,
80}
81
82#[derive(Debug, Serialize, Deserialize, Clone, PartialEq)]
83#[serde(deny_unknown_fields)]
84pub struct VaultTransitSignerFileConfig {
85    pub key_name: String,
86    pub address: String,
87    pub role_id: PlainOrEnvValue,
88    pub secret_id: PlainOrEnvValue,
89    pub pubkey: String,
90    pub mount_point: Option<String>,
91    pub namespace: Option<String>,
92}
93
94fn google_cloud_default_auth_uri() -> String {
95    "https://accounts.google.com/o/oauth2/auth".to_string()
96}
97
98fn google_cloud_default_token_uri() -> String {
99    "https://oauth2.googleapis.com/token".to_string()
100}
101
102fn google_cloud_default_auth_provider_x509_cert_url() -> String {
103    "https://www.googleapis.com/oauth2/v1/certs".to_string()
104}
105
106fn google_cloud_default_client_x509_cert_url() -> String {
107    "https://www.googleapis.com/robot/v1/metadata/x509/solana-signer%40forward-emitter-459820-r7.iam.gserviceaccount.com".to_string()
108}
109
110fn google_cloud_default_universe_domain() -> String {
111    "googleapis.com".to_string()
112}
113
114fn google_cloud_default_key_version() -> u32 {
115    1
116}
117
118fn google_cloud_default_location() -> String {
119    "global".to_string()
120}
121
122#[derive(Debug, Serialize, Deserialize, Clone, PartialEq)]
123#[serde(deny_unknown_fields)]
124pub struct GoogleCloudKmsServiceAccountFileConfig {
125    pub project_id: String,
126    pub private_key_id: PlainOrEnvValue,
127    pub private_key: PlainOrEnvValue,
128    pub client_email: PlainOrEnvValue,
129    pub client_id: String,
130    #[serde(default = "google_cloud_default_auth_uri")]
131    pub auth_uri: String,
132    #[serde(default = "google_cloud_default_token_uri")]
133    pub token_uri: String,
134    #[serde(default = "google_cloud_default_auth_provider_x509_cert_url")]
135    pub auth_provider_x509_cert_url: String,
136    #[serde(default = "google_cloud_default_client_x509_cert_url")]
137    pub client_x509_cert_url: String,
138    #[serde(default = "google_cloud_default_universe_domain")]
139    pub universe_domain: String,
140}
141
142#[derive(Debug, Serialize, Deserialize, Clone, PartialEq)]
143#[serde(deny_unknown_fields)]
144pub struct GoogleCloudKmsKeyFileConfig {
145    #[serde(default = "google_cloud_default_location")]
146    pub location: String,
147    pub key_ring_id: String,
148    pub key_id: String,
149    #[serde(default = "google_cloud_default_key_version")]
150    pub key_version: u32,
151}
152
153#[derive(Debug, Serialize, Deserialize, Clone, PartialEq)]
154#[serde(deny_unknown_fields)]
155pub struct GoogleCloudKmsSignerFileConfig {
156    pub service_account: GoogleCloudKmsServiceAccountFileConfig,
157    pub key: GoogleCloudKmsKeyFileConfig,
158}
159
160/// Main enum for all signer config types
161#[derive(Debug, Serialize, Deserialize, Clone, PartialEq)]
162#[serde(tag = "type", rename_all = "lowercase", content = "config")]
163pub enum SignerFileConfigEnum {
164    Local(LocalSignerFileConfig),
165    #[serde(rename = "aws_kms")]
166    AwsKms(AwsKmsSignerFileConfig),
167    #[serde(rename = "azure_key_vault")]
168    AzureKeyVault(AzureKeyVaultSignerFileConfig),
169    Turnkey(TurnkeySignerFileConfig),
170    Cdp(CdpSignerFileConfig),
171    Vault(VaultSignerFileConfig),
172    #[serde(rename = "vault_transit")]
173    VaultTransit(VaultTransitSignerFileConfig),
174    #[serde(rename = "google_cloud_kms")]
175    GoogleCloudKms(GoogleCloudKmsSignerFileConfig),
176}
177
178/// Individual signer configuration from config file
179#[derive(Debug, Serialize, Deserialize, Clone)]
180#[serde(deny_unknown_fields)]
181pub struct SignerFileConfig {
182    pub id: String,
183    #[serde(flatten)]
184    pub config: SignerFileConfigEnum,
185}
186
187/// Collection of signer configurations
188#[derive(Debug, Serialize, Deserialize, Clone)]
189#[serde(deny_unknown_fields)]
190pub struct SignersFileConfig {
191    pub signers: Vec<SignerFileConfig>,
192}
193
194impl SignerFileConfig {
195    pub fn validate_basic(&self) -> Result<(), ConfigFileError> {
196        if self.id.is_empty() {
197            return Err(ConfigFileError::InvalidIdLength(
198                "Signer ID cannot be empty".into(),
199            ));
200        }
201        Ok(())
202    }
203}
204
205impl SignersFileConfig {
206    pub fn new(signers: Vec<SignerFileConfig>) -> Self {
207        Self { signers }
208    }
209
210    pub fn validate(&self) -> Result<(), ConfigFileError> {
211        if self.signers.is_empty() {
212            return Ok(());
213        }
214
215        let mut ids = HashSet::new();
216        for signer in &self.signers {
217            signer.validate_basic()?;
218            if !ids.insert(signer.id.clone()) {
219                return Err(ConfigFileError::DuplicateId(signer.id.clone()));
220            }
221        }
222        Ok(())
223    }
224}
225
226impl TryFrom<LocalSignerFileConfig> for LocalSignerConfig {
227    type Error = ConfigFileError;
228
229    fn try_from(config: LocalSignerFileConfig) -> Result<Self, Self::Error> {
230        if config.path.is_empty() {
231            return Err(ConfigFileError::InvalidIdLength(
232                "Signer path cannot be empty".into(),
233            ));
234        }
235
236        let path = Path::new(&config.path);
237        if !path.exists() {
238            return Err(ConfigFileError::FileNotFound(format!(
239                "Signer file not found at path: {}",
240                path.display()
241            )));
242        }
243
244        if !path.is_file() {
245            return Err(ConfigFileError::InvalidFormat(format!(
246                "Path exists but is not a file: {}",
247                path.display()
248            )));
249        }
250
251        let passphrase = config.passphrase.get_value().map_err(|e| {
252            ConfigFileError::InvalidFormat(format!("Failed to get passphrase value: {e}"))
253        })?;
254
255        if passphrase.is_empty() {
256            return Err(ConfigFileError::InvalidFormat(
257                "Local signer passphrase cannot be empty".into(),
258            ));
259        }
260
261        let raw_key = SecretVec::new(32, |buffer| {
262            let loaded = oz_keystore::LocalClient::load(
263                Path::new(&config.path).to_path_buf(),
264                passphrase.to_str().as_str().to_string(),
265            );
266            buffer.copy_from_slice(&loaded);
267        });
268
269        Ok(LocalSignerConfig { raw_key })
270    }
271}
272
273impl TryFrom<AwsKmsSignerFileConfig> for AwsKmsSignerConfig {
274    type Error = ConfigFileError;
275
276    fn try_from(config: AwsKmsSignerFileConfig) -> Result<Self, Self::Error> {
277        Ok(AwsKmsSignerConfig {
278            region: Some(config.region),
279            key_id: config.key_id,
280        })
281    }
282}
283
284impl TryFrom<AzureKeyVaultSignerFileConfig> for AzureKeyVaultSignerConfig {
285    type Error = ConfigFileError;
286
287    fn try_from(config: AzureKeyVaultSignerFileConfig) -> Result<Self, Self::Error> {
288        let tenant_id = config
289            .tenant_id
290            .map(|value| {
291                value.get_value().map_err(|e| {
292                    ConfigFileError::InvalidFormat(format!("Failed to get tenant_id value: {e}"))
293                })
294            })
295            .transpose()?;
296        let client_id = config
297            .client_id
298            .map(|value| {
299                value.get_value().map_err(|e| {
300                    ConfigFileError::InvalidFormat(format!("Failed to get client_id value: {e}"))
301                })
302            })
303            .transpose()?;
304        let client_secret = config
305            .client_secret
306            .map(|value| {
307                value.get_value().map_err(|e| {
308                    ConfigFileError::InvalidFormat(format!(
309                        "Failed to get client_secret value: {e}"
310                    ))
311                })
312            })
313            .transpose()?;
314        let federated_token_file = config
315            .federated_token_file
316            .map(|value| {
317                value.get_value().map_err(|e| {
318                    ConfigFileError::InvalidFormat(format!(
319                        "Failed to get federated_token_file value: {e}"
320                    ))
321                })
322            })
323            .transpose()?;
324
325        Ok(AzureKeyVaultSignerConfig {
326            auth_type: config.auth_type,
327            tenant_id,
328            client_id,
329            client_secret,
330            federated_token_file,
331            vault_url: crate::models::SecretString::new(&config.vault_url),
332            key_name: crate::models::SecretString::new(&config.key_name),
333            key_version: config.key_version,
334        })
335    }
336}
337
338impl TryFrom<TurnkeySignerFileConfig> for TurnkeySignerConfig {
339    type Error = ConfigFileError;
340
341    fn try_from(config: TurnkeySignerFileConfig) -> Result<Self, Self::Error> {
342        let api_private_key = config.api_private_key.get_value().map_err(|e| {
343            ConfigFileError::InvalidFormat(format!("Failed to get API private key: {e}"))
344        })?;
345
346        Ok(TurnkeySignerConfig {
347            api_public_key: config.api_public_key,
348            api_private_key,
349            organization_id: config.organization_id,
350            private_key_id: config.private_key_id,
351            public_key: config.public_key,
352        })
353    }
354}
355
356impl TryFrom<CdpSignerFileConfig> for CdpSignerConfig {
357    type Error = ConfigFileError;
358
359    fn try_from(config: CdpSignerFileConfig) -> Result<Self, Self::Error> {
360        let api_key_secret = config.api_key_secret.get_value().map_err(|e| {
361            ConfigFileError::InvalidFormat(format!("Failed to get API key secret: {e}"))
362        })?;
363
364        let wallet_secret = config.wallet_secret.get_value().map_err(|e| {
365            ConfigFileError::InvalidFormat(format!("Failed to get wallet secret: {e}"))
366        })?;
367
368        Ok(CdpSignerConfig {
369            api_key_id: config.api_key_id,
370            api_key_secret,
371            wallet_secret,
372            account_address: config.account_address,
373        })
374    }
375}
376
377impl TryFrom<VaultSignerFileConfig> for VaultSignerConfig {
378    type Error = ConfigFileError;
379
380    fn try_from(config: VaultSignerFileConfig) -> Result<Self, Self::Error> {
381        let role_id = config
382            .role_id
383            .get_value()
384            .map_err(|e| ConfigFileError::InvalidFormat(format!("Failed to get role ID: {e}")))?;
385
386        let secret_id = config
387            .secret_id
388            .get_value()
389            .map_err(|e| ConfigFileError::InvalidFormat(format!("Failed to get secret ID: {e}")))?;
390
391        Ok(VaultSignerConfig {
392            address: config.address,
393            namespace: config.namespace,
394            role_id,
395            secret_id,
396            key_name: config.key_name,
397            mount_point: config.mount_point,
398        })
399    }
400}
401
402impl TryFrom<VaultTransitSignerFileConfig> for VaultTransitSignerConfig {
403    type Error = ConfigFileError;
404
405    fn try_from(config: VaultTransitSignerFileConfig) -> Result<Self, Self::Error> {
406        let role_id = config
407            .role_id
408            .get_value()
409            .map_err(|e| ConfigFileError::InvalidFormat(format!("Failed to get role ID: {e}")))?;
410
411        let secret_id = config
412            .secret_id
413            .get_value()
414            .map_err(|e| ConfigFileError::InvalidFormat(format!("Failed to get secret ID: {e}")))?;
415
416        Ok(VaultTransitSignerConfig {
417            key_name: config.key_name,
418            address: config.address,
419            namespace: config.namespace,
420            role_id,
421            secret_id,
422            pubkey: config.pubkey,
423            mount_point: config.mount_point,
424        })
425    }
426}
427
428impl TryFrom<GoogleCloudKmsSignerFileConfig> for GoogleCloudKmsSignerConfig {
429    type Error = ConfigFileError;
430
431    fn try_from(config: GoogleCloudKmsSignerFileConfig) -> Result<Self, Self::Error> {
432        use crate::models::SecretString;
433
434        let private_key = config
435            .service_account
436            .private_key
437            .get_value()
438            .map_err(|e| {
439                ConfigFileError::InvalidFormat(format!("Failed to get private key: {e}"))
440            })?;
441
442        let private_key_id = config
443            .service_account
444            .private_key_id
445            .get_value()
446            .map_err(|e| {
447                ConfigFileError::InvalidFormat(format!("Failed to get private key ID: {e}"))
448            })?;
449
450        let client_email = config
451            .service_account
452            .client_email
453            .get_value()
454            .map_err(|e| {
455                ConfigFileError::InvalidFormat(format!("Failed to get client email: {e}"))
456            })?;
457
458        let service_account = GoogleCloudKmsSignerServiceAccountConfig {
459            private_key,
460            private_key_id,
461            project_id: SecretString::new(&config.service_account.project_id),
462            client_email,
463            client_id: SecretString::new(&config.service_account.client_id),
464            auth_uri: SecretString::new(&config.service_account.auth_uri),
465            token_uri: SecretString::new(&config.service_account.token_uri),
466            auth_provider_x509_cert_url: SecretString::new(
467                &config.service_account.auth_provider_x509_cert_url,
468            ),
469            client_x509_cert_url: SecretString::new(&config.service_account.client_x509_cert_url),
470            universe_domain: SecretString::new(&config.service_account.universe_domain),
471        };
472
473        let key = GoogleCloudKmsSignerKeyConfig {
474            location: SecretString::new(&config.key.location),
475            key_ring_id: SecretString::new(&config.key.key_ring_id),
476            key_id: SecretString::new(&config.key.key_id),
477            key_version: config.key.key_version,
478        };
479
480        Ok(GoogleCloudKmsSignerConfig {
481            service_account,
482            key,
483        })
484    }
485}
486
487impl TryFrom<SignerFileConfigEnum> for SignerConfig {
488    type Error = ConfigFileError;
489
490    fn try_from(config: SignerFileConfigEnum) -> Result<Self, Self::Error> {
491        match config {
492            SignerFileConfigEnum::Local(local) => {
493                Ok(SignerConfig::Local(LocalSignerConfig::try_from(local)?))
494            }
495            SignerFileConfigEnum::AwsKms(aws_kms) => {
496                Ok(SignerConfig::AwsKms(AwsKmsSignerConfig::try_from(aws_kms)?))
497            }
498            SignerFileConfigEnum::AzureKeyVault(azure_key_vault) => Ok(
499                SignerConfig::AzureKeyVault(AzureKeyVaultSignerConfig::try_from(azure_key_vault)?),
500            ),
501            SignerFileConfigEnum::Turnkey(turnkey) => Ok(SignerConfig::Turnkey(
502                TurnkeySignerConfig::try_from(turnkey)?,
503            )),
504            SignerFileConfigEnum::Cdp(cdp) => {
505                Ok(SignerConfig::Cdp(CdpSignerConfig::try_from(cdp)?))
506            }
507            SignerFileConfigEnum::Vault(vault) => {
508                Ok(SignerConfig::Vault(VaultSignerConfig::try_from(vault)?))
509            }
510            SignerFileConfigEnum::VaultTransit(vault_transit) => Ok(SignerConfig::VaultTransit(
511                VaultTransitSignerConfig::try_from(vault_transit)?,
512            )),
513            SignerFileConfigEnum::GoogleCloudKms(gcp_kms) => Ok(SignerConfig::GoogleCloudKms(
514                Box::new(GoogleCloudKmsSignerConfig::try_from(gcp_kms)?),
515            )),
516        }
517    }
518}
519
520impl TryFrom<SignerFileConfig> for Signer {
521    type Error = ConfigFileError;
522
523    fn try_from(config: SignerFileConfig) -> Result<Self, Self::Error> {
524        config.validate_basic()?;
525
526        let signer_config = SignerConfig::try_from(config.config)?;
527
528        // Create core signer with configuration
529        let signer = Signer::new(config.id, signer_config);
530
531        // Validate using domain model validation logic
532        signer.validate().map_err(|e| match e {
533            crate::models::signer::SignerValidationError::EmptyId => {
534                ConfigFileError::MissingField("signer id".into())
535            }
536            crate::models::signer::SignerValidationError::InvalidIdFormat => {
537                ConfigFileError::InvalidFormat("Invalid signer ID format".into())
538            }
539            crate::models::signer::SignerValidationError::InvalidConfig(msg) => {
540                ConfigFileError::InvalidFormat(format!("Invalid signer configuration: {msg}"))
541            }
542        })?;
543
544        Ok(signer)
545    }
546}
547
548#[cfg(test)]
549mod tests {
550    use super::*;
551    use crate::models::SecretString;
552
553    #[test]
554    fn test_aws_kms_conversion() {
555        let config = AwsKmsSignerFileConfig {
556            region: "us-east-1".to_string(),
557            key_id: "test-key-id".to_string(),
558        };
559
560        let result = AwsKmsSignerConfig::try_from(config);
561        assert!(result.is_ok());
562
563        let aws_config = result.unwrap();
564        assert_eq!(aws_config.region, Some("us-east-1".to_string()));
565        assert_eq!(aws_config.key_id, "test-key-id");
566    }
567
568    #[test]
569    fn test_turnkey_conversion() {
570        let config = TurnkeySignerFileConfig {
571            api_public_key: "test-public-key".to_string(),
572            api_private_key: PlainOrEnvValue::Plain {
573                value: SecretString::new("test-private-key"),
574            },
575            organization_id: "test-org".to_string(),
576            private_key_id: "test-private-key-id".to_string(),
577            public_key: "test-public-key".to_string(),
578        };
579
580        let result = TurnkeySignerConfig::try_from(config);
581        assert!(result.is_ok());
582
583        let turnkey_config = result.unwrap();
584        assert_eq!(turnkey_config.api_public_key, "test-public-key");
585        assert_eq!(turnkey_config.organization_id, "test-org");
586    }
587
588    #[test]
589    fn test_signer_file_config_validation() {
590        let signer_config = SignerFileConfig {
591            id: "test-signer".to_string(),
592            config: SignerFileConfigEnum::Local(LocalSignerFileConfig {
593                path: "test-path".to_string(),
594                passphrase: PlainOrEnvValue::Plain {
595                    value: SecretString::new("test-passphrase"),
596                },
597            }),
598        };
599
600        assert!(signer_config.validate_basic().is_ok());
601    }
602
603    #[test]
604    fn test_empty_signer_id() {
605        let signer_config = SignerFileConfig {
606            id: "".to_string(),
607            config: SignerFileConfigEnum::Local(LocalSignerFileConfig {
608                path: "test-path".to_string(),
609                passphrase: PlainOrEnvValue::Plain {
610                    value: SecretString::new("test-passphrase"),
611                },
612            }),
613        };
614
615        assert!(signer_config.validate_basic().is_err());
616    }
617
618    #[test]
619    fn test_signers_config_validation() {
620        let configs = SignersFileConfig::new(vec![
621            SignerFileConfig {
622                id: "signer1".to_string(),
623                config: SignerFileConfigEnum::Local(LocalSignerFileConfig {
624                    path: "test-path".to_string(),
625                    passphrase: PlainOrEnvValue::Plain {
626                        value: SecretString::new("test-passphrase"),
627                    },
628                }),
629            },
630            SignerFileConfig {
631                id: "signer2".to_string(),
632                config: SignerFileConfigEnum::Local(LocalSignerFileConfig {
633                    path: "test-path".to_string(),
634                    passphrase: PlainOrEnvValue::Plain {
635                        value: SecretString::new("test-passphrase"),
636                    },
637                }),
638            },
639        ]);
640
641        assert!(configs.validate().is_ok());
642    }
643
644    #[test]
645    fn test_duplicate_signer_ids() {
646        let configs = SignersFileConfig::new(vec![
647            SignerFileConfig {
648                id: "signer1".to_string(),
649                config: SignerFileConfigEnum::Local(LocalSignerFileConfig {
650                    path: "test-path".to_string(),
651                    passphrase: PlainOrEnvValue::Plain {
652                        value: SecretString::new("test-passphrase"),
653                    },
654                }),
655            },
656            SignerFileConfig {
657                id: "signer1".to_string(), // Duplicate ID
658                config: SignerFileConfigEnum::Local(LocalSignerFileConfig {
659                    path: "test-path".to_string(),
660                    passphrase: PlainOrEnvValue::Plain {
661                        value: SecretString::new("test-passphrase"),
662                    },
663                }),
664            },
665        ]);
666
667        assert!(matches!(
668            configs.validate(),
669            Err(ConfigFileError::DuplicateId(_))
670        ));
671    }
672
673    #[test]
674    fn test_local_conversion_invalid_path() {
675        let config = LocalSignerFileConfig {
676            path: "non-existent-path".to_string(),
677            passphrase: PlainOrEnvValue::Plain {
678                value: SecretString::new("test-passphrase"),
679            },
680        };
681
682        let result = LocalSignerConfig::try_from(config);
683        assert!(result.is_err());
684        if let Err(ConfigFileError::FileNotFound(msg)) = result {
685            assert!(msg.contains("Signer file not found"));
686        } else {
687            panic!("Expected FileNotFound error");
688        }
689    }
690
691    #[test]
692    fn test_vault_conversion() {
693        let config = VaultSignerFileConfig {
694            address: "https://vault.example.com".to_string(),
695            namespace: Some("test-namespace".to_string()),
696            role_id: PlainOrEnvValue::Plain {
697                value: SecretString::new("test-role"),
698            },
699            secret_id: PlainOrEnvValue::Plain {
700                value: SecretString::new("test-secret"),
701            },
702            key_name: "test-key".to_string(),
703            mount_point: Some("test-mount".to_string()),
704        };
705
706        let result = VaultSignerConfig::try_from(config);
707        assert!(result.is_ok());
708
709        let vault_config = result.unwrap();
710        assert_eq!(vault_config.address, "https://vault.example.com");
711        assert_eq!(vault_config.namespace, Some("test-namespace".to_string()));
712    }
713
714    #[test]
715    fn test_google_cloud_kms_conversion() {
716        let config = GoogleCloudKmsSignerFileConfig {
717            service_account: GoogleCloudKmsServiceAccountFileConfig {
718                project_id: "test-project".to_string(),
719                private_key_id: PlainOrEnvValue::Plain {
720                    value: SecretString::new("test-key-id"),
721                },
722                private_key: PlainOrEnvValue::Plain {
723                    value: SecretString::new("test-private-key"),
724                },
725                client_email: PlainOrEnvValue::Plain {
726                    value: SecretString::new("test@email.com"),
727                },
728                client_id: "test-client-id".to_string(),
729                auth_uri: google_cloud_default_auth_uri(),
730                token_uri: google_cloud_default_token_uri(),
731                auth_provider_x509_cert_url: google_cloud_default_auth_provider_x509_cert_url(),
732                client_x509_cert_url: google_cloud_default_client_x509_cert_url(),
733                universe_domain: google_cloud_default_universe_domain(),
734            },
735            key: GoogleCloudKmsKeyFileConfig {
736                location: google_cloud_default_location(),
737                key_ring_id: "test-ring".to_string(),
738                key_id: "test-key".to_string(),
739                key_version: google_cloud_default_key_version(),
740            },
741        };
742
743        let result = GoogleCloudKmsSignerConfig::try_from(config);
744        assert!(result.is_ok());
745
746        let gcp_config = result.unwrap();
747        assert_eq!(gcp_config.key.key_id.to_str().as_str(), "test-key");
748        assert_eq!(
749            gcp_config.service_account.project_id.to_str().as_str(),
750            "test-project"
751        );
752    }
753
754    #[test]
755    fn test_cdp_file_config_conversion() {
756        use crate::models::SecretString;
757        let cfg = CdpSignerFileConfig {
758            api_key_id: "id".into(),
759            api_key_secret: PlainOrEnvValue::Plain {
760                value: SecretString::new("asecret"),
761            },
762            wallet_secret: PlainOrEnvValue::Plain {
763                value: SecretString::new("wsecret"),
764            },
765            account_address: "0x0000000000000000000000000000000000000000".into(),
766        };
767        let res = CdpSignerConfig::try_from(cfg);
768        assert!(res.is_ok());
769        let c = res.unwrap();
770        assert_eq!(c.api_key_id, "id");
771        assert_eq!(
772            c.account_address,
773            "0x0000000000000000000000000000000000000000"
774        );
775    }
776
777    #[test]
778    fn test_cdp_file_config_conversion_api_key_secret_error() {
779        let cfg = CdpSignerFileConfig {
780            api_key_id: "id".into(),
781            api_key_secret: PlainOrEnvValue::Env {
782                value: "NONEXISTENT_ENV_VAR".into(),
783            },
784            wallet_secret: PlainOrEnvValue::Plain {
785                value: SecretString::new("wsecret"),
786            },
787            account_address: "0x0000000000000000000000000000000000000000".into(),
788        };
789        let res = CdpSignerConfig::try_from(cfg);
790        assert!(res.is_err());
791        let err = res.unwrap_err();
792        assert!(matches!(err, ConfigFileError::InvalidFormat(_)));
793        if let ConfigFileError::InvalidFormat(msg) = err {
794            assert!(msg.contains("Failed to get API key secret"));
795        }
796    }
797
798    #[test]
799    fn test_cdp_file_config_conversion_wallet_secret_error() {
800        let cfg = CdpSignerFileConfig {
801            api_key_id: "id".into(),
802            api_key_secret: PlainOrEnvValue::Plain {
803                value: SecretString::new("asecret"),
804            },
805            wallet_secret: PlainOrEnvValue::Env {
806                value: "NONEXISTENT_ENV_VAR".into(),
807            },
808            account_address: "0x0000000000000000000000000000000000000000".into(),
809        };
810        let res = CdpSignerConfig::try_from(cfg);
811        assert!(res.is_err());
812        let err = res.unwrap_err();
813        assert!(matches!(err, ConfigFileError::InvalidFormat(_)));
814        if let ConfigFileError::InvalidFormat(msg) = err {
815            assert!(msg.contains("Failed to get wallet secret"));
816        }
817    }
818}