1use crate::primitives::hash::hash256;
8use crate::primitives::utils::{from_hex, to_hex};
9use crate::script::error::ScriptError;
10use crate::script::script::Script;
11
12pub const BIP276_PREFIX: &str = "bitcoin-script";
14
15pub fn encode_bip276(
21 prefix: &str,
22 version: u8,
23 network: u8,
24 data: &[u8],
25) -> Result<String, ScriptError> {
26 if version == 0 {
27 return Err(ScriptError::InvalidFormat(
28 "BIP276 version must be > 0".to_string(),
29 ));
30 }
31 if network == 0 {
32 return Err(ScriptError::InvalidFormat(
33 "BIP276 network must be > 0".to_string(),
34 ));
35 }
36
37 let data_hex = to_hex(data);
38 let payload = format!("{}:{:02x}{:02x}{}", prefix, version, network, data_hex);
39
40 let checksum = hash256(payload.as_bytes());
41 let checksum_hex = to_hex(&checksum[..4]);
42
43 Ok(format!("{}{}", payload, checksum_hex))
44}
45
46pub fn decode_bip276(encoded: &str) -> Result<(String, u8, u8, Vec<u8>), ScriptError> {
51 let colon_pos = encoded
52 .find(':')
53 .ok_or_else(|| ScriptError::InvalidFormat("BIP276: missing ':' separator".to_string()))?;
54
55 let prefix = &encoded[..colon_pos];
56 let rest = &encoded[colon_pos + 1..];
57
58 if rest.len() < 12 {
60 return Err(ScriptError::InvalidFormat(
62 "BIP276: encoded data too short".to_string(),
63 ));
64 }
65
66 let version = u8::from_str_radix(&rest[..2], 16)
67 .map_err(|_| ScriptError::InvalidFormat("BIP276: invalid version hex".to_string()))?;
68 let network = u8::from_str_radix(&rest[2..4], 16)
69 .map_err(|_| ScriptError::InvalidFormat("BIP276: invalid network hex".to_string()))?;
70
71 let checksum_hex = &encoded[encoded.len() - 8..];
73 let payload = &encoded[..encoded.len() - 8];
74
75 let expected_checksum = hash256(payload.as_bytes());
77 let expected_hex = to_hex(&expected_checksum[..4]);
78 if checksum_hex != expected_hex {
79 return Err(ScriptError::InvalidFormat(
80 "BIP276: checksum mismatch".to_string(),
81 ));
82 }
83
84 let data_hex = &rest[4..rest.len() - 8];
86 let data = if data_hex.is_empty() {
87 Vec::new()
88 } else {
89 from_hex(data_hex)
90 .map_err(|e| ScriptError::InvalidFormat(format!("BIP276: invalid data hex: {}", e)))?
91 };
92
93 Ok((prefix.to_string(), version, network, data))
94}
95
96pub fn encode_script_bip276(script: &Script, network: u8) -> Result<String, ScriptError> {
98 encode_bip276(BIP276_PREFIX, 1, network, &script.to_binary())
99}
100
101pub fn decode_script_bip276(encoded: &str) -> Result<Script, ScriptError> {
103 let (prefix, _version, _network, data) = decode_bip276(encoded)?;
104 if prefix != BIP276_PREFIX {
105 return Err(ScriptError::InvalidFormat(format!(
106 "BIP276: expected prefix '{}', got '{}'",
107 BIP276_PREFIX, prefix
108 )));
109 }
110 Ok(Script::from_binary(&data))
111}
112
113#[cfg(test)]
114mod tests {
115 use super::*;
116
117 #[test]
118 fn test_encode_decode_roundtrip() {
119 let data = vec![0x76, 0xa9, 0x14];
120 let encoded = encode_bip276("bitcoin-script", 1, 1, &data).unwrap();
121
122 let (prefix, version, network, decoded_data) = decode_bip276(&encoded).unwrap();
123 assert_eq!(prefix, "bitcoin-script");
124 assert_eq!(version, 1);
125 assert_eq!(network, 1);
126 assert_eq!(decoded_data, data);
127 }
128
129 #[test]
130 fn test_encode_format() {
131 let data = vec![0xab, 0xcd];
132 let encoded = encode_bip276("bitcoin-script", 1, 2, &data).unwrap();
133
134 assert!(encoded.starts_with("bitcoin-script:"));
136 let rest = &encoded["bitcoin-script:".len()..];
138 assert!(rest.starts_with("0102abcd"));
139 assert_eq!(rest.len(), 16);
141 }
142
143 #[test]
144 fn test_invalid_checksum() {
145 let data = vec![0x76, 0xa9];
146 let mut encoded = encode_bip276("bitcoin-script", 1, 1, &data).unwrap();
147
148 let len = encoded.len();
150 let last = encoded.chars().last().unwrap();
151 encoded.truncate(len - 1);
152 encoded.push(if last == '0' { '1' } else { '0' });
153
154 let result = decode_bip276(&encoded);
155 assert!(result.is_err());
156 }
157
158 #[test]
159 fn test_invalid_version_zero() {
160 let result = encode_bip276("bitcoin-script", 0, 1, &[0x01]);
161 assert!(result.is_err());
162 }
163
164 #[test]
165 fn test_invalid_network_zero() {
166 let result = encode_bip276("bitcoin-script", 1, 0, &[0x01]);
167 assert!(result.is_err());
168 }
169
170 #[test]
171 fn test_script_bip276_roundtrip() {
172 let script = Script::from_binary(&[0x76, 0xa9, 0x14, 0xab, 0xab]);
174 let encoded = encode_script_bip276(&script, 1).unwrap();
175 let decoded = decode_script_bip276(&encoded).unwrap();
176 assert_eq!(decoded.to_binary(), script.to_binary());
177 }
178
179 #[test]
180 fn test_decode_wrong_prefix() {
181 let encoded = encode_bip276("custom-prefix", 1, 1, &[0x01]).unwrap();
183 let result = decode_script_bip276(&encoded);
184 assert!(result.is_err());
185 }
186
187 #[test]
188 fn test_empty_data() {
189 let encoded = encode_bip276("bitcoin-script", 1, 1, &[]).unwrap();
190 let (_, _, _, data) = decode_bip276(&encoded).unwrap();
191 assert!(data.is_empty());
192 }
193}