Skip to main content

iota_types/block/output/
basic.rs

1// Copyright 2021-2022 IOTA Stiftung
2// SPDX-License-Identifier: Apache-2.0
3
4use alloc::vec::Vec;
5
6use packable::Packable;
7
8use crate::block::{
9    address::Address,
10    output::{
11        feature::{verify_allowed_features, Feature, FeatureFlags, Features},
12        unlock_condition::{verify_allowed_unlock_conditions, UnlockCondition, UnlockConditionFlags, UnlockConditions},
13        verify_output_amount, verify_output_amount_packable, NativeToken, NativeTokens, Output, OutputBuilderAmount,
14        OutputId, Rent, RentStructure,
15    },
16    protocol::ProtocolParameters,
17    semantic::{ConflictReason, ValidationContext},
18    unlock::Unlock,
19    Error,
20};
21
22///
23#[derive(Clone)]
24#[must_use]
25pub struct BasicOutputBuilder {
26    amount: OutputBuilderAmount,
27    native_tokens: Vec<NativeToken>,
28    unlock_conditions: Vec<UnlockCondition>,
29    features: Vec<Feature>,
30}
31
32impl BasicOutputBuilder {
33    /// Creates a [`BasicOutputBuilder`] with a provided amount.
34    #[inline(always)]
35    pub fn new_with_amount(amount: u64) -> Result<Self, Error> {
36        Self::new(OutputBuilderAmount::Amount(amount))
37    }
38
39    /// Creates an [`BasicOutputBuilder`] with a provided rent structure.
40    /// The amount will be set to the minimum storage deposit.
41    #[inline(always)]
42    pub fn new_with_minimum_storage_deposit(rent_structure: RentStructure) -> Result<Self, Error> {
43        Self::new(OutputBuilderAmount::MinimumStorageDeposit(rent_structure))
44    }
45
46    fn new(amount: OutputBuilderAmount) -> Result<Self, Error> {
47        Ok(Self {
48            amount,
49            native_tokens: Vec::new(),
50            unlock_conditions: Vec::new(),
51            features: Vec::new(),
52        })
53    }
54
55    /// Sets the amount to the provided value.
56    #[inline(always)]
57    pub fn with_amount(mut self, amount: u64) -> Result<Self, Error> {
58        self.amount = OutputBuilderAmount::Amount(amount);
59        Ok(self)
60    }
61
62    /// Sets the amount to the minimum storage deposit.
63    #[inline(always)]
64    pub fn with_minimum_storage_deposit(mut self, rent_structure: RentStructure) -> Self {
65        self.amount = OutputBuilderAmount::MinimumStorageDeposit(rent_structure);
66        self
67    }
68
69    ///
70    #[inline(always)]
71    pub fn add_native_token(mut self, native_token: NativeToken) -> Self {
72        self.native_tokens.push(native_token);
73        self
74    }
75
76    ///
77    #[inline(always)]
78    pub fn with_native_tokens(mut self, native_tokens: impl IntoIterator<Item = NativeToken>) -> Self {
79        self.native_tokens = native_tokens.into_iter().collect();
80        self
81    }
82
83    ///
84    #[inline(always)]
85    pub fn add_unlock_condition(mut self, unlock_condition: UnlockCondition) -> Self {
86        self.unlock_conditions.push(unlock_condition);
87        self
88    }
89
90    ///
91    #[inline(always)]
92    pub fn with_unlock_conditions(mut self, unlock_conditions: impl IntoIterator<Item = UnlockCondition>) -> Self {
93        self.unlock_conditions = unlock_conditions.into_iter().collect();
94        self
95    }
96
97    ///
98    pub fn replace_unlock_condition(mut self, unlock_condition: UnlockCondition) -> Self {
99        match self
100            .unlock_conditions
101            .iter_mut()
102            .find(|u| u.kind() == unlock_condition.kind())
103        {
104            Some(u) => *u = unlock_condition,
105            None => self.unlock_conditions.push(unlock_condition),
106        }
107        self
108    }
109
110    ///
111    #[inline(always)]
112    pub fn add_feature(mut self, feature: Feature) -> Self {
113        self.features.push(feature);
114        self
115    }
116
117    ///
118    #[inline(always)]
119    pub fn with_features(mut self, features: impl IntoIterator<Item = Feature>) -> Self {
120        self.features = features.into_iter().collect();
121        self
122    }
123
124    ///
125    pub fn replace_feature(mut self, feature: Feature) -> Self {
126        match self.features.iter_mut().find(|f| f.kind() == feature.kind()) {
127            Some(f) => *f = feature,
128            None => self.features.push(feature),
129        }
130        self
131    }
132
133    ///
134    pub fn finish_unverified(self) -> Result<BasicOutput, Error> {
135        let unlock_conditions = UnlockConditions::new(self.unlock_conditions)?;
136
137        verify_unlock_conditions::<true>(&unlock_conditions)?;
138
139        let features = Features::new(self.features)?;
140
141        verify_features::<true>(&features)?;
142
143        let mut output = BasicOutput {
144            amount: 1u64,
145            native_tokens: NativeTokens::new(self.native_tokens)?,
146            unlock_conditions,
147            features,
148        };
149
150        output.amount = match self.amount {
151            OutputBuilderAmount::Amount(amount) => amount,
152            OutputBuilderAmount::MinimumStorageDeposit(rent_structure) => {
153                Output::Basic(output.clone()).rent_cost(&rent_structure)
154            }
155        };
156
157        Ok(output)
158    }
159
160    ///
161    pub fn finish(self, token_supply: u64) -> Result<BasicOutput, Error> {
162        let output = self.finish_unverified()?;
163
164        verify_output_amount::<true>(&output.amount, &token_supply)?;
165
166        Ok(output)
167    }
168
169    /// Finishes the [`BasicOutputBuilder`] into an [`Output`].
170    pub fn finish_output(self, token_supply: u64) -> Result<Output, Error> {
171        Ok(Output::Basic(self.finish(token_supply)?))
172    }
173}
174
175impl From<&BasicOutput> for BasicOutputBuilder {
176    fn from(output: &BasicOutput) -> Self {
177        Self {
178            amount: OutputBuilderAmount::Amount(output.amount),
179            native_tokens: output.native_tokens.to_vec(),
180            unlock_conditions: output.unlock_conditions.to_vec(),
181            features: output.features.to_vec(),
182        }
183    }
184}
185
186/// Describes a basic output with optional features.
187#[derive(Clone, Debug, Eq, PartialEq, Ord, PartialOrd, Hash, Packable)]
188#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
189#[packable(unpack_error = Error)]
190#[packable(unpack_visitor = ProtocolParameters)]
191pub struct BasicOutput {
192    // Amount of IOTA tokens held by the output.
193    #[packable(verify_with = verify_output_amount_packable)]
194    amount: u64,
195    // Native tokens held by the output.
196    native_tokens: NativeTokens,
197    #[packable(verify_with = verify_unlock_conditions_packable)]
198    unlock_conditions: UnlockConditions,
199    #[packable(verify_with = verify_features_packable)]
200    features: Features,
201}
202
203impl BasicOutput {
204    /// The [`Output`](crate::block::output::Output) kind of an [`BasicOutput`].
205    pub const KIND: u8 = 3;
206
207    /// The set of allowed [`UnlockCondition`]s for an [`BasicOutput`].
208    const ALLOWED_UNLOCK_CONDITIONS: UnlockConditionFlags = UnlockConditionFlags::ADDRESS
209        .union(UnlockConditionFlags::STORAGE_DEPOSIT_RETURN)
210        .union(UnlockConditionFlags::TIMELOCK)
211        .union(UnlockConditionFlags::EXPIRATION);
212    /// The set of allowed [`Feature`]s for an [`BasicOutput`].
213    pub const ALLOWED_FEATURES: FeatureFlags = FeatureFlags::SENDER
214        .union(FeatureFlags::METADATA)
215        .union(FeatureFlags::TAG);
216
217    /// Creates a new [`BasicOutput`] with a provided amount.
218    #[inline(always)]
219    pub fn new_with_amount(amount: u64, token_supply: u64) -> Result<Self, Error> {
220        BasicOutputBuilder::new_with_amount(amount)?.finish(token_supply)
221    }
222
223    /// Creates a new [`BasicOutput`] with a provided rent structure.
224    /// The amount will be set to the minimum storage deposit.
225    #[inline(always)]
226    pub fn new_with_minimum_storage_deposit(rent_structure: RentStructure, token_supply: u64) -> Result<Self, Error> {
227        BasicOutputBuilder::new_with_minimum_storage_deposit(rent_structure)?.finish(token_supply)
228    }
229
230    /// Creates a new [`BasicOutputBuilder`] with a provided amount.
231    #[inline(always)]
232    pub fn build_with_amount(amount: u64) -> Result<BasicOutputBuilder, Error> {
233        BasicOutputBuilder::new_with_amount(amount)
234    }
235
236    /// Creates a new [`BasicOutputBuilder`] with a provided rent structure.
237    /// The amount will be set to the minimum storage deposit.
238    #[inline(always)]
239    pub fn build_with_minimum_storage_deposit(rent_structure: RentStructure) -> Result<BasicOutputBuilder, Error> {
240        BasicOutputBuilder::new_with_minimum_storage_deposit(rent_structure)
241    }
242
243    ///
244    #[inline(always)]
245    pub fn amount(&self) -> u64 {
246        self.amount
247    }
248
249    ///
250    #[inline(always)]
251    pub fn native_tokens(&self) -> &NativeTokens {
252        &self.native_tokens
253    }
254
255    ///
256    #[inline(always)]
257    pub fn unlock_conditions(&self) -> &UnlockConditions {
258        &self.unlock_conditions
259    }
260
261    ///
262    #[inline(always)]
263    pub fn features(&self) -> &Features {
264        &self.features
265    }
266
267    ///
268    #[inline(always)]
269    pub fn address(&self) -> &Address {
270        // An BasicOutput must have an AddressUnlockCondition.
271        self.unlock_conditions
272            .address()
273            .map(|unlock_condition| unlock_condition.address())
274            .unwrap()
275    }
276
277    ///
278    pub fn unlock(
279        &self,
280        _output_id: &OutputId,
281        unlock: &Unlock,
282        inputs: &[(OutputId, &Output)],
283        context: &mut ValidationContext<'_>,
284    ) -> Result<(), ConflictReason> {
285        self.unlock_conditions()
286            .locked_address(self.address(), context.milestone_timestamp)
287            .unlock(unlock, inputs, context)
288    }
289
290    /// Returns the address of the unlock conditions if the output is a simple deposit.
291    /// Simple deposit outputs are basic outputs with only an address unlock condition, no native tokens and no
292    /// features. They are used to return storage deposits.
293    pub fn simple_deposit_address(&self) -> Option<&Address> {
294        if let [UnlockCondition::Address(address)] = self.unlock_conditions().as_ref() {
295            if self.native_tokens.is_empty() && self.features.is_empty() {
296                return Some(address.address());
297            }
298        }
299
300        None
301    }
302}
303
304fn verify_unlock_conditions<const VERIFY: bool>(unlock_conditions: &UnlockConditions) -> Result<(), Error> {
305    if VERIFY {
306        if unlock_conditions.address().is_none() {
307            Err(Error::MissingAddressUnlockCondition)
308        } else {
309            verify_allowed_unlock_conditions(unlock_conditions, BasicOutput::ALLOWED_UNLOCK_CONDITIONS)
310        }
311    } else {
312        Ok(())
313    }
314}
315
316fn verify_unlock_conditions_packable<const VERIFY: bool>(
317    unlock_conditions: &UnlockConditions,
318    _: &ProtocolParameters,
319) -> Result<(), Error> {
320    verify_unlock_conditions::<VERIFY>(unlock_conditions)
321}
322
323fn verify_features<const VERIFY: bool>(blocks: &Features) -> Result<(), Error> {
324    if VERIFY {
325        verify_allowed_features(blocks, BasicOutput::ALLOWED_FEATURES)
326    } else {
327        Ok(())
328    }
329}
330
331fn verify_features_packable<const VERIFY: bool>(blocks: &Features, _: &ProtocolParameters) -> Result<(), Error> {
332    verify_features::<VERIFY>(blocks)
333}
334
335#[cfg(feature = "dto")]
336#[allow(missing_docs)]
337pub mod dto {
338    use serde::{Deserialize, Serialize};
339
340    use super::*;
341    use crate::block::{
342        error::dto::DtoError,
343        output::{
344            dto::OutputBuilderAmountDto, feature::dto::FeatureDto, native_token::dto::NativeTokenDto,
345            unlock_condition::dto::UnlockConditionDto,
346        },
347    };
348
349    /// Describes a basic output.
350    #[derive(Clone, Debug, Eq, PartialEq, Serialize, Deserialize)]
351    pub struct BasicOutputDto {
352        #[serde(rename = "type")]
353        pub kind: u8,
354        // Amount of IOTA tokens held by the output.
355        pub amount: String,
356        // Native tokens held by the output.
357        #[serde(rename = "nativeTokens", skip_serializing_if = "Vec::is_empty", default)]
358        pub native_tokens: Vec<NativeTokenDto>,
359        #[serde(rename = "unlockConditions")]
360        pub unlock_conditions: Vec<UnlockConditionDto>,
361        #[serde(skip_serializing_if = "Vec::is_empty", default)]
362        pub features: Vec<FeatureDto>,
363    }
364
365    impl From<&BasicOutput> for BasicOutputDto {
366        fn from(value: &BasicOutput) -> Self {
367            Self {
368                kind: BasicOutput::KIND,
369                amount: value.amount().to_string(),
370                native_tokens: value.native_tokens().iter().map(Into::into).collect::<_>(),
371                unlock_conditions: value.unlock_conditions().iter().map(Into::into).collect::<_>(),
372                features: value.features().iter().map(Into::into).collect::<_>(),
373            }
374        }
375    }
376
377    impl BasicOutput {
378        fn _try_from_dto(value: &BasicOutputDto) -> Result<BasicOutputBuilder, DtoError> {
379            let mut builder = BasicOutputBuilder::new_with_amount(
380                value.amount.parse().map_err(|_| DtoError::InvalidField("amount"))?,
381            )?;
382
383            for t in &value.native_tokens {
384                builder = builder.add_native_token(t.try_into()?);
385            }
386
387            for b in &value.features {
388                builder = builder.add_feature(b.try_into()?);
389            }
390
391            Ok(builder)
392        }
393
394        pub fn try_from_dto(value: &BasicOutputDto, token_supply: u64) -> Result<Self, DtoError> {
395            let mut builder = Self::_try_from_dto(value)?;
396
397            for u in &value.unlock_conditions {
398                builder = builder.add_unlock_condition(UnlockCondition::try_from_dto(u, token_supply)?);
399            }
400
401            Ok(builder.finish(token_supply)?)
402        }
403
404        pub fn try_from_dto_unverified(value: &BasicOutputDto) -> Result<Self, DtoError> {
405            let mut builder = Self::_try_from_dto(value)?;
406
407            for u in &value.unlock_conditions {
408                builder = builder.add_unlock_condition(UnlockCondition::try_from_dto_unverified(u)?);
409            }
410
411            Ok(builder.finish_unverified()?)
412        }
413
414        pub fn try_from_dtos(
415            amount: OutputBuilderAmountDto,
416            native_tokens: Option<Vec<NativeTokenDto>>,
417            unlock_conditions: Vec<UnlockConditionDto>,
418            features: Option<Vec<FeatureDto>>,
419            token_supply: u64,
420        ) -> Result<Self, DtoError> {
421            let mut builder = match amount {
422                OutputBuilderAmountDto::Amount(amount) => {
423                    BasicOutputBuilder::new_with_amount(amount.parse().map_err(|_| DtoError::InvalidField("amount"))?)?
424                }
425                OutputBuilderAmountDto::MinimumStorageDeposit(rent_structure) => {
426                    BasicOutputBuilder::new_with_minimum_storage_deposit(rent_structure)?
427                }
428            };
429
430            if let Some(native_tokens) = native_tokens {
431                let native_tokens = native_tokens
432                    .iter()
433                    .map(NativeToken::try_from)
434                    .collect::<Result<Vec<NativeToken>, DtoError>>()?;
435                builder = builder.with_native_tokens(native_tokens);
436            }
437
438            let unlock_conditions = unlock_conditions
439                .iter()
440                .map(|u| UnlockCondition::try_from_dto(u, token_supply))
441                .collect::<Result<Vec<UnlockCondition>, DtoError>>()?;
442            builder = builder.with_unlock_conditions(unlock_conditions);
443
444            if let Some(features) = features {
445                let features = features
446                    .iter()
447                    .map(Feature::try_from)
448                    .collect::<Result<Vec<Feature>, DtoError>>()?;
449                builder = builder.with_features(features);
450            }
451
452            Ok(builder.finish(token_supply)?)
453        }
454    }
455}