iota_types/stardust/
coin_kind.rs

1// Copyright (c) 2024 IOTA Stiftung
2// SPDX-License-Identifier: Apache-2.0
3
4//! Coin kinds introduced with the stardust models.
5//!
6//! We define as coin kinds objects that hold some kind of `Balance`
7//! and comply with the `Coin` type layout:
8//!
9//! ```rust
10//! struct T {
11//!     id: iota_types::id::UID,
12//!     balance: iota_types::balance::Balance,
13//!     // ...
14//! }
15//! ```
16
17use std::mem::size_of;
18
19use crate::{
20    balance::Balance,
21    gas_coin::GAS,
22    object::{ID_END_INDEX, Object},
23    stardust::output::{AliasOutput, BasicOutput, NftOutput},
24    timelock::timelock::TimeLock,
25};
26
27/// Infer whether the object is a kind of gas coin.
28pub fn is_gas_coin_kind(object: &Object) -> bool {
29    let Some(struct_tag) = object.struct_tag() else {
30        return false;
31    };
32    struct_tag == AliasOutput::tag(GAS::type_tag())
33        || struct_tag == BasicOutput::tag(GAS::type_tag())
34        || struct_tag == NftOutput::tag(GAS::type_tag())
35        || struct_tag == TimeLock::<Balance>::type_(Balance::type_(GAS::type_tag()).into())
36        || object.is_gas_coin()
37}
38
39/// Return the `Balance` of a gas-coin kind.
40///
41/// Useful to avoid deserialization of the entire object.
42pub fn get_gas_balance_maybe(object: &Object) -> Option<Balance> {
43    if !is_gas_coin_kind(object) {
44        return None;
45    }
46    let inner = object.data.try_as_move()?;
47    bcs::from_bytes(&inner.contents()[ID_END_INDEX..][..size_of::<Balance>()]).ok()
48}
49
50#[cfg(test)]
51mod tests {
52    use iota_protocol_config::ProtocolConfig;
53
54    use crate::{
55        balance::Balance,
56        base_types::{IotaAddress, ObjectID, TxContext},
57        id::UID,
58        object::{Object, Owner},
59        stardust::{
60            coin_kind::{get_gas_balance_maybe, is_gas_coin_kind},
61            coin_type::CoinType,
62            output::{AliasOutput, BasicOutput, NftOutput},
63        },
64        timelock::timelock::{TimeLock, to_genesis_object},
65    };
66
67    fn nft_output(balance: u64, coin_type: CoinType) -> anyhow::Result<Object> {
68        let id = UID::new(ObjectID::random());
69        let balance = Balance::new(balance);
70        let output = NftOutput {
71            id,
72            balance,
73            native_tokens: Default::default(),
74            storage_deposit_return: Default::default(),
75            timelock: Default::default(),
76            expiration: Default::default(),
77        };
78        output.to_genesis_object(
79            IotaAddress::ZERO,
80            &ProtocolConfig::get_for_min_version(),
81            &TxContext::random_for_testing_only(),
82            1.into(),
83            coin_type,
84        )
85    }
86
87    #[test]
88    fn is_coin_kind_nft_output() {
89        let object = nft_output(100, CoinType::Iota).unwrap();
90        assert!(is_gas_coin_kind(&object));
91    }
92
93    #[test]
94    fn get_gas_balance_nft_output() {
95        let value = 100;
96        let object = nft_output(value, CoinType::Iota).unwrap();
97        let gas_coin_balance = get_gas_balance_maybe(&object).unwrap();
98        assert_eq!(gas_coin_balance.value(), value);
99    }
100
101    fn alias_output(balance: u64, coin_type: CoinType) -> anyhow::Result<Object> {
102        let id = UID::new(ObjectID::random());
103        let balance = Balance::new(balance);
104        let output = AliasOutput {
105            id,
106            balance,
107            native_tokens: Default::default(),
108        };
109        output.to_genesis_object(
110            Owner::AddressOwner(IotaAddress::ZERO),
111            &ProtocolConfig::get_for_min_version(),
112            &TxContext::random_for_testing_only(),
113            1.into(),
114            coin_type,
115        )
116    }
117
118    #[test]
119    fn is_coin_kind_alias_output() {
120        let object = alias_output(100, CoinType::Iota).unwrap();
121        assert!(is_gas_coin_kind(&object));
122    }
123
124    #[test]
125    fn get_gas_balance_alias_output() {
126        let value = 100;
127        let object = alias_output(value, CoinType::Iota).unwrap();
128        let gas_coin_balance = get_gas_balance_maybe(&object).unwrap();
129        assert_eq!(gas_coin_balance.value(), value);
130    }
131
132    fn basic_output(balance: u64, coin_type: CoinType) -> anyhow::Result<Object> {
133        let id = UID::new(ObjectID::random());
134        let balance = Balance::new(balance);
135        let output = BasicOutput {
136            id,
137            balance,
138            native_tokens: Default::default(),
139            storage_deposit_return: Default::default(),
140            timelock: Default::default(),
141            expiration: Default::default(),
142            metadata: Default::default(),
143            tag: Default::default(),
144            sender: Default::default(),
145        };
146        output.to_genesis_object(
147            IotaAddress::ZERO,
148            &ProtocolConfig::get_for_min_version(),
149            &TxContext::random_for_testing_only(),
150            1.into(),
151            &coin_type,
152        )
153    }
154
155    #[test]
156    fn is_coin_kind_basic_output() {
157        let object = basic_output(100, CoinType::Iota).unwrap();
158        assert!(is_gas_coin_kind(&object));
159    }
160
161    #[test]
162    fn get_gas_balance_basic_output() {
163        let value = 100;
164        let object = basic_output(value, CoinType::Iota).unwrap();
165        let gas_coin_balance = get_gas_balance_maybe(&object).unwrap();
166        assert_eq!(gas_coin_balance.value(), value);
167    }
168
169    fn timelock(balance: u64) -> anyhow::Result<Object> {
170        let id = UID::new(ObjectID::random());
171        let balance = Balance::new(balance);
172        let expiration_timestamp_ms = 10;
173        let label = None;
174
175        let timelock = TimeLock::new(id, balance, expiration_timestamp_ms, label);
176        Ok(to_genesis_object(
177            timelock,
178            IotaAddress::ZERO,
179            &ProtocolConfig::get_for_min_version(),
180            &TxContext::random_for_testing_only(),
181            1.into(),
182        )?)
183    }
184
185    #[test]
186    fn is_coin_kind_timelock() {
187        let object = timelock(100).unwrap();
188        assert!(is_gas_coin_kind(&object));
189    }
190
191    #[test]
192    fn get_gas_balance_timelock() {
193        let value = 100;
194        let object = timelock(value).unwrap();
195        let gas_coin_balance = get_gas_balance_maybe(&object).unwrap();
196        assert_eq!(gas_coin_balance.value(), value);
197    }
198}