use bsv::primitives::private_key::PrivateKey;
use bsv::primitives::public_key::PublicKey;
use bsv::script::templates::p2pkh::P2PKH;
use bsv::script::templates::ScriptTemplateLock;
use bsv::wallet::key_deriver::KeyDeriver;
use bsv::wallet::types::{Counterparty, CounterpartyType, Protocol};
use crate::error::WalletResult;
pub const BRC29_UNLOCK_LENGTH: usize = 108;
pub const BRC29_PROTOCOL_ID: (u8, &str) = (2, "3241645161d8");
pub fn brc29_protocol() -> Protocol {
Protocol {
security_level: BRC29_PROTOCOL_ID.0,
protocol: BRC29_PROTOCOL_ID.1.to_string(),
}
}
#[derive(Debug, Clone)]
pub struct ScriptTemplateBRC29 {
pub derivation_prefix: String,
pub derivation_suffix: String,
}
impl ScriptTemplateBRC29 {
pub fn new(derivation_prefix: String, derivation_suffix: String) -> Self {
Self {
derivation_prefix,
derivation_suffix,
}
}
pub fn key_id(&self) -> String {
format!("{} {}", self.derivation_prefix, self.derivation_suffix)
}
pub fn lock(
&self,
locker_priv_key: &PrivateKey,
unlocker_pub_key: &PublicKey,
) -> WalletResult<Vec<u8>> {
let key_deriver = KeyDeriver::new(locker_priv_key.clone());
let counterparty = Counterparty {
counterparty_type: CounterpartyType::Other,
public_key: Some(unlocker_pub_key.clone()),
};
let derived_pub = key_deriver
.derive_public_key(&brc29_protocol(), &self.key_id(), &counterparty, false)
.map_err(|e| {
crate::error::WalletError::Internal(format!("BRC-29 lock derivation failed: {}", e))
})?;
let p2pkh = P2PKH::from_public_key_hash(Self::pubkey_to_hash(&derived_pub));
let locking_script = p2pkh.lock().map_err(|e| {
crate::error::WalletError::Internal(format!("P2PKH lock failed: {}", e))
})?;
Ok(locking_script.to_binary())
}
pub fn unlock(
&self,
unlocker_priv_key: &PrivateKey,
locker_pub_key: &PublicKey,
) -> WalletResult<P2PKH> {
let key_deriver = KeyDeriver::new(unlocker_priv_key.clone());
let counterparty = Counterparty {
counterparty_type: CounterpartyType::Other,
public_key: Some(locker_pub_key.clone()),
};
let derived_priv = key_deriver
.derive_private_key(&brc29_protocol(), &self.key_id(), &counterparty)
.map_err(|e| {
crate::error::WalletError::Internal(format!(
"BRC-29 unlock derivation failed: {}",
e
))
})?;
Ok(P2PKH::from_private_key(derived_priv))
}
fn pubkey_to_hash(pubkey: &PublicKey) -> [u8; 20] {
let hash_vec = pubkey.to_hash();
let mut hash = [0u8; 20];
hash.copy_from_slice(&hash_vec);
hash
}
}
#[cfg(test)]
mod tests {
use super::*;
fn test_keys() -> (PrivateKey, PublicKey, PrivateKey, PublicKey) {
let locker_priv = PrivateKey::from_hex("aa").unwrap();
let locker_pub = locker_priv.to_public_key();
let unlocker_priv = PrivateKey::from_hex("bb").unwrap();
let unlocker_pub = unlocker_priv.to_public_key();
(locker_priv, locker_pub, unlocker_priv, unlocker_pub)
}
#[test]
fn test_key_id_format() {
let tmpl = ScriptTemplateBRC29::new("prefix123".to_string(), "suffix456".to_string());
assert_eq!(tmpl.key_id(), "prefix123 suffix456");
}
#[test]
fn test_brc29_protocol_values() {
let proto = brc29_protocol();
assert_eq!(proto.security_level, 2);
assert_eq!(proto.protocol, "3241645161d8");
}
#[test]
fn test_brc29_unlock_length_constant() {
assert_eq!(BRC29_UNLOCK_LENGTH, 108);
}
#[test]
fn test_lock_produces_valid_p2pkh_script() {
let (locker_priv, _locker_pub, _unlocker_priv, unlocker_pub) = test_keys();
let tmpl = ScriptTemplateBRC29::new("test_prefix".to_string(), "test_suffix".to_string());
let script = tmpl.lock(&locker_priv, &unlocker_pub).unwrap();
assert_eq!(script.len(), 25, "P2PKH locking script should be 25 bytes");
assert_eq!(script[0], 0x76, "should start with OP_DUP");
assert_eq!(script[1], 0xa9, "second byte should be OP_HASH160");
assert_eq!(script[23], 0x88, "should have OP_EQUALVERIFY");
assert_eq!(script[24], 0xac, "should end with OP_CHECKSIG");
}
#[test]
fn test_lock_is_deterministic() {
let (locker_priv, _locker_pub, _unlocker_priv, unlocker_pub) = test_keys();
let tmpl = ScriptTemplateBRC29::new("det_prefix".to_string(), "det_suffix".to_string());
let script1 = tmpl.lock(&locker_priv, &unlocker_pub).unwrap();
let script2 = tmpl.lock(&locker_priv, &unlocker_pub).unwrap();
assert_eq!(script1, script2, "lock should be deterministic");
}
#[test]
fn test_unlock_produces_p2pkh_with_private_key() {
let (_locker_priv, locker_pub, unlocker_priv, _unlocker_pub) = test_keys();
let tmpl = ScriptTemplateBRC29::new("test_prefix".to_string(), "test_suffix".to_string());
let p2pkh = tmpl.unlock(&unlocker_priv, &locker_pub).unwrap();
assert!(
p2pkh.private_key.is_some(),
"unlock should produce P2PKH with private key"
);
}
#[test]
fn test_lock_unlock_key_correspondence() {
let (locker_priv, locker_pub, unlocker_priv, unlocker_pub) = test_keys();
let tmpl = ScriptTemplateBRC29::new("test_prefix".to_string(), "test_suffix".to_string());
let lock_script = tmpl.lock(&locker_priv, &unlocker_pub).unwrap();
let p2pkh = tmpl.unlock(&unlocker_priv, &locker_pub).unwrap();
let lock_hash = &lock_script[3..23];
let unlock_hash = p2pkh.public_key_hash.unwrap();
assert_eq!(
lock_hash,
&unlock_hash[..],
"lock and unlock should derive corresponding keys"
);
}
#[test]
fn test_self_payment_lock_unlock_correspondence() {
let priv_key = PrivateKey::from_hex("aa").unwrap();
let pub_key = priv_key.to_public_key();
let tmpl = ScriptTemplateBRC29::new(
"VOP/NBBNR3V3DeJv40Qmsg==".to_string(),
"gN0oZKrb2L03fUnl1hJOiA==".to_string(),
);
let lock_script = tmpl.lock(&priv_key, &pub_key).unwrap();
let p2pkh = tmpl.unlock(&priv_key, &pub_key).unwrap();
let lock_hash = &lock_script[3..23];
let unlock_hash = p2pkh.public_key_hash.unwrap();
assert_eq!(
lock_hash,
&unlock_hash[..],
"self-payment: lock and unlock should derive corresponding keys"
);
}
}