1use crate::{
11 jobs::JobProducerTrait,
12 models::{
13 ApiError, ApiResponse, NetworkRepoModel, NotificationRepoModel, PaginationMeta,
14 PaginationQuery, RelayerRepoModel, Signer, SignerCreateRequest, SignerRepoModel,
15 SignerResponse, SignerUpdateRequest, ThinDataAppState, TransactionRepoModel,
16 },
17 repositories::{
18 ApiKeyRepositoryTrait, NetworkRepository, PluginRepositoryTrait, RelayerRepository,
19 Repository, TransactionCounterTrait, TransactionRepository,
20 },
21};
22use actix_web::HttpResponse;
23use eyre::Result;
24
25pub async fn list_signers<J, RR, TR, NR, NFR, SR, TCR, PR, AKR>(
36 query: PaginationQuery,
37 state: ThinDataAppState<J, RR, TR, NR, NFR, SR, TCR, PR, AKR>,
38) -> Result<HttpResponse, ApiError>
39where
40 J: JobProducerTrait + Send + Sync + 'static,
41 RR: RelayerRepository + Repository<RelayerRepoModel, String> + Send + Sync + 'static,
42 TR: TransactionRepository + Repository<TransactionRepoModel, String> + Send + Sync + 'static,
43 NR: NetworkRepository + Repository<NetworkRepoModel, String> + Send + Sync + 'static,
44 NFR: Repository<NotificationRepoModel, String> + Send + Sync + 'static,
45 SR: Repository<SignerRepoModel, String> + Send + Sync + 'static,
46 TCR: TransactionCounterTrait + Send + Sync + 'static,
47 PR: PluginRepositoryTrait + Send + Sync + 'static,
48 AKR: ApiKeyRepositoryTrait + Send + Sync + 'static,
49{
50 let signers = state.signer_repository.list_paginated(query).await?;
51
52 let mapped_signers: Vec<SignerResponse> = signers.items.into_iter().map(|s| s.into()).collect();
53
54 Ok(HttpResponse::Ok().json(ApiResponse::paginated(
55 mapped_signers,
56 PaginationMeta {
57 total_items: signers.total,
58 current_page: signers.page,
59 per_page: signers.per_page,
60 },
61 )))
62}
63
64pub async fn get_signer<J, RR, TR, NR, NFR, SR, TCR, PR, AKR>(
75 signer_id: String,
76 state: ThinDataAppState<J, RR, TR, NR, NFR, SR, TCR, PR, AKR>,
77) -> Result<HttpResponse, ApiError>
78where
79 J: JobProducerTrait + Send + Sync + 'static,
80 RR: RelayerRepository + Repository<RelayerRepoModel, String> + Send + Sync + 'static,
81 TR: TransactionRepository + Repository<TransactionRepoModel, String> + Send + Sync + 'static,
82 NR: NetworkRepository + Repository<NetworkRepoModel, String> + Send + Sync + 'static,
83 NFR: Repository<NotificationRepoModel, String> + Send + Sync + 'static,
84 SR: Repository<SignerRepoModel, String> + Send + Sync + 'static,
85 TCR: TransactionCounterTrait + Send + Sync + 'static,
86 PR: PluginRepositoryTrait + Send + Sync + 'static,
87 AKR: ApiKeyRepositoryTrait + Send + Sync + 'static,
88{
89 let signer = state.signer_repository.get_by_id(signer_id).await?;
90
91 let response = SignerResponse::from(signer);
92 Ok(HttpResponse::Ok().json(ApiResponse::success(response)))
93}
94
95pub async fn create_signer<J, RR, TR, NR, NFR, SR, TCR, PR, AKR>(
113 request: SignerCreateRequest,
114 state: ThinDataAppState<J, RR, TR, NR, NFR, SR, TCR, PR, AKR>,
115) -> Result<HttpResponse, ApiError>
116where
117 J: JobProducerTrait + Send + Sync + 'static,
118 RR: RelayerRepository + Repository<RelayerRepoModel, String> + Send + Sync + 'static,
119 TR: TransactionRepository + Repository<TransactionRepoModel, String> + Send + Sync + 'static,
120 NR: NetworkRepository + Repository<NetworkRepoModel, String> + Send + Sync + 'static,
121 NFR: Repository<NotificationRepoModel, String> + Send + Sync + 'static,
122 SR: Repository<SignerRepoModel, String> + Send + Sync + 'static,
123 TCR: TransactionCounterTrait + Send + Sync + 'static,
124 PR: PluginRepositoryTrait + Send + Sync + 'static,
125 AKR: ApiKeyRepositoryTrait + Send + Sync + 'static,
126{
127 let signer = Signer::try_from(request)?;
129
130 let signer_model = SignerRepoModel::from(signer);
132
133 let created_signer = state.signer_repository.create(signer_model).await?;
134
135 let response = SignerResponse::from(created_signer);
136 Ok(HttpResponse::Created().json(ApiResponse::success(response)))
137}
138
139pub async fn update_signer<J, RR, TR, NR, NFR, SR, TCR, PR, AKR>(
156 _signer_id: String,
157 _request: SignerUpdateRequest,
158 _state: ThinDataAppState<J, RR, TR, NR, NFR, SR, TCR, PR, AKR>,
159) -> Result<HttpResponse, ApiError>
160where
161 J: JobProducerTrait + Send + Sync + 'static,
162 RR: RelayerRepository + Repository<RelayerRepoModel, String> + Send + Sync + 'static,
163 TR: TransactionRepository + Repository<TransactionRepoModel, String> + Send + Sync + 'static,
164 NR: NetworkRepository + Repository<NetworkRepoModel, String> + Send + Sync + 'static,
165 NFR: Repository<NotificationRepoModel, String> + Send + Sync + 'static,
166 SR: Repository<SignerRepoModel, String> + Send + Sync + 'static,
167 TCR: TransactionCounterTrait + Send + Sync + 'static,
168 PR: PluginRepositoryTrait + Send + Sync + 'static,
169 AKR: ApiKeyRepositoryTrait + Send + Sync + 'static,
170{
171 Err(ApiError::BadRequest(
172 "Signer updates are not allowed for security reasons. Please delete the existing signer and create a new one with the desired configuration.".to_string()
173 ))
174}
175
176pub async fn delete_signer<J, RR, TR, NR, NFR, SR, TCR, PR, AKR>(
193 signer_id: String,
194 state: ThinDataAppState<J, RR, TR, NR, NFR, SR, TCR, PR, AKR>,
195) -> Result<HttpResponse, ApiError>
196where
197 J: JobProducerTrait + Send + Sync + 'static,
198 RR: RelayerRepository + Repository<RelayerRepoModel, String> + Send + Sync + 'static,
199 TR: TransactionRepository + Repository<TransactionRepoModel, String> + Send + Sync + 'static,
200 NR: NetworkRepository + Repository<NetworkRepoModel, String> + Send + Sync + 'static,
201 NFR: Repository<NotificationRepoModel, String> + Send + Sync + 'static,
202 SR: Repository<SignerRepoModel, String> + Send + Sync + 'static,
203 TCR: TransactionCounterTrait + Send + Sync + 'static,
204 PR: PluginRepositoryTrait + Send + Sync + 'static,
205 AKR: ApiKeyRepositoryTrait + Send + Sync + 'static,
206{
207 let _signer = state.signer_repository.get_by_id(signer_id.clone()).await?;
209
210 let connected_relayers = state
212 .relayer_repository
213 .list_by_signer_id(&signer_id)
214 .await?;
215
216 if !connected_relayers.is_empty() {
217 let relayer_names: Vec<String> =
218 connected_relayers.iter().map(|r| r.name.clone()).collect();
219 return Err(ApiError::BadRequest(format!(
220 "Cannot delete signer '{}' because it is being used by {} relayer(s): {}. Please remove or reconfigure these relayers before deleting the signer.",
221 signer_id,
222 connected_relayers.len(),
223 relayer_names.join(", ")
224 )));
225 }
226
227 state.signer_repository.delete_by_id(signer_id).await?;
229
230 Ok(HttpResponse::Ok().json(ApiResponse::success("Signer deleted successfully")))
231}
232
233#[cfg(test)]
234mod tests {
235 use super::*;
236 use crate::{
237 models::{
238 AwsKmsSignerConfigStorage, AwsKmsSignerRequestConfig,
239 GoogleCloudKmsSignerKeyRequestConfig, GoogleCloudKmsSignerRequestConfig,
240 GoogleCloudKmsSignerServiceAccountRequestConfig, LocalSignerConfigStorage,
241 LocalSignerRequestConfig, SignerConfigRequest, SignerConfigStorage, SignerType,
242 SignerTypeRequest, TurnkeySignerRequestConfig, VaultSignerRequestConfig,
243 },
244 utils::mocks::mockutils::create_mock_app_state,
245 };
246 use secrets::SecretVec;
247
248 fn create_test_signer_model(id: &str, signer_type: SignerType) -> SignerRepoModel {
250 let config = match signer_type {
251 SignerType::Local => SignerConfigStorage::Local(LocalSignerConfigStorage {
252 raw_key: SecretVec::new(32, |v| v.copy_from_slice(&[1; 32])),
253 }),
254 SignerType::AwsKms => SignerConfigStorage::AwsKms(AwsKmsSignerConfigStorage {
255 region: Some("us-east-1".to_string()),
256 key_id: "test-key-id".to_string(),
257 }),
258 _ => SignerConfigStorage::Local(LocalSignerConfigStorage {
259 raw_key: SecretVec::new(32, |v| v.copy_from_slice(&[1; 32])),
260 }),
261 };
262
263 SignerRepoModel {
264 id: id.to_string(),
265 config,
266 }
267 }
268
269 fn create_test_signer_create_request(
271 id: Option<String>,
272 signer_type: SignerType,
273 ) -> SignerCreateRequest {
274 use crate::models::{
275 AwsKmsSignerRequestConfig, LocalSignerRequestConfig, SignerConfigRequest,
276 SignerTypeRequest,
277 };
278
279 let (signer_type_req, config) = match signer_type {
280 SignerType::Local => (
281 SignerTypeRequest::Local,
282 SignerConfigRequest::Local(LocalSignerRequestConfig {
283 key: "1111111111111111111111111111111111111111111111111111111111111111"
284 .to_string(), }),
286 ),
287 SignerType::AwsKms => (
288 SignerTypeRequest::AwsKms,
289 SignerConfigRequest::AwsKms(AwsKmsSignerRequestConfig {
290 region: "us-east-1".to_string(),
291 key_id: "test-key-id".to_string(),
292 }),
293 ),
294 _ => (
295 SignerTypeRequest::Local,
296 SignerConfigRequest::Local(LocalSignerRequestConfig {
297 key: "placeholder-key".to_string(),
298 }),
299 ), };
301
302 SignerCreateRequest {
303 id,
304 signer_type: signer_type_req,
305 config,
306 }
307 }
308
309 fn create_test_signer_update_request() -> SignerUpdateRequest {
311 SignerUpdateRequest {}
312 }
313
314 #[actix_web::test]
315 async fn test_list_signers_empty() {
316 let app_state = create_mock_app_state(None, None, None, None, None, None).await;
317 let query = PaginationQuery {
318 page: 1,
319 per_page: 10,
320 };
321
322 let result = list_signers(query, actix_web::web::ThinData(app_state)).await;
323
324 assert!(result.is_ok());
325 let response = result.unwrap();
326 assert_eq!(response.status(), 200);
327
328 let body = actix_web::body::to_bytes(response.into_body())
329 .await
330 .unwrap();
331 let api_response: ApiResponse<Vec<SignerResponse>> = serde_json::from_slice(&body).unwrap();
332
333 assert!(api_response.success);
334 let data = api_response.data.unwrap();
335 assert_eq!(data.len(), 0);
336 }
337
338 #[actix_web::test]
339 async fn test_list_signers_with_data() {
340 let app_state = create_mock_app_state(None, None, None, None, None, None).await;
341
342 let signer1 = create_test_signer_model("test-1", SignerType::Local);
344 let signer2 = create_test_signer_model("test-2", SignerType::AwsKms);
345
346 app_state.signer_repository.create(signer1).await.unwrap();
347 app_state.signer_repository.create(signer2).await.unwrap();
348
349 let query = PaginationQuery {
350 page: 1,
351 per_page: 10,
352 };
353
354 let result = list_signers(query, actix_web::web::ThinData(app_state)).await;
355
356 assert!(result.is_ok());
357 let response = result.unwrap();
358 assert_eq!(response.status(), 200);
359
360 let body = actix_web::body::to_bytes(response.into_body())
361 .await
362 .unwrap();
363 let api_response: ApiResponse<Vec<SignerResponse>> = serde_json::from_slice(&body).unwrap();
364
365 assert!(api_response.success);
366 let data = api_response.data.unwrap();
367 assert_eq!(data.len(), 2);
368
369 let ids: Vec<&String> = data.iter().map(|s| &s.id).collect();
371 assert!(ids.contains(&&"test-1".to_string()));
372 assert!(ids.contains(&&"test-2".to_string()));
373 }
374
375 #[actix_web::test]
376 async fn test_get_signer_success() {
377 let app_state = create_mock_app_state(None, None, None, None, None, None).await;
378
379 let signer = create_test_signer_model("test-signer", SignerType::Local);
381 app_state
382 .signer_repository
383 .create(signer.clone())
384 .await
385 .unwrap();
386
387 let result = get_signer(
388 "test-signer".to_string(),
389 actix_web::web::ThinData(app_state),
390 )
391 .await;
392
393 assert!(result.is_ok());
394 let response = result.unwrap();
395 assert_eq!(response.status(), 200);
396
397 let body = actix_web::body::to_bytes(response.into_body())
398 .await
399 .unwrap();
400 let api_response: ApiResponse<SignerResponse> = serde_json::from_slice(&body).unwrap();
401
402 assert!(api_response.success);
403 let data = api_response.data.unwrap();
404 assert_eq!(data.id, "test-signer");
405 assert_eq!(data.r#type, SignerType::Local);
406 }
407
408 #[actix_web::test]
409 async fn test_get_signer_not_found() {
410 let app_state = create_mock_app_state(None, None, None, None, None, None).await;
411
412 let result = get_signer(
413 "non-existent".to_string(),
414 actix_web::web::ThinData(app_state),
415 )
416 .await;
417
418 assert!(result.is_err());
419 let error = result.unwrap_err();
420 assert!(matches!(error, ApiError::NotFound(_)));
421 }
422
423 #[actix_web::test]
424 async fn test_create_signer_test_type_success() {
425 let app_state = create_mock_app_state(None, None, None, None, None, None).await;
426
427 let request = create_test_signer_create_request(
428 Some("new-test-signer".to_string()),
429 SignerType::Local,
430 );
431
432 let result = create_signer(request, actix_web::web::ThinData(app_state)).await;
433
434 assert!(result.is_ok());
435 let response = result.unwrap();
436 assert_eq!(response.status(), 201);
437
438 let body = actix_web::body::to_bytes(response.into_body())
439 .await
440 .unwrap();
441 let api_response: ApiResponse<SignerResponse> = serde_json::from_slice(&body).unwrap();
442
443 assert!(api_response.success);
444 let data = api_response.data.unwrap();
445 assert_eq!(data.id, "new-test-signer");
446 assert_eq!(data.r#type, SignerType::Local);
447 }
448
449 #[actix_web::test]
450 async fn test_create_signer_with_valid_configs() {
451 let app_state1 = create_mock_app_state(None, None, None, None, None, None).await;
453 let request =
454 create_test_signer_create_request(Some("local-test".to_string()), SignerType::Local);
455 let result = create_signer(request, actix_web::web::ThinData(app_state1)).await;
456 assert!(result.is_ok(), "Local signer with valid key should succeed");
457
458 let app_state2 = create_mock_app_state(None, None, None, None, None, None).await;
460 let request =
461 create_test_signer_create_request(Some("aws-test".to_string()), SignerType::AwsKms);
462 let result = create_signer(request, actix_web::web::ThinData(app_state2)).await;
463 assert!(
464 result.is_ok(),
465 "AWS KMS signer with valid config should succeed"
466 );
467 }
468
469 #[actix_web::test]
470 async fn test_create_signer_local_with_valid_key() {
471 let app_state = create_mock_app_state(None, None, None, None, None, None).await;
472
473 let request = SignerCreateRequest {
474 id: Some("local-signer-test".to_string()),
475 signer_type: SignerTypeRequest::Local,
476 config: SignerConfigRequest::Local(LocalSignerRequestConfig {
477 key: "abcdef1234567890abcdef1234567890abcdef1234567890abcdef1234567890".to_string(), }),
479 };
480
481 let result = create_signer(request, actix_web::web::ThinData(app_state)).await;
482
483 assert!(result.is_ok());
484 let response = result.unwrap();
485 assert_eq!(response.status(), 201);
486
487 let body = actix_web::body::to_bytes(response.into_body())
488 .await
489 .unwrap();
490 let api_response: ApiResponse<SignerResponse> = serde_json::from_slice(&body).unwrap();
491
492 assert!(api_response.success);
493 let data = api_response.data.unwrap();
494 assert_eq!(data.id, "local-signer-test");
495 assert_eq!(data.r#type, SignerType::Local);
496 }
497
498 #[actix_web::test]
499 async fn test_create_signer_aws_kms_comprehensive() {
500 let app_state = create_mock_app_state(None, None, None, None, None, None).await;
501
502 let request = SignerCreateRequest {
503 id: Some("aws-kms-signer".to_string()),
504 signer_type: SignerTypeRequest::AwsKms,
505 config: SignerConfigRequest::AwsKms(AwsKmsSignerRequestConfig {
506 region: "us-west-2".to_string(),
507 key_id:
508 "arn:aws:kms:us-west-2:123456789012:key/12345678-1234-1234-1234-123456789012"
509 .to_string(),
510 }),
511 };
512
513 let result = create_signer(request, actix_web::web::ThinData(app_state)).await;
514
515 assert!(result.is_ok());
516 let response = result.unwrap();
517 assert_eq!(response.status(), 201);
518
519 let body = actix_web::body::to_bytes(response.into_body())
520 .await
521 .unwrap();
522 let api_response: ApiResponse<SignerResponse> = serde_json::from_slice(&body).unwrap();
523
524 assert!(api_response.success);
525 let data = api_response.data.unwrap();
526 assert_eq!(data.id, "aws-kms-signer");
527 assert_eq!(data.r#type, SignerType::AwsKms);
528 }
529
530 #[actix_web::test]
531 async fn test_create_signer_vault() {
532 let app_state = create_mock_app_state(None, None, None, None, None, None).await;
533
534 let request = SignerCreateRequest {
535 id: Some("vault-signer".to_string()),
536 signer_type: SignerTypeRequest::Vault,
537 config: SignerConfigRequest::Vault(VaultSignerRequestConfig {
538 address: "https://vault.example.com:8200".to_string(),
539 namespace: Some("development".to_string()),
540 role_id: "test-role-id-12345".to_string(),
541 secret_id: "test-secret-id-67890".to_string(),
542 key_name: "ethereum-key".to_string(),
543 mount_point: Some("secret".to_string()),
544 }),
545 };
546
547 let result = create_signer(request, actix_web::web::ThinData(app_state)).await;
548
549 assert!(result.is_ok());
550 let response = result.unwrap();
551 assert_eq!(response.status(), 201);
552
553 let body = actix_web::body::to_bytes(response.into_body())
554 .await
555 .unwrap();
556 let api_response: ApiResponse<SignerResponse> = serde_json::from_slice(&body).unwrap();
557
558 assert!(api_response.success);
559 let data = api_response.data.unwrap();
560 assert_eq!(data.id, "vault-signer");
561 assert_eq!(data.r#type, SignerType::Vault);
562 }
563
564 #[actix_web::test]
565 async fn test_create_signer_vault_transit() {
566 let app_state = create_mock_app_state(None, None, None, None, None, None).await;
567
568 use crate::models::{
569 SignerConfigRequest, SignerTypeRequest, VaultTransitSignerRequestConfig,
570 };
571 let request = SignerCreateRequest {
572 id: Some("vault-transit-signer".to_string()),
573 signer_type: SignerTypeRequest::VaultTransit,
574 config: SignerConfigRequest::VaultTransit(VaultTransitSignerRequestConfig {
575 key_name: "ethereum-transit-key".to_string(),
576 address: "https://vault.example.com:8200".to_string(),
577 namespace: None,
578 role_id: "transit-role-id-12345".to_string(),
579 secret_id: "transit-secret-id-67890".to_string(),
580 pubkey: "0x04a34b99f22c790c4e36b2b3c2c35a36db06226e41c692fc82b8b56ac1c540c5bd5b8dec5235a0fa8722476c7709c02559e3aa73aa03918ba2d492eea75abea235".to_string(),
581 mount_point: Some("transit".to_string()),
582 }),
583 };
584
585 let result = create_signer(request, actix_web::web::ThinData(app_state)).await;
586
587 assert!(result.is_ok());
588 let response = result.unwrap();
589 assert_eq!(response.status(), 201);
590
591 let body = actix_web::body::to_bytes(response.into_body())
592 .await
593 .unwrap();
594 let api_response: ApiResponse<SignerResponse> = serde_json::from_slice(&body).unwrap();
595
596 assert!(api_response.success);
597 let data = api_response.data.unwrap();
598 assert_eq!(data.id, "vault-transit-signer");
599 assert_eq!(data.r#type, SignerType::VaultTransit);
600 }
601
602 #[actix_web::test]
603 async fn test_create_signer_turnkey() {
604 let app_state = create_mock_app_state(None, None, None, None, None, None).await;
605
606 let request = SignerCreateRequest {
607 id: Some("turnkey-signer".to_string()),
608 signer_type: SignerTypeRequest::Turnkey,
609 config: SignerConfigRequest::Turnkey(TurnkeySignerRequestConfig {
610 api_public_key: "turnkey-api-public-key-example".to_string(),
611 api_private_key: "turnkey-api-private-key-example".to_string(),
612 organization_id: "turnkey-org-12345".to_string(),
613 private_key_id: "turnkey-private-key-67890".to_string(),
614 public_key: "0x04a34b99f22c790c4e36b2b3c2c35a36db06226e41c692fc82b8b56ac1c540c5bd5b8dec5235a0fa8722476c7709c02559e3aa73aa03918ba2d492eea75abea235".to_string(),
615 }),
616 };
617
618 let result = create_signer(request, actix_web::web::ThinData(app_state)).await;
619
620 assert!(result.is_ok());
621 let response = result.unwrap();
622 assert_eq!(response.status(), 201);
623
624 let body = actix_web::body::to_bytes(response.into_body())
625 .await
626 .unwrap();
627 let api_response: ApiResponse<SignerResponse> = serde_json::from_slice(&body).unwrap();
628
629 assert!(api_response.success);
630 let data = api_response.data.unwrap();
631 assert_eq!(data.id, "turnkey-signer");
632 assert_eq!(data.r#type, SignerType::Turnkey);
633 }
634
635 #[actix_web::test]
636 async fn test_create_signer_google_cloud_kms() {
637 let app_state = create_mock_app_state(None, None, None, None, None, None).await;
638
639 let request = SignerCreateRequest {
640 id: Some("gcp-kms-signer".to_string()),
641 signer_type: SignerTypeRequest::GoogleCloudKms,
642 config: SignerConfigRequest::GoogleCloudKms(GoogleCloudKmsSignerRequestConfig {
643 service_account: GoogleCloudKmsSignerServiceAccountRequestConfig {
644 private_key: "-----BEGIN EXAMPLE PRIVATE KEY-----\nSDFGSDFGSDGSDFGSDFGSDFGSDFGSDFGSAFAS...\n-----END EXAMPLE PRIVATE KEY-----\n".to_string(), private_key_id: "gcp-private-key-id-12345".to_string(),
646 project_id: "my-gcp-project".to_string(),
647 client_email: "service-account@my-gcp-project.iam.gserviceaccount.com".to_string(),
648 client_id: "123456789012345678901".to_string(),
649 auth_uri: "https://accounts.google.com/o/oauth2/auth".to_string(),
650 token_uri: "https://oauth2.googleapis.com/token".to_string(),
651 auth_provider_x509_cert_url: "https://www.googleapis.com/oauth2/v1/certs".to_string(),
652 client_x509_cert_url: "https://www.googleapis.com/robot/v1/metadata/x509/service-account%40my-gcp-project.iam.gserviceaccount.com".to_string(),
653 universe_domain: "googleapis.com".to_string(),
654 },
655 key: GoogleCloudKmsSignerKeyRequestConfig {
656 location: "global".to_string(),
657 key_ring_id: "ethereum-keyring".to_string(),
658 key_id: "ethereum-signing-key".to_string(),
659 key_version: 1,
660 },
661 }),
662 };
663
664 let result = create_signer(request, actix_web::web::ThinData(app_state)).await;
665
666 assert!(result.is_ok());
667 let response = result.unwrap();
668 assert_eq!(response.status(), 201);
669
670 let body = actix_web::body::to_bytes(response.into_body())
671 .await
672 .unwrap();
673 let api_response: ApiResponse<SignerResponse> = serde_json::from_slice(&body).unwrap();
674
675 assert!(api_response.success);
676 let data = api_response.data.unwrap();
677 assert_eq!(data.id, "gcp-kms-signer");
678 assert_eq!(data.r#type, SignerType::GoogleCloudKms);
679 }
680
681 #[actix_web::test]
682 async fn test_create_signer_auto_generated_id() {
683 let app_state = create_mock_app_state(None, None, None, None, None, None).await;
684
685 let request = SignerCreateRequest {
686 id: None, signer_type: SignerTypeRequest::Local,
688 config: SignerConfigRequest::Local(LocalSignerRequestConfig {
689 key: "fedcba0987654321fedcba0987654321fedcba0987654321fedcba0987654321".to_string(),
690 }),
691 };
692
693 let result = create_signer(request, actix_web::web::ThinData(app_state)).await;
694
695 assert!(result.is_ok());
696 let response = result.unwrap();
697 assert_eq!(response.status(), 201);
698
699 let body = actix_web::body::to_bytes(response.into_body())
700 .await
701 .unwrap();
702 let api_response: ApiResponse<SignerResponse> = serde_json::from_slice(&body).unwrap();
703
704 assert!(api_response.success);
705 let data = api_response.data.unwrap();
706 assert!(!data.id.is_empty());
707 assert!(uuid::Uuid::parse_str(&data.id).is_ok()); assert_eq!(data.r#type, SignerType::Local);
709 }
710
711 #[actix_web::test]
712 async fn test_create_signer_invalid_local_key() {
713 let app_state = create_mock_app_state(None, None, None, None, None, None).await;
714
715 let request = SignerCreateRequest {
716 id: Some("invalid-key-signer".to_string()),
717 signer_type: SignerTypeRequest::Local,
718 config: SignerConfigRequest::Local(LocalSignerRequestConfig {
719 key: "invalid-hex-key".to_string(), }),
721 };
722
723 let result = create_signer(request, actix_web::web::ThinData(app_state)).await;
724
725 assert!(result.is_err());
726 if let Err(ApiError::BadRequest(msg)) = result {
727 assert!(msg.contains("Invalid hex key format"));
728 } else {
729 panic!("Expected BadRequest error for invalid hex key");
730 }
731 }
732
733 #[actix_web::test]
734 async fn test_create_signer_invalid_vault_address() {
735 let app_state = create_mock_app_state(None, None, None, None, None, None).await;
736
737 let request = SignerCreateRequest {
738 id: Some("invalid-vault-signer".to_string()),
739 signer_type: SignerTypeRequest::Vault,
740 config: SignerConfigRequest::Vault(VaultSignerRequestConfig {
741 address: "not-a-valid-url".to_string(), namespace: None,
743 role_id: "test-role".to_string(),
744 secret_id: "test-secret".to_string(),
745 key_name: "test-key".to_string(),
746 mount_point: None,
747 }),
748 };
749
750 let result = create_signer(request, actix_web::web::ThinData(app_state)).await;
751
752 assert!(result.is_err());
753 if let Err(ApiError::BadRequest(msg)) = result {
754 assert!(msg.contains("Address must be a valid URL"));
755 } else {
756 panic!("Expected BadRequest error for invalid Vault address");
757 }
758 }
759
760 #[actix_web::test]
761 async fn test_create_signer_empty_aws_kms_key_id() {
762 let app_state = create_mock_app_state(None, None, None, None, None, None).await;
763
764 let request = SignerCreateRequest {
765 id: Some("empty-key-id-signer".to_string()),
766 signer_type: SignerTypeRequest::AwsKms,
767 config: SignerConfigRequest::AwsKms(AwsKmsSignerRequestConfig {
768 region: "us-east-1".to_string(),
769 key_id: "".to_string(), }),
771 };
772
773 let result = create_signer(request, actix_web::web::ThinData(app_state)).await;
774
775 assert!(result.is_err());
776 if let Err(ApiError::BadRequest(msg)) = result {
777 assert!(msg.contains("Key ID cannot be empty"));
778 } else {
779 panic!("Expected BadRequest error for empty AWS KMS key ID");
780 }
781 }
782
783 #[actix_web::test]
784 async fn test_update_signer_not_allowed() {
785 let app_state = create_mock_app_state(None, None, None, None, None, None).await;
786
787 let signer = create_test_signer_model("test-signer", SignerType::Local);
789 app_state.signer_repository.create(signer).await.unwrap();
790
791 let update_request = create_test_signer_update_request();
792
793 let result = update_signer(
794 "test-signer".to_string(),
795 update_request,
796 actix_web::web::ThinData(app_state),
797 )
798 .await;
799
800 assert!(result.is_err());
801 let error = result.unwrap_err();
802 if let ApiError::BadRequest(msg) = error {
803 assert!(msg.contains("Signer updates are not allowed"));
804 assert!(msg.contains("delete the existing signer and create a new one"));
805 } else {
806 panic!("Expected BadRequest error");
807 }
808 }
809
810 #[actix_web::test]
811 async fn test_update_signer_always_fails() {
812 let app_state = create_mock_app_state(None, None, None, None, None, None).await;
813
814 let update_request = create_test_signer_update_request();
815
816 let result = update_signer(
817 "non-existent".to_string(),
818 update_request,
819 actix_web::web::ThinData(app_state),
820 )
821 .await;
822
823 assert!(result.is_err());
824 let error = result.unwrap_err();
825 if let ApiError::BadRequest(msg) = error {
826 assert!(msg.contains("Signer updates are not allowed"));
827 } else {
828 panic!("Expected BadRequest error");
829 }
830 }
831
832 #[actix_web::test]
833 async fn test_delete_signer_success() {
834 let app_state = create_mock_app_state(None, None, None, None, None, None).await;
835
836 let signer = create_test_signer_model("test-signer", SignerType::Local);
838 app_state.signer_repository.create(signer).await.unwrap();
839
840 let result = delete_signer(
841 "test-signer".to_string(),
842 actix_web::web::ThinData(app_state),
843 )
844 .await;
845
846 assert!(result.is_ok());
847 let response = result.unwrap();
848 assert_eq!(response.status(), 200);
849
850 let body = actix_web::body::to_bytes(response.into_body())
851 .await
852 .unwrap();
853 let api_response: ApiResponse<&str> = serde_json::from_slice(&body).unwrap();
854
855 assert!(api_response.success);
856 assert_eq!(api_response.data.unwrap(), "Signer deleted successfully");
857 }
858
859 #[actix_web::test]
860 async fn test_delete_signer_blocked_by_connected_relayers() {
861 let app_state = create_mock_app_state(None, None, None, None, None, None).await;
862
863 let signer = create_test_signer_model("connected-signer", SignerType::Local);
865 app_state.signer_repository.create(signer).await.unwrap();
866
867 let relayer = crate::models::RelayerRepoModel {
869 id: "test-relayer".to_string(),
870 name: "Test Relayer".to_string(),
871 network: "ethereum".to_string(),
872 paused: false,
873 network_type: crate::models::NetworkType::Evm,
874 signer_id: "connected-signer".to_string(), policies: crate::models::RelayerNetworkPolicy::Evm(
876 crate::models::RelayerEvmPolicy::default(),
877 ),
878 address: "0x742d35Cc6634C0532925a3b844Bc454e4438f44e".to_string(),
879 notification_id: None,
880 system_disabled: false,
881 custom_rpc_urls: None,
882 ..Default::default()
883 };
884 app_state.relayer_repository.create(relayer).await.unwrap();
885
886 let result = delete_signer(
888 "connected-signer".to_string(),
889 actix_web::web::ThinData(app_state),
890 )
891 .await;
892
893 assert!(result.is_err());
894 let error = result.unwrap_err();
895 if let ApiError::BadRequest(msg) = error {
896 assert!(msg.contains("Cannot delete signer"));
897 assert!(msg.contains("being used by"));
898 assert!(msg.contains("Test Relayer"));
899 assert!(msg.contains("remove or reconfigure"));
900 } else {
901 panic!("Expected BadRequest error");
902 }
903 }
904
905 #[actix_web::test]
906 async fn test_delete_signer_not_found() {
907 let app_state = create_mock_app_state(None, None, None, None, None, None).await;
908
909 let result = delete_signer(
910 "non-existent".to_string(),
911 actix_web::web::ThinData(app_state),
912 )
913 .await;
914
915 assert!(result.is_err());
916 let error = result.unwrap_err();
917 assert!(matches!(error, ApiError::NotFound(_)));
918 }
919
920 #[actix_web::test]
921 async fn test_delete_signer_after_relayer_removed() {
922 let app_state = create_mock_app_state(None, None, None, None, None, None).await;
923
924 let signer = create_test_signer_model("cleanup-signer", SignerType::Local);
926 app_state.signer_repository.create(signer).await.unwrap();
927
928 let relayer = crate::models::RelayerRepoModel {
930 id: "temp-relayer".to_string(),
931 name: "Temporary Relayer".to_string(),
932 network: "ethereum".to_string(),
933 paused: false,
934 network_type: crate::models::NetworkType::Evm,
935 signer_id: "cleanup-signer".to_string(),
936 policies: crate::models::RelayerNetworkPolicy::Evm(
937 crate::models::RelayerEvmPolicy::default(),
938 ),
939 address: "0x742d35Cc6634C0532925a3b844Bc454e4438f44e".to_string(),
940 notification_id: None,
941 system_disabled: false,
942 custom_rpc_urls: None,
943 ..Default::default()
944 };
945 app_state.relayer_repository.create(relayer).await.unwrap();
946
947 let result = delete_signer(
949 "cleanup-signer".to_string(),
950 actix_web::web::ThinData(app_state),
951 )
952 .await;
953 assert!(result.is_err());
954
955 let app_state2 = create_mock_app_state(None, None, None, None, None, None).await;
957
958 let signer2 = create_test_signer_model("cleanup-signer", SignerType::Local);
960 app_state2.signer_repository.create(signer2).await.unwrap();
961
962 let result = delete_signer(
964 "cleanup-signer".to_string(),
965 actix_web::web::ThinData(app_state2),
966 )
967 .await;
968
969 assert!(result.is_ok());
970 let response = result.unwrap();
971 assert_eq!(response.status(), 200);
972 }
973
974 #[actix_web::test]
975 async fn test_signer_response_conversion() {
976 let signer_model = SignerRepoModel {
977 id: "test-id".to_string(),
978 config: SignerConfigStorage::Local(LocalSignerConfigStorage {
979 raw_key: SecretVec::new(32, |v| v.copy_from_slice(&[1; 32])),
980 }),
981 };
982
983 let response = SignerResponse::from(signer_model);
984
985 assert_eq!(response.id, "test-id");
986 assert_eq!(response.r#type, SignerType::Local);
987 }
988
989 #[actix_web::test]
990 async fn test_delete_signer_with_multiple_relayers() {
991 let app_state = create_mock_app_state(None, None, None, None, None, None).await;
992
993 let signer = create_test_signer_model("multi-relayer-signer", SignerType::AwsKms);
995 app_state.signer_repository.create(signer).await.unwrap();
996
997 let relayers = vec![
999 crate::models::RelayerRepoModel {
1000 id: "relayer-1".to_string(),
1001 name: "EVM Relayer".to_string(),
1002 network: "ethereum".to_string(),
1003 paused: false,
1004 network_type: crate::models::NetworkType::Evm,
1005 signer_id: "multi-relayer-signer".to_string(),
1006 policies: crate::models::RelayerNetworkPolicy::Evm(
1007 crate::models::RelayerEvmPolicy::default(),
1008 ),
1009 address: "0x1111111111111111111111111111111111111111".to_string(),
1010 notification_id: None,
1011 system_disabled: false,
1012 custom_rpc_urls: None,
1013 ..Default::default()
1014 },
1015 crate::models::RelayerRepoModel {
1016 id: "relayer-2".to_string(),
1017 name: "Solana Relayer".to_string(),
1018 network: "solana".to_string(),
1019 paused: true, network_type: crate::models::NetworkType::Solana,
1021 signer_id: "multi-relayer-signer".to_string(),
1022 policies: crate::models::RelayerNetworkPolicy::Solana(
1023 crate::models::RelayerSolanaPolicy::default(),
1024 ),
1025 address: "solana-address".to_string(),
1026 notification_id: None,
1027 system_disabled: false,
1028 custom_rpc_urls: None,
1029 ..Default::default()
1030 },
1031 crate::models::RelayerRepoModel {
1032 id: "relayer-3".to_string(),
1033 name: "Stellar Relayer".to_string(),
1034 network: "stellar".to_string(),
1035 paused: false,
1036 network_type: crate::models::NetworkType::Stellar,
1037 signer_id: "multi-relayer-signer".to_string(),
1038 policies: crate::models::RelayerNetworkPolicy::Stellar(
1039 crate::models::RelayerStellarPolicy::default(),
1040 ),
1041 address: "stellar-address".to_string(),
1042 notification_id: None,
1043 system_disabled: true, custom_rpc_urls: None,
1045 ..Default::default()
1046 },
1047 ];
1048
1049 for relayer in relayers {
1051 app_state.relayer_repository.create(relayer).await.unwrap();
1052 }
1053
1054 let result = delete_signer(
1056 "multi-relayer-signer".to_string(),
1057 actix_web::web::ThinData(app_state),
1058 )
1059 .await;
1060
1061 assert!(result.is_err());
1062 let error = result.unwrap_err();
1063 if let ApiError::BadRequest(msg) = error {
1064 assert!(msg.contains("Cannot delete signer 'multi-relayer-signer'"));
1065 assert!(msg.contains("being used by 3 relayer(s)"));
1066 assert!(msg.contains("EVM Relayer"));
1067 assert!(msg.contains("Solana Relayer"));
1068 assert!(msg.contains("Stellar Relayer"));
1069 assert!(msg.contains("remove or reconfigure"));
1070 } else {
1071 panic!("Expected BadRequest error, got: {:?}", error);
1072 }
1073 }
1074
1075 #[actix_web::test]
1076 async fn test_delete_signer_with_some_relayers_using_different_signer() {
1077 let app_state = create_mock_app_state(None, None, None, None, None, None).await;
1078
1079 let signer1 = create_test_signer_model("signer-to-delete", SignerType::Local);
1081 let signer2 = create_test_signer_model("other-signer", SignerType::AwsKms);
1082 app_state.signer_repository.create(signer1).await.unwrap();
1083 app_state.signer_repository.create(signer2).await.unwrap();
1084
1085 let relayer1 = crate::models::RelayerRepoModel {
1087 id: "blocking-relayer".to_string(),
1088 name: "Blocking Relayer".to_string(),
1089 network: "ethereum".to_string(),
1090 paused: false,
1091 network_type: crate::models::NetworkType::Evm,
1092 signer_id: "signer-to-delete".to_string(), policies: crate::models::RelayerNetworkPolicy::Evm(
1094 crate::models::RelayerEvmPolicy::default(),
1095 ),
1096 address: "0x1111111111111111111111111111111111111111".to_string(),
1097 notification_id: None,
1098 system_disabled: false,
1099 custom_rpc_urls: None,
1100 ..Default::default()
1101 };
1102
1103 let relayer2 = crate::models::RelayerRepoModel {
1104 id: "non-blocking-relayer".to_string(),
1105 name: "Non-blocking Relayer".to_string(),
1106 network: "polygon".to_string(),
1107 paused: false,
1108 network_type: crate::models::NetworkType::Evm,
1109 signer_id: "other-signer".to_string(), policies: crate::models::RelayerNetworkPolicy::Evm(
1111 crate::models::RelayerEvmPolicy::default(),
1112 ),
1113 address: "0x2222222222222222222222222222222222222222".to_string(),
1114 notification_id: None,
1115 system_disabled: false,
1116 custom_rpc_urls: None,
1117 ..Default::default()
1118 };
1119
1120 app_state.relayer_repository.create(relayer1).await.unwrap();
1121 app_state.relayer_repository.create(relayer2).await.unwrap();
1122
1123 let result = delete_signer(
1125 "signer-to-delete".to_string(),
1126 actix_web::web::ThinData(app_state),
1127 )
1128 .await;
1129
1130 assert!(result.is_err());
1131 let error = result.unwrap_err();
1132 if let ApiError::BadRequest(msg) = error {
1133 assert!(msg.contains("being used by 1 relayer(s)"));
1134 assert!(msg.contains("Blocking Relayer"));
1135 assert!(!msg.contains("Non-blocking Relayer")); } else {
1137 panic!("Expected BadRequest error");
1138 }
1139
1140 let app_state2 = create_mock_app_state(None, None, None, None, None, None).await;
1142 let signer2_recreated = create_test_signer_model("other-signer", SignerType::AwsKms);
1143 app_state2
1144 .signer_repository
1145 .create(signer2_recreated)
1146 .await
1147 .unwrap();
1148
1149 let result = delete_signer(
1150 "other-signer".to_string(),
1151 actix_web::web::ThinData(app_state2),
1152 )
1153 .await;
1154
1155 assert!(result.is_ok());
1156 }
1157}