iota_genesis_builder/stardust/process_outputs.rs
1// Copyright (c) 2024 IOTA Stiftung
2// SPDX-License-Identifier: Apache-2.0
3
4use std::{
5 cmp::Ordering,
6 collections::{BTreeMap, BTreeSet},
7};
8
9use anyhow::{Result, anyhow};
10use fastcrypto::{encoding::Hex, hash::HashFunction};
11use iota_sdk::types::{
12 api::plugins::participation::types::PARTICIPATION_TAG,
13 block::{
14 address::{Address, Ed25519Address},
15 output::{
16 AliasOutputBuilder, BasicOutput, BasicOutputBuilder, FoundryOutputBuilder,
17 NftOutputBuilder, OUTPUT_INDEX_MAX, Output, OutputId,
18 feature::SenderFeature,
19 unlock_condition::{AddressUnlockCondition, StorageDepositReturnUnlockCondition},
20 },
21 payload::transaction::TransactionId,
22 },
23};
24use iota_types::{
25 base_types::IotaAddress,
26 crypto::DefaultHash,
27 timelock::timelock::{VESTED_REWARD_ID_PREFIX, is_vested_reward},
28};
29use tracing::debug;
30
31use super::types::{
32 address_swap_split_map::AddressSwapSplitMap, output_header::OutputHeader,
33 output_index::OutputIndex,
34};
35
36/// Processes an iterator of outputs coming from a Hornet snapshot chaining some
37/// filters:
38/// - the `ScaleIotaAmountIterator` scales balances of IOTA Tokens from micro to
39/// nano.
40/// - the `UnlockedVestingIterator` takes vesting outputs that can be unlocked
41/// and merges them into a unique basic output.
42/// - the `ParticipationOutputFilter` removes all features from the basic
43/// outputs with a participation tag.
44/// - the `SwapSplitIterator` performs the operation of SwapSplit given a map as
45/// input, i.e., for certain origin addresses it swaps the addressUC to a
46/// destination address and splits some amounts of tokens and/or timelocked
47/// tokens (this operation can be done for several destinations).
48pub fn process_outputs_for_iota<'a>(
49 target_milestone_timestamp: u32,
50 swap_split_map: AddressSwapSplitMap,
51 outputs: impl Iterator<Item = Result<(OutputHeader, Output)>> + 'a,
52) -> impl Iterator<Item = Result<(OutputHeader, Output), anyhow::Error>> + 'a {
53 // Create the iterator with the filters needed for an IOTA snapshot
54 outputs
55 .scale_iota_amount()
56 .filter_unlocked_vesting_outputs(target_milestone_timestamp)
57 .filter_participation_outputs()
58 .perform_swap_split(swap_split_map)
59 .map(|res| {
60 let (header, output) = res?;
61 Ok((header, output))
62 })
63}
64
65/// Take an `amount` and scale it by a multiplier defined for the IOTA token.
66pub fn scale_amount_for_iota(amount: u64) -> Result<u64> {
67 const IOTA_MULTIPLIER: u64 = 1000;
68
69 amount
70 .checked_mul(IOTA_MULTIPLIER)
71 .ok_or_else(|| anyhow!("overflow multiplying amount {amount} by {IOTA_MULTIPLIER}"))
72}
73
74// Check if the output is basic and has a feature Tag using the Participation
75// Tag: https://github.com/iota-community/treasury/blob/main/specifications/hornet-participation-plugin.md
76pub fn is_participation_output(output: &Output) -> bool {
77 if let Some(feat) = output.features() {
78 if output.is_basic() && !feat.is_empty() {
79 if let Some(tag) = feat.tag() {
80 return tag.to_string() == Hex::encode_with_format(PARTICIPATION_TAG);
81 };
82 }
83 };
84 false
85}
86
87/// Iterator that modifies some outputs address unlocked by certain origin
88/// addresses found in the `swap_split_map`. For each origin address there can
89/// be a set of destination addresses. Each destination address as either a
90/// tokens target, a tokens timelocked target or both. So, each output found by
91/// this filter with an address unlock condition being the origin address, a
92/// SwapSplit operation is performed. This operation consists in splitting the
93/// output in different outputs given the targets indicated for the destinations
94/// and swapping the address unlock condition to be the destination address one.
95/// This operation is performed on all basic outputs and (vesting) timelocked
96/// basic outputs until the targets are reached.
97struct SwapSplitIterator<I> {
98 /// Iterator over `(OutputHeader, Output)` pairs.
99 outputs: I,
100 /// Map used for the SwapSplit operation. It associates an origin address to
101 /// a vector of destinations. A destination is a tuple containing a
102 /// destination address, a tokens target and a timelocked tokens target.
103 swap_split_map: AddressSwapSplitMap,
104 /// Basic outputs with timelock unlock conditions. These are candidate
105 /// outputs that are kept in ascending order of timestamp and, when the
106 /// iteration over all outputs has finished, some of them will be popped
107 /// to be picked for the SwapSplit operation.
108 timelock_candidates: BTreeSet<TimelockOrderedOutput>,
109 /// Basic outputs that have been split during the processing. These can be
110 /// either basic outputs or (vesting) timelocked basic outputs that will
111 /// be added as new in the ledger, before the migration.
112 split_basic_outputs: Vec<(OutputHeader, Output)>,
113 num_swapped_basic: u64,
114 num_swapped_timelocks: u64,
115 num_splits: u64,
116}
117
118impl<I> SwapSplitIterator<I> {
119 fn new(outputs: I, swap_split_map: AddressSwapSplitMap) -> Self {
120 Self {
121 outputs,
122 swap_split_map,
123 timelock_candidates: Default::default(),
124 split_basic_outputs: Default::default(),
125 num_swapped_basic: 0,
126 num_swapped_timelocks: 0,
127 num_splits: 0,
128 }
129 }
130
131 /// Pop an output from `split_basic_outputs`. Since this contains newly
132 /// created outputs, there is the need to create a new OutputHeader that
133 /// is not in conflict with any other one in the ledger. Use some data
134 /// coming from the original output header plus some unique information
135 /// about the new output.
136 fn get_split_output(&mut self) -> Option<(OutputHeader, Output)> {
137 let (original_header, output) = self.split_basic_outputs.pop()?;
138 self.num_splits += 1;
139 let pos = self.split_basic_outputs.len();
140
141 let (transaction_id, output_index) = if original_header
142 .output_id()
143 .to_string()
144 .starts_with(VESTED_REWARD_ID_PREFIX)
145 {
146 // If the original basic output is a vesting output, generate the new OutputId
147 // as: original-transaction-id|index
148 // where index is a unique input
149 // index being a number in the range 1 to OUTPUT_INDEX_MAX is safe because
150 // vesting output indexes are always 0
151 // https://github.com/iotaledger/snapshot-tool-new-supply
152 if original_header.output_id().index() != 0 {
153 debug!(
154 "Found a vesting output with output index different than 0: {}",
155 original_header.output_id()
156 );
157 }
158 let index = 1 + (pos as u16 % (OUTPUT_INDEX_MAX - 1));
159 (
160 *original_header.output_id().transaction_id(),
161 OutputIndex::new(index).unwrap(),
162 )
163 } else {
164 // Otherwise, generate the new OutputId as:
165 // DefaultHash("iota-genesis-outputs"|original-output-id|pos)|index
166 // where pos is a unique input
167 let index = pos as u16 % OUTPUT_INDEX_MAX;
168 let mut hasher = DefaultHash::default();
169 hasher.update(b"iota-genesis-outputs");
170 hasher.update(original_header.output_id().hash());
171 hasher.update(pos.to_le_bytes());
172 let hash = hasher.finalize();
173 (
174 TransactionId::new(hash.into()),
175 OutputIndex::new(index).unwrap(),
176 )
177 };
178
179 Some((
180 OutputHeader::new(
181 *transaction_id,
182 output_index,
183 *original_header.block_id(),
184 *original_header.ms_index(),
185 original_header.ms_timestamp(),
186 ),
187 output,
188 ))
189 }
190}
191
192impl<I> Iterator for SwapSplitIterator<I>
193where
194 I: Iterator<Item = Result<(OutputHeader, Output)>>,
195{
196 type Item = I::Item;
197
198 /// Get the next from the chained self.outputs iterator and apply the
199 /// SwapSplit filter if that's the case.
200 fn next(&mut self) -> Option<Self::Item> {
201 for mut output in self.outputs.by_ref() {
202 if let Ok((header, inner)) = &mut output {
203 if let Output::Basic(ref basic_output) = inner {
204 let uc = basic_output.unlock_conditions();
205 // Only for outputs with timelock and/or address unlock conditions (and not
206 // holding native tokens) the SwapSplit operation can be performed
207 if uc.storage_deposit_return().is_none()
208 && uc.expiration().is_none()
209 && basic_output.native_tokens().is_empty()
210 {
211 // Now check if the addressUC's address is to swap
212 if let Some(destinations) = self
213 .swap_split_map
214 .get_destination_maybe_mut(uc.address().unwrap().address())
215 {
216 if uc.timelock().is_some() {
217 // If the output has a timelock UC (and it is a vested reward) and
218 // at least one destination requires some timelocked tokens, then
219 // store it as a candidate and continue with the iterator
220 if is_vested_reward(header.output_id(), basic_output)
221 && destinations.contains_tokens_timelocked_target()
222 {
223 // Here we store all the timelocked basic outputs we find,
224 // because we need all the ones owned by the origin address
225 // sorted by the unlocking timestamp; outside this loop,
226 // i.e., once all have been collected, we'll start the
227 // SwapSplit operation in order, starting from the one that
228 // unlocks later in time.
229 self.timelock_candidates.insert(TimelockOrderedOutput {
230 header: header.clone(),
231 output: inner.clone(),
232 });
233 continue;
234 }
235 } else {
236 // If it is just a basic output, try to perform the SwapSplit
237 // operation for several destinations once all tokens targets are
238 // meet.
239 let (original_output_opt, split_outputs) = swap_split_operation(
240 destinations.iter_by_tokens_target_mut_filtered(),
241 basic_output,
242 );
243 // If some SwapSplit were performed, their result are basic inputs
244 // stored in split_outputs; so, we save them in
245 // split_basic_outputs to return them later
246 if !split_outputs.is_empty() {
247 self.num_swapped_basic += 1;
248 }
249 self.split_basic_outputs.extend(
250 split_outputs
251 .into_iter()
252 .map(|output| (header.clone(), output)),
253 );
254 // If there was a remainder, the original output is returned for the
255 // iterator, possibly with a modified amount; else, continue the
256 // loop
257 if let Some(original_output) = original_output_opt {
258 *inner = original_output;
259 } else {
260 continue;
261 }
262 };
263 }
264 }
265 }
266 }
267 return Some(output);
268 }
269 // Now that we are out of the loop we collect the processed outputs from the
270 // timelock filter and try to fulfill the target.
271 // First, resolve timelocks SwapSplit operations, taking those out from
272 // timelocks; the ordered_timelock_candidates is ordered by timestamp
273 // and we want to take the latest ones first.
274 while let Some(TimelockOrderedOutput { header, output }) =
275 self.timelock_candidates.pop_last()
276 {
277 // We know that all of them are timelocked basic outputs
278 let timelocked_basic_output = output.as_basic();
279 let uc = timelocked_basic_output.unlock_conditions();
280 // Get destination address and mutable timelocked tokens target
281 let destinations = self
282 .swap_split_map
283 .get_destination_maybe_mut(uc.address().unwrap().address())
284 .expect("ordered timelock candidates should be part of the swap map");
285
286 // Try to perform the SwapSplit operation for several destinations once all
287 // tokens timelocked targets are met
288 let (original_output_opt, split_outputs) = swap_split_operation(
289 destinations.iter_by_tokens_timelocked_target_mut_filtered(),
290 timelocked_basic_output,
291 );
292 // If some SwapSplit were performed, their result are timelocked basic inputs
293 // stored in split_outputs; so, we save them in
294 // split_basic_outputs to return them later
295 if !split_outputs.is_empty() {
296 self.num_swapped_timelocks += 1;
297 }
298 self.split_basic_outputs.extend(
299 split_outputs
300 .into_iter()
301 .map(|output| (header.clone(), output)),
302 );
303 // If there was a remainder, the original output is returned for the
304 // iterator, possibly with a modified amount; otherwise, continue the
305 // loop
306 if let Some(original_output) = original_output_opt {
307 return Some(Ok((header, original_output)));
308 } else {
309 continue;
310 }
311 }
312 // Second, return all the remaining split outputs generated suring SwapSplit
313 // operations
314 Some(Ok(self.get_split_output()?))
315 }
316}
317
318impl<I> Drop for SwapSplitIterator<I> {
319 fn drop(&mut self) {
320 if let Some((origin, destination, tokens_target, tokens_timelocked_target)) =
321 self.swap_split_map.validate_successful_swap_split()
322 {
323 panic!(
324 "For at least one address, the SwapSplit operation was not fully performed. Origin: {}, destination: {}, tokens left: {}, timelocked tokens left: {}",
325 origin, destination, tokens_target, tokens_timelocked_target
326 )
327 }
328 debug!(
329 "Number of basic outputs used for a SwapSplit (no timelock): {}",
330 self.num_swapped_basic
331 );
332 debug!(
333 "Number of timelocked basic outputs used for a SwapSplit: {}",
334 self.num_swapped_timelocks
335 );
336 debug!("Number of outputs created with splits: {}", self.num_splits);
337 }
338}
339
340/// Iterator that modifies the amount of IOTA tokens for any output, scaling the
341/// amount from micros to nanos.
342struct ScaleIotaAmountIterator<I> {
343 /// Iterator over `(OutputHeader, Output)` pairs.
344 outputs: I,
345 num_scaled_outputs: u64,
346}
347
348impl<I> ScaleIotaAmountIterator<I> {
349 fn new(outputs: I) -> Self {
350 Self {
351 outputs,
352 num_scaled_outputs: 0,
353 }
354 }
355}
356
357impl<I> Iterator for ScaleIotaAmountIterator<I>
358where
359 I: Iterator<Item = Result<(OutputHeader, Output)>>,
360{
361 type Item = I::Item;
362
363 /// Get the next from the chained self.outputs iterator and always apply the
364 /// scaling (only an Output::Treasury kind is left out)
365 fn next(&mut self) -> Option<Self::Item> {
366 let mut output = self.outputs.next()?;
367 if let Ok((_, inner)) = &mut output {
368 self.num_scaled_outputs += 1;
369 match inner {
370 Output::Basic(ref basic_output) => {
371 // Update amount
372 let mut builder = BasicOutputBuilder::from(basic_output).with_amount(
373 scale_amount_for_iota(basic_output.amount())
374 .expect("should scale the amount for iota"),
375 );
376 // Update amount in potential storage deposit return unlock condition
377 if let Some(sdr_uc) = basic_output
378 .unlock_conditions()
379 .get(StorageDepositReturnUnlockCondition::KIND)
380 {
381 let sdr_uc = sdr_uc.as_storage_deposit_return();
382 builder = builder.replace_unlock_condition(
383 StorageDepositReturnUnlockCondition::new(
384 sdr_uc.return_address(),
385 scale_amount_for_iota(sdr_uc.amount())
386 .expect("should scale the amount for iota"),
387 u64::MAX,
388 )
389 .unwrap(),
390 );
391 };
392 *inner = builder
393 .finish()
394 .expect("failed to create basic output")
395 .into()
396 }
397 Output::Alias(ref alias_output) => {
398 *inner = AliasOutputBuilder::from(alias_output)
399 .with_amount(
400 scale_amount_for_iota(alias_output.amount())
401 .expect("should scale the amount for iota"),
402 )
403 .finish()
404 .expect("should be able to create an alias output")
405 .into()
406 }
407 Output::Foundry(ref foundry_output) => {
408 *inner = FoundryOutputBuilder::from(foundry_output)
409 .with_amount(
410 scale_amount_for_iota(foundry_output.amount())
411 .expect("should scale the amount for iota"),
412 )
413 .finish()
414 .expect("should be able to create a foundry output")
415 .into()
416 }
417 Output::Nft(ref nft_output) => {
418 // Update amount
419 let mut builder = NftOutputBuilder::from(nft_output).with_amount(
420 scale_amount_for_iota(nft_output.amount())
421 .expect("should scale the amount for iota"),
422 );
423 // Update amount in potential storage deposit return unlock condition
424 if let Some(sdr_uc) = nft_output
425 .unlock_conditions()
426 .get(StorageDepositReturnUnlockCondition::KIND)
427 {
428 let sdr_uc = sdr_uc.as_storage_deposit_return();
429 builder = builder.replace_unlock_condition(
430 StorageDepositReturnUnlockCondition::new(
431 sdr_uc.return_address(),
432 scale_amount_for_iota(sdr_uc.amount())
433 .expect("should scale the amount for iota"),
434 u64::MAX,
435 )
436 .unwrap(),
437 );
438 };
439 *inner = builder
440 .finish()
441 .expect("should be able to create an nft output")
442 .into();
443 }
444 Output::Treasury(_) => (),
445 }
446 }
447 Some(output)
448 }
449}
450
451impl<I> Drop for ScaleIotaAmountIterator<I> {
452 fn drop(&mut self) {
453 debug!("Number of scaled outputs: {}", self.num_scaled_outputs);
454 }
455}
456
457struct OutputHeaderWithBalance {
458 output_header: OutputHeader,
459 balance: u64,
460}
461
462/// Filtering iterator that looks for vesting outputs that can be unlocked and
463/// stores them during the iteration. At the end of the iteration it merges all
464/// vesting outputs owned by a single address into a unique basic output.
465struct UnlockedVestingIterator<I> {
466 /// Iterator over `(OutputHeader, Output)` pairs.
467 outputs: I,
468 /// Stores aggregated balances for eligible addresses.
469 unlocked_address_balances: BTreeMap<Address, OutputHeaderWithBalance>,
470 /// Timestamp used to evaluate timelock conditions.
471 snapshot_timestamp_s: u32,
472 /// Output picked to be merged
473 vesting_outputs: Vec<OutputId>,
474 num_vesting_outputs: u64,
475}
476
477impl<I> UnlockedVestingIterator<I> {
478 fn new(outputs: I, snapshot_timestamp_s: u32) -> Self {
479 Self {
480 outputs,
481 unlocked_address_balances: Default::default(),
482 snapshot_timestamp_s,
483 vesting_outputs: Default::default(),
484 num_vesting_outputs: Default::default(),
485 }
486 }
487}
488
489impl<I> Iterator for UnlockedVestingIterator<I>
490where
491 I: Iterator<Item = Result<(OutputHeader, Output)>>,
492{
493 type Item = I::Item;
494
495 /// Get the next from the chained self.outputs iterator and apply the
496 /// processing only if the output is an unlocked vesting one
497 fn next(&mut self) -> Option<Self::Item> {
498 for output in self.outputs.by_ref() {
499 if let Ok((header, inner)) = &output {
500 if let Some(address) =
501 get_address_if_vesting_output(header, inner, self.snapshot_timestamp_s)
502 {
503 self.vesting_outputs.push(header.output_id());
504 self.unlocked_address_balances
505 .entry(address)
506 .and_modify(|x| x.balance += inner.amount())
507 .or_insert(OutputHeaderWithBalance {
508 output_header: header.clone(),
509 balance: inner.amount(),
510 });
511 continue;
512 }
513 }
514 return Some(output);
515 }
516 // Now that we are out of the loop we collect the processed outputs from the
517 // filters
518 let (address, output_header_with_balance) = self.unlocked_address_balances.pop_first()?;
519 self.num_vesting_outputs += 1;
520 // create a new basic output which holds the aggregated balance from
521 // unlocked vesting outputs for this address
522 let basic = BasicOutputBuilder::new_with_amount(output_header_with_balance.balance)
523 .add_unlock_condition(AddressUnlockCondition::new(address))
524 .finish()
525 .expect("failed to create basic output");
526
527 Some(Ok((output_header_with_balance.output_header, basic.into())))
528 }
529}
530
531impl<I> Drop for UnlockedVestingIterator<I> {
532 fn drop(&mut self) {
533 debug!(
534 "Number of vesting outputs before merge: {}",
535 self.vesting_outputs.len()
536 );
537 debug!(
538 "Number of vesting outputs after merging: {}",
539 self.num_vesting_outputs
540 );
541 }
542}
543
544/// Iterator that looks for basic outputs having a tag being the Participation
545/// Tag and removes all features from the basic output.
546struct ParticipationOutputIterator<I> {
547 /// Iterator over `(OutputHeader, Output)` pairs.
548 outputs: I,
549 participation_outputs: Vec<OutputId>,
550}
551
552impl<I> ParticipationOutputIterator<I> {
553 fn new(outputs: I) -> Self {
554 Self {
555 outputs,
556 participation_outputs: Default::default(),
557 }
558 }
559}
560
561impl<I> Iterator for ParticipationOutputIterator<I>
562where
563 I: Iterator<Item = Result<(OutputHeader, Output)>>,
564{
565 type Item = I::Item;
566
567 /// Get the next from the chained self.outputs iterator and apply the
568 /// processing only if the output has a participation tag
569 fn next(&mut self) -> Option<Self::Item> {
570 let mut output = self.outputs.next()?;
571 if let Ok((header, inner)) = &mut output {
572 if is_participation_output(inner) {
573 self.participation_outputs.push(header.output_id());
574 let basic_output = inner.as_basic();
575 // replace the inner output
576 *inner = BasicOutputBuilder::from(basic_output)
577 .with_features(
578 vec![basic_output.features().get(SenderFeature::KIND).cloned()]
579 .into_iter()
580 .flatten(),
581 )
582 .finish()
583 .expect("failed to create basic output")
584 .into()
585 }
586 }
587 Some(output)
588 }
589}
590
591impl<I> Drop for ParticipationOutputIterator<I> {
592 fn drop(&mut self) {
593 debug!(
594 "Number of participation outputs: {}",
595 self.participation_outputs.len()
596 );
597 debug!("Participation outputs: {:?}", self.participation_outputs);
598 }
599}
600
601/// Extension trait that provides convenient methods for chaining and filtering
602/// iterator operations.
603///
604/// The iterators produced by this trait are designed to chain such that,
605/// calling `next()` on the last iterator will recursively invoke `next()` on
606/// the preceding iterators, maintaining the expected behavior.
607trait IteratorExt: Iterator<Item = Result<(OutputHeader, Output)>> + Sized {
608 fn perform_swap_split(self, swap_split_map: AddressSwapSplitMap) -> SwapSplitIterator<Self> {
609 SwapSplitIterator::new(self, swap_split_map)
610 }
611
612 fn scale_iota_amount(self) -> ScaleIotaAmountIterator<Self> {
613 ScaleIotaAmountIterator::new(self)
614 }
615
616 fn filter_unlocked_vesting_outputs(
617 self,
618 snapshot_timestamp_s: u32,
619 ) -> UnlockedVestingIterator<Self> {
620 UnlockedVestingIterator::new(self, snapshot_timestamp_s)
621 }
622
623 fn filter_participation_outputs(self) -> ParticipationOutputIterator<Self> {
624 ParticipationOutputIterator::new(self)
625 }
626}
627impl<T: Iterator<Item = Result<(OutputHeader, Output)>>> IteratorExt for T {}
628
629/// Skip all outputs that are not basic or not vesting. For vesting (basic)
630/// outputs, extract and return the address from their address unlock condition.
631fn get_address_if_vesting_output(
632 header: &OutputHeader,
633 output: &Output,
634 snapshot_timestamp_s: u32,
635) -> Option<Address> {
636 if !output.is_basic() || !is_vested_reward(header.output_id(), output.as_basic()) {
637 // if the output is not basic and a vested reward then skip
638 return None;
639 }
640
641 output.unlock_conditions().and_then(|uc| {
642 if uc.is_time_locked(snapshot_timestamp_s) {
643 // if the output would still be time locked at snapshot_timestamp_s then skip
644 None
645 } else {
646 // return the address of a vested output that is or can be unlocked
647 uc.address().map(|a| *a.address())
648 }
649 })
650}
651
652/// SwapSplit operation. Take a `basic_output` and split it until all targets
653/// found in the `destinations` are meet. In the meantime, swap the address
654/// unlock condition origin address with the destination address. Finally, if
655/// the original `basic_output` has some remainder amount, then return it
656/// (without swapping its address unlock condition).
657fn swap_split_operation<'a>(
658 destinations: impl Iterator<Item = (&'a mut IotaAddress, &'a mut u64)>,
659 basic_output: &BasicOutput,
660) -> (Option<Output>, Vec<Output>) {
661 let mut original_output_opt = None;
662 let mut split_outputs = vec![];
663 let mut original_basic_output_remainder = basic_output.amount();
664
665 // if the addressUC's address is to swap, then it can have several
666 // destinations
667 for (destination, target) in destinations {
668 // break if the basic output was drained already
669 if original_basic_output_remainder == 0 {
670 break;
671 }
672 // we need to make sure that we split at most OUTPUT_INDEX_MAX - 1 times
673 debug_assert!(
674 split_outputs.len() < OUTPUT_INDEX_MAX as usize,
675 "Too many swap split operations to perform for a single output"
676 );
677 // if the target for this destination is less than the basic output remainder,
678 // then use it to split the basic output and swap address;
679 // otherwise split and swap using the original_basic_output_remainder, and then
680 // break the loop.
681 let swap_split_amount = original_basic_output_remainder.min(*target);
682 split_outputs.push(
683 BasicOutputBuilder::from(basic_output)
684 .with_amount(swap_split_amount)
685 .replace_unlock_condition(AddressUnlockCondition::new(Ed25519Address::new(
686 destination.to_inner(),
687 )))
688 .finish()
689 .expect("failed to create basic output during split")
690 .into(),
691 );
692 *target -= swap_split_amount;
693 original_basic_output_remainder -= swap_split_amount;
694 }
695
696 // if the basic output remainder is some, it means that all destinations are
697 // already covered; so the original basic output can be just kept with (maybe)
698 // an adjusted amount.
699 if original_basic_output_remainder > 0 {
700 original_output_opt = Some(
701 BasicOutputBuilder::from(basic_output)
702 .with_amount(original_basic_output_remainder)
703 .finish()
704 .expect("failed to create basic output")
705 .into(),
706 );
707 }
708 (original_output_opt, split_outputs)
709}
710
711/// Utility struct that defines the ordering between timelocked basic outputs.
712/// It is required that the output is a basic outputs with timelock unlock
713/// condition.
714#[derive(PartialEq, Eq)]
715struct TimelockOrderedOutput {
716 header: OutputHeader,
717 output: Output,
718}
719
720impl TimelockOrderedOutput {
721 fn get_timestamp(&self) -> u32 {
722 self.output
723 .as_basic()
724 .unlock_conditions()
725 .timelock()
726 .unwrap()
727 .timestamp()
728 }
729}
730
731impl Ord for TimelockOrderedOutput {
732 fn cmp(&self, other: &Self) -> Ordering {
733 self.get_timestamp()
734 .cmp(&other.get_timestamp())
735 .then_with(|| self.header.output_id().cmp(&other.header.output_id()))
736 }
737}
738impl PartialOrd for TimelockOrderedOutput {
739 fn partial_cmp(&self, other: &Self) -> Option<Ordering> {
740 Some(self.cmp(other))
741 }
742}