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!("{prefix}:{version:02x}{network:02x}{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 '{BIP276_PREFIX}', got '{prefix}'"
107 )));
108 }
109 Ok(Script::from_binary(&data))
110}
111
112#[cfg(test)]
113mod tests {
114 use super::*;
115
116 #[test]
117 fn test_encode_decode_roundtrip() {
118 let data = vec![0x76, 0xa9, 0x14];
119 let encoded = encode_bip276("bitcoin-script", 1, 1, &data).unwrap();
120
121 let (prefix, version, network, decoded_data) = decode_bip276(&encoded).unwrap();
122 assert_eq!(prefix, "bitcoin-script");
123 assert_eq!(version, 1);
124 assert_eq!(network, 1);
125 assert_eq!(decoded_data, data);
126 }
127
128 #[test]
129 fn test_encode_format() {
130 let data = vec![0xab, 0xcd];
131 let encoded = encode_bip276("bitcoin-script", 1, 2, &data).unwrap();
132
133 assert!(encoded.starts_with("bitcoin-script:"));
135 let rest = &encoded["bitcoin-script:".len()..];
137 assert!(rest.starts_with("0102abcd"));
138 assert_eq!(rest.len(), 16);
140 }
141
142 #[test]
143 fn test_invalid_checksum() {
144 let data = vec![0x76, 0xa9];
145 let mut encoded = encode_bip276("bitcoin-script", 1, 1, &data).unwrap();
146
147 let len = encoded.len();
149 let last = encoded.chars().last().unwrap();
150 encoded.truncate(len - 1);
151 encoded.push(if last == '0' { '1' } else { '0' });
152
153 let result = decode_bip276(&encoded);
154 assert!(result.is_err());
155 }
156
157 #[test]
158 fn test_invalid_version_zero() {
159 let result = encode_bip276("bitcoin-script", 0, 1, &[0x01]);
160 assert!(result.is_err());
161 }
162
163 #[test]
164 fn test_invalid_network_zero() {
165 let result = encode_bip276("bitcoin-script", 1, 0, &[0x01]);
166 assert!(result.is_err());
167 }
168
169 #[test]
170 fn test_script_bip276_roundtrip() {
171 let script = Script::from_binary(&[0x76, 0xa9, 0x14, 0xab, 0xab]);
173 let encoded = encode_script_bip276(&script, 1).unwrap();
174 let decoded = decode_script_bip276(&encoded).unwrap();
175 assert_eq!(decoded.to_binary(), script.to_binary());
176 }
177
178 #[test]
179 fn test_decode_wrong_prefix() {
180 let encoded = encode_bip276("custom-prefix", 1, 1, &[0x01]).unwrap();
182 let result = decode_script_bip276(&encoded);
183 assert!(result.is_err());
184 }
185
186 #[test]
187 fn test_empty_data() {
188 let encoded = encode_bip276("bitcoin-script", 1, 1, &[]).unwrap();
189 let (_, _, _, data) = decode_bip276(&encoded).unwrap();
190 assert!(data.is_empty());
191 }
192}