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