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