Skip to content
Merged
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 6 additions & 0 deletions crates/lib/src/error.rs
Original file line number Diff line number Diff line change
Expand Up @@ -199,6 +199,12 @@ impl From<crate::signer::privy::types::PrivyError> for KoraError {
}
}

impl From<crate::signer::turnkey::types::TurnkeyError> for KoraError {
fn from(err: crate::signer::turnkey::types::TurnkeyError) -> Self {
KoraError::SigningError(err.to_string())
}
}

#[cfg(test)]
mod tests {
use super::*;
Expand Down
17 changes: 16 additions & 1 deletion crates/lib/src/signer/privy/signer.rs
Original file line number Diff line number Diff line change
Expand Up @@ -51,7 +51,16 @@ impl PrivySigner {
.await?;

if !response.status().is_success() {
return Err(PrivyError::ApiError(response.status().as_u16()));
let status = response.status().as_u16();
let error_text = response
.text()
.await
.unwrap_or_else(|_| "Failed to read error response".to_string());

log::error!(
"Privy API get_public_key error - status: {status}, response: {error_text}"
);
return Err(PrivyError::ApiError(status));
}

let wallet_info: WalletResponse = response.json().await?;
Expand Down Expand Up @@ -91,6 +100,12 @@ impl PrivySigner {

if !response.status().is_success() {
let status = response.status().as_u16();
let error_text = response
.text()
.await
.unwrap_or_else(|_| "Failed to read error response".to_string());

log::error!("Privy API sign_solana error - status: {status}, response: {error_text}");
return Err(PrivyError::ApiError(status));
}

Expand Down
7 changes: 1 addition & 6 deletions crates/lib/src/signer/turnkey/config.rs
Original file line number Diff line number Diff line change
Expand Up @@ -63,12 +63,7 @@ impl SignerConfigTrait for TurnkeySignerHandler {
organization_id,
private_key_id,
public_key,
)
.map_err(|e| {
KoraError::ValidationError(format!(
"Failed to create Turnkey signer '{signer_name}': {e}"
))
})?;
);

Ok(KoraSigner::Turnkey(signer))
}
Expand Down
124 changes: 85 additions & 39 deletions crates/lib/src/signer/turnkey/signer.rs
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ use solana_sdk::{pubkey::Pubkey, signature::Signature};
use solana_sdk::transaction::VersionedTransaction;

use crate::signer::{
turnkey::types::{ActivityResponse, SignParameters, SignRequest, TurnkeySigner},
turnkey::types::{ActivityResponse, SignParameters, SignRequest, TurnkeyError, TurnkeySigner},
utils::{bytes_to_hex, hex_to_bytes},
};

Expand All @@ -19,19 +19,19 @@ impl TurnkeySigner {
organization_id: String,
private_key_id: String,
public_key: String,
) -> Result<Self, anyhow::Error> {
Ok(Self {
) -> Self {
Self {
api_public_key,
api_private_key,
organization_id,
private_key_id,
public_key,
api_base_url: "https://api.turnkey.com".to_string(),
client: Client::new(),
})
}
}

pub async fn sign(&self, transaction: &VersionedTransaction) -> Result<Vec<u8>, anyhow::Error> {
pub async fn sign(&self, transaction: &VersionedTransaction) -> Result<Vec<u8>, TurnkeyError> {
let hex_message = hex::encode(transaction.message.serialize());

let request = SignRequest {
Expand All @@ -46,7 +46,7 @@ impl TurnkeySigner {
},
};

let body = serde_json::to_string(&request).map_err(|e| anyhow::anyhow!(e.to_string()))?;
let body = serde_json::to_string(&request).map_err(TurnkeyError::JsonError)?;

let stamp = self.create_stamp(&body)?;

Expand All @@ -59,22 +59,33 @@ impl TurnkeySigner {
.body(body)
.send()
.await
.map_err(|e| anyhow::anyhow!(e.to_string()))?;
.map_err(TurnkeyError::RequestError)?;

if !response.status().is_success() {
let status = response.status().as_u16();
let error_text = response
.text()
.await
.unwrap_or_else(|_| "Failed to read error response".to_string());

log::error!("Turnkey API error - status: {status}, response: {error_text}");
return Err(TurnkeyError::ApiError(status));
}

let response_text = response.text().await.map_err(TurnkeyError::RequestError)?;

let response = serde_json::from_str::<ActivityResponse>(&response.text().await.unwrap())
.map_err(|e| anyhow::anyhow!(e.to_string()))?;
let response = serde_json::from_str::<ActivityResponse>(&response_text)
.map_err(TurnkeyError::JsonError)?;

if let Some(result) = response.activity.result {
if let Some(sign_result) = result.sign_raw_payload_result {
// Decode r and s components
let r_bytes = hex::decode(&sign_result.r)
.map_err(|e| anyhow::anyhow!(format!("Invalid r component: {}", e)))?;
let s_bytes = hex::decode(&sign_result.s)
.map_err(|e| anyhow::anyhow!(format!("Invalid s component: {}", e)))?;
let r_bytes = hex::decode(&sign_result.r).map_err(TurnkeyError::InvalidHex)?;
let s_bytes = hex::decode(&sign_result.s).map_err(TurnkeyError::InvalidHex)?;

// Ensure each component is exactly 32 bytes
if r_bytes.len() > 32 || s_bytes.len() > 32 {
return Err(anyhow::anyhow!("Signature component too long"));
return Err(TurnkeyError::InvalidSignature);
}

// Create properly padded 32-byte arrays
Expand All @@ -94,26 +105,25 @@ impl TurnkeySigner {
}
}

Err(anyhow::anyhow!("Failed to get signature from response"))
Err(TurnkeyError::InvalidResponse)
}

pub async fn sign_solana(
&self,
transaction: &VersionedTransaction,
) -> Result<Signature, anyhow::Error> {
) -> Result<Signature, TurnkeyError> {
let sig = self.sign(transaction).await?;
let sig_bytes: [u8; 64] = sig.try_into().unwrap();
Ok(Signature::from(sig_bytes))
}

fn create_stamp(&self, message: &str) -> Result<String, anyhow::Error> {
fn create_stamp(&self, message: &str) -> Result<String, TurnkeyError> {
let private_key_bytes =
hex_to_bytes(&self.api_private_key).map_err(|e| anyhow::anyhow!(e.to_string()))?;
let private_key_array: [u8; 32] = private_key_bytes
.try_into()
.map_err(|_| anyhow::anyhow!("Invalid private key length"))?;
hex_to_bytes(&self.api_private_key).map_err(TurnkeyError::InvalidStamp)?;
let private_key_array: [u8; 32] =
private_key_bytes.try_into().map_err(|_| TurnkeyError::InvalidPrivateKeyLength)?;
let signing_key = p256::ecdsa::SigningKey::from_slice(&private_key_array)
.map_err(|e| anyhow::anyhow!(e.to_string()))?;
.map_err(TurnkeyError::SigningKeyError)?;

let signature: p256::ecdsa::Signature = signing_key.sign(message.as_bytes());
let signature_der = signature.to_der().to_bytes();
Expand All @@ -125,8 +135,7 @@ impl TurnkeySigner {
"scheme": "SIGNATURE_SCHEME_TK_API_P256"
});

let json_stamp =
serde_json::to_string(&stamp).map_err(|e| anyhow::anyhow!(e.to_string()))?;
let json_stamp = serde_json::to_string(&stamp).map_err(TurnkeyError::JsonError)?;

Ok(base64::engine::general_purpose::URL_SAFE_NO_PAD.encode(json_stamp.as_bytes()))
}
Expand All @@ -151,16 +160,14 @@ mod tests {
let private_key_id = "test_private_key_id".to_string();
let public_key = "11111111111111111111111111111111".to_string();

let result = TurnkeySigner::new(
let signer = TurnkeySigner::new(
api_public_key.clone(),
api_private_key.clone(),
organization_id.clone(),
private_key_id.clone(),
public_key.clone(),
);

assert!(result.is_ok());
let signer = result.unwrap();
assert_eq!(signer.api_public_key, api_public_key);
assert_eq!(signer.api_private_key, api_private_key);
assert_eq!(signer.organization_id, organization_id);
Expand All @@ -176,8 +183,7 @@ mod tests {
"org".to_string(),
"key_id".to_string(),
"11111111111111111111111111111111".to_string(),
)
.unwrap();
);

let pubkey = signer.solana_pubkey();
assert_eq!(pubkey.to_string(), "11111111111111111111111111111111");
Expand Down Expand Up @@ -220,8 +226,7 @@ mod tests {
"test_org_id".to_string(),
"test_private_key_id".to_string(),
"11111111111111111111111111111111".to_string(),
)
.unwrap();
);
signer.api_base_url = server.url();

let result = signer.sign(&test_transaction).await;
Expand Down Expand Up @@ -263,15 +268,57 @@ mod tests {
"invalid_org_id".to_string(),
"invalid_private_key_id".to_string(),
"11111111111111111111111111111111".to_string(),
)
.unwrap();
);

signer.api_base_url = server.url();

// Test API error handling
let result = signer.sign(&test_transaction).await;
assert!(result.is_err());
let error_msg = result.unwrap_err().to_string();
assert!(error_msg.contains("Failed to get signature from response"));
assert!(matches!(result.unwrap_err(), TurnkeyError::ApiError(_)));
}

#[tokio::test]
async fn test_sign_rate_limit_error() {
let mut server = Server::new_async().await;

let rate_limit_response = r#"{
"code": 8,
"message": "",
"details": [],
"turnkeyErrorCode": ""
}"#;

let _mock = server
.mock("POST", "/public/v1/submit/sign_raw_payload")
.with_status(429)
.with_header("content-type", "application/json")
.with_body(rate_limit_response)
.create_async()
.await;

let test_transaction = create_mock_transaction();

let mut signer = TurnkeySigner::new(
"test_api_public_key".to_string(),
"0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef".to_string(),
"test_org_id".to_string(),
"test_private_key_id".to_string(),
"11111111111111111111111111111111".to_string(),
);

signer.api_base_url = server.url();

// Test that 429 rate limit is properly handled
let result = signer.sign(&test_transaction).await;
assert!(result.is_err());

match result.unwrap_err() {
TurnkeyError::ApiError(status) => {
assert_eq!(status, 429);
}
_ => panic!("Expected ApiError with 429 status"),
}
}

#[tokio::test]
Expand Down Expand Up @@ -311,8 +358,8 @@ mod tests {
"test_org_id".to_string(),
"test_private_key_id".to_string(),
"11111111111111111111111111111111".to_string(),
)
.unwrap();
);

signer.api_base_url = server.url();

// Test successful signing returns Signature
Expand All @@ -331,8 +378,7 @@ mod tests {
"test_org_id".to_string(),
"test_private_key_id".to_string(),
"11111111111111111111111111111111".to_string(),
)
.unwrap();
);

let test_message = r#"{"test": "message"}"#;

Expand Down
69 changes: 69 additions & 0 deletions crates/lib/src/signer/turnkey/types.rs
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
use hex::FromHexError;
use p256::ecdsa::signature;
use reqwest::Client;
use serde::{Deserialize, Serialize};

Expand Down Expand Up @@ -55,3 +57,70 @@ pub struct SignResult {
pub r: String,
pub s: String,
}

#[derive(Debug)]
pub enum TurnkeyError {
ApiError(u16),
RequestError(reqwest::Error),
JsonError(serde_json::Error),
InvalidSignature,
InvalidHex(FromHexError),
InvalidStamp(anyhow::Error),
SigningKeyError(signature::Error),
InvalidResponse,
InvalidPrivateKeyLength,
InvalidPublicKey,
Other(anyhow::Error),
}

impl std::fmt::Display for TurnkeyError {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
TurnkeyError::ApiError(status) => write!(f, "API error: {status}"),
TurnkeyError::InvalidResponse => write!(f, "Invalid response"),
TurnkeyError::InvalidPublicKey => write!(f, "Invalid public key"),
TurnkeyError::InvalidSignature => write!(f, "Invalid signature"),
TurnkeyError::InvalidPrivateKeyLength => write!(f, "Invalid private key length"),
TurnkeyError::RequestError(e) => write!(f, "Request error: {e}"),
TurnkeyError::JsonError(e) => write!(f, "JSON error: {e}"),
TurnkeyError::InvalidStamp(e) => write!(f, "Invalid stamp: {e}"),
TurnkeyError::InvalidHex(e) => write!(f, "Invalid Hex: {e}"),
TurnkeyError::SigningKeyError(e) => write!(f, "Signing key error: {e}"),
TurnkeyError::Other(e) => write!(f, "{e}"),
}
}
}

impl std::error::Error for TurnkeyError {}

#[cfg(test)]
mod tests {
use super::*;

#[test]
fn test_turnkey_error_display() {
let error = TurnkeyError::ApiError(429);
assert_eq!(error.to_string(), "API error: 429");

let error = TurnkeyError::InvalidResponse;
assert_eq!(error.to_string(), "Invalid response");

let error = TurnkeyError::InvalidSignature;
assert_eq!(error.to_string(), "Invalid signature");
}

#[test]
fn test_turnkey_error_conversion_to_kora_error() {
use crate::error::KoraError;

let turnkey_error = TurnkeyError::ApiError(429);
let kora_error: KoraError = turnkey_error.into();

match kora_error {
KoraError::SigningError(msg) => {
assert_eq!(msg, "API error: 429");
}
_ => panic!("Expected SigningError"),
}
}
}
Loading
Loading