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 }
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 })
93 }).collect::<Result<Vec<_>, _>>()?,
94 };
95
96 Ok(package)
97 }
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, ArkoorSigningError> {
365 let request_iter = cosign_request.requests.into_iter();
366 let mut packages = Vec::with_capacity(request_iter.size_hint().0);
367 for request in request_iter {
368 packages.push(ArkoorBuilder::from_cosign_request(request)?);
369 }
370
371 Ok(Self { builders: packages })
372 }
373
374 pub fn server_cosign(
375 self,
376 server_keypair: &Keypair,
377 ) -> Result<ArkoorPackageBuilder<state::ServerSigned>, ArkoorSigningError> {
378 let mut packages = Vec::with_capacity(self.builders.len());
379 for package in self.builders.into_iter() {
380 packages.push(package.server_cosign(&server_keypair)?);
381 }
382 Ok(ArkoorPackageBuilder { builders: packages })
383 }
384}
385
386impl ArkoorPackageBuilder<state::ServerSigned> {
387 pub fn cosign_response(&self) -> ArkoorPackageCosignResponse {
388 let responses = self.builders.iter()
389 .map(|package| package.cosign_response())
390 .collect::<Vec<_>>();
391
392 ArkoorPackageCosignResponse { responses }
393 }
394}
395
396impl<S: state::BuilderState> ArkoorPackageBuilder<S> {
397 pub fn input_ids<'a>(&'a self) -> impl Iterator<Item = VtxoId> + Clone + 'a {
399 self.builders.iter().map(|b| b.input().id())
400 }
401
402 pub fn build_unsigned_vtxos<'a>(&'a self) -> impl Iterator<Item = Vtxo<Full>> + 'a {
403 self.builders.iter()
404 .map(|b| b.build_unsigned_vtxos())
405 .flatten()
406 }
407
408 pub fn build_unsigned_internal_vtxos<'a>(&'a self) -> impl Iterator<Item = ServerVtxo<Full>> + 'a {
413 self.builders.iter()
414 .map(|b| b.build_unsigned_internal_vtxos())
415 .flatten()
416 }
417
418 pub fn spend_info<'a>(&'a self) -> impl Iterator<Item = (VtxoId, Txid)> + 'a {
421 self.builders.iter()
422 .map(|b| b.spend_info())
423 .flatten()
424 }
425
426 pub fn virtual_transactions<'a>(&'a self) -> impl Iterator<Item = Txid> + 'a {
427 self.builders.iter()
428 .flat_map(|b| b.virtual_transactions())
429 }
430}
431
432#[cfg(test)]
433mod test {
434 use std::collections::{HashMap, HashSet};
435 use std::str::FromStr;
436
437 use bitcoin::{Transaction, Txid};
438 use bitcoin::secp256k1::Keypair;
439
440 use bitcoin_ext::P2TR_DUST;
441
442 use super::*;
443 use crate::test_util::dummy::DummyTestVtxoSpec;
444 use crate::PublicKey;
445
446 fn server_keypair() -> Keypair {
447 Keypair::from_str("f7a2a5d150afb575e98fff9caeebf6fbebbaeacfdfa7433307b208b39f1155f2").expect("Invalid key")
448 }
449
450 fn alice_keypair() -> Keypair {
451 Keypair::from_str("9b4382c8985f12e4bd8d1b51e63615bf0187843630829f4c5e9c45ef2cf994a4").expect("Invalid key")
452 }
453
454 fn bob_keypair() -> Keypair {
455 Keypair::from_str("c86435ba7e30d7afd7c5df9f3263ce2eb86b3ff9866a16ccd22a0260496ddf0f").expect("Invalid key")
456 }
457
458
459 fn alice_public_key() -> PublicKey {
460 alice_keypair().public_key()
461 }
462
463 fn bob_public_key() -> PublicKey {
464 bob_keypair().public_key()
465 }
466
467 fn dummy_vtxo_for_amount(amt: Amount) -> (Transaction, Vtxo<Full>) {
468 DummyTestVtxoSpec {
469 amount: amt + P2TR_DUST,
470 fee: P2TR_DUST,
471 expiry_height: 1000,
472 exit_delta: 128,
473 user_keypair: alice_keypair(),
474 server_keypair: server_keypair()
475 }.build()
476 }
477
478 fn verify_package_builder(
479 builder: ArkoorPackageBuilder<state::Initial>,
480 keypairs: &[Keypair],
481 funding_tx_map: HashMap<Txid, Transaction>,
482 ) {
483 let vtxs: Vec<Txid> = builder.virtual_transactions().collect();
485 let vtx_set: HashSet<Txid> = vtxs.iter().copied().collect();
486 let spend_txids: HashSet<Txid> = builder.spend_info().map(|(_, txid)| txid).collect();
487
488 assert_eq!(vtxs.len(), vtx_set.len(), "virtual_transactions() contains duplicates");
490
491 for txid in &vtx_set {
493 assert!(spend_txids.contains(txid), "virtual_transaction {} not in spend_info", txid);
494 }
495
496 for txid in &spend_txids {
498 assert!(vtx_set.contains(txid), "spend_info txid {} not in virtual_transactions", txid);
499 }
500
501 let user_builder = builder.generate_user_nonces(keypairs).expect("Valid nb of keypairs");
502 let cosign_requests = user_builder.cosign_request();
503
504 let cosign_responses = ArkoorPackageBuilder::from_cosign_request(cosign_requests)
505 .expect("Invalid cosign requests")
506 .server_cosign(&server_keypair())
507 .expect("Wrong server key")
508 .cosign_response();
509
510
511 let vtxos = user_builder.user_cosign(keypairs, cosign_responses)
512 .expect("Invalid cosign responses")
513 .build_signed_vtxos();
514
515 for vtxo in vtxos {
516 let funding_txid = vtxo.chain_anchor().txid;
517 let funding_tx = funding_tx_map.get(&funding_txid).expect("Funding tx not found");
518 vtxo.validate(&funding_tx).expect("Invalid vtxo");
519
520 let mut prev_tx = funding_tx.clone();
521 for tx in vtxo.transactions().map(|item| item.tx) {
522 crate::test_util::verify_tx(
523 &[prev_tx.output[vtxo.chain_anchor().vout as usize].clone()],
524 0,
525 &tx).expect("Invalid transaction");
526 prev_tx = tx;
527 }
528 }
529 }
530
531 #[test]
532 fn send_full_vtxo() {
533 let (funding_tx, alice_vtxo) = dummy_vtxo_for_amount(Amount::from_sat(100_000));
536
537 let package_builder = ArkoorPackageBuilder::new_single_output_with_checkpoints(
538 [alice_vtxo],
539 ArkoorDestination {
540 total_amount: Amount::from_sat(100_000),
541 policy: VtxoPolicy::new_pubkey(bob_public_key()),
542 },
543 VtxoPolicy::new_pubkey(alice_public_key())
544 ).expect("Valid package");
545
546 let funding_map = HashMap::from([(funding_tx.compute_txid(), funding_tx)]);
547 verify_package_builder(package_builder, &[alice_keypair()], funding_map);
548 }
549
550 #[test]
551 fn arkoor_dust_change() {
552 let (_funding_tx, alice_vtxo) = dummy_vtxo_for_amount(Amount::from_sat(1000));
556 let package_builder = ArkoorPackageBuilder::new_single_output_with_checkpoints(
557 [alice_vtxo],
558 ArkoorDestination {
559 total_amount: Amount::from_sat(900),
560 policy: VtxoPolicy::new_pubkey(bob_public_key()),
561 },
562 VtxoPolicy::new_pubkey(alice_public_key())
563 ).expect("Valid package");
564
565 let vtxos: Vec<Vtxo<Full>> = package_builder.build_unsigned_vtxos().collect();
567 assert_eq!(vtxos.len(), 3);
568 assert_eq!(vtxos[0].amount(), Amount::from_sat(670));
569 assert_eq!(vtxos[0].policy().user_pubkey(), bob_public_key());
570 assert_eq!(vtxos[1].amount(), Amount::from_sat(230));
571 assert_eq!(vtxos[1].policy().user_pubkey(), bob_public_key());
572 assert_eq!(vtxos[2].amount(), Amount::from_sat(100));
573 assert_eq!(vtxos[2].policy().user_pubkey(), alice_public_key());
574 }
575
576 #[test]
577 fn can_send_multiple_inputs() {
578 let (funding_tx_1, alice_vtxo_1) = dummy_vtxo_for_amount(Amount::from_sat(10_000));
581 let (funding_tx_2, alice_vtxo_2) = dummy_vtxo_for_amount(Amount::from_sat(5_000));
582 let (funding_tx_3, alice_vtxo_3) = dummy_vtxo_for_amount(Amount::from_sat(2_000));
583
584 let package = ArkoorPackageBuilder::new_single_output_with_checkpoints(
585 [alice_vtxo_1, alice_vtxo_2, alice_vtxo_3],
586 ArkoorDestination {
587 total_amount: Amount::from_sat(17_000),
588 policy: VtxoPolicy::new_pubkey(bob_public_key()),
589 },
590 VtxoPolicy::new_pubkey(alice_public_key())
591 ).expect("Valid package");
592
593 let vtxos: Vec<Vtxo<Full>> = package.build_unsigned_vtxos().collect();
594 assert_eq!(vtxos.len(), 3);
595 assert_eq!(vtxos[0].amount(), Amount::from_sat(10_000));
596 assert_eq!(vtxos[1].amount(), Amount::from_sat(5_000));
597 assert_eq!(vtxos[2].amount(), Amount::from_sat(2_000));
598 assert_eq!(
599 vtxos.iter().map(|v| v.policy().user_pubkey()).collect::<Vec<_>>(),
600 vec![bob_public_key(); 3],
601 );
602
603 let funding_map = HashMap::from([
604 (funding_tx_1.compute_txid(), funding_tx_1),
605 (funding_tx_2.compute_txid(), funding_tx_2),
606 (funding_tx_3.compute_txid(), funding_tx_3),
607 ]);
608 verify_package_builder(
609 package, &[alice_keypair(), alice_keypair(), alice_keypair()], funding_map,
610 );
611 }
612
613 #[test]
614 fn can_send_multiple_inputs_with_change() {
615 let (funding_tx_1, alice_vtxo_1) = dummy_vtxo_for_amount(Amount::from_sat(10_000));
619 let (funding_tx_2, alice_vtxo_2) = dummy_vtxo_for_amount(Amount::from_sat(5_000));
620 let (funding_tx_3, alice_vtxo_3) = dummy_vtxo_for_amount(Amount::from_sat(2_000));
621
622 let package = ArkoorPackageBuilder::new_single_output_with_checkpoints(
623 [alice_vtxo_1, alice_vtxo_2, alice_vtxo_3],
624 ArkoorDestination {
625 total_amount: Amount::from_sat(16_000),
626 policy: VtxoPolicy::new_pubkey(bob_public_key()),
627 },
628 VtxoPolicy::new_pubkey(alice_public_key())
629 ).expect("Valid package");
630
631 let vtxos: Vec<Vtxo<Full>> = package.build_unsigned_vtxos().collect();
632 assert_eq!(vtxos.len(), 4);
633 assert_eq!(vtxos[0].amount(), Amount::from_sat(10_000));
634 assert_eq!(vtxos[1].amount(), Amount::from_sat(5_000));
635 assert_eq!(vtxos[2].amount(), Amount::from_sat(1_000));
636 assert_eq!(vtxos[3].amount(), Amount::from_sat(1_000),
637 "Alice should receive a 1000 sats as change",
638 );
639
640 assert_eq!(vtxos[0].policy().user_pubkey(), bob_public_key());
641 assert_eq!(vtxos[1].policy().user_pubkey(), bob_public_key());
642 assert_eq!(vtxos[2].policy().user_pubkey(), bob_public_key());
643 assert_eq!(vtxos[3].policy().user_pubkey(), alice_public_key());
644
645 let funding_map = HashMap::from([
646 (funding_tx_1.compute_txid(), funding_tx_1),
647 (funding_tx_2.compute_txid(), funding_tx_2),
648 (funding_tx_3.compute_txid(), funding_tx_3),
649 ]);
650 verify_package_builder(
651 package, &[alice_keypair(), alice_keypair(), alice_keypair()], funding_map,
652 );
653 }
654
655 #[test]
656 fn can_send_multiple_vtxos_with_dust_change() {
657 let (_funding_tx_1, alice_vtxo_1) = dummy_vtxo_for_amount(Amount::from_sat(5_000));
661 let (_funding_tx_2, alice_vtxo_2) = dummy_vtxo_for_amount(Amount::from_sat(1_000));
662
663 let package = ArkoorPackageBuilder::new_single_output_with_checkpoints(
664 [alice_vtxo_1, alice_vtxo_2],
665 ArkoorDestination {
666 total_amount: Amount::from_sat(5_700),
667 policy: VtxoPolicy::new_pubkey(bob_public_key()),
668 },
669 VtxoPolicy::new_pubkey(alice_public_key())
670 ).expect("Valid package");
671
672 let vtxos: Vec<Vtxo<Full>> = package.build_unsigned_vtxos().collect();
673 assert_eq!(vtxos.len(), 4);
674 assert_eq!(vtxos[0].amount(), Amount::from_sat(5_000));
675 assert_eq!(vtxos[0].policy().user_pubkey(), bob_public_key());
676 assert_eq!(vtxos[1].amount(), Amount::from_sat(670));
677 assert_eq!(vtxos[1].policy().user_pubkey(), bob_public_key());
678 assert_eq!(vtxos[2].amount(), Amount::from_sat(30));
679 assert_eq!(vtxos[2].policy().user_pubkey(), bob_public_key());
680 assert_eq!(vtxos[3].amount(), Amount::from_sat(300));
681 assert_eq!(vtxos[3].policy().user_pubkey(), alice_public_key());
682 }
683
684 #[test]
685 fn not_enough_money() {
686 let (_funding_tx, alice_vtxo) = dummy_vtxo_for_amount(Amount::from_sat(900));
690 let result = ArkoorPackageBuilder::new_single_output_with_checkpoints(
691 [alice_vtxo],
692 ArkoorDestination {
693 total_amount: Amount::from_sat(1000),
694 policy: VtxoPolicy::new_pubkey(bob_public_key()),
695 },
696 VtxoPolicy::new_pubkey(alice_public_key())
697 );
698
699 match result {
700 Ok(_) => panic!("Package should be invalid"),
701 Err(ArkoorConstructionError::Unbalanced { input, output }) => {
702 assert_eq!(input, Amount::from_sat(900));
703 assert_eq!(output, Amount::from_sat(1000));
704 }
705 Err(e) => panic!("Unexpected error: {:?}", e),
706 }
707 }
708
709 #[test]
710 fn not_enough_money_with_multiple_inputs() {
711 let (_funding_tx, alice_vtxo_1) = dummy_vtxo_for_amount(Amount::from_sat(10_000));
715 let (_funding_tx, alice_vtxo_2) = dummy_vtxo_for_amount(Amount::from_sat(5_000));
716 let (_funding_tx, alice_vtxo_3) = dummy_vtxo_for_amount(Amount::from_sat(2_000));
717
718 let package = ArkoorPackageBuilder::new_single_output_with_checkpoints(
719 [alice_vtxo_1, alice_vtxo_2, alice_vtxo_3],
720 ArkoorDestination {
721 total_amount: Amount::from_sat(20_000),
722 policy: VtxoPolicy::new_pubkey(bob_public_key()),
723 },
724 VtxoPolicy::new_pubkey(alice_public_key())
725 );
726
727 match package {
728 Ok(_) => panic!("Package should be invalid"),
729 Err(ArkoorConstructionError::Unbalanced { input, output }) => {
730 assert_eq!(input, Amount::from_sat(17_000));
731 assert_eq!(output, Amount::from_sat(20_000));
732 }
733 Err(e) => panic!("Unexpected error: {:?}", e)
734 }
735 }
736
737 #[test]
738 fn can_use_all_provided_inputs_with_change() {
739 let (_funding_tx, alice_vtxo_1) = dummy_vtxo_for_amount(Amount::from_sat(1000));
744 let (_funding_tx, alice_vtxo_2) = dummy_vtxo_for_amount(Amount::from_sat(1000));
745 let (_funding_tx, alice_vtxo_3) = dummy_vtxo_for_amount(Amount::from_sat(1000));
746 let (_funding_tx, alice_vtxo_4) = dummy_vtxo_for_amount(Amount::from_sat(1000));
747
748 let package = ArkoorPackageBuilder::new_single_output_with_checkpoints(
749 [alice_vtxo_1, alice_vtxo_2, alice_vtxo_3, alice_vtxo_4],
750 ArkoorDestination {
751 total_amount: Amount::from_sat(2000),
752 policy: VtxoPolicy::new_pubkey(bob_public_key()),
753 },
754 VtxoPolicy::new_pubkey(alice_public_key())
755 ).expect("Package should be valid");
756
757 let vtxos = package.build_unsigned_vtxos().collect::<Vec<_>>();
759 let total_output = vtxos.iter().map(|v| v.amount()).sum::<Amount>();
760 assert_eq!(total_output, Amount::from_sat(4000));
761 }
762
763 #[test]
764 fn single_input_multiple_outputs() {
765 let (funding_tx, alice_vtxo) = dummy_vtxo_for_amount(Amount::from_sat(10_000));
767
768 let outputs = vec![
769 ArkoorDestination {
770 total_amount: Amount::from_sat(4_000),
771 policy: VtxoPolicy::new_pubkey(bob_public_key())
772 },
773 ArkoorDestination {
774 total_amount: Amount::from_sat(3_000),
775 policy: VtxoPolicy::new_pubkey(bob_public_key())
776 },
777 ArkoorDestination {
778 total_amount: Amount::from_sat(3_000),
779 policy: VtxoPolicy::new_pubkey(bob_public_key())
780 },
781 ];
782
783 let package = ArkoorPackageBuilder::new_with_checkpoints(
784 [alice_vtxo.clone()],
785 outputs,
786 ).expect("Valid package");
787
788 let vtxos: Vec<Vtxo<Full>> = package.build_unsigned_vtxos().collect();
789 assert_eq!(vtxos.len(), 3);
790 assert_eq!(vtxos[0].amount(), Amount::from_sat(4_000));
791 assert_eq!(vtxos[1].amount(), Amount::from_sat(3_000));
792 assert_eq!(vtxos[2].amount(), Amount::from_sat(3_000));
793
794 let user_keypair = alice_keypair();
796 let user_builder = package.generate_user_nonces(&[user_keypair])
797 .expect("Valid nb of keypairs");
798 let cosign_requests = user_builder.cosign_request();
799
800 let cosign_responses = ArkoorPackageBuilder::from_cosign_request(cosign_requests)
801 .expect("Invalid cosign requests")
802 .server_cosign(&server_keypair())
803 .expect("Wrong server key")
804 .cosign_response();
805
806 let signed_vtxos = user_builder.user_cosign(&[user_keypair], cosign_responses)
807 .expect("Invalid cosign responses")
808 .build_signed_vtxos();
809
810 assert_eq!(signed_vtxos.len(), 3, "Should create 3 signed vtxos");
811
812 signed_vtxos[0].validate(&funding_tx).expect("First vtxo should be valid");
814 }
815
816 #[test]
817 fn output_split_across_inputs() {
818 let (_funding_tx_1, alice_vtxo_1) = dummy_vtxo_for_amount(Amount::from_sat(600));
821 let (_funding_tx_2, alice_vtxo_2) = dummy_vtxo_for_amount(Amount::from_sat(500));
822
823 let outputs = vec![
824 ArkoorDestination {
825 total_amount: Amount::from_sat(800),
826 policy: VtxoPolicy::new_pubkey(bob_public_key())
827 },
828 ArkoorDestination {
829 total_amount: Amount::from_sat(300),
830 policy: VtxoPolicy::new_pubkey(bob_public_key())
831 },
832 ];
833
834 let package = ArkoorPackageBuilder::new_with_checkpoints(
835 [alice_vtxo_1, alice_vtxo_2],
836 outputs,
837 ).expect("Valid package");
838
839 let vtxos: Vec<Vtxo<Full>> = package.build_unsigned_vtxos().collect();
840 assert_eq!(vtxos.len(), 3);
841 assert_eq!(vtxos[0].amount(), Amount::from_sat(600));
842 assert_eq!(vtxos[0].policy().user_pubkey(), bob_public_key());
843 assert_eq!(vtxos[1].amount(), Amount::from_sat(200));
844 assert_eq!(vtxos[1].policy().user_pubkey(), bob_public_key());
845 assert_eq!(vtxos[2].amount(), Amount::from_sat(300));
846 assert_eq!(vtxos[2].policy().user_pubkey(), bob_public_key());
847 }
848
849 #[test]
850 fn dust_splits_allowed() {
851 let (_funding_tx_1, alice_vtxo_1) = dummy_vtxo_for_amount(Amount::from_sat(500));
854 let (_funding_tx_2, alice_vtxo_2) = dummy_vtxo_for_amount(Amount::from_sat(500));
855
856 let outputs = vec![
857 ArkoorDestination {
858 total_amount: Amount::from_sat(750),
859 policy: VtxoPolicy::new_pubkey(bob_public_key())
860 },
861 ArkoorDestination {
862 total_amount: Amount::from_sat(250),
863 policy: VtxoPolicy::new_pubkey(bob_public_key())
864 },
865 ];
866
867 let package = ArkoorPackageBuilder::new_with_checkpoints(
868 [alice_vtxo_1, alice_vtxo_2],
869 outputs,
870 ).expect("Valid package");
871
872 let vtxos: Vec<Vtxo<Full>> = package.build_unsigned_vtxos().collect();
873 assert_eq!(vtxos.len(), 3);
874 assert_eq!(vtxos[0].amount(), Amount::from_sat(500));
875 assert_eq!(vtxos[1].amount(), Amount::from_sat(250)); assert_eq!(vtxos[2].amount(), Amount::from_sat(250));
877 }
878
879 #[test]
880 fn unbalanced_amounts_rejected() {
881 let (_funding_tx, alice_vtxo) = dummy_vtxo_for_amount(Amount::from_sat(1000));
883
884 let outputs = vec![
885 ArkoorDestination {
886 total_amount: Amount::from_sat(600),
887 policy: VtxoPolicy::new_pubkey(bob_public_key())
888 },
889 ArkoorDestination {
890 total_amount: Amount::from_sat(600),
891 policy: VtxoPolicy::new_pubkey(bob_public_key())
892 },
893 ];
894
895 let result = ArkoorPackageBuilder::new_with_checkpoints(
896 [alice_vtxo],
897 outputs,
898 );
899
900 match result {
901 Err(ArkoorConstructionError::Unbalanced { input, output }) => {
902 assert_eq!(input, Amount::from_sat(1000));
903 assert_eq!(output, Amount::from_sat(1200));
904 }
905 _ => panic!("Expected Unbalanced error"),
906 }
907 }
908
909 #[test]
910 fn empty_outputs_rejected() {
911 let (_funding_tx, alice_vtxo) = dummy_vtxo_for_amount(Amount::from_sat(1000));
912
913 let result = ArkoorPackageBuilder::new_with_checkpoints(
914 [alice_vtxo],
915 vec![],
916 );
917
918 match result {
919 Err(ArkoorConstructionError::NoOutputs) => {}
920 Err(e) => panic!("Expected NoOutputs error, got: {:?}", e),
921 Ok(_) => panic!("Expected NoOutputs error, got Ok"),
922 }
923 }
924
925 #[test]
926 fn multiple_inputs_multiple_outputs_exact_balance() {
927 let (_funding_tx_1, alice_vtxo_1) = dummy_vtxo_for_amount(Amount::from_sat(1000));
929 let (_funding_tx_2, alice_vtxo_2) = dummy_vtxo_for_amount(Amount::from_sat(2000));
930 let (_funding_tx_3, alice_vtxo_3) = dummy_vtxo_for_amount(Amount::from_sat(1500));
931
932 let outputs = vec![
933 ArkoorDestination {
934 total_amount: Amount::from_sat(2500),
935 policy: VtxoPolicy::new_pubkey(bob_public_key())
936 },
937 ArkoorDestination {
938 total_amount: Amount::from_sat(2000),
939 policy: VtxoPolicy::new_pubkey(bob_public_key())
940 },
941 ];
942
943 let package = ArkoorPackageBuilder::new_with_checkpoints(
944 [alice_vtxo_1, alice_vtxo_2, alice_vtxo_3],
945 outputs,
946 ).expect("Valid package");
947
948 let vtxos: Vec<Vtxo<Full>> = package.build_unsigned_vtxos().collect();
949 assert_eq!(vtxos.len(), 4);
950 assert_eq!(vtxos[0].amount(), Amount::from_sat(1000));
954 assert_eq!(vtxos[1].amount(), Amount::from_sat(1500));
955 assert_eq!(vtxos[2].amount(), Amount::from_sat(500));
956 assert_eq!(vtxos[3].amount(), Amount::from_sat(1500));
957 }
958
959 #[test]
960 fn single_output_across_many_inputs() {
961 let (_funding_tx_1, alice_vtxo_1) = dummy_vtxo_for_amount(Amount::from_sat(100));
964 let (_funding_tx_2, alice_vtxo_2) = dummy_vtxo_for_amount(Amount::from_sat(100));
965 let (_funding_tx_3, alice_vtxo_3) = dummy_vtxo_for_amount(Amount::from_sat(100));
966 let (_funding_tx_4, alice_vtxo_4) = dummy_vtxo_for_amount(Amount::from_sat(100));
967
968 let outputs = vec![
969 ArkoorDestination {
970 total_amount: Amount::from_sat(400),
971 policy: VtxoPolicy::new_pubkey(bob_public_key())
972 },
973 ];
974
975 let package = ArkoorPackageBuilder::new_with_checkpoints(
976 [alice_vtxo_1, alice_vtxo_2, alice_vtxo_3, alice_vtxo_4],
977 outputs,
978 ).expect("Valid package");
979
980 let vtxos: Vec<Vtxo<Full>> = package.build_unsigned_vtxos().collect();
981 assert_eq!(vtxos.len(), 4);
982 assert_eq!(vtxos[0].amount(), Amount::from_sat(100));
983 assert_eq!(vtxos[1].amount(), Amount::from_sat(100));
984 assert_eq!(vtxos[2].amount(), Amount::from_sat(100));
985 assert_eq!(vtxos[3].amount(), Amount::from_sat(100));
986 let total: Amount = vtxos.iter().map(|v| v.amount()).sum();
987 assert_eq!(total, Amount::from_sat(400));
988 }
989
990 #[test]
991 fn many_outputs_from_single_input() {
992 let (_funding_tx, alice_vtxo) = dummy_vtxo_for_amount(Amount::from_sat(1000));
994
995 let outputs = vec![
996 ArkoorDestination {
997 total_amount: Amount::from_sat(100),
998 policy: VtxoPolicy::new_pubkey(bob_public_key())
999 },
1000 ArkoorDestination {
1001 total_amount: Amount::from_sat(200),
1002 policy: VtxoPolicy::new_pubkey(bob_public_key())
1003 },
1004 ArkoorDestination {
1005 total_amount: Amount::from_sat(150),
1006 policy: VtxoPolicy::new_pubkey(bob_public_key())
1007 },
1008 ArkoorDestination {
1009 total_amount: Amount::from_sat(250),
1010 policy: VtxoPolicy::new_pubkey(bob_public_key())
1011 },
1012 ArkoorDestination {
1013 total_amount: Amount::from_sat(300),
1014 policy: VtxoPolicy::new_pubkey(bob_public_key())
1015 },
1016 ];
1017
1018 let package = ArkoorPackageBuilder::new_with_checkpoints(
1019 [alice_vtxo],
1020 outputs,
1021 ).expect("Valid package");
1022
1023 let vtxos: Vec<Vtxo<Full>> = package.build_unsigned_vtxos().collect();
1024 assert_eq!(vtxos.len(), 5);
1025 assert_eq!(vtxos[0].amount(), Amount::from_sat(100));
1026 assert_eq!(vtxos[1].amount(), Amount::from_sat(200));
1027 assert_eq!(vtxos[2].amount(), Amount::from_sat(150));
1028 assert_eq!(vtxos[3].amount(), Amount::from_sat(250));
1029 assert_eq!(vtxos[4].amount(), Amount::from_sat(300));
1030 }
1031
1032 #[test]
1033 fn first_input_exactly_matches_first_output() {
1034 let (_funding_tx_1, alice_vtxo_1) = dummy_vtxo_for_amount(Amount::from_sat(1000));
1037 let (_funding_tx_2, alice_vtxo_2) = dummy_vtxo_for_amount(Amount::from_sat(500));
1038
1039 let outputs = vec![
1040 ArkoorDestination {
1041 total_amount: Amount::from_sat(1000),
1042 policy: VtxoPolicy::new_pubkey(bob_public_key())
1043 },
1044 ArkoorDestination {
1045 total_amount: Amount::from_sat(500),
1046 policy: VtxoPolicy::new_pubkey(bob_public_key())
1047 },
1048 ];
1049
1050 let package = ArkoorPackageBuilder::new_with_checkpoints(
1051 [alice_vtxo_1, alice_vtxo_2],
1052 outputs,
1053 ).expect("Valid package");
1054
1055 let vtxos: Vec<Vtxo<Full>> = package.build_unsigned_vtxos().collect();
1056 assert_eq!(vtxos.len(), 2);
1057 assert_eq!(vtxos[0].amount(), Amount::from_sat(1000));
1058 assert_eq!(vtxos[1].amount(), Amount::from_sat(500));
1059 }
1060
1061 #[test]
1062 fn empty_inputs_rejected() {
1063 let outputs = vec![
1065 ArkoorDestination {
1066 total_amount: Amount::from_sat(1000),
1067 policy: VtxoPolicy::new_pubkey(bob_public_key())
1068 },
1069 ];
1070
1071 let result = ArkoorPackageBuilder::new_with_checkpoints(
1072 Vec::<Vtxo<Full>>::new(),
1073 outputs,
1074 );
1075
1076 match result {
1077 Ok(_) => panic!("Should reject empty inputs"),
1078 Err(ArkoorConstructionError::Unbalanced { input, output }) => {
1079 assert_eq!(input, Amount::ZERO);
1080 assert_eq!(output, Amount::from_sat(1000));
1081 }
1082 Err(e) => panic!("Unexpected error: {:?}", e),
1083 }
1084 }
1085
1086 #[test]
1087 fn alternating_split_pattern() {
1088 let (_funding_tx_1, alice_vtxo_1) = dummy_vtxo_for_amount(Amount::from_sat(300));
1093 let (_funding_tx_2, alice_vtxo_2) = dummy_vtxo_for_amount(Amount::from_sat(700));
1094 let (_funding_tx_3, alice_vtxo_3) = dummy_vtxo_for_amount(Amount::from_sat(500));
1095
1096 let outputs = vec![
1097 ArkoorDestination {
1098 total_amount: Amount::from_sat(500),
1099 policy: VtxoPolicy::new_pubkey(bob_public_key())
1100 },
1101 ArkoorDestination {
1102 total_amount: Amount::from_sat(400),
1103 policy: VtxoPolicy::new_pubkey(bob_public_key())
1104 },
1105 ArkoorDestination {
1106 total_amount: Amount::from_sat(600),
1107 policy: VtxoPolicy::new_pubkey(bob_public_key())
1108 },
1109 ];
1110
1111 let package = ArkoorPackageBuilder::new_with_checkpoints(
1112 [alice_vtxo_1, alice_vtxo_2, alice_vtxo_3],
1113 outputs,
1114 ).expect("Valid package");
1115
1116 let vtxos: Vec<Vtxo<Full>> = package.build_unsigned_vtxos().collect();
1117 assert_eq!(vtxos.len(), 5);
1118 assert_eq!(vtxos[0].amount(), Amount::from_sat(300));
1120 assert_eq!(vtxos[1].amount(), Amount::from_sat(200));
1122 assert_eq!(vtxos[2].amount(), Amount::from_sat(400));
1123 assert_eq!(vtxos[3].amount(), Amount::from_sat(100));
1124 assert_eq!(vtxos[4].amount(), Amount::from_sat(500));
1126 let total: Amount = vtxos.iter().map(|v| v.amount()).sum();
1127 assert_eq!(total, Amount::from_sat(1500));
1128 }
1129
1130 #[test]
1131 fn spend_info_correctness_simple_checkpoint() {
1132 let (_funding_tx, alice_vtxo) = dummy_vtxo_for_amount(
1134 Amount::from_sat(100_000)
1135 );
1136 let input_id = alice_vtxo.id();
1137
1138 let package = ArkoorPackageBuilder::new_single_output_with_checkpoints(
1139 [alice_vtxo],
1140 ArkoorDestination {
1141 total_amount: Amount::from_sat(100_000),
1142 policy: VtxoPolicy::new_pubkey(bob_public_key()),
1143 },
1144 VtxoPolicy::new_pubkey(alice_public_key())
1145 ).expect("Valid package");
1146
1147 let internal_vtxos: Vec<VtxoId> = package
1149 .build_unsigned_internal_vtxos()
1150 .map(|v| v.id())
1151 .collect();
1152
1153 let spend_info: Vec<(VtxoId, Txid)> = package.spend_info().collect();
1155
1156 let mut expected_vtxo_ids = vec![input_id];
1158 expected_vtxo_ids.extend(internal_vtxos.iter());
1159
1160 let actual_vtxo_ids: Vec<VtxoId> = spend_info
1161 .iter()
1162 .map(|(id, _)| *id)
1163 .collect();
1164
1165 for id in &expected_vtxo_ids {
1167 assert!(
1168 actual_vtxo_ids.contains(id),
1169 "Expected VTXO ID {} not found in spend_info",
1170 id
1171 );
1172 }
1173
1174 assert_eq!(
1176 actual_vtxo_ids.len(),
1177 expected_vtxo_ids.len(),
1178 "spend_info contains unexpected entries"
1179 );
1180 }
1181
1182 #[test]
1183 fn spend_info_correctness_with_dust_isolation() {
1184 let (_funding_tx, alice_vtxo) = dummy_vtxo_for_amount(
1186 Amount::from_sat(1000)
1187 );
1188 let input_id = alice_vtxo.id();
1189
1190 let package = ArkoorPackageBuilder::new_single_output_with_checkpoints(
1191 [alice_vtxo],
1192 ArkoorDestination {
1193 total_amount: Amount::from_sat(900),
1194 policy: VtxoPolicy::new_pubkey(bob_public_key()),
1195 },
1196 VtxoPolicy::new_pubkey(alice_public_key())
1197 ).expect("Valid package");
1198
1199 let internal_vtxos: Vec<VtxoId> = package
1201 .build_unsigned_internal_vtxos()
1202 .map(|v| v.id())
1203 .collect();
1204
1205 let spend_info: Vec<(VtxoId, Txid)> = package.spend_info().collect();
1207
1208 let mut expected_vtxo_ids = vec![input_id];
1210 expected_vtxo_ids.extend(internal_vtxos.iter());
1211
1212 let actual_vtxo_ids: Vec<VtxoId> = spend_info
1213 .iter()
1214 .map(|(id, _)| *id)
1215 .collect();
1216
1217 for id in &expected_vtxo_ids {
1219 assert!(
1220 actual_vtxo_ids.contains(id),
1221 "Expected VTXO ID {} not found in spend_info",
1222 id
1223 );
1224 }
1225
1226 assert_eq!(
1228 actual_vtxo_ids.len(),
1229 expected_vtxo_ids.len(),
1230 "spend_info contains unexpected entries"
1231 );
1232 }
1233
1234 #[test]
1235 fn spend_info_correctness_without_checkpoints() {
1236 let (_funding_tx, alice_vtxo) = dummy_vtxo_for_amount(
1238 Amount::from_sat(100_000)
1239 );
1240 let input_id = alice_vtxo.id();
1241
1242 let package = ArkoorPackageBuilder::new_without_checkpoints(
1243 [alice_vtxo],
1244 vec![
1245 ArkoorDestination {
1246 total_amount: Amount::from_sat(100_000),
1247 policy: VtxoPolicy::new_pubkey(bob_public_key()),
1248 }
1249 ]
1250 ).expect("Valid package");
1251
1252 let internal_vtxos: Vec<VtxoId> = package
1254 .build_unsigned_internal_vtxos()
1255 .map(|v| v.id())
1256 .collect();
1257
1258 let spend_info: Vec<(VtxoId, Txid)> = package.spend_info().collect();
1260
1261 let mut expected_vtxo_ids = vec![input_id];
1263 expected_vtxo_ids.extend(internal_vtxos.iter());
1264
1265 let actual_vtxo_ids: Vec<VtxoId> = spend_info
1266 .iter()
1267 .map(|(id, _)| *id)
1268 .collect();
1269
1270 for id in &expected_vtxo_ids {
1272 assert!(
1273 actual_vtxo_ids.contains(id),
1274 "Expected VTXO ID {} not found in spend_info",
1275 id
1276 );
1277 }
1278
1279 assert_eq!(
1281 actual_vtxo_ids.len(),
1282 expected_vtxo_ids.len(),
1283 "spend_info contains unexpected entries"
1284 );
1285 }
1286
1287 #[test]
1288 fn spend_info_correctness_multiple_inputs() {
1289 let (_funding_tx_1, alice_vtxo_1) = dummy_vtxo_for_amount(
1291 Amount::from_sat(10_000)
1292 );
1293 let (_funding_tx_2, alice_vtxo_2) = dummy_vtxo_for_amount(
1294 Amount::from_sat(5_000)
1295 );
1296 let (_funding_tx_3, alice_vtxo_3) = dummy_vtxo_for_amount(
1297 Amount::from_sat(2_000)
1298 );
1299
1300 let input_ids = vec![
1301 alice_vtxo_1.id(),
1302 alice_vtxo_2.id(),
1303 alice_vtxo_3.id(),
1304 ];
1305
1306 let package = ArkoorPackageBuilder::new_single_output_with_checkpoints(
1307 [alice_vtxo_1, alice_vtxo_2, alice_vtxo_3],
1308 ArkoorDestination {
1309 total_amount: Amount::from_sat(16_000),
1310 policy: VtxoPolicy::new_pubkey(bob_public_key()),
1311 },
1312 VtxoPolicy::new_pubkey(alice_public_key())
1313 ).expect("Valid package");
1314
1315 let internal_vtxos: Vec<VtxoId> = package
1317 .build_unsigned_internal_vtxos()
1318 .map(|v| v.id())
1319 .collect();
1320
1321 let spend_info: Vec<(VtxoId, Txid)> = package.spend_info().collect();
1323
1324 let mut expected_vtxo_ids = input_ids.clone();
1326 expected_vtxo_ids.extend(internal_vtxos.iter());
1327
1328 let actual_vtxo_ids: Vec<VtxoId> = spend_info
1329 .iter()
1330 .map(|(id, _)| *id)
1331 .collect();
1332
1333 for id in &expected_vtxo_ids {
1335 assert!(
1336 actual_vtxo_ids.contains(id),
1337 "Expected VTXO ID {} not found in spend_info",
1338 id
1339 );
1340 }
1341
1342 assert_eq!(
1344 actual_vtxo_ids.len(),
1345 expected_vtxo_ids.len(),
1346 "spend_info contains unexpected entries"
1347 );
1348 }
1349}