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