1use alloc::string::String;
4use alloc::vec::Vec;
5use core::ops::Deref;
6
7use kobe_primitives::slip10::DerivedEd25519Key;
8use kobe_primitives::{
9 DerivationStyle as _,
12 Derive,
13 DeriveError,
14 DerivedAccount,
15 DerivedPublicKey,
16 Wallet,
17 derive_range,
18};
19use zeroize::Zeroizing;
20
21use crate::derivation_style::DerivationStyle;
22
23#[derive(Clone)]
33pub struct SvmAccount {
34 inner: DerivedAccount,
35 keypair_base58: Zeroizing<String>,
36}
37
38impl core::fmt::Debug for SvmAccount {
39 fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
40 f.debug_struct("SvmAccount")
41 .field("inner", &self.inner)
42 .field("keypair_base58", &"[REDACTED]")
43 .finish()
44 }
45}
46
47impl SvmAccount {
48 #[inline]
52 #[must_use]
53 pub const fn keypair_base58(&self) -> &Zeroizing<String> {
54 &self.keypair_base58
55 }
56
57 #[inline]
59 #[must_use]
60 pub const fn as_derived_account(&self) -> &DerivedAccount {
61 &self.inner
62 }
63
64 #[inline]
67 #[must_use]
68 pub fn into_derived_account(self) -> DerivedAccount {
69 self.inner
70 }
71}
72
73impl Deref for SvmAccount {
74 type Target = DerivedAccount;
75
76 #[inline]
77 fn deref(&self) -> &Self::Target {
78 &self.inner
79 }
80}
81
82impl From<SvmAccount> for DerivedAccount {
83 #[inline]
84 fn from(svm: SvmAccount) -> Self {
85 svm.inner
86 }
87}
88
89#[derive(Debug)]
94pub struct Deriver<'a> {
95 wallet: &'a Wallet,
97}
98
99impl<'a> Deriver<'a> {
100 #[inline]
102 #[must_use]
103 pub const fn new(wallet: &'a Wallet) -> Self {
104 Self { wallet }
105 }
106
107 #[inline]
115 pub fn derive(&self, index: u32) -> Result<SvmAccount, DeriveError> {
116 self.derive_with(DerivationStyle::Standard, index)
117 }
118
119 pub fn derive_with(
131 &self,
132 style: DerivationStyle,
133 index: u32,
134 ) -> Result<SvmAccount, DeriveError> {
135 let path = style.path(index);
136 let derived = self.wallet.derive_ed25519(&path)?;
137 Ok(build_svm_account(&derived, path))
138 }
139
140 pub fn derive_many_with(
146 &self,
147 style: DerivationStyle,
148 start: u32,
149 count: u32,
150 ) -> Result<Vec<SvmAccount>, DeriveError> {
151 derive_range(start, count, |i| self.derive_with(style, i))
152 }
153
154 pub fn derive_at(&self, path: &str) -> Result<SvmAccount, DeriveError> {
163 let derived = self.wallet.derive_ed25519(path)?;
164 Ok(build_svm_account(&derived, String::from(path)))
165 }
166}
167
168impl Derive for Deriver<'_> {
169 type Account = SvmAccount;
170 type Error = DeriveError;
171
172 fn derive(&self, index: u32) -> Result<SvmAccount, DeriveError> {
173 self.derive_with(DerivationStyle::Standard, index)
174 }
175
176 fn derive_path(&self, path: &str) -> Result<SvmAccount, DeriveError> {
177 self.derive_at(path)
178 }
179}
180
181impl AsRef<DerivedAccount> for SvmAccount {
182 #[inline]
183 fn as_ref(&self) -> &DerivedAccount {
184 &self.inner
185 }
186}
187
188fn build_svm_account(derived: &DerivedEd25519Key, path: String) -> SvmAccount {
190 let public_key_bytes = derived.public_key_bytes();
191 let sk_bytes = derived.private_key_bytes();
192
193 let mut keypair_bytes = Zeroizing::new([0u8; 64]);
194 let (left, right) = keypair_bytes.split_at_mut(32);
195 left.copy_from_slice(sk_bytes.as_slice());
196 right.copy_from_slice(&public_key_bytes);
197 let keypair_b58 = bs58::encode(&*keypair_bytes).into_string();
198
199 let inner = DerivedAccount::new(
200 path,
201 sk_bytes,
202 DerivedPublicKey::Ed25519(public_key_bytes),
203 bs58::encode(&public_key_bytes).into_string(),
204 );
205
206 SvmAccount {
207 inner,
208 keypair_base58: Zeroizing::new(keypair_b58),
209 }
210}
211
212#[cfg(test)]
213#[allow(clippy::indexing_slicing, reason = "test assertions")]
214mod tests {
215 use kobe_primitives::DeriveExt;
216
217 use super::*;
218
219 const MNEMONIC: &str = "abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon about";
221
222 fn test_wallet() -> Wallet {
223 Wallet::from_mnemonic(MNEMONIC, None).unwrap()
224 }
225
226 #[test]
234 fn debug_redacts_keypair_base58() {
235 let acct = Deriver::new(&test_wallet()).derive(0).unwrap();
236 let kp = acct.keypair_base58().as_str().to_owned();
237 let dbg = format!("{acct:?}");
238 assert!(dbg.contains("[REDACTED]"));
239 assert!(!dbg.contains(&kp), "Debug must not leak keypair: {dbg}");
240 }
241
242 #[test]
243 fn kat_solana_phantom_abandon_index0() {
244 let acct = Deriver::new(&test_wallet()).derive(0).unwrap();
245 assert_eq!(acct.path(), "m/44'/501'/0'/0'");
246 assert_eq!(
247 acct.address(),
248 "HAgk14JpMQLgt6rVgv7cBQFJWFto5Dqxi472uT3DKpqk"
249 );
250 assert_eq!(
251 acct.private_key_hex().as_str(),
252 "37df573b3ac4ad5b522e064e25b63ea16bcbe79d449e81a0268d1047948bb445"
253 );
254 }
255
256 #[test]
257 fn kat_solana_phantom_abandon_index1() {
258 let acct = Deriver::new(&test_wallet()).derive(1).unwrap();
259 assert_eq!(acct.path(), "m/44'/501'/1'/0'");
260 assert_eq!(
261 acct.address(),
262 "Hh8QwFUA6MtVu1qAoq12ucvFHNwCcVTV7hpWjeY1Hztb"
263 );
264 assert_eq!(
265 acct.private_key_hex().as_str(),
266 "ba5e7b6e3680b4eb81db8e54c8e466b2e9a899355888403355d858ab985d2fc4"
267 );
268 }
269
270 #[test]
273 fn derivation_styles_produce_distinct_addresses() {
274 let w = test_wallet();
275 let d = Deriver::new(&w);
276 let standard = d.derive_with(DerivationStyle::Standard, 0).unwrap();
277 let trust = d.derive_with(DerivationStyle::Trust, 0).unwrap();
278 let ledger = d.derive_with(DerivationStyle::LedgerLive, 0).unwrap();
279 let legacy = d.derive_with(DerivationStyle::Legacy, 0).unwrap();
280 assert_eq!(standard.path(), "m/44'/501'/0'/0'");
281 assert_eq!(trust.path(), "m/44'/501'/0'");
282 assert_eq!(ledger.path(), "m/44'/501'/0'/0'/0'");
283 assert_eq!(legacy.path(), "m/501'/0'/0'/0'");
284 assert_ne!(standard.address(), trust.address());
285 assert_ne!(standard.address(), ledger.address());
286 assert_ne!(standard.address(), legacy.address());
287 assert_ne!(trust.address(), ledger.address());
288 }
289
290 #[test]
292 fn derive_many_matches_individual() {
293 let w = test_wallet();
294 let d = Deriver::new(&w);
295 let batch = d.derive_many(0, 3).unwrap();
296 let single: Vec<_> = (0..3).map(|i| d.derive(i).unwrap()).collect();
297 for (b, s) in batch.iter().zip(single.iter()) {
298 assert_eq!(b.address(), s.address());
299 assert_eq!(b.path(), s.path());
300 assert_eq!(b.keypair_base58(), s.keypair_base58());
301 }
302 }
303
304 #[test]
308 fn keypair_base58_matches_64_byte_layout() {
309 let w = test_wallet();
310 let acct = Deriver::new(&w).derive(0).unwrap();
311 let decoded = bs58::decode(acct.keypair_base58().as_str())
312 .into_vec()
313 .unwrap();
314 assert_eq!(decoded.len(), 64);
315 assert_eq!(&decoded[..32], acct.private_key_bytes().as_slice());
316 assert_eq!(&decoded[32..], acct.public_key_bytes());
317 }
318}