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