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)]
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 =
295 hex::decode(self.api_private_key.to_str().as_str()).map_err(|e| {
296 TurnkeyError::ConfigError(format!("Failed to decode private key: {}", e))
297 })?;
298
299 let signing_key: SigningKey =
300 SigningKey::from_bytes(FieldBytes::from_slice(&private_api_key_bytes))
301 .map_err(|e| TurnkeyError::SigningError(format!("Turnkey stamp error: {}", e)))?;
302
303 let signature: P256Signature = signing_key.sign(message.as_bytes());
304
305 let stamp = ApiStamp {
306 public_key: self.api_public_key.clone(),
307 signature: hex::encode(signature.to_der()),
308 scheme: "SIGNATURE_SCHEME_TK_API_P256".into(),
309 };
310
311 let json_stamp = serde_json::to_string(&stamp).map_err(|e| {
312 TurnkeyError::SerializationError(format!("Serialization stamp error: {}", e))
313 })?;
314 let encoded_stamp = base64_url_encode(json_stamp.as_bytes());
315
316 Ok(encoded_stamp)
317 }
318
319 async fn make_turnkey_request<T, R>(&self, endpoint: &str, request_body: &T) -> TurnkeyResult<R>
321 where
322 T: Serialize,
323 R: for<'de> Deserialize<'de> + 'static,
324 {
325 let body = serde_json::to_string(request_body).map_err(|e| {
327 TurnkeyError::SerializationError(format!("Request serialization error: {}", e))
328 })?;
329
330 let x_stamp = self.stamp(&body)?;
332
333 debug!(endpoint = %endpoint, "sending request to turnkey api");
334 let response = self
335 .client
336 .post(format!("{}/public/v1/submit/{}", self.base_url, endpoint))
337 .header("Content-Type", "application/json")
338 .header("X-Stamp", x_stamp)
339 .body(body)
340 .send()
341 .await;
342
343 self.process_response::<R>(response).await
344 }
345
346 async fn sign_raw_payload(
348 &self,
349 payload: &[u8],
350 hash_function: &str,
351 include_v: bool,
352 ) -> TurnkeyResult<Vec<u8>> {
353 let encoded_payload = hex::encode(payload);
354
355 let sign_raw_payload_body = SignRawPayloadRequest {
356 activity_type: "ACTIVITY_TYPE_SIGN_RAW_PAYLOAD_V2".to_string(),
357 timestamp_ms: chrono::Utc::now().timestamp_millis().to_string(),
358 organization_id: self.organization_id.clone(),
359 parameters: SignRawPayloadIntentV2Parameters {
360 sign_with: self.private_key_id.clone(),
361 payload: encoded_payload,
362 encoding: "PAYLOAD_ENCODING_HEXADECIMAL".to_string(),
363 hash_function: hash_function.to_string(),
364 },
365 };
366
367 let response_body = self
368 .make_turnkey_request::<_, ActivityResponse>("sign_raw_payload", &sign_raw_payload_body)
369 .await?;
370
371 if let Some(result) = response_body.activity.result {
372 if let Some(result) = result.sign_raw_payload_result {
373 let concatenated_hex = if include_v {
374 format!("{}{}{}", result.r, result.s, result.v)
375 } else {
376 format!("{}{}", result.r, result.s)
377 };
378
379 let signature_bytes = hex::decode(&concatenated_hex).map_err(|e| {
380 TurnkeyError::SigningError(format!("Turnkey signing error {}", e))
381 })?;
382
383 return Ok(signature_bytes);
384 }
385 }
386
387 Err(TurnkeyError::OtherError(
388 "Missing SIGN_RAW_PAYLOAD result".into(),
389 ))
390 }
391
392 async fn sign_bytes_solana(&self, bytes: &[u8]) -> TurnkeyResult<Vec<u8>> {
394 self.sign_raw_payload(bytes, "HASH_FUNCTION_NOT_APPLICABLE", false)
395 .await
396 }
397
398 async fn sign_bytes_evm(&self, bytes: &[u8]) -> TurnkeyResult<Vec<u8>> {
400 let result = self
401 .sign_raw_payload(bytes, "HASH_FUNCTION_NO_OP", true)
402 .await?;
403 debug!(signature_length = %result.len(), "evm signature length");
404 Ok(result)
405 }
406
407 async fn sign_bytes_stellar(&self, bytes: &[u8]) -> TurnkeyResult<Vec<u8>> {
409 use sha2::{Digest, Sha256};
410 let hash = Sha256::digest(bytes);
411
412 self.sign_raw_payload(&hash, "HASH_FUNCTION_NOT_APPLICABLE", false)
413 .await
414 }
415
416 async fn sign_evm_transaction(&self, bytes: &[u8]) -> TurnkeyResult<Vec<u8>> {
418 let encoded_bytes = hex::encode(bytes);
419
420 let sign_transaction_body = SignEvmTransactionRequest {
422 activity_type: "ACTIVITY_TYPE_SIGN_TRANSACTION_V2".to_string(),
423 timestamp_ms: chrono::Utc::now().timestamp_millis().to_string(),
424 organization_id: self.organization_id.clone(),
425 parameters: SignEvmTransactionV2Parameters {
426 sign_with: self.private_key_id.clone(),
427 sign_type: "TRANSACTION_TYPE_ETHEREUM".to_string(),
428 unsigned_transaction: encoded_bytes,
429 },
430 };
431
432 let response_body = self
434 .make_turnkey_request::<_, ActivityResponse>("sign_transaction", &sign_transaction_body)
435 .await?;
436
437 response_body
439 .activity
440 .result
441 .and_then(|result| result.sign_transaction_result)
442 .map(|tx_result| hex::decode(&tx_result.signed_transaction))
443 .transpose()
444 .map_err(|e| {
445 TurnkeyError::SigningError(format!("Failed to decode transaction: {}", e))
446 })?
447 .ok_or_else(|| TurnkeyError::OtherError("Missing transaction result".into()))
448 }
449
450 async fn process_response<T>(
451 &self,
452 response: Result<reqwest::Response, reqwest::Error>,
453 ) -> TurnkeyResult<T>
454 where
455 T: for<'de> Deserialize<'de> + 'static,
456 {
457 match response {
458 Ok(res) => {
459 let status = res.status();
460 let headers = res.headers().clone();
461 let content_type = headers
462 .get("content-type")
463 .and_then(|v| v.to_str().ok())
464 .unwrap_or("unknown");
465
466 if res.status().is_success() {
467 res.json::<T>()
469 .await
470 .map_err(|e| TurnkeyError::HttpError(e.to_string()))
471 } else {
472 match res.text().await {
474 Ok(body_text) => {
475 debug!(status = %status, body_text = %body_text, "error response");
476
477 if content_type.contains("application/json") {
478 match serde_json::from_str::<TurnkeyResponseError>(&body_text) {
479 Ok(error) => Err(TurnkeyError::MethodError(error)),
480 Err(e) => {
481 debug!(error = %e, "failed to parse error response as json");
482 Err(TurnkeyError::HttpError(format!(
483 "HTTP {} error: {}",
484 status, body_text
485 )))
486 }
487 }
488 } else {
489 Err(TurnkeyError::HttpError(format!(
490 "HTTP {} error: {}",
491 status, body_text
492 )))
493 }
494 }
495 Err(e) => {
496 info!(error = %e, "failed to read error response body");
497 Err(TurnkeyError::HttpError(format!(
498 "HTTP {} error (failed to read body): {}",
499 status, e
500 )))
501 }
502 }
503 }
504 }
505 Err(e) => {
506 debug!(error = ?e, "turnkey api request error");
507 Err(TurnkeyError::HttpError(e.to_string()))
509 }
510 }
511 }
512}
513
514#[async_trait]
515impl TurnkeyServiceTrait for TurnkeyService {
516 fn address_solana(&self) -> Result<Address, TurnkeyError> {
517 self.address_solana()
518 }
519
520 fn address_evm(&self) -> Result<Address, TurnkeyError> {
521 self.address_evm()
522 }
523
524 fn address_stellar(&self) -> Result<Address, TurnkeyError> {
525 self.address_stellar()
526 }
527
528 async fn sign_solana(&self, message: &[u8]) -> Result<Vec<u8>, TurnkeyError> {
529 let signature_bytes = self.sign_bytes_solana(message).await?;
530 Ok(signature_bytes)
531 }
532
533 async fn sign_evm(&self, message: &[u8]) -> Result<Vec<u8>, TurnkeyError> {
534 let signature_bytes = self.sign_bytes_evm(message).await?;
535 Ok(signature_bytes)
536 }
537
538 async fn sign_stellar(&self, message: &[u8]) -> Result<Vec<u8>, TurnkeyError> {
539 let signature_bytes = self.sign_bytes_stellar(message).await?;
540 Ok(signature_bytes)
541 }
542
543 async fn sign_evm_transaction(&self, message: &[u8]) -> Result<Vec<u8>, TurnkeyError> {
544 let signature_bytes = self.sign_evm_transaction(message).await?;
545 Ok(signature_bytes)
546 }
547
548 async fn sign_solana_transaction(
549 &self,
550 transaction: &mut Transaction,
551 ) -> TurnkeyResult<(Transaction, Signature)> {
552 let serialized_message = transaction.message_data();
553
554 let public_key = Pubkey::from_str(&self.address_solana()?.to_string())
555 .map_err(|e| TurnkeyError::ConfigError(format!("Invalid pubkey: {}", e)))?;
556
557 let signature_bytes = self.sign_bytes_solana(&serialized_message).await?;
558
559 let signature = Signature::try_from(signature_bytes.as_slice())
560 .map_err(|e| TurnkeyError::SignatureError(format!("Invalid signature: {}", e)))?;
561
562 let index = transaction
563 .message
564 .account_keys
565 .iter()
566 .position(|key| key == &public_key);
567
568 match index {
569 Some(i) if i < transaction.signatures.len() => {
570 transaction.signatures[i] = signature;
571 Ok((transaction.clone(), signature))
572 }
573 _ => Err(TurnkeyError::OtherError(
574 "Unknown signer or index out of bounds".into(),
575 )),
576 }
577 }
578}
579
580#[cfg(test)]
581mod tests {
582 use super::*;
583 use mockito;
584 use serde_json::json;
585
586 fn create_solana_test_config() -> TurnkeySignerConfig {
587 TurnkeySignerConfig {
588 api_public_key: "test-api-public-key".to_string(),
589 api_private_key: SecretString::new(
590 "0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef",
591 ),
592 organization_id: "test-org-id".to_string(),
593 private_key_id: "test-private-key-id".to_string(),
594 public_key: "5720be8aa9d2bb4be8e91f31d2c44c8629e42da16981c2cebabd55cafa0b76bd"
595 .to_string(),
596 }
597 }
598
599 fn create_evm_test_config() -> TurnkeySignerConfig {
600 TurnkeySignerConfig {
601 api_public_key: "test-api-public-key".to_string(),
602 api_private_key: SecretString::new("0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef"),
603 organization_id: "test-org-id".to_string(),
604 private_key_id: "test-private-key-id".to_string(),
605 public_key: "047d3bb8e0317927700cf19fed34e0627367be1390ec247dddf8c239e4b4321a49aea80090e49b206b6a3e577a4f11d721ab063482001ee10db40d6f2963233eec".to_string(),
606 }
607 }
608
609 #[test]
610 fn test_new_turnkey_service() {
611 let config = create_evm_test_config();
612 let service = TurnkeyService::new(config);
613
614 assert!(service.is_ok());
615 let service = service.unwrap();
616 assert_eq!(service.api_public_key, "test-api-public-key");
617 assert_eq!(service.organization_id, "test-org-id");
618 assert_eq!(service.private_key_id, "test-private-key-id");
619 }
620
621 #[test]
622 fn test_address_evm() {
623 let config = create_evm_test_config();
624 let service = TurnkeyService::new(config).unwrap();
625
626 let address = service.address_evm();
627 assert!(address.is_ok());
628
629 let address = address.unwrap();
630
631 assert_eq!(
632 address.to_string(),
633 "0xb726167dc2ef2ac582f0a3de4c08ac4abb90626a"
634 );
635 }
636
637 #[test]
638 fn test_address_solana() {
639 let config = create_solana_test_config();
640 let service = TurnkeyService::new(config).unwrap();
641
642 let address = service.address_solana();
643 assert!(address.is_ok());
644
645 let address_str = address.unwrap().to_string();
646 assert_eq!(address_str, "6s7RsvzcdXFJi1tXeDoGfSKZFzN3juVt9fTar6WEhEm2");
647 }
648
649 #[test]
650 fn test_address_with_empty_pubkey() {
651 let mut config = create_solana_test_config();
652 config.public_key = "".to_string();
653 let service = TurnkeyService::new(config).unwrap();
654
655 let result = service.address_solana();
656 assert!(result.is_err());
657 if let Err(e) = result {
658 assert!(matches!(e, TurnkeyError::ConfigError(_)));
659 assert_eq!(e.to_string(), "Configuration error: Public key is empty");
660 }
661 }
662
663 #[test]
664 fn test_address_with_invalid_pubkey() {
665 let mut config = create_solana_test_config();
666 config.public_key = "invalid-hex".to_string();
667 let service = TurnkeyService::new(config).unwrap();
668
669 let result = service.address_evm();
670 assert!(result.is_err());
671 if let Err(e) = result {
672 assert!(matches!(e, TurnkeyError::ConfigError(_)));
673 assert!(e.to_string().contains("Invalid public key hex"));
674 }
675 }
676
677 async fn setup_mock_sign_raw_payload(mock_server: &mut mockito::ServerGuard) -> mockito::Mock {
679 mock_server
680 .mock("POST", "/public/v1/submit/sign_raw_payload")
681 .match_header("Content-Type", "application/json")
682 .match_header("X-Stamp", mockito::Matcher::Any)
683 .with_status(200)
684 .with_header("content-type", "application/json")
685 .with_body(serde_json::to_string(&json!({
686 "activity": {
687 "id": "test-activity-id",
688 "status": "ACTIVITY_STATUS_COMPLETE",
689 "result": {
690 "signRawPayloadResult": {
691 "r": "0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef",
692 "s": "fedcba9876543210fedcba9876543210fedcba9876543210fedcba9876543210",
693 "v": "1b"
694 }
695 }
696 }
697 })).unwrap())
698 .expect(1)
699 .create_async()
700 .await
701 }
702
703 async fn setup_mock_sign_evm_transaction(
705 mock_server: &mut mockito::ServerGuard,
706 ) -> mockito::Mock {
707 mock_server
708 .mock("POST", "/public/v1/submit/sign_transaction")
709 .match_header("Content-Type", "application/json")
710 .match_header("X-Stamp", mockito::Matcher::Any)
711 .with_status(200)
712 .with_header("content-type", "application/json")
713 .with_body(
714 serde_json::to_string(&json!({
715 "activity": {
716 "id": "test-activity-id",
717 "status": "ACTIVITY_STATUS_COMPLETE",
718 "result": {
719 "signTransactionResult": {
720 "signedTransaction": "02f1010203050607080910" }
722 }
723 }
724 }))
725 .unwrap(),
726 )
727 .expect(1)
728 .create_async()
729 .await
730 }
731
732 async fn setup_mock_error_response(mock_server: &mut mockito::ServerGuard) -> mockito::Mock {
734 mock_server
735 .mock("POST", "/public/v1/submit/sign_raw_payload")
736 .match_header("Content-Type", "application/json")
737 .match_header("X-Stamp", mockito::Matcher::Any)
738 .with_status(400)
739 .with_header("content-type", "application/json")
740 .with_body(
741 serde_json::to_string(&json!({
742 "error": {
743 "code": 400,
744 "message": "Invalid payload format"
745 }
746 }))
747 .unwrap(),
748 )
749 .expect(1)
750 .create_async()
751 .await
752 }
753
754 fn create_test_client() -> Client {
756 reqwest::ClientBuilder::new()
757 .redirect(reqwest::redirect::Policy::none())
758 .build()
759 .unwrap()
760 }
761
762 #[tokio::test]
763 async fn test_sign_solana() {
764 let mut mock_server = mockito::Server::new_async().await;
765 let _mock = setup_mock_sign_raw_payload(&mut mock_server).await;
766
767 let config = create_solana_test_config();
768
769 let service = TurnkeyService {
770 api_public_key: config.api_public_key,
771 api_private_key: config.api_private_key,
772 organization_id: config.organization_id,
773 private_key_id: config.private_key_id,
774 public_key: config.public_key,
775 base_url: mock_server.url(),
776 client: create_test_client(),
777 };
778
779 let message = b"test message";
780 let result = service.sign_solana(message).await;
781
782 assert!(result.is_ok());
783 }
784
785 #[tokio::test]
786 async fn test_sign_evm() {
787 let mut mock_server = mockito::Server::new_async().await;
788 let _mock = setup_mock_sign_raw_payload(&mut mock_server).await;
789
790 let config = create_evm_test_config();
791 let service = TurnkeyService {
792 api_public_key: config.api_public_key,
793 api_private_key: config.api_private_key,
794 organization_id: config.organization_id,
795 private_key_id: config.private_key_id,
796 public_key: config.public_key,
797 base_url: mock_server.url(),
798 client: create_test_client(),
799 };
800
801 let message = b"test message";
802 let result = service.sign_evm(message).await;
803
804 assert!(result.is_ok());
805 }
806
807 #[tokio::test]
808 async fn test_sign_evm_transaction() {
809 let mut mock_server = mockito::Server::new_async().await;
810 let _mock = setup_mock_sign_evm_transaction(&mut mock_server).await;
811
812 let config = create_evm_test_config();
813 let service = TurnkeyService {
814 api_public_key: config.api_public_key,
815 api_private_key: config.api_private_key,
816 organization_id: config.organization_id,
817 private_key_id: config.private_key_id,
818 public_key: config.public_key,
819 base_url: mock_server.url(),
820 client: create_test_client(),
821 };
822
823 let message = b"test transaction";
824 let result = service.sign_evm_transaction(message).await;
825
826 assert!(result.is_ok());
827 let result = result.unwrap();
828 let expected = hex::decode("02f1010203050607080910").unwrap();
829 assert_eq!(result, expected)
830 }
831
832 #[tokio::test]
833 async fn test_error_handling() {
834 let mut mock_server = mockito::Server::new_async().await;
835 let _mock = setup_mock_error_response(&mut mock_server).await;
836
837 let config = create_solana_test_config();
838 let service = TurnkeyService {
839 api_public_key: config.api_public_key,
840 api_private_key: config.api_private_key,
841 organization_id: config.organization_id,
842 private_key_id: config.private_key_id,
843 public_key: config.public_key,
844 base_url: mock_server.url(),
845 client: create_test_client(),
846 };
847
848 let message = b"test message";
849 let result = service.sign_solana(message).await;
850 assert!(result.is_err());
851 match result {
852 Err(TurnkeyError::MethodError(e)) => {
853 assert!(e.error.message.contains("Invalid payload format"));
854 }
855 _ => panic!("Expected MethodError for Solana signing"),
856 }
857 }
858}