1use crate::error::SignerError;
4
5pub fn cltv_script(locktime: u32, pubkey_hash: &[u8; 20]) -> Vec<u8> {
19 let mut script = Vec::with_capacity(30);
20
21 push_script_number(&mut script, locktime as i64);
23
24 script.push(0xB1); script.push(0x75); script.push(0x76); script.push(0xA9); script.push(0x14); script.extend_from_slice(pubkey_hash);
30 script.push(0x88); script.push(0xAC); script
33}
34
35pub fn csv_script(sequence: u32, pubkey_hash: &[u8; 20]) -> Vec<u8> {
45 let mut script = Vec::with_capacity(30);
46
47 push_script_number(&mut script, sequence as i64);
48
49 script.push(0xB2); script.push(0x75); script.push(0x76); script.push(0xA9); script.push(0x14); script.extend_from_slice(pubkey_hash);
55 script.push(0x88); script.push(0xAC); script
58}
59
60#[must_use]
64pub fn is_block_height_locktime(locktime: u32) -> bool {
65 locktime < 500_000_000
66}
67
68pub fn htlc_script(
91 payment_hash: &[u8; 32],
92 receiver_pubkey_hash: &[u8; 20],
93 sender_pubkey_hash: &[u8; 20],
94 timeout: u32,
95) -> Vec<u8> {
96 let mut script = Vec::with_capacity(100);
97
98 script.push(0x63); script.push(0xA8); script.push(0x20); script.extend_from_slice(payment_hash);
104 script.push(0x88); script.push(0x76); script.push(0xA9); script.push(0x14); script.extend_from_slice(receiver_pubkey_hash);
110 script.push(0x88); script.push(0xAC); script.push(0x67); push_script_number(&mut script, timeout as i64);
117 script.push(0xB1); script.push(0x75); script.push(0x76); script.push(0xA9); script.push(0x14); script.extend_from_slice(sender_pubkey_hash);
124 script.push(0x88); script.push(0xAC); script.push(0x68); script
130}
131
132pub fn htlc_payment_hash(preimage: &[u8]) -> [u8; 32] {
134 use sha2::{Digest, Sha256};
135 let mut hasher = Sha256::new();
136 hasher.update(preimage);
137 let result = hasher.finalize();
138 let mut out = [0u8; 32];
139 out.copy_from_slice(&result);
140 out
141}
142
143pub fn htlc_claim_witness(
147 signature: &[u8],
148 pubkey: &[u8],
149 preimage: &[u8],
150 htlc_script: &[u8],
151) -> Vec<Vec<u8>> {
152 vec![
153 signature.to_vec(),
154 pubkey.to_vec(),
155 preimage.to_vec(),
156 vec![0x01], htlc_script.to_vec(),
158 ]
159}
160
161pub fn htlc_refund_witness(signature: &[u8], pubkey: &[u8], htlc_script: &[u8]) -> Vec<Vec<u8>> {
165 vec![
166 signature.to_vec(),
167 pubkey.to_vec(),
168 vec![], htlc_script.to_vec(),
170 ]
171}
172
173#[derive(Clone, Debug)]
179pub struct Utxo {
180 pub txid: [u8; 32],
182 pub vout: u32,
184 pub value: u64,
186 pub input_vsize: usize,
188}
189
190#[derive(Clone, Debug)]
192pub struct CoinSelectionResult {
193 pub selected: Vec<Utxo>,
195 pub total_value: u64,
197 pub estimated_fee: u64,
199 pub change: u64,
201}
202
203pub fn select_coins_srd(
215 utxos: &[Utxo],
216 target: u64,
217 fee_rate: u64,
218 base_tx_vsize: usize,
219 change_output_vsize: usize,
220) -> Result<CoinSelectionResult, SignerError> {
221 if utxos.is_empty() {
222 return Err(SignerError::ParseError("no UTXOs available".into()));
223 }
224
225 let mut sorted: Vec<&Utxo> = utxos.iter().collect();
227 sorted.sort_by(|a, b| b.value.cmp(&a.value));
228
229 let mut selected = Vec::new();
230 let mut total_value = 0u64;
231 let mut total_input_vsize = 0usize;
232
233 for utxo in &sorted {
234 selected.push((*utxo).clone());
235 total_value += utxo.value;
236 total_input_vsize += utxo.input_vsize;
237
238 let tx_vsize = base_tx_vsize + total_input_vsize + change_output_vsize;
239 let fee = fee_rate * tx_vsize as u64;
240
241 if total_value >= target + fee {
242 let change = total_value - target - fee;
243 return Ok(CoinSelectionResult {
244 selected,
245 total_value,
246 estimated_fee: fee,
247 change,
248 });
249 }
250 }
251
252 Err(SignerError::ParseError(
253 "insufficient funds: not enough UTXOs to cover target + fees".into(),
254 ))
255}
256
257pub fn select_coins_bnb(
270 utxos: &[Utxo],
271 target: u64,
272 fee_rate: u64,
273 base_tx_vsize: usize,
274 change_output_vsize: usize,
275 cost_of_change: u64,
276) -> Result<CoinSelectionResult, SignerError> {
277 if utxos.is_empty() {
278 return Err(SignerError::ParseError("no UTXOs available".into()));
279 }
280
281 let mut sorted: Vec<&Utxo> = utxos.iter().collect();
283 sorted.sort_by(|a, b| {
284 let ev_a = a.value.saturating_sub(fee_rate * a.input_vsize as u64);
285 let ev_b = b.value.saturating_sub(fee_rate * b.input_vsize as u64);
286 ev_b.cmp(&ev_a)
287 });
288
289 let max_iterations = 100_000;
290 let mut best: Option<Vec<usize>> = None;
291 let mut best_waste = i64::MAX;
292
293 let mut stack: Vec<(usize, Vec<usize>, u64, usize)> = vec![(0, vec![], 0, 0)];
295 let mut iterations = 0;
296
297 while let Some((idx, selected_indices, total, total_vsize)) = stack.pop() {
298 iterations += 1;
299 if iterations > max_iterations {
300 break;
301 }
302
303 let fee = fee_rate * (base_tx_vsize + total_vsize) as u64;
304 let needed = target + fee;
305
306 if total >= needed && total <= needed + cost_of_change {
307 let waste = (total as i64) - (needed as i64);
309 if waste < best_waste {
310 best_waste = waste;
311 best = Some(selected_indices.clone());
312 }
313 continue;
314 }
315
316 if idx >= sorted.len() || total > needed + cost_of_change {
317 continue;
318 }
319
320 let mut with = selected_indices.clone();
322 with.push(idx);
323 stack.push((
324 idx + 1,
325 with,
326 total + sorted[idx].value,
327 total_vsize + sorted[idx].input_vsize,
328 ));
329
330 stack.push((idx + 1, selected_indices, total, total_vsize));
332 }
333
334 if let Some(indices) = best {
335 let selected: Vec<Utxo> = indices.iter().map(|i| sorted[*i].clone()).collect();
336 let total_value: u64 = selected.iter().map(|u| u.value).sum();
337 let total_input_vsize: usize = selected.iter().map(|u| u.input_vsize).sum();
338 let fee = fee_rate * (base_tx_vsize + total_input_vsize) as u64;
339 let change = total_value.saturating_sub(target + fee);
340
341 Ok(CoinSelectionResult {
342 selected,
343 total_value,
344 estimated_fee: fee,
345 change,
346 })
347 } else {
348 select_coins_srd(utxos, target, fee_rate, base_tx_vsize, change_output_vsize)
350 }
351}
352
353fn push_script_number(script: &mut Vec<u8>, n: i64) {
357 if n == 0 {
358 script.push(0x00); return;
360 }
361 if (1..=16).contains(&n) {
362 script.push(0x50 + n as u8); return;
364 }
365
366 let negative = n < 0;
368 let mut abs_n = if negative { (-n) as u64 } else { n as u64 };
369 let mut bytes = Vec::new();
370
371 while abs_n > 0 {
372 bytes.push((abs_n & 0xFF) as u8);
373 abs_n >>= 8;
374 }
375
376 if bytes.last().is_some_and(|b| b & 0x80 != 0) {
378 bytes.push(if negative { 0x80 } else { 0x00 });
379 } else if negative {
380 let last = bytes.len() - 1;
381 bytes[last] |= 0x80;
382 }
383
384 script.push(bytes.len() as u8); script.extend_from_slice(&bytes);
386}
387
388#[cfg(test)]
393#[allow(clippy::unwrap_used, clippy::expect_used)]
394mod tests {
395 use super::*;
396
397 const PKH: [u8; 20] = [0xAB; 20];
398 const PKH2: [u8; 20] = [0xCD; 20];
399
400 #[test]
403 fn test_cltv_script_structure() {
404 let script = cltv_script(500_000, &PKH);
405 assert!(script.contains(&0xB1)); assert!(script.contains(&0xAC)); assert!(script.windows(20).any(|w| w == PKH));
409 }
410
411 #[test]
412 fn test_csv_script_structure() {
413 let script = csv_script(144, &PKH); assert!(script.contains(&0xB2)); assert!(script.contains(&0xAC)); }
417
418 #[test]
419 fn test_is_block_height_locktime() {
420 assert!(is_block_height_locktime(500_000));
421 assert!(is_block_height_locktime(0));
422 assert!(!is_block_height_locktime(500_000_000));
423 assert!(!is_block_height_locktime(1_700_000_000));
424 }
425
426 #[test]
429 fn test_htlc_script_structure() {
430 let preimage = b"secret preimage";
431 let hash = htlc_payment_hash(preimage);
432 let script = htlc_script(&hash, &PKH, &PKH2, 100_000);
433
434 assert!(script.contains(&0x63)); assert!(script.contains(&0x67)); assert!(script.contains(&0x68)); assert!(script.contains(&0xA8)); assert!(script.contains(&0xB1)); assert!(script.windows(20).any(|w| w == PKH));
441 assert!(script.windows(20).any(|w| w == PKH2));
442 }
443
444 #[test]
445 fn test_htlc_payment_hash_deterministic() {
446 let preimage = b"test preimage";
447 let h1 = htlc_payment_hash(preimage);
448 let h2 = htlc_payment_hash(preimage);
449 assert_eq!(h1, h2);
450 assert_ne!(h1, [0u8; 32]);
451 }
452
453 #[test]
454 fn test_htlc_payment_hash_known_vector() {
455 let hash = htlc_payment_hash(b"");
457 assert_eq!(
458 hex::encode(hash),
459 "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855"
460 );
461 }
462
463 #[test]
464 fn test_htlc_claim_witness() {
465 let sig = vec![0x30, 0x44]; let pk = vec![0x02; 33]; let preimage = b"secret";
468 let script = vec![0x63; 10]; let witness = htlc_claim_witness(&sig, &pk, preimage, &script);
470 assert_eq!(witness.len(), 5);
471 assert_eq!(witness[3], vec![0x01]); }
473
474 #[test]
475 fn test_htlc_refund_witness() {
476 let sig = vec![0x30, 0x44];
477 let pk = vec![0x02; 33];
478 let script = vec![0x63; 10];
479 let witness = htlc_refund_witness(&sig, &pk, &script);
480 assert_eq!(witness.len(), 4);
481 assert!(witness[2].is_empty()); }
483
484 fn make_utxos() -> Vec<Utxo> {
487 vec![
488 Utxo {
489 txid: [1; 32],
490 vout: 0,
491 value: 100_000,
492 input_vsize: 68,
493 },
494 Utxo {
495 txid: [2; 32],
496 vout: 0,
497 value: 50_000,
498 input_vsize: 68,
499 },
500 Utxo {
501 txid: [3; 32],
502 vout: 0,
503 value: 200_000,
504 input_vsize: 68,
505 },
506 Utxo {
507 txid: [4; 32],
508 vout: 0,
509 value: 30_000,
510 input_vsize: 68,
511 },
512 ]
513 }
514
515 #[test]
516 fn test_srd_selects_enough() {
517 let utxos = make_utxos();
518 let result = select_coins_srd(&utxos, 150_000, 10, 10, 34).unwrap();
519 assert!(result.total_value >= 150_000 + result.estimated_fee);
520 }
521
522 #[test]
523 fn test_srd_insufficient_funds() {
524 let utxos = make_utxos();
525 let result = select_coins_srd(&utxos, 1_000_000, 10, 10, 34);
526 assert!(result.is_err());
527 }
528
529 #[test]
530 fn test_srd_empty_utxos() {
531 let result = select_coins_srd(&[], 100, 10, 10, 34);
532 assert!(result.is_err());
533 }
534
535 #[test]
536 fn test_bnb_finds_exact_match() {
537 let utxos = vec![
538 Utxo {
539 txid: [1; 32],
540 vout: 0,
541 value: 100_000,
542 input_vsize: 68,
543 },
544 Utxo {
545 txid: [2; 32],
546 vout: 0,
547 value: 50_000,
548 input_vsize: 68,
549 },
550 Utxo {
551 txid: [3; 32],
552 vout: 0,
553 value: 25_000,
554 input_vsize: 68,
555 },
556 ];
557 let result = select_coins_bnb(&utxos, 49_000, 1, 10, 34, 500).unwrap();
559 assert!(result.total_value >= 49_000);
560 }
561
562 #[test]
563 fn test_bnb_falls_back_to_srd() {
564 let utxos = make_utxos();
565 let result = select_coins_bnb(&utxos, 150_000, 10, 10, 34, 546).unwrap();
567 assert!(result.total_value >= 150_000 + result.estimated_fee);
568 }
569
570 #[test]
573 fn test_push_script_number_small() {
574 let mut s = Vec::new();
575 push_script_number(&mut s, 0);
576 assert_eq!(s, vec![0x00]); let mut s = Vec::new();
579 push_script_number(&mut s, 1);
580 assert_eq!(s, vec![0x51]); let mut s = Vec::new();
583 push_script_number(&mut s, 16);
584 assert_eq!(s, vec![0x60]); }
586
587 #[test]
588 fn test_push_script_number_large() {
589 let mut s = Vec::new();
590 push_script_number(&mut s, 500_000);
591 assert_eq!(s[0], 3); assert_eq!(s[1..], [0x20, 0xA1, 0x07]);
594 }
595}