Skip to main content

kora_lib/signer/
keypair_util.rs

1use crate::{error::KoraError, sanitize_error};
2use serde_json;
3use solana_sdk::signature::Keypair;
4use std::fs;
5
6/// Utility functions for parsing private keys in multiple formats
7pub struct KeypairUtil;
8
9impl KeypairUtil {
10    /// Creates a new keypair from a private key string that can be in multiple formats:
11    /// - Base58 encoded string (current format)
12    /// - U8Array format: "[0, 1, 2, ...]"
13    /// - File path to a JSON keypair file
14    pub fn from_private_key_string(private_key: &str) -> Result<Keypair, KoraError> {
15        // Try to parse as a file path first
16        if let Ok(file_content) = fs::read_to_string(private_key) {
17            return Self::from_json_keypair(&file_content);
18        }
19
20        // Try to parse as U8Array format
21        if private_key.trim().starts_with('[') && private_key.trim().ends_with(']') {
22            return Self::from_u8_array_string(private_key);
23        }
24
25        // Default to base58 format (with proper error handling)
26        Self::from_base58_safe(private_key)
27    }
28
29    /// Creates a new keypair from a base58-encoded private key string with proper error handling
30    pub fn from_base58_safe(private_key: &str) -> Result<Keypair, KoraError> {
31        // Try to decode as base58 first
32        let decoded = bs58::decode(private_key).into_vec().map_err(|e| {
33            KoraError::SigningError(format!("Invalid base58 string: {}", sanitize_error!(e)))
34        })?;
35
36        if decoded.len() != 64 {
37            return Err(KoraError::SigningError(format!(
38                "Invalid private key length: expected 64 bytes, got {}",
39                decoded.len()
40            )));
41        }
42
43        let keypair = Keypair::try_from(&decoded[..]).map_err(|e| {
44            KoraError::SigningError(format!("Invalid private key bytes: {}", sanitize_error!(e)))
45        })?;
46
47        Ok(keypair)
48    }
49
50    /// Creates a new keypair from a U8Array format string like "[0, 1, 2, ...]"
51    pub fn from_u8_array_string(array_str: &str) -> Result<Keypair, KoraError> {
52        let trimmed = array_str.trim();
53
54        if !trimmed.starts_with('[') || !trimmed.ends_with(']') {
55            return Err(KoraError::SigningError(
56                "U8Array string must start with '[' and end with ']'".to_string(),
57            ));
58        }
59
60        let inner = &trimmed[1..trimmed.len() - 1];
61
62        if inner.trim().is_empty() {
63            return Err(KoraError::SigningError("U8Array string cannot be empty".to_string()));
64        }
65
66        let bytes: Result<Vec<u8>, _> = inner.split(',').map(|s| s.trim().parse::<u8>()).collect();
67
68        match bytes {
69            Ok(byte_array) => {
70                if byte_array.len() != 64 {
71                    return Err(KoraError::SigningError(format!(
72                        "Private key must be exactly 64 bytes, got {}",
73                        byte_array.len()
74                    )));
75                }
76                Keypair::try_from(&byte_array[..]).map_err(|e| {
77                    KoraError::SigningError(format!(
78                        "Invalid private key bytes: {}",
79                        sanitize_error!(e)
80                    ))
81                })
82            }
83            Err(e) => Err(KoraError::SigningError(format!(
84                "Failed to parse U8Array: {}",
85                sanitize_error!(e)
86            ))),
87        }
88    }
89
90    /// Creates a new keypair from a JSON keypair file content
91    pub fn from_json_keypair(json_content: &str) -> Result<Keypair, KoraError> {
92        // Try to parse as a simple JSON array first
93        if let Ok(byte_array) = serde_json::from_str::<Vec<u8>>(json_content) {
94            if byte_array.len() != 64 {
95                return Err(KoraError::SigningError(format!(
96                    "JSON keypair must be exactly 64 bytes, got {}",
97                    byte_array.len()
98                )));
99            }
100            return Keypair::try_from(&byte_array[..]).map_err(|e| {
101                KoraError::SigningError(format!(
102                    "Invalid private key bytes: {}",
103                    sanitize_error!(e)
104                ))
105            });
106        }
107
108        Err(KoraError::SigningError(
109            "Invalid JSON keypair format. Expected either a JSON array of 64 bytes or an object with a 'keypair' field".to_string()
110        ))
111    }
112}
113
114#[cfg(test)]
115mod tests {
116    use super::*;
117    use solana_sdk::{signature::Keypair, signer::Signer};
118    use std::fs;
119    use tempfile::NamedTempFile;
120
121    #[test]
122    fn test_from_base58_format() {
123        let keypair = Keypair::new();
124        let base58_key = bs58::encode(keypair.to_bytes()).into_string();
125
126        let parsed_keypair = KeypairUtil::from_private_key_string(&base58_key).unwrap();
127        assert_eq!(parsed_keypair.pubkey(), keypair.pubkey());
128    }
129
130    #[test]
131    fn test_from_u8_array_format() {
132        let keypair = Keypair::new();
133        let bytes = keypair.to_bytes();
134
135        let u8_array_str =
136            format!("[{}]", bytes.iter().map(|b| b.to_string()).collect::<Vec<_>>().join(", "));
137
138        let parsed_keypair = KeypairUtil::from_private_key_string(&u8_array_str).unwrap();
139        assert_eq!(parsed_keypair.pubkey(), keypair.pubkey());
140    }
141
142    #[test]
143    fn test_from_json_file_path() {
144        let keypair = Keypair::new();
145        let bytes = keypair.to_bytes();
146
147        let temp_file = NamedTempFile::new().unwrap();
148        let json_str = serde_json::to_string(&bytes.to_vec()).unwrap();
149        fs::write(temp_file.path(), json_str).unwrap();
150
151        let parsed_keypair =
152            KeypairUtil::from_private_key_string(temp_file.path().to_str().unwrap()).unwrap();
153        assert_eq!(parsed_keypair.pubkey(), keypair.pubkey());
154    }
155
156    #[test]
157    fn test_invalid_formats() {
158        // Test invalid U8Array
159        let result = KeypairUtil::from_private_key_string("[1, 2, 3]");
160        assert!(result.is_err());
161
162        // Test invalid JSON
163        let result = KeypairUtil::from_private_key_string("{invalid json}");
164        assert!(result.is_err());
165
166        // Test nonexistent file
167        let result = KeypairUtil::from_private_key_string("/nonexistent/file.json");
168        assert!(result.is_err());
169    }
170}