1use 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#[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 pub fn from_bcs_bytes(content: &[u8]) -> Result<Self, bcs::Error> {
51 bcs::from_bytes(content)
52 }
53
54 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 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 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#[derive(Debug, Serialize, Deserialize, Clone, Eq, PartialEq)]
122pub struct TreasuryCap {
123 pub id: UID,
124 pub total_supply: Supply,
125}
126
127impl TreasuryCap {
128 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 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#[derive(Debug, Serialize, Deserialize, Clone, Eq, PartialEq)]
166pub struct CoinMetadata {
167 pub id: UID,
168 pub decimals: u8,
170 pub name: String,
172 pub symbol: String,
174 pub description: String,
176 pub icon_url: Option<String>,
178}
179
180impl CoinMetadata {
181 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 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}