openzeppelin_relayer/services/signer/solana/
mod.rs

1//! Solana signer implementation for managing Solana-compatible private keys and signing operations.
2//!
3//! Provides:
4//! - Local keystore support (encrypted JSON files)
5//!
6//! # Architecture
7//!
8//! ```text
9//! SolanaSigner
10//!   ├── Local (Raw Key Signer)
11//!   ├── Vault (HashiCorp Vault backend)
12//!   ├── VaultTransit (HashiCorp Vault Transit signer)
13//!   |── GoogleCloudKms (Google Cloud KMS backend)
14//!   └── Turnkey (Turnkey backend)
15
16//! ```
17use async_trait::async_trait;
18mod local_signer;
19use local_signer::*;
20
21mod vault_signer;
22use vault_signer::*;
23
24mod vault_transit_signer;
25use vault_transit_signer::*;
26
27mod turnkey_signer;
28use turnkey_signer::*;
29
30mod cdp_signer;
31use cdp_signer::*;
32
33mod google_cloud_kms_signer;
34use google_cloud_kms_signer::*;
35
36use solana_sdk::signature::Signature;
37
38use crate::{
39    domain::{
40        SignDataRequest, SignDataResponse, SignDataResponseEvm, SignTransactionResponse,
41        SignTypedDataRequest,
42    },
43    models::{
44        Address, NetworkTransactionData, Signer as SignerDomainModel, SignerConfig,
45        SignerRepoModel, SignerType, TransactionRepoModel, VaultSignerConfig,
46    },
47    services::{CdpService, GoogleCloudKmsService, TurnkeyService, VaultConfig, VaultService},
48};
49use eyre::Result;
50
51use super::{Signer, SignerError, SignerFactoryError};
52#[cfg(test)]
53use mockall::automock;
54
55pub enum SolanaSigner {
56    Local(LocalSigner),
57    Vault(VaultSigner<VaultService>),
58    VaultTransit(VaultTransitSigner),
59    Turnkey(TurnkeySigner),
60    Cdp(CdpSigner),
61    GoogleCloudKms(GoogleCloudKmsSigner),
62}
63
64#[async_trait]
65impl Signer for SolanaSigner {
66    async fn address(&self) -> Result<Address, SignerError> {
67        match self {
68            Self::Local(signer) => signer.address().await,
69            Self::Vault(signer) => signer.address().await,
70            Self::VaultTransit(signer) => signer.address().await,
71            Self::Turnkey(signer) => signer.address().await,
72            Self::Cdp(signer) => signer.address().await,
73            Self::GoogleCloudKms(signer) => signer.address().await,
74        }
75    }
76
77    async fn sign_transaction(
78        &self,
79        transaction: NetworkTransactionData,
80    ) -> Result<SignTransactionResponse, SignerError> {
81        match self {
82            Self::Local(signer) => signer.sign_transaction(transaction).await,
83            Self::Vault(signer) => signer.sign_transaction(transaction).await,
84            Self::VaultTransit(signer) => signer.sign_transaction(transaction).await,
85            Self::Turnkey(signer) => signer.sign_transaction(transaction).await,
86            Self::Cdp(signer) => signer.sign_transaction(transaction).await,
87            Self::GoogleCloudKms(signer) => signer.sign_transaction(transaction).await,
88        }
89    }
90}
91
92#[async_trait]
93#[cfg_attr(test, automock)]
94/// Trait defining Solana-specific signing operations
95///
96/// This trait extends the basic signing functionality with methods specific
97/// to the Solana blockchain, including public key retrieval and message signing.
98pub trait SolanaSignTrait: Sync + Send {
99    /// Returns the public key of the Solana signer as an Address
100    async fn pubkey(&self) -> Result<Address, SignerError>;
101
102    /// Signs a message using the Solana signing scheme
103    ///
104    /// # Arguments
105    ///
106    /// * `message` - The message bytes to sign
107    ///
108    /// # Returns
109    ///
110    /// A Result containing either the Solana Signature or a SignerError
111    async fn sign(&self, message: &[u8]) -> Result<Signature, SignerError>;
112}
113
114#[async_trait]
115impl SolanaSignTrait for SolanaSigner {
116    async fn pubkey(&self) -> Result<Address, SignerError> {
117        match self {
118            Self::Local(signer) => signer.pubkey().await,
119            Self::Vault(signer) => signer.pubkey().await,
120            Self::VaultTransit(signer) => signer.pubkey().await,
121            Self::Turnkey(signer) => signer.pubkey().await,
122            Self::Cdp(signer) => signer.pubkey().await,
123            Self::GoogleCloudKms(signer) => signer.pubkey().await,
124        }
125    }
126
127    async fn sign(&self, message: &[u8]) -> Result<Signature, SignerError> {
128        match self {
129            Self::Local(signer) => Ok(signer.sign(message).await?),
130            Self::Vault(signer) => Ok(signer.sign(message).await?),
131            Self::VaultTransit(signer) => Ok(signer.sign(message).await?),
132            Self::Turnkey(signer) => Ok(signer.sign(message).await?),
133            Self::Cdp(signer) => Ok(signer.sign(message).await?),
134            Self::GoogleCloudKms(signer) => Ok(signer.sign(message).await?),
135        }
136    }
137}
138
139pub struct SolanaSignerFactory;
140
141impl SolanaSignerFactory {
142    pub fn create_solana_signer(
143        signer_model: &SignerDomainModel,
144    ) -> Result<SolanaSigner, SignerFactoryError> {
145        let signer = match &signer_model.config {
146            SignerConfig::Local(_) => SolanaSigner::Local(LocalSigner::new(signer_model)?),
147            SignerConfig::Vault(config) => {
148                let vault_config = VaultConfig::new(
149                    config.address.clone(),
150                    config.role_id.clone(),
151                    config.secret_id.clone(),
152                    config.namespace.clone(),
153                    config
154                        .mount_point
155                        .clone()
156                        .unwrap_or_else(|| "secret".to_string()),
157                    None,
158                );
159                let vault_service = VaultService::new(vault_config);
160
161                return Ok(SolanaSigner::Vault(VaultSigner::new(
162                    signer_model.id.clone(),
163                    config.clone(),
164                    vault_service,
165                )));
166            }
167            SignerConfig::VaultTransit(vault_transit_signer_config) => {
168                let vault_service = VaultService::new(VaultConfig {
169                    address: vault_transit_signer_config.address.clone(),
170                    namespace: vault_transit_signer_config.namespace.clone(),
171                    role_id: vault_transit_signer_config.role_id.clone(),
172                    secret_id: vault_transit_signer_config.secret_id.clone(),
173                    mount_path: "transit".to_string(),
174                    token_ttl: None,
175                });
176
177                return Ok(SolanaSigner::VaultTransit(VaultTransitSigner::new(
178                    signer_model,
179                    vault_service,
180                )));
181            }
182            SignerConfig::AwsKms(_) => {
183                return Err(SignerFactoryError::UnsupportedType("AWS KMS".into()));
184            }
185            SignerConfig::Cdp(config) => {
186                let cdp_signer = CdpSigner::new(config.clone()).map_err(|e| {
187                    SignerFactoryError::CreationFailed(format!("CDP service error: {}", e))
188                })?;
189                return Ok(SolanaSigner::Cdp(cdp_signer));
190            }
191            SignerConfig::Turnkey(turnkey_signer_config) => {
192                let turnkey_service =
193                    TurnkeyService::new(turnkey_signer_config.clone()).map_err(|e| {
194                        SignerFactoryError::InvalidConfig(format!(
195                            "Failed to create Turnkey service: {}",
196                            e
197                        ))
198                    })?;
199
200                return Ok(SolanaSigner::Turnkey(TurnkeySigner::new(turnkey_service)));
201            }
202            SignerConfig::GoogleCloudKms(google_cloud_kms_signer_config) => {
203                let google_cloud_kms_service =
204                    GoogleCloudKmsService::new(google_cloud_kms_signer_config).map_err(|e| {
205                        SignerFactoryError::InvalidConfig(format!(
206                            "Failed to create Google Cloud KMS service: {}",
207                            e
208                        ))
209                    })?;
210                return Ok(SolanaSigner::GoogleCloudKms(GoogleCloudKmsSigner::new(
211                    google_cloud_kms_service,
212                )));
213            }
214        };
215
216        Ok(signer)
217    }
218}
219
220#[cfg(test)]
221mod solana_signer_factory_tests {
222    use super::*;
223    use crate::models::{
224        AwsKmsSignerConfig, CdpSignerConfig, GoogleCloudKmsSignerConfig,
225        GoogleCloudKmsSignerKeyConfig, GoogleCloudKmsSignerServiceAccountConfig, LocalSignerConfig,
226        SecretString, SignerConfig, SignerRepoModel, SolanaTransactionData, TurnkeySignerConfig,
227        VaultSignerConfig, VaultTransitSignerConfig,
228    };
229    use mockall::predicate::*;
230    use secrets::SecretVec;
231    use std::sync::Arc;
232
233    fn test_key_bytes() -> SecretVec<u8> {
234        let key_bytes = vec![
235            1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22, 23, 24,
236            25, 26, 27, 28, 29, 30, 31, 32,
237        ];
238        SecretVec::new(key_bytes.len(), |v| v.copy_from_slice(&key_bytes))
239    }
240
241    fn test_key_bytes_pubkey() -> Address {
242        Address::Solana("9C6hybhQ6Aycep9jaUnP6uL9ZYvDjUp1aSkFWPUFJtpj".to_string())
243    }
244
245    #[test]
246    fn test_create_solana_signer_local() {
247        let signer_model = SignerDomainModel {
248            id: "test".to_string(),
249            config: SignerConfig::Local(LocalSignerConfig {
250                raw_key: test_key_bytes(),
251            }),
252        };
253
254        let signer = SolanaSignerFactory::create_solana_signer(&signer_model).unwrap();
255
256        match signer {
257            SolanaSigner::Local(_) => {}
258            _ => panic!("Expected Local signer"),
259        }
260    }
261
262    #[test]
263    fn test_create_solana_signer_test() {
264        let signer_model = SignerDomainModel {
265            id: "test".to_string(),
266            config: SignerConfig::Local(LocalSignerConfig {
267                raw_key: test_key_bytes(),
268            }),
269        };
270
271        let signer = SolanaSignerFactory::create_solana_signer(&signer_model).unwrap();
272
273        match signer {
274            SolanaSigner::Local(_) => {}
275            _ => panic!("Expected Local signer"),
276        }
277    }
278
279    #[test]
280    fn test_create_solana_signer_vault() {
281        let signer_model = SignerDomainModel {
282            id: "test".to_string(),
283            config: SignerConfig::Vault(VaultSignerConfig {
284                address: "https://vault.test.com".to_string(),
285                namespace: Some("test-namespace".to_string()),
286                role_id: crate::models::SecretString::new("test-role-id"),
287                secret_id: crate::models::SecretString::new("test-secret-id"),
288                key_name: "test-key".to_string(),
289                mount_point: Some("secret".to_string()),
290            }),
291        };
292
293        let signer = SolanaSignerFactory::create_solana_signer(&signer_model).unwrap();
294
295        match signer {
296            SolanaSigner::Vault(_) => {}
297            _ => panic!("Expected Vault signer"),
298        }
299    }
300
301    #[test]
302    fn test_create_solana_signer_vault_transit() {
303        let signer_model = SignerDomainModel {
304            id: "test".to_string(),
305            config: SignerConfig::VaultTransit(VaultTransitSignerConfig {
306                key_name: "test".to_string(),
307                address: "address".to_string(),
308                namespace: None,
309                role_id: SecretString::new("role_id"),
310                secret_id: SecretString::new("secret_id"),
311                pubkey: "pubkey".to_string(),
312                mount_point: None,
313            }),
314        };
315
316        let signer = SolanaSignerFactory::create_solana_signer(&signer_model).unwrap();
317
318        match signer {
319            SolanaSigner::VaultTransit(_) => {}
320            _ => panic!("Expected Transit signer"),
321        }
322    }
323
324    #[test]
325    fn test_create_solana_signer_turnkey() {
326        let signer_model = SignerDomainModel {
327            id: "test".to_string(),
328            config: SignerConfig::Turnkey(TurnkeySignerConfig {
329                api_private_key: SecretString::new("api_private_key"),
330                api_public_key: "api_public_key".to_string(),
331                organization_id: "organization_id".to_string(),
332                private_key_id: "private_key_id".to_string(),
333                public_key: "public_key".to_string(),
334            }),
335        };
336
337        let signer = SolanaSignerFactory::create_solana_signer(&signer_model).unwrap();
338
339        match signer {
340            SolanaSigner::Turnkey(_) => {}
341            _ => panic!("Expected Turnkey signer"),
342        }
343    }
344
345    #[test]
346    fn test_create_solana_signer_cdp() {
347        let signer_model = SignerDomainModel {
348            id: "test".to_string(),
349            config: SignerConfig::Cdp(CdpSignerConfig {
350                api_key_id: "test-api-key-id".to_string(),
351                api_key_secret: SecretString::new("test-api-key-secret"),
352                wallet_secret: SecretString::new("test-wallet-secret"),
353                account_address: "6s7RsvzcdXFJi1tXeDoGfSKZFzN3juVt9fTar6WEhEm2".to_string(),
354            }),
355        };
356
357        let signer = SolanaSignerFactory::create_solana_signer(&signer_model).unwrap();
358
359        match signer {
360            SolanaSigner::Cdp(_) => {}
361            _ => panic!("Expected CDP signer"),
362        }
363    }
364
365    #[tokio::test]
366    async fn test_create_solana_signer_google_cloud_kms() {
367        let signer_model = SignerDomainModel {
368            id: "test".to_string(),
369            config: SignerConfig::GoogleCloudKms(GoogleCloudKmsSignerConfig {
370                service_account: GoogleCloudKmsSignerServiceAccountConfig {
371                    project_id: "project_id".to_string(),
372                    private_key_id: SecretString::new("private_key_id"),
373                    private_key: SecretString::new("private_key"),
374                    client_email: SecretString::new("client_email"),
375                    client_id: "client_id".to_string(),
376                    auth_uri: "auth_uri".to_string(),
377                    token_uri: "token_uri".to_string(),
378                    auth_provider_x509_cert_url: "auth_provider_x509_cert_url".to_string(),
379                    client_x509_cert_url: "client_x509_cert_url".to_string(),
380                    universe_domain: "universe_domain".to_string(),
381                },
382                key: GoogleCloudKmsSignerKeyConfig {
383                    location: "global".to_string(),
384                    key_id: "id".to_string(),
385                    key_ring_id: "key_ring".to_string(),
386                    key_version: 1,
387                },
388            }),
389        };
390
391        let signer = SolanaSignerFactory::create_solana_signer(&signer_model).unwrap();
392
393        match signer {
394            SolanaSigner::GoogleCloudKms(_) => {}
395            _ => panic!("Expected Google Cloud KMS signer"),
396        }
397    }
398
399    #[tokio::test]
400    async fn test_address_solana_signer_local() {
401        let signer_model = SignerDomainModel {
402            id: "test".to_string(),
403            config: SignerConfig::Local(LocalSignerConfig {
404                raw_key: test_key_bytes(),
405            }),
406        };
407
408        let signer = SolanaSignerFactory::create_solana_signer(&signer_model).unwrap();
409        let signer_address = signer.address().await.unwrap();
410        let signer_pubkey = signer.pubkey().await.unwrap();
411
412        assert_eq!(test_key_bytes_pubkey(), signer_address);
413        assert_eq!(test_key_bytes_pubkey(), signer_pubkey);
414    }
415
416    #[tokio::test]
417    async fn test_address_solana_signer_vault_transit() {
418        let signer_model = SignerDomainModel {
419            id: "test".to_string(),
420            config: SignerConfig::VaultTransit(VaultTransitSignerConfig {
421                key_name: "test".to_string(),
422                address: "address".to_string(),
423                namespace: None,
424                role_id: SecretString::new("role_id"),
425                secret_id: SecretString::new("secret_id"),
426                pubkey: "fV060x5X3Eo4uK/kTqQbSVL/qmMNaYKF2oaTa15hNfU=".to_string(),
427                mount_point: None,
428            }),
429        };
430        let expected_pubkey =
431            Address::Solana("9SNR5Sf993aphA7hzWSQsGv63x93trfuN8WjaToXcqKA".to_string());
432
433        let signer = SolanaSignerFactory::create_solana_signer(&signer_model).unwrap();
434        let signer_address = signer.address().await.unwrap();
435        let signer_pubkey = signer.pubkey().await.unwrap();
436
437        assert_eq!(expected_pubkey, signer_address);
438        assert_eq!(expected_pubkey, signer_pubkey);
439    }
440
441    #[tokio::test]
442    async fn test_address_solana_signer_turnkey() {
443        let signer_model = SignerDomainModel {
444            id: "test".to_string(),
445            config: SignerConfig::Turnkey(TurnkeySignerConfig {
446                api_private_key: SecretString::new("api_private_key"),
447                api_public_key: "api_public_key".to_string(),
448                organization_id: "organization_id".to_string(),
449                private_key_id: "private_key_id".to_string(),
450                public_key: "5720be8aa9d2bb4be8e91f31d2c44c8629e42da16981c2cebabd55cafa0b76bd"
451                    .to_string(),
452            }),
453        };
454        let expected_pubkey =
455            Address::Solana("6s7RsvzcdXFJi1tXeDoGfSKZFzN3juVt9fTar6WEhEm2".to_string());
456
457        let signer = SolanaSignerFactory::create_solana_signer(&signer_model).unwrap();
458        let signer_address = signer.address().await.unwrap();
459        let signer_pubkey = signer.pubkey().await.unwrap();
460
461        assert_eq!(expected_pubkey, signer_address);
462        assert_eq!(expected_pubkey, signer_pubkey);
463    }
464
465    #[tokio::test]
466    async fn test_address_solana_signer_cdp() {
467        let signer_model = SignerDomainModel {
468            id: "test".to_string(),
469            config: SignerConfig::Cdp(CdpSignerConfig {
470                api_key_id: "test-api-key-id".to_string(),
471                api_key_secret: SecretString::new("test-api-key-secret"),
472                wallet_secret: SecretString::new("test-wallet-secret"),
473                account_address: "6s7RsvzcdXFJi1tXeDoGfSKZFzN3juVt9fTar6WEhEm2".to_string(),
474            }),
475        };
476        let expected_pubkey =
477            Address::Solana("6s7RsvzcdXFJi1tXeDoGfSKZFzN3juVt9fTar6WEhEm2".to_string());
478
479        let signer = SolanaSignerFactory::create_solana_signer(&signer_model).unwrap();
480        let signer_address = signer.address().await.unwrap();
481        let signer_pubkey = signer.pubkey().await.unwrap();
482
483        assert_eq!(expected_pubkey, signer_address);
484        assert_eq!(expected_pubkey, signer_pubkey);
485    }
486
487    #[tokio::test]
488    async fn test_address_solana_signer_google_cloud_kms() {
489        let signer_model = SignerDomainModel {
490            id: "test".to_string(),
491            config: SignerConfig::GoogleCloudKms(GoogleCloudKmsSignerConfig {
492                service_account: GoogleCloudKmsSignerServiceAccountConfig {
493                    project_id: "project_id".to_string(),
494                    private_key_id: SecretString::new("private_key_id"),
495                    private_key: SecretString::new("private_key"),
496                    client_email: SecretString::new("client_email"),
497                    client_id: "client_id".to_string(),
498                    auth_uri: "auth_uri".to_string(),
499                    token_uri: "token_uri".to_string(),
500                    auth_provider_x509_cert_url: "auth_provider_x509_cert_url".to_string(),
501                    client_x509_cert_url: "client_x509_cert_url".to_string(),
502                    universe_domain: "universe_domain".to_string(),
503                },
504                key: GoogleCloudKmsSignerKeyConfig {
505                    location: "global".to_string(),
506                    key_id: "id".to_string(),
507                    key_ring_id: "key_ring".to_string(),
508                    key_version: 1,
509                },
510            }),
511        };
512
513        let signer = SolanaSignerFactory::create_solana_signer(&signer_model).unwrap();
514        let signer_address = signer.address().await;
515        let signer_pubkey = signer.pubkey().await;
516
517        // should fail due to call to google cloud
518        assert!(signer_address.is_err());
519        assert!(signer_pubkey.is_err());
520    }
521
522    #[tokio::test]
523    async fn test_sign_solana_signer_local() {
524        let signer_model = SignerDomainModel {
525            id: "test".to_string(),
526            config: SignerConfig::Local(LocalSignerConfig {
527                raw_key: test_key_bytes(),
528            }),
529        };
530
531        let signer = SolanaSignerFactory::create_solana_signer(&signer_model).unwrap();
532        let message = b"test message";
533        let signature = signer.sign(message).await;
534
535        assert!(signature.is_ok());
536    }
537
538    #[tokio::test]
539    async fn test_sign_solana_signer_test() {
540        let signer_model = SignerDomainModel {
541            id: "test".to_string(),
542            config: SignerConfig::Local(LocalSignerConfig {
543                raw_key: test_key_bytes(),
544            }),
545        };
546
547        let signer = SolanaSignerFactory::create_solana_signer(&signer_model).unwrap();
548        let message = b"test message";
549        let signature = signer.sign(message).await;
550
551        assert!(signature.is_ok());
552    }
553}