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(Box<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 async fn create_solana_signer(
244        signer_model: SignerDomainModel,
245    ) -> Result<SolanaSigner, SignerFactoryError> {
246        // Taken by value (like create_evm_signer): an `async fn` holding a
247        // `&SignerDomainModel` across `.await` is `!Send` because `Signer` is `!Sync`,
248        // and these futures are spawned on the multi-thread runtime.
249        let signer = match &signer_model.config {
250            SignerConfig::Local(_) => SolanaSigner::Local(LocalSigner::new(&signer_model)?),
251            SignerConfig::Vault(config) => {
252                let vault_config = VaultConfig::new(
253                    config.address.clone(),
254                    config.role_id.clone(),
255                    config.secret_id.clone(),
256                    config.namespace.clone(),
257                    config
258                        .mount_point
259                        .clone()
260                        .unwrap_or_else(|| "secret".to_string()),
261                    None,
262                );
263                let vault_service = VaultService::new(vault_config);
264
265                return Ok(SolanaSigner::Vault(VaultSigner::new(
266                    signer_model.id.clone(),
267                    config.clone(),
268                    vault_service,
269                )));
270            }
271            SignerConfig::VaultTransit(vault_transit_signer_config) => {
272                let vault_service = VaultService::new(VaultConfig {
273                    address: vault_transit_signer_config.address.clone(),
274                    namespace: vault_transit_signer_config.namespace.clone(),
275                    role_id: vault_transit_signer_config.role_id.clone(),
276                    secret_id: vault_transit_signer_config.secret_id.clone(),
277                    mount_path: "transit".to_string(),
278                    token_ttl: None,
279                });
280
281                return Ok(SolanaSigner::VaultTransit(VaultTransitSigner::new(
282                    &signer_model,
283                    vault_service,
284                )));
285            }
286            SignerConfig::AwsKms(config) => {
287                // Async construction (mirrors create_evm_signer); see create_stellar_signer.
288                let aws_kms_service = AwsKmsService::new(config.clone()).await.map_err(|e| {
289                    SignerFactoryError::InvalidConfig(format!(
290                        "Failed to create AWS KMS service: {e}"
291                    ))
292                })?;
293                return Ok(SolanaSigner::AwsKms(AwsKmsSigner::new(aws_kms_service)));
294            }
295            SignerConfig::AzureKeyVault(_) => {
296                return Err(SignerFactoryError::UnsupportedType(
297                    "Azure Key Vault".into(),
298                ));
299            }
300            SignerConfig::Cdp(config) => {
301                let cdp_signer = CdpSigner::new(config.clone()).map_err(|e| {
302                    SignerFactoryError::CreationFailed(format!("CDP service error: {e}"))
303                })?;
304                return Ok(SolanaSigner::Cdp(cdp_signer));
305            }
306            SignerConfig::Turnkey(turnkey_signer_config) => {
307                let turnkey_service =
308                    TurnkeyService::new(turnkey_signer_config.clone()).map_err(|e| {
309                        SignerFactoryError::InvalidConfig(format!(
310                            "Failed to create Turnkey service: {e}"
311                        ))
312                    })?;
313
314                return Ok(SolanaSigner::Turnkey(TurnkeySigner::new(turnkey_service)));
315            }
316            SignerConfig::GoogleCloudKms(google_cloud_kms_signer_config) => {
317                let google_cloud_kms_service =
318                    GoogleCloudKmsService::new(google_cloud_kms_signer_config).map_err(|e| {
319                        SignerFactoryError::InvalidConfig(format!(
320                            "Failed to create Google Cloud KMS service: {e}"
321                        ))
322                    })?;
323                return Ok(SolanaSigner::GoogleCloudKms(Box::new(
324                    GoogleCloudKmsSigner::new(google_cloud_kms_service),
325                )));
326            }
327        };
328
329        Ok(signer)
330    }
331}
332
333#[cfg(test)]
334mod solana_signer_factory_tests {
335    use super::*;
336    use crate::models::{
337        AwsKmsSignerConfig, CdpSignerConfig, GoogleCloudKmsSignerConfig,
338        GoogleCloudKmsSignerKeyConfig, GoogleCloudKmsSignerServiceAccountConfig, LocalSignerConfig,
339        SecretString, SignerConfig, SignerRepoModel, SolanaTransactionData, TurnkeySignerConfig,
340        VaultSignerConfig, VaultTransitSignerConfig,
341    };
342    use mockall::predicate::*;
343    use secrets::SecretVec;
344    use std::str::FromStr;
345    use std::sync::Arc;
346
347    fn test_key_bytes() -> SecretVec<u8> {
348        let key_bytes = vec![
349            1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22, 23, 24,
350            25, 26, 27, 28, 29, 30, 31, 32,
351        ];
352        SecretVec::new(key_bytes.len(), |v| v.copy_from_slice(&key_bytes))
353    }
354
355    fn test_key_bytes_pubkey() -> Address {
356        Address::Solana("9C6hybhQ6Aycep9jaUnP6uL9ZYvDjUp1aSkFWPUFJtpj".to_string())
357    }
358
359    #[tokio::test]
360    async fn test_create_solana_signer_local() {
361        let signer_model = SignerDomainModel {
362            id: "test".to_string(),
363            config: SignerConfig::Local(LocalSignerConfig {
364                raw_key: test_key_bytes(),
365            }),
366        };
367
368        let signer = SolanaSignerFactory::create_solana_signer(signer_model.clone())
369            .await
370            .unwrap();
371
372        match signer {
373            SolanaSigner::Local(_) => {}
374            _ => panic!("Expected Local signer"),
375        }
376    }
377
378    #[tokio::test]
379    async fn test_create_solana_signer_vault() {
380        let signer_model = SignerDomainModel {
381            id: "test".to_string(),
382            config: SignerConfig::Vault(VaultSignerConfig {
383                address: "https://vault.test.com".to_string(),
384                namespace: Some("test-namespace".to_string()),
385                role_id: crate::models::SecretString::new("test-role-id"),
386                secret_id: crate::models::SecretString::new("test-secret-id"),
387                key_name: "test-key".to_string(),
388                mount_point: Some("secret".to_string()),
389            }),
390        };
391
392        let signer = SolanaSignerFactory::create_solana_signer(signer_model.clone())
393            .await
394            .unwrap();
395
396        match signer {
397            SolanaSigner::Vault(_) => {}
398            _ => panic!("Expected Vault signer"),
399        }
400    }
401
402    #[tokio::test]
403    async fn test_create_solana_signer_vault_transit() {
404        let signer_model = SignerDomainModel {
405            id: "test".to_string(),
406            config: SignerConfig::VaultTransit(VaultTransitSignerConfig {
407                key_name: "test".to_string(),
408                address: "address".to_string(),
409                namespace: None,
410                role_id: SecretString::new("role_id"),
411                secret_id: SecretString::new("secret_id"),
412                pubkey: "pubkey".to_string(),
413                mount_point: None,
414            }),
415        };
416
417        let signer = SolanaSignerFactory::create_solana_signer(signer_model.clone())
418            .await
419            .unwrap();
420
421        match signer {
422            SolanaSigner::VaultTransit(_) => {}
423            _ => panic!("Expected Transit signer"),
424        }
425    }
426
427    #[tokio::test]
428    async fn test_create_solana_signer_turnkey() {
429        let signer_model = SignerDomainModel {
430            id: "test".to_string(),
431            config: SignerConfig::Turnkey(TurnkeySignerConfig {
432                api_private_key: SecretString::new("api_private_key"),
433                api_public_key: "api_public_key".to_string(),
434                organization_id: "organization_id".to_string(),
435                private_key_id: "private_key_id".to_string(),
436                public_key: "public_key".to_string(),
437            }),
438        };
439
440        let signer = SolanaSignerFactory::create_solana_signer(signer_model.clone())
441            .await
442            .unwrap();
443
444        match signer {
445            SolanaSigner::Turnkey(_) => {}
446            _ => panic!("Expected Turnkey signer"),
447        }
448    }
449
450    #[tokio::test]
451    async fn test_create_solana_signer_cdp() {
452        let signer_model = SignerDomainModel {
453            id: "test".to_string(),
454            config: SignerConfig::Cdp(CdpSignerConfig {
455                api_key_id: "test-api-key-id".to_string(),
456                api_key_secret: SecretString::new("test-api-key-secret"),
457                wallet_secret: SecretString::new("test-wallet-secret"),
458                account_address: "6s7RsvzcdXFJi1tXeDoGfSKZFzN3juVt9fTar6WEhEm2".to_string(),
459            }),
460        };
461
462        let signer = SolanaSignerFactory::create_solana_signer(signer_model.clone())
463            .await
464            .unwrap();
465
466        match signer {
467            SolanaSigner::Cdp(_) => {}
468            _ => panic!("Expected CDP signer"),
469        }
470    }
471
472    #[tokio::test]
473    async fn test_create_solana_signer_google_cloud_kms() {
474        let signer_model = SignerDomainModel {
475            id: "test".to_string(),
476            config: SignerConfig::GoogleCloudKms(Box::new(GoogleCloudKmsSignerConfig {
477                service_account: GoogleCloudKmsSignerServiceAccountConfig {
478                    project_id: SecretString::new("project_id"),
479                    private_key_id: SecretString::new("private_key_id"),
480                    private_key: SecretString::new("private_key"),
481                    client_email: SecretString::new("client_email"),
482                    client_id: SecretString::new("client_id"),
483                    auth_uri: SecretString::new("auth_uri"),
484                    token_uri: SecretString::new("token_uri"),
485                    auth_provider_x509_cert_url: SecretString::new("auth_provider_x509_cert_url"),
486                    client_x509_cert_url: SecretString::new("client_x509_cert_url"),
487                    universe_domain: SecretString::new("universe_domain"),
488                },
489                key: GoogleCloudKmsSignerKeyConfig {
490                    location: SecretString::new("global"),
491                    key_id: SecretString::new("id"),
492                    key_ring_id: SecretString::new("key_ring"),
493                    key_version: 1,
494                },
495            })),
496        };
497
498        let signer = SolanaSignerFactory::create_solana_signer(signer_model.clone())
499            .await
500            .unwrap();
501
502        match signer {
503            SolanaSigner::GoogleCloudKms(_) => {}
504            _ => panic!("Expected Google Cloud KMS signer"),
505        }
506    }
507
508    #[tokio::test]
509    async fn test_address_solana_signer_local() {
510        let signer_model = SignerDomainModel {
511            id: "test".to_string(),
512            config: SignerConfig::Local(LocalSignerConfig {
513                raw_key: test_key_bytes(),
514            }),
515        };
516
517        let signer = SolanaSignerFactory::create_solana_signer(signer_model.clone())
518            .await
519            .unwrap();
520        let signer_address = signer.address().await.unwrap();
521        let signer_pubkey = signer.pubkey().await.unwrap();
522
523        assert_eq!(test_key_bytes_pubkey(), signer_address);
524        assert_eq!(test_key_bytes_pubkey(), signer_pubkey);
525    }
526
527    #[tokio::test]
528    async fn test_address_solana_signer_vault_transit() {
529        let signer_model = SignerDomainModel {
530            id: "test".to_string(),
531            config: SignerConfig::VaultTransit(VaultTransitSignerConfig {
532                key_name: "test".to_string(),
533                address: "address".to_string(),
534                namespace: None,
535                role_id: SecretString::new("role_id"),
536                secret_id: SecretString::new("secret_id"),
537                pubkey: "fV060x5X3Eo4uK/kTqQbSVL/qmMNaYKF2oaTa15hNfU=".to_string(),
538                mount_point: None,
539            }),
540        };
541        let expected_pubkey =
542            Address::Solana("9SNR5Sf993aphA7hzWSQsGv63x93trfuN8WjaToXcqKA".to_string());
543
544        let signer = SolanaSignerFactory::create_solana_signer(signer_model.clone())
545            .await
546            .unwrap();
547        let signer_address = signer.address().await.unwrap();
548        let signer_pubkey = signer.pubkey().await.unwrap();
549
550        assert_eq!(expected_pubkey, signer_address);
551        assert_eq!(expected_pubkey, signer_pubkey);
552    }
553
554    #[tokio::test]
555    async fn test_address_solana_signer_turnkey() {
556        let signer_model = SignerDomainModel {
557            id: "test".to_string(),
558            config: SignerConfig::Turnkey(TurnkeySignerConfig {
559                api_private_key: SecretString::new("api_private_key"),
560                api_public_key: "api_public_key".to_string(),
561                organization_id: "organization_id".to_string(),
562                private_key_id: "private_key_id".to_string(),
563                public_key: "5720be8aa9d2bb4be8e91f31d2c44c8629e42da16981c2cebabd55cafa0b76bd"
564                    .to_string(),
565            }),
566        };
567        let expected_pubkey =
568            Address::Solana("6s7RsvzcdXFJi1tXeDoGfSKZFzN3juVt9fTar6WEhEm2".to_string());
569
570        let signer = SolanaSignerFactory::create_solana_signer(signer_model.clone())
571            .await
572            .unwrap();
573        let signer_address = signer.address().await.unwrap();
574        let signer_pubkey = signer.pubkey().await.unwrap();
575
576        assert_eq!(expected_pubkey, signer_address);
577        assert_eq!(expected_pubkey, signer_pubkey);
578    }
579
580    #[tokio::test]
581    async fn test_address_solana_signer_cdp() {
582        let signer_model = SignerDomainModel {
583            id: "test".to_string(),
584            config: SignerConfig::Cdp(CdpSignerConfig {
585                api_key_id: "test-api-key-id".to_string(),
586                api_key_secret: SecretString::new("test-api-key-secret"),
587                wallet_secret: SecretString::new("test-wallet-secret"),
588                account_address: "6s7RsvzcdXFJi1tXeDoGfSKZFzN3juVt9fTar6WEhEm2".to_string(),
589            }),
590        };
591        let expected_pubkey =
592            Address::Solana("6s7RsvzcdXFJi1tXeDoGfSKZFzN3juVt9fTar6WEhEm2".to_string());
593
594        let signer = SolanaSignerFactory::create_solana_signer(signer_model.clone())
595            .await
596            .unwrap();
597        let signer_address = signer.address().await.unwrap();
598        let signer_pubkey = signer.pubkey().await.unwrap();
599
600        assert_eq!(expected_pubkey, signer_address);
601        assert_eq!(expected_pubkey, signer_pubkey);
602    }
603
604    #[tokio::test]
605    async fn test_address_solana_signer_google_cloud_kms() {
606        let signer_model = SignerDomainModel {
607            id: "test".to_string(),
608            config: SignerConfig::GoogleCloudKms(Box::new(GoogleCloudKmsSignerConfig {
609                service_account: GoogleCloudKmsSignerServiceAccountConfig {
610                    project_id: SecretString::new("project_id"),
611                    private_key_id: SecretString::new("private_key_id"),
612                    private_key: SecretString::new("private_key"),
613                    client_email: SecretString::new("client_email"),
614                    client_id: SecretString::new("client_id"),
615                    auth_uri: SecretString::new("auth_uri"),
616                    token_uri: SecretString::new("token_uri"),
617                    auth_provider_x509_cert_url: SecretString::new("auth_provider_x509_cert_url"),
618                    client_x509_cert_url: SecretString::new("client_x509_cert_url"),
619                    universe_domain: SecretString::new("universe_domain"),
620                },
621                key: GoogleCloudKmsSignerKeyConfig {
622                    location: SecretString::new("global"),
623                    key_id: SecretString::new("id"),
624                    key_ring_id: SecretString::new("key_ring"),
625                    key_version: 1,
626                },
627            })),
628        };
629
630        let signer = SolanaSignerFactory::create_solana_signer(signer_model.clone())
631            .await
632            .unwrap();
633        let signer_address = signer.address().await;
634        let signer_pubkey = signer.pubkey().await;
635
636        // should fail due to call to google cloud
637        assert!(signer_address.is_err());
638        assert!(signer_pubkey.is_err());
639    }
640
641    #[tokio::test]
642    async fn test_sign_solana_signer_local() {
643        let signer_model = SignerDomainModel {
644            id: "test".to_string(),
645            config: SignerConfig::Local(LocalSignerConfig {
646                raw_key: test_key_bytes(),
647            }),
648        };
649
650        let signer = SolanaSignerFactory::create_solana_signer(signer_model.clone())
651            .await
652            .unwrap();
653        let message = b"test message";
654        let signature = signer.sign(message).await;
655
656        assert!(signature.is_ok());
657    }
658
659    #[tokio::test]
660    async fn test_sign_solana_signer_test() {
661        let signer_model = SignerDomainModel {
662            id: "test".to_string(),
663            config: SignerConfig::Local(LocalSignerConfig {
664                raw_key: test_key_bytes(),
665            }),
666        };
667
668        let signer = SolanaSignerFactory::create_solana_signer(signer_model.clone())
669            .await
670            .unwrap();
671        let message = b"test message";
672        let signature = signer.sign(message).await;
673
674        assert!(signature.is_ok());
675    }
676
677    #[tokio::test]
678    async fn test_sign_sdk_transaction_success() {
679        use solana_sdk::message::Message;
680        use solana_sdk::pubkey::Pubkey;
681        use solana_sdk::signature::Signature;
682        use solana_sdk::transaction::Transaction;
683
684        // Create a mock signer
685        let signer_model = SignerDomainModel {
686            id: "test".to_string(),
687            config: SignerConfig::Local(LocalSignerConfig {
688                raw_key: test_key_bytes(),
689            }),
690        };
691        let signer = SolanaSignerFactory::create_solana_signer(signer_model.clone())
692            .await
693            .unwrap();
694
695        // Create a simple transaction with our signer as the first account
696        let signer_pubkey = Pubkey::from_str(&test_key_bytes_pubkey().to_string()).unwrap();
697        let recipient = Pubkey::new_unique();
698
699        let message = Message::new(
700            &[solana_system_interface::instruction::transfer(
701                &signer_pubkey,
702                &recipient,
703                1000,
704            )],
705            Some(&signer_pubkey),
706        );
707        let transaction = Transaction::new_unsigned(message);
708
709        // Sign the transaction
710        let result = sign_sdk_transaction(&signer, transaction).await;
711        assert!(result.is_ok());
712
713        let (signed_tx, signature) = result.unwrap();
714        assert!(!signature.to_string().is_empty());
715        assert_eq!(signed_tx.signatures.len(), 1);
716        assert_eq!(signed_tx.signatures[0], signature);
717    }
718
719    #[tokio::test]
720    async fn test_sign_sdk_transaction_signer_not_in_accounts() {
721        use solana_sdk::message::Message;
722        use solana_sdk::pubkey::Pubkey;
723        use solana_sdk::transaction::Transaction;
724
725        // Create a mock signer
726        let signer_model = SignerDomainModel {
727            id: "test".to_string(),
728            config: SignerConfig::Local(LocalSignerConfig {
729                raw_key: test_key_bytes(),
730            }),
731        };
732        let signer = SolanaSignerFactory::create_solana_signer(signer_model.clone())
733            .await
734            .unwrap();
735
736        // Create a transaction where our signer is NOT in the account keys
737        let other_pubkey = Pubkey::new_unique();
738        let recipient = Pubkey::new_unique();
739
740        let message = Message::new(
741            &[solana_system_interface::instruction::transfer(
742                &other_pubkey,
743                &recipient,
744                1000,
745            )],
746            Some(&other_pubkey),
747        );
748        let transaction = Transaction::new_unsigned(message);
749
750        // Try to sign - should fail because signer is not in account_keys
751        let result = sign_sdk_transaction(&signer, transaction).await;
752        assert!(result.is_err());
753        let error = result.unwrap_err();
754        match error {
755            SignerError::SigningError(msg) => {
756                assert!(msg.contains("Signer public key not found in transaction signers"));
757            }
758            _ => panic!("Expected SigningError, got {error:?}"),
759        }
760    }
761
762    #[tokio::test]
763    async fn test_sign_sdk_transaction_signer_not_required() {
764        use solana_sdk::message::Message;
765        use solana_sdk::pubkey::Pubkey;
766        use solana_sdk::transaction::Transaction;
767
768        // Create a mock signer
769        let signer_model = SignerDomainModel {
770            id: "test".to_string(),
771            config: SignerConfig::Local(LocalSignerConfig {
772                raw_key: test_key_bytes(),
773            }),
774        };
775        let signer = SolanaSignerFactory::create_solana_signer(signer_model.clone())
776            .await
777            .unwrap();
778
779        // Create a transaction where our signer is in account_keys but NOT marked as required
780        let signer_pubkey = Pubkey::from_str(&test_key_bytes_pubkey().to_string()).unwrap();
781        let fee_payer = Pubkey::new_unique();
782        let recipient = Pubkey::new_unique();
783
784        // Create message with signer as a readonly account (not required signer)
785        // Use a different approach - create a message where signer is not the fee payer
786        let message = Message::new(
787            &[solana_system_interface::instruction::transfer(
788                &fee_payer, &recipient, 1000,
789            )],
790            Some(&fee_payer),
791        );
792        let transaction = Transaction::new_unsigned(message);
793
794        // Manually modify the message to include our signer as a readonly account
795        // This simulates a transaction where our signer is present but not required
796        let mut modified_message = transaction.message.clone();
797        modified_message.account_keys.push(signer_pubkey); // Add signer as additional account
798        modified_message.header.num_readonly_unsigned_accounts += 1; // Make it readonly unsigned
799
800        let modified_transaction = Transaction::new_unsigned(modified_message);
801
802        // Try to sign - should fail because signer is not a required signer
803        let result = sign_sdk_transaction(&signer, modified_transaction).await;
804        assert!(result.is_err());
805        let error = result.unwrap_err();
806        match error {
807            SignerError::SigningError(msg) => {
808                assert!(msg.contains("Signer is not marked as a required signer"));
809            }
810            _ => panic!("Expected SigningError, got {error:?}"),
811        }
812    }
813
814    #[tokio::test]
815    async fn test_sign_transaction_with_domain_model() {
816        use crate::models::{NetworkTransactionData, SolanaTransactionData};
817        use solana_sdk::message::Message;
818        use solana_sdk::pubkey::Pubkey;
819
820        // Create a mock signer
821        let signer_model = SignerDomainModel {
822            id: "test".to_string(),
823            config: SignerConfig::Local(LocalSignerConfig {
824                raw_key: test_key_bytes(),
825            }),
826        };
827        let signer = SolanaSignerFactory::create_solana_signer(signer_model.clone())
828            .await
829            .unwrap();
830
831        // Create a domain transaction data
832        let signer_pubkey = Pubkey::from_str(&test_key_bytes_pubkey().to_string()).unwrap();
833        let recipient = Pubkey::new_unique();
834
835        let message = Message::new(
836            &[solana_system_interface::instruction::transfer(
837                &signer_pubkey,
838                &recipient,
839                1000,
840            )],
841            Some(&signer_pubkey),
842        );
843        let transaction = solana_sdk::transaction::Transaction::new_unsigned(message);
844        let encoded_tx =
845            crate::models::EncodedSerializedTransaction::try_from(&transaction).unwrap();
846
847        let solana_data = SolanaTransactionData {
848            transaction: Some(encoded_tx.into_inner()),
849            ..Default::default()
850        };
851
852        let network_data = NetworkTransactionData::Solana(solana_data);
853
854        // Sign using the domain model method
855        let result = signer.sign_transaction(network_data).await;
856        assert!(result.is_ok());
857
858        let response = result.unwrap();
859        match response {
860            crate::domain::SignTransactionResponse::Solana(solana_response) => {
861                assert!(!solana_response.transaction.into_inner().is_empty());
862                assert!(!solana_response.signature.is_empty());
863            }
864            _ => panic!("Expected Solana response"),
865        }
866    }
867
868    #[tokio::test]
869    async fn test_create_solana_signer_aws_kms_supported() {
870        let signer_model = SignerDomainModel {
871            id: "test".to_string(),
872            config: SignerConfig::AwsKms(AwsKmsSignerConfig {
873                region: Some("us-east-1".to_string()),
874                key_id: "test-key-id".to_string(),
875            }),
876        };
877
878        let result = SolanaSignerFactory::create_solana_signer(signer_model).await;
879        assert!(result.is_ok(), "AWS KMS should be supported for Solana");
880    }
881
882    #[cfg(test)]
883    #[async_trait]
884    impl Signer for MockSolanaSignTrait {
885        async fn address(&self) -> Result<Address, SignerError> {
886            self.pubkey().await
887        }
888
889        async fn sign_transaction(
890            &self,
891            _transaction: NetworkTransactionData,
892        ) -> Result<SignTransactionResponse, SignerError> {
893            // For testing, return a mock response
894            Ok(SignTransactionResponse::Solana(
895                crate::domain::SignTransactionResponseSolana {
896                    transaction: crate::models::EncodedSerializedTransaction::new(
897                        "signed_transaction_data".to_string(),
898                    ),
899                    signature: "signature_data".to_string(),
900                },
901            ))
902        }
903    }
904}