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