iota_stardust_types/block/output/
foundry.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::AliasAddress,
11    output::{
12        ChainId, FoundryId, NativeToken, NativeTokens, OutputBuilderAmount, TokenId, TokenScheme,
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 FoundryOutputBuilder {
25    amount: OutputBuilderAmount,
26    native_tokens: BTreeSet<NativeToken>,
27    serial_number: u32,
28    token_scheme: TokenScheme,
29    unlock_conditions: BTreeSet<UnlockCondition>,
30    features: BTreeSet<Feature>,
31    immutable_features: BTreeSet<Feature>,
32}
33
34impl FoundryOutputBuilder {
35    /// Creates a [`FoundryOutputBuilder`] with a provided amount.
36    pub fn new_with_amount(amount: u64, serial_number: u32, token_scheme: TokenScheme) -> Self {
37        Self::new(
38            OutputBuilderAmount::Amount(amount),
39            serial_number,
40            token_scheme,
41        )
42    }
43
44    fn new(amount: OutputBuilderAmount, serial_number: u32, token_scheme: TokenScheme) -> Self {
45        Self {
46            amount,
47            native_tokens: BTreeSet::new(),
48            serial_number,
49            token_scheme,
50            unlock_conditions: BTreeSet::new(),
51            features: BTreeSet::new(),
52            immutable_features: BTreeSet::new(),
53        }
54    }
55
56    /// Sets the amount to the provided value.
57    #[inline(always)]
58    pub fn with_amount(mut self, amount: u64) -> Self {
59        self.amount = OutputBuilderAmount::Amount(amount);
60        self
61    }
62
63    ///
64    #[inline(always)]
65    pub fn add_native_token(mut self, native_token: NativeToken) -> Self {
66        self.native_tokens.insert(native_token);
67        self
68    }
69
70    ///
71    #[inline(always)]
72    pub fn with_native_tokens(
73        mut self,
74        native_tokens: impl IntoIterator<Item = NativeToken>,
75    ) -> Self {
76        self.native_tokens = native_tokens.into_iter().collect();
77        self
78    }
79
80    /// Sets the serial number to the provided value.
81    #[inline(always)]
82    pub fn with_serial_number(mut self, serial_number: u32) -> Self {
83        self.serial_number = serial_number;
84        self
85    }
86
87    /// Sets the token scheme to the provided value.
88    #[inline(always)]
89    pub fn with_token_scheme(mut self, token_scheme: TokenScheme) -> Self {
90        self.token_scheme = token_scheme;
91        self
92    }
93
94    /// Adds an [`UnlockCondition`] to the builder, if one does not already
95    /// exist of that type.
96    #[inline(always)]
97    pub fn add_unlock_condition(mut self, unlock_condition: impl Into<UnlockCondition>) -> Self {
98        self.unlock_conditions.insert(unlock_condition.into());
99        self
100    }
101
102    /// Sets the [`UnlockConditions`]s in the builder, overwriting any existing
103    /// values.
104    #[inline(always)]
105    pub fn with_unlock_conditions(
106        mut self,
107        unlock_conditions: impl IntoIterator<Item = impl Into<UnlockCondition>>,
108    ) -> Self {
109        self.unlock_conditions = unlock_conditions.into_iter().map(Into::into).collect();
110        self
111    }
112
113    /// Replaces an [`UnlockCondition`] of the builder with a new one, or adds
114    /// it.
115    pub fn replace_unlock_condition(
116        mut self,
117        unlock_condition: impl Into<UnlockCondition>,
118    ) -> Self {
119        self.unlock_conditions.replace(unlock_condition.into());
120        self
121    }
122
123    /// Clears all [`UnlockConditions`]s from the builder.
124    #[inline(always)]
125    pub fn clear_unlock_conditions(mut self) -> Self {
126        self.unlock_conditions.clear();
127        self
128    }
129
130    /// Adds a [`Feature`] to the builder, if one does not already exist of that
131    /// type.
132    #[inline(always)]
133    pub fn add_feature(mut self, feature: impl Into<Feature>) -> Self {
134        self.features.insert(feature.into());
135        self
136    }
137
138    /// Sets the [`Feature`]s in the builder, overwriting any existing values.
139    #[inline(always)]
140    pub fn with_features(mut self, features: impl IntoIterator<Item = impl Into<Feature>>) -> Self {
141        self.features = features.into_iter().map(Into::into).collect();
142        self
143    }
144
145    /// Replaces a [`Feature`] of the builder with a new one, or adds it.
146    pub fn replace_feature(mut self, feature: impl Into<Feature>) -> Self {
147        self.features.replace(feature.into());
148        self
149    }
150
151    /// Clears all [`Feature`]s from the builder.
152    #[inline(always)]
153    pub fn clear_features(mut self) -> Self {
154        self.features.clear();
155        self
156    }
157
158    /// Adds an immutable [`Feature`] to the builder, if one does not already
159    /// exist of that type.
160    #[inline(always)]
161    pub fn add_immutable_feature(mut self, immutable_feature: impl Into<Feature>) -> Self {
162        self.immutable_features.insert(immutable_feature.into());
163        self
164    }
165
166    /// Sets the immutable [`Feature`]s in the builder, overwriting any existing
167    /// values.
168    #[inline(always)]
169    pub fn with_immutable_features(
170        mut self,
171        immutable_features: impl IntoIterator<Item = impl Into<Feature>>,
172    ) -> Self {
173        self.immutable_features = immutable_features.into_iter().map(Into::into).collect();
174        self
175    }
176
177    /// Replaces an immutable [`Feature`] of the builder with a new one, or adds
178    /// it.
179    pub fn replace_immutable_feature(mut self, immutable_feature: impl Into<Feature>) -> Self {
180        self.immutable_features.replace(immutable_feature.into());
181        self
182    }
183
184    /// Clears all immutable [`Feature`]s from the builder.
185    #[inline(always)]
186    pub fn clear_immutable_features(mut self) -> Self {
187        self.immutable_features.clear();
188        self
189    }
190
191    ///
192    pub fn finish(self) -> Result<FoundryOutput, Error> {
193        if self.serial_number == 0 {
194            return Err(Error::InvalidFoundryZeroSerialNumber);
195        }
196
197        let unlock_conditions = UnlockConditions::from_set(self.unlock_conditions)?;
198
199        verify_unlock_conditions(&unlock_conditions)?;
200
201        let features = Features::from_set(self.features)?;
202
203        verify_allowed_features(&features, FoundryOutput::ALLOWED_FEATURES)?;
204
205        let immutable_features = Features::from_set(self.immutable_features)?;
206
207        verify_allowed_features(
208            &immutable_features,
209            FoundryOutput::ALLOWED_IMMUTABLE_FEATURES,
210        )?;
211
212        let mut output = FoundryOutput {
213            amount: 1u64,
214            native_tokens: NativeTokens::from_set(self.native_tokens)?,
215            serial_number: self.serial_number,
216            token_scheme: self.token_scheme,
217            unlock_conditions,
218            features,
219            immutable_features,
220        };
221
222        output.amount = match self.amount {
223            OutputBuilderAmount::Amount(amount) => amount,
224        };
225
226        Ok(output)
227    }
228}
229
230impl From<&FoundryOutput> for FoundryOutputBuilder {
231    fn from(output: &FoundryOutput) -> Self {
232        Self {
233            amount: OutputBuilderAmount::Amount(output.amount),
234            native_tokens: output.native_tokens.iter().copied().collect(),
235            serial_number: output.serial_number,
236            token_scheme: output.token_scheme.clone(),
237            unlock_conditions: output.unlock_conditions.iter().cloned().collect(),
238            features: output.features.iter().cloned().collect(),
239            immutable_features: output.immutable_features.iter().cloned().collect(),
240        }
241    }
242}
243
244/// Describes a foundry output that is controlled by an alias.
245#[derive(Clone, Debug, Eq, PartialEq, Ord, PartialOrd, Hash, Packable)]
246#[packable(unpack_error = Error)]
247pub struct FoundryOutput {
248    // Amount of IOTA tokens held by the output.
249    amount: u64,
250    // Native tokens held by the output.
251    native_tokens: NativeTokens,
252    // The serial number of the foundry with respect to the controlling alias.
253    serial_number: u32,
254    token_scheme: TokenScheme,
255    unlock_conditions: UnlockConditions,
256    features: Features,
257    immutable_features: Features,
258}
259
260impl FoundryOutput {
261    /// The [`super::Output`] kind of a [`FoundryOutput`].
262    pub const KIND: u8 = 5;
263    /// The set of allowed [`UnlockCondition`]s for a [`FoundryOutput`].
264    pub const ALLOWED_UNLOCK_CONDITIONS: UnlockConditionFlags =
265        UnlockConditionFlags::IMMUTABLE_ALIAS_ADDRESS;
266    /// The set of allowed [`Feature`]s for a [`FoundryOutput`].
267    pub const ALLOWED_FEATURES: FeatureFlags = FeatureFlags::METADATA;
268    /// The set of allowed immutable [`Feature`]s for a [`FoundryOutput`].
269    pub const ALLOWED_IMMUTABLE_FEATURES: FeatureFlags = FeatureFlags::METADATA;
270
271    /// Creates a new [`FoundryOutputBuilder`] with a provided amount.
272    #[inline(always)]
273    pub fn build_with_amount(
274        amount: u64,
275        serial_number: u32,
276        token_scheme: TokenScheme,
277    ) -> FoundryOutputBuilder {
278        FoundryOutputBuilder::new_with_amount(amount, serial_number, token_scheme)
279    }
280
281    ///
282    #[inline(always)]
283    pub fn amount(&self) -> u64 {
284        self.amount
285    }
286
287    ///
288    #[inline(always)]
289    pub fn native_tokens(&self) -> &NativeTokens {
290        &self.native_tokens
291    }
292
293    ///
294    #[inline(always)]
295    pub fn serial_number(&self) -> u32 {
296        self.serial_number
297    }
298
299    ///
300    #[inline(always)]
301    pub fn token_scheme(&self) -> &TokenScheme {
302        &self.token_scheme
303    }
304
305    ///
306    #[inline(always)]
307    pub fn unlock_conditions(&self) -> &UnlockConditions {
308        &self.unlock_conditions
309    }
310
311    ///
312    #[inline(always)]
313    pub fn features(&self) -> &Features {
314        &self.features
315    }
316
317    ///
318    #[inline(always)]
319    pub fn immutable_features(&self) -> &Features {
320        &self.immutable_features
321    }
322
323    ///
324    #[inline(always)]
325    pub fn alias_address(&self) -> &AliasAddress {
326        // A FoundryOutput must have an ImmutableAliasAddressUnlockCondition.
327        self.unlock_conditions
328            .immutable_alias_address()
329            .map(|unlock_condition| unlock_condition.alias_address())
330            .unwrap()
331    }
332
333    /// Returns the [`FoundryId`] of the [`FoundryOutput`].
334    pub fn id(&self) -> FoundryId {
335        FoundryId::build(
336            self.alias_address(),
337            self.serial_number,
338            self.token_scheme.kind(),
339        )
340    }
341
342    /// Returns the [`TokenId`] of the [`FoundryOutput`].
343    pub fn token_id(&self) -> TokenId {
344        TokenId::from(self.id())
345    }
346
347    ///
348    #[inline(always)]
349    pub fn chain_id(&self) -> ChainId {
350        ChainId::Foundry(self.id())
351    }
352}
353
354fn verify_unlock_conditions(unlock_conditions: &UnlockConditions) -> Result<(), Error> {
355    if unlock_conditions.immutable_alias_address().is_none() {
356        Err(Error::MissingAddressUnlockCondition)
357    } else {
358        verify_allowed_unlock_conditions(
359            unlock_conditions,
360            FoundryOutput::ALLOWED_UNLOCK_CONDITIONS,
361        )
362    }
363}