bittensor_rs/wallet/mod.rs
1//! # Wallet Module
2//!
3//! Wallet management for Bittensor, including key loading, signing, and
4//! transaction creation.
5//!
6//! # Example
7//!
8//! ```rust,no_run
9//! use bittensor_rs::wallet::Wallet;
10//!
11//! // Load an existing wallet
12//! let wallet = Wallet::load("my_wallet", "my_hotkey")?;
13//!
14//! // Sign data with the hotkey
15//! let signature = wallet.sign(b"message");
16//!
17//! // Get the hotkey address
18//! let hotkey = wallet.hotkey();
19//! # Ok::<(), Box<dyn std::error::Error>>(())
20//! ```
21
22mod keyfile;
23mod signer;
24
25pub use keyfile::{KeyfileData, KeyfileError};
26pub use signer::WalletSigner;
27
28use crate::error::BittensorError;
29use crate::types::Hotkey;
30use crate::AccountId;
31use sp_core::{sr25519, Pair};
32use std::path::{Path, PathBuf};
33
34/// Bittensor wallet for managing keys and signing transactions
35///
36/// A wallet contains:
37/// - A hotkey (required) for signing transactions
38/// - An optional coldkey for staking operations
39///
40/// # Example
41///
42/// ```rust,no_run
43/// use bittensor_rs::wallet::Wallet;
44///
45/// // Load from default ~/.bittensor/wallets path
46/// let wallet = Wallet::load("my_wallet", "my_hotkey")?;
47/// println!("Hotkey: {}", wallet.hotkey());
48/// # Ok::<(), bittensor_rs::BittensorError>(())
49/// ```
50#[derive(Clone)]
51pub struct Wallet {
52 /// Wallet name
53 pub name: String,
54 /// Hotkey name
55 pub hotkey_name: String,
56 /// Path to the wallet directory
57 pub path: PathBuf,
58 /// Hotkey keypair
59 hotkey_pair: sr25519::Pair,
60 /// Optional coldkey keypair (requires unlock)
61 coldkey_pair: Option<sr25519::Pair>,
62}
63
64impl std::fmt::Debug for Wallet {
65 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
66 f.debug_struct("Wallet")
67 .field("name", &self.name)
68 .field("hotkey_name", &self.hotkey_name)
69 .field("path", &self.path)
70 .field("hotkey", &self.hotkey().to_string())
71 .field("coldkey_unlocked", &self.is_coldkey_unlocked())
72 .finish()
73 }
74}
75
76impl Wallet {
77 /// Load a wallet from the default Bittensor wallet path
78 ///
79 /// Wallets are stored in `~/.bittensor/wallets/<wallet_name>/hotkeys/<hotkey_name>`
80 ///
81 /// # Arguments
82 ///
83 /// * `wallet_name` - Name of the wallet directory
84 /// * `hotkey_name` - Name of the hotkey file
85 ///
86 /// # Returns
87 ///
88 /// * `Ok(Wallet)` if the wallet was loaded successfully
89 /// * `Err(BittensorError)` if the wallet could not be loaded
90 ///
91 /// # Example
92 ///
93 /// ```rust,no_run
94 /// use bittensor_rs::wallet::Wallet;
95 ///
96 /// let wallet = Wallet::load("default", "default")?;
97 /// # Ok::<(), bittensor_rs::BittensorError>(())
98 /// ```
99 pub fn load(wallet_name: &str, hotkey_name: &str) -> Result<Self, BittensorError> {
100 let wallet_path = Self::default_wallet_path()?;
101 Self::load_from_path(wallet_name, hotkey_name, &wallet_path)
102 }
103
104 /// Load a wallet from a custom path
105 ///
106 /// # Arguments
107 ///
108 /// * `wallet_name` - Name of the wallet directory
109 /// * `hotkey_name` - Name of the hotkey file
110 /// * `base_path` - Base path where wallets are stored
111 ///
112 /// # Example
113 ///
114 /// ```rust,no_run
115 /// use bittensor_rs::wallet::Wallet;
116 /// use std::path::PathBuf;
117 ///
118 /// let base_path = PathBuf::from("/custom/wallets");
119 /// let wallet = Wallet::load_from_path("my_wallet", "my_hotkey", &base_path)?;
120 /// # Ok::<(), bittensor_rs::BittensorError>(())
121 /// ```
122 pub fn load_from_path(
123 wallet_name: &str,
124 hotkey_name: &str,
125 base_path: &Path,
126 ) -> Result<Self, BittensorError> {
127 let hotkey_path = base_path
128 .join(wallet_name)
129 .join("hotkeys")
130 .join(hotkey_name);
131
132 if !hotkey_path.exists() {
133 return Err(BittensorError::WalletError {
134 message: format!("Hotkey file not found: {}", hotkey_path.display()),
135 });
136 }
137
138 let keyfile_data = keyfile::load_keyfile(&hotkey_path)?;
139 let hotkey_pair = keyfile_data.to_keypair()?;
140
141 Ok(Self {
142 name: wallet_name.to_string(),
143 hotkey_name: hotkey_name.to_string(),
144 path: base_path.join(wallet_name),
145 hotkey_pair,
146 coldkey_pair: None,
147 })
148 }
149
150 /// Create a new wallet with a random seed
151 ///
152 /// # Arguments
153 ///
154 /// * `wallet_name` - Name of the wallet
155 /// * `hotkey_name` - Name of the hotkey
156 ///
157 /// # Returns
158 ///
159 /// A new wallet with a randomly generated keypair (not saved to disk)
160 ///
161 /// # Example
162 ///
163 /// ```
164 /// use bittensor_rs::wallet::Wallet;
165 ///
166 /// let wallet = Wallet::create_random("test_wallet", "test_hotkey").unwrap();
167 /// assert!(!wallet.hotkey().as_str().is_empty());
168 /// ```
169 pub fn create_random(wallet_name: &str, hotkey_name: &str) -> Result<Self, BittensorError> {
170 let (pair, _) = sr25519::Pair::generate();
171 let path = Self::default_wallet_path()?;
172
173 Ok(Self {
174 name: wallet_name.to_string(),
175 hotkey_name: hotkey_name.to_string(),
176 path: path.join(wallet_name),
177 hotkey_pair: pair,
178 coldkey_pair: None,
179 })
180 }
181
182 /// Create a wallet from a mnemonic phrase
183 ///
184 /// # Arguments
185 ///
186 /// * `wallet_name` - Name of the wallet
187 /// * `hotkey_name` - Name of the hotkey
188 /// * `mnemonic` - BIP39 mnemonic phrase (12 or 24 words)
189 ///
190 /// # Example
191 ///
192 /// ```rust,no_run
193 /// use bittensor_rs::wallet::Wallet;
194 ///
195 /// let mnemonic = "abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon about";
196 /// let wallet = Wallet::from_mnemonic("test", "test", mnemonic)?;
197 /// # Ok::<(), bittensor_rs::BittensorError>(())
198 /// ```
199 pub fn from_mnemonic(
200 wallet_name: &str,
201 hotkey_name: &str,
202 mnemonic: &str,
203 ) -> Result<Self, BittensorError> {
204 let pair = sr25519::Pair::from_string(mnemonic, None).map_err(|e| {
205 BittensorError::WalletError {
206 message: format!("Invalid mnemonic: {e:?}"),
207 }
208 })?;
209
210 let path =
211 Self::default_wallet_path().unwrap_or_else(|_| PathBuf::from("~/.bittensor/wallets"));
212
213 Ok(Self {
214 name: wallet_name.to_string(),
215 hotkey_name: hotkey_name.to_string(),
216 path: path.join(wallet_name),
217 hotkey_pair: pair,
218 coldkey_pair: None,
219 })
220 }
221
222 /// Create a wallet from a hex seed
223 ///
224 /// # Arguments
225 ///
226 /// * `wallet_name` - Name of the wallet
227 /// * `hotkey_name` - Name of the hotkey
228 /// * `seed_hex` - Hex-encoded seed (32 bytes, optionally prefixed with "0x")
229 ///
230 /// # Example
231 ///
232 /// ```
233 /// use bittensor_rs::wallet::Wallet;
234 ///
235 /// let seed = "0x0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef";
236 /// let wallet = Wallet::from_seed_hex("test", "test", seed).unwrap();
237 /// ```
238 pub fn from_seed_hex(
239 wallet_name: &str,
240 hotkey_name: &str,
241 seed_hex: &str,
242 ) -> Result<Self, BittensorError> {
243 let hex_str = seed_hex.strip_prefix("0x").unwrap_or(seed_hex);
244 let seed_bytes = hex::decode(hex_str).map_err(|e| BittensorError::WalletError {
245 message: format!("Invalid hex seed: {e}"),
246 })?;
247
248 if seed_bytes.len() != 32 {
249 return Err(BittensorError::WalletError {
250 message: format!("Seed must be 32 bytes, got {} bytes", seed_bytes.len()),
251 });
252 }
253
254 let mut seed_array = [0u8; 32];
255 seed_array.copy_from_slice(&seed_bytes);
256 let pair = sr25519::Pair::from_seed(&seed_array);
257
258 let path =
259 Self::default_wallet_path().unwrap_or_else(|_| PathBuf::from("~/.bittensor/wallets"));
260
261 Ok(Self {
262 name: wallet_name.to_string(),
263 hotkey_name: hotkey_name.to_string(),
264 path: path.join(wallet_name),
265 hotkey_pair: pair,
266 coldkey_pair: None,
267 })
268 }
269
270 /// Get the hotkey address as a `Hotkey` type
271 ///
272 /// # Example
273 ///
274 /// ```
275 /// use bittensor_rs::wallet::Wallet;
276 ///
277 /// let wallet = Wallet::create_random("test", "test").unwrap();
278 /// let hotkey = wallet.hotkey();
279 /// println!("Address: {}", hotkey);
280 /// ```
281 pub fn hotkey(&self) -> Hotkey {
282 let public = self.hotkey_pair.public();
283 let account_id = AccountId::from(public.0);
284 Hotkey::from_account_id(&account_id)
285 }
286
287 /// Get the hotkey as an AccountId
288 pub fn account_id(&self) -> AccountId {
289 AccountId::from(self.hotkey_pair.public().0)
290 }
291
292 /// Sign data with the hotkey
293 ///
294 /// # Arguments
295 ///
296 /// * `data` - The data to sign
297 ///
298 /// # Returns
299 ///
300 /// A 64-byte signature
301 ///
302 /// # Example
303 ///
304 /// ```
305 /// use bittensor_rs::wallet::Wallet;
306 ///
307 /// let wallet = Wallet::create_random("test", "test").unwrap();
308 /// let signature = wallet.sign(b"hello world");
309 /// assert_eq!(signature.len(), 64);
310 /// ```
311 pub fn sign(&self, data: &[u8]) -> Vec<u8> {
312 let signature = self.hotkey_pair.sign(data);
313 signature.0.to_vec()
314 }
315
316 /// Sign data and return hex-encoded signature
317 ///
318 /// # Example
319 ///
320 /// ```
321 /// use bittensor_rs::wallet::Wallet;
322 ///
323 /// let wallet = Wallet::create_random("test", "test").unwrap();
324 /// let sig_hex = wallet.sign_hex(b"hello");
325 /// assert_eq!(sig_hex.len(), 128); // 64 bytes = 128 hex chars
326 /// ```
327 pub fn sign_hex(&self, data: &[u8]) -> String {
328 hex::encode(self.sign(data))
329 }
330
331 /// Get a subxt-compatible signer for this wallet
332 ///
333 /// # Example
334 ///
335 /// ```
336 /// use bittensor_rs::wallet::Wallet;
337 ///
338 /// let wallet = Wallet::create_random("test", "test").unwrap();
339 /// let signer = wallet.signer();
340 /// ```
341 pub fn signer(&self) -> WalletSigner {
342 WalletSigner::from_sp_core_pair(self.hotkey_pair.clone())
343 }
344
345 /// Get the underlying keypair (for advanced usage)
346 pub fn keypair(&self) -> &sr25519::Pair {
347 &self.hotkey_pair
348 }
349
350 /// Verify a signature against this wallet's hotkey
351 ///
352 /// # Arguments
353 ///
354 /// * `data` - The original data that was signed
355 /// * `signature` - The 64-byte signature
356 ///
357 /// # Returns
358 ///
359 /// `true` if the signature is valid, `false` otherwise
360 ///
361 /// # Example
362 ///
363 /// ```
364 /// use bittensor_rs::wallet::Wallet;
365 ///
366 /// let wallet = Wallet::create_random("test", "test").unwrap();
367 /// let message = b"hello world";
368 /// let signature = wallet.sign(message);
369 /// assert!(wallet.verify(message, &signature));
370 /// ```
371 pub fn verify(&self, data: &[u8], signature: &[u8]) -> bool {
372 if signature.len() != 64 {
373 return false;
374 }
375
376 let mut sig_array = [0u8; 64];
377 sig_array.copy_from_slice(signature);
378 let sig = sr25519::Signature::from_raw(sig_array);
379
380 use sp_runtime::traits::Verify;
381 sig.verify(data, &self.hotkey_pair.public())
382 }
383
384 /// Load and unlock the coldkey with a password
385 ///
386 /// The coldkey is stored in `<wallet_path>/coldkey` and is encrypted.
387 ///
388 /// # Arguments
389 ///
390 /// * `password` - The password to decrypt the coldkey
391 ///
392 /// # Returns
393 ///
394 /// * `Ok(())` if the coldkey was loaded and decrypted
395 /// * `Err(BittensorError)` if loading or decryption failed
396 pub fn unlock_coldkey(&mut self, password: &str) -> Result<(), BittensorError> {
397 let coldkey_path = self.path.join("coldkey");
398
399 if !coldkey_path.exists() {
400 return Err(BittensorError::WalletError {
401 message: format!("Coldkey file not found: {}", coldkey_path.display()),
402 });
403 }
404
405 let keyfile_data = keyfile::load_encrypted_keyfile(&coldkey_path, password)?;
406 let coldkey_pair = keyfile_data.to_keypair()?;
407
408 self.coldkey_pair = Some(coldkey_pair);
409 Ok(())
410 }
411
412 /// Check if the coldkey is unlocked
413 pub fn is_coldkey_unlocked(&self) -> bool {
414 self.coldkey_pair.is_some()
415 }
416
417 /// Get the coldkey address if unlocked
418 pub fn coldkey(&self) -> Option<Hotkey> {
419 self.coldkey_pair.as_ref().map(|pair| {
420 let public = pair.public();
421 let account_id = AccountId::from(public.0);
422 Hotkey::from_account_id(&account_id)
423 })
424 }
425
426 /// Get the default Bittensor wallet path
427 fn default_wallet_path() -> Result<PathBuf, BittensorError> {
428 home::home_dir()
429 .map(|home| home.join(".bittensor").join("wallets"))
430 .ok_or_else(|| BittensorError::WalletError {
431 message: "Could not determine home directory".to_string(),
432 })
433 }
434}
435
436#[cfg(test)]
437mod tests {
438 use super::*;
439
440 #[test]
441 fn test_create_random_wallet() {
442 let wallet = Wallet::create_random("test_wallet", "test_hotkey").unwrap();
443 assert_eq!(wallet.name, "test_wallet");
444 assert_eq!(wallet.hotkey_name, "test_hotkey");
445 // Check that we have a valid hotkey
446 let hotkey = wallet.hotkey();
447 assert!(!hotkey.as_str().is_empty());
448 }
449
450 #[test]
451 fn test_sign_and_verify() {
452 let wallet = Wallet::create_random("test", "test").unwrap();
453 let message = b"test message";
454 let signature = wallet.sign(message);
455
456 assert_eq!(signature.len(), 64);
457 assert!(wallet.verify(message, &signature));
458 }
459
460 #[test]
461 fn test_sign_hex() {
462 let wallet = Wallet::create_random("test", "test").unwrap();
463 let sig_hex = wallet.sign_hex(b"test");
464 assert_eq!(sig_hex.len(), 128);
465 assert!(hex::decode(&sig_hex).is_ok());
466 }
467
468 #[test]
469 fn test_from_seed_hex() {
470 let seed = "0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef";
471 let wallet1 = Wallet::from_seed_hex("test", "test", seed).unwrap();
472 let wallet2 = Wallet::from_seed_hex("test", "test", &format!("0x{}", seed)).unwrap();
473
474 // Same seed should produce same hotkey
475 assert_eq!(wallet1.hotkey().as_str(), wallet2.hotkey().as_str());
476 }
477
478 #[test]
479 fn test_from_seed_hex_invalid() {
480 // Too short
481 let result = Wallet::from_seed_hex("test", "test", "0123");
482 assert!(result.is_err());
483
484 // Invalid hex
485 let result = Wallet::from_seed_hex("test", "test", "not_hex_at_all!");
486 assert!(result.is_err());
487 }
488
489 #[test]
490 fn test_verify_wrong_signature() {
491 let wallet = Wallet::create_random("test", "test").unwrap();
492 let wrong_sig = vec![0u8; 64];
493 assert!(!wallet.verify(b"test", &wrong_sig));
494 }
495
496 #[test]
497 fn test_verify_wrong_length() {
498 let wallet = Wallet::create_random("test", "test").unwrap();
499 let short_sig = vec![0u8; 32];
500 assert!(!wallet.verify(b"test", &short_sig));
501 }
502
503 #[test]
504 fn test_account_id() {
505 let wallet = Wallet::create_random("test", "test").unwrap();
506 let account_id = wallet.account_id();
507 let hotkey = wallet.hotkey();
508
509 // Account ID and hotkey should be consistent
510 assert_eq!(account_id.to_string(), hotkey.as_str());
511 }
512
513 #[test]
514 fn test_coldkey_not_unlocked() {
515 let wallet = Wallet::create_random("test", "test").unwrap();
516 assert!(!wallet.is_coldkey_unlocked());
517 assert!(wallet.coldkey().is_none());
518 }
519}