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