Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
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
2 changes: 1 addition & 1 deletion .github/badges/coverage.json
Original file line number Diff line number Diff line change
@@ -1 +1 @@
{"schemaVersion": 1, "label": "coverage", "message": "85.5%", "color": "green"}
{"schemaVersion": 1, "label": "coverage", "message": "85.7%", "color": "green"}
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