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