Skip to main content

kobe_sol/
deriver.rs

1//! Solana address derivation from HD wallet.
2
3use alloc::string::String;
4use alloc::vec::Vec;
5use ed25519_dalek::VerifyingKey;
6use zeroize::Zeroizing;
7
8use crate::Error;
9use crate::slip10::DerivedKey;
10use kobe_core::Wallet;
11
12/// A derived Solana address with associated keys.
13#[derive(Debug, Clone)]
14pub struct DerivedAddress {
15    /// Derivation path used.
16    pub path: String,
17    /// Private key as hex string (zeroized on drop).
18    pub private_key_hex: Zeroizing<String>,
19    /// Public key as hex string.
20    pub public_key_hex: String,
21    /// Solana address (Base58 encoded public key).
22    pub address: String,
23}
24
25/// Solana address deriver using SLIP-0010 Ed25519.
26#[derive(Debug)]
27pub struct Deriver<'a> {
28    wallet: &'a Wallet,
29}
30
31impl<'a> Deriver<'a> {
32    /// Create a new Solana deriver from a wallet.
33    #[inline]
34    #[must_use]
35    pub const fn new(wallet: &'a Wallet) -> Self {
36        Self { wallet }
37    }
38
39    /// Derive a Solana address at the given account index.
40    ///
41    /// Uses path: m/44'/501'/account'/0'
42    ///
43    /// # Errors
44    ///
45    /// Returns an error if derivation fails.
46    #[inline]
47    pub fn derive(&self, account: u32) -> Result<DerivedAddress, Error> {
48        self.derive_with_change(account, 0)
49    }
50
51    /// Derive a Solana address with custom account and change index.
52    ///
53    /// Uses path: m/44'/501'/account'/change'
54    ///
55    /// # Errors
56    ///
57    /// Returns an error if derivation fails.
58    pub fn derive_with_change(&self, account: u32, change: u32) -> Result<DerivedAddress, Error> {
59        let path = DerivedKey::format_path(account, change);
60        self.derive_at_path(account, change).map(|mut addr| {
61            addr.path = path;
62            addr
63        })
64    }
65
66    /// Internal derivation at specific account/change indices.
67    fn derive_at_path(&self, account: u32, change: u32) -> Result<DerivedAddress, Error> {
68        let derived = DerivedKey::derive_solana_path(self.wallet.seed(), account, change)?;
69        let signing_key = derived.to_signing_key();
70        let verifying_key: VerifyingKey = signing_key.verifying_key();
71
72        let public_key_bytes = verifying_key.as_bytes();
73        let address = bs58::encode(public_key_bytes).into_string();
74
75        Ok(DerivedAddress {
76            path: DerivedKey::format_path(account, change),
77            private_key_hex: Zeroizing::new(hex::encode(derived.private_key.as_slice())),
78            public_key_hex: hex::encode(public_key_bytes),
79            address,
80        })
81    }
82
83    /// Derive multiple addresses in sequence.
84    ///
85    /// # Errors
86    ///
87    /// Returns an error if any derivation fails.
88    pub fn derive_many(
89        &self,
90        start_account: u32,
91        count: u32,
92    ) -> Result<Vec<DerivedAddress>, Error> {
93        (start_account..start_account + count)
94            .map(|account| self.derive(account))
95            .collect()
96    }
97}
98
99#[cfg(test)]
100mod tests {
101    use super::*;
102
103    fn test_wallet() -> Wallet {
104        Wallet::from_mnemonic(
105            "abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon about",
106            None,
107        )
108        .unwrap()
109    }
110
111    #[test]
112    fn test_derive_address() {
113        let wallet = test_wallet();
114        let deriver = Deriver::new(&wallet);
115        let addr = deriver.derive(0).unwrap();
116
117        // Solana addresses are 32-44 characters in Base58
118        assert!(addr.address.len() >= 32 && addr.address.len() <= 44);
119        assert_eq!(addr.path, "m/44'/501'/0'/0'");
120    }
121
122    #[test]
123    fn test_derive_many() {
124        let wallet = test_wallet();
125        let deriver = Deriver::new(&wallet);
126        let addresses = deriver.derive_many(0, 3).unwrap();
127
128        assert_eq!(addresses.len(), 3);
129        assert_eq!(addresses[0].path, "m/44'/501'/0'/0'");
130        assert_eq!(addresses[1].path, "m/44'/501'/1'/0'");
131        assert_eq!(addresses[2].path, "m/44'/501'/2'/0'");
132
133        // All addresses should be unique
134        assert_ne!(addresses[0].address, addresses[1].address);
135        assert_ne!(addresses[1].address, addresses[2].address);
136    }
137
138    #[test]
139    fn test_deterministic_derivation() {
140        let wallet = test_wallet();
141        let deriver = Deriver::new(&wallet);
142
143        let addr1 = deriver.derive(0).unwrap();
144        let addr2 = deriver.derive(0).unwrap();
145
146        assert_eq!(addr1.address, addr2.address);
147        assert_eq!(*addr1.private_key_hex, *addr2.private_key_hex);
148    }
149}