#![forbid(unsafe_code)]
#![cfg_attr(not(feature = "std"), no_std)]
extern crate alloc;
use alloc::string::ToString;
use bech32::{self, Bech32m, Hrp};
use core::fmt;
use hex::{decode as hex_to_bytes, encode as bytes_to_hex};
#[cfg(not(feature = "std"))]
use alloc::{string::String, vec::Vec};
#[cfg(feature = "std")]
use std::{string::String, vec::Vec};
#[derive(Debug, Copy, Clone, Eq, PartialEq)]
pub enum Network {
Mainnet,
Testnet,
Signet,
Regtest,
Local,
}
impl Network {
fn hrp(self) -> &'static str {
match self {
Network::Mainnet => "sp",
Network::Testnet => "spt",
Network::Signet => "sps",
Network::Regtest => "sprt",
Network::Local => "spl",
}
}
fn from_hrp(hrp: &str) -> Option<Self> {
match hrp {
"sp" => Some(Network::Mainnet),
"spt" => Some(Network::Testnet),
"sps" => Some(Network::Signet),
"sprt" => Some(Network::Regtest),
"spl" => Some(Network::Local),
_ => None,
}
}
}
#[derive(Debug)]
pub enum SparkAddressError {
InvalidBech32(bech32::DecodeError),
UnknownPrefix(String),
InvalidVariant,
MixedCase,
InvalidLength,
BadProto,
Hex(hex::FromHexError),
WrongKeyLength(usize),
#[cfg(feature = "validate-secp256k1")]
InvalidSecp256k1,
Bech32Encode(bech32::EncodeError),
}
impl fmt::Display for SparkAddressError {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
SparkAddressError::InvalidBech32(e) => write!(f, "bech32 decode error: {e}"),
SparkAddressError::UnknownPrefix(p) => write!(f, "unknown HRP prefix: {p}"),
SparkAddressError::InvalidVariant => write!(f, "bech32 variant is not Bech32m"),
SparkAddressError::MixedCase => write!(f, "address contains mixed upper/lower case"),
SparkAddressError::InvalidLength => write!(f, "address exceeds maximum length (90)"),
SparkAddressError::BadProto => write!(f, "invalid proto payload"),
SparkAddressError::Hex(e) => write!(f, "hex decode error: {e}"),
SparkAddressError::WrongKeyLength(n) => {
write!(f, "wrong pubkey length: {n} (expected 33)")
}
#[cfg(feature = "validate-secp256k1")]
SparkAddressError::InvalidSecp256k1 => write!(f, "invalid secp256k1 pubkey"),
SparkAddressError::Bech32Encode(e) => write!(f, "bech32 encode error: {e}"),
}
}
}
#[cfg(feature = "std")]
impl std::error::Error for SparkAddressError {}
impl From<bech32::DecodeError> for SparkAddressError {
fn from(e: bech32::DecodeError) -> Self {
Self::InvalidBech32(e)
}
}
impl From<bech32::EncodeError> for SparkAddressError {
fn from(e: bech32::EncodeError) -> Self {
Self::Bech32Encode(e)
}
}
impl From<hex::FromHexError> for SparkAddressError {
fn from(e: hex::FromHexError) -> Self {
Self::Hex(e)
}
}
#[derive(Debug, Clone, Eq, PartialEq)]
pub struct SparkAddressData {
pub identity_public_key: String,
pub network: Network,
}
const TAG: u8 = 0x0a;
fn encode_proto(key: &[u8]) -> Vec<u8> {
let mut out = Vec::with_capacity(2 + key.len());
out.push(TAG);
let key_len: u8 = key.len().try_into().expect("key length exceeds 255 bytes");
out.push(key_len);
out.extend_from_slice(key);
out
}
fn decode_proto(buf: &[u8]) -> Result<&[u8], SparkAddressError> {
if buf.len() >= 3 && buf[0] == TAG && buf[1] as usize + 2 == buf.len() {
Ok(&buf[2..])
} else {
Err(SparkAddressError::BadProto)
}
}
pub fn encode_spark_address(data: &SparkAddressData) -> Result<String, SparkAddressError> {
#[cfg(feature = "validate-secp256k1")]
validate_pubkey(&data.identity_public_key)?;
let key_bytes = hex_to_bytes(&data.identity_public_key)?;
if key_bytes.len() != 33 {
return Err(SparkAddressError::WrongKeyLength(key_bytes.len()));
}
let proto = encode_proto(&key_bytes);
let hrp = Hrp::parse(data.network.hrp()).expect("static HRP is valid");
let addr = bech32::encode::<Bech32m>(hrp, &proto)?;
Ok(addr)
}
pub fn decode_spark_address(addr: &str) -> Result<SparkAddressData, SparkAddressError> {
if addr.len() > 90 {
return Err(SparkAddressError::InvalidLength);
}
let has_upper = addr.bytes().any(|b| b.is_ascii_uppercase());
let has_lower = addr.bytes().any(|b| b.is_ascii_lowercase());
if has_upper && has_lower {
return Err(SparkAddressError::MixedCase);
}
let (hrp, proto) = bech32::decode(addr)?;
let hrp_str = hrp.to_string();
if hrp_str.bytes().any(|b| b.is_ascii_uppercase()) {
return Err(SparkAddressError::MixedCase);
}
let reencoded = bech32::encode::<Bech32m>(hrp, &proto)?;
if reencoded.to_lowercase() != addr.to_lowercase() {
return Err(SparkAddressError::InvalidVariant);
}
let network = Network::from_hrp(&hrp_str)
.ok_or_else(|| SparkAddressError::UnknownPrefix(hrp_str.clone()))?;
let key = decode_proto(&proto)?;
if key.len() != 33 {
return Err(SparkAddressError::WrongKeyLength(key.len()));
}
let hex_key = bytes_to_hex(key);
#[cfg(feature = "validate-secp256k1")]
validate_pubkey(&hex_key)?;
Ok(SparkAddressData {
identity_public_key: hex_key,
network,
})
}
#[cfg(feature = "validate-secp256k1")]
fn validate_pubkey(hex_str: &str) -> Result<(), SparkAddressError> {
use secp256k1::PublicKey;
let bytes = hex_to_bytes(hex_str)?;
PublicKey::from_slice(&bytes).map_err(|_| SparkAddressError::InvalidSecp256k1)?;
Ok(())
}
#[cfg(not(feature = "validate-secp256k1"))]
fn _validate_pubkey(_: &str) {}
#[cfg(test)]
mod tests {
use super::*;
const PUBKEY: &str = "0279be667ef9dcbbac55a06295ce870b07029bfcdb2dce28d959f2815b16f81798";
const MAINNET_ADDRESS: &str =
"sp1pgssy7d7vel0nh9m4326qc54e6rskpczn07dktww9rv4nu5ptvt0s9ucez8h3s";
const REGTEST_ADDRESS: &str =
"sprt1pgssy7d7vel0nh9m4326qc54e6rskpczn07dktww9rv4nu5ptvt0s9ucd5rgc0";
#[test]
fn mainnet_round_trip() {
let data = SparkAddressData {
identity_public_key: PUBKEY.into(),
network: Network::Mainnet,
};
let encoded = encode_spark_address(&data).unwrap();
assert_eq!(encoded, MAINNET_ADDRESS);
let decoded = decode_spark_address(&encoded).unwrap();
assert_eq!(decoded, data);
let decoded = decode_spark_address(MAINNET_ADDRESS).unwrap();
assert_eq!(decoded.network, Network::Mainnet);
assert_eq!(decoded.identity_public_key, PUBKEY);
}
#[test]
fn regtest_round_trip() {
let data = SparkAddressData {
identity_public_key: PUBKEY.into(),
network: Network::Regtest,
};
let encoded = encode_spark_address(&data).unwrap();
assert_eq!(encoded, REGTEST_ADDRESS);
let decoded = decode_spark_address(&encoded).unwrap();
assert_eq!(decoded, data);
let decoded = decode_spark_address(MAINNET_ADDRESS).unwrap();
assert_eq!(decoded.network, Network::Mainnet);
assert_eq!(decoded.identity_public_key, PUBKEY);
}
}