1use std::str::FromStr;
23
24use alloy::primitives::keccak256;
25use async_trait::async_trait;
26use chrono;
27use p256::{
28 ecdsa::{signature::Signer, Signature as P256Signature, SigningKey},
29 FieldBytes,
30};
31use reqwest::Client;
32use serde::{Deserialize, Serialize};
33use solana_sdk::{pubkey::Pubkey, signature::Signature, transaction::Transaction};
34use stellar_strkey;
35use thiserror::Error;
36use tracing::{debug, info};
37
38use crate::models::{Address, SecretString, TurnkeySignerConfig};
39use crate::utils::base64_url_encode;
40
41#[derive(Error, Debug, Serialize)]
42pub enum TurnkeyError {
43 #[error("HTTP error: {0}")]
44 HttpError(String),
45
46 #[error("API method error: {0:?}")]
47 MethodError(TurnkeyResponseError),
48
49 #[error("Authentication failed: {0}")]
50 AuthenticationFailed(String),
51
52 #[error("Configuration error: {0}")]
53 ConfigError(String),
54
55 #[error("Signing error: {0}")]
56 SigningError(String),
57
58 #[error("Serialization error: {0}")]
59 SerializationError(String),
60
61 #[error("Invalid signature: {0}")]
62 SignatureError(String),
63
64 #[error("Invalid pubkey: {0}")]
65 PubkeyError(#[from] solana_sdk::pubkey::PubkeyError),
66
67 #[error("Other error: {0}")]
68 OtherError(String),
69}
70
71#[derive(Debug, Deserialize, Serialize)]
73pub struct TurnkeyResponseError {
74 pub error: TurnkeyErrorDetails,
75}
76
77#[derive(Debug, Deserialize, Serialize)]
79pub struct TurnkeyErrorDetails {
80 pub code: i32,
81 pub message: String,
82}
83
84pub type TurnkeyResult<T> = Result<T, TurnkeyError>;
86
87#[derive(Serialize)]
89struct ApiStamp {
90 pub public_key: String,
91 pub signature: String,
92 pub scheme: String,
93}
94
95#[derive(Serialize)]
97#[serde(rename_all = "camelCase")]
98struct SignRawPayloadRequest {
99 #[serde(rename = "type")]
100 activity_type: String,
101 timestamp_ms: String,
102 organization_id: String,
103 parameters: SignRawPayloadIntentV2Parameters,
104}
105
106#[derive(Serialize)]
108#[serde(rename_all = "camelCase")]
109struct SignEvmTransactionRequest {
110 #[serde(rename = "type")]
111 activity_type: String,
112 timestamp_ms: String,
113 organization_id: String,
114 parameters: SignEvmTransactionV2Parameters,
115}
116
117#[derive(Serialize)]
119#[serde(rename_all = "camelCase")]
120struct SignRawPayloadIntentV2Parameters {
121 sign_with: String,
122 payload: String,
123 encoding: String,
124 hash_function: String,
125}
126
127#[derive(Serialize)]
129#[serde(rename_all = "camelCase")]
130struct SignEvmTransactionV2Parameters {
131 sign_with: String,
132 #[serde(rename = "type")]
133 sign_type: String,
134 unsigned_transaction: String,
135}
136
137#[derive(Deserialize, Serialize)]
139struct ActivityResponse {
140 activity: Activity,
141}
142
143#[derive(Deserialize, Serialize)]
145#[serde(rename_all = "camelCase")]
146struct Activity {
147 id: Option<String>,
148 status: Option<String>,
149 result: Option<ActivityResult>,
150}
151
152#[derive(Deserialize, Serialize)]
154#[serde(rename_all = "camelCase")]
155struct ActivityResult {
156 sign_raw_payload_result: Option<SignRawPayloadResult>,
157 sign_transaction_result: Option<SignTransactionResult>,
158}
159
160#[derive(Deserialize, Serialize)]
162#[serde(rename_all = "camelCase")]
163struct SignRawPayloadResult {
164 r: String,
165 s: String,
166 v: String,
167}
168
169#[derive(Deserialize, Serialize)]
170#[serde(rename_all = "camelCase")]
171struct SignTransactionResult {
172 signed_transaction: String,
173}
174
175#[cfg(test)]
176use mockall::automock;
177
178#[async_trait]
179#[cfg_attr(test, automock)]
180pub trait TurnkeyServiceTrait: Send + Sync {
181 fn address_solana(&self) -> Result<Address, TurnkeyError>;
183
184 fn address_evm(&self) -> Result<Address, TurnkeyError>;
186
187 fn address_stellar(&self) -> Result<Address, TurnkeyError>;
189
190 async fn sign_solana(&self, message: &[u8]) -> Result<Vec<u8>, TurnkeyError>;
192
193 async fn sign_evm(&self, message: &[u8]) -> Result<Vec<u8>, TurnkeyError>;
195
196 async fn sign_stellar(&self, message: &[u8]) -> Result<Vec<u8>, TurnkeyError>;
198
199 async fn sign_evm_transaction(&self, message: &[u8]) -> Result<Vec<u8>, TurnkeyError>;
201
202 async fn sign_solana_transaction(
204 &self,
205 transaction: &mut Transaction,
206 ) -> TurnkeyResult<(Transaction, Signature)>;
207}
208
209#[derive(Clone, Debug)]
210pub struct TurnkeyService {
211 pub api_public_key: String,
212 pub api_private_key: SecretString,
213 pub organization_id: String,
214 pub private_key_id: String,
215 pub public_key: String,
216 pub base_url: String,
217 client: Client,
218}
219
220impl TurnkeyService {
221 pub fn new(config: TurnkeySignerConfig) -> Result<Self, TurnkeyError> {
222 Ok(Self {
223 api_public_key: config.api_public_key.clone(),
224 api_private_key: config.api_private_key,
225 organization_id: config.organization_id.clone(),
226 private_key_id: config.private_key_id.clone(),
227 public_key: config.public_key.clone(),
228 base_url: String::from("https://api.turnkey.com"),
229 client: Client::new(),
230 })
231 }
232
233 pub fn address_solana(&self) -> Result<Address, TurnkeyError> {
235 if self.public_key.is_empty() {
236 return Err(TurnkeyError::ConfigError("Public key is empty".to_string()));
237 }
238
239 let raw_pubkey = hex::decode(&self.public_key)
240 .map_err(|e| TurnkeyError::ConfigError(format!("Invalid public key hex: {e}")))?;
241
242 let pubkey_bs58 = bs58::encode(&raw_pubkey).into_string();
243
244 Ok(Address::Solana(pubkey_bs58))
245 }
246
247 pub fn address_evm(&self) -> Result<Address, TurnkeyError> {
249 let public_key = hex::decode(&self.public_key)
250 .map_err(|e| TurnkeyError::ConfigError(format!("Invalid public key hex: {e}")))?;
251
252 let pub_key_no_prefix = &public_key[1..];
254
255 let hash = keccak256(pub_key_no_prefix);
256
257 let address_bytes = &hash[12..];
260
261 if address_bytes.len() != 20 {
262 return Err(TurnkeyError::ConfigError(format!(
263 "EVM address should be 20 bytes, got {} bytes",
264 address_bytes.len()
265 )));
266 }
267
268 let mut array = [0u8; 20];
269 array.copy_from_slice(address_bytes);
270
271 Ok(Address::Evm(array))
272 }
273
274 pub fn address_stellar(&self) -> Result<Address, TurnkeyError> {
276 if self.public_key.is_empty() {
277 return Err(TurnkeyError::ConfigError("Public key is empty".to_string()));
278 }
279
280 let raw_pubkey = hex::decode(&self.public_key)
282 .map_err(|e| TurnkeyError::ConfigError(format!("Invalid public key hex: {e}")))?;
283
284 let stellar_address = stellar_strkey::ed25519::PublicKey::from_payload(&raw_pubkey)
286 .map_err(|e| TurnkeyError::ConfigError(format!("Invalid Ed25519 public key: {e}")))?
287 .to_string();
288
289 Ok(Address::Stellar(stellar_address))
290 }
291
292 fn stamp(&self, message: &str) -> TurnkeyResult<String> {
294 let private_api_key_bytes = hex::decode(self.api_private_key.to_str().as_str())
295 .map_err(|e| TurnkeyError::ConfigError(format!("Failed to decode private key: {e}")))?;
296
297 let key_array: [u8; 32] = private_api_key_bytes
298 .as_slice()
299 .try_into()
300 .map_err(|_| TurnkeyError::ConfigError("Invalid private key length".to_string()))?;
301 let signing_key: SigningKey = SigningKey::from_bytes(&FieldBytes::from(key_array))
302 .map_err(|e| TurnkeyError::SigningError(format!("Turnkey stamp error: {e}")))?;
303
304 let signature: P256Signature = signing_key.sign(message.as_bytes());
305
306 let stamp = ApiStamp {
307 public_key: self.api_public_key.clone(),
308 signature: hex::encode(signature.to_der()),
309 scheme: "SIGNATURE_SCHEME_TK_API_P256".into(),
310 };
311
312 let json_stamp = serde_json::to_string(&stamp).map_err(|e| {
313 TurnkeyError::SerializationError(format!("Serialization stamp error: {e}"))
314 })?;
315 let encoded_stamp = base64_url_encode(json_stamp.as_bytes());
316
317 Ok(encoded_stamp)
318 }
319
320 async fn make_turnkey_request<T, R>(&self, endpoint: &str, request_body: &T) -> TurnkeyResult<R>
322 where
323 T: Serialize,
324 R: for<'de> Deserialize<'de> + 'static,
325 {
326 let body = serde_json::to_string(request_body).map_err(|e| {
328 TurnkeyError::SerializationError(format!("Request serialization error: {e}"))
329 })?;
330
331 let x_stamp = self.stamp(&body)?;
333
334 debug!(endpoint = %endpoint, "sending request to turnkey api");
335 let response = self
336 .client
337 .post(format!("{}/public/v1/submit/{}", self.base_url, endpoint))
338 .header("Content-Type", "application/json")
339 .header("X-Stamp", x_stamp)
340 .body(body)
341 .send()
342 .await;
343
344 self.process_response::<R>(response).await
345 }
346
347 async fn sign_raw_payload(
349 &self,
350 payload: &[u8],
351 hash_function: &str,
352 include_v: bool,
353 ) -> TurnkeyResult<Vec<u8>> {
354 let encoded_payload = hex::encode(payload);
355
356 let sign_raw_payload_body = SignRawPayloadRequest {
357 activity_type: "ACTIVITY_TYPE_SIGN_RAW_PAYLOAD_V2".to_string(),
358 timestamp_ms: chrono::Utc::now().timestamp_millis().to_string(),
359 organization_id: self.organization_id.clone(),
360 parameters: SignRawPayloadIntentV2Parameters {
361 sign_with: self.private_key_id.clone(),
362 payload: encoded_payload,
363 encoding: "PAYLOAD_ENCODING_HEXADECIMAL".to_string(),
364 hash_function: hash_function.to_string(),
365 },
366 };
367
368 let response_body = self
369 .make_turnkey_request::<_, ActivityResponse>("sign_raw_payload", &sign_raw_payload_body)
370 .await?;
371
372 if let Some(result) = response_body.activity.result {
373 if let Some(result) = result.sign_raw_payload_result {
374 let concatenated_hex = if include_v {
375 format!("{}{}{}", result.r, result.s, result.v)
376 } else {
377 format!("{}{}", result.r, result.s)
378 };
379
380 let signature_bytes = hex::decode(&concatenated_hex).map_err(|e| {
381 TurnkeyError::SigningError(format!("Turnkey signing error {e}"))
382 })?;
383
384 return Ok(signature_bytes);
385 }
386 }
387
388 Err(TurnkeyError::OtherError(
389 "Missing SIGN_RAW_PAYLOAD result".into(),
390 ))
391 }
392
393 async fn sign_bytes_solana(&self, bytes: &[u8]) -> TurnkeyResult<Vec<u8>> {
395 self.sign_raw_payload(bytes, "HASH_FUNCTION_NOT_APPLICABLE", false)
396 .await
397 }
398
399 async fn sign_bytes_evm(&self, bytes: &[u8]) -> TurnkeyResult<Vec<u8>> {
401 let result = self
402 .sign_raw_payload(bytes, "HASH_FUNCTION_NO_OP", true)
403 .await?;
404 debug!(signature_length = %result.len(), "evm signature length");
405 Ok(result)
406 }
407
408 async fn sign_bytes_stellar(&self, bytes: &[u8]) -> TurnkeyResult<Vec<u8>> {
410 use sha2::{Digest, Sha256};
411 let hash = Sha256::digest(bytes);
412
413 self.sign_raw_payload(&hash, "HASH_FUNCTION_NOT_APPLICABLE", false)
414 .await
415 }
416
417 async fn sign_evm_transaction_impl(&self, bytes: &[u8]) -> TurnkeyResult<Vec<u8>> {
419 let encoded_bytes = hex::encode(bytes);
420
421 let sign_transaction_body = SignEvmTransactionRequest {
423 activity_type: "ACTIVITY_TYPE_SIGN_TRANSACTION_V2".to_string(),
424 timestamp_ms: chrono::Utc::now().timestamp_millis().to_string(),
425 organization_id: self.organization_id.clone(),
426 parameters: SignEvmTransactionV2Parameters {
427 sign_with: self.private_key_id.clone(),
428 sign_type: "TRANSACTION_TYPE_ETHEREUM".to_string(),
429 unsigned_transaction: encoded_bytes,
430 },
431 };
432
433 let response_body = self
435 .make_turnkey_request::<_, ActivityResponse>("sign_transaction", &sign_transaction_body)
436 .await?;
437
438 response_body
440 .activity
441 .result
442 .and_then(|result| result.sign_transaction_result)
443 .map(|tx_result| hex::decode(&tx_result.signed_transaction))
444 .transpose()
445 .map_err(|e| TurnkeyError::SigningError(format!("Failed to decode transaction: {e}")))?
446 .ok_or_else(|| TurnkeyError::OtherError("Missing transaction result".into()))
447 }
448
449 async fn process_response<T>(
450 &self,
451 response: Result<reqwest::Response, reqwest::Error>,
452 ) -> TurnkeyResult<T>
453 where
454 T: for<'de> Deserialize<'de> + 'static,
455 {
456 match response {
457 Ok(res) => {
458 let status = res.status();
459 let headers = res.headers().clone();
460 let content_type = headers
461 .get("content-type")
462 .and_then(|v| v.to_str().ok())
463 .unwrap_or("unknown");
464
465 if res.status().is_success() {
466 res.json::<T>()
468 .await
469 .map_err(|e| TurnkeyError::HttpError(e.to_string()))
470 } else {
471 match res.text().await {
473 Ok(body_text) => {
474 debug!(status = %status, body_text = %body_text, "error response");
475
476 if content_type.contains("application/json") {
477 match serde_json::from_str::<TurnkeyResponseError>(&body_text) {
478 Ok(error) => Err(TurnkeyError::MethodError(error)),
479 Err(e) => {
480 debug!(error = %e, "failed to parse error response as json");
481 Err(TurnkeyError::HttpError(format!(
482 "HTTP {status} error: {body_text}"
483 )))
484 }
485 }
486 } else {
487 Err(TurnkeyError::HttpError(format!(
488 "HTTP {status} error: {body_text}"
489 )))
490 }
491 }
492 Err(e) => {
493 info!(error = %e, "failed to read error response body");
494 Err(TurnkeyError::HttpError(format!(
495 "HTTP {status} error (failed to read body): {e}"
496 )))
497 }
498 }
499 }
500 }
501 Err(e) => {
502 debug!(error = ?e, "turnkey api request error");
503 Err(TurnkeyError::HttpError(e.to_string()))
505 }
506 }
507 }
508}
509
510#[async_trait]
511impl TurnkeyServiceTrait for TurnkeyService {
512 fn address_solana(&self) -> Result<Address, TurnkeyError> {
513 if self.public_key.is_empty() {
514 return Err(TurnkeyError::ConfigError("Public key is empty".to_string()));
515 }
516
517 let raw_pubkey = hex::decode(&self.public_key)
518 .map_err(|e| TurnkeyError::ConfigError(format!("Invalid public key hex: {e}")))?;
519
520 let pubkey_bs58 = bs58::encode(&raw_pubkey).into_string();
521
522 Ok(Address::Solana(pubkey_bs58))
523 }
524
525 fn address_evm(&self) -> Result<Address, TurnkeyError> {
526 let public_key = hex::decode(&self.public_key)
527 .map_err(|e| TurnkeyError::ConfigError(format!("Invalid public key hex: {e}")))?;
528
529 let pub_key_no_prefix = &public_key[1..];
531
532 let hash = keccak256(pub_key_no_prefix);
533
534 let address_bytes = &hash[12..];
537
538 if address_bytes.len() != 20 {
539 return Err(TurnkeyError::ConfigError(format!(
540 "EVM address should be 20 bytes, got {} bytes",
541 address_bytes.len()
542 )));
543 }
544
545 let mut array = [0u8; 20];
546 array.copy_from_slice(address_bytes);
547
548 Ok(Address::Evm(array))
549 }
550
551 fn address_stellar(&self) -> Result<Address, TurnkeyError> {
552 self.address_stellar()
553 }
554
555 async fn sign_solana(&self, message: &[u8]) -> Result<Vec<u8>, TurnkeyError> {
556 let signature_bytes = self.sign_bytes_solana(message).await?;
557 Ok(signature_bytes)
558 }
559
560 async fn sign_evm(&self, message: &[u8]) -> Result<Vec<u8>, TurnkeyError> {
561 let signature_bytes = self.sign_bytes_evm(message).await?;
562 Ok(signature_bytes)
563 }
564
565 async fn sign_stellar(&self, message: &[u8]) -> Result<Vec<u8>, TurnkeyError> {
566 let signature_bytes = self.sign_bytes_stellar(message).await?;
567 Ok(signature_bytes)
568 }
569
570 async fn sign_evm_transaction(&self, message: &[u8]) -> Result<Vec<u8>, TurnkeyError> {
571 self.sign_evm_transaction_impl(message).await
572 }
573
574 async fn sign_solana_transaction(
575 &self,
576 transaction: &mut Transaction,
577 ) -> TurnkeyResult<(Transaction, Signature)> {
578 let serialized_message = transaction.message_data();
579
580 let public_key = Pubkey::from_str(&self.address_solana()?.to_string())
581 .map_err(|e| TurnkeyError::ConfigError(format!("Invalid pubkey: {e}")))?;
582
583 let signature_bytes = self.sign_bytes_solana(&serialized_message).await?;
584
585 let signature = Signature::try_from(signature_bytes.as_slice())
586 .map_err(|e| TurnkeyError::SignatureError(format!("Invalid signature: {e}")))?;
587
588 let index = transaction
589 .message
590 .account_keys
591 .iter()
592 .position(|key| key == &public_key);
593
594 match index {
595 Some(i) if i < transaction.signatures.len() => {
596 transaction.signatures[i] = signature;
597 Ok((transaction.clone(), signature))
598 }
599 _ => Err(TurnkeyError::OtherError(
600 "Unknown signer or index out of bounds".into(),
601 )),
602 }
603 }
604}
605
606#[cfg(test)]
607mod tests {
608 use super::*;
609 use mockito;
610 use serde_json::json;
611
612 fn create_solana_test_config() -> TurnkeySignerConfig {
613 TurnkeySignerConfig {
614 api_public_key: "test-api-public-key".to_string(),
615 api_private_key: SecretString::new(
616 "0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef",
617 ),
618 organization_id: "test-org-id".to_string(),
619 private_key_id: "test-private-key-id".to_string(),
620 public_key: "5720be8aa9d2bb4be8e91f31d2c44c8629e42da16981c2cebabd55cafa0b76bd"
621 .to_string(),
622 }
623 }
624
625 fn create_evm_test_config() -> TurnkeySignerConfig {
626 TurnkeySignerConfig {
627 api_public_key: "test-api-public-key".to_string(),
628 api_private_key: SecretString::new("0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef"),
629 organization_id: "test-org-id".to_string(),
630 private_key_id: "test-private-key-id".to_string(),
631 public_key: "047d3bb8e0317927700cf19fed34e0627367be1390ec247dddf8c239e4b4321a49aea80090e49b206b6a3e577a4f11d721ab063482001ee10db40d6f2963233eec".to_string(),
632 }
633 }
634
635 #[test]
636 fn test_new_turnkey_service() {
637 let config = create_evm_test_config();
638 let service = TurnkeyService::new(config);
639
640 assert!(service.is_ok());
641 let service = service.unwrap();
642 assert_eq!(service.api_public_key, "test-api-public-key");
643 assert_eq!(service.organization_id, "test-org-id");
644 assert_eq!(service.private_key_id, "test-private-key-id");
645 }
646
647 #[test]
648 fn test_address_evm() {
649 let config = create_evm_test_config();
650 let service = TurnkeyService::new(config).unwrap();
651
652 let address = service.address_evm();
653 assert!(address.is_ok());
654
655 let address = address.unwrap();
656
657 assert_eq!(
658 address.to_string(),
659 "0xb726167dc2ef2ac582f0a3de4c08ac4abb90626a"
660 );
661 }
662
663 #[test]
664 fn test_address_solana() {
665 let config = create_solana_test_config();
666 let service = TurnkeyService::new(config).unwrap();
667
668 let address = service.address_solana();
669 assert!(address.is_ok());
670
671 let address_str = address.unwrap().to_string();
672 assert_eq!(address_str, "6s7RsvzcdXFJi1tXeDoGfSKZFzN3juVt9fTar6WEhEm2");
673 }
674
675 #[test]
676 fn test_address_with_empty_pubkey() {
677 let mut config = create_solana_test_config();
678 config.public_key = "".to_string();
679 let service = TurnkeyService::new(config).unwrap();
680
681 let result = service.address_solana();
682 assert!(result.is_err());
683 if let Err(e) = result {
684 assert!(matches!(e, TurnkeyError::ConfigError(_)));
685 assert_eq!(e.to_string(), "Configuration error: Public key is empty");
686 }
687 }
688
689 #[test]
690 fn test_address_with_invalid_pubkey() {
691 let mut config = create_solana_test_config();
692 config.public_key = "invalid-hex".to_string();
693 let service = TurnkeyService::new(config).unwrap();
694
695 let result = service.address_evm();
696 assert!(result.is_err());
697 if let Err(e) = result {
698 assert!(matches!(e, TurnkeyError::ConfigError(_)));
699 assert!(e.to_string().contains("Invalid public key hex"));
700 }
701 }
702
703 async fn setup_mock_sign_raw_payload(mock_server: &mut mockito::ServerGuard) -> mockito::Mock {
705 mock_server
706 .mock("POST", "/public/v1/submit/sign_raw_payload")
707 .match_header("Content-Type", "application/json")
708 .match_header("X-Stamp", mockito::Matcher::Any)
709 .with_status(200)
710 .with_header("content-type", "application/json")
711 .with_body(serde_json::to_string(&json!({
712 "activity": {
713 "id": "test-activity-id",
714 "status": "ACTIVITY_STATUS_COMPLETE",
715 "result": {
716 "signRawPayloadResult": {
717 "r": "0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef",
718 "s": "fedcba9876543210fedcba9876543210fedcba9876543210fedcba9876543210",
719 "v": "1b"
720 }
721 }
722 }
723 })).unwrap())
724 .expect(1)
725 .create_async()
726 .await
727 }
728
729 async fn setup_mock_sign_evm_transaction(
731 mock_server: &mut mockito::ServerGuard,
732 ) -> mockito::Mock {
733 mock_server
734 .mock("POST", "/public/v1/submit/sign_transaction")
735 .match_header("Content-Type", "application/json")
736 .match_header("X-Stamp", mockito::Matcher::Any)
737 .with_status(200)
738 .with_header("content-type", "application/json")
739 .with_body(
740 serde_json::to_string(&json!({
741 "activity": {
742 "id": "test-activity-id",
743 "status": "ACTIVITY_STATUS_COMPLETE",
744 "result": {
745 "signTransactionResult": {
746 "signedTransaction": "02f1010203050607080910" }
748 }
749 }
750 }))
751 .unwrap(),
752 )
753 .expect(1)
754 .create_async()
755 .await
756 }
757
758 async fn setup_mock_error_response(mock_server: &mut mockito::ServerGuard) -> mockito::Mock {
760 mock_server
761 .mock("POST", "/public/v1/submit/sign_raw_payload")
762 .match_header("Content-Type", "application/json")
763 .match_header("X-Stamp", mockito::Matcher::Any)
764 .with_status(400)
765 .with_header("content-type", "application/json")
766 .with_body(
767 serde_json::to_string(&json!({
768 "error": {
769 "code": 400,
770 "message": "Invalid payload format"
771 }
772 }))
773 .unwrap(),
774 )
775 .expect(1)
776 .create_async()
777 .await
778 }
779
780 fn create_test_client() -> Client {
782 reqwest::ClientBuilder::new()
783 .redirect(reqwest::redirect::Policy::none())
784 .build()
785 .unwrap()
786 }
787
788 #[tokio::test]
789 async fn test_sign_solana() {
790 let mut mock_server = mockito::Server::new_async().await;
791 let _mock = setup_mock_sign_raw_payload(&mut mock_server).await;
792
793 let config = create_solana_test_config();
794
795 let service = TurnkeyService {
796 api_public_key: config.api_public_key,
797 api_private_key: config.api_private_key,
798 organization_id: config.organization_id,
799 private_key_id: config.private_key_id,
800 public_key: config.public_key,
801 base_url: mock_server.url(),
802 client: create_test_client(),
803 };
804
805 let message = b"test message";
806 let result = service.sign_solana(message).await;
807
808 assert!(result.is_ok());
809 }
810
811 #[tokio::test]
812 async fn test_sign_evm() {
813 let mut mock_server = mockito::Server::new_async().await;
814 let _mock = setup_mock_sign_raw_payload(&mut mock_server).await;
815
816 let config = create_evm_test_config();
817 let service = TurnkeyService {
818 api_public_key: config.api_public_key,
819 api_private_key: config.api_private_key,
820 organization_id: config.organization_id,
821 private_key_id: config.private_key_id,
822 public_key: config.public_key,
823 base_url: mock_server.url(),
824 client: create_test_client(),
825 };
826
827 let message = b"test message";
828 let result = service.sign_evm(message).await;
829
830 assert!(result.is_ok());
831 }
832
833 #[tokio::test]
834 async fn test_sign_evm_transaction() {
835 let mut mock_server = mockito::Server::new_async().await;
836 let _mock = setup_mock_sign_evm_transaction(&mut mock_server).await;
837
838 let config = create_evm_test_config();
839 let service = TurnkeyService {
840 api_public_key: config.api_public_key,
841 api_private_key: config.api_private_key,
842 organization_id: config.organization_id,
843 private_key_id: config.private_key_id,
844 public_key: config.public_key,
845 base_url: mock_server.url(),
846 client: create_test_client(),
847 };
848
849 let message = b"test transaction";
850 let result = service.sign_evm_transaction(message).await;
851
852 assert!(result.is_ok());
853 let result = result.unwrap();
854 let expected = hex::decode("02f1010203050607080910").unwrap();
855 assert_eq!(result, expected)
856 }
857
858 #[tokio::test]
859 async fn test_error_handling() {
860 let mut mock_server = mockito::Server::new_async().await;
861 let _mock = setup_mock_error_response(&mut mock_server).await;
862
863 let config = create_solana_test_config();
864 let service = TurnkeyService {
865 api_public_key: config.api_public_key,
866 api_private_key: config.api_private_key,
867 organization_id: config.organization_id,
868 private_key_id: config.private_key_id,
869 public_key: config.public_key,
870 base_url: mock_server.url(),
871 client: create_test_client(),
872 };
873
874 let message = b"test message";
875 let result = service.sign_solana(message).await;
876 assert!(result.is_err());
877 match result {
878 Err(TurnkeyError::MethodError(e)) => {
879 assert!(e.error.message.contains("Invalid payload format"));
880 }
881 _ => panic!("Expected MethodError for Solana signing"),
882 }
883 }
884}