sui_types/
coin.rs

1// Copyright (c) Mysten Labs, Inc.
2// SPDX-License-Identifier: Apache-2.0
3
4use crate::error::ExecutionErrorKind;
5use crate::error::{SuiError, SuiErrorKind};
6use crate::{
7    SUI_FRAMEWORK_ADDRESS,
8    base_types::ObjectID,
9    id::{ID, UID},
10};
11use crate::{
12    balance::{Balance, Supply},
13    error::ExecutionError,
14    object::{Data, Object},
15};
16use move_core_types::{
17    account_address::AccountAddress,
18    annotated_value::{MoveFieldLayout, MoveStructLayout, MoveTypeLayout},
19    ident_str,
20    identifier::IdentStr,
21    language_storage::{StructTag, TypeTag},
22};
23use schemars::JsonSchema;
24use serde::{Deserialize, Serialize};
25
26pub const COIN_MODULE_NAME: &IdentStr = ident_str!("coin");
27pub const COIN_STRUCT_NAME: &IdentStr = ident_str!("Coin");
28pub const RESOLVED_COIN_STRUCT: (&AccountAddress, &IdentStr, &IdentStr) =
29    (&SUI_FRAMEWORK_ADDRESS, COIN_MODULE_NAME, COIN_STRUCT_NAME);
30pub const COIN_METADATA_STRUCT_NAME: &IdentStr = ident_str!("CoinMetadata");
31pub const COIN_TREASURE_CAP_NAME: &IdentStr = ident_str!("TreasuryCap");
32pub const REGULATED_COIN_METADATA_STRUCT_NAME: &IdentStr = ident_str!("RegulatedCoinMetadata");
33
34pub const PAY_MODULE_NAME: &IdentStr = ident_str!("pay");
35pub const PAY_JOIN_FUNC_NAME: &IdentStr = ident_str!("join");
36pub const PAY_SPLIT_N_FUNC_NAME: &IdentStr = ident_str!("divide_and_keep");
37pub const PAY_SPLIT_VEC_FUNC_NAME: &IdentStr = ident_str!("split_vec");
38
39// Rust version of the Move sui::coin::Coin type
40#[derive(Debug, Serialize, Deserialize, Clone, JsonSchema, Eq, PartialEq)]
41pub struct Coin {
42    pub id: UID,
43    pub balance: Balance,
44}
45
46impl Coin {
47    pub fn new(id: ObjectID, value: u64) -> Self {
48        Self {
49            id: UID::new(id),
50            balance: Balance::new(value),
51        }
52    }
53
54    pub fn type_(type_param: TypeTag) -> StructTag {
55        StructTag {
56            address: SUI_FRAMEWORK_ADDRESS,
57            name: COIN_STRUCT_NAME.to_owned(),
58            module: COIN_MODULE_NAME.to_owned(),
59            type_params: vec![type_param],
60        }
61    }
62
63    /// Is this other StructTag representing a Coin?
64    pub fn is_coin(other: &StructTag) -> bool {
65        other.address == SUI_FRAMEWORK_ADDRESS
66            && other.module.as_ident_str() == COIN_MODULE_NAME
67            && other.name.as_ident_str() == COIN_STRUCT_NAME
68    }
69
70    /// Checks if the provided type is `Coin<T>`, returning the type T if so.
71    pub fn is_coin_with_coin_type(other: &StructTag) -> Option<&StructTag> {
72        if Self::is_coin(other) && other.type_params.len() == 1 {
73            match other.type_params.first() {
74                Some(TypeTag::Struct(coin_type)) => Some(coin_type),
75                _ => None,
76            }
77        } else {
78            None
79        }
80    }
81
82    /// Create a coin from BCS bytes
83    pub fn from_bcs_bytes(content: &[u8]) -> Result<Self, bcs::Error> {
84        bcs::from_bytes(content)
85    }
86
87    /// If the given object is a Coin, deserialize its contents and extract the balance Ok(Some(u64)).
88    /// If it's not a Coin, return Ok(None).
89    /// The cost is 2 comparisons if not a coin, and deserialization if its a Coin.
90    pub fn extract_balance_if_coin(object: &Object) -> Result<Option<(TypeTag, u64)>, bcs::Error> {
91        let Data::Move(obj) = &object.data else {
92            return Ok(None);
93        };
94
95        let Some(type_) = obj.type_().coin_type_maybe() else {
96            return Ok(None);
97        };
98
99        let coin = Self::from_bcs_bytes(obj.contents())?;
100        Ok(Some((type_, coin.value())))
101    }
102
103    pub fn id(&self) -> &ObjectID {
104        self.id.object_id()
105    }
106
107    pub fn value(&self) -> u64 {
108        self.balance.value()
109    }
110
111    pub fn to_bcs_bytes(&self) -> Vec<u8> {
112        bcs::to_bytes(&self).unwrap()
113    }
114
115    pub fn layout(type_param: TypeTag) -> MoveStructLayout {
116        MoveStructLayout {
117            type_: Self::type_(type_param.clone()),
118            fields: vec![
119                MoveFieldLayout::new(
120                    ident_str!("id").to_owned(),
121                    MoveTypeLayout::Struct(Box::new(UID::layout())),
122                ),
123                MoveFieldLayout::new(
124                    ident_str!("balance").to_owned(),
125                    MoveTypeLayout::Struct(Box::new(Balance::layout(type_param))),
126                ),
127            ],
128        }
129    }
130
131    /// Add balance to this coin, erroring if the new total balance exceeds the maximum
132    pub fn add(&mut self, balance: Balance) -> Result<(), ExecutionError> {
133        let Some(new_value) = self.value().checked_add(balance.value()) else {
134            return Err(ExecutionError::from_kind(
135                ExecutionErrorKind::CoinBalanceOverflow,
136            ));
137        };
138        self.balance = Balance::new(new_value);
139        Ok(())
140    }
141
142    // Split amount out of this coin to a new coin.
143    // Related coin objects need to be updated in temporary_store to persist the changes,
144    // including creating the coin object related to the newly created coin.
145    pub fn split(&mut self, amount: u64, new_coin_id: ObjectID) -> Result<Coin, ExecutionError> {
146        self.balance.withdraw(amount)?;
147        Ok(Coin::new(new_coin_id, amount))
148    }
149}
150
151// Rust version of the Move sui::coin::TreasuryCap type
152#[derive(Debug, Serialize, Deserialize, Clone, Eq, PartialEq, JsonSchema)]
153pub struct TreasuryCap {
154    pub id: UID,
155    pub total_supply: Supply,
156}
157
158impl TreasuryCap {
159    pub fn is_treasury_type(other: &StructTag) -> bool {
160        other.address == SUI_FRAMEWORK_ADDRESS
161            && other.module.as_ident_str() == COIN_MODULE_NAME
162            && other.name.as_ident_str() == COIN_TREASURE_CAP_NAME
163    }
164
165    /// Create a TreasuryCap from BCS bytes
166    pub fn from_bcs_bytes(content: &[u8]) -> Result<Self, SuiError> {
167        bcs::from_bytes(content).map_err(|err| {
168            SuiErrorKind::ObjectDeserializationError {
169                error: format!("Unable to deserialize TreasuryCap object: {}", err),
170            }
171            .into()
172        })
173    }
174
175    pub fn type_(type_param: StructTag) -> StructTag {
176        StructTag {
177            address: SUI_FRAMEWORK_ADDRESS,
178            name: COIN_TREASURE_CAP_NAME.to_owned(),
179            module: COIN_MODULE_NAME.to_owned(),
180            type_params: vec![TypeTag::Struct(Box::new(type_param))],
181        }
182    }
183
184    /// Checks if the provided type is `TreasuryCap<T>`, returning the type T if so.
185    pub fn is_treasury_with_coin_type(other: &StructTag) -> Option<&StructTag> {
186        if Self::is_treasury_type(other) && other.type_params.len() == 1 {
187            match other.type_params.first() {
188                Some(TypeTag::Struct(coin_type)) => Some(coin_type),
189                _ => None,
190            }
191        } else {
192            None
193        }
194    }
195}
196
197impl TryFrom<Object> for TreasuryCap {
198    type Error = SuiError;
199    fn try_from(object: Object) -> Result<Self, Self::Error> {
200        match &object.data {
201            Data::Move(o) => {
202                if o.type_().is_treasury_cap() {
203                    return TreasuryCap::from_bcs_bytes(o.contents());
204                }
205            }
206            Data::Package(_) => {}
207        }
208
209        Err(SuiErrorKind::TypeError {
210            error: format!("Object type is not a TreasuryCap: {:?}", object),
211        }
212        .into())
213    }
214}
215
216// Rust version of the Move sui::coin::CoinMetadata type
217#[derive(Debug, Serialize, Deserialize, Clone, JsonSchema, Eq, PartialEq)]
218pub struct CoinMetadata {
219    pub id: UID,
220    /// Number of decimal places the coin uses.
221    pub decimals: u8,
222    /// Name for the token
223    pub name: String,
224    /// Symbol for the token
225    pub symbol: String,
226    /// Description of the token
227    pub description: String,
228    /// URL for the token logo
229    pub icon_url: Option<String>,
230}
231
232impl CoinMetadata {
233    /// Is this other StructTag representing a CoinMetadata?
234    pub fn is_coin_metadata(other: &StructTag) -> bool {
235        other.address == SUI_FRAMEWORK_ADDRESS
236            && other.module.as_ident_str() == COIN_MODULE_NAME
237            && other.name.as_ident_str() == COIN_METADATA_STRUCT_NAME
238    }
239
240    /// Create a coin from BCS bytes
241    pub fn from_bcs_bytes(content: &[u8]) -> Result<Self, SuiError> {
242        bcs::from_bytes(content).map_err(|err| {
243            SuiErrorKind::ObjectDeserializationError {
244                error: format!("Unable to deserialize CoinMetadata object: {}", err),
245            }
246            .into()
247        })
248    }
249
250    pub fn type_(type_param: StructTag) -> StructTag {
251        StructTag {
252            address: SUI_FRAMEWORK_ADDRESS,
253            name: COIN_METADATA_STRUCT_NAME.to_owned(),
254            module: COIN_MODULE_NAME.to_owned(),
255            type_params: vec![TypeTag::Struct(Box::new(type_param))],
256        }
257    }
258
259    /// Checks if the provided type is `CoinMetadata<T>`, returning the type T if so.
260    pub fn is_coin_metadata_with_coin_type(other: &StructTag) -> Option<&StructTag> {
261        if Self::is_coin_metadata(other) && other.type_params.len() == 1 {
262            match other.type_params.first() {
263                Some(TypeTag::Struct(coin_type)) => Some(coin_type),
264                _ => None,
265            }
266        } else {
267            None
268        }
269    }
270}
271
272impl TryFrom<Object> for CoinMetadata {
273    type Error = SuiError;
274    fn try_from(object: Object) -> Result<Self, Self::Error> {
275        TryFrom::try_from(&object)
276    }
277}
278
279impl TryFrom<&Object> for CoinMetadata {
280    type Error = SuiError;
281    fn try_from(object: &Object) -> Result<Self, Self::Error> {
282        match &object.data {
283            Data::Move(o) => {
284                if o.type_().is_coin_metadata() {
285                    return CoinMetadata::from_bcs_bytes(o.contents());
286                }
287            }
288            Data::Package(_) => {}
289        }
290
291        Err(SuiErrorKind::TypeError {
292            error: format!("Object type is not a CoinMetadata: {:?}", object),
293        }
294        .into())
295    }
296}
297
298// Rust version of the Move sui::coin::RegulatedCoinMetadata type
299#[derive(Debug, Serialize, Deserialize, Clone, Eq, PartialEq)]
300pub struct RegulatedCoinMetadata {
301    pub id: UID,
302    /// The ID of the coin's CoinMetadata object.
303    pub coin_metadata_object: ID,
304    /// The ID of the coin's DenyCap object.
305    pub deny_cap_object: ID,
306}
307
308impl RegulatedCoinMetadata {
309    /// Is this other StructTag representing a CoinMetadata?
310    pub fn is_regulated_coin_metadata(other: &StructTag) -> bool {
311        other.address == SUI_FRAMEWORK_ADDRESS
312            && other.module.as_ident_str() == COIN_MODULE_NAME
313            && other.name.as_ident_str() == REGULATED_COIN_METADATA_STRUCT_NAME
314    }
315
316    /// Create a coin from BCS bytes
317    pub fn from_bcs_bytes(content: &[u8]) -> Result<Self, SuiError> {
318        bcs::from_bytes(content).map_err(|err| {
319            SuiErrorKind::ObjectDeserializationError {
320                error: format!(
321                    "Unable to deserialize RegulatedCoinMetadata object: {}",
322                    err
323                ),
324            }
325            .into()
326        })
327    }
328
329    pub fn type_(type_param: StructTag) -> StructTag {
330        StructTag {
331            address: SUI_FRAMEWORK_ADDRESS,
332            module: COIN_MODULE_NAME.to_owned(),
333            name: REGULATED_COIN_METADATA_STRUCT_NAME.to_owned(),
334            type_params: vec![TypeTag::Struct(Box::new(type_param))],
335        }
336    }
337
338    /// Checks if the provided type is `CoinMetadata<T>`, returning the type T if so.
339    pub fn is_regulated_coin_metadata_with_coin_type(other: &StructTag) -> Option<&StructTag> {
340        if Self::is_regulated_coin_metadata(other) && other.type_params.len() == 1 {
341            match other.type_params.first() {
342                Some(TypeTag::Struct(coin_type)) => Some(coin_type),
343                _ => None,
344            }
345        } else {
346            None
347        }
348    }
349}
350
351impl TryFrom<Object> for RegulatedCoinMetadata {
352    type Error = SuiError;
353    fn try_from(object: Object) -> Result<Self, Self::Error> {
354        TryFrom::try_from(&object)
355    }
356}
357
358impl TryFrom<&Object> for RegulatedCoinMetadata {
359    type Error = SuiError;
360    fn try_from(object: &Object) -> Result<Self, Self::Error> {
361        match &object.data {
362            Data::Move(o) => {
363                if o.type_().is_regulated_coin_metadata() {
364                    return Self::from_bcs_bytes(o.contents());
365                }
366            }
367            Data::Package(_) => {}
368        }
369
370        Err(SuiErrorKind::TypeError {
371            error: format!("Object type is not a RegulatedCoinMetadata: {:?}", object),
372        }
373        .into())
374    }
375}