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