bee_block/output/
nft.rs

1// Copyright 2021-2022 IOTA Stiftung
2// SPDX-License-Identifier: Apache-2.0
3
4use alloc::vec::Vec;
5
6use packable::{
7    error::{UnpackError, UnpackErrorExt},
8    packer::Packer,
9    unpacker::Unpacker,
10    Packable,
11};
12
13use crate::{
14    address::{Address, NftAddress},
15    output::{
16        feature::{verify_allowed_features, Feature, FeatureFlags, Features},
17        unlock_condition::{verify_allowed_unlock_conditions, UnlockCondition, UnlockConditionFlags, UnlockConditions},
18        verify_output_amount, ChainId, NativeToken, NativeTokens, NftId, Output, OutputBuilderAmount, OutputId, Rent,
19        RentStructure, StateTransitionError, StateTransitionVerifier,
20    },
21    protocol::ProtocolParameters,
22    semantic::{ConflictReason, ValidationContext},
23    unlock::Unlock,
24    Error,
25};
26
27///
28#[derive(Clone)]
29#[must_use]
30pub struct NftOutputBuilder {
31    amount: OutputBuilderAmount,
32    native_tokens: Vec<NativeToken>,
33    nft_id: NftId,
34    unlock_conditions: Vec<UnlockCondition>,
35    features: Vec<Feature>,
36    immutable_features: Vec<Feature>,
37}
38
39impl NftOutputBuilder {
40    /// Creates an [`NftOutputBuilder`] with a provided amount.
41    pub fn new_with_amount(amount: u64, nft_id: NftId) -> Result<NftOutputBuilder, Error> {
42        Self::new(OutputBuilderAmount::Amount(amount), nft_id)
43    }
44
45    /// Creates an [`NftOutputBuilder`] with a provided rent structure.
46    /// The amount will be set to the minimum storage deposit.
47    pub fn new_with_minimum_storage_deposit(
48        rent_structure: RentStructure,
49        nft_id: NftId,
50    ) -> Result<NftOutputBuilder, Error> {
51        Self::new(OutputBuilderAmount::MinimumStorageDeposit(rent_structure), nft_id)
52    }
53
54    fn new(amount: OutputBuilderAmount, nft_id: NftId) -> Result<NftOutputBuilder, Error> {
55        Ok(Self {
56            amount,
57            native_tokens: Vec::new(),
58            nft_id,
59            unlock_conditions: Vec::new(),
60            features: Vec::new(),
61            immutable_features: Vec::new(),
62        })
63    }
64
65    /// Sets the amount to the provided value.
66    #[inline(always)]
67    pub fn with_amount(mut self, amount: u64) -> Result<Self, Error> {
68        self.amount = OutputBuilderAmount::Amount(amount);
69        Ok(self)
70    }
71
72    /// Sets the amount to the minimum storage deposit.
73    #[inline(always)]
74    pub fn with_minimum_storage_deposit(mut self, rent_structure: RentStructure) -> Self {
75        self.amount = OutputBuilderAmount::MinimumStorageDeposit(rent_structure);
76        self
77    }
78
79    ///
80    #[inline(always)]
81    pub fn add_native_token(mut self, native_token: NativeToken) -> Self {
82        self.native_tokens.push(native_token);
83        self
84    }
85
86    ///
87    #[inline(always)]
88    pub fn with_native_tokens(mut self, native_tokens: impl IntoIterator<Item = NativeToken>) -> Self {
89        self.native_tokens = native_tokens.into_iter().collect();
90        self
91    }
92
93    /// Sets the NFT ID to the provided value.
94    #[inline(always)]
95    pub fn with_nft_id(mut self, nft_id: NftId) -> Self {
96        self.nft_id = nft_id;
97        self
98    }
99
100    ///
101    #[inline(always)]
102    pub fn add_unlock_condition(mut self, unlock_condition: UnlockCondition) -> Self {
103        self.unlock_conditions.push(unlock_condition);
104        self
105    }
106
107    ///
108    #[inline(always)]
109    pub fn with_unlock_conditions(mut self, unlock_conditions: impl IntoIterator<Item = UnlockCondition>) -> Self {
110        self.unlock_conditions = unlock_conditions.into_iter().collect();
111        self
112    }
113
114    ///
115    pub fn replace_unlock_condition(mut self, unlock_condition: UnlockCondition) -> Result<Self, Error> {
116        match self
117            .unlock_conditions
118            .iter_mut()
119            .find(|u| u.kind() == unlock_condition.kind())
120        {
121            Some(u) => *u = unlock_condition,
122            None => return Err(Error::CannotReplaceMissingField),
123        }
124        Ok(self)
125    }
126
127    ///
128    #[inline(always)]
129    pub fn add_feature(mut self, feature: Feature) -> Self {
130        self.features.push(feature);
131        self
132    }
133
134    ///
135    #[inline(always)]
136    pub fn with_features(mut self, features: impl IntoIterator<Item = Feature>) -> Self {
137        self.features = features.into_iter().collect();
138        self
139    }
140
141    ///
142    pub fn replace_feature(mut self, feature: Feature) -> Result<Self, Error> {
143        match self.features.iter_mut().find(|f| f.kind() == feature.kind()) {
144            Some(f) => *f = feature,
145            None => return Err(Error::CannotReplaceMissingField),
146        }
147        Ok(self)
148    }
149
150    ///
151    #[inline(always)]
152    pub fn add_immutable_feature(mut self, immutable_feature: Feature) -> Self {
153        self.immutable_features.push(immutable_feature);
154        self
155    }
156
157    ///
158    #[inline(always)]
159    pub fn with_immutable_features(mut self, immutable_features: impl IntoIterator<Item = Feature>) -> Self {
160        self.immutable_features = immutable_features.into_iter().collect();
161        self
162    }
163
164    ///
165    pub fn replace_immutable_feature(mut self, immutable_feature: Feature) -> Result<Self, Error> {
166        match self
167            .immutable_features
168            .iter_mut()
169            .find(|f| f.kind() == immutable_feature.kind())
170        {
171            Some(f) => *f = immutable_feature,
172            None => return Err(Error::CannotReplaceMissingField),
173        }
174        Ok(self)
175    }
176
177    ///
178    pub fn finish(self, token_supply: u64) -> Result<NftOutput, Error> {
179        let unlock_conditions = UnlockConditions::new(self.unlock_conditions)?;
180
181        verify_unlock_conditions(&unlock_conditions, &self.nft_id)?;
182
183        let features = Features::new(self.features)?;
184
185        verify_allowed_features(&features, NftOutput::ALLOWED_FEATURES)?;
186
187        let immutable_features = Features::new(self.immutable_features)?;
188
189        verify_allowed_features(&immutable_features, NftOutput::ALLOWED_IMMUTABLE_FEATURES)?;
190
191        let mut output = NftOutput {
192            amount: 1u64,
193            native_tokens: NativeTokens::new(self.native_tokens)?,
194            nft_id: self.nft_id,
195            unlock_conditions,
196            features,
197            immutable_features,
198        };
199
200        output.amount = match self.amount {
201            OutputBuilderAmount::Amount(amount) => amount,
202            OutputBuilderAmount::MinimumStorageDeposit(rent_structure) => {
203                Output::Nft(output.clone()).rent_cost(&rent_structure)
204            }
205        };
206
207        verify_output_amount::<true>(&output.amount, &token_supply)?;
208
209        Ok(output)
210    }
211
212    /// Finishes the [`NftOutputBuilder`] into an [`Output`].
213    pub fn finish_output(self, token_supply: u64) -> Result<Output, Error> {
214        Ok(Output::Nft(self.finish(token_supply)?))
215    }
216}
217
218impl From<&NftOutput> for NftOutputBuilder {
219    fn from(output: &NftOutput) -> Self {
220        NftOutputBuilder {
221            amount: OutputBuilderAmount::Amount(output.amount),
222            native_tokens: output.native_tokens.to_vec(),
223            nft_id: output.nft_id,
224            unlock_conditions: output.unlock_conditions.to_vec(),
225            features: output.features.to_vec(),
226            immutable_features: output.immutable_features.to_vec(),
227        }
228    }
229}
230
231/// Describes an NFT output, a globally unique token with metadata attached.
232#[derive(Clone, Debug, Eq, PartialEq, Ord, PartialOrd)]
233#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
234pub struct NftOutput {
235    // Amount of IOTA tokens held by the output.
236    amount: u64,
237    // Native tokens held by the output.
238    native_tokens: NativeTokens,
239    // Unique identifier of the NFT.
240    nft_id: NftId,
241    unlock_conditions: UnlockConditions,
242    features: Features,
243    immutable_features: Features,
244}
245
246impl NftOutput {
247    /// The [`Output`](crate::output::Output) kind of an [`NftOutput`].
248    pub const KIND: u8 = 6;
249    /// The set of allowed [`UnlockCondition`]s for an [`NftOutput`].
250    pub const ALLOWED_UNLOCK_CONDITIONS: UnlockConditionFlags = UnlockConditionFlags::ADDRESS
251        .union(UnlockConditionFlags::STORAGE_DEPOSIT_RETURN)
252        .union(UnlockConditionFlags::TIMELOCK)
253        .union(UnlockConditionFlags::EXPIRATION);
254    /// The set of allowed [`Feature`]s for an [`NftOutput`].
255    pub const ALLOWED_FEATURES: FeatureFlags = FeatureFlags::SENDER
256        .union(FeatureFlags::METADATA)
257        .union(FeatureFlags::TAG);
258    /// The set of allowed immutable [`Feature`]s for an [`NftOutput`].
259    pub const ALLOWED_IMMUTABLE_FEATURES: FeatureFlags = FeatureFlags::ISSUER.union(FeatureFlags::METADATA);
260
261    /// Creates a new [`NftOutput`] with a provided amount.
262    #[inline(always)]
263    pub fn new_with_amount(amount: u64, nft_id: NftId, token_supply: u64) -> Result<Self, Error> {
264        NftOutputBuilder::new_with_amount(amount, nft_id)?.finish(token_supply)
265    }
266
267    /// Creates a new [`NftOutput`] with a provided rent structure.
268    /// The amount will be set to the minimum storage deposit.
269    #[inline(always)]
270    pub fn new_with_minimum_storage_deposit(
271        nft_id: NftId,
272        rent_structure: RentStructure,
273        token_supply: u64,
274    ) -> Result<Self, Error> {
275        NftOutputBuilder::new_with_minimum_storage_deposit(rent_structure, nft_id)?.finish(token_supply)
276    }
277
278    /// Creates a new [`NftOutputBuilder`] with a provided amount.
279    #[inline(always)]
280    pub fn build_with_amount(amount: u64, nft_id: NftId) -> Result<NftOutputBuilder, Error> {
281        NftOutputBuilder::new_with_amount(amount, nft_id)
282    }
283
284    /// Creates a new [`NftOutputBuilder`] with a provided rent structure.
285    /// The amount will be set to the minimum storage deposit.
286    #[inline(always)]
287    pub fn build_with_minimum_storage_deposit(
288        rent_structure: RentStructure,
289        nft_id: NftId,
290    ) -> Result<NftOutputBuilder, Error> {
291        NftOutputBuilder::new_with_minimum_storage_deposit(rent_structure, nft_id)
292    }
293
294    ///
295    #[inline(always)]
296    pub fn amount(&self) -> u64 {
297        self.amount
298    }
299
300    ///
301    #[inline(always)]
302    pub fn native_tokens(&self) -> &NativeTokens {
303        &self.native_tokens
304    }
305
306    ///
307    #[inline(always)]
308    pub fn nft_id(&self) -> &NftId {
309        &self.nft_id
310    }
311
312    ///
313    #[inline(always)]
314    pub fn unlock_conditions(&self) -> &UnlockConditions {
315        &self.unlock_conditions
316    }
317
318    ///
319    #[inline(always)]
320    pub fn features(&self) -> &Features {
321        &self.features
322    }
323
324    ///
325    #[inline(always)]
326    pub fn immutable_features(&self) -> &Features {
327        &self.immutable_features
328    }
329
330    ///
331    #[inline(always)]
332    pub fn address(&self) -> &Address {
333        // An NftOutput must have an AddressUnlockCondition.
334        self.unlock_conditions
335            .address()
336            .map(|unlock_condition| unlock_condition.address())
337            .unwrap()
338    }
339
340    ///
341    #[inline(always)]
342    pub fn chain_id(&self) -> ChainId {
343        ChainId::Nft(self.nft_id)
344    }
345
346    ///
347    pub fn unlock(
348        &self,
349        output_id: &OutputId,
350        unlock: &Unlock,
351        inputs: &[(OutputId, &Output)],
352        context: &mut ValidationContext,
353    ) -> Result<(), ConflictReason> {
354        self.unlock_conditions()
355            .locked_address(self.address(), context.milestone_timestamp)
356            .unlock(unlock, inputs, context)?;
357
358        let nft_id = if self.nft_id().is_null() {
359            NftId::from(*output_id)
360        } else {
361            *self.nft_id()
362        };
363
364        context
365            .unlocked_addresses
366            .insert(Address::from(NftAddress::from(nft_id)));
367
368        Ok(())
369    }
370}
371
372impl StateTransitionVerifier for NftOutput {
373    fn creation(next_state: &Self, context: &ValidationContext) -> Result<(), StateTransitionError> {
374        if !next_state.nft_id.is_null() {
375            return Err(StateTransitionError::NonZeroCreatedId);
376        }
377
378        if let Some(issuer) = next_state.immutable_features().issuer() {
379            if !context.unlocked_addresses.contains(issuer.address()) {
380                return Err(StateTransitionError::IssuerNotUnlocked);
381            }
382        }
383
384        Ok(())
385    }
386
387    fn transition(
388        current_state: &Self,
389        next_state: &Self,
390        _context: &ValidationContext,
391    ) -> Result<(), StateTransitionError> {
392        if current_state.immutable_features != next_state.immutable_features {
393            return Err(StateTransitionError::MutatedImmutableField);
394        }
395
396        Ok(())
397    }
398
399    fn destruction(_current_state: &Self, _context: &ValidationContext) -> Result<(), StateTransitionError> {
400        Ok(())
401    }
402}
403
404impl Packable for NftOutput {
405    type UnpackError = Error;
406    type UnpackVisitor = ProtocolParameters;
407
408    fn pack<P: Packer>(&self, packer: &mut P) -> Result<(), P::Error> {
409        self.amount.pack(packer)?;
410        self.native_tokens.pack(packer)?;
411        self.nft_id.pack(packer)?;
412        self.unlock_conditions.pack(packer)?;
413        self.features.pack(packer)?;
414        self.immutable_features.pack(packer)?;
415
416        Ok(())
417    }
418
419    fn unpack<U: Unpacker, const VERIFY: bool>(
420        unpacker: &mut U,
421        visitor: &Self::UnpackVisitor,
422    ) -> Result<Self, UnpackError<Self::UnpackError, U::Error>> {
423        let amount = u64::unpack::<_, VERIFY>(unpacker, &()).coerce()?;
424
425        verify_output_amount::<VERIFY>(&amount, &visitor.token_supply()).map_err(UnpackError::Packable)?;
426
427        let native_tokens = NativeTokens::unpack::<_, VERIFY>(unpacker, &())?;
428        let nft_id = NftId::unpack::<_, VERIFY>(unpacker, &()).coerce()?;
429        let unlock_conditions = UnlockConditions::unpack::<_, VERIFY>(unpacker, visitor)?;
430
431        if VERIFY {
432            verify_unlock_conditions(&unlock_conditions, &nft_id).map_err(UnpackError::Packable)?;
433        }
434
435        let features = Features::unpack::<_, VERIFY>(unpacker, &())?;
436
437        if VERIFY {
438            verify_allowed_features(&features, NftOutput::ALLOWED_FEATURES).map_err(UnpackError::Packable)?;
439        }
440
441        let immutable_features = Features::unpack::<_, VERIFY>(unpacker, &())?;
442
443        if VERIFY {
444            verify_allowed_features(&immutable_features, NftOutput::ALLOWED_IMMUTABLE_FEATURES)
445                .map_err(UnpackError::Packable)?;
446        }
447
448        Ok(Self {
449            amount,
450            native_tokens,
451            nft_id,
452            unlock_conditions,
453            features,
454            immutable_features,
455        })
456    }
457}
458
459fn verify_unlock_conditions(unlock_conditions: &UnlockConditions, nft_id: &NftId) -> Result<(), Error> {
460    if let Some(unlock_condition) = unlock_conditions.address() {
461        if let Address::Nft(nft_address) = unlock_condition.address() {
462            if nft_address.nft_id() == nft_id {
463                return Err(Error::SelfDepositNft(*nft_id));
464            }
465        }
466    } else {
467        return Err(Error::MissingAddressUnlockCondition);
468    }
469
470    verify_allowed_unlock_conditions(unlock_conditions, NftOutput::ALLOWED_UNLOCK_CONDITIONS)
471}
472
473#[cfg(feature = "dto")]
474#[allow(missing_docs)]
475pub mod dto {
476    use serde::{Deserialize, Serialize};
477
478    use super::*;
479    use crate::{
480        error::dto::DtoError,
481        output::{
482            dto::OutputBuilderAmountDto, feature::dto::FeatureDto, native_token::dto::NativeTokenDto,
483            nft_id::dto::NftIdDto, unlock_condition::dto::UnlockConditionDto,
484        },
485    };
486
487    /// Describes an NFT output, a globally unique token with metadata attached.
488    #[derive(Clone, Debug, Eq, PartialEq, Serialize, Deserialize)]
489    pub struct NftOutputDto {
490        #[serde(rename = "type")]
491        pub kind: u8,
492        // Amount of IOTA tokens held by the output.
493        pub amount: String,
494        // Native tokens held by the output.
495        #[serde(rename = "nativeTokens", skip_serializing_if = "Vec::is_empty", default)]
496        pub native_tokens: Vec<NativeTokenDto>,
497        // Unique identifier of the NFT.
498        #[serde(rename = "nftId")]
499        pub nft_id: NftIdDto,
500        #[serde(rename = "unlockConditions")]
501        pub unlock_conditions: Vec<UnlockConditionDto>,
502        #[serde(skip_serializing_if = "Vec::is_empty", default)]
503        pub features: Vec<FeatureDto>,
504        #[serde(rename = "immutableFeatures", skip_serializing_if = "Vec::is_empty", default)]
505        pub immutable_features: Vec<FeatureDto>,
506    }
507
508    impl From<&NftOutput> for NftOutputDto {
509        fn from(value: &NftOutput) -> Self {
510            Self {
511                kind: NftOutput::KIND,
512                amount: value.amount().to_string(),
513                native_tokens: value.native_tokens().iter().map(Into::into).collect::<_>(),
514                nft_id: NftIdDto(value.nft_id().to_string()),
515                unlock_conditions: value.unlock_conditions().iter().map(Into::into).collect::<_>(),
516                features: value.features().iter().map(Into::into).collect::<_>(),
517                immutable_features: value.immutable_features().iter().map(Into::into).collect::<_>(),
518            }
519        }
520    }
521
522    impl NftOutput {
523        pub fn try_from_dto(value: &NftOutputDto, token_supply: u64) -> Result<NftOutput, DtoError> {
524            let mut builder = NftOutputBuilder::new_with_amount(
525                value
526                    .amount
527                    .parse::<u64>()
528                    .map_err(|_| DtoError::InvalidField("amount"))?,
529                (&value.nft_id).try_into()?,
530            )?;
531
532            for t in &value.native_tokens {
533                builder = builder.add_native_token(t.try_into()?);
534            }
535
536            for u in &value.unlock_conditions {
537                builder = builder.add_unlock_condition(UnlockCondition::try_from_dto(u, token_supply)?);
538            }
539
540            for b in &value.features {
541                builder = builder.add_feature(b.try_into()?);
542            }
543
544            for b in &value.immutable_features {
545                builder = builder.add_immutable_feature(b.try_into()?);
546            }
547
548            Ok(builder.finish(token_supply)?)
549        }
550
551        pub fn try_from_dtos(
552            amount: OutputBuilderAmountDto,
553            native_tokens: Option<Vec<NativeTokenDto>>,
554            nft_id: &NftIdDto,
555            unlock_conditions: Vec<UnlockConditionDto>,
556            features: Option<Vec<FeatureDto>>,
557            immutable_features: Option<Vec<FeatureDto>>,
558            token_supply: u64,
559        ) -> Result<NftOutput, DtoError> {
560            let nft_id = NftId::try_from(nft_id)?;
561
562            let mut builder = match amount {
563                OutputBuilderAmountDto::Amount(amount) => NftOutputBuilder::new_with_amount(
564                    amount.parse().map_err(|_| DtoError::InvalidField("amount"))?,
565                    nft_id,
566                )?,
567                OutputBuilderAmountDto::MinimumStorageDeposit(rent_structure) => {
568                    NftOutputBuilder::new_with_minimum_storage_deposit(rent_structure, nft_id)?
569                }
570            };
571
572            if let Some(native_tokens) = native_tokens {
573                let native_tokens = native_tokens
574                    .iter()
575                    .map(NativeToken::try_from)
576                    .collect::<Result<Vec<NativeToken>, DtoError>>()?;
577                builder = builder.with_native_tokens(native_tokens);
578            }
579
580            let unlock_conditions = unlock_conditions
581                .iter()
582                .map(|u| UnlockCondition::try_from_dto(u, token_supply))
583                .collect::<Result<Vec<UnlockCondition>, DtoError>>()?;
584            builder = builder.with_unlock_conditions(unlock_conditions);
585
586            if let Some(features) = features {
587                let features = features
588                    .iter()
589                    .map(Feature::try_from)
590                    .collect::<Result<Vec<Feature>, DtoError>>()?;
591                builder = builder.with_features(features);
592            }
593
594            if let Some(immutable_features) = immutable_features {
595                let immutable_features = immutable_features
596                    .iter()
597                    .map(Feature::try_from)
598                    .collect::<Result<Vec<Feature>, DtoError>>()?;
599                builder = builder.with_immutable_features(immutable_features);
600            }
601
602            Ok(builder.finish(token_supply)?)
603        }
604    }
605}