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