#[cfg(feature = "alloc")]
use alloc::{format, string::String, vec::Vec};
use bech32::{Bech32m, Hrp};
use kobe_primitives::{Derive, DeriveError, DerivedAccount, DerivedPublicKey, Wallet};
#[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(&format!("m/{SPARK_PURPOSE}'/{index}'/0'"))
}
pub fn derive_at(&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(),
DerivedPublicKey::Secp256k1Compressed(pubkey_bytes),
address,
))
}
}
impl Derive for Deriver<'_> {
type Account = DerivedAccount;
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)
}
}
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::AddressEncoding(format!("spark: invalid HRP: {e}")))?;
bech32::encode::<Bech32m>(hrp, &payload)
.map_err(|e| DeriveError::AddressEncoding(format!("spark bech32m: {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 kat_encode_spark_address_matches_ethanmarcuss_reference() {
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_mainnet_abandon_index0() {
let a = Deriver::new(&test_wallet()).derive(0).unwrap();
assert_eq!(a.path(), "m/8797555'/0'/0'");
assert_eq!(
a.address(),
"spark1pgssy6vty7krpze82ecm8j39gd35v35aqjjmhftc4culawsavkyh564uc6zmqs"
);
}
#[test]
fn testnet_and_mainnet_share_keys_not_address() {
let w = test_wallet();
let main = Deriver::new(&w).derive(0).unwrap();
let test = Deriver::with_network(&w, Network::Testnet)
.derive(0)
.unwrap();
assert!(main.address().starts_with("spark1"));
assert!(test.address().starts_with("sparkt1"));
assert_eq!(main.private_key_bytes(), test.private_key_bytes());
assert_eq!(main.public_key_bytes(), test.public_key_bytes());
assert_ne!(main.address(), test.address());
}
#[test]
fn every_network_roundtrips_hrp() {
let w = test_wallet();
for net in [
Network::Mainnet,
Network::Testnet,
Network::Signet,
Network::Regtest,
Network::Local,
] {
let a = Deriver::with_network(&w, net).derive(0).unwrap();
let (hrp, _) = bech32::decode(a.address()).unwrap();
assert_eq!(hrp.as_str(), net.hrp(), "HRP mismatch for {net:?}");
}
}
}