1use crate::error::ExecutionErrorKind;
5use crate::error::{SuiError, SuiErrorKind};
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");
38
39#[derive(Debug, Serialize, Deserialize, Clone, JsonSchema, Eq, PartialEq)]
41pub struct Coin {
42 pub id: UID,
43 pub balance: Balance,
44}
45
46impl Coin {
47 pub fn new(id: ObjectID, value: u64) -> Self {
48 Self {
49 id: UID::new(id),
50 balance: Balance::new(value),
51 }
52 }
53
54 pub fn type_(type_param: TypeTag) -> StructTag {
55 StructTag {
56 address: SUI_FRAMEWORK_ADDRESS,
57 name: COIN_STRUCT_NAME.to_owned(),
58 module: COIN_MODULE_NAME.to_owned(),
59 type_params: vec![type_param],
60 }
61 }
62
63 pub fn is_coin(other: &StructTag) -> bool {
65 other.address == SUI_FRAMEWORK_ADDRESS
66 && other.module.as_ident_str() == COIN_MODULE_NAME
67 && other.name.as_ident_str() == COIN_STRUCT_NAME
68 }
69
70 pub fn is_coin_with_coin_type(other: &StructTag) -> Option<&StructTag> {
72 if Self::is_coin(other) && other.type_params.len() == 1 {
73 match other.type_params.first() {
74 Some(TypeTag::Struct(coin_type)) => Some(coin_type),
75 _ => None,
76 }
77 } else {
78 None
79 }
80 }
81
82 pub fn from_bcs_bytes(content: &[u8]) -> Result<Self, bcs::Error> {
84 bcs::from_bytes(content)
85 }
86
87 pub fn extract_balance_if_coin(object: &Object) -> Result<Option<(TypeTag, u64)>, bcs::Error> {
91 let Data::Move(obj) = &object.data else {
92 return Ok(None);
93 };
94
95 let Some(type_) = obj.type_().coin_type_maybe() else {
96 return Ok(None);
97 };
98
99 let coin = Self::from_bcs_bytes(obj.contents())?;
100 Ok(Some((type_, coin.value())))
101 }
102
103 pub fn id(&self) -> &ObjectID {
104 self.id.object_id()
105 }
106
107 pub fn value(&self) -> u64 {
108 self.balance.value()
109 }
110
111 pub fn to_bcs_bytes(&self) -> Vec<u8> {
112 bcs::to_bytes(&self).unwrap()
113 }
114
115 pub fn layout(type_param: TypeTag) -> MoveStructLayout {
116 MoveStructLayout {
117 type_: Self::type_(type_param.clone()),
118 fields: vec![
119 MoveFieldLayout::new(
120 ident_str!("id").to_owned(),
121 MoveTypeLayout::Struct(Box::new(UID::layout())),
122 ),
123 MoveFieldLayout::new(
124 ident_str!("balance").to_owned(),
125 MoveTypeLayout::Struct(Box::new(Balance::layout(type_param))),
126 ),
127 ],
128 }
129 }
130
131 pub fn add(&mut self, balance: Balance) -> Result<(), ExecutionError> {
133 let Some(new_value) = self.value().checked_add(balance.value()) else {
134 return Err(ExecutionError::from_kind(
135 ExecutionErrorKind::CoinBalanceOverflow,
136 ));
137 };
138 self.balance = Balance::new(new_value);
139 Ok(())
140 }
141
142 pub fn split(&mut self, amount: u64, new_coin_id: ObjectID) -> Result<Coin, ExecutionError> {
146 self.balance.withdraw(amount)?;
147 Ok(Coin::new(new_coin_id, amount))
148 }
149}
150
151#[derive(Debug, Serialize, Deserialize, Clone, Eq, PartialEq, JsonSchema)]
153pub struct TreasuryCap {
154 pub id: UID,
155 pub total_supply: Supply,
156}
157
158impl TreasuryCap {
159 pub fn is_treasury_type(other: &StructTag) -> bool {
160 other.address == SUI_FRAMEWORK_ADDRESS
161 && other.module.as_ident_str() == COIN_MODULE_NAME
162 && other.name.as_ident_str() == COIN_TREASURE_CAP_NAME
163 }
164
165 pub fn from_bcs_bytes(content: &[u8]) -> Result<Self, SuiError> {
167 bcs::from_bytes(content).map_err(|err| {
168 SuiErrorKind::ObjectDeserializationError {
169 error: format!("Unable to deserialize TreasuryCap object: {}", err),
170 }
171 .into()
172 })
173 }
174
175 pub fn type_(type_param: StructTag) -> StructTag {
176 StructTag {
177 address: SUI_FRAMEWORK_ADDRESS,
178 name: COIN_TREASURE_CAP_NAME.to_owned(),
179 module: COIN_MODULE_NAME.to_owned(),
180 type_params: vec![TypeTag::Struct(Box::new(type_param))],
181 }
182 }
183
184 pub fn is_treasury_with_coin_type(other: &StructTag) -> Option<&StructTag> {
186 if Self::is_treasury_type(other) && other.type_params.len() == 1 {
187 match other.type_params.first() {
188 Some(TypeTag::Struct(coin_type)) => Some(coin_type),
189 _ => None,
190 }
191 } else {
192 None
193 }
194 }
195}
196
197impl TryFrom<Object> for TreasuryCap {
198 type Error = SuiError;
199 fn try_from(object: Object) -> Result<Self, Self::Error> {
200 match &object.data {
201 Data::Move(o) => {
202 if o.type_().is_treasury_cap() {
203 return TreasuryCap::from_bcs_bytes(o.contents());
204 }
205 }
206 Data::Package(_) => {}
207 }
208
209 Err(SuiErrorKind::TypeError {
210 error: format!("Object type is not a TreasuryCap: {:?}", object),
211 }
212 .into())
213 }
214}
215
216#[derive(Debug, Serialize, Deserialize, Clone, JsonSchema, Eq, PartialEq)]
218pub struct CoinMetadata {
219 pub id: UID,
220 pub decimals: u8,
222 pub name: String,
224 pub symbol: String,
226 pub description: String,
228 pub icon_url: Option<String>,
230}
231
232impl CoinMetadata {
233 pub fn is_coin_metadata(other: &StructTag) -> bool {
235 other.address == SUI_FRAMEWORK_ADDRESS
236 && other.module.as_ident_str() == COIN_MODULE_NAME
237 && other.name.as_ident_str() == COIN_METADATA_STRUCT_NAME
238 }
239
240 pub fn from_bcs_bytes(content: &[u8]) -> Result<Self, SuiError> {
242 bcs::from_bytes(content).map_err(|err| {
243 SuiErrorKind::ObjectDeserializationError {
244 error: format!("Unable to deserialize CoinMetadata object: {}", err),
245 }
246 .into()
247 })
248 }
249
250 pub fn type_(type_param: StructTag) -> StructTag {
251 StructTag {
252 address: SUI_FRAMEWORK_ADDRESS,
253 name: COIN_METADATA_STRUCT_NAME.to_owned(),
254 module: COIN_MODULE_NAME.to_owned(),
255 type_params: vec![TypeTag::Struct(Box::new(type_param))],
256 }
257 }
258
259 pub fn is_coin_metadata_with_coin_type(other: &StructTag) -> Option<&StructTag> {
261 if Self::is_coin_metadata(other) && other.type_params.len() == 1 {
262 match other.type_params.first() {
263 Some(TypeTag::Struct(coin_type)) => Some(coin_type),
264 _ => None,
265 }
266 } else {
267 None
268 }
269 }
270}
271
272impl TryFrom<Object> for CoinMetadata {
273 type Error = SuiError;
274 fn try_from(object: Object) -> Result<Self, Self::Error> {
275 TryFrom::try_from(&object)
276 }
277}
278
279impl TryFrom<&Object> for CoinMetadata {
280 type Error = SuiError;
281 fn try_from(object: &Object) -> Result<Self, Self::Error> {
282 match &object.data {
283 Data::Move(o) => {
284 if o.type_().is_coin_metadata() {
285 return CoinMetadata::from_bcs_bytes(o.contents());
286 }
287 }
288 Data::Package(_) => {}
289 }
290
291 Err(SuiErrorKind::TypeError {
292 error: format!("Object type is not a CoinMetadata: {:?}", object),
293 }
294 .into())
295 }
296}
297
298#[derive(Debug, Serialize, Deserialize, Clone, Eq, PartialEq)]
300pub struct RegulatedCoinMetadata {
301 pub id: UID,
302 pub coin_metadata_object: ID,
304 pub deny_cap_object: ID,
306}
307
308impl RegulatedCoinMetadata {
309 pub fn is_regulated_coin_metadata(other: &StructTag) -> bool {
311 other.address == SUI_FRAMEWORK_ADDRESS
312 && other.module.as_ident_str() == COIN_MODULE_NAME
313 && other.name.as_ident_str() == REGULATED_COIN_METADATA_STRUCT_NAME
314 }
315
316 pub fn from_bcs_bytes(content: &[u8]) -> Result<Self, SuiError> {
318 bcs::from_bytes(content).map_err(|err| {
319 SuiErrorKind::ObjectDeserializationError {
320 error: format!(
321 "Unable to deserialize RegulatedCoinMetadata object: {}",
322 err
323 ),
324 }
325 .into()
326 })
327 }
328
329 pub fn type_(type_param: StructTag) -> StructTag {
330 StructTag {
331 address: SUI_FRAMEWORK_ADDRESS,
332 module: COIN_MODULE_NAME.to_owned(),
333 name: REGULATED_COIN_METADATA_STRUCT_NAME.to_owned(),
334 type_params: vec![TypeTag::Struct(Box::new(type_param))],
335 }
336 }
337
338 pub fn is_regulated_coin_metadata_with_coin_type(other: &StructTag) -> Option<&StructTag> {
340 if Self::is_regulated_coin_metadata(other) && other.type_params.len() == 1 {
341 match other.type_params.first() {
342 Some(TypeTag::Struct(coin_type)) => Some(coin_type),
343 _ => None,
344 }
345 } else {
346 None
347 }
348 }
349}
350
351impl TryFrom<Object> for RegulatedCoinMetadata {
352 type Error = SuiError;
353 fn try_from(object: Object) -> Result<Self, Self::Error> {
354 TryFrom::try_from(&object)
355 }
356}
357
358impl TryFrom<&Object> for RegulatedCoinMetadata {
359 type Error = SuiError;
360 fn try_from(object: &Object) -> Result<Self, Self::Error> {
361 match &object.data {
362 Data::Move(o) => {
363 if o.type_().is_regulated_coin_metadata() {
364 return Self::from_bcs_bytes(o.contents());
365 }
366 }
367 Data::Package(_) => {}
368 }
369
370 Err(SuiErrorKind::TypeError {
371 error: format!("Object type is not a RegulatedCoinMetadata: {:?}", object),
372 }
373 .into())
374 }
375}