use crate::primitives::hash::hash256;
use crate::primitives::utils::{from_hex, to_hex};
use crate::script::error::ScriptError;
use crate::script::script::Script;
pub const BIP276_PREFIX: &str = "bitcoin-script";
pub fn encode_bip276(
prefix: &str,
version: u8,
network: u8,
data: &[u8],
) -> Result<String, ScriptError> {
if version == 0 {
return Err(ScriptError::InvalidFormat(
"BIP276 version must be > 0".to_string(),
));
}
if network == 0 {
return Err(ScriptError::InvalidFormat(
"BIP276 network must be > 0".to_string(),
));
}
let data_hex = to_hex(data);
let payload = format!("{}:{:02x}{:02x}{}", prefix, version, network, data_hex);
let checksum = hash256(payload.as_bytes());
let checksum_hex = to_hex(&checksum[..4]);
Ok(format!("{}{}", payload, checksum_hex))
}
pub fn decode_bip276(encoded: &str) -> Result<(String, u8, u8, Vec<u8>), ScriptError> {
let colon_pos = encoded
.find(':')
.ok_or_else(|| ScriptError::InvalidFormat("BIP276: missing ':' separator".to_string()))?;
let prefix = &encoded[..colon_pos];
let rest = &encoded[colon_pos + 1..];
if rest.len() < 12 {
return Err(ScriptError::InvalidFormat(
"BIP276: encoded data too short".to_string(),
));
}
let version = u8::from_str_radix(&rest[..2], 16)
.map_err(|_| ScriptError::InvalidFormat("BIP276: invalid version hex".to_string()))?;
let network = u8::from_str_radix(&rest[2..4], 16)
.map_err(|_| ScriptError::InvalidFormat("BIP276: invalid network hex".to_string()))?;
let checksum_hex = &encoded[encoded.len() - 8..];
let payload = &encoded[..encoded.len() - 8];
let expected_checksum = hash256(payload.as_bytes());
let expected_hex = to_hex(&expected_checksum[..4]);
if checksum_hex != expected_hex {
return Err(ScriptError::InvalidFormat(
"BIP276: checksum mismatch".to_string(),
));
}
let data_hex = &rest[4..rest.len() - 8];
let data = if data_hex.is_empty() {
Vec::new()
} else {
from_hex(data_hex)
.map_err(|e| ScriptError::InvalidFormat(format!("BIP276: invalid data hex: {}", e)))?
};
Ok((prefix.to_string(), version, network, data))
}
pub fn encode_script_bip276(script: &Script, network: u8) -> Result<String, ScriptError> {
encode_bip276(BIP276_PREFIX, 1, network, &script.to_binary())
}
pub fn decode_script_bip276(encoded: &str) -> Result<Script, ScriptError> {
let (prefix, _version, _network, data) = decode_bip276(encoded)?;
if prefix != BIP276_PREFIX {
return Err(ScriptError::InvalidFormat(format!(
"BIP276: expected prefix '{}', got '{}'",
BIP276_PREFIX, prefix
)));
}
Ok(Script::from_binary(&data))
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_encode_decode_roundtrip() {
let data = vec![0x76, 0xa9, 0x14];
let encoded = encode_bip276("bitcoin-script", 1, 1, &data).unwrap();
let (prefix, version, network, decoded_data) = decode_bip276(&encoded).unwrap();
assert_eq!(prefix, "bitcoin-script");
assert_eq!(version, 1);
assert_eq!(network, 1);
assert_eq!(decoded_data, data);
}
#[test]
fn test_encode_format() {
let data = vec![0xab, 0xcd];
let encoded = encode_bip276("bitcoin-script", 1, 2, &data).unwrap();
assert!(encoded.starts_with("bitcoin-script:"));
let rest = &encoded["bitcoin-script:".len()..];
assert!(rest.starts_with("0102abcd"));
assert_eq!(rest.len(), 16);
}
#[test]
fn test_invalid_checksum() {
let data = vec![0x76, 0xa9];
let mut encoded = encode_bip276("bitcoin-script", 1, 1, &data).unwrap();
let len = encoded.len();
let last = encoded.chars().last().unwrap();
encoded.truncate(len - 1);
encoded.push(if last == '0' { '1' } else { '0' });
let result = decode_bip276(&encoded);
assert!(result.is_err());
}
#[test]
fn test_invalid_version_zero() {
let result = encode_bip276("bitcoin-script", 0, 1, &[0x01]);
assert!(result.is_err());
}
#[test]
fn test_invalid_network_zero() {
let result = encode_bip276("bitcoin-script", 1, 0, &[0x01]);
assert!(result.is_err());
}
#[test]
fn test_script_bip276_roundtrip() {
let script = Script::from_binary(&[0x76, 0xa9, 0x14, 0xab, 0xab]);
let encoded = encode_script_bip276(&script, 1).unwrap();
let decoded = decode_script_bip276(&encoded).unwrap();
assert_eq!(decoded.to_binary(), script.to_binary());
}
#[test]
fn test_decode_wrong_prefix() {
let encoded = encode_bip276("custom-prefix", 1, 1, &[0x01]).unwrap();
let result = decode_script_bip276(&encoded);
assert!(result.is_err());
}
#[test]
fn test_empty_data() {
let encoded = encode_bip276("bitcoin-script", 1, 1, &[]).unwrap();
let (_, _, _, data) = decode_bip276(&encoded).unwrap();
assert!(data.is_empty());
}
}