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