iota_stardust_types/block/output/
alias.rs

1// Copyright (c) 2026 IOTA Stiftung
2// SPDX-License-Identifier: Apache-2.0
3
4use alloc::{collections::BTreeSet, vec::Vec};
5
6use packable::{Packable, bounded::BoundedU16, prefix::BoxedSlicePrefix};
7
8use crate::block::{
9    Error,
10    address::{Address, AliasAddress},
11    output::{
12        AliasId, ChainId, NativeToken, NativeTokens, OutputBuilderAmount, OutputId,
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/// Types of alias transition.
22#[derive(Copy, Clone, Debug, Eq, PartialEq)]
23#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
24pub enum AliasTransition {
25    /// State transition.
26    State,
27    /// Governance transition.
28    Governance,
29}
30
31impl AliasTransition {
32    /// Checks whether the alias transition is a state one.
33    pub fn is_state(&self) -> bool {
34        matches!(self, Self::State)
35    }
36
37    /// Checks whether the alias transition is a governance one.
38    pub fn is_governance(&self) -> bool {
39        matches!(self, Self::Governance)
40    }
41}
42
43impl core::fmt::Display for AliasTransition {
44    fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
45        match self {
46            Self::State => write!(f, "state"),
47            Self::Governance => write!(f, "governance"),
48        }
49    }
50}
51
52///
53#[derive(Clone)]
54#[must_use]
55pub struct AliasOutputBuilder {
56    amount: OutputBuilderAmount,
57    native_tokens: BTreeSet<NativeToken>,
58    alias_id: AliasId,
59    state_index: Option<u32>,
60    state_metadata: Vec<u8>,
61    foundry_counter: Option<u32>,
62    unlock_conditions: BTreeSet<UnlockCondition>,
63    features: BTreeSet<Feature>,
64    immutable_features: BTreeSet<Feature>,
65}
66
67impl AliasOutputBuilder {
68    /// Creates an [`AliasOutputBuilder`] with a provided amount.
69    pub fn new_with_amount(amount: u64, alias_id: AliasId) -> Self {
70        Self::new(OutputBuilderAmount::Amount(amount), alias_id)
71    }
72
73    fn new(amount: OutputBuilderAmount, alias_id: AliasId) -> Self {
74        Self {
75            amount,
76            native_tokens: BTreeSet::new(),
77            alias_id,
78            state_index: None,
79            state_metadata: Vec::new(),
80            foundry_counter: None,
81            unlock_conditions: BTreeSet::new(),
82            features: BTreeSet::new(),
83            immutable_features: BTreeSet::new(),
84        }
85    }
86
87    /// Sets the amount to the provided value.
88    #[inline(always)]
89    pub fn with_amount(mut self, amount: u64) -> Self {
90        self.amount = OutputBuilderAmount::Amount(amount);
91        self
92    }
93
94    ///
95    #[inline(always)]
96    pub fn add_native_token(mut self, native_token: NativeToken) -> Self {
97        self.native_tokens.insert(native_token);
98        self
99    }
100
101    ///
102    #[inline(always)]
103    pub fn with_native_tokens(
104        mut self,
105        native_tokens: impl IntoIterator<Item = NativeToken>,
106    ) -> Self {
107        self.native_tokens = native_tokens.into_iter().collect();
108        self
109    }
110
111    /// Sets the alias ID to the provided value.
112    #[inline(always)]
113    pub fn with_alias_id(mut self, alias_id: AliasId) -> Self {
114        self.alias_id = alias_id;
115        self
116    }
117
118    ///
119    #[inline(always)]
120    pub fn with_state_index(mut self, state_index: impl Into<Option<u32>>) -> Self {
121        self.state_index = state_index.into();
122        self
123    }
124
125    ///
126    #[inline(always)]
127    pub fn with_state_metadata(mut self, state_metadata: impl Into<Vec<u8>>) -> Self {
128        self.state_metadata = state_metadata.into();
129        self
130    }
131
132    ///
133    #[inline(always)]
134    pub fn with_foundry_counter(mut self, foundry_counter: impl Into<Option<u32>>) -> Self {
135        self.foundry_counter = foundry_counter.into();
136        self
137    }
138
139    /// Adds an [`UnlockCondition`] to the builder, if one does not already
140    /// exist of that type.
141    #[inline(always)]
142    pub fn add_unlock_condition(mut self, unlock_condition: impl Into<UnlockCondition>) -> Self {
143        self.unlock_conditions.insert(unlock_condition.into());
144        self
145    }
146
147    /// Sets the [`UnlockConditions`]s in the builder, overwriting any existing
148    /// values.
149    #[inline(always)]
150    pub fn with_unlock_conditions(
151        mut self,
152        unlock_conditions: impl IntoIterator<Item = impl Into<UnlockCondition>>,
153    ) -> Self {
154        self.unlock_conditions = unlock_conditions.into_iter().map(Into::into).collect();
155        self
156    }
157
158    /// Replaces an [`UnlockCondition`] of the builder with a new one, or adds
159    /// it.
160    pub fn replace_unlock_condition(
161        mut self,
162        unlock_condition: impl Into<UnlockCondition>,
163    ) -> Self {
164        self.unlock_conditions.replace(unlock_condition.into());
165        self
166    }
167
168    /// Clears all [`UnlockConditions`]s from the builder.
169    #[inline(always)]
170    pub fn clear_unlock_conditions(mut self) -> Self {
171        self.unlock_conditions.clear();
172        self
173    }
174
175    /// Adds a [`Feature`] to the builder, if one does not already exist of that
176    /// type.
177    #[inline(always)]
178    pub fn add_feature(mut self, feature: impl Into<Feature>) -> Self {
179        self.features.insert(feature.into());
180        self
181    }
182
183    /// Sets the [`Feature`]s in the builder, overwriting any existing values.
184    #[inline(always)]
185    pub fn with_features(mut self, features: impl IntoIterator<Item = impl Into<Feature>>) -> Self {
186        self.features = features.into_iter().map(Into::into).collect();
187        self
188    }
189
190    /// Replaces a [`Feature`] of the builder with a new one, or adds it.
191    pub fn replace_feature(mut self, feature: impl Into<Feature>) -> Self {
192        self.features.replace(feature.into());
193        self
194    }
195
196    /// Clears all [`Feature`]s from the builder.
197    #[inline(always)]
198    pub fn clear_features(mut self) -> Self {
199        self.features.clear();
200        self
201    }
202
203    /// Adds an immutable [`Feature`] to the builder, if one does not already
204    /// exist of that type.
205    #[inline(always)]
206    pub fn add_immutable_feature(mut self, immutable_feature: impl Into<Feature>) -> Self {
207        self.immutable_features.insert(immutable_feature.into());
208        self
209    }
210
211    /// Sets the immutable [`Feature`]s in the builder, overwriting any existing
212    /// values.
213    #[inline(always)]
214    pub fn with_immutable_features(
215        mut self,
216        immutable_features: impl IntoIterator<Item = impl Into<Feature>>,
217    ) -> Self {
218        self.immutable_features = immutable_features.into_iter().map(Into::into).collect();
219        self
220    }
221
222    /// Replaces an immutable [`Feature`] of the builder with a new one, or adds
223    /// it.
224    pub fn replace_immutable_feature(mut self, immutable_feature: impl Into<Feature>) -> Self {
225        self.immutable_features.replace(immutable_feature.into());
226        self
227    }
228
229    /// Clears all immutable [`Feature`]s from the builder.
230    #[inline(always)]
231    pub fn clear_immutable_features(mut self) -> Self {
232        self.immutable_features.clear();
233        self
234    }
235
236    ///
237    pub fn finish(self) -> Result<AliasOutput, Error> {
238        let state_index = self.state_index.unwrap_or(0);
239        let foundry_counter = self.foundry_counter.unwrap_or(0);
240
241        let state_metadata = self
242            .state_metadata
243            .into_boxed_slice()
244            .try_into()
245            .map_err(Error::InvalidStateMetadataLength)?;
246
247        verify_index_counter(&self.alias_id, state_index, foundry_counter)?;
248
249        let unlock_conditions = UnlockConditions::from_set(self.unlock_conditions)?;
250
251        verify_unlock_conditions(&unlock_conditions, &self.alias_id)?;
252
253        let features = Features::from_set(self.features)?;
254
255        verify_allowed_features(&features, AliasOutput::ALLOWED_FEATURES)?;
256
257        let immutable_features = Features::from_set(self.immutable_features)?;
258
259        verify_allowed_features(&immutable_features, AliasOutput::ALLOWED_IMMUTABLE_FEATURES)?;
260
261        let mut output = AliasOutput {
262            amount: 1,
263            native_tokens: NativeTokens::from_set(self.native_tokens)?,
264            alias_id: self.alias_id,
265            state_index,
266            state_metadata,
267            foundry_counter,
268            unlock_conditions,
269            features,
270            immutable_features,
271        };
272
273        output.amount = match self.amount {
274            OutputBuilderAmount::Amount(amount) => amount,
275        };
276
277        Ok(output)
278    }
279}
280
281impl From<&AliasOutput> for AliasOutputBuilder {
282    fn from(output: &AliasOutput) -> Self {
283        Self {
284            amount: OutputBuilderAmount::Amount(output.amount),
285            native_tokens: output.native_tokens.iter().copied().collect(),
286            alias_id: output.alias_id,
287            state_index: Some(output.state_index),
288            state_metadata: output.state_metadata.to_vec(),
289            foundry_counter: Some(output.foundry_counter),
290            unlock_conditions: output.unlock_conditions.iter().cloned().collect(),
291            features: output.features.iter().cloned().collect(),
292            immutable_features: output.immutable_features.iter().cloned().collect(),
293        }
294    }
295}
296
297pub(crate) type StateMetadataLength = BoundedU16<0, { AliasOutput::STATE_METADATA_LENGTH_MAX }>;
298
299/// Describes an alias account in the ledger that can be controlled by the state
300/// and governance controllers.
301#[derive(Clone, Debug, Eq, PartialEq, Ord, PartialOrd, Hash, Packable)]
302#[packable(unpack_error = Error)]
303pub struct AliasOutput {
304    // Amount of IOTA tokens held by the output.
305    amount: u64,
306    // Native tokens held by the output.
307    native_tokens: NativeTokens,
308    // Unique identifier of the alias.
309    alias_id: AliasId,
310    // A counter that must increase by 1 every time the alias is state transitioned.
311    state_index: u32,
312    // Metadata that can only be changed by the state controller.
313    #[packable(unpack_error_with = |err| Error::InvalidStateMetadataLength(err.into_prefix_err().into()))]
314    state_metadata: BoxedSlicePrefix<u8, StateMetadataLength>,
315    // A counter that denotes the number of foundries created by this alias account.
316    foundry_counter: u32,
317    unlock_conditions: UnlockConditions,
318    //
319    features: Features,
320    //
321    immutable_features: Features,
322}
323
324impl AliasOutput {
325    /// The [`super::Output`] kind of an [`AliasOutput`].
326    pub const KIND: u8 = 4;
327    /// Maximum possible length in bytes of the state metadata.
328    pub const STATE_METADATA_LENGTH_MAX: u16 = 8192;
329    /// The set of allowed [`UnlockCondition`]s for an [`AliasOutput`].
330    pub const ALLOWED_UNLOCK_CONDITIONS: UnlockConditionFlags =
331        UnlockConditionFlags::STATE_CONTROLLER_ADDRESS
332            .union(UnlockConditionFlags::GOVERNOR_ADDRESS);
333    /// The set of allowed [`Feature`]s for an [`AliasOutput`].
334    pub const ALLOWED_FEATURES: FeatureFlags = FeatureFlags::SENDER.union(FeatureFlags::METADATA);
335    /// The set of allowed immutable [`Feature`]s for an [`AliasOutput`].
336    pub const ALLOWED_IMMUTABLE_FEATURES: FeatureFlags =
337        FeatureFlags::ISSUER.union(FeatureFlags::METADATA);
338
339    /// Creates a new [`AliasOutputBuilder`] with a provided amount.
340    #[inline(always)]
341    pub fn build_with_amount(amount: u64, alias_id: AliasId) -> AliasOutputBuilder {
342        AliasOutputBuilder::new_with_amount(amount, alias_id)
343    }
344
345    ///
346    #[inline(always)]
347    pub fn amount(&self) -> u64 {
348        self.amount
349    }
350
351    ///
352    #[inline(always)]
353    pub fn native_tokens(&self) -> &NativeTokens {
354        &self.native_tokens
355    }
356
357    ///
358    #[inline(always)]
359    pub fn alias_id(&self) -> &AliasId {
360        &self.alias_id
361    }
362
363    /// Returns the alias ID if not null, or creates it from the output ID.
364    #[inline(always)]
365    pub fn alias_id_non_null(&self, output_id: &OutputId) -> AliasId {
366        self.alias_id.or_from_output_id(output_id)
367    }
368
369    ///
370    #[inline(always)]
371    pub fn state_index(&self) -> u32 {
372        self.state_index
373    }
374
375    ///
376    #[inline(always)]
377    pub fn state_metadata(&self) -> &[u8] {
378        &self.state_metadata
379    }
380
381    ///
382    #[inline(always)]
383    pub fn foundry_counter(&self) -> u32 {
384        self.foundry_counter
385    }
386
387    ///
388    #[inline(always)]
389    pub fn unlock_conditions(&self) -> &UnlockConditions {
390        &self.unlock_conditions
391    }
392
393    ///
394    #[inline(always)]
395    pub fn features(&self) -> &Features {
396        &self.features
397    }
398
399    ///
400    #[inline(always)]
401    pub fn immutable_features(&self) -> &Features {
402        &self.immutable_features
403    }
404
405    ///
406    #[inline(always)]
407    pub fn state_controller_address(&self) -> &Address {
408        // An AliasOutput must have a StateControllerAddressUnlockCondition.
409        self.unlock_conditions
410            .state_controller_address()
411            .map(|unlock_condition| unlock_condition.address())
412            .unwrap()
413    }
414
415    ///
416    #[inline(always)]
417    pub fn governor_address(&self) -> &Address {
418        // An AliasOutput must have a GovernorAddressUnlockCondition.
419        self.unlock_conditions
420            .governor_address()
421            .map(|unlock_condition| unlock_condition.address())
422            .unwrap()
423    }
424
425    ///
426    #[inline(always)]
427    pub fn chain_id(&self) -> ChainId {
428        ChainId::Alias(self.alias_id)
429    }
430
431    /// Returns the alias address for this output.
432    pub fn alias_address(&self, output_id: &OutputId) -> AliasAddress {
433        AliasAddress::new(self.alias_id_non_null(output_id))
434    }
435}
436
437#[inline]
438fn verify_index_counter(
439    alias_id: &AliasId,
440    state_index: u32,
441    foundry_counter: u32,
442) -> Result<(), Error> {
443    if alias_id.is_null() && (state_index != 0 || foundry_counter != 0) {
444        Err(Error::NonZeroStateIndexOrFoundryCounter)
445    } else {
446        Ok(())
447    }
448}
449
450fn verify_unlock_conditions(
451    unlock_conditions: &UnlockConditions,
452    alias_id: &AliasId,
453) -> Result<(), Error> {
454    if let Some(unlock_condition) = unlock_conditions.state_controller_address() {
455        if let Address::Alias(alias_address) = unlock_condition.address() {
456            if !alias_id.is_null() && alias_address.alias_id() == alias_id {
457                return Err(Error::SelfControlledAliasOutput(*alias_id));
458            }
459        }
460    } else {
461        return Err(Error::MissingStateControllerUnlockCondition);
462    }
463
464    if let Some(unlock_condition) = unlock_conditions.governor_address() {
465        if let Address::Alias(alias_address) = unlock_condition.address() {
466            if !alias_id.is_null() && alias_address.alias_id() == alias_id {
467                return Err(Error::SelfControlledAliasOutput(*alias_id));
468            }
469        }
470    } else {
471        return Err(Error::MissingGovernorUnlockCondition);
472    }
473
474    verify_allowed_unlock_conditions(unlock_conditions, AliasOutput::ALLOWED_UNLOCK_CONDITIONS)
475}