Skip to main content

kobe_primitives/
derive.rs

1//! Unified derivation trait and account type.
2//!
3//! All chain-specific derivers implement [`Derive`], providing a consistent
4//! API surface across chains. [`DeriveExt`] is automatically implemented for
5//! all `Derive` types, providing batch derivation via [`derive_many`](DeriveExt::derive_many).
6
7use alloc::string::String;
8use alloc::vec::Vec;
9
10use zeroize::Zeroizing;
11
12use crate::DeriveError;
13
14/// A derived HD account — unified across all chains.
15///
16/// Holds the derivation path, a 32-byte private key, a chain-specific public
17/// key (33 B compressed / 65 B uncompressed secp256k1 or 32 B Ed25519 /
18/// x-only), and the on-chain address string. The private key is zeroized on
19/// drop.
20///
21/// Fields are private; use the accessor methods to read them. Hex-encoded
22/// views ([`private_key_hex`](Self::private_key_hex),
23/// [`public_key_hex`](Self::public_key_hex)) are computed on demand.
24#[derive(Debug, Clone)]
25pub struct DerivedAccount {
26    path: String,
27    private_key: Zeroizing<[u8; 32]>,
28    public_key: Vec<u8>,
29    address: String,
30}
31
32impl DerivedAccount {
33    /// Construct a derived account from its raw components.
34    ///
35    /// This is the single entry point; chain crates call it after completing
36    /// their derivation pipeline.
37    #[inline]
38    #[must_use]
39    pub const fn new(
40        path: String,
41        private_key: Zeroizing<[u8; 32]>,
42        public_key: Vec<u8>,
43        address: String,
44    ) -> Self {
45        Self {
46            path,
47            private_key,
48            public_key,
49            address,
50        }
51    }
52
53    /// BIP-32 / SLIP-10 derivation path (e.g. `m/44'/60'/0'/0/0`).
54    #[inline]
55    #[must_use]
56    pub fn path(&self) -> &str {
57        &self.path
58    }
59
60    /// Raw 32-byte private key (zeroized on drop).
61    #[inline]
62    #[must_use]
63    pub const fn private_key_bytes(&self) -> &Zeroizing<[u8; 32]> {
64        &self.private_key
65    }
66
67    /// Lowercase hex-encoded private key (64 chars, zeroized on drop).
68    #[inline]
69    #[must_use]
70    pub fn private_key_hex(&self) -> Zeroizing<String> {
71        Zeroizing::new(hex::encode(*self.private_key))
72    }
73
74    /// Chain-specific public key bytes.
75    ///
76    /// Length: 33 (compressed secp256k1), 65 (uncompressed secp256k1),
77    /// or 32 (Ed25519 / BIP-340 x-only).
78    #[inline]
79    #[must_use]
80    pub fn public_key_bytes(&self) -> &[u8] {
81        &self.public_key
82    }
83
84    /// Lowercase hex-encoded public key.
85    #[inline]
86    #[must_use]
87    pub fn public_key_hex(&self) -> String {
88        hex::encode(&self.public_key)
89    }
90
91    /// On-chain address in the chain's native format.
92    #[inline]
93    #[must_use]
94    pub fn address(&self) -> &str {
95        &self.address
96    }
97}
98
99/// Unified derivation trait implemented by all chain derivers.
100///
101/// Provides a consistent API for deriving accounts regardless of the
102/// underlying chain. Each chain crate (`kobe-evm`, `kobe-btc`, etc.)
103/// implements this trait on its `Deriver` type.
104///
105/// Batch derivation is provided by the blanket [`DeriveExt`] trait.
106///
107/// # Example
108///
109/// ```no_run
110/// use kobe_primitives::{Derive, DeriveExt, DerivedAccount};
111///
112/// fn derive_first_account<D: Derive>(d: &D) -> DerivedAccount {
113///     d.derive(0).unwrap()
114/// }
115/// ```
116pub trait Derive {
117    /// The error type returned by derivation operations.
118    type Error: core::fmt::Debug + core::fmt::Display + From<DeriveError>;
119
120    /// Derive an account at the given index using the chain's default path.
121    ///
122    /// # Errors
123    ///
124    /// Returns an error if key derivation or address encoding fails.
125    fn derive(&self, index: u32) -> Result<DerivedAccount, Self::Error>;
126
127    /// Derive an account at a custom path string.
128    ///
129    /// # Errors
130    ///
131    /// Returns an error if the path is invalid or derivation fails.
132    fn derive_path(&self, path: &str) -> Result<DerivedAccount, Self::Error>;
133}
134
135/// Extension trait providing batch derivation for all [`Derive`] implementors.
136///
137/// This trait is automatically implemented for any type implementing `Derive`.
138pub trait DeriveExt: Derive {
139    /// Derive `count` accounts starting at index `start`.
140    ///
141    /// # Errors
142    ///
143    /// Returns [`DeriveError::Input`] if `start + count` overflows `u32`.
144    fn derive_many(&self, start: u32, count: u32) -> Result<Vec<DerivedAccount>, Self::Error> {
145        let end = start.checked_add(count).ok_or_else(|| {
146            DeriveError::Input(String::from("derive_many: start + count overflows u32"))
147        })?;
148        (start..end).map(|i| self.derive(i)).collect()
149    }
150}
151
152impl<T: Derive> DeriveExt for T {}
153
154#[cfg(test)]
155mod tests {
156    use super::*;
157
158    fn sample_account() -> DerivedAccount {
159        let mut sk = Zeroizing::new([0u8; 32]);
160        hex::decode_to_slice(
161            "1ab42cc412b618bdea3a599e3c9bae199ebf030895b039e9db1e30dafb12b727",
162            sk.as_mut_slice(),
163        )
164        .unwrap();
165        DerivedAccount::new(
166            String::from("m/44'/60'/0'/0/0"),
167            sk,
168            hex::decode("0237b0bb7a8288d38ed49a524b5dc98cff3eb5ca824c9f9dc0dfdb3d9cd600f299")
169                .unwrap(),
170            String::from("0x9858EfFD232B4033E47d90003D41EC34EcaEda94"),
171        )
172    }
173
174    #[test]
175    fn accessors_expose_all_fields() {
176        let acct = sample_account();
177        assert_eq!(acct.path(), "m/44'/60'/0'/0/0");
178        assert_eq!(acct.private_key_bytes().len(), 32);
179        assert_eq!(
180            acct.private_key_hex().as_str(),
181            "1ab42cc412b618bdea3a599e3c9bae199ebf030895b039e9db1e30dafb12b727"
182        );
183        assert_eq!(acct.public_key_bytes().len(), 33);
184        assert_eq!(
185            acct.public_key_hex(),
186            "0237b0bb7a8288d38ed49a524b5dc98cff3eb5ca824c9f9dc0dfdb3d9cd600f299"
187        );
188        assert_eq!(acct.address(), "0x9858EfFD232B4033E47d90003D41EC34EcaEda94");
189    }
190
191    #[test]
192    fn private_key_hex_is_reversible() {
193        let acct = sample_account();
194        let hex = acct.private_key_hex();
195        let mut decoded = [0u8; 32];
196        hex::decode_to_slice(hex.as_str(), &mut decoded).unwrap();
197        assert_eq!(&decoded, acct.private_key_bytes().as_ref());
198    }
199}