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}