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