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}