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}