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 /// The layout depends on the deriving chain; callers that need a
77 /// uniform representation should convert with the chain's documented
78 /// procedure. Current mapping:
79 ///
80 /// | Chain(s) | Length | Encoding |
81 /// | --- | --- | --- |
82 /// | `kobe-btc`, `kobe-cosmos`, `kobe-spark`, `kobe-xrpl` | **33 B** | secp256k1 **compressed** (`0x02`/`0x03` prefix + x) |
83 /// | `kobe-evm`, `kobe-fil`, `kobe-tron` | **65 B** | secp256k1 **uncompressed** (`0x04` prefix + x + y) |
84 /// | `kobe-svm`, `kobe-sui`, `kobe-aptos`, `kobe-ton` | **32 B** | Ed25519 |
85 /// | `kobe-nostr` | **32 B** | BIP-340 / NIP-19 **x-only** secp256k1 |
86 ///
87 /// Cross-chain code that needs a single canonical form should inspect
88 /// the length and branch accordingly, or rely on chain-specific
89 /// `<Chain>Account` newtypes that expose typed views.
90 #[inline]
91 #[must_use]
92 pub fn public_key_bytes(&self) -> &[u8] {
93 &self.public_key
94 }
95
96 /// Lowercase hex-encoded public key.
97 #[inline]
98 #[must_use]
99 pub fn public_key_hex(&self) -> String {
100 hex::encode(&self.public_key)
101 }
102
103 /// On-chain address in the chain's native format.
104 #[inline]
105 #[must_use]
106 pub fn address(&self) -> &str {
107 &self.address
108 }
109}
110
111/// Derive a range of accounts by repeatedly invoking a derivation closure.
112///
113/// Generic building block for every chain's batch-derivation entry point:
114/// validates `start + count` against `u32` overflow and collects the
115/// results into a `Vec<T>`.
116///
117/// # Errors
118///
119/// Returns [`DeriveError::Input`] (wrapped via `E: From<DeriveError>`) if
120/// `start + count` overflows `u32`, or propagates any error produced by
121/// `f`.
122///
123/// # Example
124///
125/// ```no_run
126/// use kobe_primitives::{DerivedAccount, DeriveError, derive_range};
127///
128/// fn batch(count: u32) -> Result<Vec<DerivedAccount>, DeriveError> {
129/// derive_range(0, count, |_i| todo!("derive one"))
130/// }
131/// ```
132pub fn derive_range<T, E, F>(start: u32, count: u32, f: F) -> Result<Vec<T>, E>
133where
134 F: FnMut(u32) -> Result<T, E>,
135 E: From<DeriveError>,
136{
137 let end = start.checked_add(count).ok_or_else(|| {
138 E::from(DeriveError::Input(String::from(
139 "derive_many: start + count overflows u32",
140 )))
141 })?;
142 (start..end).map(f).collect()
143}
144
145/// Unified derivation trait implemented by all chain derivers.
146///
147/// Provides a consistent API for deriving accounts regardless of the
148/// underlying chain. Each chain crate (`kobe-evm`, `kobe-btc`, etc.)
149/// implements this trait on its `Deriver` type.
150///
151/// Batch derivation is provided by the blanket [`DeriveExt`] trait.
152///
153/// # Example
154///
155/// ```no_run
156/// use kobe_primitives::{Derive, DeriveExt, DerivedAccount};
157///
158/// fn derive_first_account<D: Derive>(d: &D) -> DerivedAccount {
159/// d.derive(0).unwrap()
160/// }
161/// ```
162pub trait Derive {
163 /// The error type returned by derivation operations.
164 type Error: core::fmt::Debug + core::fmt::Display + From<DeriveError>;
165
166 /// Derive an account at the given index using the chain's default path.
167 ///
168 /// # Errors
169 ///
170 /// Returns an error if key derivation or address encoding fails.
171 fn derive(&self, index: u32) -> Result<DerivedAccount, Self::Error>;
172
173 /// Derive an account at a custom path string.
174 ///
175 /// # Errors
176 ///
177 /// Returns an error if the path is invalid or derivation fails.
178 fn derive_path(&self, path: &str) -> Result<DerivedAccount, Self::Error>;
179}
180
181/// Extension trait providing batch derivation for all [`Derive`] implementors.
182///
183/// This trait is automatically implemented for any type implementing `Derive`.
184/// Import it to call `derive_many` on any deriver:
185///
186/// ```no_run
187/// use kobe_primitives::{Derive, DeriveExt};
188/// # struct D;
189/// # impl Derive for D {
190/// # type Error = kobe_primitives::DeriveError;
191/// # fn derive(&self, _: u32) -> Result<kobe_primitives::DerivedAccount, Self::Error> { unimplemented!() }
192/// # fn derive_path(&self, _: &str) -> Result<kobe_primitives::DerivedAccount, Self::Error> { unimplemented!() }
193/// # }
194/// # let d = D;
195/// let accounts = d.derive_many(0, 5).unwrap();
196/// ```
197pub trait DeriveExt: Derive {
198 /// Derive `count` accounts starting at index `start`.
199 ///
200 /// # Errors
201 ///
202 /// Returns [`DeriveError::Input`] if `start + count` overflows `u32`,
203 /// or propagates any derivation error.
204 #[inline]
205 fn derive_many(&self, start: u32, count: u32) -> Result<Vec<DerivedAccount>, Self::Error> {
206 derive_range(start, count, |i| self.derive(i))
207 }
208}
209
210impl<T: Derive> DeriveExt for T {}
211
212#[cfg(test)]
213mod tests {
214 use super::*;
215
216 fn sample_account() -> DerivedAccount {
217 let mut sk = Zeroizing::new([0u8; 32]);
218 hex::decode_to_slice(
219 "1ab42cc412b618bdea3a599e3c9bae199ebf030895b039e9db1e30dafb12b727",
220 sk.as_mut_slice(),
221 )
222 .unwrap();
223 DerivedAccount::new(
224 String::from("m/44'/60'/0'/0/0"),
225 sk,
226 hex::decode("0237b0bb7a8288d38ed49a524b5dc98cff3eb5ca824c9f9dc0dfdb3d9cd600f299")
227 .unwrap(),
228 String::from("0x9858EfFD232B4033E47d90003D41EC34EcaEda94"),
229 )
230 }
231
232 #[test]
233 fn accessors_expose_all_fields() {
234 let acct = sample_account();
235 assert_eq!(acct.path(), "m/44'/60'/0'/0/0");
236 assert_eq!(acct.private_key_bytes().len(), 32);
237 assert_eq!(
238 acct.private_key_hex().as_str(),
239 "1ab42cc412b618bdea3a599e3c9bae199ebf030895b039e9db1e30dafb12b727"
240 );
241 assert_eq!(acct.public_key_bytes().len(), 33);
242 assert_eq!(
243 acct.public_key_hex(),
244 "0237b0bb7a8288d38ed49a524b5dc98cff3eb5ca824c9f9dc0dfdb3d9cd600f299"
245 );
246 assert_eq!(acct.address(), "0x9858EfFD232B4033E47d90003D41EC34EcaEda94");
247 }
248
249 #[test]
250 fn private_key_hex_is_reversible() {
251 let acct = sample_account();
252 let hex = acct.private_key_hex();
253 let mut decoded = [0u8; 32];
254 hex::decode_to_slice(hex.as_str(), &mut decoded).unwrap();
255 assert_eq!(&decoded, acct.private_key_bytes().as_ref());
256 }
257}