mod keyfile;
mod signer;
pub use keyfile::{KeyfileData, KeyfileError};
pub use signer::WalletSigner;
use crate::error::BittensorError;
use crate::types::Hotkey;
use crate::AccountId;
use sp_core::{sr25519, Pair};
use std::path::{Path, PathBuf};
#[derive(Clone)]
pub struct Wallet {
pub name: String,
pub hotkey_name: String,
pub path: PathBuf,
hotkey_pair: sr25519::Pair,
coldkey_pair: Option<sr25519::Pair>,
}
impl std::fmt::Debug for Wallet {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.debug_struct("Wallet")
.field("name", &self.name)
.field("hotkey_name", &self.hotkey_name)
.field("path", &self.path)
.field("hotkey", &self.hotkey().to_string())
.field("coldkey_unlocked", &self.is_coldkey_unlocked())
.finish()
}
}
impl Wallet {
pub fn load(wallet_name: &str, hotkey_name: &str) -> Result<Self, BittensorError> {
let wallet_path = Self::default_wallet_path()?;
Self::load_from_path(wallet_name, hotkey_name, &wallet_path)
}
pub fn load_from_path(
wallet_name: &str,
hotkey_name: &str,
base_path: &Path,
) -> Result<Self, BittensorError> {
let hotkey_path = base_path
.join(wallet_name)
.join("hotkeys")
.join(hotkey_name);
if !hotkey_path.exists() {
return Err(BittensorError::WalletError {
message: format!("Hotkey file not found: {}", hotkey_path.display()),
});
}
let keyfile_data = keyfile::load_keyfile(&hotkey_path)?;
let hotkey_pair = keyfile_data.to_keypair()?;
Ok(Self {
name: wallet_name.to_string(),
hotkey_name: hotkey_name.to_string(),
path: base_path.join(wallet_name),
hotkey_pair,
coldkey_pair: None,
})
}
pub fn create_random(wallet_name: &str, hotkey_name: &str) -> Result<Self, BittensorError> {
let (pair, _) = sr25519::Pair::generate();
let path = Self::default_wallet_path()?;
Ok(Self {
name: wallet_name.to_string(),
hotkey_name: hotkey_name.to_string(),
path: path.join(wallet_name),
hotkey_pair: pair,
coldkey_pair: None,
})
}
pub fn from_mnemonic(
wallet_name: &str,
hotkey_name: &str,
mnemonic: &str,
) -> Result<Self, BittensorError> {
let pair = sr25519::Pair::from_string(mnemonic, None).map_err(|e| {
BittensorError::WalletError {
message: format!("Invalid mnemonic: {e:?}"),
}
})?;
let path =
Self::default_wallet_path().unwrap_or_else(|_| PathBuf::from("~/.bittensor/wallets"));
Ok(Self {
name: wallet_name.to_string(),
hotkey_name: hotkey_name.to_string(),
path: path.join(wallet_name),
hotkey_pair: pair,
coldkey_pair: None,
})
}
pub fn from_seed_hex(
wallet_name: &str,
hotkey_name: &str,
seed_hex: &str,
) -> Result<Self, BittensorError> {
let hex_str = seed_hex.strip_prefix("0x").unwrap_or(seed_hex);
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);
let pair = sr25519::Pair::from_seed(&seed_array);
let path =
Self::default_wallet_path().unwrap_or_else(|_| PathBuf::from("~/.bittensor/wallets"));
Ok(Self {
name: wallet_name.to_string(),
hotkey_name: hotkey_name.to_string(),
path: path.join(wallet_name),
hotkey_pair: pair,
coldkey_pair: None,
})
}
pub fn hotkey(&self) -> Hotkey {
let public = self.hotkey_pair.public();
let account_id = AccountId::from(public.0);
Hotkey::from_account_id(&account_id)
}
pub fn account_id(&self) -> AccountId {
AccountId::from(self.hotkey_pair.public().0)
}
pub fn sign(&self, data: &[u8]) -> Vec<u8> {
let signature = self.hotkey_pair.sign(data);
signature.0.to_vec()
}
pub fn sign_hex(&self, data: &[u8]) -> String {
hex::encode(self.sign(data))
}
pub fn signer(&self) -> WalletSigner {
WalletSigner::from_sp_core_pair(self.hotkey_pair.clone())
}
pub fn keypair(&self) -> &sr25519::Pair {
&self.hotkey_pair
}
pub fn verify(&self, data: &[u8], signature: &[u8]) -> bool {
if signature.len() != 64 {
return false;
}
let mut sig_array = [0u8; 64];
sig_array.copy_from_slice(signature);
let sig = sr25519::Signature::from_raw(sig_array);
use sp_runtime::traits::Verify;
sig.verify(data, &self.hotkey_pair.public())
}
pub fn unlock_coldkey(&mut self, password: &str) -> Result<(), BittensorError> {
let coldkey_path = self.path.join("coldkey");
if !coldkey_path.exists() {
return Err(BittensorError::WalletError {
message: format!("Coldkey file not found: {}", coldkey_path.display()),
});
}
let keyfile_data = keyfile::load_encrypted_keyfile(&coldkey_path, password)?;
let coldkey_pair = keyfile_data.to_keypair()?;
self.coldkey_pair = Some(coldkey_pair);
Ok(())
}
pub fn is_coldkey_unlocked(&self) -> bool {
self.coldkey_pair.is_some()
}
pub fn coldkey(&self) -> Option<Hotkey> {
self.coldkey_pair.as_ref().map(|pair| {
let public = pair.public();
let account_id = AccountId::from(public.0);
Hotkey::from_account_id(&account_id)
})
}
fn default_wallet_path() -> Result<PathBuf, BittensorError> {
home::home_dir()
.map(|home| home.join(".bittensor").join("wallets"))
.ok_or_else(|| BittensorError::WalletError {
message: "Could not determine home directory".to_string(),
})
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_create_random_wallet() {
let wallet = Wallet::create_random("test_wallet", "test_hotkey").unwrap();
assert_eq!(wallet.name, "test_wallet");
assert_eq!(wallet.hotkey_name, "test_hotkey");
let hotkey = wallet.hotkey();
assert!(!hotkey.as_str().is_empty());
}
#[test]
fn test_sign_and_verify() {
let wallet = Wallet::create_random("test", "test").unwrap();
let message = b"test message";
let signature = wallet.sign(message);
assert_eq!(signature.len(), 64);
assert!(wallet.verify(message, &signature));
}
#[test]
fn test_sign_hex() {
let wallet = Wallet::create_random("test", "test").unwrap();
let sig_hex = wallet.sign_hex(b"test");
assert_eq!(sig_hex.len(), 128);
assert!(hex::decode(&sig_hex).is_ok());
}
#[test]
fn test_from_seed_hex() {
let seed = "0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef";
let wallet1 = Wallet::from_seed_hex("test", "test", seed).unwrap();
let wallet2 = Wallet::from_seed_hex("test", "test", &format!("0x{}", seed)).unwrap();
assert_eq!(wallet1.hotkey().as_str(), wallet2.hotkey().as_str());
}
#[test]
fn test_from_seed_hex_invalid() {
let result = Wallet::from_seed_hex("test", "test", "0123");
assert!(result.is_err());
let result = Wallet::from_seed_hex("test", "test", "not_hex_at_all!");
assert!(result.is_err());
}
#[test]
fn test_verify_wrong_signature() {
let wallet = Wallet::create_random("test", "test").unwrap();
let wrong_sig = vec![0u8; 64];
assert!(!wallet.verify(b"test", &wrong_sig));
}
#[test]
fn test_verify_wrong_length() {
let wallet = Wallet::create_random("test", "test").unwrap();
let short_sig = vec![0u8; 32];
assert!(!wallet.verify(b"test", &short_sig));
}
#[test]
fn test_account_id() {
let wallet = Wallet::create_random("test", "test").unwrap();
let account_id = wallet.account_id();
let hotkey = wallet.hotkey();
assert_eq!(account_id.to_string(), hotkey.as_str());
}
#[test]
fn test_coldkey_not_unlocked() {
let wallet = Wallet::create_random("test", "test").unwrap();
assert!(!wallet.is_coldkey_unlocked());
assert!(wallet.coldkey().is_none());
}
}