iota_types/
coin.rs

1// Copyright (c) Mysten Labs, Inc.
2// Modifications Copyright (c) 2024 IOTA Stiftung
3// SPDX-License-Identifier: Apache-2.0
4
5use move_core_types::{
6    annotated_value::{MoveFieldLayout, MoveStructLayout, MoveTypeLayout},
7    ident_str,
8    identifier::IdentStr,
9    language_storage::{StructTag, TypeTag},
10};
11use schemars::JsonSchema;
12use serde::{Deserialize, Serialize};
13
14use crate::{
15    IOTA_FRAMEWORK_ADDRESS,
16    balance::{Balance, Supply},
17    base_types::ObjectID,
18    error::{ExecutionError, ExecutionErrorKind, IotaError},
19    id::UID,
20    object::{Data, Object},
21};
22
23pub const COIN_MODULE_NAME: &IdentStr = ident_str!("coin");
24pub const COIN_STRUCT_NAME: &IdentStr = ident_str!("Coin");
25pub const COIN_METADATA_STRUCT_NAME: &IdentStr = ident_str!("CoinMetadata");
26pub const COIN_TREASURE_CAP_NAME: &IdentStr = ident_str!("TreasuryCap");
27pub const COIN_JOIN_FUNC_NAME: &IdentStr = ident_str!("join");
28
29pub const PAY_MODULE_NAME: &IdentStr = ident_str!("pay");
30pub const PAY_SPLIT_N_FUNC_NAME: &IdentStr = ident_str!("divide_and_keep");
31pub const PAY_SPLIT_VEC_FUNC_NAME: &IdentStr = ident_str!("split_vec");
32
33// Rust version of the Move iota::coin::Coin type
34#[derive(Debug, Serialize, Deserialize, Clone, JsonSchema, Eq, PartialEq)]
35pub struct Coin {
36    pub id: UID,
37    pub balance: Balance,
38}
39
40impl Coin {
41    pub fn new(id: UID, value: u64) -> Self {
42        Self {
43            id,
44            balance: Balance::new(value),
45        }
46    }
47
48    pub fn type_(type_param: TypeTag) -> StructTag {
49        StructTag {
50            address: IOTA_FRAMEWORK_ADDRESS,
51            name: COIN_STRUCT_NAME.to_owned(),
52            module: COIN_MODULE_NAME.to_owned(),
53            type_params: vec![type_param],
54        }
55    }
56
57    /// Is this other StructTag representing a Coin?
58    pub fn is_coin(other: &StructTag) -> bool {
59        other.address == IOTA_FRAMEWORK_ADDRESS
60            && other.module.as_ident_str() == COIN_MODULE_NAME
61            && other.name.as_ident_str() == COIN_STRUCT_NAME
62    }
63
64    /// Create a coin from BCS bytes
65    pub fn from_bcs_bytes(content: &[u8]) -> Result<Self, bcs::Error> {
66        bcs::from_bytes(content)
67    }
68
69    /// If the given object is a Coin, deserialize its contents and extract the
70    /// balance Ok(Some(u64)). If it's not a Coin, return Ok(None).
71    /// The cost is 2 comparisons if not a coin, and deserialization if its a
72    /// Coin.
73    pub fn extract_balance_if_coin(object: &Object) -> Result<Option<u64>, bcs::Error> {
74        match &object.data {
75            Data::Move(move_obj) => {
76                if !move_obj.is_coin() {
77                    return Ok(None);
78                }
79
80                let coin = Self::from_bcs_bytes(move_obj.contents())?;
81                Ok(Some(coin.value()))
82            }
83            _ => Ok(None), // package
84        }
85    }
86
87    pub fn id(&self) -> &ObjectID {
88        self.id.object_id()
89    }
90
91    pub fn value(&self) -> u64 {
92        self.balance.value()
93    }
94
95    pub fn to_bcs_bytes(&self) -> Vec<u8> {
96        bcs::to_bytes(&self).unwrap()
97    }
98
99    pub fn layout(type_param: TypeTag) -> MoveStructLayout {
100        MoveStructLayout {
101            type_: Self::type_(type_param.clone()),
102            fields: vec![
103                MoveFieldLayout::new(
104                    ident_str!("id").to_owned(),
105                    MoveTypeLayout::Struct(Box::new(UID::layout())),
106                ),
107                MoveFieldLayout::new(
108                    ident_str!("balance").to_owned(),
109                    MoveTypeLayout::Struct(Box::new(Balance::layout(type_param))),
110                ),
111            ],
112        }
113    }
114
115    /// Add balance to this coin, erroring if the new total balance exceeds the
116    /// maximum
117    pub fn add(&mut self, balance: Balance) -> Result<(), ExecutionError> {
118        let Some(new_value) = self.value().checked_add(balance.value()) else {
119            return Err(ExecutionError::from_kind(
120                ExecutionErrorKind::CoinBalanceOverflow,
121            ));
122        };
123        self.balance = Balance::new(new_value);
124        Ok(())
125    }
126
127    // Split amount out of this coin to a new coin.
128    // Related coin objects need to be updated in temporary_store to persist the
129    // changes, including creating the coin object related to the newly created
130    // coin.
131    pub fn split(&mut self, amount: u64, new_coin_id: UID) -> Result<Coin, ExecutionError> {
132        self.balance.withdraw(amount)?;
133        Ok(Coin::new(new_coin_id, amount))
134    }
135}
136
137// Rust version of the Move iota::coin::TreasuryCap type
138#[derive(Debug, Serialize, Deserialize, Clone, Eq, PartialEq, JsonSchema)]
139pub struct TreasuryCap {
140    pub id: UID,
141    pub total_supply: Supply,
142}
143
144impl TreasuryCap {
145    pub fn is_treasury_type(other: &StructTag) -> bool {
146        other.address == IOTA_FRAMEWORK_ADDRESS
147            && other.module.as_ident_str() == COIN_MODULE_NAME
148            && other.name.as_ident_str() == COIN_TREASURE_CAP_NAME
149    }
150
151    /// Create a TreasuryCap from BCS bytes
152    pub fn from_bcs_bytes(content: &[u8]) -> Result<Self, IotaError> {
153        bcs::from_bytes(content).map_err(|err| IotaError::ObjectDeserialization {
154            error: format!("Unable to deserialize TreasuryCap object: {}", err),
155        })
156    }
157
158    pub fn type_(type_param: StructTag) -> StructTag {
159        StructTag {
160            address: IOTA_FRAMEWORK_ADDRESS,
161            name: COIN_TREASURE_CAP_NAME.to_owned(),
162            module: COIN_MODULE_NAME.to_owned(),
163            type_params: vec![TypeTag::Struct(Box::new(type_param))],
164        }
165    }
166
167    /// Checks if the provided type is `TreasuryCap<T>`, returning the type T if
168    /// so.
169    pub fn is_treasury_with_coin_type(other: &StructTag) -> Option<&StructTag> {
170        if Self::is_treasury_type(other) && other.type_params.len() == 1 {
171            match other.type_params.first() {
172                Some(TypeTag::Struct(coin_type)) => Some(coin_type),
173                _ => None,
174            }
175        } else {
176            None
177        }
178    }
179}
180
181impl TryFrom<Object> for TreasuryCap {
182    type Error = IotaError;
183    fn try_from(object: Object) -> Result<Self, Self::Error> {
184        match &object.data {
185            Data::Move(o) => {
186                if o.type_().is_treasury_cap() {
187                    return TreasuryCap::from_bcs_bytes(o.contents());
188                }
189            }
190            Data::Package(_) => {}
191        }
192
193        Err(IotaError::Type {
194            error: format!("Object type is not a TreasuryCap: {:?}", object),
195        })
196    }
197}
198
199// Rust version of the Move iota::coin::CoinMetadata type
200#[derive(Debug, Serialize, Deserialize, Clone, JsonSchema, Eq, PartialEq)]
201pub struct CoinMetadata {
202    pub id: UID,
203    /// Number of decimal places the coin uses.
204    pub decimals: u8,
205    /// Name for the token
206    pub name: String,
207    /// Symbol for the token
208    pub symbol: String,
209    /// Description of the token
210    pub description: String,
211    /// URL for the token logo
212    pub icon_url: Option<String>,
213}
214
215impl CoinMetadata {
216    /// Is this other StructTag representing a CoinMetadata?
217    pub fn is_coin_metadata(other: &StructTag) -> bool {
218        other.address == IOTA_FRAMEWORK_ADDRESS
219            && other.module.as_ident_str() == COIN_MODULE_NAME
220            && other.name.as_ident_str() == COIN_METADATA_STRUCT_NAME
221    }
222
223    /// Create a coin from BCS bytes
224    pub fn from_bcs_bytes(content: &[u8]) -> Result<Self, IotaError> {
225        bcs::from_bytes(content).map_err(|err| IotaError::ObjectDeserialization {
226            error: format!("Unable to deserialize CoinMetadata object: {}", err),
227        })
228    }
229
230    pub fn type_(type_param: StructTag) -> StructTag {
231        StructTag {
232            address: IOTA_FRAMEWORK_ADDRESS,
233            name: COIN_METADATA_STRUCT_NAME.to_owned(),
234            module: COIN_MODULE_NAME.to_owned(),
235            type_params: vec![TypeTag::Struct(Box::new(type_param))],
236        }
237    }
238
239    /// Checks if the provided type is `CoinMetadata<T>`, returning the type T
240    /// if so.
241    pub fn is_coin_metadata_with_coin_type(other: &StructTag) -> Option<&StructTag> {
242        if Self::is_coin_metadata(other) && other.type_params.len() == 1 {
243            match other.type_params.first() {
244                Some(TypeTag::Struct(coin_type)) => Some(coin_type),
245                _ => None,
246            }
247        } else {
248            None
249        }
250    }
251}
252
253impl TryFrom<Object> for CoinMetadata {
254    type Error = IotaError;
255    fn try_from(object: Object) -> Result<Self, Self::Error> {
256        TryFrom::try_from(&object)
257    }
258}
259
260impl TryFrom<&Object> for CoinMetadata {
261    type Error = IotaError;
262    fn try_from(object: &Object) -> Result<Self, Self::Error> {
263        match &object.data {
264            Data::Move(o) => {
265                if o.type_().is_coin_metadata() {
266                    return CoinMetadata::from_bcs_bytes(o.contents());
267                }
268            }
269            Data::Package(_) => {}
270        }
271
272        Err(IotaError::Type {
273            error: format!("Object type is not a CoinMetadata: {:?}", object),
274        })
275    }
276}