1use move_core_types::{
6 annotated_value::{MoveFieldLayout, MoveStructLayout, MoveTypeLayout},
7 ident_str,
8 identifier::IdentStr,
9 language_storage::{StructTag, TypeTag},
10};
11use schemars::JsonSchema;
12use serde::{Deserialize, Serialize};
13
14use crate::{
15 IOTA_FRAMEWORK_ADDRESS,
16 balance::{Balance, Supply},
17 base_types::ObjectID,
18 error::{ExecutionError, ExecutionErrorKind, IotaError},
19 id::UID,
20 object::{Data, Object},
21};
22
23pub const COIN_MODULE_NAME: &IdentStr = ident_str!("coin");
24pub const COIN_STRUCT_NAME: &IdentStr = ident_str!("Coin");
25pub const COIN_METADATA_STRUCT_NAME: &IdentStr = ident_str!("CoinMetadata");
26pub const COIN_TREASURE_CAP_NAME: &IdentStr = ident_str!("TreasuryCap");
27pub const COIN_JOIN_FUNC_NAME: &IdentStr = ident_str!("join");
28
29pub const PAY_MODULE_NAME: &IdentStr = ident_str!("pay");
30pub const PAY_SPLIT_N_FUNC_NAME: &IdentStr = ident_str!("divide_and_keep");
31pub const PAY_SPLIT_VEC_FUNC_NAME: &IdentStr = ident_str!("split_vec");
32
33#[derive(Debug, Serialize, Deserialize, Clone, JsonSchema, 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 type_(type_param: TypeTag) -> StructTag {
49 StructTag {
50 address: IOTA_FRAMEWORK_ADDRESS,
51 name: COIN_STRUCT_NAME.to_owned(),
52 module: COIN_MODULE_NAME.to_owned(),
53 type_params: vec![type_param],
54 }
55 }
56
57 pub fn is_coin(other: &StructTag) -> bool {
59 other.address == IOTA_FRAMEWORK_ADDRESS
60 && other.module.as_ident_str() == COIN_MODULE_NAME
61 && other.name.as_ident_str() == COIN_STRUCT_NAME
62 }
63
64 pub fn from_bcs_bytes(content: &[u8]) -> Result<Self, bcs::Error> {
66 bcs::from_bytes(content)
67 }
68
69 pub fn extract_balance_if_coin(object: &Object) -> Result<Option<u64>, bcs::Error> {
74 let Data::Move(obj) = &object.data else {
75 return Ok(None);
76 };
77 let Some(_) = obj.type_().coin_type_maybe() else {
78 return Ok(None);
79 };
80
81 let coin = Self::from_bcs_bytes(obj.contents())?;
82 Ok(Some(coin.value()))
83 }
84
85 pub fn id(&self) -> &ObjectID {
86 self.id.object_id()
87 }
88
89 pub fn value(&self) -> u64 {
90 self.balance.value()
91 }
92
93 pub fn to_bcs_bytes(&self) -> Vec<u8> {
94 bcs::to_bytes(&self).unwrap()
95 }
96
97 pub fn layout(type_param: TypeTag) -> MoveStructLayout {
98 MoveStructLayout {
99 type_: Self::type_(type_param.clone()),
100 fields: vec![
101 MoveFieldLayout::new(
102 ident_str!("id").to_owned(),
103 MoveTypeLayout::Struct(Box::new(UID::layout())),
104 ),
105 MoveFieldLayout::new(
106 ident_str!("balance").to_owned(),
107 MoveTypeLayout::Struct(Box::new(Balance::layout(type_param))),
108 ),
109 ],
110 }
111 }
112
113 pub fn add(&mut self, balance: Balance) -> Result<(), ExecutionError> {
116 let Some(new_value) = self.value().checked_add(balance.value()) else {
117 return Err(ExecutionError::from_kind(
118 ExecutionErrorKind::CoinBalanceOverflow,
119 ));
120 };
121 self.balance = Balance::new(new_value);
122 Ok(())
123 }
124
125 pub fn split(&mut self, amount: u64, new_coin_id: ObjectID) -> Result<Coin, ExecutionError> {
130 self.balance.withdraw(amount)?;
131 Ok(Coin::new(new_coin_id, amount))
132 }
133}
134
135#[derive(Debug, Serialize, Deserialize, Clone, Eq, PartialEq, JsonSchema)]
137pub struct TreasuryCap {
138 pub id: UID,
139 pub total_supply: Supply,
140}
141
142impl TreasuryCap {
143 pub fn is_treasury_type(other: &StructTag) -> bool {
144 other.address == IOTA_FRAMEWORK_ADDRESS
145 && other.module.as_ident_str() == COIN_MODULE_NAME
146 && other.name.as_ident_str() == COIN_TREASURE_CAP_NAME
147 }
148
149 pub fn from_bcs_bytes(content: &[u8]) -> Result<Self, IotaError> {
151 bcs::from_bytes(content).map_err(|err| IotaError::ObjectDeserialization {
152 error: format!("Unable to deserialize TreasuryCap object: {err}"),
153 })
154 }
155
156 pub fn type_(type_param: StructTag) -> StructTag {
157 StructTag {
158 address: IOTA_FRAMEWORK_ADDRESS,
159 name: COIN_TREASURE_CAP_NAME.to_owned(),
160 module: COIN_MODULE_NAME.to_owned(),
161 type_params: vec![TypeTag::Struct(Box::new(type_param))],
162 }
163 }
164
165 pub fn is_treasury_with_coin_type(other: &StructTag) -> Option<&StructTag> {
168 if Self::is_treasury_type(other) && other.type_params.len() == 1 {
169 match other.type_params.first() {
170 Some(TypeTag::Struct(coin_type)) => Some(coin_type),
171 _ => None,
172 }
173 } else {
174 None
175 }
176 }
177}
178
179impl TryFrom<Object> for TreasuryCap {
180 type Error = IotaError;
181 fn try_from(object: Object) -> Result<Self, Self::Error> {
182 match &object.data {
183 Data::Move(o) => {
184 if o.type_().is_treasury_cap() {
185 return TreasuryCap::from_bcs_bytes(o.contents());
186 }
187 }
188 Data::Package(_) => {}
189 }
190
191 Err(IotaError::Type {
192 error: format!("Object type is not a TreasuryCap: {object:?}"),
193 })
194 }
195}
196
197#[derive(Debug, Serialize, Deserialize, Clone, JsonSchema, Eq, PartialEq)]
199pub struct CoinMetadata {
200 pub id: UID,
201 pub decimals: u8,
203 pub name: String,
205 pub symbol: String,
207 pub description: String,
209 pub icon_url: Option<String>,
211}
212
213impl CoinMetadata {
214 pub fn is_coin_metadata(other: &StructTag) -> bool {
216 other.address == IOTA_FRAMEWORK_ADDRESS
217 && other.module.as_ident_str() == COIN_MODULE_NAME
218 && other.name.as_ident_str() == COIN_METADATA_STRUCT_NAME
219 }
220
221 pub fn from_bcs_bytes(content: &[u8]) -> Result<Self, IotaError> {
223 bcs::from_bytes(content).map_err(|err| IotaError::ObjectDeserialization {
224 error: format!("Unable to deserialize CoinMetadata object: {err}"),
225 })
226 }
227
228 pub fn type_(type_param: StructTag) -> StructTag {
229 StructTag {
230 address: IOTA_FRAMEWORK_ADDRESS,
231 name: COIN_METADATA_STRUCT_NAME.to_owned(),
232 module: COIN_MODULE_NAME.to_owned(),
233 type_params: vec![TypeTag::Struct(Box::new(type_param))],
234 }
235 }
236
237 pub fn is_coin_metadata_with_coin_type(other: &StructTag) -> Option<&StructTag> {
240 if Self::is_coin_metadata(other) && other.type_params.len() == 1 {
241 match other.type_params.first() {
242 Some(TypeTag::Struct(coin_type)) => Some(coin_type),
243 _ => None,
244 }
245 } else {
246 None
247 }
248 }
249}
250
251impl TryFrom<Object> for CoinMetadata {
252 type Error = IotaError;
253 fn try_from(object: Object) -> Result<Self, Self::Error> {
254 TryFrom::try_from(&object)
255 }
256}
257
258impl TryFrom<&Object> for CoinMetadata {
259 type Error = IotaError;
260 fn try_from(object: &Object) -> Result<Self, Self::Error> {
261 match &object.data {
262 Data::Move(o) => {
263 if o.type_().is_coin_metadata() {
264 return CoinMetadata::from_bcs_bytes(o.contents());
265 }
266 }
267 Data::Package(_) => {}
268 }
269
270 Err(IotaError::Type {
271 error: format!("Object type is not a CoinMetadata: {object:?}"),
272 })
273 }
274}