1use crate::error::{SuiError, SuiErrorKind};
5use crate::execution_status::ExecutionErrorKind;
6use crate::{
7 SUI_FRAMEWORK_ADDRESS,
8 base_types::ObjectID,
9 id::{ID, UID},
10};
11use crate::{
12 balance::{Balance, Supply},
13 error::ExecutionError,
14 object::{Data, Object},
15};
16use move_core_types::{
17 account_address::AccountAddress,
18 annotated_value::{MoveFieldLayout, MoveStructLayout, MoveTypeLayout},
19 ident_str,
20 identifier::IdentStr,
21 language_storage::{StructTag, TypeTag},
22};
23use schemars::JsonSchema;
24use serde::{Deserialize, Serialize};
25
26pub const COIN_MODULE_NAME: &IdentStr = ident_str!("coin");
27pub const COIN_STRUCT_NAME: &IdentStr = ident_str!("Coin");
28pub const RESOLVED_COIN_STRUCT: (&AccountAddress, &IdentStr, &IdentStr) =
29 (&SUI_FRAMEWORK_ADDRESS, COIN_MODULE_NAME, COIN_STRUCT_NAME);
30pub const COIN_METADATA_STRUCT_NAME: &IdentStr = ident_str!("CoinMetadata");
31pub const COIN_TREASURE_CAP_NAME: &IdentStr = ident_str!("TreasuryCap");
32pub const REGULATED_COIN_METADATA_STRUCT_NAME: &IdentStr = ident_str!("RegulatedCoinMetadata");
33
34pub const PAY_MODULE_NAME: &IdentStr = ident_str!("pay");
35pub const PAY_JOIN_FUNC_NAME: &IdentStr = ident_str!("join");
36pub const PAY_SPLIT_N_FUNC_NAME: &IdentStr = ident_str!("divide_and_keep");
37pub const PAY_SPLIT_VEC_FUNC_NAME: &IdentStr = ident_str!("split_vec");
38pub const REDEEM_FUNDS_FUNC_NAME: &IdentStr = ident_str!("redeem_funds");
39pub const SEND_FUNDS_FUNC_NAME: &IdentStr = ident_str!("send_funds");
40pub const INTO_BALANCE_FUNC_NAME: &IdentStr = ident_str!("into_balance");
41pub const PUT_FUNC_NAME: &IdentStr = ident_str!("put");
42
43#[derive(Debug, Serialize, Deserialize, Clone, JsonSchema, Eq, PartialEq)]
45pub struct Coin {
46 pub id: UID,
47 pub balance: Balance,
48}
49
50impl Coin {
51 pub fn new(id: ObjectID, value: u64) -> Self {
52 Self {
53 id: UID::new(id),
54 balance: Balance::new(value),
55 }
56 }
57
58 pub fn type_(type_param: TypeTag) -> StructTag {
59 StructTag {
60 address: SUI_FRAMEWORK_ADDRESS,
61 name: COIN_STRUCT_NAME.to_owned(),
62 module: COIN_MODULE_NAME.to_owned(),
63 type_params: vec![type_param],
64 }
65 }
66
67 pub fn is_coin(other: &StructTag) -> bool {
69 other.address == SUI_FRAMEWORK_ADDRESS
70 && other.module.as_ident_str() == COIN_MODULE_NAME
71 && other.name.as_ident_str() == COIN_STRUCT_NAME
72 }
73
74 pub fn is_coin_with_coin_type(other: &StructTag) -> Option<&StructTag> {
76 if Self::is_coin(other) && other.type_params.len() == 1 {
77 match other.type_params.first() {
78 Some(TypeTag::Struct(coin_type)) => Some(coin_type),
79 _ => None,
80 }
81 } else {
82 None
83 }
84 }
85
86 pub fn from_bcs_bytes(content: &[u8]) -> Result<Self, bcs::Error> {
88 bcs::from_bytes(content)
89 }
90
91 pub fn extract_balance_if_coin(object: &Object) -> Result<Option<(TypeTag, u64)>, bcs::Error> {
95 let Data::Move(obj) = &object.data else {
96 return Ok(None);
97 };
98
99 let Some(type_) = obj.type_().coin_type_maybe() else {
100 return Ok(None);
101 };
102
103 let coin = Self::from_bcs_bytes(obj.contents())?;
104 Ok(Some((type_, coin.value())))
105 }
106
107 pub fn id(&self) -> &ObjectID {
108 self.id.object_id()
109 }
110
111 pub fn value(&self) -> u64 {
112 self.balance.value()
113 }
114
115 pub fn to_bcs_bytes(&self) -> Vec<u8> {
116 bcs::to_bytes(&self).unwrap()
117 }
118
119 pub fn layout(type_param: TypeTag) -> MoveStructLayout {
120 MoveStructLayout {
121 type_: Self::type_(type_param.clone()),
122 fields: vec![
123 MoveFieldLayout::new(
124 ident_str!("id").to_owned(),
125 MoveTypeLayout::Struct(Box::new(UID::layout())),
126 ),
127 MoveFieldLayout::new(
128 ident_str!("balance").to_owned(),
129 MoveTypeLayout::Struct(Box::new(Balance::layout(type_param))),
130 ),
131 ],
132 }
133 }
134
135 pub fn add(&mut self, balance: Balance) -> Result<(), ExecutionError> {
137 let Some(new_value) = self.value().checked_add(balance.value()) else {
138 return Err(ExecutionError::from_kind(
139 ExecutionErrorKind::CoinBalanceOverflow,
140 ));
141 };
142 self.balance = Balance::new(new_value);
143 Ok(())
144 }
145
146 pub fn split(&mut self, amount: u64, new_coin_id: ObjectID) -> Result<Coin, ExecutionError> {
150 self.balance.withdraw(amount)?;
151 Ok(Coin::new(new_coin_id, amount))
152 }
153}
154
155#[derive(Debug, Serialize, Deserialize, Clone, Eq, PartialEq, JsonSchema)]
157pub struct TreasuryCap {
158 pub id: UID,
159 pub total_supply: Supply,
160}
161
162impl TreasuryCap {
163 pub fn is_treasury_type(other: &StructTag) -> bool {
164 other.address == SUI_FRAMEWORK_ADDRESS
165 && other.module.as_ident_str() == COIN_MODULE_NAME
166 && other.name.as_ident_str() == COIN_TREASURE_CAP_NAME
167 }
168
169 pub fn from_bcs_bytes(content: &[u8]) -> Result<Self, SuiError> {
171 bcs::from_bytes(content).map_err(|err| {
172 SuiErrorKind::ObjectDeserializationError {
173 error: format!("Unable to deserialize TreasuryCap object: {}", err),
174 }
175 .into()
176 })
177 }
178
179 pub fn type_(type_param: StructTag) -> StructTag {
180 StructTag {
181 address: SUI_FRAMEWORK_ADDRESS,
182 name: COIN_TREASURE_CAP_NAME.to_owned(),
183 module: COIN_MODULE_NAME.to_owned(),
184 type_params: vec![TypeTag::Struct(Box::new(type_param))],
185 }
186 }
187
188 pub fn is_treasury_with_coin_type(other: &StructTag) -> Option<&StructTag> {
190 if Self::is_treasury_type(other) && other.type_params.len() == 1 {
191 match other.type_params.first() {
192 Some(TypeTag::Struct(coin_type)) => Some(coin_type),
193 _ => None,
194 }
195 } else {
196 None
197 }
198 }
199}
200
201impl TryFrom<Object> for TreasuryCap {
202 type Error = SuiError;
203 fn try_from(object: Object) -> Result<Self, Self::Error> {
204 match &object.data {
205 Data::Move(o) => {
206 if o.type_().is_treasury_cap() {
207 return TreasuryCap::from_bcs_bytes(o.contents());
208 }
209 }
210 Data::Package(_) => {}
211 }
212
213 Err(SuiErrorKind::TypeError {
214 error: format!("Object type is not a TreasuryCap: {:?}", object),
215 }
216 .into())
217 }
218}
219
220#[derive(Debug, Serialize, Deserialize, Clone, JsonSchema, Eq, PartialEq)]
222pub struct CoinMetadata {
223 pub id: UID,
224 pub decimals: u8,
226 pub name: String,
228 pub symbol: String,
230 pub description: String,
232 pub icon_url: Option<String>,
234}
235
236impl CoinMetadata {
237 pub fn is_coin_metadata(other: &StructTag) -> bool {
239 other.address == SUI_FRAMEWORK_ADDRESS
240 && other.module.as_ident_str() == COIN_MODULE_NAME
241 && other.name.as_ident_str() == COIN_METADATA_STRUCT_NAME
242 }
243
244 pub fn from_bcs_bytes(content: &[u8]) -> Result<Self, SuiError> {
246 bcs::from_bytes(content).map_err(|err| {
247 SuiErrorKind::ObjectDeserializationError {
248 error: format!("Unable to deserialize CoinMetadata object: {}", err),
249 }
250 .into()
251 })
252 }
253
254 pub fn type_(type_param: StructTag) -> StructTag {
255 StructTag {
256 address: SUI_FRAMEWORK_ADDRESS,
257 name: COIN_METADATA_STRUCT_NAME.to_owned(),
258 module: COIN_MODULE_NAME.to_owned(),
259 type_params: vec![TypeTag::Struct(Box::new(type_param))],
260 }
261 }
262
263 pub fn is_coin_metadata_with_coin_type(other: &StructTag) -> Option<&StructTag> {
265 if Self::is_coin_metadata(other) && other.type_params.len() == 1 {
266 match other.type_params.first() {
267 Some(TypeTag::Struct(coin_type)) => Some(coin_type),
268 _ => None,
269 }
270 } else {
271 None
272 }
273 }
274}
275
276impl TryFrom<Object> for CoinMetadata {
277 type Error = SuiError;
278 fn try_from(object: Object) -> Result<Self, Self::Error> {
279 TryFrom::try_from(&object)
280 }
281}
282
283impl TryFrom<&Object> for CoinMetadata {
284 type Error = SuiError;
285 fn try_from(object: &Object) -> Result<Self, Self::Error> {
286 match &object.data {
287 Data::Move(o) => {
288 if o.type_().is_coin_metadata() {
289 return CoinMetadata::from_bcs_bytes(o.contents());
290 }
291 }
292 Data::Package(_) => {}
293 }
294
295 Err(SuiErrorKind::TypeError {
296 error: format!("Object type is not a CoinMetadata: {:?}", object),
297 }
298 .into())
299 }
300}
301
302#[derive(Debug, Serialize, Deserialize, Clone, Eq, PartialEq)]
304pub struct RegulatedCoinMetadata {
305 pub id: UID,
306 pub coin_metadata_object: ID,
308 pub deny_cap_object: ID,
310}
311
312impl RegulatedCoinMetadata {
313 pub fn is_regulated_coin_metadata(other: &StructTag) -> bool {
315 other.address == SUI_FRAMEWORK_ADDRESS
316 && other.module.as_ident_str() == COIN_MODULE_NAME
317 && other.name.as_ident_str() == REGULATED_COIN_METADATA_STRUCT_NAME
318 }
319
320 pub fn from_bcs_bytes(content: &[u8]) -> Result<Self, SuiError> {
322 bcs::from_bytes(content).map_err(|err| {
323 SuiErrorKind::ObjectDeserializationError {
324 error: format!(
325 "Unable to deserialize RegulatedCoinMetadata object: {}",
326 err
327 ),
328 }
329 .into()
330 })
331 }
332
333 pub fn type_(type_param: StructTag) -> StructTag {
334 StructTag {
335 address: SUI_FRAMEWORK_ADDRESS,
336 module: COIN_MODULE_NAME.to_owned(),
337 name: REGULATED_COIN_METADATA_STRUCT_NAME.to_owned(),
338 type_params: vec![TypeTag::Struct(Box::new(type_param))],
339 }
340 }
341
342 pub fn is_regulated_coin_metadata_with_coin_type(other: &StructTag) -> Option<&StructTag> {
344 if Self::is_regulated_coin_metadata(other) && other.type_params.len() == 1 {
345 match other.type_params.first() {
346 Some(TypeTag::Struct(coin_type)) => Some(coin_type),
347 _ => None,
348 }
349 } else {
350 None
351 }
352 }
353}
354
355impl TryFrom<Object> for RegulatedCoinMetadata {
356 type Error = SuiError;
357 fn try_from(object: Object) -> Result<Self, Self::Error> {
358 TryFrom::try_from(&object)
359 }
360}
361
362impl TryFrom<&Object> for RegulatedCoinMetadata {
363 type Error = SuiError;
364 fn try_from(object: &Object) -> Result<Self, Self::Error> {
365 match &object.data {
366 Data::Move(o) => {
367 if o.type_().is_regulated_coin_metadata() {
368 return Self::from_bcs_bytes(o.contents());
369 }
370 }
371 Data::Package(_) => {}
372 }
373
374 Err(SuiErrorKind::TypeError {
375 error: format!("Object type is not a RegulatedCoinMetadata: {:?}", object),
376 }
377 .into())
378 }
379}