1
2use std::convert::Infallible;
3
4use bitcoin::Txid;
5use bitcoin::secp256k1::Keypair;
6
7use crate::{Vtxo, VtxoId, VtxoPolicy, ServerVtxo, Amount};
8use crate::arkoor::ArkoorDestination;
9use crate::arkoor::{
10 ArkoorBuilder, ArkoorConstructionError, state, ArkoorCosignResponse,
11 ArkoorSigningError, ArkoorCosignRequest,
12};
13use crate::vtxo::Full;
14
15
16pub struct ArkoorPackageBuilder<S: state::BuilderState> {
24 pub builders: Vec<ArkoorBuilder<S>>,
25}
26
27#[derive(Debug, Clone)]
28pub struct ArkoorPackageCosignRequest<V> {
29 pub requests: Vec<ArkoorCosignRequest<V>>
30}
31
32impl<V> ArkoorPackageCosignRequest<V> {
33 pub fn convert_vtxo<F, O>(self, mut f: F) -> ArkoorPackageCosignRequest<O>
34 where F: FnMut(V) -> O
35 {
36 ArkoorPackageCosignRequest {
37 requests: self.requests.into_iter().map(|r| {
38 ArkoorCosignRequest {
39 user_pub_nonces: r.user_pub_nonces,
40 input: f(r.input),
41 outputs: r.outputs,
42 isolated_outputs: r.isolated_outputs,
43 use_checkpoint: r.use_checkpoint,
44 attestation: r.attestation,
45 }
46 }).collect::<Vec<_>>(),
47 }
48 }
49
50 pub fn inputs(&self) -> impl Iterator<Item=&V> {
51 self.requests.iter()
52 .map(|r| Some(&r.input))
53 .flatten()
54 }
55
56 pub fn all_outputs(
57 &self,
58 ) -> impl Iterator<Item = &ArkoorDestination> + Clone {
59 self.requests.iter()
60 .map(|r| r.all_outputs())
61 .flatten()
62 }
63}
64
65#[derive(Debug, Clone, PartialEq, Eq, Hash, thiserror::Error)]
66#[error("VTXO id mismatch. Expected {expected}, got {got}")]
67pub struct InputMismatchError {
68 expected: VtxoId,
69 got: VtxoId,
70}
71
72impl ArkoorPackageCosignRequest<VtxoId> {
73 pub fn set_vtxos(
74 self,
75 vtxos: impl IntoIterator<Item = Vtxo<Full>>,
76 ) -> Result<ArkoorPackageCosignRequest<Vtxo<Full>>, InputMismatchError> {
77 let package = ArkoorPackageCosignRequest {
78 requests: self.requests.into_iter().zip(vtxos).map(|(r, vtxo)| {
79 if r.input != vtxo.id() {
80 return Err(InputMismatchError {
81 expected: r.input,
82 got: vtxo.id(),
83 })
84 }
85
86 Ok(ArkoorCosignRequest {
87 input: vtxo,
88 user_pub_nonces: r.user_pub_nonces,
89 outputs: r.outputs,
90 isolated_outputs: r.isolated_outputs,
91 use_checkpoint: r.use_checkpoint,
92 attestation: r.attestation,
93 })
94 }).collect::<Result<Vec<_>, _>>()?,
95 };
96
97 Ok(package)
98 }
99}
100
101#[derive(Debug, Clone)]
102pub struct ArkoorPackageCosignResponse {
103 pub responses: Vec<ArkoorCosignResponse>
104}
105
106impl ArkoorPackageBuilder<state::Initial> {
107 fn allocate_outputs_to_inputs(
112 inputs: impl IntoIterator<Item = Vtxo<Full>>,
113 outputs: Vec<ArkoorDestination>,
114 ) -> Result<Vec<(Vtxo<Full>, Vec<ArkoorDestination>)>, ArkoorConstructionError> {
115 let total_output = outputs.iter().map(|r| r.total_amount).sum::<Amount>();
116 if outputs.is_empty() || total_output == Amount::ZERO {
117 return Err(ArkoorConstructionError::NoOutputs);
118 }
119
120 let mut allocations: Vec<(Vtxo<Full>, Vec<ArkoorDestination>)> = Vec::new();
121
122 let mut output_iter = outputs.into_iter();
123 let mut current_output = output_iter.next();
124 let mut current_output_remaining = current_output.as_ref()
125 .map(|o| o.total_amount).unwrap_or_default();
126
127 let mut total_input = Amount::ZERO;
128 'inputs:
129 for input in inputs {
130 total_input += input.amount();
131
132 let mut input_remaining = input.amount();
133 let mut input_allocation: Vec<ArkoorDestination> = Vec::new();
134
135 'outputs:
136 while let Some(ref output) = current_output {
137 let _: Infallible = if input_remaining == current_output_remaining {
138 input_allocation.push(ArkoorDestination {
140 total_amount: current_output_remaining,
141 policy: output.policy.clone(),
142 });
143
144 current_output = output_iter.next();
145 current_output_remaining = current_output.as_ref()
146 .map(|o| o.total_amount).unwrap_or_default();
147 allocations.push((input, input_allocation));
148 continue 'inputs;
149 } else if input_remaining > current_output_remaining {
150 input_allocation.push(ArkoorDestination {
152 total_amount: current_output_remaining,
153 policy: output.policy.clone(),
154 });
155
156 input_remaining -= current_output_remaining;
157
158 current_output = output_iter.next();
159 current_output_remaining = current_output.as_ref()
160 .map(|o| o.total_amount).unwrap_or_default();
161 continue 'outputs;
162 } else {
163 input_allocation.push(ArkoorDestination {
165 total_amount: input_remaining,
166 policy: output.policy.clone(),
167 });
168
169 current_output_remaining -= input_remaining;
170
171 allocations.push((input, input_allocation));
172 continue 'inputs;
173 };
174 }
175 }
176
177 if total_input != total_output {
178 return Err(ArkoorConstructionError::Unbalanced {
179 input: total_input,
180 output: total_output,
181 });
182 }
183
184 Ok(allocations)
185 }
186
187 pub fn new_with_checkpoints(
189 inputs: impl IntoIterator<Item = Vtxo<Full>>,
190 outputs: Vec<ArkoorDestination>,
191 ) -> Result<Self, ArkoorConstructionError> {
192 Self::new(inputs, outputs, true)
193 }
194
195 pub fn new_without_checkpoints(
197 inputs: impl IntoIterator<Item = Vtxo<Full>>,
198 outputs: Vec<ArkoorDestination>,
199 ) -> Result<Self, ArkoorConstructionError> {
200 Self::new(inputs, outputs, false)
201 }
202
203 pub fn new_single_output_with_checkpoints(
208 inputs: impl IntoIterator<Item = Vtxo<Full>>,
209 output: ArkoorDestination,
210 change_policy: VtxoPolicy,
211 ) -> Result<Self, ArkoorConstructionError> {
212 let inputs = inputs.into_iter().collect::<Vec<_>>();
214 let total_input = inputs.iter().map(|v| v.amount()).sum::<Amount>();
215
216 let change_amount = total_input.checked_sub(output.total_amount)
217 .ok_or(ArkoorConstructionError::Unbalanced {
218 input: total_input,
219 output: output.total_amount,
220 })?;
221
222 let outputs = if change_amount == Amount::ZERO {
223 vec![output]
224 } else {
225 vec![
226 output,
227 ArkoorDestination {
228 total_amount: change_amount,
229 policy: change_policy,
230 },
231 ]
232 };
233
234 Self::new_with_checkpoints(inputs, outputs)
235 }
236
237 pub fn new_claim_all_with_checkpoints(
239 inputs: impl IntoIterator<Item = Vtxo<Full>>,
240 output_policy: VtxoPolicy,
241 ) -> Result<Self, ArkoorConstructionError> {
242 let inputs = inputs.into_iter().collect::<Vec<_>>();
244 let total_input = inputs.iter().map(|v| v.amount()).sum::<Amount>();
245
246 let output = ArkoorDestination {
247 total_amount: total_input,
248 policy: output_policy,
249 };
250
251 Self::new_with_checkpoints(inputs, vec![output])
252 }
253
254 pub fn new_claim_all_without_checkpoints(
256 inputs: impl IntoIterator<Item = Vtxo<Full>>,
257 output_policy: VtxoPolicy,
258 ) -> Result<Self, ArkoorConstructionError> {
259 let inputs = inputs.into_iter().collect::<Vec<_>>();
261 let total_input = inputs.iter().map(|v| v.amount()).sum::<Amount>();
262
263 let output = ArkoorDestination {
264 total_amount: total_input,
265 policy: output_policy,
266 };
267
268 Self::new_without_checkpoints(inputs, vec![output])
269 }
270
271 fn new(
272 inputs: impl IntoIterator<Item = Vtxo<Full>>,
273 outputs: Vec<ArkoorDestination>,
274 use_checkpoint: bool,
275 ) -> Result<Self, ArkoorConstructionError> {
276 let allocations = Self::allocate_outputs_to_inputs(inputs, outputs)?;
278
279 let mut builders = Vec::with_capacity(allocations.len());
281 for (input, allocated_outputs) in allocations {
282 let builder = ArkoorBuilder::new_isolate_dust(
283 input,
284 allocated_outputs,
285 use_checkpoint,
286 )?;
287 builders.push(builder);
288 }
289
290 Ok(Self { builders })
291 }
292
293 pub fn generate_user_nonces(
294 self,
295 user_keypairs: &[Keypair],
296 ) -> Result<ArkoorPackageBuilder<state::UserGeneratedNonces>, ArkoorSigningError> {
297 if user_keypairs.len() != self.builders.len() {
298 return Err(ArkoorSigningError::InvalidNbKeypairs {
299 expected: self.builders.len(),
300 got: user_keypairs.len(),
301 })
302 }
303
304 let mut builder = Vec::with_capacity(self.builders.len());
305 for (idx, package) in self.builders.into_iter().enumerate() {
306 builder.push(package.generate_user_nonces(user_keypairs[idx]));
307 }
308 Ok(ArkoorPackageBuilder { builders: builder })
309 }
310}
311
312impl ArkoorPackageBuilder<state::UserGeneratedNonces> {
313 pub fn user_cosign(
314 self,
315 user_keypairs: &[Keypair],
316 server_cosign_response: ArkoorPackageCosignResponse,
317 ) -> Result<ArkoorPackageBuilder<state::UserSigned>, ArkoorSigningError> {
318 if server_cosign_response.responses.len() != self.builders.len() {
319 return Err(ArkoorSigningError::InvalidNbPackages {
320 expected: self.builders.len(),
321 got: server_cosign_response.responses.len()
322 })
323 }
324
325 if user_keypairs.len() != self.builders.len() {
326 return Err(ArkoorSigningError::InvalidNbKeypairs {
327 expected: self.builders.len(),
328 got: user_keypairs.len(),
329 })
330 }
331
332 let mut packages = Vec::with_capacity(self.builders.len());
333
334 for (idx, pkg) in self.builders.into_iter().enumerate() {
335 packages.push(pkg.user_cosign(
336 &user_keypairs[idx],
337 &server_cosign_response.responses[idx],
338 )?,);
339 }
340 Ok(ArkoorPackageBuilder { builders: packages })
341 }
342
343 pub fn cosign_request(&self) -> ArkoorPackageCosignRequest<Vtxo<Full>> {
344 let requests = self.builders.iter()
345 .map(|package| package.cosign_request())
346 .collect::<Vec<_>>();
347
348 ArkoorPackageCosignRequest { requests }
349 }
350}
351
352impl ArkoorPackageBuilder<state::UserSigned> {
353 pub fn build_signed_vtxos(self) -> Vec<Vtxo<Full>> {
354 self.builders.into_iter()
355 .map(|b| b.build_signed_vtxos())
356 .flatten()
357 .collect::<Vec<_>>()
358 }
359}
360
361impl ArkoorPackageBuilder<state::ServerCanCosign> {
362 pub fn from_cosign_request(
363 cosign_request: ArkoorPackageCosignRequest<Vtxo<Full>>,
364 ) -> Result<Self, (usize, ArkoorSigningError)> {
365 let request_iter = cosign_request.requests.into_iter();
366 let mut packages = Vec::with_capacity(request_iter.size_hint().0);
367 for (idx, request) in request_iter.enumerate() {
368 packages.push(ArkoorBuilder::from_cosign_request(request)
369 .map_err(|e| (idx, e))?);
370 }
371
372 Ok(Self { builders: packages })
373 }
374
375 pub fn server_cosign(
376 self,
377 server_keypair: &Keypair,
378 ) -> Result<ArkoorPackageBuilder<state::ServerSigned>, ArkoorSigningError> {
379 let mut packages = Vec::with_capacity(self.builders.len());
380 for package in self.builders.into_iter() {
381 packages.push(package.server_cosign(&server_keypair)?);
382 }
383 Ok(ArkoorPackageBuilder { builders: packages })
384 }
385}
386
387impl ArkoorPackageBuilder<state::ServerSigned> {
388 pub fn cosign_response(&self) -> ArkoorPackageCosignResponse {
389 let responses = self.builders.iter()
390 .map(|package| package.cosign_response())
391 .collect::<Vec<_>>();
392
393 ArkoorPackageCosignResponse { responses }
394 }
395}
396
397impl<S: state::BuilderState> ArkoorPackageBuilder<S> {
398 pub fn input_ids<'a>(&'a self) -> impl Iterator<Item = VtxoId> + Clone + 'a {
400 self.builders.iter().map(|b| b.input().id())
401 }
402
403 pub fn build_unsigned_vtxos<'a>(&'a self) -> impl Iterator<Item = Vtxo<Full>> + 'a {
404 self.builders.iter()
405 .map(|b| b.build_unsigned_vtxos())
406 .flatten()
407 }
408
409 pub fn build_unsigned_internal_vtxos<'a>(&'a self) -> impl Iterator<Item = ServerVtxo<Full>> + 'a {
414 self.builders.iter()
415 .map(|b| b.build_unsigned_internal_vtxos())
416 .flatten()
417 }
418
419 pub fn spend_info<'a>(&'a self) -> impl Iterator<Item = (VtxoId, Txid)> + 'a {
422 self.builders.iter()
423 .map(|b| b.spend_info())
424 .flatten()
425 }
426
427 pub fn virtual_transactions<'a>(&'a self) -> impl Iterator<Item = Txid> + 'a {
428 self.builders.iter()
429 .flat_map(|b| b.virtual_transactions())
430 }
431}
432
433#[cfg(test)]
434mod test {
435 use std::collections::{HashMap, HashSet};
436 use std::str::FromStr;
437
438 use bitcoin::{Transaction, Txid};
439 use bitcoin::secp256k1::Keypair;
440
441 use bitcoin_ext::P2TR_DUST;
442
443 use super::*;
444 use crate::test_util::dummy::DummyTestVtxoSpec;
445 use crate::PublicKey;
446
447 fn server_keypair() -> Keypair {
448 Keypair::from_str("f7a2a5d150afb575e98fff9caeebf6fbebbaeacfdfa7433307b208b39f1155f2").expect("Invalid key")
449 }
450
451 fn alice_keypair() -> Keypair {
452 Keypair::from_str("9b4382c8985f12e4bd8d1b51e63615bf0187843630829f4c5e9c45ef2cf994a4").expect("Invalid key")
453 }
454
455 fn bob_keypair() -> Keypair {
456 Keypair::from_str("c86435ba7e30d7afd7c5df9f3263ce2eb86b3ff9866a16ccd22a0260496ddf0f").expect("Invalid key")
457 }
458
459
460 fn alice_public_key() -> PublicKey {
461 alice_keypair().public_key()
462 }
463
464 fn bob_public_key() -> PublicKey {
465 bob_keypair().public_key()
466 }
467
468 fn dummy_vtxo_for_amount(amt: Amount) -> (Transaction, Vtxo<Full>) {
469 DummyTestVtxoSpec {
470 amount: amt + P2TR_DUST,
471 fee: P2TR_DUST,
472 expiry_height: 1000,
473 exit_delta: 128,
474 user_keypair: alice_keypair(),
475 server_keypair: server_keypair()
476 }.build()
477 }
478
479 fn verify_package_builder(
480 builder: ArkoorPackageBuilder<state::Initial>,
481 keypairs: &[Keypair],
482 funding_tx_map: HashMap<Txid, Transaction>,
483 ) {
484 let vtxs: Vec<Txid> = builder.virtual_transactions().collect();
486 let vtx_set: HashSet<Txid> = vtxs.iter().copied().collect();
487 let spend_txids: HashSet<Txid> = builder.spend_info().map(|(_, txid)| txid).collect();
488
489 assert_eq!(vtxs.len(), vtx_set.len(), "virtual_transactions() contains duplicates");
491
492 for txid in &vtx_set {
494 assert!(spend_txids.contains(txid), "virtual_transaction {} not in spend_info", txid);
495 }
496
497 for txid in &spend_txids {
499 assert!(vtx_set.contains(txid), "spend_info txid {} not in virtual_transactions", txid);
500 }
501
502 let user_builder = builder.generate_user_nonces(keypairs).expect("Valid nb of keypairs");
503 let cosign_requests = user_builder.cosign_request();
504
505 let cosign_responses = ArkoorPackageBuilder::from_cosign_request(cosign_requests)
506 .expect("Invalid cosign requests")
507 .server_cosign(&server_keypair())
508 .expect("Wrong server key")
509 .cosign_response();
510
511
512 let vtxos = user_builder.user_cosign(keypairs, cosign_responses)
513 .expect("Invalid cosign responses")
514 .build_signed_vtxos();
515
516 for vtxo in vtxos {
517 let funding_txid = vtxo.chain_anchor().txid;
518 let funding_tx = funding_tx_map.get(&funding_txid).expect("Funding tx not found");
519 vtxo.validate(&funding_tx).expect("Invalid vtxo");
520
521 let mut prev_tx = funding_tx.clone();
522 for tx in vtxo.transactions().map(|item| item.tx) {
523 crate::test_util::verify_tx(
524 &[prev_tx.output[vtxo.chain_anchor().vout as usize].clone()],
525 0,
526 &tx).expect("Invalid transaction");
527 prev_tx = tx;
528 }
529 }
530 }
531
532 #[test]
533 fn send_full_vtxo() {
534 let (funding_tx, alice_vtxo) = dummy_vtxo_for_amount(Amount::from_sat(100_000));
537
538 let package_builder = ArkoorPackageBuilder::new_single_output_with_checkpoints(
539 [alice_vtxo],
540 ArkoorDestination {
541 total_amount: Amount::from_sat(100_000),
542 policy: VtxoPolicy::new_pubkey(bob_public_key()),
543 },
544 VtxoPolicy::new_pubkey(alice_public_key())
545 ).expect("Valid package");
546
547 let funding_map = HashMap::from([(funding_tx.compute_txid(), funding_tx)]);
548 verify_package_builder(package_builder, &[alice_keypair()], funding_map);
549 }
550
551 #[test]
552 fn arkoor_dust_change() {
553 let (_funding_tx, alice_vtxo) = dummy_vtxo_for_amount(Amount::from_sat(1000));
557 let package_builder = ArkoorPackageBuilder::new_single_output_with_checkpoints(
558 [alice_vtxo],
559 ArkoorDestination {
560 total_amount: Amount::from_sat(900),
561 policy: VtxoPolicy::new_pubkey(bob_public_key()),
562 },
563 VtxoPolicy::new_pubkey(alice_public_key())
564 ).expect("Valid package");
565
566 let vtxos: Vec<Vtxo<Full>> = package_builder.build_unsigned_vtxos().collect();
568 assert_eq!(vtxos.len(), 3);
569 assert_eq!(vtxos[0].amount(), Amount::from_sat(670));
570 assert_eq!(vtxos[0].policy().user_pubkey(), bob_public_key());
571 assert_eq!(vtxos[1].amount(), Amount::from_sat(230));
572 assert_eq!(vtxos[1].policy().user_pubkey(), bob_public_key());
573 assert_eq!(vtxos[2].amount(), Amount::from_sat(100));
574 assert_eq!(vtxos[2].policy().user_pubkey(), alice_public_key());
575 }
576
577 #[test]
578 fn can_send_multiple_inputs() {
579 let (funding_tx_1, alice_vtxo_1) = dummy_vtxo_for_amount(Amount::from_sat(10_000));
582 let (funding_tx_2, alice_vtxo_2) = dummy_vtxo_for_amount(Amount::from_sat(5_000));
583 let (funding_tx_3, alice_vtxo_3) = dummy_vtxo_for_amount(Amount::from_sat(2_000));
584
585 let package = ArkoorPackageBuilder::new_single_output_with_checkpoints(
586 [alice_vtxo_1, alice_vtxo_2, alice_vtxo_3],
587 ArkoorDestination {
588 total_amount: Amount::from_sat(17_000),
589 policy: VtxoPolicy::new_pubkey(bob_public_key()),
590 },
591 VtxoPolicy::new_pubkey(alice_public_key())
592 ).expect("Valid package");
593
594 let vtxos: Vec<Vtxo<Full>> = package.build_unsigned_vtxos().collect();
595 assert_eq!(vtxos.len(), 3);
596 assert_eq!(vtxos[0].amount(), Amount::from_sat(10_000));
597 assert_eq!(vtxos[1].amount(), Amount::from_sat(5_000));
598 assert_eq!(vtxos[2].amount(), Amount::from_sat(2_000));
599 assert_eq!(
600 vtxos.iter().map(|v| v.policy().user_pubkey()).collect::<Vec<_>>(),
601 vec![bob_public_key(); 3],
602 );
603
604 let funding_map = HashMap::from([
605 (funding_tx_1.compute_txid(), funding_tx_1),
606 (funding_tx_2.compute_txid(), funding_tx_2),
607 (funding_tx_3.compute_txid(), funding_tx_3),
608 ]);
609 verify_package_builder(
610 package, &[alice_keypair(), alice_keypair(), alice_keypair()], funding_map,
611 );
612 }
613
614 #[test]
615 fn can_send_multiple_inputs_with_change() {
616 let (funding_tx_1, alice_vtxo_1) = dummy_vtxo_for_amount(Amount::from_sat(10_000));
620 let (funding_tx_2, alice_vtxo_2) = dummy_vtxo_for_amount(Amount::from_sat(5_000));
621 let (funding_tx_3, alice_vtxo_3) = dummy_vtxo_for_amount(Amount::from_sat(2_000));
622
623 let package = ArkoorPackageBuilder::new_single_output_with_checkpoints(
624 [alice_vtxo_1, alice_vtxo_2, alice_vtxo_3],
625 ArkoorDestination {
626 total_amount: Amount::from_sat(16_000),
627 policy: VtxoPolicy::new_pubkey(bob_public_key()),
628 },
629 VtxoPolicy::new_pubkey(alice_public_key())
630 ).expect("Valid package");
631
632 let vtxos: Vec<Vtxo<Full>> = package.build_unsigned_vtxos().collect();
633 assert_eq!(vtxos.len(), 4);
634 assert_eq!(vtxos[0].amount(), Amount::from_sat(10_000));
635 assert_eq!(vtxos[1].amount(), Amount::from_sat(5_000));
636 assert_eq!(vtxos[2].amount(), Amount::from_sat(1_000));
637 assert_eq!(vtxos[3].amount(), Amount::from_sat(1_000),
638 "Alice should receive a 1000 sats as change",
639 );
640
641 assert_eq!(vtxos[0].policy().user_pubkey(), bob_public_key());
642 assert_eq!(vtxos[1].policy().user_pubkey(), bob_public_key());
643 assert_eq!(vtxos[2].policy().user_pubkey(), bob_public_key());
644 assert_eq!(vtxos[3].policy().user_pubkey(), alice_public_key());
645
646 let funding_map = HashMap::from([
647 (funding_tx_1.compute_txid(), funding_tx_1),
648 (funding_tx_2.compute_txid(), funding_tx_2),
649 (funding_tx_3.compute_txid(), funding_tx_3),
650 ]);
651 verify_package_builder(
652 package, &[alice_keypair(), alice_keypair(), alice_keypair()], funding_map,
653 );
654 }
655
656 #[test]
657 fn can_send_multiple_vtxos_with_dust_change() {
658 let (_funding_tx_1, alice_vtxo_1) = dummy_vtxo_for_amount(Amount::from_sat(5_000));
662 let (_funding_tx_2, alice_vtxo_2) = dummy_vtxo_for_amount(Amount::from_sat(1_000));
663
664 let package = ArkoorPackageBuilder::new_single_output_with_checkpoints(
665 [alice_vtxo_1, alice_vtxo_2],
666 ArkoorDestination {
667 total_amount: Amount::from_sat(5_700),
668 policy: VtxoPolicy::new_pubkey(bob_public_key()),
669 },
670 VtxoPolicy::new_pubkey(alice_public_key())
671 ).expect("Valid package");
672
673 let vtxos: Vec<Vtxo<Full>> = package.build_unsigned_vtxos().collect();
674 assert_eq!(vtxos.len(), 4);
675 assert_eq!(vtxos[0].amount(), Amount::from_sat(5_000));
676 assert_eq!(vtxos[0].policy().user_pubkey(), bob_public_key());
677 assert_eq!(vtxos[1].amount(), Amount::from_sat(670));
678 assert_eq!(vtxos[1].policy().user_pubkey(), bob_public_key());
679 assert_eq!(vtxos[2].amount(), Amount::from_sat(30));
680 assert_eq!(vtxos[2].policy().user_pubkey(), bob_public_key());
681 assert_eq!(vtxos[3].amount(), Amount::from_sat(300));
682 assert_eq!(vtxos[3].policy().user_pubkey(), alice_public_key());
683 }
684
685 #[test]
686 fn not_enough_money() {
687 let (_funding_tx, alice_vtxo) = dummy_vtxo_for_amount(Amount::from_sat(900));
691 let result = ArkoorPackageBuilder::new_single_output_with_checkpoints(
692 [alice_vtxo],
693 ArkoorDestination {
694 total_amount: Amount::from_sat(1000),
695 policy: VtxoPolicy::new_pubkey(bob_public_key()),
696 },
697 VtxoPolicy::new_pubkey(alice_public_key())
698 );
699
700 match result {
701 Ok(_) => panic!("Package should be invalid"),
702 Err(ArkoorConstructionError::Unbalanced { input, output }) => {
703 assert_eq!(input, Amount::from_sat(900));
704 assert_eq!(output, Amount::from_sat(1000));
705 }
706 Err(e) => panic!("Unexpected error: {:?}", e),
707 }
708 }
709
710 #[test]
711 fn not_enough_money_with_multiple_inputs() {
712 let (_funding_tx, alice_vtxo_1) = dummy_vtxo_for_amount(Amount::from_sat(10_000));
716 let (_funding_tx, alice_vtxo_2) = dummy_vtxo_for_amount(Amount::from_sat(5_000));
717 let (_funding_tx, alice_vtxo_3) = dummy_vtxo_for_amount(Amount::from_sat(2_000));
718
719 let package = ArkoorPackageBuilder::new_single_output_with_checkpoints(
720 [alice_vtxo_1, alice_vtxo_2, alice_vtxo_3],
721 ArkoorDestination {
722 total_amount: Amount::from_sat(20_000),
723 policy: VtxoPolicy::new_pubkey(bob_public_key()),
724 },
725 VtxoPolicy::new_pubkey(alice_public_key())
726 );
727
728 match package {
729 Ok(_) => panic!("Package should be invalid"),
730 Err(ArkoorConstructionError::Unbalanced { input, output }) => {
731 assert_eq!(input, Amount::from_sat(17_000));
732 assert_eq!(output, Amount::from_sat(20_000));
733 }
734 Err(e) => panic!("Unexpected error: {:?}", e)
735 }
736 }
737
738 #[test]
739 fn can_use_all_provided_inputs_with_change() {
740 let (_funding_tx, alice_vtxo_1) = dummy_vtxo_for_amount(Amount::from_sat(1000));
745 let (_funding_tx, alice_vtxo_2) = dummy_vtxo_for_amount(Amount::from_sat(1000));
746 let (_funding_tx, alice_vtxo_3) = dummy_vtxo_for_amount(Amount::from_sat(1000));
747 let (_funding_tx, alice_vtxo_4) = dummy_vtxo_for_amount(Amount::from_sat(1000));
748
749 let package = ArkoorPackageBuilder::new_single_output_with_checkpoints(
750 [alice_vtxo_1, alice_vtxo_2, alice_vtxo_3, alice_vtxo_4],
751 ArkoorDestination {
752 total_amount: Amount::from_sat(2000),
753 policy: VtxoPolicy::new_pubkey(bob_public_key()),
754 },
755 VtxoPolicy::new_pubkey(alice_public_key())
756 ).expect("Package should be valid");
757
758 let vtxos = package.build_unsigned_vtxos().collect::<Vec<_>>();
760 let total_output = vtxos.iter().map(|v| v.amount()).sum::<Amount>();
761 assert_eq!(total_output, Amount::from_sat(4000));
762 }
763
764 #[test]
765 fn single_input_multiple_outputs() {
766 let (funding_tx, alice_vtxo) = dummy_vtxo_for_amount(Amount::from_sat(10_000));
768
769 let outputs = vec![
770 ArkoorDestination {
771 total_amount: Amount::from_sat(4_000),
772 policy: VtxoPolicy::new_pubkey(bob_public_key())
773 },
774 ArkoorDestination {
775 total_amount: Amount::from_sat(3_000),
776 policy: VtxoPolicy::new_pubkey(bob_public_key())
777 },
778 ArkoorDestination {
779 total_amount: Amount::from_sat(3_000),
780 policy: VtxoPolicy::new_pubkey(bob_public_key())
781 },
782 ];
783
784 let package = ArkoorPackageBuilder::new_with_checkpoints(
785 [alice_vtxo.clone()],
786 outputs,
787 ).expect("Valid package");
788
789 let vtxos: Vec<Vtxo<Full>> = package.build_unsigned_vtxos().collect();
790 assert_eq!(vtxos.len(), 3);
791 assert_eq!(vtxos[0].amount(), Amount::from_sat(4_000));
792 assert_eq!(vtxos[1].amount(), Amount::from_sat(3_000));
793 assert_eq!(vtxos[2].amount(), Amount::from_sat(3_000));
794
795 let user_keypair = alice_keypair();
797 let user_builder = package.generate_user_nonces(&[user_keypair])
798 .expect("Valid nb of keypairs");
799 let cosign_requests = user_builder.cosign_request();
800
801 let cosign_responses = ArkoorPackageBuilder::from_cosign_request(cosign_requests)
802 .expect("Invalid cosign requests")
803 .server_cosign(&server_keypair())
804 .expect("Wrong server key")
805 .cosign_response();
806
807 let signed_vtxos = user_builder.user_cosign(&[user_keypair], cosign_responses)
808 .expect("Invalid cosign responses")
809 .build_signed_vtxos();
810
811 assert_eq!(signed_vtxos.len(), 3, "Should create 3 signed vtxos");
812
813 signed_vtxos[0].validate(&funding_tx).expect("First vtxo should be valid");
815 }
816
817 #[test]
818 fn output_split_across_inputs() {
819 let (_funding_tx_1, alice_vtxo_1) = dummy_vtxo_for_amount(Amount::from_sat(600));
822 let (_funding_tx_2, alice_vtxo_2) = dummy_vtxo_for_amount(Amount::from_sat(500));
823
824 let outputs = vec![
825 ArkoorDestination {
826 total_amount: Amount::from_sat(800),
827 policy: VtxoPolicy::new_pubkey(bob_public_key())
828 },
829 ArkoorDestination {
830 total_amount: Amount::from_sat(300),
831 policy: VtxoPolicy::new_pubkey(bob_public_key())
832 },
833 ];
834
835 let package = ArkoorPackageBuilder::new_with_checkpoints(
836 [alice_vtxo_1, alice_vtxo_2],
837 outputs,
838 ).expect("Valid package");
839
840 let vtxos: Vec<Vtxo<Full>> = package.build_unsigned_vtxos().collect();
841 assert_eq!(vtxos.len(), 3);
842 assert_eq!(vtxos[0].amount(), Amount::from_sat(600));
843 assert_eq!(vtxos[0].policy().user_pubkey(), bob_public_key());
844 assert_eq!(vtxos[1].amount(), Amount::from_sat(200));
845 assert_eq!(vtxos[1].policy().user_pubkey(), bob_public_key());
846 assert_eq!(vtxos[2].amount(), Amount::from_sat(300));
847 assert_eq!(vtxos[2].policy().user_pubkey(), bob_public_key());
848 }
849
850 #[test]
851 fn dust_splits_allowed() {
852 let (_funding_tx_1, alice_vtxo_1) = dummy_vtxo_for_amount(Amount::from_sat(500));
855 let (_funding_tx_2, alice_vtxo_2) = dummy_vtxo_for_amount(Amount::from_sat(500));
856
857 let outputs = vec![
858 ArkoorDestination {
859 total_amount: Amount::from_sat(750),
860 policy: VtxoPolicy::new_pubkey(bob_public_key())
861 },
862 ArkoorDestination {
863 total_amount: Amount::from_sat(250),
864 policy: VtxoPolicy::new_pubkey(bob_public_key())
865 },
866 ];
867
868 let package = ArkoorPackageBuilder::new_with_checkpoints(
869 [alice_vtxo_1, alice_vtxo_2],
870 outputs,
871 ).expect("Valid package");
872
873 let vtxos: Vec<Vtxo<Full>> = package.build_unsigned_vtxos().collect();
874 assert_eq!(vtxos.len(), 3);
875 assert_eq!(vtxos[0].amount(), Amount::from_sat(500));
876 assert_eq!(vtxos[1].amount(), Amount::from_sat(250)); assert_eq!(vtxos[2].amount(), Amount::from_sat(250));
878 }
879
880 #[test]
881 fn unbalanced_amounts_rejected() {
882 let (_funding_tx, alice_vtxo) = dummy_vtxo_for_amount(Amount::from_sat(1000));
884
885 let outputs = vec![
886 ArkoorDestination {
887 total_amount: Amount::from_sat(600),
888 policy: VtxoPolicy::new_pubkey(bob_public_key())
889 },
890 ArkoorDestination {
891 total_amount: Amount::from_sat(600),
892 policy: VtxoPolicy::new_pubkey(bob_public_key())
893 },
894 ];
895
896 let result = ArkoorPackageBuilder::new_with_checkpoints(
897 [alice_vtxo],
898 outputs,
899 );
900
901 match result {
902 Err(ArkoorConstructionError::Unbalanced { input, output }) => {
903 assert_eq!(input, Amount::from_sat(1000));
904 assert_eq!(output, Amount::from_sat(1200));
905 }
906 _ => panic!("Expected Unbalanced error"),
907 }
908 }
909
910 #[test]
911 fn empty_outputs_rejected() {
912 let (_funding_tx, alice_vtxo) = dummy_vtxo_for_amount(Amount::from_sat(1000));
913
914 let result = ArkoorPackageBuilder::new_with_checkpoints(
915 [alice_vtxo],
916 vec![],
917 );
918
919 match result {
920 Err(ArkoorConstructionError::NoOutputs) => {}
921 Err(e) => panic!("Expected NoOutputs error, got: {:?}", e),
922 Ok(_) => panic!("Expected NoOutputs error, got Ok"),
923 }
924 }
925
926 #[test]
927 fn multiple_inputs_multiple_outputs_exact_balance() {
928 let (_funding_tx_1, alice_vtxo_1) = dummy_vtxo_for_amount(Amount::from_sat(1000));
930 let (_funding_tx_2, alice_vtxo_2) = dummy_vtxo_for_amount(Amount::from_sat(2000));
931 let (_funding_tx_3, alice_vtxo_3) = dummy_vtxo_for_amount(Amount::from_sat(1500));
932
933 let outputs = vec![
934 ArkoorDestination {
935 total_amount: Amount::from_sat(2500),
936 policy: VtxoPolicy::new_pubkey(bob_public_key())
937 },
938 ArkoorDestination {
939 total_amount: Amount::from_sat(2000),
940 policy: VtxoPolicy::new_pubkey(bob_public_key())
941 },
942 ];
943
944 let package = ArkoorPackageBuilder::new_with_checkpoints(
945 [alice_vtxo_1, alice_vtxo_2, alice_vtxo_3],
946 outputs,
947 ).expect("Valid package");
948
949 let vtxos: Vec<Vtxo<Full>> = package.build_unsigned_vtxos().collect();
950 assert_eq!(vtxos.len(), 4);
951 assert_eq!(vtxos[0].amount(), Amount::from_sat(1000));
955 assert_eq!(vtxos[1].amount(), Amount::from_sat(1500));
956 assert_eq!(vtxos[2].amount(), Amount::from_sat(500));
957 assert_eq!(vtxos[3].amount(), Amount::from_sat(1500));
958 }
959
960 #[test]
961 fn single_output_across_many_inputs() {
962 let (_funding_tx_1, alice_vtxo_1) = dummy_vtxo_for_amount(Amount::from_sat(100));
965 let (_funding_tx_2, alice_vtxo_2) = dummy_vtxo_for_amount(Amount::from_sat(100));
966 let (_funding_tx_3, alice_vtxo_3) = dummy_vtxo_for_amount(Amount::from_sat(100));
967 let (_funding_tx_4, alice_vtxo_4) = dummy_vtxo_for_amount(Amount::from_sat(100));
968
969 let outputs = vec![
970 ArkoorDestination {
971 total_amount: Amount::from_sat(400),
972 policy: VtxoPolicy::new_pubkey(bob_public_key())
973 },
974 ];
975
976 let package = ArkoorPackageBuilder::new_with_checkpoints(
977 [alice_vtxo_1, alice_vtxo_2, alice_vtxo_3, alice_vtxo_4],
978 outputs,
979 ).expect("Valid package");
980
981 let vtxos: Vec<Vtxo<Full>> = package.build_unsigned_vtxos().collect();
982 assert_eq!(vtxos.len(), 4);
983 assert_eq!(vtxos[0].amount(), Amount::from_sat(100));
984 assert_eq!(vtxos[1].amount(), Amount::from_sat(100));
985 assert_eq!(vtxos[2].amount(), Amount::from_sat(100));
986 assert_eq!(vtxos[3].amount(), Amount::from_sat(100));
987 let total: Amount = vtxos.iter().map(|v| v.amount()).sum();
988 assert_eq!(total, Amount::from_sat(400));
989 }
990
991 #[test]
992 fn many_outputs_from_single_input() {
993 let (_funding_tx, alice_vtxo) = dummy_vtxo_for_amount(Amount::from_sat(1000));
995
996 let outputs = vec![
997 ArkoorDestination {
998 total_amount: Amount::from_sat(100),
999 policy: VtxoPolicy::new_pubkey(bob_public_key())
1000 },
1001 ArkoorDestination {
1002 total_amount: Amount::from_sat(200),
1003 policy: VtxoPolicy::new_pubkey(bob_public_key())
1004 },
1005 ArkoorDestination {
1006 total_amount: Amount::from_sat(150),
1007 policy: VtxoPolicy::new_pubkey(bob_public_key())
1008 },
1009 ArkoorDestination {
1010 total_amount: Amount::from_sat(250),
1011 policy: VtxoPolicy::new_pubkey(bob_public_key())
1012 },
1013 ArkoorDestination {
1014 total_amount: Amount::from_sat(300),
1015 policy: VtxoPolicy::new_pubkey(bob_public_key())
1016 },
1017 ];
1018
1019 let package = ArkoorPackageBuilder::new_with_checkpoints(
1020 [alice_vtxo],
1021 outputs,
1022 ).expect("Valid package");
1023
1024 let vtxos: Vec<Vtxo<Full>> = package.build_unsigned_vtxos().collect();
1025 assert_eq!(vtxos.len(), 5);
1026 assert_eq!(vtxos[0].amount(), Amount::from_sat(100));
1027 assert_eq!(vtxos[1].amount(), Amount::from_sat(200));
1028 assert_eq!(vtxos[2].amount(), Amount::from_sat(150));
1029 assert_eq!(vtxos[3].amount(), Amount::from_sat(250));
1030 assert_eq!(vtxos[4].amount(), Amount::from_sat(300));
1031 }
1032
1033 #[test]
1034 fn first_input_exactly_matches_first_output() {
1035 let (_funding_tx_1, alice_vtxo_1) = dummy_vtxo_for_amount(Amount::from_sat(1000));
1038 let (_funding_tx_2, alice_vtxo_2) = dummy_vtxo_for_amount(Amount::from_sat(500));
1039
1040 let outputs = vec![
1041 ArkoorDestination {
1042 total_amount: Amount::from_sat(1000),
1043 policy: VtxoPolicy::new_pubkey(bob_public_key())
1044 },
1045 ArkoorDestination {
1046 total_amount: Amount::from_sat(500),
1047 policy: VtxoPolicy::new_pubkey(bob_public_key())
1048 },
1049 ];
1050
1051 let package = ArkoorPackageBuilder::new_with_checkpoints(
1052 [alice_vtxo_1, alice_vtxo_2],
1053 outputs,
1054 ).expect("Valid package");
1055
1056 let vtxos: Vec<Vtxo<Full>> = package.build_unsigned_vtxos().collect();
1057 assert_eq!(vtxos.len(), 2);
1058 assert_eq!(vtxos[0].amount(), Amount::from_sat(1000));
1059 assert_eq!(vtxos[1].amount(), Amount::from_sat(500));
1060 }
1061
1062 #[test]
1063 fn empty_inputs_rejected() {
1064 let outputs = vec![
1066 ArkoorDestination {
1067 total_amount: Amount::from_sat(1000),
1068 policy: VtxoPolicy::new_pubkey(bob_public_key())
1069 },
1070 ];
1071
1072 let result = ArkoorPackageBuilder::new_with_checkpoints(
1073 Vec::<Vtxo<Full>>::new(),
1074 outputs,
1075 );
1076
1077 match result {
1078 Ok(_) => panic!("Should reject empty inputs"),
1079 Err(ArkoorConstructionError::Unbalanced { input, output }) => {
1080 assert_eq!(input, Amount::ZERO);
1081 assert_eq!(output, Amount::from_sat(1000));
1082 }
1083 Err(e) => panic!("Unexpected error: {:?}", e),
1084 }
1085 }
1086
1087 #[test]
1088 fn alternating_split_pattern() {
1089 let (_funding_tx_1, alice_vtxo_1) = dummy_vtxo_for_amount(Amount::from_sat(300));
1094 let (_funding_tx_2, alice_vtxo_2) = dummy_vtxo_for_amount(Amount::from_sat(700));
1095 let (_funding_tx_3, alice_vtxo_3) = dummy_vtxo_for_amount(Amount::from_sat(500));
1096
1097 let outputs = vec![
1098 ArkoorDestination {
1099 total_amount: Amount::from_sat(500),
1100 policy: VtxoPolicy::new_pubkey(bob_public_key())
1101 },
1102 ArkoorDestination {
1103 total_amount: Amount::from_sat(400),
1104 policy: VtxoPolicy::new_pubkey(bob_public_key())
1105 },
1106 ArkoorDestination {
1107 total_amount: Amount::from_sat(600),
1108 policy: VtxoPolicy::new_pubkey(bob_public_key())
1109 },
1110 ];
1111
1112 let package = ArkoorPackageBuilder::new_with_checkpoints(
1113 [alice_vtxo_1, alice_vtxo_2, alice_vtxo_3],
1114 outputs,
1115 ).expect("Valid package");
1116
1117 let vtxos: Vec<Vtxo<Full>> = package.build_unsigned_vtxos().collect();
1118 assert_eq!(vtxos.len(), 5);
1119 assert_eq!(vtxos[0].amount(), Amount::from_sat(300));
1121 assert_eq!(vtxos[1].amount(), Amount::from_sat(200));
1123 assert_eq!(vtxos[2].amount(), Amount::from_sat(400));
1124 assert_eq!(vtxos[3].amount(), Amount::from_sat(100));
1125 assert_eq!(vtxos[4].amount(), Amount::from_sat(500));
1127 let total: Amount = vtxos.iter().map(|v| v.amount()).sum();
1128 assert_eq!(total, Amount::from_sat(1500));
1129 }
1130
1131 #[test]
1132 fn spend_info_correctness_simple_checkpoint() {
1133 let (_funding_tx, alice_vtxo) = dummy_vtxo_for_amount(
1135 Amount::from_sat(100_000)
1136 );
1137 let input_id = alice_vtxo.id();
1138
1139 let package = ArkoorPackageBuilder::new_single_output_with_checkpoints(
1140 [alice_vtxo],
1141 ArkoorDestination {
1142 total_amount: Amount::from_sat(100_000),
1143 policy: VtxoPolicy::new_pubkey(bob_public_key()),
1144 },
1145 VtxoPolicy::new_pubkey(alice_public_key())
1146 ).expect("Valid package");
1147
1148 let internal_vtxos: Vec<VtxoId> = package
1150 .build_unsigned_internal_vtxos()
1151 .map(|v| v.id())
1152 .collect();
1153
1154 let spend_info: Vec<(VtxoId, Txid)> = package.spend_info().collect();
1156
1157 let mut expected_vtxo_ids = vec![input_id];
1159 expected_vtxo_ids.extend(internal_vtxos.iter());
1160
1161 let actual_vtxo_ids: Vec<VtxoId> = spend_info
1162 .iter()
1163 .map(|(id, _)| *id)
1164 .collect();
1165
1166 for id in &expected_vtxo_ids {
1168 assert!(
1169 actual_vtxo_ids.contains(id),
1170 "Expected VTXO ID {} not found in spend_info",
1171 id
1172 );
1173 }
1174
1175 assert_eq!(
1177 actual_vtxo_ids.len(),
1178 expected_vtxo_ids.len(),
1179 "spend_info contains unexpected entries"
1180 );
1181 }
1182
1183 #[test]
1184 fn spend_info_correctness_with_dust_isolation() {
1185 let (_funding_tx, alice_vtxo) = dummy_vtxo_for_amount(
1187 Amount::from_sat(1000)
1188 );
1189 let input_id = alice_vtxo.id();
1190
1191 let package = ArkoorPackageBuilder::new_single_output_with_checkpoints(
1192 [alice_vtxo],
1193 ArkoorDestination {
1194 total_amount: Amount::from_sat(900),
1195 policy: VtxoPolicy::new_pubkey(bob_public_key()),
1196 },
1197 VtxoPolicy::new_pubkey(alice_public_key())
1198 ).expect("Valid package");
1199
1200 let internal_vtxos: Vec<VtxoId> = package
1202 .build_unsigned_internal_vtxos()
1203 .map(|v| v.id())
1204 .collect();
1205
1206 let spend_info: Vec<(VtxoId, Txid)> = package.spend_info().collect();
1208
1209 let mut expected_vtxo_ids = vec![input_id];
1211 expected_vtxo_ids.extend(internal_vtxos.iter());
1212
1213 let actual_vtxo_ids: Vec<VtxoId> = spend_info
1214 .iter()
1215 .map(|(id, _)| *id)
1216 .collect();
1217
1218 for id in &expected_vtxo_ids {
1220 assert!(
1221 actual_vtxo_ids.contains(id),
1222 "Expected VTXO ID {} not found in spend_info",
1223 id
1224 );
1225 }
1226
1227 assert_eq!(
1229 actual_vtxo_ids.len(),
1230 expected_vtxo_ids.len(),
1231 "spend_info contains unexpected entries"
1232 );
1233 }
1234
1235 #[test]
1236 fn spend_info_correctness_without_checkpoints() {
1237 let (_funding_tx, alice_vtxo) = dummy_vtxo_for_amount(
1239 Amount::from_sat(100_000)
1240 );
1241 let input_id = alice_vtxo.id();
1242
1243 let package = ArkoorPackageBuilder::new_without_checkpoints(
1244 [alice_vtxo],
1245 vec![
1246 ArkoorDestination {
1247 total_amount: Amount::from_sat(100_000),
1248 policy: VtxoPolicy::new_pubkey(bob_public_key()),
1249 }
1250 ]
1251 ).expect("Valid package");
1252
1253 let internal_vtxos: Vec<VtxoId> = package
1255 .build_unsigned_internal_vtxos()
1256 .map(|v| v.id())
1257 .collect();
1258
1259 let spend_info: Vec<(VtxoId, Txid)> = package.spend_info().collect();
1261
1262 let mut expected_vtxo_ids = vec![input_id];
1264 expected_vtxo_ids.extend(internal_vtxos.iter());
1265
1266 let actual_vtxo_ids: Vec<VtxoId> = spend_info
1267 .iter()
1268 .map(|(id, _)| *id)
1269 .collect();
1270
1271 for id in &expected_vtxo_ids {
1273 assert!(
1274 actual_vtxo_ids.contains(id),
1275 "Expected VTXO ID {} not found in spend_info",
1276 id
1277 );
1278 }
1279
1280 assert_eq!(
1282 actual_vtxo_ids.len(),
1283 expected_vtxo_ids.len(),
1284 "spend_info contains unexpected entries"
1285 );
1286 }
1287
1288 #[test]
1289 fn spend_info_correctness_multiple_inputs() {
1290 let (_funding_tx_1, alice_vtxo_1) = dummy_vtxo_for_amount(
1292 Amount::from_sat(10_000)
1293 );
1294 let (_funding_tx_2, alice_vtxo_2) = dummy_vtxo_for_amount(
1295 Amount::from_sat(5_000)
1296 );
1297 let (_funding_tx_3, alice_vtxo_3) = dummy_vtxo_for_amount(
1298 Amount::from_sat(2_000)
1299 );
1300
1301 let input_ids = vec![
1302 alice_vtxo_1.id(),
1303 alice_vtxo_2.id(),
1304 alice_vtxo_3.id(),
1305 ];
1306
1307 let package = ArkoorPackageBuilder::new_single_output_with_checkpoints(
1308 [alice_vtxo_1, alice_vtxo_2, alice_vtxo_3],
1309 ArkoorDestination {
1310 total_amount: Amount::from_sat(16_000),
1311 policy: VtxoPolicy::new_pubkey(bob_public_key()),
1312 },
1313 VtxoPolicy::new_pubkey(alice_public_key())
1314 ).expect("Valid package");
1315
1316 let internal_vtxos: Vec<VtxoId> = package
1318 .build_unsigned_internal_vtxos()
1319 .map(|v| v.id())
1320 .collect();
1321
1322 let spend_info: Vec<(VtxoId, Txid)> = package.spend_info().collect();
1324
1325 let mut expected_vtxo_ids = input_ids.clone();
1327 expected_vtxo_ids.extend(internal_vtxos.iter());
1328
1329 let actual_vtxo_ids: Vec<VtxoId> = spend_info
1330 .iter()
1331 .map(|(id, _)| *id)
1332 .collect();
1333
1334 for id in &expected_vtxo_ids {
1336 assert!(
1337 actual_vtxo_ids.contains(id),
1338 "Expected VTXO ID {} not found in spend_info",
1339 id
1340 );
1341 }
1342
1343 assert_eq!(
1345 actual_vtxo_ids.len(),
1346 expected_vtxo_ids.len(),
1347 "spend_info contains unexpected entries"
1348 );
1349 }
1350}