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