use bitcoin::bip32::{ChildNumber, DerivationPath, Xpub};
use bitcoin::secp256k1::{Secp256k1, VerifyOnly};
use bitcoin::{Address, KnownHrp, NetworkKind};
#[derive(Copy, Clone, Debug, PartialEq, Eq, clap::ValueEnum)]
#[clap(rename_all = "kebab-case")]
pub enum AddressType {
P2pkh,
P2shP2wpkh,
P2wpkh,
P2tr,
}
impl AddressType {
pub fn label(self) -> &'static str {
match self {
AddressType::P2pkh => "p2pkh",
AddressType::P2shP2wpkh => "p2sh-p2wpkh",
AddressType::P2wpkh => "p2wpkh",
AddressType::P2tr => "p2tr",
}
}
}
#[derive(Copy, Clone, Debug, PartialEq, Eq, clap::ValueEnum)]
#[clap(rename_all = "lower")]
pub enum CliNetwork {
Mainnet,
Testnet,
Signet,
Regtest,
}
impl CliNetwork {
pub fn network_kind(self) -> NetworkKind {
match self {
CliNetwork::Mainnet => NetworkKind::Main,
_ => NetworkKind::Test,
}
}
pub fn known_hrp(self) -> KnownHrp {
match self {
CliNetwork::Mainnet => KnownHrp::Mainnet,
CliNetwork::Testnet | CliNetwork::Signet => KnownHrp::Testnets,
CliNetwork::Regtest => KnownHrp::Regtest,
}
}
pub fn label(self) -> &'static str {
match self {
CliNetwork::Mainnet => "mainnet",
CliNetwork::Testnet => "testnet",
CliNetwork::Signet => "signet",
CliNetwork::Regtest => "regtest",
}
}
}
#[derive(Debug, PartialEq, Eq)]
pub enum AddressTypeInference {
Inferred(AddressType),
Multisig,
Unknown,
}
pub fn infer_address_type(origin_path: &DerivationPath) -> AddressTypeInference {
let comps: &[ChildNumber] = origin_path.as_ref();
let purpose = match comps.first() {
Some(ChildNumber::Hardened { index }) => *index,
_ => return AddressTypeInference::Unknown, };
if purpose == 48 || purpose == 87 {
return AddressTypeInference::Multisig;
}
if comps.len() != 3 {
return AddressTypeInference::Unknown; }
match purpose {
44 => AddressTypeInference::Inferred(AddressType::P2pkh),
49 => AddressTypeInference::Inferred(AddressType::P2shP2wpkh),
84 => AddressTypeInference::Inferred(AddressType::P2wpkh),
86 => AddressTypeInference::Inferred(AddressType::P2tr),
_ => AddressTypeInference::Unknown,
}
}
pub fn secp_verify() -> Secp256k1<VerifyOnly> {
Secp256k1::verification_only()
}
pub fn render_address(
secp: &Secp256k1<VerifyOnly>,
child: &Xpub,
ty: AddressType,
net: CliNetwork,
) -> String {
match ty {
AddressType::P2pkh => Address::p2pkh(child.to_pub(), net.network_kind()).to_string(),
AddressType::P2shP2wpkh => {
Address::p2shwpkh(&child.to_pub(), net.network_kind()).to_string()
}
AddressType::P2wpkh => Address::p2wpkh(&child.to_pub(), net.known_hrp()).to_string(),
AddressType::P2tr => {
Address::p2tr(secp, child.to_x_only_pub(), None, net.known_hrp()).to_string()
}
}
}
#[cfg(test)]
mod tests {
use super::*;
use std::str::FromStr;
const ACCT_84_XPUB: &str = "xpub6BmeGmRo4LosAcU21HDaGcvtaQ7GrqQcY48nBkE22qM6KVwQUjRJ1BGzk84SFVHgLcd61Vcnhr8petHexjjn5WbQ9PriVrRhphw4oCp2z6a";
fn p(s: &str) -> DerivationPath {
DerivationPath::from_str(s).unwrap()
}
#[test]
fn address_type_kebab_values() {
use clap::ValueEnum;
assert_eq!(
AddressType::P2shP2wpkh
.to_possible_value()
.unwrap()
.get_name(),
"p2sh-p2wpkh"
);
assert_eq!(
AddressType::P2wpkh.to_possible_value().unwrap().get_name(),
"p2wpkh"
);
assert_eq!(
AddressType::P2pkh.to_possible_value().unwrap().get_name(),
"p2pkh"
);
assert_eq!(
AddressType::P2tr.to_possible_value().unwrap().get_name(),
"p2tr"
);
}
#[test]
fn network_lower_values_and_hrp() {
use clap::ValueEnum;
assert_eq!(
CliNetwork::Mainnet.to_possible_value().unwrap().get_name(),
"mainnet"
);
assert_eq!(
CliNetwork::Regtest.to_possible_value().unwrap().get_name(),
"regtest"
);
assert_eq!(CliNetwork::Mainnet.known_hrp(), KnownHrp::Mainnet);
assert_eq!(CliNetwork::Regtest.known_hrp(), KnownHrp::Regtest);
assert_eq!(CliNetwork::Signet.known_hrp(), KnownHrp::Testnets);
assert_eq!(CliNetwork::Mainnet.network_kind(), NetworkKind::Main);
assert_eq!(CliNetwork::Testnet.network_kind(), NetworkKind::Test);
}
#[test]
fn infer_account_depth_only() {
use AddressTypeInference::*;
assert_eq!(
infer_address_type(&p("m/84'/0'/0'")),
Inferred(AddressType::P2wpkh)
);
assert_eq!(
infer_address_type(&p("m/44'/0'/0'")),
Inferred(AddressType::P2pkh)
);
assert_eq!(
infer_address_type(&p("m/49'/0'/0'")),
Inferred(AddressType::P2shP2wpkh)
);
assert_eq!(
infer_address_type(&p("m/86'/0'/0'")),
Inferred(AddressType::P2tr)
);
assert_eq!(infer_address_type(&p("m/48'/0'/0'/2'")), Multisig);
assert_eq!(infer_address_type(&p("m/87'/0'/0'")), Multisig);
assert_eq!(infer_address_type(&p("m/84'/0'/0'/0/5")), Unknown);
assert_eq!(infer_address_type(&p("m")), Unknown);
assert_eq!(infer_address_type(&p("m/0/0")), Unknown);
assert_eq!(infer_address_type(&p("m/9999'/1234'/56'/7'")), Unknown);
}
#[test]
fn render_all_four_types() {
let secp = secp_verify();
let xpub = Xpub::from_str(ACCT_84_XPUB).unwrap();
let child = xpub.derive_pub(&secp, &p("m/0/0")).unwrap();
assert!(
render_address(&secp, &child, AddressType::P2wpkh, CliNetwork::Mainnet)
.starts_with("bc1q")
);
assert!(
render_address(&secp, &child, AddressType::P2tr, CliNetwork::Mainnet)
.starts_with("bc1p")
);
assert!(
render_address(&secp, &child, AddressType::P2pkh, CliNetwork::Mainnet).starts_with('1')
);
assert!(
render_address(&secp, &child, AddressType::P2shP2wpkh, CliNetwork::Mainnet)
.starts_with('3')
);
assert!(
render_address(&secp, &child, AddressType::P2wpkh, CliNetwork::Regtest)
.starts_with("bcrt1q")
);
assert!(
render_address(&secp, &child, AddressType::P2wpkh, CliNetwork::Testnet)
.starts_with("tb1q")
);
}
}