iota_types/stardust/output/
basic.rs

1// Copyright (c) 2024 IOTA Stiftung
2// SPDX-License-Identifier: Apache-2.0
3
4//! Rust types and logic for the Move counterparts in the `stardust` system
5//! package.
6
7use anyhow::Result;
8use iota_protocol_config::ProtocolConfig;
9use move_core_types::{ident_str, identifier::IdentStr, language_storage::StructTag};
10use serde::{Deserialize, Serialize};
11use serde_with::serde_as;
12
13use super::unlock_conditions::{
14    ExpirationUnlockCondition, StorageDepositReturnUnlockCondition, TimelockUnlockCondition,
15};
16use crate::{
17    STARDUST_ADDRESS, TypeTag,
18    balance::Balance,
19    base_types::{IotaAddress, MoveObjectType, ObjectID, SequenceNumber, TxContext},
20    coin::Coin,
21    collection_types::Bag,
22    error::IotaError,
23    id::UID,
24    object::{Data, MoveObject, Object, Owner},
25    stardust::{coin_type::CoinType, stardust_to_iota_address},
26};
27
28pub const BASIC_OUTPUT_MODULE_NAME: &IdentStr = ident_str!("basic_output");
29pub const BASIC_OUTPUT_STRUCT_NAME: &IdentStr = ident_str!("BasicOutput");
30
31/// Rust version of the stardust basic output.
32#[serde_as]
33#[derive(Debug, Serialize, Deserialize, Clone, Eq, PartialEq)]
34pub struct BasicOutput {
35    /// Hash of the `OutputId` that was migrated.
36    pub id: UID,
37
38    /// The amount of coins held by the output.
39    pub balance: Balance,
40
41    /// The `Bag` holds native tokens, key-ed by the stringified type of the
42    /// asset. Example: key: "0xabcded::soon::SOON", value:
43    /// Balance<0xabcded::soon::SOON>.
44    pub native_tokens: Bag,
45
46    /// The storage deposit return unlock condition.
47    pub storage_deposit_return: Option<StorageDepositReturnUnlockCondition>,
48    /// The timelock unlock condition.
49    pub timelock: Option<TimelockUnlockCondition>,
50    /// The expiration unlock condition.
51    pub expiration: Option<ExpirationUnlockCondition>,
52
53    // Possible features, they have no effect and only here to hold data until the object is
54    // deleted.
55    /// The metadata feature.
56    pub metadata: Option<Vec<u8>>,
57    /// The tag feature.
58    pub tag: Option<Vec<u8>>,
59    /// The sender feature.
60    pub sender: Option<IotaAddress>,
61}
62
63impl BasicOutput {
64    /// Construct the basic output with an empty [`Bag`] using the
65    /// Output Header ID and Stardust
66    /// [`BasicOutput`][iota_stardust_sdk::types::block::output::BasicOutput].
67    pub fn new(
68        header_object_id: ObjectID,
69        output: &iota_stardust_sdk::types::block::output::BasicOutput,
70    ) -> Result<Self> {
71        let id = UID::new(header_object_id);
72        let balance = Balance::new(output.amount());
73        let native_tokens = Default::default();
74        let unlock_conditions = output.unlock_conditions();
75        let storage_deposit_return = unlock_conditions
76            .storage_deposit_return()
77            .map(|unlock| unlock.try_into())
78            .transpose()?;
79        let timelock = unlock_conditions.timelock().map(|unlock| unlock.into());
80        let expiration = output
81            .unlock_conditions()
82            .expiration()
83            .map(|expiration| ExpirationUnlockCondition::new(output.address(), expiration))
84            .transpose()?;
85        let metadata = output
86            .features()
87            .metadata()
88            .map(|metadata| metadata.data().to_vec());
89        let tag = output.features().tag().map(|tag| tag.tag().to_vec());
90        let sender = output
91            .features()
92            .sender()
93            .map(|sender| stardust_to_iota_address(sender.address()))
94            .transpose()?;
95
96        Ok(BasicOutput {
97            id,
98            balance,
99            native_tokens,
100            storage_deposit_return,
101            timelock,
102            expiration,
103            metadata,
104            tag,
105            sender,
106        })
107    }
108
109    /// Returns the struct tag of the BasicOutput struct
110    pub fn tag(type_param: TypeTag) -> StructTag {
111        StructTag {
112            address: STARDUST_ADDRESS,
113            module: BASIC_OUTPUT_MODULE_NAME.to_owned(),
114            name: BASIC_OUTPUT_STRUCT_NAME.to_owned(),
115            type_params: vec![type_param],
116        }
117    }
118
119    /// Infer whether this object can resolve into a simple coin.
120    ///
121    /// Returns `true` in particular when the given milestone timestamp is equal
122    /// or past the unix timestamp in a present timelock and no other unlock
123    /// condition or metadata, tag, sender feature is present.
124    pub fn is_simple_coin(&self, target_milestone_timestamp_sec: u32) -> bool {
125        !(self.expiration.is_some()
126            || self.storage_deposit_return.is_some()
127            || self
128                .timelock
129                .as_ref()
130                .is_some_and(|timelock| target_milestone_timestamp_sec < timelock.unix_time)
131            || self.metadata.is_some()
132            || self.tag.is_some()
133            || self.sender.is_some())
134    }
135
136    pub fn to_genesis_object(
137        &self,
138        owner: IotaAddress,
139        protocol_config: &ProtocolConfig,
140        tx_context: &TxContext,
141        version: SequenceNumber,
142        coin_type: &CoinType,
143    ) -> Result<Object> {
144        let move_object = {
145            MoveObject::new_from_execution(
146                BasicOutput::tag(coin_type.to_type_tag()).into(),
147                version,
148                bcs::to_bytes(self)?,
149                protocol_config,
150            )?
151        };
152        // Resolve ownership
153        let owner = if self.expiration.is_some() {
154            Owner::Shared {
155                initial_shared_version: version,
156            }
157        } else {
158            Owner::AddressOwner(owner)
159        };
160        Ok(Object::new_from_genesis(
161            Data::Move(move_object),
162            owner,
163            tx_context.digest(),
164        ))
165    }
166
167    pub fn into_genesis_coin_object(
168        self,
169        owner: IotaAddress,
170        protocol_config: &ProtocolConfig,
171        tx_context: &TxContext,
172        version: SequenceNumber,
173        coin_type: &CoinType,
174    ) -> Result<Object> {
175        create_coin(
176            self.id,
177            owner,
178            self.balance.value(),
179            tx_context,
180            version,
181            protocol_config,
182            coin_type,
183        )
184    }
185
186    /// Create a `BasicOutput` from BCS bytes.
187    pub fn from_bcs_bytes(content: &[u8]) -> Result<Self, IotaError> {
188        bcs::from_bytes(content).map_err(|err| IotaError::ObjectDeserialization {
189            error: format!("Unable to deserialize BasicOutput object: {:?}", err),
190        })
191    }
192
193    /// Whether the given `StructTag` represents a `BasicOutput`.
194    pub fn is_basic_output(s: &StructTag) -> bool {
195        s.address == STARDUST_ADDRESS
196            && s.module.as_ident_str() == BASIC_OUTPUT_MODULE_NAME
197            && s.name.as_ident_str() == BASIC_OUTPUT_STRUCT_NAME
198    }
199}
200
201pub(crate) fn create_coin(
202    object_id: UID,
203    owner: IotaAddress,
204    amount: u64,
205    tx_context: &TxContext,
206    version: SequenceNumber,
207    protocol_config: &ProtocolConfig,
208    coin_type: &CoinType,
209) -> Result<Object> {
210    let coin = Coin::new(object_id, amount);
211    let move_object = {
212        MoveObject::new_from_execution(
213            MoveObjectType::from(Coin::type_(coin_type.to_type_tag())),
214            version,
215            bcs::to_bytes(&coin)?,
216            protocol_config,
217        )?
218    };
219    // Resolve ownership
220    let owner = Owner::AddressOwner(owner);
221    Ok(Object::new_from_genesis(
222        Data::Move(move_object),
223        owner,
224        tx_context.digest(),
225    ))
226}
227
228impl TryFrom<&Object> for BasicOutput {
229    type Error = IotaError;
230    fn try_from(object: &Object) -> Result<Self, Self::Error> {
231        match &object.data {
232            Data::Move(o) => {
233                if o.type_().is_basic_output() {
234                    return BasicOutput::from_bcs_bytes(o.contents());
235                }
236            }
237            Data::Package(_) => {}
238        }
239
240        Err(IotaError::Type {
241            error: format!("Object type is not a BasicOutput: {:?}", object),
242        })
243    }
244}