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_program::message::compiled_instruction::CompiledInstruction;
37use solana_sdk::pubkey::Pubkey;
38use solana_sdk::signature::Signature;
39use solana_sdk::transaction::Transaction as SolanaTransaction;
40use std::str::FromStr;
41
42use solana_system_interface::instruction as system_instruction;
43
44use crate::{
45    domain::{
46        SignDataRequest, SignDataResponse, SignDataResponseEvm, SignTransactionResponse,
47        SignTransactionResponseSolana, SignTypedDataRequest,
48    },
49    models::{
50        Address, EncodedSerializedTransaction, NetworkTransactionData, Signer as SignerDomainModel,
51        SignerConfig, SignerRepoModel, SignerType, TransactionRepoModel, VaultSignerConfig,
52    },
53    services::{CdpService, GoogleCloudKmsService, TurnkeyService, VaultConfig, VaultService},
54};
55use eyre::Result;
56
57use super::{Signer, SignerError, SignerFactoryError};
58#[cfg(test)]
59use mockall::automock;
60
61#[derive(Debug)]
62pub enum SolanaSigner {
63    Local(LocalSigner),
64    Vault(VaultSigner<VaultService>),
65    VaultTransit(VaultTransitSigner),
66    Turnkey(TurnkeySigner),
67    Cdp(CdpSigner),
68    GoogleCloudKms(GoogleCloudKmsSigner),
69}
70
71#[async_trait]
72impl Signer for SolanaSigner {
73    async fn address(&self) -> Result<Address, SignerError> {
74        // Delegate to SolanaSignTrait::pubkey() which all inner types implement
75        self.pubkey().await
76    }
77
78    async fn sign_transaction(
79        &self,
80        transaction: NetworkTransactionData,
81    ) -> Result<SignTransactionResponse, SignerError> {
82        // Extract Solana transaction data
83        let solana_data = transaction.get_solana_transaction_data().map_err(|e| {
84            SignerError::SigningError(format!("Invalid transaction type for Solana signer: {e}"))
85        })?;
86
87        // Get the pre-built transaction string
88        let transaction_str = solana_data.transaction.ok_or_else(|| {
89            SignerError::SigningError(
90                "Transaction not yet built - only available after preparation".to_string(),
91            )
92        })?;
93
94        // Decode transaction from base64
95        let encoded_tx = EncodedSerializedTransaction::new(transaction_str);
96        let sdk_transaction = SolanaTransaction::try_from(encoded_tx)
97            .map_err(|e| SignerError::SigningError(format!("Failed to decode transaction: {e}")))?;
98
99        // Sign using the SDK transaction signing helper function
100        let (signed_tx, signature) = sign_sdk_transaction(self, sdk_transaction).await?;
101
102        // Encode back to base64
103        let encoded_signed_tx =
104            EncodedSerializedTransaction::try_from(&signed_tx).map_err(|e| {
105                SignerError::SigningError(format!("Failed to encode signed transaction: {e}"))
106            })?;
107
108        // Return Solana-specific response
109        Ok(SignTransactionResponse::Solana(
110            SignTransactionResponseSolana {
111                transaction: encoded_signed_tx,
112                signature: signature.to_string(),
113            },
114        ))
115    }
116}
117
118#[async_trait]
119#[cfg_attr(test, automock)]
120/// Trait defining Solana-specific signing operations
121///
122/// This trait extends the basic signing functionality with methods specific
123/// to the Solana blockchain, including public key retrieval and message signing.
124pub trait SolanaSignTrait: Sync + Send {
125    /// Returns the public key of the Solana signer as an Address
126    async fn pubkey(&self) -> Result<Address, SignerError>;
127
128    /// Signs a message using the Solana signing scheme
129    ///
130    /// # Arguments
131    ///
132    /// * `message` - The message bytes to sign
133    ///
134    /// # Returns
135    ///
136    /// A Result containing either the Solana Signature or a SignerError
137    async fn sign(&self, message: &[u8]) -> Result<Signature, SignerError>;
138}
139
140/// Signs a raw Solana SDK transaction by finding the signer's position and adding the signature
141///
142/// This helper function:
143/// 1. Retrieves the signer's public key
144/// 2. Finds its position in the transaction's account_keys
145/// 3. Validates it's marked as a required signer
146/// 4. Signs the transaction message
147/// 5. Inserts the signature at the correct position
148///
149/// # Arguments
150///
151/// * `signer` - A type implementing SolanaSignTrait
152/// * `transaction` - The Solana SDK transaction to sign
153///
154/// # Returns
155///
156/// A Result containing either a tuple of (signed Transaction, Signature) or a SignerError
157///
158/// # Note
159///
160/// This is distinct from the `Signer::sign_transaction` method which operates on domain models.
161/// This function works directly with `solana_sdk::transaction::Transaction`.
162pub async fn sign_sdk_transaction<T: SolanaSignTrait + ?Sized>(
163    signer: &T,
164    mut transaction: solana_sdk::transaction::Transaction,
165) -> Result<(solana_sdk::transaction::Transaction, Signature), SignerError> {
166    // Get signer's public key
167    let signer_address = signer.pubkey().await?;
168    let signer_pubkey = Pubkey::from_str(&signer_address.to_string())
169        .map_err(|e| SignerError::KeyError(format!("Invalid signer address: {e}")))?;
170
171    // Find the position of the signer's public key in account_keys
172    let signer_index = transaction
173        .message
174        .account_keys
175        .iter()
176        .position(|key| *key == signer_pubkey)
177        .ok_or_else(|| {
178            SignerError::SigningError(
179                "Signer public key not found in transaction signers".to_string(),
180            )
181        })?;
182
183    // Check if this is a signer position (within num_required_signatures)
184    if signer_index >= transaction.message.header.num_required_signatures as usize {
185        return Err(SignerError::SigningError(format!(
186            "Signer is not marked as a required signer in the transaction (position {} >= {})",
187            signer_index, transaction.message.header.num_required_signatures
188        )));
189    }
190
191    // Generate signature
192    let signature = signer.sign(&transaction.message_data()).await?;
193
194    // Ensure signatures array has exactly num_required_signatures slots
195    // This preserves any existing signatures and doesn't shrink the array
196    let num_required = transaction.message.header.num_required_signatures as usize;
197    transaction
198        .signatures
199        .resize(num_required, Signature::default());
200
201    // Set our signature at the correct index
202    transaction.signatures[signer_index] = signature;
203
204    Ok((transaction, signature))
205}
206
207#[async_trait]
208impl SolanaSignTrait for SolanaSigner {
209    async fn pubkey(&self) -> Result<Address, SignerError> {
210        match self {
211            Self::Local(signer) => signer.pubkey().await,
212            Self::Vault(signer) => signer.pubkey().await,
213            Self::VaultTransit(signer) => signer.pubkey().await,
214            Self::Turnkey(signer) => signer.pubkey().await,
215            Self::Cdp(signer) => signer.pubkey().await,
216            Self::GoogleCloudKms(signer) => signer.pubkey().await,
217        }
218    }
219
220    async fn sign(&self, message: &[u8]) -> Result<Signature, SignerError> {
221        match self {
222            Self::Local(signer) => Ok(signer.sign(message).await?),
223            Self::Vault(signer) => Ok(signer.sign(message).await?),
224            Self::VaultTransit(signer) => Ok(signer.sign(message).await?),
225            Self::Turnkey(signer) => Ok(signer.sign(message).await?),
226            Self::Cdp(signer) => Ok(signer.sign(message).await?),
227            Self::GoogleCloudKms(signer) => Ok(signer.sign(message).await?),
228        }
229    }
230}
231
232pub struct SolanaSignerFactory;
233
234impl SolanaSignerFactory {
235    pub fn create_solana_signer(
236        signer_model: &SignerDomainModel,
237    ) -> Result<SolanaSigner, SignerFactoryError> {
238        let signer = match &signer_model.config {
239            SignerConfig::Local(_) => SolanaSigner::Local(LocalSigner::new(signer_model)?),
240            SignerConfig::Vault(config) => {
241                let vault_config = VaultConfig::new(
242                    config.address.clone(),
243                    config.role_id.clone(),
244                    config.secret_id.clone(),
245                    config.namespace.clone(),
246                    config
247                        .mount_point
248                        .clone()
249                        .unwrap_or_else(|| "secret".to_string()),
250                    None,
251                );
252                let vault_service = VaultService::new(vault_config);
253
254                return Ok(SolanaSigner::Vault(VaultSigner::new(
255                    signer_model.id.clone(),
256                    config.clone(),
257                    vault_service,
258                )));
259            }
260            SignerConfig::VaultTransit(vault_transit_signer_config) => {
261                let vault_service = VaultService::new(VaultConfig {
262                    address: vault_transit_signer_config.address.clone(),
263                    namespace: vault_transit_signer_config.namespace.clone(),
264                    role_id: vault_transit_signer_config.role_id.clone(),
265                    secret_id: vault_transit_signer_config.secret_id.clone(),
266                    mount_path: "transit".to_string(),
267                    token_ttl: None,
268                });
269
270                return Ok(SolanaSigner::VaultTransit(VaultTransitSigner::new(
271                    signer_model,
272                    vault_service,
273                )));
274            }
275            SignerConfig::AwsKms(_) => {
276                return Err(SignerFactoryError::UnsupportedType("AWS KMS".into()));
277            }
278            SignerConfig::Cdp(config) => {
279                let cdp_signer = CdpSigner::new(config.clone()).map_err(|e| {
280                    SignerFactoryError::CreationFailed(format!("CDP service error: {e}"))
281                })?;
282                return Ok(SolanaSigner::Cdp(cdp_signer));
283            }
284            SignerConfig::Turnkey(turnkey_signer_config) => {
285                let turnkey_service =
286                    TurnkeyService::new(turnkey_signer_config.clone()).map_err(|e| {
287                        SignerFactoryError::InvalidConfig(format!(
288                            "Failed to create Turnkey service: {e}"
289                        ))
290                    })?;
291
292                return Ok(SolanaSigner::Turnkey(TurnkeySigner::new(turnkey_service)));
293            }
294            SignerConfig::GoogleCloudKms(google_cloud_kms_signer_config) => {
295                let google_cloud_kms_service =
296                    GoogleCloudKmsService::new(google_cloud_kms_signer_config).map_err(|e| {
297                        SignerFactoryError::InvalidConfig(format!(
298                            "Failed to create Google Cloud KMS service: {e}"
299                        ))
300                    })?;
301                return Ok(SolanaSigner::GoogleCloudKms(GoogleCloudKmsSigner::new(
302                    google_cloud_kms_service,
303                )));
304            }
305        };
306
307        Ok(signer)
308    }
309}
310
311#[cfg(test)]
312mod solana_signer_factory_tests {
313    use super::*;
314    use crate::models::{
315        AwsKmsSignerConfig, CdpSignerConfig, GoogleCloudKmsSignerConfig,
316        GoogleCloudKmsSignerKeyConfig, GoogleCloudKmsSignerServiceAccountConfig, LocalSignerConfig,
317        SecretString, SignerConfig, SignerRepoModel, SolanaTransactionData, TurnkeySignerConfig,
318        VaultSignerConfig, VaultTransitSignerConfig,
319    };
320    use mockall::predicate::*;
321    use secrets::SecretVec;
322    use std::str::FromStr;
323    use std::sync::Arc;
324
325    fn test_key_bytes() -> SecretVec<u8> {
326        let key_bytes = vec![
327            1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22, 23, 24,
328            25, 26, 27, 28, 29, 30, 31, 32,
329        ];
330        SecretVec::new(key_bytes.len(), |v| v.copy_from_slice(&key_bytes))
331    }
332
333    fn test_key_bytes_pubkey() -> Address {
334        Address::Solana("9C6hybhQ6Aycep9jaUnP6uL9ZYvDjUp1aSkFWPUFJtpj".to_string())
335    }
336
337    #[test]
338    fn test_create_solana_signer_local() {
339        let signer_model = SignerDomainModel {
340            id: "test".to_string(),
341            config: SignerConfig::Local(LocalSignerConfig {
342                raw_key: test_key_bytes(),
343            }),
344        };
345
346        let signer = SolanaSignerFactory::create_solana_signer(&signer_model).unwrap();
347
348        match signer {
349            SolanaSigner::Local(_) => {}
350            _ => panic!("Expected Local signer"),
351        }
352    }
353
354    #[test]
355    fn test_create_solana_signer_vault() {
356        let signer_model = SignerDomainModel {
357            id: "test".to_string(),
358            config: SignerConfig::Vault(VaultSignerConfig {
359                address: "https://vault.test.com".to_string(),
360                namespace: Some("test-namespace".to_string()),
361                role_id: crate::models::SecretString::new("test-role-id"),
362                secret_id: crate::models::SecretString::new("test-secret-id"),
363                key_name: "test-key".to_string(),
364                mount_point: Some("secret".to_string()),
365            }),
366        };
367
368        let signer = SolanaSignerFactory::create_solana_signer(&signer_model).unwrap();
369
370        match signer {
371            SolanaSigner::Vault(_) => {}
372            _ => panic!("Expected Vault signer"),
373        }
374    }
375
376    #[test]
377    fn test_create_solana_signer_vault_transit() {
378        let signer_model = SignerDomainModel {
379            id: "test".to_string(),
380            config: SignerConfig::VaultTransit(VaultTransitSignerConfig {
381                key_name: "test".to_string(),
382                address: "address".to_string(),
383                namespace: None,
384                role_id: SecretString::new("role_id"),
385                secret_id: SecretString::new("secret_id"),
386                pubkey: "pubkey".to_string(),
387                mount_point: None,
388            }),
389        };
390
391        let signer = SolanaSignerFactory::create_solana_signer(&signer_model).unwrap();
392
393        match signer {
394            SolanaSigner::VaultTransit(_) => {}
395            _ => panic!("Expected Transit signer"),
396        }
397    }
398
399    #[test]
400    fn test_create_solana_signer_turnkey() {
401        let signer_model = SignerDomainModel {
402            id: "test".to_string(),
403            config: SignerConfig::Turnkey(TurnkeySignerConfig {
404                api_private_key: SecretString::new("api_private_key"),
405                api_public_key: "api_public_key".to_string(),
406                organization_id: "organization_id".to_string(),
407                private_key_id: "private_key_id".to_string(),
408                public_key: "public_key".to_string(),
409            }),
410        };
411
412        let signer = SolanaSignerFactory::create_solana_signer(&signer_model).unwrap();
413
414        match signer {
415            SolanaSigner::Turnkey(_) => {}
416            _ => panic!("Expected Turnkey signer"),
417        }
418    }
419
420    #[test]
421    fn test_create_solana_signer_cdp() {
422        let signer_model = SignerDomainModel {
423            id: "test".to_string(),
424            config: SignerConfig::Cdp(CdpSignerConfig {
425                api_key_id: "test-api-key-id".to_string(),
426                api_key_secret: SecretString::new("test-api-key-secret"),
427                wallet_secret: SecretString::new("test-wallet-secret"),
428                account_address: "6s7RsvzcdXFJi1tXeDoGfSKZFzN3juVt9fTar6WEhEm2".to_string(),
429            }),
430        };
431
432        let signer = SolanaSignerFactory::create_solana_signer(&signer_model).unwrap();
433
434        match signer {
435            SolanaSigner::Cdp(_) => {}
436            _ => panic!("Expected CDP signer"),
437        }
438    }
439
440    #[tokio::test]
441    async fn test_create_solana_signer_google_cloud_kms() {
442        let signer_model = SignerDomainModel {
443            id: "test".to_string(),
444            config: SignerConfig::GoogleCloudKms(GoogleCloudKmsSignerConfig {
445                service_account: GoogleCloudKmsSignerServiceAccountConfig {
446                    project_id: "project_id".to_string(),
447                    private_key_id: SecretString::new("private_key_id"),
448                    private_key: SecretString::new("private_key"),
449                    client_email: SecretString::new("client_email"),
450                    client_id: "client_id".to_string(),
451                    auth_uri: "auth_uri".to_string(),
452                    token_uri: "token_uri".to_string(),
453                    auth_provider_x509_cert_url: "auth_provider_x509_cert_url".to_string(),
454                    client_x509_cert_url: "client_x509_cert_url".to_string(),
455                    universe_domain: "universe_domain".to_string(),
456                },
457                key: GoogleCloudKmsSignerKeyConfig {
458                    location: "global".to_string(),
459                    key_id: "id".to_string(),
460                    key_ring_id: "key_ring".to_string(),
461                    key_version: 1,
462                },
463            }),
464        };
465
466        let signer = SolanaSignerFactory::create_solana_signer(&signer_model).unwrap();
467
468        match signer {
469            SolanaSigner::GoogleCloudKms(_) => {}
470            _ => panic!("Expected Google Cloud KMS signer"),
471        }
472    }
473
474    #[tokio::test]
475    async fn test_address_solana_signer_local() {
476        let signer_model = SignerDomainModel {
477            id: "test".to_string(),
478            config: SignerConfig::Local(LocalSignerConfig {
479                raw_key: test_key_bytes(),
480            }),
481        };
482
483        let signer = SolanaSignerFactory::create_solana_signer(&signer_model).unwrap();
484        let signer_address = signer.address().await.unwrap();
485        let signer_pubkey = signer.pubkey().await.unwrap();
486
487        assert_eq!(test_key_bytes_pubkey(), signer_address);
488        assert_eq!(test_key_bytes_pubkey(), signer_pubkey);
489    }
490
491    #[tokio::test]
492    async fn test_address_solana_signer_vault_transit() {
493        let signer_model = SignerDomainModel {
494            id: "test".to_string(),
495            config: SignerConfig::VaultTransit(VaultTransitSignerConfig {
496                key_name: "test".to_string(),
497                address: "address".to_string(),
498                namespace: None,
499                role_id: SecretString::new("role_id"),
500                secret_id: SecretString::new("secret_id"),
501                pubkey: "fV060x5X3Eo4uK/kTqQbSVL/qmMNaYKF2oaTa15hNfU=".to_string(),
502                mount_point: None,
503            }),
504        };
505        let expected_pubkey =
506            Address::Solana("9SNR5Sf993aphA7hzWSQsGv63x93trfuN8WjaToXcqKA".to_string());
507
508        let signer = SolanaSignerFactory::create_solana_signer(&signer_model).unwrap();
509        let signer_address = signer.address().await.unwrap();
510        let signer_pubkey = signer.pubkey().await.unwrap();
511
512        assert_eq!(expected_pubkey, signer_address);
513        assert_eq!(expected_pubkey, signer_pubkey);
514    }
515
516    #[tokio::test]
517    async fn test_address_solana_signer_turnkey() {
518        let signer_model = SignerDomainModel {
519            id: "test".to_string(),
520            config: SignerConfig::Turnkey(TurnkeySignerConfig {
521                api_private_key: SecretString::new("api_private_key"),
522                api_public_key: "api_public_key".to_string(),
523                organization_id: "organization_id".to_string(),
524                private_key_id: "private_key_id".to_string(),
525                public_key: "5720be8aa9d2bb4be8e91f31d2c44c8629e42da16981c2cebabd55cafa0b76bd"
526                    .to_string(),
527            }),
528        };
529        let expected_pubkey =
530            Address::Solana("6s7RsvzcdXFJi1tXeDoGfSKZFzN3juVt9fTar6WEhEm2".to_string());
531
532        let signer = SolanaSignerFactory::create_solana_signer(&signer_model).unwrap();
533        let signer_address = signer.address().await.unwrap();
534        let signer_pubkey = signer.pubkey().await.unwrap();
535
536        assert_eq!(expected_pubkey, signer_address);
537        assert_eq!(expected_pubkey, signer_pubkey);
538    }
539
540    #[tokio::test]
541    async fn test_address_solana_signer_cdp() {
542        let signer_model = SignerDomainModel {
543            id: "test".to_string(),
544            config: SignerConfig::Cdp(CdpSignerConfig {
545                api_key_id: "test-api-key-id".to_string(),
546                api_key_secret: SecretString::new("test-api-key-secret"),
547                wallet_secret: SecretString::new("test-wallet-secret"),
548                account_address: "6s7RsvzcdXFJi1tXeDoGfSKZFzN3juVt9fTar6WEhEm2".to_string(),
549            }),
550        };
551        let expected_pubkey =
552            Address::Solana("6s7RsvzcdXFJi1tXeDoGfSKZFzN3juVt9fTar6WEhEm2".to_string());
553
554        let signer = SolanaSignerFactory::create_solana_signer(&signer_model).unwrap();
555        let signer_address = signer.address().await.unwrap();
556        let signer_pubkey = signer.pubkey().await.unwrap();
557
558        assert_eq!(expected_pubkey, signer_address);
559        assert_eq!(expected_pubkey, signer_pubkey);
560    }
561
562    #[tokio::test]
563    async fn test_address_solana_signer_google_cloud_kms() {
564        let signer_model = SignerDomainModel {
565            id: "test".to_string(),
566            config: SignerConfig::GoogleCloudKms(GoogleCloudKmsSignerConfig {
567                service_account: GoogleCloudKmsSignerServiceAccountConfig {
568                    project_id: "project_id".to_string(),
569                    private_key_id: SecretString::new("private_key_id"),
570                    private_key: SecretString::new("private_key"),
571                    client_email: SecretString::new("client_email"),
572                    client_id: "client_id".to_string(),
573                    auth_uri: "auth_uri".to_string(),
574                    token_uri: "token_uri".to_string(),
575                    auth_provider_x509_cert_url: "auth_provider_x509_cert_url".to_string(),
576                    client_x509_cert_url: "client_x509_cert_url".to_string(),
577                    universe_domain: "universe_domain".to_string(),
578                },
579                key: GoogleCloudKmsSignerKeyConfig {
580                    location: "global".to_string(),
581                    key_id: "id".to_string(),
582                    key_ring_id: "key_ring".to_string(),
583                    key_version: 1,
584                },
585            }),
586        };
587
588        let signer = SolanaSignerFactory::create_solana_signer(&signer_model).unwrap();
589        let signer_address = signer.address().await;
590        let signer_pubkey = signer.pubkey().await;
591
592        // should fail due to call to google cloud
593        assert!(signer_address.is_err());
594        assert!(signer_pubkey.is_err());
595    }
596
597    #[tokio::test]
598    async fn test_sign_solana_signer_local() {
599        let signer_model = SignerDomainModel {
600            id: "test".to_string(),
601            config: SignerConfig::Local(LocalSignerConfig {
602                raw_key: test_key_bytes(),
603            }),
604        };
605
606        let signer = SolanaSignerFactory::create_solana_signer(&signer_model).unwrap();
607        let message = b"test message";
608        let signature = signer.sign(message).await;
609
610        assert!(signature.is_ok());
611    }
612
613    #[tokio::test]
614    async fn test_sign_solana_signer_test() {
615        let signer_model = SignerDomainModel {
616            id: "test".to_string(),
617            config: SignerConfig::Local(LocalSignerConfig {
618                raw_key: test_key_bytes(),
619            }),
620        };
621
622        let signer = SolanaSignerFactory::create_solana_signer(&signer_model).unwrap();
623        let message = b"test message";
624        let signature = signer.sign(message).await;
625
626        assert!(signature.is_ok());
627    }
628
629    #[tokio::test]
630    async fn test_sign_sdk_transaction_success() {
631        use solana_sdk::message::Message;
632        use solana_sdk::pubkey::Pubkey;
633        use solana_sdk::signature::Signature;
634        use solana_sdk::transaction::Transaction;
635
636        // Create a mock signer
637        let signer_model = SignerDomainModel {
638            id: "test".to_string(),
639            config: SignerConfig::Local(LocalSignerConfig {
640                raw_key: test_key_bytes(),
641            }),
642        };
643        let signer = SolanaSignerFactory::create_solana_signer(&signer_model).unwrap();
644
645        // Create a simple transaction with our signer as the first account
646        let signer_pubkey = Pubkey::from_str(&test_key_bytes_pubkey().to_string()).unwrap();
647        let recipient = Pubkey::new_unique();
648
649        let message = Message::new(
650            &[solana_system_interface::instruction::transfer(
651                &signer_pubkey,
652                &recipient,
653                1000,
654            )],
655            Some(&signer_pubkey),
656        );
657        let transaction = Transaction::new_unsigned(message);
658
659        // Sign the transaction
660        let result = sign_sdk_transaction(&signer, transaction).await;
661        assert!(result.is_ok());
662
663        let (signed_tx, signature) = result.unwrap();
664        assert!(!signature.to_string().is_empty());
665        assert_eq!(signed_tx.signatures.len(), 1);
666        assert_eq!(signed_tx.signatures[0], signature);
667    }
668
669    #[tokio::test]
670    async fn test_sign_sdk_transaction_signer_not_in_accounts() {
671        use solana_sdk::message::Message;
672        use solana_sdk::pubkey::Pubkey;
673        use solana_sdk::transaction::Transaction;
674
675        // Create a mock signer
676        let signer_model = SignerDomainModel {
677            id: "test".to_string(),
678            config: SignerConfig::Local(LocalSignerConfig {
679                raw_key: test_key_bytes(),
680            }),
681        };
682        let signer = SolanaSignerFactory::create_solana_signer(&signer_model).unwrap();
683
684        // Create a transaction where our signer is NOT in the account keys
685        let other_pubkey = Pubkey::new_unique();
686        let recipient = Pubkey::new_unique();
687
688        let message = Message::new(
689            &[solana_system_interface::instruction::transfer(
690                &other_pubkey,
691                &recipient,
692                1000,
693            )],
694            Some(&other_pubkey),
695        );
696        let transaction = Transaction::new_unsigned(message);
697
698        // Try to sign - should fail because signer is not in account_keys
699        let result = sign_sdk_transaction(&signer, transaction).await;
700        assert!(result.is_err());
701        let error = result.unwrap_err();
702        match error {
703            SignerError::SigningError(msg) => {
704                assert!(msg.contains("Signer public key not found in transaction signers"));
705            }
706            _ => panic!("Expected SigningError, got {:?}", error),
707        }
708    }
709
710    #[tokio::test]
711    async fn test_sign_sdk_transaction_signer_not_required() {
712        use solana_sdk::message::Message;
713        use solana_sdk::pubkey::Pubkey;
714        use solana_sdk::transaction::Transaction;
715
716        // Create a mock signer
717        let signer_model = SignerDomainModel {
718            id: "test".to_string(),
719            config: SignerConfig::Local(LocalSignerConfig {
720                raw_key: test_key_bytes(),
721            }),
722        };
723        let signer = SolanaSignerFactory::create_solana_signer(&signer_model).unwrap();
724
725        // Create a transaction where our signer is in account_keys but NOT marked as required
726        let signer_pubkey = Pubkey::from_str(&test_key_bytes_pubkey().to_string()).unwrap();
727        let fee_payer = Pubkey::new_unique();
728        let recipient = Pubkey::new_unique();
729
730        // Create message with signer as a readonly account (not required signer)
731        // Use a different approach - create a message where signer is not the fee payer
732        let message = Message::new(
733            &[solana_system_interface::instruction::transfer(
734                &fee_payer, &recipient, 1000,
735            )],
736            Some(&fee_payer),
737        );
738        let transaction = Transaction::new_unsigned(message);
739
740        // Manually modify the message to include our signer as a readonly account
741        // This simulates a transaction where our signer is present but not required
742        let mut modified_message = transaction.message.clone();
743        modified_message.account_keys.push(signer_pubkey); // Add signer as additional account
744        modified_message.header.num_readonly_unsigned_accounts += 1; // Make it readonly unsigned
745
746        let modified_transaction = Transaction::new_unsigned(modified_message);
747
748        // Try to sign - should fail because signer is not a required signer
749        let result = sign_sdk_transaction(&signer, modified_transaction).await;
750        assert!(result.is_err());
751        let error = result.unwrap_err();
752        match error {
753            SignerError::SigningError(msg) => {
754                assert!(msg.contains("Signer is not marked as a required signer"));
755            }
756            _ => panic!("Expected SigningError, got {:?}", error),
757        }
758    }
759
760    #[tokio::test]
761    async fn test_sign_transaction_with_domain_model() {
762        use crate::models::{NetworkTransactionData, SolanaTransactionData};
763        use solana_sdk::message::Message;
764        use solana_sdk::pubkey::Pubkey;
765
766        // Create a mock signer
767        let signer_model = SignerDomainModel {
768            id: "test".to_string(),
769            config: SignerConfig::Local(LocalSignerConfig {
770                raw_key: test_key_bytes(),
771            }),
772        };
773        let signer = SolanaSignerFactory::create_solana_signer(&signer_model).unwrap();
774
775        // Create a domain transaction data
776        let signer_pubkey = Pubkey::from_str(&test_key_bytes_pubkey().to_string()).unwrap();
777        let recipient = Pubkey::new_unique();
778
779        let message = Message::new(
780            &[solana_system_interface::instruction::transfer(
781                &signer_pubkey,
782                &recipient,
783                1000,
784            )],
785            Some(&signer_pubkey),
786        );
787        let transaction = solana_sdk::transaction::Transaction::new_unsigned(message);
788        let encoded_tx =
789            crate::models::EncodedSerializedTransaction::try_from(&transaction).unwrap();
790
791        let solana_data = SolanaTransactionData {
792            transaction: Some(encoded_tx.into_inner()),
793            ..Default::default()
794        };
795
796        let network_data = NetworkTransactionData::Solana(solana_data);
797
798        // Sign using the domain model method
799        let result = signer.sign_transaction(network_data).await;
800        assert!(result.is_ok());
801
802        let response = result.unwrap();
803        match response {
804            crate::domain::SignTransactionResponse::Solana(solana_response) => {
805                assert!(!solana_response.transaction.into_inner().is_empty());
806                assert!(!solana_response.signature.is_empty());
807            }
808            _ => panic!("Expected Solana response"),
809        }
810    }
811
812    #[test]
813    fn test_create_solana_signer_aws_kms_unsupported() {
814        let signer_model = SignerDomainModel {
815            id: "test".to_string(),
816            config: SignerConfig::AwsKms(AwsKmsSignerConfig {
817                region: Some("us-east-1".to_string()),
818                key_id: "test-key-id".to_string(),
819            }),
820        };
821
822        let result = SolanaSignerFactory::create_solana_signer(&signer_model);
823        assert!(result.is_err());
824        let error = result.unwrap_err();
825        match error {
826            SignerFactoryError::UnsupportedType(msg) => {
827                assert_eq!(msg, "AWS KMS");
828            }
829            _ => panic!("Expected UnsupportedType error, got {:?}", error),
830        }
831    }
832
833    #[cfg(test)]
834    #[async_trait]
835    impl Signer for MockSolanaSignTrait {
836        async fn address(&self) -> Result<Address, SignerError> {
837            self.pubkey().await
838        }
839
840        async fn sign_transaction(
841            &self,
842            _transaction: NetworkTransactionData,
843        ) -> Result<SignTransactionResponse, SignerError> {
844            // For testing, return a mock response
845            Ok(SignTransactionResponse::Solana(
846                crate::domain::SignTransactionResponseSolana {
847                    transaction: crate::models::EncodedSerializedTransaction::new(
848                        "signed_transaction_data".to_string(),
849                    ),
850                    signature: "signature_data".to_string(),
851                },
852            ))
853        }
854    }
855}