Skip to main content

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 iota_sdk_types::{Identifier, ObjectData, ObjectId, StructTag, TypeTag};
6use move_core_types::{
7    account_address::AccountAddress,
8    annotated_value::{MoveFieldLayout, MoveStructLayout, MoveTypeLayout},
9    ident_str,
10    identifier::IdentStr,
11};
12use serde::{Deserialize, Serialize};
13
14use crate::{
15    balance::{Balance, Supply},
16    error::{ExecutionError, ExecutionErrorKind, IotaError},
17    id::UID,
18    iota_sdk_types_conversions::struct_tag_sdk_to_core,
19    object::Object,
20};
21
22pub const COIN_JOIN_FUNC_NAME: Identifier = Identifier::from_static("join");
23
24pub const PAY_SPLIT_N_FUNC_NAME: Identifier = Identifier::from_static("divide_and_keep");
25pub const PAY_SPLIT_VEC_FUNC_NAME: Identifier = Identifier::from_static("split_vec");
26
27pub const RESOLVED_COIN_STRUCT: (&AccountAddress, &IdentStr, &IdentStr) = (
28    &crate::IOTA_FRAMEWORK_ADDRESS,
29    ident_str!("coin"),
30    ident_str!("Coin"),
31);
32
33// Rust version of the Move iota::coin::Coin type
34#[derive(Debug, Serialize, Deserialize, Clone, 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    /// Create a coin from BCS bytes
49    pub fn from_bcs_bytes(content: &[u8]) -> Result<Self, bcs::Error> {
50        bcs::from_bytes(content)
51    }
52
53    /// If the given object is a Coin, deserialize its contents and extract the
54    /// balance Ok(Some(u64)). If it's not a Coin, return Ok(None).
55    /// The cost is 2 comparisons if not a coin, and deserialization if its a
56    /// Coin.
57    pub fn extract_balance_if_coin(object: &Object) -> Result<Option<u64>, bcs::Error> {
58        let ObjectData::Struct(obj) = &object.data else {
59            return Ok(None);
60        };
61        let Some(_) = obj.struct_tag().opt_coin_type() else {
62            return Ok(None);
63        };
64
65        let coin = Self::from_bcs_bytes(obj.contents())?;
66        Ok(Some(coin.value()))
67    }
68
69    pub fn id(&self) -> &ObjectId {
70        self.id.object_id()
71    }
72
73    pub fn value(&self) -> u64 {
74        self.balance.value()
75    }
76
77    pub fn to_bcs_bytes(&self) -> Vec<u8> {
78        bcs::to_bytes(&self).unwrap()
79    }
80
81    pub fn layout(type_param: TypeTag) -> MoveStructLayout {
82        MoveStructLayout {
83            type_: struct_tag_sdk_to_core(&StructTag::new_coin(type_param.clone())),
84            fields: vec![
85                MoveFieldLayout::new(
86                    ident_str!("id").to_owned(),
87                    MoveTypeLayout::Struct(Box::new(UID::layout())),
88                ),
89                MoveFieldLayout::new(
90                    ident_str!("balance").to_owned(),
91                    MoveTypeLayout::Struct(Box::new(Balance::layout(type_param))),
92                ),
93            ],
94        }
95    }
96
97    /// Add balance to this coin, erroring if the new total balance exceeds the
98    /// maximum
99    pub fn add(&mut self, balance: Balance) -> Result<(), ExecutionError> {
100        let Some(new_value) = self.value().checked_add(balance.value()) else {
101            return Err(ExecutionError::from_kind(
102                ExecutionErrorKind::CoinBalanceOverflow,
103            ));
104        };
105        self.balance = Balance::new(new_value);
106        Ok(())
107    }
108
109    // Split amount out of this coin to a new coin.
110    // Related coin objects need to be updated in temporary_store to persist the
111    // changes, including creating the coin object related to the newly created
112    // coin.
113    pub fn split(&mut self, amount: u64, new_coin_id: ObjectId) -> Result<Coin, ExecutionError> {
114        self.balance.withdraw(amount)?;
115        Ok(Coin::new(new_coin_id, amount))
116    }
117}
118
119// Rust version of the Move iota::coin::TreasuryCap type
120#[derive(Debug, Serialize, Deserialize, Clone, Eq, PartialEq)]
121pub struct TreasuryCap {
122    pub id: UID,
123    pub total_supply: Supply,
124}
125
126impl TreasuryCap {
127    /// Create a TreasuryCap from BCS bytes
128    pub fn from_bcs_bytes(content: &[u8]) -> Result<Self, IotaError> {
129        bcs::from_bytes(content).map_err(|err| IotaError::ObjectDeserialization {
130            error: format!("Unable to deserialize TreasuryCap object: {err}"),
131        })
132    }
133
134    /// Checks if the provided type is `TreasuryCap<T>`, returning the type T if
135    /// so.
136    pub fn is_treasury_with_coin_type(other: &StructTag) -> Option<&StructTag> {
137        if other.is_treasury_cap() {
138            match other.type_params().first() {
139                Some(TypeTag::Struct(coin_type)) => Some(coin_type),
140                _ => None,
141            }
142        } else {
143            None
144        }
145    }
146}
147
148impl TryFrom<Object> for TreasuryCap {
149    type Error = IotaError;
150    fn try_from(object: Object) -> Result<Self, Self::Error> {
151        match &object.data {
152            ObjectData::Struct(o) => {
153                if o.struct_tag().is_treasury_cap() {
154                    return TreasuryCap::from_bcs_bytes(o.contents());
155                }
156            }
157            ObjectData::Package(_) => {}
158        }
159
160        Err(IotaError::Type {
161            error: format!("Object type is not a TreasuryCap: {object:?}"),
162        })
163    }
164}
165
166// Rust version of the Move iota::coin::CoinMetadata type
167#[derive(Debug, Serialize, Deserialize, Clone, Eq, PartialEq)]
168pub struct CoinMetadata {
169    pub id: UID,
170    /// Number of decimal places the coin uses.
171    pub decimals: u8,
172    /// Name for the token
173    pub name: String,
174    /// Symbol for the token
175    pub symbol: String,
176    /// Description of the token
177    pub description: String,
178    /// URL for the token logo
179    pub icon_url: Option<String>,
180}
181
182impl CoinMetadata {
183    /// Create a coin from BCS bytes
184    pub fn from_bcs_bytes(content: &[u8]) -> Result<Self, IotaError> {
185        bcs::from_bytes(content).map_err(|err| IotaError::ObjectDeserialization {
186            error: format!("Unable to deserialize CoinMetadata object: {err}"),
187        })
188    }
189
190    /// Checks if the provided type is `CoinMetadata<T>`, returning the type T
191    /// if so.
192    pub fn is_coin_metadata_with_coin_type(other: &StructTag) -> Option<&StructTag> {
193        if other.is_coin_metadata() {
194            match other.type_params().first() {
195                Some(TypeTag::Struct(coin_type)) => Some(coin_type),
196                _ => None,
197            }
198        } else {
199            None
200        }
201    }
202}
203
204impl TryFrom<Object> for CoinMetadata {
205    type Error = IotaError;
206    fn try_from(object: Object) -> Result<Self, Self::Error> {
207        TryFrom::try_from(&object)
208    }
209}
210
211impl TryFrom<&Object> for CoinMetadata {
212    type Error = IotaError;
213    fn try_from(object: &Object) -> Result<Self, Self::Error> {
214        match &object.data {
215            ObjectData::Struct(o) => {
216                if o.struct_tag().is_coin_metadata() {
217                    return CoinMetadata::from_bcs_bytes(o.contents());
218                }
219            }
220            ObjectData::Package(_) => {}
221        }
222
223        Err(IotaError::Type {
224            error: format!("Object type is not a CoinMetadata: {object:?}"),
225        })
226    }
227}