iota_stardust_types/block/output/
basic.rs

1// Copyright (c) 2026 IOTA Stiftung
2// SPDX-License-Identifier: Apache-2.0
3
4use alloc::collections::BTreeSet;
5
6use packable::Packable;
7
8use crate::block::{
9    Error,
10    address::Address,
11    output::{
12        NativeToken, NativeTokens, OutputBuilderAmount,
13        feature::{Feature, FeatureFlags, Features, verify_allowed_features},
14        unlock_condition::{
15            UnlockCondition, UnlockConditionFlags, UnlockConditions,
16            verify_allowed_unlock_conditions,
17        },
18    },
19};
20
21///
22#[derive(Clone)]
23#[must_use]
24pub struct BasicOutputBuilder {
25    amount: OutputBuilderAmount,
26    native_tokens: BTreeSet<NativeToken>,
27    unlock_conditions: BTreeSet<UnlockCondition>,
28    features: BTreeSet<Feature>,
29}
30
31impl BasicOutputBuilder {
32    /// Creates a [`BasicOutputBuilder`] with a provided amount.
33    #[inline(always)]
34    pub fn new_with_amount(amount: u64) -> Self {
35        Self::new(OutputBuilderAmount::Amount(amount))
36    }
37
38    fn new(amount: OutputBuilderAmount) -> Self {
39        Self {
40            amount,
41            native_tokens: BTreeSet::new(),
42            unlock_conditions: BTreeSet::new(),
43            features: BTreeSet::new(),
44        }
45    }
46
47    /// Sets the amount to the provided value.
48    #[inline(always)]
49    pub fn with_amount(mut self, amount: u64) -> Self {
50        self.amount = OutputBuilderAmount::Amount(amount);
51        self
52    }
53
54    ///
55    #[inline(always)]
56    pub fn add_native_token(mut self, native_token: NativeToken) -> Self {
57        self.native_tokens.insert(native_token);
58        self
59    }
60
61    ///
62    #[inline(always)]
63    pub fn with_native_tokens(
64        mut self,
65        native_tokens: impl IntoIterator<Item = NativeToken>,
66    ) -> Self {
67        self.native_tokens = native_tokens.into_iter().collect();
68        self
69    }
70
71    /// Adds an [`UnlockCondition`] to the builder, if one does not already
72    /// exist of that type.
73    #[inline(always)]
74    pub fn add_unlock_condition(mut self, unlock_condition: impl Into<UnlockCondition>) -> Self {
75        self.unlock_conditions.insert(unlock_condition.into());
76        self
77    }
78
79    /// Sets the [`UnlockConditions`]s in the builder, overwriting any existing
80    /// values.
81    #[inline(always)]
82    pub fn with_unlock_conditions(
83        mut self,
84        unlock_conditions: impl IntoIterator<Item = impl Into<UnlockCondition>>,
85    ) -> Self {
86        self.unlock_conditions = unlock_conditions.into_iter().map(Into::into).collect();
87        self
88    }
89
90    /// Replaces an [`UnlockCondition`] of the builder with a new one, or adds
91    /// it.
92    pub fn replace_unlock_condition(
93        mut self,
94        unlock_condition: impl Into<UnlockCondition>,
95    ) -> Self {
96        self.unlock_conditions.replace(unlock_condition.into());
97        self
98    }
99
100    /// Clears all [`UnlockConditions`]s from the builder.
101    #[inline(always)]
102    pub fn clear_unlock_conditions(mut self) -> Self {
103        self.unlock_conditions.clear();
104        self
105    }
106
107    /// Adds a [`Feature`] to the builder, if one does not already exist of that
108    /// type.
109    #[inline(always)]
110    pub fn add_feature(mut self, feature: impl Into<Feature>) -> Self {
111        self.features.insert(feature.into());
112        self
113    }
114
115    /// Sets the [`Feature`]s in the builder, overwriting any existing values.
116    #[inline(always)]
117    pub fn with_features(mut self, features: impl IntoIterator<Item = impl Into<Feature>>) -> Self {
118        self.features = features.into_iter().map(Into::into).collect();
119        self
120    }
121
122    /// Replaces a [`Feature`] of the builder with a new one, or adds it.
123    pub fn replace_feature(mut self, feature: impl Into<Feature>) -> Self {
124        self.features.replace(feature.into());
125        self
126    }
127
128    /// Clears all [`Feature`]s from the builder.
129    #[inline(always)]
130    pub fn clear_features(mut self) -> Self {
131        self.features.clear();
132        self
133    }
134
135    ///
136    pub fn finish(self) -> Result<BasicOutput, Error> {
137        let unlock_conditions = UnlockConditions::from_set(self.unlock_conditions)?;
138
139        verify_unlock_conditions::<true>(&unlock_conditions)?;
140
141        let features = Features::from_set(self.features)?;
142
143        verify_features::<true>(&features)?;
144
145        let mut output = BasicOutput {
146            amount: 1u64,
147            native_tokens: NativeTokens::from_set(self.native_tokens)?,
148            unlock_conditions,
149            features,
150        };
151
152        output.amount = match self.amount {
153            OutputBuilderAmount::Amount(amount) => amount,
154        };
155
156        Ok(output)
157    }
158}
159
160impl From<&BasicOutput> for BasicOutputBuilder {
161    fn from(output: &BasicOutput) -> Self {
162        Self {
163            amount: OutputBuilderAmount::Amount(output.amount),
164            native_tokens: output.native_tokens.iter().copied().collect(),
165            unlock_conditions: output.unlock_conditions.iter().cloned().collect(),
166            features: output.features.iter().cloned().collect(),
167        }
168    }
169}
170
171/// Describes a basic output with optional features.
172#[derive(Clone, Debug, Eq, PartialEq, Ord, PartialOrd, Hash, Packable)]
173#[packable(unpack_error = Error)]
174pub struct BasicOutput {
175    // Amount of IOTA tokens held by the output.
176    amount: u64,
177    // Native tokens held by the output.
178    native_tokens: NativeTokens,
179    unlock_conditions: UnlockConditions,
180    features: Features,
181}
182
183impl BasicOutput {
184    /// The [`super::Output`] kind of an [`BasicOutput`].
185    pub const KIND: u8 = 3;
186
187    /// The set of allowed [`UnlockCondition`]s for an [`BasicOutput`].
188    const ALLOWED_UNLOCK_CONDITIONS: UnlockConditionFlags = UnlockConditionFlags::ADDRESS
189        .union(UnlockConditionFlags::STORAGE_DEPOSIT_RETURN)
190        .union(UnlockConditionFlags::TIMELOCK)
191        .union(UnlockConditionFlags::EXPIRATION);
192    /// The set of allowed [`Feature`]s for an [`BasicOutput`].
193    pub const ALLOWED_FEATURES: FeatureFlags = FeatureFlags::SENDER
194        .union(FeatureFlags::METADATA)
195        .union(FeatureFlags::TAG);
196
197    /// Creates a new [`BasicOutputBuilder`] with a provided amount.
198    #[inline(always)]
199    pub fn build_with_amount(amount: u64) -> BasicOutputBuilder {
200        BasicOutputBuilder::new_with_amount(amount)
201    }
202
203    ///
204    #[inline(always)]
205    pub fn amount(&self) -> u64 {
206        self.amount
207    }
208
209    ///
210    #[inline(always)]
211    pub fn native_tokens(&self) -> &NativeTokens {
212        &self.native_tokens
213    }
214
215    ///
216    #[inline(always)]
217    pub fn unlock_conditions(&self) -> &UnlockConditions {
218        &self.unlock_conditions
219    }
220
221    ///
222    #[inline(always)]
223    pub fn features(&self) -> &Features {
224        &self.features
225    }
226
227    ///
228    #[inline(always)]
229    pub fn address(&self) -> &Address {
230        // An BasicOutput must have an AddressUnlockCondition.
231        self.unlock_conditions
232            .address()
233            .map(|unlock_condition| unlock_condition.address())
234            .unwrap()
235    }
236
237    /// Returns the address of the unlock conditions if the output is a simple
238    /// deposit. Simple deposit outputs are basic outputs with only an
239    /// address unlock condition, no native tokens and no features. They are
240    /// used to return storage deposits.
241    pub fn simple_deposit_address(&self) -> Option<&Address> {
242        if let [UnlockCondition::Address(address)] = self.unlock_conditions().as_ref() {
243            if self.native_tokens.is_empty() && self.features.is_empty() {
244                return Some(address.address());
245            }
246        }
247
248        None
249    }
250}
251
252fn verify_unlock_conditions<const VERIFY: bool>(
253    unlock_conditions: &UnlockConditions,
254) -> Result<(), Error> {
255    if VERIFY {
256        if unlock_conditions.address().is_none() {
257            Err(Error::MissingAddressUnlockCondition)
258        } else {
259            verify_allowed_unlock_conditions(
260                unlock_conditions,
261                BasicOutput::ALLOWED_UNLOCK_CONDITIONS,
262            )
263        }
264    } else {
265        Ok(())
266    }
267}
268
269fn verify_features<const VERIFY: bool>(blocks: &Features) -> Result<(), Error> {
270    if VERIFY {
271        verify_allowed_features(blocks, BasicOutput::ALLOWED_FEATURES)
272    } else {
273        Ok(())
274    }
275}