1use 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#[derive(Debug, Deserialize, Serialize)]
72pub struct TurnkeyResponseError {
73 pub error: TurnkeyErrorDetails,
74}
75
76#[derive(Debug, Deserialize, Serialize)]
78pub struct TurnkeyErrorDetails {
79 pub code: i32,
80 pub message: String,
81}
82
83pub type TurnkeyResult<T> = Result<T, TurnkeyError>;
85
86#[derive(Serialize)]
88struct ApiStamp {
89 pub public_key: String,
90 pub signature: String,
91 pub scheme: String,
92}
93
94#[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#[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#[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#[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#[derive(Deserialize, Serialize)]
138struct ActivityResponse {
139 activity: Activity,
140}
141
142#[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#[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#[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 fn address_solana(&self) -> Result<Address, TurnkeyError>;
182
183 fn address_evm(&self) -> Result<Address, TurnkeyError>;
185
186 async fn sign_solana(&self, message: &[u8]) -> Result<Vec<u8>, TurnkeyError>;
188
189 async fn sign_evm(&self, message: &[u8]) -> Result<Vec<u8>, TurnkeyError>;
191
192 async fn sign_evm_transaction(&self, message: &[u8]) -> Result<Vec<u8>, TurnkeyError>;
194
195 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 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 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 let pub_key_no_prefix = &public_key[1..];
247
248 let hash = keccak256(pub_key_no_prefix);
249
250 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 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 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 let body = serde_json::to_string(request_body).map_err(|e| {
302 TurnkeyError::SerializationError(format!("Request serialization error: {}", e))
303 })?;
304
305 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 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 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 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 async fn sign_evm_transaction(&self, bytes: &[u8]) -> TurnkeyResult<Vec<u8>> {
384 let encoded_bytes = hex::encode(bytes);
385
386 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 let response_body = self
400 .make_turnkey_request::<_, ActivityResponse>("sign_transaction", &sign_transaction_body)
401 .await?;
402
403 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 res.json::<T>()
435 .await
436 .map_err(|e| TurnkeyError::HttpError(e.to_string()))
437 } else {
438 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 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 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 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" }
672 }
673 }
674 })))
675 .mount(mock_server)
676 .await;
677 }
678
679 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 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}