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: ObjectID, value: u64) -> Self {
42        Self {
43            id: UID::new(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        let Data::Move(obj) = &object.data else {
75            return Ok(None);
76        };
77        let Some(_) = obj.type_().coin_type_maybe() else {
78            return Ok(None);
79        };
80
81        let coin = Self::from_bcs_bytes(obj.contents())?;
82        Ok(Some(coin.value()))
83    }
84
85    pub fn id(&self) -> &ObjectID {
86        self.id.object_id()
87    }
88
89    pub fn value(&self) -> u64 {
90        self.balance.value()
91    }
92
93    pub fn to_bcs_bytes(&self) -> Vec<u8> {
94        bcs::to_bytes(&self).unwrap()
95    }
96
97    pub fn layout(type_param: TypeTag) -> MoveStructLayout {
98        MoveStructLayout {
99            type_: Self::type_(type_param.clone()),
100            fields: vec![
101                MoveFieldLayout::new(
102                    ident_str!("id").to_owned(),
103                    MoveTypeLayout::Struct(Box::new(UID::layout())),
104                ),
105                MoveFieldLayout::new(
106                    ident_str!("balance").to_owned(),
107                    MoveTypeLayout::Struct(Box::new(Balance::layout(type_param))),
108                ),
109            ],
110        }
111    }
112
113    /// Add balance to this coin, erroring if the new total balance exceeds the
114    /// maximum
115    pub fn add(&mut self, balance: Balance) -> Result<(), ExecutionError> {
116        let Some(new_value) = self.value().checked_add(balance.value()) else {
117            return Err(ExecutionError::from_kind(
118                ExecutionErrorKind::CoinBalanceOverflow,
119            ));
120        };
121        self.balance = Balance::new(new_value);
122        Ok(())
123    }
124
125    // Split amount out of this coin to a new coin.
126    // Related coin objects need to be updated in temporary_store to persist the
127    // changes, including creating the coin object related to the newly created
128    // coin.
129    pub fn split(&mut self, amount: u64, new_coin_id: ObjectID) -> Result<Coin, ExecutionError> {
130        self.balance.withdraw(amount)?;
131        Ok(Coin::new(new_coin_id, amount))
132    }
133}
134
135// Rust version of the Move iota::coin::TreasuryCap type
136#[derive(Debug, Serialize, Deserialize, Clone, Eq, PartialEq, JsonSchema)]
137pub struct TreasuryCap {
138    pub id: UID,
139    pub total_supply: Supply,
140}
141
142impl TreasuryCap {
143    pub fn is_treasury_type(other: &StructTag) -> bool {
144        other.address == IOTA_FRAMEWORK_ADDRESS
145            && other.module.as_ident_str() == COIN_MODULE_NAME
146            && other.name.as_ident_str() == COIN_TREASURE_CAP_NAME
147    }
148
149    /// Create a TreasuryCap from BCS bytes
150    pub fn from_bcs_bytes(content: &[u8]) -> Result<Self, IotaError> {
151        bcs::from_bytes(content).map_err(|err| IotaError::ObjectDeserialization {
152            error: format!("Unable to deserialize TreasuryCap object: {err}"),
153        })
154    }
155
156    pub fn type_(type_param: StructTag) -> StructTag {
157        StructTag {
158            address: IOTA_FRAMEWORK_ADDRESS,
159            name: COIN_TREASURE_CAP_NAME.to_owned(),
160            module: COIN_MODULE_NAME.to_owned(),
161            type_params: vec![TypeTag::Struct(Box::new(type_param))],
162        }
163    }
164
165    /// Checks if the provided type is `TreasuryCap<T>`, returning the type T if
166    /// so.
167    pub fn is_treasury_with_coin_type(other: &StructTag) -> Option<&StructTag> {
168        if Self::is_treasury_type(other) && other.type_params.len() == 1 {
169            match other.type_params.first() {
170                Some(TypeTag::Struct(coin_type)) => Some(coin_type),
171                _ => None,
172            }
173        } else {
174            None
175        }
176    }
177}
178
179impl TryFrom<Object> for TreasuryCap {
180    type Error = IotaError;
181    fn try_from(object: Object) -> Result<Self, Self::Error> {
182        match &object.data {
183            Data::Move(o) => {
184                if o.type_().is_treasury_cap() {
185                    return TreasuryCap::from_bcs_bytes(o.contents());
186                }
187            }
188            Data::Package(_) => {}
189        }
190
191        Err(IotaError::Type {
192            error: format!("Object type is not a TreasuryCap: {object:?}"),
193        })
194    }
195}
196
197// Rust version of the Move iota::coin::CoinMetadata type
198#[derive(Debug, Serialize, Deserialize, Clone, JsonSchema, Eq, PartialEq)]
199pub struct CoinMetadata {
200    pub id: UID,
201    /// Number of decimal places the coin uses.
202    pub decimals: u8,
203    /// Name for the token
204    pub name: String,
205    /// Symbol for the token
206    pub symbol: String,
207    /// Description of the token
208    pub description: String,
209    /// URL for the token logo
210    pub icon_url: Option<String>,
211}
212
213impl CoinMetadata {
214    /// Is this other StructTag representing a CoinMetadata?
215    pub fn is_coin_metadata(other: &StructTag) -> bool {
216        other.address == IOTA_FRAMEWORK_ADDRESS
217            && other.module.as_ident_str() == COIN_MODULE_NAME
218            && other.name.as_ident_str() == COIN_METADATA_STRUCT_NAME
219    }
220
221    /// Create a coin from BCS bytes
222    pub fn from_bcs_bytes(content: &[u8]) -> Result<Self, IotaError> {
223        bcs::from_bytes(content).map_err(|err| IotaError::ObjectDeserialization {
224            error: format!("Unable to deserialize CoinMetadata object: {err}"),
225        })
226    }
227
228    pub fn type_(type_param: StructTag) -> StructTag {
229        StructTag {
230            address: IOTA_FRAMEWORK_ADDRESS,
231            name: COIN_METADATA_STRUCT_NAME.to_owned(),
232            module: COIN_MODULE_NAME.to_owned(),
233            type_params: vec![TypeTag::Struct(Box::new(type_param))],
234        }
235    }
236
237    /// Checks if the provided type is `CoinMetadata<T>`, returning the type T
238    /// if so.
239    pub fn is_coin_metadata_with_coin_type(other: &StructTag) -> Option<&StructTag> {
240        if Self::is_coin_metadata(other) && other.type_params.len() == 1 {
241            match other.type_params.first() {
242                Some(TypeTag::Struct(coin_type)) => Some(coin_type),
243                _ => None,
244            }
245        } else {
246            None
247        }
248    }
249}
250
251impl TryFrom<Object> for CoinMetadata {
252    type Error = IotaError;
253    fn try_from(object: Object) -> Result<Self, Self::Error> {
254        TryFrom::try_from(&object)
255    }
256}
257
258impl TryFrom<&Object> for CoinMetadata {
259    type Error = IotaError;
260    fn try_from(object: &Object) -> Result<Self, Self::Error> {
261        match &object.data {
262            Data::Move(o) => {
263                if o.type_().is_coin_metadata() {
264                    return CoinMetadata::from_bcs_bytes(o.contents());
265                }
266            }
267            Data::Package(_) => {}
268        }
269
270        Err(IotaError::Type {
271            error: format!("Object type is not a CoinMetadata: {object:?}"),
272        })
273    }
274}