openzeppelin_relayer/services/turnkey/
mod.rs

1//! # Turnkey Service Module
2//!
3//! This module provides integration with Turnkey API for secure wallet management
4//! and cryptographic operations.
5//!
6//! ## Features
7//!
8//! - API key-based authentication
9//! - Digital signature generation
10//! - Message signing via Turnkey API
11//! - Secure transaction signing for blockchain operations
12//!
13//! ## Architecture
14//!
15//! ```text
16//! TurnkeyService (implements TurnkeyServiceTrait)
17//!   ├── Authentication (API key-based)
18//!   ├── Digital Stamping
19//!   ├── Transaction Signing
20//!   └── Raw Payload Signing
21//! ```
22use 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/// Error response from Turnkey API
72#[derive(Debug, Deserialize, Serialize)]
73pub struct TurnkeyResponseError {
74    pub error: TurnkeyErrorDetails,
75}
76
77/// Error details from Turnkey API
78#[derive(Debug, Deserialize, Serialize)]
79pub struct TurnkeyErrorDetails {
80    pub code: i32,
81    pub message: String,
82}
83
84/// Result type for Turnkey operations
85pub type TurnkeyResult<T> = Result<T, TurnkeyError>;
86
87/// Digital stamp for API authentication
88#[derive(Serialize)]
89struct ApiStamp {
90    pub public_key: String,
91    pub signature: String,
92    pub scheme: String,
93}
94
95/// Request to sign raw payload
96#[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/// Parameters for signing transaction payload
107#[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/// Parameters for signing raw payload
118#[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/// Parameters for signing raw payload
128#[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/// Response from activity API
138#[derive(Deserialize, Serialize)]
139struct ActivityResponse {
140    activity: Activity,
141}
142
143/// Activity details
144#[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/// Activity result
153#[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/// Sign raw payload result
161#[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    /// Returns the Solana address derived from the configured public key
182    fn address_solana(&self) -> Result<Address, TurnkeyError>;
183
184    /// Returns the EVM address derived from the configured public key
185    fn address_evm(&self) -> Result<Address, TurnkeyError>;
186
187    /// Returns the Stellar address derived from the configured public key
188    fn address_stellar(&self) -> Result<Address, TurnkeyError>;
189
190    /// Signs a message using the Solana signing scheme
191    async fn sign_solana(&self, message: &[u8]) -> Result<Vec<u8>, TurnkeyError>;
192
193    /// Signs a message using the EVM signing scheme
194    async fn sign_evm(&self, message: &[u8]) -> Result<Vec<u8>, TurnkeyError>;
195
196    /// Signs a message using the Stellar signing scheme (Ed25519)
197    async fn sign_stellar(&self, message: &[u8]) -> Result<Vec<u8>, TurnkeyError>;
198
199    /// Signs an EVM transaction using the Turnkey API
200    async fn sign_evm_transaction(&self, message: &[u8]) -> Result<Vec<u8>, TurnkeyError>;
201
202    /// Signs a Solana transaction and returns both the transaction and signature
203    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    /// Converts the public key to an Solana address
234    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    /// Converts the public key to an EVM address
248    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        // Remove the first byte (0x04 prefix)
253        let pub_key_no_prefix = &public_key[1..];
254
255        let hash = keccak256(pub_key_no_prefix);
256
257        // Ethereum addresses are the last 20 bytes of the Keccak-256 hash.
258        // Since the hash is 32 bytes, the address is bytes 12..32.
259        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    /// Converts the public key to a Stellar address
275    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        // For Stellar, we expect Ed25519 public key in hex format
281        let raw_pubkey = hex::decode(&self.public_key)
282            .map_err(|e| TurnkeyError::ConfigError(format!("Invalid public key hex: {}", e)))?;
283
284        // Stellar uses StrKey encoding with 'G' prefix for account addresses
285        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    /// Creates a digital stamp for API authentication
293    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    /// Helper method to make Turnkey API requests
320    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        // Serialize the request body
326        let body = serde_json::to_string(request_body).map_err(|e| {
327            TurnkeyError::SerializationError(format!("Request serialization error: {}", e))
328        })?;
329
330        // Create the authentication stamp
331        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    /// Helper method to sign raw payloads with configurable hash function and v inclusion
347    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    /// Signs raw bytes using the Turnkey API (for Solana)
393    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    /// Signs raw bytes using the Turnkey API (for EVM)
399    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    /// Signs raw bytes using the Turnkey API (for Stellar)
408    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    /// Signs an EVM transaction using the Turnkey API
417    async fn sign_evm_transaction(&self, bytes: &[u8]) -> TurnkeyResult<Vec<u8>> {
418        let encoded_bytes = hex::encode(bytes);
419
420        // Create the request body
421        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        // Make the API request and get the response
433        let response_body = self
434            .make_turnkey_request::<_, ActivityResponse>("sign_transaction", &sign_transaction_body)
435            .await?;
436
437        // Extract the signed transaction
438        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                    // On success, deserialize the response into the expected type T
468                    res.json::<T>()
469                        .await
470                        .map_err(|e| TurnkeyError::HttpError(e.to_string()))
471                } else {
472                    // For error responses, try to get the body text first
473                    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                // On a reqwest error, convert it into a TurnkeyError::HttpError
508                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    // Setup mock for signing raw payload
678    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    // Setup mock for signing EVM transaction
704    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" // Example signed transaction hex
721                            }
722                        }
723                    }
724                }))
725                .unwrap(),
726            )
727            .expect(1)
728            .create_async()
729            .await
730    }
731
732    // Setup mock for error response
733    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    // Helper function to create a modified client for testing
755    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}