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: {origin}, destination: {destination}, tokens left: {tokens_target}, timelocked tokens left: {tokens_timelocked_target}"
325            )
326        }
327        debug!(
328            "Number of basic outputs used for a SwapSplit (no timelock): {}",
329            self.num_swapped_basic
330        );
331        debug!(
332            "Number of timelocked basic outputs used for a SwapSplit: {}",
333            self.num_swapped_timelocks
334        );
335        debug!("Number of outputs created with splits: {}", self.num_splits);
336    }
337}
338
339/// Iterator that modifies the amount of IOTA tokens for any output, scaling the
340/// amount from micros to nanos.
341struct ScaleIotaAmountIterator<I> {
342    /// Iterator over `(OutputHeader, Output)` pairs.
343    outputs: I,
344    num_scaled_outputs: u64,
345}
346
347impl<I> ScaleIotaAmountIterator<I> {
348    fn new(outputs: I) -> Self {
349        Self {
350            outputs,
351            num_scaled_outputs: 0,
352        }
353    }
354}
355
356impl<I> Iterator for ScaleIotaAmountIterator<I>
357where
358    I: Iterator<Item = Result<(OutputHeader, Output)>>,
359{
360    type Item = I::Item;
361
362    /// Get the next from the chained self.outputs iterator and always apply the
363    /// scaling (only an Output::Treasury kind is left out)
364    fn next(&mut self) -> Option<Self::Item> {
365        let mut output = self.outputs.next()?;
366        if let Ok((_, inner)) = &mut output {
367            self.num_scaled_outputs += 1;
368            match inner {
369                Output::Basic(ref basic_output) => {
370                    // Update amount
371                    let mut builder = BasicOutputBuilder::from(basic_output).with_amount(
372                        scale_amount_for_iota(basic_output.amount())
373                            .expect("should scale the amount for iota"),
374                    );
375                    // Update amount in potential storage deposit return unlock condition
376                    if let Some(sdr_uc) = basic_output
377                        .unlock_conditions()
378                        .get(StorageDepositReturnUnlockCondition::KIND)
379                    {
380                        let sdr_uc = sdr_uc.as_storage_deposit_return();
381                        builder = builder.replace_unlock_condition(
382                            StorageDepositReturnUnlockCondition::new(
383                                sdr_uc.return_address(),
384                                scale_amount_for_iota(sdr_uc.amount())
385                                    .expect("should scale the amount for iota"),
386                                u64::MAX,
387                            )
388                            .unwrap(),
389                        );
390                    };
391                    *inner = builder
392                        .finish()
393                        .expect("failed to create basic output")
394                        .into()
395                }
396                Output::Alias(ref alias_output) => {
397                    *inner = AliasOutputBuilder::from(alias_output)
398                        .with_amount(
399                            scale_amount_for_iota(alias_output.amount())
400                                .expect("should scale the amount for iota"),
401                        )
402                        .finish()
403                        .expect("should be able to create an alias output")
404                        .into()
405                }
406                Output::Foundry(ref foundry_output) => {
407                    *inner = FoundryOutputBuilder::from(foundry_output)
408                        .with_amount(
409                            scale_amount_for_iota(foundry_output.amount())
410                                .expect("should scale the amount for iota"),
411                        )
412                        .finish()
413                        .expect("should be able to create a foundry output")
414                        .into()
415                }
416                Output::Nft(ref nft_output) => {
417                    // Update amount
418                    let mut builder = NftOutputBuilder::from(nft_output).with_amount(
419                        scale_amount_for_iota(nft_output.amount())
420                            .expect("should scale the amount for iota"),
421                    );
422                    // Update amount in potential storage deposit return unlock condition
423                    if let Some(sdr_uc) = nft_output
424                        .unlock_conditions()
425                        .get(StorageDepositReturnUnlockCondition::KIND)
426                    {
427                        let sdr_uc = sdr_uc.as_storage_deposit_return();
428                        builder = builder.replace_unlock_condition(
429                            StorageDepositReturnUnlockCondition::new(
430                                sdr_uc.return_address(),
431                                scale_amount_for_iota(sdr_uc.amount())
432                                    .expect("should scale the amount for iota"),
433                                u64::MAX,
434                            )
435                            .unwrap(),
436                        );
437                    };
438                    *inner = builder
439                        .finish()
440                        .expect("should be able to create an nft output")
441                        .into();
442                }
443                Output::Treasury(_) => (),
444            }
445        }
446        Some(output)
447    }
448}
449
450impl<I> Drop for ScaleIotaAmountIterator<I> {
451    fn drop(&mut self) {
452        debug!("Number of scaled outputs: {}", self.num_scaled_outputs);
453    }
454}
455
456struct OutputHeaderWithBalance {
457    output_header: OutputHeader,
458    balance: u64,
459}
460
461/// Filtering iterator that looks for vesting outputs that can be unlocked and
462/// stores them during the iteration. At the end of the iteration it merges all
463/// vesting outputs owned by a single address into a unique basic output.
464struct UnlockedVestingIterator<I> {
465    /// Iterator over `(OutputHeader, Output)` pairs.
466    outputs: I,
467    /// Stores aggregated balances for eligible addresses.
468    unlocked_address_balances: BTreeMap<Address, OutputHeaderWithBalance>,
469    /// Timestamp used to evaluate timelock conditions.
470    snapshot_timestamp_s: u32,
471    /// Output picked to be merged
472    vesting_outputs: Vec<OutputId>,
473    num_vesting_outputs: u64,
474}
475
476impl<I> UnlockedVestingIterator<I> {
477    fn new(outputs: I, snapshot_timestamp_s: u32) -> Self {
478        Self {
479            outputs,
480            unlocked_address_balances: Default::default(),
481            snapshot_timestamp_s,
482            vesting_outputs: Default::default(),
483            num_vesting_outputs: Default::default(),
484        }
485    }
486}
487
488impl<I> Iterator for UnlockedVestingIterator<I>
489where
490    I: Iterator<Item = Result<(OutputHeader, Output)>>,
491{
492    type Item = I::Item;
493
494    /// Get the next from the chained self.outputs iterator and apply the
495    /// processing only if the output is an unlocked vesting one
496    fn next(&mut self) -> Option<Self::Item> {
497        for output in self.outputs.by_ref() {
498            if let Ok((header, inner)) = &output {
499                if let Some(address) =
500                    get_address_if_vesting_output(header, inner, self.snapshot_timestamp_s)
501                {
502                    self.vesting_outputs.push(header.output_id());
503                    self.unlocked_address_balances
504                        .entry(address)
505                        .and_modify(|x| x.balance += inner.amount())
506                        .or_insert(OutputHeaderWithBalance {
507                            output_header: header.clone(),
508                            balance: inner.amount(),
509                        });
510                    continue;
511                }
512            }
513            return Some(output);
514        }
515        // Now that we are out of the loop we collect the processed outputs from the
516        // filters
517        let (address, output_header_with_balance) = self.unlocked_address_balances.pop_first()?;
518        self.num_vesting_outputs += 1;
519        // create a new basic output which holds the aggregated balance from
520        // unlocked vesting outputs for this address
521        let basic = BasicOutputBuilder::new_with_amount(output_header_with_balance.balance)
522            .add_unlock_condition(AddressUnlockCondition::new(address))
523            .finish()
524            .expect("failed to create basic output");
525
526        Some(Ok((output_header_with_balance.output_header, basic.into())))
527    }
528}
529
530impl<I> Drop for UnlockedVestingIterator<I> {
531    fn drop(&mut self) {
532        debug!(
533            "Number of vesting outputs before merge: {}",
534            self.vesting_outputs.len()
535        );
536        debug!(
537            "Number of vesting outputs after merging: {}",
538            self.num_vesting_outputs
539        );
540    }
541}
542
543/// Iterator that looks for basic outputs having a tag being the Participation
544/// Tag and removes all features from the basic output.
545struct ParticipationOutputIterator<I> {
546    /// Iterator over `(OutputHeader, Output)` pairs.
547    outputs: I,
548    participation_outputs: Vec<OutputId>,
549}
550
551impl<I> ParticipationOutputIterator<I> {
552    fn new(outputs: I) -> Self {
553        Self {
554            outputs,
555            participation_outputs: Default::default(),
556        }
557    }
558}
559
560impl<I> Iterator for ParticipationOutputIterator<I>
561where
562    I: Iterator<Item = Result<(OutputHeader, Output)>>,
563{
564    type Item = I::Item;
565
566    /// Get the next from the chained self.outputs iterator and apply the
567    /// processing only if the output has a participation tag
568    fn next(&mut self) -> Option<Self::Item> {
569        let mut output = self.outputs.next()?;
570        if let Ok((header, inner)) = &mut output {
571            if is_participation_output(inner) {
572                self.participation_outputs.push(header.output_id());
573                let basic_output = inner.as_basic();
574                // replace the inner output
575                *inner = BasicOutputBuilder::from(basic_output)
576                    .with_features(
577                        vec![basic_output.features().get(SenderFeature::KIND).cloned()]
578                            .into_iter()
579                            .flatten(),
580                    )
581                    .finish()
582                    .expect("failed to create basic output")
583                    .into()
584            }
585        }
586        Some(output)
587    }
588}
589
590impl<I> Drop for ParticipationOutputIterator<I> {
591    fn drop(&mut self) {
592        debug!(
593            "Number of participation outputs: {}",
594            self.participation_outputs.len()
595        );
596        debug!("Participation outputs: {:?}", self.participation_outputs);
597    }
598}
599
600/// Extension trait that provides convenient methods for chaining and filtering
601/// iterator operations.
602///
603/// The iterators produced by this trait are designed to chain such that,
604/// calling `next()` on the last iterator will recursively invoke `next()` on
605/// the preceding iterators, maintaining the expected behavior.
606trait IteratorExt: Iterator<Item = Result<(OutputHeader, Output)>> + Sized {
607    fn perform_swap_split(self, swap_split_map: AddressSwapSplitMap) -> SwapSplitIterator<Self> {
608        SwapSplitIterator::new(self, swap_split_map)
609    }
610
611    fn scale_iota_amount(self) -> ScaleIotaAmountIterator<Self> {
612        ScaleIotaAmountIterator::new(self)
613    }
614
615    fn filter_unlocked_vesting_outputs(
616        self,
617        snapshot_timestamp_s: u32,
618    ) -> UnlockedVestingIterator<Self> {
619        UnlockedVestingIterator::new(self, snapshot_timestamp_s)
620    }
621
622    fn filter_participation_outputs(self) -> ParticipationOutputIterator<Self> {
623        ParticipationOutputIterator::new(self)
624    }
625}
626impl<T: Iterator<Item = Result<(OutputHeader, Output)>>> IteratorExt for T {}
627
628/// Skip all outputs that are not basic or not vesting. For vesting (basic)
629/// outputs, extract and return the address from their address unlock condition.
630fn get_address_if_vesting_output(
631    header: &OutputHeader,
632    output: &Output,
633    snapshot_timestamp_s: u32,
634) -> Option<Address> {
635    if !output.is_basic() || !is_vested_reward(header.output_id(), output.as_basic()) {
636        // if the output is not basic and a vested reward then skip
637        return None;
638    }
639
640    output.unlock_conditions().and_then(|uc| {
641        if uc.is_time_locked(snapshot_timestamp_s) {
642            // if the output would still be time locked at snapshot_timestamp_s then skip
643            None
644        } else {
645            // return the address of a vested output that is or can be unlocked
646            uc.address().map(|a| *a.address())
647        }
648    })
649}
650
651/// SwapSplit operation. Take a `basic_output` and split it until all targets
652/// found in the `destinations` are meet. In the meantime, swap the address
653/// unlock condition origin address with the destination address. Finally, if
654/// the original `basic_output` has some remainder amount, then return it
655/// (without swapping its address unlock condition).
656fn swap_split_operation<'a>(
657    destinations: impl Iterator<Item = (&'a mut IotaAddress, &'a mut u64)>,
658    basic_output: &BasicOutput,
659) -> (Option<Output>, Vec<Output>) {
660    let mut original_output_opt = None;
661    let mut split_outputs = vec![];
662    let mut original_basic_output_remainder = basic_output.amount();
663
664    // if the addressUC's address is to swap, then it can have several
665    // destinations
666    for (destination, target) in destinations {
667        // break if the basic output was drained already
668        if original_basic_output_remainder == 0 {
669            break;
670        }
671        // we need to make sure that we split at most OUTPUT_INDEX_MAX - 1 times
672        debug_assert!(
673            split_outputs.len() < OUTPUT_INDEX_MAX as usize,
674            "Too many swap split operations to perform for a single output"
675        );
676        // if the target for this destination is less than the basic output remainder,
677        // then use it to split the basic output and swap address;
678        // otherwise split and swap using the original_basic_output_remainder, and then
679        // break the loop.
680        let swap_split_amount = original_basic_output_remainder.min(*target);
681        split_outputs.push(
682            BasicOutputBuilder::from(basic_output)
683                .with_amount(swap_split_amount)
684                .replace_unlock_condition(AddressUnlockCondition::new(Ed25519Address::new(
685                    destination.to_inner(),
686                )))
687                .finish()
688                .expect("failed to create basic output during split")
689                .into(),
690        );
691        *target -= swap_split_amount;
692        original_basic_output_remainder -= swap_split_amount;
693    }
694
695    // if the basic output remainder is some, it means that all destinations are
696    // already covered; so the original basic output can be just kept with (maybe)
697    // an adjusted amount.
698    if original_basic_output_remainder > 0 {
699        original_output_opt = Some(
700            BasicOutputBuilder::from(basic_output)
701                .with_amount(original_basic_output_remainder)
702                .finish()
703                .expect("failed to create basic output")
704                .into(),
705        );
706    }
707    (original_output_opt, split_outputs)
708}
709
710/// Utility struct that defines the ordering between timelocked basic outputs.
711/// It is required that the output is a basic outputs with timelock unlock
712/// condition.
713#[derive(PartialEq, Eq)]
714struct TimelockOrderedOutput {
715    header: OutputHeader,
716    output: Output,
717}
718
719impl TimelockOrderedOutput {
720    fn get_timestamp(&self) -> u32 {
721        self.output
722            .as_basic()
723            .unlock_conditions()
724            .timelock()
725            .unwrap()
726            .timestamp()
727    }
728}
729
730impl Ord for TimelockOrderedOutput {
731    fn cmp(&self, other: &Self) -> Ordering {
732        self.get_timestamp()
733            .cmp(&other.get_timestamp())
734            .then_with(|| self.header.output_id().cmp(&other.header.output_id()))
735    }
736}
737impl PartialOrd for TimelockOrderedOutput {
738    fn partial_cmp(&self, other: &Self) -> Option<Ordering> {
739        Some(self.cmp(other))
740    }
741}