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