iota_stardust_types/block/output/
mod.rs

1// Copyright (c) 2026 IOTA Stiftung
2// SPDX-License-Identifier: Apache-2.0
3
4mod alias_id;
5mod chain_id;
6mod foundry_id;
7mod native_token;
8mod nft_id;
9mod output_id;
10mod token_id;
11mod token_scheme;
12mod treasury;
13
14///
15pub mod alias;
16///
17pub mod basic;
18///
19pub mod feature;
20///
21pub mod foundry;
22///
23pub mod nft;
24///
25pub mod unlock_condition;
26
27use core::ops::RangeInclusive;
28
29use derive_more::From;
30use packable::{
31    Packable,
32    error::{UnpackError, UnpackErrorExt},
33    packer::Packer,
34    unpacker::Unpacker,
35};
36
37pub(crate) use self::{
38    alias::StateMetadataLength,
39    feature::{MetadataFeatureLength, TagFeatureLength},
40    native_token::NativeTokenCount,
41    output_id::OutputIndex,
42};
43pub use self::{
44    alias::{AliasOutput, AliasOutputBuilder, AliasTransition},
45    alias_id::AliasId,
46    basic::{BasicOutput, BasicOutputBuilder},
47    chain_id::ChainId,
48    feature::{Feature, Features},
49    foundry::{FoundryOutput, FoundryOutputBuilder},
50    foundry_id::FoundryId,
51    native_token::{NativeToken, NativeTokens, NativeTokensBuilder},
52    nft::{NftOutput, NftOutputBuilder},
53    nft_id::NftId,
54    output_id::OutputId,
55    token_id::TokenId,
56    token_scheme::{SimpleTokenScheme, TokenScheme},
57    treasury::TreasuryOutput,
58    unlock_condition::{UnlockCondition, UnlockConditions},
59};
60use crate::block::{Error, address::Address};
61
62/// The maximum number of outputs of a transaction.
63pub const OUTPUT_COUNT_MAX: u16 = 128;
64/// The range of valid numbers of outputs of a transaction .
65pub const OUTPUT_COUNT_RANGE: RangeInclusive<u16> = 1..=OUTPUT_COUNT_MAX; // [1..128]
66/// The maximum index of outputs of a transaction.
67pub const OUTPUT_INDEX_MAX: u16 = OUTPUT_COUNT_MAX - 1; // 127
68/// The range of valid indices of outputs of a transaction .
69pub const OUTPUT_INDEX_RANGE: RangeInclusive<u16> = 0..=OUTPUT_INDEX_MAX; // [0..127]
70#[derive(Clone)]
71pub(crate) enum OutputBuilderAmount {
72    Amount(u64),
73}
74
75/// A generic output that can represent different types defining the deposit of
76/// funds.
77#[derive(Clone, Eq, PartialEq, Ord, PartialOrd, Hash, From)]
78pub enum Output {
79    /// A treasury output.
80    Treasury(TreasuryOutput),
81    /// A basic output.
82    Basic(BasicOutput),
83    /// An alias output.
84    Alias(AliasOutput),
85    /// A foundry output.
86    Foundry(FoundryOutput),
87    /// An NFT output.
88    Nft(NftOutput),
89}
90
91impl core::fmt::Debug for Output {
92    fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
93        match self {
94            Self::Treasury(output) => output.fmt(f),
95            Self::Basic(output) => output.fmt(f),
96            Self::Alias(output) => output.fmt(f),
97            Self::Foundry(output) => output.fmt(f),
98            Self::Nft(output) => output.fmt(f),
99        }
100    }
101}
102
103impl Output {
104    /// Minimum amount for an output.
105    pub const AMOUNT_MIN: u64 = 1;
106
107    /// Return the output kind of an [`Output`].
108    pub fn kind(&self) -> u8 {
109        match self {
110            Self::Treasury(_) => TreasuryOutput::KIND,
111            Self::Basic(_) => BasicOutput::KIND,
112            Self::Alias(_) => AliasOutput::KIND,
113            Self::Foundry(_) => FoundryOutput::KIND,
114            Self::Nft(_) => NftOutput::KIND,
115        }
116    }
117
118    /// Returns the output kind of an [`Output`] as a string.
119    pub fn kind_str(&self) -> &str {
120        match self {
121            Self::Alias(_) => "Alias",
122            Self::Basic(_) => "Basic",
123            Self::Foundry(_) => "Foundry",
124            Self::Nft(_) => "Nft",
125            Self::Treasury(_) => "Treasury",
126        }
127    }
128
129    /// Returns the amount of an [`Output`].
130    pub fn amount(&self) -> u64 {
131        match self {
132            Self::Treasury(output) => output.amount(),
133            Self::Basic(output) => output.amount(),
134            Self::Alias(output) => output.amount(),
135            Self::Foundry(output) => output.amount(),
136            Self::Nft(output) => output.amount(),
137        }
138    }
139
140    /// Returns the native tokens of an [`Output`], if any.
141    pub fn native_tokens(&self) -> Option<&NativeTokens> {
142        match self {
143            Self::Treasury(_) => None,
144            Self::Basic(output) => Some(output.native_tokens()),
145            Self::Alias(output) => Some(output.native_tokens()),
146            Self::Foundry(output) => Some(output.native_tokens()),
147            Self::Nft(output) => Some(output.native_tokens()),
148        }
149    }
150
151    /// Returns the unlock conditions of an [`Output`], if any.
152    pub fn unlock_conditions(&self) -> Option<&UnlockConditions> {
153        match self {
154            Self::Treasury(_) => None,
155            Self::Basic(output) => Some(output.unlock_conditions()),
156            Self::Alias(output) => Some(output.unlock_conditions()),
157            Self::Foundry(output) => Some(output.unlock_conditions()),
158            Self::Nft(output) => Some(output.unlock_conditions()),
159        }
160    }
161
162    /// Returns the features of an [`Output`], if any.
163    pub fn features(&self) -> Option<&Features> {
164        match self {
165            Self::Treasury(_) => None,
166            Self::Basic(output) => Some(output.features()),
167            Self::Alias(output) => Some(output.features()),
168            Self::Foundry(output) => Some(output.features()),
169            Self::Nft(output) => Some(output.features()),
170        }
171    }
172
173    /// Returns the immutable features of an [`Output`], if any.
174    pub fn immutable_features(&self) -> Option<&Features> {
175        match self {
176            Self::Treasury(_) => None,
177            Self::Basic(_) => None,
178            Self::Alias(output) => Some(output.immutable_features()),
179            Self::Foundry(output) => Some(output.immutable_features()),
180            Self::Nft(output) => Some(output.immutable_features()),
181        }
182    }
183
184    /// Returns the chain identifier of an [`Output`], if any.
185    pub fn chain_id(&self) -> Option<ChainId> {
186        match self {
187            Self::Treasury(_) => None,
188            Self::Basic(_) => None,
189            Self::Alias(output) => Some(output.chain_id()),
190            Self::Foundry(output) => Some(output.chain_id()),
191            Self::Nft(output) => Some(output.chain_id()),
192        }
193    }
194
195    /// Checks whether the output is a [`TreasuryOutput`].
196    pub fn is_treasury(&self) -> bool {
197        matches!(self, Self::Treasury(_))
198    }
199
200    /// Gets the output as an actual [`TreasuryOutput`].
201    /// PANIC: do not call on a non-treasury output.
202    pub fn as_treasury(&self) -> &TreasuryOutput {
203        if let Self::Treasury(output) = self {
204            output
205        } else {
206            panic!("as_treasury called on a non-treasury output");
207        }
208    }
209
210    /// Checks whether the output is a [`BasicOutput`].
211    pub fn is_basic(&self) -> bool {
212        matches!(self, Self::Basic(_))
213    }
214
215    /// Gets the output as an actual [`BasicOutput`].
216    /// PANIC: do not call on a non-basic output.
217    pub fn as_basic(&self) -> &BasicOutput {
218        if let Self::Basic(output) = self {
219            output
220        } else {
221            panic!("as_basic called on a non-basic output");
222        }
223    }
224
225    /// Checks whether the output is an [`AliasOutput`].
226    pub fn is_alias(&self) -> bool {
227        matches!(self, Self::Alias(_))
228    }
229
230    /// Gets the output as an actual [`AliasOutput`].
231    /// PANIC: do not call on a non-alias output.
232    pub fn as_alias(&self) -> &AliasOutput {
233        if let Self::Alias(output) = self {
234            output
235        } else {
236            panic!("as_alias called on a non-alias output");
237        }
238    }
239
240    /// Checks whether the output is a [`FoundryOutput`].
241    pub fn is_foundry(&self) -> bool {
242        matches!(self, Self::Foundry(_))
243    }
244
245    /// Gets the output as an actual [`FoundryOutput`].
246    /// PANIC: do not call on a non-foundry output.
247    pub fn as_foundry(&self) -> &FoundryOutput {
248        if let Self::Foundry(output) = self {
249            output
250        } else {
251            panic!("as_foundry called on a non-foundry output");
252        }
253    }
254
255    /// Checks whether the output is an [`NftOutput`].
256    pub fn is_nft(&self) -> bool {
257        matches!(self, Self::Nft(_))
258    }
259
260    /// Gets the output as an actual [`NftOutput`].
261    /// PANIC: do not call on a non-nft output.
262    pub fn as_nft(&self) -> &NftOutput {
263        if let Self::Nft(output) = self {
264            output
265        } else {
266            panic!("as_nft called on a non-nft output");
267        }
268    }
269
270    /// Returns the address that is required to unlock this [`Output`] and the
271    /// alias or nft address that gets unlocked by it, if it's an alias or
272    /// nft. If no `alias_transition` has been provided, assumes a state
273    /// transition.
274    pub fn required_and_unlocked_address(
275        &self,
276        current_time: u32,
277        output_id: &OutputId,
278        alias_transition: Option<AliasTransition>,
279    ) -> Result<(Address, Option<Address>), Error> {
280        match self {
281            Self::Alias(output) => {
282                if alias_transition.unwrap_or(AliasTransition::State) == AliasTransition::State {
283                    // Alias address is only unlocked if it's a state transition
284                    Ok((
285                        *output.state_controller_address(),
286                        Some(Address::Alias(output.alias_address(output_id))),
287                    ))
288                } else {
289                    Ok((*output.governor_address(), None))
290                }
291            }
292            Self::Basic(output) => Ok((
293                *output
294                    .unlock_conditions()
295                    .locked_address(output.address(), current_time),
296                None,
297            )),
298            Self::Nft(output) => Ok((
299                *output
300                    .unlock_conditions()
301                    .locked_address(output.address(), current_time),
302                Some(Address::Nft(output.nft_address(output_id))),
303            )),
304            Self::Foundry(output) => Ok((Address::Alias(*output.alias_address()), None)),
305            Self::Treasury(_) => Err(Error::UnsupportedOutputKind(TreasuryOutput::KIND)),
306        }
307    }
308}
309
310impl Packable for Output {
311    type UnpackVisitor = ();
312    type UnpackError = Error;
313
314    fn pack<P: Packer>(&self, packer: &mut P) -> Result<(), P::Error> {
315        match self {
316            Self::Treasury(output) => {
317                TreasuryOutput::KIND.pack(packer)?;
318                output.pack(packer)
319            }
320            Self::Basic(output) => {
321                BasicOutput::KIND.pack(packer)?;
322                output.pack(packer)
323            }
324            Self::Alias(output) => {
325                AliasOutput::KIND.pack(packer)?;
326                output.pack(packer)
327            }
328            Self::Foundry(output) => {
329                FoundryOutput::KIND.pack(packer)?;
330                output.pack(packer)
331            }
332            Self::Nft(output) => {
333                NftOutput::KIND.pack(packer)?;
334                output.pack(packer)
335            }
336        }
337    }
338
339    fn unpack<U: Unpacker, const VERIFY: bool>(
340        unpacker: &mut U,
341        visitor: &Self::UnpackVisitor,
342    ) -> Result<Self, UnpackError<Self::UnpackError, U::Error>> {
343        let kind = u8::unpack::<_, VERIFY>(unpacker, visitor).coerce()?;
344
345        match kind {
346            TreasuryOutput::KIND => Ok(Self::Treasury(
347                TreasuryOutput::unpack::<_, VERIFY>(unpacker, visitor).coerce()?,
348            )),
349            BasicOutput::KIND => Ok(Self::Basic(
350                BasicOutput::unpack::<_, VERIFY>(unpacker, visitor).coerce()?,
351            )),
352            AliasOutput::KIND => Ok(Self::Alias(
353                AliasOutput::unpack::<_, VERIFY>(unpacker, visitor).coerce()?,
354            )),
355            FoundryOutput::KIND => Ok(Self::Foundry(
356                FoundryOutput::unpack::<_, VERIFY>(unpacker, visitor).coerce()?,
357            )),
358            NftOutput::KIND => Ok(Self::Nft(
359                NftOutput::unpack::<_, VERIFY>(unpacker, visitor).coerce()?,
360            )),
361            _ => Err(UnpackError::Packable(Error::InvalidOutputKind(kind))),
362        }
363    }
364}
365
366pub(crate) fn verify_output_amount(amount: &u64, token_supply: &u64) -> Result<(), Error> {
367    if *amount < Output::AMOUNT_MIN || amount > token_supply {
368        Err(Error::InvalidOutputAmount(*amount))
369    } else {
370        Ok(())
371    }
372}