#[cfg(feature = "alloc")]
use alloc::{format, string::String, vec::Vec};
use bech32::{Bech32m, Hrp};
pub use kobe_primitives::DerivedAccount;
use kobe_primitives::{Derive, Wallet};
use crate::DeriveError;
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Default)]
#[non_exhaustive]
pub enum Network {
#[default]
Mainnet,
Testnet,
Signet,
Regtest,
Local,
}
impl Network {
#[must_use]
pub const fn hrp(self) -> &'static str {
match self {
Self::Mainnet => "spark",
Self::Testnet => "sparkt",
Self::Signet => "sparks",
Self::Regtest => "sparkrt",
Self::Local => "sparkl",
}
}
#[must_use]
pub const fn name(self) -> &'static str {
match self {
Self::Mainnet => "mainnet",
Self::Testnet => "testnet",
Self::Signet => "signet",
Self::Regtest => "regtest",
Self::Local => "local",
}
}
}
pub const SPARK_PURPOSE: u32 = 8_797_555;
const PROTO_TAG: u8 = 0x0a;
const COMPRESSED_PUBKEY_LEN: u8 = 33;
#[derive(Debug)]
pub struct Deriver<'a> {
wallet: &'a Wallet,
network: Network,
}
impl<'a> Deriver<'a> {
#[must_use]
pub const fn new(wallet: &'a Wallet) -> Self {
Self::with_network(wallet, Network::Mainnet)
}
#[must_use]
pub const fn with_network(wallet: &'a Wallet, network: Network) -> Self {
Self { wallet, network }
}
#[inline]
#[must_use]
pub const fn network(&self) -> Network {
self.network
}
#[inline]
pub fn derive(&self, index: u32) -> Result<DerivedAccount, DeriveError> {
self.derive_at_path(&format!("m/{SPARK_PURPOSE}'/{index}'/0'"))
}
pub fn derive_at_path(&self, path: &str) -> Result<DerivedAccount, DeriveError> {
let key = self.wallet.derive_secp256k1(path)?;
let pubkey_bytes = key.compressed_pubkey();
let address = encode_spark_address(&pubkey_bytes, self.network)?;
Ok(DerivedAccount::new(
String::from(path),
key.private_key_bytes(),
pubkey_bytes.to_vec(),
address,
))
}
}
impl Derive for Deriver<'_> {
type Error = DeriveError;
fn derive(&self, index: u32) -> Result<DerivedAccount, DeriveError> {
Deriver::derive(self, index)
}
fn derive_path(&self, path: &str) -> Result<DerivedAccount, DeriveError> {
self.derive_at_path(path)
}
}
fn encode_spark_address(
compressed_pubkey: &[u8; 33],
network: Network,
) -> Result<String, DeriveError> {
let mut payload = Vec::with_capacity(2 + compressed_pubkey.len());
payload.push(PROTO_TAG);
payload.push(COMPRESSED_PUBKEY_LEN);
payload.extend_from_slice(compressed_pubkey);
let hrp =
Hrp::parse(network.hrp()).map_err(|e| DeriveError::Bech32(format!("invalid HRP: {e}")))?;
bech32::encode::<Bech32m>(hrp, &payload)
.map_err(|e| DeriveError::Bech32(format!("encoding failed: {e}")))
}
#[cfg(test)]
mod tests {
use super::*;
const TEST_MNEMONIC: &str = "abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon about";
fn test_wallet() -> Wallet {
Wallet::from_mnemonic(TEST_MNEMONIC, None).unwrap()
}
#[test]
fn derive_starts_with_spark1() {
let wallet = test_wallet();
let derived = Deriver::new(&wallet).derive(0).unwrap();
assert!(
derived.address().starts_with("spark1"),
"mainnet address should start with spark1, got: {}",
derived.address()
);
}
#[test]
fn derive_correct_path() {
let wallet = test_wallet();
let derived = Deriver::new(&wallet).derive(0).unwrap();
assert_eq!(derived.path(), "m/8797555'/0'/0'");
}
#[test]
fn deterministic() {
let w1 = Wallet::from_mnemonic(TEST_MNEMONIC, None).unwrap();
let w2 = Wallet::from_mnemonic(TEST_MNEMONIC, None).unwrap();
let a1 = Deriver::new(&w1).derive(0).unwrap();
let a2 = Deriver::new(&w2).derive(0).unwrap();
assert_eq!(a1.address(), a2.address());
}
#[test]
fn different_account_numbers_differ() {
let wallet = test_wallet();
let d = Deriver::new(&wallet);
assert_ne!(
d.derive(0).unwrap().address(),
d.derive(1).unwrap().address()
);
}
#[test]
fn testnet_prefix() {
let wallet = test_wallet();
let a = Deriver::with_network(&wallet, Network::Testnet)
.derive(0)
.unwrap();
assert!(
a.address().starts_with("sparkt1"),
"testnet address should start with sparkt1, got: {}",
a.address()
);
}
#[test]
fn regtest_prefix() {
let wallet = test_wallet();
let a = Deriver::with_network(&wallet, Network::Regtest)
.derive(0)
.unwrap();
assert!(a.address().starts_with("sparkrt1"));
}
#[test]
fn network_accessor_returns_configured_value() {
let wallet = test_wallet();
assert_eq!(
Deriver::with_network(&wallet, Network::Signet).network(),
Network::Signet
);
}
#[test]
fn mainnet_and_testnet_addresses_differ() {
let wallet = test_wallet();
let main = Deriver::with_network(&wallet, Network::Mainnet)
.derive(0)
.unwrap();
let test = Deriver::with_network(&wallet, Network::Testnet)
.derive(0)
.unwrap();
assert_ne!(main.address(), test.address());
assert_eq!(main.private_key_bytes(), test.private_key_bytes());
}
#[test]
fn public_key_is_33_byte_compressed() {
let wallet = test_wallet();
let a = Deriver::new(&wallet).derive(0).unwrap();
let pk = a.public_key_bytes();
assert_eq!(pk.len(), 33);
let prefix = pk.first().copied().expect("33-byte pubkey");
assert!(
prefix == 0x02 || prefix == 0x03,
"compressed pubkey prefix must be 0x02 or 0x03"
);
}
#[test]
fn kat_encode_spark_address_matches_reference_vector() {
let pubkey_hex = "02894808873b896e21d29856a6d7bb346fb13c019739adb9bf0b6a8b7e28da53da";
let mut pubkey = [0u8; 33];
hex::decode_to_slice(pubkey_hex, &mut pubkey).unwrap();
let encoded = encode_spark_address(&pubkey, Network::Mainnet).unwrap();
assert_eq!(
encoded,
"spark1pgss9z2gpzrnhztwy8ffs44x67angma38sqewwddhxlsk65t0c5d5576quly2j"
);
}
#[test]
fn kat_spark_abandon_mnemonic_index0() {
let wallet = test_wallet();
let a = Deriver::new(&wallet).derive(0).unwrap();
assert_eq!(a.path(), "m/8797555'/0'/0'");
assert_eq!(
a.address(),
"spark1pgssy6vty7krpze82ecm8j39gd35v35aqjjmhftc4culawsavkyh564uc6zmqs"
);
}
}