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