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