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: UID, value: u64) -> Self {
42 Self {
43 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 match &object.data {
75 Data::Move(move_obj) => {
76 if !move_obj.is_coin() {
77 return Ok(None);
78 }
79
80 let coin = Self::from_bcs_bytes(move_obj.contents())?;
81 Ok(Some(coin.value()))
82 }
83 _ => Ok(None), }
85 }
86
87 pub fn id(&self) -> &ObjectID {
88 self.id.object_id()
89 }
90
91 pub fn value(&self) -> u64 {
92 self.balance.value()
93 }
94
95 pub fn to_bcs_bytes(&self) -> Vec<u8> {
96 bcs::to_bytes(&self).unwrap()
97 }
98
99 pub fn layout(type_param: TypeTag) -> MoveStructLayout {
100 MoveStructLayout {
101 type_: Self::type_(type_param.clone()),
102 fields: vec![
103 MoveFieldLayout::new(
104 ident_str!("id").to_owned(),
105 MoveTypeLayout::Struct(Box::new(UID::layout())),
106 ),
107 MoveFieldLayout::new(
108 ident_str!("balance").to_owned(),
109 MoveTypeLayout::Struct(Box::new(Balance::layout(type_param))),
110 ),
111 ],
112 }
113 }
114
115 pub fn add(&mut self, balance: Balance) -> Result<(), ExecutionError> {
118 let Some(new_value) = self.value().checked_add(balance.value()) else {
119 return Err(ExecutionError::from_kind(
120 ExecutionErrorKind::CoinBalanceOverflow,
121 ));
122 };
123 self.balance = Balance::new(new_value);
124 Ok(())
125 }
126
127 pub fn split(&mut self, amount: u64, new_coin_id: UID) -> Result<Coin, ExecutionError> {
132 self.balance.withdraw(amount)?;
133 Ok(Coin::new(new_coin_id, amount))
134 }
135}
136
137#[derive(Debug, Serialize, Deserialize, Clone, Eq, PartialEq, JsonSchema)]
139pub struct TreasuryCap {
140 pub id: UID,
141 pub total_supply: Supply,
142}
143
144impl TreasuryCap {
145 pub fn is_treasury_type(other: &StructTag) -> bool {
146 other.address == IOTA_FRAMEWORK_ADDRESS
147 && other.module.as_ident_str() == COIN_MODULE_NAME
148 && other.name.as_ident_str() == COIN_TREASURE_CAP_NAME
149 }
150
151 pub fn from_bcs_bytes(content: &[u8]) -> Result<Self, IotaError> {
153 bcs::from_bytes(content).map_err(|err| IotaError::ObjectDeserialization {
154 error: format!("Unable to deserialize TreasuryCap object: {}", err),
155 })
156 }
157
158 pub fn type_(type_param: StructTag) -> StructTag {
159 StructTag {
160 address: IOTA_FRAMEWORK_ADDRESS,
161 name: COIN_TREASURE_CAP_NAME.to_owned(),
162 module: COIN_MODULE_NAME.to_owned(),
163 type_params: vec![TypeTag::Struct(Box::new(type_param))],
164 }
165 }
166
167 pub fn is_treasury_with_coin_type(other: &StructTag) -> Option<&StructTag> {
170 if Self::is_treasury_type(other) && other.type_params.len() == 1 {
171 match other.type_params.first() {
172 Some(TypeTag::Struct(coin_type)) => Some(coin_type),
173 _ => None,
174 }
175 } else {
176 None
177 }
178 }
179}
180
181impl TryFrom<Object> for TreasuryCap {
182 type Error = IotaError;
183 fn try_from(object: Object) -> Result<Self, Self::Error> {
184 match &object.data {
185 Data::Move(o) => {
186 if o.type_().is_treasury_cap() {
187 return TreasuryCap::from_bcs_bytes(o.contents());
188 }
189 }
190 Data::Package(_) => {}
191 }
192
193 Err(IotaError::Type {
194 error: format!("Object type is not a TreasuryCap: {:?}", object),
195 })
196 }
197}
198
199#[derive(Debug, Serialize, Deserialize, Clone, JsonSchema, Eq, PartialEq)]
201pub struct CoinMetadata {
202 pub id: UID,
203 pub decimals: u8,
205 pub name: String,
207 pub symbol: String,
209 pub description: String,
211 pub icon_url: Option<String>,
213}
214
215impl CoinMetadata {
216 pub fn is_coin_metadata(other: &StructTag) -> bool {
218 other.address == IOTA_FRAMEWORK_ADDRESS
219 && other.module.as_ident_str() == COIN_MODULE_NAME
220 && other.name.as_ident_str() == COIN_METADATA_STRUCT_NAME
221 }
222
223 pub fn from_bcs_bytes(content: &[u8]) -> Result<Self, IotaError> {
225 bcs::from_bytes(content).map_err(|err| IotaError::ObjectDeserialization {
226 error: format!("Unable to deserialize CoinMetadata object: {}", err),
227 })
228 }
229
230 pub fn type_(type_param: StructTag) -> StructTag {
231 StructTag {
232 address: IOTA_FRAMEWORK_ADDRESS,
233 name: COIN_METADATA_STRUCT_NAME.to_owned(),
234 module: COIN_MODULE_NAME.to_owned(),
235 type_params: vec![TypeTag::Struct(Box::new(type_param))],
236 }
237 }
238
239 pub fn is_coin_metadata_with_coin_type(other: &StructTag) -> Option<&StructTag> {
242 if Self::is_coin_metadata(other) && other.type_params.len() == 1 {
243 match other.type_params.first() {
244 Some(TypeTag::Struct(coin_type)) => Some(coin_type),
245 _ => None,
246 }
247 } else {
248 None
249 }
250 }
251}
252
253impl TryFrom<Object> for CoinMetadata {
254 type Error = IotaError;
255 fn try_from(object: Object) -> Result<Self, Self::Error> {
256 TryFrom::try_from(&object)
257 }
258}
259
260impl TryFrom<&Object> for CoinMetadata {
261 type Error = IotaError;
262 fn try_from(object: &Object) -> Result<Self, Self::Error> {
263 match &object.data {
264 Data::Move(o) => {
265 if o.type_().is_coin_metadata() {
266 return CoinMetadata::from_bcs_bytes(o.contents());
267 }
268 }
269 Data::Package(_) => {}
270 }
271
272 Err(IotaError::Type {
273 error: format!("Object type is not a CoinMetadata: {:?}", object),
274 })
275 }
276}