sui_types/
coin.rs

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