1use crate::primitives::{from_hex, sha256d, to_hex};
35use crate::{Error, Result};
36
37pub const BIP276_PREFIX: &str = "bitcoin-script";
39
40pub const NETWORK_MAINNET: u8 = 1;
42
43pub const NETWORK_TESTNET: u8 = 2;
45
46pub fn encode_bip276(network: u8, script_type: u8, script: &[u8]) -> String {
72 let payload = format!(
73 "{}:{:02x}{:02x}{}",
74 BIP276_PREFIX,
75 network,
76 script_type,
77 to_hex(script)
78 );
79 let checksum = sha256d(payload.as_bytes());
80 let checksum_hex = to_hex(&checksum[..4]);
81 format!("{}{}", payload, checksum_hex)
82}
83
84pub fn decode_bip276(encoded: &str) -> Result<(u8, u8, Vec<u8>)> {
117 let prefix_with_colon = format!("{}:", BIP276_PREFIX);
119 if !encoded.starts_with(&prefix_with_colon) {
120 return Err(Error::Bip276Error(format!(
121 "invalid prefix: expected '{}'",
122 BIP276_PREFIX
123 )));
124 }
125
126 let after_prefix = &encoded[prefix_with_colon.len()..];
127
128 if after_prefix.len() < 12 {
130 return Err(Error::Bip276Error("input too short".to_string()));
131 }
132
133 let network_hex = &after_prefix[..2];
135 let network_bytes = from_hex(network_hex)
136 .map_err(|_| Error::Bip276Error(format!("invalid network hex: '{}'", network_hex)))?;
137 let network = network_bytes[0];
138
139 let script_type_hex = &after_prefix[2..4];
141 let script_type_bytes = from_hex(script_type_hex).map_err(|_| {
142 Error::Bip276Error(format!("invalid script type hex: '{}'", script_type_hex))
143 })?;
144 let script_type = script_type_bytes[0];
145
146 let data_and_checksum = &after_prefix[4..];
148 if data_and_checksum.len() < 8 {
149 return Err(Error::Bip276Error(
150 "input too short for checksum".to_string(),
151 ));
152 }
153
154 let script_hex = &data_and_checksum[..data_and_checksum.len() - 8];
155 let provided_checksum = &data_and_checksum[data_and_checksum.len() - 8..];
156
157 let script_data = from_hex(script_hex)
159 .map_err(|_| Error::Bip276Error(format!("invalid script hex: '{}'", script_hex)))?;
160
161 let payload = &encoded[..encoded.len() - 8];
163 let checksum = sha256d(payload.as_bytes());
164 let expected_checksum = to_hex(&checksum[..4]);
165
166 if provided_checksum != expected_checksum {
167 return Err(Error::Bip276Error("invalid checksum".to_string()));
168 }
169
170 Ok((network, script_type, script_data))
171}
172
173#[cfg(test)]
174mod tests {
175 use super::*;
176
177 #[test]
178 fn test_encode_mainnet() {
179 let encoded = encode_bip276(NETWORK_MAINNET, 1, b"fake script");
180 assert_eq!(encoded, "bitcoin-script:010166616b65207363726970746f0cd86a");
181 }
182
183 #[test]
184 fn test_encode_testnet() {
185 let encoded = encode_bip276(NETWORK_TESTNET, 1, b"fake script");
186 assert_eq!(encoded, "bitcoin-script:020166616b65207363726970742577a444");
187 }
188
189 #[test]
190 fn test_decode_valid() {
191 let (network, script_type, data) =
192 decode_bip276("bitcoin-script:010166616b65207363726970746f0cd86a").unwrap();
193 assert_eq!(network, NETWORK_MAINNET);
194 assert_eq!(script_type, 1);
195 assert_eq!(data, b"fake script");
196 }
197
198 #[test]
199 fn test_roundtrip() {
200 let original_data = b"hello world script data";
201 let encoded = encode_bip276(NETWORK_MAINNET, 1, original_data);
202 let (network, script_type, data) = decode_bip276(&encoded).unwrap();
203 assert_eq!(network, NETWORK_MAINNET);
204 assert_eq!(script_type, 1);
205 assert_eq!(data, original_data);
206 }
207
208 #[test]
209 fn test_roundtrip_testnet() {
210 let original_data = b"\x76\xa9\x14";
211 let encoded = encode_bip276(NETWORK_TESTNET, 2, original_data);
212 let (network, script_type, data) = decode_bip276(&encoded).unwrap();
213 assert_eq!(network, NETWORK_TESTNET);
214 assert_eq!(script_type, 2);
215 assert_eq!(data, original_data);
216 }
217
218 #[test]
219 fn test_decode_invalid_prefix() {
220 let result = decode_bip276("invalid-prefix:010166616b65207363726970746f0cd86a");
221 assert!(result.is_err());
222 match result {
223 Err(Error::Bip276Error(msg)) => {
224 assert!(msg.contains("invalid prefix"));
225 }
226 _ => panic!("expected Bip276Error"),
227 }
228 }
229
230 #[test]
231 fn test_decode_invalid_checksum() {
232 let result = decode_bip276("bitcoin-script:010166616b65207363726970746f0cd8");
234 assert!(result.is_err());
235 }
236
237 #[test]
238 fn test_decode_too_short() {
239 let result = decode_bip276("bitcoin-script:01");
240 assert!(result.is_err());
241 }
242
243 #[test]
244 fn test_decode_empty_script() {
245 let encoded = encode_bip276(NETWORK_MAINNET, 1, b"");
247 let (network, script_type, data) = decode_bip276(&encoded).unwrap();
248 assert_eq!(network, NETWORK_MAINNET);
249 assert_eq!(script_type, 1);
250 assert!(data.is_empty());
251 }
252
253 #[test]
254 fn test_roundtrip_various_data() {
255 let data: Vec<u8> = (0..=255).collect();
257 let encoded = encode_bip276(NETWORK_MAINNET, 1, &data);
258 let (network, script_type, decoded) = decode_bip276(&encoded).unwrap();
259 assert_eq!(network, NETWORK_MAINNET);
260 assert_eq!(script_type, 1);
261 assert_eq!(decoded, data);
262 }
263}