use crate::bip39::Mnemonic;
use crate::slip10::DerivedKey;
use crate::types::Result;
#[derive(Clone)]
pub struct Wallet {
mnemonic: Mnemonic,
seed: [u8; 64],
}
impl Wallet {
pub fn generate_12(entropy: &[u8; 16]) -> Result<Self> {
Self::generate_12_with_passphrase(entropy, "")
}
pub fn generate_12_with_passphrase(entropy: &[u8; 16], passphrase: &str) -> Result<Self> {
let mnemonic = Mnemonic::from_entropy_128(entropy)?;
let seed = mnemonic.derive_seed(passphrase);
Ok(Self { mnemonic, seed })
}
pub fn generate_24(entropy: &[u8; 32]) -> Result<Self> {
Self::generate_24_with_passphrase(entropy, "")
}
pub fn generate_24_with_passphrase(entropy: &[u8; 32], passphrase: &str) -> Result<Self> {
let mnemonic = Mnemonic::from_entropy_256(entropy)?;
let seed = mnemonic.derive_seed(passphrase);
Ok(Self { mnemonic, seed })
}
pub fn from_mnemonic(phrase: &str) -> Result<Self> {
Self::from_mnemonic_with_passphrase(phrase, "")
}
pub fn from_mnemonic_with_passphrase(phrase: &str, passphrase: &str) -> Result<Self> {
let mnemonic = Mnemonic::from_phrase(phrase)?;
let seed = mnemonic.derive_seed(passphrase);
Ok(Self { mnemonic, seed })
}
pub fn mnemonic(&self) -> &str {
self.mnemonic.phrase()
}
pub fn word_count(&self) -> usize {
self.mnemonic.word_count()
}
pub fn seed(&self) -> &[u8; 64] {
&self.seed
}
pub fn derive_key(&self, account: u32) -> DerivedKey {
DerivedKey::derive_solana_path(&self.seed, account, 0)
}
#[cfg(feature = "crypto")]
pub fn keypair(&self, account: u32) -> Result<crate::crypto::Keypair> {
self.derive_key(account).to_keypair()
}
#[cfg(feature = "crypto")]
pub fn pubkey(&self, account: u32) -> Result<crate::types::Pubkey> {
Ok(self.keypair(account)?.pubkey())
}
#[cfg(feature = "crypto")]
pub fn default_pubkey(&self) -> Result<crate::types::Pubkey> {
self.pubkey(0)
}
}
impl core::fmt::Debug for Wallet {
fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
write!(f, "Wallet({} words)", self.word_count())
}
}
impl Drop for Wallet {
fn drop(&mut self) {
for b in self.seed.iter_mut() {
unsafe { core::ptr::write_volatile(b, 0) };
}
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn generate_12_word_wallet() {
let entropy = [0x42u8; 16];
let wallet = Wallet::generate_12(&entropy).unwrap();
assert_eq!(wallet.word_count(), 12);
assert!(!wallet.mnemonic().is_empty());
}
#[test]
fn generate_24_word_wallet() {
let entropy = [0x42u8; 32];
let wallet = Wallet::generate_24(&entropy).unwrap();
assert_eq!(wallet.word_count(), 24);
}
#[test]
fn restore_from_mnemonic() {
let phrase = "abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon about";
let wallet = Wallet::from_mnemonic(phrase).unwrap();
assert_eq!(wallet.mnemonic(), phrase);
assert_eq!(wallet.word_count(), 12);
}
#[test]
fn restore_invalid_mnemonic() {
assert!(Wallet::from_mnemonic("invalid words here").is_err());
}
#[test]
fn roundtrip_generate_restore() {
let entropy = [0xABu8; 16];
let wallet1 = Wallet::generate_12(&entropy).unwrap();
let wallet2 = Wallet::from_mnemonic(wallet1.mnemonic()).unwrap();
assert_eq!(wallet1.seed(), wallet2.seed());
}
#[test]
fn passphrase_changes_seed() {
let entropy = [0x42u8; 16];
let w1 = Wallet::generate_12(&entropy).unwrap();
let w2 = Wallet::generate_12_with_passphrase(&entropy, "secret").unwrap();
assert_eq!(w1.mnemonic(), w2.mnemonic()); assert_ne!(w1.seed(), w2.seed()); }
#[test]
fn different_accounts_different_keys() {
let entropy = [0x42u8; 16];
let wallet = Wallet::generate_12(&entropy).unwrap();
let key0 = wallet.derive_key(0);
let key1 = wallet.derive_key(1);
assert_ne!(key0.key_bytes(), key1.key_bytes());
}
#[cfg(feature = "crypto")]
#[test]
fn keypair_can_sign() {
let phrase = "abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon about";
let wallet = Wallet::from_mnemonic(phrase).unwrap();
let kp = wallet.keypair(0).unwrap();
let msg = b"hello solana from wallet";
let sig = kp.sign(msg);
assert!(crate::crypto::verify(&kp.pubkey(), msg, &sig));
}
#[cfg(feature = "crypto")]
#[test]
fn multiple_accounts_all_valid() {
let phrase = "abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon about";
let wallet = Wallet::from_mnemonic(phrase).unwrap();
for i in 0..5 {
let kp = wallet.keypair(i).unwrap();
let msg = b"test";
let sig = kp.sign(msg);
assert!(crate::crypto::verify(&kp.pubkey(), msg, &sig));
}
}
#[cfg(feature = "crypto")]
#[test]
fn known_solana_derivation() {
let wallet = Wallet::from_mnemonic(
"abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon about"
).unwrap();
let pubkey = wallet.default_pubkey().unwrap();
let wallet2 = Wallet::from_mnemonic(
"abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon about"
).unwrap();
let pubkey2 = wallet2.default_pubkey().unwrap();
assert_eq!(pubkey.as_bytes(), pubkey2.as_bytes());
let expected_bs58 = "HAgk14JpMQLgt6rVgv7cBQFJWFto5Dqxi472uT3DKpqk";
let actual_bs58 = pubkey.to_bs58();
assert_eq!(actual_bs58, expected_bs58,
"Expected Solana address {} but got {}", expected_bs58, actual_bs58);
}
}