use crate::error::BittensorError;
use serde::{Deserialize, Serialize};
use std::path::Path;
use sp_core::{sr25519, Pair};
use thiserror::Error;
#[derive(Debug, Error)]
pub enum KeyfileError {
#[error("Failed to read keyfile: {0}")]
ReadError(#[from] std::io::Error),
#[error("Failed to parse keyfile JSON: {0}")]
ParseError(String),
#[error("Decryption failed: {0}")]
DecryptionError(String),
#[error("Invalid key format: {0}")]
InvalidFormat(String),
}
impl From<KeyfileError> for BittensorError {
fn from(err: KeyfileError) -> Self {
BittensorError::WalletError {
message: err.to_string(),
}
}
}
#[derive(Debug, Clone)]
pub struct KeyfileData {
pub secret: String,
pub is_mnemonic: bool,
pub format: KeyfileFormat,
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum KeyfileFormat {
JsonSecretPhrase,
JsonSecretSeed,
PlainMnemonic,
PlainHexSeed,
Encrypted,
}
#[derive(Debug, Deserialize, Serialize)]
#[serde(rename_all = "camelCase")]
struct JsonKeyfile {
secret_phrase: Option<String>,
secret_seed: Option<String>,
public_key: Option<String>,
account_id: Option<String>,
ss58_address: Option<String>,
}
impl KeyfileData {
pub fn to_keypair(&self) -> Result<sr25519::Pair, BittensorError> {
if self.is_mnemonic {
sr25519::Pair::from_string(&self.secret, None).map_err(|e| {
BittensorError::WalletError {
message: format!("Invalid mnemonic phrase: {e:?}"),
}
})
} else {
let hex_str = self.secret.strip_prefix("0x").unwrap_or(&self.secret);
let seed_bytes = hex::decode(hex_str).map_err(|e| BittensorError::WalletError {
message: format!("Invalid hex seed: {e}"),
})?;
if seed_bytes.len() != 32 {
return Err(BittensorError::WalletError {
message: format!("Seed must be 32 bytes, got {} bytes", seed_bytes.len()),
});
}
let mut seed_array = [0u8; 32];
seed_array.copy_from_slice(&seed_bytes);
Ok(sr25519::Pair::from_seed(&seed_array))
}
}
}
pub fn load_keyfile(path: &Path) -> Result<KeyfileData, KeyfileError> {
let content = std::fs::read_to_string(path)?;
parse_keyfile_content(&content)
}
pub fn load_encrypted_keyfile(path: &Path, password: &str) -> Result<KeyfileData, KeyfileError> {
let content = std::fs::read(path)?;
decrypt_keyfile(&content, password)
}
fn parse_keyfile_content(content: &str) -> Result<KeyfileData, KeyfileError> {
let trimmed = content.trim();
if let Ok(json_keyfile) = serde_json::from_str::<JsonKeyfile>(trimmed) {
if let Some(phrase) = json_keyfile.secret_phrase {
return Ok(KeyfileData {
secret: phrase,
is_mnemonic: true,
format: KeyfileFormat::JsonSecretPhrase,
});
}
if let Some(seed) = json_keyfile.secret_seed {
return Ok(KeyfileData {
secret: seed,
is_mnemonic: false,
format: KeyfileFormat::JsonSecretSeed,
});
}
return Err(KeyfileError::InvalidFormat(
"JSON keyfile missing secretPhrase or secretSeed".to_string(),
));
}
if trimmed.starts_with("0x") && trimmed.len() == 66 {
return Ok(KeyfileData {
secret: trimmed.to_string(),
is_mnemonic: false,
format: KeyfileFormat::PlainHexSeed,
});
}
if trimmed.len() == 64 && trimmed.chars().all(|c| c.is_ascii_hexdigit()) {
return Ok(KeyfileData {
secret: format!("0x{}", trimmed),
is_mnemonic: false,
format: KeyfileFormat::PlainHexSeed,
});
}
let word_count = trimmed.split_whitespace().count();
if word_count == 12 || word_count == 24 {
return Ok(KeyfileData {
secret: trimmed.to_string(),
is_mnemonic: true,
format: KeyfileFormat::PlainMnemonic,
});
}
Ok(KeyfileData {
secret: trimmed.to_string(),
is_mnemonic: true,
format: KeyfileFormat::PlainMnemonic,
})
}
fn decrypt_keyfile(data: &[u8], _password: &str) -> Result<KeyfileData, KeyfileError> {
if data.len() < 57 {
return Err(KeyfileError::DecryptionError(
"Encrypted keyfile too short".to_string(),
));
}
Err(KeyfileError::DecryptionError(
"Encrypted coldkey decryption not yet implemented. \
Please use `btcli wallet regen_coldkey` to create an unencrypted coldkey, \
or decrypt using the Python bittensor SDK."
.to_string(),
))
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_parse_json_mnemonic() {
let content = r#"{"secretPhrase": "abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon about"}"#;
let result = parse_keyfile_content(content).unwrap();
assert!(result.is_mnemonic);
assert_eq!(result.format, KeyfileFormat::JsonSecretPhrase);
assert!(result.secret.contains("abandon"));
}
#[test]
fn test_parse_json_seed() {
let content = r#"{"secretSeed": "0x0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef"}"#;
let result = parse_keyfile_content(content).unwrap();
assert!(!result.is_mnemonic);
assert_eq!(result.format, KeyfileFormat::JsonSecretSeed);
}
#[test]
fn test_parse_plain_mnemonic() {
let content = "abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon about";
let result = parse_keyfile_content(content).unwrap();
assert!(result.is_mnemonic);
assert_eq!(result.format, KeyfileFormat::PlainMnemonic);
}
#[test]
fn test_parse_plain_hex_with_prefix() {
let content = "0x0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef";
let result = parse_keyfile_content(content).unwrap();
assert!(!result.is_mnemonic);
assert_eq!(result.format, KeyfileFormat::PlainHexSeed);
}
#[test]
fn test_parse_plain_hex_without_prefix() {
let content = "0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef";
let result = parse_keyfile_content(content).unwrap();
assert!(!result.is_mnemonic);
assert_eq!(result.format, KeyfileFormat::PlainHexSeed);
assert!(result.secret.starts_with("0x"));
}
#[test]
fn test_to_keypair_from_mnemonic() {
let data = KeyfileData {
secret: "abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon about".to_string(),
is_mnemonic: true,
format: KeyfileFormat::PlainMnemonic,
};
let result = data.to_keypair();
assert!(result.is_ok());
}
#[test]
fn test_to_keypair_from_seed() {
let data = KeyfileData {
secret: "0x0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef"
.to_string(),
is_mnemonic: false,
format: KeyfileFormat::PlainHexSeed,
};
let result = data.to_keypair();
assert!(result.is_ok());
}
#[test]
fn test_to_keypair_invalid_mnemonic() {
let data = KeyfileData {
secret: "invalid mnemonic phrase".to_string(),
is_mnemonic: true,
format: KeyfileFormat::PlainMnemonic,
};
let result = data.to_keypair();
assert!(result.is_err());
}
#[test]
fn test_to_keypair_invalid_hex() {
let data = KeyfileData {
secret: "0xNOTHEX".to_string(),
is_mnemonic: false,
format: KeyfileFormat::PlainHexSeed,
};
let result = data.to_keypair();
assert!(result.is_err());
}
#[test]
fn test_to_keypair_wrong_seed_length() {
let data = KeyfileData {
secret: "0x0123456789abcdef".to_string(), is_mnemonic: false,
format: KeyfileFormat::PlainHexSeed,
};
let result = data.to_keypair();
assert!(result.is_err());
if let Err(BittensorError::WalletError { message }) = result {
assert!(message.contains("32 bytes"));
}
}
#[test]
fn test_json_missing_secret() {
let content = r#"{"publicKey": "something"}"#;
let result = parse_keyfile_content(content);
assert!(result.is_err());
}
#[test]
fn test_decrypt_too_short() {
let data = vec![0u8; 10];
let result = decrypt_keyfile(&data, "password");
assert!(result.is_err());
}
#[test]
fn test_parse_24_word_mnemonic() {
let content = "abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon art";
let result = parse_keyfile_content(content).unwrap();
assert!(result.is_mnemonic);
assert_eq!(result.format, KeyfileFormat::PlainMnemonic);
}
#[test]
fn test_keyfile_error_display() {
let err = KeyfileError::ParseError("test".to_string());
assert!(err.to_string().contains("parse"));
let err = KeyfileError::DecryptionError("failed".to_string());
assert!(err.to_string().contains("failed"));
let err = KeyfileError::InvalidFormat("bad".to_string());
assert!(err.to_string().contains("bad"));
}
#[test]
fn test_keyfile_error_to_bittensor_error() {
let err: BittensorError = KeyfileError::ParseError("test".to_string()).into();
if let BittensorError::WalletError { message } = err {
assert!(message.contains("parse"));
} else {
panic!("Expected WalletError");
}
}
#[test]
fn test_keyfile_data_clone() {
let data = KeyfileData {
secret: "test".to_string(),
is_mnemonic: true,
format: KeyfileFormat::PlainMnemonic,
};
let cloned = data.clone();
assert_eq!(data.secret, cloned.secret);
assert_eq!(data.is_mnemonic, cloned.is_mnemonic);
}
#[test]
fn test_keyfile_format_equality() {
assert_eq!(KeyfileFormat::PlainMnemonic, KeyfileFormat::PlainMnemonic);
assert_ne!(KeyfileFormat::PlainMnemonic, KeyfileFormat::PlainHexSeed);
}
#[test]
fn test_parse_whitespace_content() {
let content = " \n abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon about \n ";
let result = parse_keyfile_content(content).unwrap();
assert!(result.is_mnemonic);
}
}