bee_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::{
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) -> Result<Self, Error> {
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 => return Err(Error::CannotReplaceMissingField),
106        }
107        Ok(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) -> Result<Self, Error> {
126        match self.features.iter_mut().find(|f| f.kind() == feature.kind()) {
127            Some(f) => *f = feature,
128            None => return Err(Error::CannotReplaceMissingField),
129        }
130        Ok(self)
131    }
132
133    ///
134    pub fn finish(self, token_supply: u64) -> 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        verify_output_amount::<true>(&output.amount, &token_supply)?;
158
159        Ok(output)
160    }
161
162    /// Finishes the [`BasicOutputBuilder`] into an [`Output`].
163    pub fn finish_output(self, token_supply: u64) -> Result<Output, Error> {
164        Ok(Output::Basic(self.finish(token_supply)?))
165    }
166}
167
168impl From<&BasicOutput> for BasicOutputBuilder {
169    fn from(output: &BasicOutput) -> Self {
170        BasicOutputBuilder {
171            amount: OutputBuilderAmount::Amount(output.amount),
172            native_tokens: output.native_tokens.to_vec(),
173            unlock_conditions: output.unlock_conditions.to_vec(),
174            features: output.features.to_vec(),
175        }
176    }
177}
178
179/// Describes a basic output with optional features.
180#[derive(Clone, Debug, Eq, PartialEq, Ord, PartialOrd, Packable)]
181#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
182#[packable(unpack_error = Error)]
183#[packable(unpack_visitor = ProtocolParameters)]
184pub struct BasicOutput {
185    // Amount of IOTA tokens held by the output.
186    #[packable(verify_with = verify_output_amount_packable)]
187    amount: u64,
188    // Native tokens held by the output.
189    native_tokens: NativeTokens,
190    #[packable(verify_with = verify_unlock_conditions_packable)]
191    unlock_conditions: UnlockConditions,
192    #[packable(verify_with = verify_features_packable)]
193    features: Features,
194}
195
196impl BasicOutput {
197    /// The [`Output`](crate::output::Output) kind of an [`BasicOutput`].
198    pub const KIND: u8 = 3;
199
200    /// The set of allowed [`UnlockCondition`]s for an [`BasicOutput`].
201    const ALLOWED_UNLOCK_CONDITIONS: UnlockConditionFlags = UnlockConditionFlags::ADDRESS
202        .union(UnlockConditionFlags::STORAGE_DEPOSIT_RETURN)
203        .union(UnlockConditionFlags::TIMELOCK)
204        .union(UnlockConditionFlags::EXPIRATION);
205    /// The set of allowed [`Feature`]s for an [`BasicOutput`].
206    pub const ALLOWED_FEATURES: FeatureFlags = FeatureFlags::SENDER
207        .union(FeatureFlags::METADATA)
208        .union(FeatureFlags::TAG);
209
210    /// Creates a new [`BasicOutput`] with a provided amount.
211    #[inline(always)]
212    pub fn new_with_amount(amount: u64, token_supply: u64) -> Result<Self, Error> {
213        BasicOutputBuilder::new_with_amount(amount)?.finish(token_supply)
214    }
215
216    /// Creates a new [`BasicOutput`] with a provided rent structure.
217    /// The amount will be set to the minimum storage deposit.
218    #[inline(always)]
219    pub fn new_with_minimum_storage_deposit(rent_structure: RentStructure, token_supply: u64) -> Result<Self, Error> {
220        BasicOutputBuilder::new_with_minimum_storage_deposit(rent_structure)?.finish(token_supply)
221    }
222
223    /// Creates a new [`BasicOutputBuilder`] with a provided amount.
224    #[inline(always)]
225    pub fn build_with_amount(amount: u64) -> Result<BasicOutputBuilder, Error> {
226        BasicOutputBuilder::new_with_amount(amount)
227    }
228
229    /// Creates a new [`BasicOutputBuilder`] with a provided rent structure.
230    /// The amount will be set to the minimum storage deposit.
231    #[inline(always)]
232    pub fn build_with_minimum_storage_deposit(rent_structure: RentStructure) -> Result<BasicOutputBuilder, Error> {
233        BasicOutputBuilder::new_with_minimum_storage_deposit(rent_structure)
234    }
235
236    ///
237    #[inline(always)]
238    pub fn amount(&self) -> u64 {
239        self.amount
240    }
241
242    ///
243    #[inline(always)]
244    pub fn native_tokens(&self) -> &NativeTokens {
245        &self.native_tokens
246    }
247
248    ///
249    #[inline(always)]
250    pub fn unlock_conditions(&self) -> &UnlockConditions {
251        &self.unlock_conditions
252    }
253
254    ///
255    #[inline(always)]
256    pub fn features(&self) -> &Features {
257        &self.features
258    }
259
260    ///
261    #[inline(always)]
262    pub fn address(&self) -> &Address {
263        // An BasicOutput must have an AddressUnlockCondition.
264        self.unlock_conditions
265            .address()
266            .map(|unlock_condition| unlock_condition.address())
267            .unwrap()
268    }
269
270    ///
271    pub fn unlock(
272        &self,
273        _output_id: &OutputId,
274        unlock: &Unlock,
275        inputs: &[(OutputId, &Output)],
276        context: &mut ValidationContext,
277    ) -> Result<(), ConflictReason> {
278        self.unlock_conditions()
279            .locked_address(self.address(), context.milestone_timestamp)
280            .unlock(unlock, inputs, context)
281    }
282
283    /// Returns the address of the unlock conditions if the output is a simple deposit.
284    /// Simple deposit outputs are basic outputs with only an address unlock condition, no native tokens and no
285    /// features. They are used to return storage deposits.
286    pub fn simple_deposit_address(&self) -> Option<&Address> {
287        if let [UnlockCondition::Address(address)] = self.unlock_conditions().as_ref() {
288            if self.native_tokens.is_empty() && self.features.is_empty() {
289                return Some(address.address());
290            }
291        }
292
293        None
294    }
295}
296
297fn verify_unlock_conditions<const VERIFY: bool>(unlock_conditions: &UnlockConditions) -> Result<(), Error> {
298    if VERIFY {
299        if unlock_conditions.address().is_none() {
300            Err(Error::MissingAddressUnlockCondition)
301        } else {
302            verify_allowed_unlock_conditions(unlock_conditions, BasicOutput::ALLOWED_UNLOCK_CONDITIONS)
303        }
304    } else {
305        Ok(())
306    }
307}
308
309fn verify_unlock_conditions_packable<const VERIFY: bool>(
310    unlock_conditions: &UnlockConditions,
311    _: &ProtocolParameters,
312) -> Result<(), Error> {
313    verify_unlock_conditions::<VERIFY>(unlock_conditions)
314}
315
316fn verify_features<const VERIFY: bool>(blocks: &Features) -> Result<(), Error> {
317    if VERIFY {
318        verify_allowed_features(blocks, BasicOutput::ALLOWED_FEATURES)
319    } else {
320        Ok(())
321    }
322}
323
324fn verify_features_packable<const VERIFY: bool>(blocks: &Features, _: &ProtocolParameters) -> Result<(), Error> {
325    verify_features::<VERIFY>(blocks)
326}
327
328#[cfg(feature = "dto")]
329#[allow(missing_docs)]
330pub mod dto {
331    use serde::{Deserialize, Serialize};
332
333    use super::*;
334    use crate::{
335        error::dto::DtoError,
336        output::{
337            dto::OutputBuilderAmountDto, feature::dto::FeatureDto, native_token::dto::NativeTokenDto,
338            unlock_condition::dto::UnlockConditionDto,
339        },
340    };
341
342    /// Describes a basic output.
343    #[derive(Clone, Debug, Eq, PartialEq, Serialize, Deserialize)]
344    pub struct BasicOutputDto {
345        #[serde(rename = "type")]
346        pub kind: u8,
347        // Amount of IOTA tokens held by the output.
348        pub amount: String,
349        // Native tokens held by the output.
350        #[serde(rename = "nativeTokens", skip_serializing_if = "Vec::is_empty", default)]
351        pub native_tokens: Vec<NativeTokenDto>,
352        #[serde(rename = "unlockConditions")]
353        pub unlock_conditions: Vec<UnlockConditionDto>,
354        #[serde(skip_serializing_if = "Vec::is_empty", default)]
355        pub features: Vec<FeatureDto>,
356    }
357
358    impl From<&BasicOutput> for BasicOutputDto {
359        fn from(value: &BasicOutput) -> Self {
360            Self {
361                kind: BasicOutput::KIND,
362                amount: value.amount().to_string(),
363                native_tokens: value.native_tokens().iter().map(Into::into).collect::<_>(),
364                unlock_conditions: value.unlock_conditions().iter().map(Into::into).collect::<_>(),
365                features: value.features().iter().map(Into::into).collect::<_>(),
366            }
367        }
368    }
369
370    impl BasicOutput {
371        pub fn try_from_dto(value: &BasicOutputDto, token_supply: u64) -> Result<BasicOutput, DtoError> {
372            let mut builder = BasicOutputBuilder::new_with_amount(
373                value.amount.parse().map_err(|_| DtoError::InvalidField("amount"))?,
374            )?;
375
376            for t in &value.native_tokens {
377                builder = builder.add_native_token(t.try_into()?);
378            }
379
380            for u in &value.unlock_conditions {
381                builder = builder.add_unlock_condition(UnlockCondition::try_from_dto(u, token_supply)?);
382            }
383
384            for b in &value.features {
385                builder = builder.add_feature(b.try_into()?);
386            }
387
388            Ok(builder.finish(token_supply)?)
389        }
390
391        pub fn try_from_dtos(
392            amount: OutputBuilderAmountDto,
393            native_tokens: Option<Vec<NativeTokenDto>>,
394            unlock_conditions: Vec<UnlockConditionDto>,
395            features: Option<Vec<FeatureDto>>,
396            token_supply: u64,
397        ) -> Result<BasicOutput, DtoError> {
398            let mut builder = match amount {
399                OutputBuilderAmountDto::Amount(amount) => {
400                    BasicOutputBuilder::new_with_amount(amount.parse().map_err(|_| DtoError::InvalidField("amount"))?)?
401                }
402                OutputBuilderAmountDto::MinimumStorageDeposit(rent_structure) => {
403                    BasicOutputBuilder::new_with_minimum_storage_deposit(rent_structure)?
404                }
405            };
406
407            if let Some(native_tokens) = native_tokens {
408                let native_tokens = native_tokens
409                    .iter()
410                    .map(NativeToken::try_from)
411                    .collect::<Result<Vec<NativeToken>, DtoError>>()?;
412                builder = builder.with_native_tokens(native_tokens);
413            }
414
415            let unlock_conditions = unlock_conditions
416                .iter()
417                .map(|u| UnlockCondition::try_from_dto(u, token_supply))
418                .collect::<Result<Vec<UnlockCondition>, DtoError>>()?;
419            builder = builder.with_unlock_conditions(unlock_conditions);
420
421            if let Some(features) = features {
422                let features = features
423                    .iter()
424                    .map(Feature::try_from)
425                    .collect::<Result<Vec<Feature>, DtoError>>()?;
426                builder = builder.with_features(features);
427            }
428
429            Ok(builder.finish(token_supply)?)
430        }
431    }
432}